From 25286c763bb59b571f3a32c692ad6a9f348f5916 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Mon, 12 Nov 2018 09:05:31 +0100 Subject: [PATCH] Toot --- config/config.exs | 2 + lib/mix/tasks/toot.ex | 34 +++++ lib/mobilizon/actors/actor.ex | 25 +++- lib/mobilizon/actors/actors.ex | 55 ++++--- lib/mobilizon/events/comment.ex | 15 +- lib/mobilizon/events/events.ex | 10 +- .../controllers/activity_pub_controller.ex | 37 ++++- lib/mobilizon_web/router.ex | 4 +- .../views/activity_pub/actor_view.ex | 6 +- .../views/activity_pub/object_view.ex | 44 +++--- lib/service/activity_pub/activity_pub.ex | 47 ++++-- lib/service/activity_pub/transmogrifier.ex | 137 +++--------------- lib/service/activity_pub/utils.ex | 69 ++++++--- lib/service/federator.ex | 2 +- .../http_signatures/http_signatures.ex | 28 ++-- lib/service/web_finger/web_finger.ex | 2 +- 16 files changed, 289 insertions(+), 228 deletions(-) create mode 100644 lib/mix/tasks/toot.ex diff --git a/config/config.exs b/config/config.exs index 33b032d9c..0b63f9ebe 100644 --- a/config/config.exs +++ b/config/config.exs @@ -65,3 +65,5 @@ config :arc, config :email_checker, validations: [EmailChecker.Check.Format] + +config :phoenix, :format_encoders, json: Jason diff --git a/lib/mix/tasks/toot.ex b/lib/mix/tasks/toot.ex new file mode 100644 index 000000000..d208a030d --- /dev/null +++ b/lib/mix/tasks/toot.ex @@ -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 diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index f0b5ebaac..c4f5f5238 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -244,20 +244,29 @@ defmodule Mobilizon.Actors.Actor do ) end - def follow(%Actor{} = follower, %Actor{} = followed) do - # Check if actor is locked - # Check if followed has blocked follower - # Check if follower already follows followed - cond do - following?(follower, followed) -> + def follow(%Actor{} = follower, %Actor{} = followed, 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) + else + {:already_following, _} -> {:error, "Could not follow actor: you are already following #{followed.preferred_username}"} - # true -> nil - # Follow the person + {:suspended, _} -> + {:error, "Could not follow actor: #{followed.preferred_username} has been suspended"} 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 Enum.member?(followers, follower) end diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index bb70d770d..427661ef4 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -203,21 +203,24 @@ defmodule Mobilizon.Actors do defp blank?(""), do: nil 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) - Repo.insert( - cs, - on_conflict: [ - set: [ - keys: data.keys, - avatar_url: data.avatar_url, - banner_url: data.banner_url, - name: data.name - ] - ], - conflict_target: [:preferred_username, :domain] - ) + actor = + Repo.insert( + cs, + on_conflict: [ + set: [ + keys: data.keys, + avatar_url: data.avatar_url, + banner_url: data.banner_url, + name: data.name + ] + ], + conflict_target: [:preferred_username, :domain] + ) + + if preload, do: {:ok, Repo.preload(actor, [:followers])}, else: {:ok, actor} end # def increase_event_count(%Actor{} = actor) do @@ -267,8 +270,24 @@ defmodule Mobilizon.Actors do end end - def get_actor_by_url(url) do - Repo.get_by(Actor, url: url) + @doc """ + 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 def get_actor_by_name(name) do @@ -304,11 +323,11 @@ defmodule Mobilizon.Actors do Repo.preload(actor, :organized_events) end - def get_or_fetch_by_url(url) do - if actor = get_actor_by_url(url) do + def get_or_fetch_by_url(url, preload \\ false) do + if {:ok, actor} = get_actor_by_url(url, preload) do {:ok, actor} else - case ActivityPub.make_actor_from_url(url) do + case ActivityPub.make_actor_from_url(url, preload) do {:ok, actor} -> {:ok, actor} diff --git a/lib/mobilizon/events/comment.ex b/lib/mobilizon/events/comment.ex index e9a960b85..d49e459ed 100644 --- a/lib/mobilizon/events/comment.ex +++ b/lib/mobilizon/events/comment.ex @@ -26,7 +26,10 @@ defmodule Mobilizon.Events.Comment do @doc false 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 url = @@ -35,7 +38,15 @@ defmodule Mobilizon.Events.Comment do else: "#{MobilizonWeb.Endpoint.url()}/comments/#{uuid}" 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(:url, url) |> validate_required([:text, :actor_id, :url]) diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index cf3164bf9..a2c5b63d4 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -885,7 +885,15 @@ defmodule Mobilizon.Events do """ 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) diff --git a/lib/mobilizon_web/controllers/activity_pub_controller.ex b/lib/mobilizon_web/controllers/activity_pub_controller.ex index 7d377df63..65b806d06 100644 --- a/lib/mobilizon_web/controllers/activity_pub_controller.ex +++ b/lib/mobilizon_web/controllers/activity_pub_controller.ex @@ -1,6 +1,7 @@ defmodule MobilizonWeb.ActivityPubController do 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 Mobilizon.Service.ActivityPub alias Mobilizon.Service.Federator @@ -9,16 +10,18 @@ defmodule MobilizonWeb.ActivityPubController do action_fallback(:errors) + @activity_pub_headers [ + "application/activity+json", + "application/activity+json, application/ld+json" + ] + def actor(conn, %{"name" => name}) do with %Actor{} = actor <- Actors.get_local_actor_by_name(name) do - case get_req_header(conn, "accept") do - ["application/activity+json"] -> + cond do + conn |> get_req_header("accept") |> is_ap_header() -> conn |> render_ap_actor(actor) - ["application/activity+json, application/ld+json"] -> - conn |> render_ap_actor(actor) - - _ -> + true -> conn |> put_resp_content_type("text/html") |> send_file(200, "priv/static/index.html") @@ -28,6 +31,10 @@ defmodule MobilizonWeb.ActivityPubController do 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 conn |> put_resp_header("content-type", "application/activity+json") @@ -41,7 +48,21 @@ defmodule MobilizonWeb.ActivityPubController do |> put_resp_header("content-type", "application/activity+json") |> json(ObjectView.render("event.json", %{event: event})) 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} end end diff --git a/lib/mobilizon_web/router.ex b/lib/mobilizon_web/router.ex index bda486b23..c40e6774f 100644 --- a/lib/mobilizon_web/router.ex +++ b/lib/mobilizon_web/router.ex @@ -14,7 +14,7 @@ defmodule MobilizonWeb.Router do end pipeline :activity_pub do - plug(:accepts, ["activity-json", "text/html"]) + plug(:accepts, ["activity-json", "html"]) plug(MobilizonWeb.HTTPSignaturePlug) end @@ -55,7 +55,7 @@ defmodule MobilizonWeb.Router do get("/@:name/following", ActivityPubController, :following) get("/@:name/followers", ActivityPubController, :followers) get("/events/:uuid", ActivityPubController, :event) - get("/comments/:uuid", ActivityPubController, :event) + get("/comments/:uuid", ActivityPubController, :comment) post("/@:name/inbox", ActivityPubController, :inbox) post("/inbox", ActivityPubController, :inbox) end diff --git a/lib/mobilizon_web/views/activity_pub/actor_view.ex b/lib/mobilizon_web/views/activity_pub/actor_view.ex index 8fdfb536f..6a94747bb 100644 --- a/lib/mobilizon_web/views/activity_pub/actor_view.ex +++ b/lib/mobilizon_web/views/activity_pub/actor_view.ex @@ -32,6 +32,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do "owner" => actor.url, "publicKeyPem" => public_key }, + # TODO : Make have actors have an uuid + # "uuid" => actor.uuid "endpoints" => %{ "sharedInbox" => actor.shared_inbox_url } @@ -135,6 +137,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do "Announce" end, "actor" => activity.actor, + # Not sure if needed since this is used into outbox "published" => Timex.now(), "to" => ["https://www.w3.org/ns/activitystreams#Public"], "object" => @@ -143,9 +146,10 @@ defmodule MobilizonWeb.ActivityPub.ActorView do render_one(activity.data, ObjectView, "event.json", as: :event) :Comment -> - render_one(activity.data, ObjectView, "note.json", as: :note) + render_one(activity.data, ObjectView, "comment.json", as: :comment) end } + |> Map.merge(Utils.make_json_ld_header()) end def collection(collection, iri, page, total \\ nil) do diff --git a/lib/mobilizon_web/views/activity_pub/object_view.ex b/lib/mobilizon_web/views/activity_pub/object_view.ex index f43d4463b..83ccab60d 100644 --- a/lib/mobilizon_web/views/activity_pub/object_view.ex +++ b/lib/mobilizon_web/views/activity_pub/object_view.ex @@ -2,20 +2,7 @@ defmodule MobilizonWeb.ActivityPub.ObjectView do use MobilizonWeb, :view alias MobilizonWeb.ActivityPub.ObjectView alias Mobilizon.Service.ActivityPub.Transmogrifier - - @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" - } - ] - } + alias Mobilizon.Service.ActivityPub.Utils def render("event.json", %{event: event}) do event = %{ @@ -24,29 +11,36 @@ defmodule MobilizonWeb.ActivityPub.ObjectView do "name" => event.title, "category" => render_one(event.category, ObjectView, "category.json", as: :category), "content" => event.description, - "mediaType" => "text/markdown", + "mediaType" => "text/html", "published" => Timex.format!(event.inserted_at, "{ISO:Extended}"), "updated" => Timex.format!(event.updated_at, "{ISO:Extended}") } - Map.merge(event, @base) + Map.merge(event, Utils.make_json_ld_header()) end - def render("note.json", %{note: note}) do - event = %{ + def render("comment.json", %{comment: comment}) do + comment = %{ + "actor" => comment.actor.url, + "uuid" => comment.uuid, + # The activity should have attributedTo, not the comment itself + # "attributedTo" => comment.attributed_to, "type" => "Note", - "id" => note.url, - "content" => note.text, - "mediaType" => "text/markdown", - "published" => Timex.format!(note.inserted_at, "{ISO:Extended}"), - "updated" => Timex.format!(note.updated_at, "{ISO:Extended}") + "id" => comment.url, + "content" => comment.text, + "mediaType" => "text/html", + "published" => Timex.format!(comment.inserted_at, "{ISO:Extended}"), + "updated" => Timex.format!(comment.updated_at, "{ISO:Extended}") } - Map.merge(event, @base) + Map.merge(comment, Utils.make_json_ld_header()) end def render("category.json", %{category: category}) do - %{"title" => category.title} + %{ + "identifier" => category.id, + "name" => category.title + } end def render("category.json", %{category: nil}) do diff --git a/lib/service/activity_pub/activity_pub.ex b/lib/service/activity_pub/activity_pub.ex index 803021e7c..db4466aa7 100644 --- a/lib/service/activity_pub/activity_pub.ex +++ b/lib/service/activity_pub/activity_pub.ex @@ -16,7 +16,7 @@ defmodule Mobilizon.Service.ActivityPub do alias Mobilizon.Service.Federator - import Logger + require Logger import Mobilizon.Service.ActivityPub.Utils def get_recipients(data) do @@ -24,9 +24,21 @@ defmodule Mobilizon.Service.ActivityPub do end 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), - :ok <- insert_full_object(map) do - map = Map.put(map, "id", Ecto.UUID.generate()) + :ok <- insert_full_object(map, local) do + 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{ data: map, @@ -106,7 +118,8 @@ defmodule Mobilizon.Service.ActivityPub do 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] || %{} # only accept false as false value local = !(params[:local] == false) @@ -114,7 +127,7 @@ defmodule Mobilizon.Service.ActivityPub do with create_data <- make_create_data( - %{to: to, actor: actor, published: published, context: context, object: object}, + %{to: to, actor: actor, published: published, object: object}, additional ), {:ok, activity} <- insert(create_data, local), @@ -157,10 +170,14 @@ defmodule Mobilizon.Service.ActivityPub do end 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 <- maybe_federate(activity) do {:ok, activity} + else + {err, _} when err in [:already_following, :suspended] -> + {:error, err} end end @@ -199,9 +216,9 @@ defmodule Mobilizon.Service.ActivityPub do def create_public_activities(%Actor{} = actor) do end - def make_actor_from_url(url) do - with {:ok, data} <- fetch_and_prepare_user_from_url(url) do - Actors.insert_or_update_actor(data) + def make_actor_from_url(url, preload \\ false) do + with {:ok, data} <- fetch_and_prepare_actor_from_url(url) do + Actors.insert_or_update_actor(data, preload) else # Request returned 410 {:error, :actor_deleted} -> @@ -243,12 +260,13 @@ defmodule Mobilizon.Service.ActivityPub do end remote_inboxes = - followers + (remote_actors(activity) ++ followers) |> Enum.map(fn follower -> follower.shared_inbox_url end) |> Enum.uniq() {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) json = Jason.encode!(data) + Logger.debug("Remote inboxes are : #{inspect(remote_inboxes)}") Enum.each(remote_inboxes, fn inbox -> Federator.enqueue(:publish_single_ap, %{ @@ -273,6 +291,9 @@ defmodule Mobilizon.Service.ActivityPub do Logger.debug("signature") Logger.debug(inspect(signature)) + Logger.debug("body json") + Logger.debug(inspect(json)) + {:ok, response} = HTTPoison.post( inbox, @@ -284,8 +305,8 @@ defmodule Mobilizon.Service.ActivityPub do Logger.debug(inspect(response)) end - def fetch_and_prepare_user_from_url(url) do - Logger.debug("Fetching and preparing user from url") + def fetch_and_prepare_actor_from_url(url) do + Logger.debug("Fetching and preparing actor from url") with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(url, [Accept: "application/activity+json"], follow_redirect: true), @@ -297,7 +318,7 @@ defmodule Mobilizon.Service.ActivityPub do {:error, :actor_deleted} e -> - Logger.error("Could not decode user at fetch #{url}, #{inspect(e)}") + Logger.error("Could not decode actor at fetch #{url}, #{inspect(e)}") e end end diff --git a/lib/service/activity_pub/transmogrifier.ex b/lib/service/activity_pub/transmogrifier.ex index 1153cf087..2c093d96e 100644 --- a/lib/service/activity_pub/transmogrifier.ex +++ b/lib/service/activity_pub/transmogrifier.ex @@ -18,7 +18,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do object |> Map.put("actor", object["attributedTo"]) |> fix_attachments - |> fix_context # |> fix_in_reply_to |> fix_tag end @@ -31,10 +30,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do # # object # |> 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 -> # 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_context(object) do - object - |> Map.put("context", object["conversation"]) - end - def fix_attachments(object) do attachments = (object["attachment"] || []) @@ -87,7 +77,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do to: data["to"], object: object, actor: actor, - context: object["conversation"], local: false, published: data["published"], additional: @@ -104,12 +93,11 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do def handle_incoming( %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data ) 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, activity} <- ActivityPub.follow(follower, followed, id, false) do ActivityPub.accept(%{to: [follower.url], actor: followed.url, object: data, local: true}) - # Actors.follow(follower, followed) {:ok, activity} else e -> @@ -225,11 +213,10 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do object # |> set_sensitive # |> add_hashtags - # |> add_mention_tags + |> add_mention_tags # |> add_emoji_tags |> add_attributed_to # |> prepare_attachments - |> set_conversation |> set_reply_to_uri end @@ -239,6 +226,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do """ def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do + Logger.debug("Prepare outgoing for a note creation") + object = object |> prepare_object @@ -248,6 +237,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do |> Map.put("object", object) |> Map.put("@context", "https://www.w3.org/ns/activitystreams") + Logger.debug("Finished prepare outgoing for a note creation") + {:ok, data} end @@ -304,22 +295,23 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do # end # end # - # def add_mention_tags(object) do - # recipients = object["to"] ++ (object["cc"] || []) - # - # mentions = - # recipients - # |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end) - # |> Enum.filter(& &1) - # |> Enum.map(fn user -> - # %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"} - # end) - # - # tags = object["tag"] || [] - # - # object - # |> Map.put("tag", tags ++ mentions) - # end + def add_mention_tags(object) do + recipients = object["to"] ++ (object["cc"] || []) + + mentions = + recipients + |> Enum.map(fn url -> Actors.get_actor_by_url!(url) end) + |> Enum.filter(& &1) + |> Enum.map(fn actor -> + %{"type" => "Mention", "href" => actor.url, "name" => "@#{actor.preferred_username}"} + end) + + tags = object["tag"] || [] + + object + |> Map.put("tag", tags ++ mentions) + end + # # # TODO: we should probably send mtime instead of unix epoch time for updated # def add_emoji_tags(object) do @@ -342,9 +334,6 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do # |> Map.put("tag", tags ++ out) # end # - def set_conversation(object) do - Map.put(object, "conversation", object["context"]) - end # # def set_sensitive(object) do @@ -370,84 +359,4 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do # object # |> Map.put("attachment", attachments) # 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 diff --git a/lib/service/activity_pub/utils.ex b/lib/service/activity_pub/utils.ex index fdbb4be54..74a645fd2 100644 --- a/lib/service/activity_pub/utils.ex +++ b/lib/service/activity_pub/utils.ex @@ -20,16 +20,19 @@ defmodule Mobilizon.Service.ActivityPub.Utils do import Ecto.Query require Logger + def make_context(%Activity{data: %{"context" => context}}), do: context + def make_context(_), do: generate_context_id() + def make_json_ld_header do %{ "@context" => [ "https://www.w3.org/ns/activitystreams", + "https://litepub.github.io/litepub/context.jsonld", %{ - "manuallyApprovesFollowers" => "as:manuallyApprovesFollowers", - "sensitive" => "as:sensitive", + "sc" => "http://schema.org#", "Hashtag" => "as:Hashtag", - "toot" => "http://joinmastodon.org/ns#", - "Emoji" => "toot:Emoji" + "category" => "sc:category", + "uuid" => "sc:identifier" } ] } @@ -74,6 +77,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do Enqueues an activity for federation if it's local """ def maybe_federate(%Activity{local: true} = activity) do + Logger.debug("Maybe federate an activity") + priority = case activity.data["type"] do "Delete" -> 10 @@ -87,6 +92,14 @@ defmodule Mobilizon.Service.ActivityPub.Utils do 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 """ Adds an id and a published data if they aren't there, also adds it to an included object @@ -123,7 +136,12 @@ defmodule Mobilizon.Service.ActivityPub.Utils do @doc """ 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 with {:ok, _} <- Events.create_event(object_data) do :ok @@ -133,7 +151,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do @doc """ 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 with {:ok, %Actor{id: actor_id}} <- Actors.get_or_fetch_by_url(object_data["actor"]) do data = %{ @@ -142,8 +160,9 @@ defmodule Mobilizon.Service.ActivityPub.Utils do "actor_id" => actor_id, "in_reply_to_comment_id" => nil, "event_id" => nil, + "uuid" => object_data["uuid"], # probably - "local" => false + "local" => local } # We fetch the parent object @@ -193,7 +212,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do end end - def insert_full_object(_), do: :ok + def insert_full_object(_, _), do: :ok # def update_object_in_activities(%{data: %{"id" => id}} = object) do # # TODO @@ -236,30 +255,31 @@ defmodule Mobilizon.Service.ActivityPub.Utils do def make_comment_data( actor, to, - context, content_html, - attachments, - inReplyTo, - tags, + # attachments, + inReplyTo \\ nil, + # tags, cw \\ nil, cc \\ [] ) do + Logger.debug("Making comment data") + uuid = Ecto.UUID.generate() + object = %{ "type" => "Note", "to" => to, - "cc" => cc, + # "cc" => cc, "content" => content_html, - "summary" => cw, - "context" => context, - "attachment" => attachments, + # "summary" => cw, + # "attachment" => attachments, "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 object - |> Map.put("inReplyTo", inReplyTo.data["object"]["id"]) - |> Map.put("inReplyToStatusId", inReplyTo.id) + |> Map.put("inReplyTo", inReplyTo) else object end @@ -311,6 +331,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do 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 + Logger.debug("Make follow data") + data = %{ "type" => "Follow", "actor" => follower_id, @@ -319,7 +341,11 @@ defmodule Mobilizon.Service.ActivityPub.Utils do "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 # 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(), "actor" => params.actor.url, "object" => params.object, - "published" => published, - "context" => params.context + "published" => published } |> Map.merge(additional) end diff --git a/lib/service/federator.ex b/lib/service/federator.ex index 24d8abf07..27b676d5c 100644 --- a/lib/service/federator.ex +++ b/lib/service/federator.ex @@ -36,7 +36,7 @@ defmodule Mobilizon.Service.Federator do Logger.debug(inspect(activity)) 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) ActivityPub.publish(actor, activity) end diff --git a/lib/service/http_signatures/http_signatures.ex b/lib/service/http_signatures/http_signatures.ex index e6e2f475e..e5e0d927b 100644 --- a/lib/service/http_signatures/http_signatures.ex +++ b/lib/service/http_signatures/http_signatures.ex @@ -90,17 +90,21 @@ defmodule Mobilizon.Service.HTTPSignatures do end def sign(%Actor{} = actor, headers) do - sigstring = build_signing_string(headers, Map.keys(headers)) - - signature = sigstring |> :public_key.sign(:sha256, actor.keys) |> Base.encode64() - - [ - 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(",") + with sigstring <- build_signing_string(headers, Map.keys(headers)), + {:ok, key} <- actor.keys |> 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)) + end end end diff --git a/lib/service/web_finger/web_finger.ex b/lib/service/web_finger/web_finger.ex index fbde30b09..65217cb0b 100644 --- a/lib/service/web_finger/web_finger.ex +++ b/lib/service/web_finger/web_finger.ex @@ -38,7 +38,7 @@ defmodule Mobilizon.Service.WebFinger do {:ok, represent_user(user, "JSON")} else _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")} else _e ->