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