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