New plugins: - plg_mokosuitecross_instagram: Meta Content Publishing API (2-step flow) - plg_mokosuitecross_youtube: YouTube Data API v3 channel bulletins Bug fixes (re-applied after rebase loss): - ConvertKit/Brevo/ConstantContact: duplicate curl_setopt_array removed - Mailchimp: campaign creation accepts 2xx range (not just 200) - Medium: getUserId() returns '' on error (not array) - Bluesky: sha256 instead of md5 for cache key - ServiceController: generic error message instead of exception details Closes #140, closes #141
This commit is contained in:
@@ -1,6 +1,19 @@
|
||||
# Changelog
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Instagram plugin**: Cross-post to Instagram via Meta Content Publishing API (2-step container flow)
|
||||
- **YouTube plugin**: Cross-post to YouTube via Data API v3 channel bulletins
|
||||
|
||||
### Fixed
|
||||
- **ConvertKit**: Removed duplicate curl_setopt_array with undefined $token
|
||||
- **Brevo**: Removed duplicate curl_setopt_array with undefined $token and wrong auth header
|
||||
- **Constant Contact**: Removed duplicate curl_setopt_array
|
||||
- **Mailchimp**: Fixed campaign creation checking HTTP 200 instead of 2xx range
|
||||
- **Medium**: Fixed getUserId() returning array instead of string on error
|
||||
- **Bluesky**: Replaced md5() with hash('sha256', ...) for cache key
|
||||
- **ServiceController**: Exception details no longer exposed to client
|
||||
|
||||
## [01.04.01] --- 2026-06-21
|
||||
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ class ServiceController extends FormController
|
||||
$app->mimeType = 'application/json';
|
||||
$app->setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
|
||||
echo new JsonResponse($e);
|
||||
echo new JsonResponse(['error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
$app->close();
|
||||
|
||||
@@ -127,7 +127,7 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuite
|
||||
|
||||
private function authenticateWithCache(string $pds, string $handle, string $appPwd): array
|
||||
{
|
||||
$cacheKey = md5($pds . $handle);
|
||||
$cacheKey = hash('sha256', $pds . $handle);
|
||||
|
||||
if (isset(self::$sessionCache[$cacheKey])) {
|
||||
$cached = self::$sessionCache[$cacheKey];
|
||||
|
||||
@@ -70,15 +70,6 @@ class BrevoService extends CMSPlugin implements SubscriberInterface, MokoSuiteCr
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => 'https://api.brevo.com/v3/emailCampaigns',
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $postData,
|
||||
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
|
||||
-9
@@ -73,15 +73,6 @@ class ConstantcontactService extends CMSPlugin implements SubscriberInterface, M
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => 'https://api.cc.email/v3/emails',
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $postData,
|
||||
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
|
||||
@@ -66,15 +66,6 @@ class ConvertkitService extends CMSPlugin implements SubscriberInterface, MokoSu
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => 'https://api.convertkit.com/v3/broadcasts',
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $postData,
|
||||
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><title></title>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteCross
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Instagram</name>
|
||||
<version>01.04.02-dev</version>
|
||||
<creationDate>2026-06-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<description>PLG_MOKOSUITECROSS_INSTAGRAM_DESCRIPTION</description>
|
||||
|
||||
<namespace path="src">Joomla\Plugin\MokoSuiteCross\Instagram</namespace>
|
||||
|
||||
<files>
|
||||
<filename plugin="instagram">instagram.php</filename>
|
||||
<folder>src</folder>
|
||||
<folder>services</folder>
|
||||
<folder>language</folder>
|
||||
</files>
|
||||
|
||||
<languages>
|
||||
<language tag="en-GB">language/en-GB/plg_mokosuitecross_instagram.ini</language>
|
||||
<language tag="en-GB">language/en-GB/plg_mokosuitecross_instagram.sys.ini</language>
|
||||
</languages>
|
||||
<config>
|
||||
<fields name="params">
|
||||
<fieldset name="basic" label="PLG_MOKOSUITECROSS_INSTAGRAM_FIELDSET_DEFAULTS">
|
||||
<field
|
||||
name="default_webhook_url"
|
||||
type="url"
|
||||
label="PLG_MOKOSUITECROSS_INSTAGRAM_DEFAULT_WEBHOOK"
|
||||
description="PLG_MOKOSUITECROSS_INSTAGRAM_DEFAULT_WEBHOOK_DESC"
|
||||
size="60"
|
||||
/>
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
</extension>
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><title></title>
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
PLG_MOKOSUITECROSS_INSTAGRAM="MokoSuiteCross - Instagram"
|
||||
PLG_MOKOSUITECROSS_INSTAGRAM_DESCRIPTION="Cross-post Joomla articles to Instagram via Meta Content Publishing API."
|
||||
PLG_MOKOSUITECROSS_INSTAGRAM_FIELDSET_DEFAULTS="Default Settings"
|
||||
PLG_MOKOSUITECROSS_INSTAGRAM_DEFAULT_WEBHOOK="Default Webhook URL"
|
||||
PLG_MOKOSUITECROSS_INSTAGRAM_DEFAULT_WEBHOOK_DESC="Pre-configured MokoSuite webhook URL. Services using default mode will use this URL."
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
PLG_MOKOSUITECROSS_INSTAGRAM="MokoSuiteCross - Instagram"
|
||||
PLG_MOKOSUITECROSS_INSTAGRAM_DESCRIPTION="Cross-post Joomla articles to Instagram via Meta Content Publishing API."
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><title></title>
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><title></title>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteCross
|
||||
* @subpackage plg_mokosuitecross_instagram
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Extension\PluginInterface;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\DI\Container;
|
||||
use Joomla\DI\ServiceProviderInterface;
|
||||
use Joomla\Event\DispatcherInterface;
|
||||
use Joomla\Plugin\MokoSuiteCross\Instagram\Extension\InstagramService;
|
||||
|
||||
return new class () implements ServiceProviderInterface {
|
||||
public function register(Container $container): void
|
||||
{
|
||||
$container->set(
|
||||
PluginInterface::class,
|
||||
function (Container $container) {
|
||||
$plugin = new InstagramService(
|
||||
$container->get(DispatcherInterface::class),
|
||||
(array) PluginHelper::getPlugin('mokosuitecross', 'instagram')
|
||||
);
|
||||
$plugin->setApplication(Factory::getApplication());
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteCross
|
||||
* @subpackage plg_mokosuitecross_instagram
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace Joomla\Plugin\MokoSuiteCross\Instagram\Extension;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
|
||||
use Joomla\Event\SubscriberInterface;
|
||||
|
||||
/**
|
||||
* Instagram service plugin for MokoSuiteCross.
|
||||
*
|
||||
* Uses the Meta Content Publishing API — a 2-step flow:
|
||||
* 1. Create a media container via POST /{ig_user_id}/media
|
||||
* 2. Publish the container via POST /{ig_user_id}/media_publish
|
||||
*/
|
||||
class InstagramService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices'];
|
||||
}
|
||||
|
||||
public function onMokoSuiteCrossGetServices(&$services): void
|
||||
{
|
||||
$services[] = $this;
|
||||
}
|
||||
|
||||
public function getServiceType(): string { return 'instagram'; }
|
||||
public function getServiceName(): string { return 'Instagram'; }
|
||||
public function getMaxLength(): int { return 2200; }
|
||||
public function supportsMedia(): bool { return true; }
|
||||
|
||||
public function publish(string $message, array $media, array $credentials, array $params): array
|
||||
{
|
||||
$token = $this->resolveCredential($credentials, 'access_token');
|
||||
$accountId = $credentials['instagram_account_id'] ?? '';
|
||||
|
||||
if (empty($token) || empty($accountId)) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or Instagram account ID.']];
|
||||
}
|
||||
|
||||
// Step 1: Create media container
|
||||
$containerUrl = 'https://graph.facebook.com/v19.0/' . urlencode($accountId) . '/media';
|
||||
$containerData = [
|
||||
'caption' => mb_substr($message, 0, 2200),
|
||||
'access_token' => $token,
|
||||
];
|
||||
|
||||
// Attach image if provided
|
||||
if (!empty($media[0])) {
|
||||
$containerData['image_url'] = $media[0];
|
||||
} else {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Instagram requires at least one image or video.']];
|
||||
}
|
||||
|
||||
$ch = curl_init($containerUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($containerData),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
|
||||
$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 || $httpCode >= 300 || empty($data['id'])) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
|
||||
}
|
||||
|
||||
$containerId = $data['id'];
|
||||
|
||||
// Step 2: Publish the container
|
||||
$publishUrl = 'https://graph.facebook.com/v19.0/' . urlencode($accountId) . '/media_publish';
|
||||
$publishData = [
|
||||
'creation_id' => $containerId,
|
||||
'access_token' => $token,
|
||||
];
|
||||
|
||||
$ch = curl_init($publishUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($publishData),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
|
||||
$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 && $httpCode < 300 && !empty($data['id'])) {
|
||||
return ['success' => true, 'platform_post_id' => (string) $data['id'], 'response' => $data];
|
||||
}
|
||||
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
|
||||
}
|
||||
|
||||
public function validateCredentials(array $credentials): array
|
||||
{
|
||||
$token = $this->resolveCredential($credentials, 'access_token');
|
||||
$accountId = $credentials['instagram_account_id'] ?? '';
|
||||
|
||||
if (empty($token) || empty($accountId)) {
|
||||
return ['valid' => false, 'message' => 'Access token and Instagram account ID are required.', 'account_name' => ''];
|
||||
}
|
||||
|
||||
$ch = curl_init('https://graph.facebook.com/v19.0/me?fields=id,username&access_token=' . urlencode($token));
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
|
||||
$curlError = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => ''];
|
||||
|
||||
}
|
||||
curl_close($ch);
|
||||
|
||||
$data = json_decode($response, true) ?: [];
|
||||
|
||||
if (!empty($data['id'])) {
|
||||
$name = $data['username'] ?? $data['id'];
|
||||
return ['valid' => true, 'message' => 'Connected', 'account_name' => '@' . $name];
|
||||
}
|
||||
|
||||
return ['valid' => false, 'message' => $data['error']['message'] ?? 'Failed to verify credentials.', 'account_name' => ''];
|
||||
}
|
||||
|
||||
private function resolveCredential(array $credentials, string $key): string
|
||||
{
|
||||
$mode = $credentials['mode'] ?? 'default';
|
||||
|
||||
if ($mode === 'custom') {
|
||||
return $credentials[$key] ?? '';
|
||||
}
|
||||
|
||||
return $this->params->get('default_' . $key, '');
|
||||
}
|
||||
|
||||
public function getSupportedMediaTypes(): array
|
||||
{
|
||||
return ['image', 'video'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><title></title>
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><title></title>
|
||||
@@ -95,7 +95,7 @@ class MailchimpService extends CMSPlugin implements SubscriberInterface, MokoSui
|
||||
|
||||
$data = json_decode($response, true) ?: [];
|
||||
|
||||
if ($httpCode !== 200 || empty($data['id'])) {
|
||||
if ($httpCode < 200 || $httpCode >= 300 || empty($data['id'])) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ class MediumService extends CMSPlugin implements SubscriberInterface, MokoSuiteC
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => ''];
|
||||
return '';
|
||||
|
||||
}
|
||||
curl_close($ch);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><title></title>
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><title></title>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
PLG_MOKOSUITECROSS_YOUTUBE="MokoSuiteCross - YouTube"
|
||||
PLG_MOKOSUITECROSS_YOUTUBE_DESCRIPTION="Cross-post Joomla articles to YouTube community posts."
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
PLG_MOKOSUITECROSS_YOUTUBE="MokoSuiteCross - YouTube"
|
||||
PLG_MOKOSUITECROSS_YOUTUBE_DESCRIPTION="Cross-post Joomla articles to YouTube community posts."
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><title></title>
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><title></title>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteCross
|
||||
* @subpackage plg_mokosuitecross_youtube
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Extension\PluginInterface;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\DI\Container;
|
||||
use Joomla\DI\ServiceProviderInterface;
|
||||
use Joomla\Event\DispatcherInterface;
|
||||
use Joomla\Plugin\MokoSuiteCross\Youtube\Extension\YoutubeService;
|
||||
|
||||
return new class () implements ServiceProviderInterface {
|
||||
public function register(Container $container): void
|
||||
{
|
||||
$container->set(
|
||||
PluginInterface::class,
|
||||
function (Container $container) {
|
||||
$plugin = new YoutubeService(
|
||||
$container->get(DispatcherInterface::class),
|
||||
(array) PluginHelper::getPlugin('mokosuitecross', 'youtube')
|
||||
);
|
||||
$plugin->setApplication(Factory::getApplication());
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteCross
|
||||
* @subpackage plg_mokosuitecross_youtube
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace Joomla\Plugin\MokoSuiteCross\Youtube\Extension;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface;
|
||||
use Joomla\Event\SubscriberInterface;
|
||||
|
||||
/**
|
||||
* YouTube service plugin for MokoSuiteCross.
|
||||
*
|
||||
* Posts to YouTube via the Data API v3 channel bulletins.
|
||||
*
|
||||
* Credentials:
|
||||
* access_token - OAuth 2.0 token with youtube.force-ssl scope
|
||||
* channel_id - YouTube channel ID
|
||||
*/
|
||||
class YoutubeService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices'];
|
||||
}
|
||||
|
||||
public function onMokoSuiteCrossGetServices(&$services): void
|
||||
{
|
||||
$services[] = $this;
|
||||
}
|
||||
|
||||
public function getServiceType(): string { return 'youtube'; }
|
||||
public function getServiceName(): string { return 'YouTube'; }
|
||||
public function getMaxLength(): int { return 5000; }
|
||||
public function supportsMedia(): bool { return true; }
|
||||
|
||||
public function publish(string $message, array $media, array $credentials, array $params): array
|
||||
{
|
||||
$token = $credentials['access_token'] ?? '';
|
||||
$channelId = $credentials['channel_id'] ?? '';
|
||||
|
||||
if (empty($token) || empty($channelId)) {
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or channel ID']];
|
||||
}
|
||||
|
||||
$postData = json_encode([
|
||||
'snippet' => [
|
||||
'channelId' => $channelId,
|
||||
'description' => $message,
|
||||
],
|
||||
'contentDetails' => [
|
||||
'bulletin' => [
|
||||
'resourceId' => [
|
||||
'kind' => 'youtube#channel',
|
||||
'channelId' => $channelId,
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => 'https://www.googleapis.com/youtube/v3/activities?part=snippet,contentDetails',
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $postData,
|
||||
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
|
||||
$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 && $httpCode < 300) {
|
||||
return ['success' => true, 'platform_post_id' => $data['id'] ?? '', 'response' => $data];
|
||||
}
|
||||
|
||||
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
|
||||
}
|
||||
|
||||
public function validateCredentials(array $credentials): array
|
||||
{
|
||||
$token = $credentials['access_token'] ?? '';
|
||||
|
||||
if (empty($token)) {
|
||||
return ['valid' => false, 'message' => 'Missing access token', 'account_name' => ''];
|
||||
}
|
||||
|
||||
$ch = curl_init('https://www.googleapis.com/youtube/v3/channels?part=snippet&mine=true');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => ''];
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
$data = json_decode($response, true) ?: [];
|
||||
|
||||
if (!empty($data['items'][0]['snippet']['title'])) {
|
||||
return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['items'][0]['snippet']['title']];
|
||||
}
|
||||
|
||||
return ['valid' => false, 'message' => 'Invalid token or no channel found', 'account_name' => ''];
|
||||
}
|
||||
|
||||
public function getSupportedMediaTypes(): array
|
||||
{
|
||||
return ['image', 'video'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><title></title>
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><title></title>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteCross
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="mokosuitecross" method="upgrade">
|
||||
<name>MokoSuiteCross - Youtube</name>
|
||||
<version>01.04.02-dev</version>
|
||||
<creationDate>2026-06-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<description>PLG_MOKOSUITECROSS_YOUTUBE_DESCRIPTION</description>
|
||||
|
||||
<namespace path="src">Joomla\Plugin\MokoSuiteCross\Youtube</namespace>
|
||||
|
||||
<files>
|
||||
<filename plugin="youtube">youtube.php</filename>
|
||||
<folder>src</folder>
|
||||
<folder>services</folder>
|
||||
<folder>language</folder>
|
||||
</files>
|
||||
|
||||
<languages>
|
||||
<language tag="en-GB">language/en-GB/plg_mokosuitecross_youtube.ini</language>
|
||||
<language tag="en-GB">language/en-GB/plg_mokosuitecross_youtube.sys.ini</language>
|
||||
</languages>
|
||||
<config>
|
||||
<fields name="params">
|
||||
<fieldset name="basic" label="PLG_MOKOSUITECROSS_YOUTUBE_FIELDSET_DEFAULTS">
|
||||
<field
|
||||
name="default_webhook_url"
|
||||
type="url"
|
||||
label="PLG_MOKOSUITECROSS_YOUTUBE_DEFAULT_WEBHOOK"
|
||||
description="PLG_MOKOSUITECROSS_YOUTUBE_DEFAULT_WEBHOOK_DESC"
|
||||
size="60"
|
||||
/>
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
</extension>
|
||||
@@ -60,6 +60,8 @@
|
||||
<file type="plugin" id="tiktok" group="mokosuitecross">plg_mokosuitecross_tiktok.zip</file>
|
||||
<file type="plugin" id="mokosuitecalendar" group="mokosuitecross">plg_mokosuitecross_mokosuitecalendar.zip</file>
|
||||
<file type="plugin" id="mokosuitegallery" group="mokosuitecross">plg_mokosuitecross_mokosuitegallery.zip</file>
|
||||
<file type="plugin" id="instagram" group="mokosuitecross">plg_mokosuitecross_instagram.zip</file>
|
||||
<file type="plugin" id="youtube" group="mokosuitecross">plg_mokosuitecross_youtube.zip</file>
|
||||
|
||||
<!-- Content Source Plugins (system group) -->
|
||||
<file type="plugin" id="mokosuitecross_events" group="system">plg_system_mokosuitecross_events.zip</file>
|
||||
|
||||
Reference in New Issue
Block a user