// Package store keeps an in-memory, newest-first view of ntfy messages. package store import ( "strings" "sync" ) // Attachment is a file linked from a message. type Attachment struct { Name string URL string Type string Size int64 } // Message is one delivered ntfy notification. type Message struct { ID string Time int64 Expires int64 Topic string Title string Body string Tags []string Priority int Click string Attachment *Attachment Actions string } // Store is safe for concurrent readers and writers. type Store struct { mu sync.Mutex byTopic map[string][]Message index map[string]string order []string } // New returns an empty store. func New() *Store { return &Store{ byTopic: map[string][]Message{}, index: map[string]string{}, } } // Put inserts a message. A repeated id is ignored. func (s *Store) Put(m Message) { s.mu.Lock() defer s.mu.Unlock() if m.ID != "" { if _, ok := s.index[m.ID]; ok { return } s.index[m.ID] = m.Topic } s.ensure(m.Topic) s.byTopic[m.Topic] = insertNewest(s.byTopic[m.Topic], m) } // Delete removes the message with id. func (s *Store) Delete(id string) { s.mu.Lock() defer s.mu.Unlock() topic, ok := s.index[id] if !ok { return } list := s.byTopic[topic] for i, m := range list { if m.ID == id { s.byTopic[topic] = append(list[:i], list[i+1:]...) break } } delete(s.index, id) } // Clear removes every message for a topic. func (s *Store) Clear(topic string) { s.mu.Lock() defer s.mu.Unlock() for _, m := range s.byTopic[topic] { if m.ID != "" { delete(s.index, m.ID) } } s.byTopic[topic] = nil } // List returns a copy of one topic, newest first. func (s *Store) List(topic string) []Message { s.mu.Lock() defer s.mu.Unlock() src := s.byTopic[topic] out := make([]Message, len(src)) copy(out, src) return out } // Topics returns topic names in the order they were first seen. func (s *Store) Topics() []string { s.mu.Lock() defer s.mu.Unlock() out := make([]string, len(s.order)) copy(out, s.order) return out } func (s *Store) ensure(topic string) { if _, ok := s.byTopic[topic]; ok { return } s.byTopic[topic] = nil s.order = append(s.order, topic) } func insertNewest(list []Message, m Message) []Message { i := 0 for i < len(list) && list[i].Time > m.Time { i++ } list = append(list, Message{}) copy(list[i+1:], list[i:]) list[i] = m return list } // Match reports whether needle is contained in the message's visible fields. func Match(m Message, title, needle string) bool { needle = strings.ToLower(strings.TrimSpace(needle)) if needle == "" { return true } hay := strings.ToLower(strings.Join([]string{ title, m.Title, m.Body, m.ID, strings.Join(m.Tags, " "), }, " ")) return strings.Contains(hay, needle) }