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