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