mirror of
https://github.com/wahyd4/links.git
synced 2026-08-09 05:06:16 +10:00
1610 lines
50 KiB
YAML
1610 lines
50 KiB
YAML
openapi: 3.0.3
|
||
info:
|
||
title: GoLinks API
|
||
description: |
|
||
# GoLinks — Link & Content Management API
|
||
|
||
GoLinks is a self-hosted link management system. It lets you create short links,
|
||
bookmark pages with auto-extracted metadata, manage image collections, upload files,
|
||
write blog posts, and serve random images from a personal library.
|
||
|
||
## Key concepts
|
||
|
||
| Resource | What it is |
|
||
|---|---|
|
||
| **Page** | A bookmarked URL. When created, the server asynchronously fetches its title, description, and a screenshot. |
|
||
| **Bookmark** | An archived social-media post (e.g. an x.com / Twitter tweet) saved with full preview data — text, author, images, video poster thumbnails, link cards, quotes — so the archive page renders offline. Bulk-import via `/api/bookmarks/import`. |
|
||
| **Post** | A Markdown blog post, optionally tagged. |
|
||
| **Collection** | A named group of uploaded images (e.g. a photo album). |
|
||
| **Image** | An image file belonging to a Collection. |
|
||
| **File** | A generic uploaded file. Files can be made public via a shareable token URL. |
|
||
| **Random image** | Serves a random image from the server's local image library, resized on the fly. Great for placeholder images. |
|
||
|
||
## Common patterns
|
||
|
||
### Pagination
|
||
All list endpoints accept `?page=` (1-based) and `?page_size=` query parameters.
|
||
Responses follow the envelope `{ count, next, previous, results[] }`.
|
||
|
||
### Image embedding
|
||
Use the random image endpoint as a live placeholder:
|
||
```
|
||

|
||
```
|
||
Or import and self-host an external image:
|
||
```
|
||

|
||
```
|
||
|
||
### File sharing
|
||
Upload a file via `POST /api/files`, call `POST /api/files/{id}/toggle-public` to make
|
||
it public, then share the returned `public_url` (`/public/files/{id}-{filename}`) —
|
||
no authentication is required for the recipient. Set an expiry with
|
||
`POST /api/files/{id}/set-expiry` to time-limit the link.
|
||
|
||
### Bookmarks bulk import
|
||
Import (or upsert) many archived posts in one request. Each item accepts the
|
||
full normalised export object from `x-bookmarks-exporter` (with nested
|
||
`author`, `media`, `card`, `quoted`, `retweet`, counters); matching is by
|
||
`tweet_id`:
|
||
```
|
||
POST /api/bookmarks/import
|
||
{ "bookmarks": [ { "id": "123", "author": {"screen_name":"u"}, "text": "...",
|
||
"media": [...], ... }, ... ] }
|
||
→ { "created": 10, "updated": 0, "skipped": 0, "total": 769 }
|
||
```
|
||
Use `PUT /api/bookmarks/bulk` with a bare JSON array of flat
|
||
`Bookmark`-shaped objects for the same upsert behaviour.
|
||
|
||
## Authentication
|
||
No authentication is currently required on any endpoint.
|
||
version: 1.0.0
|
||
x-logo:
|
||
url: /static/images/logo.png
|
||
|
||
servers:
|
||
- url: http://localhost:8000
|
||
description: Local development server
|
||
|
||
tags:
|
||
- name: Links
|
||
description: Short links with click tracking. Supports listing all links and retrieving the most visited ones sorted by click count.
|
||
- name: Pages
|
||
description: |
|
||
Bookmarked web pages. Creating a page triggers an async background job that fetches
|
||
the page title, description, and screenshot automatically.
|
||
- name: Posts
|
||
description: Markdown blog posts with optional tag categorisation.
|
||
- name: Bookmarks
|
||
description: |
|
||
Archived social-media posts (originally x.com / Twitter bookmarks), each saved with
|
||
full preview data — text, author, media poster thumbnails (no embedded video streams),
|
||
link cards, quotes — so the archive viewer page renders offline without further API calls.
|
||
Use `POST /api/bookmarks/import` for bulk upsert by `tweet_id`.
|
||
- name: Collections
|
||
description: Named groups of images. Each collection can hold many uploaded image files.
|
||
- name: Images
|
||
description: |
|
||
Image management and random image serving.
|
||
The random image endpoint is useful as a live placeholder — pass `?v={timestamp}`
|
||
to bust the browser cache on each render.
|
||
- name: Files
|
||
description: |
|
||
Generic file uploads with optional public sharing via tokenised URLs.
|
||
Files can be made public/private and given an expiry date.
|
||
|
||
paths:
|
||
/api/links:
|
||
get:
|
||
operationId: listLinks
|
||
tags: [Links]
|
||
summary: List all links
|
||
description: Retrieve a paginated list of all short links ordered by creation date (newest first).
|
||
parameters:
|
||
- in: query
|
||
name: page
|
||
schema:
|
||
type: integer
|
||
default: 1
|
||
description: Page number (1-based).
|
||
- in: query
|
||
name: page_size
|
||
schema:
|
||
type: integer
|
||
default: 10
|
||
maximum: 100
|
||
description: Number of results per page.
|
||
responses:
|
||
'200':
|
||
description: Paginated list of links.
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/LinkList'
|
||
|
||
/api/links/most-visited:
|
||
get:
|
||
operationId: listMostVisitedLinks
|
||
tags: [Links]
|
||
summary: Most visited links
|
||
description: Return links that have at least one click, sorted by click count descending.
|
||
parameters:
|
||
- in: query
|
||
name: page
|
||
schema:
|
||
type: integer
|
||
default: 1
|
||
description: Page number (1-based).
|
||
- in: query
|
||
name: page_size
|
||
schema:
|
||
type: integer
|
||
default: 10
|
||
maximum: 100
|
||
description: Number of results per page.
|
||
responses:
|
||
'200':
|
||
description: Paginated list of links sorted by click_count descending.
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/LinkList'
|
||
|
||
/api/pages:
|
||
get:
|
||
operationId: listPages
|
||
tags: [Pages]
|
||
summary: List all pages
|
||
description: Retrieve a paginated list of all bookmarked pages.
|
||
parameters:
|
||
- in: query
|
||
name: page
|
||
schema:
|
||
type: integer
|
||
description: Page number for pagination
|
||
- in: query
|
||
name: page_size
|
||
schema:
|
||
type: integer
|
||
description: Number of items per page
|
||
responses:
|
||
'200':
|
||
description: Successful response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/PageList'
|
||
|
||
post:
|
||
operationId: createPage
|
||
tags: [Pages]
|
||
summary: Create a new page
|
||
description: |
|
||
Bookmark a URL. After creation the server starts an async background job that fetches
|
||
the page title, description, content, and a screenshot.
|
||
Poll `GET /api/pages` or check `processing_status` to see when it completes.
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/PageCreate'
|
||
responses:
|
||
'201':
|
||
description: Page created successfully
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Page'
|
||
|
||
/api/bookmarks:
|
||
get:
|
||
operationId: listBookmarks
|
||
tags: [Bookmarks]
|
||
summary: List archived bookmarks
|
||
description: |
|
||
Paginated list of archived social-media bookmarks, newest first. Supports
|
||
full-text search (`q`), boolean media/type filters, author filter and
|
||
ordering. Default page size is 24; use `?page_size=` up to 200.
|
||
parameters:
|
||
- in: query
|
||
name: page
|
||
schema: { type: integer }
|
||
description: 1-based page number
|
||
- in: query
|
||
name: page_size
|
||
schema: { type: integer, maximum: 200 }
|
||
description: Items per page (default 24)
|
||
- in: query
|
||
name: q
|
||
schema: { type: string }
|
||
description: Case-insensitive search across `text`, `summary`, `author_screen_name`, `author_name`
|
||
- in: query
|
||
name: has_video
|
||
schema: { type: boolean }
|
||
description: Filter to bookmarks containing a video/animated GIF
|
||
- in: query
|
||
name: has_photo
|
||
schema: { type: boolean }
|
||
- in: query
|
||
name: has_card
|
||
schema: { type: boolean }
|
||
description: Has an attached link preview card
|
||
- in: query
|
||
name: is_quote
|
||
schema: { type: boolean }
|
||
- in: query
|
||
name: is_retweet
|
||
schema: { type: boolean }
|
||
- in: query
|
||
name: author
|
||
schema: { type: string }
|
||
description: Exact (case-insensitive) screen name
|
||
- in: query
|
||
name: ordering
|
||
schema:
|
||
type: string
|
||
enum: [tweet_created_at, -tweet_created_at, bookmark_created_at,
|
||
-bookmark_created_at, favorite_count, -favorite_count,
|
||
view_count, -view_count, created_at, -created_at]
|
||
description: Sort field (prefix `-` for descending). Default `-tweet_created_at`
|
||
responses:
|
||
'200':
|
||
description: Paginated bookmarks
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/BookmarkList'
|
||
post:
|
||
operationId: createBookmark
|
||
tags: [Bookmarks]
|
||
summary: Create a single bookmark
|
||
description: |
|
||
Create one archived bookmark from a flat `Bookmark`-shaped object.
|
||
For importing many at once (e.g. from x-bookmarks-exporter), prefer
|
||
`POST /api/bookmarks/import`.
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/BookmarkCreate'
|
||
responses:
|
||
'201':
|
||
description: Bookmark created
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Bookmark'
|
||
|
||
/api/bookmarks/stats:
|
||
get:
|
||
operationId: bookmarkStats
|
||
tags: [Bookmarks]
|
||
summary: Aggregate bookmark counts
|
||
description: Returns total count plus per-flag counts (used by the viewer's filter chips).
|
||
responses:
|
||
'200':
|
||
description: Aggregate stats
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
properties:
|
||
total: { type: integer }
|
||
has_video: { type: integer }
|
||
has_photo: { type: integer }
|
||
has_card: { type: integer }
|
||
is_quote: { type: integer }
|
||
is_retweet: { type: integer }
|
||
|
||
/api/bookmarks/import:
|
||
post:
|
||
operationId: importBookmarks
|
||
tags: [Bookmarks]
|
||
summary: Bulk upsert bookmarks from export objects
|
||
description: |
|
||
Upserts many bookmarks in one request. Each item in `bookmarks` accepts the
|
||
full normalised export object from `x-bookmarks-exporter` — with nested
|
||
`author`, `media`, `card`, `quoted`, `retweet`, engagement counters and
|
||
`created_at` (x.com-style RFC date string). Matching is by `tweet_id`
|
||
(top-level `id` alias also accepted); existing entries are updated,
|
||
new ones created. Returns `{ created, updated, skipped, total }`.
|
||
|
||
Example body:
|
||
```json
|
||
{
|
||
"bookmarks": [
|
||
{
|
||
"id": "1234567890",
|
||
"url": "https://x.com/user/status/1234567890",
|
||
"author": {"screen_name":"user","name":"User","profile_image_url":"..."},
|
||
"text": "Hello world",
|
||
"media": [{"type":"video","media_url":"...","video":{"poster":"...","best_mp4":"...","duration_ms":12000}}],
|
||
"favorite_count": 5, "view_count": 99
|
||
}
|
||
]
|
||
}
|
||
```
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required: [bookmarks]
|
||
properties:
|
||
bookmarks:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/BookmarkExportItem'
|
||
responses:
|
||
'200':
|
||
description: Upsert summary
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
properties:
|
||
created: { type: integer }
|
||
updated: { type: integer }
|
||
skipped: { type: integer }
|
||
total: { type: integer, description: Total bookmarks now in DB }
|
||
|
||
/api/bookmarks/bulk:
|
||
put:
|
||
operationId: bulkUpsertBookmarks
|
||
tags: [Bookmarks]
|
||
summary: Bulk upsert from a flat JSON array
|
||
description: |
|
||
Like `/import` but accepts a bare JSON array (not wrapped in `{bookmarks:[]}`).
|
||
Each item can be a flat `Bookmark`-shaped object (matching
|
||
`BookmarkCreate`/`Bookmark`) or a normalised export object; `tweet_id`
|
||
(or top-level `id`) is used as the upsert key.
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/BookmarkCreate'
|
||
responses:
|
||
'200':
|
||
description: Upsert summary
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
properties:
|
||
created: { type: integer }
|
||
updated: { type: integer }
|
||
skipped: { type: integer }
|
||
total: { type: integer }
|
||
post:
|
||
operationId: bulkUpsertBookmarksPost
|
||
tags: [Bookmarks]
|
||
summary: Bulk upsert from a flat JSON array (POST alias)
|
||
description: Alias of `PUT /api/bookmarks/bulk` for clients that cannot send PUT.
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/BookmarkCreate'
|
||
responses:
|
||
'200':
|
||
description: Upsert summary
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
properties:
|
||
created: { type: integer }
|
||
updated: { type: integer }
|
||
skipped: { type: integer }
|
||
total: { type: integer }
|
||
|
||
/api/bookmarks/{id}:
|
||
parameters:
|
||
- in: path
|
||
name: id
|
||
required: true
|
||
schema: { type: integer }
|
||
description: Internal DB primary key (NOT the tweet_id)
|
||
get:
|
||
operationId: retrieveBookmark
|
||
tags: [Bookmarks]
|
||
summary: Retrieve a single bookmark
|
||
responses:
|
||
'200':
|
||
description: Bookmark detail
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Bookmark'
|
||
'404':
|
||
description: Not found
|
||
patch:
|
||
operationId: updateBookmark
|
||
tags: [Bookmarks]
|
||
summary: Partially update a bookmark
|
||
description: |
|
||
Update any subset of fields. Common use by agents: write an AI-generated
|
||
`summary` for the post via `{"summary": "..."}`.
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/BookmarkCreate'
|
||
responses:
|
||
'200':
|
||
description: Updated bookmark
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Bookmark'
|
||
delete:
|
||
operationId: deleteBookmark
|
||
tags: [Bookmarks]
|
||
summary: Delete a bookmark
|
||
responses:
|
||
'204':
|
||
description: Deleted
|
||
'404':
|
||
description: Not found
|
||
|
||
/api/posts:
|
||
get:
|
||
operationId: listPosts
|
||
tags: [Posts]
|
||
summary: List all posts
|
||
description: Retrieve a paginated list of all Markdown blog posts.
|
||
parameters:
|
||
- in: query
|
||
name: page
|
||
schema:
|
||
type: integer
|
||
description: Page number for pagination
|
||
- in: query
|
||
name: page_size
|
||
schema:
|
||
type: integer
|
||
description: Number of items per page
|
||
responses:
|
||
'200':
|
||
description: Successful response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/PostList'
|
||
|
||
post:
|
||
operationId: createPost
|
||
tags: [Posts]
|
||
summary: Create a new post
|
||
description: Create a Markdown blog post. Pass tag slugs in the `tags` array to categorise it.
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/PostCreate'
|
||
responses:
|
||
'201':
|
||
description: Post created successfully
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/Post'
|
||
|
||
/api/collections:
|
||
get:
|
||
operationId: listCollections
|
||
tags: [Collections]
|
||
summary: List all collections
|
||
description: Retrieve a paginated list of all image collections.
|
||
parameters:
|
||
- in: query
|
||
name: page
|
||
schema:
|
||
type: integer
|
||
description: Page number for pagination
|
||
- in: query
|
||
name: page_size
|
||
schema:
|
||
type: integer
|
||
description: Number of items per page
|
||
responses:
|
||
'200':
|
||
description: Successful response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/CollectionList'
|
||
|
||
post:
|
||
operationId: createCollection
|
||
tags: [Collections]
|
||
summary: Create a new collection
|
||
description: Create a new named image collection.
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/CollectionCreate'
|
||
responses:
|
||
'201':
|
||
description: Collection created successfully
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/CollectionResponse'
|
||
|
||
/api/collections/{collection_id}:
|
||
delete:
|
||
operationId: deleteCollection
|
||
tags: [Collections]
|
||
summary: Delete a collection
|
||
description: Permanently delete a collection and all its images.
|
||
parameters:
|
||
- in: path
|
||
name: collection_id
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
description: The ID of the collection to delete
|
||
responses:
|
||
'200':
|
||
description: Collection deleted successfully
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/SuccessResponse'
|
||
|
||
/api/collections/{collection_id}/upload_images:
|
||
post:
|
||
operationId: uploadImagesToCollection
|
||
tags: [Collections]
|
||
summary: Upload images to a collection
|
||
description: |
|
||
Upload one or more images to a collection. Only `image/*` content types are accepted.
|
||
Optionally provide a parallel `descriptions[]` array — each entry maps to the image at the same index.
|
||
parameters:
|
||
- in: path
|
||
name: collection_id
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
description: The ID of the collection to upload images to
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
multipart/form-data:
|
||
schema:
|
||
type: object
|
||
properties:
|
||
file:
|
||
type: array
|
||
items:
|
||
type: string
|
||
format: binary
|
||
description: List of image files to upload. Only image/* content types are allowed.
|
||
descriptions:
|
||
type: array
|
||
items:
|
||
type: string
|
||
description: Optional list of descriptions for the uploaded images. Each description corresponds to the image at the same index.
|
||
required:
|
||
- file
|
||
responses:
|
||
'201':
|
||
description: Images uploaded successfully
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
properties:
|
||
status:
|
||
type: string
|
||
enum: [success]
|
||
example: success
|
||
message:
|
||
type: string
|
||
example: "Successfully uploaded 2 images"
|
||
data:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/Image'
|
||
|
||
/api/images/{image_id}:
|
||
delete:
|
||
operationId: deleteImage
|
||
tags: [Images]
|
||
summary: Delete an image
|
||
description: Permanently delete a single image from its collection.
|
||
parameters:
|
||
- in: path
|
||
name: image_id
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
description: The ID of the image to delete
|
||
responses:
|
||
'200':
|
||
description: Image deleted successfully
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/SuccessResponse'
|
||
|
||
/api/files:
|
||
get:
|
||
operationId: listFiles
|
||
tags: [Files]
|
||
summary: List all uploaded files
|
||
description: Retrieve a paginated list of all uploaded files.
|
||
parameters:
|
||
- in: query
|
||
name: page
|
||
schema:
|
||
type: integer
|
||
description: Page number for pagination
|
||
responses:
|
||
'200':
|
||
description: Successful response
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/FileList'
|
||
|
||
post:
|
||
operationId: uploadFiles
|
||
tags: [Files]
|
||
summary: Upload one or more files
|
||
description: |
|
||
Upload one or multiple files via `multipart/form-data`. Files are stored server-side
|
||
in the configured `FILE_UPLOADS_FOLDER`. Returns an array of `FileUpload` objects.
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
multipart/form-data:
|
||
schema:
|
||
type: object
|
||
required:
|
||
- files
|
||
properties:
|
||
files:
|
||
type: array
|
||
items:
|
||
type: string
|
||
format: binary
|
||
description: One or more files to upload
|
||
responses:
|
||
'201':
|
||
description: Files uploaded successfully
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/FileUpload'
|
||
'400':
|
||
description: No files provided
|
||
'500':
|
||
description: >-
|
||
None of the uploaded files could be fully persisted to storage
|
||
(e.g. a storage write failure or truncated upload). The response
|
||
body includes an `errors` array with a `name`/`error` entry per
|
||
failed file.
|
||
|
||
/api/files/{id}:
|
||
get:
|
||
operationId: getFile
|
||
tags: [Files]
|
||
summary: Get file details
|
||
description: Fetch metadata for a single file by its UUID.
|
||
parameters:
|
||
- in: path
|
||
name: id
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
responses:
|
||
'200':
|
||
description: File details
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/FileUpload'
|
||
'404':
|
||
description: File not found
|
||
|
||
delete:
|
||
operationId: deleteFile
|
||
tags: [Files]
|
||
summary: Delete a file
|
||
description: Permanently delete the file record and remove it from disk.
|
||
parameters:
|
||
- in: path
|
||
name: id
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
responses:
|
||
'204':
|
||
description: File deleted
|
||
'404':
|
||
description: File not found
|
||
|
||
/api/files/{id}/download:
|
||
get:
|
||
operationId: downloadFile
|
||
tags: [Files]
|
||
summary: Download or view a file
|
||
description: |
|
||
Serve the raw file bytes. Images, videos, and audio are returned with
|
||
`Content-Disposition: inline` (viewable in browser); all other files use
|
||
`attachment` (triggers download).
|
||
|
||
**HTTP Range requests are supported.** Send a `Range: bytes=start-end`
|
||
header to fetch a byte range; the server replies with `206 Partial Content`,
|
||
a `Content-Range: bytes start-end/total` header, and `Accept-Ranges: bytes`.
|
||
This is what enables seeking inside `<video>` / `<audio>` elements.
|
||
parameters:
|
||
- in: path
|
||
name: id
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
- in: header
|
||
name: Range
|
||
required: false
|
||
schema:
|
||
type: string
|
||
example: bytes=0-1023
|
||
description: |
|
||
Optional byte range. When present the response is `206 Partial Content`
|
||
with `Content-Range` and `Accept-Ranges` headers.
|
||
responses:
|
||
'200':
|
||
description: Full file content (no Range header sent).
|
||
headers:
|
||
Content-Disposition:
|
||
schema:
|
||
type: string
|
||
description: '`inline` for media, `attachment` for other files.'
|
||
Accept-Ranges:
|
||
schema:
|
||
type: string
|
||
enum: [bytes]
|
||
Content-Length:
|
||
schema:
|
||
type: integer
|
||
'206':
|
||
description: Partial content in response to a `Range` request.
|
||
headers:
|
||
Content-Range:
|
||
schema:
|
||
type: string
|
||
example: bytes 0-1023/9457629
|
||
Accept-Ranges:
|
||
schema:
|
||
type: string
|
||
enum: [bytes]
|
||
Content-Length:
|
||
schema:
|
||
type: integer
|
||
'416':
|
||
description: Range Not Satisfiable (invalid byte range).
|
||
'404':
|
||
description: File not found
|
||
|
||
/api/files/{id}/toggle-public:
|
||
post:
|
||
operationId: toggleFilePublic
|
||
tags: [Files]
|
||
summary: Toggle public/private visibility
|
||
description: |
|
||
Toggle a file between public and private:
|
||
- **Private → Public**: returns `is_public: true` and a `public_url`
|
||
of the form `/public/files/{id}-{filename}` that anyone can access
|
||
without authentication.
|
||
- **Public → Private**: returns `is_public: false` and `public_url: null`;
|
||
existing `public_url` links immediately return 404.
|
||
parameters:
|
||
- in: path
|
||
name: id
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
responses:
|
||
'200':
|
||
description: Updated visibility state
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
properties:
|
||
is_public:
|
||
type: boolean
|
||
public_url:
|
||
type: string
|
||
nullable: true
|
||
example: /public/files/550e8400-e29b-41d4-a716-446655440000-report.pdf
|
||
required:
|
||
- is_public
|
||
- public_url
|
||
|
||
/api/files/{id}/set-expiry:
|
||
post:
|
||
operationId: setFileExpiry
|
||
tags: [Files]
|
||
summary: Set or clear expiry date for a public file
|
||
description: |
|
||
Set an expiry datetime for a public file. After the expiry the `public_url` returns 404.
|
||
Send `expires_at: null` (or omit the field) to remove the expiry and make the link permanent.
|
||
parameters:
|
||
- in: path
|
||
name: id
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
properties:
|
||
expires_at:
|
||
type: string
|
||
format: date-time
|
||
nullable: true
|
||
description: ISO 8601 datetime. Send null or omit to clear the expiry.
|
||
responses:
|
||
'200':
|
||
description: Updated file details
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/FileUpload'
|
||
|
||
/public/files/{id}-{filename}:
|
||
get:
|
||
operationId: getPublicFile
|
||
tags: [Files]
|
||
summary: Access a public file by its shareable URL
|
||
description: |
|
||
Unauthenticated endpoint. Serves a publicly shared file. The URL is derived
|
||
from the file's UUID and original filename (filename is cosmetic — the server
|
||
only looks up the record by `id`). Images, videos, and audio are served
|
||
`inline` (with HTTP Range support for seeking in media); other files use
|
||
`attachment` (download).
|
||
|
||
External requests (outside the internal network) only get served files that
|
||
have `is_public: true` and have not expired. Internal-network requests can
|
||
fetch any file regardless of the public flag.
|
||
parameters:
|
||
- in: path
|
||
name: id
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
description: The FileUpload UUID.
|
||
- in: path
|
||
name: filename
|
||
required: true
|
||
schema:
|
||
type: string
|
||
description: The original filename (cosmetic only; ignored by the server).
|
||
- in: header
|
||
name: Range
|
||
required: false
|
||
schema:
|
||
type: string
|
||
description: Optional byte range for media seeking.
|
||
responses:
|
||
'200':
|
||
description: Full file content (no Range header).
|
||
headers:
|
||
Content-Disposition:
|
||
schema:
|
||
type: string
|
||
Accept-Ranges:
|
||
schema:
|
||
type: string
|
||
enum: [bytes]
|
||
'206':
|
||
description: Partial content in response to a `Range` request.
|
||
headers:
|
||
Content-Range:
|
||
schema:
|
||
type: string
|
||
Accept-Ranges:
|
||
schema:
|
||
type: string
|
||
enum: [bytes]
|
||
'404':
|
||
description: File not found, is private (from external network), or link has expired.
|
||
|
||
/api/images/random/:
|
||
get:
|
||
operationId: getRandomImage
|
||
tags: [Images]
|
||
summary: Get a random image
|
||
description: |
|
||
Returns a randomly selected image from the server's image library, resized to the
|
||
default dimensions. Each request returns a different image.
|
||
|
||
**Cache busting:** Add a `?v={timestamp}` query parameter to prevent browsers from
|
||
caching repeated calls (the server ignores this parameter).
|
||
parameters:
|
||
- in: query
|
||
name: fit
|
||
schema:
|
||
type: string
|
||
enum: [clip, crop, fill, scale]
|
||
default: scale
|
||
description: |
|
||
How to fit the image into the requested dimensions.
|
||
- `scale` — scale to fit, maintaining aspect ratio (may add letterbox)
|
||
- `crop` — crop to exact dimensions
|
||
- `clip` — clip to fit, no upscaling
|
||
- `fill` — fill exact dimensions, may distort
|
||
- in: query
|
||
name: v
|
||
schema:
|
||
type: integer
|
||
description: Cache-busting value (e.g. Unix timestamp). Ignored by the server.
|
||
example: 1774152070
|
||
responses:
|
||
'200':
|
||
description: Image file (JPEG or PNG)
|
||
headers:
|
||
Cache-Control:
|
||
schema:
|
||
type: string
|
||
description: Caching directives (e.g. `public, max-age=3600`)
|
||
X-Image-Source:
|
||
schema:
|
||
type: string
|
||
description: Relative path of the source image that was served
|
||
content:
|
||
image/jpeg:
|
||
schema:
|
||
type: string
|
||
format: binary
|
||
image/png:
|
||
schema:
|
||
type: string
|
||
format: binary
|
||
'404':
|
||
description: |
|
||
No images available, folder not configured, or invalid parameters.
|
||
|
||
/api/images/random/{width}/{height}/:
|
||
get:
|
||
operationId: getRandomImageSized
|
||
tags: [Images]
|
||
summary: Get a random image at specific dimensions
|
||
description: |
|
||
Returns a randomly selected image resized to `{width}×{height}` pixels.
|
||
Minimum 300 px and maximum 4096 px for each dimension.
|
||
|
||
**Cache busting:** Add a `?v={timestamp}` query parameter to prevent browsers from
|
||
caching repeated calls (the server ignores this parameter).
|
||
|
||
**Use in Markdown / posts:**
|
||
```
|
||

|
||
```
|
||
parameters:
|
||
- in: path
|
||
name: width
|
||
required: true
|
||
schema:
|
||
type: integer
|
||
minimum: 300
|
||
maximum: 4096
|
||
description: Target width in pixels
|
||
example: 300
|
||
- in: path
|
||
name: height
|
||
required: true
|
||
schema:
|
||
type: integer
|
||
minimum: 300
|
||
maximum: 4096
|
||
description: Target height in pixels
|
||
example: 450
|
||
- in: query
|
||
name: fit
|
||
schema:
|
||
type: string
|
||
enum: [clip, crop, fill, scale]
|
||
default: scale
|
||
description: |
|
||
How to fit the image into the requested dimensions.
|
||
- `scale` — scale to fit, maintaining aspect ratio (may add letterbox)
|
||
- `crop` — crop to exact dimensions
|
||
- `clip` — clip to fit, no upscaling
|
||
- `fill` — fill exact dimensions, may distort
|
||
- in: query
|
||
name: v
|
||
schema:
|
||
type: integer
|
||
description: Cache-busting value (e.g. Unix timestamp). Ignored by the server.
|
||
example: 1774152070
|
||
responses:
|
||
'200':
|
||
description: Image file (JPEG or PNG) at the requested dimensions
|
||
headers:
|
||
Cache-Control:
|
||
schema:
|
||
type: string
|
||
X-Image-Source:
|
||
schema:
|
||
type: string
|
||
description: Relative path of the source image that was served
|
||
content:
|
||
image/jpeg:
|
||
schema:
|
||
type: string
|
||
format: binary
|
||
image/png:
|
||
schema:
|
||
type: string
|
||
format: binary
|
||
'404':
|
||
description: |
|
||
No images available, folder not configured, invalid dimensions, or invalid fit mode.
|
||
|
||
/import/images/{image_url}:
|
||
get:
|
||
operationId: importExternalImage
|
||
tags: [Files]
|
||
summary: Import and cache an external image
|
||
description: |
|
||
Imports an external image by URL and saves it to the local file store.
|
||
|
||
The `image_url` path parameter is the image URL **without** the `https://` scheme prefix
|
||
(e.g. `example.com/path/to/image.jpg`). The server always tries `https://` first.
|
||
|
||
**Worker mode** — this endpoint is non-blocking:
|
||
- On the **first request** for a given URL the server creates a `FileUpload` record
|
||
(marked `is_public: true`) and kicks off a background thread to download and save the file.
|
||
The response immediately redirects (HTTP 302) to the original `https://` source URL so the
|
||
image is visible right away.
|
||
- On **subsequent requests**, once the background download has completed, the response
|
||
redirects to the locally-saved public file URL (`/public/files/{uuid}-{filename}`) so the
|
||
original host is no longer needed.
|
||
|
||
**Idempotent** — the same external URL always resolves to the same `FileUpload` record;
|
||
the file is only downloaded once.
|
||
|
||
**Use in Markdown / posts:**
|
||
```
|
||

|
||
```
|
||
parameters:
|
||
- in: path
|
||
name: image_url
|
||
required: true
|
||
schema:
|
||
type: string
|
||
description: |
|
||
External image URL without the scheme prefix.
|
||
Example: `graziamagazine.com/wp-content/uploads/2024/12/Elle-Fanning-Pigtails-scaled.jpg`
|
||
example: graziamagazine.com/wp-content/uploads/2024/12/Elle-Fanning-Pigtails-scaled.jpg
|
||
responses:
|
||
'302':
|
||
description: |
|
||
Redirect to either the original source URL (while background download is in progress)
|
||
or the locally-saved public file URL (once download has completed).
|
||
headers:
|
||
Location:
|
||
schema:
|
||
type: string
|
||
description: URL to redirect to (original source or local `/public/files/…`)
|
||
|
||
components:
|
||
schemas:
|
||
Page:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: integer
|
||
url:
|
||
type: string
|
||
title:
|
||
type: string
|
||
summary:
|
||
type: string
|
||
content:
|
||
type: string
|
||
created_at:
|
||
type: string
|
||
format: date-time
|
||
updated_at:
|
||
type: string
|
||
format: date-time
|
||
|
||
PageCreate:
|
||
type: object
|
||
required:
|
||
- url
|
||
- title
|
||
properties:
|
||
url:
|
||
type: string
|
||
title:
|
||
type: string
|
||
summary:
|
||
type: string
|
||
content:
|
||
type: string
|
||
|
||
PageList:
|
||
type: object
|
||
properties:
|
||
count:
|
||
type: integer
|
||
next:
|
||
type: string
|
||
nullable: true
|
||
previous:
|
||
type: string
|
||
nullable: true
|
||
results:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/Page'
|
||
|
||
Post:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: integer
|
||
title:
|
||
type: string
|
||
content:
|
||
type: string
|
||
tag_details:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/Tag'
|
||
created_at:
|
||
type: string
|
||
format: date-time
|
||
updated_at:
|
||
type: string
|
||
format: date-time
|
||
|
||
PostCreate:
|
||
type: object
|
||
required:
|
||
- title
|
||
- content
|
||
properties:
|
||
title:
|
||
type: string
|
||
content:
|
||
type: string
|
||
tags:
|
||
type: array
|
||
items:
|
||
type: string
|
||
description: List of tag slugs to associate with the post
|
||
|
||
PostList:
|
||
type: object
|
||
properties:
|
||
count:
|
||
type: integer
|
||
next:
|
||
type: string
|
||
nullable: true
|
||
previous:
|
||
type: string
|
||
nullable: true
|
||
results:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/Post'
|
||
|
||
Tag:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: integer
|
||
name:
|
||
type: string
|
||
slug:
|
||
type: string
|
||
description:
|
||
type: string
|
||
|
||
Collection:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
name:
|
||
type: string
|
||
description:
|
||
type: string
|
||
image_count:
|
||
type: integer
|
||
created_at:
|
||
type: string
|
||
format: date-time
|
||
|
||
CollectionCreate:
|
||
type: object
|
||
required:
|
||
- name
|
||
properties:
|
||
name:
|
||
type: string
|
||
description:
|
||
type: string
|
||
|
||
CollectionList:
|
||
type: object
|
||
properties:
|
||
count:
|
||
type: integer
|
||
next:
|
||
type: string
|
||
nullable: true
|
||
previous:
|
||
type: string
|
||
nullable: true
|
||
results:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/Collection'
|
||
|
||
CollectionResponse:
|
||
type: object
|
||
properties:
|
||
status:
|
||
type: string
|
||
enum: [success]
|
||
message:
|
||
type: string
|
||
data:
|
||
$ref: '#/components/schemas/Collection'
|
||
|
||
Image:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
collection:
|
||
type: string
|
||
format: uuid
|
||
title:
|
||
type: string
|
||
description:
|
||
type: string
|
||
content_type:
|
||
type: string
|
||
size:
|
||
type: integer
|
||
created_at:
|
||
type: string
|
||
format: date-time
|
||
updated_at:
|
||
type: string
|
||
format: date-time
|
||
url:
|
||
type: string
|
||
|
||
FileUpload:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
readOnly: true
|
||
name:
|
||
type: string
|
||
description: Original filename
|
||
mime_type:
|
||
type: string
|
||
readOnly: true
|
||
size:
|
||
type: integer
|
||
readOnly: true
|
||
description: File size in bytes
|
||
formatted_size:
|
||
type: string
|
||
readOnly: true
|
||
description: Human-readable size (e.g. "1.4 MB")
|
||
is_public:
|
||
type: boolean
|
||
public_token:
|
||
type: string
|
||
nullable: true
|
||
readOnly: true
|
||
public_url:
|
||
type: string
|
||
nullable: true
|
||
readOnly: true
|
||
example: /public/files/<token>/
|
||
expires_at:
|
||
type: string
|
||
format: date-time
|
||
nullable: true
|
||
is_expired:
|
||
type: boolean
|
||
readOnly: true
|
||
download_count:
|
||
type: integer
|
||
readOnly: true
|
||
source_url:
|
||
type: string
|
||
format: uri
|
||
nullable: true
|
||
description: |
|
||
Original external URL this file was imported from via `/import/images/…`.
|
||
`null` for files uploaded directly.
|
||
example: https://example.com/path/to/image.jpg
|
||
created_at:
|
||
type: string
|
||
format: date-time
|
||
readOnly: true
|
||
updated_at:
|
||
type: string
|
||
format: date-time
|
||
readOnly: true
|
||
|
||
FileList:
|
||
type: object
|
||
properties:
|
||
count:
|
||
type: integer
|
||
next:
|
||
type: string
|
||
nullable: true
|
||
previous:
|
||
type: string
|
||
nullable: true
|
||
results:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/FileUpload'
|
||
|
||
SuccessResponse:
|
||
type: object
|
||
properties:
|
||
status:
|
||
type: string
|
||
enum: [success]
|
||
message:
|
||
type: string
|
||
|
||
TagSummary:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: integer
|
||
name:
|
||
type: string
|
||
slug:
|
||
type: string
|
||
|
||
Link:
|
||
type: object
|
||
properties:
|
||
id:
|
||
type: integer
|
||
readOnly: true
|
||
alias:
|
||
type: string
|
||
description: Unique slug used as the short-link identifier (e.g. /gh → github.com).
|
||
original_url:
|
||
type: string
|
||
description: Target URL. May contain template parameters like `{param,default=value}`.
|
||
description:
|
||
type: string
|
||
nullable: true
|
||
link_type:
|
||
type: string
|
||
enum: [LINK, CUSTOM]
|
||
description: LINK redirects to original_url; CUSTOM renders Markdown content.
|
||
click_count:
|
||
type: integer
|
||
readOnly: true
|
||
description: Number of times the short link has been visited.
|
||
tags:
|
||
type: array
|
||
readOnly: true
|
||
items:
|
||
$ref: '#/components/schemas/TagSummary'
|
||
created_at:
|
||
type: string
|
||
format: date-time
|
||
readOnly: true
|
||
updated_at:
|
||
type: string
|
||
format: date-time
|
||
readOnly: true
|
||
|
||
LinkList:
|
||
type: object
|
||
properties:
|
||
count:
|
||
type: integer
|
||
next:
|
||
type: string
|
||
nullable: true
|
||
previous:
|
||
type: string
|
||
nullable: true
|
||
results:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/Link'
|
||
|
||
Bookmark:
|
||
type: object
|
||
description: |
|
||
An archived social-media post. The `data` field holds the complete original
|
||
normalised export object (kept for round-tripping / the offline viewer page);
|
||
the top-level denormalised fields (text, media, card_data, quoted_data,
|
||
flags, counters) are the indexed/fast-path versions used by list & filter.
|
||
properties:
|
||
id: { type: integer, description: Internal DB primary key }
|
||
tweet_id: { type: string, description: Source post id (unique) }
|
||
url: { type: string, format: uri, description: Canonical x.com post URL }
|
||
author_screen_name: { type: string }
|
||
author_name: { type: string }
|
||
author_profile_image_url: { type: string, format: uri }
|
||
text: { type: string, description: Full post text }
|
||
summary: { type: string, description: Optional human/agent-written summary }
|
||
tweet_created_at: { type: string, format: date-time, nullable: true }
|
||
bookmark_created_at: { type: string, format: date-time, nullable: true }
|
||
view_count: { type: integer }
|
||
favorite_count: { type: integer }
|
||
retweet_count: { type: integer }
|
||
reply_count: { type: integer }
|
||
bookmark_count: { type: integer }
|
||
quote_count: { type: integer }
|
||
has_video: { type: boolean }
|
||
has_photo: { type: boolean }
|
||
has_card: { type: boolean }
|
||
is_quote: { type: boolean }
|
||
is_retweet: { type: boolean }
|
||
media:
|
||
type: array
|
||
description: List of media objects (photo or video/animated_gif with poster)
|
||
items: { type: object }
|
||
card_data:
|
||
type: object
|
||
nullable: true
|
||
description: Link preview card (title, description, site, image, url)
|
||
quoted_data:
|
||
type: object
|
||
nullable: true
|
||
description: Nested quoted post (same shape as an export item)
|
||
data:
|
||
type: object
|
||
description: The complete normalised export object from x-bookmarks-exporter
|
||
created_at: { type: string, format: date-time }
|
||
updated_at: { type: string, format: date-time }
|
||
|
||
BookmarkCreate:
|
||
type: object
|
||
required: [tweet_id]
|
||
properties:
|
||
tweet_id: { type: string }
|
||
url: { type: string, format: uri }
|
||
author_screen_name: { type: string }
|
||
author_name: { type: string }
|
||
author_profile_image_url: { type: string, format: uri }
|
||
text: { type: string }
|
||
summary: { type: string }
|
||
tweet_created_at: { type: string, format: date-time, nullable: true }
|
||
bookmark_created_at: { type: string, format: date-time, nullable: true }
|
||
view_count: { type: integer }
|
||
favorite_count: { type: integer }
|
||
retweet_count: { type: integer }
|
||
reply_count: { type: integer }
|
||
bookmark_count: { type: integer }
|
||
quote_count: { type: integer }
|
||
has_video: { type: boolean }
|
||
has_photo: { type: boolean }
|
||
has_card: { type: boolean }
|
||
is_quote: { type: boolean }
|
||
is_retweet: { type: boolean }
|
||
media:
|
||
type: array
|
||
items: { type: object }
|
||
card_data:
|
||
type: object
|
||
nullable: true
|
||
quoted_data:
|
||
type: object
|
||
nullable: true
|
||
data:
|
||
type: object
|
||
|
||
BookmarkExportItem:
|
||
type: object
|
||
description: |
|
||
One element of a bulk-import payload, as produced by x-bookmarks-exporter.
|
||
All nested fields are optional except the source post id; the server maps
|
||
the nested `author`, `media`, `card`, `quoted`, `retweet`, counters and
|
||
`created_at` date string into the flat Bookmark columns and the `data`
|
||
JSON blob.
|
||
properties:
|
||
id: { type: string, description: Source tweet id (also accepted as `tweet_id`) }
|
||
url: { type: string, format: uri }
|
||
author:
|
||
type: object
|
||
properties:
|
||
screen_name: { type: string }
|
||
name: { type: string }
|
||
profile_image_url: { type: string, format: uri }
|
||
verified: { type: boolean }
|
||
description: { type: string }
|
||
text: { type: string }
|
||
summary: { type: string }
|
||
created_at: { type: string, description: x.com-style "Wed Jul 30 12:00:00 +0000 2025" }
|
||
bookmark_created_at: { type: string }
|
||
view_count: { type: integer }
|
||
favorite_count: { type: integer }
|
||
retweet_count: { type: integer }
|
||
reply_count: { type: integer }
|
||
bookmark_count: { type: integer }
|
||
quote_count: { type: integer }
|
||
media:
|
||
type: array
|
||
description: |
|
||
Each media object carries `type` (photo|video|animated_gif), image
|
||
URL variants (`media_url`, `media_thumb`, `media_large`, `media_orig`)
|
||
and, for video/gif, a `video` object with `poster`, `best_mp4`,
|
||
`duration_ms`, and `variants[]`.
|
||
items: { type: object }
|
||
card:
|
||
type: object
|
||
nullable: true
|
||
description: Link preview with title/description/site/image/url
|
||
quoted:
|
||
type: object
|
||
nullable: true
|
||
description: Nested quoted post (recursive BookmarkExportItem shape)
|
||
retweet:
|
||
type: object
|
||
nullable: true
|
||
description: Nested retweeted post
|
||
|
||
BookmarkList:
|
||
type: object
|
||
properties:
|
||
count: { type: integer }
|
||
next: { type: string, nullable: true }
|
||
previous: { type: string, nullable: true }
|
||
results:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/Bookmark'
|