Fix multiple-choice poll detection
[akkoma] / lib / pleroma / web / common_api / common_api.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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.FollowingRelationship
10 alias Pleroma.Formatter
11 alias Pleroma.Notification
12 alias Pleroma.Object
13 alias Pleroma.ThreadMute
14 alias Pleroma.User
15 alias Pleroma.UserRelationship
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.Builder
18 alias Pleroma.Web.ActivityPub.Pipeline
19 alias Pleroma.Web.ActivityPub.Utils
20 alias Pleroma.Web.ActivityPub.Visibility
21
22 import Pleroma.Web.Gettext
23 import Pleroma.Web.CommonAPI.Utils
24
25 require Pleroma.Constants
26 require Logger
27
28 def block(blocker, blocked) do
29 with {:ok, block_data, _} <- Builder.block(blocker, blocked),
30 {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
31 {:ok, block}
32 end
33 end
34
35 def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
36 with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
37 :ok <- validate_chat_content_length(content, !!maybe_attachment),
38 {_, {:ok, chat_message_data, _meta}} <-
39 {:build_object,
40 Builder.chat_message(
41 user,
42 recipient.ap_id,
43 content |> format_chat_content,
44 attachment: maybe_attachment
45 )},
46 {_, {:ok, create_activity_data, _meta}} <-
47 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
48 {_, {:ok, %Activity{} = activity, _meta}} <-
49 {:common_pipeline,
50 Pipeline.common_pipeline(create_activity_data,
51 local: true
52 )} do
53 {:ok, activity}
54 end
55 end
56
57 defp format_chat_content(nil), do: nil
58
59 defp format_chat_content(content) do
60 {text, _, _} =
61 content
62 |> Formatter.html_escape("text/plain")
63 |> Formatter.linkify()
64 |> (fn {text, mentions, tags} ->
65 {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
66 end).()
67
68 text
69 end
70
71 defp validate_chat_content_length(_, true), do: :ok
72 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
73
74 defp validate_chat_content_length(content, _) do
75 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
76 :ok
77 else
78 {:error, :content_too_long}
79 end
80 end
81
82 def unblock(blocker, blocked) do
83 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
84 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
85 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
86 {:ok, unblock}
87 else
88 {:fetch_block, nil} ->
89 if User.blocks?(blocker, blocked) do
90 User.unblock(blocker, blocked)
91 {:ok, :no_activity}
92 else
93 {:error, :not_blocking}
94 end
95
96 e ->
97 e
98 end
99 end
100
101 def follow(follower, followed) do
102 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
103
104 with {:ok, follow_data, _} <- Builder.follow(follower, followed),
105 {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
106 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
107 if activity.data["state"] == "reject" do
108 {:error, :rejected}
109 else
110 {:ok, follower, followed, activity}
111 end
112 end
113 end
114
115 def unfollow(follower, unfollowed) do
116 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
117 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
118 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
119 {:ok, follower}
120 end
121 end
122
123 def accept_follow_request(follower, followed) do
124 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
125 {:ok, follower} <- User.follow(follower, followed),
126 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
127 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
128 {:ok, _activity} <-
129 ActivityPub.accept(%{
130 to: [follower.ap_id],
131 actor: followed,
132 object: follow_activity.data["id"],
133 type: "Accept"
134 }) do
135 Notification.update_notification_type(followed, follow_activity)
136 {:ok, follower}
137 end
138 end
139
140 def reject_follow_request(follower, followed) do
141 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
142 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
143 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
144 {:ok, _notifications} <- Notification.dismiss(follow_activity),
145 {:ok, _activity} <-
146 ActivityPub.reject(%{
147 to: [follower.ap_id],
148 actor: followed,
149 object: follow_activity.data["id"],
150 type: "Reject"
151 }) do
152 {:ok, follower}
153 end
154 end
155
156 def delete(activity_id, user) do
157 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
158 {:find_activity, Activity.get_by_id(activity_id)},
159 {_, %Object{} = object, _} <-
160 {:find_object, Object.normalize(activity, false), activity},
161 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
162 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
163 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
164 {:ok, delete}
165 else
166 {:find_activity, _} ->
167 {:error, :not_found}
168
169 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
170 # We have the create activity, but not the object, it was probably pruned.
171 # Insert a tombstone and try again
172 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
173 {:ok, _tombstone} <- Object.create(tombstone_data) do
174 delete(activity_id, user)
175 else
176 _ ->
177 Logger.error(
178 "Could not insert tombstone for missing object on deletion. Object is #{object}."
179 )
180
181 {:error, dgettext("errors", "Could not delete")}
182 end
183
184 _ ->
185 {:error, dgettext("errors", "Could not delete")}
186 end
187 end
188
189 def repeat(id, user, params \\ %{}) do
190 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
191 object = %Object{} <- Object.normalize(activity, false),
192 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
193 public = public_announce?(object, params),
194 {:ok, announce, _} <- Builder.announce(user, object, public: public),
195 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
196 {:ok, activity}
197 else
198 {:existing_announce, %Activity{} = announce} ->
199 {:ok, announce}
200
201 _ ->
202 {:error, :not_found}
203 end
204 end
205
206 def unrepeat(id, user) do
207 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
208 {:find_activity, Activity.get_by_id(id)},
209 %Object{} = note <- Object.normalize(activity, false),
210 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
211 {:ok, undo, _} <- Builder.undo(user, announce),
212 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
213 {:ok, activity}
214 else
215 {:find_activity, _} -> {:error, :not_found}
216 _ -> {:error, dgettext("errors", "Could not unrepeat")}
217 end
218 end
219
220 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
221 def favorite(%User{} = user, id) do
222 case favorite_helper(user, id) do
223 {:ok, _} = res ->
224 res
225
226 {:error, :not_found} = res ->
227 res
228
229 {:error, e} ->
230 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
231 {:error, dgettext("errors", "Could not favorite")}
232 end
233 end
234
235 def favorite_helper(user, id) do
236 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
237 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
238 {_, {:ok, %Activity{} = activity, _meta}} <-
239 {:common_pipeline,
240 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
241 {:ok, activity}
242 else
243 {:find_object, _} ->
244 {:error, :not_found}
245
246 {:common_pipeline,
247 {
248 :error,
249 {
250 :validate_object,
251 {
252 :error,
253 changeset
254 }
255 }
256 }} = e ->
257 if {:object, {"already liked by this actor", []}} in changeset.errors do
258 {:ok, :already_liked}
259 else
260 {:error, e}
261 end
262
263 e ->
264 {:error, e}
265 end
266 end
267
268 def unfavorite(id, user) do
269 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
270 {:find_activity, Activity.get_by_id(id)},
271 %Object{} = note <- Object.normalize(activity, false),
272 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
273 {:ok, undo, _} <- Builder.undo(user, like),
274 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
275 {:ok, activity}
276 else
277 {:find_activity, _} -> {:error, :not_found}
278 _ -> {:error, dgettext("errors", "Could not unfavorite")}
279 end
280 end
281
282 def react_with_emoji(id, user, emoji) do
283 with %Activity{} = activity <- Activity.get_by_id(id),
284 object <- Object.normalize(activity),
285 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
286 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
287 {:ok, activity}
288 else
289 _ ->
290 {:error, dgettext("errors", "Could not add reaction emoji")}
291 end
292 end
293
294 def unreact_with_emoji(id, user, emoji) do
295 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
296 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
297 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
298 {:ok, activity}
299 else
300 _ ->
301 {:error, dgettext("errors", "Could not remove reaction emoji")}
302 end
303 end
304
305 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
306 with :ok <- validate_not_author(object, user),
307 :ok <- validate_existing_votes(user, object),
308 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
309 answer_activities =
310 Enum.map(choices, fn index ->
311 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
312
313 {:ok, activity} =
314 ActivityPub.create(%{
315 to: answer_data["to"],
316 actor: user,
317 context: object.data["context"],
318 object: answer_data,
319 additional: %{"cc" => answer_data["cc"]}
320 })
321
322 activity
323 end)
324
325 object = Object.get_cached_by_ap_id(object.data["id"])
326 {:ok, answer_activities, object}
327 end
328 end
329
330 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
331 do: {:error, dgettext("errors", "Poll's author can't vote")}
332
333 defp validate_not_author(_, _), do: :ok
334
335 defp validate_existing_votes(%{ap_id: ap_id}, object) do
336 if Utils.get_existing_votes(ap_id, object) == [] do
337 :ok
338 else
339 {:error, dgettext("errors", "Already voted")}
340 end
341 end
342
343 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
344 when is_list(any_of) and any_of != [],
345 do: {any_of, Enum.count(any_of)}
346
347 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
348 when is_list(one_of) and one_of != [],
349 do: {one_of, 1}
350
351 defp normalize_and_validate_choices(choices, object) do
352 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
353 {options, max_count} = get_options_and_max_count(object)
354 count = Enum.count(options)
355
356 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
357 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
358 {:ok, options, choices}
359 else
360 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
361 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
362 end
363 end
364
365 def public_announce?(_, %{visibility: visibility})
366 when visibility in ~w{public unlisted private direct},
367 do: visibility in ~w(public unlisted)
368
369 def public_announce?(object, _) do
370 Visibility.is_public?(object)
371 end
372
373 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
374
375 def get_visibility(%{visibility: visibility}, in_reply_to, _)
376 when visibility in ~w{public unlisted private direct},
377 do: {visibility, get_replied_to_visibility(in_reply_to)}
378
379 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
380 visibility = {:list, String.to_integer(list_id)}
381 {visibility, get_replied_to_visibility(in_reply_to)}
382 end
383
384 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
385 visibility = get_replied_to_visibility(in_reply_to)
386 {visibility, visibility}
387 end
388
389 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
390
391 def get_replied_to_visibility(nil), do: nil
392
393 def get_replied_to_visibility(activity) do
394 with %Object{} = object <- Object.normalize(activity) do
395 Visibility.get_visibility(object)
396 end
397 end
398
399 def check_expiry_date({:ok, nil} = res), do: res
400
401 def check_expiry_date({:ok, in_seconds}) do
402 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
403
404 if ActivityExpiration.expires_late_enough?(expiry) do
405 {:ok, expiry}
406 else
407 {:error, "Expiry date is too soon"}
408 end
409 end
410
411 def check_expiry_date(expiry_str) do
412 Ecto.Type.cast(:integer, expiry_str)
413 |> check_expiry_date()
414 end
415
416 def listen(user, data) do
417 visibility = Map.get(data, :visibility, "public")
418
419 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
420 listen_data <-
421 data
422 |> Map.take([:album, :artist, :title, :length])
423 |> Map.new(fn {key, value} -> {to_string(key), value} end)
424 |> Map.put("type", "Audio")
425 |> Map.put("to", to)
426 |> Map.put("cc", cc)
427 |> Map.put("actor", user.ap_id),
428 {:ok, activity} <-
429 ActivityPub.listen(%{
430 actor: user,
431 to: to,
432 object: listen_data,
433 context: Utils.generate_context_id(),
434 additional: %{"cc" => cc}
435 }) do
436 {:ok, activity}
437 end
438 end
439
440 def post(user, %{status: _} = data) do
441 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
442 ActivityPub.create(draft.changes, draft.preview?)
443 end
444 end
445
446 def pin(id, %{ap_id: user_ap_id} = user) do
447 with %Activity{
448 actor: ^user_ap_id,
449 data: %{"type" => "Create"},
450 object: %Object{data: %{"type" => object_type}}
451 } = activity <- Activity.get_by_id_with_object(id),
452 true <- object_type in ["Note", "Article", "Question"],
453 true <- Visibility.is_public?(activity),
454 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
455 {:ok, activity}
456 else
457 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
458 _ -> {:error, dgettext("errors", "Could not pin")}
459 end
460 end
461
462 def unpin(id, user) do
463 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
464 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
465 {:ok, activity}
466 else
467 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
468 _ -> {:error, dgettext("errors", "Could not unpin")}
469 end
470 end
471
472 def add_mute(user, activity) do
473 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
474 {:ok, activity}
475 else
476 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
477 end
478 end
479
480 def remove_mute(user, activity) do
481 ThreadMute.remove_mute(user.id, activity.data["context"])
482 {:ok, activity}
483 end
484
485 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
486 when is_binary("context") do
487 ThreadMute.exists?(user_id, context)
488 end
489
490 def thread_muted?(_, _), do: false
491
492 def report(user, data) do
493 with {:ok, account} <- get_reported_account(data.account_id),
494 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
495 {:ok, statuses} <- get_report_statuses(account, data) do
496 ActivityPub.flag(%{
497 context: Utils.generate_context_id(),
498 actor: user,
499 account: account,
500 statuses: statuses,
501 content: content_html,
502 forward: Map.get(data, :forward, false)
503 })
504 end
505 end
506
507 defp get_reported_account(account_id) do
508 case User.get_cached_by_id(account_id) do
509 %User{} = account -> {:ok, account}
510 _ -> {:error, dgettext("errors", "Account not found")}
511 end
512 end
513
514 def update_report_state(activity_ids, state) when is_list(activity_ids) do
515 case Utils.update_report_state(activity_ids, state) do
516 :ok -> {:ok, activity_ids}
517 _ -> {:error, dgettext("errors", "Could not update state")}
518 end
519 end
520
521 def update_report_state(activity_id, state) do
522 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
523 Utils.update_report_state(activity, state)
524 else
525 nil -> {:error, :not_found}
526 _ -> {:error, dgettext("errors", "Could not update state")}
527 end
528 end
529
530 def update_activity_scope(activity_id, opts \\ %{}) do
531 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
532 {:ok, activity} <- toggle_sensitive(activity, opts) do
533 set_visibility(activity, opts)
534 else
535 nil -> {:error, :not_found}
536 {:error, reason} -> {:error, reason}
537 end
538 end
539
540 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
541 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
542 end
543
544 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
545 when is_boolean(sensitive) do
546 new_data = Map.put(object.data, "sensitive", sensitive)
547
548 {:ok, object} =
549 object
550 |> Object.change(%{data: new_data})
551 |> Object.update_and_set_cache()
552
553 {:ok, Map.put(activity, :object, object)}
554 end
555
556 defp toggle_sensitive(activity, _), do: {:ok, activity}
557
558 defp set_visibility(activity, %{visibility: visibility}) do
559 Utils.update_activity_visibility(activity, visibility)
560 end
561
562 defp set_visibility(activity, _), do: {:ok, activity}
563
564 def hide_reblogs(%User{} = user, %User{} = target) do
565 UserRelationship.create_reblog_mute(user, target)
566 end
567
568 def show_reblogs(%User{} = user, %User{} = target) do
569 UserRelationship.delete_reblog_mute(user, target)
570 end
571 end