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