diff --git a/src/main/java/kz/konturai/parser/controller/FacebookWebhookController.java b/src/main/java/kz/konturai/parser/controller/FacebookWebhookController.java new file mode 100644 index 0000000..a5cfd6a --- /dev/null +++ b/src/main/java/kz/konturai/parser/controller/FacebookWebhookController.java @@ -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 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 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(); + } + } +} \ No newline at end of file diff --git a/src/main/java/kz/konturai/parser/service/FacebookPostingService.java b/src/main/java/kz/konturai/parser/service/FacebookPostingService.java index 5e872b1..a2c5810 100644 --- a/src/main/java/kz/konturai/parser/service/FacebookPostingService.java +++ b/src/main/java/kz/konturai/parser/service/FacebookPostingService.java @@ -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 hashtags) { + public void sendPrivateMessage(String pageAccessToken, String recipientId, String messageText) { try { - // Формируем полный текст поста с хештегами - String fullPostText = buildPostText(postText, hashtags); + Map recipient = new HashMap<>(); + recipient.put("id", recipientId); - // Получаем ID страницы пользователя (me) - String pageId = getPageId(accessToken); + Map messageContent = new HashMap<>(); + messageContent.put("text", messageText); - // Публикуем пост на странице - String postId = publishPost(accessToken, pageId, fullPostText); + Map 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 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 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 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 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 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 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 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); } -} +} \ No newline at end of file diff --git a/src/main/java/kz/konturai/parser/service/PostingTaskService.java b/src/main/java/kz/konturai/parser/service/PostingTaskService.java index 1462e38..aaf4467 100644 --- a/src/main/java/kz/konturai/parser/service/PostingTaskService.java +++ b/src/main/java/kz/konturai/parser/service/PostingTaskService.java @@ -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 createTasksFromStrategy(String strategyId) { Optional optStrategy = strategyRepository.findById(strategyId); if (optStrategy.isEmpty()) { @@ -71,7 +51,6 @@ public class PostingTaskService { return new ArrayList<>(); } - // Проверяем наличие credentials для платформ List platforms = strategy.getPostCalendar().stream() .map(MarketingStrategy.PostCalendarItem::getPlatform) .distinct() @@ -84,14 +63,12 @@ public class PostingTaskService { } } - // Проверяем, не созданы ли уже задачи для этой стратегии List existingTasks = taskRepository.findByStrategyId(strategyId); if (!existingTasks.isEmpty()) { logger.info("Tasks already exist for strategy {}. Returning existing tasks.", strategyId); return existingTasks; } - // Создаем задачи из календаря постов List 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 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 getPendingTasks(LocalDateTime beforeDate) { return taskRepository.findByStatusAndPublishDateLessThanEqual("pending", beforeDate); } - /** - * Выполняет задачу публикации - * - * @param taskId ID задачи - */ public void executeTask(String taskId) { Optional 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 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 getUserTasks(String userId) { return taskRepository.findByUserId(userId); } - /** - * Получает задачи стратегии - * - * @param strategyId ID стратегии - * @return Список задач - */ public List getStrategyTasks(String strategyId) { return taskRepository.findByStrategyId(strategyId); } - /** - * Получает задачу по ID - * - * @param taskId ID задачи - * @return Optional с задачей, если найдена - */ public Optional getTaskById(String taskId) { return taskRepository.findById(taskId); } -} +} \ No newline at end of file