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