145 lines
2.8 KiB
Go
145 lines
2.8 KiB
Go
// 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)
|
||
|
|
}
|