Add support for external sender #34
9 changed files with 212 additions and 28 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -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/
|
||||||
|
|
|
@ -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" ]
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
17
examples/docker-compose-unix.yml
Normal file
17
examples/docker-compose-unix.yml
Normal 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'
|
|
@ -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"
|
||||||
|
@ -41,10 +43,22 @@ type Notifier struct {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
26
tracker/messager/messager.go
Normal file
26
tracker/messager/messager.go
Normal 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)
|
||||||
|
}
|
66
tracker/unixsender/unixsender.go
Normal file
66
tracker/unixsender/unixsender.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue