whatsapp-claude-gpt
Version:
WhatsApp-Claude-GPT is an advanced chatbot for WhatsApp, integrating AI language models for text conversations, image generation, and voice messages.
686 lines (587 loc) • 31.2 kB
text/typescript
import { OpenaiService } from './services/openai-service';
import { Chat, Client, GroupChat, Message, MessageMedia, MessageTypes } from 'whatsapp-web.js';
import {
bufferToStream,
extractAnswer,
getContactName,
getUnsupportedMessage,
includeName,
logMessage,
parseCommand
} from './utils';
import logger from './logger';
import { AIConfig, CONFIG } from './config';
import { AIAnswer, AIContent, AiMessage, AIProvider, AIRole } from './interfaces/ai-interfaces';
import { ChatCompletionMessageParam } from 'openai/resources';
import { AnthropicService } from './services/anthropic-service';
import { ImageBlockParam, MessageParam, TextBlock } from '@anthropic-ai/sdk/src/resources/messages';
import NodeCache from 'node-cache';
import { elevenTTS } from './services/elevenlabs-service';
import { chatConfigurationManager } from './config/chat-configurations';
import { ResponseInput } from "openai/resources/responses/responses";
import { AITools } from "./config/openai-functions";
export class Roboto {
private openAIService: OpenaiService;
private claudeService: AnthropicService;
private botConfig = CONFIG.botConfig;
private allowedTypes = [MessageTypes.STICKER, MessageTypes.TEXT, MessageTypes.IMAGE, MessageTypes.VOICE, MessageTypes.AUDIO];
private cache: NodeCache;
private groupProcessingStatus: { [key: string]: boolean } = {};
public constructor() {
this.openAIService = new OpenaiService();
this.claudeService = new AnthropicService();
this.cache = new NodeCache();
}
/**
* Handles incoming WhatsApp messages and decides the appropriate action.
*
* This function evaluates incoming messages to determine if a response is needed based on:
* - Message type (text, image, audio, sticker)
* - Group context (direct mentions, quoted replies)
* - Command presence (prefixed with "-")
*
* Key functionalities:
* - Processes commands with the commandSelect method
* - Manages group processing with a queue system to prevent conflicts
* - Handles AI response generation through processMessage
* - Determines response format (text or audio)
* - Logs messages and manages errors
*
* @param message - The incoming Message object from WhatsApp Web.js
* @param client - The WhatsApp Web.js Client instance
* @returns A promise resolving to a boolean indicating if a response was sent
*/
public async readMessage(message: Message, client: Client) {
const chatData: Chat = await message.getChat();
try {
// Extract the data input (extracts command e.g., "-a", and the message)
const isAudioMsg = message.type == MessageTypes.VOICE || message.type == MessageTypes.AUDIO;
const {command, commandMessage} = parseCommand(message.body);
// If it's a "Broadcast" message, it's not processed
if (chatData.id.user == 'status' || chatData.id._serialized == 'status@broadcast') return false;
// Evaluates whether the message type will be processed
if (!this.allowedTypes.includes(message.type) || (isAudioMsg && !AIConfig.SpeechConfig.enabled)) return false;
const botName = chatData.isGroup
? chatConfigurationManager.getBotName(chatData.id._serialized) || this.botConfig.botName
: this.botConfig.botName;
// Evaluates if it should respond
const isSelfMention = message.hasQuotedMsg ? (await message.getQuotedMessage()).fromMe : false;
const isMentioned = includeName(message.body, botName);
if (!isSelfMention && !isMentioned && !command && chatData.isGroup) return false;
while (this.groupProcessingStatus[chatData.id._serialized]) {
await new Promise(resolve => setTimeout(resolve, 2000));
}
this.groupProcessingStatus[chatData.id._serialized] = true;
// Logs the message
logMessage(message, chatData);
// Evaluates if it should go to the command flow
if (!!command) {
await chatData.sendStateTyping();
await this.commandSelect(message);
await chatData.clearState();
return true;
}
// Sends message to ChatGPT
chatData.sendStateTyping();
let chatResponseString: string = await this.processMessage(chatData);
let chatResponse: AIAnswer = extractAnswer(chatResponseString, botName);
if (!chatResponse) return;
if(chatResponse.emojiReact)
message.react(chatResponse.emojiReact);
// Evaluate if response message must be Audio or Text
if (chatResponse.type.toLowerCase() == 'audio' && AIConfig.SpeechConfig.enabled) {
return this.speak(message, chatData, chatResponse.message, 'mp3');
} else {
return this.returnResponse(message, chatResponse.message, chatData.isGroup, client);
}
} catch (e: any) {
logger.error(e.message);
return message.reply('Error 😔');
} finally {
chatData.clearState();
this.groupProcessingStatus[chatData.id._serialized] = false;
}
}
/**
* Selects and executes an action based on the command in the message.
*
* This function acts as a command dispatcher that interprets commands prefixed with "-"
* and routes them to the appropriate handler methods. Currently supports:
*
* - "-image [prompt]": Generates images using AI (if enabled in config)
* - "-chatconfig [subcommand]": Manages chat-specific configurations
* - "-reset": Resets the conversation context with the AI
*
* The function parses the command and its arguments from the message body
* and executes the corresponding functionality.
*
* @param message - The Message object containing the command
* @returns A promise resolving when the command processing is complete
*/
private async commandSelect(message: Message) {
const {command, commandMessage} = parseCommand(message.body);
switch (command) {
case "image":
if (!AIConfig.ImageConfig.enabled) return;
return await this.createImage(message, commandMessage);
case "chatconfig":
return await this.handleChatConfigCommand(message, commandMessage!);
case "reset":
return await message.react('👍');
default:
return true;
}
}
/**
* Processes an incoming message and generates an AI response based on chat context.
*
* This function is responsible for:
* 1. Collecting recent chat messages to build conversation context
* 2. Processing multiple content types (text, images, audio)
* 3. Handling transcription of voice messages
* 4. Applying chat-specific configurations (custom prompts, bot names)
* 5. Selecting the appropriate AI provider and formatting messages accordingly
*
* The function limits context by time (messages older than maxHoursLimit are ignored)
* and resets context if a "-reset" command is found in the chat history.
*
* @param chatData - The Chat object representing the conversation
* @returns A promise that resolves with the AI-generated response or null if no response needed
*/
private async processMessage(chatData: Chat): Promise<string> {
const actualDate = new Date();
const analyzeImages = !AIConfig.ChatConfig.analyzeImageDisabled;
// Bot System prompt and name
let systemPrompt = CONFIG.getSystemPrompt();
let botName = this.botConfig.botName;
const chatConfiguration = chatConfigurationManager.getChatConfig(chatData.id._serialized);
if (chatConfiguration) {
botName = chatConfiguration.botName || this.botConfig.botName;
systemPrompt = CONFIG.getSystemPrompt(chatConfiguration.promptInfo, botName);
logger.debug(`Using custom configuration for ${chatConfiguration.isGroup ? 'group' : 'chat'} "${chatConfiguration.name}" with bot name "${botName}": ${systemPrompt}`);
}
// Initialize an array of messages
const messageList: AiMessage[] = [];
// Placeholder for promises for transcriptions - Image Counting
let transcriptionPromises: { index: number, promise: Promise<string> }[] = [];
let imageCount: number = 0;
// Retrieve the last 'limit' number of messages to send them in order
const fetchedMessages = await chatData.fetchMessages({limit: this.botConfig.maxMsgsLimit});
// Check for "-reset" command in chat history to potentially restart context
const resetIndex = fetchedMessages.map(msg => msg.body).lastIndexOf("-reset");
const messagesToProcess = resetIndex >= 0 ? fetchedMessages.slice(resetIndex + 1) : fetchedMessages;
for (const msg of messagesToProcess.reverse()) {
try {
// Validate if the message was written less than 24 (or maxHoursLimit) hours ago; if older, it's not considered
const msgDate = new Date(msg.timestamp * 1000);
if ((actualDate.getTime() - msgDate.getTime()) / (1000 * 60 * 60) > this.botConfig.maxHoursLimit) break;
// Checks if a message already exists in the cache
const cachedMessage = this.getCachedMessage(msg);
// Check if the message includes media or if it is of another type
const isImage = msg.type === MessageTypes.IMAGE || msg.type === MessageTypes.STICKER;
const isAudio = msg.type === MessageTypes.VOICE || msg.type === MessageTypes.AUDIO;
const isOther = !isImage && !isAudio && msg.type != 'chat';
// Limit the number of processed images to only the last few and ignore audio if cached
const media = (isImage && imageCount < this.botConfig.maxImages && analyzeImages) || (isAudio && !cachedMessage) ?
await msg.downloadMedia() : null;
if (media && isImage) imageCount++;
const role = (!msg.fromMe || isImage) ? AIRole.USER : AIRole.ASSISTANT;
const name = msg.fromMe ? botName : (await getContactName(msg));
// Assemble the content as a mix of text and any included media
const content: Array<AIContent> = [];
if (isOther || (isAudio && !AIConfig.TranscriptionConfig.enabled))
content.push({type: 'text', value: getUnsupportedMessage(msg.type, msg.body)});
else if (isAudio && media && !cachedMessage) {
transcriptionPromises.push({index: messageList.length, promise: this.transcribeVoice(media, msg)});
content.push({type: 'audio', value: '<Transcribing voice message...>'});
}
if (isAudio && cachedMessage) content.push({type: 'audio', value: cachedMessage});
if (isImage && media) content.push({type: 'image', value: media.data, media_type: media.mimetype});
if (isImage && !media) content.push({type: 'text', value: '<Unprocessed image>'});
if (msg.body && !isOther) content.push({type: 'text', value: msg.body});
messageList.push({role: role, name: name, content: content});
} catch (e: any) {
logger.error(`Error reading message - msg.type:${msg.type}; msg.body:${msg.body}. Error:${e.message}`);
}
}
// If no new messages are present, return without action
if (messageList.length == 0) return;
// Wait for all transcriptions to complete
const transcriptions = await Promise.all(transcriptionPromises.map(t => t.promise));
transcriptionPromises.forEach((transcriptionPromise, idx) => {
const transcription = transcriptions[idx];
const messageIdx = transcriptionPromise.index;
messageList[messageIdx].content = messageList[messageIdx].content.map(c =>
c.type === 'audio' && c.value === '<Transcribing voice message...>' ? {type: 'audio', value: transcription} : c
);
});
// Send the message and return the text response
if (AIConfig.ChatConfig.provider == AIProvider.CLAUDE) {
const convertedMessageList: MessageParam[] = this.convertIaMessagesLang(messageList.reverse(), AIProvider.CLAUDE) as MessageParam[];
return await this.claudeService.sendChat(convertedMessageList, CONFIG.getSystemPrompt());
} else if (AIConfig.ChatConfig.provider == AIProvider.OPENAI) {
const convertedMessageList: ResponseInput = this.convertIaMessagesLang(messageList.reverse(), AIConfig.ChatConfig.provider as AIProvider, systemPrompt) as ResponseInput;
return await this.openAIService.sendChatWithTools(convertedMessageList, 'text', AITools);
} else {
const convertedMessageList: ChatCompletionMessageParam[] = this.convertIaMessagesLang(messageList.reverse(), AIConfig.ChatConfig.provider as AIProvider, systemPrompt) as ChatCompletionMessageParam[];
return await this.openAIService.sendCompletion(convertedMessageList);
}
}
/**
* Generates and sends an audio message by synthesizing speech from provided text.
*
* This function converts text to speech using either:
* - OpenAI's text-to-speech API
* - ElevenLabs' text-to-speech service
*
* If no explicit content is provided, it attempts to use the last message sent by the bot
* as input for speech synthesis. The resulting audio is sent as a voice message.
*
* @param message - The Message object for reply context
* @param chatData - The Chat object for the conversation
* @param content - Optional text content to convert to speech
* @param responseFormat - Optional format for the audio response
* @returns A promise that resolves when the audio message is sent
*/
private async speak(message: Message, chatData: Chat, content: string | undefined, responseFormat?) {
// Set the content to be spoken. If no content is explicitly provided, fetch the last bot reply for use.
let messageToSay = content || await this.getLastBotMessage(chatData);
try {
// Generate speech audio from the given text content using the OpenAI API.
let base64Audio;
if (AIConfig.SpeechConfig.provider == AIProvider.ELEVENLABS) {
base64Audio = await elevenTTS(messageToSay);
} else {
const audioBuffer = await this.openAIService.speech(messageToSay, responseFormat);
base64Audio = audioBuffer.toString('base64');
}
let audioMedia = new MessageMedia('audio/mp3', base64Audio, 'voice.mp3');
// Reply to the message with the synthesized speech audio.
const repliedMsg = await message.reply(audioMedia, undefined, {sendAudioAsVoice: true});
this.cache.set(repliedMsg.id._serialized, messageToSay, CONFIG.botConfig.nodeCacheTime);
} catch (e: any) {
logger.error(`Error in speak function: ${e.message}`);
throw e;
}
}
/**
* Creates and sends an AI-generated image in response to a text prompt.
*
* This function uses OpenAI's DALL-E model to generate an image based on
* the provided text prompt. The resulting image is sent as a reply to the
* original message.
*
* @param message - The Message object for reply context
* @param content - The text prompt for image generation
* @returns A promise that resolves when the image is sent
*/
private async createImage(message: Message, content: string | undefined) {
// Verify that content is provided for image generation, return if not.
if (!content) return;
try {
// Calls the ChatGPT service to generate an image based on the provided textual content.
const imgResponse = await this.openAIService.createImage(content);
let media;
if (imgResponse[0].url) {
media = await MessageMedia.fromUrl(imgResponse[0].url);
} else if (imgResponse[0].b64_json) {
media = new MessageMedia('image/png', imgResponse[0].b64_json);
}
// Reply to the message with the generated image.
return await message.reply(media);
} catch (e: any) {
logger.error(`Error in createImage function: ${e.message}`);
// In case of an error during image generation or sending the image, inform the user.
return message.reply("I encountered a problem while trying to generate an image, please try again.");
}
}
/**
* Retrieves the last message sent by the bot in a chat.
*
* This function fetches recent messages from the chat history and
* finds the most recent message sent by the bot (fromMe = true)
* that contains substantive content.
*
* @param chatData - The Chat object to search for messages
* @returns A promise resolving to the text of the last bot message
*/
private async getLastBotMessage(chatData: Chat) {
const lastMessages = await chatData.fetchMessages({limit: 12});
let lastMessageBot: string = '';
for (const msg of lastMessages) {
if (msg.fromMe && msg.body.length > 1) lastMessageBot = msg.body;
}
return lastMessageBot;
}
/**
* Converts AI message structures between different language model formats.
*
* This function transforms message arrays into formats compatible with various AI providers:
* - OpenAI: Structured with system, user and assistant roles
* - Claude: Requires alternating user/assistant roles with specific content formats
* - Qwen: Similar to OpenAI but with provider-specific adaptations
* - DeepSeek: Uses JSON formatting with text blocks
* - DeepInfra/Custom: Uses simplified message formatting
*
* The function handles text, audio transcriptions, and images appropriately for each provider.
*
* @param messageList - Array of AI messages to convert
* @param lang - The target AI provider format
* @param systemPrompt - Optional system prompt to include
* @returns Formatted message array compatible with the specified AI provider
*/
private convertIaMessagesLang(messageList: AiMessage[], lang: AIProvider, systemPrompt?: string): MessageParam[] | ChatCompletionMessageParam[] | ResponseInput {
switch (lang) {
case AIProvider.CLAUDE:
const claudeMessageList: MessageParam[] = [];
let currentRole: AIRole = AIRole.USER;
let gptContent: Array<TextBlock | ImageBlockParam> = [];
messageList.forEach((msg, index) => {
const role = msg.role === AIRole.ASSISTANT && msg.content.find(c => c.type === 'image') ? AIRole.USER : msg.role;
if (role !== currentRole) { // Change role or if it's the last message
if (gptContent.length > 0) {
claudeMessageList.push({role: currentRole as any, content: gptContent});
gptContent = []; // Reset for the next block of messages
}
currentRole = role; // Ensure role alternation
}
// Add content to the current block
msg.content.forEach(c => {
if (['text', 'audio'].includes(c.type)) gptContent.push({
type: 'text',
text: JSON.stringify({message: c.value, author: msg.name, type: c.type})
});
if (['image'].includes(c.type)) gptContent.push({
type: 'image',
source: {data: c.value!, media_type: c.media_type as any, type: 'base64'}
});
});
});
// Ensure the last block is not left out
if (gptContent.length > 0) claudeMessageList.push({role: currentRole, content: gptContent});
// Ensure the first message is always AiRole.USER (by API requirement)
if (claudeMessageList.length > 0 && claudeMessageList[0].role !== AIRole.USER) {
claudeMessageList.shift(); // Remove the first element if it's not USER
}
return claudeMessageList;
case AIProvider.DEEPSEEK:
const deepSeekMsgList: any[] = [];
messageList.forEach(msg => {
if (msg.role == AIRole.ASSISTANT) {
const textContent = msg.content.find(c => ['text', 'audio'].includes(c.type))!;
const content = JSON.stringify({
type: 'text',
text: JSON.stringify({message: textContent.value, author: msg.name, type: textContent.type, response_format: "json_object"})
});
deepSeekMsgList.push({content: content, name: msg.name!, role: msg.role});
} else {
const gptContent: Array<any> = [];
msg.content.forEach(c => {
if (['image'].includes(c.type)) gptContent.push({
type: 'text',
text: JSON.stringify({message: getUnsupportedMessage('image', ''), author: msg.name, type: c.type, response_format: "json_object"})
});
if (['text', 'audio'].includes(c.type)) gptContent.push({
type: 'text',
text: JSON.stringify({message: c.value, author: msg.name, type: c.type, response_format: "json_object"})
});
})
deepSeekMsgList.push({content: gptContent, name: msg.name!, role: msg.role});
}
})
deepSeekMsgList.unshift({role: AIRole.SYSTEM, content: [{type: 'text', text: systemPrompt}]});
return deepSeekMsgList;
case AIProvider.OPENAI:
const responseAPIMessageList: ResponseInput = [];
messageList.forEach(msg => {
const gptContent: Array<any> = [];
msg.content.forEach(c => {
const fromBot = msg.role == AIRole.ASSISTANT;
if (['text', 'audio'].includes(c.type)) gptContent.push({ type: fromBot?'output_text':'input_text', text: JSON.stringify({message: c.value, author: msg.name, type: c.type, response_format:'json_object'}) });
if (['image'].includes(c.type)) gptContent.push({ type: 'input_image', image_url: `data:${c.media_type};base64,${c.value}`});
})
responseAPIMessageList.push({content: gptContent, role: msg.role});
})
responseAPIMessageList.unshift({role: AIRole.SYSTEM, content: systemPrompt});
return responseAPIMessageList;
case AIProvider.QWEN:
const chatgptMessageList: any[] = [];
messageList.forEach(msg => {
const gptContent: Array<any> = [];
msg.content.forEach(c => {
if (['text', 'audio'].includes(c.type)) gptContent.push({
type: 'text',
text: JSON.stringify({message: c.value, author: msg.name, type: c.type, response_format: 'json_object'})
});
if (['image'].includes(c.type)) gptContent.push({type: 'image_url', image_url: {url: `data:${c.media_type};base64,${c.value}`}});
})
chatgptMessageList.push({content: gptContent, name: msg.name!, role: msg.role});
})
chatgptMessageList.unshift({role: AIRole.SYSTEM, content: [{type: 'text', text: systemPrompt}]});
return chatgptMessageList;
case AIProvider.CUSTOM:
case AIProvider.DEEPINFRA:
const otherMsgList: any[] = [];
messageList.forEach(msg => {
if (msg.role == AIRole.ASSISTANT) {
const textContent = msg.content.find(c => ['text', 'audio'].includes(c.type))!;
const content = JSON.stringify({message: textContent.value, author: msg.name, type: textContent.type, response_format: "json_object"});
otherMsgList.push({content: content, name: msg.name!, role: msg.role});
} else {
const gptContent: Array<any> = [];
msg.content.forEach(c => {
if (['image'].includes(c.type)) gptContent.push(JSON.stringify({message: getUnsupportedMessage('image', ''), author: msg.name, type: c.type, response_format: "json_object"}));
if (['text', 'audio'].includes(c.type)) gptContent.push(JSON.stringify({message: c.value, author: msg.name, type: c.type, response_format: "json_object"}));
})
otherMsgList.push({content: gptContent[0], role: msg.role});
}
})
otherMsgList.unshift({role: AIRole.SYSTEM, content: systemPrompt});
return otherMsgList;
default:
return [];
}
}
/**
* Handles chat configuration commands for customizing bot behavior per chat.
*
* This function manages the following subcommands:
* - prompt: Sets a custom personality/behavior for the bot in the current chat
* - botname: Changes the bot's name for the current chat
* - remove: Removes custom configurations and reverts to defaults
* - show: Displays current custom configuration
*
* In group chats, only administrators can modify configurations.
*
* @param message - The Message object containing the command
* @param commandText - The text of the command without the prefix
* @returns A promise resolving to the sent reply message
*/
private async handleChatConfigCommand(message: Message, commandText: string) {
const chat = await message.getChat();
const isGroup = chat.isGroup;
// Solo verificar permisos de administrador en grupos
if (isGroup) {
const groupChat = chat as GroupChat;
const participant = await groupChat.participants.find(p => p.id._serialized === message.author);
const isAdmin = participant?.isAdmin || participant?.isSuperAdmin;
if (!isAdmin) {
return message.reply("Only group administrators can change the bot's configuration in groups.");
}
}
const parts = commandText.split(' ');
const subCommand = parts[0].toLowerCase();
switch (subCommand) {
case 'prompt':
case 'botname':
const value = parts.slice(1).join(' ');
if (!value) return message.reply(`Please provide a ${subCommand === 'prompt' ? 'prompt description' : 'name for the bot'}.`);
const existingConfig = chatConfigurationManager.getChatConfig(chat.id._serialized);
const updateOptions: any = {
promptInfo: existingConfig?.promptInfo || CONFIG.botConfig.promptInfo,
botName: existingConfig?.botName
};
if (subCommand === 'prompt') updateOptions.promptInfo = value;
else updateOptions.botName = value;
const updatedConfig = chatConfigurationManager.updateChatConfig(
chat.id._serialized,
chat.name,
isGroup,
updateOptions
);
return message.reply(
subCommand === 'prompt'
? `✅ Updated prompt for this ${isGroup ? 'group' : 'chat'}. The bot now: ${updatedConfig.promptInfo}`
: `✅ Bot name for this ${isGroup ? 'group' : 'chat'} has been set to: ${updatedConfig.botName}`
);
case 'remove':
const removed = chatConfigurationManager.removeChatConfig(chat.id._serialized);
return message.reply(
removed
? `✅ The custom prompt and bot name have been removed. The bot will use the default configuration.`
: `This ${isGroup ? 'group' : 'chat'} did not have a custom configuration.`
);
case 'show':
const currentConfig = chatConfigurationManager.getChatConfig(chat.id._serialized);
if (!currentConfig) return message.reply(`This ${isGroup ? 'group' : 'chat'} does not have a custom configuration.`);
let response = currentConfig.promptInfo? `Current personality: ${currentConfig.promptInfo}`:``;
if (currentConfig.botName) response += `\nBot name: ${currentConfig.botName ?? CONFIG.botConfig.botName}`;
return message.reply(response);
default:
return message.reply(
"Available commands:\n" +
`- *-chatconfig prompt [description]*: Sets the bot's personality for this ${isGroup ? 'group' : 'chat'}\n` +
`- *-chatconfig botname [name]*: Sets the bot's name for this ${isGroup ? 'group' : 'chat'}\n` +
"- *-chatconfig remove*: Removes the custom configuration\n" +
"- *-chatconfig show*: Displays the current configuration"
);
}
}
/**
* Transcribes a voice message to text using AI services.
*
* This function:
* 1. Checks if the transcription already exists in cache
* 2. Converts the media to an audio buffer and stream
* 3. Sends the audio to the configured transcription service
* 4. Caches the result for future use
*
* The function uses the OpenAI service which may route to different
* providers based on configuration.
*
* @param media - The MessageMedia object containing the voice data
* @param message - The Message object for cache identification
* @returns A promise resolving to the transcribed text
*/
private async transcribeVoice(media: MessageMedia, message: Message): Promise<string> {
try {
// Check if the transcription exists in the cache
const cachedMessage = this.getCachedMessage(message);
if (cachedMessage) return cachedMessage;
// Convert the base64 media data to a Buffer
const audioBuffer = Buffer.from(media.data, 'base64');
// Convert the buffer to a stream
const audioStream = bufferToStream(audioBuffer);
logger.debug(`[${AIConfig.TranscriptionConfig.provider}->transcribeVoice] Starting audio transcription`);
const transcribedText = await this.openAIService.transcription(audioStream);
// Log the transcribed text
logger.debug(`[${AIConfig.TranscriptionConfig.provider}->transcribeVoice] Transcribed text: ${transcribedText}`);
// Store in cache
this.cache.set(message.id._serialized, transcribedText, CONFIG.botConfig.nodeCacheTime);
return transcribedText;
} catch (error: any) {
// Error handling
logger.error(`Error transcribing voice message: ${error.message}`);
return '<Error transcribing voice message>';
}
}
/**
* Sends a response message appropriately based on chat context.
*
* This function handles the difference between group chats and direct messages:
* - In groups: Replies to the original message, creating a thread
* - In direct chats: Sends a new message to the chat
*
* @param message - The original Message object to reply to
* @param responseMsg - The text content to send
* @param isGroup - Boolean indicating if this is a group chat
* @param client - The WhatsApp client instance
* @returns A promise resolving to the sent message
*/
private returnResponse(message, responseMsg, isGroup, client) {
if (isGroup) return message.reply(responseMsg);
else return client.sendMessage(message.from, responseMsg);
}
/**
* Retrieves a cached message by its unique identifier.
*
* This function checks the NodeCache instance for previously stored
* message content, such as transcriptions or generated responses.
*
* @param msg - The Message object whose content might be cached
* @returns The cached string content or undefined if not found
*/
private getCachedMessage(msg: Message) {
return this.cache.get<string>(msg.id._serialized);
}
}