This commit is contained in:
arys
2026-01-29 12:08:53 +05:00
parent 8c18c13e42
commit 5d5657b941
3 changed files with 251 additions and 475 deletions
@@ -0,0 +1,78 @@
package kz.konturai.parser.controller;
import kz.konturai.parser.service.FacebookPostingService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@RestController
@RequestMapping("/api/facebook/webhook")
public class FacebookWebhookController {
private static final Logger logger = LoggerFactory.getLogger(FacebookWebhookController.class);
@Value("${facebook.verify.token}")
private String verifyToken;
@Value("${facebook.page.access.token}")
private String pageAccessToken;
private final FacebookPostingService facebookService;
private final ObjectMapper objectMapper;
public FacebookWebhookController(FacebookPostingService facebookService) {
this.facebookService = facebookService;
this.objectMapper = new ObjectMapper();
}
// Подтверждение вебхука для Фейсбука
@GetMapping
public ResponseEntity<String> verifyWebhook(
@RequestParam("hub.mode") String mode,
@RequestParam("hub.verify_token") String token,
@RequestParam("hub.challenge") String challenge) {
if ("subscribe".equals(mode) && verifyToken.equals(token)) {
return ResponseEntity.ok(challenge);
} else {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
}
// Сюда приходят сообщения
@PostMapping
public ResponseEntity<Void> receiveEvent(@RequestBody String payload) {
try {
JsonNode root = objectMapper.readTree(payload);
if (root.has("entry")) {
for (JsonNode entry : root.get("entry")) {
if (entry.has("messaging")) {
for (JsonNode event : entry.get("messaging")) {
if (event.has("message") && !event.get("message").has("is_echo")) {
// === ВОТ ЭТО САМОЕ ГЛАВНОЕ ===
String senderId = event.get("sender").get("id").asText();
String text = event.get("message").has("text") ? event.get("message").get("text").asText() : "";
// Логируем ID, чтобы ты мог его скопировать
logger.info(">>> ТЕСТЕР НАПИСАЛ СООБЩЕНИЕ! ID (PSID): {}", senderId);
logger.info(">>> Текст: {}", text);
// Можно сразу отправить автоответ для проверки связи
facebookService.sendPrivateMessage(pageAccessToken, senderId, "ID получен: " + senderId + ". Жди уведомления о посте!");
}
}
}
}
}
return ResponseEntity.ok().build();
} catch (Exception e) {
logger.error("Error processing webhook", e);
return ResponseEntity.ok().build();
}
}
}
@@ -19,7 +19,10 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti
import reactor.netty.http.client.HttpClient;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@@ -46,74 +49,125 @@ public class FacebookPostingService {
this.objectMapper = new ObjectMapper();
}
/**
* Публикует пост в Facebook через Graph API
*
* @param accessToken Access Token пользователя
* @param postText Текст поста
* @param hashtags Список хештегов
* @return ID опубликованного поста
* @throws FacebookTokenExpiredException если токен истек
* @throws RuntimeException если публикация не удалась
*/
public String postToFacebook(String accessToken, String postText, List<String> hashtags) {
public void sendPrivateMessage(String pageAccessToken, String recipientId, String messageText) {
try {
// Формируем полный текст поста с хештегами
String fullPostText = buildPostText(postText, hashtags);
Map<String, Object> recipient = new HashMap<>();
recipient.put("id", recipientId);
// Получаем ID страницы пользователя (me)
String pageId = getPageId(accessToken);
Map<String, String> messageContent = new HashMap<>();
messageContent.put("text", messageText);
// Публикуем пост на странице
String postId = publishPost(accessToken, pageId, fullPostText);
Map<String, Object> body = new HashMap<>();
body.put("recipient", recipient);
body.put("message", messageContent);
body.put("messaging_type", "RESPONSE");
logger.info("Successfully posted to Facebook. Post ID: {}", postId);
return postId;
String response = webClient.post()
.uri(uriBuilder -> uriBuilder
.path("/me/messages")
.queryParam("access_token", pageAccessToken)
.build())
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(body)
.retrieve()
.bodyToMono(String.class)
.block(Duration.ofMillis(timeoutMs));
logger.info("Message sent successfully to user {}. Response: {}", recipientId, response);
} catch (WebClientResponseException e) {
if (isTokenExpiredError(e)) {
throw parseFacebookError(e);
}
logger.error("Facebook API error: {} - {}", e.getStatusCode(), e.getResponseBodyAsString());
throw new RuntimeException("Failed to post to Facebook: " + e.getMessage(), e);
} catch (FacebookTokenExpiredException e) {
throw e;
} catch (Exception e) {
logger.error("Unexpected error posting to Facebook", e);
throw new RuntimeException("Failed to post to Facebook", e);
logger.error("Failed to send message: {}", e.getResponseBodyAsString());
throw new RuntimeException("Failed to send message: " + e.getMessage(), e);
}
}
/**
* Helper class to hold page credentials (Page ID and Page Access Token)
*/
public String postToPage(String userAccessToken, String postText, List<String> hashtags) throws JsonProcessingException {
try {
String fullPostText = buildPostText(postText, hashtags);
PageCredentials creds = getPageCredentials(userAccessToken);
String response = webClient.post()
.uri(uriBuilder -> uriBuilder
.path("/{pageId}/feed")
.queryParam("access_token", creds.pageToken)
.queryParam("message", fullPostText)
.build(creds.pageId))
.retrieve()
.bodyToMono(String.class)
.block(Duration.ofMillis(timeoutMs));
JsonNode jsonNode = objectMapper.readTree(response);
String postId = jsonNode.get("id").asText();
logger.info("Posted to Page {}. ID: {}", creds.pageId, postId);
return postId;
} catch (WebClientResponseException e) {
if (isTokenExpiredError(e)) throw parseFacebookError(e);
logger.error("Page API error: {}", e.getResponseBodyAsString());
throw new RuntimeException("Failed to post to Page: " + e.getMessage(), e);
}
}
public String postToPageWithImage(String userAccessToken, String postText, List<String> hashtags,
byte[] imageData) {
try {
String fullPostText = buildPostText(postText, hashtags);
PageCredentials creds = getPageCredentials(userAccessToken);
ByteArrayResource imageResource = new ByteArrayResource(imageData) {
@Override public String getFilename() { return "image.jpg"; }
};
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
formData.add("message", fullPostText);
formData.add("source", imageResource);
formData.add("access_token", creds.pageToken);
String response = webClient.post()
.uri(uriBuilder -> uriBuilder.path("/{pageId}/photos").build(creds.pageId))
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(formData))
.retrieve()
.bodyToMono(String.class)
.block(Duration.ofMillis(timeoutMs * 2));
JsonNode jsonNode = objectMapper.readTree(response);
String postId = jsonNode.get("id").asText();
logger.info("Posted image to Page {}. ID: {}", creds.pageId, postId);
return postId;
} catch (WebClientResponseException e) {
if (isTokenExpiredError(e)) throw parseFacebookError(e);
logger.error("Page image API error: {}", e.getResponseBodyAsString());
throw new RuntimeException("Failed to post image to Page: " + e.getMessage(), e);
} catch (Exception e) {
throw new RuntimeException("Failed to post image to Page", 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();
}
private static class PageCredentials {
String pageId;
String pageToken;
public PageCredentials(String pageId, String pageToken) {
this.pageId = pageId;
this.pageToken = pageToken;
}
}
/**
* Retrieves Page credentials (ID and Token).
* Handles two scenarios:
* 1. The input is a User Token: Calls /me/accounts to find the Page and its
* Page Token.
* 2. The input is ALREADY a Page Token: Detects the error, fetches the Page ID
* directly, and uses the input token.
*
* @param accessToken User Access Token or Page Access Token
* @return PageCredentials с Page ID и Page Access Token
* @throws FacebookTokenExpiredException если токен истек
* @throws RuntimeException если не удалось получить credentials
* страницы
*/
private PageCredentials getPageCredentials(String accessToken) {
try {
// SCENARIO 1: Try to treat it as a User Token and find pages
String response = webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/me/accounts")
@@ -123,49 +177,28 @@ public class FacebookPostingService {
.bodyToMono(String.class)
.block(Duration.ofMillis(timeoutMs));
JsonNode root = objectMapper.readTree(response);
JsonNode data = root.get("data");
JsonNode data = objectMapper.readTree(response).get("data");
if (data.isEmpty()) {
throw new RuntimeException("User has no pages available. Please check permissions (pages_show_list).");
return getPageDetailsDirectly(accessToken);
}
// Return the first page found
JsonNode firstPage = data.get(0);
return new PageCredentials(
firstPage.get("id").asText(),
firstPage.get("access_token").asText());
} catch (WebClientResponseException e) {
// SCENARIO 2: Check if the token is already a Page Token
// Error code 100 with message containing "node type (Page)" means we are
// already a Page
if (isTokenExpiredError(e)) {
throw parseFacebookError(e);
}
String responseBody = e.getResponseBodyAsString();
if (e.getStatusCode().value() == 400 && responseBody != null && responseBody.contains("Page")) {
logger.info("Provided token is already a Page Access Token. Skipping exchange.");
if (e.getStatusCode().value() == 400 && e.getResponseBodyAsString().contains("Page")) {
return getPageDetailsDirectly(accessToken);
}
logger.error("Failed to get Facebook page credentials: {} - {}", e.getStatusCode(), responseBody);
throw new RuntimeException("Failed to get page credentials: " + e.getMessage(), e);
} catch (JsonProcessingException e) {
logger.error("Failed to parse Facebook page credentials response", e);
throw new RuntimeException("Failed to parse page credentials response", e);
} catch (FacebookTokenExpiredException e) {
throw e;
if (isTokenExpiredError(e)) throw parseFacebookError(e);
throw new RuntimeException("Failed to get page credentials", e);
} catch (Exception e) {
logger.error("Failed to get page credentials", e);
throw new RuntimeException("Failed to get page credentials: " + e.getMessage(), e);
throw new RuntimeException("Failed to get page credentials", e);
}
}
/**
* Helper method: When we already have a Page Token, we just need the Page ID.
*/
private PageCredentials getPageDetailsDirectly(String pageAccessToken) {
try {
String response = webClient.get()
@@ -178,272 +211,32 @@ public class FacebookPostingService {
.bodyToMono(String.class)
.block(Duration.ofMillis(timeoutMs));
JsonNode root = objectMapper.readTree(response);
String pageId = root.get("id").asText();
// Return the ID we found, and reuse the token we already have
String pageId = objectMapper.readTree(response).get("id").asText();
return new PageCredentials(pageId, pageAccessToken);
} catch (WebClientResponseException e) {
if (isTokenExpiredError(e)) {
throw parseFacebookError(e);
}
logger.error("Failed to verify Page Access Token: {} - {}", e.getStatusCode(), e.getResponseBodyAsString());
throw new RuntimeException("Failed to verify Page Access Token: " + e.getMessage(), e);
} catch (JsonProcessingException e) {
logger.error("Failed to parse Page ID response", e);
throw new RuntimeException("Failed to verify Page Access Token: " + e.getMessage(), e);
} catch (Exception e) {
logger.error("Failed to verify Page Access Token", e);
throw new RuntimeException("Failed to verify Page Access Token: " + e.getMessage(), e);
throw new RuntimeException("Invalid token provided", e);
}
}
/**
* Получает ID страницы пользователя
*/
private String getPageId(String accessToken) {
try {
String response = webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/me")
.queryParam("access_token", accessToken)
.queryParam("fields", "id")
.build())
.retrieve()
.bodyToMono(String.class)
.block(Duration.ofMillis(timeoutMs));
JsonNode jsonNode = objectMapper.readTree(response);
return jsonNode.get("id").asText();
} catch (WebClientResponseException e) {
if (isTokenExpiredError(e)) {
throw parseFacebookError(e);
}
logger.error("Failed to get Facebook page ID: {} - {}", e.getStatusCode(), e.getResponseBodyAsString());
// Если не удалось получить ID страницы, используем "me" для публикации на стене
// пользователя
return "me";
} catch (JsonProcessingException e) {
logger.error("Failed to parse Facebook page ID response", e);
// Если не удалось получить ID страницы, используем "me" для публикации на стене
// пользователя
return "me";
} catch (FacebookTokenExpiredException e) {
throw e;
} catch (Exception e) {
logger.error("Failed to get Facebook page ID", e);
// Если не удалось получить ID страницы, используем "me" для публикации на стене
// пользователя
return "me";
}
}
/**
* Публикует пост на странице Facebook
*/
private String publishPost(String accessToken, String pageId, String message)
throws JsonProcessingException {
try {
String response = webClient.post()
.uri(uriBuilder -> uriBuilder
.path("/{pageId}/feed")
.queryParam("access_token", accessToken)
.queryParam("message", message)
.build(pageId))
.retrieve()
.bodyToMono(String.class)
.block(Duration.ofMillis(timeoutMs));
JsonNode jsonNode = objectMapper.readTree(response);
return jsonNode.get("id").asText();
} catch (WebClientResponseException e) {
if (isTokenExpiredError(e)) {
throw parseFacebookError(e);
}
logger.error("Facebook API error: {} - {}", e.getStatusCode(), e.getResponseBodyAsString());
throw new RuntimeException("Failed to publish post: " + e.getMessage(), e);
}
}
/**
* Публикует пост в Facebook с изображением через Graph API
*
* @param accessToken Access Token пользователя
* @param postText Текст поста
* @param hashtags Список хештегов
* @param imageData Данные изображения
* @param imageContentType MIME тип изображения (например, "image/png")
* @return ID опубликованного поста
* @throws FacebookTokenExpiredException если токен истек
* @throws RuntimeException если публикация не удалась
*/
public String postToFacebookWithImage(String accessToken, String postText, List<String> hashtags,
byte[] imageData, String imageContentType) {
try {
// Формируем полный текст поста с хештегами
String fullPostText = buildPostText(postText, hashtags);
// Получаем Page Access Token и Page ID из User Access Token
PageCredentials creds = getPageCredentials(accessToken);
// Загружаем изображение и публикуем пост используя Page Token
String postId = publishPostWithImage(creds.pageToken, creds.pageId, fullPostText, imageData,
imageContentType);
logger.info("Successfully posted to Facebook with image. Post ID: {}", postId);
return postId;
} catch (WebClientResponseException e) {
if (isTokenExpiredError(e)) {
throw parseFacebookError(e);
}
logger.error("Facebook API error: {} - {}", e.getStatusCode(), e.getResponseBodyAsString());
throw new RuntimeException("Failed to post to Facebook: " + e.getMessage(), e);
} catch (FacebookTokenExpiredException e) {
throw e;
} catch (Exception e) {
logger.error("Unexpected error posting to Facebook", e);
throw new RuntimeException("Failed to post to Facebook", e);
}
}
/**
* Публикует пост с изображением на странице Facebook
* Использует endpoint /photos для загрузки изображения с текстом
*/
private String publishPostWithImage(String accessToken, String pageId, String message,
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("message", message);
formData.add("source", imageResource);
String response;
try {
response = webClient.post()
.uri(uriBuilder -> uriBuilder
.path("/{pageId}/photos")
.queryParam("access_token", accessToken)
.build(pageId))
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(formData))
.retrieve()
.bodyToMono(String.class)
.block(Duration.ofMillis(timeoutMs * 2)); // Увеличиваем таймаут для загрузки изображения
} catch (WebClientResponseException e) {
String responseBody = e.getResponseBodyAsString();
logger.error("Facebook API error during image upload: Status {} - Response body: {}",
e.getStatusCode(), responseBody != null ? responseBody : "No response body");
if (isTokenExpiredError(e)) {
throw parseFacebookError(e);
}
throw new RuntimeException("Failed to publish post with image: " + e.getMessage(), e);
}
JsonNode jsonNode = objectMapper.readTree(response);
String photoId = jsonNode.get("id").asText();
// Возвращаем ID фото, который также является ID поста
return photoId;
} catch (JsonProcessingException e) {
logger.error("Failed to parse Facebook response", e);
throw e;
} catch (Exception e) {
logger.error("Unexpected error publishing post with image", e);
throw new RuntimeException("Failed to publish post with image", 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();
}
/**
* Проверяет, является ли ошибка истечением токена
* Facebook возвращает код 190 с error_subcode 463 для истекших токенов
*/
private boolean isTokenExpiredError(WebClientResponseException e) {
if (e.getStatusCode().value() != 400) {
return false;
}
if (e.getStatusCode().value() != 400) return false;
try {
String responseBody = e.getResponseBodyAsString();
if (responseBody == null || responseBody.isEmpty()) {
return false;
}
String body = e.getResponseBodyAsString();
if (body == null) return false;
JsonNode error = objectMapper.readTree(body).get("error");
if (error == null) return false;
JsonNode errorNode = objectMapper.readTree(responseBody);
if (!errorNode.has("error")) {
return false;
}
JsonNode error = errorNode.get("error");
int errorCode = error.has("code") ? error.get("code").asInt() : 0;
int errorSubcode = error.has("error_subcode") ? error.get("error_subcode").asInt() : 0;
String errorType = error.has("type") ? error.get("type").asText() : "";
// Код 190 с error_subcode 463 означает истекший токен
// Также проверяем тип ошибки OAuthException
return errorCode == 190 && errorSubcode == 463 && "OAuthException".equals(errorType);
int code = error.has("code") ? error.get("code").asInt() : 0;
int subcode = error.has("error_subcode") ? error.get("error_subcode").asInt() : 0;
return code == 190 && subcode == 463;
} catch (Exception ex) {
logger.warn("Failed to parse Facebook error response", ex);
return false;
}
}
/**
* Парсит ошибку Facebook API и создает исключение для истекшего токена
*/
private FacebookTokenExpiredException parseFacebookError(WebClientResponseException e) {
try {
String responseBody = e.getResponseBodyAsString();
JsonNode errorNode = objectMapper.readTree(responseBody);
JsonNode error = errorNode.get("error");
String errorMessage = error.has("message") ? error.get("message").asText() : "Access token expired";
int errorCode = error.has("code") ? error.get("code").asInt() : 190;
int errorSubcode = error.has("error_subcode") ? error.get("error_subcode").asInt() : 463;
String userMessage = "Facebook access token has expired. Please update your credentials. " +
"Error: " + errorMessage;
return new FacebookTokenExpiredException(userMessage, errorMessage, errorCode, errorSubcode, e);
} catch (Exception ex) {
logger.warn("Failed to parse Facebook error details", ex);
return new FacebookTokenExpiredException(
"Facebook access token has expired. Please update your credentials.",
"Access token expired",
190,
463,
e);
}
return new FacebookTokenExpiredException(
"Facebook token expired. Please re-login.",
"Token Expired", 190, 463, e);
}
}
}
@@ -7,6 +7,7 @@ import kz.konturai.parser.model.MarketingStrategy;
import kz.konturai.parser.model.PostingTask;
import kz.konturai.parser.repository.MarketingStrategyRepository;
import kz.konturai.parser.repository.PostingTaskRepository;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
@@ -18,10 +19,13 @@ import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class PostingTaskService {
private static final Logger logger = LoggerFactory.getLogger(PostingTaskService.class);
private static final String TEST_RECIPIENT_ID = "ВСТАВЬ_СЮДА_ID_ИЗ_ЛОГОВ";
private final PostingTaskRepository taskRepository;
private final MarketingStrategyRepository strategyRepository;
private final SocialMediaCredentialsService credentialsService;
@@ -30,30 +34,6 @@ public class PostingTaskService {
private final TelegramPostingService telegramPostingService;
private final MinIOService minIOService;
public PostingTaskService(
PostingTaskRepository taskRepository,
MarketingStrategyRepository strategyRepository,
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;
}
/**
* Создает задачи публикации из стратегии
* Конвертирует PostCalendarItem в PostingTask
*
* @param strategyId ID стратегии
* @return Список созданных задач
*/
public List<PostingTask> createTasksFromStrategy(String strategyId) {
Optional<MarketingStrategy> optStrategy = strategyRepository.findById(strategyId);
if (optStrategy.isEmpty()) {
@@ -71,7 +51,6 @@ public class PostingTaskService {
return new ArrayList<>();
}
// Проверяем наличие credentials для платформ
List<String> platforms = strategy.getPostCalendar().stream()
.map(MarketingStrategy.PostCalendarItem::getPlatform)
.distinct()
@@ -84,14 +63,12 @@ public class PostingTaskService {
}
}
// Проверяем, не созданы ли уже задачи для этой стратегии
List<PostingTask> existingTasks = taskRepository.findByStrategyId(strategyId);
if (!existingTasks.isEmpty()) {
logger.info("Tasks already exist for strategy {}. Returning existing tasks.", strategyId);
return existingTasks;
}
// Создаем задачи из календаря постов
List<PostingTask> tasks = new ArrayList<>();
LocalDateTime now = LocalDateTime.now();
@@ -104,24 +81,20 @@ public class PostingTaskService {
calendarItem.getHashtags(),
calendarItem.getPublishDate());
// Копируем данные об изображении
task.setImageUrl(calendarItem.getImageUrl());
task.setImageFilename(calendarItem.getImageFilename());
// Если дата публикации в прошлом, выполняем сразу
// Группы удалили, этот блок больше не нужен, но логику создания оставляем
if (task.getPublishDate().isBefore(now) || task.getPublishDate().isEqual(now)) {
task.setStatus("pending");
logger.info("Task created with past date, will be executed immediately: {}", task.getPublishDate());
}
tasks.add(task);
}
// Сохраняем все задачи
List<PostingTask> savedTasks = taskRepository.saveAll(tasks);
logger.info("Created {} posting tasks from strategy {}", savedTasks.size(), strategyId);
// Выполняем задачи с прошедшей датой сразу
for (PostingTask task : savedTasks) {
if (task.getPublishDate().isBefore(now) || task.getPublishDate().isEqual(now)) {
if ("pending".equals(task.getStatus())) {
@@ -133,21 +106,10 @@ public class PostingTaskService {
return savedTasks;
}
/**
* Получает задачи для выполнения (pending с датой <= указанной)
*
* @param beforeDate Максимальная дата публикации
* @return Список задач для выполнения
*/
public List<PostingTask> getPendingTasks(LocalDateTime beforeDate) {
return taskRepository.findByStatusAndPublishDateLessThanEqual("pending", beforeDate);
}
/**
* Выполняет задачу публикации
*
* @param taskId ID задачи
*/
public void executeTask(String taskId) {
Optional<PostingTask> optTask = taskRepository.findById(taskId);
if (optTask.isEmpty()) {
@@ -157,85 +119,89 @@ public class PostingTaskService {
PostingTask task = optTask.get();
// Проверяем статус
if (!"pending".equals(task.getStatus())) {
logger.warn("Task {} is not in pending status. Current status: {}", taskId, task.getStatus());
return;
}
// Обновляем статус на processing
task.setStatus("processing");
taskRepository.save(task);
try {
// Получаем credentials
String credentials = credentialsService.getCredentials(task.getUserId(), task.getPlatform());
if (credentials == null || credentials.isEmpty()) {
throw new IllegalStateException("Credentials not found for platform: " + task.getPlatform());
}
// Загружаем изображение, если оно есть
byte[] imageData = null;
if (task.getImageFilename() != null && !task.getImageFilename().isEmpty()) {
try {
java.io.InputStream imageStream = minIOService.downloadFile(task.getImageFilename());
imageData = imageStream.readAllBytes();
logger.info("Loaded image for task {}: {} bytes", taskId, imageData.length);
} catch (Exception e) {
logger.warn("Failed to load image for task {}: {}", taskId, e.getMessage());
// Продолжаем без изображения
}
}
// Выполняем публикацию в зависимости от платформы
String postId = null;
if ("facebook".equalsIgnoreCase(task.getPlatform())) {
// 1. ПУБЛИКУЕМ ПОСТ НА СТРАНИЦУ
if (imageData != null && imageData.length > 0) {
postId = facebookPostingService.postToFacebookWithImage(
postId = facebookPostingService.postToPageWithImage(
credentials,
task.getPostText(),
task.getHashtags(),
imageData,
"image/png");
imageData);
} else {
postId = facebookPostingService.postToFacebook(
postId = facebookPostingService.postToPage(
credentials,
task.getPostText(),
task.getHashtags());
}
// 2. ОТПРАВЛЯЕМ ЛИЧНОЕ СООБЩЕНИЕ (ЕСЛИ ПОСТ УДАЛСЯ)
if (postId != null) {
// Используем захардкоженный ID для теста
String targetRecipientId = TEST_RECIPIENT_ID;
if (targetRecipientId != null && !targetRecipientId.contains("ВСТАВЬ_СЮДА")) {
try {
String message = "✅ Ваш пост опубликован на Странице!\nID поста: " + postId + "\nВремя: " + LocalDateTime.now();
// Вызываем метод отправки, который ты добавил в FacebookPostingService
facebookPostingService.sendPrivateMessage(credentials, targetRecipientId, message);
logger.info("TEST: Sent confirmation message to HARDCODED user {}", targetRecipientId);
} catch (Exception msgEx) {
logger.warn("Post succeeded, but failed to send private message: {}", msgEx.getMessage());
}
} else {
logger.warn("TEST: Recipient ID is not configured. Please check TEST_RECIPIENT_ID constant.");
}
}
} else if ("linkedin".equalsIgnoreCase(task.getPlatform())) {
if (imageData != null && imageData.length > 0) {
postId = linkedInPostingService.postToLinkedInWithImage(
credentials,
task.getPostText(),
task.getHashtags(),
imageData,
"image/png");
credentials, task.getPostText(), task.getHashtags(), imageData, "image/png");
} else {
postId = linkedInPostingService.postToLinkedIn(
credentials,
task.getPostText(),
task.getHashtags());
credentials, 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");
credentials, task.getPostText(), task.getHashtags(), imageData, "image/png");
} else {
postId = telegramPostingService.postToTelegram(
credentials,
task.getPostText(),
task.getHashtags());
credentials, task.getPostText(), task.getHashtags());
}
} else {
throw new UnsupportedOperationException("Platform not supported: " + task.getPlatform());
}
// Обновляем статус на completed
task.setStatus("completed");
task.setExecutedAt(LocalDateTime.now());
task.setErrorMessage(null);
@@ -244,66 +210,29 @@ public class PostingTaskService {
logger.info("Task {} executed successfully. Post ID: {}", taskId, postId);
} catch (FacebookTokenExpiredException e) {
logger.error("Failed to execute task {}: Facebook access token has expired", taskId, e);
// Обновляем статус на failed с понятным сообщением об истекшем токене
task.setStatus("failed");
task.setExecutedAt(LocalDateTime.now());
task.setErrorMessage(
"Facebook access token has expired. Please update your Facebook credentials in the social media settings. "
+
e.getErrorMessage());
taskRepository.save(task);
handleError(task, "Facebook token expired: " + e.getErrorMessage());
} catch (LinkedInTokenExpiredException e) {
logger.error("Failed to execute task {}: LinkedIn access token has expired", taskId, e);
// Обновляем статус на failed с понятным сообщением об истекшем токене
task.setStatus("failed");
task.setExecutedAt(LocalDateTime.now());
task.setErrorMessage(
"LinkedIn access token has expired. Please update your LinkedIn credentials in the social media settings. "
+
e.getErrorMessage());
taskRepository.save(task);
handleError(task, "LinkedIn token expired: " + e.getErrorMessage());
} 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);
handleError(task, "Telegram token expired: " + e.getErrorMessage());
} catch (Exception e) {
logger.error("Failed to execute task {}", taskId, e);
// Обновляем статус на failed
task.setStatus("failed");
task.setExecutedAt(LocalDateTime.now());
task.setErrorMessage(e.getMessage());
taskRepository.save(task);
handleError(task, e.getMessage());
}
}
/**
* Асинхронное выполнение задачи
*/
private void handleError(PostingTask task, String message) {
task.setStatus("failed");
task.setExecutedAt(LocalDateTime.now());
task.setErrorMessage(message);
taskRepository.save(task);
}
@Async("reportGenerationExecutor")
public void executeTaskAsync(String taskId) {
executeTask(taskId);
}
/**
* Ручное выполнение задачи публикации (независимо от времени публикации)
* Разрешает выполнение только для задач со статусом "pending" или "failed"
*
* @param taskId ID задачи
* @throws IllegalArgumentException если задача не найдена
* @throws IllegalStateException если задача уже выполнена или имеет
* недопустимый статус
*/
public void executeTaskManually(String taskId) {
Optional<PostingTask> optTask = taskRepository.findById(taskId);
if (optTask.isEmpty()) {
@@ -311,55 +240,31 @@ public class PostingTaskService {
}
PostingTask task = optTask.get();
// Проверяем статус - разрешаем только pending или failed
String status = task.getStatus();
if (!"pending".equals(status) && !"failed".equals(status)) {
throw new IllegalStateException(
"Task cannot be executed manually. Current status: " + status +
". Only tasks with status 'pending' or 'failed' can be executed manually.");
"Task cannot be executed manually. Current status: " + status);
}
// Если задача в статусе failed, сбрасываем статус на pending для повторной
// попытки
if ("failed".equals(status)) {
task.setStatus("pending");
task.setErrorMessage(null);
taskRepository.save(task);
logger.info("Task {} status reset from 'failed' to 'pending' for manual execution", taskId);
}
// Выполняем задачу (игнорируя время публикации)
executeTask(taskId);
}
/**
* Получает задачи пользователя
*
* @param userId ID пользователя
* @return Список задач
*/
public List<PostingTask> getUserTasks(String userId) {
return taskRepository.findByUserId(userId);
}
/**
* Получает задачи стратегии
*
* @param strategyId ID стратегии
* @return Список задач
*/
public List<PostingTask> getStrategyTasks(String strategyId) {
return taskRepository.findByStrategyId(strategyId);
}
/**
* Получает задачу по ID
*
* @param taskId ID задачи
* @return Optional с задачей, если найдена
*/
public Optional<PostingTask> getTaskById(String taskId) {
return taskRepository.findById(taskId);
}
}
}