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