feat: expanded automation — Joomla event triggers, create_ticket action, behavior options (#151)

New trigger events hooked into core plugin:
- user_login — fires on successful Joomla login
- user_register — fires on new user creation
- user_login_failed — fires on failed login attempt

New action type: create_ticket with behavior options:
- append: add reply to existing open ticket (same user+category)
- always_new: always create a new ticket
- skip_if_open: do nothing if open ticket exists

New method: runSystemEventAutomation() for non-ticket events
that builds a virtual context object from event data.

Automation rules can now create tickets from any system event,
with intelligent deduplication to avoid ticket spam.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-06-04 07:08:59 -05:00
parent 1389c26895
commit 0620ffd735
2 changed files with 154 additions and 2 deletions
@@ -608,10 +608,116 @@ class TicketsModel extends BaseDatabaseModel
}
}
break;
case 'create_ticket':
// value = JSON: {"subject":"...","body":"...","category_id":1,"priority":"normal","behavior":"append"}
$ticketData = json_decode($value, true) ?: [];
$behavior = $ticketData['behavior'] ?? 'append';
$userId = (int) ($ticket->created_by ?? 0);
$catId = (int) ($ticketData['category_id'] ?? 0);
if ($behavior === 'append' && $userId > 0)
{
// Check for existing open ticket from this user in this category
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__mokowaas_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId)
->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')')
->where($catId ? $db->quoteName('category_id') . ' = ' . $catId : '1=1')
->order($db->quoteName('created') . ' DESC')
->setLimit(1)
);
$existingId = (int) $db->loadResult();
if ($existingId)
{
$this->addReply($existingId, $ticketData['body'] ?? 'Automation event', true);
break;
}
}
elseif ($behavior === 'skip_if_open' && $userId > 0)
{
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokowaas_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId)
->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')')
);
if ((int) $db->loadResult() > 0)
{
break;
}
}
// Create new ticket
$this->createTicket([
'subject' => $ticketData['subject'] ?? 'Automation: ' . ($ticket->subject ?? 'System event'),
'body' => $ticketData['body'] ?? '',
'priority' => $ticketData['priority'] ?? 'normal',
'category_id' => $catId,
]);
break;
}
}
}
/**
* Run automation for a system event (not tied to a specific ticket).
* Creates a virtual ticket context from event data.
*/
public function runSystemEventAutomation(string $event, array $eventData = []): void
{
try
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_ticket_automation'))
->where($db->quoteName('trigger_event') . ' = ' . $db->quote($event))
->where($db->quoteName('enabled') . ' = 1')
->order($db->quoteName('ordering') . ' ASC');
$db->setQuery($query);
$rules = $db->loadObjectList() ?: [];
if (empty($rules))
{
return;
}
// Build a virtual ticket-like object from event data
$context = (object) array_merge([
'id' => 0,
'subject' => $eventData['subject'] ?? $event,
'body' => $eventData['body'] ?? '',
'status' => 'open',
'priority' => $eventData['priority'] ?? 'normal',
'created_by' => $eventData['user_id'] ?? 0,
'created' => gmdate('Y-m-d H:i:s'),
'age_hours' => 0,
], $eventData);
foreach ($rules as $rule)
{
$conditions = json_decode($rule->conditions, true) ?: [];
$actions = json_decode($rule->actions, true) ?: [];
if (empty($conditions) || $this->evaluateConditions($conditions, $context))
{
$this->executeActions($actions, 0, $context);
}
}
}
catch (\Throwable $e)
{
\Joomla\CMS\Log\Log::add('System event automation error: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokowaas');
}
}
/**
* Get all automation rules.
*/
@@ -938,11 +938,57 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
return;
}
// NOTE: warnMissingLicenseKey and enforceAdminRestrictions
// are now handled by feature plugins (deferred / tenant)
$this->protectPlugin();
}
// ------------------------------------------------------------------
// Automation event hooks (#151) — delegate to ticket automation engine
// ------------------------------------------------------------------
public function onUserLogin($user, $options = [])
{
$this->fireTicketAutomation('user_login', [
'user_id' => $user['id'] ?? 0,
'username' => $user['username'] ?? '',
'subject' => 'User login: ' . ($user['username'] ?? ''),
'body' => 'User ' . ($user['username'] ?? '') . ' logged in from ' . ($_SERVER['REMOTE_ADDR'] ?? ''),
]);
}
public function onUserAfterSave($user, $isNew, $success, $msg)
{
if ($isNew && $success)
{
$this->fireTicketAutomation('user_register', [
'user_id' => $user['id'] ?? 0,
'username' => $user['username'] ?? '',
'subject' => 'New user registered: ' . ($user['username'] ?? ''),
'body' => 'New user: ' . ($user['name'] ?? '') . ' (' . ($user['email'] ?? '') . ')',
]);
}
}
public function onUserLoginFailure($response)
{
$this->fireTicketAutomation('user_login_failed', [
'subject' => 'Failed login attempt',
'body' => 'Failed login from ' . ($_SERVER['REMOTE_ADDR'] ?? '') . ': ' . ($response['username'] ?? ''),
]);
}
private function fireTicketAutomation(string $event, array $data): void
{
try
{
$model = new \Moko\Component\MokoWaaS\Administrator\Model\TicketsModel();
$model->runSystemEventAutomation($event, $data);
}
catch (\Throwable $e)
{
// Silent — automation should never break the main flow
}
}
/**
* Inject visual branding into the document head.
*