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