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.
345 lines (288 loc) • 13 kB
text/typescript
import logger from '../logger';
import { Chat, Message } from 'whatsapp-web.js';
import { Readable } from 'stream';
import { AIConfig, CONFIG } from '../config';
import { AIAnswer } from "../interfaces/ai-interfaces";
export function getFormattedDate() {
const now = new Date();
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const seconds = now.getSeconds().toString().padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
export function logMessage(message: Message, chat: Chat) {
const actualDate = new Date();
logger.info(
`{ chatUser:${chat.id.user}, isGroup:${chat.isGroup}, grId:${chat.id._serialized}, grName:${chat.name}, author:'${message.author}', date:'${actualDate.toLocaleDateString()}-${actualDate.toLocaleTimeString()}', msg:'${message.body}' }`
);
}
export function includeName(bodyMessage: string, name: string): boolean {
const regex = new RegExp(`(^|\\s)${name}($|[!?.]|\\s|,\\s)`, 'i');
return regex.test(bodyMessage);
}
export function removeNonAlphanumeric(str: string): string {
if (!str) return str;
const regex = /[^a-zA-Z0-9]/g;
const normalized = str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
return normalized.replace(regex, '');
}
export function parseCommand(input: string): { command?: string, commandMessage?: string } {
const match = input.match(/^-(\S+)\s*(.*)/);
if (!match) {
return {commandMessage: input};
}
return {command: match[1].trim(), commandMessage: match[2].trim()};
}
export async function getContactName(message: Message) {
const contactInfo = await message.getContact();
const name = contactInfo.shortName || contactInfo.name || contactInfo.pushname || contactInfo.number;
return removeNonAlphanumeric(name);
}
export function bufferToStream(buffer) {
const stream = new Readable();
stream.push(buffer);
stream.push(null);
return stream;
}
export function getUnsupportedMessage(type: string, body?: string) {
const bodyStr = body ? `, body:"${body}"` : ``;
const typeStr = `type:"${type}"`;
return `<Unsupported message: {${typeStr}${bodyStr}}>`
}
export function configValidation() {
function validateProvider(type: string, provider: string, config: any, configObject: any) {
if (!config.apiKey) {
const apiKeyEnvVar = getApiKeyEnvVarName(provider);
logger.warn(`WARNING: ${provider} API key is missing when using ${provider} as ${type} provider.`);
logger.warn(`The ${type} functionality will be automatically disabled.`);
// Disable the functionality when API key is missing
if (type === 'Image') {
AIConfig.ImageConfig.enabled = false;
} else if (type === 'Speech') {
AIConfig.SpeechConfig.enabled = false;
} else if (type === 'Transcription') {
AIConfig.TranscriptionConfig.enabled = false;
} else if (type === 'Chat') {
// For chat, we don't disable but exit since it's essential
logger.error(`ERROR: ${provider} API key is required when using ${provider} as ${type} provider.`);
logger.error(`Please set the ${apiKeyEnvVar} environment variable in your .env file.`);
process.exit(1);
}
return false;
}
if (provider === 'CUSTOM') {
if (!config.baseURL) {
logger.error(`ERROR: CUSTOM_BASEURL is required when using CUSTOM as ${type} provider.`);
logger.error(`Please set the CUSTOM_BASEURL environment variable in your .env file.`);
process.exit(1);
}
if (!config.model) {
const modelEnvVar = getModelEnvVarName('CUSTOM', type);
logger.error(`ERROR: CUSTOM model configuration is required when using CUSTOM as ${type} provider.`);
logger.error(`Please set the ${modelEnvVar} environment variable in your .env file.`);
process.exit(1);
}
}
return true;
}
function getApiKeyEnvVarName(provider: string): string {
const envVarMapping = {
'OPENAI': 'OPENAI_API_KEY',
'CLAUDE': 'CLAUDE_API_KEY',
'QWEN': 'QWEN_API_KEY',
'DEEPSEEK': 'DEEPSEEK_API_KEY',
'ELEVENLABS': 'ELEVENLABS_API_KEY',
'DEEPINFRA': 'DEEPINFRA_API_KEY',
'CUSTOM': 'CUSTOM_API_KEY'
};
return envVarMapping[provider] || `${provider}_API_KEY`;
}
function getModelEnvVarName(provider: string, type: string): string {
const typeMapping = {
'Chat': 'COMPLETION_MODEL',
'Image': 'IMAGE_MODEL',
'Transcription': 'TRANSCRIPTION_MODEL',
'Speech': 'SPEECH_MODEL'
};
if (provider === 'CUSTOM') {
return 'CUSTOM_' + typeMapping[type];
} else {
return `${provider}_${typeMapping[type]}`;
}
}
// Validate chat provider (required)
validateProvider('Chat', AIConfig.ChatConfig.provider, AIConfig.ChatConfig, AIConfig.ChatConfig);
// Validate optional providers
if (AIConfig.ImageConfig.enabled) {
validateProvider('Image', AIConfig.ImageConfig.provider, AIConfig.ImageConfig, AIConfig);
}
// If transcription is enabled, validate it (or disable if API key is missing)
if (AIConfig.TranscriptionConfig.enabled) {
validateProvider('Transcription', AIConfig.TranscriptionConfig.provider, AIConfig.TranscriptionConfig, AIConfig);
}
// If speech is enabled, validate it (or disable if API key is missing)
if (AIConfig.SpeechConfig.enabled) {
validateProvider('Speech', AIConfig.SpeechConfig.provider, AIConfig.SpeechConfig, AIConfig);
}
// If both transcription or speech are disabled, disable voice messages entirely
if (!AIConfig.TranscriptionConfig.enabled || !AIConfig.SpeechConfig.enabled) {
AIConfig.TranscriptionConfig.enabled = false;
AIConfig.SpeechConfig.enabled = false;
logger.warn('WARNING: Voice message handling has been disabled because either transcription or speech service is missing an API key.');
}
const { provider: chatProvider } = AIConfig.ChatConfig;
if (!['OPENAI', 'CLAUDE', 'QWEN', 'DEEPSEEK', 'DEEPINFRA', 'CUSTOM'].includes(chatProvider)) {
logger.error(`ERROR: Invalid CHAT_PROVIDER: ${chatProvider}`);
logger.error(`Valid options are: OPENAI, CLAUDE, QWEN, DEEPSEEK, DEEPINFRA, CUSTOM`);
logger.error(`Please set a valid CHAT_PROVIDER in your .env file.`);
process.exit(1);
}
if (AIConfig.ImageConfig.enabled) {
const { provider: imageProvider } = AIConfig.ImageConfig;
if (!['OPENAI', 'DEEPINFRA'].includes(imageProvider)) {
logger.error(`ERROR: Invalid IMAGE_PROVIDER: ${imageProvider}`);
logger.error(`Valid options are: OPENAI, DEEPINFRA`);
logger.error(`Please set a valid IMAGE_PROVIDER in your .env file.`);
process.exit(1);
}
}
if (AIConfig.TranscriptionConfig.enabled) {
const { provider: transcriptionProvider } = AIConfig.TranscriptionConfig;
if (!['OPENAI', 'DEEPINFRA'].includes(transcriptionProvider)) {
logger.error(`ERROR: Invalid TRANSCRIPTION_PROVIDER: ${transcriptionProvider}`);
logger.error(`Valid options are: OPENAI, DEEPINFRA`);
logger.error(`Please set a valid TRANSCRIPTION_PROVIDER in your .env file.`);
process.exit(1);
}
}
if (AIConfig.SpeechConfig.enabled) {
const { provider: speechProvider } = AIConfig.SpeechConfig;
if (!['OPENAI', 'ELEVENLABS'].includes(speechProvider)) {
logger.error(`ERROR: Invalid SPEECH_PROVIDER: ${speechProvider}`);
logger.error(`Valid options are: OPENAI, ELEVENLABS`);
logger.error(`Please set a valid SPEECH_PROVIDER in your .env file.`);
process.exit(1);
}
}
logger.info('Configuration validation successful.');
}
export function extractAnswer(input: string, botName: string): AIAnswer {
const regex = /<think>[\s\S]*?<\/think>/g;
const inputString = input.replace(regex, '').trim();
if (!inputString || typeof inputString !== 'string') {
return null;
}
try {
return JSON.parse(inputString.trim());
} catch (e) {
}
const startMatch = inputString.match(/[{\[]/);
if (!startMatch) {
logger.debug("[cleanFileName] Valid JSON start character not found");
return {message: inputString, author: botName, type: 'text'};
}
try {
const startIndex = startMatch.index;
let endIndex = inputString.length;
let openBraces = 0;
let openBrackets = 0;
let inString = false;
let escapeNext = false;
for (let i = startIndex; i < inputString.length; i++) {
const char = inputString[i];
if (escapeNext) {
escapeNext = false;
continue;
}
if (char === '\\') {
escapeNext = true;
continue;
}
if (char === '"' && !escapeNext) {
inString = !inString;
continue;
}
if (inString) continue;
if (char === '{') openBraces++;
if (char === '}') openBraces--;
if (char === '[') openBrackets++;
if (char === ']') openBrackets--;
if (i >= startIndex && openBraces === 0 && openBrackets === 0) {
if (startMatch[0] === '{' && char === '}') {
endIndex = i + 1;
break;
}
if (startMatch[0] === '[' && char === ']') {
endIndex = i + 1;
break;
}
}
}
const jsonString = inputString.substring(startIndex, endIndex);
return JSON.parse(jsonString);
} catch (e) {
return {message: inputString, author: botName, type: 'text' };
}
}
export function logConfigInfo() {
logger.info('========== CONFIGURATION SUMMARY ==========');
// Bot general information
logger.info(`📝 BOT CONFIGURATION:`);
logger.info(`• Bot name: ${CONFIG.botConfig.botName}`);
logger.info(`• Response character limit: ${CONFIG.botConfig.maxCharacters}`);
logger.info(`• Maximum messages considered: ${CONFIG.botConfig.maxMsgsLimit}`);
logger.info(`• Maximum message age: ${CONFIG.botConfig.maxHoursLimit} hours`);
logger.info(`• Maximum images processed: ${CONFIG.botConfig.maxImages}`);
// Chat provider and model
logger.info(`🤖 CHAT PROVIDER:`);
logger.info(`• Provider: ${AIConfig.ChatConfig.provider}`);
logger.info(`• Model: ${AIConfig.ChatConfig.model}`);
if (AIConfig.ChatConfig.baseURL && AIConfig.ChatConfig.provider !== 'OPENAI' && AIConfig.ChatConfig.provider !== 'CLAUDE') {
logger.info(`• Base URL: ${AIConfig.ChatConfig.baseURL}`);
}
logger.info(`• Image analysis: ${AIConfig.ChatConfig.analyzeImageDisabled ? 'Disabled' : 'Enabled'}`);
// Image configuration
logger.info(`🖼️ IMAGE GENERATION:`);
if (AIConfig.ImageConfig.enabled) {
logger.info(`• Status: Enabled`);
logger.info(`• Provider: ${AIConfig.ImageConfig.provider}`);
logger.info(`• Model: ${AIConfig.ImageConfig.model}`);
if (AIConfig.ImageConfig.baseURL && AIConfig.ImageConfig.provider !== 'OPENAI') {
logger.info(`• Base URL: ${AIConfig.ImageConfig.baseURL}`);
}
} else {
logger.info(`• Status: Disabled`);
}
// Voice message handling
logger.info(`🎤 VOICE MESSAGE HANDLING:`);
if (AIConfig.TranscriptionConfig.enabled && AIConfig.SpeechConfig.enabled) {
logger.info(`• Status: Enabled`);
// Transcription (Speech-to-Text)
logger.info(`TRANSCRIPTION (Speech-to-Text):`);
logger.info(` • Provider: ${AIConfig.TranscriptionConfig.provider}`);
logger.info(` • Model: ${AIConfig.TranscriptionConfig.model}`);
logger.info(` • Language: ${CONFIG.botConfig.transcriptionLanguage}`);
if (AIConfig.TranscriptionConfig.baseURL && AIConfig.TranscriptionConfig.provider !== 'OPENAI') {
logger.info(` • Base URL: ${AIConfig.TranscriptionConfig.baseURL}`);
}
// Speech (Text-to-Speech)
logger.info(` SPEECH (Text-to-Speech):`);
logger.info(` • Provider: ${AIConfig.SpeechConfig.provider}`);
logger.info(` • Model: ${AIConfig.SpeechConfig.model}`);
logger.info(` • Voice: ${AIConfig.SpeechConfig.voice}`);
if (AIConfig.SpeechConfig.baseURL && AIConfig.SpeechConfig.provider !== 'OPENAI' && AIConfig.SpeechConfig.provider !== 'ELEVENLABS') {
logger.info(` • Base URL: ${AIConfig.SpeechConfig.baseURL}`);
}
} else {
logger.info(`• Status: Disabled`);
}
// Additional information if preferred language is set
if (CONFIG.botConfig.preferredLanguage) {
logger.info(`🌐 LANGUAGE PREFERENCES:`);
logger.info(`• Preferred language: ${CONFIG.botConfig.preferredLanguage}`);
}
logger.info('===========================================');
}