summaryrefslogtreecommitdiff
path: root/lib/pleroma/web/activity_pub
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma/web/activity_pub')
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex296
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex67
-rw-r--r--lib/pleroma/web/activity_pub/mrf/drop_policy.ex8
-rw-r--r--lib/pleroma/web/activity_pub/mrf/noop_policy.ex5
-rw-r--r--lib/pleroma/web/activity_pub/mrf/simple_policy.ex84
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex298
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex139
-rw-r--r--lib/pleroma/web/activity_pub/views/user_view.ex143
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