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