Allow listing languages, setting source language (#192)
[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.ApiError
10 alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
11 alias Pleroma.Web.ApiSpec.Schemas.FlakeID
12 alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
13 alias Pleroma.Web.ApiSpec.Schemas.Status
14 alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
15
16 import Pleroma.Web.ApiSpec.Helpers
17
18 def open_api_operation(action) do
19 operation = String.to_existing_atom("#{action}_operation")
20 apply(__MODULE__, operation, [])
21 end
22
23 def index_operation do
24 %Operation{
25 tags: ["Retrieve status information"],
26 summary: "Multiple statuses",
27 security: [%{"oAuth" => ["read:statuses"]}],
28 parameters: [
29 Operation.parameter(
30 :ids,
31 :query,
32 %Schema{type: :array, items: FlakeID},
33 "Array of status IDs"
34 ),
35 Operation.parameter(
36 :with_muted,
37 :query,
38 BooleanLike,
39 "Include reactions from muted acccounts."
40 )
41 ],
42 operationId: "StatusController.index",
43 responses: %{
44 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
45 }
46 }
47 end
48
49 def create_operation do
50 %Operation{
51 tags: ["Status actions"],
52 summary: "Publish new status",
53 security: [%{"oAuth" => ["write:statuses"]}],
54 description: "Post a new status",
55 operationId: "StatusController.create",
56 requestBody: request_body("Parameters", create_request(), required: true),
57 responses: %{
58 200 =>
59 Operation.response(
60 "Status. When `scheduled_at` is present, ScheduledStatus is returned instead",
61 "application/json",
62 %Schema{anyOf: [Status, ScheduledStatus]}
63 ),
64 422 => Operation.response("Bad Request / MRF Rejection", "application/json", ApiError)
65 }
66 }
67 end
68
69 def show_operation do
70 %Operation{
71 tags: ["Retrieve status information"],
72 summary: "Status",
73 description: "View information about a status",
74 operationId: "StatusController.show",
75 security: [%{"oAuth" => ["read:statuses"]}],
76 parameters: [
77 id_param(),
78 Operation.parameter(
79 :with_muted,
80 :query,
81 BooleanLike,
82 "Include reactions from muted acccounts."
83 )
84 ],
85 responses: %{
86 200 => status_response(),
87 404 => Operation.response("Not Found", "application/json", ApiError)
88 }
89 }
90 end
91
92 def delete_operation do
93 %Operation{
94 tags: ["Status actions"],
95 summary: "Delete",
96 security: [%{"oAuth" => ["write:statuses"]}],
97 description: "Delete one of your own statuses",
98 operationId: "StatusController.delete",
99 parameters: [id_param()],
100 responses: %{
101 200 => status_response(),
102 403 => Operation.response("Forbidden", "application/json", ApiError),
103 404 => Operation.response("Not Found", "application/json", ApiError)
104 }
105 }
106 end
107
108 def reblog_operation do
109 %Operation{
110 tags: ["Status actions"],
111 summary: "Reblog",
112 security: [%{"oAuth" => ["write:statuses"]}],
113 description: "Share a status",
114 operationId: "StatusController.reblog",
115 parameters: [id_param()],
116 requestBody:
117 request_body("Parameters", %Schema{
118 type: :object,
119 properties: %{
120 visibility: %Schema{allOf: [VisibilityScope]}
121 }
122 }),
123 responses: %{
124 200 => status_response(),
125 404 => Operation.response("Not Found", "application/json", ApiError)
126 }
127 }
128 end
129
130 def unreblog_operation do
131 %Operation{
132 tags: ["Status actions"],
133 summary: "Undo reblog",
134 security: [%{"oAuth" => ["write:statuses"]}],
135 description: "Undo a reshare of a status",
136 operationId: "StatusController.unreblog",
137 parameters: [id_param()],
138 responses: %{
139 200 => status_response(),
140 404 => Operation.response("Not Found", "application/json", ApiError)
141 }
142 }
143 end
144
145 def favourite_operation do
146 %Operation{
147 tags: ["Status actions"],
148 summary: "Favourite",
149 security: [%{"oAuth" => ["write:favourites"]}],
150 description: "Add a status to your favourites list",
151 operationId: "StatusController.favourite",
152 parameters: [id_param()],
153 responses: %{
154 200 => status_response(),
155 404 => Operation.response("Not Found", "application/json", ApiError)
156 }
157 }
158 end
159
160 def unfavourite_operation do
161 %Operation{
162 tags: ["Status actions"],
163 summary: "Undo favourite",
164 security: [%{"oAuth" => ["write:favourites"]}],
165 description: "Remove a status from your favourites list",
166 operationId: "StatusController.unfavourite",
167 parameters: [id_param()],
168 responses: %{
169 200 => status_response(),
170 404 => Operation.response("Not Found", "application/json", ApiError)
171 }
172 }
173 end
174
175 def pin_operation do
176 %Operation{
177 tags: ["Status actions"],
178 summary: "Pin to profile",
179 security: [%{"oAuth" => ["write:accounts"]}],
180 description: "Feature one of your own public statuses at the top of your profile",
181 operationId: "StatusController.pin",
182 parameters: [id_param()],
183 responses: %{
184 200 => status_response(),
185 400 =>
186 Operation.response("Bad Request", "application/json", %Schema{
187 allOf: [ApiError],
188 title: "Unprocessable Entity",
189 example: %{
190 "error" => "You have already pinned the maximum number of statuses"
191 }
192 }),
193 404 =>
194 Operation.response("Not found", "application/json", %Schema{
195 allOf: [ApiError],
196 title: "Unprocessable Entity",
197 example: %{
198 "error" => "Record not found"
199 }
200 }),
201 422 =>
202 Operation.response(
203 "Unprocessable Entity",
204 "application/json",
205 %Schema{
206 allOf: [ApiError],
207 title: "Unprocessable Entity",
208 example: %{
209 "error" => "Someone else's status cannot be pinned"
210 }
211 }
212 )
213 }
214 }
215 end
216
217 def unpin_operation do
218 %Operation{
219 tags: ["Status actions"],
220 summary: "Unpin from profile",
221 security: [%{"oAuth" => ["write:accounts"]}],
222 description: "Unfeature a status from the top of your profile",
223 operationId: "StatusController.unpin",
224 parameters: [id_param()],
225 responses: %{
226 200 => status_response(),
227 400 =>
228 Operation.response("Bad Request", "application/json", %Schema{
229 allOf: [ApiError],
230 title: "Unprocessable Entity",
231 example: %{
232 "error" => "You have already pinned the maximum number of statuses"
233 }
234 }),
235 404 =>
236 Operation.response("Not found", "application/json", %Schema{
237 allOf: [ApiError],
238 title: "Unprocessable Entity",
239 example: %{
240 "error" => "Record not found"
241 }
242 })
243 }
244 }
245 end
246
247 def bookmark_operation do
248 %Operation{
249 tags: ["Status actions"],
250 summary: "Bookmark",
251 security: [%{"oAuth" => ["write:bookmarks"]}],
252 description: "Privately bookmark a status",
253 operationId: "StatusController.bookmark",
254 parameters: [id_param()],
255 responses: %{
256 200 => status_response()
257 }
258 }
259 end
260
261 def unbookmark_operation do
262 %Operation{
263 tags: ["Status actions"],
264 summary: "Undo bookmark",
265 security: [%{"oAuth" => ["write:bookmarks"]}],
266 description: "Remove a status from your private bookmarks",
267 operationId: "StatusController.unbookmark",
268 parameters: [id_param()],
269 responses: %{
270 200 => status_response()
271 }
272 }
273 end
274
275 def mute_conversation_operation do
276 %Operation{
277 tags: ["Status actions"],
278 summary: "Mute conversation",
279 security: [%{"oAuth" => ["write:mutes"]}],
280 description: "Do not receive notifications for the thread that this status is part of.",
281 operationId: "StatusController.mute_conversation",
282 requestBody:
283 request_body("Parameters", %Schema{
284 type: :object,
285 properties: %{
286 expires_in: %Schema{
287 type: :integer,
288 nullable: true,
289 description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
290 default: 0
291 }
292 }
293 }),
294 parameters: [
295 id_param(),
296 Operation.parameter(
297 :expires_in,
298 :query,
299 %Schema{type: :integer, default: 0},
300 "Expire the mute in `expires_in` seconds. Default 0 for infinity"
301 )
302 ],
303 responses: %{
304 200 => status_response(),
305 400 => Operation.response("Error", "application/json", ApiError)
306 }
307 }
308 end
309
310 def unmute_conversation_operation do
311 %Operation{
312 tags: ["Status actions"],
313 summary: "Unmute conversation",
314 security: [%{"oAuth" => ["write:mutes"]}],
315 description:
316 "Start receiving notifications again for the thread that this status is part of",
317 operationId: "StatusController.unmute_conversation",
318 parameters: [id_param()],
319 responses: %{
320 200 => status_response(),
321 400 => Operation.response("Error", "application/json", ApiError)
322 }
323 }
324 end
325
326 def favourited_by_operation do
327 %Operation{
328 tags: ["Retrieve status information"],
329 summary: "Favourited by",
330 description: "View who favourited a given status",
331 operationId: "StatusController.favourited_by",
332 security: [%{"oAuth" => ["read:accounts"]}],
333 parameters: [id_param()],
334 responses: %{
335 200 =>
336 Operation.response(
337 "Array of Accounts",
338 "application/json",
339 AccountOperation.array_of_accounts()
340 ),
341 404 => Operation.response("Not Found", "application/json", ApiError)
342 }
343 }
344 end
345
346 def reblogged_by_operation do
347 %Operation{
348 tags: ["Retrieve status information"],
349 summary: "Reblogged by",
350 description: "View who reblogged a given status",
351 operationId: "StatusController.reblogged_by",
352 security: [%{"oAuth" => ["read:accounts"]}],
353 parameters: [id_param()],
354 responses: %{
355 200 =>
356 Operation.response(
357 "Array of Accounts",
358 "application/json",
359 AccountOperation.array_of_accounts()
360 ),
361 404 => Operation.response("Not Found", "application/json", ApiError)
362 }
363 }
364 end
365
366 def context_operation do
367 %Operation{
368 tags: ["Retrieve status information"],
369 summary: "Parent and child statuses",
370 description: "View statuses above and below this status in the thread",
371 operationId: "StatusController.context",
372 security: [%{"oAuth" => ["read:statuses"]}],
373 parameters: [id_param()],
374 responses: %{
375 200 => Operation.response("Context", "application/json", context())
376 }
377 }
378 end
379
380 def favourites_operation do
381 %Operation{
382 tags: ["Timelines"],
383 summary: "Favourited statuses",
384 description:
385 "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.",
386 operationId: "StatusController.favourites",
387 parameters: pagination_params(),
388 security: [%{"oAuth" => ["read:favourites"]}],
389 responses: %{
390 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
391 }
392 }
393 end
394
395 def bookmarks_operation do
396 %Operation{
397 tags: ["Timelines"],
398 summary: "Bookmarked statuses",
399 description: "Statuses the user has bookmarked",
400 operationId: "StatusController.bookmarks",
401 parameters: pagination_params(),
402 security: [%{"oAuth" => ["read:bookmarks"]}],
403 responses: %{
404 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
405 }
406 }
407 end
408
409 def translate_operation do
410 %Operation{
411 tags: ["Retrieve status translation"],
412 summary: "Translate status",
413 description: "View the translation of a given status",
414 operationId: "StatusController.translation",
415 security: [%{"oAuth" => ["read:statuses"]}],
416 parameters: [id_param(), language_param(), source_language_param()],
417 responses: %{
418 200 => Operation.response("Translation", "application/json", translation()),
419 400 => Operation.response("Error", "application/json", ApiError),
420 404 => Operation.response("Not Found", "application/json", ApiError)
421 }
422 }
423 end
424
425 def array_of_statuses do
426 %Schema{type: :array, items: Status, example: [Status.schema().example]}
427 end
428
429 defp create_request do
430 %Schema{
431 title: "StatusCreateRequest",
432 type: :object,
433 properties: %{
434 status: %Schema{
435 type: :string,
436 nullable: true,
437 description:
438 "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
439 },
440 media_ids: %Schema{
441 nullable: true,
442 type: :array,
443 items: %Schema{type: :string},
444 description: "Array of Attachment ids to be attached as media."
445 },
446 poll: poll_params(),
447 in_reply_to_id: %Schema{
448 nullable: true,
449 allOf: [FlakeID],
450 description: "ID of the status being replied to, if status is a reply"
451 },
452 sensitive: %Schema{
453 allOf: [BooleanLike],
454 nullable: true,
455 description: "Mark status and attached media as sensitive?"
456 },
457 spoiler_text: %Schema{
458 type: :string,
459 nullable: true,
460 description:
461 "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
462 },
463 scheduled_at: %Schema{
464 type: :string,
465 format: :"date-time",
466 nullable: true,
467 description:
468 "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."
469 },
470 language: %Schema{
471 type: :string,
472 nullable: true,
473 description: "ISO 639 language code for this status."
474 },
475 # Pleroma-specific properties:
476 preview: %Schema{
477 allOf: [BooleanLike],
478 nullable: true,
479 description:
480 "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"
481 },
482 content_type: %Schema{
483 type: :string,
484 nullable: true,
485 description:
486 "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."
487 },
488 to: %Schema{
489 type: :array,
490 nullable: true,
491 items: %Schema{type: :string},
492 description:
493 "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"
494 },
495 visibility: %Schema{
496 nullable: true,
497 anyOf: [
498 VisibilityScope,
499 %Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"}
500 ],
501 description:
502 "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`"
503 },
504 expires_in: %Schema{
505 nullable: true,
506 type: :integer,
507 description:
508 "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."
509 },
510 in_reply_to_conversation_id: %Schema{
511 nullable: true,
512 type: :string,
513 description:
514 "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`."
515 },
516 quote_id: %Schema{
517 nullable: true,
518 type: :string,
519 description: "Will quote a given status."
520 }
521 },
522 example: %{
523 "status" => "What time is it?",
524 "sensitive" => "false",
525 "poll" => %{
526 "options" => ["Cofe", "Adventure"],
527 "expires_in" => 420
528 }
529 }
530 }
531 end
532
533 def poll_params do
534 %Schema{
535 nullable: true,
536 type: :object,
537 required: [:options, :expires_in],
538 properties: %{
539 options: %Schema{
540 type: :array,
541 items: %Schema{type: :string},
542 description: "Array of possible answers. Must be provided with `poll[expires_in]`."
543 },
544 expires_in: %Schema{
545 type: :integer,
546 nullable: true,
547 description:
548 "Duration the poll should be open, in seconds. Must be provided with `poll[options]`"
549 },
550 multiple: %Schema{
551 allOf: [BooleanLike],
552 nullable: true,
553 description: "Allow multiple choices?"
554 },
555 hide_totals: %Schema{
556 allOf: [BooleanLike],
557 nullable: true,
558 description: "Hide vote counts until the poll ends?"
559 }
560 }
561 }
562 end
563
564 def id_param do
565 Operation.parameter(:id, :path, FlakeID, "Status ID",
566 example: "9umDrYheeY451cQnEe",
567 required: true
568 )
569 end
570
571 defp language_param do
572 Operation.parameter(:language, :path, :string, "ISO 639 language code", example: "en")
573 end
574
575 defp source_language_param do
576 Operation.parameter(:from, :query, :string, "ISO 639 language code", example: "en")
577 end
578
579 defp status_response do
580 Operation.response("Status", "application/json", Status)
581 end
582
583 defp context do
584 %Schema{
585 title: "StatusContext",
586 description:
587 "Represents the tree around a given status. Used for reconstructing threads of statuses.",
588 type: :object,
589 required: [:ancestors, :descendants],
590 properties: %{
591 ancestors: array_of_statuses(),
592 descendants: array_of_statuses()
593 },
594 example: %{
595 "ancestors" => [Status.schema().example],
596 "descendants" => [Status.schema().example]
597 }
598 }
599 end
600
601 defp translation do
602 %Schema{
603 title: "StatusTranslation",
604 description: "The translation of a status.",
605 type: :object,
606 required: [:detected_language, :text],
607 properties: %{
608 detected_language: %Schema{
609 type: :string,
610 description: "The detected language of the text"
611 },
612 text: %Schema{type: :string, description: "The translated text"}
613 }
614 }
615 end
616 end