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