diff --git a/Technical Task: Fix Facebook Graph API Image Upload (403 Forbidden).md b/Technical Task: Fix Facebook Graph API Image Upload (403 Forbidden).md new file mode 100644 index 0000000..cc4807e --- /dev/null +++ b/Technical Task: Fix Facebook Graph API Image Upload (403 Forbidden).md @@ -0,0 +1,69 @@ +### Technical Task: Fix Facebook Graph API Image Upload (403 Forbidden) + +**Objective:** +Modify `FacebookPostingService.java` to resolve the `403 Forbidden` error caused by using a User Access Token for image uploads. Implement a mechanism to automatically exchange the User Token for a Page Access Token before attempting the upload. + +**Context:** +The application is currently failing with this error: +`(#200) This endpoint is deprecated since the required permission publish_actions is deprecated` + +This happens because we are trying to post to `/{userId}/photos` or using a User Token on a Page endpoint. Facebook requires a **Page Access Token** to post content. The user has valid permissions (`pages_read_engagement`, `pages_manage_posts`), but the code is not fetching the correct token type. + +**Required Changes:** + +1. **Implement Token Exchange Method:** + Create a helper method `getPageCredentials(String userAccessToken)` that: + + - Calls `GET /me/accounts?access_token={userToken}`. + - Parses the JSON response to find the first available Page. + - Extracts the `id` (Page ID) and `access_token` (Page Token). + - Returns these credentials. + +2. **Update `postToFacebookWithImage`:** + + - Remove the logic that calls `getPageId` (which only returns the User ID or Page ID without a token). + - Instead, call the new `getPageCredentials`. + - Use the returned **Page Token** and **Page ID** for the subsequent `/photos` upload request. + +3. **Refactor `publishPostWithImage`:** + + - Ensure the `access_token` query parameter uses the **Page Token**, not the User Token passed into the service originally. + - Keep the `ByteArrayResource` filename override (critical for multipart uploads). + +**Code Reference (Implementation Details):** + +Please replace the existing logic in `FacebookPostingService.java` with the following flow: + +```java +// 1. New Helper Class +private static class PageCredentials { + String pageId; + String pageToken; + public PageCredentials(String pageId, String pageToken) { + this.pageId = pageId; + this.pageToken = pageToken; + } +} + +// 2. New Method to fetch Page Token +private PageCredentials getPageCredentials(String userAccessToken) { + // Call https://graph.facebook.com/v18.0/me/accounts + // Return the first page found in the "data" array +} + +// 3. Updated Main Method +public String postToFacebookWithImage(String userAccessToken, ...) { + // Step A: Exchange tokens + PageCredentials creds = getPageCredentials(userAccessToken); + + // Step B: Upload using creds.pageToken and creds.pageId + return publishPostWithImage(creds.pageToken, creds.pageId, ...); +} +``` + +**Acceptance Criteria:** + +- The service must successfully fetch a Page Access Token using the provided User Access Token. +- The image upload request must target `/{pageId}/photos`. +- The upload request must use the **Page Access Token**. +- The `403 Forbidden` error regarding `publish_actions` must no longer appear. diff --git a/src/main/java/kz/konturai/parser/service/FacebookPostingService.java b/src/main/java/kz/konturai/parser/service/FacebookPostingService.java index c5ef20c..3e0c3b5 100644 --- a/src/main/java/kz/konturai/parser/service/FacebookPostingService.java +++ b/src/main/java/kz/konturai/parser/service/FacebookPostingService.java @@ -84,6 +84,87 @@ public class FacebookPostingService { } } + /** + * Helper class to hold page credentials (Page ID and Page Access Token) + */ + private static class PageCredentials { + String pageId; + String pageToken; + + public PageCredentials(String pageId, String pageToken) { + this.pageId = pageId; + this.pageToken = pageToken; + } + } + + /** + * Получает Page Access Token и Page ID из User Access Token + * Вызывает /me/accounts для получения списка страниц, которыми управляет + * пользователь + * + * @param userAccessToken User Access Token + * @return PageCredentials с Page ID и Page Access Token + * @throws FacebookTokenExpiredException если токен истек + * @throws RuntimeException если не удалось получить credentials + * страницы + */ + private PageCredentials getPageCredentials(String userAccessToken) { + try { + String response = webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/me/accounts") + .queryParam("access_token", userAccessToken) + .build()) + .retrieve() + .bodyToMono(String.class) + .block(Duration.ofMillis(timeoutMs)); + + JsonNode jsonNode = objectMapper.readTree(response); + + // Проверяем наличие массива data + if (!jsonNode.has("data") || !jsonNode.get("data").isArray()) { + logger.error("Facebook API response does not contain 'data' array"); + throw new RuntimeException("Failed to get page credentials: no pages found in response"); + } + + JsonNode dataArray = jsonNode.get("data"); + if (dataArray.size() == 0) { + logger.error("User has no pages available"); + throw new RuntimeException("Failed to get page credentials: user has no pages"); + } + + // Берем первую страницу из списка + JsonNode firstPage = dataArray.get(0); + String pageId = firstPage.has("id") ? firstPage.get("id").asText() : null; + String pageToken = firstPage.has("access_token") ? firstPage.get("access_token").asText() : null; + + if (pageId == null || pageToken == null) { + logger.error("Page credentials incomplete: pageId={}, pageToken={}", pageId, + pageToken != null ? "present" : "null"); + throw new RuntimeException("Failed to get page credentials: incomplete page data"); + } + + logger.info("Successfully retrieved page credentials for page ID: {}", pageId); + return new PageCredentials(pageId, pageToken); + + } catch (WebClientResponseException e) { + if (isTokenExpiredError(e)) { + throw parseFacebookError(e); + } + logger.error("Failed to get Facebook page credentials: {} - {}", e.getStatusCode(), + e.getResponseBodyAsString()); + 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; + } catch (Exception e) { + logger.error("Unexpected error getting page credentials", e); + throw new RuntimeException("Failed to get page credentials", e); + } + } + /** * Получает ID страницы пользователя */ @@ -169,11 +250,12 @@ public class FacebookPostingService { // Формируем полный текст поста с хештегами String fullPostText = buildPostText(postText, hashtags); - // Получаем ID страницы пользователя (me) - String pageId = getPageId(accessToken); + // Получаем Page Access Token и Page ID из User Access Token + PageCredentials creds = getPageCredentials(accessToken); - // Загружаем изображение и публикуем пост - String postId = publishPostWithImage(accessToken, pageId, fullPostText, imageData, imageContentType); + // Загружаем изображение и публикуем пост используя 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;