From 145706f1e0aeb07a56f66286111f779811d750bf Mon Sep 17 00:00:00 2001 From: Jani Haiko Date: Sun, 8 Oct 2023 13:50:13 +0300 Subject: [PATCH] Subscribing --- package-lock.json | 181 ++++++++++++++------------------ package.json | 2 +- resources/manifest.yml | 2 +- src/BlockParsers.ts | 10 +- src/BotCommands.ts | 71 ++++++++++++- src/BotEvents.ts | 126 +++++++++++++--------- src/model/Settings.ts | 5 +- src/model/SettingsRepository.ts | 15 +-- src/server.ts | 7 +- src/types.d.ts | 6 ++ 10 files changed, 261 insertions(+), 164 deletions(-) diff --git a/package-lock.json b/package-lock.json index 75220a9..a104198 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lounasbotti", - "version": "1.7.4", + "version": "1.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lounasbotti", - "version": "1.7.4", + "version": "1.8.0", "license": "GPL-3.0+", "dependencies": { "@sentry/integrations": "^7.72.0", @@ -72,9 +72,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.0.tgz", - "integrity": "sha512-zJmuCWj2VLBt4c25CfBIbMZLGLyhkvs7LznyVX5HfpzeocThgIj5XQK4L+g3U36mMcx8bPMhGyPpwCATamC4jQ==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", + "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -104,9 +104,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", - "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", + "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -215,13 +215,13 @@ } }, "node_modules/@sentry-internal/tracing": { - "version": "7.72.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.72.0.tgz", - "integrity": "sha512-DToryaRSHk9R5RLgN4ktYEXZjQdqncOAWPqyyIurji8lIobXFRfmLtGL1wjoCK6sQNgWsjhSM9kXxwGnva1DNw==", + "version": "7.73.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.73.0.tgz", + "integrity": "sha512-ig3WL/Nqp8nRQ52P205NaypGKNfIl/G+cIqge9xPW6zfRb5kJdM1YParw9GSJ1SPjEZBkBORGAML0on5H2FILw==", "dependencies": { - "@sentry/core": "7.72.0", - "@sentry/types": "7.72.0", - "@sentry/utils": "7.72.0", + "@sentry/core": "7.73.0", + "@sentry/types": "7.73.0", + "@sentry/utils": "7.73.0", "tslib": "^2.4.1 || ^1.9.3" }, "engines": { @@ -229,9 +229,9 @@ } }, "node_modules/@sentry/cli": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.21.1.tgz", - "integrity": "sha512-iJGL818zHzVb129CNWLoZriymq2nrnhk1XqN4Fh0AMxYJcOICmXYKR8RSkLhhE1U1J1D77UzA+FyBhWHOFA82A==", + "version": "2.21.2", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.21.2.tgz", + "integrity": "sha512-X1nye89zl+QV3FSuQDGItfM51tW9PQ7ce0TtV/12DgGgTVEgnVp5uvO3wX5XauHvulQzRPzwUL3ZK+yS5bAwCw==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -291,12 +291,12 @@ } }, "node_modules/@sentry/core": { - "version": "7.72.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.72.0.tgz", - "integrity": "sha512-G03JdQ5ZsFNRjcNNi+QvCjqOuBvYqU92Gs1T2iK3GE8dSBTu2khThydMpG4xrKZQLIpHOyiIhlFZiuPtZ66W8w==", + "version": "7.73.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.73.0.tgz", + "integrity": "sha512-9FEz4Gq848LOgVN2OxJGYuQqxv7cIVw69VlAzWHEm3njt8mjvlTq+7UiFsGRo84+59V2FQuHxzA7vVjl90WfSg==", "dependencies": { - "@sentry/types": "7.72.0", - "@sentry/utils": "7.72.0", + "@sentry/types": "7.73.0", + "@sentry/utils": "7.73.0", "tslib": "^2.4.1 || ^1.9.3" }, "engines": { @@ -304,12 +304,13 @@ } }, "node_modules/@sentry/integrations": { - "version": "7.72.0", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.72.0.tgz", - "integrity": "sha512-ay2WgLtjsr0WS8+N7i7VmKzondoZRh3ISx8rb91LjmVrDNu8baleZAB59jkKe/JSy0gYh99umJuptc2te/65+A==", + "version": "7.73.0", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.73.0.tgz", + "integrity": "sha512-IjVpn4d+aSL9L1Ntu/oAdRwujz4BzzavDsZf96Xgc/AjBnjAEUT+wT1dAwluThfuKDXmWOJHhZ2cHHMfqI+7vw==", "dependencies": { - "@sentry/types": "7.72.0", - "@sentry/utils": "7.72.0", + "@sentry/core": "7.73.0", + "@sentry/types": "7.73.0", + "@sentry/utils": "7.73.0", "localforage": "^1.8.1", "tslib": "^2.4.1 || ^1.9.3" }, @@ -318,14 +319,14 @@ } }, "node_modules/@sentry/node": { - "version": "7.72.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.72.0.tgz", - "integrity": "sha512-R5kNCIdaDa92EN6oCLiGJehw5wxayOM53WF60Ap6EJHZb5U8dM2BnODmQ6SCRLNB677p+620oSV6CCU286IleQ==", - "dependencies": { - "@sentry-internal/tracing": "7.72.0", - "@sentry/core": "7.72.0", - "@sentry/types": "7.72.0", - "@sentry/utils": "7.72.0", + "version": "7.73.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.73.0.tgz", + "integrity": "sha512-i50bRfmgkRRx0XXUbg9jGD/RuznDJxJXc4rBILhoJuhl+BjRIaoXA3ayplfJn8JLZxsNh75uJaCq4IUK70SORw==", + "dependencies": { + "@sentry-internal/tracing": "7.73.0", + "@sentry/core": "7.73.0", + "@sentry/types": "7.73.0", + "@sentry/utils": "7.73.0", "cookie": "^0.5.0", "https-proxy-agent": "^5.0.0", "lru_map": "^0.3.3", @@ -336,19 +337,19 @@ } }, "node_modules/@sentry/types": { - "version": "7.72.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.72.0.tgz", - "integrity": "sha512-g6u0mk62yGshx02rfFADIfyR/S9VXcf3RG2qQPuvykrWtOfN/BOTrZypF7I+MiqKwRW76r3Pcu2C/AB+6z9XQA==", + "version": "7.73.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.73.0.tgz", + "integrity": "sha512-/v8++bly8jW7r4cP2wswYiiVpn7eLLcqwnfPUMeCQze4zj3F3nTRIKc9BGHzU0V+fhHa3RwRC2ksqTGq1oJMDg==", "engines": { "node": ">=8" } }, "node_modules/@sentry/utils": { - "version": "7.72.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.72.0.tgz", - "integrity": "sha512-o/MtqI7WJXuswidH0bSgBP40KN2lrnyQEIx5uoyJUJi/QEaboIsqbxU62vaFJpde8SYrbA+rTnP3J3ujF2gUag==", + "version": "7.73.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.73.0.tgz", + "integrity": "sha512-h3ZK/qpf4k76FhJV9uiSbvMz3V/0Ovy94C+5/9UgPMVCJXFmVsdw8n/dwANJ7LupVPfYP23xFGgebDMFlK1/2w==", "dependencies": { - "@sentry/types": "7.72.0", + "@sentry/types": "7.73.0", "tslib": "^2.4.1 || ^1.9.3" }, "engines": { @@ -620,9 +621,9 @@ "integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==" }, "node_modules/@types/node": { - "version": "20.8.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.0.tgz", - "integrity": "sha512-LzcWltT83s1bthcvjBmiBvGJiiUe84NWRHkw+ZV6Fr41z2FbIzvc815dk2nQ3RAKMuN2fkenM/z3Xv2QzEpYxQ==" + "version": "20.8.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.3.tgz", + "integrity": "sha512-jxiZQFpb+NlH5kjW49vXxvxTjeeqlbsnTAdBTKpzEdPs9itay7MscYXz3Fo9VYFEsfQ6LJFitHad3faerLAjCw==" }, "node_modules/@types/node-schedule": { "version": "2.1.1", @@ -1680,31 +1681,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -1861,15 +1837,15 @@ } }, "node_modules/eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", - "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", + "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", + "@eslint/js": "8.51.0", "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -2398,12 +2374,12 @@ "integrity": "sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA==" }, "node_modules/flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", "dev": true, "dependencies": { - "flatted": "^3.2.7", + "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" }, @@ -2594,9 +2570,9 @@ } }, "node_modules/globals": { - "version": "13.22.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", - "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2666,12 +2642,9 @@ "dev": true }, "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", "engines": { "node": ">= 0.4.0" } @@ -3261,9 +3234,9 @@ } }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "dependencies": { "json-buffer": "3.0.1" @@ -3490,11 +3463,11 @@ } }, "node_modules/mongodb": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.8.1.tgz", - "integrity": "sha512-wKyh4kZvm6NrCPH8AxyzXm3JBoEf4Xulo0aUWh3hCgwgYJxyQ1KLST86ZZaSWdj6/kxYUA3+YZuyADCE61CMSg==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.0.tgz", + "integrity": "sha512-g+GCMHN1CoRUA+wb1Agv0TI4YTSiWr42B5ulkiAfLLHitGK1R+PkSAf3Lr5rPZwi/3F04LiaZEW0Kxro9Fi2TA==", "dependencies": { - "bson": "^5.4.0", + "bson": "^5.5.0", "mongodb-connection-string-url": "^2.6.0", "socks": "^2.7.1" }, @@ -3539,13 +3512,13 @@ } }, "node_modules/mongoose": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.5.3.tgz", - "integrity": "sha512-QyYzhZusux0wIJs+4rYyHvel0kJm0CT887trNd1WAB3iQnDuJow0xEnjETvuS/cTjHQUVPihOpN7OHLlpJc52w==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.6.0.tgz", + "integrity": "sha512-ztQ12rm0BQN5i7LB6xhWX4l9a9w2aa3jEwa/mM2vAutYJRyAwOzcusvKJBULMzFHyUDBOVW15grisexypgMIWA==", "dependencies": { "bson": "^5.4.0", "kareem": "2.5.1", - "mongodb": "5.8.1", + "mongodb": "5.9.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -4651,9 +4624,9 @@ } }, "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsscmp": { "version": "1.0.6", @@ -4678,6 +4651,12 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 8d508dc..6177e66 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "lounasbotti", "type": "module", - "version": "1.7.4", + "version": "1.8.0", "private": true, "description": "Slack bot for retrieving lunch menus of local restaurants. Very much WIP and not meant for public use.", "main": "./dist/server.js", diff --git a/resources/manifest.yml b/resources/manifest.yml index 9898423..d11dc94 100644 --- a/resources/manifest.yml +++ b/resources/manifest.yml @@ -25,7 +25,7 @@ features: - command: /lounasbotti url: https://lounasbotti-disec.fly.dev/slack/events description: Manage Lounasbotti - usage_hint: "[ping | restart]" + usage_hint: "[ping | restart | [un]subscribe]" should_escape: false oauth_config: scopes: diff --git a/src/BlockParsers.ts b/src/BlockParsers.ts index 42e1a85..b0ccb9e 100644 --- a/src/BlockParsers.ts +++ b/src/BlockParsers.ts @@ -1,4 +1,3 @@ -import { Job } from "node-schedule"; import { Bits, BlockCollection, Blocks, Elements, HomeTab, Md, setIfTruthy, user } from "slack-block-builder"; import { SlackBlockDto, SlackHomeTabDto } from "slack-block-builder/dist/internal"; @@ -43,13 +42,14 @@ export default class BlockParsers { ]; } - public static parseHomeTabView(data: { settings: Settings, version: string, userId: string, jobs: Record }): Readonly { + public static parseHomeTabView(data: { settings: Settings, version: string, userId: string}): Readonly { const debugInformation: string[] = [ data.settings.configSource ? `Config loaded from ${data.settings.configSource}` : null, `Data provider: ${(data.settings.dataProvider as LounasDataProvider).id} (${(data.settings.dataProvider as LounasDataProvider).baseUrl})`, `[LounasEmoji] ${data.settings.emojiRules?.size ? `${data.settings.emojiRules?.size} regular expressions successfully loaded` : "No rules loaded"}`, - data.jobs.cacheClearing ? `Cached data will be cleared at ${data.jobs.cacheClearing.nextInvocation().toLocaleString("en-US")}` : null, - data.jobs.prefetch ? `Next data prefetching will occur at ${data.jobs.prefetch.nextInvocation().toLocaleString("en-US")}` : null + global.LOUNASBOTTI_JOBS.cacheClearing ? `Cached data will be cleared at ${global.LOUNASBOTTI_JOBS.cacheClearing.nextInvocation().toLocaleString("en-US")}` : null, + global.LOUNASBOTTI_JOBS.prefetch ? `Automatic posting to subscribed channels will next occur at ${global.LOUNASBOTTI_JOBS.prefetch.nextInvocation().toLocaleString("en-US")}` : null, + `${(data.settings.subscribedChannels || []).length} channel subscription(s)` ].filter(Boolean) as string[]; return HomeTab() @@ -65,6 +65,8 @@ export default class BlockParsers { Md.bold("Komennot") + "\n" + [ ["/lounas [ | tänään | huomenna]", "Aktivoi Lounasbotti ja hae lounaslistat"], + ["/lounasbotti subscribe", "Aktivoi automaattinen tila kanavalle (Arkispäivisin klo. 10:30)"], + ["/lounasbotti unsubscribe", "Poista automaattinen tila käytöstä"], ["/lounasbotti restart", "Käynnistä sovellus uudelleen"], ["/lounasbotti ping", "Pong!"] ].map(command => `${Md.codeInline(command[0])} - ${command[1]}`).join("\n") diff --git a/src/BotCommands.ts b/src/BotCommands.ts index 02f75f9..e6235ae 100644 --- a/src/BotCommands.ts +++ b/src/BotCommands.ts @@ -1,6 +1,9 @@ import bolt from "@slack/bolt"; +import { Md } from "slack-block-builder"; +import { Settings } from "./model/Settings"; +import * as SettingsRepository from "./model/SettingsRepository.js"; -export default function(app: bolt.App) { +export default function(app: bolt.App, settings: Settings) { app.command("/whoami", async args => { args.ack(); args.respond({ @@ -40,6 +43,72 @@ export default function(app: bolt.App) { }); return; } + + if (args.command.text.trim().toLowerCase() === "subscribe") { + if (settings.subscribedChannels?.includes(args.body.channel_id)) { + args.respond({ + response_type: "ephemeral", + text: `Lounasbotti is already subscribed to this channel. Use ${Md.codeInline("/lounasbotti unsubscribe")} to unsubscribe.` + }); + return; + } + + // TODO: Check rights to channel + + SettingsRepository.update({ + instanceId: settings.instanceId, + $push: { + subscribedChannels: args.body.channel_id + } + }).then(instanceSettings => { + settings.subscribedChannels = instanceSettings.subscribedChannels; + console.info(`User ${args.body.user_id} (${args.body.user_name}) subscribed to channel ${args.body.channel_id}`); + args.respond({ + response_type: "in_channel", + text: `${Md.user(args.body.user_id)} subscribed Lounasbotti to this channel. Next automatic activation will happen at ${Md.codeInline(global.LOUNASBOTTI_JOBS.prefetch.nextInvocation().toLocaleString("en-US"))}. Use ${Md.codeInline("/lounasbotti unsubscribe")} to unsubscribe.` + }); + }).catch(error => { + console.error(error); + args.respond({ + response_type: "ephemeral", + text: "Error subscribing to channel. Please contact support." + }); + }); + + return; + } + + if (args.command.text.trim().toLowerCase() === "unsubscribe") { + if (!settings.subscribedChannels?.includes(args.body.channel_id)) { + args.respond({ + response_type: "ephemeral", + text: `Lounasbotti is not subscribed to this channel. Use ${Md.codeInline("/lounasbotti subscribe")} to subscribe.` + }); + return; + } + + SettingsRepository.update({ + instanceId: settings.instanceId, + $pull: { + subscribedChannels: args.body.channel_id + } + }).then(instanceSettings => { + settings.subscribedChannels = instanceSettings.subscribedChannels; + console.info(`User ${args.body.user_id} (${args.body.user_name}) unsubscribed channel ${args.body.channel_id}`); + args.respond({ + response_type: "in_channel", + text: `${Md.user(args.body.user_id)} unsubscribed Lounasbotti from this channel. Use ${Md.codeInline("/lounasbotti subscribe")} to subscribe again.` + }); + }).catch(error => { + console.error(error); + args.respond({ + response_type: "ephemeral", + text: "Error unsubscribing from channel. Please contact support." + }); + }); + + return; + } } args.respond({ diff --git a/src/BotEvents.ts b/src/BotEvents.ts index 42a29c5..165c5c1 100644 --- a/src/BotEvents.ts +++ b/src/BotEvents.ts @@ -1,5 +1,4 @@ import bolt, {Button, GenericMessageEvent, SectionBlock, SlackCommandMiddlewareArgs } from "@slack/bolt"; -import { Job, scheduleJob, Range } from "node-schedule"; import * as Utils from "./Utils.js"; @@ -10,6 +9,7 @@ import * as LounasRepository from "./model/LounasRepository.js"; import BlockParsers from "./BlockParsers.js"; import { BlockCollection, Blocks, Md } from "slack-block-builder"; import { StringIndexed } from "@slack/bolt/dist/types/helpers.js"; +import { Range, scheduleJob } from "node-schedule"; type MessageMiddlewareArgs = bolt.SlackEventMiddlewareArgs<"message"> & bolt.AllMiddlewareArgs; type CommandMiddlewareArgs = SlackCommandMiddlewareArgs & bolt.AllMiddlewareArgs; @@ -19,12 +19,11 @@ const TOMORROW_REQUEST_REGEXP = /huomenna|tomorrow/i; const toBeTruncated: { channel: string, ts: string }[] = []; -const jobs: Record = {}; const lounasCache: Record = {}; // eslint-disable-next-line max-params const initEvents = (app: bolt.App, settings: Settings, dataProvider: LounasDataProvider, version: string): void => { - jobs.prefetch = scheduleJob({ + global.LOUNASBOTTI_JOBS.prefetch = scheduleJob({ second: 30, minute: 30, hour: 10, @@ -32,12 +31,12 @@ const initEvents = (app: bolt.App, settings: Settings, dataProvider: LounasDataP tz: "Europe/Helsinki", }, () => { - console.debug("Prefetching data..."); - getDataAndCache(dataProvider, settings, false); + console.debug("Handling subscriptions..."); + mainTrigger(false); }); // Automatic cache clearing - jobs.cacheClearing = scheduleJob({ + global.LOUNASBOTTI_JOBS.cacheClearing = scheduleJob({ second: 15, minute: 15, hour: 0, @@ -58,7 +57,7 @@ const initEvents = (app: bolt.App, settings: Settings, dataProvider: LounasDataP app.event("app_home_opened", async args => { args.client.views.publish({ user_id: args.event.user, - view: BlockParsers.parseHomeTabView({settings, version, jobs, userId: args.event.user}) + view: BlockParsers.parseHomeTabView({settings, version, userId: args.event.user}) }); }); @@ -223,25 +222,13 @@ const initEvents = (app: bolt.App, settings: Settings, dataProvider: LounasDataP }); // The main business logic - async function mainTrigger(isTomorrowRequest: boolean, args: MessageMiddlewareArgs | CommandMiddlewareArgs): Promise { + async function mainTrigger(isTomorrowRequest: boolean, args?: MessageMiddlewareArgs | CommandMiddlewareArgs): Promise { if (isTomorrowRequest) { console.debug("Tomorrow request!"); } - const isMessage = args.payload.type === "message"; - - const cachedData = await getDataAndCache(dataProvider, settings, true, isTomorrowRequest); - cachedData.blocks.push(BlockCollection(Blocks.Context().elements( - `${Md.emoji("alarm_clock")} Tämä viesti poistetaan automaattisesti 6 tunnin kuluttua\n` - + `${Md.emoji("robot_face")} Pyynnön lähetti ${Md.user(isMessage ? ((args as MessageMiddlewareArgs).message as GenericMessageEvent).user : args.body.user_id)}` - ))[0]); - - const response = await args.say({ - blocks: cachedData.blocks, - text: "Lounaslistat", - unfurl_links: false, - unfurl_media: false - }); + const isAutomatic = !args; + const isMessage = args?.payload?.type === "message"; // Add hourglass reaction if (isMessage) { @@ -255,31 +242,50 @@ const initEvents = (app: bolt.App, settings: Settings, dataProvider: LounasDataP }); } - if (response.ok && response.ts) { - if (!settings.debug?.noDb && !isTomorrowRequest) { - LounasRepository.create({ - instanceId: settings.instanceId, - ts: response.ts, - channel: response.channel ?? args.payload.channel, - menu: cachedData.data.map(lounasResponse => { - return {restaurant: lounasResponse.restaurant, items: lounasResponse.items || null}; - }), - date: new Date(), - votes: [] - }).catch(error => { - console.error(error); + const cachedData = await getDataAndCache(dataProvider, settings, true, isTomorrowRequest); + + let requester: string; + if (isAutomatic) { + requester = Md.italic("Subscription"); + } + else if (isMessage) { + requester = Md.user(((args as MessageMiddlewareArgs).message as GenericMessageEvent).user); + } + else { + requester = Md.user(args.body.user_id); + } + + cachedData.blocks.push(BlockCollection(Blocks.Context().elements( + `${Md.emoji("alarm_clock")} Tämä viesti poistetaan automaattisesti 6 tunnin kuluttua\n` + + `${Md.emoji("robot_face")} Pyynnön lähetti ${requester}` + ))[0]); + + if (isAutomatic) { + settings.subscribedChannels?.forEach(async channel => { + const response = await app.client.chat.postMessage({ + channel, + blocks: cachedData.blocks, + text: "Lounaslistat", + unfurl_links: false, + unfurl_media: false }); - } - - toBeTruncated.push({ - channel: response.channel ?? args.payload.channel, - ts: response.ts + + handleMainTriggerResponse(response, settings, channel, cachedData.data, isTomorrowRequest); + + setTimeout(truncateMessage.bind(null, app), AUTO_TRUNCATE_TIMEOUT); }); - - // Something to think about: Is the reference to app always usable after 6 hrs? Should be as JS uses Call-by-Sharing and App is never reassigned. - setTimeout(truncateMessage.bind(null, app), AUTO_TRUNCATE_TIMEOUT); + } + else { + const response = await args.say({ + blocks: cachedData.blocks, + text: "Lounaslistat", + unfurl_links: false, + unfurl_media: false + }); + + handleMainTriggerResponse(response, settings, response.channel ?? args.payload.channel, cachedData.data, isTomorrowRequest); - if (response.channel) { + if (response.channel && response.ts) { args.client.reactions.add({ channel: response.channel, name: "thumbsup", @@ -292,9 +298,8 @@ const initEvents = (app: bolt.App, settings: Settings, dataProvider: LounasDataP } }); } - } else { - console.warn("Response not okay!"); - console.debug(response); + + setTimeout(truncateMessage.bind(null, app), AUTO_TRUNCATE_TIMEOUT); } return Promise.resolve(); @@ -303,6 +308,33 @@ const initEvents = (app: bolt.App, settings: Settings, dataProvider: LounasDataP export { initEvents }; +function handleMainTriggerResponse(response: any, settings: Settings, channel: string, cachedData: LounasResponse[], isTomorrowRequest: boolean) { + if (response.ok && response.ts) { + if (!settings.debug?.noDb && !isTomorrowRequest) { + LounasRepository.create({ + instanceId: settings.instanceId, + ts: response.ts, + channel: response.channel ?? channel, + menu: cachedData.map(lounasResponse => { + return {restaurant: lounasResponse.restaurant, items: lounasResponse.items || null}; + }), + date: new Date(), + votes: [] + }).catch(error => { + console.error(error); + }); + } + + toBeTruncated.push({ + channel: response.channel ?? channel, + ts: response.ts + }); + } else { + console.warn("Response not okay!"); + console.debug(response); + } +} + function truncateMessage(app: bolt.App): void { const message = toBeTruncated.shift(); if (!message) { diff --git a/src/model/Settings.ts b/src/model/Settings.ts index ac86648..9939af9 100644 --- a/src/model/Settings.ts +++ b/src/model/Settings.ts @@ -29,6 +29,7 @@ class Settings { // Instance settings public limitToOneVotePerUser = false; + public subscribedChannels?: string[] | undefined; constructor(json: any, VERSION: string) { this.instanceId = Utils.requireNonNullOrUndefined(json.instanceId, "Parameter instanceId is required"); @@ -123,7 +124,8 @@ class Settings { type InstanceSettings = { instanceId: string, triggerRegExp?: RegExp | undefined, - limitToOneVotePerUser?: boolean + limitToOneVotePerUser?: boolean, + subscribedChannels?: string[] | undefined; }; enum Restaurant { @@ -186,6 +188,7 @@ const readAndParseSettings = async (VERSION: string, config?: string | undefined const readInstanceSettings = (settings: Settings): void => { SettingsRepository.findOrCreate(settings.instanceId).then(instanceSettings => { settings.limitToOneVotePerUser = Boolean(instanceSettings.limitToOneVotePerUser); + settings.subscribedChannels = instanceSettings.subscribedChannels; if (instanceSettings.triggerRegExp) { console.debug(`Custom trigger enabled for instance "${settings.instanceId}" (${instanceSettings.triggerRegExp.source})`); diff --git a/src/model/SettingsRepository.ts b/src/model/SettingsRepository.ts index e270eb1..3406ddd 100644 --- a/src/model/SettingsRepository.ts +++ b/src/model/SettingsRepository.ts @@ -1,10 +1,11 @@ -import mongoose from "mongoose"; +import mongoose, { UpdateQuery } from "mongoose"; import { InstanceSettings } from "./Settings"; const instanceSettingsSchema = new mongoose.Schema({ instanceId: String, triggerRegExp: String, - limitToOneVotePerUser: Boolean + limitToOneVotePerUser: Boolean, + subscribedChannels: [String] }); const InstanceSettingsModel = mongoose.model("InstanceSettings", instanceSettingsSchema); @@ -29,12 +30,13 @@ const findOrCreate = async (instanceId: string): Promise => { return { instanceId: json.instanceId, triggerRegExp: json.triggerRegExp ? new RegExp(json.triggerRegExp, "i") : undefined, - limitToOneVotePerUser: Boolean(json.limitToOneVotePerUser) + limitToOneVotePerUser: Boolean(json.limitToOneVotePerUser), + subscribedChannels: json.subscribedChannels }; }; -const update = async (update: InstanceSettings): Promise => { - const actualUpdate: any = {...update}; +const update = async (update: InstanceSettings | UpdateQuery): Promise => { + const actualUpdate: UpdateQuery = {...update}; if (update.triggerRegExp) { actualUpdate.triggerRegExp = update.triggerRegExp.source; } @@ -55,7 +57,8 @@ const update = async (update: InstanceSettings): Promise => { return { instanceId: json.instanceId, triggerRegExp: json.triggerRegExp ? new RegExp(json.triggerRegExp, "i") : undefined, - limitToOneVotePerUser: Boolean(json.limitToOneVotePerUser) + limitToOneVotePerUser: Boolean(json.limitToOneVotePerUser), + subscribedChannels: json.subscribedChannels }; }; diff --git a/src/server.ts b/src/server.ts index 043c776..cc81df3 100644 --- a/src/server.ts +++ b/src/server.ts @@ -30,7 +30,10 @@ import AdminEvents from "./AdminEvents.js"; import { decodeBase64 } from "./Utils.js"; import BotCommands from "./BotCommands.js"; -const VERSION = process.env["npm_package_version"] ?? "1.7.4"; +// Global +global.LOUNASBOTTI_JOBS = {}; + +const VERSION = process.env["npm_package_version"] ?? "1.8.0"; console.info(`Lounasbotti v${VERSION} server starting...`); if (!process.env["SLACK_SECRET"] @@ -82,7 +85,7 @@ readAndParseSettings(VERSION, process.env["SLACK_CONFIG_NAME"], configURLs).then throw new Error("Incorrect dataProvider"); } - BotCommands(app); + BotCommands(app, settings); BotEvents.initEvents(app, settings, settings.dataProvider, VERSION); AdminEvents(app, settings); BotActions(app, settings); diff --git a/src/types.d.ts b/src/types.d.ts index e69de29..33ee327 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -0,0 +1,6 @@ +/* eslint-disable no-var */ +import { Job } from "node-schedule"; + +declare global { + var LOUNASBOTTI_JOBS: Record; +} \ No newline at end of file