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