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