JavaRush /Java блог /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. Ну и как обычно лайк - подписка - колокольчик, ставь звезду нашему проекту, пиши комментарии и оценивай статью! Всем спасибо за прочтение — до встречи!

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

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ