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 }