Files
Jonathan Miller 430d6a79f4
Universal: Auto Version Bump / Version Bump (push) Successful in 4s
Update Server / Update Server (push) Successful in 12s
feat: complete service credential fields + fix Twitter OAuth 1.0a
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>
2026-05-28 15:37:25 -05:00

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:

  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 dispatchQueueProcessor 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. Publishingpublish() 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:

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.