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