Merge branch 'proper_error_messages' into 'develop'
[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 draft.changes
286 |> ActivityPub.create(draft.preview?)
287 |> maybe_create_activity_expiration(draft.expires_at)
288 end
289 end
290
291 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
292 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
293 {:ok, activity}
294 end
295 end
296
297 defp maybe_create_activity_expiration(result, _), do: result
298
299 # Updates the emojis for a user based on their profile
300 def update(user) do
301 emoji = emoji_from_profile(user)
302 source_data = Map.put(user.source_data, "tag", emoji)
303
304 user =
305 case User.update_source_data(user, source_data) do
306 {:ok, user} -> user
307 _ -> user
308 end
309
310 ActivityPub.update(%{
311 local: true,
312 to: [Pleroma.Constants.as_public(), user.follower_address],
313 cc: [],
314 actor: user.ap_id,
315 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
316 })
317 end
318
319 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
320 with %Activity{
321 actor: ^user_ap_id,
322 data: %{"type" => "Create"},
323 object: %Object{data: %{"type" => object_type}}
324 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
325 true <- object_type in ["Note", "Article", "Question"],
326 true <- Visibility.is_public?(activity),
327 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
328 {:ok, activity}
329 else
330 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
331 _ -> {:error, dgettext("errors", "Could not pin")}
332 end
333 end
334
335 def unpin(id_or_ap_id, user) do
336 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
337 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
338 {:ok, activity}
339 else
340 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
341 _ -> {:error, dgettext("errors", "Could not unpin")}
342 end
343 end
344
345 def add_mute(user, activity) do
346 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
347 {:ok, activity}
348 else
349 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
350 end
351 end
352
353 def remove_mute(user, activity) do
354 ThreadMute.remove_mute(user.id, activity.data["context"])
355 {:ok, activity}
356 end
357
358 def thread_muted?(%{id: nil} = _user, _activity), do: false
359
360 def thread_muted?(user, activity) do
361 ThreadMute.check_muted(user.id, activity.data["context"]) != []
362 end
363
364 def report(user, %{"account_id" => account_id} = data) do
365 with {:ok, account} <- get_reported_account(account_id),
366 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
367 {:ok, statuses} <- get_report_statuses(account, data) do
368 ActivityPub.flag(%{
369 context: Utils.generate_context_id(),
370 actor: user,
371 account: account,
372 statuses: statuses,
373 content: content_html,
374 forward: data["forward"] || false
375 })
376 end
377 end
378
379 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
380
381 defp get_reported_account(account_id) do
382 case User.get_cached_by_id(account_id) do
383 %User{} = account -> {:ok, account}
384 _ -> {:error, dgettext("errors", "Account not found")}
385 end
386 end
387
388 def update_report_state(activity_ids, state) when is_list(activity_ids) do
389 case Utils.update_report_state(activity_ids, state) do
390 :ok -> {:ok, activity_ids}
391 _ -> {:error, dgettext("errors", "Could not update state")}
392 end
393 end
394
395 def update_report_state(activity_id, state) do
396 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
397 Utils.update_report_state(activity, state)
398 else
399 nil -> {:error, :not_found}
400 _ -> {:error, dgettext("errors", "Could not update state")}
401 end
402 end
403
404 def update_activity_scope(activity_id, opts \\ %{}) do
405 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
406 {:ok, activity} <- toggle_sensitive(activity, opts) do
407 set_visibility(activity, opts)
408 else
409 nil -> {:error, :not_found}
410 {:error, reason} -> {:error, reason}
411 end
412 end
413
414 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
415 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
416 end
417
418 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
419 when is_boolean(sensitive) do
420 new_data = Map.put(object.data, "sensitive", sensitive)
421
422 {:ok, object} =
423 object
424 |> Object.change(%{data: new_data})
425 |> Object.update_and_set_cache()
426
427 {:ok, Map.put(activity, :object, object)}
428 end
429
430 defp toggle_sensitive(activity, _), do: {:ok, activity}
431
432 defp set_visibility(activity, %{"visibility" => visibility}) do
433 Utils.update_activity_visibility(activity, visibility)
434 end
435
436 defp set_visibility(activity, _), do: {:ok, activity}
437
438 def hide_reblogs(%User{} = user, %User{} = target) do
439 UserRelationship.create_reblog_mute(user, target)
440 end
441
442 def show_reblogs(%User{} = user, %User{} = target) do
443 UserRelationship.delete_reblog_mute(user, target)
444 end
445 end