Quote posting (#113)
[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 array_of_statuses do
410 %Schema{type: :array, items: Status, example: [Status.schema().example]}
411 end
412
413 defp create_request do
414 %Schema{
415 title: "StatusCreateRequest",
416 type: :object,
417 properties: %{
418 status: %Schema{
419 type: :string,
420 nullable: true,
421 description:
422 "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
423 },
424 media_ids: %Schema{
425 nullable: true,
426 type: :array,
427 items: %Schema{type: :string},
428 description: "Array of Attachment ids to be attached as media."
429 },
430 poll: poll_params(),
431 in_reply_to_id: %Schema{
432 nullable: true,
433 allOf: [FlakeID],
434 description: "ID of the status being replied to, if status is a reply"
435 },
436 sensitive: %Schema{
437 allOf: [BooleanLike],
438 nullable: true,
439 description: "Mark status and attached media as sensitive?"
440 },
441 spoiler_text: %Schema{
442 type: :string,
443 nullable: true,
444 description:
445 "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
446 },
447 scheduled_at: %Schema{
448 type: :string,
449 format: :"date-time",
450 nullable: true,
451 description:
452 "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."
453 },
454 language: %Schema{
455 type: :string,
456 nullable: true,
457 description: "ISO 639 language code for this status."
458 },
459 # Pleroma-specific properties:
460 preview: %Schema{
461 allOf: [BooleanLike],
462 nullable: true,
463 description:
464 "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"
465 },
466 content_type: %Schema{
467 type: :string,
468 nullable: true,
469 description:
470 "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."
471 },
472 to: %Schema{
473 type: :array,
474 nullable: true,
475 items: %Schema{type: :string},
476 description:
477 "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"
478 },
479 visibility: %Schema{
480 nullable: true,
481 anyOf: [
482 VisibilityScope,
483 %Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"}
484 ],
485 description:
486 "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`"
487 },
488 expires_in: %Schema{
489 nullable: true,
490 type: :integer,
491 description:
492 "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."
493 },
494 in_reply_to_conversation_id: %Schema{
495 nullable: true,
496 type: :string,
497 description:
498 "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`."
499 },
500 quote_id: %Schema{
501 nullable: true,
502 type: :string,
503 description: "Will quote a given status."
504 }
505 },
506 example: %{
507 "status" => "What time is it?",
508 "sensitive" => "false",
509 "poll" => %{
510 "options" => ["Cofe", "Adventure"],
511 "expires_in" => 420
512 }
513 }
514 }
515 end
516
517 def poll_params do
518 %Schema{
519 nullable: true,
520 type: :object,
521 required: [:options, :expires_in],
522 properties: %{
523 options: %Schema{
524 type: :array,
525 items: %Schema{type: :string},
526 description: "Array of possible answers. Must be provided with `poll[expires_in]`."
527 },
528 expires_in: %Schema{
529 type: :integer,
530 nullable: true,
531 description:
532 "Duration the poll should be open, in seconds. Must be provided with `poll[options]`"
533 },
534 multiple: %Schema{
535 allOf: [BooleanLike],
536 nullable: true,
537 description: "Allow multiple choices?"
538 },
539 hide_totals: %Schema{
540 allOf: [BooleanLike],
541 nullable: true,
542 description: "Hide vote counts until the poll ends?"
543 }
544 }
545 }
546 end
547
548 def id_param do
549 Operation.parameter(:id, :path, FlakeID, "Status ID",
550 example: "9umDrYheeY451cQnEe",
551 required: true
552 )
553 end
554
555 defp status_response do
556 Operation.response("Status", "application/json", Status)
557 end
558
559 defp context do
560 %Schema{
561 title: "StatusContext",
562 description:
563 "Represents the tree around a given status. Used for reconstructing threads of statuses.",
564 type: :object,
565 required: [:ancestors, :descendants],
566 properties: %{
567 ancestors: array_of_statuses(),
568 descendants: array_of_statuses()
569 },
570 example: %{
571 "ancestors" => [Status.schema().example],
572 "descendants" => [Status.schema().example]
573 }
574 }
575 end
576 end