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