b4887d4244056157e7abfe61bd3bb59b44150e98
[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 # Remove from search index for local posts
150 Pleroma.Search.remove_from_index(object)
151
152 {:ok, delete}
153 else
154 {:find_activity, _} ->
155 {:error, :not_found}
156
157 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
158 # We have the create activity, but not the object, it was probably pruned.
159 # Insert a tombstone and try again
160 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
161 {:ok, _tombstone} <- Object.create(tombstone_data) do
162 delete(activity_id, user)
163 else
164 _ ->
165 Logger.error(
166 "Could not insert tombstone for missing object on deletion. Object is #{object}."
167 )
168
169 {:error, dgettext("errors", "Could not delete")}
170 end
171
172 _ ->
173 {:error, dgettext("errors", "Could not delete")}
174 end
175 end
176
177 def repeat(id, user, params \\ %{}) do
178 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
179 object = %Object{} <- Object.normalize(activity, fetch: false),
180 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
181 public = public_announce?(object, params),
182 {:ok, announce, _} <- Builder.announce(user, object, public: public),
183 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
184 {:ok, activity}
185 else
186 {:existing_announce, %Activity{} = announce} ->
187 {:ok, announce}
188
189 _ ->
190 {:error, :not_found}
191 end
192 end
193
194 def unrepeat(id, user) do
195 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
196 {:find_activity, Activity.get_by_id(id)},
197 %Object{} = note <- Object.normalize(activity, fetch: false),
198 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
199 {:ok, undo, _} <- Builder.undo(user, announce),
200 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
201 {:ok, activity}
202 else
203 {:find_activity, _} -> {:error, :not_found}
204 _ -> {:error, dgettext("errors", "Could not unrepeat")}
205 end
206 end
207
208 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
209 def favorite(%User{} = user, id) do
210 case favorite_helper(user, id) do
211 {:ok, _} = res ->
212 res
213
214 {:error, :not_found} = res ->
215 res
216
217 {:error, e} ->
218 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
219 {:error, dgettext("errors", "Could not favorite")}
220 end
221 end
222
223 def favorite_helper(user, id) do
224 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
225 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
226 {_, {:ok, %Activity{} = activity, _meta}} <-
227 {:common_pipeline,
228 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
229 {:ok, activity}
230 else
231 {:find_object, _} ->
232 {:error, :not_found}
233
234 {:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
235 if {:object, {"already liked by this actor", []}} in changeset.errors do
236 {:ok, :already_liked}
237 else
238 {:error, e}
239 end
240
241 e ->
242 {:error, e}
243 end
244 end
245
246 def unfavorite(id, user) do
247 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
248 {:find_activity, Activity.get_by_id(id)},
249 %Object{} = note <- Object.normalize(activity, fetch: false),
250 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
251 {:ok, undo, _} <- Builder.undo(user, like),
252 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
253 {:ok, activity}
254 else
255 {:find_activity, _} -> {:error, :not_found}
256 _ -> {:error, dgettext("errors", "Could not unfavorite")}
257 end
258 end
259
260 def react_with_emoji(id, user, emoji) do
261 with %Activity{} = activity <- Activity.get_by_id(id),
262 object <- Object.normalize(activity, fetch: false),
263 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
264 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
265 {:ok, activity}
266 else
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 post(user, %{status: _} = data) do
401 with {:ok, draft} <- ActivityDraft.create(user, data) do
402 activity = ActivityPub.create(draft.changes, draft.preview?)
403
404 unless draft.preview? do
405 Pleroma.Elasticsearch.maybe_put_into_elasticsearch(activity)
406 end
407
408 activity
409 end
410 end
411
412 @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
413 def pin(id, %User{} = user) do
414 with %Activity{} = activity <- create_activity_by_id(id),
415 true <- activity_belongs_to_actor(activity, user.ap_id),
416 true <- object_type_is_allowed_for_pin(activity.object),
417 true <- activity_is_public(activity),
418 {:ok, pin_data, _} <- Builder.pin(user, activity.object),
419 {:ok, _pin, _} <-
420 Pipeline.common_pipeline(pin_data,
421 local: true,
422 activity_id: id
423 ) do
424 {:ok, activity}
425 else
426 {:error, {:side_effects, error}} -> error
427 error -> error
428 end
429 end
430
431 defp create_activity_by_id(id) do
432 with nil <- Activity.create_by_id_with_object(id) do
433 {:error, :not_found}
434 end
435 end
436
437 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
438 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
439
440 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
441 with false <- type in ["Note", "Article", "Question"] do
442 {:error, :not_allowed}
443 end
444 end
445
446 defp activity_is_public(activity) do
447 with false <- Visibility.is_public?(activity) do
448 {:error, :visibility_error}
449 end
450 end
451
452 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
453 def unpin(id, user) do
454 with %Activity{} = activity <- create_activity_by_id(id),
455 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
456 {:ok, _unpin, _} <-
457 Pipeline.common_pipeline(unpin_data,
458 local: true,
459 activity_id: activity.id,
460 expires_at: activity.data["expires_at"],
461 featured_address: user.featured_address
462 ) do
463 {:ok, activity}
464 end
465 end
466
467 def add_mute(user, activity, params \\ %{}) do
468 expires_in = Map.get(params, :expires_in, 0)
469
470 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
471 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
472 if expires_in > 0 do
473 Pleroma.Workers.MuteExpireWorker.enqueue(
474 "unmute_conversation",
475 %{"user_id" => user.id, "activity_id" => activity.id},
476 schedule_in: expires_in
477 )
478 end
479
480 {:ok, activity}
481 else
482 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
483 end
484 end
485
486 def remove_mute(%User{} = user, %Activity{} = activity) do
487 ThreadMute.remove_mute(user.id, activity.data["context"])
488 {:ok, activity}
489 end
490
491 def remove_mute(user_id, activity_id) do
492 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
493 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
494 remove_mute(user, activity)
495 else
496 {what, result} = error ->
497 Logger.warn(
498 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
499 )
500
501 {:error, error}
502 end
503 end
504
505 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
506 when is_binary(context) do
507 ThreadMute.exists?(user_id, context)
508 end
509
510 def thread_muted?(_, _), do: false
511
512 def report(user, data) do
513 with {:ok, account} <- get_reported_account(data.account_id),
514 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
515 {:ok, statuses} <- get_report_statuses(account, data) do
516 ActivityPub.flag(%{
517 context: Utils.generate_context_id(),
518 actor: user,
519 account: account,
520 statuses: statuses,
521 content: content_html,
522 forward: Map.get(data, :forward, false)
523 })
524 end
525 end
526
527 defp get_reported_account(account_id) do
528 case User.get_cached_by_id(account_id) do
529 %User{} = account -> {:ok, account}
530 _ -> {:error, dgettext("errors", "Account not found")}
531 end
532 end
533
534 def update_report_state(activity_ids, state) when is_list(activity_ids) do
535 case Utils.update_report_state(activity_ids, state) do
536 :ok -> {:ok, activity_ids}
537 _ -> {:error, dgettext("errors", "Could not update state")}
538 end
539 end
540
541 def update_report_state(activity_id, state) do
542 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
543 Utils.update_report_state(activity, state)
544 else
545 nil -> {:error, :not_found}
546 _ -> {:error, dgettext("errors", "Could not update state")}
547 end
548 end
549
550 def update_activity_scope(activity_id, opts \\ %{}) do
551 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
552 {:ok, activity} <- toggle_sensitive(activity, opts) do
553 set_visibility(activity, opts)
554 else
555 nil -> {:error, :not_found}
556 {:error, reason} -> {:error, reason}
557 end
558 end
559
560 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
561 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
562 end
563
564 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
565 when is_boolean(sensitive) do
566 new_data = Map.put(object.data, "sensitive", sensitive)
567
568 {:ok, object} =
569 object
570 |> Object.change(%{data: new_data})
571 |> Object.update_and_set_cache()
572
573 {:ok, Map.put(activity, :object, object)}
574 end
575
576 defp toggle_sensitive(activity, _), do: {:ok, activity}
577
578 defp set_visibility(activity, %{visibility: visibility}) do
579 Utils.update_activity_visibility(activity, visibility)
580 end
581
582 defp set_visibility(activity, _), do: {:ok, activity}
583
584 def hide_reblogs(%User{} = user, %User{} = target) do
585 UserRelationship.create_reblog_mute(user, target)
586 end
587
588 def show_reblogs(%User{} = user, %User{} = target) do
589 UserRelationship.delete_reblog_mute(user, target)
590 end
591
592 def get_user(ap_id, fake_record_fallback \\ true) do
593 cond do
594 user = User.get_cached_by_ap_id(ap_id) ->
595 user
596
597 user = User.get_by_guessed_nickname(ap_id) ->
598 user
599
600 fake_record_fallback ->
601 # TODO: refactor (fake records is never a good idea)
602 User.error_user(ap_id)
603
604 true ->
605 nil
606 end
607 end
608 end