First
[anni] / lib / pleroma / web / activity_pub / mrf / steal_emoji_policy.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
6   require Logger
7
8   alias Pleroma.Config
9
10   @moduledoc "Detect new emojis by their shortcode and steals them"
11   @behaviour Pleroma.Web.ActivityPub.MRF.Policy
12
13   defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])
14
15   defp shortcode_matches?(shortcode, pattern) when is_binary(pattern) do
16     shortcode == pattern
17   end
18
19   defp shortcode_matches?(shortcode, pattern) do
20     String.match?(shortcode, pattern)
21   end
22
23   defp steal_emoji({shortcode, url}, emoji_dir_path) do
24     url = Pleroma.Web.MediaProxy.url(url)
25
26     with {:ok, %{status: status} = response} when status in 200..299 <- Pleroma.HTTP.get(url) do
27       size_limit = Config.get([:mrf_steal_emoji, :size_limit], 50_000)
28
29       if byte_size(response.body) <= size_limit do
30         extension =
31           url
32           |> URI.parse()
33           |> Map.get(:path)
34           |> Path.basename()
35           |> Path.extname()
36
37         file_path = Path.join(emoji_dir_path, shortcode <> (extension || ".png"))
38
39         case File.write(file_path, response.body) do
40           :ok ->
41             shortcode
42
43           e ->
44             Logger.warn("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}")
45             nil
46         end
47       else
48         Logger.debug(
49           "MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{size_limit} B)"
50         )
51
52         nil
53       end
54     else
55       e ->
56         Logger.warn("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}")
57         nil
58     end
59   end
60
61   @impl true
62   def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = message) do
63     host = URI.parse(actor).host
64
65     if host != Pleroma.Web.Endpoint.host() and accept_host?(host) do
66       installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
67
68       emoji_dir_path =
69         Config.get(
70           [:mrf_steal_emoji, :path],
71           Path.join(Config.get([:instance, :static_dir]), "emoji/stolen")
72         )
73
74       File.mkdir_p(emoji_dir_path)
75
76       new_emojis =
77         foreign_emojis
78         |> Enum.reject(fn {shortcode, _url} -> shortcode in installed_emoji end)
79         |> Enum.filter(fn {shortcode, _url} ->
80           reject_emoji? =
81             [:mrf_steal_emoji, :rejected_shortcodes]
82             |> Config.get([])
83             |> Enum.find(false, fn pattern -> shortcode_matches?(shortcode, pattern) end)
84
85           !reject_emoji?
86         end)
87         |> Enum.map(&steal_emoji(&1, emoji_dir_path))
88         |> Enum.filter(& &1)
89
90       if !Enum.empty?(new_emojis) do
91         Logger.info("Stole new emojis: #{inspect(new_emojis)}")
92         Pleroma.Emoji.reload()
93       end
94     end
95
96     {:ok, message}
97   end
98
99   def filter(message), do: {:ok, message}
100
101   @impl true
102   @spec config_description :: %{
103           children: [
104             %{
105               description: <<_::272, _::_*256>>,
106               key: :hosts | :rejected_shortcodes | :size_limit,
107               suggestions: [any(), ...],
108               type: {:list, :string} | {:list, :string} | :integer
109             },
110             ...
111           ],
112           description: <<_::448>>,
113           key: :mrf_steal_emoji,
114           label: <<_::80>>,
115           related_policy: <<_::352>>
116         }
117   def config_description do
118     %{
119       key: :mrf_steal_emoji,
120       related_policy: "Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy",
121       label: "MRF Emojis",
122       description: "Steals emojis from selected instances when it sees them.",
123       children: [
124         %{
125           key: :hosts,
126           type: {:list, :string},
127           description: "List of hosts to steal emojis from",
128           suggestions: [""]
129         },
130         %{
131           key: :rejected_shortcodes,
132           type: {:list, :string},
133           description: """
134             A list of patterns or matches to reject shortcodes with.
135
136             Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
137           """,
138           suggestions: ["foo", ~r/foo/]
139         },
140         %{
141           key: :size_limit,
142           type: :integer,
143           description: "File size limit (in bytes), checked before an emoji is saved to the disk",
144           suggestions: ["100000"]
145         }
146       ]
147     }
148   end
149
150   @impl true
151   def describe do
152     {:ok, %{}}
153   end
154 end