Merge branch 'reject' into 'develop'
[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) do
455 with {:ok, _} <- ThreadMute.add_mute(user.id, 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