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