b02c47059b27698a187332c3f88ae6e120642a15
[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 {:ok, activity} <-
224 ActivityPub.listen(%{
225 actor: user,
226 to: to,
227 object: listen_data,
228 context: Utils.generate_context_id(),
229 additional: %{"cc" => cc}
230 }) do
231 {:ok, activity}
232 end
233 end
234
235 def post(user, %{"status" => _} = data) do
236 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
237 draft.changes
238 |> ActivityPub.create(draft.preview?)
239 |> maybe_create_activity_expiration(draft.expires_at)
240 end
241 end
242
243 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
244 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
245 {:ok, activity}
246 end
247 end
248
249 defp maybe_create_activity_expiration(result, _), do: result
250
251 # Updates the emojis for a user based on their profile
252 def update(user) do
253 emoji = emoji_from_profile(user)
254 source_data = user.info |> Map.get(:source_data, %{}) |> Map.put("tag", emoji)
255
256 user =
257 case User.update_info(user, &User.Info.set_source_data(&1, source_data)) do
258 {:ok, user} -> user
259 _ -> user
260 end
261
262 ActivityPub.update(%{
263 local: true,
264 to: [user.follower_address],
265 cc: [],
266 actor: user.ap_id,
267 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
268 })
269 end
270
271 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
272 with %Activity{
273 actor: ^user_ap_id,
274 data: %{"type" => "Create"},
275 object: %Object{data: %{"type" => "Note"}}
276 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
277 true <- Visibility.is_public?(activity),
278 {:ok, _user} <- User.update_info(user, &User.Info.add_pinnned_activity(&1, activity)) do
279 {:ok, activity}
280 else
281 {:error, %{changes: %{info: %{errors: [pinned_activities: {err, _}]}}}} -> {:error, err}
282 _ -> {:error, dgettext("errors", "Could not pin")}
283 end
284 end
285
286 def unpin(id_or_ap_id, user) do
287 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
288 {:ok, _user} <- User.update_info(user, &User.Info.remove_pinnned_activity(&1, activity)) do
289 {:ok, activity}
290 else
291 %{errors: [pinned_activities: {err, _}]} -> {:error, err}
292 _ -> {:error, dgettext("errors", "Could not unpin")}
293 end
294 end
295
296 def add_mute(user, activity) do
297 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
298 {:ok, activity}
299 else
300 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
301 end
302 end
303
304 def remove_mute(user, activity) do
305 ThreadMute.remove_mute(user.id, activity.data["context"])
306 {:ok, activity}
307 end
308
309 def thread_muted?(%{id: nil} = _user, _activity), do: false
310
311 def thread_muted?(user, activity) do
312 ThreadMute.check_muted(user.id, activity.data["context"]) != []
313 end
314
315 def report(user, %{"account_id" => account_id} = data) do
316 with {:ok, account} <- get_reported_account(account_id),
317 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
318 {:ok, statuses} <- get_report_statuses(account, data) do
319 ActivityPub.flag(%{
320 context: Utils.generate_context_id(),
321 actor: user,
322 account: account,
323 statuses: statuses,
324 content: content_html,
325 forward: data["forward"] || false
326 })
327 end
328 end
329
330 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
331
332 defp get_reported_account(account_id) do
333 case User.get_cached_by_id(account_id) do
334 %User{} = account -> {:ok, account}
335 _ -> {:error, dgettext("errors", "Account not found")}
336 end
337 end
338
339 def update_report_state(activity_id, state) do
340 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
341 Utils.update_report_state(activity, state)
342 else
343 nil -> {:error, :not_found}
344 _ -> {:error, dgettext("errors", "Could not update state")}
345 end
346 end
347
348 def update_activity_scope(activity_id, opts \\ %{}) do
349 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
350 {:ok, activity} <- toggle_sensitive(activity, opts) do
351 set_visibility(activity, opts)
352 else
353 nil -> {:error, :not_found}
354 {:error, reason} -> {:error, reason}
355 end
356 end
357
358 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
359 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
360 end
361
362 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
363 when is_boolean(sensitive) do
364 new_data = Map.put(object.data, "sensitive", sensitive)
365
366 {:ok, object} =
367 object
368 |> Object.change(%{data: new_data})
369 |> Object.update_and_set_cache()
370
371 {:ok, Map.put(activity, :object, object)}
372 end
373
374 defp toggle_sensitive(activity, _), do: {:ok, activity}
375
376 defp set_visibility(activity, %{"visibility" => visibility}) do
377 Utils.update_activity_visibility(activity, visibility)
378 end
379
380 defp set_visibility(activity, _), do: {:ok, activity}
381
382 def hide_reblogs(user, %{ap_id: ap_id} = _muted) do
383 if ap_id not in user.info.muted_reblogs do
384 User.update_info(user, &User.Info.add_reblog_mute(&1, ap_id))
385 end
386 end
387
388 def show_reblogs(user, %{ap_id: ap_id} = _muted) do
389 if ap_id in user.info.muted_reblogs do
390 User.update_info(user, &User.Info.remove_reblog_mute(&1, ap_id))
391 end
392 end
393 end