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