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:
@@ -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,14 +73,14 @@ const start = async () => {
|
||||
for await (const change of changesStream) {
|
||||
const { seq, doc: msgDoc } = change;
|
||||
|
||||
if (!msgDoc) {
|
||||
console.error("Change missing document:", change);
|
||||
continue;
|
||||
}
|
||||
|
||||
serviceSeqTracker.checkNextSeq(seq);
|
||||
|
||||
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 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"] // Исключаем ненужные директории
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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, {
|
||||
softDelete: true,
|
||||
type: "tg_bot_session",
|
||||
});
|
||||
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, {
|
||||
client: {
|
||||
baseFetchConfig: {
|
||||
signal: abortController.signal,
|
||||
// Инициализируем бота с нужным типом контекста
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
+29
-30
@@ -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({
|
||||
serverSettingsDb,
|
||||
publicSettingsDb,
|
||||
qrDb,
|
||||
webPushSubscriptionsDb,
|
||||
serviceSeqTrackerDb,
|
||||
userChatSettingsDb,
|
||||
tgBotDb,
|
||||
tgBotSessionsDb,
|
||||
} as const).map(
|
||||
const checkDatabases = async () => {
|
||||
const databases = {
|
||||
serverSettingsDb,
|
||||
publicSettingsDb,
|
||||
qrDb,
|
||||
usersDb,
|
||||
serviceSeqTrackerDb,
|
||||
userChatSettingsDb,
|
||||
tgBotDb,
|
||||
tgBotSessionsDb,
|
||||
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();
|
||||
|
||||
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
|
||||
|
||||
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;
|
||||
|
||||
@@ -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` файлов
|
||||
"sourceMap": true, // Генерация `.map` файлов
|
||||
},
|
||||
"include": ["src"], // Указываем папку с исходниками
|
||||
"exclude": ["node_modules", "dist", "old"] // Исключаем ненужные директории
|
||||
"include": ["globals.d.ts", "."], // Указываем папку с исходниками
|
||||
"exclude": ["node_modules", "dist", "old"], // Исключаем ненужные директории
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user