Deletion: Handle the case of pruned objects.
[akkoma] / lib / pleroma / web / common_api / common_api.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.CommonAPI do
6 alias Pleroma.Activity
7 alias Pleroma.ActivityExpiration
8 alias Pleroma.Conversation.Participation
9 alias Pleroma.FollowingRelationship
10 alias Pleroma.Notification
11 alias Pleroma.Object
12 alias Pleroma.ThreadMute
13 alias Pleroma.User
14 alias Pleroma.UserRelationship
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.Builder
17 alias Pleroma.Web.ActivityPub.Pipeline
18 alias Pleroma.Web.ActivityPub.Utils
19 alias Pleroma.Web.ActivityPub.Visibility
20
21 import Pleroma.Web.Gettext
22 import Pleroma.Web.CommonAPI.Utils
23
24 require Pleroma.Constants
25 require Logger
26
27 def unblock(blocker, blocked) do
28 with %Activity{} = block <- Utils.fetch_latest_block(blocker, blocked),
29 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
30 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
31 {:ok, unblock}
32 end
33 end
34
35 def follow(follower, followed) do
36 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
37
38 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
39 {:ok, activity} <- ActivityPub.follow(follower, followed),
40 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
41 {:ok, follower, followed, activity}
42 end
43 end
44
45 def unfollow(follower, unfollowed) do
46 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
47 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
48 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
49 {:ok, follower}
50 end
51 end
52
53 def accept_follow_request(follower, followed) do
54 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
55 {:ok, follower} <- User.follow(follower, followed),
56 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
57 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
58 {:ok, _activity} <-
59 ActivityPub.accept(%{
60 to: [follower.ap_id],
61 actor: followed,
62 object: follow_activity.data["id"],
63 type: "Accept"
64 }) do
65 {:ok, follower}
66 end
67 end
68
69 def reject_follow_request(follower, followed) do
70 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
71 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
72 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
73 {:ok, _notifications} <- Notification.dismiss(follow_activity),
74 {:ok, _activity} <-
75 ActivityPub.reject(%{
76 to: [follower.ap_id],
77 actor: followed,
78 object: follow_activity.data["id"],
79 type: "Reject"
80 }) do
81 {:ok, follower}
82 end
83 end
84
85 def delete(activity_id, user) do
86 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
87 {:find_activity, Activity.get_by_id(activity_id)},
88 {_, %Object{} = object, _} <-
89 {:find_object, Object.normalize(activity, false), activity},
90 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
91 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
92 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
93 {:ok, delete}
94 else
95 {:find_activity, _} ->
96 {:error, :not_found}
97
98 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
99 # We have the create activity, but not the object, it was probably pruned.
100 # Insert a tombstone and try again
101 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
102 {:ok, _tombstone} <- Object.create(tombstone_data) do
103 delete(activity_id, user)
104 else
105 _ ->
106 Logger.error(
107 "Could not insert tombstone for missing object on deletion. Object is #{object}."
108 )
109
110 {:error, dgettext("errors", "Could not delete")}
111 end
112
113 _ ->
114 {:error, dgettext("errors", "Could not delete")}
115 end
116 end
117
118 def repeat(id, user, params \\ %{}) do
119 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
120 {:find_activity, Activity.get_by_id(id)},
121 object <- Object.normalize(activity),
122 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
123 public <- public_announce?(object, params) do
124 if announce_activity do
125 {:ok, announce_activity, object}
126 else
127 ActivityPub.announce(user, object, nil, true, public)
128 end
129 else
130 {:find_activity, _} -> {:error, :not_found}
131 _ -> {:error, dgettext("errors", "Could not repeat")}
132 end
133 end
134
135 def unrepeat(id, user) do
136 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
137 {:find_activity, Activity.get_by_id(id)},
138 %Object{} = note <- Object.normalize(activity, false),
139 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
140 {:ok, undo, _} <- Builder.undo(user, announce),
141 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
142 {:ok, activity}
143 else
144 {:find_activity, _} -> {:error, :not_found}
145 _ -> {:error, dgettext("errors", "Could not unrepeat")}
146 end
147 end
148
149 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
150 def favorite(%User{} = user, id) do
151 case favorite_helper(user, id) do
152 {:ok, _} = res ->
153 res
154
155 {:error, :not_found} = res ->
156 res
157
158 {:error, e} ->
159 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
160 {:error, dgettext("errors", "Could not favorite")}
161 end
162 end
163
164 def favorite_helper(user, id) do
165 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
166 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
167 {_, {:ok, %Activity{} = activity, _meta}} <-
168 {:common_pipeline,
169 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
170 {:ok, activity}
171 else
172 {:find_object, _} ->
173 {:error, :not_found}
174
175 {:common_pipeline,
176 {
177 :error,
178 {
179 :validate_object,
180 {
181 :error,
182 changeset
183 }
184 }
185 }} = e ->
186 if {:object, {"already liked by this actor", []}} in changeset.errors do
187 {:ok, :already_liked}
188 else
189 {:error, e}
190 end
191
192 e ->
193 {:error, e}
194 end
195 end
196
197 def unfavorite(id, user) do
198 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
199 {:find_activity, Activity.get_by_id(id)},
200 %Object{} = note <- Object.normalize(activity, false),
201 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
202 {:ok, undo, _} <- Builder.undo(user, like),
203 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
204 {:ok, activity}
205 else
206 {:find_activity, _} -> {:error, :not_found}
207 _ -> {:error, dgettext("errors", "Could not unfavorite")}
208 end
209 end
210
211 def react_with_emoji(id, user, emoji) do
212 with %Activity{} = activity <- Activity.get_by_id(id),
213 object <- Object.normalize(activity),
214 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
215 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
216 {:ok, activity}
217 else
218 _ ->
219 {:error, dgettext("errors", "Could not add reaction emoji")}
220 end
221 end
222
223 def unreact_with_emoji(id, user, emoji) do
224 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
225 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
226 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
227 {:ok, activity}
228 else
229 _ ->
230 {:error, dgettext("errors", "Could not remove reaction emoji")}
231 end
232 end
233
234 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
235 with :ok <- validate_not_author(object, user),
236 :ok <- validate_existing_votes(user, object),
237 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
238 answer_activities =
239 Enum.map(choices, fn index ->
240 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
241
242 {:ok, activity} =
243 ActivityPub.create(%{
244 to: answer_data["to"],
245 actor: user,
246 context: object.data["context"],
247 object: answer_data,
248 additional: %{"cc" => answer_data["cc"]}
249 })
250
251 activity
252 end)
253
254 object = Object.get_cached_by_ap_id(object.data["id"])
255 {:ok, answer_activities, object}
256 end
257 end
258
259 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
260 do: {:error, dgettext("errors", "Poll's author can't vote")}
261
262 defp validate_not_author(_, _), do: :ok
263
264 defp validate_existing_votes(%{ap_id: ap_id}, object) do
265 if Utils.get_existing_votes(ap_id, object) == [] do
266 :ok
267 else
268 {:error, dgettext("errors", "Already voted")}
269 end
270 end
271
272 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
273 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
274
275 defp normalize_and_validate_choices(choices, object) do
276 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
277 {options, max_count} = get_options_and_max_count(object)
278 count = Enum.count(options)
279
280 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
281 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
282 {:ok, options, choices}
283 else
284 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
285 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
286 end
287 end
288
289 def public_announce?(_, %{"visibility" => visibility})
290 when visibility in ~w{public unlisted private direct},
291 do: visibility in ~w(public unlisted)
292
293 def public_announce?(object, _) do
294 Visibility.is_public?(object)
295 end
296
297 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
298
299 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
300 when visibility in ~w{public unlisted private direct},
301 do: {visibility, get_replied_to_visibility(in_reply_to)}
302
303 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
304 visibility = {:list, String.to_integer(list_id)}
305 {visibility, get_replied_to_visibility(in_reply_to)}
306 end
307
308 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
309 visibility = get_replied_to_visibility(in_reply_to)
310 {visibility, visibility}
311 end
312
313 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
314
315 def get_replied_to_visibility(nil), do: nil
316
317 def get_replied_to_visibility(activity) do
318 with %Object{} = object <- Object.normalize(activity) do
319 Visibility.get_visibility(object)
320 end
321 end
322
323 def check_expiry_date({:ok, nil} = res), do: res
324
325 def check_expiry_date({:ok, in_seconds}) do
326 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
327
328 if ActivityExpiration.expires_late_enough?(expiry) do
329 {:ok, expiry}
330 else
331 {:error, "Expiry date is too soon"}
332 end
333 end
334
335 def check_expiry_date(expiry_str) do
336 Ecto.Type.cast(:integer, expiry_str)
337 |> check_expiry_date()
338 end
339
340 def listen(user, %{"title" => _} = data) do
341 with visibility <- data["visibility"] || "public",
342 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
343 listen_data <-
344 Map.take(data, ["album", "artist", "title", "length"])
345 |> Map.put("type", "Audio")
346 |> Map.put("to", to)
347 |> Map.put("cc", cc)
348 |> Map.put("actor", user.ap_id),
349 {:ok, activity} <-
350 ActivityPub.listen(%{
351 actor: user,
352 to: to,
353 object: listen_data,
354 context: Utils.generate_context_id(),
355 additional: %{"cc" => cc}
356 }) do
357 {:ok, activity}
358 end
359 end
360
361 def post(user, %{"status" => _} = data) do
362 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
363 draft.changes
364 |> ActivityPub.create(draft.preview?)
365 |> maybe_create_activity_expiration(draft.expires_at)
366 end
367 end
368
369 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
370 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
371 {:ok, activity}
372 end
373 end
374
375 defp maybe_create_activity_expiration(result, _), do: result
376
377 def pin(id, %{ap_id: user_ap_id} = user) do
378 with %Activity{
379 actor: ^user_ap_id,
380 data: %{"type" => "Create"},
381 object: %Object{data: %{"type" => object_type}}
382 } = activity <- Activity.get_by_id_with_object(id),
383 true <- object_type in ["Note", "Article", "Question"],
384 true <- Visibility.is_public?(activity),
385 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
386 {:ok, activity}
387 else
388 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
389 _ -> {:error, dgettext("errors", "Could not pin")}
390 end
391 end
392
393 def unpin(id, user) do
394 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
395 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
396 {:ok, activity}
397 else
398 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
399 _ -> {:error, dgettext("errors", "Could not unpin")}
400 end
401 end
402
403 def add_mute(user, activity) do
404 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
405 {:ok, activity}
406 else
407 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
408 end
409 end
410
411 def remove_mute(user, activity) do
412 ThreadMute.remove_mute(user.id, activity.data["context"])
413 {:ok, activity}
414 end
415
416 def thread_muted?(%{id: nil} = _user, _activity), do: false
417
418 def thread_muted?(user, activity) do
419 ThreadMute.exists?(user.id, activity.data["context"])
420 end
421
422 def report(user, data) do
423 with {:ok, account} <- get_reported_account(data.account_id),
424 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
425 {:ok, statuses} <- get_report_statuses(account, data) do
426 ActivityPub.flag(%{
427 context: Utils.generate_context_id(),
428 actor: user,
429 account: account,
430 statuses: statuses,
431 content: content_html,
432 forward: Map.get(data, :forward, false)
433 })
434 end
435 end
436
437 defp get_reported_account(account_id) do
438 case User.get_cached_by_id(account_id) do
439 %User{} = account -> {:ok, account}
440 _ -> {:error, dgettext("errors", "Account not found")}
441 end
442 end
443
444 def update_report_state(activity_ids, state) when is_list(activity_ids) do
445 case Utils.update_report_state(activity_ids, state) do
446 :ok -> {:ok, activity_ids}
447 _ -> {:error, dgettext("errors", "Could not update state")}
448 end
449 end
450
451 def update_report_state(activity_id, state) do
452 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
453 Utils.update_report_state(activity, state)
454 else
455 nil -> {:error, :not_found}
456 _ -> {:error, dgettext("errors", "Could not update state")}
457 end
458 end
459
460 def update_activity_scope(activity_id, opts \\ %{}) do
461 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
462 {:ok, activity} <- toggle_sensitive(activity, opts) do
463 set_visibility(activity, opts)
464 else
465 nil -> {:error, :not_found}
466 {:error, reason} -> {:error, reason}
467 end
468 end
469
470 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
471 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
472 end
473
474 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
475 when is_boolean(sensitive) do
476 new_data = Map.put(object.data, "sensitive", sensitive)
477
478 {:ok, object} =
479 object
480 |> Object.change(%{data: new_data})
481 |> Object.update_and_set_cache()
482
483 {:ok, Map.put(activity, :object, object)}
484 end
485
486 defp toggle_sensitive(activity, _), do: {:ok, activity}
487
488 defp set_visibility(activity, %{"visibility" => visibility}) do
489 Utils.update_activity_visibility(activity, visibility)
490 end
491
492 defp set_visibility(activity, _), do: {:ok, activity}
493
494 def hide_reblogs(%User{} = user, %User{} = target) do
495 UserRelationship.create_reblog_mute(user, target)
496 end
497
498 def show_reblogs(%User{} = user, %User{} = target) do
499 UserRelationship.delete_reblog_mute(user, target)
500 end
501 end