Fix Twitter posting by replacing Bearer token (app-only, read-only) with OAuth 1.0a HMAC-SHA1 signing using all 4 keys. Add credential fields for 19 previously missing services and optional fields for 7 existing services. Add Developer Guide wiki page. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
11 KiB
Developer Guide
This guide covers building new service plugins for MokoJoomCross — from directory structure through testing.
Plugin Directory Structure
Each service plugin lives in its own package under src/packages/:
plg_mokojoomcross_myservice/
├── myservice.xml ← Joomla manifest (type="plugin", group="mokojoomcross")
├── myservice.php ← Legacy loader stub (empty, required by Joomla)
├── services/
│ └── provider.php ← DI container: registers the Extension class
└── src/
└── Extension/
└── MyServiceService.php ← Main class: implements the interface
MokoJoomCrossServiceInterface
Every service plugin must implement MokoJoomCrossServiceInterface. The interface defines 5 methods:
namespace Joomla\Component\MokoJoomCross\Administrator\Service;
interface MokoJoomCrossServiceInterface
{
/**
* Unique identifier matching the service_type in service.xml.
* Must match exactly (e.g. 'mastodon', 'telegram').
*/
public function getServiceType(): string;
/**
* Human-readable display name (e.g. 'Mastodon', 'Telegram').
*/
public function getServiceName(): string;
/**
* Post content to the platform.
*
* @param string $message Rendered message text (already template-processed)
* @param array $media Array of media file paths (images)
* @param array $credentials Decrypted credential key-value pairs from the service record
* @param array $params Plugin params + service params merged
* @return array ['success' => bool, 'platform_post_id' => string, 'response' => array]
*/
public function publish(string $message, array $media, array $credentials, array $params): array;
/**
* Test whether the stored credentials are valid.
*
* @param array $credentials Decrypted credential key-value pairs
* @return array ['valid' => bool, 'message' => string, 'account_name' => string]
*/
public function validateCredentials(array $credentials): array;
/**
* Platform character limit (0 = unlimited).
*/
public function getMaxLength(): int;
/**
* Whether this service supports image/media attachments.
*/
public function supportsMedia(): bool;
}
Step-by-Step: Creating a New Service Plugin
1. Create the manifest (myservice.xml)
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="mokojoomcross" method="upgrade">
<name>plg_mokojoomcross_myservice</name>
<author>Moko Consulting</author>
<version>1.0.0</version>
<description>MyService integration for MokoJoomCross</description>
<namespace path="src">Joomla\Plugin\MokoJoomCross\MyService</namespace>
<files>
<filename plugin="myservice">myservice.php</filename>
<folder>services</folder>
<folder>src</folder>
</files>
<!-- Optional: plugin-level params (e.g. default bot tokens) -->
<config>
<fields name="params">
<fieldset name="basic">
<field name="default_token" type="password"
label="Default Bot Token"
description="Pre-configured token for default mode" />
</fieldset>
</fields>
</config>
</extension>
2. Create the legacy stub (myservice.php)
<?php
// Legacy stub — required by Joomla plugin loader. Intentionally empty.
3. Create the DI provider (services/provider.php)
<?php
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\MokoJoomCross\MyService\Extension\MyServiceService;
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 MyServiceService($dispatcher, (array) PluginHelper::getPlugin('mokojoomcross', 'myservice'));
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};
4. Create the Extension class
<?php
namespace Joomla\Plugin\MokoJoomCross\MyService\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\MokoJoomCross\Administrator\Service\MokoJoomCrossServiceInterface;
use Joomla\Event\SubscriberInterface;
class MyServiceService extends CMSPlugin implements SubscriberInterface, MokoJoomCrossServiceInterface
{
public static function getSubscribedEvents(): array
{
return [
'onMokoJoomCrossGetServices' => 'onMokoJoomCrossGetServices',
];
}
public function onMokoJoomCrossGetServices(&$services): void
{
$services[] = $this;
}
public function getServiceType(): string
{
return 'myservice';
}
public function getServiceName(): string
{
return 'My Service';
}
public function publish(string $message, array $media, array $credentials, array $params): array
{
// Your API integration here
// $credentials contains the decrypted values from service.xml fields
// e.g. $credentials['api_key'], $credentials['webhook_url']
return [
'success' => true,
'platform_post_id' => 'abc123',
'response' => ['status' => 'ok'],
];
}
public function validateCredentials(array $credentials): array
{
// Test the credentials against the platform API
return [
'valid' => true,
'message' => 'Connected',
'account_name' => 'MyAccount',
];
}
public function getMaxLength(): int
{
return 0; // 0 = no limit
}
public function supportsMedia(): bool
{
return false;
}
}
5. Add credential fields to service.xml
In src/packages/com_mokojoomcross/forms/service.xml, add your fields with showon:
<!-- ======== MY SERVICE ======== -->
<field
name="cred_myservice_api_key"
type="password"
label="COM_MOKOJOOMCROSS_CRED_MYSERVICE_KEY"
showon="service_type:myservice"
size="60"
/>
6. Add language strings to com_mokojoomcross.ini
COM_MOKOJOOMCROSS_CRED_MYSERVICE_KEY="API Key"
7. Add to the service_type dropdown (if not already listed)
In the <field name="service_type"> list in service.xml, add:
<option value="myservice">My Service</option>
How showon Credential Fields Work
Joomla's showon attribute controls field visibility client-side via JavaScript:
| Pattern | Meaning |
|---|---|
showon="service_type:telegram" |
Show when service type is Telegram |
showon="service_type:telegram[AND]cred_mode:custom" |
Show when Telegram AND custom mode |
showon="service_type:webhook[AND]cred_webhook_auth_type:bearer,basic" |
Show when webhook AND auth is bearer or basic |
Fields are hidden/shown without page reloads. The form data for hidden fields is still submitted but ignored by the component.
Dispatch Pipeline
The cross-posting flow works like this:
- Article published → System plugin (
plg_system_mokojoomcross) catchesonContentAfterSave - Queue creation → For each enabled service, a
#__mokojoomcross_postsrow is created with statusqueued - Queue processing → Either the Scheduled Task or page-load fallback picks up queued posts
- Service dispatch →
QueueProcessorfiresonMokoJoomCrossGetServicesevent in themokojoomcrossplugin group - Plugin response → Each registered service plugin adds itself to the
$servicesarray - Matching → The processor finds the plugin whose
getServiceType()matches the service record'sservice_type - Publishing →
publish()is called with the rendered message, media paths, decrypted credentials, and params - Result → The post record is updated with
posted/failedstatus and the platform response
Default Bot Mode
Some services (Telegram, Discord, Slack, Teams, Facebook, Threads) support a default mode where pre-configured MokoWaaS credentials are used. This is controlled by:
- The
cred_modefield inservice.xml(shown for services listed in itsshowon) - Plugin-level params in the plugin manifest (
<config>section) that store default tokens - The service plugin's
publish()method checks$credentials['mode']:'default'→ use plugin params ($this->params->get('default_token'))'custom'→ use the per-service credentials from$credentials
OAuth Integration
For services requiring OAuth (Facebook, LinkedIn, Twitter, Pinterest, etc.):
-
OAuthHelper (
src/packages/com_mokojoomcross/src/Helper/OAuthHelper.php) handles:- Authorization URL generation with state parameter
- Code-to-token exchange
- Token storage back to the service record's credentials
-
OauthController provides two endpoints:
task=oauth.authorize→ redirects to the platform's auth pagetask=oauth.callback→ handles the redirect, exchanges code for token
-
Plugin params store the OAuth Client ID and Secret (set in Extensions → Plugins)
-
In
edit.php, services listed in$oauthServicesget a "Connect to {Service}" button
Testing Your Plugin
- Syntax check:
php -l src/packages/plg_mokojoomcross_myservice/src/Extension/MyServiceService.php - Install: Include the plugin in
pkg_mokojoomcross.xmlor install the plugin ZIP standalone - Enable: Extensions → Plugins → search "mokojoomcross myservice" → Enable
- Add service: Components → MokoJoomCross → Services → New → select your service type
- Verify fields: Confirm your credential fields appear when your service type is selected
- Test post: Publish an article and check the Post Queue for results
Example: Building a "Fediverse" Service
Imagine building a service for a Mastodon-compatible platform:
public function publish(string $message, array $media, array $credentials, array $params): array
{
$instanceUrl = rtrim($credentials['instance_url'] ?? '', '/');
$token = $credentials['access_token'] ?? '';
$ch = curl_init($instanceUrl . '/api/v1/statuses');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['status' => $message]),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $token,
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($response, true) ?: [];
if ($httpCode === 200 && !empty($data['id'])) {
return ['success' => true, 'platform_post_id' => $data['id'], 'response' => $data];
}
return ['success' => false, 'platform_post_id' => '', 'response' => $data];
}
This pattern — curl to API, check response code, return structured result — is the same for every service plugin. The only differences are the API endpoint, authentication method, and payload format.