# 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 plg_mokojoomcross_myservice Moko Consulting 1.0.0 MyService integration for MokoJoomCross Joomla\Plugin\MokoJoomCross\MyService myservice.php services src
``` ### 2. Create the legacy stub (`myservice.php`) ```php 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 '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 ``` ### 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 `` list in `service.xml`, add: ```xml ``` ## 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 (`` 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.