JavaRush/Java блог/Java-проекты/Добавляем возможность подписаться на группу статей. (Част...
Roman Beekeeper
35 уровень

Добавляем возможность подписаться на группу статей. (Часть 3) - "Java-проект от А до Я"

Статья из группы Java-проекты
участников
Еще раз привет. Это заключительная статья из STEP_6, в которой мы поговорим о добавлении функциональности задачи JRTB-6. В тех двух предыдущих статьях (часть 1, часть 2) мы уже подготовили почти все, что нужно. В этой части — кульминация процесса. Всем, кто дочитал эту серию статей до этого момента от самого начала — большой респект. Это значит, что мотивации хватит на то, чтобы найти отличную работу. А теперь перейдем к делу."Java-проект от А до Я": Добавляем возможность подписаться на группу статей. Часть 3 - 1

Реализуем JRTB-6

В этот раз будем делать задачу со стороны телеграм-бота, потому что работа по обновлению БД вся сделана, сущности БД настроены и готовы к работе. Добавим в CommandName новое значение — LIST_GROUP_SUB:
LIST_GROUP_SUB("/listGroupSub");
Создадим команду ListGroupSubCommand:
package com.github.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.repository.entity.GroupSub;
import com.github.javarushcommunity.jrtb.repository.entity.TelegramUser;
import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import com.github.javarushcommunity.jrtb.service.TelegramUserService;
import org.telegram.telegrambots.meta.api.objects.Update;

import javax.ws.rs.NotFoundException;
import java.util.stream.Collectors;

import static com.github.javarushcommunity.jrtb.command.CommandUtils.getChatId;

/**
* {@link Command} for getting list of {@link GroupSub}.
*/
public class ListGroupSubCommand implements Command {

   private final SendBotMessageService sendBotMessageService;
   private final TelegramUserService telegramUserService;

   public ListGroupSubCommand(SendBotMessageService sendBotMessageService, TelegramUserService telegramUserService) {
       this.sendBotMessageService = sendBotMessageService;
       this.telegramUserService = telegramUserService;
   }

   @Override
   public void execute(Update update) {
       //todo add exception handling
       TelegramUser telegramUser = telegramUserService.findByChatId(getChatId(update))
               .orElseThrow(NotFoundException::new);

       String message = "Я нашел все подписки на группы: \n\n";
       String collectedGroups = telegramUser.getGroupSubs().stream()
               .map(it -> "Группа: " + it.getTitle() + " , ID = " + it.getId() + " \n")
               .collect(Collectors.joining());

       sendBotMessageService.sendMessage(telegramUser.getChatId(), message + collectedGroups);
   }
}
Здесь все максимально просто — по имеющемуся chat_id получаем пользователя, и у него уже в объекте будут собраны все его подписки на группы. Мы это настраивали во второй части. Опять-таки, добавил //todo, чтобы не забыть добавить обработку исключений, которые могут появиться при работе. Следующим этапом обновляем CommandContainer, добавляя ему новую команду:
put(LIST_GROUP_SUB.getCommandName(), new GroupSubListCommand(sendBotMessageService, telegramUserService))
По сути все: теперь нужно написать еще тесты, обновить /help команду (добавить описание для новых команд) и протестировать новый функционал через Телеграм. Напишем тест для ListGroupSubCommand. Так как логика у команды не типичная, то будем писать тест, не привязываясь к AbstractCommandTest классу, как мы делали до этого:
package com.github.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.repository.entity.GroupSub;
import com.github.javarushcommunity.jrtb.repository.entity.TelegramUser;
import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import com.github.javarushcommunity.jrtb.service.TelegramUserService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.telegram.telegrambots.meta.api.objects.Message;
import org.telegram.telegrambots.meta.api.objects.Update;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static com.github.javarushcommunity.jrtb.command.CommandName.LIST_GROUP_SUB;

@DisplayName("Unit-level testing for ListGroupSubCommand")
public class ListGroupSubCommandTest {

   @Test
   public void shouldProperlyShowsListGroupSub() {
       //given
       TelegramUser telegramUser = new TelegramUser();
       telegramUser.setActive(true);
       telegramUser.setChatId("1");

       List<GroupSub> groupSubList = new ArrayList<>();
       groupSubList.add(populateGroupSub(1, "gs1"));
       groupSubList.add(populateGroupSub(2, "gs2"));
       groupSubList.add(populateGroupSub(3, "gs3"));
       groupSubList.add(populateGroupSub(4, "gs4"));

       telegramUser.setGroupSubs(groupSubList);

       SendBotMessageService sendBotMessageService = Mockito.mock(SendBotMessageService.class);
       TelegramUserService telegramUserService = Mockito.mock(TelegramUserService.class);

       Mockito.when(telegramUserService.findByChatId(telegramUser.getChatId())).thenReturn(Optional.of(telegramUser));

       ListGroupSubCommand command = new ListGroupSubCommand(sendBotMessageService, telegramUserService);

       Update update = new Update();
       Message message = Mockito.mock(Message.class);
       Mockito.when(message.getChatId()).thenReturn(Long.valueOf(telegramUser.getChatId()));
       Mockito.when(message.getText()).thenReturn(LIST_GROUP_SUB.getCommandName());
       update.setMessage(message);

       String collectedGroups = "Я нашел все подписки на группы: \n\n" +
               telegramUser.getGroupSubs().stream()
                       .map(it -> "Группа: " + it.getTitle() + " , ID = " + it.getId() + " \n")
                       .collect(Collectors.joining());

       //when
       command.execute(update);

       //then
       Mockito.verify(sendBotMessageService).sendMessage(telegramUser.getChatId(), collectedGroups);
   }

   private GroupSub populateGroupSub(Integer id, String title) {
       GroupSub gs = new GroupSub();
       gs.setId(id);
       gs.setTitle(title);
       return gs;
   }
}

Обновим /help команду

В нашем случае /help команда выполняет роль документации для работы с ботом, поэтому ее нужно не забывать обновлять, чтобы пользователь смог ею воспользоваться. У нас добавились две команды, поэтому обновим текст, который будет приходить:
public static final String HELP_MESSAGE = String.format("✨Дотупные команды✨\n\n"

               + "Начать\\закончить работу с ботом:\n"
               + "%s - начать работу со мной\n"
               + "%s - приостановить работу со мной\n\n"

               + "Работа с подписками на группы:\n"
               + "%s - подписаться на группу статей\n"
               + "%s - получить список групп, на которые подписан\n\n"

               + "%s - получить помощь в работе со мной\n"
               + "%s - получить мою статистику использования\n",
       START.getCommandName(), STOP.getCommandName(), ADD_GROUP_SUB.getCommandName(),
       LIST_GROUP_SUB.getCommandName(), HELP.getCommandName(), STAT.getCommandName());
Также я обновил текст ответов бота: сделал так, чтобы он всегда был на “ты” с пользователем, а то там было и “ты”, и “вы”... Теперь можно будет создать хоть некую связь в работе бота.

Тестируем работу обновленного бота

Локально запускаем нашего бота и делаем следующее:
  1. Выполняем /start команду — чтобы быть уверенным, что пользователь в тестовом случае добавлен в БД.
  2. Выполняем /help команду — проверяем, что все ок, как мы хотели.
  3. Далее выполняем /addGroupSub команду.
  4. Из предложенного списка ID-шников групп добавляем в разнобой несколько.
  5. Выполняем /listGroupSub команду, чтобы удостовериться, что группы записаны на пользователя.
Поехали! Запускаем БД через docker-compose-test.yml и стартуем наш SpringBoot. Далее, заходим в нашего тестового бота и выполняем /start команду, а за ней — /help:"Java-проект от А до Я": Добавляем возможность подписаться на группу статей. Часть 3 - 2Далее вводим команду /addGroupSub:"Java-проект от А до Я": Добавляем возможность подписаться на группу статей. Часть 3 - 3Выпадающий список говорит, что Java-клиент работает как нужно: у нас все группы с их ID-шниками, описание команды помогает (надеюсь) понять, что нужно дальше, поэтому добавляем несколько групп в подписку:"Java-проект от А до Я": Добавляем возможность подписаться на группу статей. Часть 3 - 4Теперь у нас есть 5 подписок, так что можно выполнить команду /listGroupSub:"Java-проект от А до Я": Добавляем возможность подписаться на группу статей. Часть 3 - 5И тут мы получаем какую-то дичь... Непонятно, почему только что показывало title без каких-либо проблем, а здесь нет. Идем в базу данных, чтобы посмотреть что там:"Java-проект от А до Я": Добавляем возможность подписаться на группу статей. Часть 3 - 6В базе данных записаны те же вопросы, но только для тех, где кириллица. Значит какая-то проблема с кодировкой. Нужно настроить, видимо, базу данных и драйвер для соединения с БД. Нам нужно UTF-8. Но как его добавить? После нескольких минут поиска в интернетах нашел: для драйвера нужно обновить url переменную. И для настройки образа докера в docker-compose вообще самая первая ссылка, но ответ не первый)) Поэтому, зная это, обновим проперти и docker-compose файлы.
application.properties:
spring.datasource.url=jdbc:mysql://jrtb-db:3306/jrtb_db?characterEncoding=UTF-8

application-test.properties:
spring.datasource.url=jdbc:mysql://localhost:3306/dev_jrtb_db?characterEncoding=UTF-8

docker-compose.yml (добавил последнюю строку):
jrtb-db:
 image: mysql:5.7
 restart: always
 environment:
   MYSQL_USER: ${BOT_DB_USERNAME}
   MYSQL_PASSWORD: ${BOT_DB_PASSWORD}
   MYSQL_DATABASE: 'jrtb_db'
   MYSQL_ROOT_PASSWORD: 'root'
 ports:
   - '3306:3306'
 expose:
   - '3306'
 command: --character-set-server=utf8 --collation-server=utf8_general_ci

docker-compose-test.yml (добавил последнюю строку)
jrtb-db-dev:
 image: mysql:5.7
 restart: always
 environment:
   MYSQL_DATABASE: 'dev_jrtb_db'
   # So you don't have to use root, but you can if you like
   MYSQL_USER: 'dev_jrtb_db_user'
   # You can use whatever password you like
   MYSQL_PASSWORD: 'dev_jrtb_db_password'
   # Password for root access
   MYSQL_ROOT_PASSWORD: 'root'
 ports:
   # <Port exposed> : < MySQL Port running inside container>
   - '3306:3306'
 expose:
   # Opens port 3306 on the container
     - '3306'
 command: --character-set-server=utf8 --collation-server=utf8_general_ci
После этих обновлений нужно стереть все данные в БД и начать заново. Стереть очень просто: нужно выполнить команду: docker-compose -f docker-compose-test.yml down после чего все данные и БД удаляются. И заново запустить, уже с обновленной кодировкой: docker-compose -f docker-compose-test.uml up База готова. Запускаем обновленное приложение и смотрим. Я сразу пробегусь быстро и покажу результат:"Java-проект от А до Я": Добавляем возможность подписаться на группу статей. Часть 3 - 7И вот теперь мы получили именно то, что и хотели. Вот это похоже уже на правду.

Окончание

Теперь я думаю, что можно завершать работу по этому шагу. Сделано много, действительно много. Обновим версию приложения до 0.5.0-SNAPSHOT и RELEASE_NOTES.
# Release Notes ## 0.5.0-SNAPSHOT * JRTB-5: added ability to subscribe on group * JRTB-6: added ability to get a list of group subscriptions.
Дальше все как обычно: создаем новый коммит со всеми изменениями. Главное — для отчетности добавить описание двух задач, которые сделали за этот шаг. Поэтому вот такой будет коммент:
STEP_6 JRTB-5: added ability to subscribe on group JRTB-6: added ability to see the list of the group subscription.
В итоге получилось 47 измененных файлов… Это большое изменение. Хотя по описанию функциональности и не скажешь. Ведь чтобы понять всю глубину, нужно знать, что необходимо написать джава клиент к АПИ, обновить все приложение по сути. Вот такая она, работа на сервере — работы много, а видимость со стороны клиентской части небольшая…)) Друзья, традиционно предлагаю вам способ показать интерес к моей работе — подписаться на гитхаб аккаунт, присоединиться к телеграм-каналу и написать вопрос по статье, если что-то не понятно! Вот ссылка на пулл-реквест с изменениями за этот STEP_6. Всем спасибо за прочтение. Дальше больше — поговорим об удалении подписки, деактивации профиля и другое. Не переключайтесь))

Список всех материалов серии в начале этой статьи.

Комментарии (6)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Anonymous #2856674 Software Developer
20 апреля, 11:44
Роман, спасибо!!! Супер уроки!
12 февраля 2022, 15:05
При подписке на группу пишет: Неправильный ID группы, в чем может быть делоо? все сверил, ошибок по коду нет
Bolivar Li
Уровень 32
20 декабря 2023, 08:53
В том же моменте встал , не решил вопрос спустя полтора года ?)
Bolivar Li
Уровень 32
20 декабря 2023, 09:36
Уф 🧐, может кому пригодиться <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>${apache.commons.version}</version> </dependency> как то не сработала зависимость , менял версии , тоже нет . Можно было покапаться , но как то проще было решить проблему методом: private boolean isNumeric(String str) { if (str == null || str.isEmpty()) { return false; } for (char c : str.toCharArray()) { if (!Character.isDigit(c)) { return false; } } return true; } Вопрос решился. 🐧
Артем В.
Уровень 41
31 января 2022, 11:15
Опечатка - "Следующим этапом обновляем CommandContainer, добавляя ему новую команду:
put(LIST_GROUP_SUB.getCommandName(), new GroupSubListCommand(sendBotMessageService, telegramUserService))
тут не
new GroupSubListCommand
а
new ListGroupSubCommand
и в предыдущих частях пропущено создание утильного класса CommandUtils
Сергей
Уровень 8
19 апреля 2021, 14:05
Спасибо огромное за Ваши старания, очень надеюсь что Вы доведёте начатое до конца.🙂