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