Add support for combined notifications

This commit is contained in:
2025-03-05 14:08:38 +01:00
parent 0ec71cfc8e
commit c321758101
5 changed files with 93 additions and 23 deletions

View File

@@ -4,7 +4,7 @@ import { Task } from "../types/task";
export abstract class Backend<C extends BackendSettings> {
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>): C {
for (const field of this.requiredFields) {
@@ -16,13 +16,27 @@ export abstract class Backend<C extends BackendSettings> {
return config as C;
}
public remind(config: Config, task: Task) {
public async remind(config: Config, tasks: Task[]): Promise<void> {
const cfg = config?.backend?.[this.name] as Partial<C> | 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);
}
}
}

View File

@@ -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));
}

View File

@@ -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<Config> {
protected requiredFields = ['url', 'token'] as const;
protected notify(config: Config, task: Task): void {
const token = enhancedStringConfig(config.token);
const mapper = config.mapper
? jsMapper<object, NtfyDTO>(config.mapper, { mapPriority })
protected notify(config: Config, task: Task): void {
const context = {
mapPriority,
template: defaultMapper
};
const mapper = config.map
? jsMapper<object, NtfyDTO>(config.map, context)
: defaultMapper;
const dto = mapper(task);
this.#doNotify(config, dto);
}
protected async notifyCombined(config: Config, tasks: Task[]): Promise<void> {
const chunks = chunkArray(tasks, 4);
const context = {
mapPriority,
template: defaultCombineMapper
};
for (const chunk of chunks) {
const mapper = config.combineMap
? jsMapper<object, NtfyDTO>(config.combineMap, context)
: defaultCombineMapper;
const dto = mapper(chunk);
await this.#doNotify(config, dto);
await snooze(2500);
}
}
async #doNotify(config: Config, dto: NtfyDTO): Promise<Response> {
const token = enhancedStringConfig(config.token);
const headers: Record<string, string> = {
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<T>(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<string, string>) {
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';

View File

@@ -1,4 +1,4 @@
export function jsMapper<I, O>(code: string, context: Record<string, unknown>): (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));
}

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "ES2020",
"target": "ES2023",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",