CommonAPI: DRY up a bit.
[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, "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, "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 # Updates the emojis for a user based on their profile
336 def update(user) do
337 emoji = emoji_from_profile(user)
338 source_data = Map.put(user.source_data, "tag", emoji)
339
340 user =
341 case User.update_source_data(user, source_data) do
342 {:ok, user} -> user
343 _ -> user
344 end
345
346 ActivityPub.update(%{
347 local: true,
348 to: [Pleroma.Constants.as_public(), user.follower_address],
349 cc: [],
350 actor: user.ap_id,
351 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
352 })
353 end
354
355 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
356 with %Activity{
357 actor: ^user_ap_id,
358 data: %{"type" => "Create"},
359 object: %Object{data: %{"type" => object_type}}
360 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
361 true <- object_type in ["Note", "Article", "Question"],
362 true <- Visibility.is_public?(activity),
363 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
364 {:ok, activity}
365 else
366 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
367 _ -> {:error, dgettext("errors", "Could not pin")}
368 end
369 end
370
371 def unpin(id_or_ap_id, user) do
372 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
373 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
374 {:ok, activity}
375 else
376 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
377 _ -> {:error, dgettext("errors", "Could not unpin")}
378 end
379 end
380
381 def add_mute(user, activity) do
382 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
383 {:ok, activity}
384 else
385 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
386 end
387 end
388
389 def remove_mute(user, activity) do
390 ThreadMute.remove_mute(user.id, activity.data["context"])
391 {:ok, activity}
392 end
393
394 def thread_muted?(%{id: nil} = _user, _activity), do: false
395
396 def thread_muted?(user, activity) do
397 ThreadMute.check_muted(user.id, activity.data["context"]) != []
398 end
399
400 def report(user, %{"account_id" => account_id} = data) do
401 with {:ok, account} <- get_reported_account(account_id),
402 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
403 {:ok, statuses} <- get_report_statuses(account, data) do
404 ActivityPub.flag(%{
405 context: Utils.generate_context_id(),
406 actor: user,
407 account: account,
408 statuses: statuses,
409 content: content_html,
410 forward: data["forward"] || false
411 })
412 end
413 end
414
415 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
416
417 defp get_reported_account(account_id) do
418 case User.get_cached_by_id(account_id) do
419 %User{} = account -> {:ok, account}
420 _ -> {:error, dgettext("errors", "Account not found")}
421 end
422 end
423
424 def update_report_state(activity_ids, state) when is_list(activity_ids) do
425 case Utils.update_report_state(activity_ids, state) do
426 :ok -> {:ok, activity_ids}
427 _ -> {:error, dgettext("errors", "Could not update state")}
428 end
429 end
430
431 def update_report_state(activity_id, state) do
432 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
433 Utils.update_report_state(activity, state)
434 else
435 nil -> {:error, :not_found}
436 _ -> {:error, dgettext("errors", "Could not update state")}
437 end
438 end
439
440 def update_activity_scope(activity_id, opts \\ %{}) do
441 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
442 {:ok, activity} <- toggle_sensitive(activity, opts) do
443 set_visibility(activity, opts)
444 else
445 nil -> {:error, :not_found}
446 {:error, reason} -> {:error, reason}
447 end
448 end
449
450 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
451 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
452 end
453
454 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
455 when is_boolean(sensitive) do
456 new_data = Map.put(object.data, "sensitive", sensitive)
457
458 {:ok, object} =
459 object
460 |> Object.change(%{data: new_data})
461 |> Object.update_and_set_cache()
462
463 {:ok, Map.put(activity, :object, object)}
464 end
465
466 defp toggle_sensitive(activity, _), do: {:ok, activity}
467
468 defp set_visibility(activity, %{"visibility" => visibility}) do
469 Utils.update_activity_visibility(activity, visibility)
470 end
471
472 defp set_visibility(activity, _), do: {:ok, activity}
473
474 def hide_reblogs(%User{} = user, %User{} = target) do
475 UserRelationship.create_reblog_mute(user, target)
476 end
477
478 def show_reblogs(%User{} = user, %User{} = target) do
479 UserRelationship.delete_reblog_mute(user, target)
480 end
481 end