UNPKG

whatsapp-claude-gpt

Version:

WhatsApp-Claude-GPT is a WhatsApp chatbot that supports multiple AI providers for chat, optional image generation/editing, and voice (speech-to-text and text-to-speech). It’s built for natural, contextual conversations and can now also handle reminders an

380 lines (336 loc) 14.9 kB
import { Reminder, ReminderCreateInput } from '../interfaces/reminder'; import { remindersTable as remindersTable } from '../db/schema'; import logger from '../logger'; import { v4 as uuidv4 } from 'uuid'; import { format, fromZonedTime, toZonedTime } from 'date-fns-tz'; import { AiMessage, AIRole, OperationResult, ToolExecutionContext } from "../interfaces/ai-interfaces"; import { chatConfigurationManager } from "../config/chat-configurations"; import { addSeconds, extractAnswer, getUserName } from "../utils"; import { addDays, addMonths, addWeeks } from 'date-fns'; import { CONFIG } from "../config"; import Roboto from "../bot/roboto"; import WspWeb from "../bot/wsp-web"; import { db } from "../db"; import { and, eq } from "drizzle-orm"; import { Chat, Message } from "whatsapp-web.js"; class ReminderManager { private reminderInterval: NodeJS.Timeout | null = null; private isChecking = false; constructor() { // Reminder checker is now started from the 'ready' event in src/index.ts // to ensure the WhatsApp client is available. } public startReminderChecker() { if (this.reminderInterval) { logger.warn('Reminder checker already running, skipping duplicate start.'); return; } logger.info('Starting reminder checker'); this.reminderInterval = setInterval(() => { this.checkReminders().catch((e) => { logger.error(`[ReminderManager] Unhandled error in checkReminders: ${e.message}`); }); }, 59 * 1000); } public stopReminderChecker() { if (this.reminderInterval) { clearInterval(this.reminderInterval); this.reminderInterval = null; } } private async checkReminders() { // Prevent overlapping runs when a previous check is still in progress. if (this.isChecking) { logger.warn('Reminder check skipped: previous check still in progress.'); return; } this.isChecking = true; try { const now = new Date(); const dueReminders = await db.select() .from(remindersTable) .where(and( eq(remindersTable.isActive, true) )) .all(); for (const r of dueReminders) { const reminder = this.mapRowToReminder(r); const scheduledDate = fromZonedTime(reminder.reminderDate, reminder.reminderDateTZ); if (scheduledDate > now) continue; try { const diffMs = now.getTime() - scheduledDate.getTime(); if((diffMs / 60000) <= 60) { if (diffMs > 60000) { logger.warn(`Reminder for ${reminder.chatId} (${reminder.id}) is firing ${Math.round(diffMs / 60000)} minute(s) late.`); } await this.sendReminderMessage(reminder); } else { logger.info(`Reminder ${reminder.id} for chat ${reminder.chatId} has expired. Reminder not sent`); } if (reminder.recurrenceType && reminder.recurrenceType !== 'none') { const nextDate = this.calculateNextRecurrence(reminder); if (nextDate) { const zonedDate = toZonedTime(nextDate, reminder.reminderDateTZ); await this.updateReminder(reminder.id, reminder.chatId, { reminderDate: format(zonedDate, "yyyy-MM-dd'T'HH:mm:ss", { timeZone: reminder.reminderDateTZ }), updatedAt: new Date(), }); logger.info(`Recurring reminder updated for next occurrence: ${reminder.id}`); } else { await this.deactivateReminder(reminder.id, reminder.chatId); logger.info(`Recurring reminder completed and deactivated: ${reminder.id}`); } } else { await this.deleteReminder(reminder.id, reminder.chatId); } logger.info(`Reminder ${reminder.id} for chat ${reminder.chatId} processed.`); } catch (err) { logger.error(`Error processing reminder ${reminder.id} for chat ${reminder.chatId}: ${err.message}`); } } } finally { this.isChecking = false; } } private async sendReminderMessage(reminder: Reminder){ const chatConfig = await chatConfigurationManager.getChatConfig(reminder.chatId, reminder.chatName); const systemPrompt = CONFIG.getSystemPrompt(chatConfig); const aiMessage: AiMessage = { role: AIRole.USER, name: 'SYSTEM', content: [{ type: 'text', value: `SYSTEM: The user has a new reminder, write a message to remind them of the following: "${reminder.message}". RecipientName: ${reminder.chatName}. Date: "${reminder.reminderDate}"`, dateString: reminder.reminderDate, author_id: 'SYSTEM' }] }; const aiResponse = await Roboto.sendMessageToAi([aiMessage], systemPrompt, chatConfig, false); const reminderMsg = extractAnswer(aiResponse, chatConfig.botName); if (!reminderMsg || !reminderMsg.message) return false; return WspWeb.getWspClient().sendMessage(reminder.chatId, reminderMsg.message); } /** * Calculates the next recurrence date for a reminder */ private calculateNextRecurrence(reminder: Reminder): Date | null { const currentDate = fromZonedTime(reminder.reminderDate, reminder.reminderDateTZ); const interval = reminder.recurrenceInterval || 1; let nextDate: Date; switch (reminder.recurrenceType) { case 'minutes': nextDate = addSeconds(currentDate, interval*60); break; case 'daily': nextDate = addDays(currentDate, interval); break; case 'weekly': nextDate = addWeeks(currentDate, interval); break; case 'monthly': nextDate = addMonths(currentDate, interval); break; default: return null; } if (reminder.recurrenceEndDate && reminder.recurrenceEndDateTZ) { const endDate = fromZonedTime(reminder.recurrenceEndDate, reminder.recurrenceEndDateTZ); if (nextDate > endDate) { return null; } } return nextDate; } public async processFunctionCall(args, context?: ToolExecutionContext): Promise<OperationResult>{ const { action, message: reminderMessage, reminder_date, reminder_date_timezone, reminder_id, recurrence_type, recurrence_interval, recurrence_end_date, recurrence_end_date_timezone, msg_id } = args; let chatId: string; let chatName: string; // Use server-side context when available (AI tool calls); fall back to // deriving from msg_id for backward compatibility with manual flows. if (context) { chatId = context.chatId; chatName = context.chatName; } else { const wspMsg: Message = await WspWeb.getWspClient().getMessageById(msg_id) const chatData: Chat = await wspMsg.getChat(); chatId = chatData.id._serialized; chatName = chatData.name ?? await getUserName(wspMsg); } let responseMessage = ''; let reminder; switch (action) { case 'list': const remindersList = await this.getRemindersByUser(chatId); responseMessage = JSON.stringify(remindersList); break; case 'create': reminder = await this.createReminder({ message: reminderMessage, reminderDate: reminder_date, reminderDateTZ: reminder_date_timezone || CONFIG.BotConfig.botTimezone, chatId: chatId, chatName: chatName, recurrenceType: recurrence_type || 'none', recurrenceInterval: recurrence_interval || 1, recurrenceEndDate: recurrence_end_date || null, recurrenceEndDateTZ: recurrence_end_date_timezone || CONFIG.BotConfig.botTimezone }); responseMessage = `Reminder created successfully. Data: ${JSON.stringify(reminder)}. (Do not mention the reminder id to the user)`; break; case 'update': reminder = await this.updateReminder(reminder_id, chatId, { message: reminderMessage, reminderDate: reminder_date, reminderDateTZ: reminder_date_timezone || CONFIG.BotConfig.botTimezone, recurrenceType: recurrence_type, recurrenceInterval: recurrence_interval, recurrenceEndDate: recurrence_end_date, recurrenceEndDateTZ: recurrence_end_date_timezone || CONFIG.BotConfig.botTimezone }); responseMessage = `Reminder updated successfully. Data: ${JSON.stringify(reminder)}. (Do not mention the reminder id to the user)`; break; case 'delete': await this.deleteReminder(reminder_id, chatId); responseMessage = `Reminder deleted successfully`; break; case 'deactivate': await this.deactivateReminder(reminder_id, chatId); responseMessage = `Reminder deactivated successfully`; break; case 'reactivate': await this.reactivateReminder(reminder_id, chatId); responseMessage = `Reminder reactivated successfully`; break; } return {success: true, result: responseMessage}; } /** * Creates a new reminder */ private async createReminder(input: ReminderCreateInput): Promise<Reminder> { const now = new Date(); const reminder: Reminder = { id: uuidv4(), message: input.message, reminderDate: input.reminderDate, reminderDateTZ: input.reminderDateTZ, chatId: input.chatId, chatName: input.chatName, isActive: true, createdAt: now, updatedAt: now, recurrenceType: input.recurrenceType ?? 'none', recurrenceInterval: input.recurrenceInterval ?? 1, recurrenceEndDate: input.recurrenceEndDate ?? null, recurrenceEndDateTZ: input.recurrenceEndDateTZ ?? null, }; await db.insert(remindersTable).values({ ...reminder, createdAt: reminder.createdAt.toISOString(), updatedAt: reminder.updatedAt.toISOString(), }).run(); logger.info(`Created reminder with ID: ${reminder.id} for user: ${reminder.chatId}`); return reminder; } /** * Updates an existing reminder */ private async updateReminder(id: string, chatId: string, updates: Partial<ReminderCreateInput>): Promise<Reminder | null> { const updatedAt = new Date().toISOString(); const result = await db.update(remindersTable) .set({ ...updates , updatedAt }) .where(and( eq(remindersTable.id, id), eq(remindersTable.chatId, chatId) )) .returning(); if (result.length === 0) { logger.warn(`Reminder with ID ${id} not found for chat ${chatId}`); return null; } logger.info(`Updated reminder with ID: ${id} for chat ${chatId}`); return this.mapRowToReminder(result[0]); } /** * Deletes a reminder scoped to the owning chat. */ private async deleteReminder(id: string, chatId: string): Promise<boolean> { const result = await db.delete(remindersTable) .where(and( eq(remindersTable.id, id), eq(remindersTable.chatId, chatId) )) .run(); if (result.changes === 0) { logger.warn(`Reminder with ID ${id} not found for chat ${chatId}`); return false; } logger.info(`Deleted reminder with ID: ${id} for chat ${chatId}`); return true; } /** * Gets all reminders for a specific user */ private async getRemindersByUser(userId: string): Promise<Reminder[]> { const rows = await db.select().from(remindersTable).where(eq(remindersTable.chatId, userId)).all(); return rows.map(this.mapRowToReminder); } /** * Deactivates a reminder without deleting it, scoped to the owning chat. */ private async deactivateReminder(id: string, chatId: string): Promise<boolean> { const updatedAt = new Date().toISOString(); const result = await db.update(remindersTable) .set({ isActive: false, updatedAt }) .where(and( eq(remindersTable.id, id), eq(remindersTable.chatId, chatId) )) .run(); if (result.changes === 0) { logger.warn(`Reminder with ID ${id} not found for chat ${chatId}`); return false; } logger.info(`Deactivated reminder with ID: ${id} for chat ${chatId}`); return true; } /** * Reactivates a reminder, scoped to the owning chat. */ private async reactivateReminder(id: string, chatId: string): Promise<boolean> { const updatedAt = new Date().toISOString(); const result = await db.update(remindersTable) .set({ isActive: true, updatedAt }) .where(and( eq(remindersTable.id, id), eq(remindersTable.chatId, chatId) )) .run(); if (result.changes === 0) { logger.warn(`Reminder with ID ${id} not found for chat ${chatId}`); return false; } logger.info(`Reactivated reminder with ID: ${id} for chat ${chatId}`); return true; } private mapRowToReminder(row: any): Reminder { return { ...row, createdAt: new Date(row.createdAt), updatedAt: new Date(row.updatedAt), }; } } const Reminders = new ReminderManager(); export default Reminders;