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.
92 lines
2.8 KiB
TypeScript
92 lines
2.8 KiB
TypeScript
import { sha256Hex } from './crypto';
|
|
|
|
function escapeIcsText(s: string): string {
|
|
return String(s)
|
|
.replace(/\\/g, '\\\\')
|
|
.replace(/\n/g, '\\n')
|
|
.replace(/\r/g, '')
|
|
.replace(/,/g, '\\,')
|
|
.replace(/;/g, '\\;');
|
|
}
|
|
|
|
function foldIcsLine(line: string): string {
|
|
// 75 octetos; para simplicidad contamos caracteres (UTF-8 simple en nuestro caso)
|
|
const max = 75;
|
|
if (line.length <= max) return line;
|
|
const parts: string[] = [];
|
|
let i = 0;
|
|
while (i < line.length) {
|
|
const chunk = line.slice(i, i + max);
|
|
parts.push(i === 0 ? chunk : ' ' + chunk);
|
|
i += max;
|
|
}
|
|
return parts.join('\r\n');
|
|
}
|
|
|
|
function padTaskId(id: number, width: number = 4): string {
|
|
const s = String(Math.max(0, Math.floor(id)));
|
|
if (s.length >= width) return s;
|
|
return '0'.repeat(width - s.length) + s;
|
|
}
|
|
|
|
function ymdToBasic(ymd: string): string {
|
|
// Espera YYYY-MM-DD
|
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd);
|
|
if (!m) return '';
|
|
return `${m[1]}${m[2]}${m[3]}`;
|
|
}
|
|
|
|
function addDays(ymd: string, days: number): string {
|
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd);
|
|
if (!m) return ymd;
|
|
const d = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));
|
|
d.setUTCDate(d.getUTCDate() + days);
|
|
const yyyy = String(d.getUTCFullYear()).padStart(4, '0');
|
|
const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
const dd = String(d.getUTCDate()).padStart(2, '0');
|
|
return `${yyyy}-${mm}-${dd}`;
|
|
}
|
|
|
|
export type IcsEvent = {
|
|
id: number;
|
|
description: string;
|
|
due_date: string; // YYYY-MM-DD
|
|
group_name?: string | null;
|
|
prefix?: string; // ej: "T" para [T0123]
|
|
};
|
|
|
|
export async function buildIcsCalendar(title: string, events: IcsEvent[]): Promise<{ body: string; etag: string }> {
|
|
const lines: string[] = [];
|
|
lines.push('BEGIN:VCALENDAR');
|
|
lines.push('VERSION:2.0');
|
|
lines.push('PRODID:-//TaskWhatsApp//Calendar//ES');
|
|
lines.push('CALSCALE:GREGORIAN');
|
|
lines.push('METHOD:PUBLISH');
|
|
lines.push(`X-WR-CALNAME:${escapeIcsText(title)}`);
|
|
lines.push('X-WR-TIMEZONE:UTC');
|
|
|
|
for (const ev of events) {
|
|
const idPad = padTaskId(ev.id);
|
|
const summary = `[${ev.prefix || 'T'}${idPad}] ${ev.description}`;
|
|
const dtStart = ymdToBasic(ev.due_date);
|
|
const dtEnd = ymdToBasic(addDays(ev.due_date, 1));
|
|
const uid = `task-${ev.id}@tw`;
|
|
|
|
lines.push('BEGIN:VEVENT');
|
|
lines.push(foldIcsLine(`UID:${uid}`));
|
|
lines.push(foldIcsLine(`SUMMARY:${escapeIcsText(summary)}`));
|
|
lines.push(`DTSTART;VALUE=DATE:${dtStart}`);
|
|
lines.push(`DTEND;VALUE=DATE:${dtEnd}`);
|
|
if (ev.group_name) {
|
|
lines.push(foldIcsLine(`CATEGORIES:${escapeIcsText(ev.group_name || '')}`));
|
|
}
|
|
lines.push('END:VEVENT');
|
|
}
|
|
|
|
lines.push('END:VCALENDAR');
|
|
|
|
const body = lines.join('\r\n') + '\r\n';
|
|
const etag = await sha256Hex(body);
|
|
return { body, etag: `W/"${etag}"` };
|
|
}
|