Статья Создание бота на Java для сервиса Вконтакте. Часть 1

AppLoid

Новичок

AppLoid

Новичок
Статус
Оффлайн
Регистрация
12 Мар 2019
Сообщения
2
Реакции
2
В этой статье (по крайней мере в первой части) я расскажу, как создать простого (в архитектуре и функционале) чат-бота, работающего в сервисе VK.

Сама разработка бота будет состоять из трех частей, включающих:
  • Создание фундамента
  • Его расширение командами
  • Деплой бота в сервисе heroku и подключение БД

Итак, приступим к созданию основы бота

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

В чем же её простота?

А простота заключается в том, что фундамент, на котором он построен не требует знаний Java Core. Достаточно будет знать концепции ООП, практическое использование коллекций и знание стандартных функций.

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

Все зависит от ваших потребностей, а создание команд рассмотрим во второй части этой серии статей.



Как получать запросы пользователя?


Для этого VK представляет нам SDK для Java, который позволяет через использование библиотеки Java получать сообщения, отправляемые в группу.
Необходимо будет настроить окружение через maven или gradle, добавив зависимости с официальной документации Java SDK для VK API.

Я в своем проекте использовал maven. Файл pom.xml вы сможете найти в репозитории проекта.
Рекомендую для ознакомления прочитать эту статью (
Пожалуйста, авторизуйтесь для просмотра ссылки.
)

Необходимые зависимости: https://vk.com/dev/Java_SDK


Теперь настроим бота

Бот представляет с собой группу с некоторыми дополнительными разрешениями. Чтобы мы могли читать поступающие сообщения необходимо его настроить, а именно:
  • Получить ключ доступа ( access token)
  • Включить Long Poll API
  • Добавить обработку событий входящих сообщений
Все это делается в разделе “Управление” -> “Работа с API”.

Еще дополнительно нужно будет включить «Возможности ботов» в разделе “Сообщения”
2019-03-14-22-14-49.png

2019-03-14-22-15-35.png

2019-03-14-22-16-42.png

2019-03-14-22-17-36.png

2019-03-14-22-18-16.png

2019-03-14-22-18-51.png

2019-03-14-22-19-22.png

2019-03-14-22-20-37.png


Итак, окружение и группа с ботом готовы. Приступим к программированию.

Часто бывает так, что люди пишут все свои access token или пароли прямо в исходниках и заливают их, например, на github. Что может привести к неприятным последствиям, если этим воспользуются токсичные люди.

Поэтому, хорошей практикой бывает решение создать файл конфигурации. Это может обычный txt файл, но в силу того, что могут возникнуть осложнения с его чтением – лучше воспользоваться готовым решением, а именно файл properties.

С помощью таких файлов мы можем получать значения в виде карт (map или же словари). Таким образом, можно хранить свои файлы конфигураций, а потом добавить их в .gitignore, чтобы они не высвечивались в свободном доступе, если репозиторий не приватный.

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

Создадим файл vkconfig.properties и добавим туда значения:

Код:
access_token = ag3t3agagfsdagdshfjjh4w4 (ваш access token)
group_id = 125162 (ИД вашей группы)


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

Рассмотрим поля и конструктор этого класса:
Код:
public class VKCore {

    private VkApiClient vk;
    private static int ts;
    private GroupActor actor;
    private static int maxMsgId = -1;

    public VKCore() throws ClientException, ApiException {
        TransportClient transportClient = HttpTransportClient.getInstance();
        vk = new VkApiClient(transportClient);

        // Загрузка конфигураций

        Properties prop = new Properties();
        int groupId;
        String access_token;
        try {
            prop.load(new FileInputStream("src/main/resources/vkconfig.properties"));
            groupId = Integer.valueOf(prop.getProperty("groupId"));
            access_token = prop.getProperty("accessToken");
            actor = new GroupActor(groupId, access_token);
            ts = vk.messages().getLongPollServer(actor).execute().getTs();
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("Ошибка при загрузке файда конфигурации");

        }
    }

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

GroupActor actor – это по сути и есть ваша группа, с помощью которого вы сможете отправлять сообщения от имени группы.

VkApiClient vk – это интерфейс для взаимодействия с VK API с помощью запросов.

Грубо говоря, вы будете вызывать методы VKApiClient и передавать туда в качестве аргумента ваш GroupActor, чтобы идентифицировать группу.

Еще одним немаловажным является значения полей ts и maxMsgId:

ts – это идентификатор сообщения с которого нужно начинать обрабатывать сообщения. То есть, если значение ts будет константой, то VK API будет отправлять вам одни и те же сообщения. Поэтому после получения любого сообщения, необходимо его менять.

maxMsgId – сначала этот параметр может показаться ненужным, так как без него бот будет работать, но не долго, а именно как говорят в документации около суток, но лично у меня бот упал уже после 12 часов работы. К тому же выйдет непонятная ошибка internal server error, что означает внутреннюю ошибку сервера. Эта ошибка может возникнуть если ts – очень старый (более суток) и не передается параметр max_msg_id, который хранит значение максимального идентификатора сообщения среди уже имеющихся в локальной копии.

В конструкторе класса настроим соединение и создадим объект GroupActor с помощью наших access token и groupId, находящихся в файле конфигурации.


Теперь добавим метод getMessage – с помощью которого мы будем получать сообщения:

Код:
public Message getMessage() throws ClientException, ApiException {

    MessagesGetLongPollHistoryQuery eventsQuery = vk.messages()
            .getLongPollHistory(actor)
            .ts(ts);
    if (maxMsgId > 0){
        eventsQuery.maxMsgId(maxMsgId);
    }
    List<Message> messages = eventsQuery
            .execute()
            .getMessages()
            .getMessages();

    if (!messages.isEmpty()){
        try {
            ts =  vk.messages()
                    .getLongPollServer(actor)
                    .execute()
                    .getTs();

        } catch (ClientException e) {
            e.printStackTrace();

        }

    }

    if (!messages.isEmpty() && !messages.get(0).isOut()) {
            /*
            messageId - максимально полученный ID, нужен, чтобы не было ошибки 10 internal server error,
            который является ограничением в API VK. В случае, если ts слишком старый (больше суток),
            а max_msg_id не передан, метод может вернуть ошибку 10 (Internal server error).
             */
        int messageId = messages.get(0).getId();
        if (messageId > maxMsgId){
            maxMsgId = messageId;
        }
        return messages.get(0);
    }
    return null;
}

Здесь все довольно тривиально за исключением, возможно, объекта Message.

Что за файл Message и откуда он берется?

Когда мы делаем запрос в VK API, то ответ поступает в виде JSON файла. И встает вопрос: как его превратить в java объект (POJO). Для этого существуют специальные библиотеки, такие как json-simple или gson. Конкретно в случае с Java VK SDK используется gson для десериализации в java-объект.

Если рассмотреть исходный код Message:

Код:
public class Message {
    /**
     * Message ID
     */
    @SerializedName("id")
    private Integer id;

    /**
     * Date when the message has been sent in Unixtime
     */
    @SerializedName("date")
    private Integer date;

    /**
     * Date when the message has been updated in Unixtime
     */
    @SerializedName("update_time")
    private Integer updateTime;

    /**
     * Information whether the message is outcoming
     */
    @SerializedName("out")
    private BoolInt out;

То можно заметить, что это лишь некая структура данных с геттерами внизу. Чтобы получше разобраться в этом, рекомендую прочитать как работает библиотека gson (
Пожалуйста, авторизуйтесь для просмотра ссылки.
)

Если коротко, то это просто превращение JSON-данных в java-объект с помощью специальных методов.

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

Для этого создадим класс VKserver:

Код:
public class VKServer {

    public static VKCore vkCore;
    static {
        try {
            vkCore = new VKCore();
        } catch (ApiException | ClientException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws NullPointerException, ApiException, InterruptedException {

        System.out.println("Running server...");
        while (true) {
            Thread.sleep(300);
            try {
                Message message = vkCore.getMessage();
            if (message != null) {
                    ExecutorService exec = Executors.newCachedThreadPool();
                    exec.execute(new Messenger(message));
                }

            } catch (ClientException e) {
                System.out.println("Возникли проблемы");
                final int RECONNECT_TIME = 10000;
                System.out.println("Повторное соединение через " + RECONNECT_TIME / 1000 + " секунд");
                Thread.sleep(RECONNECT_TIME);

            }
        }
    }
}

Здесь тоже ничего сложного нет – делаем запрос каждые 300 мс и если есть не пустое сообщение, то отправляем его в класс Messenger в качестве аргумента конструктора.

Сразу же посмотрим класс Messenger:

Код:
public class Messenger implements Runnable{

    private Message message;

    public Messenger(Message message){
        this.message = message;
    }

    @Override
    public void run() {
        Commander.execute(message);
    }
}

Этот класс нужен, чтобы обрабатывать запросы многопоточно. Здесь минимальная реализация, чтобы было легче понять. Для действительно функционального бота – здесь еще можно много чего добавить.

И здесь мы опять встречаем непонятный класс Commander. Что же это за класс?

Это будет ядром нашего бота. Грубо говоря, он будет обрабатывать сообщения пользователя и отправлять ему какой-либо ответ.

Состоит он не только из самого себя. В его число также входят:
  • Менеджер команд, где мы храним объекты команд
  • Определитель команд, где мы делаем выборку по ключевому слову
  • И абстрактный класс Command – суперкласс для всех классов команд

Разберем по порядку каждый из них

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

Лучше всего будет посмотреть исходники:

Код:
/**
 * Abstract class for all executable classes-commands
 * Field {@link #name} identification command,he is called by this name
 *
 * @author Arthur Kupriyanov
 * @version 1.1
 */
public abstract class Command {

    public final String name;
    public Command(String name){
        this.name = name;
    }

    /**
     * Метод, который будет вызываться для исполнения команды
     * @param message сообщение пользователя
     */
    public abstract void exec(Message message);

    /**
     * Возвращает строку в формате:<br>
     * name: имяКоманды<br>
     *
     * @return форматированное имя и мод команды
     */

    @Override
    public String toString() {
        return String.format("name: %s",this.name);
    }

    /**
     * Берет хэш-код значащего поля {@link #name}
     *
     * @return хэш-код команды
     */
    @Override
    public int hashCode() {
        return this.name.hashCode();
    }

    /**
     * Объекты эквивалентны только, если поля <code>{@link #name}</code> равны
     * имеют одинаковое значение и объект является классом-наследником {@link Command}
     * @param obj сравниваемый объект
     * @return {@code true} если объекты эквивалентны; {@code false} если объекты различаются
     */
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Command){
            if (name.equals(((Command) obj).name)){
                return true;
            }
        }
        return false;
    }

}

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

В качестве примера создадим команду Unknown:

Код:
/**
 * @author Arthur Kupriyanov
 */
public class Unknown extends Command {

    public Unknown(String name) {
        super(name);
    }

    @Override
    public void exec(Message message) {
        System.out.println("Unknown command");
    }
}

Теперь рассмотрим оставшиеся два вспомогательных класса:

CommandDeterminant – определитель команд:

Код:
/**
 * Определяет команду
 *
 * @author Артур Куприянов
 * @version 1.1.0
 */
public class CommandDeterminant {

    public static Command getCommand(Collection<Command> commands, Message message) {
        String body = message.getBody();
        for (Command command : commands
        ) {
                if (command.name.equals(body.split(" ")[0])) {
                    return command;
                }
        }
        return new Unknown("unknown");
    }
}

Он производит выборку команды из коллекции команд, по первому ключевому слову. Иначе говоря, если во вводе пользователя первым словом будет имя нашей команды, которую мы указали в конструкторе Command, то метод getCommand() – возвратит именно ту команду.

CommandManager – менеджер команд, где будем хранить объекты команд

Код:
/**
 * @author Arthur Kupriyanov
 */
public class CommandManager {
    private static HashSet<Command> commands = new HashSet<>();

    static {
        commands.add(new Unknown("unknown"));
        commands.add(new Weather("weather"));
    }

    public static HashSet<Command> getCommands(){
        return commands;
    }
    public static void addCommand(Command command) { commands.add(command);}
}

Здесь мы добавляем две команды Unknown, которую мы создали ранее, и Weather – команда вывода погоды, её мы разберем во второй части.

Также необходимо обратить внимание на метод getCommands() – который возвращает коллекцию наших команд.

И наконец, перейдем к основному классу Commander:

Код:
public class Commander {

    /**
     * Обработка сообщений, получаемых через сервис Вконтакте. Имеет ряд дополнительной информации.
     * @param message сообщение (запрос) пользователя
     */
    public static void execute(Message message){
        CommandDeterminant.getCommand(CommandManager.getCommands(), message).exec(message);
    }
}

Что мы здесь делаем?

Вызываем метод getCommand у CommandDeterminant, в котором в качестве аргумента передаем команды из CommandManager и сообщение Message. Далее этот метод делает выборку и возвращает объект Command, у которого мы вызываем метод exec с аргументом Message.

Давайте проверим как все это работает, запустив класс VKServer.

2019-03-14-23-23-31.png


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

Для отправки сообщений воспользуемся классом VKManager:

Код:
/**
 * @author Arthur Kupriyanov
 */
public class VKManager {
    public static VKCore vkCore;

    static {
        try {
            vkCore = new VKCore();
        } catch (ApiException | ClientException e) {
            e.printStackTrace();
        }
    }

    public void sendMessage(String msg, int peerId){
        if (msg == null){
            System.out.println("null");
            return;
        }
        try {
            vkCore.getVk().messages().send(vkCore.getActor()).peerId(peerId).message(msg).execute();
        } catch (ApiException | ClientException e) {
            e.printStackTrace();
        }
    }

    public MessagesSendQuery getSendQuery(){
        return vkCore.getVk().messages().send(vkCore.getActor());
    }

    /**
     * Обращается к VK API и получает объект, описывающий пользователя.
     * @param id идентификатор пользователя в VK
     * @return {@link UserXtrCounters} информацию о пользователе
     * @see UserXtrCounters
     */
    public static UserXtrCounters getUserInfo(int id){
        try {
            return vkCore.getVk().users()
                    .get(vkCore.getActor())
                    .userIds(String.valueOf(id))
                    .execute()
                    .get(0);
        } catch (ApiException | ClientException e) {
            e.printStackTrace();
        }
        return null;
    }
}

Не следует углубляться во все это, главное запомнить метод sendMessage, с помощью которого мы будем отправлять сообщение. msg – это сообщение, которое мы отправим, а peerId – идентификатор пользователя в вк, которому мы будем отправлять сообщение (возьмем его из объекта Message)

Добавим его в Unknown:

Код:
/**
 * @author Arthur Kupriyanov
 */
public class Unknown extends Command {

    public Unknown(String name) {
        super(name);
    }

    @Override
    public void exec(Message message) {
        new VKManager().sendMessage("Неизвестная команда", message.getUserId());
    }
}

Проверим:

2019-03-14-23-29-34.png


Если сейчас все не очень понятно – это нормально, но думаю для тех, кто знаком с Java Core здесь все будет тривиально. Я же попытался максимально упростить архитектуру и реализацию бота.
В следующих частях разберем создание команд, тогда, надеюсь, все станет понятнее.

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

После окончания всей серии статей, возможно (если будет время, и я не поленюсь), создам шаблонный проект с минимальной реализацией, но масштабируемый, клонировав который, вы сможете создать своего бота, добавляя свои команды. А пока на этом все.

Ссылка на github:

 

Linius

Новичок

Linius

Новичок
Статус
Оффлайн
Регистрация
11 Янв 2019
Сообщения
1
Реакции
0
Статья познавательная и мне после прочтения даже захотелось покодить на жабе, но если смотреть на это от лица новичка, то ему будет проще делать бота на питоне/ноде, имхо
 

AppLoid

Новичок

AppLoid

Новичок
Статус
Оффлайн
Регистрация
12 Мар 2019
Сообщения
2
Реакции
2
Статья познавательная и мне после прочтения даже захотелось покодить на жабе, но если смотреть на это от лица новичка, то ему будет проще делать бота на питоне/ноде, имхо
Полностью с тобой согласен))) Пытался максимально сделать упрощенно, но все равно получилось сложновато. А бота на питоне написать - действительно легче ( особенно с библиотекой vk_api)
 

Электрик

Интересующийся

Электрик

Интересующийся
Статус
Оффлайн
Регистрация
17 Ноя 2019
Сообщения
211
Реакции
0
Статья познавательная и мне после прочтения даже захотелось покодить на жабе, но если смотреть на это от лица новичка, то ему будет проще делать бота на питоне/ноде, имхо
для новичка что на питонет что на джаве бедет не просто по началу если слабо знать язык
 
Сверху