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