feat: añadir router de comandos (Etapa 1) y shared.ts; usar route
Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>main
parent
d40e5e7990
commit
170859c030
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Router de comandos (Etapa 1)
|
||||
* Por ahora no maneja nada y devuelve null para forzar fallback al CommandService actual.
|
||||
* Nota: No importar CommandService aquí para evitar ciclos de import.
|
||||
*/
|
||||
|
||||
export type RoutedMessage = {
|
||||
recipient: string;
|
||||
message: string;
|
||||
mentions?: string[];
|
||||
};
|
||||
|
||||
export type RouteContext = {
|
||||
sender: string;
|
||||
groupId: string;
|
||||
message: string;
|
||||
mentions: string[];
|
||||
messageId?: string;
|
||||
participant?: string;
|
||||
fromMe?: boolean;
|
||||
};
|
||||
|
||||
export async function route(_context: RouteContext): Promise<RoutedMessage[] | null> {
|
||||
// En esta etapa no se maneja nada; devolver null para usar el código actual.
|
||||
return null;
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Utilidades compartidas para handlers de comandos (Etapa 1).
|
||||
* Aún no se usan desde CommandService; servirán en etapas siguientes.
|
||||
*/
|
||||
|
||||
import { TaskService } from '../../tasks/service';
|
||||
import { GroupSyncService } from '../group-sync';
|
||||
|
||||
export const ACTION_ALIASES: Record<string, string> = {
|
||||
'n': 'nueva',
|
||||
'nueva': 'nueva',
|
||||
'crear': 'nueva',
|
||||
'+': 'nueva',
|
||||
'ver': 'ver',
|
||||
'mostrar': 'ver',
|
||||
'listar': 'ver',
|
||||
'ls': 'ver',
|
||||
'mias': 'ver',
|
||||
'mías': 'ver',
|
||||
'todas': 'ver',
|
||||
'todos': 'ver',
|
||||
'x': 'completar',
|
||||
'hecho': 'completar',
|
||||
'completar': 'completar',
|
||||
'done': 'completar',
|
||||
'tomar': 'tomar',
|
||||
'claim': 'tomar',
|
||||
'asumir': 'tomar',
|
||||
'asumo': 'tomar',
|
||||
'soltar': 'soltar',
|
||||
'unassign': 'soltar',
|
||||
'dejar': 'soltar',
|
||||
'liberar': 'soltar',
|
||||
'renunciar': 'soltar',
|
||||
'ayuda': 'ayuda',
|
||||
'help': 'ayuda',
|
||||
'info': 'ayuda',
|
||||
'?': 'ayuda',
|
||||
'config': 'configurar',
|
||||
'configurar': 'configurar',
|
||||
'web': 'web'
|
||||
};
|
||||
|
||||
export const SCOPE_ALIASES: Record<string, 'mis' | 'todos'> = {
|
||||
'todo': 'todos',
|
||||
'todos': 'todos',
|
||||
'todas': 'todos',
|
||||
'mis': 'mis',
|
||||
'mias': 'mis',
|
||||
'mías': 'mis',
|
||||
'yo': 'mis'
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatea un Date a YYYY-MM-DD respetando TZ (por defecto Europe/Madrid).
|
||||
*/
|
||||
export function ymdInTZ(d: Date, tz?: string): string {
|
||||
const TZ = (tz && tz.trim()) || (process.env.TZ && process.env.TZ.trim()) || 'Europe/Madrid';
|
||||
const parts = new Intl.DateTimeFormat('en-GB', {
|
||||
timeZone: TZ,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
}).formatToParts(d);
|
||||
const get = (t: string) => parts.find(p => p.type === t)?.value || '';
|
||||
return `${get('year')}-${get('month')}-${get('day')}`;
|
||||
}
|
||||
|
||||
export function todayYMD(tz?: string): string {
|
||||
return ymdInTZ(new Date(), tz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsea múltiples IDs desde tokens, deduplica, y aplica límite.
|
||||
*/
|
||||
export function parseMultipleIds(tokens: string[], max: number = 10): { ids: number[]; truncated: boolean } {
|
||||
const raw = (tokens || []).join(' ').trim();
|
||||
const all = raw
|
||||
? raw
|
||||
.split(/[,\s]+/)
|
||||
.map(t => t.trim())
|
||||
.filter(Boolean)
|
||||
.map(t => parseInt(t, 10))
|
||||
.filter(n => Number.isFinite(n) && n > 0)
|
||||
: [];
|
||||
const dedup: number[] = [];
|
||||
const seen = new Set<number>();
|
||||
for (const n of all) {
|
||||
if (!seen.has(n)) {
|
||||
seen.add(n);
|
||||
dedup.push(n);
|
||||
}
|
||||
}
|
||||
const truncated = dedup.length > max;
|
||||
const ids = dedup.slice(0, max);
|
||||
return { ids, truncated };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resuelve un ID de entrada (display_code) a task.id si está activa.
|
||||
*/
|
||||
export function resolveTaskIdFromInput(n: number): number | null {
|
||||
const byCode = TaskService.getActiveTaskByDisplayCode(n);
|
||||
return byCode ? byCode.id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica la política de membresía para acciones sobre una tarea.
|
||||
* Devuelve true si el usuario está permitido según flags/env.
|
||||
*/
|
||||
export function enforceMembership(sender: string, task: { group_id?: string | null }, enforceFlag?: boolean): boolean {
|
||||
const enforce =
|
||||
typeof enforceFlag === 'boolean'
|
||||
? enforceFlag
|
||||
: String(process.env.GROUP_MEMBERS_ENFORCE || '').toLowerCase() === 'true';
|
||||
|
||||
const gid = task?.group_id || null;
|
||||
if (!gid) return true; // tareas personales no requieren membresía
|
||||
if (!enforce) return true;
|
||||
if (!GroupSyncService.isSnapshotFresh(gid)) return true;
|
||||
|
||||
return GroupSyncService.isUserActiveInGroup(sender, gid);
|
||||
}
|
||||
Loading…
Reference in New Issue