aboutsummaryrefslogtreecommitdiff
path: root/srv
diff options
context:
space:
mode:
Diffstat (limited to 'srv')
-rw-r--r--srv/corvidServer.go39
-rw-r--r--srv/notifServer.go195
-rw-r--r--srv/notification.go70
-rw-r--r--srv/server.go84
4 files changed, 388 insertions, 0 deletions
diff --git a/srv/corvidServer.go b/srv/corvidServer.go
new file mode 100644
index 0000000..4a0556d
--- /dev/null
+++ b/srv/corvidServer.go
@@ -0,0 +1,39 @@
+package srv
+
+import (
+ "log"
+
+ "github.com/godbus/dbus/v5"
+)
+
+type corvidServer server
+
+func (s corvidServer) Test(param uint32) (e *dbus.Error) {
+ log.Printf("Test called %d", param)
+ return nil
+}
+
+// func (s corvidServer) GetCapabilities() (e *dbus.Error) {
+// log.Print("GetCapabilities called")
+// return nil
+// }
+
+// func (s corvidServer) GetServerInformation() (name, vendor, version, specVersion string, e *dbus.Error) {
+// // log.Print("GetServerInformation called")
+// return "corvid", "CartConnoisseur", "0.1.0", "1.2", nil
+// }
+
+// func (s corvidServer) CloseNotification(id uint32) (e *dbus.Error) {
+// // log.Printf("CloseNotification called: %d", id)
+// notification, ok := notifications.notifications[id]
+// if ok {
+// notification.close(CloseReasonClosed)
+// }
+
+// return nil
+// }
+
+// func (s corvidServer) Notify(appName string, replacesId uint32, appIcon string, summary string, body string, actions []string, hints map[string]dbus.Variant, expireTimeout int32) (id uint32, e *dbus.Error) {
+// // log.Print("Notify called")
+
+// }
diff --git a/srv/notifServer.go b/srv/notifServer.go
new file mode 100644
index 0000000..00038cc
--- /dev/null
+++ b/srv/notifServer.go
@@ -0,0 +1,195 @@
+package srv
+
+import (
+ "encoding/json"
+ "fmt"
+ "image"
+ "image/png"
+ "log"
+ "os"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/godbus/dbus/v5"
+)
+
+type notifServer struct {
+ notifications *notificationStack
+ server
+}
+
+func (s notifServer) GetCapabilities() (capabilities []string, e *dbus.Error) {
+ // log.Print("GetCapabilities called")
+ return []string{
+ "body",
+ "actions",
+ }, nil
+}
+
+func (s notifServer) GetServerInformation() (name, vendor, version, specVersion string, e *dbus.Error) {
+ // log.Print("GetServerInformation called")
+ return "corvid", "CartConnoisseur", "0.1.0", "1.2", nil
+}
+
+func (s notifServer) CloseNotification(id uint32) (e *dbus.Error) {
+ // log.Printf("CloseNotification called: %d", id)
+ s.close(id, CloseReasonClosed)
+ return nil
+}
+
+func (s notifServer) Notify(appName string, replacesId uint32, appIcon string, summary string, body string, actions []string, hints map[string]dbus.Variant, expireTimeout int32) (id uint32, e *dbus.Error) {
+ // log.Print("Notify called")
+ s.notifications.mutex.Lock()
+ defer s.notifications.mutex.Unlock()
+
+ if replacesId == 0 {
+ id = s.notifications.nextId
+ s.notifications.nextId++
+ } else {
+ id = replacesId
+ }
+
+ actionMap := make(map[string]string)
+ for i := 0; i < len(actions)-1; i += 2 {
+ actionMap[actions[i]] = actions[i+1]
+ }
+
+ hintMap := make(map[string]hint)
+ img := ""
+
+ for key, value := range hints {
+ if !value.Signature().Empty() {
+ if strings.Contains("ybnqiuxtds", string(value.Signature().String()[0])) {
+ hintMap[key] = hint{Variant: value}
+ } else if key == "image-data" {
+ raw := value.Value().([]interface{})
+
+ var i image.Image
+ if raw[3].(bool) {
+ i = &image.NRGBA{
+ Pix: raw[6].([]uint8),
+ Stride: int(raw[2].(int32)),
+ Rect: image.Rect(0, 0, int(raw[0].(int32)), int(raw[1].(int32))),
+ }
+ } else {
+ rgb := raw[6].([]uint8)
+ rgba := make([]uint8, len(rgb)/3*4)
+
+ for i := 0; i < len(rgb)-1; i += 3 {
+ rgba[i/3*4] = rgb[i]
+ rgba[i/3*4+1] = rgb[i+1]
+ rgba[i/3*4+2] = rgb[i+2]
+ rgba[i/3*4+3] = 0xff
+ }
+
+ i = &image.NRGBA{
+ Pix: rgba,
+ Stride: int(raw[2].(int32)),
+ Rect: image.Rect(0, 0, int(raw[0].(int32)), int(raw[1].(int32))),
+ }
+ }
+ _ = i
+
+ f, err := os.CreateTemp(os.TempDir(), "corvid-*.png")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer f.Close()
+
+ png.Encode(f, i)
+
+ img = f.Name()
+ }
+ }
+ }
+
+ if expireTimeout == -1 {
+ expireTimeout = DEFAULT_EXPIRATION
+ }
+
+ notification := notification{
+ Id: id,
+ AppName: appName,
+ AppIcon: appIcon,
+ Summary: summary,
+ Body: body,
+ Actions: actionMap,
+ Hints: hintMap,
+ Timestamp: time.Now().Unix(),
+ Expiration: expireTimeout,
+ Image: img,
+ timer: nil,
+ }
+
+ if expireTimeout != 0 {
+ notification.timer = time.AfterFunc(time.Duration(expireTimeout)*time.Millisecond, func() {
+ s.close(notification.Id, CloseReasonExpire)
+ })
+ }
+
+ s.notifications.notifications[id] = notification
+ s.output()
+
+ return id, nil
+}
+
+func (s notifServer) close(id uint32, reason closeReason) {
+ s.notifications.mutex.Lock()
+ defer s.notifications.mutex.Unlock()
+
+ n, ok := s.notifications.notifications[id]
+ if !ok {
+ return
+ }
+
+ if n.timer != nil {
+ n.timer.Stop()
+ }
+
+ if n.Image != "" {
+ os.Remove(n.Image)
+ }
+
+ delete(s.notifications.notifications, n.Id)
+ s.output()
+
+ err := s.conn.Emit(s.object, s.name+".NotificationClosed", n.Id, reason)
+ if err != nil {
+ log.Print(err)
+ }
+}
+
+// TODO: relocate to cmd/corvid
+func (s notifServer) output() {
+ arr := make([]notification, len(s.notifications.notifications))
+
+ i := 0
+ for _, notification := range s.notifications.notifications {
+ arr[i] = notification
+ i++
+ }
+
+ slices.SortFunc(arr, func(a, b notification) int {
+ if a.Timestamp > b.Timestamp {
+ return SORT_DIRECTION
+ } else if a.Timestamp < b.Timestamp {
+ return -SORT_DIRECTION
+ } else {
+ if a.Id > b.Id {
+ return SORT_DIRECTION
+ } else if a.Id < b.Id {
+ return -SORT_DIRECTION
+ }
+ }
+
+ return 0
+ })
+
+ j, err := json.Marshal(arr)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ fmt.Println(string(j))
+}
diff --git a/srv/notification.go b/srv/notification.go
new file mode 100644
index 0000000..dcea9d9
--- /dev/null
+++ b/srv/notification.go
@@ -0,0 +1,70 @@
+package srv
+
+import (
+ "encoding/json"
+ "sync"
+ "time"
+
+ "github.com/godbus/dbus/v5"
+)
+
+type hint struct {
+ dbus.Variant
+}
+
+func (h hint) MarshalJSON() ([]byte, error) {
+ //TODO: find a better way lol
+ switch h.Signature().String()[0] {
+ case 'y':
+ return json.Marshal(h.Value().(uint8))
+ case 'b':
+ return json.Marshal(h.Value().(bool))
+ case 'n':
+ return json.Marshal(h.Value().(int16))
+ case 'q':
+ return json.Marshal(h.Value().(uint16))
+ case 'i':
+ return json.Marshal(h.Value().(int32))
+ case 'u':
+ return json.Marshal(h.Value().(uint32))
+ case 'x':
+ return json.Marshal(h.Value().(int64))
+ case 't':
+ return json.Marshal(h.Value().(uint64))
+ case 'd':
+ return json.Marshal(h.Value().(float64))
+ case 's':
+ return json.Marshal(h.Value().(string))
+ default:
+ panic("Impossible type")
+ }
+}
+
+type closeReason uint32
+
+const (
+ CloseReasonExpire closeReason = 1
+ CloseReasonDismissed = iota
+ CloseReasonClosed = iota
+ CloseReasonOther = iota
+)
+
+type notification struct {
+ Id uint32 `json:"id"`
+ AppName string `json:"app_name"`
+ AppIcon string `json:"app_icon"`
+ Summary string `json:"summary"`
+ Body string `json:"body"`
+ Actions map[string]string `json:"actions"`
+ Hints map[string]hint `json:"hints"`
+ Timestamp int64 `json:"timestamp"`
+ Expiration int32 `json:"expiration"`
+ Image string `json:"image"`
+ timer *time.Timer
+}
+
+type notificationStack = struct {
+ mutex *sync.Mutex
+ notifications map[uint32]notification
+ nextId uint32
+}
diff --git a/srv/server.go b/srv/server.go
new file mode 100644
index 0000000..673081b
--- /dev/null
+++ b/srv/server.go
@@ -0,0 +1,84 @@
+package srv
+
+import (
+ "fmt"
+ "log"
+ "sync"
+
+ "github.com/godbus/dbus/v5"
+)
+
+const DEFAULT_EXPIRATION = 5000
+const SORT_DIRECTION = 1 // 1 = newest first, -1 = oldest first
+
+type server = struct {
+ conn *dbus.Conn
+ object dbus.ObjectPath
+ name string
+}
+
+func Start() {
+ const NOTIF_DBUS_OBJECT = "/org/freedesktop/Notifications"
+ const NOTIF_DBUS_NAME = "org.freedesktop.Notifications"
+ const CORVID_DBUS_OBJECT = "/sh/cxl/Corvid"
+ const CORVID_DBUS_NAME = "sh.cxl.Corvid"
+
+ notifications := notificationStack{
+ mutex: &sync.Mutex{},
+ notifications: make(map[uint32]notification),
+ nextId: 1,
+ }
+
+ conn, err := dbus.SessionBus()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ err = startDBusServer(
+ conn,
+ corvidServer{
+ conn: conn,
+ object: CORVID_DBUS_OBJECT,
+ name: CORVID_DBUS_NAME,
+ },
+ CORVID_DBUS_OBJECT,
+ CORVID_DBUS_NAME,
+ )
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ err = startDBusServer(
+ conn,
+ notifServer{
+ notifications: &notifications,
+ server: server{
+ conn: conn,
+ object: NOTIF_DBUS_OBJECT,
+ name: NOTIF_DBUS_NAME,
+ },
+ },
+ NOTIF_DBUS_OBJECT,
+ NOTIF_DBUS_NAME,
+ )
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func startDBusServer(conn *dbus.Conn, v interface{}, object dbus.ObjectPath, name string) error {
+ conn.Export(v, object, name)
+
+ reply, err := conn.RequestName(name, dbus.NameFlagReplaceExisting|dbus.NameFlagDoNotQueue)
+ if err != nil {
+ return err
+ }
+
+ if reply != dbus.RequestNameReplyPrimaryOwner {
+ return fmt.Errorf("'%s' already taken", name)
+ }
+
+ log.Printf("connected to dbus as %s @ %s", name, object)
+
+ return nil
+}