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