.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user