Add bookmarks

This commit is contained in:
2026-07-30 13:25:36 +10:00
parent d5fd6b5cac
commit d857eb58c5
22 changed files with 1895 additions and 72 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+425
View File
@@ -13,6 +13,7 @@ info:
| 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. |
@@ -41,6 +42,20 @@ info:
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
@@ -60,6 +75,12 @@ tags:
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
@@ -176,6 +197,266 @@ paths:
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
@@ -1182,3 +1463,147 @@ components:
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'