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