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

This commit is contained in:
2024-12-14 23:58:29 +02:00
parent 90fe585ff8
commit d71b405277
22 changed files with 396 additions and 127 deletions
@@ -21,6 +21,8 @@ jobs:
COUCHDB_PASSWORD: ${{ secrets.COUCHDB_PASSWORD }}
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
READ_BASE_URL: ${{ vars.READ_BASE_URL }}
APP_BASE_URL: ${{ vars.APP_BASE_URL }}
HEARTBEAT_INTERVAL: ${{ vars.HEARTBEAT_INTERVAL }}
steps:
- uses: actions/checkout@v4
@@ -3,6 +3,7 @@
<div
v-for="message in messages"
:key="message._id"
:id="`message-${message.created_at}`"
class="mb-4 max-w-md"
:class="{
'ml-auto text-right': message.from === userRole,
@@ -6,7 +6,7 @@ import http from "http";
* @param defaultPort - Порт для сервера по умолчанию (если не указан в ENV HEALTH_PORT).
* @param defaultHostName - Имя хоста для сервера по умолчанию (если не указан в ENV HEALTH_HOST).
*/
export function startHealthServer(
export function createHealthServer(
controller: AbortController,
defaultPort: number = 8000,
defaultHostName: string = "127.0.0.1",
@@ -43,6 +43,18 @@ export function startHealthServer(
}
});
server?.unref?.();
const stop = () => {
return new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) reject(error);
else resolve();
});
});
};
const start = () => {
return new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(port, hostname, () => {
@@ -55,8 +67,21 @@ export function startHealthServer(
// Закрываем сервер при активации AbortController
controller.signal.addEventListener("abort", () => {
console.log("Healthcheck server shutting down due to abort signal.");
reject();
server.close();
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;
/** Включены ли push-уведомления */
is_push_notification_enabled: boolean;
is_push_notification_enabled?: boolean;
/**
* Включены ли сообщения в Telegram
*/
is_telegram_messages_enabled?: boolean;
/** Дата последнего обновления */
updated_at?: string;
+22
View File
@@ -337,6 +337,9 @@ importers:
'@grammyjs/conversations':
specifier: ^1.2.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':
specifier: workspace:^
version: link:../../packages/constants
@@ -425,6 +428,9 @@ importers:
tsx:
specifier: ^4.19.2
version: 4.19.2
type-fest:
specifier: ^4.30.1
version: 4.30.1
typescript:
specifier: ^5.7.2
version: 5.7.2
@@ -921,6 +927,12 @@ packages:
peerDependencies:
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':
resolution: {integrity: sha512-e8AR3xQwRAFX248E7Qw/7mIu1OzvoXloJzOBJVtuPKzzL7tGkn5trZAdZUBgGViVQg5ZwVS/x9N2nRrcyH/DfA==}
@@ -3637,6 +3649,10 @@ packages:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'}
type-fest@4.30.1:
resolution: {integrity: sha512-ojFL7eDMX2NF0xMbDwPZJ8sb7ckqtlAi1GsmgsFXvErT9kFTk1r0DuQKvrCh73M6D4nngeHJmvogF9OluXs7Hw==}
engines: {node: '>=16'}
typescript-eslint@8.16.0:
resolution: {integrity: sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -4426,6 +4442,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@grammyjs/parse-mode@1.10.0(grammy@1.33.0)':
dependencies:
grammy: 1.33.0
'@grammyjs/types@3.17.0': {}
'@humanfs/core@0.19.1': {}
@@ -7380,6 +7400,8 @@ snapshots:
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):
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)
@@ -10,11 +10,6 @@
"node": ">=v20.10.0",
"pnpm": ">=8.15.0"
},
"homepage": "",
"repository": {
"type": "git",
"url": ""
},
"files": [
"dist/**/*"
],
@@ -62,7 +62,7 @@ export const userChatSettingsDb = new PouchDB<UserChatSettingsDocument>(
},
);
export const checkDatabases = async () => {
const checkDatabases = async () => {
const entries = await Promise.all(
Object.entries({
serverSettingsDb,
@@ -92,3 +92,5 @@ export const checkDatabases = async () => {
return Object.fromEntries(entries);
};
export const checkDatabasesPromise = checkDatabases();
@@ -9,7 +9,7 @@ import {
messagesDbUrl,
webPushSubscriptionsDb,
userChatSettingsDb,
checkDatabases,
checkDatabasesPromise,
serviceSeqTrackerDb,
} from "./dbs";
import { setupVapidDetails } from "./setupVapidDetails";
@@ -29,7 +29,7 @@ function getTopicSafe(str: string) {
}
const start = async () => {
const checkedDatabases = await checkDatabases();
const checkedDatabases = await checkDatabasesPromise;
await setupVapidDetails();
const serviceSeqTracker = await ServiceSeqTracker.Init(
@@ -47,10 +47,7 @@ const start = async () => {
heartbeat,
filter: "_selector",
selector: {
$or: [
{ type: "message" },
{ type: "service", service: "message-delivery-method-web-push" },
],
type: "message",
},
},
);
@@ -76,6 +73,7 @@ const start = async () => {
for await (const change of changesStream) {
const { seq, doc: msgDoc } = change;
try {
if (!msgDoc) {
console.error("Change missing document:", change);
continue;
@@ -83,7 +81,6 @@ const start = async () => {
serviceSeqTracker.checkNextSeq(seq);
try {
const qrDoc = await qrDb.get(`qr_code:${msgDoc.qr_code_uri}`);
const timestamp = new Date(msgDoc.created_at).getTime();
@@ -264,7 +261,7 @@ const start = async () => {
);
} catch (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 {
await serviceSeqTracker.saveSeq(seq);
@@ -11,9 +11,6 @@
"declaration": true, // Генерация `.d.ts` файлов
"sourceMap": true, // Генерация `.map` файлов
"baseUrl": "./", // Базовая директория для путей
"paths": {
"*": ["src/*"] // Настройка алиасов (сопоставление путей)
}
},
"include": ["src"], // Указываем папку с исходниками
"exclude": ["node_modules", "dist"] // Исключаем ненужные директории
+6 -4
View File
@@ -2,7 +2,7 @@
"name": "@hereconnect/service-tg-bot",
"version": "1.0.0",
"description": "",
"main": "dist/server.ts",
"main": "dist/service.js",
"type": "module",
"license": "@hereconnect/license",
"private": true,
@@ -29,17 +29,18 @@
"contributors": [],
"scripts": {
"build": "tsc",
"dev": "tsx src/bot/index.ts",
"dev:watch": "tsx --watch src/bot/index.ts",
"dev": "tsx src/service.ts",
"dev:watch": "tsx --watch src/service.ts",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"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": {
"@grammyjs/conversations": "^1.2.0",
"@grammyjs/parse-mode": "^1.10.0",
"@hereconnect/constants": "workspace:^",
"@hereconnect/couchdb-changes-stream": "workspace:^",
"@hereconnect/lib-service-health-checkserver": "workspace:^",
@@ -71,6 +72,7 @@
"@typescript-eslint/parser": "^8.18.0",
"eslint": "^9.16.0",
"tsx": "^4.19.2",
"type-fest": "^4.30.1",
"typescript": "^5.7.2"
}
}
@@ -2,7 +2,7 @@ import { type QRCodeDocument } from "@hereconnect/types";
import { Conversation } from "@grammyjs/conversations";
import { InputFile } from "grammy";
import type { MyContext } from "../../types/myContext";
import { qrCodePlacements } from "../../models/types";
import { qrCodePlacements } from "../../types/models";
import {
buildPlacementKeyboard,
buildActionsKeyboard,
@@ -11,11 +11,8 @@ import { createQRCode } from "../../api/qrCodes";
import { generateQRCodeImage } from "../../services/qrGenerator";
import { selectMultipleActions } from "./selectMultipleActions";
// Тип разговора (конверсии)
type MyConversation = Conversation<MyContext>;
export async function newQRCodeConversation(
conversation: MyConversation,
conversation: Conversation<MyContext>,
ctx: MyContext,
) {
if (!ctx.userDoc) {
+29 -34
View File
@@ -1,64 +1,59 @@
import { Bot, session } from "grammy";
import { conversations, createConversation } from "@grammyjs/conversations";
import { startHealthServer } from "@hereconnect/lib-service-health-checkserver";
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";
import { newCommand } from "./commands/new";
import { manageCommand } from "./commands/manage";
import { callbackQueryHandler } from "./callbackHandlers";
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 = ({
abortController,
}: {
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: {
baseFetchConfig: {
signal: abortController.signal,
},
},
});
});
// Подключаем сессии. По необходимости определите initial state для сессии
bot.use(session({ storage, initial: () => ({}) }));
bot.use(hydrateReply);
// Подключаем conversations для многошаговых сценариев
bot.use(conversations());
bot.use(createConversation(newQRCodeConversation, "newQRCodeConversation"));
// Подключаем сессии. По необходимости определите initial state для сессии
bot.use(session({ storage, initial: () => ({}) }));
// Middleware для загрузки или создания пользователя
bot.use(loadUserMiddleware);
// Подключаем conversations для многошаговых сценариев
bot.use(conversations());
bot.use(createConversation(newQRCodeConversation));
// Регистрация команд
bot.command("start", startCommand);
bot.command("help", helpCommand);
bot.command("new", newCommand);
bot.command("manage", manageCommand);
// Middleware для загрузки или создания пользователя
bot.use(loadUserMiddleware);
// Обработка 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 () => {
// Установка команд
await setupCommands(bot);
// Обработка callback_query
bot.on("callback_query:data", callbackQueryHandler);
// Запуск бота
bot.start();
await startHealthServer(abortController);
console.log("Бот запущен!");
})();
export { bot };
return bot;
};
+6
View File
@@ -10,8 +10,14 @@ if (!process.env.API_URL) {
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 = {
TELEGRAM_BOT_TOKEN: process.env.BOT_TOKEN,
READ_BASE_URL: process.env.READ_BASE_URL,
APP_BASE_URL: process.env.APP_BASE_URL,
API_URL: process.env.API_URL,
HEARTBEAT_INTERVAL: parseInt(process.env.HEARTBEAT_INTERVAL, 10),
} as const;
+22 -23
View File
@@ -2,7 +2,6 @@ import PouchDB from "./PouchDB";
import { ServiceSeqTrackerDatabase } from "@hereconnect/lib-service-seq-tracker";
import type {
MessageDocument,
QRCodeDocument,
MessageDeliveryMethodWebPushVapid,
UserChatSettingsDocument,
@@ -11,25 +10,26 @@ import type {
UserDocument,
} from "@hereconnect/types";
import type { ServiceSeqTrackerDocument } from "@hereconnect/lib-service-seq-tracker";
import type { TelegramWebhookDesignDocument } from "@hereconnect/tg-bot-webhook-ddoc";
import { CouchDBSessionDocument } from "./bot/storageAdapters/CouchDBStorageAdapter";
import type { SessionData } from "./types/myContext";
import { env } from "./config/env";
// import type { TgUpdateDocument } from "./types";
if (!env.API_URL) {
throw new Error("Environment variable API_URL is not set");
}
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 MESSAGES_DB_URL = `${env.API_URL}/${MESSAGES_DB_NAME}`;
export const messagesDb = new PouchDB<MessageDocument>(
`${env.API_URL}/messages`,
{
export const messagesServiceSeqTrackerDb =
new PouchDB<ServiceSeqTrackerDocument>(MESSAGES_DB_URL, {
skip_setup: true,
},
);
});
export const serverSettingsDb = new PouchDB<MessageDeliveryMethodWebPushVapid>(
`${env.API_URL}/server_settings`,
@@ -49,18 +49,10 @@ export const qrDb = new PouchDB<QRCodeDocument>(`${env.API_URL}/qr`, {
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, {
skip_setup: true,
});
export const tgBotDesignDocumentsDb =
tgBotDb as PouchDB.Database<TelegramWebhookDesignDocument>;
export const serviceSeqTrackerDb = tgBotDb as ServiceSeqTrackerDatabase;
export const tgBotSessionsDb = new PouchDB<CouchDBSessionDocument<SessionData>>(
@@ -81,20 +73,25 @@ export const usersDb = new PouchDB<UserDocument>(`${env.API_URL}/_users`, {
skip_setup: true,
});
export const checkDatabases = async () => {
const entries = await Promise.all(
Object.entries({
const checkDatabases = async () => {
const databases = {
serverSettingsDb,
publicSettingsDb,
qrDb,
webPushSubscriptionsDb,
usersDb,
serviceSeqTrackerDb,
userChatSettingsDb,
tgBotDb,
tgBotSessionsDb,
} as const).map(
messagesServiceSeqTrackerDb,
} as const;
const databasesEntries = Object.entries(databases);
const resultEntries = await Promise.all(
databasesEntries.map(
async ([name, db]): Promise<
[string, Awaited<ReturnType<typeof db.info>>]
[typeof name, Awaited<ReturnType<typeof db.info>>]
> => {
const info = await db.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) {
console.error(info);
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();
+8
View File
@@ -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;
}
}
+31
View File
@@ -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 };
};
+8 -7
View File
@@ -1,7 +1,8 @@
// src/types/myContext.ts
import { Context, SessionFlavor } from "grammy";
import { ConversationFlavor } from "@grammyjs/conversations";
import type { Context, SessionFlavor } from "grammy";
import type { ConversationFlavor, Conversation } from "@grammyjs/conversations";
import type { ParseModeFlavor } from "@grammyjs/parse-mode";
import type { UserDocument } from "@hereconnect/types";
export interface SessionData {
@@ -9,11 +10,11 @@ export interface SessionData {
}
// Базовый контекст без conversation
interface MyContextBase extends Context, SessionFlavor<SessionData> {
interface MyContextBase {
userDoc?: UserDocument;
}
// Теперь создаём финальный тип контекста, дополняя его ConversationFlavor
type MyContext = MyContextBase & ConversationFlavor<MyContextBase>;
export { MyContext };
export type MyContext = ParseModeFlavor<Context> &
SessionFlavor<SessionData> &
ConversationFlavor &
MyContextBase;
+21
View File
@@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
+2 -2
View File
@@ -11,6 +11,6 @@
"declaration": true, // Генерация `.d.ts` файлов
"sourceMap": true, // Генерация `.map` файлов
},
"include": ["src"], // Указываем папку с исходниками
"exclude": ["node_modules", "dist", "old"] // Исключаем ненужные директории
"include": ["globals.d.ts", "."], // Указываем папку с исходниками
"exclude": ["node_modules", "dist", "old"], // Исключаем ненужные директории
}