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