Cleanup Pleroma.User
[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 emoji = emoji_from_profile(user)
306 source_data = user.info |> Map.get(:source_data, {}) |> Map.put("tag", emoji)
307
308 user =
309 with {:ok, user} <- User.update_info(user, &User.Info.set_source_data(&1, source_data)) do
310 user
311 else
312 _e -> user
313 end
314
315 ActivityPub.update(%{
316 local: true,
317 to: [user.follower_address],
318 cc: [],
319 actor: user.ap_id,
320 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
321 })
322 end
323
324 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
325 with %Activity{
326 actor: ^user_ap_id,
327 data: %{
328 "type" => "Create"
329 },
330 object: %Object{
331 data: %{
332 "type" => "Note"
333 }
334 }
335 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
336 true <- Visibility.is_public?(activity),
337 {:ok, _user} <- User.update_info(user, &User.Info.add_pinnned_activity(&1, activity)) do
338 {:ok, activity}
339 else
340 {:error, %{changes: %{info: %{errors: [pinned_activities: {err, _}]}}}} -> {:error, err}
341 _ -> {:error, dgettext("errors", "Could not pin")}
342 end
343 end
344
345 def unpin(id_or_ap_id, user) do
346 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
347 {:ok, _user} <- User.update_info(user, &User.Info.remove_pinnned_activity(&1, activity)) do
348 {:ok, activity}
349 else
350 %{errors: [pinned_activities: {err, _}]} -> {:error, err}
351 _ -> {:error, dgettext("errors", "Could not unpin")}
352 end
353 end
354
355 def add_mute(user, activity) do
356 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
357 {:ok, activity}
358 else
359 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
360 end
361 end
362
363 def remove_mute(user, activity) do
364 ThreadMute.remove_mute(user.id, activity.data["context"])
365 {:ok, activity}
366 end
367
368 def thread_muted?(%{id: nil} = _user, _activity), do: false
369
370 def thread_muted?(user, activity) do
371 with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
372 false
373 else
374 _ -> true
375 end
376 end
377
378 def report(user, data) do
379 with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
380 {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
381 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
382 {:ok, statuses} <- get_report_statuses(account, data),
383 {:ok, activity} <-
384 ActivityPub.flag(%{
385 context: Utils.generate_context_id(),
386 actor: user,
387 account: account,
388 statuses: statuses,
389 content: content_html,
390 forward: data["forward"] || false
391 }) do
392 {:ok, activity}
393 else
394 {:error, err} -> {:error, err}
395 {:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")}
396 {:account, nil} -> {:error, dgettext("errors", "Account not found")}
397 end
398 end
399
400 def update_report_state(activity_id, state) do
401 with %Activity{} = activity <- Activity.get_by_id(activity_id),
402 {:ok, activity} <- Utils.update_report_state(activity, state) do
403 {:ok, activity}
404 else
405 nil -> {:error, :not_found}
406 {:error, reason} -> {:error, reason}
407 _ -> {:error, dgettext("errors", "Could not update state")}
408 end
409 end
410
411 def update_activity_scope(activity_id, opts \\ %{}) do
412 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
413 {:ok, activity} <- toggle_sensitive(activity, opts),
414 {:ok, activity} <- set_visibility(activity, opts) do
415 {:ok, activity}
416 else
417 nil -> {:error, :not_found}
418 {:error, reason} -> {:error, reason}
419 end
420 end
421
422 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
423 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
424 end
425
426 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
427 when is_boolean(sensitive) do
428 new_data = Map.put(object.data, "sensitive", sensitive)
429
430 {:ok, object} =
431 object
432 |> Object.change(%{data: new_data})
433 |> Object.update_and_set_cache()
434
435 {:ok, Map.put(activity, :object, object)}
436 end
437
438 defp toggle_sensitive(activity, _), do: {:ok, activity}
439
440 defp set_visibility(activity, %{"visibility" => visibility}) do
441 Utils.update_activity_visibility(activity, visibility)
442 end
443
444 defp set_visibility(activity, _), do: {:ok, activity}
445
446 def hide_reblogs(user, %{ap_id: ap_id} = _muted) do
447 if ap_id not in user.info.muted_reblogs do
448 User.update_info(user, &User.Info.add_reblog_mute(&1, ap_id))
449 end
450 end
451
452 def show_reblogs(user, %{ap_id: ap_id} = _muted) do
453 if ap_id in user.info.muted_reblogs do
454 User.update_info(user, &User.Info.remove_reblog_mute(&1, ap_id))
455 end
456 end
457 end