add outbound reacts
[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 @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
59 def emoji_react(actor, object, emoji) do
60 with {:ok, data, meta} <- object_action(actor, object) do
61 data = if Emoji.is_unicode_emoji?(emoji) do
62 data
63 |> Map.put("content", emoji)
64 |> Map.put("type", "EmojiReact")
65 else
66 emojo = Emoji.get(emoji)
67 path = emojo |> Map.get(:file)
68 url = "#{Endpoint.url()}#{path}"
69 data
70 |> Map.put("content", emoji)
71 |> Map.put("type", "EmojiReact")
72 |> Map.put("tag", [
73 %{}
74 |> Map.put("id", url)
75 |> Map.put("type", "Emoji")
76 |> Map.put("name", emojo.code)
77 |> Map.put("icon",
78 %{}
79 |> Map.put("type", "Image")
80 |> Map.put("url", url)
81 )
82 ])
83 end
84
85 {:ok, data, meta}
86 end
87 end
88
89 @spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()}
90 def undo(actor, object) do
91 {:ok,
92 %{
93 "id" => Utils.generate_activity_id(),
94 "actor" => actor.ap_id,
95 "type" => "Undo",
96 "object" => object.data["id"],
97 "to" => object.data["to"] || [],
98 "cc" => object.data["cc"] || []
99 }, []}
100 end
101
102 @spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
103 def delete(actor, object_id) do
104 object = Object.normalize(object_id, fetch: false)
105
106 user = !object && User.get_cached_by_ap_id(object_id)
107
108 to =
109 case {object, user} do
110 {%Object{}, _} ->
111 # We are deleting an object, address everyone who was originally mentioned
112 (object.data["to"] || []) ++ (object.data["cc"] || [])
113
114 {_, %User{follower_address: follower_address}} ->
115 # We are deleting a user, address the followers of that user
116 [follower_address]
117 end
118
119 {:ok,
120 %{
121 "id" => Utils.generate_activity_id(),
122 "actor" => actor.ap_id,
123 "object" => object_id,
124 "to" => to,
125 "type" => "Delete"
126 }, []}
127 end
128
129 def create(actor, object, recipients) do
130 context =
131 if is_map(object) do
132 object["context"]
133 else
134 nil
135 end
136
137 {:ok,
138 %{
139 "id" => Utils.generate_activity_id(),
140 "actor" => actor.ap_id,
141 "to" => recipients,
142 "object" => object,
143 "type" => "Create",
144 "published" => DateTime.utc_now() |> DateTime.to_iso8601()
145 }
146 |> Pleroma.Maps.put_if_present("context", context), []}
147 end
148
149 @spec note(ActivityDraft.t()) :: {:ok, map(), keyword()}
150 def note(%ActivityDraft{} = draft) do
151 data =
152 %{
153 "type" => "Note",
154 "to" => draft.to,
155 "cc" => draft.cc,
156 "content" => draft.content_html,
157 "summary" => draft.summary,
158 "sensitive" => draft.sensitive,
159 "context" => draft.context,
160 "attachment" => draft.attachments,
161 "actor" => draft.user.ap_id,
162 "tag" => Keyword.values(draft.tags) |> Enum.uniq()
163 }
164 |> add_in_reply_to(draft.in_reply_to)
165 |> Map.merge(draft.extra)
166
167 {:ok, data, []}
168 end
169
170 defp add_in_reply_to(object, nil), do: object
171
172 defp add_in_reply_to(object, in_reply_to) do
173 with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to, fetch: false) do
174 Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
175 else
176 _ -> object
177 end
178 end
179
180 def chat_message(actor, recipient, content, opts \\ []) do
181 basic = %{
182 "id" => Utils.generate_object_id(),
183 "actor" => actor.ap_id,
184 "type" => "ChatMessage",
185 "to" => [recipient],
186 "content" => content,
187 "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
188 "emoji" => Emoji.Formatter.get_emoji_map(content)
189 }
190
191 case opts[:attachment] do
192 %Object{data: attachment_data} ->
193 {
194 :ok,
195 Map.put(basic, "attachment", attachment_data),
196 []
197 }
198
199 _ ->
200 {:ok, basic, []}
201 end
202 end
203
204 def answer(user, object, name) do
205 {:ok,
206 %{
207 "type" => "Answer",
208 "actor" => user.ap_id,
209 "attributedTo" => user.ap_id,
210 "cc" => [object.data["actor"]],
211 "to" => [],
212 "name" => name,
213 "inReplyTo" => object.data["id"],
214 "context" => object.data["context"],
215 "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
216 "id" => Utils.generate_object_id()
217 }, []}
218 end
219
220 @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
221 def tombstone(actor, id) do
222 {:ok,
223 %{
224 "id" => id,
225 "actor" => actor,
226 "type" => "Tombstone"
227 }, []}
228 end
229
230 @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
231 def like(actor, object) do
232 with {:ok, data, meta} <- object_action(actor, object) do
233 data =
234 data
235 |> Map.put("type", "Like")
236
237 {:ok, data, meta}
238 end
239 end
240
241 # Retricted to user updates for now, always public
242 @spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
243 def update(actor, object) do
244 to = [Pleroma.Constants.as_public(), actor.follower_address]
245
246 {:ok,
247 %{
248 "id" => Utils.generate_activity_id(),
249 "type" => "Update",
250 "actor" => actor.ap_id,
251 "object" => object,
252 "to" => to
253 }, []}
254 end
255
256 @spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
257 def block(blocker, blocked) do
258 {:ok,
259 %{
260 "id" => Utils.generate_activity_id(),
261 "type" => "Block",
262 "actor" => blocker.ap_id,
263 "object" => blocked.ap_id,
264 "to" => [blocked.ap_id]
265 }, []}
266 end
267
268 @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
269 def announce(actor, object, options \\ []) do
270 public? = Keyword.get(options, :public, false)
271
272 to =
273 cond do
274 actor.ap_id == Relay.ap_id() ->
275 [actor.follower_address]
276
277 public? and Visibility.is_local_public?(object) ->
278 [actor.follower_address, object.data["actor"], Utils.as_local_public()]
279
280 public? ->
281 [actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
282
283 true ->
284 [actor.follower_address, object.data["actor"]]
285 end
286
287 {:ok,
288 %{
289 "id" => Utils.generate_activity_id(),
290 "actor" => actor.ap_id,
291 "object" => object.data["id"],
292 "to" => to,
293 "context" => object.data["context"],
294 "type" => "Announce",
295 "published" => Utils.make_date()
296 }, []}
297 end
298
299 @spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
300 defp object_action(actor, object) do
301 object_actor = User.get_cached_by_ap_id(object.data["actor"])
302
303 # Address the actor of the object, and our actor's follower collection if the post is public.
304 to =
305 if Visibility.is_public?(object) do
306 [actor.follower_address, object.data["actor"]]
307 else
308 [object.data["actor"]]
309 end
310
311 # CC everyone who's been addressed in the object, except ourself and the object actor's
312 # follower collection
313 cc =
314 (object.data["to"] ++ (object.data["cc"] || []))
315 |> List.delete(actor.ap_id)
316 |> List.delete(object_actor.follower_address)
317
318 {:ok,
319 %{
320 "id" => Utils.generate_activity_id(),
321 "actor" => actor.ap_id,
322 "object" => object.data["id"],
323 "to" => to,
324 "cc" => cc,
325 "context" => object.data["context"]
326 }, []}
327 end
328
329 @spec pin(User.t(), Object.t()) :: {:ok, map(), keyword()}
330 def pin(%User{} = user, object) do
331 {:ok,
332 %{
333 "id" => Utils.generate_activity_id(),
334 "target" => pinned_url(user.nickname),
335 "object" => object.data["id"],
336 "actor" => user.ap_id,
337 "type" => "Add",
338 "to" => [Pleroma.Constants.as_public()],
339 "cc" => [user.follower_address]
340 }, []}
341 end
342
343 @spec unpin(User.t(), Object.t()) :: {:ok, map, keyword()}
344 def unpin(%User{} = user, object) do
345 {:ok,
346 %{
347 "id" => Utils.generate_activity_id(),
348 "target" => pinned_url(user.nickname),
349 "object" => object.data["id"],
350 "actor" => user.ap_id,
351 "type" => "Remove",
352 "to" => [Pleroma.Constants.as_public()],
353 "cc" => [user.follower_address]
354 }, []}
355 end
356
357 defp pinned_url(nickname) when is_binary(nickname) do
358 Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname)
359 end
360 end