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),
171 object = %Object{} <- Object.normalize(activity, false),
172 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
173 public = public_announce?(object, params),
174 {:ok, announce, _} <- Builder.announce(user, object, public: public),
175 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
176 {:ok, activity}
177 else
178 {:existing_announce, %Activity{} = announce} ->
179 {:ok, announce}
180
181 _ ->
182 {:error, :not_found}
183 end
184 end
185
186 def unrepeat(id, user) do
187 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
188 {:find_activity, Activity.get_by_id(id)},
189 %Object{} = note <- Object.normalize(activity, false),
190 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
191 {:ok, undo, _} <- Builder.undo(user, announce),
192 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
193 {:ok, activity}
194 else
195 {:find_activity, _} -> {:error, :not_found}
196 _ -> {:error, dgettext("errors", "Could not unrepeat")}
197 end
198 end
199
200 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
201 def favorite(%User{} = user, id) do
202 case favorite_helper(user, id) do
203 {:ok, _} = res ->
204 res
205
206 {:error, :not_found} = res ->
207 res
208
209 {:error, e} ->
210 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
211 {:error, dgettext("errors", "Could not favorite")}
212 end
213 end
214
215 def favorite_helper(user, id) do
216 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
217 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
218 {_, {:ok, %Activity{} = activity, _meta}} <-
219 {:common_pipeline,
220 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
221 {:ok, activity}
222 else
223 {:find_object, _} ->
224 {:error, :not_found}
225
226 {:common_pipeline,
227 {
228 :error,
229 {
230 :validate_object,
231 {
232 :error,
233 changeset
234 }
235 }
236 }} = e ->
237 if {:object, {"already liked by this actor", []}} in changeset.errors do
238 {:ok, :already_liked}
239 else
240 {:error, e}
241 end
242
243 e ->
244 {:error, e}
245 end
246 end
247
248 def unfavorite(id, user) do
249 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
250 {:find_activity, Activity.get_by_id(id)},
251 %Object{} = note <- Object.normalize(activity, false),
252 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
253 {:ok, undo, _} <- Builder.undo(user, like),
254 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
255 {:ok, activity}
256 else
257 {:find_activity, _} -> {:error, :not_found}
258 _ -> {:error, dgettext("errors", "Could not unfavorite")}
259 end
260 end
261
262 def react_with_emoji(id, user, emoji) do
263 with %Activity{} = activity <- Activity.get_by_id(id),
264 object <- Object.normalize(activity),
265 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
266 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
267 {:ok, activity}
268 else
269 _ ->
270 {:error, dgettext("errors", "Could not add reaction emoji")}
271 end
272 end
273
274 def unreact_with_emoji(id, user, emoji) do
275 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
276 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
277 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
278 {:ok, activity}
279 else
280 _ ->
281 {:error, dgettext("errors", "Could not remove reaction emoji")}
282 end
283 end
284
285 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
286 with :ok <- validate_not_author(object, user),
287 :ok <- validate_existing_votes(user, object),
288 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
289 answer_activities =
290 Enum.map(choices, fn index ->
291 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
292
293 {:ok, activity} =
294 ActivityPub.create(%{
295 to: answer_data["to"],
296 actor: user,
297 context: object.data["context"],
298 object: answer_data,
299 additional: %{"cc" => answer_data["cc"]}
300 })
301
302 activity
303 end)
304
305 object = Object.get_cached_by_ap_id(object.data["id"])
306 {:ok, answer_activities, object}
307 end
308 end
309
310 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
311 do: {:error, dgettext("errors", "Poll's author can't vote")}
312
313 defp validate_not_author(_, _), do: :ok
314
315 defp validate_existing_votes(%{ap_id: ap_id}, object) do
316 if Utils.get_existing_votes(ap_id, object) == [] do
317 :ok
318 else
319 {:error, dgettext("errors", "Already voted")}
320 end
321 end
322
323 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
324 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
325
326 defp normalize_and_validate_choices(choices, object) do
327 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
328 {options, max_count} = get_options_and_max_count(object)
329 count = Enum.count(options)
330
331 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
332 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
333 {:ok, options, choices}
334 else
335 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
336 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
337 end
338 end
339
340 def public_announce?(_, %{visibility: visibility})
341 when visibility in ~w{public unlisted private direct},
342 do: visibility in ~w(public unlisted)
343
344 def public_announce?(object, _) do
345 Visibility.is_public?(object)
346 end
347
348 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
349
350 def get_visibility(%{visibility: visibility}, in_reply_to, _)
351 when visibility in ~w{public unlisted private direct},
352 do: {visibility, get_replied_to_visibility(in_reply_to)}
353
354 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
355 visibility = {:list, String.to_integer(list_id)}
356 {visibility, get_replied_to_visibility(in_reply_to)}
357 end
358
359 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
360 visibility = get_replied_to_visibility(in_reply_to)
361 {visibility, visibility}
362 end
363
364 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
365
366 def get_replied_to_visibility(nil), do: nil
367
368 def get_replied_to_visibility(activity) do
369 with %Object{} = object <- Object.normalize(activity) do
370 Visibility.get_visibility(object)
371 end
372 end
373
374 def check_expiry_date({:ok, nil} = res), do: res
375
376 def check_expiry_date({:ok, in_seconds}) do
377 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
378
379 if ActivityExpiration.expires_late_enough?(expiry) do
380 {:ok, expiry}
381 else
382 {:error, "Expiry date is too soon"}
383 end
384 end
385
386 def check_expiry_date(expiry_str) do
387 Ecto.Type.cast(:integer, expiry_str)
388 |> check_expiry_date()
389 end
390
391 def listen(user, data) do
392 visibility = Map.get(data, :visibility, "public")
393
394 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
395 listen_data <-
396 data
397 |> Map.take([:album, :artist, :title, :length])
398 |> Map.new(fn {key, value} -> {to_string(key), value} end)
399 |> Map.put("type", "Audio")
400 |> Map.put("to", to)
401 |> Map.put("cc", cc)
402 |> Map.put("actor", user.ap_id),
403 {:ok, activity} <-
404 ActivityPub.listen(%{
405 actor: user,
406 to: to,
407 object: listen_data,
408 context: Utils.generate_context_id(),
409 additional: %{"cc" => cc}
410 }) do
411 {:ok, activity}
412 end
413 end
414
415 def post(user, %{status: _} = data) do
416 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
417 draft.changes
418 |> ActivityPub.create(draft.preview?)
419 |> maybe_create_activity_expiration(draft.expires_at)
420 end
421 end
422
423 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
424 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
425 {:ok, activity}
426 end
427 end
428
429 defp maybe_create_activity_expiration(result, _), do: result
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) do
458 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
459 {:ok, activity}
460 else
461 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
462 end
463 end
464
465 def remove_mute(user, activity) do
466 ThreadMute.remove_mute(user.id, activity.data["context"])
467 {:ok, activity}
468 end
469
470 def thread_muted?(%{id: nil} = _user, _activity), do: false
471
472 def thread_muted?(user, activity) do
473 ThreadMute.exists?(user.id, activity.data["context"])
474 end
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