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