{"id":31999605,"url":"https://github.com/x-oc/link-tracker","last_synced_at":"2026-04-13T15:31:30.816Z","repository":{"id":313610237,"uuid":"1048016465","full_name":"x-oc/link-tracker","owner":"x-oc","description":"Отслеживатель ссылок, оповещающий подписчиков об обновлениях на страницах, которые они захотели отслеживать.","archived":false,"fork":false,"pushed_at":"2025-09-07T08:24:14.000Z","size":200,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-25T15:29:57.752Z","etag":null,"topics":["java","junit","kafka","liquibase","mockito","postgresql","redis","resilience4j","rest-api","spring","spring-boot","telegrambot","testcontainers"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/x-oc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-08-31T18:22:42.000Z","updated_at":"2025-09-07T08:24:17.000Z","dependencies_parsed_at":"2025-09-07T10:14:29.662Z","dependency_job_id":"34829d46-14d9-490b-ac2b-29662434bd5b","html_url":"https://github.com/x-oc/link-tracker","commit_stats":null,"previous_names":["x-oc/link-tracker"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/x-oc/link-tracker","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/x-oc%2Flink-tracker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/x-oc%2Flink-tracker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/x-oc%2Flink-tracker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/x-oc%2Flink-tracker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/x-oc","download_url":"https://codeload.github.com/x-oc/link-tracker/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/x-oc%2Flink-tracker/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31759237,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-13T15:25:13.801Z","status":"ssl_error","status_checked_at":"2026-04-13T15:25:09.162Z","response_time":93,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["java","junit","kafka","liquibase","mockito","postgresql","redis","resilience4j","rest-api","spring","spring-boot","telegrambot","testcontainers"],"created_at":"2025-10-15T14:32:57.070Z","updated_at":"2026-04-13T15:31:30.775Z","avatar_url":"https://github.com/x-oc.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Link Tracker\n\nПроект сделан в рамках курса Академия Бэкенда.\n\nПриложение для отслеживания обновлений контента по ссылкам. При появлении новых событий отправляется уведомление в Telegram.\n\nПроект написан на `Java 23` с использованием `Spring Boot 3`.\n\nДля работы требуется БД `PostgreSQL`. Присутствует опциональная зависимость на `Kafka` и `Redis`. Всё это поднимается через `Docker`.\n\nДля дополнительной справки: [HELP.md](./HELP.md)\n\nДля запуска приложения бота, ему требуется передать в качестве переменной окружения токен телеграм бота.\nДля запуска скраппера, ему требуется передать в качестве переменных окружения токены и ключи доступа к GitHub и StackOverflow API (опционально).\n\n## Постановка проблемы\nКак пользователю, мне приходится посещать множество веб-сайтов: GitHub, StackOverflow, Reddit, HackerNews, YouTube и т.п.\nУ каждого из сайтов реализован свой механизм оповещения об обновлениях \n(если реализован вообще): почта, мобильные push-уведомления, popup-баннеры и т.п.\nЧтобы получить все уведомления, мне нужно зайти, в лучшем случае, на все веб-страницы и проверить список оповещений. \nВ худшем сценарии мне нужно пользоваться механизмом закладок и делать проверку обновлений самостоятельно с какой-то периодичностью.\nЯ, как пользователь, хочу иметь единый способ отслеживания обновлений для множества ресурсов, которые мне интересны.\n\n## Сценарий использования\nУправление подписками на ссылки осуществляется через чат с ботом в Telegram. \nПример сценария использования включает в себя:\n1. **Регистрация:** Пользователь инициирует взаимодействие с ботом и проходит процедуру регистрации.\n2. **Добавление/удаление ссылок:** Пользователь может ввести команду, чтобы добавить или удалить ссылку для отслеживания\n    - Команда: `/track https://stackoverflow.com/questions/292357` — для подписки на обновления указанного вопроса.\n    - Команда: `/untrack https://stackoverflow.com/questions/292357` — для отмены подписки.\n3. **Получение уведомлений:** При появлении новых ответов или обновлений по отслеживаемым ссылкам, бот автоматически отправляет уведомление в чат\n    - Уведомление: \"В вопросе https://stackoverflow.com/questions/292357 появился новый ответ: \u003cтекст ответа\u003e.\"\nКроме того, бот поддерживает команды для отображения списка всех текущих подписок и управления ими.\n\n## Техническое описание\nПроект состоит из 2-x приложений:\n1. **Сервис обработки сообщений пользователей (Bot):** отвечает за регистрацию пользователей и управление их подписками.\nЭтот сервис обеспечивает возможность добавления и удаления ссылок для отслеживания, а также сохраняет информацию о подписках в `PostgreSQL` через scrapper.\n2. **Сервис мониторинга ссылок (Scrapper):** осуществляет периодическую проверку заданных пользователями ссылок на наличие изменений.\nПри обнаружении обновлений данный сервис отправляет уведомления пользователям в первый сервис по `HTTP` и `Apache Kafka`.\n\nКроме базового функционала реализована следующая функциональность:\n- **Возможность тэгирования ссылок:** создание тэгов по темам или проектам позволит удобно управлять несколькими подписками одновременно.\nНапример, можно создать тэг \"Работа\", в который войдут ссылки на профессиональные ресурсы, и тэг \"Хобби\" для развлечений.\n\nУпрощенная схема работы системы выглядит следующим образом:\n```\n                                +-----------------+\n+-------------------+           |                 |\n|                   |      /-----    Postgres     |\n|    Scrapper       |------     |                 |\n|                   |--         +-----------------+\n+-------------------+  \\---\n               |           \\--- +-----------------+\n               |               \\-                 |\n               | HTTP           |     Kafka       |\n               |               /-                 |\n               |           /--- +-----------------+\n+-------------------+  /---\n|                   |--\n|       Bot         |           +-----------------+\n|                   |------------                 |\n+-------------------+           |  Telegram API   |\n                                |                 |\n                                +-----------------+\n```\n\nРазработка проекта велась поэтапно. \n\n## Первый этап\nЗадача - разработать сетевую часть приложения и основную логику бота.\nБыл написан скелета бота, добавлены сетевые вызовы согласно OpenAPI-контракту, реализованы HTTP-клиенты (для внешних и внутренних вызовов) и планировщик задач (проверятель ссылок).\nФактически получился работающий MVP: работала отправка сообщений в бота и была простая реакция на сообщения пользователей.\n\n### Функциональные требования\n- Бот должен поддерживать следующие команды:\n    - /start - регистрация пользователя.\n    - /help - вывод списка доступных команд.\n    - /track - начать отслеживание ссылки.\n    - /untrack - прекратить отслеживание ссылки.\n    - /list - показать список отслеживаемых ссылок (cписок ссылок, полученных при /track)\n- Команда /list должна выводить специальное сообщение, если список отслеживаемых ссылок пуст.\n- Неизвестные команды должны игнорироваться с уведомлением пользователю.\n- При попытке добавить ссылку, которая уже отслеживается, выводится сообщение: \"Ссылка уже отслеживается\".\n- Все endpoint'ы соответствуют OpenAPI-контракту: https://gist.github.com/sanyarnd/e35dc3d4e0c8000205ec5029dac38f5a.\n- Реализован базовый планировщик (`@Scheduled`): бот присылает простейшее уведомление (заглушку) в случае обнаружения изменений.\n- Общение между приложениями bot и scrapper происходит по HTTP.\n- Автоматическая реализация меню команд бота (см API), т.е. бот должен регистировать доступные команды через метод setmycommands при запуске.\n\n### Нефункциональные требования\n- Токен авторизации должен храниться в конфигурационном файле, недоступном для общего доступа.\n- Поддерживается скреппинг вопросов **StackOverflow** и репозиториев **GitHub** - написаны HTTP-клиенты для получения необходимой информации из API.\n    - Для установления, что появились обновления, достаточно использовать факт изменения поля, отвечающего за дату последнего изменения (lastUpdated и т.п.).\n    - Работа ведётся с API: [GitHub](https://docs.github.com/en/rest), [StackOverflow](https://api.stackexchange.com/), - а не с HTML.\n- Клиенты написаны руками, без использования готовых SDK для доступа к API для HTTP-клиентов (GitHub, StackOverflow):\n- Используются декларативные клиенты.\n- Бот реализует концепт машины состояний (создание /track идёт в формате диалога, а не команды):\n    - \\\u003e /track https://foo.bar/baz\n    - \u003c введите тэги (опционально)\n    - \\\u003e work hobby\n    - \u003c настройте фильтры (опционально)\n    - \\\u003e user:dummy type:comment\n- В тестах не происходят реальные вызовы к API внешних систем, используются заглушки (mocks).\n- Используется типобезопасная конфигурация (`@ConfigurationProperties`).\n- Используется структурное логирование (добавление key-value значений к логу, вместо зашивания данных в поле message) в коде приложения.\n- При создании клиента есть возможность указать базовый URL. При этом если он не указывается, используется URL по умолчанию.\n- Итоговый код является тестируемым; используется принцип Arrange-Act-Assert.\n- Используется однотипность функций разбора входного сообщения и диспатчеризации по типам команд так, чтобы команды можно было легко добавлять и удалять.\n\n### Тесты\n- Корректный парсинг ссылок\n- Правильное сохранение данных в репозиторий из запроса пользователя: ссылка, тэги, фильтры\n- Бот кидает ошибку, если команда неизвестна\n- Happy path: добавление и удаление ссылок из репозитория\n- Добавление дубля ссылки\n- Планировщик: обновление отправляется только пользователям, которые следят за ссылкой\n- Корректная обработка ошибок HTTP-запросов (некорректное тело, код) в клиентах\n- Проверка форматирования команды /list\n\n## Второй этап\nЗадача - написать схему данных, адаптировать код к ней, а также написать два провайдера для работы с БД: на чистом SQL и при помощи ORM.\n\n### Функциональные требования\n- Scrapper (планировщик) при отправке сообщения с изменениями включает детализацию данных:\n    - Для **StackOverflow** новый ответ или комментарий, сообщение включает:\n        - текст темы вопроса\n        - имя пользователя\n        - время создания\n        - превью ответа или комментария (первые 200 символов)\n    - Для **GitHub** новый PR или Issue, сообщение включает:\n        - название PR или Issue\n        - имя пользователя\n        - время создания\n        - превью описания (первые 200 символов)\n- Возможность тэгирования ссылок: создание тэгов по темам или проектам позволит группировать подписки. Например, можно создать тэг \"Работа\", в который войдут ссылки на профессиональные ресурсы, и тэг \"Хобби\" для развлечений. Соответственно должны появиться операции управления тэгами.\n\n### Нефункциональные требования\n- При проверке обновлений запрещено загружать все ссылки в память сразу.\n- Логика планировщика (проверка ссылок) и отправки (уведомления) разнесены по разным сервисам.\n    - Объявлен интерфейс для сервиса отправки уведомлений scrapper -\u003e bot.\n    - Пока что будет одна реализация - `HTTP` (далее появится `Kafka`).\n- Для хранения данных используется Postgres\n- Запуск `Postgres` осуществляется:\n    - через compose-файл для локальной разработки;\n    - через `Testcontainers` для автоматического тестирования.\n- Миграции написаны на языке `SQL`.\n- Для миграций используется `Liquibase`.\n- Схема БД находится в каталоге migrations/ в корне проекта.\n- Запуск миграций выполняется собственноручно написанной функцией.\n- Реализованы 2 способа работы с БД: \"голый\" SQL и ORM.\n- Выбор способа работы с БД осуществляется через конфигурацию: access-type=SQL или access-type=ORM.\n- Используются индексы на полях, которые используются в `WHERE` условиях.\n- Используется пагинация для обработки данных из таблиц, чтобы не перегружать память.\n- Данные загружаются пакетами 50..500, размер пакета вынесен в конфиг.\n- Настроен интервал задержки между проверками обновлений.\n- Условие фильтрации для поиска новых обновлений выполняется непосредственно в SQL-запросах, чтобы минимизировать объем передаваемых данных.\n- Бизнес-логика отделена от кода доступа к данным.\n- В интерфейсах нет типов, специфичных для определенной имплементации, например, в LinkService не должно быть RowSet, но он может быть в SqlLinkService.\n- Для выбора реализации используется `@ConditionalOnProperty`.\n- Для работы с БД используется: JDBC + JPA (Hibernate).\n\n### Тесты\n- Тесты должны запускать БД в `Testcontainers`.\n- Запросы к БД работают ожидаемым образом: вставка, удаление, обновление.\n- В зависимости от конфигурации работа идёт через разные имплементации (SQL, ORM).\n- Тесты на превью пользовательских сообщений в зависимости от типа обновления.\n\n## Третий этап\nДо текущего момента сервисы bot и scrapper общались строго синхронно по протоколу `HTTP`.\nНо что если в какой-то момент bot станет недоступен и планировщик не сможет отправить уведомление?\nДля таких ситуаций часто предпочтительным оказывается асинхронный способ общения.\nДополнительно, чтобы снизить нагрузку на сервис, будем кэшировать некоторые типы запросов в `Redis`.\n\n### Функциональные требования\n- Приложения bot и scrapper могут общаться асинхронно при помощи `Apache Kafka`\n- Используются compose и `Testcontainers`\n- Некорректные (не парсится и т.п.) сообщения отправляются в отдельную очередь dead letter queue\n- Все имена топиков задаются в конфигурационном файле\n- bot кэширует запросы типа `/list` в `Redis`\n\n### Нефункциональные требования\n- Выбор транспорта происходит в конфигурационном файле при помощи свойства `app.message-transport: {Kafka, HTTP}`\n- Отправка уведомлений реализована как абстракция (сервис): переиспользуется текущий код, в зависимости от значения в конфигурации сервис должен отправлять сообщения или в очередь, или по `HTTP`\n- Уведомления в `Kafka` передаются в формате JSON\n- bot делает инвалидацию кэша, если пользователь сделал изменение в списке своих отслеживаемых ссылок (добавил, удалил, изменил)\n\n### Тесты\nТесты должны запускать `Redis`/`Kafka` в `Testcontainers`\n- Валидное сообщение вычитывается и обрабатывается\n- Невалидные сообщения перенаправляются в DLQ: ошибки парсинга, валидации\n- Кэш подключен: он перехватывает обращения на чтение и инвалидируется при изменении данных\n- Валидный JSON корректно мапится в DTO\n- Для ручного тестирования `Kafka` используется `KafkaUI`\n\n## Четвёртый этап\nВ рамках этого этапа не будет добавлено новых функций для пользователя, а будет повышена отказоустойчивость системы \nс помощью реализации шаблонов Rate Limiting, Timeout, Retry, Circuit Breaker и Fallback.\n\n### Функциональные требования\n- Все HTTP-запросы поддерживают Timeout\n- Все HTTP-запросы поддерживают Retry\n    - сконфигурирована поддержка Retry в HTTP-клиентах\n- У каждого публичного endpoint'а есть выставленный Rate Limiting на основе IP-адреса клиента\n- В случае недоступности сервиса продолжительное время вместо Retry соединение разрывается при помощи Circuit Breaker\n- В случае отказа `HTTP` или `Kafka` при отправке уведомлений происходит fallback на альтернативный транспорт\n\n### Нефункциональные требования\n- Параметры Timeout настраиваются в конфигурации\n- Параметры Rate Limiting настраиваются в конфигурации\n- Политика Retry задаётся в конфигурации и позволяет задать количество повторов и время между попытками (constant backoff)\n- Retry происходит только в случае если это имеет смысл: в конфигурации задан настраиваемый список кодов, на которые происходит retry\n- Параметры Circuit Breaker настраиваются в конфигурации\n- Circuit Breaker настроен в режиме скользящего окна, например:\n```\nslidingWindowSize = 1\nminimumRequiredCalls = 1\nfailureRateThreshold = 100\npermittedCallsInHalfOpenState = 1\nwaitDurationInOpenState = \"1s\"\n```\n\n### Тесты\n- Тест на ретраи (см. `Stateful Behaviour у Wiremock`)\n    - в т.ч. проверка, что ретраи инициируются только для определенных кодов ошибок\n- Интеграционный тест на rate limiting\n- Тест на Circuit Breaker: ошибка должна возвращаться до того, как превышен таймаут\n- Тест на fallback `HTTP` и `Kafka`\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fx-oc%2Flink-tracker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fx-oc%2Flink-tracker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fx-oc%2Flink-tracker/lists"}