Простой ChatGPT бот для Telegram

В этом боте реализован лишь базовый функционал, позволяющий получать доступ к 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}/

Например, что бы запустить конкретный инстанс, нужно выполнить следующий скрипт:

Bash
#!/bin/bash

APP_INSTANCE=4o docker compose -p 4o -f ../docker-compose.yml up -d


Контроль доступа пользователей осуществляется при помощи списка разрешенных идентификаторов, перечисляемых в файле allowed.list.

Бот помещен в Docker контейнер, конфигурация которого приведена ниже:

YAML
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:

Dockerfile
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


Полный исходный код бота незамысловат, и приведен ниже:

JavaScript
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. Необходимость его использования задается соответствующим флагом в конфигурационном файле.

Из основных недостатков этой реализации можно отметить отсутствие диалогового режима, и лишь базовая система контроля доступа пользователей. Так же отсутствует контроль потраченных токенов. Кроме того, в некоторых случаях, могла бы быть полезной работа с изображениями, которая так же отсутствует. Но для личного использования этот бот отлично себя зарекомендовал.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *