This commit is contained in:
root
2025-12-09 22:30:01 +05:00
parent 7f77f48020
commit 734ebb9d40
2 changed files with 237 additions and 0 deletions
@@ -0,0 +1,19 @@
package kz.konturai.parser.service;
/**
* Service interface for uploading images to storage and returning public URLs.
* This is required by Instagram API which only accepts public image URLs, not
* byte arrays.
*/
public interface ImageStorageService {
/**
* Uploads an image to storage and returns a public URL.
*
* @param imageBytes The image data as byte array
* @param filename The filename for the image (e.g., "image.jpg")
* @return Public URL to the uploaded image
* @throws RuntimeException if upload fails
*/
String uploadImage(byte[] imageBytes, String filename);
}
@@ -0,0 +1,218 @@
package kz.konturai.parser.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.stereotype.Service;
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;
@Service
public class InstagramPostingService {
private static final Logger logger = LoggerFactory.getLogger(InstagramPostingService.class);
private static final String FACEBOOK_GRAPH_API_BASE = "https://graph.facebook.com/v18.0";
private final WebClient webClient;
private final ObjectMapper objectMapper;
private final ImageStorageService imageStorageService;
@Value("${facebook.api.timeout:30000}")
private int timeoutMs;
public InstagramPostingService(ImageStorageService imageStorageService) {
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofMillis(30000));
this.webClient = WebClient.builder()
.baseUrl(FACEBOOK_GRAPH_API_BASE)
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
this.objectMapper = new ObjectMapper();
this.imageStorageService = imageStorageService;
}
/**
* Publishes an image post to Instagram Business account via Graph API.
*
* This method implements the 3-step Instagram publishing workflow:
* 1. Get Instagram Business Account ID from Facebook Page
* 2. Create Media Container with image URL
* 3. Publish the media container
*
* @param pageAccessToken Facebook Page Access Token (must have instagram_basic
* permission)
* @param caption The caption for the Instagram post
* @param imageBytes The image data as byte array
* @return ID of the published Instagram post
* @throws RuntimeException if posting fails at any step
*/
public String postToInstagram(String pageAccessToken, String caption, byte[] imageBytes) {
try {
// Step 1: Get Instagram Business Account ID
String igUserId = getInstagramBusinessAccountId(pageAccessToken);
logger.info("Retrieved Instagram Business Account ID: {}", igUserId);
// Upload image to storage and get public URL
String imageUrl = imageStorageService.uploadImage(imageBytes, "instagram-image.jpg");
logger.info("Image uploaded to storage, URL: {}", imageUrl);
// Step 2: Create Media Container
String creationId = createMediaContainer(igUserId, imageUrl, caption, pageAccessToken);
logger.info("Media container created with ID: {}", creationId);
// Step 3: Publish Media
String postId = publishMedia(igUserId, creationId, pageAccessToken);
logger.info("Successfully posted to Instagram. Post ID: {}", postId);
return postId;
} catch (WebClientResponseException e) {
logger.error("Instagram API error: {} - {}", e.getStatusCode(), e.getResponseBodyAsString());
throw new RuntimeException("Failed to post to Instagram: " + e.getMessage(), e);
} catch (Exception e) {
logger.error("Unexpected error posting to Instagram", e);
throw new RuntimeException("Failed to post to Instagram", e);
}
}
/**
* Step 1: Gets the Instagram Business Account ID linked to the Facebook Page.
*
* @param pageAccessToken Facebook Page Access Token
* @return Instagram Business Account ID
* @throws RuntimeException if the account ID cannot be retrieved
*/
private String getInstagramBusinessAccountId(String pageAccessToken) {
try {
String response = webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/me")
.queryParam("access_token", pageAccessToken)
.queryParam("fields", "instagram_business_account")
.build())
.retrieve()
.bodyToMono(String.class)
.block(Duration.ofMillis(timeoutMs));
JsonNode root = objectMapper.readTree(response);
JsonNode instagramAccount = root.get("instagram_business_account");
if (instagramAccount == null || !instagramAccount.has("id")) {
throw new RuntimeException(
"Instagram Business Account is not linked to this Facebook Page. " +
"Please link your Instagram Business account to the Facebook Page first.");
}
return instagramAccount.get("id").asText();
} catch (WebClientResponseException e) {
String responseBody = e.getResponseBodyAsString();
logger.error("Failed to get Instagram Business Account ID: {} - {}",
e.getStatusCode(), responseBody != null ? responseBody : "No response body");
throw new RuntimeException("Failed to get Instagram Business Account ID: " + e.getMessage(), e);
} catch (JsonProcessingException e) {
logger.error("Failed to parse Instagram Business Account ID response", e);
throw new RuntimeException("Failed to parse Instagram Business Account ID response", e);
} catch (Exception e) {
logger.error("Failed to get Instagram Business Account ID", e);
throw new RuntimeException("Failed to get Instagram Business Account ID: " + e.getMessage(), e);
}
}
/**
* Step 2: Creates a media container for the Instagram post.
*
* @param igUserId Instagram Business Account ID
* @param imageUrl Public URL of the image (must be accessible by Instagram)
* @param caption Caption for the post
* @param accessToken Page Access Token
* @return Creation ID (media container ID)
* @throws RuntimeException if media container creation fails
*/
private String createMediaContainer(String igUserId, String imageUrl, String caption, String accessToken) {
try {
String response = webClient.post()
.uri(uriBuilder -> uriBuilder
.path("/{igUserId}/media")
.queryParam("access_token", accessToken)
.queryParam("image_url", imageUrl)
.queryParam("caption", caption != null ? caption : "")
.build(igUserId))
.retrieve()
.bodyToMono(String.class)
.block(Duration.ofMillis(timeoutMs));
JsonNode jsonNode = objectMapper.readTree(response);
if (!jsonNode.has("id")) {
throw new RuntimeException("Media container creation response missing 'id' field");
}
return jsonNode.get("id").asText();
} catch (WebClientResponseException e) {
String responseBody = e.getResponseBodyAsString();
logger.error("Failed to create Instagram media container: {} - {}",
e.getStatusCode(), responseBody != null ? responseBody : "No response body");
throw new RuntimeException("Failed to create media container: " + e.getMessage(), e);
} catch (JsonProcessingException e) {
logger.error("Failed to parse media container creation response", e);
throw new RuntimeException("Failed to parse media container creation response", e);
} catch (Exception e) {
logger.error("Unexpected error creating media container", e);
throw new RuntimeException("Failed to create media container: " + e.getMessage(), e);
}
}
/**
* Step 3: Publishes the media container to Instagram.
*
* @param igUserId Instagram Business Account ID
* @param creationId The creation ID from Step 2
* @param accessToken Page Access Token
* @return Final Instagram post ID
* @throws RuntimeException if publishing fails
*/
private String publishMedia(String igUserId, String creationId, String accessToken) {
try {
String response = webClient.post()
.uri(uriBuilder -> uriBuilder
.path("/{igUserId}/media_publish")
.queryParam("access_token", accessToken)
.queryParam("creation_id", creationId)
.build(igUserId))
.retrieve()
.bodyToMono(String.class)
.block(Duration.ofMillis(timeoutMs));
JsonNode jsonNode = objectMapper.readTree(response);
if (!jsonNode.has("id")) {
throw new RuntimeException("Media publish response missing 'id' field");
}
return jsonNode.get("id").asText();
} catch (WebClientResponseException e) {
String responseBody = e.getResponseBodyAsString();
logger.error("Failed to publish Instagram media: {} - {}",
e.getStatusCode(), responseBody != null ? responseBody : "No response body");
throw new RuntimeException("Failed to publish media: " + e.getMessage(), e);
} catch (JsonProcessingException e) {
logger.error("Failed to parse media publish response", e);
throw new RuntimeException("Failed to parse media publish response", e);
} catch (Exception e) {
logger.error("Unexpected error publishing media", e);
throw new RuntimeException("Failed to publish media: " + e.getMessage(), e);
}
}
}