Files
Yuan dcd0940f6c fix(backend): improve Fever API client compatibility
Align Fever endpoints and payload semantics with common clients by fixing mark/read flows, adding before-based read markers, and completing feeds/items/favicons responses for stable sync behavior.

Refs: https://github.com/0x2E/fusion/issues/136
2026-02-22 15:58:19 +08:00

1192 lines
28 KiB
YAML

openapi: 3.0.3
info:
title: Fusion API
version: 1.0.0
description: >-
OpenAPI contract for Fusion backend. All endpoints are under /api.
Authentication uses a session cookie named `session`.
Fever compatibility endpoints (/fever, /fever/, /fever.php) are documented
separately in docs/fever-api.md.
servers:
- url: /api
tags:
- name: Sessions
- name: OIDC
- name: Groups
- name: Feeds
- name: Items
- name: Search
- name: Bookmarks
security:
- sessionCookie: []
paths:
/sessions:
post:
tags: [Sessions]
summary: Login with password
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/LoginRequest"
responses:
"200":
description: Logged in
headers:
Set-Cookie:
description: Session cookie
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/MessageEnvelope"
example:
data:
message: logged in
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/InternalServerError"
delete:
tags: [Sessions]
summary: Logout
security: []
responses:
"204":
description: Logged out
headers:
Set-Cookie:
description: Cleared session cookie
schema:
type: string
/oidc/enabled:
get:
tags: [OIDC]
summary: Check whether OIDC is enabled
security: []
responses:
"200":
description: OIDC status
content:
application/json:
schema:
$ref: "#/components/schemas/OIDCEnabledEnvelope"
/oidc/login:
get:
tags: [OIDC]
summary: Get OIDC authorization URL
description: Endpoint is available only when OIDC is configured.
security: []
responses:
"200":
description: Authorization URL
content:
application/json:
schema:
$ref: "#/components/schemas/OIDCLoginEnvelope"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalServerError"
/oidc/callback:
get:
tags: [OIDC]
summary: OIDC callback endpoint
description: Exchanges code for identity, creates session, and redirects.
security: []
parameters:
- name: state
in: query
required: true
schema:
type: string
- name: code
in: query
required: true
schema:
type: string
responses:
"307":
description: Redirect to app or login error page
headers:
Location:
schema:
type: string
/groups:
get:
tags: [Groups]
summary: List groups
responses:
"200":
description: Group list
content:
application/json:
schema:
$ref: "#/components/schemas/GroupListEnvelope"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalServerError"
post:
tags: [Groups]
summary: Create group
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/GroupUpsertRequest"
responses:
"200":
description: Group created
content:
application/json:
schema:
$ref: "#/components/schemas/GroupEnvelope"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalServerError"
/groups/{id}:
parameters:
- $ref: "#/components/parameters/IdPath"
get:
tags: [Groups]
summary: Get group
responses:
"200":
description: Group detail
content:
application/json:
schema:
$ref: "#/components/schemas/GroupEnvelope"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
patch:
tags: [Groups]
summary: Update group
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/GroupUpsertRequest"
responses:
"200":
description: Group updated
content:
application/json:
schema:
$ref: "#/components/schemas/GroupEnvelope"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
delete:
tags: [Groups]
summary: Delete group
description: Group id=1 cannot be deleted. Feeds are moved to group id=1 before delete.
responses:
"204":
description: Group deleted
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
/feeds:
get:
tags: [Feeds]
summary: List feeds
responses:
"200":
description: Feed list
content:
application/json:
schema:
$ref: "#/components/schemas/FeedListEnvelope"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalServerError"
post:
tags: [Feeds]
summary: Create feed
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateFeedRequest"
responses:
"200":
description: Feed created
content:
application/json:
schema:
$ref: "#/components/schemas/FeedEnvelope"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalServerError"
/feeds/batch:
post:
tags: [Feeds]
summary: Batch create feeds
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/BatchCreateFeedsRequest"
responses:
"200":
description: Batch create result
content:
application/json:
schema:
$ref: "#/components/schemas/BatchCreateFeedsEnvelope"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalServerError"
/feeds/validate:
post:
tags: [Feeds]
summary: Discover feed URLs from input URL
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ValidateFeedRequest"
responses:
"200":
description: Discovered feeds
content:
application/json:
schema:
$ref: "#/components/schemas/ValidateFeedEnvelope"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
/feeds/refresh:
post:
tags: [Feeds]
summary: Trigger async refresh for all non-suspended feeds
responses:
"202":
description: Refresh accepted
"401":
$ref: "#/components/responses/Unauthorized"
/feeds/{id}:
parameters:
- $ref: "#/components/parameters/IdPath"
get:
tags: [Feeds]
summary: Get feed
responses:
"200":
description: Feed detail
content:
application/json:
schema:
$ref: "#/components/schemas/FeedEnvelope"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
patch:
tags: [Feeds]
summary: Update feed
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateFeedRequest"
responses:
"200":
description: Feed updated
content:
application/json:
schema:
$ref: "#/components/schemas/FeedEnvelope"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
delete:
tags: [Feeds]
summary: Delete feed
description: Deletes feed and its items. Matching bookmarks keep snapshot with item_id set to null.
responses:
"204":
description: Feed deleted
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
/feeds/{id}/refresh:
parameters:
- $ref: "#/components/parameters/IdPath"
post:
tags: [Feeds]
summary: Trigger async refresh for one feed
responses:
"202":
description: Refresh accepted
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
/items:
get:
tags: [Items]
summary: List items
parameters:
- name: feed_id
in: query
schema:
type: integer
format: int64
- name: group_id
in: query
schema:
type: integer
format: int64
- name: unread
in: query
schema:
type: boolean
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 10
- name: offset
in: query
schema:
type: integer
minimum: 0
default: 0
- name: order_by
in: query
schema:
type: string
enum: [pub_date, created_at]
default: pub_date
responses:
"200":
description: Item list
content:
application/json:
schema:
$ref: "#/components/schemas/ItemListEnvelope"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalServerError"
/items/{id}:
parameters:
- $ref: "#/components/parameters/IdPath"
get:
tags: [Items]
summary: Get item
responses:
"200":
description: Item detail
content:
application/json:
schema:
$ref: "#/components/schemas/ItemEnvelope"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
/items/-/read:
patch:
tags: [Items]
summary: Mark items as read
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MarkItemsRequest"
responses:
"204":
description: Read state updated
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalServerError"
/items/-/unread:
patch:
tags: [Items]
summary: Mark items as unread
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MarkItemsRequest"
responses:
"204":
description: Read state updated
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalServerError"
/search:
get:
tags: [Search]
summary: Search feeds and items
parameters:
- name: q
in: query
required: true
schema:
type: string
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 10
responses:
"200":
description: Search result
content:
application/json:
schema:
$ref: "#/components/schemas/SearchEnvelope"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalServerError"
/bookmarks:
get:
tags: [Bookmarks]
summary: List bookmarks
parameters:
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 50
- name: offset
in: query
schema:
type: integer
minimum: 0
default: 0
responses:
"200":
description: Bookmark list
content:
application/json:
schema:
$ref: "#/components/schemas/BookmarkListEnvelope"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/InternalServerError"
post:
tags: [Bookmarks]
summary: Create bookmark
description: >-
Supports two modes:
1) provide item_id only; backend snapshots fields from item
2) provide snapshot fields directly (link, title, content, feed_name; pub_date optional)
requestBody:
required: true
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/CreateBookmarkFromItemRequest"
- $ref: "#/components/schemas/CreateBookmarkSnapshotRequest"
responses:
"200":
description: Bookmark created
content:
application/json:
schema:
$ref: "#/components/schemas/BookmarkEnvelope"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
/bookmarks/{id}:
parameters:
- $ref: "#/components/parameters/IdPath"
get:
tags: [Bookmarks]
summary: Get bookmark
responses:
"200":
description: Bookmark detail
content:
application/json:
schema:
$ref: "#/components/schemas/BookmarkEnvelope"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
delete:
tags: [Bookmarks]
summary: Delete bookmark
responses:
"204":
description: Bookmark deleted
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalServerError"
components:
securitySchemes:
sessionCookie:
type: apiKey
in: cookie
name: session
parameters:
IdPath:
name: id
in: path
required: true
schema:
type: integer
format: int64
responses:
BadRequest:
description: Invalid request
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
error: invalid request
Unauthorized:
description: Unauthorized
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
error: unauthorized
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
error: resource not found
TooManyRequests:
description: Too many login attempts
headers:
Retry-After:
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
error: too many login attempts
InternalServerError:
description: Internal server error
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
example:
error: internal server error
schemas:
ErrorResponse:
type: object
required: [error]
properties:
error:
type: string
Message:
type: object
required: [message]
properties:
message:
type: string
MessageEnvelope:
type: object
required: [data]
properties:
data:
$ref: "#/components/schemas/Message"
LoginRequest:
type: object
required: [password]
properties:
password:
type: string
GroupUpsertRequest:
type: object
required: [name]
properties:
name:
type: string
Group:
type: object
required: [id, name, created_at, updated_at]
properties:
id:
type: integer
format: int64
name:
type: string
created_at:
type: integer
format: int64
updated_at:
type: integer
format: int64
GroupEnvelope:
type: object
required: [data]
properties:
data:
$ref: "#/components/schemas/Group"
GroupListEnvelope:
type: object
required: [data, total]
properties:
data:
type: array
items:
$ref: "#/components/schemas/Group"
total:
type: integer
Feed:
type: object
required:
- id
- group_id
- name
- link
- suspended
- created_at
- updated_at
- fetch_state
- unread_count
- item_count
properties:
id:
type: integer
format: int64
group_id:
type: integer
format: int64
name:
type: string
link:
type: string
site_url:
type: string
suspended:
type: boolean
proxy:
type: string
created_at:
type: integer
format: int64
updated_at:
type: integer
format: int64
fetch_state:
$ref: "#/components/schemas/FeedFetchState"
unread_count:
type: integer
format: int64
item_count:
type: integer
format: int64
FeedFetchState:
type: object
required:
- expires_at
- last_checked_at
- next_check_at
- last_http_status
- retry_after_until
- last_success_at
- last_error_at
- consecutive_failures
properties:
etag:
type: string
last_modified:
type: string
cache_control:
type: string
expires_at:
type: integer
format: int64
last_checked_at:
type: integer
format: int64
next_check_at:
type: integer
format: int64
last_http_status:
type: integer
retry_after_until:
type: integer
format: int64
last_success_at:
type: integer
format: int64
last_error_at:
type: integer
format: int64
last_error:
type: string
consecutive_failures:
type: integer
format: int64
FeedEnvelope:
type: object
required: [data]
properties:
data:
$ref: "#/components/schemas/Feed"
FeedListEnvelope:
type: object
required: [data, total]
properties:
data:
type: array
items:
$ref: "#/components/schemas/Feed"
total:
type: integer
CreateFeedRequest:
type: object
required: [group_id, name, link]
properties:
group_id:
type: integer
format: int64
name:
type: string
link:
type: string
site_url:
type: string
proxy:
type: string
UpdateFeedRequest:
type: object
properties:
group_id:
type: integer
format: int64
name:
type: string
link:
type: string
site_url:
type: string
suspended:
type: boolean
proxy:
type: string
BatchCreateFeedItem:
type: object
required: [group_id, name, link]
properties:
group_id:
type: integer
format: int64
name:
type: string
link:
type: string
site_url:
type: string
BatchCreateFeedsRequest:
type: object
required: [feeds]
properties:
feeds:
type: array
items:
$ref: "#/components/schemas/BatchCreateFeedItem"
BatchCreateFeedsResult:
type: object
required: [created, failed, errors]
properties:
created:
type: integer
failed:
type: integer
errors:
type: array
items:
type: string
BatchCreateFeedsEnvelope:
type: object
required: [data]
properties:
data:
$ref: "#/components/schemas/BatchCreateFeedsResult"
ValidateFeedRequest:
type: object
required: [url]
properties:
url:
type: string
DiscoveredFeed:
type: object
required: [title, link]
properties:
title:
type: string
link:
type: string
ValidateFeedData:
type: object
required: [feeds]
properties:
feeds:
type: array
items:
$ref: "#/components/schemas/DiscoveredFeed"
ValidateFeedEnvelope:
type: object
required: [data]
properties:
data:
$ref: "#/components/schemas/ValidateFeedData"
Item:
type: object
required:
[id, feed_id, guid, title, link, content, pub_date, unread, created_at]
properties:
id:
type: integer
format: int64
feed_id:
type: integer
format: int64
guid:
type: string
title:
type: string
link:
type: string
content:
type: string
pub_date:
type: integer
format: int64
unread:
type: boolean
created_at:
type: integer
format: int64
ItemEnvelope:
type: object
required: [data]
properties:
data:
$ref: "#/components/schemas/Item"
ItemListEnvelope:
type: object
required: [data, total]
properties:
data:
type: array
items:
$ref: "#/components/schemas/Item"
total:
type: integer
MarkItemsRequest:
type: object
required: [ids]
properties:
ids:
type: array
minItems: 1
maxItems: 1000
items:
type: integer
format: int64
SearchFeed:
type: object
required: [id, name, link, site_url]
properties:
id:
type: integer
format: int64
name:
type: string
link:
type: string
site_url:
type: string
SearchItem:
type: object
required: [id, feed_id, title, pub_date]
properties:
id:
type: integer
format: int64
feed_id:
type: integer
format: int64
title:
type: string
pub_date:
type: integer
format: int64
SearchData:
type: object
required: [feeds, items]
properties:
feeds:
type: array
items:
$ref: "#/components/schemas/SearchFeed"
items:
type: array
items:
$ref: "#/components/schemas/SearchItem"
SearchEnvelope:
type: object
required: [data]
properties:
data:
$ref: "#/components/schemas/SearchData"
Bookmark:
type: object
required: [id, link, title, content, pub_date, feed_name, created_at]
properties:
id:
type: integer
format: int64
item_id:
type: integer
format: int64
nullable: true
link:
type: string
title:
type: string
content:
type: string
pub_date:
type: integer
format: int64
feed_name:
type: string
created_at:
type: integer
format: int64
BookmarkEnvelope:
type: object
required: [data]
properties:
data:
$ref: "#/components/schemas/Bookmark"
BookmarkListEnvelope:
type: object
required: [data, total]
properties:
data:
type: array
items:
$ref: "#/components/schemas/Bookmark"
total:
type: integer
CreateBookmarkFromItemRequest:
type: object
required: [item_id]
properties:
item_id:
type: integer
format: int64
CreateBookmarkSnapshotRequest:
type: object
required: [link, title, content, feed_name]
properties:
link:
type: string
title:
type: string
content:
type: string
pub_date:
type: integer
format: int64
default: 0
feed_name:
type: string
OIDCEnabledData:
type: object
required: [enabled]
properties:
enabled:
type: boolean
OIDCEnabledEnvelope:
type: object
required: [data]
properties:
data:
$ref: "#/components/schemas/OIDCEnabledData"
OIDCLoginData:
type: object
required: [auth_url]
properties:
auth_url:
type: string
OIDCLoginEnvelope:
type: object
required: [data]
properties:
data:
$ref: "#/components/schemas/OIDCLoginData"