diff --git a/.gitignore b/.gitignore index 93a1dee2..6dffcf55 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ dist/ build/ .idea/ server/docs/ +tools/fbsend/fbsend *.iml diff --git a/config/config.go b/config/config.go index 9e1640a8..90dbe7cb 100644 --- a/config/config.go +++ b/config/config.go @@ -7,14 +7,15 @@ import ( // Defines default config settings const ( - DefaultListenHTTP = ":80" - DefaultCacheDuration = 12 * time.Hour - DefaultKeepaliveInterval = 30 * time.Second - DefaultManagerInterval = time.Minute - DefaultAtSenderInterval = 10 * time.Second - DefaultMinDelay = 10 * time.Second - DefaultMaxDelay = 3 * 24 * time.Hour - DefaultMessageLimit = 512 + DefaultListenHTTP = ":80" + DefaultCacheDuration = 12 * time.Hour + DefaultKeepaliveInterval = 30 * time.Second + DefaultManagerInterval = time.Minute + DefaultAtSenderInterval = 10 * time.Second + DefaultMinDelay = 10 * time.Second + DefaultMaxDelay = 3 * 24 * time.Hour + DefaultMessageLimit = 512 + DefaultFirebaseKeepaliveInterval = time.Hour ) // Defines all the limits @@ -40,6 +41,7 @@ type Config struct { KeepaliveInterval time.Duration ManagerInterval time.Duration AtSenderInterval time.Duration + FirebaseKeepaliveInterval time.Duration MessageLimit int MinDelay time.Duration MaxDelay time.Duration @@ -66,6 +68,7 @@ func New(listenHTTP string) *Config { MinDelay: DefaultMinDelay, MaxDelay: DefaultMaxDelay, AtSenderInterval: DefaultAtSenderInterval, + FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval, GlobalTopicLimit: DefaultGlobalTopicLimit, VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst, VisitorRequestLimitReplenish: DefaultVisitorRequestLimitReplenish, diff --git a/server/server.go b/server/server.go index 6fadd9d9..0382b51d 100644 --- a/server/server.go +++ b/server/server.go @@ -105,6 +105,10 @@ var ( errHTTPTooManyRequests = &errHTTP{http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests)} ) +const ( + firebaseControlTopic = "~control" // See Android if changed +) + // New instantiates a new Server. It creates the cache and adds a Firebase // subscriber (if configured). func New(conf *config.Config) (*Server, error) { @@ -152,9 +156,17 @@ func createFirebaseSubscriber(conf *config.Config) (subscriber, error) { return nil, err } return func(m *message) error { - _, err := msg.Send(context.Background(), &messaging.Message{ - Topic: m.Topic, - Data: map[string]string{ + var data map[string]string // Matches https://ntfy.sh/docs/subscribe/api/#json-message-format + switch m.Event { + case keepaliveEvent, openEvent: + data = map[string]string{ + "id": m.ID, + "time": fmt.Sprintf("%d", m.Time), + "event": m.Event, + "topic": m.Topic, + } + case messageEvent: + data = map[string]string{ "id": m.ID, "time": fmt.Sprintf("%d", m.Time), "event": m.Event, @@ -163,7 +175,11 @@ func createFirebaseSubscriber(conf *config.Config) (subscriber, error) { "tags": strings.Join(m.Tags, ","), "title": m.Title, "message": m.Message, - }, + } + } + _, err := msg.Send(context.Background(), &messaging.Message{ + Topic: m.Topic, + Data: data, }) return err }, nil @@ -188,6 +204,17 @@ func (s *Server) Run() error { } } }() + if s.firebase != nil { + go func() { + ticker := time.NewTicker(s.config.FirebaseKeepaliveInterval) + for { + <-ticker.C + if err := s.firebase(newKeepaliveMessage(firebaseControlTopic)); err != nil { + log.Printf("error sending Firebase keepalive message: %s", err.Error()) + } + } + }() + } listenStr := fmt.Sprintf("%s/http", s.config.ListenHTTP) if s.config.ListenHTTPS != "" { listenStr += fmt.Sprintf(" %s/https", s.config.ListenHTTPS) diff --git a/tools/fbsend/main.go b/tools/fbsend/main.go new file mode 100644 index 00000000..cd3a06d1 --- /dev/null +++ b/tools/fbsend/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "context" + firebase "firebase.google.com/go" + "firebase.google.com/go/messaging" + "flag" + "fmt" + "google.golang.org/api/option" + "os" + "strings" +) + +func main() { + conffile := flag.String("config", "/etc/fbsend/fbsend.json", "config file") + flag.Parse() + if flag.NArg() < 2 { + fail("Syntax: fbsend [-config FILE] topic key=value ...") + } + topic := flag.Arg(0) + data := make(map[string]string) + for i := 1; i < flag.NArg(); i++ { + kv := strings.SplitN(flag.Arg(i), "=", 2) + if len(kv) != 2 { + fail(fmt.Sprintf("Invalid argument: %s (%v)", flag.Arg(i), kv)) + } + data[kv[0]] = kv[1] + } + fb, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsFile(*conffile)) + if err != nil { + fail(err.Error()) + } + msg, err := fb.Messaging(context.Background()) + if err != nil { + fail(err.Error()) + } + _, err = msg.Send(context.Background(), &messaging.Message{ + Topic: topic, + Data: data, + }) + if err != nil { + fail(err.Error()) + } + fmt.Println("Sent successfully") +} + +func fail(s string) { + fmt.Println(s) + os.Exit(1) +}