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