total rebase
[anni] / lib / pleroma / web / api_spec / operations / status_operation.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 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.schema(),
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.schema(),
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       requestBody:
260         request_body("Parameters", %Schema{
261           title: "StatusUpdateRequest",
262           type: :object,
263           properties: %{
264             folder_id: %Schema{
265               nullable: true,
266               allOf: [FlakeID],
267               description: "ID of bookmarks folder, if any"
268             }
269           }
270         }),
271       responses: %{
272         200 => status_response()
273       }
274     }
275   end
276
277   def unbookmark_operation do
278     %Operation{
279       tags: ["Status actions"],
280       summary: "Undo bookmark",
281       security: [%{"oAuth" => ["write:bookmarks"]}],
282       description: "Remove a status from your private bookmarks",
283       operationId: "StatusController.unbookmark",
284       parameters: [id_param()],
285       responses: %{
286         200 => status_response()
287       }
288     }
289   end
290
291   def mute_conversation_operation do
292     %Operation{
293       tags: ["Status actions"],
294       summary: "Mute conversation",
295       security: [%{"oAuth" => ["write:mutes"]}],
296       description: "Do not receive notifications for the thread that this status is part of.",
297       operationId: "StatusController.mute_conversation",
298       requestBody:
299         request_body("Parameters", %Schema{
300           type: :object,
301           properties: %{
302             expires_in: %Schema{
303               type: :integer,
304               nullable: true,
305               description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
306               default: 0
307             }
308           }
309         }),
310       parameters: [
311         id_param(),
312         Operation.parameter(
313           :expires_in,
314           :query,
315           %Schema{type: :integer, default: 0},
316           "Expire the mute in `expires_in` seconds. Default 0 for infinity"
317         )
318       ],
319       responses: %{
320         200 => status_response(),
321         400 => Operation.response("Error", "application/json", ApiError)
322       }
323     }
324   end
325
326   def unmute_conversation_operation do
327     %Operation{
328       tags: ["Status actions"],
329       summary: "Unmute conversation",
330       security: [%{"oAuth" => ["write:mutes"]}],
331       description:
332         "Start receiving notifications again for the thread that this status is part of",
333       operationId: "StatusController.unmute_conversation",
334       parameters: [id_param()],
335       responses: %{
336         200 => status_response(),
337         400 => Operation.response("Error", "application/json", ApiError)
338       }
339     }
340   end
341
342   def card_operation do
343     %Operation{
344       tags: ["Retrieve status information"],
345       deprecated: true,
346       summary: "Preview card",
347       description: "Deprecated in favor of card property inlined on Status entity",
348       operationId: "StatusController.card",
349       parameters: [id_param()],
350       security: [%{"oAuth" => ["read:statuses"]}],
351       responses: %{
352         200 =>
353           Operation.response("Card", "application/json", %Schema{
354             type: :object,
355             nullable: true,
356             properties: %{
357               type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]},
358               provider_name: %Schema{type: :string, nullable: true},
359               provider_url: %Schema{type: :string, format: :uri},
360               url: %Schema{type: :string, format: :uri},
361               image: %Schema{type: :string, nullable: true, format: :uri},
362               title: %Schema{type: :string},
363               description: %Schema{type: :string}
364             }
365           })
366       }
367     }
368   end
369
370   def favourited_by_operation do
371     %Operation{
372       tags: ["Retrieve status information"],
373       summary: "Favourited by",
374       description: "View who favourited a given status",
375       operationId: "StatusController.favourited_by",
376       security: [%{"oAuth" => ["read:accounts"]}],
377       parameters: [id_param()],
378       responses: %{
379         200 =>
380           Operation.response(
381             "Array of Accounts",
382             "application/json",
383             AccountOperation.array_of_accounts()
384           ),
385         404 => Operation.response("Not Found", "application/json", ApiError)
386       }
387     }
388   end
389
390   def reblogged_by_operation do
391     %Operation{
392       tags: ["Retrieve status information"],
393       summary: "Reblogged by",
394       description: "View who reblogged a given status",
395       operationId: "StatusController.reblogged_by",
396       security: [%{"oAuth" => ["read:accounts"]}],
397       parameters: [id_param()],
398       responses: %{
399         200 =>
400           Operation.response(
401             "Array of Accounts",
402             "application/json",
403             AccountOperation.array_of_accounts()
404           ),
405         404 => Operation.response("Not Found", "application/json", ApiError)
406       }
407     }
408   end
409
410   def context_operation do
411     %Operation{
412       tags: ["Retrieve status information"],
413       summary: "Parent and child statuses",
414       description: "View statuses above and below this status in the thread",
415       operationId: "StatusController.context",
416       security: [%{"oAuth" => ["read:statuses"]}],
417       parameters: [id_param()],
418       responses: %{
419         200 => Operation.response("Context", "application/json", context())
420       }
421     }
422   end
423
424   def favourites_operation do
425     %Operation{
426       tags: ["Timelines"],
427       summary: "Favourited statuses",
428       description:
429         "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.",
430       operationId: "StatusController.favourites",
431       parameters: pagination_params(),
432       security: [%{"oAuth" => ["read:favourites"]}],
433       responses: %{
434         200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
435       }
436     }
437   end
438
439   def bookmarks_operation do
440     %Operation{
441       tags: ["Timelines"],
442       summary: "Bookmarked statuses",
443       description: "Statuses the user has bookmarked",
444       operationId: "StatusController.bookmarks",
445       parameters: [
446         Operation.parameter(
447           :folder_id,
448           :query,
449           FlakeID.schema(),
450           "If provided, only display bookmarks from given folder"
451         )
452         | pagination_params()
453       ],
454       security: [%{"oAuth" => ["read:bookmarks"]}],
455       responses: %{
456         200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
457       }
458     }
459   end
460
461   def show_history_operation do
462     %Operation{
463       tags: ["Retrieve status information"],
464       summary: "Status history",
465       description: "View history of a status",
466       operationId: "StatusController.show_history",
467       security: [%{"oAuth" => ["read:statuses"]}],
468       parameters: [
469         id_param()
470       ],
471       responses: %{
472         200 => status_history_response(),
473         404 => Operation.response("Not Found", "application/json", ApiError)
474       }
475     }
476   end
477
478   def show_source_operation do
479     %Operation{
480       tags: ["Retrieve status information"],
481       summary: "Status source",
482       description: "View source of a status",
483       operationId: "StatusController.show_source",
484       security: [%{"oAuth" => ["read:statuses"]}],
485       parameters: [
486         id_param()
487       ],
488       responses: %{
489         200 => status_source_response(),
490         404 => Operation.response("Not Found", "application/json", ApiError)
491       }
492     }
493   end
494
495   def update_operation do
496     %Operation{
497       tags: ["Status actions"],
498       summary: "Update status",
499       description: "Change the content of a status",
500       operationId: "StatusController.update",
501       security: [%{"oAuth" => ["write:statuses"]}],
502       parameters: [
503         id_param()
504       ],
505       requestBody: request_body("Parameters", update_request(), required: true),
506       responses: %{
507         200 => status_response(),
508         403 => Operation.response("Forbidden", "application/json", ApiError),
509         404 => Operation.response("Not Found", "application/json", ApiError)
510       }
511     }
512   end
513
514   def array_of_statuses do
515     %Schema{type: :array, items: Status, example: [Status.schema().example]}
516   end
517
518   defp create_request do
519     %Schema{
520       title: "StatusCreateRequest",
521       type: :object,
522       properties: %{
523         status: %Schema{
524           type: :string,
525           nullable: true,
526           description:
527             "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
528         },
529         media_ids: %Schema{
530           nullable: true,
531           type: :array,
532           items: %Schema{type: :string},
533           description: "Array of Attachment ids to be attached as media."
534         },
535         poll: poll_params(),
536         in_reply_to_id: %Schema{
537           nullable: true,
538           allOf: [FlakeID],
539           description: "ID of the status being replied to, if status is a reply"
540         },
541         sensitive: %Schema{
542           allOf: [BooleanLike],
543           nullable: true,
544           description: "Mark status and attached media as sensitive?"
545         },
546         spoiler_text: %Schema{
547           type: :string,
548           nullable: true,
549           description:
550             "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
551         },
552         scheduled_at: %Schema{
553           type: :string,
554           format: :"date-time",
555           nullable: true,
556           description:
557             "ISO 8601 Datetime at which to schedule a status. Providing this parameter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future."
558         },
559         language: %Schema{
560           type: :string,
561           nullable: true,
562           description: "ISO 639 language code for this status."
563         },
564         # Pleroma-specific properties:
565         preview: %Schema{
566           allOf: [BooleanLike],
567           nullable: true,
568           description:
569             "If set to `true` the post won't be actually posted, but the status entity would still be rendered back. This could be useful for previewing rich text/custom emoji, for example"
570         },
571         content_type: %Schema{
572           type: :string,
573           nullable: true,
574           description:
575             "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."
576         },
577         to: %Schema{
578           type: :array,
579           nullable: true,
580           items: %Schema{type: :string},
581           description:
582             "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"
583         },
584         visibility: %Schema{
585           nullable: true,
586           anyOf: [
587             VisibilityScope,
588             %Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"}
589           ],
590           description:
591             "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`"
592         },
593         expires_in: %Schema{
594           nullable: true,
595           type: :integer,
596           description:
597             "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."
598         },
599         in_reply_to_conversation_id: %Schema{
600           nullable: true,
601           type: :string,
602           description:
603             "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`."
604         },
605         quote_id: %Schema{
606           nullable: true,
607           allOf: [FlakeID],
608           description: "ID of the status being quoted, if any"
609         }
610       },
611       example: %{
612         "status" => "What time is it?",
613         "sensitive" => "false",
614         "poll" => %{
615           "options" => ["Cofe", "Adventure"],
616           "expires_in" => 420
617         }
618       }
619     }
620   end
621
622   defp update_request do
623     %Schema{
624       title: "StatusUpdateRequest",
625       type: :object,
626       properties: %{
627         status: %Schema{
628           type: :string,
629           nullable: true,
630           description:
631             "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
632         },
633         media_ids: %Schema{
634           nullable: true,
635           type: :array,
636           items: %Schema{type: :string},
637           description: "Array of Attachment ids to be attached as media."
638         },
639         poll: poll_params(),
640         sensitive: %Schema{
641           allOf: [BooleanLike],
642           nullable: true,
643           description: "Mark status and attached media as sensitive?"
644         },
645         spoiler_text: %Schema{
646           type: :string,
647           nullable: true,
648           description:
649             "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
650         },
651         content_type: %Schema{
652           type: :string,
653           nullable: true,
654           description:
655             "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."
656         },
657         to: %Schema{
658           type: :array,
659           nullable: true,
660           items: %Schema{type: :string},
661           description:
662             "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"
663         }
664       },
665       example: %{
666         "status" => "What time is it?",
667         "sensitive" => "false",
668         "poll" => %{
669           "options" => ["Cofe", "Adventure"],
670           "expires_in" => 420
671         }
672       }
673     }
674   end
675
676   def poll_params do
677     %Schema{
678       nullable: true,
679       type: :object,
680       required: [:options, :expires_in],
681       properties: %{
682         options: %Schema{
683           type: :array,
684           items: %Schema{type: :string},
685           description: "Array of possible answers. Must be provided with `poll[expires_in]`."
686         },
687         expires_in: %Schema{
688           type: :integer,
689           nullable: true,
690           description:
691             "Duration the poll should be open, in seconds. Must be provided with `poll[options]`"
692         },
693         multiple: %Schema{
694           allOf: [BooleanLike],
695           nullable: true,
696           description: "Allow multiple choices?"
697         },
698         hide_totals: %Schema{
699           allOf: [BooleanLike],
700           nullable: true,
701           description: "Hide vote counts until the poll ends?"
702         }
703       }
704     }
705   end
706
707   def id_param do
708     Operation.parameter(:id, :path, FlakeID.schema(), "Status ID",
709       example: "9umDrYheeY451cQnEe",
710       required: true
711     )
712   end
713
714   defp status_response do
715     Operation.response("Status", "application/json", Status)
716   end
717
718   defp status_history_response do
719     Operation.response(
720       "Status History",
721       "application/json",
722       %Schema{
723         title: "Status history",
724         description: "Response schema for history of a status",
725         type: :array,
726         items: %Schema{
727           type: :object,
728           properties: %{
729             account: %Schema{
730               allOf: [Account],
731               description: "The account that authored this status"
732             },
733             content: %Schema{
734               type: :string,
735               format: :html,
736               description: "HTML-encoded status content"
737             },
738             sensitive: %Schema{
739               type: :boolean,
740               description: "Is this status marked as sensitive content?"
741             },
742             spoiler_text: %Schema{
743               type: :string,
744               description:
745                 "Subject or summary line, below which status content is collapsed until expanded"
746             },
747             created_at: %Schema{
748               type: :string,
749               format: "date-time",
750               description: "The date when this status was created"
751             },
752             media_attachments: %Schema{
753               type: :array,
754               items: Attachment,
755               description: "Media that is attached to this status"
756             },
757             emojis: %Schema{
758               type: :array,
759               items: Emoji,
760               description: "Custom emoji to be used when rendering status content"
761             },
762             poll: %Schema{
763               allOf: [Poll],
764               nullable: true,
765               description: "The poll attached to the status"
766             }
767           }
768         }
769       }
770     )
771   end
772
773   defp status_source_response do
774     Operation.response(
775       "Status Source",
776       "application/json",
777       %Schema{
778         type: :object,
779         properties: %{
780           id: FlakeID,
781           text: %Schema{
782             type: :string,
783             description: "Raw source of status content"
784           },
785           spoiler_text: %Schema{
786             type: :string,
787             description:
788               "Subject or summary line, below which status content is collapsed until expanded"
789           },
790           content_type: %Schema{
791             type: :string,
792             description: "The content type of the source"
793           }
794         }
795       }
796     )
797   end
798
799   defp context do
800     %Schema{
801       title: "StatusContext",
802       description:
803         "Represents the tree around a given status. Used for reconstructing threads of statuses.",
804       type: :object,
805       required: [:ancestors, :descendants],
806       properties: %{
807         ancestors: array_of_statuses(),
808         descendants: array_of_statuses()
809       },
810       example: %{
811         "ancestors" => [Status.schema().example],
812         "descendants" => [Status.schema().example]
813       }
814     }
815   end
816 end