Merge remote-tracking branch 'upstream/develop' into linkify
[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 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
312
313 {:ok, activity} =
314 ActivityPub.create(%{
315 to: answer_data["to"],
316 actor: user,
317 context: object.data["context"],
318 object: answer_data,
319 additional: %{"cc" => answer_data["cc"]}
320 })
321
322 activity
323 end)
324
325 object = Object.get_cached_by_ap_id(object.data["id"])
326 {:ok, answer_activities, object}
327 end
328 end
329
330 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
331 do: {:error, dgettext("errors", "Poll's author can't vote")}
332
333 defp validate_not_author(_, _), do: :ok
334
335 defp validate_existing_votes(%{ap_id: ap_id}, object) do
336 if Utils.get_existing_votes(ap_id, object) == [] do
337 :ok
338 else
339 {:error, dgettext("errors", "Already voted")}
340 end
341 end
342
343 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
344 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
345
346 defp normalize_and_validate_choices(choices, object) do
347 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
348 {options, max_count} = get_options_and_max_count(object)
349 count = Enum.count(options)
350
351 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
352 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
353 {:ok, options, choices}
354 else
355 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
356 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
357 end
358 end
359
360 def public_announce?(_, %{visibility: visibility})
361 when visibility in ~w{public unlisted private direct},
362 do: visibility in ~w(public unlisted)
363
364 def public_announce?(object, _) do
365 Visibility.is_public?(object)
366 end
367
368 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
369
370 def get_visibility(%{visibility: visibility}, in_reply_to, _)
371 when visibility in ~w{public unlisted private direct},
372 do: {visibility, get_replied_to_visibility(in_reply_to)}
373
374 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
375 visibility = {:list, String.to_integer(list_id)}
376 {visibility, get_replied_to_visibility(in_reply_to)}
377 end
378
379 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
380 visibility = get_replied_to_visibility(in_reply_to)
381 {visibility, visibility}
382 end
383
384 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
385
386 def get_replied_to_visibility(nil), do: nil
387
388 def get_replied_to_visibility(activity) do
389 with %Object{} = object <- Object.normalize(activity) do
390 Visibility.get_visibility(object)
391 end
392 end
393
394 def check_expiry_date({:ok, nil} = res), do: res
395
396 def check_expiry_date({:ok, in_seconds}) do
397 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
398
399 if ActivityExpiration.expires_late_enough?(expiry) do
400 {:ok, expiry}
401 else
402 {:error, "Expiry date is too soon"}
403 end
404 end
405
406 def check_expiry_date(expiry_str) do
407 Ecto.Type.cast(:integer, expiry_str)
408 |> check_expiry_date()
409 end
410
411 def listen(user, data) do
412 visibility = Map.get(data, :visibility, "public")
413
414 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
415 listen_data <-
416 data
417 |> Map.take([:album, :artist, :title, :length])
418 |> Map.new(fn {key, value} -> {to_string(key), value} end)
419 |> Map.put("type", "Audio")
420 |> Map.put("to", to)
421 |> Map.put("cc", cc)
422 |> Map.put("actor", user.ap_id),
423 {:ok, activity} <-
424 ActivityPub.listen(%{
425 actor: user,
426 to: to,
427 object: listen_data,
428 context: Utils.generate_context_id(),
429 additional: %{"cc" => cc}
430 }) do
431 {:ok, activity}
432 end
433 end
434
435 def post(user, %{status: _} = data) do
436 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
437 ActivityPub.create(draft.changes, draft.preview?)
438 end
439 end
440
441 def pin(id, %{ap_id: user_ap_id} = user) do
442 with %Activity{
443 actor: ^user_ap_id,
444 data: %{"type" => "Create"},
445 object: %Object{data: %{"type" => object_type}}
446 } = activity <- Activity.get_by_id_with_object(id),
447 true <- object_type in ["Note", "Article", "Question"],
448 true <- Visibility.is_public?(activity),
449 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
450 {:ok, activity}
451 else
452 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
453 _ -> {:error, dgettext("errors", "Could not pin")}
454 end
455 end
456
457 def unpin(id, user) do
458 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
459 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
460 {:ok, activity}
461 else
462 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
463 _ -> {:error, dgettext("errors", "Could not unpin")}
464 end
465 end
466
467 def add_mute(user, activity) do
468 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
469 {:ok, activity}
470 else
471 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
472 end
473 end
474
475 def remove_mute(user, activity) do
476 ThreadMute.remove_mute(user.id, activity.data["context"])
477 {:ok, activity}
478 end
479
480 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
481 when is_binary("context") do
482 ThreadMute.exists?(user_id, context)
483 end
484
485 def thread_muted?(_, _), do: false
486
487 def report(user, data) do
488 with {:ok, account} <- get_reported_account(data.account_id),
489 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
490 {:ok, statuses} <- get_report_statuses(account, data) do
491 ActivityPub.flag(%{
492 context: Utils.generate_context_id(),
493 actor: user,
494 account: account,
495 statuses: statuses,
496 content: content_html,
497 forward: Map.get(data, :forward, false)
498 })
499 end
500 end
501
502 defp get_reported_account(account_id) do
503 case User.get_cached_by_id(account_id) do
504 %User{} = account -> {:ok, account}
505 _ -> {:error, dgettext("errors", "Account not found")}
506 end
507 end
508
509 def update_report_state(activity_ids, state) when is_list(activity_ids) do
510 case Utils.update_report_state(activity_ids, state) do
511 :ok -> {:ok, activity_ids}
512 _ -> {:error, dgettext("errors", "Could not update state")}
513 end
514 end
515
516 def update_report_state(activity_id, state) do
517 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
518 Utils.update_report_state(activity, state)
519 else
520 nil -> {:error, :not_found}
521 _ -> {:error, dgettext("errors", "Could not update state")}
522 end
523 end
524
525 def update_activity_scope(activity_id, opts \\ %{}) do
526 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
527 {:ok, activity} <- toggle_sensitive(activity, opts) do
528 set_visibility(activity, opts)
529 else
530 nil -> {:error, :not_found}
531 {:error, reason} -> {:error, reason}
532 end
533 end
534
535 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
536 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
537 end
538
539 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
540 when is_boolean(sensitive) do
541 new_data = Map.put(object.data, "sensitive", sensitive)
542
543 {:ok, object} =
544 object
545 |> Object.change(%{data: new_data})
546 |> Object.update_and_set_cache()
547
548 {:ok, Map.put(activity, :object, object)}
549 end
550
551 defp toggle_sensitive(activity, _), do: {:ok, activity}
552
553 defp set_visibility(activity, %{visibility: visibility}) do
554 Utils.update_activity_visibility(activity, visibility)
555 end
556
557 defp set_visibility(activity, _), do: {:ok, activity}
558
559 def hide_reblogs(%User{} = user, %User{} = target) do
560 UserRelationship.create_reblog_mute(user, target)
561 end
562
563 def show_reblogs(%User{} = user, %User{} = target) do
564 UserRelationship.delete_reblog_mute(user, target)
565 end
566 end