Allow reacting with remote emoji when they exist on the post (#200)
[akkoma] / lib / pleroma / web / activity_pub / builder.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.Builder do
6 @moduledoc """
7 This module builds the objects. Meant to be used for creating local objects.
8
9 This module encodes our addressing policies and general shape of our objects.
10 """
11
12 alias Pleroma.Emoji
13 alias Pleroma.Object
14 alias Pleroma.User
15 alias Pleroma.Web.ActivityPub.Relay
16 alias Pleroma.Web.ActivityPub.Utils
17 alias Pleroma.Web.ActivityPub.Visibility
18 alias Pleroma.Web.CommonAPI.ActivityDraft
19 alias Pleroma.Web.Endpoint
20
21 require Pleroma.Constants
22
23 def accept_or_reject(actor, activity, type) do
24 data = %{
25 "id" => Utils.generate_activity_id(),
26 "actor" => actor.ap_id,
27 "type" => type,
28 "object" => activity.data["id"],
29 "to" => [activity.actor]
30 }
31
32 {:ok, data, []}
33 end
34
35 @spec reject(User.t(), Activity.t()) :: {:ok, map(), keyword()}
36 def reject(actor, rejected_activity) do
37 accept_or_reject(actor, rejected_activity, "Reject")
38 end
39
40 @spec accept(User.t(), Activity.t()) :: {:ok, map(), keyword()}
41 def accept(actor, accepted_activity) do
42 accept_or_reject(actor, accepted_activity, "Accept")
43 end
44
45 @spec follow(User.t(), User.t()) :: {:ok, map(), keyword()}
46 def follow(follower, followed) do
47 data = %{
48 "id" => Utils.generate_activity_id(),
49 "actor" => follower.ap_id,
50 "type" => "Follow",
51 "object" => followed.ap_id,
52 "to" => [followed.ap_id]
53 }
54
55 {:ok, data, []}
56 end
57
58 defp unicode_emoji_react(_object, data, emoji) do
59 data
60 |> Map.put("content", emoji)
61 |> Map.put("type", "EmojiReact")
62 end
63
64 defp add_emoji_content(data, emoji, url) do
65 data
66 |> Map.put("content", Emoji.maybe_quote(emoji))
67 |> Map.put("type", "EmojiReact")
68 |> Map.put("tag", [
69 %{}
70 |> Map.put("id", url)
71 |> Map.put("type", "Emoji")
72 |> Map.put("name", Emoji.maybe_quote(emoji))
73 |> Map.put(
74 "icon",
75 %{}
76 |> Map.put("type", "Image")
77 |> Map.put("url", url)
78 )
79 ])
80 end
81
82 defp remote_custom_emoji_react(
83 %{data: %{"reactions" => existing_reactions}} = object,
84 data,
85 emoji
86 ) do
87 [emoji_code, instance] = String.split(Emoji.stripped_name(emoji), "@")
88
89 matching_reaction =
90 Enum.find(
91 existing_reactions,
92 fn [name, _, url] ->
93 url = URI.parse(url)
94 url.host == instance && name == emoji_code
95 end
96 )
97
98 if matching_reaction do
99 [name, _, url] = matching_reaction
100 add_emoji_content(data, name, url)
101 else
102 {:error, "Could not react"}
103 end
104 end
105
106 defp remote_custom_emoji_react(_object, data, emoji) do
107 {:error, "Could not react"}
108 end
109
110 defp local_custom_emoji_react(data, emoji) do
111 with %{} = emojo <- Emoji.get(emoji) do
112 path = emojo |> Map.get(:file)
113 url = "#{Endpoint.url()}#{path}"
114 add_emoji_content(data, emojo.code, url)
115 else
116 _ -> {:error, "Emoji does not exist"}
117 end
118 end
119
120 defp custom_emoji_react(object, data, emoji) do
121 if String.contains?(emoji, "@") do
122 remote_custom_emoji_react(object, data, emoji)
123 else
124 local_custom_emoji_react(data, emoji)
125 end
126 end
127
128 @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
129 def emoji_react(actor, object, emoji) do
130 with {:ok, data, meta} <- object_action(actor, object) do
131 data =
132 if Emoji.is_unicode_emoji?(emoji) do
133 unicode_emoji_react(object, data, emoji)
134 else
135 custom_emoji_react(object, data, emoji)
136 end
137
138 {:ok, data, meta}
139 end
140 end
141
142 @spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()}
143 def undo(actor, object) do
144 {:ok,
145 %{
146 "id" => Utils.generate_activity_id(),
147 "actor" => actor.ap_id,
148 "type" => "Undo",
149 "object" => object.data["id"],
150 "to" => object.data["to"] || [],
151 "cc" => object.data["cc"] || []
152 }, []}
153 end
154
155 @spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
156 def delete(actor, object_id) do
157 object = Object.normalize(object_id, fetch: false)
158
159 user = !object && User.get_cached_by_ap_id(object_id)
160
161 to =
162 case {object, user} do
163 {%Object{}, _} ->
164 # We are deleting an object, address everyone who was originally mentioned
165 (object.data["to"] || []) ++ (object.data["cc"] || [])
166
167 {_, %User{follower_address: follower_address}} ->
168 # We are deleting a user, address the followers of that user
169 [follower_address]
170 end
171
172 {:ok,
173 %{
174 "id" => Utils.generate_activity_id(),
175 "actor" => actor.ap_id,
176 "object" => object_id,
177 "to" => to,
178 "type" => "Delete"
179 }, []}
180 end
181
182 def create(actor, object, recipients) do
183 context =
184 if is_map(object) do
185 object["context"]
186 else
187 nil
188 end
189
190 {:ok,
191 %{
192 "id" => Utils.generate_activity_id(),
193 "actor" => actor.ap_id,
194 "to" => recipients,
195 "object" => object,
196 "type" => "Create",
197 "published" => DateTime.utc_now() |> DateTime.to_iso8601()
198 }
199 |> Pleroma.Maps.put_if_present("context", context), []}
200 end
201
202 @spec note(ActivityDraft.t()) :: {:ok, map(), keyword()}
203 def note(%ActivityDraft{} = draft) do
204 data =
205 %{
206 "type" => "Note",
207 "to" => draft.to,
208 "cc" => draft.cc,
209 "content" => draft.content_html,
210 "summary" => draft.summary,
211 "sensitive" => draft.sensitive,
212 "context" => draft.context,
213 "attachment" => draft.attachments,
214 "actor" => draft.user.ap_id,
215 "tag" => Keyword.values(draft.tags) |> Enum.uniq()
216 }
217 |> add_in_reply_to(draft.in_reply_to)
218 |> add_quote(draft.quote)
219 |> Map.merge(draft.extra)
220
221 {:ok, data, []}
222 end
223
224 defp add_in_reply_to(object, nil), do: object
225
226 defp add_in_reply_to(object, in_reply_to) do
227 with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to, fetch: false) do
228 Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
229 else
230 _ -> object
231 end
232 end
233
234 defp add_quote(object, nil), do: object
235
236 defp add_quote(object, quote) do
237 with %Object{} = quote_object <- Object.normalize(quote, fetch: false) do
238 Map.put(object, "quoteUri", quote_object.data["id"])
239 else
240 _ -> object
241 end
242 end
243
244 def answer(user, object, name) do
245 {:ok,
246 %{
247 "type" => "Answer",
248 "actor" => user.ap_id,
249 "attributedTo" => user.ap_id,
250 "cc" => [object.data["actor"]],
251 "to" => [],
252 "name" => name,
253 "inReplyTo" => object.data["id"],
254 "context" => object.data["context"],
255 "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
256 "id" => Utils.generate_object_id()
257 }, []}
258 end
259
260 @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
261 def tombstone(actor, id) do
262 {:ok,
263 %{
264 "id" => id,
265 "actor" => actor,
266 "type" => "Tombstone"
267 }, []}
268 end
269
270 @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
271 def like(actor, object) do
272 with {:ok, data, meta} <- object_action(actor, object) do
273 data =
274 data
275 |> Map.put("type", "Like")
276
277 {:ok, data, meta}
278 end
279 end
280
281 # Retricted to user updates for now, always public
282 @spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
283 def update(actor, object) do
284 to = [Pleroma.Constants.as_public(), actor.follower_address]
285
286 {:ok,
287 %{
288 "id" => Utils.generate_activity_id(),
289 "type" => "Update",
290 "actor" => actor.ap_id,
291 "object" => object,
292 "to" => to
293 }, []}
294 end
295
296 @spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
297 def block(blocker, blocked) do
298 {:ok,
299 %{
300 "id" => Utils.generate_activity_id(),
301 "type" => "Block",
302 "actor" => blocker.ap_id,
303 "object" => blocked.ap_id,
304 "to" => [blocked.ap_id]
305 }, []}
306 end
307
308 @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
309 def announce(actor, object, options \\ []) do
310 public? = Keyword.get(options, :public, false)
311
312 to =
313 cond do
314 actor.ap_id == Relay.ap_id() ->
315 [actor.follower_address]
316
317 public? and Visibility.is_local_public?(object) ->
318 [actor.follower_address, object.data["actor"], Utils.as_local_public()]
319
320 public? ->
321 [actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
322
323 true ->
324 [actor.follower_address, object.data["actor"]]
325 end
326
327 {:ok,
328 %{
329 "id" => Utils.generate_activity_id(),
330 "actor" => actor.ap_id,
331 "object" => object.data["id"],
332 "to" => to,
333 "context" => object.data["context"],
334 "type" => "Announce",
335 "published" => Utils.make_date()
336 }, []}
337 end
338
339 @spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
340 defp object_action(actor, object) do
341 object_actor = User.get_cached_by_ap_id(object.data["actor"])
342
343 # Address the actor of the object, and our actor's follower collection if the post is public.
344 to =
345 if Visibility.is_public?(object) do
346 [actor.follower_address, object.data["actor"]]
347 else
348 [object.data["actor"]]
349 end
350
351 # CC everyone who's been addressed in the object, except ourself and the object actor's
352 # follower collection
353 cc =
354 (object.data["to"] ++ (object.data["cc"] || []))
355 |> List.delete(actor.ap_id)
356 |> List.delete(object_actor.follower_address)
357
358 {:ok,
359 %{
360 "id" => Utils.generate_activity_id(),
361 "actor" => actor.ap_id,
362 "object" => object.data["id"],
363 "to" => to,
364 "cc" => cc,
365 "context" => object.data["context"]
366 }, []}
367 end
368
369 @spec pin(User.t(), Object.t()) :: {:ok, map(), keyword()}
370 def pin(%User{} = user, object) do
371 {:ok,
372 %{
373 "id" => Utils.generate_activity_id(),
374 "target" => pinned_url(user.nickname),
375 "object" => object.data["id"],
376 "actor" => user.ap_id,
377 "type" => "Add",
378 "to" => [Pleroma.Constants.as_public()],
379 "cc" => [user.follower_address]
380 }, []}
381 end
382
383 @spec unpin(User.t(), Object.t()) :: {:ok, map, keyword()}
384 def unpin(%User{} = user, object) do
385 {:ok,
386 %{
387 "id" => Utils.generate_activity_id(),
388 "target" => pinned_url(user.nickname),
389 "object" => object.data["id"],
390 "actor" => user.ap_id,
391 "type" => "Remove",
392 "to" => [Pleroma.Constants.as_public()],
393 "cc" => [user.follower_address]
394 }, []}
395 end
396
397 defp pinned_url(nickname) when is_binary(nickname) do
398 Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname)
399 end
400 end