Undoing: Move undoing reactions to the pipeline everywhere.
[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)},
170 %Object{} = note <- Object.normalize(activity, false),
171 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
172 {:ok, undo, _} <- Builder.undo(user, like),
173 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
174 {:ok, activity}
175 else
176 {:find_activity, _} -> {:error, :not_found}
177 _ -> {:error, dgettext("errors", "Could not unfavorite")}
178 end
179 end
180
181 def react_with_emoji(id, user, emoji) do
182 with %Activity{} = activity <- Activity.get_by_id(id),
183 object <- Object.normalize(activity) do
184 ActivityPub.react_with_emoji(user, object, emoji)
185 else
186 _ ->
187 {:error, dgettext("errors", "Could not add reaction emoji")}
188 end
189 end
190
191 def unreact_with_emoji(id, user, emoji) do
192 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
193 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
194 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
195 {:ok, activity}
196 else
197 _ ->
198 {:error, dgettext("errors", "Could not remove reaction emoji")}
199 end
200 end
201
202 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
203 with :ok <- validate_not_author(object, user),
204 :ok <- validate_existing_votes(user, object),
205 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
206 answer_activities =
207 Enum.map(choices, fn index ->
208 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
209
210 {:ok, activity} =
211 ActivityPub.create(%{
212 to: answer_data["to"],
213 actor: user,
214 context: object.data["context"],
215 object: answer_data,
216 additional: %{"cc" => answer_data["cc"]}
217 })
218
219 activity
220 end)
221
222 object = Object.get_cached_by_ap_id(object.data["id"])
223 {:ok, answer_activities, object}
224 end
225 end
226
227 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
228 do: {:error, dgettext("errors", "Poll's author can't vote")}
229
230 defp validate_not_author(_, _), do: :ok
231
232 defp validate_existing_votes(%{ap_id: ap_id}, object) do
233 if Utils.get_existing_votes(ap_id, object) == [] do
234 :ok
235 else
236 {:error, dgettext("errors", "Already voted")}
237 end
238 end
239
240 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
241 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
242
243 defp normalize_and_validate_choices(choices, object) do
244 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
245 {options, max_count} = get_options_and_max_count(object)
246 count = Enum.count(options)
247
248 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
249 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
250 {:ok, options, choices}
251 else
252 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
253 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
254 end
255 end
256
257 def public_announce?(_, %{"visibility" => visibility})
258 when visibility in ~w{public unlisted private direct},
259 do: visibility in ~w(public unlisted)
260
261 def public_announce?(object, _) do
262 Visibility.is_public?(object)
263 end
264
265 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
266
267 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
268 when visibility in ~w{public unlisted private direct},
269 do: {visibility, get_replied_to_visibility(in_reply_to)}
270
271 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
272 visibility = {:list, String.to_integer(list_id)}
273 {visibility, get_replied_to_visibility(in_reply_to)}
274 end
275
276 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
277 visibility = get_replied_to_visibility(in_reply_to)
278 {visibility, visibility}
279 end
280
281 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
282
283 def get_replied_to_visibility(nil), do: nil
284
285 def get_replied_to_visibility(activity) do
286 with %Object{} = object <- Object.normalize(activity) do
287 Visibility.get_visibility(object)
288 end
289 end
290
291 def check_expiry_date({:ok, nil} = res), do: res
292
293 def check_expiry_date({:ok, in_seconds}) do
294 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
295
296 if ActivityExpiration.expires_late_enough?(expiry) do
297 {:ok, expiry}
298 else
299 {:error, "Expiry date is too soon"}
300 end
301 end
302
303 def check_expiry_date(expiry_str) do
304 Ecto.Type.cast(:integer, expiry_str)
305 |> check_expiry_date()
306 end
307
308 def listen(user, %{"title" => _} = data) do
309 with visibility <- data["visibility"] || "public",
310 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
311 listen_data <-
312 Map.take(data, ["album", "artist", "title", "length"])
313 |> Map.put("type", "Audio")
314 |> Map.put("to", to)
315 |> Map.put("cc", cc)
316 |> Map.put("actor", user.ap_id),
317 {:ok, activity} <-
318 ActivityPub.listen(%{
319 actor: user,
320 to: to,
321 object: listen_data,
322 context: Utils.generate_context_id(),
323 additional: %{"cc" => cc}
324 }) do
325 {:ok, activity}
326 end
327 end
328
329 def post(user, %{"status" => _} = data) do
330 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
331 draft.changes
332 |> ActivityPub.create(draft.preview?)
333 |> maybe_create_activity_expiration(draft.expires_at)
334 end
335 end
336
337 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
338 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
339 {:ok, activity}
340 end
341 end
342
343 defp maybe_create_activity_expiration(result, _), do: result
344
345 def pin(id, %{ap_id: user_ap_id} = user) do
346 with %Activity{
347 actor: ^user_ap_id,
348 data: %{"type" => "Create"},
349 object: %Object{data: %{"type" => object_type}}
350 } = activity <- Activity.get_by_id_with_object(id),
351 true <- object_type in ["Note", "Article", "Question"],
352 true <- Visibility.is_public?(activity),
353 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
354 {:ok, activity}
355 else
356 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
357 _ -> {:error, dgettext("errors", "Could not pin")}
358 end
359 end
360
361 def unpin(id, user) do
362 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
363 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
364 {:ok, activity}
365 else
366 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
367 _ -> {:error, dgettext("errors", "Could not unpin")}
368 end
369 end
370
371 def add_mute(user, activity) do
372 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
373 {:ok, activity}
374 else
375 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
376 end
377 end
378
379 def remove_mute(user, activity) do
380 ThreadMute.remove_mute(user.id, activity.data["context"])
381 {:ok, activity}
382 end
383
384 def thread_muted?(%{id: nil} = _user, _activity), do: false
385
386 def thread_muted?(user, activity) do
387 ThreadMute.exists?(user.id, activity.data["context"])
388 end
389
390 def report(user, data) do
391 with {:ok, account} <- get_reported_account(data.account_id),
392 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
393 {:ok, statuses} <- get_report_statuses(account, data) do
394 ActivityPub.flag(%{
395 context: Utils.generate_context_id(),
396 actor: user,
397 account: account,
398 statuses: statuses,
399 content: content_html,
400 forward: Map.get(data, :forward, false)
401 })
402 end
403 end
404
405 defp get_reported_account(account_id) do
406 case User.get_cached_by_id(account_id) do
407 %User{} = account -> {:ok, account}
408 _ -> {:error, dgettext("errors", "Account not found")}
409 end
410 end
411
412 def update_report_state(activity_ids, state) when is_list(activity_ids) do
413 case Utils.update_report_state(activity_ids, state) do
414 :ok -> {:ok, activity_ids}
415 _ -> {:error, dgettext("errors", "Could not update state")}
416 end
417 end
418
419 def update_report_state(activity_id, state) do
420 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
421 Utils.update_report_state(activity, state)
422 else
423 nil -> {:error, :not_found}
424 _ -> {:error, dgettext("errors", "Could not update state")}
425 end
426 end
427
428 def update_activity_scope(activity_id, opts \\ %{}) do
429 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
430 {:ok, activity} <- toggle_sensitive(activity, opts) do
431 set_visibility(activity, opts)
432 else
433 nil -> {:error, :not_found}
434 {:error, reason} -> {:error, reason}
435 end
436 end
437
438 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
439 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
440 end
441
442 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
443 when is_boolean(sensitive) do
444 new_data = Map.put(object.data, "sensitive", sensitive)
445
446 {:ok, object} =
447 object
448 |> Object.change(%{data: new_data})
449 |> Object.update_and_set_cache()
450
451 {:ok, Map.put(activity, :object, object)}
452 end
453
454 defp toggle_sensitive(activity, _), do: {:ok, activity}
455
456 defp set_visibility(activity, %{"visibility" => visibility}) do
457 Utils.update_activity_visibility(activity, visibility)
458 end
459
460 defp set_visibility(activity, _), do: {:ok, activity}
461
462 def hide_reblogs(%User{} = user, %User{} = target) do
463 UserRelationship.create_reblog_mute(user, target)
464 end
465
466 def show_reblogs(%User{} = user, %User{} = target) do
467 UserRelationship.delete_reblog_mute(user, target)
468 end
469 end