172 lines
7.6 KiB
TypeScript
172 lines
7.6 KiB
TypeScript
import { NotificationSettings } from '@/types/notifications';
|
|
|
|
export interface ScheduleArgs {
|
|
runAtEpoch: number; // seconds
|
|
method: 'POST' | 'GET';
|
|
url: string;
|
|
body?: any;
|
|
headers?: Record<string, string>;
|
|
}
|
|
|
|
// Schedule a backup-email specific test (uses backupEmail target)
|
|
export async function scheduleTestBackupNotification(settings: NotificationSettings): Promise<ScheduleResult> {
|
|
const scheduler = settings.scheduler || 'schedy';
|
|
if (scheduler !== 'schedy') throw new Error('Only Schedy is supported for tests');
|
|
const backup = (settings.backupEmail || '').trim();
|
|
if (!backup) throw new Error('Please set a backup email');
|
|
|
|
if (settings.provider !== 'ntfy') {
|
|
throw new Error('Backup test currently supports ntfy provider only');
|
|
}
|
|
|
|
const topic = (settings.ntfyTopic || '').trim();
|
|
const base = (settings.ntfyServer || process.env.NEXT_PUBLIC_NTFY_URL || '/ntfy').replace(/\/$/, '');
|
|
if (!base || !topic) throw new Error('ntfy server or topic missing');
|
|
|
|
const relUrl = `${base}/${encodeURIComponent(topic)}`;
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const runAtEpoch = now + 120; // 2 minutes
|
|
const primary = (settings.email || '').trim();
|
|
const prefix = `You are the backup contact for this position. Please contact the primary contact${primary ? ' ' + primary : ''} immediately. If they do not respond, please pay down the debt to avoid liquidation in a timely manner.`;
|
|
const body = `${prefix}\n\n(MortgageFi backup email test at ${new Date().toISOString()})`;
|
|
return schedySchedule((settings.schedyBaseUrl || ENV_SCHEDY).replace(/\/$/, ''), settings.schedyApiKey || '', {
|
|
runAtEpoch,
|
|
method: 'POST',
|
|
url: relUrl,
|
|
body,
|
|
headers: {
|
|
'Content-Type': 'text/plain',
|
|
'X-Email': backup,
|
|
},
|
|
});
|
|
}
|
|
|
|
export interface ScheduleResult { jobId: string; scheduledRunAtUtc: number }
|
|
|
|
// Resolve an absolute callback URL for schedulers that execute webhooks server-side
|
|
function toAbsoluteUrl(url: string): string {
|
|
// Already absolute
|
|
if (/^https?:\/\//i.test(url)) return url;
|
|
// If relative, prefer an explicit callback origin if provided
|
|
const explicit = (process.env.NEXT_PUBLIC_CALLBACK_ORIGIN || '').replace(/\/$/, '');
|
|
if (explicit) return `${explicit}${url.startsWith('/') ? '' : '/'}${url}`;
|
|
// Fallback to browser origin (client-side only). If unavailable, return as-is.
|
|
try {
|
|
if (typeof window !== 'undefined' && window.location?.origin) {
|
|
return `${window.location.origin}${url.startsWith('/') ? '' : '/'}${url}`;
|
|
}
|
|
} catch {}
|
|
return url;
|
|
}
|
|
|
|
// Schedy implementation
|
|
async function schedySchedule(baseUrl: string, apiKey: string, args: ScheduleArgs): Promise<ScheduleResult> {
|
|
const executeAt = new Date(args.runAtEpoch * 1000).toISOString(); // RFC3339 UTC
|
|
const targetUrl = toAbsoluteUrl(args.url);
|
|
const res = await fetch(`${baseUrl.replace(/\/$/, '')}/tasks`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
|
|
body: JSON.stringify({
|
|
execute_at: executeAt,
|
|
url: targetUrl,
|
|
headers: args.headers || {},
|
|
payload: args.body ?? undefined,
|
|
}),
|
|
});
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => '');
|
|
throw new Error(`Schedy schedule failed: ${res.status} ${text}`);
|
|
}
|
|
const json = await res.json().catch(() => ({} as any));
|
|
const id = json?.id || json?.task_id || `schedy_${Math.random().toString(36).slice(2)}`;
|
|
return { jobId: String(id), scheduledRunAtUtc: args.runAtEpoch };
|
|
}
|
|
|
|
async function schedyCancel(baseUrl: string, apiKey: string, jobId: string): Promise<void> {
|
|
await fetch(`${baseUrl.replace(/\/$/, '')}/tasks/${encodeURIComponent(jobId)}`, { method: 'DELETE', headers: { 'X-API-Key': apiKey } });
|
|
}
|
|
|
|
// --- Schedy management helpers ---
|
|
interface SchedyTask { id: string; url: string; execute_at?: string; headers?: Record<string,string>; payload?: any }
|
|
|
|
async function schedyList(baseUrl: string, apiKey: string): Promise<SchedyTask[]> {
|
|
const res = await fetch(`${baseUrl.replace(/\/$/, '')}/tasks`, { headers: { 'X-API-Key': apiKey } });
|
|
if (!res.ok) throw new Error(`Schedy list failed: ${res.status}`);
|
|
const json = await res.json().catch(() => []);
|
|
return Array.isArray(json) ? json as SchedyTask[] : [];
|
|
}
|
|
|
|
async function schedyDelete(baseUrl: string, apiKey: string, id: string): Promise<void> {
|
|
await fetch(`${baseUrl.replace(/\/$/, '')}/tasks/${encodeURIComponent(id)}`, { method: 'DELETE', headers: { 'X-API-Key': apiKey } });
|
|
}
|
|
|
|
// Compute absolute ntfy topic URL for comparison (matches what schedule uses)
|
|
function absoluteNtfyUrl(settings: NotificationSettings): string | null {
|
|
const topic = (settings.ntfyTopic || '').trim();
|
|
if (!topic) return null;
|
|
const base = (settings.ntfyServer || process.env.NEXT_PUBLIC_NTFY_URL || '/ntfy').replace(/\/$/, '');
|
|
const rel = `${base}/${encodeURIComponent(topic)}`;
|
|
return toAbsoluteUrl(rel);
|
|
}
|
|
|
|
export async function purgeNtfyTopicSchedules(settings: NotificationSettings): Promise<number> {
|
|
const scheduler = settings.scheduler || 'cronhost';
|
|
if (scheduler !== 'schedy') return 0; // only implemented for schedy
|
|
const base = (settings.schedyBaseUrl || ENV_SCHEDY).replace(/\/$/, '');
|
|
const key = settings.schedyApiKey || '';
|
|
if (!base || !key) throw new Error('Schedy base URL or API key missing');
|
|
const targetUrl = absoluteNtfyUrl(settings);
|
|
if (!targetUrl) return 0;
|
|
const tasks = await schedyList(base, key);
|
|
const victims = tasks.filter(t => t.url === targetUrl);
|
|
await Promise.allSettled(victims.map(v => schedyDelete(base, key, v.id)));
|
|
return victims.length;
|
|
}
|
|
|
|
const ENV_SCHEDY = (process.env.NEXT_PUBLIC_SCHEDY_URL || process.env.SCHEDY_URL || 'http://localhost:8080').replace(/\/$/, '');
|
|
|
|
export async function scheduleJob(settings: NotificationSettings, args: ScheduleArgs): Promise<ScheduleResult> {
|
|
const base = (settings.schedyBaseUrl || ENV_SCHEDY).replace(/\/$/, '');
|
|
const key = settings.schedyApiKey || '';
|
|
if (!base || !key) throw new Error('Schedy base URL or API key missing');
|
|
return schedySchedule(base, key, args);
|
|
}
|
|
|
|
export async function cancelScheduledJob(settings: NotificationSettings, jobId: string): Promise<void> {
|
|
const base = (settings.schedyBaseUrl || ENV_SCHEDY).replace(/\/$/, '');
|
|
const key = settings.schedyApiKey || '';
|
|
if (!base || !key || !jobId) return;
|
|
return schedyCancel(base, key, jobId);
|
|
}
|
|
|
|
// Schedule a quick test notification to validate configuration end-to-end.
|
|
export async function scheduleTestNotification(settings: NotificationSettings): Promise<ScheduleResult> {
|
|
const scheduler = settings.scheduler || 'schedy';
|
|
if (scheduler !== 'schedy') throw new Error('Only Schedy is supported for tests');
|
|
if (!settings.email) throw new Error('Please set an email (used to validate requirements)');
|
|
|
|
// Currently support ntfy provider for E2E test
|
|
if (settings.provider !== 'ntfy') {
|
|
throw new Error('Test alert currently supports ntfy provider only');
|
|
}
|
|
|
|
const topic = (settings.ntfyTopic || '').trim();
|
|
const base = (settings.ntfyServer || process.env.NEXT_PUBLIC_NTFY_URL || '/ntfy').replace(/\/$/, '');
|
|
if (!base || !topic) throw new Error('ntfy server or topic missing');
|
|
|
|
const relUrl = `${base}/${encodeURIComponent(topic)}`; // can be relative; toAbsoluteUrl will fix
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const runAtEpoch = now + 120; // 2 minutes from now
|
|
const body = `MortgageFi test alert at ${new Date().toISOString()}`;
|
|
return schedySchedule((settings.schedyBaseUrl || ENV_SCHEDY).replace(/\/$/, ''), settings.schedyApiKey || '', {
|
|
runAtEpoch,
|
|
method: 'POST',
|
|
url: relUrl,
|
|
body,
|
|
headers: {
|
|
'Content-Type': 'text/plain',
|
|
...(settings.email ? { 'X-Email': settings.email } : {}),
|
|
},
|
|
});
|
|
}
|