this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs(api): update OpenAPI spec for v1 API

Update the OpenAPI specification to version 2.0.0 to document the
new v1 API endpoints:

- Add /api/v1/links endpoints (GET list, GET by ID, POST, DELETE)
- Add /api/v1/quotes endpoints (GET list, GET by ID, POST, DELETE)
- Add /api/v1/stats and /api/v1/users/{user}/stats endpoints
- Add /api/v1/search endpoint with type filtering
- Add /api/v1/cache endpoint for cache management
- Add /api/v1/kittens/daily endpoints (GET, PUT, DELETE)
- Add /r/{id} redirect shortlink endpoint

Other changes:
- Add X-API-Key security scheme
- Add reusable schema components (APIError, APIMeta, etc.)
- Use snake_case for all response field names
- Add tags for endpoint grouping
- Mark all legacy endpoints as deprecated

+1481 -444
+1481 -444
internal/assets/openapi.json
··· 2 2 "openapi": "3.0.0", 3 3 "info": { 4 4 "title": "Tumble API", 5 - "description": "API for the Tumble content application. Note: API endpoints use 308 (Permanent Redirect) instead of 301 for trailing slash redirects on POST/PUT/DELETE requests to preserve the HTTP method.", 6 - "version": "1.0.0" 5 + "description": "API for the Tumble content application. The v1 API provides a RESTful, versioned interface with consistent request/response formats. Legacy endpoints are deprecated and will be removed in a future release.", 6 + "version": "2.0.0" 7 7 }, 8 8 "servers": [ 9 9 { ··· 11 11 "description": "Local Server" 12 12 } 13 13 ], 14 + "components": { 15 + "securitySchemes": { 16 + "apiKey": { 17 + "type": "apiKey", 18 + "in": "header", 19 + "name": "X-API-Key", 20 + "description": "API key for protected endpoints" 21 + } 22 + }, 23 + "schemas": { 24 + "APIError": { 25 + "type": "object", 26 + "properties": { 27 + "error": { 28 + "type": "object", 29 + "properties": { 30 + "code": { 31 + "type": "string", 32 + "description": "Machine-readable error code", 33 + "enum": ["bad_request", "unauthorized", "forbidden", "not_found", "validation_error", "internal_error"] 34 + }, 35 + "message": { 36 + "type": "string", 37 + "description": "Human-readable error message" 38 + }, 39 + "details": { 40 + "type": "object", 41 + "additionalProperties": { 42 + "type": "string" 43 + }, 44 + "description": "Field-level validation errors (for validation_error code)" 45 + } 46 + }, 47 + "required": ["code", "message"] 48 + } 49 + }, 50 + "required": ["error"], 51 + "example": { 52 + "error": { 53 + "code": "not_found", 54 + "message": "Link 42 does not exist" 55 + } 56 + } 57 + }, 58 + "APIMeta": { 59 + "type": "object", 60 + "properties": { 61 + "total": { 62 + "type": "integer", 63 + "description": "Total number of items available" 64 + }, 65 + "limit": { 66 + "type": "integer", 67 + "description": "Maximum items per page" 68 + }, 69 + "offset": { 70 + "type": "integer", 71 + "description": "Number of items skipped" 72 + } 73 + }, 74 + "required": ["total", "limit", "offset"] 75 + }, 76 + "APILinkResponse": { 77 + "type": "object", 78 + "properties": { 79 + "id": { 80 + "type": "integer", 81 + "description": "Unique link ID" 82 + }, 83 + "url": { 84 + "type": "string", 85 + "description": "The target URL" 86 + }, 87 + "title": { 88 + "type": "string", 89 + "description": "Title of the linked page" 90 + }, 91 + "user": { 92 + "type": "string", 93 + "description": "Username who submitted the link" 94 + }, 95 + "clicks": { 96 + "type": "integer", 97 + "description": "Number of verified clicks" 98 + }, 99 + "created_at": { 100 + "type": "string", 101 + "format": "date-time", 102 + "description": "When the link was submitted" 103 + } 104 + }, 105 + "required": ["id", "url", "user", "created_at"], 106 + "example": { 107 + "id": 42, 108 + "url": "https://example.com/article", 109 + "title": "Example Article", 110 + "user": "alice", 111 + "clicks": 15, 112 + "created_at": "2026-01-15T10:30:00Z" 113 + } 114 + }, 115 + "APILinkCreateResponse": { 116 + "allOf": [ 117 + { "$ref": "#/components/schemas/APILinkResponse" }, 118 + { 119 + "type": "object", 120 + "properties": { 121 + "is_duplicate": { 122 + "type": "boolean", 123 + "description": "Whether this URL was previously submitted" 124 + }, 125 + "previous_submissions": { 126 + "type": "array", 127 + "description": "List of prior submissions if duplicate", 128 + "items": { 129 + "$ref": "#/components/schemas/APIPreviousSubmission" 130 + } 131 + } 132 + } 133 + } 134 + ], 135 + "example": { 136 + "id": 456, 137 + "url": "https://example.com/article", 138 + "title": "Example Article", 139 + "user": "bob", 140 + "clicks": 0, 141 + "created_at": "2026-02-08T14:30:00Z", 142 + "is_duplicate": true, 143 + "previous_submissions": [ 144 + { 145 + "id": 123, 146 + "user": "alice", 147 + "created_at": "2026-01-15T10:30:00Z", 148 + "title": "Example Article" 149 + } 150 + ] 151 + } 152 + }, 153 + "APIPreviousSubmission": { 154 + "type": "object", 155 + "properties": { 156 + "id": { 157 + "type": "integer", 158 + "description": "ID of the previous submission" 159 + }, 160 + "user": { 161 + "type": "string", 162 + "description": "Username who submitted previously" 163 + }, 164 + "created_at": { 165 + "type": "string", 166 + "format": "date-time", 167 + "description": "When the link was previously submitted" 168 + }, 169 + "title": { 170 + "type": "string", 171 + "description": "Title of the previous submission" 172 + } 173 + }, 174 + "required": ["id", "user", "created_at"] 175 + }, 176 + "APILinksListResponse": { 177 + "type": "object", 178 + "properties": { 179 + "data": { 180 + "type": "array", 181 + "items": { 182 + "$ref": "#/components/schemas/APILinkResponse" 183 + } 184 + }, 185 + "meta": { 186 + "$ref": "#/components/schemas/APIMeta" 187 + } 188 + }, 189 + "required": ["data", "meta"] 190 + }, 191 + "APIQuoteResponse": { 192 + "type": "object", 193 + "properties": { 194 + "id": { 195 + "type": "integer", 196 + "description": "Unique quote ID" 197 + }, 198 + "quote": { 199 + "type": "string", 200 + "description": "The quote text" 201 + }, 202 + "author": { 203 + "type": "string", 204 + "description": "Author of the quote" 205 + }, 206 + "poster": { 207 + "type": "string", 208 + "description": "Username who submitted the quote" 209 + }, 210 + "created_at": { 211 + "type": "string", 212 + "format": "date-time", 213 + "description": "When the quote was submitted" 214 + } 215 + }, 216 + "required": ["id", "quote", "created_at"], 217 + "example": { 218 + "id": 42, 219 + "quote": "The only way to do great work is to love what you do.", 220 + "author": "Steve Jobs", 221 + "poster": "alice", 222 + "created_at": "2026-01-15T10:30:00Z" 223 + } 224 + }, 225 + "APIQuotesListResponse": { 226 + "type": "object", 227 + "properties": { 228 + "data": { 229 + "type": "array", 230 + "items": { 231 + "$ref": "#/components/schemas/APIQuoteResponse" 232 + } 233 + }, 234 + "meta": { 235 + "$ref": "#/components/schemas/APIMeta" 236 + } 237 + }, 238 + "required": ["data", "meta"] 239 + }, 240 + "APISearchResponse": { 241 + "type": "object", 242 + "properties": { 243 + "links": { 244 + "type": "array", 245 + "items": { 246 + "$ref": "#/components/schemas/APILinkResponse" 247 + } 248 + }, 249 + "quotes": { 250 + "type": "array", 251 + "items": { 252 + "$ref": "#/components/schemas/APIQuoteResponse" 253 + } 254 + }, 255 + "meta": { 256 + "type": "object", 257 + "properties": { 258 + "total_links": { 259 + "type": "integer", 260 + "description": "Total matching links" 261 + }, 262 + "total_quotes": { 263 + "type": "integer", 264 + "description": "Total matching quotes" 265 + }, 266 + "limit": { 267 + "type": "integer" 268 + }, 269 + "offset": { 270 + "type": "integer" 271 + } 272 + } 273 + } 274 + }, 275 + "required": ["links", "quotes", "meta"] 276 + }, 277 + "APIStatsResponse": { 278 + "type": "object", 279 + "properties": { 280 + "site": { 281 + "type": "object", 282 + "properties": { 283 + "total_links": { 284 + "type": "integer", 285 + "description": "Total links on the site" 286 + }, 287 + "total_quotes": { 288 + "type": "integer", 289 + "description": "Total quotes on the site" 290 + }, 291 + "total_users": { 292 + "type": "integer", 293 + "description": "Total unique users" 294 + } 295 + }, 296 + "required": ["total_links", "total_quotes", "total_users"] 297 + }, 298 + "leaderboard": { 299 + "type": "array", 300 + "items": { 301 + "$ref": "#/components/schemas/APIUserStats" 302 + } 303 + }, 304 + "meta": { 305 + "$ref": "#/components/schemas/APIMeta" 306 + } 307 + }, 308 + "required": ["site", "leaderboard", "meta"], 309 + "example": { 310 + "site": { 311 + "total_links": 15000, 312 + "total_quotes": 3200, 313 + "total_users": 47 314 + }, 315 + "leaderboard": [ 316 + { "user": "alice", "link_count": 500, "quote_count": 120 }, 317 + { "user": "bob", "link_count": 450, "quote_count": 85 } 318 + ], 319 + "meta": { 320 + "total": 47, 321 + "limit": 50, 322 + "offset": 0 323 + } 324 + } 325 + }, 326 + "APIUserStats": { 327 + "type": "object", 328 + "properties": { 329 + "user": { 330 + "type": "string", 331 + "description": "Username" 332 + }, 333 + "link_count": { 334 + "type": "integer", 335 + "description": "Number of links submitted" 336 + }, 337 + "quote_count": { 338 + "type": "integer", 339 + "description": "Number of quotes submitted" 340 + } 341 + }, 342 + "required": ["user", "link_count", "quote_count"] 343 + }, 344 + "APIUserStatsResponse": { 345 + "type": "object", 346 + "properties": { 347 + "user": { 348 + "type": "string", 349 + "description": "Username" 350 + }, 351 + "link_count": { 352 + "type": "integer", 353 + "description": "Number of links submitted" 354 + }, 355 + "quote_count": { 356 + "type": "integer", 357 + "description": "Number of quotes submitted" 358 + } 359 + }, 360 + "required": ["user", "link_count", "quote_count"], 361 + "example": { 362 + "user": "alice", 363 + "link_count": 500, 364 + "quote_count": 120 365 + } 366 + }, 367 + "APICacheResponse": { 368 + "type": "object", 369 + "properties": { 370 + "cleared": { 371 + "type": "string", 372 + "description": "What was cleared ('all' or the specific URL)" 373 + }, 374 + "count": { 375 + "type": "integer", 376 + "description": "Number of entries cleared (only for 'all')" 377 + } 378 + }, 379 + "required": ["cleared"], 380 + "example": { 381 + "cleared": "all", 382 + "count": 47 383 + } 384 + }, 385 + "APIDailyKittenResponse": { 386 + "type": "object", 387 + "properties": { 388 + "url": { 389 + "type": "string", 390 + "description": "URL of the kitten image" 391 + }, 392 + "date": { 393 + "type": "string", 394 + "format": "date", 395 + "description": "Date for this kitten" 396 + }, 397 + "fetched": { 398 + "type": "boolean", 399 + "description": "Whether a new kitten was fetched (PUT only)" 400 + } 401 + }, 402 + "required": ["url", "date"], 403 + "example": { 404 + "url": "https://cataas.com/cat/abc123", 405 + "date": "2026-02-08" 406 + } 407 + }, 408 + "LinkCreateRequest": { 409 + "type": "object", 410 + "properties": { 411 + "url": { 412 + "type": "string", 413 + "description": "The URL to submit" 414 + }, 415 + "user": { 416 + "type": "string", 417 + "description": "Username of the submitter" 418 + } 419 + }, 420 + "required": ["url", "user"], 421 + "example": { 422 + "url": "https://example.com/article", 423 + "user": "alice" 424 + } 425 + }, 426 + "QuoteCreateRequest": { 427 + "type": "object", 428 + "properties": { 429 + "quote": { 430 + "type": "string", 431 + "description": "The quote text" 432 + }, 433 + "author": { 434 + "type": "string", 435 + "description": "Author of the quote" 436 + }, 437 + "poster": { 438 + "type": "string", 439 + "description": "Username of the submitter" 440 + } 441 + }, 442 + "required": ["quote"], 443 + "example": { 444 + "quote": "The only way to do great work is to love what you do.", 445 + "author": "Steve Jobs", 446 + "poster": "alice" 447 + } 448 + } 449 + } 450 + }, 14 451 "paths": { 15 - "/api/caching/invalidate": { 452 + "/r/{id}": { 453 + "get": { 454 + "summary": "Redirect to Link URL", 455 + "description": "Redirects to the URL associated with the given link ID. This is the public shortlink endpoint.", 456 + "tags": ["Shortlinks"], 457 + "parameters": [ 458 + { 459 + "name": "id", 460 + "in": "path", 461 + "description": "The link ID", 462 + "required": true, 463 + "schema": { 464 + "type": "integer" 465 + } 466 + }, 467 + { 468 + "name": "sig", 469 + "in": "query", 470 + "description": "Click signature for verified click tracking", 471 + "required": false, 472 + "schema": { 473 + "type": "string" 474 + } 475 + } 476 + ], 477 + "responses": { 478 + "302": { 479 + "description": "Redirects to the target URL" 480 + }, 481 + "404": { 482 + "description": "Link not found", 483 + "content": { 484 + "application/json": { 485 + "schema": { 486 + "$ref": "#/components/schemas/APIError" 487 + } 488 + } 489 + } 490 + } 491 + } 492 + } 493 + }, 494 + "/api/v1/links": { 16 495 "get": { 17 - "summary": "Invalidate Link Preview Cache", 18 - "description": "Removes a URL from the server-side link preview cache.", 496 + "summary": "List Links", 497 + "description": "Returns a paginated list of links. Can be filtered by user.", 498 + "tags": ["Links"], 19 499 "parameters": [ 20 500 { 21 - "name": "url", 501 + "name": "limit", 502 + "in": "query", 503 + "description": "Maximum number of results (default: 50, max: 1000)", 504 + "schema": { 505 + "type": "integer", 506 + "default": 50, 507 + "minimum": 1, 508 + "maximum": 1000 509 + } 510 + }, 511 + { 512 + "name": "offset", 513 + "in": "query", 514 + "description": "Number of results to skip (default: 0)", 515 + "schema": { 516 + "type": "integer", 517 + "default": 0, 518 + "minimum": 0 519 + } 520 + }, 521 + { 522 + "name": "user", 22 523 "in": "query", 23 - "description": "The URL to invalidate", 24 - "required": true, 524 + "description": "Filter by submitter username", 25 525 "schema": { 26 526 "type": "string" 27 527 } ··· 29 529 ], 30 530 "responses": { 31 531 "200": { 32 - "description": "Cache Invalidated", 532 + "description": "List of links with pagination metadata", 33 533 "content": { 34 534 "application/json": { 35 535 "schema": { 36 - "type": "object", 37 - "properties": { 38 - "status": { "type": "string" }, 39 - "message": { "type": "string" } 40 - } 536 + "$ref": "#/components/schemas/APILinksListResponse" 537 + } 538 + }, 539 + "text/plain": { 540 + "schema": { 541 + "type": "string", 542 + "description": "Plain text representation of links" 41 543 } 42 544 } 43 545 } 44 546 }, 45 - "400": { 46 - "description": "Missing URL" 547 + "500": { 548 + "description": "Server error", 549 + "content": { 550 + "application/json": { 551 + "schema": { 552 + "$ref": "#/components/schemas/APIError" 553 + } 554 + } 555 + } 47 556 } 48 557 } 49 - } 50 - }, 51 - "/api/kitten/fetch": { 558 + }, 52 559 "post": { 53 - "summary": "Force Fetch Daily Kitten", 54 - "description": "Forces a new daily kitten to be fetched from cataas.com. Deletes today's existing kitten (if any) and fetches a new one with a stable URL. Useful for testing.", 560 + "summary": "Create Link", 561 + "description": "Submits a new link. Links are always created even if the URL was previously submitted. Duplicate information is included in the response.", 562 + "tags": ["Links"], 563 + "requestBody": { 564 + "required": true, 565 + "content": { 566 + "application/json": { 567 + "schema": { 568 + "$ref": "#/components/schemas/LinkCreateRequest" 569 + } 570 + } 571 + } 572 + }, 55 573 "responses": { 56 - "200": { 57 - "description": "Kitten fetched successfully", 574 + "201": { 575 + "description": "Link created successfully", 58 576 "content": { 59 577 "application/json": { 60 578 "schema": { 61 - "type": "object", 62 - "properties": { 63 - "status": { "type": "string", "example": "ok" }, 64 - "url": { "type": "string", "example": "https://cataas.com/cat/abc123xyz" } 65 - } 579 + "$ref": "#/components/schemas/APILinkCreateResponse" 580 + } 581 + }, 582 + "text/plain": { 583 + "schema": { 584 + "type": "string", 585 + "example": "123", 586 + "description": "Link ID (plain text format)" 66 587 } 67 588 } 68 589 } 69 590 }, 70 - "405": { 71 - "description": "Method not allowed (use POST)" 591 + "400": { 592 + "description": "Bad request (malformed JSON)", 593 + "content": { 594 + "application/json": { 595 + "schema": { 596 + "$ref": "#/components/schemas/APIError" 597 + } 598 + } 599 + } 600 + }, 601 + "422": { 602 + "description": "Validation error (invalid URL or missing fields)", 603 + "content": { 604 + "application/json": { 605 + "schema": { 606 + "$ref": "#/components/schemas/APIError" 607 + } 608 + } 609 + } 72 610 }, 73 611 "500": { 74 - "description": "Failed to fetch kitten", 612 + "description": "Server error", 75 613 "content": { 76 614 "application/json": { 77 615 "schema": { 78 - "type": "object", 79 - "properties": { 80 - "status": { "type": "string", "example": "error" }, 81 - "error": { "type": "string" } 82 - } 616 + "$ref": "#/components/schemas/APIError" 83 617 } 84 618 } 85 619 } ··· 87 621 } 88 622 } 89 623 }, 90 - "/link/": { 91 - "post": { 92 - "summary": "Submit a new Link", 93 - "description": "Submits a new link. Links are always added even if duplicates exist. Returns different status codes and contextual information about previous submissions when duplicates are detected.", 624 + "/api/v1/links/{id}": { 625 + "get": { 626 + "summary": "Get Link", 627 + "description": "Returns a single link by ID.", 628 + "tags": ["Links"], 94 629 "parameters": [ 95 630 { 96 - "name": "user", 97 - "in": "query", 98 - "description": "The username of the submitter", 631 + "name": "id", 632 + "in": "path", 633 + "description": "The link ID", 99 634 "required": true, 100 635 "schema": { 101 - "type": "string" 636 + "type": "integer" 637 + } 638 + } 639 + ], 640 + "responses": { 641 + "200": { 642 + "description": "Link details", 643 + "content": { 644 + "application/json": { 645 + "schema": { 646 + "$ref": "#/components/schemas/APILinkResponse" 647 + } 648 + }, 649 + "text/plain": { 650 + "schema": { 651 + "type": "string", 652 + "description": "Plain text representation" 653 + } 654 + } 102 655 } 103 656 }, 657 + "404": { 658 + "description": "Link not found", 659 + "content": { 660 + "application/json": { 661 + "schema": { 662 + "$ref": "#/components/schemas/APIError" 663 + } 664 + } 665 + } 666 + } 667 + } 668 + }, 669 + "delete": { 670 + "summary": "Delete Link", 671 + "description": "Deletes a link by ID. Requires API key authentication.", 672 + "tags": ["Links"], 673 + "security": [ 674 + { "apiKey": [] } 675 + ], 676 + "parameters": [ 104 677 { 105 - "name": "url", 106 - "in": "query", 107 - "description": "The URL to submit", 678 + "name": "id", 679 + "in": "path", 680 + "description": "The link ID", 108 681 "required": true, 109 682 "schema": { 110 - "type": "string" 683 + "type": "integer" 684 + } 685 + } 686 + ], 687 + "responses": { 688 + "204": { 689 + "description": "Link deleted successfully" 690 + }, 691 + "401": { 692 + "description": "Unauthorized (missing API key)", 693 + "content": { 694 + "application/json": { 695 + "schema": { 696 + "$ref": "#/components/schemas/APIError" 697 + } 698 + } 699 + } 700 + }, 701 + "403": { 702 + "description": "Forbidden (invalid API key)", 703 + "content": { 704 + "application/json": { 705 + "schema": { 706 + "$ref": "#/components/schemas/APIError" 707 + } 708 + } 709 + } 710 + }, 711 + "404": { 712 + "description": "Link not found", 713 + "content": { 714 + "application/json": { 715 + "schema": { 716 + "$ref": "#/components/schemas/APIError" 717 + } 718 + } 719 + } 720 + } 721 + } 722 + } 723 + }, 724 + "/api/v1/quotes": { 725 + "get": { 726 + "summary": "List Quotes", 727 + "description": "Returns a paginated list of quotes.", 728 + "tags": ["Quotes"], 729 + "parameters": [ 730 + { 731 + "name": "limit", 732 + "in": "query", 733 + "description": "Maximum number of results (default: 50, max: 1000)", 734 + "schema": { 735 + "type": "integer", 736 + "default": 50, 737 + "minimum": 1, 738 + "maximum": 1000 111 739 } 112 740 }, 113 741 { 114 - "name": "source", 742 + "name": "offset", 115 743 "in": "query", 116 - "description": "Source of submission. 'irc' returns plain text ID with duplicate marker. 'api' or Accept: application/json returns JSON with duplicate context.", 744 + "description": "Number of results to skip (default: 0)", 117 745 "schema": { 118 - "type": "string", 119 - "enum": [ 120 - "irc", 121 - "api", 122 - "web" 123 - ] 746 + "type": "integer", 747 + "default": 0, 748 + "minimum": 0 124 749 } 125 750 } 126 751 ], 127 752 "responses": { 128 753 "200": { 129 - "description": "Link created successfully (HTML response)", 754 + "description": "List of quotes with pagination metadata", 130 755 "content": { 131 - "text/html": { 756 + "application/json": { 757 + "schema": { 758 + "$ref": "#/components/schemas/APIQuotesListResponse" 759 + } 760 + }, 761 + "text/plain": { 132 762 "schema": { 133 763 "type": "string", 134 - "description": "HTML redirect page with optional duplicate notification" 764 + "description": "Plain text representation of quotes" 135 765 } 136 766 } 137 767 } 138 768 }, 769 + "500": { 770 + "description": "Server error", 771 + "content": { 772 + "application/json": { 773 + "schema": { 774 + "$ref": "#/components/schemas/APIError" 775 + } 776 + } 777 + } 778 + } 779 + } 780 + }, 781 + "post": { 782 + "summary": "Create Quote", 783 + "description": "Submits a new quote. Only the quote text is required.", 784 + "tags": ["Quotes"], 785 + "requestBody": { 786 + "required": true, 787 + "content": { 788 + "application/json": { 789 + "schema": { 790 + "$ref": "#/components/schemas/QuoteCreateRequest" 791 + } 792 + } 793 + } 794 + }, 795 + "responses": { 139 796 "201": { 140 - "description": "New link created successfully (JSON response)", 797 + "description": "Quote created successfully", 141 798 "content": { 142 799 "application/json": { 143 800 "schema": { 144 - "type": "object", 145 - "properties": { 146 - "link_id": { 147 - "type": "integer", 148 - "description": "ID of the newly created link" 149 - }, 150 - "is_duplicate": { 151 - "type": "boolean", 152 - "description": "Always false for 201 responses" 153 - }, 154 - "previous_submissions": { 155 - "type": "array", 156 - "description": "Empty for new links", 157 - "items": { 158 - "type": "object" 159 - } 160 - } 161 - }, 162 - "example": { 163 - "link_id": 123, 164 - "is_duplicate": false, 165 - "previous_submissions": [] 166 - } 801 + "$ref": "#/components/schemas/APIQuoteResponse" 167 802 } 168 803 }, 169 804 "text/plain": { 170 805 "schema": { 171 806 "type": "string", 172 - "example": "123", 173 - "description": "Link ID (IRC source)" 807 + "example": "42", 808 + "description": "Quote ID (plain text format)" 174 809 } 175 810 } 176 811 } 177 812 }, 178 - "208": { 179 - "description": "Link already reported - duplicate detected (JSON response)", 813 + "400": { 814 + "description": "Bad request (malformed JSON)", 180 815 "content": { 181 816 "application/json": { 182 817 "schema": { 183 - "type": "object", 184 - "properties": { 185 - "link_id": { 186 - "type": "integer", 187 - "description": "ID of the newly created link" 188 - }, 189 - "is_duplicate": { 190 - "type": "boolean", 191 - "description": "Always true for 208 responses" 192 - }, 193 - "previous_submissions": { 194 - "type": "array", 195 - "description": "List of all previous submissions of this URL", 196 - "items": { 197 - "type": "object", 198 - "properties": { 199 - "link_id": { 200 - "type": "integer", 201 - "description": "ID of the previous submission" 202 - }, 203 - "user": { 204 - "type": "string", 205 - "description": "Username who submitted previously" 206 - }, 207 - "timestamp": { 208 - "type": "string", 209 - "format": "date-time", 210 - "description": "When the link was previously submitted" 211 - }, 212 - "title": { 213 - "type": "string", 214 - "description": "Title of the previous submission" 215 - } 216 - } 217 - } 218 - } 219 - }, 220 - "example": { 221 - "link_id": 456, 222 - "is_duplicate": true, 223 - "previous_submissions": [ 224 - { 225 - "link_id": 123, 226 - "user": "alice", 227 - "timestamp": "2026-01-15T10:30:00Z", 228 - "title": "Example Page" 229 - } 230 - ] 231 - } 818 + "$ref": "#/components/schemas/APIError" 232 819 } 233 - }, 234 - "text/plain": { 820 + } 821 + } 822 + }, 823 + "422": { 824 + "description": "Validation error (missing quote text)", 825 + "content": { 826 + "application/json": { 235 827 "schema": { 236 - "type": "string", 237 - "example": "456 (duplicate, previously posted by alice)", 238 - "description": "Link ID with duplicate marker (IRC source)" 828 + "$ref": "#/components/schemas/APIError" 239 829 } 240 830 } 241 831 } 242 832 }, 243 - "308": { 244 - "description": "Permanent redirect (preserves POST method) - occurs when trailing slash is missing from URL" 245 - }, 246 833 "500": { 247 - "description": "Server Error" 834 + "description": "Server error", 835 + "content": { 836 + "application/json": { 837 + "schema": { 838 + "$ref": "#/components/schemas/APIError" 839 + } 840 + } 841 + } 248 842 } 249 843 } 250 844 } 251 845 }, 252 - "/link/{id}": { 846 + "/api/v1/quotes/{id}": { 253 847 "get": { 254 - "summary": "Redirect to a Link", 255 - "description": "Redirects to the URL associated with the given ID.", 848 + "summary": "Get Quote", 849 + "description": "Returns a single quote by ID.", 850 + "tags": ["Quotes"], 256 851 "parameters": [ 257 852 { 258 853 "name": "id", 259 854 "in": "path", 260 - "description": "The ID of the link to redirect to", 855 + "description": "The quote ID", 261 856 "required": true, 262 857 "schema": { 263 858 "type": "integer" 264 859 } 265 - }, 266 - { 267 - "name": "sig", 268 - "in": "query", 269 - "description": "Click signature for verified click tracking", 270 - "required": false, 271 - "schema": { 272 - "type": "string" 273 - } 274 860 } 275 861 ], 276 862 "responses": { 277 - "302": { 278 - "description": "Redirects to the target URL" 863 + "200": { 864 + "description": "Quote details", 865 + "content": { 866 + "application/json": { 867 + "schema": { 868 + "$ref": "#/components/schemas/APIQuoteResponse" 869 + } 870 + }, 871 + "text/plain": { 872 + "schema": { 873 + "type": "string", 874 + "description": "Plain text representation" 875 + } 876 + } 877 + } 279 878 }, 280 879 "404": { 281 - "description": "Link not found" 282 - }, 283 - "400": { 284 - "description": "Invalid ID" 880 + "description": "Quote not found", 881 + "content": { 882 + "application/json": { 883 + "schema": { 884 + "$ref": "#/components/schemas/APIError" 885 + } 886 + } 887 + } 285 888 } 286 889 } 287 890 }, 288 891 "delete": { 289 - "summary": "Delete a link", 290 - "description": "Deletes a link by ID. Requires admin authentication via X-Admin-Secret header or secret query parameter.", 892 + "summary": "Delete Quote", 893 + "description": "Deletes a quote by ID. Requires API key authentication.", 894 + "tags": ["Quotes"], 895 + "security": [ 896 + { "apiKey": [] } 897 + ], 291 898 "parameters": [ 292 899 { 293 900 "name": "id", 294 901 "in": "path", 295 - "description": "The ID of the link to delete", 902 + "description": "The quote ID", 296 903 "required": true, 297 904 "schema": { 298 905 "type": "integer" ··· 300 907 } 301 908 ], 302 909 "responses": { 303 - "200": { 304 - "description": "Link deleted successfully" 305 - }, 306 - "308": { 307 - "description": "Permanent redirect (preserves DELETE method) - occurs when trailing slash is missing" 308 - }, 309 - "403": { 310 - "description": "Forbidden - invalid or missing admin secret" 311 - }, 312 - "404": { 313 - "description": "Link not found" 910 + "204": { 911 + "description": "Quote deleted successfully" 912 + }, 913 + "401": { 914 + "description": "Unauthorized (missing API key)", 915 + "content": { 916 + "application/json": { 917 + "schema": { 918 + "$ref": "#/components/schemas/APIError" 919 + } 920 + } 921 + } 922 + }, 923 + "403": { 924 + "description": "Forbidden (invalid API key)", 925 + "content": { 926 + "application/json": { 927 + "schema": { 928 + "$ref": "#/components/schemas/APIError" 929 + } 930 + } 931 + } 932 + }, 933 + "404": { 934 + "description": "Quote not found", 935 + "content": { 936 + "application/json": { 937 + "schema": { 938 + "$ref": "#/components/schemas/APIError" 939 + } 940 + } 941 + } 942 + } 943 + } 944 + } 945 + }, 946 + "/api/v1/stats": { 947 + "get": { 948 + "summary": "Get Site Statistics", 949 + "description": "Returns site-wide statistics and a leaderboard of users by activity.", 950 + "tags": ["Statistics"], 951 + "parameters": [ 952 + { 953 + "name": "limit", 954 + "in": "query", 955 + "description": "Maximum leaderboard entries (default: 50, max: 1000)", 956 + "schema": { 957 + "type": "integer", 958 + "default": 50, 959 + "minimum": 1, 960 + "maximum": 1000 961 + } 962 + }, 963 + { 964 + "name": "offset", 965 + "in": "query", 966 + "description": "Number of leaderboard entries to skip (default: 0)", 967 + "schema": { 968 + "type": "integer", 969 + "default": 0, 970 + "minimum": 0 971 + } 972 + } 973 + ], 974 + "responses": { 975 + "200": { 976 + "description": "Site statistics and leaderboard", 977 + "content": { 978 + "application/json": { 979 + "schema": { 980 + "$ref": "#/components/schemas/APIStatsResponse" 981 + } 982 + }, 983 + "text/plain": { 984 + "schema": { 985 + "type": "string", 986 + "description": "Plain text representation" 987 + } 988 + } 989 + } 990 + }, 991 + "500": { 992 + "description": "Server error", 993 + "content": { 994 + "application/json": { 995 + "schema": { 996 + "$ref": "#/components/schemas/APIError" 997 + } 998 + } 314 999 } 1000 + } 315 1001 } 316 1002 } 317 1003 }, 318 - "/link/{id}.json": { 1004 + "/api/v1/users/{user}/stats": { 319 1005 "get": { 320 - "summary": "Get Link Metadata", 321 - "description": "Returns the full link metadata as JSON, including the owner (user), title, URL, click count, and timestamp.", 1006 + "summary": "Get User Statistics", 1007 + "description": "Returns statistics for a specific user.", 1008 + "tags": ["Statistics"], 322 1009 "parameters": [ 323 1010 { 324 - "name": "id", 1011 + "name": "user", 325 1012 "in": "path", 326 - "description": "The ID of the link", 1013 + "description": "The username", 327 1014 "required": true, 328 1015 "schema": { 329 - "type": "integer" 1016 + "type": "string" 330 1017 } 331 1018 } 332 1019 ], 333 1020 "responses": { 334 1021 "200": { 335 - "description": "Link metadata", 1022 + "description": "User statistics", 336 1023 "content": { 337 1024 "application/json": { 338 1025 "schema": { 339 - "type": "object", 340 - "properties": { 341 - "ircLinkID": { 342 - "type": "integer", 343 - "description": "The unique link ID" 344 - }, 345 - "timestamp": { 346 - "type": "string", 347 - "format": "date-time", 348 - "description": "When the link was submitted" 349 - }, 350 - "user": { 351 - "type": "string", 352 - "description": "The username who submitted the link" 353 - }, 354 - "title": { 355 - "type": "string", 356 - "description": "The title of the linked page" 357 - }, 358 - "url": { 359 - "type": "string", 360 - "description": "The target URL" 361 - }, 362 - "clicks": { 363 - "type": "integer", 364 - "description": "Number of verified clicks" 365 - }, 366 - "content_type": { 367 - "type": "string", 368 - "description": "Content type hint (e.g., 'image' for images)" 369 - } 370 - }, 371 - "example": { 372 - "ircLinkID": 42, 373 - "timestamp": "2026-01-15T10:30:00Z", 374 - "user": "alice", 375 - "title": "Example Article", 376 - "url": "http://example.com/article", 377 - "clicks": 15, 378 - "content_type": "0" 379 - } 1026 + "$ref": "#/components/schemas/APIUserStatsResponse" 1027 + } 1028 + }, 1029 + "text/plain": { 1030 + "schema": { 1031 + "type": "string", 1032 + "description": "Plain text representation" 380 1033 } 381 1034 } 382 1035 } 383 1036 }, 384 1037 "404": { 385 - "description": "Link not found" 1038 + "description": "User not found (no submissions)", 1039 + "content": { 1040 + "application/json": { 1041 + "schema": { 1042 + "$ref": "#/components/schemas/APIError" 1043 + } 1044 + } 1045 + } 386 1046 }, 387 - "400": { 388 - "description": "Invalid ID" 1047 + "500": { 1048 + "description": "Server error", 1049 + "content": { 1050 + "application/json": { 1051 + "schema": { 1052 + "$ref": "#/components/schemas/APIError" 1053 + } 1054 + } 1055 + } 389 1056 } 390 1057 } 391 1058 } 392 1059 }, 393 - "/ogpreview": { 1060 + "/api/v1/search": { 394 1061 "get": { 395 - "summary": "Get OpenGraph Preview", 1062 + "summary": "Search Content", 1063 + "description": "Search links and quotes by keyword. Minimum 4 characters required.", 1064 + "tags": ["Search"], 1065 + "parameters": [ 1066 + { 1067 + "name": "q", 1068 + "in": "query", 1069 + "description": "Search query (minimum 4 characters)", 1070 + "required": true, 1071 + "schema": { 1072 + "type": "string", 1073 + "minLength": 4 1074 + } 1075 + }, 1076 + { 1077 + "name": "type", 1078 + "in": "query", 1079 + "description": "Content types to search (comma-separated: links, quotes). Defaults to both.", 1080 + "schema": { 1081 + "type": "string", 1082 + "example": "links,quotes" 1083 + } 1084 + }, 1085 + { 1086 + "name": "limit", 1087 + "in": "query", 1088 + "description": "Maximum results per type (default: 50)", 1089 + "schema": { 1090 + "type": "integer", 1091 + "default": 50, 1092 + "minimum": 1 1093 + } 1094 + }, 1095 + { 1096 + "name": "offset", 1097 + "in": "query", 1098 + "description": "Number of results to skip per type (default: 0)", 1099 + "schema": { 1100 + "type": "integer", 1101 + "default": 0, 1102 + "minimum": 0 1103 + } 1104 + } 1105 + ], 1106 + "responses": { 1107 + "200": { 1108 + "description": "Search results", 1109 + "content": { 1110 + "application/json": { 1111 + "schema": { 1112 + "$ref": "#/components/schemas/APISearchResponse" 1113 + } 1114 + }, 1115 + "text/plain": { 1116 + "schema": { 1117 + "type": "string", 1118 + "description": "Plain text representation" 1119 + } 1120 + } 1121 + } 1122 + }, 1123 + "422": { 1124 + "description": "Validation error (query too short)", 1125 + "content": { 1126 + "application/json": { 1127 + "schema": { 1128 + "$ref": "#/components/schemas/APIError" 1129 + } 1130 + } 1131 + } 1132 + }, 1133 + "500": { 1134 + "description": "Server error", 1135 + "content": { 1136 + "application/json": { 1137 + "schema": { 1138 + "$ref": "#/components/schemas/APIError" 1139 + } 1140 + } 1141 + } 1142 + } 1143 + } 1144 + } 1145 + }, 1146 + "/api/v1/cache": { 1147 + "delete": { 1148 + "summary": "Clear Preview Cache", 1149 + "description": "Clears the link preview cache. Can clear all entries or a specific URL.", 1150 + "tags": ["Cache"], 1151 + "security": [ 1152 + { "apiKey": [] } 1153 + ], 396 1154 "parameters": [ 397 1155 { 398 1156 "name": "url", 399 1157 "in": "query", 400 - "required": true, 1158 + "description": "Specific URL to clear from cache. If omitted, clears all cache.", 401 1159 "schema": { 402 1160 "type": "string" 403 1161 } ··· 405 1163 ], 406 1164 "responses": { 407 1165 "200": { 408 - "description": "OpenGraph Metadata", 1166 + "description": "Cache cleared successfully", 409 1167 "content": { 410 1168 "application/json": { 411 1169 "schema": { 412 - "type": "object", 413 - "properties": { 414 - "title": { "type": "string" }, 415 - "description": { "type": "string" }, 416 - "image": { "type": "string" } 417 - } 1170 + "$ref": "#/components/schemas/APICacheResponse" 1171 + } 1172 + } 1173 + } 1174 + }, 1175 + "401": { 1176 + "description": "Unauthorized (missing API key)", 1177 + "content": { 1178 + "application/json": { 1179 + "schema": { 1180 + "$ref": "#/components/schemas/APIError" 1181 + } 1182 + } 1183 + } 1184 + }, 1185 + "403": { 1186 + "description": "Forbidden (invalid API key)", 1187 + "content": { 1188 + "application/json": { 1189 + "schema": { 1190 + "$ref": "#/components/schemas/APIError" 1191 + } 1192 + } 1193 + } 1194 + }, 1195 + "500": { 1196 + "description": "Server error", 1197 + "content": { 1198 + "application/json": { 1199 + "schema": { 1200 + "$ref": "#/components/schemas/APIError" 418 1201 } 419 1202 } 420 1203 } ··· 422 1205 } 423 1206 } 424 1207 }, 425 - "/quote/": { 1208 + "/api/v1/kittens/daily": { 426 1209 "get": { 427 - "summary": "Get a Random Quote", 1210 + "summary": "Get Daily Kitten", 1211 + "description": "Returns today's kitten without triggering a fetch.", 1212 + "tags": ["Kittens"], 428 1213 "responses": { 429 1214 "200": { 430 - "description": "Random Quote", 1215 + "description": "Today's kitten", 431 1216 "content": { 1217 + "application/json": { 1218 + "schema": { 1219 + "$ref": "#/components/schemas/APIDailyKittenResponse" 1220 + } 1221 + }, 432 1222 "text/plain": { 433 1223 "schema": { 434 1224 "type": "string", 435 - "example": "Wise words -- Author" 1225 + "description": "Kitten URL" 436 1226 } 437 - }, 438 - "text/html": { 1227 + } 1228 + } 1229 + }, 1230 + "404": { 1231 + "description": "No kitten exists for today", 1232 + "content": { 1233 + "application/json": { 439 1234 "schema": { 440 - "type": "string" 1235 + "$ref": "#/components/schemas/APIError" 441 1236 } 442 - }, 1237 + } 1238 + } 1239 + }, 1240 + "500": { 1241 + "description": "Server error", 1242 + "content": { 443 1243 "application/json": { 444 1244 "schema": { 445 - "type": "object", 446 - "properties": { 447 - "quoteID": { "type": "integer" }, 448 - "timestamp": { "type": "string", "format": "date-time" }, 449 - "quote": { "type": "string" }, 450 - "author": { "type": "string" }, 451 - "poster": { "type": "string", "description": "The person who posted/submitted the quote" } 452 - } 1245 + "$ref": "#/components/schemas/APIError" 453 1246 } 454 1247 } 455 1248 } 456 1249 } 457 1250 } 458 1251 }, 459 - "post": { 460 - "summary": "Submit a Quote", 461 - "requestBody": { 462 - "content": { 463 - "application/x-www-form-urlencoded": { 464 - "schema": { 465 - "type": "object", 466 - "properties": { 467 - "quote": { "type": "string", "description": "The quote text (required)" }, 468 - "author": { "type": "string", "description": "The author of the quote (optional)" }, 469 - "poster": { "type": "string", "description": "The person posting/submitting the quote (optional)" } 470 - }, 471 - "required": ["quote"] 1252 + "put": { 1253 + "summary": "Ensure Daily Kitten", 1254 + "description": "Fetches a new kitten if one doesn't exist for today. Returns the existing kitten if one already exists.", 1255 + "tags": ["Kittens"], 1256 + "security": [ 1257 + { "apiKey": [] } 1258 + ], 1259 + "responses": { 1260 + "200": { 1261 + "description": "Daily kitten (new or existing)", 1262 + "content": { 1263 + "application/json": { 1264 + "schema": { 1265 + "allOf": [ 1266 + { "$ref": "#/components/schemas/APIDailyKittenResponse" }, 1267 + { 1268 + "type": "object", 1269 + "properties": { 1270 + "fetched": { 1271 + "type": "boolean", 1272 + "description": "True if a new kitten was fetched, false if one already existed" 1273 + } 1274 + } 1275 + } 1276 + ], 1277 + "example": { 1278 + "url": "https://cataas.com/cat/abc123", 1279 + "date": "2026-02-08", 1280 + "fetched": true 1281 + } 1282 + } 1283 + } 1284 + } 1285 + }, 1286 + "401": { 1287 + "description": "Unauthorized (missing API key)", 1288 + "content": { 1289 + "application/json": { 1290 + "schema": { 1291 + "$ref": "#/components/schemas/APIError" 1292 + } 1293 + } 1294 + } 1295 + }, 1296 + "403": { 1297 + "description": "Forbidden (invalid API key)", 1298 + "content": { 1299 + "application/json": { 1300 + "schema": { 1301 + "$ref": "#/components/schemas/APIError" 1302 + } 1303 + } 1304 + } 1305 + }, 1306 + "500": { 1307 + "description": "Server error", 1308 + "content": { 1309 + "application/json": { 1310 + "schema": { 1311 + "$ref": "#/components/schemas/APIError" 1312 + } 472 1313 } 473 1314 } 474 1315 } 475 - }, 1316 + } 1317 + }, 1318 + "delete": { 1319 + "summary": "Remove Daily Kitten", 1320 + "description": "Removes today's kitten. Use this to remove an undesirable kitten, then PUT to fetch a new one.", 1321 + "tags": ["Kittens"], 1322 + "security": [ 1323 + { "apiKey": [] } 1324 + ], 476 1325 "responses": { 477 - "200": { 478 - "description": "Quote created", 1326 + "204": { 1327 + "description": "Kitten removed successfully" 1328 + }, 1329 + "401": { 1330 + "description": "Unauthorized (missing API key)", 479 1331 "content": { 480 - "text/plain": { 1332 + "application/json": { 481 1333 "schema": { 482 - "type": "string", 483 - "example": "http://example.com/quote/42", 484 - "description": "Permalink URL to the newly created quote" 1334 + "$ref": "#/components/schemas/APIError" 485 1335 } 486 1336 } 487 1337 } 488 1338 }, 489 - "308": { 490 - "description": "Permanent redirect (preserves POST method) - occurs when trailing slash is missing from URL" 1339 + "403": { 1340 + "description": "Forbidden (invalid API key)", 1341 + "content": { 1342 + "application/json": { 1343 + "schema": { 1344 + "$ref": "#/components/schemas/APIError" 1345 + } 1346 + } 1347 + } 491 1348 }, 492 - "400": { 493 - "description": "Missing inputs" 1349 + "404": { 1350 + "description": "No kitten exists for today", 1351 + "content": { 1352 + "application/json": { 1353 + "schema": { 1354 + "$ref": "#/components/schemas/APIError" 1355 + } 1356 + } 1357 + } 494 1358 } 495 1359 } 496 1360 } 497 1361 }, 498 - "/quote/{id}": { 1362 + "/api/caching/invalidate": { 499 1363 "get": { 500 - "summary": "Get Quote by ID (Permalink)", 501 - "description": "Returns a specific quote by ID. Defaults to HTML page for browser visits, or returns JSON with Accept: application/json header.", 1364 + "summary": "Invalidate Link Preview Cache (Deprecated)", 1365 + "description": "DEPRECATED: Use DELETE /api/v1/cache instead. Removes a URL from the server-side link preview cache.", 1366 + "deprecated": true, 1367 + "tags": ["Deprecated"], 502 1368 "parameters": [ 503 1369 { 504 - "name": "id", 505 - "in": "path", 506 - "description": "The ID of the quote", 1370 + "name": "url", 1371 + "in": "query", 1372 + "description": "The URL to invalidate", 507 1373 "required": true, 508 1374 "schema": { 509 - "type": "integer" 1375 + "type": "string" 510 1376 } 511 1377 } 512 1378 ], 513 1379 "responses": { 514 1380 "200": { 515 - "description": "Quote found", 1381 + "description": "Cache Invalidated", 516 1382 "content": { 517 - "text/html": { 1383 + "application/json": { 518 1384 "schema": { 519 - "type": "string", 520 - "description": "HTML page displaying the quote" 1385 + "type": "object", 1386 + "properties": { 1387 + "status": { "type": "string" }, 1388 + "message": { "type": "string" } 1389 + } 521 1390 } 522 - }, 1391 + } 1392 + } 1393 + }, 1394 + "400": { 1395 + "description": "Missing URL" 1396 + } 1397 + } 1398 + } 1399 + }, 1400 + "/api/kitten/fetch": { 1401 + "post": { 1402 + "summary": "Force Fetch Daily Kitten (Deprecated)", 1403 + "description": "DEPRECATED: Use PUT /api/v1/kittens/daily instead. Forces a new daily kitten to be fetched from cataas.com.", 1404 + "deprecated": true, 1405 + "tags": ["Deprecated"], 1406 + "responses": { 1407 + "200": { 1408 + "description": "Kitten fetched successfully", 1409 + "content": { 1410 + "application/json": { 1411 + "schema": { 1412 + "type": "object", 1413 + "properties": { 1414 + "status": { "type": "string", "example": "ok" }, 1415 + "url": { "type": "string", "example": "https://cataas.com/cat/abc123xyz" } 1416 + } 1417 + } 1418 + } 1419 + } 1420 + }, 1421 + "405": { 1422 + "description": "Method not allowed (use POST)" 1423 + }, 1424 + "500": { 1425 + "description": "Failed to fetch kitten", 1426 + "content": { 523 1427 "application/json": { 524 1428 "schema": { 525 1429 "type": "object", 526 1430 "properties": { 527 - "quoteID": { 528 - "type": "integer", 529 - "description": "The unique quote ID" 530 - }, 531 - "timestamp": { 532 - "type": "string", 533 - "format": "date-time", 534 - "description": "When the quote was submitted" 535 - }, 536 - "quote": { 537 - "type": "string", 538 - "description": "The quote text" 539 - }, 540 - "author": { 541 - "type": "string", 542 - "description": "The author of the quote" 543 - }, 544 - "poster": { 545 - "type": "string", 546 - "description": "The person who posted/submitted the quote" 547 - } 548 - }, 549 - "example": { 550 - "quoteID": 42, 551 - "timestamp": "2026-01-15T10:30:00Z", 552 - "quote": "The only way to do great work is to love what you do.", 553 - "author": "Steve Jobs", 554 - "poster": "alice" 1431 + "status": { "type": "string", "example": "error" }, 1432 + "error": { "type": "string" } 555 1433 } 556 1434 } 557 1435 } 1436 + } 1437 + } 1438 + } 1439 + } 1440 + }, 1441 + "/link/": { 1442 + "post": { 1443 + "summary": "Submit a new Link (Deprecated)", 1444 + "description": "DEPRECATED: Use POST /api/v1/links instead. Submits a new link using query parameters.", 1445 + "deprecated": true, 1446 + "tags": ["Deprecated"], 1447 + "parameters": [ 1448 + { 1449 + "name": "user", 1450 + "in": "query", 1451 + "description": "The username of the submitter", 1452 + "required": true, 1453 + "schema": { 1454 + "type": "string" 558 1455 } 559 1456 }, 560 - "404": { 561 - "description": "Quote not found" 1457 + { 1458 + "name": "url", 1459 + "in": "query", 1460 + "description": "The URL to submit", 1461 + "required": true, 1462 + "schema": { 1463 + "type": "string" 1464 + } 1465 + }, 1466 + { 1467 + "name": "source", 1468 + "in": "query", 1469 + "description": "Source of submission", 1470 + "schema": { 1471 + "type": "string", 1472 + "enum": ["irc", "api", "web"] 1473 + } 1474 + } 1475 + ], 1476 + "responses": { 1477 + "201": { 1478 + "description": "Link created" 1479 + }, 1480 + "208": { 1481 + "description": "Duplicate link created" 1482 + }, 1483 + "500": { 1484 + "description": "Server Error" 1485 + } 1486 + } 1487 + } 1488 + }, 1489 + "/link/{id}": { 1490 + "get": { 1491 + "summary": "Redirect to a Link (Deprecated)", 1492 + "description": "DEPRECATED: Use GET /r/{id} instead. Redirects to the URL associated with the given ID.", 1493 + "deprecated": true, 1494 + "tags": ["Deprecated"], 1495 + "parameters": [ 1496 + { 1497 + "name": "id", 1498 + "in": "path", 1499 + "description": "The ID of the link", 1500 + "required": true, 1501 + "schema": { 1502 + "type": "integer" 1503 + } 1504 + } 1505 + ], 1506 + "responses": { 1507 + "302": { 1508 + "description": "Redirects to the target URL" 562 1509 }, 563 - "400": { 564 - "description": "Invalid ID" 1510 + "404": { 1511 + "description": "Link not found" 565 1512 } 566 1513 } 567 1514 }, 568 1515 "delete": { 569 - "summary": "Delete a quote", 570 - "description": "Deletes a quote by ID. Requires admin authentication via X-Admin-Secret header or secret query parameter.", 1516 + "summary": "Delete a link (Deprecated)", 1517 + "description": "DEPRECATED: Use DELETE /api/v1/links/{id} instead.", 1518 + "deprecated": true, 1519 + "tags": ["Deprecated"], 571 1520 "parameters": [ 572 1521 { 573 1522 "name": "id", 574 1523 "in": "path", 575 - "description": "The ID of the quote to delete", 1524 + "description": "The ID of the link", 576 1525 "required": true, 577 1526 "schema": { 578 1527 "type": "integer" ··· 581 1530 ], 582 1531 "responses": { 583 1532 "200": { 584 - "description": "Quote deleted successfully" 585 - }, 586 - "308": { 587 - "description": "Permanent redirect (preserves DELETE method) - occurs when trailing slash is missing" 1533 + "description": "Link deleted" 588 1534 }, 589 1535 "403": { 590 - "description": "Forbidden - invalid or missing admin secret" 1536 + "description": "Forbidden" 591 1537 }, 592 1538 "404": { 593 - "description": "Quote not found" 1539 + "description": "Not found" 594 1540 } 595 1541 } 596 1542 } 597 1543 }, 598 - "/quote/{id}.json": { 1544 + "/link/{id}.json": { 599 1545 "get": { 600 - "summary": "Get Quote Metadata as JSON", 601 - "description": "Returns the quote as JSON, regardless of Accept header.", 1546 + "summary": "Get Link Metadata (Deprecated)", 1547 + "description": "DEPRECATED: Use GET /api/v1/links/{id} instead. Returns link metadata as JSON.", 1548 + "deprecated": true, 1549 + "tags": ["Deprecated"], 602 1550 "parameters": [ 603 1551 { 604 1552 "name": "id", 605 1553 "in": "path", 606 - "description": "The ID of the quote", 1554 + "description": "The ID of the link", 607 1555 "required": true, 608 1556 "schema": { 609 1557 "type": "integer" ··· 612 1560 ], 613 1561 "responses": { 614 1562 "200": { 615 - "description": "Quote metadata", 1563 + "description": "Link metadata", 616 1564 "content": { 617 1565 "application/json": { 618 1566 "schema": { 619 1567 "type": "object", 620 1568 "properties": { 621 - "quoteID": { 622 - "type": "integer", 623 - "description": "The unique quote ID" 624 - }, 625 - "timestamp": { 626 - "type": "string", 627 - "format": "date-time", 628 - "description": "When the quote was submitted" 629 - }, 630 - "quote": { 631 - "type": "string", 632 - "description": "The quote text" 633 - }, 634 - "author": { 635 - "type": "string", 636 - "description": "The author of the quote" 637 - }, 638 - "poster": { 639 - "type": "string", 640 - "description": "The person who posted/submitted the quote" 641 - } 642 - }, 643 - "example": { 644 - "quoteID": 42, 645 - "timestamp": "2026-01-15T10:30:00Z", 646 - "quote": "The only way to do great work is to love what you do.", 647 - "author": "Steve Jobs", 648 - "poster": "alice" 1569 + "ircLinkID": { "type": "integer" }, 1570 + "timestamp": { "type": "string", "format": "date-time" }, 1571 + "user": { "type": "string" }, 1572 + "title": { "type": "string" }, 1573 + "url": { "type": "string" }, 1574 + "clicks": { "type": "integer" } 649 1575 } 650 1576 } 651 1577 } 652 1578 } 653 1579 }, 654 1580 "404": { 655 - "description": "Quote not found" 1581 + "description": "Link not found" 1582 + } 1583 + } 1584 + } 1585 + }, 1586 + "/quote/": { 1587 + "get": { 1588 + "summary": "Get a Random Quote (Deprecated)", 1589 + "description": "DEPRECATED: Use GET /api/v1/quotes instead.", 1590 + "deprecated": true, 1591 + "tags": ["Deprecated"], 1592 + "responses": { 1593 + "200": { 1594 + "description": "Random Quote" 1595 + } 1596 + } 1597 + }, 1598 + "post": { 1599 + "summary": "Submit a Quote (Deprecated)", 1600 + "description": "DEPRECATED: Use POST /api/v1/quotes instead.", 1601 + "deprecated": true, 1602 + "tags": ["Deprecated"], 1603 + "requestBody": { 1604 + "content": { 1605 + "application/x-www-form-urlencoded": { 1606 + "schema": { 1607 + "type": "object", 1608 + "properties": { 1609 + "quote": { "type": "string" }, 1610 + "author": { "type": "string" }, 1611 + "poster": { "type": "string" } 1612 + }, 1613 + "required": ["quote"] 1614 + } 1615 + } 1616 + } 1617 + }, 1618 + "responses": { 1619 + "200": { 1620 + "description": "Quote created" 656 1621 }, 657 1622 "400": { 658 - "description": "Invalid ID" 1623 + "description": "Missing inputs" 659 1624 } 660 1625 } 661 1626 } 662 1627 }, 663 - "/search": { 1628 + "/quote/{id}": { 664 1629 "get": { 665 - "summary": "Search Content", 1630 + "summary": "Get Quote by ID (Deprecated)", 1631 + "description": "DEPRECATED: Use GET /api/v1/quotes/{id} instead.", 1632 + "deprecated": true, 1633 + "tags": ["Deprecated"], 666 1634 "parameters": [ 667 1635 { 668 - "name": "search", 669 - "in": "query", 1636 + "name": "id", 1637 + "in": "path", 1638 + "description": "The ID of the quote", 670 1639 "required": true, 671 1640 "schema": { 672 - "type": "string", 673 - "minLength": 4 1641 + "type": "integer" 674 1642 } 675 1643 } 676 1644 ], 677 1645 "responses": { 678 1646 "200": { 679 - "description": "Search Results HTML", 680 - "content": { 681 - "text/html": { 682 - "schema": { 683 - "type": "string" 684 - } 685 - } 1647 + "description": "Quote found" 1648 + }, 1649 + "404": { 1650 + "description": "Quote not found" 1651 + } 1652 + } 1653 + }, 1654 + "delete": { 1655 + "summary": "Delete a quote (Deprecated)", 1656 + "description": "DEPRECATED: Use DELETE /api/v1/quotes/{id} instead.", 1657 + "deprecated": true, 1658 + "tags": ["Deprecated"], 1659 + "parameters": [ 1660 + { 1661 + "name": "id", 1662 + "in": "path", 1663 + "description": "The ID of the quote", 1664 + "required": true, 1665 + "schema": { 1666 + "type": "integer" 686 1667 } 687 1668 } 1669 + ], 1670 + "responses": { 1671 + "200": { 1672 + "description": "Quote deleted" 1673 + }, 1674 + "403": { 1675 + "description": "Forbidden" 1676 + }, 1677 + "404": { 1678 + "description": "Not found" 1679 + } 688 1680 } 689 1681 } 690 1682 }, 691 - "/stats": { 1683 + "/quote/{id}.json": { 692 1684 "get": { 693 - "summary": "Get User Statistics", 1685 + "summary": "Get Quote Metadata as JSON (Deprecated)", 1686 + "description": "DEPRECATED: Use GET /api/v1/quotes/{id} instead.", 1687 + "deprecated": true, 1688 + "tags": ["Deprecated"], 694 1689 "parameters": [ 695 1690 { 696 - "name": "page", 697 - "in": "query", 1691 + "name": "id", 1692 + "in": "path", 1693 + "description": "The ID of the quote", 1694 + "required": true, 698 1695 "schema": { 699 1696 "type": "integer" 700 1697 } 1698 + } 1699 + ], 1700 + "responses": { 1701 + "200": { 1702 + "description": "Quote metadata" 701 1703 }, 1704 + "404": { 1705 + "description": "Quote not found" 1706 + } 1707 + } 1708 + } 1709 + }, 1710 + "/search": { 1711 + "get": { 1712 + "summary": "Search Content (Deprecated)", 1713 + "description": "DEPRECATED: Use GET /api/v1/search instead.", 1714 + "deprecated": true, 1715 + "tags": ["Deprecated"], 1716 + "parameters": [ 702 1717 { 703 - "name": "sort", 1718 + "name": "search", 704 1719 "in": "query", 705 - "description": "Sort order (default: links)", 1720 + "required": true, 706 1721 "schema": { 707 - "type": "string" 1722 + "type": "string", 1723 + "minLength": 4 708 1724 } 709 1725 } 710 1726 ], 711 1727 "responses": { 712 1728 "200": { 713 - "description": "Statistics HTML Page", 714 - "content": { 715 - "text/html": { 716 - "schema": { 717 - "type": "string" 718 - } 719 - } 720 - } 1729 + "description": "Search Results HTML" 1730 + } 1731 + } 1732 + } 1733 + }, 1734 + "/stats": { 1735 + "get": { 1736 + "summary": "Get User Statistics (Deprecated)", 1737 + "description": "DEPRECATED: Use GET /api/v1/stats instead.", 1738 + "deprecated": true, 1739 + "tags": ["Deprecated"], 1740 + "responses": { 1741 + "200": { 1742 + "description": "Statistics HTML Page" 721 1743 } 722 1744 } 723 1745 } 724 1746 }, 725 1747 "/stats.json": { 726 1748 "get": { 727 - "summary": "Get User Statistics as JSON", 728 - "description": "Returns user statistics including link and quote counts for each user, sorted by link count descending. Supports pagination.", 1749 + "summary": "Get User Statistics as JSON (Deprecated)", 1750 + "description": "DEPRECATED: Use GET /api/v1/stats instead.", 1751 + "deprecated": true, 1752 + "tags": ["Deprecated"], 729 1753 "parameters": [ 730 1754 { 731 1755 "name": "limit", 732 1756 "in": "query", 733 - "description": "Maximum number of results to return (default: 50, max: 1000)", 734 1757 "schema": { 735 1758 "type": "integer", 736 - "default": 50, 737 - "minimum": 1, 738 - "maximum": 1000 1759 + "default": 50 739 1760 } 740 1761 }, 741 1762 { 742 1763 "name": "offset", 743 1764 "in": "query", 744 - "description": "Number of results to skip (default: 0)", 745 1765 "schema": { 746 1766 "type": "integer", 747 - "default": 0, 748 - "minimum": 0 1767 + "default": 0 749 1768 } 750 1769 } 751 1770 ], 752 1771 "responses": { 753 1772 "200": { 754 - "description": "Array of user statistics", 755 - "content": { 756 - "application/json": { 757 - "schema": { 758 - "type": "array", 759 - "items": { 760 - "type": "object", 761 - "properties": { 762 - "user": { 763 - "type": "string", 764 - "description": "The username" 765 - }, 766 - "link_count": { 767 - "type": "integer", 768 - "description": "Number of links submitted by this user" 769 - }, 770 - "quote_count": { 771 - "type": "integer", 772 - "description": "Number of quotes submitted by this user" 773 - } 774 - } 775 - }, 776 - "example": [ 777 - { 778 - "user": "alice", 779 - "link_count": 150, 780 - "quote_count": 42 781 - }, 782 - { 783 - "user": "bob", 784 - "link_count": 120, 785 - "quote_count": 35 786 - } 787 - ] 788 - } 789 - } 1773 + "description": "Array of user statistics" 1774 + } 1775 + } 1776 + } 1777 + }, 1778 + "/ogpreview": { 1779 + "get": { 1780 + "summary": "Get OpenGraph Preview (Deprecated)", 1781 + "description": "DEPRECATED: This endpoint may be removed in a future release.", 1782 + "deprecated": true, 1783 + "tags": ["Deprecated"], 1784 + "parameters": [ 1785 + { 1786 + "name": "url", 1787 + "in": "query", 1788 + "required": true, 1789 + "schema": { 1790 + "type": "string" 790 1791 } 791 - }, 792 - "500": { 793 - "description": "Server Error" 1792 + } 1793 + ], 1794 + "responses": { 1795 + "200": { 1796 + "description": "OpenGraph Metadata" 794 1797 } 795 1798 } 796 1799 } 797 1800 } 798 - } 1801 + }, 1802 + "tags": [ 1803 + { 1804 + "name": "Shortlinks", 1805 + "description": "Public shortlink redirects" 1806 + }, 1807 + { 1808 + "name": "Links", 1809 + "description": "Link management endpoints" 1810 + }, 1811 + { 1812 + "name": "Quotes", 1813 + "description": "Quote management endpoints" 1814 + }, 1815 + { 1816 + "name": "Statistics", 1817 + "description": "Site and user statistics" 1818 + }, 1819 + { 1820 + "name": "Search", 1821 + "description": "Search functionality" 1822 + }, 1823 + { 1824 + "name": "Cache", 1825 + "description": "Cache management" 1826 + }, 1827 + { 1828 + "name": "Kittens", 1829 + "description": "Daily kitten management" 1830 + }, 1831 + { 1832 + "name": "Deprecated", 1833 + "description": "Legacy endpoints - will be removed in a future release" 1834 + } 1835 + ] 799 1836 }