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