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