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