diff --git a/src/backend/base.ts b/src/backend/base.ts index a0ae660..62bb029 100644 --- a/src/backend/base.ts +++ b/src/backend/base.ts @@ -4,7 +4,7 @@ import { Task } from "../types/task"; export abstract class Backend { public abstract readonly name: string; protected abstract requiredFields: readonly (keyof C)[]; - protected abstract notify(config: C, task: Task): void; + protected abstract notify(config: C, task: Task): void; #validate(config: Partial): C { for (const field of this.requiredFields) { @@ -16,13 +16,27 @@ export abstract class Backend { return config as C; } - public remind(config: Config, task: Task) { + public async remind(config: Config, tasks: Task[]): Promise { const cfg = config?.backend?.[this.name] as Partial | undefined; if (cfg?.enable !== true) { - return + return; } - this.notify(this.#validate(cfg), task); + if (tasks.length === 0) { + return; + } + + if (tasks.length === 1) { + return await this.notify(this.#validate(cfg), tasks[0]); + } + + return await this.notifyCombined(this.#validate(cfg), tasks); + } + + protected async notifyCombined(config: C, tasks: Task[]) { + for (const task of tasks) { + this.notify(config, task); + } } } \ No newline at end of file diff --git a/src/backend/index.ts b/src/backend/index.ts index 4a03471..17aa8ab 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -28,15 +28,7 @@ async function run(config: Config, db: TaskDatabase, tasks?: Task[]) { return; } - for (const task of tasks) { - console.info(`Dispatching a notification: [${task.label}]`) - for (const backend of backends) { - backend.remind(config, task); - await snooze(1500); - } + for (const backend of backends) { + await backend.remind(config, tasks); } -} - -function snooze(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); } \ No newline at end of file diff --git a/src/backend/ntfy.ts b/src/backend/ntfy.ts index bc0508b..f1d6526 100644 --- a/src/backend/ntfy.ts +++ b/src/backend/ntfy.ts @@ -8,7 +8,8 @@ import { jsMapper } from "../util/code"; type Config = { url: string; token: string; - mapper?: string; + map?: string; + combineMap?: string; topic?: string; } & BackendSettings; @@ -25,28 +26,74 @@ export class NtfySH extends Backend { protected requiredFields = ['url', 'token'] as const; - protected notify(config: Config, task: Task): void { - const token = enhancedStringConfig(config.token); - const mapper = config.mapper - ? jsMapper(config.mapper, { mapPriority }) + protected notify(config: Config, task: Task): void { + const context = { + mapPriority, + template: defaultMapper + }; + + const mapper = config.map + ? jsMapper(config.map, context) : defaultMapper; const dto = mapper(task); + this.#doNotify(config, dto); + } + + protected async notifyCombined(config: Config, tasks: Task[]): Promise { + const chunks = chunkArray(tasks, 4); + + const context = { + mapPriority, + template: defaultCombineMapper + }; + + for (const chunk of chunks) { + const mapper = config.combineMap + ? jsMapper(config.combineMap, context) + : defaultCombineMapper; + + const dto = mapper(chunk); + + await this.#doNotify(config, dto); + + await snooze(2500); + } + } + + async #doNotify(config: Config, dto: NtfyDTO): Promise { + const token = enhancedStringConfig(config.token); + const headers: Record = { Authorization: `Bearer ${token}` }; buildHeaders(dto, headers); - fetch(`https://${config.url}/${config.topic || 'obsidian'}`, { + return fetch(`https://${config.url}/${config.topic || 'obsidian'}`, { method: 'POST', body: dto.text ?? "", headers - }) + }) } } +function snooze(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function chunkArray(array: T[], size: number): T[][] { + const chunks = []; + + while (array.length) { + chunks.push(array.slice(0, size)); + array = array.slice(size); + } + + return chunks; +} + function buildHeaders(dto: NtfyDTO, headers: Record) { if (dto.title) { headers.Title = dto.title; @@ -74,6 +121,23 @@ function defaultMapper(task: Task): NtfyDTO { } } +function defaultCombineMapper(tasks: Task[], bullet = "-"): NtfyDTO { + const text = tasks + .toSorted((a, b) => b.priority - a.priority) + .map(t => `${bullet} ${t.label}`) + .join("\n"); + + const priority = mapPriority(Math.max(...tasks.map(t => t.priority))); + const tags = tasks.flatMap(t => t.tags); + + return { + title: `Obsidian Task Reminder (${tasks.length})`, + text, + priority, + tags + } +} + function mapPriority(priority?: TaskPriority): string { if (!priority) { return 'default'; diff --git a/src/util/code.ts b/src/util/code.ts index f6b4eee..1d80745 100644 --- a/src/util/code.ts +++ b/src/util/code.ts @@ -1,4 +1,4 @@ export function jsMapper(code: string, context: Record): (task: I) => O { - const filter = new Function('$', ...Object.keys(context), `return ${code};`); + const filter = new Function('$', ...Object.keys(context), code); return (task: I) => filter(task, ...Object.values(context)); } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 6124404..67b94cb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ES2023", "module": "commonjs", "outDir": "./dist", "rootDir": "./src",