|
|
|
@@ -18,6 +18,9 @@ const AUTH_SECRET = process.env.AUTH_SECRET || "dev-secret-change-me";
|
|
|
|
|
const COOKIE_NAME = "mf_session";
|
|
|
|
|
const OPENAI_API_KEY = process.env.OPENAI_API_KEY || "";
|
|
|
|
|
const OPENAI_TRANSLATION_MODEL = process.env.OPENAI_TRANSLATION_MODEL || "gpt-5-mini";
|
|
|
|
|
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || "";
|
|
|
|
|
const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID || "";
|
|
|
|
|
const TELEGRAM_WEBHOOK_SECRET = process.env.TELEGRAM_WEBHOOK_SECRET || "";
|
|
|
|
|
const ALLOWED_IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp", "image/gif", "image/avif"]);
|
|
|
|
|
const ALLOWED_IMAGE_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".webp", ".gif", ".avif"]);
|
|
|
|
|
const MAX_ATTACHMENT_BYTES = 3_000_000;
|
|
|
|
@@ -70,6 +73,8 @@ async function route(req, res) {
|
|
|
|
|
}
|
|
|
|
|
if (req.method === "GET" && url.pathname.startsWith("/admin.")) return servePublic(res, url.pathname);
|
|
|
|
|
|
|
|
|
|
if (url.pathname.match(/^\/api\/telegram\/webhook\/[^/]+$/) && req.method === "POST") return telegramWebhook(req, res, url);
|
|
|
|
|
|
|
|
|
|
if (url.pathname === "/api/admin/login" && req.method === "POST") return adminLogin(req, res);
|
|
|
|
|
if (url.pathname === "/api/admin/logout" && req.method === "POST") return adminLogout(res);
|
|
|
|
|
if (url.pathname.startsWith("/api/admin/")) return requireAdmin(req, res, () => adminApi(req, res, url));
|
|
|
|
@@ -97,6 +102,7 @@ async function adminApi(req, res, url) {
|
|
|
|
|
if (url.pathname.match(/^\/api\/admin\/users\/\d+$/) && req.method === "DELETE") return deleteAdminUser(req, res, url);
|
|
|
|
|
if (url.pathname === "/api/admin/ai/models" && req.method === "GET") return adminAiModels(res);
|
|
|
|
|
if (url.pathname === "/api/admin/ai/test" && req.method === "POST") return adminAiTest(res);
|
|
|
|
|
if (url.pathname === "/api/admin/telegram/test" && req.method === "POST") return adminTelegramTest(res);
|
|
|
|
|
if (url.pathname === "/api/admin/conversations" && req.method === "GET") return adminConversations(res);
|
|
|
|
|
if (url.pathname === "/api/admin/visitors" && req.method === "GET") return adminVisitors(res);
|
|
|
|
|
if (url.pathname.match(/^\/api\/admin\/visitors\/[^/]+$/) && req.method === "PATCH") return updateVisitorProfile(req, res, url);
|
|
|
|
@@ -224,10 +230,22 @@ function initDb() {
|
|
|
|
|
UNIQUE(site_id, visitor_token)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS telegram_message_links (
|
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
|
site_id INTEGER NOT NULL REFERENCES sites(id),
|
|
|
|
|
conversation_id INTEGER NOT NULL REFERENCES conversations(id),
|
|
|
|
|
telegram_chat_id TEXT NOT NULL,
|
|
|
|
|
telegram_message_id INTEGER NOT NULL,
|
|
|
|
|
source_message_id INTEGER REFERENCES messages(id),
|
|
|
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
UNIQUE(telegram_chat_id, telegram_message_id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_conversations_site_status ON conversations(site_id, status, last_message_at);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id, created_at);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_visitor_sessions_seen ON visitor_sessions(site_id, last_seen_at);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_visitor_profiles_token ON visitor_profiles(site_id, visitor_token);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_telegram_links_conversation ON telegram_message_links(conversation_id);
|
|
|
|
|
`);
|
|
|
|
|
if (ensureColumn("conversations", "admin_seen_at", "TEXT")) {
|
|
|
|
|
db.prepare("UPDATE conversations SET admin_seen_at = last_message_at WHERE admin_seen_at IS NULL").run();
|
|
|
|
@@ -334,6 +352,13 @@ function defaultSettings() {
|
|
|
|
|
operatorLanguage: "cs",
|
|
|
|
|
model: OPENAI_TRANSLATION_MODEL,
|
|
|
|
|
openaiApiKey: ""
|
|
|
|
|
},
|
|
|
|
|
telegram: {
|
|
|
|
|
enabled: false,
|
|
|
|
|
botToken: TELEGRAM_BOT_TOKEN,
|
|
|
|
|
chatId: TELEGRAM_CHAT_ID,
|
|
|
|
|
webhookSecret: TELEGRAM_WEBHOOK_SECRET || id("tgsec"),
|
|
|
|
|
operatorName: "Alex"
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
@@ -677,6 +702,39 @@ async function adminAiTest(res) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function adminTelegramTest(res) {
|
|
|
|
|
const settings = getTelegramSettings();
|
|
|
|
|
if (!settings.enabled) return json(res, 400, { ok: false, error: "telegram_disabled", message: "Telegram neni zapnuty." });
|
|
|
|
|
if (!settings.botToken || !settings.chatId) return json(res, 400, { ok: false, error: "missing_telegram_settings", message: "Chybi bot token nebo chat ID." });
|
|
|
|
|
if (!settings.webhookSecret) return json(res, 400, { ok: false, error: "missing_webhook_secret", message: "Chybi webhook secret." });
|
|
|
|
|
try {
|
|
|
|
|
await telegramSetWebhook(settings);
|
|
|
|
|
await telegramSendMessage(settings, {
|
|
|
|
|
text: `MaalFlows test\nTelegram notifikace funguje.\n\nReply na notifikaci od zakaznika poslouzi jako odpoved do chatu.`,
|
|
|
|
|
replyMarkup: {
|
|
|
|
|
inline_keyboard: [[{ text: "Otevrit BO", url: `${PUBLIC_BASE_URL}/admin` }]]
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return json(res, 200, { ok: true, webhookUrl: telegramWebhookUrl(settings) });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("telegram_test_failed", error.message);
|
|
|
|
|
return json(res, 400, { ok: false, error: "telegram_failed", message: error.message });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function telegramWebhook(req, res, url) {
|
|
|
|
|
const secret = decodeURIComponent(url.pathname.split("/").pop() || "");
|
|
|
|
|
const settings = getTelegramSettings();
|
|
|
|
|
if (!settings.enabled || !settings.webhookSecret || secret !== settings.webhookSecret) return json(res, 403, { ok: false });
|
|
|
|
|
const update = await readJson(req, 2_000_000).catch(() => ({}));
|
|
|
|
|
try {
|
|
|
|
|
await handleTelegramUpdate(update, settings);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("telegram_webhook_failed", error.message);
|
|
|
|
|
}
|
|
|
|
|
return json(res, 200, { ok: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function updateConversationStatus(req, res, url) {
|
|
|
|
|
const body = await readJson(req);
|
|
|
|
|
const status = ["new", "open", "resolved", "spam"].includes(body.status) ? body.status : null;
|
|
|
|
@@ -818,6 +876,7 @@ async function createConversation(req, res) {
|
|
|
|
|
const conversation = findConversation(publicId);
|
|
|
|
|
publish("admin", { type: "conversation:new", conversation: formatConversationSummary(conversation) });
|
|
|
|
|
publishConversation(conversation, { type: "message:new", payload: conversationDetails(conversation) });
|
|
|
|
|
notifyTelegramVisitorMessage(conversation, insert.messageId).catch((error) => console.error("telegram_notify_failed", error.message));
|
|
|
|
|
return json(res, 201, conversationDetails(conversation));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -840,6 +899,7 @@ async function createVisitorMessage(req, res, url) {
|
|
|
|
|
bumpConversation(conversation.id, conversation.status === "resolved" ? "open" : conversation.status);
|
|
|
|
|
const updated = findConversation(conversation.public_id);
|
|
|
|
|
publishConversation(updated, { type: "message:new", payload: conversationDetails(updated) });
|
|
|
|
|
notifyTelegramVisitorMessage(updated, messageId).catch((error) => console.error("telegram_notify_failed", error.message));
|
|
|
|
|
return json(res, 201, conversationDetails(updated));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -860,14 +920,23 @@ async function createAdminMessage(req, res, url) {
|
|
|
|
|
if (!attachmentsAreAllowed(body.attachments || [])) return json(res, 400, { error: "only_images_allowed" });
|
|
|
|
|
const conversation = findConversation(publicIdFrom(url));
|
|
|
|
|
if (!conversation) return json(res, 404, { error: "conversation_not_found" });
|
|
|
|
|
const messageId = insertMessage(conversation.id, "admin", req.adminUser.chatNick || req.adminUser.displayName || req.adminUser.username, String(body.message || "").trim());
|
|
|
|
|
const updated = await createOperatorMessage(conversation, {
|
|
|
|
|
message: body.message,
|
|
|
|
|
attachments: body.attachments || [],
|
|
|
|
|
senderName: req.adminUser.chatNick || req.adminUser.displayName || req.adminUser.username
|
|
|
|
|
});
|
|
|
|
|
return json(res, 201, conversationDetails(updated));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createOperatorMessage(conversation, body) {
|
|
|
|
|
const messageId = insertMessage(conversation.id, "admin", body.senderName || "Operator", String(body.message || "").trim());
|
|
|
|
|
saveAttachments(conversation.id, messageId, body.attachments || []);
|
|
|
|
|
await translateStoredMessage(conversation.id, messageId, "admin", body);
|
|
|
|
|
bumpConversation(conversation.id, conversation.status === "new" ? "open" : conversation.status);
|
|
|
|
|
markConversationSeen(conversation.id);
|
|
|
|
|
const updated = findConversation(conversation.public_id);
|
|
|
|
|
publishConversation(updated, { type: "message:new", payload: conversationDetails(updated) });
|
|
|
|
|
return json(res, 201, conversationDetails(updated));
|
|
|
|
|
return updated;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function deleteAttachment(res, url) {
|
|
|
|
@@ -1090,6 +1159,117 @@ function outputTextFromResponse(data) {
|
|
|
|
|
return chunks.join("");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function notifyTelegramVisitorMessage(conversation, messageId) {
|
|
|
|
|
const settings = getTelegramSettings();
|
|
|
|
|
if (!settings.enabled || !settings.botToken || !settings.chatId) return;
|
|
|
|
|
const message = db.prepare("SELECT * FROM messages WHERE id = ? AND conversation_id = ?").get(messageId, conversation.id);
|
|
|
|
|
if (!message || message.sender_type !== "visitor") return;
|
|
|
|
|
const details = conversationDetails(findConversation(conversation.public_id));
|
|
|
|
|
const attachmentCount = details.messages.find((item) => item.id === message.id)?.attachments?.length || 0;
|
|
|
|
|
const sent = await telegramSendMessage(settings, {
|
|
|
|
|
text: telegramVisitorMessageText(details.conversation, message, attachmentCount),
|
|
|
|
|
replyMarkup: {
|
|
|
|
|
inline_keyboard: [[{ text: "Otevrit BO", url: `${PUBLIC_BASE_URL}/admin` }]]
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const telegramMessageId = sent?.result?.message_id;
|
|
|
|
|
const telegramChatId = String(sent?.result?.chat?.id || settings.chatId);
|
|
|
|
|
if (!telegramMessageId) return;
|
|
|
|
|
db.prepare(`
|
|
|
|
|
INSERT OR REPLACE INTO telegram_message_links
|
|
|
|
|
(site_id, conversation_id, telegram_chat_id, telegram_message_id, source_message_id)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?)
|
|
|
|
|
`).run(conversation.site_id, conversation.id, telegramChatId, telegramMessageId, message.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleTelegramUpdate(update, settings) {
|
|
|
|
|
const message = update?.message;
|
|
|
|
|
if (!message || !message.text || !message.reply_to_message) return;
|
|
|
|
|
const chatId = String(message.chat?.id || "");
|
|
|
|
|
if (String(settings.chatId) && chatId !== String(settings.chatId)) return;
|
|
|
|
|
const replyToMessageId = Number(message.reply_to_message.message_id);
|
|
|
|
|
const link = db.prepare(`
|
|
|
|
|
SELECT l.*, c.public_id
|
|
|
|
|
FROM telegram_message_links l
|
|
|
|
|
JOIN conversations c ON c.id = l.conversation_id
|
|
|
|
|
WHERE l.telegram_chat_id = ? AND l.telegram_message_id = ?
|
|
|
|
|
LIMIT 1
|
|
|
|
|
`).get(chatId, replyToMessageId);
|
|
|
|
|
if (!link) {
|
|
|
|
|
await telegramSendMessage(settings, { chatId, text: "MaalFlows: odpoved nepatri k zadne aktivni notifikaci." });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const conversation = findConversation(link.public_id);
|
|
|
|
|
if (!conversation) {
|
|
|
|
|
await telegramSendMessage(settings, { chatId, text: "MaalFlows: konverzace uz neexistuje." });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await createOperatorMessage(conversation, {
|
|
|
|
|
message: message.text,
|
|
|
|
|
attachments: [],
|
|
|
|
|
senderName: settings.operatorName
|
|
|
|
|
});
|
|
|
|
|
await telegramSendMessage(settings, { chatId, text: "MaalFlows: odpoved odeslana zakaznikovi." });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function telegramSendMessage(settings, options) {
|
|
|
|
|
const token = settings.botToken;
|
|
|
|
|
const chatId = options.chatId || settings.chatId;
|
|
|
|
|
const response = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
chat_id: chatId,
|
|
|
|
|
text: options.text,
|
|
|
|
|
disable_web_page_preview: true,
|
|
|
|
|
reply_markup: options.replyMarkup
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
const data = await response.json().catch(() => ({}));
|
|
|
|
|
if (!response.ok || data.ok === false) {
|
|
|
|
|
throw new Error(data.description || `Telegram HTTP ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function telegramSetWebhook(settings) {
|
|
|
|
|
const response = await fetch(`https://api.telegram.org/bot${settings.botToken}/setWebhook`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ url: telegramWebhookUrl(settings) })
|
|
|
|
|
});
|
|
|
|
|
const data = await response.json().catch(() => ({}));
|
|
|
|
|
if (!response.ok || data.ok === false) {
|
|
|
|
|
throw new Error(data.description || `Telegram webhook HTTP ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function telegramWebhookUrl(settings) {
|
|
|
|
|
return `${PUBLIC_BASE_URL}/api/telegram/webhook/${settings.webhookSecret}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function telegramVisitorMessageText(conversation, message, attachmentCount) {
|
|
|
|
|
const lines = [
|
|
|
|
|
`MaalFlows: nova zprava`,
|
|
|
|
|
`${conversation.countryFlag || ""} ${conversation.visitorLabel || "Navstevnik"} · ${conversation.customerLanguage || conversation.language || "jazyk ?"}`,
|
|
|
|
|
conversation.currentUrl ? `Stranka: ${shortText(conversation.currentUrl, 160)}` : "",
|
|
|
|
|
conversation.referrer ? `Prislel z: ${shortText(conversation.referrer, 120)}` : "",
|
|
|
|
|
"",
|
|
|
|
|
message.body || (attachmentCount ? "(poslana fotka)" : ""),
|
|
|
|
|
message.translated_body ? `\nPreklad: ${message.translated_body}` : "",
|
|
|
|
|
attachmentCount ? `\nPrilohy: ${attachmentCount} fotka/fotek` : "",
|
|
|
|
|
"",
|
|
|
|
|
"Odpovez pres Reply na tuto zpravu."
|
|
|
|
|
];
|
|
|
|
|
return lines.filter((line) => line !== "").join("\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function shortText(value, maxLength) {
|
|
|
|
|
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
|
|
|
return text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mergeSiteSettings(currentSettings, incomingSettings) {
|
|
|
|
|
const settings = { ...currentSettings, ...incomingSettings };
|
|
|
|
|
settings.widgetLanguage = normalizeWidgetLanguageSettings(settings.widgetLanguage || currentSettings.widgetLanguage || {});
|
|
|
|
@@ -1109,6 +1289,22 @@ function mergeSiteSettings(currentSettings, incomingSettings) {
|
|
|
|
|
settings.translations.openaiApiKey = normalizeOpenAiApiKey(settings.translations.openaiApiKey);
|
|
|
|
|
delete settings.translations.hasOpenaiApiKey;
|
|
|
|
|
delete settings.translations.openaiApiKeyMasked;
|
|
|
|
|
const currentTelegram = currentSettings.telegram || {};
|
|
|
|
|
const incomingTelegram = incomingSettings.telegram || {};
|
|
|
|
|
settings.telegram = {
|
|
|
|
|
...currentTelegram,
|
|
|
|
|
...incomingTelegram
|
|
|
|
|
};
|
|
|
|
|
if (!incomingTelegram.botToken || isMaskedApiKey(incomingTelegram.botToken)) {
|
|
|
|
|
settings.telegram.botToken = currentTelegram.botToken || "";
|
|
|
|
|
}
|
|
|
|
|
settings.telegram.botToken = normalizeSecret(settings.telegram.botToken);
|
|
|
|
|
settings.telegram.chatId = String(settings.telegram.chatId || "").trim();
|
|
|
|
|
settings.telegram.webhookSecret = normalizeWebhookSecret(settings.telegram.webhookSecret) || normalizeWebhookSecret(currentTelegram.webhookSecret) || id("tgsec");
|
|
|
|
|
settings.telegram.operatorName = normalizeDisplayName(settings.telegram.operatorName) || "Alex";
|
|
|
|
|
delete settings.telegram.hasBotToken;
|
|
|
|
|
delete settings.telegram.botTokenMasked;
|
|
|
|
|
delete settings.telegram.webhookUrl;
|
|
|
|
|
return settings;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1179,6 +1375,19 @@ function getTranslationSettings() {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getTelegramSettings() {
|
|
|
|
|
const site = getDefaultSite();
|
|
|
|
|
const settings = parseJson(site.settings_json, defaultSettings());
|
|
|
|
|
const telegram = { ...defaultSettings().telegram, ...(settings.telegram || {}) };
|
|
|
|
|
return {
|
|
|
|
|
enabled: Boolean(telegram.enabled),
|
|
|
|
|
botToken: normalizeSecret(telegram.botToken) || TELEGRAM_BOT_TOKEN,
|
|
|
|
|
chatId: String(telegram.chatId || TELEGRAM_CHAT_ID || "").trim(),
|
|
|
|
|
webhookSecret: normalizeWebhookSecret(telegram.webhookSecret) || TELEGRAM_WEBHOOK_SECRET,
|
|
|
|
|
operatorName: normalizeDisplayName(telegram.operatorName) || "Alex"
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function widgetLanguageForRequest(site, url) {
|
|
|
|
|
const settings = parseJson(site.settings_json, defaultSettings());
|
|
|
|
|
const languageSettings = normalizeWidgetLanguageSettings(settings.widgetLanguage);
|
|
|
|
@@ -1317,6 +1526,24 @@ function maskApiKey(value) {
|
|
|
|
|
return `${text.slice(0, 7)}...${text.slice(-4)}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeSecret(value) {
|
|
|
|
|
const text = String(value || "").trim();
|
|
|
|
|
if (!text || isMaskedApiKey(text)) return "";
|
|
|
|
|
return text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function maskSecret(value) {
|
|
|
|
|
const text = normalizeSecret(value);
|
|
|
|
|
if (!text) return "";
|
|
|
|
|
if (text.length <= 10) return `${text.slice(0, 2)}...${text.slice(-2)}`;
|
|
|
|
|
return `${text.slice(0, 6)}...${text.slice(-4)}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeWebhookSecret(value) {
|
|
|
|
|
const text = String(value || "").trim();
|
|
|
|
|
return /^[a-zA-Z0-9_-]{12,120}$/.test(text) ? text : "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fallbackTranslationModels() {
|
|
|
|
|
return [
|
|
|
|
|
OPENAI_TRANSLATION_MODEL,
|
|
|
|
@@ -1545,6 +1772,14 @@ function formatSite(site) {
|
|
|
|
|
settings.translations.hasOpenaiApiKey = Boolean(storedOpenAiKey);
|
|
|
|
|
settings.translations.openaiApiKeyMasked = maskApiKey(storedOpenAiKey);
|
|
|
|
|
settings.translations.openaiApiKey = "";
|
|
|
|
|
settings.telegram = { ...defaultSettings().telegram, ...(settings.telegram || {}) };
|
|
|
|
|
const storedTelegramToken = settings.telegram.botToken || TELEGRAM_BOT_TOKEN;
|
|
|
|
|
const telegramSecret = normalizeWebhookSecret(settings.telegram.webhookSecret) || TELEGRAM_WEBHOOK_SECRET || "";
|
|
|
|
|
settings.telegram.hasBotToken = Boolean(storedTelegramToken);
|
|
|
|
|
settings.telegram.botTokenMasked = maskSecret(storedTelegramToken);
|
|
|
|
|
settings.telegram.botToken = "";
|
|
|
|
|
settings.telegram.webhookSecret = telegramSecret;
|
|
|
|
|
settings.telegram.webhookUrl = telegramSecret ? telegramWebhookUrl({ webhookSecret: telegramSecret }) : "";
|
|
|
|
|
return {
|
|
|
|
|
id: site.id,
|
|
|
|
|
siteKey: site.site_key,
|
|
|
|
|