diff --git a/README.md b/README.md index c6e62cc..667c93a 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ another file provided by the `-config` argument. As a good start, you can copy the configuration file example: ``` -cp -p flexassistant.yaml.example flexassistant.yaml +cp -p flexassistant.example.yaml flexassistant.yaml ``` Then edit this file at will. @@ -71,11 +71,13 @@ Reference: * `max-blocks` (optional): maximum number of blocks to retreive from the API * `max-payments` (optional): maximum number of payments to retreive from the API * `pools` (optional): list of pools - * `coin`: coin of the pool (ex: `eth`, `xch`) + * `coin`: coin of the pool (ex: `etc`, `eth`, `xch`) * `enable-blocks` (optional): enable block notifications for this pool (disabled by default) - * `min-block-reward` (optional): send notifications when block reward has reached this minimum threshold in crypto currency unit (ETH, XCH, etc) + * `min-block-reward` (optional): send notifications when block reward has reached this minimum threshold in crypto + currency unit (ETH, XCH, etc) * `miners` (optional): list of miners and/or farmers * `address`: address of the miner or the farmer registered on the API + * `coin` (optional): coin of the miner (ex: `etc`, `eth`, `xch`) (deduced by default, can be wrong for `etc` coin) * `enable-balance` (optional): enable balance notifications (disabled by default) * `enable-payments` (optional): enable payments notifications (disabled by default) * `enable-offline-workers` (optional): enable offline/online notifications for associated workers (disabled by @@ -84,12 +86,19 @@ 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 +* `notifications` (optional): Notifications configurations + * `balance` (optional): balance notifications settings + * `template` (optional): path to [template](https://pkg.go.dev/text/template) file + * `test` (optional): send a test notification + * `payment` (optional): payment notifications settings + * `template` (optional): path to [template](https://pkg.go.dev/text/template) file + * `test` (optional): send a test notification + * `block` (optional): block notification settings + * `template` (optional): path to [template](https://pkg.go.dev/text/template) file + * `test` (optional): send a test notification + * `offline-worker` (optional): offline workers notification settings + * `template` (optional): path to [template](https://pkg.go.dev/text/template) file + * `test` (optional): send a test notification ## Templating @@ -112,7 +121,7 @@ The following **data** is available to templates: Default templates are available in the [templates](templates) directory. -Custom template files can be used with the `notification-templates` settings (see _Configuration_ section). +Custom template files can be used with the `template` settings (see _Configuration_ section). ## Usage diff --git a/client.go b/client.go index b2f961d..7d920c4 100644 --- a/client.go +++ b/client.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "math/rand" "net/http" "sort" "time" @@ -128,6 +129,16 @@ func (f *FlexpoolClient) MinerPayments(coin string, address string, limit int) ( } } +// LastMinerPayment return the last payment of a miner +func (f *FlexpoolClient) LastMinerPayment(miner *Miner) (*Payment, error) { + log.Debugf("Fetching last payment of %s", miner) + payments, err := f.MinerPayments(miner.Coin, miner.Address, 1) + if err != nil { + return nil, err + } + return payments[0], nil +} + // WorkersResponse represents the JSON structure of the Flexpool API response for workers type WorkersResponse struct { Error string `json:"error"` @@ -205,3 +216,80 @@ func (f *FlexpoolClient) PoolBlocks(coin string, limit int) (blocks []*Block, er } } } + +// LastPoolBlock return the last discovered block for a given pool +func (f *FlexpoolClient) LastPoolBlock(pool *Pool) (*Block, error) { + blocks, err := f.PoolBlocks(pool.Coin, 1) + if err != nil { + return nil, err + } + return blocks[0], nil +} + +// CoinsResponse represents the JSON structure of the Flexpool API response for pool coins +type CoinsResponse struct { + Error string `json:"error"` + Result struct { + Coins []struct { + Ticker string `json:"ticker"` + Name string `json:"name"` + } `json:"coins"` + } `json:"result"` +} + +// RandomPool returns a random pool from the API +func (f *FlexpoolClient) RandomPool() (*Pool, error) { + log.Debug("Fetching a random pool") + body, err := f.request(fmt.Sprintf("%s/pool/coins", FlexpoolAPIURL)) + if err != nil { + return nil, err + } + var response CoinsResponse + json.Unmarshal(body, &response) + randomIndex := rand.Intn(len(response.Result.Coins)) + if err != nil { + return nil, err + } + randomCoin := response.Result.Coins[randomIndex] + return NewPool(randomCoin.Ticker), nil +} + +// TopMinersResponse represents the JSON structure of the Flexpool API response for pool top miners +type TopMinersResponse struct { + Error string `json:"error"` + Result []struct { + Address string `json:"address"` + } `json:"result"` +} + +// RandomMiner returns a random miner from the API +func (f *FlexpoolClient) RandomMiner(pool *Pool) (*Miner, error) { + log.Debug("Fetching a random miner") + body, err := f.request(fmt.Sprintf("%s/pool/topMiners?coin=%s", FlexpoolAPIURL, pool.Coin)) + if err != nil { + return nil, err + } + var response TopMinersResponse + json.Unmarshal(body, &response) + randomResult := response.Result[rand.Intn(len(response.Result))] + randomMiner, err := NewMiner(randomResult.Address, pool.Coin) + if err != nil { + return nil, err + } + randomBalance, err := f.MinerBalance(pool.Coin, randomMiner.Address) + if err != nil { + return nil, err + } + randomMiner.Balance = randomBalance + return randomMiner, nil +} + +// RandomWorker returns a random worker from the API +func (f *FlexpoolClient) RandomWorker(miner *Miner) (*Worker, error) { + log.Debug("Fetching a random worker") + workers, err := f.MinerWorkers(miner.Coin, miner.Address) + if err != nil { + return nil, err + } + return workers[rand.Intn(len(workers))], nil +} diff --git a/config.go b/config.go index 692e1f6..39afb32 100644 --- a/config.go +++ b/config.go @@ -8,13 +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"` - NotificationTemplates NotificationTemplatesConfig `yaml:"notification-templates"` + 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"` + Notifications NotificationsConfig `yaml:"notifications"` } // PoolConfig to store Pool configuration @@ -27,6 +27,7 @@ type PoolConfig struct { // MinerConfig to store Miner configuration type MinerConfig struct { Address string `yaml:"address"` + Coin string `yaml:"coin"` EnableBalance bool `yaml:"enable-balance"` EnablePayments bool `yaml:"enable-payments"` EnableOfflineWorkers bool `yaml:"enable-offline-workers"` @@ -39,12 +40,18 @@ 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"` +// NotificationTemplatesConfig to store all notifications configurations +type NotificationsConfig struct { + Balance NotificationConfig `yaml:"balance"` + Payment NotificationConfig `yaml:"payment"` + Block NotificationConfig `yaml:"block"` + OfflineWorker NotificationConfig `yaml:"offline-worker"` +} + +// NotificationConfig to store a single notification configuration +type NotificationConfig struct { + Template string `yaml:"template"` + Test bool `yaml:"test"` } // NewConfig creates a Config with default values diff --git a/flexassistant.yaml.example b/flexassistant.example.yaml similarity index 69% rename from flexassistant.yaml.example rename to flexassistant.example.yaml index ea16ae6..9f8a3fe 100644 --- a/flexassistant.yaml.example +++ b/flexassistant.example.yaml @@ -4,10 +4,12 @@ max-blocks: 10 max-payments: 5 miners: - address: 0x0000000000000000000000000000000000000000 + coin: eth enable-balance: true enable-payments: true enable-offline-workers: true - address: xch00000000000000000000000000000000000000000000000000000000000 + coin: xch enable-balance: true enable-payments: true enable-offline-workers: true @@ -22,8 +24,16 @@ telegram: chat-id: 000000000 channel-name: MyTelegramChannel token: 0000000000000000000000000000000000000000000000 -notification-templates: - balance: balance.tmpl - block: block.tmpl - offline-worker; offline-worker.tmpl - payment: payment.tmpl \ No newline at end of file +#notifications: +# balance: +# template: balance.tmpl +# test: true +# block: +# template: block.tmpl +# test: true +# offline-worker: +# template: offline-worker.tmpl +# test: true +# payment: +# template: payment.tmpl +# test: true \ No newline at end of file diff --git a/main.go b/main.go index 5cbe58f..f98ce5f 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,9 @@ package main import ( "flag" "fmt" + "math/rand" "os" + "time" log "github.com/sirupsen/logrus" "gorm.io/gorm" @@ -30,6 +32,7 @@ const MaxBlocks = 50 // initialize logging func init() { log.SetOutput(os.Stdout) + rand.Seed(time.Now().UnixNano()) } func main() { @@ -84,11 +87,20 @@ func main() { client := NewFlexpoolClient() // Notifications - notifier, err := NewTelegramNotifier(&config.TelegramConfig, &config.NotificationTemplates) + notifier, err := NewTelegramNotifier(&config.TelegramConfig, &config.Notifications) if err != nil { log.Fatalf("Could not create notifier: %v", err) } + executed, err := notifier.NotifyTest(*client) + if err != nil { + log.Fatalf("Could not send test notifications: %v", err) + } + if executed { + log.Debug("Exit after sending test notifications") + return + } + // Limits var maxPayments int if config.MaxPayments > 0 { @@ -106,7 +118,7 @@ func main() { // Handle miners for _, configuredMiner := range config.Miners { - miner, err := NewMiner(configuredMiner.Address) + miner, err := NewMiner(configuredMiner.Address, configuredMiner.Coin) if err != nil { log.Warnf("Could not parse miner: %v", err) continue diff --git a/miner.go b/miner.go index 65466d6..cfbb08b 100644 --- a/miner.go +++ b/miner.go @@ -24,13 +24,15 @@ type Miner struct { } // NewMiner creates a Miner -func NewMiner(address string) (*Miner, error) { - miner := &Miner{Address: address} - coin, err := miner.ParseCoin() - if err != nil { - return nil, err +func NewMiner(address string, coin string) (*Miner, error) { + miner := &Miner{Address: address, Coin: coin} + if coin == "" { + coin, err := miner.ParseCoin() + if err != nil { + return nil, err + } + miner.Coin = coin } - miner.Coin = coin return miner, nil } diff --git a/notification.go b/notification.go index 6cf6a17..ffca65d 100644 --- a/notification.go +++ b/notification.go @@ -32,19 +32,20 @@ type Notifier interface { NotifyPayment(miner Miner, payment Payment) error NotifyBlock(pool Pool, block Block) error NotifyOfflineWorker(worker Worker) error + NotifyTest(client FlexpoolClient) error } // TelegramNotifier to send notifications using Telegram // Implements the Notifier interface type TelegramNotifier struct { - bot *telegram.BotAPI - chatID int64 - channelName string - templatesConfig *NotificationTemplatesConfig + bot *telegram.BotAPI + chatID int64 + channelName string + configurations *NotificationsConfig } // NewTelegramNotifier to create a TelegramNotifier -func NewTelegramNotifier(config *TelegramConfig, templatesConfig *NotificationTemplatesConfig) (*TelegramNotifier, error) { +func NewTelegramNotifier(config *TelegramConfig, configurations *NotificationsConfig) (*TelegramNotifier, error) { bot, err := telegram.NewBotAPI(config.Token) if err != nil { return nil, err @@ -52,10 +53,10 @@ func NewTelegramNotifier(config *TelegramConfig, templatesConfig *NotificationTe log.Debugf("Connected to Telegram as %s", bot.Self.UserName) return &TelegramNotifier{ - bot: bot, - chatID: config.ChatID, - channelName: config.ChannelName, - templatesConfig: templatesConfig, + bot: bot, + chatID: config.ChatID, + channelName: config.ChannelName, + configurations: configurations, }, nil } @@ -125,8 +126,8 @@ func fileExists(filename string) bool { // Implements the Notifier interface func (t *TelegramNotifier) NotifyBalance(miner Miner) (err error) { templateName := "templates/balance.tmpl" - if t.templatesConfig.Balance != "" { - templateName = t.templatesConfig.Balance + if t.configurations.Balance.Template != "" { + templateName = t.configurations.Balance.Template } message, err := t.formatMessage(templateName, Attachment{Miner: miner}) if err != nil { @@ -135,12 +136,26 @@ func (t *TelegramNotifier) NotifyBalance(miner Miner) (err error) { return t.sendMessage(message) } +// testNotifyBalance sends a fake balance notification +func (t *TelegramNotifier) testNotifyBalance(client FlexpoolClient) error { + log.Debug("Testing balance notification") + randomPool, err := client.RandomPool() + if err != nil { + return err + } + randomMiner, err := client.RandomMiner(randomPool) + if err != nil { + return err + } + return t.NotifyBalance(*randomMiner) +} + // 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 { templateName := "templates/payment.tmpl" - if t.templatesConfig.Payment != "" { - templateName = t.templatesConfig.Payment + if t.configurations.Payment.Template != "" { + templateName = t.configurations.Payment.Template } message, err := t.formatMessage(templateName, Attachment{Miner: miner, Payment: payment}) if err != nil { @@ -149,12 +164,30 @@ func (t *TelegramNotifier) NotifyPayment(miner Miner, payment Payment) error { return t.sendMessage(message) } +// testNotifyPayment sends a fake payment notification +func (t *TelegramNotifier) testNotifyPayment(client FlexpoolClient) error { + log.Debug("Testing payment notification") + randomPool, err := client.RandomPool() + if err != nil { + return err + } + randomMiner, err := client.RandomMiner(randomPool) + if err != nil { + return err + } + randomPayment, err := client.LastMinerPayment(randomMiner) + if err != nil { + return err + } + return t.NotifyPayment(*randomMiner, *randomPayment) +} + // 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 { templateName := "templates/block.tmpl" - if t.templatesConfig.Block != "" { - templateName = t.templatesConfig.Block + if t.configurations.Block.Template != "" { + templateName = t.configurations.Block.Template } message, err := t.formatMessage(templateName, Attachment{Pool: pool, Block: block}) if err != nil { @@ -163,11 +196,25 @@ func (t *TelegramNotifier) NotifyBlock(pool Pool, block Block) error { return t.sendMessage(message) } +// testNotifyBlock sends a random block notification +func (t *TelegramNotifier) testNotifyBlock(client FlexpoolClient) error { + log.Debug("Testing block notification") + randomPool, err := client.RandomPool() + if err != nil { + return err + } + randomBlock, err := client.LastPoolBlock(randomPool) + if err != nil { + return err + } + return t.NotifyBlock(*randomPool, *randomBlock) +} + // NotifyOfflineWorker sends a message when a worker is online or offline func (t *TelegramNotifier) NotifyOfflineWorker(worker Worker) error { templateName := "templates/offline-worker.tmpl" - if t.templatesConfig.OfflineWorker != "" { - templateName = t.templatesConfig.OfflineWorker + if t.configurations.OfflineWorker.Template != "" { + templateName = t.configurations.OfflineWorker.Template } message, err := t.formatMessage(templateName, Attachment{Worker: worker}) if err != nil { @@ -175,3 +222,58 @@ func (t *TelegramNotifier) NotifyOfflineWorker(worker Worker) error { } return t.sendMessage(message) } + +// testNotifyOfflineWorker sends a fake worker offline notification +func (t *TelegramNotifier) testNotifyOfflineWorker(client FlexpoolClient) error { + log.Debug("Testing offline worker notification") + randomBlock, err := client.RandomPool() + if err != nil { + return err + } + randomMiner, err := client.RandomMiner(randomBlock) + if err != nil { + return err + } + randomWorker, err := client.RandomWorker(randomMiner) + if err != nil { + return err + } + log.Debugf("%s", randomWorker) + return t.NotifyOfflineWorker(*randomWorker) +} + +// NotifyTest sends fake notifications +func (t *TelegramNotifier) NotifyTest(client FlexpoolClient) (executed bool, err error) { + if t.configurations.Balance.Test { + if err = t.testNotifyBalance(client); err != nil { + return false, err + } else { + executed = true + } + } + + if t.configurations.Payment.Test { + if err = t.testNotifyPayment(client); err != nil { + return false, err + } else { + executed = true + } + } + + if t.configurations.Block.Test { + if err = t.testNotifyBlock(client); err != nil { + return false, err + } else { + executed = true + } + } + + if t.configurations.OfflineWorker.Test { + if err = t.testNotifyOfflineWorker(client); err != nil { + return false, err + } else { + executed = true + } + } + return executed, nil +} diff --git a/utils.go b/utils.go index 88c29dd..97895e2 100644 --- a/utils.go +++ b/utils.go @@ -14,6 +14,8 @@ const MojoToXCHDivider = 1000000000000 // Example: for "eth", convert from Weis to ETH func ConvertCurrency(coin string, value float64) (float64, error) { switch coin { + case "etc": + return ConvertWeis(value), nil case "eth": return ConvertWeis(value), nil case "xch": @@ -36,6 +38,8 @@ func ConvertMojo(value float64) float64 { // FormatBlockURL returns the URL on the respective blockchain explorer given the coin and the block hash func FormatBlockURL(coin string, hash string) (string, error) { switch coin { + case "etc": + return fmt.Sprintf("https://etcblockexplorer.com/block/%s", hash), nil case "eth": return fmt.Sprintf("https://etherscan.io/block/%s", hash), nil case "xch": @@ -47,6 +51,8 @@ func FormatBlockURL(coin string, hash string) (string, error) { // FormatTransactionURL returns the URL on the respective blockchain explorer given the coin and the transaction hash func FormatTransactionURL(coin string, hash string) (string, error) { switch coin { + case "etc": + return fmt.Sprintf("https://etcblockexplorer.com/address/%s", hash), nil case "eth": return fmt.Sprintf("https://etherscan.io/tx/%s", hash), nil case "xch":