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