430d6a79f4
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>
338 lines
11 KiB
Markdown
338 lines
11 KiB
Markdown
# 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:
|
|
|
|
```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`
|
|
|
|
In `src/packages/com_mokojoomcross/forms/service.xml`, add your fields with `showon`:
|
|
|
|
```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.):
|
|
|
|
1. **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
|
|
|
|
2. **OauthController** provides two endpoints:
|
|
- `task=oauth.authorize` → redirects to the platform's auth page
|
|
- `task=oauth.callback` → handles the redirect, exchanges code for token
|
|
|
|
3. Plugin params store the OAuth Client ID and Secret (set in Extensions → Plugins)
|
|
|
|
4. In `edit.php`, services listed in `$oauthServices` get a "Connect to {Service}" button
|
|
|
|
## Testing Your Plugin
|
|
|
|
1. **Syntax check**: `php -l src/packages/plg_mokojoomcross_myservice/src/Extension/MyServiceService.php`
|
|
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.
|