Merge pull request '2022.09 stable' (#208) from develop into stable
[akkoma] / lib / pleroma / web / api_spec / operations / status_operation.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ApiSpec.StatusOperation do
6 alias OpenApiSpex.Operation
7 alias OpenApiSpex.Schema
8 alias Pleroma.Web.ApiSpec.AccountOperation
9 alias Pleroma.Web.ApiSpec.Schemas.Account
10 alias Pleroma.Web.ApiSpec.Schemas.ApiError
11 alias Pleroma.Web.ApiSpec.Schemas.Attachment
12 alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
13 alias Pleroma.Web.ApiSpec.Schemas.Emoji
14 alias Pleroma.Web.ApiSpec.Schemas.FlakeID
15 alias Pleroma.Web.ApiSpec.Schemas.Poll
16 alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
17 alias Pleroma.Web.ApiSpec.Schemas.Status
18 alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
19
20 import Pleroma.Web.ApiSpec.Helpers
21
22 def open_api_operation(action) do
23 operation = String.to_existing_atom("#{action}_operation")
24 apply(__MODULE__, operation, [])
25 end
26
27 def index_operation do
28 %Operation{
29 tags: ["Retrieve status information"],
30 summary: "Multiple statuses",
31 security: [%{"oAuth" => ["read:statuses"]}],
32 parameters: [
33 Operation.parameter(
34 :ids,
35 :query,
36 %Schema{type: :array, items: FlakeID},
37 "Array of status IDs"
38 ),
39 Operation.parameter(
40 :with_muted,
41 :query,
42 BooleanLike,
43 "Include reactions from muted acccounts."
44 )
45 ],
46 operationId: "StatusController.index",
47 responses: %{
48 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
49 }
50 }
51 end
52
53 def create_operation do
54 %Operation{
55 tags: ["Status actions"],
56 summary: "Publish new status",
57 security: [%{"oAuth" => ["write:statuses"]}],
58 description: "Post a new status",
59 operationId: "StatusController.create",
60 requestBody: request_body("Parameters", create_request(), required: true),
61 responses: %{
62 200 =>
63 Operation.response(
64 "Status. When `scheduled_at` is present, ScheduledStatus is returned instead",
65 "application/json",
66 %Schema{anyOf: [Status, ScheduledStatus]}
67 ),
68 422 => Operation.response("Bad Request / MRF Rejection", "application/json", ApiError)
69 }
70 }
71 end
72
73 def show_operation do
74 %Operation{
75 tags: ["Retrieve status information"],
76 summary: "Status",
77 description: "View information about a status",
78 operationId: "StatusController.show",
79 security: [%{"oAuth" => ["read:statuses"]}],
80 parameters: [
81 id_param(),
82 Operation.parameter(
83 :with_muted,
84 :query,
85 BooleanLike,
86 "Include reactions from muted acccounts."
87 )
88 ],
89 responses: %{
90 200 => status_response(),
91 404 => Operation.response("Not Found", "application/json", ApiError)
92 }
93 }
94 end
95
96 def delete_operation do
97 %Operation{
98 tags: ["Status actions"],
99 summary: "Delete",
100 security: [%{"oAuth" => ["write:statuses"]}],
101 description: "Delete one of your own statuses",
102 operationId: "StatusController.delete",
103 parameters: [id_param()],
104 responses: %{
105 200 => status_response(),
106 403 => Operation.response("Forbidden", "application/json", ApiError),
107 404 => Operation.response("Not Found", "application/json", ApiError)
108 }
109 }
110 end
111
112 def reblog_operation do
113 %Operation{
114 tags: ["Status actions"],
115 summary: "Reblog",
116 security: [%{"oAuth" => ["write:statuses"]}],
117 description: "Share a status",
118 operationId: "StatusController.reblog",
119 parameters: [id_param()],
120 requestBody:
121 request_body("Parameters", %Schema{
122 type: :object,
123 properties: %{
124 visibility: %Schema{allOf: [VisibilityScope]}
125 }
126 }),
127 responses: %{
128 200 => status_response(),
129 404 => Operation.response("Not Found", "application/json", ApiError)
130 }
131 }
132 end
133
134 def unreblog_operation do
135 %Operation{
136 tags: ["Status actions"],
137 summary: "Undo reblog",
138 security: [%{"oAuth" => ["write:statuses"]}],
139 description: "Undo a reshare of a status",
140 operationId: "StatusController.unreblog",
141 parameters: [id_param()],
142 responses: %{
143 200 => status_response(),
144 404 => Operation.response("Not Found", "application/json", ApiError)
145 }
146 }
147 end
148
149 def favourite_operation do
150 %Operation{
151 tags: ["Status actions"],
152 summary: "Favourite",
153 security: [%{"oAuth" => ["write:favourites"]}],
154 description: "Add a status to your favourites list",
155 operationId: "StatusController.favourite",
156 parameters: [id_param()],
157 responses: %{
158 200 => status_response(),
159 404 => Operation.response("Not Found", "application/json", ApiError)
160 }
161 }
162 end
163
164 def unfavourite_operation do
165 %Operation{
166 tags: ["Status actions"],
167 summary: "Undo favourite",
168 security: [%{"oAuth" => ["write:favourites"]}],
169 description: "Remove a status from your favourites list",
170 operationId: "StatusController.unfavourite",
171 parameters: [id_param()],
172 responses: %{
173 200 => status_response(),
174 404 => Operation.response("Not Found", "application/json", ApiError)
175 }
176 }
177 end
178
179 def pin_operation do
180 %Operation{
181 tags: ["Status actions"],
182 summary: "Pin to profile",
183 security: [%{"oAuth" => ["write:accounts"]}],
184 description: "Feature one of your own public statuses at the top of your profile",
185 operationId: "StatusController.pin",
186 parameters: [id_param()],
187 responses: %{
188 200 => status_response(),
189 400 =>
190 Operation.response("Bad Request", "application/json", %Schema{
191 allOf: [ApiError],
192 title: "Unprocessable Entity",
193 example: %{
194 "error" => "You have already pinned the maximum number of statuses"
195 }
196 }),
197 404 =>
198 Operation.response("Not found", "application/json", %Schema{
199 allOf: [ApiError],
200 title: "Unprocessable Entity",
201 example: %{
202 "error" => "Record not found"
203 }
204 }),
205 422 =>
206 Operation.response(
207 "Unprocessable Entity",
208 "application/json",
209 %Schema{
210 allOf: [ApiError],
211 title: "Unprocessable Entity",
212 example: %{
213 "error" => "Someone else's status cannot be pinned"
214 }
215 }
216 )
217 }
218 }
219 end
220
221 def unpin_operation do
222 %Operation{
223 tags: ["Status actions"],
224 summary: "Unpin from profile",
225 security: [%{"oAuth" => ["write:accounts"]}],
226 description: "Unfeature a status from the top of your profile",
227 operationId: "StatusController.unpin",
228 parameters: [id_param()],
229 responses: %{
230 200 => status_response(),
231 400 =>
232 Operation.response("Bad Request", "application/json", %Schema{
233 allOf: [ApiError],
234 title: "Unprocessable Entity",
235 example: %{
236 "error" => "You have already pinned the maximum number of statuses"
237 }
238 }),
239 404 =>
240 Operation.response("Not found", "application/json", %Schema{
241 allOf: [ApiError],
242 title: "Unprocessable Entity",
243 example: %{
244 "error" => "Record not found"
245 }
246 })
247 }
248 }
249 end
250
251 def bookmark_operation do
252 %Operation{
253 tags: ["Status actions"],
254 summary: "Bookmark",
255 security: [%{"oAuth" => ["write:bookmarks"]}],
256 description: "Privately bookmark a status",
257 operationId: "StatusController.bookmark",
258 parameters: [id_param()],
259 responses: %{
260 200 => status_response()
261 }
262 }
263 end
264
265 def unbookmark_operation do
266 %Operation{
267 tags: ["Status actions"],
268 summary: "Undo bookmark",
269 security: [%{"oAuth" => ["write:bookmarks"]}],
270 description: "Remove a status from your private bookmarks",
271 operationId: "StatusController.unbookmark",
272 parameters: [id_param()],
273 responses: %{
274 200 => status_response()
275 }
276 }
277 end
278
279 def mute_conversation_operation do
280 %Operation{
281 tags: ["Status actions"],
282 summary: "Mute conversation",
283 security: [%{"oAuth" => ["write:mutes"]}],
284 description: "Do not receive notifications for the thread that this status is part of.",
285 operationId: "StatusController.mute_conversation",
286 requestBody:
287 request_body("Parameters", %Schema{
288 type: :object,
289 properties: %{
290 expires_in: %Schema{
291 type: :integer,
292 nullable: true,
293 description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
294 default: 0
295 }
296 }
297 }),
298 parameters: [
299 id_param(),
300 Operation.parameter(
301 :expires_in,
302 :query,
303 %Schema{type: :integer, default: 0},
304 "Expire the mute in `expires_in` seconds. Default 0 for infinity"
305 )
306 ],
307 responses: %{
308 200 => status_response(),
309 400 => Operation.response("Error", "application/json", ApiError)
310 }
311 }
312 end
313
314 def unmute_conversation_operation do
315 %Operation{
316 tags: ["Status actions"],
317 summary: "Unmute conversation",
318 security: [%{"oAuth" => ["write:mutes"]}],
319 description:
320 "Start receiving notifications again for the thread that this status is part of",
321 operationId: "StatusController.unmute_conversation",
322 parameters: [id_param()],
323 responses: %{
324 200 => status_response(),
325 400 => Operation.response("Error", "application/json", ApiError)
326 }
327 }
328 end
329
330 def favourited_by_operation do
331 %Operation{
332 tags: ["Retrieve status information"],
333 summary: "Favourited by",
334 description: "View who favourited a given status",
335 operationId: "StatusController.favourited_by",
336 security: [%{"oAuth" => ["read:accounts"]}],
337 parameters: [id_param()],
338 responses: %{
339 200 =>
340 Operation.response(
341 "Array of Accounts",
342 "application/json",
343 AccountOperation.array_of_accounts()
344 ),
345 404 => Operation.response("Not Found", "application/json", ApiError)
346 }
347 }
348 end
349
350 def reblogged_by_operation do
351 %Operation{
352 tags: ["Retrieve status information"],
353 summary: "Reblogged by",
354 description: "View who reblogged a given status",
355 operationId: "StatusController.reblogged_by",
356 security: [%{"oAuth" => ["read:accounts"]}],
357 parameters: [id_param()],
358 responses: %{
359 200 =>
360 Operation.response(
361 "Array of Accounts",
362 "application/json",
363 AccountOperation.array_of_accounts()
364 ),
365 404 => Operation.response("Not Found", "application/json", ApiError)
366 }
367 }
368 end
369
370 def context_operation do
371 %Operation{
372 tags: ["Retrieve status information"],
373 summary: "Parent and child statuses",
374 description: "View statuses above and below this status in the thread",
375 operationId: "StatusController.context",
376 security: [%{"oAuth" => ["read:statuses"]}],
377 parameters: [id_param()],
378 responses: %{
379 200 => Operation.response("Context", "application/json", context())
380 }
381 }
382 end
383
384 def favourites_operation do
385 %Operation{
386 tags: ["Timelines"],
387 summary: "Favourited statuses",
388 description:
389 "Statuses the user has favourited. Please note that you have to use the link headers to paginate this. You can not build the query parameters yourself.",
390 operationId: "StatusController.favourites",
391 parameters: pagination_params(),
392 security: [%{"oAuth" => ["read:favourites"]}],
393 responses: %{
394 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
395 }
396 }
397 end
398
399 def bookmarks_operation do
400 %Operation{
401 tags: ["Timelines"],
402 summary: "Bookmarked statuses",
403 description: "Statuses the user has bookmarked",
404 operationId: "StatusController.bookmarks",
405 parameters: pagination_params(),
406 security: [%{"oAuth" => ["read:bookmarks"]}],
407 responses: %{
408 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
409 }
410 }
411 end
412
413 def translate_operation do
414 %Operation{
415 tags: ["Retrieve status translation"],
416 summary: "Translate status",
417 description: "View the translation of a given status",
418 operationId: "StatusController.translation",
419 security: [%{"oAuth" => ["read:statuses"]}],
420 parameters: [id_param(), language_param(), source_language_param()],
421 responses: %{
422 200 => Operation.response("Translation", "application/json", translation()),
423 400 => Operation.response("Error", "application/json", ApiError),
424 404 => Operation.response("Not Found", "application/json", ApiError)
425 }
426 }
427 end
428
429 def show_history_operation do
430 %Operation{
431 tags: ["Retrieve status history"],
432 summary: "Status history",
433 description: "View history of a status",
434 operationId: "StatusController.show_history",
435 security: [%{"oAuth" => ["read:statuses"]}],
436 parameters: [
437 id_param()
438 ],
439 responses: %{
440 200 => status_history_response(),
441 404 => Operation.response("Not Found", "application/json", ApiError)
442 }
443 }
444 end
445
446 def show_source_operation do
447 %Operation{
448 tags: ["Retrieve status source"],
449 summary: "Status source",
450 description: "View source of a status",
451 operationId: "StatusController.show_source",
452 security: [%{"oAuth" => ["read:statuses"]}],
453 parameters: [
454 id_param()
455 ],
456 responses: %{
457 200 => status_source_response(),
458 404 => Operation.response("Not Found", "application/json", ApiError)
459 }
460 }
461 end
462
463 def update_operation do
464 %Operation{
465 tags: ["Update status"],
466 summary: "Update status",
467 description: "Change the content of a status",
468 operationId: "StatusController.update",
469 security: [%{"oAuth" => ["write:statuses"]}],
470 parameters: [
471 id_param()
472 ],
473 requestBody: request_body("Parameters", update_request(), required: true),
474 responses: %{
475 200 => status_response(),
476 403 => Operation.response("Forbidden", "application/json", ApiError),
477 404 => Operation.response("Not Found", "application/json", ApiError)
478 }
479 }
480 end
481
482 def array_of_statuses do
483 %Schema{type: :array, items: Status, example: [Status.schema().example]}
484 end
485
486 defp create_request do
487 %Schema{
488 title: "StatusCreateRequest",
489 type: :object,
490 properties: %{
491 status: %Schema{
492 type: :string,
493 nullable: true,
494 description:
495 "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
496 },
497 media_ids: %Schema{
498 nullable: true,
499 type: :array,
500 items: %Schema{type: :string},
501 description: "Array of Attachment ids to be attached as media."
502 },
503 poll: poll_params(),
504 in_reply_to_id: %Schema{
505 nullable: true,
506 allOf: [FlakeID],
507 description: "ID of the status being replied to, if status is a reply"
508 },
509 sensitive: %Schema{
510 allOf: [BooleanLike],
511 nullable: true,
512 description: "Mark status and attached media as sensitive?"
513 },
514 spoiler_text: %Schema{
515 type: :string,
516 nullable: true,
517 description:
518 "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
519 },
520 scheduled_at: %Schema{
521 type: :string,
522 format: :"date-time",
523 nullable: true,
524 description:
525 "ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future."
526 },
527 language: %Schema{
528 type: :string,
529 nullable: true,
530 description: "ISO 639 language code for this status."
531 },
532 # Pleroma-specific properties:
533 preview: %Schema{
534 allOf: [BooleanLike],
535 nullable: true,
536 description:
537 "If set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example"
538 },
539 content_type: %Schema{
540 type: :string,
541 nullable: true,
542 description:
543 "The MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint."
544 },
545 to: %Schema{
546 type: :array,
547 nullable: true,
548 items: %Schema{type: :string},
549 description:
550 "A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply"
551 },
552 visibility: %Schema{
553 nullable: true,
554 anyOf: [
555 VisibilityScope,
556 %Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"}
557 ],
558 description:
559 "Visibility of the posted status. Besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`"
560 },
561 expires_in: %Schema{
562 nullable: true,
563 type: :integer,
564 description:
565 "The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour."
566 },
567 in_reply_to_conversation_id: %Schema{
568 nullable: true,
569 type: :string,
570 description:
571 "Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`."
572 },
573 quote_id: %Schema{
574 nullable: true,
575 type: :string,
576 description: "Will quote a given status."
577 }
578 },
579 example: %{
580 "status" => "What time is it?",
581 "sensitive" => "false",
582 "poll" => %{
583 "options" => ["Cofe", "Adventure"],
584 "expires_in" => 420
585 }
586 }
587 }
588 end
589
590 defp update_request do
591 %Schema{
592 title: "StatusUpdateRequest",
593 type: :object,
594 properties: %{
595 status: %Schema{
596 type: :string,
597 nullable: true,
598 description:
599 "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
600 },
601 media_ids: %Schema{
602 nullable: true,
603 type: :array,
604 items: %Schema{type: :string},
605 description: "Array of Attachment ids to be attached as media."
606 },
607 poll: poll_params(),
608 sensitive: %Schema{
609 allOf: [BooleanLike],
610 nullable: true,
611 description: "Mark status and attached media as sensitive?"
612 },
613 spoiler_text: %Schema{
614 type: :string,
615 nullable: true,
616 description:
617 "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
618 },
619 content_type: %Schema{
620 type: :string,
621 nullable: true,
622 description:
623 "The MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint."
624 },
625 to: %Schema{
626 type: :array,
627 nullable: true,
628 items: %Schema{type: :string},
629 description:
630 "A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply"
631 }
632 },
633 example: %{
634 "status" => "What time is it?",
635 "sensitive" => "false",
636 "poll" => %{
637 "options" => ["Cofe", "Adventure"],
638 "expires_in" => 420
639 }
640 }
641 }
642 end
643
644 def poll_params do
645 %Schema{
646 nullable: true,
647 type: :object,
648 required: [:options, :expires_in],
649 properties: %{
650 options: %Schema{
651 type: :array,
652 items: %Schema{type: :string},
653 description: "Array of possible answers. Must be provided with `poll[expires_in]`."
654 },
655 expires_in: %Schema{
656 type: :integer,
657 nullable: true,
658 description:
659 "Duration the poll should be open, in seconds. Must be provided with `poll[options]`"
660 },
661 multiple: %Schema{
662 allOf: [BooleanLike],
663 nullable: true,
664 description: "Allow multiple choices?"
665 },
666 hide_totals: %Schema{
667 allOf: [BooleanLike],
668 nullable: true,
669 description: "Hide vote counts until the poll ends?"
670 }
671 }
672 }
673 end
674
675 def id_param do
676 Operation.parameter(:id, :path, FlakeID, "Status ID",
677 example: "9umDrYheeY451cQnEe",
678 required: true
679 )
680 end
681
682 defp language_param do
683 Operation.parameter(:language, :path, :string, "ISO 639 language code", example: "en")
684 end
685
686 defp source_language_param do
687 Operation.parameter(:from, :query, :string, "ISO 639 language code", example: "en")
688 end
689
690 defp status_response do
691 Operation.response("Status", "application/json", Status)
692 end
693
694 defp status_history_response do
695 Operation.response(
696 "Status History",
697 "application/json",
698 %Schema{
699 title: "Status history",
700 description: "Response schema for history of a status",
701 type: :array,
702 items: %Schema{
703 type: :object,
704 properties: %{
705 account: %Schema{
706 allOf: [Account],
707 description: "The account that authored this status"
708 },
709 content: %Schema{
710 type: :string,
711 format: :html,
712 description: "HTML-encoded status content"
713 },
714 sensitive: %Schema{
715 type: :boolean,
716 description: "Is this status marked as sensitive content?"
717 },
718 spoiler_text: %Schema{
719 type: :string,
720 description:
721 "Subject or summary line, below which status content is collapsed until expanded"
722 },
723 created_at: %Schema{
724 type: :string,
725 format: "date-time",
726 description: "The date when this status was created"
727 },
728 media_attachments: %Schema{
729 type: :array,
730 items: Attachment,
731 description: "Media that is attached to this status"
732 },
733 emojis: %Schema{
734 type: :array,
735 items: Emoji,
736 description: "Custom emoji to be used when rendering status content"
737 },
738 poll: %Schema{
739 allOf: [Poll],
740 nullable: true,
741 description: "The poll attached to the status"
742 }
743 }
744 }
745 }
746 )
747 end
748
749 defp status_source_response do
750 Operation.response(
751 "Status Source",
752 "application/json",
753 %Schema{
754 type: :object,
755 properties: %{
756 id: FlakeID,
757 text: %Schema{
758 type: :string,
759 description: "Raw source of status content"
760 },
761 spoiler_text: %Schema{
762 type: :string,
763 description:
764 "Subject or summary line, below which status content is collapsed until expanded"
765 },
766 content_type: %Schema{
767 type: :string,
768 description: "The content type of the source"
769 }
770 }
771 }
772 )
773 end
774
775 defp context do
776 %Schema{
777 title: "StatusContext",
778 description:
779 "Represents the tree around a given status. Used for reconstructing threads of statuses.",
780 type: :object,
781 required: [:ancestors, :descendants],
782 properties: %{
783 ancestors: array_of_statuses(),
784 descendants: array_of_statuses()
785 },
786 example: %{
787 "ancestors" => [Status.schema().example],
788 "descendants" => [Status.schema().example]
789 }
790 }
791 end
792
793 defp translation do
794 %Schema{
795 title: "StatusTranslation",
796 description: "The translation of a status.",
797 type: :object,
798 required: [:detected_language, :text],
799 properties: %{
800 detected_language: %Schema{
801 type: :string,
802 description: "The detected language of the text"
803 },
804 text: %Schema{type: :string, description: "The translated text"}
805 }
806 }
807 end
808 end