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