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