Compare commits

...

7 Commits

@ -1,13 +1,21 @@
# Task WhatsApp Chatbot # Task WhatsApp Chatbot
## Future Steps A WhatsApp chatbot for task management, designed to work with Evolution API in a secure internal network environment.
### Core Principles ## 📌 Overview
1. **Trust-but-Verify Approach**: Leverage WhatsApp's message metadata to minimize API calls while maintaining security This service provides a WhatsApp interface for task management within WhatsApp groups. It:
2. **Progressive Validation**: Verify users naturally through interactions rather than upfront checks - Listens for `/tarea` commands in WhatsApp groups
3. **Background Reconciliation**: Use periodic syncs to maintain data accuracy without impacting real-time performance - Stores tasks in a SQLite database
- Manages user permissions and group membership
- Integrates with Evolution API for WhatsApp connectivity
### Message Processing Flow ## 🔐 Security Model
- **Internal Networking**: The webhook only accepts connections from Evolution API via internal Docker networking
- **Environment Variables**: Sensitive configuration is managed through environment variables
- **Group Restrictions**: Only operates within pre-approved WhatsApp groups
- **Input Validation**: Sanitizes and validates all user inputs
## 🧱 Architecture
```mermaid ```mermaid
graph TD graph TD
A[Webhook Received] --> B{Valid Payload?} A[Webhook Received] --> B{Valid Payload?}
@ -24,25 +32,77 @@ graph TD
H -->|No| J[Process Command] H -->|No| J[Process Command]
``` ```
### Alias for actions ## ✅ Current Features
- Task creation with optional due dates
- I want that, when an user sends `/tarea` to the bot, with no extra arguments, it should show all the pending tasks for the user in a private message (the same behaviour as calling `/tarea mostrar` - Basic command parsing (`/tarea nueva`, `/tarea mostrar`)
- Group membership tracking
### Periodic Sync Strategy - SQLite database persistence
1. **Rotating Group Check**: - Health check endpoint
- Verify 1-2 groups per sync cycle - Environment validation
- Prioritize recently active groups - Input validation for dates and commands
2. **User Reconciliation**:
- Add newly discovered users ## 🛠️ Setup
- Update `last_confirmed` for active users ### Environment Variables
3. **Optimizations**: ```env
- Cache active group IDs in memory EVOLUTION_API_URL=http://evolution-api:3000
- Batch database writes EVOLUTION_API_KEY=your-api-key
- Exponential backoff for API failures EVOLUTION_API_INSTANCE=main
WHATSAPP_COMMUNITY_ID=your-community-id
### Security Considerations CHATBOT_PHONE_NUMBER=1234567890
- Reject messages from: WEBHOOK_URL=https://your-webhook.com
- Non-community groups PORT=3007
- Unknown private chats NODE_ENV=production
- Implement rate limiting ```
- Maintain audit logs of verification events
### Development Setup
```bash
# Install dependencies
bun install
# Start development server
bun run dev
# Run tests
bun test
```
## 📅 Roadmap
### High Priority
- [ ] Implement ResponseQueue processing logic with retries
- [ ] Add database schema validation and migrations
- [ ] Add error recovery with transaction rollback
- [ ] Implement group sync delta updates
### Medium Priority
- [ ] Add task assignment and ownership
- [ ] Implement user permissions system
- [ ] Add rate limiting for API calls
- [ ] Create task history tracking
### Low Priority
- [ ] Add task reminders system
- [ ] Implement multi-language support
- [ ] Create analytics dashboard
- [ ] Add user-friendly task list UI
## 🧪 Testing
### Running Tests
```bash
bun test
```
### Test Coverage
- Webhook validation
- Command parsing
- Environment checks
- Basic error handling
- Input validation
## 🧑‍💻 Contributing
1. Fork the repository
2. Create a feature branch
3. Add tests for new functionality
4. Submit a pull request
## 📚 Documentation
For detailed API documentation and architecture decisions, see the [docs/](docs/) directory.

@ -41,12 +41,6 @@ export class WebhookServer {
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== 'test') {
console.log(' Incoming webhook request:') console.log(' Incoming webhook request:')
// console.log(' Incoming webhook request:', {
// method: request.method,
// path: url.pathname,
// time: new Date().toISOString()
// });
} }
// 1. Method validation // 1. Method validation
@ -83,10 +77,12 @@ export class WebhookServer {
switch (payload.event) { switch (payload.event) {
case 'messages.upsert': case 'messages.upsert':
console.log(' Handling message upsert:', { if (process.env.NODE_ENV !== 'test') {
groupId: payload.data?.key?.remoteJid, console.log(' Handling message upsert:', {
message: payload.data?.message?.conversation groupId: payload.data?.key?.remoteJid,
}); message: payload.data?.message?.conversation
});
}
await WebhookServer.handleMessageUpsert(payload.data); await WebhookServer.handleMessageUpsert(payload.data);
break; break;
// Other events will be added later // Other events will be added later
@ -105,8 +101,10 @@ export class WebhookServer {
static async handleMessageUpsert(data: any) { static async handleMessageUpsert(data: any) {
if (!data?.key?.remoteJid || !data.message || !data.message.conversation) { if (!data?.key?.remoteJid || !data.message || !data.message.conversation) {
console.log('⚠️ Invalid message format - missing required fields'); if (process.env.NODE_ENV !== 'test') {
console.log(data); console.log('⚠️ Invalid message format - missing required fields');
console.log(data);
}
return; return;
} }
@ -129,6 +127,9 @@ export class WebhookServer {
let dueDate = ''; let dueDate = '';
// Process remaining parts // Process remaining parts
const dateCandidates = [];
// First collect all valid future dates
for (let i = 2; i < commandParts.length; i++) { for (let i = 2; i < commandParts.length; i++) {
const part = commandParts[i]; const part = commandParts[i];
// Check for date (YYYY-MM-DD) // Check for date (YYYY-MM-DD)
@ -138,16 +139,31 @@ export class WebhookServer {
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);
if (date >= today) { if (date >= today) {
dueDate = part; dateCandidates.push({ index: i, date, part });
} else {
descriptionParts.push(part); // Keep past dates in description
} }
} else { }
}
// Use the last valid future date as due date
if (dateCandidates.length > 0) {
const lastDate = dateCandidates[dateCandidates.length - 1];
dueDate = lastDate.part;
// Add all parts except the last date to description
for (let i = 2; i < commandParts.length; i++) {
// Skip the due date part
if (i === lastDate.index) continue;
// Add to description if it's a date candidate but not selected or a regular part
const part = commandParts[i];
descriptionParts.push(part); descriptionParts.push(part);
} }
} else {
// No valid future dates, add all parts to description
descriptionParts = commandParts.slice(2);
} }
const description = descriptionParts.join(' '); const description = descriptionParts.join(' ').trim();
console.log('🔍 Detected /tarea command:', { console.log('🔍 Detected /tarea command:', {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
@ -156,7 +172,7 @@ export class WebhookServer {
rawMessage: messageText rawMessage: messageText
}); });
const mentions = data.contextInfo?.mentionedJid || []; const mentions = data.message?.contextInfo?.mentionedJid || [];
console.log('✅ Successfully parsed command:', { console.log('✅ Successfully parsed command:', {
action, action,
description, description,

@ -223,6 +223,12 @@ describe('WebhookServer', () => {
} }
}); });
function getFutureDate(days: number): string {
const date = new Date();
date.setDate(date.getDate() + days);
return date.toISOString().split('T')[0];
}
describe('/tarea command logging', () => { describe('/tarea command logging', () => {
let consoleSpy: any; let consoleSpy: any;
@ -268,6 +274,7 @@ describe('WebhookServer', () => {
}); });
test('should log command with due date', async () => { test('should log command with due date', async () => {
const futureDate = getFutureDate(3); // Get date 3 days in future
const payload = { const payload = {
event: 'messages.upsert', event: 'messages.upsert',
instance: 'test-instance', instance: 'test-instance',
@ -277,7 +284,7 @@ describe('WebhookServer', () => {
participant: 'user123@s.whatsapp.net' participant: 'user123@s.whatsapp.net'
}, },
message: { message: {
conversation: '/tarea nueva Finish project @user2 2025-04-30', conversation: `/tarea nueva Finish project @user2 ${futureDate}`,
contextInfo: { contextInfo: {
mentionedJid: ['user2@s.whatsapp.net'] mentionedJid: ['user2@s.whatsapp.net']
} }
@ -288,24 +295,25 @@ describe('WebhookServer', () => {
await WebhookServer.handleRequest(createTestRequest(payload)); await WebhookServer.handleRequest(createTestRequest(payload));
// Verify the two command-related log calls // Verify the two command-related log calls
expect(consoleSpy).toHaveBeenCalledWith( expect(consoleSpy).toHaveBeenCalledTimes(2);
'🔍 Detected /tarea command:',
expect.objectContaining({ // First call should be the command detection
from: 'user123@s.whatsapp.net', expect(consoleSpy.mock.calls[0][0]).toBe('🔍 Detected /tarea command:');
group: 'group123@g.us', expect(consoleSpy.mock.calls[0][1]).toEqual({
rawMessage: '/tarea nueva Finish project @user2 2025-04-30' timestamp: expect.any(String),
}) from: 'user123@s.whatsapp.net',
); group: 'group123@g.us',
rawMessage: `/tarea nueva Finish project @user2 ${futureDate}`
});
expect(consoleSpy).toHaveBeenCalledWith( // Second call should be the successful parsing
'✅ Successfully parsed command:', expect(consoleSpy.mock.calls[1][0]).toBe('✅ Successfully parsed command:');
expect.objectContaining({ expect(consoleSpy.mock.calls[1][1]).toEqual({
action: 'nueva', action: 'nueva',
description: 'Finish project @user2', description: 'Finish project @user2',
dueDate: '2025-04-30', dueDate: futureDate,
mentionCount: expect.any(Number) mentionCount: 1
}) });
);
}); });
}); });
@ -327,4 +335,115 @@ describe('WebhookServer', () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(mockAdd).toHaveBeenCalled(); expect(mockAdd).toHaveBeenCalled();
}); });
test('should handle multiple dates in command (use last one as due date)', async () => {
const futureDate1 = getFutureDate(3);
const futureDate2 = getFutureDate(5);
const payload = {
event: 'messages.upsert',
instance: 'test-instance',
data: {
key: {
remoteJid: 'group-id@g.us',
participant: 'sender-id@s.whatsapp.net'
},
message: {
conversation: `/tarea nueva Test task ${futureDate1} some text ${futureDate2}`
}
}
};
await WebhookServer.handleRequest(createTestRequest(payload));
expect(console.log).toHaveBeenCalledWith(
'✅ Successfully parsed command:',
expect.objectContaining({
description: `Test task ${futureDate1} some text`,
dueDate: futureDate2
})
);
});
test('should ignore past dates as due dates', async () => {
const pastDate = '2020-01-01';
const futureDate = getFutureDate(2);
const payload = {
event: 'messages.upsert',
instance: 'test-instance',
data: {
key: {
remoteJid: 'group-id@g.us',
participant: 'sender-id@s.whatsapp.net'
},
message: {
conversation: `/tarea nueva Test task ${pastDate} more text ${futureDate}`
}
}
};
await WebhookServer.handleRequest(createTestRequest(payload));
expect(console.log).toHaveBeenCalledWith(
'✅ Successfully parsed command:',
expect.objectContaining({
description: `Test task ${pastDate} more text`,
dueDate: futureDate
})
);
});
test('should handle multiple past dates correctly', async () => {
const pastDate1 = '2020-01-01';
const pastDate2 = '2021-01-01';
const payload = {
event: 'messages.upsert',
instance: 'test-instance',
data: {
key: {
remoteJid: 'group-id@g.us',
participant: 'sender-id@s.whatsapp.net'
},
message: {
conversation: `/tarea nueva Test task ${pastDate1} and ${pastDate2}`
}
}
};
await WebhookServer.handleRequest(createTestRequest(payload));
expect(console.log).toHaveBeenCalledWith(
'✅ Successfully parsed command:',
expect.objectContaining({
description: `Test task ${pastDate1} and ${pastDate2}`,
dueDate: 'none'
})
);
});
test('should handle mixed valid and invalid date formats', async () => {
const futureDate = getFutureDate(2);
const payload = {
event: 'messages.upsert',
instance: 'test-instance',
data: {
key: {
remoteJid: 'group-id@g.us',
participant: 'sender-id@s.whatsapp.net'
},
message: {
conversation: `/tarea nueva Test task 2023-13-01 (invalid) ${futureDate} 25/12/2023 (invalid)`
}
}
};
await WebhookServer.handleRequest(createTestRequest(payload));
expect(console.log).toHaveBeenCalledWith(
'✅ Successfully parsed command:',
expect.objectContaining({
description: 'Test task 2023-13-01 (invalid) 25/12/2023 (invalid)',
dueDate: futureDate
})
);
});
}); });

Loading…
Cancel
Save