captivate-chat-api
Version:
This is a wrapper for captivate chat api socket custom channel
303 lines (269 loc) • 10.5 kB
text/typescript
// Import the Conversation class
import { Conversation } from './Conversation';
// Determine the WebSocket implementation based on the environment (browser, Node.js, or React Native)
let WebSocketImpl: typeof WebSocket;
// Browser environment (web)
if (typeof window !== 'undefined' && window.WebSocket) {
WebSocketImpl = window.WebSocket;
}
// React Native environment
else if (typeof global !== 'undefined' && global.WebSocket) {
WebSocketImpl = global.WebSocket;
}
// Node.js environment
else {
try {
WebSocketImpl = require('ws');
} catch (error) {
throw new Error('WebSocket not available in this environment and ws module could not be loaded');
}
}
/**
* CaptivateChatAPI class for managing conversations through WebSocket connections.
*/
export class CaptivateChatAPI {
private apiKey: string;
private mode: 'prod' | 'dev';
private url: string;
private socket: InstanceType<typeof WebSocketImpl> | null;
private conversations: Map<string, Conversation>;
/**
* Creates an instance of CaptivateChatAPI.
* @param apiKey - The API key for authentication.
* @param mode - The mode of operation ('prod' for production or 'dev' for development).
*/
constructor(apiKey: string, mode: 'prod' | 'dev' = 'prod') {
this.apiKey = apiKey;
this.mode = mode;
this.url =
this.mode === 'prod'
? `wss://channel.wss.captivatechat.ai/dev?apiKey=${apiKey}`
: `wss://channel-dev.wss.captivatechat.ai/dev?apiKey=${apiKey}`;
this.socket = null;
this.conversations = new Map();
}
/**
* Sends a message through the WebSocket connection.
* @param message - The message object to send.
*/
private _send(message: object): void {
if (this.socket && this.socket.readyState === WebSocketImpl.OPEN) {
this.socket.send(JSON.stringify(message));
} else {
console.error('Socket is not open. Message not sent:', message);
}
}
/**
* Connects to the WebSocket server and waits for confirmation from the API.
* @returns A promise that resolves once the connection is successfully established.
*/
public async connect(): Promise<void> {
return new Promise((resolve, reject) => {
try {
this.socket = new WebSocketImpl(this.url);
const timeoutId = setTimeout(() => {
reject(new Error('Connection timeout: socket_connected not received'));
}, 10000);
this.socket.onopen = () => {
console.log('WebSocket connected, waiting for API confirmation...');
};
this.socket.onmessage = (event: MessageEvent) => {
try {
const message = JSON.parse(event.data.toString());
if (message.event?.event_type === 'socket_connected') {
console.log('API Successfully Connected');
clearTimeout(timeoutId);
resolve();
}
} catch (err) {
console.error('Error parsing message:', err);
}
};
this.socket.onerror = (event: any) => {
console.error('WebSocket Error:', event.message || event);
clearTimeout(timeoutId);
reject(new Error(event.message || 'WebSocket error'));
};
this.socket.onclose = (event) => {
console.log('WebSocket connection closed with');
if (event.code !== 1000) {
console.log('Attempting to reconnect...');
setTimeout(() => this.connect(), 3000); // Reconnect after 3 seconds
}
};
} catch (error) {
reject(error);
}
});
}
/**
* Creates a new conversation or starts a bot-first/user-first conversation.
* @param userId - The unique identifier for the user.
* @param userBasicInfo - Basic information about the user (e.g., name, email).
* @param userData - Additional optional user data.
* @param autoConversationStart - Mode to auto-start the conversation ('bot-first' or 'user-first').
* @returns A promise that resolves to the created Conversation instance.
*/
public async createConversation(
userId: string,
userBasicInfo: object = {},
metadata: object = {},
autoConversationStart: 'bot-first' | 'user-first' = 'bot-first'
): Promise<Conversation> {
return new Promise((resolve, reject) => {
try {
this._send({
action: 'sendMessage',
event: {
event_type: 'conversation_start',
event_payload: {
userId,
userBasicInfo,
metadata
},
},
});
const onMessage = (event: MessageEvent) => {
try {
const message = JSON.parse(event.data.toString());
if (message.event?.event_type === 'conversation_start_success') {
const conversationId = message.event.event_payload.conversation_id;
this.socket?.removeEventListener('message', onMessage);
const conversation = new Conversation(conversationId, this.socket!);
this.conversations.set(conversationId, conversation);
if (autoConversationStart === 'bot-first') {
conversation
.sendMessage({ type: 'text', text: '' })
.then(() => resolve(conversation))
.catch(reject);
} else {
resolve(conversation);
}
}
} catch (err) {
console.error('Error processing message:', err);
}
};
this.socket?.addEventListener('message', onMessage);
setTimeout(() => {
this.socket?.removeEventListener('message', onMessage);
reject(new Error('Timeout: No response for createConversation'));
}, 10000);
} catch (error) {
reject(error);
}
});
}
/**
* Retrieves an existing conversation by its ID or creates a new one if not found.
* @param conversationId - The unique ID of the conversation.
* @returns The Conversation instance associated with the given ID.
*/
getConversation(conversationId: string): Conversation | void {
let conversation = this.conversations.get(conversationId);
if (!conversation) {
// If conversation is not found, check if socket is initialized
if (this.socket !== null) {
// If socket is initialized, create the conversation
conversation = new Conversation(conversationId, this.socket);
this.conversations.set(conversationId, conversation);
} else {
// Handle the case where socket is not initialized
console.error('Socket is not initialized');
throw new Error('WebSocket connection not established');
}
}
conversation?.restartListeners();
return conversation;
}
/**
* Retrieves user conversations based on their userId and returns actual conversation objects.
* @param userId - The unique identifier for the user.
* @returns A promise resolving to a list of Conversation instances.
*/
public async getUserConversations(userId: string): Promise<Conversation[]> {
const conversations: Conversation[] = [];
return new Promise((resolve, reject) => {
try {
this._send({
action: 'sendMessage',
event: {
event_type: 'get_user_conversations',
event_payload: {
userId,
},
},
});
const onMessage = (event: MessageEvent) => {
try {
const message = JSON.parse(event.data.toString());
if (message.event?.event_type === 'user_conversations') {
this.socket?.removeEventListener('message', onMessage);
const payload = message.event.event_payload.conversations;
// Iterate over the payload array which contains { id, metadata }
for (const conv of payload) {
const { conversation_id, metadata } = conv; // Destructure the conversation object to get id and metadata
if (this.socket !== null) {
conversations.push(new Conversation(conversation_id, this.socket, metadata));
}
}
resolve(conversations);
}
} catch (err) {
console.error('Error processing message:', err);
reject(err);
}
};
this.socket?.addEventListener('message', onMessage);
setTimeout(() => {
this.socket?.removeEventListener('message', onMessage);
reject(new Error('Timeout: No response for getUserConversations'));
}, 10000);
} catch (error) {
reject(error);
}
});
}
/**
* Deletes all conversations for a specific user.
* @param userId - The unique identifier for the user whose conversations will be deleted.
* @returns A promise that resolves when all conversations are successfully deleted.
*/
public async deleteUserConversations(userId: string): Promise<void> {
return new Promise((resolve, reject) => {
if (!userId) {
return reject(new Error('User ID must be provided.'));
}
try {
this._send({
action: 'sendMessage',
event: {
event_type: 'delete_conversation',
event_payload: { userId: userId },
},
});
const onDeleteSuccess = (event: MessageEvent) => {
try {
const message = JSON.parse(event.data.toString());
if (
message.event?.event_type === 'delete_conversation_success' &&
message.event.event_payload.userId === userId
) {
this.socket?.removeEventListener('message', onDeleteSuccess);
resolve();
}
} catch (err) {
reject(err);
}
};
this.socket?.addEventListener('message', onDeleteSuccess);
setTimeout(() => {
this.socket?.removeEventListener('message', onDeleteSuccess);
reject(new Error(`Timeout: No response for deleting conversations for user ${userId}`));
}, 10000);
} catch (error) {
reject(error);
}
});
}
}