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