feat: add Facebook Reels, Stories, and scheduled post support (#162)
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 10s
Universal: Auto Version Bump / Version Bump (push) Successful in 18s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 54s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 10s
Universal: Auto Version Bump / Version Bump (push) Successful in 18s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 54s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Authored-by: Moko Consulting
This commit is contained in:
@@ -14,6 +14,10 @@
|
||||
- **Threads polls**: Poll creation support via poll_options parameter (2-4 options)
|
||||
- **Threads spoiler tags**: Content warning / spoiler flag support for Threads posts
|
||||
- **Threads text-only optimization**: Simplified single-step flow for text-only posts without media
|
||||
- **Facebook Reels**: Publish video Reels via Graph API video_reels endpoint (#162)
|
||||
- **Facebook Stories**: Publish image and video Stories via photo_stories/video_stories endpoints (#162)
|
||||
- **Facebook scheduled posts**: Schedule feed posts with scheduled_publish_time parameter (#162)
|
||||
- **Facebook draft posts**: Save feed posts as unpublished drafts (#162)
|
||||
|
||||
### Fixed
|
||||
- Webservices plugin Joomla 6 compatibility — `onBeforeApiRoute` receives `BeforeApiRouteEvent` object, extract router via `$event->getRouter()`
|
||||
|
||||
@@ -18,22 +18,10 @@ use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossDeleteIn
|
||||
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
|
||||
use Joomla\Event\SubscriberInterface;
|
||||
|
||||
/**
|
||||
* Facebook/Meta service plugin for MokoSuiteCross.
|
||||
*
|
||||
* Supports two modes:
|
||||
* 1. Default MokoSuite App — pre-configured app credentials (hidden from admin UI)
|
||||
* 2. Custom App — user provides their own Facebook App ID and Page Access Token
|
||||
*
|
||||
* Credentials format:
|
||||
* {
|
||||
* "mode": "default" | "custom",
|
||||
* "page_access_token": "...", // Only for custom mode
|
||||
* "page_id": "..." // Required — Facebook Page ID
|
||||
* }
|
||||
*/
|
||||
class FacebookService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface, MokoSuiteCrossDeleteInterface
|
||||
{
|
||||
private const VIDEO_EXTENSIONS = ['mp4', 'mov', 'avi', 'wmv', 'webm'];
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
@@ -65,18 +53,84 @@ class FacebookService extends CMSPlugin implements SubscriberInterface, MokoSuit
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing token or page_id']];
|
||||
}
|
||||
|
||||
$contentType = $params['content_type'] ?? 'feed';
|
||||
|
||||
return match ($contentType) {
|
||||
'reel' => $this->publishReel($message, $media, $token, $pageId, $params),
|
||||
'story' => $this->publishStory($media, $token, $pageId),
|
||||
default => $this->publishFeed($message, $token, $pageId, $params),
|
||||
};
|
||||
}
|
||||
|
||||
private function publishFeed(string $message, string $token, string $pageId, array $params): array
|
||||
{
|
||||
$apiUrl = 'https://graph.facebook.com/v19.0/' . $pageId . '/feed';
|
||||
|
||||
$postData = [
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
// Attach link if provided in params
|
||||
if (!empty($params['link'])) {
|
||||
$postData['link'] = $params['link'];
|
||||
}
|
||||
|
||||
$ch = curl_init($apiUrl);
|
||||
if (!empty($params['scheduled_at'])) {
|
||||
$timestamp = is_numeric($params['scheduled_at'])
|
||||
? (int) $params['scheduled_at']
|
||||
: strtotime($params['scheduled_at']);
|
||||
|
||||
if ($timestamp !== false && $timestamp > 0) {
|
||||
$postData['scheduled_publish_time'] = $timestamp;
|
||||
$postData['published'] = 'false';
|
||||
}
|
||||
} elseif (!empty($params['draft'])) {
|
||||
$postData['published'] = 'false';
|
||||
}
|
||||
|
||||
return $this->apiPost($apiUrl, $postData, $token);
|
||||
}
|
||||
|
||||
private function publishReel(string $message, array $media, string $token, string $pageId, array $params): array
|
||||
{
|
||||
if (empty($media[0])) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Reel requires a video URL']];
|
||||
}
|
||||
|
||||
$apiUrl = 'https://graph.facebook.com/v19.0/' . $pageId . '/video_reels';
|
||||
|
||||
$postData = [
|
||||
'upload_phase' => 'finish',
|
||||
'video_url' => $media[0],
|
||||
'description' => $message,
|
||||
];
|
||||
|
||||
return $this->apiPost($apiUrl, $postData, $token);
|
||||
}
|
||||
|
||||
private function publishStory(array $media, string $token, string $pageId): array
|
||||
{
|
||||
if (empty($media[0])) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Story requires a media URL']];
|
||||
}
|
||||
|
||||
$mediaUrl = $media[0];
|
||||
$extension = strtolower(pathinfo(parse_url($mediaUrl, PHP_URL_PATH) ?: $mediaUrl, PATHINFO_EXTENSION));
|
||||
$isVideo = in_array($extension, self::VIDEO_EXTENSIONS, true);
|
||||
|
||||
if ($isVideo) {
|
||||
$apiUrl = 'https://graph.facebook.com/v19.0/' . $pageId . '/video_stories';
|
||||
$postData = ['video_url' => $mediaUrl];
|
||||
} else {
|
||||
$apiUrl = 'https://graph.facebook.com/v19.0/' . $pageId . '/photo_stories';
|
||||
$postData = ['photo_url' => $mediaUrl];
|
||||
}
|
||||
|
||||
return $this->apiPost($apiUrl, $postData, $token);
|
||||
}
|
||||
|
||||
private function apiPost(string $url, array $postData, string $token): array
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($postData),
|
||||
@@ -88,20 +142,18 @@ class FacebookService extends CMSPlugin implements SubscriberInterface, MokoSuit
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
|
||||
$curlError = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]];
|
||||
|
||||
}
|
||||
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$data = json_decode($response, true) ?: [];
|
||||
|
||||
if ($httpCode === 200 && !empty($data['id'])) {
|
||||
if ($httpCode >= 200 && $httpCode < 300 && !empty($data['id'])) {
|
||||
return ['success' => true, 'platform_post_id' => $data['id'], 'response' => $data];
|
||||
}
|
||||
|
||||
@@ -143,7 +195,7 @@ class FacebookService extends CMSPlugin implements SubscriberInterface, MokoSuit
|
||||
|
||||
public function getMaxLength(): int
|
||||
{
|
||||
return 0; // No practical limit
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function supportsMedia(): bool
|
||||
|
||||
Reference in New Issue
Block a user