commit bd4106b69f75716982d740b62c03d3cdc0a72f48 Author: Niklas Date: Fri Oct 9 23:08:07 2020 +0200 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..784958d --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +Listen for [Gotify](https://gotify.net/) push notifications and displays a notification on the dektop via dbus. + +# Usage +```bash +gotify-dektop +``` \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4e0b28f --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module git.kapelle.org/niklas/gotify-desktop + +go 1.15 + +require ( + github.com/godbus/dbus v4.1.0+incompatible + github.com/gorilla/websocket v1.4.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e62d8aa --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= +github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= diff --git a/main.go b/main.go new file mode 100644 index 0000000..4542741 --- /dev/null +++ b/main.go @@ -0,0 +1,198 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "path" + + "github.com/godbus/dbus" + "github.com/gorilla/websocket" +) + +type messageType struct { + ID int `json:"id"` + AppID int `json:"appid"` + Message string `json:"message"` + Title string `json:"title"` + Priority int `json:"priority"` + // Date time.Time `json:"date"` // Ignore that for now +} + +type applicationType struct { + Description string `json:"description"` + ID int `json:"id"` + Image string `json:"image"` + Name string `json:"name"` + Token string `json:"token"` +} + +func listenToPushEvents(url string, c chan messageType) { + defer close(c) + connection, _, err := websocket.DefaultDialer.Dial(url, nil) + if err != nil { + log.Printf("Dial error: %s", err.Error()) + } + defer connection.Close() + + for { + var result messageType + err := connection.ReadJSON(&result) + if err != nil { + log.Printf("Connection error: %s", err.Error()) + return + } + + log.Printf("Message: %s %s\n", result.Title, result.Message) + c <- result + } +} + +func sendNotification(dbusConn *dbus.Conn, message messageType, hostname, token string) error { + + image := getAppImage(hostname, token, message.AppID) + + // https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#urgency-levels + var urgencyByte int + + if message.Priority <= 3 { + urgencyByte = 0 + } else if message.Priority <= 7 { + urgencyByte = 1 + } else { + urgencyByte = 2 + } + + obj := dbusConn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications") + call := obj.Call("org.freedesktop.Notifications.Notify", 0, "gotify", uint32(0), "", message.Title, message.Message, []string{}, + map[string]dbus.Variant{"urgency": dbus.MakeVariant(byte(urgencyByte)), "image-path": dbus.MakeVariant(image)}, int32(-1)) + + return call.Err +} + +func getAppImage(hostname, token string, appID int) string { + + cacheDir := getImageDir() + image := path.Join(cacheDir, fmt.Sprint(appID)) + + // Check if you need to download the image + _, err := os.Stat(image) + if os.IsNotExist(err) { + // File does not exist + err := downloadAppImage(hostname, token, cacheDir, appID) + if err != nil { + return "" + } + } + + return image +} + +func getImageDir() string { + cacheDir := os.Getenv("XDG_CACHE_HOME") + + if cacheDir == "" { + cacheDir = path.Join(os.Getenv("HOME"), ".cache/gotify-desktop") + } else { + cacheDir = path.Join(cacheDir, "gotify-desktop") + } + + return cacheDir +} + +func downloadAppImage(hostname string, token string, cacheDir string, appID int) error { + u, err := getAppImageURL(hostname, token, appID) + + if err != nil { + return err + } + + os.MkdirAll(cacheDir, 0755) // rwx r-x r-x + + resp, err := http.Get(u) + if err != nil { + return err + } + defer resp.Body.Close() + + out, err := os.Create(path.Join(cacheDir, fmt.Sprint(appID))) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err + +} + +func getAppImageURL(hostname string, token string, appID int) (string, error) { + u := url.URL{Scheme: "https", Host: hostname, Path: "/application"} + param := url.Values{} + param.Add("token", token) + + resp, err := http.Get(u.String() + "?" + param.Encode()) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + var result []applicationType + json.Unmarshal(body, &result) + + for i := 0; i < len(result); i++ { + if result[i].ID == appID { + u := url.URL{Scheme: "https", Host: hostname, Path: result[i].Image} + + return u.String() + "?" + param.Encode(), nil + } + } + + return "", errors.New("") +} + +func parseArgs() (string, string) { + + if len(os.Args) < 2 { + fmt.Println("Usage: gotify-dektop ") + os.Exit(1) + } + + return os.Args[1], os.Args[2] +} + +func main() { + + hostname, token := parseArgs() + + u := url.URL{Scheme: "wss", Host: hostname, Path: "/stream"} + + param := url.Values{} + param.Add("token", token) + + dbusConn, err := dbus.ConnectSessionBus() + if err != nil { + log.Printf("Dbus error: %s", err.Error()) + os.Exit(1) + } + + for { + push := make(chan messageType) + go listenToPushEvents(u.String()+"?"+param.Encode(), push) + + for pushMessage := range push { + go sendNotification(dbusConn, pushMessage, hostname, token) + } + } +}