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