diff --git a/lib/pleroma/web/activity_pub/mrf/block_notification.ex b/lib/pleroma/web/activity_pub/mrf/block_notification.ex new file mode 100644 index 000000000..b3350a338 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/block_notification.ex @@ -0,0 +1,69 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.BlockNotification do + @moduledoc "Notify local users upon remote block." + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + defp is_block_or_unblock(%{"type" => "Block", "object" => object}), + do: {true, "blocked", object} + + defp is_block_or_unblock(%{ + "type" => "Undo", + "object" => %{"type" => "Block", "object" => object} + }), + do: {true, "unblocked", object} + + defp is_block_or_unblock(_), do: {false, nil, nil} + + defp is_remote_or_displaying_local?(%User{local: false}), do: true + + defp is_remote_or_displaying_local?(_), do: true + + @impl true + def filter(message) do + + with {true, action, object} <- is_block_or_unblock(message), + %User{} = actor <- User.get_cached_by_ap_id(message["actor"]), + %User{} = recipient <- User.get_cached_by_ap_id(object), + true <- recipient.local, + true <- is_remote_or_displaying_local?(actor), + false <- User.blocks_user?(recipient, actor) do + + # Create /opt/pleroma/logs/ with write perms for user pleroma + # Make a cron job to delete the log file every hour or whatever + # Not my problem + log_file = "/opt/pleroma/logs/blocks.log" + bot_user = "cockblock" + + log_contents = if File.exists?(log_file) do + File.read!(log_file) + else + "" + end + + logged_blocks = String.split(log_contents, "\n") + + actor_name = (fn actor_uri -> Path.basename(actor_uri.path) <> "@" <> actor_uri.authority end).(URI.parse(message["actor"])) + log_entry = actor_name <> ":" <> action + + unless Enum.member?(logged_blocks, log_entry) do + File.write!(log_file, log_entry <> "\n", [:append]) + _reply = + CommonAPI.post(User.get_by_nickname(bot_user), %{ + status: "@" <> recipient.nickname <> " you have been " <> action <> " by @" <> actor_name <> " (" <> actor_name <> ")", + visibility: "unlisted" + }) + end + end + + {:ok, message} + end + + @impl true + def describe, do: {:ok, %{}} +end diff --git a/lib/pleroma/web/activity_pub/mrf/change_react_to_like.ex b/lib/pleroma/web/activity_pub/mrf/change_react_to_like.ex new file mode 100644 index 000000000..0275db93d --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/change_react_to_like.ex @@ -0,0 +1,47 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ChangeReactstoLikes do + require Logger + + @moduledoc "Changes specified EmojiReacts into a Like" + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + + defp is_remote(host) do + my_host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) + my_host != host + end + + @impl true + @spec filter(any) :: {:ok, any} + def filter(%{"type" => "EmojiReact"} = object) do + actor = object["actor"] + host = URI.parse(actor).host + + if is_remote(host) do + react = object["content"] + + # TODO: make this pull from config + if react in ["👍", "👎", "❤️", "😆", "😮", "😢", "😩", "😭", "🔥", "⭐"] do + Logger.info("MRF.ChangeReactstoLikes: Changing #{inspect(react)} to a Like") + + object = + object + |> Map.put("type", "Like") + + {:ok, object} + else + {:ok, object} + end + else + {:ok, object} + end + end + + @impl true + def filter(object), do: {:ok, object} + + @impl true + def describe, do: {:ok, %{}} +end diff --git a/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex b/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex new file mode 100644 index 000000000..f28fabc5d --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex @@ -0,0 +1,148 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do + require Pleroma.Constants + + alias Pleroma.Formatter + alias Pleroma.Object + alias Pleroma.User + + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + + defp do_extract({:a, attrs, _}, acc) do + if Enum.find(attrs, fn {name, value} -> + name == "class" && value in ["mention", "u-url mention", "mention u-url"] + end) do + href = Enum.find(attrs, fn {name, _} -> name == "href" end) |> elem(1) + acc ++ [href] + else + acc + end + end + + defp do_extract({_, _, children}, acc) do + do_extract(children, acc) + end + + defp do_extract(nodes, acc) when is_list(nodes) do + Enum.reduce(nodes, acc, fn node, acc -> do_extract(node, acc) end) + end + + defp do_extract(_, acc), do: acc + + defp extract_mention_uris_from_content(content) do + {:ok, tree} = :fast_html.decode(content, format: [:html_atoms]) + do_extract(tree, []) + end + + defp get_replied_to_user(%{"inReplyTo" => in_reply_to}) do + case Object.normalize(in_reply_to, fetch: false) do + %Object{data: %{"actor" => actor}} -> User.get_cached_by_ap_id(actor) + _ -> nil + end + end + + defp get_replied_to_user(_object), do: nil + + # Ensure the replied-to user is sorted to the left + defp sort_replied_user([%User{id: user_id} | _] = users, %User{id: user_id}), do: users + + defp sort_replied_user(users, %User{id: user_id} = user) do + if Enum.find(users, fn u -> u.id == user_id end) do + users = Enum.reject(users, fn u -> u.id == user_id end) + [user | users] + else + users + end + end + + defp sort_replied_user(users, _), do: users + + # Drop constants and the actor's own AP ID + defp clean_recipients(recipients, object) do + Enum.reject(recipients, fn ap_id -> + ap_id in [ + object["object"]["actor"], + Pleroma.Constants.as_public(), + Pleroma.Web.ActivityPub.Utils.as_local_public() + ] + end) + end + + defp is_remote(host) do + my_host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) + my_host != host + end + + @impl true + def filter( + %{ + "type" => "Create", + "object" => %{"type" => "Note", "to" => to, "inReplyTo" => in_reply_to} + } = object + ) + when is_list(to) and is_binary(in_reply_to) do + actor = object["object"]["actor"] + host = URI.parse(actor).host + + if is_remote(host) do + # image-only posts from pleroma apparently reach this MRF without the content field + content = object["object"]["content"] || "" + + # Get the replied-to user for sorting + replied_to_user = get_replied_to_user(object["object"]) + + mention_users = + to + |> clean_recipients(object) + |> Enum.map(&User.get_cached_by_ap_id/1) + |> Enum.reject(&is_nil/1) + |> sort_replied_user(replied_to_user) + + explicitly_mentioned_uris = extract_mention_uris_from_content(content) + + if Enum.empty?(explicitly_mentioned_uris) do + added_mentions = + Enum.reduce(mention_users, "", fn %User{ap_id: uri} = user, acc -> + unless uri in explicitly_mentioned_uris do + acc <> Formatter.mention_from_user(user, %{mentions_format: :compact}) <> " " + else + acc + end + end) + + recipients_inline = + if added_mentions != "", + do: "#{added_mentions}", + else: "" + + content = + cond do + # For Markdown posts, insert the mentions inside the first

tag + recipients_inline != "" && String.starts_with?(content, "

") -> + "

" <> recipients_inline <> String.trim_leading(content, "

") + + recipients_inline != "" -> + recipients_inline <> content + + true -> + content + end + + {:ok, put_in(object["object"]["content"], content)} + else + {:ok, object} + end + else + {:ok, object} + end + end + + @impl true + def filter(object), do: {:ok, object} + + @impl true + def describe, do: {:ok, %{}} +end diff --git a/lib/pleroma/web/activity_pub/mrf/no_incoming_deletes.ex b/lib/pleroma/web/activity_pub/mrf/no_incoming_deletes.ex new file mode 100644 index 000000000..ecf1976f6 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/no_incoming_deletes.ex @@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.NoIncomingDeletes do + @moduledoc "Reject remote deletes." + + require Logger + + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + @impl true + def filter(%{"type" => "Delete", "actor" => actor} = object) do + actor_info = URI.parse(actor) + instance_domain = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) + if (actor_info.host == instance_domain) do + Logger.warn("DELETE from this instance, not rejecting: #{inspect(object)}") + {:ok, object} + else + Logger.warn("DELETE rejected: #{inspect(object)}") + {:reject, object} + end + end + + @impl true + def filter(object), do: {:ok, object} + + @impl true + def describe, do: {:ok, %{}} +end