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