Stoat 0.15.5 API 使用详解

把 Stoat 0.15.5 服务端 REST API 的调用方式讲透——认证体系、账号/会话/引导(onboarding)流程、服务器与频道管理、消息收发与已读、文件上传下载,全部端点带实测请求与真实响应回显。已在自托管实例 `http://192.168.1.200:8140` 上逐条 curl 实测通过,注册/登录/建服/发消息/附件闭环全部可复现。

StoatStoatChat即时通讯2026/10/30

Stoat 0.15.5 API 使用详解

目标:把 Stoat 0.15.5 服务端 REST API 的调用方式讲透——认证体系、账号/会话/引导(onboarding)流程、服务器与频道管理、消息收发与已读、文件上传下载,全部端点带实测请求与真实响应回显。已在自托管实例 http://192.168.1.200:8140 上逐条 curl 实测通过,注册/登录/建服/发消息/附件闭环全部可复现。

一、环境信息

本篇所有请求与响应均来自以下实测环境:

项目值
服务端Stoat v0.15.5(API 自报 "revolt":"0.15.5")
实例入口http://192.168.1.200:8140(API 路径前缀 /api)
API 风格REST + JSON(delta 服务,路径与上游 Revolt 同源)
实时通道WebSocket ws://192.168.1.200:8140/ws
文件服务http://192.168.1.200:8140/autumn(multipart 上传)
客户端工具curl 7.81+ / Python 3 标准库
测试账号root@stoat.local(用户名 rootadmin,演示环境)

二、前置条件

  1. 一套可访问的 Stoat 实例(部署过程见 《Stoat 0.15.5 Docker Compose 部署指南》),且能从你的网络访问其 80/443(或自定义入口端口)。
  2. 开放注册实例可直接注册;invite_only 实例需先在库内生成邀请码(配置见 《Stoat 0.15.5 配置参数详解》5.3 节)。
  3. 准备一个不在泄露密码黑名单内的口令——0.15.5 注册与登录都会校验,q1w2e3r4 这类常见口令会被拒绝(详见第七章 Q1)。

三、API 总览

Stoat 服务端按子系统拆分为多个服务,对外由统一入口按路径分发,客户端只需面对一个 base URL:

路径前缀后端服务职责
/apiapi(delta)全部 REST 业务:账号/会话/用户/服务器/频道/消息/邀请等
/wsevents(bonfire)WebSocket 实时事件(消息推送、在线状态)
/autumnautumn文件上传下载(附件/头像/表情等按 tag 区分)
/januaryjanuary链接预览与图片代理
/gifboxgifboxTenor GIF 代理
/livekitlivekit语音视频信令
/ingressvoice-ingress语音信令入口

版本与能力自报:GET /api/ 无需认证,返回服务端版本、功能开关与限额体系,是调试的第一入口。认证方式有两种(源码认证中间件核读确认):

认证头适用身份获取方式
x-session-token用户会话POST /api/auth/session/login 返回
x-bot-token机器人账号建机器人后获取

限流约定:服务端对新注册用户(默认 72 小时内,features.limits.global.new_user_hours)施加更严格的限额——同一时刻最多 5 个好友请求、2 个 bot、50 个服务器;老用户默认 10/5/100。全部限额可通过 GET /api/ 的 limits 对象实时核对。错误响应统一为 JSON 对象,type 是机器可读错误码、location 是源码定位(调试用):

{"type":"CompromisedPassword","location":"crates/core/database/src/util/password.rs:68:20"}

四、认证与首次调用

4.1 服务自报(免认证)

curl -s http://192.168.1.200:8140/api/ | head -c 260

实测输出(节选):

{"revolt":"0.15.5","features":{"captcha":{"enabled":false,"key":""},"email":false,"invite_only":false,"autumn":{"enabled":true,"url":"http://192.168.1.200:8140/autumn"},...

关键字段解读:revolt 服务端版本;email 邮件功能是否启用;invite_only 是否邀请制;autumn/january/livekit 各子服务的公网 URL(客户端应使用这里自报的地址而非自行拼接);ws WebSocket 地址;vapid Web Push 公钥。

4.2 注册账号

curl -s -o /dev/null -w '%{http_code}\n' -X POST http://192.168.1.200:8140/api/auth/account/create \
  -H 'Content-Type: application/json' \
  -d '{"email":"root@stoat.local","password":"q1w2e3r4T!"}'

实测输出:

204

成功返回 HTTP 204 且响应体为空。已配置 SMTP 的实例会在此后发送验证邮件;未配置 SMTP 的实例(email:false)注册即生效。错误码:CompromisedPassword(400,口令命中泄露库)、ShortPassword(400,短于 8 位)、InvalidEmail、MissingInvite/InvalidInvite(邀请制)、OperationFailed(500,对已存在邮箱重复注册时的误导性报错,实际语义是"该邮箱已注册",见第七章 Q2)。

4.3 登录换取会话令牌

curl -s -X POST http://192.168.1.200:8140/api/auth/session/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"root@stoat.local","password":"q1w2e3r4T!","friendly_name":"cli"}'

实测输出(节选):

{"result": "Success", "_id": "01M658ARB9BY1A881X0ED94C13", "user_id": "01M65871VK4Y840R0RMN9X9Z38", "token": "TYcaWBdo4x_GKHNpzjwrVgqqlNVSgizglvUBkKBcG9mt2G0Arhsyol9MpNwOfCvj", "name": "cli", "last_seen": "2026-10-...

字段解读:result 成功时为 Success(开启 MFA 的账号返回 MFA,需走 /api/auth/mfa/ticket 二段流程);_id 会话 ID;token 会话令牌,后续所有请求以 x-session-token 头携带。重复登录每次生成新会话,互不影响(实测同一账号连续创建 5 个会话均成功)。错误码:UnverifiedAccount(403,邮箱未验证)、LockedOut(403,账号锁定)。

4.4 首次引导(Onboarding)

登录成功不代表可用:从未设置用户名的账号需先完成 onboarding。查询状态:

curl -s http://192.168.1.200:8140/api/onboard/hello -H "x-session-token: $TOKEN"

实测输出:

{"onboarding": true}

设置用户名完成引导(注意是 POST,不是 PUT):

curl -s -X POST http://192.168.1.200:8140/api/onboard/complete \
  -H 'Content-Type: application/json' -H "x-session-token: $TOKEN" \
  -d '{"username":"rootadmin"}'

实测输出:

{"_id": "01M65871VK4Y840R0RMN9X9Z38", "username": "rootadmin", "discriminator": "6699", "relationship": "User", "online": false}

字段解读:username + 4 位 discriminator 组成完整用户标识(类似旧版 Discord 的 用户名#编号)。错误码:InvalidUsername(400,触发保留名/长度/Unicode 校验——admin、revolt、stoat 三个名字实测均被拒绝)。

4.5 查询当前用户

curl -s http://192.168.1.200:8140/api/users/@me -H "x-session-token: $TOKEN"

实测输出:

{"_id": "01M65871VK4Y840R0RMN9X9Z38", "username": "rootadmin", "discriminator": "6699", "relationship": "User", "online": false}

@me 是当前会话用户的标准别名。至此认证链路全部打通,后续请求均可携带该令牌执行。

五、各 API 域详解

5.1 服务器域(servers)

创建服务器(同时自动生成一个默认文字频道):

curl -s -X POST http://192.168.1.200:8140/api/servers/create \
  -H 'Content-Type: application/json' -H "x-session-token: $TOKEN" \
  -d '{"name":"Stoat 实验室","description":"自托管验证用服务器"}'

实测输出(节选):

{"server": {"_id": "01M658ARBRVPTARSX9MTY4WG0M", "owner": "01M65871VK4Y840R0RMN9X9Z38", "name": "Stoat 实验室", "description": "自托管验证用服务器", "channels": ["01M658ARBRV8025VHGN5V0J84S"], "default_permissions": 77014766592, "ap...

⚠️ 注意响应结构:0.15.5 返回 {"server": {...}} 包装对象,channels 是频道 ID 字符串数组(不是嵌套对象),取默认频道 ID 写 resp["server"]["channels"][0]。

在服务器下创建文字频道:

curl -s -X POST http://192.168.1.200:8140/api/servers/01M658ARBRVPTARSX9MTY4WG0M/channels \
  -H 'Content-Type: application/json' -H "x-session-token: $TOKEN" \
  -d '{"name":"通用聊天","type":"Text","description":"验证用频道"}'

实测输出:

{"channel_type": "TextChannel", "_id": "01M658ARBX3AM67CXQJZP0452D", "server": "01M658ARBRVPTARSX9MTY4WG0M", "name": "通用聊天", "description": "验证用频道"}

type 支持 Text(文字)、Voice(语音);响应以 channel_type 自报类型。错误码:TooManyServers(403,超出 limits 中的 servers 上限)。

5.2 消息域(channels / messages)

发送消息(nonce 是客户端生成的幂等防重 ID):

curl -s -X POST http://192.168.1.200:8140/api/channels/01M658ARBX3AM67CXQJZP0452D/messages \
  -H 'Content-Type: application/json' -H "x-session-token: $TOKEN" \
  -d '{"content":"你好 Stoat!这是一条来自 0.15.5 自托管实例的验证消息。","nonce":"157cc1ba395241a99ae751f1748ec944"}'

实测输出(节选):

{"_id": "01M658ARC2XJZHEG9V2QXSX5BS", "nonce": "157cc1ba395241a99ae751f1748ec944", "channel": "01M658ARBX3AM67CXQJZP0452D", "author": "01M65871VK4Y840R0RMN9X9Z38", "user": {"_id": "01M65871VK4Y840R0RMN9X9Z38", "username...

content 长度上限 2000 字符(limits.new_user.message_length);attachments 字段可携带文件 ID 数组(见 5.4 闭环)。错误码:PayloadTooLarge(413,超 body 上限)、MissingPermission(403,无该频道发消息权限)。

读取频道历史(分页参数 limit,最大 100):

curl -s "http://192.168.1.200:8140/api/channels/01M658ARBX3AM67CXQJZP0452D/messages?limit=5" -H "x-session-token: $TOKEN"

实测输出(节选,按时间近→远排列):

[{"_id": "01M658ARC2XJZHEG9V2QXSX5BS", "nonce": "157cc1ba395241a99ae751f1748ec944", "channel": "01M658ARBX3AM67CXQJZP0452D", ...

编辑消息(PATCH,返回对象带 edited 时间戳):

curl -s -X PATCH http://192.168.1.200:8140/api/channels/01M658ARBX3AM67CXQJZP0452D/messages/01M658ARC2XJZHEG9V2QXSX5BS \
  -H 'Content-Type: application/json' -H "x-session-token: $TOKEN" \
  -d '{"content":"(编辑)你好 Stoat!0.15.5 验证消息已编辑。"}'

实测输出(节选):

{"_id": "01M658ARC2XJZHEG9V2QXSX5BS", "nonce": "157cc1ba395241a99ae751f1748ec944", ..., "content": "(编辑)你好 Stoat!0.15.5 验证消息已编辑。", "edited": "20...

标记已读(ack,成功返回 204 空体;该操作同时触发 RabbitMQ ack 队列更新推送角标):

curl -s -o /dev/null -w '%{http_code}\n' -X PUT \
  http://192.168.1.200:8140/api/channels/01M658ARBX3AM67CXQJZP0452D/ack/01M658ARC2XJZHEG9V2QXSX5BS \
  -H "x-session-token: $TOKEN"

实测输出:

204

5.3 实时事件域(WebSocket)

REST 负责写操作,消息的实时下发走 WebSocket(GET /api/ 自报的 ws 地址)。握手验证:

curl -s -i -N -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
  -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
  http://192.168.1.200:8140/ws | head -3

实测输出:

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

握手成功后(标准部署下经 Caddy 的 /ws 路径转发到 events 服务),客户端应先发送认证包(携带会话令牌),随后服务端推送 Ready 快照与后续事件流;消息的送达、编辑、已读回执都以事件形式实时推给在线客户端。REST 发消息后若无 WS 在线,事件会经 Redis/RabbitMQ 队列沉淀,客户端重连后通过 GET .../messages 增量补齐。

5.4 文件域(autumn)

上传附件(multipart,tag 为 attachments,其他 tag 如 avatars/banners/emojis 各有独立大小限额):

curl -s -H "x-session-token: $TOKEN" \
  -F 'file=@/opt/stoat/upload_test.txt' \
  http://192.168.1.200:8140/autumn/attachments

实测输出:

{"id":"hKJF9aL6PWdQByFT_miNdLqdqnAQizvdj7nN-l2jpC"}

⚠️ 注意两个实测行为:不带认证头直接上传返回 401 {"type":"NotAuthenticated",...};返回体是 {"id": ...} JSON 对象(旧版上游是裸字符串 ID,写解析时留意)。

把文件 ID 作为消息附件发送(完成"挂载"):

curl -s -X POST http://192.168.1.200:8140/api/channels/01M658ARBX3AM67CXQJZP0452D/messages \
  -H 'Content-Type: application/json' -H "x-session-token: $TOKEN" \
  -d '{"content":"附件闭环验证","attachments":["hKJF9aL6PWdQByFT_miNdLqdqnAQizvdj7nN-l2jpC"]}'

实测输出关键字段:"attachments": [{"_id": "hKJF9aL6PWdQByFT_miNdLqdqnAQizvdj7nN-l2jpC", ...}](消息对象内出现附件详情)。

下载验证(URL 形态为 /autumn/<tag>/<文件ID>/<文件名>,免认证公开可访问):

curl -s http://192.168.1.200:8140/autumn/attachments/hKJF9aL6PWdQByFT_miNdLqdqnAQizvdj7nN-l2jpC/upload_test.txt

实测输出:

stoat upload test

⚠️ 重要:上传后未挂载的文件下载返回 404(服务端按 used_for 判定,防未引用文件被盗链)——先发带 attachments 的消息再访问下载 URL。文件名与上传时不一致也会 404(可用 original 占位名重定向到真实文件名)。

六、分页、过滤与通用约定

  • 分页:列表类端点用 ?limit=N(消息历史实测 ?limit=5 生效,默认 50、上限 100);支持 ?before=<消息ID> / ?after=<消息ID> 按游标翻页,?near=<消息ID> 定位上下文,?sort=Latest 倒序。
  • 幂等:写操作请求体可带客户端生成的 nonce(32 位十六进制),同 nonce 重放不会产生重复消息——弱网重试场景务必携带。
  • 空体成功:创建账号(204)、ack 已读(204)等"无内容返回"的操作用空响应体表达成功,客户端不要按 JSON 解析。
  • 错误判定:2xx 之外统一看响应体 type 字段做分支处理;location 字段仅供报障时附上(对应服务端源码位置)。
  • ID 形态:全系统对象 ID 为 ULID(如 01M658ARC2XJZHEG9V2QXSX5BS),按字典序即时间序,可直接用于游标排序。
  • 大响应标注:本篇实测响应均为节选展示(完整对象含权限位、角色数组等字段,篇幅较长),字段语义以响应内自报为准。

七、常见问题

Q1:注册或登录都报 CompromisedPassword?

0.15.5 把约 10 万条泄露密码黑名单编译进了服务端二进制,注册(create_account)与登录(session/login)两个入口都会执行 assert_safe 校验,实测 q1w2e3r4 无论注册还是登录一律 400。换成不在黑名单的口令(如 q1w2e3r4T!)即可。服务端另支持外接 easypwned 泄露库服务(配置 [api.security] easypwned)。

Q2:对已注册邮箱再次调用注册接口返回 500 OperationFailed?

账号已存在时,注册流程转入"补发验证邮件/密码重置"分支;未配置 SMTP 的实例该分支必然失败(报错定位在 accounts/model.rs 的密码重置函数)。见到此错误按"邮箱已注册"处理,走登录流程;生产实例配置 SMTP 后此路径恢复正常语义。

Q3:建服务器成功但拿不到频道对象?

0.15.5 的创建服务器响应是 {"server": {...}} 包装结构,server.channels 是频道 ID 字符串数组。需要频道详情时逐个调 GET /api/channels/<id>,或直接用返回的默认频道 ID 发消息。

Q4:上传成功但下载 404?

附件必须先"挂载"——以 attachments 字段随消息发送(或设为头像等用途)后才开放下载,used_for 为空的文件拒绝外链。见 5.4 的闭环实测。

Q5:WebSocket 握手成功但收不到消息事件?

握手(101)只建立通道,还需在连接上发送认证包(含会话令牌)完成身份绑定后才会推送事件;同时确认客户端连接的是 GET /api/ 自报的 ws 地址——自托管实例改过域名/端口时,该自报值必须与实际入口一致(配置项见 《Stoat 0.15.5 配置参数详解》5.2 节)。

Q6:如何用脚本完成整套冒烟验证?

按本篇顺序串联即可:注册(204)→ 登录取 token → onboard(若 onboarding:true)→ 建服务器 → 建频道 → 发消息 → 读历史 → ack → 上传附件 → 挂载发消息 → 下载回读。每步的预期状态码与回显本文均已给出,全部通过即代表实例核心链路健康。

八、参考链接