From 29f5a56f21e838d96ddcf793c2ca93c9d2e2152b Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 19 Nov 2025 20:15:36 +0100 Subject: [PATCH] fixed notifications dispatch --- .../server/notificationimp/notification.go | 9 +- .../server/notificationimp/telegram/client.go | 12 +- .../notificationimp/telegram/contact.go | 3 +- .../server/notificationimp/telegram/demo.go | 5 +- .../notificationimp/telegram/message.go | 94 +++++++++++++-- .../notifications/site/notification.go | 110 +++++++++-------- .../messaging/notifications/site/processor.go | 114 +++++++++--------- api/pkg/model/notificationevent.go | 7 +- api/proto/contact_request.proto | 12 -- api/proto/demo_request.proto | 12 -- api/proto/site_request.proto | 36 ++++++ 11 files changed, 258 insertions(+), 156 deletions(-) delete mode 100644 api/proto/contact_request.proto delete mode 100644 api/proto/demo_request.proto create mode 100644 api/proto/site_request.proto diff --git a/api/notification/internal/server/notificationimp/notification.go b/api/notification/internal/server/notificationimp/notification.go index 41b3e01..8fb57b8 100644 --- a/api/notification/internal/server/notificationimp/notification.go +++ b/api/notification/internal/server/notificationimp/notification.go @@ -80,13 +80,8 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { return nil, err } - if err := a.Register().Consumer(snotifications.NewDemoRequestProcessor(p.logger, p.onDemoRequest)); err != nil { - p.logger.Error("Failed to register demo request handler", zap.Error(err)) - return nil, err - } - - if err := a.Register().Consumer(snotifications.NewContactRequestProcessor(p.logger, p.onContactRequest)); err != nil { - p.logger.Error("Failed to register contact request handler", zap.Error(err)) + if err := a.Register().Consumer(snotifications.NewSiteRequestProcessor(p.logger, p.onDemoRequest, p.onContactRequest)); err != nil { + p.logger.Error("Failed to register site request handler", zap.Error(err)) return nil, err } diff --git a/api/notification/internal/server/notificationimp/telegram/client.go b/api/notification/internal/server/notificationimp/telegram/client.go index 8b8e68c..1fed9b3 100644 --- a/api/notification/internal/server/notificationimp/telegram/client.go +++ b/api/notification/internal/server/notificationimp/telegram/client.go @@ -33,7 +33,7 @@ type client struct { botToken string chatID string threadID *int64 - parseMode string + parseMode parseMode } type sendMessagePayload struct { @@ -80,9 +80,9 @@ func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, er if apiURL == "" { apiURL = defaultAPIURL } - parseMode := strings.TrimSpace(cfg.ParseMode) - if parseMode == "" { - parseMode = "Markdown" + mode := normalizeParseMode(cfg.ParseMode) + if mode == parseModeUnset { + mode = parseModeMarkdown } return &client{ @@ -94,7 +94,7 @@ func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, er botToken: token, chatID: chatID, threadID: threadID, - parseMode: parseMode, + parseMode: mode, }, nil } @@ -166,7 +166,7 @@ func (c *client) sendForm(ctx context.Context, template messageTemplate) error { payload := sendMessagePayload{ ChatID: c.chatID, Text: message, - ParseMode: c.parseMode, + ParseMode: c.parseMode.String(), ThreadID: c.threadID, DisablePreview: true, } diff --git a/api/notification/internal/server/notificationimp/telegram/contact.go b/api/notification/internal/server/notificationimp/telegram/contact.go index 7f94750..fc73d22 100644 --- a/api/notification/internal/server/notificationimp/telegram/contact.go +++ b/api/notification/internal/server/notificationimp/telegram/contact.go @@ -4,7 +4,8 @@ import "github.com/tech/sendico/pkg/model" func newContactRequestTemplate(request *model.ContactRequest) messageTemplate { return messageTemplate{ - title: "New contact request received", + title: "New site request received", + emphasize: []string{"site request"}, fields: []messageField{ {label: "Name", value: request.Name}, {label: "Email", value: request.Email}, diff --git a/api/notification/internal/server/notificationimp/telegram/demo.go b/api/notification/internal/server/notificationimp/telegram/demo.go index d2afc20..3965f98 100644 --- a/api/notification/internal/server/notificationimp/telegram/demo.go +++ b/api/notification/internal/server/notificationimp/telegram/demo.go @@ -18,7 +18,8 @@ func newDemoRequestTemplate(request *model.DemoRequest) messageTemplate { fields = append(fields, messageField{label: "Comment", value: request.Comment}) } return messageTemplate{ - title: "New demo request received", - fields: fields, + title: "New demo request received", + fields: fields, + emphasize: []string{"demo request"}, } } diff --git a/api/notification/internal/server/notificationimp/telegram/message.go b/api/notification/internal/server/notificationimp/telegram/message.go index db2c515..e8468dc 100644 --- a/api/notification/internal/server/notificationimp/telegram/message.go +++ b/api/notification/internal/server/notificationimp/telegram/message.go @@ -6,23 +6,50 @@ import ( "strings" ) +type parseMode string + +const ( + parseModeUnset parseMode = "" + parseModeMarkdown parseMode = "markdown" + parseModeMarkdownV2 parseMode = "markdownV2" + parseModeHTML parseMode = "HTML" +) + +func normalizeParseMode(value string) parseMode { + switch strings.ToLower(strings.TrimSpace(value)) { + case "markdown": + return parseModeMarkdown + case "markdownv2": + return parseModeMarkdownV2 + case "html": + return parseModeHTML + default: + return parseModeUnset + } +} + +func (pm parseMode) String() string { + return string(pm) +} + type messageField struct { label string value string } type messageTemplate struct { - title string - fields []messageField + title string + fields []messageField + emphasize []string } -func (mt messageTemplate) Format(parseMode string) string { +func (mt messageTemplate) Format(mode parseMode) string { var builder strings.Builder - builder.WriteString(mt.title) + builder.WriteString(formatTitle(mode, mt.title, mt.emphasize)) builder.WriteString("\n") builder.WriteString("-----------------------------\n") - formatter := selectValueFormatter(parseMode) + formatter := selectValueFormatter(mode) for _, field := range mt.fields { appendMessageField(&builder, field.label, field.value, formatter) } @@ -31,6 +58,53 @@ func (mt messageTemplate) Format(parseMode string) string { type valueFormatter func(string) string +func formatTitle(mode parseMode, title string, emphasize []string) string { + switch mode { + case parseModeMarkdown: + return highlightMarkdown(title, emphasize, escapeMarkdown) + case parseModeMarkdownV2: + return highlightMarkdown(title, emphasize, escapeMarkdownV2) + case parseModeHTML: + return highlightHTML(title, emphasize) + default: + return title + } +} + +func highlightMarkdown(title string, emphasize []string, esc func(string) string) string { + if len(emphasize) == 0 { + return title + } + result := title + for _, word := range emphasize { + word = strings.TrimSpace(word) + if word == "" { + continue + } + escaped := esc(word) + replacement := fmt.Sprintf("*%s*", escaped) + result = strings.ReplaceAll(result, word, replacement) + } + return result +} + +func highlightHTML(title string, emphasize []string) string { + if len(emphasize) == 0 { + return html.EscapeString(title) + } + result := html.EscapeString(title) + for _, word := range emphasize { + word = strings.TrimSpace(word) + if word == "" { + continue + } + escaped := html.EscapeString(word) + replacement := fmt.Sprintf("%s", escaped) + result = strings.ReplaceAll(result, escaped, replacement) + } + return result +} + func appendMessageField(builder *strings.Builder, label, value string, formatter valueFormatter) { value = strings.TrimSpace(value) if value == "" { @@ -41,17 +115,17 @@ func appendMessageField(builder *strings.Builder, label, value string, formatter fmt.Fprintf(builder, "• %s: %s\n", label, value) } -func selectValueFormatter(parseMode string) valueFormatter { - switch strings.ToLower(parseMode) { - case "markdown": +func selectValueFormatter(mode parseMode) valueFormatter { + switch mode { + case parseModeMarkdown: return func(value string) string { return fmt.Sprintf("*%s*", escapeMarkdown(value)) } - case "markdownv2": + case parseModeMarkdownV2: return func(value string) string { return fmt.Sprintf("*%s*", escapeMarkdownV2(value)) } - case "html": + case parseModeHTML: return func(value string) string { return fmt.Sprintf("%s", html.EscapeString(value)) } diff --git a/api/pkg/messaging/internal/notifications/site/notification.go b/api/pkg/messaging/internal/notifications/site/notification.go index 9387fc6..47e9ed1 100644 --- a/api/pkg/messaging/internal/notifications/site/notification.go +++ b/api/pkg/messaging/internal/notifications/site/notification.go @@ -10,72 +10,84 @@ import ( "google.golang.org/protobuf/proto" ) -type DemoRequestNotification struct { +type SiteRequestNotification struct { messaging.Envelope - request *model.DemoRequest + requestType gmessaging.SiteRequestEvent_RequestType + demoRequest *model.DemoRequest + contactRequest *model.ContactRequest } -func (drn *DemoRequestNotification) Serialize() ([]byte, error) { - if drn.request == nil { - return nil, merrors.InvalidArgument("demo request payload is empty", "request") +func (srn *SiteRequestNotification) Serialize() ([]byte, error) { + msg := gmessaging.SiteRequestEvent{ + Type: srn.requestType, } - msg := gmessaging.DemoRequestEvent{ - Name: drn.request.Name, - OrganizationName: drn.request.OrganizationName, - Phone: drn.request.Phone, - WorkEmail: drn.request.WorkEmail, - PayoutVolume: drn.request.PayoutVolume, - Comment: drn.request.Comment, + + switch srn.requestType { + case gmessaging.SiteRequestEvent_REQUEST_TYPE_DEMO: + if srn.demoRequest == nil { + return nil, merrors.InvalidArgument("demo request payload is empty", "request") + } + msg.Payload = &gmessaging.SiteRequestEvent_Demo{ + Demo: &gmessaging.SiteDemoRequest{ + Name: srn.demoRequest.Name, + OrganizationName: srn.demoRequest.OrganizationName, + Phone: srn.demoRequest.Phone, + WorkEmail: srn.demoRequest.WorkEmail, + PayoutVolume: srn.demoRequest.PayoutVolume, + Comment: srn.demoRequest.Comment, + }, + } + case gmessaging.SiteRequestEvent_REQUEST_TYPE_CONTACT: + if srn.contactRequest == nil { + return nil, merrors.InvalidArgument("contact request payload is empty", "request") + } + msg.Payload = &gmessaging.SiteRequestEvent_Contact{ + Contact: &gmessaging.SiteContactRequest{ + Name: srn.contactRequest.Name, + Email: srn.contactRequest.Email, + Phone: srn.contactRequest.Phone, + Company: srn.contactRequest.Company, + Topic: srn.contactRequest.Topic, + Message: srn.contactRequest.Message, + }, + } + default: + return nil, merrors.InvalidArgument("unsupported site request type", "type") } + data, err := proto.Marshal(&msg) if err != nil { return nil, err } - return drn.Envelope.Wrap(data) + return srn.Envelope.Wrap(data) +} + +func newSiteRequestEvent() model.NotificationEvent { + return model.NewNotification(mservice.Site, nm.NACreated) } func NewDemoRequestEvent() model.NotificationEvent { - return model.NewNotification(mservice.Site, nm.NACreated) -} - -func NewDemoRequestEnvelope(sender string, request *model.DemoRequest) messaging.Envelope { - return &DemoRequestNotification{ - Envelope: messaging.CreateEnvelope(sender, NewDemoRequestEvent()), - request: request, - } -} - -type ContactRequestNotification struct { - messaging.Envelope - request *model.ContactRequest -} - -func (crn *ContactRequestNotification) Serialize() ([]byte, error) { - if crn.request == nil { - return nil, merrors.InvalidArgument("contact request payload is empty", "request") - } - msg := gmessaging.ContactRequestEvent{ - Name: crn.request.Name, - Email: crn.request.Email, - Phone: crn.request.Phone, - Company: crn.request.Company, - Topic: crn.request.Topic, - Message: crn.request.Message, - } - data, err := proto.Marshal(&msg) - if err != nil { - return nil, err - } - return crn.Envelope.Wrap(data) + return newSiteRequestEvent() } func NewContactRequestEvent() model.NotificationEvent { - return model.NewNotification(mservice.Site, nm.NACreated) + return newSiteRequestEvent() +} + +func NewDemoRequestEnvelope(sender string, request *model.DemoRequest) messaging.Envelope { + return &SiteRequestNotification{ + Envelope: messaging.CreateEnvelope(sender, newSiteRequestEvent()), + requestType: gmessaging.SiteRequestEvent_REQUEST_TYPE_DEMO, + demoRequest: request, + contactRequest: nil, + } } func NewContactRequestEnvelope(sender string, request *model.ContactRequest) messaging.Envelope { - return &ContactRequestNotification{ - Envelope: messaging.CreateEnvelope(sender, NewContactRequestEvent()), - request: request, + return &SiteRequestNotification{ + Envelope: messaging.CreateEnvelope(sender, newSiteRequestEvent()), + requestType: gmessaging.SiteRequestEvent_REQUEST_TYPE_CONTACT, + contactRequest: request, + demoRequest: nil, } } diff --git a/api/pkg/messaging/notifications/site/processor.go b/api/pkg/messaging/notifications/site/processor.go index a58ea29..1cc70f4 100644 --- a/api/pkg/messaging/notifications/site/processor.go +++ b/api/pkg/messaging/notifications/site/processor.go @@ -14,72 +14,74 @@ import ( "google.golang.org/protobuf/proto" ) -type DemoRequestProcessor struct { - logger mlogger.Logger - handler handler.DemoRequestHandler - event model.NotificationEvent +type SiteRequestProcessor struct { + logger mlogger.Logger + demoHandler handler.DemoRequestHandler + contactHandler handler.ContactRequestHandler + event model.NotificationEvent } -func (drp *DemoRequestProcessor) Process(ctx context.Context, envelope me.Envelope) error { - var msg gmessaging.DemoRequestEvent +func (srp *SiteRequestProcessor) Process(ctx context.Context, envelope me.Envelope) error { + var msg gmessaging.SiteRequestEvent if err := proto.Unmarshal(envelope.GetData(), &msg); err != nil { - drp.logger.Warn("Failed to decode demo request envelope", zap.Error(err), zap.String("topic", drp.event.ToString())) + srp.logger.Warn("Failed to decode site request envelope", zap.Error(err), zap.String("topic", srp.event.ToString())) return err } - request := &model.DemoRequest{ - Name: msg.GetName(), - OrganizationName: msg.GetOrganizationName(), - Phone: msg.GetPhone(), - WorkEmail: msg.GetWorkEmail(), - PayoutVolume: msg.GetPayoutVolume(), - Comment: msg.GetComment(), - } - return drp.handler(ctx, request) -} -func (drp *DemoRequestProcessor) GetSubject() model.NotificationEvent { - return drp.event -} - -func NewDemoRequestProcessor(logger mlogger.Logger, handler handler.DemoRequestHandler) np.EnvelopeProcessor { - return &DemoRequestProcessor{ - logger: logger.Named("demo_request_processor"), - handler: handler, - event: internalsite.NewDemoRequestEvent(), + switch msg.GetType() { + case gmessaging.SiteRequestEvent_REQUEST_TYPE_DEMO: + if srp.demoHandler == nil { + srp.logger.Warn("Demo request handler is not configured") + return nil + } + demo := msg.GetDemo() + if demo == nil { + srp.logger.Warn("Demo request payload is empty") + return nil + } + request := &model.DemoRequest{ + Name: demo.GetName(), + OrganizationName: demo.GetOrganizationName(), + Phone: demo.GetPhone(), + WorkEmail: demo.GetWorkEmail(), + PayoutVolume: demo.GetPayoutVolume(), + Comment: demo.GetComment(), + } + return srp.demoHandler(ctx, request) + case gmessaging.SiteRequestEvent_REQUEST_TYPE_CONTACT: + if srp.contactHandler == nil { + srp.logger.Warn("Contact request handler is not configured") + return nil + } + contact := msg.GetContact() + if contact == nil { + srp.logger.Warn("Contact request payload is empty") + return nil + } + request := &model.ContactRequest{ + Name: contact.GetName(), + Email: contact.GetEmail(), + Phone: contact.GetPhone(), + Company: contact.GetCompany(), + Topic: contact.GetTopic(), + Message: contact.GetMessage(), + } + return srp.contactHandler(ctx, request) + default: + srp.logger.Warn("Received site request with unsupported type", zap.Any("type", msg.GetType())) + return nil } } -type ContactRequestProcessor struct { - logger mlogger.Logger - handler handler.ContactRequestHandler - event model.NotificationEvent +func (srp *SiteRequestProcessor) GetSubject() model.NotificationEvent { + return srp.event } -func (crp *ContactRequestProcessor) Process(ctx context.Context, envelope me.Envelope) error { - var msg gmessaging.ContactRequestEvent - if err := proto.Unmarshal(envelope.GetData(), &msg); err != nil { - crp.logger.Warn("Failed to decode contact request envelope", zap.Error(err), zap.String("topic", crp.event.ToString())) - return err - } - request := &model.ContactRequest{ - Name: msg.GetName(), - Email: msg.GetEmail(), - Phone: msg.GetPhone(), - Company: msg.GetCompany(), - Topic: msg.GetTopic(), - Message: msg.GetMessage(), - } - return crp.handler(ctx, request) -} - -func (crp *ContactRequestProcessor) GetSubject() model.NotificationEvent { - return crp.event -} - -func NewContactRequestProcessor(logger mlogger.Logger, handler handler.ContactRequestHandler) np.EnvelopeProcessor { - return &ContactRequestProcessor{ - logger: logger.Named("contact_request_processor"), - handler: handler, - event: internalsite.NewContactRequestEvent(), +func NewSiteRequestProcessor(logger mlogger.Logger, demo handler.DemoRequestHandler, contact handler.ContactRequestHandler) np.EnvelopeProcessor { + return &SiteRequestProcessor{ + logger: logger.Named("site_request_processor"), + demoHandler: demo, + contactHandler: contact, + event: internalsite.NewDemoRequestEvent(), } } diff --git a/api/pkg/model/notificationevent.go b/api/pkg/model/notificationevent.go index de52d20..d6d0024 100644 --- a/api/pkg/model/notificationevent.go +++ b/api/pkg/model/notificationevent.go @@ -71,7 +71,12 @@ func FromString(s string) (*NotificationEventImp, error) { func StringToNotificationAction(s string) (nm.NotificationAction, error) { switch nm.NotificationAction(s) { - case nm.NACreated, nm.NAPending, nm.NAUpdated, nm.NADeleted, nm.NAAssigned, nm.NAPasswordReset: + case nm.NACreated, + nm.NAPending, + nm.NAUpdated, + nm.NADeleted, + nm.NAAssigned, + nm.NAPasswordReset: return nm.NotificationAction(s), nil default: return "", merrors.DataConflict("invalid Notification action: " + s) diff --git a/api/proto/contact_request.proto b/api/proto/contact_request.proto deleted file mode 100644 index 08f219a..0000000 --- a/api/proto/contact_request.proto +++ /dev/null @@ -1,12 +0,0 @@ -syntax = "proto3"; - -option go_package = "github.com/tech/sendico/pkg/generated/gmessaging"; - -message ContactRequestEvent { - string Name = 1; - string Email = 2; - string Phone = 3; - string Company = 4; - string Topic = 5; - string Message = 6; -} diff --git a/api/proto/demo_request.proto b/api/proto/demo_request.proto deleted file mode 100644 index 68aeea5..0000000 --- a/api/proto/demo_request.proto +++ /dev/null @@ -1,12 +0,0 @@ -syntax = "proto3"; - -option go_package = "github.com/tech/sendico/pkg/generated/gmessaging"; - -message DemoRequestEvent { - string Name = 1; - string OrganizationName = 2; - string Phone = 3; - string WorkEmail = 4; - string PayoutVolume = 5; - string Comment = 6; -} diff --git a/api/proto/site_request.proto b/api/proto/site_request.proto new file mode 100644 index 0000000..9187d17 --- /dev/null +++ b/api/proto/site_request.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +option go_package = "github.com/tech/sendico/pkg/generated/gmessaging"; + +message SiteRequestEvent { + enum RequestType { + REQUEST_TYPE_UNSPECIFIED = 0; + REQUEST_TYPE_DEMO = 1; + REQUEST_TYPE_CONTACT = 2; + } + + RequestType type = 1; + + oneof payload { + SiteDemoRequest demo = 2; + SiteContactRequest contact = 3; + } +} + +message SiteDemoRequest { + string name = 1; + string organization_name = 2; + string phone = 3; + string work_email = 4; + string payout_volume = 5; + string comment = 6; +} + +message SiteContactRequest { + string name = 1; + string email = 2; + string phone = 3; + string company = 4; + string topic = 5; + string message = 6; +}