add tg_bot_sessions, padding to qrcode
This commit is contained in:
Generated
+272
@@ -425,6 +425,9 @@ importers:
|
||||
eslint:
|
||||
specifier: ^9.16.0
|
||||
version: 9.16.0
|
||||
tsx:
|
||||
specifier: ^4.19.2
|
||||
version: 4.19.2
|
||||
typescript:
|
||||
specifier: ^5.7.2
|
||||
version: 5.7.2
|
||||
@@ -771,6 +774,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/aix-ppc64@0.23.1:
|
||||
resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/aix-ppc64@0.24.0:
|
||||
resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -789,6 +801,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm64@0.23.1:
|
||||
resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm64@0.24.0:
|
||||
resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -807,6 +828,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm@0.23.1:
|
||||
resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm@0.24.0:
|
||||
resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -825,6 +855,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-x64@0.23.1:
|
||||
resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-x64@0.24.0:
|
||||
resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -843,6 +882,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-arm64@0.23.1:
|
||||
resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-arm64@0.24.0:
|
||||
resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -861,6 +909,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-x64@0.23.1:
|
||||
resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-x64@0.24.0:
|
||||
resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -879,6 +936,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-arm64@0.23.1:
|
||||
resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-arm64@0.24.0:
|
||||
resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -897,6 +963,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-x64@0.23.1:
|
||||
resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-x64@0.24.0:
|
||||
resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -915,6 +990,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm64@0.23.1:
|
||||
resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm64@0.24.0:
|
||||
resolution: {integrity: sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -933,6 +1017,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm@0.23.1:
|
||||
resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm@0.24.0:
|
||||
resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -951,6 +1044,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ia32@0.23.1:
|
||||
resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ia32@0.24.0:
|
||||
resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -969,6 +1071,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-loong64@0.23.1:
|
||||
resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-loong64@0.24.0:
|
||||
resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -987,6 +1098,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-mips64el@0.23.1:
|
||||
resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-mips64el@0.24.0:
|
||||
resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1005,6 +1125,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ppc64@0.23.1:
|
||||
resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ppc64@0.24.0:
|
||||
resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1023,6 +1152,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-riscv64@0.23.1:
|
||||
resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-riscv64@0.24.0:
|
||||
resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1041,6 +1179,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-s390x@0.23.1:
|
||||
resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-s390x@0.24.0:
|
||||
resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1059,6 +1206,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-x64@0.23.1:
|
||||
resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-x64@0.24.0:
|
||||
resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1077,6 +1233,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/netbsd-x64@0.23.1:
|
||||
resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/netbsd-x64@0.24.0:
|
||||
resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1086,6 +1251,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/openbsd-arm64@0.23.1:
|
||||
resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/openbsd-arm64@0.24.0:
|
||||
resolution: {integrity: sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1104,6 +1278,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/openbsd-x64@0.23.1:
|
||||
resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/openbsd-x64@0.24.0:
|
||||
resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1122,6 +1305,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/sunos-x64@0.23.1:
|
||||
resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/sunos-x64@0.24.0:
|
||||
resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1140,6 +1332,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-arm64@0.23.1:
|
||||
resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-arm64@0.24.0:
|
||||
resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1158,6 +1359,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-ia32@0.23.1:
|
||||
resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-ia32@0.24.0:
|
||||
resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1176,6 +1386,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-x64@0.23.1:
|
||||
resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-x64@0.24.0:
|
||||
resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3430,6 +3649,38 @@ packages:
|
||||
'@esbuild/win32-x64': 0.21.5
|
||||
dev: true
|
||||
|
||||
/esbuild@0.23.1:
|
||||
resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.23.1
|
||||
'@esbuild/android-arm': 0.23.1
|
||||
'@esbuild/android-arm64': 0.23.1
|
||||
'@esbuild/android-x64': 0.23.1
|
||||
'@esbuild/darwin-arm64': 0.23.1
|
||||
'@esbuild/darwin-x64': 0.23.1
|
||||
'@esbuild/freebsd-arm64': 0.23.1
|
||||
'@esbuild/freebsd-x64': 0.23.1
|
||||
'@esbuild/linux-arm': 0.23.1
|
||||
'@esbuild/linux-arm64': 0.23.1
|
||||
'@esbuild/linux-ia32': 0.23.1
|
||||
'@esbuild/linux-loong64': 0.23.1
|
||||
'@esbuild/linux-mips64el': 0.23.1
|
||||
'@esbuild/linux-ppc64': 0.23.1
|
||||
'@esbuild/linux-riscv64': 0.23.1
|
||||
'@esbuild/linux-s390x': 0.23.1
|
||||
'@esbuild/linux-x64': 0.23.1
|
||||
'@esbuild/netbsd-x64': 0.23.1
|
||||
'@esbuild/openbsd-arm64': 0.23.1
|
||||
'@esbuild/openbsd-x64': 0.23.1
|
||||
'@esbuild/sunos-x64': 0.23.1
|
||||
'@esbuild/win32-arm64': 0.23.1
|
||||
'@esbuild/win32-ia32': 0.23.1
|
||||
'@esbuild/win32-x64': 0.23.1
|
||||
dev: true
|
||||
|
||||
/esbuild@0.24.0:
|
||||
resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3982,6 +4233,12 @@ packages:
|
||||
is-stream: 4.0.1
|
||||
dev: true
|
||||
|
||||
/get-tsconfig@4.8.1:
|
||||
resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==}
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
dev: true
|
||||
|
||||
/git-up@7.0.0:
|
||||
resolution: {integrity: sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==}
|
||||
dependencies:
|
||||
@@ -5384,6 +5641,10 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/resolve-pkg-maps@1.0.0:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
dev: true
|
||||
|
||||
/resolve@1.22.8:
|
||||
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
|
||||
hasBin: true
|
||||
@@ -6166,6 +6427,17 @@ packages:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
requiresBuild: true
|
||||
|
||||
/tsx@4.19.2:
|
||||
resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
esbuild: 0.23.1
|
||||
get-tsconfig: 4.8.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
@@ -35,7 +35,11 @@ const dbsSecurity = {
|
||||
},
|
||||
tg_bot: {
|
||||
admins: { roles: ["_admin"] },
|
||||
members: { roles: [] },
|
||||
members: { roles: ["_admin"] },
|
||||
},
|
||||
tg_bot_sessions: {
|
||||
admins: { roles: ["_admin"] },
|
||||
members: { roles: ["_admin"] },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"contributors": [],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsx src/bot/index.ts",
|
||||
"dev:watch": "tsx --watch src/bot/index.ts",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist",
|
||||
"lint": "eslint src",
|
||||
@@ -69,6 +71,7 @@
|
||||
"esbuild": "^0.24.0",
|
||||
"esbuild-copy-static-files": "^0.1.0",
|
||||
"eslint": "^9.16.0",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MyContext } from "../../types/myContext";
|
||||
import { getQRCodesByUser } from "../../api/qrCodes";
|
||||
import { MyContext } from "@/types/myContext";
|
||||
import { getQRCodesByUser } from "@/api/qrCodes";
|
||||
import { buildManageKeyboard } from "@/bot/utils/keyboards";
|
||||
|
||||
/**
|
||||
* Обработка команды /manage с выводом подменю
|
||||
@@ -23,12 +24,7 @@ export async function manageCommand(ctx: MyContext) {
|
||||
}
|
||||
|
||||
// Генерация клавиатуры с QR-кодами
|
||||
const keyboard = qrCodes.map((qr) => [
|
||||
{
|
||||
text: qr.name,
|
||||
callback_data: `manage:${qr.uri}`,
|
||||
},
|
||||
]);
|
||||
const keyboard = buildManageKeyboard(qrCodes);
|
||||
|
||||
await ctx.reply("Выберите QR-код для управления:", {
|
||||
reply_markup: {
|
||||
|
||||
@@ -68,14 +68,17 @@ export async function newQRCodeConversation(
|
||||
}
|
||||
|
||||
// 3. Выбор действий, доступных при сканировании
|
||||
await ctx.reply("Выберите доступные действия:", {
|
||||
reply_markup: {
|
||||
inline_keyboard: buildActionsKeyboard([
|
||||
{ action: "sendMessage", label: "Отправить сообщение" },
|
||||
{ action: "viewContactInfo", label: "Посмотреть контактные данные" },
|
||||
]),
|
||||
await ctx.reply(
|
||||
"Выберите одно или несколько действий, которые будут доступны при сканировании этого QR-кода:",
|
||||
{
|
||||
reply_markup: {
|
||||
inline_keyboard: buildActionsKeyboard([
|
||||
{ action: "sendMessage", label: "Отправить сообщение" },
|
||||
{ action: "viewContactInfo", label: "Посмотреть контактные данные" },
|
||||
]),
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const actionsMsg = await conversation.waitFor("callback_query:data");
|
||||
const chosenAction = actionsMsg.update.callback_query?.data;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Bot, session } from "grammy";
|
||||
import { conversations, createConversation } from "@grammyjs/conversations";
|
||||
import { MyContext } from "../types/myContext"; // Ваш расширенный контекст
|
||||
import { env } from "../config/env";
|
||||
import { MyContext } from "@/types/myContext";
|
||||
import { env } from "@/config/env";
|
||||
import { CouchDBStorageAdapter } from "./storageAdapters/CouchDBStorageAdapter";
|
||||
import { loadUserMiddleware } from "./middleware/loadUser";
|
||||
import { tgBotSessionsDb } from "@/dbs";
|
||||
import { setupCommands } from "./commands/setupCommands";
|
||||
import { startCommand } from "./commands/start";
|
||||
import { helpCommand } from "./commands/help";
|
||||
@@ -11,11 +13,15 @@ import { manageCommand } from "./commands/manage";
|
||||
import { callbackQueryHandler } from "./callbackHandlers";
|
||||
import { newQRCodeConversation } from "./conversations/newQRCodeConversation";
|
||||
|
||||
const storage = new CouchDBStorageAdapter(tgBotSessionsDb, {
|
||||
softDelete: true,
|
||||
});
|
||||
|
||||
// Инициализируем бота с нужным типом контекста
|
||||
const bot = new Bot<MyContext>(env.TELEGRAM_BOT_TOKEN);
|
||||
|
||||
// Подключаем сессии. По необходимости определите initial state для сессии
|
||||
bot.use(session({ initial: () => ({}) }));
|
||||
bot.use(session({ storage, initial: () => ({}) }));
|
||||
|
||||
// Подключаем conversations для многошаговых сценариев
|
||||
bot.use(conversations());
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import type { StorageAdapter } from "grammy";
|
||||
|
||||
export interface CouchDBStorageOptions {
|
||||
/** Если true, при удалении сессии будет ставиться _deleted: true для поддержки репликаций по условиям полей */
|
||||
softDelete?: boolean;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface CouchDBSessionDocument<T> {
|
||||
_id: string;
|
||||
_rev?: string;
|
||||
type: string;
|
||||
session?: T;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
_deleted?: boolean;
|
||||
}
|
||||
|
||||
export class CouchDBStorageAdapter<T> implements StorageAdapter<T> {
|
||||
constructor(
|
||||
private db: PouchDB.Database<CouchDBSessionDocument<T>>,
|
||||
private options: CouchDBStorageOptions = {
|
||||
softDelete: false,
|
||||
type: "tg_bot_session",
|
||||
},
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Чтение сессии по ключу.
|
||||
* Возвращает данные сессии или undefined, если сессия не найдена.
|
||||
*/
|
||||
async read(key: string) {
|
||||
try {
|
||||
const id = this.getId(key);
|
||||
const doc = await this.db.get(id);
|
||||
return doc.session;
|
||||
} catch (error: any) {
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запись или обновление сессии по ключу.
|
||||
* При конфликте повторяет попытку записи.
|
||||
*/
|
||||
async write(key: string, value: T) {
|
||||
const _id = this.getId(key);
|
||||
await this.retryOnConflict(async () => {
|
||||
let doc: CouchDBSessionDocument<T>;
|
||||
const updated_at = new Date().toISOString();
|
||||
try {
|
||||
doc = await this.db.get(_id);
|
||||
doc.session = value;
|
||||
doc.updated_at = updated_at;
|
||||
} catch (error: any) {
|
||||
if (error.status === 404) {
|
||||
// Документ не найден, создаём новый
|
||||
doc = {
|
||||
_id,
|
||||
type: "tg_bot_session",
|
||||
session: value,
|
||||
created_at: updated_at,
|
||||
updated_at,
|
||||
};
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
await this.db.put(doc);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаление сессии по ключу без предварительного получения документа.
|
||||
* Если включен softDelete, пытаемся сразу поставить _deleted: true.
|
||||
* Если нет softDelete, просто пытаемся удалить без rev, что приведёт к ошибке 409,
|
||||
* тогда при конфликте получим актуальный _rev и удалим.
|
||||
*/
|
||||
async delete(key: string) {
|
||||
const id = this.getId(key);
|
||||
await this.retryOnConflict(async () => {
|
||||
let doc: CouchDBSessionDocument<T>;
|
||||
try {
|
||||
doc = await this.db.get(id);
|
||||
} catch (error: any) {
|
||||
if (error.status === 404) {
|
||||
return; // Документ уже отсутствует
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.softDelete) {
|
||||
doc._deleted = true; // Помечаем документ как удалённый
|
||||
await this.db.put(doc);
|
||||
} else {
|
||||
await this.db.remove(id, doc._rev!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Вспомогательная функция для повторения операции при конфликте.
|
||||
* Если при put/remove случится конфликт, повторим попытку до retries раз.
|
||||
* fn принимает параметр attempt (номер попытки, начиная с 1).
|
||||
*/
|
||||
private async retryOnConflict(
|
||||
fn: (attempt: number) => Promise<void>,
|
||||
retries = 3,
|
||||
): Promise<void> {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
await fn(i + 1);
|
||||
return;
|
||||
} catch (error: any) {
|
||||
if (error.status === 409 && i < retries - 1) {
|
||||
// Попытка ещё раз при конфликте
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getId(key: string) {
|
||||
return this.options.type ? `${this.options.type}:${key}` : key;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export function buildManageKeyboard(
|
||||
return qrCodes.map((qr) => [
|
||||
{
|
||||
text: qr.name,
|
||||
callback_data: `manage:${qr.uri}:delete`, // Пример - сразу кнопка на удаление. Можно сделать отдельное меню.
|
||||
callback_data: `manage:${qr.uri}`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import type {
|
||||
} from "@hereconnect/types";
|
||||
|
||||
import type { TelegramWebhookDesignDocument } from "@hereconnect/tg-bot-webhook-ddoc";
|
||||
import { CouchDBSessionDocument } from "@/bot/storageAdapters/CouchDBStorageAdapter";
|
||||
import type { SessionData } from "@/types/myContext";
|
||||
// import type { TgUpdateDocument } from "./types";
|
||||
|
||||
if (!process.env.API_URL) {
|
||||
@@ -60,6 +62,13 @@ export const tgBotDesignDocumentsDb =
|
||||
tgBotDb as PouchDB.Database<TelegramWebhookDesignDocument>;
|
||||
export const serviceSeqTrackerDb = tgBotDb as ServiceSeqTrackerDatabase;
|
||||
|
||||
export const tgBotSessionsDb = new PouchDB<CouchDBSessionDocument<SessionData>>(
|
||||
`${process.env.API_URL}/tg_bot_sessions`,
|
||||
{
|
||||
skip_setup: true,
|
||||
},
|
||||
);
|
||||
|
||||
export const userChatSettingsDb = new PouchDB<UserChatSettingsDocument>(
|
||||
`${process.env.API_URL}/user_chat_settings`,
|
||||
{
|
||||
@@ -84,6 +93,7 @@ export const checkDatabases = async () => {
|
||||
serviceSeqTrackerDb,
|
||||
userChatSettingsDb,
|
||||
tgBotDb,
|
||||
tgBotSessionsDb,
|
||||
} as const).map(
|
||||
async ([name, db]): Promise<
|
||||
[string, Awaited<ReturnType<typeof db.info>>]
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import QRCodeStyling from "qr-code-styling";
|
||||
import { optimizeSvg } from "../utils/images/svg/optimizeSvg";
|
||||
import { replaceFillInSvg } from "../utils/images/svg/replaceFillInSvg";
|
||||
import { addLabelToSvg } from "../utils/images/svg/addLabelToSvg";
|
||||
import { SvgProcessor } from "@/utils/images/svg/SvgProcessor";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
@@ -16,6 +14,7 @@ interface GenerateOptions {
|
||||
logoColor?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
padding?: number;
|
||||
}
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -28,18 +27,21 @@ export async function generateQRCodeImage(
|
||||
label = "Сообщите о проблеме или задайте вопрос",
|
||||
color = "#000",
|
||||
logoColor = "#000",
|
||||
width = 1024,
|
||||
height = 1024,
|
||||
width = 800,
|
||||
height = 800,
|
||||
padding = 32,
|
||||
}: GenerateOptions,
|
||||
): Promise<Buffer> {
|
||||
const optimizeOutput = format === "svg";
|
||||
const logoPath = path.resolve(__dirname, "../../assets/logo.svg");
|
||||
let logoSvg = await fs.readFile(logoPath, "utf8");
|
||||
|
||||
// Заменяем цвет fill в лого
|
||||
logoSvg = replaceFillInSvg(logoSvg, logoColor);
|
||||
|
||||
// Добавляем лейбл "НаСвязи" в логотип (если нужно)
|
||||
const logoWithText = addLabelToSvg(logoSvg, "НаСвязи", logoColor, 0.01);
|
||||
const logoWithText = new SvgProcessor(
|
||||
await fs.readFile(logoPath, "utf8"),
|
||||
optimizeOutput,
|
||||
)
|
||||
.replaceFill(logoColor) // Заменяем цвет fill в лого
|
||||
.addLabel("НаСвязи", logoColor, 0.01) // Добавляем лейбл "НаСвязи" в логотип
|
||||
// .addPadding(padding) // Добавляем отступы вокруг логотипа
|
||||
.toString();
|
||||
|
||||
const qrCode = new QRCodeStyling({
|
||||
jsdom: JSDOM, // this is required
|
||||
@@ -74,22 +76,22 @@ export async function generateQRCodeImage(
|
||||
}
|
||||
|
||||
// Преобразуем SVG в строку
|
||||
const svgText = rawData.toString("utf8");
|
||||
let optimizedSvg = optimizeSvg(svgText);
|
||||
if (!optimizedSvg) {
|
||||
throw new Error("Ошибка оптимизации SVG");
|
||||
}
|
||||
let resultSvg = rawData.toString("utf8");
|
||||
|
||||
// Добавляем текст, если указано
|
||||
if (label) {
|
||||
optimizedSvg = addLabelToSvg(optimizedSvg, label, color);
|
||||
}
|
||||
resultSvg = new SvgProcessor(resultSvg, optimizeOutput)
|
||||
.addLabel(label, color)
|
||||
.addPadding(padding)
|
||||
.toString();
|
||||
|
||||
if (format === "svg") {
|
||||
if (!resultSvg) {
|
||||
throw new Error("Ошибка оптимизации SVG");
|
||||
}
|
||||
|
||||
// Если формат SVG, возвращаем буфер SVG
|
||||
return Buffer.from(optimizedSvg, "utf8");
|
||||
return Buffer.from(resultSvg, "utf8");
|
||||
}
|
||||
|
||||
// Если формат PNG, преобразуем optimizedSvg в PNG через sharp
|
||||
return sharp(Buffer.from(optimizedSvg, "utf8")).toFormat("png").toBuffer();
|
||||
// Если формат PNG, преобразуем resultSvg в PNG через sharp
|
||||
return sharp(Buffer.from(resultSvg, "utf8")).toFormat("png").toBuffer();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Context, SessionFlavor } from "grammy";
|
||||
import { ConversationFlavor } from "@grammyjs/conversations";
|
||||
import type { UserDocument } from "@hereconnect/types";
|
||||
|
||||
interface SessionData {
|
||||
export interface SessionData {
|
||||
step?: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
DOMParser,
|
||||
XMLSerializer,
|
||||
type Document as XmlDomDocument,
|
||||
type Element as XmlElement,
|
||||
} from "@xmldom/xmldom";
|
||||
import { optimize } from "svgo";
|
||||
import { calculateFontSizeForText } from "./calculateFontSizeForText";
|
||||
|
||||
export class SvgProcessor {
|
||||
private doc: XmlDomDocument;
|
||||
private svgElement: XmlElement;
|
||||
|
||||
constructor(
|
||||
svgText: string,
|
||||
private optimizeOutput: boolean = false,
|
||||
) {
|
||||
const parser = new DOMParser();
|
||||
this.doc = parser.parseFromString(svgText, "image/svg+xml");
|
||||
const svgElement = this.doc.getElementsByTagName("svg").item(0);
|
||||
if (!svgElement) {
|
||||
throw new Error("SVG не найден");
|
||||
}
|
||||
this.svgElement = svgElement;
|
||||
}
|
||||
|
||||
addPadding(padding: number, backgroundColor = "transparent"): SvgProcessor {
|
||||
const viewBox = this.svgElement
|
||||
.getAttribute("viewBox")
|
||||
?.split(" ")
|
||||
.map(Number);
|
||||
if (!viewBox) {
|
||||
throw new Error("viewBox не найден");
|
||||
}
|
||||
const [x, y, width, height] = viewBox;
|
||||
|
||||
// Увеличиваем размеры viewBox с учётом отступов
|
||||
const newWidth = width + padding * 2;
|
||||
const newHeight = height + padding * 2;
|
||||
|
||||
const newViewBox = [x, y, newWidth, newHeight].join(" ");
|
||||
this.svgElement.setAttribute("viewBox", newViewBox);
|
||||
|
||||
// Добавляем прямоугольник для фона, если требуется
|
||||
if (backgroundColor !== "transparent") {
|
||||
const rect = this.doc.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"rect",
|
||||
);
|
||||
rect.setAttribute("x", x.toString());
|
||||
rect.setAttribute("y", y.toString());
|
||||
rect.setAttribute("width", newWidth.toString());
|
||||
rect.setAttribute("height", newHeight.toString());
|
||||
rect.setAttribute("fill", backgroundColor);
|
||||
this.svgElement.insertBefore(rect, this.svgElement.firstChild);
|
||||
}
|
||||
|
||||
// Сдвигаем все элементы
|
||||
const elements = Array.from(this.svgElement.childNodes).filter(
|
||||
(node) => node.nodeType === 1 && node !== this.svgElement.firstChild,
|
||||
) as XmlElement[];
|
||||
|
||||
elements.forEach((el) => {
|
||||
const isText = el.tagName === "text";
|
||||
|
||||
// Для текста добавляем дополнительное смещение
|
||||
if (isText) {
|
||||
const dy = el.getAttribute("dy") || "0";
|
||||
if (!dy.includes("%")) {
|
||||
el.setAttribute("dy", (parseFloat(dy) - padding).toString());
|
||||
}
|
||||
const dx = el.getAttribute("dx") || "0";
|
||||
if (!dx.includes("%") && !el.getAttribute("x")?.includes("%")) {
|
||||
el.setAttribute("dx", padding.toString());
|
||||
}
|
||||
} else {
|
||||
const transform = el.getAttribute("transform") || "";
|
||||
el.setAttribute(
|
||||
"transform",
|
||||
`translate(${padding}, ${padding}) ${transform}`.trim(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Удаляем атрибуты width и height, чтобы размеры SVG задавались только через viewBox
|
||||
this.svgElement.removeAttribute("width");
|
||||
this.svgElement.removeAttribute("height");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
addLabel(
|
||||
label: string,
|
||||
color: string,
|
||||
marginMultiplier: number = 0.1,
|
||||
): SvgProcessor {
|
||||
if (!label) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const viewBoxAttr = this.svgElement.getAttribute("viewBox");
|
||||
if (!viewBoxAttr) {
|
||||
throw new Error("viewBox не найден");
|
||||
}
|
||||
|
||||
const viewBox: readonly number[] = viewBoxAttr.split(" ").map(Number);
|
||||
const [x, y, width, height] = viewBox;
|
||||
if (viewBox.length === 4) {
|
||||
// Рассчитываем размер шрифта
|
||||
const fontSize = calculateFontSizeForText(label, width);
|
||||
const textMargin = Math.min(fontSize, height * marginMultiplier);
|
||||
const extraBottomPadding = fontSize * 0.3; // 30% дополнительного пространства для висячих символов
|
||||
|
||||
// Увеличиваем высоту viewBox
|
||||
const newHeight = height + textMargin + fontSize;
|
||||
this.svgElement.setAttribute(
|
||||
"viewBox",
|
||||
[x, y, width, newHeight].join(" "),
|
||||
);
|
||||
|
||||
// Создаем элемент текста
|
||||
const textElement = this.doc.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"text",
|
||||
);
|
||||
|
||||
textElement.setAttribute("x", "50%");
|
||||
textElement.setAttribute("y", "100%");
|
||||
textElement.setAttribute("dy", `-${extraBottomPadding}px`);
|
||||
textElement.setAttribute("text-anchor", "middle");
|
||||
textElement.setAttribute("alignment-baseline", "baseline");
|
||||
textElement.setAttribute("dominant-baseline", "alphabetic");
|
||||
textElement.setAttribute("fill", color);
|
||||
textElement.setAttribute("font-size", fontSize.toString());
|
||||
textElement.setAttribute("font-family", "Arial, sans-serif");
|
||||
textElement.textContent = label;
|
||||
|
||||
this.svgElement.appendChild(textElement);
|
||||
}
|
||||
|
||||
// Удаляем атрибуты width и height, чтобы размеры SVG задавались только через viewBox
|
||||
this.svgElement.removeAttribute("width");
|
||||
this.svgElement.removeAttribute("height");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
replaceFill(fillColor: string): SvgProcessor {
|
||||
const elements = this.doc.getElementsByTagName("*");
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const el = elements.item(i);
|
||||
if (el && el.hasAttribute("fill")) {
|
||||
el.setAttribute("fill", fillColor);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Оптимизация SVG с использованием SVGO.
|
||||
* @param svgText string изображения SVG.
|
||||
* @returns Строка оптимизированного SVG.
|
||||
*/
|
||||
private optimizeSvg(svgText: string): string {
|
||||
const optimizedSvg = optimize(svgText, {
|
||||
plugins: [
|
||||
{ name: "removeDoctype" },
|
||||
{ name: "removeComments" },
|
||||
{ name: "removeDimensions" },
|
||||
{ name: "convertShapeToPath" },
|
||||
{ name: "convertPathData" },
|
||||
{ name: "mergePaths" },
|
||||
],
|
||||
});
|
||||
|
||||
if (!optimizedSvg.data) {
|
||||
throw new Error("Ошибка оптимизации SVG");
|
||||
}
|
||||
|
||||
return optimizedSvg.data;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const serializer = new XMLSerializer();
|
||||
const result = serializer.serializeToString(this.doc);
|
||||
// console.log(result);
|
||||
if (this.optimizeOutput) {
|
||||
return this.optimizeSvg(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
|
||||
import { calculateFontSizeForText } from "./calculateFontSizeForText";
|
||||
|
||||
/**
|
||||
* Добавление текста в SVG, прибитого к нижнему краю.
|
||||
* @param svgText Исходный SVG в виде строки.
|
||||
* @param label Надпись для добавления.
|
||||
* @param color Цвет текста.
|
||||
* @param marginMultiplier Множитель относительного отступа.
|
||||
* @returns SVG с добавленным текстом.
|
||||
*/
|
||||
export function addLabelToSvg(
|
||||
svgText: string,
|
||||
label: string,
|
||||
color: string,
|
||||
marginMultiplier: number = 0.1,
|
||||
): string {
|
||||
if (!label) {
|
||||
return svgText;
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
const svgDoc = parser.parseFromString(svgText, "image/svg+xml");
|
||||
const svgElement = svgDoc.getElementsByTagName("svg").item(0);
|
||||
|
||||
if (svgElement) {
|
||||
const viewBoxAttr = svgElement.getAttribute("viewBox");
|
||||
if (viewBoxAttr) {
|
||||
const viewBox = viewBoxAttr.split(" ").map(Number);
|
||||
if (viewBox.length === 4) {
|
||||
// Рассчитываем размер шрифта
|
||||
const fontSize = calculateFontSizeForText(label, viewBox[2]);
|
||||
const textMargin = Math.min(fontSize, viewBox[3] * marginMultiplier);
|
||||
|
||||
// Увеличиваем высоту viewBox
|
||||
viewBox[3] += fontSize + textMargin;
|
||||
svgElement.setAttribute("viewBox", viewBox.join(" "));
|
||||
|
||||
// Создаем элемент текста
|
||||
const textElement = svgDoc.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"text",
|
||||
);
|
||||
textElement.setAttribute("x", "50%");
|
||||
textElement.setAttribute("y", "100%");
|
||||
textElement.setAttribute("text-anchor", "middle");
|
||||
textElement.setAttribute("alignment-baseline", "baseline");
|
||||
textElement.setAttribute("dominant-baseline", "text-after-edge");
|
||||
textElement.setAttribute("fill", color);
|
||||
textElement.setAttribute("font-size", fontSize.toString());
|
||||
textElement.setAttribute("font-family", "Arial, sans-serif");
|
||||
textElement.textContent = label;
|
||||
|
||||
svgElement.appendChild(textElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const serializer = new XMLSerializer();
|
||||
return serializer.serializeToString(svgDoc);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { optimize } from "svgo";
|
||||
|
||||
/**
|
||||
* Оптимизация SVG с использованием SVGO.
|
||||
* @param svgText string изображения SVG.
|
||||
* @returns Строка оптимизированного SVG.
|
||||
*/
|
||||
export function optimizeSvg(svgText: string): string {
|
||||
const optimizedSvg = optimize(svgText, {
|
||||
plugins: [
|
||||
{ name: "removeDoctype" },
|
||||
{ name: "removeComments" },
|
||||
{ name: "removeDimensions" },
|
||||
{ name: "convertShapeToPath" },
|
||||
{ name: "convertPathData" },
|
||||
{ name: "mergePaths" },
|
||||
],
|
||||
});
|
||||
|
||||
if (!optimizedSvg.data) {
|
||||
throw new Error("Ошибка оптимизации SVG");
|
||||
}
|
||||
|
||||
return optimizedSvg.data;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
|
||||
|
||||
/**
|
||||
* Заменяет атрибут `fill` в SVG-коде.
|
||||
* @param svgText Исходный SVG в виде строки.
|
||||
* @param fillColor Новый цвет для fill.
|
||||
* @returns Измененный SVG-код.
|
||||
*/
|
||||
export function replaceFillInSvg(svgText: string, fillColor: string): string {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(svgText, "image/svg+xml");
|
||||
|
||||
const elements = doc.getElementsByTagName("*");
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const el = elements.item(i);
|
||||
if (el && el.hasAttribute("fill")) {
|
||||
el.setAttribute("fill", fillColor);
|
||||
}
|
||||
}
|
||||
|
||||
const serializer = new XMLSerializer();
|
||||
return serializer.serializeToString(doc);
|
||||
}
|
||||
@@ -10,9 +10,8 @@
|
||||
"skipLibCheck": true, // Пропуск проверки типов в библиотечных файлах
|
||||
"declaration": true, // Генерация `.d.ts` файлов
|
||||
"sourceMap": true, // Генерация `.map` файлов
|
||||
"baseUrl": "./", // Базовая директория для путей
|
||||
"paths": {
|
||||
"*": ["src/*"] // Настройка алиасов (сопоставление путей)
|
||||
"@/*": ["./src/*"] // Настройка алиасов (сопоставление путей)
|
||||
}
|
||||
},
|
||||
"include": ["src"], // Указываем папку с исходниками
|
||||
|
||||
Reference in New Issue
Block a user