JavaRush /Blog Jawa /Random-JV /Telegram-бот в качестве первого проекта и его значимость ...
Pavel Mironov (Miroha)
tingkat
Москва

Telegram-бот в качестве первого проекта и его значимость для профессионального роста на личном опыте

Diterbitake ing grup
Приветствую всех! Расскажу немного о себе. Мне 24 года, в прошлом году закончил технический ВУЗ и до сих пор не имею опыта работы. Забегая вперед хочу сказать, что изначально в заложенном плане (составленном осенью 2019 года) планировал выход на работу в марте-апреле 2020, но, к сожалению, вмешался карантин, поэтому отложил всё до середины лета и в будущем надеюсь написать свою историю успеха. Telegram-бот в качестве первого проекта и его значимость для профессионального роста на личном опыте - 1К программированию меня никогда не тянуло. В университете преподавали программирования достаточно, но вот заинтересовать меня это ремесло тогда не смогло. Были и proceduresные языки (C), годичный курс по ООП (Java), базы данных, даже ассемблер и C++. Да чего таить, к учебе в целом я был равнодушен, так How большинство преподаваемых дисциплин казались мне бесполезными, годившимися только для отчетной ведомости (в принципе это так и есть). После окончания ВУЗа необходимо было решаться: Howих-то навыков я не приобрел, а работать надо. Пришлось задуматься о самообразовании (ох, How минимум 2 полноценных года я уже упустил, сидев сложа руки) и выбор сам собой пал на Java, так How на курсе ООП в университете кто-то из ребят посоветовал курс javarush, а он, How вы знаете, посвящен именно языку Java. Заинтересовала подача курса. Да, я не любил тогда программировать, так How сразу это дело бросал, когда встречал Howую-то сложность, а сложностей в программировании хоть отбавляй. Но в то же время я чувствовал, что хочу писать code, поэтому в конце концов я и решил связать себя с программированием. Вкратце расскажу и про мой опыт на javarush. Начал я в августе 2019, сразу купил подписку на месяц, но на 7 уровне понял, что задачи даются тяжело. Отложил курс, взял в руки Шилдта. Так параллельно и проходил курс в течение 3 месяцев. Дошел до 20 уровня (это мой второй аккаунт), почти fully прочитал Шилдта, потом устал от здешних задач, в которых я перестал видеть практическую пользу для себя. Заходил на codewars, leetcode, начал смотреть видеокурсы. К слову, за 3 месяца я прошел путь от "О нет, что такое массив? Как с ним работать и почему так страшно"? до детального изучения исходного codeа классов коллекций (ArrayList, HashMap и т.д.). Основываясь на личном опыте, новичкам скажу: здесь главное побороть такое чувство, которое возникает, если ничего не понимаешь и не можешь ничего решить. Когда оно возникает, просто хочется всё бросить и кажется, что ты слишком тупой для этого дела. Если перебарывать в себе такие моменты и морально отдыхать, то успех придет. Я думаю, что многие не справляются с этим, поэтому быстро бросают подобные начинания. В итоге, в декабре 2019 задумался о своём проекте. Решил выбрать Telegram-бота, но идеи не было. В это же самое время одному знакомому понадобился функционал для своей группы в телеграмме, который он хотел бы автоматизировать. Он How раз был в курсе, что я углубленно изучаю программирование и предложил мне проект. Мне для опыта и будущего резюме, ему - для развития группы. Я даже позволю себе процитировать его идею: "Недавно софтину хотел у программиста заказать, которая загружала бы в выбранное Облако файлы по прямым linkм. Это интересно, так How аналогов нет. И просто очень удобно. Суть: копируешь ссылку, вставляешь в окно и выбираешь нужное Облако (GDrive, Mail, Яндекс Диск и т.п), в своё время софт всё делает на стороне serverа и юзеру ничего не нужно загружать на свою машину (особенно круто, когда у тебя сборка на SSD-накопителях). Думали сделать в web-интерфейсе, чтобы можно было запускать How с телефонов, так и с десктопа... Можно в принципе через приложение реализовать, а не через web-интерфейс. Тебе такое по силам?" Я начал было работать, но в итоге через пару дней понял, что у нас ничего не получится, во многом из-за нехватки знаний. Знакомому нужны были эти самые ссылки на Облако.Mail, а у них до сих пор нет API. Была попытка что-то склепать через GDrive, но реализация хромала, плюс данный облачный сервис не устраивал "заказчика". Хотя изначально он предлагал несколько облаков на выбор, но в итоге отверг всё кроме mail.ru, решения которому так и не нашлось. Да и How-то это накладно всё выходило, нужно было подключать БД, использовать server для хранения и т.д. К слову, ему по-прежнему нужно это веб-приложение. Так How у нас не срослось, я решил сделать информационного бота. Он должен был получать ссылки на игру из магазина Google Play, парсить ссылку и сохранять полученную информацию в библиотеку, а затем писать её в json файл. Тем самым при каждом requestе библиотека может расширяться благодаря усorям пользователей. В дальнейшем получить информацию об игре в удобном виде можно не заходя в Google Play. Вы просто пишите команду /library Тут_название_игры и получаете всё, что нужно. Но есть несколько трудностей, о которых еще расскажу. По началу продвигался медленно, так How параллельно начал проходить два курса по SQL. Банально не мог понять, How вообще работает бот, и How обрабатывать requestы. Встретил товарища, которому тоже было интересно поработать над проектом. Первый вариант бота был готов примерно через месяц, но с товарищем возникли разногласия (с моей стороны). Я занялся частью бота, которая отвечает за парсинг, а он непосредственно работал над requestами к боту и их обработкой. Он зачем-то стал усложнять бота, вводить Howие-то авторизации, придумывать админов, добавлять ненужную функциональность, плюс мне не совсем нравился его стиль написания codeа. На мой взгляд, это было не нужно в информационном боте. Так я принял решение, что напишу бота с нуля сам с нужным мне функционалом. Теперь расскажу, что собственно делает бот (с примера из codeа проекта). Полный code проекта приложу в конце статьи и, к сожалению, fully прокомментировать его физически не смогу. Любое сообщение пользователя, отправленное боту, - это an object класса Update. Он содержит в себе много информации (id messages, id чата, уникальный id юзера и т.д.). Есть несколько типов update: это может быть текстовое сообщение, это может быть ответ от telegram-клавиатуры (callback), фотография, аудио и т.д. Whatбы пользователь особо не баловался, я обрабатываю только текстовые requestы и callback'и от клавиатуры. Если пользователь отправит фотографию, бот его уведомит о том, что делать с ней он ничего не намерен. В главном классе бота, в методе onUpdateReceived бот получает апдейт.

@Override
    public void onUpdateReceived(Update update) {
        UpdatesReceiver.handleUpdates(update);
    }
который я передаю обработчику (собственный класс UpdatesReceiver):

public static void handleUpdates(Update update) {
        ...
        if (update.hasMessage() && update.getMessage().hasText()){
            log.info("[Update (id {}) типа \"Текстовое сообщение\"]", update.getUpdateId());
            new TextMessageHandler(update, replyGenerator).handleTextMessage();
        }
        else if (update.hasCallbackQuery()) {
            //логгирование
            new CallbackQueryHandler(update, replyGenerator).handleCallBackQuery();
        }
        else {
           //логгирование
            replyGenerator.sendTextMessage(update.getMessage().getChatId(), "Я могу принимать только текстовые messages!");
        }
    }
UpdatesReceiver - это центральный обработчик, который в зависимости от типа апдейта передает управление в другой специализированный обработчик: TextMessageHandler or CallbackQueryHandler, в конструкторы которым я дальше по цепочке передаю update. Update - это самое важное при работе с ботом и его нельзя терять, так How с помощью информации, хранящейся в апдейте, мы узнаем Howому юзеру и в Howой чат необходимо отсылать ответ. Для генерирования ответов пользователю написал отдельный класс. Он может отправлять обычное текстовое сообщение, сообщение с inline клавиатурой, сообщение с картинкой и сообщение с reply клавиатурой. Inline-клавиатура выглядит так: Telegram-бот в качестве первого проекта и его значимость для профессионального роста на личном опыте - 1Она задает кнопки, нажав на которые, пользователь отправляет callback'и serverу, которые можно обрабатывать почти таким же образом, что и обычные messages. Для её "поддержания" нужен свой обработчик. Мы задаем для каждой кнопки некое действие, которое затем записывается в an object Update. Т.е. для кнопки "Стоимость" мы задали описание "/price" для callback'а, которое в дальнейшем мы можем получить из апдейта. Далее в отдельном классе, я уже могу обработать данный callback:

public void handleCallBackQuery() {
  String call_data = update.getCallbackQuery().getData();
  long message_id = update.getCallbackQuery().getMessage().getMessageId();
  long chat_id = update.getCallbackQuery().getMessage().getChatId();
    switch (call_date){
      case "/price" :
        //тут что-то сделать
        break;
...
Reply-клавиатура выглядит так: Telegram-бот в качестве первого проекта и его значимость для профессионального роста на личном опыте - 2И по сути она заменяет пользователю набор текста. Нажав на кнопку "Библиотека", вы быстро отправите сообщение "Библиотека" боту. Для каждого типа клавиатуры я написал свой класс, реализовав паттерн "Строитель" (Builder): inline и reply. В итоге, можно по сути "нарисовать" нужную клавиатуру в зависимости от требований. Это жутко удобно, так How клавиатуры могут быть разными, а принцип остается тем же. Вот интуитивно понятный метод для отправки messages с inline-клавиатурой:

public synchronized void sendInlineKeyboardMessage(long chat_id, String gameTitle) {
        SendMessage keyboard = InlineKeyboardMarkupBuilder.create(chat_id)
                .setText("Вы может узнать следующую информацию об игре " + gameTitle)
                .row()
                .button("Стоимость " + "\uD83D\uDCB0", "/price " + gameTitle)
                .button("Обновлено " + "\uD83D\uDDD3", "/updated " + gameTitle)
                .button("Версия " + "\uD83D\uDEE0", "/version " + gameTitle)
                .endRow()
                .row()
                .button("Требования " + "\uD83D\uDCF5", "/requirements " + gameTitle)
                .button("Покупки " + "\uD83D\uDED2", "/iap " + gameTitle)
                .button("Размер " + "\uD83D\uDD0E", "/size " + gameTitle)
                .endRow()
                .row()
                .button("Получить всю информацию об игре" + "\uD83D\uDD79", "/all " + gameTitle)
                .endRow()
                .row()
                .button("Скрыть клавиатуру", "close")
                .endRow()
                .build();
        try {
            execute(keyboard);
        } catch (TelegramApiException e) {
            log.error("[Не удалось отправить сообщение с -inline- клавиатурой]: {}", e.getMessage());
        }
    }
Для придания боту строгого функционала, были придуманы специальные команды через символ слэша: /library, /help, /game и т.д. Иначе пришлось бы обрабатывать всякий мусор, который мог написать пользователь. Собственно, для этого был написан MessageHandler:

if (message.equals(ChatCommands.START.getDescription())) {
     replyGenerator.sendTextMessage(chat_id, new StartMessageHandler().reply());
     replyGenerator.sendReplyKeyboardMessage(chat_id);
}
else if (message.equals(ChatCommands.HELP.getDescription())
             || message.equalsIgnoreCase("Помощь")) {
      replyGenerator.sendTextMessage(chat_id, new HelpMessageHandler().reply());
}
 ...
Таким образом, в зависимости от того, Howую команду вы отправите боту, в работу будет включаться специальный обработчик. Идем далее и рассмотрим работу parserа и библиотеки. Если отправить боту ссылку на игру в магазине Google Play, то автоматически сработает специальный handler. В ответ пользователь получит информацию об игре в следующем виде: Telegram-бот в качестве первого проекта и его значимость для профессионального роста на личном опыте - 3Вместе с этим, будет вызван метод, который попробует добавить игру в библиотеку бота (сначала в локальную мапу, затем в -> json файл). Если игра уже есть в библиотеке, то будет осуществлена проверка (How в обычной хешмапе), и если данные полей (например, номер версии изменился), то игра в библиотеке будет перезаписана. Если же изменений не будет обнаружено, то ниHowих записей осуществляться не будет. Если игры в библиотеке вообще не было, то она сначала записывается в локальную мапу (an object вида тык), а затем пишется в json файл, так How при непредвиденном закрытии applications на serverе данные будут утеряны, а с помощью file их всегда можно будет прочитать. Собственно, библиотека при старте программы всегда в первый раз загружается из file из static блока:

static {
        TypeFactory typeFactory = mapper.getTypeFactory();
        MapType mapType = typeFactory.constructMapType(ConcurrentSkipListMap.class, String.class, GooglePlayGame.class);

        try {
            Path path = Paths.get(LIBRARY_PATH);
            if (!Files.exists(path)) {
                Files.createDirectories(path.getParent());
                Files.createFile(path);
                log.info("[Файл библиотеки создан]");
            }
            else {
                ConcurrentMap<string, googleplaygame=""> temporary = mapper.readValue(new File(LIBRARY_PATH), mapType);
                games.putAll(temporary);
                log.info("[Количество игр в загруженной библиотеке] = " + games.size());
            }
        }
        catch (IOException e) {
            log.error("[Ошибка при чтении/записи file] {}", e.getMessage());
        }
    }
Тут дополнительно приходится читать данные из file во временную мапу, которую потом "копировать" в полноценную, дабы сохранить нечувствительность к регистру при поиске игры в файле (написав tITan QuEST, бот все равно найдет игру Titan Quest в библиотеке). Другого решения найти не удалось, таковы особенности десериализации с использованием Jackson. Итак, при каждом requestе по ссылке игра по возможности добавляется в библиотеку, и библиотека тем самым расширяется. Далее информацию о конкретной игре можно достать по команде /library Название_Игры. Можно узнать How определенный параметр (например, текущую версию), так и все параметры сразу. Это реализовано с помощью inline-клавиатуры, которая была рассмотрена ранее. В ходе работы применял и навыки, полученные здесь в ходе решения задач. Например, список названий случайных игр, находящихся в библиотеке (опция доступна по команде /library):

private String getRandomTitles(){
        if (LibraryService.getLibrary().size() < 10){
            return String.join("\n", LibraryService.getLibrary().keySet());
        }
        List<string> keys = new ArrayList<>(LibraryService.getLibrary().keySet());
        Collections.shuffle(keys);
        List<string> randomKeys = keys.subList(0, 10);
        return String.join("\n", randomKeys);
    }
Как бот обрабатывает ссылки? Он проверяет их на принадлежность к Google Play (хост, протокол, порт):

private static class GooglePlayCorrectURL {

        private static final String VALID_HOST = "play.google.com";

        private static final String VALID_PROTOCOL = "https";

        private static final int VALID_PORT = -1;

        private static boolean isLinkValid(URI link) {
            return (isHostExist(link) && isProtocolExist(link) && link.getPort() == VALID_PORT);
        }

        private static boolean isProtocolExist(URI link) {
            if (link.getScheme() != null) {
                return link.getScheme().equals(VALID_PROTOCOL);
            }
            else {
                return false;
            }
        }

        private static boolean isHostExist(URI link) {
            if (link.getHost() != null) {
                return link.getHost().equals(VALID_HOST);
            }
            else {
                return false;
            }
        }
Если всё в порядке, то бот подключается по ссылке с помощью библиотеки Jsoup, которая позволяет достать HTML-code странички, подлежащий дальнейшему анализу и парсингу. Обмануть бота неправильной or вредной ссылкой не получится.

if (GooglePlayCorrectURL.isLinkValid(link)){
     if (!link.getPath().contains("apps")){
         throw new InvalidGooglePlayLinkException("К сожалению, бот работает исключительно с играми. Введите другую ссылку.");
     }
     URL = forceToRusLocalization(URL);
     document = Jsoup.connect(URL).get();
 }
     else {
         throw new NotGooglePlayLinkException();
      }
...
Здесь пришлось решать проблему с региональными настройками. Бот подключается к магазину Google Play из serverа, который находится в Европе, поэтому и страничка в магазине Google Play открывается на соответствующем языке. Пришлось писать костыль, который принудительно осуществляет "редирект" на российскую версию странички (проект все-таки нацелен был на нашу аудиторию). Для этого в конце ссылки нужно аккуратно дописать в GET requestе к serverу Google Play параметр hl: &hl=ru.

private String forceToRusLocalization(String URL) {
        if (URL.endsWith("&hl=ru")){
            return URL;
        }
        else {
            if (URL.contains("&hl=")){
                URL = URL.replace(
                        URL.substring(URL.length()-"&hl=ru".length()), "&hl=ru");
            }
            else {
                URL += "&hl=ru";
            }
        }
        return URL;
    }
После удачного подключения мы получаем HTML-document, готовый для анализа и парсинга, но это уже выходит за рамки этой статьи. Код parserа тут. Собственно parser достает нужную информацию и создает an object с игрой, который в дальнейшем в случае необходимости добавляется в библиотеку. <h2>Подытожу</h2>Бот поддерживает несколько команд, в которых заложена определенная функциональность. Он получает сообщение от пользователя и сопоставляет их со своими командами. Если это link or команда /game + link, он проверяет эту ссылку на принадлежность к Google Play. Если link корректная, он осуществляет подключение посредством Jsoup и получает HTML-document. Данный document анализируется на основе написанного parserа. Из documentа вытаскивается нужная информация об игре, и далее an object с игрой заполняется этими данными. Далее an object с игрой помещается в локальное хранorще (если игры там еще нет) и тут же записывается в файл для избежания потерь данных. Записанную в библиотеку игру (название игры - ключ для мапы, an object с игрой - meaning для мапы), можно получить по команде /library Название_игры. Если указанная игра будет найдена в библиотеке бота, то пользователю вернется inline-клавиатура, с помощью которой он может получить информацию об игре. Если игры найдено не будет, необходимо либо убедиться в правильности написания названия (оно должно целиком соответствовать названию игры в магазине Google Play за исключением регистра), либо добавить игру в библиотеку, отправив боту ссылку на игру. Деплой бота я осуществлял на heroku и для тех, кто в будущем планирует написать своего бота и разместить его бесплатно на heroku, дам парочку рекомендаций для решения трудностей, с которыми вы можете столкнуться (т.к. с ними я столкнулся сам). К сожалению, из-за особенностей heroku, библиотека бота постоянно "обнуляется" раз в 24 часа. Мой тариф не поддерживает хранение файлов на serverах heroku, поэтому он просто подтягивает мой файл с играми с гитхаба. Решений было несколько: использовать БД, либо искать другой server, который хранил бы этот файл с игрой. Я пока решил ничего не предпринимать, так How по сути бот не такой уж и полезный. Он мне был необходим скорее для получения полноценного опыта, чего я в принципе и добился. Итак, рекомендации по heroku:
  1. Регистрироваться на heroku скорее всего придется с помощью VPN, если вы живете в России.

  2. В корень проекта необходимо положить файл без расширения под названием Procfile. Его содержимое должно быть таким: https://github.com/miroha/Telegram-Bot/blob/master/Procfile

  3. В pom.xml добавить следующие строчки по образцу, где в теге mainClass указать way to классу, который содержит main метод: bot.BotApplication (если класс BotApplication лежит в папке bot).

  4. Не осуществлять Howих-то сборок проекта с помощью команд mvn package и т.д., heroku все соберет за вас сам.

  5. Желательно добавить в проект gitignore, например такой:

    
    # Log file
    *.log
    
    # Compiled resources
    target
    
    # Tests
    test
    
    # IDEA files
    .idea
    *.iml
  6. Собственно загрузить проект на github, а далее подключить репозиторий в heroku (либо используйте другие способы, там их 3, если не ошибаюсь).

  7. Если загрузка прошла успешно ("Build succeeded"), обязательно зайдите в Configure Dynos:

    Telegram-бот в качестве первого проекта и его значимость для профессионального роста на личном опыте - 4

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

  8. Прячьте токен бота на гитхабе. Для этого необходимо получать токен из переменной окружения:

    
    public class Bot extends TelegramLongPollingBot {
    
        private static final String BOT_TOKEN = System.getenv("TOKEN");
    
        @Override
        public String getBotToken() {
            return BOT_TOKEN;
        }
    ...
    }

    А затем после деплоя бота, задать эту переменную в dashboard'e heroku во вкладке Settings (справа от TOKEN будет поле VALUE, туда и копируйте токен вашего бота):

    Telegram-бот в качестве первого проекта и его значимость для профессионального роста на личном опыте - 5
Total, за 2 месяца работы над собственным проектом я:
  • получил fully рабочий проект, написанный на Java;
  • научился работать со сторонним API (Telegram Bot API);
  • на практике углубился в сериализацию, много поработал с JSON и библиотекой Jackson (изначально использовал GSON, но с ней были проблемы);
  • укрепил свои навыки при работе с fileми, познакомился с Java NIO;
  • научился работать с конфигурационными .xml fileми и приучил себя к логгированию;
  • улучшил владение средой разработки (IDEA);
  • научился работать с git и познал ценность gitignore;
  • получил навыки в парсинге веб-страничек (библиотека Jsoup);
  • изучил и использовал несколько паттернов проектирования;
  • развил в себе чувство и желание улучшать code (рефакторинг);
  • научился находить решения в сети и не стесняться задавать вопросы, на которые не удалось найти ответа.
Telegram-бот в качестве первого проекта и его значимость для профессионального роста на личном опыте - 7Я не знаю, насколько полезным or бесполезным получился бот, насколько красивый/некрасивый code, но опыт, который я получил, определенно того стоил. У меня возникло чувство ответственности за свой проект. Его то и дело хочется улучшать, добавлять что-то новое. Когда я смог его запустить и убедиться в том, что всё работает так, How я и хотел, я испытал настоящий кайф. Разве это не главное? Получать удовольствие от того, чем ты занимаешься и радоваться каждой работающей строчке codeа, How последней шоколадке. Поэтому если вы осваиваете программирование, то мой вам совет: не засиживайтесь здесь до 40 уровня, а приступайте к собственному проекту How можно раньше. Если кому интересно, исходный code проекта находится тут (переписан под Spring): https://github.com/miroha/GooglePlayGames-TelegramBot Последние месяца два я почти не изучаю новый материал, так How мне кажется достиг тупика. Без работы уже не вижу куда развиваться, разве что учить Spring Framework, чем я и планирую заняться в ближайший месяц. А потом попробую "переписать" бота с использованием этого фреймворка. Готов ответить на любые вопросы. :) Всем успехов! UPDATE от 07.07.2020 Репозиторий с ботом на чистой Java был утерян (я его удалял, копия осталась на другой локальной машине), но загрузил переписанного бота под Spring Boot: https://github.com/miroha/GooglePlayGames-TelegramBot
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION