Add support for combined notifications
This commit is contained in:
@@ -4,7 +4,7 @@ import { Task } from "../types/task";
|
|||||||
export abstract class Backend<C extends BackendSettings> {
|
export abstract class Backend<C extends BackendSettings> {
|
||||||
public abstract readonly name: string;
|
public abstract readonly name: string;
|
||||||
protected abstract requiredFields: readonly (keyof C)[];
|
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 {
|
#validate(config: Partial<C>): C {
|
||||||
for (const field of this.requiredFields) {
|
for (const field of this.requiredFields) {
|
||||||
@@ -16,13 +16,27 @@ export abstract class Backend<C extends BackendSettings> {
|
|||||||
return config as C;
|
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;
|
const cfg = config?.backend?.[this.name] as Partial<C> | undefined;
|
||||||
|
|
||||||
if (cfg?.enable !== true) {
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,15 +28,7 @@ async function run(config: Config, db: TaskDatabase, tasks?: Task[]) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const task of tasks) {
|
for (const backend of backends) {
|
||||||
console.info(`Dispatching a notification: [${task.label}]`)
|
await backend.remind(config, tasks);
|
||||||
for (const backend of backends) {
|
|
||||||
backend.remind(config, task);
|
|
||||||
await snooze(1500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function snooze(ms: number) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,8 @@ import { jsMapper } from "../util/code";
|
|||||||
type Config = {
|
type Config = {
|
||||||
url: string;
|
url: string;
|
||||||
token: string;
|
token: string;
|
||||||
mapper?: string;
|
map?: string;
|
||||||
|
combineMap?: string;
|
||||||
topic?: string;
|
topic?: string;
|
||||||
} & BackendSettings;
|
} & BackendSettings;
|
||||||
|
|
||||||
@@ -25,28 +26,74 @@ export class NtfySH extends Backend<Config> {
|
|||||||
|
|
||||||
protected requiredFields = ['url', 'token'] as const;
|
protected requiredFields = ['url', 'token'] as const;
|
||||||
|
|
||||||
protected notify(config: Config, task: Task): void {
|
protected notify(config: Config, task: Task): void {
|
||||||
const token = enhancedStringConfig(config.token);
|
const context = {
|
||||||
const mapper = config.mapper
|
mapPriority,
|
||||||
? jsMapper<object, NtfyDTO>(config.mapper, { mapPriority })
|
template: defaultMapper
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapper = config.map
|
||||||
|
? jsMapper<object, NtfyDTO>(config.map, context)
|
||||||
: defaultMapper;
|
: defaultMapper;
|
||||||
|
|
||||||
const dto = mapper(task);
|
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> = {
|
const headers: Record<string, string> = {
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
};
|
};
|
||||||
|
|
||||||
buildHeaders(dto, headers);
|
buildHeaders(dto, headers);
|
||||||
|
|
||||||
fetch(`https://${config.url}/${config.topic || 'obsidian'}`, {
|
return fetch(`https://${config.url}/${config.topic || 'obsidian'}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: dto.text ?? "",
|
body: dto.text ?? "",
|
||||||
headers
|
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>) {
|
function buildHeaders(dto: NtfyDTO, headers: Record<string, string>) {
|
||||||
if (dto.title) {
|
if (dto.title) {
|
||||||
headers.Title = 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 {
|
function mapPriority(priority?: TaskPriority): string {
|
||||||
if (!priority) {
|
if (!priority) {
|
||||||
return 'default';
|
return 'default';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export function jsMapper<I, O>(code: string, context: Record<string, unknown>): (task: I) => O {
|
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));
|
return (task: I) => filter(task, ...Object.values(context));
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2023",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
|
|||||||
Reference in New Issue
Block a user