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