From 28db9a67b632e5e83b77f9d00dd8fa8bcd74d113 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 21 Jun 2026 11:03:09 -0500 Subject: [PATCH 1/7] fix: remove duplicate curl_setopt_array calls in 4 service plugins (#139) SendGrid and Reddit had a second curl_setopt_array that referenced an undefined $token variable, silently breaking auth. TikTok and Pinterest had identical duplicates (no variable bug but dead code). Removes the duplicate block from each plugin's publish() method. --- .../src/Extension/PinterestService.php | 132 +++++++++++++++++ .../src/Extension/RedditService.php | 130 +++++++++++++++++ .../src/Extension/SendgridService.php | 133 +++++++++++++++++ .../src/Extension/TiktokService.php | 134 ++++++++++++++++++ 4 files changed, 529 insertions(+) create mode 100644 source/packages/plg_mokosuitecross_pinterest/src/Extension/PinterestService.php create mode 100644 source/packages/plg_mokosuitecross_reddit/src/Extension/RedditService.php create mode 100644 source/packages/plg_mokosuitecross_sendgrid/src/Extension/SendgridService.php create mode 100644 source/packages/plg_mokosuitecross_tiktok/src/Extension/TiktokService.php diff --git a/source/packages/plg_mokosuitecross_pinterest/src/Extension/PinterestService.php b/source/packages/plg_mokosuitecross_pinterest/src/Extension/PinterestService.php new file mode 100644 index 0000000..c612603 --- /dev/null +++ b/source/packages/plg_mokosuitecross_pinterest/src/Extension/PinterestService.php @@ -0,0 +1,132 @@ + + * @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\Pinterest\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Pinterest service plugin for MokoSuiteCross. + * + * API: https://api.pinterest.com/v5/pins + */ +class PinterestService 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 'pinterest'; } + public function getServiceName(): string { return 'Pinterest'; } + public function getMaxLength(): int { return 500; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $token = $credentials['access_token'] ?? ''; + $boardId = $credentials['board_id'] ?? ''; + + if (empty($token) || empty($boardId)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or board ID']]; + } + + if (empty($media[0])) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Pinterest requires an image']]; + } + + $postData = json_encode([ + 'board_id' => $boardId, + 'title' => mb_substr(strip_tags($message), 0, 100), + 'description' => mb_substr($message, 0, 500), + 'media_source' => [ + 'source_type' => 'image_url', + 'url' => $media[0], + ], + ]); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => 'https://api.pinterest.com/v5/pins', + 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'] ?? $data['uri'] ?? '', '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://api.pinterest.com/v5/user_account'); + 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['username'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['username']]; + } + + return ['valid' => false, 'message' => 'Invalid token', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_reddit/src/Extension/RedditService.php b/source/packages/plg_mokosuitecross_reddit/src/Extension/RedditService.php new file mode 100644 index 0000000..52dad88 --- /dev/null +++ b/source/packages/plg_mokosuitecross_reddit/src/Extension/RedditService.php @@ -0,0 +1,130 @@ + + * @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\Reddit\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Reddit service plugin for MokoSuiteCross. + * + * API: https://oauth.reddit.com/api/submit + */ +class RedditService 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 'reddit'; } + public function getServiceName(): string { return 'Reddit'; } + public function getMaxLength(): int { return 300; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $accessToken = $credentials['access_token'] ?? ''; + $subreddit = $credentials['subreddit'] ?? ''; + + if (empty($accessToken) || empty($subreddit)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or subreddit']]; + } + + $title = $params['title'] ?? mb_substr(strip_tags($message), 0, 300); + + $postData = http_build_query([ + 'sr' => $subreddit, + 'kind' => 'self', + 'title' => $title, + 'text' => $message, + ]); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => 'https://oauth.reddit.com/api/submit', + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_HTTPHEADER => [ + 'Authorization: Bearer ' . $accessToken, + 'User-Agent: MokoSuiteCross/1.0', + ], + 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'] ?? $data['uri'] ?? '', '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://oauth.reddit.com/api/v1/me'); + curl_setopt_array($ch, [ + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'User-Agent: MokoSuiteCross/1.0'], + 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['name'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => 'u/' . $data['name']]; + } + + return ['valid' => false, 'message' => 'Invalid token', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_sendgrid/src/Extension/SendgridService.php b/source/packages/plg_mokosuitecross_sendgrid/src/Extension/SendgridService.php new file mode 100644 index 0000000..e23ead4 --- /dev/null +++ b/source/packages/plg_mokosuitecross_sendgrid/src/Extension/SendgridService.php @@ -0,0 +1,133 @@ + + * @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\Sendgrid\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * SendGrid service plugin for MokoSuiteCross. + * + * API: https://api.sendgrid.com/v3/marketing/singlesends + */ +class SendgridService 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 'sendgrid'; } + public function getServiceName(): string { return 'SendGrid'; } + public function getMaxLength(): int { return 0; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $apiKey = $credentials['api_key'] ?? ''; + $listId = $credentials['list_id'] ?? ''; + $senderEmail = $credentials['sender_email'] ?? ''; + $senderName = $credentials['sender_name'] ?? 'Newsletter'; + + if (empty($apiKey) || empty($senderEmail)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing API key or sender email']]; + } + + $subject = $params['subject'] ?? mb_substr(strip_tags($message), 0, 150); + + $postData = json_encode([ + 'name' => $subject, + 'send_to' => !empty($listId) ? ['list_ids' => [$listId]] : ['all' => true], + 'email_config' => [ + 'subject' => $subject, + 'html_content' => $message, + 'sender_id' => null, + 'custom_unsubscribe_url' => '', + ], + ]); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => 'https://api.sendgrid.com/v3/marketing/singlesends', + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey, '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'] ?? $data['uri'] ?? '', 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $key = $credentials['api_key'] ?? ''; + + if (empty($key)) { + return ['valid' => false, 'message' => 'Missing API key', 'account_name' => '']; + } + + $ch = curl_init('https://api.sendgrid.com/v3/user/profile'); + curl_setopt_array($ch, [ + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key], + 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['first_name'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['first_name'] . ' ' . ($data['last_name'] ?? '')]; + } + + return ['valid' => false, 'message' => 'Invalid API key', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_tiktok/src/Extension/TiktokService.php b/source/packages/plg_mokosuitecross_tiktok/src/Extension/TiktokService.php new file mode 100644 index 0000000..561509e --- /dev/null +++ b/source/packages/plg_mokosuitecross_tiktok/src/Extension/TiktokService.php @@ -0,0 +1,134 @@ + + * @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\Tiktok\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * TikTok service plugin for MokoSuiteCross. + * + * API: https://open.tiktokapis.com/v2/post/publish/content/init/ + */ +class TiktokService 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 'tiktok'; } + public function getServiceName(): string { return 'TikTok'; } + 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 = $credentials['access_token'] ?? ''; + + if (empty($token)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token']]; + } + + if (empty($media[0])) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'TikTok requires a video or image']]; + } + + $postData = json_encode([ + 'post_info' => [ + 'title' => mb_substr(strip_tags($message), 0, 150), + 'description' => mb_substr($message, 0, 2200), + 'privacy_level' => 'SELF_ONLY', + 'disable_comment' => false, + ], + 'source_info' => [ + 'source' => 'PULL_FROM_URL', + 'video_url' => $media[0], + ], + ]); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => 'https://open.tiktokapis.com/v2/post/publish/content/init/', + 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'] ?? $data['uri'] ?? '', '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://open.tiktokapis.com/v2/user/info/?fields=display_name,username'); + 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['data']['user']['display_name'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['data']['user']['display_name']]; + } + + return ['valid' => false, 'message' => 'Invalid token', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image', 'video']; + } +} -- 2.52.0 From 65bba1f561346a84353afb52053231c163df1880 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 21 Jun 2026 16:05:17 +0000 Subject: [PATCH 2/7] chore(version): auto-bump patch 01.01.01-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 4 ++-- CHANGELOG.md | 2 +- README.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 75a6963..59e2557 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: mokocli.Automation -# VERSION: 01.00.00 +# INGROUP: moko-platform.Automation +# VERSION: 01.01.01 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CHANGELOG.md b/CHANGELOG.md index fc72f68..5056276 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## [Unreleased] - + All notable changes to MokoJoomCross will be documented in this file. diff --git a/README.md b/README.md index 33eb7e0..961fc94 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoJoomCross - + Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 5/6. -- 2.52.0 From 27505f7501b46e8fbdaf42eb52c6b390b80e90b3 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 21 Jun 2026 11:38:41 -0500 Subject: [PATCH 3/7] fix: rename all MOKOJOOMCROSS language keys and events to MOKOSUITECROSS (#128, #138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the MokoJoomCross → MokoSuiteCross rebrand across all language string keys, Joomla event names, documentation, and wiki pages. - 1,151 language key references renamed (COM_, PLG_, PKG_ prefixes) - Event names renamed (onMokoJoomCross* → onMokoSuiteCross*) - CLAUDE.md, CHANGELOG.md, wiki docs updated - Zero mokojoomcross references remaining in codebase Closes #128, closes #138 --- .mokogitea/CLAUDE.md | 83 ++ .mokogitea/manifest.xml | 26 + .mokogitea/workflows/auto-release.yml | 201 +--- .mokogitea/workflows/issue-branch.yml | 6 +- .mokogitea/workflows/pr-check.yml | 18 +- .mokogitea/workflows/pre-release.yml | 79 +- .mokogitea/workflows/repo-health.yml | 17 +- CHANGELOG.md | 201 +++- CLAUDE.md | 58 +- Makefile | 4 +- README.md | 32 +- composer.json | 2 +- .../language/en-GB/pkg_mokosuitecross.sys.ini | 8 + .../packages/com_mokosuitecross}/access.xml | 6 +- source/packages/com_mokosuitecross/config.xml | 146 +++ .../com_mokosuitecross/forms/filter_logs.xml | 37 + .../com_mokosuitecross/forms/filter_posts.xml | 52 + .../forms/filter_services.xml | 6 +- .../forms/filter_templates.xml | 51 + .../com_mokosuitecross}/forms/index.html | 0 .../com_mokosuitecross/forms/post.xml | 125 +++ .../com_mokosuitecross/forms/service.xml | 910 ++++++++++++++++++ .../com_mokosuitecross/forms/template.xml | 39 +- .../packages/com_mokosuitecross}/index.html | 0 .../language/en-GB/com_mokosuitecross.ini | 513 ++++++++++ .../language/en-GB/com_mokosuitecross.sys.ini | 11 + .../language/en-GB/index.html | 0 .../com_mokosuitecross}/language/index.html | 0 .../com_mokosuitecross/mokosuitecross.xml | 25 +- .../packages/com_mokosuitecross}/script.php | 6 +- .../com_mokosuitecross}/services/index.html | 0 .../com_mokosuitecross}/services/provider.php | 12 +- .../com_mokosuitecross}/site/index.html | 0 .../language/en-GB/com_mokosuitecross.ini | 4 +- .../site/language/en-GB/index.html | 0 .../site/language/index.html | 0 .../site/src/Controller/DisplayController.php | 6 +- .../site/src/Controller/index.html | 0 .../site/src/View/Post/index.html | 0 .../site/src/View/index.html | 0 .../com_mokosuitecross}/site/src/index.html | 0 .../com_mokosuitecross}/site/tmpl/index.html | 0 .../site/tmpl/post/index.html | 0 .../com_mokosuitecross}/sql/index.html | 0 .../com_mokosuitecross}/sql/install.mysql.sql | 48 +- .../sql/uninstall.mysql.sql | 5 + .../sql/updates/index.html | 0 .../sql/updates/mysql/01.00.00.sql | 2 +- .../sql/updates/mysql/01.01.00.sql | 14 + .../sql/updates/mysql/index.html | 0 .../src/Controller/DashboardController.php | 61 ++ .../src/Controller/DispatchController.php | 262 +++++ .../src/Controller/DisplayController.php | 6 +- .../src/Controller/OauthController.php | 198 ++++ .../src/Controller/PostController.php | 6 +- .../src/Controller/PostsController.php | 258 +++++ .../src/Controller/ServiceController.php | 104 ++ .../src/Controller/ServicesController.php | 6 +- .../src/Controller/TemplateController.php | 8 +- .../src/Controller/TemplatesController.php | 10 +- .../src/Controller/index.html | 0 .../src/Extension/MokoSuiteCrossComponent.php | 8 +- .../src/Extension/index.html | 0 .../src/Helper/CredentialHelper.php | 110 +++ .../src/Helper/CrossPostDispatcher.php | 494 ++++++++++ .../src/Helper/MigrationHelper.php | 388 ++++++++ .../src/Helper/MokoSuiteCrossHelper.php | 74 ++ .../src/Helper/OAuthHelper.php | 311 ++++++ .../src/Helper/QueueProcessor.php | 903 +++++++++++++++++ .../src/Helper/ServiceIconHelper.php | 96 ++ .../com_mokosuitecross}/src/Helper/index.html | 0 .../src/Model/DashboardModel.php | 196 ++++ .../src/Model/LogsModel.php | 10 +- .../src/Model/PostModel.php | 93 ++ .../src/Model/PostsModel.php | 26 +- .../src/Model/ServiceModel.php | 123 +++ .../src/Model/ServiceStatsModel.php | 185 ++++ .../src/Model/ServicesModel.php | 8 +- .../src/Model/TemplateModel.php | 39 + .../src/Model/TemplatesModel.php | 61 ++ .../com_mokosuitecross}/src/Model/index.html | 0 .../MokoSuiteCrossServiceInterface.php | 23 +- .../src/Service/index.html | 0 .../src/Table/PostTable.php | 8 +- .../src/Table/ServiceTable.php | 91 ++ .../src/Table/TemplateTable.php | 10 +- .../com_mokosuitecross}/src/Table/index.html | 0 .../src/View/Dashboard/HtmlView.php | 71 ++ .../src/View/Dashboard/index.html | 0 .../src/View/Logs/HtmlView.php | 56 ++ .../src/View/Logs/index.html | 0 .../src/View/Post/HtmlView.php | 59 ++ .../src/View/Posts/HtmlView.php | 82 ++ .../src/View/Posts/index.html | 0 .../src/View/Service/HtmlView.php | 61 ++ .../src/View/Service}/index.html | 0 .../src/View/ServiceStats/HtmlView.php | 84 ++ .../src/View/Services/HtmlView.php | 29 +- .../src/View/Services}/index.html | 0 .../src/View/Template/HtmlView.php | 57 ++ .../src/View/Template/index.html | 1 + .../src/View/Templates/HtmlView.php | 60 ++ .../src/View/Templates/index.html | 1 + .../com_mokosuitecross/src/View}/index.html | 0 .../com_mokosuitecross/src}/index.html | 0 .../tmpl/dashboard/default.php | 289 ++++++ .../tmpl/dashboard}/index.html | 0 .../com_mokosuitecross/tmpl}/index.html | 0 .../com_mokosuitecross/tmpl/logs/default.php | 110 +++ .../com_mokosuitecross/tmpl/logs}/index.html | 0 .../com_mokosuitecross/tmpl/post/edit.php | 79 ++ .../com_mokosuitecross/tmpl/posts/default.php | 137 +++ .../com_mokosuitecross/tmpl/posts}/index.html | 0 .../com_mokosuitecross/tmpl/service/edit.php | 216 +++++ .../tmpl/service}/index.html | 0 .../tmpl/services/default.php | 109 +++ .../tmpl/services/default_service.php | 44 + .../tmpl/services}/index.html | 0 .../tmpl/servicestats/default.php | 219 +++++ .../com_mokosuitecross/tmpl/template/edit.php | 114 +++ .../tmpl/template/index.html | 1 + .../tmpl/templates/default.php | 103 ++ .../tmpl/templates/index.html | 1 + .../plg_content_mokosuitecross}/index.html | 0 .../language/en-GB}/index.html | 0 .../en-GB/plg_content_mokosuitecross.ini | 13 + .../en-GB/plg_content_mokosuitecross.sys.ini | 2 + .../language}/index.html | 0 .../mokosuitecross.php | 12 + .../mokosuitecross.xml | 14 +- .../services}/index.html | 0 .../services/provider.php | 10 +- .../src/Extension/MokoSuiteCrossContent.php | 361 +++++++ .../src/Extension}/index.html | 0 .../src}/index.html | 0 .../activitypub.php | 3 +- .../activitypub.xml | 26 + .../plg_mokosuitecross_activitypub/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_activitypub.ini | 2 + .../plg_mokosuitecross_activitypub.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/ActivitypubService.php | 132 +++ .../src/Extension/index.html | 1 + .../src/index.html | 1 + .../plg_mokosuitecross_blogger/blogger.php | 3 +- .../plg_mokosuitecross_blogger/blogger.xml | 26 + .../plg_mokosuitecross_blogger/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_blogger.ini | 2 + .../en-GB/plg_mokosuitecross_blogger.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/BloggerService.php | 135 +++ .../src/Extension/index.html | 1 + .../plg_mokosuitecross_blogger/src/index.html | 1 + .../plg_mokosuitecross_bluesky}/bluesky.php | 2 +- .../plg_mokosuitecross_bluesky/bluesky.xml | 51 + .../plg_mokosuitecross_bluesky}/index.html | 0 .../language/en-GB}/index.html | 0 .../en-GB/plg_mokosuitecross_bluesky.ini | 7 + .../en-GB/plg_mokosuitecross_bluesky.sys.ini | 2 + .../language}/index.html | 0 .../services}/index.html | 0 .../services/provider.php | 8 +- .../src/Extension/BlueskyService.php | 76 +- .../src/Extension}/index.html | 0 .../src}/index.html | 0 .../plg_mokosuitecross_brevo/brevo.php | 11 + .../plg_mokosuitecross_brevo/brevo.xml | 26 + .../plg_mokosuitecross_brevo/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_brevo.ini | 2 + .../en-GB/plg_mokosuitecross_brevo.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/BrevoService.php | 139 +++ .../src/Extension/index.html | 1 + .../plg_mokosuitecross_brevo/src/index.html | 1 + .../constantcontact.php | 11 + .../constantcontact.xml | 26 + .../index.html | 1 + .../language/en-GB/index.html | 1 + .../plg_mokosuitecross_constantcontact.ini | 2 + ...plg_mokosuitecross_constantcontact.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/ConstantcontactService.php | 142 +++ .../src/Extension/index.html | 1 + .../src/index.html | 1 + .../convertkit.php | 11 + .../convertkit.xml | 26 + .../plg_mokosuitecross_convertkit/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_convertkit.ini | 2 + .../plg_mokosuitecross_convertkit.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/ConvertkitService.php | 134 +++ .../src/Extension/index.html | 1 + .../src/index.html | 1 + .../plg_mokosuitecross_devto/devto.php | 11 + .../plg_mokosuitecross_devto/devto.xml | 26 + .../plg_mokosuitecross_devto/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_devto.ini | 2 + .../en-GB/plg_mokosuitecross_devto.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/DevtoService.php | 134 +++ .../src/Extension/index.html | 1 + .../plg_mokosuitecross_devto/src/index.html | 1 + .../plg_mokosuitecross_discord}/discord.php | 2 +- .../plg_mokosuitecross_discord/discord.xml | 46 + .../plg_mokosuitecross_discord}/index.html | 0 .../language/en-GB}/index.html | 0 .../en-GB/plg_mokosuitecross_discord.ini | 7 + .../en-GB/plg_mokosuitecross_discord.sys.ini | 2 + .../language}/index.html | 0 .../services}/index.html | 0 .../services/provider.php | 8 +- .../src/Extension/DiscordService.php | 34 +- .../src/Extension}/index.html | 0 .../src}/index.html | 0 .../plg_mokosuitecross_facebook}/facebook.php | 2 +- .../plg_mokosuitecross_facebook/facebook.xml | 45 + .../plg_mokosuitecross_facebook}/index.html | 0 .../language/en-GB}/index.html | 0 .../en-GB/plg_mokosuitecross_facebook.ini | 7 + .../en-GB/plg_mokosuitecross_facebook.sys.ini | 2 + .../language}/index.html | 0 .../services}/index.html | 0 .../services/provider.php | 8 +- .../src/Extension/FacebookService.php | 51 +- .../src/Extension}/index.html | 0 .../src}/index.html | 0 .../plg_mokosuitecross_ghost/ghost.php | 11 + .../plg_mokosuitecross_ghost/ghost.xml | 26 + .../plg_mokosuitecross_ghost/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_ghost.ini | 2 + .../en-GB/plg_mokosuitecross_ghost.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/GhostService.php | 190 ++++ .../src/Extension/index.html | 1 + .../plg_mokosuitecross_ghost/src/index.html | 1 + .../googlebusiness.php | 11 + .../googlebusiness.xml | 26 + .../index.html | 1 + .../language/en-GB/index.html | 1 + .../plg_mokosuitecross_googlebusiness.ini | 2 + .../plg_mokosuitecross_googlebusiness.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/GoogleBusinessService.php | 150 +++ .../src/Extension/index.html | 1 + .../src/index.html | 1 + .../googlechat.php | 11 + .../googlechat.xml | 26 + .../plg_mokosuitecross_googlechat/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_googlechat.ini | 2 + .../plg_mokosuitecross_googlechat.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/GoogleChatService.php | 103 ++ .../src/Extension/index.html | 1 + .../src/index.html | 1 + .../plg_mokosuitecross_hashnode/hashnode.php | 11 + .../plg_mokosuitecross_hashnode/hashnode.xml | 26 + .../plg_mokosuitecross_hashnode/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_hashnode.ini | 2 + .../en-GB/plg_mokosuitecross_hashnode.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/HashnodeService.php | 153 +++ .../src/Extension/index.html | 1 + .../src/index.html | 1 + .../plg_mokosuitecross_linkedin}/index.html | 0 .../language/en-GB}/index.html | 0 .../en-GB/plg_mokosuitecross_linkedin.ini | 9 + .../en-GB/plg_mokosuitecross_linkedin.sys.ini | 2 + .../language}/index.html | 0 .../plg_mokosuitecross_linkedin}/linkedin.php | 2 +- .../plg_mokosuitecross_linkedin/linkedin.xml | 51 + .../services}/index.html | 0 .../services/provider.php | 8 +- .../src/Extension/LinkedinService.php | 38 +- .../src/Extension}/index.html | 0 .../src}/index.html | 0 .../plg_mokosuitecross_mailchimp}/index.html | 0 .../language/en-GB}/index.html | 0 .../en-GB/plg_mokosuitecross_mailchimp.ini | 9 + .../plg_mokosuitecross_mailchimp.sys.ini | 2 + .../language}/index.html | 0 .../mailchimp.php | 2 +- .../mailchimp.xml | 56 ++ .../services}/index.html | 0 .../services/provider.php | 8 +- .../src/Extension/MailchimpService.php | 67 +- .../src/Extension}/index.html | 0 .../src}/index.html | 0 .../plg_mokosuitecross_mastodon}/index.html | 0 .../language/en-GB}/index.html | 0 .../en-GB/plg_mokosuitecross_mastodon.ini | 13 + .../en-GB/plg_mokosuitecross_mastodon.sys.ini | 2 + .../language}/index.html | 0 .../plg_mokosuitecross_mastodon}/mastodon.php | 2 +- .../plg_mokosuitecross_mastodon/mastodon.xml | 57 ++ .../services}/index.html | 0 .../services/provider.php | 8 +- .../src/Extension/MastodonService.php | 49 +- .../src/Extension}/index.html | 0 .../src}/index.html | 0 .../plg_mokosuitecross_matrix/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_matrix.ini | 2 + .../en-GB/plg_mokosuitecross_matrix.sys.ini | 2 + .../language/index.html | 1 + .../plg_mokosuitecross_matrix/matrix.php | 11 + .../plg_mokosuitecross_matrix/matrix.xml | 26 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/MatrixService.php | 143 +++ .../src/Extension/index.html | 1 + .../plg_mokosuitecross_matrix/src/index.html | 1 + .../plg_mokosuitecross_medium/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_medium.ini | 2 + .../en-GB/plg_mokosuitecross_medium.sys.ini | 2 + .../language/index.html | 1 + .../plg_mokosuitecross_medium/medium.php | 11 + .../plg_mokosuitecross_medium/medium.xml | 26 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/MediumService.php | 180 ++++ .../src/Extension/index.html | 1 + .../plg_mokosuitecross_medium/src/index.html | 1 + .../plg_mokosuitecross_mokosuitecalendar.ini | 14 + ...g_mokosuitecross_mokosuitecalendar.sys.ini | 6 + .../mokosuitecalendar.php | 14 + .../mokosuitecalendar.xml | 62 ++ .../services/provider.php | 38 + .../src/Extension/CalendarService.php | 187 ++++ .../plg_mokosuitecross_mokosuitegallery.ini | 16 + ...lg_mokosuitecross_mokosuitegallery.sys.ini | 6 + .../mokosuitegallery.php | 14 + .../mokosuitegallery.xml | 63 ++ .../services/provider.php | 38 + .../src/Extension/GalleryService.php | 249 +++++ .../plg_mokosuitecross_nostr/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_nostr.ini | 2 + .../en-GB/plg_mokosuitecross_nostr.sys.ini | 2 + .../language/index.html | 1 + .../plg_mokosuitecross_nostr/nostr.php | 11 + .../plg_mokosuitecross_nostr/nostr.xml | 26 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/NostrService.php | 97 ++ .../src/Extension/index.html | 1 + .../plg_mokosuitecross_nostr/src/index.html | 1 + .../plg_mokosuitecross_ntfy/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_ntfy.ini | 2 + .../en-GB/plg_mokosuitecross_ntfy.sys.ini | 2 + .../language/index.html | 1 + .../packages/plg_mokosuitecross_ntfy/ntfy.php | 11 + .../packages/plg_mokosuitecross_ntfy/ntfy.xml | 26 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/NtfyService.php | 111 +++ .../src/Extension/index.html | 1 + .../plg_mokosuitecross_ntfy/src/index.html | 1 + .../plg_mokosuitecross_pinterest/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_pinterest.ini | 2 + .../plg_mokosuitecross_pinterest.sys.ini | 2 + .../language/index.html | 1 + .../pinterest.php | 11 + .../pinterest.xml | 26 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/index.html | 1 + .../src/index.html | 1 + .../plg_mokosuitecross_reddit/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_reddit.ini | 2 + .../en-GB/plg_mokosuitecross_reddit.sys.ini | 2 + .../language/index.html | 1 + .../plg_mokosuitecross_reddit/reddit.php | 11 + .../plg_mokosuitecross_reddit/reddit.xml | 26 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/index.html | 1 + .../plg_mokosuitecross_reddit/src/index.html | 1 + .../plg_mokosuitecross_rssfeed/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_rssfeed.ini | 2 + .../en-GB/plg_mokosuitecross_rssfeed.sys.ini | 2 + .../language/index.html | 1 + .../plg_mokosuitecross_rssfeed/rssfeed.php | 11 + .../plg_mokosuitecross_rssfeed/rssfeed.xml | 26 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/RssfeedService.php | 66 ++ .../src/Extension/index.html | 1 + .../plg_mokosuitecross_rssfeed/src/index.html | 1 + .../plg_mokosuitecross_sendgrid/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_sendgrid.ini | 2 + .../en-GB/plg_mokosuitecross_sendgrid.sys.ini | 2 + .../language/index.html | 1 + .../plg_mokosuitecross_sendgrid/sendgrid.php | 11 + .../plg_mokosuitecross_sendgrid/sendgrid.xml | 26 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/index.html | 1 + .../src/index.html | 1 + .../plg_mokosuitecross_slack}/index.html | 0 .../language/en-GB}/index.html | 0 .../en-GB/plg_mokosuitecross_slack.ini | 5 + .../en-GB/plg_mokosuitecross_slack.sys.ini | 2 + .../language}/index.html | 0 .../services}/index.html | 0 .../services/provider.php | 8 +- .../plg_mokosuitecross_slack}/slack.php | 2 +- .../plg_mokosuitecross_slack/slack.xml | 39 + .../src/Extension/SlackService.php | 34 +- .../src/Extension}/index.html | 0 .../plg_mokosuitecross_slack/src}/index.html | 0 .../plg_mokosuitecross_teams/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_teams.ini | 5 + .../en-GB/plg_mokosuitecross_teams.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/TeamsService.php | 133 +++ .../src/Extension/index.html | 1 + .../plg_mokosuitecross_teams/src/index.html | 1 + .../plg_mokosuitecross_teams/teams.php | 11 + .../plg_mokosuitecross_teams/teams.xml | 39 + .../plg_mokosuitecross_telegram}/index.html | 0 .../language/en-GB}/index.html | 0 .../en-GB/plg_mokosuitecross_telegram.ini | 9 + .../en-GB/plg_mokosuitecross_telegram.sys.ini | 2 + .../language}/index.html | 0 .../services}/index.html | 0 .../services/provider.php | 8 +- .../src/Extension/TelegramService.php | 45 +- .../src/Extension}/index.html | 0 .../src}/index.html | 0 .../plg_mokosuitecross_telegram}/telegram.php | 2 +- .../plg_mokosuitecross_telegram}/telegram.xml | 14 +- .../plg_mokosuitecross_threads/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_threads.ini | 5 + .../en-GB/plg_mokosuitecross_threads.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/ThreadsService.php | 188 ++++ .../src/Extension/index.html | 1 + .../plg_mokosuitecross_threads/src/index.html | 1 + .../plg_mokosuitecross_threads/threads.php | 11 + .../plg_mokosuitecross_threads/threads.xml | 39 + .../plg_mokosuitecross_tiktok/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_tiktok.ini | 2 + .../en-GB/plg_mokosuitecross_tiktok.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/index.html | 1 + .../plg_mokosuitecross_tiktok/src/index.html | 1 + .../plg_mokosuitecross_tiktok/tiktok.php | 11 + .../plg_mokosuitecross_tiktok/tiktok.xml | 26 + .../plg_mokosuitecross_tumblr/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_tumblr.ini | 2 + .../en-GB/plg_mokosuitecross_tumblr.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/TumblrService.php | 147 +++ .../src/Extension/index.html | 1 + .../plg_mokosuitecross_tumblr/src/index.html | 1 + .../plg_mokosuitecross_tumblr/tumblr.php | 11 + .../plg_mokosuitecross_tumblr/tumblr.xml | 26 + .../plg_mokosuitecross_twitter}/index.html | 0 .../language/en-GB}/index.html | 0 .../en-GB/plg_mokosuitecross_twitter.ini | 2 + .../en-GB/plg_mokosuitecross_twitter.sys.ini | 2 + .../language}/index.html | 0 .../services}/index.html | 0 .../services/provider.php | 8 +- .../src/Extension/TwitterService.php | 210 ++++ .../src/Extension}/index.html | 0 .../src}/index.html | 0 .../plg_mokosuitecross_twitter}/twitter.php | 2 +- .../plg_mokosuitecross_twitter}/twitter.xml | 14 +- .../plg_mokosuitecross_webhook/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_webhook.ini | 2 + .../en-GB/plg_mokosuitecross_webhook.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/WebhookService.php | 133 +++ .../src/Extension/index.html | 1 + .../plg_mokosuitecross_webhook/src/index.html | 1 + .../plg_mokosuitecross_webhook/webhook.php | 11 + .../plg_mokosuitecross_webhook/webhook.xml | 26 + .../plg_mokosuitecross_whatsapp/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_whatsapp.ini | 2 + .../en-GB/plg_mokosuitecross_whatsapp.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/WhatsappService.php | 146 +++ .../src/Extension/index.html | 1 + .../src/index.html | 1 + .../plg_mokosuitecross_whatsapp/whatsapp.php | 11 + .../plg_mokosuitecross_whatsapp/whatsapp.xml | 26 + .../plg_mokosuitecross_wordpress/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_mokosuitecross_wordpress.ini | 2 + .../plg_mokosuitecross_wordpress.sys.ini | 2 + .../language/index.html | 1 + .../services/index.html | 1 + .../services/provider.php | 38 + .../src/Extension/WordpressService.php | 158 +++ .../src/Extension/index.html | 1 + .../src/index.html | 1 + .../wordpress.php | 11 + .../wordpress.xml | 26 + .../plg_system_mokosuitecross}/index.html | 0 .../language/en-GB}/index.html | 0 .../en-GB/plg_system_mokosuitecross.ini | 6 + .../en-GB/plg_system_mokosuitecross.sys.ini | 6 + .../language}/index.html | 0 .../mokosuitecross.php | 12 + .../mokosuitecross.xml | 14 +- .../services}/index.html | 0 .../services/provider.php | 10 +- .../src/Extension/MokoSuiteCross.php | 189 ++++ .../src/Extension}/index.html | 0 .../plg_system_mokosuitecross/src}/index.html | 0 .../plg_system_mokosuitecross_events.ini | 2 + .../plg_system_mokosuitecross_events.sys.ini | 2 + .../mokosuitecross_events.php | 12 + .../mokosuitecross_events.xml | 26 + .../services/provider.php | 38 + .../src/Extension/MokoSuiteCrossEvents.php | 88 ++ .../plg_system_mokosuitecross_gallery.ini | 2 + .../plg_system_mokosuitecross_gallery.sys.ini | 2 + .../mokosuitecross_gallery.php | 12 + .../mokosuitecross_gallery.xml | 26 + .../services/provider.php | 38 + .../src/Extension/MokoSuiteCrossGallery.php | 137 +++ .../plg_task_mokosuitecross}/index.html | 0 .../language/en-GB}/index.html | 0 .../en-GB/plg_task_mokosuitecross.ini | 9 + .../en-GB/plg_task_mokosuitecross.sys.ini | 2 + .../language}/index.html | 0 .../mokosuitecross.php | 12 + .../mokosuitecross.xml | 16 +- .../services}/index.html | 0 .../services/provider.php | 38 + .../src/Extension/MokoSuiteCrossTask.php | 92 ++ .../src/Extension/index.html | 1 + .../plg_task_mokosuitecross/src/index.html | 1 + .../plg_webservices_mokosuitecross/index.html | 1 + .../language/en-GB/index.html | 1 + .../en-GB/plg_webservices_mokosuitecross.ini | 2 + .../plg_webservices_mokosuitecross.sys.ini | 2 + .../language/index.html | 1 + .../mokosuitecross.php | 0 .../mokosuitecross.xml | 14 +- .../services/index.html | 1 + .../services/provider.php | 10 +- .../Extension/MokoSuiteCrossWebServices.php | 52 + .../src/Extension/index.html | 1 + .../src/index.html | 1 + source/pkg_mokosuitecross.xml | 75 ++ source/script.php | 197 ++++ src/language/en-GB/pkg_mokojoomcross.sys.ini | 8 - .../com_mokojoomcross/forms/filter_posts.xml | 39 - .../language/en-GB/com_mokojoomcross.sys.ini | 10 - .../com_mokojoomcross/sql/uninstall.mysql.sql | 5 - .../src/Model/ServiceModel.php | 54 -- .../src/View/Logs/HtmlView.php | 41 - .../en-GB/plg_content_mokojoomcross.ini | 2 - .../plg_mokojoomcross_bluesky/bluesky.xml | 26 - .../en-GB/plg_mokojoomcross_bluesky.ini | 2 - .../plg_mokojoomcross_discord/discord.xml | 26 - .../en-GB/plg_mokojoomcross_discord.ini | 2 - .../plg_mokojoomcross_facebook/facebook.xml | 26 - .../en-GB/plg_mokojoomcross_facebook.ini | 2 - .../en-GB/plg_mokojoomcross_linkedin.ini | 2 - .../plg_mokojoomcross_linkedin/linkedin.xml | 26 - .../en-GB/plg_mokojoomcross_mailchimp.ini | 2 - .../plg_mokojoomcross_mailchimp/mailchimp.xml | 26 - .../en-GB/plg_mokojoomcross_mastodon.ini | 2 - .../plg_mokojoomcross_mastodon/mastodon.xml | 26 - .../en-GB/plg_mokojoomcross_slack.ini | 2 - .../en-GB/plg_mokojoomcross_telegram.ini | 2 - .../en-GB/plg_mokojoomcross_twitter.ini | 2 - .../en-GB/plg_mokojoomcross_twitter.sys.ini | 2 - .../en-GB/plg_system_mokojoomcross.ini | 6 - .../en-GB/plg_system_mokojoomcross.sys.ini | 6 - .../en-GB/plg_webservices_mokojoomcross.ini | 2 - .../plg_webservices_mokojoomcross.sys.ini | 2 - wiki/Adding-Custom-Services.md | 26 +- wiki/Configuration.md | 2 +- wiki/Developer-Guide.md | 337 +++++++ wiki/Home.md | 20 +- wiki/Installation.md | 12 +- wiki/Message-Templates.md | 77 ++ wiki/REST-API.md | 57 ++ wiki/Services.md | 60 ++ wiki/Telegram.md | 4 +- wiki/Troubleshooting.md | 48 + 638 files changed, 17392 insertions(+), 992 deletions(-) create mode 100644 .mokogitea/CLAUDE.md create mode 100644 .mokogitea/manifest.xml create mode 100644 source/language/en-GB/pkg_mokosuitecross.sys.ini rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/access.xml (69%) create mode 100644 source/packages/com_mokosuitecross/config.xml create mode 100644 source/packages/com_mokosuitecross/forms/filter_logs.xml create mode 100644 source/packages/com_mokosuitecross/forms/filter_posts.xml rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/forms/filter_services.xml (90%) create mode 100644 source/packages/com_mokosuitecross/forms/filter_templates.xml rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/forms/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/forms/post.xml create mode 100644 source/packages/com_mokosuitecross/forms/service.xml rename src/packages/com_mokojoomcross/forms/service.xml => source/packages/com_mokosuitecross/forms/template.xml (67%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.ini create mode 100644 source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.sys.ini rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/language/en-GB/index.html (100%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/language/index.html (100%) rename src/packages/com_mokojoomcross/mokojoomcross.xml => source/packages/com_mokosuitecross/mokosuitecross.xml (62%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/script.php (83%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/services/index.html (100%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/services/provider.php (82%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/site/index.html (100%) rename src/packages/com_mokojoomcross/site/language/en-GB/com_mokojoomcross.ini => source/packages/com_mokosuitecross/site/language/en-GB/com_mokosuitecross.ini (50%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/site/language/en-GB/index.html (100%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/site/language/index.html (100%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/site/src/Controller/DisplayController.php (77%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/site/src/Controller/index.html (100%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/site/src/View/Post/index.html (100%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/site/src/View/index.html (100%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/site/src/index.html (100%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/site/tmpl/index.html (100%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/site/tmpl/post/index.html (100%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/sql/index.html (100%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/sql/install.mysql.sql (61%) create mode 100644 source/packages/com_mokosuitecross/sql/uninstall.mysql.sql rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/sql/updates/index.html (100%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/sql/updates/mysql/01.00.00.sql (50%) create mode 100644 source/packages/com_mokosuitecross/sql/updates/mysql/01.01.00.sql rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/sql/updates/mysql/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/src/Controller/DashboardController.php create mode 100644 source/packages/com_mokosuitecross/src/Controller/DispatchController.php rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/Controller/DisplayController.php (79%) create mode 100644 source/packages/com_mokosuitecross/src/Controller/OauthController.php rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/Controller/PostController.php (74%) create mode 100644 source/packages/com_mokosuitecross/src/Controller/PostsController.php create mode 100644 source/packages/com_mokosuitecross/src/Controller/ServiceController.php rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/Controller/ServicesController.php (81%) rename src/packages/com_mokojoomcross/src/Controller/ServiceController.php => source/packages/com_mokosuitecross/src/Controller/TemplateController.php (65%) rename src/packages/com_mokojoomcross/src/Controller/PostsController.php => source/packages/com_mokosuitecross/src/Controller/TemplatesController.php (58%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/Controller/index.html (100%) rename src/packages/com_mokojoomcross/src/Extension/MokoJoomCrossComponent.php => source/packages/com_mokosuitecross/src/Extension/MokoSuiteCrossComponent.php (64%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/Extension/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/src/Helper/CredentialHelper.php create mode 100644 source/packages/com_mokosuitecross/src/Helper/CrossPostDispatcher.php create mode 100644 source/packages/com_mokosuitecross/src/Helper/MigrationHelper.php create mode 100644 source/packages/com_mokosuitecross/src/Helper/MokoSuiteCrossHelper.php create mode 100644 source/packages/com_mokosuitecross/src/Helper/OAuthHelper.php create mode 100644 source/packages/com_mokosuitecross/src/Helper/QueueProcessor.php create mode 100644 source/packages/com_mokosuitecross/src/Helper/ServiceIconHelper.php rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/Helper/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/src/Model/DashboardModel.php rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/Model/LogsModel.php (84%) create mode 100644 source/packages/com_mokosuitecross/src/Model/PostModel.php rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/Model/PostsModel.php (69%) create mode 100644 source/packages/com_mokosuitecross/src/Model/ServiceModel.php create mode 100644 source/packages/com_mokosuitecross/src/Model/ServiceStatsModel.php rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/Model/ServicesModel.php (90%) create mode 100644 source/packages/com_mokosuitecross/src/Model/TemplateModel.php create mode 100644 source/packages/com_mokosuitecross/src/Model/TemplatesModel.php rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/Model/index.html (100%) rename src/packages/com_mokojoomcross/src/Service/MokoJoomCrossServiceInterface.php => source/packages/com_mokosuitecross/src/Service/MokoSuiteCrossServiceInterface.php (73%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/Service/index.html (100%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/Table/PostTable.php (70%) create mode 100644 source/packages/com_mokosuitecross/src/Table/ServiceTable.php rename src/packages/com_mokojoomcross/src/Table/ServiceTable.php => source/packages/com_mokosuitecross/src/Table/TemplateTable.php (64%) rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/Table/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/src/View/Dashboard/HtmlView.php rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/View/Dashboard/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/src/View/Logs/HtmlView.php rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/View/Logs/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/src/View/Post/HtmlView.php create mode 100644 source/packages/com_mokosuitecross/src/View/Posts/HtmlView.php rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/View/Posts/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/src/View/Service/HtmlView.php rename {src/packages/com_mokojoomcross/src/View/Services => source/packages/com_mokosuitecross/src/View/Service}/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/src/View/ServiceStats/HtmlView.php rename {src/packages/com_mokojoomcross => source/packages/com_mokosuitecross}/src/View/Services/HtmlView.php (52%) rename {src/packages/com_mokojoomcross/src/View => source/packages/com_mokosuitecross/src/View/Services}/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/src/View/Template/HtmlView.php create mode 100644 source/packages/com_mokosuitecross/src/View/Template/index.html create mode 100644 source/packages/com_mokosuitecross/src/View/Templates/HtmlView.php create mode 100644 source/packages/com_mokosuitecross/src/View/Templates/index.html rename {src/packages/com_mokojoomcross/src => source/packages/com_mokosuitecross/src/View}/index.html (100%) rename {src/packages/com_mokojoomcross/tmpl/dashboard => source/packages/com_mokosuitecross/src}/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/tmpl/dashboard/default.php rename {src/packages/com_mokojoomcross/tmpl => source/packages/com_mokosuitecross/tmpl/dashboard}/index.html (100%) rename {src/packages/com_mokojoomcross/tmpl/logs => source/packages/com_mokosuitecross/tmpl}/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/tmpl/logs/default.php rename {src/packages/com_mokojoomcross/tmpl/posts => source/packages/com_mokosuitecross/tmpl/logs}/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/tmpl/post/edit.php create mode 100644 source/packages/com_mokosuitecross/tmpl/posts/default.php rename {src/packages/com_mokojoomcross/tmpl/services => source/packages/com_mokosuitecross/tmpl/posts}/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/tmpl/service/edit.php rename {src/packages => source/packages/com_mokosuitecross/tmpl/service}/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/tmpl/services/default.php create mode 100644 source/packages/com_mokosuitecross/tmpl/services/default_service.php rename {src/packages/plg_content_mokojoomcross => source/packages/com_mokosuitecross/tmpl/services}/index.html (100%) create mode 100644 source/packages/com_mokosuitecross/tmpl/servicestats/default.php create mode 100644 source/packages/com_mokosuitecross/tmpl/template/edit.php create mode 100644 source/packages/com_mokosuitecross/tmpl/template/index.html create mode 100644 source/packages/com_mokosuitecross/tmpl/templates/default.php create mode 100644 source/packages/com_mokosuitecross/tmpl/templates/index.html rename {src/packages/plg_content_mokojoomcross/language => source/packages/plg_content_mokosuitecross}/index.html (100%) rename {src/packages/plg_content_mokojoomcross/services => source/packages/plg_content_mokosuitecross/language/en-GB}/index.html (100%) create mode 100644 source/packages/plg_content_mokosuitecross/language/en-GB/plg_content_mokosuitecross.ini create mode 100644 source/packages/plg_content_mokosuitecross/language/en-GB/plg_content_mokosuitecross.sys.ini rename {src/packages/plg_content_mokojoomcross/src/Extension => source/packages/plg_content_mokosuitecross/language}/index.html (100%) create mode 100644 source/packages/plg_content_mokosuitecross/mokosuitecross.php rename src/packages/plg_content_mokojoomcross/mokojoomcross.xml => source/packages/plg_content_mokosuitecross/mokosuitecross.xml (55%) rename {src/packages/plg_content_mokojoomcross/src => source/packages/plg_content_mokosuitecross/services}/index.html (100%) rename {src/packages/plg_content_mokojoomcross => source/packages/plg_content_mokosuitecross}/services/provider.php (82%) create mode 100644 source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php rename {src/packages/plg_mokojoomcross_bluesky => source/packages/plg_content_mokosuitecross/src/Extension}/index.html (100%) rename {src/packages/plg_mokojoomcross_bluesky/language/en-GB => source/packages/plg_content_mokosuitecross/src}/index.html (100%) rename src/packages/plg_system_mokojoomcross/mokojoomcross.php => source/packages/plg_mokosuitecross_activitypub/activitypub.php (80%) create mode 100644 source/packages/plg_mokosuitecross_activitypub/activitypub.xml create mode 100644 source/packages/plg_mokosuitecross_activitypub/index.html create mode 100644 source/packages/plg_mokosuitecross_activitypub/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_activitypub/language/en-GB/plg_mokosuitecross_activitypub.ini create mode 100644 source/packages/plg_mokosuitecross_activitypub/language/en-GB/plg_mokosuitecross_activitypub.sys.ini create mode 100644 source/packages/plg_mokosuitecross_activitypub/language/index.html create mode 100644 source/packages/plg_mokosuitecross_activitypub/services/index.html create mode 100644 source/packages/plg_mokosuitecross_activitypub/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_activitypub/src/Extension/ActivitypubService.php create mode 100644 source/packages/plg_mokosuitecross_activitypub/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_activitypub/src/index.html rename src/packages/plg_content_mokojoomcross/mokojoomcross.php => source/packages/plg_mokosuitecross_blogger/blogger.php (80%) create mode 100644 source/packages/plg_mokosuitecross_blogger/blogger.xml create mode 100644 source/packages/plg_mokosuitecross_blogger/index.html create mode 100644 source/packages/plg_mokosuitecross_blogger/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_blogger/language/en-GB/plg_mokosuitecross_blogger.ini create mode 100644 source/packages/plg_mokosuitecross_blogger/language/en-GB/plg_mokosuitecross_blogger.sys.ini create mode 100644 source/packages/plg_mokosuitecross_blogger/language/index.html create mode 100644 source/packages/plg_mokosuitecross_blogger/services/index.html create mode 100644 source/packages/plg_mokosuitecross_blogger/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_blogger/src/Extension/BloggerService.php create mode 100644 source/packages/plg_mokosuitecross_blogger/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_blogger/src/index.html rename {src/packages/plg_mokojoomcross_bluesky => source/packages/plg_mokosuitecross_bluesky}/bluesky.php (90%) create mode 100644 source/packages/plg_mokosuitecross_bluesky/bluesky.xml rename {src/packages/plg_mokojoomcross_bluesky/language => source/packages/plg_mokosuitecross_bluesky}/index.html (100%) rename {src/packages/plg_mokojoomcross_bluesky/services => source/packages/plg_mokosuitecross_bluesky/language/en-GB}/index.html (100%) create mode 100644 source/packages/plg_mokosuitecross_bluesky/language/en-GB/plg_mokosuitecross_bluesky.ini create mode 100644 source/packages/plg_mokosuitecross_bluesky/language/en-GB/plg_mokosuitecross_bluesky.sys.ini rename {src/packages/plg_mokojoomcross_bluesky/src/Extension => source/packages/plg_mokosuitecross_bluesky/language}/index.html (100%) rename {src/packages/plg_mokojoomcross_bluesky/src => source/packages/plg_mokosuitecross_bluesky/services}/index.html (100%) rename {src/packages/plg_mokojoomcross_bluesky => source/packages/plg_mokosuitecross_bluesky}/services/provider.php (81%) rename {src/packages/plg_mokojoomcross_bluesky => source/packages/plg_mokosuitecross_bluesky}/src/Extension/BlueskyService.php (66%) rename {src/packages/plg_mokojoomcross_discord => source/packages/plg_mokosuitecross_bluesky/src/Extension}/index.html (100%) rename {src/packages/plg_mokojoomcross_discord/language/en-GB => source/packages/plg_mokosuitecross_bluesky/src}/index.html (100%) create mode 100644 source/packages/plg_mokosuitecross_brevo/brevo.php create mode 100644 source/packages/plg_mokosuitecross_brevo/brevo.xml create mode 100644 source/packages/plg_mokosuitecross_brevo/index.html create mode 100644 source/packages/plg_mokosuitecross_brevo/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_brevo/language/en-GB/plg_mokosuitecross_brevo.ini create mode 100644 source/packages/plg_mokosuitecross_brevo/language/en-GB/plg_mokosuitecross_brevo.sys.ini create mode 100644 source/packages/plg_mokosuitecross_brevo/language/index.html create mode 100644 source/packages/plg_mokosuitecross_brevo/services/index.html create mode 100644 source/packages/plg_mokosuitecross_brevo/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_brevo/src/Extension/BrevoService.php create mode 100644 source/packages/plg_mokosuitecross_brevo/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_brevo/src/index.html create mode 100644 source/packages/plg_mokosuitecross_constantcontact/constantcontact.php create mode 100644 source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml create mode 100644 source/packages/plg_mokosuitecross_constantcontact/index.html create mode 100644 source/packages/plg_mokosuitecross_constantcontact/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_constantcontact/language/en-GB/plg_mokosuitecross_constantcontact.ini create mode 100644 source/packages/plg_mokosuitecross_constantcontact/language/en-GB/plg_mokosuitecross_constantcontact.sys.ini create mode 100644 source/packages/plg_mokosuitecross_constantcontact/language/index.html create mode 100644 source/packages/plg_mokosuitecross_constantcontact/services/index.html create mode 100644 source/packages/plg_mokosuitecross_constantcontact/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_constantcontact/src/Extension/ConstantcontactService.php create mode 100644 source/packages/plg_mokosuitecross_constantcontact/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_constantcontact/src/index.html create mode 100644 source/packages/plg_mokosuitecross_convertkit/convertkit.php create mode 100644 source/packages/plg_mokosuitecross_convertkit/convertkit.xml create mode 100644 source/packages/plg_mokosuitecross_convertkit/index.html create mode 100644 source/packages/plg_mokosuitecross_convertkit/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_convertkit/language/en-GB/plg_mokosuitecross_convertkit.ini create mode 100644 source/packages/plg_mokosuitecross_convertkit/language/en-GB/plg_mokosuitecross_convertkit.sys.ini create mode 100644 source/packages/plg_mokosuitecross_convertkit/language/index.html create mode 100644 source/packages/plg_mokosuitecross_convertkit/services/index.html create mode 100644 source/packages/plg_mokosuitecross_convertkit/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_convertkit/src/Extension/ConvertkitService.php create mode 100644 source/packages/plg_mokosuitecross_convertkit/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_convertkit/src/index.html create mode 100644 source/packages/plg_mokosuitecross_devto/devto.php create mode 100644 source/packages/plg_mokosuitecross_devto/devto.xml create mode 100644 source/packages/plg_mokosuitecross_devto/index.html create mode 100644 source/packages/plg_mokosuitecross_devto/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_devto/language/en-GB/plg_mokosuitecross_devto.ini create mode 100644 source/packages/plg_mokosuitecross_devto/language/en-GB/plg_mokosuitecross_devto.sys.ini create mode 100644 source/packages/plg_mokosuitecross_devto/language/index.html create mode 100644 source/packages/plg_mokosuitecross_devto/services/index.html create mode 100644 source/packages/plg_mokosuitecross_devto/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_devto/src/Extension/DevtoService.php create mode 100644 source/packages/plg_mokosuitecross_devto/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_devto/src/index.html rename {src/packages/plg_mokojoomcross_discord => source/packages/plg_mokosuitecross_discord}/discord.php (90%) create mode 100644 source/packages/plg_mokosuitecross_discord/discord.xml rename {src/packages/plg_mokojoomcross_discord/language => source/packages/plg_mokosuitecross_discord}/index.html (100%) rename {src/packages/plg_mokojoomcross_discord/services => source/packages/plg_mokosuitecross_discord/language/en-GB}/index.html (100%) create mode 100644 source/packages/plg_mokosuitecross_discord/language/en-GB/plg_mokosuitecross_discord.ini create mode 100644 source/packages/plg_mokosuitecross_discord/language/en-GB/plg_mokosuitecross_discord.sys.ini rename {src/packages/plg_mokojoomcross_discord/src/Extension => source/packages/plg_mokosuitecross_discord/language}/index.html (100%) rename {src/packages/plg_mokojoomcross_discord/src => source/packages/plg_mokosuitecross_discord/services}/index.html (100%) rename {src/packages/plg_mokojoomcross_discord => source/packages/plg_mokosuitecross_discord}/services/provider.php (81%) rename {src/packages/plg_mokojoomcross_discord => source/packages/plg_mokosuitecross_discord}/src/Extension/DiscordService.php (77%) rename {src/packages/plg_mokojoomcross_facebook => source/packages/plg_mokosuitecross_discord/src/Extension}/index.html (100%) rename {src/packages/plg_mokojoomcross_facebook/language/en-GB => source/packages/plg_mokosuitecross_discord/src}/index.html (100%) rename {src/packages/plg_mokojoomcross_facebook => source/packages/plg_mokosuitecross_facebook}/facebook.php (90%) create mode 100644 source/packages/plg_mokosuitecross_facebook/facebook.xml rename {src/packages/plg_mokojoomcross_facebook/language => source/packages/plg_mokosuitecross_facebook}/index.html (100%) rename {src/packages/plg_mokojoomcross_facebook/services => source/packages/plg_mokosuitecross_facebook/language/en-GB}/index.html (100%) create mode 100644 source/packages/plg_mokosuitecross_facebook/language/en-GB/plg_mokosuitecross_facebook.ini create mode 100644 source/packages/plg_mokosuitecross_facebook/language/en-GB/plg_mokosuitecross_facebook.sys.ini rename {src/packages/plg_mokojoomcross_facebook/src/Extension => source/packages/plg_mokosuitecross_facebook/language}/index.html (100%) rename {src/packages/plg_mokojoomcross_facebook/src => source/packages/plg_mokosuitecross_facebook/services}/index.html (100%) rename {src/packages/plg_mokojoomcross_facebook => source/packages/plg_mokosuitecross_facebook}/services/provider.php (81%) rename {src/packages/plg_mokojoomcross_facebook => source/packages/plg_mokosuitecross_facebook}/src/Extension/FacebookService.php (70%) rename {src/packages/plg_mokojoomcross_linkedin => source/packages/plg_mokosuitecross_facebook/src/Extension}/index.html (100%) rename {src/packages/plg_mokojoomcross_linkedin/language/en-GB => source/packages/plg_mokosuitecross_facebook/src}/index.html (100%) create mode 100644 source/packages/plg_mokosuitecross_ghost/ghost.php create mode 100644 source/packages/plg_mokosuitecross_ghost/ghost.xml create mode 100644 source/packages/plg_mokosuitecross_ghost/index.html create mode 100644 source/packages/plg_mokosuitecross_ghost/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_ghost/language/en-GB/plg_mokosuitecross_ghost.ini create mode 100644 source/packages/plg_mokosuitecross_ghost/language/en-GB/plg_mokosuitecross_ghost.sys.ini create mode 100644 source/packages/plg_mokosuitecross_ghost/language/index.html create mode 100644 source/packages/plg_mokosuitecross_ghost/services/index.html create mode 100644 source/packages/plg_mokosuitecross_ghost/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_ghost/src/Extension/GhostService.php create mode 100644 source/packages/plg_mokosuitecross_ghost/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_ghost/src/index.html create mode 100644 source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.php create mode 100644 source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml create mode 100644 source/packages/plg_mokosuitecross_googlebusiness/index.html create mode 100644 source/packages/plg_mokosuitecross_googlebusiness/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_googlebusiness/language/en-GB/plg_mokosuitecross_googlebusiness.ini create mode 100644 source/packages/plg_mokosuitecross_googlebusiness/language/en-GB/plg_mokosuitecross_googlebusiness.sys.ini create mode 100644 source/packages/plg_mokosuitecross_googlebusiness/language/index.html create mode 100644 source/packages/plg_mokosuitecross_googlebusiness/services/index.html create mode 100644 source/packages/plg_mokosuitecross_googlebusiness/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_googlebusiness/src/Extension/GoogleBusinessService.php create mode 100644 source/packages/plg_mokosuitecross_googlebusiness/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_googlebusiness/src/index.html create mode 100644 source/packages/plg_mokosuitecross_googlechat/googlechat.php create mode 100644 source/packages/plg_mokosuitecross_googlechat/googlechat.xml create mode 100644 source/packages/plg_mokosuitecross_googlechat/index.html create mode 100644 source/packages/plg_mokosuitecross_googlechat/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_googlechat/language/en-GB/plg_mokosuitecross_googlechat.ini create mode 100644 source/packages/plg_mokosuitecross_googlechat/language/en-GB/plg_mokosuitecross_googlechat.sys.ini create mode 100644 source/packages/plg_mokosuitecross_googlechat/language/index.html create mode 100644 source/packages/plg_mokosuitecross_googlechat/services/index.html create mode 100644 source/packages/plg_mokosuitecross_googlechat/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_googlechat/src/Extension/GoogleChatService.php create mode 100644 source/packages/plg_mokosuitecross_googlechat/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_googlechat/src/index.html create mode 100644 source/packages/plg_mokosuitecross_hashnode/hashnode.php create mode 100644 source/packages/plg_mokosuitecross_hashnode/hashnode.xml create mode 100644 source/packages/plg_mokosuitecross_hashnode/index.html create mode 100644 source/packages/plg_mokosuitecross_hashnode/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_hashnode/language/en-GB/plg_mokosuitecross_hashnode.ini create mode 100644 source/packages/plg_mokosuitecross_hashnode/language/en-GB/plg_mokosuitecross_hashnode.sys.ini create mode 100644 source/packages/plg_mokosuitecross_hashnode/language/index.html create mode 100644 source/packages/plg_mokosuitecross_hashnode/services/index.html create mode 100644 source/packages/plg_mokosuitecross_hashnode/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_hashnode/src/Extension/HashnodeService.php create mode 100644 source/packages/plg_mokosuitecross_hashnode/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_hashnode/src/index.html rename {src/packages/plg_mokojoomcross_linkedin/language => source/packages/plg_mokosuitecross_linkedin}/index.html (100%) rename {src/packages/plg_mokojoomcross_linkedin/services => source/packages/plg_mokosuitecross_linkedin/language/en-GB}/index.html (100%) create mode 100644 source/packages/plg_mokosuitecross_linkedin/language/en-GB/plg_mokosuitecross_linkedin.ini create mode 100644 source/packages/plg_mokosuitecross_linkedin/language/en-GB/plg_mokosuitecross_linkedin.sys.ini rename {src/packages/plg_mokojoomcross_linkedin/src/Extension => source/packages/plg_mokosuitecross_linkedin/language}/index.html (100%) rename {src/packages/plg_mokojoomcross_linkedin => source/packages/plg_mokosuitecross_linkedin}/linkedin.php (90%) create mode 100644 source/packages/plg_mokosuitecross_linkedin/linkedin.xml rename {src/packages/plg_mokojoomcross_linkedin/src => source/packages/plg_mokosuitecross_linkedin/services}/index.html (100%) rename {src/packages/plg_mokojoomcross_linkedin => source/packages/plg_mokosuitecross_linkedin}/services/provider.php (81%) rename {src/packages/plg_mokojoomcross_linkedin => source/packages/plg_mokosuitecross_linkedin}/src/Extension/LinkedinService.php (78%) rename {src/packages/plg_mokojoomcross_mailchimp => source/packages/plg_mokosuitecross_linkedin/src/Extension}/index.html (100%) rename {src/packages/plg_mokojoomcross_mailchimp/language/en-GB => source/packages/plg_mokosuitecross_linkedin/src}/index.html (100%) rename {src/packages/plg_mokojoomcross_mailchimp/language => source/packages/plg_mokosuitecross_mailchimp}/index.html (100%) rename {src/packages/plg_mokojoomcross_mailchimp/services => source/packages/plg_mokosuitecross_mailchimp/language/en-GB}/index.html (100%) create mode 100644 source/packages/plg_mokosuitecross_mailchimp/language/en-GB/plg_mokosuitecross_mailchimp.ini create mode 100644 source/packages/plg_mokosuitecross_mailchimp/language/en-GB/plg_mokosuitecross_mailchimp.sys.ini rename {src/packages/plg_mokojoomcross_mailchimp/src/Extension => source/packages/plg_mokosuitecross_mailchimp/language}/index.html (100%) rename {src/packages/plg_mokojoomcross_mailchimp => source/packages/plg_mokosuitecross_mailchimp}/mailchimp.php (90%) create mode 100644 source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml rename {src/packages/plg_mokojoomcross_mailchimp/src => source/packages/plg_mokosuitecross_mailchimp/services}/index.html (100%) rename {src/packages/plg_mokojoomcross_mailchimp => source/packages/plg_mokosuitecross_mailchimp}/services/provider.php (81%) rename {src/packages/plg_mokojoomcross_mailchimp => source/packages/plg_mokosuitecross_mailchimp}/src/Extension/MailchimpService.php (64%) rename {src/packages/plg_mokojoomcross_mastodon => source/packages/plg_mokosuitecross_mailchimp/src/Extension}/index.html (100%) rename {src/packages/plg_mokojoomcross_mastodon/language/en-GB => source/packages/plg_mokosuitecross_mailchimp/src}/index.html (100%) rename {src/packages/plg_mokojoomcross_mastodon/language => source/packages/plg_mokosuitecross_mastodon}/index.html (100%) rename {src/packages/plg_mokojoomcross_mastodon/services => source/packages/plg_mokosuitecross_mastodon/language/en-GB}/index.html (100%) create mode 100644 source/packages/plg_mokosuitecross_mastodon/language/en-GB/plg_mokosuitecross_mastodon.ini create mode 100644 source/packages/plg_mokosuitecross_mastodon/language/en-GB/plg_mokosuitecross_mastodon.sys.ini rename {src/packages/plg_mokojoomcross_mastodon/src/Extension => source/packages/plg_mokosuitecross_mastodon/language}/index.html (100%) rename {src/packages/plg_mokojoomcross_mastodon => source/packages/plg_mokosuitecross_mastodon}/mastodon.php (90%) create mode 100644 source/packages/plg_mokosuitecross_mastodon/mastodon.xml rename {src/packages/plg_mokojoomcross_mastodon/src => source/packages/plg_mokosuitecross_mastodon/services}/index.html (100%) rename {src/packages/plg_mokojoomcross_mastodon => source/packages/plg_mokosuitecross_mastodon}/services/provider.php (81%) rename {src/packages/plg_mokojoomcross_mastodon => source/packages/plg_mokosuitecross_mastodon}/src/Extension/MastodonService.php (65%) rename {src/packages/plg_mokojoomcross_slack => source/packages/plg_mokosuitecross_mastodon/src/Extension}/index.html (100%) rename {src/packages/plg_mokojoomcross_slack/language/en-GB => source/packages/plg_mokosuitecross_mastodon/src}/index.html (100%) create mode 100644 source/packages/plg_mokosuitecross_matrix/index.html create mode 100644 source/packages/plg_mokosuitecross_matrix/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_matrix/language/en-GB/plg_mokosuitecross_matrix.ini create mode 100644 source/packages/plg_mokosuitecross_matrix/language/en-GB/plg_mokosuitecross_matrix.sys.ini create mode 100644 source/packages/plg_mokosuitecross_matrix/language/index.html create mode 100644 source/packages/plg_mokosuitecross_matrix/matrix.php create mode 100644 source/packages/plg_mokosuitecross_matrix/matrix.xml create mode 100644 source/packages/plg_mokosuitecross_matrix/services/index.html create mode 100644 source/packages/plg_mokosuitecross_matrix/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_matrix/src/Extension/MatrixService.php create mode 100644 source/packages/plg_mokosuitecross_matrix/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_matrix/src/index.html create mode 100644 source/packages/plg_mokosuitecross_medium/index.html create mode 100644 source/packages/plg_mokosuitecross_medium/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_medium/language/en-GB/plg_mokosuitecross_medium.ini create mode 100644 source/packages/plg_mokosuitecross_medium/language/en-GB/plg_mokosuitecross_medium.sys.ini create mode 100644 source/packages/plg_mokosuitecross_medium/language/index.html create mode 100644 source/packages/plg_mokosuitecross_medium/medium.php create mode 100644 source/packages/plg_mokosuitecross_medium/medium.xml create mode 100644 source/packages/plg_mokosuitecross_medium/services/index.html create mode 100644 source/packages/plg_mokosuitecross_medium/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_medium/src/Extension/MediumService.php create mode 100644 source/packages/plg_mokosuitecross_medium/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_medium/src/index.html create mode 100644 source/packages/plg_mokosuitecross_mokosuitecalendar/language/en-GB/plg_mokosuitecross_mokosuitecalendar.ini create mode 100644 source/packages/plg_mokosuitecross_mokosuitecalendar/language/en-GB/plg_mokosuitecross_mokosuitecalendar.sys.ini create mode 100644 source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.php create mode 100644 source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml create mode 100644 source/packages/plg_mokosuitecross_mokosuitecalendar/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_mokosuitecalendar/src/Extension/CalendarService.php create mode 100644 source/packages/plg_mokosuitecross_mokosuitegallery/language/en-GB/plg_mokosuitecross_mokosuitegallery.ini create mode 100644 source/packages/plg_mokosuitecross_mokosuitegallery/language/en-GB/plg_mokosuitecross_mokosuitegallery.sys.ini create mode 100644 source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.php create mode 100644 source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml create mode 100644 source/packages/plg_mokosuitecross_mokosuitegallery/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_mokosuitegallery/src/Extension/GalleryService.php create mode 100644 source/packages/plg_mokosuitecross_nostr/index.html create mode 100644 source/packages/plg_mokosuitecross_nostr/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_nostr/language/en-GB/plg_mokosuitecross_nostr.ini create mode 100644 source/packages/plg_mokosuitecross_nostr/language/en-GB/plg_mokosuitecross_nostr.sys.ini create mode 100644 source/packages/plg_mokosuitecross_nostr/language/index.html create mode 100644 source/packages/plg_mokosuitecross_nostr/nostr.php create mode 100644 source/packages/plg_mokosuitecross_nostr/nostr.xml create mode 100644 source/packages/plg_mokosuitecross_nostr/services/index.html create mode 100644 source/packages/plg_mokosuitecross_nostr/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_nostr/src/Extension/NostrService.php create mode 100644 source/packages/plg_mokosuitecross_nostr/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_nostr/src/index.html create mode 100644 source/packages/plg_mokosuitecross_ntfy/index.html create mode 100644 source/packages/plg_mokosuitecross_ntfy/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_ntfy/language/en-GB/plg_mokosuitecross_ntfy.ini create mode 100644 source/packages/plg_mokosuitecross_ntfy/language/en-GB/plg_mokosuitecross_ntfy.sys.ini create mode 100644 source/packages/plg_mokosuitecross_ntfy/language/index.html create mode 100644 source/packages/plg_mokosuitecross_ntfy/ntfy.php create mode 100644 source/packages/plg_mokosuitecross_ntfy/ntfy.xml create mode 100644 source/packages/plg_mokosuitecross_ntfy/services/index.html create mode 100644 source/packages/plg_mokosuitecross_ntfy/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_ntfy/src/Extension/NtfyService.php create mode 100644 source/packages/plg_mokosuitecross_ntfy/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_ntfy/src/index.html create mode 100644 source/packages/plg_mokosuitecross_pinterest/index.html create mode 100644 source/packages/plg_mokosuitecross_pinterest/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_pinterest/language/en-GB/plg_mokosuitecross_pinterest.ini create mode 100644 source/packages/plg_mokosuitecross_pinterest/language/en-GB/plg_mokosuitecross_pinterest.sys.ini create mode 100644 source/packages/plg_mokosuitecross_pinterest/language/index.html create mode 100644 source/packages/plg_mokosuitecross_pinterest/pinterest.php create mode 100644 source/packages/plg_mokosuitecross_pinterest/pinterest.xml create mode 100644 source/packages/plg_mokosuitecross_pinterest/services/index.html create mode 100644 source/packages/plg_mokosuitecross_pinterest/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_pinterest/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_pinterest/src/index.html create mode 100644 source/packages/plg_mokosuitecross_reddit/index.html create mode 100644 source/packages/plg_mokosuitecross_reddit/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_reddit/language/en-GB/plg_mokosuitecross_reddit.ini create mode 100644 source/packages/plg_mokosuitecross_reddit/language/en-GB/plg_mokosuitecross_reddit.sys.ini create mode 100644 source/packages/plg_mokosuitecross_reddit/language/index.html create mode 100644 source/packages/plg_mokosuitecross_reddit/reddit.php create mode 100644 source/packages/plg_mokosuitecross_reddit/reddit.xml create mode 100644 source/packages/plg_mokosuitecross_reddit/services/index.html create mode 100644 source/packages/plg_mokosuitecross_reddit/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_reddit/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_reddit/src/index.html create mode 100644 source/packages/plg_mokosuitecross_rssfeed/index.html create mode 100644 source/packages/plg_mokosuitecross_rssfeed/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_rssfeed/language/en-GB/plg_mokosuitecross_rssfeed.ini create mode 100644 source/packages/plg_mokosuitecross_rssfeed/language/en-GB/plg_mokosuitecross_rssfeed.sys.ini create mode 100644 source/packages/plg_mokosuitecross_rssfeed/language/index.html create mode 100644 source/packages/plg_mokosuitecross_rssfeed/rssfeed.php create mode 100644 source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml create mode 100644 source/packages/plg_mokosuitecross_rssfeed/services/index.html create mode 100644 source/packages/plg_mokosuitecross_rssfeed/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_rssfeed/src/Extension/RssfeedService.php create mode 100644 source/packages/plg_mokosuitecross_rssfeed/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_rssfeed/src/index.html create mode 100644 source/packages/plg_mokosuitecross_sendgrid/index.html create mode 100644 source/packages/plg_mokosuitecross_sendgrid/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_sendgrid/language/en-GB/plg_mokosuitecross_sendgrid.ini create mode 100644 source/packages/plg_mokosuitecross_sendgrid/language/en-GB/plg_mokosuitecross_sendgrid.sys.ini create mode 100644 source/packages/plg_mokosuitecross_sendgrid/language/index.html create mode 100644 source/packages/plg_mokosuitecross_sendgrid/sendgrid.php create mode 100644 source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml create mode 100644 source/packages/plg_mokosuitecross_sendgrid/services/index.html create mode 100644 source/packages/plg_mokosuitecross_sendgrid/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_sendgrid/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_sendgrid/src/index.html rename {src/packages/plg_mokojoomcross_slack/language => source/packages/plg_mokosuitecross_slack}/index.html (100%) rename {src/packages/plg_mokojoomcross_slack/services => source/packages/plg_mokosuitecross_slack/language/en-GB}/index.html (100%) create mode 100644 source/packages/plg_mokosuitecross_slack/language/en-GB/plg_mokosuitecross_slack.ini create mode 100644 source/packages/plg_mokosuitecross_slack/language/en-GB/plg_mokosuitecross_slack.sys.ini rename {src/packages/plg_mokojoomcross_slack/src/Extension => source/packages/plg_mokosuitecross_slack/language}/index.html (100%) rename {src/packages/plg_mokojoomcross_slack/src => source/packages/plg_mokosuitecross_slack/services}/index.html (100%) rename {src/packages/plg_mokojoomcross_slack => source/packages/plg_mokosuitecross_slack}/services/provider.php (81%) rename {src/packages/plg_mokojoomcross_slack => source/packages/plg_mokosuitecross_slack}/slack.php (90%) create mode 100644 source/packages/plg_mokosuitecross_slack/slack.xml rename {src/packages/plg_mokojoomcross_slack => source/packages/plg_mokosuitecross_slack}/src/Extension/SlackService.php (77%) rename {src/packages/plg_mokojoomcross_telegram => source/packages/plg_mokosuitecross_slack/src/Extension}/index.html (100%) rename {src/packages/plg_mokojoomcross_telegram/language/en-GB => source/packages/plg_mokosuitecross_slack/src}/index.html (100%) create mode 100644 source/packages/plg_mokosuitecross_teams/index.html create mode 100644 source/packages/plg_mokosuitecross_teams/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_teams/language/en-GB/plg_mokosuitecross_teams.ini create mode 100644 source/packages/plg_mokosuitecross_teams/language/en-GB/plg_mokosuitecross_teams.sys.ini create mode 100644 source/packages/plg_mokosuitecross_teams/language/index.html create mode 100644 source/packages/plg_mokosuitecross_teams/services/index.html create mode 100644 source/packages/plg_mokosuitecross_teams/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_teams/src/Extension/TeamsService.php create mode 100644 source/packages/plg_mokosuitecross_teams/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_teams/src/index.html create mode 100644 source/packages/plg_mokosuitecross_teams/teams.php create mode 100644 source/packages/plg_mokosuitecross_teams/teams.xml rename {src/packages/plg_mokojoomcross_telegram/language => source/packages/plg_mokosuitecross_telegram}/index.html (100%) rename {src/packages/plg_mokojoomcross_telegram/services => source/packages/plg_mokosuitecross_telegram/language/en-GB}/index.html (100%) create mode 100644 source/packages/plg_mokosuitecross_telegram/language/en-GB/plg_mokosuitecross_telegram.ini create mode 100644 source/packages/plg_mokosuitecross_telegram/language/en-GB/plg_mokosuitecross_telegram.sys.ini rename {src/packages/plg_mokojoomcross_telegram/src/Extension => source/packages/plg_mokosuitecross_telegram/language}/index.html (100%) rename {src/packages/plg_mokojoomcross_telegram/src => source/packages/plg_mokosuitecross_telegram/services}/index.html (100%) rename {src/packages/plg_mokojoomcross_telegram => source/packages/plg_mokosuitecross_telegram}/services/provider.php (81%) rename {src/packages/plg_mokojoomcross_telegram => source/packages/plg_mokosuitecross_telegram}/src/Extension/TelegramService.php (80%) rename {src/packages/plg_mokojoomcross_twitter => source/packages/plg_mokosuitecross_telegram/src/Extension}/index.html (100%) rename {src/packages/plg_mokojoomcross_twitter/language/en-GB => source/packages/plg_mokosuitecross_telegram/src}/index.html (100%) rename {src/packages/plg_mokojoomcross_telegram => source/packages/plg_mokosuitecross_telegram}/telegram.php (90%) rename {src/packages/plg_mokojoomcross_telegram => source/packages/plg_mokosuitecross_telegram}/telegram.xml (55%) create mode 100644 source/packages/plg_mokosuitecross_threads/index.html create mode 100644 source/packages/plg_mokosuitecross_threads/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_threads/language/en-GB/plg_mokosuitecross_threads.ini create mode 100644 source/packages/plg_mokosuitecross_threads/language/en-GB/plg_mokosuitecross_threads.sys.ini create mode 100644 source/packages/plg_mokosuitecross_threads/language/index.html create mode 100644 source/packages/plg_mokosuitecross_threads/services/index.html create mode 100644 source/packages/plg_mokosuitecross_threads/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_threads/src/Extension/ThreadsService.php create mode 100644 source/packages/plg_mokosuitecross_threads/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_threads/src/index.html create mode 100644 source/packages/plg_mokosuitecross_threads/threads.php create mode 100644 source/packages/plg_mokosuitecross_threads/threads.xml create mode 100644 source/packages/plg_mokosuitecross_tiktok/index.html create mode 100644 source/packages/plg_mokosuitecross_tiktok/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_tiktok/language/en-GB/plg_mokosuitecross_tiktok.ini create mode 100644 source/packages/plg_mokosuitecross_tiktok/language/en-GB/plg_mokosuitecross_tiktok.sys.ini create mode 100644 source/packages/plg_mokosuitecross_tiktok/language/index.html create mode 100644 source/packages/plg_mokosuitecross_tiktok/services/index.html create mode 100644 source/packages/plg_mokosuitecross_tiktok/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_tiktok/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_tiktok/src/index.html create mode 100644 source/packages/plg_mokosuitecross_tiktok/tiktok.php create mode 100644 source/packages/plg_mokosuitecross_tiktok/tiktok.xml create mode 100644 source/packages/plg_mokosuitecross_tumblr/index.html create mode 100644 source/packages/plg_mokosuitecross_tumblr/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_tumblr/language/en-GB/plg_mokosuitecross_tumblr.ini create mode 100644 source/packages/plg_mokosuitecross_tumblr/language/en-GB/plg_mokosuitecross_tumblr.sys.ini create mode 100644 source/packages/plg_mokosuitecross_tumblr/language/index.html create mode 100644 source/packages/plg_mokosuitecross_tumblr/services/index.html create mode 100644 source/packages/plg_mokosuitecross_tumblr/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_tumblr/src/Extension/TumblrService.php create mode 100644 source/packages/plg_mokosuitecross_tumblr/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_tumblr/src/index.html create mode 100644 source/packages/plg_mokosuitecross_tumblr/tumblr.php create mode 100644 source/packages/plg_mokosuitecross_tumblr/tumblr.xml rename {src/packages/plg_mokojoomcross_twitter/language => source/packages/plg_mokosuitecross_twitter}/index.html (100%) rename {src/packages/plg_mokojoomcross_twitter/services => source/packages/plg_mokosuitecross_twitter/language/en-GB}/index.html (100%) create mode 100644 source/packages/plg_mokosuitecross_twitter/language/en-GB/plg_mokosuitecross_twitter.ini create mode 100644 source/packages/plg_mokosuitecross_twitter/language/en-GB/plg_mokosuitecross_twitter.sys.ini rename {src/packages/plg_mokojoomcross_twitter/src/Extension => source/packages/plg_mokosuitecross_twitter/language}/index.html (100%) rename {src/packages/plg_mokojoomcross_twitter/src => source/packages/plg_mokosuitecross_twitter/services}/index.html (100%) rename {src/packages/plg_mokojoomcross_twitter => source/packages/plg_mokosuitecross_twitter}/services/provider.php (81%) create mode 100644 source/packages/plg_mokosuitecross_twitter/src/Extension/TwitterService.php rename {src/packages/plg_system_mokojoomcross => source/packages/plg_mokosuitecross_twitter/src/Extension}/index.html (100%) rename {src/packages/plg_system_mokojoomcross/language/en-GB => source/packages/plg_mokosuitecross_twitter/src}/index.html (100%) rename {src/packages/plg_mokojoomcross_twitter => source/packages/plg_mokosuitecross_twitter}/twitter.php (90%) rename {src/packages/plg_mokojoomcross_twitter => source/packages/plg_mokosuitecross_twitter}/twitter.xml (55%) create mode 100644 source/packages/plg_mokosuitecross_webhook/index.html create mode 100644 source/packages/plg_mokosuitecross_webhook/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_webhook/language/en-GB/plg_mokosuitecross_webhook.ini create mode 100644 source/packages/plg_mokosuitecross_webhook/language/en-GB/plg_mokosuitecross_webhook.sys.ini create mode 100644 source/packages/plg_mokosuitecross_webhook/language/index.html create mode 100644 source/packages/plg_mokosuitecross_webhook/services/index.html create mode 100644 source/packages/plg_mokosuitecross_webhook/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_webhook/src/Extension/WebhookService.php create mode 100644 source/packages/plg_mokosuitecross_webhook/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_webhook/src/index.html create mode 100644 source/packages/plg_mokosuitecross_webhook/webhook.php create mode 100644 source/packages/plg_mokosuitecross_webhook/webhook.xml create mode 100644 source/packages/plg_mokosuitecross_whatsapp/index.html create mode 100644 source/packages/plg_mokosuitecross_whatsapp/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_whatsapp/language/en-GB/plg_mokosuitecross_whatsapp.ini create mode 100644 source/packages/plg_mokosuitecross_whatsapp/language/en-GB/plg_mokosuitecross_whatsapp.sys.ini create mode 100644 source/packages/plg_mokosuitecross_whatsapp/language/index.html create mode 100644 source/packages/plg_mokosuitecross_whatsapp/services/index.html create mode 100644 source/packages/plg_mokosuitecross_whatsapp/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_whatsapp/src/Extension/WhatsappService.php create mode 100644 source/packages/plg_mokosuitecross_whatsapp/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_whatsapp/src/index.html create mode 100644 source/packages/plg_mokosuitecross_whatsapp/whatsapp.php create mode 100644 source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml create mode 100644 source/packages/plg_mokosuitecross_wordpress/index.html create mode 100644 source/packages/plg_mokosuitecross_wordpress/language/en-GB/index.html create mode 100644 source/packages/plg_mokosuitecross_wordpress/language/en-GB/plg_mokosuitecross_wordpress.ini create mode 100644 source/packages/plg_mokosuitecross_wordpress/language/en-GB/plg_mokosuitecross_wordpress.sys.ini create mode 100644 source/packages/plg_mokosuitecross_wordpress/language/index.html create mode 100644 source/packages/plg_mokosuitecross_wordpress/services/index.html create mode 100644 source/packages/plg_mokosuitecross_wordpress/services/provider.php create mode 100644 source/packages/plg_mokosuitecross_wordpress/src/Extension/WordpressService.php create mode 100644 source/packages/plg_mokosuitecross_wordpress/src/Extension/index.html create mode 100644 source/packages/plg_mokosuitecross_wordpress/src/index.html create mode 100644 source/packages/plg_mokosuitecross_wordpress/wordpress.php create mode 100644 source/packages/plg_mokosuitecross_wordpress/wordpress.xml rename {src/packages/plg_system_mokojoomcross/language => source/packages/plg_system_mokosuitecross}/index.html (100%) rename {src/packages/plg_system_mokojoomcross/services => source/packages/plg_system_mokosuitecross/language/en-GB}/index.html (100%) create mode 100644 source/packages/plg_system_mokosuitecross/language/en-GB/plg_system_mokosuitecross.ini create mode 100644 source/packages/plg_system_mokosuitecross/language/en-GB/plg_system_mokosuitecross.sys.ini rename {src/packages/plg_system_mokojoomcross/src/Extension => source/packages/plg_system_mokosuitecross/language}/index.html (100%) create mode 100644 source/packages/plg_system_mokosuitecross/mokosuitecross.php rename src/packages/plg_system_mokojoomcross/mokojoomcross.xml => source/packages/plg_system_mokosuitecross/mokosuitecross.xml (55%) rename {src/packages/plg_system_mokojoomcross/src => source/packages/plg_system_mokosuitecross/services}/index.html (100%) rename {src/packages/plg_system_mokojoomcross => source/packages/plg_system_mokosuitecross}/services/provider.php (83%) create mode 100644 source/packages/plg_system_mokosuitecross/src/Extension/MokoSuiteCross.php rename {src/packages/plg_webservices_mokojoomcross => source/packages/plg_system_mokosuitecross/src/Extension}/index.html (100%) rename {src/packages/plg_webservices_mokojoomcross/language/en-GB => source/packages/plg_system_mokosuitecross/src}/index.html (100%) create mode 100644 source/packages/plg_system_mokosuitecross_events/language/en-GB/plg_system_mokosuitecross_events.ini create mode 100644 source/packages/plg_system_mokosuitecross_events/language/en-GB/plg_system_mokosuitecross_events.sys.ini create mode 100644 source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.php create mode 100644 source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml create mode 100644 source/packages/plg_system_mokosuitecross_events/services/provider.php create mode 100644 source/packages/plg_system_mokosuitecross_events/src/Extension/MokoSuiteCrossEvents.php create mode 100644 source/packages/plg_system_mokosuitecross_gallery/language/en-GB/plg_system_mokosuitecross_gallery.ini create mode 100644 source/packages/plg_system_mokosuitecross_gallery/language/en-GB/plg_system_mokosuitecross_gallery.sys.ini create mode 100644 source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.php create mode 100644 source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml create mode 100644 source/packages/plg_system_mokosuitecross_gallery/services/provider.php create mode 100644 source/packages/plg_system_mokosuitecross_gallery/src/Extension/MokoSuiteCrossGallery.php rename {src/packages/plg_webservices_mokojoomcross/language => source/packages/plg_task_mokosuitecross}/index.html (100%) rename {src/packages/plg_webservices_mokojoomcross/services => source/packages/plg_task_mokosuitecross/language/en-GB}/index.html (100%) create mode 100644 source/packages/plg_task_mokosuitecross/language/en-GB/plg_task_mokosuitecross.ini create mode 100644 source/packages/plg_task_mokosuitecross/language/en-GB/plg_task_mokosuitecross.sys.ini rename {src/packages/plg_webservices_mokojoomcross/src/Extension => source/packages/plg_task_mokosuitecross/language}/index.html (100%) create mode 100644 source/packages/plg_task_mokosuitecross/mokosuitecross.php rename src/packages/plg_mokojoomcross_slack/slack.xml => source/packages/plg_task_mokosuitecross/mokosuitecross.xml (50%) rename {src/packages/plg_webservices_mokojoomcross/src => source/packages/plg_task_mokosuitecross/services}/index.html (100%) create mode 100644 source/packages/plg_task_mokosuitecross/services/provider.php create mode 100644 source/packages/plg_task_mokosuitecross/src/Extension/MokoSuiteCrossTask.php create mode 100644 source/packages/plg_task_mokosuitecross/src/Extension/index.html create mode 100644 source/packages/plg_task_mokosuitecross/src/index.html create mode 100644 source/packages/plg_webservices_mokosuitecross/index.html create mode 100644 source/packages/plg_webservices_mokosuitecross/language/en-GB/index.html create mode 100644 source/packages/plg_webservices_mokosuitecross/language/en-GB/plg_webservices_mokosuitecross.ini create mode 100644 source/packages/plg_webservices_mokosuitecross/language/en-GB/plg_webservices_mokosuitecross.sys.ini create mode 100644 source/packages/plg_webservices_mokosuitecross/language/index.html rename src/packages/plg_webservices_mokojoomcross/mokojoomcross.php => source/packages/plg_webservices_mokosuitecross/mokosuitecross.php (100%) rename src/packages/plg_webservices_mokojoomcross/mokojoomcross.xml => source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml (66%) create mode 100644 source/packages/plg_webservices_mokosuitecross/services/index.html rename {src/packages/plg_webservices_mokojoomcross => source/packages/plg_webservices_mokosuitecross}/services/provider.php (80%) create mode 100644 source/packages/plg_webservices_mokosuitecross/src/Extension/MokoSuiteCrossWebServices.php create mode 100644 source/packages/plg_webservices_mokosuitecross/src/Extension/index.html create mode 100644 source/packages/plg_webservices_mokosuitecross/src/index.html create mode 100644 source/pkg_mokosuitecross.xml create mode 100644 source/script.php delete mode 100644 src/language/en-GB/pkg_mokojoomcross.sys.ini delete mode 100644 src/packages/com_mokojoomcross/forms/filter_posts.xml delete mode 100644 src/packages/com_mokojoomcross/language/en-GB/com_mokojoomcross.sys.ini delete mode 100644 src/packages/com_mokojoomcross/sql/uninstall.mysql.sql delete mode 100644 src/packages/com_mokojoomcross/src/Model/ServiceModel.php delete mode 100644 src/packages/com_mokojoomcross/src/View/Logs/HtmlView.php delete mode 100644 src/packages/plg_content_mokojoomcross/language/en-GB/plg_content_mokojoomcross.ini delete mode 100644 src/packages/plg_mokojoomcross_bluesky/bluesky.xml delete mode 100644 src/packages/plg_mokojoomcross_bluesky/language/en-GB/plg_mokojoomcross_bluesky.ini delete mode 100644 src/packages/plg_mokojoomcross_discord/discord.xml delete mode 100644 src/packages/plg_mokojoomcross_discord/language/en-GB/plg_mokojoomcross_discord.ini delete mode 100644 src/packages/plg_mokojoomcross_facebook/facebook.xml delete mode 100644 src/packages/plg_mokojoomcross_facebook/language/en-GB/plg_mokojoomcross_facebook.ini delete mode 100644 src/packages/plg_mokojoomcross_linkedin/language/en-GB/plg_mokojoomcross_linkedin.ini delete mode 100644 src/packages/plg_mokojoomcross_linkedin/linkedin.xml delete mode 100644 src/packages/plg_mokojoomcross_mailchimp/language/en-GB/plg_mokojoomcross_mailchimp.ini delete mode 100644 src/packages/plg_mokojoomcross_mailchimp/mailchimp.xml delete mode 100644 src/packages/plg_mokojoomcross_mastodon/language/en-GB/plg_mokojoomcross_mastodon.ini delete mode 100644 src/packages/plg_mokojoomcross_mastodon/mastodon.xml delete mode 100644 src/packages/plg_mokojoomcross_slack/language/en-GB/plg_mokojoomcross_slack.ini delete mode 100644 src/packages/plg_mokojoomcross_telegram/language/en-GB/plg_mokojoomcross_telegram.ini delete mode 100644 src/packages/plg_mokojoomcross_twitter/language/en-GB/plg_mokojoomcross_twitter.ini delete mode 100644 src/packages/plg_mokojoomcross_twitter/language/en-GB/plg_mokojoomcross_twitter.sys.ini delete mode 100644 src/packages/plg_system_mokojoomcross/language/en-GB/plg_system_mokojoomcross.ini delete mode 100644 src/packages/plg_system_mokojoomcross/language/en-GB/plg_system_mokojoomcross.sys.ini delete mode 100644 src/packages/plg_webservices_mokojoomcross/language/en-GB/plg_webservices_mokojoomcross.ini delete mode 100644 src/packages/plg_webservices_mokojoomcross/language/en-GB/plg_webservices_mokojoomcross.sys.ini create mode 100644 wiki/Developer-Guide.md create mode 100644 wiki/Message-Templates.md create mode 100644 wiki/REST-API.md create mode 100644 wiki/Services.md create mode 100644 wiki/Troubleshooting.md diff --git a/.mokogitea/CLAUDE.md b/.mokogitea/CLAUDE.md new file mode 100644 index 0000000..8c1ca4b --- /dev/null +++ b/.mokogitea/CLAUDE.md @@ -0,0 +1,83 @@ +# MokoSuiteCross + +Cross-posting Joomla content to social media, email marketing, and chat platforms with plugin-based services. + +## Quick Reference + +| Field | Value | +|---|---| +| **Package** | `pkg_mokosuitecross` | +| **Language** | PHP 8.1+ | +| **Branch** | develop on `dev`, merge to `main` (protected) | +| **Wiki** | [MokoSuiteCross Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/wiki) | + +## Commands + +```bash +make build # Build package ZIP +make lint # Run linters +make validate # Validate structure +make release # Full release pipeline +make clean # Clean build artifacts +composer install # Install PHP dependencies +``` + +## Architecture + +Joomla **package** with core extensions + pluggable service plugins: + +### com_mokosuitecross (Component) +- Admin backend: dashboard, services, post queue, templates, logs +- Joomla 5/6 MVC: Dashboard, Services, Posts, Logs (list/edit each) +- Namespace: `Joomla\Component\MokoSuiteCross\Administrator` + +### plg_system_mokosuitecross (System Plugin) +- Hooks `onContentAfterSave` to trigger cross-posting on article publish +- Dispatches to registered service plugins via `mokosuitecross` plugin group + +### plg_content_mokosuitecross (Content Plugin) +- Adds cross-post status badges to articles via `onContentBeforeDisplay` + +### plg_webservices_mokosuitecross (WebServices Plugin) +- REST API endpoints for posts and services + +### Service Plugins (mokosuitecross group) +Each platform is a separate plugin implementing `MokoSuiteCrossServiceInterface`: +- `plg_mokosuitecross_facebook` — Facebook/Meta Graph API +- `plg_mokosuitecross_twitter` — X/Twitter API v2 +- `plg_mokosuitecross_linkedin` — LinkedIn Share API +- `plg_mokosuitecross_mastodon` — Mastodon API +- `plg_mokosuitecross_bluesky` — Bluesky AT Protocol +- `plg_mokosuitecross_mailchimp` — Mailchimp Campaigns API +- `plg_mokosuitecross_telegram` — Telegram Bot API +- `plg_mokosuitecross_discord` — Discord Webhooks +- `plg_mokosuitecross_slack` — Slack Incoming Webhooks + +### Database Schema + +- `#__mokosuitecross_services` — service configs (credentials as individual fields, not JSON) +- `#__mokosuitecross_posts` — post queue (status: queued/posting/posted/failed/scheduled) +- `#__mokosuitecross_templates` — message templates per service type +- `#__mokosuitecross_logs` — activity logs with level and context + +## Rules + +- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js` +- **Never commit** API keys, tokens, or credentials — these go in Joomla's encrypted params +- **Attribution**: `Authored-by: Moko Consulting` +- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`) +- **Minification**: handled at build time (CI) +- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files +- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/mokoplatform/wiki/Home) +- **UX**: service credentials as individual form fields, not JSON blobs; dashboard link in toolbar + +## Coding Standards + +- PHP 8.1+ minimum +- Joomla 5/6 DI container pattern: `services/provider.php` → Extension class +- Legacy stub `.php` file required for plugin loader but empty +- `SubscriberInterface` for event subscription (not `on*` method naming) +- `bind() → check() → store()` for Table operations (not `save()`) +- Language file placement: site (no `folder`) vs admin (`folder="administrator"`) +- SPDX license headers on all PHP files +- Service plugins MUST implement `MokoSuiteCrossServiceInterface` diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml new file mode 100644 index 0000000..74c6081 --- /dev/null +++ b/.mokogitea/manifest.xml @@ -0,0 +1,26 @@ + + + + MokoSuiteCross + Package - MokoSuiteCross + MokoConsulting + Cross-posting Joomla content to social media, email marketing, and chat platforms + 01.00.27 + GNU General Public License v3 + + + joomla + 05.00.00 + https://git.mokoconsulting.tech/MokoConsulting/mokoplatform + + + PHP + joomla-extension + source/ + + + true + true + https://git.mokoconsulting.tech/{org}/{repo}/updates.xml + + diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 3be5d44..a0ca422 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -4,15 +4,15 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: mokocli.Release -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli +# INGROUP: mokoplatform.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokoplatform # PATH: /templates/workflows/universal/auto-release.yml.template # VERSION: 05.00.00 # BRIEF: Universal build & release � detects platform from manifest.xml # -# +=======================================================================+ +# +========================================================================+ # | UNIVERSAL BUILD & RELEASE PIPELINE | -# +=======================================================================+ +# +========================================================================+ # | | # | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | # | | @@ -21,7 +21,7 @@ # | dolibarr: mod*.class.php, update.txt, dev version reset | # | generic: README-only, no update stream | # | | -# +=======================================================================+ +# +========================================================================+ name: "Universal: Build & Release" @@ -51,7 +51,7 @@ permissions: contents: write jobs: - # ── PR Opened → Rename branch to RC and build RC release ───────────────────────── + # ── PR Opened → Rename branch to RC and build RC release ───────────────────── promote-rc: name: Promote to RC runs-on: release @@ -66,25 +66,25 @@ jobs: token: ${{ secrets.MOKOGITEA_TOKEN }} fetch-depth: 1 - - name: Setup mokocli tools + - name: Setup mokoplatform tools env: MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting run: | - if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then - echo Using pre-installed /opt/mokocli - echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV + if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then + echo Using pre-installed /opt/mokoplatform + echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV else echo Falling back to fresh clone if ! command -v composer > /dev/null 2>&1; then sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 fi - rm -rf /tmp/mokocli - CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git - git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli - cd /tmp/mokocli + rm -rf /tmp/mokoplatform-api + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git + git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api + cd /tmp/mokoplatform-api composer install --no-dev --no-interaction --quiet - echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV + echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV fi - name: Rename branch to rc @@ -109,47 +109,13 @@ jobs: --path . --stability rc --bump minor --branch rc \ --token "${{ secrets.MOKOGITEA_TOKEN }}" - - name: Update RC release notes from CHANGELOG.md - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - # Extract [Unreleased] section from changelog - NOTES="" - if [ -f "CHANGELOG.md" ]; then - NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) - fi - [ -z "$NOTES" ] && NOTES="Release candidate" - - # Find the RC release and update its body - RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/releases/tags/release-candidate" \ - | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) - - if [ -n "$RELEASE_ID" ]; then - python3 -c " - import json, urllib.request - body = open('/dev/stdin').read() - payload = json.dumps({'body': body}).encode() - req = urllib.request.Request( - '${API_BASE}/releases/${RELEASE_ID}', - data=payload, method='PATCH', - headers={ - 'Authorization': 'token ${TOKEN}', - 'Content-Type': 'application/json' - }) - urllib.request.urlopen(req) - " <<< "$NOTES" - echo "RC release notes updated from CHANGELOG.md" - fi - - name: Summary if: always() run: | echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY - # ── Merged PR → Build & Release (or promote RC to stable) ───────────────────────── + # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── release: name: Build & Release Pipeline runs-on: release @@ -183,131 +149,50 @@ jobs: fi echo "No conflict markers found" - - name: Setup mokocli tools + - name: Setup mokoplatform tools env: MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' run: | - if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then - echo Using pre-installed /opt/mokocli - echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV + if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then + echo Using pre-installed /opt/mokoplatform + echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV else echo Falling back to fresh clone if ! command -v composer > /dev/null 2>&1; then sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 fi - rm -rf /tmp/mokocli - CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git - git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli - cd /tmp/mokocli + rm -rf /tmp/mokoplatform-api + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git + git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api + cd /tmp/mokoplatform-api composer install --no-dev --no-interaction --quiet - echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV + echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV fi - - name: "Detect platform" - id: platform - run: | - php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true - php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true - - - name: "Determine version bump level" - id: bump - run: | - # Fix/patch branches: version was already bumped by pre-release, just strip suffix - # Feature/dev branches: bump minor for the new stable release - HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}" - case "$HEAD_REF" in - fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;; - *) BUMP="minor" ;; - esac - echo "level=${BUMP}" >> "$GITHUB_OUTPUT" - echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})" - - name: "Publish stable release" run: | - BUMP_FLAG="" - if [ "${{ steps.bump.outputs.level }}" != "none" ]; then - BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}" - fi php ${MOKO_CLI}/release_publish.php \ - --path . --stability stable ${BUMP_FLAG} --branch main \ + --path . --stability stable --bump minor --branch main \ --token "${{ secrets.MOKOGITEA_TOKEN }}" - - name: "Read published version" - id: version - run: | - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "") - VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') - [ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT" - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - PLATFORM="${{ steps.platform.outputs.platform }}" - if [[ "$PLATFORM" == joomla* ]]; then - echo "tag=stable" >> "$GITHUB_OUTPUT" - echo "release_tag=stable" >> "$GITHUB_OUTPUT" - else - echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT" - echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT" - fi - echo "branch=main" >> "$GITHUB_OUTPUT" - echo "Published version: ${VERSION}" - - - name: "Create semver tag for non-Joomla repos" - id: semver - if: | - steps.version.outputs.skip != 'true' && - !startsWith(steps.platform.outputs.platform, 'joomla') - run: | - VERSION="${{ steps.version.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - SEMVER_TAG="v${VERSION}" - - echo "Creating semver tag: ${SEMVER_TAG}" - - # Create the git tag via API - HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \ - -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/tags" \ - -d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000") - - if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then - echo "Created semver tag: ${SEMVER_TAG}" - elif [ "$HTTP_CODE" = "409" ]; then - echo "Semver tag ${SEMVER_TAG} already exists (skipped)" - else - echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})" - fi - - echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT" - - - name: Update release notes and promote changelog + - name: Update release notes from CHANGELOG.md run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - # Get the stable release info (version and ID) - RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}') - RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true) - # Extract version from release name (e.g. "06.17.00" or "v06.17.00") - VERSION=$(python3 -c " - import json, sys, re - r = json.load(sys.stdin) - name = r.get('name', '') - m = re.search(r'(\d+\.\d+\.\d+)', name) - print(m.group(1) if m else '') - " <<< "$RELEASE_JSON" 2>/dev/null || true) # Extract [Unreleased] section from changelog - NOTES="" if [ -f "CHANGELOG.md" ]; then NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) + [ -z "$NOTES" ] && NOTES="Stable release" + else + NOTES="Stable release" fi - [ -z "$NOTES" ] && NOTES="Stable release" # Update release body via API + RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ + "${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + if [ -n "$RELEASE_ID" ]; then python3 -c " import json, urllib.request @@ -317,7 +202,7 @@ jobs: '${API_BASE}/releases/${RELEASE_ID}', data=payload, method='PATCH', headers={ - 'Authorization': 'token ${TOKEN}', + 'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}', 'Content-Type': 'application/json' }) urllib.request.urlopen(req) @@ -325,24 +210,6 @@ jobs: echo "Release notes updated from CHANGELOG.md" fi - # Promote [Unreleased] → [version] in CHANGELOG.md and reset - if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then - DATE=$(date +%Y-%m-%d) - python3 -c " - import sys - version, date = sys.argv[1], sys.argv[2] - content = open('CHANGELOG.md').read() - old = '## [Unreleased]' - new = f'## [Unreleased]\n\n## [{version}] --- {date}' - content = content.replace(old, new, 1) - open('CHANGELOG.md', 'w').write(content) - " "$VERSION" "$DATE" - git add CHANGELOG.md - git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true - git push origin main || true - echo "Changelog promoted: [Unreleased] → [${VERSION}]" - fi - # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - name: "Step 9: Mirror release to GitHub" if: >- diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 59e2557..9e1538b 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Automation -# VERSION: 01.01.01 +# INGROUP: mokoplatform.Automation +# VERSION: 01.00.27 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" @@ -28,7 +28,7 @@ jobs: steps: - name: Create branch and comment run: | - TOKEN="${{ secrets.GA_TOKEN }}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" ISSUE_NUM="${{ github.event.issue.number }}" ISSUE_TITLE="${{ github.event.issue.title }}" diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index d34108c..004a22a 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.CI -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# INGROUP: mokoplatform.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokoplatform # PATH: /templates/workflows/universal/pr-check.yml.template # VERSION: 09.23.00 # BRIEF: PR gate — branch policy + code validation before merge @@ -185,11 +185,11 @@ jobs: echo "::error file=${file}::Missing JEXEC guard: ${file}" ERRORS=$((ERRORS + 1)) fi - done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0) + done < <(find . -name "*.php" \( -path "*/source/*" -o -path "*/src/*" \) -not -path "./.git/*" -not -path "./vendor/*" -print0) if [ "$ERRORS" -gt 0 ]; then echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard" echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY - echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY + echo "${ERRORS} file(s) are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY exit 1 fi echo "JEXEC guard: OK" @@ -198,7 +198,8 @@ jobs: if: steps.platform.outputs.platform == 'joomla' run: | MISSING=0 - SOURCE_DIR="src" + SOURCE_DIR="source" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src" [ ! -d "$SOURCE_DIR" ] && exit 0 while IFS= read -r dir; do if [ ! -f "${dir}/index.html" ]; then @@ -246,7 +247,7 @@ jobs: echo "joomla.asset.json: valid" fi - # Validate all XML files in src/ are well-formed + # Validate all XML files in source/src are well-formed XML_ERRORS=0 if command -v php &> /dev/null; then while IFS= read -r -d '' xmlfile; do @@ -477,10 +478,11 @@ jobs: - name: Verify package source run: | - SOURCE_DIR="src" + SOURCE_DIR="source" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src" [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" if [ ! -d "$SOURCE_DIR" ]; then - echo "::warning::No src/ or htdocs/ directory" + echo "::warning::No source/, src/, or htdocs/ directory" exit 0 fi FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 4fd80eb..f98bd0c 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -4,26 +4,23 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: mokocli.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# INGROUP: mokoplatform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform # PATH: /templates/workflows/universal/pre-release.yml.template # VERSION: 05.01.00 -# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches +# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch name: "Universal: Pre-Release" on: - push: + pull_request: + types: [closed] branches: - dev - - 'fix/**' - - 'patch/**' - - 'hotfix/**' - - 'bugfix/**' - - 'chore/**' - - alpha - - beta - - rc + pull_request_target: + types: [synchronize, opened, reopened] + branches: + - main workflow_dispatch: inputs: stability: @@ -46,11 +43,12 @@ env: jobs: build: - name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})" + name: "Build Pre-Release (${{ inputs.stability || 'development' }})" runs-on: release if: >- github.event_name == 'workflow_dispatch' || - github.event_name == 'push' + (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') || + (github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main') steps: - name: Checkout @@ -58,59 +56,40 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.MOKOGITEA_TOKEN }} - ref: ${{ github.ref_name }} + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} - - name: Setup mokocli tools + - name: Setup mokoplatform tools env: MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting run: | - # Use pre-installed /opt/mokocli if available (updated by cron every 6h) - if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then - echo Using pre-installed /opt/mokocli - echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV + # Use pre-installed /opt/mokoplatform if available (updated by cron every 6h) + if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/cli/manifest_element.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then + echo Using pre-installed /opt/mokoplatform + echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV else echo Falling back to fresh clone if ! command -v composer > /dev/null 2>&1; then sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 fi - rm -rf /tmp/mokocli - CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git - git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli - cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet - echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV + rm -rf /tmp/mokoplatform-api + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git + git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api + cd /tmp/mokoplatform-api && composer install --no-dev --no-interaction --quiet + echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV fi - name: Detect platform id: platform run: | - # Auto-detect and update platform if not set in manifest - php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true php ${MOKO_CLI}/manifest_read.php --path . --github-output - - name: Check platform eligibility (Joomla only) - id: eligibility - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then - echo "proceed=true" >> "$GITHUB_OUTPUT" - else - echo "proceed=false" >> "$GITHUB_OUTPUT" - echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump" - fi - - name: Resolve metadata and bump version id: meta - if: steps.eligibility.outputs.proceed == 'true' run: | - # Auto-detect stability from branch name on push, or use input on dispatch - if [ "${{ github.event_name }}" = "push" ]; then - case "${{ github.ref_name }}" in - rc) STABILITY="release-candidate" ;; - alpha) STABILITY="alpha" ;; - beta) STABILITY="beta" ;; - *) STABILITY="development" ;; - esac + # Auto-detect stability: RC for PRs targeting main, else use input or default to development + if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then + STABILITY="release-candidate" else STABILITY="${{ inputs.stability || 'development' }}" fi @@ -178,7 +157,6 @@ jobs: - name: Create release id: release - if: steps.eligibility.outputs.proceed == 'true' run: | TAG="${{ steps.meta.outputs.tag }}" VERSION="${{ steps.meta.outputs.version }}" @@ -186,10 +164,9 @@ jobs: php ${MOKO_CLI}/release_create.php \ --path . --version "$VERSION" --tag "$TAG" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease + --repo "${GITEA_REPO}" --branch dev --prerelease - name: Update release notes from CHANGELOG.md - if: steps.eligibility.outputs.proceed == 'true' run: | TAG="${{ steps.meta.outputs.tag }}" VERSION="${{ steps.meta.outputs.version }}" @@ -226,7 +203,6 @@ jobs: - name: Build package and upload id: package - if: steps.eligibility.outputs.proceed == 'true' run: | VERSION="${{ steps.meta.outputs.version }}" TAG="${{ steps.meta.outputs.tag }}" @@ -240,7 +216,6 @@ jobs: # No need to build, commit, or sync updates.xml from workflows - name: "Delete lesser pre-release channels (cascade)" - if: steps.eligibility.outputs.proceed == 'true' continue-on-error: true run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index 154f77d..fed3014 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -7,8 +7,8 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: mokocli.Validation -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli +# INGROUP: mokoplatform.Validation +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokoplatform # PATH: /templates/workflows/joomla/repo_health.yml.template # VERSION: 09.23.00 # BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. @@ -33,8 +33,7 @@ on: - scripts - repo pull_request: - branches: - - main + push: permissions: contents: read @@ -297,17 +296,19 @@ jobs: missing_required=() missing_optional=() - # Source directory: src/ or htdocs/ (either is valid for extension repos) + # Source directory: source/, src/, or htdocs/ (any is valid for extension repos) SOURCE_DIR="" - if [ -d "src" ]; then + if [ -d "source" ]; then + SOURCE_DIR="source" + elif [ -d "src" ]; then SOURCE_DIR="src" elif [ -d "htdocs" ]; then SOURCE_DIR="htdocs" elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then - # Platform/tooling repos don't need src/ + # Platform/tooling repos don't need source/ SOURCE_DIR="" else - missing_required+=("src/ or htdocs/ (source directory required)") + missing_required+=("source/ or src/ or htdocs/ (source directory required)") fi for item in "${required_artifacts[@]}"; do diff --git a/CHANGELOG.md b/CHANGELOG.md index 5056276..e490374 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,201 @@ -All notable changes to MokoJoomCross will be documented in this file. +All notable changes to MokoSuiteCross will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). -## [01.01.00] --- 2026-06-19 +## [Unreleased] + +### Fixed +- **C-1 OauthController**: Added CSRF nonce validation to OAuth callback — session-based nonce is generated during `authorize()`, embedded in the state parameter, and verified in `callback()` to prevent CSRF attacks +- **C-2 DispatchController**: Added POST method enforcement — rejects non-POST requests with 405 status +- **C-5 ServiceModel**: Credential form fields (`cred_*`) are now collected into the `credentials` JSON column on save, and expanded back into individual fields on load — previously these fields were silently discarded +- **H-1 Event pattern**: Fixed Joomla 5 SubscriberInterface incompatibility where `onMokoSuiteCrossGetServices` by-reference pattern silently lost all service plugins — dispatchers now read plugin instances from Event ArrayAccess indices after dispatch +- **H-4 ServiceTable**: Added `check()` method with alias generation, required field validation (title, service_type), timestamp management, and JSON defaults for credentials/params +- **H-9 WebhookService**: Fixed credential key mismatch — `publish()` and `validateCredentials()` now use keys matching the service.xml form fields (`url`, `method`, `auth_type`, `bearer_token`, `basic_username`, `basic_password`, `content_type`) and properly apply Bearer/Basic auth headers +- **M-4 ServiceIconHelper**: Escaped `$extraClass` parameter in `renderIcon()` with `htmlspecialchars()` to prevent XSS +- **M-5 Content plugin**: Fixed double-escaped HTML in cross-post history panel — uses `setFieldAttribute()` to inject history HTML into the note field description after XML load, avoiding XML attribute encoding + +- **Content plugin**: Fixed `onContentBeforeDisplay` signature for Joomla 5/6 — now accepts `BeforeDisplayEvent` object instead of individual parameters + +- **QueueProcessor**: Replaced read-then-write DB lock with MySQL advisory locks (`GET_LOCK`/`RELEASE_LOCK`) to eliminate race condition +- **Twitter/X**: Replaced Bearer token auth with OAuth 1.0a (HMAC-SHA1) — Bearer tokens are app-only and cannot create tweets +- **service.xml**: Fixed missing closing `` tag on webhook method field +- **Views**: Added missing `Toolbar` and `Route` imports in Logs, Posts, Services, Template, Templates HtmlView files +- **13 service plugins**: Fixed broken `publish()` methods that had literal placeholder URLs instead of using credential values — ActivityPub, Blogger, Ghost, Google Business, Hashnode, Matrix, Medium, Nostr, RSS Feed, Threads, Tumblr, WhatsApp, WordPress +- **Ghost**: Proper JWT auth from `{id}:{secret}` admin API key format +- **WordPress**: Correct Basic Auth (not Bearer) with Application Passwords +- **Medium**: 2-step flow — fetch user ID via /v1/me, then post +- **Matrix**: PUT with transaction ID for idempotent message sending +- **Hashnode**: GraphQL mutation with proper query structure +- **Threads**: 2-step container creation + publish flow +- **WhatsApp**: Meta Cloud API with messaging_product payload +- **Nostr**: Stub with clear "not yet implemented" message (requires WebSocket) +- **RSS Feed**: Local service — no external API, always succeeds + + +### Added +- **ServiceIconHelper**: Centralised icon mapping for all 34 service types — replaces per-template icon arrays with `ServiceIconHelper::getIcon()` / `::renderIcon()` +- **Service Stats drill-down**: New `servicestats` view with per-service analytics — post counts, success rate, daily trend chart, recent posts table, and top articles list +- **Dashboard service links**: Service breakdown table rows now link to the per-service stats view with service type icons +- **Posts list icons**: Service type column in the posts list now shows the service icon +- **Category routing rules**: New `#__mokosuitecross_category_rules` table to whitelist services per Joomla category — if rules exist for a category, only those services receive posts; no rules = all services (backward compatible) +- **CrossPostDispatcher**: Category rule filtering integrated before per-article service filter in the dispatch loop +- **Template editor**: Live character counter below template body textarea with platform-aware limits (green/yellow/red badges) +- **Template editor**: Added `{tags}`, `{hashtags}`, and `{field:xxx}` rows to the placeholder reference table +- **Content plugin**: Cross-post history panel in article editor showing last 10 posts with status badges, service names, timestamps, and error messages +- **Config**: New "Category Rules" fieldset with explanatory note about the feature + +- **CrossPostDispatcher**: New static helper (`com_mokosuitecross/Helper/CrossPostDispatcher`) centralising dispatch logic for reuse by all source plugins +- **Content plugin**: Added `onContentAfterSave` and `onContentChangeState` handlers with Joomla 5/6 event compatibility, dispatching via `CrossPostDispatcher` +- **plg_system_mokosuitecross_events**: New source plugin for MokoSuiteCalendar — cross-posts calendar events when published +- **plg_system_mokosuitecross_gallery**: New source plugin for MokoSuiteGallery — cross-posts galleries and images when published + +- **Credential fields**: Added fields for 19 previously missing services (Pinterest, Tumblr, TikTok, Nostr, ActivityPub, Brevo, ConvertKit, Constant Contact, Hashnode, Blogger, Google Business, RSS Feed config) +- **Twitter**: Access Token and Access Token Secret fields for OAuth 1.0a +- **LinkedIn**: Refresh token field for automatic token renewal +- **Bluesky**: PDS URL field for self-hosted instances +- **Discord**: Username and avatar URL override fields +- **Mailchimp**: From name and from email fields +- **SendGrid**: From email and from name fields +- **Reddit**: Account password field for script-type OAuth +- **WordPress**: Default post status selector (draft/publish) +- **Dev.to**: Organization ID field +- **Ghost**: Default post status selector (draft/published) +- **Webhook**: Auth type selector (none/bearer/basic), auth token field, content type selector (JSON/form) +- **RSS Feed**: Feed title and max items config fields +- **OAuth services**: Added Pinterest, Tumblr, TikTok, Constant Contact, Blogger, Google Business to OAuth authorize flow +- **Developer Guide**: Comprehensive wiki page for building new service plugins +- **Help articles**: 42 KB articles on mokoconsulting.tech (overview, installation, 34 per-service guides, templates, queue, troubleshooting) +- **Service help link**: Per-service "Setup Guide" button in service edit sidebar links to the matching KB article +- **Evergreen re-sharing**: Articles can be marked as evergreen for automatic recurring cross-posts on a configurable interval (default 30 days) +- **Post edit form**: Full CRUD for queue posts — edit message, reschedule, change status, re-queue failed posts +- **Manual post creator**: New button in Post Queue toolbar to create manual cross-posts with article/service selection, custom message, and optional scheduling +- **Scheduled posts**: Calendar picker for scheduling posts to specific date/time; scheduled_at shown in queue list +- **Dashboard trend chart**: Chart.js line chart showing daily posted vs failed counts between stat cards and service breakdown +- **Dashboard date range filter**: Period selector (7/30/90 days, all time) filters service breakdown, top articles, and trend chart +- **Hashtag placeholders**: `{tags}` (comma-separated) and `{hashtags}` (#-prefixed space-separated) template placeholders from article tags +- **Posts service filter**: SQL-driven service dropdown filter in posts list, plus search filter by article title or message content +- **CSV export**: "Export CSV" toolbar button on posts list to download filtered post data as CSV +- **WordPress canonical URL**: WordPress cross-posts now include an "Originally published at" source link appended to content with the Joomla article URL +- **REST API dispatch endpoint**: `POST /api/v1/mokosuitecross/dispatch` — trigger cross-posts for an article via API with optional service filtering, duplicate guard, and template rendering + + +### Added (original) + +#### Core Engine +- Cross-posting engine dispatches articles to service plugins on publish +- System plugin hooks `onContentAfterSave` and `onContentChangeState` +- Duplicate guard prevents re-posting to services that already received an article +- Message template rendering with 8 placeholders: `{title}`, `{url}`, `{introtext}`, `{fulltext}`, `{image}`, `{category}`, `{author}`, `{date}` +- Custom `mokosuitecross` plugin group for extensible service architecture +- `MokoSuiteCrossServiceInterface` contract for all service plugins + +#### Admin Component (5 views) +- **Dashboard** — summary cards, posts-by-service analytics with success rates, top cross-posted articles, recent activity feed, PP Pro migration banner, page-load processing warning +- **Post Queue** — list with color-coded status badges, error messages, retry counts, platform post IDs, article/service columns, date filters +- **Services** — CRUD with service type selector (34 platforms organized by category), default/custom mode badges, publish toggle, credential editor +- **Templates** — CRUD for message templates, per-platform assignment, placeholder reference panel, template body preview +- **Activity Logs** — list with level badges (info/warning/error), service column, context data, level and search filters + +#### Queue Processing (3 methods) +- Joomla Scheduled Task plugin (`plg_task_mokosuitecross`) — preferred, processes 20 posts per run +- Page-load fallback via system plugin `onAfterRender` — configurable throttle interval, backend/frontend/both +- Shared `QueueProcessor` helper with DB lock to prevent concurrent execution +- Failed post retry with configurable max retries and exponential delay +- Scheduled post support (`scheduled_at` column) +- Automatic log cleanup based on configurable retention period + +#### Per-Article Controls +- "Cross-Posting" fieldset injected into article editor via `onContentPrepareForm` +- Skip cross-posting toggle per article +- Service selection checkboxes (unchecked = post to all enabled services) + +#### OAuth 2.0 +- `OAuthHelper` with authorization URL generation, code-to-token exchange, token storage +- Twitter PKCE flow support +- `OauthController` with authorize and callback endpoints +- Reads client ID/secret from service plugin params + +#### Perfect Publisher Pro Migration +- Reads `#__autotweet_channels` table with per-platform credential mapping +- Fallback extraction from component params when channel table missing +- Maps Facebook, Twitter, LinkedIn, Telegram, Discord, Slack, Mastodon +- Creates services in disabled state for manual verification +- One-click migration from dashboard + +#### Service Plugins (34 platforms) + +**Social Media (12)** +- Facebook / Meta — Graph API v19.0, default MokoWaaS app mode, page feed posting +- X / Twitter — API v2, OAuth 2.0 Bearer Token, 280 char limit +- LinkedIn — Share API v2, organization + personal profile, 3000 char limit +- Mastodon — API v1, multi-instance, hashtags, 500 char limit +- Bluesky — AT Protocol, session auth, app passwords, 300 char limit +- Threads (Meta) — Threads Publishing API, default app mode, 500 char limit +- Pinterest — Pins API v5, board selection, image-focused +- Reddit — OAuth2 link submission, subreddit selection +- Tumblr — API v2, link/text posts, OAuth 1.0a +- TikTok — Content Posting API, photo slideshows +- Nostr — NIP-01 event publishing, configurable relays +- ActivityPub — generic Fediverse (Pleroma, Akkoma, Misskey, Pixelfed) + +**Chat / Messaging (8)** +- Telegram — Bot API, default @MokoWaaSBot + custom bot, HTML/Markdown, 4096 chars +- Discord — Webhooks, default MokoWaaS webhook mode, embeds, 2000 chars +- Slack — Incoming Webhooks, default MokoWaaS webhook mode, Block Kit +- Microsoft Teams — Incoming Webhooks, default mode, Adaptive Cards +- Google Chat — Webhook API, card formatting +- WhatsApp Business — Meta Cloud API, template + free-form messages +- Matrix / Element — Client-Server API, self-hosted homeserver support +- Ntfy — Push notifications, priority levels, action buttons + +**Email / Newsletter (5)** +- Mailchimp — Campaigns API, audience selection, send/draft modes +- SendGrid — Marketing Campaigns API v3, Single Send creation +- Brevo (Sendinblue) — API v3, campaign creation +- ConvertKit — API v3, broadcast creation +- Constant Contact — API v3, campaign creation + +**Publishing / Blogging (6)** +- Medium — Publishing API, full HTML, canonical URL, tags +- WordPress — REST API v2, Application Passwords, category mapping +- Dev.to — Forem API, markdown, series support +- Ghost — Admin API v5, JWT auth, full HTML +- Hashnode — GraphQL API, cover image, tags +- Google Blogger — Blogger API v3, labels from categories + +**Business (1)** +- Google Business Profile — API v1, local posts (UPDATE/EVENT/OFFER) + +**Universal (2)** +- Generic Webhook — POST/PUT to any URL, JSON/form body, custom headers (IFTTT, Zapier, n8n, Make) +- RSS Feed — dedicated cross-post feed generation + +#### Plugin Configuration +- Telegram: default bot token, parse mode, link preview toggle +- Facebook: default page access token, default page ID +- Discord: default webhook URL, embed color +- Slack: default webhook URL +- LinkedIn: OAuth client ID/secret, redirect URI +- Mastodon: default instance URL, visibility, hashtags +- Bluesky: default PDS URL, auto link cards +- Mailchimp: default sender name/email, auto-send toggle +- Microsoft Teams: default webhook URL +- Threads: default webhook URL + +#### Infrastructure +- 7 CI/CD workflows: CI, auto-release, pre-release, auto-bump, update-server, cascade-dev, issue-branch +- Joomla update server (`updates.xml`) with development channel +- WebServices REST API plugin with CRUD routes for posts and services +- Database: 4 tables (services, posts, templates, logs) with default templates +- Package installer with auto-enable for core + task + service plugins +- 9 wiki documentation pages +- Windows Terminal profile in Joomla dropdown + + +## [01.01.00] - 2026-06-19 ### Added - Initial package structure with component, system plugin, content plugin, and webservices plugin @@ -17,9 +207,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - System plugin triggering cross-post on article publish via `onContentAfterSave` - Content plugin adding cross-post controls to article editor - WebServices API plugin with REST endpoints for posts and services -- Custom `mokojoomcross` plugin group for extensible service architecture +- Custom `mokosuitecross` plugin group for extensible service architecture - Service plugins: Facebook, X/Twitter, LinkedIn, Mastodon, Bluesky, Mailchimp, Telegram, Discord, Slack - Database tables: services, posts, templates, logs - Perfect Publisher Pro migration tool in installer script - Message template system with per-platform placeholders - Post queue with scheduled posting, retry logic, and delivery tracking + +## [01.00] - 2026-05-28 + +### Added +- Initial release diff --git a/CLAUDE.md b/CLAUDE.md index a8407b3..60dc44b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code when working with this repository. ## Project Overview -**MokoJoomCross** -- Cross-posting Joomla content to social media, email marketing, and chat platforms +**MokoSuiteCross** -- Cross-posting Joomla content to social media, email marketing, and chat platforms | Field | Value | |---|---| @@ -12,7 +12,7 @@ This file provides guidance to Claude Code when working with this repository. | **Language** | PHP | | **Default branch** | main | | **License** | GPL-3.0-or-later | -| **Wiki** | [MokoJoomCross Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomCross/wiki) | +| **Wiki** | [MokoSuiteCross Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/wiki) | | **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) | ## Common Commands @@ -32,60 +32,60 @@ composer install # Install PHP dependencies ## Architecture -This is a Joomla **package** extension (`pkg_mokojoomcross`) containing sub-extensions: +This is a Joomla **package** extension (`pkg_mokosuitecross`) containing sub-extensions: -### com_mokojoomcross (Component) +### com_mokosuitecross (Component) - Admin backend for managing services, post queue, templates, and logs - Joomla 5/6 MVC: Dashboard, Services, Posts, Logs (list/edit each) -- Namespace: `Joomla\Component\MokoJoomCross\Administrator` -- Database tables: `#__mokojoomcross_services`, `#__mokojoomcross_posts`, `#__mokojoomcross_templates`, `#__mokojoomcross_logs` +- Namespace: `Joomla\Component\MokoSuiteCross\Administrator` +- Database tables: `#__mokosuitecross_services`, `#__mokosuitecross_posts`, `#__mokosuitecross_templates`, `#__mokosuitecross_logs` -### plg_system_mokojoomcross (System Plugin) +### plg_system_mokosuitecross (System Plugin) - Hooks `onContentAfterSave` to trigger cross-posting when articles are published -- Dispatches to registered service plugins via the `mokojoomcross` plugin group -- Namespace: `Joomla\Plugin\System\MokoJoomCross` +- Dispatches to registered service plugins via the `mokosuitecross` plugin group +- Namespace: `Joomla\Plugin\System\MokoSuiteCross` -### plg_content_mokojoomcross (Content Plugin) +### plg_content_mokosuitecross (Content Plugin) - Hooks `onContentBeforeDisplay` to add cross-post status badges to articles -- Namespace: `Joomla\Plugin\Content\MokoJoomCross` +- Namespace: `Joomla\Plugin\Content\MokoSuiteCross` -### plg_webservices_mokojoomcross (WebServices Plugin) +### plg_webservices_mokosuitecross (WebServices Plugin) - REST API endpoints for posts and services -- Namespace: `Joomla\Plugin\WebServices\MokoJoomCross` +- Namespace: `Joomla\Plugin\WebServices\MokoSuiteCross` -### Service Plugins (mokojoomcross group) -Each platform is a separate plugin in the custom `mokojoomcross` plugin group: -- `plg_mokojoomcross_facebook` — Facebook/Meta Graph API -- `plg_mokojoomcross_twitter` — X/Twitter API v2 -- `plg_mokojoomcross_linkedin` — LinkedIn Share API -- `plg_mokojoomcross_mastodon` — Mastodon API -- `plg_mokojoomcross_bluesky` — Bluesky AT Protocol -- `plg_mokojoomcross_mailchimp` — Mailchimp Campaigns API -- `plg_mokojoomcross_telegram` — Telegram Bot API (default @MokoWaaSBot + custom bot) -- `plg_mokojoomcross_discord` — Discord Webhooks -- `plg_mokojoomcross_slack` — Slack Incoming Webhooks +### Service Plugins (mokosuitecross group) +Each platform is a separate plugin in the custom `mokosuitecross` plugin group: +- `plg_mokosuitecross_facebook` — Facebook/Meta Graph API +- `plg_mokosuitecross_twitter` — X/Twitter API v2 +- `plg_mokosuitecross_linkedin` — LinkedIn Share API +- `plg_mokosuitecross_mastodon` — Mastodon API +- `plg_mokosuitecross_bluesky` — Bluesky AT Protocol +- `plg_mokosuitecross_mailchimp` — Mailchimp Campaigns API +- `plg_mokosuitecross_telegram` — Telegram Bot API (default @MokoWaaSBot + custom bot) +- `plg_mokosuitecross_discord` — Discord Webhooks +- `plg_mokosuitecross_slack` — Slack Incoming Webhooks ### Database Schema Four tables: -`#__mokojoomcross_services`: +`#__mokosuitecross_services`: - `id`, `title`, `alias`, `service_type` (facebook, twitter, etc.) - `credentials` (JSON encrypted), `params` (JSON) - `published`, `ordering`, `created`, `modified`, `created_by` -`#__mokojoomcross_posts`: +`#__mokosuitecross_posts`: - `id`, `article_id` (FK to #__content), `service_id` (FK) - `status` (queued/posting/posted/failed/scheduled) - `message`, `platform_post_id`, `platform_response` (JSON) - `scheduled_at`, `posted_at`, `retry_count` - `created`, `modified` -`#__mokojoomcross_templates`: +`#__mokosuitecross_templates`: - `id`, `service_type`, `title`, `template_body` - `published`, `ordering`, `created`, `modified` -`#__mokojoomcross_logs`: +`#__mokosuitecross_logs`: - `id`, `post_id` (FK), `service_id` (FK) - `level` (info/warning/error), `message`, `context` (JSON) - `created` @@ -109,4 +109,4 @@ Four tables: - `bind() → check() → store()` for Table operations (not `save()`) - Language file placement: site (no `folder`) vs admin (`folder="administrator"`) - SPDX license headers on all PHP files -- Service plugins MUST implement `MokoJoomCrossServiceInterface` +- Service plugins MUST implement `MokoSuiteCrossServiceInterface` diff --git a/Makefile b/Makefile index c204119..46b9310 100644 --- a/Makefile +++ b/Makefile @@ -2,14 +2,14 @@ # Copyright (C) 2026 Moko Consulting # SPDX-License-Identifier: GPL-3.0-or-later # -# MokoJoomCross — Cross-posting Joomla content to social media, email marketing, and chat platforms +# MokoSuiteCross — Cross-posting Joomla content to social media, email marketing, and chat platforms # ============================================================================== # CONFIGURATION - Customize these for your extension # ============================================================================== # Extension Configuration -EXTENSION_NAME := mokojoomcross +EXTENSION_NAME := mokosuitecross EXTENSION_TYPE := package # Options: module, plugin, component, package, template EXTENSION_VERSION := 1.0.0 diff --git a/README.md b/README.md index 961fc94..d95d1b0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# MokoJoomCross +# MokoSuiteCross @@ -6,7 +6,7 @@ Cross-posting Joomla content to social media, email marketing, and chat platform ## Overview -MokoJoomCross automatically publishes your Joomla articles to multiple platforms when you hit publish. Connect your social media accounts, email marketing tools, and chat channels — then cross-post with one click. Each platform is a separate plugin, so you only install what you need and third-party developers can add new services. +MokoSuiteCross automatically publishes your Joomla articles to multiple platforms when you hit publish. Connect your social media accounts, email marketing tools, and chat channels — then cross-post with one click. Each platform is a separate plugin, so you only install what you need and third-party developers can add new services. ## Features @@ -22,29 +22,29 @@ MokoJoomCross automatically publishes your Joomla articles to multiple platforms | Platform | Plugin | Status | |----------|--------|--------| -| Facebook / Meta | `plg_mokojoomcross_facebook` | Planned | -| X / Twitter | `plg_mokojoomcross_twitter` | Planned | -| LinkedIn | `plg_mokojoomcross_linkedin` | Planned | -| Mastodon | `plg_mokojoomcross_mastodon` | Planned | -| Bluesky | `plg_mokojoomcross_bluesky` | Planned | -| Mailchimp | `plg_mokojoomcross_mailchimp` | Planned | -| Telegram | `plg_mokojoomcross_telegram` | Planned | -| Discord | `plg_mokojoomcross_discord` | Planned | -| Slack | `plg_mokojoomcross_slack` | Planned | +| Facebook / Meta | `plg_mokosuitecross_facebook` | Planned | +| X / Twitter | `plg_mokosuitecross_twitter` | Planned | +| LinkedIn | `plg_mokosuitecross_linkedin` | Planned | +| Mastodon | `plg_mokosuitecross_mastodon` | Planned | +| Bluesky | `plg_mokosuitecross_bluesky` | Planned | +| Mailchimp | `plg_mokosuitecross_mailchimp` | Planned | +| Telegram | `plg_mokosuitecross_telegram` | Planned | +| Discord | `plg_mokosuitecross_discord` | Planned | +| Slack | `plg_mokosuitecross_slack` | Planned | ## Installation -1. Download the latest `pkg_mokojoomcross-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomCross/releases) +1. Download the latest `pkg_mokosuitecross-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/releases) 2. In Joomla Administrator → Extensions → Install → Upload Package File 3. System and content plugins are enabled automatically on install -4. Navigate to Components → MokoJoomCross to connect your first service +4. Navigate to Components → MokoSuiteCross to connect your first service ## Migrating from Perfect Publisher Pro -MokoJoomCross includes a built-in migration tool: +MokoSuiteCross includes a built-in migration tool: -1. Install MokoJoomCross (Perfect Publisher Pro can remain installed) -2. Navigate to Components → MokoJoomCross → Dashboard +1. Install MokoSuiteCross (Perfect Publisher Pro can remain installed) +2. Navigate to Components → MokoSuiteCross → Dashboard 3. Click "Migrate from Perfect Publisher Pro" 4. Review detected services and confirm import diff --git a/composer.json b/composer.json index 565b419..99388c7 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "mokoconsulting/mokojoomcross", + "name": "mokoconsulting/mokosuitecross", "description": "Cross-posting Joomla content to social media, email marketing, and chat platforms", "type": "joomla-package", "version": "01.00.00", diff --git a/source/language/en-GB/pkg_mokosuitecross.sys.ini b/source/language/en-GB/pkg_mokosuitecross.sys.ini new file mode 100644 index 0000000..21331a3 --- /dev/null +++ b/source/language/en-GB/pkg_mokosuitecross.sys.ini @@ -0,0 +1,8 @@ +; MokoSuiteCross - Package System Language File +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PKG_MOKOSUITECROSS="MokoSuiteCross" +PKG_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms. Automatically publish articles to Facebook, X/Twitter, LinkedIn, Mastodon, Bluesky, Mailchimp, Telegram, Discord, and Slack." +PKG_MOKOSUITECROSS_PHP_VERSION_ERROR="MokoSuiteCross requires PHP %s or later." +PKG_MOKOSUITECROSS_MIGRATION_DETECTED="Perfect Publisher Pro detected! Navigate to Components → MokoSuiteCross → Dashboard to migrate your settings." diff --git a/src/packages/com_mokojoomcross/access.xml b/source/packages/com_mokosuitecross/access.xml similarity index 69% rename from src/packages/com_mokojoomcross/access.xml rename to source/packages/com_mokosuitecross/access.xml index a5d9dec..67eec1c 100644 --- a/src/packages/com_mokojoomcross/access.xml +++ b/source/packages/com_mokosuitecross/access.xml @@ -1,5 +1,5 @@ - +
@@ -8,7 +8,7 @@ - - + +
diff --git a/source/packages/com_mokosuitecross/config.xml b/source/packages/com_mokosuitecross/config.xml new file mode 100644 index 0000000..7554c62 --- /dev/null +++ b/source/packages/com_mokosuitecross/config.xml @@ -0,0 +1,146 @@ + + +
+ + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ +
+
diff --git a/source/packages/com_mokosuitecross/forms/filter_logs.xml b/source/packages/com_mokosuitecross/forms/filter_logs.xml new file mode 100644 index 0000000..2b5a76c --- /dev/null +++ b/source/packages/com_mokosuitecross/forms/filter_logs.xml @@ -0,0 +1,37 @@ + +
+ + + + + + + + + + + + + + + + + + + + +
diff --git a/source/packages/com_mokosuitecross/forms/filter_posts.xml b/source/packages/com_mokosuitecross/forms/filter_posts.xml new file mode 100644 index 0000000..4e6f8b9 --- /dev/null +++ b/source/packages/com_mokosuitecross/forms/filter_posts.xml @@ -0,0 +1,52 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/packages/com_mokojoomcross/forms/filter_services.xml b/source/packages/com_mokosuitecross/forms/filter_services.xml similarity index 90% rename from src/packages/com_mokojoomcross/forms/filter_services.xml rename to source/packages/com_mokosuitecross/forms/filter_services.xml index 4a19374..4090d84 100644 --- a/src/packages/com_mokojoomcross/forms/filter_services.xml +++ b/source/packages/com_mokosuitecross/forms/filter_services.xml @@ -4,7 +4,7 @@ @@ -19,9 +19,9 @@ - + diff --git a/source/packages/com_mokosuitecross/forms/filter_templates.xml b/source/packages/com_mokosuitecross/forms/filter_templates.xml new file mode 100644 index 0000000..f542266 --- /dev/null +++ b/source/packages/com_mokosuitecross/forms/filter_templates.xml @@ -0,0 +1,51 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/packages/com_mokojoomcross/forms/index.html b/source/packages/com_mokosuitecross/forms/index.html similarity index 100% rename from src/packages/com_mokojoomcross/forms/index.html rename to source/packages/com_mokosuitecross/forms/index.html diff --git a/source/packages/com_mokosuitecross/forms/post.xml b/source/packages/com_mokosuitecross/forms/post.xml new file mode 100644 index 0000000..da0104f --- /dev/null +++ b/source/packages/com_mokosuitecross/forms/post.xml @@ -0,0 +1,125 @@ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + +
+
diff --git a/source/packages/com_mokosuitecross/forms/service.xml b/source/packages/com_mokosuitecross/forms/service.xml new file mode 100644 index 0000000..a5befbb --- /dev/null +++ b/source/packages/com_mokosuitecross/forms/service.xml @@ -0,0 +1,910 @@ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/src/packages/com_mokojoomcross/forms/service.xml b/source/packages/com_mokosuitecross/forms/template.xml similarity index 67% rename from src/packages/com_mokojoomcross/forms/service.xml rename to source/packages/com_mokosuitecross/forms/template.xml index 55c46f3..5b69495 100644 --- a/src/packages/com_mokojoomcross/forms/service.xml +++ b/source/packages/com_mokosuitecross/forms/template.xml @@ -14,21 +14,14 @@ size="40" /> - - - - + label="COM_MOKOSUITECROSS_FIELD_SERVICE_TYPE" + description="COM_MOKOSUITECROSS_TEMPLATE_SERVICE_TYPE_DESC" + default="default"> + + @@ -39,6 +32,17 @@ + + - -
- -
diff --git a/src/packages/com_mokojoomcross/index.html b/source/packages/com_mokosuitecross/index.html similarity index 100% rename from src/packages/com_mokojoomcross/index.html rename to source/packages/com_mokosuitecross/index.html diff --git a/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.ini b/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.ini new file mode 100644 index 0000000..aa38aaa --- /dev/null +++ b/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.ini @@ -0,0 +1,513 @@ +; MokoSuiteCross — Admin Backend Language File +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +COM_MOKOSUITECROSS="MokoSuiteCross" +COM_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms" + +; Submenu +COM_MOKOSUITECROSS_SUBMENU_DASHBOARD="Dashboard" +COM_MOKOSUITECROSS_SUBMENU_POSTS="Post Queue" +COM_MOKOSUITECROSS_SUBMENU_SERVICES="Services" +COM_MOKOSUITECROSS_SUBMENU_LOGS="Activity Logs" + +; Dashboard +COM_MOKOSUITECROSS_DASHBOARD_ACTIVE_SERVICES="Active Services" +COM_MOKOSUITECROSS_DASHBOARD_QUEUED="Queued" +COM_MOKOSUITECROSS_DASHBOARD_POSTED="Posted" +COM_MOKOSUITECROSS_DASHBOARD_FAILED="Failed" +COM_MOKOSUITECROSS_DASHBOARD_QUICK_LINKS="Quick Links" + +; Migration +COM_MOKOSUITECROSS_MIGRATION_TITLE="Migrate from Perfect Publisher Pro" +COM_MOKOSUITECROSS_MIGRATION_DESCRIPTION="We detected Perfect Publisher Pro settings. Import your service configurations to MokoSuiteCross." +COM_MOKOSUITECROSS_MIGRATION_BUTTON="Start Migration" +COM_MOKOSUITECROSS_MIGRATION_SUCCESS="Migration complete: %d service(s) imported, %d skipped." +COM_MOKOSUITECROSS_MIGRATION_ERROR="Migration encountered errors: %s" + +; Services +COM_MOKOSUITECROSS_FIELD_SERVICE_TYPE="Service Type" +COM_MOKOSUITECROSS_SELECT_SERVICE_TYPE="- Select Service Type -" +COM_MOKOSUITECROSS_FIELDSET_CREDENTIALS="API Credentials" +COM_MOKOSUITECROSS_FIELD_CREDENTIALS="Credentials (JSON)" +COM_MOKOSUITECROSS_FIELD_CREDENTIALS_DESC="JSON object with API keys and tokens for this service. Keys vary by platform." + +; Posts +COM_MOKOSUITECROSS_FILTER_SEARCH="Search" +COM_MOKOSUITECROSS_FILTER_STATUS="Status" +COM_MOKOSUITECROSS_SELECT_STATUS="- Select Status -" +COM_MOKOSUITECROSS_FILTER_SERVICE_TYPE="Service Type" +COM_MOKOSUITECROSS_CREATED_ASC="Created ascending" +COM_MOKOSUITECROSS_CREATED_DESC="Created descending" +COM_MOKOSUITECROSS_STATUS_ASC="Status ascending" +COM_MOKOSUITECROSS_STATUS_DESC="Status descending" + +; Actions +COM_MOKOSUITECROSS_ACTION_CROSSPOST="Cross-post" +COM_MOKOSUITECROSS_ACTION_MIGRATE="Migrate" + +; Configuration +COM_MOKOSUITECROSS_CONFIG_COMPONENT="MokoSuiteCross Settings" +COM_MOKOSUITECROSS_CONFIG_AUTO_POST="Auto-post on Publish" +COM_MOKOSUITECROSS_CONFIG_AUTO_POST_DESC="Automatically cross-post articles when they are published" +COM_MOKOSUITECROSS_CONFIG_RETRY_MAX="Max Retries" +COM_MOKOSUITECROSS_CONFIG_RETRY_MAX_DESC="Maximum number of retry attempts for failed posts" +COM_MOKOSUITECROSS_CONFIG_RETRY_DELAY="Retry Delay (seconds)" +COM_MOKOSUITECROSS_CONFIG_RETRY_DELAY_DESC="Seconds to wait before retrying a failed post" +COM_MOKOSUITECROSS_CONFIG_LOG_RETENTION="Log Retention (days)" +COM_MOKOSUITECROSS_CONFIG_LOG_RETENTION_DESC="Number of days to keep activity logs" +COM_MOKOSUITECROSS_CONFIG_DEFAULT_TEMPLATE="Default Message Template" +COM_MOKOSUITECROSS_CONFIG_DEFAULT_TEMPLATE_DESC="Default template for cross-posts. Placeholders: {title}, {url}, {introtext}, {image}, {category}, {author}" + +; Table headings +COM_MOKOSUITECROSS_HEADING_STATUS="Status" +COM_MOKOSUITECROSS_HEADING_ARTICLE="Article" +COM_MOKOSUITECROSS_HEADING_SERVICE="Service" +COM_MOKOSUITECROSS_HEADING_MESSAGE="Message" +COM_MOKOSUITECROSS_HEADING_POSTED_AT="Posted" +COM_MOKOSUITECROSS_HEADING_CREATED="Created" +COM_MOKOSUITECROSS_HEADING_LEVEL="Level" +COM_MOKOSUITECROSS_HEADING_MODE="Mode" + +; Dashboard +COM_MOKOSUITECROSS_DASHBOARD_RECENT_ACTIVITY="Recent Activity" +COM_MOKOSUITECROSS_DASHBOARD_NO_RECENT="No recent activity." +COM_MOKOSUITECROSS_DASHBOARD_TOTAL_POSTS="Total Posts" +COM_MOKOSUITECROSS_DASHBOARD_PAGELOAD_WARNING_TITLE="Page-load queue processing is active" +COM_MOKOSUITECROSS_DASHBOARD_PAGELOAD_WARNING="You are using page-load processing for the cross-post queue. This is a fallback method and may be unreliable on low-traffic sites. For production use, switch to Joomla Scheduled Tasks: create a task of type MokoSuiteCross - Process Queue in System → Scheduled Tasks, then set queue processing to Scheduler only in component options." + +; Evergreen Configuration +COM_MOKOSUITECROSS_CONFIG_EVERGREEN="Evergreen Re-sharing" +COM_MOKOSUITECROSS_CONFIG_EVERGREEN_ENABLED="Enable Evergreen" +COM_MOKOSUITECROSS_CONFIG_EVERGREEN_ENABLED_DESC="Allow articles marked as evergreen to be automatically re-shared on a recurring schedule." +COM_MOKOSUITECROSS_CONFIG_EVERGREEN_DEFAULT_INTERVAL="Default Interval (days)" +COM_MOKOSUITECROSS_CONFIG_EVERGREEN_DEFAULT_INTERVAL_DESC="Default number of days between re-shares when no per-article interval is set." +COM_MOKOSUITECROSS_CONFIG_EVERGREEN_MAX_PER_RUN="Max Re-shares Per Run" +COM_MOKOSUITECROSS_CONFIG_EVERGREEN_MAX_PER_RUN_DESC="Maximum number of evergreen articles to re-share in a single queue processing run. Prevents flooding platforms." + +; Queue Processing Configuration +COM_MOKOSUITECROSS_CONFIG_QUEUE="Queue Processing" +COM_MOKOSUITECROSS_CONFIG_QUEUE_PROCESSING="Processing Method" +COM_MOKOSUITECROSS_CONFIG_QUEUE_PROCESSING_DESC="How queued posts, retries, and scheduled posts are processed. Scheduler (recommended) uses Joomla's built-in Task Scheduler. Page-load piggybacks on page requests." +COM_MOKOSUITECROSS_CONFIG_QUEUE_SCHEDULER="Scheduler only (recommended)" +COM_MOKOSUITECROSS_CONFIG_QUEUE_PAGELOAD="Page-load only (fallback)" +COM_MOKOSUITECROSS_CONFIG_QUEUE_BOTH="Both (scheduler + page-load)" +COM_MOKOSUITECROSS_CONFIG_PAGELOAD_CLIENT="Page-load Client" +COM_MOKOSUITECROSS_CONFIG_PAGELOAD_CLIENT_DESC="Which Joomla application triggers page-load processing." +COM_MOKOSUITECROSS_CONFIG_PAGELOAD_BOTH="Backend and Frontend" +COM_MOKOSUITECROSS_CONFIG_PAGELOAD_ADMIN="Backend only" +COM_MOKOSUITECROSS_CONFIG_PAGELOAD_SITE="Frontend only" +COM_MOKOSUITECROSS_CONFIG_PAGELOAD_INTERVAL="Page-load Interval (seconds)" +COM_MOKOSUITECROSS_CONFIG_PAGELOAD_INTERVAL_DESC="Minimum seconds between page-load queue runs. Lower = more responsive but more DB queries per page load." + +; Submenu (extended) +COM_MOKOSUITECROSS_SUBMENU_TEMPLATES="Templates" + +; Template Management +COM_MOKOSUITECROSS_TEMPLATE_BODY="Template Body" +COM_MOKOSUITECROSS_TEMPLATE_BODY_DESC="Message template with placeholders. Use the reference panel on the right for available placeholders." +COM_MOKOSUITECROSS_TEMPLATE_SERVICE_TYPE_DESC="Which platform this template is for. 'Default' is the fallback when no platform-specific template exists." +COM_MOKOSUITECROSS_TEMPLATE_TYPE_DEFAULT="Default (fallback)" +COM_MOKOSUITECROSS_TEMPLATE_PREVIEW="Preview" +COM_MOKOSUITECROSS_TEMPLATE_PLACEHOLDERS="Available Placeholders" + +; Placeholders +COM_MOKOSUITECROSS_PLACEHOLDER_TITLE="Article title" +COM_MOKOSUITECROSS_PLACEHOLDER_URL="Article URL" +COM_MOKOSUITECROSS_PLACEHOLDER_INTROTEXT="Intro text (280 chars, no HTML)" +COM_MOKOSUITECROSS_PLACEHOLDER_FULLTEXT="Full text (500 chars, no HTML)" +COM_MOKOSUITECROSS_PLACEHOLDER_IMAGE="Intro image URL" +COM_MOKOSUITECROSS_PLACEHOLDER_CATEGORY="Category name" +COM_MOKOSUITECROSS_PLACEHOLDER_AUTHOR="Author name" +COM_MOKOSUITECROSS_PLACEHOLDER_DATE="Publish date (YYYY-MM-DD)" + +; Logs +COM_MOKOSUITECROSS_FILTER_LEVEL="Level" +COM_MOKOSUITECROSS_SELECT_LEVEL="- Select Level -" +COM_MOKOSUITECROSS_LEVEL_ASC="Level ascending" +COM_MOKOSUITECROSS_LEVEL_DESC="Level descending" + +; Analytics Dashboard +COM_MOKOSUITECROSS_DASHBOARD_SERVICE_BREAKDOWN="Posts by Service" +COM_MOKOSUITECROSS_DASHBOARD_TOP_ARTICLES="Most Cross-Posted Articles" +COM_MOKOSUITECROSS_DASHBOARD_SUCCESS_RATE="Success Rate" + +; OAuth +COM_MOKOSUITECROSS_OAUTH_NO_SERVICE="No service specified for OAuth authorization." +COM_MOKOSUITECROSS_OAUTH_SERVICE_NOT_FOUND="Service not found." +COM_MOKOSUITECROSS_OAUTH_NO_CLIENT_ID="No OAuth Client ID configured for %s. Set it in Extensions → Plugins → MokoSuiteCross - %s." +COM_MOKOSUITECROSS_OAUTH_NOT_SUPPORTED="OAuth is not supported for %s." +COM_MOKOSUITECROSS_OAUTH_PLATFORM_ERROR="Platform returned error: %s" +COM_MOKOSUITECROSS_OAUTH_INVALID_CALLBACK="Invalid OAuth callback — missing code or state." +COM_MOKOSUITECROSS_OAUTH_INVALID_STATE="Invalid OAuth state parameter." +COM_MOKOSUITECROSS_OAUTH_TOKEN_ERROR="Token exchange failed: %s" +COM_MOKOSUITECROSS_OAUTH_SUCCESS="%s connected successfully! Access token stored." + +; Post edit +COM_MOKOSUITECROSS_NEW_POST="New Post" +COM_MOKOSUITECROSS_EDIT_POST="Edit Post" +COM_MOKOSUITECROSS_POST_ARTICLE="Article" +COM_MOKOSUITECROSS_POST_ARTICLE_DESC="The Joomla article to cross-post." +COM_MOKOSUITECROSS_SELECT_ARTICLE="- Select Article -" +COM_MOKOSUITECROSS_POST_SERVICE="Service" +COM_MOKOSUITECROSS_POST_SERVICE_DESC="The service to post to." +COM_MOKOSUITECROSS_SELECT_SERVICE="- Select Service -" +COM_MOKOSUITECROSS_POST_MESSAGE="Message" +COM_MOKOSUITECROSS_POST_MESSAGE_DESC="The message to send to the platform. Use template placeholders or write a custom message." +COM_MOKOSUITECROSS_POST_STATUS="Status" +COM_MOKOSUITECROSS_STATUS_QUEUED="Queued" +COM_MOKOSUITECROSS_STATUS_SCHEDULED="Scheduled" +COM_MOKOSUITECROSS_STATUS_POSTED="Posted" +COM_MOKOSUITECROSS_STATUS_FAILED="Failed" +COM_MOKOSUITECROSS_POST_SCHEDULED_AT="Scheduled Date/Time" +COM_MOKOSUITECROSS_POST_SCHEDULED_AT_DESC="When to send this post. Leave empty to process immediately. Set a future date to schedule." +COM_MOKOSUITECROSS_POST_RESULTS="Post Results" +COM_MOKOSUITECROSS_POST_PLATFORM_ID="Platform Post ID" +COM_MOKOSUITECROSS_POST_ERROR="Error Message" +COM_MOKOSUITECROSS_POST_RETRY_COUNT="Retry Count" +COM_MOKOSUITECROSS_POST_POSTED_AT="Posted At" +COM_MOKOSUITECROSS_POST_CREATE_HELP="Create a manual cross-post. Select an article and service, write your message, and optionally set a scheduled date. Leave the schedule empty to queue for immediate processing." +COM_MOKOSUITECROSS_POST_REQUEUE="Re-queue for Posting" +COM_MOKOSUITECROSS_POST_REQUEUE_HELP="Reset this post to queued status so it will be processed again on the next queue run." + +; Service edit +COM_MOKOSUITECROSS_NEW_SERVICE="New Service" +COM_MOKOSUITECROSS_EDIT_SERVICE="Edit Service" +COM_MOKOSUITECROSS_SERVICE_DETAILS="Service Details" +COM_MOKOSUITECROSS_CREDENTIALS_HELP="Fill in the connection details for the selected platform. Fields change based on the service type you choose above." + +; Credential mode +COM_MOKOSUITECROSS_FIELD_CRED_MODE="Connection Mode" +COM_MOKOSUITECROSS_FIELD_CRED_MODE_DESC="Default uses the pre-configured MokoWaaS account. Custom lets you use your own API credentials." +COM_MOKOSUITECROSS_CRED_MODE_DEFAULT="Default (MokoWaaS)" +COM_MOKOSUITECROSS_CRED_MODE_CUSTOM="Custom (your own credentials)" + +; Telegram +COM_MOKOSUITECROSS_CRED_TELEGRAM_CHAT_ID="Chat ID" +COM_MOKOSUITECROSS_CRED_TELEGRAM_CHAT_ID_DESC="Telegram channel, group, or user chat ID. Channel IDs start with -100. Get yours from @userinfobot." +COM_MOKOSUITECROSS_CRED_TELEGRAM_BOT_TOKEN="Bot Token" +COM_MOKOSUITECROSS_CRED_TELEGRAM_BOT_TOKEN_DESC="Your custom Telegram bot token from @BotFather. Only needed in Custom mode." + +; Discord +COM_MOKOSUITECROSS_CRED_DISCORD_WEBHOOK="Webhook URL" +COM_MOKOSUITECROSS_CRED_DISCORD_WEBHOOK_DESC="Discord channel webhook URL. Create one in Channel Settings → Integrations → Webhooks." + +; Slack +COM_MOKOSUITECROSS_CRED_SLACK_WEBHOOK="Webhook URL" +COM_MOKOSUITECROSS_CRED_SLACK_WEBHOOK_DESC="Slack Incoming Webhook URL. Create one at api.slack.com/apps." + +; Teams +COM_MOKOSUITECROSS_CRED_TEAMS_WEBHOOK="Webhook URL" +COM_MOKOSUITECROSS_CRED_TEAMS_WEBHOOK_DESC="Microsoft Teams Incoming Webhook URL. Create in channel Connectors." + +; Google Chat +COM_MOKOSUITECROSS_CRED_GOOGLECHAT_WEBHOOK="Webhook URL" +COM_MOKOSUITECROSS_CRED_GOOGLECHAT_WEBHOOK_DESC="Google Chat space webhook URL." + +; Facebook +COM_MOKOSUITECROSS_CRED_FACEBOOK_PAGE_ID="Facebook Page ID" +COM_MOKOSUITECROSS_CRED_FACEBOOK_PAGE_ID_DESC="Your Facebook Page numeric ID. Find it in Page Settings → About." +COM_MOKOSUITECROSS_CRED_FACEBOOK_TOKEN="Page Access Token" +COM_MOKOSUITECROSS_CRED_FACEBOOK_TOKEN_DESC="Long-lived Page Access Token. Use the Authorize button below or generate via Meta Business Suite." + +; Threads +COM_MOKOSUITECROSS_CRED_THREADS_USER_ID="Threads User ID" +COM_MOKOSUITECROSS_CRED_THREADS_TOKEN="Access Token" + +; Twitter (OAuth 1.0a) +COM_MOKOSUITECROSS_CRED_TWITTER_API_KEY="API Key (Consumer Key)" +COM_MOKOSUITECROSS_CRED_TWITTER_API_KEY_DESC="Consumer Key from the Twitter Developer Portal → Keys and Tokens." +COM_MOKOSUITECROSS_CRED_TWITTER_API_SECRET="API Secret (Consumer Secret)" +COM_MOKOSUITECROSS_CRED_TWITTER_API_SECRET_DESC="Consumer Secret from the Twitter Developer Portal → Keys and Tokens." +COM_MOKOSUITECROSS_CRED_TWITTER_ACCESS_TOKEN="Access Token" +COM_MOKOSUITECROSS_CRED_TWITTER_ACCESS_TOKEN_DESC="User access token from the Developer Portal → Keys and Tokens → Authentication Tokens." +COM_MOKOSUITECROSS_CRED_TWITTER_ACCESS_TOKEN_SECRET="Access Token Secret" +COM_MOKOSUITECROSS_CRED_TWITTER_ACCESS_TOKEN_SECRET_DESC="User access token secret from the Developer Portal → Keys and Tokens → Authentication Tokens." + +; LinkedIn +COM_MOKOSUITECROSS_CRED_LINKEDIN_TOKEN="Access Token" +COM_MOKOSUITECROSS_CRED_LINKEDIN_ORG_ID="Organization ID" +COM_MOKOSUITECROSS_CRED_LINKEDIN_ORG_ID_DESC="LinkedIn Company Page ID. Leave empty to post as yourself." + +; Mastodon +COM_MOKOSUITECROSS_CRED_MASTODON_INSTANCE="Instance URL" +COM_MOKOSUITECROSS_CRED_MASTODON_INSTANCE_DESC="Your Mastodon server (e.g. https://mastodon.social)" +COM_MOKOSUITECROSS_CRED_MASTODON_TOKEN="Access Token" + +; Bluesky +COM_MOKOSUITECROSS_CRED_BLUESKY_HANDLE="Handle" +COM_MOKOSUITECROSS_CRED_BLUESKY_HANDLE_DESC="Your Bluesky handle (e.g. user.bsky.social)" +COM_MOKOSUITECROSS_CRED_BLUESKY_APP_PWD="App Password" +COM_MOKOSUITECROSS_CRED_BLUESKY_APP_PWD_DESC="Generate in Bluesky Settings → Advanced → App Passwords." + +; WhatsApp +COM_MOKOSUITECROSS_CRED_WHATSAPP_TOKEN="Access Token" +COM_MOKOSUITECROSS_CRED_WHATSAPP_PHONE_ID="Phone Number ID" +COM_MOKOSUITECROSS_CRED_WHATSAPP_RECIPIENT="Recipient Number" +COM_MOKOSUITECROSS_CRED_WHATSAPP_RECIPIENT_DESC="Phone number to send to, with country code (e.g. +1234567890)" + +; Mailchimp +COM_MOKOSUITECROSS_CRED_MAILCHIMP_KEY="API Key" +COM_MOKOSUITECROSS_CRED_MAILCHIMP_KEY_DESC="Mailchimp API key (ends with -us1, -us2, etc.)" +COM_MOKOSUITECROSS_CRED_MAILCHIMP_LIST="Audience/List ID" +COM_MOKOSUITECROSS_CRED_MAILCHIMP_LIST_DESC="The audience to send campaigns to. Find in Audience → Settings → Audience ID." + +; SendGrid +COM_MOKOSUITECROSS_CRED_SENDGRID_KEY="API Key" +COM_MOKOSUITECROSS_CRED_SENDGRID_LIST="Contact List ID" + +; Webhook +COM_MOKOSUITECROSS_CRED_WEBHOOK_URL="Webhook URL" +COM_MOKOSUITECROSS_CRED_WEBHOOK_URL_DESC="The URL to send article data to. Works with Zapier, IFTTT, n8n, Make, or any custom endpoint." +COM_MOKOSUITECROSS_CRED_WEBHOOK_METHOD="HTTP Method" + +; Matrix +COM_MOKOSUITECROSS_CRED_MATRIX_HOMESERVER="Homeserver URL" +COM_MOKOSUITECROSS_CRED_MATRIX_TOKEN="Access Token" +COM_MOKOSUITECROSS_CRED_MATRIX_ROOM="Room ID" +COM_MOKOSUITECROSS_CRED_MATRIX_ROOM_DESC="Matrix room ID (e.g. !abc123:matrix.org)" + +; Ntfy +COM_MOKOSUITECROSS_CRED_NTFY_SERVER="Server URL" +COM_MOKOSUITECROSS_CRED_NTFY_TOPIC="Topic Name" +COM_MOKOSUITECROSS_CRED_NTFY_TOPIC_DESC="The notification topic (e.g. my-site-updates). Subscribers use this to receive push notifications." +COM_MOKOSUITECROSS_CRED_NTFY_TOKEN="Auth Token" +COM_MOKOSUITECROSS_CRED_NTFY_TOKEN_DESC="Optional authentication token if your ntfy server requires it." + +; WordPress +COM_MOKOSUITECROSS_CRED_WP_SITE="WordPress Site URL" +COM_MOKOSUITECROSS_CRED_WP_USER="Username" +COM_MOKOSUITECROSS_CRED_WP_APP_PWD="Application Password" +COM_MOKOSUITECROSS_CRED_WP_APP_PWD_DESC="Generate in WordPress → Users → Profile → Application Passwords." + +; Medium +COM_MOKOSUITECROSS_CRED_MEDIUM_TOKEN="Integration Token" + +; Dev.to +COM_MOKOSUITECROSS_CRED_DEVTO_KEY="API Key" + +; Ghost +COM_MOKOSUITECROSS_CRED_GHOST_SITE="Ghost Site URL" +COM_MOKOSUITECROSS_CRED_GHOST_KEY="Admin API Key" + +; Reddit +COM_MOKOSUITECROSS_CRED_REDDIT_CLIENT_ID="App Client ID" +COM_MOKOSUITECROSS_CRED_REDDIT_SECRET="App Secret" +COM_MOKOSUITECROSS_CRED_REDDIT_USER="Reddit Username" +COM_MOKOSUITECROSS_CRED_REDDIT_SUBREDDIT="Subreddit" +COM_MOKOSUITECROSS_CRED_REDDIT_SUBREDDIT_DESC="Subreddit to post to (without r/ prefix)" + +; Authorize / OAuth +COM_MOKOSUITECROSS_AUTHORIZE_BUTTON="Connect to %s" +COM_MOKOSUITECROSS_AUTHORIZE_HELP="Click to open the authorization page. You'll be redirected back after granting access. Your token will be saved automatically." +COM_MOKOSUITECROSS_OAUTH_HELP_TITLE="Authorization Required" +COM_MOKOSUITECROSS_OAUTH_HELP_BODY="This service requires OAuth authorization. Save the service first, then click the Connect button below to authorize access." + +; LinkedIn (additional) +COM_MOKOSUITECROSS_CRED_LINKEDIN_REFRESH_TOKEN="Refresh Token" +COM_MOKOSUITECROSS_CRED_LINKEDIN_REFRESH_TOKEN_DESC="OAuth refresh token for automatic access token renewal." + +; Bluesky (additional) +COM_MOKOSUITECROSS_CRED_BLUESKY_PDS_URL="PDS URL" +COM_MOKOSUITECROSS_CRED_BLUESKY_PDS_URL_DESC="Personal Data Server URL. Default is https://bsky.social. Only change for self-hosted PDS." + +; Discord (additional) +COM_MOKOSUITECROSS_CRED_DISCORD_USERNAME="Display Name Override" +COM_MOKOSUITECROSS_CRED_DISCORD_USERNAME_DESC="Override the webhook's default display name. Leave empty to use the webhook name." +COM_MOKOSUITECROSS_CRED_DISCORD_AVATAR="Avatar URL Override" +COM_MOKOSUITECROSS_CRED_DISCORD_AVATAR_DESC="Override the webhook's default avatar with a custom image URL." + +; Mailchimp (additional) +COM_MOKOSUITECROSS_CRED_MAILCHIMP_FROM_NAME="From Name" +COM_MOKOSUITECROSS_CRED_MAILCHIMP_FROM_NAME_DESC="Sender name for campaigns. Leave empty to use the audience default." +COM_MOKOSUITECROSS_CRED_MAILCHIMP_FROM_EMAIL="From Email" +COM_MOKOSUITECROSS_CRED_MAILCHIMP_FROM_EMAIL_DESC="Sender email for campaigns. Must be a verified sending domain." + +; SendGrid (additional) +COM_MOKOSUITECROSS_CRED_SENDGRID_FROM_EMAIL="From Email" +COM_MOKOSUITECROSS_CRED_SENDGRID_FROM_EMAIL_DESC="Verified sender email address for Single Sends." +COM_MOKOSUITECROSS_CRED_SENDGRID_FROM_NAME="From Name" +COM_MOKOSUITECROSS_CRED_SENDGRID_FROM_NAME_DESC="Display name for the sender." + +; Reddit (additional) +COM_MOKOSUITECROSS_CRED_REDDIT_PASSWORD="Account Password" +COM_MOKOSUITECROSS_CRED_REDDIT_PASSWORD_DESC="Required for Reddit script-type OAuth. The password for the Reddit account." + +; WordPress (additional) +COM_MOKOSUITECROSS_CRED_WP_DEFAULT_STATUS="Default Post Status" +COM_MOKOSUITECROSS_CRED_WP_DEFAULT_STATUS_DESC="Whether cross-posted articles appear as drafts or are published immediately." + +; Dev.to (additional) +COM_MOKOSUITECROSS_CRED_DEVTO_ORG_ID="Organization ID" +COM_MOKOSUITECROSS_CRED_DEVTO_ORG_ID_DESC="Optional. Publish under a Dev.to organization instead of your personal account." + +; Ghost (additional) +COM_MOKOSUITECROSS_CRED_GHOST_DEFAULT_STATUS="Default Post Status" +COM_MOKOSUITECROSS_CRED_GHOST_DEFAULT_STATUS_DESC="Whether cross-posted articles are saved as drafts or published immediately." + +; Status options (shared) +COM_MOKOSUITECROSS_STATUS_DRAFT="Draft" +COM_MOKOSUITECROSS_STATUS_PUBLISH="Publish" +COM_MOKOSUITECROSS_STATUS_PUBLISHED="Published" + +; Pinterest +COM_MOKOSUITECROSS_CRED_PINTEREST_TOKEN="Access Token" +COM_MOKOSUITECROSS_CRED_PINTEREST_TOKEN_DESC="Pinterest API v5 access token from the Developer Portal." +COM_MOKOSUITECROSS_CRED_PINTEREST_BOARD="Board ID" +COM_MOKOSUITECROSS_CRED_PINTEREST_BOARD_DESC="The board to pin to. Find the ID in the board URL or via the API." + +; Tumblr +COM_MOKOSUITECROSS_CRED_TUMBLR_TOKEN="Access Token" +COM_MOKOSUITECROSS_CRED_TUMBLR_TOKEN_DESC="Tumblr OAuth access token." +COM_MOKOSUITECROSS_CRED_TUMBLR_BLOG="Blog Name" +COM_MOKOSUITECROSS_CRED_TUMBLR_BLOG_DESC="Your Tumblr blog name (e.g. myblog — without .tumblr.com)." + +; TikTok +COM_MOKOSUITECROSS_CRED_TIKTOK_TOKEN="Access Token" +COM_MOKOSUITECROSS_CRED_TIKTOK_REFRESH_TOKEN="Refresh Token" +COM_MOKOSUITECROSS_CRED_TIKTOK_OPEN_ID="Open ID" +COM_MOKOSUITECROSS_CRED_TIKTOK_OPEN_ID_DESC="Your TikTok Open ID from the developer app authorization." + +; Nostr +COM_MOKOSUITECROSS_CRED_NOSTR_PRIVKEY="Private Key" +COM_MOKOSUITECROSS_CRED_NOSTR_PRIVKEY_DESC="Nostr private key in hex or nsec format. Used to sign events." +COM_MOKOSUITECROSS_CRED_NOSTR_RELAYS="Relay URLs" +COM_MOKOSUITECROSS_CRED_NOSTR_RELAYS_DESC="Comma-separated list of relay WebSocket URLs (e.g. wss://relay.damus.io, wss://nos.lol)." + +; ActivityPub +COM_MOKOSUITECROSS_CRED_ACTIVITYPUB_INSTANCE="Instance URL" +COM_MOKOSUITECROSS_CRED_ACTIVITYPUB_INSTANCE_DESC="Fediverse instance URL (Pleroma, Akkoma, Misskey, Pixelfed, etc.)." +COM_MOKOSUITECROSS_CRED_ACTIVITYPUB_TOKEN="Access Token" +COM_MOKOSUITECROSS_CRED_ACTIVITYPUB_TOKEN_DESC="API access token from the instance's developer settings." + +; Brevo (Sendinblue) +COM_MOKOSUITECROSS_CRED_BREVO_KEY="API Key" +COM_MOKOSUITECROSS_CRED_BREVO_LIST="Contact List ID" +COM_MOKOSUITECROSS_CRED_BREVO_LIST_DESC="Brevo contact list ID to send campaigns to." +COM_MOKOSUITECROSS_CRED_BREVO_SENDER_EMAIL="Sender Email" +COM_MOKOSUITECROSS_CRED_BREVO_SENDER_EMAIL_DESC="Must be a verified sender in your Brevo account." +COM_MOKOSUITECROSS_CRED_BREVO_SENDER_NAME="Sender Name" + +; ConvertKit +COM_MOKOSUITECROSS_CRED_CONVERTKIT_KEY="API Key" +COM_MOKOSUITECROSS_CRED_CONVERTKIT_SECRET="API Secret" + +; Constant Contact +COM_MOKOSUITECROSS_CRED_CONSTANTCONTACT_TOKEN="Access Token" +COM_MOKOSUITECROSS_CRED_CONSTANTCONTACT_REFRESH_TOKEN="Refresh Token" +COM_MOKOSUITECROSS_CRED_CONSTANTCONTACT_LISTS="Contact List IDs" +COM_MOKOSUITECROSS_CRED_CONSTANTCONTACT_LISTS_DESC="Comma-separated list IDs to include in the campaign." + +; Hashnode +COM_MOKOSUITECROSS_CRED_HASHNODE_TOKEN="Personal Access Token" +COM_MOKOSUITECROSS_CRED_HASHNODE_PUB_ID="Publication ID" +COM_MOKOSUITECROSS_CRED_HASHNODE_PUB_ID_DESC="Your Hashnode publication ID. Find in Dashboard → General settings." + +; Google Blogger +COM_MOKOSUITECROSS_CRED_BLOGGER_TOKEN="Access Token" +COM_MOKOSUITECROSS_CRED_BLOGGER_REFRESH_TOKEN="Refresh Token" +COM_MOKOSUITECROSS_CRED_BLOGGER_BLOG_ID="Blog ID" +COM_MOKOSUITECROSS_CRED_BLOGGER_BLOG_ID_DESC="Numeric Blog ID from Blogger settings or the Blogger API." + +; Google Business Profile +COM_MOKOSUITECROSS_CRED_GBUSINESS_TOKEN="Access Token" +COM_MOKOSUITECROSS_CRED_GBUSINESS_REFRESH_TOKEN="Refresh Token" +COM_MOKOSUITECROSS_CRED_GBUSINESS_LOCATION="Location ID" +COM_MOKOSUITECROSS_CRED_GBUSINESS_LOCATION_DESC="Google Business location ID (e.g. locations/1234567890)." +COM_MOKOSUITECROSS_CRED_GBUSINESS_ACCOUNT="Account ID" +COM_MOKOSUITECROSS_CRED_GBUSINESS_ACCOUNT_DESC="Google Business account ID (e.g. accounts/1234567890)." + +; RSS Feed +COM_MOKOSUITECROSS_CRED_RSSFEED_TITLE="Feed Title" +COM_MOKOSUITECROSS_CRED_RSSFEED_TITLE_DESC="Title for the generated RSS feed. Defaults to the site name." +COM_MOKOSUITECROSS_CRED_RSSFEED_MAX_ITEMS="Max Feed Items" +COM_MOKOSUITECROSS_CRED_RSSFEED_MAX_ITEMS_DESC="Maximum number of items to include in the feed." + +; Webhook (additional) +COM_MOKOSUITECROSS_CRED_WEBHOOK_AUTH_TYPE="Authentication" +COM_MOKOSUITECROSS_CRED_WEBHOOK_AUTH_TYPE_DESC="Authentication method for the webhook endpoint." +COM_MOKOSUITECROSS_WEBHOOK_AUTH_NONE="None" +COM_MOKOSUITECROSS_WEBHOOK_AUTH_BEARER="Bearer Token" +COM_MOKOSUITECROSS_WEBHOOK_AUTH_BASIC="Basic Auth" +COM_MOKOSUITECROSS_CRED_WEBHOOK_BEARER_TOKEN="Bearer Token" +COM_MOKOSUITECROSS_CRED_WEBHOOK_BEARER_TOKEN_DESC="Authentication token sent as Authorization: Bearer {token}." +COM_MOKOSUITECROSS_CRED_WEBHOOK_BASIC_USER="Username" +COM_MOKOSUITECROSS_CRED_WEBHOOK_BASIC_PWD="Password" +COM_MOKOSUITECROSS_CRED_WEBHOOK_CONTENT_TYPE="Content Type" + +; Service help link +COM_MOKOSUITECROSS_SERVICE_HELP_LINK="%s Setup Guide" + +; Setup help panel +COM_MOKOSUITECROSS_SETUP_HELP_TITLE="How to set up" +COM_MOKOSUITECROSS_SETUP_HELP_INTRO="Setting up a new service is easy:" +COM_MOKOSUITECROSS_SETUP_STEP1="Choose a service type from the dropdown" +COM_MOKOSUITECROSS_SETUP_STEP2="Fill in the connection details that appear" +COM_MOKOSUITECROSS_SETUP_STEP3="For OAuth services, save first, then click Connect" +COM_MOKOSUITECROSS_SETUP_STEP4="Set status to Published and save" + +; Test Connection +COM_MOKOSUITECROSS_TEST_CONNECTION_TITLE="Test Connection" +COM_MOKOSUITECROSS_TEST_CONNECTION_DESC="Verify that your credentials are valid and the service is reachable." +COM_MOKOSUITECROSS_TEST_CONNECTION_BUTTON="Test Connection" +COM_MOKOSUITECROSS_TEST_CONNECTION_TESTING="Testing..." +COM_MOKOSUITECROSS_TEST_CONNECTION_SUCCESS="Connection successful" +COM_MOKOSUITECROSS_TEST_CONNECTION_FAILED="Connection failed" +COM_MOKOSUITECROSS_TEST_CONNECTION_ERROR="Could not reach the server. Please try again." +COM_MOKOSUITECROSS_TEST_CONNECTION_NO_SERVICE="No service specified for test." +COM_MOKOSUITECROSS_TEST_CONNECTION_NOT_FOUND="Service record not found." +COM_MOKOSUITECROSS_TEST_CONNECTION_NO_PLUGIN="No service plugin available for type '%s'." + +; Bulk Queue Actions +COM_MOKOSUITECROSS_TOOLBAR_RETRY_FAILED="Retry Failed" +COM_MOKOSUITECROSS_TOOLBAR_PURGE_POSTED="Purge Posted" +COM_MOKOSUITECROSS_POSTS_N_RETRIED="%d failed post(s) re-queued for retry." +COM_MOKOSUITECROSS_POSTS_N_RETRIED_1="1 failed post re-queued for retry." +COM_MOKOSUITECROSS_POSTS_N_PURGED="%d posted record(s) purged." +COM_MOKOSUITECROSS_POSTS_N_PURGED_1="1 posted record purged." +COM_MOKOSUITECROSS_POSTS_N_SCHEDULED="%d post(s) scheduled." +COM_MOKOSUITECROSS_POSTS_NO_ITEM_SELECTED="No posts selected." +COM_MOKOSUITECROSS_SCHEDULE_NO_DATE="Please select a date and time for scheduling." +COM_MOKOSUITECROSS_TOOLBAR_SCHEDULE="Schedule" +COM_MOKOSUITECROSS_TOOLBAR_RETRY_SELECTED="Retry Selected" + +; Queue Depth Warning +COM_MOKOSUITECROSS_DASHBOARD_QUEUE_DEPTH_WARNING_TITLE="Large queue backlog" +COM_MOKOSUITECROSS_DASHBOARD_QUEUE_DEPTH_WARNING="There are %d posts waiting in the queue. Please verify that the Joomla Task Scheduler is running and the MokoSuiteCross scheduled task is enabled in System → Scheduled Tasks." + +; First-Publish-Only +COM_MOKOSUITECROSS_CONFIG_FIRST_PUBLISH_ONLY="First Publish Only" +COM_MOKOSUITECROSS_CONFIG_FIRST_PUBLISH_ONLY_DESC="When enabled, articles are only cross-posted on their first save as published. Subsequent edits to already-published articles will not trigger new cross-posts." + +; Trend Chart +COM_MOKOSUITECROSS_DASHBOARD_TREND_CHART="Daily Post Trend" + +; Date Range Period Filter +COM_MOKOSUITECROSS_PERIOD_7_DAYS="Last 7 days" +COM_MOKOSUITECROSS_PERIOD_30_DAYS="Last 30 days" +COM_MOKOSUITECROSS_PERIOD_90_DAYS="Last 90 days" +COM_MOKOSUITECROSS_PERIOD_ALL_TIME="All time" + +; Hashtag Placeholders +COM_MOKOSUITECROSS_PLACEHOLDER_TAGS="Article tags (comma-separated)" +COM_MOKOSUITECROSS_PLACEHOLDER_HASHTAGS="Article tags as hashtags (#Tag1 #Tag2)" +COM_MOKOSUITECROSS_PLACEHOLDER_CUSTOM_FIELD="Custom field value (replace xxx with field name)" + +; CSV Export +COM_MOKOSUITECROSS_EXPORT_CSV="Export CSV" + +; Service Stats (drill-down) +COM_MOKOSUITECROSS_SERVICESTATS_RECENT_POSTS="Recent Posts" +COM_MOKOSUITECROSS_SERVICESTATS_NO_POSTS="No posts for this service yet." +COM_MOKOSUITECROSS_SERVICESTATS_TOP_ARTICLES="Top Articles for This Service" + +; API Dispatch +COM_MOKOSUITECROSS_DISPATCH_MISSING_ARTICLE="Missing or invalid article_id in request body." +COM_MOKOSUITECROSS_DISPATCH_INVALID_SERVICES="service_ids must be a non-empty array of service IDs." +COM_MOKOSUITECROSS_DISPATCH_ARTICLE_NOT_FOUND="Article not found." +COM_MOKOSUITECROSS_DISPATCH_NO_SERVICES="No enabled services found matching the request." + +; Category Rules +COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES="Category Rules" +COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE="Category Routing" +COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE_DESC="Category routing rules let you map Joomla categories to specific cross-post services. When rules exist for a category, only those services receive posts. When no rules exist, all services are used (default behaviour). Rules are managed in the database table #__mokosuitecross_category_rules. A full admin UI will be added in a future release." diff --git a/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.sys.ini b/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.sys.ini new file mode 100644 index 0000000..d6e0e51 --- /dev/null +++ b/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.sys.ini @@ -0,0 +1,11 @@ +; MokoSuiteCross — System Language File +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +COM_MOKOSUITECROSS="MokoSuiteCross" +COM_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms" +COM_MOKOSUITECROSS_SUBMENU_DASHBOARD="Dashboard" +COM_MOKOSUITECROSS_SUBMENU_POSTS="Post Queue" +COM_MOKOSUITECROSS_SUBMENU_SERVICES="Services" +COM_MOKOSUITECROSS_SUBMENU_TEMPLATES="Templates" +COM_MOKOSUITECROSS_SUBMENU_LOGS="Activity Logs" diff --git a/src/packages/com_mokojoomcross/language/en-GB/index.html b/source/packages/com_mokosuitecross/language/en-GB/index.html similarity index 100% rename from src/packages/com_mokojoomcross/language/en-GB/index.html rename to source/packages/com_mokosuitecross/language/en-GB/index.html diff --git a/src/packages/com_mokojoomcross/language/index.html b/source/packages/com_mokosuitecross/language/index.html similarity index 100% rename from src/packages/com_mokojoomcross/language/index.html rename to source/packages/com_mokosuitecross/language/index.html diff --git a/src/packages/com_mokojoomcross/mokojoomcross.xml b/source/packages/com_mokosuitecross/mokosuitecross.xml similarity index 62% rename from src/packages/com_mokojoomcross/mokojoomcross.xml rename to source/packages/com_mokosuitecross/mokosuitecross.xml index 569badc..a74d62a 100644 --- a/src/packages/com_mokojoomcross/mokojoomcross.xml +++ b/source/packages/com_mokosuitecross/mokosuitecross.xml @@ -1,16 +1,16 @@ - com_mokojoomcross - 01.01.00 + com_mokosuitecross + 01.00.27-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech https://mokoconsulting.tech Copyright (C) 2026 Moko Consulting. All rights reserved. GPL-3.0-or-later - COM_MOKOJOOMCROSS_DESCRIPTION + COM_MOKOSUITECROSS_DESCRIPTION - Joomla\Component\MokoJoomCross + Joomla\Component\MokoSuiteCross script.php @@ -37,18 +37,19 @@ - site/language/en-GB/com_mokojoomcross.ini - language/en-GB/com_mokojoomcross.ini - language/en-GB/com_mokojoomcross.sys.ini + site/language/en-GB/com_mokosuitecross.ini + language/en-GB/com_mokosuitecross.ini + language/en-GB/com_mokosuitecross.sys.ini - COM_MOKOJOOMCROSS + COM_MOKOSUITECROSS - COM_MOKOJOOMCROSS_SUBMENU_DASHBOARD - COM_MOKOJOOMCROSS_SUBMENU_POSTS - COM_MOKOJOOMCROSS_SUBMENU_SERVICES - COM_MOKOJOOMCROSS_SUBMENU_LOGS + COM_MOKOSUITECROSS_SUBMENU_DASHBOARD + COM_MOKOSUITECROSS_SUBMENU_POSTS + COM_MOKOSUITECROSS_SUBMENU_SERVICES + COM_MOKOSUITECROSS_SUBMENU_TEMPLATES + COM_MOKOSUITECROSS_SUBMENU_LOGS access.xml diff --git a/src/packages/com_mokojoomcross/script.php b/source/packages/com_mokosuitecross/script.php similarity index 83% rename from src/packages/com_mokojoomcross/script.php rename to source/packages/com_mokosuitecross/script.php index c0c75f0..37d30cd 100644 --- a/src/packages/com_mokojoomcross/script.php +++ b/source/packages/com_mokosuitecross/script.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -13,7 +13,7 @@ defined('_JEXEC') or die; use Joomla\CMS\Installer\InstallerAdapter; -class Com_MokoJoomCrossInstallerScript +class Com_MokoSuiteCrossInstallerScript { public function preflight(string $type, InstallerAdapter $parent): bool { diff --git a/src/packages/com_mokojoomcross/services/index.html b/source/packages/com_mokosuitecross/services/index.html similarity index 100% rename from src/packages/com_mokojoomcross/services/index.html rename to source/packages/com_mokosuitecross/services/index.html diff --git a/src/packages/com_mokojoomcross/services/provider.php b/source/packages/com_mokosuitecross/services/provider.php similarity index 82% rename from src/packages/com_mokojoomcross/services/provider.php rename to source/packages/com_mokosuitecross/services/provider.php index fbf792c..aff11eb 100644 --- a/src/packages/com_mokojoomcross/services/provider.php +++ b/source/packages/com_mokosuitecross/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -16,7 +16,7 @@ use Joomla\CMS\Extension\ComponentInterface; use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory; use Joomla\CMS\Extension\Service\Provider\MVCFactory; use Joomla\CMS\MVC\Factory\MVCFactoryInterface; -use Joomla\Component\MokoJoomCross\Administrator\Extension\MokoJoomCrossComponent; +use Joomla\Component\MokoSuiteCross\Administrator\Extension\MokoSuiteCrossComponent; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; @@ -30,13 +30,13 @@ return new class () implements ServiceProviderInterface { */ public function register(Container $container): void { - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\MokoJoomCross')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\MokoJoomCross')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\MokoSuiteCross')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\MokoSuiteCross')); $container->set( ComponentInterface::class, function (Container $container) { - $component = new MokoJoomCrossComponent( + $component = new MokoSuiteCrossComponent( $container->get(ComponentDispatcherFactoryInterface::class) ); $component->setMVCFactory($container->get(MVCFactoryInterface::class)); diff --git a/src/packages/com_mokojoomcross/site/index.html b/source/packages/com_mokosuitecross/site/index.html similarity index 100% rename from src/packages/com_mokojoomcross/site/index.html rename to source/packages/com_mokosuitecross/site/index.html diff --git a/src/packages/com_mokojoomcross/site/language/en-GB/com_mokojoomcross.ini b/source/packages/com_mokosuitecross/site/language/en-GB/com_mokosuitecross.ini similarity index 50% rename from src/packages/com_mokojoomcross/site/language/en-GB/com_mokojoomcross.ini rename to source/packages/com_mokosuitecross/site/language/en-GB/com_mokosuitecross.ini index f138ddb..2ffcc30 100644 --- a/src/packages/com_mokojoomcross/site/language/en-GB/com_mokojoomcross.ini +++ b/source/packages/com_mokosuitecross/site/language/en-GB/com_mokosuitecross.ini @@ -1,5 +1,5 @@ -; MokoJoomCross — Site Frontend Language File +; MokoSuiteCross — Site Frontend Language File ; Copyright (C) 2026 Moko Consulting. All rights reserved. ; License: GPL-3.0-or-later -COM_MOKOJOOMCROSS="MokoJoomCross" +COM_MOKOSUITECROSS="MokoSuiteCross" diff --git a/src/packages/com_mokojoomcross/site/language/en-GB/index.html b/source/packages/com_mokosuitecross/site/language/en-GB/index.html similarity index 100% rename from src/packages/com_mokojoomcross/site/language/en-GB/index.html rename to source/packages/com_mokosuitecross/site/language/en-GB/index.html diff --git a/src/packages/com_mokojoomcross/site/language/index.html b/source/packages/com_mokosuitecross/site/language/index.html similarity index 100% rename from src/packages/com_mokojoomcross/site/language/index.html rename to source/packages/com_mokosuitecross/site/language/index.html diff --git a/src/packages/com_mokojoomcross/site/src/Controller/DisplayController.php b/source/packages/com_mokosuitecross/site/src/Controller/DisplayController.php similarity index 77% rename from src/packages/com_mokojoomcross/site/src/Controller/DisplayController.php rename to source/packages/com_mokosuitecross/site/src/Controller/DisplayController.php index 16df0d7..be00f93 100644 --- a/src/packages/com_mokojoomcross/site/src/Controller/DisplayController.php +++ b/source/packages/com_mokosuitecross/site/src/Controller/DisplayController.php @@ -1,15 +1,15 @@ * @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\Component\MokoJoomCross\Site\Controller; +namespace Joomla\Component\MokoSuiteCross\Site\Controller; defined('_JEXEC') or die; diff --git a/src/packages/com_mokojoomcross/site/src/Controller/index.html b/source/packages/com_mokosuitecross/site/src/Controller/index.html similarity index 100% rename from src/packages/com_mokojoomcross/site/src/Controller/index.html rename to source/packages/com_mokosuitecross/site/src/Controller/index.html diff --git a/src/packages/com_mokojoomcross/site/src/View/Post/index.html b/source/packages/com_mokosuitecross/site/src/View/Post/index.html similarity index 100% rename from src/packages/com_mokojoomcross/site/src/View/Post/index.html rename to source/packages/com_mokosuitecross/site/src/View/Post/index.html diff --git a/src/packages/com_mokojoomcross/site/src/View/index.html b/source/packages/com_mokosuitecross/site/src/View/index.html similarity index 100% rename from src/packages/com_mokojoomcross/site/src/View/index.html rename to source/packages/com_mokosuitecross/site/src/View/index.html diff --git a/src/packages/com_mokojoomcross/site/src/index.html b/source/packages/com_mokosuitecross/site/src/index.html similarity index 100% rename from src/packages/com_mokojoomcross/site/src/index.html rename to source/packages/com_mokosuitecross/site/src/index.html diff --git a/src/packages/com_mokojoomcross/site/tmpl/index.html b/source/packages/com_mokosuitecross/site/tmpl/index.html similarity index 100% rename from src/packages/com_mokojoomcross/site/tmpl/index.html rename to source/packages/com_mokosuitecross/site/tmpl/index.html diff --git a/src/packages/com_mokojoomcross/site/tmpl/post/index.html b/source/packages/com_mokosuitecross/site/tmpl/post/index.html similarity index 100% rename from src/packages/com_mokojoomcross/site/tmpl/post/index.html rename to source/packages/com_mokosuitecross/site/tmpl/post/index.html diff --git a/src/packages/com_mokojoomcross/sql/index.html b/source/packages/com_mokosuitecross/sql/index.html similarity index 100% rename from src/packages/com_mokojoomcross/sql/index.html rename to source/packages/com_mokosuitecross/sql/index.html diff --git a/src/packages/com_mokojoomcross/sql/install.mysql.sql b/source/packages/com_mokosuitecross/sql/install.mysql.sql similarity index 61% rename from src/packages/com_mokojoomcross/sql/install.mysql.sql rename to source/packages/com_mokosuitecross/sql/install.mysql.sql index ae0ac86..c89f0a9 100644 --- a/src/packages/com_mokojoomcross/sql/install.mysql.sql +++ b/source/packages/com_mokosuitecross/sql/install.mysql.sql @@ -1,8 +1,8 @@ --- MokoJoomCross 01.00.00 — Initial schema +-- MokoSuiteCross 01.00.00 — Initial schema -- Copyright (C) 2026 Moko Consulting. All rights reserved. -- SPDX-License-Identifier: GPL-3.0-or-later -CREATE TABLE IF NOT EXISTS `#__mokojoomcross_services` ( +CREATE TABLE IF NOT EXISTS `#__mokosuitecross_services` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL DEFAULT '', `alias` varchar(400) NOT NULL DEFAULT '', @@ -21,10 +21,10 @@ CREATE TABLE IF NOT EXISTS `#__mokojoomcross_services` ( KEY `idx_service_type` (`service_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -CREATE TABLE IF NOT EXISTS `#__mokojoomcross_posts` ( +CREATE TABLE IF NOT EXISTS `#__mokosuitecross_posts` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `article_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'FK to #__content.id', - `service_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'FK to #__mokojoomcross_services.id', + `service_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'FK to #__mokosuitecross_services.id', `status` varchar(20) NOT NULL DEFAULT 'queued' COMMENT 'queued, posting, posted, failed, scheduled', `message` text NOT NULL COMMENT 'Rendered message sent to platform', `platform_post_id` varchar(255) NOT NULL DEFAULT '' COMMENT 'Post ID returned by platform', @@ -32,7 +32,7 @@ CREATE TABLE IF NOT EXISTS `#__mokojoomcross_posts` ( `scheduled_at` datetime DEFAULT NULL COMMENT 'When to post (NULL = immediately)', `posted_at` datetime DEFAULT NULL COMMENT 'When actually posted', `retry_count` int(10) unsigned NOT NULL DEFAULT 0, - `error_message` text NOT NULL DEFAULT '', + `error_message` text NOT NULL, `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `modified` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`id`), @@ -42,7 +42,7 @@ CREATE TABLE IF NOT EXISTS `#__mokojoomcross_posts` ( KEY `idx_scheduled` (`scheduled_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -CREATE TABLE IF NOT EXISTS `#__mokojoomcross_templates` ( +CREATE TABLE IF NOT EXISTS `#__mokosuitecross_templates` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `service_type` varchar(50) NOT NULL DEFAULT '' COMMENT 'Platform this template is for (or "default")', `title` varchar(255) NOT NULL DEFAULT '', @@ -56,10 +56,10 @@ CREATE TABLE IF NOT EXISTS `#__mokojoomcross_templates` ( KEY `idx_published` (`published`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -CREATE TABLE IF NOT EXISTS `#__mokojoomcross_logs` ( +CREATE TABLE IF NOT EXISTS `#__mokosuitecross_logs` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `post_id` int(10) unsigned DEFAULT NULL COMMENT 'FK to #__mokojoomcross_posts.id', - `service_id` int(10) unsigned DEFAULT NULL COMMENT 'FK to #__mokojoomcross_services.id', + `post_id` int(10) unsigned DEFAULT NULL COMMENT 'FK to #__mokosuitecross_posts.id', + `service_id` int(10) unsigned DEFAULT NULL COMMENT 'FK to #__mokosuitecross_services.id', `level` varchar(20) NOT NULL DEFAULT 'info' COMMENT 'info, warning, error', `message` text NOT NULL, `context` text NOT NULL COMMENT 'JSON — additional context data', @@ -72,8 +72,34 @@ CREATE TABLE IF NOT EXISTS `#__mokojoomcross_logs` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Insert default templates -INSERT INTO `#__mokojoomcross_templates` (`service_type`, `title`, `template_body`, `published`, `ordering`, `created`) VALUES +INSERT INTO `#__mokosuitecross_templates` (`service_type`, `title`, `template_body`, `published`, `ordering`, `created`) VALUES ('default', 'Default Template', '{title}\n\n{introtext}\n\n{url}', 1, 1, NOW()), ('twitter', 'Twitter/X Default', '{title}\n\n{url}', 1, 2, NOW()), ('mastodon', 'Mastodon Default', '{title}\n\n{introtext}\n\n{url}\n\n#Joomla', 1, 3, NOW()), -('mailchimp', 'Mailchimp Default', '

{title}

\n

{introtext}

\n

Read more

', 1, 4, NOW()); +('mailchimp', 'Mailchimp Default', '

{title}

\n

{introtext}

\n

Read more

', 1, 4, NOW()), +('telegram', 'Telegram Default', '{title}\n\n{introtext}\n\nRead more', 1, 5, NOW()), +('discord', 'Discord Default', '**{title}**\n\n{introtext}\n\n{url}', 1, 6, NOW()), +('slack', 'Slack Default', '*{title}*\n\n{introtext}\n\n{url}', 1, 7, NOW()), +('facebook', 'Facebook Default', '{title}\n\n{introtext}\n\n{url}', 1, 8, NOW()), +('linkedin', 'LinkedIn Default', '{title}\n\n{introtext}\n\n{url}', 1, 9, NOW()), +('bluesky', 'Bluesky Default', '{title}\n\n{url}', 1, 10, NOW()), +('threads', 'Threads Default', '{title}\n\n{introtext}\n\n{url}', 1, 11, NOW()), +('teams', 'Teams Default', '**{title}**\n\n{introtext}\n\n[Read more]({url})', 1, 12, NOW()), +('medium', 'Medium Default', '{title}\n\n{introtext}\n\n{url}', 1, 13, NOW()), +('wordpress', 'WordPress Default', '{title}\n\n{introtext}\n\n{url}', 1, 14, NOW()), +('webhook', 'Webhook Default', '{title}\n\n{introtext}\n\n{url}', 1, 15, NOW()), +('sendgrid', 'SendGrid Default', '

{title}

\n

{introtext}

\n

Read more

', 1, 16, NOW()), +('brevo', 'Brevo Default', '

{title}

\n

{introtext}

\n

Read more

', 1, 17, NOW()), +('ntfy', 'Ntfy Default', '{title}: {introtext}', 1, 18, NOW()), +('reddit', 'Reddit Default', '{title}', 1, 19, NOW()), +('pinterest', 'Pinterest Default', '{title} - {introtext}', 1, 20, NOW()); + +CREATE TABLE IF NOT EXISTS `#__mokosuitecross_category_rules` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `category_id` int(10) unsigned NOT NULL, + `service_id` int(10) unsigned NOT NULL, + `published` tinyint(1) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_category_service` (`category_id`, `service_id`), + KEY `idx_category` (`category_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/source/packages/com_mokosuitecross/sql/uninstall.mysql.sql b/source/packages/com_mokosuitecross/sql/uninstall.mysql.sql new file mode 100644 index 0000000..94b03c5 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/uninstall.mysql.sql @@ -0,0 +1,5 @@ +-- MokoSuiteCross — Uninstall +DROP TABLE IF EXISTS `#__mokosuitecross_logs`; +DROP TABLE IF EXISTS `#__mokosuitecross_posts`; +DROP TABLE IF EXISTS `#__mokosuitecross_templates`; +DROP TABLE IF EXISTS `#__mokosuitecross_services`; diff --git a/src/packages/com_mokojoomcross/sql/updates/index.html b/source/packages/com_mokosuitecross/sql/updates/index.html similarity index 100% rename from src/packages/com_mokojoomcross/sql/updates/index.html rename to source/packages/com_mokosuitecross/sql/updates/index.html diff --git a/src/packages/com_mokojoomcross/sql/updates/mysql/01.00.00.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.00.00.sql similarity index 50% rename from src/packages/com_mokojoomcross/sql/updates/mysql/01.00.00.sql rename to source/packages/com_mokosuitecross/sql/updates/mysql/01.00.00.sql index 8319501..d8d2fdf 100644 --- a/src/packages/com_mokojoomcross/sql/updates/mysql/01.00.00.sql +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.00.00.sql @@ -1,2 +1,2 @@ --- MokoJoomCross 01.00.00 — Initial release +-- MokoSuiteCross 01.00.00 — Initial release -- No update queries needed for initial version diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.01.00.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.01.00.sql new file mode 100644 index 0000000..189059a --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.01.00.sql @@ -0,0 +1,14 @@ +-- MokoSuiteCross 01.01.00 — Category routing rules +-- Copyright (C) 2026 Moko Consulting. All rights reserved. +-- SPDX-License-Identifier: GPL-3.0-or-later +-- Note: also in install.mysql.sql for fresh installs; IF NOT EXISTS prevents conflicts + +CREATE TABLE IF NOT EXISTS `#__mokosuitecross_category_rules` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `category_id` int(10) unsigned NOT NULL, + `service_id` int(10) unsigned NOT NULL, + `published` tinyint(1) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_category_service` (`category_id`, `service_id`), + KEY `idx_category` (`category_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/src/packages/com_mokojoomcross/sql/updates/mysql/index.html b/source/packages/com_mokosuitecross/sql/updates/mysql/index.html similarity index 100% rename from src/packages/com_mokojoomcross/sql/updates/mysql/index.html rename to source/packages/com_mokosuitecross/sql/updates/mysql/index.html diff --git a/source/packages/com_mokosuitecross/src/Controller/DashboardController.php b/source/packages/com_mokosuitecross/src/Controller/DashboardController.php new file mode 100644 index 0000000..a872de2 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Controller/DashboardController.php @@ -0,0 +1,61 @@ + + * @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\Component\MokoSuiteCross\Administrator\Controller; + +defined('_JEXEC') or die; + +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\Router\Route; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\MigrationHelper; + +class DashboardController extends BaseController +{ + /** + * Run Perfect Publisher Pro migration. + * + * @return void + */ + public function migrate(): void + { + $this->checkToken(); + + // Check ACL + if (!$this->app->getIdentity()->authorise('mokosuitecross.migrate', 'com_mokosuitecross')) { + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=dashboard', false), + Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), + 'error' + ); + + return; + } + + $result = MigrationHelper::migrate(); + + if (!empty($result['errors'])) { + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=dashboard', false), + Text::sprintf('COM_MOKOSUITECROSS_MIGRATION_ERROR', implode('; ', $result['errors'])), + 'error' + ); + + return; + } + + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=dashboard', false), + Text::sprintf('COM_MOKOSUITECROSS_MIGRATION_SUCCESS', $result['migrated'], $result['skipped']), + 'success' + ); + } +} diff --git a/source/packages/com_mokosuitecross/src/Controller/DispatchController.php b/source/packages/com_mokosuitecross/src/Controller/DispatchController.php new file mode 100644 index 0000000..8f08367 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Controller/DispatchController.php @@ -0,0 +1,262 @@ + + * @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\Component\MokoSuiteCross\Administrator\Controller; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\CMS\Uri\Uri; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\CrossPostDispatcher; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; + +/** + * REST API controller for dispatching cross-posts. + * + * Endpoint: POST /api/index.php/v1/mokosuitecross/dispatch + * + * JSON body: + * { + * "article_id": 123, + * "service_ids": [1, 2, 3] // optional — omit to post to all enabled services + * } + * + * Returns JSON with the created post IDs and status. + * + * Authentication is handled by Joomla's API application (token or session). + * The webservices plugin routes POST requests here via the API router. + */ +class DispatchController extends BaseController +{ + /** + * Dispatch cross-posts for an article to one or more services. + * + * @return void + */ + public function dispatch(): void + { + $app = $this->app; + + // Enforce POST method — this is a state-changing action endpoint + if (strtoupper($this->input->getMethod()) !== 'POST') { + $this->sendJsonResponse(['error' => 'Method not allowed. Use POST.'], 405); + + return; + } + + // ACL check — require core.manage on the component + if (!Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) { + $this->sendJsonResponse(['error' => 'Forbidden'], 403); + + return; + } + + // Read JSON body + $input = json_decode(file_get_contents('php://input'), true) ?: []; + $articleId = (int) ($input['article_id'] ?? 0); + $serviceIds = $input['service_ids'] ?? null; + + if ($articleId < 1) { + $this->sendJsonResponse(['error' => Text::_('COM_MOKOSUITECROSS_DISPATCH_MISSING_ARTICLE')], 400); + + return; + } + + // Validate service_ids if provided + if ($serviceIds !== null) { + if (!is_array($serviceIds) || empty($serviceIds)) { + $this->sendJsonResponse(['error' => Text::_('COM_MOKOSUITECROSS_DISPATCH_INVALID_SERVICES')], 400); + + return; + } + + $serviceIds = array_map('intval', $serviceIds); + } + + $db = Factory::getDbo(); + + // Load the article + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__content')) + ->where($db->quoteName('id') . ' = ' . $articleId); + + $db->setQuery($query); + $article = $db->loadObject(); + + if (!$article) { + $this->sendJsonResponse(['error' => Text::_('COM_MOKOSUITECROSS_DISPATCH_ARTICLE_NOT_FOUND')], 404); + + return; + } + + // Load enabled services, optionally filtered by service_ids + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokosuitecross_services')) + ->where($db->quoteName('published') . ' = 1') + ->order($db->quoteName('ordering') . ' ASC'); + + if ($serviceIds !== null) { + $query->where($db->quoteName('id') . ' IN (' . implode(',', $serviceIds) . ')'); + } + + $db->setQuery($query); + $services = $db->loadObjectList() ?: []; + + if (empty($services)) { + $this->sendJsonResponse(['error' => Text::_('COM_MOKOSUITECROSS_DISPATCH_NO_SERVICES')], 404); + + return; + } + + // Import service plugins and build type-to-plugin map. + // In Joomla 5+ with SubscriberInterface, plugins receive the Event object + // as their first argument. When they do $services[] = $this, they append to + // the Event via ArrayAccess at numeric indices starting at 1. + PluginHelper::importPlugin('mokosuitecross'); + + $servicePlugins = []; + $event = new \Joomla\Event\Event('onMokoSuiteCrossGetServices', [$servicePlugins]); + + try { + $app->getDispatcher()->dispatch('onMokoSuiteCrossGetServices', $event); + } catch (\Throwable $e) { + // Dispatcher may not be available + } + + // Read plugins back from the Event's ArrayAccess indices + $idx = 1; + + while (isset($event[$idx])) { + $servicePlugins[] = $event[$idx]; + $idx++; + } + + $pluginMap = []; + + foreach ($servicePlugins as $plugin) { + if ($plugin instanceof MokoSuiteCrossServiceInterface) { + $pluginMap[$plugin->getServiceType()] = $plugin; + } + } + + // Create queue entries + $now = Factory::getDate()->toSql(); + $createdIds = []; + $skipped = []; + + // Build article URL + $articleUrl = Uri::root() . 'index.php?option=com_content&view=article&id=' . $article->id; + + if (!empty($article->catid)) { + $articleUrl .= '&catid=' . $article->catid; + } + + // Extract intro image for media + $media = []; + $images = json_decode($article->images ?? '{}'); + + if (!empty($images->image_intro)) { + $media[] = Uri::root() . ltrim($images->image_intro, '/'); + } + + foreach ($services as $service) { + // Duplicate guard — skip if article already posted/queued for this service + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuitecross_posts')) + ->where($db->quoteName('article_id') . ' = ' . (int) $article->id) + ->where($db->quoteName('service_id') . ' = ' . (int) $service->id) + ->where($db->quoteName('status') . ' IN (' . $db->quote('queued') . ',' . $db->quote('posted') . ',' . $db->quote('posting') . ')'); + + $db->setQuery($query); + + if ((int) $db->loadResult() > 0) { + $skipped[] = [ + 'service_id' => (int) $service->id, + 'service_type' => $service->service_type, + 'reason' => 'duplicate', + ]; + + continue; + } + + // Render template via shared dispatcher logic + $message = CrossPostDispatcher::renderTemplate($article, $service); + + // Create queue entry + $post = (object) [ + 'article_id' => (int) $article->id, + 'service_id' => (int) $service->id, + 'status' => 'queued', + 'message' => $message, + 'platform_post_id' => '', + 'platform_response' => '', + 'error_message' => '', + 'retry_count' => 0, + 'created' => $now, + 'modified' => $now, + ]; + + $db->insertObject('#__mokosuitecross_posts', $post); + $postId = (int) $db->insertid(); + + $createdIds[] = [ + 'post_id' => $postId, + 'service_id' => (int) $service->id, + 'service_type' => $service->service_type, + 'status' => 'queued', + ]; + + // Write log entry + $log = (object) [ + 'post_id' => $postId, + 'service_id' => (int) $service->id, + 'level' => 'info', + 'message' => sprintf('API dispatch: queued article %d to %s', $article->id, $service->service_type), + 'context' => '{}', + 'created' => $now, + ]; + + $db->insertObject('#__mokosuitecross_logs', $log); + } + + $this->sendJsonResponse([ + 'article_id' => (int) $article->id, + 'dispatched' => $createdIds, + 'skipped' => $skipped, + ], 200); + } + + /** + * Send a JSON response and close the application. + * + * @param array $data Response data + * @param int $httpCode HTTP status code + * + * @return void + */ + private function sendJsonResponse(array $data, int $httpCode): void + { + $app = $this->app; + + $app->setHeader('Content-Type', 'application/json; charset=utf-8'); + $app->setHeader('Status', (string) $httpCode); + + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + $app->close(); + } +} diff --git a/src/packages/com_mokojoomcross/src/Controller/DisplayController.php b/source/packages/com_mokosuitecross/src/Controller/DisplayController.php similarity index 79% rename from src/packages/com_mokojoomcross/src/Controller/DisplayController.php rename to source/packages/com_mokosuitecross/src/Controller/DisplayController.php index 8eaf818..507992b 100644 --- a/src/packages/com_mokojoomcross/src/Controller/DisplayController.php +++ b/source/packages/com_mokosuitecross/src/Controller/DisplayController.php @@ -1,15 +1,15 @@ * @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\Component\MokoJoomCross\Administrator\Controller; +namespace Joomla\Component\MokoSuiteCross\Administrator\Controller; defined('_JEXEC') or die; diff --git a/source/packages/com_mokosuitecross/src/Controller/OauthController.php b/source/packages/com_mokosuitecross/src/Controller/OauthController.php new file mode 100644 index 0000000..fce3cfb --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Controller/OauthController.php @@ -0,0 +1,198 @@ + + * @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\Component\MokoSuiteCross\Administrator\Controller; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\CMS\Router\Route; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\OAuthHelper; + +/** + * OAuth controller for handling browser-based authorization flows. + * + * Endpoints: + * task=oauth.authorize — Initiate OAuth flow (redirect to platform) + * task=oauth.callback — Handle platform redirect with auth code + */ +class OauthController extends BaseController +{ + /** + * Initiate OAuth authorization for a service. + * + * Expects: service_id (int) in request + */ + public function authorize(): void + { + $this->checkToken(); + + $serviceId = $this->input->getInt('service_id', 0); + + if (!$serviceId) { + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=services', false), + Text::_('COM_MOKOSUITECROSS_OAUTH_NO_SERVICE'), + 'error' + ); + + return; + } + + $db = \Joomla\CMS\Factory::getDbo(); + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokosuitecross_services')) + ->where($db->quoteName('id') . ' = ' . $serviceId); + + $db->setQuery($query); + $service = $db->loadObject(); + + if (!$service) { + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=services', false), + Text::_('COM_MOKOSUITECROSS_OAUTH_SERVICE_NOT_FOUND'), + 'error' + ); + + return; + } + + // Get client ID from plugin params + PluginHelper::importPlugin('mokosuitecross'); + $pluginParams = PluginHelper::getPlugin('mokosuitecross', $service->service_type); + $params = json_decode($pluginParams->params ?? '{}', true) ?: []; + + $clientId = $params['client_id'] ?? ''; + + if (empty($clientId)) { + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=services', false), + Text::sprintf('COM_MOKOSUITECROSS_OAUTH_NO_CLIENT_ID', ucfirst($service->service_type)), + 'error' + ); + + return; + } + + // Generate CSRF nonce and store in session + $nonce = bin2hex(random_bytes(16)); + Factory::getApplication()->getSession()->set('mokosuitecross.oauth_nonce', $nonce); + + $url = OAuthHelper::getAuthorizeUrl($service->service_type, $serviceId, $clientId, $nonce); + + if (!$url) { + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=services', false), + Text::sprintf('COM_MOKOSUITECROSS_OAUTH_NOT_SUPPORTED', ucfirst($service->service_type)), + 'error' + ); + + return; + } + + $this->app->redirect($url); + } + + /** + * Handle OAuth callback from platform. + * + * Expects: code (string), state (base64 JSON with service_id) + */ + public function callback(): void + { + $code = $this->input->getString('code', ''); + $state = $this->input->getString('state', ''); + $error = $this->input->getString('error', ''); + + if ($error) { + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=services', false), + Text::sprintf('COM_MOKOSUITECROSS_OAUTH_PLATFORM_ERROR', $error), + 'error' + ); + + return; + } + + if (empty($code) || empty($state)) { + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=services', false), + Text::_('COM_MOKOSUITECROSS_OAUTH_INVALID_CALLBACK'), + 'error' + ); + + return; + } + + $stateData = json_decode(base64_decode($state), true); + $serviceId = (int) ($stateData['service_id'] ?? 0); + $serviceType = $stateData['type'] ?? ''; + $stateNonce = $stateData['nonce'] ?? ''; + + if (!$serviceId || !$serviceType) { + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=services', false), + Text::_('COM_MOKOSUITECROSS_OAUTH_INVALID_STATE'), + 'error' + ); + + return; + } + + // CSRF nonce validation — compare state nonce against session + $session = Factory::getApplication()->getSession(); + $sessionNonce = $session->get('mokosuitecross.oauth_nonce', ''); + $session->clear('mokosuitecross.oauth_nonce'); + + if (empty($stateNonce) || !hash_equals($sessionNonce, $stateNonce)) { + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=services', false), + Text::_('COM_MOKOSUITECROSS_OAUTH_INVALID_STATE'), + 'error' + ); + + return; + } + + // Get client credentials from plugin params + PluginHelper::importPlugin('mokosuitecross'); + $pluginParams = PluginHelper::getPlugin('mokosuitecross', $serviceType); + $params = json_decode($pluginParams->params ?? '{}', true) ?: []; + + $clientId = $params['client_id'] ?? ''; + $clientSecret = $params['client_secret'] ?? ''; + + $tokenData = OAuthHelper::exchangeCode($serviceType, $code, $clientId, $clientSecret); + + if (!empty($tokenData['error'])) { + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&task=service.edit&id=' . $serviceId, false), + Text::sprintf('COM_MOKOSUITECROSS_OAUTH_TOKEN_ERROR', $tokenData['error']), + 'error' + ); + + return; + } + + OAuthHelper::storeToken($serviceId, $tokenData); + + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&task=service.edit&id=' . $serviceId, false), + Text::sprintf('COM_MOKOSUITECROSS_OAUTH_SUCCESS', ucfirst($serviceType)), + 'success' + ); + } +} diff --git a/src/packages/com_mokojoomcross/src/Controller/PostController.php b/source/packages/com_mokosuitecross/src/Controller/PostController.php similarity index 74% rename from src/packages/com_mokojoomcross/src/Controller/PostController.php rename to source/packages/com_mokosuitecross/src/Controller/PostController.php index 9ea2f46..bdfea31 100644 --- a/src/packages/com_mokojoomcross/src/Controller/PostController.php +++ b/source/packages/com_mokosuitecross/src/Controller/PostController.php @@ -1,15 +1,15 @@ * @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\Component\MokoJoomCross\Administrator\Controller; +namespace Joomla\Component\MokoSuiteCross\Administrator\Controller; defined('_JEXEC') or die; diff --git a/source/packages/com_mokosuitecross/src/Controller/PostsController.php b/source/packages/com_mokosuitecross/src/Controller/PostsController.php new file mode 100644 index 0000000..9371c9d --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Controller/PostsController.php @@ -0,0 +1,258 @@ + + * @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\Component\MokoSuiteCross\Administrator\Controller; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\AdminController; +use Joomla\CMS\Router\Route; + +class PostsController extends AdminController +{ + public function getModel($name = 'Post', $prefix = 'Administrator', $config = ['ignore_request' => true]) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Schedule selected posts for a future date/time. + * + * @return void + */ + public function schedule(): void + { + $this->checkToken(); + + $ids = $this->input->get('cid', [], 'array'); + $scheduledAt = $this->input->getString('scheduled_at', ''); + + if (empty($ids)) { + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=posts', false), + Text::_('COM_MOKOSUITECROSS_POSTS_NO_ITEM_SELECTED'), + 'warning' + ); + return; + } + + if (empty($scheduledAt)) { + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=posts', false), + Text::_('COM_MOKOSUITECROSS_SCHEDULE_NO_DATE'), + 'warning' + ); + return; + } + + try { + $scheduledDate = Factory::getDate($scheduledAt); + $scheduledAt = $scheduledDate->toSql(); + } catch (\Throwable $e) { + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=posts', false), + Text::_('COM_MOKOSUITECROSS_SCHEDULE_INVALID_DATE'), + 'error' + ); + return; + } + + $db = Factory::getDbo(); + $now = Factory::getDate()->toSql(); + + foreach ($ids as $id) { + $query = $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('scheduled_at') . ' = ' . $db->quote($scheduledAt)) + ->set($db->quoteName('status') . ' = ' . $db->quote('queued')) + ->set($db->quoteName('modified') . ' = ' . $db->quote($now)) + ->where($db->quoteName('id') . ' = ' . (int) $id) + ->where($db->quoteName('status') . ' IN (' + . $db->quote('queued') . ',' . $db->quote('failed') . ',' + . $db->quote('permanently_failed') . ',' . $db->quote('cancelled') . ')'); + + $db->setQuery($query); + $db->execute(); + } + + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=posts', false), + Text::sprintf('COM_MOKOSUITECROSS_POSTS_N_SCHEDULED', count($ids)), + 'success' + ); + } + + /** + * Retry selected failed/permanently_failed posts. + * + * @return void + */ + public function retrySelected(): void + { + $this->checkToken(); + + $ids = $this->input->get('cid', [], 'array'); + + if (empty($ids)) { + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=posts', false), + Text::_('COM_MOKOSUITECROSS_POSTS_NO_ITEM_SELECTED'), + 'warning' + ); + return; + } + + $count = \Joomla\Component\MokoSuiteCross\Administrator\Helper\QueueProcessor::retryPosts($ids); + + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=posts', false), + Text::sprintf('COM_MOKOSUITECROSS_POSTS_N_RETRIED', $count), + 'success' + ); + } + + /** + * Re-queue all failed posts by resetting their status to queued and retry count to 0. + * + * @return void + */ + public function retryFailed(): void + { + $this->checkToken(); + + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('status') . ' = ' . $db->quote('queued')) + ->set($db->quoteName('retry_count') . ' = 0') + ->set($db->quoteName('error_message') . ' = ' . $db->quote('')) + ->set($db->quoteName('modified') . ' = ' . $db->quote(Factory::getDate()->toSql())) + ->where($db->quoteName('status') . ' IN (' . $db->quote('failed') . ',' . $db->quote('permanently_failed') . ',' . $db->quote('cancelled') . ')'); + + $db->setQuery($query); + $db->execute(); + + $count = $db->getAffectedRows(); + + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=posts', false), + Text::plural('COM_MOKOSUITECROSS_POSTS_N_RETRIED', $count), + 'success' + ); + } + + /** + * Export posts as CSV download. + * + * @return void + */ + public function exportCsv(): void + { + $this->checkToken('get'); + + if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + $app = $this->app; + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select([ + $db->quoteName('c.title', 'article_title'), + 'CONCAT(' . $db->quoteName('s.title') . ', ' . $db->quote(' (') . ', ' + . $db->quoteName('s.service_type') . ', ' . $db->quote(')') . ') AS service', + $db->quoteName('a.status'), + $db->quoteName('a.message'), + $db->quoteName('a.posted_at'), + $db->quoteName('a.error_message'), + $db->quoteName('a.platform_post_id'), + $db->quoteName('a.created'), + ]) + ->from($db->quoteName('#__mokosuitecross_posts', 'a')) + ->join('LEFT', $db->quoteName('#__content', 'c') + . ' ON ' . $db->quoteName('c.id') . ' = ' . $db->quoteName('a.article_id')) + ->join('LEFT', $db->quoteName('#__mokosuitecross_services', 's') + . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('a.service_id')) + ->order($db->quoteName('a.created') . ' DESC'); + + // Apply current filters + $status = $app->input->get('filter_status', '', 'string'); + + if (!empty($status)) { + $query->where($db->quoteName('a.status') . ' = ' . $db->quote($status)); + } + + $serviceId = $app->input->getInt('filter_service_id', 0); + + if (!empty($serviceId)) { + $query->where($db->quoteName('a.service_id') . ' = ' . (int) $serviceId); + } + + $search = $app->input->get('filter_search', '', 'string'); + + if (!empty($search)) { + $search = '%' . $db->escape(trim($search), true) . '%'; + $query->where('(' . $db->quoteName('c.title') . ' LIKE ' . $db->quote($search) + . ' OR ' . $db->quoteName('a.message') . ' LIKE ' . $db->quote($search) . ')'); + } + + $db->setQuery($query); + $rows = $db->loadAssocList() ?: []; + + $filename = 'mokosuitecross-posts-' . Factory::getDate()->format('Y-m-d') . '.csv'; + + $app->setHeader('Content-Type', 'text/csv; charset=utf-8'); + $app->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"'); + $app->sendHeaders(); + + $fp = fopen('php://output', 'w'); + fputcsv($fp, ['Article', 'Service', 'Status', 'Message', 'Posted At', 'Error', 'Platform Post ID', 'Created']); + + foreach ($rows as $row) { + fputcsv($fp, $row); + } + + fclose($fp); + + $app->close(); + } + + /** + * Purge (delete) all posts with status 'posted'. + * + * @return void + */ + public function purgePosted(): void + { + $this->checkToken(); + + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__mokosuitecross_posts')) + ->where($db->quoteName('status') . ' = ' . $db->quote('posted')); + + $db->setQuery($query); + $db->execute(); + + $count = $db->getAffectedRows(); + + $this->setRedirect( + Route::_('index.php?option=com_mokosuitecross&view=posts', false), + Text::plural('COM_MOKOSUITECROSS_POSTS_N_PURGED', $count), + 'success' + ); + } +} diff --git a/source/packages/com_mokosuitecross/src/Controller/ServiceController.php b/source/packages/com_mokosuitecross/src/Controller/ServiceController.php new file mode 100644 index 0000000..882c13a --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Controller/ServiceController.php @@ -0,0 +1,104 @@ + + * @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\Component\MokoSuiteCross\Administrator\Controller; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\FormController; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\CMS\Response\JsonResponse; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; + +class ServiceController extends FormController +{ + /** + * Test connection to a service by validating its credentials. + * + * @return void + */ + public function testConnection(): void + { + $this->checkToken(); + + if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + $app = $this->app; + $id = (int) $this->input->getInt('id', 0); + + try { + if ($id <= 0) { + throw new \RuntimeException(Text::_('COM_MOKOSUITECROSS_TEST_CONNECTION_NO_SERVICE')); + } + + // Load the service record + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokosuitecross_services')) + ->where($db->quoteName('id') . ' = ' . $id); + $db->setQuery($query); + $service = $db->loadObject(); + + if (!$service) { + throw new \RuntimeException(Text::_('COM_MOKOSUITECROSS_TEST_CONNECTION_NOT_FOUND')); + } + + // Get service plugins via dispatcher (Joomla 5+ Event ArrayAccess pattern) + PluginHelper::importPlugin('mokosuitecross'); + + $servicePlugins = []; + $event = new \Joomla\Event\Event('onMokoSuiteCrossGetServices', [$servicePlugins]); + $app->getDispatcher()->dispatch('onMokoSuiteCrossGetServices', $event); + + $idx = 1; + + while (isset($event[$idx])) { + $servicePlugins[] = $event[$idx]; + $idx++; + } + + // Find the matching plugin + $plugin = null; + + foreach ($servicePlugins as $sp) { + if ($sp instanceof MokoSuiteCrossServiceInterface && $sp->getServiceType() === $service->service_type) { + $plugin = $sp; + break; + } + } + + if (!$plugin) { + throw new \RuntimeException(Text::sprintf('COM_MOKOSUITECROSS_TEST_CONNECTION_NO_PLUGIN', $service->service_type)); + } + + // Decode credentials and validate + $credentials = \Joomla\Component\MokoSuiteCross\Administrator\Helper\CredentialHelper::decrypt($service->credentials ?: ''); + $result = $plugin->validateCredentials($credentials); + + $app->mimeType = 'application/json'; + $app->setHeader('Content-Type', 'application/json; charset=utf-8'); + + echo new JsonResponse($result); + } catch (\Throwable $e) { + $app->mimeType = 'application/json'; + $app->setHeader('Content-Type', 'application/json; charset=utf-8'); + + echo new JsonResponse($e); + } + + $app->close(); + } +} diff --git a/src/packages/com_mokojoomcross/src/Controller/ServicesController.php b/source/packages/com_mokosuitecross/src/Controller/ServicesController.php similarity index 81% rename from src/packages/com_mokojoomcross/src/Controller/ServicesController.php rename to source/packages/com_mokosuitecross/src/Controller/ServicesController.php index 65b708d..80d75eb 100644 --- a/src/packages/com_mokojoomcross/src/Controller/ServicesController.php +++ b/source/packages/com_mokosuitecross/src/Controller/ServicesController.php @@ -1,15 +1,15 @@ * @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\Component\MokoJoomCross\Administrator\Controller; +namespace Joomla\Component\MokoSuiteCross\Administrator\Controller; defined('_JEXEC') or die; diff --git a/src/packages/com_mokojoomcross/src/Controller/ServiceController.php b/source/packages/com_mokosuitecross/src/Controller/TemplateController.php similarity index 65% rename from src/packages/com_mokojoomcross/src/Controller/ServiceController.php rename to source/packages/com_mokosuitecross/src/Controller/TemplateController.php index e9a3258..df31894 100644 --- a/src/packages/com_mokojoomcross/src/Controller/ServiceController.php +++ b/source/packages/com_mokosuitecross/src/Controller/TemplateController.php @@ -1,20 +1,20 @@ * @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\Component\MokoJoomCross\Administrator\Controller; +namespace Joomla\Component\MokoSuiteCross\Administrator\Controller; defined('_JEXEC') or die; use Joomla\CMS\MVC\Controller\FormController; -class ServiceController extends FormController +class TemplateController extends FormController { } diff --git a/src/packages/com_mokojoomcross/src/Controller/PostsController.php b/source/packages/com_mokosuitecross/src/Controller/TemplatesController.php similarity index 58% rename from src/packages/com_mokojoomcross/src/Controller/PostsController.php rename to source/packages/com_mokosuitecross/src/Controller/TemplatesController.php index 79adc59..591a5a6 100644 --- a/src/packages/com_mokojoomcross/src/Controller/PostsController.php +++ b/source/packages/com_mokosuitecross/src/Controller/TemplatesController.php @@ -1,23 +1,23 @@ * @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\Component\MokoJoomCross\Administrator\Controller; +namespace Joomla\Component\MokoSuiteCross\Administrator\Controller; defined('_JEXEC') or die; use Joomla\CMS\MVC\Controller\AdminController; -class PostsController extends AdminController +class TemplatesController extends AdminController { - public function getModel($name = 'Post', $prefix = 'Administrator', $config = ['ignore_request' => true]) + public function getModel($name = 'Template', $prefix = 'Administrator', $config = ['ignore_request' => true]) { return parent::getModel($name, $prefix, $config); } diff --git a/src/packages/com_mokojoomcross/src/Controller/index.html b/source/packages/com_mokosuitecross/src/Controller/index.html similarity index 100% rename from src/packages/com_mokojoomcross/src/Controller/index.html rename to source/packages/com_mokosuitecross/src/Controller/index.html diff --git a/src/packages/com_mokojoomcross/src/Extension/MokoJoomCrossComponent.php b/source/packages/com_mokosuitecross/src/Extension/MokoSuiteCrossComponent.php similarity index 64% rename from src/packages/com_mokojoomcross/src/Extension/MokoJoomCrossComponent.php rename to source/packages/com_mokosuitecross/src/Extension/MokoSuiteCrossComponent.php index ca58a96..88b9112 100644 --- a/src/packages/com_mokojoomcross/src/Extension/MokoJoomCrossComponent.php +++ b/source/packages/com_mokosuitecross/src/Extension/MokoSuiteCrossComponent.php @@ -1,20 +1,20 @@ * @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\Component\MokoJoomCross\Administrator\Extension; +namespace Joomla\Component\MokoSuiteCross\Administrator\Extension; defined('_JEXEC') or die; use Joomla\CMS\Extension\MVCComponent; -class MokoJoomCrossComponent extends MVCComponent +class MokoSuiteCrossComponent extends MVCComponent { } diff --git a/src/packages/com_mokojoomcross/src/Extension/index.html b/source/packages/com_mokosuitecross/src/Extension/index.html similarity index 100% rename from src/packages/com_mokojoomcross/src/Extension/index.html rename to source/packages/com_mokosuitecross/src/Extension/index.html diff --git a/source/packages/com_mokosuitecross/src/Helper/CredentialHelper.php b/source/packages/com_mokosuitecross/src/Helper/CredentialHelper.php new file mode 100644 index 0000000..4926aa5 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Helper/CredentialHelper.php @@ -0,0 +1,110 @@ + + * @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\Component\MokoSuiteCross\Administrator\Helper; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; + +/** + * Encrypts and decrypts service credentials using libsodium. + * + * Uses Joomla's $secret from configuration.php as the key source. + * Falls back to plaintext JSON if sodium is unavailable or decryption + * fails (backward compat with existing unencrypted credentials). + */ +class CredentialHelper +{ + private const PREFIX = 'enc:sodium:'; + + /** + * Encrypt a credentials array to a storable string. + * + * @param array $credentials Credentials to encrypt + * + * @return string Encrypted string prefixed with "enc:sodium:", or plain JSON as fallback + */ + public static function encrypt(array $credentials): string + { + $json = json_encode($credentials); + + if (!function_exists('sodium_crypto_secretbox')) { + return $json; + } + + try { + $key = self::deriveKey(); + $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + $cipher = sodium_crypto_secretbox($json, $nonce, $key); + + return self::PREFIX . base64_encode($nonce . $cipher); + } catch (\Throwable $e) { + return $json; + } + } + + /** + * Decrypt a credentials string back to an array. + * + * Handles both encrypted (prefixed) and legacy plaintext JSON. + * + * @param string $stored Stored credential string + * + * @return array Decoded credentials + */ + public static function decrypt(string $stored): array + { + if (empty($stored)) { + return []; + } + + // Legacy plaintext JSON — no prefix + if (!str_starts_with($stored, self::PREFIX)) { + return json_decode($stored, true) ?: []; + } + + if (!function_exists('sodium_crypto_secretbox_open')) { + return []; + } + + try { + $key = self::deriveKey(); + $payload = base64_decode(substr($stored, strlen(self::PREFIX))); + + if ($payload === false || strlen($payload) < SODIUM_CRYPTO_SECRETBOX_NONCEBYTES) { + return []; + } + + $nonce = substr($payload, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + $cipher = substr($payload, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + $plain = sodium_crypto_secretbox_open($cipher, $nonce, $key); + + if ($plain === false) { + return []; + } + + return json_decode($plain, true) ?: []; + } catch (\Throwable $e) { + return []; + } + } + + /** + * Derive a 32-byte encryption key from Joomla's secret. + */ + private static function deriveKey(): string + { + $secret = Factory::getApplication()->get('secret', ''); + + return sodium_crypto_generichash($secret, '', SODIUM_CRYPTO_SECRETBOX_KEYBYTES); + } +} diff --git a/source/packages/com_mokosuitecross/src/Helper/CrossPostDispatcher.php b/source/packages/com_mokosuitecross/src/Helper/CrossPostDispatcher.php new file mode 100644 index 0000000..e6d4d09 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Helper/CrossPostDispatcher.php @@ -0,0 +1,494 @@ + + * @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\Component\MokoSuiteCross\Administrator\Helper; + +defined('_JEXEC') or die; + +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\CMS\Uri\Uri; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; + +/** + * Static dispatcher for cross-posting content from any source plugin. + * + * Centralises the dispatch logic that was previously only in the system plugin, + * so content-type source plugins (articles, calendar events, gallery items) can + * trigger cross-posts without coupling to plg_system_mokosuitecross. + */ +class CrossPostDispatcher +{ + /** + * Dispatch an article-like payload to all enabled cross-post services. + * + * @param object $article Article or article-like object + * @param string $articleUrl Canonical URL for the content item + * @param string|null $contentType Content type context (e.g. 'com_content.article') + */ + public static function dispatch(object $article, string $articleUrl = '', ?string $contentType = null): void + { + $db = Factory::getDbo(); + + // Load all enabled services + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokosuitecross_services')) + ->where($db->quoteName('published') . ' = 1') + ->order($db->quoteName('ordering') . ' ASC'); + + $db->setQuery($query); + $services = $db->loadObjectList(); + + if (empty($services)) { + return; + } + + // Import service plugins so they register with the dispatcher + PluginHelper::importPlugin('mokosuitecross'); + + // Collect registered service plugin instances. + // In Joomla 5+ with SubscriberInterface, plugins receive the Event object + // as their first argument. When they do $services[] = $this, they append to + // the Event via ArrayAccess at numeric indices starting at 1. + $servicePlugins = []; + $event = new \Joomla\Event\Event('onMokoSuiteCrossGetServices', [$servicePlugins]); + + try { + Factory::getApplication()->getDispatcher()->dispatch('onMokoSuiteCrossGetServices', $event); + } catch (\Throwable $e) { + // Dispatcher may not be available in all contexts + } + + // Read plugins back from the Event's ArrayAccess indices + $idx = 1; + + while (isset($event[$idx])) { + $servicePlugins[] = $event[$idx]; + $idx++; + } + + // Index by service type for lookup + $pluginMap = []; + + foreach ($servicePlugins as $plugin) { + if ($plugin instanceof MokoSuiteCrossServiceInterface) { + $pluginMap[$plugin->getServiceType()] = $plugin; + } + } + + $componentParams = ComponentHelper::getParams('com_mokosuitecross'); + + // Per-article selective cross-posting (#19) + $attribs = json_decode($article->attribs ?? '{}', true) ?: []; + $selectedServiceIds = $attribs['mokosuitecross_services'] ?? null; + $skipCrossPost = !empty($attribs['mokosuitecross_skip']); + + if ($skipCrossPost) { + return; + } + + // If specific services selected, convert to array of ints for filtering + if (is_array($selectedServiceIds) && !empty($selectedServiceIds)) { + $selectedServiceIds = array_map('intval', $selectedServiceIds); + } else { + $selectedServiceIds = null; // null = post to all + } + + // Category routing rules — whitelist services by category + $categoryServiceIds = null; + + if (!empty($article->catid)) { + $query = $db->getQuery(true) + ->select('service_id') + ->from($db->quoteName('#__mokosuitecross_category_rules')) + ->where($db->quoteName('category_id') . ' = ' . (int) $article->catid) + ->where($db->quoteName('published') . ' = 1'); + $db->setQuery($query); + $ruleIds = $db->loadColumn(); + + if (!empty($ruleIds)) { + $categoryServiceIds = array_map('intval', $ruleIds); + } + } + + // Determine service type filter from content type property + $serviceTypeFilter = $article->_content_type ?? null; + + // Batch duplicate guard — single query for all services (fixes N query overhead) + $serviceIdList = implode(',', array_map(function ($s) { return (int) $s->id; }, $services)); + $query = $db->getQuery(true) + ->select($db->quoteName('service_id')) + ->from($db->quoteName('#__mokosuitecross_posts')) + ->where($db->quoteName('article_id') . ' = ' . (int) $article->id) + ->where($db->quoteName('service_id') . ' IN (' . $serviceIdList . ')') + ->where($db->quoteName('status') . ' IN (' . $db->quote('queued') . ',' . $db->quote('posted') . ',' . $db->quote('posting') . ')'); + $db->setQuery($query); + $existingServiceIds = array_map('intval', $db->loadColumn() ?: []); + + // Batch template loading — single query for all needed service types + default + $serviceTypes = array_unique(array_column($services, 'service_type')); + $typeQuotes = array_map([$db, 'quote'], $serviceTypes); + $typeQuotes[] = $db->quote('default'); + $query = $db->getQuery(true) + ->select([$db->quoteName('service_type'), $db->quoteName('template_body')]) + ->from($db->quoteName('#__mokosuitecross_templates')) + ->where($db->quoteName('published') . ' = 1') + ->where($db->quoteName('service_type') . ' IN (' . implode(',', $typeQuotes) . ')') + ->order($db->quoteName('service_type') . ' ASC'); + $db->setQuery($query); + $templateRows = $db->loadObjectList() ?: []; + + $templateMap = []; + + foreach ($templateRows as $row) { + $templateMap[$row->service_type] = $row->template_body; + } + + // Pre-build article metadata once (category, author, tags) — avoids N queries per service + $articleMeta = self::buildArticleMeta($article); + + foreach ($services as $service) { + // Category routing filter — if rules exist, only post to whitelisted services + if ($categoryServiceIds !== null && !in_array((int) $service->id, $categoryServiceIds, true)) { + continue; + } + // Service type filter for non-article content types + if ($serviceTypeFilter !== null && $service->service_type !== $serviceTypeFilter) { + continue; + } + + // Per-article filter + if ($selectedServiceIds !== null && !in_array((int) $service->id, $selectedServiceIds, true)) { + continue; + } + + // Batch duplicate guard check + if (in_array((int) $service->id, $existingServiceIds, true)) { + continue; + } + + $message = self::renderTemplate($article, $service, $templateMap, $articleMeta); + + // Extract intro image for media attachment + $media = []; + $images = json_decode($article->images ?? '{}'); + + if (!empty($images->image_intro)) { + $media[] = Uri::root() . ltrim($images->image_intro, '/'); + } + + // Create queue entry + $post = (object) [ + 'article_id' => (int) $article->id, + 'service_id' => (int) $service->id, + 'status' => 'queued', + 'message' => $message, + 'platform_post_id' => '', + 'platform_response' => '', + 'error_message' => '', + 'retry_count' => 0, + 'created' => Factory::getDate()->toSql(), + 'modified' => Factory::getDate()->toSql(), + ]; + + $db->insertObject('#__mokosuitecross_posts', $post); + $postId = $db->insertid(); + + // Resolve article URL + $url = $article->_article_url ?? $articleUrl; + + if (empty($url)) { + $url = Uri::root() . 'index.php?option=com_content&view=article&id=' . $article->id + . (!empty($article->catid) ? '&catid=' . $article->catid : ''); + } + + // Attempt immediate dispatch if service plugin is available + $plugin = $pluginMap[$service->service_type] ?? null; + + if ($plugin) { + self::executePost($db, $postId, $plugin, $message, $service, $media, $url); + } else { + self::log($db, $postId, $service->id, 'warning', + sprintf('No service plugin found for type "%s" — post remains queued', $service->service_type)); + } + } + } + + /** + * Execute a cross-post via the service plugin. + */ + private static function executePost($db, int $postId, MokoSuiteCrossServiceInterface $plugin, string $message, object $service, array $media = [], string $articleUrl = ''): void + { + // Mark as posting + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('status') . ' = ' . $db->quote('posting')) + ->set($db->quoteName('modified') . ' = ' . $db->quote(Factory::getDate()->toSql())) + ->where($db->quoteName('id') . ' = ' . $postId) + ); + $db->execute(); + + $credentials = CredentialHelper::decrypt($service->credentials ?: ''); + $params = json_decode($service->params ?: '{}', true) ?: []; + + if (!empty($articleUrl)) { + $params['_article_url'] = $articleUrl; + } + + // Lifecycle event: before post + $cancel = false; + $dispatcher = Factory::getApplication()->getDispatcher(); + + try { + $beforeEvent = new \Joomla\Event\Event('onMokoSuiteCrossBeforePost', [$postId, &$message, $service->service_type, &$cancel]); + $dispatcher->dispatch('onMokoSuiteCrossBeforePost', $beforeEvent); + } catch (\Throwable $e) { + // Dispatcher may not be available + } + + if ($cancel) { + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('status') . ' = ' . $db->quote('cancelled')) + ->set($db->quoteName('modified') . ' = ' . $db->quote(Factory::getDate()->toSql())) + ->where($db->quoteName('id') . ' = ' . $postId) + ); + $db->execute(); + + self::log($db, $postId, $service->id, 'info', + sprintf('Post to %s cancelled by onMokoSuiteCrossBeforePost event', $service->service_type)); + + return; + } + + try { + $result = $plugin->publish($message, $media, $credentials, $params); + + if (!empty($result['success'])) { + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('status') . ' = ' . $db->quote('posted')) + ->set($db->quoteName('platform_post_id') . ' = ' . $db->quote($result['platform_post_id'] ?? '')) + ->set($db->quoteName('platform_response') . ' = ' . $db->quote(json_encode($result['response'] ?? []))) + ->set($db->quoteName('posted_at') . ' = ' . $db->quote(Factory::getDate()->toSql())) + ->set($db->quoteName('modified') . ' = ' . $db->quote(Factory::getDate()->toSql())) + ->where($db->quoteName('id') . ' = ' . $postId) + ); + $db->execute(); + + self::log($db, $postId, $service->id, 'info', + sprintf('Posted to %s (platform ID: %s)', $service->service_type, $result['platform_post_id'] ?? 'n/a')); + + try { + $afterEvent = new \Joomla\Event\Event('onMokoSuiteCrossAfterPost', [$postId, $service->service_type, $result]); + $dispatcher->dispatch('onMokoSuiteCrossAfterPost', $afterEvent); + } catch (\Throwable $e) { + // Non-critical + } + } else { + $errorMsg = $result['response']['error'] ?? json_encode($result['response'] ?? []); + + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('status') . ' = ' . $db->quote('failed')) + ->set($db->quoteName('error_message') . ' = ' . $db->quote(mb_substr($errorMsg, 0, 1000))) + ->set($db->quoteName('platform_response') . ' = ' . $db->quote(json_encode($result['response'] ?? []))) + ->set($db->quoteName('modified') . ' = ' . $db->quote(Factory::getDate()->toSql())) + ->where($db->quoteName('id') . ' = ' . $postId) + ); + $db->execute(); + + self::log($db, $postId, $service->id, 'error', + sprintf('Failed to post to %s: %s', $service->service_type, $errorMsg)); + + try { + $failedEvent = new \Joomla\Event\Event('onMokoSuiteCrossPostFailed', [$postId, $service->service_type, $errorMsg]); + $dispatcher->dispatch('onMokoSuiteCrossPostFailed', $failedEvent); + } catch (\Throwable $e) { + // Non-critical + } + } + } catch (\Throwable $e) { + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('status') . ' = ' . $db->quote('failed')) + ->set($db->quoteName('error_message') . ' = ' . $db->quote(mb_substr($e->getMessage(), 0, 1000))) + ->set($db->quoteName('modified') . ' = ' . $db->quote(Factory::getDate()->toSql())) + ->where($db->quoteName('id') . ' = ' . $postId) + ); + $db->execute(); + + self::log($db, $postId, $service->id, 'error', + sprintf('Exception posting to %s: %s', $service->service_type, $e->getMessage())); + + try { + $failedEvent = new \Joomla\Event\Event('onMokoSuiteCrossPostFailed', [$postId, $service->service_type, $e->getMessage()]); + $dispatcher->dispatch('onMokoSuiteCrossPostFailed', $failedEvent); + } catch (\Throwable $ex) { + // Non-critical + } + } + } + + /** + * Build article metadata (category, author, tags, image) for template rendering. + * Call once per article, then pass to renderTemplate() for each service. + * + * @param object $article Article object + * + * @return array Pre-resolved metadata for template placeholders + */ + public static function buildArticleMeta(object $article): array + { + $db = Factory::getDbo(); + + $url = $article->_article_url + ?? (Uri::root() . 'index.php?option=com_content&view=article&id=' . $article->id + . (!empty($article->catid) ? '&catid=' . $article->catid : '')); + + $categoryName = ''; + + if (!empty($article->catid)) { + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('id') . ' = ' . (int) $article->catid); + $db->setQuery($query); + $categoryName = $db->loadResult() ?: ''; + } + + $authorName = ''; + + if (!empty($article->created_by)) { + $query = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('id') . ' = ' . (int) $article->created_by); + $db->setQuery($query); + $authorName = $db->loadResult() ?: ''; + } + + $introImage = ''; + $images = json_decode($article->images ?? '{}'); + + if (!empty($images->image_intro)) { + $introImage = Uri::root() . ltrim($images->image_intro, '/'); + } + + $tagNames = []; + + if (!empty($article->id)) { + $query = $db->getQuery(true) + ->select($db->quoteName('t.title')) + ->from($db->quoteName('#__tags', 't')) + ->join('INNER', $db->quoteName('#__contentitem_tag_map', 'm') + . ' ON ' . $db->quoteName('m.tag_id') . ' = ' . $db->quoteName('t.id')) + ->where($db->quoteName('m.type_alias') . ' = ' . $db->quote('com_content.article')) + ->where($db->quoteName('m.content_item_id') . ' = ' . (int) $article->id) + ->where($db->quoteName('t.published') . ' = 1'); + $db->setQuery($query); + $tagNames = $db->loadColumn() ?: []; + } + + $tagsComma = implode(', ', $tagNames); + $hashtags = implode(' ', array_map(function ($tag) { + return '#' . preg_replace('/\s+/', '', $tag); + }, $tagNames)); + + return [ + '{title}' => $article->title ?? '', + '{introtext}' => strip_tags(mb_substr($article->introtext ?? '', 0, 280)), + '{fulltext}' => strip_tags(mb_substr($article->fulltext ?? '', 0, 500)), + '{url}' => $url, + '{image}' => $introImage, + '{category}' => $categoryName, + '{author}' => $authorName, + '{date}' => Factory::getDate($article->publish_up ?? 'now')->format('Y-m-d'), + '{tags}' => $tagsComma, + '{hashtags}' => $hashtags, + ]; + } + + /** + * Render the message template for a service. + * + * @param object $article Article object + * @param object $service Service object + * @param array $templateMap Pre-loaded template map (service_type => body) + * @param array $articleMeta Pre-built article metadata from buildArticleMeta() + */ + public static function renderTemplate(object $article, object $service, array $templateMap = [], array $articleMeta = []): string + { + $db = Factory::getDbo(); + + // Use pre-loaded template map if available, otherwise query + if (!empty($templateMap)) { + $template = $templateMap[$service->service_type] ?? $templateMap['default'] ?? "{title}\n\n{url}"; + } else { + $query = $db->getQuery(true) + ->select($db->quoteName('template_body')) + ->from($db->quoteName('#__mokosuitecross_templates')) + ->where($db->quoteName('published') . ' = 1') + ->where('(' . $db->quoteName('service_type') . ' = ' . $db->quote($service->service_type) + . ' OR ' . $db->quoteName('service_type') . ' = ' . $db->quote('default') . ')') + ->order('CASE WHEN ' . $db->quoteName('service_type') . ' = ' + . $db->quote($service->service_type) . ' THEN 0 ELSE 1 END') + ->setLimit(1); + + $db->setQuery($query); + $template = $db->loadResult() ?: "{title}\n\n{url}"; + } + + // Use pre-built metadata if available, otherwise build on the fly + $replacements = !empty($articleMeta) ? $articleMeta : self::buildArticleMeta($article); + + $message = str_replace(array_keys($replacements), array_values($replacements), $template); + + // Resolve custom field placeholders: {field:field_name} + $message = preg_replace_callback('/\{field:([a-zA-Z0-9_-]+)\}/', function ($matches) use ($db, $article) { + $fieldName = $matches[1]; + $query = $db->getQuery(true) + ->select('fv.value') + ->from($db->quoteName('#__fields_values', 'fv')) + ->join('INNER', $db->quoteName('#__fields', 'f') . ' ON f.id = fv.field_id') + ->where('f.name = ' . $db->quote($fieldName)) + ->where('fv.item_id = ' . (int) $article->id); + $db->setQuery($query); + return $db->loadResult() ?: ''; + }, $message); + + return $message; + } + + /** + * Write an entry to the activity log. + */ + private static function log($db, ?int $postId, ?int $serviceId, string $level, string $message): void + { + $log = (object) [ + 'post_id' => $postId, + 'service_id' => $serviceId, + 'level' => $level, + 'message' => mb_substr($message, 0, 2000), + 'context' => '{}', + 'created' => Factory::getDate()->toSql(), + ]; + + $db->insertObject('#__mokosuitecross_logs', $log); + } +} diff --git a/source/packages/com_mokosuitecross/src/Helper/MigrationHelper.php b/source/packages/com_mokosuitecross/src/Helper/MigrationHelper.php new file mode 100644 index 0000000..0b8128a --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Helper/MigrationHelper.php @@ -0,0 +1,388 @@ + + * @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\Component\MokoSuiteCross\Administrator\Helper; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; + +/** + * Migration helper for importing settings from Perfect Publisher Pro (com_autotweet). + * + * PP Pro stores channels in #__autotweet_channels with a channeltype_id FK + * to #__autotweet_channeltypes. Each channel has a JSON params column + * containing OAuth tokens, API keys, webhook URLs, etc. + * + * This helper reads those channels and creates MokoSuiteCross service records. + */ +class MigrationHelper +{ + /** + * Channel type name → MokoSuiteCross service type mapping. + * PP Pro channeltype names vary; we match common patterns. + */ + private const CHANNEL_MAP = [ + 'facebook' => 'facebook', + 'fb' => 'facebook', + 'twitter' => 'twitter', + 'tw' => 'twitter', + 'linkedin' => 'linkedin', + 'li' => 'linkedin', + 'telegram' => 'telegram', + 'tg' => 'telegram', + 'discord' => 'discord', + 'slack' => 'slack', + 'mastodon' => 'mastodon', + ]; + + /** + * Run the full migration from Perfect Publisher Pro. + * + * Strategy: + * 1. Try reading #__autotweet_channels (PP Pro's channel table) + * 2. Fall back to reading component params if table doesn't exist + * 3. Create disabled MokoSuiteCross service records + * + * @return array ['migrated' => int, 'skipped' => int, 'errors' => string[]] + */ + public static function migrate(): array + { + $db = Factory::getDbo(); + $result = ['migrated' => 0, 'skipped' => 0, 'errors' => []]; + + // Check if PP Pro is installed + if (!self::isPPProInstalled($db)) { + $result['errors'][] = 'Perfect Publisher Pro (com_autotweet) is not installed.'; + + return $result; + } + + // Try channel-based migration first (PP Pro stores configs in #__autotweet_channels) + if (self::hasChannelTable($db)) { + $result = self::migrateFromChannels($db, $result); + } else { + // Fall back to component params extraction + $result = self::migrateFromParams($db, $result); + } + + // Clear migration flag from MokoSuiteCross params + self::clearMigrationFlag($db); + + return $result; + } + + /** + * Check if PP Pro is installed. + */ + private static function isPPProInstalled($db): bool + { + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__extensions')) + ->where('(' . $db->quoteName('element') . ' = ' . $db->quote('com_autotweet') + . ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('%perfectpublisher%') . ')') + ->where($db->quoteName('type') . ' = ' . $db->quote('component')); + + $db->setQuery($query); + + return (int) $db->loadResult() > 0; + } + + /** + * Check if the autotweet_channels table exists. + */ + private static function hasChannelTable($db): bool + { + $prefix = $db->getPrefix(); + + try { + $db->setQuery('SHOW TABLES LIKE ' . $db->quote($prefix . 'autotweet_channels')); + + return !empty($db->loadResult()); + } catch (\Throwable $e) { + return false; + } + } + + /** + * Migrate from #__autotweet_channels table (primary method). + */ + private static function migrateFromChannels($db, array $result): array + { + // Load channels with their type names + $query = $db->getQuery(true) + ->select('c.id, c.name, c.published, c.params') + ->select($db->quoteName('ct.name', 'type_name')) + ->from($db->quoteName('#__autotweet_channels', 'c')) + ->join('LEFT', $db->quoteName('#__autotweet_channeltypes', 'ct') + . ' ON ' . $db->quoteName('ct.id') . ' = ' . $db->quoteName('c.channeltype_id')); + + $db->setQuery($query); + $channels = $db->loadObjectList(); + + if (empty($channels)) { + $result['errors'][] = 'No channels found in Perfect Publisher Pro.'; + + return $result; + } + + foreach ($channels as $channel) { + $typeName = strtolower(trim($channel->type_name ?? '')); + + // Match to MokoSuiteCross service type + $mjcType = null; + + foreach (self::CHANNEL_MAP as $pattern => $serviceType) { + if (str_contains($typeName, $pattern)) { + $mjcType = $serviceType; + break; + } + } + + if (!$mjcType) { + $result['skipped']++; + continue; + } + + // Check for duplicate (same type + migrated alias) + $alias = $mjcType . '-pp-' . $channel->id; + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuitecross_services')) + ->where($db->quoteName('alias') . ' = ' . $db->quote($alias)); + $db->setQuery($query); + + if ((int) $db->loadResult() > 0) { + $result['skipped']++; + continue; + } + + // Parse channel params to extract credentials + $channelParams = json_decode($channel->params ?: '{}', true) ?: []; + $credentials = self::mapChannelCredentials($mjcType, $channelParams); + + if (empty($credentials)) { + $result['skipped']++; + continue; + } + + // Create MokoSuiteCross service record + $service = (object) [ + 'title' => $channel->name ?: ucfirst($mjcType) . ' (PP Pro #' . $channel->id . ')', + 'alias' => $alias, + 'service_type' => $mjcType, + 'credentials' => json_encode($credentials), + 'params' => '{}', + 'published' => 0, // Disabled — user must verify before enabling + 'ordering' => 0, + 'created' => Factory::getDate()->toSql(), + 'modified' => Factory::getDate()->toSql(), + 'created_by' => Factory::getApplication()->getIdentity()->id ?? 0, + ]; + + try { + $db->insertObject('#__mokosuitecross_services', $service); + $result['migrated']++; + } catch (\Throwable $e) { + $result['errors'][] = sprintf('Failed to create %s service: %s', $mjcType, $e->getMessage()); + } + } + + return $result; + } + + /** + * Map PP Pro channel params to MokoSuiteCross credential format. + * + * PP Pro stores various keys in channel params depending on the type. + * We normalize them to MokoSuiteCross's expected credential structure. + */ + private static function mapChannelCredentials(string $serviceType, array $channelParams): array + { + $creds = ['mode' => 'custom']; + + // Common OAuth fields PP Pro uses + $oauthFields = ['access_token', 'access_secret', 'client_id', 'client_secret', + 'api_key', 'api_secret', 'app_id', 'app_secret', 'token']; + + switch ($serviceType) { + case 'facebook': + $creds['page_access_token'] = $channelParams['access_token'] ?? $channelParams['token'] ?? ''; + $creds['page_id'] = $channelParams['page_id'] ?? $channelParams['pageid'] ?? ''; + break; + + case 'twitter': + $creds['bearer_token'] = $channelParams['bearer_token'] ?? ''; + $creds['api_key'] = $channelParams['api_key'] ?? $channelParams['consumer_key'] ?? ''; + $creds['api_secret'] = $channelParams['api_secret'] ?? $channelParams['consumer_secret'] ?? ''; + $creds['access_token'] = $channelParams['access_token'] ?? ''; + $creds['access_token_secret'] = $channelParams['access_secret'] ?? $channelParams['access_token_secret'] ?? ''; + break; + + case 'linkedin': + $creds['access_token'] = $channelParams['access_token'] ?? $channelParams['token'] ?? ''; + $creds['organization_id'] = $channelParams['company_id'] ?? $channelParams['organization_id'] ?? ''; + $creds['person_id'] = $channelParams['person_id'] ?? $channelParams['member_id'] ?? ''; + break; + + case 'telegram': + $creds['bot_token'] = $channelParams['bot_token'] ?? $channelParams['token'] ?? $channelParams['api_key'] ?? ''; + $creds['chat_id'] = $channelParams['chat_id'] ?? $channelParams['channel_id'] ?? ''; + break; + + case 'discord': + $creds['webhook_url'] = $channelParams['webhook_url'] ?? $channelParams['webhook'] ?? ''; + break; + + case 'slack': + $creds['webhook_url'] = $channelParams['webhook_url'] ?? $channelParams['webhook'] ?? ''; + break; + + case 'mastodon': + $creds['instance_url'] = $channelParams['instance_url'] ?? $channelParams['server'] ?? ''; + $creds['access_token'] = $channelParams['access_token'] ?? $channelParams['token'] ?? ''; + break; + + default: + // Generic: copy all non-empty params + foreach ($channelParams as $key => $value) { + if (!empty($value) && is_string($value)) { + $creds[$key] = $value; + } + } + } + + // Remove empty credential values and the mode key for check + $check = array_filter($creds, fn($v, $k) => $k !== 'mode' && !empty($v), ARRAY_FILTER_USE_BOTH); + + return empty($check) ? [] : $creds; + } + + /** + * Fallback: migrate from component params when channel table doesn't exist. + */ + private static function migrateFromParams($db, array $result): array + { + $query = $db->getQuery(true) + ->select($db->quoteName('params')) + ->from($db->quoteName('#__extensions')) + ->where('(' . $db->quoteName('element') . ' = ' . $db->quote('com_autotweet') + . ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('%perfectpublisher%') . ')') + ->where($db->quoteName('type') . ' = ' . $db->quote('component')); + + $db->setQuery($query); + $rawParams = $db->loadResult(); + + if (!$rawParams) { + $result['errors'][] = 'No PP Pro configuration found.'; + + return $result; + } + + $params = json_decode($rawParams, true); + + if (!is_array($params)) { + $result['errors'][] = 'Could not parse PP Pro configuration.'; + + return $result; + } + + // Extract services from component params using prefix patterns + $servicePatterns = [ + 'facebook' => ['facebook_', 'fb_'], + 'twitter' => ['twitter_', 'tw_'], + 'linkedin' => ['linkedin_', 'li_'], + 'telegram' => ['telegram_', 'tg_'], + ]; + + foreach ($servicePatterns as $mjcType => $prefixes) { + $credentials = ['mode' => 'custom']; + $found = false; + + foreach ($params as $key => $value) { + foreach ($prefixes as $prefix) { + if (str_starts_with($key, $prefix) && !empty($value)) { + $cleanKey = substr($key, strlen($prefix)); + $credentials[$cleanKey] = $value; + $found = true; + } + } + } + + if (!$found) { + $result['skipped']++; + continue; + } + + // Duplicate check + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuitecross_services')) + ->where($db->quoteName('service_type') . ' = ' . $db->quote($mjcType)) + ->where($db->quoteName('alias') . ' LIKE ' . $db->quote('%-migrated%')); + $db->setQuery($query); + + if ((int) $db->loadResult() > 0) { + $result['skipped']++; + continue; + } + + $service = (object) [ + 'title' => ucfirst($mjcType) . ' (migrated from PP Pro)', + 'alias' => $mjcType . '-migrated', + 'service_type' => $mjcType, + 'credentials' => json_encode($credentials), + 'params' => '{}', + 'published' => 0, + 'ordering' => 0, + 'created' => Factory::getDate()->toSql(), + 'modified' => Factory::getDate()->toSql(), + 'created_by' => Factory::getApplication()->getIdentity()->id ?? 0, + ]; + + try { + $db->insertObject('#__mokosuitecross_services', $service); + $result['migrated']++; + } catch (\Throwable $e) { + $result['errors'][] = sprintf('Failed to create %s: %s', $mjcType, $e->getMessage()); + } + } + + return $result; + } + + /** + * Clear the migration flag from MokoSuiteCross component params. + */ + private static function clearMigrationFlag($db): void + { + $query = $db->getQuery(true) + ->select($db->quoteName('params')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuitecross')); + + $db->setQuery($query); + $params = json_decode($db->loadResult() ?: '{}', true) ?: []; + + unset($params['migration_available'], $params['migration_source_params']); + + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params))) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuitecross')); + + $db->setQuery($query); + $db->execute(); + } +} diff --git a/source/packages/com_mokosuitecross/src/Helper/MokoSuiteCrossHelper.php b/source/packages/com_mokosuitecross/src/Helper/MokoSuiteCrossHelper.php new file mode 100644 index 0000000..2762fc1 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Helper/MokoSuiteCrossHelper.php @@ -0,0 +1,74 @@ + + * @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\Component\MokoSuiteCross\Administrator\Helper; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; + +/** + * Component helper — renders the admin submenu. + * + * Uses Joomla 5+ toolbar submenu API when available, falling back to the + * deprecated Sidebar API for Joomla 4 compatibility. + */ +class MokoSuiteCrossHelper +{ + /** + * Configure the submenu links. + * + * Called from each view's display() to highlight the active item. + * + * @param string $activeView The current view name + * + * @return void + */ + public static function addSubmenu(string $activeView): void + { + $views = [ + 'dashboard' => 'COM_MOKOSUITECROSS_SUBMENU_DASHBOARD', + 'posts' => 'COM_MOKOSUITECROSS_SUBMENU_POSTS', + 'services' => 'COM_MOKOSUITECROSS_SUBMENU_SERVICES', + 'templates' => 'COM_MOKOSUITECROSS_SUBMENU_TEMPLATES', + 'logs' => 'COM_MOKOSUITECROSS_SUBMENU_LOGS', + ]; + + // Joomla 5+ toolbar submenu + if (class_exists('Joomla\CMS\Toolbar\Toolbar')) { + try { + $toolbar = Factory::getApplication()->getDocument()->getToolbar('submenu'); + + if ($toolbar && method_exists($toolbar, 'linkButton')) { + foreach ($views as $view => $langKey) { + $toolbar->linkButton($view, Text::_($langKey)) + ->url('index.php?option=com_mokosuitecross&view=' . $view) + ->active($activeView === $view); + } + + return; + } + } catch (\Throwable $e) { + // Fall through to legacy sidebar + } + } + + // Legacy fallback for Joomla 4 + foreach ($views as $view => $langKey) { + \Joomla\CMS\HTML\Sidebar::addEntry( + Text::_($langKey), + 'index.php?option=com_mokosuitecross&view=' . $view, + $activeView === $view + ); + } + } +} diff --git a/source/packages/com_mokosuitecross/src/Helper/OAuthHelper.php b/source/packages/com_mokosuitecross/src/Helper/OAuthHelper.php new file mode 100644 index 0000000..0d51b3b --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Helper/OAuthHelper.php @@ -0,0 +1,311 @@ + + * @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\Component\MokoSuiteCross\Administrator\Helper; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\CMS\Uri\Uri; + +/** + * OAuth helper for services requiring browser-based authorization. + * + * Handles the OAuth 2.0 authorization code flow: + * 1. Generate authorize URL → redirect user to platform + * 2. Platform redirects back with auth code + * 3. Exchange code for access token + * 4. Store token in service credentials + * + * Each platform has its own endpoints and scopes. The service plugin + * provides these via OAuthConfigInterface (if it supports OAuth). + */ +class OAuthHelper +{ + /** + * OAuth endpoint configs per service type. + */ + private const OAUTH_CONFIGS = [ + 'facebook' => [ + 'authorize_url' => 'https://www.facebook.com/v19.0/dialog/oauth', + 'token_url' => 'https://graph.facebook.com/v19.0/oauth/access_token', + 'scopes' => 'pages_manage_posts,pages_read_engagement', + ], + 'linkedin' => [ + 'authorize_url' => 'https://www.linkedin.com/oauth/v2/authorization', + 'token_url' => 'https://www.linkedin.com/oauth/v2/accessToken', + 'scopes' => 'w_member_social', + ], + 'twitter' => [ + 'authorize_url' => 'https://twitter.com/i/oauth2/authorize', + 'token_url' => 'https://api.twitter.com/2/oauth2/token', + 'scopes' => 'tweet.read tweet.write users.read', + ], + ]; + + /** + * Build the authorization URL for a given service. + * + * @param string $serviceType Service type (facebook, linkedin, twitter) + * @param int $serviceId Service record ID (passed through state param) + * @param string $clientId OAuth client/app ID + * + * @return string|null Authorization URL or null if not supported + */ + public static function getAuthorizeUrl(string $serviceType, int $serviceId, string $clientId, string $nonce = ''): ?string + { + $config = self::OAUTH_CONFIGS[$serviceType] ?? null; + + if (!$config) { + return null; + } + + $redirectUri = self::getCallbackUrl(); + $statePayload = ['service_id' => $serviceId, 'type' => $serviceType]; + + if (!empty($nonce)) { + $statePayload['nonce'] = $nonce; + } + + $state = base64_encode(json_encode($statePayload)); + + $params = [ + 'client_id' => $clientId, + 'redirect_uri' => $redirectUri, + 'response_type' => 'code', + 'scope' => $config['scopes'], + 'state' => $state, + ]; + + // Twitter uses PKCE + if ($serviceType === 'twitter') { + $verifier = bin2hex(random_bytes(32)); + $challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '='); + + // Store verifier in session for token exchange + Factory::getApplication()->getSession()->set('mokosuitecross.pkce_verifier', $verifier); + + $params['code_challenge'] = $challenge; + $params['code_challenge_method'] = 'S256'; + } + + return $config['authorize_url'] . '?' . http_build_query($params); + } + + /** + * Exchange authorization code for access token. + * + * @param string $serviceType Service type + * @param string $code Authorization code from callback + * @param string $clientId OAuth client ID + * @param string $clientSecret OAuth client secret + * + * @return array ['access_token' => '...', 'expires_in' => N, ...] or ['error' => '...'] + */ + public static function exchangeCode(string $serviceType, string $code, string $clientId, string $clientSecret): array + { + $config = self::OAUTH_CONFIGS[$serviceType] ?? null; + + if (!$config) { + return ['error' => 'Unsupported service type for OAuth']; + } + + $postData = [ + 'grant_type' => 'authorization_code', + 'code' => $code, + 'redirect_uri' => self::getCallbackUrl(), + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + ]; + + // Twitter PKCE + if ($serviceType === 'twitter') { + $verifier = Factory::getApplication()->getSession()->get('mokosuitecross.pkce_verifier', ''); + $postData['code_verifier'] = $verifier; + } + + $ch = curl_init($config['token_url']); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query($postData), + CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded', 'Accept: application/json'], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $data = json_decode($response, true) ?: []; + + if ($httpCode >= 200 && $httpCode < 300 && !empty($data['access_token'])) { + return $data; + } + + return ['error' => $data['error_description'] ?? $data['error'] ?? 'Token exchange failed']; + } + + /** + * Store OAuth token in the service credentials. + * + * @param int $serviceId Service record ID + * @param array $tokenData Token response from platform + * + * @return bool + */ + public static function storeToken(int $serviceId, array $tokenData): bool + { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select($db->quoteName('credentials')) + ->from($db->quoteName('#__mokosuitecross_services')) + ->where($db->quoteName('id') . ' = ' . $serviceId); + + $db->setQuery($query); + $credentials = json_decode($db->loadResult() ?: '{}', true) ?: []; + + $credentials['access_token'] = $tokenData['access_token']; + $credentials['mode'] = 'custom'; + + if (!empty($tokenData['refresh_token'])) { + $credentials['refresh_token'] = $tokenData['refresh_token']; + } + + if (!empty($tokenData['expires_in'])) { + $credentials['token_expires'] = time() + (int) $tokenData['expires_in']; + } + + $query = $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_services')) + ->set($db->quoteName('credentials') . ' = ' . $db->quote(CredentialHelper::encrypt($credentials))) + ->set($db->quoteName('modified') . ' = ' . $db->quote(Factory::getDate()->toSql())) + ->where($db->quoteName('id') . ' = ' . $serviceId); + + $db->setQuery($query); + $db->execute(); + + return true; + } + + /** + * Refresh an OAuth token if it has expired. + * + * Checks `token_expires` in the credentials array. If the token is expired + * and a refresh_token is available, performs the refresh grant and updates + * both the DB and the passed-in credentials array. + * + * @param int $serviceId Service record ID + * @param array &$credentials Credentials array (updated by reference on refresh) + * + * @return bool True if token was refreshed, false otherwise + */ + public static function refreshTokenIfNeeded(int $serviceId, array &$credentials): bool + { + // No expiry set — nothing to refresh + if (empty($credentials['token_expires'])) { + return false; + } + + // Token not yet expired + if ((int) $credentials['token_expires'] >= time()) { + return false; + } + + // Expired but no refresh token available + if (empty($credentials['refresh_token'])) { + return false; + } + + // Look up the service type from DB + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select($db->quoteName('service_type')) + ->from($db->quoteName('#__mokosuitecross_services')) + ->where($db->quoteName('id') . ' = ' . $serviceId); + $db->setQuery($query); + $serviceType = $db->loadResult(); + + if (!$serviceType) { + return false; + } + + // Get OAuth config for this service type + $config = self::OAUTH_CONFIGS[$serviceType] ?? null; + + if (!$config || empty($config['token_url'])) { + return false; + } + + // POST refresh token grant + $postData = [ + 'grant_type' => 'refresh_token', + 'refresh_token' => $credentials['refresh_token'], + ]; + + // Include client credentials if available + if (!empty($credentials['client_id'])) { + $postData['client_id'] = $credentials['client_id']; + } + + if (!empty($credentials['client_secret'])) { + $postData['client_secret'] = $credentials['client_secret']; + } + + $ch = curl_init($config['token_url']); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query($postData), + CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded', 'Accept: application/json'], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $data = json_decode($response, true) ?: []; + + if ($httpCode >= 200 && $httpCode < 300 && !empty($data['access_token'])) { + // Store updated token in DB + self::storeToken($serviceId, $data); + + // Update credentials by reference + $credentials['access_token'] = $data['access_token']; + + if (!empty($data['refresh_token'])) { + $credentials['refresh_token'] = $data['refresh_token']; + } + + if (!empty($data['expires_in'])) { + $credentials['token_expires'] = time() + (int) $data['expires_in']; + } + + return true; + } + + return false; + } + + /** + * Get the OAuth callback URL for this Joomla installation. + * + * @return string + */ + public static function getCallbackUrl(): string + { + return Uri::root() . 'administrator/index.php?option=com_mokosuitecross&task=oauth.callback'; + } +} diff --git a/source/packages/com_mokosuitecross/src/Helper/QueueProcessor.php b/source/packages/com_mokosuitecross/src/Helper/QueueProcessor.php new file mode 100644 index 0000000..c515ca4 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Helper/QueueProcessor.php @@ -0,0 +1,903 @@ + + * @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\Component\MokoSuiteCross\Administrator\Helper; + +defined('_JEXEC') or die; + +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\CMS\Uri\Uri; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\CredentialHelper; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; + +/** + * Shared queue processor used by: + * - System plugin onAfterRender (page-load processing) + * - Task scheduler plugin (Joomla scheduled task) + * + * Handles: queued posts, failed retries, scheduled posts, and log cleanup. + * Uses a simple DB-based lock to prevent concurrent execution. + */ +class QueueProcessor +{ + /** + * Process the post queue: dispatch queued posts, retry failed, fire scheduled. + * + * @param int $batchSize Max posts to process per run + * + * @return array ['processed' => int, 'succeeded' => int, 'failed' => int, 'skipped' => int] + */ + public static function processQueue(int $batchSize = 10): array + { + $result = ['processed' => 0, 'succeeded' => 0, 'failed' => 0, 'skipped' => 0]; + + if (!self::acquireLock()) { + $result['skipped'] = -1; + + return $result; + } + + try { + $db = Factory::getDbo(); + $componentParams = ComponentHelper::getParams('com_mokosuitecross'); + $maxRetry = (int) $componentParams->get('retry_max', 3); + $retryDelay = (int) $componentParams->get('retry_delay', 300); + $now = Factory::getDate()->toSql(); + + // Build service plugin map + $pluginMap = self::getServicePluginMap(); + + // 1. Process queued posts + $query = $db->getQuery(true) + ->select('p.*, s.service_type, s.credentials, s.params AS service_params') + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') + . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')) + ->where($db->quoteName('p.status') . ' = ' . $db->quote('queued')) + ->where('(' . $db->quoteName('p.scheduled_at') . ' IS NULL OR ' + . $db->quoteName('p.scheduled_at') . ' <= ' . $db->quote($now) . ')') + ->where($db->quoteName('s.published') . ' = 1') + ->order($db->quoteName('p.created') . ' ASC') + ->setLimit($batchSize); + + $db->setQuery($query); + $queuedPosts = $db->loadObjectList() ?: []; + + // 2. Process failed posts eligible for retry (exponential backoff) + // Retry 1 waits retryDelay, retry 2 waits retryDelay*2, retry 3 waits retryDelay*4, etc. + $query = $db->getQuery(true) + ->select('p.*, s.service_type, s.credentials, s.params AS service_params') + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') + . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')) + ->where($db->quoteName('p.status') . ' = ' . $db->quote('failed')) + ->where($db->quoteName('p.retry_count') . ' < ' . $maxRetry) + ->where($db->quoteName('p.modified') . ' <= DATE_SUB(NOW(), INTERVAL (' + . (int) $retryDelay . ' * POW(2, ' . $db->quoteName('p.retry_count') . ')) SECOND)') + ->where($db->quoteName('s.published') . ' = 1') + ->order($db->quoteName('p.modified') . ' ASC') + ->setLimit($batchSize); + + $db->setQuery($query); + $retryPosts = $db->loadObjectList() ?: []; + + $allPosts = array_merge($queuedPosts, $retryPosts); + + foreach ($allPosts as $post) { + $result['processed']++; + + $plugin = $pluginMap[$post->service_type] ?? null; + + if (!$plugin) { + $result['skipped']++; + continue; + } + + $isRetry = ($post->status === 'failed'); + + if ($isRetry) { + $newRetryCount = (int) $post->retry_count + 1; + + // If this is the last retry attempt, mark permanently failed on failure + if ($newRetryCount >= $maxRetry) { + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('status') . ' = ' . $db->quote('permanently_failed')) + ->set($db->quoteName('retry_count') . ' = ' . $newRetryCount) + ->set($db->quoteName('error_message') . ' = CONCAT(' . $db->quoteName('error_message') . ', ' . $db->quote(' [max retries exceeded]') . ')') + ->set($db->quoteName('modified') . ' = ' . $db->quote(Factory::getDate()->toSql())) + ->where($db->quoteName('id') . ' = ' . (int) $post->id) + ); + $db->execute(); + + self::log($db, (int) $post->id, (int) $post->service_id, 'error', + sprintf('Permanently failed %s: max retries (%d) exceeded', $post->service_type, $maxRetry)); + + $result['failed']++; + continue; + } + + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('retry_count') . ' = ' . $newRetryCount) + ->where($db->quoteName('id') . ' = ' . (int) $post->id) + ); + $db->execute(); + } + + // Mark as posting + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('status') . ' = ' . $db->quote('posting')) + ->set($db->quoteName('modified') . ' = ' . $db->quote(Factory::getDate()->toSql())) + ->where($db->quoteName('id') . ' = ' . (int) $post->id) + ); + $db->execute(); + + $credentials = CredentialHelper::decrypt($post->credentials ?: ''); + $params = json_decode($post->service_params ?: '{}', true) ?: []; + + // Token auto-refresh before posting + OAuthHelper::refreshTokenIfNeeded((int) $post->service_id, $credentials); + + // Extract intro image for media attachment + $media = []; + + if (!empty($post->article_id)) { + $imgQuery = $db->getQuery(true) + ->select($db->quoteName('images')) + ->from($db->quoteName('#__content')) + ->where($db->quoteName('id') . ' = ' . (int) $post->article_id); + $db->setQuery($imgQuery); + $imgJson = $db->loadResult(); + + if ($imgJson) { + $imgData = json_decode($imgJson); + + if (!empty($imgData->image_intro)) { + $media[] = Uri::root() . ltrim($imgData->image_intro, '/'); + } + } + } + + // Lifecycle event: before post + $cancel = false; + $message = $post->message; + + try { + $dispatcher = Factory::getApplication()->getDispatcher(); + $beforeEvent = new \Joomla\Event\Event('onMokoSuiteCrossBeforePost', [(int) $post->id, &$message, $post->service_type, &$cancel]); + $dispatcher->dispatch('onMokoSuiteCrossBeforePost', $beforeEvent); + } catch (\Throwable $e) { + // Dispatcher may not be available + } + + if ($cancel) { + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('status') . ' = ' . $db->quote('cancelled')) + ->set($db->quoteName('modified') . ' = ' . $db->quote(Factory::getDate()->toSql())) + ->where($db->quoteName('id') . ' = ' . (int) $post->id) + ); + $db->execute(); + + self::log($db, (int) $post->id, (int) $post->service_id, 'info', + sprintf('Post to %s cancelled by onMokoSuiteCrossBeforePost event', $post->service_type)); + + $result['skipped']++; + continue; + } + + try { + $apiResult = $plugin->publish($message, $media, $credentials, $params); + + if (!empty($apiResult['success'])) { + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('status') . ' = ' . $db->quote('posted')) + ->set($db->quoteName('platform_post_id') . ' = ' . $db->quote($apiResult['platform_post_id'] ?? '')) + ->set($db->quoteName('platform_response') . ' = ' . $db->quote(json_encode($apiResult['response'] ?? []))) + ->set($db->quoteName('posted_at') . ' = ' . $db->quote(Factory::getDate()->toSql())) + ->set($db->quoteName('modified') . ' = ' . $db->quote(Factory::getDate()->toSql())) + ->where($db->quoteName('id') . ' = ' . (int) $post->id) + ); + $db->execute(); + + self::log($db, (int) $post->id, (int) $post->service_id, 'info', + sprintf('%s to %s (ID: %s)', $isRetry ? 'Retry succeeded' : 'Posted', $post->service_type, $apiResult['platform_post_id'] ?? 'n/a')); + + // Lifecycle event: after successful post + try { + $afterEvent = new \Joomla\Event\Event('onMokoSuiteCrossAfterPost', [(int) $post->id, $post->service_type, $apiResult]); + $dispatcher->dispatch('onMokoSuiteCrossAfterPost', $afterEvent); + } catch (\Throwable $e) { + // Non-critical + } + + $result['succeeded']++; + } else { + $errorMsg = $apiResult['response']['error'] ?? json_encode($apiResult['response'] ?? []); + + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('status') . ' = ' . $db->quote('failed')) + ->set($db->quoteName('error_message') . ' = ' . $db->quote(mb_substr($errorMsg, 0, 1000))) + ->set($db->quoteName('platform_response') . ' = ' . $db->quote(json_encode($apiResult['response'] ?? []))) + ->set($db->quoteName('modified') . ' = ' . $db->quote(Factory::getDate()->toSql())) + ->where($db->quoteName('id') . ' = ' . (int) $post->id) + ); + $db->execute(); + + self::log($db, (int) $post->id, (int) $post->service_id, 'error', + sprintf('Failed %s: %s', $post->service_type, mb_substr($errorMsg, 0, 500))); + + // Lifecycle event: post failed + try { + $failedEvent = new \Joomla\Event\Event('onMokoSuiteCrossPostFailed', [(int) $post->id, $post->service_type, $errorMsg]); + $dispatcher->dispatch('onMokoSuiteCrossPostFailed', $failedEvent); + } catch (\Throwable $e) { + // Non-critical + } + + $result['failed']++; + } + } catch (\Throwable $e) { + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('status') . ' = ' . $db->quote('failed')) + ->set($db->quoteName('error_message') . ' = ' . $db->quote(mb_substr($e->getMessage(), 0, 1000))) + ->set($db->quoteName('modified') . ' = ' . $db->quote(Factory::getDate()->toSql())) + ->where($db->quoteName('id') . ' = ' . (int) $post->id) + ); + $db->execute(); + + self::log($db, (int) $post->id, (int) $post->service_id, 'error', + sprintf('Exception %s: %s', $post->service_type, mb_substr($e->getMessage(), 0, 500))); + + // Lifecycle event: post failed (exception) + try { + $failedEvent = new \Joomla\Event\Event('onMokoSuiteCrossPostFailed', [(int) $post->id, $post->service_type, $e->getMessage()]); + $dispatcher->dispatch('onMokoSuiteCrossPostFailed', $failedEvent); + } catch (\Throwable $ex) { + // Non-critical + } + + $result['failed']++; + } + } + + // 3. Clean up old logs + self::cleanupLogs($db, $componentParams); + + } finally { + self::releaseLock(); + } + + return $result; + } + + /** + * Process evergreen re-shares: find articles marked as evergreen whose last + * successful post to each service was longer ago than the configured interval, + * and create new queue entries for them. + * + * @return array ['queued' => int] + */ + public static function processEvergreen(): array + { + $result = ['queued' => 0]; + + $componentParams = ComponentHelper::getParams('com_mokosuitecross'); + + if (!$componentParams->get('evergreen_enabled', 1)) { + return $result; + } + + $defaultInterval = (int) $componentParams->get('evergreen_default_interval', 30); + $maxPerRun = (int) $componentParams->get('evergreen_max_per_run', 3); + + $db = Factory::getDbo(); + $now = Factory::getDate()->toSql(); + + // Find published articles with evergreen=1 in attribs + $query = $db->getQuery(true) + ->select('c.id, c.attribs') + ->from($db->quoteName('#__content', 'c')) + ->where($db->quoteName('c.state') . ' = 1') + ->where('JSON_EXTRACT(' . $db->quoteName('c.attribs') . ', ' . $db->quote('$.mokosuitecross_evergreen') . ') = ' . $db->quote('1')); + + $db->setQuery($query); + $articles = $db->loadObjectList() ?: []; + + if (empty($articles)) { + return $result; + } + + // Load all published services + $query = $db->getQuery(true) + ->select('id, service_type') + ->from($db->quoteName('#__mokosuitecross_services')) + ->where($db->quoteName('published') . ' = 1'); + + $db->setQuery($query); + $services = $db->loadObjectList() ?: []; + + if (empty($services)) { + return $result; + } + + // Import service plugins (not used for direct dispatch here, but ensures + // they are loaded in case any lifecycle events depend on them) + PluginHelper::importPlugin('mokosuitecross'); + + // Batch pre-load: latest posted_at per article+service (eliminates N*M queries) + $articleIds = implode(',', array_map(function ($a) { return (int) $a->id; }, $articles)); + $serviceIds = implode(',', array_map(function ($s) { return (int) $s->id; }, $services)); + + $query = $db->getQuery(true) + ->select(['article_id', 'service_id', 'MAX(' . $db->quoteName('posted_at') . ') AS last_posted']) + ->from($db->quoteName('#__mokosuitecross_posts')) + ->where($db->quoteName('article_id') . ' IN (' . $articleIds . ')') + ->where($db->quoteName('service_id') . ' IN (' . $serviceIds . ')') + ->where($db->quoteName('status') . ' = ' . $db->quote('posted')) + ->group(['article_id', 'service_id']); + $db->setQuery($query); + $lastPostedRows = $db->loadObjectList() ?: []; + + $lastPostedMap = []; + foreach ($lastPostedRows as $row) { + $lastPostedMap[$row->article_id . ':' . $row->service_id] = $row->last_posted; + } + + // Batch pre-load: existing queued/posting entries + $query = $db->getQuery(true) + ->select(['article_id', 'service_id']) + ->from($db->quoteName('#__mokosuitecross_posts')) + ->where($db->quoteName('article_id') . ' IN (' . $articleIds . ')') + ->where($db->quoteName('service_id') . ' IN (' . $serviceIds . ')') + ->where($db->quoteName('status') . ' IN (' . $db->quote('queued') . ',' . $db->quote('posting') . ')'); + $db->setQuery($query); + $pendingRows = $db->loadObjectList() ?: []; + + $pendingSet = []; + foreach ($pendingRows as $row) { + $pendingSet[$row->article_id . ':' . $row->service_id] = true; + } + + foreach ($articles as $article) { + if ($result['queued'] >= $maxPerRun) { + break; + } + + $attribs = json_decode($article->attribs ?? '{}', true) ?: []; + $interval = (int) ($attribs['mokosuitecross_evergreen_interval'] ?? $defaultInterval); + + if ($interval < 1) { + $interval = $defaultInterval; + } + + // Per-article service filter + $selectedServiceIds = $attribs['mokosuitecross_services'] ?? null; + + if (is_array($selectedServiceIds) && !empty($selectedServiceIds)) { + $selectedServiceIds = array_map('intval', $selectedServiceIds); + } else { + $selectedServiceIds = null; + } + + // Load the full article for template rendering + $fullArticle = null; + + foreach ($services as $service) { + if ($result['queued'] >= $maxPerRun) { + break; + } + + // Per-article service filter + if ($selectedServiceIds !== null && !in_array((int) $service->id, $selectedServiceIds, true)) { + continue; + } + + $key = $article->id . ':' . $service->id; + + // Check last successful post from batch-loaded map + $lastPosted = $lastPostedMap[$key] ?? null; + + if (empty($lastPosted)) { + // Never posted — skip, the initial cross-post will handle it + continue; + } + + // Check if interval has elapsed + $dueDate = Factory::getDate($lastPosted . ' + ' . $interval . ' days'); + + if ($dueDate->toUnix() > Factory::getDate()->toUnix()) { + continue; + } + + // Skip if there's already a queued/posting entry + if (isset($pendingSet[$key])) { + continue; + } + + // Load full article if not already loaded + if ($fullArticle === null) { + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__content')) + ->where($db->quoteName('id') . ' = ' . (int) $article->id); + $db->setQuery($query); + $fullArticle = $db->loadObject(); + + if (!$fullArticle) { + break; + } + } + + // Render message using default template + $template = $componentParams->get('default_template', "{title}\n\n{url}"); + $message = self::renderEvergreenMessage($db, $fullArticle, $template); + + // Create queue entry + $post = (object) [ + 'article_id' => (int) $article->id, + 'service_id' => (int) $service->id, + 'status' => 'queued', + 'message' => $message, + 'platform_post_id' => '', + 'platform_response' => '', + 'error_message' => '', + 'retry_count' => 0, + 'created' => $now, + 'modified' => $now, + ]; + + $db->insertObject('#__mokosuitecross_posts', $post); + + self::log($db, $db->insertid(), (int) $service->id, 'info', + sprintf('Evergreen re-share queued for article %d to %s (interval: %d days)', + $article->id, $service->service_type, $interval)); + + $result['queued']++; + } + } + + return $result; + } + + /** + * Render a message for an evergreen re-share using the default template. + */ + private static function renderEvergreenMessage($db, object $article, string $template): string + { + $url = \Joomla\CMS\Uri\Uri::root() . 'index.php?option=com_content&view=article&id=' . $article->id; + + if (!empty($article->catid)) { + $url .= '&catid=' . $article->catid; + } + + $categoryName = ''; + + if (!empty($article->catid)) { + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('id') . ' = ' . (int) $article->catid); + $db->setQuery($query); + $categoryName = $db->loadResult() ?: ''; + } + + $authorName = ''; + + if (!empty($article->created_by)) { + $query = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('id') . ' = ' . (int) $article->created_by); + $db->setQuery($query); + $authorName = $db->loadResult() ?: ''; + } + + $introImage = ''; + $images = json_decode($article->images ?? '{}'); + + if (!empty($images->image_intro)) { + $introImage = \Joomla\CMS\Uri\Uri::root() . ltrim($images->image_intro, '/'); + } + + // Resolve article tags + $tagNames = []; + + if (!empty($article->id)) { + $query = $db->getQuery(true) + ->select($db->quoteName('t.title')) + ->from($db->quoteName('#__tags', 't')) + ->join('INNER', $db->quoteName('#__contentitem_tag_map', 'm') + . ' ON ' . $db->quoteName('m.tag_id') . ' = ' . $db->quoteName('t.id')) + ->where($db->quoteName('m.type_alias') . ' = ' . $db->quote('com_content.article')) + ->where($db->quoteName('m.content_item_id') . ' = ' . (int) $article->id) + ->where($db->quoteName('t.published') . ' = 1'); + $db->setQuery($query); + $tagNames = $db->loadColumn() ?: []; + } + + $tagsComma = implode(', ', $tagNames); + $hashtags = implode(' ', array_map(function ($tag) { + return '#' . preg_replace('/\s+/', '', $tag); + }, $tagNames)); + + $replacements = [ + '{title}' => $article->title ?? '', + '{introtext}' => strip_tags(mb_substr($article->introtext ?? '', 0, 280)), + '{fulltext}' => strip_tags(mb_substr($article->fulltext ?? '', 0, 500)), + '{url}' => $url, + '{image}' => $introImage, + '{category}' => $categoryName, + '{author}' => $authorName, + '{date}' => Factory::getDate($article->publish_up ?? 'now')->format('Y-m-d'), + '{tags}' => $tagsComma, + '{hashtags}' => $hashtags, + ]; + + $message = str_replace(array_keys($replacements), array_values($replacements), $template); + + // Resolve custom field placeholders: {field:field_name} + $message = preg_replace_callback('/\{field:([a-zA-Z0-9_-]+)\}/', function ($matches) use ($db, $article) { + $fieldName = $matches[1]; + $query = $db->getQuery(true) + ->select('fv.value') + ->from($db->quoteName('#__fields_values', 'fv')) + ->join('INNER', $db->quoteName('#__fields', 'f') . ' ON f.id = fv.field_id') + ->where('f.name = ' . $db->quote($fieldName)) + ->where('fv.item_id = ' . (int) $article->id); + $db->setQuery($query); + return $db->loadResult() ?: ''; + }, $message); + + return $message; + } + + /** + * Manually retry one or more failed/permanently_failed posts. + * + * Resets status to 'queued' and retry_count to 0 so the queue processor + * picks them up on the next run. + * + * @param array $postIds Post IDs to retry + * + * @return int Number of posts re-queued + */ + public static function retryPosts(array $postIds): int + { + if (empty($postIds)) { + return 0; + } + + $db = Factory::getDbo(); + $now = Factory::getDate()->toSql(); + $ids = implode(',', array_map('intval', $postIds)); + + $query = $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('status') . ' = ' . $db->quote('queued')) + ->set($db->quoteName('retry_count') . ' = 0') + ->set($db->quoteName('error_message') . ' = ' . $db->quote('')) + ->set($db->quoteName('modified') . ' = ' . $db->quote($now)) + ->where($db->quoteName('id') . ' IN (' . $ids . ')') + ->where($db->quoteName('status') . ' IN (' . $db->quote('failed') . ',' . $db->quote('permanently_failed') . ')'); + + $db->setQuery($query); + $db->execute(); + $count = $db->getAffectedRows(); + + if ($count > 0) { + self::log($db, null, null, 'info', sprintf('Manual retry: %d post(s) re-queued', $count)); + } + + return $count; + } + + /** + * Retry all failed posts for a specific service. + * + * @param int $serviceId Service ID + * + * @return int Number of posts re-queued + */ + public static function retryService(int $serviceId): int + { + $db = Factory::getDbo(); + $now = Factory::getDate()->toSql(); + + $query = $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('status') . ' = ' . $db->quote('queued')) + ->set($db->quoteName('retry_count') . ' = 0') + ->set($db->quoteName('error_message') . ' = ' . $db->quote('')) + ->set($db->quoteName('modified') . ' = ' . $db->quote($now)) + ->where($db->quoteName('service_id') . ' = ' . $serviceId) + ->where($db->quoteName('status') . ' IN (' . $db->quote('failed') . ',' . $db->quote('permanently_failed') . ')'); + + $db->setQuery($query); + $db->execute(); + $count = $db->getAffectedRows(); + + if ($count > 0) { + self::log($db, null, $serviceId, 'info', sprintf('Bulk retry: %d post(s) re-queued for service %d', $count, $serviceId)); + } + + return $count; + } + + /** + * Check if there are pending items in the queue. + * + * @return bool + */ + public static function hasPendingWork(): bool + { + $db = Factory::getDbo(); + + $componentParams = ComponentHelper::getParams('com_mokosuitecross'); + $maxRetry = (int) $componentParams->get('retry_max', 3); + $retryDelay = (int) $componentParams->get('retry_delay', 300); + $now = Factory::getDate()->toSql(); + + // Queued posts ready to go + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuitecross_posts')) + ->where($db->quoteName('status') . ' = ' . $db->quote('queued')) + ->where('(' . $db->quoteName('scheduled_at') . ' IS NULL OR ' + . $db->quoteName('scheduled_at') . ' <= ' . $db->quote($now) . ')'); + $db->setQuery($query); + $queued = (int) $db->loadResult(); + + // Failed posts eligible for retry (exponential backoff matching processQueue) + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuitecross_posts')) + ->where($db->quoteName('status') . ' = ' . $db->quote('failed')) + ->where($db->quoteName('retry_count') . ' < ' . $maxRetry) + ->where($db->quoteName('modified') . ' <= DATE_SUB(NOW(), INTERVAL (' + . (int) $retryDelay . ' * POW(2, ' . $db->quoteName('retry_count') . ')) SECOND)'); + $db->setQuery($query); + $retryable = (int) $db->loadResult(); + + return ($queued + $retryable) > 0; + } + + /** + * Import mokosuitecross plugins and build a type → plugin instance map. + * + * @return array + */ + private static function getServicePluginMap(): array + { + PluginHelper::importPlugin('mokosuitecross'); + + // In Joomla 5+ with SubscriberInterface, plugins receive the Event object + // as their first argument. When they do $services[] = $this, they append to + // the Event via ArrayAccess at numeric indices starting at 1. + $servicePlugins = []; + $event = new \Joomla\Event\Event('onMokoSuiteCrossGetServices', [$servicePlugins]); + + try { + Factory::getApplication()->getDispatcher()->dispatch( + 'onMokoSuiteCrossGetServices', + $event + ); + } catch (\Throwable $e) { + // Dispatcher may not be available in all contexts + } + + // Read plugins back from the Event's ArrayAccess indices + $idx = 1; + + while (isset($event[$idx])) { + $servicePlugins[] = $event[$idx]; + $idx++; + } + + $map = []; + + foreach ($servicePlugins as $plugin) { + if ($plugin instanceof MokoSuiteCrossServiceInterface) { + $map[$plugin->getServiceType()] = $plugin; + } + } + + return $map; + } + + /** + * Delete logs older than the configured retention period. + */ + private static function cleanupLogs($db, $componentParams): void + { + $retentionDays = (int) $componentParams->get('log_retention_days', 90); + + if ($retentionDays <= 0) { + return; + } + + $cutoff = Factory::getDate('now - ' . $retentionDays . ' days')->toSql(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__mokosuitecross_logs')) + ->where($db->quoteName('created') . ' < ' . $db->quote($cutoff)); + + $db->setQuery($query); + $db->execute(); + } + + /** + * Acquire a database lock to prevent concurrent queue processing. + * + * Uses MySQL GET_LOCK() or PostgreSQL pg_advisory_lock() when available, + * falling back to a timestamp-based check for other databases. + */ + private static function acquireLock(): bool + { + $db = Factory::getDbo(); + + try { + $serverType = $db->getServerType(); + + if ($serverType === 'mysql' || $serverType === 'mariadb') { + $db->setQuery("SELECT GET_LOCK('mokosuitecross_queue', 0)"); + + return (int) $db->loadResult() === 1; + } + + if ($serverType === 'postgresql') { + $db->setQuery("SELECT pg_try_advisory_lock(hashtext('mokosuitecross_queue'))"); + + return (bool) $db->loadResult(); + } + } catch (\Throwable $e) { + // Fall through to timestamp-based lock + } + + return self::acquireTimestampLock($db); + } + + /** + * Release the database lock. + */ + private static function releaseLock(): void + { + $db = Factory::getDbo(); + + try { + $serverType = $db->getServerType(); + + if ($serverType === 'mysql' || $serverType === 'mariadb') { + $db->setQuery("SELECT RELEASE_LOCK('mokosuitecross_queue')"); + $db->execute(); + + return; + } + + if ($serverType === 'postgresql') { + $db->setQuery("SELECT pg_advisory_unlock(hashtext('mokosuitecross_queue'))"); + $db->execute(); + + return; + } + } catch (\Throwable $e) { + // Fall through to timestamp-based release + } + + self::releaseTimestampLock($db); + } + + /** + * Timestamp-based lock fallback for databases without advisory locks. + * + * Uses an atomic UPDATE with a WHERE clause to prevent TOCTOU race + * conditions. The lock is considered stale after 120 seconds. + */ + private static function acquireTimestampLock($db): bool + { + $now = time(); + $staleThreshold = $now - 120; + + // Atomic: only succeeds if lock is absent (0) or stale + $params = ComponentHelper::getParams('com_mokosuitecross'); + $oldParams = $params->toString(); + $params->set('queue_lock_time', $now); + $newParams = $params->toString(); + + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote($newParams)) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuitecross')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')) + ->where('(' . $db->quoteName('params') . ' NOT LIKE ' . $db->quote('%"queue_lock_time"%') + . ' OR ' . $db->quoteName('params') . ' LIKE ' . $db->quote('%"queue_lock_time":0%') + . ' OR ' . $db->quoteName('params') . ' LIKE ' . $db->quote('%"queue_lock_time":"0"%') + . ')'); + + $db->setQuery($query); + $db->execute(); + + if ($db->getAffectedRows() > 0) { + return true; + } + + // Check if the existing lock is stale + $params = ComponentHelper::getParams('com_mokosuitecross'); + $lockTime = (int) $params->get('queue_lock_time', 0); + + if ($lockTime > 0 && $lockTime <= $staleThreshold) { + // Force acquire stale lock + $params->set('queue_lock_time', $now); + + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote($params->toString())) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuitecross')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')); + + $db->setQuery($query); + $db->execute(); + + return true; + } + + return false; + } + + /** + * Release the timestamp-based lock. + */ + private static function releaseTimestampLock($db): void + { + $params = ComponentHelper::getParams('com_mokosuitecross'); + $params->set('queue_lock_time', 0); + + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote($params->toString())) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuitecross')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')); + + $db->setQuery($query); + $db->execute(); + } + + /** + * Write a log entry. + */ + private static function log($db, ?int $postId, ?int $serviceId, string $level, string $message): void + { + $log = (object) [ + 'post_id' => $postId, + 'service_id' => $serviceId, + 'level' => $level, + 'message' => mb_substr($message, 0, 2000), + 'context' => '{}', + 'created' => Factory::getDate()->toSql(), + ]; + + $db->insertObject('#__mokosuitecross_logs', $log); + } +} diff --git a/source/packages/com_mokosuitecross/src/Helper/ServiceIconHelper.php b/source/packages/com_mokosuitecross/src/Helper/ServiceIconHelper.php new file mode 100644 index 0000000..55015b9 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Helper/ServiceIconHelper.php @@ -0,0 +1,96 @@ + + * @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\Component\MokoSuiteCross\Administrator\Helper; + +defined('_JEXEC') or die; + +/** + * Static helper that maps service types to Joomla Bootstrap icons. + */ +class ServiceIconHelper +{ + /** + * Map of service type identifiers to icon CSS classes. + * + * @var array + */ + private const ICONS = [ + // Social + 'facebook' => 'icon-facebook', + 'twitter' => 'icon-twitter', + 'linkedin' => 'icon-linkedin', + 'mastodon' => 'icon-globe', + 'bluesky' => 'icon-cloud', + 'threads' => 'icon-comments', + 'pinterest' => 'icon-thumbtack', + 'reddit' => 'icon-comments-alt', + 'tumblr' => 'icon-pencil-alt', + 'tiktok' => 'icon-play-circle', + 'nostr' => 'icon-key', + 'activitypub' => 'icon-network-wired', + // Chat + 'telegram' => 'icon-paper-plane', + 'discord' => 'icon-headset', + 'slack' => 'icon-hashtag', + 'teams' => 'icon-users', + 'googlechat' => 'icon-comment', + 'whatsapp' => 'icon-mobile', + 'matrix' => 'icon-th', + 'ntfy' => 'icon-bell', + // Email + 'mailchimp' => 'icon-envelope', + 'sendgrid' => 'icon-envelope-open', + 'brevo' => 'icon-at', + 'convertkit' => 'icon-mail-bulk', + 'constantcontact' => 'icon-address-book', + // Publishing + 'medium' => 'icon-book', + 'wordpress' => 'icon-blog', + 'devto' => 'icon-code', + 'ghost' => 'icon-ghost', + 'hashnode' => 'icon-newspaper', + 'blogger' => 'icon-rss', + // Business + 'googlebusiness' => 'icon-store', + // Universal + 'webhook' => 'icon-plug', + 'rssfeed' => 'icon-rss-square', + ]; + + /** + * Get the icon CSS class for a service type. + * + * @param string $serviceType The service type identifier + * + * @return string Icon CSS class + */ + public static function getIcon(string $serviceType): string + { + return self::ICONS[$serviceType] ?? 'icon-share-alt'; + } + + /** + * Render an icon span element for a service type. + * + * @param string $serviceType The service type identifier + * @param string $extraClass Additional CSS classes to append + * + * @return string HTML span element + */ + public static function renderIcon(string $serviceType, string $extraClass = ''): string + { + $icon = self::getIcon($serviceType); + $class = trim($icon . ' ' . htmlspecialchars($extraClass, ENT_QUOTES, 'UTF-8')); + + return ''; + } +} diff --git a/src/packages/com_mokojoomcross/src/Helper/index.html b/source/packages/com_mokosuitecross/src/Helper/index.html similarity index 100% rename from src/packages/com_mokojoomcross/src/Helper/index.html rename to source/packages/com_mokosuitecross/src/Helper/index.html diff --git a/source/packages/com_mokosuitecross/src/Model/DashboardModel.php b/source/packages/com_mokosuitecross/src/Model/DashboardModel.php new file mode 100644 index 0000000..98f968e --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Model/DashboardModel.php @@ -0,0 +1,196 @@ + + * @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\Component\MokoSuiteCross\Administrator\Model; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\Model\BaseDatabaseModel; + +class DashboardModel extends BaseDatabaseModel +{ + /** + * Get summary statistics for the dashboard. + * + * @return object Stats object with counts + */ + public function getStats(): object + { + $db = $this->getDatabase(); + + $stats = new \stdClass(); + + // Active services count + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuitecross_services')) + ->where($db->quoteName('published') . ' = 1'); + $db->setQuery($query); + $stats->active_services = (int) $db->loadResult(); + + // Posts by status + foreach (['queued', 'posted', 'failed'] as $status) { + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuitecross_posts')) + ->where($db->quoteName('status') . ' = ' . $db->quote($status)); + $db->setQuery($query); + $stats->{$status . '_count'} = (int) $db->loadResult(); + } + + return $stats; + } + + /** + * Check if Perfect Publisher Pro migration is available. + * + * @return bool + */ + public function isMigrationAvailable(): bool + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select($db->quoteName('params')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuitecross')); + + $db->setQuery($query); + $params = json_decode($db->loadResult() ?: '{}', true); + + return !empty($params['migration_available']); + } + + /** + * Get recent activity log entries. + * + * @param int $limit Number of entries to return + * + * @return array + */ + public function getRecentActivity(int $limit = 10): array + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('l.*, s.title AS service_title, s.service_type') + ->from($db->quoteName('#__mokosuitecross_logs', 'l')) + ->join('LEFT', $db->quoteName('#__mokosuitecross_services', 's') + . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('l.service_id')) + ->order($db->quoteName('l.created') . ' DESC'); + + $db->setQuery($query, 0, $limit); + + return $db->loadObjectList() ?: []; + } + + /** + * Get posts-per-service breakdown for the analytics chart. + * + * @param string|null $since Only count posts created on or after this datetime + * + * @return array [['service_type' => '...', 'posted' => N, 'failed' => N, 'queued' => N], ...] + */ + public function getServiceBreakdown(?string $since = null): array + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select([ + $db->quoteName('s.id', 'service_id'), + $db->quoteName('s.service_type'), + $db->quoteName('s.title', 'service_title'), + 'SUM(CASE WHEN ' . $db->quoteName('p.status') . ' = ' . $db->quote('posted') . ' THEN 1 ELSE 0 END) AS posted', + 'SUM(CASE WHEN ' . $db->quoteName('p.status') . ' = ' . $db->quote('failed') . ' THEN 1 ELSE 0 END) AS failed', + 'SUM(CASE WHEN ' . $db->quoteName('p.status') . ' = ' . $db->quote('queued') . ' THEN 1 ELSE 0 END) AS queued', + 'COUNT(*) AS total', + ]) + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') + . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')) + ->group($db->quoteName(['s.id', 's.service_type', 's.title'])) + ->order('total DESC'); + + if ($since !== null) { + $query->where($db->quoteName('p.created') . ' >= ' . $db->quote($since)); + } + + $db->setQuery($query); + + return $db->loadAssocList() ?: []; + } + + /** + * Get posts-per-day for the last N days (for trend chart). + * + * @param int $days Number of days to look back + * + * @return array [['day' => '2026-05-28', 'posted' => N, 'failed' => N], ...] + */ + public function getDailyTrend(int $days = 14): array + { + $db = $this->getDatabase(); + + $cutoff = Factory::getDate('now - ' . $days . ' days')->format('Y-m-d'); + + $query = $db->getQuery(true) + ->select([ + 'DATE(' . $db->quoteName('created') . ') AS day', + 'SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('posted') . ' THEN 1 ELSE 0 END) AS posted', + 'SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('failed') . ' THEN 1 ELSE 0 END) AS failed', + 'COUNT(*) AS total', + ]) + ->from($db->quoteName('#__mokosuitecross_posts')) + ->where('DATE(' . $db->quoteName('created') . ') >= ' . $db->quote($cutoff)) + ->group('DATE(' . $db->quoteName('created') . ')') + ->order('day ASC'); + + $db->setQuery($query); + + return $db->loadAssocList() ?: []; + } + + /** + * Get most cross-posted articles. + * + * @param int $limit Number of articles + * @param string|null $since Only count posts created on or after this datetime + * + * @return array + */ + public function getTopArticles(int $limit = 5, ?string $since = null): array + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select([ + $db->quoteName('c.id'), + $db->quoteName('c.title'), + 'COUNT(*) AS post_count', + 'SUM(CASE WHEN ' . $db->quoteName('p.status') . ' = ' . $db->quote('posted') . ' THEN 1 ELSE 0 END) AS success_count', + ]) + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->join('INNER', $db->quoteName('#__content', 'c') + . ' ON ' . $db->quoteName('c.id') . ' = ' . $db->quoteName('p.article_id')) + ->group($db->quoteName(['c.id', 'c.title'])) + ->order('post_count DESC'); + + if ($since !== null) { + $query->where($db->quoteName('p.created') . ' >= ' . $db->quote($since)); + } + + $db->setQuery($query, 0, $limit); + + return $db->loadAssocList() ?: []; + } +} diff --git a/src/packages/com_mokojoomcross/src/Model/LogsModel.php b/source/packages/com_mokosuitecross/src/Model/LogsModel.php similarity index 84% rename from src/packages/com_mokojoomcross/src/Model/LogsModel.php rename to source/packages/com_mokosuitecross/src/Model/LogsModel.php index 6c31682..f47b007 100644 --- a/src/packages/com_mokojoomcross/src/Model/LogsModel.php +++ b/source/packages/com_mokosuitecross/src/Model/LogsModel.php @@ -1,15 +1,15 @@ * @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\Component\MokoJoomCross\Administrator\Model; +namespace Joomla\Component\MokoSuiteCross\Administrator\Model; defined('_JEXEC') or die; @@ -38,8 +38,8 @@ class LogsModel extends ListModel $query->select('a.*') ->select($db->quoteName('s.title', 'service_title')) - ->from($db->quoteName('#__mokojoomcross_logs', 'a')) - ->join('LEFT', $db->quoteName('#__mokojoomcross_services', 's') + ->from($db->quoteName('#__mokosuitecross_logs', 'a')) + ->join('LEFT', $db->quoteName('#__mokosuitecross_services', 's') . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('a.service_id')); $level = $this->getState('filter.level'); diff --git a/source/packages/com_mokosuitecross/src/Model/PostModel.php b/source/packages/com_mokosuitecross/src/Model/PostModel.php new file mode 100644 index 0000000..5c0e769 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Model/PostModel.php @@ -0,0 +1,93 @@ + + * @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\Component\MokoSuiteCross\Administrator\Model; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\Model\AdminModel; + +class PostModel extends AdminModel +{ + public function getForm($data = [], $loadData = true) + { + $form = $this->loadForm( + 'com_mokosuitecross.post', + 'post', + ['control' => 'jform', 'load_data' => $loadData] + ); + + if (empty($form)) { + return false; + } + + // Lock article_id and service_id on existing records + $id = $this->getState('post.id', 0); + + if ($id > 0) { + $form->setFieldAttribute('article_id', 'readonly', 'true'); + $form->setFieldAttribute('service_id', 'readonly', 'true'); + } + + return $form; + } + + protected function loadFormData() + { + return $this->getItem(); + } + + /** + * Prepare and sanitise the table prior to saving. + */ + protected function prepareTable($table) + { + $now = Factory::getDate()->toSql(); + + // Validate scheduled_at datetime format + if (!empty($table->scheduled_at)) { + try { + $date = Factory::getDate($table->scheduled_at); + $table->scheduled_at = $date->toSql(); + } catch (\Throwable $e) { + $table->scheduled_at = null; + } + } + + if (empty($table->id)) { + $table->created = $now; + $table->modified = $now; + + if (empty($table->status)) { + $table->status = empty($table->scheduled_at) ? 'queued' : 'scheduled'; + } + + if (empty($table->retry_count)) { + $table->retry_count = 0; + } + + if (empty($table->platform_post_id)) { + $table->platform_post_id = ''; + } + + if (empty($table->platform_response)) { + $table->platform_response = ''; + } + + if (empty($table->error_message)) { + $table->error_message = ''; + } + } else { + $table->modified = $now; + } + } +} diff --git a/src/packages/com_mokojoomcross/src/Model/PostsModel.php b/source/packages/com_mokosuitecross/src/Model/PostsModel.php similarity index 69% rename from src/packages/com_mokojoomcross/src/Model/PostsModel.php rename to source/packages/com_mokosuitecross/src/Model/PostsModel.php index 2260a41..3ac31c4 100644 --- a/src/packages/com_mokojoomcross/src/Model/PostsModel.php +++ b/source/packages/com_mokosuitecross/src/Model/PostsModel.php @@ -1,15 +1,15 @@ * @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\Component\MokoJoomCross\Administrator\Model; +namespace Joomla\Component\MokoSuiteCross\Administrator\Model; defined('_JEXEC') or die; @@ -52,10 +52,10 @@ class PostsModel extends ListModel ->select($db->quoteName('c.title', 'article_title')) ->select($db->quoteName('s.title', 'service_title')) ->select($db->quoteName('s.service_type')) - ->from($db->quoteName('#__mokojoomcross_posts', 'a')) + ->from($db->quoteName('#__mokosuitecross_posts', 'a')) ->join('LEFT', $db->quoteName('#__content', 'c') . ' ON ' . $db->quoteName('c.id') . ' = ' . $db->quoteName('a.article_id')) - ->join('LEFT', $db->quoteName('#__mokojoomcross_services', 's') + ->join('LEFT', $db->quoteName('#__mokosuitecross_services', 's') . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('a.service_id')); // Filter by status @@ -65,6 +65,22 @@ class PostsModel extends ListModel $query->where($db->quoteName('a.status') . ' = ' . $db->quote($status)); } + // Filter by service + $serviceId = $this->getState('filter.service_id'); + + if (!empty($serviceId)) { + $query->where($db->quoteName('a.service_id') . ' = ' . (int) $serviceId); + } + + // Filter by search (article title or message content) + $search = $this->getState('filter.search'); + + if (!empty($search)) { + $search = '%' . $db->escape(trim($search), true) . '%'; + $query->where('(' . $db->quoteName('c.title') . ' LIKE ' . $db->quote($search) + . ' OR ' . $db->quoteName('a.message') . ' LIKE ' . $db->quote($search) . ')'); + } + // Ordering $orderCol = $this->state->get('list.ordering', 'a.created'); $orderDirn = $this->state->get('list.direction', 'DESC'); diff --git a/source/packages/com_mokosuitecross/src/Model/ServiceModel.php b/source/packages/com_mokosuitecross/src/Model/ServiceModel.php new file mode 100644 index 0000000..aeb1d77 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Model/ServiceModel.php @@ -0,0 +1,123 @@ + + * @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\Component\MokoSuiteCross\Administrator\Model; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Filter\OutputFilter; +use Joomla\CMS\MVC\Model\AdminModel; + +class ServiceModel extends AdminModel +{ + /** + * Method to get the record form. + * + * @param array $data Data for the form + * @param boolean $loadData True if the form is to load its own data + * + * @return \Joomla\CMS\Form\Form|boolean + */ + public function getForm($data = [], $loadData = true) + { + $form = $this->loadForm( + 'com_mokosuitecross.service', + 'service', + ['control' => 'jform', 'load_data' => $loadData] + ); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * Expands the JSON credentials column back into individual cred_* form fields + * so they are populated when editing an existing service. + * + * @return mixed The data for the form + */ + protected function loadFormData() + { + $data = $this->getItem(); + + if ($data && !empty($data->credentials)) { + $credentials = \Joomla\Component\MokoSuiteCross\Administrator\Helper\CredentialHelper::decrypt($data->credentials); + $serviceType = $data->service_type ?? ''; + + foreach ($credentials as $key => $value) { + // Map credential keys back to form field names. + // The mode field has no service type prefix. + if ($key === 'mode') { + $data->cred_mode = $value; + } else { + $data->{'cred_' . $serviceType . '_' . $key} = $value; + } + } + } + + return $data; + } + + /** + * Override save to collect cred_* form fields into the credentials JSON column. + * + * The service form has individual fields (cred_twitter_api_key, cred_facebook_page_id, etc.) + * but the database stores them as a single JSON blob in the `credentials` column. + * + * @param array $data The form data + * + * @return boolean True on success + */ + public function save($data) + { + $serviceType = $data['service_type'] ?? ''; + $credentials = []; + $credPrefix = 'cred_'; + + // Collect all cred_* fields into the credentials array + foreach ($data as $key => $value) { + if (strpos($key, $credPrefix) !== 0) { + continue; + } + + $credKey = substr($key, strlen($credPrefix)); + + // The mode field is shared across service types (no service_type prefix) + if ($credKey === 'mode') { + $credentials['mode'] = $value; + } elseif ($serviceType && strpos($credKey, $serviceType . '_') === 0) { + // Strip the service_type prefix: cred_twitter_api_key -> api_key + $strippedKey = substr($credKey, strlen($serviceType) + 1); + $credentials[$strippedKey] = $value; + } + } + + // Store credentials encrypted + $data['credentials'] = !empty($credentials) + ? \Joomla\Component\MokoSuiteCross\Administrator\Helper\CredentialHelper::encrypt($credentials) + : '{}'; + + // Remove individual cred_* fields so they don't cause column-not-found errors + foreach (array_keys($data) as $key) { + if (strpos($key, $credPrefix) === 0) { + unset($data[$key]); + } + } + + return parent::save($data); + } +} diff --git a/source/packages/com_mokosuitecross/src/Model/ServiceStatsModel.php b/source/packages/com_mokosuitecross/src/Model/ServiceStatsModel.php new file mode 100644 index 0000000..8ab760f --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Model/ServiceStatsModel.php @@ -0,0 +1,185 @@ + + * @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\Component\MokoSuiteCross\Administrator\Model; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\Model\BaseDatabaseModel; + +/** + * Per-service analytics drill-down model. + */ +class ServiceStatsModel extends BaseDatabaseModel +{ + /** + * Get the service ID from the request. + * + * @return int + */ + public function getServiceId(): int + { + return Factory::getApplication()->input->getInt('id', 0); + } + + /** + * Load a single service record by ID. + * + * @param int $id Service ID + * + * @return object|null + */ + public function getService(int $id = 0): ?object + { + if ($id === 0) { + $id = $this->getServiceId(); + } + + if ($id === 0) { + return null; + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokosuitecross_services')) + ->where($db->quoteName('id') . ' = ' . (int) $id); + + $db->setQuery($query); + + return $db->loadObject() ?: null; + } + + /** + * Get post status counts for a specific service. + * + * @param int $serviceId Service ID + * + * @return object Object with total, posted, failed, queued properties + */ + public function getPostStats(int $serviceId): object + { + $db = $this->getDatabase(); + + $stats = new \stdClass(); + + foreach (['queued', 'posted', 'failed'] as $status) { + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuitecross_posts')) + ->where($db->quoteName('service_id') . ' = ' . (int) $serviceId) + ->where($db->quoteName('status') . ' = ' . $db->quote($status)); + $db->setQuery($query); + $stats->{$status} = (int) $db->loadResult(); + } + + $stats->total = $stats->queued + $stats->posted + $stats->failed; + + return $stats; + } + + /** + * Get daily post trend for a specific service. + * + * @param int $serviceId Service ID + * @param int $days Number of days to look back + * + * @return array [['day' => '2026-05-28', 'posted' => N, 'failed' => N], ...] + */ + public function getDailyTrend(int $serviceId, int $days = 30): array + { + $db = $this->getDatabase(); + + $cutoff = Factory::getDate('now - ' . $days . ' days')->format('Y-m-d'); + + $query = $db->getQuery(true) + ->select([ + 'DATE(' . $db->quoteName('created') . ') AS day', + 'SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('posted') . ' THEN 1 ELSE 0 END) AS posted', + 'SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('failed') . ' THEN 1 ELSE 0 END) AS failed', + 'COUNT(*) AS total', + ]) + ->from($db->quoteName('#__mokosuitecross_posts')) + ->where($db->quoteName('service_id') . ' = ' . (int) $serviceId) + ->where('DATE(' . $db->quoteName('created') . ') >= ' . $db->quote($cutoff)) + ->group('DATE(' . $db->quoteName('created') . ')') + ->order('day ASC'); + + $db->setQuery($query); + + return $db->loadAssocList() ?: []; + } + + /** + * Get recent posts for a specific service with article titles. + * + * @param int $serviceId Service ID + * @param int $limit Number of posts to return + * + * @return array + */ + public function getRecentPosts(int $serviceId, int $limit = 20): array + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select([ + $db->quoteName('p.id'), + $db->quoteName('p.status'), + $db->quoteName('p.posted_at'), + $db->quoteName('p.created'), + $db->quoteName('p.error_message'), + $db->quoteName('p.retry_count'), + $db->quoteName('c.title', 'article_title'), + ]) + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->join('LEFT', $db->quoteName('#__content', 'c') + . ' ON ' . $db->quoteName('c.id') . ' = ' . $db->quoteName('p.article_id')) + ->where($db->quoteName('p.service_id') . ' = ' . (int) $serviceId) + ->order($db->quoteName('p.created') . ' DESC'); + + $db->setQuery($query, 0, $limit); + + return $db->loadAssocList() ?: []; + } + + /** + * Get the most cross-posted articles for a specific service. + * + * @param int $serviceId Service ID + * @param int $limit Number of articles to return + * + * @return array + */ + public function getTopArticles(int $serviceId, int $limit = 10): array + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select([ + $db->quoteName('c.id'), + $db->quoteName('c.title'), + 'COUNT(*) AS post_count', + 'SUM(CASE WHEN ' . $db->quoteName('p.status') . ' = ' . $db->quote('posted') . ' THEN 1 ELSE 0 END) AS success_count', + ]) + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->join('INNER', $db->quoteName('#__content', 'c') + . ' ON ' . $db->quoteName('c.id') . ' = ' . $db->quoteName('p.article_id')) + ->where($db->quoteName('p.service_id') . ' = ' . (int) $serviceId) + ->group($db->quoteName(['c.id', 'c.title'])) + ->order('post_count DESC'); + + $db->setQuery($query, 0, $limit); + + return $db->loadAssocList() ?: []; + } +} diff --git a/src/packages/com_mokojoomcross/src/Model/ServicesModel.php b/source/packages/com_mokosuitecross/src/Model/ServicesModel.php similarity index 90% rename from src/packages/com_mokojoomcross/src/Model/ServicesModel.php rename to source/packages/com_mokosuitecross/src/Model/ServicesModel.php index 10eadf9..18beabc 100644 --- a/src/packages/com_mokojoomcross/src/Model/ServicesModel.php +++ b/source/packages/com_mokosuitecross/src/Model/ServicesModel.php @@ -1,15 +1,15 @@ * @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\Component\MokoJoomCross\Administrator\Model; +namespace Joomla\Component\MokoSuiteCross\Administrator\Model; defined('_JEXEC') or die; @@ -48,7 +48,7 @@ class ServicesModel extends ListModel $query = $db->getQuery(true); $query->select('a.*') - ->from($db->quoteName('#__mokojoomcross_services', 'a')); + ->from($db->quoteName('#__mokosuitecross_services', 'a')); // Filter by published state $published = $this->getState('filter.published'); diff --git a/source/packages/com_mokosuitecross/src/Model/TemplateModel.php b/source/packages/com_mokosuitecross/src/Model/TemplateModel.php new file mode 100644 index 0000000..0928a99 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Model/TemplateModel.php @@ -0,0 +1,39 @@ + + * @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\Component\MokoSuiteCross\Administrator\Model; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\Model\AdminModel; + +class TemplateModel extends AdminModel +{ + public function getForm($data = [], $loadData = true) + { + $form = $this->loadForm( + 'com_mokosuitecross.template', + 'template', + ['control' => 'jform', 'load_data' => $loadData] + ); + + if (empty($form)) { + return false; + } + + return $form; + } + + protected function loadFormData() + { + return $this->getItem(); + } +} diff --git a/source/packages/com_mokosuitecross/src/Model/TemplatesModel.php b/source/packages/com_mokosuitecross/src/Model/TemplatesModel.php new file mode 100644 index 0000000..bd6c871 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Model/TemplatesModel.php @@ -0,0 +1,61 @@ + + * @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\Component\MokoSuiteCross\Administrator\Model; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\Model\ListModel; + +class TemplatesModel extends ListModel +{ + public function __construct($config = []) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = [ + 'id', 'a.id', + 'title', 'a.title', + 'service_type', 'a.service_type', + 'published', 'a.published', + 'ordering', 'a.ordering', + ]; + } + + parent::__construct($config); + } + + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select('a.*') + ->from($db->quoteName('#__mokosuitecross_templates', 'a')); + + $published = $this->getState('filter.published'); + + if (is_numeric($published)) { + $query->where($db->quoteName('a.published') . ' = ' . (int) $published); + } + + $serviceType = $this->getState('filter.service_type'); + + if (!empty($serviceType)) { + $query->where($db->quoteName('a.service_type') . ' = ' . $db->quote($serviceType)); + } + + $orderCol = $this->state->get('list.ordering', 'a.ordering'); + $orderDirn = $this->state->get('list.direction', 'ASC'); + $query->order($db->escape($orderCol) . ' ' . $db->escape($orderDirn)); + + return $query; + } +} diff --git a/src/packages/com_mokojoomcross/src/Model/index.html b/source/packages/com_mokosuitecross/src/Model/index.html similarity index 100% rename from src/packages/com_mokojoomcross/src/Model/index.html rename to source/packages/com_mokosuitecross/src/Model/index.html diff --git a/src/packages/com_mokojoomcross/src/Service/MokoJoomCrossServiceInterface.php b/source/packages/com_mokosuitecross/src/Service/MokoSuiteCrossServiceInterface.php similarity index 73% rename from src/packages/com_mokojoomcross/src/Service/MokoJoomCrossServiceInterface.php rename to source/packages/com_mokosuitecross/src/Service/MokoSuiteCrossServiceInterface.php index d27718c..b272c59 100644 --- a/src/packages/com_mokojoomcross/src/Service/MokoJoomCrossServiceInterface.php +++ b/source/packages/com_mokosuitecross/src/Service/MokoSuiteCrossServiceInterface.php @@ -1,26 +1,26 @@ * @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\Component\MokoJoomCross\Administrator\Service; +namespace Joomla\Component\MokoSuiteCross\Administrator\Service; defined('_JEXEC') or die; /** - * Interface that all MokoJoomCross service plugins must implement. + * Interface that all MokoSuiteCross service plugins must implement. * - * Service plugins in the `mokojoomcross` plugin group register themselves + * Service plugins in the `mokosuitecross` plugin group register themselves * by implementing this interface. The system plugin dispatches cross-post * requests to all enabled service plugins via this contract. */ -interface MokoJoomCrossServiceInterface +interface MokoSuiteCrossServiceInterface { /** * Get the unique service type identifier. @@ -70,4 +70,15 @@ interface MokoJoomCrossServiceInterface * @return bool */ public function supportsMedia(): bool; + + /** + * Get the media types this service supports. + * + * Return an array of supported types: 'image', 'video', 'gif', 'document'. + * Services that return an empty array are text-only. + * Default implementation returns ['image'] if supportsMedia() is true. + * + * @return string[] e.g. ['image', 'video', 'gif'] + */ + public function getSupportedMediaTypes(): array; } diff --git a/src/packages/com_mokojoomcross/src/Service/index.html b/source/packages/com_mokosuitecross/src/Service/index.html similarity index 100% rename from src/packages/com_mokojoomcross/src/Service/index.html rename to source/packages/com_mokosuitecross/src/Service/index.html diff --git a/src/packages/com_mokojoomcross/src/Table/PostTable.php b/source/packages/com_mokosuitecross/src/Table/PostTable.php similarity index 70% rename from src/packages/com_mokojoomcross/src/Table/PostTable.php rename to source/packages/com_mokosuitecross/src/Table/PostTable.php index c78d3f7..b1c8ec3 100644 --- a/src/packages/com_mokojoomcross/src/Table/PostTable.php +++ b/source/packages/com_mokosuitecross/src/Table/PostTable.php @@ -1,15 +1,15 @@ * @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\Component\MokoJoomCross\Administrator\Table; +namespace Joomla\Component\MokoSuiteCross\Administrator\Table; defined('_JEXEC') or die; @@ -20,6 +20,6 @@ class PostTable extends Table { public function __construct(DatabaseDriver $db) { - parent::__construct('#__mokojoomcross_posts', 'id', $db); + parent::__construct('#__mokosuitecross_posts', 'id', $db); } } diff --git a/source/packages/com_mokosuitecross/src/Table/ServiceTable.php b/source/packages/com_mokosuitecross/src/Table/ServiceTable.php new file mode 100644 index 0000000..63ca2e2 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Table/ServiceTable.php @@ -0,0 +1,91 @@ + + * @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\Component\MokoSuiteCross\Administrator\Table; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Filter\OutputFilter; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Table\Table; +use Joomla\Database\DatabaseDriver; + +class ServiceTable extends Table +{ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__mokosuitecross_services', 'id', $db); + } + + /** + * Validate the record before storing. + * + * Generates alias from title if empty, validates required fields, + * sets created/modified timestamps. + * + * @return boolean True if the record is valid + */ + public function check(): bool + { + // Title is required + if (empty($this->title)) { + $this->setError(Text::_('COM_MOKOSUITECROSS_ERROR_TITLE_REQUIRED')); + + return false; + } + + // Service type is required + if (empty($this->service_type)) { + $this->setError(Text::_('COM_MOKOSUITECROSS_ERROR_SERVICE_TYPE_REQUIRED')); + + return false; + } + + // Generate alias from title if empty + if (empty($this->alias)) { + $this->alias = $this->title; + } + + $this->alias = OutputFilter::stringURLSafe($this->alias); + + // Make sure alias is unique + if (empty($this->alias)) { + $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); + } + + // Set timestamps + $now = Factory::getDate()->toSql(); + + if (empty($this->created)) { + $this->created = $now; + } + + $this->modified = $now; + + // Set created_by if not set + if (empty($this->created_by)) { + $this->created_by = Factory::getApplication()->getIdentity()->id ?? 0; + } + + // Ensure credentials is valid JSON + if (empty($this->credentials)) { + $this->credentials = '{}'; + } + + // Ensure params is valid JSON + if (empty($this->params)) { + $this->params = '{}'; + } + + return true; + } +} diff --git a/src/packages/com_mokojoomcross/src/Table/ServiceTable.php b/source/packages/com_mokosuitecross/src/Table/TemplateTable.php similarity index 64% rename from src/packages/com_mokojoomcross/src/Table/ServiceTable.php rename to source/packages/com_mokosuitecross/src/Table/TemplateTable.php index c89fc64..7eb2a80 100644 --- a/src/packages/com_mokojoomcross/src/Table/ServiceTable.php +++ b/source/packages/com_mokosuitecross/src/Table/TemplateTable.php @@ -1,25 +1,25 @@ * @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\Component\MokoJoomCross\Administrator\Table; +namespace Joomla\Component\MokoSuiteCross\Administrator\Table; defined('_JEXEC') or die; use Joomla\CMS\Table\Table; use Joomla\Database\DatabaseDriver; -class ServiceTable extends Table +class TemplateTable extends Table { public function __construct(DatabaseDriver $db) { - parent::__construct('#__mokojoomcross_services', 'id', $db); + parent::__construct('#__mokosuitecross_templates', 'id', $db); } } diff --git a/src/packages/com_mokojoomcross/src/Table/index.html b/source/packages/com_mokosuitecross/src/Table/index.html similarity index 100% rename from src/packages/com_mokojoomcross/src/Table/index.html rename to source/packages/com_mokosuitecross/src/Table/index.html diff --git a/source/packages/com_mokosuitecross/src/View/Dashboard/HtmlView.php b/source/packages/com_mokosuitecross/src/View/Dashboard/HtmlView.php new file mode 100644 index 0000000..172821d --- /dev/null +++ b/source/packages/com_mokosuitecross/src/View/Dashboard/HtmlView.php @@ -0,0 +1,71 @@ + + * @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\Component\MokoSuiteCross\Administrator\View\Dashboard; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\MokoSuiteCrossHelper; + +class HtmlView extends BaseHtmlView +{ + protected $stats; + protected $migrationAvailable; + protected $recentActivity; + protected $serviceBreakdown; + protected $dailyTrend; + protected $topArticles; + public $sidebar; + public $period; + + public function display($tpl = null): void + { + $model = $this->getModel(); + + // Read period parameter for date range filtering + $this->period = Factory::getApplication()->input->getInt('period', 30); + $validPeriods = [7, 30, 90, 0]; + + if (!in_array($this->period, $validPeriods, true)) { + $this->period = 30; + } + + // Calculate the since date based on period (0 = all time) + $since = null; + + if ($this->period > 0) { + $since = Factory::getDate('now - ' . $this->period . ' days')->toSql(); + } + + $this->stats = $this->get('Stats'); + $this->migrationAvailable = $this->get('MigrationAvailable'); + $this->recentActivity = $model->getRecentActivity(10); + $this->serviceBreakdown = $model->getServiceBreakdown($since); + $this->dailyTrend = $model->getDailyTrend($this->period ?: 365); + $this->topArticles = $model->getTopArticles(5, $since); + + $this->addToolbar(); + + MokoSuiteCrossHelper::addSubmenu('dashboard'); + $this->sidebar = \Joomla\CMS\HTML\Sidebar::render(); + + parent::display($tpl); + } + + protected function addToolbar(): void + { + ToolbarHelper::title('MokoSuiteCross — Dashboard', 'share-alt'); + ToolbarHelper::preferences('com_mokosuitecross'); + } +} diff --git a/src/packages/com_mokojoomcross/src/View/Dashboard/index.html b/source/packages/com_mokosuitecross/src/View/Dashboard/index.html similarity index 100% rename from src/packages/com_mokojoomcross/src/View/Dashboard/index.html rename to source/packages/com_mokosuitecross/src/View/Dashboard/index.html diff --git a/source/packages/com_mokosuitecross/src/View/Logs/HtmlView.php b/source/packages/com_mokosuitecross/src/View/Logs/HtmlView.php new file mode 100644 index 0000000..f71795d --- /dev/null +++ b/source/packages/com_mokosuitecross/src/View/Logs/HtmlView.php @@ -0,0 +1,56 @@ + + * @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\Component\MokoSuiteCross\Administrator\View\Logs; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Toolbar\Toolbar; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + protected $items; + protected $pagination; + protected $state; + public $filterForm; + public $activeFilters; + + public function display($tpl = null): void + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + $this->addToolbar(); + + parent::display($tpl); + } + + protected function addToolbar(): void + { + ToolbarHelper::title('MokoSuiteCross — Activity Logs', 'share-alt'); + ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'logs.delete', 'JTOOLBAR_DELETE'); + + // Dashboard link in toolbar + $toolbar = Toolbar::getInstance('toolbar'); + $toolbar->appendButton( + 'Link', + 'home', + 'COM_MOKOSUITECROSS_SUBMENU_DASHBOARD', + Route::_('index.php?option=com_mokosuitecross&view=dashboard', false) + ); + } +} diff --git a/src/packages/com_mokojoomcross/src/View/Logs/index.html b/source/packages/com_mokosuitecross/src/View/Logs/index.html similarity index 100% rename from src/packages/com_mokojoomcross/src/View/Logs/index.html rename to source/packages/com_mokosuitecross/src/View/Logs/index.html diff --git a/source/packages/com_mokosuitecross/src/View/Post/HtmlView.php b/source/packages/com_mokosuitecross/src/View/Post/HtmlView.php new file mode 100644 index 0000000..07e4e7a --- /dev/null +++ b/source/packages/com_mokosuitecross/src/View/Post/HtmlView.php @@ -0,0 +1,59 @@ + + * @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\Component\MokoSuiteCross\Administrator\View\Post; + +defined('_JEXEC') or die; + +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Toolbar\Toolbar; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + protected $form; + protected $item; + + public function display($tpl = null): void + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + + $this->addToolbar(); + + parent::display($tpl); + } + + protected function addToolbar(): void + { + $isNew = empty($this->item->id); + + ToolbarHelper::title( + 'MokoSuiteCross — ' . ($isNew ? Text::_('COM_MOKOSUITECROSS_NEW_POST') : Text::_('COM_MOKOSUITECROSS_EDIT_POST')), + 'share-alt' + ); + + ToolbarHelper::apply('post.apply'); + ToolbarHelper::save('post.save'); + + $toolbar = Toolbar::getInstance('toolbar'); + $toolbar->appendButton( + 'Link', + 'home', + 'COM_MOKOSUITECROSS_SUBMENU_DASHBOARD', + Route::_('index.php?option=com_mokosuitecross&view=dashboard', false) + ); + + ToolbarHelper::cancel('post.cancel'); + } +} diff --git a/source/packages/com_mokosuitecross/src/View/Posts/HtmlView.php b/source/packages/com_mokosuitecross/src/View/Posts/HtmlView.php new file mode 100644 index 0000000..a6b6dd9 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/View/Posts/HtmlView.php @@ -0,0 +1,82 @@ + + * @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\Component\MokoSuiteCross\Administrator\View\Posts; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Toolbar\Toolbar; +use Joomla\CMS\Toolbar\ToolbarHelper; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\MokoSuiteCrossHelper; + +class HtmlView extends BaseHtmlView +{ + protected $items; + protected $pagination; + protected $state; + public $filterForm; + public $activeFilters; + public $sidebar; + + public function display($tpl = null): void + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + $this->addToolbar(); + + parent::display($tpl); + } + + protected function addToolbar(): void + { + ToolbarHelper::title('MokoSuiteCross — Post Queue', 'share-alt'); + ToolbarHelper::addNew('post.add'); + + $toolbar = Toolbar::getInstance('toolbar'); + $toolbar->standardButton('retry', 'COM_MOKOSUITECROSS_TOOLBAR_RETRY_FAILED', 'posts.retryFailed') + ->icon('icon-refresh') + ->listCheck(false); + $toolbar->standardButton('purge', 'COM_MOKOSUITECROSS_TOOLBAR_PURGE_POSTED', 'posts.purgePosted') + ->icon('icon-trash') + ->listCheck(false); + + $toolbar->standardButton('retry-selected', 'COM_MOKOSUITECROSS_TOOLBAR_RETRY_SELECTED', 'posts.retrySelected') + ->icon('icon-redo') + ->listCheck(true); + $toolbar->standardButton('schedule', 'COM_MOKOSUITECROSS_TOOLBAR_SCHEDULE', 'posts.schedule') + ->icon('icon-calendar') + ->listCheck(true); + + ToolbarHelper::deleteList('', 'posts.delete', 'JTOOLBAR_DELETE'); + + // Export CSV button + $toolbar->appendButton( + 'Link', + 'download', + 'COM_MOKOSUITECROSS_EXPORT_CSV', + Route::_('index.php?option=com_mokosuitecross&task=posts.exportCsv&format=raw', false) + ); + + // Dashboard link in toolbar + $toolbar->appendButton( + 'Link', + 'home', + 'COM_MOKOSUITECROSS_SUBMENU_DASHBOARD', + Route::_('index.php?option=com_mokosuitecross&view=dashboard', false) + ); + } +} diff --git a/src/packages/com_mokojoomcross/src/View/Posts/index.html b/source/packages/com_mokosuitecross/src/View/Posts/index.html similarity index 100% rename from src/packages/com_mokojoomcross/src/View/Posts/index.html rename to source/packages/com_mokosuitecross/src/View/Posts/index.html diff --git a/source/packages/com_mokosuitecross/src/View/Service/HtmlView.php b/source/packages/com_mokosuitecross/src/View/Service/HtmlView.php new file mode 100644 index 0000000..5112bf9 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/View/Service/HtmlView.php @@ -0,0 +1,61 @@ + + * @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\Component\MokoSuiteCross\Administrator\View\Service; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Toolbar\Toolbar; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + protected $form; + protected $item; + + public function display($tpl = null): void + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + + $this->addToolbar(); + + parent::display($tpl); + } + + protected function addToolbar(): void + { + $isNew = empty($this->item->id); + + ToolbarHelper::title( + 'MokoSuiteCross — ' . ($isNew ? Text::_('COM_MOKOSUITECROSS_NEW_SERVICE') : Text::_('COM_MOKOSUITECROSS_EDIT_SERVICE')), + 'share-alt' + ); + + ToolbarHelper::apply('service.apply'); + ToolbarHelper::save('service.save'); + + // Dashboard button in toolbar + $toolbar = Toolbar::getInstance('toolbar'); + $toolbar->appendButton( + 'Link', + 'home', + 'COM_MOKOSUITECROSS_SUBMENU_DASHBOARD', + Route::_('index.php?option=com_mokosuitecross&view=dashboard', false) + ); + + ToolbarHelper::cancel('service.cancel'); + } +} diff --git a/src/packages/com_mokojoomcross/src/View/Services/index.html b/source/packages/com_mokosuitecross/src/View/Service/index.html similarity index 100% rename from src/packages/com_mokojoomcross/src/View/Services/index.html rename to source/packages/com_mokosuitecross/src/View/Service/index.html diff --git a/source/packages/com_mokosuitecross/src/View/ServiceStats/HtmlView.php b/source/packages/com_mokosuitecross/src/View/ServiceStats/HtmlView.php new file mode 100644 index 0000000..fd8ffbc --- /dev/null +++ b/source/packages/com_mokosuitecross/src/View/ServiceStats/HtmlView.php @@ -0,0 +1,84 @@ + + * @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\Component\MokoSuiteCross\Administrator\View\ServiceStats; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Toolbar\Toolbar; +use Joomla\CMS\Toolbar\ToolbarHelper; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\MokoSuiteCrossHelper; + +/** + * Per-service analytics drill-down view. + */ +class HtmlView extends BaseHtmlView +{ + public $service; + public $postStats; + public $dailyTrend; + public $recentPosts; + public $topArticles; + public $period; + + public function display($tpl = null): void + { + /** @var \Joomla\Component\MokoSuiteCross\Administrator\Model\ServiceStatsModel $model */ + $model = $this->getModel(); + + $serviceId = $model->getServiceId(); + + $this->service = $model->getService($serviceId); + + if (!$this->service) { + throw new \RuntimeException('Service not found.', 404); + } + + $this->period = Factory::getApplication()->input->getInt('period', 30); + $validPeriods = [7, 30, 90, 0]; + + if (!\in_array($this->period, $validPeriods, true)) { + $this->period = 30; + } + + $days = $this->period ?: 365; + + $this->postStats = $model->getPostStats($serviceId); + $this->dailyTrend = $model->getDailyTrend($serviceId, $days); + $this->recentPosts = $model->getRecentPosts($serviceId, 20); + $this->topArticles = $model->getTopArticles($serviceId, 10); + + $this->addToolbar(); + + MokoSuiteCrossHelper::addSubmenu('servicestats'); + + parent::display($tpl); + } + + protected function addToolbar(): void + { + ToolbarHelper::title( + 'MokoSuiteCross — ' . $this->escape($this->service->title), + 'share-alt' + ); + + $toolbar = Toolbar::getInstance('toolbar'); + $toolbar->appendButton( + 'Link', + 'home', + 'COM_MOKOSUITECROSS_SUBMENU_DASHBOARD', + Route::_('index.php?option=com_mokosuitecross&view=dashboard', false) + ); + } +} diff --git a/src/packages/com_mokojoomcross/src/View/Services/HtmlView.php b/source/packages/com_mokosuitecross/src/View/Services/HtmlView.php similarity index 52% rename from src/packages/com_mokojoomcross/src/View/Services/HtmlView.php rename to source/packages/com_mokosuitecross/src/View/Services/HtmlView.php index 56fdc8d..8618d1b 100644 --- a/src/packages/com_mokojoomcross/src/View/Services/HtmlView.php +++ b/source/packages/com_mokosuitecross/src/View/Services/HtmlView.php @@ -1,19 +1,21 @@ * @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\Component\MokoJoomCross\Administrator\View\Services; +namespace Joomla\Component\MokoSuiteCross\Administrator\View\Services; defined('_JEXEC') or die; use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Toolbar\Toolbar; use Joomla\CMS\Toolbar\ToolbarHelper; class HtmlView extends BaseHtmlView @@ -21,12 +23,16 @@ class HtmlView extends BaseHtmlView protected $items; protected $pagination; protected $state; + public $filterForm; + public $activeFilters; public function display($tpl = null): void { - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); $this->addToolbar(); @@ -35,11 +41,20 @@ class HtmlView extends BaseHtmlView protected function addToolbar(): void { - ToolbarHelper::title('MokoJoomCross — Services', 'share-alt'); + ToolbarHelper::title('MokoSuiteCross — Services', 'share-alt'); ToolbarHelper::addNew('service.add'); ToolbarHelper::editList('service.edit'); ToolbarHelper::publish('services.publish', 'JTOOLBAR_PUBLISH', true); ToolbarHelper::unpublish('services.unpublish', 'JTOOLBAR_UNPUBLISH', true); ToolbarHelper::deleteList('', 'services.delete', 'JTOOLBAR_DELETE'); + + // Dashboard link in toolbar + $toolbar = Toolbar::getInstance('toolbar'); + $toolbar->appendButton( + 'Link', + 'home', + 'COM_MOKOSUITECROSS_SUBMENU_DASHBOARD', + Route::_('index.php?option=com_mokosuitecross&view=dashboard', false) + ); } } diff --git a/src/packages/com_mokojoomcross/src/View/index.html b/source/packages/com_mokosuitecross/src/View/Services/index.html similarity index 100% rename from src/packages/com_mokojoomcross/src/View/index.html rename to source/packages/com_mokosuitecross/src/View/Services/index.html diff --git a/source/packages/com_mokosuitecross/src/View/Template/HtmlView.php b/source/packages/com_mokosuitecross/src/View/Template/HtmlView.php new file mode 100644 index 0000000..70fa2e1 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/View/Template/HtmlView.php @@ -0,0 +1,57 @@ + + * @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\Component\MokoSuiteCross\Administrator\View\Template; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Toolbar\Toolbar; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + protected $form; + protected $item; + + public function display($tpl = null): void + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + + $this->addToolbar(); + + parent::display($tpl); + } + + protected function addToolbar(): void + { + $isNew = empty($this->item->id); + + ToolbarHelper::title( + 'MokoSuiteCross — ' . ($isNew ? 'New Template' : 'Edit Template'), + 'share-alt' + ); + ToolbarHelper::apply('template.apply'); + ToolbarHelper::save('template.save'); + ToolbarHelper::cancel('template.cancel'); + + // Dashboard link in toolbar + $toolbar = Toolbar::getInstance('toolbar'); + $toolbar->appendButton( + 'Link', + 'home', + 'COM_MOKOSUITECROSS_SUBMENU_DASHBOARD', + Route::_('index.php?option=com_mokosuitecross&view=dashboard', false) + ); + } +} diff --git a/source/packages/com_mokosuitecross/src/View/Template/index.html b/source/packages/com_mokosuitecross/src/View/Template/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/com_mokosuitecross/src/View/Template/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/com_mokosuitecross/src/View/Templates/HtmlView.php b/source/packages/com_mokosuitecross/src/View/Templates/HtmlView.php new file mode 100644 index 0000000..efe75aa --- /dev/null +++ b/source/packages/com_mokosuitecross/src/View/Templates/HtmlView.php @@ -0,0 +1,60 @@ + + * @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\Component\MokoSuiteCross\Administrator\View\Templates; + +defined('_JEXEC') or die; + +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Toolbar\Toolbar; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + protected $items; + protected $pagination; + protected $state; + public $filterForm; + public $activeFilters; + + public function display($tpl = null): void + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + $this->addToolbar(); + + parent::display($tpl); + } + + protected function addToolbar(): void + { + ToolbarHelper::title('MokoSuiteCross — Message Templates', 'share-alt'); + ToolbarHelper::addNew('template.add'); + ToolbarHelper::editList('template.edit'); + ToolbarHelper::publish('templates.publish', 'JTOOLBAR_PUBLISH', true); + ToolbarHelper::unpublish('templates.unpublish', 'JTOOLBAR_UNPUBLISH', true); + ToolbarHelper::deleteList('', 'templates.delete', 'JTOOLBAR_DELETE'); + + // Dashboard link in toolbar + $toolbar = Toolbar::getInstance('toolbar'); + $toolbar->appendButton( + 'Link', + 'home', + 'COM_MOKOSUITECROSS_SUBMENU_DASHBOARD', + Route::_('index.php?option=com_mokosuitecross&view=dashboard', false) + ); + } +} diff --git a/source/packages/com_mokosuitecross/src/View/Templates/index.html b/source/packages/com_mokosuitecross/src/View/Templates/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/com_mokosuitecross/src/View/Templates/index.html @@ -0,0 +1 @@ + diff --git a/src/packages/com_mokojoomcross/src/index.html b/source/packages/com_mokosuitecross/src/View/index.html similarity index 100% rename from src/packages/com_mokojoomcross/src/index.html rename to source/packages/com_mokosuitecross/src/View/index.html diff --git a/src/packages/com_mokojoomcross/tmpl/dashboard/index.html b/source/packages/com_mokosuitecross/src/index.html similarity index 100% rename from src/packages/com_mokojoomcross/tmpl/dashboard/index.html rename to source/packages/com_mokosuitecross/src/index.html diff --git a/source/packages/com_mokosuitecross/tmpl/dashboard/default.php b/source/packages/com_mokosuitecross/tmpl/dashboard/default.php new file mode 100644 index 0000000..13be578 --- /dev/null +++ b/source/packages/com_mokosuitecross/tmpl/dashboard/default.php @@ -0,0 +1,289 @@ + + * @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\Component\ComponentHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\ServiceIconHelper; + +/** @var \Joomla\Component\MokoSuiteCross\Administrator\View\Dashboard\HtmlView $this */ +$stats = $this->stats; +$componentParams = ComponentHelper::getParams('com_mokosuitecross'); +$queueProcessing = $componentParams->get('queue_processing', 'scheduler'); +?> + +
+ +
+
+ +
+
+ + +queued_count > 50) : ?> +
+ +
+
+ queued_count); ?> +
+
+ + +
+
+
+
+
+
+
+

active_services; ?>

+
+
+
+
+
+
+
+

queued_count; ?>

+
+
+
+
+
+
+
+

posted_count; ?>

+
+
+
+
+
+
+
+

failed_count; ?>

+
+
+
+
+ + + dailyTrend)) : ?> +
+
+
+
+ + + +
+
+
+ +
+
+ + + + + migrationAvailable) : ?> +
+

+

+ + + +
+ + + + serviceBreakdown)) : ?> +
+
+
+
+
+ + + + + + + + + + + + + serviceBreakdown as $row) : + $rate = $row['total'] > 0 ? round(($row['posted'] / $row['total']) * 100) : 0; + $rateClass = $rate >= 80 ? 'text-success' : ($rate >= 50 ? 'text-warning' : 'text-danger'); + ?> + + + + + + + + + + +
+ + + + + %
+
+
+ + + + topArticles)) : ?> +
+
+
+
+
+
+ topArticles as $row) : ?> +
+ + + + / + + +
+ +
+
+
+ + + +
+
+
+
+
+ recentActivity)) : ?> +

+ +
+ recentActivity as $entry) : + $levelClass = match ($entry->level) { + 'error' => 'text-danger', + 'warning' => 'text-warning', + default => 'text-muted', + }; + $levelIcon = match ($entry->level) { + 'error' => 'icon-times-circle', + 'warning' => 'icon-exclamation-triangle', + default => 'icon-info-circle', + }; + ?> +
+
+ + + message, 0, 120)); ?> + + created, 'Y-m-d H:i'); ?> +
+ service_title) : ?> + service_title); ?> + +
+ +
+ +
+
+
+ +
+
+ +
+
+
diff --git a/src/packages/com_mokojoomcross/tmpl/index.html b/source/packages/com_mokosuitecross/tmpl/dashboard/index.html similarity index 100% rename from src/packages/com_mokojoomcross/tmpl/index.html rename to source/packages/com_mokosuitecross/tmpl/dashboard/index.html diff --git a/src/packages/com_mokojoomcross/tmpl/logs/index.html b/source/packages/com_mokosuitecross/tmpl/index.html similarity index 100% rename from src/packages/com_mokojoomcross/tmpl/logs/index.html rename to source/packages/com_mokosuitecross/tmpl/index.html diff --git a/source/packages/com_mokosuitecross/tmpl/logs/default.php b/source/packages/com_mokosuitecross/tmpl/logs/default.php new file mode 100644 index 0000000..030d361 --- /dev/null +++ b/source/packages/com_mokosuitecross/tmpl/logs/default.php @@ -0,0 +1,110 @@ + + * @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\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Layout\LayoutHelper; +use Joomla\CMS\Router\Route; + +/** @var \Joomla\Component\MokoSuiteCross\Administrator\View\Logs\HtmlView $this */ + +HTMLHelper::_('behavior.multiselect'); + +$listOrder = $this->escape($this->state->get('list.ordering')); +$listDirn = $this->escape($this->state->get('list.direction')); + +$levelBadges = [ + 'info' => 'bg-info', + 'warning' => 'bg-warning text-dark', + 'error' => 'bg-danger', +]; +?> +
+
+
+
+ $this]); ?> + + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + items as $i => $item) : + $badgeClass = $levelBadges[$item->level] ?? 'bg-secondary'; + ?> + + + + + + + + + + +
+ + + + + + + + + + + +
+ id, false, 'cid', 'cb', ''); ?> + + + escape(ucfirst($item->level)); ?> + + + escape($item->message); ?> + context) && $item->context !== '{}') : ?> +
escape(mb_substr($item->context, 0, 200)); ?> + +
+ escape($item->service_title ?? '—'); ?> + + created, 'Y-m-d H:i:s'); ?> + + id; ?> +
+ + pagination->getListFooter(); ?> + + + + + +
+
+
+
diff --git a/src/packages/com_mokojoomcross/tmpl/posts/index.html b/source/packages/com_mokosuitecross/tmpl/logs/index.html similarity index 100% rename from src/packages/com_mokojoomcross/tmpl/posts/index.html rename to source/packages/com_mokosuitecross/tmpl/logs/index.html diff --git a/source/packages/com_mokosuitecross/tmpl/post/edit.php b/source/packages/com_mokosuitecross/tmpl/post/edit.php new file mode 100644 index 0000000..1ce1a3a --- /dev/null +++ b/source/packages/com_mokosuitecross/tmpl/post/edit.php @@ -0,0 +1,79 @@ + + * @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\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; + +/** @var \Joomla\Component\MokoSuiteCross\Administrator\View\Post\HtmlView $this */ + +HTMLHelper::_('behavior.formvalidator'); +HTMLHelper::_('behavior.keepalive'); + +$postId = (int) ($this->item->id ?? 0); +$isNew = empty($postId); +?> +
+ +
+
+
+

+ + +
+ + +
+ + + form->renderFieldset('details'); ?> +
+ +
+ +
+
+
+ + +
+
+
+ form->renderFieldset('readonly'); ?> +
+
+ + item->status ?? ''; + if (in_array($status, ['failed', 'posted'])) : ?> +
+
+

+ +
+
+ + +
+
+
+ + + +
diff --git a/source/packages/com_mokosuitecross/tmpl/posts/default.php b/source/packages/com_mokosuitecross/tmpl/posts/default.php new file mode 100644 index 0000000..39bedfd --- /dev/null +++ b/source/packages/com_mokosuitecross/tmpl/posts/default.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 + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Layout\LayoutHelper; +use Joomla\CMS\Router\Route; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\ServiceIconHelper; + +/** @var \Joomla\Component\MokoSuiteCross\Administrator\View\Posts\HtmlView $this */ + +HTMLHelper::_('behavior.multiselect'); + +$listOrder = $this->escape($this->state->get('list.ordering')); +$listDirn = $this->escape($this->state->get('list.direction')); + +$statusBadges = [ + 'queued' => 'bg-warning text-dark', + 'posting' => 'bg-info', + 'posted' => 'bg-success', + 'failed' => 'bg-danger', + 'scheduled' => 'bg-secondary', +]; +?> +
+
+
+
+ $this]); ?> + + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + items as $i => $item) : + $badgeClass = $statusBadges[$item->status] ?? 'bg-secondary'; + ?> + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->article_title ?? ''); ?> + + + escape(ucfirst($item->status)); ?> + + status === 'failed' && !empty($item->error_message)) : ?> +
escape(mb_substr($item->error_message, 0, 80)); ?> + + retry_count > 0) : ?> +
Retries: retry_count; ?> + +
+ + escape($item->article_title ?? 'Article #' . $item->article_id); ?> + + scheduled_at)) : ?> +
scheduled_at, 'Y-m-d H:i'); ?> + +
+ escape($item->service_title ?? ''); ?> +
service_type ?? ''); ?> escape($item->service_type ?? ''); ?> +
+ escape(mb_substr($item->message ?? '', 0, 100)); ?> + platform_post_id)) : ?> +
ID: escape($item->platform_post_id); ?> + +
+ posted_at ? HTMLHelper::_('date', $item->posted_at, 'Y-m-d H:i') : '—'; ?> + + created, 'Y-m-d H:i'); ?> + + id; ?> +
+ + pagination->getListFooter(); ?> + + + + + +
+
+
+
diff --git a/src/packages/com_mokojoomcross/tmpl/services/index.html b/source/packages/com_mokosuitecross/tmpl/posts/index.html similarity index 100% rename from src/packages/com_mokojoomcross/tmpl/services/index.html rename to source/packages/com_mokosuitecross/tmpl/posts/index.html diff --git a/source/packages/com_mokosuitecross/tmpl/service/edit.php b/source/packages/com_mokosuitecross/tmpl/service/edit.php new file mode 100644 index 0000000..b7fa41b --- /dev/null +++ b/source/packages/com_mokosuitecross/tmpl/service/edit.php @@ -0,0 +1,216 @@ + + * @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\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; + +/** @var \Joomla\Component\MokoSuiteCross\Administrator\View\Service\HtmlView $this */ + +HTMLHelper::_('behavior.formvalidator'); +HTMLHelper::_('behavior.keepalive'); + +$serviceType = $this->item->service_type ?? ''; +$serviceId = (int) ($this->item->id ?? 0); + +// Services that support OAuth authorize flow +$oauthServices = ['facebook', 'linkedin', 'twitter', 'threads', 'pinterest', 'tumblr', 'tiktok', 'constantcontact', 'blogger', 'googlebusiness']; +$showAuthorize = in_array($serviceType, $oauthServices) && $serviceId > 0; + +// Map service types to KB article aliases on mokoconsulting.tech +$helpArticles = [ + 'facebook' => 'service-facebook-mokosuitecross', + 'twitter' => 'service-twitter-mokosuitecross', + 'linkedin' => 'service-linkedin-mokosuitecross', + 'mastodon' => 'service-mastodon-mokosuitecross', + 'bluesky' => 'service-bluesky-mokosuitecross', + 'threads' => 'service-threads-mokosuitecross', + 'pinterest' => 'service-pinterest-mokosuitecross', + 'reddit' => 'service-reddit-mokosuitecross', + 'tumblr' => 'service-tumblr-mokosuitecross', + 'tiktok' => 'service-tiktok-mokosuitecross', + 'nostr' => 'service-nostr-mokosuitecross', + 'activitypub' => 'service-activitypub-mokosuitecross', + 'telegram' => 'service-telegram-mokosuitecross', + 'discord' => 'service-discord-mokosuitecross', + 'slack' => 'service-slack-mokosuitecross', + 'teams' => 'service-teams-mokosuitecross', + 'googlechat' => 'service-googlechat-mokosuitecross', + 'whatsapp' => 'service-whatsapp-mokosuitecross', + 'matrix' => 'service-matrix-mokosuitecross', + 'ntfy' => 'service-ntfy-mokosuitecross', + 'mailchimp' => 'service-mailchimp-mokosuitecross', + 'sendgrid' => 'service-sendgrid-mokosuitecross', + 'brevo' => 'service-brevo-mokosuitecross', + 'convertkit' => 'service-convertkit-mokosuitecross', + 'constantcontact' => 'service-constantcontact-mokosuitecross', + 'medium' => 'service-medium-mokosuitecross', + 'wordpress' => 'service-wordpress-mokosuitecross', + 'devto' => 'service-devto-mokosuitecross', + 'ghost' => 'service-ghost-mokosuitecross', + 'hashnode' => 'service-hashnode-mokosuitecross', + 'blogger' => 'service-blogger-mokosuitecross', + 'googlebusiness' => 'service-googlebusiness-mokosuitecross', + 'webhook' => 'service-webhook-mokosuitecross', + 'rssfeed' => 'service-rssfeed-mokosuitecross', +]; +$helpAlias = $helpArticles[$serviceType] ?? ''; +?> +
+ +
+
+
+

+ form->renderFieldset('details'); ?> + +
+ +

+

+ +

+ form->renderFieldset('credentials'); ?> + + +
+ + + + +

+ +

+
+ +
+ +
+
+
+
+ + +
+
+
+

+
    +
  1. +
  2. +
  3. +
  4. +
+
+
+ + +
+ +
+ + + +
+
+
+ + +
+
+
+

+
+
+ + + 0 && !empty($serviceType)) : ?> +
+
+
+ + +
+
+
+

+ + +
+
+ + +
+
+
+ + + +
diff --git a/src/packages/index.html b/source/packages/com_mokosuitecross/tmpl/service/index.html similarity index 100% rename from src/packages/index.html rename to source/packages/com_mokosuitecross/tmpl/service/index.html diff --git a/source/packages/com_mokosuitecross/tmpl/services/default.php b/source/packages/com_mokosuitecross/tmpl/services/default.php new file mode 100644 index 0000000..dae05c0 --- /dev/null +++ b/source/packages/com_mokosuitecross/tmpl/services/default.php @@ -0,0 +1,109 @@ + + * @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\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Layout\LayoutHelper; +use Joomla\CMS\Router\Route; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\ServiceIconHelper; + +/** @var \Joomla\Component\MokoSuiteCross\Administrator\View\Services\HtmlView $this */ + +HTMLHelper::_('behavior.multiselect'); + +$listOrder = $this->escape($this->state->get('list.ordering')); +$listDirn = $this->escape($this->state->get('list.direction')); + +?> +
+
+
+
+ $this]); ?> + + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + items as $i => $item) : + $credentials = json_decode($item->credentials ?: '{}', true) ?: []; + $mode = $credentials['mode'] ?? 'custom'; + ?> + + + + + + + + + + +
+ + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->title); ?> + + published, $i, 'services.', true); ?> + + + escape($item->title); ?> + + + service_type); ?> + escape(ucfirst($item->service_type)); ?> + + + Default Bot + + Custom + + + id; ?> +
+ + pagination->getListFooter(); ?> + + + + + +
+
+
+
diff --git a/source/packages/com_mokosuitecross/tmpl/services/default_service.php b/source/packages/com_mokosuitecross/tmpl/services/default_service.php new file mode 100644 index 0000000..4f9572b --- /dev/null +++ b/source/packages/com_mokosuitecross/tmpl/services/default_service.php @@ -0,0 +1,44 @@ + + * @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\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; + +/** @var \Joomla\CMS\Form\Form $this->form */ + +HTMLHelper::_('behavior.formvalidator'); +HTMLHelper::_('behavior.keepalive'); +?> +
+ +
+
+
+ form->renderFieldset('details'); ?> +
+
+
+
+

+ form->renderFieldset('credentials'); ?> +
+
+
+
+
+ + + +
diff --git a/src/packages/plg_content_mokojoomcross/index.html b/source/packages/com_mokosuitecross/tmpl/services/index.html similarity index 100% rename from src/packages/plg_content_mokojoomcross/index.html rename to source/packages/com_mokosuitecross/tmpl/services/index.html diff --git a/source/packages/com_mokosuitecross/tmpl/servicestats/default.php b/source/packages/com_mokosuitecross/tmpl/servicestats/default.php new file mode 100644 index 0000000..e2c6afc --- /dev/null +++ b/source/packages/com_mokosuitecross/tmpl/servicestats/default.php @@ -0,0 +1,219 @@ + + * @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\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\ServiceIconHelper; + +/** @var \Joomla\Component\MokoSuiteCross\Administrator\View\ServiceStats\HtmlView $this */ + +$service = $this->service; +$stats = $this->postStats; +$rate = $stats->total > 0 ? round(($stats->posted / $stats->total) * 100) : 0; +$rateClass = $rate >= 80 ? 'text-success' : ($rate >= 50 ? 'text-warning' : 'text-danger'); + +$statusBadges = [ + 'queued' => 'bg-warning text-dark', + 'posting' => 'bg-info', + 'posted' => 'bg-success', + 'failed' => 'bg-danger', + 'scheduled' => 'bg-secondary', +]; +?> + + +
+ service_type, 'fs-3 me-2'); ?> +

escape($service->title); ?>

+ escape(ucfirst($service->service_type)); ?> +
+ + +
+
+
+
+
+

total; ?>

+
+
+
+
+
+
+
+

posted; ?>

+
+
+
+
+
+
+
+

failed; ?>

+
+
+
+
+
+
+
+

%

+
+
+
+
+ + +dailyTrend)) : ?> +
+
+
+
+ + + + +
+
+
+ +
+
+ + + + + +
+
+
+
+
+ recentPosts)) : ?> +

+ + + + + + + + + + + + recentPosts as $post) : + $badgeClass = $statusBadges[$post['status']] ?? 'bg-secondary'; + ?> + + + + + + + + +
+ + escape(ucfirst($post['status'])); ?> + + 0) : ?> +
Retries: + +
+ + escape($post['article_title'] ?? 'Article #' . $post['id']); ?> + + + + + + escape(mb_substr($post['error_message'], 0, 100)); ?> + + — + +
+ +
+
+ + +topArticles)) : ?> +
+
+
+
+
+
+ topArticles as $row) : ?> +
+ + + + / + + +
+ +
+
+
+ diff --git a/source/packages/com_mokosuitecross/tmpl/template/edit.php b/source/packages/com_mokosuitecross/tmpl/template/edit.php new file mode 100644 index 0000000..687bbdb --- /dev/null +++ b/source/packages/com_mokosuitecross/tmpl/template/edit.php @@ -0,0 +1,114 @@ + + * @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\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; + +/** @var \Joomla\Component\MokoSuiteCross\Administrator\View\Template\HtmlView $this */ + +HTMLHelper::_('behavior.formvalidator'); +HTMLHelper::_('behavior.keepalive'); +?> +
+ +
+
+
+ form->renderFieldset('details'); ?> +
+
+
+
+
+
+
+ + + + + + + + + + + + + + +
{title}
{url}
{introtext}
{fulltext}
{image}
{category}
{author}
{date}
{tags}
{hashtags}
{field:xxx}
+
+
+
+
+
+ + + +
+ + diff --git a/source/packages/com_mokosuitecross/tmpl/template/index.html b/source/packages/com_mokosuitecross/tmpl/template/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/com_mokosuitecross/tmpl/template/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/com_mokosuitecross/tmpl/templates/default.php b/source/packages/com_mokosuitecross/tmpl/templates/default.php new file mode 100644 index 0000000..5ebc8b4 --- /dev/null +++ b/source/packages/com_mokosuitecross/tmpl/templates/default.php @@ -0,0 +1,103 @@ + + * @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\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Layout\LayoutHelper; +use Joomla\CMS\Router\Route; + +/** @var \Joomla\Component\MokoSuiteCross\Administrator\View\Templates\HtmlView $this */ + +HTMLHelper::_('behavior.multiselect'); + +$listOrder = $this->escape($this->state->get('list.ordering')); +$listDirn = $this->escape($this->state->get('list.direction')); +?> +
+
+
+
+ $this]); ?> + + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + + + +
+ + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->title); ?> + + published, $i, 'templates.', true); ?> + + + escape($item->title); ?> + + + service_type === 'default') : ?> + Default + + escape(ucfirst($item->service_type)); ?> + + + escape(mb_substr($item->template_body, 0, 80)); ?> + + id; ?> +
+ + pagination->getListFooter(); ?> + + + + + +
+
+
+
diff --git a/source/packages/com_mokosuitecross/tmpl/templates/index.html b/source/packages/com_mokosuitecross/tmpl/templates/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/com_mokosuitecross/tmpl/templates/index.html @@ -0,0 +1 @@ + diff --git a/src/packages/plg_content_mokojoomcross/language/index.html b/source/packages/plg_content_mokosuitecross/index.html similarity index 100% rename from src/packages/plg_content_mokojoomcross/language/index.html rename to source/packages/plg_content_mokosuitecross/index.html diff --git a/src/packages/plg_content_mokojoomcross/services/index.html b/source/packages/plg_content_mokosuitecross/language/en-GB/index.html similarity index 100% rename from src/packages/plg_content_mokojoomcross/services/index.html rename to source/packages/plg_content_mokosuitecross/language/en-GB/index.html diff --git a/source/packages/plg_content_mokosuitecross/language/en-GB/plg_content_mokosuitecross.ini b/source/packages/plg_content_mokosuitecross/language/en-GB/plg_content_mokosuitecross.ini new file mode 100644 index 0000000..dd1b16d --- /dev/null +++ b/source/packages/plg_content_mokosuitecross/language/en-GB/plg_content_mokosuitecross.ini @@ -0,0 +1,13 @@ +PLG_CONTENT_MOKOSUITECROSS="Content - MokoSuiteCross" +PLG_CONTENT_MOKOSUITECROSS_DESCRIPTION="Adds cross-post status badges and per-article service selection to the article editor." + +PLG_CONTENT_MOKOSUITECROSS_FIELDSET_CROSSPOST="Cross-Posting" +PLG_CONTENT_MOKOSUITECROSS_SKIP="Skip Cross-Posting" +PLG_CONTENT_MOKOSUITECROSS_SKIP_DESC="Skip all cross-posting for this article." +PLG_CONTENT_MOKOSUITECROSS_SERVICES="Post to Services" +PLG_CONTENT_MOKOSUITECROSS_SERVICES_DESC="Select which services to cross-post to. Leave all unchecked to post to all enabled services." +PLG_CONTENT_MOKOSUITECROSS_EVERGREEN="Evergreen Content" +PLG_CONTENT_MOKOSUITECROSS_EVERGREEN_DESC="Automatically re-share this article on a recurring schedule. Great for high-value content that stays relevant." +PLG_CONTENT_MOKOSUITECROSS_EVERGREEN_INTERVAL="Re-share Interval (days)" +PLG_CONTENT_MOKOSUITECROSS_EVERGREEN_INTERVAL_DESC="How many days to wait between automatic re-shares. Default: 30 days." +PLG_CONTENT_MOKOSUITECROSS_HISTORY="Cross-Post History" diff --git a/source/packages/plg_content_mokosuitecross/language/en-GB/plg_content_mokosuitecross.sys.ini b/source/packages/plg_content_mokosuitecross/language/en-GB/plg_content_mokosuitecross.sys.ini new file mode 100644 index 0000000..d165f00 --- /dev/null +++ b/source/packages/plg_content_mokosuitecross/language/en-GB/plg_content_mokosuitecross.sys.ini @@ -0,0 +1,2 @@ +PLG_CONTENT_MOKOSUITECROSS="Content - MokoSuiteCross" +PLG_CONTENT_MOKOSUITECROSS_DESCRIPTION="Adds cross-post status badges to articles in the admin backend." diff --git a/src/packages/plg_content_mokojoomcross/src/Extension/index.html b/source/packages/plg_content_mokosuitecross/language/index.html similarity index 100% rename from src/packages/plg_content_mokojoomcross/src/Extension/index.html rename to source/packages/plg_content_mokosuitecross/language/index.html diff --git a/source/packages/plg_content_mokosuitecross/mokosuitecross.php b/source/packages/plg_content_mokosuitecross/mokosuitecross.php new file mode 100644 index 0000000..f540464 --- /dev/null +++ b/source/packages/plg_content_mokosuitecross/mokosuitecross.php @@ -0,0 +1,12 @@ + + * @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/src/packages/plg_content_mokojoomcross/mokojoomcross.xml b/source/packages/plg_content_mokosuitecross/mokosuitecross.xml similarity index 55% rename from src/packages/plg_content_mokojoomcross/mokojoomcross.xml rename to source/packages/plg_content_mokosuitecross/mokosuitecross.xml index cc0fade..20fb72b 100644 --- a/src/packages/plg_content_mokojoomcross/mokojoomcross.xml +++ b/source/packages/plg_content_mokosuitecross/mokosuitecross.xml @@ -1,26 +1,26 @@ - Content - MokoJoomCross - 01.01.00 + Content - MokoSuiteCross + 01.00.27-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech https://mokoconsulting.tech Copyright (C) 2026 Moko Consulting. All rights reserved. GPL-3.0-or-later - PLG_CONTENT_MOKOJOOMCROSS_DESCRIPTION + PLG_CONTENT_MOKOSUITECROSS_DESCRIPTION - Joomla\Plugin\Content\MokoJoomCross + Joomla\Plugin\Content\MokoSuiteCross - mokojoomcross.php + mokosuitecross.php src services language - language/en-GB/plg_content_mokojoomcross.ini - language/en-GB/plg_content_mokojoomcross.sys.ini + language/en-GB/plg_content_mokosuitecross.ini + language/en-GB/plg_content_mokosuitecross.sys.ini diff --git a/src/packages/plg_content_mokojoomcross/src/index.html b/source/packages/plg_content_mokosuitecross/services/index.html similarity index 100% rename from src/packages/plg_content_mokojoomcross/src/index.html rename to source/packages/plg_content_mokosuitecross/services/index.html diff --git a/src/packages/plg_content_mokojoomcross/services/provider.php b/source/packages/plg_content_mokosuitecross/services/provider.php similarity index 82% rename from src/packages/plg_content_mokojoomcross/services/provider.php rename to source/packages/plg_content_mokosuitecross/services/provider.php index 07cfb0b..2f1b8cf 100644 --- a/src/packages/plg_content_mokojoomcross/services/provider.php +++ b/source/packages/plg_content_mokosuitecross/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -17,7 +17,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\Content\MokoJoomCross\Extension\MokoJoomCrossContent; +use Joomla\Plugin\Content\MokoSuiteCross\Extension\MokoSuiteCrossContent; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -25,9 +25,9 @@ return new class () implements ServiceProviderInterface { $container->set( PluginInterface::class, function (Container $container) { - $plugin = new MokoJoomCrossContent( + $plugin = new MokoSuiteCrossContent( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('content', 'mokojoomcross') + (array) PluginHelper::getPlugin('content', 'mokosuitecross') ); $plugin->setApplication(Factory::getApplication()); diff --git a/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php b/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php new file mode 100644 index 0000000..cf5a069 --- /dev/null +++ b/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php @@ -0,0 +1,361 @@ + + * @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\Content\MokoSuiteCross\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Event\Model\PrepareFormEvent; +use Joomla\CMS\Factory; +use Joomla\CMS\Form\Form; +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\CMS\Uri\Uri; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\CrossPostDispatcher; +use Joomla\Event\SubscriberInterface; + +/** + * Content plugin that: + * 1. Adds cross-post status badges to article views in admin + * 2. Injects service selection checkboxes into the article editor (#19) + */ +class MokoSuiteCrossContent extends CMSPlugin implements SubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + 'onContentBeforeDisplay' => 'onContentBeforeDisplay', + 'onContentPrepareForm' => 'onContentPrepareForm', + 'onContentAfterSave' => 'onContentAfterSave', + 'onContentChangeState' => 'onContentChangeState', + ]; + } + + /** + * Inject cross-post service selection fields into article edit form. + * + * Adds a "Cross-Posting" fieldset to the article attribs tab with: + * - Checkbox list of all enabled services + * - Skip cross-posting toggle + */ + /** + * Joomla 5/6 compatible — accepts both PrepareFormEvent and legacy Form signature. + */ + public function onContentPrepareForm($event): void + { + // Joomla 5+ passes PrepareFormEvent; extract the Form from it + if ($event instanceof PrepareFormEvent) { + $form = $event->getForm(); + } elseif ($event instanceof Form) { + $form = $event; + } else { + return; + } + + if ($form->getName() !== 'com_content.article') { + return; + } + + $app = $this->getApplication(); + + if (!$app->isClient('administrator')) { + return; + } + + $db = Factory::getDbo(); + + // Load enabled services for the checkbox list + $query = $db->getQuery(true) + ->select('id, title, service_type') + ->from($db->quoteName('#__mokosuitecross_services')) + ->where($db->quoteName('published') . ' = 1') + ->order($db->quoteName('ordering') . ' ASC'); + + $db->setQuery($query); + $services = $db->loadObjectList(); + + if (empty($services)) { + return; + } + + // Build dynamic XML form for the attribs fieldset + $options = ''; + + foreach ($services as $svc) { + $label = htmlspecialchars($svc->title . ' (' . ucfirst($svc->service_type) . ')', ENT_XML1); + $options .= ''; + } + + $xml = << +
+ +
+ + + + + + {$options} + + + + + + +
+
+
+XML; + + $form->load($xml); + + // Cross-post history panel for existing articles + $articleId = Factory::getApplication()->input->getInt('id', 0); + + if ($articleId > 0) { + $query = $db->getQuery(true) + ->select('p.status, p.posted_at, p.error_message, s.title AS service_title, s.service_type') + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->join('LEFT', $db->quoteName('#__mokosuitecross_services', 's') . ' ON s.id = p.service_id') + ->where($db->quoteName('p.article_id') . ' = ' . $articleId) + ->order('p.created DESC'); + $db->setQuery($query, 0, 10); + $history = $db->loadObjectList(); + + if (!empty($history)) { + $historyHtml = '
'; + + foreach ($history as $post) { + $badgeClass = match ($post->status) { + 'posted' => 'bg-success', + 'failed' => 'bg-danger', + 'queued' => 'bg-warning', + default => 'bg-secondary', + }; + $historyHtml .= '
' + . '' . ucfirst($post->status) . '' + . '' . htmlspecialchars($post->service_title ?? '') . ''; + + if ($post->posted_at) { + $historyHtml .= ' ' . HTMLHelper::_('date', $post->posted_at, 'Y-m-d H:i') . ''; + } + + if ($post->status === 'failed' && $post->error_message) { + $historyHtml .= '
' . htmlspecialchars(mb_substr($post->error_message, 0, 60)) . ''; + } + + $historyHtml .= '
'; + } + + $historyHtml .= '
'; + + // Add the note field first with an empty description, then set the + // description via setFieldAttribute() to avoid double-escaping. + // Putting raw HTML into an XML attribute via htmlspecialchars() causes + // Joomla's note field renderer to display escaped tags since it outputs + // the description as raw HTML. + $historyXml = ' +
+ +
'; + $form->load($historyXml); + $form->setFieldAttribute('mokosuitecross_history', 'description', $historyHtml, 'attribs'); + } + } + } + + /** + * Add cross-post status badges before article content in admin. + * + * Joomla 5/6 compatible — accepts both BeforeDisplayEvent and legacy parameters. + */ + public function onContentBeforeDisplay($event): string + { + // Joomla 5/6 compatibility + if ($event instanceof \Joomla\CMS\Event\Content\BeforeDisplayEvent) { + $context = $event->getContext(); + $article = $event->getItem(); + } elseif (is_string($event)) { + $context = $event; + $article = func_get_arg(1); + } else { + return ''; + } + + if ($context !== 'com_content.article') { + return ''; + } + + $app = $this->getApplication(); + + if (!$app->isClient('administrator')) { + return ''; + } + + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select('p.status, s.service_type') + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->join('LEFT', $db->quoteName('#__mokosuitecross_services', 's') + . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')) + ->where($db->quoteName('p.article_id') . ' = ' . (int) $article->id) + ->order($db->quoteName('p.created') . ' DESC'); + + $db->setQuery($query, 0, 10); + $posts = $db->loadObjectList(); + + if (empty($posts)) { + return ''; + } + + $badges = ''; + + foreach ($posts as $post) { + $class = match ($post->status) { + 'posted' => 'badge bg-success', + 'failed' => 'badge bg-danger', + 'queued' => 'badge bg-warning', + default => 'badge bg-secondary', + }; + $badges .= '' . htmlspecialchars($post->service_type) . ''; + } + + return '
' . $badges . '
'; + } + + /** + * Dispatch cross-post when an article is saved and published. + * + * Joomla 5/6 compatible — accepts both AfterSaveEvent and legacy parameters. + */ + public function onContentAfterSave($event): void + { + // Joomla 5/6 compatibility + if ($event instanceof \Joomla\CMS\Event\Content\AfterSaveEvent) { + $context = $event->getContext(); + $article = $event->getItem(); + $isNew = $event->getIsNew(); + } else { + $context = $event; + $article = func_get_arg(1); + $isNew = func_get_arg(2); + } + + if ($context !== 'com_content.article') { + return; + } + + if ((int) ($article->state ?? 0) !== 1) { + return; + } + + $params = ComponentHelper::getParams('com_mokosuitecross'); + + if (!$params->get('auto_post_on_publish', 1)) { + return; + } + + if ($params->get('post_on_first_publish_only', 0) && !$isNew) { + return; + } + + $url = Uri::root() . 'index.php?option=com_content&view=article&id=' . $article->id; + + if (!empty($article->catid)) { + $url .= '&catid=' . $article->catid; + } + + CrossPostDispatcher::dispatch($article, $url, 'com_content.article'); + } + + /** + * Dispatch cross-post when article state changes to published. + * + * Joomla 5/6 compatible — accepts both ContentChangeStateEvent and legacy parameters. + */ + public function onContentChangeState($event): void + { + if ($event instanceof \Joomla\CMS\Event\Content\ContentChangeStateEvent) { + $context = $event->getContext(); + $pks = $event->getPks(); + $value = $event->getValue(); + } else { + $context = $event; + $pks = func_get_arg(1); + $value = func_get_arg(2); + } + + if ($context !== 'com_content.article' || $value !== 1) { + return; + } + + $params = ComponentHelper::getParams('com_mokosuitecross'); + + if (!$params->get('auto_post_on_publish', 1)) { + return; + } + + $db = Factory::getDbo(); + + foreach ($pks as $pk) { + $query = $db->getQuery(true) + ->select('*') + ->from('#__content') + ->where('id = ' . (int) $pk); + $db->setQuery($query); + $article = $db->loadObject(); + + if (!$article) { + continue; + } + + $url = Uri::root() . 'index.php?option=com_content&view=article&id=' . $article->id; + + if (!empty($article->catid)) { + $url .= '&catid=' . $article->catid; + } + + CrossPostDispatcher::dispatch($article, $url, 'com_content.article'); + } + } +} diff --git a/src/packages/plg_mokojoomcross_bluesky/index.html b/source/packages/plg_content_mokosuitecross/src/Extension/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_bluesky/index.html rename to source/packages/plg_content_mokosuitecross/src/Extension/index.html diff --git a/src/packages/plg_mokojoomcross_bluesky/language/en-GB/index.html b/source/packages/plg_content_mokosuitecross/src/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_bluesky/language/en-GB/index.html rename to source/packages/plg_content_mokosuitecross/src/index.html diff --git a/src/packages/plg_system_mokojoomcross/mokojoomcross.php b/source/packages/plg_mokosuitecross_activitypub/activitypub.php similarity index 80% rename from src/packages/plg_system_mokojoomcross/mokojoomcross.php rename to source/packages/plg_mokosuitecross_activitypub/activitypub.php index ffae23a..9b76408 100644 --- a/src/packages/plg_system_mokojoomcross/mokojoomcross.php +++ b/source/packages/plg_mokosuitecross_activitypub/activitypub.php @@ -1,8 +1,7 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE diff --git a/source/packages/plg_mokosuitecross_activitypub/activitypub.xml b/source/packages/plg_mokosuitecross_activitypub/activitypub.xml new file mode 100644 index 0000000..7a8a257 --- /dev/null +++ b/source/packages/plg_mokosuitecross_activitypub/activitypub.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - ActivityPub (Fediverse) + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_ACTIVITYPUB_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Activitypub + + + activitypub.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_activitypub.ini + language/en-GB/plg_mokosuitecross_activitypub.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_activitypub/index.html b/source/packages/plg_mokosuitecross_activitypub/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_activitypub/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_activitypub/language/en-GB/index.html b/source/packages/plg_mokosuitecross_activitypub/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_activitypub/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_activitypub/language/en-GB/plg_mokosuitecross_activitypub.ini b/source/packages/plg_mokosuitecross_activitypub/language/en-GB/plg_mokosuitecross_activitypub.ini new file mode 100644 index 0000000..c775431 --- /dev/null +++ b/source/packages/plg_mokosuitecross_activitypub/language/en-GB/plg_mokosuitecross_activitypub.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_ACTIVITYPUB="MokoSuiteCross - ActivityPub (Fediverse)" +PLG_MOKOSUITECROSS_ACTIVITYPUB_DESCRIPTION="Cross-post Joomla articles to ActivityPub (Fediverse)." diff --git a/source/packages/plg_mokosuitecross_activitypub/language/en-GB/plg_mokosuitecross_activitypub.sys.ini b/source/packages/plg_mokosuitecross_activitypub/language/en-GB/plg_mokosuitecross_activitypub.sys.ini new file mode 100644 index 0000000..c775431 --- /dev/null +++ b/source/packages/plg_mokosuitecross_activitypub/language/en-GB/plg_mokosuitecross_activitypub.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_ACTIVITYPUB="MokoSuiteCross - ActivityPub (Fediverse)" +PLG_MOKOSUITECROSS_ACTIVITYPUB_DESCRIPTION="Cross-post Joomla articles to ActivityPub (Fediverse)." diff --git a/source/packages/plg_mokosuitecross_activitypub/language/index.html b/source/packages/plg_mokosuitecross_activitypub/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_activitypub/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_activitypub/services/index.html b/source/packages/plg_mokosuitecross_activitypub/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_activitypub/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_activitypub/services/provider.php b/source/packages/plg_mokosuitecross_activitypub/services/provider.php new file mode 100644 index 0000000..f35a345 --- /dev/null +++ b/source/packages/plg_mokosuitecross_activitypub/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\Activitypub\Extension\ActivitypubService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new ActivitypubService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'activitypub') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_activitypub/src/Extension/ActivitypubService.php b/source/packages/plg_mokosuitecross_activitypub/src/Extension/ActivitypubService.php new file mode 100644 index 0000000..b56a5b4 --- /dev/null +++ b/source/packages/plg_mokosuitecross_activitypub/src/Extension/ActivitypubService.php @@ -0,0 +1,132 @@ + + * @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\Activitypub\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * ActivityPub (Fediverse) service plugin for MokoSuiteCross. + * + * Works with Mastodon-compatible APIs (Pleroma, Akkoma, Misskey, Pixelfed). + * Uses the /api/v1/statuses endpoint with Bearer token auth. + */ +class ActivitypubService 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 'activitypub'; } + public function getServiceName(): string { return 'ActivityPub (Fediverse)'; } + public function getMaxLength(): int { return 500; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $instanceUrl = rtrim($credentials['instance_url'] ?? '', '/'); + $token = $credentials['access_token'] ?? ''; + + if (empty($instanceUrl) || empty($token)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing instance URL or access token.']]; + } + + $apiUrl = $instanceUrl . '/api/v1/statuses'; + $payload = json_encode(['status' => mb_substr($message, 0, 500)]); + + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + 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 && !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 + { + $instanceUrl = rtrim($credentials['instance_url'] ?? '', '/'); + $token = $credentials['access_token'] ?? ''; + + if (empty($instanceUrl) || empty($token)) { + return ['valid' => false, 'message' => 'Instance URL and access token are required.', 'account_name' => '']; + } + + $ch = curl_init($instanceUrl . '/api/v1/accounts/verify_credentials'); + 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['username'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => '@' . $data['username'] . '@' . parse_url($instanceUrl, PHP_URL_HOST)]; + } + + return ['valid' => false, 'message' => $data['error'] ?? 'Failed to verify credentials.', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image', 'video']; + } +} diff --git a/source/packages/plg_mokosuitecross_activitypub/src/Extension/index.html b/source/packages/plg_mokosuitecross_activitypub/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_activitypub/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_activitypub/src/index.html b/source/packages/plg_mokosuitecross_activitypub/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_activitypub/src/index.html @@ -0,0 +1 @@ + diff --git a/src/packages/plg_content_mokojoomcross/mokojoomcross.php b/source/packages/plg_mokosuitecross_blogger/blogger.php similarity index 80% rename from src/packages/plg_content_mokojoomcross/mokojoomcross.php rename to source/packages/plg_mokosuitecross_blogger/blogger.php index 8edfa28..9b76408 100644 --- a/src/packages/plg_content_mokojoomcross/mokojoomcross.php +++ b/source/packages/plg_mokosuitecross_blogger/blogger.php @@ -1,8 +1,7 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE diff --git a/source/packages/plg_mokosuitecross_blogger/blogger.xml b/source/packages/plg_mokosuitecross_blogger/blogger.xml new file mode 100644 index 0000000..42cf014 --- /dev/null +++ b/source/packages/plg_mokosuitecross_blogger/blogger.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - Google Blogger + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_BLOGGER_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Blogger + + + blogger.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_blogger.ini + language/en-GB/plg_mokosuitecross_blogger.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_blogger/index.html b/source/packages/plg_mokosuitecross_blogger/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_blogger/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_blogger/language/en-GB/index.html b/source/packages/plg_mokosuitecross_blogger/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_blogger/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_blogger/language/en-GB/plg_mokosuitecross_blogger.ini b/source/packages/plg_mokosuitecross_blogger/language/en-GB/plg_mokosuitecross_blogger.ini new file mode 100644 index 0000000..3033058 --- /dev/null +++ b/source/packages/plg_mokosuitecross_blogger/language/en-GB/plg_mokosuitecross_blogger.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_BLOGGER="MokoSuiteCross - Google Blogger" +PLG_MOKOSUITECROSS_BLOGGER_DESCRIPTION="Cross-post Joomla articles to Google Blogger." diff --git a/source/packages/plg_mokosuitecross_blogger/language/en-GB/plg_mokosuitecross_blogger.sys.ini b/source/packages/plg_mokosuitecross_blogger/language/en-GB/plg_mokosuitecross_blogger.sys.ini new file mode 100644 index 0000000..3033058 --- /dev/null +++ b/source/packages/plg_mokosuitecross_blogger/language/en-GB/plg_mokosuitecross_blogger.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_BLOGGER="MokoSuiteCross - Google Blogger" +PLG_MOKOSUITECROSS_BLOGGER_DESCRIPTION="Cross-post Joomla articles to Google Blogger." diff --git a/source/packages/plg_mokosuitecross_blogger/language/index.html b/source/packages/plg_mokosuitecross_blogger/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_blogger/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_blogger/services/index.html b/source/packages/plg_mokosuitecross_blogger/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_blogger/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_blogger/services/provider.php b/source/packages/plg_mokosuitecross_blogger/services/provider.php new file mode 100644 index 0000000..9f1fb72 --- /dev/null +++ b/source/packages/plg_mokosuitecross_blogger/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\Blogger\Extension\BloggerService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new BloggerService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'blogger') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_blogger/src/Extension/BloggerService.php b/source/packages/plg_mokosuitecross_blogger/src/Extension/BloggerService.php new file mode 100644 index 0000000..33b065a --- /dev/null +++ b/source/packages/plg_mokosuitecross_blogger/src/Extension/BloggerService.php @@ -0,0 +1,135 @@ + + * @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\Blogger\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Google Blogger service plugin for MokoSuiteCross. + * + * Uses the Blogger API v3 with OAuth Bearer token. + */ +class BloggerService 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 'blogger'; } + public function getServiceName(): string { return 'Google Blogger'; } + public function getMaxLength(): int { return 0; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $token = $credentials['access_token'] ?? ''; + $blogId = $credentials['blog_id'] ?? ''; + + if (empty($token) || empty($blogId)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or blog ID.']]; + } + + $apiUrl = 'https://www.googleapis.com/blogger/v3/blogs/' . urlencode($blogId) . '/posts'; + $payload = json_encode([ + 'kind' => 'blogger#post', + 'title' => mb_substr(strip_tags($message), 0, 150), + 'content' => $message, + ]); + + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + 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 && !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 = $credentials['access_token'] ?? ''; + $blogId = $credentials['blog_id'] ?? ''; + + if (empty($token) || empty($blogId)) { + return ['valid' => false, 'message' => 'Access token and blog ID are required.', 'account_name' => '']; + } + + $ch = curl_init('https://www.googleapis.com/blogger/v3/blogs/' . urlencode($blogId)); + 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['name'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['name']]; + } + + return ['valid' => false, 'message' => $data['error']['message'] ?? 'Failed to verify credentials.', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_blogger/src/Extension/index.html b/source/packages/plg_mokosuitecross_blogger/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_blogger/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_blogger/src/index.html b/source/packages/plg_mokosuitecross_blogger/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_blogger/src/index.html @@ -0,0 +1 @@ + diff --git a/src/packages/plg_mokojoomcross_bluesky/bluesky.php b/source/packages/plg_mokosuitecross_bluesky/bluesky.php similarity index 90% rename from src/packages/plg_mokojoomcross_bluesky/bluesky.php rename to source/packages/plg_mokosuitecross_bluesky/bluesky.php index f74ab97..9b76408 100644 --- a/src/packages/plg_mokojoomcross_bluesky/bluesky.php +++ b/source/packages/plg_mokosuitecross_bluesky/bluesky.php @@ -1,7 +1,7 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE diff --git a/source/packages/plg_mokosuitecross_bluesky/bluesky.xml b/source/packages/plg_mokosuitecross_bluesky/bluesky.xml new file mode 100644 index 0000000..1087eb8 --- /dev/null +++ b/source/packages/plg_mokosuitecross_bluesky/bluesky.xml @@ -0,0 +1,51 @@ + + + MokoSuiteCross - Bluesky + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_BLUESKY_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross${CLASS_NAME} + + + bluesky.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_bluesky.ini + language/en-GB/plg_mokosuitecross_bluesky.sys.ini + + + + +
+ + + + + +
+
+
+
diff --git a/src/packages/plg_mokojoomcross_bluesky/language/index.html b/source/packages/plg_mokosuitecross_bluesky/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_bluesky/language/index.html rename to source/packages/plg_mokosuitecross_bluesky/index.html diff --git a/src/packages/plg_mokojoomcross_bluesky/services/index.html b/source/packages/plg_mokosuitecross_bluesky/language/en-GB/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_bluesky/services/index.html rename to source/packages/plg_mokosuitecross_bluesky/language/en-GB/index.html diff --git a/source/packages/plg_mokosuitecross_bluesky/language/en-GB/plg_mokosuitecross_bluesky.ini b/source/packages/plg_mokosuitecross_bluesky/language/en-GB/plg_mokosuitecross_bluesky.ini new file mode 100644 index 0000000..894c483 --- /dev/null +++ b/source/packages/plg_mokosuitecross_bluesky/language/en-GB/plg_mokosuitecross_bluesky.ini @@ -0,0 +1,7 @@ +PLG_MOKOSUITECROSS_BLUESKY="MokoSuiteCross - Bluesky" +PLG_MOKOSUITECROSS_BLUESKY_DESCRIPTION="Cross-post Joomla articles to Bluesky." +PLG_MOKOSUITECROSS_BLUESKY_FIELDSET_DEFAULTS="Bluesky Defaults" +PLG_MOKOSUITECROSS_BLUESKY_DEFAULT_PDS_URL="Default PDS URL" +PLG_MOKOSUITECROSS_BLUESKY_DEFAULT_PDS_URL_DESC="Default Bluesky PDS URL (e.g. https://bsky.social)." +PLG_MOKOSUITECROSS_BLUESKY_AUTO_LINK_CARD="Auto Link Card" +PLG_MOKOSUITECROSS_BLUESKY_AUTO_LINK_CARD_DESC="Automatically detect URLs and create link cards in posts." diff --git a/source/packages/plg_mokosuitecross_bluesky/language/en-GB/plg_mokosuitecross_bluesky.sys.ini b/source/packages/plg_mokosuitecross_bluesky/language/en-GB/plg_mokosuitecross_bluesky.sys.ini new file mode 100644 index 0000000..d3c6b77 --- /dev/null +++ b/source/packages/plg_mokosuitecross_bluesky/language/en-GB/plg_mokosuitecross_bluesky.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_BLUESKY="MokoSuiteCross - Bluesky" +PLG_MOKOSUITECROSS_BLUESKY_DESCRIPTION="Cross-post Joomla articles to Bluesky." diff --git a/src/packages/plg_mokojoomcross_bluesky/src/Extension/index.html b/source/packages/plg_mokosuitecross_bluesky/language/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_bluesky/src/Extension/index.html rename to source/packages/plg_mokosuitecross_bluesky/language/index.html diff --git a/src/packages/plg_mokojoomcross_bluesky/src/index.html b/source/packages/plg_mokosuitecross_bluesky/services/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_bluesky/src/index.html rename to source/packages/plg_mokosuitecross_bluesky/services/index.html diff --git a/src/packages/plg_mokojoomcross_bluesky/services/provider.php b/source/packages/plg_mokosuitecross_bluesky/services/provider.php similarity index 81% rename from src/packages/plg_mokojoomcross_bluesky/services/provider.php rename to source/packages/plg_mokosuitecross_bluesky/services/provider.php index e441098..d2b1245 100644 --- a/src/packages/plg_mokojoomcross_bluesky/services/provider.php +++ b/source/packages/plg_mokosuitecross_bluesky/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -17,7 +17,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\MokoJoomCross${CLASS_NAME}\Extension${CLASS_NAME}Service; +use Joomla\Plugin\MokoSuiteCross\Bluesky\Extension\BlueskyService; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -27,7 +27,7 @@ return new class () implements ServiceProviderInterface { function (Container $container) { $plugin = new BlueskyService( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('mokojoomcross', 'bluesky') + (array) PluginHelper::getPlugin('mokosuitecross', 'bluesky') ); $plugin->setApplication(Factory::getApplication()); diff --git a/src/packages/plg_mokojoomcross_bluesky/src/Extension/BlueskyService.php b/source/packages/plg_mokosuitecross_bluesky/src/Extension/BlueskyService.php similarity index 66% rename from src/packages/plg_mokojoomcross_bluesky/src/Extension/BlueskyService.php rename to source/packages/plg_mokosuitecross_bluesky/src/Extension/BlueskyService.php index b9d4711..7efb369 100644 --- a/src/packages/plg_mokojoomcross_bluesky/src/Extension/BlueskyService.php +++ b/source/packages/plg_mokosuitecross_bluesky/src/Extension/BlueskyService.php @@ -1,24 +1,24 @@ * @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\MokoJoomCross\Bluesky\Extension; +namespace Joomla\Plugin\MokoSuiteCross\Bluesky\Extension; defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\Component\MokoJoomCross\Administrator\Service\MokoJoomCrossServiceInterface; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Event\SubscriberInterface; /** - * Bluesky service plugin for MokoJoomCross. + * Bluesky service plugin for MokoSuiteCross. * * Uses the AT Protocol (atproto) to post to Bluesky. * @@ -29,14 +29,14 @@ use Joomla\Event\SubscriberInterface; * "pds_url": "https://bsky.social" // Optional, defaults to bsky.social * } */ -class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoJoomCrossServiceInterface +class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface { public static function getSubscribedEvents(): array { - return ['onMokoJoomCrossGetServices' => 'onMokoJoomCrossGetServices']; + return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices']; } - public function onMokoJoomCrossGetServices(&$services): void + public function onMokoSuiteCrossGetServices(&$services): void { $services[] = $this; } @@ -46,6 +46,8 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoJoomC public function getMaxLength(): int { return 300; } public function supportsMedia(): bool { return true; } + private static array $sessionCache = []; + public function publish(string $message, array $media, array $credentials, array $params): array { $pds = rtrim($credentials['pds_url'] ?? 'https://bsky.social', '/'); @@ -56,8 +58,8 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoJoomC return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing credentials']]; } - // Authenticate - $authData = $this->authenticate($pds, $handle, $appPwd); + // Authenticate (uses cached session if still valid) + $authData = $this->authenticateWithCache($pds, $handle, $appPwd); if (empty($authData['accessJwt'])) { return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Authentication failed']]; @@ -79,11 +81,23 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoJoomC CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postData, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $authData['accessJwt'], 'Content-Type: application/json'], - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 30, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP, ]); $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); @@ -111,6 +125,29 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoJoomC return ['valid' => false, 'message' => $authData['message'] ?? 'Failed', 'account_name' => '']; } + private function authenticateWithCache(string $pds, string $handle, string $appPwd): array + { + $cacheKey = md5($pds . $handle); + + if (isset(self::$sessionCache[$cacheKey])) { + $cached = self::$sessionCache[$cacheKey]; + + // AT Protocol access tokens are valid for ~2 hours; re-auth after 90 minutes + if (time() - $cached['_created'] < 5400) { + return $cached; + } + } + + $authData = $this->authenticate($pds, $handle, $appPwd); + + if (!empty($authData['accessJwt'])) { + $authData['_created'] = time(); + self::$sessionCache[$cacheKey] = $authData; + } + + return $authData; + } + private function authenticate(string $pds, string $handle, string $appPwd): array { $ch = curl_init($pds . '/xrpc/com.atproto.server.createSession'); @@ -123,8 +160,23 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoJoomC ]); $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); return json_decode($response, true) ?: []; } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } } diff --git a/src/packages/plg_mokojoomcross_discord/index.html b/source/packages/plg_mokosuitecross_bluesky/src/Extension/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_discord/index.html rename to source/packages/plg_mokosuitecross_bluesky/src/Extension/index.html diff --git a/src/packages/plg_mokojoomcross_discord/language/en-GB/index.html b/source/packages/plg_mokosuitecross_bluesky/src/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_discord/language/en-GB/index.html rename to source/packages/plg_mokosuitecross_bluesky/src/index.html diff --git a/source/packages/plg_mokosuitecross_brevo/brevo.php b/source/packages/plg_mokosuitecross_brevo/brevo.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_brevo/brevo.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_brevo/brevo.xml b/source/packages/plg_mokosuitecross_brevo/brevo.xml new file mode 100644 index 0000000..0cdf41b --- /dev/null +++ b/source/packages/plg_mokosuitecross_brevo/brevo.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - Brevo (Sendinblue) + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_BREVO_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Brevo + + + brevo.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_brevo.ini + language/en-GB/plg_mokosuitecross_brevo.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_brevo/index.html b/source/packages/plg_mokosuitecross_brevo/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_brevo/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_brevo/language/en-GB/index.html b/source/packages/plg_mokosuitecross_brevo/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_brevo/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_brevo/language/en-GB/plg_mokosuitecross_brevo.ini b/source/packages/plg_mokosuitecross_brevo/language/en-GB/plg_mokosuitecross_brevo.ini new file mode 100644 index 0000000..f4cc531 --- /dev/null +++ b/source/packages/plg_mokosuitecross_brevo/language/en-GB/plg_mokosuitecross_brevo.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_BREVO="MokoSuiteCross - Brevo (Sendinblue)" +PLG_MOKOSUITECROSS_BREVO_DESCRIPTION="Cross-post Joomla articles to Brevo (Sendinblue)." diff --git a/source/packages/plg_mokosuitecross_brevo/language/en-GB/plg_mokosuitecross_brevo.sys.ini b/source/packages/plg_mokosuitecross_brevo/language/en-GB/plg_mokosuitecross_brevo.sys.ini new file mode 100644 index 0000000..f4cc531 --- /dev/null +++ b/source/packages/plg_mokosuitecross_brevo/language/en-GB/plg_mokosuitecross_brevo.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_BREVO="MokoSuiteCross - Brevo (Sendinblue)" +PLG_MOKOSUITECROSS_BREVO_DESCRIPTION="Cross-post Joomla articles to Brevo (Sendinblue)." diff --git a/source/packages/plg_mokosuitecross_brevo/language/index.html b/source/packages/plg_mokosuitecross_brevo/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_brevo/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_brevo/services/index.html b/source/packages/plg_mokosuitecross_brevo/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_brevo/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_brevo/services/provider.php b/source/packages/plg_mokosuitecross_brevo/services/provider.php new file mode 100644 index 0000000..a449628 --- /dev/null +++ b/source/packages/plg_mokosuitecross_brevo/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\Brevo\Extension\BrevoService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new BrevoService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'brevo') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_brevo/src/Extension/BrevoService.php b/source/packages/plg_mokosuitecross_brevo/src/Extension/BrevoService.php new file mode 100644 index 0000000..fbf55f3 --- /dev/null +++ b/source/packages/plg_mokosuitecross_brevo/src/Extension/BrevoService.php @@ -0,0 +1,139 @@ + + * @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\Brevo\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Brevo (Sendinblue) service plugin for MokoSuiteCross. + * + * API: https://api.brevo.com/v3/emailCampaigns + */ +class BrevoService 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 'brevo'; } + public function getServiceName(): string { return 'Brevo (Sendinblue)'; } + public function getMaxLength(): int { return 0; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $apiKey = $credentials['api_key'] ?? ''; + $listId = (int) ($credentials['list_id'] ?? 0); + $senderName = $credentials['sender_name'] ?? 'Newsletter'; + $senderEmail = $credentials['sender_email'] ?? ''; + + if (empty($apiKey) || empty($listId) || empty($senderEmail)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing API key, list ID, or sender email']]; + } + + $subject = $params['subject'] ?? mb_substr(strip_tags($message), 0, 150); + + $postData = json_encode([ + 'name' => $subject, + 'subject' => $subject, + 'sender' => ['name' => $senderName, 'email' => $senderEmail], + 'htmlContent' => $message, + 'recipients' => ['listIds' => [$listId]], + ]); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => 'https://api.brevo.com/v3/emailCampaigns', + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_HTTPHEADER => ['api-key: ' . $apiKey, 'Content-Type: application/json'], + CURLOPT_RETURNTRANSFER => true, + 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) { + + $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'] ?? $data['uri'] ?? '', 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $key = $credentials['api_key'] ?? ''; + + if (empty($key)) { + return ['valid' => false, 'message' => 'Missing API key', 'account_name' => '']; + } + + $ch = curl_init('https://api.brevo.com/v3/account'); + curl_setopt_array($ch, [ + CURLOPT_HTTPHEADER => ['api-key: ' . $key], + 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['companyName'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['companyName']]; + } + + return ['valid' => false, 'message' => $data['message'] ?? 'Invalid API key', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_brevo/src/Extension/index.html b/source/packages/plg_mokosuitecross_brevo/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_brevo/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_brevo/src/index.html b/source/packages/plg_mokosuitecross_brevo/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_brevo/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_constantcontact/constantcontact.php b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.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_constantcontact/constantcontact.xml b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml new file mode 100644 index 0000000..963b5a9 --- /dev/null +++ b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - Constant Contact + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_CONSTANTCONTACT_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Constantcontact + + + constantcontact.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_constantcontact.ini + language/en-GB/plg_mokosuitecross_constantcontact.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_constantcontact/index.html b/source/packages/plg_mokosuitecross_constantcontact/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_constantcontact/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_constantcontact/language/en-GB/index.html b/source/packages/plg_mokosuitecross_constantcontact/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_constantcontact/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_constantcontact/language/en-GB/plg_mokosuitecross_constantcontact.ini b/source/packages/plg_mokosuitecross_constantcontact/language/en-GB/plg_mokosuitecross_constantcontact.ini new file mode 100644 index 0000000..5752048 --- /dev/null +++ b/source/packages/plg_mokosuitecross_constantcontact/language/en-GB/plg_mokosuitecross_constantcontact.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_CONSTANTCONTACT="MokoSuiteCross - Constant Contact" +PLG_MOKOSUITECROSS_CONSTANTCONTACT_DESCRIPTION="Cross-post Joomla articles to Constant Contact." diff --git a/source/packages/plg_mokosuitecross_constantcontact/language/en-GB/plg_mokosuitecross_constantcontact.sys.ini b/source/packages/plg_mokosuitecross_constantcontact/language/en-GB/plg_mokosuitecross_constantcontact.sys.ini new file mode 100644 index 0000000..5752048 --- /dev/null +++ b/source/packages/plg_mokosuitecross_constantcontact/language/en-GB/plg_mokosuitecross_constantcontact.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_CONSTANTCONTACT="MokoSuiteCross - Constant Contact" +PLG_MOKOSUITECROSS_CONSTANTCONTACT_DESCRIPTION="Cross-post Joomla articles to Constant Contact." diff --git a/source/packages/plg_mokosuitecross_constantcontact/language/index.html b/source/packages/plg_mokosuitecross_constantcontact/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_constantcontact/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_constantcontact/services/index.html b/source/packages/plg_mokosuitecross_constantcontact/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_constantcontact/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_constantcontact/services/provider.php b/source/packages/plg_mokosuitecross_constantcontact/services/provider.php new file mode 100644 index 0000000..e517227 --- /dev/null +++ b/source/packages/plg_mokosuitecross_constantcontact/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\Constantcontact\Extension\ConstantcontactService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new ConstantcontactService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'constantcontact') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_constantcontact/src/Extension/ConstantcontactService.php b/source/packages/plg_mokosuitecross_constantcontact/src/Extension/ConstantcontactService.php new file mode 100644 index 0000000..e3ae973 --- /dev/null +++ b/source/packages/plg_mokosuitecross_constantcontact/src/Extension/ConstantcontactService.php @@ -0,0 +1,142 @@ + + * @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\Constantcontact\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Constant Contact service plugin for MokoSuiteCross. + * + * API: https://api.cc.email/v3/emails + */ +class ConstantcontactService 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 'constantcontact'; } + public function getServiceName(): string { return 'Constant Contact'; } + public function getMaxLength(): int { return 0; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $token = $credentials['access_token'] ?? ''; + $listId = $credentials['list_id'] ?? ''; + $fromName = $credentials['from_name'] ?? 'Newsletter'; + $fromEmail = $credentials['from_email'] ?? ''; + + if (empty($token) || empty($fromEmail)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or sender email']]; + } + + $subject = $params['subject'] ?? mb_substr(strip_tags($message), 0, 150); + + $postData = json_encode([ + 'name' => $subject, + 'email_campaign_activities' => [[ + 'format_type' => 5, + 'from_name' => $fromName, + 'from_email' => $fromEmail, + 'subject' => $subject, + 'html_content' => $message, + ]], + ]); + + $ch = curl_init(); + 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, + ]); + + 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) { + + $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'] ?? $data['uri'] ?? '', '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://api.cc.email/v3/account/summary'); + 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['organization_name'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['organization_name']]; + } + + return ['valid' => false, 'message' => 'Invalid token', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_constantcontact/src/Extension/index.html b/source/packages/plg_mokosuitecross_constantcontact/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_constantcontact/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_constantcontact/src/index.html b/source/packages/plg_mokosuitecross_constantcontact/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_constantcontact/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_convertkit/convertkit.php b/source/packages/plg_mokosuitecross_convertkit/convertkit.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_convertkit/convertkit.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_convertkit/convertkit.xml b/source/packages/plg_mokosuitecross_convertkit/convertkit.xml new file mode 100644 index 0000000..53a0f7f --- /dev/null +++ b/source/packages/plg_mokosuitecross_convertkit/convertkit.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - ConvertKit + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_CONVERTKIT_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Convertkit + + + convertkit.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_convertkit.ini + language/en-GB/plg_mokosuitecross_convertkit.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_convertkit/index.html b/source/packages/plg_mokosuitecross_convertkit/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_convertkit/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_convertkit/language/en-GB/index.html b/source/packages/plg_mokosuitecross_convertkit/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_convertkit/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_convertkit/language/en-GB/plg_mokosuitecross_convertkit.ini b/source/packages/plg_mokosuitecross_convertkit/language/en-GB/plg_mokosuitecross_convertkit.ini new file mode 100644 index 0000000..95ab689 --- /dev/null +++ b/source/packages/plg_mokosuitecross_convertkit/language/en-GB/plg_mokosuitecross_convertkit.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_CONVERTKIT="MokoSuiteCross - ConvertKit" +PLG_MOKOSUITECROSS_CONVERTKIT_DESCRIPTION="Cross-post Joomla articles to ConvertKit." diff --git a/source/packages/plg_mokosuitecross_convertkit/language/en-GB/plg_mokosuitecross_convertkit.sys.ini b/source/packages/plg_mokosuitecross_convertkit/language/en-GB/plg_mokosuitecross_convertkit.sys.ini new file mode 100644 index 0000000..95ab689 --- /dev/null +++ b/source/packages/plg_mokosuitecross_convertkit/language/en-GB/plg_mokosuitecross_convertkit.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_CONVERTKIT="MokoSuiteCross - ConvertKit" +PLG_MOKOSUITECROSS_CONVERTKIT_DESCRIPTION="Cross-post Joomla articles to ConvertKit." diff --git a/source/packages/plg_mokosuitecross_convertkit/language/index.html b/source/packages/plg_mokosuitecross_convertkit/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_convertkit/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_convertkit/services/index.html b/source/packages/plg_mokosuitecross_convertkit/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_convertkit/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_convertkit/services/provider.php b/source/packages/plg_mokosuitecross_convertkit/services/provider.php new file mode 100644 index 0000000..8f838ae --- /dev/null +++ b/source/packages/plg_mokosuitecross_convertkit/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\Convertkit\Extension\ConvertkitService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new ConvertkitService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'convertkit') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_convertkit/src/Extension/ConvertkitService.php b/source/packages/plg_mokosuitecross_convertkit/src/Extension/ConvertkitService.php new file mode 100644 index 0000000..2a296f2 --- /dev/null +++ b/source/packages/plg_mokosuitecross_convertkit/src/Extension/ConvertkitService.php @@ -0,0 +1,134 @@ + + * @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\Convertkit\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * ConvertKit service plugin for MokoSuiteCross. + * + * API: https://api.convertkit.com/v3/broadcasts + */ +class ConvertkitService 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 'convertkit'; } + public function getServiceName(): string { return 'ConvertKit'; } + public function getMaxLength(): int { return 0; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $apiSecret = $credentials['api_secret'] ?? ''; + + if (empty($apiSecret)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing API secret']]; + } + + $subject = $params['subject'] ?? mb_substr(strip_tags($message), 0, 150); + + $postData = json_encode([ + 'api_secret' => $apiSecret, + 'content' => $message, + 'subject' => $subject, + 'description' => mb_substr(strip_tags($message), 0, 200), + ]); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => 'https://api.convertkit.com/v3/broadcasts', + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_RETURNTRANSFER => true, + 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) { + + $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'] ?? $data['uri'] ?? '', 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $apiSecret = $credentials['api_secret'] ?? ''; + + if (empty($apiSecret)) { + return ['valid' => false, 'message' => 'Missing API secret', 'account_name' => '']; + } + + $ch = curl_init('https://api.convertkit.com/v3/account?api_secret=' . urlencode($apiSecret)); + 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['name'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['name']]; + } + + return ['valid' => false, 'message' => 'Invalid API secret', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return []; + } +} diff --git a/source/packages/plg_mokosuitecross_convertkit/src/Extension/index.html b/source/packages/plg_mokosuitecross_convertkit/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_convertkit/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_convertkit/src/index.html b/source/packages/plg_mokosuitecross_convertkit/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_convertkit/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_devto/devto.php b/source/packages/plg_mokosuitecross_devto/devto.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_devto/devto.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_devto/devto.xml b/source/packages/plg_mokosuitecross_devto/devto.xml new file mode 100644 index 0000000..8b47e2a --- /dev/null +++ b/source/packages/plg_mokosuitecross_devto/devto.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - Dev.to + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_DEVTO_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Devto + + + devto.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_devto.ini + language/en-GB/plg_mokosuitecross_devto.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_devto/index.html b/source/packages/plg_mokosuitecross_devto/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_devto/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_devto/language/en-GB/index.html b/source/packages/plg_mokosuitecross_devto/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_devto/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_devto/language/en-GB/plg_mokosuitecross_devto.ini b/source/packages/plg_mokosuitecross_devto/language/en-GB/plg_mokosuitecross_devto.ini new file mode 100644 index 0000000..3363ce1 --- /dev/null +++ b/source/packages/plg_mokosuitecross_devto/language/en-GB/plg_mokosuitecross_devto.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_DEVTO="MokoSuiteCross - Dev.to" +PLG_MOKOSUITECROSS_DEVTO_DESCRIPTION="Cross-post Joomla articles to Dev.to." diff --git a/source/packages/plg_mokosuitecross_devto/language/en-GB/plg_mokosuitecross_devto.sys.ini b/source/packages/plg_mokosuitecross_devto/language/en-GB/plg_mokosuitecross_devto.sys.ini new file mode 100644 index 0000000..3363ce1 --- /dev/null +++ b/source/packages/plg_mokosuitecross_devto/language/en-GB/plg_mokosuitecross_devto.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_DEVTO="MokoSuiteCross - Dev.to" +PLG_MOKOSUITECROSS_DEVTO_DESCRIPTION="Cross-post Joomla articles to Dev.to." diff --git a/source/packages/plg_mokosuitecross_devto/language/index.html b/source/packages/plg_mokosuitecross_devto/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_devto/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_devto/services/index.html b/source/packages/plg_mokosuitecross_devto/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_devto/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_devto/services/provider.php b/source/packages/plg_mokosuitecross_devto/services/provider.php new file mode 100644 index 0000000..e98cac0 --- /dev/null +++ b/source/packages/plg_mokosuitecross_devto/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\Devto\Extension\DevtoService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new DevtoService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'devto') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_devto/src/Extension/DevtoService.php b/source/packages/plg_mokosuitecross_devto/src/Extension/DevtoService.php new file mode 100644 index 0000000..b62a98a --- /dev/null +++ b/source/packages/plg_mokosuitecross_devto/src/Extension/DevtoService.php @@ -0,0 +1,134 @@ + + * @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\Devto\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Dev.to service plugin for MokoSuiteCross. + * + * API: https://dev.to/api/articles + */ +class DevtoService 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 'devto'; } + public function getServiceName(): string { return 'Dev.to'; } + public function getMaxLength(): int { return 0; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $token = $credentials['api_key'] ?? ''; + + if (empty($token)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing API key']]; + } + + $title = mb_substr(strip_tags($message), 0, 150); + $body = $message; + + // Prepend image in markdown if available + if (!empty($media[0])) { + $body = '![Cover image](' . $media[0] . ")\n\n" . $body; + } + + $postData = json_encode([ + 'article' => [ + 'title' => $title, + 'body_markdown' => $body, + 'published' => true, + ], + ]); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => 'https://dev.to/api/articles', + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_HTTPHEADER => ['api-key: ' . $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'] ?? $data['uri'] ?? '', 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $key = $credentials['api_key'] ?? ''; + + if (empty($key)) { + return ['valid' => false, 'message' => 'Missing API key', 'account_name' => '']; + } + + $ch = curl_init('https://dev.to/api/users/me'); + curl_setopt_array($ch, [ + CURLOPT_HTTPHEADER => ['api-key: ' . $key], + 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['username'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['username']]; + } + + return ['valid' => false, 'message' => $data['error'] ?? 'Invalid API key', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_devto/src/Extension/index.html b/source/packages/plg_mokosuitecross_devto/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_devto/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_devto/src/index.html b/source/packages/plg_mokosuitecross_devto/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_devto/src/index.html @@ -0,0 +1 @@ + diff --git a/src/packages/plg_mokojoomcross_discord/discord.php b/source/packages/plg_mokosuitecross_discord/discord.php similarity index 90% rename from src/packages/plg_mokojoomcross_discord/discord.php rename to source/packages/plg_mokosuitecross_discord/discord.php index f74ab97..9b76408 100644 --- a/src/packages/plg_mokojoomcross_discord/discord.php +++ b/source/packages/plg_mokosuitecross_discord/discord.php @@ -1,7 +1,7 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE diff --git a/source/packages/plg_mokosuitecross_discord/discord.xml b/source/packages/plg_mokosuitecross_discord/discord.xml new file mode 100644 index 0000000..f695366 --- /dev/null +++ b/source/packages/plg_mokosuitecross_discord/discord.xml @@ -0,0 +1,46 @@ + + + MokoSuiteCross - Discord + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_DISCORD_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross${CLASS_NAME} + + + discord.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_discord.ini + language/en-GB/plg_mokosuitecross_discord.sys.ini + + + + +
+ + +
+
+
+
diff --git a/src/packages/plg_mokojoomcross_discord/language/index.html b/source/packages/plg_mokosuitecross_discord/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_discord/language/index.html rename to source/packages/plg_mokosuitecross_discord/index.html diff --git a/src/packages/plg_mokojoomcross_discord/services/index.html b/source/packages/plg_mokosuitecross_discord/language/en-GB/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_discord/services/index.html rename to source/packages/plg_mokosuitecross_discord/language/en-GB/index.html diff --git a/source/packages/plg_mokosuitecross_discord/language/en-GB/plg_mokosuitecross_discord.ini b/source/packages/plg_mokosuitecross_discord/language/en-GB/plg_mokosuitecross_discord.ini new file mode 100644 index 0000000..4c09ce3 --- /dev/null +++ b/source/packages/plg_mokosuitecross_discord/language/en-GB/plg_mokosuitecross_discord.ini @@ -0,0 +1,7 @@ +PLG_MOKOSUITECROSS_DISCORD="MokoSuiteCross - Discord" +PLG_MOKOSUITECROSS_DISCORD_DESCRIPTION="Cross-post Joomla articles to Discord." +PLG_MOKOSUITECROSS_DISCORD_FIELDSET_DEFAULTS="Default Settings" +PLG_MOKOSUITECROSS_DISCORD_DEFAULT_WEBHOOK_URL="Default Webhook URL" +PLG_MOKOSUITECROSS_DISCORD_DEFAULT_WEBHOOK_URL_DESC="The default MokoWaaS Discord webhook URL used when a service is set to 'default' mode." +PLG_MOKOSUITECROSS_DISCORD_EMBED_COLOR="Embed Color" +PLG_MOKOSUITECROSS_DISCORD_EMBED_COLOR_DESC="Default color for Discord embed messages. Defaults to Discord blurple (#5865F2)." diff --git a/source/packages/plg_mokosuitecross_discord/language/en-GB/plg_mokosuitecross_discord.sys.ini b/source/packages/plg_mokosuitecross_discord/language/en-GB/plg_mokosuitecross_discord.sys.ini new file mode 100644 index 0000000..516d59e --- /dev/null +++ b/source/packages/plg_mokosuitecross_discord/language/en-GB/plg_mokosuitecross_discord.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_DISCORD="MokoSuiteCross - Discord" +PLG_MOKOSUITECROSS_DISCORD_DESCRIPTION="Cross-post Joomla articles to Discord." diff --git a/src/packages/plg_mokojoomcross_discord/src/Extension/index.html b/source/packages/plg_mokosuitecross_discord/language/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_discord/src/Extension/index.html rename to source/packages/plg_mokosuitecross_discord/language/index.html diff --git a/src/packages/plg_mokojoomcross_discord/src/index.html b/source/packages/plg_mokosuitecross_discord/services/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_discord/src/index.html rename to source/packages/plg_mokosuitecross_discord/services/index.html diff --git a/src/packages/plg_mokojoomcross_discord/services/provider.php b/source/packages/plg_mokosuitecross_discord/services/provider.php similarity index 81% rename from src/packages/plg_mokojoomcross_discord/services/provider.php rename to source/packages/plg_mokosuitecross_discord/services/provider.php index 03b0862..ecf8ab8 100644 --- a/src/packages/plg_mokojoomcross_discord/services/provider.php +++ b/source/packages/plg_mokosuitecross_discord/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -17,7 +17,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\MokoJoomCross${CLASS_NAME}\Extension${CLASS_NAME}Service; +use Joomla\Plugin\MokoSuiteCross\Discord\Extension\DiscordService; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -27,7 +27,7 @@ return new class () implements ServiceProviderInterface { function (Container $container) { $plugin = new DiscordService( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('mokojoomcross', 'discord') + (array) PluginHelper::getPlugin('mokosuitecross', 'discord') ); $plugin->setApplication(Factory::getApplication()); diff --git a/src/packages/plg_mokojoomcross_discord/src/Extension/DiscordService.php b/source/packages/plg_mokosuitecross_discord/src/Extension/DiscordService.php similarity index 77% rename from src/packages/plg_mokojoomcross_discord/src/Extension/DiscordService.php rename to source/packages/plg_mokosuitecross_discord/src/Extension/DiscordService.php index f156998..638a71b 100644 --- a/src/packages/plg_mokojoomcross_discord/src/Extension/DiscordService.php +++ b/source/packages/plg_mokosuitecross_discord/src/Extension/DiscordService.php @@ -1,24 +1,24 @@ * @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\MokoJoomCross\Discord\Extension; +namespace Joomla\Plugin\MokoSuiteCross\Discord\Extension; defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\Component\MokoJoomCross\Administrator\Service\MokoJoomCrossServiceInterface; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Event\SubscriberInterface; /** - * Discord service plugin for MokoJoomCross. + * Discord service plugin for MokoSuiteCross. * * Supports two modes: * 1. Default MokoWaaS Webhook — pre-configured webhook URL (hidden from admin UI) @@ -30,14 +30,14 @@ use Joomla\Event\SubscriberInterface; * "webhook_url": "https://discord.com/api/webhooks/..." // Only for custom mode * } */ -class DiscordService extends CMSPlugin implements SubscriberInterface, MokoJoomCrossServiceInterface +class DiscordService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface { public static function getSubscribedEvents(): array { - return ['onMokoJoomCrossGetServices' => 'onMokoJoomCrossGetServices']; + return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices']; } - public function onMokoJoomCrossGetServices(&$services): void + public function onMokoSuiteCrossGetServices(&$services): void { $services[] = $this; } @@ -72,6 +72,16 @@ class DiscordService extends CMSPlugin implements SubscriberInterface, MokoJoomC ]); $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); @@ -113,7 +123,11 @@ class DiscordService extends CMSPlugin implements SubscriberInterface, MokoJoomC return $credentials['webhook_url'] ?? ''; } - return \Joomla\CMS\Component\ComponentHelper::getParams('com_mokojoomcross') - ->get('discord_default_webhook', ''); + return $this->params->get('default_webhook_url', ''); + } + + public function getSupportedMediaTypes(): array + { + return ['image', 'video']; } } diff --git a/src/packages/plg_mokojoomcross_facebook/index.html b/source/packages/plg_mokosuitecross_discord/src/Extension/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_facebook/index.html rename to source/packages/plg_mokosuitecross_discord/src/Extension/index.html diff --git a/src/packages/plg_mokojoomcross_facebook/language/en-GB/index.html b/source/packages/plg_mokosuitecross_discord/src/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_facebook/language/en-GB/index.html rename to source/packages/plg_mokosuitecross_discord/src/index.html diff --git a/src/packages/plg_mokojoomcross_facebook/facebook.php b/source/packages/plg_mokosuitecross_facebook/facebook.php similarity index 90% rename from src/packages/plg_mokojoomcross_facebook/facebook.php rename to source/packages/plg_mokosuitecross_facebook/facebook.php index f74ab97..9b76408 100644 --- a/src/packages/plg_mokojoomcross_facebook/facebook.php +++ b/source/packages/plg_mokosuitecross_facebook/facebook.php @@ -1,7 +1,7 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE diff --git a/source/packages/plg_mokosuitecross_facebook/facebook.xml b/source/packages/plg_mokosuitecross_facebook/facebook.xml new file mode 100644 index 0000000..4d8cb82 --- /dev/null +++ b/source/packages/plg_mokosuitecross_facebook/facebook.xml @@ -0,0 +1,45 @@ + + + MokoSuiteCross - Facebook / Meta + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_FACEBOOK_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross${CLASS_NAME} + + + facebook.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_facebook.ini + language/en-GB/plg_mokosuitecross_facebook.sys.ini + + + + +
+ + +
+
+
+
diff --git a/src/packages/plg_mokojoomcross_facebook/language/index.html b/source/packages/plg_mokosuitecross_facebook/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_facebook/language/index.html rename to source/packages/plg_mokosuitecross_facebook/index.html diff --git a/src/packages/plg_mokojoomcross_facebook/services/index.html b/source/packages/plg_mokosuitecross_facebook/language/en-GB/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_facebook/services/index.html rename to source/packages/plg_mokosuitecross_facebook/language/en-GB/index.html diff --git a/source/packages/plg_mokosuitecross_facebook/language/en-GB/plg_mokosuitecross_facebook.ini b/source/packages/plg_mokosuitecross_facebook/language/en-GB/plg_mokosuitecross_facebook.ini new file mode 100644 index 0000000..2794f7b --- /dev/null +++ b/source/packages/plg_mokosuitecross_facebook/language/en-GB/plg_mokosuitecross_facebook.ini @@ -0,0 +1,7 @@ +PLG_MOKOSUITECROSS_FACEBOOK="MokoSuiteCross - Facebook / Meta" +PLG_MOKOSUITECROSS_FACEBOOK_DESCRIPTION="Cross-post Joomla articles to Facebook / Meta." +PLG_MOKOSUITECROSS_FACEBOOK_FIELDSET_DEFAULTS="Default Settings" +PLG_MOKOSUITECROSS_FACEBOOK_DEFAULT_PAGE_ACCESS_TOKEN="Default Page Access Token" +PLG_MOKOSUITECROSS_FACEBOOK_DEFAULT_PAGE_ACCESS_TOKEN_DESC="The default MokoWaaS Facebook Page Access Token used when a service is set to 'default' mode." +PLG_MOKOSUITECROSS_FACEBOOK_DEFAULT_PAGE_ID="Default Page ID" +PLG_MOKOSUITECROSS_FACEBOOK_DEFAULT_PAGE_ID_DESC="The default Facebook Page ID used when a service is set to 'default' mode." diff --git a/source/packages/plg_mokosuitecross_facebook/language/en-GB/plg_mokosuitecross_facebook.sys.ini b/source/packages/plg_mokosuitecross_facebook/language/en-GB/plg_mokosuitecross_facebook.sys.ini new file mode 100644 index 0000000..d8f58ca --- /dev/null +++ b/source/packages/plg_mokosuitecross_facebook/language/en-GB/plg_mokosuitecross_facebook.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_FACEBOOK="MokoSuiteCross - Facebook / Meta" +PLG_MOKOSUITECROSS_FACEBOOK_DESCRIPTION="Cross-post Joomla articles to Facebook / Meta." diff --git a/src/packages/plg_mokojoomcross_facebook/src/Extension/index.html b/source/packages/plg_mokosuitecross_facebook/language/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_facebook/src/Extension/index.html rename to source/packages/plg_mokosuitecross_facebook/language/index.html diff --git a/src/packages/plg_mokojoomcross_facebook/src/index.html b/source/packages/plg_mokosuitecross_facebook/services/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_facebook/src/index.html rename to source/packages/plg_mokosuitecross_facebook/services/index.html diff --git a/src/packages/plg_mokojoomcross_facebook/services/provider.php b/source/packages/plg_mokosuitecross_facebook/services/provider.php similarity index 81% rename from src/packages/plg_mokojoomcross_facebook/services/provider.php rename to source/packages/plg_mokosuitecross_facebook/services/provider.php index 3ed00d2..fda8da7 100644 --- a/src/packages/plg_mokojoomcross_facebook/services/provider.php +++ b/source/packages/plg_mokosuitecross_facebook/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -17,7 +17,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\MokoJoomCross${CLASS_NAME}\Extension${CLASS_NAME}Service; +use Joomla\Plugin\MokoSuiteCross\Facebook\Extension\FacebookService; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -27,7 +27,7 @@ return new class () implements ServiceProviderInterface { function (Container $container) { $plugin = new FacebookService( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('mokojoomcross', 'facebook') + (array) PluginHelper::getPlugin('mokosuitecross', 'facebook') ); $plugin->setApplication(Factory::getApplication()); diff --git a/src/packages/plg_mokojoomcross_facebook/src/Extension/FacebookService.php b/source/packages/plg_mokosuitecross_facebook/src/Extension/FacebookService.php similarity index 70% rename from src/packages/plg_mokojoomcross_facebook/src/Extension/FacebookService.php rename to source/packages/plg_mokosuitecross_facebook/src/Extension/FacebookService.php index fc9dc3c..3cd2cb4 100644 --- a/src/packages/plg_mokojoomcross_facebook/src/Extension/FacebookService.php +++ b/source/packages/plg_mokosuitecross_facebook/src/Extension/FacebookService.php @@ -1,24 +1,24 @@ * @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\MokoJoomCross\Facebook\Extension; +namespace Joomla\Plugin\MokoSuiteCross\Facebook\Extension; defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\Component\MokoJoomCross\Administrator\Service\MokoJoomCrossServiceInterface; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Event\SubscriberInterface; /** - * Facebook/Meta service plugin for MokoJoomCross. + * Facebook/Meta service plugin for MokoSuiteCross. * * Supports two modes: * 1. Default MokoWaaS App — pre-configured app credentials (hidden from admin UI) @@ -31,16 +31,16 @@ use Joomla\Event\SubscriberInterface; * "page_id": "..." // Required — Facebook Page ID * } */ -class FacebookService extends CMSPlugin implements SubscriberInterface, MokoJoomCrossServiceInterface +class FacebookService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface { public static function getSubscribedEvents(): array { return [ - 'onMokoJoomCrossGetServices' => 'onMokoJoomCrossGetServices', + 'onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices', ]; } - public function onMokoJoomCrossGetServices(&$services): void + public function onMokoSuiteCrossGetServices(&$services): void { $services[] = $this; } @@ -67,8 +67,7 @@ class FacebookService extends CMSPlugin implements SubscriberInterface, MokoJoom $apiUrl = 'https://graph.facebook.com/v19.0/' . $pageId . '/feed'; $postData = [ - 'message' => $message, - 'access_token' => $token, + 'message' => $message, ]; // Attach link if provided in params @@ -80,11 +79,22 @@ class FacebookService extends CMSPlugin implements SubscriberInterface, MokoJoom curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($postData), + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token], 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); @@ -105,11 +115,20 @@ class FacebookService extends CMSPlugin implements SubscriberInterface, MokoJoom return ['valid' => false, 'message' => 'No access token', 'account_name' => '']; } - $apiUrl = 'https://graph.facebook.com/v19.0/me?access_token=' . urlencode($token); + $apiUrl = 'https://graph.facebook.com/v19.0/me'; $ch = curl_init($apiUrl); - curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10]); + 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) ?: []; @@ -139,7 +158,11 @@ class FacebookService extends CMSPlugin implements SubscriberInterface, MokoJoom return $credentials['page_access_token'] ?? ''; } - return \Joomla\CMS\Component\ComponentHelper::getParams('com_mokojoomcross') - ->get('facebook_default_token', ''); + return $this->params->get('default_page_access_token', ''); + } + + public function getSupportedMediaTypes(): array + { + return ['image', 'video', 'gif']; } } diff --git a/src/packages/plg_mokojoomcross_linkedin/index.html b/source/packages/plg_mokosuitecross_facebook/src/Extension/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_linkedin/index.html rename to source/packages/plg_mokosuitecross_facebook/src/Extension/index.html diff --git a/src/packages/plg_mokojoomcross_linkedin/language/en-GB/index.html b/source/packages/plg_mokosuitecross_facebook/src/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_linkedin/language/en-GB/index.html rename to source/packages/plg_mokosuitecross_facebook/src/index.html diff --git a/source/packages/plg_mokosuitecross_ghost/ghost.php b/source/packages/plg_mokosuitecross_ghost/ghost.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_ghost/ghost.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_ghost/ghost.xml b/source/packages/plg_mokosuitecross_ghost/ghost.xml new file mode 100644 index 0000000..a7770c6 --- /dev/null +++ b/source/packages/plg_mokosuitecross_ghost/ghost.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - Ghost + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_GHOST_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Ghost + + + ghost.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_ghost.ini + language/en-GB/plg_mokosuitecross_ghost.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_ghost/index.html b/source/packages/plg_mokosuitecross_ghost/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_ghost/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_ghost/language/en-GB/index.html b/source/packages/plg_mokosuitecross_ghost/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_ghost/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_ghost/language/en-GB/plg_mokosuitecross_ghost.ini b/source/packages/plg_mokosuitecross_ghost/language/en-GB/plg_mokosuitecross_ghost.ini new file mode 100644 index 0000000..fa80630 --- /dev/null +++ b/source/packages/plg_mokosuitecross_ghost/language/en-GB/plg_mokosuitecross_ghost.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_GHOST="MokoSuiteCross - Ghost" +PLG_MOKOSUITECROSS_GHOST_DESCRIPTION="Cross-post Joomla articles to Ghost." diff --git a/source/packages/plg_mokosuitecross_ghost/language/en-GB/plg_mokosuitecross_ghost.sys.ini b/source/packages/plg_mokosuitecross_ghost/language/en-GB/plg_mokosuitecross_ghost.sys.ini new file mode 100644 index 0000000..fa80630 --- /dev/null +++ b/source/packages/plg_mokosuitecross_ghost/language/en-GB/plg_mokosuitecross_ghost.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_GHOST="MokoSuiteCross - Ghost" +PLG_MOKOSUITECROSS_GHOST_DESCRIPTION="Cross-post Joomla articles to Ghost." diff --git a/source/packages/plg_mokosuitecross_ghost/language/index.html b/source/packages/plg_mokosuitecross_ghost/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_ghost/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_ghost/services/index.html b/source/packages/plg_mokosuitecross_ghost/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_ghost/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_ghost/services/provider.php b/source/packages/plg_mokosuitecross_ghost/services/provider.php new file mode 100644 index 0000000..c571293 --- /dev/null +++ b/source/packages/plg_mokosuitecross_ghost/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\Ghost\Extension\GhostService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new GhostService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'ghost') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_ghost/src/Extension/GhostService.php b/source/packages/plg_mokosuitecross_ghost/src/Extension/GhostService.php new file mode 100644 index 0000000..0695d66 --- /dev/null +++ b/source/packages/plg_mokosuitecross_ghost/src/Extension/GhostService.php @@ -0,0 +1,190 @@ + + * @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\Ghost\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Ghost service plugin for MokoSuiteCross. + * + * Uses Ghost Admin API v5 with JWT authentication. + * The admin_api_key is in format {id}:{secret} — split to build a short-lived JWT. + */ +class GhostService 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 'ghost'; } + public function getServiceName(): string { return 'Ghost'; } + public function getMaxLength(): int { return 0; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $siteUrl = rtrim($credentials['site_url'] ?? '', '/'); + $apiKey = $credentials['admin_api_key'] ?? ''; + $status = $credentials['default_status'] ?? 'draft'; + + if (empty($siteUrl) || empty($apiKey)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing site URL or admin API key.']]; + } + + $jwt = $this->buildGhostJwt($apiKey); + + if (empty($jwt)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Invalid admin API key format. Expected {id}:{secret}.']]; + } + + $apiUrl = $siteUrl . '/ghost/api/admin/posts/'; + $payload = json_encode([ + 'posts' => [[ + 'title' => mb_substr(strip_tags($message), 0, 255), + 'html' => $message, + 'status' => $status, + ]], + ]); + + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HTTPHEADER => [ + 'Authorization: Ghost ' . $jwt, + 'Content-Type: application/json', + ], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP, + ]); + + $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 === 201 && !empty($data['posts'][0]['id'])) { + return ['success' => true, 'platform_post_id' => $data['posts'][0]['id'], 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $siteUrl = rtrim($credentials['site_url'] ?? '', '/'); + $apiKey = $credentials['admin_api_key'] ?? ''; + + if (empty($siteUrl) || empty($apiKey)) { + return ['valid' => false, 'message' => 'Site URL and admin API key are required.', 'account_name' => '']; + } + + $jwt = $this->buildGhostJwt($apiKey); + + if (empty($jwt)) { + return ['valid' => false, 'message' => 'Invalid API key format. Expected {id}:{secret}.', 'account_name' => '']; + } + + $ch = curl_init($siteUrl . '/ghost/api/admin/site/'); + curl_setopt_array($ch, [ + CURLOPT_HTTPHEADER => ['Authorization: Ghost ' . $jwt], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP, + ]); + + $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['site']['title'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['site']['title']]; + } + + return ['valid' => false, 'message' => $data['errors'][0]['message'] ?? 'Failed to connect.', 'account_name' => '']; + } + + /** + * Build a short-lived JWT for Ghost Admin API. + * + * Ghost admin API keys are in format "{id}:{secret}". The JWT uses HS256 + * with the secret (hex-decoded) as the signing key. + */ + private function buildGhostJwt(string $apiKey): string + { + $parts = explode(':', $apiKey, 2); + + if (count($parts) !== 2 || empty($parts[0]) || empty($parts[1])) { + return ''; + } + + [$keyId, $secret] = $parts; + + $header = $this->base64url(json_encode(['alg' => 'HS256', 'typ' => 'JWT', 'kid' => $keyId])); + $now = time(); + $payload = $this->base64url(json_encode([ + 'iat' => $now, + 'exp' => $now + 300, + 'aud' => '/admin/', + ])); + + $signingKey = hex2bin($secret); + $signature = $this->base64url(hash_hmac('sha256', $header . '.' . $payload, $signingKey, true)); + + return $header . '.' . $payload . '.' . $signature; + } + + private function base64url(string $data): string + { + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_ghost/src/Extension/index.html b/source/packages/plg_mokosuitecross_ghost/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_ghost/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_ghost/src/index.html b/source/packages/plg_mokosuitecross_ghost/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_ghost/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.php b/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.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_googlebusiness/googlebusiness.xml b/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml new file mode 100644 index 0000000..e43135a --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - Google Business Profile + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_GOOGLEBUSINESS_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\GoogleBusiness + + + googlebusiness.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_googlebusiness.ini + language/en-GB/plg_mokosuitecross_googlebusiness.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_googlebusiness/index.html b/source/packages/plg_mokosuitecross_googlebusiness/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlebusiness/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_googlebusiness/language/en-GB/index.html b/source/packages/plg_mokosuitecross_googlebusiness/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlebusiness/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_googlebusiness/language/en-GB/plg_mokosuitecross_googlebusiness.ini b/source/packages/plg_mokosuitecross_googlebusiness/language/en-GB/plg_mokosuitecross_googlebusiness.ini new file mode 100644 index 0000000..89be092 --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlebusiness/language/en-GB/plg_mokosuitecross_googlebusiness.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_GOOGLEBUSINESS="MokoSuiteCross - Google Business Profile" +PLG_MOKOSUITECROSS_GOOGLEBUSINESS_DESCRIPTION="Cross-post Joomla articles to Google Business Profile." diff --git a/source/packages/plg_mokosuitecross_googlebusiness/language/en-GB/plg_mokosuitecross_googlebusiness.sys.ini b/source/packages/plg_mokosuitecross_googlebusiness/language/en-GB/plg_mokosuitecross_googlebusiness.sys.ini new file mode 100644 index 0000000..89be092 --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlebusiness/language/en-GB/plg_mokosuitecross_googlebusiness.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_GOOGLEBUSINESS="MokoSuiteCross - Google Business Profile" +PLG_MOKOSUITECROSS_GOOGLEBUSINESS_DESCRIPTION="Cross-post Joomla articles to Google Business Profile." diff --git a/source/packages/plg_mokosuitecross_googlebusiness/language/index.html b/source/packages/plg_mokosuitecross_googlebusiness/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlebusiness/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_googlebusiness/services/index.html b/source/packages/plg_mokosuitecross_googlebusiness/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlebusiness/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_googlebusiness/services/provider.php b/source/packages/plg_mokosuitecross_googlebusiness/services/provider.php new file mode 100644 index 0000000..e4ccbb6 --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlebusiness/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\GoogleBusiness\Extension\GoogleBusinessService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new GoogleBusinessService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'googlebusiness') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_googlebusiness/src/Extension/GoogleBusinessService.php b/source/packages/plg_mokosuitecross_googlebusiness/src/Extension/GoogleBusinessService.php new file mode 100644 index 0000000..74a78af --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlebusiness/src/Extension/GoogleBusinessService.php @@ -0,0 +1,150 @@ + + * @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\GoogleBusiness\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Google Business Profile service plugin for MokoSuiteCross. + * + * Uses the My Business API v4 to create local posts. + */ +class GoogleBusinessService 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 'googlebusiness'; } + public function getServiceName(): string { return 'Google Business Profile'; } + public function getMaxLength(): int { return 1500; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $token = $credentials['access_token'] ?? ''; + $accountId = $credentials['account_id'] ?? ''; + $locationId = $credentials['location_id'] ?? ''; + + if (empty($token) || empty($accountId) || empty($locationId)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token, account ID, or location ID.']]; + } + + $apiUrl = 'https://mybusiness.googleapis.com/v4/accounts/' + . urlencode($accountId) . '/locations/' + . urlencode($locationId) . '/localPosts'; + + $postData = [ + 'languageCode' => 'en', + 'summary' => mb_substr($message, 0, 1500), + 'topicType' => 'STANDARD', + ]; + + // Attach image if provided + if (!empty($media[0])) { + $postData['media'] = [ + 'mediaFormat' => 'PHOTO', + 'sourceUrl' => $media[0], + ]; + } + + $payload = json_encode($postData); + + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + 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 && !empty($data['name'])) { + return ['success' => true, 'platform_post_id' => $data['name'], 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $token = $credentials['access_token'] ?? ''; + $accountId = $credentials['account_id'] ?? ''; + $locationId = $credentials['location_id'] ?? ''; + + if (empty($token) || empty($accountId) || empty($locationId)) { + return ['valid' => false, 'message' => 'Access token, account ID, and location ID are required.', 'account_name' => '']; + } + + $ch = curl_init('https://mybusiness.googleapis.com/v4/' . urlencode($accountId) . '/' . urlencode($locationId)); + 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['locationName'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['locationName']]; + } + + return ['valid' => false, 'message' => $data['error']['message'] ?? 'Failed to verify credentials.', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_googlebusiness/src/Extension/index.html b/source/packages/plg_mokosuitecross_googlebusiness/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlebusiness/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_googlebusiness/src/index.html b/source/packages/plg_mokosuitecross_googlebusiness/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlebusiness/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_googlechat/googlechat.php b/source/packages/plg_mokosuitecross_googlechat/googlechat.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlechat/googlechat.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_googlechat/googlechat.xml b/source/packages/plg_mokosuitecross_googlechat/googlechat.xml new file mode 100644 index 0000000..a8fb758 --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlechat/googlechat.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - Google Chat + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_GOOGLECHAT_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\GoogleChat + + + googlechat.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_googlechat.ini + language/en-GB/plg_mokosuitecross_googlechat.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_googlechat/index.html b/source/packages/plg_mokosuitecross_googlechat/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlechat/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_googlechat/language/en-GB/index.html b/source/packages/plg_mokosuitecross_googlechat/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlechat/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_googlechat/language/en-GB/plg_mokosuitecross_googlechat.ini b/source/packages/plg_mokosuitecross_googlechat/language/en-GB/plg_mokosuitecross_googlechat.ini new file mode 100644 index 0000000..3e7387a --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlechat/language/en-GB/plg_mokosuitecross_googlechat.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_GOOGLECHAT="MokoSuiteCross - Google Chat" +PLG_MOKOSUITECROSS_GOOGLECHAT_DESCRIPTION="Cross-post Joomla articles to Google Chat." diff --git a/source/packages/plg_mokosuitecross_googlechat/language/en-GB/plg_mokosuitecross_googlechat.sys.ini b/source/packages/plg_mokosuitecross_googlechat/language/en-GB/plg_mokosuitecross_googlechat.sys.ini new file mode 100644 index 0000000..3e7387a --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlechat/language/en-GB/plg_mokosuitecross_googlechat.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_GOOGLECHAT="MokoSuiteCross - Google Chat" +PLG_MOKOSUITECROSS_GOOGLECHAT_DESCRIPTION="Cross-post Joomla articles to Google Chat." diff --git a/source/packages/plg_mokosuitecross_googlechat/language/index.html b/source/packages/plg_mokosuitecross_googlechat/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlechat/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_googlechat/services/index.html b/source/packages/plg_mokosuitecross_googlechat/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlechat/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_googlechat/services/provider.php b/source/packages/plg_mokosuitecross_googlechat/services/provider.php new file mode 100644 index 0000000..9005a76 --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlechat/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\GoogleChat\Extension\GoogleChatService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new GoogleChatService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'googlechat') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_googlechat/src/Extension/GoogleChatService.php b/source/packages/plg_mokosuitecross_googlechat/src/Extension/GoogleChatService.php new file mode 100644 index 0000000..e734fe1 --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlechat/src/Extension/GoogleChatService.php @@ -0,0 +1,103 @@ + + * @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\GoogleChat\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Google Chat service plugin for MokoSuiteCross. + * + * API: configured webhook URL + */ +class GoogleChatService 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 'googlechat'; } + public function getServiceName(): string { return 'Google Chat'; } + public function getMaxLength(): int { return 4096; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $url = $credentials['webhook_url'] ?? $credentials['webhook_url'] ?? ''; + + if (empty($url)) { + $url = $this->params->get('default_webhook_url', ''); + } + + if (empty($url)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing webhook URL']]; + } + + $postData = json_encode(['text' => $message]); + + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_HTTPHEADER => ['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) ?: ['raw' => $response]; + + 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 + { + $key = $credentials['webhook_url'] ?? $credentials['webhook_url'] ?? ''; + + if (empty($key)) { + return ['valid' => false, 'message' => 'Missing credentials', 'account_name' => '']; + } + + return ['valid' => true, 'message' => 'Credentials configured', 'account_name' => 'Google Chat']; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_googlechat/src/Extension/index.html b/source/packages/plg_mokosuitecross_googlechat/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlechat/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_googlechat/src/index.html b/source/packages/plg_mokosuitecross_googlechat/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_googlechat/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_hashnode/hashnode.php b/source/packages/plg_mokosuitecross_hashnode/hashnode.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_hashnode/hashnode.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_hashnode/hashnode.xml b/source/packages/plg_mokosuitecross_hashnode/hashnode.xml new file mode 100644 index 0000000..d012f90 --- /dev/null +++ b/source/packages/plg_mokosuitecross_hashnode/hashnode.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - Hashnode + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_HASHNODE_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Hashnode + + + hashnode.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_hashnode.ini + language/en-GB/plg_mokosuitecross_hashnode.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_hashnode/index.html b/source/packages/plg_mokosuitecross_hashnode/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_hashnode/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_hashnode/language/en-GB/index.html b/source/packages/plg_mokosuitecross_hashnode/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_hashnode/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_hashnode/language/en-GB/plg_mokosuitecross_hashnode.ini b/source/packages/plg_mokosuitecross_hashnode/language/en-GB/plg_mokosuitecross_hashnode.ini new file mode 100644 index 0000000..c903379 --- /dev/null +++ b/source/packages/plg_mokosuitecross_hashnode/language/en-GB/plg_mokosuitecross_hashnode.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_HASHNODE="MokoSuiteCross - Hashnode" +PLG_MOKOSUITECROSS_HASHNODE_DESCRIPTION="Cross-post Joomla articles to Hashnode." diff --git a/source/packages/plg_mokosuitecross_hashnode/language/en-GB/plg_mokosuitecross_hashnode.sys.ini b/source/packages/plg_mokosuitecross_hashnode/language/en-GB/plg_mokosuitecross_hashnode.sys.ini new file mode 100644 index 0000000..c903379 --- /dev/null +++ b/source/packages/plg_mokosuitecross_hashnode/language/en-GB/plg_mokosuitecross_hashnode.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_HASHNODE="MokoSuiteCross - Hashnode" +PLG_MOKOSUITECROSS_HASHNODE_DESCRIPTION="Cross-post Joomla articles to Hashnode." diff --git a/source/packages/plg_mokosuitecross_hashnode/language/index.html b/source/packages/plg_mokosuitecross_hashnode/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_hashnode/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_hashnode/services/index.html b/source/packages/plg_mokosuitecross_hashnode/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_hashnode/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_hashnode/services/provider.php b/source/packages/plg_mokosuitecross_hashnode/services/provider.php new file mode 100644 index 0000000..422ea30 --- /dev/null +++ b/source/packages/plg_mokosuitecross_hashnode/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\Hashnode\Extension\HashnodeService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new HashnodeService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'hashnode') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_hashnode/src/Extension/HashnodeService.php b/source/packages/plg_mokosuitecross_hashnode/src/Extension/HashnodeService.php new file mode 100644 index 0000000..8c1062c --- /dev/null +++ b/source/packages/plg_mokosuitecross_hashnode/src/Extension/HashnodeService.php @@ -0,0 +1,153 @@ + + * @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\Hashnode\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Hashnode service plugin for MokoSuiteCross. + * + * Uses the Hashnode GraphQL API at https://gql.hashnode.com. + */ +class HashnodeService 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 'hashnode'; } + public function getServiceName(): string { return 'Hashnode'; } + public function getMaxLength(): int { return 0; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $token = $credentials['token'] ?? ''; + $publicationId = $credentials['publication_id'] ?? ''; + + if (empty($token) || empty($publicationId)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing API token or publication ID.']]; + } + + $title = mb_substr(strip_tags($message), 0, 150); + + $query = 'mutation PublishPost($input: PublishPostInput!) { publishPost(input: $input) { post { id url } } }'; + $variables = [ + 'input' => [ + 'title' => $title, + 'contentMarkdown' => $message, + 'publicationId' => $publicationId, + 'tags' => [], + ], + ]; + + $payload = json_encode(['query' => $query, 'variables' => $variables]); + + $ch = curl_init('https://gql.hashnode.com'); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HTTPHEADER => [ + 'Authorization: ' . $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) ?: []; + + $postId = $data['data']['publishPost']['post']['id'] ?? ''; + + if ($httpCode >= 200 && $httpCode < 300 && !empty($postId)) { + return ['success' => true, 'platform_post_id' => $postId, 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $token = $credentials['token'] ?? ''; + $publicationId = $credentials['publication_id'] ?? ''; + + if (empty($token) || empty($publicationId)) { + return ['valid' => false, 'message' => 'API token and publication ID are required.', 'account_name' => '']; + } + + $query = '{ me { username name } }'; + $payload = json_encode(['query' => $query]); + + $ch = curl_init('https://gql.hashnode.com'); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HTTPHEADER => [ + 'Authorization: ' . $token, + 'Content-Type: application/json', + ], + 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) ?: []; + $name = $data['data']['me']['name'] ?? $data['data']['me']['username'] ?? ''; + + if (!empty($name)) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $name]; + } + + return ['valid' => false, 'message' => 'Failed to verify token.', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_hashnode/src/Extension/index.html b/source/packages/plg_mokosuitecross_hashnode/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_hashnode/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_hashnode/src/index.html b/source/packages/plg_mokosuitecross_hashnode/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_hashnode/src/index.html @@ -0,0 +1 @@ + diff --git a/src/packages/plg_mokojoomcross_linkedin/language/index.html b/source/packages/plg_mokosuitecross_linkedin/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_linkedin/language/index.html rename to source/packages/plg_mokosuitecross_linkedin/index.html diff --git a/src/packages/plg_mokojoomcross_linkedin/services/index.html b/source/packages/plg_mokosuitecross_linkedin/language/en-GB/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_linkedin/services/index.html rename to source/packages/plg_mokosuitecross_linkedin/language/en-GB/index.html diff --git a/source/packages/plg_mokosuitecross_linkedin/language/en-GB/plg_mokosuitecross_linkedin.ini b/source/packages/plg_mokosuitecross_linkedin/language/en-GB/plg_mokosuitecross_linkedin.ini new file mode 100644 index 0000000..866a4c5 --- /dev/null +++ b/source/packages/plg_mokosuitecross_linkedin/language/en-GB/plg_mokosuitecross_linkedin.ini @@ -0,0 +1,9 @@ +PLG_MOKOSUITECROSS_LINKEDIN="MokoSuiteCross - LinkedIn" +PLG_MOKOSUITECROSS_LINKEDIN_DESCRIPTION="Cross-post Joomla articles to LinkedIn." +PLG_MOKOSUITECROSS_LINKEDIN_FIELDSET_DEFAULTS="LinkedIn Defaults" +PLG_MOKOSUITECROSS_LINKEDIN_CLIENT_ID="Client ID" +PLG_MOKOSUITECROSS_LINKEDIN_CLIENT_ID_DESC="LinkedIn App Client ID." +PLG_MOKOSUITECROSS_LINKEDIN_CLIENT_SECRET="Client Secret" +PLG_MOKOSUITECROSS_LINKEDIN_CLIENT_SECRET_DESC="LinkedIn App Client Secret." +PLG_MOKOSUITECROSS_LINKEDIN_REDIRECT_URI="Redirect URI" +PLG_MOKOSUITECROSS_LINKEDIN_REDIRECT_URI_DESC="OAuth callback URL for LinkedIn authentication." diff --git a/source/packages/plg_mokosuitecross_linkedin/language/en-GB/plg_mokosuitecross_linkedin.sys.ini b/source/packages/plg_mokosuitecross_linkedin/language/en-GB/plg_mokosuitecross_linkedin.sys.ini new file mode 100644 index 0000000..d9acc78 --- /dev/null +++ b/source/packages/plg_mokosuitecross_linkedin/language/en-GB/plg_mokosuitecross_linkedin.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_LINKEDIN="MokoSuiteCross - LinkedIn" +PLG_MOKOSUITECROSS_LINKEDIN_DESCRIPTION="Cross-post Joomla articles to LinkedIn." diff --git a/src/packages/plg_mokojoomcross_linkedin/src/Extension/index.html b/source/packages/plg_mokosuitecross_linkedin/language/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_linkedin/src/Extension/index.html rename to source/packages/plg_mokosuitecross_linkedin/language/index.html diff --git a/src/packages/plg_mokojoomcross_linkedin/linkedin.php b/source/packages/plg_mokosuitecross_linkedin/linkedin.php similarity index 90% rename from src/packages/plg_mokojoomcross_linkedin/linkedin.php rename to source/packages/plg_mokosuitecross_linkedin/linkedin.php index f74ab97..9b76408 100644 --- a/src/packages/plg_mokojoomcross_linkedin/linkedin.php +++ b/source/packages/plg_mokosuitecross_linkedin/linkedin.php @@ -1,7 +1,7 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE diff --git a/source/packages/plg_mokosuitecross_linkedin/linkedin.xml b/source/packages/plg_mokosuitecross_linkedin/linkedin.xml new file mode 100644 index 0000000..26e45b6 --- /dev/null +++ b/source/packages/plg_mokosuitecross_linkedin/linkedin.xml @@ -0,0 +1,51 @@ + + + MokoSuiteCross - LinkedIn + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_LINKEDIN_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross${CLASS_NAME} + + + linkedin.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_linkedin.ini + language/en-GB/plg_mokosuitecross_linkedin.sys.ini + + + + +
+ + + +
+
+
+
diff --git a/src/packages/plg_mokojoomcross_linkedin/src/index.html b/source/packages/plg_mokosuitecross_linkedin/services/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_linkedin/src/index.html rename to source/packages/plg_mokosuitecross_linkedin/services/index.html diff --git a/src/packages/plg_mokojoomcross_linkedin/services/provider.php b/source/packages/plg_mokosuitecross_linkedin/services/provider.php similarity index 81% rename from src/packages/plg_mokojoomcross_linkedin/services/provider.php rename to source/packages/plg_mokosuitecross_linkedin/services/provider.php index 53e3007..a1e6f58 100644 --- a/src/packages/plg_mokojoomcross_linkedin/services/provider.php +++ b/source/packages/plg_mokosuitecross_linkedin/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -17,7 +17,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\MokoJoomCross${CLASS_NAME}\Extension${CLASS_NAME}Service; +use Joomla\Plugin\MokoSuiteCross\Linkedin\Extension\LinkedinService; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -27,7 +27,7 @@ return new class () implements ServiceProviderInterface { function (Container $container) { $plugin = new LinkedinService( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('mokojoomcross', 'linkedin') + (array) PluginHelper::getPlugin('mokosuitecross', 'linkedin') ); $plugin->setApplication(Factory::getApplication()); diff --git a/src/packages/plg_mokojoomcross_linkedin/src/Extension/LinkedinService.php b/source/packages/plg_mokosuitecross_linkedin/src/Extension/LinkedinService.php similarity index 78% rename from src/packages/plg_mokojoomcross_linkedin/src/Extension/LinkedinService.php rename to source/packages/plg_mokosuitecross_linkedin/src/Extension/LinkedinService.php index 6beb24b..b1dbd10 100644 --- a/src/packages/plg_mokojoomcross_linkedin/src/Extension/LinkedinService.php +++ b/source/packages/plg_mokosuitecross_linkedin/src/Extension/LinkedinService.php @@ -1,24 +1,24 @@ * @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\MokoJoomCross\Linkedin\Extension; +namespace Joomla\Plugin\MokoSuiteCross\Linkedin\Extension; defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\Component\MokoJoomCross\Administrator\Service\MokoJoomCrossServiceInterface; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Event\SubscriberInterface; /** - * LinkedIn service plugin for MokoJoomCross. + * LinkedIn service plugin for MokoSuiteCross. * * Uses LinkedIn Share API v2 with OAuth 2.0. * @@ -29,14 +29,14 @@ use Joomla\Event\SubscriberInterface; * "person_id": "..." // LinkedIn Person URN (fallback) * } */ -class LinkedinService extends CMSPlugin implements SubscriberInterface, MokoJoomCrossServiceInterface +class LinkedinService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface { public static function getSubscribedEvents(): array { - return ['onMokoJoomCrossGetServices' => 'onMokoJoomCrossGetServices']; + return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices']; } - public function onMokoJoomCrossGetServices(&$services): void + public function onMokoSuiteCrossGetServices(&$services): void { $services[] = $this; } @@ -54,7 +54,7 @@ class LinkedinService extends CMSPlugin implements SubscriberInterface, MokoJoom public function publish(string $message, array $media, array $credentials, array $params): array { $token = $credentials['access_token'] ?? ''; - $author = $credentials['organization_id'] + $author = !empty($credentials['organization_id']) ? 'urn:li:organization:' . $credentials['organization_id'] : 'urn:li:person:' . ($credentials['person_id'] ?? ''); @@ -84,6 +84,16 @@ class LinkedinService extends CMSPlugin implements SubscriberInterface, MokoJoom ]); $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); @@ -111,6 +121,11 @@ class LinkedinService extends CMSPlugin implements SubscriberInterface, MokoJoom 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) ?: []; @@ -131,4 +146,9 @@ class LinkedinService extends CMSPlugin implements SubscriberInterface, MokoJoom { return true; } + + public function getSupportedMediaTypes(): array + { + return ['image', 'video']; + } } diff --git a/src/packages/plg_mokojoomcross_mailchimp/index.html b/source/packages/plg_mokosuitecross_linkedin/src/Extension/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_mailchimp/index.html rename to source/packages/plg_mokosuitecross_linkedin/src/Extension/index.html diff --git a/src/packages/plg_mokojoomcross_mailchimp/language/en-GB/index.html b/source/packages/plg_mokosuitecross_linkedin/src/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_mailchimp/language/en-GB/index.html rename to source/packages/plg_mokosuitecross_linkedin/src/index.html diff --git a/src/packages/plg_mokojoomcross_mailchimp/language/index.html b/source/packages/plg_mokosuitecross_mailchimp/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_mailchimp/language/index.html rename to source/packages/plg_mokosuitecross_mailchimp/index.html diff --git a/src/packages/plg_mokojoomcross_mailchimp/services/index.html b/source/packages/plg_mokosuitecross_mailchimp/language/en-GB/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_mailchimp/services/index.html rename to source/packages/plg_mokosuitecross_mailchimp/language/en-GB/index.html diff --git a/source/packages/plg_mokosuitecross_mailchimp/language/en-GB/plg_mokosuitecross_mailchimp.ini b/source/packages/plg_mokosuitecross_mailchimp/language/en-GB/plg_mokosuitecross_mailchimp.ini new file mode 100644 index 0000000..d688e32 --- /dev/null +++ b/source/packages/plg_mokosuitecross_mailchimp/language/en-GB/plg_mokosuitecross_mailchimp.ini @@ -0,0 +1,9 @@ +PLG_MOKOSUITECROSS_MAILCHIMP="MokoSuiteCross - Mailchimp" +PLG_MOKOSUITECROSS_MAILCHIMP_DESCRIPTION="Cross-post Joomla articles to Mailchimp." +PLG_MOKOSUITECROSS_MAILCHIMP_FIELDSET_DEFAULTS="Mailchimp Defaults" +PLG_MOKOSUITECROSS_MAILCHIMP_DEFAULT_FROM_NAME="Default From Name" +PLG_MOKOSUITECROSS_MAILCHIMP_DEFAULT_FROM_NAME_DESC="Default sender name for Mailchimp campaigns." +PLG_MOKOSUITECROSS_MAILCHIMP_DEFAULT_FROM_EMAIL="Default From Email" +PLG_MOKOSUITECROSS_MAILCHIMP_DEFAULT_FROM_EMAIL_DESC="Default sender email address for Mailchimp campaigns." +PLG_MOKOSUITECROSS_MAILCHIMP_AUTO_SEND="Auto Send" +PLG_MOKOSUITECROSS_MAILCHIMP_AUTO_SEND_DESC="Automatically send the campaign on creation instead of saving as draft." diff --git a/source/packages/plg_mokosuitecross_mailchimp/language/en-GB/plg_mokosuitecross_mailchimp.sys.ini b/source/packages/plg_mokosuitecross_mailchimp/language/en-GB/plg_mokosuitecross_mailchimp.sys.ini new file mode 100644 index 0000000..2fb3eab --- /dev/null +++ b/source/packages/plg_mokosuitecross_mailchimp/language/en-GB/plg_mokosuitecross_mailchimp.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_MAILCHIMP="MokoSuiteCross - Mailchimp" +PLG_MOKOSUITECROSS_MAILCHIMP_DESCRIPTION="Cross-post Joomla articles to Mailchimp." diff --git a/src/packages/plg_mokojoomcross_mailchimp/src/Extension/index.html b/source/packages/plg_mokosuitecross_mailchimp/language/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_mailchimp/src/Extension/index.html rename to source/packages/plg_mokosuitecross_mailchimp/language/index.html diff --git a/src/packages/plg_mokojoomcross_mailchimp/mailchimp.php b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.php similarity index 90% rename from src/packages/plg_mokojoomcross_mailchimp/mailchimp.php rename to source/packages/plg_mokosuitecross_mailchimp/mailchimp.php index f74ab97..9b76408 100644 --- a/src/packages/plg_mokojoomcross_mailchimp/mailchimp.php +++ b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.php @@ -1,7 +1,7 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE diff --git a/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml new file mode 100644 index 0000000..d347497 --- /dev/null +++ b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml @@ -0,0 +1,56 @@ + + + MokoSuiteCross - Mailchimp + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_MAILCHIMP_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross${CLASS_NAME} + + + mailchimp.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_mailchimp.ini + language/en-GB/plg_mokosuitecross_mailchimp.sys.ini + + + + +
+ + + + + + +
+
+
+
diff --git a/src/packages/plg_mokojoomcross_mailchimp/src/index.html b/source/packages/plg_mokosuitecross_mailchimp/services/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_mailchimp/src/index.html rename to source/packages/plg_mokosuitecross_mailchimp/services/index.html diff --git a/src/packages/plg_mokojoomcross_mailchimp/services/provider.php b/source/packages/plg_mokosuitecross_mailchimp/services/provider.php similarity index 81% rename from src/packages/plg_mokojoomcross_mailchimp/services/provider.php rename to source/packages/plg_mokosuitecross_mailchimp/services/provider.php index bca54a1..1923e7d 100644 --- a/src/packages/plg_mokojoomcross_mailchimp/services/provider.php +++ b/source/packages/plg_mokosuitecross_mailchimp/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -17,7 +17,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\MokoJoomCross${CLASS_NAME}\Extension${CLASS_NAME}Service; +use Joomla\Plugin\MokoSuiteCross\Mailchimp\Extension\MailchimpService; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -27,7 +27,7 @@ return new class () implements ServiceProviderInterface { function (Container $container) { $plugin = new MailchimpService( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('mokojoomcross', 'mailchimp') + (array) PluginHelper::getPlugin('mokosuitecross', 'mailchimp') ); $plugin->setApplication(Factory::getApplication()); diff --git a/src/packages/plg_mokojoomcross_mailchimp/src/Extension/MailchimpService.php b/source/packages/plg_mokosuitecross_mailchimp/src/Extension/MailchimpService.php similarity index 64% rename from src/packages/plg_mokojoomcross_mailchimp/src/Extension/MailchimpService.php rename to source/packages/plg_mokosuitecross_mailchimp/src/Extension/MailchimpService.php index cd6f156..d56bffb 100644 --- a/src/packages/plg_mokojoomcross_mailchimp/src/Extension/MailchimpService.php +++ b/source/packages/plg_mokosuitecross_mailchimp/src/Extension/MailchimpService.php @@ -1,24 +1,24 @@ * @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\MokoJoomCross\Mailchimp\Extension; +namespace Joomla\Plugin\MokoSuiteCross\Mailchimp\Extension; defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\Component\MokoJoomCross\Administrator\Service\MokoJoomCrossServiceInterface; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Event\SubscriberInterface; /** - * Mailchimp service plugin for MokoJoomCross. + * Mailchimp service plugin for MokoSuiteCross. * * Creates a Mailchimp campaign from cross-posted content. * @@ -30,14 +30,14 @@ use Joomla\Event\SubscriberInterface; * "from_email": "..." * } */ -class MailchimpService extends CMSPlugin implements SubscriberInterface, MokoJoomCrossServiceInterface +class MailchimpService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface { public static function getSubscribedEvents(): array { - return ['onMokoJoomCrossGetServices' => 'onMokoJoomCrossGetServices']; + return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices']; } - public function onMokoJoomCrossGetServices(&$services): void + public function onMokoSuiteCrossGetServices(&$services): void { $services[] = $this; } @@ -80,6 +80,16 @@ class MailchimpService extends CMSPlugin implements SubscriberInterface, MokoJoo ]); $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); @@ -103,10 +113,37 @@ class MailchimpService extends CMSPlugin implements SubscriberInterface, MokoJoo CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, ]); - curl_exec($ch); + $contentResponse = curl_exec($ch); + $contentCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); - return ['success' => true, 'platform_post_id' => $campaignId, 'response' => $data]; + if ($contentCode !== 200) { + return ['success' => false, 'platform_post_id' => $campaignId, 'response' => ['error' => 'Failed to set campaign content', 'detail' => json_decode($contentResponse, true)]]; + } + + // Send the campaign if auto_send is enabled (default: no — leaves as draft) + $autoSend = $this->params->get('auto_send', 0); + + if ($autoSend) { + $ch = curl_init("https://{$dc}.api.mailchimp.com/3.0/campaigns/{$campaignId}/actions/send"); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => '', + CURLOPT_USERPWD => 'anystring:' . $apiKey, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + ]); + + $sendResponse = curl_exec($ch); + $sendCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($sendCode !== 204) { + return ['success' => false, 'platform_post_id' => $campaignId, 'response' => ['error' => 'Campaign created but send failed', 'detail' => json_decode($sendResponse, true)]]; + } + } + + return ['success' => true, 'platform_post_id' => $campaignId, 'response' => array_merge($data, ['sent' => (bool) $autoSend])]; } public function validateCredentials(array $credentials): array @@ -125,6 +162,11 @@ class MailchimpService extends CMSPlugin implements SubscriberInterface, MokoJoo 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) ?: []; @@ -142,4 +184,9 @@ class MailchimpService extends CMSPlugin implements SubscriberInterface, MokoJoo return end($parts) ?: 'us1'; } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } } diff --git a/src/packages/plg_mokojoomcross_mastodon/index.html b/source/packages/plg_mokosuitecross_mailchimp/src/Extension/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_mastodon/index.html rename to source/packages/plg_mokosuitecross_mailchimp/src/Extension/index.html diff --git a/src/packages/plg_mokojoomcross_mastodon/language/en-GB/index.html b/source/packages/plg_mokosuitecross_mailchimp/src/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_mastodon/language/en-GB/index.html rename to source/packages/plg_mokosuitecross_mailchimp/src/index.html diff --git a/src/packages/plg_mokojoomcross_mastodon/language/index.html b/source/packages/plg_mokosuitecross_mastodon/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_mastodon/language/index.html rename to source/packages/plg_mokosuitecross_mastodon/index.html diff --git a/src/packages/plg_mokojoomcross_mastodon/services/index.html b/source/packages/plg_mokosuitecross_mastodon/language/en-GB/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_mastodon/services/index.html rename to source/packages/plg_mokosuitecross_mastodon/language/en-GB/index.html diff --git a/source/packages/plg_mokosuitecross_mastodon/language/en-GB/plg_mokosuitecross_mastodon.ini b/source/packages/plg_mokosuitecross_mastodon/language/en-GB/plg_mokosuitecross_mastodon.ini new file mode 100644 index 0000000..48b602b --- /dev/null +++ b/source/packages/plg_mokosuitecross_mastodon/language/en-GB/plg_mokosuitecross_mastodon.ini @@ -0,0 +1,13 @@ +PLG_MOKOSUITECROSS_MASTODON="MokoSuiteCross - Mastodon" +PLG_MOKOSUITECROSS_MASTODON_DESCRIPTION="Cross-post Joomla articles to Mastodon." +PLG_MOKOSUITECROSS_MASTODON_FIELDSET_DEFAULTS="Mastodon Defaults" +PLG_MOKOSUITECROSS_MASTODON_DEFAULT_INSTANCE_URL="Default Instance URL" +PLG_MOKOSUITECROSS_MASTODON_DEFAULT_INSTANCE_URL_DESC="Default Mastodon instance URL (e.g. https://mastodon.social)." +PLG_MOKOSUITECROSS_MASTODON_DEFAULT_VISIBILITY="Default Visibility" +PLG_MOKOSUITECROSS_MASTODON_DEFAULT_VISIBILITY_DESC="Default post visibility for Mastodon toots." +PLG_MOKOSUITECROSS_MASTODON_VISIBILITY_PUBLIC="Public" +PLG_MOKOSUITECROSS_MASTODON_VISIBILITY_UNLISTED="Unlisted" +PLG_MOKOSUITECROSS_MASTODON_VISIBILITY_PRIVATE="Private" +PLG_MOKOSUITECROSS_MASTODON_VISIBILITY_DIRECT="Direct" +PLG_MOKOSUITECROSS_MASTODON_APPEND_HASHTAGS="Append Hashtags" +PLG_MOKOSUITECROSS_MASTODON_APPEND_HASHTAGS_DESC="Default hashtags to append to posts (e.g. #Joomla #MokoWaaS)." diff --git a/source/packages/plg_mokosuitecross_mastodon/language/en-GB/plg_mokosuitecross_mastodon.sys.ini b/source/packages/plg_mokosuitecross_mastodon/language/en-GB/plg_mokosuitecross_mastodon.sys.ini new file mode 100644 index 0000000..94b16ef --- /dev/null +++ b/source/packages/plg_mokosuitecross_mastodon/language/en-GB/plg_mokosuitecross_mastodon.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_MASTODON="MokoSuiteCross - Mastodon" +PLG_MOKOSUITECROSS_MASTODON_DESCRIPTION="Cross-post Joomla articles to Mastodon." diff --git a/src/packages/plg_mokojoomcross_mastodon/src/Extension/index.html b/source/packages/plg_mokosuitecross_mastodon/language/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_mastodon/src/Extension/index.html rename to source/packages/plg_mokosuitecross_mastodon/language/index.html diff --git a/src/packages/plg_mokojoomcross_mastodon/mastodon.php b/source/packages/plg_mokosuitecross_mastodon/mastodon.php similarity index 90% rename from src/packages/plg_mokojoomcross_mastodon/mastodon.php rename to source/packages/plg_mokosuitecross_mastodon/mastodon.php index f74ab97..9b76408 100644 --- a/src/packages/plg_mokojoomcross_mastodon/mastodon.php +++ b/source/packages/plg_mokosuitecross_mastodon/mastodon.php @@ -1,7 +1,7 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE diff --git a/source/packages/plg_mokosuitecross_mastodon/mastodon.xml b/source/packages/plg_mokosuitecross_mastodon/mastodon.xml new file mode 100644 index 0000000..a305afc --- /dev/null +++ b/source/packages/plg_mokosuitecross_mastodon/mastodon.xml @@ -0,0 +1,57 @@ + + + MokoSuiteCross - Mastodon + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_MASTODON_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross${CLASS_NAME} + + + mastodon.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_mastodon.ini + language/en-GB/plg_mokosuitecross_mastodon.sys.ini + + + + +
+ + + + + + + + +
+
+
+
diff --git a/src/packages/plg_mokojoomcross_mastodon/src/index.html b/source/packages/plg_mokosuitecross_mastodon/services/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_mastodon/src/index.html rename to source/packages/plg_mokosuitecross_mastodon/services/index.html diff --git a/src/packages/plg_mokojoomcross_mastodon/services/provider.php b/source/packages/plg_mokosuitecross_mastodon/services/provider.php similarity index 81% rename from src/packages/plg_mokojoomcross_mastodon/services/provider.php rename to source/packages/plg_mokosuitecross_mastodon/services/provider.php index 0033174..d4f8249 100644 --- a/src/packages/plg_mokojoomcross_mastodon/services/provider.php +++ b/source/packages/plg_mokosuitecross_mastodon/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -17,7 +17,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\MokoJoomCross${CLASS_NAME}\Extension${CLASS_NAME}Service; +use Joomla\Plugin\MokoSuiteCross\Mastodon\Extension\MastodonService; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -27,7 +27,7 @@ return new class () implements ServiceProviderInterface { function (Container $container) { $plugin = new MastodonService( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('mokojoomcross', 'mastodon') + (array) PluginHelper::getPlugin('mokosuitecross', 'mastodon') ); $plugin->setApplication(Factory::getApplication()); diff --git a/src/packages/plg_mokojoomcross_mastodon/src/Extension/MastodonService.php b/source/packages/plg_mokosuitecross_mastodon/src/Extension/MastodonService.php similarity index 65% rename from src/packages/plg_mokojoomcross_mastodon/src/Extension/MastodonService.php rename to source/packages/plg_mokosuitecross_mastodon/src/Extension/MastodonService.php index dd008f4..397a538 100644 --- a/src/packages/plg_mokojoomcross_mastodon/src/Extension/MastodonService.php +++ b/source/packages/plg_mokosuitecross_mastodon/src/Extension/MastodonService.php @@ -1,24 +1,24 @@ * @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\MokoJoomCross\Mastodon\Extension; +namespace Joomla\Plugin\MokoSuiteCross\Mastodon\Extension; defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\Component\MokoJoomCross\Administrator\Service\MokoJoomCrossServiceInterface; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Event\SubscriberInterface; /** - * Mastodon service plugin for MokoJoomCross. + * Mastodon service plugin for MokoSuiteCross. * * Credentials format: * { @@ -26,14 +26,14 @@ use Joomla\Event\SubscriberInterface; * "access_token": "..." * } */ -class MastodonService extends CMSPlugin implements SubscriberInterface, MokoJoomCrossServiceInterface +class MastodonService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface { public static function getSubscribedEvents(): array { - return ['onMokoJoomCrossGetServices' => 'onMokoJoomCrossGetServices']; + return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices']; } - public function onMokoJoomCrossGetServices(&$services): void + public function onMokoSuiteCrossGetServices(&$services): void { $services[] = $this; } @@ -57,11 +57,23 @@ class MastodonService extends CMSPlugin implements SubscriberInterface, MokoJoom CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode(['status' => mb_substr($message, 0, 500)]), CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'], - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 30, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP, ]); $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); @@ -85,10 +97,18 @@ class MastodonService extends CMSPlugin implements SubscriberInterface, MokoJoom $ch = curl_init($instance . '/api/v1/accounts/verify_credentials'); curl_setopt_array($ch, [ - CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token], - CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP, ]); $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) ?: []; @@ -99,4 +119,9 @@ class MastodonService extends CMSPlugin implements SubscriberInterface, MokoJoom return ['valid' => false, 'message' => 'Failed', 'account_name' => '']; } + + public function getSupportedMediaTypes(): array + { + return ['image', 'video', 'gif']; + } } diff --git a/src/packages/plg_mokojoomcross_slack/index.html b/source/packages/plg_mokosuitecross_mastodon/src/Extension/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_slack/index.html rename to source/packages/plg_mokosuitecross_mastodon/src/Extension/index.html diff --git a/src/packages/plg_mokojoomcross_slack/language/en-GB/index.html b/source/packages/plg_mokosuitecross_mastodon/src/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_slack/language/en-GB/index.html rename to source/packages/plg_mokosuitecross_mastodon/src/index.html diff --git a/source/packages/plg_mokosuitecross_matrix/index.html b/source/packages/plg_mokosuitecross_matrix/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_matrix/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_matrix/language/en-GB/index.html b/source/packages/plg_mokosuitecross_matrix/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_matrix/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_matrix/language/en-GB/plg_mokosuitecross_matrix.ini b/source/packages/plg_mokosuitecross_matrix/language/en-GB/plg_mokosuitecross_matrix.ini new file mode 100644 index 0000000..d1233b1 --- /dev/null +++ b/source/packages/plg_mokosuitecross_matrix/language/en-GB/plg_mokosuitecross_matrix.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_MATRIX="MokoSuiteCross - Matrix / Element" +PLG_MOKOSUITECROSS_MATRIX_DESCRIPTION="Cross-post Joomla articles to Matrix / Element." diff --git a/source/packages/plg_mokosuitecross_matrix/language/en-GB/plg_mokosuitecross_matrix.sys.ini b/source/packages/plg_mokosuitecross_matrix/language/en-GB/plg_mokosuitecross_matrix.sys.ini new file mode 100644 index 0000000..d1233b1 --- /dev/null +++ b/source/packages/plg_mokosuitecross_matrix/language/en-GB/plg_mokosuitecross_matrix.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_MATRIX="MokoSuiteCross - Matrix / Element" +PLG_MOKOSUITECROSS_MATRIX_DESCRIPTION="Cross-post Joomla articles to Matrix / Element." diff --git a/source/packages/plg_mokosuitecross_matrix/language/index.html b/source/packages/plg_mokosuitecross_matrix/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_matrix/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_matrix/matrix.php b/source/packages/plg_mokosuitecross_matrix/matrix.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_matrix/matrix.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_matrix/matrix.xml b/source/packages/plg_mokosuitecross_matrix/matrix.xml new file mode 100644 index 0000000..5f96dfc --- /dev/null +++ b/source/packages/plg_mokosuitecross_matrix/matrix.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - Matrix / Element + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_MATRIX_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Matrix + + + matrix.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_matrix.ini + language/en-GB/plg_mokosuitecross_matrix.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_matrix/services/index.html b/source/packages/plg_mokosuitecross_matrix/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_matrix/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_matrix/services/provider.php b/source/packages/plg_mokosuitecross_matrix/services/provider.php new file mode 100644 index 0000000..fb7ec7c --- /dev/null +++ b/source/packages/plg_mokosuitecross_matrix/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\Matrix\Extension\MatrixService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new MatrixService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'matrix') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_matrix/src/Extension/MatrixService.php b/source/packages/plg_mokosuitecross_matrix/src/Extension/MatrixService.php new file mode 100644 index 0000000..06e140f --- /dev/null +++ b/source/packages/plg_mokosuitecross_matrix/src/Extension/MatrixService.php @@ -0,0 +1,143 @@ + + * @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\Matrix\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Matrix / Element service plugin for MokoSuiteCross. + * + * Uses the Matrix Client-Server API v3 to send messages to rooms. + * Message send uses PUT with a transaction ID to prevent duplicates. + */ +class MatrixService 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 'matrix'; } + public function getServiceName(): string { return 'Matrix / Element'; } + public function getMaxLength(): int { return 65536; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $homeserver = rtrim($credentials['homeserver'] ?? '', '/'); + $token = $credentials['access_token'] ?? ''; + $roomId = $credentials['room_id'] ?? ''; + + if (empty($homeserver) || empty($token) || empty($roomId)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing homeserver, access token, or room ID.']]; + } + + // Matrix uses PUT with a transaction ID to ensure idempotency + $txnId = bin2hex(random_bytes(16)); + $apiUrl = $homeserver . '/_matrix/client/v3/rooms/' + . rawurlencode($roomId) . '/send/m.room.message/' . $txnId; + + $payload = json_encode([ + 'msgtype' => 'm.text', + 'body' => strip_tags($message), + 'format' => 'org.matrix.custom.html', + 'formatted_body' => $message, + ]); + + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_CUSTOMREQUEST => 'PUT', + CURLOPT_POSTFIELDS => $payload, + 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 && !empty($data['event_id'])) { + return ['success' => true, 'platform_post_id' => $data['event_id'], 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $homeserver = rtrim($credentials['homeserver'] ?? '', '/'); + $token = $credentials['access_token'] ?? ''; + $roomId = $credentials['room_id'] ?? ''; + + if (empty($homeserver) || empty($token) || empty($roomId)) { + return ['valid' => false, 'message' => 'Homeserver, access token, and room ID are required.', 'account_name' => '']; + } + + $ch = curl_init($homeserver . '/_matrix/client/v3/account/whoami'); + 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['user_id'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['user_id']]; + } + + return ['valid' => false, 'message' => $data['error'] ?? 'Failed to verify credentials.', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image', 'video']; + } +} diff --git a/source/packages/plg_mokosuitecross_matrix/src/Extension/index.html b/source/packages/plg_mokosuitecross_matrix/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_matrix/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_matrix/src/index.html b/source/packages/plg_mokosuitecross_matrix/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_matrix/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_medium/index.html b/source/packages/plg_mokosuitecross_medium/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_medium/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_medium/language/en-GB/index.html b/source/packages/plg_mokosuitecross_medium/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_medium/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_medium/language/en-GB/plg_mokosuitecross_medium.ini b/source/packages/plg_mokosuitecross_medium/language/en-GB/plg_mokosuitecross_medium.ini new file mode 100644 index 0000000..89d5686 --- /dev/null +++ b/source/packages/plg_mokosuitecross_medium/language/en-GB/plg_mokosuitecross_medium.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_MEDIUM="MokoSuiteCross - Medium" +PLG_MOKOSUITECROSS_MEDIUM_DESCRIPTION="Cross-post Joomla articles to Medium." diff --git a/source/packages/plg_mokosuitecross_medium/language/en-GB/plg_mokosuitecross_medium.sys.ini b/source/packages/plg_mokosuitecross_medium/language/en-GB/plg_mokosuitecross_medium.sys.ini new file mode 100644 index 0000000..89d5686 --- /dev/null +++ b/source/packages/plg_mokosuitecross_medium/language/en-GB/plg_mokosuitecross_medium.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_MEDIUM="MokoSuiteCross - Medium" +PLG_MOKOSUITECROSS_MEDIUM_DESCRIPTION="Cross-post Joomla articles to Medium." diff --git a/source/packages/plg_mokosuitecross_medium/language/index.html b/source/packages/plg_mokosuitecross_medium/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_medium/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_medium/medium.php b/source/packages/plg_mokosuitecross_medium/medium.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_medium/medium.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_medium/medium.xml b/source/packages/plg_mokosuitecross_medium/medium.xml new file mode 100644 index 0000000..747f431 --- /dev/null +++ b/source/packages/plg_mokosuitecross_medium/medium.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - Medium + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_MEDIUM_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Medium + + + medium.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_medium.ini + language/en-GB/plg_mokosuitecross_medium.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_medium/services/index.html b/source/packages/plg_mokosuitecross_medium/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_medium/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_medium/services/provider.php b/source/packages/plg_mokosuitecross_medium/services/provider.php new file mode 100644 index 0000000..de44b88 --- /dev/null +++ b/source/packages/plg_mokosuitecross_medium/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\Medium\Extension\MediumService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new MediumService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'medium') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_medium/src/Extension/MediumService.php b/source/packages/plg_mokosuitecross_medium/src/Extension/MediumService.php new file mode 100644 index 0000000..37dfb77 --- /dev/null +++ b/source/packages/plg_mokosuitecross_medium/src/Extension/MediumService.php @@ -0,0 +1,180 @@ + + * @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\Medium\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Medium service plugin for MokoSuiteCross. + * + * Uses the Medium Publishing API. Requires fetching the user ID first + * via /v1/me, then posting to /v1/users/{id}/posts. + */ +class MediumService 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 'medium'; } + public function getServiceName(): string { return 'Medium'; } + public function getMaxLength(): int { return 0; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $token = $credentials['access_token'] ?? ''; + + if (empty($token)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token.']]; + } + + // Step 1: Get the authenticated user's ID + $userId = $this->getUserId($token); + + if (empty($userId)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Failed to fetch Medium user ID.']]; + } + + // Step 2: Create the post + $apiUrl = 'https://api.medium.com/v1/users/' . $userId . '/posts'; + $title = mb_substr(strip_tags($message), 0, 150); + $content = $message; + + // Prepend image if provided + if (!empty($media[0])) { + $content = '
' . "\n\n" . $content; + } + + $publishStatus = $this->params->get('publish_status', 'draft'); + + $payload = json_encode([ + 'title' => $title, + 'contentFormat' => 'html', + 'content' => $content, + 'publishStatus' => $publishStatus, + ]); + + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + 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 === 201 && !empty($data['data']['id'])) { + return ['success' => true, 'platform_post_id' => $data['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' => 'Access token is required.', 'account_name' => '']; + } + + $ch = curl_init('https://api.medium.com/v1/me'); + 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['data']['username'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => '@' . $data['data']['username']]; + } + + return ['valid' => false, 'message' => 'Failed to verify token.', 'account_name' => '']; + } + + private function getUserId(string $token): string + { + $ch = curl_init('https://api.medium.com/v1/me'); + 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) ?: []; + + return $data['data']['id'] ?? ''; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_medium/src/Extension/index.html b/source/packages/plg_mokosuitecross_medium/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_medium/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_medium/src/index.html b/source/packages/plg_mokosuitecross_medium/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_medium/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_mokosuitecalendar/language/en-GB/plg_mokosuitecross_mokosuitecalendar.ini b/source/packages/plg_mokosuitecross_mokosuitecalendar/language/en-GB/plg_mokosuitecross_mokosuitecalendar.ini new file mode 100644 index 0000000..eb63b98 --- /dev/null +++ b/source/packages/plg_mokosuitecross_mokosuitecalendar/language/en-GB/plg_mokosuitecross_mokosuitecalendar.ini @@ -0,0 +1,14 @@ +; MokoSuiteCross - MokoSuiteCalendar Events Service +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR="MokoSuiteCross - MokoSuiteCalendar Events" +PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_DESCRIPTION="Cross-posts MokoSuiteCalendar events to connected platforms. Enriches messages with event date, time, location, and calendar details." + +PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_FIELDSET_DEFAULTS="Event Cross-Post Settings" +PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_INCLUDE_LOCATION="Include Location" +PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_INCLUDE_LOCATION_DESC="Append the event location to the cross-post message." +PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_INCLUDE_DATE="Include Date/Time" +PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_INCLUDE_DATE_DESC="Append the event date and time to the cross-post message." +PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_DATE_FORMAT="Date Format" +PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_DATE_FORMAT_DESC="PHP date format string for event dates. Default: l, F j, Y at g:ia" diff --git a/source/packages/plg_mokosuitecross_mokosuitecalendar/language/en-GB/plg_mokosuitecross_mokosuitecalendar.sys.ini b/source/packages/plg_mokosuitecross_mokosuitecalendar/language/en-GB/plg_mokosuitecross_mokosuitecalendar.sys.ini new file mode 100644 index 0000000..dd03880 --- /dev/null +++ b/source/packages/plg_mokosuitecross_mokosuitecalendar/language/en-GB/plg_mokosuitecross_mokosuitecalendar.sys.ini @@ -0,0 +1,6 @@ +; MokoSuiteCross - MokoSuiteCalendar Events Service +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR="Plugin - MokoSuiteCross MokoSuiteCalendar Events" +PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_DESCRIPTION="Cross-posts MokoSuiteCalendar events to connected platforms." diff --git a/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.php b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.php new file mode 100644 index 0000000..826d027 --- /dev/null +++ b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.php @@ -0,0 +1,14 @@ + + * @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 + * + * Legacy entry point — required by Joomla's plugin loader. + */ + +defined('_JEXEC') or die; diff --git a/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml new file mode 100644 index 0000000..eaa1e74 --- /dev/null +++ b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml @@ -0,0 +1,62 @@ + + + MokoSuiteCross - MokoSuiteCalendar Events + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_MOKOJOOMCALENDAR_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\MokoSuiteCalendar + + + mokosuitecalendar.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_mokosuitecalendar.ini + language/en-GB/plg_mokosuitecross_mokosuitecalendar.sys.ini + + + + +
+ + + + + + + + + +
+
+
+
diff --git a/source/packages/plg_mokosuitecross_mokosuitecalendar/services/provider.php b/source/packages/plg_mokosuitecross_mokosuitecalendar/services/provider.php new file mode 100644 index 0000000..e4abc85 --- /dev/null +++ b/source/packages/plg_mokosuitecross_mokosuitecalendar/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\MokoSuiteCalendar\Extension\CalendarService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new CalendarService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'mokosuitecalendar') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_mokosuitecalendar/src/Extension/CalendarService.php b/source/packages/plg_mokosuitecross_mokosuitecalendar/src/Extension/CalendarService.php new file mode 100644 index 0000000..5e01527 --- /dev/null +++ b/source/packages/plg_mokosuitecross_mokosuitecalendar/src/Extension/CalendarService.php @@ -0,0 +1,187 @@ + + * @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\MokoSuiteCalendar\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\CMS\Uri\Uri; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * MokoSuiteCalendar service plugin for MokoSuiteCross. + * + * Cross-posts calendar events when they are published. + * Enriches the message with event date, time, location, and calendar name. + * + * Credentials format: + * { + * "site_url": "https://example.com" // Optional, defaults to current site + * } + */ +class CalendarService 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 'mokosuitecalendar'; + } + + public function getServiceName(): string + { + return 'MokoSuiteCalendar Events'; + } + + public function getMaxLength(): int + { + return 0; + } + + public function supportsMedia(): bool + { + return true; + } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $db = Factory::getDbo(); + + $articleId = (int) ($params['_article_id'] ?? 0); + + if (!$articleId) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'No article ID']]; + } + + // Load the event data from mokosuitecalendar + $query = $db->getQuery(true) + ->select('e.*, c.title AS calendar_title, c.color AS calendar_color, l.title AS location_title, l.address, l.city, l.state') + ->from($db->quoteName('#__mokosuitecalendar_events', 'e')) + ->leftJoin($db->quoteName('#__mokosuitecalendar_calendars', 'c') . ' ON c.id = e.calendar_id') + ->leftJoin($db->quoteName('#__mokosuitecalendar_locations', 'l') . ' ON l.id = e.location_id') + ->where($db->quoteName('e.id') . ' = ' . $articleId); + + $db->setQuery($query); + $event = $db->loadObject(); + + if (!$event) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Event not found']]; + } + + // Enrich the message with event metadata + $enriched = $this->enrichMessage($message, $event, $params); + + // Build media array from event image + if (empty($media) && !empty($event->image)) { + $siteUrl = rtrim($credentials['site_url'] ?? Uri::root(), '/'); + $media = [$siteUrl . '/' . ltrim($event->image, '/')]; + } + + // Store enriched post internally (this plugin creates queue entries for other services) + return [ + 'success' => true, + 'platform_post_id' => 'event-' . $event->id, + 'response' => [ + 'event_id' => (int) $event->id, + 'event_title' => $event->title, + 'calendar' => $event->calendar_title, + 'start_date' => $event->start_date, + 'end_date' => $event->end_date, + 'location' => $event->location_title, + 'enriched_message' => $enriched, + 'media' => $media, + ], + ]; + } + + public function validateCredentials(array $credentials): array + { + // Check if com_mokosuitecalendar is installed + if (!file_exists(JPATH_ADMINISTRATOR . '/components/com_mokosuitecalendar')) { + return [ + 'valid' => false, + 'message' => 'MokoSuiteCalendar component is not installed.', + 'account_name' => '', + ]; + } + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuitecalendar_events')); + $db->setQuery($query); + $count = (int) $db->loadResult(); + + return [ + 'valid' => true, + 'message' => "Connected. {$count} event(s) in database.", + 'account_name' => 'MokoSuiteCalendar', + ]; + } + + /** + * Enrich the cross-post message with event-specific data. + */ + private function enrichMessage(string $message, object $event, array $params): string + { + $parts = [$message]; + + // Add date/time + if ((int) ($params['include_date'] ?? $this->params->get('include_date', 1))) { + $format = $params['date_format'] ?? $this->params->get('date_format', 'l, F j, Y \\a\\t g:ia'); + $startDate = Factory::getDate($event->start_date); + $dateStr = $startDate->format($format); + + if (!empty($event->end_date) && $event->end_date !== $event->start_date) { + $endDate = Factory::getDate($event->end_date); + $dateStr .= ' — ' . $endDate->format($format); + } + + if (!empty($event->all_day)) { + $dateStr = $startDate->format('l, F j, Y') . ' (All Day)'; + } + + $parts[] = $dateStr; + } + + // Add location + if ((int) ($params['include_location'] ?? $this->params->get('include_location', 1))) { + $locationParts = array_filter([ + $event->location_title ?? '', + $event->address ?? '', + $event->city ?? '', + $event->state ?? '', + ]); + + if (!empty($locationParts)) { + $parts[] = implode(', ', $locationParts); + } + } + + return implode("\n\n", array_filter($parts)); + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_mokosuitegallery/language/en-GB/plg_mokosuitecross_mokosuitegallery.ini b/source/packages/plg_mokosuitecross_mokosuitegallery/language/en-GB/plg_mokosuitecross_mokosuitegallery.ini new file mode 100644 index 0000000..83d6183 --- /dev/null +++ b/source/packages/plg_mokosuitecross_mokosuitegallery/language/en-GB/plg_mokosuitecross_mokosuitegallery.ini @@ -0,0 +1,16 @@ +; MokoSuiteCross - MokoSuiteGallery Service +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_MOKOSUITECROSS_MOKOJOOMGALLERY="MokoSuiteCross - MokoSuiteGallery" +PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_DESCRIPTION="Cross-posts MokoSuiteGallery content to connected platforms. Supports gallery announcements with preview images and individual image posts." + +PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_FIELDSET_DEFAULTS="Gallery Cross-Post Settings" +PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_POST_MODE="Post Mode" +PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_POST_MODE_DESC="Gallery mode posts when a gallery is published (with preview images). Image mode posts each individual image." +PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_MODE_GALLERY="Gallery (with preview images)" +PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_MODE_IMAGE="Individual Images" +PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_MAX_IMAGES="Max Preview Images" +PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_MAX_IMAGES_DESC="Maximum number of preview images to attach when cross-posting a gallery." +PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_INCLUDE_DESCRIPTION="Include Description" +PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_INCLUDE_DESCRIPTION_DESC="Append the gallery or image description to the cross-post message." diff --git a/source/packages/plg_mokosuitecross_mokosuitegallery/language/en-GB/plg_mokosuitecross_mokosuitegallery.sys.ini b/source/packages/plg_mokosuitecross_mokosuitegallery/language/en-GB/plg_mokosuitecross_mokosuitegallery.sys.ini new file mode 100644 index 0000000..531d64d --- /dev/null +++ b/source/packages/plg_mokosuitecross_mokosuitegallery/language/en-GB/plg_mokosuitecross_mokosuitegallery.sys.ini @@ -0,0 +1,6 @@ +; MokoSuiteCross - MokoSuiteGallery Service +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_MOKOSUITECROSS_MOKOJOOMGALLERY="Plugin - MokoSuiteCross MokoSuiteGallery" +PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_DESCRIPTION="Cross-posts MokoSuiteGallery galleries and images to connected platforms." diff --git a/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.php b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.php new file mode 100644 index 0000000..1d672d6 --- /dev/null +++ b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.php @@ -0,0 +1,14 @@ + + * @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 + * + * Legacy entry point — required by Joomla's plugin loader. + */ + +defined('_JEXEC') or die; diff --git a/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml new file mode 100644 index 0000000..7398c47 --- /dev/null +++ b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml @@ -0,0 +1,63 @@ + + + MokoSuiteCross - MokoSuiteGallery + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_MOKOJOOMGALLERY_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\MokoSuiteGallery + + + mokosuitegallery.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_mokosuitegallery.ini + language/en-GB/plg_mokosuitecross_mokosuitegallery.sys.ini + + + + +
+ + + + + + + + + +
+
+
+
diff --git a/source/packages/plg_mokosuitecross_mokosuitegallery/services/provider.php b/source/packages/plg_mokosuitecross_mokosuitegallery/services/provider.php new file mode 100644 index 0000000..f4e0635 --- /dev/null +++ b/source/packages/plg_mokosuitecross_mokosuitegallery/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\MokoSuiteGallery\Extension\GalleryService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new GalleryService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'mokosuitegallery') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_mokosuitegallery/src/Extension/GalleryService.php b/source/packages/plg_mokosuitecross_mokosuitegallery/src/Extension/GalleryService.php new file mode 100644 index 0000000..e9248fb --- /dev/null +++ b/source/packages/plg_mokosuitecross_mokosuitegallery/src/Extension/GalleryService.php @@ -0,0 +1,249 @@ + + * @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\MokoSuiteGallery\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\CMS\Uri\Uri; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * MokoSuiteGallery service plugin for MokoSuiteCross. + * + * Cross-posts gallery content when new images or galleries are published. + * Two modes: + * - "gallery" mode: posts when a gallery is created/published (includes preview images) + * - "image" mode: posts each individual image when published + * + * Credentials format: + * { + * "site_url": "https://example.com" // Optional, defaults to current site + * } + */ +class GalleryService 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 'mokosuitegallery'; + } + + public function getServiceName(): string + { + return 'MokoSuiteGallery'; + } + + public function getMaxLength(): int + { + return 0; + } + + public function supportsMedia(): bool + { + return true; + } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $db = Factory::getDbo(); + $siteUrl = rtrim($credentials['site_url'] ?? Uri::root(), '/'); + $mode = $params['post_mode'] ?? $this->params->get('post_mode', 'gallery'); + + $articleId = (int) ($params['_article_id'] ?? 0); + + if (!$articleId) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'No content ID']]; + } + + if ($mode === 'image') { + return $this->publishImage($articleId, $message, $media, $siteUrl, $params); + } + + return $this->publishGallery($articleId, $message, $media, $siteUrl, $params); + } + + public function validateCredentials(array $credentials): array + { + if (!file_exists(JPATH_ADMINISTRATOR . '/components/com_mokosuitegallery')) { + return [ + 'valid' => false, + 'message' => 'MokoSuiteGallery component is not installed.', + 'account_name' => '', + ]; + } + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuitegallery_galleries')); + $db->setQuery($query); + $galleries = (int) $db->loadResult(); + + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuitegallery_images')); + $db->setQuery($query); + $images = (int) $db->loadResult(); + + return [ + 'valid' => true, + 'message' => "Connected. {$galleries} gallery(ies), {$images} image(s).", + 'account_name' => 'MokoSuiteGallery', + ]; + } + + /** + * Cross-post a gallery with preview images. + */ + private function publishGallery(int $galleryId, string $message, array $media, string $siteUrl, array $params): array + { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokosuitegallery_galleries')) + ->where($db->quoteName('id') . ' = ' . $galleryId); + $db->setQuery($query); + $gallery = $db->loadObject(); + + if (!$gallery) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Gallery not found']]; + } + + // Get preview images + $maxImages = (int) ($params['max_images'] ?? $this->params->get('max_images', 4)); + + $query = $db->getQuery(true) + ->select($db->quoteName(['thumbnail', 'original', 'title'])) + ->from($db->quoteName('#__mokosuitegallery_images')) + ->where($db->quoteName('gallery_id') . ' = ' . $galleryId) + ->where($db->quoteName('published') . ' = 1') + ->order($db->quoteName('ordering') . ' ASC') + ->setLimit($maxImages); + $db->setQuery($query); + $images = $db->loadObjectList(); + + // Build media URLs from gallery images + if (empty($media) && !empty($images)) { + $media = []; + + foreach ($images as $img) { + $path = $img->thumbnail ?: $img->original; + + if ($path) { + $media[] = $siteUrl . '/' . ltrim($path, '/'); + } + } + } + + // Enrich message with gallery info + $enriched = $message; + + if ((int) ($params['include_description'] ?? $this->params->get('include_description', 1))) { + $desc = strip_tags($gallery->description ?? ''); + + if ($desc !== '') { + $enriched .= "\n\n" . mb_substr($desc, 0, 200); + } + } + + $imageCount = $db->setQuery( + $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuitegallery_images')) + ->where($db->quoteName('gallery_id') . ' = ' . $galleryId) + ->where($db->quoteName('published') . ' = 1') + )->loadResult(); + + return [ + 'success' => true, + 'platform_post_id' => 'gallery-' . $galleryId, + 'response' => [ + 'gallery_id' => $galleryId, + 'gallery_title' => $gallery->title, + 'image_count' => (int) $imageCount, + 'preview_count' => count($images), + 'enriched_message' => $enriched, + 'media' => $media, + ], + ]; + } + + /** + * Cross-post a single image. + */ + private function publishImage(int $imageId, string $message, array $media, string $siteUrl, array $params): array + { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select('i.*, g.title AS gallery_title') + ->from($db->quoteName('#__mokosuitegallery_images', 'i')) + ->leftJoin($db->quoteName('#__mokosuitegallery_galleries', 'g') . ' ON g.id = i.gallery_id') + ->where($db->quoteName('i.id') . ' = ' . $imageId); + $db->setQuery($query); + $image = $db->loadObject(); + + if (!$image) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Image not found']]; + } + + // Use image as media + if (empty($media)) { + $path = $image->original ?: $image->thumbnail; + + if ($path) { + $media = [$siteUrl . '/' . ltrim($path, '/')]; + } + } + + // Enrich with image description + $enriched = $message; + + if ((int) ($params['include_description'] ?? $this->params->get('include_description', 1))) { + $desc = strip_tags($image->description ?? ''); + + if ($desc !== '') { + $enriched .= "\n\n" . mb_substr($desc, 0, 200); + } + } + + return [ + 'success' => true, + 'platform_post_id' => 'image-' . $imageId, + 'response' => [ + 'image_id' => $imageId, + 'image_title' => $image->title, + 'gallery_title' => $image->gallery_title, + 'enriched_message' => $enriched, + 'media' => $media, + ], + ]; + } + + public function getSupportedMediaTypes(): array + { + return ['image', 'video']; + } +} diff --git a/source/packages/plg_mokosuitecross_nostr/index.html b/source/packages/plg_mokosuitecross_nostr/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_nostr/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_nostr/language/en-GB/index.html b/source/packages/plg_mokosuitecross_nostr/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_nostr/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_nostr/language/en-GB/plg_mokosuitecross_nostr.ini b/source/packages/plg_mokosuitecross_nostr/language/en-GB/plg_mokosuitecross_nostr.ini new file mode 100644 index 0000000..3c87b21 --- /dev/null +++ b/source/packages/plg_mokosuitecross_nostr/language/en-GB/plg_mokosuitecross_nostr.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_NOSTR="MokoSuiteCross - Nostr" +PLG_MOKOSUITECROSS_NOSTR_DESCRIPTION="Cross-post Joomla articles to Nostr." diff --git a/source/packages/plg_mokosuitecross_nostr/language/en-GB/plg_mokosuitecross_nostr.sys.ini b/source/packages/plg_mokosuitecross_nostr/language/en-GB/plg_mokosuitecross_nostr.sys.ini new file mode 100644 index 0000000..3c87b21 --- /dev/null +++ b/source/packages/plg_mokosuitecross_nostr/language/en-GB/plg_mokosuitecross_nostr.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_NOSTR="MokoSuiteCross - Nostr" +PLG_MOKOSUITECROSS_NOSTR_DESCRIPTION="Cross-post Joomla articles to Nostr." diff --git a/source/packages/plg_mokosuitecross_nostr/language/index.html b/source/packages/plg_mokosuitecross_nostr/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_nostr/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_nostr/nostr.php b/source/packages/plg_mokosuitecross_nostr/nostr.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_nostr/nostr.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_nostr/nostr.xml b/source/packages/plg_mokosuitecross_nostr/nostr.xml new file mode 100644 index 0000000..fd5c2ab --- /dev/null +++ b/source/packages/plg_mokosuitecross_nostr/nostr.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - Nostr + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_NOSTR_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Nostr + + + nostr.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_nostr.ini + language/en-GB/plg_mokosuitecross_nostr.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_nostr/services/index.html b/source/packages/plg_mokosuitecross_nostr/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_nostr/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_nostr/services/provider.php b/source/packages/plg_mokosuitecross_nostr/services/provider.php new file mode 100644 index 0000000..cc8af75 --- /dev/null +++ b/source/packages/plg_mokosuitecross_nostr/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\Nostr\Extension\NostrService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new NostrService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'nostr') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_nostr/src/Extension/NostrService.php b/source/packages/plg_mokosuitecross_nostr/src/Extension/NostrService.php new file mode 100644 index 0000000..56a5eed --- /dev/null +++ b/source/packages/plg_mokosuitecross_nostr/src/Extension/NostrService.php @@ -0,0 +1,97 @@ + + * @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\Nostr\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Nostr service plugin for MokoSuiteCross. + * + * Nostr uses NIP-01 WebSocket relays for event publishing. + * This is a stub — full WebSocket implementation is deferred. + * Events are signed with the private key and sent to configured relays. + */ +class NostrService 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 'nostr'; } + public function getServiceName(): string { return 'Nostr'; } + public function getMaxLength(): int { return 0; } + public function supportsMedia(): bool { return false; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $privateKey = $credentials['private_key'] ?? ''; + $relays = $credentials['relays'] ?? ''; + + if (empty($privateKey) || empty($relays)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing private key or relay URLs.']]; + } + + // Nostr requires WebSocket connections to relays (wss://). + // Full NIP-01 event signing and relay publishing is not yet implemented. + return [ + 'success' => false, + 'platform_post_id' => '', + 'response' => ['error' => 'Nostr WebSocket relay publishing is not yet implemented. This service will be available in a future release.'], + ]; + } + + public function validateCredentials(array $credentials): array + { + $privateKey = $credentials['private_key'] ?? ''; + $relays = $credentials['relays'] ?? ''; + + if (empty($privateKey)) { + return ['valid' => false, 'message' => 'Private key is required.', 'account_name' => '']; + } + + if (empty($relays)) { + return ['valid' => false, 'message' => 'At least one relay URL is required.', 'account_name' => '']; + } + + // Validate that relay URLs look like WebSocket URLs + $relayList = array_filter(array_map('trim', explode(',', $relays))); + $valid = true; + + foreach ($relayList as $relay) { + if (!str_starts_with($relay, 'wss://') && !str_starts_with($relay, 'ws://')) { + $valid = false; + break; + } + } + + if (!$valid) { + return ['valid' => false, 'message' => 'Relay URLs must start with wss:// or ws://.', 'account_name' => '']; + } + + return ['valid' => true, 'message' => 'Credentials configured (' . count($relayList) . ' relay(s))', 'account_name' => 'Nostr']; + } + + public function getSupportedMediaTypes(): array + { + return []; + } +} diff --git a/source/packages/plg_mokosuitecross_nostr/src/Extension/index.html b/source/packages/plg_mokosuitecross_nostr/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_nostr/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_nostr/src/index.html b/source/packages/plg_mokosuitecross_nostr/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_nostr/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_ntfy/index.html b/source/packages/plg_mokosuitecross_ntfy/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_ntfy/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_ntfy/language/en-GB/index.html b/source/packages/plg_mokosuitecross_ntfy/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_ntfy/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_ntfy/language/en-GB/plg_mokosuitecross_ntfy.ini b/source/packages/plg_mokosuitecross_ntfy/language/en-GB/plg_mokosuitecross_ntfy.ini new file mode 100644 index 0000000..9d9d2ef --- /dev/null +++ b/source/packages/plg_mokosuitecross_ntfy/language/en-GB/plg_mokosuitecross_ntfy.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_NTFY="MokoSuiteCross - Ntfy Push Notifications" +PLG_MOKOSUITECROSS_NTFY_DESCRIPTION="Cross-post Joomla articles to Ntfy Push Notifications." diff --git a/source/packages/plg_mokosuitecross_ntfy/language/en-GB/plg_mokosuitecross_ntfy.sys.ini b/source/packages/plg_mokosuitecross_ntfy/language/en-GB/plg_mokosuitecross_ntfy.sys.ini new file mode 100644 index 0000000..9d9d2ef --- /dev/null +++ b/source/packages/plg_mokosuitecross_ntfy/language/en-GB/plg_mokosuitecross_ntfy.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_NTFY="MokoSuiteCross - Ntfy Push Notifications" +PLG_MOKOSUITECROSS_NTFY_DESCRIPTION="Cross-post Joomla articles to Ntfy Push Notifications." diff --git a/source/packages/plg_mokosuitecross_ntfy/language/index.html b/source/packages/plg_mokosuitecross_ntfy/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_ntfy/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_ntfy/ntfy.php b/source/packages/plg_mokosuitecross_ntfy/ntfy.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_ntfy/ntfy.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_ntfy/ntfy.xml b/source/packages/plg_mokosuitecross_ntfy/ntfy.xml new file mode 100644 index 0000000..cfe9bec --- /dev/null +++ b/source/packages/plg_mokosuitecross_ntfy/ntfy.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - Ntfy Push Notifications + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_NTFY_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Ntfy + + + ntfy.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_ntfy.ini + language/en-GB/plg_mokosuitecross_ntfy.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_ntfy/services/index.html b/source/packages/plg_mokosuitecross_ntfy/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_ntfy/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_ntfy/services/provider.php b/source/packages/plg_mokosuitecross_ntfy/services/provider.php new file mode 100644 index 0000000..8a5ff57 --- /dev/null +++ b/source/packages/plg_mokosuitecross_ntfy/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\Ntfy\Extension\NtfyService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new NtfyService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'ntfy') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_ntfy/src/Extension/NtfyService.php b/source/packages/plg_mokosuitecross_ntfy/src/Extension/NtfyService.php new file mode 100644 index 0000000..abf7953 --- /dev/null +++ b/source/packages/plg_mokosuitecross_ntfy/src/Extension/NtfyService.php @@ -0,0 +1,111 @@ + + * @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\Ntfy\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Ntfy Push Notifications service plugin for MokoSuiteCross. + * + * API: {server_url}/{topic} + */ +class NtfyService 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 'ntfy'; } + public function getServiceName(): string { return 'Ntfy Push Notifications'; } + public function getMaxLength(): int { return 4096; } + public function supportsMedia(): bool { return false; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $url = $credentials['topic'] ?? $credentials['webhook_url'] ?? ''; + + $serverUrl = rtrim($credentials['server_url'] ?? 'https://ntfy.sh', '/'); + $topic = $credentials['topic'] ?? ''; + $token = $credentials['token'] ?? ''; + + if (empty($topic)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing topic']]; + } + + $headers = ['Content-Type: text/plain', 'Title: ' . ($params['title'] ?? 'New Article')]; + + if (!empty($token)) { + $headers[] = 'Authorization: Bearer ' . $token; + } + + if (!empty($params['url'])) { + $headers[] = 'Click: ' . $params['url']; + } + + $ch = curl_init($serverUrl . '/' . $topic); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $message, + CURLOPT_HTTPHEADER => $headers, + 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) ?: ['raw' => $response]; + + 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 + { + $key = $credentials['topic'] ?? $credentials['webhook_url'] ?? ''; + + if (empty($key)) { + return ['valid' => false, 'message' => 'Missing credentials', 'account_name' => '']; + } + + return ['valid' => true, 'message' => 'Credentials configured', 'account_name' => 'Ntfy Push Notifications']; + } + + public function getSupportedMediaTypes(): array + { + return []; + } +} diff --git a/source/packages/plg_mokosuitecross_ntfy/src/Extension/index.html b/source/packages/plg_mokosuitecross_ntfy/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_ntfy/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_ntfy/src/index.html b/source/packages/plg_mokosuitecross_ntfy/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_ntfy/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_pinterest/index.html b/source/packages/plg_mokosuitecross_pinterest/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_pinterest/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_pinterest/language/en-GB/index.html b/source/packages/plg_mokosuitecross_pinterest/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_pinterest/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_pinterest/language/en-GB/plg_mokosuitecross_pinterest.ini b/source/packages/plg_mokosuitecross_pinterest/language/en-GB/plg_mokosuitecross_pinterest.ini new file mode 100644 index 0000000..602fef8 --- /dev/null +++ b/source/packages/plg_mokosuitecross_pinterest/language/en-GB/plg_mokosuitecross_pinterest.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_PINTEREST="MokoSuiteCross - Pinterest" +PLG_MOKOSUITECROSS_PINTEREST_DESCRIPTION="Cross-post Joomla articles to Pinterest." diff --git a/source/packages/plg_mokosuitecross_pinterest/language/en-GB/plg_mokosuitecross_pinterest.sys.ini b/source/packages/plg_mokosuitecross_pinterest/language/en-GB/plg_mokosuitecross_pinterest.sys.ini new file mode 100644 index 0000000..602fef8 --- /dev/null +++ b/source/packages/plg_mokosuitecross_pinterest/language/en-GB/plg_mokosuitecross_pinterest.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_PINTEREST="MokoSuiteCross - Pinterest" +PLG_MOKOSUITECROSS_PINTEREST_DESCRIPTION="Cross-post Joomla articles to Pinterest." diff --git a/source/packages/plg_mokosuitecross_pinterest/language/index.html b/source/packages/plg_mokosuitecross_pinterest/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_pinterest/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_pinterest/pinterest.php b/source/packages/plg_mokosuitecross_pinterest/pinterest.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_pinterest/pinterest.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_pinterest/pinterest.xml b/source/packages/plg_mokosuitecross_pinterest/pinterest.xml new file mode 100644 index 0000000..8709ec0 --- /dev/null +++ b/source/packages/plg_mokosuitecross_pinterest/pinterest.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - Pinterest + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_PINTEREST_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Pinterest + + + pinterest.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_pinterest.ini + language/en-GB/plg_mokosuitecross_pinterest.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_pinterest/services/index.html b/source/packages/plg_mokosuitecross_pinterest/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_pinterest/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_pinterest/services/provider.php b/source/packages/plg_mokosuitecross_pinterest/services/provider.php new file mode 100644 index 0000000..b33a49d --- /dev/null +++ b/source/packages/plg_mokosuitecross_pinterest/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\Pinterest\Extension\PinterestService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new PinterestService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'pinterest') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_pinterest/src/Extension/index.html b/source/packages/plg_mokosuitecross_pinterest/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_pinterest/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_pinterest/src/index.html b/source/packages/plg_mokosuitecross_pinterest/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_pinterest/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_reddit/index.html b/source/packages/plg_mokosuitecross_reddit/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_reddit/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_reddit/language/en-GB/index.html b/source/packages/plg_mokosuitecross_reddit/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_reddit/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_reddit/language/en-GB/plg_mokosuitecross_reddit.ini b/source/packages/plg_mokosuitecross_reddit/language/en-GB/plg_mokosuitecross_reddit.ini new file mode 100644 index 0000000..9723946 --- /dev/null +++ b/source/packages/plg_mokosuitecross_reddit/language/en-GB/plg_mokosuitecross_reddit.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_REDDIT="MokoSuiteCross - Reddit" +PLG_MOKOSUITECROSS_REDDIT_DESCRIPTION="Cross-post Joomla articles to Reddit." diff --git a/source/packages/plg_mokosuitecross_reddit/language/en-GB/plg_mokosuitecross_reddit.sys.ini b/source/packages/plg_mokosuitecross_reddit/language/en-GB/plg_mokosuitecross_reddit.sys.ini new file mode 100644 index 0000000..9723946 --- /dev/null +++ b/source/packages/plg_mokosuitecross_reddit/language/en-GB/plg_mokosuitecross_reddit.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_REDDIT="MokoSuiteCross - Reddit" +PLG_MOKOSUITECROSS_REDDIT_DESCRIPTION="Cross-post Joomla articles to Reddit." diff --git a/source/packages/plg_mokosuitecross_reddit/language/index.html b/source/packages/plg_mokosuitecross_reddit/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_reddit/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_reddit/reddit.php b/source/packages/plg_mokosuitecross_reddit/reddit.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_reddit/reddit.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_reddit/reddit.xml b/source/packages/plg_mokosuitecross_reddit/reddit.xml new file mode 100644 index 0000000..d90fbaf --- /dev/null +++ b/source/packages/plg_mokosuitecross_reddit/reddit.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - Reddit + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_REDDIT_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Reddit + + + reddit.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_reddit.ini + language/en-GB/plg_mokosuitecross_reddit.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_reddit/services/index.html b/source/packages/plg_mokosuitecross_reddit/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_reddit/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_reddit/services/provider.php b/source/packages/plg_mokosuitecross_reddit/services/provider.php new file mode 100644 index 0000000..adbae3c --- /dev/null +++ b/source/packages/plg_mokosuitecross_reddit/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\Reddit\Extension\RedditService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new RedditService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'reddit') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_reddit/src/Extension/index.html b/source/packages/plg_mokosuitecross_reddit/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_reddit/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_reddit/src/index.html b/source/packages/plg_mokosuitecross_reddit/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_reddit/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_rssfeed/index.html b/source/packages/plg_mokosuitecross_rssfeed/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_rssfeed/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_rssfeed/language/en-GB/index.html b/source/packages/plg_mokosuitecross_rssfeed/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_rssfeed/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_rssfeed/language/en-GB/plg_mokosuitecross_rssfeed.ini b/source/packages/plg_mokosuitecross_rssfeed/language/en-GB/plg_mokosuitecross_rssfeed.ini new file mode 100644 index 0000000..fcbcbc7 --- /dev/null +++ b/source/packages/plg_mokosuitecross_rssfeed/language/en-GB/plg_mokosuitecross_rssfeed.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_RSSFEED="MokoSuiteCross - RSS Feed" +PLG_MOKOSUITECROSS_RSSFEED_DESCRIPTION="Cross-post Joomla articles to RSS Feed." diff --git a/source/packages/plg_mokosuitecross_rssfeed/language/en-GB/plg_mokosuitecross_rssfeed.sys.ini b/source/packages/plg_mokosuitecross_rssfeed/language/en-GB/plg_mokosuitecross_rssfeed.sys.ini new file mode 100644 index 0000000..fcbcbc7 --- /dev/null +++ b/source/packages/plg_mokosuitecross_rssfeed/language/en-GB/plg_mokosuitecross_rssfeed.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_RSSFEED="MokoSuiteCross - RSS Feed" +PLG_MOKOSUITECROSS_RSSFEED_DESCRIPTION="Cross-post Joomla articles to RSS Feed." diff --git a/source/packages/plg_mokosuitecross_rssfeed/language/index.html b/source/packages/plg_mokosuitecross_rssfeed/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_rssfeed/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_rssfeed/rssfeed.php b/source/packages/plg_mokosuitecross_rssfeed/rssfeed.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_rssfeed/rssfeed.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_rssfeed/rssfeed.xml b/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml new file mode 100644 index 0000000..38d493c --- /dev/null +++ b/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - RSS Feed + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_RSSFEED_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Rssfeed + + + rssfeed.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_rssfeed.ini + language/en-GB/plg_mokosuitecross_rssfeed.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_rssfeed/services/index.html b/source/packages/plg_mokosuitecross_rssfeed/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_rssfeed/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_rssfeed/services/provider.php b/source/packages/plg_mokosuitecross_rssfeed/services/provider.php new file mode 100644 index 0000000..30eea2b --- /dev/null +++ b/source/packages/plg_mokosuitecross_rssfeed/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\Rssfeed\Extension\RssfeedService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new RssfeedService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'rssfeed') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_rssfeed/src/Extension/RssfeedService.php b/source/packages/plg_mokosuitecross_rssfeed/src/Extension/RssfeedService.php new file mode 100644 index 0000000..afb5474 --- /dev/null +++ b/source/packages/plg_mokosuitecross_rssfeed/src/Extension/RssfeedService.php @@ -0,0 +1,66 @@ + + * @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\Rssfeed\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * RSS Feed service plugin for MokoSuiteCross. + * + * This is a local service — it doesn't call an external API. + * When an article is "published" to the RSS feed service, it simply + * marks the post as successful. The feed view reads from the posts + * table to generate the RSS/Atom XML output. + */ +class RssfeedService 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 'rssfeed'; } + public function getServiceName(): string { return 'RSS Feed'; } + public function getMaxLength(): int { return 0; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + // RSS Feed is a local service — no external API call needed. + // The post record in the queue table serves as the feed data source. + return [ + 'success' => true, + 'platform_post_id' => 'feed-' . time(), + 'response' => ['type' => 'rss_feed'], + ]; + } + + public function validateCredentials(array $credentials): array + { + // No credentials required for local RSS feed generation. + return ['valid' => true, 'message' => 'RSS feed is a local service — no credentials needed.', 'account_name' => 'RSS Feed']; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_rssfeed/src/Extension/index.html b/source/packages/plg_mokosuitecross_rssfeed/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_rssfeed/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_rssfeed/src/index.html b/source/packages/plg_mokosuitecross_rssfeed/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_rssfeed/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_sendgrid/index.html b/source/packages/plg_mokosuitecross_sendgrid/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_sendgrid/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_sendgrid/language/en-GB/index.html b/source/packages/plg_mokosuitecross_sendgrid/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_sendgrid/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_sendgrid/language/en-GB/plg_mokosuitecross_sendgrid.ini b/source/packages/plg_mokosuitecross_sendgrid/language/en-GB/plg_mokosuitecross_sendgrid.ini new file mode 100644 index 0000000..b8e39ff --- /dev/null +++ b/source/packages/plg_mokosuitecross_sendgrid/language/en-GB/plg_mokosuitecross_sendgrid.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_SENDGRID="MokoSuiteCross - SendGrid" +PLG_MOKOSUITECROSS_SENDGRID_DESCRIPTION="Cross-post Joomla articles to SendGrid." diff --git a/source/packages/plg_mokosuitecross_sendgrid/language/en-GB/plg_mokosuitecross_sendgrid.sys.ini b/source/packages/plg_mokosuitecross_sendgrid/language/en-GB/plg_mokosuitecross_sendgrid.sys.ini new file mode 100644 index 0000000..b8e39ff --- /dev/null +++ b/source/packages/plg_mokosuitecross_sendgrid/language/en-GB/plg_mokosuitecross_sendgrid.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_SENDGRID="MokoSuiteCross - SendGrid" +PLG_MOKOSUITECROSS_SENDGRID_DESCRIPTION="Cross-post Joomla articles to SendGrid." diff --git a/source/packages/plg_mokosuitecross_sendgrid/language/index.html b/source/packages/plg_mokosuitecross_sendgrid/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_sendgrid/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_sendgrid/sendgrid.php b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.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_sendgrid/sendgrid.xml b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml new file mode 100644 index 0000000..133c9d0 --- /dev/null +++ b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - SendGrid + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_SENDGRID_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Sendgrid + + + sendgrid.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_sendgrid.ini + language/en-GB/plg_mokosuitecross_sendgrid.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_sendgrid/services/index.html b/source/packages/plg_mokosuitecross_sendgrid/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_sendgrid/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_sendgrid/services/provider.php b/source/packages/plg_mokosuitecross_sendgrid/services/provider.php new file mode 100644 index 0000000..84cc520 --- /dev/null +++ b/source/packages/plg_mokosuitecross_sendgrid/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\Sendgrid\Extension\SendgridService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new SendgridService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'sendgrid') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_sendgrid/src/Extension/index.html b/source/packages/plg_mokosuitecross_sendgrid/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_sendgrid/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_sendgrid/src/index.html b/source/packages/plg_mokosuitecross_sendgrid/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_sendgrid/src/index.html @@ -0,0 +1 @@ + diff --git a/src/packages/plg_mokojoomcross_slack/language/index.html b/source/packages/plg_mokosuitecross_slack/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_slack/language/index.html rename to source/packages/plg_mokosuitecross_slack/index.html diff --git a/src/packages/plg_mokojoomcross_slack/services/index.html b/source/packages/plg_mokosuitecross_slack/language/en-GB/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_slack/services/index.html rename to source/packages/plg_mokosuitecross_slack/language/en-GB/index.html diff --git a/source/packages/plg_mokosuitecross_slack/language/en-GB/plg_mokosuitecross_slack.ini b/source/packages/plg_mokosuitecross_slack/language/en-GB/plg_mokosuitecross_slack.ini new file mode 100644 index 0000000..4466008 --- /dev/null +++ b/source/packages/plg_mokosuitecross_slack/language/en-GB/plg_mokosuitecross_slack.ini @@ -0,0 +1,5 @@ +PLG_MOKOSUITECROSS_SLACK="MokoSuiteCross - Slack" +PLG_MOKOSUITECROSS_SLACK_DESCRIPTION="Cross-post Joomla articles to Slack." +PLG_MOKOSUITECROSS_SLACK_FIELDSET_DEFAULTS="Default Settings" +PLG_MOKOSUITECROSS_SLACK_DEFAULT_WEBHOOK_URL="Default Webhook URL" +PLG_MOKOSUITECROSS_SLACK_DEFAULT_WEBHOOK_URL_DESC="The default MokoWaaS Slack webhook URL used when a service is set to 'default' mode." diff --git a/source/packages/plg_mokosuitecross_slack/language/en-GB/plg_mokosuitecross_slack.sys.ini b/source/packages/plg_mokosuitecross_slack/language/en-GB/plg_mokosuitecross_slack.sys.ini new file mode 100644 index 0000000..56130cc --- /dev/null +++ b/source/packages/plg_mokosuitecross_slack/language/en-GB/plg_mokosuitecross_slack.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_SLACK="MokoSuiteCross - Slack" +PLG_MOKOSUITECROSS_SLACK_DESCRIPTION="Cross-post Joomla articles to Slack." diff --git a/src/packages/plg_mokojoomcross_slack/src/Extension/index.html b/source/packages/plg_mokosuitecross_slack/language/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_slack/src/Extension/index.html rename to source/packages/plg_mokosuitecross_slack/language/index.html diff --git a/src/packages/plg_mokojoomcross_slack/src/index.html b/source/packages/plg_mokosuitecross_slack/services/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_slack/src/index.html rename to source/packages/plg_mokosuitecross_slack/services/index.html diff --git a/src/packages/plg_mokojoomcross_slack/services/provider.php b/source/packages/plg_mokosuitecross_slack/services/provider.php similarity index 81% rename from src/packages/plg_mokojoomcross_slack/services/provider.php rename to source/packages/plg_mokosuitecross_slack/services/provider.php index 28b9e2d..67b02b1 100644 --- a/src/packages/plg_mokojoomcross_slack/services/provider.php +++ b/source/packages/plg_mokosuitecross_slack/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -17,7 +17,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\MokoJoomCross${CLASS_NAME}\Extension${CLASS_NAME}Service; +use Joomla\Plugin\MokoSuiteCross\Slack\Extension\SlackService; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -27,7 +27,7 @@ return new class () implements ServiceProviderInterface { function (Container $container) { $plugin = new SlackService( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('mokojoomcross', 'slack') + (array) PluginHelper::getPlugin('mokosuitecross', 'slack') ); $plugin->setApplication(Factory::getApplication()); diff --git a/src/packages/plg_mokojoomcross_slack/slack.php b/source/packages/plg_mokosuitecross_slack/slack.php similarity index 90% rename from src/packages/plg_mokojoomcross_slack/slack.php rename to source/packages/plg_mokosuitecross_slack/slack.php index f74ab97..9b76408 100644 --- a/src/packages/plg_mokojoomcross_slack/slack.php +++ b/source/packages/plg_mokosuitecross_slack/slack.php @@ -1,7 +1,7 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE diff --git a/source/packages/plg_mokosuitecross_slack/slack.xml b/source/packages/plg_mokosuitecross_slack/slack.xml new file mode 100644 index 0000000..c95c412 --- /dev/null +++ b/source/packages/plg_mokosuitecross_slack/slack.xml @@ -0,0 +1,39 @@ + + + MokoSuiteCross - Slack + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_SLACK_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross${CLASS_NAME} + + + slack.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_slack.ini + language/en-GB/plg_mokosuitecross_slack.sys.ini + + + + +
+ +
+
+
+
diff --git a/src/packages/plg_mokojoomcross_slack/src/Extension/SlackService.php b/source/packages/plg_mokosuitecross_slack/src/Extension/SlackService.php similarity index 77% rename from src/packages/plg_mokojoomcross_slack/src/Extension/SlackService.php rename to source/packages/plg_mokosuitecross_slack/src/Extension/SlackService.php index 66519a5..57af26b 100644 --- a/src/packages/plg_mokojoomcross_slack/src/Extension/SlackService.php +++ b/source/packages/plg_mokosuitecross_slack/src/Extension/SlackService.php @@ -1,24 +1,24 @@ * @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\MokoJoomCross\Slack\Extension; +namespace Joomla\Plugin\MokoSuiteCross\Slack\Extension; defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\Component\MokoJoomCross\Administrator\Service\MokoJoomCrossServiceInterface; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Event\SubscriberInterface; /** - * Slack service plugin for MokoJoomCross. + * Slack service plugin for MokoSuiteCross. * * Supports two modes: * 1. Default MokoWaaS Webhook — pre-configured Slack webhook (hidden from admin UI) @@ -30,14 +30,14 @@ use Joomla\Event\SubscriberInterface; * "webhook_url": "https://hooks.slack.com/services/..." // Only for custom mode * } */ -class SlackService extends CMSPlugin implements SubscriberInterface, MokoJoomCrossServiceInterface +class SlackService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface { public static function getSubscribedEvents(): array { - return ['onMokoJoomCrossGetServices' => 'onMokoJoomCrossGetServices']; + return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices']; } - public function onMokoJoomCrossGetServices(&$services): void + public function onMokoSuiteCrossGetServices(&$services): void { $services[] = $this; } @@ -72,6 +72,16 @@ class SlackService extends CMSPlugin implements SubscriberInterface, MokoJoomCro ]); $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); @@ -111,7 +121,11 @@ class SlackService extends CMSPlugin implements SubscriberInterface, MokoJoomCro return $credentials['webhook_url'] ?? ''; } - return \Joomla\CMS\Component\ComponentHelper::getParams('com_mokojoomcross') - ->get('slack_default_webhook', ''); + return $this->params->get('default_webhook_url', ''); + } + + public function getSupportedMediaTypes(): array + { + return ['image']; } } diff --git a/src/packages/plg_mokojoomcross_telegram/index.html b/source/packages/plg_mokosuitecross_slack/src/Extension/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_telegram/index.html rename to source/packages/plg_mokosuitecross_slack/src/Extension/index.html diff --git a/src/packages/plg_mokojoomcross_telegram/language/en-GB/index.html b/source/packages/plg_mokosuitecross_slack/src/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_telegram/language/en-GB/index.html rename to source/packages/plg_mokosuitecross_slack/src/index.html diff --git a/source/packages/plg_mokosuitecross_teams/index.html b/source/packages/plg_mokosuitecross_teams/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_teams/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_teams/language/en-GB/index.html b/source/packages/plg_mokosuitecross_teams/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_teams/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_teams/language/en-GB/plg_mokosuitecross_teams.ini b/source/packages/plg_mokosuitecross_teams/language/en-GB/plg_mokosuitecross_teams.ini new file mode 100644 index 0000000..6921f33 --- /dev/null +++ b/source/packages/plg_mokosuitecross_teams/language/en-GB/plg_mokosuitecross_teams.ini @@ -0,0 +1,5 @@ +PLG_MOKOSUITECROSS_TEAMS="MokoSuiteCross - Microsoft Teams" +PLG_MOKOSUITECROSS_TEAMS_DESCRIPTION="Cross-post Joomla articles to Microsoft Teams." +PLG_MOKOSUITECROSS_TEAMS_FIELDSET_DEFAULTS="Default Settings" +PLG_MOKOSUITECROSS_TEAMS_DEFAULT_WEBHOOK="Default Webhook URL" +PLG_MOKOSUITECROSS_TEAMS_DEFAULT_WEBHOOK_DESC="Pre-configured MokoWaaS webhook URL. Services using default mode will use this URL." diff --git a/source/packages/plg_mokosuitecross_teams/language/en-GB/plg_mokosuitecross_teams.sys.ini b/source/packages/plg_mokosuitecross_teams/language/en-GB/plg_mokosuitecross_teams.sys.ini new file mode 100644 index 0000000..5235849 --- /dev/null +++ b/source/packages/plg_mokosuitecross_teams/language/en-GB/plg_mokosuitecross_teams.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_TEAMS="MokoSuiteCross - Microsoft Teams" +PLG_MOKOSUITECROSS_TEAMS_DESCRIPTION="Cross-post Joomla articles to Microsoft Teams." diff --git a/source/packages/plg_mokosuitecross_teams/language/index.html b/source/packages/plg_mokosuitecross_teams/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_teams/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_teams/services/index.html b/source/packages/plg_mokosuitecross_teams/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_teams/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_teams/services/provider.php b/source/packages/plg_mokosuitecross_teams/services/provider.php new file mode 100644 index 0000000..1bb9466 --- /dev/null +++ b/source/packages/plg_mokosuitecross_teams/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\Teams\Extension\TeamsService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new TeamsService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'teams') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_teams/src/Extension/TeamsService.php b/source/packages/plg_mokosuitecross_teams/src/Extension/TeamsService.php new file mode 100644 index 0000000..c817b28 --- /dev/null +++ b/source/packages/plg_mokosuitecross_teams/src/Extension/TeamsService.php @@ -0,0 +1,133 @@ + + * @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\Teams\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Microsoft Teams service plugin for MokoSuiteCross. + * + * API: https://outlook.office.com/webhook/... + */ +class TeamsService 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 'teams'; } + public function getServiceName(): string { return 'Microsoft Teams'; } + public function getMaxLength(): int { return 28000; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $url = $credentials['webhook_url'] ?? ''; + + if (empty($url)) { + $url = $this->params->get('default_webhook_url', ''); + } + + if (empty($url)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing webhook URL']]; + } + + // Adaptive Card format (Teams Workflows / Power Automate) + $card = [ + 'type' => 'message', + 'attachments' => [[ + 'contentType' => 'application/vnd.microsoft.card.adaptive', + 'contentUrl' => null, + 'content' => [ + '$schema' => 'http://adaptivecards.io/schemas/adaptive-card.json', + 'type' => 'AdaptiveCard', + 'version' => '1.4', + 'body' => [ + ['type' => 'TextBlock', 'text' => $message, 'wrap' => true], + ], + ], + ]], + ]; + + // Add image if provided + if (!empty($media[0])) { + $card['attachments'][0]['content']['body'][] = [ + 'type' => 'Image', + 'url' => $media[0], + 'size' => 'large', + ]; + } + + $postData = json_encode($card); + + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_HTTPHEADER => ['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) ?: ['raw' => $response]; + + 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 + { + $url = $credentials['webhook_url'] ?? ''; + + if (empty($url)) { + return ['valid' => false, 'message' => 'Missing webhook URL', 'account_name' => '']; + } + + if (!filter_var($url, FILTER_VALIDATE_URL)) { + return ['valid' => false, 'message' => 'Invalid webhook URL', 'account_name' => '']; + } + + return ['valid' => true, 'message' => 'Webhook URL configured', 'account_name' => 'Microsoft Teams']; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_teams/src/Extension/index.html b/source/packages/plg_mokosuitecross_teams/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_teams/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_teams/src/index.html b/source/packages/plg_mokosuitecross_teams/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_teams/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_teams/teams.php b/source/packages/plg_mokosuitecross_teams/teams.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_teams/teams.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_teams/teams.xml b/source/packages/plg_mokosuitecross_teams/teams.xml new file mode 100644 index 0000000..3938bb5 --- /dev/null +++ b/source/packages/plg_mokosuitecross_teams/teams.xml @@ -0,0 +1,39 @@ + + + MokoSuiteCross - Microsoft Teams + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_TEAMS_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Teams + + + teams.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_teams.ini + language/en-GB/plg_mokosuitecross_teams.sys.ini + + + +
+ +
+
+
+
\ No newline at end of file diff --git a/src/packages/plg_mokojoomcross_telegram/language/index.html b/source/packages/plg_mokosuitecross_telegram/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_telegram/language/index.html rename to source/packages/plg_mokosuitecross_telegram/index.html diff --git a/src/packages/plg_mokojoomcross_telegram/services/index.html b/source/packages/plg_mokosuitecross_telegram/language/en-GB/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_telegram/services/index.html rename to source/packages/plg_mokosuitecross_telegram/language/en-GB/index.html diff --git a/source/packages/plg_mokosuitecross_telegram/language/en-GB/plg_mokosuitecross_telegram.ini b/source/packages/plg_mokosuitecross_telegram/language/en-GB/plg_mokosuitecross_telegram.ini new file mode 100644 index 0000000..e478ada --- /dev/null +++ b/source/packages/plg_mokosuitecross_telegram/language/en-GB/plg_mokosuitecross_telegram.ini @@ -0,0 +1,9 @@ +PLG_MOKOSUITECROSS_TELEGRAM="MokoSuiteCross - Telegram" +PLG_MOKOSUITECROSS_TELEGRAM_DESCRIPTION="Cross-post Joomla articles to Telegram channels and groups. Supports default @MokoWaaSBot and custom bot modes." +PLG_MOKOSUITECROSS_TELEGRAM_FIELDSET_DEFAULTS="Default Bot Settings" +PLG_MOKOSUITECROSS_TELEGRAM_DEFAULT_BOT_TOKEN="Default Bot Token" +PLG_MOKOSUITECROSS_TELEGRAM_DEFAULT_BOT_TOKEN_DESC="Bot API token for the default MokoWaaS bot. Services using 'default' mode will use this token. Leave empty to require custom tokens on each service." +PLG_MOKOSUITECROSS_TELEGRAM_PARSE_MODE="Message Format" +PLG_MOKOSUITECROSS_TELEGRAM_PARSE_MODE_DESC="How Telegram parses formatting in messages." +PLG_MOKOSUITECROSS_TELEGRAM_DISABLE_PREVIEW="Disable Link Preview" +PLG_MOKOSUITECROSS_TELEGRAM_DISABLE_PREVIEW_DESC="Disable automatic link preview in Telegram messages." diff --git a/source/packages/plg_mokosuitecross_telegram/language/en-GB/plg_mokosuitecross_telegram.sys.ini b/source/packages/plg_mokosuitecross_telegram/language/en-GB/plg_mokosuitecross_telegram.sys.ini new file mode 100644 index 0000000..9a047c9 --- /dev/null +++ b/source/packages/plg_mokosuitecross_telegram/language/en-GB/plg_mokosuitecross_telegram.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_TELEGRAM="MokoSuiteCross - Telegram" +PLG_MOKOSUITECROSS_TELEGRAM_DESCRIPTION="Cross-post Joomla articles to Telegram." diff --git a/src/packages/plg_mokojoomcross_telegram/src/Extension/index.html b/source/packages/plg_mokosuitecross_telegram/language/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_telegram/src/Extension/index.html rename to source/packages/plg_mokosuitecross_telegram/language/index.html diff --git a/src/packages/plg_mokojoomcross_telegram/src/index.html b/source/packages/plg_mokosuitecross_telegram/services/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_telegram/src/index.html rename to source/packages/plg_mokosuitecross_telegram/services/index.html diff --git a/src/packages/plg_mokojoomcross_telegram/services/provider.php b/source/packages/plg_mokosuitecross_telegram/services/provider.php similarity index 81% rename from src/packages/plg_mokojoomcross_telegram/services/provider.php rename to source/packages/plg_mokosuitecross_telegram/services/provider.php index da7a416..3f5ddcb 100644 --- a/src/packages/plg_mokojoomcross_telegram/services/provider.php +++ b/source/packages/plg_mokosuitecross_telegram/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -17,7 +17,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\MokoJoomCross${CLASS_NAME}\Extension${CLASS_NAME}Service; +use Joomla\Plugin\MokoSuiteCross\Telegram\Extension\TelegramService; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -27,7 +27,7 @@ return new class () implements ServiceProviderInterface { function (Container $container) { $plugin = new TelegramService( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('mokojoomcross', 'telegram') + (array) PluginHelper::getPlugin('mokosuitecross', 'telegram') ); $plugin->setApplication(Factory::getApplication()); diff --git a/src/packages/plg_mokojoomcross_telegram/src/Extension/TelegramService.php b/source/packages/plg_mokosuitecross_telegram/src/Extension/TelegramService.php similarity index 80% rename from src/packages/plg_mokojoomcross_telegram/src/Extension/TelegramService.php rename to source/packages/plg_mokosuitecross_telegram/src/Extension/TelegramService.php index 1f44a36..1b9bc10 100644 --- a/src/packages/plg_mokojoomcross_telegram/src/Extension/TelegramService.php +++ b/source/packages/plg_mokosuitecross_telegram/src/Extension/TelegramService.php @@ -1,24 +1,24 @@ * @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\MokoJoomCross\Telegram\Extension; +namespace Joomla\Plugin\MokoSuiteCross\Telegram\Extension; defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\Component\MokoJoomCross\Administrator\Service\MokoJoomCrossServiceInterface; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Event\SubscriberInterface; /** - * Telegram service plugin for MokoJoomCross. + * Telegram service plugin for MokoSuiteCross. * * Supports two modes: * 1. Default MokoWaaS Bot — pre-configured bot token (hidden from admin UI) @@ -31,7 +31,7 @@ use Joomla\Event\SubscriberInterface; * "chat_id": "-100xxxxxxx" // Required — channel/group/user chat ID * } */ -class TelegramService extends CMSPlugin implements SubscriberInterface, MokoJoomCrossServiceInterface +class TelegramService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface { /** * Default MokoWaaS Bot token — resolved at runtime from component params. @@ -42,11 +42,11 @@ class TelegramService extends CMSPlugin implements SubscriberInterface, MokoJoom public static function getSubscribedEvents(): array { return [ - 'onMokoJoomCrossGetServices' => 'onMokoJoomCrossGetServices', + 'onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices', ]; } - public function onMokoJoomCrossGetServices(&$services): void + public function onMokoSuiteCrossGetServices(&$services): void { $services[] = $this; } @@ -97,6 +97,16 @@ class TelegramService extends CMSPlugin implements SubscriberInterface, MokoJoom ]); $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); @@ -134,6 +144,16 @@ class TelegramService extends CMSPlugin implements SubscriberInterface, MokoJoom ]); $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) ?: []; @@ -181,9 +201,12 @@ class TelegramService extends CMSPlugin implements SubscriberInterface, MokoJoom return $credentials['bot_token'] ?? ''; } - // Default mode — load from component encrypted params - $componentParams = \Joomla\CMS\Component\ComponentHelper::getParams('com_mokojoomcross'); + // Default mode — load from plugin params (set in Extensions → Plugins → MokoSuiteCross - Telegram) + return $this->params->get('default_bot_token', ''); + } - return $componentParams->get('telegram_default_bot_token', ''); + public function getSupportedMediaTypes(): array + { + return ['image', 'video', 'document']; } } diff --git a/src/packages/plg_mokojoomcross_twitter/index.html b/source/packages/plg_mokosuitecross_telegram/src/Extension/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_twitter/index.html rename to source/packages/plg_mokosuitecross_telegram/src/Extension/index.html diff --git a/src/packages/plg_mokojoomcross_twitter/language/en-GB/index.html b/source/packages/plg_mokosuitecross_telegram/src/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_twitter/language/en-GB/index.html rename to source/packages/plg_mokosuitecross_telegram/src/index.html diff --git a/src/packages/plg_mokojoomcross_telegram/telegram.php b/source/packages/plg_mokosuitecross_telegram/telegram.php similarity index 90% rename from src/packages/plg_mokojoomcross_telegram/telegram.php rename to source/packages/plg_mokosuitecross_telegram/telegram.php index f74ab97..9b76408 100644 --- a/src/packages/plg_mokojoomcross_telegram/telegram.php +++ b/source/packages/plg_mokosuitecross_telegram/telegram.php @@ -1,7 +1,7 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE diff --git a/src/packages/plg_mokojoomcross_telegram/telegram.xml b/source/packages/plg_mokosuitecross_telegram/telegram.xml similarity index 55% rename from src/packages/plg_mokojoomcross_telegram/telegram.xml rename to source/packages/plg_mokosuitecross_telegram/telegram.xml index d963704..4d41c85 100644 --- a/src/packages/plg_mokojoomcross_telegram/telegram.xml +++ b/source/packages/plg_mokosuitecross_telegram/telegram.xml @@ -1,16 +1,16 @@ - - MokoJoomCross - Telegram - 01.01.00 + + MokoSuiteCross - Telegram + 01.00.27-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech https://mokoconsulting.tech Copyright (C) 2026 Moko Consulting. All rights reserved. GPL-3.0-or-later - PLG_MOKOJOOMCROSS_TELEGRAM_DESCRIPTION + PLG_MOKOSUITECROSS_TELEGRAM_DESCRIPTION - Joomla\Plugin\MokoJoomCross${CLASS_NAME} + Joomla\Plugin\MokoSuiteCross${CLASS_NAME} telegram.php @@ -20,7 +20,7 @@ - language/en-GB/plg_mokojoomcross_telegram.ini - language/en-GB/plg_mokojoomcross_telegram.sys.ini + language/en-GB/plg_mokosuitecross_telegram.ini + language/en-GB/plg_mokosuitecross_telegram.sys.ini diff --git a/source/packages/plg_mokosuitecross_threads/index.html b/source/packages/plg_mokosuitecross_threads/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_threads/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_threads/language/en-GB/index.html b/source/packages/plg_mokosuitecross_threads/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_threads/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_threads/language/en-GB/plg_mokosuitecross_threads.ini b/source/packages/plg_mokosuitecross_threads/language/en-GB/plg_mokosuitecross_threads.ini new file mode 100644 index 0000000..5a4edde --- /dev/null +++ b/source/packages/plg_mokosuitecross_threads/language/en-GB/plg_mokosuitecross_threads.ini @@ -0,0 +1,5 @@ +PLG_MOKOSUITECROSS_THREADS="MokoSuiteCross - Threads (Meta)" +PLG_MOKOSUITECROSS_THREADS_DESCRIPTION="Cross-post Joomla articles to Threads (Meta)." +PLG_MOKOSUITECROSS_THREADS_FIELDSET_DEFAULTS="Default Settings" +PLG_MOKOSUITECROSS_THREADS_DEFAULT_WEBHOOK="Default Webhook URL" +PLG_MOKOSUITECROSS_THREADS_DEFAULT_WEBHOOK_DESC="Pre-configured MokoWaaS webhook URL. Services using default mode will use this URL." diff --git a/source/packages/plg_mokosuitecross_threads/language/en-GB/plg_mokosuitecross_threads.sys.ini b/source/packages/plg_mokosuitecross_threads/language/en-GB/plg_mokosuitecross_threads.sys.ini new file mode 100644 index 0000000..03fbabc --- /dev/null +++ b/source/packages/plg_mokosuitecross_threads/language/en-GB/plg_mokosuitecross_threads.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_THREADS="MokoSuiteCross - Threads (Meta)" +PLG_MOKOSUITECROSS_THREADS_DESCRIPTION="Cross-post Joomla articles to Threads (Meta)." diff --git a/source/packages/plg_mokosuitecross_threads/language/index.html b/source/packages/plg_mokosuitecross_threads/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_threads/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_threads/services/index.html b/source/packages/plg_mokosuitecross_threads/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_threads/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_threads/services/provider.php b/source/packages/plg_mokosuitecross_threads/services/provider.php new file mode 100644 index 0000000..40d6602 --- /dev/null +++ b/source/packages/plg_mokosuitecross_threads/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\Threads\Extension\ThreadsService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new ThreadsService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'threads') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_threads/src/Extension/ThreadsService.php b/source/packages/plg_mokosuitecross_threads/src/Extension/ThreadsService.php new file mode 100644 index 0000000..04b77d5 --- /dev/null +++ b/source/packages/plg_mokosuitecross_threads/src/Extension/ThreadsService.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\Threads\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Threads (Meta) service plugin for MokoSuiteCross. + * + * Uses the Threads Publishing API — a 2-step flow: + * 1. Create a media container via POST /{user_id}/threads + * 2. Publish the container via POST /{user_id}/threads_publish + */ +class ThreadsService 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 'threads'; } + public function getServiceName(): string { return 'Threads (Meta)'; } + public function getMaxLength(): int { return 500; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $token = $this->resolveCredential($credentials, 'access_token'); + $userId = $credentials['user_id'] ?? ''; + + if (empty($token) || empty($userId)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or user ID.']]; + } + + // Step 1: Create media container + $containerUrl = 'https://graph.threads.net/v1.0/' . urlencode($userId) . '/threads'; + $containerData = [ + 'text' => mb_substr($message, 0, 500), + 'access_token' => $token, + ]; + + // Attach image if provided + if (!empty($media[0])) { + $containerData['media_type'] = 'IMAGE'; + $containerData['image_url'] = $media[0]; + } else { + $containerData['media_type'] = 'TEXT'; + } + + $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.threads.net/v1.0/' . urlencode($userId) . '/threads_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'); + $userId = $credentials['user_id'] ?? ''; + + if (empty($token) || empty($userId)) { + return ['valid' => false, 'message' => 'Access token and user ID are required.', 'account_name' => '']; + } + + $ch = curl_init('https://graph.threads.net/v1.0/' . urlencode($userId) . '?fields=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['username'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => '@' . $data['username']]; + } + + 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_threads/src/Extension/index.html b/source/packages/plg_mokosuitecross_threads/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_threads/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_threads/src/index.html b/source/packages/plg_mokosuitecross_threads/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_threads/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_threads/threads.php b/source/packages/plg_mokosuitecross_threads/threads.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_threads/threads.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_threads/threads.xml b/source/packages/plg_mokosuitecross_threads/threads.xml new file mode 100644 index 0000000..dc950cb --- /dev/null +++ b/source/packages/plg_mokosuitecross_threads/threads.xml @@ -0,0 +1,39 @@ + + + MokoSuiteCross - Threads (Meta) + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_THREADS_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Threads + + + threads.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_threads.ini + language/en-GB/plg_mokosuitecross_threads.sys.ini + + + +
+ +
+
+
+
\ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_tiktok/index.html b/source/packages/plg_mokosuitecross_tiktok/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_tiktok/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_tiktok/language/en-GB/index.html b/source/packages/plg_mokosuitecross_tiktok/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_tiktok/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_tiktok/language/en-GB/plg_mokosuitecross_tiktok.ini b/source/packages/plg_mokosuitecross_tiktok/language/en-GB/plg_mokosuitecross_tiktok.ini new file mode 100644 index 0000000..168c4a3 --- /dev/null +++ b/source/packages/plg_mokosuitecross_tiktok/language/en-GB/plg_mokosuitecross_tiktok.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_TIKTOK="MokoSuiteCross - TikTok" +PLG_MOKOSUITECROSS_TIKTOK_DESCRIPTION="Cross-post Joomla articles to TikTok." diff --git a/source/packages/plg_mokosuitecross_tiktok/language/en-GB/plg_mokosuitecross_tiktok.sys.ini b/source/packages/plg_mokosuitecross_tiktok/language/en-GB/plg_mokosuitecross_tiktok.sys.ini new file mode 100644 index 0000000..168c4a3 --- /dev/null +++ b/source/packages/plg_mokosuitecross_tiktok/language/en-GB/plg_mokosuitecross_tiktok.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_TIKTOK="MokoSuiteCross - TikTok" +PLG_MOKOSUITECROSS_TIKTOK_DESCRIPTION="Cross-post Joomla articles to TikTok." diff --git a/source/packages/plg_mokosuitecross_tiktok/language/index.html b/source/packages/plg_mokosuitecross_tiktok/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_tiktok/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_tiktok/services/index.html b/source/packages/plg_mokosuitecross_tiktok/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_tiktok/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_tiktok/services/provider.php b/source/packages/plg_mokosuitecross_tiktok/services/provider.php new file mode 100644 index 0000000..c05c10f --- /dev/null +++ b/source/packages/plg_mokosuitecross_tiktok/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\Tiktok\Extension\TiktokService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new TiktokService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'tiktok') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_tiktok/src/Extension/index.html b/source/packages/plg_mokosuitecross_tiktok/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_tiktok/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_tiktok/src/index.html b/source/packages/plg_mokosuitecross_tiktok/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_tiktok/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_tiktok/tiktok.php b/source/packages/plg_mokosuitecross_tiktok/tiktok.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_tiktok/tiktok.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_tiktok/tiktok.xml b/source/packages/plg_mokosuitecross_tiktok/tiktok.xml new file mode 100644 index 0000000..5479523 --- /dev/null +++ b/source/packages/plg_mokosuitecross_tiktok/tiktok.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - TikTok + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_TIKTOK_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Tiktok + + + tiktok.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_tiktok.ini + language/en-GB/plg_mokosuitecross_tiktok.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_tumblr/index.html b/source/packages/plg_mokosuitecross_tumblr/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_tumblr/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_tumblr/language/en-GB/index.html b/source/packages/plg_mokosuitecross_tumblr/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_tumblr/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_tumblr/language/en-GB/plg_mokosuitecross_tumblr.ini b/source/packages/plg_mokosuitecross_tumblr/language/en-GB/plg_mokosuitecross_tumblr.ini new file mode 100644 index 0000000..e0be0ea --- /dev/null +++ b/source/packages/plg_mokosuitecross_tumblr/language/en-GB/plg_mokosuitecross_tumblr.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_TUMBLR="MokoSuiteCross - Tumblr" +PLG_MOKOSUITECROSS_TUMBLR_DESCRIPTION="Cross-post Joomla articles to Tumblr." diff --git a/source/packages/plg_mokosuitecross_tumblr/language/en-GB/plg_mokosuitecross_tumblr.sys.ini b/source/packages/plg_mokosuitecross_tumblr/language/en-GB/plg_mokosuitecross_tumblr.sys.ini new file mode 100644 index 0000000..e0be0ea --- /dev/null +++ b/source/packages/plg_mokosuitecross_tumblr/language/en-GB/plg_mokosuitecross_tumblr.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_TUMBLR="MokoSuiteCross - Tumblr" +PLG_MOKOSUITECROSS_TUMBLR_DESCRIPTION="Cross-post Joomla articles to Tumblr." diff --git a/source/packages/plg_mokosuitecross_tumblr/language/index.html b/source/packages/plg_mokosuitecross_tumblr/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_tumblr/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_tumblr/services/index.html b/source/packages/plg_mokosuitecross_tumblr/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_tumblr/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_tumblr/services/provider.php b/source/packages/plg_mokosuitecross_tumblr/services/provider.php new file mode 100644 index 0000000..2627931 --- /dev/null +++ b/source/packages/plg_mokosuitecross_tumblr/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\Tumblr\Extension\TumblrService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new TumblrService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'tumblr') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_tumblr/src/Extension/TumblrService.php b/source/packages/plg_mokosuitecross_tumblr/src/Extension/TumblrService.php new file mode 100644 index 0000000..c53c1cd --- /dev/null +++ b/source/packages/plg_mokosuitecross_tumblr/src/Extension/TumblrService.php @@ -0,0 +1,147 @@ + + * @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\Tumblr\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Tumblr service plugin for MokoSuiteCross. + * + * Uses the Tumblr API v2 with OAuth Bearer token to create link posts. + */ +class TumblrService 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 'tumblr'; } + public function getServiceName(): string { return 'Tumblr'; } + public function getMaxLength(): int { return 0; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $token = $credentials['access_token'] ?? ''; + $blogName = $credentials['blog_name'] ?? ''; + + if (empty($token) || empty($blogName)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or blog name.']]; + } + + $apiUrl = 'https://api.tumblr.com/v2/blog/' . urlencode($blogName) . '/post'; + + $postData = [ + 'title' => mb_substr(strip_tags($message), 0, 150), + 'body' => $message, + ]; + + if (!empty($media[0])) { + $postData['type'] = 'photo'; + $postData['source'] = $media[0]; + } else { + $postData['type'] = 'text'; + } + + $payload = json_encode($postData); + + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + 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) ?: []; + + $postId = $data['response']['id'] ?? ''; + + if ($httpCode === 201 && !empty($postId)) { + return ['success' => true, 'platform_post_id' => (string) $postId, 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $token = $credentials['access_token'] ?? ''; + $blogName = $credentials['blog_name'] ?? ''; + + if (empty($token) || empty($blogName)) { + return ['valid' => false, 'message' => 'Access token and blog name are required.', 'account_name' => '']; + } + + $ch = curl_init('https://api.tumblr.com/v2/blog/' . urlencode($blogName) . '/info'); + 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) ?: []; + $name = $data['response']['blog']['title'] ?? $data['response']['blog']['name'] ?? ''; + + if (!empty($name)) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $name]; + } + + return ['valid' => false, 'message' => $data['meta']['msg'] ?? 'Failed to verify credentials.', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image', 'video', 'gif']; + } +} diff --git a/source/packages/plg_mokosuitecross_tumblr/src/Extension/index.html b/source/packages/plg_mokosuitecross_tumblr/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_tumblr/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_tumblr/src/index.html b/source/packages/plg_mokosuitecross_tumblr/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_tumblr/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_tumblr/tumblr.php b/source/packages/plg_mokosuitecross_tumblr/tumblr.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_tumblr/tumblr.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_tumblr/tumblr.xml b/source/packages/plg_mokosuitecross_tumblr/tumblr.xml new file mode 100644 index 0000000..81226dc --- /dev/null +++ b/source/packages/plg_mokosuitecross_tumblr/tumblr.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - Tumblr + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_TUMBLR_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Tumblr + + + tumblr.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_tumblr.ini + language/en-GB/plg_mokosuitecross_tumblr.sys.ini + + \ No newline at end of file diff --git a/src/packages/plg_mokojoomcross_twitter/language/index.html b/source/packages/plg_mokosuitecross_twitter/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_twitter/language/index.html rename to source/packages/plg_mokosuitecross_twitter/index.html diff --git a/src/packages/plg_mokojoomcross_twitter/services/index.html b/source/packages/plg_mokosuitecross_twitter/language/en-GB/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_twitter/services/index.html rename to source/packages/plg_mokosuitecross_twitter/language/en-GB/index.html diff --git a/source/packages/plg_mokosuitecross_twitter/language/en-GB/plg_mokosuitecross_twitter.ini b/source/packages/plg_mokosuitecross_twitter/language/en-GB/plg_mokosuitecross_twitter.ini new file mode 100644 index 0000000..b5163e7 --- /dev/null +++ b/source/packages/plg_mokosuitecross_twitter/language/en-GB/plg_mokosuitecross_twitter.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_TWITTER="MokoSuiteCross - X / Twitter" +PLG_MOKOSUITECROSS_TWITTER_DESCRIPTION="Cross-post Joomla articles to X / Twitter." diff --git a/source/packages/plg_mokosuitecross_twitter/language/en-GB/plg_mokosuitecross_twitter.sys.ini b/source/packages/plg_mokosuitecross_twitter/language/en-GB/plg_mokosuitecross_twitter.sys.ini new file mode 100644 index 0000000..b5163e7 --- /dev/null +++ b/source/packages/plg_mokosuitecross_twitter/language/en-GB/plg_mokosuitecross_twitter.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_TWITTER="MokoSuiteCross - X / Twitter" +PLG_MOKOSUITECROSS_TWITTER_DESCRIPTION="Cross-post Joomla articles to X / Twitter." diff --git a/src/packages/plg_mokojoomcross_twitter/src/Extension/index.html b/source/packages/plg_mokosuitecross_twitter/language/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_twitter/src/Extension/index.html rename to source/packages/plg_mokosuitecross_twitter/language/index.html diff --git a/src/packages/plg_mokojoomcross_twitter/src/index.html b/source/packages/plg_mokosuitecross_twitter/services/index.html similarity index 100% rename from src/packages/plg_mokojoomcross_twitter/src/index.html rename to source/packages/plg_mokosuitecross_twitter/services/index.html diff --git a/src/packages/plg_mokojoomcross_twitter/services/provider.php b/source/packages/plg_mokosuitecross_twitter/services/provider.php similarity index 81% rename from src/packages/plg_mokojoomcross_twitter/services/provider.php rename to source/packages/plg_mokosuitecross_twitter/services/provider.php index 9e21213..0d893cc 100644 --- a/src/packages/plg_mokojoomcross_twitter/services/provider.php +++ b/source/packages/plg_mokosuitecross_twitter/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -17,7 +17,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\MokoJoomCross${CLASS_NAME}\Extension${CLASS_NAME}Service; +use Joomla\Plugin\MokoSuiteCross\Twitter\Extension\TwitterService; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -27,7 +27,7 @@ return new class () implements ServiceProviderInterface { function (Container $container) { $plugin = new TwitterService( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('mokojoomcross', 'twitter') + (array) PluginHelper::getPlugin('mokosuitecross', 'twitter') ); $plugin->setApplication(Factory::getApplication()); diff --git a/source/packages/plg_mokosuitecross_twitter/src/Extension/TwitterService.php b/source/packages/plg_mokosuitecross_twitter/src/Extension/TwitterService.php new file mode 100644 index 0000000..ef6b4e0 --- /dev/null +++ b/source/packages/plg_mokosuitecross_twitter/src/Extension/TwitterService.php @@ -0,0 +1,210 @@ + + * @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\Twitter\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * X/Twitter service plugin for MokoSuiteCross. + * + * Uses Twitter API v2 with OAuth 1.0a (HMAC-SHA1) for posting. + * Bearer tokens are app-only and cannot create tweets — OAuth 1.0a + * with consumer key/secret + access token/secret is required. + */ +class TwitterService 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 'twitter'; + } + + public function getServiceName(): string + { + return 'X / Twitter'; + } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $apiUrl = 'https://api.twitter.com/2/tweets'; + $postData = json_encode(['text' => mb_substr($message, 0, 280)]); + + $consumerKey = $credentials['api_key'] ?? ''; + $consumerSecret = $credentials['api_secret'] ?? ''; + $accessToken = $credentials['access_token'] ?? ''; + $tokenSecret = $credentials['access_token_secret'] ?? ''; + + if (!$consumerKey || !$consumerSecret || !$accessToken || !$tokenSecret) { + return [ + 'success' => false, + 'platform_post_id' => '', + 'response' => ['error' => 'Missing OAuth 1.0a credentials. All 4 keys are required.'], + ]; + } + + $authHeader = $this->buildOAuth1Header('POST', $apiUrl, $consumerKey, $consumerSecret, $accessToken, $tokenSecret); + + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'Authorization: ' . $authHeader, + ], + 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 === 201 && !empty($data['data']['id'])) { + return ['success' => true, 'platform_post_id' => $data['data']['id'], 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $apiUrl = 'https://api.twitter.com/2/users/me'; + + $consumerKey = $credentials['api_key'] ?? ''; + $consumerSecret = $credentials['api_secret'] ?? ''; + $accessToken = $credentials['access_token'] ?? ''; + $tokenSecret = $credentials['access_token_secret'] ?? ''; + + if (!$consumerKey || !$consumerSecret || !$accessToken || !$tokenSecret) { + return ['valid' => false, 'message' => 'All 4 OAuth keys are required.', 'account_name' => '']; + } + + $authHeader = $this->buildOAuth1Header('GET', $apiUrl, $consumerKey, $consumerSecret, $accessToken, $tokenSecret); + + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_HTTPHEADER => ['Authorization: ' . $authHeader], + 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['data']['username'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => '@' . $data['data']['username']]; + } + + return ['valid' => false, 'message' => $data['detail'] ?? 'Failed', 'account_name' => '']; + } + + public function getMaxLength(): int + { + return 280; + } + + public function supportsMedia(): bool + { + return true; + } + + /** + * Build an OAuth 1.0a Authorization header with HMAC-SHA1 signature. + */ + private function buildOAuth1Header( + string $method, + string $url, + string $consumerKey, + string $consumerSecret, + string $accessToken, + string $tokenSecret + ): string { + $oauthParams = [ + 'oauth_consumer_key' => $consumerKey, + 'oauth_nonce' => bin2hex(random_bytes(16)), + 'oauth_signature_method' => 'HMAC-SHA1', + 'oauth_timestamp' => (string) time(), + 'oauth_token' => $accessToken, + 'oauth_version' => '1.0', + ]; + + // Build signature base string: METHOD&URL¶ms (all percent-encoded) + ksort($oauthParams); + + $paramString = http_build_query($oauthParams, '', '&', PHP_QUERY_RFC3986); + + $baseString = strtoupper($method) . '&' + . rawurlencode($url) . '&' + . rawurlencode($paramString); + + // Signing key: consumer_secret&token_secret (both percent-encoded) + $signingKey = rawurlencode($consumerSecret) . '&' . rawurlencode($tokenSecret); + + $oauthParams['oauth_signature'] = base64_encode( + hash_hmac('sha1', $baseString, $signingKey, true) + ); + + // Build Authorization header + $parts = []; + + foreach ($oauthParams as $key => $value) { + $parts[] = rawurlencode($key) . '="' . rawurlencode($value) . '"'; + } + + return 'OAuth ' . implode(', ', $parts); + } + + public function getSupportedMediaTypes(): array + { + return ['image', 'video', 'gif']; + } +} diff --git a/src/packages/plg_system_mokojoomcross/index.html b/source/packages/plg_mokosuitecross_twitter/src/Extension/index.html similarity index 100% rename from src/packages/plg_system_mokojoomcross/index.html rename to source/packages/plg_mokosuitecross_twitter/src/Extension/index.html diff --git a/src/packages/plg_system_mokojoomcross/language/en-GB/index.html b/source/packages/plg_mokosuitecross_twitter/src/index.html similarity index 100% rename from src/packages/plg_system_mokojoomcross/language/en-GB/index.html rename to source/packages/plg_mokosuitecross_twitter/src/index.html diff --git a/src/packages/plg_mokojoomcross_twitter/twitter.php b/source/packages/plg_mokosuitecross_twitter/twitter.php similarity index 90% rename from src/packages/plg_mokojoomcross_twitter/twitter.php rename to source/packages/plg_mokosuitecross_twitter/twitter.php index f74ab97..9b76408 100644 --- a/src/packages/plg_mokojoomcross_twitter/twitter.php +++ b/source/packages/plg_mokosuitecross_twitter/twitter.php @@ -1,7 +1,7 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE diff --git a/src/packages/plg_mokojoomcross_twitter/twitter.xml b/source/packages/plg_mokosuitecross_twitter/twitter.xml similarity index 55% rename from src/packages/plg_mokojoomcross_twitter/twitter.xml rename to source/packages/plg_mokosuitecross_twitter/twitter.xml index 9381469..9730db1 100644 --- a/src/packages/plg_mokojoomcross_twitter/twitter.xml +++ b/source/packages/plg_mokosuitecross_twitter/twitter.xml @@ -1,16 +1,16 @@ - - MokoJoomCross - X / Twitter - 01.01.00 + + MokoSuiteCross - X / Twitter + 01.00.27-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech https://mokoconsulting.tech Copyright (C) 2026 Moko Consulting. All rights reserved. GPL-3.0-or-later - PLG_MOKOJOOMCROSS_TWITTER_DESCRIPTION + PLG_MOKOSUITECROSS_TWITTER_DESCRIPTION - Joomla\Plugin\MokoJoomCross${CLASS_NAME} + Joomla\Plugin\MokoSuiteCross${CLASS_NAME} twitter.php @@ -20,7 +20,7 @@ - language/en-GB/plg_mokojoomcross_twitter.ini - language/en-GB/plg_mokojoomcross_twitter.sys.ini + language/en-GB/plg_mokosuitecross_twitter.ini + language/en-GB/plg_mokosuitecross_twitter.sys.ini diff --git a/source/packages/plg_mokosuitecross_webhook/index.html b/source/packages/plg_mokosuitecross_webhook/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_webhook/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_webhook/language/en-GB/index.html b/source/packages/plg_mokosuitecross_webhook/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_webhook/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_webhook/language/en-GB/plg_mokosuitecross_webhook.ini b/source/packages/plg_mokosuitecross_webhook/language/en-GB/plg_mokosuitecross_webhook.ini new file mode 100644 index 0000000..be10c17 --- /dev/null +++ b/source/packages/plg_mokosuitecross_webhook/language/en-GB/plg_mokosuitecross_webhook.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_WEBHOOK="MokoSuiteCross - Generic Webhook" +PLG_MOKOSUITECROSS_WEBHOOK_DESCRIPTION="Cross-post Joomla articles to Generic Webhook." diff --git a/source/packages/plg_mokosuitecross_webhook/language/en-GB/plg_mokosuitecross_webhook.sys.ini b/source/packages/plg_mokosuitecross_webhook/language/en-GB/plg_mokosuitecross_webhook.sys.ini new file mode 100644 index 0000000..be10c17 --- /dev/null +++ b/source/packages/plg_mokosuitecross_webhook/language/en-GB/plg_mokosuitecross_webhook.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_WEBHOOK="MokoSuiteCross - Generic Webhook" +PLG_MOKOSUITECROSS_WEBHOOK_DESCRIPTION="Cross-post Joomla articles to Generic Webhook." diff --git a/source/packages/plg_mokosuitecross_webhook/language/index.html b/source/packages/plg_mokosuitecross_webhook/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_webhook/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_webhook/services/index.html b/source/packages/plg_mokosuitecross_webhook/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_webhook/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_webhook/services/provider.php b/source/packages/plg_mokosuitecross_webhook/services/provider.php new file mode 100644 index 0000000..615a8ae --- /dev/null +++ b/source/packages/plg_mokosuitecross_webhook/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\Webhook\Extension\WebhookService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new WebhookService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'webhook') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_webhook/src/Extension/WebhookService.php b/source/packages/plg_mokosuitecross_webhook/src/Extension/WebhookService.php new file mode 100644 index 0000000..bba986d --- /dev/null +++ b/source/packages/plg_mokosuitecross_webhook/src/Extension/WebhookService.php @@ -0,0 +1,133 @@ + + * @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\Webhook\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Generic Webhook service plugin for MokoSuiteCross. + * + * API: configured webhook URL + */ +class WebhookService 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 'webhook'; } + public function getServiceName(): string { return 'Generic Webhook'; } + public function getMaxLength(): int { return 0; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + // Credential keys match the service.xml form field names (after stripping cred_webhook_ prefix): + // url, method, auth_type, bearer_token, basic_username, basic_password, content_type + $url = $credentials['url'] ?? ''; + + if (empty($url)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing webhook URL']]; + } + + $method = $credentials['method'] ?? 'POST'; + $format = $credentials['content_type'] ?? 'json'; + + $payload = [ + 'title' => $params['title'] ?? '', + 'url' => $params['_article_url'] ?? $params['url'] ?? '', + 'message' => $message, + 'image' => $params['image'] ?? '', + 'category' => $params['category'] ?? '', + 'author' => $params['author'] ?? '', + 'timestamp' => date('c'), + ]; + + $httpHeaders = ['Content-Type: application/json']; + + $body = ($format === 'form') ? http_build_query($payload) : json_encode($payload); + + if ($format === 'form') { + $httpHeaders[0] = 'Content-Type: application/x-www-form-urlencoded'; + } + + // Apply authentication based on auth_type + $authType = $credentials['auth_type'] ?? 'none'; + + if ($authType === 'bearer' && !empty($credentials['bearer_token'])) { + $httpHeaders[] = 'Authorization: Bearer ' . $credentials['bearer_token']; + } elseif ($authType === 'basic' && !empty($credentials['basic_username'])) { + $httpHeaders[] = 'Authorization: Basic ' . base64_encode( + $credentials['basic_username'] . ':' . ($credentials['basic_password'] ?? '') + ); + } + + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_POSTFIELDS => $body, + CURLOPT_HTTPHEADER => $httpHeaders, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP, + ]); + + $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) ?: ['raw' => $response]; + + 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 + { + $url = $credentials['url'] ?? ''; + + if (empty($url)) { + return ['valid' => false, 'message' => 'Missing webhook URL', 'account_name' => '']; + } + + return ['valid' => true, 'message' => 'Credentials configured', 'account_name' => 'Generic Webhook']; + } + + public function getSupportedMediaTypes(): array + { + return ['image', 'video', 'document']; + } +} diff --git a/source/packages/plg_mokosuitecross_webhook/src/Extension/index.html b/source/packages/plg_mokosuitecross_webhook/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_webhook/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_webhook/src/index.html b/source/packages/plg_mokosuitecross_webhook/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_webhook/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_webhook/webhook.php b/source/packages/plg_mokosuitecross_webhook/webhook.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_webhook/webhook.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_webhook/webhook.xml b/source/packages/plg_mokosuitecross_webhook/webhook.xml new file mode 100644 index 0000000..985c700 --- /dev/null +++ b/source/packages/plg_mokosuitecross_webhook/webhook.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - Generic Webhook + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_WEBHOOK_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Webhook + + + webhook.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_webhook.ini + language/en-GB/plg_mokosuitecross_webhook.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_whatsapp/index.html b/source/packages/plg_mokosuitecross_whatsapp/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_whatsapp/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_whatsapp/language/en-GB/index.html b/source/packages/plg_mokosuitecross_whatsapp/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_whatsapp/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_whatsapp/language/en-GB/plg_mokosuitecross_whatsapp.ini b/source/packages/plg_mokosuitecross_whatsapp/language/en-GB/plg_mokosuitecross_whatsapp.ini new file mode 100644 index 0000000..cc473a6 --- /dev/null +++ b/source/packages/plg_mokosuitecross_whatsapp/language/en-GB/plg_mokosuitecross_whatsapp.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_WHATSAPP="MokoSuiteCross - WhatsApp Business" +PLG_MOKOSUITECROSS_WHATSAPP_DESCRIPTION="Cross-post Joomla articles to WhatsApp Business." diff --git a/source/packages/plg_mokosuitecross_whatsapp/language/en-GB/plg_mokosuitecross_whatsapp.sys.ini b/source/packages/plg_mokosuitecross_whatsapp/language/en-GB/plg_mokosuitecross_whatsapp.sys.ini new file mode 100644 index 0000000..cc473a6 --- /dev/null +++ b/source/packages/plg_mokosuitecross_whatsapp/language/en-GB/plg_mokosuitecross_whatsapp.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_WHATSAPP="MokoSuiteCross - WhatsApp Business" +PLG_MOKOSUITECROSS_WHATSAPP_DESCRIPTION="Cross-post Joomla articles to WhatsApp Business." diff --git a/source/packages/plg_mokosuitecross_whatsapp/language/index.html b/source/packages/plg_mokosuitecross_whatsapp/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_whatsapp/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_whatsapp/services/index.html b/source/packages/plg_mokosuitecross_whatsapp/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_whatsapp/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_whatsapp/services/provider.php b/source/packages/plg_mokosuitecross_whatsapp/services/provider.php new file mode 100644 index 0000000..4bbf4f1 --- /dev/null +++ b/source/packages/plg_mokosuitecross_whatsapp/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\Whatsapp\Extension\WhatsappService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new WhatsappService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'whatsapp') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_whatsapp/src/Extension/WhatsappService.php b/source/packages/plg_mokosuitecross_whatsapp/src/Extension/WhatsappService.php new file mode 100644 index 0000000..39e71f4 --- /dev/null +++ b/source/packages/plg_mokosuitecross_whatsapp/src/Extension/WhatsappService.php @@ -0,0 +1,146 @@ + + * @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\Whatsapp\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * WhatsApp Business service plugin for MokoSuiteCross. + * + * Uses the Meta Cloud API (graph.facebook.com) to send messages + * via the WhatsApp Business Platform. + */ +class WhatsappService 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 'whatsapp'; } + public function getServiceName(): string { return 'WhatsApp Business'; } + public function getMaxLength(): int { return 4096; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $token = $credentials['access_token'] ?? ''; + $phoneId = $credentials['phone_number_id'] ?? ''; + $recipient = $credentials['recipient'] ?? ''; + + if (empty($token) || empty($phoneId) || empty($recipient)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token, phone number ID, or recipient.']]; + } + + $apiUrl = 'https://graph.facebook.com/v19.0/' . urlencode($phoneId) . '/messages'; + $payload = json_encode([ + 'messaging_product' => 'whatsapp', + 'to' => $recipient, + 'type' => 'text', + 'text' => ['body' => mb_substr($message, 0, 4096)], + ]); + + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + 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) ?: []; + + $messageId = $data['messages'][0]['id'] ?? ''; + + if ($httpCode >= 200 && $httpCode < 300 && !empty($messageId)) { + return ['success' => true, 'platform_post_id' => $messageId, 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $token = $credentials['access_token'] ?? ''; + $phoneId = $credentials['phone_number_id'] ?? ''; + $recipient = $credentials['recipient'] ?? ''; + + if (empty($token) || empty($phoneId)) { + return ['valid' => false, 'message' => 'Access token and phone number ID are required.', 'account_name' => '']; + } + + if (empty($recipient)) { + return ['valid' => false, 'message' => 'Recipient phone number is required.', 'account_name' => '']; + } + + // Verify the phone number ID exists + $ch = curl_init('https://graph.facebook.com/v19.0/' . urlencode($phoneId) . '?fields=display_phone_number,verified_name'); + 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['verified_name'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['verified_name'] . ' (' . ($data['display_phone_number'] ?? '') . ')']; + } + + return ['valid' => false, 'message' => $data['error']['message'] ?? 'Failed to verify credentials.', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image', 'video', 'document']; + } +} diff --git a/source/packages/plg_mokosuitecross_whatsapp/src/Extension/index.html b/source/packages/plg_mokosuitecross_whatsapp/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_whatsapp/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_whatsapp/src/index.html b/source/packages/plg_mokosuitecross_whatsapp/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_whatsapp/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_whatsapp/whatsapp.php b/source/packages/plg_mokosuitecross_whatsapp/whatsapp.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_whatsapp/whatsapp.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_whatsapp/whatsapp.xml b/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml new file mode 100644 index 0000000..4e7536c --- /dev/null +++ b/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - WhatsApp Business + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_WHATSAPP_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Whatsapp + + + whatsapp.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_whatsapp.ini + language/en-GB/plg_mokosuitecross_whatsapp.sys.ini + + \ No newline at end of file diff --git a/source/packages/plg_mokosuitecross_wordpress/index.html b/source/packages/plg_mokosuitecross_wordpress/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_wordpress/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_wordpress/language/en-GB/index.html b/source/packages/plg_mokosuitecross_wordpress/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_wordpress/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_wordpress/language/en-GB/plg_mokosuitecross_wordpress.ini b/source/packages/plg_mokosuitecross_wordpress/language/en-GB/plg_mokosuitecross_wordpress.ini new file mode 100644 index 0000000..ecca709 --- /dev/null +++ b/source/packages/plg_mokosuitecross_wordpress/language/en-GB/plg_mokosuitecross_wordpress.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_WORDPRESS="MokoSuiteCross - WordPress" +PLG_MOKOSUITECROSS_WORDPRESS_DESCRIPTION="Cross-post Joomla articles to WordPress." diff --git a/source/packages/plg_mokosuitecross_wordpress/language/en-GB/plg_mokosuitecross_wordpress.sys.ini b/source/packages/plg_mokosuitecross_wordpress/language/en-GB/plg_mokosuitecross_wordpress.sys.ini new file mode 100644 index 0000000..ecca709 --- /dev/null +++ b/source/packages/plg_mokosuitecross_wordpress/language/en-GB/plg_mokosuitecross_wordpress.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_WORDPRESS="MokoSuiteCross - WordPress" +PLG_MOKOSUITECROSS_WORDPRESS_DESCRIPTION="Cross-post Joomla articles to WordPress." diff --git a/source/packages/plg_mokosuitecross_wordpress/language/index.html b/source/packages/plg_mokosuitecross_wordpress/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_wordpress/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_wordpress/services/index.html b/source/packages/plg_mokosuitecross_wordpress/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_wordpress/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_wordpress/services/provider.php b/source/packages/plg_mokosuitecross_wordpress/services/provider.php new file mode 100644 index 0000000..e2b5322 --- /dev/null +++ b/source/packages/plg_mokosuitecross_wordpress/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\Wordpress\Extension\WordpressService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new WordpressService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'wordpress') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_wordpress/src/Extension/WordpressService.php b/source/packages/plg_mokosuitecross_wordpress/src/Extension/WordpressService.php new file mode 100644 index 0000000..98f7e9a --- /dev/null +++ b/source/packages/plg_mokosuitecross_wordpress/src/Extension/WordpressService.php @@ -0,0 +1,158 @@ + + * @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\Wordpress\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * WordPress service plugin for MokoSuiteCross. + * + * Uses the WordPress REST API v2 with Application Passwords (Basic Auth). + */ +class WordpressService 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 'wordpress'; } + public function getServiceName(): string { return 'WordPress'; } + public function getMaxLength(): int { return 0; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $siteUrl = rtrim($credentials['site_url'] ?? '', '/'); + $username = $credentials['username'] ?? ''; + $appPassword = $credentials['app_password'] ?? ''; + $status = $credentials['default_status'] ?? 'draft'; + + if (empty($siteUrl) || empty($username) || empty($appPassword)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing site URL, username, or application password.']]; + } + + $apiUrl = $siteUrl . '/wp-json/wp/v2/posts'; + $title = mb_substr(strip_tags($message), 0, 200); + + // Append source link if the original article URL is available + $articleUrl = $params['_article_url'] ?? ''; + $content = ''; + + // Prepend featured image if available + if (!empty($media[0])) { + $content .= '
' . "\n\n"; + } + + $content .= $message; + + if (!empty($articleUrl)) { + $content .= "\n\n

Originally published at ' + . htmlspecialchars($articleUrl, ENT_QUOTES, 'UTF-8') + . '

'; + } + + $payload = json_encode([ + 'title' => $title, + 'content' => $content, + 'status' => $status, + ]); + + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HTTPHEADER => [ + 'Authorization: Basic ' . base64_encode($username . ':' . $appPassword), + '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 === 201 && !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 + { + $siteUrl = rtrim($credentials['site_url'] ?? '', '/'); + $username = $credentials['username'] ?? ''; + $appPassword = $credentials['app_password'] ?? ''; + + if (empty($siteUrl) || empty($username) || empty($appPassword)) { + return ['valid' => false, 'message' => 'Site URL, username, and application password are required.', 'account_name' => '']; + } + + $ch = curl_init($siteUrl . '/wp-json/wp/v2/users/me'); + curl_setopt_array($ch, [ + CURLOPT_HTTPHEADER => ['Authorization: Basic ' . base64_encode($username . ':' . $appPassword)], + 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['name'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['name']]; + } + + return ['valid' => false, 'message' => $data['message'] ?? 'Failed to verify credentials.', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_wordpress/src/Extension/index.html b/source/packages/plg_mokosuitecross_wordpress/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_wordpress/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_wordpress/src/index.html b/source/packages/plg_mokosuitecross_wordpress/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_wordpress/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_wordpress/wordpress.php b/source/packages/plg_mokosuitecross_wordpress/wordpress.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_wordpress/wordpress.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_wordpress/wordpress.xml b/source/packages/plg_mokosuitecross_wordpress/wordpress.xml new file mode 100644 index 0000000..977dab3 --- /dev/null +++ b/source/packages/plg_mokosuitecross_wordpress/wordpress.xml @@ -0,0 +1,26 @@ + + + MokoSuiteCross - WordPress + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_WORDPRESS_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Wordpress + + + wordpress.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_wordpress.ini + language/en-GB/plg_mokosuitecross_wordpress.sys.ini + + \ No newline at end of file diff --git a/src/packages/plg_system_mokojoomcross/language/index.html b/source/packages/plg_system_mokosuitecross/index.html similarity index 100% rename from src/packages/plg_system_mokojoomcross/language/index.html rename to source/packages/plg_system_mokosuitecross/index.html diff --git a/src/packages/plg_system_mokojoomcross/services/index.html b/source/packages/plg_system_mokosuitecross/language/en-GB/index.html similarity index 100% rename from src/packages/plg_system_mokojoomcross/services/index.html rename to source/packages/plg_system_mokosuitecross/language/en-GB/index.html diff --git a/source/packages/plg_system_mokosuitecross/language/en-GB/plg_system_mokosuitecross.ini b/source/packages/plg_system_mokosuitecross/language/en-GB/plg_system_mokosuitecross.ini new file mode 100644 index 0000000..7ed66b0 --- /dev/null +++ b/source/packages/plg_system_mokosuitecross/language/en-GB/plg_system_mokosuitecross.ini @@ -0,0 +1,6 @@ +; System - MokoSuiteCross Language File +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_SYSTEM_MOKOSUITECROSS="System - MokoSuiteCross" +PLG_SYSTEM_MOKOSUITECROSS_DESCRIPTION="Triggers cross-posting to connected services when Joomla articles are published." diff --git a/source/packages/plg_system_mokosuitecross/language/en-GB/plg_system_mokosuitecross.sys.ini b/source/packages/plg_system_mokosuitecross/language/en-GB/plg_system_mokosuitecross.sys.ini new file mode 100644 index 0000000..429418a --- /dev/null +++ b/source/packages/plg_system_mokosuitecross/language/en-GB/plg_system_mokosuitecross.sys.ini @@ -0,0 +1,6 @@ +; System - MokoSuiteCross System Language File +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_SYSTEM_MOKOSUITECROSS="System - MokoSuiteCross" +PLG_SYSTEM_MOKOSUITECROSS_DESCRIPTION="Triggers cross-posting to connected services when Joomla articles are published." diff --git a/src/packages/plg_system_mokojoomcross/src/Extension/index.html b/source/packages/plg_system_mokosuitecross/language/index.html similarity index 100% rename from src/packages/plg_system_mokojoomcross/src/Extension/index.html rename to source/packages/plg_system_mokosuitecross/language/index.html diff --git a/source/packages/plg_system_mokosuitecross/mokosuitecross.php b/source/packages/plg_system_mokosuitecross/mokosuitecross.php new file mode 100644 index 0000000..d83553d --- /dev/null +++ b/source/packages/plg_system_mokosuitecross/mokosuitecross.php @@ -0,0 +1,12 @@ + + * @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/src/packages/plg_system_mokojoomcross/mokojoomcross.xml b/source/packages/plg_system_mokosuitecross/mokosuitecross.xml similarity index 55% rename from src/packages/plg_system_mokojoomcross/mokojoomcross.xml rename to source/packages/plg_system_mokosuitecross/mokosuitecross.xml index a35bb91..e7311a1 100644 --- a/src/packages/plg_system_mokojoomcross/mokojoomcross.xml +++ b/source/packages/plg_system_mokosuitecross/mokosuitecross.xml @@ -1,26 +1,26 @@ - System - MokoJoomCross - 01.01.00 + System - MokoSuiteCross + 01.00.27-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech https://mokoconsulting.tech Copyright (C) 2026 Moko Consulting. All rights reserved. GPL-3.0-or-later - PLG_SYSTEM_MOKOJOOMCROSS_DESCRIPTION + PLG_SYSTEM_MOKOSUITECROSS_DESCRIPTION - Joomla\Plugin\System\MokoJoomCross + Joomla\Plugin\System\MokoSuiteCross - mokojoomcross.php + mokosuitecross.php src services language - language/en-GB/plg_system_mokojoomcross.ini - language/en-GB/plg_system_mokojoomcross.sys.ini + language/en-GB/plg_system_mokosuitecross.ini + language/en-GB/plg_system_mokosuitecross.sys.ini diff --git a/src/packages/plg_system_mokojoomcross/src/index.html b/source/packages/plg_system_mokosuitecross/services/index.html similarity index 100% rename from src/packages/plg_system_mokojoomcross/src/index.html rename to source/packages/plg_system_mokosuitecross/services/index.html diff --git a/src/packages/plg_system_mokojoomcross/services/provider.php b/source/packages/plg_system_mokosuitecross/services/provider.php similarity index 83% rename from src/packages/plg_system_mokojoomcross/services/provider.php rename to source/packages/plg_system_mokosuitecross/services/provider.php index b7946da..30861a8 100644 --- a/src/packages/plg_system_mokojoomcross/services/provider.php +++ b/source/packages/plg_system_mokosuitecross/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -17,7 +17,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\System\MokoJoomCross\Extension\MokoJoomCross; +use Joomla\Plugin\System\MokoSuiteCross\Extension\MokoSuiteCross; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -25,9 +25,9 @@ return new class () implements ServiceProviderInterface { $container->set( PluginInterface::class, function (Container $container) { - $plugin = new MokoJoomCross( + $plugin = new MokoSuiteCross( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('system', 'mokojoomcross') + (array) PluginHelper::getPlugin('system', 'mokosuitecross') ); $plugin->setApplication(Factory::getApplication()); diff --git a/source/packages/plg_system_mokosuitecross/src/Extension/MokoSuiteCross.php b/source/packages/plg_system_mokosuitecross/src/Extension/MokoSuiteCross.php new file mode 100644 index 0000000..44626b5 --- /dev/null +++ b/source/packages/plg_system_mokosuitecross/src/Extension/MokoSuiteCross.php @@ -0,0 +1,189 @@ + + * @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\System\MokoSuiteCross\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Event\SubscriberInterface; + +/** + * System plugin that handles page-load queue processing for MokoSuiteCross. + * + * When queue processing mode is set to "pageload" or "both", this plugin + * processes a small batch of queued cross-posts on each page render, + * throttled to a configurable interval (default 5 minutes). + * + * Content-type event handlers (articles, calendar events, gallery items) + * are handled by their respective plugins, which delegate to the + * CrossPostDispatcher helper for dispatch logic. + */ +class MokoSuiteCross extends CMSPlugin implements SubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + 'onAfterRoute' => 'onAfterRoute', + 'onAfterRender' => 'onAfterRender', + ]; + } + + public function onAfterRoute(): void + { + $app = $this->getApplication(); + + if ($app->isClient('administrator')) { + $this->warnMissingLicenseKey(); + } + } + + /** + * Process queued posts on page load (backend and/or frontend). + * + * Only runs if page-load processing is enabled in component config, + * and only once per throttle interval (default 5 minutes). + */ + public function onAfterRender(): void + { + $componentParams = ComponentHelper::getParams('com_mokosuitecross'); + $processingMode = $componentParams->get('queue_processing', 'scheduler'); + + if ($processingMode !== 'pageload' && $processingMode !== 'both') { + return; + } + + $app = $this->getApplication(); + + $pageloadClient = $componentParams->get('pageload_client', 'both'); + + if ($pageloadClient === 'admin' && !$app->isClient('administrator')) { + return; + } + + if ($pageloadClient === 'site' && !$app->isClient('site')) { + return; + } + + // Throttle: only run once per interval + $throttleSeconds = (int) $componentParams->get('pageload_interval', 300); + $lastRun = (int) $componentParams->get('_pageload_last_run', 0); + + if ((time() - $lastRun) < $throttleSeconds) { + return; + } + + if (!\Joomla\Component\MokoSuiteCross\Administrator\Helper\QueueProcessor::hasPendingWork()) { + return; + } + + $this->updateLastRunTimestamp(); + + // Small batch to avoid slowing page loads + \Joomla\Component\MokoSuiteCross\Administrator\Helper\QueueProcessor::processQueue(5); + } + + /** + * Warn administrators once per session when no license key is configured. + * + * @return void + */ + private function warnMissingLicenseKey(): void + { + $session = Factory::getSession(); + + if ($session->get('mokosuitecross.license_warned', false)) { + return; + } + + $user = Factory::getUser(); + + if ($user->guest || !$user->authorise('core.manage')) { + return; + } + + $session->set('mokosuitecross.license_warned', true); + + try { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select($db->quoteName('extra_query')) + ->from($db->quoteName('#__update_sites')) + ->where($db->quoteName('name') . ' = ' . $db->quote('MokoSuiteCross Updates')) + ->setLimit(1); + $db->setQuery($query); + $extraQuery = (string) $db->loadResult(); + + if (!empty($extraQuery)) { + parse_str($extraQuery, $parsed); + + if (!empty($parsed['dlid']) && preg_match('/^MOKO-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/', $parsed['dlid'])) { + return; + } + } + + $this->getApplication()->enqueueMessage( + 'Moko Consulting License Key Required — ' + . 'No download key is configured. Updates will not be available until a valid license key is entered. ' + . 'Go to System → Update Sites ' + . 'and enter your license key (MOKO-XXXX-XXXX-XXXX-XXXX) in the Download Key field ' + . 'for the MokoSuiteCross update site.', + 'warning' + ); + } catch (\Throwable $e) { + // Don't break admin over a license check + } + } + + /** + * Store the last page-load run timestamp. + */ + private function updateLastRunTimestamp(): void + { + $db = Factory::getDbo(); + $now = time(); + + // Use JSON_SET for atomic update without read-modify-write race + try { + $db->setQuery( + 'UPDATE ' . $db->quoteName('#__extensions') + . ' SET ' . $db->quoteName('params') . ' = JSON_SET(' + . $db->quoteName('params') . ', ' . $db->quote('$._pageload_last_run') . ', ' . $now . ')' + . ' WHERE ' . $db->quoteName('type') . ' = ' . $db->quote('component') + . ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('com_mokosuitecross') + ); + $db->execute(); + } catch (\Throwable $e) { + // Fallback for databases without JSON_SET + $query = $db->getQuery(true) + ->select($db->quoteName('params')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuitecross')); + + $db->setQuery($query); + $params = json_decode($db->loadResult() ?: '{}', true) ?: []; + $params['_pageload_last_run'] = $now; + + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params))) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuitecross')); + + $db->setQuery($query); + $db->execute(); + } + } +} diff --git a/src/packages/plg_webservices_mokojoomcross/index.html b/source/packages/plg_system_mokosuitecross/src/Extension/index.html similarity index 100% rename from src/packages/plg_webservices_mokojoomcross/index.html rename to source/packages/plg_system_mokosuitecross/src/Extension/index.html diff --git a/src/packages/plg_webservices_mokojoomcross/language/en-GB/index.html b/source/packages/plg_system_mokosuitecross/src/index.html similarity index 100% rename from src/packages/plg_webservices_mokojoomcross/language/en-GB/index.html rename to source/packages/plg_system_mokosuitecross/src/index.html diff --git a/source/packages/plg_system_mokosuitecross_events/language/en-GB/plg_system_mokosuitecross_events.ini b/source/packages/plg_system_mokosuitecross_events/language/en-GB/plg_system_mokosuitecross_events.ini new file mode 100644 index 0000000..24712bb --- /dev/null +++ b/source/packages/plg_system_mokosuitecross_events/language/en-GB/plg_system_mokosuitecross_events.ini @@ -0,0 +1,2 @@ +PLG_SYSTEM_MOKOSUITECROSS_EVENTS="System - MokoSuiteCross Events" +PLG_SYSTEM_MOKOSUITECROSS_EVENTS_DESCRIPTION="Cross-posts MokoSuiteCalendar events to social media and messaging platforms via MokoSuiteCross." diff --git a/source/packages/plg_system_mokosuitecross_events/language/en-GB/plg_system_mokosuitecross_events.sys.ini b/source/packages/plg_system_mokosuitecross_events/language/en-GB/plg_system_mokosuitecross_events.sys.ini new file mode 100644 index 0000000..24712bb --- /dev/null +++ b/source/packages/plg_system_mokosuitecross_events/language/en-GB/plg_system_mokosuitecross_events.sys.ini @@ -0,0 +1,2 @@ +PLG_SYSTEM_MOKOSUITECROSS_EVENTS="System - MokoSuiteCross Events" +PLG_SYSTEM_MOKOSUITECROSS_EVENTS_DESCRIPTION="Cross-posts MokoSuiteCalendar events to social media and messaging platforms via MokoSuiteCross." diff --git a/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.php b/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.php new file mode 100644 index 0000000..e298800 --- /dev/null +++ b/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.php @@ -0,0 +1,12 @@ + + * @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_system_mokosuitecross_events/mokosuitecross_events.xml b/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml new file mode 100644 index 0000000..e38f450 --- /dev/null +++ b/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml @@ -0,0 +1,26 @@ + + + System - MokoSuiteCross Events + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_SYSTEM_MOKOSUITECROSS_EVENTS_DESCRIPTION + + Joomla\Plugin\System\MokoSuiteCrossEvents + + + mokosuitecross_events.php + src + services + language + + + + language/en-GB/plg_system_mokosuitecross_events.ini + language/en-GB/plg_system_mokosuitecross_events.sys.ini + + diff --git a/source/packages/plg_system_mokosuitecross_events/services/provider.php b/source/packages/plg_system_mokosuitecross_events/services/provider.php new file mode 100644 index 0000000..72eb108 --- /dev/null +++ b/source/packages/plg_system_mokosuitecross_events/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\System\MokoSuiteCrossEvents\Extension\MokoSuiteCrossEvents; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new MokoSuiteCrossEvents( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('system', 'mokosuitecross_events') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_system_mokosuitecross_events/src/Extension/MokoSuiteCrossEvents.php b/source/packages/plg_system_mokosuitecross_events/src/Extension/MokoSuiteCrossEvents.php new file mode 100644 index 0000000..ff19f2f --- /dev/null +++ b/source/packages/plg_system_mokosuitecross_events/src/Extension/MokoSuiteCrossEvents.php @@ -0,0 +1,88 @@ + + * @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\System\MokoSuiteCrossEvents\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\CMS\Uri\Uri; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\CrossPostDispatcher; +use Joomla\Event\SubscriberInterface; + +/** + * System plugin that cross-posts MokoSuiteCalendar events when published. + * + * Subscribes to the custom onMokoSuiteCalendarEventAfterSave event fired by + * MokoSuiteCalendar and maps the calendar event to an article-like payload + * for dispatch through MokoSuiteCross services. + */ +class MokoSuiteCrossEvents extends CMSPlugin implements SubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + 'onMokoSuiteCalendarEventAfterSave' => 'onMokoSuiteCalendarEventAfterSave', + ]; + } + + /** + * Cross-post calendar events when published. + */ + public function onMokoSuiteCalendarEventAfterSave($event): void + { + // Check com_mokosuitecalendar is installed + if (!file_exists(JPATH_ADMINISTRATOR . '/components/com_mokosuitecalendar')) { + return; + } + + $item = $event->getArgument('item'); + $isNew = $event->getArgument('isNew'); + + if ((int) ($item->published ?? 0) !== 1) { + return; + } + + $componentParams = ComponentHelper::getParams('com_mokosuitecross'); + + if (!$componentParams->get('auto_post_on_publish', 1)) { + return; + } + + if ($componentParams->get('post_on_first_publish_only', 0) && !$isNew) { + return; + } + + // Map calendar event to article-like structure + $url = Uri::root() . 'index.php?option=com_mokosuitecalendar&view=event&id=' . $item->id; + + $article = (object) [ + 'id' => (int) $item->id, + 'title' => $item->title ?? '', + 'introtext' => strip_tags(mb_substr($item->description ?? '', 0, 280)), + 'fulltext' => $item->description ?? '', + 'images' => !empty($item->image) + ? json_encode(['image_intro' => $item->image]) + : '{}', + 'state' => 1, + 'catid' => 0, + 'attribs' => $item->params ?? '{}', + 'publish_up' => $item->start_date ?? $item->created ?? '', + 'created_by' => $item->created_by ?? 0, + '_content_type' => 'mokosuitecalendar', + '_article_id' => (int) $item->id, + '_article_url' => $url, + ]; + + CrossPostDispatcher::dispatch($article, $url, 'com_mokosuitecalendar.event'); + } +} diff --git a/source/packages/plg_system_mokosuitecross_gallery/language/en-GB/plg_system_mokosuitecross_gallery.ini b/source/packages/plg_system_mokosuitecross_gallery/language/en-GB/plg_system_mokosuitecross_gallery.ini new file mode 100644 index 0000000..16b2c40 --- /dev/null +++ b/source/packages/plg_system_mokosuitecross_gallery/language/en-GB/plg_system_mokosuitecross_gallery.ini @@ -0,0 +1,2 @@ +PLG_SYSTEM_MOKOSUITECROSS_GALLERY="System - MokoSuiteCross Gallery" +PLG_SYSTEM_MOKOSUITECROSS_GALLERY_DESCRIPTION="Cross-posts MokoSuiteGallery galleries and images to social media and messaging platforms via MokoSuiteCross." diff --git a/source/packages/plg_system_mokosuitecross_gallery/language/en-GB/plg_system_mokosuitecross_gallery.sys.ini b/source/packages/plg_system_mokosuitecross_gallery/language/en-GB/plg_system_mokosuitecross_gallery.sys.ini new file mode 100644 index 0000000..16b2c40 --- /dev/null +++ b/source/packages/plg_system_mokosuitecross_gallery/language/en-GB/plg_system_mokosuitecross_gallery.sys.ini @@ -0,0 +1,2 @@ +PLG_SYSTEM_MOKOSUITECROSS_GALLERY="System - MokoSuiteCross Gallery" +PLG_SYSTEM_MOKOSUITECROSS_GALLERY_DESCRIPTION="Cross-posts MokoSuiteGallery galleries and images to social media and messaging platforms via MokoSuiteCross." diff --git a/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.php b/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.php new file mode 100644 index 0000000..72592fb --- /dev/null +++ b/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.php @@ -0,0 +1,12 @@ + + * @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_system_mokosuitecross_gallery/mokosuitecross_gallery.xml b/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml new file mode 100644 index 0000000..112d8ab --- /dev/null +++ b/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml @@ -0,0 +1,26 @@ + + + System - MokoSuiteCross Gallery + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_SYSTEM_MOKOSUITECROSS_GALLERY_DESCRIPTION + + Joomla\Plugin\System\MokoSuiteCrossGallery + + + mokosuitecross_gallery.php + src + services + language + + + + language/en-GB/plg_system_mokosuitecross_gallery.ini + language/en-GB/plg_system_mokosuitecross_gallery.sys.ini + + diff --git a/source/packages/plg_system_mokosuitecross_gallery/services/provider.php b/source/packages/plg_system_mokosuitecross_gallery/services/provider.php new file mode 100644 index 0000000..9ac1177 --- /dev/null +++ b/source/packages/plg_system_mokosuitecross_gallery/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\System\MokoSuiteCrossGallery\Extension\MokoSuiteCrossGallery; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new MokoSuiteCrossGallery( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('system', 'mokosuitecross_gallery') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_system_mokosuitecross_gallery/src/Extension/MokoSuiteCrossGallery.php b/source/packages/plg_system_mokosuitecross_gallery/src/Extension/MokoSuiteCrossGallery.php new file mode 100644 index 0000000..ab852d0 --- /dev/null +++ b/source/packages/plg_system_mokosuitecross_gallery/src/Extension/MokoSuiteCrossGallery.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\System\MokoSuiteCrossGallery\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\CMS\Uri\Uri; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\CrossPostDispatcher; +use Joomla\Event\SubscriberInterface; + +/** + * System plugin that cross-posts MokoSuiteGallery galleries and images when published. + * + * Subscribes to custom events fired by MokoSuiteGallery and maps gallery/image + * items to article-like payloads for dispatch through MokoSuiteCross services. + */ +class MokoSuiteCrossGallery extends CMSPlugin implements SubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + 'onMokoSuiteGalleryGalleryAfterSave' => 'onMokoSuiteGalleryGalleryAfterSave', + 'onMokoSuiteGalleryImageAfterSave' => 'onMokoSuiteGalleryImageAfterSave', + ]; + } + + /** + * Cross-post galleries when published. + */ + public function onMokoSuiteGalleryGalleryAfterSave($event): void + { + if (!file_exists(JPATH_ADMINISTRATOR . '/components/com_mokosuitegallery')) { + return; + } + + $item = $event->getArgument('item'); + $isNew = $event->getArgument('isNew'); + + if ((int) ($item->published ?? 0) !== 1) { + return; + } + + $componentParams = ComponentHelper::getParams('com_mokosuitecross'); + + if (!$componentParams->get('auto_post_on_publish', 1)) { + return; + } + + if ($componentParams->get('post_on_first_publish_only', 0) && !$isNew) { + return; + } + + $url = Uri::root() . 'index.php?option=com_mokosuitegallery&view=category&id=' . $item->id; + + $article = (object) [ + 'id' => (int) $item->id, + 'title' => $item->title ?? '', + 'introtext' => strip_tags(mb_substr($item->description ?? '', 0, 280)), + 'fulltext' => $item->description ?? '', + 'images' => !empty($item->image) + ? json_encode(['image_intro' => $item->image]) + : '{}', + 'state' => 1, + 'catid' => 0, + 'attribs' => $item->params ?? '{}', + 'publish_up' => $item->created ?? '', + 'created_by' => $item->created_by ?? 0, + '_content_type' => 'mokosuitegallery', + '_article_id' => (int) $item->id, + '_article_url' => $url, + ]; + + CrossPostDispatcher::dispatch($article, $url, 'com_mokosuitegallery.gallery'); + } + + /** + * Cross-post individual images when published. + */ + public function onMokoSuiteGalleryImageAfterSave($event): void + { + if (!file_exists(JPATH_ADMINISTRATOR . '/components/com_mokosuitegallery')) { + return; + } + + $item = $event->getArgument('item'); + $isNew = $event->getArgument('isNew'); + + if ((int) ($item->published ?? 0) !== 1) { + return; + } + + $componentParams = ComponentHelper::getParams('com_mokosuitecross'); + + if (!$componentParams->get('auto_post_on_publish', 1)) { + return; + } + + if ($componentParams->get('post_on_first_publish_only', 0) && !$isNew) { + return; + } + + $imagePath = $item->original ?? $item->thumbnail ?? ''; + + $url = Uri::root() . 'index.php?option=com_mokosuitegallery&view=category&id=' . ($item->gallery_id ?? 0); + + $article = (object) [ + 'id' => (int) $item->id, + 'title' => $item->title ?? '', + 'introtext' => strip_tags(mb_substr($item->description ?? '', 0, 280)), + 'fulltext' => $item->description ?? '', + 'images' => $imagePath + ? json_encode(['image_intro' => $imagePath]) + : '{}', + 'state' => 1, + 'catid' => 0, + 'attribs' => '{}', + 'publish_up' => $item->created ?? '', + 'created_by' => $item->created_by ?? 0, + '_content_type' => 'mokosuitegallery', + '_article_id' => (int) $item->id, + '_article_url' => $url, + ]; + + CrossPostDispatcher::dispatch($article, $url, 'com_mokosuitegallery.image'); + } +} diff --git a/src/packages/plg_webservices_mokojoomcross/language/index.html b/source/packages/plg_task_mokosuitecross/index.html similarity index 100% rename from src/packages/plg_webservices_mokojoomcross/language/index.html rename to source/packages/plg_task_mokosuitecross/index.html diff --git a/src/packages/plg_webservices_mokojoomcross/services/index.html b/source/packages/plg_task_mokosuitecross/language/en-GB/index.html similarity index 100% rename from src/packages/plg_webservices_mokojoomcross/services/index.html rename to source/packages/plg_task_mokosuitecross/language/en-GB/index.html diff --git a/source/packages/plg_task_mokosuitecross/language/en-GB/plg_task_mokosuitecross.ini b/source/packages/plg_task_mokosuitecross/language/en-GB/plg_task_mokosuitecross.ini new file mode 100644 index 0000000..8d7f94b --- /dev/null +++ b/source/packages/plg_task_mokosuitecross/language/en-GB/plg_task_mokosuitecross.ini @@ -0,0 +1,9 @@ +; Task - MokoSuiteCross Queue Processor Language File +; Copyright (C) 2026 Moko Consulting. All rights reserved. +; License: GPL-3.0-or-later + +PLG_TASK_MOKOSUITECROSS="Task - MokoSuiteCross Queue Processor" +PLG_TASK_MOKOSUITECROSS_DESCRIPTION="Joomla Scheduled Task for processing the MokoSuiteCross cross-post queue. Handles queued posts, retries, scheduled posts, and log cleanup." + +PLG_TASK_MOKOSUITECROSS_PROCESS_QUEUE_TITLE="MokoSuiteCross - Process Queue" +PLG_TASK_MOKOSUITECROSS_PROCESS_QUEUE_DESC="Process queued cross-posts, retry failed posts, fire scheduled posts, and clean up old logs." diff --git a/source/packages/plg_task_mokosuitecross/language/en-GB/plg_task_mokosuitecross.sys.ini b/source/packages/plg_task_mokosuitecross/language/en-GB/plg_task_mokosuitecross.sys.ini new file mode 100644 index 0000000..114da8f --- /dev/null +++ b/source/packages/plg_task_mokosuitecross/language/en-GB/plg_task_mokosuitecross.sys.ini @@ -0,0 +1,2 @@ +PLG_TASK_MOKOSUITECROSS="Task - MokoSuiteCross Queue Processor" +PLG_TASK_MOKOSUITECROSS_DESCRIPTION="Joomla Scheduled Task for processing the MokoSuiteCross cross-post queue." diff --git a/src/packages/plg_webservices_mokojoomcross/src/Extension/index.html b/source/packages/plg_task_mokosuitecross/language/index.html similarity index 100% rename from src/packages/plg_webservices_mokojoomcross/src/Extension/index.html rename to source/packages/plg_task_mokosuitecross/language/index.html diff --git a/source/packages/plg_task_mokosuitecross/mokosuitecross.php b/source/packages/plg_task_mokosuitecross/mokosuitecross.php new file mode 100644 index 0000000..f3fad27 --- /dev/null +++ b/source/packages/plg_task_mokosuitecross/mokosuitecross.php @@ -0,0 +1,12 @@ + + * @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/src/packages/plg_mokojoomcross_slack/slack.xml b/source/packages/plg_task_mokosuitecross/mokosuitecross.xml similarity index 50% rename from src/packages/plg_mokojoomcross_slack/slack.xml rename to source/packages/plg_task_mokosuitecross/mokosuitecross.xml index 82a7bba..5298491 100644 --- a/src/packages/plg_mokojoomcross_slack/slack.xml +++ b/source/packages/plg_task_mokosuitecross/mokosuitecross.xml @@ -1,26 +1,26 @@ - - MokoJoomCross - Slack - 01.01.00 + + Task - MokoSuiteCross Queue Processor + 01.00.27-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech https://mokoconsulting.tech Copyright (C) 2026 Moko Consulting. All rights reserved. GPL-3.0-or-later - PLG_MOKOJOOMCROSS_SLACK_DESCRIPTION + PLG_TASK_MOKOSUITECROSS_DESCRIPTION - Joomla\Plugin\MokoJoomCross${CLASS_NAME} + Joomla\Plugin\Task\MokoSuiteCross - slack.php + mokosuitecross.php src services language - language/en-GB/plg_mokojoomcross_slack.ini - language/en-GB/plg_mokojoomcross_slack.sys.ini + language/en-GB/plg_task_mokosuitecross.ini + language/en-GB/plg_task_mokosuitecross.sys.ini diff --git a/src/packages/plg_webservices_mokojoomcross/src/index.html b/source/packages/plg_task_mokosuitecross/services/index.html similarity index 100% rename from src/packages/plg_webservices_mokojoomcross/src/index.html rename to source/packages/plg_task_mokosuitecross/services/index.html diff --git a/source/packages/plg_task_mokosuitecross/services/provider.php b/source/packages/plg_task_mokosuitecross/services/provider.php new file mode 100644 index 0000000..58028c7 --- /dev/null +++ b/source/packages/plg_task_mokosuitecross/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\Task\MokoSuiteCross\Extension\MokoSuiteCrossTask; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new MokoSuiteCrossTask( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('task', 'mokosuitecross') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_task_mokosuitecross/src/Extension/MokoSuiteCrossTask.php b/source/packages/plg_task_mokosuitecross/src/Extension/MokoSuiteCrossTask.php new file mode 100644 index 0000000..b007c0e --- /dev/null +++ b/source/packages/plg_task_mokosuitecross/src/Extension/MokoSuiteCrossTask.php @@ -0,0 +1,92 @@ + + * @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\Task\MokoSuiteCross\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Helper\QueueProcessor; +use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent; +use Joomla\Component\Scheduler\Administrator\Task\Status as TaskStatus; +use Joomla\Component\Scheduler\Administrator\Traits\TaskPluginTrait; +use Joomla\Event\SubscriberInterface; + +/** + * Joomla Scheduled Task plugin for MokoSuiteCross queue processing. + * + * Registers with Joomla's Task Scheduler (System → Scheduled Tasks). + * Admin can create a task of type "MokoSuiteCross - Process Queue" + * and configure the interval (recommended: every 5 minutes). + * + * This is the PREFERRED processing method. Page-load processing is + * a fallback for environments without cron/scheduler access. + */ +class MokoSuiteCrossTask extends CMSPlugin implements SubscriberInterface +{ + use TaskPluginTrait; + + /** + * @var string[] The task type IDs this plugin provides + */ + protected const TASKS_MAP = [ + 'mokosuitecross.process_queue' => [ + 'langConstPrefix' => 'PLG_TASK_MOKOSUITECROSS_PROCESS_QUEUE', + 'method' => 'processQueue', + 'form' => '', + ], + ]; + + public static function getSubscribedEvents(): array + { + return [ + 'onTaskOptionsList' => 'advertiseRoutines', + 'onExecuteTask' => 'standardRoutineHandler', + 'onContentPrepareForm' => 'enhanceTaskItemForm', + ]; + } + + /** + * Process the cross-post queue. + * + * @param ExecuteTaskEvent $event The task event + * + * @return int Task status code + */ + private function processQueue(ExecuteTaskEvent $event): int + { + // 1. Process evergreen re-shares (queues new posts for due articles) + $evergreen = QueueProcessor::processEvergreen(); + + if ($evergreen['queued'] > 0) { + $this->logTask(sprintf('MokoSuiteCross evergreen: %d re-shares queued', $evergreen['queued'])); + } + + // 2. Process the queue (including any newly queued evergreen posts) + $result = QueueProcessor::processQueue(20); + + $this->logTask(sprintf( + 'MokoSuiteCross queue: %d processed, %d succeeded, %d failed, %d skipped', + $result['processed'], + $result['succeeded'], + $result['failed'], + $result['skipped'] + )); + + if ($result['skipped'] === -1) { + $this->logTask('Queue processing skipped — another process holds the lock'); + + return TaskStatus::KNOCKOUT; + } + + return TaskStatus::OK; + } +} diff --git a/source/packages/plg_task_mokosuitecross/src/Extension/index.html b/source/packages/plg_task_mokosuitecross/src/Extension/index.html new file mode 100644 index 0000000..6182993 --- /dev/null +++ b/source/packages/plg_task_mokosuitecross/src/Extension/index.html @@ -0,0 +1 @@ +<\!DOCTYPE html> diff --git a/source/packages/plg_task_mokosuitecross/src/index.html b/source/packages/plg_task_mokosuitecross/src/index.html new file mode 100644 index 0000000..6182993 --- /dev/null +++ b/source/packages/plg_task_mokosuitecross/src/index.html @@ -0,0 +1 @@ +<\!DOCTYPE html> diff --git a/source/packages/plg_webservices_mokosuitecross/index.html b/source/packages/plg_webservices_mokosuitecross/index.html new file mode 100644 index 0000000..6182993 --- /dev/null +++ b/source/packages/plg_webservices_mokosuitecross/index.html @@ -0,0 +1 @@ +<\!DOCTYPE html> diff --git a/source/packages/plg_webservices_mokosuitecross/language/en-GB/index.html b/source/packages/plg_webservices_mokosuitecross/language/en-GB/index.html new file mode 100644 index 0000000..6182993 --- /dev/null +++ b/source/packages/plg_webservices_mokosuitecross/language/en-GB/index.html @@ -0,0 +1 @@ +<\!DOCTYPE html> diff --git a/source/packages/plg_webservices_mokosuitecross/language/en-GB/plg_webservices_mokosuitecross.ini b/source/packages/plg_webservices_mokosuitecross/language/en-GB/plg_webservices_mokosuitecross.ini new file mode 100644 index 0000000..171eba1 --- /dev/null +++ b/source/packages/plg_webservices_mokosuitecross/language/en-GB/plg_webservices_mokosuitecross.ini @@ -0,0 +1,2 @@ +PLG_WEBSERVICES_MOKOSUITECROSS="Web Services - MokoSuiteCross" +PLG_WEBSERVICES_MOKOSUITECROSS_DESCRIPTION="Provides REST API endpoints for MokoSuiteCross posts and services." diff --git a/source/packages/plg_webservices_mokosuitecross/language/en-GB/plg_webservices_mokosuitecross.sys.ini b/source/packages/plg_webservices_mokosuitecross/language/en-GB/plg_webservices_mokosuitecross.sys.ini new file mode 100644 index 0000000..171eba1 --- /dev/null +++ b/source/packages/plg_webservices_mokosuitecross/language/en-GB/plg_webservices_mokosuitecross.sys.ini @@ -0,0 +1,2 @@ +PLG_WEBSERVICES_MOKOSUITECROSS="Web Services - MokoSuiteCross" +PLG_WEBSERVICES_MOKOSUITECROSS_DESCRIPTION="Provides REST API endpoints for MokoSuiteCross posts and services." diff --git a/source/packages/plg_webservices_mokosuitecross/language/index.html b/source/packages/plg_webservices_mokosuitecross/language/index.html new file mode 100644 index 0000000..6182993 --- /dev/null +++ b/source/packages/plg_webservices_mokosuitecross/language/index.html @@ -0,0 +1 @@ +<\!DOCTYPE html> diff --git a/src/packages/plg_webservices_mokojoomcross/mokojoomcross.php b/source/packages/plg_webservices_mokosuitecross/mokosuitecross.php similarity index 100% rename from src/packages/plg_webservices_mokojoomcross/mokojoomcross.php rename to source/packages/plg_webservices_mokosuitecross/mokosuitecross.php diff --git a/src/packages/plg_webservices_mokojoomcross/mokojoomcross.xml b/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml similarity index 66% rename from src/packages/plg_webservices_mokojoomcross/mokojoomcross.xml rename to source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml index 700387b..3f1ffb9 100644 --- a/src/packages/plg_webservices_mokojoomcross/mokojoomcross.xml +++ b/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml @@ -1,26 +1,26 @@ - Web Services - MokoJoomCross - 01.01.00 + Web Services - MokoSuiteCross + 01.00.27-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech https://mokoconsulting.tech Copyright (C) 2026 Moko Consulting. All rights reserved. GPL-3.0-or-later - PLG_WEBSERVICES_MOKOJOOMCROSS_DESCRIPTION + PLG_WEBSERVICES_MOKOSUITECROSS_DESCRIPTION - Joomla\Plugin\WebServices\MokoJoomCross + Joomla\Plugin\WebServices\MokoSuiteCross - mokojoomcross.php + mokosuitecross.php src services language - language/en-GB/plg_webservices_mokojoomcross.ini - language/en-GB/plg_webservices_mokojoomcross.sys.ini + language/en-GB/plg_webservices_mokosuitecross.ini + language/en-GB/plg_webservices_mokosuitecross.sys.ini diff --git a/source/packages/plg_webservices_mokosuitecross/services/index.html b/source/packages/plg_webservices_mokosuitecross/services/index.html new file mode 100644 index 0000000..6182993 --- /dev/null +++ b/source/packages/plg_webservices_mokosuitecross/services/index.html @@ -0,0 +1 @@ +<\!DOCTYPE html> diff --git a/src/packages/plg_webservices_mokojoomcross/services/provider.php b/source/packages/plg_webservices_mokosuitecross/services/provider.php similarity index 80% rename from src/packages/plg_webservices_mokojoomcross/services/provider.php rename to source/packages/plg_webservices_mokosuitecross/services/provider.php index d1a4cc6..cb04b6f 100644 --- a/src/packages/plg_webservices_mokojoomcross/services/provider.php +++ b/source/packages/plg_webservices_mokosuitecross/services/provider.php @@ -1,8 +1,8 @@ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE @@ -17,7 +17,7 @@ use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; -use Joomla\Plugin\WebServices\MokoJoomCross\Extension\MokoJoomCrossWebServices; +use Joomla\Plugin\WebServices\MokoSuiteCross\Extension\MokoSuiteCrossWebServices; return new class () implements ServiceProviderInterface { public function register(Container $container): void @@ -25,9 +25,9 @@ return new class () implements ServiceProviderInterface { $container->set( PluginInterface::class, function (Container $container) { - $plugin = new MokoJoomCrossWebServices( + $plugin = new MokoSuiteCrossWebServices( $container->get(DispatcherInterface::class), - (array) PluginHelper::getPlugin('webservices', 'mokojoomcross') + (array) PluginHelper::getPlugin('webservices', 'mokosuitecross') ); $plugin->setApplication(Factory::getApplication()); diff --git a/source/packages/plg_webservices_mokosuitecross/src/Extension/MokoSuiteCrossWebServices.php b/source/packages/plg_webservices_mokosuitecross/src/Extension/MokoSuiteCrossWebServices.php new file mode 100644 index 0000000..f01e64c --- /dev/null +++ b/source/packages/plg_webservices_mokosuitecross/src/Extension/MokoSuiteCrossWebServices.php @@ -0,0 +1,52 @@ + + * @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\WebServices\MokoSuiteCross\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Event\SubscriberInterface; + +/** + * WebServices plugin providing REST API endpoints for MokoSuiteCross. + * + * Endpoints: + * /api/index.php/v1/mokosuitecross/posts — CRUD cross-posts + * /api/index.php/v1/mokosuitecross/services — CRUD services + * /api/index.php/v1/mokosuitecross/templates — CRUD templates + * /api/index.php/v1/mokosuitecross/logs — Read logs + * /api/index.php/v1/mokosuitecross/dispatch — POST dispatch cross-posts for an article + */ +class MokoSuiteCrossWebServices extends CMSPlugin implements SubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + 'onBeforeApiRoute' => 'onBeforeApiRoute', + ]; + } + + public function onBeforeApiRoute(&$router): void + { + $defaults = ['component' => 'com_mokosuitecross']; + + $router->createCRUDRoutes('v1/mokosuitecross/posts', 'posts', $defaults); + $router->createCRUDRoutes('v1/mokosuitecross/services', 'services', $defaults); + $router->createCRUDRoutes('v1/mokosuitecross/templates', 'templates', $defaults); + $router->createCRUDRoutes('v1/mokosuitecross/logs', 'logs', $defaults); + + // Action endpoint: dispatch cross-posts for an article (POST only) + $router->addRoute( + new \Joomla\Router\Route(['POST'], 'v1/mokosuitecross/dispatch', 'dispatch.dispatch', [], $defaults) + ); + } +} diff --git a/source/packages/plg_webservices_mokosuitecross/src/Extension/index.html b/source/packages/plg_webservices_mokosuitecross/src/Extension/index.html new file mode 100644 index 0000000..6182993 --- /dev/null +++ b/source/packages/plg_webservices_mokosuitecross/src/Extension/index.html @@ -0,0 +1 @@ +<\!DOCTYPE html> diff --git a/source/packages/plg_webservices_mokosuitecross/src/index.html b/source/packages/plg_webservices_mokosuitecross/src/index.html new file mode 100644 index 0000000..6182993 --- /dev/null +++ b/source/packages/plg_webservices_mokosuitecross/src/index.html @@ -0,0 +1 @@ +<\!DOCTYPE html> diff --git a/source/pkg_mokosuitecross.xml b/source/pkg_mokosuitecross.xml new file mode 100644 index 0000000..5b18093 --- /dev/null +++ b/source/pkg_mokosuitecross.xml @@ -0,0 +1,75 @@ + + + MokoSuiteCross + mokosuitecross + 01.00.27-dev + 2026-05-28 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PKG_MOKOSUITECROSS_DESCRIPTION + + script.php + + + + com_mokosuitecross.zip + plg_system_mokosuitecross.zip + plg_content_mokosuitecross.zip + plg_webservices_mokosuitecross.zip + plg_task_mokosuitecross.zip + + + plg_mokosuitecross_facebook.zip + plg_mokosuitecross_twitter.zip + plg_mokosuitecross_linkedin.zip + plg_mokosuitecross_mastodon.zip + plg_mokosuitecross_bluesky.zip + plg_mokosuitecross_mailchimp.zip + plg_mokosuitecross_telegram.zip + plg_mokosuitecross_discord.zip + plg_mokosuitecross_slack.zip + + + plg_mokosuitecross_webhook.zip + plg_mokosuitecross_teams.zip + plg_mokosuitecross_threads.zip + plg_mokosuitecross_googlebusiness.zip + plg_mokosuitecross_whatsapp.zip + plg_mokosuitecross_googlechat.zip + plg_mokosuitecross_medium.zip + plg_mokosuitecross_pinterest.zip + plg_mokosuitecross_reddit.zip + plg_mokosuitecross_sendgrid.zip + plg_mokosuitecross_brevo.zip + plg_mokosuitecross_wordpress.zip + plg_mokosuitecross_ntfy.zip + plg_mokosuitecross_tumblr.zip + plg_mokosuitecross_convertkit.zip + plg_mokosuitecross_nostr.zip + plg_mokosuitecross_activitypub.zip + plg_mokosuitecross_devto.zip + plg_mokosuitecross_ghost.zip + plg_mokosuitecross_hashnode.zip + plg_mokosuitecross_blogger.zip + plg_mokosuitecross_matrix.zip + plg_mokosuitecross_rssfeed.zip + plg_mokosuitecross_constantcontact.zip + plg_mokosuitecross_tiktok.zip + plg_mokosuitecross_mokosuitecalendar.zip + plg_mokosuitecross_mokosuitegallery.zip + + + + language/en-GB/pkg_mokosuitecross.sys.ini + + + + true + + + https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/updates.xml + + diff --git a/source/script.php b/source/script.php new file mode 100644 index 0000000..7c4bfcb --- /dev/null +++ b/source/script.php @@ -0,0 +1,197 @@ + + * @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\Factory; +use Joomla\CMS\Installer\InstallerAdapter; +use Joomla\CMS\Language\Text; + +class Pkg_MokoSuiteCrossInstallerScript +{ + protected $minimumPhp = '8.1.0'; + + + + + public function preflight(string $type, InstallerAdapter $parent): bool + { + if (version_compare(PHP_VERSION, $this->minimumPhp, '<')) + { + Factory::getApplication()->enqueueMessage( + Text::sprintf('PKG_MOKOSUITECROSS_PHP_VERSION_ERROR', $this->minimumPhp), + 'error' + ); + + return false; + } + + $this->saveDownloadKey(); + + return true; + } + + public function postflight(string $type, InstallerAdapter $parent): void + { + $this->restoreDownloadKey(); + $this->warnMissingLicenseKey(); + + $db = Factory::getDbo(); + + if ($type === 'install') + { + $corePlugins = [ + ['system', 'mokosuitecross'], + ['content', 'mokosuitecross'], + ['webservices', 'mokosuitecross'], + ['task', 'mokosuitecross'], + ]; + + foreach ($corePlugins as [$folder, $element]) + { + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote($folder)) + ->where($db->quoteName('element') . ' = ' . $db->quote($element)); + $db->setQuery($query); + $db->execute(); + } + + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('mokosuitecross')); + $db->setQuery($query); + $db->execute(); + + $this->detectPerfectPublisherPro($db); + } + } + + private function detectPerfectPublisherPro($db): void + { + $query = $db->getQuery(true) + ->select($db->quoteName(['element', 'params'])) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' LIKE ' . $db->quote('%perfectpublisher%')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')); + $db->setQuery($query); + $result = $db->loadObject(); + + if ($result) + { + Factory::getApplication()->enqueueMessage( + Text::_('PKG_MOKOSUITECROSS_MIGRATION_DETECTED'), + 'notice' + ); + + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote(json_encode([ + 'migration_available' => 'perfectpublisher', + 'migration_source_params' => $result->params, + ]))) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuitecross')); + $db->setQuery($query); + $db->execute(); + } + } + + private ?string $savedDownloadKey = null; + + private function saveDownloadKey(): void + { + try + { + $db = \Joomla\CMS\Factory::getDbo(); + $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('us.extra_query')) + ->from($db->quoteName('#__update_sites', 'us')) + ->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id') + ->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id') + ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokosuitecross')) + ->setLimit(1) + ); + $key = $db->loadResult(); + if (!empty($key)) { $this->savedDownloadKey = $key; } + } + catch (\Throwable $e) {} + } + + private function restoreDownloadKey(): void + { + if ($this->savedDownloadKey === null) { return; } + + try + { + $db = \Joomla\CMS\Factory::getDbo(); + $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('us.update_site_id')) + ->from($db->quoteName('#__update_sites', 'us')) + ->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id') + ->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id') + ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokosuitecross')) + ->setLimit(1) + ); + $siteId = (int) $db->loadResult(); + if ($siteId > 0) + { + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__update_sites')) + ->set($db->quoteName('extra_query') . ' = ' . $db->quote($this->savedDownloadKey)) + ->where($db->quoteName('update_site_id') . ' = ' . $siteId) + )->execute(); + } + } + catch (\Throwable $e) {} + } + + private function warnMissingLicenseKey(): void + { + try + { + $db = \Joomla\CMS\Factory::getDbo(); + $db->setQuery( + $db->getQuery(true) + ->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')]) + ->from($db->quoteName('#__update_sites')) + ->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoSuiteCross%') . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuiteCross%') . ')') + ->setLimit(1) + ); + $site = $db->loadObject(); + + if ($site) + { + $eq = (string) ($site->extra_query ?? ''); + if (!empty($eq) && strpos($eq, 'dlid=') !== false) { parse_str($eq, $p); if (!empty($p['dlid'])) { return; } } + $editUrl = 'index.php?option=com_installer&task=updatesite.edit&update_site_id=' . (int) $site->update_site_id; + } + else + { + $editUrl = 'index.php?option=com_installer&view=updatesites'; + } + + \Joomla\CMS\Factory::getApplication()->enqueueMessage( + 'Moko Consulting License Key Required — ' + . 'No download key is configured. Updates will not be available until a valid license key is entered. ' + . 'Enter License Key', + 'warning' + ); + } + catch (\Throwable $e) {} + } +} diff --git a/src/language/en-GB/pkg_mokojoomcross.sys.ini b/src/language/en-GB/pkg_mokojoomcross.sys.ini deleted file mode 100644 index 601fc74..0000000 --- a/src/language/en-GB/pkg_mokojoomcross.sys.ini +++ /dev/null @@ -1,8 +0,0 @@ -; MokoJoomCross - Package System Language File -; Copyright (C) 2026 Moko Consulting. All rights reserved. -; License: GPL-3.0-or-later - -PKG_MOKOJOOMCROSS="MokoJoomCross" -PKG_MOKOJOOMCROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms. Automatically publish articles to Facebook, X/Twitter, LinkedIn, Mastodon, Bluesky, Mailchimp, Telegram, Discord, and Slack." -PKG_MOKOJOOMCROSS_PHP_VERSION_ERROR="MokoJoomCross requires PHP %s or later." -PKG_MOKOJOOMCROSS_MIGRATION_DETECTED="Perfect Publisher Pro detected! Navigate to Components → MokoJoomCross → Dashboard to migrate your settings." diff --git a/src/packages/com_mokojoomcross/forms/filter_posts.xml b/src/packages/com_mokojoomcross/forms/filter_posts.xml deleted file mode 100644 index 0e70b49..0000000 --- a/src/packages/com_mokojoomcross/forms/filter_posts.xml +++ /dev/null @@ -1,39 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/packages/com_mokojoomcross/language/en-GB/com_mokojoomcross.sys.ini b/src/packages/com_mokojoomcross/language/en-GB/com_mokojoomcross.sys.ini deleted file mode 100644 index 2f8c6a4..0000000 --- a/src/packages/com_mokojoomcross/language/en-GB/com_mokojoomcross.sys.ini +++ /dev/null @@ -1,10 +0,0 @@ -; MokoJoomCross — System Language File -; Copyright (C) 2026 Moko Consulting. All rights reserved. -; License: GPL-3.0-or-later - -COM_MOKOJOOMCROSS="MokoJoomCross" -COM_MOKOJOOMCROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms" -COM_MOKOJOOMCROSS_SUBMENU_DASHBOARD="Dashboard" -COM_MOKOJOOMCROSS_SUBMENU_POSTS="Post Queue" -COM_MOKOJOOMCROSS_SUBMENU_SERVICES="Services" -COM_MOKOJOOMCROSS_SUBMENU_LOGS="Activity Logs" diff --git a/src/packages/com_mokojoomcross/sql/uninstall.mysql.sql b/src/packages/com_mokojoomcross/sql/uninstall.mysql.sql deleted file mode 100644 index 1811c80..0000000 --- a/src/packages/com_mokojoomcross/sql/uninstall.mysql.sql +++ /dev/null @@ -1,5 +0,0 @@ --- MokoJoomCross — Uninstall -DROP TABLE IF EXISTS `#__mokojoomcross_logs`; -DROP TABLE IF EXISTS `#__mokojoomcross_posts`; -DROP TABLE IF EXISTS `#__mokojoomcross_templates`; -DROP TABLE IF EXISTS `#__mokojoomcross_services`; diff --git a/src/packages/com_mokojoomcross/src/Model/ServiceModel.php b/src/packages/com_mokojoomcross/src/Model/ServiceModel.php deleted file mode 100644 index 1e71511..0000000 --- a/src/packages/com_mokojoomcross/src/Model/ServiceModel.php +++ /dev/null @@ -1,54 +0,0 @@ - - * @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\Component\MokoJoomCross\Administrator\Model; - -defined('_JEXEC') or die; - -use Joomla\CMS\MVC\Model\AdminModel; - -class ServiceModel extends AdminModel -{ - /** - * Method to get the record form. - * - * @param array $data Data for the form - * @param boolean $loadData True if the form is to load its own data - * - * @return \Joomla\CMS\Form\Form|boolean - */ - public function getForm($data = [], $loadData = true) - { - $form = $this->loadForm( - 'com_mokojoomcross.service', - 'service', - ['control' => 'jform', 'load_data' => $loadData] - ); - - if (empty($form)) { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form - */ - protected function loadFormData() - { - $data = $this->getItem(); - - return $data; - } -} diff --git a/src/packages/com_mokojoomcross/src/View/Logs/HtmlView.php b/src/packages/com_mokojoomcross/src/View/Logs/HtmlView.php deleted file mode 100644 index 1d3228b..0000000 --- a/src/packages/com_mokojoomcross/src/View/Logs/HtmlView.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @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\Component\MokoJoomCross\Administrator\View\Logs; - -defined('_JEXEC') or die; - -use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; -use Joomla\CMS\Toolbar\ToolbarHelper; - -class HtmlView extends BaseHtmlView -{ - protected $items; - protected $pagination; - protected $state; - - public function display($tpl = null): void - { - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - - $this->addToolbar(); - - parent::display($tpl); - } - - protected function addToolbar(): void - { - ToolbarHelper::title('MokoJoomCross — Activity Logs', 'share-alt'); - ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'logs.delete', 'JTOOLBAR_DELETE'); - } -} diff --git a/src/packages/plg_content_mokojoomcross/language/en-GB/plg_content_mokojoomcross.ini b/src/packages/plg_content_mokojoomcross/language/en-GB/plg_content_mokojoomcross.ini deleted file mode 100644 index b3bd1ee..0000000 --- a/src/packages/plg_content_mokojoomcross/language/en-GB/plg_content_mokojoomcross.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_CONTENT_MOKOJOOMCROSS="Content - MokoJoomCross" -PLG_CONTENT_MOKOJOOMCROSS_DESCRIPTION="Adds cross-post status badges to articles in the admin backend." diff --git a/src/packages/plg_mokojoomcross_bluesky/bluesky.xml b/src/packages/plg_mokojoomcross_bluesky/bluesky.xml deleted file mode 100644 index 3be4988..0000000 --- a/src/packages/plg_mokojoomcross_bluesky/bluesky.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - MokoJoomCross - Bluesky - 01.01.00 - 2026-05-28 - Moko Consulting - hello@mokoconsulting.tech - https://mokoconsulting.tech - Copyright (C) 2026 Moko Consulting. All rights reserved. - GPL-3.0-or-later - PLG_MOKOJOOMCROSS_BLUESKY_DESCRIPTION - - Joomla\Plugin\MokoJoomCross${CLASS_NAME} - - - bluesky.php - src - services - language - - - - language/en-GB/plg_mokojoomcross_bluesky.ini - language/en-GB/plg_mokojoomcross_bluesky.sys.ini - - diff --git a/src/packages/plg_mokojoomcross_bluesky/language/en-GB/plg_mokojoomcross_bluesky.ini b/src/packages/plg_mokojoomcross_bluesky/language/en-GB/plg_mokojoomcross_bluesky.ini deleted file mode 100644 index fc9b753..0000000 --- a/src/packages/plg_mokojoomcross_bluesky/language/en-GB/plg_mokojoomcross_bluesky.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_MOKOJOOMCROSS_BLUESKY="MokoJoomCross - Bluesky" -PLG_MOKOJOOMCROSS_BLUESKY_DESCRIPTION="Cross-post Joomla articles to Bluesky." diff --git a/src/packages/plg_mokojoomcross_discord/discord.xml b/src/packages/plg_mokojoomcross_discord/discord.xml deleted file mode 100644 index 43598f0..0000000 --- a/src/packages/plg_mokojoomcross_discord/discord.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - MokoJoomCross - Discord - 01.01.00 - 2026-05-28 - Moko Consulting - hello@mokoconsulting.tech - https://mokoconsulting.tech - Copyright (C) 2026 Moko Consulting. All rights reserved. - GPL-3.0-or-later - PLG_MOKOJOOMCROSS_DISCORD_DESCRIPTION - - Joomla\Plugin\MokoJoomCross${CLASS_NAME} - - - discord.php - src - services - language - - - - language/en-GB/plg_mokojoomcross_discord.ini - language/en-GB/plg_mokojoomcross_discord.sys.ini - - diff --git a/src/packages/plg_mokojoomcross_discord/language/en-GB/plg_mokojoomcross_discord.ini b/src/packages/plg_mokojoomcross_discord/language/en-GB/plg_mokojoomcross_discord.ini deleted file mode 100644 index 10a174f..0000000 --- a/src/packages/plg_mokojoomcross_discord/language/en-GB/plg_mokojoomcross_discord.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_MOKOJOOMCROSS_DISCORD="MokoJoomCross - Discord" -PLG_MOKOJOOMCROSS_DISCORD_DESCRIPTION="Cross-post Joomla articles to Discord." diff --git a/src/packages/plg_mokojoomcross_facebook/facebook.xml b/src/packages/plg_mokojoomcross_facebook/facebook.xml deleted file mode 100644 index 3586353..0000000 --- a/src/packages/plg_mokojoomcross_facebook/facebook.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - MokoJoomCross - Facebook / Meta - 01.01.00 - 2026-05-28 - Moko Consulting - hello@mokoconsulting.tech - https://mokoconsulting.tech - Copyright (C) 2026 Moko Consulting. All rights reserved. - GPL-3.0-or-later - PLG_MOKOJOOMCROSS_FACEBOOK_DESCRIPTION - - Joomla\Plugin\MokoJoomCross${CLASS_NAME} - - - facebook.php - src - services - language - - - - language/en-GB/plg_mokojoomcross_facebook.ini - language/en-GB/plg_mokojoomcross_facebook.sys.ini - - diff --git a/src/packages/plg_mokojoomcross_facebook/language/en-GB/plg_mokojoomcross_facebook.ini b/src/packages/plg_mokojoomcross_facebook/language/en-GB/plg_mokojoomcross_facebook.ini deleted file mode 100644 index 168e629..0000000 --- a/src/packages/plg_mokojoomcross_facebook/language/en-GB/plg_mokojoomcross_facebook.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_MOKOJOOMCROSS_FACEBOOK="MokoJoomCross - Facebook / Meta" -PLG_MOKOJOOMCROSS_FACEBOOK_DESCRIPTION="Cross-post Joomla articles to Facebook / Meta." diff --git a/src/packages/plg_mokojoomcross_linkedin/language/en-GB/plg_mokojoomcross_linkedin.ini b/src/packages/plg_mokojoomcross_linkedin/language/en-GB/plg_mokojoomcross_linkedin.ini deleted file mode 100644 index f7793bb..0000000 --- a/src/packages/plg_mokojoomcross_linkedin/language/en-GB/plg_mokojoomcross_linkedin.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_MOKOJOOMCROSS_LINKEDIN="MokoJoomCross - LinkedIn" -PLG_MOKOJOOMCROSS_LINKEDIN_DESCRIPTION="Cross-post Joomla articles to LinkedIn." diff --git a/src/packages/plg_mokojoomcross_linkedin/linkedin.xml b/src/packages/plg_mokojoomcross_linkedin/linkedin.xml deleted file mode 100644 index 60c9b35..0000000 --- a/src/packages/plg_mokojoomcross_linkedin/linkedin.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - MokoJoomCross - LinkedIn - 01.01.00 - 2026-05-28 - Moko Consulting - hello@mokoconsulting.tech - https://mokoconsulting.tech - Copyright (C) 2026 Moko Consulting. All rights reserved. - GPL-3.0-or-later - PLG_MOKOJOOMCROSS_LINKEDIN_DESCRIPTION - - Joomla\Plugin\MokoJoomCross${CLASS_NAME} - - - linkedin.php - src - services - language - - - - language/en-GB/plg_mokojoomcross_linkedin.ini - language/en-GB/plg_mokojoomcross_linkedin.sys.ini - - diff --git a/src/packages/plg_mokojoomcross_mailchimp/language/en-GB/plg_mokojoomcross_mailchimp.ini b/src/packages/plg_mokojoomcross_mailchimp/language/en-GB/plg_mokojoomcross_mailchimp.ini deleted file mode 100644 index 6b08b10..0000000 --- a/src/packages/plg_mokojoomcross_mailchimp/language/en-GB/plg_mokojoomcross_mailchimp.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_MOKOJOOMCROSS_MAILCHIMP="MokoJoomCross - Mailchimp" -PLG_MOKOJOOMCROSS_MAILCHIMP_DESCRIPTION="Cross-post Joomla articles to Mailchimp." diff --git a/src/packages/plg_mokojoomcross_mailchimp/mailchimp.xml b/src/packages/plg_mokojoomcross_mailchimp/mailchimp.xml deleted file mode 100644 index 6e90b36..0000000 --- a/src/packages/plg_mokojoomcross_mailchimp/mailchimp.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - MokoJoomCross - Mailchimp - 01.01.00 - 2026-05-28 - Moko Consulting - hello@mokoconsulting.tech - https://mokoconsulting.tech - Copyright (C) 2026 Moko Consulting. All rights reserved. - GPL-3.0-or-later - PLG_MOKOJOOMCROSS_MAILCHIMP_DESCRIPTION - - Joomla\Plugin\MokoJoomCross${CLASS_NAME} - - - mailchimp.php - src - services - language - - - - language/en-GB/plg_mokojoomcross_mailchimp.ini - language/en-GB/plg_mokojoomcross_mailchimp.sys.ini - - diff --git a/src/packages/plg_mokojoomcross_mastodon/language/en-GB/plg_mokojoomcross_mastodon.ini b/src/packages/plg_mokojoomcross_mastodon/language/en-GB/plg_mokojoomcross_mastodon.ini deleted file mode 100644 index b663b97..0000000 --- a/src/packages/plg_mokojoomcross_mastodon/language/en-GB/plg_mokojoomcross_mastodon.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_MOKOJOOMCROSS_MASTODON="MokoJoomCross - Mastodon" -PLG_MOKOJOOMCROSS_MASTODON_DESCRIPTION="Cross-post Joomla articles to Mastodon." diff --git a/src/packages/plg_mokojoomcross_mastodon/mastodon.xml b/src/packages/plg_mokojoomcross_mastodon/mastodon.xml deleted file mode 100644 index 3483e0f..0000000 --- a/src/packages/plg_mokojoomcross_mastodon/mastodon.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - MokoJoomCross - Mastodon - 01.01.00 - 2026-05-28 - Moko Consulting - hello@mokoconsulting.tech - https://mokoconsulting.tech - Copyright (C) 2026 Moko Consulting. All rights reserved. - GPL-3.0-or-later - PLG_MOKOJOOMCROSS_MASTODON_DESCRIPTION - - Joomla\Plugin\MokoJoomCross${CLASS_NAME} - - - mastodon.php - src - services - language - - - - language/en-GB/plg_mokojoomcross_mastodon.ini - language/en-GB/plg_mokojoomcross_mastodon.sys.ini - - diff --git a/src/packages/plg_mokojoomcross_slack/language/en-GB/plg_mokojoomcross_slack.ini b/src/packages/plg_mokojoomcross_slack/language/en-GB/plg_mokojoomcross_slack.ini deleted file mode 100644 index e6dbc44..0000000 --- a/src/packages/plg_mokojoomcross_slack/language/en-GB/plg_mokojoomcross_slack.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_MOKOJOOMCROSS_SLACK="MokoJoomCross - Slack" -PLG_MOKOJOOMCROSS_SLACK_DESCRIPTION="Cross-post Joomla articles to Slack." diff --git a/src/packages/plg_mokojoomcross_telegram/language/en-GB/plg_mokojoomcross_telegram.ini b/src/packages/plg_mokojoomcross_telegram/language/en-GB/plg_mokojoomcross_telegram.ini deleted file mode 100644 index 2b2467f..0000000 --- a/src/packages/plg_mokojoomcross_telegram/language/en-GB/plg_mokojoomcross_telegram.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_MOKOJOOMCROSS_TELEGRAM="MokoJoomCross - Telegram" -PLG_MOKOJOOMCROSS_TELEGRAM_DESCRIPTION="Cross-post Joomla articles to Telegram." diff --git a/src/packages/plg_mokojoomcross_twitter/language/en-GB/plg_mokojoomcross_twitter.ini b/src/packages/plg_mokojoomcross_twitter/language/en-GB/plg_mokojoomcross_twitter.ini deleted file mode 100644 index c93d18d..0000000 --- a/src/packages/plg_mokojoomcross_twitter/language/en-GB/plg_mokojoomcross_twitter.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_MOKOJOOMCROSS_TWITTER="MokoJoomCross - X / Twitter" -PLG_MOKOJOOMCROSS_TWITTER_DESCRIPTION="Cross-post Joomla articles to X / Twitter." diff --git a/src/packages/plg_mokojoomcross_twitter/language/en-GB/plg_mokojoomcross_twitter.sys.ini b/src/packages/plg_mokojoomcross_twitter/language/en-GB/plg_mokojoomcross_twitter.sys.ini deleted file mode 100644 index c93d18d..0000000 --- a/src/packages/plg_mokojoomcross_twitter/language/en-GB/plg_mokojoomcross_twitter.sys.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_MOKOJOOMCROSS_TWITTER="MokoJoomCross - X / Twitter" -PLG_MOKOJOOMCROSS_TWITTER_DESCRIPTION="Cross-post Joomla articles to X / Twitter." diff --git a/src/packages/plg_system_mokojoomcross/language/en-GB/plg_system_mokojoomcross.ini b/src/packages/plg_system_mokojoomcross/language/en-GB/plg_system_mokojoomcross.ini deleted file mode 100644 index ecfce52..0000000 --- a/src/packages/plg_system_mokojoomcross/language/en-GB/plg_system_mokojoomcross.ini +++ /dev/null @@ -1,6 +0,0 @@ -; System - MokoJoomCross Language File -; Copyright (C) 2026 Moko Consulting. All rights reserved. -; License: GPL-3.0-or-later - -PLG_SYSTEM_MOKOJOOMCROSS="System - MokoJoomCross" -PLG_SYSTEM_MOKOJOOMCROSS_DESCRIPTION="Triggers cross-posting to connected services when Joomla articles are published." diff --git a/src/packages/plg_system_mokojoomcross/language/en-GB/plg_system_mokojoomcross.sys.ini b/src/packages/plg_system_mokojoomcross/language/en-GB/plg_system_mokojoomcross.sys.ini deleted file mode 100644 index 92791fe..0000000 --- a/src/packages/plg_system_mokojoomcross/language/en-GB/plg_system_mokojoomcross.sys.ini +++ /dev/null @@ -1,6 +0,0 @@ -; System - MokoJoomCross System Language File -; Copyright (C) 2026 Moko Consulting. All rights reserved. -; License: GPL-3.0-or-later - -PLG_SYSTEM_MOKOJOOMCROSS="System - MokoJoomCross" -PLG_SYSTEM_MOKOJOOMCROSS_DESCRIPTION="Triggers cross-posting to connected services when Joomla articles are published." diff --git a/src/packages/plg_webservices_mokojoomcross/language/en-GB/plg_webservices_mokojoomcross.ini b/src/packages/plg_webservices_mokojoomcross/language/en-GB/plg_webservices_mokojoomcross.ini deleted file mode 100644 index 4488621..0000000 --- a/src/packages/plg_webservices_mokojoomcross/language/en-GB/plg_webservices_mokojoomcross.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_WEBSERVICES_MOKOJOOMCROSS="Web Services - MokoJoomCross" -PLG_WEBSERVICES_MOKOJOOMCROSS_DESCRIPTION="Provides REST API endpoints for MokoJoomCross posts and services." diff --git a/src/packages/plg_webservices_mokojoomcross/language/en-GB/plg_webservices_mokojoomcross.sys.ini b/src/packages/plg_webservices_mokojoomcross/language/en-GB/plg_webservices_mokojoomcross.sys.ini deleted file mode 100644 index 4488621..0000000 --- a/src/packages/plg_webservices_mokojoomcross/language/en-GB/plg_webservices_mokojoomcross.sys.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_WEBSERVICES_MOKOJOOMCROSS="Web Services - MokoJoomCross" -PLG_WEBSERVICES_MOKOJOOMCROSS_DESCRIPTION="Provides REST API endpoints for MokoJoomCross posts and services." diff --git a/wiki/Adding-Custom-Services.md b/wiki/Adding-Custom-Services.md index 81defc7..6a3dc22 100644 --- a/wiki/Adding-Custom-Services.md +++ b/wiki/Adding-Custom-Services.md @@ -1,45 +1,45 @@ # Adding Custom Services -MokoJoomCross uses a plugin-based architecture. Any developer can create a new service plugin. +MokoSuiteCross uses a plugin-based architecture. Any developer can create a new service plugin. ## Plugin Structure -Create a Joomla plugin in the `mokojoomcross` group: +Create a Joomla plugin in the `mokosuitecross` group: ``` -plg_mokojoomcross_myservice/ -├── myservice.xml # Plugin manifest (group="mokojoomcross") +plg_mokosuitecross_myservice/ +├── myservice.xml # Plugin manifest (group="mokosuitecross") ├── myservice.php # Legacy stub (empty) ├── src/ │ └── Extension/ -│ └── MyserviceService.php # Implements MokoJoomCrossServiceInterface +│ └── MyserviceService.php # Implements MokoSuiteCrossServiceInterface ├── services/ │ └── provider.php # DI container registration └── language/ └── en-GB/ - ├── plg_mokojoomcross_myservice.ini - └── plg_mokojoomcross_myservice.sys.ini + ├── plg_mokosuitecross_myservice.ini + └── plg_mokosuitecross_myservice.sys.ini ``` ## Implement the Interface -Your Extension class must implement `MokoJoomCrossServiceInterface`: +Your Extension class must implement `MokoSuiteCrossServiceInterface`: ```php -namespace Joomla\Plugin\MokoJoomCross\Myservice\Extension; +namespace Joomla\Plugin\MokoSuiteCross\Myservice\Extension; use Joomla\CMS\Plugin\CMSPlugin; -use Joomla\Component\MokoJoomCross\Administrator\Service\MokoJoomCrossServiceInterface; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Event\SubscriberInterface; -class MyserviceService extends CMSPlugin implements SubscriberInterface, MokoJoomCrossServiceInterface +class MyserviceService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface { public static function getSubscribedEvents(): array { - return ['onMokoJoomCrossGetServices' => 'onMokoJoomCrossGetServices']; + return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices']; } - public function onMokoJoomCrossGetServices(&$services): void + public function onMokoSuiteCrossGetServices(&$services): void { $services[] = $this; } diff --git a/wiki/Configuration.md b/wiki/Configuration.md index e9730b5..5b3ef1a 100644 --- a/wiki/Configuration.md +++ b/wiki/Configuration.md @@ -1,6 +1,6 @@ # Configuration -Navigate to **Components → MokoJoomCross** to access the admin panel. +Navigate to **Components → MokoSuiteCross** to access the admin panel. ## Global Settings diff --git a/wiki/Developer-Guide.md b/wiki/Developer-Guide.md new file mode 100644 index 0000000..d449a35 --- /dev/null +++ b/wiki/Developer-Guide.md @@ -0,0 +1,337 @@ +# Developer Guide + +This guide covers building new service plugins for MokoSuiteCross — from directory structure through testing. + +## Plugin Directory Structure + +Each service plugin lives in its own package under `source/packages/`: + +``` +plg_mokosuitecross_myservice/ +├── myservice.xml ← Joomla manifest (type="plugin", group="mokosuitecross") +├── myservice.php ← Legacy loader stub (empty, required by Joomla) +├── services/ +│ └── provider.php ← DI container: registers the Extension class +└── src/ + └── Extension/ + └── MyServiceService.php ← Main class: implements the interface +``` + +## MokoSuiteCrossServiceInterface + +Every service plugin **must** implement `MokoSuiteCrossServiceInterface`. The interface defines 5 methods: + +```php +namespace Joomla\Component\MokoSuiteCross\Administrator\Service; + +interface MokoSuiteCrossServiceInterface +{ + /** + * Unique identifier matching the service_type in service.xml. + * Must match exactly (e.g. 'mastodon', 'telegram'). + */ + public function getServiceType(): string; + + /** + * Human-readable display name (e.g. 'Mastodon', 'Telegram'). + */ + public function getServiceName(): string; + + /** + * Post content to the platform. + * + * @param string $message Rendered message text (already template-processed) + * @param array $media Array of media file paths (images) + * @param array $credentials Decrypted credential key-value pairs from the service record + * @param array $params Plugin params + service params merged + * @return array ['success' => bool, 'platform_post_id' => string, 'response' => array] + */ + public function publish(string $message, array $media, array $credentials, array $params): array; + + /** + * Test whether the stored credentials are valid. + * + * @param array $credentials Decrypted credential key-value pairs + * @return array ['valid' => bool, 'message' => string, 'account_name' => string] + */ + public function validateCredentials(array $credentials): array; + + /** + * Platform character limit (0 = unlimited). + */ + public function getMaxLength(): int; + + /** + * Whether this service supports image/media attachments. + */ + public function supportsMedia(): bool; +} +``` + +## Step-by-Step: Creating a New Service Plugin + +### 1. Create the manifest (`myservice.xml`) + +```xml + + + plg_mokosuitecross_myservice + Moko Consulting + 1.0.0 + MyService integration for MokoSuiteCross + Joomla\Plugin\MokoSuiteCross\MyService + + myservice.php + services + src + + + + +
+ +
+
+
+
+``` + +### 2. Create the legacy stub (`myservice.php`) + +```php +set( + PluginInterface::class, + function (Container $container) { + $dispatcher = $container->get(DispatcherInterface::class); + $plugin = new MyServiceService($dispatcher, (array) PluginHelper::getPlugin('mokosuitecross', 'myservice')); + $plugin->setApplication(Factory::getApplication()); + return $plugin; + } + ); + } +}; +``` + +### 4. Create the Extension class + +```php + 'onMokoSuiteCrossGetServices', + ]; + } + + public function onMokoSuiteCrossGetServices(&$services): void + { + $services[] = $this; + } + + public function getServiceType(): string + { + return 'myservice'; + } + + public function getServiceName(): string + { + return 'My Service'; + } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + // Your API integration here + // $credentials contains the decrypted values from service.xml fields + // e.g. $credentials['api_key'], $credentials['webhook_url'] + + return [ + 'success' => true, + 'platform_post_id' => 'abc123', + 'response' => ['status' => 'ok'], + ]; + } + + public function validateCredentials(array $credentials): array + { + // Test the credentials against the platform API + return [ + 'valid' => true, + 'message' => 'Connected', + 'account_name' => 'MyAccount', + ]; + } + + public function getMaxLength(): int + { + return 0; // 0 = no limit + } + + public function supportsMedia(): bool + { + return false; + } +} +``` + +### 5. Add credential fields to `service.xml` + +In `source/packages/com_mokosuitecross/forms/service.xml`, add your fields with `showon`: + +```xml + + +``` + +### 6. Add language strings to `com_mokosuitecross.ini` + +```ini +COM_MOKOSUITECROSS_CRED_MYSERVICE_KEY="API Key" +``` + +### 7. Add to the service_type dropdown (if not already listed) + +In the `` list in `service.xml`, add: + +```xml + +``` + +## How `showon` Credential Fields Work + +Joomla's `showon` attribute controls field visibility client-side via JavaScript: + +| Pattern | Meaning | +|---------|---------| +| `showon="service_type:telegram"` | Show when service type is Telegram | +| `showon="service_type:telegram[AND]cred_mode:custom"` | Show when Telegram AND custom mode | +| `showon="service_type:webhook[AND]cred_webhook_auth_type:bearer,basic"` | Show when webhook AND auth is bearer or basic | + +Fields are hidden/shown without page reloads. The form data for hidden fields is still submitted but ignored by the component. + +## Dispatch Pipeline + +The cross-posting flow works like this: + +1. **Article published** → System plugin (`plg_system_mokosuitecross`) catches `onContentAfterSave` +2. **Queue creation** → For each enabled service, a `#__mokosuitecross_posts` row is created with status `queued` +3. **Queue processing** → Either the Scheduled Task or page-load fallback picks up queued posts +4. **Service dispatch** → `QueueProcessor` fires `onMokoSuiteCrossGetServices` event in the `mokosuitecross` plugin group +5. **Plugin response** → Each registered service plugin adds itself to the `$services` array +6. **Matching** → The processor finds the plugin whose `getServiceType()` matches the service record's `service_type` +7. **Publishing** → `publish()` is called with the rendered message, media paths, decrypted credentials, and params +8. **Result** → The post record is updated with `posted`/`failed` status and the platform response + +## Default Bot Mode + +Some services (Telegram, Discord, Slack, Teams, Facebook, Threads) support a **default mode** where pre-configured MokoWaaS credentials are used. This is controlled by: + +1. The `cred_mode` field in `service.xml` (shown for services listed in its `showon`) +2. Plugin-level params in the plugin manifest (`` section) that store default tokens +3. The service plugin's `publish()` method checks `$credentials['mode']`: + - `'default'` → use plugin params (`$this->params->get('default_token')`) + - `'custom'` → use the per-service credentials from `$credentials` + +## OAuth Integration + +For services requiring OAuth (Facebook, LinkedIn, Twitter, Pinterest, etc.): + +1. **OAuthHelper** (`source/packages/com_mokosuitecross/src/Helper/OAuthHelper.php`) handles: + - Authorization URL generation with state parameter + - Code-to-token exchange + - Token storage back to the service record's credentials + +2. **OauthController** provides two endpoints: + - `task=oauth.authorize` → redirects to the platform's auth page + - `task=oauth.callback` → handles the redirect, exchanges code for token + +3. Plugin params store the OAuth Client ID and Secret (set in Extensions → Plugins) + +4. In `edit.php`, services listed in `$oauthServices` get a "Connect to {Service}" button + +## Testing Your Plugin + +1. **Syntax check**: `php -l source/packages/plg_mokosuitecross_myservice/src/Extension/MyServiceService.php` +2. **Install**: Include the plugin in `pkg_mokosuitecross.xml` or install the plugin ZIP standalone +3. **Enable**: Extensions → Plugins → search "mokosuitecross myservice" → Enable +4. **Add service**: Components → MokoSuiteCross → Services → New → select your service type +5. **Verify fields**: Confirm your credential fields appear when your service type is selected +6. **Test post**: Publish an article and check the Post Queue for results + +## Example: Building a "Fediverse" Service + +Imagine building a service for a Mastodon-compatible platform: + +```php +public function publish(string $message, array $media, array $credentials, array $params): array +{ + $instanceUrl = rtrim($credentials['instance_url'] ?? '', '/'); + $token = $credentials['access_token'] ?? ''; + + $ch = curl_init($instanceUrl . '/api/v1/statuses'); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode(['status' => $message]), + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $token, + ], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $data = json_decode($response, true) ?: []; + + if ($httpCode === 200 && !empty($data['id'])) { + return ['success' => true, 'platform_post_id' => $data['id'], 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; +} +``` + +This pattern — curl to API, check response code, return structured result — is the same for every service plugin. The only differences are the API endpoint, authentication method, and payload format. diff --git a/wiki/Home.md b/wiki/Home.md index fa66416..80eb53c 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -1,11 +1,11 @@ -# MokoJoomCross Wiki +# MokoSuiteCross Wiki -**MokoJoomCross** — Cross-posting Joomla content to social media, email marketing, and chat platforms. +**MokoSuiteCross** — Cross-posting Joomla content to social media, email marketing, and chat platforms. ## Quick Start -1. Install `pkg_mokojoomcross-*.zip` via Joomla Extensions → Install -2. Navigate to **Components → MokoJoomCross → Services** +1. Install `pkg_mokosuitecross-*.zip` via Joomla Extensions → Install +2. Navigate to **Components → MokoSuiteCross → Services** 3. Add your first service (e.g., Telegram, Discord, Facebook) 4. Publish an article — it's automatically cross-posted to all active services @@ -32,18 +32,18 @@ ## Architecture -MokoJoomCross uses a **plugin-based service architecture**. Each social platform is a separate Joomla plugin in the custom `mokojoomcross` plugin group. This means: +MokoSuiteCross uses a **plugin-based service architecture**. Each social platform is a separate Joomla plugin in the custom `mokosuitecross` plugin group. This means: - Install only the platforms you need - Third-party developers can add new platforms as plugins -- Each service plugin implements `MokoJoomCrossServiceInterface` +- Each service plugin implements `MokoSuiteCrossServiceInterface` - Services support both **default bot/app** mode (pre-configured by Moko) and **custom** mode (bring your own API keys) ## Database Tables | Table | Purpose | |-------|---------| -| `#__mokojoomcross_services` | Connected service accounts | -| `#__mokojoomcross_posts` | Cross-post queue and history | -| `#__mokojoomcross_templates` | Per-platform message templates | -| `#__mokojoomcross_logs` | Activity and error logs | +| `#__mokosuitecross_services` | Connected service accounts | +| `#__mokosuitecross_posts` | Cross-post queue and history | +| `#__mokosuitecross_templates` | Per-platform message templates | +| `#__mokosuitecross_logs` | Activity and error logs | diff --git a/wiki/Installation.md b/wiki/Installation.md index bb4ee4c..9085192 100644 --- a/wiki/Installation.md +++ b/wiki/Installation.md @@ -8,7 +8,7 @@ ## Install -1. Download the latest `pkg_mokojoomcross-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomCross/releases) +1. Download the latest `pkg_mokosuitecross-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/releases) 2. In Joomla Administrator → **Extensions → Install → Upload Package File** 3. Upload and install the package @@ -16,18 +16,18 @@ The installer automatically: - Enables the system plugin (triggers cross-posting on article publish) - Enables the content plugin (shows cross-post status badges in admin) - Enables the webservices plugin (REST API) -- Enables all service plugins in the `mokojoomcross` group +- Enables all service plugins in the `mokosuitecross` group ## Migrating from Perfect Publisher Pro -If Perfect Publisher Pro is installed, MokoJoomCross detects it and offers one-click migration: +If Perfect Publisher Pro is installed, MokoSuiteCross detects it and offers one-click migration: -1. Install MokoJoomCross (PP Pro can remain installed) -2. Navigate to **Components → MokoJoomCross → Dashboard** +1. Install MokoSuiteCross (PP Pro can remain installed) +2. Navigate to **Components → MokoSuiteCross → Dashboard** 3. Click **"Migrate from Perfect Publisher Pro"** 4. Review detected services and confirm import 5. Imported services are created in **disabled** state — verify credentials before enabling ## Uninstall -Uninstalling the package removes all MokoJoomCross database tables and data. Export any needed data before uninstalling. +Uninstalling the package removes all MokoSuiteCross database tables and data. Export any needed data before uninstalling. diff --git a/wiki/Message-Templates.md b/wiki/Message-Templates.md new file mode 100644 index 0000000..2913d2a --- /dev/null +++ b/wiki/Message-Templates.md @@ -0,0 +1,77 @@ +# Message Templates + +MokoSuiteCross uses message templates to format the content sent to each platform. Templates support placeholders that are replaced with article data at post time. + +## Managing Templates + +Navigate to **Components → MokoSuiteCross → Templates** to create and edit templates. + +## Template Priority + +When cross-posting, the system looks for templates in this order: +1. **Platform-specific template** — matches the service type exactly (e.g., "twitter") +2. **Default template** — fallback used when no platform-specific template exists + +## Available Placeholders + +| Placeholder | Description | Example | +|-------------|-------------|---------| +| `{title}` | Article title | "New Product Launch" | +| `{url}` | Full article URL | "https://example.com/article/123" | +| `{introtext}` | Intro text (280 chars, HTML stripped) | "We're excited to announce..." | +| `{fulltext}` | Full text (500 chars, HTML stripped) | Extended content | +| `{image}` | Intro image full URL | "https://example.com/images/photo.jpg" | +| `{category}` | Article category name | "News" | +| `{author}` | Author display name | "John Smith" | +| `{date}` | Publish date (YYYY-MM-DD) | "2026-05-28" | + +## Example Templates + +### Default (all platforms) +``` +{title} + +{introtext} + +{url} +``` + +### Twitter / X (280 char limit) +``` +{title} + +{url} +``` + +### Mastodon (with hashtags) +``` +{title} + +{introtext} + +{url} + +#Joomla #{category} +``` + +### Mailchimp (HTML email) +```html +

{title}

+

{introtext}

+

Read the full article

+``` + +### Telegram (HTML format) +```html +{title} + +{introtext} + +Read more +``` + +## Per-Article Override + +In the article editor, the **Cross-Posting** tab lets you: +- Skip cross-posting entirely for a specific article +- Select which services to post to (instead of all enabled services) diff --git a/wiki/REST-API.md b/wiki/REST-API.md new file mode 100644 index 0000000..6059b28 --- /dev/null +++ b/wiki/REST-API.md @@ -0,0 +1,57 @@ +# REST API + +MokoSuiteCross includes a WebServices plugin that provides REST API endpoints via Joomla's API application. + +## Authentication + +All endpoints require a Joomla API token. Generate one in **Users → Manage → [User] → API Tokens**. + +Include the token in the `Authorization` header: + +``` +Authorization: Bearer YOUR_API_TOKEN +``` + +## Base URL + +``` +https://yoursite.com/api/index.php/v1/mokosuitecross/ +``` + +## Endpoints + +### Posts + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/v1/mokosuitecross/posts` | List all cross-posts | +| GET | `/v1/mokosuitecross/posts/:id` | Get single post details | +| POST | `/v1/mokosuitecross/posts` | Create a cross-post entry | +| DELETE | `/v1/mokosuitecross/posts/:id` | Delete a post | + +### Services + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/v1/mokosuitecross/services` | List connected services | +| GET | `/v1/mokosuitecross/services/:id` | Get service details | + +## Example + +```bash +# List all posts +curl -H "Authorization: Bearer YOUR_TOKEN" \ + https://yoursite.com/api/index.php/v1/mokosuitecross/posts + +# List services +curl -H "Authorization: Bearer YOUR_TOKEN" \ + https://yoursite.com/api/index.php/v1/mokosuitecross/services +``` + +## Filtering + +Posts support query parameters: +- `filter[status]=posted` — Filter by status (queued, posting, posted, failed, scheduled) +- `filter[service_id]=5` — Filter by service +- `page[limit]=20` — Pagination limit +- `page[offset]=0` — Pagination offset diff --git a/wiki/Services.md b/wiki/Services.md new file mode 100644 index 0000000..3d72627 --- /dev/null +++ b/wiki/Services.md @@ -0,0 +1,60 @@ +# Services + +MokoSuiteCross supports 9 platforms. Each is a separate plugin that can be enabled or disabled independently. + +## Social Media + +| Platform | Plugin | Character Limit | Media | Default Bot | +|----------|--------|----------------|-------|-------------| +| **Facebook** | plg_mokosuitecross_facebook | No limit | Yes | Yes | +| **X / Twitter** | plg_mokosuitecross_twitter | 280 | Yes | No | +| **LinkedIn** | plg_mokosuitecross_linkedin | 3,000 | Yes | No | +| **Mastodon** | plg_mokosuitecross_mastodon | 500 | Yes | No | +| **Bluesky** | plg_mokosuitecross_bluesky | 300 | Yes | No | + +## Email Marketing + +| Platform | Plugin | Character Limit | Media | Default Bot | +|----------|--------|----------------|-------|-------------| +| **Mailchimp** | plg_mokosuitecross_mailchimp | No limit | Yes | No | + +## Chat / Messaging + +| Platform | Plugin | Character Limit | Media | Default Bot | +|----------|--------|----------------|-------|-------------| +| **Telegram** | plg_mokosuitecross_telegram | 4,096 | Yes | Yes (@MokoWaaSBot) | +| **Discord** | plg_mokosuitecross_discord | 2,000 | Yes | Yes (webhook) | +| **Slack** | plg_mokosuitecross_slack | 40,000 | Yes | Yes (webhook) | + +## Default vs Custom Mode + +Services with "Default Bot" support offer two operating modes: + +- **Default Mode**: Uses a pre-configured bot/app token managed by Moko. The admin only needs to provide a destination (chat ID, page ID, etc.). The API key is stored in the plugin's configuration and never visible in the service record. + +- **Custom Mode**: The admin provides their own API keys, tokens, or webhook URLs. Full control, but requires setting up your own app/bot on the platform. + +Configure default tokens in **Extensions → Plugins → MokoSuiteCross - [Platform]**. + +## Adding a Service + +1. Go to **Components → MokoSuiteCross → Services** +2. Click **New** +3. Select the service type +4. Enter a title and choose credentials mode +5. For **Default mode**: enter only the destination (chat ID, channel, etc.) +6. For **Custom mode**: enter your full API credentials as JSON +7. Save and enable + +## Credentials Format + +Each service expects specific JSON fields. See the individual service pages: +- [[Telegram]] — bot_token, chat_id +- [[Facebook]] — page_access_token, page_id +- [[Discord]] — webhook_url +- [[Slack]] — webhook_url +- [[LinkedIn]] — access_token, organization_id +- [[Mastodon]] — instance_url, access_token +- [[Bluesky]] — handle, app_password +- [[Mailchimp]] — api_key, list_id +- [[Twitter (X)]] — bearer_token, api_key, api_secret diff --git a/wiki/Telegram.md b/wiki/Telegram.md index a11761c..09c19e0 100644 --- a/wiki/Telegram.md +++ b/wiki/Telegram.md @@ -6,7 +6,7 @@ Cross-post Joomla articles to Telegram channels, groups, or users. ### Default Bot (@MokoWaaSBot) -1. Add MokoJoomCross service with type **Telegram** +1. Add MokoSuiteCross service with type **Telegram** 2. Set mode to **Default** 3. Enter your **Chat ID** (channel, group, or user) 4. Add @MokoWaaSBot to your channel/group as admin @@ -14,7 +14,7 @@ Cross-post Joomla articles to Telegram channels, groups, or users. ### Custom Bot 1. Create a bot via [@BotFather](https://t.me/BotFather) -2. Add MokoJoomCross service with type **Telegram** +2. Add MokoSuiteCross service with type **Telegram** 3. Set mode to **Custom** 4. Enter your Bot Token and Chat ID 5. Add your bot to the target channel/group as admin diff --git a/wiki/Troubleshooting.md b/wiki/Troubleshooting.md new file mode 100644 index 0000000..352eb25 --- /dev/null +++ b/wiki/Troubleshooting.md @@ -0,0 +1,48 @@ +# Troubleshooting + +## Posts Stuck in "Queued" Status + +**Cause**: The queue processor isn't running. + +**Fix**: +1. Check **Components → MokoSuiteCross → Options → Queue Processing** — ensure it's set to "Scheduler" or "Both" +2. If using Scheduler: verify a task exists in **System → Scheduled Tasks** of type "MokoSuiteCross - Process Queue" +3. If using Page-load: ensure the system plugin is enabled and check the throttle interval + +## Posts Failing + +**Cause**: Invalid credentials or platform API changes. + +**Fix**: +1. Check the error message in **Components → MokoSuiteCross → Post Queue** (hover over the red "Failed" badge) +2. Check **Activity Logs** for detailed error messages +3. Go to **Services** and verify credentials +4. For services using Default mode, check the plugin params in **Extensions → Plugins** + +## "No service plugin found" Warning + +**Cause**: The service plugin for that platform is disabled. + +**Fix**: Go to **Extensions → Plugins**, search for "MokoSuiteCross", and enable the relevant service plugin. + +## Cross-posting Not Triggering on Publish + +**Cause**: Auto-post is disabled or system plugin is inactive. + +**Fix**: +1. Check **Components → MokoSuiteCross → Options** — "Auto-post on Publish" should be "Yes" +2. Verify **Extensions → Plugins → System - MokoSuiteCross** is enabled +3. Check that at least one service is configured and enabled + +## Duplicate Posts + +MokoSuiteCross has a built-in duplicate guard. If you're seeing duplicates: +1. Check if the article was saved multiple times in quick succession +2. Check if both page-load and scheduler are running (shouldn't cause duplicates, but verify) +3. Review the **Activity Logs** for the article in question + +## OAuth Connection Failing + +1. Verify the OAuth Client ID and Secret are correct in the plugin params +2. Check that the redirect URI matches: `https://yoursite.com/administrator/index.php?option=com_mokosuitecross&task=oauth.callback` +3. Ensure your Joomla site uses HTTPS (required by most OAuth providers) -- 2.52.0 From 8ab62abf2930012cb87a84e0115bf581512f574b Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 21 Jun 2026 16:41:16 +0000 Subject: [PATCH 4/7] chore(version): auto-bump 01.01.02-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- .mokogitea/workflows/issue-branch.yml | 2 +- CHANGELOG.md | 2 +- README.md | 2 +- source/packages/com_mokosuitecross/mokosuitecross.xml | 2 +- source/packages/plg_content_mokosuitecross/mokosuitecross.xml | 2 +- source/packages/plg_mokosuitecross_activitypub/activitypub.xml | 2 +- source/packages/plg_mokosuitecross_blogger/blogger.xml | 2 +- source/packages/plg_mokosuitecross_bluesky/bluesky.xml | 2 +- source/packages/plg_mokosuitecross_brevo/brevo.xml | 2 +- .../plg_mokosuitecross_constantcontact/constantcontact.xml | 2 +- source/packages/plg_mokosuitecross_convertkit/convertkit.xml | 2 +- source/packages/plg_mokosuitecross_devto/devto.xml | 2 +- source/packages/plg_mokosuitecross_discord/discord.xml | 2 +- source/packages/plg_mokosuitecross_facebook/facebook.xml | 2 +- source/packages/plg_mokosuitecross_ghost/ghost.xml | 2 +- .../plg_mokosuitecross_googlebusiness/googlebusiness.xml | 2 +- source/packages/plg_mokosuitecross_googlechat/googlechat.xml | 2 +- source/packages/plg_mokosuitecross_hashnode/hashnode.xml | 2 +- source/packages/plg_mokosuitecross_linkedin/linkedin.xml | 2 +- source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml | 2 +- source/packages/plg_mokosuitecross_mastodon/mastodon.xml | 2 +- source/packages/plg_mokosuitecross_matrix/matrix.xml | 2 +- source/packages/plg_mokosuitecross_medium/medium.xml | 2 +- .../plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml | 2 +- .../plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml | 2 +- source/packages/plg_mokosuitecross_nostr/nostr.xml | 2 +- source/packages/plg_mokosuitecross_ntfy/ntfy.xml | 2 +- source/packages/plg_mokosuitecross_pinterest/pinterest.xml | 2 +- source/packages/plg_mokosuitecross_reddit/reddit.xml | 2 +- source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml | 2 +- source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml | 2 +- source/packages/plg_mokosuitecross_slack/slack.xml | 2 +- source/packages/plg_mokosuitecross_teams/teams.xml | 2 +- source/packages/plg_mokosuitecross_telegram/telegram.xml | 2 +- source/packages/plg_mokosuitecross_threads/threads.xml | 2 +- source/packages/plg_mokosuitecross_tiktok/tiktok.xml | 2 +- source/packages/plg_mokosuitecross_tumblr/tumblr.xml | 2 +- source/packages/plg_mokosuitecross_twitter/twitter.xml | 2 +- source/packages/plg_mokosuitecross_webhook/webhook.xml | 2 +- source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml | 2 +- source/packages/plg_mokosuitecross_wordpress/wordpress.xml | 2 +- source/packages/plg_system_mokosuitecross/mokosuitecross.xml | 2 +- .../plg_system_mokosuitecross_events/mokosuitecross_events.xml | 2 +- .../mokosuitecross_gallery.xml | 2 +- source/packages/plg_task_mokosuitecross/mokosuitecross.xml | 2 +- .../packages/plg_webservices_mokosuitecross/mokosuitecross.xml | 2 +- source/pkg_mokosuitecross.xml | 2 +- 48 files changed, 48 insertions(+), 48 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 74c6081..d6dfa36 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -5,7 +5,7 @@ Package - MokoSuiteCross MokoConsulting Cross-posting Joomla content to social media, email marketing, and chat platforms - 01.00.27 + 01.01.02 GNU General Public License v3 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 9e1538b..3451c7f 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokoplatform.Automation -# VERSION: 01.00.27 +# VERSION: 01.01.02 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CHANGELOG.md b/CHANGELOG.md index e490374..45f6db1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## [Unreleased] - + All notable changes to MokoSuiteCross will be documented in this file. diff --git a/README.md b/README.md index d95d1b0..f154263 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoSuiteCross - + Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 5/6. diff --git a/source/packages/com_mokosuitecross/mokosuitecross.xml b/source/packages/com_mokosuitecross/mokosuitecross.xml index a74d62a..07b9c33 100644 --- a/source/packages/com_mokosuitecross/mokosuitecross.xml +++ b/source/packages/com_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ com_mokosuitecross - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_content_mokosuitecross/mokosuitecross.xml b/source/packages/plg_content_mokosuitecross/mokosuitecross.xml index 20fb72b..5774463 100644 --- a/source/packages/plg_content_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_content_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Content - MokoSuiteCross - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_activitypub/activitypub.xml b/source/packages/plg_mokosuitecross_activitypub/activitypub.xml index 7a8a257..51f4768 100644 --- a/source/packages/plg_mokosuitecross_activitypub/activitypub.xml +++ b/source/packages/plg_mokosuitecross_activitypub/activitypub.xml @@ -1,7 +1,7 @@ MokoSuiteCross - ActivityPub (Fediverse) - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_blogger/blogger.xml b/source/packages/plg_mokosuitecross_blogger/blogger.xml index 42cf014..5446da9 100644 --- a/source/packages/plg_mokosuitecross_blogger/blogger.xml +++ b/source/packages/plg_mokosuitecross_blogger/blogger.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Blogger - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_bluesky/bluesky.xml b/source/packages/plg_mokosuitecross_bluesky/bluesky.xml index 1087eb8..94ce58d 100644 --- a/source/packages/plg_mokosuitecross_bluesky/bluesky.xml +++ b/source/packages/plg_mokosuitecross_bluesky/bluesky.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Bluesky - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_brevo/brevo.xml b/source/packages/plg_mokosuitecross_brevo/brevo.xml index 0cdf41b..f5f014f 100644 --- a/source/packages/plg_mokosuitecross_brevo/brevo.xml +++ b/source/packages/plg_mokosuitecross_brevo/brevo.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Brevo (Sendinblue) - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml index 963b5a9..f2b272c 100644 --- a/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml +++ b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Constant Contact - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_convertkit/convertkit.xml b/source/packages/plg_mokosuitecross_convertkit/convertkit.xml index 53a0f7f..20c274f 100644 --- a/source/packages/plg_mokosuitecross_convertkit/convertkit.xml +++ b/source/packages/plg_mokosuitecross_convertkit/convertkit.xml @@ -1,7 +1,7 @@ MokoSuiteCross - ConvertKit - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_devto/devto.xml b/source/packages/plg_mokosuitecross_devto/devto.xml index 8b47e2a..aea391d 100644 --- a/source/packages/plg_mokosuitecross_devto/devto.xml +++ b/source/packages/plg_mokosuitecross_devto/devto.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Dev.to - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_discord/discord.xml b/source/packages/plg_mokosuitecross_discord/discord.xml index f695366..60ac8ba 100644 --- a/source/packages/plg_mokosuitecross_discord/discord.xml +++ b/source/packages/plg_mokosuitecross_discord/discord.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Discord - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_facebook/facebook.xml b/source/packages/plg_mokosuitecross_facebook/facebook.xml index 4d8cb82..5af6075 100644 --- a/source/packages/plg_mokosuitecross_facebook/facebook.xml +++ b/source/packages/plg_mokosuitecross_facebook/facebook.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Facebook / Meta - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_ghost/ghost.xml b/source/packages/plg_mokosuitecross_ghost/ghost.xml index a7770c6..5af2673 100644 --- a/source/packages/plg_mokosuitecross_ghost/ghost.xml +++ b/source/packages/plg_mokosuitecross_ghost/ghost.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Ghost - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml b/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml index e43135a..a2812ad 100644 --- a/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml +++ b/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Business Profile - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_googlechat/googlechat.xml b/source/packages/plg_mokosuitecross_googlechat/googlechat.xml index a8fb758..ad83078 100644 --- a/source/packages/plg_mokosuitecross_googlechat/googlechat.xml +++ b/source/packages/plg_mokosuitecross_googlechat/googlechat.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Chat - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_hashnode/hashnode.xml b/source/packages/plg_mokosuitecross_hashnode/hashnode.xml index d012f90..c97d05b 100644 --- a/source/packages/plg_mokosuitecross_hashnode/hashnode.xml +++ b/source/packages/plg_mokosuitecross_hashnode/hashnode.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Hashnode - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_linkedin/linkedin.xml b/source/packages/plg_mokosuitecross_linkedin/linkedin.xml index 26e45b6..e8b8601 100644 --- a/source/packages/plg_mokosuitecross_linkedin/linkedin.xml +++ b/source/packages/plg_mokosuitecross_linkedin/linkedin.xml @@ -1,7 +1,7 @@ MokoSuiteCross - LinkedIn - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml index d347497..eea914d 100644 --- a/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml +++ b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Mailchimp - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mastodon/mastodon.xml b/source/packages/plg_mokosuitecross_mastodon/mastodon.xml index a305afc..3562d95 100644 --- a/source/packages/plg_mokosuitecross_mastodon/mastodon.xml +++ b/source/packages/plg_mokosuitecross_mastodon/mastodon.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Mastodon - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_matrix/matrix.xml b/source/packages/plg_mokosuitecross_matrix/matrix.xml index 5f96dfc..9f695ff 100644 --- a/source/packages/plg_mokosuitecross_matrix/matrix.xml +++ b/source/packages/plg_mokosuitecross_matrix/matrix.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Matrix / Element - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_medium/medium.xml b/source/packages/plg_mokosuitecross_medium/medium.xml index 747f431..ca6b2e0 100644 --- a/source/packages/plg_mokosuitecross_medium/medium.xml +++ b/source/packages/plg_mokosuitecross_medium/medium.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Medium - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml index eaa1e74..0ed6059 100644 --- a/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml +++ b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml @@ -1,7 +1,7 @@ MokoSuiteCross - MokoSuiteCalendar Events - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml index 7398c47..38f6832 100644 --- a/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml +++ b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml @@ -1,7 +1,7 @@ MokoSuiteCross - MokoSuiteGallery - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_nostr/nostr.xml b/source/packages/plg_mokosuitecross_nostr/nostr.xml index fd5c2ab..64fab7b 100644 --- a/source/packages/plg_mokosuitecross_nostr/nostr.xml +++ b/source/packages/plg_mokosuitecross_nostr/nostr.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Nostr - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_ntfy/ntfy.xml b/source/packages/plg_mokosuitecross_ntfy/ntfy.xml index cfe9bec..7899899 100644 --- a/source/packages/plg_mokosuitecross_ntfy/ntfy.xml +++ b/source/packages/plg_mokosuitecross_ntfy/ntfy.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Ntfy Push Notifications - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_pinterest/pinterest.xml b/source/packages/plg_mokosuitecross_pinterest/pinterest.xml index 8709ec0..9577975 100644 --- a/source/packages/plg_mokosuitecross_pinterest/pinterest.xml +++ b/source/packages/plg_mokosuitecross_pinterest/pinterest.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Pinterest - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_reddit/reddit.xml b/source/packages/plg_mokosuitecross_reddit/reddit.xml index d90fbaf..78c22ca 100644 --- a/source/packages/plg_mokosuitecross_reddit/reddit.xml +++ b/source/packages/plg_mokosuitecross_reddit/reddit.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Reddit - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml b/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml index 38d493c..31b42cc 100644 --- a/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml +++ b/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml @@ -1,7 +1,7 @@ MokoSuiteCross - RSS Feed - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml index 133c9d0..0ab5457 100644 --- a/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml +++ b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml @@ -1,7 +1,7 @@ MokoSuiteCross - SendGrid - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_slack/slack.xml b/source/packages/plg_mokosuitecross_slack/slack.xml index c95c412..0a8be09 100644 --- a/source/packages/plg_mokosuitecross_slack/slack.xml +++ b/source/packages/plg_mokosuitecross_slack/slack.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Slack - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_teams/teams.xml b/source/packages/plg_mokosuitecross_teams/teams.xml index 3938bb5..2c11a11 100644 --- a/source/packages/plg_mokosuitecross_teams/teams.xml +++ b/source/packages/plg_mokosuitecross_teams/teams.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Microsoft Teams - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_telegram/telegram.xml b/source/packages/plg_mokosuitecross_telegram/telegram.xml index 4d41c85..bba8351 100644 --- a/source/packages/plg_mokosuitecross_telegram/telegram.xml +++ b/source/packages/plg_mokosuitecross_telegram/telegram.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Telegram - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_threads/threads.xml b/source/packages/plg_mokosuitecross_threads/threads.xml index dc950cb..ee6fba3 100644 --- a/source/packages/plg_mokosuitecross_threads/threads.xml +++ b/source/packages/plg_mokosuitecross_threads/threads.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Threads (Meta) - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_tiktok/tiktok.xml b/source/packages/plg_mokosuitecross_tiktok/tiktok.xml index 5479523..abb9ea5 100644 --- a/source/packages/plg_mokosuitecross_tiktok/tiktok.xml +++ b/source/packages/plg_mokosuitecross_tiktok/tiktok.xml @@ -1,7 +1,7 @@ MokoSuiteCross - TikTok - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_tumblr/tumblr.xml b/source/packages/plg_mokosuitecross_tumblr/tumblr.xml index 81226dc..b12a66c 100644 --- a/source/packages/plg_mokosuitecross_tumblr/tumblr.xml +++ b/source/packages/plg_mokosuitecross_tumblr/tumblr.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Tumblr - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_twitter/twitter.xml b/source/packages/plg_mokosuitecross_twitter/twitter.xml index 9730db1..1004289 100644 --- a/source/packages/plg_mokosuitecross_twitter/twitter.xml +++ b/source/packages/plg_mokosuitecross_twitter/twitter.xml @@ -1,7 +1,7 @@ MokoSuiteCross - X / Twitter - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_webhook/webhook.xml b/source/packages/plg_mokosuitecross_webhook/webhook.xml index 985c700..d69b3c5 100644 --- a/source/packages/plg_mokosuitecross_webhook/webhook.xml +++ b/source/packages/plg_mokosuitecross_webhook/webhook.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Generic Webhook - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml b/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml index 4e7536c..95e6f47 100644 --- a/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml +++ b/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml @@ -1,7 +1,7 @@ MokoSuiteCross - WhatsApp Business - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_wordpress/wordpress.xml b/source/packages/plg_mokosuitecross_wordpress/wordpress.xml index 977dab3..d12aba9 100644 --- a/source/packages/plg_mokosuitecross_wordpress/wordpress.xml +++ b/source/packages/plg_mokosuitecross_wordpress/wordpress.xml @@ -1,7 +1,7 @@ MokoSuiteCross - WordPress - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross/mokosuitecross.xml b/source/packages/plg_system_mokosuitecross/mokosuitecross.xml index e7311a1..a3cac08 100644 --- a/source/packages/plg_system_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_system_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml b/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml index e38f450..347b05f 100644 --- a/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml +++ b/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross Events - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml b/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml index 112d8ab..32e0864 100644 --- a/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml +++ b/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross Gallery - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_task_mokosuitecross/mokosuitecross.xml b/source/packages/plg_task_mokosuitecross/mokosuitecross.xml index 5298491..7e0c2b4 100644 --- a/source/packages/plg_task_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_task_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Task - MokoSuiteCross Queue Processor - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml b/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml index 3f1ffb9..54dd823 100644 --- a/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Web Services - MokoSuiteCross - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/pkg_mokosuitecross.xml b/source/pkg_mokosuitecross.xml index 5b18093..cf9fd60 100644 --- a/source/pkg_mokosuitecross.xml +++ b/source/pkg_mokosuitecross.xml @@ -2,7 +2,7 @@ MokoSuiteCross mokosuitecross - 01.00.27-dev + 01.01.02-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech -- 2.52.0 From 122c7b630a2c432d98e8a1c5424fc7eb0c5676c2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 21 Jun 2026 12:00:37 -0500 Subject: [PATCH 5/7] feat: Telegram @mokosuite_bot default, wiki folders, README/CHANGELOG update - Telegram: updated default bot from @MokoWaaSBot to @mokosuite_bot - Telegram: embedded obfuscated bot token in plugin PHP (XOR + base64) - Telegram: added section to plugin XML for parse_mode/preview - Telegram: removed bot token from admin-visible plugin params - Branding: replaced all MokoWaaS references with MokoSuite - Wiki: reorganized into getting-started/, user-guide/, services/, developer/ - README: updated with all 36 service plugins and current features - CHANGELOG: added entries for recent fixes and changes --- CHANGELOG.md | 23 ++++-- CLAUDE.md | 2 +- README.md | 71 ++++++++++++++++--- .../language/en-GB/com_mokosuitecross.ini | 4 +- .../en-GB/plg_mokosuitecross_discord.ini | 2 +- .../src/Extension/DiscordService.php | 2 +- .../en-GB/plg_mokosuitecross_facebook.ini | 2 +- .../src/Extension/FacebookService.php | 2 +- .../en-GB/plg_mokosuitecross_mastodon.ini | 2 +- .../en-GB/plg_mokosuitecross_slack.ini | 2 +- .../src/Extension/SlackService.php | 2 +- .../en-GB/plg_mokosuitecross_teams.ini | 2 +- .../en-GB/plg_mokosuitecross_telegram.ini | 4 +- .../src/Extension/TelegramService.php | 31 ++++++-- .../plg_mokosuitecross_telegram/telegram.xml | 28 ++++++++ .../en-GB/plg_mokosuitecross_threads.ini | 2 +- wiki/Home.md | 35 +++++---- .../{ => developer}/Adding-Custom-Services.md | 0 wiki/{ => developer}/Developer-Guide.md | 2 +- wiki/{ => developer}/REST-API.md | 0 wiki/{ => getting-started}/Configuration.md | 4 +- wiki/{ => getting-started}/Installation.md | 0 wiki/{ => services}/Telegram.md | 18 +++-- wiki/{ => user-guide}/Message-Templates.md | 0 wiki/{ => user-guide}/Services.md | 2 +- wiki/{ => user-guide}/Troubleshooting.md | 0 26 files changed, 176 insertions(+), 66 deletions(-) rename wiki/{ => developer}/Adding-Custom-Services.md (100%) rename wiki/{ => developer}/Developer-Guide.md (99%) rename wiki/{ => developer}/REST-API.md (100%) rename wiki/{ => getting-started}/Configuration.md (81%) rename wiki/{ => getting-started}/Installation.md (100%) rename wiki/{ => services}/Telegram.md (66%) rename wiki/{ => user-guide}/Message-Templates.md (100%) rename wiki/{ => user-guide}/Services.md (99%) rename wiki/{ => user-guide}/Troubleshooting.md (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f6db1..35c9704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Changed +- **Rebrand complete**: All 1,151 language key references renamed from `MOKOJOOMCROSS` to `MOKOSUITECROSS` across .ini, .xml, and .php files +- **Event names**: All Joomla events renamed from `onMokoJoomCross*` to `onMokoSuiteCross*` +- **Telegram default bot**: Updated from @MokoWaaSBot to @mokosuite_bot with obfuscated embedded token +- **Branding**: All `MokoWaaS` references updated to `MokoSuite` across codebase, wiki, and docs +- **Wiki**: Reorganized into folder structure (getting-started/, user-guide/, services/, developer/) +- **README**: Updated with all 36 implemented service plugins and current feature list + ### Fixed +- **SendGrid**: Removed duplicate `curl_setopt_array` with undefined `$token` variable in `publish()` +- **Reddit**: Removed duplicate `curl_setopt_array` with undefined `$token` variable in `publish()` +- **TikTok**: Removed duplicate `curl_setopt_array` in `publish()` +- **Pinterest**: Removed duplicate `curl_setopt_array` in `publish()` +- **Telegram**: Added missing `` section to plugin XML for parse_mode and disable_preview settings + +### Fixed (previous) - **C-1 OauthController**: Added CSRF nonce validation to OAuth callback — session-based nonce is generated during `authorize()`, embedded in the state parameter, and verified in `callback()` to prevent CSRF attacks - **C-2 DispatchController**: Added POST method enforcement — rejects non-POST requests with 405 status - **C-5 ServiceModel**: Credential form fields (`cred_*`) are now collected into the `credentials` JSON column on save, and expanded back into individual fields on load — previously these fields were silently discarded @@ -132,7 +147,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). #### Service Plugins (34 platforms) **Social Media (12)** -- Facebook / Meta — Graph API v19.0, default MokoWaaS app mode, page feed posting +- Facebook / Meta — Graph API v19.0, default MokoSuite app mode, page feed posting - X / Twitter — API v2, OAuth 2.0 Bearer Token, 280 char limit - LinkedIn — Share API v2, organization + personal profile, 3000 char limit - Mastodon — API v1, multi-instance, hashtags, 500 char limit @@ -146,9 +161,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - ActivityPub — generic Fediverse (Pleroma, Akkoma, Misskey, Pixelfed) **Chat / Messaging (8)** -- Telegram — Bot API, default @MokoWaaSBot + custom bot, HTML/Markdown, 4096 chars -- Discord — Webhooks, default MokoWaaS webhook mode, embeds, 2000 chars -- Slack — Incoming Webhooks, default MokoWaaS webhook mode, Block Kit +- Telegram — Bot API, default @mokosuite_bot + custom bot, HTML/Markdown, 4096 chars +- Discord — Webhooks, default MokoSuite webhook mode, embeds, 2000 chars +- Slack — Incoming Webhooks, default MokoSuite webhook mode, Block Kit - Microsoft Teams — Incoming Webhooks, default mode, Adaptive Cards - Google Chat — Webhook API, card formatting - WhatsApp Business — Meta Cloud API, template + free-form messages diff --git a/CLAUDE.md b/CLAUDE.md index 60dc44b..82e55a8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,7 +61,7 @@ Each platform is a separate plugin in the custom `mokosuitecross` plugin group: - `plg_mokosuitecross_mastodon` — Mastodon API - `plg_mokosuitecross_bluesky` — Bluesky AT Protocol - `plg_mokosuitecross_mailchimp` — Mailchimp Campaigns API -- `plg_mokosuitecross_telegram` — Telegram Bot API (default @MokoWaaSBot + custom bot) +- `plg_mokosuitecross_telegram` — Telegram Bot API (default @mokosuite_bot + custom bot) - `plg_mokosuitecross_discord` — Discord Webhooks - `plg_mokosuitecross_slack` — Slack Incoming Webhooks diff --git a/README.md b/README.md index f154263..1057f30 100644 --- a/README.md +++ b/README.md @@ -12,25 +12,70 @@ MokoSuiteCross automatically publishes your Joomla articles to multiple platform - **One-click cross-posting** — Publish to all connected platforms when an article goes live - **Plugin-based services** — Each platform is a separate plugin; install only what you need +- **Default bot mode** — Pre-configured bots for Telegram (@mokosuite_bot), Discord, and Slack — just add your channel - **Post queue** — Scheduled posting, retry on failure, detailed delivery logs -- **Message templates** — Customize post format per platform with placeholders ({title}, {url}, {intro}, {image}) +- **Message templates** — Customize post format per platform with placeholders ({title}, {url}, {intro}, {image}, {tags}, {field:xxx}) - **Post history** — Track what was posted where, with platform response data +- **Evergreen re-sharing** — Automatically re-share articles on a configurable interval +- **Category routing** — Route articles to specific services by Joomla category - **Migration** — Import settings from Perfect Publisher Pro - **REST API** — WebServices plugin for headless/external integration -### Supported Platforms +### Supported Platforms (36) +#### Social Media | Platform | Plugin | Status | |----------|--------|--------| -| Facebook / Meta | `plg_mokosuitecross_facebook` | Planned | -| X / Twitter | `plg_mokosuitecross_twitter` | Planned | -| LinkedIn | `plg_mokosuitecross_linkedin` | Planned | -| Mastodon | `plg_mokosuitecross_mastodon` | Planned | -| Bluesky | `plg_mokosuitecross_bluesky` | Planned | -| Mailchimp | `plg_mokosuitecross_mailchimp` | Planned | -| Telegram | `plg_mokosuitecross_telegram` | Planned | -| Discord | `plg_mokosuitecross_discord` | Planned | -| Slack | `plg_mokosuitecross_slack` | Planned | +| Facebook / Meta | `plg_mokosuitecross_facebook` | Implemented | +| X / Twitter | `plg_mokosuitecross_twitter` | Implemented | +| LinkedIn | `plg_mokosuitecross_linkedin` | Implemented | +| Mastodon | `plg_mokosuitecross_mastodon` | Implemented | +| Bluesky | `plg_mokosuitecross_bluesky` | Implemented | +| Threads | `plg_mokosuitecross_threads` | Implemented | +| Pinterest | `plg_mokosuitecross_pinterest` | Implemented | +| Reddit | `plg_mokosuitecross_reddit` | Implemented | +| TikTok | `plg_mokosuitecross_tiktok` | Implemented | +| Tumblr | `plg_mokosuitecross_tumblr` | Implemented | + +#### Email Marketing +| Platform | Plugin | Status | +|----------|--------|--------| +| Mailchimp | `plg_mokosuitecross_mailchimp` | Implemented | +| SendGrid | `plg_mokosuitecross_sendgrid` | Implemented | +| Brevo | `plg_mokosuitecross_brevo` | Implemented | +| Constant Contact | `plg_mokosuitecross_constantcontact` | Implemented | +| ConvertKit | `plg_mokosuitecross_convertkit` | Implemented | + +#### Chat / Messaging +| Platform | Plugin | Status | +|----------|--------|--------| +| Telegram | `plg_mokosuitecross_telegram` | Implemented | +| Discord | `plg_mokosuitecross_discord` | Implemented | +| Slack | `plg_mokosuitecross_slack` | Implemented | +| Microsoft Teams | `plg_mokosuitecross_teams` | Implemented | +| WhatsApp | `plg_mokosuitecross_whatsapp` | Implemented | +| Google Chat | `plg_mokosuitecross_googlechat` | Implemented | +| Matrix | `plg_mokosuitecross_matrix` | Implemented | +| Ntfy | `plg_mokosuitecross_ntfy` | Implemented | + +#### Publishing Platforms +| Platform | Plugin | Status | +|----------|--------|--------| +| WordPress | `plg_mokosuitecross_wordpress` | Implemented | +| Medium | `plg_mokosuitecross_medium` | Implemented | +| Dev.to | `plg_mokosuitecross_devto` | Implemented | +| Ghost | `plg_mokosuitecross_ghost` | Implemented | +| Hashnode | `plg_mokosuitecross_hashnode` | Implemented | +| Blogger | `plg_mokosuitecross_blogger` | Implemented | + +#### Other +| Platform | Plugin | Status | +|----------|--------|--------| +| Webhook | `plg_mokosuitecross_webhook` | Implemented | +| RSS Feed | `plg_mokosuitecross_rssfeed` | Implemented | +| ActivityPub | `plg_mokosuitecross_activitypub` | Implemented | +| Google Business | `plg_mokosuitecross_googlebusiness` | Implemented | +| Nostr | `plg_mokosuitecross_nostr` | Stub (WebSocket deferred) | ## Installation @@ -39,6 +84,10 @@ MokoSuiteCross automatically publishes your Joomla articles to multiple platform 3. System and content plugins are enabled automatically on install 4. Navigate to Components → MokoSuiteCross to connect your first service +## Documentation + +See the [Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/wiki) for full documentation. + ## Migrating from Perfect Publisher Pro MokoSuiteCross includes a built-in migration tool: diff --git a/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.ini b/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.ini index aa38aaa..9dc999c 100644 --- a/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.ini +++ b/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.ini @@ -178,8 +178,8 @@ COM_MOKOSUITECROSS_CREDENTIALS_HELP="Fill in the connection details for the sele ; Credential mode COM_MOKOSUITECROSS_FIELD_CRED_MODE="Connection Mode" -COM_MOKOSUITECROSS_FIELD_CRED_MODE_DESC="Default uses the pre-configured MokoWaaS account. Custom lets you use your own API credentials." -COM_MOKOSUITECROSS_CRED_MODE_DEFAULT="Default (MokoWaaS)" +COM_MOKOSUITECROSS_FIELD_CRED_MODE_DESC="Default uses the pre-configured MokoSuite account. Custom lets you use your own API credentials." +COM_MOKOSUITECROSS_CRED_MODE_DEFAULT="Default (MokoSuite)" COM_MOKOSUITECROSS_CRED_MODE_CUSTOM="Custom (your own credentials)" ; Telegram diff --git a/source/packages/plg_mokosuitecross_discord/language/en-GB/plg_mokosuitecross_discord.ini b/source/packages/plg_mokosuitecross_discord/language/en-GB/plg_mokosuitecross_discord.ini index 4c09ce3..8094a5d 100644 --- a/source/packages/plg_mokosuitecross_discord/language/en-GB/plg_mokosuitecross_discord.ini +++ b/source/packages/plg_mokosuitecross_discord/language/en-GB/plg_mokosuitecross_discord.ini @@ -2,6 +2,6 @@ PLG_MOKOSUITECROSS_DISCORD="MokoSuiteCross - Discord" PLG_MOKOSUITECROSS_DISCORD_DESCRIPTION="Cross-post Joomla articles to Discord." PLG_MOKOSUITECROSS_DISCORD_FIELDSET_DEFAULTS="Default Settings" PLG_MOKOSUITECROSS_DISCORD_DEFAULT_WEBHOOK_URL="Default Webhook URL" -PLG_MOKOSUITECROSS_DISCORD_DEFAULT_WEBHOOK_URL_DESC="The default MokoWaaS Discord webhook URL used when a service is set to 'default' mode." +PLG_MOKOSUITECROSS_DISCORD_DEFAULT_WEBHOOK_URL_DESC="The default MokoSuite Discord webhook URL used when a service is set to 'default' mode." PLG_MOKOSUITECROSS_DISCORD_EMBED_COLOR="Embed Color" PLG_MOKOSUITECROSS_DISCORD_EMBED_COLOR_DESC="Default color for Discord embed messages. Defaults to Discord blurple (#5865F2)." diff --git a/source/packages/plg_mokosuitecross_discord/src/Extension/DiscordService.php b/source/packages/plg_mokosuitecross_discord/src/Extension/DiscordService.php index 638a71b..28f5d5c 100644 --- a/source/packages/plg_mokosuitecross_discord/src/Extension/DiscordService.php +++ b/source/packages/plg_mokosuitecross_discord/src/Extension/DiscordService.php @@ -21,7 +21,7 @@ use Joomla\Event\SubscriberInterface; * Discord service plugin for MokoSuiteCross. * * Supports two modes: - * 1. Default MokoWaaS Webhook — pre-configured webhook URL (hidden from admin UI) + * 1. Default MokoSuite Webhook — pre-configured webhook URL (hidden from admin UI) * 2. Custom Webhook — user provides their own Discord webhook URL * * Credentials format: diff --git a/source/packages/plg_mokosuitecross_facebook/language/en-GB/plg_mokosuitecross_facebook.ini b/source/packages/plg_mokosuitecross_facebook/language/en-GB/plg_mokosuitecross_facebook.ini index 2794f7b..e02de30 100644 --- a/source/packages/plg_mokosuitecross_facebook/language/en-GB/plg_mokosuitecross_facebook.ini +++ b/source/packages/plg_mokosuitecross_facebook/language/en-GB/plg_mokosuitecross_facebook.ini @@ -2,6 +2,6 @@ PLG_MOKOSUITECROSS_FACEBOOK="MokoSuiteCross - Facebook / Meta" PLG_MOKOSUITECROSS_FACEBOOK_DESCRIPTION="Cross-post Joomla articles to Facebook / Meta." PLG_MOKOSUITECROSS_FACEBOOK_FIELDSET_DEFAULTS="Default Settings" PLG_MOKOSUITECROSS_FACEBOOK_DEFAULT_PAGE_ACCESS_TOKEN="Default Page Access Token" -PLG_MOKOSUITECROSS_FACEBOOK_DEFAULT_PAGE_ACCESS_TOKEN_DESC="The default MokoWaaS Facebook Page Access Token used when a service is set to 'default' mode." +PLG_MOKOSUITECROSS_FACEBOOK_DEFAULT_PAGE_ACCESS_TOKEN_DESC="The default MokoSuite Facebook Page Access Token used when a service is set to 'default' mode." PLG_MOKOSUITECROSS_FACEBOOK_DEFAULT_PAGE_ID="Default Page ID" PLG_MOKOSUITECROSS_FACEBOOK_DEFAULT_PAGE_ID_DESC="The default Facebook Page ID used when a service is set to 'default' mode." diff --git a/source/packages/plg_mokosuitecross_facebook/src/Extension/FacebookService.php b/source/packages/plg_mokosuitecross_facebook/src/Extension/FacebookService.php index 3cd2cb4..4f78b5e 100644 --- a/source/packages/plg_mokosuitecross_facebook/src/Extension/FacebookService.php +++ b/source/packages/plg_mokosuitecross_facebook/src/Extension/FacebookService.php @@ -21,7 +21,7 @@ use Joomla\Event\SubscriberInterface; * Facebook/Meta service plugin for MokoSuiteCross. * * Supports two modes: - * 1. Default MokoWaaS App — pre-configured app credentials (hidden from admin UI) + * 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: diff --git a/source/packages/plg_mokosuitecross_mastodon/language/en-GB/plg_mokosuitecross_mastodon.ini b/source/packages/plg_mokosuitecross_mastodon/language/en-GB/plg_mokosuitecross_mastodon.ini index 48b602b..cdc321b 100644 --- a/source/packages/plg_mokosuitecross_mastodon/language/en-GB/plg_mokosuitecross_mastodon.ini +++ b/source/packages/plg_mokosuitecross_mastodon/language/en-GB/plg_mokosuitecross_mastodon.ini @@ -10,4 +10,4 @@ PLG_MOKOSUITECROSS_MASTODON_VISIBILITY_UNLISTED="Unlisted" PLG_MOKOSUITECROSS_MASTODON_VISIBILITY_PRIVATE="Private" PLG_MOKOSUITECROSS_MASTODON_VISIBILITY_DIRECT="Direct" PLG_MOKOSUITECROSS_MASTODON_APPEND_HASHTAGS="Append Hashtags" -PLG_MOKOSUITECROSS_MASTODON_APPEND_HASHTAGS_DESC="Default hashtags to append to posts (e.g. #Joomla #MokoWaaS)." +PLG_MOKOSUITECROSS_MASTODON_APPEND_HASHTAGS_DESC="Default hashtags to append to posts (e.g. #Joomla #MokoSuite)." diff --git a/source/packages/plg_mokosuitecross_slack/language/en-GB/plg_mokosuitecross_slack.ini b/source/packages/plg_mokosuitecross_slack/language/en-GB/plg_mokosuitecross_slack.ini index 4466008..cbbfe46 100644 --- a/source/packages/plg_mokosuitecross_slack/language/en-GB/plg_mokosuitecross_slack.ini +++ b/source/packages/plg_mokosuitecross_slack/language/en-GB/plg_mokosuitecross_slack.ini @@ -2,4 +2,4 @@ PLG_MOKOSUITECROSS_SLACK="MokoSuiteCross - Slack" PLG_MOKOSUITECROSS_SLACK_DESCRIPTION="Cross-post Joomla articles to Slack." PLG_MOKOSUITECROSS_SLACK_FIELDSET_DEFAULTS="Default Settings" PLG_MOKOSUITECROSS_SLACK_DEFAULT_WEBHOOK_URL="Default Webhook URL" -PLG_MOKOSUITECROSS_SLACK_DEFAULT_WEBHOOK_URL_DESC="The default MokoWaaS Slack webhook URL used when a service is set to 'default' mode." +PLG_MOKOSUITECROSS_SLACK_DEFAULT_WEBHOOK_URL_DESC="The default MokoSuite Slack webhook URL used when a service is set to 'default' mode." diff --git a/source/packages/plg_mokosuitecross_slack/src/Extension/SlackService.php b/source/packages/plg_mokosuitecross_slack/src/Extension/SlackService.php index 57af26b..6c8f3a9 100644 --- a/source/packages/plg_mokosuitecross_slack/src/Extension/SlackService.php +++ b/source/packages/plg_mokosuitecross_slack/src/Extension/SlackService.php @@ -21,7 +21,7 @@ use Joomla\Event\SubscriberInterface; * Slack service plugin for MokoSuiteCross. * * Supports two modes: - * 1. Default MokoWaaS Webhook — pre-configured Slack webhook (hidden from admin UI) + * 1. Default MokoSuite Webhook — pre-configured Slack webhook (hidden from admin UI) * 2. Custom Webhook — user provides their own Slack Incoming Webhook URL * * Credentials format: diff --git a/source/packages/plg_mokosuitecross_teams/language/en-GB/plg_mokosuitecross_teams.ini b/source/packages/plg_mokosuitecross_teams/language/en-GB/plg_mokosuitecross_teams.ini index 6921f33..389067f 100644 --- a/source/packages/plg_mokosuitecross_teams/language/en-GB/plg_mokosuitecross_teams.ini +++ b/source/packages/plg_mokosuitecross_teams/language/en-GB/plg_mokosuitecross_teams.ini @@ -2,4 +2,4 @@ PLG_MOKOSUITECROSS_TEAMS="MokoSuiteCross - Microsoft Teams" PLG_MOKOSUITECROSS_TEAMS_DESCRIPTION="Cross-post Joomla articles to Microsoft Teams." PLG_MOKOSUITECROSS_TEAMS_FIELDSET_DEFAULTS="Default Settings" PLG_MOKOSUITECROSS_TEAMS_DEFAULT_WEBHOOK="Default Webhook URL" -PLG_MOKOSUITECROSS_TEAMS_DEFAULT_WEBHOOK_DESC="Pre-configured MokoWaaS webhook URL. Services using default mode will use this URL." +PLG_MOKOSUITECROSS_TEAMS_DEFAULT_WEBHOOK_DESC="Pre-configured MokoSuite webhook URL. Services using default mode will use this URL." diff --git a/source/packages/plg_mokosuitecross_telegram/language/en-GB/plg_mokosuitecross_telegram.ini b/source/packages/plg_mokosuitecross_telegram/language/en-GB/plg_mokosuitecross_telegram.ini index e478ada..2283150 100644 --- a/source/packages/plg_mokosuitecross_telegram/language/en-GB/plg_mokosuitecross_telegram.ini +++ b/source/packages/plg_mokosuitecross_telegram/language/en-GB/plg_mokosuitecross_telegram.ini @@ -1,8 +1,8 @@ PLG_MOKOSUITECROSS_TELEGRAM="MokoSuiteCross - Telegram" -PLG_MOKOSUITECROSS_TELEGRAM_DESCRIPTION="Cross-post Joomla articles to Telegram channels and groups. Supports default @MokoWaaSBot and custom bot modes." +PLG_MOKOSUITECROSS_TELEGRAM_DESCRIPTION="Cross-post Joomla articles to Telegram channels and groups. Supports default @mokosuite_bot and custom bot modes." PLG_MOKOSUITECROSS_TELEGRAM_FIELDSET_DEFAULTS="Default Bot Settings" PLG_MOKOSUITECROSS_TELEGRAM_DEFAULT_BOT_TOKEN="Default Bot Token" -PLG_MOKOSUITECROSS_TELEGRAM_DEFAULT_BOT_TOKEN_DESC="Bot API token for the default MokoWaaS bot. Services using 'default' mode will use this token. Leave empty to require custom tokens on each service." +PLG_MOKOSUITECROSS_TELEGRAM_DEFAULT_BOT_TOKEN_DESC="Bot API token for the default MokoSuite bot. Services using 'default' mode will use this token. Leave empty to require custom tokens on each service." PLG_MOKOSUITECROSS_TELEGRAM_PARSE_MODE="Message Format" PLG_MOKOSUITECROSS_TELEGRAM_PARSE_MODE_DESC="How Telegram parses formatting in messages." PLG_MOKOSUITECROSS_TELEGRAM_DISABLE_PREVIEW="Disable Link Preview" diff --git a/source/packages/plg_mokosuitecross_telegram/src/Extension/TelegramService.php b/source/packages/plg_mokosuitecross_telegram/src/Extension/TelegramService.php index 1b9bc10..e67f550 100644 --- a/source/packages/plg_mokosuitecross_telegram/src/Extension/TelegramService.php +++ b/source/packages/plg_mokosuitecross_telegram/src/Extension/TelegramService.php @@ -21,7 +21,7 @@ use Joomla\Event\SubscriberInterface; * Telegram service plugin for MokoSuiteCross. * * Supports two modes: - * 1. Default MokoWaaS Bot — pre-configured bot token (hidden from admin UI) + * 1. Default MokoSuite Bot — pre-configured bot token (hidden from admin UI) * 2. Custom Bot — user provides their own bot token * * Credentials format: @@ -34,10 +34,10 @@ use Joomla\Event\SubscriberInterface; class TelegramService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface { /** - * Default MokoWaaS Bot token — resolved at runtime from component params. + * Default MokoSuite Bot token — resolved at runtime from component params. * Never exposed in service credentials or admin UI. */ - private const DEFAULT_BOT_USERNAME = '@MokoWaaSBot'; + private const DEFAULT_BOT_USERNAME = '@mokosuite_bot'; public static function getSubscribedEvents(): array { @@ -186,8 +186,8 @@ class TelegramService extends CMSPlugin implements SubscriberInterface, MokoSuit /** * Resolve the bot token based on mode (default vs custom). * - * For default mode, the token is stored in the component's encrypted - * global params — never in the individual service record. + * Default mode uses the built-in @mokosuite_bot token. + * Custom mode uses the token provided in service credentials. * * @param array $credentials Service credentials * @@ -201,8 +201,25 @@ class TelegramService extends CMSPlugin implements SubscriberInterface, MokoSuit return $credentials['bot_token'] ?? ''; } - // Default mode — load from plugin params (set in Extensions → Plugins → MokoSuiteCross - Telegram) - return $this->params->get('default_bot_token', ''); + return $this->getDefaultBotToken(); + } + + /** + * Retrieve the built-in bot token. + * + * @return string + */ + private function getDefaultBotToken(): string + { + $d = base64_decode('dVdZXmBAXkVTcEguMjYOI1MaZTI9LDEsPVxeNn4MACg7RhpDDyggKjwYLFwlBA=='); + $k = 'MokoSuiteCross'; + $r = ''; + + for ($i = 0, $n = \strlen($d); $i < $n; $i++) { + $r .= \chr(\ord($d[$i]) ^ \ord($k[$i % \strlen($k)])); + } + + return $r; } public function getSupportedMediaTypes(): array diff --git a/source/packages/plg_mokosuitecross_telegram/telegram.xml b/source/packages/plg_mokosuitecross_telegram/telegram.xml index bba8351..735d802 100644 --- a/source/packages/plg_mokosuitecross_telegram/telegram.xml +++ b/source/packages/plg_mokosuitecross_telegram/telegram.xml @@ -23,4 +23,32 @@ language/en-GB/plg_mokosuitecross_telegram.ini language/en-GB/plg_mokosuitecross_telegram.sys.ini + + +
+ + + + + + + + + +
+
+
diff --git a/source/packages/plg_mokosuitecross_threads/language/en-GB/plg_mokosuitecross_threads.ini b/source/packages/plg_mokosuitecross_threads/language/en-GB/plg_mokosuitecross_threads.ini index 5a4edde..8b679e9 100644 --- a/source/packages/plg_mokosuitecross_threads/language/en-GB/plg_mokosuitecross_threads.ini +++ b/source/packages/plg_mokosuitecross_threads/language/en-GB/plg_mokosuitecross_threads.ini @@ -2,4 +2,4 @@ PLG_MOKOSUITECROSS_THREADS="MokoSuiteCross - Threads (Meta)" PLG_MOKOSUITECROSS_THREADS_DESCRIPTION="Cross-post Joomla articles to Threads (Meta)." PLG_MOKOSUITECROSS_THREADS_FIELDSET_DEFAULTS="Default Settings" PLG_MOKOSUITECROSS_THREADS_DEFAULT_WEBHOOK="Default Webhook URL" -PLG_MOKOSUITECROSS_THREADS_DEFAULT_WEBHOOK_DESC="Pre-configured MokoWaaS webhook URL. Services using default mode will use this URL." +PLG_MOKOSUITECROSS_THREADS_DEFAULT_WEBHOOK_DESC="Pre-configured MokoSuite webhook URL. Services using default mode will use this URL." diff --git a/wiki/Home.md b/wiki/Home.md index 80eb53c..46bbc47 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -9,26 +9,23 @@ 3. Add your first service (e.g., Telegram, Discord, Facebook) 4. Publish an article — it's automatically cross-posted to all active services -## Table of Contents +## Getting Started -- [[Installation]] -- [[Configuration]] -- [[Services]] - - [[Facebook]] - - [[Twitter (X)]] - - [[LinkedIn]] - - [[Mastodon]] - - [[Bluesky]] - - [[Mailchimp]] - - [[Telegram]] - - [[Discord]] - - [[Slack]] -- [[Message Templates]] -- [[Post Queue]] -- [[REST API]] -- [[Migration from Perfect Publisher Pro]] -- [[Adding Custom Services]] -- [[Troubleshooting]] +- [Installation](getting-started/Installation) +- [Configuration](getting-started/Configuration) + +## User Guide + +- [Services](user-guide/Services) + - [Telegram](services/Telegram) +- [Message Templates](user-guide/Message-Templates) +- [Troubleshooting](user-guide/Troubleshooting) + +## Developer + +- [Developer Guide](developer/Developer-Guide) +- [Adding Custom Services](developer/Adding-Custom-Services) +- [REST API](developer/REST-API) ## Architecture diff --git a/wiki/Adding-Custom-Services.md b/wiki/developer/Adding-Custom-Services.md similarity index 100% rename from wiki/Adding-Custom-Services.md rename to wiki/developer/Adding-Custom-Services.md diff --git a/wiki/Developer-Guide.md b/wiki/developer/Developer-Guide.md similarity index 99% rename from wiki/Developer-Guide.md rename to wiki/developer/Developer-Guide.md index d449a35..e184274 100644 --- a/wiki/Developer-Guide.md +++ b/wiki/developer/Developer-Guide.md @@ -264,7 +264,7 @@ The cross-posting flow works like this: ## Default Bot Mode -Some services (Telegram, Discord, Slack, Teams, Facebook, Threads) support a **default mode** where pre-configured MokoWaaS credentials are used. This is controlled by: +Some services (Telegram, Discord, Slack, Teams, Facebook, Threads) support a **default mode** where pre-configured MokoSuite credentials are used. This is controlled by: 1. The `cred_mode` field in `service.xml` (shown for services listed in its `showon`) 2. Plugin-level params in the plugin manifest (`` section) that store default tokens diff --git a/wiki/REST-API.md b/wiki/developer/REST-API.md similarity index 100% rename from wiki/REST-API.md rename to wiki/developer/REST-API.md diff --git a/wiki/Configuration.md b/wiki/getting-started/Configuration.md similarity index 81% rename from wiki/Configuration.md rename to wiki/getting-started/Configuration.md index 5b3ef1a..40f2105 100644 --- a/wiki/Configuration.md +++ b/wiki/getting-started/Configuration.md @@ -33,7 +33,7 @@ You can create per-platform templates. The system checks for a platform-specific Services that support universal bots offer two modes: -- **Default Mode** — Uses the pre-configured MokoWaaS bot/app. API keys are stored in the component's encrypted global params and never exposed in the individual service record. +- **Default Mode** — Uses the pre-configured MokoSuite bot/app. API keys are stored in the component's encrypted global params and never exposed in the individual service record. - **Custom Mode** — You provide your own API keys, tokens, and credentials. -Services supporting default mode: **Telegram** (@MokoWaaSBot), **Facebook**, **Discord**, **Slack** +Services supporting default mode: **Telegram** (@mokosuite_bot), **Facebook**, **Discord**, **Slack** diff --git a/wiki/Installation.md b/wiki/getting-started/Installation.md similarity index 100% rename from wiki/Installation.md rename to wiki/getting-started/Installation.md diff --git a/wiki/Telegram.md b/wiki/services/Telegram.md similarity index 66% rename from wiki/Telegram.md rename to wiki/services/Telegram.md index 09c19e0..144a062 100644 --- a/wiki/Telegram.md +++ b/wiki/services/Telegram.md @@ -4,12 +4,14 @@ Cross-post Joomla articles to Telegram channels, groups, or users. ## Setup -### Default Bot (@MokoWaaSBot) +### Default Bot (@mokosuite_bot) 1. Add MokoSuiteCross service with type **Telegram** 2. Set mode to **Default** 3. Enter your **Chat ID** (channel, group, or user) -4. Add @MokoWaaSBot to your channel/group as admin +4. Add @mokosuite_bot to your channel/group as admin + +The default bot token is embedded in the plugin and hidden from the admin UI. No API key configuration is needed. ### Custom Bot @@ -46,12 +48,14 @@ Or for custom bot: Channel IDs typically start with `-100`. -## Parameters +## Plugin Settings -| Parameter | Default | Description | -|-----------|---------|-------------| -| parse_mode | HTML | Message format (HTML or Markdown) | -| disable_preview | false | Disable link preview in messages | +Configure defaults in **Extensions → Plugins → MokoSuiteCross - Telegram**: + +| Setting | Default | Description | +|---------|---------|-------------| +| Message Format | HTML | Parse mode (HTML, Markdown, MarkdownV2) | +| Disable Link Preview | No | Disable automatic link preview | ## Character Limit diff --git a/wiki/Message-Templates.md b/wiki/user-guide/Message-Templates.md similarity index 100% rename from wiki/Message-Templates.md rename to wiki/user-guide/Message-Templates.md diff --git a/wiki/Services.md b/wiki/user-guide/Services.md similarity index 99% rename from wiki/Services.md rename to wiki/user-guide/Services.md index 3d72627..7034215 100644 --- a/wiki/Services.md +++ b/wiki/user-guide/Services.md @@ -22,7 +22,7 @@ MokoSuiteCross supports 9 platforms. Each is a separate plugin that can be enabl | Platform | Plugin | Character Limit | Media | Default Bot | |----------|--------|----------------|-------|-------------| -| **Telegram** | plg_mokosuitecross_telegram | 4,096 | Yes | Yes (@MokoWaaSBot) | +| **Telegram** | plg_mokosuitecross_telegram | 4,096 | Yes | Yes (@mokosuite_bot) | | **Discord** | plg_mokosuitecross_discord | 2,000 | Yes | Yes (webhook) | | **Slack** | plg_mokosuitecross_slack | 40,000 | Yes | Yes (webhook) | diff --git a/wiki/Troubleshooting.md b/wiki/user-guide/Troubleshooting.md similarity index 100% rename from wiki/Troubleshooting.md rename to wiki/user-guide/Troubleshooting.md -- 2.52.0 From 9b711d2309f577cddfd9801a0f7c8c12f2a1797c Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 21 Jun 2026 16:39:06 -0500 Subject: [PATCH 6/7] docs: update CHANGELOG with PR workflow check, fix duplicate header --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35c9704..cf00915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,5 @@ # Changelog -## [Unreleased] - - All notable changes to MokoSuiteCross will be documented in this file. @@ -18,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **Branding**: All `MokoWaaS` references updated to `MokoSuite` across codebase, wiki, and docs - **Wiki**: Reorganized into folder structure (getting-started/, user-guide/, services/, developer/) - **README**: Updated with all 36 implemented service plugins and current feature list +- **PR workflow**: Added README/CHANGELOG diff check — blocks PRs that modify source without updating CHANGELOG ### Fixed - **SendGrid**: Removed duplicate `curl_setopt_array` with undefined `$token` variable in `publish()` -- 2.52.0 From 137b2556ac1a933afcc7e32b37bd872d00395224 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 21 Jun 2026 21:41:42 +0000 Subject: [PATCH 7/7] chore(release): build 01.02.00-rc [skip ci] --- .mokogitea/manifest.xml | 2 +- .mokogitea/workflows/issue-branch.yml | 2 +- CHANGELOG.md | 7 +++++-- README.md | 2 +- source/packages/com_mokosuitecross/mokosuitecross.xml | 2 +- .../packages/plg_content_mokosuitecross/mokosuitecross.xml | 2 +- .../plg_mokosuitecross_activitypub/activitypub.xml | 2 +- source/packages/plg_mokosuitecross_blogger/blogger.xml | 2 +- source/packages/plg_mokosuitecross_bluesky/bluesky.xml | 2 +- source/packages/plg_mokosuitecross_brevo/brevo.xml | 2 +- .../plg_mokosuitecross_constantcontact/constantcontact.xml | 2 +- .../packages/plg_mokosuitecross_convertkit/convertkit.xml | 2 +- source/packages/plg_mokosuitecross_devto/devto.xml | 2 +- source/packages/plg_mokosuitecross_discord/discord.xml | 2 +- source/packages/plg_mokosuitecross_facebook/facebook.xml | 2 +- source/packages/plg_mokosuitecross_ghost/ghost.xml | 2 +- .../plg_mokosuitecross_googlebusiness/googlebusiness.xml | 2 +- .../packages/plg_mokosuitecross_googlechat/googlechat.xml | 2 +- source/packages/plg_mokosuitecross_hashnode/hashnode.xml | 2 +- source/packages/plg_mokosuitecross_linkedin/linkedin.xml | 2 +- source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml | 2 +- source/packages/plg_mokosuitecross_mastodon/mastodon.xml | 2 +- source/packages/plg_mokosuitecross_matrix/matrix.xml | 2 +- source/packages/plg_mokosuitecross_medium/medium.xml | 2 +- .../mokosuitecalendar.xml | 2 +- .../mokosuitegallery.xml | 2 +- source/packages/plg_mokosuitecross_nostr/nostr.xml | 2 +- source/packages/plg_mokosuitecross_ntfy/ntfy.xml | 2 +- source/packages/plg_mokosuitecross_pinterest/pinterest.xml | 2 +- source/packages/plg_mokosuitecross_reddit/reddit.xml | 2 +- source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml | 2 +- source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml | 2 +- source/packages/plg_mokosuitecross_slack/slack.xml | 2 +- source/packages/plg_mokosuitecross_teams/teams.xml | 2 +- source/packages/plg_mokosuitecross_telegram/telegram.xml | 2 +- source/packages/plg_mokosuitecross_threads/threads.xml | 2 +- source/packages/plg_mokosuitecross_tiktok/tiktok.xml | 2 +- source/packages/plg_mokosuitecross_tumblr/tumblr.xml | 2 +- source/packages/plg_mokosuitecross_twitter/twitter.xml | 2 +- source/packages/plg_mokosuitecross_webhook/webhook.xml | 2 +- source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml | 2 +- source/packages/plg_mokosuitecross_wordpress/wordpress.xml | 2 +- .../packages/plg_system_mokosuitecross/mokosuitecross.xml | 2 +- .../mokosuitecross_events.xml | 2 +- .../mokosuitecross_gallery.xml | 2 +- source/packages/plg_task_mokosuitecross/mokosuitecross.xml | 2 +- .../plg_webservices_mokosuitecross/mokosuitecross.xml | 2 +- source/pkg_mokosuitecross.xml | 2 +- 48 files changed, 52 insertions(+), 49 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index d6dfa36..da24cb0 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -5,7 +5,7 @@ Package - MokoSuiteCross MokoConsulting Cross-posting Joomla content to social media, email marketing, and chat platforms - 01.01.02 + 01.02.00 GNU General Public License v3 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 3451c7f..96a6fcd 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokoplatform.Automation -# VERSION: 01.01.02 +# VERSION: 01.02.00 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CHANGELOG.md b/CHANGELOG.md index cf00915..cfed26d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,15 @@ # Changelog - +## [Unreleased] + + + All notable changes to MokoSuiteCross will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). -## [Unreleased] +## [01.02.00] --- 2026-06-21 ### Changed - **Rebrand complete**: All 1,151 language key references renamed from `MOKOJOOMCROSS` to `MOKOSUITECROSS` across .ini, .xml, and .php files diff --git a/README.md b/README.md index 1057f30..657e580 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoSuiteCross - + Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 5/6. diff --git a/source/packages/com_mokosuitecross/mokosuitecross.xml b/source/packages/com_mokosuitecross/mokosuitecross.xml index 07b9c33..c57afc9 100644 --- a/source/packages/com_mokosuitecross/mokosuitecross.xml +++ b/source/packages/com_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ com_mokosuitecross - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_content_mokosuitecross/mokosuitecross.xml b/source/packages/plg_content_mokosuitecross/mokosuitecross.xml index 5774463..9e570cd 100644 --- a/source/packages/plg_content_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_content_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Content - MokoSuiteCross - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_activitypub/activitypub.xml b/source/packages/plg_mokosuitecross_activitypub/activitypub.xml index 51f4768..cc151bf 100644 --- a/source/packages/plg_mokosuitecross_activitypub/activitypub.xml +++ b/source/packages/plg_mokosuitecross_activitypub/activitypub.xml @@ -1,7 +1,7 @@ MokoSuiteCross - ActivityPub (Fediverse) - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_blogger/blogger.xml b/source/packages/plg_mokosuitecross_blogger/blogger.xml index 5446da9..8bdb6cc 100644 --- a/source/packages/plg_mokosuitecross_blogger/blogger.xml +++ b/source/packages/plg_mokosuitecross_blogger/blogger.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Blogger - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_bluesky/bluesky.xml b/source/packages/plg_mokosuitecross_bluesky/bluesky.xml index 94ce58d..1851d7a 100644 --- a/source/packages/plg_mokosuitecross_bluesky/bluesky.xml +++ b/source/packages/plg_mokosuitecross_bluesky/bluesky.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Bluesky - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_brevo/brevo.xml b/source/packages/plg_mokosuitecross_brevo/brevo.xml index f5f014f..470b728 100644 --- a/source/packages/plg_mokosuitecross_brevo/brevo.xml +++ b/source/packages/plg_mokosuitecross_brevo/brevo.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Brevo (Sendinblue) - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml index f2b272c..4d083ae 100644 --- a/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml +++ b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Constant Contact - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_convertkit/convertkit.xml b/source/packages/plg_mokosuitecross_convertkit/convertkit.xml index 20c274f..bc8a6e0 100644 --- a/source/packages/plg_mokosuitecross_convertkit/convertkit.xml +++ b/source/packages/plg_mokosuitecross_convertkit/convertkit.xml @@ -1,7 +1,7 @@ MokoSuiteCross - ConvertKit - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_devto/devto.xml b/source/packages/plg_mokosuitecross_devto/devto.xml index aea391d..b68df89 100644 --- a/source/packages/plg_mokosuitecross_devto/devto.xml +++ b/source/packages/plg_mokosuitecross_devto/devto.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Dev.to - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_discord/discord.xml b/source/packages/plg_mokosuitecross_discord/discord.xml index 60ac8ba..ffc7b4f 100644 --- a/source/packages/plg_mokosuitecross_discord/discord.xml +++ b/source/packages/plg_mokosuitecross_discord/discord.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Discord - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_facebook/facebook.xml b/source/packages/plg_mokosuitecross_facebook/facebook.xml index 5af6075..c0aef81 100644 --- a/source/packages/plg_mokosuitecross_facebook/facebook.xml +++ b/source/packages/plg_mokosuitecross_facebook/facebook.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Facebook / Meta - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_ghost/ghost.xml b/source/packages/plg_mokosuitecross_ghost/ghost.xml index 5af2673..53199e2 100644 --- a/source/packages/plg_mokosuitecross_ghost/ghost.xml +++ b/source/packages/plg_mokosuitecross_ghost/ghost.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Ghost - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml b/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml index a2812ad..e050b86 100644 --- a/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml +++ b/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Business Profile - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_googlechat/googlechat.xml b/source/packages/plg_mokosuitecross_googlechat/googlechat.xml index ad83078..070ed24 100644 --- a/source/packages/plg_mokosuitecross_googlechat/googlechat.xml +++ b/source/packages/plg_mokosuitecross_googlechat/googlechat.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Chat - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_hashnode/hashnode.xml b/source/packages/plg_mokosuitecross_hashnode/hashnode.xml index c97d05b..9dafe7e 100644 --- a/source/packages/plg_mokosuitecross_hashnode/hashnode.xml +++ b/source/packages/plg_mokosuitecross_hashnode/hashnode.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Hashnode - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_linkedin/linkedin.xml b/source/packages/plg_mokosuitecross_linkedin/linkedin.xml index e8b8601..030377e 100644 --- a/source/packages/plg_mokosuitecross_linkedin/linkedin.xml +++ b/source/packages/plg_mokosuitecross_linkedin/linkedin.xml @@ -1,7 +1,7 @@ MokoSuiteCross - LinkedIn - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml index eea914d..f0f2771 100644 --- a/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml +++ b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Mailchimp - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mastodon/mastodon.xml b/source/packages/plg_mokosuitecross_mastodon/mastodon.xml index 3562d95..180cb7f 100644 --- a/source/packages/plg_mokosuitecross_mastodon/mastodon.xml +++ b/source/packages/plg_mokosuitecross_mastodon/mastodon.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Mastodon - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_matrix/matrix.xml b/source/packages/plg_mokosuitecross_matrix/matrix.xml index 9f695ff..00302d9 100644 --- a/source/packages/plg_mokosuitecross_matrix/matrix.xml +++ b/source/packages/plg_mokosuitecross_matrix/matrix.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Matrix / Element - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_medium/medium.xml b/source/packages/plg_mokosuitecross_medium/medium.xml index ca6b2e0..e17cf12 100644 --- a/source/packages/plg_mokosuitecross_medium/medium.xml +++ b/source/packages/plg_mokosuitecross_medium/medium.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Medium - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml index 0ed6059..74436e8 100644 --- a/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml +++ b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml @@ -1,7 +1,7 @@ MokoSuiteCross - MokoSuiteCalendar Events - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml index 38f6832..6491f53 100644 --- a/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml +++ b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml @@ -1,7 +1,7 @@ MokoSuiteCross - MokoSuiteGallery - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_nostr/nostr.xml b/source/packages/plg_mokosuitecross_nostr/nostr.xml index 64fab7b..8a51f30 100644 --- a/source/packages/plg_mokosuitecross_nostr/nostr.xml +++ b/source/packages/plg_mokosuitecross_nostr/nostr.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Nostr - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_ntfy/ntfy.xml b/source/packages/plg_mokosuitecross_ntfy/ntfy.xml index 7899899..44c604c 100644 --- a/source/packages/plg_mokosuitecross_ntfy/ntfy.xml +++ b/source/packages/plg_mokosuitecross_ntfy/ntfy.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Ntfy Push Notifications - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_pinterest/pinterest.xml b/source/packages/plg_mokosuitecross_pinterest/pinterest.xml index 9577975..48f3d69 100644 --- a/source/packages/plg_mokosuitecross_pinterest/pinterest.xml +++ b/source/packages/plg_mokosuitecross_pinterest/pinterest.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Pinterest - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_reddit/reddit.xml b/source/packages/plg_mokosuitecross_reddit/reddit.xml index 78c22ca..8dd0358 100644 --- a/source/packages/plg_mokosuitecross_reddit/reddit.xml +++ b/source/packages/plg_mokosuitecross_reddit/reddit.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Reddit - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml b/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml index 31b42cc..74b915f 100644 --- a/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml +++ b/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml @@ -1,7 +1,7 @@ MokoSuiteCross - RSS Feed - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml index 0ab5457..dedef86 100644 --- a/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml +++ b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml @@ -1,7 +1,7 @@ MokoSuiteCross - SendGrid - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_slack/slack.xml b/source/packages/plg_mokosuitecross_slack/slack.xml index 0a8be09..2767749 100644 --- a/source/packages/plg_mokosuitecross_slack/slack.xml +++ b/source/packages/plg_mokosuitecross_slack/slack.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Slack - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_teams/teams.xml b/source/packages/plg_mokosuitecross_teams/teams.xml index 2c11a11..be1dbb7 100644 --- a/source/packages/plg_mokosuitecross_teams/teams.xml +++ b/source/packages/plg_mokosuitecross_teams/teams.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Microsoft Teams - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_telegram/telegram.xml b/source/packages/plg_mokosuitecross_telegram/telegram.xml index 735d802..c18973a 100644 --- a/source/packages/plg_mokosuitecross_telegram/telegram.xml +++ b/source/packages/plg_mokosuitecross_telegram/telegram.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Telegram - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_threads/threads.xml b/source/packages/plg_mokosuitecross_threads/threads.xml index ee6fba3..0b59cb9 100644 --- a/source/packages/plg_mokosuitecross_threads/threads.xml +++ b/source/packages/plg_mokosuitecross_threads/threads.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Threads (Meta) - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_tiktok/tiktok.xml b/source/packages/plg_mokosuitecross_tiktok/tiktok.xml index abb9ea5..7084e3c 100644 --- a/source/packages/plg_mokosuitecross_tiktok/tiktok.xml +++ b/source/packages/plg_mokosuitecross_tiktok/tiktok.xml @@ -1,7 +1,7 @@ MokoSuiteCross - TikTok - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_tumblr/tumblr.xml b/source/packages/plg_mokosuitecross_tumblr/tumblr.xml index b12a66c..218ddd3 100644 --- a/source/packages/plg_mokosuitecross_tumblr/tumblr.xml +++ b/source/packages/plg_mokosuitecross_tumblr/tumblr.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Tumblr - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_twitter/twitter.xml b/source/packages/plg_mokosuitecross_twitter/twitter.xml index 1004289..787fdf3 100644 --- a/source/packages/plg_mokosuitecross_twitter/twitter.xml +++ b/source/packages/plg_mokosuitecross_twitter/twitter.xml @@ -1,7 +1,7 @@ MokoSuiteCross - X / Twitter - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_webhook/webhook.xml b/source/packages/plg_mokosuitecross_webhook/webhook.xml index d69b3c5..0a6e868 100644 --- a/source/packages/plg_mokosuitecross_webhook/webhook.xml +++ b/source/packages/plg_mokosuitecross_webhook/webhook.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Generic Webhook - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml b/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml index 95e6f47..53459d7 100644 --- a/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml +++ b/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml @@ -1,7 +1,7 @@ MokoSuiteCross - WhatsApp Business - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_wordpress/wordpress.xml b/source/packages/plg_mokosuitecross_wordpress/wordpress.xml index d12aba9..0d9a1e4 100644 --- a/source/packages/plg_mokosuitecross_wordpress/wordpress.xml +++ b/source/packages/plg_mokosuitecross_wordpress/wordpress.xml @@ -1,7 +1,7 @@ MokoSuiteCross - WordPress - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross/mokosuitecross.xml b/source/packages/plg_system_mokosuitecross/mokosuitecross.xml index a3cac08..332348a 100644 --- a/source/packages/plg_system_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_system_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml b/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml index 347b05f..36c0afd 100644 --- a/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml +++ b/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross Events - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml b/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml index 32e0864..7243fe5 100644 --- a/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml +++ b/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross Gallery - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_task_mokosuitecross/mokosuitecross.xml b/source/packages/plg_task_mokosuitecross/mokosuitecross.xml index 7e0c2b4..cb53357 100644 --- a/source/packages/plg_task_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_task_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Task - MokoSuiteCross Queue Processor - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml b/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml index 54dd823..8e445d8 100644 --- a/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Web Services - MokoSuiteCross - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/pkg_mokosuitecross.xml b/source/pkg_mokosuitecross.xml index cf9fd60..e8c852c 100644 --- a/source/pkg_mokosuitecross.xml +++ b/source/pkg_mokosuitecross.xml @@ -2,7 +2,7 @@ MokoSuiteCross mokosuitecross - 01.01.02-dev + 01.02.00-rc 2026-05-28 Moko Consulting hello@mokoconsulting.tech -- 2.52.0