diff --git a/cmd/gh-not/main.go b/cmd/gh-not/main.go index 0f7325b..db3d8da 100644 --- a/cmd/gh-not/main.go +++ b/cmd/gh-not/main.go @@ -2,15 +2,12 @@ package main import ( "fmt" - "log" "time" "github.com/nobe4/gh-not/internal/actors" "github.com/nobe4/gh-not/internal/cache" "github.com/nobe4/gh-not/internal/config" "github.com/nobe4/gh-not/internal/gh" - "github.com/nobe4/gh-not/internal/jq" - "github.com/nobe4/gh-not/internal/notifications" ) const CacheTTL = time.Hour * 4 @@ -28,37 +25,27 @@ func main() { panic(err) } - fmt.Printf("all notifications %v\n", allNotifications) - - filteredNotifications := []notifications.Notification{} + fmt.Println(allNotifications.ToString()) actorsMap := map[string]actors.Actor{ "debug": &actors.DebugActor{}, "print": &actors.PrintActor{}, + "hide": &actors.HideActor{}, } - config, err := config.Load("config.json") + config, err := config.New("config.json") if err != nil { panic(err) } - for _, group := range config.Groups { - for _, filter := range group.Filters { - selectedNotifications, err := jq.Filter(filter, allNotifications) - if err != nil { - panic(err) - } + allNotifications, err = config.Apply(allNotifications, actorsMap) + if err != nil { + panic(err) + } - filteredNotifications = append(filteredNotifications, selectedNotifications...) - } - filteredNotifications = notifications.Uniq(filteredNotifications) + fmt.Println(allNotifications.ToString()) - for _, notification := range filteredNotifications { - if actor, ok := actorsMap[group.Action]; ok == true { - actor.Run(notification) - } else { - log.Fatalf("unknown action '%s'", group.Action) - } - } + if err := cache.Write(allNotifications); err != nil { + panic(err) } } diff --git a/internal/actors/actors.go b/internal/actors/actors.go index fcd51b1..b96ea8c 100644 --- a/internal/actors/actors.go +++ b/internal/actors/actors.go @@ -7,19 +7,31 @@ import ( ) type Actor interface { - Run(notifications.Notification) error + Run(notifications.Notification) (notifications.Notification, error) } type DebugActor struct{} -func (_ *DebugActor) Run(n notifications.Notification) error { - fmt.Printf("DEBUG Run %#v\n", n) - return nil +func (_ *DebugActor) Run(n notifications.Notification) (notifications.Notification, error) { + fmt.Printf("DEBUG Run %s\n", n.ToString()) + return n, nil } type PrintActor struct{} -func (_ *PrintActor) Run(n notifications.Notification) error { - fmt.Printf("%#v\n", n) - return nil +func (_ *PrintActor) Run(n notifications.Notification) (notifications.Notification, error) { + if n.Meta.Hidden { + return n, nil + } + + fmt.Println(n.ToString()) + return n, nil +} + +type HideActor struct{} + +func (_ *HideActor) Run(n notifications.Notification) (notifications.Notification, error) { + // fmt.Printf("HIDDING %s\n", n.ToString()) + n.Meta.Hidden = !n.Meta.Hidden + return n, nil } diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 663f297..4810ffd 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -1,14 +1,17 @@ package cache import ( + "encoding/json" "errors" "os" "time" + + "github.com/nobe4/gh-not/internal/notifications" ) type ExpiringReadWriter interface { - Read() ([]byte, error) - Write([]byte) error + Read() (notifications.NotificationMap, error) + Write(notifications.NotificationMap) error Expired() (bool, error) } @@ -24,17 +27,31 @@ func NewFileCache(ttl time.Duration, path string) *FileCache { } } -func (c *FileCache) Read() ([]byte, error) { +func (c *FileCache) Read() (notifications.NotificationMap, error) { content, err := os.ReadFile(c.path) if err != nil { if errors.Is(err, os.ErrNotExist) { - return []byte{}, nil + return nil, nil } return nil, err } - return content, nil + notifications := notifications.NotificationMap{} + if err := json.Unmarshal(content, ¬ifications); err != nil { + return nil, err + } + + return notifications, nil +} + +func (c *FileCache) Write(n notifications.NotificationMap) error { + marshalled, err := json.Marshal(n) + if err != nil { + return err + } + + return os.WriteFile(c.path, marshalled, 0644) } func (c *FileCache) Expired() (bool, error) { @@ -52,7 +69,3 @@ func (c *FileCache) Expired() (bool, error) { return expired, nil } - -func (c *FileCache) Write(content []byte) error { - return os.WriteFile(c.path, content, 0644) -} diff --git a/internal/config/config.go b/internal/config/config.go index 8132d69..a7b9a96 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,7 +2,12 @@ package config import ( "encoding/json" + "log" "os" + + "github.com/nobe4/gh-not/internal/actors" + "github.com/nobe4/gh-not/internal/jq" + "github.com/nobe4/gh-not/internal/notifications" ) type Config struct { @@ -15,17 +20,47 @@ type Group struct { Action string `json:"action"` } -func Load(path string) (Config, error) { - var config Config +func New(path string) (*Config, error) { + config := &Config{} content, err := os.ReadFile(path) if err != nil { - return config, err + return nil, err } - if err := json.Unmarshal(content, &config); err != nil { - return config, err + if err := json.Unmarshal(content, config); err != nil { + return nil, err } return config, nil } + +func (c *Config) Apply(n notifications.NotificationMap, actors map[string]actors.Actor) (notifications.NotificationMap, error) { + err := error(nil) + for _, group := range c.Groups { + notificationList := n.ToSlice() + selectedNotifications := notifications.Notifications{} + + for _, filter := range group.Filters { + selectedNotifications, err = jq.Filter(filter, notificationList) + if err != nil { + return nil, err + } + } + + for _, notification := range selectedNotifications { + if actor, ok := actors[group.Action]; ok == true { + notification, err = actor.Run(notification) + if err != nil { + log.Fatalf("action '%s' failed: %v", group.Action, err) + } + + n[notification.Id] = notification + } else { + log.Fatalf("unknown action '%s'", group.Action) + } + } + } + + return n, nil +} diff --git a/internal/gh/gh.go b/internal/gh/gh.go index 4679f5c..26fc862 100644 --- a/internal/gh/gh.go +++ b/internal/gh/gh.go @@ -1,7 +1,6 @@ package gh import ( - "encoding/json" "fmt" "github.com/cli/go-gh/v2/pkg/api" @@ -26,42 +25,28 @@ func NewClient(cache cache.ExpiringReadWriter) (*Client, error) { }, err } -func (c *Client) loadCache() ([]notifications.Notification, bool, error) { +func (c *Client) loadCache() (notifications.NotificationMap, bool, error) { expired, err := c.cache.Expired() if err != nil { return nil, false, err } - content, err := c.cache.Read() + n, err := c.cache.Read() if err != nil { return nil, expired, err } - notifications := []notifications.Notification{} - if err := json.Unmarshal(content, ¬ifications); err != nil { - return nil, expired, err - } - - return notifications, expired, nil -} - -func (c *Client) writeCache(n []notifications.Notification) error { - marshalled, err := json.Marshal(n) - if err != nil { - return err - } - - return c.cache.Write(marshalled) + return n, expired, nil } -func (c *Client) Notifications() ([]notifications.Notification, error) { - allNotifications := []notifications.Notification{} +func (c *Client) Notifications() (notifications.NotificationMap, error) { + allNotifications := make(notifications.NotificationMap) cachedNotifications, expired, err := c.loadCache() if err != nil { fmt.Printf("Error while reading the cache: %#v\n", err) - } else { - allNotifications = append(allNotifications, cachedNotifications...) + } else if cachedNotifications != nil { + allNotifications = cachedNotifications } if expired { @@ -71,13 +56,14 @@ func (c *Client) Notifications() ([]notifications.Notification, error) { return nil, err } - allNotifications = append(allNotifications, pulledNotifications...) + allNotifications.Append(pulledNotifications) - allNotifications = notifications.Uniq(allNotifications) + // This will favor the cached notifications as they are first into the + // slice. + // allNotifications = allNotifications.Uniq() - if err := c.writeCache(allNotifications); err != nil { + if err := c.cache.Write(allNotifications); err != nil { fmt.Printf("Error while writing the cache: %#v", err) - cachedNotifications = []notifications.Notification{} } } diff --git a/internal/jq/jq.go b/internal/jq/jq.go index 4fc0e4d..a42556d 100644 --- a/internal/jq/jq.go +++ b/internal/jq/jq.go @@ -1,7 +1,6 @@ package jq import ( - "encoding/json" "fmt" "github.com/itchyny/gojq" @@ -9,13 +8,20 @@ import ( ) // Filter applies a `.[] | select(filter)` on the notifications. -func Filter(filter string, n []notifications.Notification) ([]notifications.Notification, error) { +func Filter(filter string, n notifications.Notifications) (notifications.Notifications, error) { + if filter == "" { + return n, nil + } + query, err := gojq.Parse(fmt.Sprintf(".[] | select(%s)", filter)) if err != nil { panic(err) } - notificationsRaw, err := notificationsToInterface(n) + // gojq works only on any data, so we need to convert Notifications to + // interface{}. + // This also gives us back the JSON fields from the API. + notificationsRaw, err := n.ToInterface() if err != nil { return nil, err } @@ -37,41 +43,10 @@ func Filter(filter string, n []notifications.Notification) ([]notifications.Noti fitleredNotificationsRaw = append(fitleredNotificationsRaw, v) } - filteredNotifications, err := interfaceToNotifications(fitleredNotificationsRaw) + filteredNotifications, err := notifications.FromInterface(fitleredNotificationsRaw) if err != nil { return nil, err } return filteredNotifications, nil } - -func notificationsToInterface(n []notifications.Notification) (interface{}, error) { - // gojq works only on any data, so we need to convert []Notifications to - // interface{} - // This also gives us back the JSON fields from the API. - marshalled, err := json.Marshal(n) - if err != nil { - return nil, fmt.Errorf("cannot marshal notifications: %w", err) - } - - var i interface{} - if err := json.Unmarshal(marshalled, &i); err != nil { - return nil, fmt.Errorf("cannot unmarshal interface: %w", err) - } - - return i, nil -} - -func interfaceToNotifications(n interface{}) ([]notifications.Notification, error) { - marshalled, err := json.Marshal(n) - if err != nil { - return nil, fmt.Errorf("cannot marshall interface: %w", err) - } - - var notifications []notifications.Notification - if err := json.Unmarshal(marshalled, ¬ifications); err != nil { - return nil, fmt.Errorf("cannot unmarshall into notification: %w", err) - } - - return notifications, nil -} diff --git a/internal/notifications/notifications.go b/internal/notifications/notifications.go index d504797..491b312 100644 --- a/internal/notifications/notifications.go +++ b/internal/notifications/notifications.go @@ -1,10 +1,27 @@ package notifications +import ( + "encoding/json" + "fmt" +) + +type NotificationMap map[string]Notification + +type Notifications []Notification + type Notification struct { + // API fields Title string `json:"title"` Id string `json:"id"` Unread bool `json:"unread"` Repository Repository `json:"repository"` + + // gh-not fields + Meta Meta `json:"meta"` +} + +type Meta struct { + Hidden bool `json:"hidden"` } type Repository struct { @@ -21,16 +38,72 @@ type Owner struct { Type string `json:"type"` } -func Uniq(notificationsToFilter []Notification) []Notification { - seen := make(map[string]bool) - unique := []Notification{} +func (n NotificationMap) Append(notifications []Notification) { + for _, notification := range notifications { + if _, ok := n[notification.Id]; !ok { + n[notification.Id] = notification + } + } +} + +func (n NotificationMap) Uniq() NotificationMap { + unique := NotificationMap{} - for _, notification := range notificationsToFilter { - if _, ok := seen[notification.Id]; !ok { - seen[notification.Id] = true - unique = append(unique, notification) + for _, notification := range n { + if _, ok := unique[notification.Id]; !ok { + unique[notification.Id] = notification } } return unique } + +func (n Notification) ToString() string { + return fmt.Sprintf("%s %+v", n.Id, n) +} + +func (n NotificationMap) ToString() string { + out := "" + for _, n := range n { + out += n.ToString() + "\n" + } + return out +} + +func (n NotificationMap) ToSlice() Notifications { + s := Notifications{} + + for _, n := range n { + s = append(s, n) + } + + return s +} + +func (n Notifications) ToInterface() (interface{}, error) { + marshalled, err := json.Marshal(n) + if err != nil { + return nil, fmt.Errorf("cannot marshal notifications: %w", err) + } + + var i interface{} + if err := json.Unmarshal(marshalled, &i); err != nil { + return nil, fmt.Errorf("cannot unmarshal interface: %w", err) + } + + return i, nil +} + +func FromInterface(i interface{}) (Notifications, error) { + marshalled, err := json.Marshal(i) + if err != nil { + return nil, fmt.Errorf("cannot marshall interface: %w", err) + } + + notifications := Notifications{} + if err := json.Unmarshal(marshalled, ¬ifications); err != nil { + return nil, fmt.Errorf("cannot unmarshall into notification: %w", err) + } + + return notifications, nil +}