- Added support for Faster Whisper transcription via Jigasi and Skynet. - Updated .env.example to include new environment variables for transcription settings. - Enhanced Jitsi Docker Compose configuration to include Skynet and Jigasi services. - Introduced new API endpoints for managing organizational folders in the drive service. - Updated Nextcloud initialization script to enable external file mounting. - Improved error handling and response structures in the drive API. - Added new properties for organization settings related to transcription and agenda management.
258 lines
6.8 KiB
Go
258 lines
6.8 KiB
Go
package automation
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/ultisuite/ulti-backend/internal/mail/rules"
|
|
"github.com/ultisuite/ulti-backend/internal/mail/webhooks"
|
|
"github.com/ultisuite/ulti-backend/internal/nextcloud"
|
|
)
|
|
|
|
type Dispatcher struct {
|
|
db *pgxpool.Pool
|
|
rules *rules.Engine
|
|
hooks *webhooks.Executor
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewDispatcher(db *pgxpool.Pool, rulesEngine *rules.Engine, hookExec *webhooks.Executor) *Dispatcher {
|
|
return &Dispatcher{
|
|
db: db,
|
|
rules: rulesEngine,
|
|
hooks: hookExec,
|
|
logger: slog.Default().With("component", "automation-dispatcher"),
|
|
}
|
|
}
|
|
|
|
type DrivePayload struct {
|
|
FilePath string
|
|
FileName string
|
|
MimeType string
|
|
FileSize int64
|
|
IsFolder bool
|
|
}
|
|
|
|
type ContactPayload struct {
|
|
ID string
|
|
BookID string
|
|
Name string
|
|
Email string
|
|
Phone string
|
|
Org string
|
|
Labels []string
|
|
}
|
|
|
|
func DrivePayloadFromPath(filePath string, isFolder bool) DrivePayload {
|
|
normalized := nextcloud.NormalizeClientPath(filePath)
|
|
name := path.Base(strings.TrimSuffix(normalized, "/"))
|
|
if name == "" || name == "." {
|
|
name = normalized
|
|
}
|
|
return DrivePayload{
|
|
FilePath: normalized,
|
|
FileName: name,
|
|
IsFolder: isFolder,
|
|
}
|
|
}
|
|
|
|
func (d *Dispatcher) OnMailCreated(ctx context.Context, userID, accountID, messageID string, msg *rules.Message) {
|
|
if d == nil || userID == "" || msg == nil {
|
|
return
|
|
}
|
|
evt := &rules.EventContext{Type: rules.TriggerMessageReceived}
|
|
if msg.AccountID == "" {
|
|
msg.AccountID = accountID
|
|
}
|
|
if msg.ID == "" {
|
|
msg.ID = messageID
|
|
}
|
|
d.runRules(ctx, userID, msg, evt)
|
|
d.dispatchWebhooks(ctx, userID, string(rules.TriggerMessageReceived), evt, msg, accountID, "", "", "")
|
|
}
|
|
|
|
func (d *Dispatcher) OnDriveEvent(ctx context.Context, externalUserID string, trigger rules.TriggerType, payload DrivePayload) {
|
|
if d == nil || externalUserID == "" {
|
|
return
|
|
}
|
|
userID, err := d.resolveUserID(ctx, externalUserID)
|
|
if err != nil {
|
|
d.logger.Error("resolve user for drive automation", "error", err, "sub", externalUserID)
|
|
return
|
|
}
|
|
evt := driveEventContext(trigger, payload)
|
|
msg := &rules.Message{}
|
|
d.runRules(ctx, userID, msg, evt)
|
|
d.dispatchWebhooks(ctx, userID, string(trigger), evt, msg, "", payload.FilePath, "", "")
|
|
}
|
|
|
|
func (d *Dispatcher) OnContactEvent(ctx context.Context, externalUserID string, trigger rules.TriggerType, payload ContactPayload) {
|
|
if d == nil || externalUserID == "" {
|
|
return
|
|
}
|
|
userID, err := d.resolveUserID(ctx, externalUserID)
|
|
if err != nil {
|
|
d.logger.Error("resolve user for contact automation", "error", err, "sub", externalUserID)
|
|
return
|
|
}
|
|
evt := contactEventContext(trigger, payload)
|
|
msg := &rules.Message{}
|
|
d.runRules(ctx, userID, msg, evt)
|
|
d.dispatchWebhooks(ctx, userID, string(trigger), evt, msg, "", "", payload.BookID, "")
|
|
}
|
|
|
|
func (d *Dispatcher) runRules(ctx context.Context, userID string, msg *rules.Message, evt *rules.EventContext) {
|
|
if d.rules == nil {
|
|
return
|
|
}
|
|
if err := d.rules.EvaluateMessageEvent(ctx, userID, msg, evt); err != nil {
|
|
d.logger.Error("rules evaluation", "error", err, "user_id", userID, "trigger", evt.Type)
|
|
}
|
|
}
|
|
|
|
type webhookTemplateRow struct {
|
|
id string
|
|
eventTypes []byte
|
|
mailScope []byte
|
|
driveScope []byte
|
|
contactsScope []byte
|
|
agendaScope []byte
|
|
}
|
|
|
|
func (d *Dispatcher) dispatchWebhooks(
|
|
ctx context.Context,
|
|
userID string,
|
|
eventType string,
|
|
evt *rules.EventContext,
|
|
msg *rules.Message,
|
|
accountID string,
|
|
drivePath string,
|
|
bookID string,
|
|
calendarID string,
|
|
) {
|
|
if d.hooks == nil || d.db == nil {
|
|
return
|
|
}
|
|
rows, err := d.db.Query(ctx, `
|
|
SELECT id, event_types, mail_scope, drive_scope, contacts_scope, agenda_scope
|
|
FROM webhook_templates
|
|
WHERE user_id = $1 AND is_active = true
|
|
`, userID)
|
|
if err != nil {
|
|
d.logger.Error("list webhook templates", "error", err)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
msgCtx := rules.WebhookContextFromEvent(evt, msg)
|
|
for rows.Next() {
|
|
var row webhookTemplateRow
|
|
if err := rows.Scan(&row.id, &row.eventTypes, &row.mailScope, &row.driveScope, &row.contactsScope, &row.agendaScope); err != nil {
|
|
d.logger.Error("scan webhook template", "error", err)
|
|
continue
|
|
}
|
|
if !webhookMatchesEvent(row, eventType) {
|
|
continue
|
|
}
|
|
if !webhookMatchesScope(row, accountID, drivePath, bookID, calendarID) {
|
|
continue
|
|
}
|
|
if err := d.hooks.Execute(ctx, row.id, msgCtx); err != nil {
|
|
d.logger.Error("webhook dispatch", "template_id", row.id, "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func webhookMatchesEvent(row webhookTemplateRow, eventType string) bool {
|
|
var types []string
|
|
if len(row.eventTypes) > 0 {
|
|
_ = json.Unmarshal(row.eventTypes, &types)
|
|
}
|
|
if len(types) == 0 {
|
|
return false
|
|
}
|
|
for _, t := range types {
|
|
if t == eventType {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func webhookMatchesScope(row webhookTemplateRow, accountID, drivePath, bookID, calendarID string) bool {
|
|
var mailScope MailScope
|
|
var driveScope DriveScope
|
|
var contactsScope ContactsScope
|
|
var agendaScope AgendaScope
|
|
_ = json.Unmarshal(row.mailScope, &mailScope)
|
|
_ = json.Unmarshal(row.driveScope, &driveScope)
|
|
_ = json.Unmarshal(row.contactsScope, &contactsScope)
|
|
_ = json.Unmarshal(row.agendaScope, &agendaScope)
|
|
|
|
if accountID != "" {
|
|
return AllowsMailScope(mailScope, accountID)
|
|
}
|
|
if drivePath != "" {
|
|
return AllowsDriveScope(driveScope, drivePath)
|
|
}
|
|
if bookID != "" {
|
|
return AllowsContactsScope(contactsScope, bookID)
|
|
}
|
|
if calendarID != "" {
|
|
return AllowsAgendaScope(agendaScope, calendarID)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (d *Dispatcher) resolveUserID(ctx context.Context, externalID string) (string, error) {
|
|
var userID string
|
|
err := d.db.QueryRow(ctx, `SELECT id FROM users WHERE external_id = $1`, externalID).Scan(&userID)
|
|
return userID, err
|
|
}
|
|
|
|
func driveEventContext(trigger rules.TriggerType, p DrivePayload) *rules.EventContext {
|
|
return &rules.EventContext{
|
|
Type: trigger,
|
|
FolderPath: path.Dir(p.FilePath),
|
|
DriveFileName: p.FileName,
|
|
DriveFilePath: p.FilePath,
|
|
DriveMimeType: p.MimeType,
|
|
DriveFileSize: p.FileSize,
|
|
DriveIsFolder: p.IsFolder,
|
|
}
|
|
}
|
|
|
|
func contactEventContext(trigger rules.TriggerType, p ContactPayload) *rules.EventContext {
|
|
return &rules.EventContext{
|
|
Type: trigger,
|
|
ContactLabel: strings.Join(p.Labels, ", "),
|
|
ContactID: p.ID,
|
|
ContactName: p.Name,
|
|
ContactEmail: p.Email,
|
|
ContactPhone: p.Phone,
|
|
ContactOrg: p.Org,
|
|
ContactBookID: p.BookID,
|
|
}
|
|
}
|
|
|
|
func EventDomain(trigger rules.TriggerType) string {
|
|
switch trigger {
|
|
case rules.TriggerDriveFileCreated, rules.TriggerDriveFileUpdated, rules.TriggerDriveFileDeleted,
|
|
rules.TriggerDriveFileMoved, rules.TriggerDriveShareUpdated:
|
|
return "drive"
|
|
case rules.TriggerContactCreated, rules.TriggerContactUpdated, rules.TriggerContactDeleted:
|
|
return "contacts"
|
|
default:
|
|
return "mail"
|
|
}
|
|
}
|
|
|
|
func NowISO() string {
|
|
return time.Now().UTC().Format(time.RFC3339)
|
|
}
|