add ci for tg bot
Deploy app frontend / app frontend (push) Failing after 54s
Deploy service couchdb / service couchdb (push) Successful in 38s
Deploy db-migrations / db-migrations (push) Failing after 50s
Deploy service message-delivery-method-web-push / service message-delivery-method-web-push (push) Failing after 1m27s
Deploy service tg-bot / service tg-bot (push) Failing after 44s

This commit is contained in:
2024-12-12 17:45:01 +02:00
parent e07d748e87
commit 7df04c0103
24 changed files with 309 additions and 365 deletions
+109
View File
@@ -0,0 +1,109 @@
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));
}
+91
View File
@@ -0,0 +1,91 @@
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);
});
+23
View File
@@ -0,0 +1,23 @@
import { setupWebhookDesignDocument } from "./setupWebhookDesignDocument";
import { TG_BOT_DB_NAME, tgBotDesignDocumentsDb } from "dbs";
import { bot } from "old_bot";
const PUBLIC_API_URL = process.env.PUBLIC_API_URL;
if (!PUBLIC_API_URL) {
throw new Error("Необходимо указать PUBLIC_API_URL в переменных окружения.");
}
export const setupWebHook = async () => {
const designDocument = await setupWebhookDesignDocument();
const update: keyof typeof designDocument.updates = "saveUpdate";
const webhookUrl = `${process.env.PUBLIC_API_URL}/${TG_BOT_DB_NAME}/${designDocument._id}/_update/${update}`;
if (designDocument.constants.telegram_webhook_url !== webhookUrl) {
await bot.api.setWebhook(webhookUrl, {
secret_token: designDocument.constants.telegram_secret_token,
});
designDocument.constants.telegram_webhook_url = webhookUrl;
const { rev } = await tgBotDesignDocumentsDb.put(designDocument);
designDocument._rev = rev;
}
return designDocument;
};
@@ -0,0 +1,68 @@
import { tgBotDesignDocumentsDb } from "../dbs";
import { saveUpdateString as saveUpdate } from "@hereconnect/tg-bot-webhook-ddoc";
import { type TelegramWebhookDesignDocument } from "@hereconnect/tg-bot-webhook-ddoc";
function generateTelegramSecretToken(length = 256) {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
let token = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
token += characters[randomIndex];
}
return token;
}
export const setupWebhookDesignDocument = async () => {
const _id = "_design/webhook";
const result: TelegramWebhookDesignDocument = {
_id,
type: "_design/webhook",
created_at: "",
updated_at: "",
updates: {
saveUpdate,
},
constants: {
telegram_secret_token: "",
},
};
const { rev } = await tgBotDesignDocumentsDb.upsert(_id, (ddoc) => {
const isEmptyTelegramSecretToken =
!ddoc.constants?.telegram_secret_token?.length;
const needToSetSaveUpdateFunction =
ddoc.updates?.saveUpdate !== result.updates.saveUpdate;
if (!needToSetSaveUpdateFunction && !isEmptyTelegramSecretToken) {
Object.assign(result, ddoc);
return false;
}
ddoc.updated_at = new Date().toISOString();
if (!ddoc.created_at) {
ddoc.created_at = ddoc.updated_at;
}
if (isEmptyTelegramSecretToken) {
if (!ddoc.constants) {
ddoc.constants = result.constants;
}
ddoc.constants.telegram_secret_token = generateTelegramSecretToken();
}
if (needToSetSaveUpdateFunction) {
if (ddoc.updates) {
ddoc.updates.saveUpdate = result.updates.saveUpdate;
} else {
ddoc.updates = result.updates;
}
}
return Object.assign(result, ddoc) satisfies TelegramWebhookDesignDocument;
});
result._rev = rev;
return result;
};