Некоторые проверки не удались
CI / test (push) Successful in 2m5s
Docker / Build and publish worker image (push) Failing after 31s
167 строки
5.2 KiB
Go
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
|
|
}
|