diff --git a/src/main/java/kz/konturai/parser/exception/FacebookTokenExpiredException.java b/src/main/java/kz/konturai/parser/exception/FacebookTokenExpiredException.java new file mode 100644 index 0000000..ea21241 --- /dev/null +++ b/src/main/java/kz/konturai/parser/exception/FacebookTokenExpiredException.java @@ -0,0 +1,38 @@ +package kz.konturai.parser.exception; + +/** + * Исключение, выбрасываемое когда Facebook access token истек + */ +public class FacebookTokenExpiredException extends RuntimeException { + + private final String errorMessage; + private final int errorCode; + private final int errorSubcode; + + public FacebookTokenExpiredException(String message, String errorMessage, int errorCode, int errorSubcode) { + super(message); + this.errorMessage = errorMessage; + this.errorCode = errorCode; + this.errorSubcode = errorSubcode; + } + + public FacebookTokenExpiredException(String message, String errorMessage, int errorCode, int errorSubcode, + Throwable cause) { + super(message, cause); + this.errorMessage = errorMessage; + this.errorCode = errorCode; + this.errorSubcode = errorSubcode; + } + + public String getErrorMessage() { + return errorMessage; + } + + public int getErrorCode() { + return errorCode; + } + + public int getErrorSubcode() { + return errorSubcode; + } +} diff --git a/src/main/java/kz/konturai/parser/exception/LinkedInTokenExpiredException.java b/src/main/java/kz/konturai/parser/exception/LinkedInTokenExpiredException.java new file mode 100644 index 0000000..3ba82ae --- /dev/null +++ b/src/main/java/kz/konturai/parser/exception/LinkedInTokenExpiredException.java @@ -0,0 +1,32 @@ +package kz.konturai.parser.exception; + +/** + * Исключение, выбрасываемое когда LinkedIn access token истек + */ +public class LinkedInTokenExpiredException extends RuntimeException { + + private final String errorMessage; + private final int statusCode; + + public LinkedInTokenExpiredException(String message, String errorMessage, int statusCode) { + super(message); + this.errorMessage = errorMessage; + this.statusCode = statusCode; + } + + public LinkedInTokenExpiredException(String message, String errorMessage, int statusCode, + Throwable cause) { + super(message, cause); + this.errorMessage = errorMessage; + this.statusCode = statusCode; + } + + public String getErrorMessage() { + return errorMessage; + } + + public int getStatusCode() { + return statusCode; + } +} + diff --git a/src/main/java/kz/konturai/parser/service/FacebookPostingService.java b/src/main/java/kz/konturai/parser/service/FacebookPostingService.java index a60af95..d1c014e 100644 --- a/src/main/java/kz/konturai/parser/service/FacebookPostingService.java +++ b/src/main/java/kz/konturai/parser/service/FacebookPostingService.java @@ -3,6 +3,7 @@ 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.FacebookTokenExpiredException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -54,7 +55,8 @@ public class FacebookPostingService { * @param postText Текст поста * @param hashtags Список хештегов * @return ID опубликованного поста - * @throws RuntimeException если публикация не удалась + * @throws FacebookTokenExpiredException если токен истек + * @throws RuntimeException если публикация не удалась */ public String postToFacebook(String accessToken, String postText, List hashtags) { try { @@ -71,8 +73,13 @@ public class FacebookPostingService { 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); @@ -96,11 +103,21 @@ public class FacebookPostingService { 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" для публикации на стене @@ -114,18 +131,26 @@ public class FacebookPostingService { */ private String publishPost(String accessToken, String pageId, String message) throws JsonProcessingException { - 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)); + 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(); + 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); + } } /** @@ -137,7 +162,8 @@ public class FacebookPostingService { * @param imageData Данные изображения * @param imageContentType MIME тип изображения (например, "image/png") * @return ID опубликованного поста - * @throws RuntimeException если публикация не удалась + * @throws FacebookTokenExpiredException если токен истек + * @throws RuntimeException если публикация не удалась */ public String postToFacebookWithImage(String accessToken, String postText, List hashtags, byte[] imageData, String imageContentType) { @@ -155,8 +181,13 @@ public class FacebookPostingService { 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); @@ -170,30 +201,38 @@ public class FacebookPostingService { private String publishPostWithImage(String accessToken, String pageId, String message, byte[] imageData, String imageContentType) throws JsonProcessingException { - // Создаем multipart form data для загрузки изображения - DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); - DataBuffer imageBuffer = bufferFactory.wrap(imageData); + try { + // Создаем multipart form data для загрузки изображения + DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); + DataBuffer imageBuffer = bufferFactory.wrap(imageData); - MultiValueMap formData = new LinkedMultiValueMap<>(); - formData.add("message", message); - formData.add("source", imageBuffer); + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("message", message); + formData.add("source", imageBuffer); - String 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)); // Увеличиваем таймаут для загрузки изображения + String 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)); // Увеличиваем таймаут для загрузки изображения - JsonNode jsonNode = objectMapper.readTree(response); - String photoId = jsonNode.get("id").asText(); + JsonNode jsonNode = objectMapper.readTree(response); + String photoId = jsonNode.get("id").asText(); - // Возвращаем ID фото, который также является ID поста - return photoId; + // Возвращаем ID фото, который также является ID поста + return photoId; + } 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 with image: " + e.getMessage(), e); + } } /** @@ -217,4 +256,66 @@ public class FacebookPostingService { return fullText.toString(); } + + /** + * Проверяет, является ли ошибка истечением токена + * Facebook возвращает код 190 с error_subcode 463 для истекших токенов + */ + private boolean isTokenExpiredError(WebClientResponseException e) { + if (e.getStatusCode().value() != 400) { + return false; + } + + try { + String responseBody = e.getResponseBodyAsString(); + if (responseBody == null || responseBody.isEmpty()) { + 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); + } 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); + } + } } diff --git a/src/main/java/kz/konturai/parser/service/LinkedInPostingService.java b/src/main/java/kz/konturai/parser/service/LinkedInPostingService.java new file mode 100644 index 0000000..ee9c98b --- /dev/null +++ b/src/main/java/kz/konturai/parser/service/LinkedInPostingService.java @@ -0,0 +1,372 @@ +package kz.konturai.parser.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import kz.konturai.parser.exception.LinkedInTokenExpiredException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.stereotype.Service; +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 LinkedInPostingService { + + private static final Logger logger = LoggerFactory.getLogger(LinkedInPostingService.class); + private static final String LINKEDIN_API_BASE = "https://api.linkedin.com/v2"; + + private final WebClient webClient; + private final ObjectMapper objectMapper; + + @Value("${linkedin.api.timeout:30000}") + private int timeoutMs; + + public LinkedInPostingService() { + HttpClient httpClient = HttpClient.create() + .responseTimeout(Duration.ofMillis(30000)); + + this.webClient = WebClient.builder() + .baseUrl(LINKEDIN_API_BASE) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .build(); + + this.objectMapper = new ObjectMapper(); + } + + /** + * Публикует пост в LinkedIn через REST API v2 + * + * @param accessToken Access Token пользователя + * @param postText Текст поста + * @param hashtags Список хештегов + * @return ID опубликованного поста + * @throws LinkedInTokenExpiredException если токен истек + * @throws RuntimeException если публикация не удалась + */ + public String postToLinkedIn(String accessToken, String postText, List hashtags) { + try { + // Формируем полный текст поста с хештегами + String fullPostText = buildPostText(postText, hashtags); + + // Получаем URN пользователя + String personUrn = getPersonUrn(accessToken); + + // Публикуем пост + String postId = publishPost(accessToken, personUrn, fullPostText, null); + + logger.info("Successfully posted to LinkedIn. Post ID: {}", postId); + return postId; + + } catch (WebClientResponseException e) { + if (isTokenExpiredError(e)) { + throw parseLinkedInError(e); + } + logger.error("LinkedIn API error: {} - {}", e.getStatusCode(), e.getResponseBodyAsString()); + throw new RuntimeException("Failed to post to LinkedIn: " + e.getMessage(), e); + } catch (LinkedInTokenExpiredException e) { + throw e; + } catch (Exception e) { + logger.error("Unexpected error posting to LinkedIn", e); + throw new RuntimeException("Failed to post to LinkedIn", e); + } + } + + /** + * Публикует пост в LinkedIn с изображением через REST API v2 + * + * @param accessToken Access Token пользователя + * @param postText Текст поста + * @param hashtags Список хештегов + * @param imageData Данные изображения + * @param imageContentType MIME тип изображения (например, "image/png") + * @return ID опубликованного поста + * @throws LinkedInTokenExpiredException если токен истек + * @throws RuntimeException если публикация не удалась + */ + public String postToLinkedInWithImage(String accessToken, String postText, List hashtags, + byte[] imageData, String imageContentType) { + try { + // Формируем полный текст поста с хештегами + String fullPostText = buildPostText(postText, hashtags); + + // Получаем URN пользователя + String personUrn = getPersonUrn(accessToken); + + // Загружаем изображение и получаем его URN + String imageUrn = uploadImage(accessToken, imageData, imageContentType); + + // Публикуем пост с изображением + String postId = publishPost(accessToken, personUrn, fullPostText, imageUrn); + + logger.info("Successfully posted to LinkedIn with image. Post ID: {}", postId); + return postId; + + } catch (WebClientResponseException e) { + if (isTokenExpiredError(e)) { + throw parseLinkedInError(e); + } + logger.error("LinkedIn API error: {} - {}", e.getStatusCode(), e.getResponseBodyAsString()); + throw new RuntimeException("Failed to post to LinkedIn: " + e.getMessage(), e); + } catch (LinkedInTokenExpiredException e) { + throw e; + } catch (Exception e) { + logger.error("Unexpected error posting to LinkedIn", e); + throw new RuntimeException("Failed to post to LinkedIn", e); + } + } + + /** + * Получает URN пользователя через /me endpoint + */ + private String getPersonUrn(String accessToken) { + try { + String response = webClient.get() + .uri("/me") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .retrieve() + .bodyToMono(String.class) + .block(Duration.ofMillis(timeoutMs)); + + JsonNode jsonNode = objectMapper.readTree(response); + String id = jsonNode.get("id").asText(); + + // Формируем URN в формате urn:li:person:{id} + return "urn:li:person:" + id; + } catch (WebClientResponseException e) { + if (isTokenExpiredError(e)) { + throw parseLinkedInError(e); + } + logger.error("Failed to get LinkedIn person URN: {} - {}", e.getStatusCode(), e.getResponseBodyAsString()); + throw new RuntimeException("Failed to get LinkedIn person URN: " + e.getMessage(), e); + } catch (JsonProcessingException e) { + logger.error("Failed to parse LinkedIn person URN response", e); + throw new RuntimeException("Failed to parse LinkedIn person URN response", e); + } catch (LinkedInTokenExpiredException e) { + throw e; + } catch (Exception e) { + logger.error("Failed to get LinkedIn person URN", e); + throw new RuntimeException("Failed to get LinkedIn person URN", e); + } + } + + /** + * Загружает изображение в LinkedIn и возвращает его URN + */ + private String uploadImage(String accessToken, byte[] imageData, String imageContentType) { + try { + // Шаг 1: Инициализируем загрузку изображения + ObjectNode initializeRequest = objectMapper.createObjectNode(); + ObjectNode initializeUploadRequest = objectMapper.createObjectNode(); + initializeUploadRequest.put("owner", getPersonUrn(accessToken)); + initializeRequest.set("initializeUploadRequest", initializeUploadRequest); + + String initializeResponse = webClient.post() + .uri("/images?action=initializeUpload") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(initializeRequest.toString())) + .retrieve() + .bodyToMono(String.class) + .block(Duration.ofMillis(timeoutMs)); + + JsonNode initializeJson = objectMapper.readTree(initializeResponse); + JsonNode value = initializeJson.get("value"); + String uploadUrl = value.get("uploadUrl").asText(); + String imageUrn = value.get("image").asText(); + + // Шаг 2: Загружаем изображение по полученному URL + uploadImageToUrl(uploadUrl, imageData, imageContentType); + + return imageUrn; + } catch (WebClientResponseException e) { + if (isTokenExpiredError(e)) { + throw parseLinkedInError(e); + } + logger.error("LinkedIn API error uploading image: {} - {}", e.getStatusCode(), e.getResponseBodyAsString()); + throw new RuntimeException("Failed to upload image to LinkedIn: " + e.getMessage(), e); + } catch (JsonProcessingException e) { + logger.error("Failed to parse LinkedIn image upload response", e); + throw new RuntimeException("Failed to parse LinkedIn image upload response", e); + } catch (Exception e) { + logger.error("Failed to upload image to LinkedIn", e); + throw new RuntimeException("Failed to upload image to LinkedIn", e); + } + } + + /** + * Загружает изображение по указанному URL + */ + private void uploadImageToUrl(String uploadUrl, byte[] imageData, String imageContentType) { + try { + WebClient uploadClient = WebClient.builder() + .build(); + + uploadClient.put() + .uri(uploadUrl) + .contentType(MediaType.parseMediaType(imageContentType)) + .body(BodyInserters.fromValue(imageData)) + .retrieve() + .bodyToMono(Void.class) + .block(Duration.ofMillis(timeoutMs * 2)); + + logger.debug("Image uploaded successfully to LinkedIn"); + } catch (Exception e) { + logger.error("Failed to upload image to LinkedIn upload URL", e); + throw new RuntimeException("Failed to upload image to LinkedIn upload URL", e); + } + } + + /** + * Публикует пост в LinkedIn через /ugcPosts endpoint + */ + private String publishPost(String accessToken, String personUrn, String message, String imageUrn) + throws JsonProcessingException { + try { + // Создаем JSON структуру для UGC Post + ObjectNode requestBody = objectMapper.createObjectNode(); + + // Author + requestBody.put("author", personUrn); + + // Lifecycle state + requestBody.put("lifecycleState", "PUBLISHED"); + + // Specific content + ObjectNode specificContent = objectMapper.createObjectNode(); + ObjectNode shareContent = objectMapper.createObjectNode(); + + // Share commentary (текст поста) + ObjectNode shareCommentary = objectMapper.createObjectNode(); + shareCommentary.put("text", message); + shareContent.set("shareCommentary", shareCommentary); + + // Share media category + if (imageUrn != null && !imageUrn.isEmpty()) { + shareContent.put("shareMediaCategory", "IMAGE"); + + // Media + com.fasterxml.jackson.databind.node.ArrayNode mediaArray = objectMapper.createArrayNode(); + ObjectNode mediaItem = objectMapper.createObjectNode(); + mediaItem.put("status", "READY"); + mediaItem.put("media", imageUrn); + mediaArray.add(mediaItem); + + ObjectNode media = objectMapper.createObjectNode(); + media.set("media", mediaArray); + shareContent.set("media", media); + } else { + shareContent.put("shareMediaCategory", "NONE"); + } + + specificContent.set("com.linkedin.ugc.ShareContent", shareContent); + requestBody.set("specificContent", specificContent); + + // Visibility + ObjectNode visibility = objectMapper.createObjectNode(); + visibility.put("com.linkedin.ugc.MemberNetworkVisibility", "PUBLIC"); + requestBody.set("visibility", visibility); + + String response = webClient.post() + .uri("/ugcPosts") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(requestBody.toString())) + .retrieve() + .bodyToMono(String.class) + .block(Duration.ofMillis(timeoutMs)); + + JsonNode jsonNode = objectMapper.readTree(response); + String postId = jsonNode.get("id").asText(); + + return postId; + } catch (WebClientResponseException e) { + if (isTokenExpiredError(e)) { + throw parseLinkedInError(e); + } + logger.error("LinkedIn API error: {} - {}", e.getStatusCode(), e.getResponseBodyAsString()); + throw new RuntimeException("Failed to publish post: " + e.getMessage(), e); + } + } + + /** + * Формирует полный текст поста с хештегами + */ + private String buildPostText(String postText, List hashtags) { + StringBuilder fullText = new StringBuilder(postText != null ? postText : ""); + + if (hashtags != null && !hashtags.isEmpty()) { + if (fullText.length() > 0) { + fullText.append("\n\n"); + } + + // Добавляем хештеги, убеждаясь что они начинаются с # + String hashtagsText = hashtags.stream() + .map(tag -> tag.startsWith("#") ? tag : "#" + tag) + .collect(Collectors.joining(" ")); + + fullText.append(hashtagsText); + } + + return fullText.toString(); + } + + /** + * Проверяет, является ли ошибка истечением токена + * LinkedIn возвращает HTTP 401 (Unauthorized) для истекших токенов + */ + private boolean isTokenExpiredError(WebClientResponseException e) { + return e.getStatusCode().value() == 401; + } + + /** + * Парсит ошибку LinkedIn API и создает исключение для истекшего токена + */ + private LinkedInTokenExpiredException parseLinkedInError(WebClientResponseException e) { + try { + String responseBody = e.getResponseBodyAsString(); + String errorMessage = "Access token expired or invalid"; + + if (responseBody != null && !responseBody.isEmpty()) { + try { + JsonNode errorNode = objectMapper.readTree(responseBody); + if (errorNode.has("message")) { + errorMessage = errorNode.get("message").asText(); + } else if (errorNode.has("error")) { + JsonNode error = errorNode.get("error"); + if (error.has("message")) { + errorMessage = error.get("message").asText(); + } + } + } catch (Exception ex) { + logger.warn("Failed to parse LinkedIn error response", ex); + } + } + + String userMessage = "LinkedIn access token has expired. Please update your credentials. " + + "Error: " + errorMessage; + + return new LinkedInTokenExpiredException(userMessage, errorMessage, e.getStatusCode().value(), e); + } catch (Exception ex) { + logger.warn("Failed to parse LinkedIn error details", ex); + return new LinkedInTokenExpiredException( + "LinkedIn access token has expired. Please update your credentials.", + "Access token expired", + 401, + e); + } + } +} + diff --git a/src/main/java/kz/konturai/parser/service/PostingTaskService.java b/src/main/java/kz/konturai/parser/service/PostingTaskService.java index 535198c..7d9a769 100644 --- a/src/main/java/kz/konturai/parser/service/PostingTaskService.java +++ b/src/main/java/kz/konturai/parser/service/PostingTaskService.java @@ -1,5 +1,7 @@ package kz.konturai.parser.service; +import kz.konturai.parser.exception.FacebookTokenExpiredException; +import kz.konturai.parser.exception.LinkedInTokenExpiredException; import kz.konturai.parser.model.MarketingStrategy; import kz.konturai.parser.model.PostingTask; import kz.konturai.parser.repository.MarketingStrategyRepository; @@ -23,6 +25,7 @@ public class PostingTaskService { private final MarketingStrategyRepository strategyRepository; private final SocialMediaCredentialsService credentialsService; private final FacebookPostingService facebookPostingService; + private final LinkedInPostingService linkedInPostingService; private final MinIOService minIOService; public PostingTaskService( @@ -30,11 +33,13 @@ public class PostingTaskService { MarketingStrategyRepository strategyRepository, SocialMediaCredentialsService credentialsService, FacebookPostingService facebookPostingService, + LinkedInPostingService linkedInPostingService, MinIOService minIOService) { this.taskRepository = taskRepository; this.strategyRepository = strategyRepository; this.credentialsService = credentialsService; this.facebookPostingService = facebookPostingService; + this.linkedInPostingService = linkedInPostingService; this.minIOService = minIOService; } @@ -194,6 +199,20 @@ public class PostingTaskService { task.getPostText(), task.getHashtags()); } + } else if ("linkedin".equalsIgnoreCase(task.getPlatform())) { + if (imageData != null && imageData.length > 0) { + postId = linkedInPostingService.postToLinkedInWithImage( + credentials, + task.getPostText(), + task.getHashtags(), + imageData, + "image/png"); + } else { + postId = linkedInPostingService.postToLinkedIn( + credentials, + task.getPostText(), + task.getHashtags()); + } } else { throw new UnsupportedOperationException("Platform not supported: " + task.getPlatform()); } @@ -206,6 +225,28 @@ 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); + } 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); } catch (Exception e) { logger.error("Failed to execute task {}", taskId, e);