В продолжение тем: https://mikulski.rocks/ru/kak-ustroena-rekvest-sistema/ - принцип работы реквестов, боты для Twitch и DonationAlerts https://mikulski.rocks/ru/kulstory-obnovi-na-mikulski_radio/ - Telegram-бот. https://mikulski.rocks/ru/kulstory-discord-rekvest-bot/ - Discord-бот.
Вообще, я никогда не рассматривал VK в качестве платформы для реквестов на Mikulski_Radio, так как считал это бессмысленной затеей. Но желание «прикрутить что-нибудь» к серверу все-таки победило.
Найдя несколько рабочих api-библиотек для nodejs, я решил, что дело за малым: всего-то скопировать команды из других моих ботов, да чуть их отредактировать. Но, как обычно, все пошло не столь гладким путем. Плюс, стоит упомянуть несколько нюансов, о которых стоит знать, если вы решите собрать своего бота.
Настройка сообщества и получение токена
Для публичных страниц/сообществ процесс получения токена достаточно прост: нужно перейти в меню управления сообществом, затем на вкладку «Работа с API» и нажать «Создать ключ», выдав разрешения на управление сообществом и доступ к сообщениям.
Полученный ключ и есть наш токен.
Вероятно, что доступ к управлению сообществом необязателен, но в моем случае, бот без этой отметки не реагировал на сообщения.



Далее надо перейти на вкладку «Long Poll API» (бот использует только этот метод,- Callback API, в данном примере, можно проигнорировать), убедиться, что он включен и перейти на следующую вкладку «Типы событий», где надо отметить чекбокс «Входящее сообщение».


Теперь нужно включить возможность писать сообщения в ваше сообщество:
Вкладка «Сообщения» -> включить «Сообщения сообщества».
А также в подменю «Настройки для бота» -> включить «Возможности ботов»



После этого, можно создать чат и настроить его.
Так как мне нужен закрытый чат для подписчиков Boosty, то я отключил его видимость на странице сообщества в списке чатов, а также указал, что только администраторы могут делиться ссылкой на эту беседку и видеть ее.
Наверное, здесь стоит упомянуть о первых любопытных моментах: по умолчанию, бот будет видеть все входящие сообщения. И те, что приходят в ЛС сообщества и те, что приходят в чат. Следовательно и на команды он будет реагировать из обоих источников.
Но все же есть неочевидный обходной путь, чтобы бот реагировал на сообщения только из чата — об этом подробнее будет сказано, когда доберемся до кода.
Второй момент. По правилам VK, нельзя выставлять какое-либо требование для пользователя, чтобы бот выполнял свои функции.
Честно говоря, не очень понятно, подпадает ли мой бот под нарушение, ведь он работает в закрытом чате, доступ к которому я выдаю вручную подписчикам. При этом сам бот ничего взамен не клянчит и указанные правила так-то соблюдает.
Тем не менее, за нарушение грозит отключение бота и бан возможностей API. Об этом стоит знать и иметь в виду.
VK-IO
Сначала я стал собирать бота на библиотеке node-vk-bot-api, так как она мне показалась проще в освоении. Но, когда дело дошло до первых попыток работоспособности скрипта выяснилось, что при перезапуске бота — он по новой прочитывал историю сообщений и пытался исполнить все введенные до перезапуска команды. Очевидно, что это можно было поправить с помощью кода, но для меня это оказалось непосильной задачей. А потому более простым решением оказалось переехать на библиотеку VK-IO и ее дополнительный модуль VK-IO Hear, который облегчает работу с командами.
Создается папка для бота и в нее устанавливаются необходимые репозитории:
npm i vk-io@2.2.7
npm i @vk-io/hear
Файл config.json, где будет храниться ключ/токен, полученный в самом начале:
{
"token": "КЛЮЧ ВК"
}
Самый простейший скрипт будет выглядеть примерно так (я брал пример отсюда):
// Импорт библиотек
const { VK, Upload } = require('vk-io'),
{ HearManager } = require('@vk-io/hear');
// Импорт токена и создание класса-подключения(?)
const {token} = require('./config.json');
const vk = new VK({token});
//Создание отлова команд в новых сообщениях
const command = new HearManager();
vk.updates.on('message_new', command.middleware);
//Команда приветствия пользователя с обращением по имени-фамилии
command.hear('/start', async (context) => {
let [userData]= await vk.api.users.get({user_id: context.senderId});
await context.reply(`Привет, ${userData.first_name} ${userData.last_name}!`);
})
//запуск бота
vk.updates.start()
.then(() => console.log('Бот запущен!'))
.catch(console.error);
Стоит отметить, что бот может отвечать, цитируя сообщение пользователя, как указано в примере: context.reply
Либо, без цитаты: context.send
Теперь как быть с тем, чтобы бот слушал команды только в чате? Если, вкратце, то нужно указывать peer_id больше 2000000000. Все, что меньше — это личные сообщения. Мне попался достаточно изящный работающий метод (к сожалению, не могу найти сейчас источник):
if (String(context.message.peer_id)[0] == 2)
Если же нужно, чтобы бот читал только личные сообщения, но не чаты, то видел вот такое решение (сам не тестил):
if(context.message.chatId) return //вместо chatId - peer_id?
Не знаю, как делать это правильно — я просто подставляю данное условие во все команды:
command.hear('/start', async (context) => {
let [userData]= await vk.api.users.get({user_id: context.senderId});
if (String(context.message.peer_id)[0] == 2)
await context.reply(`Привет, ${userData.first_name} ${userData.last_name}!`);
})
Особенно веселый колхоз в блоке /sr, где необходимо соблюдать несколько условий:
//request
command.hear(/^\/sr (.+)/, async (context) => {
const argument = context.$match[1].toLowerCase();
let [userData]= await vk.api.users.get({user_id: context.senderId});
if (String(context.message.peer_id)[0] == 2) //РАЗ
fs.writeFile(request,'/home/mikulski/radio/music/Mikulski - ' + argument +'.mp4', (err) => {
if (err) {
console.log(err);
}});
if (String(context.message.peer_id)[0] == 2) //ДВА
fs.writeFile(next_song, `VK request by ${userData.first_name} - ` + argument, (err) => {
if (err) {
console.log(err);
}});
if (String(context.message.peer_id)[0] == 2) //ТРИ
console.log(`VK request by ${userData.first_name} - ` + argument);
if (String(context.message.peer_id)[0] == 2) //ЧЕТЫРЕ!!!
setTimeout(response, 3500); //напомню, что здесь дается время другому скрипту на проверку реквеста
function response() {
fs.readFile(next_song, "utf8", function(error,data2) {
if(error) throw error;
context.reply(data2);
});
}
});
Подозреваю, что нужно правильно перенести закрывающие скобки. Но вот так сходу не получилось подобрать их расположение, поэтому я забил и оставил как есть. Если кто-то сможет указать как это выглядит правильно, то дайте знать — поправлю!
В общем целом, мой скрипт для реквестов выглядит примерно вот так:
const fs = require('fs');
const request = '/home/mikulski/radio/request';
const next_song = '/home/mikulski/radio/next-song';
const current_song = '/home/mikulski/radio/current-song';
const { VK, Upload } = require('vk-io'),
{ HearManager } = require('@vk-io/hear');
const {token} = require('./config.json');
const vk = new VK({token});
const command = new HearManager();
vk.updates.on('message_new', command.middleware);
//start
command.hear('/start', async (context) => {
let [userData]= await vk.api.users.get({user_id: context.senderId});
if (String(context.message.peer_id)[0] == 2)
await context.reply(`Привет, ${userData.first_name} ${userData.last_name}!\nИспользуй команду /help чтобы получить список команд`);
})
//current_song
command.hear('/now', async (context) => {
if (String(context.message.peer_id)[0] == 2)
fs.readFile(current_song, (err, data) => {
if (err) throw error;
context.reply('Сейчас играет: ' + data);
});
});
//next_song
command.hear('/next', async (context) => {
if (String(context.message.peer_id)[0] == 2)
fs.readFile(next_song, (err, data) => {
if (err) throw error;
context.reply('' + data);
});
});
//songlist
command.hear('/songlist', async (context) => {
if (String(context.message.peer_id)[0] == 2)
context.reply('Сонглист можно посмотреть здесь: https://mikulski.rocks/ru/tracklist/');
});
//how
command.hear('/how', async (context) => {
if (String(context.message.peer_id)[0] == 2)
context.reply('Важно точно указывать название трека без лишних символов и пробелов, иначе скрипт не найдет нужный файл для воспроизведения.' +
'\nЯ настоятельно рекомендую пользоваться /songlist – копируйте название и вставляйте после /sr.');
});
//help
command.hear('/help', async (context) => {
let [userData]= await vk.api.users.get({user_id: context.senderId});
if (String(context.message.peer_id)[0] == 2)
await context.reply('/sr Название трека - заказать трек'
+ '\n/now - показать текущий воспроизводимый трек\n/next - показать следующий трек в очереди\n'+
'/songlist - ссылка на список треков\n/how - инструкция');
})
//request
command.hear(/^\/sr (.+)/, async (context) => {
const argument = context.$match[1].toLowerCase();
let [userData]= await vk.api.users.get({user_id: context.senderId});
if (String(context.message.peer_id)[0] == 2)
fs.writeFile(request,'/home/mikulski/radio/music/Mikulski - ' + argument +'.mp4', (err) => {
if (err) {
console.log(err);
}});
if (String(context.message.peer_id)[0] == 2)
fs.writeFile(next_song, `VK request by ${userData.first_name} - ` + argument, (err) => {
if (err) {
console.log(err);
}});
if (String(context.message.peer_id)[0] == 2)
console.log(`VK request by ${userData.first_name} - ` + argument);
if (String(context.message.peer_id)[0] == 2)
setTimeout(response, 3500);
function response() {
fs.readFile(next_song, "utf8", function(error,data2) {
if(error) throw error;
context.reply(data2);
});
}
});
vk.updates.start()
.then(() => console.log('Бот запущен!'))
.catch(console.error);