Gotify support (#17)

* Added Gotify support and notifications

* Readme update for Gotify
This commit is contained in:
Quentin McGaw
2019-09-06 13:33:31 -04:00
committed by GitHub
parent dd155fcf41
commit 19f275ac03
11 changed files with 176 additions and 10 deletions

53
pkg/admin/gotify.go Normal file
View File

@@ -0,0 +1,53 @@
package admin
import (
"fmt"
"net/http"
"net/url"
"github.com/gotify/go-api-client/v2/auth"
"github.com/gotify/go-api-client/v2/client"
"github.com/gotify/go-api-client/v2/client/message"
"github.com/gotify/go-api-client/v2/gotify"
"github.com/gotify/go-api-client/v2/models"
"go.uber.org/zap"
)
// Gotify contains the Gotify API client and the token for the application
type Gotify struct {
client *client.GotifyREST
token string
}
// NewGotify creates an API client with the token for the Gotify server
func NewGotify(URL *url.URL, token string, httpClient *http.Client) (g *Gotify, err error) {
if URL == nil {
return &Gotify{}, fmt.Errorf("Gotify URL not provided")
} else if token == "" {
return &Gotify{}, fmt.Errorf("Gotify token not provided")
}
client := gotify.NewClient(URL, httpClient)
_, err = client.Version.GetVersion(nil)
if err != nil {
return &Gotify{}, fmt.Errorf("cannot communicate with Gotify server: %w", err)
}
return &Gotify{client: client, token: token}, nil
}
// Notify sends a notification to the Gotify server
func (g *Gotify) Notify(title string, priority int, content string, args ...interface{}) {
if g == nil || g.client == nil {
return
}
content = fmt.Sprintf(content, args...)
params := message.NewCreateMessageParams()
params.Body = &models.MessageExternal{
Title: title,
Message: content,
Priority: priority,
}
_, err := g.client.Message.CreateMessage(params, auth.TokenAuth(g.token))
if err != nil {
zap.S().Warn("cannot send message to Gotify: %s", err)
}
}

View File

@@ -12,6 +12,8 @@ func init() {
viper.SetDefault("logmode", "")
viper.SetDefault("loglevel", "")
viper.SetDefault("nodeid", "0")
viper.SetDefault("gotifyurl", "")
viper.SetDefault("gotifytoken", "")
viper.BindEnv("listeningport")
viper.BindEnv("rooturl")
viper.BindEnv("delay")
@@ -19,4 +21,6 @@ func init() {
viper.BindEnv("logmode")
viper.BindEnv("loglevel")
viper.BindEnv("nodeid")
viper.BindEnv("gotifyurl")
viper.BindEnv("gotifytoken")
}

View File

@@ -1,6 +1,7 @@
package params
import (
"net/url"
"os"
"path/filepath"
"strconv"
@@ -107,6 +108,25 @@ func GetNodeID() int {
return value
}
// GetGotifyURL obtains the URL to the Gotify server
func GetGotifyURL() (URL *url.URL) {
s := viper.GetString("gotifyurl")
if s == "" {
return nil
}
URL, err := url.Parse(s)
if err != nil {
zap.S().Fatalf("URL %s is not valid", s)
}
return URL
}
// GetGotifyToken obtains the token for the app on the Gotify server
func GetGotifyToken() (token string) {
token = viper.GetString("gotifytoken")
return token
}
func stringInAny(s string, ss ...string) bool {
for _, x := range ss {
if s == x {

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"text/template"
"ddns-updater/pkg/admin"
"ddns-updater/pkg/models"
"ddns-updater/pkg/network"
@@ -14,6 +15,7 @@ import (
type healthcheckParamsType struct {
recordsConfigs []models.RecordConfigType
gotify *admin.Gotify
}
type indexParamsType struct {
@@ -27,9 +29,10 @@ type updateParamsType struct {
}
// CreateRouter returns a router with all the necessary routes configured
func CreateRouter(rootURL, dir string, forceCh chan struct{}, recordsConfigs []models.RecordConfigType) *httprouter.Router {
func CreateRouter(rootURL, dir string, forceCh chan struct{}, recordsConfigs []models.RecordConfigType, gotify *admin.Gotify) *httprouter.Router {
healthcheckParams := healthcheckParamsType{
recordsConfigs: recordsConfigs,
gotify: gotify,
}
indexParams := indexParamsType{
dir: dir,
@@ -62,17 +65,20 @@ func (params *healthcheckParamsType) get(w http.ResponseWriter, r *http.Request,
clientIP, err := network.GetClientIP(r)
if err != nil {
zap.S().Infof("Cannot detect client IP: %s", err)
params.gotify.Notify("DDNS Updater", 5, "Cannot detect client IP: %s", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
if clientIP != "127.0.0.1" && clientIP != "::1" {
zap.S().Infof("IP address %s tried to perform the healthcheck", clientIP)
params.gotify.Notify("DDNS Updater", 5, "IP address %s tried to perform the healthcheck", clientIP)
w.WriteHeader(http.StatusUnauthorized)
return
}
err = healthcheckHandler(params.recordsConfigs)
if err != nil {
zap.S().Warnf("Responded with error to healthcheck: %s", err)
params.gotify.Notify("DDNS Updater", 4, "Responded with error to healthcheck: %s", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

View File

@@ -2,6 +2,7 @@ package update
import (
"ddns-updater/pkg/database"
"ddns-updater/pkg/admin"
"ddns-updater/pkg/models"
"net/http"
"time"
@@ -14,6 +15,7 @@ func TriggerServer(
recordsConfigs []models.RecordConfigType, // does not change size so no pointer needed
httpClient *http.Client,
sqlDb *database.DB,
gotify *admin.Gotify,
) {
var chQuitArr, chForceArr []chan struct{}
defer func() {
@@ -31,9 +33,9 @@ func TriggerServer(
chQuitArr = append(chQuitArr, make(chan struct{}))
customDelay := recordsConfigs[i].Settings.Delay
if customDelay > 0 {
go periodicServer(&recordsConfigs[i], customDelay, httpClient, sqlDb, chForceArr[i], chQuitArr[i])
go periodicServer(&recordsConfigs[i], customDelay, httpClient, sqlDb, chForceArr[i], chQuitArr[i], gotify)
} else {
go periodicServer(&recordsConfigs[i], delay, httpClient, sqlDb, chForceArr[i], chQuitArr[i])
go periodicServer(&recordsConfigs[i], delay, httpClient, sqlDb, chForceArr[i], chQuitArr[i], gotify)
}
}
// fan out channel signals
@@ -58,6 +60,7 @@ func periodicServer(
httpClient *http.Client,
sqlDb *database.DB,
chForce, chQuit chan struct{},
gotify *admin.Gotify,
) {
ticker := time.NewTicker(delay * time.Second)
defer func() {
@@ -68,9 +71,9 @@ func periodicServer(
for {
select {
case <-ticker.C:
go update(recordConfig, httpClient, sqlDb)
go update(recordConfig, httpClient, sqlDb, gotify)
case <-chForce:
go update(recordConfig, httpClient, sqlDb)
go update(recordConfig, httpClient, sqlDb, gotify)
case <-chQuit:
recordConfig.IsUpdating.Lock() // wait for an eventual update to finish
ticker.Stop()

View File

@@ -10,6 +10,7 @@ import (
"strings"
"time"
"ddns-updater/pkg/admin"
"ddns-updater/pkg/database"
"ddns-updater/pkg/models"
"ddns-updater/pkg/network"
@@ -32,6 +33,7 @@ func update(
recordConfig *models.RecordConfigType,
httpClient *http.Client,
sqlDb *database.DB,
gotify *admin.Gotify,
) {
var err error
recordConfig.IsUpdating.Lock()
@@ -44,6 +46,7 @@ func update(
recordConfig.Status.SetCode(models.FAIL)
recordConfig.Status.SetMessage("%s", err)
zap.S().Warn(recordConfig)
gotify.Notify("DDNS Updater", 5, recordConfig.String())
return
}
// Note: empty IP means DNS provider provided
@@ -123,6 +126,7 @@ func update(
recordConfig.Status.SetCode(models.FAIL)
recordConfig.Status.SetMessage("%s", err)
zap.S().Warn(recordConfig)
gotify.Notify("DDNS Updater", 5, recordConfig.String())
return
}
if len(ips) > 0 && ip == ips[0] { // same IP
@@ -132,6 +136,7 @@ func update(
if err != nil {
recordConfig.Status.SetCode(models.FAIL)
recordConfig.Status.SetMessage("Cannot update database: %s", err)
gotify.Notify("DDNS Updater", 4, "Cannot update database: %s", err)
}
return
}
@@ -140,10 +145,12 @@ func update(
recordConfig.Status.SetMessage("")
recordConfig.History.SetTSuccess(time.Now())
recordConfig.History.PrependIP(ip)
gotify.Notify("DDNS Updater", 1, "%s changed from %s to %s", recordConfig.Settings.BuildDomainName(), ips[0], ip)
err = sqlDb.StoreNewIP(recordConfig.Settings.Domain, recordConfig.Settings.Host, ip)
if err != nil {
recordConfig.Status.SetCode(models.FAIL)
recordConfig.Status.SetMessage("Cannot update database: %s", err)
gotify.Notify("DDNS Updater", 4, "Cannot update database: %s", err)
}
}