follows almost work with mastodon

This commit is contained in:
Moon Man 2024-08-26 21:07:50 +00:00
parent a434c693d5
commit 0168c366d5
10 changed files with 538 additions and 168 deletions

View File

@ -14,4 +14,6 @@ config :vonbraun,
summary: "I am the famous Fediverse Moon",
approve_followers: false,
# MUST be 32
id_key: "53F0CB234DA3DB1A43AE32DE4038516B"
id_key: "53F0CB234DA3DB1A43AE32DE4038516B",
fluree_url: "http://localhost:58090/fluree/",
fluree_ledger: "vonbraun"

View File

@ -1,19 +1,27 @@
defmodule Vonbraun.ActivityPubReq do
require Logger
alias Vonbraun.HTTPSignature
defp key_id() do
@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}#main-key"
"https://#{domain}/users/#{nickname}"
end
def get(url = %URI{:path => path, :query => query}) do
@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
headers = %{
"host" => host,
"accept" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
}
target =
if query do
if query && query != "" do
path <> "?" <> query
else
path
@ -24,13 +32,14 @@ defmodule Vonbraun.ActivityPubReq do
Req.get(url, headers: headers)
end
def post(url = %URI{:path => path, :query => query}, body) when is_binary(body) do
def post(url = %URI{:path => path, :query => query, :host => host}, body) when is_binary(body) do
headers = %{
"host" => host,
"accept" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
}
target =
if query do
if query && query != "" do
path <> "?" <> query
else
path
@ -38,6 +47,39 @@ defmodule Vonbraun.ActivityPubReq do
headers = HTTPSignature.add_post_signature(headers, key_id(), target, body)
Req.post(url, headers: headers)
Logger.debug("POST payload is: `#{body}`")
Req.post(url, headers: headers, body: body)
end
def get_actor(actor_url) when is_binary(actor_url) do
get_actor(URI.parse(actor_url))
end
def get_actor(actor_url = %URI{}) do
case get(actor_url) do
{:ok, %{:status => 200, :body => %{} = actor}} ->
{:ok, actor}
{:error, error} ->
Logger.warning("Failed to get actor: #{actor_url} error: #{inspect(error)}")
{:error, error}
response ->
Logger.warning("Failed to get actor: #{actor_url} response: #{inspect(response)}")
{:error, response}
end
end
def extract_actor_inbox(%{"inbox" => inbox}) when is_binary(inbox) do
{:ok, inbox}
end
def extract_actor_inbox(%{"endpoints" => %{"sharedInbox" => inbox}}) when is_binary(inbox) do
{:ok, inbox}
end
def extract_actor_inbox(%{}) do
{:error, :notfound}
end
end

View File

@ -1,5 +1,52 @@
defmodule Vonbraun.Control do
def follow(id) when is_binary(id) do
require Logger
alias Vonbraun.ActivityPubReq
defp self_id() do
domain = Application.fetch_env!(:vonbraun, :domain)
nickname = Application.fetch_env!(:vonbraun, :nickname)
"https://#{domain}/users/#{nickname}"
end
def follow(followee_actor_id) when is_binary(followee_actor_id) do
with {:actor, {:ok, actor}} <- {:actor, ActivityPubReq.get_actor(followee_actor_id)},
{:inbox, {:ok, raw_inbox}} <- {:inbox, ActivityPubReq.extract_actor_inbox(actor)},
{:parse_inbox, inbox = %URI{}} <- {:parse_inbox, URI.parse(raw_inbox)} do
domain = Application.fetch_env!(:vonbraun, :domain)
# Is it a bad idea making this absolute?
activity_id = "https://#{domain}/id/follow:#{URI.encode(followee_actor_id)}"
payload =
%{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => activity_id,
"to" => [
followee_actor_id
],
"cc" => [],
"bcc" => [],
"bto" => [],
"type" => "Follow",
"actor" => self_id(),
"object" => followee_actor_id
}
|> Jason.encode!()
case ActivityPubReq.post(inbox, payload) do
{:ok, %{:status => status, :body => body}} when status >= 200 and status <= 299 ->
Logger.debug(inspect(body))
Logger.info("Follow request sent successfully, now it's up to the remote server.")
:ok
{:ok, %{:status => status, :body => body}} ->
Logger.debug(inspect(body))
Logger.warning("Got a status of: #{status}, probably not good.")
{:error, error} ->
Logger.error("Failed to send follow request: #{inspect(error)}")
{:error, error}
end
end
end
def unfollow(id) when is_binary(id) do

View File

@ -2,6 +2,7 @@ defmodule Vonbraun.Ecto.Schema.Actor do
use Ecto.Schema
alias Ecto.Changeset
alias Vonbraun.Repo
import Ecto.Query, only: [from: 2]
@primary_key {:id, :string, autogenerate: false}
@ -17,12 +18,12 @@ defmodule Vonbraun.Ecto.Schema.Actor do
end
@type t :: %__MODULE__{
muted: boolean(),
blocked: DateTime.t(),
muted: boolean() | nil,
blocked: DateTime.t() | nil,
follows_me_state: String.t() | nil,
follows_me_ts: DateTime.t(),
follows_me_ts: DateTime.t() | nil,
following_state: String.t() | nil,
following_ts: String.t()
following_ts: String.t() | nil
}
def changeset(struct, params \\ %{}) do
@ -165,4 +166,24 @@ defmodule Vonbraun.Ecto.Schema.Actor do
{:error, error}
end
end
@spec get_my_followers() :: list(String.t())
def get_my_followers() do
query = from u in __MODULE__,
where: u.follows_me_state == "accepted",
select: u.id,
order_by: u.follows_me_ts
Repo.all(query)
end
@spec get_my_follows() :: list(String.t())
def get_my_follows() do
query = from u in __MODULE__,
where: u.following_state == "accepted",
select: u.id,
order_by: u.following_ts
Repo.all(query)
end
end

90
lib/vonbraun/fluree.ex Normal file
View File

@ -0,0 +1,90 @@
defmodule Vonbraun.Fluree do
defp strip_at_from_properties(object = %{}) do
Enum.reduce(object, Map.new(), fn
{"@context", value}, map -> Map.put(map, "@context", value)
{"@" <> bare_key, value}, map -> Map.put(map, bare_key, value)
{key, value}, map -> Map.put(map, key, value)
end)
end
defp restore_at_to_properties(object = %{}) do
Enum.reduce(object, Map.new(), fn
{"type", value}, map -> Map.put(map, "@type", value)
{"id", value}, map -> Map.put(map, "@id", value)
end)
end
defp fluree_post(path, body = %{}) when is_binary(path) do
body = Jason.encode!(body)
url = Application.fetch_env!(:vonbraun, :fluree_url) <> path
headers = [{"Content-Type", "application/json"}]
Req.post(url, headers: headers, body: body)
end
@spec get(binary()) :: {:error, any()} | {:ok, map() | nil}
def get(id) when is_binary(id) do
payload = %{
"select" => %{
id => ["*"]
},
"from" => Application.fetch_env!(:vonbraun, :fluree_ledger)
}
case fluree_post("query", payload) do
{:ok, %{:body => %{"error" => error}}} ->
{:error, error}
{:ok, %{:status => status, :body => []}} when status >= 200 and status <= 299 ->
{:ok, nil}
{:ok, %{:status => status, :body => [object]}} when status >= 200 and status <= 299 ->
{:ok, strip_at_from_properties(object)}
{:error, error} ->
{:error, error}
end
end
def insert_actor(actor = %{}, options \\ []) do
actor = restore_at_to_properties(actor)
me? = Keyword.get(options, :me, false)
is_follower? = Keyword.get(options, :follower, false)
i_follow? = Keyword.get(options, :following, false)
props = Map.new()
props = if me? do
Map.put(props, "me", true)
else
props
end
props = if is_follower? do
Map.put(props, "isFollower", true)
else
props
end
props = if i_follow? do
Map.put(props, "following", true)
else
props
end
Map.put(actor, "vonbraun:props", props)
payload = %{
"ledger" => "vonbraun",
"insert" => actor
}
case fluree_post("/transact", payload) do
{:ok, %{:status => status}} when status >= 200 and status <= 299 ->
:ok
{:ok, %{:status => status, :body => body}} ->
{:error, %{status: status, body: body}}
{:error, error} ->
{:error, error}
end
end
end

View File

@ -3,17 +3,26 @@ defmodule Vonbraun.HTTPSignature do
Implements RFC 9421 HTTP Signatures using the Digest header.
"""
require Logger
alias Plug.Conn
# (request-body) is really only for signing not sending as a header, but send
# it anyway for broken implementations.
@post_headers ["(request-body)", "host", "date", "digest"]
@other_headers ["(request-body)", "host", "date"]
@post_headers ["(request-target)", "host", "date", "digest"]
@other_headers ["(request-target)", "host", "date"]
def signed_date, do: signed_date(NaiveDateTime.utc_now())
def signed_date(%NaiveDateTime{} = date) do
Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
end
@spec add_post_signature(map(), String.t(), String.t(), String.t()) :: map()
def add_post_signature(headers, key_id, path, body)
when is_map(headers) and is_binary(key_id) and is_binary(body) and is_binary(path) do
request_target = "post #{path}"
digest = compute_digest(body)
date = Timex.format!(Timex.now(:utc), "{RFC1123}")
date = signed_date()
headers =
Map.put(headers, "digest", digest)
@ -23,56 +32,105 @@ defmodule Vonbraun.HTTPSignature do
signing_string = construct_signing_string(headers, @post_headers)
signature = sign(signing_string)
signature_header = build_signature_header(key_id, signature, @post_headers)
Map.put(headers, "signature", signature_header)
Map.put(headers, "signature", signature_header) |> Map.delete("(request-target)")
end
@spec add_get_signature(map(), String.t(), String.t()) :: map()
def add_get_signature(headers, key_id, path)
when is_map(headers) and is_binary(key_id) and is_binary(path) do
request_target = "get #{path}"
date = Timex.format!(Timex.now(:utc), "{RFC1123}")
headers = Map.put(headers, "date", date) |> Map.put("(request-target)", request_target)
date = signed_date()
headers =
Map.put(headers, "date", date)
|> Map.put("(request-target)", request_target)
signing_string = construct_signing_string(headers, @other_headers)
signature = sign(signing_string)
signature_header = build_signature_header(key_id, signature, @other_headers)
Map.put(headers, "signature", signature_header)
Map.put(headers, "signature", signature_header) |> Map.delete("(request-target)")
end
@spec verify_post_signature(map(), String.t()) :: boolean()
def verify_post_signature(headers, body) when is_map(headers) and is_binary(body) do
signature_header = Map.get(headers, "signature")
def verify_post_signature(conn = %Conn{}, public_key) do
target =
if conn.query_string && conn.query_string != "" do
"#{conn.request_path}?#{conn.query_string}"
else
conn.request_path
end
case parse_signature_header(signature_header) do
{:ok, {_key_id, signature}} ->
digest = compute_digest(body)
test_headers = Map.put(headers, "digest", digest)
signing_string = construct_signing_string(test_headers, @post_headers)
expected_signature = sign(signing_string)
SecureCompare.compare(expected_signature, Base.decode64!(signature))
valid_request_target = "post #{target}"
{:error, _error} ->
with {:raw_body, {:ok, raw_body, _}} <- {:raw_body, Conn.read_body(conn)},
{:signature_header, [signature_header]} <-
{:signature_header, Conn.get_req_header(conn, "signature")},
{:parse,
{:ok, %{
"keyId" => _key_id,
"headers" => signing_headers,
"signature" => {:ok, signature}
}
}} <- {:parse, parse_signature_header(signature_header)} do
# Reconstruct the signing data
reconstructed_header_list = Enum.reduce_while(signing_headers |> Enum.reverse(), [], fn
"(request-target)", list ->
{:cont, ["(request-target): #{valid_request_target}" | list]}
"digest", list ->
with {:get, [header_value]} <- {:get, Conn.get_req_header(conn, "digest")},
{:split, [digest_algo, encoded_digest]} <- {:split, String.split(header_value, "=", parts: 2)},
{:decode, {:ok, provided_digest}} <- {:decode, Base.decode64(encoded_digest)} do
new_digest = :crypto.hash(:sha256, raw_body)
test_digest = digest_algo <> "=" <> (new_digest |> Base.encode64())
if provided_digest == new_digest do
{:cont, ["digest: #{test_digest}" | list]}
else
Logger.warning("digest failed: #{provided_digest} <> #{test_digest}")
{:halt, false}
end
else
error ->
Logger.warning("Failed to handle digest: #{inspect(error)}")
{:halt, false}
end
header_name, list ->
case Conn.get_req_header(conn, header_name) do
[] ->
Logger.warning("header not found for http signature verification: #{header_name}")
{:halt, false}
[header_value] ->
{:cont, ["#{header_name}: #{header_value}" | list]}
end
end)
case reconstructed_header_list do
false -> false
_ ->
# Hope they use the same newline chars
test_str = Enum.join(reconstructed_header_list, "\n")
Logger.debug("Here is the test string for signature verification: `#{test_str}`")
case ExPublicKey.verify(test_str, signature, public_key) do
{:ok, val} -> val
error ->
Logger.warning("Error verifying signature: #{inspect(error)}")
false
end
end
@spec verify_get_signature(map()) :: boolean()
def verify_get_signature(headers) when is_map(headers) do
signature_header = Map.get(headers, "signature")
case parse_signature_header(signature_header) do
{:ok, {_key_id, signature}} ->
signing_string = construct_signing_string(headers, @other_headers)
expected_signature = sign(signing_string)
SecureCompare.compare(expected_signature, Base.decode64!(signature))
{:error, _error} ->
else
error ->
Logger.warning("Verify http sig unmatch: #{inspect(error)}")
false
end
end
defp compute_digest(body) when is_binary(body) do
encoded = :crypto.hash(:sha256, body) |> Base.encode64()
"sha-256=" <> encoded
"SHA-256=" <> encoded
end
defp construct_signing_string(headers, include_headers) do
@ -85,20 +143,35 @@ defmodule Vonbraun.HTTPSignature do
defp sign(data) when is_binary(data) do
{:ok, signature} = ExPublicKey.sign(data, Vonbraun.KeyAgent.get_private_key())
Base.encode64(signature)
encoded_signature = Base.encode64(signature)
Logger.debug("Data to be signed: `#{data}`")
Logger.debug("Signature: #{encoded_signature}")
encoded_signature
end
defp build_signature_header(key_id, signature, headers) do
"Signature keyId=\"#{key_id}\",headers=\"#{Enum.join(headers, " ")}\",signature=\"#{signature}\""
"keyId=\"#{key_id}\",headers=\"#{Enum.join(headers, " ")}\",algorithm=\"rsa-sha256\",signature=\"#{signature}\""
end
defp parse_signature_header(header_value) when is_binary(header_value) do
with {:prefix, "Signature " <> pairs_string} <- {:prefix, header_value},
{:parse, items} <- {:parse, Regex.scan(~r/([-a-z-A-Z]+)="(.+)"/U, pairs_string)},
{:reduce, %{"keyId" => key_id, "signature" => signature}} <-
with {:parse, items} <- {:parse, Regex.scan(~r/([-a-z-A-Z]+)="(.+)"/U, header_value)},
{:reduce, map = %{}} <-
{:reduce,
Enum.reduce(items, Map.new(), fn [_, key, value], map -> Map.put(map, key, value) end)} do
{:ok, {key_id, signature}}
Enum.reduce(items, Map.new(), fn
[_, "headers", headers_str], map ->
Map.put(map, "headers", String.split(headers_str, " "))
[_, "signature", encoded_signature], map ->
Map.put(map, "signature", Base.decode64(encoded_signature))
[_, "digest", value], map ->
[algo, encoded_digest] = String.split(value, "=", parts: 2)
Map.put(map, "digest", {algo, Base.decode64(encoded_digest)})
[_, key, value], map ->
Map.put(map, key, value)
end)} do
{:ok, map}
else
_ -> {:error, :invalid}
end

View File

@ -1,4 +1,7 @@
defmodule Vonbraun.InboxRouter do
alias Vonbraun.HTTPSignature
alias Vonbraun.ActivityPubReq
alias Vonbraun.Ecto.Schema.Actor
use Plug.Router
require Logger
@ -16,12 +19,119 @@ defmodule Vonbraun.InboxRouter do
end
post "/" do
nickname = Application.fetch_env!(:vonbraun, :nickname)
{:ok, body, _conn} = Plug.Conn.read_body(conn)
Logger.debug("Got inbox POST: #{body}")
if conn.params["user"] == nickname do
send_resp(conn, 200, "ok")
cond do
conn.params["user"] in [nil, Application.fetch_env!(:vonbraun, :nickname)] ->
with {:actor_url, {:ok, activity = %{"actor" => actor_url}}} <-
{:actor_url, Jason.decode(body)},
{:actor, {:ok, actor = %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}} <-
{:actor, ActivityPubReq.get_actor(actor_url)},
{:load, {:ok, public_key}} <- {:load, ExPublicKey.loads(public_key_pem)},
{:verify, true} <- {:verify, HTTPSignature.verify_post_signature(conn, public_key)},
{:send, {:ok, response}} <- {:send, handle_activity(activity, actor)} do
status_code =
case response do
:ignored ->
200
:unauthorized ->
401
:accepted ->
200
:rejected ->
200
end
send_resp(conn, status_code, "boop")
else
error ->
Logger.warning("Some kind of failure: #{inspect(error)}")
send_resp(conn, 401, "fuck off")
end
true ->
send_resp(conn, 404, "fuck off")
end
end
defp handle_activity(
%{"type" => "Follow", "actor" => actor_id, "object" => follow_target},
actor = %{}
) do
with {:valid_target, true} <- {:valid_target, ActivityPubReq.actor_id() == follow_target},
{: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
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 =
case follows_me_state do
"accepted" ->
"Accept"
"rejected" ->
"Reject"
"pending" ->
nil
end
if activity_type do
payload = Map.put(payload, "type", activity_type) |> Jason.encode!()
Logger.debug("Replying to follow request with: #{activity_type}")
Logger.debug("And payload: `#{payload}`")
with {:inbox, {:ok, inbox}} <- {:inbox, ActivityPubReq.extract_actor_inbox(actor)},
{:inbox_uri, inbox = %URI{}} <- {:inbox_uri, URI.parse(inbox)} do
Task.start(fn ->
case ActivityPubReq.post(inbox, payload) do
{:ok, %{:status => status, :body => body}} ->
Logger.debug("Accept response status: #{status} body: #{inspect(body)}")
{:error, error} ->
Logger.error("Failed to Accept: #{inspect(error)}")
end
end)
{:ok, String.to_atom(follows_me_state)}
else
{:inbox, {:error, _}} ->
{:error, :inbox}
end
else
{:ok, :ignored}
end
else
{:valid_target, false} ->
{:ok, :unauthorized}
{:add, {:ok, %Actor{:blocked => blocked_ts}}} when not is_nil(blocked_ts) ->
{:ok, :unauthorized}
{:add, {:ok, %Actor{:follows_me_state => nil}}} ->
Logger.error("follows-me state was nil, this should never happen")
{:error, :impossible}
{:add, {:error, error}} ->
{:error, error}
end
end
defp handle_activity(_activity = %{}, _) do
{:ok, :ignored}
end
end

View File

@ -1,6 +1,8 @@
defmodule Vonbraun.KeyAgent do
use Agent
require Logger
def start_link(_opts) do
Agent.start_link(fn -> load_keys() end, name: __MODULE__)
end
@ -13,6 +15,10 @@ defmodule Vonbraun.KeyAgent do
ExPublicKey.pem_encode(public_key)
end
public_pem = String.replace(public_pem, "\\n", "\n")
Logger.debug("Public key is: `#{public_pem}`")
%{
private_key: private_key,
public_pem: public_pem

View File

@ -3,135 +3,108 @@ defmodule Vonbraun.MyRouter do
require Logger
alias Vonbraun.Ecto.Schema.Pair
alias Vonbraun.Ecto.Schema.Actor
alias Vonbraun.InboxRouter
plug(:match)
plug(:dispatch)
defp render_outbox(nickname, domain),
do: """
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://#{domain}/schemas/litepub-0.1.jsonld",
{
"@language": "und"
do: %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "https://#{domain}/users/#{nickname}/outbox",
"type" => "OrderedCollection",
"first" => "https://#{domain}/users/#{nickname}/outbox?page=true"
}
],
"first": "https://#{domain}/users/#{nickname}/outbox?page=true",
"id": "https://#{domain}/users/#{nickname}/outbox",
"type": "OrderedCollection"
}
"""
defp render_outbox_page(nickname, domain),
do: """
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://#{domain}/schemas/litepub-0.1.jsonld",
{
"@language": "und"
do: %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "https://#{domain}/users/#{nickname}/outbox?page=true",
"type" => "OrderedCollectionPage",
"orderedItems" => [],
"partOf" => "https://#{domain}/users/#{nickname}/outbox"
}
],
"id": "https://#{domain}/users/#{nickname}/outbox?page=true",
"orderedItems": [
],
"partOf": "https://#{domain}/users/#{nickname}/outbox",
"type": "OrderedCollectionPage"
}
"""
defp render_webfinger(nickname, domain),
do: """
{
"subject": "acct:#{nickname}@#{domain}",
"aliases": [
do: %{
"subject" => "acct:#{nickname}@#{domain}",
"aliases" => [
"https://#{domain}/users/#{nickname}"
],
"links": [
{
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html",
"href": "https://#{domain}/users/#{nickname}"
"links" => [
%{
"rel" => "http://webfinger.net/rel/profile-page",
"type" => "text/html",
"href" => "https://#{domain}/users/#{nickname}"
},
{
"rel": "self",
"type": "application/activity+json",
"href": "https://#{domain}/users/#{nickname}"
%{
"rel" => "self",
"type" => "application/activity+json",
"href" => "https://#{domain}/users/#{nickname}"
}
]
}
"""
defp render_user(nickname, domain, name, summary, public_key),
do: """
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://#{domain}/schemas/litepub-0.1.jsonld",
{
"@language": "und"
do: %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "https://#{domain}/users/#{nickname}",
"type" => "Person",
"url" => "https://#{domain}/users/#{nickname}",
"name" => name,
"preferredUsername" => nickname,
"alsoKnownAs" => [],
"webfinger" => "acct:#{nickname}@#{domain}",
"summary" => summary,
"attachment" => [],
"discoverable" => false,
"endpoints" => %{
"sharedInbox" => "https://#{domain}/inbox"
},
"followers" => "https://#{domain}/users/#{nickname}/followers",
"following" => "https://#{domain}/users/#{nickname}/following",
"icon" => %{
"type" => "Image",
"url" => "https://#{domain}/icon.png"
},
"outbox" => "https://#{domain}/users/#{nickname}/outbox",
"inbox" => "https://#{domain}/users/#{nickname}/inbox",
"manuallyApprovesFollowers" => false,
"publicKey" => %{
"id" => "https://#{domain}/users/#{nickname}#main-key",
"owner" => "https://#{domain}/users/#{nickname}",
"publicKeyPem" => public_key
},
"tag" => [],
"vonbraun:props" => %{
"me" => true
}
],
"alsoKnownAs": [],
"attachment": [],
"capabilities": {
"acceptsChatMessages": true
},
"discoverable": false,
"endpoints": {
"sharedInbox": "https://#{domain}/inbox"
},
"followers": "https://#{domain}/users/#{nickname}/followers",
"following": "https://#{domain}/users/#{nickname}/following",
"icon": {
"type": "Image",
"url": "https://#{domain}/icon.png"
},
"id": "https://#{domain}/users/#{nickname}",
"inbox": "https://#{domain}/users/#{nickname}/inbox",
"manuallyApprovesFollowers": false,
"name": "#{name}",
"outbox": "https://#{domain}/users/#{nickname}/outbox",
"preferredUsername": "#{nickname}",
"publicKey": {
"id": "https://#{domain}/users/#{nickname}#main-key",
"owner": "https://#{domain}/users/#{nickname}",
"publicKeyPem": "#{public_key}"
},
"summary": "#{summary}",
"tag": [],
"type": "Person",
"url": "https://#{domain}/users/#{nickname}",
"vcard:bday": null,
"webfinger": "acct:#{nickname}@#{domain}"
}
"""
defp render_following(nickname, domain),
do: """
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://#{domain}/users/#{nickname}/following",
"type": "OrderedCollection",
"totalItems": 0,
"orderedItems": [
]
}
"""
defp render_following(nickname, domain) do
following = Actor.get_my_follows()
defp render_followers(nickname, domain),
do: """
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://#{domain}/users/#{nickname}/followers",
"type": "OrderedCollection",
"totalItems": 0,
"orderedItems": [
]
%{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "https://#{domain}/users/#{nickname}/following",
"type" => "OrderedCollection",
"totalItems" => length(following),
"orderedItems" => following
}
"""
end
defp render_followers(nickname, domain) do
followers = Actor.get_my_followers()
%{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "https://#{domain}/users/#{nickname}/followers",
"type" => "OrderedCollection",
"totalItems" => length(followers),
"orderedItems" => followers
}
end
get "/.well-known/webfinger" do
conn = Plug.Conn.fetch_query_params(conn)
@ -148,7 +121,7 @@ defmodule Vonbraun.MyRouter do
"https://#{domain}/users/#{nickname}"
]} do
conn = Plug.Conn.put_resp_content_type(conn, "application/jrd+json")
send_resp(conn, 200, render_webfinger(nickname, domain))
send_resp(conn, 200, render_webfinger(nickname, domain) |> Jason.encode!())
else
{:resource, resource} ->
Logger.error("not a binary resource: #{resource}")
@ -167,10 +140,15 @@ defmodule Vonbraun.MyRouter do
domain = Application.fetch_env!(:vonbraun, :domain)
name = Application.fetch_env!(:vonbraun, :name)
summary = Application.fetch_env!(:vonbraun, :summary)
public_pem = Vonbraun.KeyAgent.get_public_pem() |> String.replace("\n", "\\n")
public_pem = Vonbraun.KeyAgent.get_public_pem()
conn = Plug.Conn.put_resp_content_type(conn, "application/activity+json")
send_resp(conn, 200, render_user(nickname, domain, name, summary, public_pem))
send_resp(
conn,
200,
render_user(nickname, domain, name, summary, public_pem) |> Jason.encode!()
)
else
send_resp(conn, 404, "fuck off")
end
@ -183,7 +161,7 @@ defmodule Vonbraun.MyRouter do
domain = Application.fetch_env!(:vonbraun, :domain)
conn = Plug.Conn.put_resp_content_type(conn, "application/activity+json")
send_resp(conn, 200, render_following(nickname, domain))
send_resp(conn, 200, render_following(nickname, domain) |> Jason.encode!())
else
send_resp(conn, 404, "fuck off")
end
@ -196,7 +174,7 @@ defmodule Vonbraun.MyRouter do
domain = Application.fetch_env!(:vonbraun, :domain)
conn = Plug.Conn.put_resp_content_type(conn, "application/activity+json")
send_resp(conn, 200, render_followers(nickname, domain))
send_resp(conn, 200, render_followers(nickname, domain) |> Jason.encode!())
else
send_resp(conn, 404, "fuck off")
end
@ -212,14 +190,14 @@ defmodule Vonbraun.MyRouter do
with {:user, ^nickname} <- {:user, user},
{:page, "true"} <- {:page, conn.params["page"]} do
conn = Plug.Conn.put_resp_content_type(conn, "application/activity+json")
send_resp(conn, 200, render_outbox_page(nickname, domain))
send_resp(conn, 200, render_outbox_page(nickname, domain) |> Jason.encode!())
else
{:user, _} ->
send_resp(conn, 404, "fuck off")
{:page, _} ->
conn = Plug.Conn.put_resp_content_type(conn, "application/activity+json")
send_resp(conn, 200, render_outbox(nickname, domain))
send_resp(conn, 200, render_outbox(nickname, domain) |> Jason.encode!())
end
end

View File

@ -13,6 +13,7 @@ defmodule Vonbraun.Repo.Migrations.Initial do
add(:id, :string, primary_key: true)
add(:muted, :boolean)
add(:blocked, :naive_datetime)
add(:blocks_me, :naive_datetime)
add(:follows_me_state, :string)
add(:follows_me_ts, :naive_datetime)
add(:following_state, :string)