NodeJS Криптовалюты Обучение Телеграм

Пишем телеграм бот автопродаж на NodeJS и принимаем платежи в биткоинах. Абсолютно бесплатно. Полный ликбез для самых маленьких и ленивых

Пишем телеграм бот автопродаж на NodeJS и принимаем платежи в биткоинах. Абсолютно бесплатно. Полный ликбез для самых маленьких и ленивых

Мне очень часто задают вопросы о создании ботов в телеграм и автоматизации продаж. И в этом нет абсолютно ничего сложного. Важно лишь понять основы, знать как взаимодействовать с API Telegram и иметь рудиментарные навыки программирования. Сегодня я расскажу как создать такой бот абсолютно бесплатно. Всё, как ты любишь.

Я пошагово распишу как создать Telegram-бота с функцией автопродаж и оплатой товаров биткоинами. Для разработки я буду использовать NodeJS, MySQL и Blockchain. NodeJS я решил использовать потому что, он преимущественно создан для серверов, да и на этом язаке уже были наработки в этом направлении. 

Для этих же задач великолепно подходят и другие языки програмирования, например, многие люди имеющие дело с разработкой ботов предпочитают Python. Для любителей Python, примеры уже готовых ботов можно глянуть здесь, а прочитать о том, как это делать – например, здесь.

В любом случае, для того, чтобы понять основы, можно за пару часов освоить основы и NodeJS, разобраться с тем, что написано здесь, а потом переключиться уже на своё.

В интернете куча материалов о том, как установить NodeJS и MySQL, поэтому я не буду тратить на это время, демонстрируя полную установку. 

Концепция

Концепция такая: клиент запускает бота, дальше у него  есть две команды на выбор: /showproducts – показать продукты, /checkorder – проверить статус заказа. При просмотре всех продуктов клиент будет получать ID, имя, описание, цену(в долларах), количество товара в наличии и снизу под сообщением инлайновая кнопочка “Купить”. Все что ему нужно будет сделать это нажать на кнопку купить, получить ID заказа и реквизиты для пополнения, оплатить и проверить статус заказа по его ID.  Всё просто, но нам больше и не надо.

После того как клиент будет нажимать “Купить” нам будет приходить ID товара и его цена в долларах, затем мы просто пересчитываем по текущему курсу, подбираем свободный для оплаты адрес, добавляем данные в бд и N-количество минут будем проверять прошла ли оплата, если оплата прошла будет добавлять в таблицу с ордерами продукт, по истечении определенного времени (90 минут в нашем случае) в случае не оплаты ордера он будет удаляться из бд, что бы не захламлять базу. Так же у нас будет админка со своими командами для добавления/удаления продуктов и т.д.

Создание и настройка бота

Первое надо сделать это получить токен бота для дальнейшей работы. Находим в телеграмме botfather, запускаем его и пишем /newbot после чего он попросит дать название боту, а затем и юзернейм по которому его будут находить другие пользователи, юзернейм обязательно должен заканчиваться на bot, когда все будет сделано вы получите токен для доступа к боту, никому не пересылайте этот токен, это чревато компрометация вашего магазина.  Небольшой список второстепенных настроек, которые вы можете сделать: 

Далее устанавливаем основные команды для бота которые будут видны пользователю, сначало отправляем бате ботов /setcommands, выбираем там нашего бота и отправляем ему следующие команды одним сообщением: 

Ну вот впринципе и все настройки бота.

База данных 

Теперь самое интересное, база данных, тут все проще чем может показаться на первый взгляд, я назвал свою бд my_store, у нас будет три таблицы: my_order, my_products, my_productsinfo. Команда для создания таблиц:

CREATE TABLE my_products( -- здесь будут сами товары
    product_id INT NOT NULL,
    product_data VARCHAR(255) NOT NULL
    );

CREATE TABLE my_productsinfo( -- здесь будет информация о самих товарах
    product_id INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(255) NOT NULL,
    description VARCHAR(255) NOT NULL,
    price INT NOT NULL,
    PRIMARY KEY(product_id)
    );

CREATE TABLE my_orders( -- здесь будут храниться непосредственно сами заказы
    order_id CHAR(32) CHARACTER SET 'latin1' NOT NULL
    address VARCHAR(255) NOT NULL,
    status VARCHAR(255) NOT NULL,
    price FLOAT(8,8) NOT NULL,
    product_id INT NOT NULL,
    product_data VARCHAR(255),
    order_data TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    );

Бот

Пришло время писать код нашего бота, и начнем пожалуй с создания конфиг файла (config.js) в котором будут храниться настройки нашего магазина.

module.exports = {
    authToken: "1701858052:AAGWYQ5Kp-4EiOi_9GJKBXqMYj3UrIzXHhk",  
    MySQL: {
        client: "mysql",
        connection: {
            host: "127.0.0.1",
            user: "root",
            password: "",
            database: "my_store"
        }
    },
    adminChatId: 437619229,
    xPub: 'xpub6FBEgyfiZ79TbeZdgo39Ahr4pRQaoqJMAs7mQNV8MLPaHB19PX7PMhPP12Hjp32jduEA2rQ93DNYgtzm92ZAUizKdUAGWnYdxWCmJwNCtpK'
}

Тут не так уж и много настроек, первое authToken это токен бота который мы получили на этапе создания бота в botfather просто вставляем свой токен и все. Далее настройка соединения с бд, тут мы указываем адресс хоста, имя пользователя, пароль и название базы данных если оно отличается. Что бы узнать свой ID чата, нужно отправить нашему боту /echo, дальше вы увидите это в коде.  

xPub а вот это уже штучка поинтереснее, xPub – это расширенный открытый ключ. Он является частью стандарта биткоина BIP32. Если у вас есть xPub ключ, единственное что вы можете сделать это генерировать адреса, но уже без приватных ключей к ним, в нашем случае это очень удобно, так как если кто-то вдруг взломает ваш сервер и будет видеть код который выполняется, он не сможет спиздить ваши бетки, максимум что он сможет сделать это облизнуться при виде того сколько вы продали и на какую сумму, так уж устроен xPub. Где взять? В https://www.blockchain.com/, регистрируем там себе новый аккаунт, специально для нашего магазинчика и переходим в Настройки->Кошельки и адреса->Мой кошелек Bitcoin->Управлять->Дополнительные опции->Показать xPub, вуаля, вот и ваш xPub. Я выбрал Blockchain потому что это удобно и просто, да и вы 100% слышали об этом кошельке. Дальнейшие коментарии будут в коде.

сonst conf = require('./Config') //
const MD5 = require("md5")
const {Telegraf, Markup} = require('telegraf')
const bot = new Telegraf(conf.authToken)
const knex = require('knex')(conf.MySQL)
const Axios = require('axios')
const bjs = require('bitcoinjs-lib');
const XPubGenerator = require('xpub-generator').XPubGenerator;

const TenMinutes = 10 * 60 * 1000 //Интервал с которым мы будем проверять коши на оплату
var Status = 'Sleep' //Текущее действие в админке
var checkorder = [] //Массив в котором будут храниться id чатов для проверки ордеров, дальше поймете
var Product = {
    Name: '',
    Description: '',
    Price: 0
}

bot.start(ctx => { //Собственно привественное сообщение, при старте бота
    ctx.reply(`Добро пожаловать ${ctx.message.from.first_name}, рад приветствовать тебя в моем магазине\n/showproducts - Просмотр всех продуктов \n/checkorder - Проверить статус заказа`)
})

bot.help( ctx => ctx.reply('/showproducs - Просмотр всех продуктов \n/checkorder - Проверить статус заказа'))

 /* Команды для покупателей*/

async function calcPrice(price){ //Эта функия будет пересчитывать $ в BTC по текущему курсу
    try{
        let response = await Axios.get(`https://web-api.coinmarketcap.com/v1/tools/price-conversion?amount=${price}&convert_id=1&id=2781`)
        return Number(response.data.data.quote['1'].price.toFixed(8))  
    } catch(err){
        return 'Error'
    }     
}

async function getBalance(address){ //Функция проверки баланса
    try{
        let response = await Axios.get(`https://chain.api.btc.com/v3/address/${address}`)        
        return {received: Number((response.data.data.received * 0.00000001).toFixed(8)), unconfirmed: Number((response.data.data.unconfirmed_received * 0.00000001).toFixed(8))}
    } catch(err){
        return {received: 'Error', unconfirmed: 'Error'}
    }
}

bot.on('callback_query', async ctx =>{//Событие которое срабатывает при нажатии на кнопку купить
    try{
        let t = ctx.update.callback_query.data.split('$') //тут мы сплитаем дату которая вложена в кнопку которую нажали
        let summa = await calcPrice(t[1]) //считаем цену
        if (summa === 'Error') throw new Error('Во время просчета цены произошла ошибка')         
        let didi = -1
        let addresses = []
        for (let addr of await knex('my_orders').select('address')) addresses.push(addr.address) //вытаскиваем из бд btc-адресса заказов       
        
        do {
            didi++
            t_address = new XPubGenerator(conf.xPub, bjs.networks.bitcoin).nthReceiving(didi)    
        } while (addresses.includes(t_address)) //По xPub генерируем адреса до тех пор пока не попадется тот которого нет в бд
        
        let Arra = {
            order_id: MD5(Date.now().toString+ctx.update.callback_query.id), //Уникальный ID заказа по которому в итоге клиент будет находить заказ
            address: t_address,
            status: 'В ожидании оплаты',            
            price: summa,                    
            product_id: t[0],
            product_data: 'Будет доступно после оплаты'        
        } 
        await knex('my_orders').insert(Arra) //Создаем ордер 
        ctx.reply(`Ваш заказ находится в обработке, в случае не оплаты в течении полутора часа, заказ будет ликвидирован. \nID заказа: ${Arra.order_id}\nРеквизиты для оплаты: ${Arra.address}\nСумма к оплате: ${Arra.price}\nВы можете проверить статус вашего заказа отправив отправив команду /checkorder`)
    } catch(err){
        ctx.reply('Произошла ошибка попробуйте позднее')
    }
})

bot.command('/showproducts', ctx =>{ //ответ на команду показать продукты
    knex.select().from('my_productsinfo')
    .then( resp =>{
        for (let product of resp){
            knex('my_products').where({product_id: product.product_id}).count({count: '*'})
            .then( resp => ctx.reply(`ID: ${product.product_id}\nName: ${product.name}\nDescription: ${product.description}\nPrice: ${product.price}$\nCount: ${resp[0].count}`,
                Markup.inlineKeyboard([Markup.button.callback('Купить', `${product.product_id}$${product.price}`)]) )) //Дата в кнопке это ID$Price продукта
            .catch( err => ctx.reply('Произошла ошибка при получении товаров'))            
        }
    })
    .catch(err => ctx.reply('Ошибка при получении списка продуктов'))
})

bot.command('/checkorder', ctx =>{ //Переводим пользователя в режим проверки ордера
    checkorder.push(ctx.message.chat.id)
    ctx.reply('Введите ID заказа')
}) 

bot.on('text', async (ctx, next) =>{ //Это событие срабатывает на все текстовые сообщения
    if (checkorder.includes(ctx.message.chat.id)){ //Собственно если чат в режиме проверки заказа выполняется следующий код       
        const STF = await knex('my_orders').where({order_id: ctx.message.text})
        if (STF[0] == undefined){            
            ctx.reply('Ордер не найден')
        } else {                             
            ctx.reply(`ID заказа: ${STF[0].order_id}\nID продукта: ${STF[0].product_id}\nРеквизиты: ${STF[0].address}\nСумма к оплате: ${STF[0].price}\nСтатус: ${STF[0].status}\nТовар: ${STF[0].product_data}`)
        }                    
    checkorder.splice(checkorder.indexOf(ctx.message.chat.id), 1) //Удаляем из массива, соответственно статус проверки заказа убирается                           
    }
    next()
})

bot.command('/echo', ctx =>{ //Эта команда нужна что бы узнать id чата с нами, после того как укажите нужный id в конфиге можете удалять эту команду
  ctx.reply(ctx.message.chat.id)  
})

/* Команды для администратора*/

bot.use((ctx, next) =>{ //Интересная вещь, middleware, те кто юзал фреймворк Express, точно знают что это за штучка 
    if (ctx.message.chat.id === conf.adminChatId) next() // Если мы из чата администратора то едем дальше и выполнятся следующие функции
})

bot.command('/cancel', ctx =>{ 
    Status = 'Sleep' //Отменяем текущие операции
    ctx.reply('Все текущие операции были отменены')
})

bot.command('/addproduct', ctx =>{
    Status = 'AddProduct_N' //перехоим в режим добавления продукта
    ctx.reply('Укажите название товара')     
})

bot.command('/addproductdata', ctx =>{
    Status = 'AddProductData' //добавляем сами продукты
    ctx.reply('Отправьте Данные для добавления в формате ID$ProductData\nНапример 3$email:password')     
})

bot.command('/showproductdata', ctx =>{
    knex('my_products').select() //Показывает все товары которые есть на продажу
    .then( resp => ctx.reply(resp))
    .catch( err => ctx.reply('Произошла ошибка')) 
})

bot.command('/delproductdata', ctx =>{
    Status = 'DelProductData' //Переходим в режим удаления какой-то определенного продукта из таблицы my_products
    ctx.reply('Отправьте данные о продукте который хотите удалить в следующем формате ID$ProductData')         
})

bot.command('/delproduct', ctx =>{
    Status = 'DelProduct' //Удаляем продукты которые видит клиент
    ctx.reply('Отправьте ID продукта который хотите удалить')  
})

bot.on('text', ctx =>{ //Обрабатывает то что мы вводим, то что вводит админ
    switch(Status){ //То что находится в этом свиче я описал выше
        case 'DelProduct':
            Status = 'Sleep'
            knex('my_productsinfo').where({product_id: ctx.message.text}).del()
            .then( resp => ctx.reply('Товар Успешно удален'))
            .catch( err => ctx.reply('Во время удаления произошла ошибка'))
            break
        case 'AddProduct_N':
            Status = 'AddProduct_D'
            Product.Name = ctx.message.text
            ctx.reply('Укажите описание товара') 
            break    
        case 'AddProduct_D':
            Status = 'AddProduct_P'
            Product.Description = ctx.message.text
            ctx.reply('Укажите цену товара')
            break
        case 'AddProduct_P':
            Status = 'Sleep'
            Product.Price = parseInt(ctx.message.text)
            knex('my_productsinfo').insert({name: Product.Name, description: Product.Description, price: Product.Price})
            .then( resp =>ctx.reply('Товар успешно добавлен'))
            .catch( err => ctx.reply('Произошла ошибка во время добавления товара'))
            break
        case 'AddProductData':
            Status = 'Sleep'
            let t = ctx.message.text.split('$')
            knex('my_products').insert({product_id: t[0], product_data: t[1]})
            .then( resp => ctx.reply('Продукт успешно добавлен в БД'))                
            .catch( err => ctx.reply('Во время добавления в БД произошла ошибка'))                
            break   
        case 'DelProductData':
            Status = 'Sleep'
            let t = ctx.message.text.split('$')
            knex('my_products').where({product_id: t[0], product_data: t[1]}).del()
            .then( resp => ctx.reply('Продукт успешно удален'))            
            .catch( err => ctx.reply('Во время удаления произошла ошибка'))
            break
    }
})


bot.launch().then( () =>{ //Собственно стартуем нашего бота
    console.log('Bot Started!') 
    let timerId = setInterval( async () => { //После старта запускаем таймер который будет срабатывать каждые 10 минут, для проверки ордеров и удаления лишнего     
        my_orders = await knex('my_orders').whereNot({status: 'Выполнен'}).select('address', 'status', 'price', 'product_id', 'order_data')
        for (let order of my_orders){ //Получаем заказы которые не выполнены и проходимся по каждому из заказов
            let balance = await getBalance(order.address)
            if (balance.received >= order.price){ //Если есть баланс то собственно изменяем статус, и закидываем продукт
                let response = await knex('my_products').where({product_id: order.product_id})
                if (response != 0){
                    await knex('my_products').where({product_id: response[0].product_id, product_data: response[0].product_data}).del()
                    await knex('my_orders').where({address: order.address}).update({status: 'Выполнен', product_data: response[0].product_data})
                }
            } else if (balance.unconfirmed  >= order.price){ //Смотрим есть ли не подтвержденные ордеры
                await knex('my_orders').where({address: order.address}).update({status: 'В ожидании подтверждений'})
            } else if (balance.received != 'Error'){ //Удаляем лишние ордеры если прошло 90 и больше минут с момента его создания
                if (order.order_data.setMinutes(order.order_data.getMinutes()+90) <= new Date ){
                    await knex('my_orders').where({address: order.address}).del()
                }
            }     
        }        
    }, TenMinutes) 
})

Запускаем бота, добавляем чат с самим собой в админку, добавляем продукты в лист, командой /addproduct и добавляем сами товары командой /addproductdata. Все, магазин запущен, можно смело продавать.

Заключение

Все гениальное просто. Особенно боты. Мы с вами прошли по пути меньшего сопротивления и упростили всё по максимуму, проще уже некуда.

Уже дальше можно сделать, чтобы бот картинки выдавал или принимал оплату в другой крипте, но это уже делай сам, я же не стал ничего усложнять, что бы оставить непринужденность и легкость, показал концепт, основные моменты и то, что из этого всего получается. Перед запуском не забудь инициализировать проект командой npm i в консоли, в случае если вы сами будете вручную переписывать, не забывайте устанавливать требующиеся фреймворки вручную.

Статья написана по материалам Konung. Спасибо ему большое за предоставленную информацию.

Ссылки и источники 

Очень злой админ
Очень злой админ Автор статьи

Админ сайта. Публикует интересные статьи с других ресурсов, либо их переводы. Если есть настроение, бывает, что пишет и что-то своё.

Leave a Reply

Your email address will not be published. Required fields are marked *