diff --git a/CHANGELOG.md b/CHANGELOG.md
index 75e62211..e168f594 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/source/packages/com_mokosuitecross/src/Controller/ServiceController.php b/source/packages/com_mokosuitecross/src/Controller/ServiceController.php
index 882c13a7..ecdd833c 100644
--- a/source/packages/com_mokosuitecross/src/Controller/ServiceController.php
+++ b/source/packages/com_mokosuitecross/src/Controller/ServiceController.php
@@ -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();
diff --git a/source/packages/plg_mokosuitecross_bluesky/src/Extension/BlueskyService.php b/source/packages/plg_mokosuitecross_bluesky/src/Extension/BlueskyService.php
index 7efb369a..01d1a298 100644
--- a/source/packages/plg_mokosuitecross_bluesky/src/Extension/BlueskyService.php
+++ b/source/packages/plg_mokosuitecross_bluesky/src/Extension/BlueskyService.php
@@ -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];
diff --git a/source/packages/plg_mokosuitecross_brevo/src/Extension/BrevoService.php b/source/packages/plg_mokosuitecross_brevo/src/Extension/BrevoService.php
index fbf55f30..42beb732 100644
--- a/source/packages/plg_mokosuitecross_brevo/src/Extension/BrevoService.php
+++ b/source/packages/plg_mokosuitecross_brevo/src/Extension/BrevoService.php
@@ -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) {
diff --git a/source/packages/plg_mokosuitecross_constantcontact/src/Extension/ConstantcontactService.php b/source/packages/plg_mokosuitecross_constantcontact/src/Extension/ConstantcontactService.php
index e3ae973b..eb38c64c 100644
--- a/source/packages/plg_mokosuitecross_constantcontact/src/Extension/ConstantcontactService.php
+++ b/source/packages/plg_mokosuitecross_constantcontact/src/Extension/ConstantcontactService.php
@@ -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) {
diff --git a/source/packages/plg_mokosuitecross_convertkit/src/Extension/ConvertkitService.php b/source/packages/plg_mokosuitecross_convertkit/src/Extension/ConvertkitService.php
index 2a296f21..d2a48cfe 100644
--- a/source/packages/plg_mokosuitecross_convertkit/src/Extension/ConvertkitService.php
+++ b/source/packages/plg_mokosuitecross_convertkit/src/Extension/ConvertkitService.php
@@ -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) {
diff --git a/source/packages/plg_mokosuitecross_instagram/index.html b/source/packages/plg_mokosuitecross_instagram/index.html
new file mode 100644
index 00000000..2efb97f3
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_instagram/index.html
@@ -0,0 +1 @@
+
diff --git a/source/packages/plg_mokosuitecross_instagram/instagram.php b/source/packages/plg_mokosuitecross_instagram/instagram.php
new file mode 100644
index 00000000..9b76408c
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_instagram/instagram.php
@@ -0,0 +1,11 @@
+
+ * @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;
diff --git a/source/packages/plg_mokosuitecross_instagram/instagram.xml b/source/packages/plg_mokosuitecross_instagram/instagram.xml
new file mode 100644
index 00000000..505c8409
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_instagram/instagram.xml
@@ -0,0 +1,39 @@
+
+
+ MokoSuiteCross - Instagram
+ 01.04.02-dev
+ 2026-06-23
+ Moko Consulting
+ hello@mokoconsulting.tech
+ https://mokoconsulting.tech
+ Copyright (C) 2026 Moko Consulting. All rights reserved.
+ GPL-3.0-or-later
+ PLG_MOKOSUITECROSS_INSTAGRAM_DESCRIPTION
+
+ Joomla\Plugin\MokoSuiteCross\Instagram
+
+
+ instagram.php
+ src
+ services
+ language
+
+
+
+ language/en-GB/plg_mokosuitecross_instagram.ini
+ language/en-GB/plg_mokosuitecross_instagram.sys.ini
+
+
+
+
+
+
+
diff --git a/source/packages/plg_mokosuitecross_instagram/language/en-GB/index.html b/source/packages/plg_mokosuitecross_instagram/language/en-GB/index.html
new file mode 100644
index 00000000..2efb97f3
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_instagram/language/en-GB/index.html
@@ -0,0 +1 @@
+
diff --git a/source/packages/plg_mokosuitecross_instagram/language/en-GB/plg_mokosuitecross_instagram.ini b/source/packages/plg_mokosuitecross_instagram/language/en-GB/plg_mokosuitecross_instagram.ini
new file mode 100644
index 00000000..30f71369
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_instagram/language/en-GB/plg_mokosuitecross_instagram.ini
@@ -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."
diff --git a/source/packages/plg_mokosuitecross_instagram/language/en-GB/plg_mokosuitecross_instagram.sys.ini b/source/packages/plg_mokosuitecross_instagram/language/en-GB/plg_mokosuitecross_instagram.sys.ini
new file mode 100644
index 00000000..6f74ebb3
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_instagram/language/en-GB/plg_mokosuitecross_instagram.sys.ini
@@ -0,0 +1,2 @@
+PLG_MOKOSUITECROSS_INSTAGRAM="MokoSuiteCross - Instagram"
+PLG_MOKOSUITECROSS_INSTAGRAM_DESCRIPTION="Cross-post Joomla articles to Instagram via Meta Content Publishing API."
diff --git a/source/packages/plg_mokosuitecross_instagram/language/index.html b/source/packages/plg_mokosuitecross_instagram/language/index.html
new file mode 100644
index 00000000..2efb97f3
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_instagram/language/index.html
@@ -0,0 +1 @@
+
diff --git a/source/packages/plg_mokosuitecross_instagram/services/index.html b/source/packages/plg_mokosuitecross_instagram/services/index.html
new file mode 100644
index 00000000..2efb97f3
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_instagram/services/index.html
@@ -0,0 +1 @@
+
diff --git a/source/packages/plg_mokosuitecross_instagram/services/provider.php b/source/packages/plg_mokosuitecross_instagram/services/provider.php
new file mode 100644
index 00000000..985e45df
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_instagram/services/provider.php
@@ -0,0 +1,38 @@
+
+ * @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;
+ }
+ );
+ }
+};
diff --git a/source/packages/plg_mokosuitecross_instagram/src/Extension/InstagramService.php b/source/packages/plg_mokosuitecross_instagram/src/Extension/InstagramService.php
new file mode 100644
index 00000000..693d5da5
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_instagram/src/Extension/InstagramService.php
@@ -0,0 +1,188 @@
+
+ * @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'];
+ }
+}
diff --git a/source/packages/plg_mokosuitecross_instagram/src/Extension/index.html b/source/packages/plg_mokosuitecross_instagram/src/Extension/index.html
new file mode 100644
index 00000000..2efb97f3
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_instagram/src/Extension/index.html
@@ -0,0 +1 @@
+
diff --git a/source/packages/plg_mokosuitecross_instagram/src/index.html b/source/packages/plg_mokosuitecross_instagram/src/index.html
new file mode 100644
index 00000000..2efb97f3
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_instagram/src/index.html
@@ -0,0 +1 @@
+
diff --git a/source/packages/plg_mokosuitecross_mailchimp/src/Extension/MailchimpService.php b/source/packages/plg_mokosuitecross_mailchimp/src/Extension/MailchimpService.php
index d56bffb6..8455740f 100644
--- a/source/packages/plg_mokosuitecross_mailchimp/src/Extension/MailchimpService.php
+++ b/source/packages/plg_mokosuitecross_mailchimp/src/Extension/MailchimpService.php
@@ -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];
}
diff --git a/source/packages/plg_mokosuitecross_medium/src/Extension/MediumService.php b/source/packages/plg_mokosuitecross_medium/src/Extension/MediumService.php
index 37dfb772..44391f53 100644
--- a/source/packages/plg_mokosuitecross_medium/src/Extension/MediumService.php
+++ b/source/packages/plg_mokosuitecross_medium/src/Extension/MediumService.php
@@ -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);
diff --git a/source/packages/plg_mokosuitecross_youtube/index.html b/source/packages/plg_mokosuitecross_youtube/index.html
new file mode 100644
index 00000000..2efb97f3
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_youtube/index.html
@@ -0,0 +1 @@
+
diff --git a/source/packages/plg_mokosuitecross_youtube/language/en-GB/index.html b/source/packages/plg_mokosuitecross_youtube/language/en-GB/index.html
new file mode 100644
index 00000000..2efb97f3
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_youtube/language/en-GB/index.html
@@ -0,0 +1 @@
+
diff --git a/source/packages/plg_mokosuitecross_youtube/language/en-GB/plg_mokosuitecross_youtube.ini b/source/packages/plg_mokosuitecross_youtube/language/en-GB/plg_mokosuitecross_youtube.ini
new file mode 100644
index 00000000..339a9cc6
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_youtube/language/en-GB/plg_mokosuitecross_youtube.ini
@@ -0,0 +1,2 @@
+PLG_MOKOSUITECROSS_YOUTUBE="MokoSuiteCross - YouTube"
+PLG_MOKOSUITECROSS_YOUTUBE_DESCRIPTION="Cross-post Joomla articles to YouTube community posts."
diff --git a/source/packages/plg_mokosuitecross_youtube/language/en-GB/plg_mokosuitecross_youtube.sys.ini b/source/packages/plg_mokosuitecross_youtube/language/en-GB/plg_mokosuitecross_youtube.sys.ini
new file mode 100644
index 00000000..339a9cc6
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_youtube/language/en-GB/plg_mokosuitecross_youtube.sys.ini
@@ -0,0 +1,2 @@
+PLG_MOKOSUITECROSS_YOUTUBE="MokoSuiteCross - YouTube"
+PLG_MOKOSUITECROSS_YOUTUBE_DESCRIPTION="Cross-post Joomla articles to YouTube community posts."
diff --git a/source/packages/plg_mokosuitecross_youtube/language/index.html b/source/packages/plg_mokosuitecross_youtube/language/index.html
new file mode 100644
index 00000000..2efb97f3
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_youtube/language/index.html
@@ -0,0 +1 @@
+
diff --git a/source/packages/plg_mokosuitecross_youtube/services/index.html b/source/packages/plg_mokosuitecross_youtube/services/index.html
new file mode 100644
index 00000000..2efb97f3
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_youtube/services/index.html
@@ -0,0 +1 @@
+
diff --git a/source/packages/plg_mokosuitecross_youtube/services/provider.php b/source/packages/plg_mokosuitecross_youtube/services/provider.php
new file mode 100644
index 00000000..755296a5
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_youtube/services/provider.php
@@ -0,0 +1,38 @@
+
+ * @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;
+ }
+ );
+ }
+};
diff --git a/source/packages/plg_mokosuitecross_youtube/src/Extension/YoutubeService.php b/source/packages/plg_mokosuitecross_youtube/src/Extension/YoutubeService.php
new file mode 100644
index 00000000..c06bf815
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_youtube/src/Extension/YoutubeService.php
@@ -0,0 +1,137 @@
+
+ * @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'];
+ }
+}
diff --git a/source/packages/plg_mokosuitecross_youtube/src/Extension/index.html b/source/packages/plg_mokosuitecross_youtube/src/Extension/index.html
new file mode 100644
index 00000000..2efb97f3
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_youtube/src/Extension/index.html
@@ -0,0 +1 @@
+
diff --git a/source/packages/plg_mokosuitecross_youtube/src/index.html b/source/packages/plg_mokosuitecross_youtube/src/index.html
new file mode 100644
index 00000000..2efb97f3
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_youtube/src/index.html
@@ -0,0 +1 @@
+
diff --git a/source/packages/plg_mokosuitecross_youtube/youtube.php b/source/packages/plg_mokosuitecross_youtube/youtube.php
new file mode 100644
index 00000000..9b76408c
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_youtube/youtube.php
@@ -0,0 +1,11 @@
+
+ * @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;
diff --git a/source/packages/plg_mokosuitecross_youtube/youtube.xml b/source/packages/plg_mokosuitecross_youtube/youtube.xml
new file mode 100644
index 00000000..217d838f
--- /dev/null
+++ b/source/packages/plg_mokosuitecross_youtube/youtube.xml
@@ -0,0 +1,39 @@
+
+
+ MokoSuiteCross - Youtube
+ 01.04.02-dev
+ 2026-06-23
+ Moko Consulting
+ hello@mokoconsulting.tech
+ https://mokoconsulting.tech
+ Copyright (C) 2026 Moko Consulting. All rights reserved.
+ GPL-3.0-or-later
+ PLG_MOKOSUITECROSS_YOUTUBE_DESCRIPTION
+
+ Joomla\Plugin\MokoSuiteCross\Youtube
+
+
+ youtube.php
+ src
+ services
+ language
+
+
+
+ language/en-GB/plg_mokosuitecross_youtube.ini
+ language/en-GB/plg_mokosuitecross_youtube.sys.ini
+
+
+
+
+
+
+
diff --git a/source/pkg_mokosuitecross.xml b/source/pkg_mokosuitecross.xml
index d0383aeb..8762b3d4 100644
--- a/source/pkg_mokosuitecross.xml
+++ b/source/pkg_mokosuitecross.xml
@@ -60,6 +60,8 @@
plg_mokosuitecross_tiktok.zip
plg_mokosuitecross_mokosuitecalendar.zip
plg_mokosuitecross_mokosuitegallery.zip
+ plg_mokosuitecross_instagram.zip
+ plg_mokosuitecross_youtube.zip
plg_system_mokosuitecross_events.zip