JavaRush /Java блог /Random UA /Додаємо можливість передплатити групу статей. (Частина 2)...
Roman Beekeeper
35 рівень

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

Стаття з групи Random UA
Всім привіт! Продовжуємо роботу над завданням, яке ми розпочали минулого тижня ."Java-проект від А до Я": Додаємо можливість передплатити групу статей.  Частина 2 - 1

Реалізуємо JRTB-5

Тепер нам потрібно додати команду, щоб ми могли передплатити якусь групу статей з JavaRush. Як це зробити? Ітимемо за найпростішим сценарієм, який я придумав. Так як доступ у нас по ID групи, нам потрібно, щоб його користувач передав. Для цього користувач вводитиме команду /addGroupSub GROUP_ID, яка буде працювати за одним із двох варіантів: якщо приходить тільки сама команда: /addGroupSub , у відповідь передається список усіх груп та їх ID-шники. Тоді користувач зможе вибрати потрібний ID групи і скласти другий варіант запиту в цій команді: /addGroupSubGROUP_ID - і тоді вже буде запис цієї групи з цим користувачем. Думаю, у майбутньому можна буде зробити й краще. Наша мета — показати саме розробку, а не супер крутий user experience (соромно сказати, але я не знаю терміну російською, яка б означала це). Щоб правильно додати функціональність, яка йде крізь усі програми (у нашому випадку - від клієнта телеграм-бота до бази даних), потрібно починати з якогось кінця. Робитимемо це з боку БД.

Додаємо нову міграцію до БД

Перше, що потрібно зробити, - додати нову міграцію бази даних і можливість зберігати дані про підписку користувачів на групи в JR. Щоб згадати, як це має бути, поверніться до статті “ Планування проекту: сім разів відміряй ”. Там на другому фото намальовано приблизну схему БД. Нам потрібно додати таблицю для збереження інформації групи:
  • ID групи у JavaRush буде і нашим ID. Ми довіряємо їм і вважаємо, що ці ID є унікальними;
  • title – у наших картинках це було name – неформальна назва групи; тобто те, що бачимо на сайті JavaRush;
  • last_article_id – а це цікаве поле. Воно буде зберігати останній ID-шник статті у цій групі, яку бот вже надіслав своїм передплатникам. За допомогою цього поля працюватиме механізм пошуку нових статей. Новим передплатникам не будуть надходити статті, опубліковані до того, як користувач підписався: лише ті, що вийшли після підписки на групу.
Також у нас буде зв'язок many-to-many між таблицею груп та користувачів, тому що у кожного користувача може бути багато підписок на групи (one-to-many), і у кожної підписки на групу може бути багато користувачів (one-to- багато, тільки з іншого боку). Виходить, що це буде наш many-to-many. У кого це викликає питання, перегляньте статті за бази даних. Так, скоро планую створити пост у Телеграм-каналі, де зберу докупи всі статті з БД. Ось як виглядатиме наша друга міграція БД.
V00002__created_groupsub_many_to_many.sql:

-- add PRIMARY KEY FOR tg_user
ALTER TABLE tg_user ADD PRIMARY KEY (chat_id);

-- ensure that the tables with these names are removed before creating a new one.
DROP TABLE IF EXISTS group_sub;
DROP TABLE IF EXISTS group_x_user;

CREATE TABLE group_sub (
   id INT,
   title VARCHAR(100),
   last_article_id INT,
   PRIMARY KEY (id)
);

CREATE TABLE group_x_user (
   group_sub_id INT NOT NULL,
   user_id VARCHAR(100) NOT NULL,
   FOREIGN KEY (user_id) REFERENCES tg_user(chat_id),
   FOREIGN KEY (group_sub_id) REFERENCES group_sub(id),
   UNIQUE(user_id, group_sub_id)
);
Спочатку я змінюю стару таблицю — додаю їй первинний ключ. Я якось це змарнував того разу, але тепер мені MySQL не давав можливість додати FOREIGN KEY для таблиці gorup_x_user, і я в рамках цієї міграції оновив базу даних. Зверніть увагу на важливий аспект. Зміну БД потрібно робити саме так — у новій міграції все те, що потрібно, але ніяк не шляхом оновлення вже випущеної міграції. Так, у нашому випадку нічого не сталося б, оскільки це тестовий проект і ми знаємо, що він розгортається тільки в одному місці, але це був би неправильний підхід. Адже ми хочемо, щоб усе було правильно. Далі йде видалення таблиць перед їх створенням. Навіщо це? Щоб якби за якоюсь випадковістю таблиці з такими іменами були в БД, міграція не впала б і відпрацювала саме так, як і очікується. І далі додаємо дві таблиці. Усі як і хотіли. Тепер потрібно запустити нашу програму. Якщо все запуститься і не зламається, то міграція записана. А щоб це перевірити ще раз, йдемо в базу даних переконатися, що: а) такі таблиці з'явабося; б) є ​​новий запис у технічній таблиці flyway. У цьому роботу з міграцією закінчено, переходимо до репозиторіям.

Додаємо шар репозиторію

Завдяки Spring Boot Data тут все дуже просто: нам потрібно додати сутність GroupSub, трохи оновити TelegramUser і додати майже порожній GroupSubRepository: GroupSub entity додаємо в той же пакет, що і TelegramUser:
package com.github.codegymcommunity.jrtb.repository.entity;

import lombok.Data;
import lombok.EqualsAndHashCode;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

import static java.util.Objects.isNull;

@Data
@Entity
@Table(name = "group_sub")
@EqualsAndHashCode
public class GroupSub {

   @Id
   private Integer id;

   @Column(name = "title")
   private String title;

   @Column(name = "last_article_id")
   private Integer lastArticleId;

   @ManyToMany(fetch = FetchType.EAGER)
   @JoinTable(
           name = "group_x_user",
           joinColumns = @JoinColumn(name = "group_sub_id"),
           inverseJoinColumns = @JoinColumn(name = "user_id")
   )
   private List<TelegramUser> users;

   public void addUser(TelegramUser telegramUser) {
       if (isNull(users)) {
           users = new ArrayList<>();
       }
       users.add(telegramUser);
   }
}
З того, що варто відзначити, у нас є додаткове поле users, яке міститиме колекцію всіх користувачів, підписаних на групу. І дві інструкції - ManyToMany і JoinTable - саме для цього нам і потрібні. Таке за змістом поле потрібно додати і до TelegramUser:
@ManyToMany(mappedBy = "users", fetch = FetchType.EAGER)
private List<GroupSub> groupSubs;
Це поле використовує джоїни, написані в GroupSub сутності. І, власне, наш клас репозиторій для GroupSub — GroupSubRepository :
package com.github.codegymcommunity.jrtb.repository;

import com.github.codegymcommunity.jrtb.repository.entity.GroupSub;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
* {@link Repository} for {@link GroupSub} entity.
*/
@Repository
public interface GroupSubRepository extends JpaRepository<GroupSub, Integer> {
}
На цьому етапі нам не потрібні додаткові методи: тих, що реалізовані в предку JpaRepository, нам вистачає. Напишемо тест в TelegramUserRepositoryIT, який перевірятиме, що наша багато-багато працює. Ідея тесту полягає в тому, що ми додамо до бази даних 5 груп підписок на одного користувача через sql скрипт, отримаємо цього користувача за його ID та перевіримо, що нам прийшли саме ті групи і саме з такими значеннями. Як це зробити? У дані можна зашити лічильник, яким потім пройдемо і перевіримо. Ось скрипт fiveGroupSubsForUser.sql:
INSERT INTO tg_user VALUES (1, 1);

INSERT INTO group_sub VALUES
(1, 'g1', 1),
(2, 'g2', 2),
(3, 'g3', 3),
(4, 'g4', 4),
(5, 'g5', 5);

INSERT INTO group_x_user VALUES
(1, 1),
(2, 1),
(3, 1),
(4, 1),
(5, 1);
І сам тест:
@Sql(scripts = {"/sql/clearDbs.sql", "/sql/fiveGroupSubsForUser.sql"})
@Test
public void shouldProperlyGetAllGroupSubsForUser() {
   //when
   Optional<TelegramUser> userFromDB = telegramUserRepository.findById("1");

   //then
   Assertions.assertTrue(userFromDB.isPresent());
   List<GroupSub> groupSubs = userFromDB.get().getGroupSubs();
   for (int i = 0; i < groupSubs.size(); i++) {
       Assertions.assertEquals(String.format("g%s", (i + 1)), groupSubs.get(i).getTitle());
       Assertions.assertEquals(i + 1, groupSubs.get(i).getId());
       Assertions.assertEquals(i + 1, groupSubs.get(i).getLastArticleId());
   }
}
Тепер додамо такий самий за змістом тест для GroupSub сутності. Для цього створимо тестовий клас groupSubRepositoryIT у тому ж пакеті, що і groupSubRepositoryIT :
package com.github.codegymcommunity.jrtb.repository;

import com.github.codegymcommunity.jrtb.repository.entity.GroupSub;
import com.github.codegymcommunity.jrtb.repository.entity.TelegramUser;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;

import java.util.List;
import java.util.Optional;

import static org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.NONE;

/**
* Integration-level testing for {@link GroupSubRepository}.
*/
@ActiveProfiles("test")
@DataJpaTest
@AutoConfigureTestDatabase(replace = NONE)
public class GroupSubRepositoryIT {

   @Autowired
   private GroupSubRepository groupSubRepository;

   @Sql(scripts = {"/sql/clearDbs.sql", "/sql/fiveUsersForGroupSub.sql"})
   @Test
   public void shouldProperlyGetAllUsersForGroupSub() {
       //when
       Optional<GroupSub> groupSubFromDB = groupSubRepository.findById(1);

       //then
       Assertions.assertTrue(groupSubFromDB.isPresent());
       Assertions.assertEquals(1, groupSubFromDB.get().getId());
       List<TelegramUser> users = groupSubFromDB.get().getUsers();
       for(int i=0; i<users.size(); i++) {
           Assertions.assertEquals(String.valueOf(i + 1), users.get(i).getChatId());
           Assertions.assertTrue(users.get(i).isActive());
       }
   }
}
І відсутній скрипт fiveUsersForGroupSub.sql:
INSERT INTO tg_user VALUES
(1, 1),
(2, 1),
(3, 1),
(4, 1),
(5, 1);

INSERT INTO group_sub VALUES (1, 'g1', 1);

INSERT INTO group_x_user VALUES
(1, 1),
(1, 2),
(1, 3),
(1, 4),
(1, 5);
У цьому частину роботи з репозиторієм вважатимуться закінченою. Тепер напишемо шар сервісів.

Пишемо GroupSubService

На даному етапі для роботи з групами передплат нам потрібно лише вміти зберігати їх, тому жодних проблем: створюємо сервіс GroupSubService та його реалізацію GroupSubServiceImpl у пакеті, де є й інші сервіси — service:
package com.github.codegymcommunity.jrtb.service;

import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupDiscussionInfo;
import com.github.codegymcommunity.jrtb.repository.entity.GroupSub;

/**
* Service for manipulating with {@link GroupSub}.
*/
public interface GroupSubService {

   GroupSub save(String chatId, GroupDiscussionInfo groupDiscussionInfo);
}
І його реалізацію:
package com.github.codegymcommunity.jrtb.service;

import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupDiscussionInfo;
import com.github.codegymcommunity.jrtb.repository.GroupSubRepository;
import com.github.codegymcommunity.jrtb.repository.entity.GroupSub;
import com.github.codegymcommunity.jrtb.repository.entity.TelegramUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.ws.rs.NotFoundException;
import java.util.Optional;

@Service
public class GroupSubServiceImpl implements GroupSubService {

   private final GroupSubRepository groupSubRepository;
   private final TelegramUserService telegramUserService;

   @Autowired
   public GroupSubServiceImpl(GroupSubRepository groupSubRepository, TelegramUserService telegramUserService) {
       this.groupSubRepository = groupSubRepository;
       this.telegramUserService = telegramUserService;
   }

   @Override
   public GroupSub save(String chatId, GroupDiscussionInfo groupDiscussionInfo) {
       TelegramUser telegramUser = telegramUserService.findByChatId(chatId).orElseThrow(NotFoundException::new);
       //TODO add exception handling
       GroupSub groupSub;
       Optional<GroupSub> groupSubFromDB = groupSubRepository.findById(groupDiscussionInfo.getId());
       if(groupSubFromDB.isPresent()) {
           groupSub = groupSubFromDB.get();
           Optional<TelegramUser> first = groupSub.getUsers().stream()
                   .filter(it -> it.getChatId().equalsIgnoreCase(chatId))
                   .findFirst();
           if(first.isEmpty()) {
               groupSub.addUser(telegramUser);
           }
       } else {
           groupSub = new GroupSub();
           groupSub.addUser(telegramUser);
           groupSub.setId(groupDiscussionInfo.getId());
           groupSub.setTitle(groupDiscussionInfo.getTitle());
       }
       return groupSubRepository.save(groupSub);
   }
}
Щоб Spring Data запрацювала правильно і створився запис many-to-many, нам потрібно для групи підписки, яку ми створюємо, дістати з нашого БД користувача і додати його в об'єкт GroupSub. Тим самим, коли ми передамо на збереження цю передплату, буде створено ще й зв'язок через таблицю group_x_user. Можливо ситуація, коли вже створено таку групу передплати і треба легко додати до неї ще одного користувача. Для цього ми спочатку отримуємо за ID групи з БД, і якщо запис є, працюємо з ним, якщо ні – створюємо новий. Важливо, що для роботи з TelegramUser ми використовуємо TelegramUserService, щоб дотримуватися останнього з принципів SOLID. На даний момент, якщо ми не знаходимо запису за ID, я просто викидаю виняток. Воно ніяк зараз не обробляється: ми це зробимо вже наприкінці перед MVP. Напишемо два юніт-тести на класGroupSubServiceTest . Які нам потрібні? Я хочу бути впевненим, що GroupSubRepository викликає метод save і буде передана GroupSub сутність з одним єдиним користувачем — тим, що поверне нам TelegramUserService за наданим ID. І другий варіант, коли група з таким ID вже є в БД і вже ця група має один користувач, і потрібно перевірити, що до цієї групи буде додано ще один користувач і цей об'єкт збережений. Ось реалізація:
package com.github.codegymcommunity.jrtb.service;

import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupDiscussionInfo;
import com.github.codegymcommunity.jrtb.repository.GroupSubRepository;
import com.github.codegymcommunity.jrtb.repository.entity.GroupSub;
import com.github.codegymcommunity.jrtb.repository.entity.TelegramUser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.Optional;

@DisplayName("Unit-level testing for GroupSubService")
public class GroupSubServiceTest {

   private GroupSubService groupSubService;
   private GroupSubRepository groupSubRepository;
   private TelegramUser newUser;

   private final static String CHAT_ID = "1";

   @BeforeEach
   public void init() {
       TelegramUserService telegramUserService = Mockito.mock(TelegramUserService.class);
       groupSubRepository = Mockito.mock(GroupSubRepository.class);
       groupSubService = new GroupSubServiceImpl(groupSubRepository, telegramUserService);

       newUser = new TelegramUser();
       newUser.setActive(true);
       newUser.setChatId(CHAT_ID);

       Mockito.when(telegramUserService.findByChatId(CHAT_ID)).thenReturn(Optional.of(newUser));
   }

   @Test
   public void shouldProperlySaveGroup() {
       //given

       GroupDiscussionInfo groupDiscussionInfo = new GroupDiscussionInfo();
       groupDiscussionInfo.setId(1);
       groupDiscussionInfo.setTitle("g1");

       GroupSub expectedGroupSub = new GroupSub();
       expectedGroupSub.setId(groupDiscussionInfo.getId());
       expectedGroupSub.setTitle(groupDiscussionInfo.getTitle());
       expectedGroupSub.addUser(newUser);

       //when
       groupSubService.save(CHAT_ID, groupDiscussionInfo);

       //then
       Mockito.verify(groupSubRepository).save(expectedGroupSub);
   }

   @Test
   public void shouldProperlyAddUserToExistingGroup() {
       //given
       TelegramUser oldTelegramUser = new TelegramUser();
       oldTelegramUser.setChatId("2");
       oldTelegramUser.setActive(true);

       GroupDiscussionInfo groupDiscussionInfo = new GroupDiscussionInfo();
       groupDiscussionInfo.setId(1);
       groupDiscussionInfo.setTitle("g1");

       GroupSub groupFromDB = new GroupSub();
       groupFromDB.setId(groupDiscussionInfo.getId());
       groupFromDB.setTitle(groupDiscussionInfo.getTitle());
       groupFromDB.addUser(oldTelegramUser);

       Mockito.when(groupSubRepository.findById(groupDiscussionInfo.getId())).thenReturn(Optional.of(groupFromDB));

       GroupSub expectedGroupSub = new GroupSub();
       expectedGroupSub.setId(groupDiscussionInfo.getId());
       expectedGroupSub.setTitle(groupDiscussionInfo.getTitle());
       expectedGroupSub.addUser(oldTelegramUser);
       expectedGroupSub.addUser(newUser);

       //when
       groupSubService.save(CHAT_ID, groupDiscussionInfo);

       //then
       Mockito.verify(groupSubRepository).findById(groupDiscussionInfo.getId());
       Mockito.verify(groupSubRepository).save(expectedGroupSub);
   }

}
Додав тут ще метод init() з інструкцією BeforeEach. Таким чином, зазвичай створюють метод, який буде виконуватися перед запуском кожного тесту, і в нього можна винести загальну логіку для всіх тестів. У нашому випадку замокати TelegramUserService потрібно однаково для всіх тестів цього класу, тому розумно перенести цю логіку в загальний метод. Тут використовується дві конструкції з мокіто:
  • Mockito.when(o1.m1(a1)).thenReturn(o2) — в ній говоримо, що коли в об'єкті o1 буде викликаний метод m1 з аргументом a1 , метод поверне об'єкт o2 . Це чи не найголовніша функціональність мокито – змусити моковий об'єкт повертати саме те, що нам потрібно;

  • Mockito.verify(o1).m1(a1) - який перевіряє, що в об'єкті o1 був викликаний метод m1 з аргументом a1 . Можна було, звичайно, скористатися об'єктом методу save, що повертається, але я вирішив зробити трохи складніше, показавши ще один з можливих способів. Коли він може бути корисним? У випадках, коли методи мокових класів повертають void. Тоді без Mockito.verify справи не буде)))

Продовжуємо дотримуватись ідеї, що тести писати потрібно, і писати їх потрібно багато. Наступний етап – робота з командою телеграм-бота.

Створюємо команду /addGroupSub

Тут потрібно виконати наступну логіку: якщо нам приходить просто команда, без будь-якого контексту, ми допомагаємо користувачеві та передаємо йому список усіх груп з їхніми ID-шниками, щоб він зміг передати потрібну інформацію. А якщо користувач передає боту команду ще з якимось словом (словами) — знайти групу з таким ID або написати, що такої групи немає. Додамо нове значення в нашому єнамі - CommandName:
ADD_GROUP_SUB("/addgroupsub")
Рухаємося далі від БД до телеграм-бота – створюємо клас AddGroupSubCommand у пакеті command:
package com.github.codegymcommunity.jrtb.command;

import com.github.codegymcommunity.jrtb.codegymclient.CodeGymGroupClient;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupDiscussionInfo;
import com.github.codegymcommunity.jrtb.codegymclient.dto.GroupRequestArgs;
import com.github.codegymcommunity.jrtb.repository.entity.GroupSub;
import com.github.codegymcommunity.jrtb.service.GroupSubService;
import com.github.codegymcommunity.jrtb.service.SendBotMessageService;
import org.telegram.telegrambots.meta.api.objects.Update;

import java.util.stream.Collectors;

import static com.github.codegymcommunity.jrtb.command.CommandName.ADD_GROUP_SUB;
import static com.github.codegymcommunity.jrtb.command.CommandUtils.getChatId;
import static com.github.codegymcommunity.jrtb.command.CommandUtils.getMessage;
import static java.util.Objects.isNull;
import static org.apache.commons.lang3.StringUtils.SPACE;
import static org.apache.commons.lang3.StringUtils.isNumeric;

/**
* Add Group subscription {@link Command}.
*/
public class AddGroupSubCommand implements Command {

   private final SendBotMessageService sendBotMessageService;
   private final CodeGymGroupClient codeGymGroupClient;
   private final GroupSubService groupSubService;

   public AddGroupSubCommand(SendBotMessageService sendBotMessageService, CodeGymGroupClient codeGymGroupClient,
                             GroupSubService groupSubService) {
       this.sendBotMessageService = sendBotMessageService;
       this.codeGymGroupClient = codeGymGroupClient;
       this.groupSubService = groupSubService;
   }

   @Override
   public void execute(Update update) {
       if (getMessage(update).equalsIgnoreCase(ADD_GROUP_SUB.getCommandName())) {
           sendGroupIdList(getChatId(update));
           return;
       }
       String groupId = getMessage(update).split(SPACE)[1];
       String chatId = getChatId(update);
       if (isNumeric(groupId)) {
           GroupDiscussionInfo groupById = codeGymGroupClient.getGroupById(Integer.parseInt(groupId));
           if (isNull(groupById.getId())) {
               sendGroupNotFound(chatId, groupId);
           }
           GroupSub savedGroupSub = groupSubService.save(chatId, groupById);
           sendBotMessageService.sendMessage(chatId, "Подписал на группу " + savedGroupSub.getTitle());
       } else {
           sendGroupNotFound(chatId, groupId);
       }
   }

   private void sendGroupNotFound(String chatId, String groupId) {
       String groupNotFoundMessage = "Нет группы с ID = \"%s\"";
       sendBotMessageService.sendMessage(chatId, String.format(groupNotFoundMessage, groupId));
   }

   private void sendGroupIdList(String chatId) {
       String groupIds = codeGymGroupClient.getGroupList(GroupRequestArgs.builder().build()).stream()
               .map(group -> String.format("%s - %s \n", group.getTitle(), group.getId()))
               .collect(Collectors.joining());

       String message = "Щобы подписаться на группу - передай комадну вместе с ID группы. \n" +
               "Например: /addGroupSub 16. \n\n" +
               "я подготовил список всех групп - выберай якую хочешь :) \n\n" +
               "ім'я группы - ID группы \n\n" +
               "%s";

       sendBotMessageService.sendMessage(chatId, String.format(message, groupIds));
   }
}
У цьому класі використовується метод isNumeric з apache-commons бібліотеки, тому додамо її до нашого пам'ятника:
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>${apache.commons.version}</version>
</dependency>
І в блок properties:
<apache.commons.version>3.11</apache.commons.version>
Уся ця логіка перебуває у класі. Прочитайте її уважно. Будуть запитання/пропозиції – пишіть у коментарях. Після цього потрібно додати команду в CommandContainer до нашої карти команд:
.put(ADD_GROUP_SUB.getCommandName(), new AddGroupSubCommand(sendBotMessageService, codeGymGroupClient, groupSubService))
І все для цієї команди. Хочеться перевірити цю функціональність, але поки що реально можна подивитися тільки в БД. У третій частині я додам зміни з JRTB-6, щоб ми могли переглянути список груп, на які користувач підписав. Тепер добре б це все перевірити. Для цього виконаємо всі дії у Телеграмі та перевірити у базі даних. Так як у нас написані тести, все має бути чудово. Стаття вже й так вийшла чимала, тому тест для AddGroupSubCommand напишемо пізніше, а код додамо TODO, щоб не забути.

Висновки

У цій статті ми розглянули роботу додавання функціональності через всю програму, починаючи від бази даних і закінчуючи роботою з клієнтом, який користується ботом. Зазвичай такі завдання допомагають розібратися у проекті, зрозуміти його суть. Зрозуміти, як він влаштований. Зараз теми йдуть непрості, тож не соромтеся: пишіть свої запитання у коментарях, а я постараюся на них відповісти. Подобається проект? Ставте йому зірку на гітхабі : таким чином буде зрозуміло, що проектом цікавляться, і я задоволений. Як то кажуть, майстру завжди приємно, коли його роботу цінують. Код буде містити всі три частини STEP_6 і буде доступний раніше цієї статті. Як дізнатися про нього? Легко приєднатися до телеграм-каналу, де я публікую всю інформацію про свої статті про телеграм-бот. Дякую за прочитання! Частина 3 вже тут .

Список всіх матеріалів серії на початку цієї статті.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ