Add support for external sender
All checks were successful
continuous-integration/drone/push Build is passing

This commit allows using an external unix exporter to send the messages
instead of directly sending the messages to Telegram
This commit is contained in:
Melora Hugues 2023-03-12 14:12:48 +01:00
parent d9c57f7441
commit bd14b3c731
9 changed files with 212 additions and 28 deletions

2
.gitignore vendored
View file

@ -16,7 +16,7 @@
*.out
# build directory
tracker/build
**/build
# Dependency directories (remove the comment below to include it)
# vendor/

View file

@ -10,7 +10,7 @@ FROM --platform=$TARGETPLATFORM alpine:latest
WORKDIR /root
COPY --from=builder go/src/git.faercol.me/public-ip-tracker/build/tracker ./
VOLUME [ "/config" ]
VOLUME [ "/config", "/output" ]
ENTRYPOINT [ "./tracker" ]
CMD [ "-config", "/config/config.json" ]

View file

@ -44,6 +44,9 @@ For now, the program is configured through a JSON configuration file. Here is a
"polling_frequency": 5,
"log": {
"level": "info"
},
"export": {
"mode": "native
}
}
```

View file

@ -0,0 +1,17 @@
services:
telegram_exporter:
container_name: telegram_exporter
image: git.faercol.me/notification/telegram-notifier:latest
environment:
- SOCK_PATH=/input/telegram.sock
volumes:
- './build/config/exporter:/config'
- './build/run:/input'
ip_tracker:
container_name: ip_tracker
image: git.faercol.me/faercol/public-ip-tracker:latest
# Volumes store your data between container upgrades
volumes:
- './build/config/tracker:/config'
- './build/run:/output'

View file

@ -10,6 +10,8 @@ import (
"git.faercol.me/faercol/public-ip-tracker/tracker/config"
"git.faercol.me/faercol/public-ip-tracker/tracker/ip"
"git.faercol.me/faercol/public-ip-tracker/tracker/logger"
"git.faercol.me/faercol/public-ip-tracker/tracker/messager"
"git.faercol.me/faercol/public-ip-tracker/tracker/unixsender"
"github.com/ahugues/go-telegram-api/bot"
"github.com/ahugues/go-telegram-api/notifier"
"github.com/ahugues/go-telegram-api/structs"
@ -41,10 +43,22 @@ type Notifier struct {
currentIP net.IP
frequency time.Duration
errChan chan error
changesChan chan net.IP
exitChan chan struct{}
logger logrus.Logger
hostname string
sendMode config.ExportMode
sender messager.Sender
}
func (n *Notifier) sendMessage(msg string) error {
switch n.sendMode {
case config.ExportNative:
return n.tgBot.SendMessage(n.ctx, n.tgChatID, msg, structs.FormattingMarkdownV2)
case config.ExportUnix:
return n.sender.SendMessage(msg)
default:
return fmt.Errorf("invalid sending mode %v", n.sendMode)
}
}
func (n *Notifier) SendInitMessage() error {
@ -59,7 +73,8 @@ func (n *Notifier) SendInitMessage() error {
currentTime := n.timeGetter()
n.logger.Debug("Sending init message to Telegram")
if err := n.tgBot.SendMessage(n.ctx, n.tgChatID, formatInitMsg(currentTime, publicIP, n.hostname), structs.FormattingMarkdownV2); err != nil {
initMsg := formatInitMsg(currentTime, publicIP, n.hostname)
if err := n.sendMessage(initMsg); err != nil {
return fmt.Errorf("failed to send initialization message: %w", err)
}
n.logger.Debug("Message sent")
@ -67,7 +82,8 @@ func (n *Notifier) SendInitMessage() error {
}
func (n *Notifier) sendUpdatedIPMsg() error {
if err := n.tgBot.SendMessage(n.ctx, n.tgChatID, formatUpdate(n.timeGetter(), n.currentIP, n.hostname), structs.FormattingMarkdownV2); err != nil {
updateMsg := formatUpdate(n.timeGetter(), n.currentIP, n.hostname)
if err := n.sendMessage(updateMsg); err != nil {
return fmt.Errorf("failed to send update message: %w", err)
}
return nil
@ -75,7 +91,7 @@ func (n *Notifier) sendUpdatedIPMsg() error {
func (n *Notifier) sendCurrentIP() error {
statusMsg := fmt.Sprintf("Current public IP is %s", n.currentIP)
if err := n.tgBot.SendMessage(n.ctx, n.tgChatID, statusMsg, structs.FormattingMarkdownV2); err != nil {
if err := n.sendMessage(statusMsg); err != nil {
return fmt.Errorf("failed to send message: %w", err)
}
return nil
@ -140,10 +156,26 @@ func (n *Notifier) Exit() <-chan struct{} {
return n.exitChan
}
func New(ctx context.Context, config *config.Config) *Notifier {
func buildSender(ctx context.Context, conf *config.Config) (messager.Sender, error) {
switch conf.Export.Mode {
case config.ExportUnix:
logger.L.Infof("Building notifier in Unix mode")
return unixsender.New(ctx, conf)
default:
return nil, nil
}
}
func New(ctx context.Context, config *config.Config) (*Notifier, error) {
subCtx, cancel := context.WithCancel(ctx)
tgBot := bot.New(config.Telegram.Token)
sender, err := buildSender(subCtx, config)
if err != nil {
cancel()
return nil, fmt.Errorf("failed to build message sender: %w", err)
}
return &Notifier{
ctx: subCtx,
cancel: cancel,
@ -157,5 +189,7 @@ func New(ctx context.Context, config *config.Config) *Notifier {
tgWatcher: notifier.New(config.Telegram.Token),
logger: logger.L,
hostname: config.Hostname,
}
sendMode: config.Export.Mode,
sender: sender,
}, nil
}

View file

@ -9,6 +9,24 @@ import (
"github.com/sirupsen/logrus"
)
type ExportMode int64
const (
ExportNative = iota
ExportUnix
)
func exportModeFromStr(val string) ExportMode {
switch val {
case "native":
return ExportNative
case "unix":
return ExportUnix
default:
return ExportNative
}
}
type TelegramConfig struct {
ChannelID int64 `json:"channel_id"`
Token string `json:"token"`
@ -22,18 +40,30 @@ type jsonLogConfig struct {
Level string `json:"level"`
}
type ExportConfig struct {
Mode ExportMode
UnixSock string
}
type jsonExportConfig struct {
Mode string `json:"mode"`
UnixSock string `json:"sock_path"`
}
type Config struct {
Telegram *TelegramConfig
PollingFrequency time.Duration
Hostname string
Log *LogConfig
Log LogConfig
Export ExportConfig
}
type jsonConfig struct {
Telegram *TelegramConfig `json:"telegram"`
PollingFrequency int64 `json:"polling_frequency"`
Hostname string `json:"hostname"`
Log *jsonLogConfig `json:"log"`
Log jsonLogConfig `json:"log"`
Export jsonExportConfig `json:"export"`
}
func parseLevel(lvlStr string) logrus.Level {
@ -54,12 +84,17 @@ func New(filepath string) (*Config, error) {
if err := json.Unmarshal(content, &jsonConf); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
}
return &Config{
Telegram: jsonConf.Telegram,
PollingFrequency: time.Duration(jsonConf.PollingFrequency) * time.Second,
Hostname: jsonConf.Hostname,
Log: &LogConfig{
Log: LogConfig{
Level: parseLevel(jsonConf.Log.Level),
},
Export: ExportConfig{
UnixSock: jsonConf.Export.UnixSock,
Mode: exportModeFromStr(jsonConf.Export.Mode),
},
}, nil
}

View file

@ -48,7 +48,10 @@ func main() {
}
logger.L.Debug("Initializing notification bot")
notifBot := bot.New(mainCtx, conf)
notifBot, err := bot.New(mainCtx, conf)
if err != nil {
logger.L.Fatalf("Failed to create notification bot: %s", err.Error())
}
logger.L.Debug("Sending initialization message to Telegram")
if err := notifBot.SendInitMessage(); err != nil {

View file

@ -0,0 +1,26 @@
package messager
import (
"fmt"
"net"
"strings"
"time"
)
type Sender interface {
SendMessage(message string) error
}
func FormatInitMsg(currentTime time.Time, publicIP net.IP, hostname string) string {
formattedHostname := fmt.Sprintf("`%s`", hostname)
formattedIP := strings.ReplaceAll(publicIP.String(), ".", "\\.")
return fmt.Sprintf(`\[Host %s\] %s
Public IP tracker initialized\. Current IP is %s`, formattedHostname, currentTime.Format(time.RFC1123), formattedIP)
}
func FormatUpdate(currentTime time.Time, publicIP net.IP, hostname string) string {
formattedHostname := fmt.Sprintf("`%s`", hostname)
formattedIP := strings.ReplaceAll(publicIP.String(), ".", "\\.")
return fmt.Sprintf(`\[Host %s\] %s
Public IP has changed, new IP is %s`, formattedHostname, currentTime.Format(time.RFC1123), formattedIP)
}

View file

@ -0,0 +1,66 @@
package unixsender
import (
"bytes"
"context"
"fmt"
"net"
"net/http"
"git.faercol.me/faercol/public-ip-tracker/tracker/config"
"git.faercol.me/faercol/public-ip-tracker/tracker/logger"
"github.com/sirupsen/logrus"
)
const method = http.MethodPost
const baseURL = "http://unix/"
type UnixSender struct {
logger *logrus.Logger
ctx context.Context
cancel context.CancelFunc
httpClt *http.Client
}
func (s *UnixSender) SendMessage(message string) error {
s.logger.Debug("Sending message to unix sock")
body := bytes.NewBufferString(message)
req, err := http.NewRequestWithContext(s.ctx, method, baseURL, body)
if err != nil {
return fmt.Errorf("failed to prepare request: %w", err)
}
resp, err := s.httpClt.Do(req)
if err != nil {
return fmt.Errorf("failed to run query: %w", err)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("invalid returncode %d", resp.StatusCode)
}
return nil
}
func New(ctx context.Context, config *config.Config) (*UnixSender, error) {
subCtx, cancel := context.WithCancel(ctx)
logger.L.Infof("Creating Unix exporter to sock %q", config.Export.UnixSock)
httpClt := http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dialer := net.Dialer{}
return dialer.DialContext(ctx, "unix", config.Export.UnixSock)
},
},
}
return &UnixSender{
ctx: subCtx,
cancel: cancel,
httpClt: &httpClt,
logger: &logger.L,
}, nil
}