send messages to telegram from RQCP (ReadQrcodeChatPage) with link to MQCP (ManageQrcodeChatPage)
Deploy app frontend / app frontend (push) Successful in 2m43s
Deploy db-migrations / db-migrations (push) Successful in 1m8s
Deploy service message-delivery-method-web-push / service message-delivery-method-web-push (push) Successful in 1m30s
Deploy service tg-bot / service tg-bot (push) Failing after 1m54s
Deploy app frontend / app frontend (push) Successful in 2m43s
Deploy db-migrations / db-migrations (push) Successful in 1m8s
Deploy service message-delivery-method-web-push / service message-delivery-method-web-push (push) Successful in 1m30s
Deploy service tg-bot / service tg-bot (push) Failing after 1m54s
This commit is contained in:
@@ -21,6 +21,8 @@ jobs:
|
|||||||
COUCHDB_PASSWORD: ${{ secrets.COUCHDB_PASSWORD }}
|
COUCHDB_PASSWORD: ${{ secrets.COUCHDB_PASSWORD }}
|
||||||
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
|
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
|
||||||
READ_BASE_URL: ${{ vars.READ_BASE_URL }}
|
READ_BASE_URL: ${{ vars.READ_BASE_URL }}
|
||||||
|
APP_BASE_URL: ${{ vars.APP_BASE_URL }}
|
||||||
|
HEARTBEAT_INTERVAL: ${{ vars.HEARTBEAT_INTERVAL }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="message in messages"
|
v-for="message in messages"
|
||||||
:key="message._id"
|
:key="message._id"
|
||||||
|
:id="`message-${message.created_at}`"
|
||||||
class="mb-4 max-w-md"
|
class="mb-4 max-w-md"
|
||||||
:class="{
|
:class="{
|
||||||
'ml-auto text-right': message.from === userRole,
|
'ml-auto text-right': message.from === userRole,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import http from "http";
|
|||||||
* @param defaultPort - Порт для сервера по умолчанию (если не указан в ENV HEALTH_PORT).
|
* @param defaultPort - Порт для сервера по умолчанию (если не указан в ENV HEALTH_PORT).
|
||||||
* @param defaultHostName - Имя хоста для сервера по умолчанию (если не указан в ENV HEALTH_HOST).
|
* @param defaultHostName - Имя хоста для сервера по умолчанию (если не указан в ENV HEALTH_HOST).
|
||||||
*/
|
*/
|
||||||
export function startHealthServer(
|
export function createHealthServer(
|
||||||
controller: AbortController,
|
controller: AbortController,
|
||||||
defaultPort: number = 8000,
|
defaultPort: number = 8000,
|
||||||
defaultHostName: string = "127.0.0.1",
|
defaultHostName: string = "127.0.0.1",
|
||||||
@@ -43,20 +43,45 @@ export function startHealthServer(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
server?.unref?.();
|
||||||
server.once("error", reject);
|
|
||||||
server.listen(port, hostname, () => {
|
|
||||||
resolve();
|
|
||||||
console.log(
|
|
||||||
`Healthcheck server running on http://${hostname}:${port}/health`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Закрываем сервер при активации AbortController
|
const stop = () => {
|
||||||
controller.signal.addEventListener("abort", () => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
console.log("Healthcheck server shutting down due to abort signal.");
|
server.close((error) => {
|
||||||
reject();
|
if (error) reject(error);
|
||||||
server.close();
|
else resolve();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const start = () => {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
server.once("error", reject);
|
||||||
|
server.listen(port, hostname, () => {
|
||||||
|
resolve();
|
||||||
|
console.log(
|
||||||
|
`Healthcheck server running on http://${hostname}:${port}/health`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Закрываем сервер при активации AbortController
|
||||||
|
controller.signal.addEventListener("abort", () => {
|
||||||
|
console.log("Healthcheck server shutting down due to abort signal.");
|
||||||
|
stop().then(reject, reject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const startHealthServer = (
|
||||||
|
controller: AbortController,
|
||||||
|
defaultPort: number = 8000,
|
||||||
|
defaultHostName: string = "127.0.0.1",
|
||||||
|
) => {
|
||||||
|
return createHealthServer(controller, defaultPort, defaultHostName).start();
|
||||||
|
};
|
||||||
|
|||||||
@@ -24,7 +24,12 @@ export interface UserChatSettingsDocument extends BaseDocument {
|
|||||||
browser_uuid: string;
|
browser_uuid: string;
|
||||||
|
|
||||||
/** Включены ли push-уведомления */
|
/** Включены ли push-уведомления */
|
||||||
is_push_notification_enabled: boolean;
|
is_push_notification_enabled?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Включены ли сообщения в Telegram
|
||||||
|
*/
|
||||||
|
is_telegram_messages_enabled?: boolean;
|
||||||
|
|
||||||
/** Дата последнего обновления */
|
/** Дата последнего обновления */
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
|
|||||||
Generated
+22
@@ -337,6 +337,9 @@ importers:
|
|||||||
'@grammyjs/conversations':
|
'@grammyjs/conversations':
|
||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.2.0(grammy@1.33.0)
|
version: 1.2.0(grammy@1.33.0)
|
||||||
|
'@grammyjs/parse-mode':
|
||||||
|
specifier: ^1.10.0
|
||||||
|
version: 1.10.0(grammy@1.33.0)
|
||||||
'@hereconnect/constants':
|
'@hereconnect/constants':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/constants
|
version: link:../../packages/constants
|
||||||
@@ -425,6 +428,9 @@ importers:
|
|||||||
tsx:
|
tsx:
|
||||||
specifier: ^4.19.2
|
specifier: ^4.19.2
|
||||||
version: 4.19.2
|
version: 4.19.2
|
||||||
|
type-fest:
|
||||||
|
specifier: ^4.30.1
|
||||||
|
version: 4.30.1
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.7.2
|
specifier: ^5.7.2
|
||||||
version: 5.7.2
|
version: 5.7.2
|
||||||
@@ -921,6 +927,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
grammy: ^1.20.1
|
grammy: ^1.20.1
|
||||||
|
|
||||||
|
'@grammyjs/parse-mode@1.10.0':
|
||||||
|
resolution: {integrity: sha512-ZjbY2Ax0b4Nf8lPz3NV0cWDxUC10kOkzgxws+iTdgG+hAiPQVUnP/oJnAKrW1ZQOWULZYQ4GOR/+aCZHyUuLQA==}
|
||||||
|
engines: {node: '>=14.13.1'}
|
||||||
|
peerDependencies:
|
||||||
|
grammy: ^1.20.1
|
||||||
|
|
||||||
'@grammyjs/types@3.17.0':
|
'@grammyjs/types@3.17.0':
|
||||||
resolution: {integrity: sha512-e8AR3xQwRAFX248E7Qw/7mIu1OzvoXloJzOBJVtuPKzzL7tGkn5trZAdZUBgGViVQg5ZwVS/x9N2nRrcyH/DfA==}
|
resolution: {integrity: sha512-e8AR3xQwRAFX248E7Qw/7mIu1OzvoXloJzOBJVtuPKzzL7tGkn5trZAdZUBgGViVQg5ZwVS/x9N2nRrcyH/DfA==}
|
||||||
|
|
||||||
@@ -3637,6 +3649,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
|
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
type-fest@4.30.1:
|
||||||
|
resolution: {integrity: sha512-ojFL7eDMX2NF0xMbDwPZJ8sb7ckqtlAi1GsmgsFXvErT9kFTk1r0DuQKvrCh73M6D4nngeHJmvogF9OluXs7Hw==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
typescript-eslint@8.16.0:
|
typescript-eslint@8.16.0:
|
||||||
resolution: {integrity: sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==}
|
resolution: {integrity: sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -4426,6 +4442,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@grammyjs/parse-mode@1.10.0(grammy@1.33.0)':
|
||||||
|
dependencies:
|
||||||
|
grammy: 1.33.0
|
||||||
|
|
||||||
'@grammyjs/types@3.17.0': {}
|
'@grammyjs/types@3.17.0': {}
|
||||||
|
|
||||||
'@humanfs/core@0.19.1': {}
|
'@humanfs/core@0.19.1': {}
|
||||||
@@ -7380,6 +7400,8 @@ snapshots:
|
|||||||
|
|
||||||
type-fest@0.20.2: {}
|
type-fest@0.20.2: {}
|
||||||
|
|
||||||
|
type-fest@4.30.1: {}
|
||||||
|
|
||||||
typescript-eslint@8.16.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3):
|
typescript-eslint@8.16.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)
|
'@typescript-eslint/eslint-plugin': 8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)
|
||||||
|
|||||||
@@ -10,11 +10,6 @@
|
|||||||
"node": ">=v20.10.0",
|
"node": ">=v20.10.0",
|
||||||
"pnpm": ">=8.15.0"
|
"pnpm": ">=8.15.0"
|
||||||
},
|
},
|
||||||
"homepage": "",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": ""
|
|
||||||
},
|
|
||||||
"files": [
|
"files": [
|
||||||
"dist/**/*"
|
"dist/**/*"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export const userChatSettingsDb = new PouchDB<UserChatSettingsDocument>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const checkDatabases = async () => {
|
const checkDatabases = async () => {
|
||||||
const entries = await Promise.all(
|
const entries = await Promise.all(
|
||||||
Object.entries({
|
Object.entries({
|
||||||
serverSettingsDb,
|
serverSettingsDb,
|
||||||
@@ -92,3 +92,5 @@ export const checkDatabases = async () => {
|
|||||||
|
|
||||||
return Object.fromEntries(entries);
|
return Object.fromEntries(entries);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkDatabasesPromise = checkDatabases();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
messagesDbUrl,
|
messagesDbUrl,
|
||||||
webPushSubscriptionsDb,
|
webPushSubscriptionsDb,
|
||||||
userChatSettingsDb,
|
userChatSettingsDb,
|
||||||
checkDatabases,
|
checkDatabasesPromise,
|
||||||
serviceSeqTrackerDb,
|
serviceSeqTrackerDb,
|
||||||
} from "./dbs";
|
} from "./dbs";
|
||||||
import { setupVapidDetails } from "./setupVapidDetails";
|
import { setupVapidDetails } from "./setupVapidDetails";
|
||||||
@@ -29,7 +29,7 @@ function getTopicSafe(str: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
const checkedDatabases = await checkDatabases();
|
const checkedDatabases = await checkDatabasesPromise;
|
||||||
await setupVapidDetails();
|
await setupVapidDetails();
|
||||||
|
|
||||||
const serviceSeqTracker = await ServiceSeqTracker.Init(
|
const serviceSeqTracker = await ServiceSeqTracker.Init(
|
||||||
@@ -47,10 +47,7 @@ const start = async () => {
|
|||||||
heartbeat,
|
heartbeat,
|
||||||
filter: "_selector",
|
filter: "_selector",
|
||||||
selector: {
|
selector: {
|
||||||
$or: [
|
type: "message",
|
||||||
{ type: "message" },
|
|
||||||
{ type: "service", service: "message-delivery-method-web-push" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -76,14 +73,14 @@ const start = async () => {
|
|||||||
for await (const change of changesStream) {
|
for await (const change of changesStream) {
|
||||||
const { seq, doc: msgDoc } = change;
|
const { seq, doc: msgDoc } = change;
|
||||||
|
|
||||||
if (!msgDoc) {
|
|
||||||
console.error("Change missing document:", change);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceSeqTracker.checkNextSeq(seq);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!msgDoc) {
|
||||||
|
console.error("Change missing document:", change);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceSeqTracker.checkNextSeq(seq);
|
||||||
|
|
||||||
const qrDoc = await qrDb.get(`qr_code:${msgDoc.qr_code_uri}`);
|
const qrDoc = await qrDb.get(`qr_code:${msgDoc.qr_code_uri}`);
|
||||||
const timestamp = new Date(msgDoc.created_at).getTime();
|
const timestamp = new Date(msgDoc.created_at).getTime();
|
||||||
|
|
||||||
@@ -264,7 +261,7 @@ const start = async () => {
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
`Failed processing: ${JSON.stringify(error)} change: ${JSON.stringify({ id: msgDoc._id, rev: msgDoc._rev, seq: seq })}`,
|
`Failed processing: ${JSON.stringify(error)} change: ${JSON.stringify({ id: msgDoc?._id, rev: msgDoc?._rev, seq: seq })}`,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
await serviceSeqTracker.saveSeq(seq);
|
await serviceSeqTracker.saveSeq(seq);
|
||||||
|
|||||||
@@ -11,9 +11,6 @@
|
|||||||
"declaration": true, // Генерация `.d.ts` файлов
|
"declaration": true, // Генерация `.d.ts` файлов
|
||||||
"sourceMap": true, // Генерация `.map` файлов
|
"sourceMap": true, // Генерация `.map` файлов
|
||||||
"baseUrl": "./", // Базовая директория для путей
|
"baseUrl": "./", // Базовая директория для путей
|
||||||
"paths": {
|
|
||||||
"*": ["src/*"] // Настройка алиасов (сопоставление путей)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"include": ["src"], // Указываем папку с исходниками
|
"include": ["src"], // Указываем папку с исходниками
|
||||||
"exclude": ["node_modules", "dist"] // Исключаем ненужные директории
|
"exclude": ["node_modules", "dist"] // Исключаем ненужные директории
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "@hereconnect/service-tg-bot",
|
"name": "@hereconnect/service-tg-bot",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/server.ts",
|
"main": "dist/service.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "@hereconnect/license",
|
"license": "@hereconnect/license",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -29,17 +29,18 @@
|
|||||||
"contributors": [],
|
"contributors": [],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "tsx src/bot/index.ts",
|
"dev": "tsx src/service.ts",
|
||||||
"dev:watch": "tsx --watch src/bot/index.ts",
|
"dev:watch": "tsx --watch src/service.ts",
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"lint:fix": "eslint src --fix",
|
"lint:fix": "eslint src --fix",
|
||||||
"validate": "npm run type-check && npm run lint",
|
"validate": "npm run type-check && npm run lint",
|
||||||
"start": "node --import=extensionless/register dist/bot/index.js"
|
"start": "node --import=extensionless/register dist/service.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grammyjs/conversations": "^1.2.0",
|
"@grammyjs/conversations": "^1.2.0",
|
||||||
|
"@grammyjs/parse-mode": "^1.10.0",
|
||||||
"@hereconnect/constants": "workspace:^",
|
"@hereconnect/constants": "workspace:^",
|
||||||
"@hereconnect/couchdb-changes-stream": "workspace:^",
|
"@hereconnect/couchdb-changes-stream": "workspace:^",
|
||||||
"@hereconnect/lib-service-health-checkserver": "workspace:^",
|
"@hereconnect/lib-service-health-checkserver": "workspace:^",
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
"@typescript-eslint/parser": "^8.18.0",
|
"@typescript-eslint/parser": "^8.18.0",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.16.0",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
|
"type-fest": "^4.30.1",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { type QRCodeDocument } from "@hereconnect/types";
|
|||||||
import { Conversation } from "@grammyjs/conversations";
|
import { Conversation } from "@grammyjs/conversations";
|
||||||
import { InputFile } from "grammy";
|
import { InputFile } from "grammy";
|
||||||
import type { MyContext } from "../../types/myContext";
|
import type { MyContext } from "../../types/myContext";
|
||||||
import { qrCodePlacements } from "../../models/types";
|
import { qrCodePlacements } from "../../types/models";
|
||||||
import {
|
import {
|
||||||
buildPlacementKeyboard,
|
buildPlacementKeyboard,
|
||||||
buildActionsKeyboard,
|
buildActionsKeyboard,
|
||||||
@@ -11,11 +11,8 @@ import { createQRCode } from "../../api/qrCodes";
|
|||||||
import { generateQRCodeImage } from "../../services/qrGenerator";
|
import { generateQRCodeImage } from "../../services/qrGenerator";
|
||||||
import { selectMultipleActions } from "./selectMultipleActions";
|
import { selectMultipleActions } from "./selectMultipleActions";
|
||||||
|
|
||||||
// Тип разговора (конверсии)
|
|
||||||
type MyConversation = Conversation<MyContext>;
|
|
||||||
|
|
||||||
export async function newQRCodeConversation(
|
export async function newQRCodeConversation(
|
||||||
conversation: MyConversation,
|
conversation: Conversation<MyContext>,
|
||||||
ctx: MyContext,
|
ctx: MyContext,
|
||||||
) {
|
) {
|
||||||
if (!ctx.userDoc) {
|
if (!ctx.userDoc) {
|
||||||
|
|||||||
@@ -1,64 +1,59 @@
|
|||||||
import { Bot, session } from "grammy";
|
import { Bot, session } from "grammy";
|
||||||
import { conversations, createConversation } from "@grammyjs/conversations";
|
import { conversations, createConversation } from "@grammyjs/conversations";
|
||||||
import { startHealthServer } from "@hereconnect/lib-service-health-checkserver";
|
|
||||||
import { MyContext } from "../types/myContext";
|
import { MyContext } from "../types/myContext";
|
||||||
import { env } from "../config/env";
|
import { env } from "../config/env";
|
||||||
import { CouchDBStorageAdapter } from "./storageAdapters/CouchDBStorageAdapter";
|
import { CouchDBStorageAdapter } from "./storageAdapters/CouchDBStorageAdapter";
|
||||||
import { loadUserMiddleware } from "./middleware/loadUser";
|
import { loadUserMiddleware } from "./middleware/loadUser";
|
||||||
import { tgBotSessionsDb } from "../dbs";
|
import { tgBotSessionsDb } from "../dbs";
|
||||||
import { setupCommands } from "./commands/setupCommands";
|
|
||||||
import { startCommand } from "./commands/start";
|
import { startCommand } from "./commands/start";
|
||||||
import { helpCommand } from "./commands/help";
|
import { helpCommand } from "./commands/help";
|
||||||
import { newCommand } from "./commands/new";
|
import { newCommand } from "./commands/new";
|
||||||
import { manageCommand } from "./commands/manage";
|
import { manageCommand } from "./commands/manage";
|
||||||
import { callbackQueryHandler } from "./callbackHandlers";
|
import { callbackQueryHandler } from "./callbackHandlers";
|
||||||
import { newQRCodeConversation } from "./conversations/newQRCodeConversation";
|
import { newQRCodeConversation } from "./conversations/newQRCodeConversation";
|
||||||
|
import { hydrateReply } from "@grammyjs/parse-mode";
|
||||||
|
|
||||||
const abortController = new AbortController();
|
export { setupCommands } from "./commands/setupCommands";
|
||||||
|
|
||||||
const storage = new CouchDBStorageAdapter(tgBotSessionsDb, {
|
export const createBot = ({
|
||||||
softDelete: true,
|
abortController,
|
||||||
type: "tg_bot_session",
|
}: {
|
||||||
});
|
abortController: AbortController;
|
||||||
|
}) => {
|
||||||
|
const storage = new CouchDBStorageAdapter(tgBotSessionsDb, {
|
||||||
|
softDelete: true,
|
||||||
|
type: "tg_bot_session",
|
||||||
|
});
|
||||||
|
|
||||||
// Инициализируем бота с нужным типом контекста
|
// Инициализируем бота с нужным типом контекста
|
||||||
const bot = new Bot<MyContext>(env.TELEGRAM_BOT_TOKEN, {
|
const bot = new Bot<MyContext>(env.TELEGRAM_BOT_TOKEN, {
|
||||||
client: {
|
client: {
|
||||||
baseFetchConfig: {
|
baseFetchConfig: {
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Подключаем сессии. По необходимости определите initial state для сессии
|
bot.use(hydrateReply);
|
||||||
bot.use(session({ storage, initial: () => ({}) }));
|
|
||||||
|
|
||||||
// Подключаем conversations для многошаговых сценариев
|
// Подключаем сессии. По необходимости определите initial state для сессии
|
||||||
bot.use(conversations());
|
bot.use(session({ storage, initial: () => ({}) }));
|
||||||
bot.use(createConversation(newQRCodeConversation, "newQRCodeConversation"));
|
|
||||||
|
|
||||||
// Middleware для загрузки или создания пользователя
|
// Подключаем conversations для многошаговых сценариев
|
||||||
bot.use(loadUserMiddleware);
|
bot.use(conversations());
|
||||||
|
bot.use(createConversation(newQRCodeConversation));
|
||||||
|
|
||||||
// Регистрация команд
|
// Middleware для загрузки или создания пользователя
|
||||||
bot.command("start", startCommand);
|
bot.use(loadUserMiddleware);
|
||||||
bot.command("help", helpCommand);
|
|
||||||
bot.command("new", newCommand);
|
|
||||||
bot.command("manage", manageCommand);
|
|
||||||
|
|
||||||
// Обработка callback_query
|
// Регистрация команд
|
||||||
bot.on("callback_query:data", callbackQueryHandler);
|
bot.command("start", startCommand);
|
||||||
|
bot.command("help", helpCommand);
|
||||||
|
bot.command("new", newCommand);
|
||||||
|
bot.command("manage", manageCommand);
|
||||||
|
|
||||||
(async () => {
|
// Обработка callback_query
|
||||||
// Установка команд
|
bot.on("callback_query:data", callbackQueryHandler);
|
||||||
await setupCommands(bot);
|
|
||||||
|
|
||||||
// Запуск бота
|
return bot;
|
||||||
bot.start();
|
};
|
||||||
|
|
||||||
await startHealthServer(abortController);
|
|
||||||
|
|
||||||
console.log("Бот запущен!");
|
|
||||||
})();
|
|
||||||
|
|
||||||
export { bot };
|
|
||||||
|
|||||||
@@ -10,8 +10,14 @@ if (!process.env.API_URL) {
|
|||||||
throw new Error("Environment variable API_URL is not set");
|
throw new Error("Environment variable API_URL is not set");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!process.env.HEARTBEAT_INTERVAL) {
|
||||||
|
throw new Error("Environment variable HEARTBEAT_INTERVAL is not set");
|
||||||
|
}
|
||||||
|
|
||||||
export const env = {
|
export const env = {
|
||||||
TELEGRAM_BOT_TOKEN: process.env.BOT_TOKEN,
|
TELEGRAM_BOT_TOKEN: process.env.BOT_TOKEN,
|
||||||
READ_BASE_URL: process.env.READ_BASE_URL,
|
READ_BASE_URL: process.env.READ_BASE_URL,
|
||||||
|
APP_BASE_URL: process.env.APP_BASE_URL,
|
||||||
API_URL: process.env.API_URL,
|
API_URL: process.env.API_URL,
|
||||||
|
HEARTBEAT_INTERVAL: parseInt(process.env.HEARTBEAT_INTERVAL, 10),
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
+29
-30
@@ -2,7 +2,6 @@ import PouchDB from "./PouchDB";
|
|||||||
import { ServiceSeqTrackerDatabase } from "@hereconnect/lib-service-seq-tracker";
|
import { ServiceSeqTrackerDatabase } from "@hereconnect/lib-service-seq-tracker";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
MessageDocument,
|
|
||||||
QRCodeDocument,
|
QRCodeDocument,
|
||||||
MessageDeliveryMethodWebPushVapid,
|
MessageDeliveryMethodWebPushVapid,
|
||||||
UserChatSettingsDocument,
|
UserChatSettingsDocument,
|
||||||
@@ -11,25 +10,26 @@ import type {
|
|||||||
UserDocument,
|
UserDocument,
|
||||||
} from "@hereconnect/types";
|
} from "@hereconnect/types";
|
||||||
|
|
||||||
|
import type { ServiceSeqTrackerDocument } from "@hereconnect/lib-service-seq-tracker";
|
||||||
|
|
||||||
import type { TelegramWebhookDesignDocument } from "@hereconnect/tg-bot-webhook-ddoc";
|
import type { TelegramWebhookDesignDocument } from "@hereconnect/tg-bot-webhook-ddoc";
|
||||||
import { CouchDBSessionDocument } from "./bot/storageAdapters/CouchDBStorageAdapter";
|
import { CouchDBSessionDocument } from "./bot/storageAdapters/CouchDBStorageAdapter";
|
||||||
import type { SessionData } from "./types/myContext";
|
import type { SessionData } from "./types/myContext";
|
||||||
import { env } from "./config/env";
|
import { env } from "./config/env";
|
||||||
// import type { TgUpdateDocument } from "./types";
|
|
||||||
|
|
||||||
if (!env.API_URL) {
|
if (!env.API_URL) {
|
||||||
throw new Error("Environment variable API_URL is not set");
|
throw new Error("Environment variable API_URL is not set");
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TG_BOT_DB_NAME = "tg_bot";
|
export const TG_BOT_DB_NAME = "tg_bot";
|
||||||
|
export const MESSAGES_DB_NAME = "messages";
|
||||||
export const TG_BOT_DB_URL = `${env.API_URL}/${TG_BOT_DB_NAME}`;
|
export const TG_BOT_DB_URL = `${env.API_URL}/${TG_BOT_DB_NAME}`;
|
||||||
|
export const MESSAGES_DB_URL = `${env.API_URL}/${MESSAGES_DB_NAME}`;
|
||||||
|
|
||||||
export const messagesDb = new PouchDB<MessageDocument>(
|
export const messagesServiceSeqTrackerDb =
|
||||||
`${env.API_URL}/messages`,
|
new PouchDB<ServiceSeqTrackerDocument>(MESSAGES_DB_URL, {
|
||||||
{
|
|
||||||
skip_setup: true,
|
skip_setup: true,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const serverSettingsDb = new PouchDB<MessageDeliveryMethodWebPushVapid>(
|
export const serverSettingsDb = new PouchDB<MessageDeliveryMethodWebPushVapid>(
|
||||||
`${env.API_URL}/server_settings`,
|
`${env.API_URL}/server_settings`,
|
||||||
@@ -49,18 +49,10 @@ export const qrDb = new PouchDB<QRCodeDocument>(`${env.API_URL}/qr`, {
|
|||||||
skip_setup: true,
|
skip_setup: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const webPushSubscriptionsDb = new PouchDB<
|
|
||||||
WebPushSubscriptionDocument<PushSubscription>
|
|
||||||
>(`${env.API_URL}/web_push_subscriptions`, {
|
|
||||||
skip_setup: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const tgBotDb = new PouchDB(TG_BOT_DB_URL, {
|
export const tgBotDb = new PouchDB(TG_BOT_DB_URL, {
|
||||||
skip_setup: true,
|
skip_setup: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const tgBotDesignDocumentsDb =
|
|
||||||
tgBotDb as PouchDB.Database<TelegramWebhookDesignDocument>;
|
|
||||||
export const serviceSeqTrackerDb = tgBotDb as ServiceSeqTrackerDatabase;
|
export const serviceSeqTrackerDb = tgBotDb as ServiceSeqTrackerDatabase;
|
||||||
|
|
||||||
export const tgBotSessionsDb = new PouchDB<CouchDBSessionDocument<SessionData>>(
|
export const tgBotSessionsDb = new PouchDB<CouchDBSessionDocument<SessionData>>(
|
||||||
@@ -81,20 +73,25 @@ export const usersDb = new PouchDB<UserDocument>(`${env.API_URL}/_users`, {
|
|||||||
skip_setup: true,
|
skip_setup: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const checkDatabases = async () => {
|
const checkDatabases = async () => {
|
||||||
const entries = await Promise.all(
|
const databases = {
|
||||||
Object.entries({
|
serverSettingsDb,
|
||||||
serverSettingsDb,
|
publicSettingsDb,
|
||||||
publicSettingsDb,
|
qrDb,
|
||||||
qrDb,
|
usersDb,
|
||||||
webPushSubscriptionsDb,
|
serviceSeqTrackerDb,
|
||||||
serviceSeqTrackerDb,
|
userChatSettingsDb,
|
||||||
userChatSettingsDb,
|
tgBotDb,
|
||||||
tgBotDb,
|
tgBotSessionsDb,
|
||||||
tgBotSessionsDb,
|
messagesServiceSeqTrackerDb,
|
||||||
} as const).map(
|
} as const;
|
||||||
|
|
||||||
|
const databasesEntries = Object.entries(databases);
|
||||||
|
|
||||||
|
const resultEntries = await Promise.all(
|
||||||
|
databasesEntries.map(
|
||||||
async ([name, db]): Promise<
|
async ([name, db]): Promise<
|
||||||
[string, Awaited<ReturnType<typeof db.info>>]
|
[typeof name, Awaited<ReturnType<typeof db.info>>]
|
||||||
> => {
|
> => {
|
||||||
const info = await db.info();
|
const info = await db.info();
|
||||||
return [name, info];
|
return [name, info];
|
||||||
@@ -102,7 +99,7 @@ export const checkDatabases = async () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const [name, info] of entries) {
|
for (const [name, info] of resultEntries) {
|
||||||
if ("error" in info && "reason" in info) {
|
if ("error" in info && "reason" in info) {
|
||||||
console.error(info);
|
console.error(info);
|
||||||
throw new Error(`[${name}] ${info.error}: ${info.reason}`);
|
throw new Error(`[${name}] ${info.error}: ${info.reason}`);
|
||||||
@@ -112,5 +109,7 @@ export const checkDatabases = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.fromEntries(entries);
|
return Object.fromEntries(resultEntries);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkDatabasesPromise = checkDatabases();
|
||||||
|
|||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import type { Entries, Entry } from "type-fest";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface ObjectConstructor {
|
||||||
|
entries<T extends object>(o: T): Entries<T>;
|
||||||
|
fromEntries<T>(entries: Entry<T>[]): T;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { createHealthServer } from "@hereconnect/lib-service-health-checkserver";
|
||||||
|
import { createBot, setupCommands } from "./bot";
|
||||||
|
import { createServiceChangesMessagesDb } from "./services/serviceChangesMessagesDb";
|
||||||
|
import { iterateMessagesChanges } from "./services/iterateMessagesChanges";
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const healthServer = createHealthServer(abortController);
|
||||||
|
const bot = createBot({ abortController });
|
||||||
|
const { changesStream, serviceSeqTracker } =
|
||||||
|
await createServiceChangesMessagesDb({
|
||||||
|
abortController,
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", async () => {
|
||||||
|
changesStream.stop();
|
||||||
|
bot.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
await healthServer.start();
|
||||||
|
|
||||||
|
await bot.init();
|
||||||
|
// Устанавливаем команд
|
||||||
|
await setupCommands(bot);
|
||||||
|
|
||||||
|
bot.start();
|
||||||
|
console.log("Бот запущен!");
|
||||||
|
|
||||||
|
iterateMessagesChanges({ changesStream, serviceSeqTracker, bot });
|
||||||
|
})();
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import type { MyContext } from "../types/myContext";
|
||||||
|
import { Bot, Context, Keyboard } from "grammy";
|
||||||
|
import type { ServiceSeqTracker } from "@hereconnect/lib-service-seq-tracker";
|
||||||
|
import type { CouchDBChangesStream } from "@hereconnect/couchdb-changes-stream";
|
||||||
|
import type { MessageDocument, UserDocument } from "@hereconnect/types";
|
||||||
|
import { qrDb, userChatSettingsDb, usersDb } from "../dbs";
|
||||||
|
// import { blockquote, fmt, italic, link } from "@grammyjs/parse-mode";
|
||||||
|
import { env } from "../config/env";
|
||||||
|
import { escapeHtml } from "../utils/escapeHtml";
|
||||||
|
|
||||||
|
async function handleChange(msgDoc: MessageDocument, bot: Bot<MyContext>) {
|
||||||
|
const qrDoc = await qrDb.get(`qr_code:${msgDoc.qr_code_uri}`);
|
||||||
|
if (qrDoc.messageDeliveryMethod !== "telegram") {
|
||||||
|
console.log(`Skip !telegram`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = ["telegram_id"] as const;
|
||||||
|
const [{ docs: telegramUsersDocs }, { rows: userChatSettingsRows }] =
|
||||||
|
await Promise.all([
|
||||||
|
usersDb.find({
|
||||||
|
selector: {
|
||||||
|
user_uuid: qrDoc.user_uuid,
|
||||||
|
telegram_id: {
|
||||||
|
$exists: true,
|
||||||
|
$ne: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fields: [...fields],
|
||||||
|
}) as Promise<{
|
||||||
|
docs: Required<Pick<UserDocument, (typeof fields)[number]>>[];
|
||||||
|
}>,
|
||||||
|
|
||||||
|
userChatSettingsDb.allDocs({
|
||||||
|
startkey: `user_chat_settings:${msgDoc.qr_code_uri}:${msgDoc.chat}:owner:`,
|
||||||
|
endkey: `user_chat_settings:${msgDoc.qr_code_uri}:${msgDoc.chat}:owner:\uffff`,
|
||||||
|
include_docs: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
userChatSettingsRows.some(
|
||||||
|
(row) => row.doc?.is_telegram_messages_enabled === false,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
console.log("telegram notifications is disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (telegramUsersDocs.length === 0) {
|
||||||
|
console.log("NO TELEGRAM USERS");
|
||||||
|
} else {
|
||||||
|
console.log(`sending to ${telegramUsersDocs.length} telegram accounts`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashTag = `#qr_${qrDoc.uri}_c${msgDoc.chat}`;
|
||||||
|
const manageMessageUrl = `${env.APP_BASE_URL}/manage/qr/${qrDoc.uri}/chat/${msgDoc.chat}#message-${encodeURIComponent(msgDoc.created_at)}`;
|
||||||
|
// const manageMessageUrl = `tg://resolve?domain=${bot.botInfo.username}&start=message:${qrDoc.uri}:${msgDoc.created_at}`;
|
||||||
|
// const msgCaption = ` — ${link("сообщение", manageMessageUrl)} QR-кода «${qrDoc.name}»`;
|
||||||
|
|
||||||
|
// const caption = fmt`${link("сообщение", manageMessageUrl)} QR-кода «${qrDoc.name}»`;
|
||||||
|
// const body = fmt`${msgDoc.body.trim()} — ${italic(caption)}`;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
text: `${escapeHtml(msgDoc.body.trim())} — <i><a href="${escapeHtml(manageMessageUrl)}">сообщение</a> ${escapeHtml(hashTag)} QR-кода <b>«${escapeHtml(qrDoc.name)}»</b></i>`,
|
||||||
|
entities: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const telegramUserDoc of telegramUsersDocs) {
|
||||||
|
await bot.api.sendMessage(telegramUserDoc.telegram_id, body.text, {
|
||||||
|
parse_mode: "HTML",
|
||||||
|
// parse_mode: "MarkdownV2",
|
||||||
|
link_preview_options: { is_disabled: true },
|
||||||
|
entities: body.entities,
|
||||||
|
reply_markup: { force_reply: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const iterateMessagesChanges = async ({
|
||||||
|
changesStream,
|
||||||
|
serviceSeqTracker,
|
||||||
|
bot,
|
||||||
|
}: {
|
||||||
|
changesStream: CouchDBChangesStream<MessageDocument>;
|
||||||
|
serviceSeqTracker: ServiceSeqTracker;
|
||||||
|
bot: Bot<MyContext>;
|
||||||
|
}) => {
|
||||||
|
for await (const change of changesStream) {
|
||||||
|
const { id, seq, doc: msgDoc, changes } = change;
|
||||||
|
|
||||||
|
console.log("seq", seq);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!msgDoc) {
|
||||||
|
console.error("Change missing document:", change);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceSeqTracker.checkNextSeq(seq);
|
||||||
|
|
||||||
|
await handleChange(msgDoc, bot);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed processing: ${JSON.stringify(error)} change: ${JSON.stringify({ id, rev: changes.map((c) => c.rev).join(","), seq: seq })}`,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await serviceSeqTracker.saveSeq(seq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { ServiceSeqTracker } from "@hereconnect/lib-service-seq-tracker";
|
||||||
|
import { CouchDBChangesStream } from "@hereconnect/couchdb-changes-stream";
|
||||||
|
import type { MessageDocument } from "@hereconnect/types";
|
||||||
|
import {
|
||||||
|
MESSAGES_DB_URL,
|
||||||
|
messagesServiceSeqTrackerDb,
|
||||||
|
checkDatabasesPromise,
|
||||||
|
} from "../dbs";
|
||||||
|
import { env } from "../config/env";
|
||||||
|
|
||||||
|
export const createServiceChangesMessagesDb = async ({
|
||||||
|
abortController,
|
||||||
|
}: {
|
||||||
|
abortController: AbortController;
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
messagesServiceSeqTrackerDb: { update_seq: lastSeq },
|
||||||
|
} = await checkDatabasesPromise;
|
||||||
|
|
||||||
|
const serviceSeqTracker = await ServiceSeqTracker.Init(
|
||||||
|
"_local/service:tg-bot",
|
||||||
|
messagesServiceSeqTrackerDb,
|
||||||
|
);
|
||||||
|
|
||||||
|
const changesStream = new CouchDBChangesStream<MessageDocument>(
|
||||||
|
MESSAGES_DB_URL,
|
||||||
|
{
|
||||||
|
abortController,
|
||||||
|
since: serviceSeqTracker.lastSeq,
|
||||||
|
feed: "continuous",
|
||||||
|
include_docs: true,
|
||||||
|
heartbeat: env.HEARTBEAT_INTERVAL,
|
||||||
|
filter: "_selector",
|
||||||
|
selector: {
|
||||||
|
type: "message",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.info("Service started from sequence", serviceSeqTracker.lastSeq);
|
||||||
|
|
||||||
|
if (
|
||||||
|
parseInt(serviceSeqTracker.lastSeq.toString(), 10) ===
|
||||||
|
parseInt(lastSeq.toString(), 10)
|
||||||
|
) {
|
||||||
|
console.log("Since is the last seq");
|
||||||
|
} else {
|
||||||
|
console.log("Last seq is", lastSeq);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { changesStream, serviceSeqTracker };
|
||||||
|
};
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
// src/types/myContext.ts
|
// src/types/myContext.ts
|
||||||
|
|
||||||
import { Context, SessionFlavor } from "grammy";
|
import type { Context, SessionFlavor } from "grammy";
|
||||||
import { ConversationFlavor } from "@grammyjs/conversations";
|
import type { ConversationFlavor, Conversation } from "@grammyjs/conversations";
|
||||||
|
import type { ParseModeFlavor } from "@grammyjs/parse-mode";
|
||||||
import type { UserDocument } from "@hereconnect/types";
|
import type { UserDocument } from "@hereconnect/types";
|
||||||
|
|
||||||
export interface SessionData {
|
export interface SessionData {
|
||||||
@@ -9,11 +10,11 @@ export interface SessionData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Базовый контекст без conversation
|
// Базовый контекст без conversation
|
||||||
interface MyContextBase extends Context, SessionFlavor<SessionData> {
|
interface MyContextBase {
|
||||||
userDoc?: UserDocument;
|
userDoc?: UserDocument;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Теперь создаём финальный тип контекста, дополняя его ConversationFlavor
|
export type MyContext = ParseModeFlavor<Context> &
|
||||||
type MyContext = MyContextBase & ConversationFlavor<MyContextBase>;
|
SessionFlavor<SessionData> &
|
||||||
|
ConversationFlavor &
|
||||||
export { MyContext };
|
MyContextBase;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Экранирует HTML-код, заменяя символы "<", ">", "&" на их соответствующие entity-кодировки.
|
||||||
|
*
|
||||||
|
* @param text
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Экранирует HTML-код, заменяя символы "<", ">", "&", '"' и "'" на их соответствующие entity-кодировки.
|
||||||
|
*
|
||||||
|
* @param text
|
||||||
|
*/
|
||||||
|
export function escapeHtml(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
@@ -11,6 +11,6 @@
|
|||||||
"declaration": true, // Генерация `.d.ts` файлов
|
"declaration": true, // Генерация `.d.ts` файлов
|
||||||
"sourceMap": true, // Генерация `.map` файлов
|
"sourceMap": true, // Генерация `.map` файлов
|
||||||
},
|
},
|
||||||
"include": ["src"], // Указываем папку с исходниками
|
"include": ["globals.d.ts", "."], // Указываем папку с исходниками
|
||||||
"exclude": ["node_modules", "dist", "old"] // Исключаем ненужные директории
|
"exclude": ["node_modules", "dist", "old"], // Исключаем ненужные директории
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user