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