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