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