Add actors/fetching

This commit is contained in:
Shadowfacts 2019-09-29 18:30:59 -04:00
parent 70370fb824
commit ae9da65e1b
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
12 changed files with 226 additions and 1 deletions

2
.gitignore vendored
View File

@ -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

View File

@ -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"

View File

@ -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

71
lib/clacks/actor.ex Normal file
View File

@ -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

19
lib/clacks/keys.ex Normal file
View File

@ -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

21
lib/clacks/user.ex Normal file
View File

@ -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

View File

@ -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

View File

@ -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"},
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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