Fix formatter warnings
[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.Utils
16 alias Pleroma.Web.ActivityPub.Visibility
17
18 import Pleroma.Web.Gettext
19 import Pleroma.Web.CommonAPI.Utils
20
21 require Pleroma.Constants
22
23 def follow(follower, followed) do
24 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
25
26 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
27 {:ok, activity} <- ActivityPub.follow(follower, followed),
28 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
29 {:ok, follower, followed, activity}
30 end
31 end
32
33 def unfollow(follower, unfollowed) do
34 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
35 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
36 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
37 {:ok, follower}
38 end
39 end
40
41 def accept_follow_request(follower, followed) do
42 with {:ok, follower} <- User.follow(follower, followed),
43 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
44 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
45 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"),
46 {:ok, _activity} <-
47 ActivityPub.accept(%{
48 to: [follower.ap_id],
49 actor: followed,
50 object: follow_activity.data["id"],
51 type: "Accept"
52 }) do
53 {:ok, follower}
54 end
55 end
56
57 def reject_follow_request(follower, followed) do
58 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
59 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
60 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
61 {:ok, _activity} <-
62 ActivityPub.reject(%{
63 to: [follower.ap_id],
64 actor: followed,
65 object: follow_activity.data["id"],
66 type: "Reject"
67 }) do
68 {:ok, follower}
69 end
70 end
71
72 def delete(activity_id, user) do
73 with {_, %Activity{data: %{"object" => _}} = activity} <-
74 {:find_activity, Activity.get_by_id_with_object(activity_id)},
75 %Object{} = object <- Object.normalize(activity),
76 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
77 {:ok, _} <- unpin(activity_id, user),
78 {:ok, delete} <- ActivityPub.delete(object) do
79 {:ok, delete}
80 else
81 {:find_activity, _} -> {:error, :not_found}
82 _ -> {:error, dgettext("errors", "Could not delete")}
83 end
84 end
85
86 def repeat(id_or_ap_id, user, params \\ %{}) do
87 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
88 object <- Object.normalize(activity),
89 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
90 public <- public_announce?(object, params) do
91 if announce_activity do
92 {:ok, announce_activity, object}
93 else
94 ActivityPub.announce(user, object, nil, true, public)
95 end
96 else
97 {:find_activity, _} -> {:error, :not_found}
98 _ -> {:error, dgettext("errors", "Could not repeat")}
99 end
100 end
101
102 def unrepeat(id_or_ap_id, user) do
103 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
104 object = Object.normalize(activity)
105 ActivityPub.unannounce(user, object)
106 else
107 {:find_activity, _} -> {:error, :not_found}
108 _ -> {:error, dgettext("errors", "Could not unrepeat")}
109 end
110 end
111
112 def favorite(id_or_ap_id, user) do
113 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
114 object <- Object.normalize(activity),
115 like_activity <- Utils.get_existing_like(user.ap_id, object) do
116 if like_activity do
117 {:ok, like_activity, object}
118 else
119 ActivityPub.like(user, object)
120 end
121 else
122 {:find_activity, _} -> {:error, :not_found}
123 _ -> {:error, dgettext("errors", "Could not favorite")}
124 end
125 end
126
127 def unfavorite(id_or_ap_id, user) do
128 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
129 object = Object.normalize(activity)
130 ActivityPub.unlike(user, object)
131 else
132 {:find_activity, _} -> {:error, :not_found}
133 _ -> {:error, dgettext("errors", "Could not unfavorite")}
134 end
135 end
136
137 def react_with_emoji(id, user, emoji) do
138 with %Activity{} = activity <- Activity.get_by_id(id),
139 object <- Object.normalize(activity) do
140 ActivityPub.react_with_emoji(user, object, emoji)
141 else
142 _ ->
143 {:error, dgettext("errors", "Could not add reaction emoji")}
144 end
145 end
146
147 def unreact_with_emoji(id, user, emoji) do
148 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
149 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
150 else
151 _ ->
152 {:error, dgettext("errors", "Could not remove reaction emoji")}
153 end
154 end
155
156 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
157 with :ok <- validate_not_author(object, user),
158 :ok <- validate_existing_votes(user, object),
159 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
160 answer_activities =
161 Enum.map(choices, fn index ->
162 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
163
164 {:ok, activity} =
165 ActivityPub.create(%{
166 to: answer_data["to"],
167 actor: user,
168 context: object.data["context"],
169 object: answer_data,
170 additional: %{"cc" => answer_data["cc"]}
171 })
172
173 activity
174 end)
175
176 object = Object.get_cached_by_ap_id(object.data["id"])
177 {:ok, answer_activities, object}
178 end
179 end
180
181 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
182 do: {:error, dgettext("errors", "Poll's author can't vote")}
183
184 defp validate_not_author(_, _), do: :ok
185
186 defp validate_existing_votes(%{ap_id: ap_id}, object) do
187 if Utils.get_existing_votes(ap_id, object) == [] do
188 :ok
189 else
190 {:error, dgettext("errors", "Already voted")}
191 end
192 end
193
194 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
195 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
196
197 defp normalize_and_validate_choices(choices, object) do
198 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
199 {options, max_count} = get_options_and_max_count(object)
200 count = Enum.count(options)
201
202 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
203 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
204 {:ok, options, choices}
205 else
206 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
207 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
208 end
209 end
210
211 def public_announce?(_, %{"visibility" => visibility})
212 when visibility in ~w{public unlisted private direct},
213 do: visibility in ~w(public unlisted)
214
215 def public_announce?(object, _) do
216 Visibility.is_public?(object)
217 end
218
219 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
220
221 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
222 when visibility in ~w{public unlisted private direct},
223 do: {visibility, get_replied_to_visibility(in_reply_to)}
224
225 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
226 visibility = {:list, String.to_integer(list_id)}
227 {visibility, get_replied_to_visibility(in_reply_to)}
228 end
229
230 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
231 visibility = get_replied_to_visibility(in_reply_to)
232 {visibility, visibility}
233 end
234
235 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
236
237 def get_replied_to_visibility(nil), do: nil
238
239 def get_replied_to_visibility(activity) do
240 with %Object{} = object <- Object.normalize(activity) do
241 Visibility.get_visibility(object)
242 end
243 end
244
245 def check_expiry_date({:ok, nil} = res), do: res
246
247 def check_expiry_date({:ok, in_seconds}) do
248 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
249
250 if ActivityExpiration.expires_late_enough?(expiry) do
251 {:ok, expiry}
252 else
253 {:error, "Expiry date is too soon"}
254 end
255 end
256
257 def check_expiry_date(expiry_str) do
258 Ecto.Type.cast(:integer, expiry_str)
259 |> check_expiry_date()
260 end
261
262 def listen(user, %{"title" => _} = data) do
263 with visibility <- data["visibility"] || "public",
264 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
265 listen_data <-
266 Map.take(data, ["album", "artist", "title", "length"])
267 |> Map.put("type", "Audio")
268 |> Map.put("to", to)
269 |> Map.put("cc", cc)
270 |> Map.put("actor", user.ap_id),
271 {:ok, activity} <-
272 ActivityPub.listen(%{
273 actor: user,
274 to: to,
275 object: listen_data,
276 context: Utils.generate_context_id(),
277 additional: %{"cc" => cc}
278 }) do
279 {:ok, activity}
280 end
281 end
282
283 def post(user, %{"status" => _} = data) do
284 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
285 ActivityPub.create(draft.changes, draft.preview?)
286 end
287 end
288
289 # Updates the emojis for a user based on their profile
290 def update(user) do
291 emoji = emoji_from_profile(user)
292 source_data = Map.put(user.source_data, "tag", emoji)
293
294 user =
295 case User.update_source_data(user, source_data) do
296 {:ok, user} -> user
297 _ -> user
298 end
299
300 ActivityPub.update(%{
301 local: true,
302 to: [Pleroma.Constants.as_public(), user.follower_address],
303 cc: [],
304 actor: user.ap_id,
305 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
306 })
307 end
308
309 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
310 with %Activity{
311 actor: ^user_ap_id,
312 data: %{"type" => "Create"},
313 object: %Object{data: %{"type" => object_type}}
314 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
315 true <- object_type in ["Note", "Article", "Question"],
316 true <- Visibility.is_public?(activity),
317 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
318 {:ok, activity}
319 else
320 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
321 _ -> {:error, dgettext("errors", "Could not pin")}
322 end
323 end
324
325 def unpin(id_or_ap_id, user) do
326 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
327 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
328 {:ok, activity}
329 else
330 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
331 _ -> {:error, dgettext("errors", "Could not unpin")}
332 end
333 end
334
335 def add_mute(user, activity) do
336 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
337 {:ok, activity}
338 else
339 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
340 end
341 end
342
343 def remove_mute(user, activity) do
344 ThreadMute.remove_mute(user.id, activity.data["context"])
345 {:ok, activity}
346 end
347
348 def thread_muted?(%{id: nil} = _user, _activity), do: false
349
350 def thread_muted?(user, activity) do
351 ThreadMute.check_muted(user.id, activity.data["context"]) != []
352 end
353
354 def report(user, %{"account_id" => account_id} = data) do
355 with {:ok, account} <- get_reported_account(account_id),
356 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
357 {:ok, statuses} <- get_report_statuses(account, data) do
358 ActivityPub.flag(%{
359 context: Utils.generate_context_id(),
360 actor: user,
361 account: account,
362 statuses: statuses,
363 content: content_html,
364 forward: data["forward"] || false
365 })
366 end
367 end
368
369 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
370
371 defp get_reported_account(account_id) do
372 case User.get_cached_by_id(account_id) do
373 %User{} = account -> {:ok, account}
374 _ -> {:error, dgettext("errors", "Account not found")}
375 end
376 end
377
378 def update_report_state(activity_ids, state) when is_list(activity_ids) do
379 case Utils.update_report_state(activity_ids, state) do
380 :ok -> {:ok, activity_ids}
381 _ -> {:error, dgettext("errors", "Could not update state")}
382 end
383 end
384
385 def update_report_state(activity_id, state) do
386 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
387 Utils.update_report_state(activity, state)
388 else
389 nil -> {:error, :not_found}
390 _ -> {:error, dgettext("errors", "Could not update state")}
391 end
392 end
393
394 def update_activity_scope(activity_id, opts \\ %{}) do
395 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
396 {:ok, activity} <- toggle_sensitive(activity, opts) do
397 set_visibility(activity, opts)
398 else
399 nil -> {:error, :not_found}
400 {:error, reason} -> {:error, reason}
401 end
402 end
403
404 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
405 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
406 end
407
408 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
409 when is_boolean(sensitive) do
410 new_data = Map.put(object.data, "sensitive", sensitive)
411
412 {:ok, object} =
413 object
414 |> Object.change(%{data: new_data})
415 |> Object.update_and_set_cache()
416
417 {:ok, Map.put(activity, :object, object)}
418 end
419
420 defp toggle_sensitive(activity, _), do: {:ok, activity}
421
422 defp set_visibility(activity, %{"visibility" => visibility}) do
423 Utils.update_activity_visibility(activity, visibility)
424 end
425
426 defp set_visibility(activity, _), do: {:ok, activity}
427
428 def hide_reblogs(%User{} = user, %User{} = target) do
429 UserRelationship.create_reblog_mute(user, target)
430 end
431
432 def show_reblogs(%User{} = user, %User{} = target) do
433 UserRelationship.delete_reblog_mute(user, target)
434 end
435 end