b003e30c7b6fd84b56ede42da927f60efa60c585
[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 def pin(id, %{ap_id: user_ap_id} = user) do
415 with %Activity{
416 actor: ^user_ap_id,
417 data: %{"type" => "Create"},
418 object: %Object{data: %{"type" => object_type}}
419 } = activity <- Activity.get_by_id_with_object(id),
420 true <- object_type in ["Note", "Article", "Question"],
421 true <- Visibility.is_public?(activity),
422 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
423 {:ok, activity}
424 else
425 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
426 _ -> {:error, dgettext("errors", "Could not pin")}
427 end
428 end
429
430 def unpin(id, user) do
431 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
432 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
433 {:ok, activity}
434 else
435 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
436 _ -> {:error, dgettext("errors", "Could not unpin")}
437 end
438 end
439
440 def add_mute(user, activity, params \\ %{}) do
441 expires_in = Map.get(params, :expires_in, 0)
442
443 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
444 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
445 if expires_in > 0 do
446 Pleroma.Workers.MuteExpireWorker.enqueue(
447 "unmute_conversation",
448 %{"user_id" => user.id, "activity_id" => activity.id},
449 schedule_in: expires_in
450 )
451 end
452
453 {:ok, activity}
454 else
455 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
456 end
457 end
458
459 def remove_mute(%User{} = user, %Activity{} = activity) do
460 ThreadMute.remove_mute(user.id, activity.data["context"])
461 {:ok, activity}
462 end
463
464 def remove_mute(user_id, activity_id) do
465 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
466 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
467 remove_mute(user, activity)
468 else
469 {what, result} = error ->
470 Logger.warn(
471 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{
472 activity_id
473 }"
474 )
475
476 {:error, error}
477 end
478 end
479
480 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
481 when is_binary(context) do
482 ThreadMute.exists?(user_id, context)
483 end
484
485 def thread_muted?(_, _), do: false
486
487 def report(user, data) do
488 with {:ok, account} <- get_reported_account(data.account_id),
489 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
490 {:ok, statuses} <- get_report_statuses(account, data) do
491 ActivityPub.flag(%{
492 context: Utils.generate_context_id(),
493 actor: user,
494 account: account,
495 statuses: statuses,
496 content: content_html,
497 forward: Map.get(data, :forward, false)
498 })
499 end
500 end
501
502 defp get_reported_account(account_id) do
503 case User.get_cached_by_id(account_id) do
504 %User{} = account -> {:ok, account}
505 _ -> {:error, dgettext("errors", "Account not found")}
506 end
507 end
508
509 def update_report_state(activity_ids, state) when is_list(activity_ids) do
510 case Utils.update_report_state(activity_ids, state) do
511 :ok -> {:ok, activity_ids}
512 _ -> {:error, dgettext("errors", "Could not update state")}
513 end
514 end
515
516 def update_report_state(activity_id, state) do
517 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
518 Utils.update_report_state(activity, state)
519 else
520 nil -> {:error, :not_found}
521 _ -> {:error, dgettext("errors", "Could not update state")}
522 end
523 end
524
525 def update_activity_scope(activity_id, opts \\ %{}) do
526 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
527 {:ok, activity} <- toggle_sensitive(activity, opts) do
528 set_visibility(activity, opts)
529 else
530 nil -> {:error, :not_found}
531 {:error, reason} -> {:error, reason}
532 end
533 end
534
535 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
536 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
537 end
538
539 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
540 when is_boolean(sensitive) do
541 new_data = Map.put(object.data, "sensitive", sensitive)
542
543 {:ok, object} =
544 object
545 |> Object.change(%{data: new_data})
546 |> Object.update_and_set_cache()
547
548 {:ok, Map.put(activity, :object, object)}
549 end
550
551 defp toggle_sensitive(activity, _), do: {:ok, activity}
552
553 defp set_visibility(activity, %{visibility: visibility}) do
554 Utils.update_activity_visibility(activity, visibility)
555 end
556
557 defp set_visibility(activity, _), do: {:ok, activity}
558
559 def hide_reblogs(%User{} = user, %User{} = target) do
560 UserRelationship.create_reblog_mute(user, target)
561 end
562
563 def show_reblogs(%User{} = user, %User{} = target) do
564 UserRelationship.delete_reblog_mute(user, target)
565 end
566
567 def get_user(ap_id, fake_record_fallback \\ true) do
568 cond do
569 user = User.get_cached_by_ap_id(ap_id) ->
570 user
571
572 user = User.get_by_guessed_nickname(ap_id) ->
573 user
574
575 fake_record_fallback ->
576 # TODO: refactor (fake records is never a good idea)
577 User.error_user(ap_id)
578
579 true ->
580 nil
581 end
582 end
583 end