1c47a31d76be0c919403a6a24c99e59036e3c3e7
[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.Bookmark
8 alias Pleroma.Formatter
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 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
21 {:ok, activity} <- ActivityPub.follow(follower, followed),
22 {:ok, follower, followed} <-
23 User.wait_and_refresh(
24 Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
25 follower,
26 followed
27 ) 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) do
35 {:ok, follower}
36 end
37 end
38
39 def accept_follow_request(follower, followed) do
40 with {:ok, follower} <- User.follow(follower, followed),
41 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
42 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
43 {:ok, _activity} <-
44 ActivityPub.accept(%{
45 to: [follower.ap_id],
46 actor: followed,
47 object: follow_activity.data["id"],
48 type: "Accept"
49 }) do
50 {:ok, follower}
51 end
52 end
53
54 def reject_follow_request(follower, followed) do
55 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
56 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
57 {:ok, _activity} <-
58 ActivityPub.reject(%{
59 to: [follower.ap_id],
60 actor: followed,
61 object: follow_activity.data["id"],
62 type: "Reject"
63 }) do
64 {:ok, follower}
65 end
66 end
67
68 def delete(activity_id, user) do
69 with %Activity{data: %{"object" => _}} = activity <-
70 Activity.get_by_id_with_object(activity_id),
71 %Object{} = object <- Object.normalize(activity),
72 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
73 {:ok, _} <- unpin(activity_id, user),
74 {:ok, delete} <- ActivityPub.delete(object) do
75 {:ok, delete}
76 else
77 _ ->
78 {:error, dgettext("errors", "Could not delete")}
79 end
80 end
81
82 def repeat(id_or_ap_id, user) do
83 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
84 object <- Object.normalize(activity),
85 nil <- Utils.get_existing_announce(user.ap_id, object) do
86 ActivityPub.announce(user, object)
87 else
88 _ ->
89 {:error, dgettext("errors", "Could not repeat")}
90 end
91 end
92
93 def unrepeat(id_or_ap_id, user) do
94 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
95 object <- Object.normalize(activity) do
96 ActivityPub.unannounce(user, object)
97 else
98 _ ->
99 {:error, dgettext("errors", "Could not unrepeat")}
100 end
101 end
102
103 def favorite(id_or_ap_id, user) do
104 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
105 object <- Object.normalize(activity),
106 nil <- Utils.get_existing_like(user.ap_id, object) do
107 ActivityPub.like(user, object)
108 else
109 _ ->
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),
116 object <- Object.normalize(activity) do
117 ActivityPub.unlike(user, object)
118 else
119 _ ->
120 {:error, dgettext("errors", "Could not unfavorite")}
121 end
122 end
123
124 def vote(user, object, choices) do
125 with "Question" <- object.data["type"],
126 {:author, false} <- {:author, object.data["actor"] == user.ap_id},
127 {:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)},
128 {options, max_count} <- get_options_and_max_count(object),
129 option_count <- Enum.count(options),
130 {:choice_check, {choices, true}} <-
131 {:choice_check, normalize_and_validate_choice_indices(choices, option_count)},
132 {:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do
133 answer_activities =
134 Enum.map(choices, fn index ->
135 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
136
137 {:ok, activity} =
138 ActivityPub.create(%{
139 to: answer_data["to"],
140 actor: user,
141 context: object.data["context"],
142 object: answer_data,
143 additional: %{"cc" => answer_data["cc"]}
144 })
145
146 activity
147 end)
148
149 object = Object.get_cached_by_ap_id(object.data["id"])
150 {:ok, answer_activities, object}
151 else
152 {:author, _} -> {:error, dgettext("errors", "Poll's author can't vote")}
153 {:existing_votes, _} -> {:error, dgettext("errors", "Already voted")}
154 {:choice_check, {_, false}} -> {:error, dgettext("errors", "Invalid indices")}
155 {:count_check, false} -> {:error, dgettext("errors", "Too many choices")}
156 end
157 end
158
159 defp get_options_and_max_count(object) do
160 if Map.has_key?(object.data, "anyOf") do
161 {object.data["anyOf"], Enum.count(object.data["anyOf"])}
162 else
163 {object.data["oneOf"], 1}
164 end
165 end
166
167 defp normalize_and_validate_choice_indices(choices, count) do
168 Enum.map_reduce(choices, true, fn index, valid ->
169 index = if is_binary(index), do: String.to_integer(index), else: index
170 {index, if(valid, do: index < count, else: valid)}
171 end)
172 end
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 Pleroma.Web.ActivityPub.Visibility.get_visibility(object)
195 end
196 end
197
198 def post(user, %{"status" => status} = data) do
199 limit = Pleroma.Config.get([:instance, :limit])
200
201 with status <- String.trim(status),
202 attachments <- attachments_from_ids(data),
203 in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
204 {visibility, in_reply_to_visibility} <- get_visibility(data, in_reply_to),
205 {_, false} <-
206 {:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"},
207 {content_html, mentions, tags} <-
208 make_content_html(
209 status,
210 attachments,
211 data,
212 visibility
213 ),
214 mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id),
215 addressed_users <- get_addressed_users(mentioned_users, data["to"]),
216 {poll, poll_emoji} <- make_poll_data(data),
217 {to, cc} <- get_to_and_cc(user, addressed_users, in_reply_to, visibility),
218 context <- make_context(in_reply_to),
219 cw <- data["spoiler_text"] || "",
220 sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
221 full_payload <- String.trim(status <> cw),
222 :ok <- validate_character_limit(full_payload, attachments, limit),
223 object <-
224 make_note_data(
225 user.ap_id,
226 to,
227 context,
228 content_html,
229 attachments,
230 in_reply_to,
231 tags,
232 cw,
233 cc,
234 sensitive,
235 poll
236 ),
237 object <-
238 Map.put(
239 object,
240 "emoji",
241 Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)
242 ) do
243 preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
244 direct? = visibility == "direct"
245
246 additional_data =
247 %{"cc" => cc, "directMessage" => direct?} |> maybe_add_list_data(user, visibility)
248
249 params = %{
250 to: to,
251 actor: user,
252 context: context,
253 object: object,
254 additional: additional_data
255 }
256
257 ActivityPub.create(params, preview?)
258 else
259 {:private_to_public, true} ->
260 {:error, dgettext("errors", "The message visibility must be direct")}
261
262 {:error, _} = e ->
263 e
264
265 e ->
266 {:error, e}
267 end
268 end
269
270 # Updates the emojis for a user based on their profile
271 def update(user) do
272 user =
273 with emoji <- emoji_from_profile(user),
274 source_data <- (user.info.source_data || %{}) |> Map.put("tag", emoji),
275 info_cng <- User.Info.set_source_data(user.info, source_data),
276 change <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
277 {:ok, user} <- User.update_and_set_cache(change) do
278 user
279 else
280 _e ->
281 user
282 end
283
284 ActivityPub.update(%{
285 local: true,
286 to: [user.follower_address],
287 cc: [],
288 actor: user.ap_id,
289 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
290 })
291 end
292
293 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
294 with %Activity{
295 actor: ^user_ap_id,
296 data: %{
297 "type" => "Create"
298 },
299 object: %Object{
300 data: %{
301 "type" => "Note"
302 }
303 }
304 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
305 true <- Visibility.is_public?(activity),
306 %{valid?: true} = info_changeset <-
307 User.Info.add_pinnned_activity(user.info, activity),
308 changeset <-
309 Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
310 {:ok, _user} <- User.update_and_set_cache(changeset) do
311 {:ok, activity}
312 else
313 %{errors: [pinned_activities: {err, _}]} ->
314 {:error, err}
315
316 _ ->
317 {:error, dgettext("errors", "Could not pin")}
318 end
319 end
320
321 def unpin(id_or_ap_id, user) do
322 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
323 %{valid?: true} = info_changeset <-
324 User.Info.remove_pinnned_activity(user.info, activity),
325 changeset <-
326 Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
327 {:ok, _user} <- User.update_and_set_cache(changeset) do
328 {:ok, activity}
329 else
330 %{errors: [pinned_activities: {err, _}]} ->
331 {:error, err}
332
333 _ ->
334 {:error, dgettext("errors", "Could not unpin")}
335 end
336 end
337
338 def add_mute(user, activity) do
339 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
340 {:ok, activity}
341 else
342 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
343 end
344 end
345
346 def remove_mute(user, activity) do
347 ThreadMute.remove_mute(user.id, activity.data["context"])
348 {:ok, activity}
349 end
350
351 def thread_muted?(%{id: nil} = _user, _activity), do: false
352
353 def thread_muted?(user, activity) do
354 with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
355 false
356 else
357 _ -> true
358 end
359 end
360
361 def bookmarked?(user, activity) do
362 with %Bookmark{} <- Bookmark.get(user.id, activity.id) do
363 true
364 else
365 _ ->
366 false
367 end
368 end
369
370 def report(user, data) do
371 with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
372 {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
373 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
374 {:ok, statuses} <- get_report_statuses(account, data),
375 {:ok, activity} <-
376 ActivityPub.flag(%{
377 context: Utils.generate_context_id(),
378 actor: user,
379 account: account,
380 statuses: statuses,
381 content: content_html,
382 forward: data["forward"] || false
383 }) do
384 {:ok, activity}
385 else
386 {:error, err} -> {:error, err}
387 {:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")}
388 {:account, nil} -> {:error, dgettext("errors", "Account not found")}
389 end
390 end
391
392 def update_report_state(activity_id, state) do
393 with %Activity{} = activity <- Activity.get_by_id(activity_id),
394 {:ok, activity} <- Utils.update_report_state(activity, state) do
395 {:ok, activity}
396 else
397 nil -> {:error, :not_found}
398 {:error, reason} -> {:error, reason}
399 _ -> {:error, dgettext("errors", "Could not update state")}
400 end
401 end
402
403 def update_activity_scope(activity_id, opts \\ %{}) do
404 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
405 {:ok, activity} <- toggle_sensitive(activity, opts),
406 {:ok, activity} <- set_visibility(activity, opts) do
407 {:ok, activity}
408 else
409 nil -> {:error, :not_found}
410 {:error, reason} -> {:error, reason}
411 end
412 end
413
414 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
415 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
416 end
417
418 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
419 when is_boolean(sensitive) do
420 new_data = Map.put(object.data, "sensitive", sensitive)
421
422 {:ok, object} =
423 object
424 |> Object.change(%{data: new_data})
425 |> Object.update_and_set_cache()
426
427 {:ok, Map.put(activity, :object, object)}
428 end
429
430 defp toggle_sensitive(activity, _), do: {:ok, activity}
431
432 defp set_visibility(activity, %{"visibility" => visibility}) do
433 Utils.update_activity_visibility(activity, visibility)
434 end
435
436 defp set_visibility(activity, _), do: {:ok, activity}
437
438 def hide_reblogs(user, muted) do
439 ap_id = muted.ap_id
440
441 if ap_id not in user.info.muted_reblogs do
442 info_changeset = User.Info.add_reblog_mute(user.info, ap_id)
443 changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
444 User.update_and_set_cache(changeset)
445 end
446 end
447
448 def show_reblogs(user, muted) do
449 ap_id = muted.ap_id
450
451 if ap_id in user.info.muted_reblogs do
452 info_changeset = User.Info.remove_reblog_mute(user.info, ap_id)
453 changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
454 User.update_and_set_cache(changeset)
455 end
456 end
457 end