Merge branch 'show-media-endpoint-fixes' into 'develop'
[akkoma] / lib / pleroma / web / api_spec / operations / status_operation.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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.FlakeID
11 alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
12 alias Pleroma.Web.ApiSpec.Schemas.Status
13 alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
14
15 import Pleroma.Web.ApiSpec.Helpers
16
17 def open_api_operation(action) do
18 operation = String.to_existing_atom("#{action}_operation")
19 apply(__MODULE__, operation, [])
20 end
21
22 def index_operation do
23 %Operation{
24 tags: ["Statuses"],
25 summary: "Get multiple statuses by IDs",
26 security: [%{"oAuth" => ["read:statuses"]}],
27 parameters: [
28 Operation.parameter(
29 :ids,
30 :query,
31 %Schema{type: :array, items: FlakeID},
32 "Array of status IDs"
33 )
34 ],
35 operationId: "StatusController.index",
36 responses: %{
37 200 => Operation.response("Array of Status", "application/json", array_of_statuses())
38 }
39 }
40 end
41
42 def create_operation do
43 %Operation{
44 tags: ["Statuses"],
45 summary: "Publish new status",
46 security: [%{"oAuth" => ["write:statuses"]}],
47 description: "Post a new status",
48 operationId: "StatusController.create",
49 requestBody: request_body("Parameters", create_request(), required: true),
50 responses: %{
51 200 =>
52 Operation.response(
53 "Status. When `scheduled_at` is present, ScheduledStatus is returned instead",
54 "application/json",
55 %Schema{oneOf: [Status, ScheduledStatus]}
56 ),
57 422 => Operation.response("Bad Request", "application/json", ApiError)
58 }
59 }
60 end
61
62 def show_operation do
63 %Operation{
64 tags: ["Statuses"],
65 summary: "View specific status",
66 description: "View information about a status",
67 operationId: "StatusController.show",
68 security: [%{"oAuth" => ["read:statuses"]}],
69 parameters: [id_param()],
70 responses: %{
71 200 => status_response(),
72 404 => Operation.response("Not Found", "application/json", ApiError)
73 }
74 }
75 end
76
77 def delete_operation do
78 %Operation{
79 tags: ["Statuses"],
80 summary: "Delete status",
81 security: [%{"oAuth" => ["write:statuses"]}],
82 description: "Delete one of your own statuses",
83 operationId: "StatusController.delete",
84 parameters: [id_param()],
85 responses: %{
86 200 => empty_object_response(),
87 403 => Operation.response("Forbidden", "application/json", ApiError),
88 404 => Operation.response("Not Found", "application/json", ApiError)
89 }
90 }
91 end
92
93 def reblog_operation do
94 %Operation{
95 tags: ["Statuses"],
96 summary: "Boost",
97 security: [%{"oAuth" => ["write:statuses"]}],
98 description: "Share a status",
99 operationId: "StatusController.reblog",
100 parameters: [id_param()],
101 requestBody:
102 request_body("Parameters", %Schema{
103 type: :object,
104 properties: %{
105 visibility: %Schema{allOf: [VisibilityScope], default: "public"}
106 }
107 }),
108 responses: %{
109 200 => status_response(),
110 404 => Operation.response("Not Found", "application/json", ApiError)
111 }
112 }
113 end
114
115 def unreblog_operation do
116 %Operation{
117 tags: ["Statuses"],
118 summary: "Undo boost",
119 security: [%{"oAuth" => ["write:statuses"]}],
120 description: "Undo a reshare of a status",
121 operationId: "StatusController.unreblog",
122 parameters: [id_param()],
123 responses: %{
124 200 => status_response(),
125 404 => Operation.response("Not Found", "application/json", ApiError)
126 }
127 }
128 end
129
130 def favourite_operation do
131 %Operation{
132 tags: ["Statuses"],
133 summary: "Favourite",
134 security: [%{"oAuth" => ["write:favourites"]}],
135 description: "Add a status to your favourites list",
136 operationId: "StatusController.favourite",
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 unfavourite_operation do
146 %Operation{
147 tags: ["Statuses"],
148 summary: "Undo favourite",
149 security: [%{"oAuth" => ["write:favourites"]}],
150 description: "Remove a status from your favourites list",
151 operationId: "StatusController.unfavourite",
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 pin_operation do
161 %Operation{
162 tags: ["Statuses"],
163 summary: "Pin to profile",
164 security: [%{"oAuth" => ["write:accounts"]}],
165 description: "Feature one of your own public statuses at the top of your profile",
166 operationId: "StatusController.pin",
167 parameters: [id_param()],
168 responses: %{
169 200 => status_response(),
170 400 => Operation.response("Error", "application/json", ApiError)
171 }
172 }
173 end
174
175 def unpin_operation do
176 %Operation{
177 tags: ["Statuses"],
178 summary: "Unpin to profile",
179 security: [%{"oAuth" => ["write:accounts"]}],
180 description: "Unfeature a status from the top of your profile",
181 operationId: "StatusController.unpin",
182 parameters: [id_param()],
183 responses: %{
184 200 => status_response(),
185 400 => Operation.response("Error", "application/json", ApiError)
186 }
187 }
188 end
189
190 def bookmark_operation do
191 %Operation{
192 tags: ["Statuses"],
193 summary: "Bookmark",
194 security: [%{"oAuth" => ["write:bookmarks"]}],
195 description: "Privately bookmark a status",
196 operationId: "StatusController.bookmark",
197 parameters: [id_param()],
198 responses: %{
199 200 => status_response()
200 }
201 }
202 end
203
204 def unbookmark_operation do
205 %Operation{
206 tags: ["Statuses"],
207 summary: "Undo bookmark",
208 security: [%{"oAuth" => ["write:bookmarks"]}],
209 description: "Remove a status from your private bookmarks",
210 operationId: "StatusController.unbookmark",
211 parameters: [id_param()],
212 responses: %{
213 200 => status_response()
214 }
215 }
216 end
217
218 def mute_conversation_operation do
219 %Operation{
220 tags: ["Statuses"],
221 summary: "Mute conversation",
222 security: [%{"oAuth" => ["write:mutes"]}],
223 description: "Do not receive notifications for the thread that this status is part of.",
224 operationId: "StatusController.mute_conversation",
225 parameters: [id_param()],
226 responses: %{
227 200 => status_response(),
228 400 => Operation.response("Error", "application/json", ApiError)
229 }
230 }
231 end
232
233 def unmute_conversation_operation do
234 %Operation{
235 tags: ["Statuses"],
236 summary: "Unmute conversation",
237 security: [%{"oAuth" => ["write:mutes"]}],
238 description:
239 "Start receiving notifications again for the thread that this status is part of",
240 operationId: "StatusController.unmute_conversation",
241 parameters: [id_param()],
242 responses: %{
243 200 => status_response(),
244 400 => Operation.response("Error", "application/json", ApiError)
245 }
246 }
247 end
248
249 def card_operation do
250 %Operation{
251 tags: ["Statuses"],
252 deprecated: true,
253 summary: "Preview card",
254 description: "Deprecated in favor of card property inlined on Status entity",
255 operationId: "StatusController.card",
256 parameters: [id_param()],
257 security: [%{"oAuth" => ["read:statuses"]}],
258 responses: %{
259 200 =>
260 Operation.response("Card", "application/json", %Schema{
261 type: :object,
262 nullable: true,
263 properties: %{
264 type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]},
265 provider_name: %Schema{type: :string, nullable: true},
266 provider_url: %Schema{type: :string, format: :uri},
267 url: %Schema{type: :string, format: :uri},
268 image: %Schema{type: :string, nullable: true, format: :uri},
269 title: %Schema{type: :string},
270 description: %Schema{type: :string}
271 }
272 })
273 }
274 }
275 end
276
277 def favourited_by_operation do
278 %Operation{
279 tags: ["Statuses"],
280 summary: "Favourited by",
281 description: "View who favourited a given status",
282 operationId: "StatusController.favourited_by",
283 security: [%{"oAuth" => ["read:accounts"]}],
284 parameters: [id_param()],
285 responses: %{
286 200 =>
287 Operation.response(
288 "Array of Accounts",
289 "application/json",
290 AccountOperation.array_of_accounts()
291 ),
292 404 => Operation.response("Not Found", "application/json", ApiError)
293 }
294 }
295 end
296
297 def reblogged_by_operation do
298 %Operation{
299 tags: ["Statuses"],
300 summary: "Boosted by",
301 description: "View who boosted a given status",
302 operationId: "StatusController.reblogged_by",
303 security: [%{"oAuth" => ["read:accounts"]}],
304 parameters: [id_param()],
305 responses: %{
306 200 =>
307 Operation.response(
308 "Array of Accounts",
309 "application/json",
310 AccountOperation.array_of_accounts()
311 ),
312 404 => Operation.response("Not Found", "application/json", ApiError)
313 }
314 }
315 end
316
317 def context_operation do
318 %Operation{
319 tags: ["Statuses"],
320 summary: "Parent and child statuses",
321 description: "View statuses above and below this status in the thread",
322 operationId: "StatusController.context",
323 security: [%{"oAuth" => ["read:statuses"]}],
324 parameters: [id_param()],
325 responses: %{
326 200 => Operation.response("Context", "application/json", context())
327 }
328 }
329 end
330
331 def favourites_operation do
332 %Operation{
333 tags: ["Statuses"],
334 summary: "Favourited statuses",
335 description: "Statuses the user has favourited",
336 operationId: "StatusController.favourites",
337 parameters: pagination_params(),
338 security: [%{"oAuth" => ["read:favourites"]}],
339 responses: %{
340 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
341 }
342 }
343 end
344
345 def bookmarks_operation do
346 %Operation{
347 tags: ["Statuses"],
348 summary: "Bookmarked statuses",
349 description: "Statuses the user has bookmarked",
350 operationId: "StatusController.bookmarks",
351 parameters: pagination_params(),
352 security: [%{"oAuth" => ["read:bookmarks"]}],
353 responses: %{
354 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
355 }
356 }
357 end
358
359 def array_of_statuses do
360 %Schema{type: :array, items: Status, example: [Status.schema().example]}
361 end
362
363 defp create_request do
364 %Schema{
365 title: "StatusCreateRequest",
366 type: :object,
367 properties: %{
368 status: %Schema{
369 type: :string,
370 nullable: true,
371 description:
372 "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
373 },
374 media_ids: %Schema{
375 nullable: true,
376 type: :array,
377 items: %Schema{type: :string},
378 description: "Array of Attachment ids to be attached as media."
379 },
380 poll: %Schema{
381 nullable: true,
382 type: :object,
383 required: [:options],
384 properties: %{
385 options: %Schema{
386 type: :array,
387 items: %Schema{type: :string},
388 description: "Array of possible answers. Must be provided with `poll[expires_in]`."
389 },
390 expires_in: %Schema{
391 type: :integer,
392 nullable: true,
393 description:
394 "Duration the poll should be open, in seconds. Must be provided with `poll[options]`"
395 },
396 multiple: %Schema{
397 type: :boolean,
398 nullable: true,
399 description: "Allow multiple choices?"
400 },
401 hide_totals: %Schema{
402 type: :boolean,
403 nullable: true,
404 description: "Hide vote counts until the poll ends?"
405 }
406 }
407 },
408 in_reply_to_id: %Schema{
409 nullable: true,
410 allOf: [FlakeID],
411 description: "ID of the status being replied to, if status is a reply"
412 },
413 sensitive: %Schema{
414 type: :boolean,
415 nullable: true,
416 description: "Mark status and attached media as sensitive?"
417 },
418 spoiler_text: %Schema{
419 type: :string,
420 nullable: true,
421 description:
422 "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
423 },
424 scheduled_at: %Schema{
425 type: :string,
426 format: :"date-time",
427 nullable: true,
428 description:
429 "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."
430 },
431 language: %Schema{
432 type: :string,
433 nullable: true,
434 description: "ISO 639 language code for this status."
435 },
436 # Pleroma-specific properties:
437 preview: %Schema{
438 type: :boolean,
439 nullable: true,
440 description:
441 "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"
442 },
443 content_type: %Schema{
444 type: :string,
445 nullable: true,
446 description:
447 "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."
448 },
449 to: %Schema{
450 type: :array,
451 nullable: true,
452 items: %Schema{type: :string},
453 description:
454 "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"
455 },
456 visibility: %Schema{
457 nullable: true,
458 anyOf: [
459 VisibilityScope,
460 %Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"}
461 ],
462 description:
463 "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`"
464 },
465 expires_in: %Schema{
466 nullable: true,
467 type: :integer,
468 description:
469 "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."
470 },
471 in_reply_to_conversation_id: %Schema{
472 nullable: true,
473 type: :string,
474 description:
475 "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`."
476 }
477 },
478 example: %{
479 "status" => "What time is it?",
480 "sensitive" => "false",
481 "poll" => %{
482 "options" => ["Cofe", "Adventure"],
483 "expires_in" => 420
484 }
485 }
486 }
487 end
488
489 defp id_param do
490 Operation.parameter(:id, :path, FlakeID, "Status ID",
491 example: "9umDrYheeY451cQnEe",
492 required: true
493 )
494 end
495
496 defp status_response do
497 Operation.response("Status", "application/json", Status)
498 end
499
500 defp context do
501 %Schema{
502 title: "StatusContext",
503 description:
504 "Represents the tree around a given status. Used for reconstructing threads of statuses.",
505 type: :object,
506 required: [:ancestors, :descendants],
507 properties: %{
508 ancestors: array_of_statuses(),
509 descendants: array_of_statuses()
510 },
511 example: %{
512 "ancestors" => [Status.schema().example],
513 "descendants" => [Status.schema().example]
514 }
515 }
516 end
517 end