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