261 lines
5.4 KiB
Go
261 lines
5.4 KiB
Go
package watchlist
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Entry struct {
|
|
Callsign string `json:"callsign"`
|
|
LastSeen time.Time `json:"lastSeen"`
|
|
LastSeenStr string `json:"lastSeenStr"`
|
|
AddedAt time.Time `json:"addedAt"`
|
|
SpotCount int `json:"spotCount"`
|
|
IsContest bool `json:"isContest"`
|
|
Notify bool `json:"notify"`
|
|
|
|
// ActiveSpotIDs n'est pas sérialisé — reconstruit en mémoire
|
|
activeSpotIDs map[int64]bool
|
|
}
|
|
|
|
type Watchlist struct {
|
|
entries map[string]*Entry
|
|
filePath string
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func New(filePath string) *Watchlist {
|
|
w := &Watchlist{
|
|
entries: make(map[string]*Entry),
|
|
filePath: filePath,
|
|
}
|
|
w.load()
|
|
return w
|
|
}
|
|
|
|
func (w *Watchlist) load() {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
data, err := os.ReadFile(w.filePath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var entries []Entry
|
|
if err := json.Unmarshal(data, &entries); err != nil {
|
|
return
|
|
}
|
|
|
|
for i := range entries {
|
|
e := &entries[i]
|
|
e.activeSpotIDs = make(map[int64]bool)
|
|
if e.LastSeen.IsZero() {
|
|
e.LastSeenStr = "Never"
|
|
} else {
|
|
e.LastSeenStr = FormatLastSeen(e.LastSeen)
|
|
}
|
|
w.entries[e.Callsign] = e
|
|
}
|
|
}
|
|
|
|
func (w *Watchlist) save() error {
|
|
entries := make([]Entry, 0, len(w.entries))
|
|
for _, e := range w.entries {
|
|
entries = append(entries, *e)
|
|
}
|
|
data, err := json.MarshalIndent(entries, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(w.filePath, data, 0644)
|
|
}
|
|
|
|
func (w *Watchlist) Add(callsign string) error {
|
|
return w.add(callsign, false)
|
|
}
|
|
|
|
func (w *Watchlist) AddContest(callsign string) error {
|
|
return w.add(callsign, true)
|
|
}
|
|
|
|
func (w *Watchlist) add(callsign string, isContest bool) error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
callsign = strings.ToUpper(strings.TrimSpace(callsign))
|
|
if callsign == "" {
|
|
return fmt.Errorf("callsign cannot be empty")
|
|
}
|
|
if _, exists := w.entries[callsign]; exists {
|
|
return fmt.Errorf("callsign already in watchlist")
|
|
}
|
|
|
|
w.entries[callsign] = &Entry{
|
|
Callsign: callsign,
|
|
AddedAt: time.Now(),
|
|
LastSeenStr: "Never",
|
|
activeSpotIDs: make(map[int64]bool),
|
|
IsContest: isContest,
|
|
}
|
|
|
|
return w.save()
|
|
}
|
|
|
|
func (w *Watchlist) Remove(callsign string) error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
callsign = strings.ToUpper(strings.TrimSpace(callsign))
|
|
if _, exists := w.entries[callsign]; !exists {
|
|
return fmt.Errorf("callsign not in watchlist")
|
|
}
|
|
delete(w.entries, callsign)
|
|
return w.save()
|
|
}
|
|
|
|
func (w *Watchlist) SetNotify(callsign string, notify bool) error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
callsign = strings.ToUpper(strings.TrimSpace(callsign))
|
|
e, exists := w.entries[callsign]
|
|
if !exists {
|
|
return fmt.Errorf("callsign not found")
|
|
}
|
|
e.Notify = notify
|
|
return w.save()
|
|
}
|
|
|
|
func (w *Watchlist) Matches(callsign string) bool {
|
|
w.mu.RLock()
|
|
defer w.mu.RUnlock()
|
|
_, ok := w.entries[strings.ToUpper(callsign)]
|
|
return ok
|
|
}
|
|
|
|
func (w *Watchlist) GetEntry(callsign string) *Entry {
|
|
w.mu.RLock()
|
|
defer w.mu.RUnlock()
|
|
e, ok := w.entries[strings.ToUpper(callsign)]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
copy := *e
|
|
return ©
|
|
}
|
|
|
|
func (w *Watchlist) GetAll() []Entry {
|
|
w.mu.RLock()
|
|
defer w.mu.RUnlock()
|
|
entries := make([]Entry, 0, len(w.entries))
|
|
for _, e := range w.entries {
|
|
cp := *e
|
|
if !cp.LastSeen.IsZero() {
|
|
cp.LastSeenStr = FormatLastSeen(cp.LastSeen)
|
|
}
|
|
entries = append(entries, cp)
|
|
}
|
|
return entries
|
|
}
|
|
|
|
func (w *Watchlist) GetAllCallsigns() []string {
|
|
w.mu.RLock()
|
|
defer w.mu.RUnlock()
|
|
callsigns := make([]string, 0, len(w.entries))
|
|
for cs := range w.entries {
|
|
callsigns = append(callsigns, cs)
|
|
}
|
|
return callsigns
|
|
}
|
|
|
|
func (w *Watchlist) MarkSeen(callsign string) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
e, ok := w.entries[strings.ToUpper(callsign)]
|
|
if !ok {
|
|
return
|
|
}
|
|
e.LastSeen = time.Now()
|
|
e.LastSeenStr = FormatLastSeen(e.LastSeen)
|
|
e.SpotCount++
|
|
}
|
|
|
|
func (w *Watchlist) AddActiveSpot(callsign string, spotID int64) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
e, ok := w.entries[strings.ToUpper(callsign)]
|
|
if !ok {
|
|
return
|
|
}
|
|
if e.activeSpotIDs == nil {
|
|
e.activeSpotIDs = make(map[int64]bool)
|
|
}
|
|
e.activeSpotIDs[spotID] = true
|
|
}
|
|
|
|
func (w *Watchlist) RemoveActiveSpot(spotID int64) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
for _, e := range w.entries {
|
|
delete(e.activeSpotIDs, spotID)
|
|
}
|
|
}
|
|
|
|
func (w *Watchlist) GetAllWithActiveStatus() []map[string]interface{} {
|
|
w.mu.RLock()
|
|
defer w.mu.RUnlock()
|
|
result := make([]map[string]interface{}, 0, len(w.entries))
|
|
for _, e := range w.entries {
|
|
lastSeenStr := "Never"
|
|
if !e.LastSeen.IsZero() {
|
|
lastSeenStr = FormatLastSeen(e.LastSeen)
|
|
}
|
|
result = append(result, map[string]interface{}{
|
|
"callsign": e.Callsign,
|
|
"lastSeen": e.LastSeen,
|
|
"lastSeenStr": lastSeenStr,
|
|
"addedAt": e.AddedAt,
|
|
"spotCount": e.SpotCount,
|
|
"notify": e.Notify,
|
|
"isContest": e.IsContest,
|
|
"hasActiveSpots": len(e.activeSpotIDs) > 0,
|
|
"activeCount": len(e.activeSpotIDs),
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func FormatLastSeen(t time.Time) string {
|
|
if t.IsZero() {
|
|
return "Never"
|
|
}
|
|
d := time.Since(t)
|
|
switch {
|
|
case d < time.Minute:
|
|
return "Just now"
|
|
case d < time.Hour:
|
|
m := int(d.Minutes())
|
|
if m == 1 {
|
|
return "1 minute ago"
|
|
}
|
|
return fmt.Sprintf("%d minutes ago", m)
|
|
case d < 24*time.Hour:
|
|
h := int(d.Hours())
|
|
if h == 1 {
|
|
return "1 hour ago"
|
|
}
|
|
return fmt.Sprintf("%d hours ago", h)
|
|
default:
|
|
days := int(d.Hours() / 24)
|
|
if days == 1 {
|
|
return "1 day ago"
|
|
}
|
|
return fmt.Sprintf("%d days ago", days)
|
|
}
|
|
}
|