refactor!: rewrite bot core; add AI (Ollama, Gemini), DB, new commands
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import {CallbackQuery, InlineKeyboardButton} from "typescript-telegram-bot-api";
|
||||
import {Requirements} from "./requirements";
|
||||
import {bot} from "../index";
|
||||
|
||||
export abstract class CallbackCommand {
|
||||
|
||||
abstract text: string;
|
||||
abstract data: string;
|
||||
requirements?: Requirements = null;
|
||||
|
||||
abstract execute(query: CallbackQuery): Promise<void>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
afterExecute(query: CallbackQuery): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected getOptions(query: CallbackQuery): AnswerCallbackQueryOptions {
|
||||
return {callback_query_id: query.id};
|
||||
}
|
||||
|
||||
async answerCallbackQuery(query: CallbackQuery): Promise<void> {
|
||||
bot.answerCallbackQuery(this.getOptions(query)).catch(console.error);
|
||||
}
|
||||
|
||||
asButton(): InlineKeyboardButton {
|
||||
return {
|
||||
text: this.text,
|
||||
callback_data: this.data
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface AnswerCallbackQueryOptions {
|
||||
callback_query_id: string;
|
||||
text?: string;
|
||||
show_alert?: boolean;
|
||||
url?: string;
|
||||
cache_time?: number;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {Requirements} from "./requirements";
|
||||
|
||||
export abstract class ChatCommand {
|
||||
|
||||
abstract regexp: RegExp;
|
||||
requirements?: Requirements = null;
|
||||
title?: string;
|
||||
description?: string;
|
||||
|
||||
abstract execute(
|
||||
msg: Message,
|
||||
match?: RegExpExecArray
|
||||
): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export abstract class Dao<I> {
|
||||
abstract getAll(): Promise<I[]>;
|
||||
|
||||
abstract getById(params: never): Promise<I | null>
|
||||
|
||||
abstract getByIds(params: never): Promise<I[]>
|
||||
|
||||
abstract insert(items: never[]): Promise<true>
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum Requirement {
|
||||
BOT_CREATOR,
|
||||
BOT_ADMIN,
|
||||
BOT_CHAT_ADMIN,
|
||||
CHAT,
|
||||
REPLY
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {Requirement} from "./requirement";
|
||||
|
||||
export class Requirements {
|
||||
requirements: Requirement[] = [];
|
||||
|
||||
private constructor(requirements?: Requirement[]) {
|
||||
this.requirements = requirements;
|
||||
}
|
||||
|
||||
static Build(...requirements: Requirement[]): Requirements {
|
||||
return new Requirements(requirements);
|
||||
}
|
||||
|
||||
isRequiresBotCreator(): boolean {
|
||||
return this.requirements.includes(Requirement.BOT_CREATOR);
|
||||
}
|
||||
|
||||
isRequiresBotAdmin(): boolean {
|
||||
return this.requirements.includes(Requirement.BOT_ADMIN);
|
||||
}
|
||||
|
||||
isRequiresBotChatAdmin(): boolean {
|
||||
return this.requirements.includes(Requirement.BOT_CHAT_ADMIN);
|
||||
}
|
||||
|
||||
isRequiresChat(): boolean {
|
||||
return this.requirements.includes(Requirement.CHAT);
|
||||
}
|
||||
|
||||
isRequiresReply(): boolean {
|
||||
return this.requirements.includes(Requirement.REPLY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {fullName, logError, oldSendMessage} from "../util/utils";
|
||||
import {Environment} from "../common/environment";
|
||||
import {botUser} from "../index";
|
||||
|
||||
export class AdminsAdd extends ChatCommand {
|
||||
regexp = /^\/addadmin/i;
|
||||
title = "/addAdmin";
|
||||
description = "Add user to admins";
|
||||
|
||||
requirements = Requirements.Build(
|
||||
Requirement.BOT_CREATOR,
|
||||
Requirement.REPLY,
|
||||
Requirement.CHAT
|
||||
);
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
if (!msg.reply_to_message) return;
|
||||
|
||||
const id = msg.reply_to_message.from.id;
|
||||
const text = fullName(msg.reply_to_message.from);
|
||||
|
||||
if (id === botUser.id) {
|
||||
await oldSendMessage(msg, "Бот не может сам себя сделать админом").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (id === Environment.CREATOR_ID) {
|
||||
await oldSendMessage(msg, "Создатель бота и так является админом").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await Environment.addAdmin(id)) {
|
||||
await oldSendMessage(msg, text + " теперь админ!").catch(logError);
|
||||
} else {
|
||||
await oldSendMessage(msg, text + " и так уже админ 🤔").catch(logError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {fullName, logError, oldSendMessage} from "../util/utils";
|
||||
import {Environment} from "../common/environment";
|
||||
import {botUser} from "../index";
|
||||
|
||||
export class AdminsRemove extends ChatCommand {
|
||||
regexp = /^\/removeadmin/i;
|
||||
title = "/removeAdmin";
|
||||
description = "Remove user from admins";
|
||||
|
||||
requirements = Requirements.Build(
|
||||
Requirement.BOT_ADMIN,
|
||||
Requirement.REPLY,
|
||||
Requirement.CHAT,
|
||||
);
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
if (!msg.reply_to_message) return;
|
||||
|
||||
const id = msg.reply_to_message.from.id;
|
||||
const text = fullName(msg.reply_to_message.from);
|
||||
|
||||
if (id === botUser.id) {
|
||||
await oldSendMessage(msg, "Бот не может сам себя убрать из админов").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (id === Environment.CREATOR_ID) {
|
||||
await oldSendMessage(msg, "Создатель бота не может перестать быть админом").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await Environment.removeAdmin(id)) {
|
||||
await oldSendMessage(msg, text + " больше не админ!").catch(logError);
|
||||
} else {
|
||||
await oldSendMessage(msg, text + " и так не был админом 🤔").catch(logError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {errorPlaceholder, logError, oldSendMessage} from "../util/utils";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
|
||||
export class Ae implements ChatCommand {
|
||||
regexp = /^\/ae\s([^]+)/i;
|
||||
title = "/ae";
|
||||
description = "evaluation";
|
||||
|
||||
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
||||
|
||||
async execute(msg: Message, params: string[]) {
|
||||
const match = params[1];
|
||||
|
||||
try {
|
||||
let e = eval(match);
|
||||
|
||||
e = ((typeof e == "string") ? e : JSON.stringify(e));
|
||||
|
||||
await oldSendMessage(msg, e).catch(async () => await errorPlaceholder(msg));
|
||||
} catch (e) {
|
||||
const text = e.message.toString();
|
||||
|
||||
if (text.includes("is not defined")) {
|
||||
await oldSendMessage(msg, "variable is not defined").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`${text}
|
||||
* Stacktrace: ${e.stack}`);
|
||||
|
||||
await oldSendMessage(msg, text).catch(logError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {bot, botUser} from "../index";
|
||||
import {fullName, logError, oldSendMessage, replyToMessage} from "../util/utils";
|
||||
import {Environment} from "../common/environment";
|
||||
|
||||
export class Ban extends ChatCommand {
|
||||
regexp = /^\/ban/i;
|
||||
title = "/ban [reply]";
|
||||
description = "ban user from chat";
|
||||
|
||||
requirements = Requirements.Build(
|
||||
Requirement.CHAT,
|
||||
Requirement.BOT_CHAT_ADMIN,
|
||||
Requirement.REPLY,
|
||||
Requirement.BOT_ADMIN
|
||||
);
|
||||
|
||||
async execute(msg: Message) {
|
||||
if (!msg.reply_to_message) return;
|
||||
|
||||
const user = msg.reply_to_message.from;
|
||||
const userId = user.id;
|
||||
|
||||
if (userId === botUser.id) {
|
||||
await replyToMessage(msg, "Используй /leave").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (userId === Environment.CREATOR_ID) {
|
||||
await replyToMessage(msg, "Бот не будет банить своего создателя.").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.from.id !== Environment.CREATOR_ID && Environment.ADMIN_IDS.has(userId)) {
|
||||
await replyToMessage(msg, "Бот не будет банить своих администраторов.").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
bot.banChatMember({chat_id: msg.chat.id, user_id: userId})
|
||||
.then(async () => {
|
||||
await oldSendMessage(msg, `${fullName(user)} забанен 🚫`).catch(logError);
|
||||
})
|
||||
.catch(async () => {
|
||||
await oldSendMessage(msg, `Не смог забанить ${fullName(user)} ☹️`).catch(logError);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {MessageStore} from "../common/message-store";
|
||||
import {logError, sendMessage} from "../util/utils";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
|
||||
export class CacheClear extends ChatCommand {
|
||||
regexp = /^\/clearcache$/i;
|
||||
|
||||
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
const size = MessageStore.all().size;
|
||||
MessageStore.clear();
|
||||
await sendMessage({chatId: msg.chat.id, text: `Было удалено сообщений: ${size}`}).catch(logError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {logError, sendMessage} from "../util/utils";
|
||||
import {MessageStore} from "../common/message-store";
|
||||
|
||||
export class CacheSize extends ChatCommand {
|
||||
regexp = /^\/cachesize$/i;
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
const cacheSize = MessageStore.all();
|
||||
|
||||
await sendMessage({
|
||||
chatId: msg.chat.id,
|
||||
text: `Количество сохранённых сообщений: ${cacheSize.size}`
|
||||
}).catch(logError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {logError, randomValue, replyToMessage} from "../util/utils";
|
||||
|
||||
export class Choice extends ChatCommand {
|
||||
regexp = /^\/choice\b\s*(.*)$/i;
|
||||
title = "/choice a, b, ..., c";
|
||||
description = "Выбор случайного значения";
|
||||
|
||||
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
|
||||
console.log("match", match);
|
||||
|
||||
const payload = match[1];
|
||||
|
||||
const re =
|
||||
/\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^,]+?))\s*(?:,|$)/g;
|
||||
|
||||
const out: string[] = [];
|
||||
for (const mm of payload.matchAll(re)) {
|
||||
const raw = (mm[1] ?? mm[2] ?? mm[3] ?? "").trim();
|
||||
|
||||
const val = raw
|
||||
.replace(/\\n/g, "\n")
|
||||
.replace(/\\r/g, "\r")
|
||||
.replace(/\\t/g, "\t")
|
||||
.replace(/\\"/g, "\"")
|
||||
.replace(/\\'/g, "'")
|
||||
.replace(/\\\\/g, "\\");
|
||||
|
||||
if (val.length) out.push(val);
|
||||
}
|
||||
|
||||
const random = randomValue(out);
|
||||
|
||||
await replyToMessage(msg, `Выбрал *${random}*`, "Markdown").catch(logError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {getRangedRandomInt, logError, replyToMessage} from "../util/utils";
|
||||
|
||||
export class Coin extends ChatCommand {
|
||||
regexp = /^\/coin$/i;
|
||||
title = "/coin";
|
||||
description = "Heads or tails";
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
const random = getRangedRandomInt(0, 2);
|
||||
const headsOrTails = random === 1 ? "Выпал *Орёл* 🪙" : "Выпала *Решка* 🪙";
|
||||
await replyToMessage(msg, headsOrTails, "Markdown").catch(logError); }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {logError, randomValue} from "../util/utils";
|
||||
import {bot} from "../index";
|
||||
|
||||
type DiceEmoji = "🎲" | "🎯" | "🏀" | "⚽" | "🎳" | "🎰";
|
||||
const emojis = ["🎲", "🎯", "🏀", "⚽", "🎳", "🎰"];
|
||||
|
||||
export class Dice extends ChatCommand {
|
||||
regexp = /^\/dice/i;
|
||||
title = "/dice [emoji]";
|
||||
description = "Sends random or specific dice";
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
const split = msg.text.split("/dice ");
|
||||
const secondPart = split[1]?.trim();
|
||||
const emojiIndex = emojis.indexOf(secondPart);
|
||||
const emojiToDice: DiceEmoji = (emojiIndex >= 0 ? emojis[emojiIndex] : randomValue(emojis)) as DiceEmoji;
|
||||
|
||||
await bot.sendDice({
|
||||
chat_id: msg.chat.id,
|
||||
emoji: emojiToDice,
|
||||
reply_parameters: {
|
||||
message_id: msg.message_id
|
||||
}
|
||||
}).catch(logError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {downloadTelegramFile, extractImageFileId, logError, replyToMessage, waveDistortSharp} from "../util/utils";
|
||||
import {bot} from "../index";
|
||||
|
||||
export class Distort extends ChatCommand {
|
||||
regexp = /^\/distort(?:@[\w_]+)?(?:\s+(\d+))?(?:\s+(\d+))?\s*$/i;
|
||||
title = "/distort [amp] [wavelength]";
|
||||
description = "Distortion of picture";
|
||||
|
||||
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
const reply = msg.reply_to_message;
|
||||
if (!reply) {
|
||||
await replyToMessage(
|
||||
msg,
|
||||
"Ответь командой /distort на сообщение с картинкой (фото, документ или стикер).\n" + "Пример: /distort 16 80"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileId = extractImageFileId(reply);
|
||||
if (!fileId) {
|
||||
await replyToMessage(
|
||||
msg,
|
||||
"В реплае не вижу картинку. Пришли фото или файл-изображение."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const amp = match?.[1] ? parseInt(match[1], 10) : 14;
|
||||
const wavelength = match?.[2] ? parseInt(match[2], 10) : 72;
|
||||
|
||||
try {
|
||||
await bot.sendChatAction({chat_id: chatId, action: "upload_photo"});
|
||||
|
||||
const file = await bot.getFile({file_id: fileId});
|
||||
if (!file.file_path) throw new Error("No file_path in Telegram getFile response");
|
||||
|
||||
const inputBuf = await downloadTelegramFile(file.file_path);
|
||||
|
||||
const outBuf = await waveDistortSharp(inputBuf, amp, wavelength);
|
||||
|
||||
await bot.sendPhoto({
|
||||
chat_id: chatId,
|
||||
photo: outBuf,
|
||||
caption: `Искажение готово ✅ (amp=${amp}, wavelength=${wavelength})`,
|
||||
});
|
||||
} catch (e) {
|
||||
await replyToMessage(
|
||||
msg, `Не получилось исказить изображение: ${e?.message ?? String(e)}`
|
||||
).catch(logError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {
|
||||
collectReplyChainText,
|
||||
editMessageText,
|
||||
escapeMarkdownV2Text,
|
||||
logError,
|
||||
replyToMessage,
|
||||
startIntervalEditor
|
||||
} from "../util/utils";
|
||||
import {Environment} from "../common/environment";
|
||||
import {bot, googleAi} from "../index";
|
||||
import {MessageStore} from "../common/message-store";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {ApiError} from "@google/genai";
|
||||
|
||||
export class GeminiChat extends ChatCommand {
|
||||
regexp = /^\/gemini\s([^]+)/i;
|
||||
title = "/gemini";
|
||||
description = "Chat with AI (Gemini)";
|
||||
|
||||
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
||||
|
||||
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
|
||||
console.log("match", match);
|
||||
return this.executeGemini(msg, match?.[1]);
|
||||
}
|
||||
|
||||
async executeGemini(msg: Message, text: string): Promise<void> {
|
||||
if (!text || text.trim().length === 0) return;
|
||||
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
const messageParts = await collectReplyChainText(msg, "/gemini");
|
||||
console.log("MESSAGE PARTS", messageParts);
|
||||
|
||||
const chatMessages = messageParts.map(part => {
|
||||
return {
|
||||
role: part.bot ? "ASSISTANT" : "USER",
|
||||
content: part.content
|
||||
};
|
||||
});
|
||||
chatMessages.reverse();
|
||||
chatMessages.unshift({role: "SYSTEM", content: Environment.SYSTEM_PROMPT});
|
||||
|
||||
let chatContent = "";
|
||||
for (const part of chatMessages) {
|
||||
chatContent += `${part.role.toUpperCase()}:\n${part.content}\n\n`;
|
||||
}
|
||||
|
||||
chatContent = chatContent.trim();
|
||||
|
||||
let waitMessage: Message;
|
||||
|
||||
const startTime = new Date().getSeconds();
|
||||
|
||||
try {
|
||||
waitMessage = await bot.sendMessage({
|
||||
chat_id: chatId,
|
||||
text: Environment.waitText,
|
||||
reply_parameters: {
|
||||
chat_id: chatId,
|
||||
message_id: msg.message_id
|
||||
}
|
||||
});
|
||||
|
||||
const stream = await googleAi.models.generateContentStream({
|
||||
model: "gemini-2.5-flash",
|
||||
contents: chatContent,
|
||||
});
|
||||
|
||||
let messageText = "";
|
||||
let shouldBreak = false;
|
||||
let diff = 0;
|
||||
|
||||
const editor = startIntervalEditor({
|
||||
intervalMs: 4500,
|
||||
getText: () => messageText,
|
||||
editFn: async (text) => {
|
||||
await editMessageText(chatId, waitMessage.message_id, escapeMarkdownV2Text(text), "Markdown");
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
const text = chunk.text;
|
||||
|
||||
const length = (messageText + text).length;
|
||||
if (length > 4096) {
|
||||
messageText = messageText.slice(0, 4093) + "...";
|
||||
shouldBreak = true;
|
||||
} else {
|
||||
messageText += text;
|
||||
}
|
||||
|
||||
if (shouldBreak) {
|
||||
console.log("messageText", messageText);
|
||||
console.log("length", length);
|
||||
console.log("break", true);
|
||||
|
||||
diff = Math.abs(new Date().getSeconds() - startTime);
|
||||
await editor.tick();
|
||||
await editor.stop();
|
||||
break;
|
||||
}
|
||||
|
||||
console.log("messageText", messageText);
|
||||
console.log("length", messageText.length);
|
||||
|
||||
diff = Math.abs(new Date().getSeconds() - startTime);
|
||||
}
|
||||
} finally {
|
||||
await editor.tick();
|
||||
await editor.stop();
|
||||
|
||||
console.log("time", diff);
|
||||
console.log("ended", true);
|
||||
|
||||
waitMessage.reply_to_message = msg;
|
||||
waitMessage.text = messageText;
|
||||
MessageStore.put(waitMessage);
|
||||
|
||||
await replyToMessage(waitMessage, `⏱️ ${diff}s`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
if (error instanceof ApiError) {
|
||||
if (error.status === 429) {
|
||||
await replyToMessage(waitMessage, "На сегодня всё, лимиты закончились.").catch(logError);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await replyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {chatCommandToString, delay, logError, sendMessage} from "../util/utils";
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {chatCommands} from "../index";
|
||||
import {TelegramError} from "typescript-telegram-bot-api/dist/errors";
|
||||
|
||||
export class Help implements ChatCommand {
|
||||
regexp = /^\/(h|help)/i;
|
||||
title = "/help";
|
||||
description = "Show list of commands";
|
||||
|
||||
async execute(msg: Message) {
|
||||
let text = "Commands:\n\n";
|
||||
|
||||
chatCommands.forEach(c => {
|
||||
text += `${chatCommandToString(c)}\n`;
|
||||
});
|
||||
|
||||
await sendMessage({chatId: msg.from.id, text: text})
|
||||
.then(async () => {
|
||||
if (msg.chat.type !== "private") {
|
||||
await sendMessage({message: msg, text: "Отправил команды в ЛС 😎"}).catch(logError);
|
||||
}
|
||||
})
|
||||
.catch(async (e) => {
|
||||
if (e instanceof TelegramError) {
|
||||
if (e.response?.error_code === 403) {
|
||||
await sendMessage({
|
||||
message: msg,
|
||||
text: "Не смог отправить команды в ЛС ☹️\nТогда отправлю сюда"
|
||||
}).catch(logError);
|
||||
|
||||
await delay(1000);
|
||||
await sendMessage({message: msg, text: text}).catch(logError);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {logError, replyToMessage} from "../util/utils";
|
||||
|
||||
export class Id extends ChatCommand {
|
||||
regexp = /^\/id/i;
|
||||
title = "/id";
|
||||
description = "ID of chat, user and reply (if replied to any message)";
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
let text = `chat id: \n\`\`\`${msg.chat.id}\`\`\` \nfrom id: \n\`\`\`${msg.from.id}\`\`\``;
|
||||
if (msg.reply_to_message) {
|
||||
text += ` \nreply id: \n\`\`\`${msg.reply_to_message.from.id}\`\`\``;
|
||||
}
|
||||
|
||||
await replyToMessage(msg, text, "MarkdownV2").catch(logError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {bot} from "../index";
|
||||
|
||||
export class Leave extends ChatCommand {
|
||||
regexp = /^\/leave/i;
|
||||
title = "/leave";
|
||||
description = "Bot will leave current chat";
|
||||
|
||||
requirements = Requirements.Build(Requirement.BOT_ADMIN, Requirement.CHAT);
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
await bot.leaveChat({chat_id: msg.chat.id});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import {addMute} from "../db/database";
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {fullName, logError, oldSendMessage} from "../util/utils";
|
||||
import {botUser} from "../index";
|
||||
import {Environment} from "../common/environment";
|
||||
|
||||
export class Mute implements ChatCommand {
|
||||
regexp = /^\/mute/i;
|
||||
title = "/mute";
|
||||
description = "Bot will ignore user";
|
||||
|
||||
requirements = Requirements.Build(Requirement.BOT_ADMIN, Requirement.REPLY);
|
||||
|
||||
async execute(msg: Message) {
|
||||
if (!msg.reply_to_message) return;
|
||||
|
||||
const id = msg.reply_to_message.from.id;
|
||||
const text = fullName(msg.reply_to_message.from);
|
||||
|
||||
if (id === botUser.id) {
|
||||
await oldSendMessage(msg, "Бот не может сам себя игнорировать").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (id === Environment.CREATOR_ID) {
|
||||
await oldSendMessage(msg, "Бот не будет игнорировать своего создателя").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await addMute(id)) {
|
||||
await oldSendMessage(msg, text + " в муте! 🔇").catch(logError);
|
||||
} else {
|
||||
await oldSendMessage(msg, text + " уже в муте 🤔").catch(logError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {bot, ollama} from "../index";
|
||||
import {
|
||||
collectReplyChainText,
|
||||
editMessageText,
|
||||
escapeMarkdownV2Text,
|
||||
extractText,
|
||||
logError,
|
||||
replyToMessage,
|
||||
startIntervalEditor
|
||||
} from "../util/utils";
|
||||
import {Environment} from "../common/environment";
|
||||
import {MessageStore} from "../common/message-store";
|
||||
|
||||
export class OllamaChat extends ChatCommand {
|
||||
regexp = /^\/ollama\s([^]+)/;
|
||||
title = "/ollama";
|
||||
description = "talk to AI (Ollama)";
|
||||
|
||||
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
|
||||
console.log("match", match);
|
||||
return this.executeOllama(msg, match?.[1]);
|
||||
}
|
||||
|
||||
async executeOllama(msg: Message, text: string): Promise<void> {
|
||||
if (!text || text.trim().length === 0) return;
|
||||
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
const messageParts = await collectReplyChainText(msg);
|
||||
console.log("MESSAGE PARTS", messageParts);
|
||||
|
||||
const chatMessages = messageParts.map(part => {
|
||||
return {
|
||||
role: part.bot ? "ASSISTANT" : "USER",
|
||||
content: extractText(part.content, Environment.BOT_PREFIX)
|
||||
};
|
||||
});
|
||||
chatMessages.reverse();
|
||||
chatMessages.unshift({role: "SYSTEM", content: Environment.SYSTEM_PROMPT});
|
||||
|
||||
let waitMessage: Message;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
waitMessage = await bot.sendMessage({
|
||||
chat_id: chatId,
|
||||
text: Environment.waitText,
|
||||
reply_parameters: {
|
||||
chat_id: chatId,
|
||||
message_id: msg.message_id
|
||||
}
|
||||
});
|
||||
|
||||
const stream = await ollama.chat({
|
||||
model: Environment.OLLAMA_MODEL,
|
||||
stream: true,
|
||||
think: false,
|
||||
keep_alive: 300,
|
||||
messages: chatMessages
|
||||
});
|
||||
|
||||
let currentText = "";
|
||||
let shouldBreak = false;
|
||||
|
||||
const editor = startIntervalEditor({
|
||||
intervalMs: 4500,
|
||||
getText: () => currentText,
|
||||
editFn: async (text) => {
|
||||
await editMessageText(chatId, waitMessage.message_id, escapeMarkdownV2Text(text), "Markdown");
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
const content = chunk.message.content;
|
||||
currentText += content;
|
||||
|
||||
const length = currentText.length;
|
||||
if (length > 4096) {
|
||||
currentText = currentText.slice(0, 4093) + "...";
|
||||
shouldBreak = true;
|
||||
}
|
||||
|
||||
if (shouldBreak || chunk.done) {
|
||||
console.log("messageText", currentText);
|
||||
console.log("length", length);
|
||||
|
||||
if (shouldBreak) {
|
||||
console.log("break", true);
|
||||
} else {
|
||||
console.log("ended", true);
|
||||
}
|
||||
|
||||
stream.abort();
|
||||
|
||||
const diff = Math.abs(Date.now() - startTime) / 1000;
|
||||
|
||||
await editor.tick();
|
||||
await editor.stop();
|
||||
|
||||
waitMessage.reply_to_message = msg;
|
||||
waitMessage.text = currentText;
|
||||
MessageStore.put(waitMessage);
|
||||
|
||||
await replyToMessage(waitMessage, `⏱️ ${diff}s`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await editor.tick();
|
||||
await editor.stop();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await replyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {oldSendMessage} from "../util/utils";
|
||||
import {ollama} from "../index";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
|
||||
export class OllamaKill extends ChatCommand {
|
||||
regexp = /^\/killollama/i;
|
||||
title = "/killOllama";
|
||||
description = "dunno, do some shit";
|
||||
|
||||
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
try {
|
||||
ollama.abort();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
await oldSendMessage(msg, "Остановил все генерации");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {bot, ollama} from "../index";
|
||||
import {editMessageText, ignore, replyToMessage} from "../util/utils";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {Environment} from "../common/environment";
|
||||
|
||||
export class OllamaPrompt extends ChatCommand {
|
||||
regexp = /^\/ollamaprompt\s([^]+)/i;
|
||||
title = "/ollamaPrompt";
|
||||
description = "Custom prompt for AI (Ollama)";
|
||||
|
||||
requirements = Requirements.Build(Requirement.BOT_ADMIN);
|
||||
|
||||
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
|
||||
console.log("match", match);
|
||||
return this.executeOllama(msg, match?.[1]);
|
||||
}
|
||||
|
||||
async executeOllama(msg: Message, text: string): Promise<void> {
|
||||
if (!text || text.trim().length === 0) return;
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
let waitMessage: Message;
|
||||
|
||||
try {
|
||||
waitMessage = await bot.sendMessage({
|
||||
chat_id: chatId,
|
||||
text: Environment.waitText,
|
||||
reply_parameters: {
|
||||
chat_id: chatId,
|
||||
message_id: msg.message_id
|
||||
},
|
||||
parse_mode: "Markdown"
|
||||
});
|
||||
|
||||
const stream = await ollama.chat({
|
||||
model: Environment.OLLAMA_MODEL,
|
||||
stream: true,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: text
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let ended = false;
|
||||
let messageText = "";
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
const length = messageText.length;
|
||||
|
||||
console.log("messageText", messageText);
|
||||
console.log("length", length);
|
||||
console.log("ended", ended);
|
||||
await editMessageText(chatId, waitMessage.message_id, messageText);
|
||||
if (ended) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 4500);
|
||||
|
||||
let shouldBreak = false;
|
||||
|
||||
for await (const chunk of stream) {
|
||||
messageText += chunk.message.content;
|
||||
|
||||
const length = messageText.length;
|
||||
|
||||
if (length > 4096) {
|
||||
messageText = messageText.slice(0, 4093) + "...";
|
||||
shouldBreak = true;
|
||||
}
|
||||
|
||||
if (shouldBreak) {
|
||||
console.log("messageText", messageText);
|
||||
console.log("length", length);
|
||||
console.log("break", true);
|
||||
ended = true;
|
||||
|
||||
stream.abort();
|
||||
clearInterval(interval);
|
||||
|
||||
const diff = Math.abs(new Date().getSeconds() - waitMessage.date);
|
||||
messageText += `\n\nДумал ${diff}s`;
|
||||
|
||||
await editMessageText(chatId, waitMessage.message_id, messageText);
|
||||
await replyToMessage(waitMessage, "Закончил лишь часть 😉");
|
||||
break;
|
||||
}
|
||||
|
||||
if (chunk.done) {
|
||||
console.log("messageText", messageText);
|
||||
console.log("length", messageText.length);
|
||||
console.log("ended", true);
|
||||
ended = true;
|
||||
clearInterval(interval);
|
||||
|
||||
const diff = Math.abs(Date.now() / 1000 - waitMessage.date);
|
||||
messageText += `\n\nДумал ${diff}s`;
|
||||
|
||||
await editMessageText(chatId, waitMessage.message_id, messageText);
|
||||
await replyToMessage(waitMessage, "Закончил 😉");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await editMessageText(chatId, waitMessage.message_id, `Произошла ошибка!\n${error.toString()}`)
|
||||
.catch(async (e) => {
|
||||
await editMessageText(chatId, waitMessage.message_id, `Произошла ошибка!\n${e.toString()}`).catch(ignore);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {bot, ollama} from "../index";
|
||||
import {WebSearchResponse} from "../model/web-search-response";
|
||||
import {editMessageText} from "../util/utils";
|
||||
import {Environment} from "../common/environment";
|
||||
|
||||
export class OllamaSearch extends ChatCommand {
|
||||
regexp = /^\/(s|search)\s([^]+)/;
|
||||
title = "/search";
|
||||
description = "Web search via Ollama";
|
||||
|
||||
override requirements = Requirements.Build(Requirement.BOT_ADMIN);
|
||||
|
||||
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
|
||||
console.log("match", match);
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
try {
|
||||
const wait = await bot.sendMessage({
|
||||
chat_id: chatId,
|
||||
text: Environment.waitText,
|
||||
reply_parameters: {
|
||||
chat_id: chatId,
|
||||
message_id: msg.message_id
|
||||
},
|
||||
parse_mode: "Markdown"
|
||||
});
|
||||
|
||||
const results = await ollama.webSearch({query: match?.[1]});
|
||||
console.log("results", results);
|
||||
|
||||
let message = "Результаты:\n\n";
|
||||
results.results.forEach((result, index) => {
|
||||
const r = result as WebSearchResponse;
|
||||
message += `${index + 1}. ${r.url}\n`;
|
||||
});
|
||||
|
||||
await editMessageText(chatId, wait.message_id, message);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import {logError, oldSendMessage} from "../util/utils";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
|
||||
export class Ping implements ChatCommand {
|
||||
regexp = /^\/ping/i;
|
||||
title = "/ping";
|
||||
description = "Ping between received and sent message";
|
||||
|
||||
async execute(msg: Message) {
|
||||
const then = new Date().getMilliseconds();
|
||||
await oldSendMessage(msg, "pong").catch(logError);
|
||||
const now = new Date().getMilliseconds();
|
||||
const diff = Math.abs(now - then);
|
||||
await oldSendMessage(msg, `ping: ${diff}ms`).catch(logError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {logError, randomValue, replyToMessage} from "../util/utils";
|
||||
import {prefixAnswers} from "../db/database";
|
||||
|
||||
export class PrefixResponse extends ChatCommand {
|
||||
regexp: RegExp;
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
await replyToMessage(msg, randomValue(prefixAnswers)).catch(logError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {extractMessagePayload, logError, sendMessage} from "../util/utils";
|
||||
import {bot} from "../index";
|
||||
import QRCode from "qrcode";
|
||||
|
||||
export class Qr extends ChatCommand {
|
||||
regexp = /^\/qr/i;
|
||||
title = "/qr";
|
||||
description = "Generates QR-code from text you sent or replied to.";
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
const split = msg.text?.split("/qr ");
|
||||
const matchText = split[1];
|
||||
|
||||
const payload = extractMessagePayload(msg, matchText);
|
||||
if (!payload) {
|
||||
await sendMessage({
|
||||
chatId: chatId,
|
||||
text: "Отправь: /qr <текст или ссылка>\n" + "или ответь командой /qr на сообщение, из которого взять текст."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.length > 1500) {
|
||||
await sendMessage({
|
||||
chatId: chatId,
|
||||
text: `Слишком длинный текст для QR (${payload.length} символов). Максимум 1500 символов.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await bot.sendChatAction({chat_id: chatId, action: "upload_photo"});
|
||||
|
||||
const pngBuffer = await QRCode.toBuffer(payload, {
|
||||
type: "png",
|
||||
errorCorrectionLevel: "L",
|
||||
margin: 2,
|
||||
scale: 8,
|
||||
});
|
||||
|
||||
await bot.sendPhoto({
|
||||
chat_id: chatId,
|
||||
photo: pngBuffer,
|
||||
caption: `QR готов ✅\nСодержимое: ${payload.length > 80 ? payload.slice(0, 80) + "…" : payload}`,
|
||||
reply_parameters: {
|
||||
message_id: msg.message_id,
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
await sendMessage({chatId: chatId, text: `Не получилось сгенерировать QR: ${e?.message ?? String(e)}`}).catch(logError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
import axios from "axios";
|
||||
import sharp from "sharp";
|
||||
import twemoji from "twemoji";
|
||||
import emojiRegex from "emoji-regex";
|
||||
|
||||
import {createCanvas, GlobalFonts, type Image as CanvasImage, loadImage, SKRSContext2D} from "@napi-rs/canvas";
|
||||
import {Message, PhotoSize} from "typescript-telegram-bot-api";
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {bot, botUser} from "../index";
|
||||
import {
|
||||
getChatAvatar,
|
||||
getFileUrl,
|
||||
getUserAvatar,
|
||||
logError,
|
||||
makeDarkGradientBgFancy,
|
||||
oldSendMessage,
|
||||
replyToMessage
|
||||
} from "../util/utils";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
|
||||
try {
|
||||
GlobalFonts.registerFromPath("./assets/Inter_18pt-ExtraThin.ttf", "InterExtraThin");
|
||||
GlobalFonts.registerFromPath("./assets/Inter_18pt-Thin.ttf", "InterThin");
|
||||
GlobalFonts.registerFromPath("./assets/Inter_18pt-Light.ttf", "InterLight");
|
||||
GlobalFonts.registerFromPath("./assets/Inter_18pt-Regular.ttf", "Inter");
|
||||
GlobalFonts.registerFromPath("./assets/Inter_18pt-Medium.ttf", "InterMedium");
|
||||
GlobalFonts.registerFromPath("./assets/Inter_18pt-SemiBold.ttf", "InterSemiBold");
|
||||
GlobalFonts.registerFromPath("./assets/Inter_18pt-Bold.ttf", "InterBold");
|
||||
GlobalFonts.registerFromPath("./assets/Inter_18pt-ExtraBold.ttf", "InterExtraBold");
|
||||
GlobalFonts.registerFromPath("./assets/Inter_18pt-Black.ttf", "InterBlack");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
export class Quote extends ChatCommand {
|
||||
regexp = /^\/(cit|q|quote)$/i;
|
||||
title = "/quote";
|
||||
description = "Make quote from text (or quote)";
|
||||
|
||||
requirements = Requirements.Build(Requirement.REPLY);
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
const chatId = msg.chat.id;
|
||||
const reply = msg.reply_to_message;
|
||||
|
||||
if (!reply) {
|
||||
await replyToMessage(msg, "Сделай /quote реплаем на сообщение 🙂").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const quoteRaw = (msg.quote?.text ?? reply.text ?? reply.caption ?? "").trim();
|
||||
if (quoteRaw.length === 0) {
|
||||
await replyToMessage(msg, "Не нашёл в сообщении текста 😢").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
let quote = quoteRaw.length ? quoteRaw : "…";
|
||||
if (quote.length > 2500) quote = quote.slice(0, 2497) + "…";
|
||||
|
||||
const png = await renderQuoteCard(quote, reply);
|
||||
await bot.sendPhoto({
|
||||
chat_id: chatId,
|
||||
photo: png,
|
||||
reply_parameters: {
|
||||
message_id: msg.message_id,
|
||||
},
|
||||
}).catch(logError);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
await oldSendMessage(msg, "Не смог собрать цитату 😢").catch(logError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Emoji cache & helpers =====
|
||||
|
||||
const emojiCache = new Map<string, CanvasImage>();
|
||||
|
||||
function twemojiUrl(emoji: string) {
|
||||
const code = twemoji.convert.toCodePoint(emoji);
|
||||
return `https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/72x72/${code}.png`;
|
||||
}
|
||||
|
||||
async function loadEmoji(emoji: string): Promise<CanvasImage> {
|
||||
const url = twemojiUrl(emoji);
|
||||
const cached = emojiCache.get(url);
|
||||
if (cached) return cached;
|
||||
|
||||
const res = await axios.get<ArrayBuffer>(url, {responseType: "arraybuffer"});
|
||||
const img = await loadImage(Buffer.from(res.data));
|
||||
emojiCache.set(url, img);
|
||||
return img;
|
||||
}
|
||||
|
||||
type Segment = { type: "text"; v: string } | { type: "emoji"; v: string };
|
||||
|
||||
function splitSegments(text: string): Segment[] {
|
||||
const re = emojiRegex();
|
||||
const out: Segment[] = [];
|
||||
let last = 0;
|
||||
|
||||
for (const m of text.matchAll(re)) {
|
||||
const i = m.index ?? 0;
|
||||
if (i > last) out.push({type: "text", v: text.slice(last, i)});
|
||||
out.push({type: "emoji", v: m[0]});
|
||||
last = i + m[0].length;
|
||||
}
|
||||
if (last < text.length) out.push({type: "text", v: text.slice(last)});
|
||||
return out;
|
||||
}
|
||||
|
||||
function measure(ctx: SKRSContext2D, s: string) {
|
||||
return ctx.measureText(s).width;
|
||||
}
|
||||
|
||||
function wrapSegments(ctx: SKRSContext2D, segments: Segment[], maxW: number, emojiW: number) {
|
||||
const lines: { segments: Segment[]; width: number }[] = [];
|
||||
let cur: Segment[] = [];
|
||||
let w = 0;
|
||||
|
||||
const push = () => {
|
||||
lines.push({segments: cur, width: w});
|
||||
cur = [];
|
||||
w = 0;
|
||||
};
|
||||
|
||||
const add = (seg: Segment, segW: number) => {
|
||||
if (cur.length && w + segW > maxW) push();
|
||||
cur.push(seg);
|
||||
w += segW;
|
||||
};
|
||||
|
||||
for (const seg of segments) {
|
||||
if (seg.type === "emoji") {
|
||||
add(seg, emojiW);
|
||||
continue;
|
||||
}
|
||||
|
||||
// переносы/пробелы
|
||||
const parts = seg.v.split(/(\s+)/);
|
||||
for (const p of parts) {
|
||||
if (!p) continue;
|
||||
|
||||
const sub = p.split("\n");
|
||||
for (let si = 0; si < sub.length; si++) {
|
||||
const chunk = sub[si];
|
||||
if (chunk) add({type: "text", v: chunk}, measure(ctx, chunk));
|
||||
if (si !== sub.length - 1) push();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cur.length) push();
|
||||
return lines;
|
||||
}
|
||||
|
||||
function lineWidth(ctx: SKRSContext2D, segments: Segment[], fontSize: number) {
|
||||
const emojiSize = Math.round(fontSize * 1.05);
|
||||
let w = 0;
|
||||
for (const s of segments) {
|
||||
w += s.type === "emoji" ? emojiSize : ctx.measureText(s.v).width;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
function addEllipsisToFit(ctx: SKRSContext2D, segments: Segment[], maxW: number, fontSize: number): Segment[] {
|
||||
const emojiSize = Math.round(fontSize * 1.05);
|
||||
const ell: Segment = {type: "text", v: "…"};
|
||||
const ellW = ctx.measureText("…").width;
|
||||
|
||||
const out = segments.map((s) => ({...s})) as Segment[];
|
||||
|
||||
const widthOf = (arr: Segment[]) => {
|
||||
let w = 0;
|
||||
for (const s of arr) w += s.type === "emoji" ? emojiSize : ctx.measureText(s.v).width;
|
||||
return w;
|
||||
};
|
||||
|
||||
while (out.length && widthOf(out) + ellW > maxW) {
|
||||
const last = out[out.length - 1];
|
||||
if (last.type === "emoji") {
|
||||
out.pop();
|
||||
continue;
|
||||
}
|
||||
if (last.v.length <= 1) {
|
||||
out.pop();
|
||||
continue;
|
||||
}
|
||||
last.v = last.v.slice(0, -1);
|
||||
}
|
||||
|
||||
return [...out, ell];
|
||||
}
|
||||
|
||||
async function drawLine(ctx: SKRSContext2D, line: Segment[], x: number, baselineY: number, fontSize: number) {
|
||||
const emojiSize = Math.round(fontSize * 1.05);
|
||||
let cx = x;
|
||||
|
||||
for (const seg of line) {
|
||||
if (seg.type === "text") {
|
||||
ctx.fillText(seg.v, cx, baselineY);
|
||||
cx += measure(ctx, seg.v);
|
||||
} else {
|
||||
const img = await loadEmoji(seg.v);
|
||||
const y = baselineY - emojiSize + Math.round(fontSize * 0.2);
|
||||
ctx.drawImage(img, cx, y, emojiSize, emojiSize);
|
||||
cx += emojiSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Fitted = {
|
||||
fontSize: number;
|
||||
lineH: number;
|
||||
lines: { segments: Segment[]; width: number }[];
|
||||
truncated: boolean;
|
||||
};
|
||||
|
||||
function fitQuoteToBox(ctx: SKRSContext2D, quoteWithOpen: string, boxW: number, boxH: number): Fitted {
|
||||
const MAX_FONT = 64;
|
||||
const MIN_FONT = 18;
|
||||
const endSuffix = " »";
|
||||
|
||||
const segments = splitSegments(quoteWithOpen);
|
||||
|
||||
for (let fontSize = MAX_FONT; fontSize >= MIN_FONT; fontSize -= 2) {
|
||||
const emojiSize = Math.round(fontSize * 1.05);
|
||||
ctx.font = `${fontSize}px Inter, sans-serif`;
|
||||
|
||||
const lines = wrapSegments(ctx, segments, boxW, emojiSize);
|
||||
const lineH = Math.round(fontSize * 1.20);
|
||||
const totalH = lines.length * lineH;
|
||||
|
||||
if (!lines.length) continue;
|
||||
|
||||
const endW = ctx.measureText(endSuffix).width;
|
||||
const last = lines[lines.length - 1];
|
||||
|
||||
if (totalH <= boxH && last.width + endW <= boxW) {
|
||||
last.segments = [...last.segments, {type: "text", v: endSuffix}];
|
||||
last.width += endW;
|
||||
|
||||
return {fontSize: fontSize, lineH, lines, truncated: false};
|
||||
}
|
||||
}
|
||||
|
||||
const fontSize = MIN_FONT;
|
||||
const emojiSize = Math.round(fontSize * 1.05);
|
||||
ctx.font = `${fontSize}px Inter, sans-serif`;
|
||||
|
||||
const lineH = Math.round(fontSize * 1.20);
|
||||
const maxLinesByHeight = Math.max(1, Math.floor(boxH / lineH));
|
||||
|
||||
let lines = wrapSegments(ctx, segments, boxW, emojiSize);
|
||||
|
||||
const endW = ctx.measureText(endSuffix).width;
|
||||
|
||||
if (lines.length > maxLinesByHeight) {
|
||||
lines = lines.slice(0, maxLinesByHeight);
|
||||
const last = lines[lines.length - 1];
|
||||
|
||||
if (last.width + endW > boxW) {
|
||||
last.segments = addEllipsisToFit(ctx, last.segments, boxW - endW, fontSize);
|
||||
last.width = lineWidth(ctx, last.segments, fontSize);
|
||||
} else {
|
||||
last.segments = addEllipsisToFit(ctx, last.segments, boxW - endW, fontSize);
|
||||
last.width = lineWidth(ctx, last.segments, fontSize);
|
||||
}
|
||||
} else {
|
||||
const last = lines[lines.length - 1];
|
||||
if (last && last.width + endW > boxW) {
|
||||
last.segments = addEllipsisToFit(ctx, last.segments, boxW - endW, fontSize);
|
||||
last.width = lineWidth(ctx, last.segments, fontSize);
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.length) {
|
||||
const last = lines[lines.length - 1];
|
||||
last.segments = [...last.segments, {type: "text", v: endSuffix}];
|
||||
last.width += endW;
|
||||
}
|
||||
|
||||
return {fontSize: fontSize, lineH, lines, truncated: true};
|
||||
}
|
||||
|
||||
async function getBackground(
|
||||
reply: Message,
|
||||
W: number,
|
||||
H: number,
|
||||
author: QuoteAuthor,
|
||||
isForwarded: boolean
|
||||
): Promise<Buffer> {
|
||||
let src: Buffer | null = null;
|
||||
|
||||
const photoArr = reply.photo as PhotoSize[] | undefined;
|
||||
const msgPhoto = photoArr && photoArr.length ? photoArr[photoArr.length - 1] : undefined;
|
||||
|
||||
if (msgPhoto?.file_id) {
|
||||
const url = await getFileUrl(bot, msgPhoto.file_id);
|
||||
const res = await axios.get<ArrayBuffer>(url, {responseType: "arraybuffer"});
|
||||
src = Buffer.from(res.data);
|
||||
} else {
|
||||
if (author.userId) {
|
||||
src = await getUserAvatar(bot, author.userId);
|
||||
} else if (author.chatId) {
|
||||
src = await getChatAvatar(bot, author.chatId);
|
||||
} else if (!isForwarded && reply.from?.id) {
|
||||
src = await getUserAvatar(bot, reply.from.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (!src) {
|
||||
return makeDarkGradientBgFancy(W, H, `${reply.message_id}-${reply.date ?? ""}`);
|
||||
// return sharp({create: {width: W, height: H, channels: 3, background: "#1f1f1f"}})
|
||||
// .png()
|
||||
// .toBuffer();
|
||||
}
|
||||
|
||||
return sharp(src)
|
||||
.resize(W, H, {fit: "cover"})
|
||||
.blur(18)
|
||||
.modulate({brightness: 0.75, saturation: 1.1})
|
||||
.png()
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
async function renderQuoteCard(quote: string, reply: Message) {
|
||||
const W = 1280;
|
||||
const H = 720;
|
||||
|
||||
const author = getQuoteAuthor(reply);
|
||||
const forwarded = !!reply.forward_origin;
|
||||
const name = author.name;
|
||||
const userTag = author.username ? `@${author.username}` : "";
|
||||
|
||||
const me = botUser;
|
||||
const botTag = me.username ? `@${me.username}` : "@bot";
|
||||
|
||||
const date = new Date((reply.date ?? Math.floor(Date.now() / 1000)) * 1000);
|
||||
const dd = String(date.getDate()).padStart(2, "0");
|
||||
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const yyyy = String(date.getFullYear());
|
||||
const dateStr = `${dd}.${mm}.${yyyy}`;
|
||||
|
||||
const bgBuf = await getBackground(reply, W, H, author, forwarded);
|
||||
|
||||
const canvas = createCanvas(W, H);
|
||||
const c = canvas.getContext("2d");
|
||||
|
||||
const bgImg = await loadImage(bgBuf);
|
||||
c.drawImage(bgImg, 0, 0, W, H);
|
||||
|
||||
c.fillStyle = "rgba(0,0,0,0.35)";
|
||||
c.fillRect(0, 0, W, H);
|
||||
|
||||
const edgePad = 56;
|
||||
|
||||
const reservedBottom = 140;
|
||||
|
||||
const quoteBoxX = edgePad;
|
||||
const quoteBoxW = W - edgePad * 2;
|
||||
|
||||
const quoteTop = 90;
|
||||
const quoteBottom = H - reservedBottom;
|
||||
const quoteH = quoteBottom - quoteTop;
|
||||
|
||||
c.fillStyle = "rgba(255,255,255,0.92)";
|
||||
c.textBaseline = "alphabetic";
|
||||
c.shadowColor = "rgba(0,0,0,0.55)";
|
||||
c.shadowBlur = 10;
|
||||
c.shadowOffsetY = 2;
|
||||
|
||||
const quoteForFit = `« ${quote}`;
|
||||
const fitted = fitQuoteToBox(c, quoteForFit, quoteBoxW, quoteH);
|
||||
|
||||
c.font = `${fitted.fontSize}px InterSemiBold, sans-serif`;
|
||||
|
||||
const totalTextH = fitted.lines.length * fitted.lineH;
|
||||
let y = quoteTop + (quoteH - totalTextH) / 2 + fitted.fontSize;
|
||||
|
||||
for (const ln of fitted.lines) {
|
||||
const x = quoteBoxX + (quoteBoxW - ln.width) / 2;
|
||||
await drawLine(c, ln.segments, x, y, fitted.fontSize);
|
||||
y += fitted.lineH;
|
||||
}
|
||||
|
||||
c.shadowBlur = 0;
|
||||
c.shadowOffsetY = 0;
|
||||
c.fillStyle = "rgba(255,255,255,0.70)";
|
||||
c.font = "28px InterLight, Inter, sans-serif";
|
||||
c.textAlign = "center";
|
||||
c.fillText(userTag ? `${name} | ${userTag}` : name, W / 2, H - 86);
|
||||
|
||||
c.font = "22px InterMedium, sans-serif";
|
||||
c.fillStyle = "rgba(255,255,255,0.45)";
|
||||
c.textAlign = "left";
|
||||
c.fillText(botTag, edgePad, H - 34);
|
||||
c.textAlign = "right";
|
||||
c.fillText(dateStr, W - edgePad, H - 34);
|
||||
|
||||
return canvas.toBuffer("image/png");
|
||||
}
|
||||
|
||||
type QuoteAuthor = {
|
||||
name: string;
|
||||
username?: string;
|
||||
userId?: number;
|
||||
chatId?: number;
|
||||
};
|
||||
|
||||
function getQuoteAuthor(reply: Message): QuoteAuthor {
|
||||
const origin = reply.forward_origin;
|
||||
if (origin) {
|
||||
switch (origin.type) {
|
||||
case "user": {
|
||||
const u = origin.sender_user;
|
||||
const name = [u.first_name, u.last_name].filter(Boolean).join(" ") || u.username || "Unknown";
|
||||
return {name, username: u.username, userId: u.id};
|
||||
}
|
||||
case "hidden_user": {
|
||||
const name = origin.sender_user_name || "Unknown";
|
||||
return {name};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const u = reply.from!;
|
||||
const name = [u.first_name, u.last_name].filter(Boolean).join(" ") || u.username || "Unknown";
|
||||
return {name, username: u.username, userId: u.id};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {getRandomInt, getRangedRandomInt, logError, oldSendMessage} from "../util/utils";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
|
||||
export class RandomInt extends ChatCommand {
|
||||
regexp = /^\/randomInt/i;
|
||||
title = "/randomInt [min] [max]";
|
||||
description = "Ranged random integer from parameters";
|
||||
|
||||
async execute(msg: Message) {
|
||||
const split = msg.text.split(" ");
|
||||
const min = parseInt(split[1]);
|
||||
const max = parseInt(split[2]);
|
||||
|
||||
const good = max > min;
|
||||
const sufficient = !!(min && max) && good;
|
||||
|
||||
const random = !sufficient ? getRandomInt(Math.pow(2, 60)) : getRangedRandomInt(min, max);
|
||||
|
||||
const randomText = !sufficient ? random.toString() : `[${min}; ${max}]: ${random}`;
|
||||
|
||||
await oldSendMessage(msg, randomText).catch(logError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {getRandomInt, logError, oldSendMessage} from "../util/utils";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
|
||||
export class RandomString implements ChatCommand {
|
||||
regexp = /^\/randomString/i;
|
||||
title = "/randomString [length]";
|
||||
description = "literally random string (up to 4096 symbols)";
|
||||
|
||||
async execute(msg: Message) {
|
||||
const split = msg.text.split(" ");
|
||||
const l = parseInt(split.length > 1 ? split[1] : "1");
|
||||
|
||||
const length = (l <= 0 || l > 4096) ? 1 : l;
|
||||
|
||||
let result = "";
|
||||
|
||||
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя0123456789";
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(getRandomInt(characters.length));
|
||||
}
|
||||
|
||||
await oldSendMessage(msg, result).catch(logError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {bot} from "../index";
|
||||
import {delay, logError, randomValue} from "../util/utils";
|
||||
|
||||
const texts = [
|
||||
"ну что-же, господа",
|
||||
"приятно было с вами пообщаться",
|
||||
"но мне пора на покой",
|
||||
"всего хорошего"
|
||||
];
|
||||
|
||||
const timings = [1500, 2500];
|
||||
const timer = [3, 2, 1];
|
||||
|
||||
export class Shutdown extends ChatCommand {
|
||||
regexp = /^\/shutdown/i;
|
||||
title = "/shutdown";
|
||||
description = "Self-destruction sequence for bot (shutdown)";
|
||||
|
||||
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
await bot.sendMessage({chat_id: msg.chat.id, text: "..."}).catch(logError);
|
||||
|
||||
if (msg.chat.type !== "private" && !msg.text.toLowerCase().startsWith("/shutdown now")) {
|
||||
for (const text of texts) {
|
||||
await delay(randomValue(timings));
|
||||
await bot.sendMessage({chat_id: msg.chat.id, text: text}).catch(logError);
|
||||
}
|
||||
|
||||
await delay(randomValue(timings));
|
||||
|
||||
for (const t of timer) {
|
||||
await bot.sendMessage({chat_id: msg.chat.id, text: `${t}`}).catch(logError);
|
||||
await delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
await bot.sendMessage({chat_id: msg.chat.id, text: "*R.I.P*"}).catch(logError);
|
||||
|
||||
delay(2000).then(() => process.exit(0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {chatCommands} from "../index";
|
||||
import {Help} from "./help";
|
||||
|
||||
export class Start extends ChatCommand {
|
||||
regexp = /^\/start/i;
|
||||
title = "/start";
|
||||
description = "Start the bot";
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
await chatCommands.find(e => e instanceof Help).execute(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {logError, oldSendMessage} from "../util/utils";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {systemSpecsText} from "../index";
|
||||
|
||||
export class SystemSpecs implements ChatCommand {
|
||||
regexp = /^\/systemspecs/i;
|
||||
title = "/systemSpecs";
|
||||
description = "System specifications of system";
|
||||
|
||||
async execute(msg: Message) {
|
||||
await oldSendMessage(msg, systemSpecsText).catch(logError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {logError, randomValue, replyToMessage} from "../util/utils";
|
||||
import {testAnswers} from "../db/database";
|
||||
|
||||
export class Test implements ChatCommand {
|
||||
regexp = /^(test|тест|еуые|ntcn|инноке(нтий|ш|нтич))/i;
|
||||
title = "тест";
|
||||
description = "System functionality check";
|
||||
|
||||
async execute(msg: Message) {
|
||||
await replyToMessage(msg, randomValue(testAnswers) || "а").catch(logError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {logError, replyToMessage} from "../util/utils";
|
||||
import {bot} from "../index";
|
||||
|
||||
export class Title extends ChatCommand {
|
||||
regexp = /^\/title\s([^]+)/;
|
||||
title = "/title [title]";
|
||||
description = "Change group title";
|
||||
|
||||
requirements = Requirements.Build(
|
||||
Requirement.CHAT,
|
||||
Requirement.BOT_ADMIN,
|
||||
Requirement.BOT_CHAT_ADMIN
|
||||
);
|
||||
|
||||
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
|
||||
const title = (match?.[1] ?? "").trim();
|
||||
if (title.length === 0) {
|
||||
await replyToMessage(msg, "Не нашёл название...").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
await bot.setChatTitle({chat_id: msg.chat.id, title: title}).catch(logError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {bot, botUser} from "../index";
|
||||
import {fullName, logError, oldSendMessage, replyToMessage} from "../util/utils";
|
||||
import {Environment} from "../common/environment";
|
||||
|
||||
export class Unban extends ChatCommand {
|
||||
regexp = /^\/unban/i;
|
||||
title = "/unban [reply]";
|
||||
description = "unban user from chat";
|
||||
|
||||
requirements = Requirements.Build(
|
||||
Requirement.CHAT,
|
||||
Requirement.BOT_CHAT_ADMIN,
|
||||
Requirement.REPLY,
|
||||
Requirement.BOT_ADMIN
|
||||
);
|
||||
|
||||
async execute(msg: Message) {
|
||||
if (!msg.reply_to_message) return;
|
||||
|
||||
const user = msg.reply_to_message.from;
|
||||
const userId = user.id;
|
||||
|
||||
if (userId === botUser.id) {
|
||||
await replyToMessage(msg, "Бот и так не в бане сам у себя.").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (userId === Environment.CREATOR_ID) {
|
||||
await replyToMessage(msg, "Создатель бота и так не в бане и никогда не будет.").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.from.id !== Environment.CREATOR_ID && Environment.ADMIN_IDS.has(userId)) {
|
||||
await replyToMessage(msg, "Админимтраторы бота и так не в бане.").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
bot.unbanChatMember({chat_id: msg.chat.id, user_id: userId})
|
||||
.then(async () => {
|
||||
await oldSendMessage(msg, `${fullName(user)} разбанен ⛓️💥`).catch(logError);
|
||||
})
|
||||
.catch(async () => {
|
||||
await oldSendMessage(msg, `Не смог разбанить ${fullName(user)} ☹️`).catch(logError);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {removeMute} from "../db/database";
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {fullName, logError, oldSendMessage} from "../util/utils";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {botUser} from "../index";
|
||||
import {Environment} from "../common/environment";
|
||||
|
||||
export class Unmute implements ChatCommand {
|
||||
regexp = /^\/unmute/i;
|
||||
title = "/unmute";
|
||||
description = "Bot will start responding to the user";
|
||||
requirements = Requirements.Build(Requirement.BOT_ADMIN, Requirement.CHAT, Requirement.REPLY);
|
||||
|
||||
async execute(msg: Message) {
|
||||
if (!msg.reply_to_message) return;
|
||||
|
||||
const id = msg.reply_to_message.from.id;
|
||||
const text = fullName(msg.reply_to_message.from);
|
||||
|
||||
if (id === botUser.id) {
|
||||
await oldSendMessage(msg, "Бот и так всегда к себе прислушивается").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (id === Environment.CREATOR_ID) {
|
||||
await oldSendMessage(msg, "Бот всегда слушает своего создателя").catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await removeMute(id)) {
|
||||
await oldSendMessage(msg, text + " больше не в муте! 🔈").catch(logError);
|
||||
} else {
|
||||
await oldSendMessage(msg, text + " не был в муте 🤔").catch(logError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {getUptime, logError, oldSendMessage} from "../util/utils";
|
||||
|
||||
export class Uptime extends ChatCommand {
|
||||
regexp = /^\/uptime/i;
|
||||
title = "/uptime";
|
||||
description = "Bot's uptime";
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
await oldSendMessage(msg, getUptime()).catch(logError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {logError, randomValue, oldSendMessage} from "../util/utils";
|
||||
import {betterAnswers} from "../db/database";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
|
||||
export class WhatBetter extends ChatCommand {
|
||||
regexp = /^\/(what|что)\s(better|лучше)\s([^]+)\s(or|или)\s([^]+)/i;
|
||||
title = "/what better [a] or [b]";
|
||||
description = "either a or b randomly (50% chance)";
|
||||
|
||||
async execute(msg: Message, match?: RegExpExecArray) {
|
||||
const a = match[3];
|
||||
const b = match[5].trimStart();
|
||||
|
||||
const text = `${randomValue(betterAnswers)} ${randomValue([a, b])}`;
|
||||
|
||||
await oldSendMessage(msg, text).catch(logError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {getRandomInt, getRangedRandomInt, logError, replyToMessage} from "../util/utils";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
|
||||
export class When extends ChatCommand {
|
||||
regexp = /^\/(when|когда)\s([^]+)/i;
|
||||
title = "/when [value]";
|
||||
description = "random date";
|
||||
|
||||
async execute(msg: Message) {
|
||||
let text = "через ";
|
||||
|
||||
const type = getRandomInt(8);
|
||||
|
||||
switch (type) {
|
||||
case 0:
|
||||
text = "сейчас";
|
||||
break;
|
||||
case 1:
|
||||
text = "никогда";
|
||||
break;
|
||||
case 2: //seconds
|
||||
{
|
||||
const seconds = getRangedRandomInt(1, 60);
|
||||
|
||||
text += `${seconds} `;
|
||||
|
||||
text += (
|
||||
(seconds == 1 || seconds % 10 == 1) ? "секунду" :
|
||||
((seconds > 1 && seconds < 5) || (seconds % 10 > 1 && seconds % 10 < 5)) ? "секунды" : "секунд"
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
const minutes = getRangedRandomInt(1, 60);
|
||||
|
||||
text += `${minutes} `;
|
||||
|
||||
text += (
|
||||
(minutes == 1 || minutes % 10 == 1) ? "минуту" :
|
||||
((minutes > 1 && minutes < 5) || (minutes % 10 > 1 && minutes % 10 < 5)) ? "минуты" : "минут"
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
const hours = getRangedRandomInt(1, 24);
|
||||
|
||||
text += `${hours} `;
|
||||
|
||||
text += (
|
||||
(hours == 1 || hours % 10 == 1) ? "час" :
|
||||
((hours > 1 && hours < 5) || (hours % 10 > 1 && hours % 10 < 5)) ? "часа" : "часов"
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
const weeks = getRangedRandomInt(1, 4);
|
||||
|
||||
text += `${weeks} `;
|
||||
|
||||
text += (weeks == 1 ? "неделю" : "недель");
|
||||
break;
|
||||
}
|
||||
case 6: {
|
||||
const months = getRandomInt(12);
|
||||
|
||||
text += `${months} `;
|
||||
|
||||
text += (
|
||||
(months == 1 || months % 10 == 1) ? "месяц" :
|
||||
((months > 1 && months < 5) || (months % 10 > 1 && months % 10 < 5)) ? "месяца" : "месяцев"
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 7: {
|
||||
const years = getRangedRandomInt(1, 100);
|
||||
|
||||
text += `${years} `;
|
||||
|
||||
text += (
|
||||
(years == 1 || years % 10 == 1) ? "год" :
|
||||
((years > 1 && years < 5) || (years % 10 > 1 && years % 10 < 5)) ? "года" : "лет"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await replyToMessage(msg, text).catch(logError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import path from "node:path";
|
||||
import {saveData} from "../db/database";
|
||||
|
||||
export class Environment {
|
||||
static BOT_TOKEN: string;
|
||||
static TEST_ENVIRONMENT: boolean;
|
||||
static ADMIN_IDS: Set<number> = new Set<number>();
|
||||
static CHAT_IDS_WHITELIST: Set<number> = new Set<number>();
|
||||
static BOT_PREFIX: string;
|
||||
static CREATOR_ID: number;
|
||||
static IS_DOCKER: boolean;
|
||||
static DATA_PATH: string;
|
||||
static DB_FILE_NAME: string = "database.db";
|
||||
static DB_PATH: string;
|
||||
|
||||
static USE_MOM: boolean;
|
||||
static USE_DAD: boolean;
|
||||
static USE_FU: boolean;
|
||||
|
||||
static OLLAMA_MODEL: string;
|
||||
static OLLAMA_ADDRESS: string;
|
||||
static OLLAMA_API_KEY?: string;
|
||||
static SYSTEM_PROMPT: string;
|
||||
|
||||
static GEMINI_API_KEY: string;
|
||||
|
||||
static waitText = "⏳ Дайте-ка подумать...";
|
||||
|
||||
static load() {
|
||||
Environment.BOT_TOKEN = process.env.BOT_TOKEN;
|
||||
Environment.TEST_ENVIRONMENT = process.env.TEST_ENVIRONMENT === "true";
|
||||
Environment.CHAT_IDS_WHITELIST = new Set(process.env.CHAT_IDS_WHITELIST?.split(",")?.map(e => parseInt(e.trim(), 10)) || []);
|
||||
Environment.BOT_PREFIX = process.env.BOT_PREFIX || "";
|
||||
Environment.CREATOR_ID = parseInt(process.env.CREATOR_ID || "");
|
||||
Environment.IS_DOCKER = process.env.IS_DOCKER == "true";
|
||||
Environment.DATA_PATH = Environment.IS_DOCKER ? path.join("", "config", "data") : "data";
|
||||
Environment.DB_PATH = "file:" + path.join(Environment.DATA_PATH, Environment.DB_FILE_NAME);
|
||||
|
||||
Environment.USE_MOM = process.env.USE_MOM == "true";
|
||||
Environment.USE_DAD = process.env.USE_DAD == "true";
|
||||
Environment.USE_FU = process.env.USE_FU == "true";
|
||||
|
||||
Environment.OLLAMA_MODEL = process.env.OLLAMA_MODEL || "llama3.2";
|
||||
Environment.OLLAMA_ADDRESS = process.env.OLLAMA_ADDRESS || "127.0.0.1";
|
||||
Environment.OLLAMA_API_KEY = process.env.OLLAMA_API_KEY;
|
||||
Environment.SYSTEM_PROMPT = (process.env.SYSTEM_PROMPT?.trim()) || "";
|
||||
|
||||
Environment.GEMINI_API_KEY = process.env.GEMINI_API_KEY;
|
||||
}
|
||||
|
||||
static setAdmins(admins: Set<number>) {
|
||||
this.ADMIN_IDS = admins;
|
||||
}
|
||||
|
||||
static async addAdmin(id: number): Promise<boolean> {
|
||||
const has = this.ADMIN_IDS.has(id);
|
||||
if (!has) {
|
||||
this.ADMIN_IDS.add(id);
|
||||
await saveData();
|
||||
}
|
||||
|
||||
return !has;
|
||||
}
|
||||
|
||||
static async removeAdmin(id: number): Promise<boolean> {
|
||||
const has = this.ADMIN_IDS.has(id);
|
||||
if (has) {
|
||||
this.ADMIN_IDS.delete(id);
|
||||
await saveData();
|
||||
}
|
||||
|
||||
return has;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export type MessagePart = {
|
||||
bot: boolean;
|
||||
name?: string;
|
||||
content: string;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import {StoredMessage} from "../model/stored-message";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {extractTextMessage} from "../util/utils";
|
||||
import {Environment} from "./environment";
|
||||
import {messageDao} from "../index";
|
||||
|
||||
export class MessageStore {
|
||||
private static map = new Map<string, StoredMessage>();
|
||||
|
||||
private static key(chatId: number, messageId: number) {
|
||||
return `${chatId}:${messageId}`;
|
||||
}
|
||||
|
||||
static all(): Map<string, StoredMessage> {
|
||||
return this.map;
|
||||
}
|
||||
|
||||
static async put(m: Message, prefix: string = Environment.BOT_PREFIX) {
|
||||
const msg: StoredMessage = {
|
||||
chatId: m.chat.id,
|
||||
messageId: m.message_id,
|
||||
replyToMessageId: m.reply_to_message?.message_id ?? null,
|
||||
fromId: m.from.id,
|
||||
text: extractTextMessage(m, prefix),
|
||||
date: m.date ?? 0,
|
||||
};
|
||||
|
||||
this.map.set(this.key(m.chat.id, m.message_id), msg);
|
||||
|
||||
await messageDao.insert(messageDao.mapTo([m]));
|
||||
}
|
||||
|
||||
static async get(chatId: number, messageId: number): Promise<StoredMessage | null> {
|
||||
const message = await messageDao.getById({chatId: chatId, id: messageId});
|
||||
if (!message) return null;
|
||||
|
||||
this.map.set(this.key(message.chatId, messageId), message);
|
||||
return message;
|
||||
}
|
||||
|
||||
static clear() {
|
||||
this.map.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {User} from "typescript-telegram-bot-api";
|
||||
import {userDao} from "../index";
|
||||
import {StoredUser} from "../model/stored-user";
|
||||
|
||||
export class UserStore {
|
||||
private static map = new Map<number, StoredUser>();
|
||||
|
||||
static all(): Map<number, StoredUser> {
|
||||
return this.map;
|
||||
}
|
||||
|
||||
static async put(u: User) {
|
||||
const user: StoredUser = {
|
||||
id: u.id,
|
||||
isBot: u.is_bot,
|
||||
firstName: u.first_name,
|
||||
lastName: u.last_name,
|
||||
userName: u.username,
|
||||
isPremium: u.is_premium,
|
||||
};
|
||||
|
||||
this.map.set(u.id, user);
|
||||
|
||||
await userDao.insert(userDao.mapTo([u]));
|
||||
}
|
||||
|
||||
static async get(id: number): Promise<StoredUser | null> {
|
||||
const user = await userDao.getById({id: id});
|
||||
if (!user) return null;
|
||||
|
||||
this.map.set(id, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
static clear() {
|
||||
this.map.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import "dotenv/config";
|
||||
import {drizzle, LibSQLDatabase} from "drizzle-orm/libsql";
|
||||
import {Environment} from "../common/environment";
|
||||
|
||||
export class DatabaseManager {
|
||||
|
||||
static db: LibSQLDatabase;
|
||||
|
||||
static init() {
|
||||
DatabaseManager.db = drizzle(Environment.DB_PATH);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import * as fs from "fs";
|
||||
import {Environment} from "../common/environment";
|
||||
|
||||
|
||||
export let muted: Set<number> = new Set<number>();
|
||||
|
||||
type DataJsonFile = {
|
||||
admins: number[]
|
||||
muted: number[]
|
||||
}
|
||||
|
||||
export let jsonFile: DataJsonFile;
|
||||
|
||||
type AnswersJsonFile = {
|
||||
test: string[]
|
||||
prefix: string[]
|
||||
better: string[]
|
||||
who: string[]
|
||||
kick: string[]
|
||||
invite: string[]
|
||||
day: number[]
|
||||
}
|
||||
|
||||
export const testAnswers: string[] = [];
|
||||
export const prefixAnswers: string[] = [];
|
||||
export const betterAnswers: string[] = [];
|
||||
export const whoAnswers: string[] = [];
|
||||
export const kickAnswers: string[] = [];
|
||||
export const inviteAnswers: string[] = [];
|
||||
export const dayAnswers: number[] = [];
|
||||
|
||||
export async function addMute(id: number): Promise<boolean> {
|
||||
if (muted.has(id)) return Promise.resolve(false);
|
||||
|
||||
muted.add(id);
|
||||
await saveData();
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
export async function removeMute(id: number): Promise<boolean> {
|
||||
if (!muted.has(id)) return Promise.resolve(false);
|
||||
muted.delete(id);
|
||||
await saveData();
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
export async function readData(): Promise<void> {
|
||||
try {
|
||||
jsonFile = JSON.parse(fs.readFileSync(`${Environment.DATA_PATH}/data.json`).toString());
|
||||
|
||||
const admins = jsonFile.admins || [];
|
||||
admins.unshift(Environment.CREATOR_ID);
|
||||
|
||||
Environment.setAdmins(new Set<number>(admins));
|
||||
|
||||
muted = new Set<number>(jsonFile.muted || []);
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveData(): Promise<void> {
|
||||
const adminIds: number[] = [];
|
||||
Environment.ADMIN_IDS.forEach(id => adminIds.push(id));
|
||||
jsonFile.admins = adminIds;
|
||||
|
||||
const mutedList: number[] = [];
|
||||
muted.forEach(id => mutedList.push(id));
|
||||
jsonFile.muted = mutedList;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(`${Environment.DATA_PATH}/data.json`, JSON.stringify(jsonFile));
|
||||
return readData();
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function retrieveAnswers(): Promise<void> {
|
||||
try {
|
||||
const json: AnswersJsonFile = JSON.parse(fs.readFileSync(`${Environment.DATA_PATH}/answers.json`).toString());
|
||||
json.test.forEach(e => testAnswers.push(e));
|
||||
json.prefix.forEach(e => prefixAnswers.push(e));
|
||||
json.better.forEach(e => betterAnswers.push(e));
|
||||
json.who.forEach(e => whoAnswers.push(e));
|
||||
json.kick.forEach(e => kickAnswers.push(e));
|
||||
json.invite.forEach(e => inviteAnswers.push(e));
|
||||
json.day.forEach(e => dayAnswers.push(e));
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import {messagesTable} from "./schema";
|
||||
import {DatabaseManager} from "./database-manager";
|
||||
import {StoredMessage} from "../model/stored-message";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {inArray} from "drizzle-orm/sql/expressions/conditions";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {Dao} from "../base/dao";
|
||||
import {buildExcludedSet} from "../util/utils";
|
||||
|
||||
export class MessageDao extends Dao<StoredMessage> {
|
||||
|
||||
private tag: string = "MessageDao";
|
||||
|
||||
override async getAll(): Promise<StoredMessage[]> {
|
||||
const then = Date.now();
|
||||
|
||||
const messages = await DatabaseManager.db.select().from(messagesTable);
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - then;
|
||||
console.log(`${this.tag}: getAll()`, `took ${diff}ms; size: ${messages.length}`);
|
||||
|
||||
return this.mapFrom(messages);
|
||||
}
|
||||
|
||||
override async getById(params: { chatId: number, id: number }): Promise<StoredMessage | null> {
|
||||
const then = Date.now();
|
||||
|
||||
const messages =
|
||||
await DatabaseManager.db.select()
|
||||
.from(messagesTable)
|
||||
.where(
|
||||
and(
|
||||
eq(messagesTable.chatId, params.chatId),
|
||||
eq(messagesTable.id, params.id)
|
||||
)
|
||||
);
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - then;
|
||||
console.log(`${this.tag}: getById(${params.chatId}, ${params.id})`, `took ${diff}ms; size: ${messages.length}`);
|
||||
|
||||
const m = messages[0];
|
||||
if (!m) return null;
|
||||
return this.mapFrom([m])[0];
|
||||
}
|
||||
|
||||
override async getByIds(params: { chatId: number, ids: number[] }): Promise<StoredMessage[]> {
|
||||
const then = Date.now();
|
||||
|
||||
const messages =
|
||||
await DatabaseManager.db.select()
|
||||
.from(messagesTable)
|
||||
.where(
|
||||
and(
|
||||
eq(messagesTable.chatId, params.chatId),
|
||||
inArray(messagesTable.id, params.ids)
|
||||
)
|
||||
);
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - then;
|
||||
console.log(`${this.tag}: getByIds(${params.chatId}, ${params.ids})`, `took ${diff}ms; size: ${messages.length}`);
|
||||
|
||||
return this.mapFrom(messages);
|
||||
}
|
||||
|
||||
async insert(values: typeof messagesTable.$inferInsert[]): Promise<true> {
|
||||
const then = Date.now();
|
||||
const r = await DatabaseManager.db
|
||||
.insert(messagesTable)
|
||||
.values(values)
|
||||
.onConflictDoUpdate({
|
||||
target: messagesTable.id,
|
||||
set: buildExcludedSet(messagesTable, ["id"])
|
||||
});
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - then;
|
||||
console.log(`${this.tag}: insert(size: ${values.length})`, `took ${diff}ms'; inserted: ${r.rowsAffected}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
mapTo(messages: Message[]): typeof messagesTable.$inferInsert[] {
|
||||
return messages.map(msg => {
|
||||
return {
|
||||
chatId: msg.chat.id,
|
||||
id: msg.message_id,
|
||||
replyToMessageId: msg.reply_to_message?.message_id,
|
||||
fromId: msg.from.id,
|
||||
text: msg.text,
|
||||
date: msg.date,
|
||||
firstName: msg.from.first_name,
|
||||
lastName: msg.from.last_name,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
mapFrom(messages: typeof messagesTable.$inferInsert[]): StoredMessage[] {
|
||||
return messages.map(m => {
|
||||
return {
|
||||
firstName: m.firstName,
|
||||
lastName: m.lastName,
|
||||
chatId: m.chatId,
|
||||
messageId: m.id,
|
||||
replyToMessageId: m.replyToMessageId,
|
||||
fromId: m.fromId,
|
||||
text: m.text,
|
||||
date: m.date
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {int, sqliteTable, text} from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const messagesTable = sqliteTable("messages", {
|
||||
id: int().primaryKey().unique().notNull(),
|
||||
chatId: int().notNull(),
|
||||
replyToMessageId: int(),
|
||||
fromId: int().notNull(),
|
||||
text: text().notNull(),
|
||||
date: int().notNull(),
|
||||
firstName: text().notNull(),
|
||||
lastName: text(),
|
||||
});
|
||||
|
||||
export type MessageInsert = typeof messagesTable.$inferInsert;
|
||||
|
||||
export const usersTable = sqliteTable("users", {
|
||||
id: int().primaryKey().unique().notNull(),
|
||||
isBot: int().notNull(),
|
||||
firstName: text().notNull(),
|
||||
lastName: text(),
|
||||
userName: text(),
|
||||
isPremium: int(),
|
||||
});
|
||||
|
||||
export type UserInsert = typeof usersTable.$inferInsert;
|
||||
@@ -0,0 +1,105 @@
|
||||
import {StoredUser} from "../model/stored-user";
|
||||
import {Dao} from "../base/dao";
|
||||
import {DatabaseManager} from "./database-manager";
|
||||
import {UserInsert, usersTable} from "./schema";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {inArray} from "drizzle-orm/sql/expressions/conditions";
|
||||
import {User} from "typescript-telegram-bot-api";
|
||||
import {boolToInt, buildExcludedSet} from "../util/utils";
|
||||
|
||||
export class UserDao extends Dao<StoredUser> {
|
||||
|
||||
private tag: string = "UserDao";
|
||||
|
||||
override async getAll(): Promise<StoredUser[]> {
|
||||
const then = Date.now();
|
||||
|
||||
const users = await DatabaseManager.db.select().from(usersTable);
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - then;
|
||||
console.log(`${this.tag}: getAll()`, `took ${diff}ms; size: ${users.length}`);
|
||||
|
||||
return this.mapFrom(users);
|
||||
}
|
||||
|
||||
override async getById(params: { id: number }): Promise<StoredUser | null> {
|
||||
const then = Date.now();
|
||||
|
||||
const users =
|
||||
await DatabaseManager.db.select()
|
||||
.from(usersTable)
|
||||
.where(
|
||||
eq(usersTable.id, params.id)
|
||||
);
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - then;
|
||||
console.log(`${this.tag}: getById(${params.id})`, `took ${diff}ms; size: ${users.length}`);
|
||||
|
||||
const u = users[0];
|
||||
if (!u) return null;
|
||||
return this.mapFrom([u])[0];
|
||||
}
|
||||
|
||||
override async getByIds(params: { ids: number[] }): Promise<StoredUser[]> {
|
||||
const then = Date.now();
|
||||
|
||||
const users =
|
||||
await DatabaseManager.db.select()
|
||||
.from(usersTable)
|
||||
.where(
|
||||
inArray(usersTable.id, params.ids)
|
||||
);
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - then;
|
||||
console.log(`${this.tag}: getByIds(${params.ids})`, `took ${diff}ms; size: ${users.length}`);
|
||||
|
||||
return this.mapFrom(users);
|
||||
}
|
||||
|
||||
override async insert(values: UserInsert[] | UserInsert): Promise<true> {
|
||||
const rows = Array.isArray(values) ? values : [values];
|
||||
|
||||
const then = Date.now();
|
||||
const r = await DatabaseManager.db
|
||||
.insert(usersTable)
|
||||
.values(rows)
|
||||
.onConflictDoUpdate({
|
||||
target: usersTable.id,
|
||||
set: buildExcludedSet(usersTable, ["id"])
|
||||
});
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - then;
|
||||
console.log(`${this.tag}: insert(size: ${rows.length})`, `took ${diff}ms; inserted: ${r.rowsAffected}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
mapTo(users: User[]): UserInsert[] {
|
||||
return users.map(u => {
|
||||
return {
|
||||
id: u.id,
|
||||
isBot: boolToInt(u.is_bot),
|
||||
firstName: u.first_name,
|
||||
lastName: u.last_name,
|
||||
userName: u.username,
|
||||
isPremium: boolToInt(u.is_premium)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
mapFrom(users: UserInsert[]): StoredUser[] {
|
||||
return users.map(u => {
|
||||
return {
|
||||
id: u.id,
|
||||
isBot: u.isBot === 1,
|
||||
firstName: u.firstName,
|
||||
lastName: u.lastName,
|
||||
userName: u.userName,
|
||||
isPremium: u.isPremium === 1
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
import "dotenv/config";
|
||||
import {Environment} from "./common/environment";
|
||||
import {InlineQueryResult, TelegramBot, User} from "typescript-telegram-bot-api";
|
||||
import {ChatCommand} from "./base/chat-command";
|
||||
import {
|
||||
checkRequirements,
|
||||
executeChatCommand,
|
||||
extractTextMessage,
|
||||
initSystemSpecs,
|
||||
logError,
|
||||
randomValue,
|
||||
searchChatCommand
|
||||
} from "./util/utils";
|
||||
import {Ae} from "./commands/ae";
|
||||
import {Help} from "./commands/help";
|
||||
import {Mute} from "./commands/mute";
|
||||
import {Unmute} from "./commands/unmute";
|
||||
import {Ping} from "./commands/ping";
|
||||
import {RandomString} from "./commands/random-string";
|
||||
import {SystemSpecs} from "./commands/system-specs";
|
||||
import {Test} from "./commands/test";
|
||||
import {inviteAnswers, kickAnswers, muted, readData, retrieveAnswers} from "./db/database";
|
||||
import {Uptime} from "./commands/uptime";
|
||||
import {WhatBetter} from "./commands/what-better";
|
||||
import {When} from "./commands/when";
|
||||
import {RandomInt} from "./commands/random-int";
|
||||
import {Ban} from "./commands/ban";
|
||||
import {Quote} from "./commands/quote";
|
||||
import {Ollama} from "ollama";
|
||||
import {WebSearchResponse} from "./model/web-search-response";
|
||||
import {OllamaSearch} from "./commands/ollama-search";
|
||||
import {Id} from "./commands/id";
|
||||
import {OllamaPrompt} from "./commands/ollama-prompt";
|
||||
import {AdminsAdd} from "./commands/admins-add";
|
||||
import {AdminsRemove} from "./commands/admins-remove";
|
||||
import {Shutdown} from "./commands/shutdown";
|
||||
import {OllamaKill} from "./commands/ollama-kill";
|
||||
import {Leave} from "./commands/leave";
|
||||
import {OllamaChat} from "./commands/ollama-chat";
|
||||
import {Start} from "./commands/start";
|
||||
import {MessageStore} from "./common/message-store";
|
||||
import {PrefixResponse} from "./commands/prefix-response";
|
||||
import {GoogleGenAI} from "@google/genai";
|
||||
import {GeminiChat} from "./commands/gemini-chat";
|
||||
import {Choice} from "./commands/choice";
|
||||
import {Coin} from "./commands/coin";
|
||||
import {Qr} from "./commands/qr";
|
||||
import {Distort} from "./commands/distort";
|
||||
import {CacheSize} from "./commands/cache-size";
|
||||
import {CacheClear} from "./commands/cache-clear";
|
||||
import {Dice} from "./commands/dice";
|
||||
import {Unban} from "./commands/unban";
|
||||
import {Title} from "./commands/title";
|
||||
import {MessageDao} from "./db/message-dao";
|
||||
import {DatabaseManager} from "./db/database-manager";
|
||||
import {UserDao} from "./db/user-dao";
|
||||
import {UserStore} from "./common/user-store";
|
||||
|
||||
process.setUncaughtExceptionCaptureCallback(console.error);
|
||||
|
||||
Environment.load();
|
||||
DatabaseManager.init();
|
||||
|
||||
export const messageDao = new MessageDao();
|
||||
export const userDao = new UserDao();
|
||||
|
||||
export const bot = new TelegramBot({botToken: Environment.BOT_TOKEN, testEnvironment: Environment.TEST_ENVIRONMENT});
|
||||
export let botUser: User;
|
||||
|
||||
export const ollama = new Ollama({
|
||||
host: Environment.OLLAMA_ADDRESS,
|
||||
headers: {"Authorization": `Bearer ${Environment.OLLAMA_API_KEY}`}
|
||||
});
|
||||
|
||||
export const googleAi = new GoogleGenAI({apiKey: Environment.GEMINI_API_KEY});
|
||||
|
||||
export let systemSpecsText: string = "";
|
||||
|
||||
export function setSystemSpecs(systemSpecs: string) {
|
||||
systemSpecsText = systemSpecs;
|
||||
}
|
||||
|
||||
export const chatCommands: ChatCommand[] = [
|
||||
new Start(),
|
||||
new Help(),
|
||||
new Test(),
|
||||
new Ae(),
|
||||
new Mute(),
|
||||
new Unmute(),
|
||||
new Ping(),
|
||||
new RandomInt(),
|
||||
new RandomString(),
|
||||
new SystemSpecs(),
|
||||
new Uptime(),
|
||||
|
||||
new WhatBetter(),
|
||||
new When(),
|
||||
|
||||
new Ban(),
|
||||
new Unban(),
|
||||
|
||||
new Quote(),
|
||||
new Id(),
|
||||
new Choice(),
|
||||
new Coin(),
|
||||
new Qr(),
|
||||
new Distort(),
|
||||
new Dice(),
|
||||
new Title(),
|
||||
|
||||
new AdminsAdd(),
|
||||
new AdminsRemove(),
|
||||
|
||||
new Shutdown(),
|
||||
new Leave(),
|
||||
|
||||
new OllamaChat(),
|
||||
new OllamaSearch(),
|
||||
new OllamaPrompt(),
|
||||
new OllamaKill(),
|
||||
|
||||
new GeminiChat(),
|
||||
|
||||
new CacheSize(),
|
||||
new CacheClear()
|
||||
];
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const messages = await messageDao.getAll();
|
||||
const users = await userDao.getAll();
|
||||
console.log("Messages: ", messages);
|
||||
console.log("Users: ", users);
|
||||
|
||||
const results = await Promise.all(
|
||||
[
|
||||
initSystemSpecs(), readData(), retrieveAnswers(),
|
||||
bot.getMe()
|
||||
]
|
||||
);
|
||||
botUser = results[3];
|
||||
await UserStore.put(botUser);
|
||||
await bot.startPolling();
|
||||
|
||||
console.log(`Bot started! testEnvironment: ${Environment.TEST_ENVIRONMENT}`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
bot.on("my_chat_member", async (u) => {
|
||||
console.log("my_chat_member", u);
|
||||
});
|
||||
|
||||
bot.on("message", async (message) => {
|
||||
console.log("message", message);
|
||||
|
||||
await UserStore.put(message.from);
|
||||
|
||||
if ((message.new_chat_members?.length || 0 > 0)) {
|
||||
await bot.sendMessage({chat_id: message.chat.id, text: randomValue(inviteAnswers)}).catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.left_chat_member && message.left_chat_member.id !== botUser.id) {
|
||||
await bot.sendMessage({chat_id: message.chat.id, text: randomValue(kickAnswers)}).catch(logError);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
bot.on("edited_message", async (msg) => {
|
||||
console.log("edited_message", msg);
|
||||
|
||||
await UserStore.put(msg.from);
|
||||
|
||||
if (!extractTextMessage(msg) || msg.from.id === botUser.id) return;
|
||||
|
||||
await MessageStore.put(msg);
|
||||
});
|
||||
|
||||
bot.on("message:text", async (msg) => {
|
||||
await MessageStore.put(msg);
|
||||
|
||||
if (muted.has(msg.from.id)) return;
|
||||
|
||||
if (msg.forward_origin) return;
|
||||
|
||||
const cmdText = msg.text || msg.caption || "";
|
||||
|
||||
const cmd = searchChatCommand(chatCommands, cmdText);
|
||||
const executed = await executeChatCommand(cmd, msg, cmdText);
|
||||
if (executed || !cmdText) return;
|
||||
|
||||
const startsWithPrefix = cmdText.toLowerCase().startsWith(Environment.BOT_PREFIX.toLowerCase());
|
||||
const messageWithoutPrefix = cmdText.substring(Environment.BOT_PREFIX.length).trim();
|
||||
|
||||
if (startsWithPrefix && messageWithoutPrefix.length === 0) {
|
||||
const prefixResponse = new PrefixResponse();
|
||||
if (await checkRequirements(prefixResponse, msg)) {
|
||||
await prefixResponse.execute(msg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!startsWithPrefix && msg.chat.type !== "private") return;
|
||||
if (msg.chat.type === "private" && !Environment.ADMIN_IDS.has(msg.chat.id)) return;
|
||||
|
||||
const chat = chatCommands.find(e => e instanceof OllamaChat);
|
||||
if (await checkRequirements(chat, msg)) {
|
||||
await chat.executeOllama(msg, startsWithPrefix ? messageWithoutPrefix : cmdText);
|
||||
}
|
||||
});
|
||||
|
||||
bot.on("inline_query", async (query) => {
|
||||
console.log("query", query);
|
||||
|
||||
if (Environment.CREATOR_ID !== query.from.id) {
|
||||
await bot.answerInlineQuery({
|
||||
inline_query_id: query.id,
|
||||
results: [],
|
||||
button: {
|
||||
text: "No access",
|
||||
start_parameter: "nope"
|
||||
}
|
||||
}).catch(logError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (query.query.trim().length !== 0) {
|
||||
try {
|
||||
const queryResults: InlineQueryResult[] = [];
|
||||
const results = await ollama.webSearch({query: query.query});
|
||||
|
||||
console.log("results", results);
|
||||
|
||||
results.results.forEach((result, i) => {
|
||||
const r = result as WebSearchResponse;
|
||||
queryResults.push({
|
||||
type: "article",
|
||||
id: `${i}`,
|
||||
title: `${r.title}`,
|
||||
input_message_content: {
|
||||
message_text: `${r.title}\n\n${r.url}`
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await bot.answerInlineQuery({
|
||||
inline_query_id: query.id,
|
||||
results: queryResults,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} else {
|
||||
await bot.answerInlineQuery({
|
||||
inline_query_id: query.id,
|
||||
results: [],
|
||||
}).catch(logError);
|
||||
}
|
||||
});
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,8 @@
|
||||
export type StoredMessage = {
|
||||
chatId: number;
|
||||
messageId: number;
|
||||
replyToMessageId?: number | null;
|
||||
fromId: number;
|
||||
text: string;
|
||||
date: number;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export type StoredUser = {
|
||||
id: number;
|
||||
isBot: boolean;
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
userName?: string;
|
||||
isPremium?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface WebSearchResponse {
|
||||
title: string;
|
||||
url: string;
|
||||
content: string;
|
||||
}
|
||||
@@ -0,0 +1,731 @@
|
||||
import * as si from "systeminformation";
|
||||
import {ChatCommand} from "../base/chat-command";
|
||||
import {CallbackCommand} from "../base/callback-command";
|
||||
import {
|
||||
CallbackQuery,
|
||||
InlineKeyboardMarkup,
|
||||
Message,
|
||||
ParseMode,
|
||||
PhotoSize,
|
||||
TelegramBot,
|
||||
User
|
||||
} from "typescript-telegram-bot-api";
|
||||
import {Environment} from "../common/environment";
|
||||
import {TelegramError} from "typescript-telegram-bot-api/dist/errors";
|
||||
import {bot, botUser, messageDao, setSystemSpecs} from "../index";
|
||||
import os from "os";
|
||||
import axios from "axios";
|
||||
import {MessagePart} from "../common/message-part";
|
||||
import {StoredMessage} from "../model/stored-message";
|
||||
import sharp from "sharp";
|
||||
import {UserStore} from "../common/user-store";
|
||||
import * as orm from "drizzle-orm";
|
||||
import {sql, type SQL} from "drizzle-orm";
|
||||
|
||||
export const ignore = () => {
|
||||
};
|
||||
|
||||
export const ignoreIfNotChanged = (e: Error | TelegramError) => {
|
||||
if (!(e instanceof TelegramError && e?.response?.description?.startsWith("Bad Request: message is not modified"))) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const ignoreIfMarkupFailed = (e: Error | TelegramError) => {
|
||||
if (!(e instanceof TelegramError && e?.response?.description?.startsWith("Bad Request: can't parse entities"))) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const logError = (e: Error | TelegramError) => {
|
||||
console.error(e);
|
||||
};
|
||||
|
||||
export const errorPlaceholder = async (msg: Message) => {
|
||||
await sendErrorPlaceholder(msg).catch(logError);
|
||||
};
|
||||
|
||||
export function searchChatCommand(commands: ChatCommand[], text: string): ChatCommand | null {
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
const command = commands[i];
|
||||
if (command?.regexp.test(text)) {
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function searchCallbackCommand(commands: CallbackCommand[], data: string): CallbackCommand | null {
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
const command = commands[i];
|
||||
if (!command?.data) continue;
|
||||
if (data.startsWith(command.data)) {
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function checkRequirements(cmd: ChatCommand | null, msg: Message): Promise<boolean> {
|
||||
if (!cmd) return false;
|
||||
|
||||
const fromId = msg.from?.id || -1;
|
||||
|
||||
if (Environment.CHAT_IDS_WHITELIST.size > 0 &&
|
||||
!Environment.CHAT_IDS_WHITELIST.has(msg.chat.id) &&
|
||||
!Environment.ADMIN_IDS.has(msg.chat.id) &&
|
||||
!Environment.ADMIN_IDS.has(msg.from.id)) {
|
||||
console.log(`${cmd.title}: chatId whitelist ignored.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const reqs = cmd.requirements;
|
||||
if (!reqs) return true;
|
||||
|
||||
if (reqs.isRequiresBotCreator() && fromId !== Environment.CREATOR_ID) {
|
||||
console.log(`${cmd.title}: creatorId is bad`);
|
||||
await replyToMessage(msg, "Вы не являетесь создателем бота.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reqs.isRequiresBotAdmin() && !Environment.ADMIN_IDS.has(fromId)) {
|
||||
console.log(`${cmd.title}: adminId is bad`);
|
||||
await replyToMessage(msg, "Вы не являетесь администратором бота.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reqs.isRequiresBotChatAdmin() && msg.chat.type !== "private") {
|
||||
const member = await bot.getChatMember({chat_id: msg.chat.id, user_id: botUser.id});
|
||||
const isAdmin = member.status === "administrator" || member.status === "creator";
|
||||
|
||||
if (!isAdmin) {
|
||||
console.log(`${cmd.title}: chatAdminId is bad`);
|
||||
await replyToMessage(msg, "Бот не является администратором чата.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (reqs.isRequiresChat() && msg.chat.type === "private") {
|
||||
console.log(`${cmd.title}: chatId is bad`);
|
||||
await replyToMessage(msg, "Тут Вам не чат.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reqs.isRequiresReply() && !msg.reply_to_message) {
|
||||
console.log(`${cmd.title}: replyMessage is bad`);
|
||||
await replyToMessage(msg, "Отсутствует ответ на сообщение.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function executeChatCommand(cmd: ChatCommand | null, msg: Message, text: string): Promise<boolean> {
|
||||
if (!cmd) return false;
|
||||
|
||||
if (!await checkRequirements(cmd, msg)) return false;
|
||||
|
||||
await cmd.execute(msg, cmd.regexp.exec(text));
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function findAndExecuteCallbackCommand(commands: CallbackCommand[], query: CallbackQuery): Promise<boolean> {
|
||||
const fromId = query.from.id;
|
||||
const data = query.data || "";
|
||||
|
||||
const command = searchCallbackCommand(commands, data);
|
||||
if (!command) return false;
|
||||
|
||||
const requirements = command.requirements;
|
||||
if (requirements) {
|
||||
if (requirements.isRequiresBotAdmin() && !Environment.ADMIN_IDS.has(fromId)) {
|
||||
console.log(`${command.data}: adminId is bad: ${fromId}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
await command.execute(query);
|
||||
await command.answerCallbackQuery(query);
|
||||
await command.afterExecute(query);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function editMessageText(chatId: number, messageId: number, messageText: string, parseMode?: ParseMode, replyMarkup?: InlineKeyboardMarkup): Promise<void> {
|
||||
if (messageText.trim().length === 0) return Promise.resolve();
|
||||
try {
|
||||
await bot.editMessageText({
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
text: messageText,
|
||||
parse_mode: parseMode,
|
||||
link_preview_options: {
|
||||
is_disabled: true
|
||||
},
|
||||
reply_markup: replyMarkup
|
||||
}).catch(ignoreIfMarkupFailed);
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
if (e instanceof TelegramError && e.response.description.includes("Too Many Requests")) {
|
||||
const delay = Number(e.message.split("retry after ")[1]) || 30;
|
||||
setTimeout(() => {
|
||||
return Promise.resolve();
|
||||
}, delay * 1000);
|
||||
} else if (e instanceof TelegramError && e.response.description.includes("MESSAGE_TOO_LONG")) {
|
||||
return Promise.reject(e);
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type SendOptions = {
|
||||
chatId?: number;
|
||||
message?: Message,
|
||||
text: string,
|
||||
parseMode?: ParseMode,
|
||||
};
|
||||
|
||||
export async function oldSendMessage(message: Message, text: string, parseMode?: ParseMode): Promise<Message> {
|
||||
const response = await bot.sendMessage({
|
||||
chat_id: message.chat.id,
|
||||
text: text,
|
||||
parse_mode: parseMode
|
||||
});
|
||||
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
export async function sendMessage(options: SendOptions): Promise<Message> {
|
||||
const response = await bot.sendMessage({
|
||||
chat_id: options.chatId ?? options.message?.chat?.id,
|
||||
text: options.text,
|
||||
parse_mode: options.parseMode
|
||||
});
|
||||
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
export async function replyToMessage(message: Message, text: string, parseMode?: ParseMode): Promise<Message> {
|
||||
const response = await bot.sendMessage({
|
||||
chat_id: message.chat.id,
|
||||
text: text,
|
||||
reply_parameters: {
|
||||
message_id: message.message_id
|
||||
},
|
||||
parse_mode: parseMode,
|
||||
});
|
||||
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
export async function sendErrorPlaceholder(message: Message): Promise<Message> {
|
||||
return await sendMessage({message: message, text: "Произошла ошибка ⚠️"}).catch(console.error) as Message;
|
||||
}
|
||||
|
||||
export async function initSystemSpecs(): Promise<void> {
|
||||
try {
|
||||
const [os, cpu, mem] = await Promise.all([si.osInfo(), si.cpu(), si.mem()]);
|
||||
setSystemSpecs(`OS: ${os.distro}\n` +
|
||||
`CPU: ${cpu.manufacturer} ${cpu.brand} ${cpu.physicalCores} cores ${cpu.cores} threads\n` +
|
||||
`RAM: ${Math.round(mem.total / Math.pow(2, 30))} GB`
|
||||
);
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
export function getRandomInt(max: number) {
|
||||
return Math.floor(Math.random() * Math.floor(max));
|
||||
}
|
||||
|
||||
export function getRangedRandomInt(from: number, to: number): number {
|
||||
return getRandomInt(to - from) + from;
|
||||
}
|
||||
|
||||
export function randomValue<T>(list: T[]): T {
|
||||
return list[Math.floor(Math.random() * list.length)];
|
||||
}
|
||||
|
||||
export function chatCommandToString(cmd: ChatCommand): string {
|
||||
if (!cmd.title && !cmd.description) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (cmd.title && cmd.description) {
|
||||
return `${cmd.title}: ${cmd.description}`;
|
||||
}
|
||||
|
||||
return `${cmd.title ? `${cmd.title}: ` : ""}${cmd.description ? `${cmd.description}` : ""}`;
|
||||
}
|
||||
|
||||
export function fullName(from: User): string {
|
||||
let fullName = from.first_name;
|
||||
|
||||
if (from.last_name) {
|
||||
fullName += " " + from.last_name;
|
||||
}
|
||||
|
||||
return fullName;
|
||||
}
|
||||
|
||||
export function getUptime(): string {
|
||||
const processUptime = Math.ceil(process.uptime());
|
||||
|
||||
const processDays = Math.floor(processUptime / (3600 * 24));
|
||||
const processHours = Math.floor((processUptime % (3600 * 24)) / 3600);
|
||||
const processMinutes = Math.floor((processUptime % 3600) / 60);
|
||||
const processSeconds = Math.floor(processUptime % 60);
|
||||
|
||||
const processUptimeText = `${processDays > 0 ? `${processDays} д. ` : ""}` +
|
||||
`${processHours > 0 ? `${processHours} ч. ` : ""}` +
|
||||
`${processMinutes > 0 ? `${processMinutes} м. ` : ""}` +
|
||||
`${processSeconds > 0 ? `${processSeconds} с.` : ""}`;
|
||||
|
||||
const osUptime = Math.ceil(os.uptime());
|
||||
|
||||
const osDays = Math.floor(osUptime / (3600 * 24));
|
||||
const osHours = Math.floor((osUptime % (3600 * 24)) / 3600);
|
||||
const osMinutes = Math.floor((osUptime % 3600) / 60);
|
||||
const osSeconds = Math.floor(osUptime % 60);
|
||||
|
||||
const osUptimeText = `${osDays > 0 ? `${osDays} д. ` : ""}` +
|
||||
`${osHours > 0 ? `${osHours} ч. ` : ""}` +
|
||||
`${osMinutes > 0 ? `${osMinutes} м. ` : ""}` +
|
||||
`${osSeconds > 0 ? `${osSeconds} с.` : ""}`;
|
||||
|
||||
return `${Environment.IS_DOCKER ? "Docker контейнер" : "Процесс"}:\n${processUptimeText}\n\nСистема:\n${osUptimeText}`;
|
||||
}
|
||||
|
||||
export const delay = (ms: number, signal?: AbortSignal): Promise<void> =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (signal?.aborted) {
|
||||
reject(new DOMException("Aborted", "AbortError"));
|
||||
return;
|
||||
}
|
||||
|
||||
const id = setTimeout(resolve, ms);
|
||||
|
||||
if (signal) {
|
||||
const onAbort = () => {
|
||||
clearTimeout(id);
|
||||
reject(new DOMException("Aborted", "AbortError"));
|
||||
};
|
||||
signal.addEventListener("abort", onAbort, {once: true});
|
||||
}
|
||||
});
|
||||
|
||||
export function escapeMarkdownV2Text(s: string) {
|
||||
s = s.replace(/^\*{3,}\s*$/gm, "— — —");
|
||||
s = s.replace(/^\*\s+(?=\S)/gm, "• ");
|
||||
s = s.replace(/\*\*(.+?)\*\*/g, "*$1*");
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
export async function getFileUrl(bot: TelegramBot, fileId: string) {
|
||||
const file = await bot.getFile({file_id: fileId});
|
||||
return `https://api.telegram.org/file/bot${bot.botToken}/${file.file_path}`;
|
||||
}
|
||||
|
||||
export async function getChatAvatar(bot: TelegramBot, chatId: number): Promise<Buffer | null> {
|
||||
try {
|
||||
const chat = await bot.getChat({chat_id: chatId});
|
||||
const photo = chat?.photo?.big_file_id || chat?.photo?.small_file_id;
|
||||
if (!photo) return null;
|
||||
|
||||
const url = await getFileUrl(bot, photo);
|
||||
const res = await axios.get<ArrayBuffer>(url, {responseType: "arraybuffer"});
|
||||
return Buffer.from(res.data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserAvatar(bot: TelegramBot, userId: number): Promise<Buffer | null> {
|
||||
const photos = await bot.getUserProfilePhotos({user_id: userId, limit: 1});
|
||||
const last: PhotoSize | undefined = photos.photos?.[0]?.[photos.photos[0].length - 1];
|
||||
if (!last) return null;
|
||||
|
||||
const url = await getFileUrl(bot, last.file_id);
|
||||
const res = await axios.get<ArrayBuffer>(url, {responseType: "arraybuffer"});
|
||||
return Buffer.from(res.data);
|
||||
}
|
||||
|
||||
export function extractTextMessage(msg: Message, prefix: string = ""): string | null {
|
||||
let text = (msg?.text ?? msg?.caption ?? "").trim();
|
||||
if (text.toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||
text = text.substring(prefix.length);
|
||||
}
|
||||
|
||||
text = text.trim();
|
||||
if (text.length === 0) return null;
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export function extractTextStored(msg: StoredMessage, prefix: string): string {
|
||||
let text = (msg?.text ?? "").trim();
|
||||
if (text.toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||
text = text.substring(prefix.length).trim();
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export function extractText(text: string, prefix: string): string {
|
||||
if (text.toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||
text = text.substring(prefix.length).trim();
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export async function collectReplyChainText(triggerMsg: Message, prefix: string = Environment.BOT_PREFIX, limit: number = 40, includeTrigger = true): Promise<MessagePart[]> {
|
||||
const chatId = triggerMsg.chat.id as number;
|
||||
|
||||
const parts: MessagePart[] = [];
|
||||
if (includeTrigger) {
|
||||
const t = extractTextMessage(triggerMsg, prefix);
|
||||
if (t) parts.push({
|
||||
bot: triggerMsg.from.id === botUser.id,
|
||||
content: triggerMsg.text,
|
||||
name: triggerMsg.from.first_name
|
||||
});
|
||||
}
|
||||
|
||||
const first = triggerMsg.reply_to_message;
|
||||
if (!first) {
|
||||
return parts;
|
||||
}
|
||||
|
||||
const firstText = extractTextMessage(first, prefix);
|
||||
if (firstText) parts.push({bot: first.from.id === botUser.id, content: firstText, name: first.from.first_name});
|
||||
|
||||
let curId = first.message_id;
|
||||
|
||||
while (parts.length < limit) {
|
||||
const cur = await messageDao.getById({chatId: chatId, id: curId});
|
||||
const parentId = cur?.replyToMessageId ?? null;
|
||||
if (!parentId) break;
|
||||
|
||||
const parent = await messageDao.getById({chatId: chatId, id: parentId});
|
||||
if (!parent?.text) break;
|
||||
|
||||
const user = await UserStore.get(parent.fromId);
|
||||
|
||||
parts.push({
|
||||
bot: parent.fromId === botUser.id,
|
||||
content: extractTextStored(parent, prefix),
|
||||
name: user?.firstName
|
||||
});
|
||||
curId = parentId;
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
export function extractMessagePayload(msg: Message, matchText?: string): string | null {
|
||||
const payload = (matchText ?? "").trim();
|
||||
if (payload.length) return payload;
|
||||
|
||||
const quote = msg.quote;
|
||||
if (quote?.text) return quote.text;
|
||||
|
||||
const r = msg.reply_to_message;
|
||||
if (!r) return null;
|
||||
|
||||
const t =
|
||||
(r.text ?? "") ||
|
||||
(r.caption ?? "") ||
|
||||
(r.document?.file_name ?? "") ||
|
||||
"";
|
||||
|
||||
return t.trim().length ? t.trim() : null;
|
||||
}
|
||||
|
||||
export function clamp(n: number, a: number, b: number) {
|
||||
return Math.max(a, Math.min(b, n));
|
||||
}
|
||||
|
||||
export async function waveDistortSharp(
|
||||
input: Buffer,
|
||||
amp = 14,
|
||||
wavelength = 72,
|
||||
maxSide = 1024
|
||||
): Promise<Buffer> {
|
||||
amp = clamp(amp, 2, 60);
|
||||
wavelength = clamp(wavelength, 16, 300);
|
||||
|
||||
const phase1 = Math.random() * Math.PI * 2;
|
||||
const phase2 = Math.random() * Math.PI * 2;
|
||||
const amp2 = Math.max(6, Math.floor(amp * 0.6));
|
||||
const wavelength2 = Math.max(32, Math.floor(wavelength * 1.4));
|
||||
|
||||
const {data, info} = await sharp(input)
|
||||
.resize({width: maxSide, height: maxSide, fit: "inside", withoutEnlargement: true})
|
||||
.ensureAlpha()
|
||||
.raw()
|
||||
.toBuffer({resolveWithObject: true});
|
||||
|
||||
const width = info.width!;
|
||||
const height = info.height!;
|
||||
const channels = info.channels!; // обычно 4 (RGBA)
|
||||
|
||||
const out = Buffer.alloc(data.length);
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
const dx = amp * Math.sin((2 * Math.PI * y) / wavelength + phase1);
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
const dy = amp2 * Math.sin((2 * Math.PI * x) / wavelength2 + phase2);
|
||||
|
||||
const sx = Math.round(x + dx);
|
||||
const sy = Math.round(y + dy);
|
||||
|
||||
const di = (y * width + x) * channels;
|
||||
|
||||
if (sx < 0 || sx >= width || sy < 0 || sy >= height) {
|
||||
// прозрачный пиксель
|
||||
out[di + 0] = 0;
|
||||
out[di + 1] = 0;
|
||||
out[di + 2] = 0;
|
||||
out[di + 3] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
const si = (sy * width + sx) * channels;
|
||||
data.copy(out, di, si, si + channels);
|
||||
}
|
||||
}
|
||||
|
||||
return await sharp(out, {raw: {width, height, channels}})
|
||||
.png()
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
export async function downloadTelegramFile(filePath: string): Promise<Buffer> {
|
||||
const url = `https://api.telegram.org/file/bot${Environment.BOT_TOKEN}/${filePath}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`Failed to download file: ${res.status} ${res.statusText}`);
|
||||
const ab = await res.arrayBuffer();
|
||||
return Buffer.from(ab);
|
||||
}
|
||||
|
||||
export function extractImageFileId(reply: Message): string | null {
|
||||
// photo (сжатое)
|
||||
if (reply.photo?.length) {
|
||||
return reply.photo[reply.photo.length - 1]!.file_id; // самое большое
|
||||
}
|
||||
// document (обычно оригинал)
|
||||
if (reply.document?.mime_type?.startsWith("image/")) {
|
||||
return reply.document.file_id;
|
||||
}
|
||||
|
||||
if (reply.sticker?.file_id) {
|
||||
return reply.sticker.file_id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function makeDarkGradientBgFancy(
|
||||
width: number,
|
||||
height: number,
|
||||
seed?: string
|
||||
): Promise<Buffer> {
|
||||
const rnd = seed ? seededRand(seed) : Math.random;
|
||||
|
||||
const hue1 = Math.floor(rnd() * 360);
|
||||
const hue2 = (hue1 + 25 + Math.floor(rnd() * 55)) % 360;
|
||||
const hue3 = (hue2 + 25 + Math.floor(rnd() * 55)) % 360;
|
||||
|
||||
const c1 = hslToHex(hue1, 35 + rndInt(rnd, 0, 14), 12 + rndInt(rnd, 0, 6));
|
||||
const c2 = hslToHex(hue2, 35 + rndInt(rnd, 0, 14), 9 + rndInt(rnd, 0, 5));
|
||||
const c3 = hslToHex(hue3, 30 + rndInt(rnd, 0, 14), 8 + rndInt(rnd, 0, 5));
|
||||
|
||||
// случайный угол градиента
|
||||
const x1 = rnd(), y1 = rnd();
|
||||
const x2 = 1 - x1, y2 = 1 - y1;
|
||||
|
||||
// мягкое свечение
|
||||
const glowHue = (hue1 + rndInt(rnd, -25, 25) + 360) % 360;
|
||||
const glowColor = hslToHex(glowHue, 60, 60);
|
||||
const glowCx = 0.35 + rnd() * 0.30;
|
||||
const glowCy = 0.30 + rnd() * 0.35;
|
||||
const glowR = 0.55 + rnd() * 0.25;
|
||||
const glowOpacity = 0.14 + rnd() * 0.10;
|
||||
|
||||
// виньетка
|
||||
const vignetteStrength = 0.55 + rnd() * 0.15;
|
||||
|
||||
// зерно
|
||||
const grainSeed = Math.floor(rnd() * 10_000);
|
||||
const grainAlpha = 0.10 + rnd() * 0.06; // 0.10..0.16
|
||||
const grainFreq = 0.75 + rnd() * 0.35; // 0.75..1.10
|
||||
|
||||
const svg = Buffer.from(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">
|
||||
<stop offset="0%" stop-color="${c1}"/>
|
||||
<stop offset="55%" stop-color="${c2}"/>
|
||||
<stop offset="100%" stop-color="${c3}"/>
|
||||
</linearGradient>
|
||||
|
||||
<radialGradient id="glow" cx="${glowCx}" cy="${glowCy}" r="${glowR}">
|
||||
<stop offset="0%" stop-color="${glowColor}" stop-opacity="${glowOpacity}"/>
|
||||
<stop offset="55%" stop-color="${glowColor}" stop-opacity="${glowOpacity * 0.45}"/>
|
||||
<stop offset="100%" stop-color="${glowColor}" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
|
||||
<radialGradient id="vig" cx="0.5" cy="0.5" r="0.85">
|
||||
<stop offset="0%" stop-color="#000" stop-opacity="0"/>
|
||||
<stop offset="55%" stop-color="#000" stop-opacity="${vignetteStrength * 0.25}"/>
|
||||
<stop offset="100%" stop-color="#000" stop-opacity="${vignetteStrength}"/>
|
||||
</radialGradient>
|
||||
|
||||
<!-- зерно: чёрный шум с маленькой альфой -->
|
||||
<filter id="grain" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feTurbulence type="fractalNoise"
|
||||
baseFrequency="${grainFreq}"
|
||||
numOctaves="2"
|
||||
seed="${grainSeed}"
|
||||
result="t"/>
|
||||
<!-- RGB -> Alpha, RGB = 0 -->
|
||||
<feColorMatrix in="t" type="matrix"
|
||||
values="0 0 0 0 0
|
||||
0 0 0 0 0
|
||||
0 0 0 0 0
|
||||
0.33 0.33 0.33 0 0"
|
||||
result="a"/>
|
||||
<!-- масштабируем альфу (интенсивность зерна) -->
|
||||
<feComponentTransfer in="a">
|
||||
<feFuncA type="linear" slope="${grainAlpha}"/>
|
||||
</feComponentTransfer>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="url(#bg)"/>
|
||||
<rect width="100%" height="100%" fill="url(#glow)"/>
|
||||
<rect width="100%" height="100%" fill="url(#vig)"/>
|
||||
<rect width="100%" height="100%" filter="url(#grain)"/>
|
||||
</svg>`);
|
||||
|
||||
return sharp(svg)
|
||||
.resize(width, height)
|
||||
.blur(0.6) // чуть сгладить градиент/свечение (зерно тоже мягче)
|
||||
.png()
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
export function rndInt(rnd: () => number, min: number, max: number) {
|
||||
return Math.floor(rnd() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
export function seededRand(seed: string): () => number {
|
||||
// xorshift32 via seed->uint32 (FNV-ish)
|
||||
let x = 2166136261;
|
||||
for (let i = 0; i < seed.length; i++) {
|
||||
x ^= seed.charCodeAt(i);
|
||||
x = Math.imul(x, 16777619);
|
||||
}
|
||||
x >>>= 0;
|
||||
|
||||
return () => {
|
||||
x ^= x << 13;
|
||||
x >>>= 0;
|
||||
x ^= x >> 17;
|
||||
x >>>= 0;
|
||||
x ^= x << 5;
|
||||
x >>>= 0;
|
||||
return (x >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
export function hslToHex(h: number, s: number, l: number) {
|
||||
s /= 100;
|
||||
l /= 100;
|
||||
|
||||
const k = (n: number) => (n + h / 30) % 12;
|
||||
const a = s * Math.min(l, 1 - l);
|
||||
const f = (n: number) =>
|
||||
l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
||||
|
||||
const r = Math.round(255 * f(0));
|
||||
const g = Math.round(255 * f(8));
|
||||
const b = Math.round(255 * f(4));
|
||||
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
}
|
||||
|
||||
function toHex(v: number) {
|
||||
return v.toString(16).padStart(2, "0");
|
||||
}
|
||||
|
||||
export function startIntervalEditor(params: {
|
||||
intervalMs: number;
|
||||
getText: () => string;
|
||||
editFn: (text: string) => Promise<void>;
|
||||
}) {
|
||||
let lastSent = "";
|
||||
let stopped = false;
|
||||
|
||||
const tick = async () => {
|
||||
if (stopped) return;
|
||||
const next = params.getText();
|
||||
if (!next || next === lastSent) return;
|
||||
|
||||
console.log("tick");
|
||||
|
||||
try {
|
||||
await params.editFn(next);
|
||||
lastSent = next;
|
||||
} catch (e) {
|
||||
if ((e?.description ?? e?.message ?? "").includes("message is not modified")) return;
|
||||
console.error("edit failed:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const timer = setInterval(async () => await tick(), params.intervalMs);
|
||||
|
||||
return {
|
||||
tick,
|
||||
stop: async () => {
|
||||
stopped = true;
|
||||
clearInterval(timer);
|
||||
await tick();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function boolToInt(bool: boolean): number {
|
||||
return bool ? 1 : 0;
|
||||
}
|
||||
|
||||
type AnyDrizzleTable = {
|
||||
_: {
|
||||
columns: Record<string, { name: string }>;
|
||||
};
|
||||
};
|
||||
|
||||
export function buildExcludedSet<
|
||||
T extends AnyDrizzleTable,
|
||||
K extends keyof T["_"]["columns"] & string,
|
||||
E extends readonly K[] = readonly []
|
||||
>(table: T, exclude: E = [] as unknown as E): Record<Exclude<K, E[number]>, SQL> {
|
||||
const cols = orm.getColumns(table as never) as T["_"]["columns"];
|
||||
const excludeSet = new Set<string>(exclude as readonly string[]);
|
||||
|
||||
const entries = Object.keys(cols)
|
||||
.filter((key) => !excludeSet.has(key))
|
||||
.map((key) => {
|
||||
const realName = (cols as unknown)[key].name; // actual DB column name
|
||||
return [key, sql.raw(`excluded.${realName}`)] as const;
|
||||
});
|
||||
|
||||
return Object.fromEntries(entries) as Record<Exclude<K, E[number]>, SQL>;
|
||||
}
|
||||
Reference in New Issue
Block a user