Кулстори: Реквест-бот Вконтакте (nodejs, vk-io, liquidsoap) — Mikulski
Наложение сайта

Кулстори: Реквест-бот Вконтакте (nodejs, vk-io, liquidsoap)

В продолжение тем:
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);