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, "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, "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 # Updates the emojis for a user based on their profile
326 def update(user) do
327 emoji = emoji_from_profile(user)
328 source_data = Map.put(user.source_data, "tag", emoji)
329
330 user =
331 case User.update_source_data(user, source_data) do
332 {:ok, user} -> user
333 _ -> user
334 end
335
336 ActivityPub.update(%{
337 local: true,
338 to: [Pleroma.Constants.as_public(), user.follower_address],
339 cc: [],
340 actor: user.ap_id,
341 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
342 })
343 end
344
345 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
346 with %Activity{
347 actor: ^user_ap_id,
348 data: %{"type" => "Create"},
349 object: %Object{data: %{"type" => object_type}}
350 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
351 true <- object_type in ["Note", "Article", "Question"],
352 true <- Visibility.is_public?(activity),
353 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
354 {:ok, activity}
355 else
356 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
357 _ -> {:error, dgettext("errors", "Could not pin")}
358 end
359 end
360
361 def unpin(id_or_ap_id, user) do
362 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
363 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
364 {:ok, activity}
365 else
366 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
367 _ -> {:error, dgettext("errors", "Could not unpin")}
368 end
369 end
370
371 def add_mute(user, activity) do
372 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
373 {:ok, activity}
374 else
375 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
376 end
377 end
378
379 def remove_mute(user, activity) do
380 ThreadMute.remove_mute(user.id, activity.data["context"])
381 {:ok, activity}
382 end
383
384 def thread_muted?(%{id: nil} = _user, _activity), do: false
385
386 def thread_muted?(user, activity) do
387 ThreadMute.exists?(user.id, activity.data["context"])
388 end
389
390 def report(user, %{"account_id" => account_id} = data) do
391 with {:ok, account} <- get_reported_account(account_id),
392 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
393 {:ok, statuses} <- get_report_statuses(account, data) do
394 ActivityPub.flag(%{
395 context: Utils.generate_context_id(),
396 actor: user,
397 account: account,
398 statuses: statuses,
399 content: content_html,
400 forward: data["forward"] || false
401 })
402 end
403 end
404
405 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
406
407 defp get_reported_account(account_id) do
408 case User.get_cached_by_id(account_id) do
409 %User{} = account -> {:ok, account}
410 _ -> {:error, dgettext("errors", "Account not found")}
411 end
412 end
413
414 def update_report_state(activity_ids, state) when is_list(activity_ids) do
415 case Utils.update_report_state(activity_ids, state) do
416 :ok -> {:ok, activity_ids}
417 _ -> {:error, dgettext("errors", "Could not update state")}
418 end
419 end
420
421 def update_report_state(activity_id, state) do
422 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
423 Utils.update_report_state(activity, state)
424 else
425 nil -> {:error, :not_found}
426 _ -> {:error, dgettext("errors", "Could not update state")}
427 end
428 end
429
430 def update_activity_scope(activity_id, opts \\ %{}) do
431 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
432 {:ok, activity} <- toggle_sensitive(activity, opts) do
433 set_visibility(activity, opts)
434 else
435 nil -> {:error, :not_found}
436 {:error, reason} -> {:error, reason}
437 end
438 end
439
440 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
441 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
442 end
443
444 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
445 when is_boolean(sensitive) do
446 new_data = Map.put(object.data, "sensitive", sensitive)
447
448 {:ok, object} =
449 object
450 |> Object.change(%{data: new_data})
451 |> Object.update_and_set_cache()
452
453 {:ok, Map.put(activity, :object, object)}
454 end
455
456 defp toggle_sensitive(activity, _), do: {:ok, activity}
457
458 defp set_visibility(activity, %{"visibility" => visibility}) do
459 Utils.update_activity_visibility(activity, visibility)
460 end
461
462 defp set_visibility(activity, _), do: {:ok, activity}
463
464 def hide_reblogs(%User{} = user, %User{} = target) do
465 UserRelationship.create_reblog_mute(user, target)
466 end
467
468 def show_reblogs(%User{} = user, %User{} = target) do
469 UserRelationship.delete_reblog_mute(user, target)
470 end
471 end