From 0d1cfbfeaf01fc57f309730e82b06aeb2d30a05f Mon Sep 17 00:00:00 2001 From: root Date: Sat, 6 Dec 2025 22:16:57 +0500 Subject: [PATCH] . --- .../TelegramTokenExpiredException.java | 32 ++ .../parser/service/PostingTaskService.java | 29 ++ .../service/TelegramPostingService.java | 360 ++++++++++++++++++ src/main/resources/application.properties | 3 + 4 files changed, 424 insertions(+) create mode 100644 src/main/java/kz/konturai/parser/exception/TelegramTokenExpiredException.java create mode 100644 src/main/java/kz/konturai/parser/service/TelegramPostingService.java diff --git a/src/main/java/kz/konturai/parser/exception/TelegramTokenExpiredException.java b/src/main/java/kz/konturai/parser/exception/TelegramTokenExpiredException.java new file mode 100644 index 0000000..eb82aeb --- /dev/null +++ b/src/main/java/kz/konturai/parser/exception/TelegramTokenExpiredException.java @@ -0,0 +1,32 @@ +package kz.konturai.parser.exception; + +/** + * Исключение, выбрасываемое когда Telegram bot token истек или недействителен + */ +public class TelegramTokenExpiredException extends RuntimeException { + + private final String errorMessage; + private final int statusCode; + + public TelegramTokenExpiredException(String message, String errorMessage, int statusCode) { + super(message); + this.errorMessage = errorMessage; + this.statusCode = statusCode; + } + + public TelegramTokenExpiredException(String message, String errorMessage, int statusCode, + Throwable cause) { + super(message, cause); + this.errorMessage = errorMessage; + this.statusCode = statusCode; + } + + public String getErrorMessage() { + return errorMessage; + } + + public int getStatusCode() { + return statusCode; + } +} + diff --git a/src/main/java/kz/konturai/parser/service/PostingTaskService.java b/src/main/java/kz/konturai/parser/service/PostingTaskService.java index 7d9a769..1462e38 100644 --- a/src/main/java/kz/konturai/parser/service/PostingTaskService.java +++ b/src/main/java/kz/konturai/parser/service/PostingTaskService.java @@ -2,6 +2,7 @@ package kz.konturai.parser.service; import kz.konturai.parser.exception.FacebookTokenExpiredException; import kz.konturai.parser.exception.LinkedInTokenExpiredException; +import kz.konturai.parser.exception.TelegramTokenExpiredException; import kz.konturai.parser.model.MarketingStrategy; import kz.konturai.parser.model.PostingTask; import kz.konturai.parser.repository.MarketingStrategyRepository; @@ -26,6 +27,7 @@ public class PostingTaskService { private final SocialMediaCredentialsService credentialsService; private final FacebookPostingService facebookPostingService; private final LinkedInPostingService linkedInPostingService; + private final TelegramPostingService telegramPostingService; private final MinIOService minIOService; public PostingTaskService( @@ -34,12 +36,14 @@ public class PostingTaskService { SocialMediaCredentialsService credentialsService, FacebookPostingService facebookPostingService, LinkedInPostingService linkedInPostingService, + TelegramPostingService telegramPostingService, MinIOService minIOService) { this.taskRepository = taskRepository; this.strategyRepository = strategyRepository; this.credentialsService = credentialsService; this.facebookPostingService = facebookPostingService; this.linkedInPostingService = linkedInPostingService; + this.telegramPostingService = telegramPostingService; this.minIOService = minIOService; } @@ -213,6 +217,20 @@ public class PostingTaskService { task.getPostText(), task.getHashtags()); } + } else if ("telegram".equalsIgnoreCase(task.getPlatform())) { + if (imageData != null && imageData.length > 0) { + postId = telegramPostingService.postToTelegramWithImage( + credentials, + task.getPostText(), + task.getHashtags(), + imageData, + "image/png"); + } else { + postId = telegramPostingService.postToTelegram( + credentials, + task.getPostText(), + task.getHashtags()); + } } else { throw new UnsupportedOperationException("Platform not supported: " + task.getPlatform()); } @@ -247,6 +265,17 @@ public class PostingTaskService { + e.getErrorMessage()); taskRepository.save(task); + } catch (TelegramTokenExpiredException e) { + logger.error("Failed to execute task {}: Telegram bot token has expired", taskId, e); + + // Обновляем статус на failed с понятным сообщением об истекшем токене + task.setStatus("failed"); + task.setExecutedAt(LocalDateTime.now()); + task.setErrorMessage( + "Telegram bot token has expired or is invalid. Please update your Telegram credentials in the social media settings. " + + + e.getErrorMessage()); + taskRepository.save(task); } catch (Exception e) { logger.error("Failed to execute task {}", taskId, e); diff --git a/src/main/java/kz/konturai/parser/service/TelegramPostingService.java b/src/main/java/kz/konturai/parser/service/TelegramPostingService.java new file mode 100644 index 0000000..4be2571 --- /dev/null +++ b/src/main/java/kz/konturai/parser/service/TelegramPostingService.java @@ -0,0 +1,360 @@ +package kz.konturai.parser.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import kz.konturai.parser.exception.TelegramTokenExpiredException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.netty.http.client.HttpClient; + +import java.time.Duration; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class TelegramPostingService { + + private static final Logger logger = LoggerFactory.getLogger(TelegramPostingService.class); + private static final String TELEGRAM_API_BASE = "https://api.telegram.org/bot"; + + private final ObjectMapper objectMapper; + + @Value("${telegram.api.timeout:30000}") + private int timeoutMs; + + public TelegramPostingService() { + this.objectMapper = new ObjectMapper(); + } + + /** + * Публикует пост в Telegram через Bot API + * + * @param credentials JSON строка с botToken и chatId: {"botToken": "...", "chatId": "..."} + * @param postText Текст поста + * @param hashtags Список хештегов + * @return ID опубликованного сообщения + * @throws TelegramTokenExpiredException если токен истек + * @throws RuntimeException если публикация не удалась + */ + public String postToTelegram(String credentials, String postText, List hashtags) { + try { + // Парсим credentials + TelegramCredentials creds = parseCredentials(credentials); + + // Формируем полный текст поста с хештегами + String fullPostText = buildPostText(postText, hashtags); + + // Проверяем длину сообщения (Telegram limit: 4096 characters) + if (fullPostText.length() > 4096) { + logger.warn("Message too long ({} chars), truncating to 4096 characters", fullPostText.length()); + fullPostText = fullPostText.substring(0, 4093) + "..."; + } + + // Создаем WebClient с токеном в базовом URL + WebClient webClient = createWebClient(creds.botToken); + + // Публикуем сообщение + String messageId = sendMessage(webClient, creds.chatId, fullPostText); + + logger.info("Successfully posted to Telegram. Message ID: {}", messageId); + return messageId; + + } catch (WebClientResponseException e) { + if (isTokenExpiredError(e)) { + throw parseTelegramError(e); + } + logger.error("Telegram API error: {} - {}", e.getStatusCode(), e.getResponseBodyAsString()); + throw new RuntimeException("Failed to post to Telegram: " + e.getMessage(), e); + } catch (TelegramTokenExpiredException e) { + throw e; + } catch (Exception e) { + logger.error("Unexpected error posting to Telegram", e); + throw new RuntimeException("Failed to post to Telegram", e); + } + } + + /** + * Публикует пост в Telegram с изображением через Bot API + * + * @param credentials JSON строка с botToken и chatId: {"botToken": "...", "chatId": "..."} + * @param postText Текст поста + * @param hashtags Список хештегов + * @param imageData Данные изображения + * @param imageContentType MIME тип изображения (например, "image/png") + * @return ID опубликованного сообщения + * @throws TelegramTokenExpiredException если токен истек + * @throws RuntimeException если публикация не удалась + */ + public String postToTelegramWithImage(String credentials, String postText, List hashtags, + byte[] imageData, String imageContentType) { + try { + // Парсим credentials + TelegramCredentials creds = parseCredentials(credentials); + + // Формируем полный текст поста с хештегами + String fullPostText = buildPostText(postText, hashtags); + + // Проверяем длину сообщения (Telegram limit: 1024 characters для caption) + if (fullPostText.length() > 1024) { + logger.warn("Caption too long ({} chars), truncating to 1024 characters", fullPostText.length()); + fullPostText = fullPostText.substring(0, 1021) + "..."; + } + + // Создаем WebClient с токеном в базовом URL + WebClient webClient = createWebClient(creds.botToken); + + // Публикуем сообщение с изображением + String messageId = sendPhoto(webClient, creds.chatId, fullPostText, imageData, imageContentType); + + logger.info("Successfully posted to Telegram with image. Message ID: {}", messageId); + return messageId; + + } catch (WebClientResponseException e) { + if (isTokenExpiredError(e)) { + throw parseTelegramError(e); + } + logger.error("Telegram API error: {} - {}", e.getStatusCode(), e.getResponseBodyAsString()); + throw new RuntimeException("Failed to post to Telegram: " + e.getMessage(), e); + } catch (TelegramTokenExpiredException e) { + throw e; + } catch (Exception e) { + logger.error("Unexpected error posting to Telegram", e); + throw new RuntimeException("Failed to post to Telegram", e); + } + } + + /** + * Парсит credentials из JSON строки + */ + private TelegramCredentials parseCredentials(String credentials) { + try { + JsonNode jsonNode = objectMapper.readTree(credentials); + String botToken = jsonNode.has("botToken") ? jsonNode.get("botToken").asText() : null; + String chatId = jsonNode.has("chatId") ? jsonNode.get("chatId").asText() : null; + + if (botToken == null || botToken.isEmpty()) { + throw new IllegalArgumentException("botToken is required in credentials"); + } + if (chatId == null || chatId.isEmpty()) { + throw new IllegalArgumentException("chatId is required in credentials"); + } + + return new TelegramCredentials(botToken, chatId); + } catch (JsonProcessingException e) { + logger.error("Failed to parse Telegram credentials", e); + throw new IllegalArgumentException("Invalid credentials format. Expected JSON: {\"botToken\": \"...\", \"chatId\": \"...\"}", e); + } + } + + /** + * Создает WebClient с токеном в базовом URL + */ + private WebClient createWebClient(String botToken) { + HttpClient httpClient = HttpClient.create() + .responseTimeout(Duration.ofMillis(timeoutMs)); + + return WebClient.builder() + .baseUrl(TELEGRAM_API_BASE + botToken) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); + } + + /** + * Отправляет текстовое сообщение в Telegram + */ + private String sendMessage(WebClient webClient, String chatId, String text) throws JsonProcessingException { + try { + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("chat_id", chatId); + formData.add("text", text); + formData.add("parse_mode", "HTML"); // Используем HTML для форматирования + + String response = webClient.post() + .uri("/sendMessage") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(BodyInserters.fromFormData(formData)) + .retrieve() + .bodyToMono(String.class) + .block(Duration.ofMillis(timeoutMs)); + + JsonNode jsonNode = objectMapper.readTree(response); + if (!jsonNode.get("ok").asBoolean()) { + String errorDescription = jsonNode.has("description") + ? jsonNode.get("description").asText() + : "Unknown error"; + throw new RuntimeException("Telegram API error: " + errorDescription); + } + + JsonNode result = jsonNode.get("result"); + return String.valueOf(result.get("message_id").asLong()); + } catch (WebClientResponseException e) { + if (isTokenExpiredError(e)) { + throw parseTelegramError(e); + } + logger.error("Telegram API error sending message: {} - {}", e.getStatusCode(), e.getResponseBodyAsString()); + throw new RuntimeException("Failed to send message: " + e.getMessage(), e); + } + } + + /** + * Отправляет сообщение с изображением в Telegram + */ + private String sendPhoto(WebClient webClient, String chatId, String caption, byte[] imageData, String imageContentType) + throws JsonProcessingException { + try { + // Создаем multipart form data для загрузки изображения + ByteArrayResource imageResource = new ByteArrayResource(imageData) { + @Override + public String getFilename() { + return "image.jpg"; + } + }; + + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("chat_id", chatId); + formData.add("photo", imageResource); + if (caption != null && !caption.isEmpty()) { + formData.add("caption", caption); + } + formData.add("parse_mode", "HTML"); // Используем HTML для форматирования + + String response = webClient.post() + .uri("/sendPhoto") + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(BodyInserters.fromMultipartData(formData)) + .retrieve() + .bodyToMono(String.class) + .block(Duration.ofMillis(timeoutMs * 2)); // Увеличиваем таймаут для загрузки изображения + + JsonNode jsonNode = objectMapper.readTree(response); + if (!jsonNode.get("ok").asBoolean()) { + String errorDescription = jsonNode.has("description") + ? jsonNode.get("description").asText() + : "Unknown error"; + throw new RuntimeException("Telegram API error: " + errorDescription); + } + + JsonNode result = jsonNode.get("result"); + return String.valueOf(result.get("message_id").asLong()); + } catch (WebClientResponseException e) { + if (isTokenExpiredError(e)) { + throw parseTelegramError(e); + } + logger.error("Telegram API error sending photo: {} - {}", e.getStatusCode(), e.getResponseBodyAsString()); + throw new RuntimeException("Failed to send photo: " + e.getMessage(), e); + } + } + + /** + * Формирует полный текст поста с хештегами + */ + private String buildPostText(String postText, List hashtags) { + StringBuilder fullText = new StringBuilder(postText != null ? postText : ""); + + if (hashtags != null && !hashtags.isEmpty()) { + if (fullText.length() > 0) { + fullText.append("\n\n"); + } + + // Добавляем хештеги, убеждаясь что они начинаются с # + String hashtagsText = hashtags.stream() + .map(tag -> tag.startsWith("#") ? tag : "#" + tag) + .collect(Collectors.joining(" ")); + + fullText.append(hashtagsText); + } + + return fullText.toString(); + } + + /** + * Проверяет, является ли ошибка истечением токена + * Telegram возвращает HTTP 401 (Unauthorized) для недействительных токенов + */ + private boolean isTokenExpiredError(WebClientResponseException e) { + if (e.getStatusCode().value() == 401) { + return true; + } + + // Также проверяем ответ API на наличие ошибки "Unauthorized" + try { + String responseBody = e.getResponseBodyAsString(); + if (responseBody == null || responseBody.isEmpty()) { + return false; + } + + JsonNode errorNode = objectMapper.readTree(responseBody); + if (errorNode.has("ok") && !errorNode.get("ok").asBoolean()) { + String description = errorNode.has("description") + ? errorNode.get("description").asText().toLowerCase() + : ""; + // Проверяем на типичные сообщения об ошибке авторизации + return description.contains("unauthorized") + || description.contains("invalid token") + || description.contains("token is invalid"); + } + } catch (Exception ex) { + logger.warn("Failed to parse Telegram error response", ex); + } + + return false; + } + + /** + * Парсит ошибку Telegram API и создает исключение для истекшего токена + */ + private TelegramTokenExpiredException parseTelegramError(WebClientResponseException e) { + try { + String responseBody = e.getResponseBodyAsString(); + String errorMessage = "Bot token is invalid or expired"; + + if (responseBody != null && !responseBody.isEmpty()) { + try { + JsonNode errorNode = objectMapper.readTree(responseBody); + if (errorNode.has("description")) { + errorMessage = errorNode.get("description").asText(); + } + } catch (Exception ex) { + logger.warn("Failed to parse Telegram error response", ex); + } + } + + String userMessage = "Telegram bot token has expired or is invalid. Please update your credentials. " + + "Error: " + errorMessage; + + return new TelegramTokenExpiredException(userMessage, errorMessage, e.getStatusCode().value(), e); + } catch (Exception ex) { + logger.warn("Failed to parse Telegram error details", ex); + return new TelegramTokenExpiredException( + "Telegram bot token has expired or is invalid. Please update your credentials.", + "Bot token expired or invalid", + 401, + e); + } + } + + /** + * Внутренний класс для хранения credentials + */ + private static class TelegramCredentials { + final String botToken; + final String chatId; + + TelegramCredentials(String botToken, String chatId) { + this.botToken = botToken; + this.chatId = chatId; + } + } +} + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6b36448..5574a83 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -103,3 +103,6 @@ posting.scheduler.enabled=true # Facebook API Configuration facebook.api.timeout=30000 + +# Telegram API Configuration +telegram.api.timeout=30000