Merge remote-tracking branch 'origin/patch/readd-mastofe' into develop
[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, {:error, {:validate, {:error, changeset}}}} = e ->
232 if {:object, {"already liked by this actor", []}} in changeset.errors do
233 {:ok, :already_liked}
234 else
235 {:error, e}
236 end
237
238 e ->
239 {:error, e}
240 end
241 end
242
243 def unfavorite(id, user) do
244 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
245 {:find_activity, Activity.get_by_id(id)},
246 %Object{} = note <- Object.normalize(activity, fetch: false),
247 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
248 {:ok, undo, _} <- Builder.undo(user, like),
249 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
250 {:ok, activity}
251 else
252 {:find_activity, _} -> {:error, :not_found}
253 _ -> {:error, dgettext("errors", "Could not unfavorite")}
254 end
255 end
256
257 def react_with_emoji(id, user, emoji) do
258 with %Activity{} = activity <- Activity.get_by_id(id),
259 object <- Object.normalize(activity, fetch: false),
260 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
261 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
262 {:ok, activity}
263 else
264 _ ->
265 {:error, dgettext("errors", "Could not add reaction emoji")}
266 end
267 end
268
269 def unreact_with_emoji(id, user, emoji) do
270 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
271 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
272 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
273 {:ok, activity}
274 else
275 _ ->
276 {:error, dgettext("errors", "Could not remove reaction emoji")}
277 end
278 end
279
280 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
281 with :ok <- validate_not_author(object, user),
282 :ok <- validate_existing_votes(user, object),
283 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
284 answer_activities =
285 Enum.map(choices, fn index ->
286 {:ok, answer_object, _meta} =
287 Builder.answer(user, object, Enum.at(options, index)["name"])
288
289 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
290
291 {:ok, activity, _meta} =
292 activity_data
293 |> Map.put("cc", answer_object["cc"])
294 |> Map.put("context", answer_object["context"])
295 |> Pipeline.common_pipeline(local: true)
296
297 # TODO: Do preload of Pleroma.Object in Pipeline
298 Activity.normalize(activity.data)
299 end)
300
301 object = Object.get_cached_by_ap_id(object.data["id"])
302 {:ok, answer_activities, object}
303 end
304 end
305
306 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
307 do: {:error, dgettext("errors", "Poll's author can't vote")}
308
309 defp validate_not_author(_, _), do: :ok
310
311 defp validate_existing_votes(%{ap_id: ap_id}, object) do
312 if Utils.get_existing_votes(ap_id, object) == [] do
313 :ok
314 else
315 {:error, dgettext("errors", "Already voted")}
316 end
317 end
318
319 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
320 when is_list(any_of) and any_of != [],
321 do: {any_of, Enum.count(any_of)}
322
323 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
324 when is_list(one_of) and one_of != [],
325 do: {one_of, 1}
326
327 defp normalize_and_validate_choices(choices, object) do
328 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
329 {options, max_count} = get_options_and_max_count(object)
330 count = Enum.count(options)
331
332 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
333 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
334 {:ok, options, choices}
335 else
336 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
337 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
338 end
339 end
340
341 def public_announce?(_, %{visibility: visibility})
342 when visibility in ~w{public unlisted private direct},
343 do: visibility in ~w(public unlisted)
344
345 def public_announce?(object, _) do
346 Visibility.is_public?(object)
347 end
348
349 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
350
351 def get_visibility(%{visibility: visibility}, in_reply_to, _)
352 when visibility in ~w{public local unlisted private direct},
353 do: {visibility, get_replied_to_visibility(in_reply_to)}
354
355 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
356 visibility = {:list, String.to_integer(list_id)}
357 {visibility, get_replied_to_visibility(in_reply_to)}
358 end
359
360 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
361 visibility = get_replied_to_visibility(in_reply_to)
362 {visibility, visibility}
363 end
364
365 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
366
367 def get_replied_to_visibility(nil), do: nil
368
369 def get_replied_to_visibility(activity) do
370 with %Object{} = object <- Object.normalize(activity, fetch: false) do
371 Visibility.get_visibility(object)
372 end
373 end
374
375 def check_expiry_date({:ok, nil} = res), do: res
376
377 def check_expiry_date({:ok, in_seconds}) do
378 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
379
380 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
381 {:ok, expiry}
382 else
383 {:error, "Expiry date is too soon"}
384 end
385 end
386
387 def check_expiry_date(expiry_str) do
388 Ecto.Type.cast(:integer, expiry_str)
389 |> check_expiry_date()
390 end
391
392 def listen(user, data) do
393 with {:ok, draft} <- ActivityDraft.listen(user, data) do
394 ActivityPub.listen(draft.changes)
395 end
396 end
397
398 def post(user, %{status: _} = data) do
399 with {:ok, draft} <- ActivityDraft.create(user, data) do
400 activity = ActivityPub.create(draft.changes, draft.preview?)
401
402 unless draft.preview? do
403 Pleroma.Elasticsearch.maybe_put_into_elasticsearch(activity)
404 end
405
406 activity
407 end
408 end
409
410 @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
411 def pin(id, %User{} = user) do
412 with %Activity{} = activity <- create_activity_by_id(id),
413 true <- activity_belongs_to_actor(activity, user.ap_id),
414 true <- object_type_is_allowed_for_pin(activity.object),
415 true <- activity_is_public(activity),
416 {:ok, pin_data, _} <- Builder.pin(user, activity.object),
417 {:ok, _pin, _} <-
418 Pipeline.common_pipeline(pin_data,
419 local: true,
420 activity_id: id
421 ) do
422 {:ok, activity}
423 else
424 {:error, {:side_effects, error}} -> error
425 error -> error
426 end
427 end
428
429 defp create_activity_by_id(id) do
430 with nil <- Activity.create_by_id_with_object(id) do
431 {:error, :not_found}
432 end
433 end
434
435 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
436 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
437
438 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
439 with false <- type in ["Note", "Article", "Question"] do
440 {:error, :not_allowed}
441 end
442 end
443
444 defp activity_is_public(activity) do
445 with false <- Visibility.is_public?(activity) do
446 {:error, :visibility_error}
447 end
448 end
449
450 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
451 def unpin(id, user) do
452 with %Activity{} = activity <- create_activity_by_id(id),
453 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
454 {:ok, _unpin, _} <-
455 Pipeline.common_pipeline(unpin_data,
456 local: true,
457 activity_id: activity.id,
458 expires_at: activity.data["expires_at"],
459 featured_address: user.featured_address
460 ) do
461 {:ok, activity}
462 end
463 end
464
465 def add_mute(user, activity, params \\ %{}) do
466 expires_in = Map.get(params, :expires_in, 0)
467
468 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
469 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
470 if expires_in > 0 do
471 Pleroma.Workers.MuteExpireWorker.enqueue(
472 "unmute_conversation",
473 %{"user_id" => user.id, "activity_id" => activity.id},
474 schedule_in: expires_in
475 )
476 end
477
478 {:ok, activity}
479 else
480 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
481 end
482 end
483
484 def remove_mute(%User{} = user, %Activity{} = activity) do
485 ThreadMute.remove_mute(user.id, activity.data["context"])
486 {:ok, activity}
487 end
488
489 def remove_mute(user_id, activity_id) do
490 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
491 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
492 remove_mute(user, activity)
493 else
494 {what, result} = error ->
495 Logger.warn(
496 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
497 )
498
499 {:error, error}
500 end
501 end
502
503 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
504 when is_binary(context) do
505 ThreadMute.exists?(user_id, context)
506 end
507
508 def thread_muted?(_, _), do: false
509
510 def report(user, data) do
511 with {:ok, account} <- get_reported_account(data.account_id),
512 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
513 {:ok, statuses} <- get_report_statuses(account, data) do
514 ActivityPub.flag(%{
515 context: Utils.generate_context_id(),
516 actor: user,
517 account: account,
518 statuses: statuses,
519 content: content_html,
520 forward: Map.get(data, :forward, false)
521 })
522 end
523 end
524
525 defp get_reported_account(account_id) do
526 case User.get_cached_by_id(account_id) do
527 %User{} = account -> {:ok, account}
528 _ -> {:error, dgettext("errors", "Account not found")}
529 end
530 end
531
532 def update_report_state(activity_ids, state) when is_list(activity_ids) do
533 case Utils.update_report_state(activity_ids, state) do
534 :ok -> {:ok, activity_ids}
535 _ -> {:error, dgettext("errors", "Could not update state")}
536 end
537 end
538
539 def update_report_state(activity_id, state) do
540 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
541 Utils.update_report_state(activity, state)
542 else
543 nil -> {:error, :not_found}
544 _ -> {:error, dgettext("errors", "Could not update state")}
545 end
546 end
547
548 def update_activity_scope(activity_id, opts \\ %{}) do
549 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
550 {:ok, activity} <- toggle_sensitive(activity, opts) do
551 set_visibility(activity, opts)
552 else
553 nil -> {:error, :not_found}
554 {:error, reason} -> {:error, reason}
555 end
556 end
557
558 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
559 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
560 end
561
562 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
563 when is_boolean(sensitive) do
564 new_data = Map.put(object.data, "sensitive", sensitive)
565
566 {:ok, object} =
567 object
568 |> Object.change(%{data: new_data})
569 |> Object.update_and_set_cache()
570
571 {:ok, Map.put(activity, :object, object)}
572 end
573
574 defp toggle_sensitive(activity, _), do: {:ok, activity}
575
576 defp set_visibility(activity, %{visibility: visibility}) do
577 Utils.update_activity_visibility(activity, visibility)
578 end
579
580 defp set_visibility(activity, _), do: {:ok, activity}
581
582 def hide_reblogs(%User{} = user, %User{} = target) do
583 UserRelationship.create_reblog_mute(user, target)
584 end
585
586 def show_reblogs(%User{} = user, %User{} = target) do
587 UserRelationship.delete_reblog_mute(user, target)
588 end
589
590 def get_user(ap_id, fake_record_fallback \\ true) do
591 cond do
592 user = User.get_cached_by_ap_id(ap_id) ->
593 user
594
595 user = User.get_by_guessed_nickname(ap_id) ->
596 user
597
598 fake_record_fallback ->
599 # TODO: refactor (fake records is never a good idea)
600 User.error_user(ap_id)
601
602 true ->
603 nil
604 end
605 end
606 end