Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into feature/expire...
[akkoma] / lib / pleroma / web / common_api.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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
19 import Pleroma.Web.Gettext
20 import Pleroma.Web.CommonAPI.Utils
21
22 require Pleroma.Constants
23 require Logger
24
25 def block(blocker, blocked) do
26 with {:ok, block_data, _} <- Builder.block(blocker, blocked),
27 {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
28 {:ok, block}
29 end
30 end
31
32 def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
33 with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
34 :ok <- validate_chat_content_length(content, !!maybe_attachment),
35 {_, {:ok, chat_message_data, _meta}} <-
36 {:build_object,
37 Builder.chat_message(
38 user,
39 recipient.ap_id,
40 content |> format_chat_content,
41 attachment: maybe_attachment
42 )},
43 {_, {:ok, create_activity_data, _meta}} <-
44 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
45 {_, {:ok, %Activity{} = activity, _meta}} <-
46 {:common_pipeline,
47 Pipeline.common_pipeline(create_activity_data,
48 local: true,
49 idempotency_key: opts[:idempotency_key]
50 )} do
51 {:ok, activity}
52 else
53 {:common_pipeline, {:reject, _} = e} -> e
54 e -> e
55 end
56 end
57
58 defp format_chat_content(nil), do: nil
59
60 defp format_chat_content(content) do
61 {text, _, _} =
62 content
63 |> Formatter.html_escape("text/plain")
64 |> Formatter.linkify()
65 |> (fn {text, mentions, tags} ->
66 {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
67 end).()
68
69 text
70 end
71
72 defp validate_chat_content_length(_, true), do: :ok
73 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
74
75 defp validate_chat_content_length(content, _) do
76 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
77 :ok
78 else
79 {:error, :content_too_long}
80 end
81 end
82
83 def unblock(blocker, blocked) do
84 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
85 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
86 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
87 {:ok, unblock}
88 else
89 {:fetch_block, nil} ->
90 if User.blocks?(blocker, blocked) do
91 User.unblock(blocker, blocked)
92 {:ok, :no_activity}
93 else
94 {:error, :not_blocking}
95 end
96
97 e ->
98 e
99 end
100 end
101
102 def follow(follower, followed) do
103 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
104
105 with {:ok, follow_data, _} <- Builder.follow(follower, followed),
106 {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
107 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
108 if activity.data["state"] == "reject" do
109 {:error, :rejected}
110 else
111 {:ok, follower, followed, activity}
112 end
113 end
114 end
115
116 def unfollow(follower, unfollowed) do
117 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
118 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
119 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
120 {:ok, follower}
121 end
122 end
123
124 def accept_follow_request(follower, followed) do
125 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
126 {:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
127 {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do
128 {:ok, follower}
129 end
130 end
131
132 def reject_follow_request(follower, followed) do
133 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
134 {:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
135 {:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do
136 {:ok, follower}
137 end
138 end
139
140 def delete(activity_id, user) do
141 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
142 {:find_activity, Activity.get_by_id(activity_id)},
143 {_, %Object{} = object, _} <-
144 {:find_object, Object.normalize(activity, false), activity},
145 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
146 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
147 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
148 {:ok, delete}
149 else
150 {:find_activity, _} ->
151 {:error, :not_found}
152
153 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
154 # We have the create activity, but not the object, it was probably pruned.
155 # Insert a tombstone and try again
156 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
157 {:ok, _tombstone} <- Object.create(tombstone_data) do
158 delete(activity_id, user)
159 else
160 _ ->
161 Logger.error(
162 "Could not insert tombstone for missing object on deletion. Object is #{object}."
163 )
164
165 {:error, dgettext("errors", "Could not delete")}
166 end
167
168 _ ->
169 {:error, dgettext("errors", "Could not delete")}
170 end
171 end
172
173 def repeat(id, user, params \\ %{}) do
174 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
175 object = %Object{} <- Object.normalize(activity, false),
176 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
177 public = public_announce?(object, params),
178 {:ok, announce, _} <- Builder.announce(user, object, public: public),
179 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
180 {:ok, activity}
181 else
182 {:existing_announce, %Activity{} = announce} ->
183 {:ok, announce}
184
185 _ ->
186 {:error, :not_found}
187 end
188 end
189
190 def unrepeat(id, user) do
191 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
192 {:find_activity, Activity.get_by_id(id)},
193 %Object{} = note <- Object.normalize(activity, false),
194 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
195 {:ok, undo, _} <- Builder.undo(user, announce),
196 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
197 {:ok, activity}
198 else
199 {:find_activity, _} -> {:error, :not_found}
200 _ -> {:error, dgettext("errors", "Could not unrepeat")}
201 end
202 end
203
204 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
205 def favorite(%User{} = user, id) do
206 case favorite_helper(user, id) do
207 {:ok, _} = res ->
208 res
209
210 {:error, :not_found} = res ->
211 res
212
213 {:error, e} ->
214 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
215 {:error, dgettext("errors", "Could not favorite")}
216 end
217 end
218
219 def favorite_helper(user, id) do
220 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
221 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
222 {_, {:ok, %Activity{} = activity, _meta}} <-
223 {:common_pipeline,
224 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
225 {:ok, activity}
226 else
227 {:find_object, _} ->
228 {:error, :not_found}
229
230 {:common_pipeline,
231 {
232 :error,
233 {
234 :validate_object,
235 {
236 :error,
237 changeset
238 }
239 }
240 }} = e ->
241 if {:object, {"already liked by this actor", []}} in changeset.errors do
242 {:ok, :already_liked}
243 else
244 {:error, e}
245 end
246
247 e ->
248 {:error, e}
249 end
250 end
251
252 def unfavorite(id, user) do
253 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
254 {:find_activity, Activity.get_by_id(id)},
255 %Object{} = note <- Object.normalize(activity, false),
256 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
257 {:ok, undo, _} <- Builder.undo(user, like),
258 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
259 {:ok, activity}
260 else
261 {:find_activity, _} -> {:error, :not_found}
262 _ -> {:error, dgettext("errors", "Could not unfavorite")}
263 end
264 end
265
266 def react_with_emoji(id, user, emoji) do
267 with %Activity{} = activity <- Activity.get_by_id(id),
268 object <- Object.normalize(activity),
269 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
270 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
271 {:ok, activity}
272 else
273 _ ->
274 {:error, dgettext("errors", "Could not add reaction emoji")}
275 end
276 end
277
278 def unreact_with_emoji(id, user, emoji) do
279 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
280 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
281 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
282 {:ok, activity}
283 else
284 _ ->
285 {:error, dgettext("errors", "Could not remove reaction emoji")}
286 end
287 end
288
289 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
290 with :ok <- validate_not_author(object, user),
291 :ok <- validate_existing_votes(user, object),
292 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
293 answer_activities =
294 Enum.map(choices, fn index ->
295 {:ok, answer_object, _meta} =
296 Builder.answer(user, object, Enum.at(options, index)["name"])
297
298 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
299
300 {:ok, activity, _meta} =
301 activity_data
302 |> Map.put("cc", answer_object["cc"])
303 |> Map.put("context", answer_object["context"])
304 |> Pipeline.common_pipeline(local: true)
305
306 # TODO: Do preload of Pleroma.Object in Pipeline
307 Activity.normalize(activity.data)
308 end)
309
310 object = Object.get_cached_by_ap_id(object.data["id"])
311 {:ok, answer_activities, object}
312 end
313 end
314
315 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
316 do: {:error, dgettext("errors", "Poll's author can't vote")}
317
318 defp validate_not_author(_, _), do: :ok
319
320 defp validate_existing_votes(%{ap_id: ap_id}, object) do
321 if Utils.get_existing_votes(ap_id, object) == [] do
322 :ok
323 else
324 {:error, dgettext("errors", "Already voted")}
325 end
326 end
327
328 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
329 when is_list(any_of) and any_of != [],
330 do: {any_of, Enum.count(any_of)}
331
332 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
333 when is_list(one_of) and one_of != [],
334 do: {one_of, 1}
335
336 defp normalize_and_validate_choices(choices, object) do
337 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
338 {options, max_count} = get_options_and_max_count(object)
339 count = Enum.count(options)
340
341 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
342 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
343 {:ok, options, choices}
344 else
345 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
346 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
347 end
348 end
349
350 def public_announce?(_, %{visibility: visibility})
351 when visibility in ~w{public unlisted private direct},
352 do: visibility in ~w(public unlisted)
353
354 def public_announce?(object, _) do
355 Visibility.is_public?(object)
356 end
357
358 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
359
360 def get_visibility(%{visibility: visibility}, in_reply_to, _)
361 when visibility in ~w{public unlisted private direct},
362 do: {visibility, get_replied_to_visibility(in_reply_to)}
363
364 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
365 visibility = {:list, String.to_integer(list_id)}
366 {visibility, get_replied_to_visibility(in_reply_to)}
367 end
368
369 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
370 visibility = get_replied_to_visibility(in_reply_to)
371 {visibility, visibility}
372 end
373
374 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
375
376 def get_replied_to_visibility(nil), do: nil
377
378 def get_replied_to_visibility(activity) do
379 with %Object{} = object <- Object.normalize(activity) do
380 Visibility.get_visibility(object)
381 end
382 end
383
384 def check_expiry_date({:ok, nil} = res), do: res
385
386 def check_expiry_date({:ok, in_seconds}) do
387 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
388
389 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
390 {:ok, expiry}
391 else
392 {:error, "Expiry date is too soon"}
393 end
394 end
395
396 def check_expiry_date(expiry_str) do
397 Ecto.Type.cast(:integer, expiry_str)
398 |> check_expiry_date()
399 end
400
401 def listen(user, data) do
402 visibility = Map.get(data, :visibility, "public")
403
404 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
405 listen_data <-
406 data
407 |> Map.take([:album, :artist, :title, :length])
408 |> Map.new(fn {key, value} -> {to_string(key), value} end)
409 |> Map.put("type", "Audio")
410 |> Map.put("to", to)
411 |> Map.put("cc", cc)
412 |> Map.put("actor", user.ap_id),
413 {:ok, activity} <-
414 ActivityPub.listen(%{
415 actor: user,
416 to: to,
417 object: listen_data,
418 context: Utils.generate_context_id(),
419 additional: %{"cc" => cc}
420 }) do
421 {:ok, activity}
422 end
423 end
424
425 def post(user, %{status: _} = data) do
426 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
427 ActivityPub.create(draft.changes, draft.preview?)
428 end
429 end
430
431 def pin(id, %{ap_id: user_ap_id} = user) do
432 with %Activity{
433 actor: ^user_ap_id,
434 data: %{"type" => "Create"},
435 object: %Object{data: %{"type" => object_type}}
436 } = activity <- Activity.get_by_id_with_object(id),
437 true <- object_type in ["Note", "Article", "Question"],
438 true <- Visibility.is_public?(activity),
439 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
440 {:ok, activity}
441 else
442 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
443 _ -> {:error, dgettext("errors", "Could not pin")}
444 end
445 end
446
447 def unpin(id, user) do
448 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
449 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
450 {:ok, activity}
451 else
452 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
453 _ -> {:error, dgettext("errors", "Could not unpin")}
454 end
455 end
456
457 def add_mute(user, activity, params \\ %{}) do
458 expires_in = Map.get(params, :expires_in, 0)
459
460 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
461 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
462 if expires_in > 0 do
463 Pleroma.Workers.MuteExpireWorker.enqueue(
464 "unmute_conversation",
465 %{"user_id" => user.id, "activity_id" => activity.id},
466 schedule_in: expires_in
467 )
468 end
469
470 {:ok, activity}
471 else
472 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
473 end
474 end
475
476 def remove_mute(%User{} = user, %Activity{} = activity) do
477 ThreadMute.remove_mute(user.id, activity.data["context"])
478 {:ok, activity}
479 end
480
481 def remove_mute(user_id, activity_id) do
482 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
483 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
484 remove_mute(user, activity)
485 else
486 {what, result} = error ->
487 Logger.warn(
488 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{
489 activity_id
490 }"
491 )
492
493 {:error, error}
494 end
495 end
496
497 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
498 when is_binary(context) do
499 ThreadMute.exists?(user_id, context)
500 end
501
502 def thread_muted?(_, _), do: false
503
504 def report(user, data) do
505 with {:ok, account} <- get_reported_account(data.account_id),
506 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
507 {:ok, statuses} <- get_report_statuses(account, data) do
508 ActivityPub.flag(%{
509 context: Utils.generate_context_id(),
510 actor: user,
511 account: account,
512 statuses: statuses,
513 content: content_html,
514 forward: Map.get(data, :forward, false)
515 })
516 end
517 end
518
519 defp get_reported_account(account_id) do
520 case User.get_cached_by_id(account_id) do
521 %User{} = account -> {:ok, account}
522 _ -> {:error, dgettext("errors", "Account not found")}
523 end
524 end
525
526 def update_report_state(activity_ids, state) when is_list(activity_ids) do
527 case Utils.update_report_state(activity_ids, state) do
528 :ok -> {:ok, activity_ids}
529 _ -> {:error, dgettext("errors", "Could not update state")}
530 end
531 end
532
533 def update_report_state(activity_id, state) do
534 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
535 Utils.update_report_state(activity, state)
536 else
537 nil -> {:error, :not_found}
538 _ -> {:error, dgettext("errors", "Could not update state")}
539 end
540 end
541
542 def update_activity_scope(activity_id, opts \\ %{}) do
543 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
544 {:ok, activity} <- toggle_sensitive(activity, opts) do
545 set_visibility(activity, opts)
546 else
547 nil -> {:error, :not_found}
548 {:error, reason} -> {:error, reason}
549 end
550 end
551
552 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
553 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
554 end
555
556 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
557 when is_boolean(sensitive) do
558 new_data = Map.put(object.data, "sensitive", sensitive)
559
560 {:ok, object} =
561 object
562 |> Object.change(%{data: new_data})
563 |> Object.update_and_set_cache()
564
565 {:ok, Map.put(activity, :object, object)}
566 end
567
568 defp toggle_sensitive(activity, _), do: {:ok, activity}
569
570 defp set_visibility(activity, %{visibility: visibility}) do
571 Utils.update_activity_visibility(activity, visibility)
572 end
573
574 defp set_visibility(activity, _), do: {:ok, activity}
575
576 def hide_reblogs(%User{} = user, %User{} = target) do
577 UserRelationship.create_reblog_mute(user, target)
578 end
579
580 def show_reblogs(%User{} = user, %User{} = target) do
581 UserRelationship.delete_reblog_mute(user, target)
582 end
583
584 def get_user(ap_id, fake_record_fallback \\ true) do
585 cond do
586 user = User.get_cached_by_ap_id(ap_id) ->
587 user
588
589 user = User.get_by_guessed_nickname(ap_id) ->
590 user
591
592 fake_record_fallback ->
593 # TODO: refactor (fake records is never a good idea)
594 User.error_user(ap_id)
595
596 true ->
597 nil
598 end
599 end
600 end