В этом боте реализован лишь базовый функционал, позволяющий получать доступ к ChatGPT при помощи платного OpenAI API, выполняя запросы через мессенджер Telegram.
Бот написан на Node.js. Взаимодействие с Telegram API реализовано при помощи библиотеки telegraf
, а доступ к OpenAI API осуществляется с использованием официальной библиотеки openai
.
Кроме того, для беспрепятственного доступа к OpenAI API реализована возможность подключения через socks5 прокси. При этом необходимость использования прокси и его параметры указываются в общем конфиге.
Также стоит добавить, что бот поддерживает работу нескольких одновременно запущенных экземпляров. Это может быть полезно для реализации доступа для разных версий API ChatGPT, например один бот для ChatGPT 3.5, а второй для ChatGPT 4o. Для каждого хоста и экземпляра настраивается своя конфигурация, которая находится в:
/data/secrets/${SERVER_DOMAIN}/tgbot-chatgpt/${APP_INSTANCE}/
Например, что бы запустить конкретный инстанс, нужно выполнить следующий скрипт:
#!/bin/bash
APP_INSTANCE=4o docker compose -p 4o -f ../docker-compose.yml up -d
Контроль доступа пользователей осуществляется при помощи списка разрешенных идентификаторов, перечисляемых в файле allowed.list
.
Бот помещен в Docker контейнер, конфигурация которого приведена ниже:
services:
tgbot-chatgpt:
container_name: tgbot-chatgpt-${APP_INSTANCE}
build:
context: .
env_file:
- /data/secrets/${SERVER_DOMAIN}/tgbot-chatgpt/${APP_INSTANCE}/app.env
volumes:
- .:/app:rw
- app_node_modules:/app/node_modules
- /data/secrets/${SERVER_DOMAIN}/tgbot-chatgpt/${APP_INSTANCE}/allowed.list:/app/allowed.list:ro
command: npm run start
restart: unless-stopped
volumes:
node_modules:
app_node_modules:
FROM node:22.7.0-alpine3.19
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY /src /app/src
ENV NODE_PATH=./node_modules
ENV HELLO_FILE=hello.txt
ENV ALLOWED_FILE=allowed.list
Полный исходный код бота незамысловат, и приведен ниже:
import { Telegraf } from 'telegraf';
import { message } from 'telegraf/filters';
import { code } from 'telegraf/format';
import OpenAI from 'openai';
import { promises as fs } from 'fs';
import { SocksProxyAgent } from 'socks-proxy-agent';
const isInstruct = process.env.API_INSTRUCT === 'true';
const proxyEnable = process.env.PROXY_ENABLE;
let proxyConfig = {};
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const timeout = parseInt(process.env.TIMEOUT, 10);
let allowedUsers = [];
console.log("GPT_MODEL: " + process.env.GPT_MODEL);
console.log("API_INSTRUCT: " + process.env.API_INSTRUCT);
if(proxyEnable === 'true') {
const proxyHost = process.env.PROXY_HOST;
const proxyPort = process.env.PROXY_PORT;
const proxyLogin = process.env.PROXY_LOGIN;
const proxyPassword = process.env.PROXY_PASSWORD;
const proxyOptions = `socks5://${proxyLogin}:${proxyPassword}@${proxyHost}:${proxyPort}`;
const proxyAgent = new SocksProxyAgent(proxyOptions);
proxyConfig = { httpAgent: proxyAgent, httpsAgent: proxyAgent };
}
const bot = new Telegraf(
process.env.BOT_TOKEN,
{ handlerTimeout: timeout }
);
bot.command('new', initCommand);
bot.command('help', initCommand);
bot.command('start', initCommand);
async function chatGPT(content) {
try {
const chatCompletion = await openai.chat.completions.create({
messages: [
{ role: 'system', content: 'Ответ пиши без markdown.' },
{ role: 'user', content }
],
model: process.env.GPT_MODEL,
}, proxyConfig);
return chatCompletion.choices[0].message.content;
} catch (e) {
console.log('Error while gpt chat', e.message);
}
}
async function chatGPTinstruct(content) {
try {
const chatCompletion = await openai.completions.create({
model: process.env.GPT_MODEL,
prompt: content,
max_tokens: 3000
}, proxyConfig);
return chatCompletion.choices[0].text;
} catch (e) {
console.log('Error while gpt chat', e.message);
}
}
bot.on(message('text'), async (ctx) => {
if (await auth(ctx.message.from.id)) {
try {
await ctx.reply(code('🕰️⏰🕙⏱️⏳...'));
let responce;
if(isInstruct) {
responce = await chatGPTinstruct(ctx.message.text);
} else {
responce = await chatGPT(ctx.message.text);
}
if(responce) {
await ctx.reply(responce);
} else {
await ctx.reply("Не могу ответить");
}
} catch (e) {
console.log('Error GPT', e.message);
}
} else {
await ctx.reply(code(`Access denied for ${ctx.message.from.first_name} (${ctx.message.from.id})`));
}
});
async function fileToString(filePath) {
try {
return await fs.readFile(filePath, { encoding: 'utf-8' });
} catch {
return 'Error';
}
}
async function initCommand(ctx) {
const helloText = await fileToString(process.env.HELLO_FILE);
await ctx.reply(helloText);
}
async function auth(fromId) {
try {
return allowedUsers.includes(fromId);
} catch(error) {
console.error(error);
return false;
}
}
async function loadAllowedIds(filePath) {
try {
const data = await fileToString(filePath);
const lines = data.split('\n');
return lines
.map(line => line.split('//')[0].trim())
.filter(line => line.length > 0)
.map(line => parseInt(line, 10))
.filter(id => !isNaN(id));
} catch (err) {
console.error(err);
return [];
}
}
async function start() {
allowedUsers = await loadAllowedIds(process.env.ALLOWED_FILE);
await bot.launch();
}
start();
Дополнительно в этом коде присутствует реализация доступа к устаревшей версии API instruct
. Необходимость его использования задается соответствующим флагом в конфигурационном файле.
Из основных недостатков этой реализации можно отметить отсутствие диалогового режима, и лишь базовая система контроля доступа пользователей. Так же отсутствует контроль потраченных токенов. Кроме того, в некоторых случаях, могла бы быть полезной работа с изображениями, которая так же отсутствует. Но для личного использования этот бот отлично себя зарекомендовал.