Merge branch 'toot' into 'master'

Toot

See merge request framasoft/mobilizon!20
This commit is contained in:
Thomas Citharel 2018-11-12 09:05:31 +01:00
commit c63394288b
16 changed files with 289 additions and 228 deletions

View File

@ -65,3 +65,5 @@ config :arc,
config :email_checker, config :email_checker,
validations: [EmailChecker.Check.Format] validations: [EmailChecker.Check.Format]
config :phoenix, :format_encoders, json: Jason

34
lib/mix/tasks/toot.ex Normal file
View File

@ -0,0 +1,34 @@
defmodule Mix.Tasks.Toot do
@moduledoc """
Creates a bot from a source
"""
use Mix.Task
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Repo
alias Mobilizon.Events
alias Mobilizon.Events.Comment
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils
require Logger
@shortdoc "Toot to an user"
def run([from, to, content]) do
Mix.Task.run("app.start")
with %Actor{} = from <- Actors.get_actor_by_name(from),
{:ok, %Actor{} = to} <- ActivityPub.find_or_make_actor_from_nickname(to) do
comment = Utils.make_comment_data(from.url, [to.url], content)
ActivityPub.create(%{
to: [to.url],
actor: from,
object: comment,
local: true
})
else
e -> Logger.error(inspect(e))
end
end
end

View File

@ -244,20 +244,29 @@ defmodule Mobilizon.Actors.Actor do
) )
end end
def follow(%Actor{} = follower, %Actor{} = followed) do def follow(%Actor{} = follower, %Actor{} = followed, approved \\ true) do
# Check if actor is locked with {:suspended, false} <- {:suspended, followed.suspended},
# Check if followed has blocked follower # Check if followed has blocked follower
# Check if follower already follows followed {:already_following, false} <- {:already_following, following?(follower, followed)} do
cond do do_follow(follower, followed, approved)
following?(follower, followed) -> else
{:already_following, _} ->
{:error, {:error,
"Could not follow actor: you are already following #{followed.preferred_username}"} "Could not follow actor: you are already following #{followed.preferred_username}"}
# true -> nil {:suspended, _} ->
# Follow the person {:error, "Could not follow actor: #{followed.preferred_username} has been suspended"}
end end
end end
defp do_follow(%Actor{} = follower, %Actor{} = followed, approved \\ true) do
Actors.create_follower(%{
"actor_id" => follower.id,
"target_actor_id" => followed.id,
"approved" => approved
})
end
def following?(%Actor{} = follower, %Actor{followers: followers}) do def following?(%Actor{} = follower, %Actor{followers: followers}) do
Enum.member?(followers, follower) Enum.member?(followers, follower)
end end

View File

@ -203,21 +203,24 @@ defmodule Mobilizon.Actors do
defp blank?(""), do: nil defp blank?(""), do: nil
defp blank?(n), do: n defp blank?(n), do: n
def insert_or_update_actor(data) do def insert_or_update_actor(data, preload \\ false) do
cs = Actor.remote_actor_creation(data) cs = Actor.remote_actor_creation(data)
Repo.insert( actor =
cs, Repo.insert(
on_conflict: [ cs,
set: [ on_conflict: [
keys: data.keys, set: [
avatar_url: data.avatar_url, keys: data.keys,
banner_url: data.banner_url, avatar_url: data.avatar_url,
name: data.name banner_url: data.banner_url,
] name: data.name
], ]
conflict_target: [:preferred_username, :domain] ],
) conflict_target: [:preferred_username, :domain]
)
if preload, do: {:ok, Repo.preload(actor, [:followers])}, else: {:ok, actor}
end end
# def increase_event_count(%Actor{} = actor) do # def increase_event_count(%Actor{} = actor) do
@ -267,8 +270,24 @@ defmodule Mobilizon.Actors do
end end
end end
def get_actor_by_url(url) do @doc """
Repo.get_by(Actor, url: url) Get an actor by it's URL (ActivityPub ID)
"""
@spec get_actor_by_url(String.t(), boolean()) :: {:ok, struct()} | {:error, :actor_not_found}
def get_actor_by_url(url, preload \\ false) do
case Repo.get_by(Actor, url: url) do
nil ->
{:error, :actor_not_found}
actor ->
if preload, do: {:ok, Repo.preload(actor, [:followers])}, else: {:ok, actor}
end
end
@spec get_actor_by_url!(String.t(), boolean()) :: struct()
def get_actor_by_url!(url, preload \\ false) do
actor = Repo.get_by!(Actor, url: url)
if preload, do: Repo.preload(actor, [:followers]), else: actor
end end
def get_actor_by_name(name) do def get_actor_by_name(name) do
@ -304,11 +323,11 @@ defmodule Mobilizon.Actors do
Repo.preload(actor, :organized_events) Repo.preload(actor, :organized_events)
end end
def get_or_fetch_by_url(url) do def get_or_fetch_by_url(url, preload \\ false) do
if actor = get_actor_by_url(url) do if {:ok, actor} = get_actor_by_url(url, preload) do
{:ok, actor} {:ok, actor}
else else
case ActivityPub.make_actor_from_url(url) do case ActivityPub.make_actor_from_url(url, preload) do
{:ok, actor} -> {:ok, actor} ->
{:ok, actor} {:ok, actor}

View File

@ -26,7 +26,10 @@ defmodule Mobilizon.Events.Comment do
@doc false @doc false
def changeset(comment, attrs) do def changeset(comment, attrs) do
uuid = Ecto.UUID.generate() uuid =
if Map.has_key?(attrs, "uuid"),
do: attrs["uuid"],
else: Ecto.UUID.generate()
# TODO : really change me right away # TODO : really change me right away
url = url =
@ -35,7 +38,15 @@ defmodule Mobilizon.Events.Comment do
else: "#{MobilizonWeb.Endpoint.url()}/comments/#{uuid}" else: "#{MobilizonWeb.Endpoint.url()}/comments/#{uuid}"
comment comment
|> cast(attrs, [:url, :text, :actor_id, :event_id, :in_reply_to_comment_id, :origin_comment_id, :attributed_to_id]) |> cast(attrs, [
:url,
:text,
:actor_id,
:event_id,
:in_reply_to_comment_id,
:origin_comment_id,
:attributed_to_id
])
|> put_change(:uuid, uuid) |> put_change(:uuid, uuid)
|> put_change(:url, url) |> put_change(:url, url)
|> validate_required([:text, :actor_id, :url]) |> validate_required([:text, :actor_id, :url])

View File

@ -885,7 +885,15 @@ defmodule Mobilizon.Events do
""" """
def get_comment!(id), do: Repo.get!(Comment, id) def get_comment!(id), do: Repo.get!(Comment, id)
def get_comment_with_uuid!(uuid), do: Repo.get_by!(Comment, uuid: uuid) def get_comment_from_uuid(uuid), do: Repo.get_by(Comment, uuid: uuid)
def get_comment_from_uuid!(uuid), do: Repo.get_by!(Comment, uuid: uuid)
def get_comment_full_from_uuid(uuid) do
with %Comment{} = comment <- Repo.get_by!(Comment, uuid: uuid) do
Repo.preload(comment, [:actor, :attributed_to])
end
end
def get_comment_from_url(url), do: Repo.get_by(Comment, url: url) def get_comment_from_url(url), do: Repo.get_by(Comment, url: url)

View File

@ -1,6 +1,7 @@
defmodule MobilizonWeb.ActivityPubController do defmodule MobilizonWeb.ActivityPubController do
use MobilizonWeb, :controller use MobilizonWeb, :controller
alias Mobilizon.{Actors, Actors.Actor, Events, Events.Event} alias Mobilizon.{Actors, Actors.Actor, Events}
alias Mobilizon.Events.{Event, Comment}
alias MobilizonWeb.ActivityPub.{ObjectView, ActorView} alias MobilizonWeb.ActivityPub.{ObjectView, ActorView}
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.Federator alias Mobilizon.Service.Federator
@ -9,16 +10,18 @@ defmodule MobilizonWeb.ActivityPubController do
action_fallback(:errors) action_fallback(:errors)
@activity_pub_headers [
"application/activity+json",
"application/activity+json, application/ld+json"
]
def actor(conn, %{"name" => name}) do def actor(conn, %{"name" => name}) do
with %Actor{} = actor <- Actors.get_local_actor_by_name(name) do with %Actor{} = actor <- Actors.get_local_actor_by_name(name) do
case get_req_header(conn, "accept") do cond do
["application/activity+json"] -> conn |> get_req_header("accept") |> is_ap_header() ->
conn |> render_ap_actor(actor) conn |> render_ap_actor(actor)
["application/activity+json, application/ld+json"] -> true ->
conn |> render_ap_actor(actor)
_ ->
conn conn
|> put_resp_content_type("text/html") |> put_resp_content_type("text/html")
|> send_file(200, "priv/static/index.html") |> send_file(200, "priv/static/index.html")
@ -28,6 +31,10 @@ defmodule MobilizonWeb.ActivityPubController do
end end
end end
defp is_ap_header(ap_headers) do
length(@activity_pub_headers -- ap_headers) < 2
end
defp render_ap_actor(conn, %Actor{} = actor) do defp render_ap_actor(conn, %Actor{} = actor) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
@ -41,7 +48,21 @@ defmodule MobilizonWeb.ActivityPubController do
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(ObjectView.render("event.json", %{event: event})) |> json(ObjectView.render("event.json", %{event: event}))
else else
false -> _ ->
{:error, :not_found}
end
end
def comment(conn, %{"uuid" => uuid}) do
with %Comment{} = comment <- Events.get_comment_full_from_uuid(uuid) do
# Comments are always public for now
# TODO : Make comments maybe restricted
# true <- comment.public do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ObjectView.render("comment.json", %{comment: comment}))
else
_ ->
{:error, :not_found} {:error, :not_found}
end end
end end

View File

@ -14,7 +14,7 @@ defmodule MobilizonWeb.Router do
end end
pipeline :activity_pub do pipeline :activity_pub do
plug(:accepts, ["activity-json", "text/html"]) plug(:accepts, ["activity-json", "html"])
plug(MobilizonWeb.HTTPSignaturePlug) plug(MobilizonWeb.HTTPSignaturePlug)
end end
@ -55,7 +55,7 @@ defmodule MobilizonWeb.Router do
get("/@:name/following", ActivityPubController, :following) get("/@:name/following", ActivityPubController, :following)
get("/@:name/followers", ActivityPubController, :followers) get("/@:name/followers", ActivityPubController, :followers)
get("/events/:uuid", ActivityPubController, :event) get("/events/:uuid", ActivityPubController, :event)
get("/comments/:uuid", ActivityPubController, :event) get("/comments/:uuid", ActivityPubController, :comment)
post("/@:name/inbox", ActivityPubController, :inbox) post("/@:name/inbox", ActivityPubController, :inbox)
post("/inbox", ActivityPubController, :inbox) post("/inbox", ActivityPubController, :inbox)
end end

View File

@ -32,6 +32,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
"owner" => actor.url, "owner" => actor.url,
"publicKeyPem" => public_key "publicKeyPem" => public_key
}, },
# TODO : Make have actors have an uuid
# "uuid" => actor.uuid
"endpoints" => %{ "endpoints" => %{
"sharedInbox" => actor.shared_inbox_url "sharedInbox" => actor.shared_inbox_url
} }
@ -135,6 +137,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
"Announce" "Announce"
end, end,
"actor" => activity.actor, "actor" => activity.actor,
# Not sure if needed since this is used into outbox
"published" => Timex.now(), "published" => Timex.now(),
"to" => ["https://www.w3.org/ns/activitystreams#Public"], "to" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => "object" =>
@ -143,9 +146,10 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
render_one(activity.data, ObjectView, "event.json", as: :event) render_one(activity.data, ObjectView, "event.json", as: :event)
:Comment -> :Comment ->
render_one(activity.data, ObjectView, "note.json", as: :note) render_one(activity.data, ObjectView, "comment.json", as: :comment)
end end
} }
|> Map.merge(Utils.make_json_ld_header())
end end
def collection(collection, iri, page, total \\ nil) do def collection(collection, iri, page, total \\ nil) do

View File

@ -2,20 +2,7 @@ defmodule MobilizonWeb.ActivityPub.ObjectView do
use MobilizonWeb, :view use MobilizonWeb, :view
alias MobilizonWeb.ActivityPub.ObjectView alias MobilizonWeb.ActivityPub.ObjectView
alias Mobilizon.Service.ActivityPub.Transmogrifier alias Mobilizon.Service.ActivityPub.Transmogrifier
alias Mobilizon.Service.ActivityPub.Utils
@base %{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
%{
"manuallyApprovesFollowers" => "as:manuallyApprovesFollowers",
"sensitive" => "as:sensitive",
"Hashtag" => "as:Hashtag",
"toot" => "http://joinmastodon.org/ns#",
"Emoji" => "toot:Emoji"
}
]
}
def render("event.json", %{event: event}) do def render("event.json", %{event: event}) do
event = %{ event = %{
@ -24,29 +11,36 @@ defmodule MobilizonWeb.ActivityPub.ObjectView do
"name" => event.title, "name" => event.title,
"category" => render_one(event.category, ObjectView, "category.json", as: :category), "category" => render_one(event.category, ObjectView, "category.json", as: :category),
"content" => event.description, "content" => event.description,
"mediaType" => "text/markdown", "mediaType" => "text/html",
"published" => Timex.format!(event.inserted_at, "{ISO:Extended}"), "published" => Timex.format!(event.inserted_at, "{ISO:Extended}"),
"updated" => Timex.format!(event.updated_at, "{ISO:Extended}") "updated" => Timex.format!(event.updated_at, "{ISO:Extended}")
} }
Map.merge(event, @base) Map.merge(event, Utils.make_json_ld_header())
end end
def render("note.json", %{note: note}) do def render("comment.json", %{comment: comment}) do
event = %{ comment = %{
"actor" => comment.actor.url,
"uuid" => comment.uuid,
# The activity should have attributedTo, not the comment itself
# "attributedTo" => comment.attributed_to,
"type" => "Note", "type" => "Note",
"id" => note.url, "id" => comment.url,
"content" => note.text, "content" => comment.text,
"mediaType" => "text/markdown", "mediaType" => "text/html",
"published" => Timex.format!(note.inserted_at, "{ISO:Extended}"), "published" => Timex.format!(comment.inserted_at, "{ISO:Extended}"),
"updated" => Timex.format!(note.updated_at, "{ISO:Extended}") "updated" => Timex.format!(comment.updated_at, "{ISO:Extended}")
} }
Map.merge(event, @base) Map.merge(comment, Utils.make_json_ld_header())
end end
def render("category.json", %{category: category}) do def render("category.json", %{category: category}) do
%{"title" => category.title} %{
"identifier" => category.id,
"name" => category.title
}
end end
def render("category.json", %{category: nil}) do def render("category.json", %{category: nil}) do

View File

@ -16,7 +16,7 @@ defmodule Mobilizon.Service.ActivityPub do
alias Mobilizon.Service.Federator alias Mobilizon.Service.Federator
import Logger require Logger
import Mobilizon.Service.ActivityPub.Utils import Mobilizon.Service.ActivityPub.Utils
def get_recipients(data) do def get_recipients(data) do
@ -24,9 +24,21 @@ defmodule Mobilizon.Service.ActivityPub do
end end
def insert(map, local \\ true) when is_map(map) do def insert(map, local \\ true) when is_map(map) do
Logger.debug("preparing an activity")
Logger.debug(inspect(map))
with map <- lazy_put_activity_defaults(map), with map <- lazy_put_activity_defaults(map),
:ok <- insert_full_object(map) do :ok <- insert_full_object(map, local) do
map = Map.put(map, "id", Ecto.UUID.generate()) object_id =
cond do
is_map(map["object"]) ->
map["object"]["id"]
is_binary(map["object"]) ->
map["id"]
end
map = Map.put(map, "id", "#{object_id}/activity")
activity = %Activity{ activity = %Activity{
data: map, data: map,
@ -106,7 +118,8 @@ defmodule Mobilizon.Service.ActivityPub do
end end
end end
def create(%{to: to, actor: actor, context: context, object: object} = params) do def create(%{to: to, actor: actor, object: object} = params) do
Logger.debug("creating an activity")
additional = params[:additional] || %{} additional = params[:additional] || %{}
# only accept false as false value # only accept false as false value
local = !(params[:local] == false) local = !(params[:local] == false)
@ -114,7 +127,7 @@ defmodule Mobilizon.Service.ActivityPub do
with create_data <- with create_data <-
make_create_data( make_create_data(
%{to: to, actor: actor, published: published, context: context, object: object}, %{to: to, actor: actor, published: published, object: object},
additional additional
), ),
{:ok, activity} <- insert(create_data, local), {:ok, activity} <- insert(create_data, local),
@ -157,10 +170,14 @@ defmodule Mobilizon.Service.ActivityPub do
end end
def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with data <- make_follow_data(follower, followed, activity_id), with {:ok, follow} <- Actor.follow(follower, followed, true),
data <- make_follow_data(follower, followed, follow.id),
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
else
{err, _} when err in [:already_following, :suspended] ->
{:error, err}
end end
end end
@ -199,9 +216,9 @@ defmodule Mobilizon.Service.ActivityPub do
def create_public_activities(%Actor{} = actor) do def create_public_activities(%Actor{} = actor) do
end end
def make_actor_from_url(url) do def make_actor_from_url(url, preload \\ false) do
with {:ok, data} <- fetch_and_prepare_user_from_url(url) do with {:ok, data} <- fetch_and_prepare_actor_from_url(url) do
Actors.insert_or_update_actor(data) Actors.insert_or_update_actor(data, preload)
else else
# Request returned 410 # Request returned 410
{:error, :actor_deleted} -> {:error, :actor_deleted} ->
@ -243,12 +260,13 @@ defmodule Mobilizon.Service.ActivityPub do
end end
remote_inboxes = remote_inboxes =
followers (remote_actors(activity) ++ followers)
|> Enum.map(fn follower -> follower.shared_inbox_url end) |> Enum.map(fn follower -> follower.shared_inbox_url end)
|> Enum.uniq() |> Enum.uniq()
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data) {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
json = Jason.encode!(data) json = Jason.encode!(data)
Logger.debug("Remote inboxes are : #{inspect(remote_inboxes)}")
Enum.each(remote_inboxes, fn inbox -> Enum.each(remote_inboxes, fn inbox ->
Federator.enqueue(:publish_single_ap, %{ Federator.enqueue(:publish_single_ap, %{
@ -273,6 +291,9 @@ defmodule Mobilizon.Service.ActivityPub do
Logger.debug("signature") Logger.debug("signature")
Logger.debug(inspect(signature)) Logger.debug(inspect(signature))
Logger.debug("body json")
Logger.debug(inspect(json))
{:ok, response} = {:ok, response} =
HTTPoison.post( HTTPoison.post(
inbox, inbox,
@ -284,8 +305,8 @@ defmodule Mobilizon.Service.ActivityPub do
Logger.debug(inspect(response)) Logger.debug(inspect(response))
end end
def fetch_and_prepare_user_from_url(url) do def fetch_and_prepare_actor_from_url(url) do
Logger.debug("Fetching and preparing user from url") Logger.debug("Fetching and preparing actor from url")
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
HTTPoison.get(url, [Accept: "application/activity+json"], follow_redirect: true), HTTPoison.get(url, [Accept: "application/activity+json"], follow_redirect: true),
@ -297,7 +318,7 @@ defmodule Mobilizon.Service.ActivityPub do
{:error, :actor_deleted} {:error, :actor_deleted}
e -> e ->
Logger.error("Could not decode user at fetch #{url}, #{inspect(e)}") Logger.error("Could not decode actor at fetch #{url}, #{inspect(e)}")
e e
end end
end end

View File

@ -18,7 +18,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
object object
|> Map.put("actor", object["attributedTo"]) |> Map.put("actor", object["attributedTo"])
|> fix_attachments |> fix_attachments
|> fix_context
# |> fix_in_reply_to # |> fix_in_reply_to
|> fix_tag |> fix_tag
end end
@ -31,10 +30,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# #
# object # object
# |> Map.put("inReplyTo", replied_object.data["id"]) # |> Map.put("inReplyTo", replied_object.data["id"])
# |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
# |> Map.put("inReplyToStatusId", activity.id)
# |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
# |> Map.put("context", replied_object.data["context"] || object["conversation"])
# #
# e -> # e ->
# Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}") # Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}")
@ -44,11 +39,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
def fix_in_reply_to(object), do: object def fix_in_reply_to(object), do: object
def fix_context(object) do
object
|> Map.put("context", object["conversation"])
end
def fix_attachments(object) do def fix_attachments(object) do
attachments = attachments =
(object["attachment"] || []) (object["attachment"] || [])
@ -87,7 +77,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
to: data["to"], to: data["to"],
object: object, object: object,
actor: actor, actor: actor,
context: object["conversation"],
local: false, local: false,
published: data["published"], published: data["published"],
additional: additional:
@ -104,12 +93,11 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
def handle_incoming( def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
) do ) do
with {:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed), with {:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed, true),
{:ok, %Actor{} = follower} <- Actors.get_or_fetch_by_url(follower), {:ok, %Actor{} = follower} <- Actors.get_or_fetch_by_url(follower),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
ActivityPub.accept(%{to: [follower.url], actor: followed.url, object: data, local: true}) ActivityPub.accept(%{to: [follower.url], actor: followed.url, object: data, local: true})
# Actors.follow(follower, followed)
{:ok, activity} {:ok, activity}
else else
e -> e ->
@ -225,11 +213,10 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
object object
# |> set_sensitive # |> set_sensitive
# |> add_hashtags # |> add_hashtags
# |> add_mention_tags |> add_mention_tags
# |> add_emoji_tags # |> add_emoji_tags
|> add_attributed_to |> add_attributed_to
# |> prepare_attachments # |> prepare_attachments
|> set_conversation
|> set_reply_to_uri |> set_reply_to_uri
end end
@ -239,6 +226,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
""" """
def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
Logger.debug("Prepare outgoing for a note creation")
object = object =
object object
|> prepare_object |> prepare_object
@ -248,6 +237,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|> Map.put("object", object) |> Map.put("object", object)
|> Map.put("@context", "https://www.w3.org/ns/activitystreams") |> Map.put("@context", "https://www.w3.org/ns/activitystreams")
Logger.debug("Finished prepare outgoing for a note creation")
{:ok, data} {:ok, data}
end end
@ -304,22 +295,23 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# end # end
# end # end
# #
# def add_mention_tags(object) do def add_mention_tags(object) do
# recipients = object["to"] ++ (object["cc"] || []) recipients = object["to"] ++ (object["cc"] || [])
#
# mentions = mentions =
# recipients recipients
# |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end) |> Enum.map(fn url -> Actors.get_actor_by_url!(url) end)
# |> Enum.filter(& &1) |> Enum.filter(& &1)
# |> Enum.map(fn user -> |> Enum.map(fn actor ->
# %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"} %{"type" => "Mention", "href" => actor.url, "name" => "@#{actor.preferred_username}"}
# end) end)
#
# tags = object["tag"] || [] tags = object["tag"] || []
#
# object object
# |> Map.put("tag", tags ++ mentions) |> Map.put("tag", tags ++ mentions)
# end end
# #
# # TODO: we should probably send mtime instead of unix epoch time for updated # # TODO: we should probably send mtime instead of unix epoch time for updated
# def add_emoji_tags(object) do # def add_emoji_tags(object) do
@ -342,9 +334,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# |> Map.put("tag", tags ++ out) # |> Map.put("tag", tags ++ out)
# end # end
# #
def set_conversation(object) do
Map.put(object, "conversation", object["context"])
end
# #
# def set_sensitive(object) do # def set_sensitive(object) do
@ -370,84 +359,4 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# object # object
# |> Map.put("attachment", attachments) # |> Map.put("attachment", attachments)
# end # end
#
# defp user_upgrade_task(user) do
# old_follower_address = User.ap_followers(user)
#
# q =
# from(
# u in User,
# where: ^old_follower_address in u.following,
# update: [
# set: [
# following:
# fragment(
# "array_replace(?,?,?)",
# u.following,
# ^old_follower_address,
# ^user.follower_address
# )
# ]
# ]
# )
#
# Repo.update_all(q, [])
#
# maybe_retire_websub(user.ap_id)
#
# # Only do this for recent activties, don't go through the whole db.
# # Only look at the last 1000 activities.
# since = (Repo.aggregate(Activity, :max, :id) || 0) - 1_000
#
# q =
# from(
# a in Activity,
# where: ^old_follower_address in a.recipients,
# where: a.id > ^since,
# update: [
# set: [
# recipients:
# fragment(
# "array_replace(?,?,?)",
# a.recipients,
# ^old_follower_address,
# ^user.follower_address
# )
# ]
# ]
# )
#
# Repo.update_all(q, [])
# end
#
# def upgrade_user_from_ap_id(ap_id, async \\ true) do
# with %User{local: false} = user <- User.get_by_ap_id(ap_id),
# {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
# data =
# data
# |> Map.put(:info, Map.merge(user.info, data[:info]))
#
# already_ap = User.ap_enabled?(user)
#
# {:ok, user} =
# User.upgrade_changeset(user, data)
# |> Repo.update()
#
# if !already_ap do
# # This could potentially take a long time, do it in the background
# if async do
# Task.start(fn ->
# user_upgrade_task(user)
# end)
# else
# user_upgrade_task(user)
# end
# end
#
# {:ok, user}
# else
# e -> e
# end
# end
#
end end

View File

@ -20,16 +20,19 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
import Ecto.Query import Ecto.Query
require Logger require Logger
def make_context(%Activity{data: %{"context" => context}}), do: context
def make_context(_), do: generate_context_id()
def make_json_ld_header do def make_json_ld_header do
%{ %{
"@context" => [ "@context" => [
"https://www.w3.org/ns/activitystreams", "https://www.w3.org/ns/activitystreams",
"https://litepub.github.io/litepub/context.jsonld",
%{ %{
"manuallyApprovesFollowers" => "as:manuallyApprovesFollowers", "sc" => "http://schema.org#",
"sensitive" => "as:sensitive",
"Hashtag" => "as:Hashtag", "Hashtag" => "as:Hashtag",
"toot" => "http://joinmastodon.org/ns#", "category" => "sc:category",
"Emoji" => "toot:Emoji" "uuid" => "sc:identifier"
} }
] ]
} }
@ -74,6 +77,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
Enqueues an activity for federation if it's local Enqueues an activity for federation if it's local
""" """
def maybe_federate(%Activity{local: true} = activity) do def maybe_federate(%Activity{local: true} = activity) do
Logger.debug("Maybe federate an activity")
priority = priority =
case activity.data["type"] do case activity.data["type"] do
"Delete" -> 10 "Delete" -> 10
@ -87,6 +92,14 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
def maybe_federate(_), do: :ok def maybe_federate(_), do: :ok
def remote_actors(%{data: %{"to" => to} = data}) do
to = to ++ (data["cc"] || [])
to
|> Enum.map(fn url -> Actors.get_actor_by_url!(url) end)
|> Enum.filter(fn actor -> actor && !is_nil(actor.domain) end)
end
@doc """ @doc """
Adds an id and a published data if they aren't there, Adds an id and a published data if they aren't there,
also adds it to an included object also adds it to an included object
@ -123,7 +136,12 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
@doc """ @doc """
Inserts a full object if it is contained in an activity. Inserts a full object if it is contained in an activity.
""" """
def insert_full_object(%{"object" => %{"type" => type} = object_data}) def insert_full_object(object_data, local \\ false)
@doc """
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => type} = object_data}, _local)
when is_map(object_data) and type == "Event" do when is_map(object_data) and type == "Event" do
with {:ok, _} <- Events.create_event(object_data) do with {:ok, _} <- Events.create_event(object_data) do
:ok :ok
@ -133,7 +151,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
@doc """ @doc """
Inserts a full object if it is contained in an activity. Inserts a full object if it is contained in an activity.
""" """
def insert_full_object(%{"object" => %{"type" => type} = object_data}) def insert_full_object(%{"object" => %{"type" => type} = object_data}, local)
when is_map(object_data) and type == "Note" do when is_map(object_data) and type == "Note" do
with {:ok, %Actor{id: actor_id}} <- Actors.get_or_fetch_by_url(object_data["actor"]) do with {:ok, %Actor{id: actor_id}} <- Actors.get_or_fetch_by_url(object_data["actor"]) do
data = %{ data = %{
@ -142,8 +160,9 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"actor_id" => actor_id, "actor_id" => actor_id,
"in_reply_to_comment_id" => nil, "in_reply_to_comment_id" => nil,
"event_id" => nil, "event_id" => nil,
"uuid" => object_data["uuid"],
# probably # probably
"local" => false "local" => local
} }
# We fetch the parent object # We fetch the parent object
@ -193,7 +212,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
end end
end end
def insert_full_object(_), do: :ok def insert_full_object(_, _), do: :ok
# def update_object_in_activities(%{data: %{"id" => id}} = object) do # def update_object_in_activities(%{data: %{"id" => id}} = object) do
# # TODO # # TODO
@ -236,30 +255,31 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
def make_comment_data( def make_comment_data(
actor, actor,
to, to,
context,
content_html, content_html,
attachments, # attachments,
inReplyTo, inReplyTo \\ nil,
tags, # tags,
cw \\ nil, cw \\ nil,
cc \\ [] cc \\ []
) do ) do
Logger.debug("Making comment data")
uuid = Ecto.UUID.generate()
object = %{ object = %{
"type" => "Note", "type" => "Note",
"to" => to, "to" => to,
"cc" => cc, # "cc" => cc,
"content" => content_html, "content" => content_html,
"summary" => cw, # "summary" => cw,
"context" => context, # "attachment" => attachments,
"attachment" => attachments,
"actor" => actor, "actor" => actor,
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq() "id" => "#{MobilizonWeb.Endpoint.url()}/comments/#{uuid}"
# "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
} }
if inReplyTo do if inReplyTo do
object object
|> Map.put("inReplyTo", inReplyTo.data["object"]["id"]) |> Map.put("inReplyTo", inReplyTo)
|> Map.put("inReplyToStatusId", inReplyTo.id)
else else
object object
end end
@ -311,6 +331,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
Makes a follow activity data for the given follower and followed Makes a follow activity data for the given follower and followed
""" """
def make_follow_data(%Actor{url: follower_id}, %Actor{url: followed_id}, activity_id) do def make_follow_data(%Actor{url: follower_id}, %Actor{url: followed_id}, activity_id) do
Logger.debug("Make follow data")
data = %{ data = %{
"type" => "Follow", "type" => "Follow",
"actor" => follower_id, "actor" => follower_id,
@ -319,7 +341,11 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"object" => followed_id "object" => followed_id
} }
if activity_id, do: Map.put(data, "id", activity_id), else: data Logger.debug(inspect(data))
if activity_id,
do: Map.put(data, "id", "#{MobilizonWeb.Endpoint.url()}/follow/#{activity_id}/activity"),
else: data
end end
# def fetch_latest_follow(%Actor{url: follower_id}, %Actor{url: followed_id}) do # def fetch_latest_follow(%Actor{url: follower_id}, %Actor{url: followed_id}) do
@ -388,8 +414,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"to" => params.to |> Enum.uniq(), "to" => params.to |> Enum.uniq(),
"actor" => params.actor.url, "actor" => params.actor.url,
"object" => params.object, "object" => params.object,
"published" => published, "published" => published
"context" => params.context
} }
|> Map.merge(additional) |> Map.merge(additional)
end end

View File

@ -36,7 +36,7 @@ defmodule Mobilizon.Service.Federator do
Logger.debug(inspect(activity)) Logger.debug(inspect(activity))
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end) Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
with actor when not is_nil(actor) <- Actors.get_actor_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) Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end)
ActivityPub.publish(actor, activity) ActivityPub.publish(actor, activity)
end end

View File

@ -90,17 +90,21 @@ defmodule Mobilizon.Service.HTTPSignatures do
end end
def sign(%Actor{} = actor, headers) do def sign(%Actor{} = actor, headers) do
sigstring = build_signing_string(headers, Map.keys(headers)) with sigstring <- build_signing_string(headers, Map.keys(headers)),
{:ok, key} <- actor.keys |> prepare_public_key(),
signature = sigstring |> :public_key.sign(:sha256, actor.keys) |> Base.encode64() signature <- sigstring |> :public_key.sign(:sha256, key) |> Base.encode64() do
[
[ keyId: actor.url <> "#main-key",
keyId: actor.url <> "#main-key", algorithm: "rsa-sha256",
algorithm: "rsa-sha256", headers: headers |> Map.keys() |> Enum.join(" "),
headers: headers |> Map.keys() |> Enum.join(" "), signature: signature
signature: signature ]
] |> Enum.map(fn {k, v} -> "#{k}=\"#{v}\"" end)
|> Enum.map(fn {k, v} -> "#{k}=\"#{v}\"" end) |> Enum.join(",")
|> Enum.join(",") else
err ->
Logger.error("Unable to sign headers")
Logger.error(inspect(err))
end
end end
end end

View File

@ -38,7 +38,7 @@ defmodule Mobilizon.Service.WebFinger do
{:ok, represent_user(user, "JSON")} {:ok, represent_user(user, "JSON")}
else else
_e -> _e ->
with user when not is_nil(user) <- Actors.get_actor_by_url(resource) do with user when not is_nil(user) <- Actors.get_actor_by_url!(resource) do
{:ok, represent_user(user, "JSON")} {:ok, represent_user(user, "JSON")}
else else
_e -> _e ->