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