diff --git a/source/packages/plg_webservices_perfectpublisher/perfectpublisher.xml b/source/packages/plg_webservices_perfectpublisher/perfectpublisher.xml
deleted file mode 100644
index fee522db..00000000
--- a/source/packages/plg_webservices_perfectpublisher/perfectpublisher.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
- Web Services - Perfect Publisher
- Moko Consulting
- 2026-05-28
- Copyright (C) 2026 Moko Consulting. All rights reserved.
- GPL-3.0-or-later
- hello@mokoconsulting.tech
- https://mokoconsulting.tech
- 02.34.30-dev
- Joomla Web Services API routes for Perfect Publisher (com_autotweet) — channels, posts, requests, rules, and feeds.
- Moko\Plugin\WebServices\PerfectPublisher
-
- services
- src
-
-
diff --git a/source/packages/plg_webservices_perfectpublisher/services/provider.php b/source/packages/plg_webservices_perfectpublisher/services/provider.php
deleted file mode 100644
index d7f8ced6..00000000
--- a/source/packages/plg_webservices_perfectpublisher/services/provider.php
+++ /dev/null
@@ -1,42 +0,0 @@
-
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- *
- * FILE INFORMATION
- * DEFGROUP: Joomla.Plugin
- * INGROUP: MokoWaaS
- * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
- * PATH: /source/packages/plg_webservices_perfectpublisher/services/provider.php
- * VERSION: 02.34.30
- * BRIEF: DI service provider for Perfect Publisher Web Services plugin
- */
-
-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 Moko\Plugin\WebServices\PerfectPublisher\Extension\PerfectPublisherApi;
-
-return new class implements ServiceProviderInterface
-{
- public function register(Container $container): void
- {
- $container->set(
- PluginInterface::class,
- function (Container $container) {
- $dispatcher = $container->get(DispatcherInterface::class);
- $plugin = new PerfectPublisherApi(
- $dispatcher,
- (array) PluginHelper::getPlugin('webservices', 'perfectpublisher')
- );
- $plugin->setApplication(Factory::getApplication());
- return $plugin;
- }
- );
- }
-};
diff --git a/source/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php b/source/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
deleted file mode 100644
index 5bdd432e..00000000
--- a/source/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
+++ /dev/null
@@ -1,539 +0,0 @@
-
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- *
- * FILE INFORMATION
- * DEFGROUP: Joomla.Plugin
- * INGROUP: MokoWaaS
- * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
- * PATH: /source/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
- * VERSION: 02.34.30
- * BRIEF: Web Services API plugin for Perfect Publisher (com_autotweet)
- */
-
-namespace Moko\Plugin\WebServices\PerfectPublisher\Extension;
-
-defined('_JEXEC') or die;
-
-use Joomla\CMS\Factory;
-use Joomla\CMS\Plugin\CMSPlugin;
-use Joomla\CMS\Event\Application\BeforeApiRouteEvent;
-use Joomla\CMS\Router\ApiRouter;
-use Joomla\Event\SubscriberInterface;
-
-/**
- * Perfect Publisher Web Services API Plugin
- *
- * Registers REST API routes for Perfect Publisher (com_autotweet) data.
- * Provides read access to channels, posts, requests, rules, and feeds.
- * Provides write access to create publish requests.
- *
- * Routes:
- * GET /v1/perfectpublisher/channels List social channels
- * GET /v1/perfectpublisher/channels/:id Get channel detail
- * GET /v1/perfectpublisher/posts List published posts
- * GET /v1/perfectpublisher/posts/:id Get post detail
- * GET /v1/perfectpublisher/requests List pending requests
- * POST /v1/perfectpublisher/requests Create a publish request
- * GET /v1/perfectpublisher/rules List publishing rules
- * GET /v1/perfectpublisher/feeds List RSS feeds
- * GET /v1/perfectpublisher/channeltypes List channel type definitions
- * GET /v1/perfectpublisher/stats Dashboard statistics
- *
- * @since 02.13.01
- */
-final class PerfectPublisherApi extends CMSPlugin implements SubscriberInterface
-{
- /**
- * @return array
- */
- public static function getSubscribedEvents(): array
- {
- return [
- 'onBeforeApiRoute' => 'onBeforeApiRoute',
- ];
- }
-
- /**
- * Register API routes.
- *
- * @param BeforeApiRouteEvent $event The API route event
- *
- * @return void
- */
- public function onBeforeApiRoute(BeforeApiRouteEvent $event): void
- {
- $router = $event->getRouter();
-
- // All routes are handled by this plugin directly via custom callbacks
- // because com_autotweet uses FOF, not standard Joomla MVC
-
- $router->addRoute(
- new \Joomla\Router\Route(
- ['GET'],
- 'v1/perfectpublisher/channels',
- [$this, 'getChannels']
- )
- );
-
- $router->addRoute(
- new \Joomla\Router\Route(
- ['GET'],
- 'v1/perfectpublisher/channels/:id',
- [$this, 'getChannel']
- )
- );
-
- $router->addRoute(
- new \Joomla\Router\Route(
- ['GET'],
- 'v1/perfectpublisher/posts',
- [$this, 'getPosts']
- )
- );
-
- $router->addRoute(
- new \Joomla\Router\Route(
- ['GET'],
- 'v1/perfectpublisher/posts/:id',
- [$this, 'getPost']
- )
- );
-
- $router->addRoute(
- new \Joomla\Router\Route(
- ['GET'],
- 'v1/perfectpublisher/requests',
- [$this, 'getRequests']
- )
- );
-
- $router->addRoute(
- new \Joomla\Router\Route(
- ['POST'],
- 'v1/perfectpublisher/requests',
- [$this, 'createRequest']
- )
- );
-
- $router->addRoute(
- new \Joomla\Router\Route(
- ['GET'],
- 'v1/perfectpublisher/rules',
- [$this, 'getRules']
- )
- );
-
- $router->addRoute(
- new \Joomla\Router\Route(
- ['GET'],
- 'v1/perfectpublisher/feeds',
- [$this, 'getFeeds']
- )
- );
-
- $router->addRoute(
- new \Joomla\Router\Route(
- ['GET'],
- 'v1/perfectpublisher/channeltypes',
- [$this, 'getChannelTypes']
- )
- );
-
- $router->addRoute(
- new \Joomla\Router\Route(
- ['GET'],
- 'v1/perfectpublisher/stats',
- [$this, 'getStats']
- )
- );
- }
-
- /**
- * GET /v1/perfectpublisher/channels
- *
- * @return void
- */
- public function getChannels(): void
- {
- $db = Factory::getDbo();
- $app = Factory::getApplication();
- $limit = (int) $app->input->get('limit', 20);
- $offset = (int) $app->input->get('offset', 0);
-
- $query = $db->getQuery(true)
- ->select('c.*, ct.name AS channeltype_name, ct.max_chars')
- ->from($db->quoteName('#__autotweet_channels', 'c'))
- ->leftJoin(
- $db->quoteName('#__autotweet_channeltypes', 'ct')
- . ' ON ' . $db->quoteName('c.channeltype_id')
- . ' = ' . $db->quoteName('ct.id')
- )
- ->order($db->quoteName('c.ordering') . ' ASC');
-
- $published = $app->input->get('published', null);
- if ($published !== null) {
- $query->where($db->quoteName('c.published') . ' = ' . (int) $published);
- }
-
- $db->setQuery($query, $offset, $limit);
-
- $this->sendJsonResponse($db->loadObjectList());
- }
-
- /**
- * GET /v1/perfectpublisher/channels/:id
- *
- * @return void
- */
- public function getChannel(): void
- {
- $id = (int) Factory::getApplication()->input->get('id', 0);
- $db = Factory::getDbo();
-
- $query = $db->getQuery(true)
- ->select('c.*, ct.name AS channeltype_name, ct.max_chars, ct.description AS channeltype_desc')
- ->from($db->quoteName('#__autotweet_channels', 'c'))
- ->leftJoin(
- $db->quoteName('#__autotweet_channeltypes', 'ct')
- . ' ON ' . $db->quoteName('c.channeltype_id')
- . ' = ' . $db->quoteName('ct.id')
- )
- ->where($db->quoteName('c.id') . ' = ' . $id);
-
- $db->setQuery($query);
- $result = $db->loadObject();
-
- if (!$result) {
- $this->sendJsonError('Channel not found', 404);
- return;
- }
-
- // Strip sensitive OAuth params
- if (isset($result->params)) {
- $params = json_decode($result->params, true);
- if (is_array($params)) {
- foreach (['access_token', 'access_secret', 'client_secret', 'api_secret', 'password'] as $key) {
- if (isset($params[$key])) {
- $params[$key] = '***';
- }
- }
- $result->params = json_encode($params);
- }
- }
-
- $this->sendJsonResponse($result);
- }
-
- /**
- * GET /v1/perfectpublisher/posts
- *
- * @return void
- */
- public function getPosts(): void
- {
- $db = Factory::getDbo();
- $app = Factory::getApplication();
- $limit = (int) $app->input->get('limit', 20);
- $offset = (int) $app->input->get('offset', 0);
-
- $query = $db->getQuery(true)
- ->select('p.*, c.name AS channel_name')
- ->from($db->quoteName('#__autotweet_posts', 'p'))
- ->leftJoin(
- $db->quoteName('#__autotweet_channels', 'c')
- . ' ON ' . $db->quoteName('p.channel_id')
- . ' = ' . $db->quoteName('c.id')
- )
- ->order($db->quoteName('p.postdate') . ' DESC');
-
- $pubstate = $app->input->get('pubstate', '');
- if ($pubstate !== '') {
- $query->where($db->quoteName('p.pubstate') . ' = ' . $db->quote($pubstate));
- }
-
- $channel = (int) $app->input->get('channel_id', 0);
- if ($channel > 0) {
- $query->where($db->quoteName('p.channel_id') . ' = ' . $channel);
- }
-
- $db->setQuery($query, $offset, $limit);
-
- $this->sendJsonResponse($db->loadObjectList());
- }
-
- /**
- * GET /v1/perfectpublisher/posts/:id
- *
- * @return void
- */
- public function getPost(): void
- {
- $id = (int) Factory::getApplication()->input->get('id', 0);
- $db = Factory::getDbo();
-
- $query = $db->getQuery(true)
- ->select('p.*, c.name AS channel_name, ct.name AS channeltype_name')
- ->from($db->quoteName('#__autotweet_posts', 'p'))
- ->leftJoin(
- $db->quoteName('#__autotweet_channels', 'c')
- . ' ON ' . $db->quoteName('p.channel_id')
- . ' = ' . $db->quoteName('c.id')
- )
- ->leftJoin(
- $db->quoteName('#__autotweet_channeltypes', 'ct')
- . ' ON ' . $db->quoteName('c.channeltype_id')
- . ' = ' . $db->quoteName('ct.id')
- )
- ->where($db->quoteName('p.id') . ' = ' . $id);
-
- $db->setQuery($query);
- $result = $db->loadObject();
-
- if (!$result) {
- $this->sendJsonError('Post not found', 404);
- return;
- }
-
- $this->sendJsonResponse($result);
- }
-
- /**
- * GET /v1/perfectpublisher/requests
- *
- * @return void
- */
- public function getRequests(): void
- {
- $db = Factory::getDbo();
- $app = Factory::getApplication();
- $limit = (int) $app->input->get('limit', 20);
- $offset = (int) $app->input->get('offset', 0);
-
- $query = $db->getQuery(true)
- ->select('*')
- ->from($db->quoteName('#__autotweet_requests'))
- ->order($db->quoteName('publish_up') . ' ASC');
-
- $published = $app->input->get('published', null);
- if ($published !== null) {
- $query->where($db->quoteName('published') . ' = ' . (int) $published);
- }
-
- $db->setQuery($query, $offset, $limit);
-
- $this->sendJsonResponse($db->loadObjectList());
- }
-
- /**
- * POST /v1/perfectpublisher/requests
- *
- * Create a new publish request. Required fields: description.
- * Optional: url, image_url, publish_up, plugin, priority.
- *
- * @return void
- */
- public function createRequest(): void
- {
- $app = Factory::getApplication();
- $db = Factory::getDbo();
- $data = json_decode($app->input->json->getRaw(), true);
-
- if (empty($data['description'])) {
- $this->sendJsonError('Field "description" is required', 400);
- return;
- }
-
- $now = Factory::getDate()->toSql();
- $user = Factory::getUser();
-
- $row = (object) [
- 'ref_id' => $data['ref_id'] ?? null,
- 'plugin' => $data['plugin'] ?? 'manual-api',
- 'priority' => (int) ($data['priority'] ?? 5),
- 'publish_up' => $data['publish_up'] ?? $now,
- 'description' => $data['description'],
- 'typeinfo' => (int) ($data['typeinfo'] ?? 0),
- 'url' => $data['url'] ?? null,
- 'image_url' => $data['image_url'] ?? null,
- 'created' => $now,
- 'created_by' => $user->id,
- 'params' => json_encode($data['params'] ?? []),
- 'published' => (int) ($data['published'] ?? 1),
- ];
-
- $db->insertObject('#__autotweet_requests', $row, 'id');
-
- $this->sendJsonResponse(
- ['id' => $row->id, 'status' => 'created'],
- 201
- );
- }
-
- /**
- * GET /v1/perfectpublisher/rules
- *
- * @return void
- */
- public function getRules(): void
- {
- $db = Factory::getDbo();
-
- $query = $db->getQuery(true)
- ->select('r.*, rt.name AS ruletype_name, rt.description AS ruletype_desc, c.name AS channel_name')
- ->from($db->quoteName('#__autotweet_rules', 'r'))
- ->leftJoin(
- $db->quoteName('#__autotweet_ruletypes', 'rt')
- . ' ON ' . $db->quoteName('r.ruletype_id')
- . ' = ' . $db->quoteName('rt.id')
- )
- ->leftJoin(
- $db->quoteName('#__autotweet_channels', 'c')
- . ' ON ' . $db->quoteName('r.channel_id')
- . ' = ' . $db->quoteName('c.id')
- )
- ->order($db->quoteName('r.ordering') . ' ASC');
-
- $db->setQuery($query);
-
- $this->sendJsonResponse($db->loadObjectList());
- }
-
- /**
- * GET /v1/perfectpublisher/feeds
- *
- * @return void
- */
- public function getFeeds(): void
- {
- $db = Factory::getDbo();
-
- $query = $db->getQuery(true)
- ->select('*')
- ->from($db->quoteName('#__autotweet_feeds'))
- ->order($db->quoteName('ordering') . ' ASC');
-
- $db->setQuery($query);
-
- $this->sendJsonResponse($db->loadObjectList());
- }
-
- /**
- * GET /v1/perfectpublisher/channeltypes
- *
- * @return void
- */
- public function getChannelTypes(): void
- {
- $db = Factory::getDbo();
-
- $query = $db->getQuery(true)
- ->select('*')
- ->from($db->quoteName('#__autotweet_channeltypes'))
- ->order($db->quoteName('id') . ' ASC');
-
- $db->setQuery($query);
-
- $this->sendJsonResponse($db->loadObjectList());
- }
-
- /**
- * GET /v1/perfectpublisher/stats
- *
- * Dashboard statistics: post counts by status, channel counts, recent activity.
- *
- * @return void
- */
- public function getStats(): void
- {
- $db = Factory::getDbo();
-
- // Posts by status
- $db->setQuery(
- $db->getQuery(true)
- ->select('pubstate, COUNT(*) AS total')
- ->from($db->quoteName('#__autotweet_posts'))
- ->group($db->quoteName('pubstate'))
- );
- $postsByStatus = $db->loadObjectList('pubstate');
-
- // Active channels
- $db->setQuery(
- $db->getQuery(true)
- ->select('COUNT(*) AS total')
- ->from($db->quoteName('#__autotweet_channels'))
- ->where($db->quoteName('published') . ' = 1')
- );
- $activeChannels = (int) $db->loadResult();
-
- // Pending requests
- $db->setQuery(
- $db->getQuery(true)
- ->select('COUNT(*) AS total')
- ->from($db->quoteName('#__autotweet_requests'))
- ->where($db->quoteName('published') . ' = 1')
- );
- $pendingRequests = (int) $db->loadResult();
-
- // Posts last 24h
- $db->setQuery(
- $db->getQuery(true)
- ->select('COUNT(*) AS total')
- ->from($db->quoteName('#__autotweet_posts'))
- ->where($db->quoteName('postdate') . ' >= DATE_SUB(NOW(), INTERVAL 1 DAY)')
- );
- $posts24h = (int) $db->loadResult();
-
- // Posts last 7d
- $db->setQuery(
- $db->getQuery(true)
- ->select('COUNT(*) AS total')
- ->from($db->quoteName('#__autotweet_posts'))
- ->where($db->quoteName('postdate') . ' >= DATE_SUB(NOW(), INTERVAL 7 DAY)')
- );
- $posts7d = (int) $db->loadResult();
-
- $this->sendJsonResponse([
- 'posts_by_status' => $postsByStatus,
- 'active_channels' => $activeChannels,
- 'pending_requests' => $pendingRequests,
- 'posts_24h' => $posts24h,
- 'posts_7d' => $posts7d,
- ]);
- }
-
- /**
- * Send a JSON API response.
- *
- * @param mixed $data Response data
- * @param int $status HTTP status code
- *
- * @return void
- */
- private function sendJsonResponse($data, int $status = 200): void
- {
- $app = Factory::getApplication();
- $app->setHeader('Content-Type', 'application/json; charset=utf-8');
- $app->setHeader('Status', (string) $status);
- echo json_encode(['data' => $data], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
- $app->close();
- }
-
- /**
- * Send a JSON error response.
- *
- * @param string $message Error message
- * @param int $status HTTP status code
- *
- * @return void
- */
- private function sendJsonError(string $message, int $status = 400): void
- {
- $app = Factory::getApplication();
- $app->setHeader('Content-Type', 'application/json; charset=utf-8');
- $app->setHeader('Status', (string) $status);
- echo json_encode(['error' => $message], JSON_UNESCAPED_SLASHES);
- $app->close();
- }
-}
diff --git a/source/pkg_mokowaas.xml b/source/pkg_mokowaas.xml
index e898479c..07365e9e 100644
--- a/source/pkg_mokowaas.xml
+++ b/source/pkg_mokowaas.xml
@@ -26,7 +26,6 @@
mod_mokowaas_cache.zip
mod_mokowaas_categories.zip
plg_webservices_mokowaas.zip
- plg_webservices_perfectpublisher.zip
plg_task_mokowaasdemo.zip
plg_task_mokowaassync.zip
plg_task_mokowaas_tickets.zip
diff --git a/source/script.php b/source/script.php
index c0c63580..5bca126c 100644
--- a/source/script.php
+++ b/source/script.php
@@ -498,8 +498,7 @@ class Pkg_MokowaasInstallerScript
$db->quote('mokowaasdemo'),
$db->quote('mokowaassync'),
$db->quote('mokowaas_tickets'),
- $db->quote('perfectpublisher'),
- $db->quote('mokoonyx'),
+ $db->quote('mokoonyx'),
];
$query = $db->getQuery(true)