import nodemailer from 'nodemailer'; import type { Transporter } from 'nodemailer'; import { config } from '../config.ts'; import type { Db } from '../db/index.ts'; import { customFieldValues, transactions } from '../db/schema.ts'; export interface ActionExecutor { execute(payload: ActionPayload): Promise<{ success: boolean; message: string }>; } export interface ActionPayload { scripId: string; scripName: string; actionType: string; actionConfig: Record; ticketId?: string; recipients?: string[]; subject?: string; body?: string; url?: string; method?: string; headers?: Record; field_id?: string; value?: string; } export class SendEmail implements ActionExecutor { private transporter: Transporter | null = null; private getTransporter(): Transporter { if (!this.transporter) { this.transporter = nodemailer.createTransport({ host: config.SMTP_HOST, port: config.SMTP_PORT, secure: config.SMTP_PORT === 465, ...(config.SMTP_USER ? { auth: { user: config.SMTP_USER, pass: config.SMTP_PASS } } : {}), }); } return this.transporter; } async execute(payload: ActionPayload): Promise<{ success: boolean; message: string }> { const recipients = payload.recipients ?? (payload.actionConfig['recipients'] as string[] | undefined) ?? []; const subject = payload.subject ?? (payload.actionConfig['subject'] as string | undefined); const body = payload.body ?? (payload.actionConfig['body'] as string | undefined); if (recipients.length === 0) { return { success: false, message: 'SendEmail: no recipients configured' }; } try { await this.getTransporter().sendMail({ from: config.SMTP_FROM, to: recipients.join(', '), subject: subject ?? 'No subject', text: body ?? '', }); return { success: true, message: `Email sent to ${recipients.join(', ')}` }; } catch (err) { const message = err instanceof Error ? err.message : String(err); return { success: false, message: `SendEmail failed: ${message}` }; } } } export class Webhook implements ActionExecutor { async execute(payload: ActionPayload): Promise<{ success: boolean; message: string }> { const url = payload.url ?? (payload.actionConfig['url'] as string | undefined); const method = payload.method ?? (payload.actionConfig['method'] as string | undefined) ?? 'POST'; const headers = (payload.headers ?? payload.actionConfig['headers'] ?? {}) as Record; const body = payload.body ?? (payload.actionConfig['body'] as string | undefined); if (!url) { return { success: false, message: 'Webhook: no URL configured' }; } try { const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json', ...headers }, body: body ?? undefined, }); if (!response.ok) { return { success: false, message: `Webhook failed: HTTP ${response.status}` }; } return { success: true, message: `Webhook POST to ${url} succeeded` }; } catch (err) { const message = err instanceof Error ? err.message : String(err); return { success: false, message: `Webhook failed: ${message}` }; } } } export class SetCustomField implements ActionExecutor { constructor(private db: Db) {} async execute(payload: ActionPayload): Promise<{ success: boolean; message: string }> { const fieldId = payload.field_id ?? String(payload.actionConfig['field_id'] ?? ''); const value = payload.value ?? String(payload.actionConfig['value'] ?? ''); const ticketId = payload.ticketId ?? String(payload.actionConfig['ticket_id'] ?? ''); if (!fieldId || !value || !ticketId) { return { success: false, message: 'SetCustomField: missing field_id, value, or ticket_id' }; } try { await this.db.insert(customFieldValues).values({ custom_field_id: fieldId, ticket_id: ticketId, value, }); return { success: true, message: `Custom field ${fieldId} set to "${value}"` }; } catch (err) { const message = err instanceof Error ? err.message : String(err); return { success: false, message: `SetCustomField failed: ${message}` }; } } } export class CreateTransaction implements ActionExecutor { constructor(private db: Db) {} async execute(payload: ActionPayload): Promise<{ success: boolean; message: string }> { const ticketId = payload.ticketId ?? String(payload.actionConfig['ticket_id'] ?? ''); const transactionType = String(payload.actionConfig['transaction_type'] ?? ''); const field = payload.actionConfig['field'] as string | undefined ?? null; const oldValue = payload.actionConfig['old_value'] as string | undefined ?? null; const newValue = payload.actionConfig['new_value'] as string | undefined ?? null; if (!ticketId || !transactionType) { return { success: false, message: 'CreateTransaction: missing ticket_id or transaction_type' }; } try { await this.db.insert(transactions).values({ ticket_id: ticketId, transaction_type: transactionType, field, old_value: oldValue, new_value: newValue, creator_id: '00000000-0000-0000-0000-000000000000', }); return { success: true, message: `Transaction ${transactionType} created for ticket ${ticketId}` }; } catch (err) { const message = err instanceof Error ? err.message : String(err); return { success: false, message: `CreateTransaction failed: ${message}` }; } } } export function createActionRegistry(db: Db): Record { return { SendEmail: new SendEmail(), Webhook: new Webhook(), SetCustomField: new SetCustomField(db), CreateTransaction: new CreateTransaction(db), }; } let _actionRegistry: Record | null = null; export function getActionRegistry(db: Db): Record { if (!_actionRegistry) { _actionRegistry = createActionRegistry(db); } return _actionRegistry; } export function getActionExecutor(db: Db, type: string): ActionExecutor | null { return getActionRegistry(db)[type] ?? null; }