feat: allow agent bots to toggle typing status (#13705)

Agent bot conversations now feel more natural because AgentBot tokens
can toggle typing status, so end users see a live typing indicator in
the widget while the bot is preparing a reply. This keeps the
interaction responsive and human-like without weakening token
authorization boundaries.

## Closes
- https://github.com/chatwoot/chatwoot/issues/8928
- https://linear.app/chatwoot/issue/CW-5205

## How to test
1. Open the widget and start a conversation as a customer.
2. Connect an AgentBot to the same inbox.
3. Trigger `toggle_typing_status` with the AgentBot token
(`typing_status: on`).
4. Confirm the customer sees the typing indicator in the widget.
5. Trigger `toggle_typing_status` with `typing_status: off` and confirm
the indicator disappears.

## What changed
- Added `toggle_typing_status` to bot-accessible conversation endpoints.
- Restricted bot-accessible endpoint usage to `AgentBot` token owners
only (non-user tokens like `PlatformApp` remain unauthorized).
- Updated typing status flow to preserve AgentBot identity in
dispatch/broadcast paths.
- Added request coverage for AgentBot success and PlatformApp
unauthorized behavior.
- Added Swagger documentation for `POST
/api/v1/accounts/{account_id}/conversations/{conversation_id}/toggle_typing_status`
and regenerated swagger artifacts.
This commit is contained in:
Sojan Jose
2026-03-05 08:13:52 -08:00
committed by GitHub
parent fd69b4c8f2
commit 397b0bcc9d
11 changed files with 279 additions and 7 deletions

View File

@@ -0,0 +1,41 @@
tags:
- Conversations
operationId: toggle-typing-status-of-a-conversation
summary: Toggle Typing Status
description: Toggles the typing status for a conversation.
security:
- userApiKey: []
- agentBotApiKey: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- typing_status
properties:
typing_status:
type: string
enum: ['on', 'off']
description: Typing status to set.
example: 'on'
is_private:
type: boolean
description: Whether the typing event is for private notes.
example: false
responses:
'200':
description: Success
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
'404':
description: Conversation not found
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'

View File

@@ -367,6 +367,12 @@
- $ref: '#/components/parameters/conversation_id'
post:
$ref: ./application/conversation/toggle_priority.yml
/api/v1/accounts/{account_id}/conversations/{conversation_id}/toggle_typing_status:
parameters:
- $ref: '#/components/parameters/account_id'
- $ref: '#/components/parameters/conversation_id'
post:
$ref: ./application/conversation/toggle_typing_status.yml
/api/v1/accounts/{account_id}/conversations/{conversation_id}/custom_attributes:
parameters:

View File

@@ -4938,6 +4938,86 @@
}
}
},
"/api/v1/accounts/{account_id}/conversations/{conversation_id}/toggle_typing_status": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
},
{
"$ref": "#/components/parameters/conversation_id"
}
],
"post": {
"tags": [
"Conversations"
],
"operationId": "toggle-typing-status-of-a-conversation",
"summary": "Toggle Typing Status",
"description": "Toggles the typing status for a conversation.",
"security": [
{
"userApiKey": []
},
{
"agentBotApiKey": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"typing_status"
],
"properties": {
"typing_status": {
"type": "string",
"enum": [
"on",
"off"
],
"description": "Typing status to set.",
"example": "on"
},
"is_private": {
"type": "boolean",
"description": "Whether the typing event is for private notes.",
"example": false
}
}
}
}
}
},
"responses": {
"200": {
"description": "Success"
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
},
"404": {
"description": "Conversation not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
},
"/api/v1/accounts/{account_id}/conversations/{conversation_id}/custom_attributes": {
"parameters": [
{

View File

@@ -3481,6 +3481,86 @@
}
}
},
"/api/v1/accounts/{account_id}/conversations/{conversation_id}/toggle_typing_status": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
},
{
"$ref": "#/components/parameters/conversation_id"
}
],
"post": {
"tags": [
"Conversations"
],
"operationId": "toggle-typing-status-of-a-conversation",
"summary": "Toggle Typing Status",
"description": "Toggles the typing status for a conversation.",
"security": [
{
"userApiKey": []
},
{
"agentBotApiKey": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"typing_status"
],
"properties": {
"typing_status": {
"type": "string",
"enum": [
"on",
"off"
],
"description": "Typing status to set.",
"example": "on"
},
"is_private": {
"type": "boolean",
"description": "Whether the typing event is for private notes.",
"example": false
}
}
}
}
}
},
"responses": {
"200": {
"description": "Success"
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
},
"404": {
"description": "Conversation not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
},
"/api/v1/accounts/{account_id}/conversations/{conversation_id}/custom_attributes": {
"parameters": [
{