public-ip-tracker/tracker/bot/bot.go
Melora Hugues bd14b3c731
All checks were successful
continuous-integration/drone/push Build is passing
Add support for external sender
This commit allows using an external unix exporter to send the messages
instead of directly sending the messages to Telegram
2023-03-12 14:12:48 +01:00

195 lines
5.5 KiB
Go

package bot
import (
"context"
"fmt"
"net"
"strings"
"time"
"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"
"github.com/sirupsen/logrus"
)
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)
}
type Notifier struct {
ctx context.Context
cancel context.CancelFunc
tgBot bot.Bot
tgChatID int64
tgWatcher notifier.EventNotifier
ipGetter ip.IPGetter
timeGetter func() time.Time
currentIP net.IP
frequency time.Duration
errChan chan error
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 {
n.logger.Debug("Getting current public IP")
publicIP, err := n.ipGetter.GetCurrentPublicIP(n.ctx)
if err != nil {
return fmt.Errorf("failed to get current public IP: %w", err)
}
n.logger.Debugf("Current public IP is %s", publicIP.String())
n.currentIP = publicIP
currentTime := n.timeGetter()
n.logger.Debug("Sending init message to Telegram")
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")
return nil
}
func (n *Notifier) sendUpdatedIPMsg() error {
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
}
func (n *Notifier) sendCurrentIP() error {
statusMsg := fmt.Sprintf("Current public IP is %s", n.currentIP)
if err := n.sendMessage(statusMsg); err != nil {
return fmt.Errorf("failed to send message: %w", err)
}
return nil
}
func (n *Notifier) watchTG() {
n.logger.Debug("Subscribing to messages notificator")
id, updateChan := n.tgWatcher.Subscribe([]structs.UpdateType{structs.UpdateMessage})
go n.tgWatcher.Run(n.ctx)
for {
select {
case update := <-updateChan:
if update.Message.Text == "/getIP" {
if err := n.sendCurrentIP(); err != nil {
n.errChan <- fmt.Errorf("failed to reply current public IP: %w", err)
}
}
case <-n.ctx.Done():
n.tgWatcher.Unsubscribe(id)
return
}
}
}
func (n *Notifier) Run() {
go n.watchTG()
n.logger.Infof("Start watching for public IP changes, polling frequency is %v", n.frequency)
for {
select {
case <-time.After(n.frequency):
n.logger.Debug("Checking if current IP has changed")
newIP, err := n.ipGetter.GetCurrentPublicIP(n.ctx)
if err != nil {
n.errChan <- fmt.Errorf("failed to update public IP: %w", err)
continue
}
n.logger.Debugf("Got new public IP %s", newIP.String())
if !newIP.Equal(n.currentIP) {
n.logger.Debug("New public IP is different from previous IP")
n.logger.Warnf("Public IP has changed from %s to %s", n.currentIP.String(), newIP.String())
n.currentIP = newIP
if err := n.sendUpdatedIPMsg(); err != nil {
n.errChan <- err
}
} else {
n.logger.Debug("Public IP has not changed")
}
case <-n.ctx.Done():
n.logger.Info("Stopping notification daemon")
n.exitChan <- struct{}{}
return
}
}
}
func (n *Notifier) ErrChan() <-chan error {
return n.errChan
}
func (n *Notifier) Exit() <-chan struct{} {
return n.exitChan
}
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,
tgBot: tgBot,
tgChatID: config.Telegram.ChannelID,
timeGetter: time.Now,
ipGetter: ip.New(),
errChan: make(chan error, 10),
exitChan: make(chan struct{}, 1),
frequency: config.PollingFrequency,
tgWatcher: notifier.New(config.Telegram.Token),
logger: logger.L,
hostname: config.Hostname,
sendMode: config.Export.Mode,
sender: sender,
}, nil
}