From 632da289542d17da1a488c5f84d588ccb46de618 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Wed, 13 Oct 2021 14:38:31 +0200 Subject: [PATCH] feat: Add notification templates (#2) Signed-off-by: Julien Riou --- README.md | 27 +++++++ config.go | 21 +++-- flexassistant.yaml.example | 7 +- go.mod | 1 - go.sum | 9 --- main.go | 2 +- notification.go | 141 ++++++++++++++++++++++------------ templates/balance.tmpl | 1 + templates/block.tmpl | 6 ++ templates/offline-worker.tmpl | 5 ++ templates/payment.tmpl | 1 + 11 files changed, 154 insertions(+), 67 deletions(-) create mode 100644 templates/balance.tmpl create mode 100644 templates/block.tmpl create mode 100644 templates/offline-worker.tmpl create mode 100644 templates/payment.tmpl diff --git a/README.md b/README.md index fffd450..22fcba8 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,33 @@ Reference: * `token`: token of the Telegram bot * `chat-id` (optional if `channel-name` is present): chat identifier to send Telegram notifications * `channel-name` (optional if `chat-id` is present): channel name to send Telegram notifications +* `notification-templates` (optional): path to [template](https://pkg.go.dev/text/template) files for each notification type + * `balance` (optional): path to template file to format balance notifications + * `payment` (optional): path to template file to format payment notifications + * `block` (optional): path to template file to format block notifications + * `offline-worker` (optional): path to template file to format offline worker notifications + +## Templating + +Notifications can be customized with [templating](https://pkg.go.dev/text/template). + +The following **functions** are available to templates: +* `upper(str string)`: convert string to upper case +* `lower(str string)`: convert string to lower case +* `convertCurrency(coin string, value int64)`: convert the smallest unit of a coin to a human readable unit +* `convertAction(coin string)`: return "Farmed" word for Chia coin or "Mined" for other coins +* `formatBlockURL(coin string, hash string)`: return the URL on the explorer website of the coin of the block identified by its hash +* `formatTransactionURL(coin string, hash string)`: return the URL on the explorer website of the coin of the transaction identified by its hash + +The following **data** is available to templates: +* balance: `.Miner` +* payment: `.Miner`, `.Payment` +* block: `.Pool`, `.Block` +* offline-worker: `.Worker` + +Default templates are available in the [templates](templates) directory. + +Custom template files can be used with the `notification-templates` settings (see _Configuration_ section). ## Usage diff --git a/config.go b/config.go index 0fdf836..6d3e7ab 100644 --- a/config.go +++ b/config.go @@ -8,12 +8,13 @@ import ( // Config to receive settings from the configuration file type Config struct { - DatabaseFile string `yaml:"database-file"` - MaxBlocks int `yaml:"max-blocks"` - MaxPayments int `yaml:"max-payments"` - Pools []PoolConfig `yaml:"pools"` - Miners []MinerConfig `yaml:"miners"` - TelegramConfig TelegramConfig `yaml:"telegram"` + DatabaseFile string `yaml:"database-file"` + MaxBlocks int `yaml:"max-blocks"` + MaxPayments int `yaml:"max-payments"` + Pools []PoolConfig `yaml:"pools"` + Miners []MinerConfig `yaml:"miners"` + TelegramConfig TelegramConfig `yaml:"telegram"` + NotificationTemplates NotificationTemplatesConfig `yaml:"notification-templates"` } // PoolConfig to store Pool configuration @@ -37,6 +38,14 @@ type TelegramConfig struct { ChannelName string `yaml:"channel-name"` } +// NotificationTemplatesConfig to store notifications templates configuration +type NotificationTemplatesConfig struct { + Balance string `yaml:"balance"` + Payment string `yaml:"payment"` + Block string `yaml:"block"` + OfflineWorker string `yaml:"offline-worker"` +} + // NewConfig creates a Config with default values func NewConfig() *Config { return &Config{ diff --git a/flexassistant.yaml.example b/flexassistant.yaml.example index 066f713..a4586d4 100644 --- a/flexassistant.yaml.example +++ b/flexassistant.yaml.example @@ -19,4 +19,9 @@ pools: telegram: chat-id: 000000000 channel-name: MyTelegramChannel - token: 0000000000000000000000000000000000000000000000 \ No newline at end of file + token: 0000000000000000000000000000000000000000000000 +notification-templates: + balance: balance.tmpl + block: block.tmpl + offline-worker; offline-worker.tmpl + payment: payment.tmpl \ No newline at end of file diff --git a/go.mod b/go.mod index 5aba9cd..76d849f 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.16 require ( github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.0.0-rc1 - github.com/leekchan/accounting v1.0.0 // indirect github.com/sirupsen/logrus v1.8.1 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b gorm.io/driver/sqlite v1.1.5 diff --git a/go.sum b/go.sum index e520815..7327a0f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.0.0-rc1 h1:Mr8jIV7wDfLw5Fw6BPupm0aduTFdLjhI3wFuIIZKvO4= @@ -8,17 +6,10 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI= github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/leekchan/accounting v1.0.0 h1:+Wd7dJ//dFPa28rc1hjyy+qzCbXPMR91Fb6F1VGTQHg= -github.com/leekchan/accounting v1.0.0/go.mod h1:3timm6YPhY3YDaGxl0q3eaflX0eoSx3FXn7ckHe4tO0= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU= github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= diff --git a/main.go b/main.go index a43f262..17bf1cb 100644 --- a/main.go +++ b/main.go @@ -84,7 +84,7 @@ func main() { client := NewFlexpoolClient() // Notifications - notifier, err := NewTelegramNotifier(&config.TelegramConfig) + notifier, err := NewTelegramNotifier(&config.TelegramConfig, &config.NotificationTemplates) if err != nil { log.Fatalf("Could not create notifier: %v", err) } diff --git a/notification.go b/notification.go index 9c6925f..25410d5 100644 --- a/notification.go +++ b/notification.go @@ -1,15 +1,30 @@ package main import ( + "bytes" + "embed" "fmt" + "os" + "path" "strings" - - "github.com/leekchan/accounting" + "text/template" telegram "github.com/go-telegram-bot-api/telegram-bot-api/v5" log "github.com/sirupsen/logrus" ) +//go:embed templates +var templateFiles embed.FS + +// Attachment is used to attach objects to templates +type Attachment struct { + Miner Miner + Payment Payment + Pool Pool + Block Block + Worker Worker +} + // Notifier interface to define how to send all kind of notifications type Notifier interface { NotifyBalance(miner Miner, difference float64) error @@ -21,13 +36,14 @@ type Notifier interface { // TelegramNotifier to send notifications using Telegram // Implements the Notifier interface type TelegramNotifier struct { - bot *telegram.BotAPI - chatID int64 - channelName string + bot *telegram.BotAPI + chatID int64 + channelName string + templatesConfig *NotificationTemplatesConfig } // NewTelegramNotifier to create a TelegramNotifier -func NewTelegramNotifier(config *TelegramConfig) (*TelegramNotifier, error) { +func NewTelegramNotifier(config *TelegramConfig, templatesConfig *NotificationTemplatesConfig) (*TelegramNotifier, error) { bot, err := telegram.NewBotAPI(config.Token) if err != nil { return nil, err @@ -35,9 +51,10 @@ func NewTelegramNotifier(config *TelegramConfig) (*TelegramNotifier, error) { log.Debugf("Connected to Telegram as %s", bot.Self.UserName) return &TelegramNotifier{ - bot: bot, - chatID: config.ChatID, - channelName: config.ChannelName, + bot: bot, + chatID: config.ChatID, + channelName: config.ChannelName, + templatesConfig: templatesConfig, }, nil } @@ -60,77 +77,103 @@ func (t *TelegramNotifier) sendMessage(message string) error { return nil } +// formatMessage to create a message with a template file name (either embeded or on disk) +func (t *TelegramNotifier) formatMessage(templateFileName string, attachment interface{}) (message string, err error) { + // Deduce if template file is embeded or is a file on disk + embeded := true + if _, err = os.Stat(templateFileName); os.IsExist(err) { + embeded = false + } + // Reinitialize the error because it was only used for the test + err = nil + + // Create template + templateName := path.Base(templateFileName) + templateFunctions := template.FuncMap{ + "upper": strings.ToUpper, + "lower": strings.ToLower, + "convertCurrency": ConvertCurrency, + "convertAction": ConvertAction, + "formatBlockURL": FormatBlockURL, + "formatTransactionURL": FormatTransactionURL, + } + tmpl := template.New(templateName).Funcs(templateFunctions) + + // Parse template + if embeded { + log.Debugf("Parsing embeded template file %s", templateFileName) + tmpl, err = tmpl.ParseFS(templateFiles, templateFileName) + } else { + log.Debugf("Parsing template file %s", templateFileName) + tmpl, err = tmpl.ParseFiles(templateFileName) + } + if err != nil { + return "", fmt.Errorf("parse failed: %v", err) + } + + // Execute template + var buffer bytes.Buffer + err = tmpl.Execute(&buffer, attachment) + if err != nil { + return "", fmt.Errorf("execute failed: %v", err) + } + + // Extract and return the formatted message + message = buffer.String() + return message, nil +} + // NotifyBalance to format and send a notification when the unpaid balance has changed // Implements the Notifier interface -func (t *TelegramNotifier) NotifyBalance(miner Miner) error { - ac := accounting.Accounting{ - Symbol: strings.ToUpper(miner.Coin), - Precision: 6, - Format: "%v %s", +func (t *TelegramNotifier) NotifyBalance(miner Miner) (err error) { + templateName := "templates/balance.tmpl" + if t.templatesConfig.Balance != "" { + templateName = t.templatesConfig.Balance } - convertedBalance, err := ConvertCurrency(miner.Coin, miner.Balance) + message, err := t.formatMessage(templateName, Attachment{Miner: miner}) if err != nil { return err } - message := fmt.Sprintf("💰 *Balance* _%s_", ac.FormatMoney(convertedBalance)) return t.sendMessage(message) } // NotifyPayment to format and send a notification when a new payment has been detected // Implements the Notifier interface func (t *TelegramNotifier) NotifyPayment(miner Miner, payment Payment) error { - ac := accounting.Accounting{ - Symbol: strings.ToUpper(miner.Coin), - Precision: 6, - Format: "%v %s", + templateName := "templates/payment.tmpl" + if t.templatesConfig.Payment != "" { + templateName = t.templatesConfig.Payment } - convertedValue, err := ConvertCurrency(miner.Coin, payment.Value) + message, err := t.formatMessage(templateName, Attachment{Miner: miner, Payment: payment}) if err != nil { return err } - - message := fmt.Sprintf("💵 *Payment* _%s_", ac.FormatMoney(convertedValue)) return t.sendMessage(message) } // NotifyBlock to format and send a notification when a new block has been detected // Implements the Notifier interface func (t *TelegramNotifier) NotifyBlock(pool Pool, block Block) error { - precision := 6 - if pool.Coin == "xch" { - precision = 2 + templateName := "templates/block.tmpl" + if t.templatesConfig.Block != "" { + templateName = t.templatesConfig.Block } - ac := accounting.Accounting{ - Symbol: strings.ToUpper(pool.Coin), - Precision: precision, - Format: "%v %s", - } - - convertedValue, err := ConvertCurrency(pool.Coin, block.Reward) + message, err := t.formatMessage(templateName, Attachment{Pool: pool, Block: block}) if err != nil { return err } - verb, err := ConvertAction(pool.Coin) - if err != nil { - return err - } - url, err := FormatBlockURL(pool.Coin, block.Hash) - if err != nil { - return err - } - - message := fmt.Sprintf("🎉 *%s* [#%d](%s) _%s_", verb, block.Number, url, ac.FormatMoney(convertedValue)) return t.sendMessage(message) } // NotifyOfflineWorker sends a message when a worker is online or offline func (t *TelegramNotifier) NotifyOfflineWorker(worker Worker) error { - stateIcon := "🟢" - stateMessage := "online" - if !worker.IsOnline { - stateIcon = "🔴" - stateMessage = "offline" + templateName := "templates/offline-worker.tmpl" + if t.templatesConfig.OfflineWorker != "" { + templateName = t.templatesConfig.OfflineWorker + } + message, err := t.formatMessage(templateName, Attachment{Worker: worker}) + if err != nil { + return err } - message := fmt.Sprintf("%s *Worker* _%s_ is %s", stateIcon, worker.Name, stateMessage) return t.sendMessage(message) } diff --git a/templates/balance.tmpl b/templates/balance.tmpl new file mode 100644 index 0000000..0a45707 --- /dev/null +++ b/templates/balance.tmpl @@ -0,0 +1 @@ +💰 *Balance* _{{ printf "%.6f" (convertCurrency .Miner.Coin .Miner.Balance) }} {{ upper .Miner.Coin }}_ \ No newline at end of file diff --git a/templates/block.tmpl b/templates/block.tmpl new file mode 100644 index 0000000..397abc4 --- /dev/null +++ b/templates/block.tmpl @@ -0,0 +1,6 @@ +🎉 *{{ convertAction .Pool.Coin }}* [#{{ .Block.Number }}]({{ formatBlockURL .Pool.Coin .Block.Hash }}) _ +{{- if (eq .Pool.Coin "xch") -}} + {{ printf "%.2f" (convertCurrency .Pool.Coin .Block.Reward) }} + {{- else -}} + {{ printf "%.6f" (convertCurrency .Pool.Coin .Block.Reward) }} + {{- end }} {{ upper .Pool.Coin }}_ \ No newline at end of file diff --git a/templates/offline-worker.tmpl b/templates/offline-worker.tmpl new file mode 100644 index 0000000..bd3edef --- /dev/null +++ b/templates/offline-worker.tmpl @@ -0,0 +1,5 @@ +{{ if .Worker.IsOnline -}} +🟢 *Worker* _{{ .Worker.Name }}_ is online +{{- else -}} +🔴 *Worker* _{{ .Worker.Name }}_ is offline +{{- end -}} \ No newline at end of file diff --git a/templates/payment.tmpl b/templates/payment.tmpl new file mode 100644 index 0000000..38579fb --- /dev/null +++ b/templates/payment.tmpl @@ -0,0 +1 @@ +💵 *Payment* _{{ printf "%.6f" (convertCurrency .Miner.Coin .Payment.Value) }} {{ upper .Miner.Coin }}_ \ No newline at end of file