Files
worker/internal/notifier/producer.go
Gleb Tv 2c884c5612
Некоторые проверки не удались
CI / test (push) Successful in 2m5s
Docker / Build and publish worker image (push) Failing after 31s
refactor: adopt worker module path
2026-07-13 17:56:12 +03:00

167 строки
5.2 KiB
Go

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
}