@@ -0,0 +1,187 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
type Room struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
ID int64 `json:"id"`
|
||||
RoomID int64 `json:"room_id"`
|
||||
Username string `json:"username"`
|
||||
Body string `json:"body"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
mu sync.RWMutex
|
||||
path string
|
||||
nextRoomID int64
|
||||
nextMessageID int64
|
||||
rooms []Room
|
||||
messages []Message
|
||||
}
|
||||
|
||||
type diskData struct {
|
||||
NextRoomID int64 `json:"next_room_id"`
|
||||
NextMessageID int64 `json:"next_message_id"`
|
||||
Rooms []Room `json:"rooms"`
|
||||
Messages []Message `json:"messages"`
|
||||
}
|
||||
|
||||
func Open(path string) (*Store, error) {
|
||||
s := &Store{path: path, nextRoomID: 1, nextMessageID: 1}
|
||||
if err := s.load(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(s.rooms) == 0 {
|
||||
now := time.Now().UTC()
|
||||
s.rooms = []Room{
|
||||
{ID: s.nextRoomID, Name: "Lobby", Description: "Allgemeiner Chat für alle", CreatedAt: now},
|
||||
{ID: s.nextRoomID + 1, Name: "Go", Description: "Golang, Backend und Deployment", CreatedAt: now},
|
||||
{ID: s.nextRoomID + 2, Name: "Random", Description: "Alles, was sonst nirgends passt", CreatedAt: now},
|
||||
}
|
||||
s.nextRoomID += 3
|
||||
if err := s.saveLocked(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Store) Close() error { return nil }
|
||||
|
||||
func (s *Store) load() error {
|
||||
b, err := os.ReadFile(s.path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var d diskData
|
||||
if err := json.Unmarshal(b, &d); err != nil {
|
||||
return err
|
||||
}
|
||||
s.nextRoomID = maxInt64(d.NextRoomID, 1)
|
||||
s.nextMessageID = maxInt64(d.NextMessageID, 1)
|
||||
s.rooms = d.Rooms
|
||||
s.messages = d.Messages
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) saveLocked() error {
|
||||
if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil && filepath.Dir(s.path) != "." {
|
||||
return err
|
||||
}
|
||||
d := diskData{NextRoomID: s.nextRoomID, NextMessageID: s.nextMessageID, Rooms: s.rooms, Messages: s.messages}
|
||||
b, err := json.MarshalIndent(d, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := s.path + ".tmp"
|
||||
if err := os.WriteFile(tmp, b, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, s.path)
|
||||
}
|
||||
|
||||
func (s *Store) Rooms() ([]Room, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
rooms := append([]Room(nil), s.rooms...)
|
||||
sort.Slice(rooms, func(i, j int) bool { return rooms[i].Name < rooms[j].Name })
|
||||
return rooms, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateRoom(name, description string) (Room, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for _, r := range s.rooms {
|
||||
if r.Name == name {
|
||||
return Room{}, errors.New("room exists")
|
||||
}
|
||||
}
|
||||
r := Room{ID: s.nextRoomID, Name: name, Description: description, CreatedAt: time.Now().UTC()}
|
||||
s.nextRoomID++
|
||||
s.rooms = append(s.rooms, r)
|
||||
if err := s.saveLocked(); err != nil {
|
||||
return Room{}, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (s *Store) Room(id int64) (Room, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, r := range s.rooms {
|
||||
if r.ID == id {
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
return Room{}, ErrNotFound
|
||||
}
|
||||
|
||||
func (s *Store) RecentMessages(roomID int64, limit int) ([]Message, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var msgs []Message
|
||||
for _, m := range s.messages {
|
||||
if m.RoomID == roomID {
|
||||
msgs = append(msgs, m)
|
||||
}
|
||||
}
|
||||
sort.Slice(msgs, func(i, j int) bool {
|
||||
if msgs[i].CreatedAt.Equal(msgs[j].CreatedAt) {
|
||||
return msgs[i].ID < msgs[j].ID
|
||||
}
|
||||
return msgs[i].CreatedAt.Before(msgs[j].CreatedAt)
|
||||
})
|
||||
if limit > 0 && len(msgs) > limit {
|
||||
msgs = msgs[len(msgs)-limit:]
|
||||
}
|
||||
return append([]Message(nil), msgs...), nil
|
||||
}
|
||||
|
||||
func (s *Store) AddMessage(roomID int64, username, body string) (Message, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
found := false
|
||||
for _, r := range s.rooms {
|
||||
if r.ID == roomID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return Message{}, ErrNotFound
|
||||
}
|
||||
m := Message{ID: s.nextMessageID, RoomID: roomID, Username: username, Body: body, CreatedAt: time.Now().UTC()}
|
||||
s.nextMessageID++
|
||||
s.messages = append(s.messages, m)
|
||||
if err := s.saveLocked(); err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func maxInt64(a, b int64) int64 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user