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