Русский
Русский
English
Статистика
Реклама

Discord

Из песочницы Создание Discord-бота, используя библиотеку discord.js Часть 1

23.06.2020 20:12:31 | Автор: admin

Введение


В этой статье я подробно расскажу о том, как работать с библиотекой discord.js, создать своего Discord-бота, а также покажу несколько интересных и полезных команд.

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

Начало работы


Если вы уже знакомы с приведёнными ниже материалами, смело можете пролистать этот раздел.

Установка среды разработки
Для начала работы с кодом нам нужно установить среду разработки, это может быть:


и так далее.

Среда разработки выбирается по удобству использования и практичности, она у вас может быть любая, но мы рассмотрим её на примере Visual Studio Code, так как она является одной из самых приемлемых для новичков, а также для опытных программистов.

Для установки переходим по этой ссылке.


Выбираем свою операционную систему и запускаем скачивание.

Установка среды выполнения
Для создания бота мы используем среду выполнения node.js. Для её установки нам необходимо перейти на этот сайт.



На данный момент нас интересует версия долгосрочной поддержки (LTS), скачиваем её.

Установка полезных расширений
В Visual Studio Code присутствует возможность устанавливать расширения.
Для этого, кликните по отмеченной ниже иконке.



В открывшемся окне вписываем название название/идентификатор нужного нам расширения, после чего устанавливаем его.



Из полезных расширений могу отметить:

  1. Discord Presence расширение, позволяющее отображать рабочую область и файл, в котором вы работаете в вашей игровой активности (функция работает только при использовании приложения Discord).

    Идентификатор расширения: icrawl.discord-vscode


  2. Code runner расширение, с помощью которого предоставляется возможность запускать определённые фрагменты кода.
    Идентификатор расширения: formulahendry.code-runner


Создание бота


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

Здесь всё просто. Переходим на портал разработчиков и нажимаем на кнопку с надписью New Application она находится в правом верхнем углу.

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



На этой странице мы можем изменить имя бота, загрузить для него иконку, заполнить описание.

Теперь наша задача воплотить бота в жизнь. Для этого переходим во вкладку Bot.



Нажимаем на кнопку с надписью Add Bot и воплощаем бота в жизнь.

Поздравляю! Вы создали аккаунт для вашего бота. Теперь у него есть тег, токен, ник и иконка.

Подготовка к написанию кода


После создания аккаунта для бота, мы должны установить нужные пакеты и модули, чтобы в дальнейшем он корректно работал.

Первым делом создаём папку, после чего открываем её в VS Code (Файл > Открыть папку) / (Ctrl + K Ctrl + O)

Далее нам нужно открыть терминал (Терминал > Создать терминал) / (Ctrl + Shift + `)



Теперь мы должны создать файл с неким описанием нашего бота, сделаем это через терминал.

Вписываем данную строку в терминал и нажимаем Enter:

npm init

После каждой появившейся строки нажимаем Enter или вписываем свои значения.
Значения в этом файле можно будет изменить в любой момент.

Далее, мы должны поочерёдно вводить в терминал эти строки:

npm install

npm install discord.js

Install также можно сокращать в I, но необязательно.

Итого, если вы следовали инструкциям и всё сделали правильно, в вашей папке должны были появиться 3 объекта:



Написание кода


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

Существует множество вариантов для его написания: используя один файл, два, несколько, и т.д

Мы рассмотрим вариант с двумя файлами, так как его использовать очень легко и удобно, но у каждого варианта есть свои недостатки например, у этого недостатком является сложность в написании начального кода.

Но не волнуйтесь, весь код вам писать не придётся.

О хранении данных
Для начала, нам нужно где-то хранить основные параметры и информацию о боте.

Мы можем сделать это двумя способами:

  1. Создать отдельный файл
  2. Записать всё в константы

Я не советую вам использовать второй вариант, так как в будущем вам придётся работать с большим объёмом информации, и такая запись будет доставлять неудобства.

Разберём хранение параметров в отдельном файле.

Итак, создаем файл config.json

Вставляем в него следующий код:

{    "token" : "Ваш_токен",    "prefix" : "Ваш_префикс"}

* Для получения токена зайдите на портал разработчиков, перейдите во вкладку Bot и скопируйте его.



* Самым распространённым среди разработчиков префиксом является !

Далее нам нужно создать файл bot.js и вставить в него данный код:

const Discord = require('discord.js'); // Подключаем библиотеку discord.jsconst robot = new Discord.Client(); // Объявляем, что robot - ботconst comms = require("./comms.js"); // Подключаем файл с командами для ботаconst fs = require('fs'); // Подключаем родной модуль файловой системы node.js  let config = require('./config.json'); // Подключаем файл с параметрами и информациейlet token = config.token; // Вытаскиваем из него токенlet prefix = config.prefix; // Вытаскиваем из него префиксrobot.on("ready", function(){ /* Бот при запуске должен отправить в терминал сообщение [Имя бота] запустился! */console.log(robot.user.username + " запустился!");});robot.on('message', (msg) => { // Реагирование на сообщенияif(msg.author.username != robot.user.username && msg.author.discriminator != robot.user.discriminator){    var comm = msg.content.trim()+" ";    var ok = false;    var comm_name = comm.slice(0, comm.indexOf(" "));    var messArr = comm.split(" ");    for(comm_count in comms.comms){    var comm2 = prefix + comms.comms[comm_count].name;    if(comm2 == comm_name){    comms.comms[comm_count].out(robot, msg, messArr);    }    }    } });robot.login(token); // Авторизация бота

Теперь создаём файл comms.js, в нём будут сами команды.

В нём должен быть следующий код:

const config = require('./config.json'); // Подключаем файл с параметрами и информациейconst Discord = require('discord.js'); // Подключаем библиотеку discord.jsconst prefix = config.prefix; // Вытаскиваем префикс// Команды //    function test(robot, mess , args) {        mess.channel.send('Test!')    } // Список комманд //var comms_list = [{name: "test", out: test, about: "Тестовая команда"}];// Name - название команды, на которую будет реагировать бот// Out - название функции с командой// About - описание команды module.exports.comms = comms_list;

Чтобы добавить больше команд просто объявляйте больше функций и добавляйте их в список, например:

const config = require('./config.json');const Discord = require('discord.js');const prefix = config.prefix;const versions = config.versions;// Команды //    function test(robot, mess, args) {        mess.channel.send("Тест!")    }    function hello(robot, mess, args) {        mess.reply("Привет!")    } // Список комманд //var comms_list = [{name: "test", out: test, about: "Тестовая команда"},        {name: "hello", out: hello, about: "Команда для приветствия!"}}module.exports.comms = comms_list;

И вот, мы вышли на финишную прямую!

Осталось всего ничего запустить бота.

Для этого открываем терминал и вставляем в него следующую строку:

node bot.js



Готово! Бот запущен и вы можете им пользоваться, ура!

Чтобы пригласить бота на свой сервер, воспользуемся нам уже известным порталом разработчиков.

Перейдём во вкладку OAuth2, пролистаем чуть ниже, выберем Bot и отметим нужные боту привилегии.

Теперь осталось скопировать ссылку-приглашение и добавить бота на свой сервер.



Как вывести ссылку-приглашение в терминал, при запуске бота?
Существует два способа:

  1. Заранее отметить нужные привилегии.

    Для этого, сначала мы должны скопировать ссылку-приглашение.
    После чего перейти в файл bot.js и вставить данную строчку кода сюда:

    robot.on("ready", function(){    console.log(robot.user.username + " запустился!");    console.log("Ссылка-приглашение")  // << //})
    

    Итоговый код должен быть таким:

    const Discord = require('discord.js'); const robot = new Discord.Client();var comms = require("./comms.js");const fs = require('fs');let config = require('./config.json');let token = config.token;let prefix = config.prefix; robot.on("ready", function(){    console.log(robot.user.username + " запустился!");    console.log("Ссылка-приглашение")})robot.on('message', (msg) => {if(msg.author.username != robot.user.username && msg.author.discriminator != robot.user.discriminator){    var comm = msg.content.trim()+" ";    var ok = false;    var comm_name = comm.slice(0, comm.indexOf(" "));    var messArr = comm.split(" ");    for(comm_count in comms.comms){    var comm2 = prefix + comms.comms[comm_count].name;    if(comm2 == comm_name){    comms.comms[comm_count].out(robot, msg, messArr);    }    }    } });robot.login(token)  robot.login(token);
    

  2. Отметить нужные привилегии в самом коде.

    Повторяем процедуры из первого способа, но уже с другими строками кода:

    robot.on("ready", function(){    console.log(robot.user.username + " запустился!");    robot.generateInvite(["ADMINISTRATOR"]).then((link) => { // < //        console.log(link); // < //})})
    

    Итоговый код:

    const Discord = require('discord.js'); const robot = new Discord.Client();var comms = require("./comms.js");const fs = require('fs');let config = require('./config.json');let token = config.token;let prefix = config.prefix;robot.on("ready", function(){    console.log(robot.user.username + " запустился!");    robot.generateInvite(["ADMINISTRATOR"]).then((link) => {         console.log(link);})})robot.on('message', (msg) => {if(msg.author.username != robot.user.username && msg.author.discriminator != robot.user.discriminator){    var comm = msg.content.trim()+" ";    var ok = false;    var comm_name = comm.slice(0, comm.indexOf(" "));    var messArr = comm.split(" ");    for(comm_count in comms.comms){    var comm2 = prefix + comms.comms[comm_count].name;    if(comm2 == comm_name){    comms.comms[comm_count].out(robot, msg, messArr);    }    }    } });   robot.login(token);
    

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

        robot.generateInvite(['KICK_MEMBERS', 'BAN_MEMBERS', 'SEND_MESSAGES']).then((link) => {         console.log(link);
    

    * Все привилегии указываются заглавными буквами

    Список доступных привилегий:

    ADMINISTRATOR
    CREATE_INSTANT_INVITE
    KICK_MEMBERS
    BAN_MEMBERS
    MANAGE_CHANNELS
    MANAGE_GUILD
    ADD_REACTIONS
    VIEW_AUDIT_LOG
    PRIORITY_SPEAKER
    STREAM
    VIEW_CHANNEL
    SEND_MESSAGES
    SEND_TTS_MESSAGES
    MANAGE_MESSAGES
    EMBED_LINKS
    ATTACH_FILES
    READ_MESSAGE_HISTORY
    MENTION_EVERYONE
    USE_EXTERNAL_EMOJIS
    VIEW_GUILD_INSIGHTS
    CONNECT
    SPEAK
    MUTE_MEMBERS
    DEAFEN_MEMBERS
    MOVE_MEMBERS
    USE_VAD
    CHANGE_NICKNAME
    MANAGE_NICKNAMES
    MANAGE_ROLES
    MANAGE_WEBHOOKS
    MANAGE_EMOJIS


    Я не советую вам из привилегий выбирать только ADMINISTRATOR, поэтому лучше указать только те привилегии, которые бот действительно использует для корректной работы


Полезные и интересные команды


В предыдущем разделе я показал вам, как запустить бота и как писать для него команды.
Теперь я хочу поделиться с вами несколькими своими командами.

  1. !say с помощью этой команды бот может повторить ваше сообщение.



    Код:

        if(!mess.member.hasPermission("MANAGE_MESSAGES")) return mess.channel.send("У  вас нет прав"); /* Если у исполнителя команды нету привилегии MANGAGE_MESSAGES, он не сможет её использовать */        let robotmessage = args = mess.content.split(' '); // Пробелы между словами     args.shift();    args = args.join(' ');    mess.delete().catch(); // Удаление сообщения пользователя после отправки     mess.channel.send(robotmessage).then(mess.channel.send(mess.author)) /*             Отправление в чат сообщения бота */
    

  2. !heads_or_tails игра Орёл или Решка.



    Код:

            mess.channel.send('Монета подбрасывается...')        var random = Math.floor(Math.random() * 4); // Объявление переменной random - она вычисляет случайное число от 1 до 3        if (random == 1) { // Если вычислено число 1, то выпадает орёл.            mess.channel.send(':full_moon: Орёл!')        } else if (random == 2) { // Если вычислено число 2, то выпадает решка.            mess.channel.send(':new_moon: Решка!')        } else if (random == 3) { // Если вычислено число 3, то монета падает         ребром.            mess.channel.send(':last_quarter_moon: Монета упала ребром!')        }
    

  3. !clear удаление определённого количества сообщений.



    Код:

            const arggs = mess.content.split(' ').slice(1); // Все аргументы за именем команды с префиксом        const amount = arggs.join(' '); // Количество сообщений, которые должны быть удалены        if (!amount) return mess.channel.send('Вы не указали, сколько сообщений нужно удалить!'); // Проверка, задан ли параметр количества        if (isNaN(amount)) return mess.channel.send('Это не число!'); // Проверка, является ли числом ввод пользователя                 if (amount > 100) return mess.channel.send('Вы не можете удалить 100 сообщений за раз'); // Проверка, является ли ввод пользователя числом больше 100        if (amount < 1) return mess.channel.send('Вы должны ввести число больше чем 1'); // Проверка, является ли ввод пользователя числом меньше 1                async function delete_messages() { // Объявление асинхронной функции        await mess.channel.messages.fetch({ limit: amount }).then(messages => {            mess.channel.bulkDelete(messages)            mess.channel.send(`Удалено ${amount} сообщений!`)        })};        delete_messages(); // Вызов асинхронной функции
    
  4. !random_name генерация случайного имени.



    Не стоит пугаться большого кода, здесь всё предельно просто.

    Код:

            var name = new Array( // Объявление массива name и занесение в него большого количества имён            'Абрам',' Аваз',' Аввакум',' Август',' Августин',            ' Авдей',' Авраам',' Автандил',' Агап',' Агафон',            ' Аггей',' Адам',' Адис',' Адольф',' Адриан',            ' Азамат',' Айдар',' Айнур',' Айрат',' Аким',            ' Алан',' Алей',' Александр',' Алексей',' Али',            ' Альберт',' Альфред',' Амадей',' Амадеус',            ' Амаяк',' Амвросий',' Ананий',' Анастасий',            ' Анатолий',' Анвар',' Ангел',' Андоим',' Андрей',            ' Аникита',' Антон',' Арам',' Арий',' Аристарх',            ' Аркадий',' Арман',' Арно',' Арнольд',' Арон',' Арсен',            ' Арсений',' Арслан',' Артем',' Артемий',' Артур',' Архип'            ,' Аскар',' Аскольд',' Аслан',' Афанасий',' Ахмет',' Ашот'            ,' Бальтазар',' Бежен',' Бенедикт',' Берек',' Бернард',            ' Бертран',' Богдан',' Болеслав',' Борис',' Бронислав',            ' Булат',' Вадим',' Валентин',' Валерий',' Вальтер',            ' Варфоломей',' Василий',' Вацлав',' Велизар',' Венедикт',' Вениамин',' Викентий',' Виктор',' Вилли',' Вильгельм',' Виссарион',' Виталий',' Витольд',' Владимир',' Владислав',' Владлен',' Володар',' Вольдемар',' Всеволод',' Вячеслав',' Гавриил',' Галактион',' Гарри',' Гастон',' Гаяс',' Гевор',' Геннадий',' Генрих',' Георгий',' Геракл',' Геральд',' Герасим',' Герман',' Глеб',' Гордей',' Гордон',' Горислав',' Градимир',' Григорий',' Гурий',' Густав',' Давид',' Дамир',' Даниил',' Даниэль',' Данияр',' Дарий',' Дементий',' Демид',' Демосфен',' Демьян',' Денис',' Джамал',' Джордан',' Дмитрий',' Добрыня',' Дональд',' Донат',' Дорофей',' Евгений',' Евграф',' Евдоким',' Евсевий',' Евсей',' Евстафий',' Егор',' Елеазар',' Елисей',' Емельян',' Еремей',' Ермолай',' Ерофей',' Ефим',' Ефрем',' Жан',' Ждан',' Жорж',' Захар',' Зиновий',' Ибрагим',' Иван',' Игнатий',' Игорь',' Илларион',' Ильдар',' Ильнар',' Ильнур',' Илья',' Ильяс',' Иннокентий',' Иоанн',' Иосиф',' Ипполит',' Искандер',' Ислам',' Камиль',' Карим',' Карл',' Кирилл',' Клим',' Кондрат',' Константин',' Корней',' Кузьма',' Лавр',' Лаврентий',' Лев',' Леон',' Леонид',' Леонтий',' Леопольд',' Лука',' Лукьян',' Любим',' Макар',' Максим',' Максимилиан',' Марат',' Марк',' Марсель',' Мартин',' Матвей',' Мирон',' Мирослав',' Митрофан',' Михаил',' Михей',' Мишель',' Мстислав',' Мурат',' Муслим',' Назар','Абрам',' Аваз',' Аввакум',' Август',' Августин',' Авдей',' Авраам',' Автандил',' Агап',' Агафон',' Аггей',' Адам',' Адис',' Адольф',' Адриан',' Азамат',' Айдар',' Айнур',' Айрат',' Аким',' Алан',' Алей',' Александр',            ' Алексей',' Али',' Альберт',' Альфред',' Амадей',' Амадеус',' Амаяк',' Амвросий',' Ананий',' Анастасий',' Анатолий',' Анвар',' Ангел',' Андоим',' Андрей',' Аникита',' Антон',' Арам',' Арий',' Аристарх',' Аркадий',' Арман',' Арно',' Арнольд',' Арон',' Арсен',' Арсений',' Арслан',' Артем',' Артемий',' Артур',' Архип',' Аскар',' Аскольд',' Аслан',' Афанасий',' Ахмет',' Ашот',' Бальтазар',' Бежен',' Бенедикт',' Берек',' Бернард',' Бертран',' Богдан',' Болеслав',' Борис',' Бронислав',' Булат',' Вадим',' Валентин',' Валерий',' Вальтер',' Варфоломей',' Василий',' Вацлав',' Велизар',' Венедикт',' Вениамин',' Викентий',' Виктор',' Вилли',' Вильгельм',' Виссарион',' Виталий',' Витольд',' Владимир',' Владислав',' Владлен',' Володар',' Вольдемар',' Всеволод',' Вячеслав',' Гавриил',' Галактион',' Гарри',' Гастон',            ' Гаяс',' Гевор',' Геннадий',' Генрих',' Георгий',' Геракл',            ' Геральд',' Герасим',' Герман',' Глеб',' Гордей',' Гордон',            ' Горислав',' Градимир',' Григорий',' Гурий',' Густав',            ' Давид',' Дамир',' Даниил',' Даниэль',' Данияр',            ' Дарий',' Дементий',' Демид',' Демосфен',            ' Демьян',' Денис',' Джамал',' Джордан',' Дмитрий',' Добрыня',            ' Дональд',' Донат',' Дорофей',' Евгений',' Евграф',' Евдоким',' Евсевий',' Евсей',' Евстафий',' Егор',' Елеазар',' Елисей',' Емельян',' Еремей',' Ермолай',' Ерофей',' Ефим',' Ефрем',' Жан',' Ждан',' Жорж',' Захар',' Зиновий',' Ибрагим',' Иван',' Игнатий',' Игорь',' Илларион',' Ильдар',' Ильнар',' Ильнур',' Илья',' Ильяс',' Иннокентий',' Иоанн',' Иосиф',' Ипполит',' Искандер',' Ислам',' Камиль',' Карим',' Карл',' Кирилл',' Клим',' Кондрат',' Константин',' Корней',' Кузьма',' Лавр',' Лаврентий',' Лев',' Леон',' Леонид',' Леонтий',' Леопольд',' Лука',' Лукьян',' Любим',' Макар',' Максим',' Максимилиан',' Марат',' Марк',' Марсель',' Мартин',' Матвей',' Мирон',' Мирослав',' Митрофан',' Михаил',' Михей',' Мишель',' Мстислав',' Мурат',            ' Муслим',' Назар'        );        var RandElement = name[Math.floor(Math.random()*(name.length))]; // Выбор случайного элемента из массива        mess.channel.send(RandElement) // Отправка сообщения со случайным элементом из массива в чат
    


Заключение


Вот и подошла к концу первая часть обучения, как вы могли заметить, создать бота, используя библиотеку discord.js очень просто.

Итого, из этой статьи мы выяснили:

  • Как установить нужные модули и пакеты
  • Как установить среду разработки
  • Как установить среду выполнения
  • Как установить полезные расширения
  • Как создать аккаунт для бота
  • Как воплотить бота в жизнь
  • Как запустить бота
  • Как научить бота реагировать на команды
  • Как пригласить бота на свой сервер
  • Как писать код для работы команд

А также научились некоторым интересным и полезным командам.

Надеюсь, что вам понравилась моя статья и вы узнали из неё что-то новое.

Я постарался объяснить всё максимально доходчиво и подробно.

Следующую часть обучения я планирую сделать об использовании аргументов, а также покажу вам команды для модерации (!kick, !ban, !warn, !mute и т.д).

Сайты для самостоятельного изучения


Основная документация discord.js
Документация discord.js 2
Руководство discord.js
Руководство discord.js 2
Подробнее..
Категории: Javascript , Node.js , Discord , Bot , Боты

Создание Discord-бота, используя библиотеку discord.js Часть 2 Аргументы

21.07.2020 20:05:11 | Автор: admin

Введение


Аргументы являются важнейшей частью 70% команд их изредка называют вводом пользователя, что вполне является правильным.

В данной статье я подробно расскажу вам об их использовании, видах, а также o применении в командах для модерации.

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

const args = message.content.slice(prefix.length).split(/ +/);

Если же вы создаёте бота по моим статьям и пишите код в функциях, вам не придётся ничего добавлять.

Конкретный аргумент


Конкретный аргумент можно получить таким образом:

args[0]

В квадратных скобках указывается номер значения в сообщении:



if (mess.content === `${prefix}test`, args[0]){     mess.channel.send(args[0]);}

В Javascript отсчёт всегда идёт с нуля, поэтому, если мы в квадратных скобках укажем 0, бот отправит в чат сообщение с командой, которую мы вписали нулевой аргумент:



Примеры:

args[1]



args[10]



if (mess.content === `${prefix}test`, args[0]){     mess.channel.send(args[1] + ' ' + args[2]);}



Множество аргументов


Все аргументы в сообщении (или по-другому, содержание сообщения) можно получить следующим образом:

args

if (mess.content === `${prefix}test`, args){     mess.channel.send(args)}



Как видите, бот вывел в чат полное содержание сообщения.

Разделение аргументов


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

if (!args[1]) { // Если пользователь не ввёл аргументов после команды, бот отправляет следующее сообщение:     mess.channel.send('Вы не указали никаких аргументов.');}if (args[1]) { // Если пользователь ввёл хоть один аргумент, бот отправляет введённую команду:    mess.channel.send(`Команда: ${args[0]}`);    args.shift(); // Удаление введённой пользователем команды     mess.channel.send(`Аргументы: ${args}`) // Отправление сообщения с аргументами после введённой пользователем команды}

Результат:



Также вы можете добавить определение количества аргументов:

mess.channel.send(`Количество аргументов: ${args.length}`);



Работа с упоминаниями


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

const mention = message.mentions.users.first();

С помощью неё мы получим первого упомянутого пользователя.
Далее, выведем в чат имя упомянутого пользователя:

mess.channel.send(`Пользователь, которого вы упомянули: ${mention.username}`);



Но теперь, если мы пропишем команду без упоминания, в консоли появится ошибка:

TypeError: Cannot read property 'username' of undefined

Это легко исправить, добавим следующие строки:

if (!message.mentions.users.size) {     return mess.channel.send('Вы не указали пользователя!');}



Команды для модерации


Наконец, мы можем приступить к командам для модерации.

Начнём, пожалуй, с команды для предупреждения пользователя.

Для того, чтобы мы в будущем смогли сделать кик/бан после определённого количества предупреждений, нам нужно создать JSON-файл, в котором будет храниться информация о предупреждённом участнике: его ID и количество предупреждений.

Назовём файл data.json и вставим в него две фигурных скобки:

{}

После чего, вернёмся к файлу с нашей командой и добавим в начало кода следующие строки:

const fs = require('fs'); // Подключаем родной модуль файловой системы node.jsvar warns = JSON.parse(fs.readFileSync("./data.json", "utf8")); // Объявляем переменную warns, с помощью которой бот сможет прочитать файл data.json

Несколько проверок:

if (!mess.member.hasPermission("KICK_MEMBERS")) return mess.reply("У вас нет прав для использования данной команды"); // Если пользователь попытается предупредить участника сервера без привилегии KICK_MEMBERS, ему будет в этом отказано.if (!mess.guild.me.hasPermission("KICK_MEMBERS")) return mess.reply("У меня нет прав!") // Если у бота нету привилегии KICK_MEMBERS, он отправит соответствующее сообщениеlet wUser = mess.guild.member(mess.mentions.users.first()) || mess.guild.members.cache.get(args[0]) // Восприятие упоминания и аргументаif(wUser.id === mess.author.id) return message.channel.send("Вы не можете выдать предупреждение самому себе!"); // Если пользователь попытается предупредить самого себя, ему будет в этом отказано.if (!wUser) return mess.reply("Вы не указали пользователя") // Если пользователь не найден/не указан - в предупреждении пользователю будет отказано

Запись данных в .json файл:

if (!warns[wUser.id]) warns[wUser.id] = { // Если ID пользователя не найден, количество предупреждений устанавливается на 0     warns: 0};warns[wUser.id].warns++; // Если все проверки прошли успешно, к текущему количеству предупреждений пользователя прибавляется +1fs.writeFile("./data.json", JSON.stringify(warns), (err) => { // Все данные сохраняются в .json файле        if (err) console.log(err)});

Перейдём к самой большой и заключительной части кода она отвечает за кик участника сервера после обнаружения трёх предупреждений, за обнуление счётчика предупреждений и за отправку embed'ов.

if (warns[wUser.id].warns >= 3) { // Если обнаружено 3+ предупреждений, то...    wUser.kick("3/3 предупреждений") // Кикнуть участника сервера по причине "3/3 предупреждений"    if (warns[wUser.id].warns >= 3) warns[wUser.id] = { // Если обнаружено 3+ предупреждений, их количество устанавливается на 0            warns: 0    };    fs.writeFile("./data.json", JSON.stringify(warns), (err) => { // Всё сохраняется в .json файл          if (err) console.log(err)    });    var warn_embed1 = new Discord.MessageEmbed() // Embed, отправляющийся при третьем предупреждении         .setColor('#db0f0f')         .addFields(                { name: 'Пользователь', value: `<@${mess.author.id}>` },                { name: 'Выдал предупреждение', value: wUser },                { name: 'Количество предупреждений', value: '3/3 **[Кик]**' }    );    mess.channel.send(warn_embed1)    } else { // Иначе...    var warn_embed2 = new Discord.MessageEmbed() // Embed, отправляющийся при 1 и 2 предупреждении         .setColor('#db0f0f')         .addFields(                { name: 'Пользователь', value: `<@${mess.author.id}>` },                { name: 'Выдал предупреждение', value: wUser },                { name: 'Количество предупреждений', value: `${warns[wUser.id].warns}/3` }    );    mess.channel.send(warn_embed2)    if (warns[wUser.id].warns >= 3) warns[wUser.id] = { // Если обнаружено 3+ предупреждений, их количество устанавливается на 0          warns: 0    };    fs.writeFile("./data.json", JSON.stringify(warns), (err) => { // Всё сохраняется в .json файле        if (err) console.log(err)    });   }}

Результат:



После выдачи предупреждения, в файле data.json должна появиться подобная строка:

{"123456789012345678":{"warns":1}}

Это и есть ID предупреждённого пользователя и количество предупреждений.
После достижения трёх предупреждений, их количество устанавливается на 0.

Перейдём к команде для бана, она значительно проще в написании и компактнее в объёме, т.к нам не понадобится .json файл и запись в него данных.

Для начала, добавим уже знакомые нам строки, но немного видоизменённые:

if (!mess.member.hasPermission("BAN_MEMBERS")) return mess.reply("У вас нет прав для использования данной команды");let bUser = mess.guild.member(mess.mentions.users.first()) || mess.guild.members.cache.get(args[0])if(bUser.id === mess.author.id) return message.channel.send("Вы не можете забанить самого себя!"); if (!mess.guild.me.hasPermission("BAN_MEMBERS")) return mess.reply("У меня нет прав!")

Переназначим переменную args для последующего добавления причины бана:

args = mess.content.split(' '); // Занесли в массив всё содержание сообщенияargs.splice(0, 2); // Начиная с позиции 0, удалили 2 элемента из массиваargs = args.join(' '); // Объединили все элементы в массиве

Добавим пару проверок и функцию бана:

if (!bUser) return mess.reply("Вы не указали пользователя!") if (!args) return mess.reply("Вы не указали причину!")bUser.ban()

И наконец, напишем заключительный embed:

var ban_embed = new Discord.MessageEmbed()     .setColor('#db0f0f')     .addFields(         { name: 'Пользователь', value: `<@${mess.author.id}>` },         { name: 'Забанил', value: bUser },         { name: 'По причине', value: args }     );mess.channel.send(ban_embed)

Результат:



На команде для кика мы заострять внимание не будем, так как она является точной копией команды для бана.
Код
if (!mess.member.hasPermission("KICK_MEMBERS")) return mess.reply("У вас нет прав для использования данной команды");if (!mess.guild.me.hasPermission("KICK_MEMBERS")) return mess.reply("У меня нет прав!")let kUser = mess.guild.member(mess.mentions.users.first()) || mess.guild.members.cache.get(args[0])if(kUser.id === mess.author.id) return message.channel.send("Вы не можете кикнуть самого себя!"); args = mess.content.split(' ');args.splice(0, 2);args = args.join(' ');if (!kUser) return mess.reply("Вы не указали пользователя!")if (!args) return mess.reply("Вы не указали причину!")kUser.kick(args)var kick_embed = new Discord.MessageEmbed()     .setColor('#db0f0f')     .addFields(         { name: 'Пользователь', value: `<@${mess.author.id}>` },         { name: 'Кикнул', value: kUser },         { name: 'По причине', value: args }     );mess.channel.send(kick_embed)


Следующая по счёту команда для заглушения пользователя.

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

Начнём с объявления привычной нам уже переменной, переназначения переменной args:

let mUser = mess.guild.member(mess.mentions.users.first()) || mess.guild.members.cache.get(args[0])args = mess.content.split(' ');args.splice(0, 2);args = args.join(' ');

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

let muterole = mess.guild.roles.cache.find(role => role.name === "Muted");if (!mUser) return mess.reply("Вы не указали пользователя!")if (!args) return mess.reply("Вы не указали причину!")if (!muterole) return mess.reply("На этом сервере нету роли **Muted**, создайте её!");mUser.roles.add(muterole)

Два embed'а и их отправка:

var mute_embed = new Discord.MessageEmbed()    .setColor('#db0f0f')    .addFields(        { name: 'Пользователь', value: `<@${mess.author.id}>` },        { name: 'Выдал мут', value: mUser },        { name: 'По причине', value: args });var dm_mute_embed = new Discord.MessageEmbed()    .setColor('#db0f0f')    .addFields(        { name: 'Пользователь', value: `<@${mess.author.id}>` },        { name: 'Выдал мут', value: `Вам, ${mUser}` },        { name: 'На сервере', value: mUser.guild },        { name: 'По причине', value: args });mUser.send(dm_mute_embed) // Embed отправляется пользователю, которому выдан мут в ЛСmess.channel.send(mute_embed) // Embed отправляется в чат

Заключение


Из этой статьи мы узнали:

  • Как работать с конкретным аргументом
  • Как работать с множеством аргументов
  • Как разделять аргументы
  • Как работать с упоминаниями

А также написали несколько команд для модерации.
Следующая часть будет посвящена embed'ам и работе с ними.

Спасибо за прочтение!

Сайты для самостоятельного изучения



Предыдущие статьи


Подробнее..
Категории: Javascript , Node.js , Discord , Bot , Боты

Прекратите повторять за мной как хайп вокруг Clubhouse повлиял на планы других соцсетей

02.05.2021 22:08:33 | Автор: admin

Анализируем релизы Reddit, Discord и Telegram их команды буквально на днях предложили свой взгляд на интерфейсы для голосового общения. Главный фактор, подстегнувший соцсети к вводу такого функционала, это фантастическая концентрация внимания аудитории и инвесторов на проекте Clubhouse. Как вы уже знаете, он выстрелил в прошлом году, а в апреле 2021-го его капитализацию оценили в четыре миллиарда долларов. Разбираемся, за тем ли зайцем погнались крупнейшие социальные платформы, и как идет этот процесс.

Фотография: Kristina Litvjak. Источник: Unsplash.comФотография: Kristina Litvjak. Источник: Unsplash.com

Дом для Reddit

В России интерес к Clubhouse, кажется, падает. Однако в мире о проекте продолжают говорить. Массовый хайп вокруг него мотивирует руководство крупнейших соцсетей к экстренному запуску похожего функционала. Так, команда Reddit пару недель назад анонсировала сервис для общения в прямом эфире Talk. Он предназначен для тематических сообществ, чьи участники с помощью Reddit Talk смогут проводить сессии вопросов и ответов, организовывать лекции и в любое удобное время общаться в голосовых чатах. Пока их не открыли для всех пользователей платформы и ожидают тех, кто будет готов попробовать сервис в деле в числе первых.

Как пишут в релизе, права на запуск аудиокомнат будут исключительно у модераторов сообществ. Еще они получат возможность приглашать спикеров и управлять их микрофонами во время беседы, плюс удалять пользователей из аудиочата и ограничивать для них вход. Последние смогут подключаться к прослушиванию и сигнализировать о готовности высказаться по тому или иному вопросу с помощью поднятых рук. Словом, все как в Clubhouse.

Новость о готовящемся запуске сообщество площадки встретило без энтузиазма. Первым под огонь критики попал интерфейс многим он показался перегруженным аватарами спикеров и слушателей. Еще больший гнев вызвала необходимость модерации происходящего в голосовых чатах менеджеры сообществ пожаловались, что им не хватает времени на анализ контента в текстовом виде, а тут появляется новый инструмент, требующий дополнительных трудозатрат. Третий момент, на который обратили внимание резиденты площадки, это вопросы относительно того, как Reddit собирается хранить и обрабатывать аудиоконтент: будут ли записи оставаться в облаке, планируется ли возможность бэкапа бесед по запросу организаторов и с разрешения участников. Все это еще предстоит выяснить.

Фотография: Steven Weeks. Источник: Unsplash.comФотография: Steven Weeks. Источник: Unsplash.com

Обработка аудиозаписей пользователей соцсетей значимая тема, и обсуждать ее начали задолго до появления клубного дома. Однако пристальное внимание к этому проекту и скандалы, связанные с утечками данных из Clubhouse, вместе с общими сложностями с модерацией потокового аудио подогрели обстановку вокруг платформ, разрабатывающих подобный функционал. Reddit не исключение, и у его аудитории возникло множество сомнений на этот счет, учитывая тот факт, что пресс-релиз вышел без каких-либо пояснений.

Перестройка в Discord и Telegram

Даже сервисы, казалось бы, не требовавшие доработки, решили подвергнуть локальному переформатированию в свете хайпа вокруг Clubhouse. Чуть более месяца назад Discord представили так называемые Stage Channels каналы, доступные для тематических серверов [отличаются от обычных Discord-комнат по типу и задуманы для сообществ]. Они очень похожи на аудиочаты в Reddit и комнаты в Clubhouse: здесь у модераторов есть возможность назначить спикеров, управлять их микрофонами, а у аудитории присоединяться к прослушиванию бесед и поднимать руку в надежде, что вопрос или реплику дадут произнести в эфире.

В Telegram голосовые чаты появились раньше, в конце прошлого года. В середине марта их обновили. Реализованы здесь аудиокомнаты на свой лад с учетом зарекомендовавшего себя функционала мессенджера. Хотя и все основные Clubhouse-возможности тут тоже есть. Как и одно заметное отличие простой и понятный подход к записи бесед в аудиочатах.

Фотография: Dan-Cristian Pdure. Источник: Unsplash.comФотография: Dan-Cristian Pdure. Источник: Unsplash.com

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

Глухая стена

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

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


Что еще у нас есть в нашем Мире Hi-Fi:


У нас на Хабре: парочка доступных бандлов с устройствами для аудиозаписи.


Подробнее..

Как я сделал Discord бота для игровой гильдии с помощью .NET Core

05.06.2021 18:10:05 | Автор: admin
Батрак предупреждает о том что к гильдии присоединился игрокБатрак предупреждает о том что к гильдии присоединился игрок

Вступление

Всем привет! Недавно я написал Discord бота для World of Warcraft гильдии. Он регулярно забирает данные об игроках с серверов игры и пишет сообщения в Discord о том что к гильдии присоединился новый игрок или о том что гильдию покинул старый игрок. Между собой мы прозвали этого бота Батрак.

В этой статье я решил поделиться опытом и рассказать как сделать такой проект. По сути мы будем реализовывать микросервис на .NET Core: напишем логику, проведем интеграцию с api сторонних сервисов, покроем тестами, упакуем в Docker и разместим в Heroku. Кроме этого я покажу как реализовать continuous integration с помощью Github Actions.

От вас не потребуется никаких знаний об игре. Я написал материал так чтобы можно было абстрагироваться от игры и сделал заглушку для данных об игроках. Но если у вас есть учетная запись в Battle.net, то вы сможете получать реальные данные.

Для понимания материала, от вас ожидается хотя бы минимальный опыт создания веб сервисов с помощью фреймворка ASP.NET и небольшой опыт работы с Docker.

План

На каждом шаге будем постепенно наращивать функционал.

  1. Создадим новый web api проект с одним контроллером /check. При обращении к этому адресу будем отправлять строку Hello! в Discord чат.

  2. Научимся получать данные о составе гильдии с помощью готовой библиотеки или заглушки.

  3. Научимся сохранять в кэш полученный список игроков чтобы при следующих проверках находить различия с предыдущей версией списка. Обо всех изменениях будем писать в Discord.

  4. Напишем Dockerfile для нашего проекта и разместим проект на хостинге Heroku.

  5. Посмотрим на несколько способов сделать периодическое выполнение кода.

  6. Реализуем автоматическую сборку, запуск тестов и публикацию проекта после каждого коммита в master

Шаг 1. Отправляем сообщение в Discord

Нам потребуется создать новый ASP.NET Core Web API проект.

Создание нового проекта - это одна из фундаментальных вещей которые я не буду подробно расписывать. При работе над проектом используйте сервис Github для хранения кода. В дальнейшем мы воспользуемся несколькими возможностями Github.

Добавим к проекту новый контроллер

[ApiController]public class GuildController : ControllerBase{    [HttpGet("/check")]    public async Task<IActionResult> Check(CancellationToken ct)    {        return Ok();    }}

Затем нам понадобится webhook от вашего Discord сервера. Webhook - это механизм отправки событий. В данном случае, то это адрес к которому можно слать простые http запросы с сообщениями внутри.

Получить его можно в пункте integrations в настройках любого текстового канала вашего Discord сервера.

Создание webhookСоздание webhook

Добавим webhook в appsettings.json нашего проекта. Позже мы унесем его в переменные окружения Heroku. Если вы не знакомы с тем как работать с конфигурацией в ASP Core проектах предварительно изучите эту тему.

{"DiscordWebhook":"https://discord.com/api/webhooks/****/***"}

Теперь создадим новый сервис DiscordBroker, который умеет отправлять сообщения в Discord. Создайте папку Services и поместите туда новый класс, эта папка нам еще пригодится.

По сути этот новый сервис делает post запрос по адресу из webhook и содержит сообщение в теле запроса.

public class DiscordBroker : IDiscordBroker{    private readonly string _webhook;    private readonly HttpClient _client;    public DiscordBroker(IHttpClientFactory clientFactory, IConfiguration configuration)    {        _client = clientFactory.CreateClient();        _webhook = configuration["DiscordWebhook"];    }    public async Task SendMessage(string message, CancellationToken ct)    {        var request = new HttpRequestMessage        {            Method = HttpMethod.Post,            RequestUri = new Uri(_webhook),            Content = new FormUrlEncodedContent(new[] {new KeyValuePair<string, string>("content", message)})        };        await _client.SendAsync(request, ct);    }}

Как видите, мы используем внедрение зависимостей. IConfiguration позволит нам достать webhook из конфигов, а IHttpClientFactory создать новый HttpClient.

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

Не забудьте что новый класс нужно будет зарегистрировать в Startup.

services.AddScoped<IDiscordBroker, DiscordBroker>();

А также нужно будет зарегистрировать HttpClient, для работы IHttpClientFactory.

services.AddHttpClient();

Теперь можно воспользоваться новым классом в контроллере.

private readonly IDiscordBroker _discordBroker;public GuildController(IDiscordBroker discordBroker){  _discordBroker = discordBroker;}[HttpGet("/check")]public async Task<IActionResult> Check(CancellationToken ct){  await _discordBroker.SendMessage("Hello", ct);  return Ok();}

Запустите проект, зайдите по адресу /check в браузере и убедитесь что в Discord пришло новое сообщение.

Шаг 2. Получаем данные из Battle.net

У нас есть два варианта: получать данные из настоящих серверов battle.net или из моей заглушки. Если у вас нет аккаунта в battle.net, то пропустите следующий кусок статьи до момента где приводится реализация заглушки.

Получаем реальные данные

Вам понадобится зайти на https://develop.battle.net/ и получить там две персональных строки BattleNetId и BattleNetSecret. Они будут нужны нам чтобы авторизоваться в api перед отправкой запросов. Поместите их в appsettings.

Подключим к проекту библиотеку ArgentPonyWarcraftClient.

Создадим новый класс BattleNetApiClient в папке Services.

public class BattleNetApiClient{   private readonly string _guildName;   private readonly string _realmName;   private readonly IWarcraftClient _warcraftClient;   public BattleNetApiClient(IHttpClientFactory clientFactory, IConfiguration configuration)   {       _warcraftClient = new WarcraftClient(           configuration["BattleNetId"],           configuration["BattleNetSecret"],           Region.Europe,           Locale.ru_RU,           clientFactory.CreateClient()       );       _realmName = configuration["RealmName"];       _guildName = configuration["GuildName"];   }}

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

Кроме этого, нужно создать в appsettings проекта две новых записи RealmName и GuildName. RealmName это название игрового мира, а GuildName это название гильдии. Их будем использовать как параметры при запросе.

Сделаем метод GetGuildMembers чтобы получать состав гильдии и создадим модель WowCharacterToken которая будет представлять собой информацию об игроке.

public async Task<WowCharacterToken[]> GetGuildMembers(){   var roster = await _warcraftClient.GetGuildRosterAsync(_realmName, _guildName, "profile-eu");   if (!roster.Success) throw new ApplicationException("get roster failed");   return roster.Value.Members.Select(x => new WowCharacterToken   {       WowId = x.Character.Id,       Name = x.Character.Name   }).ToArray();}
public class WowCharacterToken{  public int WowId { get; set; }  public string Name { get; set; }}

Класс WowCharacterToken следует поместить в папку Models.

Не забудьте подключить BattleNetApiClient в Startup.

services.AddScoped<IBattleNetApiClient, BattleNetApiClient>();

Берем данные из заглушки

Для начала создадим модель WowCharacterToken и поместим ее в папку Models. Она представляет собой информацию об игроке.

public class WowCharacterToken{  public int WowId { get; set; }  public string Name { get; set; }}

Дальше сделаем вот такой класс

public class BattleNetApiClient{    private bool _firstTime = true;    public Task<WowCharacterToken[]> GetGuildMembers()    {        if (_firstTime)        {            _firstTime = false;            return Task.FromResult(new[]            {                new WowCharacterToken                {                    WowId = 1,                    Name = "Артас"                },                new WowCharacterToken                {                    WowId = 2,                    Name = "Сильвана"                }            });        }        return Task.FromResult(new[]        {            new WowCharacterToken            {                WowId = 1,                Name = "Артас"            },            new WowCharacterToken            {                WowId = 3,                Name = "Непобедимый"            }        });    }}

Он возвращает зашитый в него список игроков. При первом вызове метода мы вернем один список, при последующих другой. Это нужно нам что смоделировать изменчивое поведение api. Этой заглушки хватит чтобы продолжить делать проект.

Сделайте интерфейс и подключите все что мы создали в Startup.

services.AddScoped<IBattleNetApiClient, BattleNetApiClient>();

Выведем результаты в Discord

После того как мы сделали BattleNetApiClient, им можно воспользоваться в контроллере чтобы вывести кол-во игроков в Discord.

[ApiController]public class GuildController : ControllerBase{  private readonly IDiscordBroker _discordBroker;  private readonly IBattleNetApiClient _battleNetApiClient;  public GuildController(IDiscordBroker discordBroker, IBattleNetApiClient battleNetApiClient)  {     _discordBroker = discordBroker;     _battleNetApiClient = battleNetApiClient;  }  [HttpGet("/check")]  public async Task<IActionResult> Check(CancellationToken ct)  {     var members = await _battleNetApiClient.GetGuildMembers();     await _discordBroker.SendMessage($"Members count: {members.Length}", ct);     return Ok();  }}

Шаг 3. Находим новых и ушедших игроков

Нужно научиться определять какие игроки появились или пропали из списка при последующих запросах к api. Для этого мы можем закэшировать список в InMemory кэше (в оперативной памяти) или во внешнем хранилище.

Если закэшировать список в InMemory кэше, то мы потеряем его при перезапуске приложения. Поэтому позже мы подключим базу данных Redis как аддон в Heroku и будем кешировать туда.

А пока что подключим InMemory кэш в Startup.

services.AddMemoryCache(); 

Теперь в нашем распоряжении есть IDistributedCache, который можно подключить через конструктор. Я предпочел не использовать его напрямую , а написать для него обертку. Создайте класс GuildRepository и поместите его в новую папку Repositories.

public class GuildRepository : IGuildRepository{    private readonly IDistributedCache _cache;    private const string Key = "wowcharacters";    public GuildRepository(IDistributedCache cache)    {        _cache = cache;    }    public async Task<WowCharacterToken[]> GetCharacters(CancellationToken ct)    {        var value = await _cache.GetAsync(Key, ct);        if (value == null) return Array.Empty<WowCharacterToken>();        return await Deserialize(value);    }    public async Task SaveCharacters(WowCharacterToken[] characters, CancellationToken ct)    {        var value = await Serialize(characters);        await _cache.SetAsync(Key, value, ct);    }        private static async Task<byte[]> Serialize(WowCharacterToken[] tokens)    {        var binaryFormatter = new BinaryFormatter();        await using var memoryStream = new MemoryStream();        binaryFormatter.Serialize(memoryStream, tokens);        return memoryStream.ToArray();    }    private static async Task<WowCharacterToken[]> Deserialize(byte[] bytes)    {        await using var memoryStream = new MemoryStream();        var binaryFormatter = new BinaryFormatter();        memoryStream.Write(bytes, 0, bytes.Length);        memoryStream.Seek(0, SeekOrigin.Begin);        return (WowCharacterToken[]) binaryFormatter.Deserialize(memoryStream);    }}

Теперь можно написать сервис который будет сравнивать новый список игроков с сохраненным.

public class GuildService{    private readonly IBattleNetApiClient _battleNetApiClient;    private readonly IGuildRepository _repository;    public GuildService(IBattleNetApiClient battleNetApiClient, IGuildRepository repository)    {        _battleNetApiClient = battleNetApiClient;        _repository = repository;    }    public async Task<Report> Check(CancellationToken ct)    {        var newCharacters = await _battleNetApiClient.GetGuildMembers();        var savedCharacters = await _repository.GetCharacters(ct);        await _repository.SaveCharacters(newCharacters, ct);        if (!savedCharacters.Any())            return new Report            {                JoinedMembers = Array.Empty<WowCharacterToken>(),                DepartedMembers = Array.Empty<WowCharacterToken>(),                TotalCount = newCharacters.Length            };        var joined = newCharacters.Where(x => savedCharacters.All(y => y.WowId != x.WowId)).ToArray();        var departed = savedCharacters.Where(x => newCharacters.All(y => y.Name != x.Name)).ToArray();        return new Report        {            JoinedMembers = joined,            DepartedMembers = departed,            TotalCount = newCharacters.Length        };    }}

В качестве возвращаемого результата используется модель Report. Ее нужно создать и поместить в папку Models.

public class Report{   public WowCharacterToken[] JoinedMembers { get; set; }   public WowCharacterToken[] DepartedMembers { get; set; }   public int TotalCount { get; set; }}

Применим GuildService в контроллере.

[HttpGet("/check")]public async Task<IActionResult> Check(CancellationToken ct){   var report = await _guildService.Check(ct);   return new JsonResult(report, new JsonSerializerOptions   {      Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin, UnicodeRanges.Cyrillic)   });}

Теперь отправим в Discord какие игроки присоединились или покинули гильдию.

if (joined.Any() || departed.Any()){   foreach (var c in joined)      await _discordBroker.SendMessage(         $":smile: **{c.Name}** присоединился к гильдии",         ct);   foreach (var c in departed)      await _discordBroker.SendMessage(         $":smile: **{c.Name}** покинул гильдию",         ct);}

Эту логику я добавил в GuildService в конец метода Check. Писать бизнес логику в контроллере не стоит, у него другое назначение. В самом начале мы делали там отправку сообщения в Discord потому что еще не существовало GuildService.

На первом скриншоте в статье вы видели что я вывел больше информации об игроке. Ее можно получить если воспользоваться библиотекой ArgentPonyWarcraftClient

await _warcraftClient.GetCharacterProfileSummaryAsync(_realmName, name.ToLower(), Namespace);

Я решил не добавлять в статью больше кода в BattleNetApiClient, чтобы статья не разрослась до безумных размеров.

Unit тесты

У нас появился класс GuildService с нетривиальной логикой, который будет изменяться и расширяться в будущем. Стоит написать на него тесты. Для этого нужно будет сделать заглушки для BattleNetApiClient, GuildRepository и DiscordBroker. Я специально просил создавать интерфейсы для этих классов чтобы можно было сделать их фейки.

Создайте новый проект для Unit тестов. Заведите в нем папку Fakes и сделайте три фейка.

public class DiscordBrokerFake : IDiscordBroker{   public List<string> SentMessages { get; } = new();   public Task SendMessage(string message, CancellationToken ct)   {      SentMessages.Add(message);      return Task.CompletedTask;   }}
public class GuildRepositoryFake : IGuildRepository{    public List<WowCharacterToken> Characters { get; } = new();    public Task<WowCharacterToken[]> GetCharacters(CancellationToken ct)    {        return Task.FromResult(Characters.ToArray());    }    public Task SaveCharacters(WowCharacterToken[] characters, CancellationToken ct)    {        Characters.Clear();        Characters.AddRange(characters);        return Task.CompletedTask;    }}
public class BattleNetApiClientFake : IBattleNetApiClient{   public List<WowCharacterToken> GuildMembers { get; } = new();   public List<WowCharacter> Characters { get; } = new();   public Task<WowCharacterToken[]> GetGuildMembers()   {      return Task.FromResult(GuildMembers.ToArray());   }}

Эти фейки позволяют заранее задать возвращаемое значение для методов. Для этих же целей можно использовать популярную библиотеку Moq. Но для нашего простого примера достаточно самодельных фейков.

Первый тест на GuildService будет выглядеть так:

[Test]public async Task SaveNewMembers_WhenCacheIsEmpty(){   var wowCharacterToken = new WowCharacterToken   {      WowId = 100,      Name = "Sam"   };      var battleNetApiClient = new BattleNetApiApiClientFake();   battleNetApiClient.GuildMembers.Add(wowCharacterToken);   var guildRepositoryFake = new GuildRepositoryFake();   var guildService = new GuildService(battleNetApiClient, null, guildRepositoryFake);   var changes = await guildService.Check(CancellationToken.None);   changes.JoinedMembers.Length.Should().Be(0);   changes.DepartedMembers.Length.Should().Be(0);   changes.TotalCount.Should().Be(1);   guildRepositoryFake.Characters.Should().BeEquivalentTo(wowCharacterToken);}

Как видно из названия, тест позволяет проверить что мы сохраним список игроков, если кэш пуст. Заметьте, в конце теста используется специальный набор методов Should, Be... Это методы из библиотеки FluentAssertions, которые помогают нам сделать Assertion более читабельным.

Теперь у нас есть база для написания тестов. Я показал вам основную идею, дальнейшее написание тестов оставляю вам.

Главный функционал проекта готов. Теперь можно подумать о его публикации.

Шаг 4. Привет Docker и Heroku!

Мы будем размещать проект на платформе Heroku. Heroku не позволяет запускать .NET проекты из коробки, но она позволяет запускать Docker образы.

Чтобы упаковать проект в Docker нам понадобится создать в корне репозитория Dockerfile со следующим содержимым

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS builderWORKDIR /sourcesCOPY *.sln .COPY ./src/peon.csproj ./src/COPY ./tests/tests.csproj ./tests/RUN dotnet restoreCOPY . .RUN dotnet publish --output /app/ --configuration ReleaseFROM mcr.microsoft.com/dotnet/core/aspnet:3.1WORKDIR /appCOPY --from=builder /app .CMD ["dotnet", "peon.dll"]

peon.dll это название моего Solution. Peon переводится как батрак.

О том как работать с Docker и Heroku можно прочитать здесь. Но я все же опишу последовательность действий.

Вам понадобится создать аккаунт в Heroku, установить Heroku CLI.

Создайте новый проект в heroku и свяжите его с вашим репозиторием.

heroku git:remote -a project_name

Теперь нам необходимо создать файл heroku.yml в папке с проектом. У него будет такое содержимое:

build:  docker:    web: Dockerfile

Дальше выполним небольшую череду команд:

# Залогинимся в heroku registryheroku container:login# Соберем и запушим образ в registryheroku container:push web# Зарелизим приложение из образаheroku container:release web

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

heroku open

После того как мы разместили приложение в Heroku, нужно подключить базу данных Redis для кэша. Как вы помните InMemory кэш будет исчезать после перезапуска приложения.

Установите для нашего Heroku приложения бесплатный аддон RedisCloud.

Строку подключения для Redis можно будет получить через переменную окружения REDISCLOUD_URL. Она будет доступна, когда приложение будет запущено в экосистеме Heroku.

Нам нужно получить эту переменную в коде приложения.

Установите библиотеку Microsoft.Extensions.Caching.StackExchangeRedis.

С помощью нее можно зарегистрировать Redis реализацию для IDistributedCache в Startup.

services.AddStackExchangeRedisCache(o =>{   o.InstanceName = "PeonCache";   var redisCloudUrl = Environment.GetEnvironmentVariable("REDISCLOUD_URL");   if (string.IsNullOrEmpty(redisCloudUrl))   {      throw new ApplicationException("redis connection string was not found");   }   var (endpoint, password) = RedisUtils.ParseConnectionString(redisCloudUrl);   o.ConfigurationOptions = new ConfigurationOptions   {      EndPoints = {endpoint},      Password = password   };});

В этом коде мы получили переменную REDISCLOUD_URL из переменных окружения системы. После этого мы извлекли адрес и пароль базы данных с помощью класса RedisUtils. Его написал я сам:

public static class RedisUtils{   public static (string endpoint, string password) ParseConnectionString(string connectionString)   {      var bodyPart = connectionString.Split("://")[1];      var authPart = bodyPart.Split("@")[0];      var password = authPart.Split(":")[1];      var endpoint = bodyPart.Split("@")[1];      return (endpoint, password);   }}

На этот класс можно сделать простой Unit тест.

[Test]public void ParseConnectionString(){   const string example = "redis://user:password@url:port";   var (endpoint, password) = RedisUtils.ParseConnectionString(example);   endpoint.Should().Be("url:port");   password.Should().Be("password");}

После того что мы сделали, GuildRepository будет сохранять кэш не в оперативную память, а в Redis. Нам даже не нужно ничего менять в коде приложения.

Опубликуйте новую версию приложения.

Шаг 5. Реализуем циклическое выполнение

Нам нужно сделать так чтобы проверка состава гильдии происходила регулярно, например каждые 15 минут.

Есть несколько способов это реализовать:

Самый простой способ - это сделать задание на сайте https://cron-job.org. Этот сервис будет слать get запрос на /check вашего приложения каждые N минут.

Второй способ - это использовать Hosted Services. В этой статье подробно описано как создать повторяющееся задание в ASP.NET Core проекте. Учтите, бесплатный тариф в Heroku подразумевает что ваше приложение будет засыпать после того как к нему некоторое время не делали запросов. Hosted Service перестанет работать после того как приложение заснет. В этом варианте вам следует перейти на платный тариф. Кстати, так сейчас работает мой бот.

Третий способ - это подключить к проекту специальные Cron аддоны. Например Heroku Scheduler. Можете пойти этим путем и разобраться как создать cron job в Heroku.

Шаг 6. Автоматическая сборка, прогон тестов и публикация

Во-первых, зайдите в настройки приложения в Heroku.

Там есть пункт Deploy. Подключите там свой Github аккаунт и включите Automatic deploys после каждого коммита в master.

Поставьте галочку у пункта Wait for CI to pass before deploy. Нам нужно чтобы Heroku дожидался сборки и прогонки тестов. Если тесты покраснеют, то публикация не случится.

Сделаем сборку и прогонку тестов в Github Actions.

Зайдите в репозиторий и перейдите в пункт Actions. Теперь создайте новый workflow на основе шаблона .NET

В репозитории появится новый файл dotnet.yml. Он описывает процесс сборки.

Как видите по его содержимому, задание build будет запускаться после пуша в ветку master.

on:  push:    branches: [ master ]  pull_request:    branches: [ master ]

Содержимое самого задания нас полностью устраивает. Если вы вчитаетесь в то что там происходит, то увидите что там происходит запуск команд dotnet build и dotnet test.

    steps:    - uses: actions/checkout@v2    - name: Setup .NET      uses: actions/setup-dotnet@v1      with:        dotnet-version: 5.0.x    - name: Restore dependencies      run: dotnet restore    - name: Build      run: dotnet build --no-restore    - name: Test      run: dotnet test --no-build --verbosity normal

Менять в этом файле ничего не нужно, все уже будет работать из коробки.

Запуште что-нибудь в master и посмотрите что задание запускается. Кстати, оно уже должно было запуститься после создания нового workflow.

Отлично! Вот мы и сделали микросервис на .NET Core который собирается и публикуется в Heroku. У проекта есть множество точек для развития: можно было бы добавить логирование, прокачать тесты, повесить метрики и. т. д.

Надеюсь данная статья подкинула вам пару новых идей и тем для изучения. Спасибо за внимание. Удачи вам в ваших проектах!

Подробнее..
Категории: C , Net , Api , Docker , Dotnet , Discord , Bot , Бот , Heroku , Микросервис , Wow

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru