mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-21 16:09:12 +03:00
UBERF-7604: Telegram notifications service (#6182)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
c577282192
commit
36cbccfeb2
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@ -244,7 +244,7 @@
|
||||
"MINIO_ACCESS_KEY": "minioadmin",
|
||||
"MINIO_SECRET_KEY": "minioadmin",
|
||||
"FRONT_URL": "http://localhost:8080",
|
||||
"PORT": "3500",
|
||||
"PORT": "3500"
|
||||
},
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"sourceMaps": true,
|
||||
@ -299,7 +299,7 @@
|
||||
"MONGO_URL": "mongodb://localhost:27017",
|
||||
"MINIO_ENDPOINT": "localhost",
|
||||
"MINIO_ACCESS_KEY": "minioadmin",
|
||||
"MINIO_SECRET_KEY": "minioadmin",
|
||||
"MINIO_SECRET_KEY": "minioadmin"
|
||||
},
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"runtimeVersion": "20",
|
||||
|
@ -620,6 +620,9 @@ dependencies:
|
||||
'@rush-temp/pod-telegram':
|
||||
specifier: file:./projects/pod-telegram.tgz
|
||||
version: file:projects/pod-telegram.tgz(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4)
|
||||
'@rush-temp/pod-telegram-bot':
|
||||
specifier: file:./projects/pod-telegram-bot.tgz
|
||||
version: file:projects/pod-telegram-bot.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
||||
'@rush-temp/preference':
|
||||
specifier: file:./projects/preference.tgz
|
||||
version: file:projects/preference.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2)
|
||||
@ -1100,6 +1103,9 @@ dependencies:
|
||||
'@storybook/testing-library':
|
||||
specifier: ^0.0.14-next.2
|
||||
version: 0.0.14-next.2
|
||||
'@telegraf/entity':
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0
|
||||
'@tiptap/core':
|
||||
specifier: ^2.2.4
|
||||
version: 2.2.4(@tiptap/pm@2.2.4)
|
||||
@ -1307,9 +1313,6 @@ dependencies:
|
||||
'@types/web-push':
|
||||
specifier: ~3.6.3
|
||||
version: 3.6.3
|
||||
'@types/ws':
|
||||
specifier: ^8.5.11
|
||||
version: 8.5.11
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^6.11.0
|
||||
version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
|
||||
@ -1508,9 +1511,6 @@ dependencies:
|
||||
eslint-plugin-svelte:
|
||||
specifier: ^2.35.1
|
||||
version: 2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2)
|
||||
express:
|
||||
specifier: ^4.19.2
|
||||
version: 4.19.2
|
||||
express-fileupload:
|
||||
specifier: ^1.5.1
|
||||
version: 1.5.1
|
||||
@ -1778,6 +1778,9 @@ dependencies:
|
||||
tar-stream:
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
telegraf:
|
||||
specifier: ^4.16.3
|
||||
version: 4.16.3
|
||||
telegram:
|
||||
specifier: 2.22.2
|
||||
version: 2.22.2
|
||||
@ -1835,9 +1838,6 @@ dependencies:
|
||||
winston-daily-rotate-file:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0(winston@3.13.1)
|
||||
ws:
|
||||
specifier: ^8.18.0
|
||||
version: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
||||
y-prosemirror:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.2(prosemirror-model@1.19.4)(y-protocols@1.0.6)(yjs@13.6.12)
|
||||
@ -2688,7 +2688,7 @@ packages:
|
||||
'@babel/core': 7.23.9
|
||||
'@babel/helper-compilation-targets': 7.23.6
|
||||
'@babel/helper-plugin-utils': 7.22.5
|
||||
debug: 4.3.4
|
||||
debug: 4.3.5
|
||||
lodash.debounce: 4.0.8
|
||||
resolve: 1.22.8
|
||||
transitivePeerDependencies:
|
||||
@ -8283,6 +8283,16 @@ packages:
|
||||
defer-to-connect: 2.0.1
|
||||
dev: false
|
||||
|
||||
/@telegraf/entity@0.5.0:
|
||||
resolution: {integrity: sha512-4oHOoXcrNaK44zPq4GuTgMmUvCSQxJRVAuPFzVtSeiKzCBJnLeYblsMqWotokhrZSDnNpunC1sxhqI3iVYa/sg==}
|
||||
dependencies:
|
||||
'@telegraf/types': 7.1.0
|
||||
dev: false
|
||||
|
||||
/@telegraf/types@7.1.0:
|
||||
resolution: {integrity: sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==}
|
||||
dev: false
|
||||
|
||||
/@testing-library/dom@8.20.1:
|
||||
resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==}
|
||||
engines: {node: '>=12'}
|
||||
@ -10175,6 +10185,13 @@ packages:
|
||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||
dev: false
|
||||
|
||||
/abort-controller@3.0.0:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||
engines: {node: '>=6.5'}
|
||||
dependencies:
|
||||
event-target-shim: 5.0.1
|
||||
dev: false
|
||||
|
||||
/accepts@1.3.8:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -11216,6 +11233,17 @@ packages:
|
||||
resolution: {integrity: sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==}
|
||||
dev: false
|
||||
|
||||
/buffer-alloc-unsafe@1.1.0:
|
||||
resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==}
|
||||
dev: false
|
||||
|
||||
/buffer-alloc@1.2.0:
|
||||
resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==}
|
||||
dependencies:
|
||||
buffer-alloc-unsafe: 1.1.0
|
||||
buffer-fill: 1.0.0
|
||||
dev: false
|
||||
|
||||
/buffer-crc32@0.2.13:
|
||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||
dev: false
|
||||
@ -11239,6 +11267,10 @@ packages:
|
||||
engines: {node: '>=0.4'}
|
||||
dev: false
|
||||
|
||||
/buffer-fill@1.0.0:
|
||||
resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==}
|
||||
dev: false
|
||||
|
||||
/buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
dev: false
|
||||
@ -14126,6 +14158,11 @@ packages:
|
||||
es5-ext: 0.10.64
|
||||
dev: false
|
||||
|
||||
/event-target-shim@5.0.1:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/eventemitter3@4.0.7:
|
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||
dev: false
|
||||
@ -18870,6 +18907,11 @@ packages:
|
||||
retry: 0.13.1
|
||||
dev: false
|
||||
|
||||
/p-timeout@4.1.0:
|
||||
resolution: {integrity: sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==}
|
||||
engines: {node: '>=10'}
|
||||
dev: false
|
||||
|
||||
/p-timeout@5.1.0:
|
||||
resolution: {integrity: sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==}
|
||||
engines: {node: '>=12'}
|
||||
@ -20513,6 +20555,12 @@ packages:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
dev: false
|
||||
|
||||
/safe-compare@1.1.4:
|
||||
resolution: {integrity: sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==}
|
||||
dependencies:
|
||||
buffer-alloc: 1.2.0
|
||||
dev: false
|
||||
|
||||
/safe-regex-test@1.0.3:
|
||||
resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -20540,6 +20588,11 @@ packages:
|
||||
rimraf: 2.7.1
|
||||
dev: false
|
||||
|
||||
/sandwich-stream@2.0.2:
|
||||
resolution: {integrity: sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==}
|
||||
engines: {node: '>= 0.10'}
|
||||
dev: false
|
||||
|
||||
/sanitize-filename@1.6.3:
|
||||
resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==}
|
||||
dependencies:
|
||||
@ -21848,6 +21901,24 @@ packages:
|
||||
yallist: 4.0.0
|
||||
dev: false
|
||||
|
||||
/telegraf@4.16.3:
|
||||
resolution: {integrity: sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==}
|
||||
engines: {node: ^12.20.0 || >=14.13.1}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@telegraf/types': 7.1.0
|
||||
abort-controller: 3.0.0
|
||||
debug: 4.3.5
|
||||
mri: 1.2.0
|
||||
node-fetch: 2.7.0
|
||||
p-timeout: 4.1.0
|
||||
safe-compare: 1.1.4
|
||||
sandwich-stream: 2.0.2
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/telegram@2.22.2:
|
||||
resolution: {integrity: sha512-9payizc801Aqqu4eTGPc0huxKnIwFe0hn18mrpbgAKKiMLurX/UIQW/fjPhI5ONzockDFsaXDFqTreXBoG1u3A==}
|
||||
dependencies:
|
||||
@ -27423,7 +27494,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/model-notification.tgz:
|
||||
resolution: {integrity: sha512-cz+OQrGb1FXp0P2DrgPLtwpwFx3gamnrO71yB3KC429Juu+HrO5oakAI34vsXZyLKC5vddp0WigK4VObJFPv7A==, tarball: file:projects/model-notification.tgz}
|
||||
resolution: {integrity: sha512-k7valecajKQwOhAeROASPm+RO2/D3xZSkan8Z09kmy+9dNLYBxQT/mr7k+HBdlzU4JD3fDQRP6EStyJBkLFZsQ==, tarball: file:projects/model-notification.tgz}
|
||||
name: '@rush-temp/model-notification'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
@ -28602,7 +28673,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/notification.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-fdBECYYan/iSrhZ5bICAU35WUBHFdiCD+D5xJTddijqu6PSNj0Q4hqkphWnmg0MXNKfqDg/RaLA0rPZBdklekA==, tarball: file:projects/notification.tgz}
|
||||
resolution: {integrity: sha512-wk161j1J5wPe+ZzgCc7y5X+0fyWi+NpQJ/nrGgDMd6hOW4K7patIv3iwCF/sY28WD5ygVe1ezi4x0bbv4bQJzw==, tarball: file:projects/notification.tgz}
|
||||
id: file:projects/notification.tgz
|
||||
name: '@rush-temp/notification'
|
||||
version: 0.0.0
|
||||
@ -29445,7 +29516,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/pod-server.tgz:
|
||||
resolution: {integrity: sha512-XTI4qfulSUmxQELlvipQdb6F4i2MG1uBldX7HHP19oBB7fTn85S/qXQols9NOggFWtgIw5ApT2VdQqiJLqmOGQ==, tarball: file:projects/pod-server.tgz}
|
||||
resolution: {integrity: sha512-91Ac7EN5mpCKBG8sIWUljU+qSb6mtEJNVt9TynP8U0++Bhlmyvcryd4k2rMC5S+M/GvmlikUpAZ+MdIBsqUM+g==, tarball: file:projects/pod-server.tgz}
|
||||
name: '@rush-temp/pod-server'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
@ -29579,6 +29650,65 @@ packages:
|
||||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
file:projects/pod-telegram-bot.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4):
|
||||
resolution: {integrity: sha512-T7EdAT4nMEnEDROxVBBAwOD7ssjiSl6eOr96YFwfZvHlbaR6wJm/XYNzwOM61oc0+rjfnmkmM+FzOwTSifePEA==, tarball: file:projects/pod-telegram-bot.tgz}
|
||||
id: file:projects/pod-telegram-bot.tgz
|
||||
name: '@rush-temp/pod-telegram-bot'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@telegraf/entity': 0.5.0
|
||||
'@tsconfig/node16': 1.0.4
|
||||
'@types/cors': 2.8.17
|
||||
'@types/express': 4.17.21
|
||||
'@types/jest': 29.5.12
|
||||
'@types/node': 20.11.19
|
||||
'@types/node-fetch': 2.6.11
|
||||
'@types/otp-generator': 4.0.2
|
||||
'@types/ws': 8.5.11
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
|
||||
cors: 2.8.5
|
||||
dotenv: 16.0.3
|
||||
esbuild: 0.20.1
|
||||
eslint: 8.56.0
|
||||
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint-plugin-import: 2.29.1(eslint@8.56.0)
|
||||
eslint-plugin-n: 15.7.0(eslint@8.56.0)
|
||||
eslint-plugin-node: 11.1.0(eslint@8.56.0)
|
||||
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
|
||||
express: 4.19.2
|
||||
htmlparser2: 9.1.0
|
||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
||||
mongodb: 6.8.0
|
||||
node-fetch: 2.7.0
|
||||
otp-generator: 4.0.1
|
||||
prettier: 3.2.5
|
||||
telegraf: 4.16.3
|
||||
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
|
||||
ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/credential-providers'
|
||||
- '@babel/core'
|
||||
- '@jest/types'
|
||||
- '@mongodb-js/zstd'
|
||||
- '@swc/core'
|
||||
- '@swc/wasm'
|
||||
- babel-jest
|
||||
- babel-plugin-macros
|
||||
- bufferutil
|
||||
- encoding
|
||||
- gcp-metadata
|
||||
- kerberos
|
||||
- mongodb-client-encryption
|
||||
- node-notifier
|
||||
- snappy
|
||||
- socks
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
file:projects/pod-telegram.tgz(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4):
|
||||
resolution: {integrity: sha512-MF+eEeVhFR4XQj2YaAP6gjvm1tijtnRUMDrQt48vZBD8mwQA8B0drOVHCkOQ+NJOYqi1c3pRG/+kPKWqswbplQ==, tarball: file:projects/pod-telegram.tgz}
|
||||
id: file:projects/pod-telegram.tgz
|
||||
@ -32367,7 +32497,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/server-telegram-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-4Pob/P+5sdtU7LGgb1jJbqVms72MXOpAvOZ+GivgI3j9JvqvbOfHn87O1jVhrWa865xZrdGlV9+T9grap3W3xg==, tarball: file:projects/server-telegram-resources.tgz}
|
||||
resolution: {integrity: sha512-VsEVxoPM/hyelQa9/rQFzo+vbS8ixxU4RvVWdM0ae9HJuaEyszX4y9GheG/SDMa0NzCtyOOLf9KqL3ohGpkWRg==, tarball: file:projects/server-telegram-resources.tgz}
|
||||
id: file:projects/server-telegram-resources.tgz
|
||||
name: '@rush-temp/server-telegram-resources'
|
||||
version: 0.0.0
|
||||
@ -32387,20 +32517,13 @@ packages:
|
||||
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/credential-providers'
|
||||
- '@babel/core'
|
||||
- '@jest/types'
|
||||
- '@mongodb-js/zstd'
|
||||
- '@types/node'
|
||||
- babel-jest
|
||||
- babel-plugin-macros
|
||||
- esbuild
|
||||
- gcp-metadata
|
||||
- kerberos
|
||||
- mongodb-client-encryption
|
||||
- node-notifier
|
||||
- snappy
|
||||
- socks
|
||||
- supports-color
|
||||
- ts-node
|
||||
dev: false
|
||||
|
@ -221,6 +221,7 @@ export async function configurePlatform (): Promise<void> {
|
||||
setMetadata(presentation.metadata.FrontVersion, config.VERSION)
|
||||
}
|
||||
setMetadata(telegram.metadata.TelegramURL, config.TELEGRAM_URL ?? 'http://localhost:8086')
|
||||
setMetadata(telegram.metadata.BotUrl, config.TELEGRAM_BOT_URL)
|
||||
setMetadata(gmail.metadata.GmailURL, config.GMAIL_URL ?? 'http://localhost:8087')
|
||||
setMetadata(calendar.metadata.CalendarServiceURL, config.CALENDAR_URL ?? 'http://localhost:8095')
|
||||
setMetadata(notification.metadata.PushPublicKey, config.PUSH_PUBLIC_KEY)
|
||||
|
@ -30,6 +30,7 @@ export interface Config {
|
||||
PREVIEW_CONFIG: string
|
||||
DESKTOP_UPDATES_URL?: string
|
||||
DESKTOP_UPDATES_CHANNEL?: string
|
||||
TELEGRAM_BOT_URL?: string
|
||||
}
|
||||
|
||||
export interface Branding {
|
||||
|
@ -28,7 +28,7 @@
|
||||
let isConnectionEstablished = false
|
||||
let connectionError: Error | undefined
|
||||
|
||||
let info: { name: string, username: string } | undefined = undefined
|
||||
let info: { name: string, username: string, photoUrl: string } | undefined = undefined
|
||||
let isLoading = false
|
||||
|
||||
const url = getMetadata(telegram.metadata.BotUrl) ?? ''
|
||||
@ -122,8 +122,11 @@
|
||||
{:else if info}
|
||||
<div class="flex-col mt-2">
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
<!--TODO: use telegram bot avatar-->
|
||||
<Icon icon={TelegramColor} size="medium" />
|
||||
{#if info.photoUrl !== ''}
|
||||
<img class="photo" src={info.photoUrl} alt="" />
|
||||
{:else}
|
||||
<Icon icon={TelegramColor} size="x-large" />
|
||||
{/if}
|
||||
{info.name} (@{info.username})
|
||||
<ModernButton
|
||||
label={telegram.string.TestConnection}
|
||||
@ -186,4 +189,10 @@
|
||||
color: var(--theme-link-color);
|
||||
}
|
||||
}
|
||||
|
||||
.photo {
|
||||
border-radius: 50%;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
</style>
|
||||
|
@ -72,6 +72,8 @@
|
||||
"@hcengineering/contact": "^0.6.24",
|
||||
"@hcengineering/notification": "^0.6.23",
|
||||
"@hcengineering/server-notification": "^0.6.1",
|
||||
"@hcengineering/server-telegram": "^0.6.0",
|
||||
"@hcengineering/pod-telegram-bot": "^0.6.0",
|
||||
"@hcengineering/server-ai-bot": "^0.6.0",
|
||||
"@hcengineering/server-ai-bot-resources": "^0.6.0"
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import serverNotification from '@hcengineering/server-notification'
|
||||
import serverToken from '@hcengineering/server-token'
|
||||
import { serverFactories } from '@hcengineering/server-ws/src/factories'
|
||||
import { SplitLogger, configureAnalytics } from '@hcengineering/analytics-service'
|
||||
import serverTelegram from '@hcengineering/server-telegram'
|
||||
import serverAiBot from '@hcengineering/server-ai-bot'
|
||||
import { join } from 'path'
|
||||
import { start } from '.'
|
||||
@ -51,6 +52,7 @@ setMetadata(notification.metadata.PushPublicKey, config.pushPublicKey)
|
||||
setMetadata(serverNotification.metadata.PushPrivateKey, config.pushPrivateKey)
|
||||
setMetadata(serverNotification.metadata.PushSubject, config.pushSubject)
|
||||
setMetadata(serverCore.metadata.ElasticIndexName, config.elasticIndexName)
|
||||
setMetadata(serverTelegram.metadata.BotUrl, process.env.TELEGRAM_BOT_URL)
|
||||
setMetadata(serverAiBot.metadata.SupportWorkspaceId, process.env.SUPPORT_WORKSPACE)
|
||||
|
||||
const shutdown = start(config.url, {
|
||||
|
@ -2116,6 +2116,11 @@
|
||||
"packageName": "@hcengineering/server-ai-bot-resources",
|
||||
"projectFolder": "server-plugins/ai-bot-resources",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/pod-telegram-bot",
|
||||
"projectFolder": "services/telegram-bot/pod-telegram-bot",
|
||||
"shouldPublish": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
7
services/telegram-bot/pod-telegram-bot/.eslintrc.js
Normal file
7
services/telegram-bot/pod-telegram-bot/.eslintrc.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
4
services/telegram-bot/pod-telegram-bot/.npmignore
Normal file
4
services/telegram-bot/pod-telegram-bot/.npmignore
Normal file
@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
8
services/telegram-bot/pod-telegram-bot/Dockerfile
Normal file
8
services/telegram-bot/pod-telegram-bot/Dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY bundle/bundle.js ./
|
||||
|
||||
EXPOSE 4020
|
||||
CMD [ "node", "bundle.js" ]
|
4
services/telegram-bot/pod-telegram-bot/config/rig.json
Normal file
4
services/telegram-bot/pod-telegram-bot/config/rig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig"
|
||||
}
|
7
services/telegram-bot/pod-telegram-bot/jest.config.js
Normal file
7
services/telegram-bot/pod-telegram-bot/jest.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
84
services/telegram-bot/pod-telegram-bot/package.json
Normal file
84
services/telegram-bot/pod-telegram-bot/package.json
Normal file
@ -0,0 +1,84 @@
|
||||
{
|
||||
"name": "@hcengineering/pod-telegram-bot",
|
||||
"version": "0.6.0",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"files": [
|
||||
"lib/**/*",
|
||||
"types/**/*",
|
||||
"tsconfig.json"
|
||||
],
|
||||
"author": "Hardcore Engineering Inc.",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"test": "jest --passWithNoTests --silent",
|
||||
"_phase:bundle": "rushx bundle",
|
||||
"_phase:docker-build": "rushx docker:build",
|
||||
"_phase:docker-staging": "rushx docker:staging",
|
||||
"bundle": "mkdir -p bundle && esbuild src/index.ts --bundle --platform=node > bundle/bundle.js",
|
||||
"docker:build": "../../../common/scripts/docker_build.sh hardcoreeng/telegram-bot",
|
||||
"docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/telegram-bot staging",
|
||||
"docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/telegram-bot",
|
||||
"run-local": "cross-env ts-node src/index.ts",
|
||||
"format": "format src",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:test": "jest --passWithNoTests --silent",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "^0.6.0",
|
||||
"@tsconfig/node16": "^1.0.4",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/node": "~20.11.16",
|
||||
"@types/node-fetch": "~2.6.2",
|
||||
"@types/otp-generator": "^4.0.2",
|
||||
"@types/ws": "^8.5.11",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"esbuild": "^0.20.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.1.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.8.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/activity": "^0.6.0",
|
||||
"@hcengineering/chunter": "^0.6.20",
|
||||
"@hcengineering/client": "^0.6.18",
|
||||
"@hcengineering/client-resources": "^0.6.27",
|
||||
"@hcengineering/contact": "^0.6.24",
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
"@hcengineering/mongo": "^0.6.1",
|
||||
"@hcengineering/notification": "^0.6.23",
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@hcengineering/server-client": "^0.6.0",
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/server-token": "^0.6.11",
|
||||
"@hcengineering/setting": "^0.6.17",
|
||||
"@hcengineering/telegram": "^0.6.21",
|
||||
"@hcengineering/telegram-assets": "^0.6.0",
|
||||
"@hcengineering/text": "^0.6.5",
|
||||
"@telegraf/entity": "^0.5.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "~16.0.0",
|
||||
"express": "^4.19.2",
|
||||
"htmlparser2": "^9.0.0",
|
||||
"mongodb": "^6.8.0",
|
||||
"node-fetch": "^2.6.6",
|
||||
"otp-generator": "^4.0.1",
|
||||
"telegraf": "^4.16.3",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
}
|
148
services/telegram-bot/pod-telegram-bot/src/bot.ts
Normal file
148
services/telegram-bot/pod-telegram-bot/src/bot.ts
Normal file
@ -0,0 +1,148 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Context, Telegraf, NarrowedContext } from 'telegraf'
|
||||
import { Update, Message } from 'telegraf/typings/core/types/typegram'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import telegram from '@hcengineering/telegram'
|
||||
import { htmlToMarkup } from '@hcengineering/text'
|
||||
import { message } from 'telegraf/filters'
|
||||
import { toHTML } from '@telegraf/entity'
|
||||
|
||||
import config from './config'
|
||||
import { PlatformWorker } from './worker'
|
||||
import { getBotCommands, getCommandsHelp } from './utils'
|
||||
|
||||
async function onStart (ctx: Context, worker: PlatformWorker): Promise<void> {
|
||||
const id = ctx.from?.id
|
||||
const lang = ctx.from?.language_code ?? 'en'
|
||||
const record = id !== undefined ? await worker.getUserRecord(id) : undefined
|
||||
|
||||
const commandsHelp = await getCommandsHelp(lang)
|
||||
const welcomeMessage = await translate(telegram.string.WelcomeMessage, { app: config.App }, lang)
|
||||
|
||||
if (record !== undefined) {
|
||||
const connectedMessage = await translate(
|
||||
telegram.string.ConnectedDescriptionHtml,
|
||||
{ email: record.email, app: config.App },
|
||||
lang
|
||||
)
|
||||
const message = welcomeMessage + '\n\n' + commandsHelp + '\n\n' + connectedMessage
|
||||
|
||||
await ctx.replyWithHTML(message)
|
||||
} else {
|
||||
const connectMessage = await translate(telegram.string.ConnectMessage, { app: config.App }, lang)
|
||||
const message = welcomeMessage + '\n\n' + commandsHelp + '\n\n' + connectMessage
|
||||
|
||||
await ctx.reply(message)
|
||||
}
|
||||
}
|
||||
|
||||
async function onHelp (ctx: Context): Promise<void> {
|
||||
const lang = ctx.from?.language_code ?? 'en'
|
||||
const commandsHelp = await getCommandsHelp(lang)
|
||||
|
||||
await ctx.reply(commandsHelp)
|
||||
}
|
||||
|
||||
async function onStop (ctx: Context, worker: PlatformWorker): Promise<void> {
|
||||
if (ctx.from?.id !== undefined) {
|
||||
await worker.removeUserByTelegramId(ctx.from?.id)
|
||||
}
|
||||
const lang = ctx.from?.language_code ?? 'en'
|
||||
const message = await translate(telegram.string.StopMessage, { app: config.App }, lang)
|
||||
|
||||
await ctx.reply(message)
|
||||
}
|
||||
|
||||
async function onConnect (ctx: Context, worker: PlatformWorker): Promise<void> {
|
||||
const id = ctx.from?.id
|
||||
const lang = ctx.from?.language_code ?? 'en'
|
||||
|
||||
if (id === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const account = await worker.getUserRecord(id)
|
||||
|
||||
if (account !== undefined) {
|
||||
const reply = await translate(
|
||||
telegram.string.AccountAlreadyConnectedHtml,
|
||||
{ email: account.email, app: config.App },
|
||||
lang
|
||||
)
|
||||
await ctx.replyWithHTML(reply)
|
||||
return
|
||||
}
|
||||
|
||||
const code = await worker.generateCode(id)
|
||||
await ctx.reply(`*${code}*`, { parse_mode: 'MarkdownV2' })
|
||||
}
|
||||
|
||||
type TextMessage = Record<'text', any> & Message.TextMessage
|
||||
|
||||
async function onReply (
|
||||
ctx: NarrowedContext<Context<Update>, Update.MessageUpdate<TextMessage>>,
|
||||
worker: PlatformWorker
|
||||
): Promise<void> {
|
||||
const id = ctx.chat?.id
|
||||
const message = ctx.message
|
||||
|
||||
if (id === undefined || message.reply_to_message === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const replyTo = message.reply_to_message
|
||||
const userRecord = await worker.getUserRecord(id)
|
||||
|
||||
if (userRecord === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const notification = await worker.getNotificationRecord(replyTo.message_id, userRecord.email)
|
||||
|
||||
if (notification === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const isReplied = await worker.reply(notification, htmlToMarkup(toHTML(message)))
|
||||
if (isReplied) {
|
||||
await ctx.react('👍')
|
||||
}
|
||||
}
|
||||
|
||||
export async function setUpBot (worker: PlatformWorker): Promise<Telegraf> {
|
||||
const bot = new Telegraf(config.BotToken)
|
||||
|
||||
await bot.telegram.setMyCommands(await getBotCommands())
|
||||
|
||||
bot.start((ctx) => onStart(ctx, worker))
|
||||
bot.help(onHelp)
|
||||
|
||||
bot.command('stop', (ctx) => onStop(ctx, worker))
|
||||
bot.command('connect', (ctx) => onConnect(ctx, worker))
|
||||
|
||||
bot.on(message('text'), async (ctx) => {
|
||||
await onReply(ctx, worker)
|
||||
})
|
||||
|
||||
const description = await translate(telegram.string.BotDescription, { app: config.App })
|
||||
const shortDescription = await translate(telegram.string.BotShortDescription, { app: config.App })
|
||||
|
||||
await bot.telegram.setMyDescription(description, 'en')
|
||||
await bot.telegram.setMyShortDescription(shortDescription, 'en')
|
||||
|
||||
return bot
|
||||
}
|
61
services/telegram-bot/pod-telegram-bot/src/config.ts
Normal file
61
services/telegram-bot/pod-telegram-bot/src/config.ts
Normal file
@ -0,0 +1,61 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
export interface Config {
|
||||
Port: number
|
||||
BotToken: string
|
||||
FrontUrl: string
|
||||
MongoURL: string
|
||||
MongoDB: string
|
||||
ServiceId: string
|
||||
Secret: string
|
||||
Domain: string
|
||||
BotPort: number
|
||||
App: string
|
||||
OtpTimeToLiveSec: number
|
||||
OtpRetryDelaySec: number
|
||||
AccountsUrl: string
|
||||
}
|
||||
|
||||
const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined)
|
||||
|
||||
const config: Config = (() => {
|
||||
const params: Partial<Config> = {
|
||||
Port: parseNumber(process.env.PORT) ?? 4020,
|
||||
BotToken: process.env.BOT_TOKEN,
|
||||
FrontUrl: process.env.FRONT_URL,
|
||||
MongoURL: process.env.MONGO_URL,
|
||||
MongoDB: process.env.MONGO_DB,
|
||||
AccountsUrl: process.env.ACCOUNTS_URL,
|
||||
ServiceId: process.env.SERVICE_ID,
|
||||
Secret: process.env.SECRET,
|
||||
Domain: process.env.DOMAIN,
|
||||
BotPort: parseNumber(process.env.BOT_PORT) ?? 8443,
|
||||
// TODO: later we should get this title from branding map
|
||||
App: process.env.APP ?? 'Huly',
|
||||
OtpTimeToLiveSec: parseNumber(process.env.OTP_TIME_TO_LIVE_SEC) ?? 60,
|
||||
OtpRetryDelaySec: parseNumber(process.env.OTP_RETRY_DELAY_SEC) ?? 60
|
||||
}
|
||||
|
||||
const missingEnv = (Object.keys(params) as Array<keyof Config>).filter((key) => params[key] === undefined)
|
||||
|
||||
if (missingEnv.length > 0) {
|
||||
throw Error(`Missing config for attributes: ${missingEnv.join(', ')}`)
|
||||
}
|
||||
|
||||
return params as Config
|
||||
})()
|
||||
|
||||
export default config
|
39
services/telegram-bot/pod-telegram-bot/src/error.ts
Normal file
39
services/telegram-bot/pod-telegram-bot/src/error.ts
Normal file
@ -0,0 +1,39 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
const generateMessage = (code: number): string => {
|
||||
if (code === 401) {
|
||||
return 'Unauthorized'
|
||||
}
|
||||
|
||||
if (code === 404) {
|
||||
return 'Not Found'
|
||||
}
|
||||
|
||||
if (code === 400) {
|
||||
return 'Bad Request'
|
||||
}
|
||||
|
||||
return 'Error'
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
readonly code: number
|
||||
|
||||
constructor (code: number, message?: string) {
|
||||
super(message ?? generateMessage(code))
|
||||
this.code = code
|
||||
}
|
||||
}
|
21
services/telegram-bot/pod-telegram-bot/src/index.ts
Normal file
21
services/telegram-bot/pod-telegram-bot/src/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { config } from 'dotenv'
|
||||
|
||||
import { start } from './start'
|
||||
|
||||
config()
|
||||
void start()
|
89
services/telegram-bot/pod-telegram-bot/src/limiter.ts
Normal file
89
services/telegram-bot/pod-telegram-bot/src/limiter.ts
Normal file
@ -0,0 +1,89 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Timestamp } from '@hcengineering/core'
|
||||
|
||||
export class Limiter {
|
||||
// Bot should not send more than 30 messages per second, but we will limit it to 25 just to be safe
|
||||
maxMsgPerTime = 25
|
||||
timeLimit = 1000
|
||||
|
||||
sentMsg = 0
|
||||
clearTimeLimitOn: Timestamp = Date.now()
|
||||
|
||||
// Bot should not send more than 20 messages per minute to the same chat
|
||||
chatTimeLimit = 60 * 1000
|
||||
maxMsgPerChat = 20
|
||||
|
||||
clearChatLimitOn: Timestamp = Date.now()
|
||||
sentMsgByChat = new Map<number, number>()
|
||||
|
||||
constructor () {
|
||||
setInterval(() => {
|
||||
this.sentMsg = 0
|
||||
this.clearTimeLimitOn = Date.now() + this.timeLimit
|
||||
}, this.timeLimit)
|
||||
|
||||
setInterval(() => {
|
||||
this.sentMsgByChat.clear()
|
||||
this.clearChatLimitOn = Date.now() + this.chatTimeLimit
|
||||
}, this.chatTimeLimit)
|
||||
}
|
||||
|
||||
async exec<T>(op: () => Promise<T>): Promise<void> {
|
||||
while (this.sentMsg >= this.maxMsgPerTime) {
|
||||
await new Promise((resolve) => setTimeout(resolve, Math.max(Date.now() - this.clearTimeLimitOn), 10))
|
||||
}
|
||||
|
||||
this.sentMsg++
|
||||
|
||||
try {
|
||||
await op()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async add<T>(telegramId: number, op: () => Promise<T>): Promise<void> {
|
||||
await this.updateChatLimits(telegramId)
|
||||
|
||||
if (this.sentMsg >= this.maxMsgPerTime) {
|
||||
await new Promise((resolve) => setTimeout(resolve, this.maxMsgPerTime))
|
||||
}
|
||||
|
||||
void this.exec(op)
|
||||
}
|
||||
|
||||
async updateChatLimits (telegramId: number): Promise<void> {
|
||||
let counts = this.sentMsgByChat.get(telegramId) ?? 0
|
||||
|
||||
while (counts >= this.maxMsgPerChat) {
|
||||
if (counts >= this.maxMsgPerChat) {
|
||||
await this.waitChatLimits()
|
||||
}
|
||||
|
||||
counts = this.sentMsgByChat.get(telegramId) ?? 0
|
||||
}
|
||||
|
||||
this.sentMsgByChat.set(telegramId, counts + 1)
|
||||
}
|
||||
|
||||
async waitChatLimits (): Promise<void> {
|
||||
const now = Date.now()
|
||||
const diff = Math.max(this.clearChatLimitOn - now, 0)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, diff))
|
||||
}
|
||||
}
|
27
services/telegram-bot/pod-telegram-bot/src/loaders.ts
Normal file
27
services/telegram-bot/pod-telegram-bot/src/loaders.ts
Normal file
@ -0,0 +1,27 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { telegramId } from '@hcengineering/telegram'
|
||||
import { addStringsLoader, platformId } from '@hcengineering/platform'
|
||||
import { coreId } from '@hcengineering/core'
|
||||
import coreEng from '@hcengineering/core/lang/en.json'
|
||||
import platformEng from '@hcengineering/platform/lang/en.json'
|
||||
import telegramEng from '@hcengineering/telegram-assets/lang/en.json'
|
||||
|
||||
export function registerLoaders (): void {
|
||||
addStringsLoader(coreId, async (lang: string) => coreEng)
|
||||
addStringsLoader(platformId, async (lang: string) => platformEng)
|
||||
addStringsLoader(telegramId, async (lang: string) => telegramEng)
|
||||
}
|
224
services/telegram-bot/pod-telegram-bot/src/server.ts
Normal file
224
services/telegram-bot/pod-telegram-bot/src/server.ts
Normal file
@ -0,0 +1,224 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Token, decodeToken } from '@hcengineering/server-token'
|
||||
import cors from 'cors'
|
||||
import express, { type Express, type NextFunction, type Request, type Response } from 'express'
|
||||
import { IncomingHttpHeaders, type Server } from 'http'
|
||||
import { MeasureContext } from '@hcengineering/core'
|
||||
import { Telegraf } from 'telegraf'
|
||||
import telegram, { TelegramNotificationRecord } from '@hcengineering/telegram'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
|
||||
import { ApiError } from './error'
|
||||
import { PlatformWorker } from './worker'
|
||||
import { Limiter } from './limiter'
|
||||
import config from './config'
|
||||
import { toTelegramHtml } from './utils'
|
||||
|
||||
const extractCookieToken = (cookie?: string): Token | null => {
|
||||
if (cookie === undefined || cookie === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const cookies = cookie.split(';')
|
||||
const tokenCookie = cookies.find((cookie) => cookie.toLocaleLowerCase().includes('token'))
|
||||
if (tokenCookie === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
const encodedToken = tokenCookie.split('=')[1]
|
||||
if (encodedToken === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
return decodeToken(encodedToken)
|
||||
}
|
||||
|
||||
const extractAuthorizationToken = (authorization?: string): Token | null => {
|
||||
if (authorization === undefined || authorization === null) {
|
||||
return null
|
||||
}
|
||||
const encodedToken = authorization.split(' ')[1]
|
||||
|
||||
if (encodedToken === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
return decodeToken(encodedToken)
|
||||
}
|
||||
|
||||
const extractToken = (headers: IncomingHttpHeaders): Token => {
|
||||
try {
|
||||
const token = extractCookieToken(headers.cookie) ?? extractAuthorizationToken(headers.authorization)
|
||||
|
||||
if (token === null) {
|
||||
throw new ApiError(401)
|
||||
}
|
||||
|
||||
return token
|
||||
} catch {
|
||||
throw new ApiError(401)
|
||||
}
|
||||
}
|
||||
|
||||
type AsyncRequestHandler = (req: Request, res: Response, token: Token, next: NextFunction) => Promise<void>
|
||||
|
||||
const handleRequest = async (
|
||||
fn: AsyncRequestHandler,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const token = extractToken(req.headers)
|
||||
await fn(req, res, token, next)
|
||||
} catch (err: unknown) {
|
||||
next(err)
|
||||
}
|
||||
}
|
||||
|
||||
const wrapRequest = (fn: AsyncRequestHandler) => (req: Request, res: Response, next: NextFunction) => {
|
||||
void handleRequest(fn, req, res, next)
|
||||
}
|
||||
|
||||
export function createServer (bot: Telegraf, worker: PlatformWorker): Express {
|
||||
const limiter = new Limiter()
|
||||
const app = express()
|
||||
|
||||
app.use(cors())
|
||||
app.use(express.json())
|
||||
|
||||
app.post(
|
||||
'/test',
|
||||
wrapRequest(async (_, res, token) => {
|
||||
const record = await worker.getUserRecordByEmail(token.email)
|
||||
if (record === undefined) {
|
||||
throw new ApiError(404)
|
||||
}
|
||||
|
||||
await limiter.add(record.telegramId, async () => {
|
||||
const testMessage = await translate(telegram.string.TestMessage, { app: config.App })
|
||||
await bot.telegram.sendMessage(record.telegramId, testMessage)
|
||||
})
|
||||
|
||||
res.status(200)
|
||||
res.json({})
|
||||
})
|
||||
)
|
||||
|
||||
app.post(
|
||||
'/auth',
|
||||
wrapRequest(async (req, res, token) => {
|
||||
if (req.body == null || typeof req.body !== 'object') {
|
||||
throw new ApiError(400)
|
||||
}
|
||||
|
||||
const { code } = req.body
|
||||
|
||||
if (code == null || code === '' || typeof code !== 'string') {
|
||||
throw new ApiError(400)
|
||||
}
|
||||
|
||||
const record = await worker.getUserRecordByEmail(token.email)
|
||||
|
||||
if (record !== undefined) {
|
||||
throw new ApiError(409, 'User already authorized')
|
||||
}
|
||||
|
||||
const newRecord = await worker.authorizeUser(code, token.email)
|
||||
|
||||
if (newRecord === undefined) {
|
||||
throw new ApiError(500)
|
||||
}
|
||||
|
||||
void limiter.add(newRecord.telegramId, async () => {
|
||||
const message = await translate(telegram.string.AccountConnectedHtml, { app: config.App, email: token.email })
|
||||
await bot.telegram.sendMessage(newRecord.telegramId, message, { parse_mode: 'HTML' })
|
||||
})
|
||||
|
||||
res.status(200)
|
||||
res.json({})
|
||||
})
|
||||
)
|
||||
|
||||
app.get(
|
||||
'/info',
|
||||
wrapRequest(async (_, res, token) => {
|
||||
const me = await bot.telegram.getMe()
|
||||
const profilePhotos = await bot.telegram.getUserProfilePhotos(me.id)
|
||||
const photoId = profilePhotos.photos[0]?.[0]?.file_id
|
||||
|
||||
let photoUrl = ''
|
||||
|
||||
if (photoId !== undefined) {
|
||||
photoUrl = (await bot.telegram.getFileLink(photoId)).toString()
|
||||
}
|
||||
res.status(200)
|
||||
res.json({ username: me.username, name: me.first_name, photoUrl })
|
||||
})
|
||||
)
|
||||
|
||||
app.post(
|
||||
'/notify',
|
||||
wrapRequest(async (req, res, token) => {
|
||||
if (req.body == null || !Array.isArray(req.body)) {
|
||||
throw new ApiError(400)
|
||||
}
|
||||
const notificationRecords = req.body as TelegramNotificationRecord[]
|
||||
const usersRecords = await worker.getUsersRecords()
|
||||
|
||||
for (const notificationRecord of notificationRecords) {
|
||||
const userRecord = usersRecords.find((record) => record.email === token.email)
|
||||
if (userRecord !== undefined) {
|
||||
void limiter.add(userRecord.telegramId, async () => {
|
||||
const formattedMessage = toTelegramHtml(notificationRecord)
|
||||
const message = await bot.telegram.sendMessage(userRecord.telegramId, formattedMessage, {
|
||||
parse_mode: 'HTML'
|
||||
})
|
||||
await worker.addNotificationRecord({
|
||||
notificationId: notificationRecord.notificationId,
|
||||
email: userRecord.email,
|
||||
workspace: notificationRecord.workspace,
|
||||
telegramId: message.message_id
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200)
|
||||
res.json({})
|
||||
})
|
||||
)
|
||||
|
||||
app.use((err: any, _req: any, res: any, _next: any) => {
|
||||
if (err instanceof ApiError) {
|
||||
res.status(err.code).send({ code: err.code, message: err.message })
|
||||
return
|
||||
}
|
||||
|
||||
res.status(500).send(err.message?.length > 0 ? { message: err.message } : err)
|
||||
})
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
export function listen (e: Express, ctx: MeasureContext, port: number, host?: string): Server {
|
||||
const cb = (): void => {
|
||||
ctx.info(`Telegram bot service has been started at ${host ?? '*'}:${port}`)
|
||||
}
|
||||
|
||||
return host !== undefined ? e.listen(port, host, cb) : e.listen(port, cb)
|
||||
}
|
72
services/telegram-bot/pod-telegram-bot/src/start.ts
Normal file
72
services/telegram-bot/pod-telegram-bot/src/start.ts
Normal file
@ -0,0 +1,72 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { MeasureMetricsContext } from '@hcengineering/core'
|
||||
import { setMetadata } from '@hcengineering/platform'
|
||||
import serverToken from '@hcengineering/server-token'
|
||||
import serverClient from '@hcengineering/server-client'
|
||||
|
||||
import config from './config'
|
||||
import { createServer, listen } from './server'
|
||||
import { setUpBot } from './bot'
|
||||
import { PlatformWorker } from './worker'
|
||||
import { registerLoaders } from './loaders'
|
||||
|
||||
export const start = async (): Promise<void> => {
|
||||
setMetadata(serverToken.metadata.Secret, config.Secret)
|
||||
setMetadata(serverClient.metadata.Endpoint, config.AccountsUrl)
|
||||
setMetadata(serverClient.metadata.UserAgent, config.ServiceId)
|
||||
registerLoaders()
|
||||
|
||||
const ctx = new MeasureMetricsContext('telegram-bot', {})
|
||||
|
||||
const worker = await PlatformWorker.create()
|
||||
const bot = await setUpBot(worker)
|
||||
const app = createServer(bot, worker)
|
||||
|
||||
void bot.launch({ webhook: { domain: config.Domain, port: config.BotPort } }).then(() => {
|
||||
void bot.telegram.getWebhookInfo().then((info) => {
|
||||
ctx.info('Webhook info', info)
|
||||
})
|
||||
})
|
||||
app.get(`/telegraf/${bot.secretPathComponent()}`, (req, res) => {
|
||||
res.status(200).send()
|
||||
})
|
||||
app.post(`/telegraf/${bot.secretPathComponent()}`, (req, res) => {
|
||||
void bot.handleUpdate(req.body, res)
|
||||
res.status(200).send()
|
||||
})
|
||||
|
||||
const server = listen(app, ctx, config.Port)
|
||||
|
||||
const onClose = (): void => {
|
||||
server.close(() => process.exit())
|
||||
}
|
||||
|
||||
process.once('SIGINT', () => {
|
||||
bot.stop('SIGINT')
|
||||
onClose()
|
||||
})
|
||||
process.once('SIGTERM', () => {
|
||||
bot.stop('SIGTERM')
|
||||
onClose()
|
||||
})
|
||||
process.on('uncaughtException', (e: Error) => {
|
||||
console.error(e)
|
||||
})
|
||||
process.on('unhandledRejection', (e: Error) => {
|
||||
console.error(e)
|
||||
})
|
||||
}
|
31
services/telegram-bot/pod-telegram-bot/src/storage.ts
Normal file
31
services/telegram-bot/pod-telegram-bot/src/storage.ts
Normal file
@ -0,0 +1,31 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { MongoClient } from 'mongodb'
|
||||
|
||||
import config from './config'
|
||||
|
||||
export const getDB = (() => {
|
||||
let client: MongoClient | undefined
|
||||
|
||||
return async () => {
|
||||
if (client === undefined) {
|
||||
client = new MongoClient(config.MongoURL)
|
||||
await client.connect()
|
||||
}
|
||||
|
||||
return client.db(config.MongoDB)
|
||||
}
|
||||
})()
|
36
services/telegram-bot/pod-telegram-bot/src/types.ts
Normal file
36
services/telegram-bot/pod-telegram-bot/src/types.ts
Normal file
@ -0,0 +1,36 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Ref, Timestamp } from '@hcengineering/core'
|
||||
import { InboxNotification } from '@hcengineering/notification'
|
||||
|
||||
export interface UserRecord {
|
||||
telegramId: number
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface NotificationRecord {
|
||||
notificationId: Ref<InboxNotification>
|
||||
workspace: string
|
||||
email: string
|
||||
telegramId: number
|
||||
}
|
||||
|
||||
export interface OtpRecord {
|
||||
telegramId: number
|
||||
code: string
|
||||
expires: Timestamp
|
||||
createdOn: Timestamp
|
||||
}
|
179
services/telegram-bot/pod-telegram-bot/src/utils.ts
Normal file
179
services/telegram-bot/pod-telegram-bot/src/utils.ts
Normal file
@ -0,0 +1,179 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Collection } from 'mongodb'
|
||||
import otpGenerator from 'otp-generator'
|
||||
import { BotCommand } from 'telegraf/typings/core/types/typegram'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import telegram, { TelegramNotificationRecord } from '@hcengineering/telegram'
|
||||
import { Parser } from 'htmlparser2'
|
||||
|
||||
import { OtpRecord } from './types'
|
||||
import config from './config'
|
||||
|
||||
export async function getNewOtp (otpCollection: Collection<OtpRecord>): Promise<string> {
|
||||
let otp = otpGenerator.generate(6, {
|
||||
upperCaseAlphabets: false,
|
||||
lowerCaseAlphabets: false,
|
||||
specialChars: false
|
||||
})
|
||||
|
||||
let exist = await otpCollection.findOne({ otp })
|
||||
|
||||
while (exist != null) {
|
||||
otp = otpGenerator.generate(6, {
|
||||
lowerCaseAlphabets: false
|
||||
})
|
||||
exist = await otpCollection.findOne({ otp })
|
||||
}
|
||||
|
||||
return otp
|
||||
}
|
||||
|
||||
export async function getBotCommands (lang: string = 'en'): Promise<BotCommand[]> {
|
||||
return [
|
||||
{
|
||||
command: 'start',
|
||||
description: await translate(telegram.string.StartBot, { app: config.App }, lang)
|
||||
},
|
||||
{
|
||||
command: 'connect',
|
||||
description: await translate(telegram.string.ConnectAccount, { app: config.App }, lang)
|
||||
},
|
||||
{
|
||||
command: 'help',
|
||||
description: await translate(telegram.string.ShowCommandsDetails, { app: config.App }, lang)
|
||||
},
|
||||
{
|
||||
command: 'stop',
|
||||
description: await translate(telegram.string.TurnNotificationsOff, { app: config.App }, lang)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export async function getCommandsHelp (lang: string): Promise<string> {
|
||||
const myCommands = await getBotCommands(lang)
|
||||
return myCommands.map(({ command, description }) => `/${command} - ${description}`).join('\n')
|
||||
}
|
||||
|
||||
const maxTitleLength = 300
|
||||
const maxQuoteLength = 500
|
||||
const maxBodyLength = 2000
|
||||
const maxSenderLength = 100
|
||||
|
||||
export function toTelegramHtml (record: TelegramNotificationRecord): string {
|
||||
const title = record.title !== '' ? `<b>${platformToTelegram(record.title, maxTitleLength)}</b>` + '\n' : ''
|
||||
const quote =
|
||||
record.quote !== undefined && record.quote !== ''
|
||||
? `<blockquote>${platformToTelegram(record.quote, maxQuoteLength)}</blockquote>` + '\n'
|
||||
: ''
|
||||
const body = platformToTelegram(record.body, maxBodyLength)
|
||||
const sender = `<i>— ${record.sender.slice(0, maxSenderLength)}</i>`
|
||||
|
||||
return title + quote + body + '\n' + sender
|
||||
}
|
||||
|
||||
const supportedTags = ['strong', 'em', 's', 'blockquote', 'code', 'a']
|
||||
|
||||
export function platformToTelegram (message: string, limit: number): string {
|
||||
let textLength = 0
|
||||
let newMessage = ''
|
||||
const openedTags = new Map<
|
||||
string,
|
||||
{
|
||||
count: number
|
||||
}
|
||||
>()
|
||||
|
||||
const parser = new Parser({
|
||||
onopentag: (tag, attrs) => {
|
||||
if (tag === 'br' || tag === 'p') {
|
||||
return
|
||||
}
|
||||
|
||||
if (textLength >= limit) {
|
||||
return
|
||||
}
|
||||
|
||||
// Just skip unsupported tag
|
||||
if (!supportedTags.includes(tag)) {
|
||||
return
|
||||
}
|
||||
|
||||
const existingTag = openedTags.get(tag)
|
||||
if (existingTag !== undefined) {
|
||||
existingTag.count += 1
|
||||
return
|
||||
}
|
||||
|
||||
openedTags.set(tag, {
|
||||
count: 1
|
||||
})
|
||||
newMessage += `<${tag}>`
|
||||
},
|
||||
ontext: (text) => {
|
||||
if (textLength >= limit) {
|
||||
return
|
||||
}
|
||||
|
||||
textLength += unescape(text).length
|
||||
newMessage += unescape(text)
|
||||
|
||||
if (textLength > limit) {
|
||||
const extra = textLength - limit
|
||||
newMessage = newMessage.slice(0, -extra)
|
||||
}
|
||||
},
|
||||
onclosetag: (tag) => {
|
||||
const isLimit = textLength >= limit
|
||||
if (tag === 'br' && !isLimit) {
|
||||
newMessage += '\n'
|
||||
textLength += 1
|
||||
return
|
||||
}
|
||||
|
||||
if (tag === 'p' && !isLimit) {
|
||||
newMessage += '\n\n'
|
||||
textLength += 2
|
||||
return
|
||||
}
|
||||
|
||||
// Just skip unsupported tag
|
||||
if (!supportedTags.includes(tag)) {
|
||||
return
|
||||
}
|
||||
|
||||
const existingTag = openedTags.get(tag)
|
||||
|
||||
// We have unknown tag
|
||||
if (existingTag === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
existingTag.count -= 1
|
||||
|
||||
if (existingTag.count <= 0) {
|
||||
openedTags.delete(tag)
|
||||
}
|
||||
|
||||
newMessage += `</${tag}>`
|
||||
}
|
||||
})
|
||||
|
||||
parser.write(message)
|
||||
parser.end()
|
||||
|
||||
return newMessage.trim()
|
||||
}
|
192
services/telegram-bot/pod-telegram-bot/src/worker.ts
Normal file
192
services/telegram-bot/pod-telegram-bot/src/worker.ts
Normal file
@ -0,0 +1,192 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Collection } from 'mongodb'
|
||||
import { Account, Ref, SortingOrder } from '@hcengineering/core'
|
||||
|
||||
import { UserRecord, NotificationRecord, OtpRecord } from './types'
|
||||
import { getDB } from './storage'
|
||||
import { WorkspaceClient } from './workspace'
|
||||
import { getNewOtp } from './utils'
|
||||
import config from './config'
|
||||
|
||||
const closeWorkspaceTimeout = 10 * 60 * 1000 // 10 minutes
|
||||
|
||||
export class PlatformWorker {
|
||||
private readonly workspacesClients = new Map<string, WorkspaceClient>()
|
||||
private readonly closeWorkspaceTimeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>()
|
||||
private readonly intervalId: NodeJS.Timeout | undefined
|
||||
|
||||
private constructor (
|
||||
private readonly usersStorage: Collection<UserRecord>,
|
||||
private readonly notificationsStorage: Collection<NotificationRecord>,
|
||||
private readonly otpStorage: Collection<OtpRecord>
|
||||
) {
|
||||
this.intervalId = setInterval(
|
||||
() => {
|
||||
void otpStorage.deleteMany({ expires: { $lte: Date.now() } })
|
||||
},
|
||||
3 * 60 * 1000
|
||||
)
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
if (this.intervalId !== undefined) {
|
||||
clearInterval(this.intervalId)
|
||||
}
|
||||
}
|
||||
|
||||
async closeWorkspaceClient (workspace: string): Promise<void> {
|
||||
const timeoutId = this.closeWorkspaceTimeouts.get(workspace)
|
||||
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId)
|
||||
this.closeWorkspaceTimeouts.delete(workspace)
|
||||
}
|
||||
|
||||
const client = this.workspacesClients.get(workspace)
|
||||
|
||||
if (client !== undefined) {
|
||||
await client.close()
|
||||
this.workspacesClients.delete(workspace)
|
||||
}
|
||||
}
|
||||
|
||||
async getUsersRecords (): Promise<UserRecord[]> {
|
||||
return await this.usersStorage.find().toArray()
|
||||
}
|
||||
|
||||
async addUser (id: number, email: string): Promise<UserRecord | undefined> {
|
||||
const emailRes = await this.usersStorage.findOne({ email })
|
||||
|
||||
if (emailRes !== null) {
|
||||
console.log('Account is already registered', { id, email })
|
||||
return
|
||||
}
|
||||
|
||||
const tRes = await this.usersStorage.findOne({ telegramId: id })
|
||||
|
||||
if (tRes !== null) {
|
||||
console.log('Account is already registered', { id, email })
|
||||
return
|
||||
}
|
||||
|
||||
const insertResult = await this.usersStorage.insertOne({ telegramId: id, email })
|
||||
|
||||
return (await this.usersStorage.findOne({ _id: insertResult.insertedId })) ?? undefined
|
||||
}
|
||||
|
||||
async addNotificationRecord (record: NotificationRecord): Promise<void> {
|
||||
await this.notificationsStorage.insertOne(record)
|
||||
}
|
||||
|
||||
async removeUserByTelegramId (id: number): Promise<void> {
|
||||
await this.usersStorage.deleteOne({ telegramId: id })
|
||||
}
|
||||
|
||||
async removeUserByAccount (_id: Ref<Account>): Promise<void> {
|
||||
await this.usersStorage.deleteOne({ account: _id })
|
||||
}
|
||||
|
||||
async getNotificationRecord (id: number, email: string): Promise<NotificationRecord | undefined> {
|
||||
return (await this.notificationsStorage.findOne({ telegramId: id, email })) ?? undefined
|
||||
}
|
||||
|
||||
async getUserRecord (id: number): Promise<UserRecord | undefined> {
|
||||
return (await this.usersStorage.findOne({ telegramId: id })) ?? undefined
|
||||
}
|
||||
|
||||
async getUserRecordByEmail (email: string): Promise<UserRecord | undefined> {
|
||||
return (await this.usersStorage.findOne({ email })) ?? undefined
|
||||
}
|
||||
|
||||
async getWorkspaceClient (workspace: string): Promise<WorkspaceClient> {
|
||||
const wsClient = this.workspacesClients.get(workspace) ?? (await WorkspaceClient.create(workspace))
|
||||
|
||||
if (!this.workspacesClients.has(workspace)) {
|
||||
this.workspacesClients.set(workspace, wsClient)
|
||||
}
|
||||
|
||||
const timeoutId = this.closeWorkspaceTimeouts.get(workspace)
|
||||
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
|
||||
const newTimeoutId = setTimeout(() => {
|
||||
void this.closeWorkspaceClient(workspace)
|
||||
}, closeWorkspaceTimeout)
|
||||
|
||||
this.closeWorkspaceTimeouts.set(workspace, newTimeoutId)
|
||||
|
||||
return wsClient
|
||||
}
|
||||
|
||||
async reply (notification: NotificationRecord, text: string): Promise<boolean> {
|
||||
const client = await this.getWorkspaceClient(notification.workspace)
|
||||
return await client.reply(notification, text)
|
||||
}
|
||||
|
||||
async authorizeUser (code: string, email: string): Promise<UserRecord | undefined> {
|
||||
const otpData = (await this.otpStorage.findOne({ code })) ?? undefined
|
||||
const isExpired = otpData !== undefined && otpData.expires < Date.now()
|
||||
const isValid = otpData !== undefined && !isExpired && code === otpData.code
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error('Invalid OTP')
|
||||
}
|
||||
|
||||
return await this.addUser(otpData.telegramId, email)
|
||||
}
|
||||
|
||||
async generateCode (telegramId: number): Promise<string> {
|
||||
const now = Date.now()
|
||||
const otpData = (
|
||||
await this.otpStorage.find({ telegramId }).sort({ createdOn: SortingOrder.Descending }).limit(1).toArray()
|
||||
)[0]
|
||||
const retryDelay = config.OtpRetryDelaySec * 1000
|
||||
const isValid = otpData !== undefined && otpData.expires > now
|
||||
const canRetry = otpData !== undefined && otpData.createdOn + retryDelay < now
|
||||
|
||||
if (isValid && !canRetry) {
|
||||
return otpData.code
|
||||
}
|
||||
|
||||
const newCode = await getNewOtp(this.otpStorage)
|
||||
const timeToLive = config.OtpTimeToLiveSec * 1000
|
||||
const expires = now + timeToLive
|
||||
|
||||
await this.otpStorage.insertOne({ telegramId, code: newCode, expires, createdOn: now })
|
||||
|
||||
return newCode
|
||||
}
|
||||
|
||||
static async createStorages (): Promise<
|
||||
[Collection<UserRecord>, Collection<NotificationRecord>, Collection<OtpRecord>]
|
||||
> {
|
||||
const db = await getDB()
|
||||
const userStorage = db.collection<UserRecord>('users')
|
||||
const notificationsStorage = db.collection<NotificationRecord>('notifications')
|
||||
const otpStorage = db.collection<OtpRecord>('otp')
|
||||
|
||||
return [userStorage, notificationsStorage, otpStorage]
|
||||
}
|
||||
|
||||
static async create (): Promise<PlatformWorker> {
|
||||
const [userStorage, notificationsStorage, otpStorage] = await PlatformWorker.createStorages()
|
||||
|
||||
return new PlatformWorker(userStorage, notificationsStorage, otpStorage)
|
||||
}
|
||||
}
|
145
services/telegram-bot/pod-telegram-bot/src/workspace.ts
Normal file
145
services/telegram-bot/pod-telegram-bot/src/workspace.ts
Normal file
@ -0,0 +1,145 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Client, getWorkspaceId, systemAccountEmail, TxFactory, WorkspaceId } from '@hcengineering/core'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
import notification, { ActivityInboxNotification, MentionInboxNotification } from '@hcengineering/notification'
|
||||
import chunter, { ThreadMessage } from '@hcengineering/chunter'
|
||||
import contact, { PersonAccount } from '@hcengineering/contact'
|
||||
import { createClient, getTransactorEndpoint } from '@hcengineering/server-client'
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
|
||||
import { NotificationRecord } from './types'
|
||||
|
||||
export class WorkspaceClient {
|
||||
private constructor (
|
||||
private readonly client: Client,
|
||||
private readonly token: string,
|
||||
private readonly workspace: WorkspaceId
|
||||
) {}
|
||||
|
||||
static async create (workspace: string): Promise<WorkspaceClient> {
|
||||
const workspaceId = getWorkspaceId(workspace)
|
||||
const token = generateToken(systemAccountEmail, workspaceId)
|
||||
const client = await connectPlatform(token)
|
||||
|
||||
return new WorkspaceClient(client, token, workspaceId)
|
||||
}
|
||||
|
||||
async replyToMessage (message: ActivityMessage, account: PersonAccount, text: string): Promise<void> {
|
||||
const txFactory = new TxFactory(account._id)
|
||||
const hierarchy = this.client.getHierarchy()
|
||||
if (hierarchy.isDerived(message._class, chunter.class.ThreadMessage)) {
|
||||
const thread = message as ThreadMessage
|
||||
const collectionTx = txFactory.createTxCollectionCUD(
|
||||
thread.attachedToClass,
|
||||
thread.attachedTo,
|
||||
message.space,
|
||||
'replies',
|
||||
txFactory.createTxCreateDoc(chunter.class.ThreadMessage, message.space, {
|
||||
attachedTo: thread.attachedTo,
|
||||
attachedToClass: thread.attachedToClass,
|
||||
objectId: thread.objectId,
|
||||
objectClass: thread.objectClass,
|
||||
message: text,
|
||||
attachments: 0,
|
||||
collection: 'replies'
|
||||
})
|
||||
)
|
||||
await this.client.tx(collectionTx)
|
||||
} else {
|
||||
const collectionTx = txFactory.createTxCollectionCUD(
|
||||
message._class,
|
||||
message._id,
|
||||
message.space,
|
||||
'replies',
|
||||
txFactory.createTxCreateDoc(chunter.class.ThreadMessage, message.space, {
|
||||
attachedTo: message._id,
|
||||
attachedToClass: message._class,
|
||||
objectId: message.attachedTo,
|
||||
objectClass: message.attachedToClass,
|
||||
message: text,
|
||||
attachments: 0,
|
||||
collection: 'replies'
|
||||
})
|
||||
)
|
||||
await this.client.tx(collectionTx)
|
||||
}
|
||||
}
|
||||
|
||||
async replyToActivityNotification (
|
||||
it: ActivityInboxNotification,
|
||||
account: PersonAccount,
|
||||
text: string
|
||||
): Promise<boolean> {
|
||||
const message = await this.client.findOne(it.attachedToClass, { _id: it.attachedTo })
|
||||
|
||||
if (message !== undefined) {
|
||||
await this.replyToMessage(message, account, text)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async replyToMention (it: MentionInboxNotification, account: PersonAccount, text: string): Promise<boolean> {
|
||||
const hierarchy = this.client.getHierarchy()
|
||||
|
||||
if (!hierarchy.isDerived(it.mentionedInClass, activity.class.ActivityMessage)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const message = (await this.client.findOne(it.mentionedInClass, { _id: it.mentionedIn })) as ActivityMessage
|
||||
|
||||
if (message !== undefined) {
|
||||
await this.replyToMessage(message, account, text)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public async reply (record: NotificationRecord, text: string): Promise<boolean> {
|
||||
const account = await this.client.getModel().findOne(contact.class.PersonAccount, { email: record.email })
|
||||
if (account === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
const inboxNotification = await this.client.findOne(notification.class.InboxNotification, {
|
||||
_id: record.notificationId
|
||||
})
|
||||
|
||||
if (inboxNotification === undefined) {
|
||||
return false
|
||||
}
|
||||
const hierarchy = this.client.getHierarchy()
|
||||
if (hierarchy.isDerived(inboxNotification._class, notification.class.ActivityInboxNotification)) {
|
||||
return await this.replyToActivityNotification(inboxNotification as ActivityInboxNotification, account, text)
|
||||
} else if (hierarchy.isDerived(inboxNotification._class, notification.class.MentionInboxNotification)) {
|
||||
return await this.replyToMention(inboxNotification as MentionInboxNotification, account, text)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
await this.client.close()
|
||||
}
|
||||
}
|
||||
|
||||
async function connectPlatform (token: string): Promise<Client> {
|
||||
const endpoint = await getTransactorEndpoint(token)
|
||||
return await createClient(endpoint, token)
|
||||
}
|
10
services/telegram-bot/pod-telegram-bot/tsconfig.json
Normal file
10
services/telegram-bot/pod-telegram-bot/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user