Introduce relay

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2019-07-30 16:40:59 +02:00
parent 56467301a1
commit c51115bdbe
54 changed files with 3100 additions and 1038 deletions

View File

@@ -22,6 +22,11 @@ defmodule Mix.Tasks.Mobilizon.Common do
end
end
def start_mobilizon do
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
{:ok, _} = Application.ensure_all_started(:mobilizon)
end
def escape_sh_path(path) do
~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
end

View File

@@ -0,0 +1,65 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/mix/tasks/pleroma/relay.ex
defmodule Mix.Tasks.Mobilizon.Relay do
use Mix.Task
alias Mobilizon.Service.ActivityPub.Relay
alias Mix.Tasks.Mobilizon.Common
@shortdoc "Manages remote relays"
@moduledoc """
Manages remote relays
## Follow a remote relay
``mix mobilizon.relay follow <relay_url>``
Example: ``mix mobilizon.relay follow https://example.org/relay``
## Unfollow a remote relay
``mix mobilizon.relay unfollow <relay_url>``
Example: ``mix mobilizon.relay unfollow https://example.org/relay``
"""
def run(["follow", target]) do
Common.start_mobilizon()
case Relay.follow(target) do
{:ok, _activity} ->
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
{:error, e} ->
IO.puts(:stderr, "Error while following #{target}: #{inspect(e)}")
end
end
def run(["unfollow", target]) do
Common.start_mobilizon()
case Relay.unfollow(target) do
{:ok, _activity} ->
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
{:error, e} ->
IO.puts(:stderr, "Error while unfollowing #{target}: #{inspect(e)}")
end
end
def run(["accept", target]) do
Common.start_mobilizon()
case Relay.accept(target) do
{:ok, _activity} ->
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
{:error, e} ->
IO.puts(:stderr, "Error while accept #{target} follow: #{inspect(e)}")
end
end
end

View File

@@ -163,7 +163,6 @@ defmodule Mobilizon.Actors.Actor do
])
|> validate_required([
:url,
:outbox_url,
:inbox_url,
:type,
:domain,
@@ -184,6 +183,44 @@ defmodule Mobilizon.Actors.Actor do
changes
end
def relay_creation(%{url: url, preferred_username: preferred_username} = _params) do
key = :public_key.generate_key({:rsa, 2048, 65_537})
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
pem = [entry] |> :public_key.pem_encode() |> String.trim_trailing()
vars = %{
"name" => Mobilizon.CommonConfig.get([:instance, :name], "Mobilizon"),
"summary" =>
Mobilizon.CommonConfig.get(
[:instance, :description],
"An internal service actor for this Mobilizon instance"
),
"url" => url,
"keys" => pem,
"preferred_username" => preferred_username,
"domain" => nil,
"inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
"followers_url" => "#{url}/followers",
"following_url" => "#{url}/following",
"shared_inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
"type" => :Application
}
cast(%Actor{}, vars, [
:type,
:name,
:summary,
:url,
:keys,
:preferred_username,
:domain,
:inbox_url,
:followers_url,
:following_url,
:shared_inbox_url
])
end
@doc """
Changeset for group creation
"""
@@ -240,6 +277,14 @@ defmodule Mobilizon.Actors.Actor do
:outbox_url,
build_url(username, :outbox)
)
|> put_change(
:followers_url,
build_url(username, :followers)
)
|> put_change(
:following_url,
build_url(username, :following)
)
|> put_change(
:inbox_url,
build_url(username, :inbox)
@@ -325,18 +370,30 @@ defmodule Mobilizon.Actors.Actor do
%{total: Task.await(total), elements: Task.await(elements)}
end
@spec get_full_followers(struct()) :: list()
def get_full_followers(%Actor{id: actor_id} = _actor) do
Repo.all(
from(
a in Actor,
join: f in Follower,
on: a.id == f.actor_id,
where: f.target_actor_id == ^actor_id
)
defp get_full_followers_query(%Actor{id: actor_id} = _actor) do
from(
a in Actor,
join: f in Follower,
on: a.id == f.actor_id,
where: f.target_actor_id == ^actor_id
)
end
@spec get_full_followers(struct()) :: list()
def get_full_followers(%Actor{} = actor) do
actor
|> get_full_followers_query()
|> Repo.all()
end
@spec get_full_external_followers(struct()) :: list()
def get_full_external_followers(%Actor{} = actor) do
actor
|> get_full_followers_query()
|> where([a], not is_nil(a.domain))
|> Repo.all()
end
@doc """
Get followings from an actor
@@ -404,18 +461,19 @@ defmodule Mobilizon.Actors.Actor do
Make an actor follow another
"""
@spec follow(struct(), struct(), boolean()) :: Follower.t() | {:error, String.t()}
def follow(%Actor{} = followed, %Actor{} = follower, approved \\ true) do
def follow(%Actor{} = followed, %Actor{} = follower, url \\ nil, approved \\ true) do
with {:suspended, false} <- {:suspended, followed.suspended},
# Check if followed has blocked follower
{:already_following, false} <- {:already_following, following?(follower, followed)} do
do_follow(follower, followed, approved)
do_follow(follower, followed, approved, url)
else
{:already_following, %Follower{}} ->
{:error,
{:error, :already_following,
"Could not follow actor: you are already following #{followed.preferred_username}"}
{:suspended, _} ->
{:error, "Could not follow actor: #{followed.preferred_username} has been suspended"}
{:error, :suspended,
"Could not follow actor: #{followed.preferred_username} has been suspended"}
end
end
@@ -433,13 +491,20 @@ defmodule Mobilizon.Actors.Actor do
end
end
@spec do_follow(struct(), struct(), boolean) ::
@spec do_follow(struct(), struct(), boolean(), String.t()) ::
{:ok, Follower.t()} | {:error, Ecto.Changeset.t()}
defp do_follow(%Actor{} = follower, %Actor{} = followed, approved) do
defp do_follow(%Actor{} = follower, %Actor{} = followed, approved, url) do
Logger.info(
"Making #{follower.preferred_username} follow #{followed.preferred_username} (approved: #{
approved
})"
)
Actors.create_follower(%{
"actor_id" => follower.id,
"target_actor_id" => followed.id,
"approved" => approved
"approved" => approved,
"url" => url
})
end

View File

@@ -297,7 +297,7 @@ defmodule Mobilizon.Actors do
{:ok, actor}
err ->
Logger.error(inspect(err))
Logger.debug(inspect(err))
{:error, err}
end
end
@@ -475,16 +475,16 @@ defmodule Mobilizon.Actors do
@spec get_or_fetch_by_url(String.t(), bool()) :: {:ok, Actor.t()} | {:error, String.t()}
def get_or_fetch_by_url(url, preload \\ false) do
case get_actor_by_url(url, preload) do
{:ok, actor} ->
{:ok, %Actor{} = actor} ->
{:ok, actor}
_ ->
case ActivityPub.make_actor_from_url(url, preload) do
{:ok, actor} ->
{:ok, %Actor{} = actor} ->
{:ok, actor}
_ ->
Logger.error("Could not fetch by AP id")
Logger.warn("Could not fetch by AP id")
{:error, "Could not fetch by AP id"}
end
end
@@ -655,6 +655,18 @@ defmodule Mobilizon.Actors do
end
end
def get_or_create_service_actor_by_url(url, preferred_username \\ "relay") do
case get_actor_by_url(url) do
{:ok, %Actor{} = actor} ->
{:ok, actor}
_ ->
%{url: url, preferred_username: preferred_username}
|> Actor.relay_creation()
|> Repo.insert()
end
end
alias Mobilizon.Actors.Member
@doc """
@@ -895,7 +907,7 @@ defmodule Mobilizon.Actors do
end
@doc """
Get a follower by the followed actor and following actor
Get a follow by the followed actor and following actor
"""
@spec get_follower(Actor.t(), Actor.t()) :: Follower.t()
def get_follower(%Actor{id: followed_id}, %Actor{id: follower_id}) do
@@ -904,6 +916,19 @@ defmodule Mobilizon.Actors do
)
end
@doc """
Get a follow by the followed actor and following actor
"""
@spec get_follow_by_url(String.t()) :: Follower.t()
def get_follow_by_url(url) do
Repo.one(
from(f in Follower,
where: f.url == ^url,
preload: [:actor, :target_actor]
)
)
end
@doc """
Creates a follower.
@@ -1009,7 +1034,7 @@ defmodule Mobilizon.Actors do
{:error, error} ->
Logger.error("Error while removing an upload file")
Logger.error(inspect(error))
Logger.debug(inspect(error))
{:ok, actor}
end
end

View File

@@ -7,9 +7,11 @@ defmodule Mobilizon.Actors.Follower do
alias Mobilizon.Actors.Follower
alias Mobilizon.Actors.Actor
@primary_key {:id, :binary_id, autogenerate: true}
schema "followers" do
field(:approved, :boolean, default: false)
field(:score, :integer, default: 1000)
field(:url, :string)
belongs_to(:target_actor, Actor)
belongs_to(:actor, Actor)
end
@@ -17,8 +19,34 @@ defmodule Mobilizon.Actors.Follower do
@doc false
def changeset(%Follower{} = member, attrs) do
member
|> cast(attrs, [:score, :approved, :target_actor_id, :actor_id])
|> validate_required([:score, :approved, :target_actor_id, :actor_id])
|> cast(attrs, [:url, :approved, :target_actor_id, :actor_id])
|> generate_url()
|> validate_required([:url, :approved, :target_actor_id, :actor_id])
|> unique_constraint(:target_actor_id, name: :followers_actor_target_actor_unique_index)
end
# If there's a blank URL that's because we're doing the first insert
defp generate_url(%Ecto.Changeset{data: %Follower{url: nil}} = changeset) do
case fetch_change(changeset, :url) do
{:ok, _url} -> changeset
:error -> do_generate_url(changeset)
end
end
# Most time just go with the given URL
defp generate_url(%Ecto.Changeset{} = changeset), do: changeset
defp do_generate_url(%Ecto.Changeset{} = changeset) do
uuid = Ecto.UUID.generate()
changeset
|> put_change(
:url,
"#{MobilizonWeb.Endpoint.url()}/follow/#{uuid}"
)
|> put_change(
:id,
uuid
)
end
end

View File

@@ -32,7 +32,8 @@ defmodule Mobilizon.Events do
:tracks,
:tags,
:participants,
:physical_address
:physical_address,
:picture
]
)
|> paginate(page, limit)
@@ -248,7 +249,8 @@ defmodule Mobilizon.Events do
:tracks,
:tags,
:participants,
:physical_address
:physical_address,
:picture
]
)
)

View File

@@ -9,6 +9,8 @@ defmodule MobilizonWeb.API.Events do
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
alias MobilizonWeb.API.Utils
@visibility %{"PUBLIC" => :public, "PRIVATE" => :private}
@doc """
Create an event
"""

View File

@@ -0,0 +1,51 @@
defmodule MobilizonWeb.API.Follows do
@moduledoc """
Common API for following, unfollowing, accepting and rejecting stuff.
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Service.ActivityPub
require Logger
def follow(%Actor{} = follower, %Actor{} = followed) do
case ActivityPub.follow(follower, followed) do
{:ok, activity, _} ->
{:ok, activity}
e ->
Logger.warn("Error while following actor: #{inspect(e)}")
{:error, e}
end
end
def unfollow(%Actor{} = follower, %Actor{} = followed) do
case ActivityPub.unfollow(follower, followed) do
{:ok, activity, _} ->
{:ok, activity}
e ->
Logger.warn("Error while unfollowing actor: #{inspect(e)}")
{:error, e}
end
end
def accept(%Actor{} = follower, %Actor{} = followed) do
with %Follower{approved: false, id: follow_id, url: follow_url} = follow <-
Actor.following?(follower, followed),
activity_follow_url <- "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}",
data <-
ActivityPub.Utils.make_follow_data(followed, follower, follow_url),
{:ok, activity, _} <-
ActivityPub.accept(
%{to: [follower.url], actor: followed.url, object: data},
activity_follow_url
),
{:ok, %Follower{approved: true}} <- Actors.update_follower(follow, %{"approved" => true}) do
{:ok, activity}
else
%Follower{approved: true} ->
{:error, "Follow already accepted"}
end
end
end

View File

@@ -9,7 +9,7 @@ defmodule MobilizonWeb.API.Utils do
Determines the full audience based on mentions for a public audience
Audience is:
* `to` : the mentionned actors, the eventual actor we're replying to and the public
* `to` : the mentioned actors, the eventual actor we're replying to and the public
* `cc` : the actor's followers
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
@@ -72,7 +72,9 @@ defmodule MobilizonWeb.API.Utils do
end
end
def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []}
def get_to_and_cc(_actor, mentions, _inReplyTo, {:list, _}) do
{mentions, []}
end
# def get_addressed_users(_, to) when is_list(to) do
# Actors.get(to)
@@ -138,7 +140,7 @@ defmodule MobilizonWeb.API.Utils do
make_content_html(
content,
tags,
"text/plain"
"text/html"
),
mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.url),
addressed_users <- get_addressed_users(mentioned_users, nil),

View File

@@ -14,6 +14,19 @@ defmodule MobilizonWeb.ActivityPubController do
action_fallback(:errors)
plug(:relay_active? when action in [:relay])
def relay_active?(conn, _) do
if Mobilizon.CommonConfig.get([:instance, :allow_relay]) do
conn
else
conn
|> put_status(404)
|> json("Not found")
|> halt()
end
end
def following(conn, %{"name" => name, "page" => page}) do
with {page, ""} <- Integer.parse(page),
%Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do
@@ -67,6 +80,7 @@ defmodule MobilizonWeb.ActivityPubController do
# TODO: Ensure that this inbox is a recipient of the message
def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
Logger.debug("Got something with valid signature inside inbox")
Federator.enqueue(:incoming_ap_doc, params)
json(conn, "ok")
end
@@ -90,19 +104,35 @@ defmodule MobilizonWeb.ActivityPubController do
"Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
)
Logger.error(inspect(conn.req_headers))
Logger.debug(inspect(conn.req_headers))
end
json(conn, "error")
end
def relay(conn, _params) do
with {status, actor} <-
Cachex.fetch(
:activity_pub,
"relay_actor",
&Mobilizon.Service.ActivityPub.Relay.get_actor/0
),
true <- status in [:ok, :commit] do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("actor.json", %{actor: actor}))
end
end
def errors(conn, {:error, :not_found}) do
conn
|> put_status(404)
|> json("Not found")
end
def errors(conn, _e) do
def errors(conn, e) do
Logger.debug(inspect(e))
conn
|> put_status(500)
|> json("Unknown Error")

View File

@@ -10,7 +10,6 @@ defmodule MobilizonWeb.HTTPSignaturePlug do
Plug to check HTTP Signatures on every incoming request
"""
alias Mobilizon.Service.HTTPSignatures
import Plug.Conn
require Logger
@@ -23,32 +22,30 @@ defmodule MobilizonWeb.HTTPSignaturePlug do
end
def call(conn, _opts) do
actor = conn.params["actor"]
Logger.debug(fn ->
"Checking sig for #{actor}"
end)
[signature | _] = get_req_header(conn, "signature")
cond do
String.contains?(signature, actor) ->
conn =
conn
|> put_req_header(
"(request-target)",
String.downcase("#{conn.method}") <> " #{conn.request_path}"
)
assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
signature ->
Logger.debug("Signature not from actor")
assign(conn, :valid_signature, false)
true ->
Logger.debug("No signature header!")
if signature do
# set (request-target) header to the appropriate value
# we also replace the digest header with the one we computed
conn =
conn
|> put_req_header(
"(request-target)",
String.downcase("#{conn.method}") <> " #{conn.request_path}"
)
conn =
if conn.assigns[:digest] do
conn
|> put_req_header("digest", conn.assigns[:digest])
else
conn
end
assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
else
Logger.debug("No signature header!")
conn
end
end
end

View File

@@ -18,6 +18,10 @@ defmodule MobilizonWeb.Router do
plug(MobilizonWeb.HTTPSignaturePlug)
end
pipeline :relay do
plug(:accepts, ["activity-json", "json"])
end
pipeline :activity_pub do
plug(:accepts, ["activity-json"])
end
@@ -97,6 +101,13 @@ defmodule MobilizonWeb.Router do
post("/inbox", ActivityPubController, :inbox)
end
scope "/relay", MobilizonWeb do
pipe_through(:relay)
get("/", ActivityPubController, :relay)
post("/inbox", ActivityPubController, :inbox)
end
scope "/proxy/", MobilizonWeb do
pipe_through(:remote_media)

View File

@@ -12,12 +12,12 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
public_key = Mobilizon.Service.ActivityPub.Utils.pem_to_public_key_pem(actor.keys)
%{
"id" => Actor.build_url(actor.preferred_username, :page),
"type" => "Person",
"following" => Actor.build_url(actor.preferred_username, :following),
"followers" => Actor.build_url(actor.preferred_username, :followers),
"inbox" => Actor.build_url(actor.preferred_username, :inbox),
"outbox" => Actor.build_url(actor.preferred_username, :outbox),
"id" => actor.url,
"type" => to_string(actor.type),
"following" => actor.following_url,
"followers" => actor.followers_url,
"inbox" => actor.inbox_url,
"outbox" => actor.outbox_url,
"preferredUsername" => actor.preferred_username,
"name" => actor.name,
"summary" => actor.summary,

View File

@@ -31,8 +31,8 @@ defmodule MobilizonWeb.ErrorView do
# template is found, let's render it as 500
def template_not_found(template, assigns) do
require Logger
Logger.error("Template not found")
Logger.error(inspect(template))
Logger.warn("Template not found")
Logger.debug(inspect(template))
render("500.html", assigns)
end
end

View File

@@ -46,24 +46,8 @@ defmodule MobilizonWeb.PageView do
end
def render("event.activity-json", %{conn: %{assigns: %{object: event}}}) do
event = Mobilizon.Service.ActivityPub.Converters.Event.model_to_as(event)
{:ok, html, []} = Earmark.as_html(event["summary"])
%{
"type" => "Event",
"attributedTo" => event["actor"],
"id" => event["id"],
"name" => event["title"],
"category" => event["category"],
"content" => html,
"source" => %{
"content" => event["summary"],
"mediaType" => "text/markdown"
},
"mediaType" => "text/html",
"published" => event["publish_at"],
"updated" => event["updated_at"]
}
event
|> Mobilizon.Service.ActivityPub.Converters.Event.model_to_as()
|> Map.merge(Utils.make_json_ld_header())
end

View File

@@ -21,10 +21,11 @@ defmodule Mobilizon.Service.ActivityPub do
alias Mobilizon.Actors.Follower
alias Mobilizon.Service.Federator
alias Mobilizon.Service.HTTPSignatures
alias Mobilizon.Service.HTTPSignatures.Signature
require Logger
import Mobilizon.Service.ActivityPub.Utils
import Mobilizon.Service.ActivityPub.Visibility
@doc """
Get recipients for an activity or object
@@ -42,10 +43,6 @@ defmodule Mobilizon.Service.ActivityPub do
def insert(map, local \\ true) when is_map(map) do
with map <- lazy_put_activity_defaults(map),
{:ok, object} <- insert_full_object(map) do
object_id = if is_map(map["object"]), do: map["object"]["id"], else: map["id"]
map = if local, do: Map.put(map, "id", "#{object_id}/activity"), else: map
activity = %Activity{
data: map,
local: local,
@@ -94,7 +91,7 @@ defmodule Mobilizon.Service.ActivityPub do
{:ok, _activity, %{url: object_url} = _object} <- Transmogrifier.handle_incoming(params) do
case data["type"] do
"Event" ->
{:ok, Events.get_event_by_url!(object_url)}
{:ok, Events.get_event_full_by_url!(object_url)}
"Note" ->
{:ok, Events.get_comment_full_from_url!(object_url)}
@@ -107,15 +104,17 @@ defmodule Mobilizon.Service.ActivityPub do
end
else
{:existing_event, %Event{url: event_url}} ->
{:ok, Events.get_event_by_url!(event_url)}
{:ok, Events.get_event_full_by_url!(event_url)}
{:existing_comment, %Comment{url: comment_url}} ->
{:ok, Events.get_comment_full_from_url!(comment_url)}
{:existing_actor, %Actor{url: actor_url}} ->
{:existing_actor, {:ok, %Actor{url: actor_url}}} ->
{:ok, Actors.get_actor_by_url!(actor_url, true)}
e ->
require Logger
Logger.error(inspect(e))
{:error, e}
end
end
@@ -137,24 +136,40 @@ defmodule Mobilizon.Service.ActivityPub do
%{to: to, actor: actor, published: published, object: object},
additional
),
:ok <- Logger.debug(inspect(create_data)),
{:ok, activity, object} <- insert(create_data, local),
:ok <- maybe_federate(activity) do
# {:ok, actor} <- Actors.increase_event_count(actor) do
{:ok, activity, object}
else
err ->
Logger.error("Something went wrong")
Logger.error(inspect(err))
Logger.error("Something went wrong while creating an activity")
Logger.debug(inspect(err))
err
end
end
def accept(%{to: to, actor: actor, object: object} = params) do
def accept(%{to: to, actor: actor, object: object} = params, activity_follow_id \\ nil) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{"to" => to, "type" => "Accept", "actor" => actor, "object" => object},
with data <- %{
"to" => to,
"type" => "Accept",
"actor" => actor,
"object" => object,
"id" => activity_follow_id || get_url(object) <> "/activity"
},
{:ok, activity, object} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
end
end
def reject(%{to: to, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{"to" => to, "type" => "Reject", "actor" => actor.url, "object" => object},
{:ok, activity, object} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
@@ -168,6 +183,7 @@ defmodule Mobilizon.Service.ActivityPub do
with data <- %{
"to" => to,
"cc" => cc,
"id" => object["url"],
"type" => "Update",
"actor" => actor,
"object" => object
@@ -215,55 +231,56 @@ defmodule Mobilizon.Service.ActivityPub do
# end
# end
# def announce(
# %Actor{} = actor,
# object,
# activity_id \\ nil,
# local \\ true
# ) do
# #with true <- is_public?(object),
# with announce_data <- make_announce_data(actor, object, activity_id),
# {:ok, activity, object} <- insert(announce_data, local),
# # {:ok, object} <- add_announce_to_object(activity, object),
# :ok <- maybe_federate(activity) do
# {:ok, activity, object}
# else
# error -> {:error, error}
# end
# end
def announce(
%Actor{} = actor,
object,
activity_id \\ nil,
local \\ true,
public \\ true
) do
with true <- is_public?(object),
announce_data <- make_announce_data(actor, object, activity_id, public),
{:ok, activity, object} <- insert(announce_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
error ->
{:error, error}
end
end
# def unannounce(
# %Actor{} = actor,
# object,
# activity_id \\ nil,
# local \\ true
# ) do
# with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
# unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
# {:ok, unannounce_activity, _object} <- insert(unannounce_data, local),
# :ok <- maybe_federate(unannounce_activity),
# {:ok, _activity} <- Repo.delete(announce_activity),
# {:ok, object} <- remove_announce_from_object(announce_activity, object) do
# {:ok, unannounce_activity, object}
# else
# _e -> {:ok, object}
# end
# end
def unannounce(
%Actor{} = actor,
object,
activity_id \\ nil,
cancelled_activity_id \\ nil,
local \\ true
) do
with announce_activity <- make_announce_data(actor, object, cancelled_activity_id),
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
{:ok, unannounce_activity, _object} <- insert(unannounce_data, local),
:ok <- maybe_federate(unannounce_activity) do
{:ok, unannounce_activity, object}
else
_e -> {:ok, object}
end
end
@doc """
Make an actor follow another
"""
def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{id: follow_id}} <- Actor.follow(followed, follower, true),
with {:ok, %Follower{url: follow_url}} <-
Actor.follow(followed, follower, activity_id, false),
activity_follow_id <-
activity_id || "#{MobilizonWeb.Endpoint.url()}/follow/#{follow_id}/activity",
activity_id || follow_url,
data <- make_follow_data(followed, follower, activity_follow_id),
{:ok, activity, object} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
{err, _} when err in [:already_following, :suspended] ->
{:error, err}
{:error, err, msg} when err in [:already_following, :suspended] ->
{:error, msg}
end
end
@@ -271,18 +288,26 @@ defmodule Mobilizon.Service.ActivityPub do
Make an actor unfollow another
"""
@spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any()
def unfollow(%Actor{} = followed, %Actor{} = follower, activity_id \\ nil, local \\ true) do
def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{id: follow_id}} <- Actor.unfollow(followed, follower),
# We recreate the follow activity
data <- make_follow_data(followed, follower, follow_id),
data <-
make_follow_data(
followed,
follower,
"#{MobilizonWeb.Endpoint.url()}/follow/#{follow_id}/activity"
),
{:ok, follow_activity, _object} <- insert(data, local),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
activity_unfollow_id <-
activity_id || "#{MobilizonWeb.Endpoint.url()}/unfollow/#{follow_id}/activity",
unfollow_data <-
make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id),
{:ok, activity, object} <- insert(unfollow_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
err ->
Logger.error(inspect(err))
Logger.debug("Error while unfollowing an actor #{inspect(err)}")
err
end
end
@@ -294,7 +319,8 @@ defmodule Mobilizon.Service.ActivityPub do
"type" => "Delete",
"actor" => actor.url,
"object" => url,
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"],
"id" => url <> "/delete"
}
with {:ok, _} <- Events.delete_event(event),
@@ -309,6 +335,7 @@ defmodule Mobilizon.Service.ActivityPub do
"type" => "Delete",
"actor" => actor.url,
"object" => url,
"id" => url <> "/delete",
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
}
@@ -324,6 +351,7 @@ defmodule Mobilizon.Service.ActivityPub do
"type" => "Delete",
"actor" => url,
"object" => url,
"id" => url <> "/delete",
"to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
}
@@ -366,11 +394,11 @@ defmodule Mobilizon.Service.ActivityPub do
# Request returned 410
{:error, :actor_deleted} ->
Logger.info("Actor was deleted")
{:error, :actor_deleted}
e ->
Logger.error("Failed to make actor from url")
Logger.error(inspect(e))
Logger.warn("Failed to make actor from url")
{:error, e}
end
end
@@ -414,10 +442,18 @@ defmodule Mobilizon.Service.ActivityPub do
"""
def publish(actor, activity) do
Logger.debug("Publishing an activity")
Logger.debug(inspect(activity))
public = is_public?(activity)
if public && Mobilizon.CommonConfig.get([:instance, :allow_relay]) do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Mobilizon.Service.ActivityPub.Relay.publish(activity)
end
followers =
if actor.followers_url in activity.recipients do
Actor.get_full_followers(actor) |> Enum.filter(fn follower -> is_nil(follower.domain) end)
Actor.get_full_external_followers(actor)
else
[]
end
@@ -448,15 +484,16 @@ defmodule Mobilizon.Service.ActivityPub do
Logger.info("Federating #{id} to #{inbox}")
%URI{host: host, path: path} = URI.parse(inbox)
digest = HTTPSignatures.build_digest(json)
date = HTTPSignatures.generate_date_header()
request_target = HTTPSignatures.generate_request_target("POST", path)
digest = Signature.build_digest(json)
date = Signature.generate_date_header()
# request_target = Signature.generate_request_target("POST", path)
signature =
HTTPSignatures.sign(actor, %{
Signature.sign(actor, %{
host: host,
"content-length": byte_size(json),
"(request-target)": request_target,
# TODO : Look me up in depth why Pleroma handles this inside lib/mobilizon_web/http_signature.ex
# "(request-target)": request_target,
digest: digest,
date: date
})
@@ -478,20 +515,27 @@ defmodule Mobilizon.Service.ActivityPub do
@spec fetch_and_prepare_actor_from_url(String.t()) :: {:ok, struct()} | {:error, atom()} | any()
defp fetch_and_prepare_actor_from_url(url) do
Logger.debug("Fetching and preparing actor from url")
Logger.debug(inspect(url))
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
HTTPoison.get(url, [Accept: "application/activity+json"], follow_redirect: true),
{:ok, data} <- Jason.decode(body) do
actor_data_from_actor_object(data)
else
# Actor is gone, probably deleted
{:ok, %HTTPoison.Response{status_code: 410}} ->
{:error, :actor_deleted}
res =
with %HTTPoison.Response{status_code: 200, body: body} <-
HTTPoison.get!(url, [Accept: "application/activity+json"], follow_redirect: true),
:ok <- Logger.debug("response okay, now decoding json"),
{:ok, data} <- Jason.decode(body) do
Logger.debug("Got activity+json response at actor's endpoint, now converting data")
actor_data_from_actor_object(data)
else
# Actor is gone, probably deleted
{:ok, %HTTPoison.Response{status_code: 410}} ->
Logger.info("Response HTTP 410")
{:error, :actor_deleted}
e ->
Logger.error("Could not decode actor at fetch #{url}, #{inspect(e)}")
e
end
e ->
Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}")
{:error, e}
end
res
end
@doc """

View File

@@ -7,3 +7,10 @@ defmodule Mobilizon.Service.ActivityPub.Converter do
@callback as_to_model_data(map()) :: map()
@callback model_to_as(struct()) :: map()
end
defprotocol Mobilizon.Service.ActivityPub.Convertible do
@type activitystreams :: map()
@spec model_to_as(t) :: activitystreams
def model_to_as(convertible)
end

View File

@@ -45,3 +45,9 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Actor do
}
end
end
defimpl Mobilizon.Service.ActivityPub.Convertible, for: Mobilizon.Actors.Actor do
alias Mobilizon.Service.ActivityPub.Converters.Actor, as: ActorConverter
defdelegate model_to_as(actor), to: ActorConverter
end

View File

@@ -52,7 +52,29 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Address do
"""
@impl Converter
@spec model_to_as(AddressModel.t()) :: map()
def model_to_as(%AddressModel{} = _address) do
nil
def model_to_as(%AddressModel{} = address) do
res = %{
"type" => "Place",
"name" => address.description,
"id" => address.url,
"address" => %{
"type" => "PostalAddress",
"streetAddress" => address.street,
"postalCode" => address.postal_code,
"addressLocality" => address.locality,
"addressRegion" => address.region,
"addressCountry" => address.country
}
}
if is_nil(address.geom) do
res
else
Map.put(res, "geo", %{
"type" => "GeoCoordinates",
"latitude" => address.geom.coordinates |> elem(0),
"longitude" => address.geom.coordinates |> elem(1)
})
end
end
end

View File

@@ -84,7 +84,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
"actor" => comment.actor.url,
"attributedTo" => comment.actor.url,
"uuid" => comment.uuid,
"id" => Routes.page_url(Endpoint, :comment, comment.uuid)
"id" => comment.url
}
if comment.in_reply_to_comment do
@@ -94,3 +94,9 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
end
end
end
defimpl Mobilizon.Service.ActivityPub.Convertible, for: Mobilizon.Events.Comment do
alias Mobilizon.Service.ActivityPub.Converters.Comment, as: CommentConverter
defdelegate model_to_as(comment), to: CommentConverter
end

View File

@@ -10,6 +10,8 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event, as: EventModel
alias Mobilizon.Service.ActivityPub.Converter
alias Mobilizon.Service.ActivityPub.Converters.Address, as: AddressConverter
alias Mobilizon.Service.ActivityPub.Utils
alias Mobilizon.Events
alias Mobilizon.Events.Tag
alias Mobilizon.Addresses
@@ -26,6 +28,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
@spec as_to_model_data(map()) :: map()
def as_to_model_data(object) do
Logger.debug("event as_to_model_data")
Logger.debug(inspect(object))
with {:actor, {:ok, %Actor{id: actor_id}}} <-
{:actor, Actors.get_actor_by_url(object["actor"])},
@@ -99,6 +102,8 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
end
defp fetch_tags(tags) do
Logger.debug("fetching tags")
Enum.reduce(tags, [], fn tag, acc ->
with true <- tag["type"] == "Hashtag",
{:ok, %Tag{} = tag} <- Events.get_or_create_tag(tag) do
@@ -110,23 +115,62 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
end)
end
defp build_tags(tags) do
Enum.map(tags, fn %Tag{} = tag ->
%{
"href" => MobilizonWeb.Endpoint.url() <> "/tags/#{tag.slug}",
"name" => "##{tag.title}",
"type" => "Hashtag"
}
end)
end
@doc """
Convert an event struct to an ActivityStream representation
"""
@impl Converter
@spec model_to_as(EventModel.t()) :: map()
def model_to_as(%EventModel{} = event) do
%{
to =
if event.visibility == :public,
do: ["https://www.w3.org/ns/activitystreams#Public"],
else: [event.organizer_actor.followers_url]
res = %{
"type" => "Event",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"title" => event.title,
"to" => to,
"cc" => [],
"attributedTo" => event.organizer_actor.url,
"name" => event.title,
"actor" => event.organizer_actor.url,
"uuid" => event.uuid,
"category" => event.category,
"summary" => event.description,
"publish_at" => (event.publish_at || event.inserted_at) |> DateTime.to_iso8601(),
"updated_at" => event.updated_at |> DateTime.to_iso8601(),
"content" => event.description,
"publish_at" => (event.publish_at || event.inserted_at) |> date_to_string(),
"updated_at" => event.updated_at |> date_to_string(),
"mediaType" => "text/html",
"startTime" => event.begins_on |> date_to_string(),
"endTime" => event.ends_on |> date_to_string(),
"tag" => event.tags |> build_tags(),
"id" => event.url
}
res =
if is_nil(event.physical_address),
do: res,
else: Map.put(res, "location", AddressConverter.model_to_as(event.physical_address))
if is_nil(event.picture),
do: res,
else: Map.put(res, "attachment", [Utils.make_picture_data(event.picture)])
end
defp date_to_string(nil), do: nil
defp date_to_string(date), do: DateTime.to_iso8601(date)
end
defimpl Mobilizon.Service.ActivityPub.Convertible, for: Mobilizon.Events.Event do
alias Mobilizon.Service.ActivityPub.Converters.Event, as: EventConverter
defdelegate model_to_as(event), to: EventConverter
end

View File

@@ -0,0 +1,88 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/activity_pub/relay.ex
defmodule Mobilizon.Service.ActivityPub.Relay do
@moduledoc """
Handles following and unfollowing relays and instances
"""
alias Mobilizon.Activity
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub
alias MobilizonWeb.API.Follows
require Logger
def get_actor do
with {:ok, %Actor{} = actor} <-
Actors.get_or_create_service_actor_by_url("#{MobilizonWeb.Endpoint.url()}/relay") do
actor
end
end
def follow(target_instance) do
with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
{:ok, activity} <- Follows.follow(local_actor, target_actor) do
Logger.info("Relay: followed instance #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity}
else
e ->
Logger.warn("Error while following remote instance: #{inspect(e)}")
{:error, e}
end
end
def unfollow(target_instance) do
with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
{:ok, activity} <- Follows.unfollow(local_actor, target_actor) do
Logger.info("Relay: unfollowed instance #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity}
else
e ->
Logger.warn("Error while unfollowing remote instance: #{inspect(e)}")
{:error, e}
end
end
def accept(target_instance) do
with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
{:ok, activity} <- Follows.accept(target_actor, local_actor) do
{:ok, activity}
end
end
# def reject(target_instance) do
# with %Actor{} = local_actor <- get_actor(),
# {:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
# {:ok, activity} <- Follows.reject(target_actor, local_actor) do
# {:ok, activity}
# end
# end
@doc """
Publish an activity to all relays following this instance
"""
def publish(%Activity{data: %{"object" => object}} = _activity) do
with %Actor{id: actor_id} = actor <- get_actor(),
{:ok, object} <-
Mobilizon.Service.ActivityPub.Transmogrifier.fetch_obj_helper_as_activity_streams(
object
) do
ActivityPub.announce(actor, object, "#{object["id"]}/announces/#{actor_id}", true, false)
else
e ->
Logger.error("Error while getting local instance actor: #{inspect(e)}")
end
end
def publish(err) do
Logger.error("Tried to publish a bad activity")
Logger.debug(inspect(err))
nil
end
end

View File

@@ -8,11 +8,12 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
A module to handle coding from internal to wire ActivityPub and back.
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Comment}
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils
alias Mobilizon.Service.ActivityPub.Visibility
require Logger
@@ -45,7 +46,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|> Map.put("actor", object["attributedTo"])
|> fix_attachments
|> fix_in_reply_to
|> fix_tag
# |> fix_tag
end
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
@@ -69,8 +71,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
when not is_nil(in_reply_to) do
Logger.error("inReplyTo ID seem incorrect")
Logger.error(inspect(in_reply_to))
Logger.warn("inReplyTo ID seem incorrect: #{inspect(in_reply_to)}")
do_fix_in_reply_to("", object)
end
@@ -87,7 +88,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
object
e ->
Logger.error("Couldn't fetch #{in_reply_to_id} #{inspect(e)}")
Logger.warn("Couldn't fetch #{in_reply_to_id} #{inspect(e)}")
object
end
end
@@ -116,6 +117,9 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|> Map.put("tag", combined)
end
def handle_incoming(%{"id" => nil}), do: :error
def handle_incoming(%{"id" => ""}), do: :error
def handle_incoming(%{"type" => "Flag"} = data) do
with params <- Mobilizon.Service.ActivityPub.Converters.Flag.as_to_model(data) do
params = %{
@@ -186,13 +190,69 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
with {:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed, true),
{:ok, %Actor{} = follower} <- Actors.get_or_fetch_by_url(follower),
{:ok, activity, object} <- ActivityPub.follow(follower, followed, id, false) do
ActivityPub.accept(%{to: [follower.url], actor: followed.url, object: data, local: true})
{:ok, activity, object}
else
e ->
Logger.error("Unable to handle Follow activity")
Logger.error(inspect(e))
Logger.warn("Unable to handle Follow activity #{inspect(e)}")
:error
end
end
# TODO : Handle object being a Link
def handle_incoming(
%{
"type" => "Accept",
"object" => follow_object,
"actor" => _actor,
"id" => _id
} = data
) do
with followed_actor_url <- get_actor(data),
{:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed_actor_url),
{:ok, %Follower{approved: false, actor: follower, id: follow_id} = follow} <-
get_follow(follow_object),
{:ok, activity, _} <-
ActivityPub.accept(
%{
to: [follower.url],
actor: followed.url,
object: follow_object,
local: false
},
"#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}"
),
{:ok, %Follower{approved: true}} <- Actors.update_follower(follow, %{"approved" => true}) do
{:ok, activity, follow}
else
{:ok, %Follower{approved: true} = _follow} ->
{:error, "Follow already accepted"}
e ->
Logger.warn("Unable to process Accept Follow activity #{inspect(e)}")
:error
end
end
def handle_incoming(
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
) do
with followed_actor_url <- get_actor(data),
{:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed_actor_url),
{:ok, %Follower{approved: false, actor: follower, id: follow_id} = follow} <-
get_follow(follow_object),
{:ok, activity, object} <-
ActivityPub.reject(%{
to: [follower.url],
type: "Reject",
actor: followed,
object: follow_object,
local: false
}),
{:ok, _follower} <- Actor.unfollow(followed, follower) do
{:ok, activity, object}
else
e ->
Logger.debug(inspect(e))
:error
end
end
@@ -211,19 +271,21 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# end
# end
# #
# def handle_incoming(
# %{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data
# ) do
# with actor <- get_actor(data),
# {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor),
# {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
# {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false) do
# {:ok, activity}
# else
# e -> Logger.error(inspect e)
# :error
# end
# end
def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data
) do
with actor <- get_actor(data),
{:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor),
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
public <- Visibility.is_public?(data),
{:ok, activity, object} <- ActivityPub.announce(actor, object, id, false, public) do
{:ok, activity, object}
else
e ->
Logger.debug(inspect(e))
:error
end
end
def handle_incoming(
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => _actor_id} =
@@ -245,28 +307,33 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
})
e ->
Logger.error(inspect(e))
Logger.debug(inspect(e))
:error
end
end
# def handle_incoming(
# %{
# "type" => "Undo",
# "object" => %{"type" => "Announce", "object" => object_id},
# "actor" => actor,
# "id" => id
# } = data
# ) do
# with actor <- get_actor(data),
# {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor),
# {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
# {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
# {:ok, activity}
# else
# _e -> :error
# end
# end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{
"type" => "Announce",
"object" => object_id,
"id" => cancelled_activity_id
},
"actor" => actor,
"id" => id
} = data
) do
with actor <- get_actor(data),
{:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor),
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
{:ok, activity, object} <-
ActivityPub.unannounce(actor, object, id, cancelled_activity_id, false) do
{:ok, activity, object}
else
_e -> :error
end
end
def handle_incoming(
%{
@@ -278,12 +345,11 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
) do
with {:ok, %Actor{domain: nil} = followed} <- Actors.get_actor_by_url(followed),
{:ok, %Actor{} = follower} <- Actors.get_actor_by_url(follower),
{:ok, activity, object} <- ActivityPub.unfollow(followed, follower, id, false) do
Actor.unfollow(follower, followed)
{:ok, activity, object} <- ActivityPub.unfollow(follower, followed, id, false) do
{:ok, activity, object}
else
e ->
Logger.error(inspect(e))
Logger.debug(inspect(e))
:error
end
end
@@ -300,14 +366,14 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
with actor <- get_actor(data),
{:ok, %Actor{url: _actor_url}} <- Actors.get_actor_by_url(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
{:ok, object} <- fetch_obj_helper(object_id),
# TODO : Validate that DELETE comes indeed form right domain (see above)
# :ok <- contain_origin(actor_url, object.data),
{:ok, activity, object} <- ActivityPub.delete(object, false) do
{:ok, activity, object}
else
e ->
Logger.error(inspect(e))
Logger.debug(inspect(e))
:error
end
end
@@ -327,7 +393,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# ) do
# with actor <- get_actor(data),
# %Actor{} = actor <- Actors.get_or_fetch_by_url(actor),
# {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
# {:ok, object} <- fetch_obj_helper(object_id) || fetch_obj_helper(object_id),
# {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
# {:ok, activity}
# else
@@ -340,6 +406,20 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
{:error, :not_supported}
end
defp get_follow(follow_object) do
with follow_object_id when not is_nil(follow_object_id) <- Utils.get_url(follow_object),
{:not_found, %Follower{} = follow} <-
{:not_found, Actors.get_follow_by_url(follow_object_id)} do
{:ok, follow}
else
{:not_found, err} ->
{:error, "Follow URL not found"}
_ ->
{:error, "ActivityPub ID not found in Accept Follow object"}
end
end
def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) do
with false <- String.starts_with?(in_reply_to, "http"),
{:ok, replied_to_object} <- fetch_obj_helper(in_reply_to) do
@@ -523,50 +603,23 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# |> Map.put("attachment", attachments)
# end
@spec fetch_obj_helper(String.t()) :: {:ok, %Event{}} | {:ok, %Comment{}} | {:error, any()}
def fetch_obj_helper(url) when is_bitstring(url), do: ActivityPub.fetch_object_from_url(url)
@spec fetch_obj_helper(map() | String.t()) :: Event.t() | Comment.t() | Actor.t() | any()
def fetch_obj_helper(object) do
Logger.debug("Fetching object #{inspect(object)}")
@spec fetch_obj_helper(map()) :: {:ok, %Event{}} | {:ok, %Comment{}} | {:error, any()}
def fetch_obj_helper(obj) when is_map(obj), do: ActivityPub.fetch_object_from_url(obj["id"])
case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do
{:ok, object} ->
{:ok, object}
@spec get_obj_helper(String.t()) :: {:ok, struct()} | nil
def get_obj_helper(id) do
if object = normalize(id), do: {:ok, object}, else: nil
end
@spec normalize(map()) :: struct() | nil
def normalize(obj) when is_map(obj), do: get_anything_by_url(obj["id"])
@spec normalize(String.t()) :: struct() | nil
def normalize(url) when is_binary(url), do: get_anything_by_url(url)
@spec normalize(any()) :: nil
def normalize(_), do: nil
@spec normalize(String.t()) :: struct() | nil
def get_anything_by_url(url) do
Logger.debug(fn -> "Getting anything from url #{url}" end)
get_actor_url(url) || get_event_url(url) || get_comment_url(url)
end
defp get_actor_url(url) do
case Actors.get_actor_by_url(url) do
{:ok, %Actor{} = actor} -> actor
_ -> nil
err ->
Logger.info("Error while fetching #{inspect(object)}")
{:error, err}
end
end
defp get_event_url(url) do
case Events.get_event_by_url(url) do
{:ok, %Event{} = event} -> event
_ -> nil
end
end
defp get_comment_url(url) do
case Events.get_comment_full_from_url(url) do
{:ok, %Comment{} = comment} -> comment
_ -> nil
def fetch_obj_helper_as_activity_streams(object) do
with {:ok, object} <- fetch_obj_helper(object) do
{:ok, Mobilizon.Service.ActivityPub.Convertible.model_to_as(object)}
end
end
end

View File

@@ -30,12 +30,9 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
# Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have.
def get_url(object) do
case object do
%{"id" => id} -> id
id -> id
end
end
def get_url(%{"id" => id}), do: id
def get_url(id) when is_bitstring(id), do: id
def get_url(_), do: nil
def make_json_ld_header do
%{
@@ -150,7 +147,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
else
err ->
Logger.error("Error while inserting a remote comment inside database")
Logger.error(inspect(err))
Logger.debug(inspect(err))
{:error, err}
end
end
@@ -172,7 +169,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
else
err ->
Logger.error("Error while inserting a remote comment inside database")
Logger.error(inspect(err))
Logger.debug(inspect(err))
{:error, err}
end
end
@@ -463,61 +460,98 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"object" => followed_id
}
data =
if activity_id,
do: Map.put(data, "id", activity_id),
else: data
Logger.debug(inspect(data))
if activity_id,
do: Map.put(data, "id", activity_id),
else: data
data
end
#### Announce-related helpers
require Logger
@doc """
Make announce activity data for the given actor and object
"""
def make_announce_data(actor, object, activity_id, public \\ true)
def make_announce_data(
%Actor{url: actor_url} = actor,
%Event{url: event_url} = object,
activity_id
) do
%Actor{url: actor_url, followers_url: actor_followers_url} = _actor,
%{"id" => url, "type" => type} = _object,
activity_id,
public
)
when type in ["Group", "Person", "Application"] do
do_make_announce_data(actor_url, actor_followers_url, url, url, activity_id, public)
end
def make_announce_data(
%Actor{url: actor_url, followers_url: actor_followers_url} = _actor,
%{"id" => url, "type" => type, "actor" => object_actor_url} = _object,
activity_id,
public
)
when type in ["Note", "Event"] do
do_make_announce_data(
actor_url,
actor_followers_url,
object_actor_url,
url,
activity_id,
public
)
end
defp do_make_announce_data(
actor_url,
actor_followers_url,
object_actor_url,
object_url,
activity_id,
public \\ true
) do
{to, cc} =
if public do
{[actor_followers_url, object_actor_url],
["https://www.w3.org/ns/activitystreams#Public"]}
else
{[actor_followers_url], []}
end
data = %{
"type" => "Announce",
"actor" => actor_url,
"object" => event_url,
"to" => [actor.followers_url, object.actor.url],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"]
# "context" => object.data["context"]
"object" => object_url,
"to" => to,
"cc" => cc
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
@doc """
Make announce activity data for the given actor and object
Make unannounce activity data for the given actor and object
"""
def make_announce_data(
%Actor{url: actor_url} = actor,
%Comment{url: comment_url} = object,
def make_unannounce_data(
%Actor{url: url} = actor,
activity,
activity_id
) do
data = %{
"type" => "Announce",
"actor" => actor_url,
"object" => comment_url,
"to" => [actor.followers_url, object.actor.url],
"type" => "Undo",
"actor" => url,
"object" => activity,
"to" => [actor.followers_url, actor.url],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"]
# "context" => object.data["context"]
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
def add_announce_to_object(%Activity{data: %{"actor" => actor}}, object) do
with announcements <- [actor | object.data["announcements"] || []] |> Enum.uniq() do
update_element_in_object("announcement", announcements, object)
end
end
#### Unfollow-related helpers
@spec make_unfollow_data(Actor.t(), Actor.t(), map(), String.t()) :: map()
@@ -553,7 +587,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"to" => params.to |> Enum.uniq(),
"actor" => params.actor.url,
"object" => params.object,
"published" => published
"published" => published,
"id" => params.object["id"] <> "/activity"
}
|> Map.merge(additional)
end

View File

@@ -0,0 +1,21 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/activity_pub/visibility.ex
defmodule Mobilizon.Service.ActivityPub.Visibility do
@moduledoc """
Utility functions related to content visibility
"""
alias Mobilizon.Activity
alias Mobilizon.Events.Event
@public "https://www.w3.org/ns/activitystreams#Public"
@spec is_public?(Activity.t() | map()) :: boolean()
def is_public?(%{data: %{"type" => "Tombstone"}}), do: false
def is_public?(%{data: data}), do: is_public?(data)
def is_public?(%Activity{data: data}), do: is_public?(data)
def is_public?(data) when is_map(data), do: @public in (data["to"] ++ (data["cc"] || []))
def is_public?(err), do: raise(ArgumentError, message: "Invalid argument #{inspect(err)}")
end

View File

@@ -58,10 +58,11 @@ defmodule Mobilizon.Service.Federator do
%Activity{} ->
Logger.info("Already had #{params["id"]}")
_e ->
e ->
# Just drop those for now
Logger.error("Unhandled activity")
Logger.error(Jason.encode!(params))
Logger.debug(inspect(e))
Logger.debug(Jason.encode!(params))
end
end
@@ -75,7 +76,7 @@ defmodule Mobilizon.Service.Federator do
end
def enqueue(type, payload, priority \\ 1) do
Logger.debug("enqueue")
Logger.debug("enqueue something with type #{inspect(type)}")
if Mix.env() == :test do
handle(type, payload)
@@ -111,7 +112,7 @@ defmodule Mobilizon.Service.Federator do
end
def handle_cast(m, state) do
Logger.error(fn ->
Logger.debug(fn ->
"Unknown: #{inspect(m)}, #{inspect(state)}"
end)

View File

@@ -9,6 +9,7 @@ defmodule Mobilizon.Service.Formatter do
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Actors
alias Mobilizon.Service.HTML
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
@@ -87,8 +88,8 @@ defmodule Mobilizon.Service.Formatter do
{html_escape(text, type), mentions, hashtags}
end
def html_escape(_text, "text/html") do
# HTML.filter_tags(text)
def html_escape(text, "text/html") do
HTML.filter_tags(text)
end
def html_escape(text, "text/plain") do

73
lib/service/html.ex Normal file
View File

@@ -0,0 +1,73 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/html.ex
defmodule Mobilizon.Service.HTML do
@moduledoc """
Service to filter tags out of HTML content
"""
alias HtmlSanitizeEx.Scrubber
alias Mobilizon.Service.HTML.Scrubber.Default
def filter_tags(html), do: Scrubber.scrub(html, Default)
end
defmodule Mobilizon.Service.HTML.Scrubber.Default do
@moduledoc "Custom strategy to filter HTML content"
require HtmlSanitizeEx.Scrubber.Meta
alias HtmlSanitizeEx.Scrubber.Meta
# credo:disable-for-previous-line
# No idea how to fix this one…
Meta.remove_cdata_sections_before_scrub()
Meta.strip_comments()
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], ["https", "http"])
Meta.allow_tag_with_this_attribute_values("a", "class", [
"hashtag",
"u-url",
"mention",
"u-url mention",
"mention u-url"
])
Meta.allow_tag_with_this_attribute_values("a", "rel", [
"tag",
"nofollow",
"noopener",
"noreferrer"
])
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
Meta.allow_tag_with_these_attributes("abbr", ["title"])
Meta.allow_tag_with_these_attributes("b", [])
Meta.allow_tag_with_these_attributes("blockquote", [])
Meta.allow_tag_with_these_attributes("br", [])
Meta.allow_tag_with_these_attributes("code", [])
Meta.allow_tag_with_these_attributes("del", [])
Meta.allow_tag_with_these_attributes("em", [])
Meta.allow_tag_with_these_attributes("i", [])
Meta.allow_tag_with_these_attributes("li", [])
Meta.allow_tag_with_these_attributes("ol", [])
Meta.allow_tag_with_these_attributes("p", [])
Meta.allow_tag_with_these_attributes("pre", [])
Meta.allow_tag_with_these_attributes("strong", [])
Meta.allow_tag_with_these_attributes("u", [])
Meta.allow_tag_with_these_attributes("ul", [])
Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"])
Meta.allow_tag_with_these_attributes("span", [])
Meta.allow_tag_with_these_attributes("h1", [])
Meta.allow_tag_with_these_attributes("h2", [])
Meta.allow_tag_with_these_attributes("h3", [])
Meta.allow_tag_with_these_attributes("h4", [])
Meta.allow_tag_with_these_attributes("h5", [])
Meta.strip_everything_not_covered()
end

View File

@@ -1,123 +0,0 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/http_signatures/http_signatures.ex
# https://tools.ietf.org/html/draft-cavage-http-signatures-08
defmodule Mobilizon.Service.HTTPSignatures do
@moduledoc """
# HTTP Signatures
Generates and checks HTTP Signatures
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub
require Logger
def split_signature(sig) do
default = %{"headers" => "date"}
sig =
sig
|> String.trim()
|> String.split(",")
|> Enum.reduce(default, fn part, acc ->
[key | rest] = String.split(part, "=")
value = Enum.join(rest, "=")
Map.put(acc, key, String.trim(value, "\""))
end)
Map.put(sig, "headers", String.split(sig["headers"], ~r/\s/))
end
def validate(headers, signature, public_key) do
sigstring = build_signing_string(headers, signature["headers"])
Logger.debug(fn ->
"Signature: #{signature["signature"]}"
end)
Logger.debug(fn ->
"Sigstring: #{sigstring}"
end)
{:ok, sig} = Base.decode64(signature["signature"])
: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.
case conn.params["actor"] |> Actor.get_public_key_for_url() do
{:ok, public_key} ->
if validate_conn(conn, public_key) do
true
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, _actor} <- ActivityPub.make_actor_from_url(actor_id),
{:ok, public_key} <- actor_id |> Actor.get_public_key_for_url() do
validate_conn(conn, public_key)
end
end
e ->
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, %{})
host_without_port = String.split(headers["host"], ":") |> hd
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
|> Enum.map(fn header -> "#{header}: #{headers[header]}" end)
|> Enum.join("\n")
end
def sign(%Actor{} = actor, headers) do
with sigstring <- build_signing_string(headers, Map.keys(headers)),
{:ok, key} <- actor.keys |> Actor.prepare_public_key(),
signature <- sigstring |> :public_key.sign(:sha256, key) |> Base.encode64() do
[
keyId: actor.url <> "#main-key",
algorithm: "rsa-sha256",
headers: headers |> Map.keys() |> Enum.join(" "),
signature: signature
]
|> Enum.map(fn {k, v} -> "#{k}=\"#{v}\"" end)
|> Enum.join(",")
else
err ->
Logger.error("Unable to sign headers")
Logger.error(inspect(err))
nil
end
end
def generate_date_header(date \\ Timex.now("GMT")) do
case Timex.format(date, "%a, %d %b %Y %H:%M:%S %Z", :strftime) do
{:ok, date} ->
date
{:error, err} ->
Logger.error("Unable to generate date header")
Logger.error(inspect(err))
nil
end
end
def generate_request_target(method, path), do: "#{method} #{path}"
def build_digest(body) do
"SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64())
end
end

View File

@@ -0,0 +1,83 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/signature.ex
defmodule Mobilizon.Service.HTTPSignatures.Signature do
@moduledoc """
Adapter for the `HTTPSignatures` lib that handles signing and providing public keys to verify HTTPSignatures
"""
@behaviour HTTPSignatures.Adapter
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub
require Logger
def key_id_to_actor_url(key_id) do
uri =
URI.parse(key_id)
|> Map.put(:fragment, nil)
uri =
if not is_nil(uri.path) and String.ends_with?(uri.path, "/publickey") do
Map.put(uri, :path, String.replace(uri.path, "/publickey", ""))
else
uri
end
URI.to_string(uri)
end
def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
actor_id <- key_id_to_actor_url(kid),
:ok <- Logger.debug("Fetching public key for #{actor_id}"),
{:ok, public_key} <- Actor.get_public_key_for_url(actor_id) do
{:ok, public_key}
else
e ->
{:error, e}
end
end
def refetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
actor_id <- key_id_to_actor_url(kid),
:ok <- Logger.debug("Refetching public key for #{actor_id}"),
{:ok, _actor} <- ActivityPub.make_actor_from_url(actor_id),
{:ok, public_key} <- Actor.get_public_key_for_url(actor_id) do
{:ok, public_key}
else
e ->
{:error, e}
end
end
def sign(%Actor{} = actor, headers) do
Logger.debug("Signing on behalf of #{actor.url}")
Logger.debug("headers")
Logger.debug(inspect(headers))
with {:ok, key} <- actor.keys |> Actor.prepare_public_key() do
HTTPSignatures.sign(key, actor.url <> "#main-key", headers)
end
end
def generate_date_header(date \\ Timex.now("GMT")) do
case Timex.format(date, "%a, %d %b %Y %H:%M:%S %Z", :strftime) do
{:ok, date} ->
date
{:error, err} ->
Logger.error("Unable to generate date header")
Logger.debug(inspect(err))
nil
end
end
def generate_request_target(method, path), do: "#{method} #{path}"
def build_digest(body) do
"SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64())
end
end