8c51b8d6330e0fd5ea678bf5bbba4041096c0656
[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 answer(user, object, name) do
187 {:ok,
188 %{
189 "type" => "Answer",
190 "actor" => user.ap_id,
191 "attributedTo" => user.ap_id,
192 "cc" => [object.data["actor"]],
193 "to" => [],
194 "name" => name,
195 "inReplyTo" => object.data["id"],
196 "context" => object.data["context"],
197 "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
198 "id" => Utils.generate_object_id()
199 }, []}
200 end
201
202 @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
203 def tombstone(actor, id) do
204 {:ok,
205 %{
206 "id" => id,
207 "actor" => actor,
208 "type" => "Tombstone"
209 }, []}
210 end
211
212 @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
213 def like(actor, object) do
214 with {:ok, data, meta} <- object_action(actor, object) do
215 data =
216 data
217 |> Map.put("type", "Like")
218
219 {:ok, data, meta}
220 end
221 end
222
223 # Retricted to user updates for now, always public
224 @spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
225 def update(actor, object) do
226 to = [Pleroma.Constants.as_public(), actor.follower_address]
227
228 {:ok,
229 %{
230 "id" => Utils.generate_activity_id(),
231 "type" => "Update",
232 "actor" => actor.ap_id,
233 "object" => object,
234 "to" => to
235 }, []}
236 end
237
238 @spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
239 def block(blocker, blocked) do
240 {:ok,
241 %{
242 "id" => Utils.generate_activity_id(),
243 "type" => "Block",
244 "actor" => blocker.ap_id,
245 "object" => blocked.ap_id,
246 "to" => [blocked.ap_id]
247 }, []}
248 end
249
250 @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
251 def announce(actor, object, options \\ []) do
252 public? = Keyword.get(options, :public, false)
253
254 to =
255 cond do
256 actor.ap_id == Relay.ap_id() ->
257 [actor.follower_address]
258
259 public? and Visibility.is_local_public?(object) ->
260 [actor.follower_address, object.data["actor"], Utils.as_local_public()]
261
262 public? ->
263 [actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
264
265 true ->
266 [actor.follower_address, object.data["actor"]]
267 end
268
269 {:ok,
270 %{
271 "id" => Utils.generate_activity_id(),
272 "actor" => actor.ap_id,
273 "object" => object.data["id"],
274 "to" => to,
275 "context" => object.data["context"],
276 "type" => "Announce",
277 "published" => Utils.make_date()
278 }, []}
279 end
280
281 @spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
282 defp object_action(actor, object) do
283 object_actor = User.get_cached_by_ap_id(object.data["actor"])
284
285 # Address the actor of the object, and our actor's follower collection if the post is public.
286 to =
287 if Visibility.is_public?(object) do
288 [actor.follower_address, object.data["actor"]]
289 else
290 [object.data["actor"]]
291 end
292
293 # CC everyone who's been addressed in the object, except ourself and the object actor's
294 # follower collection
295 cc =
296 (object.data["to"] ++ (object.data["cc"] || []))
297 |> List.delete(actor.ap_id)
298 |> List.delete(object_actor.follower_address)
299
300 {:ok,
301 %{
302 "id" => Utils.generate_activity_id(),
303 "actor" => actor.ap_id,
304 "object" => object.data["id"],
305 "to" => to,
306 "cc" => cc,
307 "context" => object.data["context"]
308 }, []}
309 end
310
311 @spec pin(User.t(), Object.t()) :: {:ok, map(), keyword()}
312 def pin(%User{} = user, object) do
313 {:ok,
314 %{
315 "id" => Utils.generate_activity_id(),
316 "target" => pinned_url(user.nickname),
317 "object" => object.data["id"],
318 "actor" => user.ap_id,
319 "type" => "Add",
320 "to" => [Pleroma.Constants.as_public()],
321 "cc" => [user.follower_address]
322 }, []}
323 end
324
325 @spec unpin(User.t(), Object.t()) :: {:ok, map, keyword()}
326 def unpin(%User{} = user, object) do
327 {:ok,
328 %{
329 "id" => Utils.generate_activity_id(),
330 "target" => pinned_url(user.nickname),
331 "object" => object.data["id"],
332 "actor" => user.ap_id,
333 "type" => "Remove",
334 "to" => [Pleroma.Constants.as_public()],
335 "cc" => [user.follower_address]
336 }, []}
337 end
338
339 defp pinned_url(nickname) when is_binary(nickname) do
340 Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname)
341 end
342 end