schedule activity expiration in Oban
[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) do
454 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
455 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
456 {:ok, activity}
457 else
458 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
459 end
460 end
461
462 def remove_mute(user, activity) do
463 ThreadMute.remove_mute(user.id, activity.data["context"])
464 {:ok, activity}
465 end
466
467 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
468 when is_binary(context) do
469 ThreadMute.exists?(user_id, context)
470 end
471
472 def thread_muted?(_, _), do: false
473
474 def report(user, data) do
475 with {:ok, account} <- get_reported_account(data.account_id),
476 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
477 {:ok, statuses} <- get_report_statuses(account, data) do
478 ActivityPub.flag(%{
479 context: Utils.generate_context_id(),
480 actor: user,
481 account: account,
482 statuses: statuses,
483 content: content_html,
484 forward: Map.get(data, :forward, false)
485 })
486 end
487 end
488
489 defp get_reported_account(account_id) do
490 case User.get_cached_by_id(account_id) do
491 %User{} = account -> {:ok, account}
492 _ -> {:error, dgettext("errors", "Account not found")}
493 end
494 end
495
496 def update_report_state(activity_ids, state) when is_list(activity_ids) do
497 case Utils.update_report_state(activity_ids, state) do
498 :ok -> {:ok, activity_ids}
499 _ -> {:error, dgettext("errors", "Could not update state")}
500 end
501 end
502
503 def update_report_state(activity_id, state) do
504 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
505 Utils.update_report_state(activity, state)
506 else
507 nil -> {:error, :not_found}
508 _ -> {:error, dgettext("errors", "Could not update state")}
509 end
510 end
511
512 def update_activity_scope(activity_id, opts \\ %{}) do
513 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
514 {:ok, activity} <- toggle_sensitive(activity, opts) do
515 set_visibility(activity, opts)
516 else
517 nil -> {:error, :not_found}
518 {:error, reason} -> {:error, reason}
519 end
520 end
521
522 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
523 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
524 end
525
526 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
527 when is_boolean(sensitive) do
528 new_data = Map.put(object.data, "sensitive", sensitive)
529
530 {:ok, object} =
531 object
532 |> Object.change(%{data: new_data})
533 |> Object.update_and_set_cache()
534
535 {:ok, Map.put(activity, :object, object)}
536 end
537
538 defp toggle_sensitive(activity, _), do: {:ok, activity}
539
540 defp set_visibility(activity, %{visibility: visibility}) do
541 Utils.update_activity_visibility(activity, visibility)
542 end
543
544 defp set_visibility(activity, _), do: {:ok, activity}
545
546 def hide_reblogs(%User{} = user, %User{} = target) do
547 UserRelationship.create_reblog_mute(user, target)
548 end
549
550 def show_reblogs(%User{} = user, %User{} = target) do
551 UserRelationship.delete_reblog_mute(user, target)
552 end
553 end