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