update adminfe
[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 ActivityPub.create(draft.changes, draft.preview?)
427 end
428 end
429
430 def pin(id, %{ap_id: user_ap_id} = user) do
431 with %Activity{
432 actor: ^user_ap_id,
433 data: %{"type" => "Create"},
434 object: %Object{data: %{"type" => object_type}}
435 } = activity <- Activity.get_by_id_with_object(id),
436 true <- object_type in ["Note", "Article", "Question"],
437 true <- Visibility.is_public?(activity),
438 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
439 {:ok, activity}
440 else
441 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
442 _ -> {:error, dgettext("errors", "Could not pin")}
443 end
444 end
445
446 def unpin(id, user) do
447 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
448 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
449 {:ok, activity}
450 else
451 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
452 _ -> {:error, dgettext("errors", "Could not unpin")}
453 end
454 end
455
456 def add_mute(user, activity) do
457 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
458 {:ok, activity}
459 else
460 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
461 end
462 end
463
464 def remove_mute(user, activity) do
465 ThreadMute.remove_mute(user.id, activity.data["context"])
466 {:ok, activity}
467 end
468
469 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
470 when is_binary("context") do
471 ThreadMute.exists?(user_id, context)
472 end
473
474 def thread_muted?(_, _), do: false
475
476 def report(user, data) do
477 with {:ok, account} <- get_reported_account(data.account_id),
478 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
479 {:ok, statuses} <- get_report_statuses(account, data) do
480 ActivityPub.flag(%{
481 context: Utils.generate_context_id(),
482 actor: user,
483 account: account,
484 statuses: statuses,
485 content: content_html,
486 forward: Map.get(data, :forward, false)
487 })
488 end
489 end
490
491 defp get_reported_account(account_id) do
492 case User.get_cached_by_id(account_id) do
493 %User{} = account -> {:ok, account}
494 _ -> {:error, dgettext("errors", "Account not found")}
495 end
496 end
497
498 def update_report_state(activity_ids, state) when is_list(activity_ids) do
499 case Utils.update_report_state(activity_ids, state) do
500 :ok -> {:ok, activity_ids}
501 _ -> {:error, dgettext("errors", "Could not update state")}
502 end
503 end
504
505 def update_report_state(activity_id, state) do
506 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
507 Utils.update_report_state(activity, state)
508 else
509 nil -> {:error, :not_found}
510 _ -> {:error, dgettext("errors", "Could not update state")}
511 end
512 end
513
514 def update_activity_scope(activity_id, opts \\ %{}) do
515 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
516 {:ok, activity} <- toggle_sensitive(activity, opts) do
517 set_visibility(activity, opts)
518 else
519 nil -> {:error, :not_found}
520 {:error, reason} -> {:error, reason}
521 end
522 end
523
524 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
525 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
526 end
527
528 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
529 when is_boolean(sensitive) do
530 new_data = Map.put(object.data, "sensitive", sensitive)
531
532 {:ok, object} =
533 object
534 |> Object.change(%{data: new_data})
535 |> Object.update_and_set_cache()
536
537 {:ok, Map.put(activity, :object, object)}
538 end
539
540 defp toggle_sensitive(activity, _), do: {:ok, activity}
541
542 defp set_visibility(activity, %{visibility: visibility}) do
543 Utils.update_activity_visibility(activity, visibility)
544 end
545
546 defp set_visibility(activity, _), do: {:ok, activity}
547
548 def hide_reblogs(%User{} = user, %User{} = target) do
549 UserRelationship.create_reblog_mute(user, target)
550 end
551
552 def show_reblogs(%User{} = user, %User{} = target) do
553 UserRelationship.delete_reblog_mute(user, target)
554 end
555 end