UNPKG

agent-twitter-summary

Version:

A twitter client for agents

194 lines (169 loc) 4.66 kB
// src/core/ChatClient.ts import WebSocket from 'ws'; import { EventEmitter } from 'events'; import type { SpeakerRequest, OccupancyUpdate } from '../types'; import { Logger } from '../logger'; interface ChatClientConfig { spaceId: string; accessToken: string; endpoint: string; logger: Logger; } export class ChatClient extends EventEmitter { private ws?: WebSocket; private connected = false; private logger: Logger; private readonly spaceId: string; private readonly accessToken: string; private endpoint: string; constructor(config: ChatClientConfig) { super(); this.spaceId = config.spaceId; this.accessToken = config.accessToken; this.endpoint = config.endpoint; this.logger = config.logger; } async connect() { const wsUrl = `${this.endpoint}/chatapi/v1/chatnow`.replace( 'https://', 'wss://', ); this.logger.info('[ChatClient] Connecting =>', wsUrl); this.ws = new WebSocket(wsUrl, { headers: { Origin: 'https://x.com', 'User-Agent': 'Mozilla/5.0', }, }); await this.setupHandlers(); } private setupHandlers(): Promise<void> { if (!this.ws) { throw new Error('No WebSocket instance'); } return new Promise((resolve, reject) => { this.ws!.on('open', () => { this.logger.info('[ChatClient] Connected'); this.connected = true; this.sendAuthAndJoin(); resolve(); }); this.ws!.on('message', (data: { toString: () => string }) => { this.handleMessage(data.toString()); }); this.ws!.on('close', () => { this.logger.info('[ChatClient] Closed'); this.connected = false; this.emit('disconnected'); }); this.ws!.on('error', (err) => { this.logger.error('[ChatClient] Error =>', err); reject(err); }); }); } private sendAuthAndJoin() { if (!this.ws) return; this.ws.send( JSON.stringify({ payload: JSON.stringify({ access_token: this.accessToken }), kind: 3, }), ); this.ws.send( JSON.stringify({ payload: JSON.stringify({ body: JSON.stringify({ room: this.spaceId }), kind: 1, }), kind: 2, }), ); } reactWithEmoji(emoji: string) { if (!this.ws) return; const payload = JSON.stringify({ body: JSON.stringify({ body: emoji, type: 2, v: 2 }), kind: 1, /* // The 'sender' field is not required, it's not even verified by the server // Instead of passing attributes down here it's easier to ignore it sender: { user_id: null, twitter_id: null, username: null, display_name: null, }, */ payload: JSON.stringify({ room: this.spaceId, body: JSON.stringify({ body: emoji, type: 2, v: 2 }), }), type: 2, }); this.ws.send(payload); } private handleMessage(raw: string) { let msg: any; try { msg = JSON.parse(raw); } catch { return; } if (!msg.payload) return; const payload = safeJson(msg.payload); if (!payload?.body) return; const body = safeJson(payload.body); if (body.guestBroadcastingEvent === 1) { const req: SpeakerRequest = { userId: body.guestRemoteID, username: body.guestUsername, displayName: payload.sender?.display_name || body.guestUsername, sessionUUID: body.sessionUUID, }; this.emit('speakerRequest', req); } if (typeof body.occupancy === 'number') { const update: OccupancyUpdate = { occupancy: body.occupancy, totalParticipants: body.total_participants || 0, }; this.emit('occupancyUpdate', update); } if (body.guestBroadcastingEvent === 16) { this.emit('muteStateChanged', { userId: body.guestRemoteID, muted: true, }); } if (body.guestBroadcastingEvent === 17) { this.emit('muteStateChanged', { userId: body.guestRemoteID, muted: false, }); } // Example of guest reaction if (body?.type === 2) { this.logger.info('[ChatClient] Emitting guest reaction event =>', body); this.emit('guestReaction', { displayName: body.displayName, emoji: body.body, }); } } async disconnect() { if (this.ws) { this.logger.info('[ChatClient] Disconnecting...'); this.ws.close(); this.ws = undefined; this.connected = false; } } } function safeJson(text: string): any { try { return JSON.parse(text); } catch { return null; } }