This commit is contained in:
Moon Man 2024-08-29 10:31:28 +00:00
parent 1470dcebb7
commit 249ae3619b
10 changed files with 160 additions and 34 deletions

View File

@ -0,0 +1,29 @@
defmodule Vonbraun.ActivityPub.Handler do
use Agent
require Logger
def start_link(_opts) do
Agent.start_link(fn -> load_handlers() end, name: __MODULE__)
end
defp load_handlers() do
[
Vonbraun.ActivityPub.Handler.Follow,
Vonbraun.ActivityPub.Handler.Accept,
Vonbraun.ActivityPub.Handler.Reject,
Vonbraun.ActivityPub.Handler.Undo
]
|> Enum.reduce(Map.new(), fn mod, map ->
type = apply(mod, :type, [])
Map.put(map, type, &mod.handle/1)
end)
end
@spec handle(%{type: String.t()}) :: :ok | {:ok, atom()} | {:error, any()}
def handle(activity = %{"type" => type}) when is_binary(type) do
Agent.get(__MODULE__, fn map ->
func = Map.get(map, type, fn _ -> {:error, :type} end)
apply(func, [activity])
end)
end
end

View File

@ -0,0 +1,10 @@
defmodule Vonbraun.ActivityPub.Handler.Accept do
@behaviour Vonbraun.ActivityPub.HandlerBehaviour
def type, do: "Accept"
# Lots of kinds of things can be accepted.
def handle(_activity = %{"type" => "Accept"}) do
:ok
end
end

View File

@ -0,0 +1,9 @@
defmodule Vonbraun.ActivityPub.Handler.Follow do
@behaviour Vonbraun.ActivityPub.HandlerBehaviour
def type, do: "Follow"
def handle(_activity = %{"type" => "Follow"}) do
:ok
end
end

View File

@ -0,0 +1,10 @@
defmodule Vonbraun.ActivityPub.Handler.Reject do
@behaviour Vonbraun.ActivityPub.HandlerBehaviour
def type, do: "Reject"
# Lots of kinds of things can be accepted.
def handle(_activity = %{"type" => "Reject"}) do
:ok
end
end

View File

@ -0,0 +1,10 @@
defmodule Vonbraun.ActivityPub.Handler.Undo do
@behaviour Vonbraun.ActivityPub.HandlerBehaviour
def type, do: "Undo"
# Lots of different kinds of things can be undone.
def handle(_activity = %{"type" => "Undo"}) do
:ok
end
end

View File

@ -0,0 +1,4 @@
defmodule Vonbraun.ActivityPub.HandlerBehaviour do
@callback type() :: String.t()
@callback handle(activity :: map()) :: :ok | {:ok, atom()} | {:error, any()}
end

View File

@ -0,0 +1,78 @@
defmodule Vonbraun.ActivityPub.Object do
@context "https://www.w3.org/ns/activitystreams"
@spec my_id() :: String.t()
def my_id() do
domain = Application.fetch_env!(:vonbraun, :domain)
nickname = Application.fetch_env!(:vonbraun, :nickname)
"https://#{domain}/users/#{nickname}"
end
@spec my_key_id() :: String.t()
def my_key_id() do
"#{my_id()}#main-key"
end
def activity(type, id, object, options \\ [])
when is_binary(type) and is_binary(id) and (is_binary(object) or is_map(object)) do
to =
case Keyword.get(options, :to, []) do
to when is_binary(to) -> [to]
to when is_list(to) -> to
end
cc =
case Keyword.get(options, :cc, []) do
cc when is_binary(cc) -> [cc]
cc when is_list(cc) -> cc
end
copy_recipients? = Keyword.get(options, :copy_recipients, false)
object =
if is_map(object) && copy_recipients? do
Map.merge(object, %{"to" => to, "cc" => cc})
else
object
end
%{
"@context" => @context,
"id" => id,
"to" => to,
"cc" => cc,
"bcc" => [],
"bto" => [],
"type" => type,
"actor" => my_id(),
"object" => object
}
end
def my_follow_activity_id(to_follow_id) when is_binary(to_follow_id),
do:
"https://#{Application.fetch_env!(:vonbraun, :domain)}/id/follow:#{URI.encode(to_follow_id)}"
def follow_activity(to_follow_id) when is_binary(to_follow_id),
do: activity("Follow", my_follow_activity_id(to_follow_id), to_follow_id, to: to_follow_id)
def accept_follow_activity(followee_id, type \\ :accept)
when is_binary(followee_id) and type in [:accept, :reject] do
activity_type =
if type == :accept do
"Accept"
else
"Reject"
end
object = %{
"type" => "Follow",
"actor" => followee_id
}
accept_activity_id =
"https://#{Application.fetch_env!(:vonbraun, :domain)}/id/follow-reply:#{URI.encode(followee_id)}"
activity(activity_type, accept_activity_id, object)
end
end

View File

@ -1,18 +1,7 @@
defmodule Vonbraun.ActivityPubReq do defmodule Vonbraun.ActivityPubReq do
require Logger require Logger
alias Vonbraun.HTTPSignature alias Vonbraun.HTTPSignature
alias Vonbraun.ActivityPub.Object
@spec actor_id() :: String.t()
def actor_id() do
domain = Application.fetch_env!(:vonbraun, :domain)
nickname = Application.fetch_env!(:vonbraun, :nickname)
"https://#{domain}/users/#{nickname}"
end
@spec key_id() :: String.t()
def key_id() do
"#{actor_id()}#main-key"
end
def get(url = %URI{:path => path, :query => query, :host => host}) do def get(url = %URI{:path => path, :query => query, :host => host}) do
headers = %{ headers = %{
@ -27,7 +16,7 @@ defmodule Vonbraun.ActivityPubReq do
path path
end end
headers = HTTPSignature.add_get_signature(headers, key_id(), target) headers = HTTPSignature.add_get_signature(headers, Object.my_key_id(), target)
Req.get(url, headers: headers) Req.get(url, headers: headers)
end end
@ -46,7 +35,7 @@ defmodule Vonbraun.ActivityPubReq do
path path
end end
headers = HTTPSignature.add_post_signature(headers, key_id(), target, body) headers = HTTPSignature.add_post_signature(headers, Object.my_key_id(), target, body)
Logger.debug("POST payload is: `#{body}`") Logger.debug("POST payload is: `#{body}`")

View File

@ -9,6 +9,7 @@ defmodule Vonbraun.Application do
def start(_type, _args) do def start(_type, _args) do
children = [ children = [
Vonbraun.KeyAgent, Vonbraun.KeyAgent,
Vonbraun.ActivityPub.Handler,
Vonbraun.Repo, Vonbraun.Repo,
{Bandit, scheme: :http, plug: Vonbraun.MyRouter, port: 4012} {Bandit, scheme: :http, plug: Vonbraun.MyRouter, port: 4012}
] ]

View File

@ -2,6 +2,7 @@ defmodule Vonbraun.InboxRouter do
alias Vonbraun.HTTPSignature alias Vonbraun.HTTPSignature
alias Vonbraun.ActivityPubReq alias Vonbraun.ActivityPubReq
alias Vonbraun.Ecto.Schema.Actor alias Vonbraun.Ecto.Schema.Actor
alias Vonbraun.ActivityPub.Object
use Plug.Router use Plug.Router
require Logger require Logger
@ -83,43 +84,28 @@ defmodule Vonbraun.InboxRouter do
{:error, :notfound} {:error, :notfound}
end end
# TODO: make sure I'm in the to list.
defp handle_activity( defp handle_activity(
%{"type" => "Follow", "actor" => actor_id, "object" => follow_target}, %{"type" => "Follow", "actor" => actor_id, "object" => follow_target},
actor = %{} actor = %{}
) )
when is_binary(follow_target) do when is_binary(follow_target) do
with {:valid_target, true} <- {:valid_target, ActivityPubReq.actor_id() == follow_target}, with {:valid_target, true} <- {:valid_target, Object.my_id() == follow_target},
{:add, {:ok, %Actor{:blocked => nil, :follows_me_state => follows_me_state}}} {:add, {:ok, %Actor{:blocked => nil, :follows_me_state => follows_me_state}}}
when not is_nil(follows_me_state) <- {:add, Actor.maybe_add_follower(actor_id)} do when not is_nil(follows_me_state) <- {:add, Actor.maybe_add_follower(actor_id)} do
domain = Application.fetch_env!(:vonbraun, :domain)
activity_id = "https://#{domain}/id/follow-reply:#{URI.encode(actor_id)}"
payload = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => activity_id,
"to" => [actor_id],
"actor" => ActivityPubReq.actor_id(),
"object" => %{
"type" => "Follow",
"actor" => actor_id
}
}
activity_type = activity_type =
case follows_me_state do case follows_me_state do
"accepted" -> "accepted" ->
"Accept" :accept
"rejected" -> "rejected" ->
"Reject" :reject
"pending" -> "pending" ->
nil nil
end end
if activity_type do if activity_type do
payload = Map.put(payload, "type", activity_type) |> Jason.encode!() payload = Object.accept_follow_activity(actor_id, activity_type) |> Jason.encode!()
Logger.debug("Replying to follow request with: #{activity_type}") Logger.debug("Replying to follow request with: #{activity_type}")
Logger.debug("And payload: `#{payload}`") Logger.debug("And payload: `#{payload}`")
@ -172,7 +158,7 @@ defmodule Vonbraun.InboxRouter do
with {:asked, {:ok, %Actor{:blocked => nil, :following_state => "accepted"}}} <- with {:asked, {:ok, %Actor{:blocked => nil, :following_state => "accepted"}}} <-
{:asked, Actor.mark_pending_follow(actor_id, "accepted", force: true)}, {:asked, Actor.mark_pending_follow(actor_id, "accepted", force: true)},
{:actor, {:ok, follow_actor_id}} <- {:actor, extract_follow_object_actor(object)}, {:actor, {:ok, follow_actor_id}} <- {:actor, extract_follow_object_actor(object)},
{:match, true} <- {:match, follow_actor_id == ActivityPubReq.actor_id()} do {:match, true} <- {:match, follow_actor_id == Object.my_id()} do
Logger.info("Now following: #{actor_id}") Logger.info("Now following: #{actor_id}")
{:ok, :following} {:ok, :following}
else else