Привет! Сегодня будем добавлять подписку на группу статей в JavaRush. Этому соответствует issue JRTB-5 на GitHub.
Поясню: в JavaRush есть раздел Статьи, а в нем — Группы статей. Идея состоит в том, чтобы через телеграм-бота получать уведомления о новой статье из одной или нескольких групп.
Добавляем JRTB-5
Скажем, мне интересны статьи из группы Истории успеха. Поэтому я хочу подписаться на обновления этой группы и получать каждый раз ссылку на новую публикацию.
В рамках реализации этой задачи нам нужно научиться пользоваться открытым API для работы с группами в JavaRush. Как раз к этому моменту такая вещь подоспела. Вот ссылка на описание открытого API.
Друзья! Хотите сразу узнавать, когда выйдет новый код проекту или новая статья? Присоединяйтесь к моему тг-каналу. Там собираю свою статьи, мысли и open-source разработку воедино.
Что такое swagger? Сейчас разберемся
Про сваггер мы еще не говорили. Для тех, кто не знает, объясню вкратце: это место, где можно открыто посмотреть на API какого-то сервера и попробовать выполнить какие-то запросы к нему.
Обычно в сваггере сгруппированы возможные запросы. В нашем случае есть три группы: forum-question, group, post. В каждой группе будет один и более запросов с указанием всех необходимых данных, чтобы этот запрос построить (то есть, какие дополнительные параметры можно передать, что с ними делать, какой http метод, и так далее). Советую почитать и посмотреть больше по этой теме, потому это та часть разработки, с которой столкнется почти каждый из вас.
Чтобы разобраться, узнаем, какое количество групп есть в JavaRush. Для этого раскроем группу group-controller и выберем Get запрос /api/1.0/rest/groups/count. Он вернет нам количество групп в JavaRush.
Смотрим:На изображении видно, что этот запрос поддерживает несколько параметров (query, type, filter).
Чтобы попробовать этот запрос на вкус, нужно найти кнопку Try it out, после чего эти параметры можно будет настроить:Также там можно настроить и тип, и фильтр, и кверю (здесь вообще интересно: это будет поиск по тексту в группе). Но пока запустим без каких-либо ограничений и посмотрим, сколько всего групп в JavaRush. Для этого нажмем на Execute. Чуть ниже будет ответ (в секции Server Response) на этот запрос:Мы видим, что всего групп 30, запрос этот выполнился за 95ms и есть набор некоторых хедеров в ответе.
Далее попробуем настроить какие-то параметры. Выберем параметр type равным значению COMPANY и посмотрим, как изменится результат:Их 4. Как это проверить? Легко: можно пойти на сайт, найти секцию статьи, выбрать все группы и там добавить соответствующий фильтр (https://javarush.com/groups/all?type=COMPANY). И да, действительно, их всего 4. Хотя реально три :DПока что сходится. К слову, если мы проверим университеты, то их пока что нет.
Ради интереса посмотрите, что будет, если поставить filter = MY в браузере, где вы залогинены в Javarush и не залогинены.
Больше о сваггере — в этой статье на Хабре.
Пишем клиента к Javarush API для групп
Теперь на основе открытого API напишем Java клиента, который умеет делать запросы, получать ответы и знает, какие именно объекты будут приходить.
Объекты также возьмем из сваггера, из секции Models (в самом низу страницы).
Создадим новый пакет и назовем его javarushclient рядом с service, repository. В будущем мы это вынесем в отдельную библиотеку в рамках организации Javarush Community и будем использовать исключительно как зависимость.
Когда у нас появится зависимость, можно начинать добавлять код.
Создадим клиент для групп JavaRushGroupClient и реализацию в класс JavaRushGroupClientImpl.
Но сперва нужно создать DTO-шки (data transfer object) — то есть, классы, объекты которых будут носить все нужные для клиента данные.
Все модели можно посмотреть в сваггере, В самом низу есть секция Models, в которой можно их считать.
Вот как выглядит GroupDiscussionInfo в сваггере:В пакете javarushclient создадим пакет dto, в который добавим, на основе данных из сваггера, следующие классы:
MeGroupInfoStatus:
package com.github.javarushcommunity.jrtb.javarushclient.dto;
/**
* Member group status.
*/
public enum MeGroupInfoStatus {
UNKNOWN, CANDIDATE, INVITEE, MEMBER, EDITOR, MODERATOR, ADMINISTRATOR, BANNED
}
MeGroupInfo:
package com.github.javarushcommunity.jrtb.javarushclient.dto;
import lombok.Data;
/**
* Group information related to authorized user. If there is no user - will be null.
*/
@Data
public class MeGroupInfo {
private MeGroupInfoStatus status;
private Integer userGroupId;
}
GroupInfoType:
package com.github.javarushcommunity.jrtb.javarushclient.dto;
/**
* Group Info type;
*/
public enum GroupInfoType {
UNKNOWN, CITY, COMPANY, COLLEGE, TECH, SPECIAL, COUNTRY
}
UserDiscussionInfo:
package com.github.javarushcommunity.jrtb.javarushclient.dto;
import lombok.Data;
/**
* DTO for User discussion info.
*/
@Data
public class UserDiscussionInfo {
private Boolean isBookmarked;
private Integer lastTime;
private Integer newCommentsCount;
}
GroupVisibilitysdtatus:
package com.github.javarushcommunity.jrtb.javarushclient.dto;
/**
* Group Visibility status.
*/
public enum GroupVisibilityStatus {
UNKNOWN, RESTRICTED, PUBLIC, PROTECTED, PRIVATE, DISABLED, DELETED
}
Так как GroupInfo и GroupDiscussionInfo почти полностью совпадают, свяжем их в наследовании — GroupDiscusionInfo:
package com.github.javarushcommunity.jrtb.javarushclient.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* Group discussion info class.
*/
@EqualsAndHashCode(callSuper = true)
@Data
@ToString(callSuper = true)
public class GroupDiscussionInfo extends GroupInfo {
private UserDiscussionInfo userDiscussionInfo;
private Integer commentsCount;
}
Также нам понадобится фильтр для запроса GroupFilter:
package com.github.javarushcommunity.jrtb.javarushclient.dto;
/**
* Filters for group requests.
*/
public enum GroupFilter {
UNKNOWN, MY, ALL
}
В запросе на получение по ID-шнику выдает GroupDiscussionInfo, а в запросе на коллекцию групп можно получить как GroupInfo, так и GroupDiscussionInfo.
Так как у запросов могут быть type, query, filter, offset и limit, создадим отдельный класс GroupRequestArgs и сделаем его классом-билдером (почитайте, что такое паттерн builder):
package com.github.javarushcommunity.jrtb.javarushclient.dto;
import lombok.*;
import java.util.HashMap;
import java.util.Map;
import static java.util.Objects.nonNull;
/**
* Request arguments for group requests.
*/
@Builder
@Getter
public class GroupRequestArgs {
private final String query;
private final GroupInfoType type;
private final GroupFilter filter;
/**
* specified where to start getting groups
*/
private final Integer offset;
/**
* Limited number of groups.
*/
private final Integer limit;
public Map populateQueries() {
Map queries = new HashMap<>();
if(nonNull(query)) {
queries.put("query", query);
}
if(nonNull(type)) {
queries.put("type", type);
}
if(nonNull(filter)) {
queries.put("filter", filter);
}
if(nonNull(offset)) {
queries.put("offset", offset);
}
if(nonNull(limit)) {
queries.put("limit", limit);
}
return queries;
}
}
Для поиска количества групп он несколько отличается. В нем есть только query, type и filter. И казалось бы, не хочется дублировать код. Вместе с тем, если начать их объединять, получается некрасиво в работе с билдерами. Поэтому я решил их разделить и сделать повторение кода.
Вот как выглядит GroupCountRequestArgs:
package com.github.javarushcommunity.jrtb.javarushclient.dto;
import lombok.Builder;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
import static java.util.Objects.nonNull;
/**
* Request arguments for group count requests.
*/
@Builder
@Getter
public class GroupsCountRequestArgs {
private final String query;
private final GroupInfoType type;
private final GroupFilter filter;
public Map populateQueries() {
Map queries = new HashMap<>();
if (nonNull(query)) {
queries.put("query", query);
}
if (nonNull(type)) {
queries.put("type", type);
}
if (nonNull(filter)) {
queries.put("filter", filter);
}
return queries;
}
}
Да, я не упомянул, что в последних двух классах есть метод populateQueries, который подготовит мапу для создания запроса (увидите его далее).
Основываясь на описанных выше классах, создадим интерфейс для JavaRushGroupClient:
package com.github.javarushcommunity.jrtb.javarushclient;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupDiscussionInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupRequestArgs;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupsCountRequestArgs;
import java.util.List;
/**
* Client for Javarush Open API corresponds to Groups.
*/
public interface JavaRushGroupClient {
/**
* Get all the {@link GroupInfo} filtered by provided {@link GroupRequestArgs}.
*
* @param requestArgs provided {@link GroupRequestArgs}.
* @return the collection of the {@link GroupInfo} objects.
*/
List<GroupInfo> getGroupList(GroupRequestArgs requestArgs);
/**
* Get all the {@link GroupDiscussionInfo} filtered by provided {@link GroupRequestArgs}.
*
* @param requestArgs provided {@link GroupRequestArgs}
* @return the collection of the {@link GroupDiscussionInfo} objects.
*/
List<GroupDiscussionInfo> getGroupDiscussionList(GroupRequestArgs requestArgs);
/**
* Get count of groups filtered by provided {@link GroupRequestArgs}.
*
* @param countRequestArgs provided {@link GroupsCountRequestArgs}.
* @return the count of the groups.
*/
Integer getGroupCount(GroupsCountRequestArgs countRequestArgs);
/**
* Get {@link GroupDiscussionInfo} by provided ID.
*
* @param id provided ID.
* @return {@link GroupDiscussionInfo} object.
*/
GroupDiscussionInfo getGroupById(Integer id);
}
Два разных запроса на случай, когда мы хотим получить информацию GroupInfo или GroupDiscussionInfo добавил. В остальном эти запросы тождественны, и разница будет лишь в том, что в одном флаг includeDiscussion будет true, а в другом — false. Поэтому вышло 4 метода, а не три.
Теперь приступим к реализации:
Путь к АПИшке я добавляю в конструкторе, используя уже знакомую нам аннотацию Value. Она подразумевает, что значение внутри аннотации соответствует полю в файле с пропертями. Поэтому добавим в application.properties новую строку:
Теперь это значение будет находиться в одном месте для всех клиентов АПИ, и если путь к АПИ изменится, мы его быстро обновим.
Раньше я забивал гвозди микроскопом получал ответ из http-запроса через Unirest, переводил его в строку и потом эту строку парсил через Jackson… Это было страшно, муторно и требовало многих дополнительных вещей. В этой библиотеке можно посмотреть, как это выглядит. Как дойдут руки — отрефакторю все.
У кого будет желание попробовать обновить эту библиотеку — добавить получение объектов только при помощи инструментов библиотеки unirest — пишите в личку или как новое issue в самой библиотеке. Для вас это будет реальный опыт работы, а мне не жалко. Проведу полноценный code review и помогу, если нужно будет.
Теперь вопрос: а работает ли наш код так, как мы ожидаем? Ответить несложно: нужно просто написать тесты для них. Как я уже не раз говорил, разработчики должны уметь писать тесты.
Поэтому, пользуясь нашим Swagger UI, будем отправлять запросы, смотреть ответы и подставлять их в тесты как ожидаемый результат.
Вы могли сразу заметить, что количество групп не статическое и может меняться. И вы правы. Вопрос лишь в том, насколько часто это количество меняется? Очень редко, так что в разрезе нескольких месяцев можно утверждать, что это значение будет статично. А если что-то изменится, обновим тесты.
Встречайте — JavaRushGroupClientTest:
Тесты написаны в таком же стиле, как и до этого. Есть по нескольку тестов на каждый запрос. Тестировать все не имеет смысла, так как я думаю, что этот АПИ и так уже оттестирован лучшим образом.
Вывод
В рамках этой статьи мы добавили Java клиента для групп к JavaRush API. Как говорится, век живи — век учись. Пока писал этого клиента, воспользовался их документацией и с удобством использовал работу с объектами, которую они предоставляют.
Обращаю внимание на задачу, которую я предложил. Кому интересно — пишите личным сообщением, я более чем уверен, что это будет очень интересный опыт.
Это была первая часть. Во второй мы реализуем непосредственно команду по добавлению и (если уложимся в одну статью) добавим получение списка групп, на которых пользователь подписан.
Далее, у кого есть желание и талант к написанию текстов для бота, прошу написать мне в ЛС. Я не специалист в этом деле и любая помощь мне будет очень кстати. Оформим это все как разработку в open source, будет интересно!
Ну и как обычно — лайк, подписка, колокольчик, ставь звезду нашему проекту, пиши комментарии и оценивай статью!
Народ, имейте ввиду, что когда вы авторизированы на javarush, то количество возвращаемых групп будет отличаться от случая, когда вы не авторизированы
В моем случае в браузере было 64, а запрос возвращал 27 - это норм. Там левые группы на иностранных языках выводястся
Написал в техподдержку вопрос, почему так
Роман, все-таки интересно зачем было вводить класс GroupCountRequestArgs с дублированием кода, это описание:
Для поиска количества групп он несколько отличается. В нем есть только query, type и filter. И казалось бы, не хочется дублировать код. Вместе с тем, если начать их объединять, получается некрасиво в работе с билдерами. Поэтому я решил их разделить и сделать повторение кода.
Не совсем объясняет, что некрасивого получается при работе с билдерами.
В своем варианте сделал только 1 класс, все работает без проблем, и нет дублирование кода
Всем привет! У меня появилась непреодолимая проблема и не могу толково погуглить в чем причина(
Была предпосылка запустить приложение через VPN (proxy) не помогло, все случилось после второго "угона" токена от бота, сделал revoke (в первый раз помогло, бот вернулся под мой контроль)
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
ебатьсебе мозги и просто подписаться по rss ?