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