☰
Rocket.Chat v1 REST API 新增 POST /v1/chat.readThread:按线程粒度标记已读,替代 DDP readThreads 方法
2026/11/6 12:19:07 网站建设 项目流程

Rocket.Chat v1 REST API 新增 POST /v1/chat.readThread:按线程粒度标记已读,替代 DDP readThreads 方法

【免费下载链接】Rocket.ChatThe Secure CommsOS™ for mission-critical operations项目地址: https://gitcode.com/GitHub_Trending/ro/Rocket.Chat

本文围绕 Rocket.Chat 的一次 API 变更(.changeset/rest-chat-read-thread.md)展开:新增POST /v1/chat.readThread端点,允许调用方通过单个tmid将某一条线程标记为已读——将其从自己的订阅未读列表中移除,并触发beforeReadMessages/afterReadMessages回调;同时原 DDP 方法readThreads被标记弃用(保留至 9.0.0)。读完本文,你可以直接使用这条 REST 路由完成线程级已读同步,并理解它与POST /v1/subscriptions.read的语义边界、以及与底层readThread实现之间的完整调用链。

端点概览

该端点随@rocket.chat/meteor的 minor 版本引入,核心语义(见变更说明 rest-chat-read-thread.md)为:

  • 请求:POST /v1/chat.readThread,Body 为{ tmid }(tmid即线程首条消息的_id);
  • 行为:将单条线程标记为已读,作用于调用者自身的订阅——把tmid从该订阅的未读线程列表中清除,并运行beforeReadMessages/afterReadMessages回调;
  • 定位:它是 DDP 方法readThreads的 REST 替代,旧方法保留注册但会输出弃用日志,日志指向新路由,计划保留至 9.0.0;
  • 与POST /v1/subscriptions.read不同:后者接受{ rid, readThreads? },作用于整个房间(整屋已读或整屋线程已读),无法精确指定某一条线程。

端点在 apps/meteor/server/api/v1/chat.ts 中通过API.v1.post('chat.readThread', ...)注册,声明authRequired: true,即必须携带有效的 Bearer Token 访问。

请求体与响应契约(源码级定义)

请求体由 AJV 校验。在 chat.ts 中定义了类型与 Schema:

type ChatReadThread = { tmid: string; }; const ChatReadThreadSchema = { /* tmid: 字符串,必填 */ }; const isChatReadThreadProps = ajv.compile<ChatReadThread>(ChatReadThreadSchema);

响应契约同样由 AJV 编译(chat.ts#L269-L280):

状态码契约
200{ "success": true },且additionalProperties: false(严格模式,不允许多余字段)
400validateBadRequestErrorResponse(Body 不符合{ tmid: string }时)
401validateUnauthorizedErrorResponse(未认证或 Token 无效)

一个可直接复制的调用示例(tmid为线程首条消息 ID):

curl -X POST "https://<instance>/api/v1/chat.readThread" \ -H "Authorization: Bearer <API_TOKEN>" \ -H "Content-Type: application/json" \ -d '{"tmid":"<thread_message_id>"}' # 成功返回 200: {"success":true}

服务端处理流程:从参数校验到权限检查

action()的处理链(chat.ts#L282-L311)按顺序执行以下检查,每一步都有明确的错误码,便于集成方做幂等与故障定位:

  1. 功能开关:settings.get('Threads_enabled')为false时抛出error-not-allowed("Threads Disabled")。线程功能可以在服务器端整体关闭,端点随之不可用;
  2. 线程存在性:Messages.findOneById(tmid, { projection: { rid: 1 } })仅取出rid;tmid不存在(或无rid)时抛出error-invalid-message("Invalid Message")。注意这里对"线程不存在"的语义是报错,而不是静默成功;
  3. 用户与房间并行加载:Promise.all([Users.findOneById(this.userId), Rooms.findOneById(thread.rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } })]);房间不存在抛出error-room-does-not-exist;
  4. 访问权限:!user || !(await canAccessRoomAsync(room, user))时抛出error-not-allowed。canAccessRoomAsync复用了与其他消息 API 一致的房间级授权逻辑,保证只有能访问该房间的用户才能执行标记已读;
  5. 前置回调 + 核心操作:先callbacks.run('beforeReadMessages', room._id, user._id),再执行readThread({ user, room, tmid });
  6. 返回API.v1.success(),即上文 200 契约中的{ success: true }。

核心实现:readThread 如何清除未读状态

真正的状态变更位于 apps/meteor/server/lib/messaging/threads/functions.ts,readThread被 REST 端点与 DDP 旧方法共同复用(见下文弃用一节),保证两条入口的行为一致:

export const readThread = async ({ user, room, tmid }) => { const sub = await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { projection: { tunread: 1 } }); if (!sub) return; // if the thread being marked as read is the last one unread also clear the unread subscription flag const clearAlert = sub.tunread && sub.tunread?.length <= 1 && sub.tunread.includes(tmid); const removeUnreadThreadResponse = await Subscriptions.removeUnreadThreadByRoomIdAndUserId( room._id, user._id, tmid, clearAlert, ); if (removeUnreadThreadResponse.modifiedCount) { void notifyOnSubscriptionChangedByRoomIdAndUserId(room._id, user._id); } await NotificationQueue.clearQueueByUserId(user._id); callbacks.runAsync('afterReadMessages', room, { uid: user._id, tmid }); };

从源码可以读出四个关键细节:

  • 仅作用于调用者本人的订阅:查找订阅用的room._id + user._id,不会把其他用户的未读线程清掉——这正是"marks a single thread as readfor the caller"(变更文档原话)的实现依据;
  • tunread数组与"最后一未读线程"优化:clearAlert在"被读线程是本订阅最后一个未读线程,且确实出现在tunread中"时为true,此时removeUnreadThreadByRoomIdAndUserId会顺带清除订阅级别的未读提示标志,避免残留角标;
  • 客户端实时刷新:若数据库实际发生了修改(modifiedCount > 0),会通过notifyOnSubscriptionChangedByRoomIdAndUserId推送订阅变更通知,客户端侧边栏/线程未读数随之更新;
  • 通知队列与后置回调:NotificationQueue.clearQueueByUserId(user._id)清空该用户的本地通知队列;afterReadMessages通过callbacks.runAsync异步执行,不阻塞响应返回。注意两个回调的对称性:beforeReadMessages在 handler 中以run(同步)方式先行执行,afterReadMessages则在readThread内部以runAsync(异步)方式收尾,与变更文档中"running thebeforeReadMessages/afterReadMessagescallbacks"的描述完全对应。

从源码结构看,afterReadMessages回调在 EE 层还有具体实现(如 ee/server/hooks/messages/afterReadMessages.ts),已读事件可被扩展用于已读回执等能力,这是把"标记已读"做成回调链而非私有写库的原因之一。

与 POST /v1/subscriptions.read 的语义边界

变更文档专门指出:POST /v1/subscriptions.read不覆盖单线程已读场景。对照 apps/meteor/server/api/v1/subscriptions.ts 的实现可以验证这一点:

async function action() { const { readThreads = false } = this.bodyParams; const roomId = 'rid' in this.bodyParams ? this.bodyParams.rid : this.bodyParams.roomId; const room = await Rooms.findOneById(roomId); if (!room) { throw new Error('error-invalid-subscription'); } await readMessages(room, this.userId, readThreads); return API.v1.success(); }
维度POST /v1/chat.readThreadPOST /v1/subscriptions.read
地址参数tmid(线程首条消息 ID)rid(或roomId)
可选参数无readThreads?(布尔,默认false)
作用粒度单条线程整个房间(含房间内全部线程)
适用场景用户打开/离开某一条线程时精确同步已读用户进入房间时"整屋已读"

因此选型建议是:在 UI 中用户打开线程面板、线程阅读完毕等场景用chat.readThread做精确更新;用户进入房间、清空房间未读的场景才用subscriptions.read。用后者"顺便"读线程会把整房间的未读一并清除,粒度不对。

弃用路径:DDP readThreads 到 REST 路由的迁移

旧的 DDP 方法readThreads仍被注册,但每次调用都会先经过弃用日志(apps/meteor/server/meteor-methods/messages/readThreads.ts):

async readThreads(tmid) { methodDeprecationLogger.method('readThreads', '9.0.0', '/v1/chat.readThread'); check(tmid, String); if (!Meteor.userId() || !settings.get('Threads_enabled')) { throw new Meteor.Error('error-not-allowed', 'Threads Disabled', { method: 'getThreadMessages' }); } // ... 查找线程/房间、canAccessRoomAsync 校验后: await callbacks.run('beforeReadMessages', thread.rid, user?._id); if (user?._id) { await readThread({ user: user as IUser, room, tmid }); } },

methodDeprecationLogger.method('readThreads', '9.0.0', '/v1/chat.readThread')即变更文档所说的"deprecation log pointing at the new route until 9.0.0":日志声明了移除版本(9.0.0)与迁移目标(/v1/chat.readThread)。两条入口在业务逻辑上是等价的——同样校验Threads_enabled、同样做canAccessRoomAsync授权、同样先跑beforeReadMessages再调用同一个readThread函数——迁移时的主要工作是把客户端的Meteor.call('readThreads', tmid)替换为携带 Bearer Token 的 HTTP POST,并调整错误处理:REST 侧的失败以 HTTP 状态码(400/401)和error-code表达,而非 DDP 异常。

小结与使用前提

  • 端点:POST /v1/chat.readThread,Body{ tmid },成功返回{ success: true }(严格 Schema,禁止额外字段);
  • 前提条件:实例开启Threads_enabled设置;调用者拥有线程所在房间的访问权限;只修改调用者本人的订阅未读状态;
  • 效果:从订阅tunread移除该线程(最后一个未读线程时顺带清除未读角标)、推送订阅变更通知、清空用户通知队列、触发afterReadMessages异步回调;
  • 关键源码位置:路由与校验 apps/meteor/server/api/v1/chat.ts、核心状态变更 apps/meteor/server/lib/messaging/threads/functions.ts、弃用 DDP 方法 apps/meteor/server/meteor-methods/messages/readThreads.ts、房间级对照端点 apps/meteor/server/api/v1/subscriptions.ts。

【免费下载链接】Rocket.ChatThe Secure CommsOS™ for mission-critical operations项目地址: https://gitcode.com/GitHub_Trending/ro/Rocket.Chat

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询