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