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