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 *.out
# build directory # build directory
tracker/build **/build
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/

View file

@ -10,7 +10,7 @@ FROM --platform=$TARGETPLATFORM alpine:latest
WORKDIR /root WORKDIR /root
COPY --from=builder go/src/git.faercol.me/public-ip-tracker/build/tracker ./ COPY --from=builder go/src/git.faercol.me/public-ip-tracker/build/tracker ./
VOLUME [ "/config" ] VOLUME [ "/config", "/output" ]
ENTRYPOINT [ "./tracker" ] ENTRYPOINT [ "./tracker" ]
CMD [ "-config", "/config/config.json" ] 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, "polling_frequency": 5,
"log": { "log": {
"level": "info" "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/config"
"git.faercol.me/faercol/public-ip-tracker/tracker/ip" "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/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/bot"
"github.com/ahugues/go-telegram-api/notifier" "github.com/ahugues/go-telegram-api/notifier"
"github.com/ahugues/go-telegram-api/structs" "github.com/ahugues/go-telegram-api/structs"
@ -31,20 +33,32 @@ Public IP has changed, new IP is %s`, formattedHostname, currentTime.Format(time
} }
type Notifier struct { type Notifier struct {
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
tgBot bot.Bot tgBot bot.Bot
tgChatID int64 tgChatID int64
tgWatcher notifier.EventNotifier tgWatcher notifier.EventNotifier
ipGetter ip.IPGetter ipGetter ip.IPGetter
timeGetter func() time.Time timeGetter func() time.Time
currentIP net.IP currentIP net.IP
frequency time.Duration frequency time.Duration
errChan chan error errChan chan error
changesChan chan net.IP exitChan chan struct{}
exitChan chan struct{} logger logrus.Logger
logger logrus.Logger hostname string
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 { func (n *Notifier) SendInitMessage() error {
@ -59,7 +73,8 @@ func (n *Notifier) SendInitMessage() error {
currentTime := n.timeGetter() currentTime := n.timeGetter()
n.logger.Debug("Sending init message to Telegram") 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) return fmt.Errorf("failed to send initialization message: %w", err)
} }
n.logger.Debug("Message sent") n.logger.Debug("Message sent")
@ -67,7 +82,8 @@ func (n *Notifier) SendInitMessage() error {
} }
func (n *Notifier) sendUpdatedIPMsg() 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 fmt.Errorf("failed to send update message: %w", err)
} }
return nil return nil
@ -75,7 +91,7 @@ func (n *Notifier) sendUpdatedIPMsg() error {
func (n *Notifier) sendCurrentIP() error { func (n *Notifier) sendCurrentIP() error {
statusMsg := fmt.Sprintf("Current public IP is %s", n.currentIP) 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 fmt.Errorf("failed to send message: %w", err)
} }
return nil return nil
@ -140,10 +156,26 @@ func (n *Notifier) Exit() <-chan struct{} {
return n.exitChan 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) subCtx, cancel := context.WithCancel(ctx)
tgBot := bot.New(config.Telegram.Token) 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{ return &Notifier{
ctx: subCtx, ctx: subCtx,
cancel: cancel, cancel: cancel,
@ -157,5 +189,7 @@ func New(ctx context.Context, config *config.Config) *Notifier {
tgWatcher: notifier.New(config.Telegram.Token), tgWatcher: notifier.New(config.Telegram.Token),
logger: logger.L, logger: logger.L,
hostname: config.Hostname, hostname: config.Hostname,
} sendMode: config.Export.Mode,
sender: sender,
}, nil
} }

View file

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

View file

@ -48,7 +48,10 @@ func main() {
} }
logger.L.Debug("Initializing notification bot") 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") logger.L.Debug("Sending initialization message to Telegram")
if err := notifBot.SendInitMessage(); err != nil { 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
}