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 { Notification } from "../types/notification";
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, notification: Notification): void;
protected abstract notify(config: C, task: Task): void;
#validate(config: Partial<C>): C {
for (const field of this.requiredFields) {
@@ -16,13 +16,13 @@ export abstract class Backend<C extends BackendSettings> {
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;
if (cfg?.enable !== true) {
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 { Notification } from "../types/notification";
import { Task } from "../types/task";
import { Backend } from "./base";
type Config = BackendSettings;
@@ -9,7 +9,7 @@ export class Debug extends Backend<Config> {
protected requiredFields = [] as const;
protected notify(config: Config, notification: Notification): void {
console.log(JSON.stringify(notification, undefined, 2));
protected notify(config: Config, task: Task): void {
console.log(JSON.stringify(task, undefined, 2));
}
}

View File

@@ -1,5 +1,5 @@
import dayjs from "dayjs";
import { Notification, NotificationDatabase } from "../types/notification";
import { Task, TaskDatabase } from "../types/task";
import { Config } from "../types/config";
import { NtfySH } from "./ntfy";
import { Debug } from "./debug";
@@ -13,7 +13,7 @@ const backends = [
* Iterates through all the database notifications for current time
* 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");
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[]) {
if(!notifications) {
async function run(config: Config, db: TaskDatabase, tasks?: Task[]) {
if(!tasks) {
return;
}
for (const notification of notifications) {
console.info(`Dispatching a notification: [${notification.text}]`)
for (const task of tasks) {
console.info(`Dispatching a notification: [${task.label}]`)
for (const backend of backends) {
backend.remind(config, notification);
backend.remind(config, task);
await snooze(1500);
}
}

View File

@@ -1,37 +1,79 @@
import fs from "fs";
import { BackendSettings } from "../types/config";
import { Notification } from "../types/notification";
import { Backend } from "./base";
import { TaskPriority } from "../types/task";
import { Task, TaskPriority } from "../types/task";
import { enhancedStringConfig } from "../util/config";
import { jsMapper } from "../util/code";
type Config = {
url: string;
token: string;
mapper?: string;
topic?: string;
} & BackendSettings;
type NtfyDTO = {
title?: string;
text?: string;
priority?: string;
tags?: string[];
icon?: string;
};
export class NtfySH extends Backend<Config> {
public name = "ntfy";
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 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'}`, {
method: 'POST',
body: notification.text,
headers: {
'Authorization': `Bearer ${token}`,
'Title': notification.title ?? "",
'Priority': mapPriority(notification.priority),
'Tags': notification.tags?.join(",") ?? ""
}
body: dto.text ?? "",
headers
})
}
}
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 {
if (!priority) {
return 'default';

View File

@@ -1,10 +1,10 @@
import fs from "fs";
import { NotificationDatabase } from "../types/notification";
import { TaskDatabase } from "../types/task";
/**
* 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();
return deserializeDatabase(text);
}
@@ -12,6 +12,6 @@ export function loadDatabase(file: string): NotificationDatabase {
/**
* Deserializes database from JSON format.
*/
export function deserializeDatabase(json: string): NotificationDatabase {
return JSON.parse(json) as NotificationDatabase;
export function deserializeDatabase(json: string): TaskDatabase {
return JSON.parse(json) as TaskDatabase;
}

View File

@@ -1,57 +1,35 @@
import fs from "fs";
import { Task } from "../types/task";
import { Notification, NotificationDatabase } from "../types/notification";
import { jsMapper } from "../util/code";
import { Task, TaskDatabase } from "../types/task";
/**
* 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) {
const data = serializeDatabase(tasks, mapper);
export function dumpDatabase(file: string, tasks: Task[]) {
const data = serializeDatabase(tasks);
fs.writeFileSync(file, data);
}
/**
* 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 {
return JSON.stringify(createDatabase(tasks, mapper));
export function serializeDatabase(tasks: Task[]): string {
const x = JSON.stringify(createDatabase(tasks));
console.log(x);
return x;
}
/**
* Applies the mapper for each task from list and groups them by time.
*/
export function createDatabase(tasks: Task[], mapper?: string): NotificationDatabase {
const transformer = mapper
? jsMapper<Task, Notification>(mapper, {})
: defaultMapper;
export function createDatabase(tasks: Task[]): TaskDatabase {
return tasks.map(wrapWithTimeFiller(transformer)).reduce((acc, n) => {
if (n.time) {
(acc[n.time] = (acc[n.time] || [])).push(n);
return tasks.filter(t => t.reminder).reduce((acc, t) => {
if (t.reminder) {
(acc[t.reminder] = (acc[t.reminder] || [])).push(t);
};
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(", ")}}`;
}
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) {
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)) {
console.log(time);
for (const notification of db[time]) {
console.log(` - title: ${notification.title}\n text: ${notification.text}\n priority: ${notification.priority}\n tags: ${notification.tags?.join(",")}`)
for (const task of db[time]) {
console.log(task.toString());
}
console.log();
@@ -21,7 +21,7 @@ export async function test(config: Config) {
export async function scan(config: Config) {
const tasks = await loadTasks(config.sources, config.query);
dumpDatabase(config.databaseFile, tasks, config.mapper);
dumpDatabase(config.databaseFile, tasks);
}
export async function notify(config: Config) {

View File

@@ -1,7 +1,6 @@
export type Config = {
sources: string[];
query?: string;
mapper?: string;
query?: string;
defaultTime?: string;
databaseFile: string;
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

@@ -17,7 +17,8 @@ export type Task = {
onDelete?: string;
id?: string;
dependsOn?: string[];
reminder?: string;
reminder?: string;
}
export enum TaskPriority {
@@ -27,4 +28,6 @@ export enum TaskPriority {
MEDIUM,
HIGH,
HIGHEST,
};
};
export type TaskDatabase = Record<string, Task[]>;