diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 53738ec..adc9f5b 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.04.01 + 01.04.12 GNU General Public License v3 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 75a6963..da9d64e 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.00.00 +# VERSION: 01.04.12 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CHANGELOG.md b/CHANGELOG.md index feeb88c..402b40d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,37 @@ # Changelog ## [Unreleased] +### Added +- **Instagram plugin**: Cross-post to Instagram via Meta Content Publishing API (2-step container flow) +- **YouTube plugin**: Cross-post to YouTube via Data API v3 channel bulletins +- **Share Content panel**: Per-article editor panel with platform-specific share text fields +- **New placeholders**: {social}, {short}, {chat}, {email_subject}, {email_body} for platform-optimized templates +- **Share image control**: Choose intro image, fulltext image, custom image, or no image per article +- **Mailchimp templates**: Support Mailchimp saved templates with section injection, plus responsive email wrapper fallback +- **Delete from platforms**: New MokoSuiteCrossDeleteInterface for removing cross-posted content from remote platforms +- **Delete support**: Twitter, Mastodon, Bluesky, Facebook, LinkedIn, Telegram, Discord (7 of 38 plugins) +- **Auto-delete on unpublish**: Component config option to delete from platforms when articles are unpublished or trashed +- **UTM auto-tagging**: Append utm_source, utm_medium, utm_campaign to shared URLs with {platform} token support +- **Caption rotation**: {random:opt1|opt2|opt3} placeholder picks a random option per post +- **{url_raw} placeholder**: Clean article URL without UTM parameters +- **Mastodon enhancements**: Visibility levels, content warnings, scheduled posts, polls, language tags +- **Bluesky threads**: Auto-split long messages into reply chains at sentence boundaries +- **Bluesky link cards**: External link card embeds with article title and description +- **Ntfy default server**: Default server changed to ntfy.mokoconsulting.tech with configurable plugin params + +### Changed +- **Default templates**: Updated to use platform-specific placeholders (social/short/chat/email) with graceful fallback + +### Fixed +- **Mailchimp**: Fixed broken namespace placeholder in XML manifest +- **ConvertKit**: Removed duplicate curl_setopt_array with undefined $token +- **Brevo**: Removed duplicate curl_setopt_array with undefined $token and wrong auth header +- **Constant Contact**: Removed duplicate curl_setopt_array +- **Mailchimp**: Fixed campaign creation checking HTTP 200 instead of 2xx range +- **Medium**: Fixed getUserId() returning array instead of string on error +- **Bluesky**: Replaced md5() with hash('sha256', ...) for cache key +- **ServiceController**: Exception details no longer exposed to client + ## [01.04.01] --- 2026-06-21 @@ -16,7 +47,7 @@ ## [01.03.00] --- 2026-06-21 - + All notable changes to MokoSuiteCross will be documented in this file. diff --git a/Makefile b/Makefile deleted file mode 100644 index 46b9310..0000000 --- a/Makefile +++ /dev/null @@ -1,203 +0,0 @@ -# Makefile for Joomla Extensions -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# -# MokoSuiteCross — Cross-posting Joomla content to social media, email marketing, and chat platforms - -# ============================================================================== -# CONFIGURATION - Customize these for your extension -# ============================================================================== - -# Extension Configuration -EXTENSION_NAME := mokosuitecross -EXTENSION_TYPE := package -# Options: module, plugin, component, package, template -EXTENSION_VERSION := 1.0.0 - -# Module Configuration (for modules only) -MODULE_TYPE := site -# Options: site, admin - -# Plugin Configuration (for plugins only) -PLUGIN_GROUP := system -# Options: system, content, user, authentication, etc. - -# Directories -SRC_DIR := src -BUILD_DIR := build -DIST_DIR := dist -DOCS_DIR := docs - -# Joomla Installation (for local testing - customize paths) -JOOMLA_ROOT := /var/www/html/joomla -JOOMLA_VERSION := 4 - -# Tools -PHP := php -COMPOSER := composer -NPM := npm -PHPCS := vendor/bin/phpcs -PHPCBF := vendor/bin/phpcbf -PHPUNIT := vendor/bin/phpunit -ZIP := zip - -# Coding Standards -PHPCS_STANDARD := Joomla - -# Colors for output -COLOR_RESET := \033[0m -COLOR_GREEN := \033[32m -COLOR_YELLOW := \033[33m -COLOR_BLUE := \033[34m -COLOR_RED := \033[31m - -# ============================================================================== -# TARGETS -# ============================================================================== - -.PHONY: help -help: ## Show this help message - @echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)" - @echo "$(COLOR_BLUE)║ Joomla Extension Makefile ║$(COLOR_RESET)" - @echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)" - @echo "" - @echo "Extension: $(EXTENSION_NAME) ($(EXTENSION_TYPE)) v$(EXTENSION_VERSION)" - @echo "" - @echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)" - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}' - @echo "" - -.PHONY: install-deps -install-deps: ## Install all dependencies (Composer + npm) - @echo "$(COLOR_BLUE)Installing dependencies...$(COLOR_RESET)" - @if [ -f "composer.json" ]; then \ - $(COMPOSER) install; \ - echo "$(COLOR_GREEN)✓ Composer dependencies installed$(COLOR_RESET)"; \ - fi - -.PHONY: lint -lint: ## Run PHP linter (syntax check) - @echo "$(COLOR_BLUE)Running PHP linter...$(COLOR_RESET)" - @find . -name "*.php" ! -path "./vendor/*" ! -path "./node_modules/*" ! -path "./$(BUILD_DIR)/*" \ - -exec $(PHP) -l {} \; | grep -v "No syntax errors" || true - @echo "$(COLOR_GREEN)✓ PHP linting complete$(COLOR_RESET)" - -.PHONY: phpcs -phpcs: ## Run PHP CodeSniffer (Joomla standards) - @echo "$(COLOR_BLUE)Running PHP CodeSniffer...$(COLOR_RESET)" - @if [ -f "$(PHPCS)" ]; then \ - $(PHPCS) --standard=$(PHPCS_STANDARD) --extensions=php --ignore=vendor,node_modules,$(BUILD_DIR) .; \ - else \ - echo "$(COLOR_YELLOW)⚠ PHP CodeSniffer not installed. Run: make install-deps$(COLOR_RESET)"; \ - fi - -.PHONY: validate -validate: lint phpcs ## Run all validation checks - @echo "$(COLOR_GREEN)✓ All validation checks passed$(COLOR_RESET)" - -.PHONY: clean -clean: ## Clean build artifacts - @echo "$(COLOR_BLUE)Cleaning build artifacts...$(COLOR_RESET)" - @rm -rf $(BUILD_DIR) $(DIST_DIR) - @echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)" - -MOKO_PLATFORM ?= $(or $(wildcard ../moko-platform),$(wildcard $(HOME)/moko-platform),$(wildcard /opt/moko-platform)) -MINIFY_SCRIPT := $(MOKO_PLATFORM)/build/minify.js - -.PHONY: minify -minify: ## Minify CSS/JS assets - @echo "Minifying assets..." - @if [ -f "$(MINIFY_SCRIPT)" ]; then \ - node "$(MINIFY_SCRIPT)" $(SRC_DIR); \ - elif [ -f "scripts/minify.js" ]; then \ - node scripts/minify.js; \ - else \ - echo "No minify script found"; \ - fi - -.PHONY: build -build: clean validate minify ## Build extension package - @echo "$(COLOR_BLUE)Building Joomla extension package...$(COLOR_RESET)" - @mkdir -p $(DIST_DIR) $(BUILD_DIR) - - # Determine package prefix based on extension type - @case "$(EXTENSION_TYPE)" in \ - module) \ - PACKAGE_PREFIX="mod_$(EXTENSION_NAME)"; \ - BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \ - ;; \ - plugin) \ - PACKAGE_PREFIX="plg_$(PLUGIN_GROUP)_$(EXTENSION_NAME)"; \ - BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \ - ;; \ - component) \ - PACKAGE_PREFIX="com_$(EXTENSION_NAME)"; \ - BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \ - ;; \ - package) \ - PACKAGE_PREFIX="pkg_$(EXTENSION_NAME)"; \ - BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \ - ;; \ - template) \ - PACKAGE_PREFIX="tpl_$(EXTENSION_NAME)"; \ - BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \ - ;; \ - *) \ - echo "$(COLOR_RED)✗ Unknown extension type: $(EXTENSION_TYPE)$(COLOR_RESET)"; \ - exit 1; \ - ;; \ - esac; \ - \ - mkdir -p "$$BUILD_TARGET"; \ - \ - echo "Building $$PACKAGE_PREFIX..."; \ - \ - rsync -av --progress \ - --exclude='$(BUILD_DIR)' \ - --exclude='$(DIST_DIR)' \ - --exclude='.git*' \ - --exclude='vendor/' \ - --exclude='node_modules/' \ - --exclude='tests/' \ - --exclude='Makefile' \ - --exclude='composer.json' \ - --exclude='composer.lock' \ - --exclude='package.json' \ - --exclude='package-lock.json' \ - --exclude='phpunit.xml' \ - --exclude='*.md' \ - --exclude='.editorconfig' \ - . "$$BUILD_TARGET/"; \ - \ - cd $(BUILD_DIR) && $(ZIP) -r "../$(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip" "$${PACKAGE_PREFIX}"; \ - \ - echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip$(COLOR_RESET)" - -.PHONY: package -package: build ## Alias for build - @echo "$(COLOR_GREEN)✓ Package ready for distribution$(COLOR_RESET)" - -.PHONY: release -release: validate build ## Create a release (validate + build) - @echo "$(COLOR_GREEN)✓ Release package ready$(COLOR_RESET)" - -.PHONY: version -version: ## Display version information - @echo "$(COLOR_BLUE)Extension Information:$(COLOR_RESET)" - @echo " Name: $(EXTENSION_NAME)" - @echo " Type: $(EXTENSION_TYPE)" - @echo " Version: $(EXTENSION_VERSION)" - -.PHONY: security-check -security-check: ## Run security checks on dependencies - @echo "$(COLOR_BLUE)Running security checks...$(COLOR_RESET)" - @if [ -f "composer.json" ]; then \ - $(COMPOSER) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \ - fi - -.PHONY: all -all: install-deps validate build ## Run complete build pipeline - @echo "$(COLOR_GREEN)✓ Complete build pipeline finished$(COLOR_RESET)" - -# Default target -.DEFAULT_GOAL := help diff --git a/README.md b/README.md index ab8b4e4..a25625f 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. @@ -14,20 +14,27 @@ MokoSuiteCross automatically publishes your Joomla articles to multiple platform - **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}, {tags}, {field:xxx}) +- **Message templates** — Customize post format per platform with placeholders ({title}, {url}, {social}, {short}, {chat}, {email_subject}, {email_body}, {field:xxx}) +- **Share Content panel** — Per-article fields for platform-optimized text (social, short, chat, email) with image picker +- **Caption rotation** — {random:opt1|opt2|opt3} placeholder for varying evergreen re-shares +- **UTM tracking** — Auto-append UTM parameters to shared links with {platform} token +- **Delete from platforms** — Remove cross-posted content when articles are unpublished/trashed (7 platforms) - **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 +- **Mailchimp templates** — Use saved Mailchimp templates with section injection, or built-in responsive email wrapper - **Migration** — Import settings from Perfect Publisher Pro - **REST API** — WebServices plugin for headless/external integration -### Supported Platforms (36) +### Supported Platforms (38) #### Social Media | Platform | Plugin | Status | |----------|--------|--------| | Facebook / Meta | `plg_mokosuitecross_facebook` | Implemented | | X / Twitter | `plg_mokosuitecross_twitter` | Implemented | +| Instagram | `plg_mokosuitecross_instagram` | Implemented | +| YouTube | `plg_mokosuitecross_youtube` | Implemented | | LinkedIn | `plg_mokosuitecross_linkedin` | Implemented | | Mastodon | `plg_mokosuitecross_mastodon` | Implemented | | Bluesky | `plg_mokosuitecross_bluesky` | Implemented | diff --git a/source/packages/com_mokosuitecross/config.xml b/source/packages/com_mokosuitecross/config.xml index 7554c62..7cbbb36 100644 --- a/source/packages/com_mokosuitecross/config.xml +++ b/source/packages/com_mokosuitecross/config.xml @@ -24,6 +24,17 @@ + + + + + +
+ + + + + + + + +
+
com_mokosuitecross - 01.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/com_mokosuitecross/sql/install.mysql.sql b/source/packages/com_mokosuitecross/sql/install.mysql.sql index c89f0a9..0a788f5 100644 --- a/source/packages/com_mokosuitecross/sql/install.mysql.sql +++ b/source/packages/com_mokosuitecross/sql/install.mysql.sql @@ -25,7 +25,7 @@ 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 #__mokosuitecross_services.id', - `status` varchar(20) NOT NULL DEFAULT 'queued' COMMENT 'queued, posting, posted, failed, scheduled', + `status` varchar(20) NOT NULL DEFAULT 'queued' COMMENT 'queued, posting, posted, failed, scheduled, deleted', `message` text NOT NULL COMMENT 'Rendered message sent to platform', `platform_post_id` varchar(255) NOT NULL DEFAULT '' COMMENT 'Post ID returned by platform', `platform_response` text NOT NULL COMMENT 'JSON — full API response from platform', @@ -74,25 +74,27 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitecross_logs` ( -- Insert default templates 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()), -('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()), +('twitter', 'Twitter/X Default', '{short}\n\n{url}', 1, 2, NOW()), +('mastodon', 'Mastodon Default', '{social}\n\n{url}\n\n{hashtags}', 1, 3, NOW()), +('mailchimp', 'Mailchimp Default', '

{email_subject}

\n{email_body}\n

Read more

', 1, 4, NOW()), +('telegram', 'Telegram Default', '{title}\n\n{chat}\n\nRead more', 1, 5, NOW()), +('discord', 'Discord Default', '**{title}**\n\n{chat}\n\n{url}', 1, 6, NOW()), +('slack', 'Slack Default', '*{title}*\n\n{chat}\n\n{url}', 1, 7, NOW()), +('facebook', 'Facebook Default', '{social}\n\n{url}', 1, 8, NOW()), +('linkedin', 'LinkedIn Default', '{social}\n\n{url}\n\n{hashtags}', 1, 9, NOW()), +('bluesky', 'Bluesky Default', '{short}\n\n{url}', 1, 10, NOW()), +('threads', 'Threads Default', '{social}\n\n{url}', 1, 11, NOW()), +('teams', 'Teams Default', '**{title}**\n\n{chat}\n\n[Read more]({url})', 1, 12, NOW()), +('medium', 'Medium Default', '{title}\n\n{social}\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()), +('sendgrid', 'SendGrid Default', '

{email_subject}

\n{email_body}\n

Read more

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

{email_subject}

\n{email_body}\n

Read more

', 1, 17, NOW()), +('ntfy', 'Ntfy Default', '{title}: {short}', 1, 18, NOW()), ('reddit', 'Reddit Default', '{title}', 1, 19, NOW()), -('pinterest', 'Pinterest Default', '{title} - {introtext}', 1, 20, NOW()); +('pinterest', 'Pinterest Default', '{title} - {social}', 1, 20, NOW()), +('instagram', 'Instagram Default', '{social}\n\n{hashtags}', 1, 21, NOW()), +('youtube', 'YouTube Default', '{social}\n\n{url}', 1, 22, NOW()); CREATE TABLE IF NOT EXISTS `#__mokosuitecross_category_rules` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, diff --git a/source/packages/com_mokosuitecross/src/Controller/ServiceController.php b/source/packages/com_mokosuitecross/src/Controller/ServiceController.php index 882c13a..ecdd833 100644 --- a/source/packages/com_mokosuitecross/src/Controller/ServiceController.php +++ b/source/packages/com_mokosuitecross/src/Controller/ServiceController.php @@ -96,7 +96,7 @@ class ServiceController extends FormController $app->mimeType = 'application/json'; $app->setHeader('Content-Type', 'application/json; charset=utf-8'); - echo new JsonResponse($e); + echo new JsonResponse(['error' => $e->getMessage()]); } $app->close(); diff --git a/source/packages/com_mokosuitecross/src/Helper/CrossPostDispatcher.php b/source/packages/com_mokosuitecross/src/Helper/CrossPostDispatcher.php index e6d4d09..71b5092 100644 --- a/source/packages/com_mokosuitecross/src/Helper/CrossPostDispatcher.php +++ b/source/packages/com_mokosuitecross/src/Helper/CrossPostDispatcher.php @@ -17,6 +17,7 @@ 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\MokoSuiteCrossDeleteInterface; use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; /** @@ -243,7 +244,21 @@ class CrossPostDispatcher $params = json_decode($service->params ?: '{}', true) ?: []; if (!empty($articleUrl)) { - $params['_article_url'] = $articleUrl; + $params['article_url'] = $articleUrl; + } + + // Pass article title for platforms that need it (e.g. Bluesky link cards) + $db2 = Factory::getDbo(); + $postRow = $db2->setQuery( + $db2->getQuery(true)->select('article_id')->from('#__mokosuitecross_posts')->where('id = ' . $postId) + )->loadObject(); + if ($postRow && $postRow->article_id) { + $articleTitle = $db2->setQuery( + $db2->getQuery(true)->select('title')->from('#__content')->where('id = ' . (int) $postRow->article_id) + )->loadResult(); + if ($articleTitle) { + $params['article_title'] = $articleTitle; + } } // Lifecycle event: before post @@ -383,11 +398,33 @@ class CrossPostDispatcher $authorName = $db->loadResult() ?: ''; } + // Resolve share image from article attribs + $attribs = json_decode($article->attribs ?? '{}', true) ?: []; + $imageMode = $attribs['mokosuitecross_share_image'] ?? 'intro'; + $images = json_decode($article->images ?? '{}'); $introImage = ''; - $images = json_decode($article->images ?? '{}'); - if (!empty($images->image_intro)) { - $introImage = Uri::root() . ltrim($images->image_intro, '/'); + switch ($imageMode) { + case 'fulltext': + if (!empty($images->image_fulltext)) { + $introImage = Uri::root() . ltrim($images->image_fulltext, '/'); + } + break; + case 'custom': + $customImg = $attribs['mokosuitecross_custom_image'] ?? ''; + if (!empty($customImg)) { + $introImage = Uri::root() . ltrim($customImg, '/'); + } + break; + case 'none': + $introImage = ''; + break; + case 'intro': + default: + if (!empty($images->image_intro)) { + $introImage = Uri::root() . ltrim($images->image_intro, '/'); + } + break; } $tagNames = []; @@ -410,17 +447,54 @@ class CrossPostDispatcher return '#' . preg_replace('/\s+/', '', $tag); }, $tagNames)); + // Per-article share text (from article editor Share Content panel) + $socialText = $attribs['mokosuitecross_social_text'] ?? ''; + $shortText = $attribs['mokosuitecross_short_text'] ?? ''; + $chatText = $attribs['mokosuitecross_chat_text'] ?? ''; + $emailSubject = $attribs['mokosuitecross_email_subject'] ?? ''; + $emailBody = $attribs['mokosuitecross_email_body'] ?? ''; + + $introStripped = strip_tags(mb_substr($article->introtext ?? '', 0, 280)); + $titleText = $article->title ?? ''; + + // UTM auto-tagging (#154) + $componentParams = ComponentHelper::getParams('com_mokosuitecross'); + $urlRaw = $url; + + if ($componentParams->get('utm_enabled', 0)) { + $utmParams = [ + 'utm_source' => $componentParams->get('utm_source', '{platform}'), + 'utm_medium' => $componentParams->get('utm_medium', 'social'), + 'utm_campaign' => $componentParams->get('utm_campaign', 'mokosuitecross'), + ]; + $utmContent = $componentParams->get('utm_content', ''); + + if (!empty($utmContent)) { + $utmParams['utm_content'] = $utmContent; + } + + $separator = (strpos($url, '?') !== false) ? '&' : '?'; + $url = $url . $separator . http_build_query($utmParams); + } + 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, + '{title}' => $titleText, + '{introtext}' => $introStripped, + '{fulltext}' => strip_tags(mb_substr($article->fulltext ?? '', 0, 500)), + '{url}' => $url, + '{url_raw}' => $urlRaw, + '{image}' => $introImage, + '{category}' => $categoryName, + '{author}' => $authorName, + '{date}' => Factory::getDate($article->publish_up ?? 'now')->format('Y-m-d'), + '{tags}' => $tagsComma, + '{hashtags}' => $hashtags, + // Platform-specific share content (falls back to introtext/title if empty) + '{social}' => !empty($socialText) ? $socialText : $introStripped, + '{short}' => !empty($shortText) ? $shortText : mb_substr($titleText, 0, 250), + '{chat}' => !empty($chatText) ? $chatText : $introStripped, + '{email_subject}' => !empty($emailSubject) ? $emailSubject : $titleText, + '{email_body}' => !empty($emailBody) ? $emailBody : ($article->fulltext ?? $article->introtext ?? ''), ]; } @@ -459,6 +533,15 @@ class CrossPostDispatcher $message = str_replace(array_keys($replacements), array_values($replacements), $template); + // Resolve {platform} token in UTM params (replaced with service_type) + $message = str_replace('{platform}', $service->service_type, $message); + + // Resolve caption rotation: {random:option1|option2|option3} (#155) + $message = preg_replace_callback('/\{random:([^}]+)\}/', function ($matches) { + $options = explode('|', $matches[1]); + return $options[array_rand($options)]; + }, $message); + // Resolve custom field placeholders: {field:field_name} $message = preg_replace_callback('/\{field:([a-zA-Z0-9_-]+)\}/', function ($matches) use ($db, $article) { $fieldName = $matches[1]; @@ -478,6 +561,82 @@ class CrossPostDispatcher /** * Write an entry to the activity log. */ + /** + * Delete cross-posted content from remote platforms for a given article. + * + * Finds all posts with status 'posted' for this article, resolves the + * service plugin, and calls deletePost() if the plugin supports it. + * + * @param int $articleId The Joomla article ID + */ + public static function deleteFromPlatforms(int $articleId): void + { + $db = Factory::getDbo(); + + // Find all successfully posted entries for this article + $query = $db->getQuery(true) + ->select('p.*, s.service_type, s.credentials') + ->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.article_id') . ' = ' . $articleId) + ->where($db->quoteName('p.status') . ' = ' . $db->quote('posted')) + ->where($db->quoteName('p.platform_post_id') . ' != ' . $db->quote('')); + + $db->setQuery($query); + $posts = $db->loadObjectList(); + + if (empty($posts)) { + return; + } + + // Load service plugins + PluginHelper::importPlugin('mokosuitecross'); + $plugins = []; + Factory::getApplication()->triggerEvent('onMokoSuiteCrossGetServices', [&$plugins]); + + $pluginMap = []; + foreach ($plugins as $plugin) { + $pluginMap[$plugin->getServiceType()] = $plugin; + } + + foreach ($posts as $post) { + $plugin = $pluginMap[$post->service_type] ?? null; + + if (!$plugin instanceof MokoSuiteCrossDeleteInterface) { + self::log($db, $post->id, $post->service_id, 'info', + 'Delete not supported for ' . $post->service_type); + continue; + } + + $credentials = json_decode($post->credentials, true) ?: []; + + try { + $result = $plugin->deletePost($post->platform_post_id, $credentials); + + if (!empty($result['success'])) { + // Mark as deleted + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('status') . ' = ' . $db->quote('deleted')) + ->where($db->quoteName('id') . ' = ' . (int) $post->id) + ); + $db->execute(); + + self::log($db, $post->id, $post->service_id, 'info', + 'Deleted from ' . $post->service_type . ': ' . ($result['message'] ?? 'OK')); + } else { + self::log($db, $post->id, $post->service_id, 'warning', + 'Delete failed on ' . $post->service_type . ': ' . ($result['message'] ?? 'Unknown error')); + } + } catch (\Throwable $e) { + self::log($db, $post->id, $post->service_id, 'error', + 'Delete exception on ' . $post->service_type . ': ' . $e->getMessage()); + } + } + } + private static function log($db, ?int $postId, ?int $serviceId, string $level, string $message): void { $log = (object) [ diff --git a/source/packages/com_mokosuitecross/src/Service/MokoSuiteCrossDeleteInterface.php b/source/packages/com_mokosuitecross/src/Service/MokoSuiteCrossDeleteInterface.php new file mode 100644 index 0000000..ebff724 --- /dev/null +++ b/source/packages/com_mokosuitecross/src/Service/MokoSuiteCrossDeleteInterface.php @@ -0,0 +1,35 @@ + + * @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\Service; + +defined('_JEXEC') or die; + +/** + * Optional interface for service plugins that support deleting posts + * from the remote platform. + * + * Plugins that implement this can be invoked when a Joomla article + * is unpublished or trashed, or when a user manually requests deletion + * from the Post Queue view. + */ +interface MokoSuiteCrossDeleteInterface +{ + /** + * Delete a previously published post from the remote platform. + * + * @param string $platformPostId The platform-specific post ID + * @param array $credentials Decrypted credentials for this service + * + * @return array ['success' => bool, 'message' => string] + */ + public function deletePost(string $platformPostId, array $credentials): array; +} 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 index dd1b16d..50ff417 100644 --- 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 @@ -11,3 +11,23 @@ PLG_CONTENT_MOKOSUITECROSS_EVERGREEN_DESC="Automatically re-share this article o 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" + +PLG_CONTENT_MOKOSUITECROSS_FIELDSET_SHARE="Share Content" +PLG_CONTENT_MOKOSUITECROSS_SOCIAL_TEXT="Social Media Text" +PLG_CONTENT_MOKOSUITECROSS_SOCIAL_TEXT_DESC="Custom text for Facebook, LinkedIn, Threads. Use {social} placeholder in templates. Falls back to intro text if empty." +PLG_CONTENT_MOKOSUITECROSS_SHORT_TEXT="Short Text (Twitter/Bluesky)" +PLG_CONTENT_MOKOSUITECROSS_SHORT_TEXT_DESC="Optimized text for character-limited platforms (Twitter 280, Bluesky 300). Use {short} placeholder. Falls back to truncated title." +PLG_CONTENT_MOKOSUITECROSS_CHAT_TEXT="Chat Text" +PLG_CONTENT_MOKOSUITECROSS_CHAT_TEXT_DESC="Custom text for Telegram, Discord, Slack, Teams. Use {chat} placeholder. Falls back to intro text." +PLG_CONTENT_MOKOSUITECROSS_EMAIL_SUBJECT="Email Subject" +PLG_CONTENT_MOKOSUITECROSS_EMAIL_SUBJECT_DESC="Subject line for Mailchimp, SendGrid, Brevo campaigns. Use {email_subject} placeholder. Falls back to article title." +PLG_CONTENT_MOKOSUITECROSS_EMAIL_BODY="Email Body" +PLG_CONTENT_MOKOSUITECROSS_EMAIL_BODY_DESC="HTML content for email campaigns. Use {email_body} placeholder. Falls back to full article text." +PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE="Share Image" +PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_DESC="Which image to use when cross-posting this article." +PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_INTRO="Intro Image" +PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_FULLTEXT="Full Text Image" +PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_CUSTOM="Custom Image" +PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_NONE="No Image" +PLG_CONTENT_MOKOSUITECROSS_CUSTOM_IMAGE="Custom Share Image" +PLG_CONTENT_MOKOSUITECROSS_CUSTOM_IMAGE_DESC="Select an image from the media manager to use for cross-posting." diff --git a/source/packages/plg_content_mokosuitecross/mokosuitecross.xml b/source/packages/plg_content_mokosuitecross/mokosuitecross.xml index ba6963f..730ac92 100644 --- a/source/packages/plg_content_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_content_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Content - MokoSuiteCross - 01.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php b/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php index cf5a069..59df6e7 100644 --- a/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php +++ b/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php @@ -140,6 +140,71 @@ class MokoSuiteCrossContent extends CMSPlugin implements SubscriberInterface showon="mokosuitecross_skip:0[AND]mokosuitecross_evergreen:1" />
+
+ + + + + + + + + + + + +
XML; @@ -325,12 +390,28 @@ XML; $value = func_get_arg(2); } - if ($context !== 'com_content.article' || $value !== 1) { + if ($context !== 'com_content.article') { return; } $params = ComponentHelper::getParams('com_mokosuitecross'); + // Unpublish/trash: delete from platforms if configured + if ($value === 0 || $value === -2) { + if ($params->get('delete_on_unpublish', 0)) { + foreach ($pks as $pk) { + CrossPostDispatcher::deleteFromPlatforms((int) $pk); + } + } + + return; + } + + // Publish: auto-post if configured + if ($value !== 1) { + return; + } + if (!$params->get('auto_post_on_publish', 1)) { return; } diff --git a/source/packages/plg_mokosuitecross_activitypub/activitypub.xml b/source/packages/plg_mokosuitecross_activitypub/activitypub.xml index 7b37e77..1dd3a10 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.04.01 + 01.04.12-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 3515698..3595095 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.04.01 + 01.04.12-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 7324023..1364e65 100644 --- a/source/packages/plg_mokosuitecross_bluesky/bluesky.xml +++ b/source/packages/plg_mokosuitecross_bluesky/bluesky.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Bluesky - 01.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech @@ -10,7 +10,7 @@ GPL-3.0-or-later PLG_MOKOSUITECROSS_BLUESKY_DESCRIPTION - Joomla\Plugin\MokoSuiteCross${CLASS_NAME} + Joomla\Plugin\MokoSuiteCross\Bluesky bluesky.php diff --git a/source/packages/plg_mokosuitecross_bluesky/src/Extension/BlueskyService.php b/source/packages/plg_mokosuitecross_bluesky/src/Extension/BlueskyService.php index 7efb369..04dad2c 100644 --- a/source/packages/plg_mokosuitecross_bluesky/src/Extension/BlueskyService.php +++ b/source/packages/plg_mokosuitecross_bluesky/src/Extension/BlueskyService.php @@ -14,6 +14,7 @@ namespace Joomla\Plugin\MokoSuiteCross\Bluesky\Extension; defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossDeleteInterface; use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Event\SubscriberInterface; @@ -29,7 +30,7 @@ use Joomla\Event\SubscriberInterface; * "pds_url": "https://bsky.social" // Optional, defaults to bsky.social * } */ -class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface +class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface, MokoSuiteCrossDeleteInterface { public static function getSubscribedEvents(): array { @@ -65,49 +66,95 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuite return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Authentication failed']]; } - // Create post - $postData = json_encode([ - 'repo' => $authData['did'], - 'collection' => 'app.bsky.feed.post', - 'record' => [ + // Build external link card embed if URL is in params + $embed = null; + $articleUrl = $params['article_url'] ?? ''; + if (!empty($articleUrl)) { + $embed = [ + '$type' => 'app.bsky.embed.external', + 'external' => [ + 'uri' => $articleUrl, + 'title' => $params['article_title'] ?? '', + 'description' => mb_substr(strip_tags($message), 0, 200), + ], + ]; + } + + // Auto-thread: split long messages at sentence boundaries + $chunks = $this->splitIntoThread($message, 300); + + if (count($chunks) === 1) { + // Single post + return $this->createPost($pds, $authData, $chunks[0], $embed); + } + + // Thread: post each chunk as a reply to the previous + $rootUri = null; + $rootCid = null; + $parentUri = null; + $parentCid = null; + $lastResult = []; + + foreach ($chunks as $i => $chunk) { + $record = [ '$type' => 'app.bsky.feed.post', - 'text' => mb_substr($message, 0, 300), + 'text' => $chunk, 'createdAt' => gmdate('Y-m-d\TH:i:s\Z'), - ], - ]); + ]; - $ch = curl_init($pds . '/xrpc/com.atproto.repo.createRecord'); - curl_setopt_array($ch, [ - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $postData, - CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $authData['accessJwt'], 'Content-Type: application/json'], - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 30, - CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP, - CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP, - ]); + // Add reply reference for thread posts after the first + if ($rootUri !== null) { + $record['reply'] = [ + 'root' => ['uri' => $rootUri, 'cid' => $rootCid], + 'parent' => ['uri' => $parentUri, 'cid' => $parentCid], + ]; + } - $response = curl_exec($ch); + // Attach link card embed to last post only + if ($embed !== null && $i === count($chunks) - 1) { + $record['embed'] = $embed; + } - if ($response === false) { + $postData = json_encode([ + 'repo' => $authData['did'], + 'collection' => 'app.bsky.feed.post', + 'record' => $record, + ]); - $curlError = curl_error($ch); + $ch = curl_init($pds . '/xrpc/com.atproto.repo.createRecord'); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $authData['accessJwt'], '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' => $rootUri ?? '', 'response' => ['error' => 'Thread error at post ' . ($i + 1) . ': ' . $curlError]]; + } + + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); + $data = json_decode($response, true) ?: []; - return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]]; + if ($httpCode !== 200 || empty($data['uri'])) { + return ['success' => false, 'platform_post_id' => $rootUri ?? '', 'response' => $data]; + } - } - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - $data = json_decode($response, true) ?: []; - - if ($httpCode === 200 && !empty($data['uri'])) { - return ['success' => true, 'platform_post_id' => $data['uri'], 'response' => $data]; + if ($rootUri === null) { + $rootUri = $data['uri']; + $rootCid = $data['cid']; + } + $parentUri = $data['uri']; + $parentCid = $data['cid']; + $lastResult = $data; } - return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + return ['success' => true, 'platform_post_id' => $rootUri, 'response' => array_merge($lastResult, ['thread_count' => count($chunks)])]; } public function validateCredentials(array $credentials): array @@ -127,7 +174,7 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuite private function authenticateWithCache(string $pds, string $handle, string $appPwd): array { - $cacheKey = md5($pds . $handle); + $cacheKey = hash('sha256', $pds . $handle); if (isset(self::$sessionCache[$cacheKey])) { $cached = self::$sessionCache[$cacheKey]; @@ -175,6 +222,157 @@ class BlueskyService extends CMSPlugin implements SubscriberInterface, MokoSuite return json_decode($response, true) ?: []; } + /** + * Create a single Bluesky post (used for non-threaded messages). + */ + private function createPost(string $pds, array $authData, string $text, ?array $embed = null): array + { + $record = [ + '$type' => 'app.bsky.feed.post', + 'text' => $text, + 'createdAt' => gmdate('Y-m-d\TH:i:s\Z'), + ]; + + if ($embed !== null) { + $record['embed'] = $embed; + } + + $postData = json_encode([ + 'repo' => $authData['did'], + 'collection' => 'app.bsky.feed.post', + 'record' => $record, + ]); + + $ch = curl_init($pds . '/xrpc/com.atproto.repo.createRecord'); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $authData['accessJwt'], '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 && !empty($data['uri'])) { + return ['success' => true, 'platform_post_id' => $data['uri'], 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + /** + * Split a long message into thread-sized chunks at sentence boundaries. + */ + private function splitIntoThread(string $message, int $maxLength): array + { + if (mb_strlen($message) <= $maxLength) { + return [$message]; + } + + $chunks = []; + $remaining = $message; + + while (mb_strlen($remaining) > $maxLength) { + $segment = mb_substr($remaining, 0, $maxLength); + + // Try to break at last sentence boundary (. ! ? followed by space) + $breakPos = max( + mb_strrpos($segment, '. ') ?: 0, + mb_strrpos($segment, '! ') ?: 0, + mb_strrpos($segment, '? ') ?: 0 + ); + + if ($breakPos < $maxLength * 0.3) { + // No good sentence break; try last space + $breakPos = mb_strrpos($segment, ' ') ?: $maxLength; + } else { + $breakPos += 1; // Include the punctuation + } + + $chunks[] = trim(mb_substr($remaining, 0, $breakPos)); + $remaining = trim(mb_substr($remaining, $breakPos)); + } + + if (!empty($remaining)) { + $chunks[] = $remaining; + } + + return $chunks; + } + + public function deletePost(string $platformPostId, array $credentials): array + { + $pds = rtrim($credentials['pds_url'] ?? 'https://bsky.social', '/'); + $handle = $credentials['handle'] ?? ''; + $appPwd = $credentials['app_password'] ?? ''; + + if (empty($handle) || empty($appPwd)) { + return ['success' => false, 'message' => 'Missing credentials.']; + } + + // Parse AT URI: at://did:plc:xxx/app.bsky.feed.post/rkey + $parts = explode('/', $platformPostId); + $rkey = end($parts); + + if (empty($rkey)) { + return ['success' => false, 'message' => 'Invalid AT URI -- could not extract rkey.']; + } + + // Authenticate (uses cached session if still valid) + $authData = $this->authenticateWithCache($pds, $handle, $appPwd); + + if (empty($authData['accessJwt'])) { + return ['success' => false, 'message' => 'Authentication failed.']; + } + + $postData = json_encode([ + 'repo' => $authData['did'], + 'collection' => 'app.bsky.feed.post', + 'rkey' => $rkey, + ]); + + $ch = curl_init($pds . '/xrpc/com.atproto.repo.deleteRecord'); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $authData['accessJwt'], '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, 'message' => 'Connection error: ' . $curlError]; + } + + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200) { + return ['success' => true, 'message' => 'Post deleted successfully.']; + } + + $data = json_decode($response, true) ?: []; + + return ['success' => false, 'message' => $data['message'] ?? 'Delete failed with HTTP ' . $httpCode]; + } + public function getSupportedMediaTypes(): array { return ['image']; diff --git a/source/packages/plg_mokosuitecross_brevo/brevo.xml b/source/packages/plg_mokosuitecross_brevo/brevo.xml index 2b2271a..29848a8 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.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_brevo/src/Extension/BrevoService.php b/source/packages/plg_mokosuitecross_brevo/src/Extension/BrevoService.php index fbf55f3..42beb73 100644 --- a/source/packages/plg_mokosuitecross_brevo/src/Extension/BrevoService.php +++ b/source/packages/plg_mokosuitecross_brevo/src/Extension/BrevoService.php @@ -70,15 +70,6 @@ class BrevoService extends CMSPlugin implements SubscriberInterface, MokoSuiteCr CURLOPT_TIMEOUT => 30, ]); - curl_setopt_array($ch, [ - CURLOPT_URL => 'https://api.brevo.com/v3/emailCampaigns', - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $postData, - CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'], - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 30, - ]); - $response = curl_exec($ch); if ($response === false) { diff --git a/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml index c6f4265..920c09b 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.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_constantcontact/src/Extension/ConstantcontactService.php b/source/packages/plg_mokosuitecross_constantcontact/src/Extension/ConstantcontactService.php index e3ae973..eb38c64 100644 --- a/source/packages/plg_mokosuitecross_constantcontact/src/Extension/ConstantcontactService.php +++ b/source/packages/plg_mokosuitecross_constantcontact/src/Extension/ConstantcontactService.php @@ -73,15 +73,6 @@ class ConstantcontactService extends CMSPlugin implements SubscriberInterface, M CURLOPT_TIMEOUT => 30, ]); - curl_setopt_array($ch, [ - CURLOPT_URL => 'https://api.cc.email/v3/emails', - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $postData, - CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'], - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 30, - ]); - $response = curl_exec($ch); if ($response === false) { diff --git a/source/packages/plg_mokosuitecross_convertkit/convertkit.xml b/source/packages/plg_mokosuitecross_convertkit/convertkit.xml index 63d3c19..ff9e718 100644 --- a/source/packages/plg_mokosuitecross_convertkit/convertkit.xml +++ b/source/packages/plg_mokosuitecross_convertkit/convertkit.xml @@ -1,7 +1,7 @@ MokoSuiteCross - ConvertKit - 01.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_convertkit/src/Extension/ConvertkitService.php b/source/packages/plg_mokosuitecross_convertkit/src/Extension/ConvertkitService.php index 2a296f2..d2a48cf 100644 --- a/source/packages/plg_mokosuitecross_convertkit/src/Extension/ConvertkitService.php +++ b/source/packages/plg_mokosuitecross_convertkit/src/Extension/ConvertkitService.php @@ -66,15 +66,6 @@ class ConvertkitService extends CMSPlugin implements SubscriberInterface, MokoSu CURLOPT_TIMEOUT => 30, ]); - curl_setopt_array($ch, [ - CURLOPT_URL => 'https://api.convertkit.com/v3/broadcasts', - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $postData, - CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'], - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 30, - ]); - $response = curl_exec($ch); if ($response === false) { diff --git a/source/packages/plg_mokosuitecross_devto/devto.xml b/source/packages/plg_mokosuitecross_devto/devto.xml index f9f873f..18ac26b 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.04.01 + 01.04.12-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 6a77882..e01b373 100644 --- a/source/packages/plg_mokosuitecross_discord/discord.xml +++ b/source/packages/plg_mokosuitecross_discord/discord.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Discord - 01.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech @@ -10,7 +10,7 @@ GPL-3.0-or-later PLG_MOKOSUITECROSS_DISCORD_DESCRIPTION - Joomla\Plugin\MokoSuiteCross${CLASS_NAME} + Joomla\Plugin\MokoSuiteCross\Discord discord.php diff --git a/source/packages/plg_mokosuitecross_discord/src/Extension/DiscordService.php b/source/packages/plg_mokosuitecross_discord/src/Extension/DiscordService.php index 28f5d5c..f488df7 100644 --- a/source/packages/plg_mokosuitecross_discord/src/Extension/DiscordService.php +++ b/source/packages/plg_mokosuitecross_discord/src/Extension/DiscordService.php @@ -14,6 +14,7 @@ namespace Joomla\Plugin\MokoSuiteCross\Discord\Extension; defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossDeleteInterface; use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Event\SubscriberInterface; @@ -30,7 +31,7 @@ use Joomla\Event\SubscriberInterface; * "webhook_url": "https://discord.com/api/webhooks/..." // Only for custom mode * } */ -class DiscordService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface +class DiscordService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface, MokoSuiteCrossDeleteInterface { public static function getSubscribedEvents(): array { @@ -126,6 +127,44 @@ class DiscordService extends CMSPlugin implements SubscriberInterface, MokoSuite return $this->params->get('default_webhook_url', ''); } + public function deletePost(string $platformPostId, array $credentials): array + { + $webhookUrl = $this->resolveWebhook($credentials); + + if (empty($webhookUrl)) { + return ['success' => false, 'message' => 'Missing webhook URL.']; + } + + $apiUrl = $webhookUrl . '/messages/' . $platformPostId; + + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_CUSTOMREQUEST => 'DELETE', + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + ]); + + $response = curl_exec($ch); + + if ($response === false) { + $curlError = curl_error($ch); + curl_close($ch); + + return ['success' => false, 'message' => 'Connection error: ' . $curlError]; + } + + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 204) { + return ['success' => true, 'message' => 'Message deleted successfully.']; + } + + $data = json_decode($response, true) ?: []; + + return ['success' => false, 'message' => $data['message'] ?? 'Delete failed (HTTP ' . $httpCode . ').']; + } + public function getSupportedMediaTypes(): array { return ['image', 'video']; diff --git a/source/packages/plg_mokosuitecross_facebook/facebook.xml b/source/packages/plg_mokosuitecross_facebook/facebook.xml index 9b9cd38..064ccd6 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.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech @@ -10,7 +10,7 @@ GPL-3.0-or-later PLG_MOKOSUITECROSS_FACEBOOK_DESCRIPTION - Joomla\Plugin\MokoSuiteCross${CLASS_NAME} + Joomla\Plugin\MokoSuiteCross\Facebook facebook.php diff --git a/source/packages/plg_mokosuitecross_facebook/src/Extension/FacebookService.php b/source/packages/plg_mokosuitecross_facebook/src/Extension/FacebookService.php index 4f78b5e..7812f16 100644 --- a/source/packages/plg_mokosuitecross_facebook/src/Extension/FacebookService.php +++ b/source/packages/plg_mokosuitecross_facebook/src/Extension/FacebookService.php @@ -14,6 +14,7 @@ namespace Joomla\Plugin\MokoSuiteCross\Facebook\Extension; defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossDeleteInterface; use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Event\SubscriberInterface; @@ -31,7 +32,7 @@ use Joomla\Event\SubscriberInterface; * "page_id": "..." // Required — Facebook Page ID * } */ -class FacebookService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface +class FacebookService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface, MokoSuiteCrossDeleteInterface { public static function getSubscribedEvents(): array { @@ -161,6 +162,44 @@ class FacebookService extends CMSPlugin implements SubscriberInterface, MokoSuit return $this->params->get('default_page_access_token', ''); } + public function deletePost(string $platformPostId, array $credentials): array + { + $token = $this->resolveToken($credentials); + + if (empty($token)) { + return ['success' => false, 'message' => 'Missing access token.']; + } + + $apiUrl = 'https://graph.facebook.com/v19.0/' . $platformPostId . '?access_token=' . $token; + + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_CUSTOMREQUEST => 'DELETE', + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + ]); + + $response = curl_exec($ch); + + if ($response === false) { + $curlError = curl_error($ch); + curl_close($ch); + + return ['success' => false, 'message' => 'Connection error: ' . $curlError]; + } + + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $data = json_decode($response, true) ?: []; + + if ($httpCode === 200 && !empty($data['success'])) { + return ['success' => true, 'message' => 'Post deleted successfully.']; + } + + return ['success' => false, 'message' => $data['error']['message'] ?? 'Delete failed (HTTP ' . $httpCode . ').']; + } + public function getSupportedMediaTypes(): array { return ['image', 'video', 'gif']; diff --git a/source/packages/plg_mokosuitecross_ghost/ghost.xml b/source/packages/plg_mokosuitecross_ghost/ghost.xml index 57fbf10..a41eb23 100644 --- a/source/packages/plg_mokosuitecross_ghost/ghost.xml +++ b/source/packages/plg_mokosuitecross_ghost/ghost.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Ghost - 01.04.01 + 01.04.12-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 479643f..3988dbd 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.04.01 + 01.04.12-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 9836127..a9c46fa 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.04.01 + 01.04.12-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 df5f53a..38f5a5a 100644 --- a/source/packages/plg_mokosuitecross_hashnode/hashnode.xml +++ b/source/packages/plg_mokosuitecross_hashnode/hashnode.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Hashnode - 01.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_instagram/index.html b/source/packages/plg_mokosuitecross_instagram/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_instagram/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_instagram/instagram.php b/source/packages/plg_mokosuitecross_instagram/instagram.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_instagram/instagram.php @@ -0,0 +1,11 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +defined('_JEXEC') or die; diff --git a/source/packages/plg_mokosuitecross_instagram/instagram.xml b/source/packages/plg_mokosuitecross_instagram/instagram.xml new file mode 100644 index 0000000..ed0a60f --- /dev/null +++ b/source/packages/plg_mokosuitecross_instagram/instagram.xml @@ -0,0 +1,39 @@ + + + MokoSuiteCross - Instagram + 01.04.12-dev + 2026-06-23 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_INSTAGRAM_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Instagram + + + instagram.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_instagram.ini + language/en-GB/plg_mokosuitecross_instagram.sys.ini + + + +
+ +
+
+
+
diff --git a/source/packages/plg_mokosuitecross_instagram/language/en-GB/index.html b/source/packages/plg_mokosuitecross_instagram/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_instagram/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_instagram/language/en-GB/plg_mokosuitecross_instagram.ini b/source/packages/plg_mokosuitecross_instagram/language/en-GB/plg_mokosuitecross_instagram.ini new file mode 100644 index 0000000..30f7136 --- /dev/null +++ b/source/packages/plg_mokosuitecross_instagram/language/en-GB/plg_mokosuitecross_instagram.ini @@ -0,0 +1,5 @@ +PLG_MOKOSUITECROSS_INSTAGRAM="MokoSuiteCross - Instagram" +PLG_MOKOSUITECROSS_INSTAGRAM_DESCRIPTION="Cross-post Joomla articles to Instagram via Meta Content Publishing API." +PLG_MOKOSUITECROSS_INSTAGRAM_FIELDSET_DEFAULTS="Default Settings" +PLG_MOKOSUITECROSS_INSTAGRAM_DEFAULT_WEBHOOK="Default Webhook URL" +PLG_MOKOSUITECROSS_INSTAGRAM_DEFAULT_WEBHOOK_DESC="Pre-configured MokoSuite webhook URL. Services using default mode will use this URL." diff --git a/source/packages/plg_mokosuitecross_instagram/language/en-GB/plg_mokosuitecross_instagram.sys.ini b/source/packages/plg_mokosuitecross_instagram/language/en-GB/plg_mokosuitecross_instagram.sys.ini new file mode 100644 index 0000000..6f74ebb --- /dev/null +++ b/source/packages/plg_mokosuitecross_instagram/language/en-GB/plg_mokosuitecross_instagram.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_INSTAGRAM="MokoSuiteCross - Instagram" +PLG_MOKOSUITECROSS_INSTAGRAM_DESCRIPTION="Cross-post Joomla articles to Instagram via Meta Content Publishing API." diff --git a/source/packages/plg_mokosuitecross_instagram/language/index.html b/source/packages/plg_mokosuitecross_instagram/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_instagram/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_instagram/services/index.html b/source/packages/plg_mokosuitecross_instagram/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_instagram/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_instagram/services/provider.php b/source/packages/plg_mokosuitecross_instagram/services/provider.php new file mode 100644 index 0000000..985e45d --- /dev/null +++ b/source/packages/plg_mokosuitecross_instagram/services/provider.php @@ -0,0 +1,38 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Joomla\Plugin\MokoSuiteCross\Instagram\Extension\InstagramService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new InstagramService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'instagram') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_instagram/src/Extension/InstagramService.php b/source/packages/plg_mokosuitecross_instagram/src/Extension/InstagramService.php new file mode 100644 index 0000000..693d5da --- /dev/null +++ b/source/packages/plg_mokosuitecross_instagram/src/Extension/InstagramService.php @@ -0,0 +1,188 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +namespace Joomla\Plugin\MokoSuiteCross\Instagram\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Instagram service plugin for MokoSuiteCross. + * + * Uses the Meta Content Publishing API — a 2-step flow: + * 1. Create a media container via POST /{ig_user_id}/media + * 2. Publish the container via POST /{ig_user_id}/media_publish + */ +class InstagramService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface +{ + public static function getSubscribedEvents(): array + { + return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices']; + } + + public function onMokoSuiteCrossGetServices(&$services): void + { + $services[] = $this; + } + + public function getServiceType(): string { return 'instagram'; } + public function getServiceName(): string { return 'Instagram'; } + public function getMaxLength(): int { return 2200; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $token = $this->resolveCredential($credentials, 'access_token'); + $accountId = $credentials['instagram_account_id'] ?? ''; + + if (empty($token) || empty($accountId)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or Instagram account ID.']]; + } + + // Step 1: Create media container + $containerUrl = 'https://graph.facebook.com/v19.0/' . urlencode($accountId) . '/media'; + $containerData = [ + 'caption' => mb_substr($message, 0, 2200), + 'access_token' => $token, + ]; + + // Attach image if provided + if (!empty($media[0])) { + $containerData['image_url'] = $media[0]; + } else { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Instagram requires at least one image or video.']]; + } + + $ch = curl_init($containerUrl); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query($containerData), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + ]); + + $response = curl_exec($ch); + + if ($response === false) { + + $curlError = curl_error($ch); + + curl_close($ch); + + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]]; + + } + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $data = json_decode($response, true) ?: []; + + if ($httpCode < 200 || $httpCode >= 300 || empty($data['id'])) { + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + $containerId = $data['id']; + + // Step 2: Publish the container + $publishUrl = 'https://graph.facebook.com/v19.0/' . urlencode($accountId) . '/media_publish'; + $publishData = [ + 'creation_id' => $containerId, + 'access_token' => $token, + ]; + + $ch = curl_init($publishUrl); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query($publishData), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + ]); + + $response = curl_exec($ch); + + if ($response === false) { + + $curlError = curl_error($ch); + + curl_close($ch); + + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]]; + + } + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $data = json_decode($response, true) ?: []; + + if ($httpCode >= 200 && $httpCode < 300 && !empty($data['id'])) { + return ['success' => true, 'platform_post_id' => (string) $data['id'], 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $token = $this->resolveCredential($credentials, 'access_token'); + $accountId = $credentials['instagram_account_id'] ?? ''; + + if (empty($token) || empty($accountId)) { + return ['valid' => false, 'message' => 'Access token and Instagram account ID are required.', 'account_name' => '']; + } + + $ch = curl_init('https://graph.facebook.com/v19.0/me?fields=id,username&access_token=' . urlencode($token)); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + ]); + + $response = curl_exec($ch); + + if ($response === false) { + + $curlError = curl_error($ch); + + curl_close($ch); + + return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => '']; + + } + curl_close($ch); + + $data = json_decode($response, true) ?: []; + + if (!empty($data['id'])) { + $name = $data['username'] ?? $data['id']; + return ['valid' => true, 'message' => 'Connected', 'account_name' => '@' . $name]; + } + + return ['valid' => false, 'message' => $data['error']['message'] ?? 'Failed to verify credentials.', 'account_name' => '']; + } + + private function resolveCredential(array $credentials, string $key): string + { + $mode = $credentials['mode'] ?? 'default'; + + if ($mode === 'custom') { + return $credentials[$key] ?? ''; + } + + return $this->params->get('default_' . $key, ''); + } + + public function getSupportedMediaTypes(): array + { + return ['image', 'video']; + } +} diff --git a/source/packages/plg_mokosuitecross_instagram/src/Extension/index.html b/source/packages/plg_mokosuitecross_instagram/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_instagram/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_instagram/src/index.html b/source/packages/plg_mokosuitecross_instagram/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_instagram/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_linkedin/linkedin.xml b/source/packages/plg_mokosuitecross_linkedin/linkedin.xml index 4859781..fef965a 100644 --- a/source/packages/plg_mokosuitecross_linkedin/linkedin.xml +++ b/source/packages/plg_mokosuitecross_linkedin/linkedin.xml @@ -1,7 +1,7 @@ MokoSuiteCross - LinkedIn - 01.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech @@ -10,7 +10,7 @@ GPL-3.0-or-later PLG_MOKOSUITECROSS_LINKEDIN_DESCRIPTION - Joomla\Plugin\MokoSuiteCross${CLASS_NAME} + Joomla\Plugin\MokoSuiteCross\Linkedin linkedin.php diff --git a/source/packages/plg_mokosuitecross_linkedin/src/Extension/LinkedinService.php b/source/packages/plg_mokosuitecross_linkedin/src/Extension/LinkedinService.php index b1dbd10..5add24a 100644 --- a/source/packages/plg_mokosuitecross_linkedin/src/Extension/LinkedinService.php +++ b/source/packages/plg_mokosuitecross_linkedin/src/Extension/LinkedinService.php @@ -14,6 +14,7 @@ namespace Joomla\Plugin\MokoSuiteCross\Linkedin\Extension; defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossDeleteInterface; use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Event\SubscriberInterface; @@ -29,7 +30,7 @@ use Joomla\Event\SubscriberInterface; * "person_id": "..." // LinkedIn Person URN (fallback) * } */ -class LinkedinService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface +class LinkedinService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface, MokoSuiteCrossDeleteInterface { public static function getSubscribedEvents(): array { @@ -147,6 +148,46 @@ class LinkedinService extends CMSPlugin implements SubscriberInterface, MokoSuit return true; } + public function deletePost(string $platformPostId, array $credentials): array + { + $token = $credentials['access_token'] ?? ''; + + if (empty($token)) { + return ['success' => false, 'message' => 'Missing access token.']; + } + + $encodedId = urlencode($platformPostId); + $apiUrl = 'https://api.linkedin.com/v2/ugcPosts/' . $encodedId; + + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_CUSTOMREQUEST => 'DELETE', + 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, 'message' => 'Connection error: ' . $curlError]; + } + + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 204) { + return ['success' => true, 'message' => 'Post deleted successfully.']; + } + + $data = json_decode($response, true) ?: []; + + return ['success' => false, 'message' => $data['message'] ?? 'Delete failed (HTTP ' . $httpCode . ').']; + } + public function getSupportedMediaTypes(): array { return ['image', 'video']; 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 index d688e32..fd8fd77 100644 --- 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 @@ -7,3 +7,8 @@ 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." +PLG_MOKOSUITECROSS_MAILCHIMP_FIELDSET_TEMPLATE="Email Template" +PLG_MOKOSUITECROSS_MAILCHIMP_TEMPLATE_ID="Mailchimp Template ID" +PLG_MOKOSUITECROSS_MAILCHIMP_TEMPLATE_ID_DESC="Numeric ID of a saved Mailchimp template. Article content is injected into the template section. Leave empty to use the built-in responsive wrapper." +PLG_MOKOSUITECROSS_MAILCHIMP_TEMPLATE_SECTION="Template Section Name" +PLG_MOKOSUITECROSS_MAILCHIMP_TEMPLATE_SECTION_DESC="The editable section name in your Mailchimp template where article content is injected. Default: body_content." diff --git a/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml index 9ee4511..cc034b9 100644 --- a/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml +++ b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Mailchimp - 01.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech @@ -10,7 +10,7 @@ GPL-3.0-or-later PLG_MOKOSUITECROSS_MAILCHIMP_DESCRIPTION - Joomla\Plugin\MokoSuiteCross${CLASS_NAME} + Joomla\Plugin\MokoSuiteCross\Mailchimp mailchimp.php @@ -51,6 +51,24 @@
+
+ + +
diff --git a/source/packages/plg_mokosuitecross_mailchimp/src/Extension/MailchimpService.php b/source/packages/plg_mokosuitecross_mailchimp/src/Extension/MailchimpService.php index d56bffb..125f10e 100644 --- a/source/packages/plg_mokosuitecross_mailchimp/src/Extension/MailchimpService.php +++ b/source/packages/plg_mokosuitecross_mailchimp/src/Extension/MailchimpService.php @@ -95,14 +95,30 @@ class MailchimpService extends CMSPlugin implements SubscriberInterface, MokoSui $data = json_decode($response, true) ?: []; - if ($httpCode !== 200 || empty($data['id'])) { + if ($httpCode < 200 || $httpCode >= 300 || empty($data['id'])) { return ['success' => false, 'platform_post_id' => '', 'response' => $data]; } $campaignId = $data['id']; - // Set campaign content (HTML) - $contentData = json_encode(['html' => $message]); + // Set campaign content — template injection or responsive wrapper + $templateId = (int) $this->params->get('template_id', 0); + $templateSection = $this->params->get('template_section', 'body_content'); + + if ($templateId > 0) { + // Inject article content into a saved Mailchimp template section + $contentData = json_encode([ + 'template' => [ + 'id' => $templateId, + 'sections' => [ + $templateSection => $message, + ], + ], + ]); + } else { + // Wrap in responsive email skeleton + $contentData = json_encode(['html' => $this->wrapEmailHtml($message)]); + } $ch = curl_init("https://{$dc}.api.mailchimp.com/3.0/campaigns/{$campaignId}/content"); curl_setopt_array($ch, [ @@ -185,6 +201,27 @@ class MailchimpService extends CMSPlugin implements SubscriberInterface, MokoSui return end($parts) ?: 'us1'; } + /** + * Wrap content in a responsive email HTML skeleton. + * Used when no Mailchimp template ID is configured. + */ + private function wrapEmailHtml(string $content): string + { + return '' + . '' + . '' + . '' + . '' + . '
' + . '' + . '' + . '
' + . $content + . '
' + . '
' + . ''; + } + public function getSupportedMediaTypes(): array { return ['image']; diff --git a/source/packages/plg_mokosuitecross_mastodon/mastodon.xml b/source/packages/plg_mokosuitecross_mastodon/mastodon.xml index 36343b4..9cc84e5 100644 --- a/source/packages/plg_mokosuitecross_mastodon/mastodon.xml +++ b/source/packages/plg_mokosuitecross_mastodon/mastodon.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Mastodon - 01.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech @@ -10,7 +10,7 @@ GPL-3.0-or-later PLG_MOKOSUITECROSS_MASTODON_DESCRIPTION - Joomla\Plugin\MokoSuiteCross${CLASS_NAME} + Joomla\Plugin\MokoSuiteCross\Mastodon mastodon.php diff --git a/source/packages/plg_mokosuitecross_mastodon/src/Extension/MastodonService.php b/source/packages/plg_mokosuitecross_mastodon/src/Extension/MastodonService.php index 397a538..29c3f85 100644 --- a/source/packages/plg_mokosuitecross_mastodon/src/Extension/MastodonService.php +++ b/source/packages/plg_mokosuitecross_mastodon/src/Extension/MastodonService.php @@ -14,6 +14,7 @@ namespace Joomla\Plugin\MokoSuiteCross\Mastodon\Extension; defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossDeleteInterface; use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Event\SubscriberInterface; @@ -26,7 +27,7 @@ use Joomla\Event\SubscriberInterface; * "access_token": "..." * } */ -class MastodonService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface +class MastodonService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface, MokoSuiteCrossDeleteInterface { public static function getSubscribedEvents(): array { @@ -52,10 +53,46 @@ class MastodonService extends CMSPlugin implements SubscriberInterface, MokoSuit return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing credentials']]; } + // Build status payload with optional Mastodon features + $postBody = ['status' => mb_substr($message, 0, 500)]; + + // Visibility: public (default), unlisted, private, direct + $visibility = $params['visibility'] ?? $this->params->get('default_visibility', 'public'); + if (in_array($visibility, ['public', 'unlisted', 'private', 'direct'], true)) { + $postBody['visibility'] = $visibility; + } + + // Content warning / spoiler text + $spoiler = $params['spoiler_text'] ?? ''; + if (!empty($spoiler)) { + $postBody['spoiler_text'] = $spoiler; + } + + // Scheduled posting (must be 5+ minutes in future) + $scheduledAt = $params['scheduled_at'] ?? ''; + if (!empty($scheduledAt)) { + $postBody['scheduled_at'] = $scheduledAt; + } + + // Poll support (mutually exclusive with media) + if (!empty($params['poll']['options']) && empty($media)) { + $postBody['poll'] = [ + 'options' => $params['poll']['options'], + 'expires_in' => (int) ($params['poll']['expires_in'] ?? 86400), + 'multiple' => !empty($params['poll']['multiple']), + ]; + } + + // Language tag + $language = $params['language'] ?? ''; + if (!empty($language)) { + $postBody['language'] = $language; + } + $ch = curl_init($instance . '/api/v1/statuses'); curl_setopt_array($ch, [ CURLOPT_POST => true, - CURLOPT_POSTFIELDS => json_encode(['status' => mb_substr($message, 0, 500)]), + CURLOPT_POSTFIELDS => json_encode($postBody), CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, @@ -120,6 +157,46 @@ class MastodonService extends CMSPlugin implements SubscriberInterface, MokoSuit return ['valid' => false, 'message' => 'Failed', 'account_name' => '']; } + public function deletePost(string $platformPostId, array $credentials): array + { + $instance = rtrim($credentials['instance_url'] ?? '', '/'); + $token = $credentials['access_token'] ?? ''; + + if (empty($instance) || empty($token)) { + return ['success' => false, 'message' => 'Missing credentials.']; + } + + $ch = curl_init($instance . '/api/v1/statuses/' . $platformPostId); + curl_setopt_array($ch, [ + CURLOPT_CUSTOMREQUEST => 'DELETE', + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token], + 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, 'message' => 'Connection error: ' . $curlError]; + } + + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200) { + return ['success' => true, 'message' => 'Status deleted successfully.']; + } + + $data = json_decode($response, true) ?: []; + + return ['success' => false, 'message' => $data['error'] ?? 'Delete failed with HTTP ' . $httpCode]; + } + public function getSupportedMediaTypes(): array { return ['image', 'video', 'gif']; diff --git a/source/packages/plg_mokosuitecross_matrix/matrix.xml b/source/packages/plg_mokosuitecross_matrix/matrix.xml index 4e14c45..c4f16a3 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.04.01 + 01.04.12-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 ab12cfa..6be06f2 100644 --- a/source/packages/plg_mokosuitecross_medium/medium.xml +++ b/source/packages/plg_mokosuitecross_medium/medium.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Medium - 01.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_medium/src/Extension/MediumService.php b/source/packages/plg_mokosuitecross_medium/src/Extension/MediumService.php index 37dfb77..44391f5 100644 --- a/source/packages/plg_mokosuitecross_medium/src/Extension/MediumService.php +++ b/source/packages/plg_mokosuitecross_medium/src/Extension/MediumService.php @@ -163,7 +163,7 @@ class MediumService extends CMSPlugin implements SubscriberInterface, MokoSuiteC curl_close($ch); - return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => '']; + return ''; } curl_close($ch); diff --git a/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml index f56938d..3ee603c 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.04.01 + 01.04.12-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 6e11dc8..34516bf 100644 --- a/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml +++ b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml @@ -1,7 +1,7 @@ MokoSuiteCross - MokoSuiteGallery - 01.04.01 + 01.04.12-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 5352baf..b08e054 100644 --- a/source/packages/plg_mokosuitecross_nostr/nostr.xml +++ b/source/packages/plg_mokosuitecross_nostr/nostr.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Nostr - 01.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech 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 index 9d9d2ef..9947959 100644 --- 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 @@ -1,2 +1,7 @@ PLG_MOKOSUITECROSS_NTFY="MokoSuiteCross - Ntfy Push Notifications" -PLG_MOKOSUITECROSS_NTFY_DESCRIPTION="Cross-post Joomla articles to Ntfy Push Notifications." +PLG_MOKOSUITECROSS_NTFY_DESCRIPTION="Cross-post Joomla articles to Ntfy push notifications. Default server: ntfy.mokoconsulting.tech." +PLG_MOKOSUITECROSS_NTFY_FIELDSET_DEFAULTS="Ntfy Defaults" +PLG_MOKOSUITECROSS_NTFY_DEFAULT_SERVER_URL="Default Server URL" +PLG_MOKOSUITECROSS_NTFY_DEFAULT_SERVER_URL_DESC="Default ntfy server URL. Override per-service in credentials. Default: https://ntfy.mokoconsulting.tech" +PLG_MOKOSUITECROSS_NTFY_DEFAULT_TOPIC="Default Topic" +PLG_MOKOSUITECROSS_NTFY_DEFAULT_TOPIC_DESC="Default ntfy topic name. Each service can override this in its credentials." diff --git a/source/packages/plg_mokosuitecross_ntfy/ntfy.xml b/source/packages/plg_mokosuitecross_ntfy/ntfy.xml index a116ff1..43f60f7 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.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech @@ -23,4 +23,25 @@ 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/src/Extension/NtfyService.php b/source/packages/plg_mokosuitecross_ntfy/src/Extension/NtfyService.php index abf7953..6592d75 100644 --- a/source/packages/plg_mokosuitecross_ntfy/src/Extension/NtfyService.php +++ b/source/packages/plg_mokosuitecross_ntfy/src/Extension/NtfyService.php @@ -43,7 +43,8 @@ class NtfyService extends CMSPlugin implements SubscriberInterface, MokoSuiteCro { $url = $credentials['topic'] ?? $credentials['webhook_url'] ?? ''; - $serverUrl = rtrim($credentials['server_url'] ?? 'https://ntfy.sh', '/'); + $defaultServer = $this->params->get('default_server_url', 'https://ntfy.mokoconsulting.tech'); + $serverUrl = rtrim($credentials['server_url'] ?? $defaultServer, '/'); $topic = $credentials['topic'] ?? ''; $token = $credentials['token'] ?? ''; diff --git a/source/packages/plg_mokosuitecross_pinterest/pinterest.xml b/source/packages/plg_mokosuitecross_pinterest/pinterest.xml index b77eb0e..2781b8c 100644 --- a/source/packages/plg_mokosuitecross_pinterest/pinterest.xml +++ b/source/packages/plg_mokosuitecross_pinterest/pinterest.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Pinterest - 01.04.01 + 01.04.12-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 3548423..62273a6 100644 --- a/source/packages/plg_mokosuitecross_reddit/reddit.xml +++ b/source/packages/plg_mokosuitecross_reddit/reddit.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Reddit - 01.04.01 + 01.04.12-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 ab90241..15bc7c2 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.04.01 + 01.04.12-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 db1ee94..7134a3a 100644 --- a/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml +++ b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml @@ -1,7 +1,7 @@ MokoSuiteCross - SendGrid - 01.04.01 + 01.04.12-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 73abcc2..2c82a94 100644 --- a/source/packages/plg_mokosuitecross_slack/slack.xml +++ b/source/packages/plg_mokosuitecross_slack/slack.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Slack - 01.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech @@ -10,7 +10,7 @@ GPL-3.0-or-later PLG_MOKOSUITECROSS_SLACK_DESCRIPTION - Joomla\Plugin\MokoSuiteCross${CLASS_NAME} + Joomla\Plugin\MokoSuiteCross\Slack slack.php diff --git a/source/packages/plg_mokosuitecross_teams/teams.xml b/source/packages/plg_mokosuitecross_teams/teams.xml index 7496ae5..0e54111 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.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_telegram/src/Extension/TelegramService.php b/source/packages/plg_mokosuitecross_telegram/src/Extension/TelegramService.php index e67f550..86599f1 100644 --- a/source/packages/plg_mokosuitecross_telegram/src/Extension/TelegramService.php +++ b/source/packages/plg_mokosuitecross_telegram/src/Extension/TelegramService.php @@ -14,6 +14,7 @@ namespace Joomla\Plugin\MokoSuiteCross\Telegram\Extension; defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossDeleteInterface; use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Event\SubscriberInterface; @@ -31,7 +32,7 @@ use Joomla\Event\SubscriberInterface; * "chat_id": "-100xxxxxxx" // Required — channel/group/user chat ID * } */ -class TelegramService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface +class TelegramService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface, MokoSuiteCrossDeleteInterface { /** * Default MokoSuite Bot token — resolved at runtime from component params. @@ -222,6 +223,52 @@ class TelegramService extends CMSPlugin implements SubscriberInterface, MokoSuit return $r; } + public function deletePost(string $platformPostId, array $credentials): array + { + $botToken = $this->resolveBotToken($credentials); + $chatId = $credentials['chat_id'] ?? ''; + + if (empty($botToken) || empty($chatId)) { + return ['success' => false, 'message' => 'Missing bot token or chat_id.']; + } + + $apiUrl = 'https://api.telegram.org/bot' . $botToken . '/deleteMessage'; + + $postData = json_encode([ + 'chat_id' => $chatId, + 'message_id' => (int) $platformPostId, + ]); + + $ch = curl_init($apiUrl); + 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, 'message' => 'Connection error: ' . $curlError]; + } + + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $data = json_decode($response, true) ?: []; + + if ($httpCode === 200 && !empty($data['ok'])) { + return ['success' => true, 'message' => 'Message deleted successfully.']; + } + + return ['success' => false, 'message' => $data['description'] ?? 'Delete failed (HTTP ' . $httpCode . ').']; + } + public function getSupportedMediaTypes(): array { return ['image', 'video', 'document']; diff --git a/source/packages/plg_mokosuitecross_telegram/telegram.xml b/source/packages/plg_mokosuitecross_telegram/telegram.xml index 336e9ca..53b0359 100644 --- a/source/packages/plg_mokosuitecross_telegram/telegram.xml +++ b/source/packages/plg_mokosuitecross_telegram/telegram.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Telegram - 01.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech @@ -10,7 +10,7 @@ GPL-3.0-or-later PLG_MOKOSUITECROSS_TELEGRAM_DESCRIPTION - Joomla\Plugin\MokoSuiteCross${CLASS_NAME} + Joomla\Plugin\MokoSuiteCross\Telegram telegram.php diff --git a/source/packages/plg_mokosuitecross_threads/threads.xml b/source/packages/plg_mokosuitecross_threads/threads.xml index ab4b7bf..f8ed58c 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.04.01 + 01.04.12-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 f2ecbdb..cc297f9 100644 --- a/source/packages/plg_mokosuitecross_tiktok/tiktok.xml +++ b/source/packages/plg_mokosuitecross_tiktok/tiktok.xml @@ -1,7 +1,7 @@ MokoSuiteCross - TikTok - 01.04.01 + 01.04.12-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 48602c7..1504b8d 100644 --- a/source/packages/plg_mokosuitecross_tumblr/tumblr.xml +++ b/source/packages/plg_mokosuitecross_tumblr/tumblr.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Tumblr - 01.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_twitter/src/Extension/TwitterService.php b/source/packages/plg_mokosuitecross_twitter/src/Extension/TwitterService.php index ef6b4e0..8d9d2ab 100644 --- a/source/packages/plg_mokosuitecross_twitter/src/Extension/TwitterService.php +++ b/source/packages/plg_mokosuitecross_twitter/src/Extension/TwitterService.php @@ -14,6 +14,7 @@ namespace Joomla\Plugin\MokoSuiteCross\Twitter\Extension; defined('_JEXEC') or die; use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossDeleteInterface; use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; use Joomla\Event\SubscriberInterface; @@ -24,7 +25,7 @@ use Joomla\Event\SubscriberInterface; * 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 +class TwitterService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface, MokoSuiteCrossDeleteInterface { public static function getSubscribedEvents(): array { @@ -203,6 +204,50 @@ class TwitterService extends CMSPlugin implements SubscriberInterface, MokoSuite return 'OAuth ' . implode(', ', $parts); } + public function deletePost(string $platformPostId, array $credentials): array + { + $apiUrl = 'https://api.twitter.com/2/tweets/' . $platformPostId; + + $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, 'message' => 'Missing OAuth 1.0a credentials. All 4 keys are required.']; + } + + $authHeader = $this->buildOAuth1Header('DELETE', $apiUrl, $consumerKey, $consumerSecret, $accessToken, $tokenSecret); + + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_CUSTOMREQUEST => 'DELETE', + CURLOPT_HTTPHEADER => ['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, 'message' => 'Connection error: ' . $curlError]; + } + + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $data = json_decode($response, true) ?: []; + + if ($httpCode === 200 && !empty($data['data']['deleted']) && $data['data']['deleted'] === true) { + return ['success' => true, 'message' => 'Tweet deleted successfully.']; + } + + return ['success' => false, 'message' => $data['detail'] ?? $data['title'] ?? 'Delete failed with HTTP ' . $httpCode]; + } + public function getSupportedMediaTypes(): array { return ['image', 'video', 'gif']; diff --git a/source/packages/plg_mokosuitecross_twitter/twitter.xml b/source/packages/plg_mokosuitecross_twitter/twitter.xml index 3beee80..0cc7a77 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.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech @@ -10,7 +10,7 @@ GPL-3.0-or-later PLG_MOKOSUITECROSS_TWITTER_DESCRIPTION - Joomla\Plugin\MokoSuiteCross${CLASS_NAME} + Joomla\Plugin\MokoSuiteCross\Twitter twitter.php diff --git a/source/packages/plg_mokosuitecross_webhook/webhook.xml b/source/packages/plg_mokosuitecross_webhook/webhook.xml index 5014c64..2307377 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.04.01 + 01.04.12-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 b1be7e9..5e07b6a 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.04.01 + 01.04.12-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 c5409b7..06ef169 100644 --- a/source/packages/plg_mokosuitecross_wordpress/wordpress.xml +++ b/source/packages/plg_mokosuitecross_wordpress/wordpress.xml @@ -1,7 +1,7 @@ MokoSuiteCross - WordPress - 01.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_youtube/index.html b/source/packages/plg_mokosuitecross_youtube/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_youtube/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_youtube/language/en-GB/index.html b/source/packages/plg_mokosuitecross_youtube/language/en-GB/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_youtube/language/en-GB/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_youtube/language/en-GB/plg_mokosuitecross_youtube.ini b/source/packages/plg_mokosuitecross_youtube/language/en-GB/plg_mokosuitecross_youtube.ini new file mode 100644 index 0000000..339a9cc --- /dev/null +++ b/source/packages/plg_mokosuitecross_youtube/language/en-GB/plg_mokosuitecross_youtube.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_YOUTUBE="MokoSuiteCross - YouTube" +PLG_MOKOSUITECROSS_YOUTUBE_DESCRIPTION="Cross-post Joomla articles to YouTube community posts." diff --git a/source/packages/plg_mokosuitecross_youtube/language/en-GB/plg_mokosuitecross_youtube.sys.ini b/source/packages/plg_mokosuitecross_youtube/language/en-GB/plg_mokosuitecross_youtube.sys.ini new file mode 100644 index 0000000..339a9cc --- /dev/null +++ b/source/packages/plg_mokosuitecross_youtube/language/en-GB/plg_mokosuitecross_youtube.sys.ini @@ -0,0 +1,2 @@ +PLG_MOKOSUITECROSS_YOUTUBE="MokoSuiteCross - YouTube" +PLG_MOKOSUITECROSS_YOUTUBE_DESCRIPTION="Cross-post Joomla articles to YouTube community posts." diff --git a/source/packages/plg_mokosuitecross_youtube/language/index.html b/source/packages/plg_mokosuitecross_youtube/language/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_youtube/language/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_youtube/services/index.html b/source/packages/plg_mokosuitecross_youtube/services/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_youtube/services/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_youtube/services/provider.php b/source/packages/plg_mokosuitecross_youtube/services/provider.php new file mode 100644 index 0000000..755296a --- /dev/null +++ b/source/packages/plg_mokosuitecross_youtube/services/provider.php @@ -0,0 +1,38 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Joomla\Plugin\MokoSuiteCross\Youtube\Extension\YoutubeService; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new YoutubeService( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('mokosuitecross', 'youtube') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/source/packages/plg_mokosuitecross_youtube/src/Extension/YoutubeService.php b/source/packages/plg_mokosuitecross_youtube/src/Extension/YoutubeService.php new file mode 100644 index 0000000..c06bf81 --- /dev/null +++ b/source/packages/plg_mokosuitecross_youtube/src/Extension/YoutubeService.php @@ -0,0 +1,137 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +namespace Joomla\Plugin\MokoSuiteCross\Youtube\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * YouTube service plugin for MokoSuiteCross. + * + * Posts to YouTube via the Data API v3 channel bulletins. + * + * Credentials: + * access_token - OAuth 2.0 token with youtube.force-ssl scope + * channel_id - YouTube channel ID + */ +class YoutubeService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface +{ + public static function getSubscribedEvents(): array + { + return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices']; + } + + public function onMokoSuiteCrossGetServices(&$services): void + { + $services[] = $this; + } + + public function getServiceType(): string { return 'youtube'; } + public function getServiceName(): string { return 'YouTube'; } + public function getMaxLength(): int { return 5000; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $token = $credentials['access_token'] ?? ''; + $channelId = $credentials['channel_id'] ?? ''; + + if (empty($token) || empty($channelId)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or channel ID']]; + } + + $postData = json_encode([ + 'snippet' => [ + 'channelId' => $channelId, + 'description' => $message, + ], + 'contentDetails' => [ + 'bulletin' => [ + 'resourceId' => [ + 'kind' => 'youtube#channel', + 'channelId' => $channelId, + ], + ], + ], + ]); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => 'https://www.googleapis.com/youtube/v3/activities?part=snippet,contentDetails', + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + ]); + + $response = curl_exec($ch); + + if ($response === false) { + $curlError = curl_error($ch); + curl_close($ch); + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]]; + } + + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $data = json_decode($response, true) ?: []; + + if ($httpCode >= 200 && $httpCode < 300) { + return ['success' => true, 'platform_post_id' => $data['id'] ?? '', 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $token = $credentials['access_token'] ?? ''; + + if (empty($token)) { + return ['valid' => false, 'message' => 'Missing access token', 'account_name' => '']; + } + + $ch = curl_init('https://www.googleapis.com/youtube/v3/channels?part=snippet&mine=true'); + curl_setopt_array($ch, [ + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + ]); + + $response = curl_exec($ch); + + if ($response === false) { + $curlError = curl_error($ch); + curl_close($ch); + return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => '']; + } + + curl_close($ch); + $data = json_decode($response, true) ?: []; + + if (!empty($data['items'][0]['snippet']['title'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['items'][0]['snippet']['title']]; + } + + return ['valid' => false, 'message' => 'Invalid token or no channel found', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image', 'video']; + } +} diff --git a/source/packages/plg_mokosuitecross_youtube/src/Extension/index.html b/source/packages/plg_mokosuitecross_youtube/src/Extension/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_youtube/src/Extension/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_youtube/src/index.html b/source/packages/plg_mokosuitecross_youtube/src/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/source/packages/plg_mokosuitecross_youtube/src/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/plg_mokosuitecross_youtube/youtube.php b/source/packages/plg_mokosuitecross_youtube/youtube.php new file mode 100644 index 0000000..9b76408 --- /dev/null +++ b/source/packages/plg_mokosuitecross_youtube/youtube.php @@ -0,0 +1,11 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +defined('_JEXEC') or die; diff --git a/source/packages/plg_mokosuitecross_youtube/youtube.xml b/source/packages/plg_mokosuitecross_youtube/youtube.xml new file mode 100644 index 0000000..2c2b902 --- /dev/null +++ b/source/packages/plg_mokosuitecross_youtube/youtube.xml @@ -0,0 +1,39 @@ + + + MokoSuiteCross - Youtube + 01.04.12-dev + 2026-06-23 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_MOKOSUITECROSS_YOUTUBE_DESCRIPTION + + Joomla\Plugin\MokoSuiteCross\Youtube + + + youtube.php + src + services + language + + + + language/en-GB/plg_mokosuitecross_youtube.ini + language/en-GB/plg_mokosuitecross_youtube.sys.ini + + + +
+ +
+
+
+
diff --git a/source/packages/plg_system_mokosuitecross/mokosuitecross.xml b/source/packages/plg_system_mokosuitecross/mokosuitecross.xml index 292781f..5d358cb 100644 --- a/source/packages/plg_system_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_system_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross - 01.04.01 + 01.04.12-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 6a047c5..c8b8972 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.04.01 + 01.04.12-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 3bfd766..54aaaf7 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.04.01 + 01.04.12-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 0ab653f..7a71d5a 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.04.01 + 01.04.12-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 4c7b7bc..f38e01a 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.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/pkg_mokosuitecross.xml b/source/pkg_mokosuitecross.xml index cb961a9..4dc6d41 100644 --- a/source/pkg_mokosuitecross.xml +++ b/source/pkg_mokosuitecross.xml @@ -2,7 +2,7 @@ MokoSuiteCross mokosuitecross - 01.04.01 + 01.04.12-dev 2026-05-28 Moko Consulting hello@mokoconsulting.tech @@ -60,6 +60,8 @@ plg_mokosuitecross_tiktok.zip plg_mokosuitecross_mokosuitecalendar.zip plg_mokosuitecross_mokosuitegallery.zip + plg_mokosuitecross_instagram.zip + plg_mokosuitecross_youtube.zip plg_system_mokosuitecross_events.zip