better message formatting
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful

This commit is contained in:
Stephan D
2025-11-19 13:54:25 +01:00
parent 62956b06ca
commit 717dafc673
26 changed files with 202 additions and 56 deletions

View File

@@ -61,7 +61,7 @@ api:
thread_id_env: TELEGRAM_THREAD_ID
api_url: "https://api.telegram.org"
timeout_seconds: 10
parse_mode: ""
parse_mode: markdown
localizer:
path: "./i18n"

View File

@@ -42,7 +42,7 @@ func (mb *MessageBuilderImp) AddData(key, value string) mmail.MailBuilder {
func (mb *MessageBuilderImp) Build() (mmail.Message, error) {
if len(mb.message.recipients) == 0 {
return nil, merrors.InvalidArgument("Recipient not set")
return nil, merrors.InvalidArgument("Recipient not set", "recipients")
}
return mb.message, nil
}

View File

@@ -69,7 +69,7 @@ func (c *Client) Send(r mmail.MailBuilder) error {
c.logger.Warn("Malformed messge", zap.String("template_id", m.TemplateID()),
zap.String("locale", m.Locale()), zap.Strings("recipients", m.Recipients()),
zap.Int("body_size", len(body)))
return merrors.InvalidArgument("malformed message")
return merrors.InvalidArgument("malformed message", "message.body", "message.recipients")
}
subj, err := mailkey.Subject(c.l, m.Parameters(), m.TemplateID(), m.Locale())
if err != nil {

View File

@@ -1,6 +1,7 @@
package mail
import (
"github.com/mitchellh/mapstructure"
"github.com/tech/sendico/notification/interface/api/localizer"
notification "github.com/tech/sendico/notification/interface/services/notification/config"
mi "github.com/tech/sendico/notification/internal/server/notificationimp/mail/internal"
@@ -9,7 +10,6 @@ import (
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger"
"github.com/mitchellh/mapstructure"
"go.uber.org/zap"
)
@@ -22,7 +22,7 @@ type Config = notification.Config
func createMailClient(logger mlogger.Logger, producer messaging.Producer, l localizer.Localizer, dp domainprovider.DomainProvider, config *Config) (Client, error) {
if len(config.Driver) == 0 {
return nil, merrors.InvalidArgument("Mail driver name must be provided")
return nil, merrors.InvalidArgument("Mail driver name must be provided", "config.driver")
}
logger.Info("Connecting mail client...", zap.String("driver", config.Driver))
if config.Driver == "dummy" {
@@ -45,7 +45,7 @@ func createMailClient(logger mlogger.Logger, producer messaging.Producer, l loca
return mi.NewClient(logger, l, dp, &gsmconfing), nil
}
return nil, merrors.InvalidArgument("Unkwnown mail driver: " + config.Driver)
return nil, merrors.InvalidArgument("Unkwnown mail driver: "+config.Driver, "config.driver")
}
func CreateMailClient(logger mlogger.Logger, sender string, producer messaging.Producer, l localizer.Localizer, dp domainprovider.DomainProvider, config *Config) (Client, error) {

View File

@@ -39,10 +39,10 @@ func CreateAPI(a api.API) (*NotificationAPI, error) {
p.logger = a.Logger().Named(p.Name())
if a.Config().Notification == nil {
return nil, merrors.InvalidArgument("notification configuration is missing")
return nil, merrors.InvalidArgument("notification configuration is missing", "config.notification")
}
if a.Config().Notification.Telegram == nil {
return nil, merrors.InvalidArgument("telegram configuration is missing")
return nil, merrors.InvalidArgument("telegram configuration is missing", "config.notification.telegram")
}
var err error

View File

@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"html"
"io"
"net/http"
"os"
@@ -47,15 +48,15 @@ type sendMessagePayload struct {
func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, error) {
if cfg == nil {
return nil, merrors.InvalidArgument("telegram configuration is not provided")
return nil, merrors.InvalidArgument("telegram configuration is not provided", "config.notification.telegram")
}
token := strings.TrimSpace(os.Getenv(cfg.BotTokenEnv))
if token == "" {
return nil, merrors.InvalidArgument(fmt.Sprintf("telegram bot token env %s is empty", cfg.BotTokenEnv))
return nil, merrors.InvalidArgument(fmt.Sprintf("telegram bot token env %s is empty", cfg.BotTokenEnv), cfg.BotTokenEnv)
}
chatID := strings.TrimSpace(os.Getenv(cfg.ChatIDEnv))
if chatID == "" {
return nil, merrors.InvalidArgument(fmt.Sprintf("telegram chat id env %s is empty", cfg.ChatIDEnv))
return nil, merrors.InvalidArgument(fmt.Sprintf("telegram chat id env %s is empty", cfg.ChatIDEnv), cfg.ChatIDEnv)
}
var threadID *int64
@@ -64,7 +65,7 @@ func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, er
if raw != "" {
val, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
return nil, merrors.InvalidArgumentWrap(err, fmt.Sprintf("telegram thread id env %s is invalid", env))
return nil, merrors.InvalidArgumentWrap(err, fmt.Sprintf("telegram thread id env %s is invalid", env), env)
}
threadID = &val
}
@@ -79,6 +80,10 @@ func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, er
if apiURL == "" {
apiURL = defaultAPIURL
}
parseMode := strings.TrimSpace(cfg.ParseMode)
if parseMode == "" {
parseMode = "Markdown"
}
return &client{
logger: logger.Named("telegram"),
@@ -89,15 +94,15 @@ func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, er
botToken: token,
chatID: chatID,
threadID: threadID,
parseMode: strings.TrimSpace(cfg.ParseMode),
parseMode: parseMode,
}, nil
}
func (c *client) SendDemoRequest(ctx context.Context, request *model.DemoRequest) error {
if request == nil {
return merrors.InvalidArgument("demo request payload is nil")
return merrors.InvalidArgument("demo request payload is nil", "request")
}
message := buildMessage(request)
message := buildMessage(request, c.parseMode)
payload := sendMessagePayload{
ChatID: c.chatID,
Text: message,
@@ -157,16 +162,89 @@ func (c *client) endpoint() string {
return fmt.Sprintf("%s/bot%s/sendMessage", c.apiURL, c.botToken)
}
func buildMessage(req *model.DemoRequest) string {
func buildMessage(req *model.DemoRequest, parseMode string) string {
var builder strings.Builder
builder.WriteString("New demo request received\n")
builder.WriteString(fmt.Sprintf("Name: %s\n", req.Name))
builder.WriteString(fmt.Sprintf("Organization: %s\n", req.OrganizationName))
builder.WriteString(fmt.Sprintf("Phone: %s\n", req.Phone))
builder.WriteString(fmt.Sprintf("Work email: %s\n", req.WorkEmail))
builder.WriteString(fmt.Sprintf("Payout volume: %s\n", req.PayoutVolume))
if req.Comment != "" {
builder.WriteString(fmt.Sprintf("Comment: %s\n", req.Comment))
builder.WriteString("-----------------------------\n")
formatter := selectValueFormatter(parseMode)
appendMessageField(&builder, "Name", req.Name, formatter)
appendMessageField(&builder, "Organization", req.OrganizationName, formatter)
appendMessageField(&builder, "Phone", req.Phone, formatter)
appendMessageField(&builder, "Work email", req.WorkEmail, formatter)
appendMessageField(&builder, "Payout volume", req.PayoutVolume, formatter)
if strings.TrimSpace(req.Comment) != "" {
appendMessageField(&builder, "Comment", req.Comment, formatter)
}
return builder.String()
}
type valueFormatter func(string) string
func appendMessageField(builder *strings.Builder, label, value string, formatter valueFormatter) {
value = strings.TrimSpace(value)
if value == "" {
value = "—"
} else if formatter != nil {
value = formatter(value)
}
fmt.Fprintf(builder, "• %s: %s\n", label, value)
}
func selectValueFormatter(parseMode string) valueFormatter {
switch strings.ToLower(parseMode) {
case "markdown":
return func(value string) string {
return fmt.Sprintf("*%s*", escapeMarkdown(value))
}
case "markdownv2":
return func(value string) string {
return fmt.Sprintf("*%s*", escapeMarkdownV2(value))
}
case "html":
return func(value string) string {
return fmt.Sprintf("<b>%s</b>", html.EscapeString(value))
}
default:
return nil
}
}
var markdownEscaper = strings.NewReplacer(
"*", "\\*",
"_", "\\_",
"[", "\\[",
"]", "\\]",
"(", "\\(",
")", "\\)",
"`", "\\`",
)
var markdownV2Escaper = strings.NewReplacer(
"_", "\\_",
"*", "\\*",
"[", "\\[",
"]", "\\]",
"(", "\\(",
")", "\\)",
"~", "\\~",
"`", "\\`",
">", "\\>",
"#", "\\#",
"+", "\\+",
"-", "\\-",
"=", "\\=",
"|", "\\|",
"{", "\\{",
"}", "\\}",
".", "\\.",
"!", "\\!",
)
func escapeMarkdown(value string) string {
return markdownEscaper.Replace(value)
}
func escapeMarkdownV2(value string) string {
return markdownV2Escaper.Replace(value)
}