package bot import ( "context" "fmt" "net" "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" "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" ) 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 changesChan chan net.IP exitChan chan struct{} logger logrus.Logger } 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 := fmt.Sprintf("Public IP tracker initialized at %v, public IP is %s", currentTime, publicIP) if err := n.tgBot.SendMessage(n.ctx, n.tgChatID, 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 := fmt.Sprintf("Public IP has been changed, is now %s", n.currentIP) if err := n.tgBot.SendMessage(n.ctx, n.tgChatID, 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.tgBot.SendMessage(n.ctx, n.tgChatID, 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 New(ctx context.Context, config *config.Config) *Notifier { subCtx, cancel := context.WithCancel(ctx) tgBot := bot.New(config.Telegram.Token) 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, } }