2026-05-28 15:36:40 -05:00
# Developer Guide
This guide covers building new service plugins for MokoJoomCross — from directory structure through testing.
## Plugin Directory Structure
2026-06-06 08:09:07 -05:00
Each service plugin lives in its own package under `source/packages/` :
2026-05-28 15:36:40 -05:00
```
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:
```php
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
<?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
<? php
// Legacy stub — required by Joomla plugin loader. Intentionally empty.
```
### 3. Create the DI provider (`services/provider.php`)
```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
<? 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`
2026-06-06 08:09:07 -05:00
In `source/packages/com_mokojoomcross/forms/service.xml` , add your fields with `showon` :
2026-05-28 15:36:40 -05:00
```xml
<!-- ======== 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`
```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:
```xml
<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:
1. **Article published** → System plugin (`plg_system_mokojoomcross` ) catches `onContentAfterSave`
2. **Queue creation** → For each enabled service, a `#__mokojoomcross_posts` row is created with status `queued`
3. **Queue processing** → Either the Scheduled Task or page-load fallback picks up queued posts
4. **Service dispatch** → `QueueProcessor` fires `onMokoJoomCrossGetServices` event in the `mokojoomcross` plugin group
5. **Plugin response** → Each registered service plugin adds itself to the `$services` array
6. **Matching** → The processor finds the plugin whose `getServiceType()` matches the service record's `service_type`
7. **Publishing** → `publish()` is called with the rendered message, media paths, decrypted credentials, and params
8. **Result** → The post record is updated with `posted` /`failed` status and the platform response
## Default Bot Mode
Some services (Telegram, Discord, Slack, Teams, Facebook, Threads) support a **default mode** where pre-configured MokoWaaS credentials are used. This is controlled by:
1. The `cred_mode` field in `service.xml` (shown for services listed in its `showon` )
2. Plugin-level params in the plugin manifest (`<config>` section) that store default tokens
3. The service plugin's `publish()` method checks `$credentials['mode']` :
- `'default'` → use plugin params (`$this->params->get('default_token')` )
- `'custom'` → use the per-service credentials from `$credentials`
## OAuth Integration
For services requiring OAuth (Facebook, LinkedIn, Twitter, Pinterest, etc.):
2026-06-06 08:09:07 -05:00
1. **OAuthHelper** (`source/packages/com_mokojoomcross/src/Helper/OAuthHelper.php` ) handles:
2026-05-28 15:36:40 -05:00
- Authorization URL generation with state parameter
- Code-to-token exchange
- Token storage back to the service record's credentials
2. **OauthController** provides two endpoints:
- `task=oauth.authorize` → redirects to the platform's auth page
- `task=oauth.callback` → handles the redirect, exchanges code for token
3. Plugin params store the OAuth Client ID and Secret (set in Extensions → Plugins)
4. In `edit.php` , services listed in `$oauthServices` get a "Connect to {Service}" button
## Testing Your Plugin
2026-06-06 08:09:07 -05:00
1. **Syntax check** : `php -l source/packages/plg_mokojoomcross_myservice/src/Extension/MyServiceService.php`
2026-05-28 15:36:40 -05:00
2. **Install** : Include the plugin in `pkg_mokojoomcross.xml` or install the plugin ZIP standalone
3. **Enable** : Extensions → Plugins → search "mokojoomcross myservice" → Enable
4. **Add service** : Components → MokoJoomCross → Services → New → select your service type
5. **Verify fields** : Confirm your credential fields appear when your service type is selected
6. **Test post** : Publish an article and check the Post Queue for results
## Example: Building a "Fediverse" Service
Imagine building a service for a Mastodon-compatible platform:
```php
public function publish ( string $message , array $media , array $credentials , array $params ) : array
{
$instanceUrl = rtrim ( $credentials [ 'instance_url' ] ?? '' , '/' );
$token = $credentials [ 'access_token' ] ?? '' ;
$ch = curl_init ( $instanceUrl . '/api/v1/statuses' );
curl_setopt_array ( $ch , [
CURLOPT_POST => true ,
CURLOPT_POSTFIELDS => json_encode ([ 'status' => $message ]),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json' ,
'Authorization: Bearer ' . $token ,
],
CURLOPT_RETURNTRANSFER => true ,
CURLOPT_TIMEOUT => 30 ,
]);
$response = curl_exec ( $ch );
$httpCode = curl_getinfo ( $ch , CURLINFO_HTTP_CODE );
curl_close ( $ch );
$data = json_decode ( $response , true ) ?: [];
if ( $httpCode === 200 && ! empty ( $data [ 'id' ])) {
return [ 'success' => true , 'platform_post_id' => $data [ 'id' ], 'response' => $data ];
}
return [ 'success' => false , 'platform_post_id' => '' , 'response' => $data ];
}
```
This pattern — curl to API, check response code, return structured result — is the same for every service plugin. The only differences are the API endpoint, authentication method, and payload format.