Initial Obsidian reminder notifier plugin
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
node_modules/
|
||||
.DS_Store
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.env
|
||||
.env.*
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
# Obsidian Reminder Notifier
|
||||
|
||||
Obsidian Reminder Notifier is an Obsidian plugin for creating reminders from notes, the command palette, editor selection, or the left ribbon. It can send notifications through local Obsidian notices, Telegram, ntfy, Discord webhooks, or generic webhooks.
|
||||
|
||||
> Reminder delivery works while Obsidian is running. If Obsidian is closed at the due time, pending reminders are checked when Obsidian starts again.
|
||||
|
||||
## English
|
||||
|
||||
### Features
|
||||
|
||||
- Create reminders from the command palette.
|
||||
- Create reminders from selected text in the editor context menu.
|
||||
- Create reminders from the left ribbon button.
|
||||
- Show active, sent, completed, failed, and snoozed reminders.
|
||||
- Scan the current note or the whole vault for `@remind(...)` syntax.
|
||||
- Send reminders through local Obsidian notices, Telegram, ntfy, Discord, or generic webhooks.
|
||||
- Configure notification providers from built-in templates.
|
||||
|
||||
### Installation
|
||||
|
||||
#### Manual installation
|
||||
|
||||
1. Download or build the plugin files.
|
||||
2. Create this folder inside your vault:
|
||||
|
||||
```text
|
||||
<your-vault>/.obsidian/plugins/reminder-notifier/
|
||||
```
|
||||
|
||||
3. Copy these files into that folder:
|
||||
|
||||
```text
|
||||
manifest.json
|
||||
main.js
|
||||
styles.css
|
||||
```
|
||||
|
||||
4. Restart Obsidian or reload plugins.
|
||||
5. Open `Settings -> Community plugins`.
|
||||
6. Disable Safe mode if needed.
|
||||
7. Enable `Reminder Notifier`.
|
||||
|
||||
#### Build from source
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
After the build, copy `manifest.json`, `main.js`, and `styles.css` into your vault plugin folder.
|
||||
|
||||
### Usage
|
||||
|
||||
#### Create a reminder from the UI
|
||||
|
||||
Use any of these entry points:
|
||||
|
||||
- left ribbon button `Create reminder`;
|
||||
- command palette command `Create reminder`;
|
||||
- editor right-click menu item `Create reminder`;
|
||||
- command palette command `Create reminder from selected text`.
|
||||
|
||||
The create dialog contains:
|
||||
|
||||
- reminder text input;
|
||||
- year dropdown;
|
||||
- month dropdown;
|
||||
- day dropdown;
|
||||
- hour dropdown;
|
||||
- minute dropdown;
|
||||
- optional details field;
|
||||
- optional channels field.
|
||||
|
||||
Leave the channels field empty to send through every enabled provider. Enter provider IDs or provider types separated by commas to target specific providers, for example:
|
||||
|
||||
```text
|
||||
ntfy
|
||||
telegram, ntfy
|
||||
discord
|
||||
```
|
||||
|
||||
#### Show reminders
|
||||
|
||||
Use any of these entry points:
|
||||
|
||||
- left ribbon button `Show reminders`;
|
||||
- editor right-click menu item `Show reminders`;
|
||||
- command palette command `Show active reminders`;
|
||||
- command palette command `Show all reminders`.
|
||||
|
||||
The list lets you snooze a reminder for 15 minutes, mark it done, or delete it.
|
||||
|
||||
#### Markdown syntax
|
||||
|
||||
```md
|
||||
- [ ] Call the doctor @remind(2026-07-01 10:00)
|
||||
- [ ] Check server @remind(2026-07-01 09:00, ntfy)
|
||||
- [ ] Take a break @remind(+2h, local)
|
||||
- [ ] Weekly planning @remind(next monday 14:30, telegram, ntfy)
|
||||
```
|
||||
|
||||
If no channel is provided, scanned reminders are sent through all enabled notification providers.
|
||||
|
||||
Frontmatter is supported:
|
||||
|
||||
```yaml
|
||||
---
|
||||
reminder:
|
||||
at: 2026-07-01 10:00
|
||||
title: Call the doctor
|
||||
notify:
|
||||
- telegram
|
||||
- ntfy
|
||||
---
|
||||
```
|
||||
|
||||
Reminder code blocks are supported:
|
||||
|
||||
````
|
||||
```reminder
|
||||
title: Make a backup
|
||||
at: 2026-07-01 23:00
|
||||
notify: ntfy
|
||||
priority: high
|
||||
```
|
||||
````
|
||||
|
||||
After adding reminders to notes, run `Scan current note for reminders` or `Scan vault for reminders`.
|
||||
|
||||
### Provider setup
|
||||
|
||||
Open `Settings -> Reminder Notifier`.
|
||||
|
||||
#### ntfy
|
||||
|
||||
1. Click `Add provider`.
|
||||
2. Choose `ntfy.sh`.
|
||||
3. Enable the provider.
|
||||
4. Set the topic in the JSON config:
|
||||
|
||||
```json
|
||||
{
|
||||
"serverUrl": "https://ntfy.sh",
|
||||
"topic": "my-obsidian-reminders",
|
||||
"token": "",
|
||||
"priority": 3,
|
||||
"tags": ["calendar", "obsidian"]
|
||||
}
|
||||
```
|
||||
|
||||
5. Click `Test notification`.
|
||||
|
||||
For self-hosted ntfy, change `serverUrl` and set `token` if your server requires authentication.
|
||||
|
||||
#### Telegram
|
||||
|
||||
1. Create a bot with BotFather.
|
||||
2. Add the Telegram provider from the template.
|
||||
3. Enable it.
|
||||
4. Set `botToken` and `chatId`.
|
||||
5. Click `Test notification`.
|
||||
|
||||
#### Webhook and Discord
|
||||
|
||||
Use the webhook templates for Discord, Slack-compatible tools, Make, n8n, Home Assistant, or custom services. Webhook methods are limited to `POST`, `PUT`, and `PATCH`.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- If a test notification works but a note reminder does not arrive, run `Scan current note for reminders` after editing the note.
|
||||
- Check `Show all reminders` for failed reminders and error messages.
|
||||
- Make sure the provider is enabled in settings.
|
||||
- Make sure the reminder due time is in the future when created.
|
||||
- Obsidian must be running for reminders to be delivered at the exact due time.
|
||||
|
||||
## Русский
|
||||
|
||||
### Возможности
|
||||
|
||||
- Создание напоминаний из командной палитры.
|
||||
- Создание напоминаний из выделенного текста через меню правой кнопки мыши.
|
||||
- Создание напоминаний через кнопку в левой панели Obsidian.
|
||||
- Просмотр активных, отправленных, выполненных, отложенных и ошибочных напоминаний.
|
||||
- Сканирование текущей заметки или всего vault по синтаксису `@remind(...)`.
|
||||
- Отправка уведомлений через локальные уведомления Obsidian, Telegram, ntfy, Discord или универсальный webhook.
|
||||
- Настройка провайдеров из готовых шаблонов.
|
||||
|
||||
### Установка
|
||||
|
||||
#### Ручная установка
|
||||
|
||||
1. Скачайте или соберите файлы плагина.
|
||||
2. Создайте папку внутри вашего vault:
|
||||
|
||||
```text
|
||||
<ваш-vault>/.obsidian/plugins/reminder-notifier/
|
||||
```
|
||||
|
||||
3. Скопируйте туда файлы:
|
||||
|
||||
```text
|
||||
manifest.json
|
||||
main.js
|
||||
styles.css
|
||||
```
|
||||
|
||||
4. Перезапустите Obsidian или перезагрузите плагины.
|
||||
5. Откройте `Settings -> Community plugins`.
|
||||
6. При необходимости отключите Safe mode.
|
||||
7. Включите `Reminder Notifier`.
|
||||
|
||||
#### Сборка из исходников
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
После сборки скопируйте `manifest.json`, `main.js` и `styles.css` в папку плагина внутри vault.
|
||||
|
||||
### Использование
|
||||
|
||||
#### Создание напоминания из интерфейса
|
||||
|
||||
Доступные способы:
|
||||
|
||||
- кнопка `Create reminder` в левой панели;
|
||||
- команда `Create reminder` в командной палитре;
|
||||
- пункт `Create reminder` в меню правой кнопки мыши в редакторе;
|
||||
- команда `Create reminder from selected text`.
|
||||
|
||||
В окне создания есть:
|
||||
|
||||
- поле для текста напоминания;
|
||||
- выпадающий список года;
|
||||
- выпадающий список месяца;
|
||||
- выпадающий список дня;
|
||||
- выпадающий список часа;
|
||||
- выпадающий список минут;
|
||||
- дополнительное поле описания;
|
||||
- поле каналов уведомлений.
|
||||
|
||||
Если поле каналов оставить пустым, уведомление будет отправлено во все включённые провайдеры. Если нужно отправить только в конкретные каналы, укажите ID или типы провайдеров через запятую:
|
||||
|
||||
```text
|
||||
ntfy
|
||||
telegram, ntfy
|
||||
discord
|
||||
```
|
||||
|
||||
#### Просмотр напоминаний
|
||||
|
||||
Доступные способы:
|
||||
|
||||
- кнопка `Show reminders` в левой панели;
|
||||
- пункт `Show reminders` в меню правой кнопки мыши;
|
||||
- команда `Show active reminders`;
|
||||
- команда `Show all reminders`.
|
||||
|
||||
В списке можно отложить напоминание на 15 минут, отметить выполненным или удалить.
|
||||
|
||||
#### Синтаксис в Markdown
|
||||
|
||||
```md
|
||||
- [ ] Позвонить врачу @remind(2026-07-01 10:00)
|
||||
- [ ] Проверить сервер @remind(2026-07-01 09:00, ntfy)
|
||||
- [ ] Сделать перерыв @remind(+2h, local)
|
||||
- [ ] Планирование недели @remind(next monday 14:30, telegram, ntfy)
|
||||
```
|
||||
|
||||
Если канал не указан, найденное в заметке напоминание отправляется во все включённые провайдеры уведомлений.
|
||||
|
||||
Поддерживается frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
reminder:
|
||||
at: 2026-07-01 10:00
|
||||
title: Позвонить врачу
|
||||
notify:
|
||||
- telegram
|
||||
- ntfy
|
||||
---
|
||||
```
|
||||
|
||||
Поддерживаются code blocks:
|
||||
|
||||
````
|
||||
```reminder
|
||||
title: Сделать бэкап
|
||||
at: 2026-07-01 23:00
|
||||
notify: ntfy
|
||||
priority: high
|
||||
```
|
||||
````
|
||||
|
||||
После добавления напоминаний в заметки запустите `Scan current note for reminders` или `Scan vault for reminders`.
|
||||
|
||||
### Настройка провайдеров
|
||||
|
||||
Откройте `Settings -> Reminder Notifier`.
|
||||
|
||||
#### ntfy
|
||||
|
||||
1. Нажмите `Add provider`.
|
||||
2. Выберите `ntfy.sh`.
|
||||
3. Включите провайдер.
|
||||
4. Укажите topic в JSON-конфиге:
|
||||
|
||||
```json
|
||||
{
|
||||
"serverUrl": "https://ntfy.sh",
|
||||
"topic": "my-obsidian-reminders",
|
||||
"token": "",
|
||||
"priority": 3,
|
||||
"tags": ["calendar", "obsidian"]
|
||||
}
|
||||
```
|
||||
|
||||
5. Нажмите `Test notification`.
|
||||
|
||||
Для self-hosted ntfy измените `serverUrl` и заполните `token`, если сервер требует авторизацию.
|
||||
|
||||
#### Telegram
|
||||
|
||||
1. Создайте бота через BotFather.
|
||||
2. Добавьте Telegram-провайдер из шаблона.
|
||||
3. Включите его.
|
||||
4. Укажите `botToken` и `chatId`.
|
||||
5. Нажмите `Test notification`.
|
||||
|
||||
#### Webhook и Discord
|
||||
|
||||
Webhook-шаблоны можно использовать для Discord, Slack-совместимых сервисов, Make, n8n, Home Assistant и других HTTP-интеграций. Разрешённые методы webhook: `POST`, `PUT`, `PATCH`.
|
||||
|
||||
### Если уведомление не пришло
|
||||
|
||||
- Если тестовое уведомление работает, а напоминание из заметки не пришло, запустите `Scan current note for reminders` после изменения заметки.
|
||||
- Откройте `Show all reminders` и проверьте статус и текст ошибки.
|
||||
- Убедитесь, что нужный провайдер включён.
|
||||
- Убедитесь, что время напоминания было в будущем при создании.
|
||||
- Для точной отправки в заданное время Obsidian должен быть открыт.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
The generated production bundle is `main.js`.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = process.argv[2] === "production";
|
||||
const watch = process.argv.includes("--watch");
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins,
|
||||
],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
minify: prod,
|
||||
});
|
||||
|
||||
if (watch) {
|
||||
await context.watch();
|
||||
console.log("Watching for changes...");
|
||||
} else {
|
||||
await context.rebuild();
|
||||
await context.dispose();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "reminder-notifier",
|
||||
"name": "Reminder Notifier",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "1.5.0",
|
||||
"description": "Create reminders from notes and send notifications through Telegram, ntfy, webhooks, Discord, or local Obsidian notices.",
|
||||
"author": "",
|
||||
"authorUrl": "",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
Generated
+611
@@ -0,0 +1,611 @@
|
||||
{
|
||||
"name": "obsidian-reminder-notifier",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-reminder-notifier",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.30",
|
||||
"builtin-modules": "^3.3.0",
|
||||
"esbuild": "^0.21.5",
|
||||
"obsidian": "^1.5.12",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
|
||||
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.38.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
|
||||
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
||||
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
||||
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
||||
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
||||
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/codemirror": {
|
||||
"version": "5.60.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
|
||||
"integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/tern": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.43",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
|
||||
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/tern": {
|
||||
"version": "0.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
|
||||
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/builtin-modules": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
|
||||
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/obsidian": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.13.1.tgz",
|
||||
"integrity": "sha512-qtTEA2pmhJzhuhJqzbBFRYhpIOqvW+krDYjtFynv66KbxBbumHBlsJfWw3I4jtnK/6fZwbQhCrmmDdRwXmX56w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/codemirror": "5.60.8",
|
||||
"moment": "2.29.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/state": "6.5.0",
|
||||
"@codemirror/view": "6.38.6"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "obsidian-reminder-notifier",
|
||||
"version": "0.1.0",
|
||||
"description": "Obsidian plugin for note-based reminders and external notifications.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs --watch",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
|
||||
},
|
||||
"keywords": [
|
||||
"obsidian",
|
||||
"reminders",
|
||||
"notifications",
|
||||
"telegram",
|
||||
"ntfy"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.30",
|
||||
"builtin-modules": "^3.3.0",
|
||||
"esbuild": "^0.21.5",
|
||||
"obsidian": "^1.5.12",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import { Editor, MarkdownView, Menu, Notice, Plugin, TFile } from "obsidian";
|
||||
import { ReminderListModal } from "./reminders/ReminderListModal";
|
||||
import { ReminderModal } from "./reminders/ReminderModal";
|
||||
import { ReminderScheduler } from "./reminders/ReminderScheduler";
|
||||
import { ReminderStore } from "./reminders/ReminderStore";
|
||||
import { ReminderNotifierSettingTab } from "./settings/SettingsTab";
|
||||
import { NotificationProviderConfig, ProviderType } from "./types";
|
||||
import { createProviderRegistry } from "./notifiers";
|
||||
|
||||
export default class ReminderNotifierPlugin extends Plugin {
|
||||
store!: ReminderStore;
|
||||
scheduler!: ReminderScheduler;
|
||||
providers = createProviderRegistry();
|
||||
|
||||
async onload(): Promise<void> {
|
||||
this.store = new ReminderStore(this);
|
||||
await this.store.load();
|
||||
|
||||
this.scheduler = new ReminderScheduler(this.app.vault, this.store, this.providers);
|
||||
this.addSettingTab(new ReminderNotifierSettingTab(this.app, this));
|
||||
this.registerCommands();
|
||||
this.registerRibbonActions();
|
||||
this.registerContextMenus();
|
||||
this.registerFileWatcher();
|
||||
|
||||
if (this.store.getSettings().scanVaultOnStartup) {
|
||||
const count = await this.scheduler.scanVault();
|
||||
if (count > 0) {
|
||||
new Notice(`Reminder Notifier indexed ${count} reminders.`);
|
||||
}
|
||||
}
|
||||
|
||||
this.scheduler.start();
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
this.scheduler?.stop();
|
||||
}
|
||||
|
||||
restartScheduler(): void {
|
||||
this.scheduler.stop();
|
||||
this.scheduler.start();
|
||||
}
|
||||
|
||||
async testProvider(provider: NotificationProviderConfig): Promise<void> {
|
||||
const notifier = this.providers.get(provider.type as ProviderType);
|
||||
if (!notifier) {
|
||||
throw new Error(`Unknown provider: ${provider.type}`);
|
||||
}
|
||||
await notifier.test(provider);
|
||||
}
|
||||
|
||||
private registerCommands(): void {
|
||||
this.addCommand({
|
||||
id: "create-reminder",
|
||||
name: "Create reminder",
|
||||
callback: () => {
|
||||
new ReminderModal(this.app, this.store.getSettings().defaultChannels, async (input) => {
|
||||
await this.store.add(input, this.store.getSettings().defaultChannels);
|
||||
new Notice("Reminder created.");
|
||||
}).open();
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "create-reminder-from-selection",
|
||||
name: "Create reminder from selected text",
|
||||
editorCallback: (editor: Editor) => {
|
||||
const selection = editor.getSelection();
|
||||
new ReminderModal(this.app, this.store.getSettings().defaultChannels, async (input) => {
|
||||
await this.store.add(input, this.store.getSettings().defaultChannels);
|
||||
new Notice("Reminder created.");
|
||||
}, selection).open();
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "show-active-reminders",
|
||||
name: "Show active reminders",
|
||||
callback: () => {
|
||||
new ReminderListModal(this.app, this.store, false).open();
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "show-all-reminders",
|
||||
name: "Show all reminders",
|
||||
callback: () => {
|
||||
new ReminderListModal(this.app, this.store, true).open();
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "scan-current-note",
|
||||
name: "Scan current note for reminders",
|
||||
checkCallback: (checking) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
if (!checking) {
|
||||
void this.scanCurrentFile(file);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "scan-vault",
|
||||
name: "Scan vault for reminders",
|
||||
callback: async () => {
|
||||
const count = await this.scheduler.scanVault();
|
||||
new Notice(`Indexed ${count} reminders.`);
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "test-notification-providers",
|
||||
name: "Test notification providers",
|
||||
callback: async () => {
|
||||
const providers = this.store.getSettings().notificationProviders.filter((provider) => provider.enabled);
|
||||
for (const provider of providers) {
|
||||
await this.testProvider(provider);
|
||||
}
|
||||
new Notice(`Tested ${providers.length} providers.`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private registerRibbonActions(): void {
|
||||
this.addRibbonIcon("bell-plus", "Create reminder", () => {
|
||||
new ReminderModal(this.app, this.store.getSettings().defaultChannels, async (input) => {
|
||||
await this.store.add(input, this.store.getSettings().defaultChannels);
|
||||
new Notice("Reminder created.");
|
||||
}).open();
|
||||
});
|
||||
|
||||
this.addRibbonIcon("list-checks", "Show reminders", () => {
|
||||
new ReminderListModal(this.app, this.store, true).open();
|
||||
});
|
||||
}
|
||||
|
||||
private registerContextMenus(): void {
|
||||
this.registerEvent(this.app.workspace.on("editor-menu", (menu: Menu, editor: Editor) => {
|
||||
menu.addSeparator();
|
||||
menu.addItem((item) => item
|
||||
.setTitle("Create reminder")
|
||||
.setIcon("bell-plus")
|
||||
.onClick(() => {
|
||||
const selection = editor.getSelection();
|
||||
new ReminderModal(this.app, this.store.getSettings().defaultChannels, async (input) => {
|
||||
await this.store.add(input, this.store.getSettings().defaultChannels);
|
||||
new Notice("Reminder created.");
|
||||
}, selection).open();
|
||||
}));
|
||||
|
||||
menu.addItem((item) => item
|
||||
.setTitle("Show reminders")
|
||||
.setIcon("list-checks")
|
||||
.onClick(() => {
|
||||
new ReminderListModal(this.app, this.store, true).open();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
private registerFileWatcher(): void {
|
||||
this.registerEvent(this.app.vault.on("modify", (file) => {
|
||||
if (!this.store.getSettings().scanOnFileChange || !(file instanceof TFile) || file.extension !== "md") {
|
||||
return;
|
||||
}
|
||||
void this.scheduler.scanFile(file);
|
||||
}));
|
||||
}
|
||||
|
||||
private async scanCurrentFile(file: TFile): Promise<void> {
|
||||
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
const target = view?.file ?? file;
|
||||
const count = await this.scheduler.scanFile(target);
|
||||
new Notice(`Indexed ${count} reminders from ${target.basename}.`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { requestUrl } from "obsidian";
|
||||
import { NotificationPayload, NotificationProvider, NotificationProviderConfig } from "../types";
|
||||
import { asString } from "../utils/template";
|
||||
|
||||
export class DiscordNotifier implements NotificationProvider {
|
||||
id = "discord" as const;
|
||||
name = "Discord webhook";
|
||||
|
||||
async send(payload: NotificationPayload, provider: NotificationProviderConfig): Promise<void> {
|
||||
const webhookUrl = asString(provider.config.webhookUrl);
|
||||
if (!webhookUrl) {
|
||||
throw new Error("Discord webhookUrl is required.");
|
||||
}
|
||||
|
||||
await requestUrl({
|
||||
url: webhookUrl,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
username: asString(provider.config.username, "Obsidian Reminder"),
|
||||
content: `**${payload.title}**\n${payload.message}`,
|
||||
}),
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
|
||||
async test(provider: NotificationProviderConfig): Promise<void> {
|
||||
await this.send({
|
||||
title: "Obsidian Reminder Notifier",
|
||||
message: "Test notification",
|
||||
dueAt: new Date().toISOString(),
|
||||
}, provider);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Notice } from "obsidian";
|
||||
import { NotificationPayload, NotificationProvider, NotificationProviderConfig } from "../types";
|
||||
|
||||
export class LocalNotifier implements NotificationProvider {
|
||||
id = "local" as const;
|
||||
name = "Local Obsidian notice";
|
||||
|
||||
async send(payload: NotificationPayload): Promise<void> {
|
||||
new Notice(`${payload.title}\n${payload.message}`, 10_000);
|
||||
}
|
||||
|
||||
async test(_config: NotificationProviderConfig): Promise<void> {
|
||||
new Notice("Reminder Notifier test notification", 5_000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { requestUrl } from "obsidian";
|
||||
import { NotificationPayload, NotificationProvider, NotificationProviderConfig } from "../types";
|
||||
import { asString } from "../utils/template";
|
||||
|
||||
export class NtfyNotifier implements NotificationProvider {
|
||||
id = "ntfy" as const;
|
||||
name = "ntfy";
|
||||
|
||||
async send(payload: NotificationPayload, provider: NotificationProviderConfig): Promise<void> {
|
||||
const serverUrl = asString(provider.config.serverUrl, "https://ntfy.sh").replace(/\/$/, "");
|
||||
const topic = asString(provider.config.topic);
|
||||
if (!topic) {
|
||||
throw new Error("ntfy topic is required.");
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Title: payload.title,
|
||||
Priority: String(provider.config.priority ?? priorityToNtfy(payload.priority)),
|
||||
};
|
||||
|
||||
const token = asString(provider.config.token);
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const tags = provider.config.tags;
|
||||
if (Array.isArray(tags)) {
|
||||
headers.Tags = tags.join(",");
|
||||
}
|
||||
|
||||
await requestUrl({
|
||||
url: `${serverUrl}/${encodeURIComponent(topic)}`,
|
||||
method: "POST",
|
||||
headers,
|
||||
body: payload.message,
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
|
||||
async test(provider: NotificationProviderConfig): Promise<void> {
|
||||
await this.send({
|
||||
title: "Obsidian Reminder Notifier",
|
||||
message: "Test notification",
|
||||
dueAt: new Date().toISOString(),
|
||||
}, provider);
|
||||
}
|
||||
}
|
||||
|
||||
function priorityToNtfy(priority = "normal"): number {
|
||||
if (priority === "high") {
|
||||
return 4;
|
||||
}
|
||||
if (priority === "low") {
|
||||
return 2;
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { requestUrl } from "obsidian";
|
||||
import { NotificationPayload, NotificationProvider, NotificationProviderConfig } from "../types";
|
||||
import { asString } from "../utils/template";
|
||||
|
||||
export class TelegramNotifier implements NotificationProvider {
|
||||
id = "telegram" as const;
|
||||
name = "Telegram";
|
||||
|
||||
async send(payload: NotificationPayload, provider: NotificationProviderConfig): Promise<void> {
|
||||
const botToken = asString(provider.config.botToken);
|
||||
const chatId = asString(provider.config.chatId);
|
||||
if (!botToken || !chatId) {
|
||||
throw new Error("Telegram botToken and chatId are required.");
|
||||
}
|
||||
|
||||
const text = `*${payload.title}*\n\n${payload.message}`;
|
||||
await requestUrl({
|
||||
url: `https://api.telegram.org/bot${botToken}/sendMessage`,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
text,
|
||||
parse_mode: asString(provider.config.parseMode, "Markdown"),
|
||||
disable_web_page_preview: true,
|
||||
}),
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
|
||||
async test(provider: NotificationProviderConfig): Promise<void> {
|
||||
await this.send({
|
||||
title: "Obsidian Reminder Notifier",
|
||||
message: "Test notification",
|
||||
dueAt: new Date().toISOString(),
|
||||
}, provider);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { requestUrl } from "obsidian";
|
||||
import { NotificationPayload, NotificationProvider, NotificationProviderConfig } from "../types";
|
||||
import { asString, renderTemplate } from "../utils/template";
|
||||
|
||||
export class WebhookNotifier implements NotificationProvider {
|
||||
id = "webhook" as const;
|
||||
name = "Generic webhook";
|
||||
|
||||
async send(payload: NotificationPayload, provider: NotificationProviderConfig): Promise<void> {
|
||||
const url = asString(provider.config.url);
|
||||
if (!url) {
|
||||
throw new Error("Webhook URL is required.");
|
||||
}
|
||||
|
||||
const method = asString(provider.config.method, "POST").toUpperCase();
|
||||
if (!["POST", "PUT", "PATCH"].includes(method)) {
|
||||
throw new Error("Webhook method must be POST, PUT, or PATCH.");
|
||||
}
|
||||
|
||||
const headers = renderTemplate(provider.config.headers ?? { "Content-Type": "application/json" }, payload) as Record<string, string>;
|
||||
const body = renderTemplate(provider.config.bodyTemplate ?? {
|
||||
title: "{{title}}",
|
||||
message: "{{message}}",
|
||||
dueAt: "{{dueAt}}",
|
||||
sourceFile: "{{sourceFile}}",
|
||||
}, payload);
|
||||
|
||||
await requestUrl({
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body: typeof body === "string" ? body : JSON.stringify(body),
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
|
||||
async test(provider: NotificationProviderConfig): Promise<void> {
|
||||
await this.send({
|
||||
title: "Obsidian Reminder Notifier",
|
||||
message: "Test notification",
|
||||
dueAt: new Date().toISOString(),
|
||||
}, provider);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NotificationProvider, ProviderType } from "../types";
|
||||
import { DiscordNotifier } from "./DiscordNotifier";
|
||||
import { LocalNotifier } from "./LocalNotifier";
|
||||
import { NtfyNotifier } from "./NtfyNotifier";
|
||||
import { TelegramNotifier } from "./TelegramNotifier";
|
||||
import { WebhookNotifier } from "./WebhookNotifier";
|
||||
|
||||
export function createProviderRegistry(): Map<ProviderType, NotificationProvider> {
|
||||
const providers: NotificationProvider[] = [
|
||||
new LocalNotifier(),
|
||||
new TelegramNotifier(),
|
||||
new NtfyNotifier(),
|
||||
new WebhookNotifier(),
|
||||
new DiscordNotifier(),
|
||||
];
|
||||
|
||||
return new Map(providers.map((provider) => [provider.id, provider]));
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { App, Modal, Setting } from "obsidian";
|
||||
import { ReminderStore } from "./ReminderStore";
|
||||
import { formatDateTime } from "../utils/date";
|
||||
|
||||
export class ReminderListModal extends Modal {
|
||||
constructor(app: App, private store: ReminderStore, private showCompleted = false) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.render();
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h2", { text: this.showCompleted ? "All reminders" : "Active reminders" });
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton((button) => button
|
||||
.setButtonText(this.showCompleted ? "Show active only" : "Show all")
|
||||
.onClick(() => {
|
||||
this.showCompleted = !this.showCompleted;
|
||||
this.render();
|
||||
}));
|
||||
|
||||
const reminders = this.showCompleted
|
||||
? this.store.getAll()
|
||||
: this.store.getAll().filter((reminder) => !reminder.completed && reminder.status !== "cancelled" && reminder.status !== "sent" && reminder.status !== "done");
|
||||
if (!reminders.length) {
|
||||
contentEl.createEl("p", { text: this.showCompleted ? "No reminders." : "No active reminders." });
|
||||
return;
|
||||
}
|
||||
|
||||
const list = contentEl.createDiv({ cls: "reminder-notifier-list" });
|
||||
for (const reminder of reminders) {
|
||||
const item = list.createDiv({ cls: "reminder-notifier-item" });
|
||||
item.createDiv({ cls: "reminder-notifier-item-title", text: reminder.title });
|
||||
item.createDiv({
|
||||
cls: "reminder-notifier-item-meta",
|
||||
text: `${formatDateTime(reminder.dueAt)} - ${reminder.status} - ${reminder.channels.join(", ") || "all enabled"}`,
|
||||
});
|
||||
if (reminder.sourceFile) {
|
||||
item.createDiv({
|
||||
cls: "reminder-notifier-item-meta",
|
||||
text: `${reminder.sourceFile}${reminder.sourceLine ? `:${reminder.sourceLine}` : ""}`,
|
||||
});
|
||||
}
|
||||
if (reminder.lastError) {
|
||||
item.createDiv({ cls: "reminder-notifier-item-meta", text: `Last error: ${reminder.lastError}` });
|
||||
}
|
||||
|
||||
const actions = item.createDiv({ cls: "reminder-notifier-actions" });
|
||||
new Setting(actions)
|
||||
.addButton((button) => button
|
||||
.setButtonText("Snooze 15m")
|
||||
.onClick(async () => {
|
||||
await this.store.snooze(reminder.id, 15);
|
||||
this.render();
|
||||
}))
|
||||
.addButton((button) => button
|
||||
.setButtonText("Done")
|
||||
.onClick(async () => {
|
||||
await this.store.complete(reminder.id);
|
||||
this.render();
|
||||
}))
|
||||
.addButton((button) => button
|
||||
.setButtonText("Delete")
|
||||
.onClick(async () => {
|
||||
await this.store.remove(reminder.id);
|
||||
this.render();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { App, Modal, Notice, Setting } from "obsidian";
|
||||
import { ParsedReminderInput } from "../types";
|
||||
|
||||
export class ReminderModal extends Modal {
|
||||
private title = "";
|
||||
private body = "";
|
||||
private year = "";
|
||||
private month = "";
|
||||
private day = "";
|
||||
private hour = "";
|
||||
private minute = "";
|
||||
private channels = "";
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
private defaultChannels: string[],
|
||||
private onSubmit: (input: ParsedReminderInput) => Promise<void>,
|
||||
initialTitle = "",
|
||||
) {
|
||||
super(app);
|
||||
const initialDate = new Date(Date.now() + 60 * 60 * 1000);
|
||||
this.title = initialTitle;
|
||||
this.body = initialTitle;
|
||||
this.channels = defaultChannels.join(", ");
|
||||
this.year = String(initialDate.getFullYear());
|
||||
this.month = String(initialDate.getMonth() + 1).padStart(2, "0");
|
||||
this.day = String(initialDate.getDate()).padStart(2, "0");
|
||||
this.hour = String(initialDate.getHours()).padStart(2, "0");
|
||||
this.minute = String(Math.ceil(initialDate.getMinutes() / 5) * 5).padStart(2, "0");
|
||||
if (this.minute === "60") {
|
||||
this.minute = "00";
|
||||
this.hour = String((initialDate.getHours() + 1) % 24).padStart(2, "0");
|
||||
}
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h2", { text: "Create reminder" });
|
||||
|
||||
this.renderReminderRow(contentEl);
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName("Details")
|
||||
.setDesc("Optional longer message.")
|
||||
.addTextArea((text) => {
|
||||
text.setValue(this.body);
|
||||
text.inputEl.rows = 3;
|
||||
text.onChange((value) => {
|
||||
this.body = value;
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName("Channels")
|
||||
.setDesc("Comma separated provider IDs or types. Leave empty to use all enabled providers.")
|
||||
.addText((text) => text
|
||||
.setValue(this.channels)
|
||||
.onChange((value) => {
|
||||
this.channels = value;
|
||||
}));
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton((button) => button
|
||||
.setButtonText("Create")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
const dueAt = this.getSelectedDate();
|
||||
if (!this.title.trim() || !dueAt) {
|
||||
new Notice("Title and valid due date are required.");
|
||||
return;
|
||||
}
|
||||
|
||||
await this.onSubmit({
|
||||
title: this.title.trim(),
|
||||
body: this.body.trim(),
|
||||
dueAt: dueAt.toISOString(),
|
||||
channels: this.channels.split(",").map((value) => value.trim()).filter(Boolean),
|
||||
});
|
||||
this.close();
|
||||
}));
|
||||
}
|
||||
|
||||
private renderReminderRow(container: HTMLElement): void {
|
||||
const setting = new Setting(container)
|
||||
.setName("Reminder")
|
||||
.setDesc("Text, date, and time.");
|
||||
|
||||
setting.addText((text) => {
|
||||
text.setPlaceholder("Reminder text");
|
||||
text.setValue(this.title);
|
||||
text.onChange((value) => {
|
||||
this.title = value;
|
||||
if (!this.body || this.body === this.title) {
|
||||
this.body = value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setting.addDropdown((dropdown) => {
|
||||
for (const year of this.range(new Date().getFullYear(), new Date().getFullYear() + 3)) {
|
||||
dropdown.addOption(String(year), String(year));
|
||||
}
|
||||
dropdown.setValue(this.year);
|
||||
dropdown.onChange((value) => {
|
||||
this.year = value;
|
||||
this.ensureValidDay();
|
||||
});
|
||||
});
|
||||
|
||||
setting.addDropdown((dropdown) => {
|
||||
for (const month of this.range(1, 12)) {
|
||||
const value = String(month).padStart(2, "0");
|
||||
dropdown.addOption(value, value);
|
||||
}
|
||||
dropdown.setValue(this.month);
|
||||
dropdown.onChange((value) => {
|
||||
this.month = value;
|
||||
this.ensureValidDay();
|
||||
});
|
||||
});
|
||||
|
||||
setting.addDropdown((dropdown) => {
|
||||
for (const day of this.range(1, 31)) {
|
||||
const value = String(day).padStart(2, "0");
|
||||
dropdown.addOption(value, value);
|
||||
}
|
||||
dropdown.setValue(this.day);
|
||||
dropdown.onChange((value) => {
|
||||
this.day = value;
|
||||
});
|
||||
});
|
||||
|
||||
setting.addDropdown((dropdown) => {
|
||||
for (const hour of this.range(0, 23)) {
|
||||
const value = String(hour).padStart(2, "0");
|
||||
dropdown.addOption(value, value);
|
||||
}
|
||||
dropdown.setValue(this.hour);
|
||||
dropdown.onChange((value) => {
|
||||
this.hour = value;
|
||||
});
|
||||
});
|
||||
|
||||
setting.addDropdown((dropdown) => {
|
||||
for (const minute of this.range(0, 55, 5)) {
|
||||
const value = String(minute).padStart(2, "0");
|
||||
dropdown.addOption(value, value);
|
||||
}
|
||||
dropdown.setValue(this.minute);
|
||||
dropdown.onChange((value) => {
|
||||
this.minute = value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getSelectedDate(): Date | null {
|
||||
const date = new Date(`${this.year}-${this.month}-${this.day}T${this.hour}:${this.minute}:00`);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
private ensureValidDay(): void {
|
||||
const maxDay = new Date(Number(this.year), Number(this.month), 0).getDate();
|
||||
if (Number(this.day) > maxDay) {
|
||||
this.day = String(maxDay).padStart(2, "0");
|
||||
}
|
||||
}
|
||||
|
||||
private range(from: number, to: number, step = 1): number[] {
|
||||
const values: number[] = [];
|
||||
for (let value = from; value <= to; value += step) {
|
||||
values.push(value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { ParsedReminderInput, ReminderPriority } from "../types";
|
||||
import { parseReminderDate } from "../utils/date";
|
||||
|
||||
const INLINE_PATTERN = /@remind\(([^)]+)\)/gi;
|
||||
const BELL_PATTERN = /(?:\uD83D\uDD14|\[!\s*reminder\s*\])\s*(\d{4}-\d{2}-\d{2}(?:[ T]\d{1,2}(?::\d{2})?)?)/i;
|
||||
|
||||
export class ReminderParser {
|
||||
parseMarkdown(content: string, sourceFile?: string): ParsedReminderInput[] {
|
||||
return [
|
||||
...this.parseInline(content, sourceFile),
|
||||
...this.parseFrontmatter(content, sourceFile),
|
||||
...this.parseReminderBlocks(content, sourceFile),
|
||||
];
|
||||
}
|
||||
|
||||
private parseInline(content: string, sourceFile?: string): ParsedReminderInput[] {
|
||||
const reminders: ParsedReminderInput[] = [];
|
||||
const lines = content.split(/\r?\n/);
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
for (const match of line.matchAll(INLINE_PATTERN)) {
|
||||
const args = match[1].split(",").map((part) => part.trim()).filter(Boolean);
|
||||
const date = parseReminderDate(args[0]);
|
||||
if (!date) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawTitle = line
|
||||
.replace(INLINE_PATTERN, "")
|
||||
.replace(/^[-*]\s+\[[ xX]\]\s+/, "")
|
||||
.trim();
|
||||
|
||||
reminders.push({
|
||||
title: rawTitle || "Reminder",
|
||||
body: rawTitle,
|
||||
dueAt: date.toISOString(),
|
||||
sourceFile,
|
||||
sourceLine: index + 1,
|
||||
channels: args.slice(1),
|
||||
priority: this.parsePriority(args),
|
||||
});
|
||||
}
|
||||
|
||||
const bell = line.match(BELL_PATTERN);
|
||||
if (bell) {
|
||||
const date = parseReminderDate(bell[1]);
|
||||
if (!date) {
|
||||
return;
|
||||
}
|
||||
const title = line
|
||||
.replace(BELL_PATTERN, "")
|
||||
.replace(/^[-*]\s+\[[ xX]\]\s+/, "")
|
||||
.trim();
|
||||
reminders.push({
|
||||
title: title || "Reminder",
|
||||
body: title,
|
||||
dueAt: date.toISOString(),
|
||||
sourceFile,
|
||||
sourceLine: index + 1,
|
||||
channels: [],
|
||||
priority: "normal",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return reminders;
|
||||
}
|
||||
|
||||
private parseFrontmatter(content: string, sourceFile?: string): ParsedReminderInput[] {
|
||||
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!frontmatter) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.parseYamlLike(frontmatter[1], sourceFile, 1);
|
||||
}
|
||||
|
||||
private parseReminderBlocks(content: string, sourceFile?: string): ParsedReminderInput[] {
|
||||
const reminders: ParsedReminderInput[] = [];
|
||||
const pattern = /```reminder\r?\n([\s\S]*?)```/gi;
|
||||
|
||||
for (const match of content.matchAll(pattern)) {
|
||||
const line = content.slice(0, match.index).split(/\r?\n/).length;
|
||||
reminders.push(...this.parseYamlLike(match[1], sourceFile, line));
|
||||
}
|
||||
|
||||
return reminders;
|
||||
}
|
||||
|
||||
private parseYamlLike(input: string, sourceFile?: string, sourceLine?: number): ParsedReminderInput[] {
|
||||
const objects = input.includes("reminders:")
|
||||
? this.parseReminderList(input)
|
||||
: [this.parseKeyValueBlock(input)];
|
||||
|
||||
return objects.flatMap((item) => {
|
||||
const at = item.at ?? item.date ?? item.dueAt;
|
||||
if (!at) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const date = parseReminderDate(at);
|
||||
if (!date) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const channels = (item.notify ?? item.channels ?? "")
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return [{
|
||||
title: item.title || item.message || "Reminder",
|
||||
body: item.message || item.body || item.title || "",
|
||||
dueAt: date.toISOString(),
|
||||
sourceFile,
|
||||
sourceLine,
|
||||
channels,
|
||||
priority: this.normalizePriority(item.priority),
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
private parseReminderList(input: string): Record<string, string>[] {
|
||||
const items: Record<string, string>[] = [];
|
||||
let current: Record<string, string> | null = null;
|
||||
|
||||
for (const line of input.split(/\r?\n/)) {
|
||||
const itemStart = line.match(/^\s*-\s+(\w+):\s*(.+)$/);
|
||||
if (itemStart) {
|
||||
current = { [itemStart[1]]: itemStart[2].trim() };
|
||||
items.push(current);
|
||||
continue;
|
||||
}
|
||||
|
||||
const pair = line.match(/^\s+(\w+):\s*(.+)$/);
|
||||
if (pair && current) {
|
||||
current[pair[1]] = pair[2].trim();
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private parseKeyValueBlock(input: string): Record<string, string> {
|
||||
const values: Record<string, string> = {};
|
||||
const lines = input.split(/\r?\n/);
|
||||
let inReminderObject = false;
|
||||
let lastListKey = "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (/^\s*reminder:\s*$/.test(line)) {
|
||||
inReminderObject = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const listItem = line.match(/^\s*-\s+(.+)$/);
|
||||
if (listItem && lastListKey) {
|
||||
values[lastListKey] = [values[lastListKey], listItem[1].trim()].filter(Boolean).join(",");
|
||||
continue;
|
||||
}
|
||||
|
||||
const pair = line.match(/^\s*(\w+):\s*(.*)$/);
|
||||
if (pair && (inReminderObject || !line.startsWith(" "))) {
|
||||
values[pair[1]] = pair[2].trim();
|
||||
lastListKey = pair[1];
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private parsePriority(values: string[]): ReminderPriority {
|
||||
const priority = values.find((value) => /^(low|normal|high)$/i.test(value));
|
||||
return this.normalizePriority(priority);
|
||||
}
|
||||
|
||||
private normalizePriority(value?: string): ReminderPriority {
|
||||
if (value === "low" || value === "high" || value === "normal") {
|
||||
return value;
|
||||
}
|
||||
return "normal";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Notice, TFile, Vault } from "obsidian";
|
||||
import { NotificationProvider, ProviderType, Reminder } from "../types";
|
||||
import { ReminderParser } from "./ReminderParser";
|
||||
import { ReminderStore } from "./ReminderStore";
|
||||
|
||||
export class ReminderScheduler {
|
||||
private timer: number | null = null;
|
||||
private parser = new ReminderParser();
|
||||
|
||||
constructor(
|
||||
private vault: Vault,
|
||||
private store: ReminderStore,
|
||||
private providers: Map<ProviderType, NotificationProvider>,
|
||||
) {}
|
||||
|
||||
start(): void {
|
||||
this.stop();
|
||||
const interval = Math.max(10, this.store.getSettings().checkIntervalSeconds) * 1000;
|
||||
this.timer = window.setInterval(() => {
|
||||
void this.tick();
|
||||
}, interval);
|
||||
void this.tick();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.timer !== null) {
|
||||
window.clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
async tick(): Promise<void> {
|
||||
for (const reminder of this.store.getDue()) {
|
||||
await this.sendReminder(reminder);
|
||||
}
|
||||
}
|
||||
|
||||
async scanFile(file: TFile): Promise<number> {
|
||||
const content = await this.vault.read(file);
|
||||
const parsed = this.parser.parseMarkdown(content, file.path);
|
||||
return this.store.addMany(parsed);
|
||||
}
|
||||
|
||||
async scanVault(): Promise<number> {
|
||||
let count = 0;
|
||||
const files = this.vault.getMarkdownFiles();
|
||||
for (const file of files) {
|
||||
count += await this.scanFile(file);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private async sendReminder(reminder: Reminder): Promise<void> {
|
||||
const enabledProviders = this.store.getSettings().notificationProviders.filter((provider) => provider.enabled);
|
||||
const configs = enabledProviders.filter((provider) =>
|
||||
reminder.channels.includes(provider.id) || reminder.channels.includes(provider.type)
|
||||
);
|
||||
|
||||
const selected = configs.length
|
||||
? configs
|
||||
: reminder.channels.length === 0
|
||||
? enabledProviders
|
||||
: [];
|
||||
|
||||
if (!selected.length) {
|
||||
const message = reminder.channels.length
|
||||
? `Reminder has no matching enabled provider: ${reminder.title}`
|
||||
: `Reminder has no enabled providers: ${reminder.title}`;
|
||||
new Notice(message);
|
||||
await this.store.markFailed(reminder.id, message);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const config of selected) {
|
||||
const provider = this.providers.get(config.type);
|
||||
if (!provider) {
|
||||
throw new Error(`Unknown provider: ${config.type}`);
|
||||
}
|
||||
await provider.send({
|
||||
title: reminder.title,
|
||||
message: reminder.body || reminder.title,
|
||||
sourceFile: reminder.sourceFile,
|
||||
sourceLine: reminder.sourceLine,
|
||||
priority: reminder.priority,
|
||||
dueAt: reminder.dueAt,
|
||||
}, config);
|
||||
}
|
||||
await this.store.markSent(reminder.id);
|
||||
} catch (error) {
|
||||
await this.store.markFailed(reminder.id, error);
|
||||
new Notice(`Reminder failed: ${reminder.title}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { Plugin } from "obsidian";
|
||||
import { ParsedReminderInput, PluginData, PluginSettings, Reminder } from "../types";
|
||||
import { createReminderId } from "../utils/ids";
|
||||
import { createDefaultSettings } from "../settings/templates";
|
||||
|
||||
export class ReminderStore {
|
||||
private data: PluginData = {
|
||||
settings: createDefaultSettings(),
|
||||
reminders: [],
|
||||
};
|
||||
|
||||
constructor(private plugin: Plugin) {}
|
||||
|
||||
async load(): Promise<void> {
|
||||
const loaded = await this.plugin.loadData() as Partial<PluginData> | null;
|
||||
const defaults = createDefaultSettings();
|
||||
this.data = {
|
||||
settings: {
|
||||
...defaults,
|
||||
...(loaded?.settings ?? {}),
|
||||
notificationProviders: loaded?.settings?.notificationProviders ?? defaults.notificationProviders,
|
||||
},
|
||||
reminders: loaded?.reminders ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
await this.plugin.saveData(this.data);
|
||||
}
|
||||
|
||||
getSettings(): PluginSettings {
|
||||
return this.data.settings;
|
||||
}
|
||||
|
||||
async updateSettings(settings: PluginSettings): Promise<void> {
|
||||
this.data.settings = settings;
|
||||
await this.save();
|
||||
}
|
||||
|
||||
getAll(): Reminder[] {
|
||||
return [...this.data.reminders].sort((a, b) => a.dueAt.localeCompare(b.dueAt));
|
||||
}
|
||||
|
||||
getDue(now = new Date()): Reminder[] {
|
||||
return this.data.reminders.filter((reminder) =>
|
||||
(reminder.status === "pending" || reminder.status === "snoozed" || reminder.status === "failed")
|
||||
&& new Date(reminder.dueAt).getTime() <= now.getTime()
|
||||
);
|
||||
}
|
||||
|
||||
async add(input: ParsedReminderInput, defaultChannels: string[]): Promise<Reminder> {
|
||||
const now = new Date().toISOString();
|
||||
const reminder: Reminder = {
|
||||
id: createReminderId([input.sourceFile ?? "manual", String(input.sourceLine ?? ""), input.title, input.dueAt]),
|
||||
title: input.title,
|
||||
body: input.body,
|
||||
dueAt: input.dueAt,
|
||||
sourceFile: input.sourceFile,
|
||||
sourceLine: input.sourceLine,
|
||||
completed: false,
|
||||
channels: input.channels.length ? input.channels : defaultChannels,
|
||||
priority: input.priority ?? "normal",
|
||||
status: "pending",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
const existing = this.data.reminders.find((item) => item.id === reminder.id);
|
||||
if (existing) {
|
||||
Object.assign(existing, {
|
||||
...reminder,
|
||||
status: existing.status,
|
||||
completed: existing.completed,
|
||||
createdAt: existing.createdAt,
|
||||
updatedAt: now,
|
||||
});
|
||||
await this.save();
|
||||
return existing;
|
||||
}
|
||||
|
||||
this.data.reminders.push(reminder);
|
||||
await this.save();
|
||||
return reminder;
|
||||
}
|
||||
|
||||
async addMany(inputs: ParsedReminderInput[]): Promise<number> {
|
||||
let count = 0;
|
||||
for (const input of inputs) {
|
||||
await this.add(input, []);
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
async markSent(id: string): Promise<void> {
|
||||
const reminder = this.find(id);
|
||||
if (!reminder) {
|
||||
return;
|
||||
}
|
||||
|
||||
reminder.status = this.data.settings.afterSend === "mark-done" ? "done" : "sent";
|
||||
reminder.completed = this.data.settings.afterSend === "mark-done";
|
||||
reminder.updatedAt = new Date().toISOString();
|
||||
reminder.lastError = undefined;
|
||||
await this.save();
|
||||
}
|
||||
|
||||
async markFailed(id: string, error: unknown): Promise<void> {
|
||||
const reminder = this.find(id);
|
||||
if (!reminder) {
|
||||
return;
|
||||
}
|
||||
|
||||
reminder.status = "failed";
|
||||
reminder.lastError = error instanceof Error ? error.message : String(error);
|
||||
reminder.updatedAt = new Date().toISOString();
|
||||
await this.save();
|
||||
}
|
||||
|
||||
async complete(id: string): Promise<void> {
|
||||
const reminder = this.find(id);
|
||||
if (!reminder) {
|
||||
return;
|
||||
}
|
||||
reminder.completed = true;
|
||||
reminder.status = "done";
|
||||
reminder.updatedAt = new Date().toISOString();
|
||||
await this.save();
|
||||
}
|
||||
|
||||
async snooze(id: string, minutes: number): Promise<void> {
|
||||
const reminder = this.find(id);
|
||||
if (!reminder) {
|
||||
return;
|
||||
}
|
||||
reminder.dueAt = new Date(Date.now() + minutes * 60_000).toISOString();
|
||||
reminder.status = "snoozed";
|
||||
reminder.updatedAt = new Date().toISOString();
|
||||
await this.save();
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
this.data.reminders = this.data.reminders.filter((reminder) => reminder.id !== id);
|
||||
await this.save();
|
||||
}
|
||||
|
||||
private find(id: string): Reminder | undefined {
|
||||
return this.data.reminders.find((reminder) => reminder.id === id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import { App, Notice, PluginSettingTab, Setting } from "obsidian";
|
||||
import ReminderNotifierPlugin from "../main";
|
||||
import { NotificationProviderConfig, ProviderType } from "../types";
|
||||
import { cloneTemplate, PROVIDER_TEMPLATES } from "./templates";
|
||||
|
||||
export class ReminderNotifierSettingTab extends PluginSettingTab {
|
||||
constructor(app: App, private plugin: ReminderNotifierPlugin) {
|
||||
super(app, plugin);
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
const settings = this.plugin.store.getSettings();
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl("h2", { text: "Reminder Notifier" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Scan vault on startup")
|
||||
.addToggle((toggle) => toggle
|
||||
.setValue(settings.scanVaultOnStartup)
|
||||
.onChange(async (value) => {
|
||||
settings.scanVaultOnStartup = value;
|
||||
await this.plugin.store.updateSettings(settings);
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Scan changed notes")
|
||||
.addToggle((toggle) => toggle
|
||||
.setValue(settings.scanOnFileChange)
|
||||
.onChange(async (value) => {
|
||||
settings.scanOnFileChange = value;
|
||||
await this.plugin.store.updateSettings(settings);
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Check interval")
|
||||
.setDesc("Seconds between reminder checks.")
|
||||
.addText((text) => text
|
||||
.setValue(String(settings.checkIntervalSeconds))
|
||||
.onChange(async (value) => {
|
||||
settings.checkIntervalSeconds = Math.max(10, Number(value) || 60);
|
||||
await this.plugin.store.updateSettings(settings);
|
||||
this.plugin.restartScheduler();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Default channels")
|
||||
.setDesc("Comma separated provider IDs or types. Empty means every enabled provider.")
|
||||
.addText((text) => text
|
||||
.setValue(settings.defaultChannels.join(", "))
|
||||
.onChange(async (value) => {
|
||||
settings.defaultChannels = value.split(",").map((part) => part.trim()).filter(Boolean);
|
||||
await this.plugin.store.updateSettings(settings);
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("After send")
|
||||
.addDropdown((dropdown) => dropdown
|
||||
.addOption("mark-sent", "Mark as sent")
|
||||
.addOption("mark-done", "Mark as done")
|
||||
.addOption("keep", "Keep pending")
|
||||
.setValue(settings.afterSend)
|
||||
.onChange(async (value) => {
|
||||
settings.afterSend = value as typeof settings.afterSend;
|
||||
await this.plugin.store.updateSettings(settings);
|
||||
}));
|
||||
|
||||
containerEl.createEl("h3", { text: "Notification providers" });
|
||||
|
||||
let selectedTemplate: ProviderType = PROVIDER_TEMPLATES[0].type;
|
||||
new Setting(containerEl)
|
||||
.setName("Add from template")
|
||||
.addDropdown((dropdown) => {
|
||||
for (const template of PROVIDER_TEMPLATES) {
|
||||
dropdown.addOption(template.type, template.name);
|
||||
}
|
||||
dropdown.onChange((value) => {
|
||||
selectedTemplate = value as ProviderType;
|
||||
});
|
||||
})
|
||||
.addButton((button) => button
|
||||
.setButtonText("Add provider")
|
||||
.onClick(async () => {
|
||||
const provider = cloneTemplate(selectedTemplate);
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
settings.notificationProviders.push(provider);
|
||||
await this.plugin.store.updateSettings(settings);
|
||||
this.display();
|
||||
}));
|
||||
|
||||
for (const provider of settings.notificationProviders) {
|
||||
this.renderProvider(provider);
|
||||
}
|
||||
}
|
||||
|
||||
private renderProvider(provider: NotificationProviderConfig): void {
|
||||
const settings = this.plugin.store.getSettings();
|
||||
const wrapper = this.containerEl.createDiv({ cls: "reminder-notifier-provider" });
|
||||
wrapper.createEl("h4", { text: provider.name });
|
||||
|
||||
new Setting(wrapper)
|
||||
.setName("Enabled")
|
||||
.addToggle((toggle) => toggle
|
||||
.setValue(provider.enabled)
|
||||
.onChange(async (value) => {
|
||||
provider.enabled = value;
|
||||
await this.plugin.store.updateSettings(settings);
|
||||
}));
|
||||
|
||||
new Setting(wrapper)
|
||||
.setName("ID")
|
||||
.setDesc("Use this value in @remind(..., provider-id).")
|
||||
.addText((text) => text
|
||||
.setValue(provider.id)
|
||||
.onChange(async (value) => {
|
||||
provider.id = value.trim();
|
||||
await this.plugin.store.updateSettings(settings);
|
||||
}));
|
||||
|
||||
new Setting(wrapper)
|
||||
.setName("Name")
|
||||
.addText((text) => text
|
||||
.setValue(provider.name)
|
||||
.onChange(async (value) => {
|
||||
provider.name = value;
|
||||
await this.plugin.store.updateSettings(settings);
|
||||
}));
|
||||
|
||||
new Setting(wrapper)
|
||||
.setName("Config JSON")
|
||||
.setDesc("Secrets are stored locally in Obsidian plugin data.")
|
||||
.addTextArea((text) => {
|
||||
text.inputEl.addClass("reminder-notifier-json");
|
||||
text.setValue(JSON.stringify(provider.config, null, 2));
|
||||
text.onChange(async (value) => {
|
||||
try {
|
||||
provider.config = JSON.parse(value) as Record<string, unknown>;
|
||||
await this.plugin.store.updateSettings(settings);
|
||||
} catch {
|
||||
// Allow editing incomplete JSON without immediately discarding input.
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(wrapper)
|
||||
.addButton((button) => button
|
||||
.setButtonText("Test notification")
|
||||
.onClick(async () => {
|
||||
try {
|
||||
await this.plugin.testProvider(provider);
|
||||
new Notice("Test notification sent.");
|
||||
} catch (error) {
|
||||
new Notice(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}))
|
||||
.addButton((button) => button
|
||||
.setButtonText("Delete")
|
||||
.onClick(async () => {
|
||||
settings.notificationProviders = settings.notificationProviders.filter((item) => item !== provider);
|
||||
await this.plugin.store.updateSettings(settings);
|
||||
this.display();
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { NotificationProviderConfig, PluginSettings } from "../types";
|
||||
|
||||
export const PROVIDER_TEMPLATES: NotificationProviderConfig[] = [
|
||||
{
|
||||
id: "local",
|
||||
type: "local",
|
||||
name: "Local notice",
|
||||
enabled: true,
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: "telegram",
|
||||
type: "telegram",
|
||||
name: "Telegram Bot",
|
||||
enabled: false,
|
||||
config: {
|
||||
botToken: "123456:ABCDEF_your_bot_token",
|
||||
chatId: "123456789",
|
||||
parseMode: "Markdown",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "ntfy",
|
||||
type: "ntfy",
|
||||
name: "ntfy.sh",
|
||||
enabled: false,
|
||||
config: {
|
||||
serverUrl: "https://ntfy.sh",
|
||||
topic: "my-obsidian-reminders",
|
||||
token: "",
|
||||
priority: 3,
|
||||
tags: ["calendar", "obsidian"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "discord",
|
||||
type: "discord",
|
||||
name: "Discord Webhook",
|
||||
enabled: false,
|
||||
config: {
|
||||
webhookUrl: "https://discord.com/api/webhooks/...",
|
||||
username: "Obsidian Reminder",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "webhook",
|
||||
type: "webhook",
|
||||
name: "Generic Webhook",
|
||||
enabled: false,
|
||||
config: {
|
||||
method: "POST",
|
||||
url: "https://example.com/reminder",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
bodyTemplate: {
|
||||
title: "{{title}}",
|
||||
message: "{{message}}",
|
||||
dueAt: "{{dueAt}}",
|
||||
sourceFile: "{{sourceFile}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_SETTINGS: PluginSettings = {
|
||||
defaultChannels: [],
|
||||
scanVaultOnStartup: true,
|
||||
scanOnFileChange: true,
|
||||
checkIntervalSeconds: 60,
|
||||
afterSend: "mark-sent",
|
||||
reminderSyntax: "@remind(YYYY-MM-DD HH:mm, provider)",
|
||||
notificationProviders: [PROVIDER_TEMPLATES[0]],
|
||||
};
|
||||
|
||||
export function createDefaultSettings(): PluginSettings {
|
||||
return JSON.parse(JSON.stringify(DEFAULT_SETTINGS)) as PluginSettings;
|
||||
}
|
||||
|
||||
export function cloneTemplate(type: string): NotificationProviderConfig | null {
|
||||
const template = PROVIDER_TEMPLATES.find((item) => item.type === type);
|
||||
if (!template) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const suffix = Date.now().toString(36);
|
||||
return {
|
||||
...template,
|
||||
id: `${template.type}-${suffix}`,
|
||||
config: JSON.parse(JSON.stringify(template.config)) as Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
export type ReminderStatus = "pending" | "sent" | "failed" | "snoozed" | "cancelled" | "done";
|
||||
export type ReminderPriority = "low" | "normal" | "high";
|
||||
export type ProviderType = "local" | "telegram" | "ntfy" | "webhook" | "discord";
|
||||
|
||||
export interface Reminder {
|
||||
id: string;
|
||||
title: string;
|
||||
body?: string;
|
||||
dueAt: string;
|
||||
sourceFile?: string;
|
||||
sourceLine?: number;
|
||||
completed: boolean;
|
||||
channels: string[];
|
||||
priority: ReminderPriority;
|
||||
status: ReminderStatus;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastError?: string;
|
||||
}
|
||||
|
||||
export interface NotificationPayload {
|
||||
title: string;
|
||||
message: string;
|
||||
sourceFile?: string;
|
||||
sourceLine?: number;
|
||||
priority?: ReminderPriority;
|
||||
url?: string;
|
||||
dueAt: string;
|
||||
}
|
||||
|
||||
export interface NotificationProviderConfig {
|
||||
id: string;
|
||||
type: ProviderType;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface PluginSettings {
|
||||
defaultChannels: string[];
|
||||
scanVaultOnStartup: boolean;
|
||||
scanOnFileChange: boolean;
|
||||
checkIntervalSeconds: number;
|
||||
afterSend: "keep" | "mark-sent" | "mark-done";
|
||||
reminderSyntax: string;
|
||||
notificationProviders: NotificationProviderConfig[];
|
||||
}
|
||||
|
||||
export interface PluginData {
|
||||
settings: PluginSettings;
|
||||
reminders: Reminder[];
|
||||
}
|
||||
|
||||
export interface ParsedReminderInput {
|
||||
title: string;
|
||||
body?: string;
|
||||
dueAt: string;
|
||||
sourceFile?: string;
|
||||
sourceLine?: number;
|
||||
channels: string[];
|
||||
priority?: ReminderPriority;
|
||||
}
|
||||
|
||||
export interface NotificationProvider {
|
||||
id: ProviderType;
|
||||
name: string;
|
||||
send(payload: NotificationPayload, config: NotificationProviderConfig): Promise<void>;
|
||||
test(config: NotificationProviderConfig): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
const WEEKDAYS = new Map([
|
||||
["sunday", 0],
|
||||
["monday", 1],
|
||||
["tuesday", 2],
|
||||
["wednesday", 3],
|
||||
["thursday", 4],
|
||||
["friday", 5],
|
||||
["saturday", 6],
|
||||
]);
|
||||
|
||||
export function parseReminderDate(input: string, now = new Date()): Date | null {
|
||||
const value = input.trim();
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const relative = value.match(/^\+(\d+)\s*(m|min|minute|minutes|h|hour|hours|d|day|days)$/i);
|
||||
if (relative) {
|
||||
const amount = Number(relative[1]);
|
||||
const unit = relative[2].toLowerCase();
|
||||
const millis = unit.startsWith("m")
|
||||
? amount * 60_000
|
||||
: unit.startsWith("h")
|
||||
? amount * 3_600_000
|
||||
: amount * 86_400_000;
|
||||
return new Date(now.getTime() + millis);
|
||||
}
|
||||
|
||||
const tomorrow = value.match(/^tomorrow(?:\s+(\d{1,2})(?::(\d{2}))?)?$/i);
|
||||
if (tomorrow) {
|
||||
const date = new Date(now);
|
||||
date.setDate(date.getDate() + 1);
|
||||
date.setHours(Number(tomorrow[1] ?? 9), Number(tomorrow[2] ?? 0), 0, 0);
|
||||
return date;
|
||||
}
|
||||
|
||||
const nextWeekday = value.match(/^next\s+(sunday|monday|tuesday|wednesday|thursday|friday|saturday)(?:\s+(\d{1,2})(?::(\d{2}))?)?$/i);
|
||||
if (nextWeekday) {
|
||||
const wanted = WEEKDAYS.get(nextWeekday[1].toLowerCase());
|
||||
if (wanted === undefined) {
|
||||
return null;
|
||||
}
|
||||
const date = new Date(now);
|
||||
const days = (wanted - date.getDay() + 7) || 7;
|
||||
date.setDate(date.getDate() + days);
|
||||
date.setHours(Number(nextWeekday[2] ?? 9), Number(nextWeekday[3] ?? 0), 0, 0);
|
||||
return date;
|
||||
}
|
||||
|
||||
const simple = value.match(/^(\d{4}-\d{2}-\d{2})(?:[ T](\d{1,2})(?::(\d{2}))?)?$/);
|
||||
if (simple) {
|
||||
const hours = simple[2] ?? "9";
|
||||
const minutes = simple[3] ?? "0";
|
||||
const date = new Date(`${simple[1]}T${hours.padStart(2, "0")}:${minutes.padStart(2, "0")}:00`);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
const parsed = new Date(value);
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||||
}
|
||||
|
||||
export function formatDateTime(value: string): string {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value;
|
||||
}
|
||||
return date.toLocaleString();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function createReminderId(parts: string[]): string {
|
||||
const source = parts.join("|");
|
||||
let hash = 0;
|
||||
for (let index = 0; index < source.length; index += 1) {
|
||||
hash = ((hash << 5) - hash + source.charCodeAt(index)) | 0;
|
||||
}
|
||||
return `rem_${Math.abs(hash).toString(36)}`;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { NotificationPayload } from "../types";
|
||||
|
||||
export function renderTemplate(value: unknown, payload: NotificationPayload): unknown {
|
||||
if (typeof value === "string") {
|
||||
return Object.entries({
|
||||
"{{title}}": payload.title,
|
||||
"{{message}}": payload.message,
|
||||
"{{body}}": payload.message,
|
||||
"{{file}}": payload.sourceFile ?? "",
|
||||
"{{sourceFile}}": payload.sourceFile ?? "",
|
||||
"{{sourceLine}}": payload.sourceLine?.toString() ?? "",
|
||||
"{{priority}}": payload.priority ?? "normal",
|
||||
"{{dueAt}}": payload.dueAt,
|
||||
"{{time}}": payload.dueAt,
|
||||
"{{url}}": payload.url ?? "",
|
||||
}).reduce((result, [token, replacement]) => result.split(token).join(replacement), value);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => renderTemplate(item, payload));
|
||||
}
|
||||
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value as Record<string, unknown>).map(([key, item]) => [key, renderTemplate(item, payload)])
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function asString(value: unknown, fallback = ""): string {
|
||||
return typeof value === "string" ? value : fallback;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
.reminder-notifier-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.reminder-notifier-item {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.reminder-notifier-item-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.reminder-notifier-item-meta {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-smaller);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.reminder-notifier-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.reminder-notifier-provider {
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.reminder-notifier-json {
|
||||
min-height: 110px;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES2018",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES2018"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"0.1.0": "1.5.0"
|
||||
}
|
||||
Reference in New Issue
Block a user