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