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