Skip to content

Commit

Permalink
Integrate Telegram (#15)
Browse files Browse the repository at this point in the history
* integrate telegram as an optional system logger

* fix config.example.yml

* update readme

* add some unit tests
  • Loading branch information
utkuufuk authored Jun 2, 2020
1 parent 3d8454e commit ea4a612
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 24 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
17 changes: 11 additions & 6 deletions cmd/entrello/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
14 changes: 5 additions & 9 deletions cmd/entrello/queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package main
import (
"context"
"fmt"
"log"

"github.com/utkuufuk/entrello/internal/trello"
)
Expand Down Expand Up @@ -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
}
Expand Down
4 changes: 1 addition & 3 deletions cmd/entrello/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package main
import (
"context"
"fmt"
"log"
"time"

"github.com/utkuufuk/entrello/internal/config"
Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 5 additions & 0 deletions config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ sources:
email: abc@def.com
password: xxxxxxxx
label_id: xxxxxxxxxxxxxxxxxxxxxxxx

telegram:
enabled: true
token: xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
chat_id: 1234567890
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand All @@ -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=
Expand Down
13 changes: 10 additions & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 60 additions & 0 deletions internal/syslog/syslog.go
Original file line number Diff line number Diff line change
@@ -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)
}
81 changes: 81 additions & 0 deletions internal/syslog/syslog_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}

0 comments on commit ea4a612

Please sign in to comment.