Compare commits

...

9 commits
v1.3.0 ... main

13 changed files with 652 additions and 164 deletions

View file

@ -1,4 +1,3 @@
version: "3.9"
services: services:
bot: bot:
build: build:
@ -13,5 +12,6 @@ services:
- "--keywords-file" - "--keywords-file"
- "/app/keywords.yml" - "/app/keywords.yml"
- "--watch-file" - "--watch-file"
# - "--reactions-collector-load-history"
ports: ports:
- 127.0.0.1:9669:9669 - 0.0.0.0:9669:9669

View file

@ -5,6 +5,11 @@ keywords:
# will match plain regex # will match plain regex
- name: woof - name: woof
pattern: 'w[oa]+f' pattern: 'w[oa]+f'
# you can also specify flags for your regex
flags:
global: true
multi_line: false
insensitive: true
# will match regex wraped with word borders # will match regex wraped with word borders
# will match 'hi, woof :3', 'woof!', 'i heard a woof' but not 'i like subwoofers' # will match 'hi, woof :3', 'woof!', 'i heard a woof' but not 'i like subwoofers'

View file

@ -1,7 +1,7 @@
{ {
"name": "mtproto_exporter", "name": "mtproto_exporter",
"type": "module", "type": "module",
"version": "1.3.0", "version": "1.5.3",
"packageManager": "pnpm@10.6.5", "packageManager": "pnpm@10.6.5",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
@ -11,8 +11,8 @@
"build": "tsc" "build": "tsc"
}, },
"dependencies": { "dependencies": {
"@mtcute/dispatcher": "^0.22.2", "@mtcute/dispatcher": "^0.27.6",
"@mtcute/node": "^0.22.3", "@mtcute/node": "^0.27.6",
"command-line-args": "^6.0.1", "command-line-args": "^6.0.1",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",

318
pnpm-lock.yaml generated
View file

@ -9,11 +9,11 @@ importers:
.: .:
dependencies: dependencies:
'@mtcute/dispatcher': '@mtcute/dispatcher':
specifier: ^0.22.2 specifier: ^0.27.6
version: 0.22.2 version: 0.27.6
'@mtcute/node': '@mtcute/node':
specifier: ^0.22.3 specifier: ^0.27.6
version: 0.22.3 version: 0.27.6
command-line-args: command-line-args:
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.1 version: 6.0.1
@ -104,21 +104,25 @@ packages:
resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.25.9': '@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.25.9': '@babel/helper-validator-identifier@7.25.9':
resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/parser@7.27.0': '@babel/helper-validator-identifier@7.27.1':
resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.27.5':
resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
hasBin: true hasBin: true
'@babel/types@7.27.0': '@babel/types@7.27.6':
resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@clack/core@0.4.1': '@clack/core@0.4.1':
@ -306,6 +310,12 @@ packages:
peerDependencies: peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
'@eslint-community/eslint-utils@4.7.0':
resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
'@eslint-community/regexpp@4.12.1': '@eslint-community/regexpp@4.12.1':
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
@ -319,12 +329,12 @@ packages:
eslint: eslint:
optional: true optional: true
'@eslint/config-array@0.20.0': '@eslint/config-array@0.20.1':
resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} resolution: {integrity: sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/config-helpers@0.2.1': '@eslint/config-helpers@0.2.3':
resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==} resolution: {integrity: sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/core@0.10.0': '@eslint/core@0.10.0':
@ -359,17 +369,25 @@ packages:
resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==} resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@fuman/io@0.0.8': '@fuman/io@0.0.17':
resolution: {integrity: sha512-+cRZ2JOMYceNQ2Rh6UYro5XVa11j29Sdd3Yhi4GfxAx6ZwCNIw3P80xcTRwCZSfMPLDNN9Etkq7NIc5v9lpItw==} resolution: {integrity: sha512-VmMnfHtXzBfEddEfptn/oYshUzWqW2XUkdVnwKuHWphEQTQZrOWxC7G12FI9U2EhEYt4nRdrUTYk65U8GVJWYw==}
'@fuman/net@0.0.9': '@fuman/net@0.0.17':
resolution: {integrity: sha512-asn7VJbT8woVXAFCUMZrdyNZCSsXZclraeVZ6RYJ+T3RwQ+JfMMZtXLLTZ7XHrBPxk8x8hoHOJa/Fnyfm+ggbQ==} resolution: {integrity: sha512-x/kK3kWQ+gy5rfsoS6QVCsodh9n/XJeM3c6m1YHPUiQ0gWWQd4CC1bcQ/rh2UHh9DQyJJeWjCQXWH2xmsVCcFQ==}
'@fuman/node@0.0.9': '@fuman/node@0.0.17':
resolution: {integrity: sha512-ImOGEv1T1n/AOqfPH8ag1q/i0RvxnG0EfYVwlfRl/PXW/uNJiH3PRgT4euvXPNlHx6DViDiFn4ADecz9bTrtUg==} resolution: {integrity: sha512-XXRlJthuCnJBnIrg/tZcqCfv/cPuXuNOVUN521oJgKrW8FyFmt+lAt2MlYw3TROumGNRMtvn3ySjdQRpBT2sLw==}
peerDependencies:
ws: ^8.18.1
peerDependenciesMeta:
ws:
optional: true
'@fuman/utils@0.0.4': '@fuman/utils@0.0.15':
resolution: {integrity: sha512-YBZIlGDbM8s9G85pWFZJ9wQrDY4511XLHZ06/uxZfXBY0eSStwje8JFNmRMNF0SjRk4D3iRmPl9wirHKTkg89w==} resolution: {integrity: sha512-3H3WzkfG7iLKCa/yNV4s80lYD4yr5hgiNzU13ysLY2BcDqFjM08XGYuLd5wFVp4V8+DA/fe8gIDW96To/JwDyA==}
'@fuman/utils@0.0.17':
resolution: {integrity: sha512-hy1Xu1146nOspVam8FC6p4yakb1FV1V3KrS85RzcHiK7AccFKR43Fgtv8exC8Ybsw6MtMU+MRNyaPqVhA+7TsA==}
'@humanfs/core@0.19.1': '@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
@ -387,39 +405,39 @@ packages:
resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
engines: {node: '>=18.18'} engines: {node: '>=18.18'}
'@humanwhocodes/retry@0.4.2': '@humanwhocodes/retry@0.4.3':
resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'} engines: {node: '>=18.18'}
'@jridgewell/sourcemap-codec@1.5.0': '@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@mtcute/core@0.22.3': '@mtcute/core@0.27.6':
resolution: {integrity: sha512-oyCSliNUBsLlHn4aNPfB/lWEleZFGk1dhcmC0LoVlqHvZN22/RiwRDxJPdlGqjfq6/dSmNbz+n7EZjna3CB1dA==} resolution: {integrity: sha512-OqjQ2hchF15yJAjcAgBuWx4RCnvMED0P3kiUfU3EsDMMIJlh8TgOgm0QspIda/Uz7icZ5+pi0rHyCYblw2MMKw==}
'@mtcute/dispatcher@0.22.2': '@mtcute/dispatcher@0.27.6':
resolution: {integrity: sha512-pkvm1eLBD5SILcZjFHhHTaWw6eX8arygsyJNFh1R6IQLAOZmgRucDaj0rQD+vNJzEJJNFsvZABc0yq+IVvi3jA==} resolution: {integrity: sha512-5ZmI5cmyeWVYY5BtPlGYB0b4oxmF86xiP/c1Wo3VQ0SElYuknf+xZDlFxt6AMlHk9d/v2rxEd+dBXB5kRczpUA==}
'@mtcute/file-id@0.22.0': '@mtcute/file-id@0.27.6':
resolution: {integrity: sha512-siY+eGrfYvTuC2xuyf4oaD09ZpaLtG2qrOlwdEpwXFnyjjavsQyWkyPwAemTPclcRuYB+/axD9+RBhj3EGBNSQ==} resolution: {integrity: sha512-ZSPxbGjS6YdcZv4xW0zHJ/iR28nEBisG3G6gDTwVS4gU51SJ4vlGcwGjF1uLyEeuGGbPFemQHLCP6CMSzqMRvA==}
'@mtcute/html-parser@0.22.1': '@mtcute/html-parser@0.27.6':
resolution: {integrity: sha512-KojNrJPvbsRyzc0pBbRrU03VZfgRRj+gkWBP+lcXJgdM+SypGkxUM2wQMxCwsRQ8JVGAmGLna1IPrYoiBW0rJw==} resolution: {integrity: sha512-zxTuT0nv0CBR4qy7KyKB9vGQ++DxeiofKJEwHFSj5oG/7qUARm21G5GZaVlel/v7oRzx6V3u9mKDzdlbv8BcxA==}
'@mtcute/markdown-parser@0.22.3': '@mtcute/markdown-parser@0.27.6':
resolution: {integrity: sha512-kg7oOEX69Y5LJd2E52uLZWilSetbEHWSO8R1XVrI0ONaL3T2NnGDaGZGI80KuI4CCpNyWdDqB6EggoBeQJshnA==} resolution: {integrity: sha512-YB4HXeDGQi+ilbOp1qDJ/iP3VfBFrsR+gEyQcaQo/PAR4NLtD+rZ5veWM/OSVjbawYl2OpFpfXzQdINAAlaEJg==}
'@mtcute/node@0.22.3': '@mtcute/node@0.27.6':
resolution: {integrity: sha512-KTJ7O/mzXpBMcF0K/NPksfpQovIhOXayTdiP/cHrSp7VMdBBeo+5G1THk1SwPWi6iFmz3AS5d+WO+Ybff08qFg==} resolution: {integrity: sha512-fDnufwcRJyqMr7rpCIiSW6GIRR1j+tgM8Og1Rx38U16Ftmu3gB7Xt5K7lHJJWQHDhv59w8AiU+NksiPdTXbxxQ==}
'@mtcute/tl-runtime@0.22.0': '@mtcute/tl-runtime@0.24.3':
resolution: {integrity: sha512-t4ThFk7gCrgMa5sx+C+2qDwKmNuWLKMK/75oGYhsa7IJLgdGc+s4F+SM5TA0DynmG/sUh2BfYNZr6/rotJM01A==} resolution: {integrity: sha512-61J3cgYgNOQT532GdIiuezRrSC7v6cc9MfvWv9GO27bRGf7JUKWVbFt4U0KzQ9Tp0J1uMOUfi1EbQKkYKacIKQ==}
'@mtcute/tl@200.0.0': '@mtcute/tl@221.0.0':
resolution: {integrity: sha512-YMiWjLMtqRSoUxcJbrCt2a8VWbIYDkvmipOmvNR2oAFzFmYmYXjR+jYDnbt1zmBoRWc3IrwddqdkVGK99Sc4bQ==} resolution: {integrity: sha512-Wp01L9nznTMLl2s9rbKnzQ8pij72eF4HK2XIziOQoJXiObPKZxQdxvMj+C6l0ArxMFmpT0H0/3EL5RB7O6VPwg==}
'@mtcute/wasm@0.22.3': '@mtcute/wasm@0.27.0':
resolution: {integrity: sha512-YyzxgWcx30HYuBsxvu43IDekIXV2u5xhdEPXqf7SCYDvWDZtQJ2ox3s4BPspRJ0LiKuYtaY4pdny+7I741Hcog==} resolution: {integrity: sha512-1v4eO1N1BVRQ8L+cyUsMAeLXs5suTGXyVv/tftkbd/mGGHxc+fvOWItp3Fmq+GIwN7m4VX7kztuMMLhHxv2i2Q==}
'@napi-rs/wasm-runtime@0.2.8': '@napi-rs/wasm-runtime@0.2.8':
resolution: {integrity: sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==} resolution: {integrity: sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==}
@ -472,6 +490,9 @@ packages:
'@types/estree@1.0.7': '@types/estree@1.0.7':
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/events@3.0.0': '@types/events@3.0.0':
resolution: {integrity: sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==} resolution: {integrity: sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==}
@ -656,6 +677,11 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
acorn@8.15.0:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'}
hasBin: true
ajv@6.12.6: ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@ -684,8 +710,9 @@ packages:
base64-js@1.5.1: base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
better-sqlite3@11.3.0: better-sqlite3@12.6.2:
resolution: {integrity: sha512-iHt9j8NPYF3oKCNOO5ZI4JwThjt3Z6J6XrcwG85VNMVzv1ByqrHWv5VILEbCMFWDsoHhXvQ7oC8vgRXFAKgl9w==} resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==}
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
bindings@1.5.0: bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
@ -699,8 +726,8 @@ packages:
boolbase@1.0.0: boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
brace-expansion@1.1.11: brace-expansion@1.1.12:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
brace-expansion@2.0.1: brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
@ -811,6 +838,15 @@ packages:
supports-color: supports-color:
optional: true optional: true
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
decode-named-character-reference@1.1.0: decode-named-character-reference@1.1.0:
resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==}
@ -1051,6 +1087,10 @@ packages:
resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-scope@8.4.0:
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-visitor-keys@3.4.3: eslint-visitor-keys@3.4.3:
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@ -1059,6 +1099,10 @@ packages:
resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-visitor-keys@4.2.1:
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint@9.24.0: eslint@9.24.0:
resolution: {integrity: sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==} resolution: {integrity: sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -1073,6 +1117,10 @@ packages:
resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
espree@10.4.0:
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
espree@9.6.1: espree@9.6.1:
resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@ -1606,8 +1654,8 @@ packages:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'} engines: {node: '>=4'}
postcss@8.5.3: postcss@8.5.6:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
prebuild-install@7.1.3: prebuild-install@7.1.3:
@ -1973,18 +2021,20 @@ snapshots:
js-tokens: 4.0.0 js-tokens: 4.0.0
picocolors: 1.1.1 picocolors: 1.1.1
'@babel/helper-string-parser@7.25.9': {} '@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.25.9': {} '@babel/helper-validator-identifier@7.25.9': {}
'@babel/parser@7.27.0': '@babel/helper-validator-identifier@7.27.1': {}
dependencies:
'@babel/types': 7.27.0
'@babel/types@7.27.0': '@babel/parser@7.27.5':
dependencies: dependencies:
'@babel/helper-string-parser': 7.25.9 '@babel/types': 7.27.6
'@babel/helper-validator-identifier': 7.25.9
'@babel/types@7.27.6':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@clack/core@0.4.1': '@clack/core@0.4.1':
dependencies: dependencies:
@ -2114,21 +2164,26 @@ snapshots:
eslint: 9.24.0 eslint: 9.24.0
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
'@eslint-community/eslint-utils@4.7.0(eslint@9.24.0)':
dependencies:
eslint: 9.24.0
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1': {} '@eslint-community/regexpp@4.12.1': {}
'@eslint/compat@1.2.8(eslint@9.24.0)': '@eslint/compat@1.2.8(eslint@9.24.0)':
optionalDependencies: optionalDependencies:
eslint: 9.24.0 eslint: 9.24.0
'@eslint/config-array@0.20.0': '@eslint/config-array@0.20.1':
dependencies: dependencies:
'@eslint/object-schema': 2.1.6 '@eslint/object-schema': 2.1.6
debug: 4.4.0 debug: 4.4.1
minimatch: 3.1.2 minimatch: 3.1.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@eslint/config-helpers@0.2.1': {} '@eslint/config-helpers@0.2.3': {}
'@eslint/core@0.10.0': '@eslint/core@0.10.0':
dependencies: dependencies:
@ -2145,8 +2200,8 @@ snapshots:
'@eslint/eslintrc@3.3.1': '@eslint/eslintrc@3.3.1':
dependencies: dependencies:
ajv: 6.12.6 ajv: 6.12.6
debug: 4.4.0 debug: 4.4.1
espree: 10.3.0 espree: 10.4.0
globals: 14.0.0 globals: 14.0.0
ignore: 5.3.2 ignore: 5.3.2
import-fresh: 3.3.1 import-fresh: 3.3.1
@ -2175,22 +2230,24 @@ snapshots:
'@eslint/core': 0.13.0 '@eslint/core': 0.13.0
levn: 0.4.1 levn: 0.4.1
'@fuman/io@0.0.8': '@fuman/io@0.0.17':
dependencies: dependencies:
'@fuman/utils': 0.0.4 '@fuman/utils': 0.0.17
'@fuman/net@0.0.9': '@fuman/net@0.0.17':
dependencies: dependencies:
'@fuman/io': 0.0.8 '@fuman/io': 0.0.17
'@fuman/utils': 0.0.4 '@fuman/utils': 0.0.17
'@fuman/node@0.0.9': '@fuman/node@0.0.17':
dependencies: dependencies:
'@fuman/io': 0.0.8 '@fuman/io': 0.0.17
'@fuman/net': 0.0.9 '@fuman/net': 0.0.17
'@fuman/utils': 0.0.4 '@fuman/utils': 0.0.17
'@fuman/utils@0.0.4': {} '@fuman/utils@0.0.15': {}
'@fuman/utils@0.0.17': {}
'@humanfs/core@0.19.1': {} '@humanfs/core@0.19.1': {}
@ -2203,64 +2260,66 @@ snapshots:
'@humanwhocodes/retry@0.3.1': {} '@humanwhocodes/retry@0.3.1': {}
'@humanwhocodes/retry@0.4.2': {} '@humanwhocodes/retry@0.4.3': {}
'@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/sourcemap-codec@1.5.0': {}
'@mtcute/core@0.22.3': '@mtcute/core@0.27.6':
dependencies: dependencies:
'@fuman/io': 0.0.8 '@fuman/io': 0.0.17
'@fuman/net': 0.0.9 '@fuman/net': 0.0.17
'@fuman/utils': 0.0.4 '@fuman/utils': 0.0.17
'@mtcute/file-id': 0.22.0 '@mtcute/file-id': 0.27.6
'@mtcute/tl': 200.0.0 '@mtcute/tl': 221.0.0
'@mtcute/tl-runtime': 0.22.0 '@mtcute/tl-runtime': 0.24.3
'@types/events': 3.0.0 '@types/events': 3.0.0
long: 5.2.3 long: 5.2.3
'@mtcute/dispatcher@0.22.2': '@mtcute/dispatcher@0.27.6':
dependencies: dependencies:
'@fuman/utils': 0.0.4 '@fuman/utils': 0.0.17
'@mtcute/core': 0.22.3 '@mtcute/core': 0.27.6
'@mtcute/file-id@0.22.0': '@mtcute/file-id@0.27.6':
dependencies: dependencies:
'@fuman/utils': 0.0.4 '@fuman/utils': 0.0.17
'@mtcute/tl-runtime': 0.22.0 '@mtcute/tl-runtime': 0.24.3
long: 5.2.3 long: 5.2.3
'@mtcute/html-parser@0.22.1': '@mtcute/html-parser@0.27.6':
dependencies: dependencies:
'@mtcute/core': 0.22.3 '@mtcute/core': 0.27.6
htmlparser2: 10.0.0 htmlparser2: 10.0.0
long: 5.2.3 long: 5.2.3
'@mtcute/markdown-parser@0.22.3': '@mtcute/markdown-parser@0.27.6':
dependencies: dependencies:
'@mtcute/core': 0.22.3 '@mtcute/core': 0.27.6
long: 5.2.3 long: 5.2.3
'@mtcute/node@0.22.3': '@mtcute/node@0.27.6':
dependencies: dependencies:
'@fuman/net': 0.0.9 '@fuman/net': 0.0.17
'@fuman/node': 0.0.9 '@fuman/node': 0.0.17
'@fuman/utils': 0.0.4 '@fuman/utils': 0.0.17
'@mtcute/core': 0.22.3 '@mtcute/core': 0.27.6
'@mtcute/html-parser': 0.22.1 '@mtcute/html-parser': 0.27.6
'@mtcute/markdown-parser': 0.22.3 '@mtcute/markdown-parser': 0.27.6
'@mtcute/wasm': 0.22.3 '@mtcute/wasm': 0.27.0
better-sqlite3: 11.3.0 better-sqlite3: 12.6.2
transitivePeerDependencies:
- ws
'@mtcute/tl-runtime@0.22.0': '@mtcute/tl-runtime@0.24.3':
dependencies: dependencies:
'@fuman/utils': 0.0.4 '@fuman/utils': 0.0.15
long: 5.2.3 long: 5.2.3
'@mtcute/tl@200.0.0': '@mtcute/tl@221.0.0':
dependencies: dependencies:
long: 5.2.3 long: 5.2.3
'@mtcute/wasm@0.22.3': {} '@mtcute/wasm@0.27.0': {}
'@napi-rs/wasm-runtime@0.2.8': '@napi-rs/wasm-runtime@0.2.8':
dependencies: dependencies:
@ -2319,6 +2378,8 @@ snapshots:
'@types/estree@1.0.7': {} '@types/estree@1.0.7': {}
'@types/estree@1.0.8': {}
'@types/events@3.0.0': {} '@types/events@3.0.0': {}
'@types/js-yaml@4.0.9': {} '@types/js-yaml@4.0.9': {}
@ -2472,7 +2533,7 @@ snapshots:
'@vue/compiler-core@3.5.13': '@vue/compiler-core@3.5.13':
dependencies: dependencies:
'@babel/parser': 7.27.0 '@babel/parser': 7.27.5
'@vue/shared': 3.5.13 '@vue/shared': 3.5.13
entities: 4.5.0 entities: 4.5.0
estree-walker: 2.0.2 estree-walker: 2.0.2
@ -2485,14 +2546,14 @@ snapshots:
'@vue/compiler-sfc@3.5.13': '@vue/compiler-sfc@3.5.13':
dependencies: dependencies:
'@babel/parser': 7.27.0 '@babel/parser': 7.27.5
'@vue/compiler-core': 3.5.13 '@vue/compiler-core': 3.5.13
'@vue/compiler-dom': 3.5.13 '@vue/compiler-dom': 3.5.13
'@vue/compiler-ssr': 3.5.13 '@vue/compiler-ssr': 3.5.13
'@vue/shared': 3.5.13 '@vue/shared': 3.5.13
estree-walker: 2.0.2 estree-walker: 2.0.2
magic-string: 0.30.17 magic-string: 0.30.17
postcss: 8.5.3 postcss: 8.5.6
source-map-js: 1.2.1 source-map-js: 1.2.1
'@vue/compiler-ssr@3.5.13': '@vue/compiler-ssr@3.5.13':
@ -2506,8 +2567,14 @@ snapshots:
dependencies: dependencies:
acorn: 8.14.1 acorn: 8.14.1
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.15.0
acorn@8.14.1: {} acorn@8.14.1: {}
acorn@8.15.0: {}
ajv@6.12.6: ajv@6.12.6:
dependencies: dependencies:
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
@ -2531,7 +2598,7 @@ snapshots:
base64-js@1.5.1: {} base64-js@1.5.1: {}
better-sqlite3@11.3.0: better-sqlite3@12.6.2:
dependencies: dependencies:
bindings: 1.5.0 bindings: 1.5.0
prebuild-install: 7.1.3 prebuild-install: 7.1.3
@ -2550,7 +2617,7 @@ snapshots:
boolbase@1.0.0: {} boolbase@1.0.0: {}
brace-expansion@1.1.11: brace-expansion@1.1.12:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
concat-map: 0.0.1 concat-map: 0.0.1
@ -2641,6 +2708,10 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
debug@4.4.1:
dependencies:
ms: 2.1.3
decode-named-character-reference@1.1.0: decode-named-character-reference@1.1.0:
dependencies: dependencies:
character-entities: 2.0.2 character-entities: 2.0.2
@ -2964,33 +3035,40 @@ snapshots:
esrecurse: 4.3.0 esrecurse: 4.3.0
estraverse: 5.3.0 estraverse: 5.3.0
eslint-scope@8.4.0:
dependencies:
esrecurse: 4.3.0
estraverse: 5.3.0
eslint-visitor-keys@3.4.3: {} eslint-visitor-keys@3.4.3: {}
eslint-visitor-keys@4.2.0: {} eslint-visitor-keys@4.2.0: {}
eslint-visitor-keys@4.2.1: {}
eslint@9.24.0: eslint@9.24.0:
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.5.1(eslint@9.24.0) '@eslint-community/eslint-utils': 4.7.0(eslint@9.24.0)
'@eslint-community/regexpp': 4.12.1 '@eslint-community/regexpp': 4.12.1
'@eslint/config-array': 0.20.0 '@eslint/config-array': 0.20.1
'@eslint/config-helpers': 0.2.1 '@eslint/config-helpers': 0.2.3
'@eslint/core': 0.12.0 '@eslint/core': 0.12.0
'@eslint/eslintrc': 3.3.1 '@eslint/eslintrc': 3.3.1
'@eslint/js': 9.24.0 '@eslint/js': 9.24.0
'@eslint/plugin-kit': 0.2.8 '@eslint/plugin-kit': 0.2.8
'@humanfs/node': 0.16.6 '@humanfs/node': 0.16.6
'@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.2 '@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.7 '@types/estree': 1.0.8
'@types/json-schema': 7.0.15 '@types/json-schema': 7.0.15
ajv: 6.12.6 ajv: 6.12.6
chalk: 4.1.2 chalk: 4.1.2
cross-spawn: 7.0.6 cross-spawn: 7.0.6
debug: 4.4.0 debug: 4.4.1
escape-string-regexp: 4.0.0 escape-string-regexp: 4.0.0
eslint-scope: 8.3.0 eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.0 eslint-visitor-keys: 4.2.1
espree: 10.3.0 espree: 10.4.0
esquery: 1.6.0 esquery: 1.6.0
esutils: 2.0.3 esutils: 2.0.3
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
@ -3014,6 +3092,12 @@ snapshots:
acorn-jsx: 5.3.2(acorn@8.14.1) acorn-jsx: 5.3.2(acorn@8.14.1)
eslint-visitor-keys: 4.2.0 eslint-visitor-keys: 4.2.0
espree@10.4.0:
dependencies:
acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.15.0)
eslint-visitor-keys: 4.2.1
espree@9.6.1: espree@9.6.1:
dependencies: dependencies:
acorn: 8.14.1 acorn: 8.14.1
@ -3543,7 +3627,7 @@ snapshots:
minimatch@3.1.2: minimatch@3.1.2:
dependencies: dependencies:
brace-expansion: 1.1.11 brace-expansion: 1.1.12
minimatch@9.0.5: minimatch@9.0.5:
dependencies: dependencies:
@ -3665,7 +3749,7 @@ snapshots:
cssesc: 3.0.0 cssesc: 3.0.0
util-deprecate: 1.0.2 util-deprecate: 1.0.2
postcss@8.5.3: postcss@8.5.6:
dependencies: dependencies:
nanoid: 3.3.11 nanoid: 3.3.11
picocolors: 1.1.1 picocolors: 1.1.1

View file

@ -10,21 +10,45 @@ export interface Configuration {
wordsCounter: boolean; wordsCounter: boolean;
keywordsFile: string; keywordsFile: string;
watchFile: boolean; watchFile: boolean;
watchFileIntervalSeconds: number;
includePeers?: number[]; includePeers?: number[];
excludePeers?: number[]; excludePeers?: number[];
keywords?: RawKeywordLike[]; keywords?: RawKeywordLike[];
collectors: {
dialogs: boolean;
messages: boolean;
reactions: boolean;
};
reactionsCollector: {
loadHistory: boolean;
loadHistorySize: number;
};
messagesCollector: {
includeSender: boolean;
};
} }
/* eslint-disable style/no-multi-spaces, style/key-spacing */
const optionDefinitions: OptionDefinition[] = [ const optionDefinitions: OptionDefinition[] = [
{ name: "bind-host", alias: "b", type: String, defaultValue: "0.0.0.0" }, { name: "bind-host", alias: "b", type: String, defaultValue: "0.0.0.0" },
{ name: "port", alias: "p", type: Number, defaultValue: 9669 }, { name: "port", alias: "p", type: Number, defaultValue: 9669 },
{ name: "words-counter", type: Boolean, defaultValue: false }, { name: "words-counter", type: Boolean, defaultValue: false },
{ name: "keywords-file", alias: "k", type: String }, { name: "keywords-file", alias: "k", type: String },
{ name: "watch-file", alias: "w", type: Boolean, defaultValue: false }, { name: "watch-file", alias: "w", type: Boolean, defaultValue: false },
{ name: "watch-file-interval-seconds", alias: "W", type: Number, defaultValue: 60 },
{ name: "include-peers", alias: "i", type: String, multiple: true }, { name: "include-peers", alias: "i", type: String, multiple: true },
{ name: "exclude-peers", alias: "x", type: String, multiple: true }, { name: "exclude-peers", alias: "x", type: String, multiple: true },
{ name: "reactions-collector", type: Boolean, defaultValue: false },
{ name: "reactions-collector-load-history", type: Boolean, defaultValue: false },
{ name: "reactions-collector-load-history-size", type: Number, defaultValue: 1000 },
{ name: "dialogs-collector", type: Boolean, defaultValue: true },
{ name: "messages-collector", type: Boolean, defaultValue: true },
{ name: "messages-collector-include-sender", type: Boolean, defaultValue: false },
]; ];
/* eslint-enable style/no-multi-spaces, style/key-spacing */
const cli = cmdline(optionDefinitions); const cli = cmdline(optionDefinitions);
const config: Configuration = { const config: Configuration = {
@ -33,7 +57,20 @@ const config: Configuration = {
wordsCounter: cli["words-counter"], wordsCounter: cli["words-counter"],
keywordsFile: cli["keywords-file"], keywordsFile: cli["keywords-file"],
watchFile: cli["watch-file"], watchFile: cli["watch-file"],
watchFileIntervalSeconds: cli["watch-file-interval-seconds"],
keywords: cli["keywords-file"] ? await readKeywords(cli["keywords-file"]) : undefined, keywords: cli["keywords-file"] ? await readKeywords(cli["keywords-file"]) : undefined,
collectors: {
dialogs: cli["dialogs-collector"],
messages: cli["messages-collector"],
reactions: cli["reactions-collector"],
},
reactionsCollector: {
loadHistory: cli["reactions-collector-load-history"],
loadHistorySize: cli["reactions-collector-load-history-size"],
},
messagesCollector: {
includeSender: cli["messages-collector-include-sender"],
},
}; };
if (cli["include-peers"] && cli["exclude-peers"]) { if (cli["include-peers"] && cli["exclude-peers"]) {
@ -64,11 +101,32 @@ export async function readKeywords(filePath: string): Promise<RawKeywordLike[]>
keywords.push(item); keywords.push(item);
} else if (typeof item === "object" && typeof item.name === "string") { } else if (typeof item === "object" && typeof item.name === "string") {
if (typeof item.pattern === "string") { if (typeof item.pattern === "string") {
keywords.push({ const result = {
name: item.name, name: item.name,
pattern: item.pattern, pattern: item.pattern,
word: Boolean(item.word ?? false), word: Boolean(item.word ?? false),
}); flags: {
global: true,
multi_line: false,
insensitive: true,
},
};
if (typeof item.flags === "object") {
if (typeof item.flags.global === "boolean") {
result.flags.global = item.flags.global;
}
if (typeof item.flags.multi_line === "boolean") {
result.flags.multi_line = item.flags.multi_line;
}
if (typeof item.flags.insensitive === "boolean") {
result.flags.insensitive = item.flags.insensitive;
}
}
keywords.push(result);
} }
} }
} }

View file

@ -7,8 +7,10 @@ const USERBOT_PHONE = process.env.USERBOT_PHONE;
const USERBOT_2FACODE = process.env.USERBOT_2FACODE; const USERBOT_2FACODE = process.env.USERBOT_2FACODE;
const USERBOT_PASSWORD = process.env.USERBOT_PASSWORD; const USERBOT_PASSWORD = process.env.USERBOT_PASSWORD;
const SOCKS_PROXY = process.env.SOCKS_PROXY;
if (Number.isNaN(API_ID) || !API_HASH) { if (Number.isNaN(API_ID) || !API_HASH) {
throw new Error("API_ID or API_HASH not set!"); throw new Error("API_ID or API_HASH not set!");
} }
export { API_HASH, API_ID, USERBOT_2FACODE, USERBOT_PASSWORD, USERBOT_PHONE }; export { API_HASH, API_ID, USERBOT_2FACODE, USERBOT_PASSWORD, USERBOT_PHONE, SOCKS_PROXY };

View file

@ -1,8 +1,8 @@
import fs from "node:fs"; import fs from "node:fs";
import { Dispatcher } from "@mtcute/dispatcher"; import { Dispatcher } from "@mtcute/dispatcher";
import { TelegramClient } from "@mtcute/node"; import { SocksProxyTcpTransport, TcpTransport, TelegramClient } from "@mtcute/node";
import { collectDefaultMetrics, Registry } from "prom-client";
import { collectDefaultMetrics, Registry } from "prom-client";
import { config, readKeywords } from "./config.js"; import { config, readKeywords } from "./config.js";
import * as env from "./env.js"; import * as env from "./env.js";
import { rawToPatterns } from "./metrics/keywords.js"; import { rawToPatterns } from "./metrics/keywords.js";
@ -16,10 +16,32 @@ collectDefaultMetrics({ register: registry });
const server = new MetricsServer(registry); const server = new MetricsServer(registry);
server.listen(config.bindHost, config.port); server.listen(config.bindHost, config.port);
let transport;
if (env.SOCKS_PROXY) {
const parts = env.SOCKS_PROXY!.split(":");
if (parts.length !== 2) {
throw new Error("Malformed SOCKS_PROXY: " + env.SOCKS_PROXY);
}
const port = parseInt(parts[1]);
if (isNaN(port) || port < 1 || port > 65535) {
throw new Error("Invalid port in SOCKS_PROXY: " + parts[1]);
}
transport = new SocksProxyTcpTransport({
host: parts[0],
port,
});
} else {
transport = new TcpTransport();
}
const tg = new TelegramClient({ const tg = new TelegramClient({
apiId: env.API_ID, apiId: env.API_ID,
apiHash: env.API_HASH, apiHash: env.API_HASH,
storage: "bot-data/session", storage: "bot-data/session",
transport,
}); });
const dp = Dispatcher.for(tg); const dp = Dispatcher.for(tg);
@ -32,26 +54,49 @@ const user = await tg.start({
console.log("Logged in as", user.username); console.log("Logged in as", user.username);
metrics.collectDialogMetrics(tg, registry); if (config.collectors.dialogs) {
metrics.collectNewMessageMetrics(dp, registry); metrics.collectDialogMetrics(tg, registry);
}
if (config.collectors.messages) {
metrics.collectNewMessageMetrics(dp, registry);
}
if (config.collectors.reactions) {
console.log("[WARN] reactions-collector is enabled, but it is very experimental and almost does not work. i don't recommend enabling it especially for production use.");
metrics.collectReactionsMetrics(tg, dp, registry);
}
if (config.keywords) { if (config.keywords) {
const counter = new metrics.KeywordsCounter(dp, rawToPatterns(config.keywords)); const counter = new metrics.KeywordsCounter(dp, rawToPatterns(config.keywords));
console.log("[keywords] Initialized keywords counter with", counter.keywords.length, "keywords/patterns.");
registry.registerMetric(counter); registry.registerMetric(counter);
if (config.watchFile) { if (config.watchFile) {
fs.watchFile(config.keywordsFile, async (curr, prev) => { const reloadConfig = async () => {
if (curr.mtimeMs === prev.mtimeMs) {
return;
}
console.log("[watch-file] Keywords file was updated. Re-reading keywords configuration...");
try { try {
config.keywords = await readKeywords(config.keywordsFile); config.keywords = await readKeywords(config.keywordsFile);
counter.setKeywords(rawToPatterns(config.keywords)); counter.setKeywords(rawToPatterns(config.keywords));
console.log(`Loaded ${counter.keywords.length} keywords/patterns.`);
} catch (e) { } catch (e) {
console.error("Failed to read keywords file", config.keywordsFile, e); console.error("Failed to read keywords file", config.keywordsFile, e);
} }
}); };
let lastMtimeMs = (await fs.promises.stat(config.keywordsFile)).mtimeMs;
setInterval(async () => {
const stat = await fs.promises.stat(config.keywordsFile);
if (lastMtimeMs === stat.mtimeMs) {
return;
}
lastMtimeMs = stat.mtimeMs;
console.log("[watch-file] Keywords file was updated. Reloading keywords configuration...");
await reloadConfig();
}, config.watchFileIntervalSeconds * 1000);
} }
} }

View file

@ -3,8 +3,8 @@ import type { Dialog, TelegramClient } from "@mtcute/node";
import type { Registry } from "prom-client"; import type { Registry } from "prom-client";
import process from "node:process"; import process from "node:process";
import timers from "node:timers/promises"; import timers from "node:timers/promises";
import { Gauge, Histogram, Summary } from "prom-client";
import { Gauge, Histogram } from "prom-client";
import { config } from "../config.js"; import { config } from "../config.js";
import { peersConfigBoolFilter } from "../filters.js"; import { peersConfigBoolFilter } from "../filters.js";
@ -41,9 +41,26 @@ export function collectDialogMetrics(tg: TelegramClient, registry: Registry) {
}, },
}); });
const members = new Gauge({
name: "messenger_dialog_chat_members_count",
help: "Number of members in the chat",
labelNames: ["peerId"],
collect: async () => {
members.reset();
for (const d of await dialogs.get()) {
if (d.peer.type !== "chat") continue;
if (d.peer.membersCount === null) continue;
members.set({
peerId: d.peer.id,
}, d.peer.membersCount);
}
},
});
dialogs.registerMetrics(registry); dialogs.registerMetrics(registry);
registry.registerMetric(info); registry.registerMetric(info);
registry.registerMetric(unread); registry.registerMetric(unread);
registry.registerMetric(members);
} }
class DialogsHolder { class DialogsHolder {
@ -53,6 +70,7 @@ class DialogsHolder {
private ttl: bigint; private ttl: bigint;
private dialogsIterDurationHistogram: Histogram; private dialogsIterDurationHistogram: Histogram;
private dialogsIterDurationSummary: Summary;
constructor(private tg: TelegramClient, ttl: number, private timeout: number, private pollInterval = 10) { constructor(private tg: TelegramClient, ttl: number, private timeout: number, private pollInterval = 10) {
this.ttl = BigInt(ttl) * 1000000n; this.ttl = BigInt(ttl) * 1000000n;
@ -60,10 +78,15 @@ class DialogsHolder {
name: "telegram_api_dialogs_iter_duration", name: "telegram_api_dialogs_iter_duration",
help: "Duration of iteration over telegram dialogs", help: "Duration of iteration over telegram dialogs",
}); });
this.dialogsIterDurationSummary = new Summary({
name: "telegram_api_dialogs_iter_duration_summary",
help: "Duration of iteration over telegram dialogs",
});
} }
public registerMetrics(registry: Registry) { public registerMetrics(registry: Registry) {
registry.registerMetric(this.dialogsIterDurationHistogram); registry.registerMetric(this.dialogsIterDurationHistogram);
registry.registerMetric(this.dialogsIterDurationSummary);
} }
public async get() { public async get() {
@ -79,13 +102,18 @@ class DialogsHolder {
this.isUpdating = true; this.isUpdating = true;
this.dialogs = []; this.dialogs = [];
const end = this.dialogsIterDurationHistogram.startTimer(); const end = this.dialogsIterDurationHistogram.startTimer();
try {
for await (const d of this.tg.iterDialogs()) { for await (const d of this.tg.iterDialogs()) {
if (!peersConfigBoolFilter(config, d.peer.id)) { if (!peersConfigBoolFilter(config, d.peer.id)) {
continue; continue;
} }
this.dialogs.push(d); this.dialogs.push(d);
} }
end(); } catch (e) {
console.error("Failed to iterate over telegram dialogs:", e);
}
this.dialogsIterDurationSummary.observe(end());
this.lastUpdate = process.hrtime.bigint(); this.lastUpdate = process.hrtime.bigint();
this.isUpdating = false; this.isUpdating = false;
} }

View file

@ -11,6 +11,11 @@ export interface RawKeywordPattern {
name: string; name: string;
pattern: string; pattern: string;
word: boolean; word: boolean;
flags: {
global: boolean;
multi_line: boolean;
insensitive: boolean;
};
} }
export type RawKeywordLike = string | RawKeywordPattern; export type RawKeywordLike = string | RawKeywordPattern;
@ -26,6 +31,7 @@ export function rawToPatterns(raw: RawKeywordLike[]): KeywordPattern[] {
let pattern; let pattern;
let name; let name;
let addBorders = false; let addBorders = false;
let flags = "giu";
if (typeof keyword === "string") { if (typeof keyword === "string") {
pattern = escapeRegex(keyword); pattern = escapeRegex(keyword);
@ -35,15 +41,28 @@ export function rawToPatterns(raw: RawKeywordLike[]): KeywordPattern[] {
pattern = keyword.pattern; pattern = keyword.pattern;
name = keyword.name; name = keyword.name;
addBorders = keyword.word; addBorders = keyword.word;
flags = "u";
if (keyword.flags.global) {
flags += "g";
}
if (keyword.flags.insensitive) {
flags += "i";
}
if (keyword.flags.multi_line) {
flags += "m";
}
} }
const wordBorder = escapeRegex("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"); const wordBorder = escapeRegex("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~");
const borderStart = addBorders ? `(?:[${wordBorder}\\s]|^)` : ""; const borderStart = addBorders ? `(?<=[${wordBorder}\\s]|^)` : "";
const borderEnd = addBorders ? `(?:[${wordBorder}\\s]|$)` : ""; const borderEnd = addBorders ? `(?=[${wordBorder}\\s]|$)` : "";
patterns.push({ patterns.push({
name, name,
pattern: new RegExp(borderStart + pattern + borderEnd), pattern: new RegExp(`${borderStart}(?:${pattern})${borderEnd}`, flags),
}); });
} }

View file

@ -2,37 +2,71 @@ import type { Dispatcher } from "@mtcute/dispatcher";
import type { Registry } from "prom-client"; import type { Registry } from "prom-client";
import { PropagationAction } from "@mtcute/dispatcher"; import { PropagationAction } from "@mtcute/dispatcher";
import { Counter } from "prom-client"; import { Counter, Gauge } from "prom-client";
import { config } from "../config.js"; import { config } from "../config.js";
import { peersConfigFilter } from "../filters.js"; import { peersConfigFilter } from "../filters.js";
export function collectNewMessageMetrics(dp: Dispatcher, registry: Registry) { export function collectNewMessageMetrics(dp: Dispatcher, registry: Registry) {
const sendersMap = new Map<number, string>();
const senderInfo = new Gauge({
name: "messenger_dialog_sender_info",
help: "Sender's information exposed as labels",
labelNames: ["senderId", "displayName"],
collect: () => {
senderInfo.reset();
for (const [senderId, displayName] of sendersMap.entries()) {
senderInfo.set({ senderId, displayName }, 1);
}
},
});
let labelNames;
if (config.messagesCollector.includeSender) {
labelNames = ["peerId", "senderId"];
} else {
labelNames = ["peerId"];
}
const messages = new Counter({ const messages = new Counter({
name: "messenger_dialog_messages_count", name: "messenger_dialog_messages_count",
help: "Messages count since exporter startup", help: "Messages count since exporter startup",
labelNames: ["peerId"], labelNames,
}); });
const media = new Counter({ const media = new Counter({
name: "messenger_dialog_media_sent_count", name: "messenger_dialog_media_sent_count",
help: "Medias sent since exporter startup", help: "Medias sent since exporter startup",
labelNames: ["peerId"], labelNames,
}); });
const stickers = new Counter({ const stickers = new Counter({
name: "messenger_dialog_stickers_sent_count", name: "messenger_dialog_stickers_sent_count",
help: "Stickers sent since exporter startup", help: "Stickers sent since exporter startup",
labelNames: ["peerId"], labelNames,
}); });
const voice = new Counter({ const voice = new Counter({
name: "messenger_dialog_voice_messages_count", name: "messenger_dialog_voice_messages_count",
help: "Voice messages sent since exporter startup", help: "Voice messages sent since exporter startup",
labelNames: ["peerId"], labelNames,
}); });
dp.onNewMessage(peersConfigFilter(config), (msg) => { dp.onNewMessage(peersConfigFilter(config), (msg) => {
let labelValues;
if (config.messagesCollector.includeSender) {
sendersMap.set(msg.sender.id, msg.sender.displayName);
labelValues = {
peerId: msg.chat.id,
senderId: msg.sender.id,
};
} else {
labelValues = {
peerId: msg.chat.id,
};
}
if (msg.media) { if (msg.media) {
let counter; let counter;
switch (msg.media.type) { switch (msg.media.type) {
@ -58,19 +92,17 @@ export function collectNewMessageMetrics(dp: Dispatcher, registry: Registry) {
} }
} }
if (counter) { if (counter) {
counter.inc({ counter.inc(labelValues);
peerId: msg.chat.id,
});
} }
} }
messages.inc({ messages.inc(labelValues);
peerId: msg.chat.id,
});
return PropagationAction.Continue; return PropagationAction.Continue;
}); });
if (config.messagesCollector.includeSender) {
registry.registerMetric(senderInfo);
}
registry.registerMetric(media); registry.registerMetric(media);
registry.registerMetric(stickers); registry.registerMetric(stickers);
registry.registerMetric(voice); registry.registerMetric(voice);

View file

@ -9,6 +9,7 @@ import { peersConfigFilter } from "../filters.js";
import { collectDialogMetrics } from "./dialogs.js"; import { collectDialogMetrics } from "./dialogs.js";
import { KeywordsCounter } from "./keywords.js"; import { KeywordsCounter } from "./keywords.js";
import { collectNewMessageMetrics } from "./message.js"; import { collectNewMessageMetrics } from "./message.js";
import { collectReactionsMetrics } from "./reactions.js";
function newWordsCounter(dp: Dispatcher) { function newWordsCounter(dp: Dispatcher) {
const counter = new Counter({ const counter = new Counter({
@ -32,6 +33,7 @@ function newWordsCounter(dp: Dispatcher) {
export { export {
collectDialogMetrics, collectDialogMetrics,
collectNewMessageMetrics, collectNewMessageMetrics,
collectReactionsMetrics,
KeywordsCounter, KeywordsCounter,
newWordsCounter, newWordsCounter,
}; };

213
src/metrics/reactions.ts Normal file
View file

@ -0,0 +1,213 @@
import type { Dispatcher } from "@mtcute/dispatcher";
import type { TelegramClient, tl } from "@mtcute/node";
import type { Registry } from "prom-client";
import { setTimeout } from "node:timers/promises";
import { PropagationAction } from "@mtcute/dispatcher";
import { Counter, Gauge } from "prom-client";
import { config } from "../config.js";
import { peersConfigBoolFilter, peersConfigFilter } from "../filters.js";
type ReactionsMap = Map<string, number>;
type MessageReactionsMap = Map<number, ReactionsMap>;
type PeerMessagesMap = Map<number, MessageReactionsMap>;
function getRawPeerId(peer: tl.TypePeer) {
switch (peer._) {
case "peerUser": {
return peer.userId;
}
case "peerChat": {
return peer.chatId;
}
case "peerChannel": {
return peer.channelId;
}
}
}
function getRawReactionEmoji(reaction: tl.TypeReaction) {
let emojiId: string;
let emojiName: string;
switch (reaction._) {
case "reactionEmoji": {
emojiId = reaction.emoticon;
emojiName = reaction.emoticon;
break;
}
case "reactionCustomEmoji": {
emojiId = `<custom:${reaction.documentId.toString()}>`;
emojiName = "<custom>";
break;
}
case "reactionPaid": {
emojiId = "<star_paid>";
emojiName = "⭐ (Paid)";
break;
}
case "reactionEmpty": {
emojiId = "<empty>";
emojiName = "<empty>";
break;
}
}
return { id: emojiId, name: emojiName };
}
function getEmojiNameFromId(id: string) {
if (id === "<star_paid>") {
return "⭐ (Paid)";
}
if (id.startsWith("<custom:")) {
return "<custom>";
}
return id;
}
export async function collectReactionsMetrics(tg: TelegramClient, dp: Dispatcher, registry: Registry) {
const peers: PeerMessagesMap = new Map();
const set = new Counter({
name: "messenger_dialog_reactions_set_count",
help: "Reactions set count since exporter startup",
labelNames: ["peerId", "emoji"],
});
const removed = new Counter({
name: "messenger_dialog_reactions_removed_count",
help: "Reactions removed count since exporter startup",
labelNames: ["peerId", "emoji"],
});
const peersSize = new Gauge({
name: "mtproto_exporter_reactions_collector_peers_cache_size",
help: "Size of peers cache map size in reactions collector",
collect: () => {
peersSize.set(peers.size);
},
});
const messagesSize = new Gauge({
name: "mtproto_exporter_reactions_collector_messages_cache_size",
help: "Size of messages cache map size in reactions collector",
collect: () => {
messagesSize.reset();
for (const m of peers.values()) {
messagesSize.inc(m.size);
}
},
});
const reactionsSize = new Gauge({
name: "mtproto_exporter_reactions_collector_reactions_cache_size",
help: "Size of reactions cache map size in reactions collector",
collect: () => {
reactionsSize.reset();
for (const m of peers.values()) {
for (const r of m.values()) {
reactionsSize.inc(r.size);
}
}
},
});
registry.registerMetric(set);
registry.registerMetric(removed);
registry.registerMetric(peersSize);
registry.registerMetric(messagesSize);
registry.registerMetric(reactionsSize);
if (config.reactionsCollector.loadHistory) {
console.log("fetching dialogs history into reactions collector cache....");
const historyIterOptions = {
limit: config.reactionsCollector.loadHistorySize,
};
for await (const dialog of tg.iterDialogs()) {
console.log("fetching dialog with peer id", dialog.peer.id);
if (!peersConfigBoolFilter(config, dialog.peer.id)) {
continue;
}
for await (const message of tg.iterHistory(dialog.peer.id, historyIterOptions)) {
await handleReactionsUpdate(message.id, dialog.peer.id, message.reactions?.raw.results ?? []);
}
await setTimeout(5000);
}
}
// we need to count only new messages
// because we don't know true number of reactions before updates
dp.onNewMessage((message) => {
const messages: MessageReactionsMap = peers.get(message.chat.id) ?? new Map();
const reactions: ReactionsMap = messages.get(message.id) ?? new Map();
reactions.clear();
messages.set(message.id, reactions);
peers.set(message.chat.id, messages);
return PropagationAction.Continue;
});
tg.onRawUpdate.add(async (info) => {
if ("updates" in info) {
const updates = info.updates as tl.TypeUpdate[];
const reactionsUpdates = updates.filter(u => u._ === "updateMessageReactions");
for (const update of reactionsUpdates) {
await handleReactionsUpdate(update.msgId, getRawPeerId(update.peer), update.reactions.results);
}
} else if (info.update && info.update._ === "updateMessageReactions") {
await handleReactionsUpdate(info.update.msgId, getRawPeerId(info.update.peer), info.update.reactions.results);
}
});
dp.onEditMessage(peersConfigFilter(config), async (message) => {
if (!message.reactions || !message.reactions.reactions) {
return;
}
await handleReactionsUpdate(message.id, message.chat.id, message.reactions.raw.results);
return PropagationAction.Continue;
});
async function handleReactionsUpdate(messageId: number, peerId: number, reactions: tl.RawReactionCount[]) {
const peer: MessageReactionsMap = peers.get(peerId) ?? new Map();
const oldReactions = peer.get(messageId);
const newReactions = new Map<string, number>();
for (const r of reactions) {
const emoji = getRawReactionEmoji(r.reaction);
newReactions.set(emoji.id, r.count);
}
if (!oldReactions) {
peer.set(messageId, newReactions);
peers.set(peerId, peer);
return;
}
const allReactions = new Set<string>([
...newReactions.keys(),
...oldReactions.keys(),
]);
for (const r of allReactions) {
const countBefore = oldReactions.get(r) ?? 0;
const countAfter = newReactions.get(r) ?? 0;
const diff = countAfter - countBefore;
if (diff > 0) {
set.inc({
peerId,
emoji: getEmojiNameFromId(r),
});
} else if (diff < 0) {
removed.inc({
peerId,
emoji: getEmojiNameFromId(r),
});
}
oldReactions.set(r, countAfter);
}
peer.set(messageId, oldReactions);
peers.set(peerId, peer);
}
}

View file

@ -24,7 +24,7 @@ export default class MetricsServer {
private async _requestHandler(req: http.IncomingMessage, res: http.ServerResponse) { private async _requestHandler(req: http.IncomingMessage, res: http.ServerResponse) {
const url = new URL(`http://${req.headers.host ?? "localhost"}${req.url}`); const url = new URL(`http://${req.headers.host ?? "localhost"}${req.url}`);
console.log(`[HTTP] ${req.method} - ${url.href} from ${req.socket.remoteAddress}:${req.socket.remotePort}`); console.log(`[HTTP] ${req.method} - ${req.socket.localAddress}:${req.socket.localPort} (${url.href}) from ${req.socket.remoteAddress}:${req.socket.remotePort}`);
if (req.method === "GET" && url.pathname === "/metrics") { if (req.method === "GET" && url.pathname === "/metrics") {
try { try {