@@ -5,8 +5,8 @@ defmodule Eventos.Service.ActivityPub do
|
||||
alias Eventos.Service.WebFinger
|
||||
alias Eventos.Activity
|
||||
|
||||
alias Eventos.Accounts
|
||||
alias Eventos.Accounts.Account
|
||||
alias Eventos.Actors
|
||||
alias Eventos.Actors.Actor
|
||||
|
||||
alias Eventos.Service.Federator
|
||||
|
||||
@@ -83,8 +83,12 @@ defmodule Eventos.Service.ActivityPub do
|
||||
),
|
||||
{:ok, activity} <- insert(create_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
# {:ok, actor} <- Accounts.increase_event_count(actor) do
|
||||
# {:ok, actor} <- Actors.increase_event_count(actor) do
|
||||
{:ok, activity}
|
||||
else
|
||||
err ->
|
||||
Logger.debug("Something went wrong")
|
||||
Logger.debug(inspect err)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -124,13 +128,13 @@ defmodule Eventos.Service.ActivityPub do
|
||||
end
|
||||
end
|
||||
|
||||
def delete(%Event{url: url, organizer_account: account} = event, local \\ true) do
|
||||
def delete(%Event{url: url, organizer_actor: actor} = event, local \\ true) do
|
||||
|
||||
data = %{
|
||||
"type" => "Delete",
|
||||
"actor" => account.url,
|
||||
"actor" => actor.url,
|
||||
"object" => url,
|
||||
"to" => [account.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
|
||||
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
|
||||
}
|
||||
|
||||
with Events.delete_event(event),
|
||||
@@ -141,40 +145,43 @@ defmodule Eventos.Service.ActivityPub do
|
||||
end
|
||||
end
|
||||
|
||||
def create_public_activities(%Account{} = account) do
|
||||
def create_public_activities(%Actor{} = actor) do
|
||||
|
||||
end
|
||||
|
||||
def make_account_from_url(url) do
|
||||
def make_actor_from_url(url) do
|
||||
with {:ok, data} <- fetch_and_prepare_user_from_url(url) do
|
||||
Accounts.insert_or_update_account(data)
|
||||
Actors.insert_or_update_actor(data)
|
||||
else
|
||||
e ->
|
||||
Logger.error("Failed to make account from url")
|
||||
Logger.error("Failed to make actor from url")
|
||||
Logger.error(inspect e)
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def make_account_from_nickname(nickname) do
|
||||
def make_actor_from_nickname(nickname) do
|
||||
with {:ok, %{"url" => url}} when not is_nil(url) <- WebFinger.finger(nickname) do
|
||||
make_account_from_url(url)
|
||||
make_actor_from_url(url)
|
||||
else
|
||||
_e -> {:error, "No ActivityPub URL found in WebFinger"}
|
||||
end
|
||||
end
|
||||
|
||||
def publish(actor, activity) do
|
||||
# followers =
|
||||
# if actor.follower_address in activity.recipients do
|
||||
# {:ok, followers} = User.get_followers(actor)
|
||||
# followers |> Enum.filter(&(!&1.local))
|
||||
# else
|
||||
# []
|
||||
# end
|
||||
followers = ["http://localhost:3000/users/tcit/inbox"]
|
||||
Logger.debug("Publishing an activity")
|
||||
followers =
|
||||
if actor.followers_url in activity.recipients do
|
||||
{:ok, followers} = Actor.get_followers(actor)
|
||||
followers |> Enum.filter(fn follower -> is_nil(follower.domain) end)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
remote_inboxes = followers
|
||||
remote_inboxes =
|
||||
followers
|
||||
|> Enum.map(fn follower -> follower.shared_inbox_url end)
|
||||
|> Enum.uniq()
|
||||
|
||||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
json = Jason.encode!(data)
|
||||
@@ -219,6 +226,8 @@ defmodule Eventos.Service.ActivityPub do
|
||||
end
|
||||
|
||||
def user_data_from_user_object(data) do
|
||||
Logger.debug("user_data_from_user_object")
|
||||
Logger.debug(inspect data)
|
||||
avatar =
|
||||
data["icon"]["url"] &&
|
||||
%{
|
||||
@@ -241,19 +250,27 @@ defmodule Eventos.Service.ActivityPub do
|
||||
"banner" => banner
|
||||
},
|
||||
avatar: avatar,
|
||||
username: "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}",
|
||||
display_name: data["name"],
|
||||
name: data["name"],
|
||||
preferred_username: data["preferredUsername"],
|
||||
follower_address: data["followers"],
|
||||
description: data["summary"],
|
||||
summary: data["summary"],
|
||||
public_key: data["publicKey"]["publicKeyPem"],
|
||||
inbox_url: data["inbox"],
|
||||
outbox_url: data["outbox"],
|
||||
following_url: data["following"],
|
||||
followers_url: data["followers"],
|
||||
shared_inbox_url: data["sharedInbox"],
|
||||
domain: URI.parse(data["id"]).host,
|
||||
manually_approves_followers: data["manuallyApprovesFollowers"],
|
||||
type: data["type"],
|
||||
}
|
||||
|
||||
{:ok, user_data}
|
||||
end
|
||||
|
||||
@spec fetch_public_activities_for_account(Account.t, integer(), integer()) :: list()
|
||||
def fetch_public_activities_for_account(%Account{} = account, page \\ 10, limit \\ 1) do
|
||||
{:ok, events, total} = Events.get_events_for_account(account, page, limit)
|
||||
@spec fetch_public_activities_for_actor(Actor.t, integer(), integer()) :: list()
|
||||
def fetch_public_activities_for_actor(%Actor{} = actor, page \\ 10, limit \\ 1) do
|
||||
{:ok, events, total} = Events.get_events_for_actor(actor, page, limit)
|
||||
activities = Enum.map(events, fn event ->
|
||||
{:ok, activity} = event_to_activity(event)
|
||||
activity
|
||||
@@ -265,7 +282,7 @@ defmodule Eventos.Service.ActivityPub do
|
||||
activity = %Activity{
|
||||
data: event,
|
||||
local: true,
|
||||
actor: event.organizer_account.url,
|
||||
actor: event.organizer_actor.url,
|
||||
recipients: ["https://www.w3.org/ns/activitystreams#Public"]
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ defmodule Eventos.Service.ActivityPub.Transmogrifier do
|
||||
@moduledoc """
|
||||
A module to handle coding from internal to wire ActivityPub and back.
|
||||
"""
|
||||
alias Eventos.Accounts.Account
|
||||
alias Eventos.Accounts
|
||||
alias Eventos.Actors.Actor
|
||||
alias Eventos.Actors
|
||||
alias Eventos.Events.Event
|
||||
alias Eventos.Service.ActivityPub
|
||||
|
||||
@@ -101,13 +101,15 @@ defmodule Eventos.Service.ActivityPub.Transmogrifier do
|
||||
# - tags
|
||||
# - emoji
|
||||
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
|
||||
with %Account{} = account <- Account.get_or_fetch_by_url(data["actor"]) do
|
||||
Logger.debug("Handle incoming to create notes")
|
||||
with %Actor{} = actor <- Actor.get_or_fetch_by_url(data["actor"]) do
|
||||
Logger.debug("found actor")
|
||||
object = fix_object(data["object"])
|
||||
|
||||
params = %{
|
||||
to: data["to"],
|
||||
object: object,
|
||||
actor: account,
|
||||
actor: actor,
|
||||
context: object["conversation"],
|
||||
local: false,
|
||||
published: data["published"],
|
||||
@@ -122,15 +124,13 @@ defmodule Eventos.Service.ActivityPub.Transmogrifier do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
|
||||
) do
|
||||
with %Account{} = followed <- Accounts.get_account_by_url(followed),
|
||||
%Account{} = follower <- Accounts.get_or_fetch_by_url(follower),
|
||||
def handle_incoming(%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data) do
|
||||
with %Actor{} = followed <- Actors.get_actor_by_url(followed),
|
||||
%Actor{} = follower <- Actors.get_or_fetch_by_url(follower),
|
||||
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
|
||||
ActivityPub.accept(%{to: [follower.url], actor: followed.url, object: data, local: true})
|
||||
|
||||
#Accounts.follow(follower, followed)
|
||||
#Actors.follow(follower, followed)
|
||||
{:ok, activity}
|
||||
else
|
||||
_e -> :error
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
defmodule Eventos.Service.ActivityPub.Utils do
|
||||
alias Eventos.Repo
|
||||
alias Eventos.Accounts
|
||||
alias Eventos.Accounts.Account
|
||||
alias Eventos.Actors
|
||||
alias Eventos.Actors.Actor
|
||||
alias Eventos.Events.Event
|
||||
alias Eventos.Events
|
||||
alias Eventos.Activity
|
||||
@@ -126,8 +126,11 @@ defmodule Eventos.Service.ActivityPub.Utils do
|
||||
"""
|
||||
def insert_full_object(%{"object" => %{"type" => type} = object_data})
|
||||
when is_map(object_data) and type == "Note" do
|
||||
account = Accounts.get_account_by_url(object_data["actor"])
|
||||
data = %{"text" => object_data["content"], "url" => object_data["url"], "account_id" => account.id, "in_reply_to_comment_id" => object_data["inReplyTo"]}
|
||||
import Logger
|
||||
Logger.debug("insert full object")
|
||||
Logger.debug(inspect object_data)
|
||||
actor = Actors.get_actor_by_url(object_data["actor"])
|
||||
data = %{"text" => object_data["content"], "url" => object_data["id"], "actor_id" => actor.id, "in_reply_to_comment_id" => object_data["inReplyTo"]}
|
||||
with {:ok, _} <- Events.create_comment(data) do
|
||||
:ok
|
||||
end
|
||||
@@ -173,7 +176,7 @@ defmodule Eventos.Service.ActivityPub.Utils do
|
||||
# Repo.one(query)
|
||||
# end
|
||||
|
||||
def make_like_data(%Account{url: url} = actor, %{data: %{"id" => id}} = object, activity_id) do
|
||||
def make_like_data(%Actor{url: url} = actor, %{data: %{"id" => id}} = object, activity_id) do
|
||||
data = %{
|
||||
"type" => "Like",
|
||||
"actor" => url,
|
||||
@@ -218,7 +221,7 @@ defmodule Eventos.Service.ActivityPub.Utils do
|
||||
@doc """
|
||||
Makes a follow activity data for the given follower and followed
|
||||
"""
|
||||
def make_follow_data(%Account{url: follower_id}, %Account{url: followed_id}, activity_id) do
|
||||
def make_follow_data(%Actor{url: follower_id}, %Actor{url: followed_id}, activity_id) do
|
||||
data = %{
|
||||
"type" => "Follow",
|
||||
"actor" => follower_id,
|
||||
@@ -230,7 +233,7 @@ defmodule Eventos.Service.ActivityPub.Utils do
|
||||
if activity_id, do: Map.put(data, "id", activity_id), else: data
|
||||
end
|
||||
|
||||
# def fetch_latest_follow(%Account{url: follower_id}, %Account{url: followed_id}) do
|
||||
# def fetch_latest_follow(%Actor{url: follower_id}, %Actor{url: followed_id}) do
|
||||
# query =
|
||||
# from(
|
||||
# activity in Activity,
|
||||
@@ -253,7 +256,7 @@ defmodule Eventos.Service.ActivityPub.Utils do
|
||||
Make announce activity data for the given actor and object
|
||||
"""
|
||||
def make_announce_data(
|
||||
%Account{url: url} = user,
|
||||
%Actor{url: url} = user,
|
||||
%Event{id: id} = object,
|
||||
activity_id
|
||||
) do
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
defmodule Eventos.Service.Federator do
|
||||
use GenServer
|
||||
alias Eventos.Accounts
|
||||
alias Eventos.Actors
|
||||
alias Eventos.Activity
|
||||
alias Eventos.Service.ActivityPub
|
||||
alias Eventos.Service.ActivityPub.Transmogrifier
|
||||
@@ -33,7 +33,7 @@ defmodule Eventos.Service.Federator do
|
||||
Logger.debug(inspect activity)
|
||||
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
|
||||
|
||||
with actor when not is_nil(actor) <- Accounts.get_account_by_url(activity.data["actor"]) do
|
||||
with actor when not is_nil(actor) <- Actors.get_actor_by_url(activity.data["actor"]) do
|
||||
|
||||
Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end)
|
||||
ActivityPub.publish(actor, activity)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# https://tools.ietf.org/html/draft-cavage-http-signatures-08
|
||||
defmodule Eventos.Service.HTTPSignatures do
|
||||
alias Eventos.Accounts.Account
|
||||
alias Eventos.Actors.Actor
|
||||
alias Eventos.Service.ActivityPub
|
||||
require Logger
|
||||
|
||||
@@ -25,52 +25,44 @@ defmodule Eventos.Service.HTTPSignatures do
|
||||
Logger.debug("Signature: #{signature["signature"]}")
|
||||
Logger.debug("Sigstring: #{sigstring}")
|
||||
{:ok, sig} = Base.decode64(signature["signature"])
|
||||
Logger.debug(inspect sig)
|
||||
Logger.debug(inspect public_key)
|
||||
case ExPublicKey.verify(sigstring, sig, public_key) do
|
||||
{:ok, sig_valid} ->
|
||||
sig_valid
|
||||
{:error, err} ->
|
||||
Logger.error(err)
|
||||
false
|
||||
end
|
||||
:public_key.verify(sigstring, :sha256, sig, public_key)
|
||||
end
|
||||
|
||||
def validate_conn(conn) do
|
||||
# TODO: How to get the right key and see if it is actually valid for that request.
|
||||
# For now, fetch the key for the actor.
|
||||
with actor_id <- conn.params["actor"],
|
||||
{:ok, public_key} <- Account.get_public_key_for_url(actor_id) do
|
||||
case HTTPSign.verify(conn, public_key) do
|
||||
{:ok, conn} ->
|
||||
true
|
||||
_ ->
|
||||
Logger.debug("Could not validate, re-fetching user and trying one more time")
|
||||
{:ok, public_key_code} <- Actor.get_public_key_for_url(actor_id),
|
||||
[public_key] = :public_key.pem_decode(public_key_code),
|
||||
public_key = :public_key.pem_entry_decode(public_key) do
|
||||
if validate_conn(conn, public_key) do
|
||||
true
|
||||
else
|
||||
Logger.info("Could not validate request, re-fetching user and trying one more time")
|
||||
# Fetch user anew and try one more time
|
||||
with actor_id <- conn.params["actor"],
|
||||
{:ok, _user} <- ActivityPub.make_account_from_url(actor_id),
|
||||
{:ok, public_key} <- Account.get_public_key_for_url(actor_id) do
|
||||
case HTTPSign.verify(conn, public_key) do
|
||||
{:ok, conn} ->
|
||||
true
|
||||
{:error, :forbidden} ->
|
||||
false
|
||||
end
|
||||
{:ok, _actor} <- ActivityPub.make_actor_from_url(actor_id),
|
||||
{:ok, public_key_code} <- Actor.get_public_key_for_url(actor_id),
|
||||
[public_key] = :public_key.pem_decode(public_key_code),
|
||||
public_key = :public_key.pem_entry_decode(public_key) do
|
||||
validate_conn(conn, public_key)
|
||||
end
|
||||
end
|
||||
else
|
||||
e ->
|
||||
Logger.debug("Could not public key!")
|
||||
Logger.debug("Could not found url for actor!")
|
||||
Logger.debug(inspect e)
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# def validate_conn(conn, public_key) do
|
||||
# headers = Enum.into(conn.req_headers, %{})
|
||||
# signature = split_signature(headers["signature"])
|
||||
# validate(headers, signature, public_key)
|
||||
# end
|
||||
def validate_conn(conn, public_key) do
|
||||
headers = Enum.into(conn.req_headers, %{})
|
||||
[host_without_port, _] = String.split(headers["host"], ":")
|
||||
headers = Map.put(headers, "host", host_without_port)
|
||||
signature = split_signature(headers["signature"])
|
||||
validate(headers, signature, public_key)
|
||||
end
|
||||
|
||||
def build_signing_string(headers, used_headers) do
|
||||
used_headers
|
||||
@@ -78,35 +70,24 @@ defmodule Eventos.Service.HTTPSignatures do
|
||||
|> Enum.join("\n")
|
||||
end
|
||||
|
||||
def sign(account, headers) do
|
||||
sigstring = build_signing_string(headers, Map.keys(headers))
|
||||
def sign(actor, headers) do
|
||||
with {:ok, private_key_code} = Actor.get_private_key_for_actor(actor),
|
||||
[private_key] = :public_key.pem_decode(private_key_code),
|
||||
private_key = :public_key.pem_entry_decode(private_key) do
|
||||
sigstring = build_signing_string(headers, Map.keys(headers))
|
||||
|
||||
{:ok, private_key} = Account.get_private_key_for_account(account)
|
||||
signature =
|
||||
:public_key.sign(sigstring, :sha256, private_key)
|
||||
|> Base.encode64()
|
||||
|
||||
Logger.debug("private_key")
|
||||
Logger.debug(inspect private_key)
|
||||
Logger.debug("sigstring")
|
||||
Logger.debug(inspect sigstring)
|
||||
{:ok, signature} = HTTPSign.Crypto.sign(:rsa, sigstring, private_key)
|
||||
Logger.debug(inspect signature)
|
||||
|
||||
signature = Base.encode64(signature)
|
||||
|
||||
sign = [
|
||||
keyId: account.url <> "#main-key",
|
||||
algorithm: "rsa-sha256",
|
||||
headers: Map.keys(headers) |> Enum.join(" "),
|
||||
signature: signature
|
||||
]
|
||||
|> Enum.map(fn {k, v} -> "#{k}=\"#{v}\"" end)
|
||||
|> Enum.join(",")
|
||||
|
||||
Logger.debug("sign")
|
||||
Logger.debug(inspect sign)
|
||||
{:ok, public_key} = Account.get_public_key_for_account(account)
|
||||
Logger.debug("inspect split signature inside sign")
|
||||
Logger.debug(inspect split_signature(sign))
|
||||
Logger.debug(inspect validate(headers, split_signature(sign), public_key))
|
||||
sign
|
||||
[
|
||||
keyId: actor.url <> "#main-key",
|
||||
algorithm: "rsa-sha256",
|
||||
headers: Map.keys(headers) |> Enum.join(" "),
|
||||
signature: signature
|
||||
]
|
||||
|> Enum.map(fn {k, v} -> "#{k}=\"#{v}\"" end)
|
||||
|> Enum.join(",")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
defmodule Eventos.Service.WebFinger do
|
||||
|
||||
alias Eventos.Accounts
|
||||
alias Eventos.Actors
|
||||
alias Eventos.Service.XmlBuilder
|
||||
alias Eventos.Repo
|
||||
require Jason
|
||||
@@ -26,14 +26,14 @@ defmodule Eventos.Service.WebFinger do
|
||||
|
||||
def webfinger(resource, "JSON") do
|
||||
host = EventosWeb.Endpoint.host()
|
||||
regex = ~r/(acct:)?(?<username>\w+)@#{host}/
|
||||
regex = ~r/(acct:)?(?<name>\w+)@#{host}/
|
||||
|
||||
with %{"username" => username} <- Regex.named_captures(regex, resource) do
|
||||
user = Accounts.get_account_by_username(username)
|
||||
with %{"name" => name} <- Regex.named_captures(regex, resource) do
|
||||
user = Actors.get_local_actor_by_name(name)
|
||||
{:ok, represent_user(user, "JSON")}
|
||||
else
|
||||
_e ->
|
||||
with user when not is_nil(user) <- Accounts.get_account_by_url(resource) do
|
||||
with user when not is_nil(user) <- Actors.get_actor_by_url(resource) do
|
||||
{:ok, represent_user(user, "JSON")}
|
||||
else
|
||||
_e ->
|
||||
@@ -44,7 +44,7 @@ defmodule Eventos.Service.WebFinger do
|
||||
|
||||
def represent_user(user, "JSON") do
|
||||
%{
|
||||
"subject" => "acct:#{user.username}@#{EventosWeb.Endpoint.host() <> ":4001"}",
|
||||
"subject" => "acct:#{user.preferred_username}@#{EventosWeb.Endpoint.host() <> ":4001"}",
|
||||
"aliases" => [user.url],
|
||||
"links" => [
|
||||
%{"rel" => "self", "type" => "application/activity+json", "href" => user.url},
|
||||
@@ -67,18 +67,18 @@ defmodule Eventos.Service.WebFinger do
|
||||
{:ok, data}
|
||||
end
|
||||
|
||||
def finger(account) do
|
||||
account = String.trim_leading(account, "@")
|
||||
def finger(actor) do
|
||||
actor = String.trim_leading(actor, "@")
|
||||
|
||||
domain =
|
||||
with [_name, domain] <- String.split(account, "@") do
|
||||
with [_name, domain] <- String.split(actor, "@") do
|
||||
domain
|
||||
else
|
||||
_e ->
|
||||
URI.parse(account).host
|
||||
URI.parse(actor).host
|
||||
end
|
||||
|
||||
address = "http://#{domain}/.well-known/webfinger?resource=acct:#{account}"
|
||||
address = "http://#{domain}/.well-known/webfinger?resource=acct:#{actor}"
|
||||
|
||||
with response <- HTTPoison.get(address, [Accept: "application/json"],follow_redirect: true),
|
||||
{:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <- response do
|
||||
@@ -86,7 +86,7 @@ defmodule Eventos.Service.WebFinger do
|
||||
webfinger_from_json(doc)
|
||||
else
|
||||
e ->
|
||||
Logger.debug(fn -> "Couldn't finger #{account}" end)
|
||||
Logger.debug(fn -> "Couldn't finger #{actor}" end)
|
||||
Logger.debug(fn -> inspect(e) end)
|
||||
{:error, e}
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user