import webpush from 'web-push'; import getStorage from './storage'; import { getClientData, getToday } from './service'; import { getIsWeekend } from './utils'; import { LunchChoices } from '../../types'; const storage = getStorage(); const REGISTRY_KEY = 'push_reminder_registry'; interface RegistryEntry { time: string; subscription: webpush.PushSubscription; } type Registry = Record; /** Mapa login → datum (YYYY-MM-DD), kdy byl uživatel naposledy upozorněn. */ const remindedToday = new Map(); function getTodayDateString(): string { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; } function getCurrentTimeHHMM(): string { const now = new Date(); return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`; } /** Zjistí, zda má uživatel zvolenou nějakou možnost stravování. */ function userHasChoice(choices: LunchChoices, login: string): boolean { for (const locationKey of Object.keys(choices)) { const locationChoices = choices[locationKey as keyof LunchChoices]; if (locationChoices && login in locationChoices) { return true; } } return false; } async function getRegistry(): Promise { return await storage.getData(REGISTRY_KEY) ?? {}; } async function saveRegistry(registry: Registry): Promise { await storage.setData(REGISTRY_KEY, registry); } /** Přidá nebo aktualizuje push subscription pro uživatele. */ export async function subscribePush(login: string, subscription: webpush.PushSubscription, reminderTime: string): Promise { const registry = await getRegistry(); registry[login] = { time: reminderTime, subscription }; await saveRegistry(registry); console.log(`Push reminder: uživatel ${login} přihlášen k připomínkám v ${reminderTime}`); } /** Odebere push subscription pro uživatele. */ export async function unsubscribePush(login: string): Promise { const registry = await getRegistry(); delete registry[login]; await saveRegistry(registry); remindedToday.delete(login); console.log(`Push reminder: uživatel ${login} odhlášen z připomínek`); } /** Vrátí veřejný VAPID klíč. */ export function getVapidPublicKey(): string | undefined { return process.env.VAPID_PUBLIC_KEY; } /** Najde login uživatele podle push subscription endpointu. */ export async function findLoginByEndpoint(endpoint: string): Promise { const registry = await getRegistry(); for (const [login, entry] of Object.entries(registry)) { if (entry.subscription.endpoint === endpoint) { return login; } } return undefined; } /** Zkontroluje a odešle připomínky uživatelům, kteří si nezvolili oběd. */ async function checkAndSendReminders(): Promise { // Přeskočit víkendy if (getIsWeekend(getToday())) { return; } const registry = await getRegistry(); const entries = Object.entries(registry); if (entries.length === 0) { return; } const currentTime = getCurrentTimeHHMM(); const todayStr = getTodayDateString(); // Získáme data pro dnešek jednou pro všechny uživatele let clientData; try { clientData = await getClientData(getToday()); } catch (e) { console.error('Push reminder: chyba při získávání dat', e); return; } for (const [login, entry] of entries) { // Ještě nedosáhl čas připomínky if (currentTime < entry.time) { continue; } // Už jsme dnes připomenuli if (remindedToday.get(login) === todayStr) { continue; } // Uživatel už má zvolenou možnost if (clientData.choices && userHasChoice(clientData.choices, login)) { continue; } // Odešleme push notifikaci try { await webpush.sendNotification( entry.subscription, JSON.stringify({ title: 'Luncher', body: 'Ještě nemáte zvolený oběd!', }) ); remindedToday.set(login, todayStr); console.log(`Push reminder: odeslána připomínka uživateli ${login}`); } catch (error: any) { if (error.statusCode === 410 || error.statusCode === 404) { // Subscription expirovala nebo je neplatná — odebereme z registry console.log(`Push reminder: subscription uživatele ${login} expirovala, odebírám`); delete registry[login]; await saveRegistry(registry); } else { console.error(`Push reminder: chyba při odesílání notifikace uživateli ${login}:`, error); } } } } /** Spustí scheduler pro kontrolu a odesílání připomínek každou minutu. */ export function startReminderScheduler(): void { const publicKey = process.env.VAPID_PUBLIC_KEY; const privateKey = process.env.VAPID_PRIVATE_KEY; const subject = process.env.VAPID_SUBJECT; if (!publicKey || !privateKey || !subject) { console.log('Push reminder: VAPID klíče nejsou nastaveny, scheduler nebude spuštěn'); return; } webpush.setVapidDetails(subject, publicKey, privateKey); // Spustíme kontrolu každou minutu setInterval(checkAndSendReminders, 60_000); console.log('Push reminder: scheduler spuštěn'); }