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