Add basic support for task completion
This commit is contained in:
891
package-lock.json
generated
891
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,9 +20,11 @@
|
|||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/express": "^5.0.2",
|
||||||
"commander": "^13.0.0",
|
"commander": "^13.0.0",
|
||||||
"cron": "^4.3.1",
|
"cron": "^4.3.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"express": "^5.1.0",
|
||||||
"peggy": "^4.2.0",
|
"peggy": "^4.2.0",
|
||||||
"yaml": "^2.7.0"
|
"yaml": "^2.7.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ buildNpmPackage {
|
|||||||
pname = "obsidian-tasks-reminder";
|
pname = "obsidian-tasks-reminder";
|
||||||
version = "0.0.1";
|
version = "0.0.1";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
npmDepsHash = "sha256-K06A/j2GfM0nCiFv4Pho907VWLa2pPHjtV9BgKxwdnE=";
|
npmDepsHash = "sha256-5BsZ4Z7/YjAnOLc6IBU+O0T1p0ncDd7kwzerI5PJYQM=";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { BackendSettings, ProfileConfig } from "../types/config";
|
import { BackendSettings, Config, ProfileConfig } from "../types/config";
|
||||||
import { Task } from "../types/task";
|
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: Config, profileConfig: ProfileConfig, backendConfig: C, task: Task): void;
|
||||||
|
|
||||||
#validate(config: Partial<C>): C {
|
#validate(backendConfig: Partial<C>): C {
|
||||||
for (const field of this.requiredFields) {
|
for (const field of this.requiredFields) {
|
||||||
if (config[field] === undefined) {
|
if (backendConfig[field] === undefined) {
|
||||||
throw new Error(`The '${String(field)}' configuration field of'${this.name}' consumer is required`)
|
throw new Error(`The '${String(field)}' configuration field of'${this.name}' consumer is required`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config as C;
|
return backendConfig as C;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remind(config: ProfileConfig, tasks: Task[]): Promise<void> {
|
public async remind(config: Config, profileConfig: ProfileConfig, tasks: Task[]): Promise<void> {
|
||||||
const cfg = config?.backend?.[this.name] as Partial<C> | undefined;
|
const cfg = profileConfig?.backend?.[this.name] as Partial<C> | undefined;
|
||||||
|
|
||||||
if (cfg?.enable !== true) {
|
if (cfg?.enable !== true) {
|
||||||
return;
|
return;
|
||||||
@@ -28,15 +28,15 @@ export abstract class Backend<C extends BackendSettings> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tasks.length === 1) {
|
if (tasks.length === 1) {
|
||||||
return await this.notify(this.#validate(cfg), tasks[0]);
|
return await this.notify(config, profileConfig, this.#validate(cfg), tasks[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.notifyCombined(this.#validate(cfg), tasks);
|
return await this.notifyCombined(config, profileConfig, this.#validate(cfg), tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async notifyCombined(config: C, tasks: Task[]) {
|
protected async notifyCombined(config: Config, profileConfig: ProfileConfig, backendConfig: C, tasks: Task[]) {
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
this.notify(config, task);
|
this.notify(config, profileConfig, backendConfig, task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
import { BackendSettings } from "../types/config";
|
import { BackendSettings, Config, ProfileConfig } from "../types/config";
|
||||||
import { Task } from "../types/task";
|
import { Task } from "../types/task";
|
||||||
import { Backend } from "./base";
|
import { Backend } from "./base";
|
||||||
|
|
||||||
type Config = BackendSettings;
|
export class Debug extends Backend<BackendSettings> {
|
||||||
|
|
||||||
export class Debug extends Backend<Config> {
|
|
||||||
public name = "debug";
|
public name = "debug";
|
||||||
|
|
||||||
protected requiredFields = [] as const;
|
protected requiredFields = [] as const;
|
||||||
|
|
||||||
protected notify(config: Config, task: Task): void {
|
protected notify(config: Config, profileConfig: ProfileConfig, backendConfig: BackendSettings, task: Task): void {
|
||||||
console.log(JSON.stringify(task, undefined, 2));
|
console.log(JSON.stringify(task, undefined, 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { Task, TaskDatabase } from "../types/task";
|
import { Task, TaskDatabase } from "../types/task";
|
||||||
import { ProfileConfig } from "../types/config";
|
import { Config, ProfileConfig } from "../types/config";
|
||||||
import { NtfySH } from "./ntfy";
|
import { NtfySH } from "./ntfy";
|
||||||
import { Debug } from "./debug";
|
import { Debug } from "./debug";
|
||||||
|
|
||||||
@@ -13,22 +13,22 @@ 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: ProfileConfig, db: TaskDatabase) {
|
export async function remind(config: Config, profileConfig: ProfileConfig, db: TaskDatabase) {
|
||||||
const now = dayjs().format("HH:mm");
|
const now = dayjs().format("HH:mm");
|
||||||
|
|
||||||
await run(config, db, db[now]);
|
await run(config, profileConfig, db, db[now]);
|
||||||
|
|
||||||
if(config?.defaultTime && config?.defaultTime === now) {
|
if(profileConfig?.defaultTime && profileConfig?.defaultTime === now) {
|
||||||
run(config, db, db.default);
|
run(config, profileConfig, db, db.default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function run(config: ProfileConfig, db: TaskDatabase, tasks?: Task[]) {
|
async function run(config: Config, profileConfig: ProfileConfig, db: TaskDatabase, tasks?: Task[]) {
|
||||||
if(!tasks) {
|
if(!tasks) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const backend of backends) {
|
for (const backend of backends) {
|
||||||
await backend.remind(config, tasks);
|
await backend.remind(config, profileConfig, tasks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { BackendSettings } from "../types/config";
|
import { BackendSettings, Config, ProfileConfig } from "../types/config";
|
||||||
|
import { CompleteTaskDTO } from "../types/dto";
|
||||||
import { Backend } from "./base";
|
import { Backend } from "./base";
|
||||||
import { Task, 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";
|
import { jsMapper } from "../util/code";
|
||||||
|
|
||||||
type Config = {
|
type NtfyConfig = {
|
||||||
url: string;
|
url: string;
|
||||||
token: string;
|
token: string;
|
||||||
map?: string;
|
map?: string;
|
||||||
@@ -21,27 +22,27 @@ type NtfyDTO = {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class NtfySH extends Backend<Config> {
|
export class NtfySH extends Backend<NtfyConfig> {
|
||||||
public name = "ntfy";
|
public name = "ntfy";
|
||||||
|
|
||||||
protected requiredFields = ['url', 'token'] as const;
|
protected requiredFields = ['url', 'token'] as const;
|
||||||
|
|
||||||
protected notify(config: Config, task: Task): void {
|
protected notify(config: Config, profileConfig: ProfileConfig, backendConfig: NtfyConfig, task: Task): void {
|
||||||
const context = {
|
const context = {
|
||||||
mapPriority,
|
mapPriority,
|
||||||
template: defaultMapper
|
template: defaultMapper
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapper = config.map
|
const mapper = backendConfig.map
|
||||||
? jsMapper<object, NtfyDTO>(config.map, context)
|
? jsMapper<object, NtfyDTO>(backendConfig.map, context)
|
||||||
: defaultMapper;
|
: defaultMapper;
|
||||||
|
|
||||||
const dto = mapper(task);
|
const dto = mapper(task);
|
||||||
|
|
||||||
this.#doNotify(config, dto);
|
this.#doNotify(config, profileConfig, backendConfig, dto, task);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async notifyCombined(config: Config, tasks: Task[]): Promise<void> {
|
protected async notifyCombined(config: Config, profileConfig: ProfileConfig, backendConfig: NtfyConfig, tasks: Task[]): Promise<void> {
|
||||||
const chunks = chunkArray(tasks, 4);
|
const chunks = chunkArray(tasks, 4);
|
||||||
|
|
||||||
const context = {
|
const context = {
|
||||||
@@ -50,31 +51,55 @@ export class NtfySH extends Backend<Config> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (const chunk of chunks) {
|
for (const chunk of chunks) {
|
||||||
const mapper = config.combineMap
|
const mapper = backendConfig.combineMap
|
||||||
? jsMapper<object, NtfyDTO>(config.combineMap, context)
|
? jsMapper<object, NtfyDTO>(backendConfig.combineMap, context)
|
||||||
: defaultCombineMapper;
|
: defaultCombineMapper;
|
||||||
|
|
||||||
const dto = mapper(chunk);
|
const dto = mapper(chunk);
|
||||||
|
|
||||||
await this.#doNotify(config, dto);
|
await this.#doNotify(config, profileConfig, backendConfig, dto);
|
||||||
|
|
||||||
await snooze(2500);
|
await snooze(2500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #doNotify(config: Config, dto: NtfyDTO): Promise<Response> {
|
async #doNotify(config: Config, profileConfig: ProfileConfig, backendConfig: NtfyConfig, dto: NtfyDTO, task?: Task): Promise<Response> {
|
||||||
const token = enhancedStringConfig(config.token);
|
const token = enhancedStringConfig(backendConfig.token);
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const actions: object[] = [];
|
||||||
|
|
||||||
|
if (task && profileConfig.completion?.enable && config.server?.baseUrl && !task.recurrenceRule) {
|
||||||
|
const body: CompleteTaskDTO = {
|
||||||
|
task,
|
||||||
|
profile: profileConfig.name
|
||||||
|
};
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
action: "http",
|
||||||
|
label: "Mark as completed",
|
||||||
|
url: `${config.server.baseUrl}/complete`,
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
buildHeaders(dto, headers);
|
buildHeaders(dto, headers);
|
||||||
|
|
||||||
return fetch(`https://${config.url}/${config.topic || 'obsidian'}`, {
|
return fetch(`https://${backendConfig.url}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: dto.text ?? "",
|
headers,
|
||||||
headers
|
body: JSON.stringify({
|
||||||
|
topic: backendConfig.topic || 'obsidian',
|
||||||
|
message: dto.text ?? "",
|
||||||
|
actions
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CLIOptions } from "../types/cli";
|
|||||||
import { loadConfig } from "../config";
|
import { loadConfig } from "../config";
|
||||||
import { notify, scan, test } from "../runner";
|
import { notify, scan, test } from "../runner";
|
||||||
import { CronJob } from "cron";
|
import { CronJob } from "cron";
|
||||||
|
import { startServer } from "../server";
|
||||||
|
|
||||||
const getOptions = () => program
|
const getOptions = () => program
|
||||||
.name("obsidian-tasks-reminder")
|
.name("obsidian-tasks-reminder")
|
||||||
@@ -11,7 +12,7 @@ const getOptions = () => program
|
|||||||
.requiredOption("-c, --config <file>", "sets the path to the YAML file with configuration")
|
.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("-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("-t, --test", "evaluates the query, applies the mapper and prints to stdout the notifications about to be trigger, without actual triggering them")
|
.option("-t, --test", "evaluates the query, applies the mapper and prints to stdout the notifications about to be trigger, without actual triggering them")
|
||||||
.option("-p, --profile <name>", "(applicable only with '--test' option) limits the current operation only to specified profile. If missing, all profiles will be affected")
|
.option("-p, --profile <name>", "limits the current operation only to specified profile. If missing, all profiles will be affected")
|
||||||
.parse()
|
.parse()
|
||||||
.opts<CLIOptions>();
|
.opts<CLIOptions>();
|
||||||
|
|
||||||
@@ -43,15 +44,19 @@ const getOptions = () => program
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await scan(config);
|
||||||
|
|
||||||
const scanJob = CronJob.from({
|
const scanJob = CronJob.from({
|
||||||
cronTime: config.scanCron ?? '0 0 * * * *',
|
cronTime: config.scanCron ?? '0 0 * * * *',
|
||||||
onTick () { scan(config) },
|
onTick () { scan(config, options.profile) },
|
||||||
start: true,
|
start: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const notifyJob = CronJob.from({
|
const notifyJob = CronJob.from({
|
||||||
cronTime: config.notifyCron ?? '0 * * * * *',
|
cronTime: config.notifyCron ?? '0 * * * * *',
|
||||||
onTick() { notify(config) },
|
onTick() { notify(config, options.profile) },
|
||||||
start: true,
|
start: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await startServer(config);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,13 @@ import { Config } from "../types/config";
|
|||||||
|
|
||||||
export function loadConfig(file: string): Config {
|
export function loadConfig(file: string): Config {
|
||||||
const text = fs.readFileSync(file, 'utf-8');
|
const text = fs.readFileSync(file, 'utf-8');
|
||||||
return yaml.parse(text) as Config;
|
const cfg = yaml.parse(text) as Config;
|
||||||
|
|
||||||
|
for (const profile of Object.keys(cfg.profiles)) {
|
||||||
|
if (cfg.profiles[profile]) {
|
||||||
|
cfg.profiles[profile].name = profile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
}
|
}
|
||||||
@@ -71,10 +71,11 @@ async function readTasksFromFile(path: string): Promise<Task[]> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const list: Task[] = [];
|
const list: Task[] = [];
|
||||||
|
let lineNumber = 1;
|
||||||
|
|
||||||
for await (const line of lines) {
|
for await (const line of lines) {
|
||||||
try {
|
try {
|
||||||
const task = parseTask(line);
|
const task = parseTask(line, path, lineNumber);
|
||||||
|
|
||||||
if(task) {
|
if(task) {
|
||||||
list.push(task);
|
list.push(task);
|
||||||
@@ -87,6 +88,8 @@ async function readTasksFromFile(path: string): Promise<Task[]> {
|
|||||||
}
|
}
|
||||||
console.warn(e.message);
|
console.warn(e.message);
|
||||||
console.warn("This line will be ignored. Please check the source and adjust it accordingly.");
|
console.warn("This line will be ignored. Please check the source and adjust it accordingly.");
|
||||||
|
} finally {
|
||||||
|
lineNumber++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,11 +100,11 @@ async function readTasksFromFile(path: string): Promise<Task[]> {
|
|||||||
* Converts line to task model.
|
* Converts line to task model.
|
||||||
* If the line does not represent task, returns undefined.
|
* If the line does not represent task, returns undefined.
|
||||||
*/
|
*/
|
||||||
function parseTask(line: string): Task|undefined {
|
function parseTask(line: string, path: string, lineNumber: number): Task|undefined {
|
||||||
const item = parse(line) as ParseResult;
|
const item = parse(line) as ParseResult;
|
||||||
|
|
||||||
if (item.type === 'task') {
|
if (item.type === 'task') {
|
||||||
return new DefaultTask(item.data);
|
return new DefaultTask(path, line, lineNumber, item.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -5,10 +5,17 @@ import { Task, TaskPriority } from "../types/task";
|
|||||||
export class LazyTask implements Task {
|
export class LazyTask implements Task {
|
||||||
#parsed: ParsedTask;
|
#parsed: ParsedTask;
|
||||||
#dates: ParsedTaskDate[];
|
#dates: ParsedTaskDate[];
|
||||||
|
source: string;
|
||||||
|
sourceFile: string;
|
||||||
|
sourceLine: number;
|
||||||
|
|
||||||
|
|
||||||
constructor(task: ParsedTask) {
|
constructor(path: string, line: string, lineNumber: number, task: ParsedTask) {
|
||||||
this.#parsed = task;
|
this.#parsed = task;
|
||||||
this.#dates = task.meta.filter(x => x.feature === 'date');
|
this.#dates = task.meta.filter(x => x.feature === 'date');
|
||||||
|
this.source = line;
|
||||||
|
this.sourceFile = path;
|
||||||
|
this.sourceLine = lineNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
get status(): string {
|
get status(): string {
|
||||||
@@ -169,6 +176,9 @@ export class LazyTask implements Task {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
dependsOn: this.dependsOn,
|
dependsOn: this.dependsOn,
|
||||||
reminder: this.reminder,
|
reminder: this.reminder,
|
||||||
|
source: this.source,
|
||||||
|
sourceLine: this.sourceLine,
|
||||||
|
sourceFile: this.sourceFile
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,8 @@ import { loadDatabase } from "../database/deserializer";
|
|||||||
import { remind } from "../backend";
|
import { remind } from "../backend";
|
||||||
import { Config, ProfileConfig } from "../types/config";
|
import { Config, ProfileConfig } from "../types/config";
|
||||||
|
|
||||||
export const test = handleProfile(async config => {
|
export const test = handleProfile(async (config, profileConfig) => {
|
||||||
const tasks = await loadTasks(config.sources, config.query, config.exclude);
|
const tasks = await loadTasks(profileConfig.sources, profileConfig.query, profileConfig.exclude);
|
||||||
const db = createDatabase(tasks);
|
const db = createDatabase(tasks);
|
||||||
|
|
||||||
for (const time of Object.keys(db)) {
|
for (const time of Object.keys(db)) {
|
||||||
@@ -19,17 +19,17 @@ export const test = handleProfile(async config => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const scan = handleProfile(async config => {
|
export const scan = handleProfile(async (config, profileConfig) => {
|
||||||
const tasks = await loadTasks(config.sources, config.query, config.exclude);
|
const tasks = await loadTasks(profileConfig.sources, profileConfig.query, profileConfig.exclude);
|
||||||
dumpDatabase(config.databaseFile, tasks);
|
dumpDatabase(profileConfig.databaseFile, tasks);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const notify = handleProfile(async config => {
|
export const notify = handleProfile(async (config, profileConfig) => {
|
||||||
const db = loadDatabase(config.databaseFile);
|
const db = loadDatabase(profileConfig.databaseFile);
|
||||||
remind(config, db);
|
remind(config, profileConfig, db);
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleProfile(handler: (profile: ProfileConfig) => Promise<void>) {
|
function handleProfile(handler: (config: Config, profile: ProfileConfig) => Promise<void>) {
|
||||||
return async (config: Config, profile?: string) => {
|
return async (config: Config, profile?: string) => {
|
||||||
if (profile !== undefined) {
|
if (profile !== undefined) {
|
||||||
const cfg = config.profiles[profile];
|
const cfg = config.profiles[profile];
|
||||||
@@ -38,9 +38,9 @@ function handleProfile(handler: (profile: ProfileConfig) => Promise<void>) {
|
|||||||
throw new Error(`Undefined profile: ${profile}`);
|
throw new Error(`Undefined profile: ${profile}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler(cfg);
|
return handler(config, cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await Promise.all(Object.values(config.profiles).map(handler));
|
return await Promise.all(Object.values(config.profiles).map(c => handler(config, c)));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
22
src/server/index.ts
Normal file
22
src/server/index.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Task } from "../model";
|
||||||
|
import { Config } from "../types/config";
|
||||||
|
import express from "express";
|
||||||
|
import { CompleteTaskDTO } from "../types/dto";
|
||||||
|
import { complete } from "../services/task.service";
|
||||||
|
|
||||||
|
export async function startServer(config: Config) {
|
||||||
|
const server = express();
|
||||||
|
const port = config?.server?.port ?? 8080;
|
||||||
|
|
||||||
|
server.post('/complete', express.json(), async (req, res) => {
|
||||||
|
const { profile, task } = await req.body as CompleteTaskDTO;
|
||||||
|
|
||||||
|
complete(config, profile, task);
|
||||||
|
|
||||||
|
res.json({ status: "ok" });
|
||||||
|
})
|
||||||
|
|
||||||
|
server.listen(port, () => {
|
||||||
|
console.log(`Server listens on ${port} port`);
|
||||||
|
});
|
||||||
|
}
|
||||||
38
src/services/task.service.ts
Normal file
38
src/services/task.service.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import { Task } from "../types/task";
|
||||||
|
import { Config } from "../types/config";
|
||||||
|
|
||||||
|
export function complete(config: Config, profile: string, task: Task) {
|
||||||
|
const profileConfig = config.profiles[profile];
|
||||||
|
|
||||||
|
if (profileConfig === undefined) {
|
||||||
|
throw new Error(`Unknown profile ${profile}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profileConfig.completion?.enable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.recurrenceRule) {
|
||||||
|
throw new Error('Recurrence tasks are not supported for now');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = fs.readFileSync(task.sourceFile, 'utf8').split(/\n/);
|
||||||
|
const output = [];
|
||||||
|
|
||||||
|
for (const [number0, line] of data.entries()) {
|
||||||
|
if (task.sourceLine === number0 + 1) {
|
||||||
|
if (task.source !== line) {
|
||||||
|
throw new Error(`Cannot complete task, file has been changed since last scan. Rememberred line: ${task.source}, current line: ${line}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push(line.replace(/^-(\s+)\[.\]/, `-$1[${profileConfig.completion?.status ?? 'x'}]`));
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
output.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(task.sourceFile, output.join("\n"));
|
||||||
|
}
|
||||||
@@ -2,19 +2,32 @@ export type Config = {
|
|||||||
profiles: Record<string, ProfileConfig>;
|
profiles: Record<string, ProfileConfig>;
|
||||||
notifyCron?: string;
|
notifyCron?: string;
|
||||||
scanCron?: string;
|
scanCron?: string;
|
||||||
|
server?: ServerConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServerConfig = {
|
||||||
|
port?: number;
|
||||||
|
baseUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProfileConfig = {
|
export type ProfileConfig = {
|
||||||
|
name: string;
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
sources: string[];
|
sources: string[];
|
||||||
query?: string;
|
query?: string;
|
||||||
exclude?: string;
|
exclude?: string;
|
||||||
defaultTime?: string;
|
defaultTime?: string;
|
||||||
databaseFile: string;
|
databaseFile: string;
|
||||||
backend: Record<string, unknown>;
|
backend: Record<string, unknown>;
|
||||||
|
completion?: CompletionConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BackendSettings = {
|
export type CompletionConfig = {
|
||||||
|
enable?: boolean;
|
||||||
|
status?: 'x';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BackendSettings = {
|
||||||
enable?: boolean;
|
enable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
6
src/types/dto.ts
Normal file
6
src/types/dto.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Task } from "./task";
|
||||||
|
|
||||||
|
export type CompleteTaskDTO = {
|
||||||
|
profile: string;
|
||||||
|
task: Task
|
||||||
|
};
|
||||||
@@ -17,9 +17,11 @@ export type Task = {
|
|||||||
onDelete?: string;
|
onDelete?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
dependsOn?: string[];
|
dependsOn?: string[];
|
||||||
reminder?: string;
|
reminder?: string;
|
||||||
|
sourceFile: string;
|
||||||
}
|
sourceLine: number;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
export enum TaskPriority {
|
export enum TaskPriority {
|
||||||
LOWEST,
|
LOWEST,
|
||||||
|
|||||||
Reference in New Issue
Block a user