Merge branch 'develop' into issue/1218
[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.Emoji
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 <- put_emoji(object, full_payload, poll_emoji) do
265 preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
266 direct? = visibility == "direct"
267
268 result =
269 %{
270 to: to,
271 actor: user,
272 context: context,
273 object: object,
274 additional: %{"cc" => cc, "directMessage" => direct?}
275 }
276 |> maybe_add_list_data(user, visibility)
277 |> ActivityPub.create(preview?)
278
279 if expires_at do
280 with {:ok, activity} <- result do
281 {:ok, _} = ActivityExpiration.create(activity, expires_at)
282 end
283 end
284
285 result
286 else
287 {:private_to_public, true} ->
288 {:error, dgettext("errors", "The message visibility must be direct")}
289
290 {:error, _} = e ->
291 e
292
293 e ->
294 {:error, e}
295 end
296 end
297
298 # parse and put emoji to object data
299 defp put_emoji(map, text, emojis) do
300 Map.put(
301 map,
302 "emoji",
303 Map.merge(Emoji.Formatter.get_emoji_map(text), emojis)
304 )
305 end
306
307 # Updates the emojis for a user based on their profile
308 def update(user) do
309 user =
310 with emoji <- emoji_from_profile(user),
311 source_data <- (user.info.source_data || %{}) |> Map.put("tag", emoji),
312 info_cng <- User.Info.set_source_data(user.info, source_data),
313 change <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
314 {:ok, user} <- User.update_and_set_cache(change) do
315 user
316 else
317 _e ->
318 user
319 end
320
321 ActivityPub.update(%{
322 local: true,
323 to: [user.follower_address],
324 cc: [],
325 actor: user.ap_id,
326 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
327 })
328 end
329
330 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
331 with %Activity{
332 actor: ^user_ap_id,
333 data: %{
334 "type" => "Create"
335 },
336 object: %Object{
337 data: %{
338 "type" => "Note"
339 }
340 }
341 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
342 true <- Visibility.is_public?(activity),
343 %{valid?: true} = info_changeset <- User.Info.add_pinnned_activity(user.info, activity),
344 changeset <-
345 Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
346 {:ok, _user} <- User.update_and_set_cache(changeset) do
347 {:ok, activity}
348 else
349 %{errors: [pinned_activities: {err, _}]} ->
350 {:error, err}
351
352 _ ->
353 {:error, dgettext("errors", "Could not pin")}
354 end
355 end
356
357 def unpin(id_or_ap_id, user) do
358 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
359 %{valid?: true} = info_changeset <-
360 User.Info.remove_pinnned_activity(user.info, activity),
361 changeset <-
362 Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
363 {:ok, _user} <- User.update_and_set_cache(changeset) do
364 {:ok, activity}
365 else
366 %{errors: [pinned_activities: {err, _}]} ->
367 {:error, err}
368
369 _ ->
370 {:error, dgettext("errors", "Could not unpin")}
371 end
372 end
373
374 def add_mute(user, activity) do
375 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
376 {:ok, activity}
377 else
378 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
379 end
380 end
381
382 def remove_mute(user, activity) do
383 ThreadMute.remove_mute(user.id, activity.data["context"])
384 {:ok, activity}
385 end
386
387 def thread_muted?(%{id: nil} = _user, _activity), do: false
388
389 def thread_muted?(user, activity) do
390 with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
391 false
392 else
393 _ -> true
394 end
395 end
396
397 def report(user, data) do
398 with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
399 {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
400 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
401 {:ok, statuses} <- get_report_statuses(account, data),
402 {:ok, activity} <-
403 ActivityPub.flag(%{
404 context: Utils.generate_context_id(),
405 actor: user,
406 account: account,
407 statuses: statuses,
408 content: content_html,
409 forward: data["forward"] || false
410 }) do
411 {:ok, activity}
412 else
413 {:error, err} -> {:error, err}
414 {:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")}
415 {:account, nil} -> {:error, dgettext("errors", "Account not found")}
416 end
417 end
418
419 def update_report_state(activity_id, state) do
420 with %Activity{} = activity <- Activity.get_by_id(activity_id),
421 {:ok, activity} <- Utils.update_report_state(activity, state) do
422 {:ok, activity}
423 else
424 nil -> {:error, :not_found}
425 {:error, reason} -> {:error, reason}
426 _ -> {:error, dgettext("errors", "Could not update state")}
427 end
428 end
429
430 def update_activity_scope(activity_id, opts \\ %{}) do
431 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
432 {:ok, activity} <- toggle_sensitive(activity, opts),
433 {:ok, activity} <- set_visibility(activity, opts) do
434 {:ok, activity}
435 else
436 nil -> {:error, :not_found}
437 {:error, reason} -> {:error, reason}
438 end
439 end
440
441 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
442 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
443 end
444
445 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
446 when is_boolean(sensitive) do
447 new_data = Map.put(object.data, "sensitive", sensitive)
448
449 {:ok, object} =
450 object
451 |> Object.change(%{data: new_data})
452 |> Object.update_and_set_cache()
453
454 {:ok, Map.put(activity, :object, object)}
455 end
456
457 defp toggle_sensitive(activity, _), do: {:ok, activity}
458
459 defp set_visibility(activity, %{"visibility" => visibility}) do
460 Utils.update_activity_visibility(activity, visibility)
461 end
462
463 defp set_visibility(activity, _), do: {:ok, activity}
464
465 def hide_reblogs(user, muted) do
466 ap_id = muted.ap_id
467
468 if ap_id not in user.info.muted_reblogs do
469 info_changeset = User.Info.add_reblog_mute(user.info, ap_id)
470 changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
471 User.update_and_set_cache(changeset)
472 end
473 end
474
475 def show_reblogs(user, muted) do
476 ap_id = muted.ap_id
477
478 if ap_id in user.info.muted_reblogs do
479 info_changeset = User.Info.remove_reblog_mute(user.info, ap_id)
480 changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
481 User.update_and_set_cache(changeset)
482 end
483 end
484 end