From 901a1fa35c898c1dddaa0d4d6bed3f55e639547d Mon Sep 17 00:00:00 2001 From: Sergey <83376337+freak12techno@users.noreply.github.com> Date: Tue, 20 Feb 2024 01:10:39 +0300 Subject: [PATCH] feat: refactor reporters configuration (#47) * feat: refactor reporters configuration * feat: validate reporters and chains names uniqueness * chore: pass reporter name to Telegram * chore: rename TelegramReporter -> Reporter * chore: pass type to reporter --- .golangci.yml | 1 + config.example.toml | 17 ++-- pkg/app.go | 18 +++- pkg/config/config.go | 40 +++----- pkg/config/toml_config/chain.go | 27 ++++- pkg/config/toml_config/reporter.go | 103 ++++++++++++++++++++ pkg/config/toml_config/toml_config.go | 27 +++-- pkg/config/types/reporter.go | 14 +++ pkg/constants/constants.go | 8 ++ pkg/reporters/reporter.go | 32 ++++++ pkg/reporters/telegram/get_aliases.go | 2 +- pkg/reporters/telegram/get_config.go | 2 +- pkg/reporters/telegram/help.go | 2 +- pkg/reporters/telegram/list_nodes_status.go | 2 +- pkg/reporters/telegram/set_alias.go | 2 +- pkg/reporters/telegram/telegram.go | 51 ++++++---- pkg/utils/utils.go | 10 ++ 17 files changed, 275 insertions(+), 83 deletions(-) create mode 100644 pkg/config/toml_config/reporter.go create mode 100644 pkg/config/types/reporter.go diff --git a/.golangci.yml b/.golangci.yml index ac1503e..7d165df 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -49,3 +49,4 @@ linters: - deadcode - depguard - interfacebloat + - perfsprint diff --git a/config.example.toml b/config.example.toml index cbb7d41..15f0fff 100644 --- a/config.example.toml +++ b/config.example.toml @@ -12,16 +12,13 @@ level = "info" # solutions like ELK. Defaults to false. json = false -# Telegram reporter configuration. See README.md for more details -[telegram] -# Telegram bot token. -token = "xxx:yyy" -# Chat ID to send reports to. -chat = 12345 -# A list of user IDs that are allowed to contact the bot. The bot won't respond to others -# if this list is not empty. Strongly recommended to not leave it out, as otherwise -# anyone would be able to use your bot. -admins = [67890] +# Reporters configuration. +[[reporters]] +# Reporter type. Currently, the only supported type is "telegram", which is the default. +type = "telegram" +# Telegram config configuration. Required if the type is "telegram". +# See README.md for more details. +telegram-config = { token = "xxx:yyy", chat = 12345, admins = [67890] } # Per-chain configuration. There can be multiple chains. [[chains]] diff --git a/pkg/app.go b/pkg/app.go index 2759479..3618edf 100644 --- a/pkg/app.go +++ b/pkg/app.go @@ -15,7 +15,6 @@ import ( metricsPkg "main/pkg/metrics" nodesManagerPkg "main/pkg/nodes_manager" reportersPkg "main/pkg/reporters" - "main/pkg/reporters/telegram" "github.com/rs/zerolog" ) @@ -41,8 +40,16 @@ func NewApp(config *config.AppConfig, version string) *App { nodesManager := nodesManagerPkg.NewNodesManager(logger, config, metricsManager) - reporters := []reportersPkg.Reporter{ - telegram.NewTelegramReporter(config, logger, nodesManager, aliasManager, version), + reporters := make([]reportersPkg.Reporter, len(config.Reporters)) + for index, reporterConfig := range config.Reporters { + reporters[index] = reportersPkg.GetReporter( + reporterConfig, + config, + logger, + nodesManager, + aliasManager, + version, + ) } dataFetchers := make(map[string]*data_fetcher.DataFetcher, len(config.Chains)) @@ -76,7 +83,10 @@ func (a *App) Start() { reporter.Init() a.MetricsManager.LogReporterEnabled(reporter.Name(), reporter.Enabled()) if reporter.Enabled() { - a.Logger.Info().Str("name", reporter.Name()).Msg("Init reporter") + a.Logger.Info(). + Str("name", reporter.Name()). + Str("type", reporter.Type()). + Msg("Init reporter") } } diff --git a/pkg/config/config.go b/pkg/config/config.go index 8d475a8..0254378 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -28,20 +28,16 @@ func (c Chains) FindByName(name string) *types.Chain { return nil } -type AppConfig struct { - Path string - AliasesPath string - TelegramConfig TelegramConfig - LogConfig LogConfig - Chains Chains - Metrics MetricsConfig - Timezone *time.Location -} +type Reporters []*types.Reporter -type TelegramConfig struct { - Chat int64 - Token string - Admins []int64 +type AppConfig struct { + Path string + AliasesPath string + LogConfig LogConfig + Chains Chains + Reporters Reporters + Metrics MetricsConfig + Timezone *time.Location } type LogConfig struct { @@ -84,11 +80,6 @@ func FromTomlConfig(c *tomlConfig.TomlConfig, path string) *AppConfig { return &AppConfig{ Path: path, AliasesPath: c.AliasesPath, - TelegramConfig: TelegramConfig{ - Chat: c.TelegramConfig.Chat, - Token: c.TelegramConfig.Token, - Admins: c.TelegramConfig.Admins, - }, LogConfig: LogConfig{ LogLevel: c.LogConfig.LogLevel, JSONOutput: c.LogConfig.JSONOutput.Bool, @@ -100,6 +91,9 @@ func FromTomlConfig(c *tomlConfig.TomlConfig, path string) *AppConfig { Chains: utils.Map(c.Chains, func(c *tomlConfig.Chain) *types.Chain { return c.ToAppConfigChain() }), + Reporters: utils.Map(c.Reporters, func(r *tomlConfig.Reporter) *types.Reporter { + return r.ToAppConfigReporter() + }), Timezone: timezone, } } @@ -107,11 +101,6 @@ func FromTomlConfig(c *tomlConfig.TomlConfig, path string) *AppConfig { func (c *AppConfig) ToTomlConfig() *tomlConfig.TomlConfig { return &tomlConfig.TomlConfig{ AliasesPath: c.AliasesPath, - TelegramConfig: tomlConfig.TelegramConfig{ - Chat: c.TelegramConfig.Chat, - Token: c.TelegramConfig.Token, - Admins: c.TelegramConfig.Admins, - }, LogConfig: tomlConfig.LogConfig{ LogLevel: c.LogConfig.LogLevel, JSONOutput: null.BoolFrom(c.LogConfig.JSONOutput), @@ -120,8 +109,9 @@ func (c *AppConfig) ToTomlConfig() *tomlConfig.TomlConfig { ListenAddr: c.Metrics.ListenAddr, Enabled: null.BoolFrom(c.Metrics.Enabled), }, - Chains: utils.Map(c.Chains, tomlConfig.FromAppConfigChain), - Timezone: c.Timezone.String(), + Chains: utils.Map(c.Chains, tomlConfig.FromAppConfigChain), + Reporters: utils.Map(c.Reporters, tomlConfig.FromAppConfigReporter), + Timezone: c.Timezone.String(), } } diff --git a/pkg/config/toml_config/chain.go b/pkg/config/toml_config/chain.go index d88706d..76bc13e 100644 --- a/pkg/config/toml_config/chain.go +++ b/pkg/config/toml_config/chain.go @@ -46,19 +46,19 @@ func (c *Chain) Validate() error { for index, q := range c.Queries { if _, err := query.New(q); err != nil { - return fmt.Errorf("Error in query %d: %s", index, err) + return fmt.Errorf("error in query %d: %s", index, err) } } for index, filter := range c.Filters { if _, err := query.New(filter); err != nil { - return fmt.Errorf("Error in filter %d: %s", index, err) + return fmt.Errorf("error in filter %d: %s", index, err) } } for index, denom := range c.Denoms { if err := denom.Validate(); err != nil { - return fmt.Errorf("Error in denom %d: %s", index, err) + return fmt.Errorf("error in denom %d: %s", index, err) } } @@ -152,3 +152,24 @@ func FromAppConfigChain(c *types.Chain) *Chain { } type Chains []*Chain + +func (chains Chains) Validate() error { + for index, chain := range chains { + if err := chain.Validate(); err != nil { + return fmt.Errorf("error in chain %d: %s", index, err) + } + } + + // checking names uniqueness + names := map[string]bool{} + + for _, chain := range chains { + if _, ok := names[chain.Name]; ok { + return fmt.Errorf("duplicate chain name: %s", chain.Name) + } + + names[chain.Name] = true + } + + return nil +} diff --git a/pkg/config/toml_config/reporter.go b/pkg/config/toml_config/reporter.go new file mode 100644 index 0000000..80b5592 --- /dev/null +++ b/pkg/config/toml_config/reporter.go @@ -0,0 +1,103 @@ +package toml_config + +import ( + "errors" + "fmt" + "main/pkg/config/types" + "main/pkg/constants" + "main/pkg/utils" + "strings" +) + +type TelegramConfig struct { + Chat int64 `toml:"chat"` + Token string `toml:"token"` + Admins []int64 `toml:"admins"` +} + +type Reporter struct { + Name string `toml:"name"` + Type string `default:"telegram" toml:"type"` + + TelegramConfig *TelegramConfig `toml:"telegram-config"` +} + +func (reporter *Reporter) Validate() error { + if reporter.Name == "" { + return errors.New("reporter name not provided") + } + + reporterTypes := constants.GetReporterTypes() + if !utils.Contains(reporterTypes, reporter.Type) { + return fmt.Errorf( + "expected type to be one of %s, but got %s", + strings.Join(reporterTypes, ", "), + reporter.Type, + ) + } + + if reporter.Type == constants.ReporterTypeTelegram && reporter.TelegramConfig == nil { + return errors.New("missing telegram-config for Telegram reporter") + } + + return nil +} + +type Reporters []*Reporter + +func (reporters Reporters) Validate() error { + for index, reporter := range reporters { + if err := reporter.Validate(); err != nil { + return fmt.Errorf("error in reporter %d: %s", index, err) + } + } + + // checking names uniqueness + names := map[string]bool{} + + for _, reporter := range reporters { + if _, ok := names[reporter.Name]; ok { + return fmt.Errorf("duplicate reporter name: %s", reporter.Name) + } + + names[reporter.Name] = true + } + + return nil +} + +func FromAppConfigReporter(reporter *types.Reporter) *Reporter { + var telegramConfig *TelegramConfig + + if reporter.TelegramConfig != nil { + telegramConfig = &TelegramConfig{ + Chat: reporter.TelegramConfig.Chat, + Token: reporter.TelegramConfig.Token, + Admins: reporter.TelegramConfig.Admins, + } + } + + return &Reporter{ + Name: reporter.Name, + Type: reporter.Type, + TelegramConfig: telegramConfig, + } +} + +func (reporter *Reporter) ToAppConfigReporter() *types.Reporter { + var telegramConfig *types.TelegramConfig + + if reporter.TelegramConfig != nil { + telegramConfig = &types.TelegramConfig{ + Chat: reporter.TelegramConfig.Chat, + Token: reporter.TelegramConfig.Token, + Admins: reporter.TelegramConfig.Admins, + } + } + + return &types.Reporter{ + Name: reporter.Name, + Type: reporter.Type, + TelegramConfig: telegramConfig, + } +} diff --git a/pkg/config/toml_config/toml_config.go b/pkg/config/toml_config/toml_config.go index ebafbc6..7dfb80a 100644 --- a/pkg/config/toml_config/toml_config.go +++ b/pkg/config/toml_config/toml_config.go @@ -8,18 +8,13 @@ import ( ) type TomlConfig struct { - AliasesPath string `toml:"aliases"` - TelegramConfig TelegramConfig `toml:"telegram"` - LogConfig LogConfig `toml:"log"` - MetricsConfig MetricsConfig `toml:"metrics"` - Chains Chains `toml:"chains"` - Timezone string `default:"Etc/GMT" toml:"timezone"` -} + AliasesPath string `toml:"aliases"` + LogConfig LogConfig `toml:"log"` + MetricsConfig MetricsConfig `toml:"metrics"` + Chains Chains `toml:"chains"` + Timezone string `default:"Etc/GMT" toml:"timezone"` -type TelegramConfig struct { - Chat int64 `toml:"chat"` - Token string `toml:"token"` - Admins []int64 `toml:"admins"` + Reporters Reporters `toml:"reporters"` } type LogConfig struct { @@ -36,10 +31,12 @@ func (c *TomlConfig) Validate() error { return fmt.Errorf("error parsing timezone: %s", err) } - for index, chain := range c.Chains { - if err := chain.Validate(); err != nil { - return fmt.Errorf("error in chain %d: %s", index, err) - } + if err := c.Chains.Validate(); err != nil { + return fmt.Errorf("error in chains: %s", err) + } + + if err := c.Reporters.Validate(); err != nil { + return fmt.Errorf("error in reporters: %s", err) } return nil diff --git a/pkg/config/types/reporter.go b/pkg/config/types/reporter.go new file mode 100644 index 0000000..f930388 --- /dev/null +++ b/pkg/config/types/reporter.go @@ -0,0 +1,14 @@ +package types + +type TelegramConfig struct { + Chat int64 + Token string + Admins []int64 +} + +type Reporter struct { + Name string + Type string + + TelegramConfig *TelegramConfig +} diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 528f169..6d85dc4 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -2,4 +2,12 @@ package constants const ( PrometheusMetricsPrefix = "cosmos_transactions_bot_" + + ReporterTypeTelegram string = "telegram" ) + +func GetReporterTypes() []string { + return []string{ + ReporterTypeTelegram, + } +} diff --git a/pkg/reporters/reporter.go b/pkg/reporters/reporter.go index 9c59a5d..f271f3c 100644 --- a/pkg/reporters/reporter.go +++ b/pkg/reporters/reporter.go @@ -1,12 +1,44 @@ package reporters import ( + "main/pkg/alias_manager" + "main/pkg/config" + configTypes "main/pkg/config/types" + "main/pkg/constants" + nodesManager "main/pkg/nodes_manager" + "main/pkg/reporters/telegram" "main/pkg/types" + + "github.com/rs/zerolog" ) type Reporter interface { Init() Name() string + Type() string Enabled() bool Send(report types.Report) error } + +func GetReporter( + reporterConfig *configTypes.Reporter, + appConfig *config.AppConfig, + logger *zerolog.Logger, + nodesManager *nodesManager.NodesManager, + aliasManager *alias_manager.AliasManager, + version string, +) Reporter { + if reporterConfig.Type == constants.ReporterTypeTelegram { + return telegram.NewReporter( + reporterConfig, + appConfig, + logger, + nodesManager, + aliasManager, + version, + ) + } + + logger.Fatal().Str("type", reporterConfig.Type).Msg("Unsupported reporter received!") + return nil +} diff --git a/pkg/reporters/telegram/get_aliases.go b/pkg/reporters/telegram/get_aliases.go index eec1f63..1278ae6 100644 --- a/pkg/reporters/telegram/get_aliases.go +++ b/pkg/reporters/telegram/get_aliases.go @@ -6,7 +6,7 @@ import ( tele "gopkg.in/telebot.v3" ) -func (reporter *TelegramReporter) HandleGetAliases(c tele.Context) error { +func (reporter *Reporter) HandleGetAliases(c tele.Context) error { reporter.Logger.Info(). Str("sender", c.Sender().Username). Str("text", c.Text()). diff --git a/pkg/reporters/telegram/get_config.go b/pkg/reporters/telegram/get_config.go index eafa224..b3033c7 100644 --- a/pkg/reporters/telegram/get_config.go +++ b/pkg/reporters/telegram/get_config.go @@ -6,7 +6,7 @@ import ( tele "gopkg.in/telebot.v3" ) -func (reporter *TelegramReporter) HandleGetConfig(c tele.Context) error { +func (reporter *Reporter) HandleGetConfig(c tele.Context) error { reporter.Logger.Info(). Str("sender", c.Sender().Username). Str("text", c.Text()). diff --git a/pkg/reporters/telegram/help.go b/pkg/reporters/telegram/help.go index 9b9bf24..01a9597 100644 --- a/pkg/reporters/telegram/help.go +++ b/pkg/reporters/telegram/help.go @@ -4,7 +4,7 @@ import ( tele "gopkg.in/telebot.v3" ) -func (reporter *TelegramReporter) HandleHelp(c tele.Context) error { +func (reporter *Reporter) HandleHelp(c tele.Context) error { reporter.Logger.Info(). Str("sender", c.Sender().Username). Str("text", c.Text()). diff --git a/pkg/reporters/telegram/list_nodes_status.go b/pkg/reporters/telegram/list_nodes_status.go index 8cd8559..49b9fed 100644 --- a/pkg/reporters/telegram/list_nodes_status.go +++ b/pkg/reporters/telegram/list_nodes_status.go @@ -6,7 +6,7 @@ import ( tele "gopkg.in/telebot.v3" ) -func (reporter *TelegramReporter) HandleListNodesStatus(c tele.Context) error { +func (reporter *Reporter) HandleListNodesStatus(c tele.Context) error { reporter.Logger.Info(). Str("sender", c.Sender().Username). Str("text", c.Text()). diff --git a/pkg/reporters/telegram/set_alias.go b/pkg/reporters/telegram/set_alias.go index c05322c..2d07e1f 100644 --- a/pkg/reporters/telegram/set_alias.go +++ b/pkg/reporters/telegram/set_alias.go @@ -7,7 +7,7 @@ import ( tele "gopkg.in/telebot.v3" ) -func (reporter *TelegramReporter) HandleSetAlias(c tele.Context) error { +func (reporter *Reporter) HandleSetAlias(c tele.Context) error { reporter.Logger.Info(). Str("sender", c.Sender().Username). Str("text", c.Text()). diff --git a/pkg/reporters/telegram/telegram.go b/pkg/reporters/telegram/telegram.go index 2f24d9d..a908c5c 100644 --- a/pkg/reporters/telegram/telegram.go +++ b/pkg/reporters/telegram/telegram.go @@ -5,6 +5,7 @@ import ( "fmt" "html" "html/template" + "main/pkg/constants" "main/pkg/types" "main/pkg/types/amount" "time" @@ -23,7 +24,9 @@ import ( tele "gopkg.in/telebot.v3" ) -type TelegramReporter struct { +type Reporter struct { + ReporterName string + Token string Chat int64 Admins []int64 @@ -41,17 +44,19 @@ const ( MaxMessageSize = 4096 ) -func NewTelegramReporter( +func NewReporter( + reporterConfig *configTypes.Reporter, config *config.AppConfig, logger *zerolog.Logger, nodesManager *nodesManager.NodesManager, aliasManager *alias_manager.AliasManager, version string, -) *TelegramReporter { - return &TelegramReporter{ - Token: config.TelegramConfig.Token, - Chat: config.TelegramConfig.Chat, - Admins: config.TelegramConfig.Admins, +) *Reporter { + return &Reporter{ + ReporterName: reporterConfig.Name, + Token: reporterConfig.TelegramConfig.Token, + Chat: reporterConfig.TelegramConfig.Chat, + Admins: reporterConfig.TelegramConfig.Admins, Config: config, Logger: logger.With().Str("component", "telegram_reporter").Logger(), Templates: make(map[string]*template.Template, 0), @@ -61,7 +66,7 @@ func NewTelegramReporter( } } -func (reporter *TelegramReporter) Init() { +func (reporter *Reporter) Init() { if reporter.Token == "" || reporter.Chat == 0 { reporter.Logger.Debug().Msg("Telegram credentials not set, not creating Telegram reporter.") return @@ -92,11 +97,11 @@ func (reporter *TelegramReporter) Init() { go reporter.TelegramBot.Start() } -func (reporter TelegramReporter) Enabled() bool { +func (reporter *Reporter) Enabled() bool { return reporter.Token != "" && reporter.Chat != 0 } -func (reporter TelegramReporter) GetTemplate(name string) (*template.Template, error) { +func (reporter *Reporter) GetTemplate(name string) (*template.Template, error) { if cachedTemplate, ok := reporter.Templates[name]; ok { reporter.Logger.Trace().Str("type", name).Msg("Using cached template") return cachedTemplate, nil @@ -121,7 +126,7 @@ func (reporter TelegramReporter) GetTemplate(name string) (*template.Template, e return t, nil } -func (reporter *TelegramReporter) Render(templateName string, data interface{}) (string, error) { +func (reporter *Reporter) Render(templateName string, data interface{}) (string, error) { template, err := reporter.GetTemplate(templateName) if err != nil { reporter.Logger.Error().Err(err).Str("type", templateName).Msg("Error loading template") @@ -138,12 +143,12 @@ func (reporter *TelegramReporter) Render(templateName string, data interface{}) return buffer.String(), err } -func (reporter *TelegramReporter) SerializeReport(r types.Report) (string, error) { +func (reporter *Reporter) SerializeReport(r types.Report) (string, error) { reportableType := r.Reportable.Type() return reporter.Render(reportableType, r) } -func (reporter *TelegramReporter) SerializeMessage(msg types.Message) template.HTML { +func (reporter *Reporter) SerializeMessage(msg types.Message) template.HTML { msgType := msg.Type() reporterTemplate, err := reporter.GetTemplate(msgType) @@ -162,7 +167,7 @@ func (reporter *TelegramReporter) SerializeMessage(msg types.Message) template.H return template.HTML(buffer.String()) } -func (reporter TelegramReporter) Send(report types.Report) error { +func (reporter *Reporter) Send(report types.Report) error { reportString, err := reporter.SerializeReport(report) if err != nil { reporter.Logger.Error(). @@ -186,11 +191,15 @@ func (reporter TelegramReporter) Send(report types.Report) error { return nil } -func (reporter TelegramReporter) Name() string { - return "telegram-reporter" +func (reporter *Reporter) Name() string { + return reporter.ReporterName +} + +func (reporter *Reporter) Type() string { + return constants.ReporterTypeTelegram } -func (reporter *TelegramReporter) BotSend(msg string) error { +func (reporter *Reporter) BotSend(msg string) error { messages := utils.SplitStringIntoChunks(msg, MaxMessageSize) for _, message := range messages { @@ -209,7 +218,7 @@ func (reporter *TelegramReporter) BotSend(msg string) error { return nil } -func (reporter *TelegramReporter) BotReply(c tele.Context, msg string) error { +func (reporter *Reporter) BotReply(c tele.Context, msg string) error { messages := utils.SplitStringIntoChunks(msg, MaxMessageSize) for _, message := range messages { @@ -221,7 +230,7 @@ func (reporter *TelegramReporter) BotReply(c tele.Context, msg string) error { return nil } -func (reporter *TelegramReporter) SerializeLink(link configTypes.Link) template.HTML { +func (reporter *Reporter) SerializeLink(link configTypes.Link) template.HTML { value := link.Title if value == "" { value = link.Value @@ -234,7 +243,7 @@ func (reporter *TelegramReporter) SerializeLink(link configTypes.Link) template. return template.HTML(value) } -func (reporter *TelegramReporter) SerializeAmount(amount amount.Amount) template.HTML { +func (reporter *Reporter) SerializeAmount(amount amount.Amount) template.HTML { if amount.PriceUSD == nil { return template.HTML(fmt.Sprintf( "%s %s", @@ -251,6 +260,6 @@ func (reporter *TelegramReporter) SerializeAmount(amount amount.Amount) template )) } -func (reporter *TelegramReporter) SerializeDate(date time.Time) template.HTML { +func (reporter *Reporter) SerializeDate(date time.Time) template.HTML { return template.HTML(date.In(reporter.Config.Timezone).Format(time.RFC822)) } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 04591c7..e3962dd 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -14,6 +14,16 @@ func Map[T any, V any](source []T, mapper func(T) V) []V { return destination } +func Contains[T comparable](slice []T, value T) bool { + for _, elt := range slice { + if elt == value { + return true + } + } + + return false +} + func RemoveFirstSlash(str string) string { if len(str) == 0 { return str