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