Compare commits
2 Commits
67db439f84
...
a270ee4ae5
| Author | SHA1 | Date | |
|---|---|---|---|
|
a270ee4ae5
|
|||
|
f3b68dca33
|
35
package-lock.json
generated
35
package-lock.json
generated
@@ -9,8 +9,10 @@
|
||||
"version": "0.0.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"commander": "^13.0.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"peggy": "^4.2.0"
|
||||
"peggy": "^4.2.0",
|
||||
"yaml": "^2.7.0"
|
||||
},
|
||||
"bin": {
|
||||
"obsidian-tasks-reminder": "dist/index.js"
|
||||
@@ -566,13 +568,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||
"dev": true,
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz",
|
||||
"integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
@@ -1053,6 +1054,16 @@
|
||||
"tsc-alias": "dist/bin/index.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tsc-alias/node_modules/commander": {
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz",
|
||||
@@ -1093,6 +1104,18 @@
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
|
||||
"integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^13.0.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"peggy": "^4.2.0"
|
||||
"peggy": "^4.2.0",
|
||||
"yaml": "^2.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@ buildNpmPackage {
|
||||
pname = "obsidian-tasks-reminder";
|
||||
version = "0.0.1";
|
||||
src = ./.;
|
||||
npmDepsHash = "sha256-d8uZWYmroWoju976WXnCaYX+0uTLK/tc6hS/WgEHv/o=";
|
||||
npmDepsHash = "sha256-ofPAFnHbW+M0uQN/OXDLK+PQ3ls6AtD58/AOmSxn754=";
|
||||
}
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
import { BackendConfig } from "../types/config";
|
||||
import { BackendSettings, Config } from "../types/config";
|
||||
import { Notification } from "../types/notification";
|
||||
|
||||
export abstract class Backend {
|
||||
constructor(config: BackendConfig) {
|
||||
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;
|
||||
|
||||
#validate(config: Partial<C>): C {
|
||||
for (const field of this.requiredFields) {
|
||||
if (config[field] === undefined) {
|
||||
throw new Error(`The '${String(field)}' configuration field of'${this.name}' consumer is required`)
|
||||
}
|
||||
}
|
||||
|
||||
return config as C;
|
||||
}
|
||||
|
||||
abstract notify(notification: Notification): void;
|
||||
public remind(config: Config, notification: Notification) {
|
||||
const cfg = config.backend[this.name] as Partial<C>;
|
||||
|
||||
if (cfg.enable !== true) {
|
||||
return
|
||||
}
|
||||
|
||||
this.notify(this.#validate(cfg), notification);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,31 @@
|
||||
import dayjs from "dayjs";
|
||||
import { NotificationDatabase } from "../types/notification";
|
||||
import { Config } from "../types/config";
|
||||
import { NtfySH } from "./ntfy-sh";
|
||||
|
||||
export { Backend } from "./base";
|
||||
export { NtfySH } from "./ntfy-sh";
|
||||
|
||||
const backends = [
|
||||
new NtfySH()
|
||||
];
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const now = dayjs().format("HH:mm");
|
||||
const notifications = db[now] ?? [];
|
||||
|
||||
for (const notification of notifications) {
|
||||
for (const backend of backends) {
|
||||
backend.remind(config, notification);
|
||||
await snooze(1500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function snooze(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -1,27 +1,25 @@
|
||||
import { BackendSettings } from "../types/config";
|
||||
import { Notification } from "../types/notification";
|
||||
import { Backend } from "./base";
|
||||
|
||||
type Config = {
|
||||
url: string;
|
||||
token: string;
|
||||
topic: string;
|
||||
}
|
||||
topic?: string;
|
||||
} & BackendSettings;
|
||||
|
||||
export class NtfySH extends Backend {
|
||||
#config: Config;
|
||||
export class NtfySH extends Backend<Config> {
|
||||
public name = "ntfy.sh";
|
||||
|
||||
constructor(config: Config) {
|
||||
super(config);
|
||||
this.#config = config;
|
||||
}
|
||||
|
||||
notify(notification: Notification): void {
|
||||
fetch(`https://${this.#config.url}/${this.#config.topic}`, {
|
||||
protected requiredFields = ['url', 'token'] as const;
|
||||
|
||||
protected notify(config: Config, notification: Notification): void {
|
||||
fetch(`https://${config.url}/${config.topic || 'obsidian'}`, {
|
||||
method: 'POST',
|
||||
body: notification.text,
|
||||
headers: {
|
||||
'Title': notification.title,
|
||||
'Authorization': `Bearer ${this.#config.token}`
|
||||
'Authorization': `Bearer ${config.token}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
47
src/cli/index.ts
Normal file
47
src/cli/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { program } from "commander";
|
||||
import { CLIOptions } from "../types/cli";
|
||||
|
||||
import { loadConfig } from "../config";
|
||||
import { notify, scan } from "../runner";
|
||||
|
||||
const getOptions = () => program
|
||||
.name("obsidian-tasks-reminder")
|
||||
.version("0.0.1")
|
||||
.requiredOption("-c, --config <file>", "sets the path to the YAML file with configuration")
|
||||
.option("-x, --set <arg>", "overrides the config option for this specific run (arg: <key>=<name>, i.e. backend.ntfy.enable=false", (v: string, prev: string[]) => prev.concat([v]), [])
|
||||
.option("-s, --scan", "scans new tasks for future notifications and generates the database")
|
||||
.option("-n, --notify", "reads the generated database and triggers notifications if any")
|
||||
.parse()
|
||||
.opts<CLIOptions>();
|
||||
|
||||
export const run = async () => {
|
||||
const options = getOptions();
|
||||
const config = loadConfig(options.config);
|
||||
|
||||
for (const override of options.set) {
|
||||
const [path, value] = override.split("=");
|
||||
|
||||
const segments = path.trim().split(".")
|
||||
|
||||
let current: any = config;
|
||||
segments.map(s => s.trim()).forEach((segment: string, idx) => {
|
||||
if(!current[segment]) {
|
||||
current[segment] = {};
|
||||
}
|
||||
|
||||
if(idx === segments.length - 1) {
|
||||
current[segment] = JSON.parse(value.trim());
|
||||
}
|
||||
|
||||
current = current[segment];
|
||||
});
|
||||
}
|
||||
|
||||
if (options.scan) {
|
||||
scan(config);
|
||||
}
|
||||
|
||||
if (options.notify) {
|
||||
notify(config);
|
||||
}
|
||||
}
|
||||
8
src/config/index.ts
Normal file
8
src/config/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import fs from "fs";
|
||||
import yaml from "yaml";
|
||||
import { Config } from "../types/config";
|
||||
|
||||
export function loadConfig(file: string): Config {
|
||||
const text = fs.readFileSync(file, 'utf-8');
|
||||
return yaml.parse(text) as Config;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { Notification, NotificationDatabase } from "../types/notification";
|
||||
/**
|
||||
* 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: (task: Task) => Notification[]) {
|
||||
export function dumpDatabase(file: string, tasks: Task[], mapper: string) {
|
||||
const data = serializeDatabase(tasks, mapper);
|
||||
fs.writeFileSync(file, data);
|
||||
}
|
||||
@@ -13,8 +13,10 @@ export function dumpDatabase(file: string, tasks: Task[], mapper: (task: Task) =
|
||||
/**
|
||||
* Applies the mapper for each task from list, groups them by time and serializes into JSON format.
|
||||
*/
|
||||
export function serializeDatabase(tasks: Task[], mapper: (task: Task) => Notification[]): string {
|
||||
const output = tasks.flatMap(wrapWithTimeFiller(mapper)).reduce((acc, n) => {
|
||||
export function serializeDatabase(tasks: Task[], mapper: string): string {
|
||||
const transformer = new Function("$", `return ${mapper}`) as (task: Task) => Notification;
|
||||
|
||||
const output = tasks.map(wrapWithTimeFiller(transformer)).reduce((acc, n) => {
|
||||
if (n.time) {
|
||||
(acc[n.time] = (acc[n.time] || [])).push(n);
|
||||
};
|
||||
@@ -25,10 +27,13 @@ export function serializeDatabase(tasks: Task[], mapper: (task: Task) => Notific
|
||||
return JSON.stringify(output);
|
||||
}
|
||||
|
||||
function wrapWithTimeFiller(mapper: (task: Task) => Notification[]): (task: Task) => Notification[] {
|
||||
return (task: Task) => mapper(task)
|
||||
.map(notification => ({
|
||||
function wrapWithTimeFiller(mapper: (task: Task) => Notification): (task: Task) => Notification {
|
||||
return (task: Task) => {
|
||||
const notification = mapper(task);
|
||||
|
||||
return {
|
||||
...notification,
|
||||
time: task.reminder ?? notification.time,
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { run } from "./cli";
|
||||
|
||||
run();
|
||||
@@ -1,21 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
import { NotificationDatabase } from "../types/notification";
|
||||
import { Backend } from "../backend";
|
||||
|
||||
/**
|
||||
* Iterates through all the database notifications for current time
|
||||
* and triggers the notification using specified backend.
|
||||
*/
|
||||
export async function remind(db: NotificationDatabase, backend: Backend) {
|
||||
const now = dayjs().format("HH:mm");
|
||||
const notifications = db[now] ?? [];
|
||||
|
||||
for (const notification of notifications) {
|
||||
backend.notify(notification);
|
||||
await snooze(1500);
|
||||
}
|
||||
}
|
||||
|
||||
function snooze(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
15
src/runner/index.ts
Normal file
15
src/runner/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { loadTasks } from "../loader";
|
||||
import { dumpDatabase } from "../database/serializer";
|
||||
import { loadDatabase } from "../database/deserializer";
|
||||
import { remind } from "../backend";
|
||||
import { Config } from "../types/config";
|
||||
|
||||
export async function scan(config: Config) {
|
||||
const tasks = await loadTasks(config.sources, config.query);
|
||||
dumpDatabase(config.databaseFile, tasks, config.mapper);
|
||||
}
|
||||
|
||||
export async function notify(config: Config) {
|
||||
const db = loadDatabase(config.databaseFile);
|
||||
remind(config, db);
|
||||
}
|
||||
6
src/types/cli.ts
Normal file
6
src/types/cli.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type CLIOptions = {
|
||||
config: string;
|
||||
scan: boolean;
|
||||
notify: boolean;
|
||||
set: string[];
|
||||
};
|
||||
@@ -1 +1,17 @@
|
||||
export type BackendConfig = Record<string, unknown>;
|
||||
export type Config = {
|
||||
sources: string[];
|
||||
query: string;
|
||||
mapper: string;
|
||||
databaseFile: string;
|
||||
backend: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type BackendSettings = {
|
||||
enable?: boolean;
|
||||
};
|
||||
|
||||
export type SupportedBackends = 'ntfy.sh';
|
||||
export type BackendConfig = {
|
||||
backend: SupportedBackends;
|
||||
settings: BackendSettings;
|
||||
}
|
||||
Reference in New Issue
Block a user