4df37b69561b0e1a3c91edeac62e08e36d019478
[akkoma] / lib / pleroma / web / 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.Conversation.Participation
8 alias Pleroma.Formatter
9 alias Pleroma.Object
10 alias Pleroma.ThreadMute
11 alias Pleroma.User
12 alias Pleroma.UserRelationship
13 alias Pleroma.Web.ActivityPub.ActivityPub
14 alias Pleroma.Web.ActivityPub.Builder
15 alias Pleroma.Web.ActivityPub.Pipeline
16 alias Pleroma.Web.ActivityPub.Utils
17 alias Pleroma.Web.ActivityPub.Visibility
18 alias Pleroma.Web.CommonAPI.ActivityDraft
19
20 import Pleroma.Web.Gettext
21 import Pleroma.Web.CommonAPI.Utils
22
23 require Pleroma.Constants
24 require Logger
25
26 def block(blocker, blocked) do
27 with {:ok, block_data, _} <- Builder.block(blocker, blocked),
28 {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
29 {:ok, block}
30 end
31 end
32
33 def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
34 with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
35 :ok <- validate_chat_content_length(content, !!maybe_attachment),
36 {_, {:ok, chat_message_data, _meta}} <-
37 {:build_object,
38 Builder.chat_message(
39 user,
40 recipient.ap_id,
41 content |> format_chat_content,
42 attachment: maybe_attachment
43 )},
44 {_, {:ok, create_activity_data, _meta}} <-
45 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
46 {_, {:ok, %Activity{} = activity, _meta}} <-
47 {:common_pipeline,
48 Pipeline.common_pipeline(create_activity_data,
49 local: true,
50 idempotency_key: opts[:idempotency_key]
51 )} do
52 {:ok, activity}
53 else
54 {:common_pipeline, {:reject, _} = e} -> e
55 e -> e
56 end
57 end
58
59 defp format_chat_content(nil), do: nil
60
61 defp format_chat_content(content) do
62 {text, _, _} =
63 content
64 |> Formatter.html_escape("text/plain")
65 |> Formatter.linkify()
66 |> (fn {text, mentions, tags} ->
67 {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
68 end).()
69
70 text
71 end
72
73 defp validate_chat_content_length(_, true), do: :ok
74 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
75
76 defp validate_chat_content_length(content, _) do
77 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
78 :ok
79 else
80 {:error, :content_too_long}
81 end
82 end
83
84 def unblock(blocker, blocked) do
85 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
86 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
87 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
88 {:ok, unblock}
89 else
90 {:fetch_block, nil} ->
91 if User.blocks?(blocker, blocked) do
92 User.unblock(blocker, blocked)
93 {:ok, :no_activity}
94 else
95 {:error, :not_blocking}
96 end
97
98 e ->
99 e
100 end
101 end
102
103 def follow(follower, followed) do
104 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
105
106 with {:ok, follow_data, _} <- Builder.follow(follower, followed),
107 {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
108 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
109 if activity.data["state"] == "reject" do
110 {:error, :rejected}
111 else
112 {:ok, follower, followed, activity}
113 end
114 end
115 end
116
117 def unfollow(follower, unfollowed) do
118 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
119 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
120 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
121 {:ok, follower}
122 end
123 end
124
125 def accept_follow_request(follower, followed) do
126 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
127 {:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
128 {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do
129 {:ok, follower}
130 end
131 end
132
133 def reject_follow_request(follower, followed) do
134 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
135 {:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
136 {:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do
137 {:ok, follower}
138 end
139 end
140
141 def delete(activity_id, user) do
142 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
143 {:find_activity, Activity.get_by_id(activity_id)},
144 {_, %Object{} = object, _} <-
145 {:find_object, Object.normalize(activity, false), activity},
146 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
147 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
148 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
149 {:ok, delete}
150 else
151 {:find_activity, _} ->
152 {:error, :not_found}
153
154 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
155 # We have the create activity, but not the object, it was probably pruned.
156 # Insert a tombstone and try again
157 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
158 {:ok, _tombstone} <- Object.create(tombstone_data) do
159 delete(activity_id, user)
160 else
161 _ ->
162 Logger.error(
163 "Could not insert tombstone for missing object on deletion. Object is #{object}."
164 )
165
166 {:error, dgettext("errors", "Could not delete")}
167 end
168
169 _ ->
170 {:error, dgettext("errors", "Could not delete")}
171 end
172 end
173
174 def repeat(id, user, params \\ %{}) do
175 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
176 object = %Object{} <- Object.normalize(activity, false),
177 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
178 public = public_announce?(object, params),
179 {:ok, announce, _} <- Builder.announce(user, object, public: public),
180 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
181 {:ok, activity}
182 else
183 {:existing_announce, %Activity{} = announce} ->
184 {:ok, announce}
185
186 _ ->
187 {:error, :not_found}
188 end
189 end
190
191 def unrepeat(id, user) do
192 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
193 {:find_activity, Activity.get_by_id(id)},
194 %Object{} = note <- Object.normalize(activity, false),
195 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
196 {:ok, undo, _} <- Builder.undo(user, announce),
197 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
198 {:ok, activity}
199 else
200 {:find_activity, _} -> {:error, :not_found}
201 _ -> {:error, dgettext("errors", "Could not unrepeat")}
202 end
203 end
204
205 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
206 def favorite(%User{} = user, id) do
207 case favorite_helper(user, id) do
208 {:ok, _} = res ->
209 res
210
211 {:error, :not_found} = res ->
212 res
213
214 {:error, e} ->
215 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
216 {:error, dgettext("errors", "Could not favorite")}
217 end
218 end
219
220 def favorite_helper(user, id) do
221 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
222 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
223 {_, {:ok, %Activity{} = activity, _meta}} <-
224 {:common_pipeline,
225 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
226 {:ok, activity}
227 else
228 {:find_object, _} ->
229 {:error, :not_found}
230
231 {:common_pipeline,
232 {
233 :error,
234 {
235 :validate_object,
236 {
237 :error,
238 changeset
239 }
240 }
241 }} = e ->
242 if {:object, {"already liked by this actor", []}} in changeset.errors do
243 {:ok, :already_liked}
244 else
245 {:error, e}
246 end
247
248 e ->
249 {:error, e}
250 end
251 end
252
253 def unfavorite(id, user) do
254 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
255 {:find_activity, Activity.get_by_id(id)},
256 %Object{} = note <- Object.normalize(activity, false),
257 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
258 {:ok, undo, _} <- Builder.undo(user, like),
259 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
260 {:ok, activity}
261 else
262 {:find_activity, _} -> {:error, :not_found}
263 _ -> {:error, dgettext("errors", "Could not unfavorite")}
264 end
265 end
266
267 def react_with_emoji(id, user, emoji) do
268 with %Activity{} = activity <- Activity.get_by_id(id),
269 object <- Object.normalize(activity),
270 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
271 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
272 {:ok, activity}
273 else
274 _ ->
275 {:error, dgettext("errors", "Could not add reaction emoji")}
276 end
277 end
278
279 def unreact_with_emoji(id, user, emoji) do
280 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
281 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
282 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
283 {:ok, activity}
284 else
285 _ ->
286 {:error, dgettext("errors", "Could not remove reaction emoji")}
287 end
288 end
289
290 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
291 with :ok <- validate_not_author(object, user),
292 :ok <- validate_existing_votes(user, object),
293 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
294 answer_activities =
295 Enum.map(choices, fn index ->
296 {:ok, answer_object, _meta} =
297 Builder.answer(user, object, Enum.at(options, index)["name"])
298
299 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
300
301 {:ok, activity, _meta} =
302 activity_data
303 |> Map.put("cc", answer_object["cc"])
304 |> Map.put("context", answer_object["context"])
305 |> Pipeline.common_pipeline(local: true)
306
307 # TODO: Do preload of Pleroma.Object in Pipeline
308 Activity.normalize(activity.data)
309 end)
310
311 object = Object.get_cached_by_ap_id(object.data["id"])
312 {:ok, answer_activities, object}
313 end
314 end
315
316 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
317 do: {:error, dgettext("errors", "Poll's author can't vote")}
318
319 defp validate_not_author(_, _), do: :ok
320
321 defp validate_existing_votes(%{ap_id: ap_id}, object) do
322 if Utils.get_existing_votes(ap_id, object) == [] do
323 :ok
324 else
325 {:error, dgettext("errors", "Already voted")}
326 end
327 end
328
329 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
330 when is_list(any_of) and any_of != [],
331 do: {any_of, Enum.count(any_of)}
332
333 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
334 when is_list(one_of) and one_of != [],
335 do: {one_of, 1}
336
337 defp normalize_and_validate_choices(choices, object) do
338 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
339 {options, max_count} = get_options_and_max_count(object)
340 count = Enum.count(options)
341
342 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
343 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
344 {:ok, options, choices}
345 else
346 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
347 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
348 end
349 end
350
351 def public_announce?(_, %{visibility: visibility})
352 when visibility in ~w{public unlisted private direct},
353 do: visibility in ~w(public unlisted)
354
355 def public_announce?(object, _) do
356 Visibility.is_public?(object)
357 end
358
359 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
360
361 def get_visibility(%{visibility: visibility}, in_reply_to, _)
362 when visibility in ~w{public unlisted private direct},
363 do: {visibility, get_replied_to_visibility(in_reply_to)}
364
365 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
366 visibility = {:list, String.to_integer(list_id)}
367 {visibility, get_replied_to_visibility(in_reply_to)}
368 end
369
370 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
371 visibility = get_replied_to_visibility(in_reply_to)
372 {visibility, visibility}
373 end
374
375 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
376
377 def get_replied_to_visibility(nil), do: nil
378
379 def get_replied_to_visibility(activity) do
380 with %Object{} = object <- Object.normalize(activity) do
381 Visibility.get_visibility(object)
382 end
383 end
384
385 def check_expiry_date({:ok, nil} = res), do: res
386
387 def check_expiry_date({:ok, in_seconds}) do
388 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
389
390 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
391 {:ok, expiry}
392 else
393 {:error, "Expiry date is too soon"}
394 end
395 end
396
397 def check_expiry_date(expiry_str) do
398 Ecto.Type.cast(:integer, expiry_str)
399 |> check_expiry_date()
400 end
401
402 def listen(user, data) do
403 with {:ok, draft} <- ActivityDraft.listen(user, data) do
404 ActivityPub.listen(draft.changes)
405 end
406 end
407
408 def post(user, %{status: _} = data) do
409 with {:ok, draft} <- ActivityDraft.create(user, data) do
410 ActivityPub.create(draft.changes, draft.preview?)
411 end
412 end
413
414 def pin(id, %{ap_id: user_ap_id} = user) do
415 with %Activity{
416 actor: ^user_ap_id,
417 data: %{"type" => "Create"},
418 object: %Object{data: %{"type" => object_type}}
419 } = activity <- Activity.get_by_id_with_object(id),
420 true <- object_type in ["Note", "Article", "Question"],
421 true <- Visibility.is_public?(activity),
422 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
423 {:ok, activity}
424 else
425 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
426 _ -> {:error, dgettext("errors", "Could not pin")}
427 end
428 end
429
430 def unpin(id, user) do
431 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
432 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
433 {:ok, activity}
434 else
435 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
436 _ -> {:error, dgettext("errors", "Could not unpin")}
437 end
438 end
439
440 def add_mute(user, activity) do
441 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
442 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
443 {:ok, activity}
444 else
445 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
446 end
447 end
448
449 def remove_mute(user, activity) do
450 ThreadMute.remove_mute(user.id, activity.data["context"])
451 {:ok, activity}
452 end
453
454 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
455 when is_binary(context) do
456 ThreadMute.exists?(user_id, context)
457 end
458
459 def thread_muted?(_, _), do: false
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
541 def get_user(ap_id, fake_record_fallback \\ true) do
542 cond do
543 user = User.get_cached_by_ap_id(ap_id) ->
544 user
545
546 user = User.get_by_guessed_nickname(ap_id) ->
547 user
548
549 fake_record_fallback ->
550 # TODO: refactor (fake records is never a good idea)
551 User.error_user(ap_id)
552
553 true ->
554 nil
555 end
556 end
557 end