Compare commits
6 Commits
ae9da65e1b
...
541e329dc4
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 541e329dc4 | |
Shadowfacts | 6feba31ff3 | |
Shadowfacts | 8dddb42fd8 | |
Shadowfacts | 7548cbe40c | |
Shadowfacts | db9564cbae | |
Shadowfacts | 6318c16afe |
|
@ -1,6 +1,9 @@
|
||||||
defmodule Clacks.Activity do
|
defmodule Clacks.Activity do
|
||||||
|
require Logger
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
alias Clacks.Repo
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
@type t() :: %__MODULE__{}
|
@type t() :: %__MODULE__{}
|
||||||
|
|
||||||
|
@ -10,7 +13,6 @@ defmodule Clacks.Activity do
|
||||||
field :data, :map
|
field :data, :map
|
||||||
field :local, :boolean
|
field :local, :boolean
|
||||||
field :actor, :string
|
field :actor, :string
|
||||||
field :recipients, {:array, :string}, default: []
|
|
||||||
|
|
||||||
has_one :object, Clacks.Object, on_delete: :nothing, foreign_key: :id
|
has_one :object, Clacks.Object, on_delete: :nothing, foreign_key: :id
|
||||||
|
|
||||||
|
@ -19,7 +21,52 @@ defmodule Clacks.Activity do
|
||||||
|
|
||||||
def changeset(%__MODULE__{} = schema, attrs) do
|
def changeset(%__MODULE__{} = schema, attrs) do
|
||||||
schema
|
schema
|
||||||
|> cast(attrs, [:data, :local, :actor, :recipients])
|
|> cast(attrs, [:data, :local, :actor])
|
||||||
|> validate_required([:data, :local, :actor, :recipients])
|
|> validate_required([:data, :local, :actor])
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec get_by_ap_id(ap_id :: String.t(), force_refetch :: boolean()) :: t() | nil
|
||||||
|
def get_by_ap_id(ap_id, force_refetch \\ false) do
|
||||||
|
if force_refetch do
|
||||||
|
fetch(ap_id)
|
||||||
|
else
|
||||||
|
get_cached_by_ap_id(ap_id) || fetch(ap_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec get_cached_by_ap_id(ap_id :: String.t()) :: t() | nil
|
||||||
|
def get_cached_by_ap_id(ap_id) do
|
||||||
|
Repo.one(from a in __MODULE__, where: fragment("?->>'id'", a.data) == ^ap_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec fetch(ap_id :: String.t()) :: t() | nil
|
||||||
|
def fetch(ap_id) do
|
||||||
|
case Clacks.ActivityPub.Fetcher.fetch_activity(ap_id) do
|
||||||
|
nil ->
|
||||||
|
nil
|
||||||
|
|
||||||
|
data ->
|
||||||
|
actor = data["actor"] || data["attributedTo"]
|
||||||
|
|
||||||
|
existing = get_cached_by_ap_id(data["id"])
|
||||||
|
|
||||||
|
changeset =
|
||||||
|
changeset(existing || %__MODULE__{}, %{
|
||||||
|
data: data,
|
||||||
|
local: false,
|
||||||
|
actor: actor
|
||||||
|
})
|
||||||
|
|
||||||
|
case Repo.insert_or_update(changeset) do
|
||||||
|
{:ok, activity} ->
|
||||||
|
_ = Clacks.Actor.get_by_ap_id(actor)
|
||||||
|
|
||||||
|
activity
|
||||||
|
|
||||||
|
{:error, changeset} ->
|
||||||
|
Logger.error("Couldn't store remote activity #{ap_id}: #{inspect(changeset)}")
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,32 @@ defmodule Clacks.ActivityPub do
|
||||||
@context ["https://www.w3.org/ns/activitystreams"]
|
@context ["https://www.w3.org/ns/activitystreams"]
|
||||||
@public "https://www.w3.org/ns/activitystreams#Public"
|
@public "https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
|
||||||
|
@spec actor(
|
||||||
|
id :: String.t(),
|
||||||
|
username :: String.t(),
|
||||||
|
display_name :: String.t(),
|
||||||
|
pem :: String.t()
|
||||||
|
) :: map()
|
||||||
|
def actor(id, username, display_name, pem) do
|
||||||
|
%{
|
||||||
|
"@context" => @context,
|
||||||
|
"type" => "Person",
|
||||||
|
"id" => id,
|
||||||
|
"url" => id,
|
||||||
|
"preferredUsername" => username,
|
||||||
|
"name" => display_name,
|
||||||
|
"followers" => Path.join(id, "followers"),
|
||||||
|
"following" => Path.join(id, "following"),
|
||||||
|
"inbox" => Path.join(id, "inbox"),
|
||||||
|
"outbox" => Path.join(id, "outbox"),
|
||||||
|
"publicKey" => %{
|
||||||
|
"id" => id <> "#main-key",
|
||||||
|
"owner" => id,
|
||||||
|
"publicKeyPem" => pem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
@spec note(
|
@spec note(
|
||||||
actor :: String.t(),
|
actor :: String.t(),
|
||||||
html :: String.t(),
|
html :: String.t(),
|
||||||
|
@ -57,6 +83,18 @@ defmodule Clacks.ActivityPub do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec synthesized_create(object :: map()) :: map()
|
||||||
|
def synthesized_create(object) do
|
||||||
|
%{
|
||||||
|
"@context" => @context,
|
||||||
|
"type" => "Create",
|
||||||
|
"object" => object,
|
||||||
|
"actor" => object["actor"] || object["attributedTo"],
|
||||||
|
"to" => object["to"],
|
||||||
|
"cc" => object["cc"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
@spec object_id(id :: String.t()) :: String.t()
|
@spec object_id(id :: String.t()) :: String.t()
|
||||||
def object_id(id) do
|
def object_id(id) do
|
||||||
url = Application.get_env(:clacks, ClacksWeb.Endpoint)[:url]
|
url = Application.get_env(:clacks, ClacksWeb.Endpoint)[:url]
|
||||||
|
|
|
@ -28,6 +28,11 @@ defmodule Clacks.ActivityPub.Fetcher do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec fetch_activity(id :: String.t()) :: map() | nil
|
||||||
|
def fetch_activity(id) do
|
||||||
|
fetch_object(id)
|
||||||
|
end
|
||||||
|
|
||||||
@spec fetch(uri :: String.t()) :: map() | nil
|
@spec fetch(uri :: String.t()) :: map() | nil
|
||||||
defp fetch(uri) do
|
defp fetch(uri) do
|
||||||
Logger.debug("Attempting to fetch AP object at #{uri}")
|
Logger.debug("Attempting to fetch AP object at #{uri}")
|
||||||
|
|
|
@ -14,6 +14,7 @@ defmodule Clacks.Actor do
|
||||||
field :nickname, :string
|
field :nickname, :string
|
||||||
field :local, :boolean
|
field :local, :boolean
|
||||||
field :data, :map
|
field :data, :map
|
||||||
|
field :followers, {:array, :string}, default: []
|
||||||
|
|
||||||
belongs_to :user, Clacks.User
|
belongs_to :user, Clacks.User
|
||||||
|
|
||||||
|
@ -22,12 +23,17 @@ defmodule Clacks.Actor do
|
||||||
|
|
||||||
def changeset(%__MODULE__{} = schema, attrs) do
|
def changeset(%__MODULE__{} = schema, attrs) do
|
||||||
schema
|
schema
|
||||||
|> cast(attrs, [:ap_id, :nickname, :local, :data])
|
|> cast(attrs, [:ap_id, :nickname, :local, :data, :followers])
|
||||||
|> validate_required([:ap_id, :nickname, :local, :data])
|
|> validate_required([:ap_id, :nickname, :local, :data])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec get_by_nickanme(nickname :: String.t()) :: t() | nil
|
||||||
|
def get_by_nickanme(nickname) do
|
||||||
|
Repo.one(from a in __MODULE__, where: a.nickname == ^nickname)
|
||||||
|
end
|
||||||
|
|
||||||
@spec get_by_ap_id(ap_id :: String.t(), force_refetch :: boolean()) :: t() | nil
|
@spec get_by_ap_id(ap_id :: String.t(), force_refetch :: boolean()) :: t() | nil
|
||||||
def get_by_ap_id(ap_id, force_refetch \\ false) when is_binary(ap_id) do
|
def get_by_ap_id(ap_id, force_refetch \\ false) do
|
||||||
if force_refetch do
|
if force_refetch do
|
||||||
fetch(ap_id)
|
fetch(ap_id)
|
||||||
else
|
else
|
||||||
|
@ -36,12 +42,12 @@ defmodule Clacks.Actor do
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec get_cached_by_ap_id(ap_id :: String.t()) :: t() | nil
|
@spec get_cached_by_ap_id(ap_id :: String.t()) :: t() | nil
|
||||||
def get_cached_by_ap_id(ap_id) when is_binary(ap_id) do
|
def get_cached_by_ap_id(ap_id) do
|
||||||
Repo.one(from a in __MODULE__, where: a.ap_id == ^ap_id)
|
Repo.one(from a in __MODULE__, where: a.ap_id == ^ap_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec fetch(ap_id :: String.t()) :: t() | nil
|
@spec fetch(ap_id :: String.t()) :: t() | nil
|
||||||
def fetch(ap_id) when is_binary(ap_id) do
|
def fetch(ap_id) do
|
||||||
case Clacks.ActivityPub.Fetcher.fetch_actor(ap_id) do
|
case Clacks.ActivityPub.Fetcher.fetch_actor(ap_id) do
|
||||||
nil ->
|
nil ->
|
||||||
nil
|
nil
|
||||||
|
|
|
@ -16,4 +16,10 @@ defmodule Clacks.Keys do
|
||||||
{:error, error}
|
{:error, error}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def public_key_pem({:RSAPublicKey, _, _} = key) do
|
||||||
|
entry = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, key)
|
||||||
|
pem = :public_key.pem_encode([entry])
|
||||||
|
{:ok, pem}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
defmodule Clacks.Object do
|
defmodule Clacks.Object do
|
||||||
|
require Logger
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
alias Clacks.Repo
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
@type t() :: %__MODULE__{}
|
@type t() :: %__MODULE__{}
|
||||||
|
|
||||||
|
@ -15,4 +18,63 @@ defmodule Clacks.Object do
|
||||||
|> cast(attrs, [:data])
|
|> cast(attrs, [:data])
|
||||||
|> validate_required([:data])
|
|> validate_required([:data])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec get_by_ap_id(
|
||||||
|
ap_id :: String.t(),
|
||||||
|
force_refetch :: boolean(),
|
||||||
|
synthesize_create :: boolean()
|
||||||
|
) :: t() | nil
|
||||||
|
def get_by_ap_id(ap_id, force_refetch \\ false, synthesize_create \\ true) do
|
||||||
|
if force_refetch do
|
||||||
|
fetch(ap_id, synthesize_create)
|
||||||
|
else
|
||||||
|
get_cached_by_ap_id(ap_id) || fetch(ap_id, synthesize_create)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec get_cached_by_ap_id(ap_id :: String.t()) :: t() | nil
|
||||||
|
def get_cached_by_ap_id(ap_id) do
|
||||||
|
Repo.one(from o in __MODULE__, where: fragment("?->>'id'", o.data) == ^ap_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec fetch(ap_id :: String.t(), synthesize_create :: boolean()) :: t() | nil
|
||||||
|
def fetch(ap_id, synthesize_create \\ true) do
|
||||||
|
case Clacks.ActivityPub.Fetcher.fetch_object(ap_id) do
|
||||||
|
nil ->
|
||||||
|
nil
|
||||||
|
|
||||||
|
data ->
|
||||||
|
existing = get_cached_by_ap_id(data["id"])
|
||||||
|
|
||||||
|
changeset =
|
||||||
|
changeset(existing || %__MODULE__{}, %{
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
|
||||||
|
case Repo.insert_or_update(changeset) do
|
||||||
|
{:ok, object} ->
|
||||||
|
actor = data["actor"] || data["attributedTo"]
|
||||||
|
_ = Clacks.Actor.get_by_ap_id(actor)
|
||||||
|
|
||||||
|
if synthesize_create do
|
||||||
|
create = Clacks.ActivityPub.synthesized_create(data)
|
||||||
|
|
||||||
|
changeset =
|
||||||
|
Clacks.Activity.changeset(%Clacks.Activity{}, %{
|
||||||
|
data: create,
|
||||||
|
local: false,
|
||||||
|
actor: actor
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, _create} = Repo.insert_or_update(changeset)
|
||||||
|
end
|
||||||
|
|
||||||
|
object
|
||||||
|
|
||||||
|
{:error, changeset} ->
|
||||||
|
Logger.error("Couldn't store remote object #{ap_id}: #{inspect(changeset)}")
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
defmodule ClacksWeb.ActorController do
|
||||||
|
use ClacksWeb, :controller
|
||||||
|
alias Clacks.{Repo, Actor}
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
def get(conn, %{"nickname" => nickname}) do
|
||||||
|
case Actor.get_by_nickanme(nickname) do
|
||||||
|
%Actor{local: true, data: data} ->
|
||||||
|
conn
|
||||||
|
|> put_resp_header("content-type", "application/activity+json")
|
||||||
|
|> json(data)
|
||||||
|
|
||||||
|
%Actor{local: false, ap_id: ap_id} ->
|
||||||
|
conn
|
||||||
|
|> redirect(external: ap_id)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
conn
|
||||||
|
|> put_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -15,14 +15,13 @@ defmodule ClacksWeb.Router do
|
||||||
|
|
||||||
scope "/", ClacksWeb do
|
scope "/", ClacksWeb do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
get "/", PageController, :index
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", ClacksWeb do
|
scope "/", ClacksWeb do
|
||||||
pipe_through :activitypub
|
pipe_through :activitypub
|
||||||
|
|
||||||
get "/objects/:id", ObjectsController, :get
|
get "/objects/:id", ObjectsController, :get
|
||||||
|
get "/users/:nickname", ActorController, :get
|
||||||
end
|
end
|
||||||
|
|
||||||
# Other scopes may use custom stacks.
|
# Other scopes may use custom stacks.
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
defmodule Mix.Tasks.Clacks.User do
|
||||||
|
use Mix.Task
|
||||||
|
alias Clacks.{Repo, User, Actor, Keys, ActivityPub}
|
||||||
|
|
||||||
|
@shortdoc "Creates a new user"
|
||||||
|
def run(["create"]) do
|
||||||
|
username = IO.gets("Username: ") |> String.trim()
|
||||||
|
# password = IO.gets("Password: ") |> String.trim()
|
||||||
|
|
||||||
|
{:ok, pem} = Keys.generate_rsa_pem()
|
||||||
|
{:ok, _private, public} = Keys.keys_from_pem(pem)
|
||||||
|
{:ok, public_key_pem} = Keys.public_key_pem(public)
|
||||||
|
|
||||||
|
changeset = User.changeset(%User{}, %{username: username, private_key: pem})
|
||||||
|
|
||||||
|
# start the app so the DB connection is established
|
||||||
|
Mix.Task.run("app.start")
|
||||||
|
|
||||||
|
{:ok, user} = Repo.insert(changeset)
|
||||||
|
|
||||||
|
url = Application.get_env(:clacks, ClacksWeb.Endpoint)[:url]
|
||||||
|
|
||||||
|
uri =
|
||||||
|
URI.to_string(%URI{
|
||||||
|
scheme: url[:scheme],
|
||||||
|
host: url[:host],
|
||||||
|
port: url[:port],
|
||||||
|
path: Path.join("/users", username)
|
||||||
|
})
|
||||||
|
|
||||||
|
actor =
|
||||||
|
Ecto.build_assoc(user, :actor, %{
|
||||||
|
ap_id: uri,
|
||||||
|
nickname: username,
|
||||||
|
local: true,
|
||||||
|
data: ActivityPub.actor(uri, username, username, public_key_pem)
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, actor} = Repo.insert(actor)
|
||||||
|
|
||||||
|
IO.puts("User #{username} successfully created")
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,7 +8,6 @@ defmodule Clacks.Repo.Migrations.CreateActivities do
|
||||||
add :data, :jsonb, null: false
|
add :data, :jsonb, null: false
|
||||||
add :local, :boolean
|
add :local, :boolean
|
||||||
add :actor, :string
|
add :actor, :string
|
||||||
add :recipients, {:array, :string}
|
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule Clacks.Repo.Migrations.ActorsAddFollowers do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:actors) do
|
||||||
|
add :followers, {:array, :string}, default: []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue