Let pins federate
[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{ap_id: actor} = user) do
416 with %Activity{} = activity <- create_activity_by_id(id),
417 true <- activity_belongs_to_actor(activity, actor),
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, local: true, activity_id: id) do
423 {:ok, activity}
424 else
425 {:error, {:execute_side_effects, error}} -> error
426 error -> error
427 end
428 end
429
430 defp create_activity_by_id(id) do
431 with nil <- Activity.create_by_id_with_object(id) do
432 {:error, :not_found}
433 end
434 end
435
436 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
437 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
438
439 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
440 with false <- type in ["Note", "Article", "Question"] do
441 {:error, :not_allowed}
442 end
443 end
444
445 defp activity_is_public(activity) do
446 with false <- Visibility.is_public?(activity) do
447 {:error, :visibility_error}
448 end
449 end
450
451 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
452 def unpin(id, user) do
453 with %Activity{} = activity <- create_activity_by_id(id),
454 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
455 {:ok, _unpin, _} <-
456 Pipeline.common_pipeline(unpin_data,
457 local: true,
458 activity_id: activity.id,
459 expires_at: activity.data["expires_at"]
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: #{
497 activity_id
498 }"
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