diff --git a/.gitignore b/.gitignore
index 810ebf3..862eb15 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,5 +22,5 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
-# IJ IDEA #
+# idea
.idea
\ No newline at end of file
diff --git a/FP/bot/pom.xml b/FP/bot/pom.xml
new file mode 100644
index 0000000..60f49bd
--- /dev/null
+++ b/FP/bot/pom.xml
@@ -0,0 +1,88 @@
+
+
+
+ org.example
+ FP
+ 1.0
+
+
+ 4.0.0
+
+ bot
+
+
+ 3.0.1
+ 2.7.6
+
+
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.0.2
+
+
+ org.apache.commons
+ commons-lang3
+ 3.10
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+ ${starter-validation.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ ${starter-web.version}
+
+
+ org.apache.logging.log4j
+ log4j-to-slf4j
+
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+ org.springframework
+ spring-context-indexer
+ true
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ com.github.pengrad
+ java-telegram-bot-api
+ 6.6.0
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.22.2
+
+
+
\ No newline at end of file
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/BotApplication.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/BotApplication.java
new file mode 100644
index 0000000..312857e
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/BotApplication.java
@@ -0,0 +1,21 @@
+package ru.tinkoff.edu.java.bot;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import ru.tinkoff.edu.java.bot.configuration.ApplicationConfig;
+import ru.tinkoff.edu.java.bot.firstBot.BotMain;
+
+
+@SpringBootApplication
+@EnableConfigurationProperties(ApplicationConfig.class)
+public class BotApplication
+{
+ public static void main(String[] args){
+ var ctx = SpringApplication.run(BotApplication.class, args);
+ ApplicationConfig config = ctx.getBean(ApplicationConfig.class);
+
+ BotMain bot = new BotMain(config.bot().token());
+ bot.start();
+ }
+}
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/api/BotController.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/api/BotController.java
new file mode 100644
index 0000000..25186cf
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/api/BotController.java
@@ -0,0 +1,14 @@
+package ru.tinkoff.edu.java.bot.api;
+
+import org.springframework.web.bind.annotation.*;
+import ru.tinkoff.edu.java.bot.api.model.LinkUpdate;
+
+@RestController
+@RequestMapping("/update")
+public class BotController {
+
+ @PostMapping
+ public String updateChat(@RequestBody LinkUpdate update) {
+ return update.toString();
+ }
+}
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/api/exceptionHandler/BotExceptionHandler.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/api/exceptionHandler/BotExceptionHandler.java
new file mode 100644
index 0000000..732a56e
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/api/exceptionHandler/BotExceptionHandler.java
@@ -0,0 +1,36 @@
+package ru.tinkoff.edu.java.bot.api.exceptionHandler;
+
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import ru.tinkoff.edu.java.bot.api.model.ApiErrorResponse;
+
+@RestControllerAdvice
+public class BotExceptionHandler {
+
+ private String getDescription(String message) {
+ ApiErrorResponse errorObj = new ApiErrorResponse(
+ message,
+ null,
+ null,
+ null,
+ null
+ );
+ return errorObj.description();
+ }
+
+ @ExceptionHandler(HttpMessageNotReadableException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public String MessageNotReadable(HttpMessageNotReadableException Exception) {
+ return getDescription("Некорректные значения параметров или их нет!");
+ }
+
+ @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public String MethodNotSupported(HttpRequestMethodNotSupportedException Exception) {
+ return getDescription("Метод не разрешен!");
+ }
+}
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/api/model/ApiErrorResponse.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/api/model/ApiErrorResponse.java
new file mode 100644
index 0000000..f43001e
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/api/model/ApiErrorResponse.java
@@ -0,0 +1,11 @@
+package ru.tinkoff.edu.java.bot.api.model;
+
+import java.util.List;
+
+public record ApiErrorResponse(
+ String description,
+ String code,
+ String exceptionName,
+ String exceptionMessage,
+ List stacktrace
+) {}
\ No newline at end of file
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/api/model/LinkUpdate.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/api/model/LinkUpdate.java
new file mode 100644
index 0000000..0f44607
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/api/model/LinkUpdate.java
@@ -0,0 +1,6 @@
+package ru.tinkoff.edu.java.bot.api.model;
+
+import java.util.List;
+
+public record LinkUpdate(long id, String url, String description, List tgChatIds) {
+}
\ No newline at end of file
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/configuration/ApplicationConfig.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/configuration/ApplicationConfig.java
new file mode 100644
index 0000000..49ce5e3
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/configuration/ApplicationConfig.java
@@ -0,0 +1,18 @@
+package ru.tinkoff.edu.java.bot.configuration;
+
+import jakarta.validation.constraints.NotNull;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.validation.annotation.Validated;
+import ru.tinkoff.edu.java.bot.configuration.configRecords.Bot;
+import ru.tinkoff.edu.java.bot.configuration.configRecords.Scheduler;
+
+@Validated
+@EnableScheduling
+@ConfigurationProperties(prefix = "app", ignoreUnknownFields = false)
+public record ApplicationConfig(
+ @NotNull String test,
+ @NotNull Scheduler scheduler,
+ Bot bot
+) {
+}
\ No newline at end of file
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/configuration/configRecords/Bot.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/configuration/configRecords/Bot.java
new file mode 100644
index 0000000..37f4842
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/configuration/configRecords/Bot.java
@@ -0,0 +1,4 @@
+package ru.tinkoff.edu.java.bot.configuration.configRecords;
+
+public record Bot(String token, String name) {
+}
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/configuration/configRecords/Scheduler.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/configuration/configRecords/Scheduler.java
new file mode 100644
index 0000000..08278f6
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/configuration/configRecords/Scheduler.java
@@ -0,0 +1,5 @@
+package ru.tinkoff.edu.java.bot.configuration.configRecords;
+
+import java.time.Duration;
+
+public record Scheduler(Duration interval) {}
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/BotMain.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/BotMain.java
new file mode 100644
index 0000000..f758472
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/BotMain.java
@@ -0,0 +1,36 @@
+package ru.tinkoff.edu.java.bot.firstBot;
+
+import com.pengrad.telegrambot.TelegramBot;
+import com.pengrad.telegrambot.request.SendMessage;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import ru.tinkoff.edu.java.bot.configuration.ApplicationConfig;
+
+
+@EnableConfigurationProperties(ApplicationConfig.class)
+public class BotMain {
+
+ String token;
+ static TelegramBot bot;
+
+ public BotMain(String token) {
+ this.token = token;
+ }
+
+ public void start() {
+ bot = new TelegramBot(token);
+ bot.setUpdatesListener(new Updater(bot));
+ }
+
+ public void end() {
+ bot.removeGetUpdatesListener();
+ }
+
+ public static void apiCommand(long tgChatId, String command) {
+
+ MessageHandler handler = new MessageHandler();
+ String[] parse = command.split(" ");
+ if(parse.length > 1) command = handler.call_command(parse[0], parse[1]);
+ else command = handler.call_command(parse[0]);
+ bot.execute(new SendMessage(tgChatId, command));
+ }
+}
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/DB.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/DB.java
new file mode 100644
index 0000000..486a886
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/DB.java
@@ -0,0 +1,42 @@
+package ru.tinkoff.edu.java.bot.firstBot;
+
+import java.util.ArrayList;
+
+public class DB {
+
+ static ArrayList list = new ArrayList();
+
+ public static void addLink(String link) {
+ list.add(link);
+ }
+
+ public static void rmLink(String link) {
+ list.remove(link);
+ }
+
+ public static String getListParse() {
+
+ int i = 1;
+
+ String out_list = "";
+
+ for(String element: list){
+
+ out_list += element;
+
+ if(i != list.size()) {
+ out_list += ", ";
+ }
+ i++;
+ }
+ return out_list;
+ }
+
+ public static boolean listIsEmpty() {
+ return list.isEmpty();
+ }
+
+ public static boolean linkContain(String link) {
+ return list.contains(link);
+ }
+}
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/MessageHandler.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/MessageHandler.java
new file mode 100644
index 0000000..f003c84
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/MessageHandler.java
@@ -0,0 +1,30 @@
+package ru.tinkoff.edu.java.bot.firstBot;
+
+import ru.tinkoff.edu.java.bot.firstBot.commands.All;
+
+public class MessageHandler extends All {
+
+ public boolean is_command(String message) {
+ return message.startsWith("/");
+ }
+
+ public String call_command(String command, String arg) {
+ return switch (command) {
+ case "/start" -> start();
+ case "/help" -> help();
+ case "/track" -> track(arg);
+ case "/list" -> list();
+ case "/untrack" -> untrack(arg);
+ default -> unknow();
+ };
+ }
+
+ public String call_command(String command) {
+ return switch (command) {
+ case "/start" -> start();
+ case "/help" -> help();
+ case "/list" -> list();
+ default -> unknow();
+ };
+ }
+}
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/Updater.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/Updater.java
new file mode 100644
index 0000000..a01e4d9
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/Updater.java
@@ -0,0 +1,39 @@
+package ru.tinkoff.edu.java.bot.firstBot;
+
+import com.pengrad.telegrambot.TelegramBot;
+import com.pengrad.telegrambot.UpdatesListener;
+import com.pengrad.telegrambot.model.Update;
+import com.pengrad.telegrambot.model.request.*;
+import com.pengrad.telegrambot.request.SendMessage;
+
+import java.util.List;
+
+public class Updater implements UpdatesListener {
+
+ MessageHandler handler = new MessageHandler();
+ String command;
+ TelegramBot bot;
+
+ public Updater(TelegramBot bot) {
+ this.bot = bot;
+ }
+
+ @Override
+ public int process(List updates) {
+ Update update = updates.get(0);
+ if(handler.is_command(update.message().text())) {
+ String[] parse = update.message().text().split(" ");
+ if(parse.length > 1) command = handler.call_command(parse[0], parse[1]);
+ else command = handler.call_command(parse[0]);
+ bot.execute(
+ new SendMessage(update.message().chat().id(), command)
+ .replyMarkup(new ReplyKeyboardMarkup(new String[][]{
+ {"/start", "/help"},
+ {"/track testlink", "/untrack testlink", "/list"}
+ }).resizeKeyboard(true)
+ ).parseMode(ParseMode.HTML)
+ );
+ }
+ return UpdatesListener.CONFIRMED_UPDATES_ALL;
+ }
+}
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/All.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/All.java
new file mode 100644
index 0000000..991b177
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/All.java
@@ -0,0 +1,7 @@
+package ru.tinkoff.edu.java.bot.firstBot.commands;
+
+public class All implements List, Start, Track, Untrack, Help {
+ protected String unknow() {
+ return "Неизвестная команда";
+ }
+}
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/Help.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/Help.java
new file mode 100644
index 0000000..0761e05
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/Help.java
@@ -0,0 +1,11 @@
+package ru.tinkoff.edu.java.bot.firstBot.commands;
+
+public interface Help {
+ default String help() {
+ return "/start -- зарегистрировать пользователя\n" +
+ "/help -- вывести окно с командами\n" +
+ "/track [link] -- начать отслеживание ссылки\n" +
+ "/untrack [link] -- прекратить отслеживание ссылки\n" +
+ "/list -- показать список отслеживаемых ссылок";
+ }
+}
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/List.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/List.java
new file mode 100644
index 0000000..8673887
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/List.java
@@ -0,0 +1,10 @@
+package ru.tinkoff.edu.java.bot.firstBot.commands;
+
+import ru.tinkoff.edu.java.bot.firstBot.DB;
+
+public interface List {
+ default String list() {
+ if(DB.listIsEmpty()) return "list пустой";
+ return "Отслеживаемые ссылки: " + DB.getListParse() + "";
+ }
+}
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/Start.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/Start.java
new file mode 100644
index 0000000..d58c89c
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/Start.java
@@ -0,0 +1,9 @@
+package ru.tinkoff.edu.java.bot.firstBot.commands;
+
+public interface Start {
+
+ default String start() {
+ return "Бот начал работу";
+ }
+}
+
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/Track.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/Track.java
new file mode 100644
index 0000000..0f4669e
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/Track.java
@@ -0,0 +1,13 @@
+package ru.tinkoff.edu.java.bot.firstBot.commands;
+
+import ru.tinkoff.edu.java.bot.firstBot.DB;
+
+public interface Track {
+ default String track(String link) {
+ link = link.trim();
+ if(link.equals("")) return "ведите ссылку";
+ if(DB.linkContain(link)) return "Ссылка уже есть";
+ DB.addLink(link);
+ return "ссылка " + link + " добавлена";
+ }
+}
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/Untrack.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/Untrack.java
new file mode 100644
index 0000000..6550daf
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/firstBot/commands/Untrack.java
@@ -0,0 +1,14 @@
+package ru.tinkoff.edu.java.bot.firstBot.commands;
+
+import ru.tinkoff.edu.java.bot.firstBot.DB;
+
+public interface Untrack {
+ default String untrack(String link) {
+ if(link.equals("")) return "ведите ссылку";
+ if(DB.linkContain(link)) {
+ DB.rmLink(link);
+ return "ссылка " + link + " удалена";
+ }
+ return "ссылки " + link + " нет в пуле";
+ }
+}
diff --git a/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/schedule/LinkUpdaterScheduler.java b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/schedule/LinkUpdaterScheduler.java
new file mode 100644
index 0000000..1b2ff0f
--- /dev/null
+++ b/FP/bot/src/main/java/ru/tinkoff/edu/java/bot/schedule/LinkUpdaterScheduler.java
@@ -0,0 +1,17 @@
+package ru.tinkoff.edu.java.bot.schedule;
+
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+@Service
+public class LinkUpdaterScheduler {
+ private final static Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
+
+ @Scheduled(fixedDelayString = "${app.scheduler.interval}")
+ public void update() {
+ LOGGER.log(Level.INFO, "Info bot called");
+ }
+}
diff --git a/FP/bot/src/main/resources/application.properties b/FP/bot/src/main/resources/application.properties
new file mode 100644
index 0000000..a54b385
--- /dev/null
+++ b/FP/bot/src/main/resources/application.properties
@@ -0,0 +1,5 @@
+app.test=123
+springdoc.swagger-ui.path=/swagger-ui
+app.scheduler.interval=50000
+app.bot.token=5805337447:AAGnmh2isW2115L7tJWFojbpmSjNrarTvxQ
+app.bot.name=@SFRETbot
diff --git a/FP/bot/src/main/resources/botapi.yml b/FP/bot/src/main/resources/botapi.yml
new file mode 100644
index 0000000..489441e
--- /dev/null
+++ b/FP/bot/src/main/resources/botapi.yml
@@ -0,0 +1,59 @@
+openapi: 3.0.1
+info:
+ title: Bot API
+ version: 1.0.0
+ contact:
+ name: Alexander Biryukov
+ url: https://github.com
+paths:
+ /updates:
+ post:
+ summary: Отправить обновление
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/LinkUpdate'
+ required: true
+ responses:
+ '200':
+ description: Обновление обработано
+ '400':
+ description: Некорректные параметры запроса
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ApiErrorResponse'
+components:
+ schemas:
+ ApiErrorResponse:
+ type: object
+ properties:
+ description:
+ type: string
+ code:
+ type: string
+ exceptionName:
+ type: string
+ exceptionMessage:
+ type: string
+ stacktrace:
+ type: array
+ items:
+ type: string
+ LinkUpdate:
+ type: object
+ properties:
+ id:
+ type: integer
+ format: int64
+ url:
+ type: string
+ format: uri
+ description:
+ type: string
+ tgChatIds:
+ type: array
+ items:
+ type: integer
+ format: int64
diff --git a/FP/bot/src/test/java/BotTest.java b/FP/bot/src/test/java/BotTest.java
new file mode 100644
index 0000000..1548bc9
--- /dev/null
+++ b/FP/bot/src/test/java/BotTest.java
@@ -0,0 +1,24 @@
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import ru.tinkoff.edu.java.bot.firstBot.DB;
+import ru.tinkoff.edu.java.bot.firstBot.MessageHandler;
+
+
+public class BotTest {
+
+ String except_empty_list = "list пустой";
+ String except_unknow_command = "Неизвестная команда";
+ MessageHandler handler = new MessageHandler();
+
+ @Test
+ public void listTest() {
+ Assertions.assertEquals(handler.call_command("/list"), except_empty_list);
+ DB.addLink("link");
+ Assertions.assertNotEquals(handler.call_command("/list"), except_empty_list);
+ }
+
+ @Test
+ public void unknowCommandAndFormatTest() {
+ Assertions.assertEquals(handler.call_command("/uno"), except_unknow_command);
+ }
+}
diff --git a/FP/docker-compose.yml b/FP/docker-compose.yml
new file mode 100644
index 0000000..4829d18
--- /dev/null
+++ b/FP/docker-compose.yml
@@ -0,0 +1,42 @@
+version: '1.-'
+
+services:
+ postgres:
+ container_name: postgres
+ image: postgres
+ environment:
+ POSTGRES_DB: scrapper
+ POSTGRES_USER: scrap_user
+ POSTGRES_PASSWORD: hard_password
+ volumes:
+ - ./migrations/postgres_data:/var/lib/postgresql/data
+ ports:
+ - "5432:5432"
+ networks:
+ - backend
+
+ liquibase-migrations:
+ container_name: liquibase
+ image: liquibase/liquibase
+ deploy:
+ restart_policy:
+ condition: on-failure
+ delay: 10s
+ max_attempts: 10
+ command:
+ - --hub-mode=off
+ - --changelog-file=master.xml
+ - --driver=org.postgresql.Driver
+ - --url=jdbc:postgresql://postgres:5432/scrapper
+ - --username=scrap_user
+ - --password=hard_password
+ - --logLevel=debug
+ - update
+ volumes:
+ - ./migrations:/liquibase/changelog
+ networks:
+ - backend
+
+networks:
+ backend:
+ driver: bridge
\ No newline at end of file
diff --git a/FP/link-parser/pom.xml b/FP/link-parser/pom.xml
new file mode 100644
index 0000000..088df38
--- /dev/null
+++ b/FP/link-parser/pom.xml
@@ -0,0 +1,26 @@
+
+
+
+ org.example
+ FP
+ 1.0
+
+
+ 4.0.0
+
+ link-parser
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.22.2
+
+
+
\ No newline at end of file
diff --git a/FP/link-parser/src/main/java/ru/tinkoff/edu/java/linkparser/LinkParser.java b/FP/link-parser/src/main/java/ru/tinkoff/edu/java/linkparser/LinkParser.java
new file mode 100644
index 0000000..1f217ea
--- /dev/null
+++ b/FP/link-parser/src/main/java/ru/tinkoff/edu/java/linkparser/LinkParser.java
@@ -0,0 +1,18 @@
+package ru.tinkoff.edu.java.linkparser;
+
+import ru.tinkoff.edu.java.linkparser.absracts.*;
+
+public class LinkParser {
+
+ public String getLink(String link) {
+
+ AbstractParser gitParser = new GitParser();
+ AbstractParser stackParser = new StackParser();
+ AbstractParser otherParser = new OtherParser();
+
+ gitParser.setNextParser(stackParser);
+ stackParser.setNextParser(otherParser);
+
+ return gitParser.logParser(link);
+ }
+}
diff --git a/FP/link-parser/src/main/java/ru/tinkoff/edu/java/linkparser/absracts/AbstractParser.java b/FP/link-parser/src/main/java/ru/tinkoff/edu/java/linkparser/absracts/AbstractParser.java
new file mode 100644
index 0000000..503e413
--- /dev/null
+++ b/FP/link-parser/src/main/java/ru/tinkoff/edu/java/linkparser/absracts/AbstractParser.java
@@ -0,0 +1,20 @@
+package ru.tinkoff.edu.java.linkparser.absracts;
+
+public abstract class AbstractParser {
+
+ private AbstractParser nextParser;
+
+ public void setNextParser(AbstractParser nextParser) {
+ this.nextParser = nextParser;
+ }
+
+ public String logParser (String link) {
+ if(nextParser != null) {
+ if(this.parsAbstract(link) == null) return nextParser.logParser(link);
+ }
+ return this.parsAbstract(link);
+ }
+
+ abstract protected String parsAbstract(String link);
+
+}
diff --git a/FP/link-parser/src/main/java/ru/tinkoff/edu/java/linkparser/absracts/GitParser.java b/FP/link-parser/src/main/java/ru/tinkoff/edu/java/linkparser/absracts/GitParser.java
new file mode 100644
index 0000000..9525306
--- /dev/null
+++ b/FP/link-parser/src/main/java/ru/tinkoff/edu/java/linkparser/absracts/GitParser.java
@@ -0,0 +1,20 @@
+package ru.tinkoff.edu.java.linkparser.absracts;
+
+import java.util.Objects;
+
+public class GitParser extends AbstractParser {
+
+ @Override
+ protected String parsAbstract(String link) {
+
+ String[] parsed = link.split("/");
+
+ if (parsed.length < 2) return null;
+
+ if (!Objects.equals(parsed[2], "github.com")) return null;
+
+ if (parsed.length > 4) return parsed[3] + "/" + parsed[4];
+
+ return null;
+ }
+}
diff --git a/FP/link-parser/src/main/java/ru/tinkoff/edu/java/linkparser/absracts/OtherParser.java b/FP/link-parser/src/main/java/ru/tinkoff/edu/java/linkparser/absracts/OtherParser.java
new file mode 100644
index 0000000..f3746fe
--- /dev/null
+++ b/FP/link-parser/src/main/java/ru/tinkoff/edu/java/linkparser/absracts/OtherParser.java
@@ -0,0 +1,9 @@
+package ru.tinkoff.edu.java.linkparser.absracts;
+
+public class OtherParser extends AbstractParser {
+
+ @Override
+ protected String parsAbstract(String link) {
+ return null;
+ }
+}
diff --git a/FP/link-parser/src/main/java/ru/tinkoff/edu/java/linkparser/absracts/StackParser.java b/FP/link-parser/src/main/java/ru/tinkoff/edu/java/linkparser/absracts/StackParser.java
new file mode 100644
index 0000000..a73b23b
--- /dev/null
+++ b/FP/link-parser/src/main/java/ru/tinkoff/edu/java/linkparser/absracts/StackParser.java
@@ -0,0 +1,20 @@
+package ru.tinkoff.edu.java.linkparser.absracts;
+
+import java.util.Objects;
+
+public class StackParser extends AbstractParser {
+
+ @Override
+ protected String parsAbstract(String link) {
+
+ String[] parsed = link.split("/");
+
+ if (!Objects.equals(parsed[2], "stackoverflow.com")) return null;
+ if (!Objects.equals(parsed[3], "questions")) return null;
+
+
+ if (parsed.length > 4) return parsed[4];
+
+ return null;
+ }
+}
diff --git a/FP/link-parser/src/test/java/LinkParserTest.java b/FP/link-parser/src/test/java/LinkParserTest.java
new file mode 100644
index 0000000..7bd8349
--- /dev/null
+++ b/FP/link-parser/src/test/java/LinkParserTest.java
@@ -0,0 +1,24 @@
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import ru.tinkoff.edu.java.linkparser.LinkParser;
+
+public class LinkParserTest {
+
+ String gitLink = "https://github.com/Ray-Not/JavaGuava";
+ String invalidLink = "https://gitrub.com/";
+ String invalidLink2 = "https://github.com/Ray-Not";
+ String expectGitLink = "Ray-Not/JavaGuava";
+ String stackLink = "https://stackoverflow.com/questions/1642028/what-is-the-operator-in-c";
+ String expectStackLink = "1642028";
+
+ LinkParser pars = new LinkParser();
+
+ @Test
+ public void validGitPars(){
+ Assertions.assertNull(pars.getLink(invalidLink));
+ Assertions.assertNull(pars.getLink(invalidLink2));
+ Assertions.assertNotNull(pars.getLink(gitLink));
+ Assertions.assertEquals(pars.getLink(gitLink), expectGitLink);
+ Assertions.assertEquals(pars.getLink(stackLink), expectStackLink);
+ }
+}
diff --git a/FP/migrations/chats_links_scheme.sql b/FP/migrations/chats_links_scheme.sql
new file mode 100644
index 0000000..7de0483
--- /dev/null
+++ b/FP/migrations/chats_links_scheme.sql
@@ -0,0 +1,10 @@
+CREATE TABLE links (id INTEGER PRIMARY KEY, link VARCHAR(128) NOT NULL);
+
+CREATE TABLE tgChats (id INTEGER PRIMARY KEY, tg_chat_id BIGINT NOT NULL);
+
+CREATE TABLE links_tgChats (
+ LinkID INTEGER,
+ FOREIGN KEY (LinkID) REFERENCES links (id),
+ ChatID INTEGER,
+ FOREIGN KEY (ChatID) REFERENCES tgChats (id)
+);
\ No newline at end of file
diff --git a/FP/migrations/master.xml b/FP/migrations/master.xml
new file mode 100644
index 0000000..5d643de
--- /dev/null
+++ b/FP/migrations/master.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FP/pom.xml b/FP/pom.xml
new file mode 100644
index 0000000..e38a124
--- /dev/null
+++ b/FP/pom.xml
@@ -0,0 +1,116 @@
+
+
+ 4.0.0
+
+ org.example
+
+ FP
+
+ pom
+
+ 1.0
+
+
+ bot
+ link-parser
+ scrapper
+
+
+
+ UTF-8
+ 17
+ 17
+ 3.8.1
+ 3.0.1
+ 2022.0.0
+ 23.1.0
+ 5.8.1
+ 1.18.0
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.22.2
+
+ false
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring-boot.version
+
+
+ true
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+ repackage
+ build-info
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${maven-compiler-plugin.version}
+
+ 17
+ true
+
+
+
+
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-dependencies
+ ${spring-cloud.version}
+ pom
+ import
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring-boot.version}
+ pom
+ import
+
+
+ org.jetbrains
+ annotations
+ ${annotations.version}
+ provided
+
+
+ org.testcontainers
+ testcontainers-bom
+ ${testcontainers.version}
+ pom
+ import
+
+
+
+
\ No newline at end of file
diff --git a/FP/scrapper/pom.xml b/FP/scrapper/pom.xml
new file mode 100644
index 0000000..bbd37fb
--- /dev/null
+++ b/FP/scrapper/pom.xml
@@ -0,0 +1,116 @@
+
+
+
+ org.example
+ FP
+ 1.0
+
+
+ 4.0.0
+
+ scrapper
+
+
+ 3.0.1
+ 2.7.6
+
+
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.0.2
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+ ${starter-validation.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ ${starter-web.version}
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+ org.springframework
+ spring-context-indexer
+ true
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.example
+ link-parser
+ 1.0
+ compile
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+ 3.0.5
+
+
+
+ org.example
+ bot
+ 1.0
+ compile
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+ org.testcontainers
+ postgresql
+ test
+
+
+ org.liquibase
+ liquibase-core
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.22.2
+
+
+
\ No newline at end of file
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/ScrapperApplication.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/ScrapperApplication.java
new file mode 100644
index 0000000..55f205a
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/ScrapperApplication.java
@@ -0,0 +1,20 @@
+package ru.tinkoff.edu.java.scrapper;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import ru.tinkoff.edu.java.scrapper.client.ClientConfiguration;
+import ru.tinkoff.edu.java.scrapper.configuration.ApplicationConfig;
+
+@SpringBootApplication
+@EnableConfigurationProperties(ApplicationConfig.class)
+public class ScrapperApplication {
+public static void main(String[] args) {
+ var ctx = SpringApplication.run(ScrapperApplication.class, args);
+ ApplicationConfig config = ctx.getBean(ApplicationConfig.class);
+ System.out.println("----------------------------------------------------------------");
+ ClientConfiguration cls = ctx.getBean(ClientConfiguration.class);
+ System.out.println(cls.gitHubClient());
+ System.out.println(cls.stackOverflowClient());
+ }
+}
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/DB.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/DB.java
new file mode 100644
index 0000000..392e291
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/DB.java
@@ -0,0 +1,42 @@
+package ru.tinkoff.edu.java.scrapper.api;
+
+import java.util.ArrayList;
+
+public class DB {
+
+ static ArrayList tgChatIDList = new ArrayList();
+
+ public static void addId(Long id) {
+ tgChatIDList.add(id);
+ }
+
+ public static void rmId(Long id) {
+ tgChatIDList.remove(id);
+ }
+
+ public static String getListParse() {
+
+ int i = 1;
+
+ String out_list = "";
+
+ for(Long element: tgChatIDList){
+
+ out_list += element;
+
+ if(i != tgChatIDList.size()) {
+ out_list += ", ";
+ }
+ i++;
+ }
+ return out_list;
+ }
+
+ public static boolean listIsEmpty() {
+ return tgChatIDList.isEmpty();
+ }
+
+ public static boolean linkContain(Long id) {
+ return tgChatIDList.contains(id);
+ }
+}
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/ScrapperControllerLink.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/ScrapperControllerLink.java
new file mode 100644
index 0000000..1b4c616
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/ScrapperControllerLink.java
@@ -0,0 +1,36 @@
+package ru.tinkoff.edu.java.scrapper.api;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import ru.tinkoff.edu.java.bot.firstBot.BotMain;
+import ru.tinkoff.edu.java.bot.firstBot.DB;
+import ru.tinkoff.edu.java.scrapper.api.model.*;
+
+@RequestMapping("/links")
+@RestController
+public class ScrapperControllerLink {
+
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ @DeleteMapping
+ public void linksDelete(
+ @RequestHeader("Tg-Chat-Id") Long tgChatId,
+ @RequestBody RemoveLinkRequest removeLinkRequest
+ ) {
+ BotMain.apiCommand(tgChatId, "/untrack" + " " + removeLinkRequest.link());
+ }
+
+ @GetMapping
+ public void linksGet(@RequestHeader("Tg-Chat-Id") Long tgChatId) {
+ BotMain.apiCommand(tgChatId, "/list");
+ }
+
+ @PostMapping
+ public void linksPost(
+ @RequestHeader("Tg-Chat-Id") Long tgChatId,
+ @RequestBody AddLinkRequest addLinkRequest
+ ) {
+ BotMain.apiCommand(tgChatId, "/track" + " " + addLinkRequest.link());
+ }
+}
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/ScrapperControllerTg.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/ScrapperControllerTg.java
new file mode 100644
index 0000000..5968bd0
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/ScrapperControllerTg.java
@@ -0,0 +1,20 @@
+package ru.tinkoff.edu.java.scrapper.api;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+
+@RequestMapping("/tg-chat")
+@RestController
+public class ScrapperControllerTg {
+
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ @DeleteMapping("/{id}")
+ public void tgChatIdDelete(@PathVariable Long id) {
+ DB.addId(id);
+ }
+
+ @PostMapping("/{id}")
+ public void tgChatIdPost(@PathVariable Long id) {
+ DB.rmId(id);
+ }
+}
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/exceptionHandler/ScrapperExceptionHandler.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/exceptionHandler/ScrapperExceptionHandler.java
new file mode 100644
index 0000000..f6f64c9
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/exceptionHandler/ScrapperExceptionHandler.java
@@ -0,0 +1,50 @@
+package ru.tinkoff.edu.java.scrapper.api.exceptionHandler;
+
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.MissingRequestHeaderException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+import ru.tinkoff.edu.java.scrapper.api.model.ApiErrorResponse;
+
+@RestControllerAdvice
+public class ScrapperExceptionHandler {
+
+ private String getDescription(String message) {
+ ApiErrorResponse errorObj = new ApiErrorResponse(
+ message,
+ null,
+ null,
+ null,
+ null
+ );
+ return errorObj.description();
+ }
+
+ @ExceptionHandler(HttpMessageNotReadableException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public String MessageNotReadable(HttpMessageNotReadableException Exception) {
+ return getDescription("Некорректные значения параметров или их нет!");
+ }
+
+ @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public String MethodNotSupported(HttpRequestMethodNotSupportedException Exception) {
+ return getDescription("Метод не разрешен!");
+ }
+
+ @ExceptionHandler(MissingRequestHeaderException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public String MissingRequestHeader(MissingRequestHeaderException Exception) {
+ return getDescription("Нет свойства в Headers!");
+ }
+
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public String ArgumentTypeMismatch(MethodArgumentTypeMismatchException Exception) {
+ return getDescription("Неверный тип свойства в Headers!");
+ }
+}
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/model/AddLinkRequest.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/model/AddLinkRequest.java
new file mode 100644
index 0000000..7282291
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/model/AddLinkRequest.java
@@ -0,0 +1,3 @@
+package ru.tinkoff.edu.java.scrapper.api.model;
+
+public record AddLinkRequest(String link) {}
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/model/ApiErrorResponse.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/model/ApiErrorResponse.java
new file mode 100644
index 0000000..42e9be3
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/model/ApiErrorResponse.java
@@ -0,0 +1,11 @@
+package ru.tinkoff.edu.java.scrapper.api.model;
+
+import java.util.List;
+
+public record ApiErrorResponse(
+ String description,
+ String code,
+ String ExceptionName,
+ String exceptionMessage,
+ List stacktrace
+) {}
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/model/LinkResponse.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/model/LinkResponse.java
new file mode 100644
index 0000000..023172d
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/model/LinkResponse.java
@@ -0,0 +1,3 @@
+package ru.tinkoff.edu.java.scrapper.api.model;
+
+public record LinkResponse(String url, int id) {}
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/model/ListLinksResponse.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/model/ListLinksResponse.java
new file mode 100644
index 0000000..9c9ed5b
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/model/ListLinksResponse.java
@@ -0,0 +1,6 @@
+package ru.tinkoff.edu.java.scrapper.api.model;
+
+import java.util.*;
+
+public record ListLinksResponse(List links, int size) {}
+
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/model/RemoveLinkRequest.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/model/RemoveLinkRequest.java
new file mode 100644
index 0000000..3e2a8f0
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/api/model/RemoveLinkRequest.java
@@ -0,0 +1,3 @@
+package ru.tinkoff.edu.java.scrapper.api.model;
+
+public record RemoveLinkRequest(String link) {}
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/client/ClientConfiguration.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/client/ClientConfiguration.java
new file mode 100644
index 0000000..7bb87b7
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/client/ClientConfiguration.java
@@ -0,0 +1,66 @@
+package ru.tinkoff.edu.java.scrapper.client;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.reactive.function.client.WebClient;
+import ru.tinkoff.edu.java.linkparser.LinkParser;
+
+import java.util.Objects;
+
+
+@Configuration
+public class ClientConfiguration {
+
+ private final String BASE_GIT_URL = "https://github.com/Ray-Not/JavaGuava";
+ private final String BASE_STACK_URL = "https://stackoverflow.com/questions/1642028/what-is-the-operator-in-c";
+ @Value("${git.link}")
+ String gitLink;
+ @Value("${stack.link}")
+ String stackLink;
+ static WebClient.Builder builder = WebClient.builder();
+ LinkParser pars = new LinkParser();
+
+ @Bean
+ public WeatherRecord weatherClient() {
+ WeatherRecord weatherResponse = builder.build()
+ .get()
+ .uri("http://api.weatherapi.com/v1/current.json?key=3ff5d13401e44f30a14170938230204&q=Russia&aqi=no")
+ .retrieve()
+ .bodyToMono(WeatherRecord.class)
+ .block();
+ return weatherResponse;
+ }
+
+ @Bean
+ public GitHubRecord gitHubClient() {
+
+ if(Objects.equals(gitLink, "")) gitLink = BASE_GIT_URL;
+
+ gitLink = pars.getLink(gitLink);
+ GitHubRecord gitHubResponse = builder.build()
+ .get()
+ .uri("https://api.github.com/repos/" + gitLink)
+ .retrieve()
+ .bodyToMono(GitHubRecord.class)
+ .block();
+ return gitHubResponse;
+ }
+
+ @Bean
+ public StackOverflowRecord stackOverflowClient() {
+
+
+ if(Objects.equals(stackLink, "")) stackLink = BASE_STACK_URL;
+
+ String params = "?order=desc&sort=activity&site=stackoverflow";
+ stackLink = pars.getLink(stackLink);
+ StackOverflowRecord stackOverflowResponse = builder.build()
+ .get()
+ .uri("https://api.stackexchange.com/2.3/questions/" + stackLink + params)
+ .retrieve()
+ .bodyToMono(StackOverflowRecord.class)
+ .block();
+ return stackOverflowResponse;
+ }
+}
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/client/GitHubRecord.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/client/GitHubRecord.java
new file mode 100644
index 0000000..97ef980
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/client/GitHubRecord.java
@@ -0,0 +1,19 @@
+package ru.tinkoff.edu.java.scrapper.client;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.time.OffsetDateTime;
+import java.util.HashMap;
+
+public record GitHubRecord(
+ @JsonProperty("full_name") String name,
+ @JsonProperty("owner") HashMap owner,
+ @JsonProperty("private") boolean is_private,
+ @JsonProperty("node_id") String node_id,
+ @JsonProperty("html_url") String url,
+ @JsonProperty("description") String description,
+ @JsonProperty("created_at") OffsetDateTime createdAt,
+ @JsonProperty("updated_at") OffsetDateTime updatedAt,
+ @JsonProperty("pushed_at") OffsetDateTime pushedAt
+
+) {}
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/client/StackOverflowRecord.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/client/StackOverflowRecord.java
new file mode 100644
index 0000000..6013400
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/client/StackOverflowRecord.java
@@ -0,0 +1,10 @@
+package ru.tinkoff.edu.java.scrapper.client;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+public record StackOverflowRecord(
+ @JsonProperty("has_more") boolean has_more,
+ @JsonProperty("items") List owner
+) {}
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/client/WeatherRecord.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/client/WeatherRecord.java
new file mode 100644
index 0000000..5ee9adc
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/client/WeatherRecord.java
@@ -0,0 +1,11 @@
+package ru.tinkoff.edu.java.scrapper.client;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.HashMap;
+import java.util.List;
+
+public record WeatherRecord(
+ @JsonProperty("location") HashMap location,
+ @JsonProperty("current") HashMap current
+ ) {}
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/configuration/ApplicationConfig.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/configuration/ApplicationConfig.java
new file mode 100644
index 0000000..3a82d60
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/configuration/ApplicationConfig.java
@@ -0,0 +1,15 @@
+package ru.tinkoff.edu.java.scrapper.configuration;
+
+import jakarta.validation.constraints.NotNull;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.validation.annotation.Validated;
+import ru.tinkoff.edu.java.scrapper.schedule.Scheduler;
+
+@Validated
+@ConfigurationProperties(prefix = "app", ignoreUnknownFields = false)
+@EnableScheduling
+public record ApplicationConfig(
+ @NotNull String test,
+ @NotNull Scheduler scheduler
+) {}
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/schedule/LinkUpdaterScheduler.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/schedule/LinkUpdaterScheduler.java
new file mode 100644
index 0000000..37ca0de
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/schedule/LinkUpdaterScheduler.java
@@ -0,0 +1,17 @@
+package ru.tinkoff.edu.java.scrapper.schedule;
+
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+@Service
+public class LinkUpdaterScheduler {
+ private final static Logger LOGGER = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
+
+ @Scheduled(fixedDelayString = "${app.scheduler.interval}")
+ public void update() {
+ LOGGER.log(Level.INFO, "Info scrapper called");
+ }
+}
diff --git a/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/schedule/Scheduler.java b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/schedule/Scheduler.java
new file mode 100644
index 0000000..d060c22
--- /dev/null
+++ b/FP/scrapper/src/main/java/ru/tinkoff/edu/java/scrapper/schedule/Scheduler.java
@@ -0,0 +1,5 @@
+package ru.tinkoff.edu.java.scrapper.schedule;
+
+import java.time.Duration;
+
+public record Scheduler(Duration interval) {}
diff --git a/FP/scrapper/src/main/resources/application.properties b/FP/scrapper/src/main/resources/application.properties
new file mode 100644
index 0000000..b9ad588
--- /dev/null
+++ b/FP/scrapper/src/main/resources/application.properties
@@ -0,0 +1,5 @@
+app.test=123
+springdoc.swagger-ui.path=/swagger-ui
+app.scheduler.interval=5000
+git.link=
+stack.link=
diff --git a/FP/scrapper/src/main/resources/scrapperapi.yml b/FP/scrapper/src/main/resources/scrapperapi.yml
new file mode 100644
index 0000000..c98cf7d
--- /dev/null
+++ b/FP/scrapper/src/main/resources/scrapperapi.yml
@@ -0,0 +1,184 @@
+openapi: 3.0.1
+info:
+ title: Scrapper API
+ version: 1.0.0
+ contact:
+ name: Alexander Biryukov
+ url: https://github.com
+paths:
+ /tg-chat/{id}:
+ post:
+ summary: Зарегистрировать чат
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '200':
+ description: Чат зарегистрирован
+ '400':
+ description: Некорректные параметры запроса
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ApiErrorResponse'
+ delete:
+ summary: Удалить чат
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '200':
+ description: Чат успешно удалён
+ '400':
+ description: Некорректные параметры запроса
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ApiErrorResponse'
+ '404':
+ description: Чат не существует
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ApiErrorResponse'
+ /links:
+ get:
+ summary: Получить все отслеживаемые ссылки
+ parameters:
+ - name: Tg-Chat-Id
+ in: header
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '200':
+ description: Ссылки успешно получены
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ListLinksResponse'
+ '400':
+ description: Некорректные параметры запроса
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ApiErrorResponse'
+ post:
+ summary: Добавить отслеживание ссылки
+ parameters:
+ - name: Tg-Chat-Id
+ in: header
+ required: true
+ schema:
+ type: integer
+ format: int64
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AddLinkRequest'
+ required: true
+ responses:
+ '200':
+ description: Ссылка успешно добавлена
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/LinkResponse'
+ '400':
+ description: Некорректные параметры запроса
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ApiErrorResponse'
+ delete:
+ summary: Убрать отслеживание ссылки
+ parameters:
+ - name: Tg-Chat-Id
+ in: header
+ required: true
+ schema:
+ type: integer
+ format: int64
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RemoveLinkRequest'
+ required: true
+ responses:
+ '200':
+ description: Ссылка успешно убрана
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/LinkResponse'
+ '400':
+ description: Некорректные параметры запроса
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ApiErrorResponse'
+ '404':
+ description: Ссылка не найдена
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ApiErrorResponse'
+components:
+ schemas:
+ LinkResponse:
+ type: object
+ properties:
+ id:
+ type: integer
+ format: int64
+ url:
+ type: string
+ format: uri
+ ApiErrorResponse:
+ type: object
+ properties:
+ description:
+ type: string
+ code:
+ type: string
+ exceptionName:
+ type: string
+ exceptionMessage:
+ type: string
+ stacktrace:
+ type: array
+ items:
+ type: string
+ AddLinkRequest:
+ type: object
+ properties:
+ link:
+ type: string
+ format: uri
+ ListLinksResponse:
+ type: object
+ properties:
+ links:
+ type: array
+ items:
+ $ref: '#/components/schemas/LinkResponse'
+ size:
+ type: integer
+ format: int32
+ RemoveLinkRequest:
+ type: object
+ properties:
+ link:
+ type: string
+ format: uri
diff --git a/FP/scrapper/src/test/java/BaseTest.java b/FP/scrapper/src/test/java/BaseTest.java
new file mode 100644
index 0000000..447e5c5
--- /dev/null
+++ b/FP/scrapper/src/test/java/BaseTest.java
@@ -0,0 +1,11 @@
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class BaseTest extends IntegrationEnvironment {
+
+ @Test
+ void create_test() {
+ Assertions.assertTrue(Postgre_container.isCreated());
+ System.out.println(Postgre_container.getImage());
+ }
+}
\ No newline at end of file
diff --git a/FP/scrapper/src/test/java/IntegrationEnvironment.java b/FP/scrapper/src/test/java/IntegrationEnvironment.java
new file mode 100644
index 0000000..675b2c1
--- /dev/null
+++ b/FP/scrapper/src/test/java/IntegrationEnvironment.java
@@ -0,0 +1,45 @@
+import liquibase.Contexts;
+import liquibase.LabelExpression;
+import liquibase.Liquibase;
+import liquibase.database.Database;
+import liquibase.database.DatabaseFactory;
+import liquibase.database.jvm.JdbcConnection;
+import liquibase.exception.LiquibaseException;
+import liquibase.resource.DirectoryResourceAccessor;
+import org.testcontainers.containers.PostgreSQLContainer;
+
+import java.io.FileNotFoundException;
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+
+abstract class IntegrationEnvironment {
+
+ static final protected PostgreSQLContainer Postgre_container;
+
+ static {
+ Postgre_container = new PostgreSQLContainer<>("postgres:14");
+ Postgre_container.start();
+
+ try {
+ Connection connection = DriverManager.getConnection(
+ Postgre_container.getJdbcUrl(),
+ Postgre_container.getUsername(),
+ Postgre_container.getPassword());
+ Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
+ Liquibase liquibase = new liquibase.Liquibase(
+ "master.xml",
+ new DirectoryResourceAccessor(Path.of(".")
+ .toAbsolutePath()
+ .getParent()
+ .getParent()
+ .resolve("migrations")),
+ database);
+ liquibase.update(new Contexts(), new LabelExpression());
+ } catch (SQLException | LiquibaseException | FileNotFoundException e) {
+ System.out.println(e.getMessage());
+ throw new RuntimeException(e);
+ }
+ }
+}