Compare commits
7 Commits
27ca75ed7c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 89b89c6dcf | |||
| 13af873ec2 | |||
| d95c37a322 | |||
| a7fcb8074c | |||
| c003d443e3 | |||
| 874f312465 | |||
| d41a4ed3ea |
@@ -0,0 +1,47 @@
|
||||
name: TypeScript Bot CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: gitea.mlgt.ru
|
||||
IMAGE_OWNER: ${{ gitea.repository_owner }}
|
||||
IMAGE_NAME: tg-chat-bot
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: node26-docker
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ gitea.ref_name == 'master' }}
|
||||
type=ref,event=branch
|
||||
type=sha,prefix=sha-
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.IMAGE_NAME }}:buildcache
|
||||
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user