From ae9da65e1b9c5f614b10ca090f94d1951741f880 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 29 Sep 2019 18:30:59 -0400 Subject: [PATCH] Add actors/fetching --- .gitignore | 2 + config/dev.exs | 2 + lib/clacks/activitypub/fetcher.ex | 52 ++++++++++++++ lib/clacks/actor.ex | 71 +++++++++++++++++++ lib/clacks/keys.ex | 19 +++++ lib/clacks/user.ex | 21 ++++++ mix.exs | 6 +- mix.lock | 10 +++ .../20190929031943_create_actors.exs | 16 +++++ .../20190929182812_create_users.exs | 12 ++++ .../20190929183129_actors_add_user_id.exs | 9 +++ ...29213050_actors_add_ap_id_unique_index.exs | 7 ++ 12 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 lib/clacks/activitypub/fetcher.ex create mode 100644 lib/clacks/actor.ex create mode 100644 lib/clacks/keys.ex create mode 100644 lib/clacks/user.ex create mode 100644 priv/repo/migrations/20190929031943_create_actors.exs create mode 100644 priv/repo/migrations/20190929182812_create_users.exs create mode 100644 priv/repo/migrations/20190929183129_actors_add_user_id.exs create mode 100644 priv/repo/migrations/20190929213050_actors_add_ap_id_unique_index.exs diff --git a/.gitignore b/.gitignore index 179979c..f59482f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ npm-debug.log # we ignore priv/static. You may want to comment # this depending on your deployment strategy. /priv/static/ + +config/dev.secret.exs diff --git a/config/dev.exs b/config/dev.exs index 001ac20..6c1fd22 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -75,3 +75,5 @@ config :phoenix, :stacktrace_depth, 20 # Initialize plugs at runtime for faster development compilation config :phoenix, :plug_init_mode, :runtime + +import_config "dev.secret.exs" diff --git a/lib/clacks/activitypub/fetcher.ex b/lib/clacks/activitypub/fetcher.ex new file mode 100644 index 0000000..af61a29 --- /dev/null +++ b/lib/clacks/activitypub/fetcher.ex @@ -0,0 +1,52 @@ +defmodule Clacks.ActivityPub.Fetcher do + require Logger + + @spec fetch_actor(id :: String.t()) :: map() | nil + def fetch_actor(id) do + %{host: id_host} = URI.parse(id) + + with %{"type" => type, "id" => remote_id} = actor <- fetch(id), + "person" <- String.downcase(type), + %{host: actor_host} when actor_host == id_host <- URI.parse(remote_id) do + actor + else + _ -> + nil + end + end + + @spec fetch_object(id :: String.t()) :: map() | nil + def fetch_object(id) do + %{host: id_host} = URI.parse(id) + + with %{"actor" => remote_actor} = object <- fetch(id), + %{host: actor_host} when actor_host == id_host <- URI.parse(remote_actor) do + object + else + _ -> + nil + end + end + + @spec fetch(uri :: String.t()) :: map() | nil + defp fetch(uri) do + Logger.debug("Attempting to fetch AP object at #{uri}") + + headers = [Accept: "application/activity+json, application/ld+json"] + opts = [hackney: Application.get_env(:clacks, :hackney_opts, [])] + + with {:ok, %HTTPoison.Response{status_code: status_code, body: body}} + when status_code in 200..299 <- + HTTPoison.get(uri, headers, opts), + {:ok, data} <- Jason.decode(body) do + data + else + {:ok, %HTTPoison.Response{}} -> + nil + + {:error, reason} -> + Logger.warn("Couldn't fetch AP object at #{uri}: #{inspect(reason)}") + nil + end + end +end diff --git a/lib/clacks/actor.ex b/lib/clacks/actor.ex new file mode 100644 index 0000000..99bf1d5 --- /dev/null +++ b/lib/clacks/actor.ex @@ -0,0 +1,71 @@ +defmodule Clacks.Actor do + require Logger + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query + alias Clacks.Repo + + @type t() :: %__MODULE__{} + + @primary_key {:id, FlakeId.Ecto.Type, autogenerate: true} + + schema "actors" do + field :ap_id, :string + field :nickname, :string + field :local, :boolean + field :data, :map + + belongs_to :user, Clacks.User + + timestamps() + end + + def changeset(%__MODULE__{} = schema, attrs) do + schema + |> cast(attrs, [:ap_id, :nickname, :local, :data]) + |> validate_required([:ap_id, :nickname, :local, :data]) + 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) when is_binary(ap_id) 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) when is_binary(ap_id) do + Repo.one(from a in __MODULE__, where: a.ap_id == ^ap_id) + end + + @spec fetch(ap_id :: String.t()) :: t() | nil + def fetch(ap_id) when is_binary(ap_id) do + case Clacks.ActivityPub.Fetcher.fetch_actor(ap_id) do + nil -> + nil + + data -> + ap_id = data["id"] + existing = get_cached_by_ap_id(ap_id) |> Repo.preload(:user) + + changeset = + changeset(existing || %__MODULE__{}, %{ + ap_id: ap_id, + nickname: data["preferredUsername"] <> "@" <> URI.parse(ap_id).host, + local: false, + data: data + }) + + case Repo.insert_or_update(changeset) do + {:ok, actor} -> + actor + + {:error, changeset} -> + Logger.error("Couldn't store remote actor #{ap_id}: #{inspect(changeset)}") + nil + end + end + end +end diff --git a/lib/clacks/keys.ex b/lib/clacks/keys.ex new file mode 100644 index 0000000..67695cb --- /dev/null +++ b/lib/clacks/keys.ex @@ -0,0 +1,19 @@ +defmodule Clacks.Keys do + def generate_rsa_pem() do + key = :public_key.generate_key({:rsa, 2048, 65_537}) + entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) + pem = :public_key.pem_encode([entry]) |> String.trim_trailing() + {:ok, pem} + end + + def keys_from_pem(pem) do + with [private_key_code] <- :public_key.pem_decode(pem), + private_key <- :public_key.pem_entry_decode(private_key_code), + {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} <- private_key do + {:ok, private_key, {:RSAPublicKey, modulus, exponent}} + else + error -> + {:error, error} + end + end +end diff --git a/lib/clacks/user.ex b/lib/clacks/user.ex new file mode 100644 index 0000000..8dcca89 --- /dev/null +++ b/lib/clacks/user.ex @@ -0,0 +1,21 @@ +defmodule Clacks.User do + use Ecto.Schema + import Ecto.Changeset + + @type t() :: %__MODULE__{} + + schema "users" do + field :username, :string + field :private_key, :string + + has_one :actor, Clacks.Actor + + timestamps() + end + + def changeset(%__MODULE__{} = schema, attrs) do + schema + |> cast(attrs, [:username, :private_key]) + |> validate_required([:username, :private_key]) + end +end diff --git a/mix.exs b/mix.exs index b4d00d7..77593bd 100644 --- a/mix.exs +++ b/mix.exs @@ -43,7 +43,11 @@ defmodule Clacks.MixProject do {:gettext, "~> 0.11"}, {:jason, "~> 1.0"}, {:plug_cowboy, "~> 2.0"}, - {:flake_id, "~> 0.1.0"} + {:flake_id, "~> 0.1.0"}, + {:http_signatures, + git: "https://git.pleroma.social/pleroma/http_signatures.git", + ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"}, + {:httpoison, "~> 1.5.1"} ] end diff --git a/mix.lock b/mix.lock index 016c7e8..5bfe9e5 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,6 @@ %{ "base62": {:hex, :base62, "1.2.1", "4866763e08555a7b3917064e9eef9194c41667276c51b59de2bc42c6ea65f806", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm"}, + "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, "cowboy": {:hex, :cowboy, "2.6.3", "99aa50e94e685557cad82e704457336a453d4abcb77839ad22dbe71f311fcc06", [:rebar3], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm"}, @@ -11,8 +12,15 @@ "file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"}, "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm"}, + "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, + "http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]}, + "httpoison": {:hex, :httpoison, "1.5.1", "0f55b5b673b03c5c327dac7015a67cb571b99b631acc0bc1b0b98dcd6b9f2104", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, + "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, "phoenix": {:hex, :phoenix, "1.4.10", "619e4a545505f562cd294df52294372d012823f4fd9d34a6657a8b242898c255", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, @@ -23,5 +31,7 @@ "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, "postgrex": {:hex, :postgrex, "0.15.1", "23ce3417de70f4c0e9e7419ad85bdabcc6860a6925fe2c6f3b1b5b1e8e47bf2f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"}, "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, } diff --git a/priv/repo/migrations/20190929031943_create_actors.exs b/priv/repo/migrations/20190929031943_create_actors.exs new file mode 100644 index 0000000..18e5db3 --- /dev/null +++ b/priv/repo/migrations/20190929031943_create_actors.exs @@ -0,0 +1,16 @@ +defmodule Clacks.Repo.Migrations.CreateActors do + use Ecto.Migration + + def change do + create table(:actors, primary_key: false) do + add :id, :uuid, primary_key: true + + add :ap_id, :string + add :nickname, :string + add :local, :boolean + add :data, :jsonb + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20190929182812_create_users.exs b/priv/repo/migrations/20190929182812_create_users.exs new file mode 100644 index 0000000..1c2b06e --- /dev/null +++ b/priv/repo/migrations/20190929182812_create_users.exs @@ -0,0 +1,12 @@ +defmodule Clacks.Repo.Migrations.CreateUsers do + use Ecto.Migration + + def change do + create table(:users) do + add :username, :string + add :private_key, :text + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20190929183129_actors_add_user_id.exs b/priv/repo/migrations/20190929183129_actors_add_user_id.exs new file mode 100644 index 0000000..eb2ee24 --- /dev/null +++ b/priv/repo/migrations/20190929183129_actors_add_user_id.exs @@ -0,0 +1,9 @@ +defmodule Clacks.Repo.Migrations.ActorsAddUserId do + use Ecto.Migration + + def change do + alter table(:actors) do + add :user_id, references(:users) + end + end +end diff --git a/priv/repo/migrations/20190929213050_actors_add_ap_id_unique_index.exs b/priv/repo/migrations/20190929213050_actors_add_ap_id_unique_index.exs new file mode 100644 index 0000000..b4ae6a2 --- /dev/null +++ b/priv/repo/migrations/20190929213050_actors_add_ap_id_unique_index.exs @@ -0,0 +1,7 @@ +defmodule Clacks.Repo.Migrations.ActorsAddApIdUniqueIndex do + use Ecto.Migration + + def change do + create unique_index(:actors, [:ap_id], name: :actors_unique_ap_id_index) + end +end