.
This commit is contained in:
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user