Merge branch 'feature/reports-groups-and-multiple-state-update' 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.Web.ActivityPub.ActivityPub
14 alias Pleroma.Web.ActivityPub.Utils
15 alias Pleroma.Web.ActivityPub.Visibility
16
17 import Pleroma.Web.Gettext
18 import Pleroma.Web.CommonAPI.Utils
19
20 require Pleroma.Constants
21
22 def follow(follower, followed) do
23 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
24
25 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
26 {:ok, activity} <- ActivityPub.follow(follower, followed),
27 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
28 {:ok, follower, followed, activity}
29 end
30 end
31
32 def unfollow(follower, unfollowed) do
33 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
34 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
35 {:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do
36 {:ok, follower}
37 end
38 end
39
40 def accept_follow_request(follower, followed) do
41 with {:ok, follower} <- User.follow(follower, followed),
42 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
43 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
44 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"),
45 {:ok, _activity} <-
46 ActivityPub.accept(%{
47 to: [follower.ap_id],
48 actor: followed,
49 object: follow_activity.data["id"],
50 type: "Accept"
51 }) do
52 {:ok, follower}
53 end
54 end
55
56 def reject_follow_request(follower, followed) do
57 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
58 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
59 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
60 {:ok, _activity} <-
61 ActivityPub.reject(%{
62 to: [follower.ap_id],
63 actor: followed,
64 object: follow_activity.data["id"],
65 type: "Reject"
66 }) do
67 {:ok, follower}
68 end
69 end
70
71 def delete(activity_id, user) do
72 with %Activity{data: %{"object" => _}} = activity <-
73 Activity.get_by_id_with_object(activity_id),
74 %Object{} = object <- Object.normalize(activity),
75 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
76 {:ok, _} <- unpin(activity_id, user),
77 {:ok, delete} <- ActivityPub.delete(object) do
78 {:ok, delete}
79 else
80 _ -> {:error, dgettext("errors", "Could not delete")}
81 end
82 end
83
84 def repeat(id_or_ap_id, user, params \\ %{}) do
85 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
86 object <- Object.normalize(activity),
87 nil <- Utils.get_existing_announce(user.ap_id, object),
88 public <- public_announce?(object, params) do
89 ActivityPub.announce(user, object, nil, true, public)
90 else
91 _ -> {:error, dgettext("errors", "Could not repeat")}
92 end
93 end
94
95 def unrepeat(id_or_ap_id, user) do
96 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
97 object = Object.normalize(activity)
98 ActivityPub.unannounce(user, object)
99 else
100 _ -> {:error, dgettext("errors", "Could not unrepeat")}
101 end
102 end
103
104 def favorite(id_or_ap_id, user) do
105 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
106 object <- Object.normalize(activity),
107 nil <- Utils.get_existing_like(user.ap_id, object) do
108 ActivityPub.like(user, object)
109 else
110 _ -> {:error, dgettext("errors", "Could not favorite")}
111 end
112 end
113
114 def unfavorite(id_or_ap_id, user) do
115 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
116 object = Object.normalize(activity)
117 ActivityPub.unlike(user, object)
118 else
119 _ -> {:error, dgettext("errors", "Could not unfavorite")}
120 end
121 end
122
123 def react_with_emoji(id, user, emoji) do
124 with %Activity{} = activity <- Activity.get_by_id(id),
125 object <- Object.normalize(activity) do
126 ActivityPub.react_with_emoji(user, object, emoji)
127 else
128 _ ->
129 {:error, dgettext("errors", "Could not add reaction emoji")}
130 end
131 end
132
133 def unreact_with_emoji(id, user, emoji) do
134 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
135 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
136 else
137 _ ->
138 {:error, dgettext("errors", "Could not remove reaction emoji")}
139 end
140 end
141
142 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
143 with :ok <- validate_not_author(object, user),
144 :ok <- validate_existing_votes(user, object),
145 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
146 answer_activities =
147 Enum.map(choices, fn index ->
148 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
149
150 {:ok, activity} =
151 ActivityPub.create(%{
152 to: answer_data["to"],
153 actor: user,
154 context: object.data["context"],
155 object: answer_data,
156 additional: %{"cc" => answer_data["cc"]}
157 })
158
159 activity
160 end)
161
162 object = Object.get_cached_by_ap_id(object.data["id"])
163 {:ok, answer_activities, object}
164 end
165 end
166
167 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
168 do: {:error, dgettext("errors", "Poll's author can't vote")}
169
170 defp validate_not_author(_, _), do: :ok
171
172 defp validate_existing_votes(%{ap_id: ap_id}, object) do
173 if Utils.get_existing_votes(ap_id, object) == [] do
174 :ok
175 else
176 {:error, dgettext("errors", "Already voted")}
177 end
178 end
179
180 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
181 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
182
183 defp normalize_and_validate_choices(choices, object) do
184 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
185 {options, max_count} = get_options_and_max_count(object)
186 count = Enum.count(options)
187
188 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
189 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
190 {:ok, options, choices}
191 else
192 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
193 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
194 end
195 end
196
197 def public_announce?(_, %{"visibility" => visibility})
198 when visibility in ~w{public unlisted private direct},
199 do: visibility in ~w(public unlisted)
200
201 def public_announce?(object, _) do
202 Visibility.is_public?(object)
203 end
204
205 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
206
207 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
208 when visibility in ~w{public unlisted private direct},
209 do: {visibility, get_replied_to_visibility(in_reply_to)}
210
211 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
212 visibility = {:list, String.to_integer(list_id)}
213 {visibility, get_replied_to_visibility(in_reply_to)}
214 end
215
216 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
217 visibility = get_replied_to_visibility(in_reply_to)
218 {visibility, visibility}
219 end
220
221 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
222
223 def get_replied_to_visibility(nil), do: nil
224
225 def get_replied_to_visibility(activity) do
226 with %Object{} = object <- Object.normalize(activity) do
227 Visibility.get_visibility(object)
228 end
229 end
230
231 def check_expiry_date({:ok, nil} = res), do: res
232
233 def check_expiry_date({:ok, in_seconds}) do
234 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
235
236 if ActivityExpiration.expires_late_enough?(expiry) do
237 {:ok, expiry}
238 else
239 {:error, "Expiry date is too soon"}
240 end
241 end
242
243 def check_expiry_date(expiry_str) do
244 Ecto.Type.cast(:integer, expiry_str)
245 |> check_expiry_date()
246 end
247
248 def listen(user, %{"title" => _} = data) do
249 with visibility <- data["visibility"] || "public",
250 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
251 listen_data <-
252 Map.take(data, ["album", "artist", "title", "length"])
253 |> Map.put("type", "Audio")
254 |> Map.put("to", to)
255 |> Map.put("cc", cc)
256 |> Map.put("actor", user.ap_id),
257 {:ok, activity} <-
258 ActivityPub.listen(%{
259 actor: user,
260 to: to,
261 object: listen_data,
262 context: Utils.generate_context_id(),
263 additional: %{"cc" => cc}
264 }) do
265 {:ok, activity}
266 end
267 end
268
269 def post(user, %{"status" => _} = data) do
270 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
271 draft.changes
272 |> ActivityPub.create(draft.preview?)
273 |> maybe_create_activity_expiration(draft.expires_at)
274 end
275 end
276
277 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
278 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
279 {:ok, activity}
280 end
281 end
282
283 defp maybe_create_activity_expiration(result, _), do: result
284
285 # Updates the emojis for a user based on their profile
286 def update(user) do
287 emoji = emoji_from_profile(user)
288 source_data = Map.put(user.source_data, "tag", emoji)
289
290 user =
291 case User.update_source_data(user, source_data) do
292 {:ok, user} -> user
293 _ -> user
294 end
295
296 ActivityPub.update(%{
297 local: true,
298 to: [Pleroma.Constants.as_public(), user.follower_address],
299 cc: [],
300 actor: user.ap_id,
301 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
302 })
303 end
304
305 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
306 with %Activity{
307 actor: ^user_ap_id,
308 data: %{"type" => "Create"},
309 object: %Object{data: %{"type" => "Note"}}
310 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
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, %{ap_id: ap_id} = _muted) do
424 if ap_id not in user.muted_reblogs do
425 User.add_reblog_mute(user, ap_id)
426 end
427 end
428
429 def show_reblogs(user, %{ap_id: ap_id} = _muted) do
430 if ap_id in user.muted_reblogs do
431 User.remove_reblog_mute(user, ap_id)
432 end
433 end
434 end