f764e87af7a4e02afafca43346764c3dfcf660d8
[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 %{
247 to: to,
248 actor: user,
249 context: context,
250 object: object,
251 additional: %{"cc" => cc, "directMessage" => direct?}
252 }
253 |> maybe_add_list_data(user, visibility)
254 |> ActivityPub.create(preview?)
255 else
256 {:private_to_public, true} ->
257 {:error, dgettext("errors", "The message visibility must be direct")}
258
259 {:error, _} = e ->
260 e
261
262 e ->
263 {:error, e}
264 end
265 end
266
267 # Updates the emojis for a user based on their profile
268 def update(user) do
269 user =
270 with emoji <- emoji_from_profile(user),
271 source_data <- (user.info.source_data || %{}) |> Map.put("tag", emoji),
272 info_cng <- User.Info.set_source_data(user.info, source_data),
273 change <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
274 {:ok, user} <- User.update_and_set_cache(change) do
275 user
276 else
277 _e ->
278 user
279 end
280
281 ActivityPub.update(%{
282 local: true,
283 to: [user.follower_address],
284 cc: [],
285 actor: user.ap_id,
286 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
287 })
288 end
289
290 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
291 with %Activity{
292 actor: ^user_ap_id,
293 data: %{
294 "type" => "Create"
295 },
296 object: %Object{
297 data: %{
298 "type" => "Note"
299 }
300 }
301 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
302 true <- Visibility.is_public?(activity),
303 %{valid?: true} = info_changeset <-
304 User.Info.add_pinnned_activity(user.info, activity),
305 changeset <-
306 Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
307 {:ok, _user} <- User.update_and_set_cache(changeset) do
308 {:ok, activity}
309 else
310 %{errors: [pinned_activities: {err, _}]} ->
311 {:error, err}
312
313 _ ->
314 {:error, dgettext("errors", "Could not pin")}
315 end
316 end
317
318 def unpin(id_or_ap_id, user) do
319 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
320 %{valid?: true} = info_changeset <-
321 User.Info.remove_pinnned_activity(user.info, activity),
322 changeset <-
323 Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
324 {:ok, _user} <- User.update_and_set_cache(changeset) do
325 {:ok, activity}
326 else
327 %{errors: [pinned_activities: {err, _}]} ->
328 {:error, err}
329
330 _ ->
331 {:error, dgettext("errors", "Could not unpin")}
332 end
333 end
334
335 def add_mute(user, activity) do
336 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
337 {:ok, activity}
338 else
339 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
340 end
341 end
342
343 def remove_mute(user, activity) do
344 ThreadMute.remove_mute(user.id, activity.data["context"])
345 {:ok, activity}
346 end
347
348 def thread_muted?(%{id: nil} = _user, _activity), do: false
349
350 def thread_muted?(user, activity) do
351 with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
352 false
353 else
354 _ -> true
355 end
356 end
357
358 def bookmarked?(user, activity) do
359 with %Bookmark{} <- Bookmark.get(user.id, activity.id) do
360 true
361 else
362 _ ->
363 false
364 end
365 end
366
367 def report(user, data) do
368 with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
369 {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
370 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
371 {:ok, statuses} <- get_report_statuses(account, data),
372 {:ok, activity} <-
373 ActivityPub.flag(%{
374 context: Utils.generate_context_id(),
375 actor: user,
376 account: account,
377 statuses: statuses,
378 content: content_html,
379 forward: data["forward"] || false
380 }) do
381 {:ok, activity}
382 else
383 {:error, err} -> {:error, err}
384 {:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")}
385 {:account, nil} -> {:error, dgettext("errors", "Account not found")}
386 end
387 end
388
389 def update_report_state(activity_id, state) do
390 with %Activity{} = activity <- Activity.get_by_id(activity_id),
391 {:ok, activity} <- Utils.update_report_state(activity, state) do
392 {:ok, activity}
393 else
394 nil -> {:error, :not_found}
395 {:error, reason} -> {:error, reason}
396 _ -> {:error, dgettext("errors", "Could not update state")}
397 end
398 end
399
400 def update_activity_scope(activity_id, opts \\ %{}) do
401 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
402 {:ok, activity} <- toggle_sensitive(activity, opts),
403 {:ok, activity} <- set_visibility(activity, opts) do
404 {:ok, activity}
405 else
406 nil -> {:error, :not_found}
407 {:error, reason} -> {:error, reason}
408 end
409 end
410
411 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
412 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
413 end
414
415 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
416 when is_boolean(sensitive) do
417 new_data = Map.put(object.data, "sensitive", sensitive)
418
419 {:ok, object} =
420 object
421 |> Object.change(%{data: new_data})
422 |> Object.update_and_set_cache()
423
424 {:ok, Map.put(activity, :object, object)}
425 end
426
427 defp toggle_sensitive(activity, _), do: {:ok, activity}
428
429 defp set_visibility(activity, %{"visibility" => visibility}) do
430 Utils.update_activity_visibility(activity, visibility)
431 end
432
433 defp set_visibility(activity, _), do: {:ok, activity}
434
435 def hide_reblogs(user, muted) do
436 ap_id = muted.ap_id
437
438 if ap_id not in user.info.muted_reblogs do
439 info_changeset = User.Info.add_reblog_mute(user.info, ap_id)
440 changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
441 User.update_and_set_cache(changeset)
442 end
443 end
444
445 def show_reblogs(user, muted) do
446 ap_id = muted.ap_id
447
448 if ap_id in user.info.muted_reblogs do
449 info_changeset = User.Info.remove_reblog_mute(user.info, ap_id)
450 changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
451 User.update_and_set_cache(changeset)
452 end
453 end
454 end