ActivityPub: Remove ActivityPub.accept
[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 block(blocker, blocked) do
29 with {:ok, block_data, _} <- Builder.block(blocker, blocked),
30 {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
31 {:ok, block}
32 end
33 end
34
35 def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
36 with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
37 :ok <- validate_chat_content_length(content, !!maybe_attachment),
38 {_, {:ok, chat_message_data, _meta}} <-
39 {:build_object,
40 Builder.chat_message(
41 user,
42 recipient.ap_id,
43 content |> format_chat_content,
44 attachment: maybe_attachment
45 )},
46 {_, {:ok, create_activity_data, _meta}} <-
47 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
48 {_, {:ok, %Activity{} = activity, _meta}} <-
49 {:common_pipeline,
50 Pipeline.common_pipeline(create_activity_data,
51 local: true
52 )} do
53 {:ok, activity}
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, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
134 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
135 {:ok, _notifications} <- Notification.dismiss(follow_activity),
136 {:ok, _activity} <-
137 ActivityPub.reject(%{
138 to: [follower.ap_id],
139 actor: followed,
140 object: follow_activity.data["id"],
141 type: "Reject"
142 }) do
143 {:ok, follower}
144 end
145 end
146
147 def delete(activity_id, user) do
148 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
149 {:find_activity, Activity.get_by_id(activity_id)},
150 {_, %Object{} = object, _} <-
151 {:find_object, Object.normalize(activity, false), activity},
152 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
153 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
154 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
155 {:ok, delete}
156 else
157 {:find_activity, _} ->
158 {:error, :not_found}
159
160 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
161 # We have the create activity, but not the object, it was probably pruned.
162 # Insert a tombstone and try again
163 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
164 {:ok, _tombstone} <- Object.create(tombstone_data) do
165 delete(activity_id, user)
166 else
167 _ ->
168 Logger.error(
169 "Could not insert tombstone for missing object on deletion. Object is #{object}."
170 )
171
172 {:error, dgettext("errors", "Could not delete")}
173 end
174
175 _ ->
176 {:error, dgettext("errors", "Could not delete")}
177 end
178 end
179
180 def repeat(id, user, params \\ %{}) do
181 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
182 object = %Object{} <- Object.normalize(activity, false),
183 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
184 public = public_announce?(object, params),
185 {:ok, announce, _} <- Builder.announce(user, object, public: public),
186 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
187 {:ok, activity}
188 else
189 {:existing_announce, %Activity{} = announce} ->
190 {:ok, announce}
191
192 _ ->
193 {:error, :not_found}
194 end
195 end
196
197 def unrepeat(id, user) do
198 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
199 {:find_activity, Activity.get_by_id(id)},
200 %Object{} = note <- Object.normalize(activity, false),
201 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
202 {:ok, undo, _} <- Builder.undo(user, announce),
203 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
204 {:ok, activity}
205 else
206 {:find_activity, _} -> {:error, :not_found}
207 _ -> {:error, dgettext("errors", "Could not unrepeat")}
208 end
209 end
210
211 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
212 def favorite(%User{} = user, id) do
213 case favorite_helper(user, id) do
214 {:ok, _} = res ->
215 res
216
217 {:error, :not_found} = res ->
218 res
219
220 {:error, e} ->
221 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
222 {:error, dgettext("errors", "Could not favorite")}
223 end
224 end
225
226 def favorite_helper(user, id) do
227 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
228 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
229 {_, {:ok, %Activity{} = activity, _meta}} <-
230 {:common_pipeline,
231 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
232 {:ok, activity}
233 else
234 {:find_object, _} ->
235 {:error, :not_found}
236
237 {:common_pipeline,
238 {
239 :error,
240 {
241 :validate_object,
242 {
243 :error,
244 changeset
245 }
246 }
247 }} = e ->
248 if {:object, {"already liked by this actor", []}} in changeset.errors do
249 {:ok, :already_liked}
250 else
251 {:error, e}
252 end
253
254 e ->
255 {:error, e}
256 end
257 end
258
259 def unfavorite(id, user) do
260 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
261 {:find_activity, Activity.get_by_id(id)},
262 %Object{} = note <- Object.normalize(activity, false),
263 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
264 {:ok, undo, _} <- Builder.undo(user, like),
265 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
266 {:ok, activity}
267 else
268 {:find_activity, _} -> {:error, :not_found}
269 _ -> {:error, dgettext("errors", "Could not unfavorite")}
270 end
271 end
272
273 def react_with_emoji(id, user, emoji) do
274 with %Activity{} = activity <- Activity.get_by_id(id),
275 object <- Object.normalize(activity),
276 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
277 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
278 {:ok, activity}
279 else
280 _ ->
281 {:error, dgettext("errors", "Could not add reaction emoji")}
282 end
283 end
284
285 def unreact_with_emoji(id, user, emoji) do
286 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
287 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
288 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
289 {:ok, activity}
290 else
291 _ ->
292 {:error, dgettext("errors", "Could not remove reaction emoji")}
293 end
294 end
295
296 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
297 with :ok <- validate_not_author(object, user),
298 :ok <- validate_existing_votes(user, object),
299 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
300 answer_activities =
301 Enum.map(choices, fn index ->
302 {:ok, answer_object, _meta} =
303 Builder.answer(user, object, Enum.at(options, index)["name"])
304
305 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
306
307 {:ok, activity, _meta} =
308 activity_data
309 |> Map.put("cc", answer_object["cc"])
310 |> Map.put("context", answer_object["context"])
311 |> Pipeline.common_pipeline(local: true)
312
313 # TODO: Do preload of Pleroma.Object in Pipeline
314 Activity.normalize(activity.data)
315 end)
316
317 object = Object.get_cached_by_ap_id(object.data["id"])
318 {:ok, answer_activities, object}
319 end
320 end
321
322 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
323 do: {:error, dgettext("errors", "Poll's author can't vote")}
324
325 defp validate_not_author(_, _), do: :ok
326
327 defp validate_existing_votes(%{ap_id: ap_id}, object) do
328 if Utils.get_existing_votes(ap_id, object) == [] do
329 :ok
330 else
331 {:error, dgettext("errors", "Already voted")}
332 end
333 end
334
335 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
336 when is_list(any_of) and any_of != [],
337 do: {any_of, Enum.count(any_of)}
338
339 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
340 when is_list(one_of) and one_of != [],
341 do: {one_of, 1}
342
343 defp normalize_and_validate_choices(choices, object) do
344 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
345 {options, max_count} = get_options_and_max_count(object)
346 count = Enum.count(options)
347
348 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
349 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
350 {:ok, options, choices}
351 else
352 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
353 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
354 end
355 end
356
357 def public_announce?(_, %{visibility: visibility})
358 when visibility in ~w{public unlisted private direct},
359 do: visibility in ~w(public unlisted)
360
361 def public_announce?(object, _) do
362 Visibility.is_public?(object)
363 end
364
365 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
366
367 def get_visibility(%{visibility: visibility}, in_reply_to, _)
368 when visibility in ~w{public unlisted private direct},
369 do: {visibility, get_replied_to_visibility(in_reply_to)}
370
371 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
372 visibility = {:list, String.to_integer(list_id)}
373 {visibility, get_replied_to_visibility(in_reply_to)}
374 end
375
376 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
377 visibility = get_replied_to_visibility(in_reply_to)
378 {visibility, visibility}
379 end
380
381 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
382
383 def get_replied_to_visibility(nil), do: nil
384
385 def get_replied_to_visibility(activity) do
386 with %Object{} = object <- Object.normalize(activity) do
387 Visibility.get_visibility(object)
388 end
389 end
390
391 def check_expiry_date({:ok, nil} = res), do: res
392
393 def check_expiry_date({:ok, in_seconds}) do
394 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
395
396 if ActivityExpiration.expires_late_enough?(expiry) do
397 {:ok, expiry}
398 else
399 {:error, "Expiry date is too soon"}
400 end
401 end
402
403 def check_expiry_date(expiry_str) do
404 Ecto.Type.cast(:integer, expiry_str)
405 |> check_expiry_date()
406 end
407
408 def listen(user, data) do
409 visibility = Map.get(data, :visibility, "public")
410
411 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
412 listen_data <-
413 data
414 |> Map.take([:album, :artist, :title, :length])
415 |> Map.new(fn {key, value} -> {to_string(key), value} end)
416 |> Map.put("type", "Audio")
417 |> Map.put("to", to)
418 |> Map.put("cc", cc)
419 |> Map.put("actor", user.ap_id),
420 {:ok, activity} <-
421 ActivityPub.listen(%{
422 actor: user,
423 to: to,
424 object: listen_data,
425 context: Utils.generate_context_id(),
426 additional: %{"cc" => cc}
427 }) do
428 {:ok, activity}
429 end
430 end
431
432 def post(user, %{status: _} = data) do
433 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
434 ActivityPub.create(draft.changes, draft.preview?)
435 end
436 end
437
438 def pin(id, %{ap_id: user_ap_id} = user) do
439 with %Activity{
440 actor: ^user_ap_id,
441 data: %{"type" => "Create"},
442 object: %Object{data: %{"type" => object_type}}
443 } = activity <- Activity.get_by_id_with_object(id),
444 true <- object_type in ["Note", "Article", "Question"],
445 true <- Visibility.is_public?(activity),
446 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
447 {:ok, activity}
448 else
449 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
450 _ -> {:error, dgettext("errors", "Could not pin")}
451 end
452 end
453
454 def unpin(id, user) do
455 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
456 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
457 {:ok, activity}
458 else
459 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
460 _ -> {:error, dgettext("errors", "Could not unpin")}
461 end
462 end
463
464 def add_mute(user, activity) do
465 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
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