Skip to content

Commit

Permalink
[TRAC-5] - Create Alert , Notification , Command /add /get monitoring…
Browse files Browse the repository at this point in the history
… and BinanceApiClient
  • Loading branch information
OleksandrRym committed Dec 13, 2024
1 parent c3cbc9d commit 9016e13
Show file tree
Hide file tree
Showing 21 changed files with 539 additions and 25 deletions.
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>6.2.0</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
@EnableConfigurationProperties(TelegramBotSettings.class)
public class TrackmycoinApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.vladyslavpalamarchuk.trackmycoin.adaptors.api;

import com.vladyslavpalamarchuk.trackmycoin.adaptors.api.DTO.BinancePriceResponse;
import java.math.BigDecimal;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

@Service
@RequiredArgsConstructor
@Slf4j
public class BinanceApiClient {
private final String BINANCE_URL = "https://api.binance.com/api/v3/ticker/price?symbol=";
private final RestTemplate restTemplate = new RestTemplate();
private final String TICKER_CURRENCY = "USDT";

public boolean isCoinAvailable(String ticker) {
String url = BINANCE_URL + ticker.toUpperCase() + TICKER_CURRENCY;
try {
restTemplate.getForEntity(url, String.class);
return true;
} catch (HttpClientErrorException.NotFound e) {
log.warn("Coin not available: {}", ticker);
return false;
} catch (Exception e) {
log.error("Error while checking coin availability", e);
return false;
}
}

public Optional<BigDecimal> getPrice(String ticker) {
String url = BINANCE_URL + ticker;
try {
RestTemplate restTemplate = new RestTemplate();
BinancePriceResponse response = restTemplate.getForObject(url, BinancePriceResponse.class);
if (response != null && response.getPrice() != null) {
return Optional.of(response.getPrice());
} else {
log.warn("Price not found for ticker: {}", ticker);
return Optional.empty();
}
} catch (Exception e) {
log.error("Failed to fetch price for ticker: {}. Error: {}", ticker, e.getMessage(), e);
return Optional.empty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.vladyslavpalamarchuk.trackmycoin.adaptors.api.DTO;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.math.BigDecimal;
import lombok.Setter;

@Setter
public class BinancePriceResponse {
private String symbol;
private BigDecimal price;

@JsonProperty("symbol")
public String getSymbol() {
return symbol;
}

@JsonProperty("price")
public BigDecimal getPrice() {
return price;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.vladyslavpalamarchuk.trackmycoin.adaptors.persistence;

import com.vladyslavpalamarchuk.trackmycoin.domain.Monitoring;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MonitoringRepository extends JpaRepository<Monitoring, Long> {

List<Monitoring> findByUserId(Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.vladyslavpalamarchuk.trackmycoin.adaptors.persistence;

import com.vladyslavpalamarchuk.trackmycoin.domain.User;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findByChatId(Long chatId);

User findUserByChatId(Long chatId);
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ public class TelegramBotConsumer implements LongPollingSingleThreadUpdateConsume
public void consume(Update update) {
if (update.hasMessage() && update.getMessage().hasText()) {
logUpdateMessage(update.getMessage());
commandProcessorRegistry.get(update.getMessage().getText()).process(update);
commandProcessorRegistry
.get(update.getMessage().getChatId(), update.getMessage().getText())
.process(update);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public enum Command {
START("/start"),
INFO("/info"),
HELP("/help"),
GETMONITOR("/get"),
ADDMONITOR("/add"),
NON_COMMAND("");

private final String command;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
Expand All @@ -12,22 +13,39 @@
public class CommandProcessorRegistry {

private final Map<Command, CommandProcessor> commandToProcessors;
private final Map<Long, Command> userLastCommands = new ConcurrentHashMap<>();

public CommandProcessorRegistry(List<CommandProcessor> processors) {
commandToProcessors =
processors.stream()
.collect(Collectors.toMap(CommandProcessor::getCommand, Function.identity()));
.collect(
Collectors.toMap(
com.vladyslavpalamarchuk.trackmycoin.command.processor.CommandProcessor
::getCommand,
Function.identity()));
}

public CommandProcessor get(String messageText) {
public CommandProcessor get(Long chatId, String messageText) {
messageText = messageText.trim();

if (!messageText.startsWith("/")) {
Command lastCommand = userLastCommands.get(chatId);
if (lastCommand == Command.ADDMONITOR) {
return commandToProcessors.get(Command.ADDMONITOR);
}
return commandToProcessors.get(Command.NON_COMMAND);
}

return Arrays.stream(Command.values())
.filter(c -> c.getCommand().equalsIgnoreCase(messageText))
.findFirst()
.map(commandToProcessors::get)
.orElse(commandToProcessors.get(Command.NON_COMMAND));
// Знаходимо відповідну команду
String finalMessageText = messageText;
Command matchedCommand =
Arrays.stream(Command.values())
.filter(c -> c.getCommand().equalsIgnoreCase(finalMessageText))
.findFirst()
.orElse(Command.NON_COMMAND);

userLastCommands.put(chatId, matchedCommand);

return commandToProcessors.get(matchedCommand);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.vladyslavpalamarchuk.trackmycoin.command.processor;

import com.vladyslavpalamarchuk.trackmycoin.adaptors.api.BinanceApiClient;
import com.vladyslavpalamarchuk.trackmycoin.adaptors.telegram.TelegramBotClient;
import com.vladyslavpalamarchuk.trackmycoin.command.Command;
import com.vladyslavpalamarchuk.trackmycoin.service.MonitoringService;
import java.math.BigDecimal;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.telegram.telegrambots.meta.api.objects.Update;

@Component
@RequiredArgsConstructor
public class AddMonitoringCommandProcessor implements CommandProcessor {

private final BinanceApiClient binanceApiClient;
private final TelegramBotClient telegramBotClient;
private final MonitoringService monitoringService;
private final String ADD_COMMAND = "/add";
private final int PART_LENGTH = 2;

@Override
public void process(Update update) {
Long chatId = update.getMessage().getChatId();
String userMessage = update.getMessage().getText();
if (userMessage.equalsIgnoreCase(ADD_COMMAND)) {
telegramBotClient.sendMessage(
chatId, "Please enter the coin ticker and its current price (for example, BTC 50000):");
return;
}

handleTickerInput(update);
}

private void handleTickerInput(Update update) {
Long chatId = getChatId(update);

String[] inputParts = extractAndValidateInput(update, chatId);

if (inputParts != null) {
processValidTicker(chatId, inputParts[0], inputParts[1]);
}
}

private Long getChatId(Update update) {
return update.getMessage().getChatId();
}

private String[] extractAndValidateInput(Update update, Long chatId) {
String[] parts = update.getMessage().getText().split(" ");
if (parts.length != PART_LENGTH) {
telegramBotClient.sendMessage(
chatId, "Please enter two values: the coin ticker and its current price.");
return null;
}
return parts;
}

private void processValidTicker(Long chatId, String ticker, String price) {
if (isCoinAvailable(ticker) && isPriceAvailable(price)) {
addTickerToMonitoring(ticker, price, chatId);
} else {
sendCoinNotFoundMessage(chatId);
}
}

private boolean isCoinAvailable(String ticker) {
return binanceApiClient.isCoinAvailable(ticker);
}

private boolean isPriceAvailable(String price) {
if (price == null || price.trim().isEmpty()) {
return false;
}
try {
new BigDecimal(price);
return true;
} catch (NumberFormatException e) {
return false;
}
}

private void addTickerToMonitoring(String ticker, String price, Long chatId) {
monitoringService.add(ticker, price, chatId);
}

private void sendCoinNotFoundMessage(Long chatId) {
telegramBotClient.sendMessage(chatId, "Coin not found. Please try again.");
}

@Override
public Command getCommand() {
return Command.ADDMONITOR;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.vladyslavpalamarchuk.trackmycoin.command.processor;

import com.vladyslavpalamarchuk.trackmycoin.adaptors.persistence.UserRepository;
import com.vladyslavpalamarchuk.trackmycoin.command.Command;
import com.vladyslavpalamarchuk.trackmycoin.service.MonitoringService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.telegram.telegrambots.meta.api.objects.Update;

@Component
@RequiredArgsConstructor
public class GetMonitoringCommandProcessor implements CommandProcessor {

private final UserRepository userRepository;
private final MonitoringService monitoringService;

@Override
public void process(Update update) {
Long chatId = update.getMessage().getChatId();

userRepository
.findByChatId(chatId)
.ifPresent(
user -> {
monitoringService.get(user.getId());
});
}

@Override
public Command getCommand() {
return Command.GETMONITOR;
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
package com.vladyslavpalamarchuk.trackmycoin.command.processor;

import com.vladyslavpalamarchuk.trackmycoin.adaptors.persistence.UserRepository;
import com.vladyslavpalamarchuk.trackmycoin.adaptors.telegram.TelegramBotClient;
import com.vladyslavpalamarchuk.trackmycoin.command.Command;
import com.vladyslavpalamarchuk.trackmycoin.config.TelegramBotDescription;
import com.vladyslavpalamarchuk.trackmycoin.domain.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.telegram.telegrambots.meta.api.objects.Update;

@Slf4j
@Component
@RequiredArgsConstructor
public class StartCommandProcessor implements CommandProcessor {

private final TelegramBotClient telegramBotClient;

private final TelegramBotDescription telegramBotDescription;
private final UserRepository userRepository;

@Override
public void process(Update update) {
telegramBotClient.sendMessage(
update.getMessage().getChatId(), telegramBotDescription.getStart());
Long chatId = update.getMessage().getChatId();

userRepository
.findByChatId(chatId)
.ifPresentOrElse(
user -> {
telegramBotClient.sendMessage(chatId, telegramBotDescription.getStart());
},
() -> {
User newUser = new User();
newUser.setChatId(chatId);
newUser.setCreatedBy("bot");
newUser.setUpdatedBy("bot");
userRepository.save(newUser);
telegramBotClient.sendMessage(chatId, telegramBotDescription.getStart());
});
}

@Override
Expand Down
Loading

0 comments on commit 9016e13

Please sign in to comment.