package notifier import ( "encoding/json" "errors" "log" "time" "gorm.io/gorm" "rocketgit.ru/rsmon/worker/app/models" "rocketgit.ru/rsmon/worker/internal/notifyrender" "rocketgit.ru/rsmon/worker/internal/wire" ) // langEN is the wire-side default language tag used when a Message carries // no language hint of its own. Centralized so the literal does not appear // three or more times across this package (goconst). const langEN = "en" // ContactKindToMethod maps the legacy Contact.Kind enum used by the sender onto // the worker notification_method enum introduced in // docs/plans/worker-notifier-mvp.md section 4.3. sms/voice remain placeholders // until phase 4. // //nolint:goconst // match arm values must be the wire-method enum literals func ContactKindToMethod(kind string) string { switch kind { case "email": return "email" case "telegram_private", "telegram_group": return "telegram" case "webhook": return "webhook" case "mattermost": return "mattermost" case "sms": return "sms" case "voice": return "voice" } return "" } // RenderNotificationContent pre-renders subject + bodies for one Message using // the existing internal/sender/get_content.go helpers. The result is what the // worker binary consumes directly so it does not need access to Message/Event // rows, workdays, or NotificationDayStart logic on the data plane. // // Returns the four bodies (subject, text, markdown, html). The caller is // responsible for passing them through to EnqueueNotificationTask. func RenderNotificationContent(msg *models.Message, now time.Time) (subject, bodyText, bodyMarkdown, bodyHTML string, err error) { if msg == nil { return "", "", "", "", errors.New("notifier: nil message") } defer func() { if r := recover(); r != nil { err = errors.New("notifier: render panicked") } }() sbuf, tbuf, mbuf, hbuf := notifyrender.GetContent(msg, now) return sbuf.String(), tbuf.String(), mbuf.String(), hbuf.String(), nil } // EnqueueNotificationTaskFromMessage is the producer-side hook called from // performEvents (or its replacement). It builds a wire.NotificationTask from the // freshly created Message and enqueues one Task row keyed by the stable // (notification, contact, first-event) idempotency key. // // If the producer's authorization precheck fails the function returns an error: // worker notification tasks are now the only delivery path. func EnqueueNotificationTaskFromMessage(n *models.Notification, c *models.Contact, msg *models.Message) (*models.Task, error) { return enqueueNotificationTaskFromMessageTx(models.DB(), n, c, msg) } // enqueueNotificationTaskFromMessageTx keeps message creation and durable task // production in the caller's notifier transaction. func enqueueNotificationTaskFromMessageTx(tx *gorm.DB, n *models.Notification, c *models.Contact, msg *models.Message) (*models.Task, error) { if msg == nil || n == nil || c == nil { return nil, errors.New("notifier: nil message/notification/contact") } if len(msg.Events) == 0 { return nil, errors.New("notifier: message has no events (exp messages go through a separate path)") } method := ContactKindToMethod(c.Kind) if method == "" { log.Printf("notifier: unknown contact kind %q for contact %d, skipping enqueue", c.Kind, c.ID) return nil, nil } now := time.Now() subject, bodyText, bodyMarkdown, bodyHTML, err := RenderNotificationContent(msg, now) if err != nil { log.Printf("notifier: render content failed for message %d: %v", msg.ID, err) return nil, err } checkID := msg.CheckID monitorID := msg.Events[0].MonitorID task := wire.NotificationTask{ AccountID: n.AccountID, MessageID: msg.ID, NotificationID: n.ID, EventIDs: eventIDs(msg), CheckID: checkID, MonitorID: &monitorID, Method: method, Contact: wire.NotificationContact{ ID: c.ID, Kind: c.Kind, Value: c.Value, Name: c.Name, }, Subject: subject, BodyText: bodyText, BodyMarkdown: bodyMarkdown, BodyHTML: bodyHTML, Language: langEN, MessageKind: msg.Kind, } payload, err := json.Marshal(task) if err != nil { return nil, err } contactID := c.ID monitorPtr := task.MonitorID checkPtr := task.CheckID messagePtr := msg.ID taskRow, err := models.EnqueueNotificationTaskTx(tx, &models.EnqueueNotificationTaskInput{ AccountID: n.AccountID, NotificationID: n.ID, ContactID: contactID, MessageID: &messagePtr, MonitorID: monitorPtr, CheckID: checkPtr, EventIDs: task.EventIDs, Method: method, Subject: subject, BodyText: bodyText, BodyHTML: bodyHTML, BodyMarkdown: bodyMarkdown, Language: task.Language, MessageKind: msg.Kind, NotBefore: now, Payload: payload, }) if err != nil { return nil, err } log.Printf( "notifier: task enqueued id=%d job=%s kind=notification account=%d method=%s notification=%d contact=%d event=%d idempotency=%s", taskRow.ID, taskRow.JobID, n.AccountID, method, n.ID, c.ID, task.EventIDs[0], taskRow.IdempotencyKey, ) return taskRow, nil } func eventIDs(msg *models.Message) []int64 { out := make([]int64, 0, len(msg.Events)) for _, e := range msg.Events { //nolint:gocritic // range copy is acceptable here out = append(out, e.ID) } return out }