Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into feature/expire...
[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 requestBody:
227 request_body("Parameters", %Schema{
228 type: :object,
229 properties: %{
230 expires_in: %Schema{
231 type: :integer,
232 nullable: true,
233 description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
234 default: 0
235 }
236 }
237 }),
238 parameters: [
239 id_param(),
240 Operation.parameter(
241 :expires_in,
242 :query,
243 %Schema{type: :integer, default: 0},
244 "Expire the mute in `expires_in` seconds. Default 0 for infinity"
245 )
246 ],
247 responses: %{
248 200 => status_response(),
249 400 => Operation.response("Error", "application/json", ApiError)
250 }
251 }
252 end
253
254 def unmute_conversation_operation do
255 %Operation{
256 tags: ["Statuses"],
257 summary: "Unmute conversation",
258 security: [%{"oAuth" => ["write:mutes"]}],
259 description:
260 "Start receiving notifications again for the thread that this status is part of",
261 operationId: "StatusController.unmute_conversation",
262 parameters: [id_param()],
263 responses: %{
264 200 => status_response(),
265 400 => Operation.response("Error", "application/json", ApiError)
266 }
267 }
268 end
269
270 def card_operation do
271 %Operation{
272 tags: ["Statuses"],
273 deprecated: true,
274 summary: "Preview card",
275 description: "Deprecated in favor of card property inlined on Status entity",
276 operationId: "StatusController.card",
277 parameters: [id_param()],
278 security: [%{"oAuth" => ["read:statuses"]}],
279 responses: %{
280 200 =>
281 Operation.response("Card", "application/json", %Schema{
282 type: :object,
283 nullable: true,
284 properties: %{
285 type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]},
286 provider_name: %Schema{type: :string, nullable: true},
287 provider_url: %Schema{type: :string, format: :uri},
288 url: %Schema{type: :string, format: :uri},
289 image: %Schema{type: :string, nullable: true, format: :uri},
290 title: %Schema{type: :string},
291 description: %Schema{type: :string}
292 }
293 })
294 }
295 }
296 end
297
298 def favourited_by_operation do
299 %Operation{
300 tags: ["Statuses"],
301 summary: "Favourited by",
302 description: "View who favourited a given status",
303 operationId: "StatusController.favourited_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 reblogged_by_operation do
319 %Operation{
320 tags: ["Statuses"],
321 summary: "Boosted by",
322 description: "View who boosted a given status",
323 operationId: "StatusController.reblogged_by",
324 security: [%{"oAuth" => ["read:accounts"]}],
325 parameters: [id_param()],
326 responses: %{
327 200 =>
328 Operation.response(
329 "Array of Accounts",
330 "application/json",
331 AccountOperation.array_of_accounts()
332 ),
333 404 => Operation.response("Not Found", "application/json", ApiError)
334 }
335 }
336 end
337
338 def context_operation do
339 %Operation{
340 tags: ["Statuses"],
341 summary: "Parent and child statuses",
342 description: "View statuses above and below this status in the thread",
343 operationId: "StatusController.context",
344 security: [%{"oAuth" => ["read:statuses"]}],
345 parameters: [id_param()],
346 responses: %{
347 200 => Operation.response("Context", "application/json", context())
348 }
349 }
350 end
351
352 def favourites_operation do
353 %Operation{
354 tags: ["Statuses"],
355 summary: "Favourited statuses",
356 description:
357 "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.",
358 operationId: "StatusController.favourites",
359 parameters: pagination_params(),
360 security: [%{"oAuth" => ["read:favourites"]}],
361 responses: %{
362 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
363 }
364 }
365 end
366
367 def bookmarks_operation do
368 %Operation{
369 tags: ["Statuses"],
370 summary: "Bookmarked statuses",
371 description: "Statuses the user has bookmarked",
372 operationId: "StatusController.bookmarks",
373 parameters: pagination_params(),
374 security: [%{"oAuth" => ["read:bookmarks"]}],
375 responses: %{
376 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
377 }
378 }
379 end
380
381 def array_of_statuses do
382 %Schema{type: :array, items: Status, example: [Status.schema().example]}
383 end
384
385 defp create_request do
386 %Schema{
387 title: "StatusCreateRequest",
388 type: :object,
389 properties: %{
390 status: %Schema{
391 type: :string,
392 nullable: true,
393 description:
394 "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
395 },
396 media_ids: %Schema{
397 nullable: true,
398 type: :array,
399 items: %Schema{type: :string},
400 description: "Array of Attachment ids to be attached as media."
401 },
402 poll: %Schema{
403 nullable: true,
404 type: :object,
405 required: [:options],
406 properties: %{
407 options: %Schema{
408 type: :array,
409 items: %Schema{type: :string},
410 description: "Array of possible answers. Must be provided with `poll[expires_in]`."
411 },
412 expires_in: %Schema{
413 type: :integer,
414 nullable: true,
415 description:
416 "Duration the poll should be open, in seconds. Must be provided with `poll[options]`"
417 },
418 multiple: %Schema{
419 allOf: [BooleanLike],
420 nullable: true,
421 description: "Allow multiple choices?"
422 },
423 hide_totals: %Schema{
424 allOf: [BooleanLike],
425 nullable: true,
426 description: "Hide vote counts until the poll ends?"
427 }
428 }
429 },
430 in_reply_to_id: %Schema{
431 nullable: true,
432 allOf: [FlakeID],
433 description: "ID of the status being replied to, if status is a reply"
434 },
435 sensitive: %Schema{
436 allOf: [BooleanLike],
437 nullable: true,
438 description: "Mark status and attached media as sensitive?"
439 },
440 spoiler_text: %Schema{
441 type: :string,
442 nullable: true,
443 description:
444 "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
445 },
446 scheduled_at: %Schema{
447 type: :string,
448 format: :"date-time",
449 nullable: true,
450 description:
451 "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."
452 },
453 language: %Schema{
454 type: :string,
455 nullable: true,
456 description: "ISO 639 language code for this status."
457 },
458 # Pleroma-specific properties:
459 preview: %Schema{
460 allOf: [BooleanLike],
461 nullable: true,
462 description:
463 "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"
464 },
465 content_type: %Schema{
466 type: :string,
467 nullable: true,
468 description:
469 "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."
470 },
471 to: %Schema{
472 type: :array,
473 nullable: true,
474 items: %Schema{type: :string},
475 description:
476 "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"
477 },
478 visibility: %Schema{
479 nullable: true,
480 anyOf: [
481 VisibilityScope,
482 %Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"}
483 ],
484 description:
485 "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`"
486 },
487 expires_in: %Schema{
488 nullable: true,
489 type: :integer,
490 description:
491 "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."
492 },
493 in_reply_to_conversation_id: %Schema{
494 nullable: true,
495 type: :string,
496 description:
497 "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`."
498 }
499 },
500 example: %{
501 "status" => "What time is it?",
502 "sensitive" => "false",
503 "poll" => %{
504 "options" => ["Cofe", "Adventure"],
505 "expires_in" => 420
506 }
507 }
508 }
509 end
510
511 def id_param do
512 Operation.parameter(:id, :path, FlakeID, "Status ID",
513 example: "9umDrYheeY451cQnEe",
514 required: true
515 )
516 end
517
518 defp status_response do
519 Operation.response("Status", "application/json", Status)
520 end
521
522 defp context do
523 %Schema{
524 title: "StatusContext",
525 description:
526 "Represents the tree around a given status. Used for reconstructing threads of statuses.",
527 type: :object,
528 required: [:ancestors, :descendants],
529 properties: %{
530 ancestors: array_of_statuses(),
531 descendants: array_of_statuses()
532 },
533 example: %{
534 "ancestors" => [Status.schema().example],
535 "descendants" => [Status.schema().example]
536 }
537 }
538 end
539 end