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