This commit is contained in:
root
2025-12-05 19:39:51 +05:00
parent 894b226b27
commit 021e6e7c60
5 changed files with 617 additions and 33 deletions
@@ -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;
}
}
@@ -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;
}
}
@@ -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<String> 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<String> 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<String, Object> formData = new LinkedMultiValueMap<>();
formData.add("message", message);
formData.add("source", imageBuffer);
MultiValueMap<String, Object> 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);
}
}
}
@@ -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<String> 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<String> 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<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();
}
/**
* Проверяет, является ли ошибка истечением токена
* 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);
}
}
}
@@ -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);