expanding AddRemoveValidator
[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 {:ok, delete}
150 else
151 {:find_activity, _} ->
152 {:error, :not_found}
153
154 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
155 # We have the create activity, but not the object, it was probably pruned.
156 # Insert a tombstone and try again
157 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
158 {:ok, _tombstone} <- Object.create(tombstone_data) do
159 delete(activity_id, user)
160 else
161 _ ->
162 Logger.error(
163 "Could not insert tombstone for missing object on deletion. Object is #{object}."
164 )
165
166 {:error, dgettext("errors", "Could not delete")}
167 end
168
169 _ ->
170 {:error, dgettext("errors", "Could not delete")}
171 end
172 end
173
174 def repeat(id, user, params \\ %{}) do
175 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
176 object = %Object{} <- Object.normalize(activity, fetch: false),
177 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
178 public = public_announce?(object, params),
179 {:ok, announce, _} <- Builder.announce(user, object, public: public),
180 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
181 {:ok, activity}
182 else
183 {:existing_announce, %Activity{} = announce} ->
184 {:ok, announce}
185
186 _ ->
187 {:error, :not_found}
188 end
189 end
190
191 def unrepeat(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{} = announce <- Utils.get_existing_announce(user.ap_id, note),
196 {:ok, undo, _} <- Builder.undo(user, announce),
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 unrepeat")}
202 end
203 end
204
205 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
206 def favorite(%User{} = user, id) do
207 case favorite_helper(user, id) do
208 {:ok, _} = res ->
209 res
210
211 {:error, :not_found} = res ->
212 res
213
214 {:error, e} ->
215 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
216 {:error, dgettext("errors", "Could not favorite")}
217 end
218 end
219
220 def favorite_helper(user, id) do
221 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
222 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
223 {_, {:ok, %Activity{} = activity, _meta}} <-
224 {:common_pipeline,
225 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
226 {:ok, activity}
227 else
228 {:find_object, _} ->
229 {:error, :not_found}
230
231 {:common_pipeline,
232 {
233 :error,
234 {
235 :validate_object,
236 {
237 :error,
238 changeset
239 }
240 }
241 }} = e ->
242 if {:object, {"already liked by this actor", []}} in changeset.errors do
243 {:ok, :already_liked}
244 else
245 {:error, e}
246 end
247
248 e ->
249 {:error, e}
250 end
251 end
252
253 def unfavorite(id, user) do
254 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
255 {:find_activity, Activity.get_by_id(id)},
256 %Object{} = note <- Object.normalize(activity, fetch: false),
257 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
258 {:ok, undo, _} <- Builder.undo(user, like),
259 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
260 {:ok, activity}
261 else
262 {:find_activity, _} -> {:error, :not_found}
263 _ -> {:error, dgettext("errors", "Could not unfavorite")}
264 end
265 end
266
267 def react_with_emoji(id, user, emoji) do
268 with %Activity{} = activity <- Activity.get_by_id(id),
269 object <- Object.normalize(activity, fetch: false),
270 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
271 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
272 {:ok, activity}
273 else
274 _ ->
275 {:error, dgettext("errors", "Could not add reaction emoji")}
276 end
277 end
278
279 def unreact_with_emoji(id, user, emoji) do
280 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
281 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
282 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
283 {:ok, activity}
284 else
285 _ ->
286 {:error, dgettext("errors", "Could not remove reaction emoji")}
287 end
288 end
289
290 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
291 with :ok <- validate_not_author(object, user),
292 :ok <- validate_existing_votes(user, object),
293 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
294 answer_activities =
295 Enum.map(choices, fn index ->
296 {:ok, answer_object, _meta} =
297 Builder.answer(user, object, Enum.at(options, index)["name"])
298
299 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
300
301 {:ok, activity, _meta} =
302 activity_data
303 |> Map.put("cc", answer_object["cc"])
304 |> Map.put("context", answer_object["context"])
305 |> Pipeline.common_pipeline(local: true)
306
307 # TODO: Do preload of Pleroma.Object in Pipeline
308 Activity.normalize(activity.data)
309 end)
310
311 object = Object.get_cached_by_ap_id(object.data["id"])
312 {:ok, answer_activities, object}
313 end
314 end
315
316 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
317 do: {:error, dgettext("errors", "Poll's author can't vote")}
318
319 defp validate_not_author(_, _), do: :ok
320
321 defp validate_existing_votes(%{ap_id: ap_id}, object) do
322 if Utils.get_existing_votes(ap_id, object) == [] do
323 :ok
324 else
325 {:error, dgettext("errors", "Already voted")}
326 end
327 end
328
329 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
330 when is_list(any_of) and any_of != [],
331 do: {any_of, Enum.count(any_of)}
332
333 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
334 when is_list(one_of) and one_of != [],
335 do: {one_of, 1}
336
337 defp normalize_and_validate_choices(choices, object) do
338 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
339 {options, max_count} = get_options_and_max_count(object)
340 count = Enum.count(options)
341
342 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
343 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
344 {:ok, options, choices}
345 else
346 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
347 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
348 end
349 end
350
351 def public_announce?(_, %{visibility: visibility})
352 when visibility in ~w{public unlisted private direct},
353 do: visibility in ~w(public unlisted)
354
355 def public_announce?(object, _) do
356 Visibility.is_public?(object)
357 end
358
359 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
360
361 def get_visibility(%{visibility: visibility}, in_reply_to, _)
362 when visibility in ~w{public local unlisted private direct},
363 do: {visibility, get_replied_to_visibility(in_reply_to)}
364
365 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
366 visibility = {:list, String.to_integer(list_id)}
367 {visibility, get_replied_to_visibility(in_reply_to)}
368 end
369
370 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
371 visibility = get_replied_to_visibility(in_reply_to)
372 {visibility, visibility}
373 end
374
375 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
376
377 def get_replied_to_visibility(nil), do: nil
378
379 def get_replied_to_visibility(activity) do
380 with %Object{} = object <- Object.normalize(activity, fetch: false) do
381 Visibility.get_visibility(object)
382 end
383 end
384
385 def check_expiry_date({:ok, nil} = res), do: res
386
387 def check_expiry_date({:ok, in_seconds}) do
388 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
389
390 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
391 {:ok, expiry}
392 else
393 {:error, "Expiry date is too soon"}
394 end
395 end
396
397 def check_expiry_date(expiry_str) do
398 Ecto.Type.cast(:integer, expiry_str)
399 |> check_expiry_date()
400 end
401
402 def listen(user, data) do
403 with {:ok, draft} <- ActivityDraft.listen(user, data) do
404 ActivityPub.listen(draft.changes)
405 end
406 end
407
408 def post(user, %{status: _} = data) do
409 with {:ok, draft} <- ActivityDraft.create(user, data) do
410 ActivityPub.create(draft.changes, draft.preview?)
411 end
412 end
413
414 @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
415 def pin(id, %User{} = user) do
416 with %Activity{} = activity <- create_activity_by_id(id),
417 true <- activity_belongs_to_actor(activity, user.ap_id),
418 true <- object_type_is_allowed_for_pin(activity.object),
419 true <- activity_is_public(activity),
420 {:ok, pin_data, _} <- Builder.pin(user, activity.object),
421 {:ok, _pin, _} <-
422 Pipeline.common_pipeline(pin_data,
423 local: true,
424 activity_id: id
425 ) do
426 {:ok, activity}
427 else
428 {:error, {:execute_side_effects, error}} -> error
429 error -> error
430 end
431 end
432
433 defp create_activity_by_id(id) do
434 with nil <- Activity.create_by_id_with_object(id) do
435 {:error, :not_found}
436 end
437 end
438
439 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
440 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
441
442 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
443 with false <- type in ["Note", "Article", "Question"] do
444 {:error, :not_allowed}
445 end
446 end
447
448 defp activity_is_public(activity) do
449 with false <- Visibility.is_public?(activity) do
450 {:error, :visibility_error}
451 end
452 end
453
454 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
455 def unpin(id, user) do
456 with %Activity{} = activity <- create_activity_by_id(id),
457 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
458 {:ok, _unpin, _} <-
459 Pipeline.common_pipeline(unpin_data,
460 local: true,
461 activity_id: activity.id,
462 expires_at: activity.data["expires_at"],
463 featured_address: user.featured_address
464 ) do
465 {:ok, activity}
466 end
467 end
468
469 def add_mute(user, activity, params \\ %{}) do
470 expires_in = Map.get(params, :expires_in, 0)
471
472 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
473 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
474 if expires_in > 0 do
475 Pleroma.Workers.MuteExpireWorker.enqueue(
476 "unmute_conversation",
477 %{"user_id" => user.id, "activity_id" => activity.id},
478 schedule_in: expires_in
479 )
480 end
481
482 {:ok, activity}
483 else
484 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
485 end
486 end
487
488 def remove_mute(%User{} = user, %Activity{} = activity) do
489 ThreadMute.remove_mute(user.id, activity.data["context"])
490 {:ok, activity}
491 end
492
493 def remove_mute(user_id, activity_id) do
494 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
495 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
496 remove_mute(user, activity)
497 else
498 {what, result} = error ->
499 Logger.warn(
500 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{
501 activity_id
502 }"
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