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