802fbef3e90545a725d25e88a85afc7387b865e7
[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 card_operation do
327 %Operation{
328 tags: ["Retrieve status information"],
329 deprecated: true,
330 summary: "Preview card",
331 description: "Deprecated in favor of card property inlined on Status entity",
332 operationId: "StatusController.card",
333 parameters: [id_param()],
334 security: [%{"oAuth" => ["read:statuses"]}],
335 responses: %{
336 200 =>
337 Operation.response("Card", "application/json", %Schema{
338 type: :object,
339 nullable: true,
340 properties: %{
341 type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]},
342 provider_name: %Schema{type: :string, nullable: true},
343 provider_url: %Schema{type: :string, format: :uri},
344 url: %Schema{type: :string, format: :uri},
345 image: %Schema{type: :string, nullable: true, format: :uri},
346 title: %Schema{type: :string},
347 description: %Schema{type: :string}
348 }
349 })
350 }
351 }
352 end
353
354 def favourited_by_operation do
355 %Operation{
356 tags: ["Retrieve status information"],
357 summary: "Favourited by",
358 description: "View who favourited a given status",
359 operationId: "StatusController.favourited_by",
360 security: [%{"oAuth" => ["read:accounts"]}],
361 parameters: [id_param()],
362 responses: %{
363 200 =>
364 Operation.response(
365 "Array of Accounts",
366 "application/json",
367 AccountOperation.array_of_accounts()
368 ),
369 404 => Operation.response("Not Found", "application/json", ApiError)
370 }
371 }
372 end
373
374 def reblogged_by_operation do
375 %Operation{
376 tags: ["Retrieve status information"],
377 summary: "Reblogged by",
378 description: "View who reblogged a given status",
379 operationId: "StatusController.reblogged_by",
380 security: [%{"oAuth" => ["read:accounts"]}],
381 parameters: [id_param()],
382 responses: %{
383 200 =>
384 Operation.response(
385 "Array of Accounts",
386 "application/json",
387 AccountOperation.array_of_accounts()
388 ),
389 404 => Operation.response("Not Found", "application/json", ApiError)
390 }
391 }
392 end
393
394 def context_operation do
395 %Operation{
396 tags: ["Retrieve status information"],
397 summary: "Parent and child statuses",
398 description: "View statuses above and below this status in the thread",
399 operationId: "StatusController.context",
400 security: [%{"oAuth" => ["read:statuses"]}],
401 parameters: [id_param()],
402 responses: %{
403 200 => Operation.response("Context", "application/json", context())
404 }
405 }
406 end
407
408 def favourites_operation do
409 %Operation{
410 tags: ["Timelines"],
411 summary: "Favourited statuses",
412 description:
413 "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.",
414 operationId: "StatusController.favourites",
415 parameters: pagination_params(),
416 security: [%{"oAuth" => ["read:favourites"]}],
417 responses: %{
418 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
419 }
420 }
421 end
422
423 def bookmarks_operation do
424 %Operation{
425 tags: ["Timelines"],
426 summary: "Bookmarked statuses",
427 description: "Statuses the user has bookmarked",
428 operationId: "StatusController.bookmarks",
429 parameters: pagination_params(),
430 security: [%{"oAuth" => ["read:bookmarks"]}],
431 responses: %{
432 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
433 }
434 }
435 end
436
437 def array_of_statuses do
438 %Schema{type: :array, items: Status, example: [Status.schema().example]}
439 end
440
441 defp create_request do
442 %Schema{
443 title: "StatusCreateRequest",
444 type: :object,
445 properties: %{
446 status: %Schema{
447 type: :string,
448 nullable: true,
449 description:
450 "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
451 },
452 media_ids: %Schema{
453 nullable: true,
454 type: :array,
455 items: %Schema{type: :string},
456 description: "Array of Attachment ids to be attached as media."
457 },
458 poll: poll_params(),
459 in_reply_to_id: %Schema{
460 nullable: true,
461 allOf: [FlakeID],
462 description: "ID of the status being replied to, if status is a reply"
463 },
464 sensitive: %Schema{
465 allOf: [BooleanLike],
466 nullable: true,
467 description: "Mark status and attached media as sensitive?"
468 },
469 spoiler_text: %Schema{
470 type: :string,
471 nullable: true,
472 description:
473 "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
474 },
475 scheduled_at: %Schema{
476 type: :string,
477 format: :"date-time",
478 nullable: true,
479 description:
480 "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."
481 },
482 language: %Schema{
483 type: :string,
484 nullable: true,
485 description: "ISO 639 language code for this status."
486 },
487 # Pleroma-specific properties:
488 preview: %Schema{
489 allOf: [BooleanLike],
490 nullable: true,
491 description:
492 "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"
493 },
494 content_type: %Schema{
495 type: :string,
496 nullable: true,
497 description:
498 "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."
499 },
500 to: %Schema{
501 type: :array,
502 nullable: true,
503 items: %Schema{type: :string},
504 description:
505 "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"
506 },
507 visibility: %Schema{
508 nullable: true,
509 anyOf: [
510 VisibilityScope,
511 %Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"}
512 ],
513 description:
514 "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`"
515 },
516 expires_in: %Schema{
517 nullable: true,
518 type: :integer,
519 description:
520 "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."
521 },
522 in_reply_to_conversation_id: %Schema{
523 nullable: true,
524 type: :string,
525 description:
526 "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`."
527 }
528 },
529 example: %{
530 "status" => "What time is it?",
531 "sensitive" => "false",
532 "poll" => %{
533 "options" => ["Cofe", "Adventure"],
534 "expires_in" => 420
535 }
536 }
537 }
538 end
539
540 def poll_params do
541 %Schema{
542 nullable: true,
543 type: :object,
544 required: [:options, :expires_in],
545 properties: %{
546 options: %Schema{
547 type: :array,
548 items: %Schema{type: :string},
549 description: "Array of possible answers. Must be provided with `poll[expires_in]`."
550 },
551 expires_in: %Schema{
552 type: :integer,
553 nullable: true,
554 description:
555 "Duration the poll should be open, in seconds. Must be provided with `poll[options]`"
556 },
557 multiple: %Schema{
558 allOf: [BooleanLike],
559 nullable: true,
560 description: "Allow multiple choices?"
561 },
562 hide_totals: %Schema{
563 allOf: [BooleanLike],
564 nullable: true,
565 description: "Hide vote counts until the poll ends?"
566 }
567 }
568 }
569 end
570
571 def id_param do
572 Operation.parameter(:id, :path, FlakeID, "Status ID",
573 example: "9umDrYheeY451cQnEe",
574 required: true
575 )
576 end
577
578 defp status_response do
579 Operation.response("Status", "application/json", Status)
580 end
581
582 defp context do
583 %Schema{
584 title: "StatusContext",
585 description:
586 "Represents the tree around a given status. Used for reconstructing threads of statuses.",
587 type: :object,
588 required: [:ancestors, :descendants],
589 properties: %{
590 ancestors: array_of_statuses(),
591 descendants: array_of_statuses()
592 },
593 example: %{
594 "ancestors" => [Status.schema().example],
595 "descendants" => [Status.schema().example]
596 }
597 }
598 end
599 end