Allow reacting with remote emoji when they exist on the post (#200)
[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 _ ->
213 {:error, dgettext("errors", "Could not add reaction emoji")}
214 end
215 end
216
217 def unreact_with_emoji(id, user, emoji) do
218 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
219 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
220 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
221 {:ok, activity}
222 else
223 _ ->
224 {:error, dgettext("errors", "Could not remove reaction emoji")}
225 end
226 end
227
228 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
229 with :ok <- validate_not_author(object, user),
230 :ok <- validate_existing_votes(user, object),
231 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
232 answer_activities =
233 Enum.map(choices, fn index ->
234 {:ok, answer_object, _meta} =
235 Builder.answer(user, object, Enum.at(options, index)["name"])
236
237 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
238
239 {:ok, activity, _meta} =
240 activity_data
241 |> Map.put("cc", answer_object["cc"])
242 |> Map.put("context", answer_object["context"])
243 |> Pipeline.common_pipeline(local: true)
244
245 # TODO: Do preload of Pleroma.Object in Pipeline
246 Activity.normalize(activity.data)
247 end)
248
249 object = Object.get_cached_by_ap_id(object.data["id"])
250 {:ok, answer_activities, object}
251 end
252 end
253
254 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
255 do: {:error, dgettext("errors", "Poll's author can't vote")}
256
257 defp validate_not_author(_, _), do: :ok
258
259 defp validate_existing_votes(%{ap_id: ap_id}, object) do
260 if Utils.get_existing_votes(ap_id, object) == [] do
261 :ok
262 else
263 {:error, dgettext("errors", "Already voted")}
264 end
265 end
266
267 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
268 when is_list(any_of) and any_of != [],
269 do: {any_of, Enum.count(any_of)}
270
271 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
272 when is_list(one_of) and one_of != [],
273 do: {one_of, 1}
274
275 defp normalize_and_validate_choices(choices, object) do
276 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
277 {options, max_count} = get_options_and_max_count(object)
278 count = Enum.count(options)
279
280 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
281 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
282 {:ok, options, choices}
283 else
284 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
285 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
286 end
287 end
288
289 def public_announce?(_, %{visibility: visibility})
290 when visibility in ~w{public unlisted private direct},
291 do: visibility in ~w(public unlisted)
292
293 def public_announce?(object, _) do
294 Visibility.is_public?(object)
295 end
296
297 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
298
299 def get_visibility(%{visibility: visibility}, in_reply_to, _)
300 when visibility in ~w{public local unlisted private direct},
301 do: {visibility, get_replied_to_visibility(in_reply_to)}
302
303 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
304 visibility = {:list, String.to_integer(list_id)}
305 {visibility, get_replied_to_visibility(in_reply_to)}
306 end
307
308 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
309 visibility = get_replied_to_visibility(in_reply_to)
310 {visibility, visibility}
311 end
312
313 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
314
315 def get_replied_to_visibility(nil), do: nil
316
317 def get_replied_to_visibility(activity) do
318 with %Object{} = object <- Object.normalize(activity, fetch: false) do
319 Visibility.get_visibility(object)
320 end
321 end
322
323 def get_quoted_visibility(nil), do: nil
324
325 def get_quoted_visibility(activity), do: get_replied_to_visibility(activity)
326
327 def check_expiry_date({:ok, nil} = res), do: res
328
329 def check_expiry_date({:ok, in_seconds}) do
330 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
331
332 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
333 {:ok, expiry}
334 else
335 {:error, "Expiry date is too soon"}
336 end
337 end
338
339 def check_expiry_date(expiry_str) do
340 Ecto.Type.cast(:integer, expiry_str)
341 |> check_expiry_date()
342 end
343
344 def post(user, %{status: _} = data) do
345 with {:ok, draft} <- ActivityDraft.create(user, data) do
346 ActivityPub.create(draft.changes, draft.preview?)
347 end
348 end
349
350 @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
351 def pin(id, %User{} = user) do
352 with %Activity{} = activity <- create_activity_by_id(id),
353 true <- activity_belongs_to_actor(activity, user.ap_id),
354 true <- object_type_is_allowed_for_pin(activity.object),
355 true <- activity_is_public(activity),
356 {:ok, pin_data, _} <- Builder.pin(user, activity.object),
357 {:ok, _pin, _} <-
358 Pipeline.common_pipeline(pin_data,
359 local: true,
360 activity_id: id
361 ) do
362 {:ok, activity}
363 else
364 {:error, {:side_effects, error}} -> error
365 error -> error
366 end
367 end
368
369 defp create_activity_by_id(id) do
370 with nil <- Activity.create_by_id_with_object(id) do
371 {:error, :not_found}
372 end
373 end
374
375 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
376 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
377
378 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
379 with false <- type in ["Note", "Article", "Question"] do
380 {:error, :not_allowed}
381 end
382 end
383
384 defp activity_is_public(activity) do
385 with false <- Visibility.is_public?(activity) do
386 {:error, :visibility_error}
387 end
388 end
389
390 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
391 def unpin(id, user) do
392 with %Activity{} = activity <- create_activity_by_id(id),
393 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
394 {:ok, _unpin, _} <-
395 Pipeline.common_pipeline(unpin_data,
396 local: true,
397 activity_id: activity.id,
398 expires_at: activity.data["expires_at"],
399 featured_address: user.featured_address
400 ) do
401 {:ok, activity}
402 end
403 end
404
405 def add_mute(user, activity, params \\ %{}) do
406 expires_in = Map.get(params, :expires_in, 0)
407
408 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
409 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
410 if expires_in > 0 do
411 Pleroma.Workers.MuteExpireWorker.enqueue(
412 "unmute_conversation",
413 %{"user_id" => user.id, "activity_id" => activity.id},
414 schedule_in: expires_in
415 )
416 end
417
418 {:ok, activity}
419 else
420 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
421 end
422 end
423
424 def remove_mute(%User{} = user, %Activity{} = activity) do
425 ThreadMute.remove_mute(user.id, activity.data["context"])
426 {:ok, activity}
427 end
428
429 def remove_mute(user_id, activity_id) do
430 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
431 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
432 remove_mute(user, activity)
433 else
434 {what, result} = error ->
435 Logger.warn(
436 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
437 )
438
439 {:error, error}
440 end
441 end
442
443 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
444 when is_binary(context) do
445 ThreadMute.exists?(user_id, context)
446 end
447
448 def thread_muted?(_, _), do: false
449
450 def report(user, data) do
451 with {:ok, account} <- get_reported_account(data.account_id),
452 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
453 {:ok, statuses} <- get_report_statuses(account, data) do
454 ActivityPub.flag(%{
455 context: Utils.generate_context_id(),
456 actor: user,
457 account: account,
458 statuses: statuses,
459 content: content_html,
460 forward: Map.get(data, :forward, false)
461 })
462 end
463 end
464
465 defp get_reported_account(account_id) do
466 case User.get_cached_by_id(account_id) do
467 %User{} = account -> {:ok, account}
468 _ -> {:error, dgettext("errors", "Account not found")}
469 end
470 end
471
472 def update_report_state(activity_ids, state) when is_list(activity_ids) do
473 case Utils.update_report_state(activity_ids, state) do
474 :ok -> {:ok, activity_ids}
475 _ -> {:error, dgettext("errors", "Could not update state")}
476 end
477 end
478
479 def update_report_state(activity_id, state) do
480 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
481 Utils.update_report_state(activity, state)
482 else
483 nil -> {:error, :not_found}
484 _ -> {:error, dgettext("errors", "Could not update state")}
485 end
486 end
487
488 def update_activity_scope(activity_id, opts \\ %{}) do
489 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
490 {:ok, activity} <- toggle_sensitive(activity, opts) do
491 set_visibility(activity, opts)
492 else
493 nil -> {:error, :not_found}
494 {:error, reason} -> {:error, reason}
495 end
496 end
497
498 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
499 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
500 end
501
502 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
503 when is_boolean(sensitive) do
504 new_data = Map.put(object.data, "sensitive", sensitive)
505
506 {:ok, object} =
507 object
508 |> Object.change(%{data: new_data})
509 |> Object.update_and_set_cache()
510
511 {:ok, Map.put(activity, :object, object)}
512 end
513
514 defp toggle_sensitive(activity, _), do: {:ok, activity}
515
516 defp set_visibility(activity, %{visibility: visibility}) do
517 Utils.update_activity_visibility(activity, visibility)
518 end
519
520 defp set_visibility(activity, _), do: {:ok, activity}
521
522 def hide_reblogs(%User{} = user, %User{} = target) do
523 UserRelationship.create_reblog_mute(user, target)
524 end
525
526 def show_reblogs(%User{} = user, %User{} = target) do
527 UserRelationship.delete_reblog_mute(user, target)
528 end
529
530 def get_user(ap_id, fake_record_fallback \\ true) do
531 cond do
532 user = User.get_cached_by_ap_id(ap_id) ->
533 user
534
535 user = User.get_by_guessed_nickname(ap_id) ->
536 user
537
538 fake_record_fallback ->
539 # TODO: refactor (fake records is never a good idea)
540 User.error_user(ap_id)
541
542 true ->
543 nil
544 end
545 end
546 end