11import { createHmac , timingSafeEqual } from "node:crypto" ;
2- import { extractCard , ValidationError } from "@chat-adapter/shared" ;
2+ import {
3+ AdapterError ,
4+ extractCard ,
5+ ValidationError ,
6+ } from "@chat-adapter/shared" ;
37import type {
48 Adapter ,
59 AdapterPostableMessage ,
@@ -24,6 +28,7 @@ import {
2428 defaultEmojiResolver ,
2529 getEmoji ,
2630 Message ,
31+ MessageHistoryCache ,
2732} from "chat" ;
2833import { cardToWhatsApp , decodeWhatsAppCallbackData } from "./cards" ;
2934import { WhatsAppFormatConverter } from "./markdown" ;
@@ -36,11 +41,12 @@ import type {
3641 WhatsAppRawMessage ,
3742 WhatsAppSendResponse ,
3843 WhatsAppThreadId ,
44+ WhatsAppTypingIndicatorResponse ,
3945 WhatsAppWebhookPayload ,
4046} from "./types" ;
4147
4248/** Default Graph API version */
43- const DEFAULT_API_VERSION = "v21 .0" ;
49+ const DEFAULT_API_VERSION = "v25 .0" ;
4450
4551/** Maximum message length for WhatsApp Cloud API */
4652const WHATSAPP_MESSAGE_LIMIT = 4096 ;
@@ -968,13 +974,57 @@ export class WhatsAppAdapter
968974 /**
969975 * Start typing indicator.
970976 *
971- * WhatsApp supports typing indicators via the messages endpoint .
972- * The indicator displays for up to 25 seconds or until the next message.
977+ * WhatsApp typing indicators require the most recent inbound message ID .
978+ * They also implicitly mark the referenced message as read .
973979 *
974- * @see https://developers.facebook.com/docs/whatsapp/cloud-api/messages/mark-messages-as-read
980+ * @see https://developers.facebook.com/documentation/business-messaging/whatsapp/typing-indicators
975981 */
976- async startTyping ( _threadId : string , _status ?: string ) : Promise < void > {
977- // WhatsApp Cloud API does not support typing indicators.
982+ async startTyping ( threadId : string , status ?: string ) : Promise < void > {
983+ const messageId = await this . resolveTypingTargetMessageId ( threadId ) ;
984+ this . logger . debug ( "WhatsApp typing indicator requested" , {
985+ messageId,
986+ threadId,
987+ } ) ;
988+
989+ if ( ! messageId ) {
990+ this . logger . warn (
991+ "WhatsApp typing indicator skipped - no inbound message context" ,
992+ { threadId }
993+ ) ;
994+ return ;
995+ }
996+
997+ if ( status ) {
998+ this . logger . warn ( "WhatsApp typing indicator ignores custom status text" , {
999+ status,
1000+ threadId,
1001+ messageId,
1002+ } ) ;
1003+ }
1004+
1005+ const response =
1006+ await this . graphApiRequest < WhatsAppTypingIndicatorResponse > (
1007+ `/${ this . phoneNumberId } /messages` ,
1008+ {
1009+ messaging_product : "whatsapp" ,
1010+ status : "read" ,
1011+ message_id : messageId ,
1012+ typing_indicator : {
1013+ type : "text" ,
1014+ } ,
1015+ }
1016+ ) ;
1017+
1018+ if ( ! response . success ) {
1019+ this . logger . error (
1020+ "WhatsApp typing indicator failed: API returned success=false" ,
1021+ {
1022+ messageId,
1023+ threadId,
1024+ }
1025+ ) ;
1026+ throw new AdapterError ( "WhatsApp typing indicator failed" , "whatsapp" ) ;
1027+ }
9781028 }
9791029
9801030 /**
@@ -1138,6 +1188,29 @@ export class WhatsAppAdapter
11381188 // Private helpers
11391189 // =============================================================================
11401190
1191+ /**
1192+ * Resolve the latest inbound message ID for a thread.
1193+ */
1194+ private async resolveTypingTargetMessageId (
1195+ threadId : string
1196+ ) : Promise < string | null > {
1197+ if ( ! this . chat ) {
1198+ return null ;
1199+ }
1200+
1201+ const state = this . chat . getState ( ) ;
1202+ const history = await new MessageHistoryCache ( state ) . getMessages ( threadId ) ;
1203+
1204+ for ( let index = history . length - 1 ; index >= 0 ; index -- ) {
1205+ const message = history [ index ] ;
1206+ if ( message && ! message . author . isMe ) {
1207+ return message . id ;
1208+ }
1209+ }
1210+
1211+ return null ;
1212+ }
1213+
11411214 /**
11421215 * Make a request to the Meta Graph API.
11431216 */
@@ -1161,7 +1234,10 @@ export class WhatsAppAdapter
11611234 body : errorBody ,
11621235 path,
11631236 } ) ;
1164- throw new Error ( `WhatsApp API error: ${ response . status } ${ errorBody } ` ) ;
1237+ throw new AdapterError (
1238+ `WhatsApp API error: ${ response . status } ${ errorBody } ` ,
1239+ "whatsapp"
1240+ ) ;
11651241 }
11661242
11671243 return response . json ( ) as Promise < T > ;
0 commit comments