You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
186 lines
5.8 KiB
TypeScript
186 lines
5.8 KiB
TypeScript
import type { Database } from 'bun:sqlite';
|
|
import { db, ensureUserExists } from '../db';
|
|
import { normalizeWhatsAppId } from '../utils/whatsapp';
|
|
import { TaskService } from '../tasks/service';
|
|
import { GroupSyncService } from './group-sync';
|
|
|
|
type CommandContext = {
|
|
sender: string; // normalized user id (digits only), but accept raw too
|
|
groupId: string; // full JID (e.g., xxx@g.us)
|
|
message: string; // raw message text
|
|
mentions: string[]; // array of raw JIDs mentioned
|
|
};
|
|
|
|
export type CommandResponse = {
|
|
recipient: string;
|
|
message: string;
|
|
mentions?: string[]; // full JIDs to mention in the outgoing message
|
|
};
|
|
|
|
export class CommandService {
|
|
static dbInstance: Database = db;
|
|
|
|
private static parseNueva(message: string, mentionsNormalized: string[]): {
|
|
action: string;
|
|
description: string;
|
|
dueDate: string | null;
|
|
} {
|
|
const parts = (message || '').trim().split(/\s+/);
|
|
const action = (parts[1] || '').toLowerCase();
|
|
|
|
// Buscar última fecha futura con formato YYYY-MM-DD
|
|
const dateIndices: { index: number; text: string }[] = [];
|
|
for (let i = 2; i < parts.length; i++) {
|
|
const p = parts[i];
|
|
if (/^\d{4}-\d{2}-\d{2}$/.test(p)) {
|
|
const d = new Date(p);
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
if (!isNaN(d.getTime()) && d >= today) {
|
|
dateIndices.push({ index: i, text: p });
|
|
}
|
|
}
|
|
}
|
|
|
|
let dueDate: string | null = null;
|
|
let descriptionTokens: string[] = [];
|
|
|
|
const isMentionToken = (token: string) => token.startsWith('@');
|
|
|
|
if (dateIndices.length > 0) {
|
|
const last = dateIndices[dateIndices.length - 1];
|
|
dueDate = last.text;
|
|
for (let i = 2; i < parts.length; i++) {
|
|
if (i === last.index) continue;
|
|
const token = parts[i];
|
|
if (isMentionToken(token)) continue; // quitar @menciones del texto
|
|
descriptionTokens.push(token);
|
|
}
|
|
} else {
|
|
for (let i = 2; i < parts.length; i++) {
|
|
const token = parts[i];
|
|
if (isMentionToken(token)) continue;
|
|
descriptionTokens.push(token);
|
|
}
|
|
}
|
|
|
|
const description = descriptionTokens.join(' ').trim();
|
|
|
|
return {
|
|
action,
|
|
description,
|
|
dueDate,
|
|
};
|
|
}
|
|
|
|
private static async processTareaCommand(
|
|
context: CommandContext
|
|
): Promise<CommandResponse[]> {
|
|
const trimmed = (context.message || '').trim();
|
|
const tokens = trimmed.split(/\s+/);
|
|
const action = (tokens[1] || '').toLowerCase();
|
|
|
|
if (action !== 'nueva') {
|
|
return [{
|
|
recipient: context.sender,
|
|
message: `Acción ${action || '(vacía)'} no implementada aún`
|
|
}];
|
|
}
|
|
|
|
// Parseo específico de "nueva"
|
|
// Normalizar menciones del contexto para parseo y asignaciones
|
|
const mentionsNormalizedFromContext = Array.from(new Set(
|
|
(context.mentions || [])
|
|
.map(j => normalizeWhatsAppId(j))
|
|
.filter((id): id is string => !!id)
|
|
));
|
|
// Detectar también tokens de texto que empiezan por '@' como posibles asignados
|
|
const atTokenCandidates = tokens.slice(2)
|
|
.filter(t => t.startsWith('@'))
|
|
.map(t => t.replace(/^@+/, ''));
|
|
const normalizedFromAtTokens = Array.from(new Set(
|
|
atTokenCandidates
|
|
.map(v => normalizeWhatsAppId(v))
|
|
.filter((id): id is string => !!id)
|
|
));
|
|
const combinedAssigneeCandidates = Array.from(new Set([
|
|
...mentionsNormalizedFromContext,
|
|
...normalizedFromAtTokens
|
|
]));
|
|
|
|
const { description, dueDate } = this.parseNueva(trimmed, mentionsNormalizedFromContext);
|
|
|
|
// Asegurar creador
|
|
const createdBy = ensureUserExists(context.sender, this.dbInstance);
|
|
if (!createdBy) {
|
|
throw new Error('No se pudo asegurar el usuario creador');
|
|
}
|
|
|
|
// Normalizar menciones y excluir duplicados y el número del bot
|
|
const botNumber = process.env.CHATBOT_PHONE_NUMBER || '';
|
|
const assigneesNormalized = Array.from(new Set(
|
|
combinedAssigneeCandidates
|
|
.filter(id => !botNumber || id !== botNumber)
|
|
));
|
|
|
|
// Asegurar usuarios asignados
|
|
const ensuredAssignees = assigneesNormalized
|
|
.map(id => ensureUserExists(id, this.dbInstance))
|
|
.filter((id): id is string => !!id);
|
|
|
|
// Si no hay asignados, asignar al creador
|
|
const assignmentUserIds = ensuredAssignees.length > 0 ? ensuredAssignees : [createdBy];
|
|
|
|
// Definir group_id solo si el grupo está activo
|
|
const groupIdToUse = (context.groupId && GroupSyncService.isGroupActive(context.groupId))
|
|
? context.groupId
|
|
: null;
|
|
|
|
// Crear tarea y asignaciones
|
|
const taskId = TaskService.createTask(
|
|
{
|
|
description: description || '',
|
|
due_date: dueDate ?? null,
|
|
group_id: groupIdToUse,
|
|
created_by: createdBy,
|
|
},
|
|
assignmentUserIds.map(uid => ({
|
|
user_id: uid,
|
|
assigned_by: createdBy,
|
|
}))
|
|
);
|
|
|
|
const mentionsForSending = assignmentUserIds.map(uid => `${uid}@s.whatsapp.net`);
|
|
|
|
const assignedList = assignmentUserIds.map(uid => `@${uid}`).join(' ');
|
|
const resp =
|
|
`✅ Tarea ${taskId} creada: "${description || '(sin descripción)'}"` +
|
|
(dueDate ? ` (vence ${dueDate})` : '') +
|
|
(assignedList ? ` — asignados: ${assignedList}` : '');
|
|
|
|
// Enviar al grupo si está activo; si no, al creador (DM)
|
|
const recipient = groupIdToUse || createdBy;
|
|
|
|
return [{
|
|
recipient,
|
|
message: resp,
|
|
mentions: mentionsForSending.length > 0 ? mentionsForSending : undefined
|
|
}];
|
|
}
|
|
|
|
static async handle(context: CommandContext): Promise<CommandResponse[]> {
|
|
if (!context.message.startsWith('/tarea')) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
return await this.processTareaCommand(context);
|
|
} catch (error) {
|
|
return [{
|
|
recipient: context.sender,
|
|
message: 'Error processing command'
|
|
}];
|
|
}
|
|
}
|
|
}
|