Transmogrifier: Strip internal emoji reaction fields.
[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 react_with_emoji(id, user, emoji) do
127 with %Activity{} = activity <- Activity.get_by_id(id),
128 object <- Object.normalize(activity) do
129 ActivityPub.react_with_emoji(user, object, emoji)
130 else
131 _ ->
132 {:error, dgettext("errors", "Could not add reaction emoji")}
133 end
134 end
135
136 def vote(user, object, choices) do
137 with "Question" <- object.data["type"],
138 {:author, false} <- {:author, object.data["actor"] == user.ap_id},
139 {:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)},
140 {options, max_count} <- get_options_and_max_count(object),
141 option_count <- Enum.count(options),
142 {:choice_check, {choices, true}} <-
143 {:choice_check, normalize_and_validate_choice_indices(choices, option_count)},
144 {:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do
145 answer_activities =
146 Enum.map(choices, fn index ->
147 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
148
149 {:ok, activity} =
150 ActivityPub.create(%{
151 to: answer_data["to"],
152 actor: user,
153 context: object.data["context"],
154 object: answer_data,
155 additional: %{"cc" => answer_data["cc"]}
156 })
157
158 activity
159 end)
160
161 object = Object.get_cached_by_ap_id(object.data["id"])
162 {:ok, answer_activities, object}
163 else
164 {:author, _} -> {:error, dgettext("errors", "Poll's author can't vote")}
165 {:existing_votes, _} -> {:error, dgettext("errors", "Already voted")}
166 {:choice_check, {_, false}} -> {:error, dgettext("errors", "Invalid indices")}
167 {:count_check, false} -> {:error, dgettext("errors", "Too many choices")}
168 end
169 end
170
171 defp get_options_and_max_count(object) do
172 if Map.has_key?(object.data, "anyOf") do
173 {object.data["anyOf"], Enum.count(object.data["anyOf"])}
174 else
175 {object.data["oneOf"], 1}
176 end
177 end
178
179 defp normalize_and_validate_choice_indices(choices, count) do
180 Enum.map_reduce(choices, true, fn index, valid ->
181 index = if is_binary(index), do: String.to_integer(index), else: index
182 {index, if(valid, do: index < count, else: valid)}
183 end)
184 end
185
186 def get_visibility(_, _, %Participation{}) do
187 {"direct", "direct"}
188 end
189
190 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
191 when visibility in ~w{public unlisted private direct},
192 do: {visibility, get_replied_to_visibility(in_reply_to)}
193
194 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
195 visibility = {:list, String.to_integer(list_id)}
196 {visibility, get_replied_to_visibility(in_reply_to)}
197 end
198
199 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
200 visibility = get_replied_to_visibility(in_reply_to)
201 {visibility, visibility}
202 end
203
204 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
205
206 def get_replied_to_visibility(nil), do: nil
207
208 def get_replied_to_visibility(activity) do
209 with %Object{} = object <- Object.normalize(activity) do
210 Pleroma.Web.ActivityPub.Visibility.get_visibility(object)
211 end
212 end
213
214 defp check_expiry_date({:ok, nil} = res), do: res
215
216 defp check_expiry_date({:ok, in_seconds}) do
217 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
218
219 if ActivityExpiration.expires_late_enough?(expiry) do
220 {:ok, expiry}
221 else
222 {:error, "Expiry date is too soon"}
223 end
224 end
225
226 defp check_expiry_date(expiry_str) do
227 Ecto.Type.cast(:integer, expiry_str)
228 |> check_expiry_date()
229 end
230
231 def post(user, %{"status" => status} = data) do
232 limit = Pleroma.Config.get([:instance, :limit])
233
234 with status <- String.trim(status),
235 attachments <- attachments_from_ids(data),
236 in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
237 in_reply_to_conversation <- Participation.get(data["in_reply_to_conversation_id"]),
238 {visibility, in_reply_to_visibility} <-
239 get_visibility(data, in_reply_to, in_reply_to_conversation),
240 {_, false} <-
241 {:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"},
242 {content_html, mentions, tags} <-
243 make_content_html(
244 status,
245 attachments,
246 data,
247 visibility
248 ),
249 mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id),
250 addressed_users <- get_addressed_users(mentioned_users, data["to"]),
251 {poll, poll_emoji} <- make_poll_data(data),
252 {to, cc} <-
253 get_to_and_cc(user, addressed_users, in_reply_to, visibility, in_reply_to_conversation),
254 context <- make_context(in_reply_to, in_reply_to_conversation),
255 cw <- data["spoiler_text"] || "",
256 sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
257 {:ok, expires_at} <- check_expiry_date(data["expires_in"]),
258 full_payload <- String.trim(status <> cw),
259 :ok <- validate_character_limit(full_payload, attachments, limit),
260 object <-
261 make_note_data(
262 user.ap_id,
263 to,
264 context,
265 content_html,
266 attachments,
267 in_reply_to,
268 tags,
269 cw,
270 cc,
271 sensitive,
272 poll
273 ),
274 object <-
275 Map.put(
276 object,
277 "emoji",
278 Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)
279 ) do
280 preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
281 direct? = visibility == "direct"
282
283 result =
284 %{
285 to: to,
286 actor: user,
287 context: context,
288 object: object,
289 additional: %{"cc" => cc, "directMessage" => direct?}
290 }
291 |> maybe_add_list_data(user, visibility)
292 |> ActivityPub.create(preview?)
293
294 if expires_at do
295 with {:ok, activity} <- result do
296 {:ok, _} = ActivityExpiration.create(activity, expires_at)
297 end
298 end
299
300 result
301 else
302 {:private_to_public, true} ->
303 {:error, dgettext("errors", "The message visibility must be direct")}
304
305 {:error, _} = e ->
306 e
307
308 e ->
309 {:error, e}
310 end
311 end
312
313 # Updates the emojis for a user based on their profile
314 def update(user) do
315 user =
316 with emoji <- emoji_from_profile(user),
317 source_data <- (user.info.source_data || %{}) |> Map.put("tag", emoji),
318 info_cng <- User.Info.set_source_data(user.info, source_data),
319 change <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
320 {:ok, user} <- User.update_and_set_cache(change) do
321 user
322 else
323 _e ->
324 user
325 end
326
327 ActivityPub.update(%{
328 local: true,
329 to: [user.follower_address],
330 cc: [],
331 actor: user.ap_id,
332 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
333 })
334 end
335
336 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
337 with %Activity{
338 actor: ^user_ap_id,
339 data: %{
340 "type" => "Create"
341 },
342 object: %Object{
343 data: %{
344 "type" => "Note"
345 }
346 }
347 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
348 true <- Visibility.is_public?(activity),
349 %{valid?: true} = info_changeset <- User.Info.add_pinnned_activity(user.info, activity),
350 changeset <-
351 Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
352 {:ok, _user} <- User.update_and_set_cache(changeset) do
353 {:ok, activity}
354 else
355 %{errors: [pinned_activities: {err, _}]} ->
356 {:error, err}
357
358 _ ->
359 {:error, dgettext("errors", "Could not pin")}
360 end
361 end
362
363 def unpin(id_or_ap_id, user) do
364 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
365 %{valid?: true} = info_changeset <-
366 User.Info.remove_pinnned_activity(user.info, activity),
367 changeset <-
368 Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
369 {:ok, _user} <- User.update_and_set_cache(changeset) do
370 {:ok, activity}
371 else
372 %{errors: [pinned_activities: {err, _}]} ->
373 {:error, err}
374
375 _ ->
376 {:error, dgettext("errors", "Could not unpin")}
377 end
378 end
379
380 def add_mute(user, activity) do
381 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
382 {:ok, activity}
383 else
384 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
385 end
386 end
387
388 def remove_mute(user, activity) do
389 ThreadMute.remove_mute(user.id, activity.data["context"])
390 {:ok, activity}
391 end
392
393 def thread_muted?(%{id: nil} = _user, _activity), do: false
394
395 def thread_muted?(user, activity) do
396 with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
397 false
398 else
399 _ -> true
400 end
401 end
402
403 def report(user, data) do
404 with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
405 {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
406 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
407 {:ok, statuses} <- get_report_statuses(account, data),
408 {:ok, activity} <-
409 ActivityPub.flag(%{
410 context: Utils.generate_context_id(),
411 actor: user,
412 account: account,
413 statuses: statuses,
414 content: content_html,
415 forward: data["forward"] || false
416 }) do
417 {:ok, activity}
418 else
419 {:error, err} -> {:error, err}
420 {:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")}
421 {:account, nil} -> {:error, dgettext("errors", "Account not found")}
422 end
423 end
424
425 def update_report_state(activity_id, state) do
426 with %Activity{} = activity <- Activity.get_by_id(activity_id),
427 {:ok, activity} <- Utils.update_report_state(activity, state) do
428 {:ok, activity}
429 else
430 nil -> {:error, :not_found}
431 {:error, reason} -> {:error, reason}
432 _ -> {:error, dgettext("errors", "Could not update state")}
433 end
434 end
435
436 def update_activity_scope(activity_id, opts \\ %{}) do
437 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
438 {:ok, activity} <- toggle_sensitive(activity, opts),
439 {:ok, activity} <- set_visibility(activity, opts) do
440 {:ok, activity}
441 else
442 nil -> {:error, :not_found}
443 {:error, reason} -> {:error, reason}
444 end
445 end
446
447 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
448 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
449 end
450
451 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
452 when is_boolean(sensitive) do
453 new_data = Map.put(object.data, "sensitive", sensitive)
454
455 {:ok, object} =
456 object
457 |> Object.change(%{data: new_data})
458 |> Object.update_and_set_cache()
459
460 {:ok, Map.put(activity, :object, object)}
461 end
462
463 defp toggle_sensitive(activity, _), do: {:ok, activity}
464
465 defp set_visibility(activity, %{"visibility" => visibility}) do
466 Utils.update_activity_visibility(activity, visibility)
467 end
468
469 defp set_visibility(activity, _), do: {:ok, activity}
470
471 def hide_reblogs(user, muted) do
472 ap_id = muted.ap_id
473
474 if ap_id not in user.info.muted_reblogs do
475 info_changeset = User.Info.add_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
481 def show_reblogs(user, muted) do
482 ap_id = muted.ap_id
483
484 if ap_id in user.info.muted_reblogs do
485 info_changeset = User.Info.remove_reblog_mute(user.info, ap_id)
486 changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
487 User.update_and_set_cache(changeset)
488 end
489 end
490 end