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