This commit is contained in:
root
2025-12-06 22:16:57 +05:00
parent 1245a37c8f
commit 0d1cfbfeaf
4 changed files with 424 additions and 0 deletions
@@ -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;
}
}
@@ -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);
@@ -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<String> 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<String> 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<String, String> 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<String, Object> 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<String> 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;
}
}
}
@@ -103,3 +103,6 @@ posting.scheduler.enabled=true
# Facebook API Configuration
facebook.api.timeout=30000
# Telegram API Configuration
telegram.api.timeout=30000