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