ActivityPub: Remove `react_with_emoji`.
[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.Notification
11 alias Pleroma.Object
12 alias Pleroma.ThreadMute
13 alias Pleroma.User
14 alias Pleroma.UserRelationship
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.Builder
17 alias Pleroma.Web.ActivityPub.Pipeline
18 alias Pleroma.Web.ActivityPub.Utils
19 alias Pleroma.Web.ActivityPub.Visibility
20
21 import Pleroma.Web.Gettext
22 import Pleroma.Web.CommonAPI.Utils
23
24 require Pleroma.Constants
25 require Logger
26
27 def follow(follower, followed) do
28 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
29
30 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
31 {:ok, activity} <- ActivityPub.follow(follower, followed),
32 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
33 {:ok, follower, followed, activity}
34 end
35 end
36
37 def unfollow(follower, unfollowed) do
38 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
39 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
40 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
41 {:ok, follower}
42 end
43 end
44
45 def accept_follow_request(follower, followed) do
46 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
47 {:ok, follower} <- User.follow(follower, followed),
48 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
49 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
50 {:ok, _activity} <-
51 ActivityPub.accept(%{
52 to: [follower.ap_id],
53 actor: followed,
54 object: follow_activity.data["id"],
55 type: "Accept"
56 }) do
57 {:ok, follower}
58 end
59 end
60
61 def reject_follow_request(follower, followed) do
62 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
63 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
64 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
65 {:ok, _notifications} <- Notification.dismiss(follow_activity),
66 {:ok, _activity} <-
67 ActivityPub.reject(%{
68 to: [follower.ap_id],
69 actor: followed,
70 object: follow_activity.data["id"],
71 type: "Reject"
72 }) do
73 {:ok, follower}
74 end
75 end
76
77 def delete(activity_id, user) do
78 with {_, %Activity{data: %{"object" => _}} = activity} <-
79 {:find_activity, Activity.get_by_id_with_object(activity_id)},
80 %Object{} = object <- Object.normalize(activity),
81 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
82 {:ok, _} <- unpin(activity_id, user),
83 {:ok, delete} <- ActivityPub.delete(object) do
84 {:ok, delete}
85 else
86 {:find_activity, _} -> {:error, :not_found}
87 _ -> {:error, dgettext("errors", "Could not delete")}
88 end
89 end
90
91 def repeat(id, user, params \\ %{}) do
92 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
93 {:find_activity, Activity.get_by_id(id)},
94 object <- Object.normalize(activity),
95 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
96 public <- public_announce?(object, params) do
97 if announce_activity do
98 {:ok, announce_activity, object}
99 else
100 ActivityPub.announce(user, object, nil, true, public)
101 end
102 else
103 {:find_activity, _} -> {:error, :not_found}
104 _ -> {:error, dgettext("errors", "Could not repeat")}
105 end
106 end
107
108 def unrepeat(id, user) do
109 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
110 {:find_activity, Activity.get_by_id(id)} do
111 object = Object.normalize(activity)
112 ActivityPub.unannounce(user, object)
113 else
114 {:find_activity, _} -> {:error, :not_found}
115 _ -> {:error, dgettext("errors", "Could not unrepeat")}
116 end
117 end
118
119 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
120 def favorite(%User{} = user, id) do
121 case favorite_helper(user, id) do
122 {:ok, _} = res ->
123 res
124
125 {:error, :not_found} = res ->
126 res
127
128 {:error, e} ->
129 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
130 {:error, dgettext("errors", "Could not favorite")}
131 end
132 end
133
134 def favorite_helper(user, id) do
135 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
136 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
137 {_, {:ok, %Activity{} = activity, _meta}} <-
138 {:common_pipeline,
139 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
140 {:ok, activity}
141 else
142 {:find_object, _} ->
143 {:error, :not_found}
144
145 {:common_pipeline,
146 {
147 :error,
148 {
149 :validate_object,
150 {
151 :error,
152 changeset
153 }
154 }
155 }} = e ->
156 if {:object, {"already liked by this actor", []}} in changeset.errors do
157 {:ok, :already_liked}
158 else
159 {:error, e}
160 end
161
162 e ->
163 {:error, e}
164 end
165 end
166
167 def unfavorite(id, user) do
168 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
169 {:find_activity, Activity.get_by_id(id)} do
170 object = Object.normalize(activity)
171 ActivityPub.unlike(user, object)
172 else
173 {:find_activity, _} -> {:error, :not_found}
174 _ -> {:error, dgettext("errors", "Could not unfavorite")}
175 end
176 end
177
178 def react_with_emoji(id, user, emoji) do
179 with %Activity{} = activity <- Activity.get_by_id(id),
180 object <- Object.normalize(activity),
181 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
182 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
183 {:ok, activity}
184 else
185 _ ->
186 {:error, dgettext("errors", "Could not add reaction emoji")}
187 end
188 end
189
190 def unreact_with_emoji(id, user, emoji) do
191 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
192 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
193 else
194 _ ->
195 {:error, dgettext("errors", "Could not remove reaction emoji")}
196 end
197 end
198
199 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
200 with :ok <- validate_not_author(object, user),
201 :ok <- validate_existing_votes(user, object),
202 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
203 answer_activities =
204 Enum.map(choices, fn index ->
205 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
206
207 {:ok, activity} =
208 ActivityPub.create(%{
209 to: answer_data["to"],
210 actor: user,
211 context: object.data["context"],
212 object: answer_data,
213 additional: %{"cc" => answer_data["cc"]}
214 })
215
216 activity
217 end)
218
219 object = Object.get_cached_by_ap_id(object.data["id"])
220 {:ok, answer_activities, object}
221 end
222 end
223
224 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
225 do: {:error, dgettext("errors", "Poll's author can't vote")}
226
227 defp validate_not_author(_, _), do: :ok
228
229 defp validate_existing_votes(%{ap_id: ap_id}, object) do
230 if Utils.get_existing_votes(ap_id, object) == [] do
231 :ok
232 else
233 {:error, dgettext("errors", "Already voted")}
234 end
235 end
236
237 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
238 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
239
240 defp normalize_and_validate_choices(choices, object) do
241 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
242 {options, max_count} = get_options_and_max_count(object)
243 count = Enum.count(options)
244
245 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
246 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
247 {:ok, options, choices}
248 else
249 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
250 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
251 end
252 end
253
254 def public_announce?(_, %{"visibility" => visibility})
255 when visibility in ~w{public unlisted private direct},
256 do: visibility in ~w(public unlisted)
257
258 def public_announce?(object, _) do
259 Visibility.is_public?(object)
260 end
261
262 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
263
264 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
265 when visibility in ~w{public unlisted private direct},
266 do: {visibility, get_replied_to_visibility(in_reply_to)}
267
268 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
269 visibility = {:list, String.to_integer(list_id)}
270 {visibility, get_replied_to_visibility(in_reply_to)}
271 end
272
273 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
274 visibility = get_replied_to_visibility(in_reply_to)
275 {visibility, visibility}
276 end
277
278 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
279
280 def get_replied_to_visibility(nil), do: nil
281
282 def get_replied_to_visibility(activity) do
283 with %Object{} = object <- Object.normalize(activity) do
284 Visibility.get_visibility(object)
285 end
286 end
287
288 def check_expiry_date({:ok, nil} = res), do: res
289
290 def check_expiry_date({:ok, in_seconds}) do
291 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
292
293 if ActivityExpiration.expires_late_enough?(expiry) do
294 {:ok, expiry}
295 else
296 {:error, "Expiry date is too soon"}
297 end
298 end
299
300 def check_expiry_date(expiry_str) do
301 Ecto.Type.cast(:integer, expiry_str)
302 |> check_expiry_date()
303 end
304
305 def listen(user, %{"title" => _} = data) do
306 with visibility <- data["visibility"] || "public",
307 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
308 listen_data <-
309 Map.take(data, ["album", "artist", "title", "length"])
310 |> Map.put("type", "Audio")
311 |> Map.put("to", to)
312 |> Map.put("cc", cc)
313 |> Map.put("actor", user.ap_id),
314 {:ok, activity} <-
315 ActivityPub.listen(%{
316 actor: user,
317 to: to,
318 object: listen_data,
319 context: Utils.generate_context_id(),
320 additional: %{"cc" => cc}
321 }) do
322 {:ok, activity}
323 end
324 end
325
326 def post(user, %{"status" => _} = data) do
327 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
328 draft.changes
329 |> ActivityPub.create(draft.preview?)
330 |> maybe_create_activity_expiration(draft.expires_at)
331 end
332 end
333
334 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
335 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
336 {:ok, activity}
337 end
338 end
339
340 defp maybe_create_activity_expiration(result, _), do: result
341
342 def pin(id, %{ap_id: user_ap_id} = user) do
343 with %Activity{
344 actor: ^user_ap_id,
345 data: %{"type" => "Create"},
346 object: %Object{data: %{"type" => object_type}}
347 } = activity <- Activity.get_by_id_with_object(id),
348 true <- object_type in ["Note", "Article", "Question"],
349 true <- Visibility.is_public?(activity),
350 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
351 {:ok, activity}
352 else
353 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
354 _ -> {:error, dgettext("errors", "Could not pin")}
355 end
356 end
357
358 def unpin(id, user) do
359 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
360 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
361 {:ok, activity}
362 else
363 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
364 _ -> {:error, dgettext("errors", "Could not unpin")}
365 end
366 end
367
368 def add_mute(user, activity) do
369 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
370 {:ok, activity}
371 else
372 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
373 end
374 end
375
376 def remove_mute(user, activity) do
377 ThreadMute.remove_mute(user.id, activity.data["context"])
378 {:ok, activity}
379 end
380
381 def thread_muted?(%{id: nil} = _user, _activity), do: false
382
383 def thread_muted?(user, activity) do
384 ThreadMute.exists?(user.id, activity.data["context"])
385 end
386
387 def report(user, data) do
388 with {:ok, account} <- get_reported_account(data.account_id),
389 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
390 {:ok, statuses} <- get_report_statuses(account, data) do
391 ActivityPub.flag(%{
392 context: Utils.generate_context_id(),
393 actor: user,
394 account: account,
395 statuses: statuses,
396 content: content_html,
397 forward: Map.get(data, :forward, false)
398 })
399 end
400 end
401
402 defp get_reported_account(account_id) do
403 case User.get_cached_by_id(account_id) do
404 %User{} = account -> {:ok, account}
405 _ -> {:error, dgettext("errors", "Account not found")}
406 end
407 end
408
409 def update_report_state(activity_ids, state) when is_list(activity_ids) do
410 case Utils.update_report_state(activity_ids, state) do
411 :ok -> {:ok, activity_ids}
412 _ -> {:error, dgettext("errors", "Could not update state")}
413 end
414 end
415
416 def update_report_state(activity_id, state) do
417 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
418 Utils.update_report_state(activity, state)
419 else
420 nil -> {:error, :not_found}
421 _ -> {:error, dgettext("errors", "Could not update state")}
422 end
423 end
424
425 def update_activity_scope(activity_id, opts \\ %{}) do
426 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
427 {:ok, activity} <- toggle_sensitive(activity, opts) do
428 set_visibility(activity, opts)
429 else
430 nil -> {:error, :not_found}
431 {:error, reason} -> {:error, reason}
432 end
433 end
434
435 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
436 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
437 end
438
439 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
440 when is_boolean(sensitive) do
441 new_data = Map.put(object.data, "sensitive", sensitive)
442
443 {:ok, object} =
444 object
445 |> Object.change(%{data: new_data})
446 |> Object.update_and_set_cache()
447
448 {:ok, Map.put(activity, :object, object)}
449 end
450
451 defp toggle_sensitive(activity, _), do: {:ok, activity}
452
453 defp set_visibility(activity, %{"visibility" => visibility}) do
454 Utils.update_activity_visibility(activity, visibility)
455 end
456
457 defp set_visibility(activity, _), do: {:ok, activity}
458
459 def hide_reblogs(%User{} = user, %User{} = target) do
460 UserRelationship.create_reblog_mute(user, target)
461 end
462
463 def show_reblogs(%User{} = user, %User{} = target) do
464 UserRelationship.delete_reblog_mute(user, target)
465 end
466 end