read tg_bot_updates from db
Deploy app frontend / app frontend (push) Successful in 2m34s
Deploy db-migrations / db-migrations (push) Successful in 1m12s
Deploy service message-delivery-method-web-push / service message-delivery-method-web-push (push) Successful in 1m38s
Deploy service tg-bot / service tg-bot (push) Failing after 2m11s
Deploy app frontend / app frontend (push) Successful in 2m34s
Deploy db-migrations / db-migrations (push) Successful in 1m12s
Deploy service message-delivery-method-web-push / service message-delivery-method-web-push (push) Successful in 1m38s
Deploy service tg-bot / service tg-bot (push) Failing after 2m11s
This commit is contained in:
Vendored
+1
@@ -1 +1,2 @@
|
|||||||
export * from "./updates/saveUpdate";
|
export * from "./updates/saveUpdate";
|
||||||
|
export type * from "./types";
|
||||||
Vendored
@@ -0,0 +1,23 @@
|
|||||||
|
import type { BaseDocument } from "@hereconnect/types";
|
||||||
|
export interface TgBotUpdateWebhookDesignDocument extends BaseDocument {
|
||||||
|
_id: "_design/tg_bot_webhook";
|
||||||
|
type: "_design/tg_bot_webhook";
|
||||||
|
updated_at: BaseDocument["created_at"];
|
||||||
|
updates: {
|
||||||
|
saveUpdate: string;
|
||||||
|
};
|
||||||
|
constants: {
|
||||||
|
telegram_secret_token: string;
|
||||||
|
telegram_webhook_url?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export interface TgBotUpdateDocument extends BaseDocument {
|
||||||
|
/**
|
||||||
|
* Тип документа для идентификации (фиксированное значение "tg_bot_update")
|
||||||
|
*/
|
||||||
|
type: "tg_bot_update";
|
||||||
|
/**
|
||||||
|
* Данные обновления Telegram
|
||||||
|
*/
|
||||||
|
data: unknown;
|
||||||
|
}
|
||||||
Vendored
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
import { TelegramWebhookDesignDocument, TgUpdateDocument } from "../types";
|
import { TgBotUpdateWebhookDesignDocument, TgBotUpdateDocument } from "../types";
|
||||||
export type { TelegramWebhookDesignDocument };
|
export type { TgBotUpdateWebhookDesignDocument };
|
||||||
type CouchRequest = {
|
type CouchRequest = {
|
||||||
body: string;
|
body: string;
|
||||||
headers: Record<string, string>;
|
headers: Record<string, string>;
|
||||||
@@ -11,12 +11,12 @@ type CouchRequest = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
type CouchResponse = [
|
type CouchResponse = [
|
||||||
TgUpdateDocument | null,
|
TgBotUpdateDocument | null,
|
||||||
{
|
{
|
||||||
code: number;
|
code: number;
|
||||||
body: string;
|
body: string;
|
||||||
} | string
|
} | string
|
||||||
];
|
];
|
||||||
type UpdateFunction = (doc: TgUpdateDocument | null, req: CouchRequest) => CouchResponse;
|
type UpdateFunction = (doc: TgBotUpdateDocument | null, req: CouchRequest) => CouchResponse;
|
||||||
export declare const saveUpdate: UpdateFunction;
|
export declare const saveUpdate: UpdateFunction;
|
||||||
export declare const saveUpdateString: string;
|
export declare const saveUpdateString: string;
|
||||||
+2
-2
@@ -11,8 +11,8 @@ var saveUpdate = function (doc, req) {
|
|||||||
return [null, { code: 304, body: "Update already exists" }];
|
return [null, { code: 304, body: "Update already exists" }];
|
||||||
}
|
}
|
||||||
doc = {
|
doc = {
|
||||||
_id: "tg_update:" + data.update_id.toString(),
|
_id: "tg_bot_update:" + data.update_id.toString(),
|
||||||
type: "tg_update",
|
type: "tg_bot_update",
|
||||||
data: data,
|
data: data,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"saveUpdate.js","sourceRoot":"","sources":["../../src/updates/saveUpdate.ts"],"names":[],"mappings":";;;AAyBO,IAAM,UAAU,GAAmB,UAExC,GAA4B,EAC5B,GAAiB;IAEjB,IAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,iCAAiC,CAAC,CAAC;IAC7D,IAAI,KAAK,KAAK,IAAI,CAAC,SAAS,CAAC,qBAAqB,EAAE,CAAC;QACnD,OAAO,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,0BAA0B,EAAE,CAAC,CAAC;IACjE,CAAC;IAED,IAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAElC,IAAI,GAAG,EAAE,CAAC;QACR,OAAO,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC,CAAC;IAC9D,CAAC;IAED,GAAG,GAAG;QACJ,GAAG,EAAE,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE;QAC7C,IAAI,EAAE,WAAW;QACjB,IAAI,EAAE,IAAI;QACV,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACrC,CAAC;IAEF,OAAO,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;AAChC,CAAC,CAAC;AAxBW,QAAA,UAAU,cAwBrB;AAEW,QAAA,gBAAgB,GAAG,kBAAU,CAAC,QAAQ,EAAE,CAAC"}
|
{"version":3,"file":"saveUpdate.js","sourceRoot":"","sources":["../../src/updates/saveUpdate.ts"],"names":[],"mappings":";;;AA4BO,IAAM,UAAU,GAAmB,UAExC,GAA+B,EAC/B,GAAiB;IAEjB,IAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,iCAAiC,CAAC,CAAC;IAC7D,IAAI,KAAK,KAAK,IAAI,CAAC,SAAS,CAAC,qBAAqB,EAAE,CAAC;QACnD,OAAO,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,0BAA0B,EAAE,CAAC,CAAC;IACjE,CAAC;IAED,IAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAElC,IAAI,GAAG,EAAE,CAAC;QACR,OAAO,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC,CAAC;IAC9D,CAAC;IAED,GAAG,GAAG;QACJ,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE;QACjD,IAAI,EAAE,eAAe;QACrB,IAAI,EAAE,IAAI;QACV,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACrC,CAAC;IAEF,OAAO,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;AAChC,CAAC,CAAC;AAxBW,QAAA,UAAU,cAwBrB;AAEW,QAAA,gBAAgB,GAAG,kBAAU,CAAC,QAAQ,EAAE,CAAC"}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@hereconnect/tg-bot-webhook-ddoc",
|
"name": "@hereconnect/tg-bot-update-webhook-ddoc",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"license": "@hereconnect/license",
|
"license": "@hereconnect/license",
|
||||||
"private": true,
|
"private": true,
|
||||||
+1
@@ -1 +1,2 @@
|
|||||||
export * from "./updates/saveUpdate";
|
export * from "./updates/saveUpdate";
|
||||||
|
export type * from "./types";
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { BaseDocument } from "@hereconnect/types";
|
||||||
|
|
||||||
|
export interface TgBotUpdateWebhookDesignDocument extends BaseDocument {
|
||||||
|
_id: "_design/tg_bot_webhook";
|
||||||
|
type: "_design/tg_bot_webhook";
|
||||||
|
updated_at: BaseDocument["created_at"];
|
||||||
|
|
||||||
|
updates: {
|
||||||
|
saveUpdate: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
constants: {
|
||||||
|
telegram_secret_token: string;
|
||||||
|
telegram_webhook_url?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TgBotUpdateDocument extends BaseDocument {
|
||||||
|
/**
|
||||||
|
* Тип документа для идентификации (фиксированное значение "tg_bot_update")
|
||||||
|
*/
|
||||||
|
type: "tg_bot_update";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Данные обновления Telegram
|
||||||
|
*/
|
||||||
|
data: unknown;
|
||||||
|
}
|
||||||
+11
-8
@@ -1,6 +1,9 @@
|
|||||||
import { TelegramWebhookDesignDocument, TgUpdateDocument } from "../types";
|
import {
|
||||||
|
TgBotUpdateWebhookDesignDocument,
|
||||||
|
TgBotUpdateDocument,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
export type { TelegramWebhookDesignDocument };
|
export type { TgBotUpdateWebhookDesignDocument };
|
||||||
|
|
||||||
type CouchRequest = {
|
type CouchRequest = {
|
||||||
body: string; // JSON-строка
|
body: string; // JSON-строка
|
||||||
@@ -14,18 +17,18 @@ type CouchRequest = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type CouchResponse = [
|
type CouchResponse = [
|
||||||
TgUpdateDocument | null,
|
TgBotUpdateDocument | null,
|
||||||
{ code: number; body: string } | string,
|
{ code: number; body: string } | string,
|
||||||
];
|
];
|
||||||
|
|
||||||
type UpdateFunction = (
|
type UpdateFunction = (
|
||||||
doc: TgUpdateDocument | null,
|
doc: TgBotUpdateDocument | null,
|
||||||
req: CouchRequest,
|
req: CouchRequest,
|
||||||
) => CouchResponse;
|
) => CouchResponse;
|
||||||
|
|
||||||
export const saveUpdate: UpdateFunction = function (
|
export const saveUpdate: UpdateFunction = function (
|
||||||
this: TelegramWebhookDesignDocument,
|
this: TgBotUpdateWebhookDesignDocument,
|
||||||
doc: TgUpdateDocument | null,
|
doc: TgBotUpdateDocument | null,
|
||||||
req: CouchRequest,
|
req: CouchRequest,
|
||||||
): CouchResponse {
|
): CouchResponse {
|
||||||
const token = req.headers["X-Telegram-Bot-Api-Secret-Token"];
|
const token = req.headers["X-Telegram-Bot-Api-Secret-Token"];
|
||||||
@@ -40,8 +43,8 @@ export const saveUpdate: UpdateFunction = function (
|
|||||||
}
|
}
|
||||||
|
|
||||||
doc = {
|
doc = {
|
||||||
_id: "tg_update:" + data.update_id.toString(),
|
_id: "tg_bot_update:" + data.update_id.toString(),
|
||||||
type: "tg_update",
|
type: "tg_bot_update",
|
||||||
data: data,
|
data: data,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
import type { BaseDocument } from "@hereconnect/types";
|
|
||||||
export interface TelegramWebhookDesignDocument extends BaseDocument {
|
|
||||||
_id: "_design/webhook";
|
|
||||||
type: "_design/webhook";
|
|
||||||
updated_at: BaseDocument["created_at"];
|
|
||||||
updates: {
|
|
||||||
saveUpdate: string;
|
|
||||||
};
|
|
||||||
constants: {
|
|
||||||
telegram_secret_token: string;
|
|
||||||
telegram_webhook_url?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export interface TgUpdateDocument extends BaseDocument {
|
|
||||||
data: unknown;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import type { BaseDocument } from "@hereconnect/types";
|
|
||||||
|
|
||||||
export interface TelegramWebhookDesignDocument extends BaseDocument {
|
|
||||||
_id: "_design/webhook";
|
|
||||||
type: "_design/webhook";
|
|
||||||
updated_at: BaseDocument["created_at"];
|
|
||||||
|
|
||||||
updates: {
|
|
||||||
saveUpdate: string;
|
|
||||||
};
|
|
||||||
constants: {
|
|
||||||
telegram_secret_token: string;
|
|
||||||
telegram_webhook_url?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TgUpdateDocument extends BaseDocument {
|
|
||||||
data: unknown;
|
|
||||||
}
|
|
||||||
Generated
+3
-3
@@ -238,7 +238,7 @@ importers:
|
|||||||
specifier: ~5.7.2
|
specifier: ~5.7.2
|
||||||
version: 5.7.2
|
version: 5.7.2
|
||||||
|
|
||||||
packages/tg-bot-webhook-ddoc:
|
packages/tg-bot-update-webhook-ddoc:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@hereconnect/types':
|
'@hereconnect/types':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
@@ -349,9 +349,9 @@ importers:
|
|||||||
'@hereconnect/lib-service-seq-tracker':
|
'@hereconnect/lib-service-seq-tracker':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/lib-service-seq-tracker
|
version: link:../../packages/lib-service-seq-tracker
|
||||||
'@hereconnect/tg-bot-webhook-ddoc':
|
'@hereconnect/tg-bot-update-webhook-ddoc':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/tg-bot-webhook-ddoc
|
version: link:../../packages/tg-bot-update-webhook-ddoc
|
||||||
'@xmldom/xmldom':
|
'@xmldom/xmldom':
|
||||||
specifier: ^0.9.6
|
specifier: ^0.9.6
|
||||||
version: 0.9.6
|
version: 0.9.6
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const dbsSecurity = {
|
|||||||
admins: { roles: ["_admin"] },
|
admins: { roles: ["_admin"] },
|
||||||
members: { roles: [] },
|
members: { roles: [] },
|
||||||
},
|
},
|
||||||
tg_bot: {
|
tg_bot_updates: {
|
||||||
admins: { roles: ["_admin"] },
|
admins: { roles: ["_admin"] },
|
||||||
members: { roles: ["_admin"] },
|
members: { roles: ["_admin"] },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
import { Bot, Context, InlineKeyboard } from "grammy";
|
|
||||||
import { type AbortSignal } from "grammy/out/shim.node";
|
|
||||||
import { qrCodePlacements } from "@hereconnect/constants";
|
|
||||||
import type { QRCodeDocument } from "@hereconnect/types";
|
|
||||||
|
|
||||||
const BOT_TOKEN = process.env.BOT_TOKEN;
|
|
||||||
if (!BOT_TOKEN) {
|
|
||||||
throw new Error("Необходимо указать BOT_TOKEN в переменных окружения.");
|
|
||||||
}
|
|
||||||
|
|
||||||
export const bot = new Bot(BOT_TOKEN);
|
|
||||||
|
|
||||||
export const init = async (signal: AbortSignal) => {
|
|
||||||
await bot.init(signal);
|
|
||||||
|
|
||||||
// Настройка команд для английского языка (по умолчанию)
|
|
||||||
await bot.api.setMyCommands([
|
|
||||||
{ command: "create_qr", description: "Create a QR code" },
|
|
||||||
{ command: "help", description: "Get help" },
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Настройка команд для русского языка
|
|
||||||
await bot.api.setMyCommands(
|
|
||||||
[
|
|
||||||
{ command: "create_qr", description: "Создать QR-код" },
|
|
||||||
{ command: "help", description: "Получить помощь" },
|
|
||||||
],
|
|
||||||
{ language_code: "ru" },
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const actionsOptions = [
|
|
||||||
{
|
|
||||||
type: "sendMessage",
|
|
||||||
label: "Отправить сообщение",
|
|
||||||
},
|
|
||||||
{ type: "viewContactInfo", label: "Посмотреть ваши контактные данные" },
|
|
||||||
];
|
|
||||||
|
|
||||||
bot.command("start", async (ctx) => {
|
|
||||||
const user = ctx.from;
|
|
||||||
|
|
||||||
const message = `Привет${user?.first_name ? ", " + user?.first_name : ""}!\n\nЯ бот сервиса "НаСвязи". Помогу вам создать QR-код (для автомобиля, питомца, входной двери и т.д.).!`;
|
|
||||||
|
|
||||||
// Отправляем сообщение
|
|
||||||
return ctx.reply(message, {
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
text: "Создать QR-код",
|
|
||||||
callback_data: "create_qr",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const createQrCode = async (ctx: Context) => {
|
|
||||||
const keyboard = new InlineKeyboard();
|
|
||||||
for (const { type, label } of qrCodePlacements) {
|
|
||||||
keyboard.text(label, `select_placement=${type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.reply(
|
|
||||||
"Где вы хотите разместить QR‑код?\n\nЭтот выбор поможет настроить QR‑код с подходящими действиями.",
|
|
||||||
{ reply_markup: { inline_keyboard: keyboard.toFlowed(1).inline_keyboard } },
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
bot.command("create_qr", createQrCode);
|
|
||||||
|
|
||||||
bot.callbackQuery("cancel", async (ctx) => {
|
|
||||||
await ctx.deleteMessage();
|
|
||||||
});
|
|
||||||
|
|
||||||
bot.callbackQuery(
|
|
||||||
/^(select_action=|select_placement=|create_qr$)/,
|
|
||||||
async (ctx) => {
|
|
||||||
const selectedActions: QRCodeDocument["actions"] = [];
|
|
||||||
const keyboard = new InlineKeyboard();
|
|
||||||
for (const { type, label } of actionsOptions) {
|
|
||||||
keyboard.text(
|
|
||||||
(selectedActions.includes(type) ? "✓ " : "") + label,
|
|
||||||
`select_action=${type}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.reply(
|
|
||||||
"Выберите доступные действия?\n\nУкажите, какие действия будут доступны для сканирующих QR‑код.\nВы сможете изменить эти настройки позже.",
|
|
||||||
{ reply_markup: { inline_keyboard: keyboard.inline_keyboard } },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (process.argv.includes("start")) {
|
|
||||||
const abortController = new AbortController();
|
|
||||||
|
|
||||||
process.on("SIGINT", async () => {
|
|
||||||
abortController.abort();
|
|
||||||
setTimeout(() => process.exit(1), 1_000).unref();
|
|
||||||
});
|
|
||||||
|
|
||||||
init(abortController.signal as AbortSignal)
|
|
||||||
.then(() => bot.start())
|
|
||||||
.then(() => console.log("Bot started"))
|
|
||||||
.catch((error) => console.error(error));
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import { startHealthServer } from "@hereconnect/lib-service-health-checkserver";
|
|
||||||
import { ServiceSeqTracker } from "@hereconnect/lib-service-seq-tracker";
|
|
||||||
import { type AbortSignal } from "grammy/out/shim.node";
|
|
||||||
import { setupWebHook } from "setup/setupWebHook";
|
|
||||||
|
|
||||||
const heartbeat = Number(process.env.HEARTBEAT_INTERVAL ?? 30_000);
|
|
||||||
|
|
||||||
import { CouchDBChangesStream } from "@hereconnect/couchdb-changes-stream";
|
|
||||||
import { TG_BOT_DB_URL, serviceSeqTrackerDb, checkDatabases } from "dbs";
|
|
||||||
import type { TgUpdateDocument } from "types";
|
|
||||||
import { init, bot } from "old_bot";
|
|
||||||
|
|
||||||
const abortController = new AbortController();
|
|
||||||
|
|
||||||
const start = async () => {
|
|
||||||
const checkedDatabases = await checkDatabases();
|
|
||||||
|
|
||||||
await init(abortController.signal as AbortSignal);
|
|
||||||
await setupWebHook();
|
|
||||||
|
|
||||||
const serviceSeqTracker = await ServiceSeqTracker.Init(
|
|
||||||
"_local/service:tg-bot",
|
|
||||||
serviceSeqTrackerDb,
|
|
||||||
);
|
|
||||||
|
|
||||||
const changesStream = new CouchDBChangesStream<TgUpdateDocument>(
|
|
||||||
TG_BOT_DB_URL,
|
|
||||||
{
|
|
||||||
abortController,
|
|
||||||
since: serviceSeqTracker.lastSeq,
|
|
||||||
feed: "continuous",
|
|
||||||
include_docs: true,
|
|
||||||
heartbeat,
|
|
||||||
filter: "_selector",
|
|
||||||
selector: {
|
|
||||||
type: "tg_update" satisfies TgUpdateDocument["type"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Gracefully handle SIGINT
|
|
||||||
process.on("SIGINT", async () => {
|
|
||||||
changesStream?.stop();
|
|
||||||
setTimeout(() => process.exit(1), 5_000).unref();
|
|
||||||
});
|
|
||||||
|
|
||||||
console.info("Service started from sequence", serviceSeqTracker.lastSeq);
|
|
||||||
if (
|
|
||||||
parseInt(serviceSeqTracker.lastSeq.toString(), 10) ===
|
|
||||||
parseInt(checkedDatabases.tgBotDb.update_seq.toString(), 10)
|
|
||||||
) {
|
|
||||||
console.log("Since is the last seq");
|
|
||||||
} else {
|
|
||||||
console.log("Last seq is", checkedDatabases.tgBotDb.update_seq);
|
|
||||||
}
|
|
||||||
|
|
||||||
await startHealthServer(abortController);
|
|
||||||
|
|
||||||
for await (const change of changesStream) {
|
|
||||||
const { seq, doc: changeDoc } = change;
|
|
||||||
|
|
||||||
if (!changeDoc) {
|
|
||||||
console.error("Change missing document:", change);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceSeqTracker.checkNextSeq(seq);
|
|
||||||
await bot.handleUpdate(changeDoc.data);
|
|
||||||
|
|
||||||
// const ctx = new Context(changeDoc.data, bot.api, bot.me);
|
|
||||||
// if (ctx.hasCommand("start")) {
|
|
||||||
// }
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(
|
|
||||||
`Processed change: ${JSON.stringify({ _id: change.id, ref: changeDoc._rev, seq: change.seq })}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`Failed processing: ${JSON.stringify(error)} change: ${JSON.stringify({ id: changeDoc._id, rev: changeDoc._rev, seq: seq })}`,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
await serviceSeqTracker.saveSeq(seq);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
start().catch((error) => {
|
|
||||||
console.error("error", error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"@hereconnect/couchdb-changes-stream": "workspace:^",
|
"@hereconnect/couchdb-changes-stream": "workspace:^",
|
||||||
"@hereconnect/lib-service-health-checkserver": "workspace:^",
|
"@hereconnect/lib-service-health-checkserver": "workspace:^",
|
||||||
"@hereconnect/lib-service-seq-tracker": "workspace:^",
|
"@hereconnect/lib-service-seq-tracker": "workspace:^",
|
||||||
"@hereconnect/tg-bot-webhook-ddoc": "workspace:^",
|
"@hereconnect/tg-bot-update-webhook-ddoc": "workspace:^",
|
||||||
"@xmldom/xmldom": "^0.9.6",
|
"@xmldom/xmldom": "^0.9.6",
|
||||||
"canvas": "^2.11.2",
|
"canvas": "^2.11.2",
|
||||||
"extensionless": "^1.9.9",
|
"extensionless": "^1.9.9",
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export const forwardQrCodeMessageMiddleware = async (
|
|||||||
|
|
||||||
const hashTag = `#qr${qr_code_uri}_c${chat}`;
|
const hashTag = `#qr${qr_code_uri}_c${chat}`;
|
||||||
const manageMessageUrl = `${env.APP_BASE_URL}/manage/qr/${qr_code_uri}/chat/${chat}#message-${encodeURIComponent(created_at)}`;
|
const manageMessageUrl = `${env.APP_BASE_URL}/manage/qr/${qr_code_uri}/chat/${chat}#message-${encodeURIComponent(created_at)}`;
|
||||||
const text = `<i>сообщение <a href="${escapeHtml(manageMessageUrl)}">диалог QR-кода <b>«${escapeHtml(qrCodeDoc.name)}»</b></a> ${escapeHtml(hashTag)}</i>`;
|
const text = `<i>сообщение отправлено <a href="${escapeHtml(manageMessageUrl)}">в диалог QR-кода <b>«${escapeHtml(qrCodeDoc.name)}»</b></a> ${escapeHtml(hashTag)}</i>`;
|
||||||
|
|
||||||
// await ctx.react("⚡");
|
// await ctx.react("⚡");
|
||||||
await ctx.reply(text, {
|
await ctx.reply(text, {
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ 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.PUBLIC_API_URL) {
|
||||||
|
throw new Error("Environment variable PUBLIC_API_URL is not set");
|
||||||
|
}
|
||||||
|
|
||||||
if (!process.env.HEARTBEAT_INTERVAL) {
|
if (!process.env.HEARTBEAT_INTERVAL) {
|
||||||
throw new Error("Environment variable HEARTBEAT_INTERVAL is not set");
|
throw new Error("Environment variable HEARTBEAT_INTERVAL is not set");
|
||||||
}
|
}
|
||||||
@@ -23,5 +27,6 @@ export const env = {
|
|||||||
READ_BASE_URL: process.env.READ_BASE_URL,
|
READ_BASE_URL: process.env.READ_BASE_URL,
|
||||||
APP_BASE_URL: process.env.APP_BASE_URL,
|
APP_BASE_URL: process.env.APP_BASE_URL,
|
||||||
API_URL: process.env.API_URL,
|
API_URL: process.env.API_URL,
|
||||||
|
PUBLIC_API_URL: process.env.PUBLIC_API_URL,
|
||||||
HEARTBEAT_INTERVAL: parseInt(process.env.HEARTBEAT_INTERVAL, 10),
|
HEARTBEAT_INTERVAL: parseInt(process.env.HEARTBEAT_INTERVAL, 10),
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
+21
-21
@@ -11,55 +11,55 @@ import type {
|
|||||||
|
|
||||||
import type { ServiceSeqTrackerDocument } from "@hereconnect/lib-service-seq-tracker";
|
import type { ServiceSeqTrackerDocument } from "@hereconnect/lib-service-seq-tracker";
|
||||||
|
|
||||||
import type { TelegramWebhookDesignDocument } from "@hereconnect/tg-bot-webhook-ddoc";
|
import type { TgBotUpdateWebhookDesignDocument } from "@hereconnect/tg-bot-update-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 { TgBotUpdateDocument } from "./types/TgBotUpdateDocument";
|
||||||
import { env } from "./config/env";
|
import { env } from "./config/env";
|
||||||
|
|
||||||
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_UPDATES_DB_NAME = "tg_bot_updates";
|
||||||
export const MESSAGES_DB_NAME = "messages";
|
export const MESSAGES_DB_NAME = "messages";
|
||||||
export const TG_BOT_DB_URL = `${env.API_URL}/${TG_BOT_DB_NAME}`;
|
export const TG_BOT_UPDATES_DB_URL = `${env.API_URL}/${TG_BOT_UPDATES_DB_NAME}`;
|
||||||
export const MESSAGES_DB_URL = `${env.API_URL}/${MESSAGES_DB_NAME}`;
|
export const MESSAGES_DB_URL = `${env.API_URL}/${MESSAGES_DB_NAME}`;
|
||||||
|
|
||||||
export const messagesDb = new PouchDB<MessageDocument>(MESSAGES_DB_URL, {
|
export const messagesDb = new PouchDB<MessageDocument>(MESSAGES_DB_URL, {
|
||||||
skip_setup: true,
|
skip_setup: true,
|
||||||
});
|
});
|
||||||
export const messagesServiceSeqTrackerDb =
|
export const messagesServiceSeqTrackerDb =
|
||||||
messagesDb satisfies PouchDB.Database as unknown as PouchDB.Database<ServiceSeqTrackerDocument>;
|
messagesDb satisfies PouchDB.Database as PouchDB.Database as PouchDB.Database<ServiceSeqTrackerDocument>;
|
||||||
|
|
||||||
export const publicSettingsDb = new PouchDB<PublicSettingsDocument>(
|
export const publicSettingsDb = new PouchDB<PublicSettingsDocument>(
|
||||||
`${env.API_URL}/public_settings`,
|
`${env.API_URL}/public_settings`,
|
||||||
{
|
{ skip_setup: true },
|
||||||
skip_setup: true,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const qrDb = new PouchDB<QRCodeDocument>(`${env.API_URL}/qr`, {
|
export const qrDb = new PouchDB<QRCodeDocument>(`${env.API_URL}/qr`, {
|
||||||
skip_setup: true,
|
skip_setup: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const tgBotDb = new PouchDB(TG_BOT_DB_URL, {
|
|
||||||
skip_setup: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const serviceSeqTrackerDb = tgBotDb as ServiceSeqTrackerDatabase;
|
|
||||||
|
|
||||||
export const tgBotSessionsDb = new PouchDB<CouchDBSessionDocument<SessionData>>(
|
export const tgBotSessionsDb = new PouchDB<CouchDBSessionDocument<SessionData>>(
|
||||||
`${env.API_URL}/tg_bot_sessions`,
|
`${env.API_URL}/tg_bot_sessions`,
|
||||||
{
|
{ skip_setup: true },
|
||||||
skip_setup: true,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const tgBotUpdatesDb = new PouchDB<TgBotUpdateDocument>(
|
||||||
|
`${TG_BOT_UPDATES_DB_URL}`,
|
||||||
|
{ skip_setup: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const tgBotUpdatesDesignDocumentsDb =
|
||||||
|
tgBotUpdatesDb satisfies PouchDB.Database as PouchDB.Database as PouchDB.Database<TgBotUpdateWebhookDesignDocument>;
|
||||||
|
|
||||||
|
export const tgBotUpdatesServiceSeqTrackerDb =
|
||||||
|
tgBotUpdatesDb satisfies PouchDB.Database as PouchDB.Database as ServiceSeqTrackerDatabase;
|
||||||
|
|
||||||
export const userChatSettingsDb = new PouchDB<UserChatSettingsDocument>(
|
export const userChatSettingsDb = new PouchDB<UserChatSettingsDocument>(
|
||||||
`${env.API_URL}/user_chat_settings`,
|
`${env.API_URL}/user_chat_settings`,
|
||||||
{
|
{ skip_setup: true },
|
||||||
skip_setup: true,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const usersDb = new PouchDB<UserDocument>(`${env.API_URL}/_users`, {
|
export const usersDb = new PouchDB<UserDocument>(`${env.API_URL}/_users`, {
|
||||||
@@ -71,11 +71,11 @@ const checkDatabases = async () => {
|
|||||||
publicSettingsDb,
|
publicSettingsDb,
|
||||||
qrDb,
|
qrDb,
|
||||||
usersDb,
|
usersDb,
|
||||||
serviceSeqTrackerDb,
|
|
||||||
userChatSettingsDb,
|
userChatSettingsDb,
|
||||||
tgBotDb,
|
tgBotUpdatesDb,
|
||||||
tgBotSessionsDb,
|
tgBotSessionsDb,
|
||||||
messagesServiceSeqTrackerDb,
|
messagesServiceSeqTrackerDb,
|
||||||
|
messagesDb,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const databasesEntries = Object.entries(databases);
|
const databasesEntries = Object.entries(databases);
|
||||||
|
|||||||
@@ -1,31 +1,45 @@
|
|||||||
import { createHealthServer } from "@hereconnect/lib-service-health-checkserver";
|
import { createHealthServer } from "@hereconnect/lib-service-health-checkserver";
|
||||||
import { createBot, setupCommands } from "./bot";
|
import { createBot, setupCommands } from "./bot";
|
||||||
import { createServiceChangesMessagesDb } from "./services/serviceChangesMessagesDb";
|
import { createServiceChangesMessagesDb } from "./services/serviceChangesMessagesDb";
|
||||||
|
import { createServiceChangesTgUpdatesDb } from "./services/serviceChangesTgUpdatesDb";
|
||||||
import { iterateMessagesChanges } from "./services/iterateMessagesChanges";
|
import { iterateMessagesChanges } from "./services/iterateMessagesChanges";
|
||||||
|
import { setupWebHook } from "./services/setupWebHook";
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const healthServer = createHealthServer(abortController);
|
const healthServer = createHealthServer(abortController);
|
||||||
const bot = createBot({ abortController });
|
const bot = createBot({ abortController });
|
||||||
const { changesStream, serviceSeqTracker } =
|
const messagesChanges = await createServiceChangesMessagesDb({
|
||||||
await createServiceChangesMessagesDb({
|
abortController,
|
||||||
abortController,
|
});
|
||||||
});
|
const tgUpdatesChanges = await createServiceChangesTgUpdatesDb({
|
||||||
|
abortController,
|
||||||
|
});
|
||||||
|
|
||||||
process.on("SIGINT", async () => {
|
process.on("SIGINT", async () => {
|
||||||
changesStream.stop();
|
messagesChanges.changesStream.stop();
|
||||||
|
tgUpdatesChanges.changesStream.stop();
|
||||||
bot.stop();
|
bot.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
await healthServer.start();
|
await healthServer.start();
|
||||||
|
|
||||||
await bot.init();
|
await bot.init();
|
||||||
// Устанавливаем команд
|
|
||||||
|
await setupWebHook(bot);
|
||||||
await setupCommands(bot);
|
await setupCommands(bot);
|
||||||
|
|
||||||
bot.start();
|
// bot.start();
|
||||||
console.log("Бот запущен!");
|
console.log("Бот запущен!");
|
||||||
|
|
||||||
iterateMessagesChanges({ changesStream, serviceSeqTracker, bot });
|
iterateMessagesChanges({ bot, ...messagesChanges });
|
||||||
|
|
||||||
|
for await (const change of tgUpdatesChanges.changesStream) {
|
||||||
|
tgUpdatesChanges.serviceSeqTracker.checkNextSeq(change.seq);
|
||||||
|
if (change.doc) {
|
||||||
|
await bot.handleUpdate(change.doc.data);
|
||||||
|
}
|
||||||
|
await tgUpdatesChanges.serviceSeqTracker.saveSeq(change.seq);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { ServiceSeqTracker } from "@hereconnect/lib-service-seq-tracker";
|
||||||
|
import { CouchDBChangesStream } from "@hereconnect/couchdb-changes-stream";
|
||||||
|
import type { TgBotUpdateDocument } from "../types/TgBotUpdateDocument";
|
||||||
|
import {
|
||||||
|
TG_BOT_UPDATES_DB_URL,
|
||||||
|
tgBotUpdatesServiceSeqTrackerDb,
|
||||||
|
checkDatabasesPromise,
|
||||||
|
} from "../dbs";
|
||||||
|
import { env } from "../config/env";
|
||||||
|
|
||||||
|
export const createServiceChangesTgUpdatesDb = async ({
|
||||||
|
abortController,
|
||||||
|
}: {
|
||||||
|
abortController: AbortController;
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
tgBotUpdatesDb: { update_seq: lastSeq },
|
||||||
|
} = await checkDatabasesPromise;
|
||||||
|
|
||||||
|
const serviceSeqTracker = await ServiceSeqTracker.Init(
|
||||||
|
"_local/service:tg-bot",
|
||||||
|
tgBotUpdatesServiceSeqTrackerDb,
|
||||||
|
);
|
||||||
|
|
||||||
|
const changesStream = new CouchDBChangesStream<TgBotUpdateDocument>(
|
||||||
|
TG_BOT_UPDATES_DB_URL,
|
||||||
|
{
|
||||||
|
abortController,
|
||||||
|
since: serviceSeqTracker.lastSeq,
|
||||||
|
feed: "continuous",
|
||||||
|
include_docs: true,
|
||||||
|
heartbeat: env.HEARTBEAT_INTERVAL,
|
||||||
|
filter: "_selector",
|
||||||
|
selector: {
|
||||||
|
type: "tg_bot_update",
|
||||||
|
} satisfies Partial<TgBotUpdateDocument>,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.info(
|
||||||
|
"Service started listening changes tg_bot_update 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 };
|
||||||
|
};
|
||||||
+7
-10
@@ -1,22 +1,19 @@
|
|||||||
import { setupWebhookDesignDocument } from "./setupWebhookDesignDocument";
|
import { setupWebhookDesignDocument } from "./setupWebhookDesignDocument";
|
||||||
import { TG_BOT_DB_NAME, tgBotDesignDocumentsDb } from "dbs";
|
import { TG_BOT_UPDATES_DB_NAME, tgBotUpdatesDesignDocumentsDb } from "../dbs";
|
||||||
import { bot } from "old_bot";
|
import { env } from "../config/env";
|
||||||
|
import { Bot } from "grammy";
|
||||||
|
import { MyContext } from "../types/myContext";
|
||||||
|
|
||||||
const PUBLIC_API_URL = process.env.PUBLIC_API_URL;
|
export const setupWebHook = async (bot: Bot<MyContext>) => {
|
||||||
if (!PUBLIC_API_URL) {
|
|
||||||
throw new Error("Необходимо указать PUBLIC_API_URL в переменных окружения.");
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setupWebHook = async () => {
|
|
||||||
const designDocument = await setupWebhookDesignDocument();
|
const designDocument = await setupWebhookDesignDocument();
|
||||||
const update: keyof typeof designDocument.updates = "saveUpdate";
|
const update: keyof typeof designDocument.updates = "saveUpdate";
|
||||||
const webhookUrl = `${process.env.PUBLIC_API_URL}/${TG_BOT_DB_NAME}/${designDocument._id}/_update/${update}`;
|
const webhookUrl = `${env.PUBLIC_API_URL}/${TG_BOT_UPDATES_DB_NAME}/${designDocument._id}/_update/${update}`;
|
||||||
if (designDocument.constants.telegram_webhook_url !== webhookUrl) {
|
if (designDocument.constants.telegram_webhook_url !== webhookUrl) {
|
||||||
await bot.api.setWebhook(webhookUrl, {
|
await bot.api.setWebhook(webhookUrl, {
|
||||||
secret_token: designDocument.constants.telegram_secret_token,
|
secret_token: designDocument.constants.telegram_secret_token,
|
||||||
});
|
});
|
||||||
designDocument.constants.telegram_webhook_url = webhookUrl;
|
designDocument.constants.telegram_webhook_url = webhookUrl;
|
||||||
const { rev } = await tgBotDesignDocumentsDb.put(designDocument);
|
const { rev } = await tgBotUpdatesDesignDocumentsDb.put(designDocument);
|
||||||
designDocument._rev = rev;
|
designDocument._rev = rev;
|
||||||
}
|
}
|
||||||
return designDocument;
|
return designDocument;
|
||||||
+11
-8
@@ -1,6 +1,6 @@
|
|||||||
import { tgBotDesignDocumentsDb } from "../dbs";
|
import { tgBotUpdatesDesignDocumentsDb } from "../dbs";
|
||||||
import { saveUpdateString as saveUpdate } from "@hereconnect/tg-bot-webhook-ddoc";
|
import { saveUpdateString as saveUpdate } from "@hereconnect/tg-bot-update-webhook-ddoc";
|
||||||
import { type TelegramWebhookDesignDocument } from "@hereconnect/tg-bot-webhook-ddoc";
|
import { type TgBotUpdateWebhookDesignDocument } from "@hereconnect/tg-bot-update-webhook-ddoc";
|
||||||
|
|
||||||
function generateTelegramSecretToken(length = 256) {
|
function generateTelegramSecretToken(length = 256) {
|
||||||
const characters =
|
const characters =
|
||||||
@@ -14,11 +14,11 @@ function generateTelegramSecretToken(length = 256) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const setupWebhookDesignDocument = async () => {
|
export const setupWebhookDesignDocument = async () => {
|
||||||
const _id = "_design/webhook";
|
const _id = "_design/tg_bot_webhook";
|
||||||
|
|
||||||
const result: TelegramWebhookDesignDocument = {
|
const result: TgBotUpdateWebhookDesignDocument = {
|
||||||
_id,
|
_id,
|
||||||
type: "_design/webhook",
|
type: _id,
|
||||||
created_at: "",
|
created_at: "",
|
||||||
updated_at: "",
|
updated_at: "",
|
||||||
updates: {
|
updates: {
|
||||||
@@ -29,7 +29,7 @@ export const setupWebhookDesignDocument = async () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { rev } = await tgBotDesignDocumentsDb.upsert(_id, (ddoc) => {
|
const { rev } = await tgBotUpdatesDesignDocumentsDb.upsert(_id, (ddoc) => {
|
||||||
const isEmptyTelegramSecretToken =
|
const isEmptyTelegramSecretToken =
|
||||||
!ddoc.constants?.telegram_secret_token?.length;
|
!ddoc.constants?.telegram_secret_token?.length;
|
||||||
const needToSetSaveUpdateFunction =
|
const needToSetSaveUpdateFunction =
|
||||||
@@ -60,7 +60,10 @@ export const setupWebhookDesignDocument = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.assign(result, ddoc) satisfies TelegramWebhookDesignDocument;
|
return Object.assign(
|
||||||
|
result,
|
||||||
|
ddoc,
|
||||||
|
) satisfies TgBotUpdateWebhookDesignDocument;
|
||||||
});
|
});
|
||||||
|
|
||||||
result._rev = rev;
|
result._rev = rev;
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import type { BaseDocument } from "@hereconnect/types";
|
|
||||||
import type { Update } from "@grammyjs/types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Интерфейс для документа CouchDB, представляющего обновление Telegram
|
|
||||||
*/
|
|
||||||
export interface TgUpdateDocument extends BaseDocument {
|
|
||||||
/**
|
|
||||||
* Уникальный идентификатор документа, например, "tg_update:<update_id>"
|
|
||||||
*/
|
|
||||||
_id: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ревизия документа (если документ уже существует, автоматически добавляется CouchDB)
|
|
||||||
*/
|
|
||||||
_rev?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Тип документа для идентификации (фиксированное значение "tg_update")
|
|
||||||
*/
|
|
||||||
type: "tg_update";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Данные обновления Telegram
|
|
||||||
*/
|
|
||||||
data: Update;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { Update } from "@grammyjs/types";
|
||||||
|
import type { TgBotUpdateDocument as BaseTgBotUpdateDocument } from "@hereconnect/tg-bot-update-webhook-ddoc";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Интерфейс для документа CouchDB, представляющего обновление Telegram
|
||||||
|
*/
|
||||||
|
export interface TgBotUpdateDocument extends BaseTgBotUpdateDocument {
|
||||||
|
/**
|
||||||
|
* Данные обновления Telegram
|
||||||
|
*/
|
||||||
|
data: Update;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user