diff --git a/README.md b/README.md index 8745028..ec7e75c 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,11 @@ Strict mode can be enabled for individual data sources by setting the `strict` f For instance, strict mode can be used to automatically remove resolved GitHub issues from the board. Every time the source is queried, it will return an up-to-date set of open issues. If the board contains any cards that doesn't exist in that set, they will be automatically deleted. +#### Telegram +Since it's not very practical to manually check a log file for a cron job, entrello has an optional Telegram integration that you can use if you want to receive messages on card updates & possible errors. + +You need a Telegram token & a chat ID in order to enable the integration. + #### Custom Periods You can define a custom query period for each source, by populating the `type` and `interval` fields under the `period` for a source. diff --git a/cmd/entrello/main.go b/cmd/entrello/main.go index d4d2182..4686411 100644 --- a/cmd/entrello/main.go +++ b/cmd/entrello/main.go @@ -6,17 +6,24 @@ import ( "time" "github.com/utkuufuk/entrello/internal/config" + "github.com/utkuufuk/entrello/internal/syslog" "github.com/utkuufuk/entrello/internal/trello" ) +var ( + logger syslog.Logger +) + func main() { // read config params cfg, err := config.ReadConfig("config.yml") if err != nil { - // @todo: send telegram notification instead if enabled - log.Fatalf("[-] could not read config variables: %v", err) + log.Fatalf("Could not read config variables: %v", err) } + // get a system logger instance + logger = syslog.NewLogger(cfg.Telegram) + // set global timeout timeout := time.Second * time.Duration(cfg.TimeoutSeconds) ctx, cancel := context.WithTimeout(context.Background(), timeout) @@ -31,14 +38,12 @@ func main() { // initialize the Trello client client, err := trello.NewClient(cfg.Trello) if err != nil { - // @todo: send telegram notification instead if enabled - log.Fatalf("[-] could not create trello client: %v", err) + logger.Fatalf("could not create trello client: %v", err) } // within the Trello client, load the existing cards (only with relevant labels) if err := client.LoadCards(labels); err != nil { - // @todo: send telegram notification instead if enabled - log.Fatalf("[-] could not load existing cards from the board: %v", err) + logger.Fatalf("could not load existing cards from the board: %v", err) } // concurrently fetch new cards from sources and start processing cards to be created & deleted diff --git a/cmd/entrello/queue.go b/cmd/entrello/queue.go index e3a955a..9f7d473 100644 --- a/cmd/entrello/queue.go +++ b/cmd/entrello/queue.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "log" "github.com/utkuufuk/entrello/internal/trello" ) @@ -44,22 +43,19 @@ func processActionables(ctx context.Context, client trello.Client, q CardQueue) for { select { case c := <-q.add: - // @todo: send telegram notification instead if enabled if err := client.CreateCard(c); err != nil { - log.Printf("[-] error occurred while creating card: %v", err) + logger.Errorf("could not create Trello card: %v", err) break } - log.Printf("[+] created new card: %s", c.Name) + logger.Printf("created new card: %s", c.Name) case c := <-q.del: - // @todo: send telegram notification instead if enabled if err := client.ArchiveCard(c); err != nil { - log.Printf("[-] error occurred while archiving card: %v", err) + logger.Errorf("could not archive card card: %v", err) break } - log.Printf("[+] archived stale card: %s", c.Name) + logger.Printf("archived stale card: %s", c.Name) case err := <-q.err: - // @todo: send telegram notification instead if enabled - log.Printf("[-] %v", err) + logger.Errorf("%v", err) case <-ctx.Done(): return } diff --git a/cmd/entrello/source.go b/cmd/entrello/source.go index 77258d6..c542fa9 100644 --- a/cmd/entrello/source.go +++ b/cmd/entrello/source.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "log" "time" "github.com/utkuufuk/entrello/internal/config" @@ -44,8 +43,7 @@ func getEnabledSourcesAndLabels(ctx context.Context, cfg config.Sources) (source for _, src := range arr { if ok, err := shouldQuery(src, now); !ok { if err != nil { - // @todo: send telegram notification instead if enabled - log.Printf("[-] could not check if '%s' should be queried or not, skipping", src.GetName()) + logger.Errorf("could not check if '%s' should be queried or not, skipping", src.GetName()) } continue } diff --git a/config.example.yml b/config.example.yml index 6d2042d..b013db8 100644 --- a/config.example.yml +++ b/config.example.yml @@ -25,3 +25,8 @@ sources: email: abc@def.com password: xxxxxxxx label_id: xxxxxxxxxxxxxxxxxxxxxxxx + +telegram: + enabled: true + token: xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + chat_id: 1234567890 diff --git a/go.mod b/go.mod index fc90051..c99e458 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,13 @@ go 1.13 require ( github.com/adlio/trello v1.7.0 + github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible github.com/golang/protobuf v1.4.2 // indirect github.com/google/go-cmp v0.4.1 github.com/google/go-github v17.0.0+incompatible github.com/google/go-querystring v1.0.0 // indirect - golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 // indirect + github.com/technoweenie/multipartstreamer v1.0.1 // indirect + golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d gopkg.in/yaml.v2 v2.3.0 ) diff --git a/go.sum b/go.sum index a7428a4..a97268b 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/adlio/trello v1.7.0 h1:syLRJ27wCM8URf7zOBWGr981cG+dpmLSyMqjEoQc+4g= github.com/adlio/trello v1.7.0/go.mod h1:l2068AhUuUuQ9Vsb95ECMueHThYyAj4e85lWPmr2/LE= +github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU= +github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -20,13 +22,15 @@ github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASu github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 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/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= +github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 h1:eDrdRpKgkcCqKZQwyZRyeFZgfqt37SL7Kv3tok06cKE= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/internal/config/config.go b/internal/config/config.go index cb592af..b5904e2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -48,10 +48,17 @@ type Trello struct { ListId string `yaml:"list_id"` } +type Telegram struct { + Enabled bool `yaml:"enabled"` + Token string `yaml:"token"` + ChatId int64 `yaml:"chat_id"` +} + type Config struct { - TimeoutSeconds int `yaml:"timeout_secs"` - Trello Trello `yaml:"trello"` - Sources Sources `yaml:"sources"` + TimeoutSeconds int `yaml:"timeout_secs"` + Trello Trello `yaml:"trello"` + Sources Sources `yaml:"sources"` + Telegram Telegram `yaml:"telegram"` } // ReadConfig reads the YAML config file & decodes all parameters diff --git a/internal/syslog/syslog.go b/internal/syslog/syslog.go new file mode 100644 index 0000000..b451672 --- /dev/null +++ b/internal/syslog/syslog.go @@ -0,0 +1,60 @@ +package syslog + +import ( + "fmt" + "log" + "os" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" + "github.com/utkuufuk/entrello/internal/config" +) + +type Logger struct { + enabled bool + api *tgbotapi.BotAPI + chatId int64 +} + +// NewLogger creates a new system logger instance +func NewLogger(cfg config.Telegram) (l Logger) { + if !cfg.Enabled { + return l + } + + api, err := tgbotapi.NewBotAPI(cfg.Token) + if err != nil { + log.Printf("could not create Telegram Logger: %v", err) + return l + } + return Logger{true, api, cfg.ChatId} +} + +// Printf logs an informational message to stdout, and also sends a Telegram message if enabled +func (l Logger) Printf(msg string, v ...interface{}) { + l.logf("Entrello:", msg, v...) +} + +// Errorf logs an error message to stdout, and also sends a Telegram message if enabled +func (l Logger) Errorf(msg string, v ...interface{}) { + l.logf("Entrello Error:", msg, v...) +} + +// Fatalf works like Errorf, but it returns with a non-zero exit code after logging +func (l Logger) Fatalf(msg string, v ...interface{}) { + l.Errorf(msg, v...) + os.Exit(1) +} + +// logf prints the message to stdout, and after prepending the given prefix to the message, +// also sends a Telegram message if enabled +func (l Logger) logf(prefix, msg string, v ...interface{}) { + msg = fmt.Sprintf(msg, v...) + log.Println(msg) + + if !l.enabled || l.api == nil || l.chatId == 0 { + return + } + msg = fmt.Sprintf("%s %s", prefix, msg) + m := tgbotapi.NewMessage(l.chatId, msg) + l.api.Send(m) +} diff --git a/internal/syslog/syslog_test.go b/internal/syslog/syslog_test.go new file mode 100644 index 0000000..cb24805 --- /dev/null +++ b/internal/syslog/syslog_test.go @@ -0,0 +1,81 @@ +package syslog + +import ( + "bytes" + "log" + "os" + "strings" + "testing" + + "github.com/utkuufuk/entrello/internal/config" +) + +func TestSystemLog(t *testing.T) { + tt := []struct { + name string + cfg config.Telegram + }{ + { + name: "null config", + cfg: config.Telegram{Enabled: false, Token: "", ChatId: 0}, + }, + { + name: "psuedo-valid config but disabled", + cfg: config.Telegram{ + Enabled: false, + Token: "xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + ChatId: 1234567890, + }, + }, + { + name: "psuedo-valid config and enabled", + cfg: config.Telegram{ + Enabled: false, + Token: "xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + ChatId: 1234567890, + }, + }, + { + name: "enabled but invalid token", + cfg: config.Telegram{ + Enabled: true, + Token: "banana", + ChatId: 1234567890, + }, + }, + { + name: "enabled but invalid chat ID", + cfg: config.Telegram{ + Enabled: true, + Token: "xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + ChatId: 0, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + msg := "test" + logger := NewLogger(tc.cfg) + var i, e bytes.Buffer + + log.SetOutput(&i) + logger.Printf(msg) + + log.SetOutput(&e) + logger.Errorf(msg) + + log.SetOutput(os.Stderr) + + oi := i.String() + if !strings.Contains(oi, msg) { + t.Errorf("wanted '%s' to contain '%s'", oi, msg) + } + + oe := e.String() + if !strings.Contains(oe, msg) { + t.Errorf("wanted '%s' to contain '%s'", oe, msg) + } + }) + } +}