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