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