follows almost work with mastodon
This commit is contained in:
parent
a434c693d5
commit
0168c366d5
|
@ -14,4 +14,6 @@ config :vonbraun,
|
||||||
summary: "I am the famous Fediverse Moon",
|
summary: "I am the famous Fediverse Moon",
|
||||||
approve_followers: false,
|
approve_followers: false,
|
||||||
# MUST be 32
|
# MUST be 32
|
||||||
id_key: "53F0CB234DA3DB1A43AE32DE4038516B"
|
id_key: "53F0CB234DA3DB1A43AE32DE4038516B",
|
||||||
|
fluree_url: "http://localhost:58090/fluree/",
|
||||||
|
fluree_ledger: "vonbraun"
|
||||||
|
|
|
@ -1,19 +1,27 @@
|
||||||
defmodule Vonbraun.ActivityPubReq do
|
defmodule Vonbraun.ActivityPubReq do
|
||||||
|
require Logger
|
||||||
alias Vonbraun.HTTPSignature
|
alias Vonbraun.HTTPSignature
|
||||||
|
|
||||||
defp key_id() do
|
@spec actor_id() :: String.t()
|
||||||
|
def actor_id() do
|
||||||
domain = Application.fetch_env!(:vonbraun, :domain)
|
domain = Application.fetch_env!(:vonbraun, :domain)
|
||||||
nickname = Application.fetch_env!(:vonbraun, :nickname)
|
nickname = Application.fetch_env!(:vonbraun, :nickname)
|
||||||
"https://#{domain}/users/#{nickname}#main-key"
|
"https://#{domain}/users/#{nickname}"
|
||||||
end
|
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 = %{
|
headers = %{
|
||||||
|
"host" => host,
|
||||||
"accept" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
"accept" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
||||||
}
|
}
|
||||||
|
|
||||||
target =
|
target =
|
||||||
if query do
|
if query && query != "" do
|
||||||
path <> "?" <> query
|
path <> "?" <> query
|
||||||
else
|
else
|
||||||
path
|
path
|
||||||
|
@ -24,13 +32,14 @@ defmodule Vonbraun.ActivityPubReq do
|
||||||
Req.get(url, headers: headers)
|
Req.get(url, headers: headers)
|
||||||
end
|
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 = %{
|
headers = %{
|
||||||
|
"host" => host,
|
||||||
"accept" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
"accept" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
||||||
}
|
}
|
||||||
|
|
||||||
target =
|
target =
|
||||||
if query do
|
if query && query != "" do
|
||||||
path <> "?" <> query
|
path <> "?" <> query
|
||||||
else
|
else
|
||||||
path
|
path
|
||||||
|
@ -38,6 +47,39 @@ defmodule Vonbraun.ActivityPubReq do
|
||||||
|
|
||||||
headers = HTTPSignature.add_post_signature(headers, key_id(), target, body)
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,52 @@
|
||||||
defmodule Vonbraun.Control do
|
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
|
end
|
||||||
|
|
||||||
def unfollow(id) when is_binary(id) do
|
def unfollow(id) when is_binary(id) do
|
||||||
|
|
|
@ -2,6 +2,7 @@ defmodule Vonbraun.Ecto.Schema.Actor do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
alias Ecto.Changeset
|
alias Ecto.Changeset
|
||||||
alias Vonbraun.Repo
|
alias Vonbraun.Repo
|
||||||
|
import Ecto.Query, only: [from: 2]
|
||||||
|
|
||||||
@primary_key {:id, :string, autogenerate: false}
|
@primary_key {:id, :string, autogenerate: false}
|
||||||
|
|
||||||
|
@ -17,12 +18,12 @@ defmodule Vonbraun.Ecto.Schema.Actor do
|
||||||
end
|
end
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
muted: boolean(),
|
muted: boolean() | nil,
|
||||||
blocked: DateTime.t(),
|
blocked: DateTime.t() | nil,
|
||||||
follows_me_state: String.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_state: String.t() | nil,
|
||||||
following_ts: String.t()
|
following_ts: String.t() | nil
|
||||||
}
|
}
|
||||||
|
|
||||||
def changeset(struct, params \\ %{}) do
|
def changeset(struct, params \\ %{}) do
|
||||||
|
@ -165,4 +166,24 @@ defmodule Vonbraun.Ecto.Schema.Actor do
|
||||||
{:error, error}
|
{:error, error}
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -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
|
|
@ -3,17 +3,26 @@ defmodule Vonbraun.HTTPSignature do
|
||||||
Implements RFC 9421 HTTP Signatures using the Digest header.
|
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
|
# (request-body) is really only for signing not sending as a header, but send
|
||||||
# it anyway for broken implementations.
|
# it anyway for broken implementations.
|
||||||
@post_headers ["(request-body)", "host", "date", "digest"]
|
@post_headers ["(request-target)", "host", "date", "digest"]
|
||||||
@other_headers ["(request-body)", "host", "date"]
|
@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()
|
@spec add_post_signature(map(), String.t(), String.t(), String.t()) :: map()
|
||||||
def add_post_signature(headers, key_id, path, body)
|
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
|
when is_map(headers) and is_binary(key_id) and is_binary(body) and is_binary(path) do
|
||||||
request_target = "post #{path}"
|
request_target = "post #{path}"
|
||||||
digest = compute_digest(body)
|
digest = compute_digest(body)
|
||||||
date = Timex.format!(Timex.now(:utc), "{RFC1123}")
|
date = signed_date()
|
||||||
|
|
||||||
headers =
|
headers =
|
||||||
Map.put(headers, "digest", digest)
|
Map.put(headers, "digest", digest)
|
||||||
|
@ -23,56 +32,105 @@ defmodule Vonbraun.HTTPSignature do
|
||||||
signing_string = construct_signing_string(headers, @post_headers)
|
signing_string = construct_signing_string(headers, @post_headers)
|
||||||
signature = sign(signing_string)
|
signature = sign(signing_string)
|
||||||
signature_header = build_signature_header(key_id, signature, @post_headers)
|
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
|
end
|
||||||
|
|
||||||
@spec add_get_signature(map(), String.t(), String.t()) :: map()
|
@spec add_get_signature(map(), String.t(), String.t()) :: map()
|
||||||
def add_get_signature(headers, key_id, path)
|
def add_get_signature(headers, key_id, path)
|
||||||
when is_map(headers) and is_binary(key_id) and is_binary(path) do
|
when is_map(headers) and is_binary(key_id) and is_binary(path) do
|
||||||
request_target = "get #{path}"
|
request_target = "get #{path}"
|
||||||
date = Timex.format!(Timex.now(:utc), "{RFC1123}")
|
date = signed_date()
|
||||||
headers = Map.put(headers, "date", date) |> Map.put("(request-target)", request_target)
|
|
||||||
|
headers =
|
||||||
|
Map.put(headers, "date", date)
|
||||||
|
|> Map.put("(request-target)", request_target)
|
||||||
|
|
||||||
signing_string = construct_signing_string(headers, @other_headers)
|
signing_string = construct_signing_string(headers, @other_headers)
|
||||||
signature = sign(signing_string)
|
signature = sign(signing_string)
|
||||||
signature_header = build_signature_header(key_id, signature, @other_headers)
|
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
|
end
|
||||||
|
|
||||||
@spec verify_post_signature(map(), String.t()) :: boolean()
|
def verify_post_signature(conn = %Conn{}, public_key) do
|
||||||
def verify_post_signature(headers, body) when is_map(headers) and is_binary(body) do
|
target =
|
||||||
signature_header = Map.get(headers, "signature")
|
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
|
valid_request_target = "post #{target}"
|
||||||
{: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))
|
|
||||||
|
|
||||||
{: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
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
else
|
||||||
@spec verify_get_signature(map()) :: boolean()
|
error ->
|
||||||
def verify_get_signature(headers) when is_map(headers) do
|
Logger.warning("Verify http sig unmatch: #{inspect(error)}")
|
||||||
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} ->
|
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp compute_digest(body) when is_binary(body) do
|
defp compute_digest(body) when is_binary(body) do
|
||||||
encoded = :crypto.hash(:sha256, body) |> Base.encode64()
|
encoded = :crypto.hash(:sha256, body) |> Base.encode64()
|
||||||
"sha-256=" <> encoded
|
"SHA-256=" <> encoded
|
||||||
end
|
end
|
||||||
|
|
||||||
defp construct_signing_string(headers, include_headers) do
|
defp construct_signing_string(headers, include_headers) do
|
||||||
|
@ -85,20 +143,35 @@ defmodule Vonbraun.HTTPSignature do
|
||||||
|
|
||||||
defp sign(data) when is_binary(data) do
|
defp sign(data) when is_binary(data) do
|
||||||
{:ok, signature} = ExPublicKey.sign(data, Vonbraun.KeyAgent.get_private_key())
|
{: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
|
end
|
||||||
|
|
||||||
defp build_signature_header(key_id, signature, headers) do
|
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
|
end
|
||||||
|
|
||||||
defp parse_signature_header(header_value) when is_binary(header_value) do
|
defp parse_signature_header(header_value) when is_binary(header_value) do
|
||||||
with {:prefix, "Signature " <> pairs_string} <- {:prefix, header_value},
|
with {:parse, items} <- {:parse, Regex.scan(~r/([-a-z-A-Z]+)="(.+)"/U, header_value)},
|
||||||
{:parse, items} <- {:parse, Regex.scan(~r/([-a-z-A-Z]+)="(.+)"/U, pairs_string)},
|
{:reduce, map = %{}} <-
|
||||||
{:reduce, %{"keyId" => key_id, "signature" => signature}} <-
|
|
||||||
{:reduce,
|
{:reduce,
|
||||||
Enum.reduce(items, Map.new(), fn [_, key, value], map -> Map.put(map, key, value) end)} do
|
Enum.reduce(items, Map.new(), fn
|
||||||
{:ok, {key_id, signature}}
|
[_, "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
|
else
|
||||||
_ -> {:error, :invalid}
|
_ -> {:error, :invalid}
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
defmodule Vonbraun.InboxRouter do
|
defmodule Vonbraun.InboxRouter do
|
||||||
|
alias Vonbraun.HTTPSignature
|
||||||
|
alias Vonbraun.ActivityPubReq
|
||||||
|
alias Vonbraun.Ecto.Schema.Actor
|
||||||
use Plug.Router
|
use Plug.Router
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
@ -16,12 +19,119 @@ defmodule Vonbraun.InboxRouter do
|
||||||
end
|
end
|
||||||
|
|
||||||
post "/" do
|
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
|
cond do
|
||||||
send_resp(conn, 200, "ok")
|
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
|
else
|
||||||
|
error ->
|
||||||
|
Logger.warning("Some kind of failure: #{inspect(error)}")
|
||||||
|
send_resp(conn, 401, "fuck off")
|
||||||
|
end
|
||||||
|
|
||||||
|
true ->
|
||||||
send_resp(conn, 404, "fuck off")
|
send_resp(conn, 404, "fuck off")
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
defmodule Vonbraun.KeyAgent do
|
defmodule Vonbraun.KeyAgent do
|
||||||
use Agent
|
use Agent
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
def start_link(_opts) do
|
def start_link(_opts) do
|
||||||
Agent.start_link(fn -> load_keys() end, name: __MODULE__)
|
Agent.start_link(fn -> load_keys() end, name: __MODULE__)
|
||||||
end
|
end
|
||||||
|
@ -13,6 +15,10 @@ defmodule Vonbraun.KeyAgent do
|
||||||
ExPublicKey.pem_encode(public_key)
|
ExPublicKey.pem_encode(public_key)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
public_pem = String.replace(public_pem, "\\n", "\n")
|
||||||
|
|
||||||
|
Logger.debug("Public key is: `#{public_pem}`")
|
||||||
|
|
||||||
%{
|
%{
|
||||||
private_key: private_key,
|
private_key: private_key,
|
||||||
public_pem: public_pem
|
public_pem: public_pem
|
||||||
|
|
|
@ -3,135 +3,108 @@ defmodule Vonbraun.MyRouter do
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
alias Vonbraun.Ecto.Schema.Pair
|
alias Vonbraun.Ecto.Schema.Pair
|
||||||
|
alias Vonbraun.Ecto.Schema.Actor
|
||||||
alias Vonbraun.InboxRouter
|
alias Vonbraun.InboxRouter
|
||||||
|
|
||||||
plug(:match)
|
plug(:match)
|
||||||
plug(:dispatch)
|
plug(:dispatch)
|
||||||
|
|
||||||
defp render_outbox(nickname, domain),
|
defp render_outbox(nickname, domain),
|
||||||
do: """
|
do: %{
|
||||||
{
|
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||||
"@context": [
|
"id" => "https://#{domain}/users/#{nickname}/outbox",
|
||||||
"https://www.w3.org/ns/activitystreams",
|
"type" => "OrderedCollection",
|
||||||
"https://#{domain}/schemas/litepub-0.1.jsonld",
|
"first" => "https://#{domain}/users/#{nickname}/outbox?page=true"
|
||||||
{
|
|
||||||
"@language": "und"
|
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"first": "https://#{domain}/users/#{nickname}/outbox?page=true",
|
|
||||||
"id": "https://#{domain}/users/#{nickname}/outbox",
|
|
||||||
"type": "OrderedCollection"
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
defp render_outbox_page(nickname, domain),
|
defp render_outbox_page(nickname, domain),
|
||||||
do: """
|
do: %{
|
||||||
{
|
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||||
"@context": [
|
"id" => "https://#{domain}/users/#{nickname}/outbox?page=true",
|
||||||
"https://www.w3.org/ns/activitystreams",
|
"type" => "OrderedCollectionPage",
|
||||||
"https://#{domain}/schemas/litepub-0.1.jsonld",
|
"orderedItems" => [],
|
||||||
{
|
"partOf" => "https://#{domain}/users/#{nickname}/outbox"
|
||||||
"@language": "und"
|
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"id": "https://#{domain}/users/#{nickname}/outbox?page=true",
|
|
||||||
"orderedItems": [
|
|
||||||
],
|
|
||||||
"partOf": "https://#{domain}/users/#{nickname}/outbox",
|
|
||||||
"type": "OrderedCollectionPage"
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
defp render_webfinger(nickname, domain),
|
defp render_webfinger(nickname, domain),
|
||||||
do: """
|
do: %{
|
||||||
{
|
"subject" => "acct:#{nickname}@#{domain}",
|
||||||
"subject": "acct:#{nickname}@#{domain}",
|
"aliases" => [
|
||||||
"aliases": [
|
|
||||||
"https://#{domain}/users/#{nickname}"
|
"https://#{domain}/users/#{nickname}"
|
||||||
],
|
],
|
||||||
"links": [
|
"links" => [
|
||||||
{
|
%{
|
||||||
"rel": "http://webfinger.net/rel/profile-page",
|
"rel" => "http://webfinger.net/rel/profile-page",
|
||||||
"type": "text/html",
|
"type" => "text/html",
|
||||||
"href": "https://#{domain}/users/#{nickname}"
|
"href" => "https://#{domain}/users/#{nickname}"
|
||||||
},
|
},
|
||||||
{
|
%{
|
||||||
"rel": "self",
|
"rel" => "self",
|
||||||
"type": "application/activity+json",
|
"type" => "application/activity+json",
|
||||||
"href": "https://#{domain}/users/#{nickname}"
|
"href" => "https://#{domain}/users/#{nickname}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
"""
|
|
||||||
|
|
||||||
defp render_user(nickname, domain, name, summary, public_key),
|
defp render_user(nickname, domain, name, summary, public_key),
|
||||||
do: """
|
do: %{
|
||||||
{
|
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||||
"@context": [
|
"id" => "https://#{domain}/users/#{nickname}",
|
||||||
"https://www.w3.org/ns/activitystreams",
|
"type" => "Person",
|
||||||
"https://#{domain}/schemas/litepub-0.1.jsonld",
|
"url" => "https://#{domain}/users/#{nickname}",
|
||||||
{
|
"name" => name,
|
||||||
"@language": "und"
|
"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),
|
defp render_following(nickname, domain) do
|
||||||
do: """
|
following = Actor.get_my_follows()
|
||||||
{
|
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
|
||||||
"id": "https://#{domain}/users/#{nickname}/following",
|
|
||||||
"type": "OrderedCollection",
|
|
||||||
"totalItems": 0,
|
|
||||||
"orderedItems": [
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
defp render_followers(nickname, domain),
|
%{
|
||||||
do: """
|
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||||
{
|
"id" => "https://#{domain}/users/#{nickname}/following",
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"type" => "OrderedCollection",
|
||||||
"id": "https://#{domain}/users/#{nickname}/followers",
|
"totalItems" => length(following),
|
||||||
"type": "OrderedCollection",
|
"orderedItems" => following
|
||||||
"totalItems": 0,
|
|
||||||
"orderedItems": [
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
"""
|
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
|
get "/.well-known/webfinger" do
|
||||||
conn = Plug.Conn.fetch_query_params(conn)
|
conn = Plug.Conn.fetch_query_params(conn)
|
||||||
|
@ -148,7 +121,7 @@ defmodule Vonbraun.MyRouter do
|
||||||
"https://#{domain}/users/#{nickname}"
|
"https://#{domain}/users/#{nickname}"
|
||||||
]} do
|
]} do
|
||||||
conn = Plug.Conn.put_resp_content_type(conn, "application/jrd+json")
|
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
|
else
|
||||||
{:resource, resource} ->
|
{:resource, resource} ->
|
||||||
Logger.error("not a binary resource: #{resource}")
|
Logger.error("not a binary resource: #{resource}")
|
||||||
|
@ -167,10 +140,15 @@ defmodule Vonbraun.MyRouter do
|
||||||
domain = Application.fetch_env!(:vonbraun, :domain)
|
domain = Application.fetch_env!(:vonbraun, :domain)
|
||||||
name = Application.fetch_env!(:vonbraun, :name)
|
name = Application.fetch_env!(:vonbraun, :name)
|
||||||
summary = Application.fetch_env!(:vonbraun, :summary)
|
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")
|
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
|
else
|
||||||
send_resp(conn, 404, "fuck off")
|
send_resp(conn, 404, "fuck off")
|
||||||
end
|
end
|
||||||
|
@ -183,7 +161,7 @@ defmodule Vonbraun.MyRouter do
|
||||||
domain = Application.fetch_env!(:vonbraun, :domain)
|
domain = Application.fetch_env!(:vonbraun, :domain)
|
||||||
|
|
||||||
conn = Plug.Conn.put_resp_content_type(conn, "application/activity+json")
|
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
|
else
|
||||||
send_resp(conn, 404, "fuck off")
|
send_resp(conn, 404, "fuck off")
|
||||||
end
|
end
|
||||||
|
@ -196,7 +174,7 @@ defmodule Vonbraun.MyRouter do
|
||||||
domain = Application.fetch_env!(:vonbraun, :domain)
|
domain = Application.fetch_env!(:vonbraun, :domain)
|
||||||
|
|
||||||
conn = Plug.Conn.put_resp_content_type(conn, "application/activity+json")
|
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
|
else
|
||||||
send_resp(conn, 404, "fuck off")
|
send_resp(conn, 404, "fuck off")
|
||||||
end
|
end
|
||||||
|
@ -212,14 +190,14 @@ defmodule Vonbraun.MyRouter do
|
||||||
with {:user, ^nickname} <- {:user, user},
|
with {:user, ^nickname} <- {:user, user},
|
||||||
{:page, "true"} <- {:page, conn.params["page"]} do
|
{:page, "true"} <- {:page, conn.params["page"]} do
|
||||||
conn = Plug.Conn.put_resp_content_type(conn, "application/activity+json")
|
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
|
else
|
||||||
{:user, _} ->
|
{:user, _} ->
|
||||||
send_resp(conn, 404, "fuck off")
|
send_resp(conn, 404, "fuck off")
|
||||||
|
|
||||||
{:page, _} ->
|
{:page, _} ->
|
||||||
conn = Plug.Conn.put_resp_content_type(conn, "application/activity+json")
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ defmodule Vonbraun.Repo.Migrations.Initial do
|
||||||
add(:id, :string, primary_key: true)
|
add(:id, :string, primary_key: true)
|
||||||
add(:muted, :boolean)
|
add(:muted, :boolean)
|
||||||
add(:blocked, :naive_datetime)
|
add(:blocked, :naive_datetime)
|
||||||
|
add(:blocks_me, :naive_datetime)
|
||||||
add(:follows_me_state, :string)
|
add(:follows_me_state, :string)
|
||||||
add(:follows_me_ts, :naive_datetime)
|
add(:follows_me_ts, :naive_datetime)
|
||||||
add(:following_state, :string)
|
add(:following_state, :string)
|
||||||
|
|
Loading…
Reference in New Issue