Improve mapping tasks to notifications

This commit is contained in:
2025-03-03 15:03:56 +01:00
parent b1c89efab9
commit 0ec71cfc8e
11 changed files with 115 additions and 82 deletions

View File

@@ -1,10 +1,10 @@
import { BackendSettings, Config } from "../types/config"; import { BackendSettings, Config } from "../types/config";
import { Notification } from "../types/notification"; 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, notification: Notification): 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,13 @@ export abstract class Backend<C extends BackendSettings> {
return config as C; return config as C;
} }
public remind(config: Config, notification: Notification) { public remind(config: Config, task: Task) {
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), notification); this.notify(this.#validate(cfg), task);
} }
} }

View File

@@ -1,5 +1,5 @@
import { BackendSettings } from "../types/config"; import { BackendSettings } from "../types/config";
import { Notification } from "../types/notification"; import { Task } from "../types/task";
import { Backend } from "./base"; import { Backend } from "./base";
type Config = BackendSettings; type Config = BackendSettings;
@@ -9,7 +9,7 @@ export class Debug extends Backend<Config> {
protected requiredFields = [] as const; protected requiredFields = [] as const;
protected notify(config: Config, notification: Notification): void { protected notify(config: Config, task: Task): void {
console.log(JSON.stringify(notification, undefined, 2)); console.log(JSON.stringify(task, undefined, 2));
} }
} }

View File

@@ -1,5 +1,5 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Notification, NotificationDatabase } from "../types/notification"; import { Task, TaskDatabase } from "../types/task";
import { Config } from "../types/config"; import { Config } from "../types/config";
import { NtfySH } from "./ntfy"; import { NtfySH } from "./ntfy";
import { Debug } from "./debug"; import { Debug } from "./debug";
@@ -13,7 +13,7 @@ const backends = [
* Iterates through all the database notifications for current time * Iterates through all the database notifications for current time
* and triggers the notification using specified backends in the config. * and triggers the notification using specified backends in the config.
*/ */
export async function remind(config: Config, db: NotificationDatabase) { export async function remind(config: Config, db: TaskDatabase) {
const now = dayjs().format("HH:mm"); const now = dayjs().format("HH:mm");
await run(config, db, db[now]); await run(config, db, db[now]);
@@ -23,15 +23,15 @@ export async function remind(config: Config, db: NotificationDatabase) {
} }
} }
async function run(config: Config, db: NotificationDatabase, notifications?: Notification[]) { async function run(config: Config, db: TaskDatabase, tasks?: Task[]) {
if(!notifications) { if(!tasks) {
return; return;
} }
for (const notification of notifications) { for (const task of tasks) {
console.info(`Dispatching a notification: [${notification.text}]`) console.info(`Dispatching a notification: [${task.label}]`)
for (const backend of backends) { for (const backend of backends) {
backend.remind(config, notification); backend.remind(config, task);
await snooze(1500); await snooze(1500);
} }
} }

View File

@@ -1,37 +1,79 @@
import fs from "fs"; import fs from "fs";
import { BackendSettings } from "../types/config"; import { BackendSettings } from "../types/config";
import { Notification } from "../types/notification";
import { Backend } from "./base"; import { Backend } from "./base";
import { TaskPriority } from "../types/task"; import { Task, TaskPriority } from "../types/task";
import { enhancedStringConfig } from "../util/config"; import { enhancedStringConfig } from "../util/config";
import { jsMapper } from "../util/code";
type Config = { type Config = {
url: string; url: string;
token: string; token: string;
mapper?: string;
topic?: string; topic?: string;
} & BackendSettings; } & BackendSettings;
type NtfyDTO = {
title?: string;
text?: string;
priority?: string;
tags?: string[];
icon?: string;
};
export class NtfySH extends Backend<Config> { export class NtfySH extends Backend<Config> {
public name = "ntfy"; public name = "ntfy";
protected requiredFields = ['url', 'token'] as const; protected requiredFields = ['url', 'token'] as const;
protected notify(config: Config, notification: Notification): void { protected notify(config: Config, task: Task): void {
const token = enhancedStringConfig(config.token); const token = enhancedStringConfig(config.token);
const mapper = config.mapper
? jsMapper<object, NtfyDTO>(config.mapper, { mapPriority })
: defaultMapper;
const dto = mapper(task);
const headers: Record<string, string> = {
Authorization: `Bearer ${token}`
};
buildHeaders(dto, headers);
fetch(`https://${config.url}/${config.topic || 'obsidian'}`, { fetch(`https://${config.url}/${config.topic || 'obsidian'}`, {
method: 'POST', method: 'POST',
body: notification.text, body: dto.text ?? "",
headers: { headers
'Authorization': `Bearer ${token}`,
'Title': notification.title ?? "",
'Priority': mapPriority(notification.priority),
'Tags': notification.tags?.join(",") ?? ""
}
}) })
} }
} }
function buildHeaders(dto: NtfyDTO, headers: Record<string, string>) {
if (dto.title) {
headers.Title = dto.title;
}
if (dto.priority) {
headers.Priority = dto.priority;
}
if (dto.tags) {
headers.Tags = dto.tags.join(",");
}
if (dto.icon) {
headers.Icon = dto.icon;
}
}
function defaultMapper(task: Task): NtfyDTO {
return {
title: "Obsidian Task Reminder",
text: task.label,
priority: mapPriority(task.priority),
tags: task.tags,
}
}
function mapPriority(priority?: TaskPriority): string { function mapPriority(priority?: TaskPriority): string {
if (!priority) { if (!priority) {
return 'default'; return 'default';

View File

@@ -1,10 +1,10 @@
import fs from "fs"; import fs from "fs";
import { NotificationDatabase } from "../types/notification"; import { TaskDatabase } from "../types/task";
/** /**
* Loads and deserializes database from JSON formatted file. * Loads and deserializes database from JSON formatted file.
*/ */
export function loadDatabase(file: string): NotificationDatabase { export function loadDatabase(file: string): TaskDatabase {
const text = fs.readFileSync(file).toString(); const text = fs.readFileSync(file).toString();
return deserializeDatabase(text); return deserializeDatabase(text);
} }
@@ -12,6 +12,6 @@ export function loadDatabase(file: string): NotificationDatabase {
/** /**
* Deserializes database from JSON format. * Deserializes database from JSON format.
*/ */
export function deserializeDatabase(json: string): NotificationDatabase { export function deserializeDatabase(json: string): TaskDatabase {
return JSON.parse(json) as NotificationDatabase; return JSON.parse(json) as TaskDatabase;
} }

View File

@@ -1,57 +1,35 @@
import fs from "fs"; import fs from "fs";
import { Task } from "../types/task"; import { Task, TaskDatabase } from "../types/task";
import { Notification, NotificationDatabase } from "../types/notification";
import { jsMapper } from "../util/code";
/** /**
* Applies the mapper for each task from list, groups them by time and dumps the data into JSON formatted file. * Applies the mapper for each task from list, groups them by time and dumps the data into JSON formatted file.
*/ */
export function dumpDatabase(file: string, tasks: Task[], mapper?: string) { export function dumpDatabase(file: string, tasks: Task[]) {
const data = serializeDatabase(tasks, mapper); const data = serializeDatabase(tasks);
fs.writeFileSync(file, data); fs.writeFileSync(file, data);
} }
/** /**
* Applies the mapper for each task from list, groups them by time and serializes into JSON format. * Applies the mapper for each task from list, groups them by time and serializes into JSON format.
*/ */
export function serializeDatabase(tasks: Task[], mapper?: string): string { export function serializeDatabase(tasks: Task[]): string {
return JSON.stringify(createDatabase(tasks, mapper));
const x = JSON.stringify(createDatabase(tasks));
console.log(x);
return x;
} }
/** /**
* Applies the mapper for each task from list and groups them by time. * Applies the mapper for each task from list and groups them by time.
*/ */
export function createDatabase(tasks: Task[], mapper?: string): NotificationDatabase { export function createDatabase(tasks: Task[]): TaskDatabase {
const transformer = mapper
? jsMapper<Task, Notification>(mapper, {})
: defaultMapper;
return tasks.map(wrapWithTimeFiller(transformer)).reduce((acc, n) => { return tasks.filter(t => t.reminder).reduce((acc, t) => {
if (n.time) { if (t.reminder) {
(acc[n.time] = (acc[n.time] || [])).push(n); (acc[t.reminder] = (acc[t.reminder] || [])).push(t);
}; };
return acc; return acc;
}, {} as NotificationDatabase); }, {} as TaskDatabase);
}
function wrapWithTimeFiller(mapper: (task: Task) => Notification): (task: Task) => Notification {
return (task: Task) => {
const notification = mapper(task);
return {
...notification,
time: task.reminder ?? notification.time,
}
};
}
function defaultMapper(task: Task): Notification {
return {
title: "Obsidian Tasks Reminder",
text: task.label,
priority: task.priority,
tags: task.tags,
};
} }

View File

@@ -149,4 +149,26 @@ export class LazyTask implements Task {
return `- [${this.status}] ${this.label} {${items.filter(x => x.length > 0).join(", ")}}`; return `- [${this.status}] ${this.label} {${items.filter(x => x.length > 0).join(", ")}}`;
} }
toJSON(): Task {
return {
status: this.status,
label: this.label,
fullLabel: this.fullLabel,
tags: this.tags,
priority: this.priority,
priorityStr: this.priorityStr,
createdDate: this.createdDate,
startDate: this.startDate,
scheduledDate: this.scheduledDate,
dueDate: this.dueDate,
completedDate: this.completedDate,
cancelledDate: this.cancelledDate,
recurrenceRule: this.recurrenceRule,
onDelete: this.onDelete,
id: this.id,
dependsOn: this.dependsOn,
reminder: this.reminder,
};
}
} }

View File

@@ -6,13 +6,13 @@ import { Config } from "../types/config";
export async function test(config: Config) { export async function test(config: Config) {
const tasks = await loadTasks(config.sources, config.query); const tasks = await loadTasks(config.sources, config.query);
const db = createDatabase(tasks, config.mapper); const db = createDatabase(tasks);
for (const time of Object.keys(db)) { for (const time of Object.keys(db)) {
console.log(time); console.log(time);
for (const notification of db[time]) { for (const task of db[time]) {
console.log(` - title: ${notification.title}\n text: ${notification.text}\n priority: ${notification.priority}\n tags: ${notification.tags?.join(",")}`) console.log(task.toString());
} }
console.log(); console.log();
@@ -21,7 +21,7 @@ export async function test(config: Config) {
export async function scan(config: Config) { export async function scan(config: Config) {
const tasks = await loadTasks(config.sources, config.query); const tasks = await loadTasks(config.sources, config.query);
dumpDatabase(config.databaseFile, tasks, config.mapper); dumpDatabase(config.databaseFile, tasks);
} }
export async function notify(config: Config) { export async function notify(config: Config) {

View File

@@ -1,7 +1,6 @@
export type Config = { export type Config = {
sources: string[]; sources: string[];
query?: string; query?: string;
mapper?: string;
defaultTime?: string; defaultTime?: string;
databaseFile: string; databaseFile: string;
backend: Record<string, unknown>; backend: Record<string, unknown>;

View File

@@ -1,11 +0,0 @@
import { TaskPriority } from "./task";
export type Notification = {
text: string;
time?: string;
title?: string;
priority?: TaskPriority;
tags?: string[];
};
export type NotificationDatabase = Record<string, Notification[]>;

View File

@@ -18,6 +18,7 @@ export type Task = {
id?: string; id?: string;
dependsOn?: string[]; dependsOn?: string[];
reminder?: string; reminder?: string;
} }
export enum TaskPriority { export enum TaskPriority {
@@ -28,3 +29,5 @@ export enum TaskPriority {
HIGH, HIGH,
HIGHEST, HIGHEST,
}; };
export type TaskDatabase = Record<string, Task[]>;