diff options
Diffstat (limited to 'lib/pleroma/web/activity_pub')
-rw-r--r-- | lib/pleroma/web/activity_pub/activity_pub.ex | 296 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/activity_pub_controller.ex | 67 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/mrf/drop_policy.ex | 8 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/mrf/noop_policy.ex | 5 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/mrf/simple_policy.ex | 84 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/transmogrifier.ex | 298 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/utils.ex | 139 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/views/user_view.ex | 143 |
8 files changed, 814 insertions, 226 deletions
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 965f2cc9b..fde6e12d7 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -10,6 +10,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do @httpoison Application.get_env(:pleroma, :httpoison) + @instance Application.get_env(:pleroma, :instance) + @rewrite_policy Keyword.get(@instance, :rewrite_policy) + def get_recipients(data) do (data["to"] || []) ++ (data["cc"] || []) end @@ -17,8 +20,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def insert(map, local \\ true) when is_map(map) do with nil <- Activity.get_by_ap_id(map["id"]), map <- lazy_put_activity_defaults(map), + {:ok, map} <- @rewrite_policy.filter(map), :ok <- insert_full_object(map) do - {:ok, activity} = Repo.insert(%Activity{data: map, local: local, actor: map["actor"], recipients: get_recipients(map)}) + {:ok, activity} = + Repo.insert(%Activity{ + data: map, + local: local, + actor: map["actor"], + recipients: get_recipients(map) + }) + Notification.create_notifications(activity) stream_out(activity) {:ok, activity} @@ -31,8 +42,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def stream_out(activity) do if activity.data["type"] in ["Create", "Announce"] do Pleroma.Web.Streamer.stream("user", activity) + if Enum.member?(activity.data["to"], "https://www.w3.org/ns/activitystreams#Public") do Pleroma.Web.Streamer.stream("public", activity) + if activity.local do Pleroma.Web.Streamer.stream("public:local", activity) end @@ -42,18 +55,25 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def create(%{to: to, actor: actor, context: context, object: object} = params) do additional = params[:additional] || %{} - local = !(params[:local] == false) # only accept false as false value + # only accept false as false value + local = !(params[:local] == false) published = params[:published] - with create_data <- make_create_data(%{to: to, actor: actor, published: published, context: context, object: object}, additional), + with create_data <- + make_create_data( + %{to: to, actor: actor, published: published, context: context, object: object}, + additional + ), {:ok, activity} <- insert(create_data, local), - :ok <- maybe_federate(activity) do + :ok <- maybe_federate(activity), + {:ok, _actor} <- User.increase_note_count(actor) do {:ok, activity} end end def accept(%{to: to, actor: actor, object: object} = params) do - local = !(params[:local] == false) # only accept false as false value + # only accept false as false value + local = !(params[:local] == false) with data <- %{"to" => to, "type" => "Accept", "actor" => actor, "object" => object}, {:ok, activity} <- insert(data, local), @@ -63,9 +83,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end def update(%{to: to, cc: cc, actor: actor, object: object} = params) do - local = !(params[:local] == false) # only accept false as false value - - with data <- %{"to" => to, "cc" => cc, "type" => "Update", "actor" => actor, "object" => object}, + # only accept false as false value + local = !(params[:local] == false) + + with data <- %{ + "to" => to, + "cc" => cc, + "type" => "Update", + "actor" => actor, + "object" => object + }, {:ok, activity} <- insert(data, local), :ok <- maybe_federate(activity) do {:ok, activity} @@ -73,7 +100,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end # TODO: This is weird, maybe we shouldn't check here if we can make the activity. - def like(%User{ap_id: ap_id} = user, %Object{data: %{"id" => _}} = object, activity_id \\ nil, local \\ true) do + def like( + %User{ap_id: ap_id} = user, + %Object{data: %{"id" => _}} = object, + activity_id \\ nil, + local \\ true + ) do with nil <- get_existing_like(ap_id, object), like_data <- make_like_data(user, object, activity_id), {:ok, activity} <- insert(like_data, local), @@ -91,11 +123,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do {:ok, _activity} <- Repo.delete(activity), {:ok, object} <- remove_like_from_object(activity, object) do {:ok, object} - else _e -> {:ok, object} + else + _e -> {:ok, object} end end - def announce(%User{ap_id: _} = user, %Object{data: %{"id" => _}} = object, activity_id \\ nil, local \\ true) do + def announce( + %User{ap_id: _} = user, + %Object{data: %{"id" => _}} = object, + activity_id \\ nil, + local \\ true + ) do with true <- is_public?(object), announce_data <- make_announce_data(user, object, activity_id), {:ok, activity} <- insert(announce_data, local), @@ -119,135 +157,186 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed), unfollow_data <- make_unfollow_data(follower, followed, follow_activity), {:ok, activity} <- insert(unfollow_data, local), - :ok, maybe_federate(activity) do + :ok, + maybe_federate(activity) do {:ok, activity} end end def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do user = User.get_cached_by_ap_id(actor) + data = %{ "type" => "Delete", "actor" => actor, "object" => id, "to" => [user.follower_address, "https://www.w3.org/ns/activitystreams#Public"] } + with Repo.delete(object), Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)), {:ok, activity} <- insert(data, local), - :ok <- maybe_federate(activity) do + :ok <- maybe_federate(activity), + {:ok, _actor} <- User.decrease_note_count(user) do {:ok, activity} end end def fetch_activities_for_context(context, opts \\ %{}) do public = ["https://www.w3.org/ns/activitystreams#Public"] - recipients = if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public - query = from activity in Activity - query = query + recipients = + if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public + + query = from(activity in Activity) + + query = + query |> restrict_blocked(opts) |> restrict_recipients(recipients, opts["user"]) - query = from activity in query, - where: fragment("?->>'type' = ? and ?->>'context' = ?", activity.data, "Create", activity.data, ^context), - order_by: [desc: :id] + query = + from( + activity in query, + where: + fragment( + "?->>'type' = ? and ?->>'context' = ?", + activity.data, + "Create", + activity.data, + ^context + ), + order_by: [desc: :id] + ) + Repo.all(query) end # TODO: Make this work properly with unlisted. def fetch_public_activities(opts \\ %{}) do q = fetch_activities_query(["https://www.w3.org/ns/activitystreams#Public"], opts) + q - |> Repo.all - |> Enum.reverse + |> Repo.all() + |> Enum.reverse() end defp restrict_since(query, %{"since_id" => since_id}) do - from activity in query, where: activity.id > ^since_id + from(activity in query, where: activity.id > ^since_id) end + defp restrict_since(query, _), do: query defp restrict_tag(query, %{"tag" => tag}) do - from activity in query, + from( + activity in query, where: fragment("? <@ (? #> '{\"object\",\"tag\"}')", ^tag, activity.data) + ) end + defp restrict_tag(query, _), do: query - defp restrict_recipients(query, [], user), do: query + defp restrict_recipients(query, [], _user), do: query + defp restrict_recipients(query, recipients, nil) do - from activity in query, - where: fragment("? && ?", ^recipients, activity.recipients) + from(activity in query, where: fragment("? && ?", ^recipients, activity.recipients)) end + defp restrict_recipients(query, recipients, user) do - from activity in query, + from( + activity in query, where: fragment("? && ?", ^recipients, activity.recipients), or_where: activity.actor == ^user.ap_id + ) end + defp restrict_limit(query, %{"limit" => limit}) do + from(activity in query, limit: ^limit) + end + + defp restrict_limit(query, _), do: query + defp restrict_local(query, %{"local_only" => true}) do - from activity in query, where: activity.local == true + from(activity in query, where: activity.local == true) end + defp restrict_local(query, _), do: query defp restrict_max(query, %{"max_id" => max_id}) do - from activity in query, where: activity.id < ^max_id + from(activity in query, where: activity.id < ^max_id) end + defp restrict_max(query, _), do: query defp restrict_actor(query, %{"actor_id" => actor_id}) do - from activity in query, - where: activity.actor == ^actor_id + from(activity in query, where: activity.actor == ^actor_id) end + defp restrict_actor(query, _), do: query defp restrict_type(query, %{"type" => type}) when is_binary(type) do restrict_type(query, %{"type" => [type]}) end + defp restrict_type(query, %{"type" => type}) do - from activity in query, - where: fragment("?->>'type' = ANY(?)", activity.data, ^type) + from(activity in query, where: fragment("?->>'type' = ANY(?)", activity.data, ^type)) end + defp restrict_type(query, _), do: query defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do - from activity in query, + from( + activity in query, where: fragment("? <@ (? #> '{\"object\",\"likes\"}')", ^ap_id, activity.data) + ) end + defp restrict_favorited_by(query, _), do: query defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do - from activity in query, + from( + activity in query, where: fragment("not (? #> '{\"object\",\"attachment\"}' = ?)", activity.data, ^[]) + ) end + defp restrict_media(query, _), do: query # Only search through last 100_000 activities by default defp restrict_recent(query, %{"whole_db" => true}), do: query + defp restrict_recent(query, _) do since = (Repo.aggregate(Activity, :max, :id) || 0) - 100_000 - from activity in query, - where: activity.id > ^since + from(activity in query, where: activity.id > ^since) end defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do blocks = info["blocks"] || [] - from activity in query, - where: fragment("not (? = ANY(?))", activity.actor, ^blocks) + + from( + activity in query, + where: fragment("not (? = ANY(?))", activity.actor, ^blocks), + where: fragment("not (?->'to' \\?| ?)", activity.data, ^blocks) + ) end + defp restrict_blocked(query, _), do: query def fetch_activities_query(recipients, opts \\ %{}) do - base_query = from activity in Activity, - limit: 20, - order_by: [fragment("? desc nulls last", activity.id)] + base_query = + from( + activity in Activity, + limit: 20, + order_by: [fragment("? desc nulls last", activity.id)] + ) base_query |> restrict_recipients(recipients, opts["user"]) |> restrict_tag(opts) |> restrict_since(opts) |> restrict_local(opts) + |> restrict_limit(opts) |> restrict_max(opts) |> restrict_actor(opts) |> restrict_type(opts) @@ -259,8 +348,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def fetch_activities(recipients, opts \\ %{}) do fetch_activities_query(recipients, opts) - |> Repo.all - |> Enum.reverse + |> Repo.all() + |> Enum.reverse() end def upload(file) do @@ -269,15 +358,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end def user_data_from_user_object(data) do - avatar = data["icon"]["url"] && %{ - "type" => "Image", - "url" => [%{"href" => data["icon"]["url"]}] - } - - banner = data["image"]["url"] && %{ - "type" => "Image", - "url" => [%{"href" => data["image"]["url"]}] - } + avatar = + data["icon"]["url"] && + %{ + "type" => "Image", + "url" => [%{"href" => data["icon"]["url"]}] + } + + banner = + data["image"]["url"] && + %{ + "type" => "Image", + "url" => [%{"href" => data["image"]["url"]}] + } user_data = %{ ap_id: data["id"], @@ -297,16 +390,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end def fetch_and_prepare_user_from_ap_id(ap_id) do - with {:ok, %{status_code: 200, body: body}} <- @httpoison.get(ap_id, ["Accept": "application/activity+json"]), - {:ok, data} <- Poison.decode(body) do + with {:ok, %{status_code: 200, body: body}} <- + @httpoison.get(ap_id, Accept: "application/activity+json"), + {:ok, data} <- Jason.decode(body) do user_data_from_user_object(data) else - e -> Logger.error("Could not user at fetch #{ap_id}, #{inspect(e)}") + e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}") end end def make_user_from_ap_id(ap_id) do - if user = User.get_by_ap_id(ap_id) do + if _user = User.get_by_ap_id(ap_id) do Transmogrifier.upgrade_user_from_ap_id(ap_id) else with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do @@ -321,37 +415,53 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do with {:ok, %{"ap_id" => ap_id}} when not is_nil(ap_id) <- WebFinger.finger(nickname) do make_user_from_ap_id(ap_id) else - _e -> {:error, "No ap id in webfinger"} + _e -> {:error, "No AP id in WebFinger"} end end def publish(actor, activity) do - followers = if actor.follower_address in activity.recipients do - {:ok, followers} = User.get_followers(actor) - followers |> Enum.filter(&(!&1.local)) - else - [] - end + followers = + if actor.follower_address in activity.recipients do + {:ok, followers} = User.get_followers(actor) + followers |> Enum.filter(&(!&1.local)) + else + [] + end - remote_inboxes = (Pleroma.Web.Salmon.remote_users(activity) ++ followers) - |> Enum.filter(fn (user) -> User.ap_enabled?(user) end) - |> Enum.map(fn (%{info: %{"source_data" => data}}) -> - (data["endpoints"] && data["endpoints"]["sharedInbox"]) || data["inbox"] - end) - |> Enum.uniq + remote_inboxes = + (Pleroma.Web.Salmon.remote_users(activity) ++ followers) + |> Enum.filter(fn user -> User.ap_enabled?(user) end) + |> Enum.map(fn %{info: %{"source_data" => data}} -> + (data["endpoints"] && data["endpoints"]["sharedInbox"]) || data["inbox"] + end) + |> Enum.uniq() {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) - json = Poison.encode!(data) - Enum.each remote_inboxes, fn(inbox) -> - Federator.enqueue(:publish_single_ap, %{inbox: inbox, json: json, actor: actor, id: activity.data["id"]}) - end + json = Jason.encode!(data) + + Enum.each(remote_inboxes, fn inbox -> + Federator.enqueue(:publish_single_ap, %{ + inbox: inbox, + json: json, + actor: actor, + id: activity.data["id"] + }) + end) end def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do Logger.info("Federating #{id} to #{inbox}") host = URI.parse(inbox).host - signature = Pleroma.Web.HTTPSignatures.sign(actor, %{host: host, "content-length": byte_size(json)}) - @httpoison.post(inbox, json, [{"Content-Type", "application/activity+json"}, {"signature", signature}]) + + signature = + Pleroma.Web.HTTPSignatures.sign(actor, %{host: host, "content-length": byte_size(json)}) + + @httpoison.post( + inbox, + json, + [{"Content-Type", "application/activity+json"}, {"signature", signature}], + hackney: [pool: :default] + ) end # TODO: @@ -361,16 +471,34 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do {:ok, object} else Logger.info("Fetching #{id} via AP") - with {:ok, %{body: body, status_code: code}} when code in 200..299 <- @httpoison.get(id, [Accept: "application/activity+json"], follow_redirect: true, timeout: 10000, recv_timeout: 20000), - {:ok, data} <- Poison.decode(body), + + with true <- String.starts_with?(id, "http"), + {:ok, %{body: body, status_code: code}} when code in 200..299 <- + @httpoison.get( + id, + [Accept: "application/activity+json"], + follow_redirect: true, + timeout: 10000, + recv_timeout: 20000 + ), + {:ok, data} <- Jason.decode(body), nil <- Object.get_by_ap_id(data["id"]), - params <- %{"type" => "Create", "to" => data["to"], "cc" => data["cc"], "actor" => data["attributedTo"], "object" => data}, + params <- %{ + "type" => "Create", + "to" => data["to"], + "cc" => data["cc"], + "actor" => data["attributedTo"], + "object" => data + }, {:ok, activity} <- Transmogrifier.handle_incoming(params) do {:ok, Object.get_by_ap_id(activity.data["object"]["id"])} else - object = %Object{} -> {:ok, object} - e -> + object = %Object{} -> + {:ok, object} + + _e -> Logger.info("Couldn't get object via AP, trying out OStatus fetching...") + case OStatus.fetch_activity_from_url(id) do {:ok, [activity | _]} -> {:ok, Object.get_by_ap_id(activity.data["object"]["id"])} e -> e @@ -380,15 +508,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end def is_public?(activity) do - "https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++ (activity.data["cc"] || [])) + "https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++ + (activity.data["cc"] || [])) end def visible_for_user?(activity, nil) do is_public?(activity) end + def visible_for_user?(activity, user) do x = [user.ap_id | user.following] - y = (activity.data["to"] ++ (activity.data["cc"] || [])) + y = activity.data["to"] ++ (activity.data["cc"] || []) visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y)) end end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index edbcb938a..80aae4f0f 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -1,13 +1,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do use Pleroma.Web, :controller - alias Pleroma.{User, Repo, Object, Activity} - alias Pleroma.Web.ActivityPub.{ObjectView, UserView, Transmogrifier} + alias Pleroma.{User, Object} + alias Pleroma.Web.ActivityPub.{ObjectView, UserView} alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.Federator require Logger - action_fallback :errors + action_fallback(:errors) def user(conn, %{"nickname" => nickname}) do with %User{} = user <- User.get_cached_by_nickname(nickname), @@ -27,6 +27,59 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end end + def following(conn, %{"nickname" => nickname, "page" => page}) do + with %User{} = user <- User.get_cached_by_nickname(nickname), + {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do + {page, _} = Integer.parse(page) + + conn + |> put_resp_header("content-type", "application/activity+json") + |> json(UserView.render("following.json", %{user: user, page: page})) + end + end + + def following(conn, %{"nickname" => nickname}) do + with %User{} = user <- User.get_cached_by_nickname(nickname), + {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do + conn + |> put_resp_header("content-type", "application/activity+json") + |> json(UserView.render("following.json", %{user: user})) + end + end + + def followers(conn, %{"nickname" => nickname, "page" => page}) do + with %User{} = user <- User.get_cached_by_nickname(nickname), + {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do + {page, _} = Integer.parse(page) + + conn + |> put_resp_header("content-type", "application/activity+json") + |> json(UserView.render("followers.json", %{user: user, page: page})) + end + end + + def followers(conn, %{"nickname" => nickname}) do + with %User{} = user <- User.get_cached_by_nickname(nickname), + {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do + conn + |> put_resp_header("content-type", "application/activity+json") + |> json(UserView.render("followers.json", %{user: user})) + end + end + + def outbox(conn, %{"nickname" => nickname, "max_id" => max_id}) do + with %User{} = user <- User.get_cached_by_nickname(nickname), + {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do + conn + |> put_resp_header("content-type", "application/activity+json") + |> json(UserView.render("outbox.json", %{user: user, max_id: max_id})) + end + end + + def outbox(conn, %{"nickname" => nickname}) do + outbox(conn, %{"nickname" => nickname, "max_id" => nil}) + end + # TODO: Ensure that this inbox is a recipient of the message def inbox(%{assigns: %{valid_signature: true}} = conn, params) do Federator.enqueue(:incoming_ap_doc, params) @@ -35,10 +88,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do def inbox(conn, params) do headers = Enum.into(conn.req_headers, %{}) - if !(String.contains?(headers["signature"] || "", params["actor"])) do - Logger.info("Signature not from author, relayed message, ignoring.") + + if !String.contains?(headers["signature"] || "", params["actor"]) do + Logger.info("Signature not from author, relayed message, fetching from source") + ActivityPub.fetch_object_from_id(params["object"]["id"]) else - Logger.info("Signature error.") + Logger.info("Signature error") Logger.info("Could not validate #{params["actor"]}") Logger.info(inspect(conn.req_headers)) end diff --git a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex new file mode 100644 index 000000000..4333bca28 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex @@ -0,0 +1,8 @@ +defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do + require Logger + + def filter(object) do + Logger.info("REJECTING #{inspect(object)}") + {:reject, object} + end +end diff --git a/lib/pleroma/web/activity_pub/mrf/noop_policy.ex b/lib/pleroma/web/activity_pub/mrf/noop_policy.ex new file mode 100644 index 000000000..9dd3acb04 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/noop_policy.ex @@ -0,0 +1,5 @@ +defmodule Pleroma.Web.ActivityPub.MRF.NoOpPolicy do + def filter(object) do + {:ok, object} + end +end diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex new file mode 100644 index 000000000..d840d759d --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -0,0 +1,84 @@ +defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do + alias Pleroma.User + + @mrf_policy Application.get_env(:pleroma, :mrf_simple) + + @reject Keyword.get(@mrf_policy, :reject) + defp check_reject(actor_info, object) do + if actor_info.host in @reject do + {:reject, nil} + else + {:ok, object} + end + end + + @media_removal Keyword.get(@mrf_policy, :media_removal) + defp check_media_removal(actor_info, object) do + if actor_info.host in @media_removal do + child_object = Map.delete(object["object"], "attachment") + object = Map.put(object, "object", child_object) + {:ok, object} + else + {:ok, object} + end + end + + @media_nsfw Keyword.get(@mrf_policy, :media_nsfw) + defp check_media_nsfw(actor_info, object) do + child_object = object["object"] + + if actor_info.host in @media_nsfw and child_object["attachment"] != nil and + length(child_object["attachment"]) > 0 do + tags = (child_object["tag"] || []) ++ ["nsfw"] + child_object = Map.put(child_object, "tags", tags) + child_object = Map.put(child_object, "sensitive", true) + object = Map.put(object, "object", child_object) + {:ok, object} + else + {:ok, object} + end + end + + @ftl_removal Keyword.get(@mrf_policy, :federated_timeline_removal) + defp check_ftl_removal(actor_info, object) do + if actor_info.host in @ftl_removal do + user = User.get_by_ap_id(object["actor"]) + + # flip to/cc relationship to make the post unlisted + object = + if "https://www.w3.org/ns/activitystreams#Public" in object["to"] and + user.follower_address in object["cc"] do + to = + List.delete(object["to"], "https://www.w3.org/ns/activitystreams#Public") ++ + [user.follower_address] + + cc = + List.delete(object["cc"], user.follower_address) ++ + ["https://www.w3.org/ns/activitystreams#Public"] + + object + |> Map.put("to", to) + |> Map.put("cc", cc) + else + object + end + + {:ok, object} + else + {:ok, object} + end + end + + def filter(object) do + actor_info = URI.parse(object["actor"]) + + with {:ok, object} <- check_reject(actor_info, object), + {:ok, object} <- check_media_removal(actor_info, object), + {:ok, object} <- check_media_nsfw(actor_info, object), + {:ok, object} <- check_ftl_removal(actor_info, object) do + {:ok, object} + else + _e -> {:reject, nil} + end + end +end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index d759ca2b2..a0e45510c 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -22,23 +22,28 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> fix_context |> fix_in_reply_to |> fix_emoji + |> fix_tag end - def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object) when not is_nil(in_reply_to_id) do + def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object) + when not is_nil(in_reply_to_id) do case ActivityPub.fetch_object_from_id(in_reply_to_id) do {:ok, replied_object} -> activity = Activity.get_create_activity_by_object_ap_id(replied_object.data["id"]) + 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)}") object end end + def fix_in_reply_to(object), do: object def fix_context(object) do @@ -47,27 +52,35 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def fix_attachments(object) do - attachments = (object["attachment"] || []) - |> Enum.map(fn (data) -> - url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}] - Map.put(data, "url", url) - end) + attachments = + (object["attachment"] || []) + |> Enum.map(fn data -> + url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}] + Map.put(data, "url", url) + end) object |> Map.put("attachment", attachments) end def fix_emoji(object) do - tags = (object["tag"] || []) - emoji = tags |> Enum.filter(fn (data) -> data["type"] == "Emoji" and data["icon"] end) - emoji = emoji |> Enum.reduce(%{}, fn (data, mapping) -> - name = data["name"] - if String.starts_with?(name, ":") do - name = name |> String.slice(1..-2) - end + tags = object["tag"] || [] + emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end) - mapping |> Map.put(name, data["icon"]["url"]) - end) + emoji = + emoji + |> Enum.reduce(%{}, fn data, mapping -> + name = data["name"] + + name = + if String.starts_with?(name, ":") do + name = name |> String.slice(1..-2) + else + name + end + + mapping |> Map.put(name, data["icon"]["url"]) + end) # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats emoji = Map.merge(object["emoji"] || %{}, emoji) @@ -76,6 +89,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> Map.put("emoji", emoji) end + def fix_tag(object) do + tags = + (object["tag"] || []) + |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end) + |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end) + + combined = (object["tag"] || []) ++ tags + + object + |> Map.put("tag", combined) + end + # TODO: validate those with a Ecto scheme # - tags # - emoji @@ -91,13 +116,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do context: object["conversation"], local: false, published: data["published"], - additional: Map.take(data, [ - "cc", - "id" - ]) + additional: + Map.take(data, [ + "cc", + "id" + ]) } - ActivityPub.create(params) else %Activity{} = activity -> {:ok, activity} @@ -105,11 +130,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def handle_incoming(%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data) do + def handle_incoming( + %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data + ) do with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), %User{} = follower <- User.get_or_fetch_by_ap_id(follower), {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do ActivityPub.accept(%{to: [follower.ap_id], actor: followed.ap_id, object: data, local: true}) + User.follow(follower, followed) {:ok, activity} else @@ -117,40 +145,57 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def handle_incoming(%{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = data) do + def handle_incoming( + %{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = _data + ) do with %User{} = actor <- User.get_or_fetch_by_ap_id(actor), - {:ok, object} <- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id), - {:ok, activity, object} <- ActivityPub.like(actor, object, id, false) do + {:ok, object} <- + get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id), + {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do {:ok, activity} else _e -> :error end end - def handle_incoming(%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data) do + def handle_incoming( + %{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = _data + ) do with %User{} = actor <- User.get_or_fetch_by_ap_id(actor), - {:ok, object} <- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id), - {:ok, activity, object} <- ActivityPub.announce(actor, object, id, false) do + {:ok, object} <- + get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id), + {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false) do {:ok, activity} else _e -> :error end end - def handle_incoming(%{"type" => "Update", "object" => %{"type" => "Person"} = object, "actor" => actor_id} = data) do + def handle_incoming( + %{"type" => "Update", "object" => %{"type" => "Person"} = object, "actor" => actor_id} = + data + ) do with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object) banner = new_user_data[:info]["banner"] - update_data = new_user_data - |> Map.take([:name, :bio, :avatar]) - |> Map.put(:info, Map.merge(actor.info, %{"banner" => banner})) + + update_data = + new_user_data + |> Map.take([:name, :bio, :avatar]) + |> Map.put(:info, Map.merge(actor.info, %{"banner" => banner})) actor |> User.upgrade_changeset(update_data) |> User.update_and_set_cache() - ActivityPub.update(%{local: false, to: data["to"] || [], cc: data["cc"] || [], object: object, actor: actor_id}) + ActivityPub.update(%{ + local: false, + to: data["to"] || [], + cc: data["cc"] || [], + object: object, + actor: actor_id + }) else e -> Logger.error(e) @@ -159,17 +204,22 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end # TODO: Make secure. - def handle_incoming(%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data) do - object_id = case object_id do - %{"id" => id} -> id - id -> id - end - with %User{} = actor <- User.get_or_fetch_by_ap_id(actor), - {:ok, object} <- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id), + def handle_incoming( + %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = _data + ) do + object_id = + case object_id do + %{"id" => id} -> id + id -> id + end + + with %User{} = _actor <- User.get_or_fetch_by_ap_id(actor), + {:ok, object} <- + get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id), {:ok, activity} <- ActivityPub.delete(object, false) do {:ok, activity} else - e -> :error + _e -> :error end end @@ -183,6 +233,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do if object = Object.get_by_ap_id(id), do: {:ok, object}, else: nil end + def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) do + with false <- String.starts_with?(inReplyTo, "http"), + {:ok, %{data: replied_to_object}} <- get_obj_helper(inReplyTo) do + Map.put(object, "inReplyTo", replied_to_object["external_url"] || inReplyTo) + else + _e -> object + end + end + + def set_reply_to_uri(obj), do: obj + + # Prepares the object of an outgoing create activity. def prepare_object(object) do object |> set_sensitive @@ -192,26 +254,32 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> add_attributed_to |> prepare_attachments |> set_conversation + |> set_reply_to_uri end - @doc - """ - internal -> Mastodon - """ + # @doc + # """ + # internal -> Mastodon + # """ + def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do - object = object - |> prepare_object - data = data - |> Map.put("object", object) - |> Map.put("@context", "https://www.w3.org/ns/activitystreams") + object = + object + |> prepare_object + + data = + data + |> Map.put("object", object) + |> Map.put("@context", "https://www.w3.org/ns/activitystreams") {:ok, data} end - def prepare_outgoing(%{"type" => type} = data) do - data = data - |> maybe_fix_object_url - |> Map.put("@context", "https://www.w3.org/ns/activitystreams") + def prepare_outgoing(%{"type" => _type} = data) do + data = + data + |> maybe_fix_object_url + |> Map.put("@context", "https://www.w3.org/ns/activitystreams") {:ok, data} end @@ -221,11 +289,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do case ActivityPub.fetch_object_from_id(data["object"]) do {:ok, relative_object} -> if relative_object.data["external_url"] do - data = data - |> Map.put("object", relative_object.data["external_url"]) + _data = + data + |> Map.put("object", relative_object.data["external_url"]) else data end + e -> Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}") data @@ -236,8 +306,15 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def add_hashtags(object) do - tags = (object["tag"] || []) - |> Enum.map fn (tag) -> %{"href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}", "name" => "##{tag}", "type" => "Hashtag"} end + tags = + (object["tag"] || []) + |> Enum.map(fn tag -> + %{ + "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}", + "name" => "##{tag}", + "type" => "Hashtag" + } + end) object |> Map.put("tag", tags) @@ -245,10 +322,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do 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) + + 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"] || [] @@ -260,13 +341,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def add_emoji_tags(object) do tags = object["tag"] || [] emoji = object["emoji"] || [] - out = emoji |> Enum.map(fn {name, url} -> - %{"icon" => %{"url" => url, "type" => "Image"}, - "name" => ":" <> name <> ":", - "type" => "Emoji", - "updated" => "1970-01-01T00:00:00Z", - "id" => url} - end) + + out = + emoji + |> Enum.map(fn {name, url} -> + %{ + "icon" => %{"url" => url, "type" => "Image"}, + "name" => ":" <> name <> ":", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z", + "id" => url + } + end) object |> Map.put("tag", tags ++ out) @@ -289,11 +375,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def prepare_attachments(object) do - attachments = (object["attachment"] || []) - |> Enum.map(fn (data) -> - [%{"mediaType" => media_type, "href" => href} | _] = data["url"] - %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"} - end) + attachments = + (object["attachment"] || []) + |> Enum.map(fn data -> + [%{"mediaType" => media_type, "href" => href} | _] = data["url"] + %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"} + end) object |> Map.put("attachment", attachments) @@ -301,9 +388,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do 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)]] + + 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) @@ -311,22 +413,40 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do # 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)]] + + 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])) + 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() + + {: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 @@ -347,9 +467,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def maybe_retire_websub(ap_id) do # some sanity checks - if is_binary(ap_id) && (String.length(ap_id) > 8) do - q = from ws in Pleroma.Web.Websub.WebsubClientSubscription, - where: fragment("? like ?", ws.topic, ^"#{ap_id}%") + if is_binary(ap_id) && String.length(ap_id) > 8 do + q = + from( + ws in Pleroma.Web.Websub.WebsubClientSubscription, + where: fragment("? like ?", ws.topic, ^"#{ap_id}%") + ) + Repo.delete_all(q) end end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index cda106283..7b2bf8fa7 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -5,8 +5,28 @@ defmodule Pleroma.Web.ActivityPub.Utils do alias Ecto.{Changeset, UUID} import Ecto.Query + def make_json_ld_header do + %{ + "@context" => [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + %{ + "manuallyApprovesFollowers" => "as:manuallyApprovesFollowers", + "sensitive" => "as:sensitive", + "Hashtag" => "as:Hashtag", + "ostatus" => "http://ostatus.org#", + "atomUri" => "ostatus:atomUri", + "inReplyToAtomUri" => "ostatus:inReplyToAtomUri", + "conversation" => "ostatus:conversation", + "toot" => "http://joinmastodon.org/ns#", + "Emoji" => "toot:Emoji" + } + ] + } + end + def make_date do - DateTime.utc_now() |> DateTime.to_iso8601 + DateTime.utc_now() |> DateTime.to_iso8601() end def generate_activity_id do @@ -18,25 +38,43 @@ defmodule Pleroma.Web.ActivityPub.Utils do end def generate_object_id do - Helpers.o_status_url(Endpoint, :object, UUID.generate) + Helpers.o_status_url(Endpoint, :object, UUID.generate()) end def generate_id(type) do - "#{Web.base_url()}/#{type}/#{UUID.generate}" + "#{Web.base_url()}/#{type}/#{UUID.generate()}" + end + + def create_context(context) do + context = context || generate_id("contexts") + changeset = Object.context_mapping(context) + + case Repo.insert(changeset) do + {:ok, object} -> + object + + # This should be solved by an upsert, but it seems ecto + # has problems accessing the constraint inside the jsonb. + {:error, _} -> + Object.get_cached_by_ap_id(context) + end end @doc """ Enqueues an activity for federation if it's local """ def maybe_federate(%Activity{local: true} = activity) do - priority = case activity.data["type"] do - "Delete" -> 10 - "Create" -> 1 - _ -> 5 - end + priority = + case activity.data["type"] do + "Delete" -> 10 + "Create" -> 1 + _ -> 5 + end + Pleroma.Web.Federator.enqueue(:publish, activity, priority) :ok end + def maybe_federate(_), do: :ok @doc """ @@ -44,12 +82,17 @@ defmodule Pleroma.Web.ActivityPub.Utils do also adds it to an included object """ def lazy_put_activity_defaults(map) do - map = map - |> Map.put_new_lazy("id", &generate_activity_id/0) - |> Map.put_new_lazy("published", &make_date/0) + %{data: %{"id" => context}, id: context_id} = create_context(map["context"]) + + map = + map + |> Map.put_new_lazy("id", &generate_activity_id/0) + |> Map.put_new_lazy("published", &make_date/0) + |> Map.put_new("context", context) + |> Map.put_new("context_id", context_id) if is_map(map["object"]) do - object = lazy_put_object_defaults(map["object"]) + object = lazy_put_object_defaults(map["object"], map) %{map | "object" => object} else map @@ -59,20 +102,24 @@ defmodule Pleroma.Web.ActivityPub.Utils do @doc """ Adds an id and published date if they aren't there. """ - def lazy_put_object_defaults(map) do + def lazy_put_object_defaults(map, activity \\ %{}) do map |> Map.put_new_lazy("id", &generate_object_id/0) |> Map.put_new_lazy("published", &make_date/0) + |> Map.put_new("context", activity["context"]) + |> Map.put_new("context_id", activity["context_id"]) end @doc """ Inserts a full object if it is contained in an activity. """ - def insert_full_object(%{"object" => %{"type" => type} = object_data}) when is_map(object_data) and type in ["Note"] do + def insert_full_object(%{"object" => %{"type" => type} = object_data}) + when is_map(object_data) and type in ["Note"] do with {:ok, _} <- Object.create(object_data) do :ok end end + def insert_full_object(_), do: :ok def update_object_in_activities(%{data: %{"id" => id}} = object) do @@ -81,7 +128,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do # Alternatively, just don't do this and fetch the current object each time. Most # could probably be taken from cache. relevant_activities = Activity.all_by_object_ap_id(id) - Enum.map(relevant_activities, fn (activity) -> + + Enum.map(relevant_activities, fn activity -> new_activity_data = activity.data |> Map.put("object", object.data) changeset = Changeset.change(activity, data: new_activity_data) Repo.update(changeset) @@ -94,11 +142,20 @@ defmodule Pleroma.Web.ActivityPub.Utils do Returns an existing like if a user already liked an object """ def get_existing_like(actor, %{data: %{"id" => id}}) do - query = from activity in Activity, - where: fragment("(?)->>'actor' = ?", activity.data, ^actor), - # this is to use the index - where: fragment("coalesce((?)->'object'->>'id', (?)->>'object') = ?", activity.data, activity.data, ^id), - where: fragment("(?)->>'type' = 'Like'", activity.data) + query = + from( + activity in Activity, + where: fragment("(?)->>'actor' = ?", activity.data, ^actor), + # this is to use the index + where: + fragment( + "coalesce((?)->'object'->>'id', (?)->>'object') = ?", + activity.data, + activity.data, + ^id + ), + where: fragment("(?)->>'type' = 'Like'", activity.data) + ) Repo.one(query) end @@ -117,10 +174,13 @@ defmodule Pleroma.Web.ActivityPub.Utils do end def update_element_in_object(property, element, object) do - with new_data <- object.data |> Map.put("#{property}_count", length(element)) |> Map.put("#{property}s", element), + with new_data <- + object.data + |> Map.put("#{property}_count", length(element)) + |> Map.put("#{property}s", element), changeset <- Changeset.change(object, data: new_data), {:ok, object} <- Repo.update(changeset), - _ <- update_object_in_activities(object) do + _ <- update_object_in_activities(object) do {:ok, object} end end @@ -130,7 +190,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do end def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do - with likes <- [actor | (object.data["likes"] || [])] |> Enum.uniq do + with likes <- [actor | object.data["likes"] || []] |> Enum.uniq() do update_likes_in_object(likes, object) end end @@ -158,13 +218,20 @@ defmodule Pleroma.Web.ActivityPub.Utils do if activity_id, do: Map.put(data, "id", activity_id), else: data end - def fetch_latest_follow(%User{ap_id: follower_id}, - %User{ap_id: followed_id}) do - query = from activity in Activity, - where: fragment("? @> ?", activity.data, ^%{type: "Follow", actor: follower_id, - object: followed_id}), - order_by: [desc: :id], - limit: 1 + def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do + query = + from( + activity in Activity, + where: + fragment( + "? @> ?", + activity.data, + ^%{type: "Follow", actor: follower_id, object: followed_id} + ), + order_by: [desc: :id], + limit: 1 + ) + Repo.one(query) end @@ -173,7 +240,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do @doc """ Make announce activity data for the given actor and object """ - def make_announce_data(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object, activity_id) do + def make_announce_data( + %User{ap_id: ap_id} = user, + %Object{data: %{"id" => id}} = object, + activity_id + ) do data = %{ "type" => "Announce", "actor" => ap_id, @@ -187,7 +258,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do end def add_announce_to_object(%Activity{data: %{"actor" => actor}}, object) do - with announcements <- [actor | (object.data["announcements"] || [])] |> Enum.uniq do + with announcements <- [actor | object.data["announcements"] || []] |> Enum.uniq() do update_element_in_object("announcement", announcements, object) end end @@ -203,14 +274,14 @@ defmodule Pleroma.Web.ActivityPub.Utils do } end - #### Create-related helpers def make_create_data(params, additional) do published = params.published || make_date() + %{ "type" => "Create", - "to" => params.to |> Enum.uniq, + "to" => params.to |> Enum.uniq(), "actor" => params.actor.ap_id, "object" => params.object, "published" => published, diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 179636884..a1f0be9ed 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -3,28 +3,19 @@ defmodule Pleroma.Web.ActivityPub.UserView do alias Pleroma.Web.Salmon alias Pleroma.Web.WebFinger alias Pleroma.User + alias Pleroma.Repo + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.ActivityPub.Utils + import Ecto.Query def render("user.json", %{user: user}) do {:ok, user} = WebFinger.ensure_keys_present(user) {:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"]) public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key) public_key = :public_key.pem_encode([public_key]) + %{ - "@context" => [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - %{ - "manuallyApprovesFollowers" => "as:manuallyApprovesFollowers", - "sensitive" => "as:sensitive", - "Hashtag" => "as:Hashtag", - "ostatus" => "http://ostatus.org#", - "atomUri" => "ostatus:atomUri", - "inReplyToAtomUri" => "ostatus:inReplyToAtomUri", - "conversation" => "ostatus:conversation", - "toot" => "http://joinmastodon.org/ns#", - "Emoji" => "toot:Emoji" - } - ], "id" => user.ap_id, "type" => "Person", "following" => "#{user.ap_id}/following", @@ -42,7 +33,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do "publicKeyPem" => public_key }, "endpoints" => %{ - "sharedInbox" => "#{Pleroma.Web.Endpoint.url}/inbox" + "sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox" }, "icon" => %{ "type" => "Image", @@ -53,5 +44,125 @@ defmodule Pleroma.Web.ActivityPub.UserView do "url" => User.banner_url(user) } } + |> Map.merge(Utils.make_json_ld_header()) + end + + def render("following.json", %{user: user, page: page}) do + query = User.get_friends_query(user) + query = from(user in query, select: [:ap_id]) + following = Repo.all(query) + + collection(following, "#{user.ap_id}/following", page) + |> Map.merge(Utils.make_json_ld_header()) + end + + def render("following.json", %{user: user}) do + query = User.get_friends_query(user) + query = from(user in query, select: [:ap_id]) + following = Repo.all(query) + + %{ + "id" => "#{user.ap_id}/following", + "type" => "OrderedCollection", + "totalItems" => length(following), + "first" => collection(following, "#{user.ap_id}/following", 1) + } + |> Map.merge(Utils.make_json_ld_header()) + end + + def render("followers.json", %{user: user, page: page}) do + query = User.get_followers_query(user) + query = from(user in query, select: [:ap_id]) + followers = Repo.all(query) + + collection(followers, "#{user.ap_id}/followers", page) + |> Map.merge(Utils.make_json_ld_header()) + end + + def render("followers.json", %{user: user}) do + query = User.get_followers_query(user) + query = from(user in query, select: [:ap_id]) + followers = Repo.all(query) + + %{ + "id" => "#{user.ap_id}/followers", + "type" => "OrderedCollection", + "totalItems" => length(followers), + "first" => collection(followers, "#{user.ap_id}/followers", 1) + } + |> Map.merge(Utils.make_json_ld_header()) + end + + def render("outbox.json", %{user: user, max_id: max_qid}) do + # XXX: technically note_count is wrong for this, but it's better than nothing + info = User.user_info(user) + + params = %{ + "type" => ["Create", "Announce"], + "actor_id" => user.ap_id, + "whole_db" => true, + "limit" => "10" + } + + params = + if max_qid != nil do + Map.put(params, "max_id", max_qid) + else + params + end + + activities = ActivityPub.fetch_public_activities(params) + min_id = Enum.at(activities, 0).id + + activities = Enum.reverse(activities) + max_id = Enum.at(activities, 0).id + + collection = + Enum.map(activities, fn act -> + {:ok, data} = Transmogrifier.prepare_outgoing(act.data) + data + end) + + iri = "#{user.ap_id}/outbox" + + page = %{ + "id" => "#{iri}?max_id=#{max_id}", + "type" => "OrderedCollectionPage", + "partOf" => iri, + "totalItems" => info.note_count, + "orderedItems" => collection, + "next" => "#{iri}?max_id=#{min_id - 1}" + } + + if max_qid == nil do + %{ + "id" => iri, + "type" => "OrderedCollection", + "totalItems" => info.note_count, + "first" => page + } + |> Map.merge(Utils.make_json_ld_header()) + else + page |> Map.merge(Utils.make_json_ld_header()) + end + end + + def collection(collection, iri, page, _total \\ nil) do + offset = (page - 1) * 10 + items = Enum.slice(collection, offset, 10) + items = Enum.map(items, fn user -> user.ap_id end) + total = _total || length(collection) + + map = %{ + "id" => "#{iri}?page=#{page}", + "type" => "OrderedCollectionPage", + "partOf" => iri, + "totalItems" => length(collection), + "orderedItems" => items + } + + if offset < length(collection) do + Map.put(map, "next", "#{iri}?page=#{page + 1}") + end end end |