From 63206f858190b35a95ae8a461d8262834d33a1f5 Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Mon, 13 Dec 2021 22:30:28 -0500
Subject: [PATCH] Firebase keepalive, supports #56

---
 .gitignore           |  1 +
 config/config.go     | 19 ++++++++++-------
 server/server.go     | 35 +++++++++++++++++++++++++++----
 tools/fbsend/main.go | 50 ++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 93 insertions(+), 12 deletions(-)
 create mode 100644 tools/fbsend/main.go

diff --git a/.gitignore b/.gitignore
index 93a1dee..6dffcf5 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 9e1640a..90dbe7c 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 6fadd9d..0382b51 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 0000000..cd3a06d
--- /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)
+}
-- 
GitLab