aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.env.sample7
-rw-r--r--.gitignore1
-rw-r--r--README.md73
-rw-r--r--package-lock.json1008
-rw-r--r--package.json8
-rw-r--r--src/bot.ts49
-rw-r--r--src/config.ts30
-rw-r--r--src/db.ts74
-rw-r--r--src/index.ts51
-rw-r--r--src/middleware.ts10
-rw-r--r--src/sheets.ts203
-rw-r--r--src/tasks.ts39
-rw-r--r--src/tg.ts119
13 files changed, 306 insertions, 1366 deletions
diff --git a/.env.sample b/.env.sample
index e0e4e42..3436eeb 100644
--- a/.env.sample
+++ b/.env.sample
@@ -1,6 +1,3 @@
-PORT=3000
-SHEETS_SPREADSHEET_ID=
-SHEETS_RANGE=
TELEGRAM_BOT_TOKEN=
-G_SA_JSON_B64=
-SQLITE_PATH=
+# TZ=Europe/Helsinki
+# SQLITE_PATH=./data/hommabot.db
diff --git a/.gitignore b/.gitignore
index 9106da9..f71223e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,3 @@ node_modules/
build/
.vscode/
.env
-sa_key.json
diff --git a/README.md b/README.md
index 5d7ddc1..e2d95e7 100644
--- a/README.md
+++ b/README.md
@@ -1,85 +1,60 @@
# hommabot2
-Rewrite of hommabot (Python3) in TS.
+Telegram bot for recurring household tasks and shopping lists.
-1. Reads Google Sheets for recurring tasks
-2. Determines which tasks fall in the current week
-3. Broadcasts a Telegram message to all active chats
-4. Updates the "last done" column for tasks that became due
+## Features
-## Persistence
+- **Recurring tasks** — add tasks with intervals (days/weeks/months/years), get weekly Monday broadcasts of what's due
+- **Shopping list** — shared list with inline delete buttons
+- **Persistence** — embedded SQLite (STRICT tables), no external services needed
-- Embedded SQLite (STRICT tables) auto-initialized on first run
-- Single connection (read + write)
+## Commands
+
+| Command | Description |
+|---------|-------------|
+| `/start` | Register chat for weekly broadcasts |
+| `/stop` | Unregister chat |
+| `/newtask <interval> <name>` | Add recurring task (e.g. `/newtask 2vk Imurointi`) |
+| `/tasks` | Show all tasks with ✅ done / 🗑️ delete buttons |
+| `/done <id>` | Mark task done |
+| `/edittask <id> <interval> <name>` | Edit a task |
+| `/add <item>` | Add item to shopping list |
+| `/list` | Show shopping list with ❌ delete buttons |
+
+**Intervals:** `pv` (day), `vk` (week), `kk` (month), `v` (year). Prefix with number, e.g. `2vk` = every 2 weeks.
## Setup
-1. Install dependencies (includes better-sqlite3):
+1. Install dependencies:
```sh
npm install
```
-2. Create/update your `.env` (minimum required):
+2. Create `.env`:
```sh
TELEGRAM_BOT_TOKEN=...
-SHEETS_SPREADSHEET_ID=...
-SHEETS_RANGE=...
-G_SA_JSON_B64=... # Base64-encoded Google Service Account JSON
# Optional:
# SQLITE_PATH=./data/hommabot.db
```
-3. (Optional) Seed permitted Telegram user IDs (only these can /start to register chats):
+3. (Optional) Seed permitted users:
```sh
sqlite3 data/hommabot.db "INSERT OR IGNORE INTO permitted_users (id) VALUES (123456789);"
```
-Repeat with additional IDs as needed.
-
-4. (Optional) Pre-register an active chat manually (normally added via /start):
-
-```sh
-sqlite3 data/hommabot.db "INSERT OR IGNORE INTO active_chats (id) VALUES (-1001234567890);"
-```
-
## Running
-Direct TypeScript execution:
-
```sh
npm start
```
+The bot runs as a long-lived process. Weekly task broadcasts fire every Monday at 09:00 Finland time.
+
## Building
```sh
npm run build
```
-
-## Automating (cron example)
-
-Run every Monday at 08:00:
-
-```
-0 8 * * 1 /usr/bin/node /path/to/hommabot2/build/index.js >> /path/to/hommabot2/hommabot.log 2>&1
-```
-
-## Inspecting the database
-
-```sh
-sqlite3 data/hommabot.db ".tables"
-sqlite3 data/hommabot.db "SELECT * FROM active_chats;"
-sqlite3 data/hommabot.db "SELECT * FROM permitted_users;"
-```
-
-## Notes
-
-- To add new permitted users at any time, insert into `permitted_users`.
-- To clear active chats:
-
-```sh
-sqlite3 data/hommabot.db "DELETE FROM active_chats;"
-```
diff --git a/package-lock.json b/package-lock.json
index f8fb6a9..3a4b4c4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,7 +1,7 @@
{
"name": "hommabot2",
"version": "2.0.0",
- "lockfileVersion": 2,
+ "lockfileVersion": 3,
"requires": true,
"packages": {
"": {
@@ -12,13 +12,10 @@
"better-sqlite3": "12.9.0",
"date-fns": "4.1.0",
"dotenv": "17.4.2",
- "googleapis": "171.4.0",
- "got": "15.0.3",
"telegraf": "4.16.3"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
- "@types/got": "9.6.12",
"@types/node": "25.6.0"
}
},
@@ -1588,1008 +1585,5 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}
- },
- "dependencies": {
- "@keyv/serialize": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
- "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="
- },
- "@sec-ant/readable-stream": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
- "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="
- },
- "@sindresorhus/is": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-8.0.0.tgz",
- "integrity": "sha512-YvOsokBE5NsyHuXMnwEVB7lgFk6lX8F4OXCruYVgFII0hUHof0RGUvhMoabVt9hRqbg+qVzayzEG24RCcxcYeA=="
- },
- "@telegraf/types": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/@telegraf/types/-/types-7.1.0.tgz",
- "integrity": "sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw=="
- },
- "@types/better-sqlite3": {
- "version": "7.6.13",
- "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
- "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
- "dev": true,
- "requires": {
- "@types/node": "*"
- }
- },
- "@types/got": {
- "version": "9.6.12",
- "resolved": "https://registry.npmjs.org/@types/got/-/got-9.6.12.tgz",
- "integrity": "sha512-X4pj/HGHbXVLqTpKjA2ahI4rV/nNBc9mGO2I/0CgAra+F2dKgMXnENv2SRpemScBzBAI4vMelIVYViQxlSE6xA==",
- "dev": true,
- "requires": {
- "@types/node": "*",
- "@types/tough-cookie": "*",
- "form-data": "^2.5.0"
- }
- },
- "@types/http-cache-semantics": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
- "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="
- },
- "@types/node": {
- "version": "25.6.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
- "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
- "dev": true,
- "requires": {
- "undici-types": "~7.19.0"
- }
- },
- "@types/tough-cookie": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz",
- "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==",
- "dev": true
- },
- "abort-controller": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
- "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
- "requires": {
- "event-target-shim": "^5.0.0"
- }
- },
- "agent-base": {
- "version": "7.1.4",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
- "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="
- },
- "asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
- "dev": true
- },
- "base64-js": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
- },
- "better-sqlite3": {
- "version": "12.9.0",
- "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz",
- "integrity": "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==",
- "requires": {
- "bindings": "^1.5.0",
- "prebuild-install": "^7.1.1"
- }
- },
- "bignumber.js": {
- "version": "9.3.1",
- "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
- "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="
- },
- "bindings": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
- "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
- "requires": {
- "file-uri-to-path": "1.0.0"
- }
- },
- "bl": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
- "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
- "requires": {
- "buffer": "^5.5.0",
- "inherits": "^2.0.4",
- "readable-stream": "^3.4.0"
- }
- },
- "buffer": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "requires": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.1.13"
- }
- },
- "buffer-alloc": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
- "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
- "requires": {
- "buffer-alloc-unsafe": "^1.1.0",
- "buffer-fill": "^1.0.0"
- }
- },
- "buffer-alloc-unsafe": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
- "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
- },
- "buffer-equal-constant-time": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
- "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
- },
- "buffer-fill": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
- "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw="
- },
- "byte-counter": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz",
- "integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ=="
- },
- "cacheable-lookup": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
- "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="
- },
- "cacheable-request": {
- "version": "13.0.18",
- "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.18.tgz",
- "integrity": "sha512-rFWadDRKJs3s2eYdXlGggnBZKG7MTblkFBB0YllFds+UYnfogDp2wcR6JN97FhRkHTvq59n2vhNoHNZn29dh/Q==",
- "requires": {
- "@types/http-cache-semantics": "^4.0.4",
- "get-stream": "^9.0.1",
- "http-cache-semantics": "^4.2.0",
- "keyv": "^5.5.5",
- "mimic-response": "^4.0.0",
- "normalize-url": "^8.1.1",
- "responselike": "^4.0.2"
- }
- },
- "call-bind-apply-helpers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "requires": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
- }
- },
- "call-bound": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
- "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
- "requires": {
- "call-bind-apply-helpers": "^1.0.2",
- "get-intrinsic": "^1.3.0"
- }
- },
- "chownr": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
- },
- "chunk-data": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/chunk-data/-/chunk-data-0.1.0.tgz",
- "integrity": "sha512-zFyPtyC0SZ6Zu79b9sOYtXZcgrsXe0RpePrzRyj52hYVFG1+Rk6rBqjjOEk+GNQwc3PIX+86teQMok970pod1g=="
- },
- "combined-stream": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
- "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "dev": true,
- "requires": {
- "delayed-stream": "~1.0.0"
- }
- },
- "data-uri-to-buffer": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
- "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="
- },
- "date-fns": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
- "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
- },
- "debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "requires": {
- "ms": "^2.1.3"
- }
- },
- "decompress-response": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
- "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
- "requires": {
- "mimic-response": "^3.1.0"
- },
- "dependencies": {
- "mimic-response": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
- "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
- }
- }
- },
- "deep-extend": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
- },
- "delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
- "dev": true
- },
- "detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="
- },
- "dotenv": {
- "version": "17.4.2",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
- "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="
- },
- "dunder-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "requires": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-errors": "^1.3.0",
- "gopd": "^1.2.0"
- }
- },
- "ecdsa-sig-formatter": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
- "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
- "requires": {
- "safe-buffer": "^5.0.1"
- }
- },
- "end-of-stream": {
- "version": "1.4.4",
- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
- "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
- "requires": {
- "once": "^1.4.0"
- }
- },
- "es-define-property": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
- },
- "es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
- },
- "es-object-atoms": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "requires": {
- "es-errors": "^1.3.0"
- }
- },
- "event-target-shim": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
- "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
- },
- "expand-template": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
- "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="
- },
- "extend": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
- "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
- },
- "fetch-blob": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
- "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
- "requires": {
- "node-domexception": "^1.0.0",
- "web-streams-polyfill": "^3.0.3"
- }
- },
- "file-uri-to-path": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
- "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
- },
- "form-data": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
- "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
- "dev": true,
- "requires": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.6",
- "mime-types": "^2.1.12"
- }
- },
- "formdata-polyfill": {
- "version": "4.0.10",
- "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
- "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
- "requires": {
- "fetch-blob": "^3.1.2"
- }
- },
- "fs-constants": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
- "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
- },
- "function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
- },
- "gaxios": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.2.tgz",
- "integrity": "sha512-/Szrn8nr+2TsQT1Gp8iIe/BEytJmbyfrbFh419DfGQSkEgNEhbPi7JRJuughjkTzPWgU9gBQf5AVu3DbHt0OXA==",
- "requires": {
- "extend": "^3.0.2",
- "https-proxy-agent": "^7.0.1",
- "node-fetch": "^3.3.2"
- },
- "dependencies": {
- "node-fetch": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
- "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
- "requires": {
- "data-uri-to-buffer": "^4.0.0",
- "fetch-blob": "^3.1.4",
- "formdata-polyfill": "^4.0.10"
- }
- }
- }
- },
- "gcp-metadata": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz",
- "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==",
- "requires": {
- "gaxios": "^7.0.0",
- "google-logging-utils": "^1.0.0",
- "json-bigint": "^1.0.0"
- }
- },
- "get-intrinsic": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
- "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "requires": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "function-bind": "^1.1.2",
- "get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "math-intrinsics": "^1.1.0"
- }
- },
- "get-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "requires": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
- }
- },
- "get-stream": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
- "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
- "requires": {
- "@sec-ant/readable-stream": "^0.4.1",
- "is-stream": "^4.0.1"
- }
- },
- "github-from-package": {
- "version": "0.0.0",
- "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
- "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
- },
- "google-auth-library": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.4.0.tgz",
- "integrity": "sha512-CmIrSy1bqMQUsPmA9+hcSbAXL80cFhu40cGMUjCaLpNKVzzvi+0uAHq8GNZxkoGYIsTX4ZQ7e4aInAqWxgn4fg==",
- "requires": {
- "base64-js": "^1.3.0",
- "ecdsa-sig-formatter": "^1.0.11",
- "gaxios": "^7.0.0",
- "gcp-metadata": "^7.0.0",
- "google-logging-utils": "^1.0.0",
- "gtoken": "^8.0.0",
- "jws": "^4.0.0"
- }
- },
- "google-logging-utils": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.1.tgz",
- "integrity": "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A=="
- },
- "googleapis": {
- "version": "171.4.0",
- "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-171.4.0.tgz",
- "integrity": "sha512-xybFL2SmmUgIifgsbsRQYRdNrSAYwxWZDmkZTGjUIaRnX5jPqR8el/cEvo6rCqh7iaZx6MfEPS/lrDgZ0bymkg==",
- "requires": {
- "google-auth-library": "^10.2.0",
- "googleapis-common": "^8.0.0"
- }
- },
- "googleapis-common": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.0.tgz",
- "integrity": "sha512-66if47It7y+Sab3HMkwEXx1kCq9qUC9px8ZXoj1CMrmLmUw81GpbnsNlXnlyZyGbGPGcj+tDD9XsZ23m7GLaJQ==",
- "requires": {
- "extend": "^3.0.2",
- "gaxios": "^7.0.0-rc.4",
- "google-auth-library": "^10.1.0",
- "qs": "^6.7.0",
- "url-template": "^2.0.8"
- }
- },
- "gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
- },
- "got": {
- "version": "15.0.3",
- "resolved": "https://registry.npmjs.org/got/-/got-15.0.3.tgz",
- "integrity": "sha512-630of5aMTYM6g6epok5nuqvctP8InJISvt39iNt7y0r27rcGCdiPv9Cc6EbwfnkyT1g/shBoVhvDjtXZUbn/Rw==",
- "requires": {
- "@sindresorhus/is": "^8.0.0",
- "byte-counter": "^0.1.0",
- "cacheable-lookup": "^7.0.0",
- "cacheable-request": "^13.0.18",
- "chunk-data": "^0.1.0",
- "decompress-response": "^10.0.0",
- "http2-wrapper": "^2.2.1",
- "keyv": "^5.6.0",
- "lowercase-keys": "^4.0.1",
- "responselike": "^4.0.2",
- "type-fest": "^5.6.0",
- "uint8array-extras": "^1.5.0"
- },
- "dependencies": {
- "decompress-response": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz",
- "integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==",
- "requires": {
- "mimic-response": "^4.0.0"
- }
- }
- }
- },
- "gtoken": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
- "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
- "requires": {
- "gaxios": "^7.0.0",
- "jws": "^4.0.0"
- }
- },
- "has-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
- },
- "hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "requires": {
- "function-bind": "^1.1.2"
- }
- },
- "http-cache-semantics": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
- "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="
- },
- "http2-wrapper": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz",
- "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==",
- "requires": {
- "quick-lru": "^5.1.1",
- "resolve-alpn": "^1.2.0"
- }
- },
- "https-proxy-agent": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
- "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
- "requires": {
- "agent-base": "^7.1.2",
- "debug": "4"
- }
- },
- "ieee754": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
- },
- "inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
- },
- "ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
- },
- "is-stream": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
- "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="
- },
- "json-bigint": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
- "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
- "requires": {
- "bignumber.js": "^9.0.0"
- }
- },
- "jwa": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
- "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
- "requires": {
- "buffer-equal-constant-time": "^1.0.1",
- "ecdsa-sig-formatter": "1.0.11",
- "safe-buffer": "^5.0.1"
- }
- },
- "jws": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
- "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
- "requires": {
- "jwa": "^2.0.0",
- "safe-buffer": "^5.0.1"
- }
- },
- "keyv": {
- "version": "5.6.0",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
- "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
- "requires": {
- "@keyv/serialize": "^1.1.1"
- }
- },
- "lowercase-keys": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-4.0.1.tgz",
- "integrity": "sha512-wI9Nui/L8VfADa/cr/7NQruaASk1k23/Uh1khQ02BCVYiiy8F4AhOGnQzJy3Fl/c44GnYSbZHv8g7EcG3kJ1Qg=="
- },
- "math-intrinsics": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
- },
- "mime-db": {
- "version": "1.47.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz",
- "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==",
- "dev": true
- },
- "mime-types": {
- "version": "2.1.30",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz",
- "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==",
- "dev": true,
- "requires": {
- "mime-db": "1.47.0"
- }
- },
- "mimic-response": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
- "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="
- },
- "minimist": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
- },
- "mkdirp-classic": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
- "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
- },
- "mri": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
- "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="
- },
- "ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
- },
- "napi-build-utils": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
- "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="
- },
- "node-abi": {
- "version": "3.78.0",
- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz",
- "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==",
- "requires": {
- "semver": "^7.3.5"
- }
- },
- "node-domexception": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
- "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="
- },
- "node-fetch": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
- "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
- "requires": {
- "whatwg-url": "^5.0.0"
- },
- "dependencies": {
- "tr46": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
- },
- "webidl-conversions": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
- },
- "whatwg-url": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
- "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "requires": {
- "tr46": "~0.0.3",
- "webidl-conversions": "^3.0.0"
- }
- }
- }
- },
- "normalize-url": {
- "version": "8.1.1",
- "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz",
- "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ=="
- },
- "object-inspect": {
- "version": "1.13.4",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
- "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
- },
- "once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
- "requires": {
- "wrappy": "1"
- }
- },
- "p-timeout": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-4.1.0.tgz",
- "integrity": "sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw=="
- },
- "prebuild-install": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
- "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
- "requires": {
- "detect-libc": "^2.0.0",
- "expand-template": "^2.0.3",
- "github-from-package": "0.0.0",
- "minimist": "^1.2.3",
- "mkdirp-classic": "^0.5.3",
- "napi-build-utils": "^2.0.0",
- "node-abi": "^3.3.0",
- "pump": "^3.0.0",
- "rc": "^1.2.7",
- "simple-get": "^4.0.0",
- "tar-fs": "^2.0.0",
- "tunnel-agent": "^0.6.0"
- }
- },
- "pump": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
- "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
- "requires": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
- }
- },
- "qs": {
- "version": "6.14.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
- "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
- "requires": {
- "side-channel": "^1.1.0"
- }
- },
- "quick-lru": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
- "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
- },
- "rc": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
- "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
- "requires": {
- "deep-extend": "^0.6.0",
- "ini": "~1.3.0",
- "minimist": "^1.2.0",
- "strip-json-comments": "~2.0.1"
- },
- "dependencies": {
- "strip-json-comments": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
- "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="
- }
- }
- },
- "readable-stream": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
- "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
- "requires": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- }
- },
- "resolve-alpn": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
- "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="
- },
- "responselike": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz",
- "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==",
- "requires": {
- "lowercase-keys": "^3.0.0"
- },
- "dependencies": {
- "lowercase-keys": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
- "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="
- }
- }
- },
- "safe-buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
- },
- "safe-compare": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/safe-compare/-/safe-compare-1.1.4.tgz",
- "integrity": "sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==",
- "requires": {
- "buffer-alloc": "^1.2.0"
- }
- },
- "sandwich-stream": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/sandwich-stream/-/sandwich-stream-2.0.2.tgz",
- "integrity": "sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ=="
- },
- "semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="
- },
- "side-channel": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
- "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
- "requires": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3",
- "side-channel-list": "^1.0.0",
- "side-channel-map": "^1.0.1",
- "side-channel-weakmap": "^1.0.2"
- }
- },
- "side-channel-list": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
- "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
- "requires": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
- }
- },
- "side-channel-map": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
- "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
- "requires": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3"
- }
- },
- "side-channel-weakmap": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
- "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
- "requires": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3",
- "side-channel-map": "^1.0.1"
- }
- },
- "simple-concat": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
- "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
- },
- "simple-get": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
- "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
- "requires": {
- "decompress-response": "^6.0.0",
- "once": "^1.3.1",
- "simple-concat": "^1.0.0"
- }
- },
- "string_decoder": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
- "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "requires": {
- "safe-buffer": "~5.2.0"
- }
- },
- "tagged-tag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
- "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="
- },
- "tar-fs": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
- "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
- "requires": {
- "chownr": "^1.1.1",
- "mkdirp-classic": "^0.5.2",
- "pump": "^3.0.0",
- "tar-stream": "^2.1.4"
- }
- },
- "tar-stream": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
- "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
- "requires": {
- "bl": "^4.0.3",
- "end-of-stream": "^1.4.1",
- "fs-constants": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^3.1.1"
- }
- },
- "telegraf": {
- "version": "4.16.3",
- "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.16.3.tgz",
- "integrity": "sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==",
- "requires": {
- "@telegraf/types": "^7.1.0",
- "abort-controller": "^3.0.0",
- "debug": "^4.3.4",
- "mri": "^1.2.0",
- "node-fetch": "^2.7.0",
- "p-timeout": "^4.1.0",
- "safe-compare": "^1.1.4",
- "sandwich-stream": "^2.0.2"
- }
- },
- "tunnel-agent": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
- "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
- "requires": {
- "safe-buffer": "^5.0.1"
- }
- },
- "type-fest": {
- "version": "5.6.0",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz",
- "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==",
- "requires": {
- "tagged-tag": "^1.0.0"
- }
- },
- "uint8array-extras": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
- "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="
- },
- "undici-types": {
- "version": "7.19.2",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
- "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
- "dev": true
- },
- "url-template": {
- "version": "2.0.8",
- "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
- "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw=="
- },
- "util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
- },
- "web-streams-polyfill": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
- "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="
- },
- "wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
- }
}
}
diff --git a/package.json b/package.json
index 2f0f1fb..6a5da49 100644
--- a/package.json
+++ b/package.json
@@ -2,9 +2,10 @@
"name": "hommabot2",
"version": "2.0.0",
"description": "",
- "main": "src/index.ts",
+ "main": "src/bot.ts",
"scripts": {
- "build": "bun build src/index.ts --outfile=build/index.js --target=node --format=cjs --minify",
+ "start": "npx tsx src/bot.ts",
+ "build": "bun build src/bot.ts --outfile=build/index.js --target=node --format=cjs --minify",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
@@ -13,14 +14,11 @@
"dependencies": {
"date-fns": "4.1.0",
"dotenv": "17.4.2",
- "googleapis": "171.4.0",
- "got": "15.0.3",
"better-sqlite3": "12.9.0",
"telegraf": "4.16.3"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
- "@types/got": "9.6.12",
"@types/node": "25.6.0"
}
}
diff --git a/src/bot.ts b/src/bot.ts
new file mode 100644
index 0000000..1de3a22
--- /dev/null
+++ b/src/bot.ts
@@ -0,0 +1,49 @@
+import { bot, broadcastMessage } from "./tg";
+import { getRecurringTasks, markTaskDone } from "./db";
+import { isThisWeek, today, now } from "./tasks";
+import { isMonday, getHours, format } from "date-fns";
+
+const CHECK_INTERVAL_MS = 60 * 1000;
+let lastRunKey: string | null = null;
+
+const runWeeklyTasks = async (): Promise<void> => {
+ try {
+ console.log("[weekly] Starting recurring tasks check");
+ const tasks = getRecurringTasks();
+ const dueTasks = tasks.filter(isThisWeek);
+
+ if (dueTasks.length > 0) {
+ const taskText = dueTasks.map((t) => `• ${t.name} (${t.interval})`).join("\n");
+ await broadcastMessage(`📋 Tällä viikolla tehtävät hommat:\n${taskText}`);
+ const todayStr = today();
+ for (const t of dueTasks) {
+ markTaskDone(t.id, todayStr);
+ }
+ } else {
+ await broadcastMessage("Tällä viikolla ei toistuvia hommia! 🎉");
+ }
+ console.log("[weekly] Done");
+ } catch (err) {
+ console.error("[weekly] Error:", err);
+ }
+};
+
+const checkAndRun = async (): Promise<void> => {
+ const current = now();
+ if (isMonday(current) && getHours(current) === 9) {
+ const key = format(current, "yyyy-MM-dd");
+ if (key !== lastRunKey) {
+ lastRunKey = key;
+ await runWeeklyTasks();
+ }
+ }
+};
+
+setInterval(checkAndRun, CHECK_INTERVAL_MS);
+console.log("[bot] Scheduled weekly tasks for Monday 09:00");
+
+bot.launch();
+console.log("[bot] Bot is running");
+
+process.once("SIGINT", () => bot.stop("SIGINT"));
+process.once("SIGTERM", () => bot.stop("SIGTERM"));
diff --git a/src/config.ts b/src/config.ts
index d8e3ca0..0384656 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,5 +1,4 @@
import dotenv from "dotenv";
-import fs from "fs";
import path from "path";
if (!process.env.SKIP_DOTENV) {
@@ -13,45 +12,20 @@ class ConfigError extends Error {
}
}
-const nodeEnv = process.env.NODE_ENV || "development";
-
const tgBotToken = process.env.TELEGRAM_BOT_TOKEN || null;
if (!tgBotToken) {
throw new ConfigError("Missing env var TELEGRAM_BOT_TOKEN");
}
-const sheetsSpreadsheetId = process.env.SHEETS_SPREADSHEET_ID || null;
-if (!sheetsSpreadsheetId) {
- throw new ConfigError("Missing env var SHEETS_SPREADSHEET_ID");
-}
-
-const sheetsRange = process.env.SHEETS_RANGE || null;
-if (!sheetsRange) {
- throw new ConfigError("Missing env var SHEETS_RANGE");
-}
+const tz = process.env.TZ || "Europe/Helsinki";
const sqlitePath =
process.env.SQLITE_PATH || path.join(process.cwd(), "data", "hommabot.db");
-const googleSaJsonB64 = process.env.G_SA_JSON_B64 || null;
-if (!googleSaJsonB64) {
- throw new ConfigError("Missing env var G_SA_JSON_B64");
-}
-
-const googleSaJsonBuff = Buffer.from(googleSaJsonB64, "base64");
-const googleSaJsonUtf8 = googleSaJsonBuff.toString("utf-8");
-const googleSaJson: Record<string, string> = JSON.parse(googleSaJsonUtf8);
-const googleSaJsonPath = path.join("sa_key.json");
-fs.writeFileSync(googleSaJsonPath, googleSaJsonUtf8);
-
const config = {
- nodeEnv,
tgBotToken,
- sheetsSpreadsheetId,
- sheetsRange,
+ tz,
sqlitePath,
- googleSaJson,
- googleSaJsonPath,
};
export default config;
diff --git a/src/db.ts b/src/db.ts
index 511b519..a2c5cee 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -51,6 +51,20 @@ const ddl = `
CREATE TABLE IF NOT EXISTS permitted_users (
id INTEGER PRIMARY KEY
) STRICT;
+
+ CREATE TABLE IF NOT EXISTS shopping_list (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ item TEXT NOT NULL,
+ added_by INTEGER NOT NULL,
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
+ ) STRICT;
+
+ CREATE TABLE IF NOT EXISTS recurring_tasks (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ interval TEXT NOT NULL,
+ last_done TEXT
+ ) STRICT;
COMMIT;
`;
db.exec(ddl);
@@ -117,3 +131,63 @@ export const getPermittedUsers = async (): Promise<
const stmt = db.prepare("SELECT id FROM permitted_users");
return stmt.all() as PermittedUser[];
};
+
+/* Shopping list */
+
+interface ShoppingItem {
+ id: number;
+ item: string;
+}
+
+export const addShoppingItem = (item: string, addedBy: number): void => {
+ withWriteTx((db) => {
+ db.prepare("INSERT INTO shopping_list (item, added_by) VALUES (?, ?)").run(item, addedBy);
+ });
+};
+
+export const getShoppingList = (): ShoppingItem[] => {
+ return db.prepare("SELECT id, item FROM shopping_list ORDER BY id").all() as ShoppingItem[];
+};
+
+export const removeShoppingItem = (id: number): void => {
+ withWriteTx((db) => {
+ db.prepare("DELETE FROM shopping_list WHERE id = ?").run(id);
+ });
+};
+
+/* Recurring tasks */
+
+export interface RecurringTask {
+ id: number;
+ name: string;
+ interval: string;
+ last_done: string | null;
+}
+
+export const addRecurringTask = (name: string, interval: string): void => {
+ withWriteTx((db) => {
+ db.prepare("INSERT INTO recurring_tasks (name, interval) VALUES (?, ?)").run(name, interval);
+ });
+};
+
+export const getRecurringTasks = (): RecurringTask[] => {
+ return db.prepare("SELECT id, name, interval, last_done FROM recurring_tasks ORDER BY id").all() as RecurringTask[];
+};
+
+export const updateRecurringTask = (id: number, name: string, interval: string): void => {
+ withWriteTx((db) => {
+ db.prepare("UPDATE recurring_tasks SET name = ?, interval = ? WHERE id = ?").run(name, interval, id);
+ });
+};
+
+export const markTaskDone = (id: number, date: string): void => {
+ withWriteTx((db) => {
+ db.prepare("UPDATE recurring_tasks SET last_done = ? WHERE id = ?").run(date, id);
+ });
+};
+
+export const removeRecurringTask = (id: number): void => {
+ withWriteTx((db) => {
+ db.prepare("DELETE FROM recurring_tasks WHERE id = ?").run(id);
+ });
+};
diff --git a/src/index.ts b/src/index.ts
deleted file mode 100644
index c6d92eb..0000000
--- a/src/index.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { buildSheetsClient } from "./sheets";
-import { broadcastMessage } from "./tg";
-
-const main = async (): Promise<void> => {
- const startTime = Date.now();
- console.log("[run] Starting recurring tasks notification script");
-
- const sheets = await buildSheetsClient();
-
- // 1. Fetch raw sheet data
- const sheetData = await sheets.fetchSheetData();
- console.log(`[run] Retrieved ${sheetData.length} raw rows from Sheets`);
-
- // 2. Process rows (compute next/last dates)
- const entries = sheets.processRows(sheetData);
- console.log(`[run] Processed ${entries.length} entries`);
-
- // 3. Filter only tasks due this week
- const thisWeeksEntries = sheets.filterOnlyThisWeek(entries);
- console.log(`[run] Found ${thisWeeksEntries.length} entries due this week`);
-
- // 4. Compose & broadcast Telegram message
- if (thisWeeksEntries.length > 0) {
- const tasks = thisWeeksEntries.map((e) => `${e.name} (${e.interval})`);
- const taskText = tasks.join("\n");
- const msg = `Tällä viikolla tehtävät hommat:\n${taskText}`;
- await broadcastMessage(msg);
- console.log("[run] Broadcasted weekly task message");
- } else {
- const msg = "Tällä viikolla ei toistuvia hommia! 🎉";
- await broadcastMessage(msg);
- console.log("[run] Broadcasted empty-week message");
- }
-
- // 5. Update "last done" column with new date stamps (only tasks done this week are advanced)
- const newDateStamps = sheets.updatedLastDoneDateStamps(entries);
- await sheets.updateSheetLastDoneColumn(newDateStamps);
- console.log("[run] Updated last done column in sheet");
-
- const durationMs = Date.now() - startTime;
- console.log(`[run] Completed successfully in ${durationMs}ms`);
-};
-
-main()
- .then(() => {
- process.exit(0);
- })
- .catch((err) => {
- console.error("[run] Fatal error:", err);
- process.exit(1);
- });
diff --git a/src/middleware.ts b/src/middleware.ts
index 589022f..6c56352 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,11 +1,5 @@
/**
* Telegram-only middleware utilities.
- *
- * HTTP server functionality has been removed, so any HTTP-specific middleware
- * (e.g. header-based auth) has been deleted.
- *
- * This module currently exposes:
- * tgAuth - Ensures that only permitted Telegram user IDs can invoke bot commands.
*/
import { Context } from "telegraf";
@@ -25,7 +19,7 @@ export const tgAuth = async (
ctx: Context,
next: () => Promise<void>,
): Promise<void> => {
- const userId = ctx.message?.from.id;
+ const userId = ctx.from?.id;
if (!userId) {
await ctx.reply("Käyttäjätunnusta ei voitu lukea (user id puuttuu).");
return;
@@ -43,6 +37,6 @@ export const tgAuth = async (
await next();
} catch (err) {
console.error("[tgAuth] Authorization check failed:", err);
- await ctx.reply("Odottamaton virhe valtuutuksessa.");
+ await ctx.reply("Odottamaton virhe luvituksessa.");
}
};
diff --git a/src/sheets.ts b/src/sheets.ts
deleted file mode 100644
index 3578198..0000000
--- a/src/sheets.ts
+++ /dev/null
@@ -1,203 +0,0 @@
-import { google } from "googleapis";
-import {
- add,
- set,
- Duration,
- parse,
- format,
- getDay,
- subDays,
- addDays,
-} from "date-fns";
-
-import config from "./config";
-
-interface SheetsRawEntry {
- intervalStr: string;
- name: string;
- lastDoneDateStr: string;
-}
-
-interface SheetsProcessedEntry {
- name: string;
- nextDateStamp: string;
- nextDate: Date;
- lastDateStamp: string;
- lastDate?: Date;
- interval: string;
-}
-
-const INTERVAL_ABBREVS = {
- pv: 1,
- vk: 7,
- kk: 30,
- v: 365,
-} as const;
-
-type IntervalAbbrevKey = keyof typeof INTERVAL_ABBREVS;
-
-const strToDate = (s: string) => parse(s, "yyyy-MM-dd", new Date());
-const dateToStr = (d: Date) => format(d, "yyyy-MM-dd");
-
-const calculateDuration = (intervalStr: string) => {
- const match = /(\d+)(\w+)/.exec(intervalStr);
-
- if (!match) {
- throw new Error("Given interval string does not match regular expression");
- }
-
- const number = Number.parseInt(match[1]);
- const abbrev = match[2];
-
- if (number <= 0) {
- throw new Error(`"${number}" is an invalid interval number`);
- }
-
- if (!Object.keys(INTERVAL_ABBREVS).includes(abbrev)) {
- throw new Error(`"${abbrev}" is an invalid interval string`);
- }
-
- const days = INTERVAL_ABBREVS[abbrev as IntervalAbbrevKey];
-
- const totalDuration: Duration = {
- days: number * days,
- };
-
- return totalDuration;
-};
-
-const processRow = (row: SheetsRawEntry) => {
- const duration = calculateDuration(row.intervalStr);
-
- const calcValues = () => {
- if (row.lastDoneDateStr) {
- const lastDate = strToDate(row.lastDoneDateStr);
- const nextDate = add(lastDate, duration);
- const nextDateStamp = dateToStr(nextDate);
-
- return { lastDate, nextDate, nextDateStamp };
- } else {
- const lastDate = undefined;
- const nextDate = new Date();
- const nextDateStamp = dateToStr(nextDate);
-
- return { lastDate, nextDate, nextDateStamp };
- }
- };
-
- const processedEntry: SheetsProcessedEntry = {
- ...calcValues(),
- name: row.name,
- lastDateStamp: row.lastDoneDateStr,
- interval: row.intervalStr,
- };
-
- return processedEntry;
-};
-
-const isThisWeek = (entry: SheetsProcessedEntry): boolean => {
- const nextDate = entry.nextDate;
- const currentDateTime = new Date();
- const today = set(currentDateTime, {
- hours: 0,
- minutes: 0,
- seconds: 0,
- milliseconds: 0,
- });
- const dayOfWeek = (getDay(today) + 7 - 1) % 7; // getDay returns sunday = 0
- const currentWeekStart = subDays(today, dayOfWeek);
- const currentWeekEnd = addDays(currentWeekStart, 7);
-
- return nextDate >= currentWeekStart && nextDate < currentWeekEnd;
-};
-
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-export const buildSheetsClient = async () => {
- const auth = new google.auth.GoogleAuth({
- // Scopes can be specified either as an array or as a single, space-delimited string.
- scopes: [
- "https://www.googleapis.com/auth/spreadsheets",
- "https://www.googleapis.com/auth/spreadsheets.readonly",
- ],
- keyFile: config.googleSaJsonPath,
- });
-
- google.options({ auth });
- const sheets = google.sheets({ version: "v4" });
-
- const fetchSheetData = async (): Promise<SheetsRawEntry[]> => {
- try {
- const res = await sheets.spreadsheets.values.get({
- spreadsheetId: config.sheetsSpreadsheetId,
- range: config.sheetsRange,
- });
-
- const entriesAsLists = res.data.values || [];
- const rawEntries: SheetsRawEntry[] = entriesAsLists.map((row) => ({
- intervalStr: row[0],
- name: row[1],
- lastDoneDateStr: row[2],
- }));
-
- console.log(`Fetched ${rawEntries.length} rows from Sheets.`);
- return rawEntries;
- } catch (err) {
- console.error(err);
- throw err;
- }
- };
-
- const getRightmostColumnInRange = (range: string): string => {
- const match = /(.+!)?([A-Z]+?)(\d+):([A-Z]+?)(\d+)/.exec(range);
-
- if (!match) {
- throw new Error("Given range does not match regular expression");
- }
-
- const sheetName = match[1];
- const topLeftRow = Number.parseInt(match[3]);
- const bottomRightCol = match[4];
- const bottomRightRow = Number.parseInt(match[5]);
-
- const rmCol = bottomRightCol;
- const rmRowStart = topLeftRow;
- const rmRowEnd = bottomRightRow;
-
- return `${sheetName}${rmCol}${rmRowStart}:${rmCol}${rmRowEnd}`;
- };
-
- const updateSheetLastDoneColumn = async (newDateStamps: string[]) => {
- const lastDoneColumnRange = getRightmostColumnInRange(config.sheetsRange);
-
- const body = {
- values: newDateStamps.map((nds) => [nds]),
- };
-
- await sheets.spreadsheets.values.update({
- spreadsheetId: config.sheetsSpreadsheetId,
- range: lastDoneColumnRange,
- valueInputOption: "RAW",
- requestBody: body,
- });
- };
-
- const processRows = (rows: SheetsRawEntry[]) => rows.map(processRow);
-
- const filterOnlyThisWeek = (
- entries: SheetsProcessedEntry[],
- ): SheetsProcessedEntry[] => entries.filter(isThisWeek);
-
- const updatedLastDoneDateStamps = (
- entries: SheetsProcessedEntry[],
- ): string[] =>
- entries.map((e) => (isThisWeek(e) ? e.nextDateStamp : e.lastDateStamp));
-
- return {
- fetchSheetData,
- getRightmostColumnInRange,
- updateSheetLastDoneColumn,
- processRows,
- filterOnlyThisWeek,
- updatedLastDoneDateStamps,
- };
-};
diff --git a/src/tasks.ts b/src/tasks.ts
new file mode 100644
index 0000000..b370aad
--- /dev/null
+++ b/src/tasks.ts
@@ -0,0 +1,39 @@
+import { add, parse, format, startOfWeek, addWeeks, isWithinInterval, Duration } from "date-fns";
+import { RecurringTask } from "./db";
+import config from "./config";
+
+const INTERVAL_ABBREVS: Record<string, number> = {
+ pv: 1,
+ vk: 7,
+ kk: 30,
+ v: 365,
+};
+
+export const now = (): Date => {
+ return new Date(new Date().toLocaleString("en-US", { timeZone: config.tz }));
+};
+
+export const parseInterval = (intervalStr: string): Duration => {
+ const match = /^(\d+)(pv|vk|kk|v)$/.exec(intervalStr);
+ if (!match) throw new Error(`Virheellinen intervalli: "${intervalStr}"`);
+ const num = parseInt(match[1]);
+ const days = num * INTERVAL_ABBREVS[match[2]];
+ return { days };
+};
+
+export const getNextDate = (task: RecurringTask): Date => {
+ if (!task.last_done) return now();
+ const lastDate = parse(task.last_done, "yyyy-MM-dd", now());
+ return add(lastDate, parseInterval(task.interval));
+};
+
+export const isThisWeek = (task: RecurringTask): boolean => {
+ const nextDate = getNextDate(task);
+ const weekStart = startOfWeek(now(), { weekStartsOn: 1 });
+ const weekEnd = addWeeks(weekStart, 1);
+ return isWithinInterval(nextDate, { start: weekStart, end: weekEnd });
+};
+
+export const formatDate = (d: Date): string => format(d, "yyyy-MM-dd");
+
+export const today = (): string => format(now(), "yyyy-MM-dd");
diff --git a/src/tg.ts b/src/tg.ts
index 4850c88..2e5e49d 100644
--- a/src/tg.ts
+++ b/src/tg.ts
@@ -1,11 +1,14 @@
-import { Telegraf } from "telegraf";
+import { Markup, Telegraf } from "telegraf";
import config from "./config";
-import { addActiveChat, getActiveChats, removeActiveChat } from "./db";
+import {
+ addActiveChat, getActiveChats, removeActiveChat,
+ addShoppingItem, getShoppingList, removeShoppingItem,
+ addRecurringTask, getRecurringTasks, updateRecurringTask, markTaskDone, removeRecurringTask,
+} from "./db";
import { tgAuth } from "./middleware";
+import { getNextDate, formatDate, parseInterval, today } from "./tasks";
-console.log(`Setting up Telegram bot with token ${config.tgBotToken}`);
const bot = new Telegraf(config.tgBotToken);
-
bot.use(tgAuth);
bot.command("/start", async (ctx) => {
@@ -18,13 +21,111 @@ bot.command("/stop", async (ctx) => {
await ctx.reply("HommaBot pysäytetty!");
});
+// --- Shopping list ---
+
+bot.command("add", async (ctx) => {
+ const item = ctx.message.text.replace(/^\/add\s*/, "").trim();
+ if (!item) { await ctx.reply("Käyttö: /add <tuote>"); return; }
+ addShoppingItem(item, ctx.message.from.id);
+ await ctx.reply(`Lisätty: ${item}`);
+});
+
+bot.command("list", async (ctx) => {
+ const items = getShoppingList();
+ if (items.length === 0) { await ctx.reply("Ostoslista on tyhjä."); return; }
+ const text = items.map((i, idx) => `${idx + 1}. ${i.item}`).join("\n");
+ const buttons = items.map((i) => [Markup.button.callback(`❌ ${i.item}`, `del:${i.id}`)]);
+ await ctx.reply(`🛒 Ostoslista:\n${text}`, Markup.inlineKeyboard(buttons));
+});
+
+bot.action(/^del:(\d+)$/, async (ctx) => {
+ const id = Number(ctx.match[1]);
+ removeShoppingItem(id);
+ await ctx.answerCbQuery("Poistettu!");
+ const items = getShoppingList();
+ if (items.length === 0) { await ctx.editMessageText("Ostoslista on tyhjä."); return; }
+ const text = items.map((i, idx) => `${idx + 1}. ${i.item}`).join("\n");
+ const buttons = items.map((i) => [Markup.button.callback(`❌ ${i.item}`, `del:${i.id}`)]);
+ await ctx.editMessageText(`🛒 Ostoslista:\n${text}`, Markup.inlineKeyboard(buttons));
+});
+
+// --- Recurring tasks ---
+
+bot.command("newtask", async (ctx) => {
+ const args = ctx.message.text.replace(/^\/newtask\s*/, "").trim();
+ const match = /^(\d+(?:pv|vk|kk|v))\s+(.+)$/.exec(args);
+ if (!match) {
+ await ctx.reply("Käyttö: /newtask <intervalli> <nimi>\nEsim: /newtask 2vk Imurointi\n\nIntervallit: pv, vk, kk, v");
+ return;
+ }
+ try {
+ parseInterval(match[1]);
+ } catch {
+ await ctx.reply("Virheellinen intervalli. Käytä: pv, vk, kk, v (esim. 2vk, 1kk)");
+ return;
+ }
+ addRecurringTask(match[2], match[1]);
+ await ctx.reply(`✅ Uusi tehtävä: ${match[2]} (joka ${match[1]})`);
+});
+
+bot.command("tasks", async (ctx) => {
+ const tasks = getRecurringTasks();
+ if (tasks.length === 0) { await ctx.reply("Ei tehtäviä. Lisää: /newtask"); return; }
+ const lines = tasks.map((t) => {
+ const next = formatDate(getNextDate(t));
+ const done = t.last_done || "—";
+ return `*${t.id}.* ${t.name}\n ↻ ${t.interval} | edellinen: ${done} | seuraava: ${next}`;
+ });
+ const buttons = tasks.map((t) => [
+ Markup.button.callback(`✅ ${t.name}`, `taskdone:${t.id}`),
+ Markup.button.callback(`🗑️`, `taskdel:${t.id}`),
+ ]);
+ await ctx.reply(`📋 Toistuvat tehtävät:\n\n${lines.join("\n\n")}`, {
+ parse_mode: "Markdown",
+ ...Markup.inlineKeyboard(buttons),
+ });
+});
+
+bot.command("edittask", async (ctx) => {
+ const args = ctx.message.text.replace(/^\/edittask\s*/, "").trim();
+ const match = /^(\d+)\s+(\d+(?:pv|vk|kk|v))\s+(.+)$/.exec(args);
+ if (!match) {
+ await ctx.reply("Käyttö: /edittask <id> <intervalli> <nimi>\nEsim: /edittask 3 1kk Ikkunoiden pesu");
+ return;
+ }
+ try {
+ parseInterval(match[2]);
+ } catch {
+ await ctx.reply("Virheellinen intervalli.");
+ return;
+ }
+ updateRecurringTask(Number(match[1]), match[3], match[2]);
+ await ctx.reply(`✏️ Päivitetty: ${match[3]} (${match[2]})`);
+});
+
+bot.command("done", async (ctx) => {
+ const idStr = ctx.message.text.replace(/^\/done\s*/, "").trim();
+ const id = Number(idStr);
+ if (!id) { await ctx.reply("Käyttö: /done <id>"); return; }
+ markTaskDone(id, today());
+ await ctx.reply("✅ Merkitty tehdyksi!");
+});
+
+bot.action(/^taskdone:(\d+)$/, async (ctx) => {
+ const id = Number(ctx.match[1]);
+ markTaskDone(id, today());
+ await ctx.answerCbQuery("Merkitty tehdyksi!");
+});
+
+bot.action(/^taskdel:(\d+)$/, async (ctx) => {
+ const id = Number(ctx.match[1]);
+ removeRecurringTask(id);
+ await ctx.answerCbQuery("Poistettu!");
+});
+
const broadcastMessage = async (msg: string): Promise<void> => {
const activeChats = await getActiveChats();
- const chatIds = activeChats.map(c => c.id);
-
- await Promise.all(chatIds.map(async chatId =>
- bot.telegram.sendMessage(chatId, msg),
- ));
+ await Promise.all(activeChats.map((c) => bot.telegram.sendMessage(c.id, msg)));
};
export { bot, broadcastMessage };