6 Commits

Author SHA1 Message Date
dependabot[bot] b016575f36 Bump @google/genai from 1.50.1 to 2.4.0
Bumps [@google/genai](https://github.com/googleapis/js-genai) from 1.50.1 to 2.4.0.
- [Release notes](https://github.com/googleapis/js-genai/releases)
- [Changelog](https://github.com/googleapis/js-genai/blob/main/CHANGELOG.md)
- [Commits](https://github.com/googleapis/js-genai/compare/v1.50.1...v2.4.0)

---
updated-dependencies:
- dependency-name: "@google/genai"
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 19:18:32 +00:00
melod1n d95c37a322 improvements 2026-04-29 19:54:55 +03:00
melod1n a7fcb8074c improvements, fixes 2026-04-29 19:48:38 +03:00
melod1n c003d443e3 temp disable prefix requirement for AI reply 2026-04-29 19:24:31 +03:00
melod1n 874f312465 temp disable prefix requirement for AI reply 2026-04-29 19:19:50 +03:00
melod1n d41a4ed3ea improve replies, formatting and system prompt handling 2026-04-29 19:13:28 +03:00
10 changed files with 300 additions and 29 deletions
+5 -4
View File
@@ -8,7 +8,7 @@
"name": "tg-chat-bot",
"version": "1.0.0",
"dependencies": {
"@google/genai": "^1.50.1",
"@google/genai": "^2.4.0",
"@libsql/client": "^0.17.3",
"@mistralai/mistralai": "^1.15.1",
"@napi-rs/canvas": "^0.1.100",
@@ -996,9 +996,10 @@
}
},
"node_modules/@google/genai": {
"version": "1.50.1",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.50.1.tgz",
"integrity": "sha512-YbkX7H9+1Pt8wOt7DDREy8XSoiL6fRDzZQRyaVBarFf8MR3zHGqVdvM4cLbDXqPhxqvegZShgfxb8kw9C7YhAQ==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.4.0.tgz",
"integrity": "sha512-q5q26X/yNKjbzrRdVVDIM9KEmN4dhezmhyliCDIn8mPGT0AlfzOqQfZ5iNCGRCEHSPd86oUdhpNpuzAkEZ5LQg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^10.3.0",
+1 -1
View File
@@ -8,7 +8,7 @@
"bun:start": "bun run dist/index.js"
},
"dependencies": {
"@google/genai": "^1.50.1",
"@google/genai": "^2.4.0",
"@libsql/client": "^0.17.3",
"@mistralai/mistralai": "^1.15.1",
"@napi-rs/canvas": "^0.1.100",
+10 -4
View File
@@ -9,7 +9,7 @@ import {
collectReplyChainText,
escapeMarkdownV2Text,
logError,
oldReplyToMessage,
oldReplyToMessage, replyToMessage,
startIntervalEditor
} from "../util/utils";
import {ChatCommand} from "../base/chat-command";
@@ -44,7 +44,10 @@ export class GeminiChat extends ChatCommand {
};
});
chatMessages.reverse();
chatMessages.unshift({role: "system", content: Environment.SYSTEM_PROMPT});
if (Environment.SYSTEM_PROMPT) {
chatMessages.unshift({role: "system", content: Environment.SYSTEM_PROMPT});
}
let chatContent = "";
for (const part of chatMessages) {
@@ -105,7 +108,7 @@ export class GeminiChat extends ChatCommand {
chat_id: chatId,
message_id: waitMessage.message_id,
text: escapeMarkdownV2Text(text),
parse_mode: "Markdown"
parse_mode: "MarkdownV2"
}
).catch(logError);
@@ -164,7 +167,10 @@ export class GeminiChat extends ChatCommand {
waitMessage.reply_to_message = msg;
waitMessage.text = currentText;
await MessageStore.put(waitMessage);
await oldReplyToMessage(waitMessage, `⏱️ ${diff}s`);
if (Environment.SEND_TIME_TOOK) {
await replyToMessage({message: waitMessage, text: `⏱️ ${diff}s`});
}
}
} catch (error) {
logError(error);
+8 -3
View File
@@ -58,7 +58,10 @@ export class MistralChat extends ChatCommand {
};
});
chatMessages.reverse();
chatMessages.unshift({role: "system", content: [{type: "text", text: Environment.SYSTEM_PROMPT}]});
if (Environment.SYSTEM_PROMPT) {
chatMessages.unshift({role: "system", content: [{type: "text", text: Environment.SYSTEM_PROMPT}]});
}
let waitMessage: Message;
@@ -116,7 +119,7 @@ export class MistralChat extends ChatCommand {
chat_id: chatId,
message_id: waitMessage.message_id,
text: escapeMarkdownV2Text(text),
parse_mode: "Markdown"
parse_mode: "MarkdownV2"
}
).catch(logError);
@@ -165,7 +168,9 @@ export class MistralChat extends ChatCommand {
waitMessage.reply_to_message = msg;
waitMessage.text = currentText;
await MessageStore.put(waitMessage);
await oldReplyToMessage(waitMessage, `⏱️ ${diff}s`);
if (Environment.SEND_TIME_TOOK) {
await replyToMessage({message: waitMessage, text: `⏱️ ${diff}s`});
}
}
} catch (error) {
logError(error);
+9 -3
View File
@@ -44,7 +44,10 @@ export class OllamaChat extends ChatCommand {
};
});
chatMessages.reverse();
chatMessages.unshift({role: "system", content: Environment.SYSTEM_PROMPT, images: []});
if (Environment.SYSTEM_PROMPT) {
chatMessages.unshift({role: "system", content: Environment.SYSTEM_PROMPT, images: []});
}
let waitMessage: Message;
@@ -140,7 +143,7 @@ export class OllamaChat extends ChatCommand {
chat_id: chatId,
message_id: waitMessage.message_id,
text: escapeMarkdownV2Text(text),
parse_mode: "Markdown",
parse_mode: "MarkdownV2",
reply_markup: cancelMarkup
}).catch(logError);
@@ -217,7 +220,10 @@ export class OllamaChat extends ChatCommand {
waitMessage.reply_to_message = msg;
waitMessage.text = currentText;
await MessageStore.put(waitMessage);
await oldReplyToMessage(waitMessage, `⏱️ ${diff}s`);
if (Environment.SEND_TIME_TOOK) {
await replyToMessage({message: waitMessage, text: `⏱️ ${diff}s`});
}
break;
}
}
+13 -7
View File
@@ -58,11 +58,14 @@ export class OpenAIChat extends ChatCommand {
};
});
chatMessages.reverse();
chatMessages.unshift({
role: "system",
content: [{type: "input_text", text: Environment.SYSTEM_PROMPT}],
type: "message"
});
if (Environment.SYSTEM_PROMPT) {
chatMessages.unshift({
role: "system",
content: [{type: "input_text", text: Environment.SYSTEM_PROMPT}],
type: "message"
});
}
let waitMessage: Message;
@@ -97,7 +100,7 @@ export class OpenAIChat extends ChatCommand {
chat_id: chatId,
message_id: waitMessage.message_id,
text: escapeMarkdownV2Text(text),
parse_mode: "Markdown"
parse_mode: "MarkdownV2"
}
).catch(logError);
@@ -148,7 +151,10 @@ export class OpenAIChat extends ChatCommand {
waitMessage.reply_to_message = msg;
waitMessage.text = currentText;
await MessageStore.put(waitMessage);
await replyToMessage({message: waitMessage, text: `⏱️ ${diff}s`});
if (Environment.SEND_TIME_TOOK) {
await replyToMessage({message: waitMessage, text: `⏱️ ${diff}s`});
}
}
} catch (error) {
logError(error);
+6 -1
View File
@@ -37,6 +37,7 @@ export class Environment {
static IMAGE_HANDLE_FALLBACK_POLICY: ImageHandleFallbackPolicy;
static SYSTEM_PROMPT?: string;
static SEND_TIME_TOOK: boolean;
static OLLAMA_ADDRESS?: string;
static OLLAMA_MODEL?: string;
@@ -112,7 +113,7 @@ export class Environment {
Environment.IMAGE_HANDLE_FALLBACK_POLICY = ImageHandleFallbackPolicy.NOTIFY_USER;
}
Environment.SYSTEM_PROMPT = process.env.SYSTEM_PROMPT?.trim();
Environment.SEND_TIME_TOOK = ifTrue(process.env.SEND_TOOK_TIME || false);
Environment.OLLAMA_ADDRESS = process.env.OLLAMA_ADDRESS;
Environment.OLLAMA_MODEL = process.env.OLLAMA_MODEL || "gemma3:4b";
@@ -133,6 +134,10 @@ export class Environment {
Environment.OPENAI_IMAGE_MODEL = process.env.OPENAI_IMAGE_MODEL || "gpt-image-1-mini";
}
static setSystemPrompt(prompt: string) {
this.SYSTEM_PROMPT = prompt;
}
static setAdmins(admins: Set<number>) {
this.ADMIN_IDS = admins;
}
+14
View File
@@ -2,6 +2,7 @@ import * as fs from "fs";
import {Environment} from "../common/environment";
import {logError} from "../util/utils";
import {Answers} from "../model/answers";
import path from "node:path";
type DataJsonFile = {
admins: number[]
@@ -27,6 +28,19 @@ export async function readData(): Promise<void> {
}
}
export async function readPrompts(): Promise<void> {
try {
const prompt = fs.readFileSync(path.join(Environment.DATA_PATH, "system_prompt.txt")).toString().trim();
if (prompt.length) {
Environment.setSystemPrompt(prompt);
}
} catch (e) {
logError(e);
}
return Promise.resolve();
}
export async function saveData(): Promise<void> {
const adminIds: number[] = [];
Environment.ADMIN_IDS.forEach(id => adminIds.push(id));
+5 -1
View File
@@ -20,7 +20,7 @@ import {Ping} from "./commands/ping";
import {RandomString} from "./commands/random-string";
import {SystemInfo} from "./commands/system-info";
import {Test} from "./commands/test";
import {readData, retrieveAnswers} from "./db/database";
import {readData, readPrompts, retrieveAnswers} from "./db/database";
import {Uptime} from "./commands/uptime";
import {WhatBetter} from "./commands/what-better";
import {When} from "./commands/when";
@@ -252,6 +252,10 @@ async function shutdown(signal: NodeJS.Signals) {
async function main() {
const start = Date.now();
await readPrompts();
console.log(Environment.SYSTEM_PROMPT);
console.log(
`TEST_ENVIRONMENT: ${Environment.TEST_ENVIRONMENT}\n` +
`DATA_PATH: ${Environment.DATA_PATH}\n` +
+229 -5
View File
@@ -312,6 +312,8 @@ export async function sendMessage(options: SendOptions): Promise<Message> {
reply_markup: options.reply_markup,
});
await MessageStore.put(response);
return Promise.resolve(response);
}
@@ -338,6 +340,8 @@ export async function replyToMessage(options: SendOptions): Promise<Message> {
link_preview_options: options.link_preview_options
});
await MessageStore.put(response);
return Promise.resolve(response);
}
@@ -452,14 +456,230 @@ export const delay = (ms: number, signal?: AbortSignal): Promise<void> =>
}
});
export function escapeMarkdownV2Text(s: string) {
s = s.replace(/^\*{3,}\s*$/gm, "— — —");
s = s.replace(/^\*\s+(?=\S)/gm, "• ");
s = s.replace(/\*\*(.+?)\*\*/g, "*$1*");
const MARKDOWN_V2_RESERVED_RE = /([\\_*\[\]()~`>#+\-=|{}.!])/g;
function escapePlainMarkdownV2(s: string): string {
return s.replace(MARKDOWN_V2_RESERVED_RE, "\\$1");
}
function escapeCodeMarkdownV2(s: string): string {
return s.replace(/[\\`]/g, "\\$&");
}
function escapeLinkUrlMarkdownV2(s: string): string {
return s.replace(/[\\)]/g, "\\$&");
}
function escapeMarkdownV2PreservingAllowedFormatting(s: string): string {
let result = "";
let i = 0;
while (i < s.length) {
// links: [text](url)
if (s[i] === "[") {
const linkMatch = s.slice(i).match(/^\[([^\]\n]+)]\(([^)\n]+)\)/);
if (linkMatch) {
const [, text, url] = linkMatch;
result += `[${escapePlainMarkdownV2(text)}](${escapeLinkUrlMarkdownV2(url)})`;
i += linkMatch[0].length;
continue;
}
}
// monospace: `text`
if (s[i] === "`") {
const end = s.indexOf("`", i + 1);
if (end !== -1) {
const content = s.slice(i + 1, end);
result += "`" + escapeCodeMarkdownV2(content) + "`";
i = end + 1;
continue;
}
}
// spoiler: ||text||
if (s.startsWith("||", i)) {
const end = s.indexOf("||", i + 2);
if (end !== -1) {
const content = s.slice(i + 2, end);
result += "||" + escapeMarkdownV2PreservingAllowedFormatting(content) + "||";
i = end + 2;
continue;
}
}
// underline: __text__
if (s.startsWith("__", i)) {
const end = s.indexOf("__", i + 2);
if (end !== -1) {
const content = s.slice(i + 2, end);
result += "__" + escapeMarkdownV2PreservingAllowedFormatting(content) + "__";
i = end + 2;
continue;
}
}
// bold: *text*
if (s[i] === "*") {
const end = s.indexOf("*", i + 1);
if (end !== -1) {
const content = s.slice(i + 1, end);
result += "*" + escapeMarkdownV2PreservingAllowedFormatting(content) + "*";
i = end + 1;
continue;
}
}
// italic: _text_
if (s[i] === "_") {
const end = s.indexOf("_", i + 1);
if (end !== -1) {
const content = s.slice(i + 1, end);
result += "_" + escapeMarkdownV2PreservingAllowedFormatting(content) + "_";
i = end + 1;
continue;
}
}
// strikethrough: ~text~
if (s[i] === "~") {
const end = s.indexOf("~", i + 1);
if (end !== -1) {
const content = s.slice(i + 1, end);
result += "~" + escapeMarkdownV2PreservingAllowedFormatting(content) + "~";
i = end + 1;
continue;
}
}
result += escapePlainMarkdownV2(s[i]);
i++;
}
return result;
}
function unescapeAccidentalMarkdownV2(s: string): string {
let prev: string;
do {
prev = s;
s = s.replace(/\\([_*\[\]()~`>#+\-=|{}.!\\])/g, "$1");
} while (s !== prev);
return s;
}
function escapeTelegramQuoteLine(line: string): string {
const content = line.replace(/^>\s*/, "");
if (!content.trim()) {
return ">";
}
return ">" + escapeMarkdownV2PreservingAllowedFormatting(content);
}
function normalizeTelegramQuoteLines(s: string): string {
return s
.split("\n")
.map(line => {
if (!line.startsWith(">")) return line;
return line.replace(/^>\s+/, ">");
})
.join("\n");
}
function looksLikeMarkdownTableRow(line: string): boolean {
const trimmed = line.trim();
if (trimmed.startsWith("||") && trimmed.endsWith("||")) {
return false;
}
const pipeCount = (trimmed.match(/\|/g) ?? []).length;
if (pipeCount < 2) {
return false;
}
return trimmed.startsWith("|") || trimmed.endsWith("|") || pipeCount >= 2;
}
function isMarkdownTableSeparator(line: string): boolean {
return /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
}
function normalizeMarkdownTables(s: string): string {
return s
.split("\n")
.filter(line => !isMarkdownTableSeparator(line))
.map(line => {
if (!looksLikeMarkdownTableRow(line)) {
return line;
}
return line
.replace(/^\s*\|/, "")
.replace(/\|\s*$/, "")
.split("|")
.map(cell => cell.trim())
.filter(Boolean)
.join(" — ");
})
.join("\n");
}
export function escapeMarkdownV2Text(s: string): string {
s = unescapeAccidentalMarkdownV2(s);
s = normalizeTelegramQuoteLines(s);
s = s.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
s = s.replace(/^\s*[-*_]{3,}\s*$/gm, "— — —");
s = s.replace(/^\s*[-*+]\s+(?=\S)/gm, "• ");
s = s.replace(/\*\*(.+?)\*\*/gs, "*$1*");
s = s.replace(/~~(.+?)~~/gs, "~$1~");
s = s.replace(/^#{1,6}\s+/gm, "");
s = s.replace(/```[a-zA-Z0-9_-]*\n?([\s\S]*?)```/g, (_, code) => {
return code.trim();
});
s = s.replace(/!\[([^\]]*)]\(([^)]+)\)/g, (_, alt, url) => {
return alt ? `${alt}: ${url}` : url;
});
s = normalizeMarkdownTables(s);
s = s
.split("\n")
.map(line => {
if (line.startsWith(">")) {
return escapeTelegramQuoteLine(line);
}
if (line === ">") {
return ">";
}
return escapeMarkdownV2PreservingAllowedFormatting(line);
})
.join("\n");
s = s.replace(/\n{3,}/g, "\n\n");
return s.trim();
}
export async function getFileUrl(fileId: string): Promise<string> {
const file = await bot.getFile({file_id: fileId});
return `https://api.telegram.org/file/bot${bot.botToken}/${file.file_path}`;
@@ -517,6 +737,8 @@ export function cutPrefixes(msg: Message | StoredMessage | string): string {
});
const text = extractTextMessage(msg);
if (!text || !text.length) return "";
let newText = text;
for (const prefix of prefixes) {
@@ -1215,7 +1437,9 @@ export async function processNewMessage(msg: Message): Promise<void> {
const textToCheck = startsWithPrefix ? messageWithoutPrefix : cmdText;
if (Environment.PROCESS_LINKS && await processYouTubeLink(msg, getFirstLink(msg))) return;
if (!startsWithPrefix && msg.chat.type !== "private") return;
if (msg.chat.type !== "private" && (!msg.reply_to_message || msg.reply_to_message.from.id !== botUser.id) && !startsWithPrefix) return;
if (msg.chat.type === "private" && !Environment.ADMIN_IDS.has(msg.chat.id)) return;
switch (Environment.DEFAULT_AI_PROVIDER) {