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