Merge remote-tracking branch 'origin/develop' into sixohsix/pleroma-post_expiration
[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(expiry_str) do
205 {:ok, expiry} = Ecto.Type.cast(:naive_datetime, expiry_str)
206
207 if is_nil(expiry) || ActivityExpiration.expires_late_enough?(expiry) do
208 {:ok, expiry}
209 else
210 {:error, "Expiry date is too soon"}
211 end
212 end
213
214 def post(user, %{"status" => status} = data) do
215 limit = Pleroma.Config.get([:instance, :limit])
216
217 with status <- String.trim(status),
218 attachments <- attachments_from_ids(data),
219 in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
220 in_reply_to_conversation <- Participation.get(data["in_reply_to_conversation_id"]),
221 {visibility, in_reply_to_visibility} <-
222 get_visibility(data, in_reply_to, in_reply_to_conversation),
223 {_, false} <-
224 {:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"},
225 {content_html, mentions, tags} <-
226 make_content_html(
227 status,
228 attachments,
229 data,
230 visibility
231 ),
232 mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id),
233 addressed_users <- get_addressed_users(mentioned_users, data["to"]),
234 {poll, poll_emoji} <- make_poll_data(data),
235 {to, cc} <-
236 get_to_and_cc(user, addressed_users, in_reply_to, visibility, in_reply_to_conversation),
237 context <- make_context(in_reply_to, in_reply_to_conversation),
238 cw <- data["spoiler_text"] || "",
239 sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
240 {:ok, expires_at} <- check_expiry_date(data["expires_at"]),
241 full_payload <- String.trim(status <> cw),
242 :ok <- validate_character_limit(full_payload, attachments, limit),
243 object <-
244 make_note_data(
245 user.ap_id,
246 to,
247 context,
248 content_html,
249 attachments,
250 in_reply_to,
251 tags,
252 cw,
253 cc,
254 sensitive,
255 poll
256 ),
257 object <-
258 Map.put(
259 object,
260 "emoji",
261 Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)
262 ) do
263 preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
264 direct? = visibility == "direct"
265
266 result =
267 %{
268 to: to,
269 actor: user,
270 context: context,
271 object: object,
272 additional: %{"cc" => cc, "directMessage" => direct?}
273 }
274 |> maybe_add_list_data(user, visibility)
275 |> ActivityPub.create(preview?)
276
277 if expires_at do
278 with {:ok, activity} <- result do
279 {:ok, _} = ActivityExpiration.create(activity, expires_at)
280 end
281 end
282
283 result
284 else
285 {:private_to_public, true} ->
286 {:error, dgettext("errors", "The message visibility must be direct")}
287
288 {:error, _} = e ->
289 e
290
291 e ->
292 {:error, e}
293 end
294 end
295
296 # Updates the emojis for a user based on their profile
297 def update(user) do
298 user =
299 with emoji <- emoji_from_profile(user),
300 source_data <- (user.info.source_data || %{}) |> Map.put("tag", emoji),
301 info_cng <- User.Info.set_source_data(user.info, source_data),
302 change <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
303 {:ok, user} <- User.update_and_set_cache(change) do
304 user
305 else
306 _e ->
307 user
308 end
309
310 ActivityPub.update(%{
311 local: true,
312 to: [user.follower_address],
313 cc: [],
314 actor: user.ap_id,
315 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
316 })
317 end
318
319 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
320 with %Activity{
321 actor: ^user_ap_id,
322 data: %{
323 "type" => "Create"
324 },
325 object: %Object{
326 data: %{
327 "type" => "Note"
328 }
329 }
330 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
331 true <- Visibility.is_public?(activity),
332 %{valid?: true} = info_changeset <- User.Info.add_pinnned_activity(user.info, activity),
333 changeset <-
334 Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
335 {:ok, _user} <- User.update_and_set_cache(changeset) do
336 {:ok, activity}
337 else
338 %{errors: [pinned_activities: {err, _}]} ->
339 {:error, err}
340
341 _ ->
342 {:error, dgettext("errors", "Could not pin")}
343 end
344 end
345
346 def unpin(id_or_ap_id, user) do
347 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
348 %{valid?: true} = info_changeset <-
349 User.Info.remove_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 unpin")}
360 end
361 end
362
363 def add_mute(user, activity) do
364 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
365 {:ok, activity}
366 else
367 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
368 end
369 end
370
371 def remove_mute(user, activity) do
372 ThreadMute.remove_mute(user.id, activity.data["context"])
373 {:ok, activity}
374 end
375
376 def thread_muted?(%{id: nil} = _user, _activity), do: false
377
378 def thread_muted?(user, activity) do
379 with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
380 false
381 else
382 _ -> true
383 end
384 end
385
386 def report(user, data) do
387 with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
388 {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
389 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
390 {:ok, statuses} <- get_report_statuses(account, data),
391 {:ok, activity} <-
392 ActivityPub.flag(%{
393 context: Utils.generate_context_id(),
394 actor: user,
395 account: account,
396 statuses: statuses,
397 content: content_html,
398 forward: data["forward"] || false
399 }) do
400 {:ok, activity}
401 else
402 {:error, err} -> {:error, err}
403 {:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")}
404 {:account, nil} -> {:error, dgettext("errors", "Account not found")}
405 end
406 end
407
408 def update_report_state(activity_id, state) do
409 with %Activity{} = activity <- Activity.get_by_id(activity_id),
410 {:ok, activity} <- Utils.update_report_state(activity, state) do
411 {:ok, activity}
412 else
413 nil -> {:error, :not_found}
414 {:error, reason} -> {:error, reason}
415 _ -> {:error, dgettext("errors", "Could not update state")}
416 end
417 end
418
419 def update_activity_scope(activity_id, opts \\ %{}) do
420 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
421 {:ok, activity} <- toggle_sensitive(activity, opts),
422 {:ok, activity} <- set_visibility(activity, opts) do
423 {:ok, activity}
424 else
425 nil -> {:error, :not_found}
426 {:error, reason} -> {:error, reason}
427 end
428 end
429
430 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
431 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
432 end
433
434 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
435 when is_boolean(sensitive) do
436 new_data = Map.put(object.data, "sensitive", sensitive)
437
438 {:ok, object} =
439 object
440 |> Object.change(%{data: new_data})
441 |> Object.update_and_set_cache()
442
443 {:ok, Map.put(activity, :object, object)}
444 end
445
446 defp toggle_sensitive(activity, _), do: {:ok, activity}
447
448 defp set_visibility(activity, %{"visibility" => visibility}) do
449 Utils.update_activity_visibility(activity, visibility)
450 end
451
452 defp set_visibility(activity, _), do: {:ok, activity}
453
454 def hide_reblogs(user, muted) do
455 ap_id = muted.ap_id
456
457 if ap_id not in user.info.muted_reblogs do
458 info_changeset = User.Info.add_reblog_mute(user.info, ap_id)
459 changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
460 User.update_and_set_cache(changeset)
461 end
462 end
463
464 def show_reblogs(user, muted) do
465 ap_id = muted.ap_id
466
467 if ap_id in user.info.muted_reblogs do
468 info_changeset = User.Info.remove_reblog_mute(user.info, ap_id)
469 changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
470 User.update_and_set_cache(changeset)
471 end
472 end
473 end