168 lines
4.0 KiB
Go
168 lines
4.0 KiB
Go
package chat
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Store handles persistence for chat channels and message logs.
|
|
type Store struct {
|
|
dataDir string
|
|
channels map[string]*Channel
|
|
history map[string][]*Message // keyed by channel name
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewStore initializes channel and message storage.
|
|
func NewStore(dataDir string) (*Store, error) {
|
|
chDir := filepath.Join(dataDir, "chat_history")
|
|
if err := os.MkdirAll(chDir, 0700); err != nil {
|
|
return nil, fmt.Errorf("failed to create chat directory: %w", err)
|
|
}
|
|
|
|
s := &Store{
|
|
dataDir: chDir,
|
|
channels: make(map[string]*Channel),
|
|
history: make(map[string][]*Message),
|
|
}
|
|
|
|
// Setup default channels
|
|
s.channels["general"] = &Channel{Name: "general", Topic: "General team discussion and announcements", CreatedBy: "system"}
|
|
s.channels["dev"] = &Channel{Name: "dev", Topic: "Engineering, git commits, and tech topics", CreatedBy: "system"}
|
|
s.channels["random"] = &Channel{Name: "random", Topic: "Casual coffee break and memes", CreatedBy: "system"}
|
|
|
|
_ = s.loadAll()
|
|
return s, nil
|
|
}
|
|
|
|
func (s *Store) channelFile(name string) string {
|
|
clean := strings.ToLower(strings.TrimSpace(name))
|
|
clean = strings.ReplaceAll(clean, "/", "_")
|
|
return filepath.Join(s.dataDir, clean+".json")
|
|
}
|
|
|
|
func (s *Store) loadAll() error {
|
|
entries, err := os.ReadDir(s.dataDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, e := range entries {
|
|
if !e.IsDir() && strings.HasSuffix(e.Name(), ".json") {
|
|
chName := strings.TrimSuffix(e.Name(), ".json")
|
|
data, err := os.ReadFile(filepath.Join(s.dataDir, e.Name()))
|
|
if err == nil {
|
|
var msgs []*Message
|
|
if err := json.Unmarshal(data, &msgs); err == nil {
|
|
s.history[chName] = msgs
|
|
if _, exists := s.channels[chName]; !exists {
|
|
s.channels[chName] = &Channel{Name: chName, Topic: "Discussions in #" + chName, CreatedBy: "user"}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) saveChannel(name string) error {
|
|
msgs := s.history[name]
|
|
data, err := json.MarshalIndent(msgs, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cFile := s.channelFile(name)
|
|
tmp := cFile + ".tmp"
|
|
if err := os.WriteFile(tmp, data, 0600); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp, cFile)
|
|
}
|
|
|
|
// SaveMessage appends a message to the channel history.
|
|
func (s *Store) SaveMessage(msg *Message) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
ch := strings.ToLower(msg.Channel)
|
|
if msg.ID == "" {
|
|
msg.ID = fmt.Sprintf("msg_%d", time.Now().UnixNano())
|
|
}
|
|
if msg.Timestamp.IsZero() {
|
|
msg.Timestamp = time.Now().UTC()
|
|
}
|
|
|
|
s.history[ch] = append(s.history[ch], msg)
|
|
if _, exists := s.channels[ch]; !exists {
|
|
s.channels[ch] = &Channel{Name: ch, Topic: "Discussions in #" + ch, CreatedBy: msg.Sender}
|
|
}
|
|
|
|
return s.saveChannel(ch)
|
|
}
|
|
|
|
// GetMessages retrieves recent messages for a channel.
|
|
func (s *Store) GetMessages(channel string, limit int) []*Message {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
ch := strings.ToLower(channel)
|
|
msgs := s.history[ch]
|
|
if len(msgs) == 0 {
|
|
return []*Message{}
|
|
}
|
|
|
|
if limit <= 0 || limit > len(msgs) {
|
|
limit = len(msgs)
|
|
}
|
|
|
|
start := len(msgs) - limit
|
|
result := make([]*Message, limit)
|
|
for i := 0; i < limit; i++ {
|
|
copyM := *msgs[start+i]
|
|
result[i] = ©M
|
|
}
|
|
return result
|
|
}
|
|
|
|
// GetChannels returns list of all channels.
|
|
func (s *Store) GetChannels() []*Channel {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
var list []*Channel
|
|
for _, c := range s.channels {
|
|
copyC := *c
|
|
list = append(list, ©C)
|
|
}
|
|
return list
|
|
}
|
|
|
|
// CreateChannel creates a new channel if it doesn't already exist.
|
|
func (s *Store) CreateChannel(name, topic, createdBy string) (*Channel, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
clean := strings.ToLower(strings.TrimSpace(name))
|
|
clean = strings.TrimPrefix(clean, "#")
|
|
if clean == "" {
|
|
return nil, fmt.Errorf("channel name cannot be empty")
|
|
}
|
|
|
|
if ch, exists := s.channels[clean]; exists {
|
|
return ch, nil
|
|
}
|
|
|
|
ch := &Channel{
|
|
Name: clean,
|
|
Topic: topic,
|
|
CreatedBy: createdBy,
|
|
}
|
|
s.channels[clean] = ch
|
|
_ = s.saveChannel(clean)
|
|
return ch, nil
|
|
}
|