diff --git a/configs/config.example.yml b/configs/config.example.yml index 007116a..da9c063 100644 --- a/configs/config.example.yml +++ b/configs/config.example.yml @@ -2,6 +2,9 @@ telegram: token: # токен бота chatId: -1234567890123 # ИД чата для уведомлений webhookUrl: # TODO: URL для получения веб-хуков, если не задан, то используется long-poll + messages: + online: "✅ {{.Name}} is *online*" + offline: "❌ {{.Name}} is *offline*: {{.Error}}" services: # список сервисов для мониторинга - name: Google # наименование для уведомления initialDelaySeconds: 5 # пауза перед первым опросом в секундах, по умолчанию: 0; если меньше 0, то используется случайное значение между 0 и `periodSeconds` diff --git a/internal/bot/app.go b/internal/bot/app.go index b67c960..d92fa5e 100644 --- a/internal/bot/app.go +++ b/internal/bot/app.go @@ -20,6 +20,7 @@ func Run() error { cfg := config.GetConfig() ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt) + messages := NewMessages(cfg.Telegram.Messages) tg := infrastructure.NewTelegramBot(cfg.Telegram) tgapi, err := tg.Api() if err != nil { @@ -40,9 +41,25 @@ func Run() error { msg := tgbotapi.NewMessage(cfg.Telegram.ChatID, "") msg.ParseMode = tgbotapi.ModeMarkdownV2 if v.State == monitor.ServiceOffline { - msg.Text = "❌ " + tgbotapi.EscapeText(msg.ParseMode, v.Name) + " is *offline*: " + tgbotapi.EscapeText(msg.ParseMode, v.Error.Error()) + // msg.Text = "❌ " + tgbotapi.EscapeText(msg.ParseMode, v.Name) + " is *offline*: " + tgbotapi.EscapeText(msg.ParseMode, v.Error.Error()) + context := OfflineContext{ + OnlineContext: OnlineContext{ + Name: tgbotapi.EscapeText(msg.ParseMode, v.Name), + }, + Error: tgbotapi.EscapeText(msg.ParseMode, v.Error.Error()), + } + msg.Text, err = messages.Render(TemplateOffline, context) } else { - msg.Text = "✅ " + tgbotapi.EscapeText(msg.ParseMode, v.Name) + " is *online*" + // msg.Text = "✅ " + tgbotapi.EscapeText(msg.ParseMode, v.Name) + " is *online*" + context := OnlineContext{ + Name: tgbotapi.EscapeText(msg.ParseMode, v.Name), + } + msg.Text, err = messages.Render(TemplateOnline, context) + } + + if err != nil { + errorLog.Println(err) + continue } if _, err := tgapi.Send(msg); err != nil { diff --git a/internal/bot/messages.go b/internal/bot/messages.go new file mode 100644 index 0000000..f021902 --- /dev/null +++ b/internal/bot/messages.go @@ -0,0 +1,73 @@ +package bot + +import ( + "fmt" + "strings" + "sync" + "text/template" + + "github.com/capcom6/service-monitor-tgbot/internal/config" +) + +type TemplateName string + +const ( + TemplateOnline TemplateName = "online" + TemplateOffline TemplateName = "offline" +) + +type Messages struct { + Templates config.TelegramMessages + cachedTemplates map[TemplateName]*template.Template + mux sync.Mutex +} + +type OnlineContext struct { + Name string +} + +type OfflineContext struct { + OnlineContext + Error string +} + +func NewMessages(templates config.TelegramMessages) *Messages { + return &Messages{ + Templates: templates, + cachedTemplates: make(map[TemplateName]*template.Template), + } +} + +func (m *Messages) prepare(name TemplateName) (prepared *template.Template, err error) { + m.mux.Lock() + defer m.mux.Unlock() + + if prepared = m.cachedTemplates[name]; prepared != nil { + return prepared, nil + } + + var tmplString string + var ok bool + if tmplString, ok = m.Templates[string(name)]; !ok { + return nil, fmt.Errorf("template %s not found", name) + } + + if m.cachedTemplates[name], err = template.New(string(name)).Parse(tmplString); err != nil { + return nil, fmt.Errorf("can't parse template %s: %w", name, err) + } + + return m.cachedTemplates[name], nil +} + +func (m *Messages) Render(name TemplateName, context any) (string, error) { + tmpl, err := m.prepare(name) + if err != nil { + return "", err + } + + builder := strings.Builder{} + if err := tmpl.Execute(&builder, context); err != nil { + return "", err + } + return builder.String(), nil +} diff --git a/internal/config/config.go b/internal/config/config.go index 177b0f5..f36e871 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -20,11 +20,13 @@ type Config struct { Storage Storage `yaml:"storage"` } type Telegram struct { - Token string `yaml:"token" envconfig:"TELEGRAM__TOKEN" validate:"required"` - ChatID int64 `yaml:"chatId" envconfig:"TELEGRAM__CHAT_ID"` - WebhookURL string `yaml:"webhookUrl" envconfig:"TELEGRAM__WEBHOOK_URL" validate:"required"` - Debug bool `yaml:"debug" envconfig:"TELEGRAM__DEBUG"` + Token string `yaml:"token" envconfig:"TELEGRAM__TOKEN" validate:"required"` + ChatID int64 `yaml:"chatId" envconfig:"TELEGRAM__CHAT_ID"` + WebhookURL string `yaml:"webhookUrl" envconfig:"TELEGRAM__WEBHOOK_URL" validate:"required"` + Debug bool `yaml:"debug" envconfig:"TELEGRAM__DEBUG"` + Messages TelegramMessages `yaml:"messages"` } +type TelegramMessages map[string]string type HTTPHeader struct { Name string `yaml:"name"` Value string `yaml:"value"` @@ -170,7 +172,7 @@ func loadConfig() Config { path = "config.yml" } - config := Config{} + config := defaultConfig if err := fromYaml(path, &config); err != nil { errorLog.Printf("couldn'n load config from %s: %s\r\n", path, err.Error()) @@ -205,5 +207,7 @@ func GetConfig() Config { instance = loadConfig() }) + log.Printf("config: %+v", instance) + return instance } diff --git a/internal/config/defaults.go b/internal/config/defaults.go new file mode 100644 index 0000000..18c3610 --- /dev/null +++ b/internal/config/defaults.go @@ -0,0 +1,20 @@ +package config + +var ( + defaultTelegramMessages TelegramMessages = TelegramMessages{ + "online": "✅ {{.Name}} is *online*", + "offline": "❌ {{.Name}} is *offline*: {{.Error}}", + } + + defaultConfig Config = Config{ + Telegram: Telegram{ + Token: "", + ChatID: 0, + WebhookURL: "", + Debug: false, + Messages: defaultTelegramMessages, + }, + Services: []Service{}, + Storage: Storage{}, + } +)