JavaRush /Блог /Java-проекты /Добавляем клиента к статьям - "Java-проект от А до Я"
Roman Beekeeper
35 уровень

Добавляем клиента к статьям - "Java-проект от А до Я"

Статья из группы Java-проекты
Всем привет, мои дорогие друзья. Шаг за шагом мы все ближе к нашей цели — к выходу в MVP нашего проекта — JavaRush Telegram Bot. Как я уже говорил в прошлой статье, осталось всего 5 задач. Сегодня мы покроем две из них. "Java-проект от А до Я": Добавляем клиента к статьям - 1Хочу повторить, что проект на этом не закончится. У меня есть еще тьма идей и видений того, как должен развиваться этот проект, что в нем можно добавить новое, что сделать лучше. Перед MVP сделаем еще отдельную статью на тему рефакторинга — то есть, об улучшении качества кода без изменения его функциональности. К тому моменту уже будет видно весь проект и будет понятно, что и где можно улучшить. В нашем случае мы будем максимально защищены от того, чтобы не сломать функционал, потому что написано много тестов. Также напишем ретроспективу на тему того, что мы хотели и что получили в итоге. Это очень полезная вещь: посмотрим насколько правильно виделся весь полгода назад. Мне по крайней мере очень это интересно. Если у кого-то будет желание попробовать себя в роли мануального тестировщика — пишите, посотрудничаем. Сделаем этот проект лучше вместе! Итак, вот они: две задачи, описанные еще полгода назад: JRTB-8 и JRTB-9. Начал я смотреть, что нужно реализовать по этим задачам, и понял, что в плане запуска команд уже все готово. Так бывает…) Вот, можно посмотреть в StartCommand, метод execute:

@Override
public void execute(Update update) {
   String chatId = update.getMessage().getChatId().toString();

   telegramUserService.findByChatId(chatId).ifPresentOrElse(
           user -> {
               user.setActive(true);
               telegramUserService.save(user);
           },
           () -> {
               TelegramUser telegramUser = new TelegramUser();
               telegramUser.setActive(true);
               telegramUser.setChatId(chatId);
               telegramUserService.save(telegramUser);
           });

   sendBotMessageService.sendMessage(chatId, START_MESSAGE);
}
Здесь работает логика, что если в нашей базе уже есть такой юзер по chatId, мы ему просто ставим поле active = true. А если нет такого пользователя — создаем нового. Так же и для команды /stop в StopCommand:

@Override
public void execute(Update update) {
   telegramUserService.findByChatId(update.getMessage().getChatId().toString())
           .ifPresent(it -> {
               it.setActive(false);
               telegramUserService.save(it);
           });
   sendBotMessageService.sendMessage(update.getMessage().getChatId().toString(), STOP_MESSAGE);
}
Видно, что при вызове этой команды для пользователя ставят только поле active = false. И все: его подписки будут жить и ждать своего часа, когда пользователь опять решит активировать чат с ботом. И казалось бы, задача уже сделана и ее можно закрыть. Но не тут-то было. Самая главная задача — создать оповещение о новых статьях в подписке. Вот как раз там и будет полностью обновлены и сделаны эти задачи. То есть, пока мы не реализовали оповещение о новых статьях, нельзя закрыть. Поэтому займемся задачей JRTB-4 — созданием проверки каждые 20 минут и оповещения о новых статьях. Друзья! Хотите сразу узнавать, когда выйдет новый код проекту? Когда выходит новая статья? Присоединяйтесь к моему тг-каналу. Там собираю свою статьи, свои мысли, свою open-source разработку воедино.

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

Что нам нужно сделать в рамках этой задачи:
  1. Создать джобу, которая будет периодически ходить во все группы, подписки на которые есть у нас БД, сортировать статьи по дате публикации и проверять, совпадает ли ID последней публикации со значением в GroupSub. Если не совпадает, значит, нужно понять, сколько именно статей вышло с последнего раза. Обновляем last_article_id в GroupSub7 до актуального состояния.

  2. Когда нашли список вышедших статей, находим всех АКТИВНЫХ пользователей для этих групп и пересылаем им уведомления о новых статьях.

Чтобы это сделать, воспользуемся такой вещью как Spring Scheduler. Это механизм в Spring Framework, с ним можно создавать задачи, которые будут выполняться в определенное время. То ли каждые 15-20-40 минут, то ли каждый четверг в 15:30 или какой-то другой вариант. Их еще называют калькой с английского — джоба. Пока мы будем делать эту задачу, я умышленно оставляю один дефект в поиске новых статей. Он достаточно редкий и проявился только в ситуации, когда я тестировал мануально работу этой задачи. Чтобы это сделать нужно написать клиент по поиску статей. Для этого воспользуемся уже знакомым нам Swagger API. Там есть post-controller. Нас интересует только поиск коллекции статей по определенным фильтрам:
/api/1.0/rest/posts Get posts by filters
Будем работать с этим запросом. Что нам в нем нужно? Получить список статей, которые относятся к определенной группе, при этом они должны быть отсортированы по дате публикации. Таким образом мы можем взять последние 15 статей и проверить, не вышли ли новые публикации судя по lastArticleId из нашей базы данных. Если таки есть, мы их передадим далее для обработки и отправки уже пользователю. Поэтому нам нужно написать JavaRushPostClient.

Пишем JavaRushPostClient

Здесь не будем стараться охватить все запросы, которые нам передали в API и создадим только тот, который нам нужен. Делая это, мы достигаем сразу две цели:
  1. Ускоряем процесс написания нашего приложения.

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

Поэтому начем. Для запроса по секции Models в Swagger UI создадим следующие DTO:"Java-проект от А до Я": Добавляем клиента к статьям - 2

BaseUserInfo:


package com.github.javarushcommunity.jrtb.javarushclient.dto;

import lombok.Data;

/**
* DTO, which represents base user information.
*/
@Data
public class BaseUserInfo {
   private String city;
   private String country;
   private String displayName;
   private Integer id;
   private String job;
   private String key;
   private Integer level;
   private String pictureUrl;
   private String position;
   private UserPublicStatus publicStatus;
   private String publicStatusMessage;
   private Integer rating;
   private Integer userId;
}

Language:


package com.github.javarushcommunity.jrtb.javarushclient.dto;

/**
* DTO, which represents languages.
*/
public enum Language {
   UNKNOWN,
   ENGLISH,
   GERMAN,
   SPANISH,
   HINDI,
   FRENCH,
   PORTUGUESE,
   POLISH,
   BENGALI,
   PUNJABI,
   CHINESE,
   ITALIAN,
   INDONESIAN,
   MARATHI,
   TAMIL,
   TELUGU,
   JAPANESE,
   KOREAN,
   URDU,
   TAIWANESE,
   NETHERLANDS,
   RUSSIAN,
   UKRAINIAN
}

LikesInfo:


package com.github.javarushcommunity.jrtb.javarushclient.dto;

/**
* DTO, which represents like's information.
*/
public class LikesInfo {

   private Integer count;
   private LikeStatus status;
}

LikeStatus:


package com.github.javarushcommunity.jrtb.javarushclient.dto;

/**
* DTO, which represents like's status.
*/
public enum LikeStatus {

   UNKNOWN,
   LIKE,
   HOT,
   FOLLOW,
   FAVORITE,
   SOLUTION,
   HELPFUL,
   ARTICLE,
   OSCAR,
   DISLIKE,
   WRONG,
   SPAM,
   ABUSE,
   FOUL,
   TROLLING,
   OFFTOPIC,
   DUPLICATE,
   DIRTY,
   OUTDATED,
   BORING,
   UNCLEAR,
   HARD,
   EASY,
   FAKE,
   SHAM,
   AWFUL
}

PostType:


package com.github.javarushcommunity.jrtb.javarushclient.dto;

/**
* DTO, which represents post types.
*/
public enum PostType {
   UNKNOWN, USUAL, INNER_LINK, OUTER_LINK
}

UserPublicStatus:


package com.github.javarushcommunity.jrtb.javarushclient.dto;

/**
* DTO, which represents user public status.
*/
public enum UserPublicStatus {
   UNKNOWN,
   BEGINNER,
   ACTIVE,
   STRONG,
   GRADUATED,
   INTERNSHIP_IN_PROGRESS,
   INTERNSHIP_COMPLETED,
   RESUME_COMPLETED,
   LOOKING_FOR_JOB,
   HAVE_JOB;
}

VisibilityStatus:
package com.github.javarushcommunity.jrtb.javarushclient.dto;

/**
* DTO, which represents visibility status.
*/
public enum VisibilityStatus {
   UNKNOWN,
   RESTRICTED,
   PUBLIC,
   PROTECTED,
   PRIVATE,
   DISABLED,
   DELETED
}
Основываясь на всех этих DTO, напишем основной класс для получения статей:

PostInfo:


package com.github.javarushcommunity.jrtb.javarushclient.dto;

import lombok.Data;

/**
* DTO, which represents post information.
*/
@Data
public class PostInfo {

   private BaseUserInfo authorInfo;
   private Integer commentsCount;
   private String content;
   private Long createdTime;
   private String description;
   private GroupInfo groupInfo;
   private Integer id;
   private String key;
   private Language language;
   private LikesInfo likesInfo;
   private GroupInfo originalGroupInfo;
   private String pictureUrl;
   private Double rating;
   private Integer ratingCount;
   private String title;
   private PostType type;
   private Long updatedTime;
   private UserDiscussionInfo userDiscussionInfo;
   private Integer views;
   private VisibilityStatus visibilityStatus;

}
Теперь создадим интерфейс для работы и его реализацию. Нам нужен будет только один метод для работы со статьями:

JavaRushPostClient:


package com.github.javarushcommunity.jrtb.javarushclient;

import com.github.javarushcommunity.jrtb.javarushclient.dto.PostInfo;

import java.util.List;

/**
* Client for Javarush Open API corresponds to Posts.
*/
public interface JavaRushPostClient {

   /**
    * Find new posts since lastPostId in provided group.
    *
    * @param groupId provided group ID.
    * @param lastPostId provided last post ID.
    * @return the collection of the new {@link PostInfo}.
    */
   List<PostInfo> findNewPosts(Integer groupId, Integer lastPostId);
}
findNewPosts принимает два аргумента: ID группы и последний ID статьи, которую бот уже отправлял. Поэтому передадутся все те статьи, которые вышли позже статьи с lastPostId. И его реализация:

package com.github.javarushcommunity.jrtb.javarushclient;

import com.github.javarushcommunity.jrtb.javarushclient.dto.PostInfo;
import kong.unirest.GenericType;
import kong.unirest.Unirest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
public class JavaRushPostClientImpl implements JavaRushPostClient {

   private final String javarushApiPostPath;

   public JavaRushPostClientImpl(@Value("${javarush.api.path}") String javarushApi) {
       this.javarushApiPostPath = javarushApi + "/posts";
   }

   @Override
   public List<PostInfo> findNewPosts(Integer groupId, Integer lastPostId) {
       List<PostInfo> lastPostsByGroup = Unirest.get(javarushApiPostPath)
               .queryString("order", "NEW")
               .queryString("groupKid", groupId)
               .queryString("limit", 15)
               .asObject(new GenericType<List<PostInfo>>() {
               }).getBody();
       List<PostInfo> newPosts = new ArrayList<>();
       for (PostInfo post : lastPostsByGroup) {
           if (lastPostId.equals(post.getId())) {
               return newPosts;
           }
           newPosts.add(post);
       }
       return newPosts;
   }
}
В запросе добавляем несколько фильтров:
  • order = NEW — чтобы в списке были вначале новые;
  • groupKid = groupId — поиск только по определенным группам;
  • limit = 15 — ограничиваем количество статей на запрос. У нас периодичность 15-20 минут и мы ожидаем, что за это время не будет написано БОЛЬШЕ, чем 15(!).
Далее, когда мы нашли статьи, пробегаем по списку и ищем новые. Алгоритм простой и наглядный. Если есть желание его улучшить — пишите). Напишем простой тест для этого клиента:

package com.github.javarushcommunity.jrtb.javarushclient;

import com.github.javarushcommunity.jrtb.javarushclient.dto.PostInfo;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.List;

import static com.github.javarushcommunity.jrtb.javarushclient.JavaRushGroupClientTest.JAVARUSH_API_PATH;

@DisplayName("Integration-level testing for JavaRushPostClient")
class JavaRushPostClientTest {

   private final JavaRushPostClient postClient = new JavaRushPostClientImpl(JAVARUSH_API_PATH);

   @Test
   public void shouldProperlyGetNew15Posts() {
       //when
       List<PostInfo> newPosts = postClient.findNewPosts(30, 2935);

       //then
       Assertions.assertEquals(15, newPosts.size());
   }
}
Это очень простой тест, который проверяет, есть ли вообще связь с клиентом или нет. Он находит 15 новых статей в группе Java-проекты, потому что я ему передаю ID первой в этой группе статьи, а их уже больше 15… Их уже 22! Я даже не думал, что их будет так много. Как я быстро это узнал? Думаете, пошел считать их? Не-а) Я воспользовался свагером и посмотрел количество статей по определенной группе. Кстати, так можно посмотреть и в других… А сколько всего статей в группе RANDOM?... сейчас скажу: их 1062! Серьезное количество.

Окончание первой части

Здесь мы добавили работу с клиентом по статьям. Все уже делали, в этот раз, я думаю, все должно пройти просто и быстро. В следующей статье будем добавлять Spring Scheduler и писать FindNewArticleService. Ну и как обычно лайк - подписка - колокольчик, ставь звезду нашему проекту, пиши комментарии и оценивай статью! Всем спасибо за прочтение — до встречи!

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

Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Anonymous #2856674 Уровень 18
28 апреля 2024
Не совсем понятно, зачем нужно создавать так много ДТОшек, если по факту нам нужен только id поста. Даже id группы из GroupInfo не особо нужен, т.к. мы накладываем фильтр по группе Сделал ДТОшку PostInfo, состоящую чисто из id и всё отлично Будет нужна доп инфа с javarush - добавлю новые поля