Add ability to set a default post expiry (#321)
[akkoma] / lib / pleroma / web / common_api.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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.Object
9 alias Pleroma.ThreadMute
10 alias Pleroma.User
11 alias Pleroma.UserRelationship
12 alias Pleroma.Web.ActivityPub.ActivityPub
13 alias Pleroma.Web.ActivityPub.Builder
14 alias Pleroma.Web.ActivityPub.Pipeline
15 alias Pleroma.Web.ActivityPub.Utils
16 alias Pleroma.Web.ActivityPub.Visibility
17 alias Pleroma.Web.CommonAPI.ActivityDraft
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 unblock(blocker, blocked) do
33 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
34 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
35 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
36 {:ok, unblock}
37 else
38 {:fetch_block, nil} ->
39 if User.blocks?(blocker, blocked) do
40 User.unblock(blocker, blocked)
41 {:ok, :no_activity}
42 else
43 {:error, :not_blocking}
44 end
45
46 e ->
47 e
48 end
49 end
50
51 def follow(follower, followed) do
52 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
53
54 with {:ok, follow_data, _} <- Builder.follow(follower, followed),
55 {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
56 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
57 if activity.data["state"] == "reject" do
58 {:error, :rejected}
59 else
60 {:ok, follower, followed, activity}
61 end
62 end
63 end
64
65 def unfollow(follower, unfollowed) do
66 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
67 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
68 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
69 {:ok, follower}
70 end
71 end
72
73 def accept_follow_request(follower, followed) do
74 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
75 {:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
76 {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do
77 {:ok, follower}
78 end
79 end
80
81 def reject_follow_request(follower, followed) do
82 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
83 {:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
84 {:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do
85 {:ok, follower}
86 end
87 end
88
89 def delete(activity_id, user) do
90 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
91 {:find_activity, Activity.get_by_id(activity_id)},
92 {_, %Object{} = object, _} <-
93 {:find_object, Object.normalize(activity, fetch: false), activity},
94 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
95 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
96 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
97 {:ok, delete}
98 else
99 {:find_activity, _} ->
100 {:error, :not_found}
101
102 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
103 # We have the create activity, but not the object, it was probably pruned.
104 # Insert a tombstone and try again
105 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
106 {:ok, _tombstone} <- Object.create(tombstone_data) do
107 delete(activity_id, user)
108 else
109 _ ->
110 Logger.error(
111 "Could not insert tombstone for missing object on deletion. Object is #{object}."
112 )
113
114 {:error, dgettext("errors", "Could not delete")}
115 end
116
117 _ ->
118 {:error, dgettext("errors", "Could not delete")}
119 end
120 end
121
122 def repeat(id, user, params \\ %{}) do
123 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
124 object = %Object{} <- Object.normalize(activity, fetch: false),
125 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
126 public = public_announce?(object, params),
127 {:ok, announce, _} <- Builder.announce(user, object, public: public),
128 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
129 {:ok, activity}
130 else
131 {:existing_announce, %Activity{} = announce} ->
132 {:ok, announce}
133
134 _ ->
135 {:error, :not_found}
136 end
137 end
138
139 def unrepeat(id, user) do
140 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
141 {:find_activity, Activity.get_by_id(id)},
142 %Object{} = note <- Object.normalize(activity, fetch: false),
143 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
144 {:ok, undo, _} <- Builder.undo(user, announce),
145 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
146 {:ok, activity}
147 else
148 {:find_activity, _} -> {:error, :not_found}
149 _ -> {:error, dgettext("errors", "Could not unrepeat")}
150 end
151 end
152
153 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
154 def favorite(%User{} = user, id) do
155 case favorite_helper(user, id) do
156 {:ok, _} = res ->
157 res
158
159 {:error, :not_found} = res ->
160 res
161
162 {:error, e} ->
163 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
164 {:error, dgettext("errors", "Could not favorite")}
165 end
166 end
167
168 def favorite_helper(user, id) do
169 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
170 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
171 {_, {:ok, %Activity{} = activity, _meta}} <-
172 {:common_pipeline,
173 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
174 {:ok, activity}
175 else
176 {:find_object, _} ->
177 {:error, :not_found}
178
179 {:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
180 if {:object, {"already liked by this actor", []}} in changeset.errors do
181 {:ok, :already_liked}
182 else
183 {:error, e}
184 end
185
186 e ->
187 {:error, e}
188 end
189 end
190
191 def unfavorite(id, user) do
192 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
193 {:find_activity, Activity.get_by_id(id)},
194 %Object{} = note <- Object.normalize(activity, fetch: false),
195 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
196 {:ok, undo, _} <- Builder.undo(user, like),
197 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
198 {:ok, activity}
199 else
200 {:find_activity, _} -> {:error, :not_found}
201 _ -> {:error, dgettext("errors", "Could not unfavorite")}
202 end
203 end
204
205 def react_with_emoji(id, user, emoji) do
206 with %Activity{} = activity <- Activity.get_by_id(id),
207 object <- Object.normalize(activity, fetch: false),
208 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
209 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
210 {:ok, activity}
211 else
212 _ ->
213 {:error, dgettext("errors", "Could not add reaction emoji")}
214 end
215 end
216
217 def unreact_with_emoji(id, user, emoji) do
218 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
219 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
220 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
221 {:ok, activity}
222 else
223 _ ->
224 {:error, dgettext("errors", "Could not remove reaction emoji")}
225 end
226 end
227
228 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
229 with :ok <- validate_not_author(object, user),
230 :ok <- validate_existing_votes(user, object),
231 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
232 answer_activities =
233 Enum.map(choices, fn index ->
234 {:ok, answer_object, _meta} =
235 Builder.answer(user, object, Enum.at(options, index)["name"])
236
237 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
238
239 {:ok, activity, _meta} =
240 activity_data
241 |> Map.put("cc", answer_object["cc"])
242 |> Map.put("context", answer_object["context"])
243 |> Pipeline.common_pipeline(local: true)
244
245 # TODO: Do preload of Pleroma.Object in Pipeline
246 Activity.normalize(activity.data)
247 end)
248
249 object = Object.get_cached_by_ap_id(object.data["id"])
250 {:ok, answer_activities, object}
251 end
252 end
253
254 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
255 do: {:error, dgettext("errors", "Poll's author can't vote")}
256
257 defp validate_not_author(_, _), do: :ok
258
259 defp validate_existing_votes(%{ap_id: ap_id}, object) do
260 if Utils.get_existing_votes(ap_id, object) == [] do
261 :ok
262 else
263 {:error, dgettext("errors", "Already voted")}
264 end
265 end
266
267 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
268 when is_list(any_of) and any_of != [],
269 do: {any_of, Enum.count(any_of)}
270
271 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
272 when is_list(one_of) and one_of != [],
273 do: {one_of, 1}
274
275 defp normalize_and_validate_choices(choices, object) do
276 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
277 {options, max_count} = get_options_and_max_count(object)
278 count = Enum.count(options)
279
280 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
281 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
282 {:ok, options, choices}
283 else
284 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
285 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
286 end
287 end
288
289 def public_announce?(_, %{visibility: visibility})
290 when visibility in ~w{public unlisted private direct},
291 do: visibility in ~w(public unlisted)
292
293 def public_announce?(object, _) do
294 Visibility.is_public?(object)
295 end
296
297 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
298
299 def get_visibility(%{visibility: visibility}, in_reply_to, _)
300 when visibility in ~w{public local unlisted private direct},
301 do: {visibility, get_replied_to_visibility(in_reply_to)}
302
303 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
304 visibility = {:list, String.to_integer(list_id)}
305 {visibility, get_replied_to_visibility(in_reply_to)}
306 end
307
308 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
309 visibility = get_replied_to_visibility(in_reply_to)
310 {visibility, visibility}
311 end
312
313 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
314
315 def get_replied_to_visibility(nil), do: nil
316
317 def get_replied_to_visibility(activity) do
318 with %Object{} = object <- Object.normalize(activity, fetch: false) do
319 Visibility.get_visibility(object)
320 end
321 end
322
323 def get_quoted_visibility(nil), do: nil
324
325 def get_quoted_visibility(activity), do: get_replied_to_visibility(activity)
326
327 def check_expiry_date({:ok, nil} = res), do: res
328
329 def check_expiry_date({:ok, in_seconds}) do
330 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
331
332 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
333 {:ok, expiry}
334 else
335 {:error, "Expiry date is too soon"}
336 end
337 end
338
339 def check_expiry_date(expiry_str) do
340 Ecto.Type.cast(:integer, expiry_str)
341 |> check_expiry_date()
342 end
343
344 def post(user, %{status: _} = data) do
345 with {:ok, draft} <- ActivityDraft.create(user, data) do
346 ActivityPub.create(draft.changes, draft.preview?)
347 end
348 end
349
350 def update(user, orig_activity, changes) do
351 with orig_object <- Object.normalize(orig_activity),
352 {:ok, new_object} <- make_update_data(user, orig_object, changes),
353 {:ok, update_data, _} <- Builder.update(user, new_object),
354 {:ok, update, _} <- Pipeline.common_pipeline(update_data, local: true) do
355 {:ok, update}
356 else
357 _ -> {:error, nil}
358 end
359 end
360
361 defp make_update_data(user, orig_object, changes) do
362 kept_params = %{
363 visibility: Visibility.get_visibility(orig_object),
364 in_reply_to_id:
365 with replied_id when is_binary(replied_id) <- orig_object.data["inReplyTo"],
366 %Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(replied_id) do
367 activity_id
368 else
369 _ -> nil
370 end
371 }
372
373 params = Map.merge(changes, kept_params)
374
375 with {:ok, draft} <- ActivityDraft.create(user, params) do
376 change =
377 Object.Updater.make_update_object_data(orig_object.data, draft.object, Utils.make_date())
378
379 {:ok, change}
380 else
381 _ -> {:error, nil}
382 end
383 end
384
385 @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
386 def pin(id, %User{} = user) do
387 with %Activity{} = activity <- create_activity_by_id(id),
388 true <- activity_belongs_to_actor(activity, user.ap_id),
389 true <- object_type_is_allowed_for_pin(activity.object),
390 true <- activity_is_public(activity),
391 {:ok, pin_data, _} <- Builder.pin(user, activity.object),
392 {:ok, _pin, _} <-
393 Pipeline.common_pipeline(pin_data,
394 local: true,
395 activity_id: id
396 ) do
397 {:ok, activity}
398 else
399 {:error, {:side_effects, error}} -> error
400 error -> error
401 end
402 end
403
404 defp create_activity_by_id(id) do
405 with nil <- Activity.create_by_id_with_object(id) do
406 {:error, :not_found}
407 end
408 end
409
410 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
411 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
412
413 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
414 with false <- type in ["Note", "Article", "Question"] do
415 {:error, :not_allowed}
416 end
417 end
418
419 defp activity_is_public(activity) do
420 with false <- Visibility.is_public?(activity) do
421 {:error, :visibility_error}
422 end
423 end
424
425 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
426 def unpin(id, user) do
427 with %Activity{} = activity <- create_activity_by_id(id),
428 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
429 {:ok, _unpin, _} <-
430 Pipeline.common_pipeline(unpin_data,
431 local: true,
432 activity_id: activity.id,
433 expires_at: activity.data["expires_at"],
434 featured_address: user.featured_address
435 ) do
436 {:ok, activity}
437 end
438 end
439
440 def add_mute(user, activity, params \\ %{}) do
441 expires_in = Map.get(params, :expires_in, 0)
442
443 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
444 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
445 if expires_in > 0 do
446 Pleroma.Workers.MuteExpireWorker.enqueue(
447 "unmute_conversation",
448 %{"user_id" => user.id, "activity_id" => activity.id},
449 schedule_in: expires_in
450 )
451 end
452
453 {:ok, activity}
454 else
455 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
456 end
457 end
458
459 def remove_mute(%User{} = user, %Activity{} = activity) do
460 ThreadMute.remove_mute(user.id, activity.data["context"])
461 {:ok, activity}
462 end
463
464 def remove_mute(user_id, activity_id) do
465 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
466 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
467 remove_mute(user, activity)
468 else
469 {what, result} = error ->
470 Logger.warn(
471 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
472 )
473
474 {:error, error}
475 end
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
565 def get_user(ap_id, fake_record_fallback \\ true) do
566 cond do
567 user = User.get_cached_by_ap_id(ap_id) ->
568 user
569
570 user = User.get_by_guessed_nickname(ap_id) ->
571 user
572
573 fake_record_fallback ->
574 # TODO: refactor (fake records is never a good idea)
575 User.error_user(ap_id)
576
577 true ->
578 nil
579 end
580 end
581 end