summaryrefslogtreecommitdiff
path: root/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex
blob: 255910b2f9abc372f4c1efc94325e260cecf1cc7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# 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

  @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
    # 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)

    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: "<span class=\"recipients-inline\">#{added_mentions}</span>",
        else: ""

    content =
      cond do
        # For Markdown posts, insert the mentions inside the first <p> tag
        recipients_inline != "" && String.starts_with?(content, "<p>") ->
          "<p>" <> recipients_inline <> String.trim_leading(content, "<p>")

        recipients_inline != "" ->
          recipients_inline <> content

        true ->
          content
      end

    {:ok, put_in(object["object"]["content"], content)}
  end

  @impl true
  def filter(object), do: {:ok, object}

  @impl true
  def describe, do: {:ok, %{}}
end