]> git.r.bdr.sh - rbdr/mobius/commitdiff
Extensive refactor and clean up
authorJeff Halter <redacted>
Wed, 10 Jul 2024 04:36:27 +0000 (21:36 -0700)
committerJeff Halter <redacted>
Wed, 10 Jul 2024 04:42:05 +0000 (21:42 -0700)
35 files changed:
cmd/mobius-hotline-server/main.go
hotline/access.go
hotline/account_manager.go [new file with mode: 0644]
hotline/ban.go
hotline/chat.go [new file with mode: 0644]
hotline/chat_test.go [new file with mode: 0644]
hotline/client.go
hotline/client_conn.go
hotline/client_manager.go [new file with mode: 0644]
hotline/field.go
hotline/field_test.go
hotline/file_transfer.go
hotline/file_transfer_test.go
hotline/handshake.go
hotline/message_board.go [new file with mode: 0644]
hotline/message_board_test.go [new file with mode: 0644]
hotline/news.go
hotline/news_test.go
hotline/server.go
hotline/server_blackbox_test.go
hotline/server_test.go
hotline/stats.go
hotline/tracker.go
hotline/transaction.go
hotline/transaction_handlers.go
hotline/transaction_handlers_test.go
hotline/transaction_test.go
hotline/transfer.go
hotline/user.go
hotline/util.go [deleted file]
hotline/util_test.go [deleted file]
internal/mobius/ban.go [new file with mode: 0644]
internal/mobius/config.go [new file with mode: 0644]
internal/mobius/news.go [new file with mode: 0644]
internal/mobius/threaded_news.go [new file with mode: 0644]

index 1e9b5b6fd55f77eb1758f888cf84af7492923523..878b425952a700355178dcc6b4d9dd15ebe6aa3a 100644 (file)
@@ -7,14 +7,17 @@ import (
        "flag"
        "fmt"
        "github.com/jhalter/mobius/hotline"
        "flag"
        "fmt"
        "github.com/jhalter/mobius/hotline"
+       "github.com/jhalter/mobius/internal/mobius"
        "gopkg.in/natefinch/lumberjack.v2"
        "io"
        "log"
        "log/slog"
        "net/http"
        "os"
        "gopkg.in/natefinch/lumberjack.v2"
        "io"
        "log"
        "log/slog"
        "net/http"
        "os"
+       "os/signal"
        "path"
        "runtime"
        "path"
        "runtime"
+       "syscall"
 )
 
 //go:embed mobius/config
 )
 
 //go:embed mobius/config
@@ -36,28 +39,16 @@ var (
 )
 
 func main() {
 )
 
 func main() {
-       ctx, _ := context.WithCancel(context.Background())
-
-       // TODO: implement graceful shutdown by closing context
-       //c := make(chan os.Signal, 1)
-       //signal.Notify(c, os.Interrupt)
-       //defer func() {
-       //      signal.Stop(c)
-       //      cancel()
-       //}()
-       //go func() {
-       //      select {
-       //      case <-c:
-       //              cancel()
-       //      case <-ctx.Done():
-       //      }
-       //}()
+       ctx, cancel := context.WithCancel(context.Background())
+
+       sigChan := make(chan os.Signal, 1)
+       signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGINT, os.Interrupt)
 
        netInterface := flag.String("interface", "", "IP addr of interface to listen on.  Defaults to all interfaces.")
        basePort := flag.Int("bind", defaultPort, "Base Hotline server port.  File transfer port is base port + 1.")
        statsPort := flag.String("stats-port", "", "Enable stats HTTP endpoint on address and port")
        configDir := flag.String("config", defaultConfigPath(), "Path to config root")
 
        netInterface := flag.String("interface", "", "IP addr of interface to listen on.  Defaults to all interfaces.")
        basePort := flag.Int("bind", defaultPort, "Base Hotline server port.  File transfer port is base port + 1.")
        statsPort := flag.String("stats-port", "", "Enable stats HTTP endpoint on address and port")
        configDir := flag.String("config", defaultConfigPath(), "Path to config root")
-       printVersion := flag.Bool("version", false, "print version and exit")
+       printVersion := flag.Bool("version", false, "Print version and exit")
        logLevel := flag.String("log-level", "info", "Log level")
        logFile := flag.String("log-file", "", "Path to log file")
 
        logLevel := flag.String("log-level", "info", "Log level")
        logFile := flag.String("log-file", "", "Path to log file")
 
@@ -105,12 +96,36 @@ func main() {
                os.Exit(1)
        }
 
                os.Exit(1)
        }
 
-       srv, err := hotline.NewServer(*configDir, *netInterface, *basePort, slogger, &hotline.OSFileStore{})
+       config, err := mobius.LoadConfig(path.Join(*configDir, "config.yaml"))
+       if err != nil {
+               slogger.Error(fmt.Sprintf("Error loading config: %v", err))
+               os.Exit(1)
+       }
+
+       srv, err := hotline.NewServer(*config, *configDir, *netInterface, *basePort, slogger, &hotline.OSFileStore{})
        if err != nil {
                slogger.Error(fmt.Sprintf("Error starting server: %s", err))
                os.Exit(1)
        }
 
        if err != nil {
                slogger.Error(fmt.Sprintf("Error starting server: %s", err))
                os.Exit(1)
        }
 
+       srv.MessageBoard, err = mobius.NewFlatNews(path.Join(*configDir, "MessageBoard.txt"))
+       if err != nil {
+               slogger.Error(fmt.Sprintf("Error loading message board: %v", err))
+               os.Exit(1)
+       }
+
+       srv.BanList, err = mobius.NewBanFile(path.Join(*configDir, "Banlist.yaml"))
+       if err != nil {
+               slogger.Error(fmt.Sprintf("Error loading ban list: %v", err))
+               os.Exit(1)
+       }
+
+       srv.ThreadedNewsMgr, err = mobius.NewThreadedNewsYAML(path.Join(*configDir, "ThreadedNews.yaml"))
+       if err != nil {
+               slogger.Error(fmt.Sprintf("Error loading news: %v", err))
+               os.Exit(1)
+       }
+
        sh := statHandler{hlServer: srv}
        if *statsPort != "" {
                http.HandleFunc("/", sh.RenderStats)
        sh := statHandler{hlServer: srv}
        if *statsPort != "" {
                http.HandleFunc("/", sh.RenderStats)
@@ -124,6 +139,33 @@ func main() {
                }(srv)
        }
 
                }(srv)
        }
 
+       go func() {
+               for {
+                       sig := <-sigChan
+                       switch sig {
+                       case syscall.SIGHUP:
+                               slogger.Info("SIGHUP received.  Reloading configuration.")
+
+                               if err := srv.MessageBoard.(*mobius.FlatNews).Reload(); err != nil {
+                                       slogger.Error("Error reloading news", "err", err)
+                               }
+
+                               if err := srv.BanList.(*mobius.BanFile).Load(); err != nil {
+                                       slogger.Error("Error reloading ban list", "err", err)
+                               }
+
+                               if err := srv.ThreadedNewsMgr.(*mobius.ThreadedNewsYAML).Load(); err != nil {
+                                       slogger.Error("Error reloading threaded news list", "err", err)
+                               }
+                       default:
+                               signal.Stop(sigChan)
+                               cancel()
+                               os.Exit(0)
+                       }
+
+               }
+       }()
+
        slogger.Info("Hotline server started",
                "version", version,
                "API port", fmt.Sprintf("%s:%v", *netInterface, *basePort),
        slogger.Info("Hotline server started",
                "version", version,
                "API port", fmt.Sprintf("%s:%v", *netInterface, *basePort),
@@ -168,24 +210,6 @@ func defaultConfigPath() string {
        return cfgPath
 }
 
        return cfgPath
 }
 
-// copyFile copies a file from src to dst. If dst does not exist, it is created.
-func copyFile(src, dst string) error {
-       sourceFile, err := os.Open(src)
-       if err != nil {
-               return err
-       }
-       defer sourceFile.Close()
-
-       destinationFile, err := os.Create(dst)
-       if err != nil {
-               return err
-       }
-       defer destinationFile.Close()
-
-       _, err = io.Copy(destinationFile, sourceFile)
-       return err
-}
-
 // copyDir recursively copies a directory tree, attempting to preserve permissions.
 func copyDir(src, dst string) error {
        entries, err := cfgTemplate.ReadDir(src)
 // copyDir recursively copies a directory tree, attempting to preserve permissions.
 func copyDir(src, dst string) error {
        entries, err := cfgTemplate.ReadDir(src)
index 90740fc16fafd77e75338b52252061afdf00b52f..179941f29be631875f82e3212c3140e44d607bf8 100644 (file)
@@ -1,44 +1,44 @@
 package hotline
 
 const (
 package hotline
 
 const (
-       accessDeleteFile   = 0  // File System Maintenance: Can Delete Files
-       accessUploadFile   = 1  // File System Maintenance: Can Upload Files
-       accessDownloadFile = 2  // File System Maintenance: Can Download Files
-       accessRenameFile   = 3  // File System Maintenance: Can Rename Files
-       accessMoveFile     = 4  // File System Maintenance: Can Move Files
-       accessCreateFolder = 5  // File System Maintenance: Can Create Folders
-       accessDeleteFolder = 6  // File System Maintenance: Can Delete Folders
-       accessRenameFolder = 7  // File System Maintenance: Can Rename Folders
-       accessMoveFolder   = 8  // File System Maintenance: Can Move Folders
-       accessReadChat     = 9  // Chat: Can Read Chat
-       accessSendChat     = 10 // Chat: Can Send Chat
-       accessOpenChat     = 11 // Chat: Can Initial Private Chat
-       // accessCloseChat        = 12 // Present in the Hotline 1.9 protocol documentation, but seemingly unused
-       // accessShowInList       = 13 // Present in the Hotline 1.9 protocol documentation, but seemingly unused
-       accessCreateUser = 14 // User Maintenance: Can Create Accounts
-       accessDeleteUser = 15 // User Maintenance: Can Delete Accounts
-       accessOpenUser   = 16 // User Maintenance: Can Read Accounts
-       accessModifyUser = 17 // User Maintenance: Can Modify Accounts
-       // accessChangeOwnPass    = 18 // Present in the Hotline 1.9 protocol documentation, but seemingly unused
-       accessNewsReadArt      = 20 // News: Can Read Articles
-       accessNewsPostArt      = 21 // News: Can Post Articles
-       accessDisconUser       = 22 // User Maintenance: Can Disconnect Users (Note: Turns username red in user list)
-       accessCannotBeDiscon   = 23 // User Maintenance: Cannot be Disconnected
-       accessGetClientInfo    = 24 // User Maintenance: Can Get User Info
-       accessUploadAnywhere   = 25 // File System Maintenance: Can Upload Anywhere
-       accessAnyName          = 26 // Miscellaneous: Can User Any Name
-       accessNoAgreement      = 27 // Miscellaneous: Don't Show Agreement
-       accessSetFileComment   = 28 // File System Maintenance: Can Comment Files
-       accessSetFolderComment = 29 // File System Maintenance: Can Comment Folders
-       accessViewDropBoxes    = 30 // File System Maintenance: Can View Drop Boxes
-       accessMakeAlias        = 31 // File System Maintenance: Can Make Aliases
-       accessBroadcast        = 32 // Messaging: Can Broadcast
-       accessNewsDeleteArt    = 33 // News: Can Delete Articles
-       accessNewsCreateCat    = 34 // News: Can Create Categories
-       accessNewsDeleteCat    = 35 // News: Can Delete Categories
-       accessNewsCreateFldr   = 36 // News: Can Create News Bundles
-       accessNewsDeleteFldr   = 37 // News: Can Delete News Bundles
-       accessSendPrivMsg      = 40 // Messaging: Can Send Messages (Note: 1.9 protocol doc incorrectly says this is bit 19)
+       AccessDeleteFile       = 0  // File System Maintenance: Can Delete Files
+       AccessUploadFile       = 1  // File System Maintenance: Can Upload Files
+       AccessDownloadFile     = 2  // File System Maintenance: Can Download Files
+       AccessRenameFile       = 3  // File System Maintenance: Can Rename Files
+       AccessMoveFile         = 4  // File System Maintenance: Can Move Files
+       AccessCreateFolder     = 5  // File System Maintenance: Can Create Folders
+       AccessDeleteFolder     = 6  // File System Maintenance: Can Delete Folders
+       AccessRenameFolder     = 7  // File System Maintenance: Can Rename Folders
+       AccessMoveFolder       = 8  // File System Maintenance: Can Move Folders
+       AccessReadChat         = 9  // Chat: Can Read Chat
+       AccessSendChat         = 10 // Chat: Can Send Chat
+       AccessOpenChat         = 11 // Chat: Can Initial Private Chat
+       AccessCloseChat        = 12 // Present in the Hotline 1.9 protocol documentation, but seemingly unused
+       AccessShowInList       = 13 // Present in the Hotline 1.9 protocol documentation, but seemingly unused
+       AccessCreateUser       = 14 // User Maintenance: Can Create Accounts
+       AccessDeleteUser       = 15 // User Maintenance: Can Delete Accounts
+       AccessOpenUser         = 16 // User Maintenance: Can Read Accounts
+       AccessModifyUser       = 17 // User Maintenance: Can Modify Accounts
+       AccessChangeOwnPass    = 18 // Present in the Hotline 1.9 protocol documentation, but seemingly unused
+       AccessNewsReadArt      = 20 // News: Can Read Articles
+       AccessNewsPostArt      = 21 // News: Can Post Articles
+       AccessDisconUser       = 22 // User Maintenance: Can Disconnect Users (Note: Turns username red in user list)
+       AccessCannotBeDiscon   = 23 // User Maintenance: Cannot be Disconnected
+       AccessGetClientInfo    = 24 // User Maintenance: Can Get User Info
+       AccessUploadAnywhere   = 25 // File System Maintenance: Can Upload Anywhere
+       AccessAnyName          = 26 // Miscellaneous: Can User Any Name
+       AccessNoAgreement      = 27 // Miscellaneous: Don't Show Agreement
+       AccessSetFileComment   = 28 // File System Maintenance: Can Comment Files
+       AccessSetFolderComment = 29 // File System Maintenance: Can Comment Folders
+       AccessViewDropBoxes    = 30 // File System Maintenance: Can View Drop Boxes
+       AccessMakeAlias        = 31 // File System Maintenance: Can Make Aliases
+       AccessBroadcast        = 32 // Messaging: Can Broadcast
+       AccessNewsDeleteArt    = 33 // News: Can Delete Articles
+       AccessNewsCreateCat    = 34 // News: Can Create Categories
+       AccessNewsDeleteCat    = 35 // News: Can Delete Categories
+       AccessNewsCreateFldr   = 36 // News: Can Create News Bundles
+       AccessNewsDeleteFldr   = 37 // News: Can Delete News Bundles
+       AccessSendPrivMsg      = 40 // Messaging: Can Send Messages (Note: 1.9 protocol doc incorrectly says this is bit 19)
 )
 
 type accessBitmap [8]byte
 )
 
 type accessBitmap [8]byte
diff --git a/hotline/account_manager.go b/hotline/account_manager.go
new file mode 100644 (file)
index 0000000..769d12c
--- /dev/null
@@ -0,0 +1,188 @@
+package hotline
+
+import (
+       "fmt"
+       "github.com/stretchr/testify/mock"
+       "gopkg.in/yaml.v3"
+       "os"
+       "path"
+       "path/filepath"
+       "sync"
+)
+
+type AccountManager interface {
+       Create(account Account) error
+       Update(account Account, newLogin string) error
+       Get(login string) *Account
+       List() []Account
+       Delete(login string) error
+}
+
+type YAMLAccountManager struct {
+       accounts   map[string]Account
+       accountDir string
+
+       mu sync.Mutex
+}
+
+func NewYAMLAccountManager(accountDir string) (*YAMLAccountManager, error) {
+       accountMgr := YAMLAccountManager{
+               accountDir: accountDir,
+               accounts:   make(map[string]Account),
+       }
+
+       matches, err := filepath.Glob(filepath.Join(accountDir, "*.yaml"))
+       if err != nil {
+               return nil, err
+       }
+
+       if len(matches) == 0 {
+               return nil, fmt.Errorf("no accounts found in directory: %s", accountDir)
+       }
+
+       for _, file := range matches {
+               var account Account
+               if err = loadFromYAMLFile(file, &account); err != nil {
+                       return nil, fmt.Errorf("error loading account %s: %w", file, err)
+               }
+
+               accountMgr.accounts[account.Login] = account
+       }
+
+       return &accountMgr, nil
+}
+
+func (am *YAMLAccountManager) Create(account Account) error {
+       am.mu.Lock()
+       defer am.mu.Unlock()
+
+       // Create account file, returning an error if one already exists.
+       file, err := os.OpenFile(
+               filepath.Join(am.accountDir, path.Join("/", account.Login+".yaml")),
+               os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644,
+       )
+       if err != nil {
+               return fmt.Errorf("error creating account file: %w", err)
+       }
+       defer file.Close()
+
+       b, err := yaml.Marshal(account)
+       if err != nil {
+               return fmt.Errorf("marshal account to YAML: %v", err)
+       }
+
+       _, err = file.Write(b)
+       if err != nil {
+               return fmt.Errorf("write account file: %w", err)
+       }
+
+       am.accounts[account.Login] = account
+
+       return nil
+}
+
+func (am *YAMLAccountManager) Update(account Account, newLogin string) error {
+       am.mu.Lock()
+       defer am.mu.Unlock()
+
+       // If the login has changed, rename the account file.
+       if account.Login != newLogin {
+               err := os.Rename(
+                       filepath.Join(am.accountDir, path.Join("/", account.Login)+".yaml"),
+                       filepath.Join(am.accountDir, path.Join("/", newLogin)+".yaml"),
+               )
+               if err != nil {
+                       return fmt.Errorf("error renaming account file: %w", err)
+               }
+
+               account.Login = newLogin
+               am.accounts[newLogin] = account
+
+               delete(am.accounts, account.Login)
+       }
+
+       out, err := yaml.Marshal(&account)
+       if err != nil {
+               return err
+       }
+
+       if err := os.WriteFile(filepath.Join(am.accountDir, newLogin+".yaml"), out, 0644); err != nil {
+               return fmt.Errorf("error writing account file: %w", err)
+       }
+
+       am.accounts[account.Login] = account
+
+       return nil
+}
+
+func (am *YAMLAccountManager) Get(login string) *Account {
+       am.mu.Lock()
+       defer am.mu.Unlock()
+
+       account, ok := am.accounts[login]
+       if !ok {
+               return nil
+       }
+
+       return &account
+}
+
+func (am *YAMLAccountManager) List() []Account {
+       am.mu.Lock()
+       defer am.mu.Unlock()
+
+       var accounts []Account
+       for _, account := range am.accounts {
+               accounts = append(accounts, account)
+       }
+
+       return accounts
+}
+
+func (am *YAMLAccountManager) Delete(login string) error {
+       am.mu.Lock()
+       defer am.mu.Unlock()
+
+       err := os.Remove(filepath.Join(am.accountDir, path.Join("/", login+".yaml")))
+       if err != nil {
+               return fmt.Errorf("delete account file: %v", err)
+       }
+
+       delete(am.accounts, login)
+
+       return nil
+}
+
+type MockAccountManager struct {
+       mock.Mock
+}
+
+func (m *MockAccountManager) Create(account Account) error {
+       args := m.Called(account)
+
+       return args.Error(0)
+}
+
+func (m *MockAccountManager) Update(account Account, newLogin string) error {
+       args := m.Called(account, newLogin)
+
+       return args.Error(0)
+}
+
+func (m *MockAccountManager) Get(login string) *Account {
+       args := m.Called(login)
+
+       return args.Get(0).(*Account)
+}
+
+func (m *MockAccountManager) List() []Account {
+       args := m.Called()
+
+       return args.Get(0).([]Account)
+}
+
+func (m *MockAccountManager) Delete(login string) error {
+       args := m.Called(login)
+
+       return args.Error(0)
+}
index 0b75e8ee3e732a194b33c28c50039b3c095af112..14f41b9728253353cdbcf40fe6eb5c56d87ecde6 100644 (file)
@@ -3,3 +3,8 @@ package hotline
 import "time"
 
 const tempBanDuration = 30 * time.Minute
 import "time"
 
 const tempBanDuration = 30 * time.Minute
+
+type BanMgr interface {
+       Add(ip string, until *time.Time) error
+       IsBanned(ip string) (bool, *time.Time)
+}
diff --git a/hotline/chat.go b/hotline/chat.go
new file mode 100644 (file)
index 0000000..6f2a4dd
--- /dev/null
@@ -0,0 +1,137 @@
+package hotline
+
+import (
+       "crypto/rand"
+       "github.com/stretchr/testify/mock"
+       "slices"
+       "sync"
+)
+
+type PrivateChat struct {
+       Subject    string
+       ClientConn map[[2]byte]*ClientConn
+}
+
+type ChatID [4]byte
+
+type ChatManager interface {
+       New(cc *ClientConn) ChatID
+       GetSubject(id ChatID) string
+       Join(id ChatID, cc *ClientConn)
+       Leave(id ChatID, clientID [2]byte)
+       SetSubject(id ChatID, subject string)
+       Members(id ChatID) []*ClientConn
+}
+
+type MockChatManager struct {
+       mock.Mock
+}
+
+func (m *MockChatManager) New(cc *ClientConn) ChatID {
+       args := m.Called(cc)
+
+       return args.Get(0).(ChatID)
+}
+
+func (m *MockChatManager) GetSubject(id ChatID) string {
+       args := m.Called(id)
+
+       return args.String(0)
+}
+
+func (m *MockChatManager) Join(id ChatID, cc *ClientConn) {
+       m.Called(id, cc)
+}
+
+func (m *MockChatManager) Leave(id ChatID, clientID [2]byte) {
+       m.Called(id, clientID)
+}
+
+func (m *MockChatManager) SetSubject(id ChatID, subject string) {
+       m.Called(id, subject)
+
+}
+
+func (m *MockChatManager) Members(id ChatID) []*ClientConn {
+       args := m.Called(id)
+
+       return args.Get(0).([]*ClientConn)
+}
+
+type MemChatManager struct {
+       chats map[ChatID]*PrivateChat
+
+       mu sync.Mutex
+}
+
+func NewMemChatManager() *MemChatManager {
+       return &MemChatManager{
+               chats: make(map[ChatID]*PrivateChat),
+       }
+}
+
+func (cm *MemChatManager) New(cc *ClientConn) ChatID {
+       cm.mu.Lock()
+       defer cm.mu.Unlock()
+
+       var randID [4]byte
+       _, _ = rand.Read(randID[:])
+
+       cm.chats[randID] = &PrivateChat{ClientConn: make(map[[2]byte]*ClientConn)}
+
+       cm.chats[randID].ClientConn[cc.ID] = cc
+
+       return randID
+}
+
+func (cm *MemChatManager) Join(id ChatID, cc *ClientConn) {
+       cm.mu.Lock()
+       defer cm.mu.Unlock()
+
+       chat := cm.chats[id]
+       chat.ClientConn[cc.ID] = cc
+}
+
+func (cm *MemChatManager) Leave(id ChatID, clientID [2]byte) {
+       cm.mu.Lock()
+       defer cm.mu.Unlock()
+
+       privChat, ok := cm.chats[id]
+       if !ok {
+               return
+       }
+
+       delete(privChat.ClientConn, clientID)
+}
+
+func (cm *MemChatManager) GetSubject(id ChatID) string {
+       cm.mu.Lock()
+       defer cm.mu.Unlock()
+
+       return cm.chats[id].Subject
+}
+
+func (cm *MemChatManager) Members(id ChatID) []*ClientConn {
+       cm.mu.Lock()
+       defer cm.mu.Unlock()
+
+       chat := cm.chats[id]
+
+       var members []*ClientConn
+       for _, cc := range chat.ClientConn {
+               members = append(members, cc)
+       }
+
+       slices.SortFunc(members, clientConnSortFunc)
+
+       return members
+}
+
+func (cm *MemChatManager) SetSubject(id ChatID, subject string) {
+       cm.mu.Lock()
+       defer cm.mu.Unlock()
+
+       chat := cm.chats[id]
+
+       chat.Subject = subject
+}
diff --git a/hotline/chat_test.go b/hotline/chat_test.go
new file mode 100644 (file)
index 0000000..0bacc09
--- /dev/null
@@ -0,0 +1,67 @@
+package hotline
+
+import (
+       "github.com/stretchr/testify/assert"
+       "testing"
+)
+
+func TestMemChatManager(t *testing.T) {
+       cc1 := &ClientConn{ID: [2]byte{1}}
+       cc2 := &ClientConn{ID: [2]byte{2}}
+
+       cm := NewMemChatManager()
+
+       // Create a new chat with cc1 as initial member.
+       randChatID := cm.New(cc1)
+       assert.Equalf(t, []*ClientConn{cc1}, cm.Members(randChatID), "Initial ChatMembers")
+
+       // Second client joins.
+       cm.Join(randChatID, cc2)
+       assert.Equalf(t, []*ClientConn{cc1, cc2}, cm.Members(randChatID), "ChatMembers")
+
+       // Initial subject is blank.
+       assert.Equalf(t, "", cm.GetSubject(randChatID), "ChatMembers")
+
+       // Update subject.
+       cm.SetSubject(randChatID, "test")
+       assert.Equalf(t, "test", cm.GetSubject(randChatID), "ChatMembers")
+
+       // Second client leaves.
+       cm.Leave(randChatID, cc2.ID)
+       assert.Equalf(t, []*ClientConn{cc1}, cm.Members(randChatID), "ChatMembers")
+
+       //
+       //type fields struct {
+       //      chats map[ChatID]*PrivateChat
+       //}
+       //type args struct {
+       //      cc *ClientConn
+       //}
+       //tests := []struct {
+       //      name   string
+       //      fields fields
+       //      args   args
+       //      want   ChatID
+       //}{
+       //      {
+       //              name: "creates new chat",
+       //              fields: fields{
+       //                      chats: make(map[ChatID]*PrivateChat),
+       //              },
+       //              args: args{
+       //                      cc: &ClientConn{ID: [2]byte{1}},
+       //              },
+       //              //want: ,
+       //      },
+       //}
+       //for _, tt := range tests {
+       //      t.Run(tt.name, func(t *testing.T) {
+       //              cm := &MemChatManager{
+       //                      chats: tt.fields.chats,
+       //                      mu:    sync.Mutex{},
+       //              }
+       //
+       //              assert.Equalf(t, tt.want, cm.New(tt.args.cc), "New(%v)", tt.args.cc)
+       //      })
+       //}
+}
index 2389ee59b52118a1bcde343722f3b92ef932d1dd..e75f77bfe4a8a4c5db55c87d1bacb9ec57f7111c 100644 (file)
@@ -119,8 +119,8 @@ var ServerHandshake = []byte{
 }
 
 func (c *Client) Handshake() error {
 }
 
 func (c *Client) Handshake() error {
-       // Protocol ID  4       ‘TRTP’      0x54 52 54 50
-       // Sub-protocol ID      4               User defined
+       // Protocol Type        4       ‘TRTP’      0x54 52 54 50
+       // Sub-protocol Type    4               User defined
        // Version      2       1       Currently 1
        // Sub-version  2               User defined
        if _, err := c.Connection.Write(ClientHandshake); err != nil {
        // Version      2       1       Currently 1
        // Sub-version  2               User defined
        if _, err := c.Connection.Write(ClientHandshake); err != nil {
index e527eba99db5ef1ad101abda698991572a622243..dc589967c7f46ca810790a8cea949119f10dc869 100644 (file)
@@ -7,64 +7,117 @@ import (
        "golang.org/x/crypto/bcrypt"
        "io"
        "log/slog"
        "golang.org/x/crypto/bcrypt"
        "io"
        "log/slog"
-       "slices"
        "strings"
        "sync"
 )
 
        "strings"
        "sync"
 )
 
+var clientConnSortFunc = func(a, b *ClientConn) int {
+       return cmp.Compare(
+               binary.BigEndian.Uint16(a.ID[:]),
+               binary.BigEndian.Uint16(b.ID[:]),
+       )
+}
+
 // ClientConn represents a client connected to a Server
 type ClientConn struct {
        Connection io.ReadWriteCloser
        RemoteAddr string
 // ClientConn represents a client connected to a Server
 type ClientConn struct {
        Connection io.ReadWriteCloser
        RemoteAddr string
-       ID         [2]byte
-       Icon       []byte
-       flagsMU    sync.Mutex
-       Flags      UserFlags
-       UserName   []byte
-       Account    *Account
-       IdleTime   int
-       Server     *Server
-       Version    []byte
-       Idle       bool
-       AutoReply  []byte
-
-       transfersMU sync.Mutex
-       transfers   map[int]map[[4]byte]*FileTransfer
+       ID         ClientID
+       Icon       []byte // TODO: make fixed size of 2
+       Version    []byte // TODO: make fixed size of 2
+
+       flagsMU sync.Mutex // TODO: move into UserFlags struct
+       Flags   UserFlags
+
+       UserName  []byte
+       Account   *Account
+       IdleTime  int
+       Server    *Server // TODO: consider adding methods to interact with server
+       AutoReply []byte
+
+       ClientFileTransferMgr ClientFileTransferMgr
 
        logger *slog.Logger
 
 
        logger *slog.Logger
 
-       sync.Mutex
+       mu sync.RWMutex
+}
+
+type ClientFileTransferMgr struct {
+       transfers map[FileTransferType]map[FileTransferID]*FileTransfer
+
+       mu sync.RWMutex
+}
+
+func NewClientFileTransferMgr() ClientFileTransferMgr {
+       return ClientFileTransferMgr{
+               transfers: map[FileTransferType]map[FileTransferID]*FileTransfer{
+                       FileDownload:   {},
+                       FileUpload:     {},
+                       FolderDownload: {},
+                       FolderUpload:   {},
+                       BannerDownload: {},
+               },
+       }
+}
+
+func (cftm *ClientFileTransferMgr) Add(ftType FileTransferType, ft *FileTransfer) {
+       cftm.mu.Lock()
+       defer cftm.mu.Unlock()
+
+       cftm.transfers[ftType][ft.refNum] = ft
+}
+
+func (cftm *ClientFileTransferMgr) Get(ftType FileTransferType) []FileTransfer {
+       cftm.mu.Lock()
+       defer cftm.mu.Unlock()
+
+       fts := cftm.transfers[ftType]
+
+       var transfers []FileTransfer
+       for _, ft := range fts {
+               transfers = append(transfers, *ft)
+       }
+
+       return transfers
 }
 
 }
 
-func (cc *ClientConn) sendAll(t [2]byte, fields ...Field) {
-       for _, c := range cc.Server.Clients {
+func (cftm *ClientFileTransferMgr) Delete(ftType FileTransferType, id FileTransferID) {
+       cftm.mu.Lock()
+       defer cftm.mu.Unlock()
+
+       delete(cftm.transfers[ftType], id)
+}
+
+func (cc *ClientConn) SendAll(t [2]byte, fields ...Field) {
+       for _, c := range cc.Server.ClientMgr.List() {
                cc.Server.outbox <- NewTransaction(t, c.ID, fields...)
        }
 }
 
 func (cc *ClientConn) handleTransaction(transaction Transaction) {
        if handler, ok := TransactionHandlers[transaction.Type]; ok {
                cc.Server.outbox <- NewTransaction(t, c.ID, fields...)
        }
 }
 
 func (cc *ClientConn) handleTransaction(transaction Transaction) {
        if handler, ok := TransactionHandlers[transaction.Type]; ok {
-               cc.logger.Debug("Received Transaction", "RequestType", transaction.Type)
+               if transaction.Type != TranKeepAlive {
+                       cc.logger.Info(tranTypeNames[transaction.Type])
+               }
 
                for _, t := range handler(cc, &transaction) {
                        cc.Server.outbox <- t
                }
        }
 
 
                for _, t := range handler(cc, &transaction) {
                        cc.Server.outbox <- t
                }
        }
 
-       cc.Server.mux.Lock()
-       defer cc.Server.mux.Unlock()
-
        if transaction.Type != TranKeepAlive {
        if transaction.Type != TranKeepAlive {
+               cc.mu.Lock()
+               defer cc.mu.Unlock()
+
                // reset the user idle timer
                cc.IdleTime = 0
 
                // if user was previously idle, mark as not idle and notify other connected clients that
                // the user is no longer away
                // reset the user idle timer
                cc.IdleTime = 0
 
                // if user was previously idle, mark as not idle and notify other connected clients that
                // the user is no longer away
-               if cc.Idle {
+               if cc.Flags.IsSet(UserFlagAway) {
                        cc.Flags.Set(UserFlagAway, 0)
                        cc.Flags.Set(UserFlagAway, 0)
-                       cc.Idle = false
 
 
-                       cc.sendAll(
+                       cc.SendAll(
                                TranNotifyChangeUser,
                                NewField(FieldUserID, cc.ID[:]),
                                NewField(FieldUserFlags, cc.Flags[:]),
                                TranNotifyChangeUser,
                                NewField(FieldUserID, cc.ID[:]),
                                NewField(FieldUserFlags, cc.Flags[:]),
@@ -76,7 +129,7 @@ func (cc *ClientConn) handleTransaction(transaction Transaction) {
 }
 
 func (cc *ClientConn) Authenticate(login string, password []byte) bool {
 }
 
 func (cc *ClientConn) Authenticate(login string, password []byte) bool {
-       if account, ok := cc.Server.Accounts[login]; ok {
+       if account := cc.Server.AccountManager.Get(login); account != nil {
                return bcrypt.CompareHashAndPassword([]byte(account.Password), password) == nil
        }
 
                return bcrypt.CompareHashAndPassword([]byte(account.Password), password) == nil
        }
 
@@ -85,8 +138,6 @@ func (cc *ClientConn) Authenticate(login string, password []byte) bool {
 
 // Authorize checks if the user account has the specified permission
 func (cc *ClientConn) Authorize(access int) bool {
 
 // Authorize checks if the user account has the specified permission
 func (cc *ClientConn) Authorize(access int) bool {
-       cc.Lock()
-       defer cc.Unlock()
        if cc.Account == nil {
                return false
        }
        if cc.Account == nil {
                return false
        }
@@ -95,11 +146,9 @@ func (cc *ClientConn) Authorize(access int) bool {
 
 // Disconnect notifies other clients that a client has disconnected
 func (cc *ClientConn) Disconnect() {
 
 // Disconnect notifies other clients that a client has disconnected
 func (cc *ClientConn) Disconnect() {
-       cc.Server.mux.Lock()
-       delete(cc.Server.Clients, cc.ID)
-       cc.Server.mux.Unlock()
+       cc.Server.ClientMgr.Delete(cc.ID)
 
 
-       for _, t := range cc.notifyOthers(NewTransaction(TranNotifyDeleteUser, [2]byte{}, NewField(FieldUserID, cc.ID[:]))) {
+       for _, t := range cc.NotifyOthers(NewTransaction(TranNotifyDeleteUser, [2]byte{}, NewField(FieldUserID, cc.ID[:]))) {
                cc.Server.outbox <- t
        }
 
                cc.Server.outbox <- t
        }
 
@@ -108,11 +157,9 @@ func (cc *ClientConn) Disconnect() {
        }
 }
 
        }
 }
 
-// notifyOthers sends transaction t to other clients connected to the server
-func (cc *ClientConn) notifyOthers(t Transaction) (trans []Transaction) {
-       cc.Server.mux.Lock()
-       defer cc.Server.mux.Unlock()
-       for _, c := range cc.Server.Clients {
+// NotifyOthers sends transaction t to other clients connected to the server
+func (cc *ClientConn) NotifyOthers(t Transaction) (trans []Transaction) {
+       for _, c := range cc.Server.ClientMgr.List() {
                if c.ID != cc.ID {
                        t.clientID = c.ID
                        trans = append(trans, t)
                if c.ID != cc.ID {
                        t.clientID = c.ID
                        trans = append(trans, t)
@@ -146,25 +193,6 @@ func (cc *ClientConn) NewErrReply(t *Transaction, errMsg string) []Transaction {
        }
 }
 
        }
 }
 
-var clientSortFunc = func(a, b *ClientConn) int {
-       return cmp.Compare(
-               binary.BigEndian.Uint16(a.ID[:]),
-               binary.BigEndian.Uint16(b.ID[:]),
-       )
-}
-
-// sortedClients is a utility function that takes a map of *ClientConn and returns a sorted slice of the values.
-// The purpose of this is to ensure that the ordering of client connections is deterministic so that test assertions work.
-func sortedClients(unsortedClients map[[2]byte]*ClientConn) (clients []*ClientConn) {
-       for _, c := range unsortedClients {
-               clients = append(clients, c)
-       }
-
-       slices.SortFunc(clients, clientSortFunc)
-
-       return clients
-}
-
 const userInfoTemplate = `Nickname:   %s
 Name:       %s
 Account:    %s
 const userInfoTemplate = `Nickname:   %s
 Name:       %s
 Account:    %s
@@ -187,7 +215,7 @@ Address:    %s
 %s
 `
 
 %s
 `
 
-func formatDownloadList(fts map[[4]byte]*FileTransfer) (s string) {
+func formatDownloadList(fts []FileTransfer) (s string) {
        if len(fts) == 0 {
                return "None.\n"
        }
        if len(fts) == 0 {
                return "None.\n"
        }
@@ -200,18 +228,16 @@ func formatDownloadList(fts map[[4]byte]*FileTransfer) (s string) {
 }
 
 func (cc *ClientConn) String() string {
 }
 
 func (cc *ClientConn) String() string {
-       cc.transfersMU.Lock()
-       defer cc.transfersMU.Unlock()
        template := fmt.Sprintf(
                userInfoTemplate,
                cc.UserName,
                cc.Account.Name,
                cc.Account.Login,
                cc.RemoteAddr,
        template := fmt.Sprintf(
                userInfoTemplate,
                cc.UserName,
                cc.Account.Name,
                cc.Account.Login,
                cc.RemoteAddr,
-               formatDownloadList(cc.transfers[FileDownload]),
-               formatDownloadList(cc.transfers[FolderDownload]),
-               formatDownloadList(cc.transfers[FileUpload]),
-               formatDownloadList(cc.transfers[FolderUpload]),
+               formatDownloadList(cc.ClientFileTransferMgr.Get(FileDownload)),
+               formatDownloadList(cc.ClientFileTransferMgr.Get(FolderDownload)),
+               formatDownloadList(cc.ClientFileTransferMgr.Get(FileUpload)),
+               formatDownloadList(cc.ClientFileTransferMgr.Get(FolderUpload)),
                "None.\n",
        )
 
                "None.\n",
        )
 
diff --git a/hotline/client_manager.go b/hotline/client_manager.go
new file mode 100644 (file)
index 0000000..ab6372c
--- /dev/null
@@ -0,0 +1,99 @@
+package hotline
+
+import (
+       "cmp"
+       "encoding/binary"
+       "github.com/stretchr/testify/mock"
+       "slices"
+       "sync"
+       "sync/atomic"
+)
+
+type ClientID [2]byte
+
+type ClientManager interface {
+       List() []*ClientConn // Returns list of sorted clients
+       Get(id ClientID) *ClientConn
+       Add(cc *ClientConn)
+       Delete(id ClientID)
+}
+
+type MockClientMgr struct {
+       mock.Mock
+}
+
+func (m *MockClientMgr) List() []*ClientConn {
+       args := m.Called()
+
+       return args.Get(0).([]*ClientConn)
+}
+
+func (m *MockClientMgr) Get(id ClientID) *ClientConn {
+       args := m.Called(id)
+
+       return args.Get(0).(*ClientConn)
+}
+
+func (m *MockClientMgr) Add(cc *ClientConn) {
+       m.Called(cc)
+}
+func (m *MockClientMgr) Delete(id ClientID) {
+       m.Called(id)
+}
+
+type MemClientMgr struct {
+       clients map[ClientID]*ClientConn
+
+       mu           sync.Mutex
+       nextClientID atomic.Uint32
+}
+
+func NewMemClientMgr() *MemClientMgr {
+       return &MemClientMgr{
+               clients: make(map[ClientID]*ClientConn),
+       }
+}
+
+// List returns slice of sorted clients.
+func (cm *MemClientMgr) List() []*ClientConn {
+       cm.mu.Lock()
+       defer cm.mu.Unlock()
+
+       var clients []*ClientConn
+       for _, client := range cm.clients {
+               clients = append(clients, client)
+       }
+
+       slices.SortFunc(clients, func(a, b *ClientConn) int {
+               return cmp.Compare(
+                       binary.BigEndian.Uint16(a.ID[:]),
+                       binary.BigEndian.Uint16(b.ID[:]),
+               )
+       })
+
+       return clients
+}
+
+func (cm *MemClientMgr) Get(id ClientID) *ClientConn {
+       cm.mu.Lock()
+       defer cm.mu.Unlock()
+
+       return cm.clients[id]
+}
+
+func (cm *MemClientMgr) Add(cc *ClientConn) {
+       cm.mu.Lock()
+       defer cm.mu.Unlock()
+
+       cm.nextClientID.Add(1)
+       binary.BigEndian.PutUint16(cc.ID[:], uint16(cm.nextClientID.Load()))
+
+       cm.clients[cc.ID] = cc
+}
+
+func (cm *MemClientMgr) Delete(id ClientID) {
+       cm.mu.Lock()
+       defer cm.mu.Unlock()
+
+       delete(cm.clients, id)
+}
index 7bba0d709c35da7fa6d5432dc47f196a805f2f8d..4c760d5f248691867eb09e58f09a7672df74f8e0 100644 (file)
@@ -1,6 +1,8 @@
 package hotline
 
 import (
 package hotline
 
 import (
+       "bufio"
+       "bytes"
        "encoding/binary"
        "errors"
        "io"
        "encoding/binary"
        "errors"
        "io"
@@ -62,24 +64,24 @@ var (
        FieldNewsArtData         = [2]byte{0x01, 0x4D} // 333
        FieldNewsArtParentArt    = [2]byte{0x01, 0x4F} // 335
        FieldNewsArt1stChildArt  = [2]byte{0x01, 0x50} // 336
        FieldNewsArtData         = [2]byte{0x01, 0x4D} // 333
        FieldNewsArtParentArt    = [2]byte{0x01, 0x4F} // 335
        FieldNewsArt1stChildArt  = [2]byte{0x01, 0x50} // 336
+       FieldNewsArtRecurseDel   = [2]byte{0x01, 0x51} // 337
 
        // These fields are documented, but seemingly unused.
        // FieldUserAlias           = [2]byte{0x00, 0x6F} // 111
        // FieldNewsArtFlags        = [2]byte{0x01, 0x4E} // 334
 
        // These fields are documented, but seemingly unused.
        // FieldUserAlias           = [2]byte{0x00, 0x6F} // 111
        // FieldNewsArtFlags        = [2]byte{0x01, 0x4E} // 334
-       // FieldNewsArtRecurseDel   = [2]byte{0x01, 0x51} // 337
 )
 
 type Field struct {
 )
 
 type Field struct {
-       ID        [2]byte // Type of field
-       FieldSize [2]byte // Size of the data part
-       Data      []byte  // Actual field content
+       Type      [2]byte // Type of field
+       FieldSize [2]byte // Size of the data field
+       Data      []byte  // Field data
 
        readOffset int // Internal offset to track read progress
 }
 
 
        readOffset int // Internal offset to track read progress
 }
 
-func NewField(id [2]byte, data []byte) Field {
+func NewField(fieldType [2]byte, data []byte) Field {
        f := Field{
        f := Field{
-               ID:   id,
+               Type: fieldType,
                Data: make([]byte, len(data)),
        }
 
                Data: make([]byte, len(data)),
        }
 
@@ -105,9 +107,53 @@ func fieldScanner(data []byte, _ bool) (advance int, token []byte, err error) {
        return neededSize, data[0:neededSize], nil
 }
 
        return neededSize, data[0:neededSize], nil
 }
 
+// DecodeInt decodes the field bytes to an int.
+// The official Hotline clients will send uint32s as 2 bytes if possible, but
+// some third party clients such as Frogblast and Heildrun will always send 4 bytes
+func (f *Field) DecodeInt() (int, error) {
+       switch len(f.Data) {
+       case 2:
+               return int(binary.BigEndian.Uint16(f.Data)), nil
+       case 4:
+               return int(binary.BigEndian.Uint32(f.Data)), nil
+       }
+
+       return 0, errors.New("unknown byte length")
+}
+
+func (f *Field) DecodeObfuscatedString() string {
+       return string(encodeString(f.Data))
+}
+
+// DecodeNewsPath decodes the field data to a news path.
+// Example News Path data for a Category nested under two Bundles:
+// 00000000  00 03 00 00 10 54 6f 70  20 4c 65 76 65 6c 20 42  |.....Top Level B|
+// 00000010  75 6e 64 6c 65 00 00 13  53 65 63 6f 6e 64 20 4c  |undle...Second L|
+// 00000020  65 76 65 6c 20 42 75 6e  64 6c 65 00 00 0f 4e 65  |evel Bundle...Ne|
+// 00000030  73 74 65 64 20 43 61 74  65 67 6f 72 79           |sted Category|
+func (f *Field) DecodeNewsPath() ([]string, error) {
+       if len(f.Data) == 0 {
+               return []string{}, nil
+       }
+
+       pathCount := binary.BigEndian.Uint16(f.Data[0:2])
+
+       scanner := bufio.NewScanner(bytes.NewReader(f.Data[2:]))
+       scanner.Split(newsPathScanner)
+
+       var paths []string
+
+       for i := uint16(0); i < pathCount; i++ {
+               scanner.Scan()
+               paths = append(paths, scanner.Text())
+       }
+
+       return paths, nil
+}
+
 // Read implements io.Reader for Field
 func (f *Field) Read(p []byte) (int, error) {
 // Read implements io.Reader for Field
 func (f *Field) Read(p []byte) (int, error) {
-       buf := slices.Concat(f.ID[:], f.FieldSize[:], f.Data)
+       buf := slices.Concat(f.Type[:], f.FieldSize[:], f.Data)
 
        if f.readOffset >= len(buf) {
                return 0, io.EOF // All bytes have been read
 
        if f.readOffset >= len(buf) {
                return 0, io.EOF // All bytes have been read
@@ -125,7 +171,7 @@ func (f *Field) Write(p []byte) (int, error) {
                return 0, errors.New("input slice too short")
        }
 
                return 0, errors.New("input slice too short")
        }
 
-       copy(f.ID[:], p[0:2])
+       copy(f.Type[:], p[0:2])
        copy(f.FieldSize[:], p[2:4])
 
        dataSize := int(binary.BigEndian.Uint16(f.FieldSize[:]))
        copy(f.FieldSize[:], p[2:4])
 
        dataSize := int(binary.BigEndian.Uint16(f.FieldSize[:]))
@@ -141,7 +187,7 @@ func (f *Field) Write(p []byte) (int, error) {
 
 func getField(id [2]byte, fields *[]Field) *Field {
        for _, field := range *fields {
 
 func getField(id [2]byte, fields *[]Field) *Field {
        for _, field := range *fields {
-               if id == field.ID {
+               if id == field.Type {
                        return &field
                }
        }
                        return &field
                }
        }
index ecae9c77351e76b6306085ba610c02af1ed972ea..abb4d44b1a832c5e67265d0e575ac5f8fa11ccaf 100644 (file)
@@ -172,7 +172,7 @@ func TestField_Read(t *testing.T) {
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
                        f := &Field{
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
                        f := &Field{
-                               ID:         tt.fields.ID,
+                               Type:       tt.fields.ID,
                                FieldSize:  tt.fields.FieldSize,
                                Data:       tt.fields.Data,
                                readOffset: tt.fields.readOffset,
                                FieldSize:  tt.fields.FieldSize,
                                Data:       tt.fields.Data,
                                readOffset: tt.fields.readOffset,
@@ -186,3 +186,46 @@ func TestField_Read(t *testing.T) {
                })
        }
 }
                })
        }
 }
+
+func TestField_DecodeInt(t *testing.T) {
+       type fields struct {
+               Data []byte
+       }
+       tests := []struct {
+               name    string
+               fields  fields
+               want    int
+               wantErr assert.ErrorAssertionFunc
+       }{
+               {
+                       name:    "with 2 bytes of input",
+                       fields:  fields{Data: []byte{0, 1}},
+                       want:    1,
+                       wantErr: assert.NoError,
+               },
+               {
+                       name:    "with 4 bytes of input",
+                       fields:  fields{Data: []byte{0, 1, 0, 0}},
+                       want:    65536,
+                       wantErr: assert.NoError,
+               },
+               {
+                       name:    "with invalid number of bytes of input",
+                       fields:  fields{Data: []byte{1, 0, 0, 0, 0, 0, 0, 0}},
+                       want:    0,
+                       wantErr: assert.Error,
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       f := &Field{
+                               Data: tt.fields.Data,
+                       }
+                       got, err := f.DecodeInt()
+                       if !tt.wantErr(t, err, fmt.Sprintf("DecodeInt()")) {
+                               return
+                       }
+                       assert.Equalf(t, tt.want, got, "DecodeInt()")
+               })
+       }
+}
index c883822a994783f0b95ad8ba3e5c80c857648dc5..b567b182f5767bd646b65328a5018dcaa51fece0 100644 (file)
@@ -16,20 +16,87 @@ import (
        "sync"
 )
 
        "sync"
 )
 
+// Folder download actions.  Send by the client to indicate the next action the server should take
+// for a folder download.
+const (
+       DlFldrActionSendFile   = 1
+       DlFldrActionResumeFile = 2
+       DlFldrActionNextFile   = 3
+)
+
 // File transfer types
 // File transfer types
+type FileTransferType uint8
+
 const (
 const (
-       FileDownload = iota
-       FileUpload
-       FolderDownload
-       FolderUpload
-       bannerDownload
+       FileDownload   = FileTransferType(0)
+       FileUpload     = FileTransferType(1)
+       FolderDownload = FileTransferType(2)
+       FolderUpload   = FileTransferType(3)
+       BannerDownload = FileTransferType(4)
 )
 
 )
 
+type FileTransferID [4]byte
+
+type FileTransferMgr interface {
+       Add(ft *FileTransfer)
+       Get(id FileTransferID) *FileTransfer
+       Delete(id FileTransferID)
+}
+
+type MemFileTransferMgr struct {
+       fileTransfers map[FileTransferID]*FileTransfer
+
+       mu sync.Mutex
+}
+
+func NewMemFileTransferMgr() *MemFileTransferMgr {
+       return &MemFileTransferMgr{
+               fileTransfers: make(map[FileTransferID]*FileTransfer),
+       }
+}
+
+func (ftm *MemFileTransferMgr) Add(ft *FileTransfer) {
+       ftm.mu.Lock()
+       defer ftm.mu.Unlock()
+
+       _, _ = rand.Read(ft.refNum[:])
+
+       ftm.fileTransfers[ft.refNum] = ft
+
+       ft.ClientConn.ClientFileTransferMgr.Add(ft.Type, ft)
+
+       //ft.ClientConn.transfersMU.Lock()
+       //ft.ClientConn.transfers[ft.Type] = ft
+       //ft.ClientConn.transfersMU.Unlock()
+}
+
+func (ftm *MemFileTransferMgr) Get(id FileTransferID) *FileTransfer {
+       ftm.mu.Lock()
+       defer ftm.mu.Unlock()
+
+       return ftm.fileTransfers[id]
+}
+
+func (ftm *MemFileTransferMgr) Delete(id FileTransferID) {
+       ftm.mu.Lock()
+       defer ftm.mu.Unlock()
+
+       ft := ftm.fileTransfers[id]
+
+       //ft.ClientConn.transfersMU.Lock()
+       //delete(ft.ClientConn.transfers[ft.Type], ft.refNum)
+       //ft.ClientConn.transfersMU.Unlock()
+       ft.ClientConn.ClientFileTransferMgr.Delete(ft.Type, id)
+
+       delete(ftm.fileTransfers, id)
+
+}
+
 type FileTransfer struct {
        FileName         []byte
        FilePath         []byte
        refNum           [4]byte
 type FileTransfer struct {
        FileName         []byte
        FilePath         []byte
        refNum           [4]byte
-       Type             int
+       Type             FileTransferType
        TransferSize     []byte
        FolderItemCount  []byte
        fileResumeData   *FileResumeData
        TransferSize     []byte
        FolderItemCount  []byte
        fileResumeData   *FileResumeData
@@ -55,7 +122,7 @@ func (wc *WriteCounter) Write(p []byte) (int, error) {
        return n, nil
 }
 
        return n, nil
 }
 
-func (cc *ClientConn) newFileTransfer(transferType int, fileName, filePath, size []byte) *FileTransfer {
+func (cc *ClientConn) newFileTransfer(transferType FileTransferType, fileName, filePath, size []byte) *FileTransfer {
        ft := &FileTransfer{
                FileName:         fileName,
                FilePath:         filePath,
        ft := &FileTransfer{
                FileName:         fileName,
                FilePath:         filePath,
@@ -64,16 +131,18 @@ func (cc *ClientConn) newFileTransfer(transferType int, fileName, filePath, size
                ClientConn:       cc,
                bytesSentCounter: &WriteCounter{},
        }
                ClientConn:       cc,
                bytesSentCounter: &WriteCounter{},
        }
+       //
+       //_, _ = rand.Read(ft.refNum[:])
+       //
+       //cc.transfersMU.Lock()
+       //defer cc.transfersMU.Unlock()
+       //cc.transfers[transferType][ft.refNum] = ft
 
 
-       _, _ = rand.Read(ft.refNum[:])
-
-       cc.transfersMU.Lock()
-       defer cc.transfersMU.Unlock()
-       cc.transfers[transferType][ft.refNum] = ft
+       cc.Server.FileTransferMgr.Add(ft)
 
 
-       cc.Server.mux.Lock()
-       defer cc.Server.mux.Unlock()
-       cc.Server.fileTransfers[ft.refNum] = ft
+       //cc.Server.mux.Lock()
+       //defer cc.Server.mux.Unlock()
+       //cc.Server.fileTransfers[ft.refNum] = ft
 
        return ft
 }
 
        return ft
 }
@@ -148,7 +217,7 @@ func (fu *folderUpload) FormattedPath() string {
        return filepath.Join(pathSegments...)
 }
 
        return filepath.Join(pathSegments...)
 }
 
-func DownloadHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileTransfer, fs FileStore, rLogger *slog.Logger, preserveForks bool) error {
+func DownloadHandler(w io.Writer, fullPath string, fileTransfer *FileTransfer, fs FileStore, rLogger *slog.Logger, preserveForks bool) error {
        //s.Stats.DownloadCounter += 1
        //s.Stats.DownloadsInProgress += 1
        //defer func() {
        //s.Stats.DownloadCounter += 1
        //s.Stats.DownloadsInProgress += 1
        //defer func() {
@@ -162,48 +231,48 @@ func DownloadHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileTrans
 
        fw, err := newFileWrapper(fs, fullPath, 0)
        if err != nil {
 
        fw, err := newFileWrapper(fs, fullPath, 0)
        if err != nil {
-               //return err
+               return fmt.Errorf("reading file header: %v", err)
        }
 
        }
 
-       rLogger.Info("File download started", "filePath", fullPath)
+       rLogger.Info("Download file", "filePath", fullPath)
 
 
-       // if file transfer options are included, that means this is a "quick preview" request from a 1.5+ client
+       // If file transfer options are included, that means this is a "quick preview" request.  In this case skip sending
+       // the flat file info and proceed directly to sending the file data.
        if fileTransfer.options == nil {
        if fileTransfer.options == nil {
-               _, err = io.Copy(rwc, fw.ffo)
-               if err != nil {
-                       //return err
+               if _, err = io.Copy(w, fw.ffo); err != nil {
+                       return fmt.Errorf("send flat file object: %v", err)
                }
        }
 
        file, err := fw.dataForkReader()
        if err != nil {
                }
        }
 
        file, err := fw.dataForkReader()
        if err != nil {
-               //return err
+               return fmt.Errorf("open data fork reader: %v", err)
        }
 
        br := bufio.NewReader(file)
        if _, err := br.Discard(int(dataOffset)); err != nil {
        }
 
        br := bufio.NewReader(file)
        if _, err := br.Discard(int(dataOffset)); err != nil {
-               //return err
+               return fmt.Errorf("seek to resume offsent: %v", err)
        }
 
        }
 
-       if _, err = io.Copy(rwc, io.TeeReader(br, fileTransfer.bytesSentCounter)); err != nil {
-               return err
+       if _, err = io.Copy(w, io.TeeReader(br, fileTransfer.bytesSentCounter)); err != nil {
+               return fmt.Errorf("send data fork: %v", err)
        }
 
        }
 
-       // if the client requested to resume transfer, do not send the resource fork header, or it will be appended into the fileWrapper data
+       // If the client requested to resume transfer, do not send the resource fork header.
        if fileTransfer.fileResumeData == nil {
        if fileTransfer.fileResumeData == nil {
-               err = binary.Write(rwc, binary.BigEndian, fw.rsrcForkHeader())
+               err = binary.Write(w, binary.BigEndian, fw.rsrcForkHeader())
                if err != nil {
                if err != nil {
-                       return err
+                       return fmt.Errorf("send resource fork header: %v", err)
                }
        }
 
        rFile, err := fw.rsrcForkFile()
        if err != nil {
                }
        }
 
        rFile, err := fw.rsrcForkFile()
        if err != nil {
-               return nil
+               // return fmt.Errorf("open resource fork file: %v", err)
        }
 
        }
 
-       if _, err = io.Copy(rwc, io.TeeReader(rFile, fileTransfer.bytesSentCounter)); err != nil {
-               return err
+       if _, err = io.Copy(w, io.TeeReader(rFile, fileTransfer.bytesSentCounter)); err != nil {
+               // return fmt.Errorf("send resource fork data: %v", err)
        }
 
        return nil
        }
 
        return nil
@@ -347,7 +416,7 @@ func DownloadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *Fil
                var dataOffset int64
 
                switch nextAction[1] {
                var dataOffset int64
 
                switch nextAction[1] {
-               case dlFldrActionResumeFile:
+               case DlFldrActionResumeFile:
                        // get size of resumeData
                        resumeDataByteLen := make([]byte, 2)
                        if _, err := io.ReadFull(rwc, resumeDataByteLen); err != nil {
                        // get size of resumeData
                        resumeDataByteLen := make([]byte, 2)
                        if _, err := io.ReadFull(rwc, resumeDataByteLen); err != nil {
@@ -365,7 +434,7 @@ func DownloadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *Fil
                                return err
                        }
                        dataOffset = int64(binary.BigEndian.Uint32(frd.ForkInfoList[0].DataSize[:]))
                                return err
                        }
                        dataOffset = int64(binary.BigEndian.Uint32(frd.ForkInfoList[0].DataSize[:]))
-               case dlFldrActionNextFile:
+               case DlFldrActionNextFile:
                        // client asked to skip this file
                        return nil
                }
                        // client asked to skip this file
                        return nil
                }
@@ -442,7 +511,7 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT
        }
 
        // Begin the folder upload flow by sending the "next file action" to client
        }
 
        // Begin the folder upload flow by sending the "next file action" to client
-       if _, err := rwc.Write([]byte{0, dlFldrActionNextFile}); err != nil {
+       if _, err := rwc.Write([]byte{0, DlFldrActionNextFile}); err != nil {
                return err
        }
 
                return err
        }
 
@@ -475,11 +544,11 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT
                        }
 
                        // Tell client to send next file
                        }
 
                        // Tell client to send next file
-                       if _, err := rwc.Write([]byte{0, dlFldrActionNextFile}); err != nil {
+                       if _, err := rwc.Write([]byte{0, DlFldrActionNextFile}); err != nil {
                                return err
                        }
                } else {
                                return err
                        }
                } else {
-                       nextAction := dlFldrActionSendFile
+                       nextAction := DlFldrActionSendFile
 
                        // Check if we have the full file already.  If so, send dlFldrAction_NextFile to client to skip.
                        _, err := os.Stat(filepath.Join(fullPath, fu.FormattedPath()))
 
                        // Check if we have the full file already.  If so, send dlFldrAction_NextFile to client to skip.
                        _, err := os.Stat(filepath.Join(fullPath, fu.FormattedPath()))
@@ -487,7 +556,7 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT
                                return err
                        }
                        if err == nil {
                                return err
                        }
                        if err == nil {
-                               nextAction = dlFldrActionNextFile
+                               nextAction = DlFldrActionNextFile
                        }
 
                        //  Check if we have a partial file already.  If so, send dlFldrAction_ResumeFile to client to resume upload.
                        }
 
                        //  Check if we have a partial file already.  If so, send dlFldrAction_ResumeFile to client to resume upload.
@@ -496,7 +565,7 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT
                                return err
                        }
                        if err == nil {
                                return err
                        }
                        if err == nil {
-                               nextAction = dlFldrActionResumeFile
+                               nextAction = DlFldrActionResumeFile
                        }
 
                        if _, err := rwc.Write([]byte{0, uint8(nextAction)}); err != nil {
                        }
 
                        if _, err := rwc.Write([]byte{0, uint8(nextAction)}); err != nil {
@@ -504,9 +573,9 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT
                        }
 
                        switch nextAction {
                        }
 
                        switch nextAction {
-                       case dlFldrActionNextFile:
+                       case DlFldrActionNextFile:
                                continue
                                continue
-                       case dlFldrActionResumeFile:
+                       case DlFldrActionResumeFile:
                                offset := make([]byte, 4)
                                binary.BigEndian.PutUint32(offset, uint32(incompleteFile.Size()))
 
                                offset := make([]byte, 4)
                                binary.BigEndian.PutUint32(offset, uint32(incompleteFile.Size()))
 
@@ -539,7 +608,7 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT
                                        return err
                                }
 
                                        return err
                                }
 
-                       case dlFldrActionSendFile:
+                       case DlFldrActionSendFile:
                                if _, err := io.ReadFull(rwc, fileSize); err != nil {
                                        return err
                                }
                                if _, err := io.ReadFull(rwc, fileSize); err != nil {
                                        return err
                                }
@@ -581,7 +650,7 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT
                        }
 
                        // Tell client to send next fileWrapper
                        }
 
                        // Tell client to send next fileWrapper
-                       if _, err := rwc.Write([]byte{0, dlFldrActionNextFile}); err != nil {
+                       if _, err := rwc.Write([]byte{0, DlFldrActionNextFile}); err != nil {
                                return err
                        }
                }
                                return err
                        }
                }
index 213cb9aacaac12a4b54ad66a1857bf1af26c7a4f..d12f29acde3e6b9d7dd39aa49e1bd1d7a910740c 100644 (file)
@@ -11,7 +11,7 @@ func TestFileTransfer_String(t *testing.T) {
                FileName         []byte
                FilePath         []byte
                refNum           [4]byte
                FileName         []byte
                FilePath         []byte
                refNum           [4]byte
-               Type             int
+               Type             FileTransferType
                TransferSize     []byte
                FolderItemCount  []byte
                fileResumeData   *FileResumeData
                TransferSize     []byte
                FolderItemCount  []byte
                fileResumeData   *FileResumeData
index c54359e0e648acf41b16c3ad3cab8cfc6e73018a..55b074d8ba5a61faca6ecd2ceb1f9c10099c4270 100644 (file)
@@ -19,14 +19,14 @@ import (
 //
 // The following information is sent to the server:
 // Description         Size    Data    Note
 //
 // The following information is sent to the server:
 // Description         Size    Data    Note
-// Protocol ID         4               TRTP    0x54525450
-// Sub-protocol ID     4               HOTL    User defined
+// Protocol Type               4               TRTP    0x54525450
+// Sub-protocol Type   4               HOTL    User defined
 // VERSION                     2               1               Currently 1
 // Sub-version         2               2               User defined
 //
 // The server replies with the following:
 // Description         Size    Data    Note
 // VERSION                     2               1               Currently 1
 // Sub-version         2               2               User defined
 //
 // The server replies with the following:
 // Description         Size    Data    Note
-// Protocol ID         4               TRTP
+// Protocol Type               4               TRTP
 // Error code          4                               Error code returned by the server (0 = no error)
 
 type handshake struct {
 // Error code          4                               Error code returned by the server (0 = no error)
 
 type handshake struct {
@@ -53,10 +53,10 @@ func (h *handshake) Valid() bool {
 }
 
 var (
 }
 
 var (
-       // trtp represents the Protocol ID "TRTP" in hex
+       // trtp represents the Protocol Type "TRTP" in hex
        trtp = [4]byte{0x54, 0x52, 0x54, 0x50}
 
        trtp = [4]byte{0x54, 0x52, 0x54, 0x50}
 
-       // hotl represents the Sub-protocol ID "HOTL" in hex
+       // hotl represents the Sub-protocol Type "HOTL" in hex
        hotl = [4]byte{0x48, 0x4F, 0x54, 0x4C}
 
        // handshakeResponse represents the server's response after a successful handshake
        hotl = [4]byte{0x48, 0x4F, 0x54, 0x4C}
 
        // handshakeResponse represents the server's response after a successful handshake
diff --git a/hotline/message_board.go b/hotline/message_board.go
new file mode 100644 (file)
index 0000000..e5022a4
--- /dev/null
@@ -0,0 +1,9 @@
+package hotline
+
+const defaultNewsDateFormat = "Jan02 15:04" // Jun23 20:49
+
+const defaultNewsTemplate = `From %s (%s):
+
+%s
+
+__________________________________________________________`
diff --git a/hotline/message_board_test.go b/hotline/message_board_test.go
new file mode 100644 (file)
index 0000000..64db714
--- /dev/null
@@ -0,0 +1,25 @@
+package hotline
+
+import "github.com/stretchr/testify/mock"
+
+type mockReadWriteSeeker struct {
+       mock.Mock
+}
+
+func (m *mockReadWriteSeeker) Read(p []byte) (int, error) {
+       args := m.Called(p)
+
+       return args.Int(0), args.Error(1)
+}
+
+func (m *mockReadWriteSeeker) Write(p []byte) (int, error) {
+       args := m.Called(p)
+
+       return args.Int(0), args.Error(1)
+}
+
+func (m *mockReadWriteSeeker) Seek(offset int64, whence int) (int64, error) {
+       args := m.Called(offset, whence)
+
+       return args.Get(0).(int64), args.Error(1)
+}
index 6e89388028a8d034ce1ed36102d2bb674bd25071..0d0400c6e7bc7b15ff2946e0429c18874435d81d 100644 (file)
@@ -1,21 +1,29 @@
 package hotline
 
 import (
 package hotline
 
 import (
+       "cmp"
        "encoding/binary"
        "io"
        "slices"
        "encoding/binary"
        "io"
        "slices"
-       "sort"
 )
 
 )
 
-const defaultNewsDateFormat = "Jan02 15:04" // Jun23 20:49
-
-const defaultNewsTemplate = `From %s (%s):
-
-%s
+var (
+       NewsBundle   = [2]byte{0, 2}
+       NewsCategory = [2]byte{0, 3}
+)
 
 
-__________________________________________________________`
+type ThreadedNewsMgr interface {
+       ListArticles(newsPath []string) NewsArtListData
+       GetArticle(newsPath []string, articleID uint32) *NewsArtData
+       DeleteArticle(newsPath []string, articleID uint32, recursive bool) error
+       PostArticle(newsPath []string, parentArticleID uint32, article NewsArtData) error
+       CreateGrouping(newsPath []string, name string, t [2]byte) error
+       GetCategories(paths []string) []NewsCategoryListData15
+       NewsItem(newsPath []string) NewsCategoryListData15
+       DeleteNewsItem(newsPath []string) error
+}
 
 
-// ThreadedNews is the top level struct containing all threaded news categories, bundles, and articles
+// ThreadedNews contains the top level of threaded news categories, bundles, and articles.
 type ThreadedNews struct {
        Categories map[string]NewsCategoryListData15 `yaml:"Categories"`
 }
 type ThreadedNews struct {
        Categories map[string]NewsCategoryListData15 `yaml:"Categories"`
 }
@@ -40,19 +48,23 @@ func (newscat *NewsCategoryListData15) GetNewsArtListData() NewsArtListData {
                id := make([]byte, 4)
                binary.BigEndian.PutUint32(id, i)
 
                id := make([]byte, 4)
                binary.BigEndian.PutUint32(id, i)
 
-               newArt := NewsArtList{
+               newsArts = append(newsArts, NewsArtList{
                        ID:          [4]byte(id),
                        TimeStamp:   art.Date,
                        ParentID:    art.ParentArt,
                        Title:       []byte(art.Title),
                        Poster:      []byte(art.Poster),
                        ArticleSize: art.DataSize(),
                        ID:          [4]byte(id),
                        TimeStamp:   art.Date,
                        ParentID:    art.ParentArt,
                        Title:       []byte(art.Title),
                        Poster:      []byte(art.Poster),
                        ArticleSize: art.DataSize(),
-               }
-
-               newsArts = append(newsArts, newArt)
+               })
        }
 
        }
 
-       sort.Sort(byID(newsArts))
+       // Sort the articles by ID.  This is important for displaying the message threading correctly on the client side.
+       slices.SortFunc(newsArts, func(a, b NewsArtList) int {
+               return cmp.Compare(
+                       binary.BigEndian.Uint32(a.ID[:]),
+                       binary.BigEndian.Uint32(b.ID[:]),
+               )
+       })
 
        for _, v := range newsArts {
                b, err := io.ReadAll(&v)
 
        for _, v := range newsArts {
                b, err := io.ReadAll(&v)
@@ -71,7 +83,7 @@ func (newscat *NewsCategoryListData15) GetNewsArtListData() NewsArtListData {
        }
 }
 
        }
 }
 
-// NewsArtData represents single news article
+// NewsArtData represents an individual news article.
 type NewsArtData struct {
        Title         string  `yaml:"Title"`
        Poster        string  `yaml:"Poster"`
 type NewsArtData struct {
        Title         string  `yaml:"Title"`
        Poster        string  `yaml:"Poster"`
@@ -80,19 +92,19 @@ type NewsArtData struct {
        NextArt       [4]byte `yaml:"NextArt,flow"`
        ParentArt     [4]byte `yaml:"ParentArt,flow"`
        FirstChildArt [4]byte `yaml:"FirstChildArtArt,flow"`
        NextArt       [4]byte `yaml:"NextArt,flow"`
        ParentArt     [4]byte `yaml:"ParentArt,flow"`
        FirstChildArt [4]byte `yaml:"FirstChildArtArt,flow"`
-       DataFlav      []byte  `yaml:"-"` // "text/plain"
+       DataFlav      []byte  `yaml:"-"` // MIME type string.  Always "text/plain".
        Data          string  `yaml:"Data"`
 }
 
        Data          string  `yaml:"Data"`
 }
 
-func (art *NewsArtData) DataSize() []byte {
+func (art *NewsArtData) DataSize() [2]byte {
        dataLen := make([]byte, 2)
        binary.BigEndian.PutUint16(dataLen, uint16(len(art.Data)))
 
        dataLen := make([]byte, 2)
        binary.BigEndian.PutUint16(dataLen, uint16(len(art.Data)))
 
-       return dataLen
+       return [2]byte(dataLen)
 }
 
 type NewsArtListData struct {
 }
 
 type NewsArtListData struct {
-       ID          [4]byte `yaml:"ID"`
+       ID          [4]byte `yaml:"Type"`
        Name        []byte  `yaml:"Name"`
        Description []byte  `yaml:"Description"` // not used?
        NewsArtList []byte  // List of articles                 Optional (if article count > 0)
        Name        []byte  `yaml:"Name"`
        Description []byte  `yaml:"Description"` // not used?
        NewsArtList []byte  // List of articles                 Optional (if article count > 0)
@@ -138,23 +150,11 @@ type NewsArtList struct {
        Poster     []byte
        FlavorList []NewsFlavorList
        // Flavor list…                       Optional (if flavor count > 0)
        Poster     []byte
        FlavorList []NewsFlavorList
        // Flavor list…                       Optional (if flavor count > 0)
-       ArticleSize []byte // Size 2
+       ArticleSize [2]byte // Size 2
 
        readOffset int // Internal offset to track read progress
 }
 
 
        readOffset int // Internal offset to track read progress
 }
 
-type byID []NewsArtList
-
-func (s byID) Len() int {
-       return len(s)
-}
-func (s byID) Swap(i, j int) {
-       s[i], s[j] = s[j], s[i]
-}
-func (s byID) Less(i, j int) bool {
-       return binary.BigEndian.Uint32(s[i].ID[:]) < binary.BigEndian.Uint32(s[j].ID[:])
-}
-
 var (
        NewsFlavorLen = []byte{0x0a}
        NewsFlavor    = []byte("text/plain")
 var (
        NewsFlavorLen = []byte{0x0a}
        NewsFlavor    = []byte("text/plain")
@@ -173,7 +173,7 @@ func (nal *NewsArtList) Read(p []byte) (int, error) {
                nal.Poster,
                NewsFlavorLen,
                NewsFlavor,
                nal.Poster,
                NewsFlavorLen,
                NewsFlavor,
-               nal.ArticleSize,
+               nal.ArticleSize[:],
        )
 
        if nal.readOffset >= len(out) {
        )
 
        if nal.readOffset >= len(out) {
@@ -200,22 +200,24 @@ func (newscat *NewsCategoryListData15) Read(p []byte) (int, error) {
                newscat.Type[:],
                count,
        )
                newscat.Type[:],
                count,
        )
-
-       // If type is category
-       if newscat.Type == [2]byte{0, 3} {
-               out = append(out, newscat.GUID[:]...)
-               out = append(out, newscat.AddSN[:]...)
-               out = append(out, newscat.DeleteSN[:]...)
+       if newscat.Type == NewsCategory {
+               out = slices.Concat(out,
+                       newscat.GUID[:],
+                       newscat.AddSN[:],
+                       newscat.DeleteSN[:],
+               )
        }
        }
-
-       out = append(out, newscat.nameLen()...)
-       out = append(out, []byte(newscat.Name)...)
+       out = slices.Concat(out,
+               newscat.nameLen(),
+               []byte(newscat.Name),
+       )
 
        if newscat.readOffset >= len(out) {
                return 0, io.EOF // All bytes have been read
        }
 
        n := copy(p, out)
 
        if newscat.readOffset >= len(out) {
                return 0, io.EOF // All bytes have been read
        }
 
        n := copy(p, out)
+
        newscat.readOffset = n
 
        return n, nil
        newscat.readOffset = n
 
        return n, nil
@@ -225,30 +227,12 @@ func (newscat *NewsCategoryListData15) nameLen() []byte {
        return []byte{uint8(len(newscat.Name))}
 }
 
        return []byte{uint8(len(newscat.Name))}
 }
 
-// TODO: re-implement as bufio.Scanner interface
-func ReadNewsPath(newsPath []byte) []string {
-       if len(newsPath) == 0 {
-               return []string{}
+// newsPathScanner implements bufio.SplitFunc for parsing incoming byte slices into complete tokens
+func newsPathScanner(data []byte, _ bool) (advance int, token []byte, err error) {
+       if len(data) < 3 {
+               return 0, nil, nil
        }
        }
-       pathCount := binary.BigEndian.Uint16(newsPath[0:2])
-
-       pathData := newsPath[2:]
-       var paths []string
-
-       for i := uint16(0); i < pathCount; i++ {
-               pathLen := pathData[2]
-               paths = append(paths, string(pathData[3:3+pathLen]))
 
 
-               pathData = pathData[pathLen+3:]
-       }
-
-       return paths
-}
-
-func (s *Server) GetNewsCatByPath(paths []string) map[string]NewsCategoryListData15 {
-       cats := s.ThreadedNews.Categories
-       for _, path := range paths {
-               cats = cats[path].SubCats
-       }
-       return cats
+       advance = 3 + int(data[2])
+       return advance, data[3:advance], nil
 }
 }
index d1b043edc333762066a694d62c61846757ac04e0..588c9796ce1dac31496e2377cba16320a7349f11 100644 (file)
@@ -2,10 +2,61 @@ package hotline
 
 import (
        "github.com/stretchr/testify/assert"
 
 import (
        "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
        "io"
        "testing"
 )
 
        "io"
        "testing"
 )
 
+type mockThreadNewsMgr struct {
+       mock.Mock
+}
+
+func (m *mockThreadNewsMgr) ListArticles(newsPath []string) NewsArtListData {
+       args := m.Called(newsPath)
+
+       return args.Get(0).(NewsArtListData)
+}
+
+func (m *mockThreadNewsMgr) GetArticle(newsPath []string, articleID uint32) *NewsArtData {
+       args := m.Called(newsPath, articleID)
+
+       return args.Get(0).(*NewsArtData)
+}
+func (m *mockThreadNewsMgr) DeleteArticle(newsPath []string, articleID uint32, recursive bool) error {
+       args := m.Called(newsPath, articleID, recursive)
+
+       return args.Error(0)
+}
+
+func (m *mockThreadNewsMgr) PostArticle(newsPath []string, parentArticleID uint32, article NewsArtData) error {
+       args := m.Called(newsPath, parentArticleID, article)
+
+       return args.Error(0)
+}
+func (m *mockThreadNewsMgr) CreateGrouping(newsPath []string, name string, itemType [2]byte) error {
+       args := m.Called(newsPath, name, itemType)
+
+       return args.Error(0)
+}
+
+func (m *mockThreadNewsMgr) GetCategories(paths []string) []NewsCategoryListData15 {
+       args := m.Called(paths)
+
+       return args.Get(0).([]NewsCategoryListData15)
+}
+
+func (m *mockThreadNewsMgr) NewsItem(newsPath []string) NewsCategoryListData15 {
+       args := m.Called(newsPath)
+
+       return args.Get(0).(NewsCategoryListData15)
+}
+
+func (m *mockThreadNewsMgr) DeleteNewsItem(newsPath []string) error {
+       args := m.Called(newsPath)
+
+       return args.Error(0)
+}
+
 func TestNewsCategoryListData15_MarshalBinary(t *testing.T) {
        type fields struct {
                Type     [2]byte
 func TestNewsCategoryListData15_MarshalBinary(t *testing.T) {
        type fields struct {
                Type     [2]byte
index 0ee2dd7322d3dc6a0c4317a30c33a26b19dcc0ff..62322539eed67e01f85f9d69b07f0defc8577756 100644 (file)
@@ -8,7 +8,6 @@ import (
        "encoding/binary"
        "errors"
        "fmt"
        "encoding/binary"
        "errors"
        "fmt"
-       "github.com/go-playground/validator/v10"
        "golang.org/x/text/encoding/charmap"
        "gopkg.in/yaml.v3"
        "io"
        "golang.org/x/text/encoding/charmap"
        "gopkg.in/yaml.v3"
        "io"
@@ -16,11 +15,9 @@ import (
        "log/slog"
        "net"
        "os"
        "log/slog"
        "net"
        "os"
-       "path"
        "path/filepath"
        "strings"
        "sync"
        "path/filepath"
        "strings"
        "sync"
-       "sync/atomic"
        "time"
 )
 
        "time"
 )
 
@@ -41,54 +38,94 @@ var txtEncoder = charmap.Macintosh.NewEncoder()
 type Server struct {
        NetInterface string
        Port         int
 type Server struct {
        NetInterface string
        Port         int
-       Accounts     map[string]*Account
-       Agreement    []byte
 
 
-       Clients       map[[2]byte]*ClientConn
-       fileTransfers map[[4]byte]*FileTransfer
-
-       Config    *Config
+       Config    Config
        ConfigDir string
        Logger    *slog.Logger
        ConfigDir string
        Logger    *slog.Logger
-       banner    []byte
 
 
-       PrivateChatsMu sync.Mutex
-       PrivateChats   map[[4]byte]*PrivateChat
-
-       nextClientID  atomic.Uint32
        TrackerPassID [4]byte
 
        TrackerPassID [4]byte
 
-       statsMu sync.Mutex
-       Stats   *Stats
+       Stats Counter
 
        FS FileStore // Storage backend to use for File storage
 
        outbox chan Transaction
 
        FS FileStore // Storage backend to use for File storage
 
        outbox chan Transaction
-       mux    sync.Mutex
 
 
-       threadedNewsMux sync.Mutex
-       ThreadedNews    *ThreadedNews
+       // TODO
+       Agreement []byte
+       banner    []byte
+       // END TODO
 
 
-       flatNewsMux sync.Mutex
-       FlatNews    []byte
+       FileTransferMgr FileTransferMgr
+       ChatMgr         ChatManager
+       ClientMgr       ClientManager
+       AccountManager  AccountManager
+       ThreadedNewsMgr ThreadedNewsMgr
+       BanList         BanMgr
 
 
-       banListMU sync.Mutex
-       banList   map[string]*time.Time
+       MessageBoard io.ReadWriteSeeker
 }
 
 }
 
-func (s *Server) CurrentStats() Stats {
-       s.statsMu.Lock()
-       defer s.statsMu.Unlock()
+// NewServer constructs a new Server from a config dir
+func NewServer(config Config, configDir, netInterface string, netPort int, logger *slog.Logger, fs FileStore) (*Server, error) {
+       server := Server{
+               NetInterface:    netInterface,
+               Port:            netPort,
+               Config:          config,
+               ConfigDir:       configDir,
+               Logger:          logger,
+               outbox:          make(chan Transaction),
+               Stats:           NewStats(),
+               FS:              fs,
+               ChatMgr:         NewMemChatManager(),
+               ClientMgr:       NewMemClientMgr(),
+               FileTransferMgr: NewMemFileTransferMgr(),
+       }
+
+       // generate a new random passID for tracker registration
+       _, err := rand.Read(server.TrackerPassID[:])
+       if err != nil {
+               return nil, err
+       }
+
+       server.Agreement, err = os.ReadFile(filepath.Join(configDir, agreementFile))
+       if err != nil {
+               return nil, err
+       }
+
+       server.AccountManager, err = NewYAMLAccountManager(filepath.Join(configDir, "Users/"))
+       if err != nil {
+               return nil, fmt.Errorf("error loading accounts: %w", err)
+       }
+
+       // If the FileRoot is an absolute path, use it, otherwise treat as a relative path to the config dir.
+       if !filepath.IsAbs(server.Config.FileRoot) {
+               server.Config.FileRoot = filepath.Join(configDir, server.Config.FileRoot)
+       }
+
+       server.banner, err = os.ReadFile(filepath.Join(server.ConfigDir, server.Config.BannerFile))
+       if err != nil {
+               return nil, fmt.Errorf("error opening banner: %w", err)
+       }
+
+       if server.Config.EnableTrackerRegistration {
+               server.Logger.Info(
+                       "Tracker registration enabled",
+                       "frequency", fmt.Sprintf("%vs", trackerUpdateFrequency),
+                       "trackers", server.Config.Trackers,
+               )
+
+               go server.registerWithTrackers()
+       }
 
 
-       stats := s.Stats
-       stats.CurrentlyConnected = len(s.Clients)
+       // Start Client Keepalive go routine
+       go server.keepaliveHandler()
 
 
-       return *stats
+       return &server, nil
 }
 
 }
 
-type PrivateChat struct {
-       Subject    string
-       ClientConn map[[2]byte]*ClientConn
+func (s *Server) CurrentStats() map[string]interface{} {
+       return s.Stats.Values()
 }
 
 func (s *Server) ListenAndServe(ctx context.Context) error {
 }
 
 func (s *Server) ListenAndServe(ctx context.Context) error {
@@ -142,11 +179,9 @@ func (s *Server) ServeFileTransfers(ctx context.Context, ln net.Listener) error
 }
 
 func (s *Server) sendTransaction(t Transaction) error {
 }
 
 func (s *Server) sendTransaction(t Transaction) error {
-       s.mux.Lock()
-       client, ok := s.Clients[t.clientID]
-       s.mux.Unlock()
+       client := s.ClientMgr.Get(t.clientID)
 
 
-       if !ok || client == nil {
+       if client == nil {
                return nil
        }
 
                return nil
        }
 
@@ -200,126 +235,40 @@ const (
        agreementFile = "Agreement.txt"
 )
 
        agreementFile = "Agreement.txt"
 )
 
-// NewServer constructs a new Server from a config dir
-// TODO: move config file reads out of this function
-func NewServer(configDir, netInterface string, netPort int, logger *slog.Logger, fs FileStore) (*Server, error) {
-       server := Server{
-               NetInterface:  netInterface,
-               Port:          netPort,
-               Accounts:      make(map[string]*Account),
-               Config:        new(Config),
-               Clients:       make(map[[2]byte]*ClientConn),
-               fileTransfers: make(map[[4]byte]*FileTransfer),
-               PrivateChats:  make(map[[4]byte]*PrivateChat),
-               ConfigDir:     configDir,
-               Logger:        logger,
-               outbox:        make(chan Transaction),
-               Stats:         &Stats{Since: time.Now()},
-               ThreadedNews:  &ThreadedNews{},
-               FS:            fs,
-               banList:       make(map[string]*time.Time),
-       }
-
-       var err error
-
-       // generate a new random passID for tracker registration
-       if _, err := rand.Read(server.TrackerPassID[:]); err != nil {
-               return nil, err
-       }
-
-       server.Agreement, err = os.ReadFile(filepath.Join(configDir, agreementFile))
-       if err != nil {
-               return nil, err
-       }
-
-       if server.FlatNews, err = os.ReadFile(filepath.Join(configDir, "MessageBoard.txt")); err != nil {
-               return nil, err
-       }
-
-       // try to load the ban list, but ignore errors as this file may not be present or may be empty
-       //_ = server.loadBanList(filepath.Join(configDir, "Banlist.yaml"))
-
-       _ = loadFromYAMLFile(filepath.Join(configDir, "Banlist.yaml"), &server.banList)
-
-       err = loadFromYAMLFile(filepath.Join(configDir, "ThreadedNews.yaml"), &server.ThreadedNews)
-       if err != nil {
-               return nil, fmt.Errorf("error loading threaded news: %w", err)
-       }
-
-       err = server.loadConfig(filepath.Join(configDir, "config.yaml"))
-       if err != nil {
-               return nil, fmt.Errorf("error loading config: %w", err)
-       }
-
-       if err := server.loadAccounts(filepath.Join(configDir, "Users/")); err != nil {
-               return nil, err
-       }
-
-       // If the FileRoot is an absolute path, use it, otherwise treat as a relative path to the config dir.
-       if !filepath.IsAbs(server.Config.FileRoot) {
-               server.Config.FileRoot = filepath.Join(configDir, server.Config.FileRoot)
-       }
-
-       server.banner, err = os.ReadFile(filepath.Join(server.ConfigDir, server.Config.BannerFile))
-       if err != nil {
-               return nil, fmt.Errorf("error opening banner: %w", err)
-       }
-
-       if server.Config.EnableTrackerRegistration {
-               server.Logger.Info(
-                       "Tracker registration enabled",
-                       "frequency", fmt.Sprintf("%vs", trackerUpdateFrequency),
-                       "trackers", server.Config.Trackers,
-               )
-
-               go func() {
-                       for {
-                               tr := &TrackerRegistration{
-                                       UserCount:   server.userCount(),
-                                       PassID:      server.TrackerPassID,
-                                       Name:        server.Config.Name,
-                                       Description: server.Config.Description,
-                               }
-                               binary.BigEndian.PutUint16(tr.Port[:], uint16(server.Port))
-                               for _, t := range server.Config.Trackers {
-                                       if err := register(&RealDialer{}, t, tr); err != nil {
-                                               server.Logger.Error("unable to register with tracker %v", "error", err)
-                                       }
-                                       server.Logger.Debug("Sent Tracker registration", "addr", t)
-                               }
-
-                               time.Sleep(trackerUpdateFrequency * time.Second)
+func (s *Server) registerWithTrackers() {
+       for {
+               tr := &TrackerRegistration{
+                       UserCount:   len(s.ClientMgr.List()),
+                       PassID:      s.TrackerPassID,
+                       Name:        s.Config.Name,
+                       Description: s.Config.Description,
+               }
+               binary.BigEndian.PutUint16(tr.Port[:], uint16(s.Port))
+               for _, t := range s.Config.Trackers {
+                       if err := register(&RealDialer{}, t, tr); err != nil {
+                               s.Logger.Error(fmt.Sprintf("unable to register with tracker %v", t), "error", err)
                        }
                        }
-               }()
-       }
-
-       // Start Client Keepalive go routine
-       go server.keepaliveHandler()
-
-       return &server, nil
-}
-
-func (s *Server) userCount() int {
-       s.mux.Lock()
-       defer s.mux.Unlock()
+               }
 
 
-       return len(s.Clients)
+               time.Sleep(trackerUpdateFrequency * time.Second)
+       }
 }
 
 }
 
+// keepaliveHandler
 func (s *Server) keepaliveHandler() {
        for {
                time.Sleep(idleCheckInterval * time.Second)
 func (s *Server) keepaliveHandler() {
        for {
                time.Sleep(idleCheckInterval * time.Second)
-               s.mux.Lock()
 
 
-               for _, c := range s.Clients {
+               for _, c := range s.ClientMgr.List() {
+                       c.mu.Lock()
+
                        c.IdleTime += idleCheckInterval
                        c.IdleTime += idleCheckInterval
-                       if c.IdleTime > userIdleSeconds && !c.Idle {
-                               c.Idle = true
 
 
-                               c.flagsMU.Lock()
+                       // Check if the user
+                       if c.IdleTime > userIdleSeconds && !c.Flags.IsSet(UserFlagAway) {
                                c.Flags.Set(UserFlagAway, 1)
                                c.Flags.Set(UserFlagAway, 1)
-                               c.flagsMU.Unlock()
-                               c.sendAll(
+
+                               c.SendAll(
                                        TranNotifyChangeUser,
                                        NewField(FieldUserID, c.ID[:]),
                                        NewField(FieldUserFlags, c.Flags[:]),
                                        TranNotifyChangeUser,
                                        NewField(FieldUserID, c.ID[:]),
                                        NewField(FieldUserFlags, c.Flags[:]),
@@ -327,169 +276,24 @@ func (s *Server) keepaliveHandler() {
                                        NewField(FieldUserIconID, c.Icon),
                                )
                        }
                                        NewField(FieldUserIconID, c.Icon),
                                )
                        }
+                       c.mu.Unlock()
                }
                }
-               s.mux.Unlock()
-       }
-}
-
-func (s *Server) writeBanList() error {
-       s.banListMU.Lock()
-       defer s.banListMU.Unlock()
-
-       out, err := yaml.Marshal(s.banList)
-       if err != nil {
-               return err
        }
        }
-       err = os.WriteFile(
-               filepath.Join(s.ConfigDir, "Banlist.yaml"),
-               out,
-               0666,
-       )
-       return err
-}
-
-func (s *Server) writeThreadedNews() error {
-       s.threadedNewsMux.Lock()
-       defer s.threadedNewsMux.Unlock()
-
-       out, err := yaml.Marshal(s.ThreadedNews)
-       if err != nil {
-               return err
-       }
-       err = s.FS.WriteFile(
-               filepath.Join(s.ConfigDir, "ThreadedNews.yaml"),
-               out,
-               0666,
-       )
-       return err
 }
 
 func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *ClientConn {
 }
 
 func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *ClientConn {
-       s.mux.Lock()
-       defer s.mux.Unlock()
-
        clientConn := &ClientConn{
                Icon:       []byte{0, 0}, // TODO: make array type
                Connection: conn,
                Server:     s,
                RemoteAddr: remoteAddr,
        clientConn := &ClientConn{
                Icon:       []byte{0, 0}, // TODO: make array type
                Connection: conn,
                Server:     s,
                RemoteAddr: remoteAddr,
-               transfers: map[int]map[[4]byte]*FileTransfer{
-                       FileDownload:   {},
-                       FileUpload:     {},
-                       FolderDownload: {},
-                       FolderUpload:   {},
-                       bannerDownload: {},
-               },
-       }
-
-       s.nextClientID.Add(1)
-
-       binary.BigEndian.PutUint16(clientConn.ID[:], uint16(s.nextClientID.Load()))
-       s.Clients[clientConn.ID] = clientConn
-
-       return clientConn
-}
-
-// NewUser creates a new user account entry in the server map and config file
-func (s *Server) NewUser(login, name, password string, access accessBitmap) error {
-       s.mux.Lock()
-       defer s.mux.Unlock()
-
-       account := NewAccount(login, name, password, access)
-
-       // Create account file, returning an error if one already exists.
-       file, err := os.OpenFile(
-               filepath.Join(s.ConfigDir, "Users", path.Join("/", login)+".yaml"),
-               os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644,
-       )
-       if err != nil {
-               return fmt.Errorf("error creating account file: %w", err)
-       }
-       defer file.Close()
-
-       b, err := yaml.Marshal(account)
-       if err != nil {
-               return err
-       }
-
-       _, err = file.Write(b)
-       if err != nil {
-               return fmt.Errorf("error writing account file: %w", err)
-       }
-
-       s.Accounts[login] = account
-
-       return nil
-}
-
-func (s *Server) UpdateUser(login, newLogin, name, password string, access accessBitmap) error {
-       s.mux.Lock()
-       defer s.mux.Unlock()
-
-       // If the login has changed, rename the account file.
-       if login != newLogin {
-               err := os.Rename(
-                       filepath.Join(s.ConfigDir, "Users", path.Join("/", login)+".yaml"),
-                       filepath.Join(s.ConfigDir, "Users", path.Join("/", newLogin)+".yaml"),
-               )
-               if err != nil {
-                       return fmt.Errorf("error renaming account file: %w", err)
-               }
-               s.Accounts[newLogin] = s.Accounts[login]
-               s.Accounts[newLogin].Login = newLogin
-               delete(s.Accounts, login)
-       }
-
-       account := s.Accounts[newLogin]
-       account.Access = access
-       account.Name = name
-       account.Password = password
-
-       out, err := yaml.Marshal(&account)
-       if err != nil {
-               return err
-       }
-
-       if err := os.WriteFile(filepath.Join(s.ConfigDir, "Users", newLogin+".yaml"), out, 0666); err != nil {
-               return fmt.Errorf("error writing account file: %w", err)
-       }
-
-       return nil
-}
 
 
-// DeleteUser deletes the user account
-func (s *Server) DeleteUser(login string) error {
-       s.mux.Lock()
-       defer s.mux.Unlock()
-
-       err := s.FS.Remove(filepath.Join(s.ConfigDir, "Users", path.Join("/", login)+".yaml"))
-       if err != nil {
-               return err
+               ClientFileTransferMgr: NewClientFileTransferMgr(),
        }
 
        }
 
-       delete(s.Accounts, login)
+       s.ClientMgr.Add(clientConn)
 
 
-       return nil
-}
-
-func (s *Server) connectedUsers() []Field {
-       //s.mux.Lock()
-       //defer s.mux.Unlock()
-
-       var connectedUsers []Field
-       for _, c := range sortedClients(s.Clients) {
-               b, err := io.ReadAll(&User{
-                       ID:    c.ID,
-                       Icon:  c.Icon,
-                       Flags: c.Flags[:],
-                       Name:  string(c.UserName),
-               })
-               if err != nil {
-                       return nil
-               }
-               connectedUsers = append(connectedUsers, NewField(FieldUsernameWithInfo, b))
-       }
-       return connectedUsers
+       return clientConn
 }
 
 // loadFromYAMLFile loads data from a YAML file into the provided data structure.
 }
 
 // loadFromYAMLFile loads data from a YAML file into the provided data structure.
@@ -504,48 +308,6 @@ func loadFromYAMLFile(path string, data interface{}) error {
        return decoder.Decode(data)
 }
 
        return decoder.Decode(data)
 }
 
-// loadAccounts loads account data from disk
-func (s *Server) loadAccounts(userDir string) error {
-       matches, err := filepath.Glob(filepath.Join(userDir, "*.yaml"))
-       if err != nil {
-               return err
-       }
-
-       if len(matches) == 0 {
-               return fmt.Errorf("no accounts found in directory: %s", userDir)
-       }
-
-       for _, file := range matches {
-               var account Account
-               if err = loadFromYAMLFile(file, &account); err != nil {
-                       return fmt.Errorf("error loading account %s: %w", file, err)
-               }
-
-               s.Accounts[account.Login] = &account
-       }
-       return nil
-}
-
-func (s *Server) loadConfig(path string) error {
-       fh, err := s.FS.Open(path)
-       if err != nil {
-               return err
-       }
-
-       decoder := yaml.NewDecoder(fh)
-       err = decoder.Decode(s.Config)
-       if err != nil {
-               return err
-       }
-
-       validate := validator.New()
-       err = validate.Struct(s.Config)
-       if err != nil {
-               return err
-       }
-       return nil
-}
-
 func sendBanMessage(rwc io.Writer, message string) {
        t := NewTransaction(
                TranServerMsg,
 func sendBanMessage(rwc io.Writer, message string) {
        t := NewTransaction(
                TranServerMsg,
@@ -563,7 +325,7 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser
 
        // Check if remoteAddr is present in the ban list
        ipAddr := strings.Split(remoteAddr, ":")[0]
 
        // Check if remoteAddr is present in the ban list
        ipAddr := strings.Split(remoteAddr, ":")[0]
-       if banUntil, ok := s.banList[ipAddr]; ok {
+       if isBanned, banUntil := s.BanList.IsBanned(ipAddr); isBanned {
                // permaban
                if banUntil == nil {
                        sendBanMessage(rwc, "You are permanently banned on this server")
                // permaban
                if banUntil == nil {
                        sendBanMessage(rwc, "You are permanently banned on this server")
@@ -605,12 +367,12 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser
        encodedPassword := clientLogin.GetField(FieldUserPassword).Data
        c.Version = clientLogin.GetField(FieldVersion).Data
 
        encodedPassword := clientLogin.GetField(FieldUserPassword).Data
        c.Version = clientLogin.GetField(FieldVersion).Data
 
-       login := string(encodeString(clientLogin.GetField(FieldUserLogin).Data))
+       login := clientLogin.GetField(FieldUserLogin).DecodeObfuscatedString()
        if login == "" {
                login = GuestAccount
        }
 
        if login == "" {
                login = GuestAccount
        }
 
-       c.logger = s.Logger.With("remoteAddr", remoteAddr, "login", login)
+       c.logger = s.Logger.With("ip", ipAddr, "login", login)
 
        // If authentication fails, send error reply and close connection
        if !c.Authenticate(login, encodedPassword) {
 
        // If authentication fails, send error reply and close connection
        if !c.Authenticate(login, encodedPassword) {
@@ -630,19 +392,20 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser
                c.Icon = clientLogin.GetField(FieldUserIconID).Data
        }
 
                c.Icon = clientLogin.GetField(FieldUserIconID).Data
        }
 
-       c.Lock()
-       c.Account = c.Server.Accounts[login]
-       c.Unlock()
+       c.Account = c.Server.AccountManager.Get(login)
+       if c.Account == nil {
+               return nil
+       }
 
        if clientLogin.GetField(FieldUserName).Data != nil {
 
        if clientLogin.GetField(FieldUserName).Data != nil {
-               if c.Authorize(accessAnyName) {
+               if c.Authorize(AccessAnyName) {
                        c.UserName = clientLogin.GetField(FieldUserName).Data
                } else {
                        c.UserName = []byte(c.Account.Name)
                }
        }
 
                        c.UserName = clientLogin.GetField(FieldUserName).Data
                } else {
                        c.UserName = []byte(c.Account.Name)
                }
        }
 
-       if c.Authorize(accessDisconUser) {
+       if c.Authorize(AccessDisconUser) {
                c.Flags.Set(UserFlagAdmin, 1)
        }
 
                c.Flags.Set(UserFlagAdmin, 1)
        }
 
@@ -655,10 +418,10 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser
        // Send user access privs so client UI knows how to behave
        c.Server.outbox <- NewTransaction(TranUserAccess, c.ID, NewField(FieldUserAccess, c.Account.Access[:]))
 
        // Send user access privs so client UI knows how to behave
        c.Server.outbox <- NewTransaction(TranUserAccess, c.ID, NewField(FieldUserAccess, c.Account.Access[:]))
 
-       // Accounts with accessNoAgreement do not receive the server agreement on login.  The behavior is different between
+       // Accounts with AccessNoAgreement do not receive the server agreement on login.  The behavior is different between
        // client versions.  For 1.2.3 client, we do not send TranShowAgreement.  For other client versions, we send
        // TranShowAgreement but with the NoServerAgreement field set to 1.
        // client versions.  For 1.2.3 client, we do not send TranShowAgreement.  For other client versions, we send
        // TranShowAgreement but with the NoServerAgreement field set to 1.
-       if c.Authorize(accessNoAgreement) {
+       if c.Authorize(AccessNoAgreement) {
                // If client version is nil, then the client uses the 1.2.3 login behavior
                if c.Version != nil {
                        c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldNoServerAgreement, []byte{1}))
                // If client version is nil, then the client uses the 1.2.3 login behavior
                if c.Version != nil {
                        c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldNoServerAgreement, []byte{1}))
@@ -677,7 +440,7 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser
 
                // Notify other clients on the server that the new user has logged in.  For 1.5+ clients we don't have this
                // information yet, so we do it in TranAgreed instead
 
                // Notify other clients on the server that the new user has logged in.  For 1.5+ clients we don't have this
                // information yet, so we do it in TranAgreed instead
-               for _, t := range c.notifyOthers(
+               for _, t := range c.NotifyOthers(
                        NewTransaction(
                                TranNotifyChangeUser, [2]byte{0, 0},
                                NewField(FieldUserName, c.UserName),
                        NewTransaction(
                                TranNotifyChangeUser, [2]byte{0, 0},
                                NewField(FieldUserName, c.UserName),
@@ -690,12 +453,12 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser
                }
        }
 
                }
        }
 
-       c.Server.mux.Lock()
-       c.Server.Stats.ConnectionCounter += 1
-       if len(s.Clients) > c.Server.Stats.ConnectionPeak {
-               c.Server.Stats.ConnectionPeak = len(s.Clients)
+       c.Server.Stats.Increment(StatConnectionCounter, StatCurrentlyConnected)
+       defer c.Server.Stats.Decrement(StatCurrentlyConnected)
+
+       if len(s.ClientMgr.List()) > c.Server.Stats.Get(StatConnectionPeak) {
+               c.Server.Stats.Set(StatConnectionPeak, len(s.ClientMgr.List()))
        }
        }
-       c.Server.mux.Unlock()
 
        // Scan for new transactions and handle them as they come in.
        for scanner.Scan() {
 
        // Scan for new transactions and handle them as they come in.
        for scanner.Scan() {
@@ -713,25 +476,6 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser
        return nil
 }
 
        return nil
 }
 
-func (s *Server) NewPrivateChat(cc *ClientConn) [4]byte {
-       s.PrivateChatsMu.Lock()
-       defer s.PrivateChatsMu.Unlock()
-
-       var randID [4]byte
-       _, _ = rand.Read(randID[:])
-
-       s.PrivateChats[randID] = &PrivateChat{
-               ClientConn: make(map[[2]byte]*ClientConn),
-       }
-       s.PrivateChats[randID].ClientConn[cc.ID] = cc
-
-       return randID
-}
-
-const dlFldrActionSendFile = 1
-const dlFldrActionResumeFile = 2
-const dlFldrActionNextFile = 3
-
 // handleFileTransfer receives a client net.Conn from the file transfer server, performs the requested transfer type, then closes the connection
 func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) error {
        defer dontPanic(s.Logger)
 // handleFileTransfer receives a client net.Conn from the file transfer server, performs the requested transfer type, then closes the connection
 func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) error {
        defer dontPanic(s.Logger)
@@ -742,10 +486,13 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro
                return fmt.Errorf("error reading file transfer: %w", err)
        }
 
                return fmt.Errorf("error reading file transfer: %w", err)
        }
 
+       fileTransfer := s.FileTransferMgr.Get(t.ReferenceNumber)
+       if fileTransfer == nil {
+               return errors.New("invalid transaction ID")
+       }
+
        defer func() {
        defer func() {
-               s.mux.Lock()
-               delete(s.fileTransfers, t.ReferenceNumber)
-               s.mux.Unlock()
+               s.FileTransferMgr.Delete(t.ReferenceNumber)
 
                // Wait a few seconds before closing the connection: this is a workaround for problems
                // observed with Windows clients where the client must initiate close of the TCP connection before
 
                // Wait a few seconds before closing the connection: this is a workaround for problems
                // observed with Windows clients where the client must initiate close of the TCP connection before
@@ -753,19 +500,6 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro
                time.Sleep(3 * time.Second)
        }()
 
                time.Sleep(3 * time.Second)
        }()
 
-       s.mux.Lock()
-       fileTransfer, ok := s.fileTransfers[t.ReferenceNumber]
-       s.mux.Unlock()
-       if !ok {
-               return errors.New("invalid transaction ID")
-       }
-
-       defer func() {
-               fileTransfer.ClientConn.transfersMU.Lock()
-               delete(fileTransfer.ClientConn.transfers[fileTransfer.Type], t.ReferenceNumber)
-               fileTransfer.ClientConn.transfersMU.Unlock()
-       }()
-
        rLogger := s.Logger.With(
                "remoteAddr", ctx.Value(contextKeyReq).(requestCtx).remoteAddr,
                "login", fileTransfer.ClientConn.Account.Login,
        rLogger := s.Logger.With(
                "remoteAddr", ctx.Value(contextKeyReq).(requestCtx).remoteAddr,
                "login", fileTransfer.ClientConn.Account.Login,
@@ -778,26 +512,26 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro
        }
 
        switch fileTransfer.Type {
        }
 
        switch fileTransfer.Type {
-       case bannerDownload:
+       case BannerDownload:
                if _, err := io.Copy(rwc, bytes.NewBuffer(s.banner)); err != nil {
                        return fmt.Errorf("error sending banner: %w", err)
                }
        case FileDownload:
                if _, err := io.Copy(rwc, bytes.NewBuffer(s.banner)); err != nil {
                        return fmt.Errorf("error sending banner: %w", err)
                }
        case FileDownload:
-               s.Stats.DownloadCounter += 1
-               s.Stats.DownloadsInProgress += 1
+               s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
                defer func() {
                defer func() {
-                       s.Stats.DownloadsInProgress -= 1
+                       s.Stats.Decrement(StatDownloadsInProgress)
                }()
 
                err = DownloadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, true)
                if err != nil {
                }()
 
                err = DownloadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, true)
                if err != nil {
-                       return fmt.Errorf("file download error: %w", err)
+                       return fmt.Errorf("file download: %w", err)
                }
 
        case FileUpload:
                }
 
        case FileUpload:
-               s.Stats.UploadCounter += 1
-               s.Stats.UploadsInProgress += 1
-               defer func() { s.Stats.UploadsInProgress -= 1 }()
+               s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
+               defer func() {
+                       s.Stats.Decrement(StatUploadsInProgress)
+               }()
 
                err = UploadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
                if err != nil {
 
                err = UploadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
                if err != nil {
@@ -805,9 +539,10 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro
                }
 
        case FolderDownload:
                }
 
        case FolderDownload:
-               s.Stats.DownloadCounter += 1
-               s.Stats.DownloadsInProgress += 1
-               defer func() { s.Stats.DownloadsInProgress -= 1 }()
+               s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
+               defer func() {
+                       s.Stats.Decrement(StatDownloadsInProgress)
+               }()
 
                err = DownloadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
                if err != nil {
 
                err = DownloadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
                if err != nil {
@@ -815,9 +550,11 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro
                }
 
        case FolderUpload:
                }
 
        case FolderUpload:
-               s.Stats.UploadCounter += 1
-               s.Stats.UploadsInProgress += 1
-               defer func() { s.Stats.UploadsInProgress -= 1 }()
+               s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
+               defer func() {
+                       s.Stats.Decrement(StatUploadsInProgress)
+               }()
+
                rLogger.Info(
                        "Folder upload started",
                        "dstPath", fullPath,
                rLogger.Info(
                        "Folder upload started",
                        "dstPath", fullPath,
index 06e7771eef1342d84402235206d2fb489f576aed..45d32d4dc2285f121e233f157080321814fee02d 100644 (file)
@@ -25,11 +25,11 @@ func assertTransferBytesEqual(t *testing.T, wantHexDump string, got []byte) bool
                return true
        }
 
                return true
        }
 
-       var clean []byte
-       clean = append(clean, got[:92]...)         // keep the first 92 bytes
-       clean = append(clean, make([]byte, 16)...) // replace the next 16 bytes for create/modify timestamps
-       clean = append(clean, got[108:]...)        // keep the rest
-
+       clean := slices.Concat(
+               got[:92],
+               make([]byte, 16),
+               got[108:],
+       )
        return assert.Equal(t, wantHexDump, hex.Dump(clean))
 }
 
        return assert.Equal(t, wantHexDump, hex.Dump(clean))
 }
 
@@ -40,7 +40,7 @@ var tranSortFunc = func(a, b Transaction) int {
        )
 }
 
        )
 }
 
-// tranAssertEqual compares equality of transactions slices after stripping out the random transaction ID
+// tranAssertEqual compares equality of transactions slices after stripping out the random transaction Type
 func tranAssertEqual(t *testing.T, tran1, tran2 []Transaction) bool {
        var newT1 []Transaction
        var newT2 []Transaction
 func tranAssertEqual(t *testing.T, tran1, tran2 []Transaction) bool {
        var newT1 []Transaction
        var newT2 []Transaction
@@ -49,12 +49,13 @@ func tranAssertEqual(t *testing.T, tran1, tran2 []Transaction) bool {
                trans.ID = [4]byte{0, 0, 0, 0}
                var fs []Field
                for _, field := range trans.Fields {
                trans.ID = [4]byte{0, 0, 0, 0}
                var fs []Field
                for _, field := range trans.Fields {
-                       if field.ID == [2]byte{0x00, 0x6b} { // FieldRefNum
+                       if field.Type == FieldRefNum { // FieldRefNum
                                continue
                        }
                                continue
                        }
-                       if field.ID == [2]byte{0x00, 0x72} { // FieldChatID
+                       if field.Type == FieldChatID { // FieldChatID
                                continue
                        }
                                continue
                        }
+
                        fs = append(fs, field)
                }
                trans.Fields = fs
                        fs = append(fs, field)
                }
                trans.Fields = fs
@@ -65,12 +66,13 @@ func tranAssertEqual(t *testing.T, tran1, tran2 []Transaction) bool {
                trans.ID = [4]byte{0, 0, 0, 0}
                var fs []Field
                for _, field := range trans.Fields {
                trans.ID = [4]byte{0, 0, 0, 0}
                var fs []Field
                for _, field := range trans.Fields {
-                       if field.ID == [2]byte{0x00, 0x6b} { // FieldRefNum
+                       if field.Type == FieldRefNum { // FieldRefNum
                                continue
                        }
                                continue
                        }
-                       if field.ID == [2]byte{0x00, 0x72} { // FieldChatID
+                       if field.Type == FieldChatID { // FieldChatID
                                continue
                        }
                                continue
                        }
+
                        fs = append(fs, field)
                }
                trans.Fields = fs
                        fs = append(fs, field)
                }
                trans.Fields = fs
index a29a4f8299d830dbcb30fa5537f8c3259fde1d85..0487d2e586a650b7e268138f99f86491586767a4 100644 (file)
@@ -8,7 +8,6 @@ import (
        "io"
        "log/slog"
        "os"
        "io"
        "log/slog"
        "os"
-       "sync"
        "testing"
 )
 
        "testing"
 )
 
@@ -27,21 +26,13 @@ func (mrw mockReadWriter) Write(p []byte) (n int, err error) {
 
 func TestServer_handleFileTransfer(t *testing.T) {
        type fields struct {
 
 func TestServer_handleFileTransfer(t *testing.T) {
        type fields struct {
-               Port          int
-               Accounts      map[string]*Account
-               Agreement     []byte
-               Clients       map[[2]byte]*ClientConn
-               ThreadedNews  *ThreadedNews
-               fileTransfers map[[4]byte]*FileTransfer
-               Config        *Config
-               ConfigDir     string
-               Logger        *slog.Logger
-               PrivateChats  map[uint32]*PrivateChat
-               NextGuestID   *uint16
-               TrackerPassID [4]byte
-               Stats         *Stats
-               FS            FileStore
-               FlatNews      []byte
+               ThreadedNews    *ThreadedNews
+               FileTransferMgr FileTransferMgr
+               Config          Config
+               ConfigDir       string
+               Stats           *Stats
+               Logger          *slog.Logger
+               FS              FileStore
        }
        type args struct {
                ctx context.Context
        }
        type args struct {
                ctx context.Context
@@ -79,7 +70,10 @@ func TestServer_handleFileTransfer(t *testing.T) {
                        wantErr: assert.Error,
                },
                {
                        wantErr: assert.Error,
                },
                {
-                       name: "with invalid transfer ID",
+                       name: "with invalid transfer Type",
+                       fields: fields{
+                               FileTransferMgr: NewMemFileTransferMgr(),
+                       },
                        args: args{
                                ctx: func() context.Context {
                                        ctx := context.Background()
                        args: args{
                                ctx: func() context.Context {
                                        ctx := context.Background()
@@ -106,31 +100,34 @@ func TestServer_handleFileTransfer(t *testing.T) {
                        name: "file download",
                        fields: fields{
                                FS: &OSFileStore{},
                        name: "file download",
                        fields: fields{
                                FS: &OSFileStore{},
-                               Config: &Config{
+                               Config: Config{
                                        FileRoot: func() string {
                                                path, _ := os.Getwd()
                                                return path + "/test/config/Files"
                                        }()},
                                Logger: NewTestLogger(),
                                        FileRoot: func() string {
                                                path, _ := os.Getwd()
                                                return path + "/test/config/Files"
                                        }()},
                                Logger: NewTestLogger(),
-                               Stats:  &Stats{},
-                               fileTransfers: map[[4]byte]*FileTransfer{
-                                       {0, 0, 0, 5}: {
-                                               refNum:   [4]byte{0, 0, 0, 5},
-                                               Type:     FileDownload,
-                                               FileName: []byte("testfile-8b"),
-                                               FilePath: []byte{},
-                                               ClientConn: &ClientConn{
-                                                       Account: &Account{
-                                                               Login: "foo",
-                                                       },
-                                                       transfersMU: sync.Mutex{},
-                                                       transfers: map[int]map[[4]byte]*FileTransfer{
-                                                               FileDownload: {
-                                                                       [4]byte{0, 0, 0, 5}: &FileTransfer{},
+                               Stats:  NewStats(),
+                               FileTransferMgr: &MemFileTransferMgr{
+                                       fileTransfers: map[FileTransferID]*FileTransfer{
+                                               {0, 0, 0, 5}: {
+                                                       refNum:   [4]byte{0, 0, 0, 5},
+                                                       Type:     FileDownload,
+                                                       FileName: []byte("testfile-8b"),
+                                                       FilePath: []byte{},
+                                                       ClientConn: &ClientConn{
+                                                               Account: &Account{
+                                                                       Login: "foo",
+                                                               },
+                                                               ClientFileTransferMgr: ClientFileTransferMgr{
+                                                                       transfers: map[FileTransferType]map[FileTransferID]*FileTransfer{
+                                                                               FileDownload: {
+                                                                                       [4]byte{0, 0, 0, 5}: &FileTransfer{},
+                                                                               },
+                                                                       },
                                                                },
                                                        },
                                                                },
                                                        },
+                                                       bytesSentCounter: &WriteCounter{},
                                                },
                                                },
-                                               bytesSentCounter: &WriteCounter{},
                                        },
                                },
                        },
                                        },
                                },
                        },
@@ -172,18 +169,14 @@ func TestServer_handleFileTransfer(t *testing.T) {
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
                        s := &Server{
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
                        s := &Server{
-                               Port:          tt.fields.Port,
-                               Accounts:      tt.fields.Accounts,
-                               Agreement:     tt.fields.Agreement,
-                               Clients:       tt.fields.Clients,
-                               ThreadedNews:  tt.fields.ThreadedNews,
-                               fileTransfers: tt.fields.fileTransfers,
-                               Config:        tt.fields.Config,
-                               ConfigDir:     tt.fields.ConfigDir,
-                               Logger:        tt.fields.Logger,
-                               Stats:         tt.fields.Stats,
-                               FS:            tt.fields.FS,
+                               FileTransferMgr: tt.fields.FileTransferMgr,
+                               Config:          tt.fields.Config,
+                               ConfigDir:       tt.fields.ConfigDir,
+                               Logger:          tt.fields.Logger,
+                               Stats:           tt.fields.Stats,
+                               FS:              tt.fields.FS,
                        }
                        }
+
                        tt.wantErr(t, s.handleFileTransfer(tt.args.ctx, tt.args.rwc), fmt.Sprintf("handleFileTransfer(%v, %v)", tt.args.ctx, tt.args.rwc))
 
                        assertTransferBytesEqual(t, tt.wantDump, tt.args.rwc.(mockReadWriter).WBuf.Bytes())
                        tt.wantErr(t, s.handleFileTransfer(tt.args.ctx, tt.args.rwc), fmt.Sprintf("handleFileTransfer(%v, %v)", tt.args.ctx, tt.args.rwc))
 
                        assertTransferBytesEqual(t, tt.wantDump, tt.args.rwc.(mockReadWriter).WBuf.Bytes())
index d1a3c5f84dcb6bc572baff87fb64f017ce09c73d..316a67df9429e675e740b447b6cbf2df458b557d 100644 (file)
@@ -5,16 +5,92 @@ import (
        "time"
 )
 
        "time"
 )
 
+// Stat counter keys
+const (
+       StatCurrentlyConnected = iota
+       StatDownloadsInProgress
+       StatUploadsInProgress
+       StatWaitingDownloads
+       StatConnectionPeak
+       StatConnectionCounter
+       StatDownloadCounter
+       StatUploadCounter
+)
+
+type Counter interface {
+       Increment(keys ...int)
+       Decrement(key int)
+       Set(key, val int)
+       Get(key int) int
+       Values() map[string]interface{}
+}
+
 type Stats struct {
 type Stats struct {
-       CurrentlyConnected  int
-       DownloadsInProgress int
-       UploadsInProgress   int
-       WaitingDownloads    int
-       ConnectionPeak      int
-       ConnectionCounter   int
-       DownloadCounter     int
-       UploadCounter       int
-       Since               time.Time
-
-       sync.Mutex
+       stats map[int]int
+       since time.Time
+
+       mu sync.RWMutex
+}
+
+func NewStats() *Stats {
+       return &Stats{
+               since: time.Now(),
+               stats: map[int]int{
+                       StatCurrentlyConnected:  0,
+                       StatDownloadsInProgress: 0,
+                       StatUploadsInProgress:   0,
+                       StatWaitingDownloads:    0,
+                       StatConnectionPeak:      0,
+                       StatDownloadCounter:     0,
+                       StatUploadCounter:       0,
+                       StatConnectionCounter:   0,
+               },
+       }
+}
+
+func (s *Stats) Increment(keys ...int) {
+       s.mu.Lock()
+       defer s.mu.Unlock()
+
+       for _, key := range keys {
+               s.stats[key]++
+       }
+}
+
+func (s *Stats) Decrement(key int) {
+       s.mu.Lock()
+       defer s.mu.Unlock()
+
+       s.stats[key]--
+}
+
+func (s *Stats) Set(key, val int) {
+       s.mu.Lock()
+       defer s.mu.Unlock()
+
+       s.stats[key] = val
+}
+
+func (s *Stats) Get(key int) int {
+       s.mu.RLock()
+       defer s.mu.RUnlock()
+
+       return s.stats[key]
+}
+
+func (s *Stats) Values() map[string]interface{} {
+       s.mu.RLock()
+       defer s.mu.RUnlock()
+
+       return map[string]interface{}{
+               "CurrentlyConnected":  s.stats[StatCurrentlyConnected],
+               "DownloadsInProgress": s.stats[StatDownloadsInProgress],
+               "UploadsInProgress":   s.stats[StatUploadsInProgress],
+               "WaitingDownloads":    s.stats[StatWaitingDownloads],
+               "ConnectionPeak":      s.stats[StatConnectionPeak],
+               "ConnectionCounter":   s.stats[StatConnectionCounter],
+               "DownloadCounter":     s.stats[StatDownloadCounter],
+               "UploadCounter":       s.stats[StatUploadCounter],
+               "Since":               s.since,
+       }
 }
 }
index d0ccff4658a3a19bf32f7f14a8dcfe1362f95dde..2d655d97b5762c26ddd16a078b5f1033447d2a5a 100644 (file)
@@ -64,7 +64,7 @@ func (d *RealDialer) Dial(network, address string) (net.Conn, error) {
 func register(dialer Dialer, tracker string, tr io.Reader) error {
        conn, err := dialer.Dial("udp", tracker)
        if err != nil {
 func register(dialer Dialer, tracker string, tr io.Reader) error {
        conn, err := dialer.Dial("udp", tracker)
        if err != nil {
-               return fmt.Errorf("failed to dial tracker: %w", err)
+               return fmt.Errorf("failed to dial tracker: %v", err)
        }
        defer conn.Close()
 
        }
        defer conn.Close()
 
index f8c7dfda71437a57f1deec3eba80d2468013c9a1..7a456a53237ad7f552a026fb2407c5891c0aa470 100644 (file)
@@ -7,70 +7,71 @@ import (
        "errors"
        "fmt"
        "io"
        "errors"
        "fmt"
        "io"
-       "log/slog"
        "math/rand"
        "slices"
 )
 
        "math/rand"
        "slices"
 )
 
+type TranType [2]byte
+
 var (
 var (
-       TranError                = [2]byte{0x00, 0x00} // 0
-       TranGetMsgs              = [2]byte{0x00, 0x65} // 101
-       TranNewMsg               = [2]byte{0x00, 0x66} // 102
-       TranOldPostNews          = [2]byte{0x00, 0x67} // 103
-       TranServerMsg            = [2]byte{0x00, 0x68} // 104
-       TranChatSend             = [2]byte{0x00, 0x69} // 105
-       TranChatMsg              = [2]byte{0x00, 0x6A} // 106
-       TranLogin                = [2]byte{0x00, 0x6B} // 107
-       TranSendInstantMsg       = [2]byte{0x00, 0x6C} // 108
-       TranShowAgreement        = [2]byte{0x00, 0x6D} // 109
-       TranDisconnectUser       = [2]byte{0x00, 0x6E} // 110
-       TranDisconnectMsg        = [2]byte{0x00, 0x6F} // 111
-       TranInviteNewChat        = [2]byte{0x00, 0x70} // 112
-       TranInviteToChat         = [2]byte{0x00, 0x71} // 113
-       TranRejectChatInvite     = [2]byte{0x00, 0x72} // 114
-       TranJoinChat             = [2]byte{0x00, 0x73} // 115
-       TranLeaveChat            = [2]byte{0x00, 0x74} // 116
-       TranNotifyChatChangeUser = [2]byte{0x00, 0x75} // 117
-       TranNotifyChatDeleteUser = [2]byte{0x00, 0x76} // 118
-       TranNotifyChatSubject    = [2]byte{0x00, 0x77} // 119
-       TranSetChatSubject       = [2]byte{0x00, 0x78} // 120
-       TranAgreed               = [2]byte{0x00, 0x79} // 121
-       TranServerBanner         = [2]byte{0x00, 0x7A} // 122
-       TranGetFileNameList      = [2]byte{0x00, 0xC8} // 200
-       TranDownloadFile         = [2]byte{0x00, 0xCA} // 202
-       TranUploadFile           = [2]byte{0x00, 0xCB} // 203
-       TranNewFolder            = [2]byte{0x00, 0xCD} // 205
-       TranDeleteFile           = [2]byte{0x00, 0xCC} // 204
-       TranGetFileInfo          = [2]byte{0x00, 0xCE} // 206
-       TranSetFileInfo          = [2]byte{0x00, 0xCF} // 207
-       TranMoveFile             = [2]byte{0x00, 0xD0} // 208
-       TranMakeFileAlias        = [2]byte{0x00, 0xD1} // 209
-       TranDownloadFldr         = [2]byte{0x00, 0xD2} // 210
-       TranDownloadInfo         = [2]byte{0x00, 0xD3} // 211
-       TranDownloadBanner       = [2]byte{0x00, 0xD4} // 212
-       TranUploadFldr           = [2]byte{0x00, 0xD5} // 213
-       TranGetUserNameList      = [2]byte{0x01, 0x2C} // 300
-       TranNotifyChangeUser     = [2]byte{0x01, 0x2D} // 301
-       TranNotifyDeleteUser     = [2]byte{0x01, 0x2E} // 302
-       TranGetClientInfoText    = [2]byte{0x01, 0x2F} // 303
-       TranSetClientUserInfo    = [2]byte{0x01, 0x30} // 304
-       TranListUsers            = [2]byte{0x01, 0x5C} // 348
-       TranUpdateUser           = [2]byte{0x01, 0x5D} // 349
-       TranNewUser              = [2]byte{0x01, 0x5E} // 350
-       TranDeleteUser           = [2]byte{0x01, 0x5F} // 351
-       TranGetUser              = [2]byte{0x01, 0x60} // 352
-       TranSetUser              = [2]byte{0x01, 0x61} // 353
-       TranUserAccess           = [2]byte{0x01, 0x62} // 354
-       TranUserBroadcast        = [2]byte{0x01, 0x63} // 355
-       TranGetNewsCatNameList   = [2]byte{0x01, 0x72} // 370
-       TranGetNewsArtNameList   = [2]byte{0x01, 0x73} // 371
-       TranDelNewsItem          = [2]byte{0x01, 0x7C} // 380
-       TranNewNewsFldr          = [2]byte{0x01, 0x7D} // 381
-       TranNewNewsCat           = [2]byte{0x01, 0x7E} // 382
-       TranGetNewsArtData       = [2]byte{0x01, 0x90} // 400
-       TranPostNewsArt          = [2]byte{0x01, 0x9A} // 410
-       TranDelNewsArt           = [2]byte{0x01, 0x9B} // 411
-       TranKeepAlive            = [2]byte{0x01, 0xF4} // 500
+       TranError                = TranType{0x00, 0x00} // 0
+       TranGetMsgs              = TranType{0x00, 0x65} // 101
+       TranNewMsg               = TranType{0x00, 0x66} // 102
+       TranOldPostNews          = TranType{0x00, 0x67} // 103
+       TranServerMsg            = TranType{0x00, 0x68} // 104
+       TranChatSend             = TranType{0x00, 0x69} // 105
+       TranChatMsg              = TranType{0x00, 0x6A} // 106
+       TranLogin                = TranType{0x00, 0x6B} // 107
+       TranSendInstantMsg       = TranType{0x00, 0x6C} // 108
+       TranShowAgreement        = TranType{0x00, 0x6D} // 109
+       TranDisconnectUser       = TranType{0x00, 0x6E} // 110
+       TranDisconnectMsg        = TranType{0x00, 0x6F} // 111
+       TranInviteNewChat        = TranType{0x00, 0x70} // 112
+       TranInviteToChat         = TranType{0x00, 0x71} // 113
+       TranRejectChatInvite     = TranType{0x00, 0x72} // 114
+       TranJoinChat             = TranType{0x00, 0x73} // 115
+       TranLeaveChat            = TranType{0x00, 0x74} // 116
+       TranNotifyChatChangeUser = TranType{0x00, 0x75} // 117
+       TranNotifyChatDeleteUser = TranType{0x00, 0x76} // 118
+       TranNotifyChatSubject    = TranType{0x00, 0x77} // 119
+       TranSetChatSubject       = TranType{0x00, 0x78} // 120
+       TranAgreed               = TranType{0x00, 0x79} // 121
+       TranServerBanner         = TranType{0x00, 0x7A} // 122
+       TranGetFileNameList      = TranType{0x00, 0xC8} // 200
+       TranDownloadFile         = TranType{0x00, 0xCA} // 202
+       TranUploadFile           = TranType{0x00, 0xCB} // 203
+       TranNewFolder            = TranType{0x00, 0xCD} // 205
+       TranDeleteFile           = TranType{0x00, 0xCC} // 204
+       TranGetFileInfo          = TranType{0x00, 0xCE} // 206
+       TranSetFileInfo          = TranType{0x00, 0xCF} // 207
+       TranMoveFile             = TranType{0x00, 0xD0} // 208
+       TranMakeFileAlias        = TranType{0x00, 0xD1} // 209
+       TranDownloadFldr         = TranType{0x00, 0xD2} // 210
+       TranDownloadInfo         = TranType{0x00, 0xD3} // 211
+       TranDownloadBanner       = TranType{0x00, 0xD4} // 212
+       TranUploadFldr           = TranType{0x00, 0xD5} // 213
+       TranGetUserNameList      = TranType{0x01, 0x2C} // 300
+       TranNotifyChangeUser     = TranType{0x01, 0x2D} // 301
+       TranNotifyDeleteUser     = TranType{0x01, 0x2E} // 302
+       TranGetClientInfoText    = TranType{0x01, 0x2F} // 303
+       TranSetClientUserInfo    = TranType{0x01, 0x30} // 304
+       TranListUsers            = TranType{0x01, 0x5C} // 348
+       TranUpdateUser           = TranType{0x01, 0x5D} // 349
+       TranNewUser              = TranType{0x01, 0x5E} // 350
+       TranDeleteUser           = TranType{0x01, 0x5F} // 351
+       TranGetUser              = TranType{0x01, 0x60} // 352
+       TranSetUser              = TranType{0x01, 0x61} // 353
+       TranUserAccess           = TranType{0x01, 0x62} // 354
+       TranUserBroadcast        = TranType{0x01, 0x63} // 355
+       TranGetNewsCatNameList   = TranType{0x01, 0x72} // 370
+       TranGetNewsArtNameList   = TranType{0x01, 0x73} // 371
+       TranDelNewsItem          = TranType{0x01, 0x7C} // 380
+       TranNewNewsFldr          = TranType{0x01, 0x7D} // 381
+       TranNewNewsCat           = TranType{0x01, 0x7E} // 382
+       TranGetNewsArtData       = TranType{0x01, 0x90} // 400
+       TranPostNewsArt          = TranType{0x01, 0x9A} // 410
+       TranDelNewsArt           = TranType{0x01, 0x9B} // 411
+       TranKeepAlive            = TranType{0x01, 0xF4} // 500
 )
 
 type Transaction struct {
 )
 
 type Transaction struct {
@@ -88,66 +89,64 @@ type Transaction struct {
        readOffset int     // Internal offset to track read progress
 }
 
        readOffset int     // Internal offset to track read progress
 }
 
-type TranType [2]byte
-
 var tranTypeNames = map[TranType]string{
 var tranTypeNames = map[TranType]string{
-       TranChatMsg:            "Receive Chat",
-       TranNotifyChangeUser:   "TranNotifyChangeUser",
-       TranError:              "TranError",
-       TranShowAgreement:      "TranShowAgreement",
-       TranUserAccess:         "TranUserAccess",
-       TranNotifyDeleteUser:   "TranNotifyDeleteUser",
+       TranChatMsg:            "Receive chat",
+       TranNotifyChangeUser:   "User change",
+       TranError:              "Error",
+       TranShowAgreement:      "Show Agreement",
+       TranUserAccess:         "User access",
+       TranNotifyDeleteUser:   "User left",
        TranAgreed:             "TranAgreed",
        TranAgreed:             "TranAgreed",
-       TranChatSend:           "Send Chat",
-       TranDelNewsArt:         "TranDelNewsArt",
-       TranDelNewsItem:        "TranDelNewsItem",
-       TranDeleteFile:         "TranDeleteFile",
-       TranDeleteUser:         "TranDeleteUser",
-       TranDisconnectUser:     "TranDisconnectUser",
-       TranDownloadFile:       "TranDownloadFile",
-       TranDownloadFldr:       "TranDownloadFldr",
-       TranGetClientInfoText:  "TranGetClientInfoText",
-       TranGetFileInfo:        "TranGetFileInfo",
-       TranGetFileNameList:    "TranGetFileNameList",
-       TranGetMsgs:            "TranGetMsgs",
-       TranGetNewsArtData:     "TranGetNewsArtData",
-       TranGetNewsArtNameList: "TranGetNewsArtNameList",
-       TranGetNewsCatNameList: "TranGetNewsCatNameList",
-       TranGetUser:            "TranGetUser",
-       TranGetUserNameList:    "tranHandleGetUserNameList",
-       TranInviteNewChat:      "TranInviteNewChat",
-       TranInviteToChat:       "TranInviteToChat",
-       TranJoinChat:           "TranJoinChat",
-       TranKeepAlive:          "TranKeepAlive",
-       TranLeaveChat:          "TranJoinChat",
-       TranListUsers:          "TranListUsers",
-       TranMoveFile:           "TranMoveFile",
-       TranNewFolder:          "TranNewFolder",
-       TranNewNewsCat:         "TranNewNewsCat",
-       TranNewNewsFldr:        "TranNewNewsFldr",
-       TranNewUser:            "TranNewUser",
-       TranUpdateUser:         "TranUpdateUser",
-       TranOldPostNews:        "TranOldPostNews",
-       TranPostNewsArt:        "TranPostNewsArt",
-       TranRejectChatInvite:   "TranRejectChatInvite",
-       TranSendInstantMsg:     "TranSendInstantMsg",
-       TranSetChatSubject:     "TranSetChatSubject",
-       TranMakeFileAlias:      "TranMakeFileAlias",
-       TranSetClientUserInfo:  "TranSetClientUserInfo",
-       TranSetFileInfo:        "TranSetFileInfo",
-       TranSetUser:            "TranSetUser",
-       TranUploadFile:         "TranUploadFile",
-       TranUploadFldr:         "TranUploadFldr",
-       TranUserBroadcast:      "TranUserBroadcast",
-       TranDownloadBanner:     "TranDownloadBanner",
+       TranChatSend:           "Send chat",
+       TranDelNewsArt:         "Delete news article",
+       TranDelNewsItem:        "Delete news item",
+       TranDeleteFile:         "Delete file",
+       TranDeleteUser:         "Delete user",
+       TranDisconnectUser:     "Disconnect user",
+       TranDownloadFile:       "Download file",
+       TranDownloadFldr:       "Download folder",
+       TranGetClientInfoText:  "Get client info",
+       TranGetFileInfo:        "Get file info",
+       TranGetFileNameList:    "Get file list",
+       TranGetMsgs:            "Get messages",
+       TranGetNewsArtData:     "Get news article",
+       TranGetNewsArtNameList: "Get news article list",
+       TranGetNewsCatNameList: "Get news categories",
+       TranGetUser:            "Get user",
+       TranGetUserNameList:    "Get user list",
+       TranInviteNewChat:      "Invite to new chat",
+       TranInviteToChat:       "Invite to chat",
+       TranJoinChat:           "Join chat",
+       TranKeepAlive:          "Keepalive",
+       TranLeaveChat:          "Leave chat",
+       TranListUsers:          "List user accounts",
+       TranMoveFile:           "Move file",
+       TranNewFolder:          "Create folder",
+       TranNewNewsCat:         "Create news category",
+       TranNewNewsFldr:        "Create news bundle",
+       TranNewUser:            "Create user account",
+       TranUpdateUser:         "Update user account",
+       TranOldPostNews:        "Post to message board",
+       TranPostNewsArt:        "Create news article",
+       TranRejectChatInvite:   "Decline chat invite",
+       TranSendInstantMsg:     "Send message",
+       TranSetChatSubject:     "Set chat subject",
+       TranMakeFileAlias:      "Make file alias",
+       TranSetClientUserInfo:  "Set client user info",
+       TranSetFileInfo:        "Set file info",
+       TranSetUser:            "Set user",
+       TranUploadFile:         "Upload file",
+       TranUploadFldr:         "Upload folder",
+       TranUserBroadcast:      "Send broadcast",
+       TranDownloadBanner:     "Download banner",
 }
 
 }
 
-func (t TranType) LogValue() slog.Value {
-       return slog.StringValue(tranTypeNames[t])
-}
+//func (t TranType) LogValue() slog.Value {
+//     return slog.StringValue(tranTypeNames[t])
+//}
 
 
-// NewTransaction creates a new Transaction with the specified type, client ID, and optional fields.
-func NewTransaction(t, clientID [2]byte, fields ...Field) Transaction {
+// NewTransaction creates a new Transaction with the specified type, client Type, and optional fields.
+func NewTransaction(t TranType, clientID [2]byte, fields ...Field) Transaction {
        transaction := Transaction{
                Type:     t,
                clientID: clientID,
        transaction := Transaction{
                Type:     t,
                clientID: clientID,
@@ -227,45 +226,6 @@ func transactionScanner(data []byte, _ bool) (advance int, token []byte, err err
 
 const minFieldLen = 4
 
 
 const minFieldLen = 4
 
-func ReadFields(paramCount []byte, buf []byte) ([]Field, error) {
-       paramCountInt := int(binary.BigEndian.Uint16(paramCount))
-       if paramCountInt > 0 && len(buf) < minFieldLen {
-               return []Field{}, fmt.Errorf("invalid field length %v", len(buf))
-       }
-
-       // A Field consists of:
-       // ID: 2 bytes
-       // Size: 2 bytes
-       // Data: FieldSize number of bytes
-       var fields []Field
-       for i := 0; i < paramCountInt; i++ {
-               if len(buf) < minFieldLen {
-                       return []Field{}, fmt.Errorf("invalid field length %v", len(buf))
-               }
-               fieldID := buf[0:2]
-               fieldSize := buf[2:4]
-               fieldSizeInt := int(binary.BigEndian.Uint16(buf[2:4]))
-               expectedLen := minFieldLen + fieldSizeInt
-               if len(buf) < expectedLen {
-                       return []Field{}, fmt.Errorf("field length too short")
-               }
-
-               fields = append(fields, Field{
-                       ID:        [2]byte(fieldID),
-                       FieldSize: [2]byte(fieldSize),
-                       Data:      buf[4 : 4+fieldSizeInt],
-               })
-
-               buf = buf[fieldSizeInt+4:]
-       }
-
-       if len(buf) != 0 {
-               return []Field{}, fmt.Errorf("extra field bytes")
-       }
-
-       return fields, nil
-}
-
 // Read implements the io.Reader interface for Transaction
 func (t *Transaction) Read(p []byte) (int, error) {
        payloadSize := t.Size()
 // Read implements the io.Reader interface for Transaction
 func (t *Transaction) Read(p []byte) (int, error) {
        payloadSize := t.Size()
@@ -318,12 +278,12 @@ func (t *Transaction) Size() []byte {
        return bs
 }
 
        return bs
 }
 
-func (t *Transaction) GetField(id [2]byte) Field {
+func (t *Transaction) GetField(id [2]byte) *Field {
        for _, field := range t.Fields {
        for _, field := range t.Fields {
-               if id == field.ID {
-                       return field
+               if id == field.Type {
+                       return &field
                }
        }
 
                }
        }
 
-       return Field{}
+       return &Field{}
 }
 }
index 271f3085d07b683f2225fe88c992beda028b03d9..ead394a50775b198491e9f60de7cafc04e955e52 100644 (file)
@@ -5,14 +5,11 @@ import (
        "bytes"
        "encoding/binary"
        "fmt"
        "bytes"
        "encoding/binary"
        "fmt"
-       "github.com/davecgh/go-spew/spew"
-       "gopkg.in/yaml.v3"
        "io"
        "math/big"
        "os"
        "path"
        "path/filepath"
        "io"
        "math/big"
        "os"
        "path"
        "path/filepath"
-       "sort"
        "strings"
        "time"
 )
        "strings"
        "time"
 )
@@ -71,7 +68,7 @@ var TransactionHandlers = map[TranType]HandlerFunc{
 const chatMsgLimit = 8192
 
 func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction) {
 const chatMsgLimit = 8192
 
 func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessSendChat) {
+       if !cc.Authorize(AccessSendChat) {
                return cc.NewErrReply(t, "You are not allowed to participate in chat.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to participate in chat.")
        }
 
@@ -95,10 +92,9 @@ func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction) {
        // All clients *except* Frogblast omit this field for public chat, but Frogblast sends a value of 00 00 00 00.
        chatID := t.GetField(FieldChatID).Data
        if chatID != nil && !bytes.Equal([]byte{0, 0, 0, 0}, chatID) {
        // All clients *except* Frogblast omit this field for public chat, but Frogblast sends a value of 00 00 00 00.
        chatID := t.GetField(FieldChatID).Data
        if chatID != nil && !bytes.Equal([]byte{0, 0, 0, 0}, chatID) {
-               privChat := cc.Server.PrivateChats[[4]byte(chatID)]
 
                // send the message to all connected clients of the private chat
 
                // send the message to all connected clients of the private chat
-               for _, c := range privChat.ClientConn {
+               for _, c := range cc.Server.ChatMgr.Members([4]byte(chatID)) {
                        res = append(res, NewTransaction(
                                TranChatMsg,
                                c.ID,
                        res = append(res, NewTransaction(
                                TranChatMsg,
                                c.ID,
@@ -110,12 +106,12 @@ func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction) {
        }
 
        //cc.Server.mux.Lock()
        }
 
        //cc.Server.mux.Lock()
-       for _, c := range cc.Server.Clients {
+       for _, c := range cc.Server.ClientMgr.List() {
                if c == nil || cc.Account == nil {
                        continue
                }
                // Skip clients that do not have the read chat permission.
                if c == nil || cc.Account == nil {
                        continue
                }
                // Skip clients that do not have the read chat permission.
-               if c.Authorize(accessReadChat) {
+               if c.Authorize(AccessReadChat) {
                        res = append(res, NewTransaction(TranChatMsg, c.ID, NewField(FieldData, []byte(formattedMsg))))
                }
        }
                        res = append(res, NewTransaction(TranChatMsg, c.ID, NewField(FieldData, []byte(formattedMsg))))
                }
        }
@@ -127,7 +123,7 @@ func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction) {
 // HandleSendInstantMsg sends instant message to the user on the current server.
 // Fields used in the request:
 //
 // HandleSendInstantMsg sends instant message to the user on the current server.
 // Fields used in the request:
 //
-//     103     User ID
+//     103     User Type
 //     113     Options
 //             One of the following values:
 //             - User message (myOpt_UserMessage = 1)
 //     113     Options
 //             One of the following values:
 //             - User message (myOpt_UserMessage = 1)
@@ -140,7 +136,7 @@ func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction) {
 // Fields used in the reply:
 // None
 func HandleSendInstantMsg(cc *ClientConn, t *Transaction) (res []Transaction) {
 // Fields used in the reply:
 // None
 func HandleSendInstantMsg(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessSendPrivMsg) {
+       if !cc.Authorize(AccessSendPrivMsg) {
                return cc.NewErrReply(t, "You are not allowed to send private messages.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to send private messages.")
        }
 
@@ -162,8 +158,8 @@ func HandleSendInstantMsg(cc *ClientConn, t *Transaction) (res []Transaction) {
                reply.Fields = append(reply.Fields, NewField(FieldQuotingMsg, t.GetField(FieldQuotingMsg).Data))
        }
 
                reply.Fields = append(reply.Fields, NewField(FieldQuotingMsg, t.GetField(FieldQuotingMsg).Data))
        }
 
-       otherClient, ok := cc.Server.Clients[[2]byte(userID.Data)]
-       if !ok {
+       otherClient := cc.Server.ClientMgr.Get([2]byte(userID.Data))
+       if otherClient == nil {
                return res
        }
 
                return res
        }
 
@@ -272,11 +268,11 @@ func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction) {
        if t.GetField(FieldFileComment).Data != nil {
                switch mode := fi.Mode(); {
                case mode.IsDir():
        if t.GetField(FieldFileComment).Data != nil {
                switch mode := fi.Mode(); {
                case mode.IsDir():
-                       if !cc.Authorize(accessSetFolderComment) {
+                       if !cc.Authorize(AccessSetFolderComment) {
                                return cc.NewErrReply(t, "You are not allowed to set comments for folders.")
                        }
                case mode.IsRegular():
                                return cc.NewErrReply(t, "You are not allowed to set comments for folders.")
                        }
                case mode.IsRegular():
-                       if !cc.Authorize(accessSetFileComment) {
+                       if !cc.Authorize(AccessSetFileComment) {
                                return cc.NewErrReply(t, "You are not allowed to set comments for files.")
                        }
                }
                                return cc.NewErrReply(t, "You are not allowed to set comments for files.")
                        }
                }
@@ -304,7 +300,7 @@ func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction) {
        if fileNewName != nil {
                switch mode := fi.Mode(); {
                case mode.IsDir():
        if fileNewName != nil {
                switch mode := fi.Mode(); {
                case mode.IsDir():
-                       if !cc.Authorize(accessRenameFolder) {
+                       if !cc.Authorize(AccessRenameFolder) {
                                return cc.NewErrReply(t, "You are not allowed to rename folders.")
                        }
                        err = os.Rename(fullFilePath, fullNewFilePath)
                                return cc.NewErrReply(t, "You are not allowed to rename folders.")
                        }
                        err = os.Rename(fullFilePath, fullNewFilePath)
@@ -313,7 +309,7 @@ func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction) {
 
                        }
                case mode.IsRegular():
 
                        }
                case mode.IsRegular():
-                       if !cc.Authorize(accessRenameFile) {
+                       if !cc.Authorize(AccessRenameFile) {
                                return cc.NewErrReply(t, "You are not allowed to rename files.")
                        }
                        fileDir, err := readPath(cc.Server.Config.FileRoot, filePath, []byte{})
                                return cc.NewErrReply(t, "You are not allowed to rename files.")
                        }
                        fileDir, err := readPath(cc.Server.Config.FileRoot, filePath, []byte{})
@@ -365,11 +361,11 @@ func HandleDeleteFile(cc *ClientConn, t *Transaction) (res []Transaction) {
 
        switch mode := fi.Mode(); {
        case mode.IsDir():
 
        switch mode := fi.Mode(); {
        case mode.IsDir():
-               if !cc.Authorize(accessDeleteFolder) {
+               if !cc.Authorize(AccessDeleteFolder) {
                        return cc.NewErrReply(t, "You are not allowed to delete folders.")
                }
        case mode.IsRegular():
                        return cc.NewErrReply(t, "You are not allowed to delete folders.")
                }
        case mode.IsRegular():
-               if !cc.Authorize(accessDeleteFile) {
+               if !cc.Authorize(AccessDeleteFile) {
                        return cc.NewErrReply(t, "You are not allowed to delete files.")
                }
        }
                        return cc.NewErrReply(t, "You are not allowed to delete files.")
                }
        }
@@ -409,11 +405,11 @@ func HandleMoveFile(cc *ClientConn, t *Transaction) (res []Transaction) {
        }
        switch mode := fi.Mode(); {
        case mode.IsDir():
        }
        switch mode := fi.Mode(); {
        case mode.IsDir():
-               if !cc.Authorize(accessMoveFolder) {
+               if !cc.Authorize(AccessMoveFolder) {
                        return cc.NewErrReply(t, "You are not allowed to move folders.")
                }
        case mode.IsRegular():
                        return cc.NewErrReply(t, "You are not allowed to move folders.")
                }
        case mode.IsRegular():
-               if !cc.Authorize(accessMoveFile) {
+               if !cc.Authorize(AccessMoveFile) {
                        return cc.NewErrReply(t, "You are not allowed to move files.")
                }
        }
                        return cc.NewErrReply(t, "You are not allowed to move files.")
                }
        }
@@ -427,7 +423,7 @@ func HandleMoveFile(cc *ClientConn, t *Transaction) (res []Transaction) {
 }
 
 func HandleNewFolder(cc *ClientConn, t *Transaction) (res []Transaction) {
 }
 
 func HandleNewFolder(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessCreateFolder) {
+       if !cc.Authorize(AccessCreateFolder) {
                return cc.NewErrReply(t, "You are not allowed to create folders.")
        }
        folderName := string(t.GetField(FieldFileName).Data)
                return cc.NewErrReply(t, "You are not allowed to create folders.")
        }
        folderName := string(t.GetField(FieldFileName).Data)
@@ -466,21 +462,20 @@ func HandleNewFolder(cc *ClientConn, t *Transaction) (res []Transaction) {
                return cc.NewErrReply(t, msg)
        }
 
                return cc.NewErrReply(t, msg)
        }
 
-       res = append(res, cc.NewReply(t))
-       return res
+       return append(res, cc.NewReply(t))
 }
 
 func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction) {
 }
 
 func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessModifyUser) {
+       if !cc.Authorize(AccessModifyUser) {
                return cc.NewErrReply(t, "You are not allowed to modify accounts.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to modify accounts.")
        }
 
-       login := string(encodeString(t.GetField(FieldUserLogin).Data))
+       login := t.GetField(FieldUserLogin).DecodeObfuscatedString()
        userName := string(t.GetField(FieldUserName).Data)
 
        newAccessLvl := t.GetField(FieldUserAccess).Data
 
        userName := string(t.GetField(FieldUserName).Data)
 
        newAccessLvl := t.GetField(FieldUserAccess).Data
 
-       account := cc.Server.Accounts[login]
+       account := cc.Server.AccountManager.Get(login)
        if account == nil {
                return cc.NewErrReply(t, "Account not found.")
        }
        if account == nil {
                return cc.NewErrReply(t, "Account not found.")
        }
@@ -497,21 +492,18 @@ func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction) {
                account.Password = hashAndSalt(t.GetField(FieldUserPassword).Data)
        }
 
                account.Password = hashAndSalt(t.GetField(FieldUserPassword).Data)
        }
 
-       out, err := yaml.Marshal(&account)
+       err := cc.Server.AccountManager.Update(*account, account.Login)
        if err != nil {
        if err != nil {
-               return res
-       }
-       if err := os.WriteFile(filepath.Join(cc.Server.ConfigDir, "Users", login+".yaml"), out, 0666); err != nil {
-               return res
+               cc.logger.Error("Error updating account", "Err", err)
        }
 
        // Notify connected clients logged in as the user of the new access level
        }
 
        // Notify connected clients logged in as the user of the new access level
-       for _, c := range cc.Server.Clients {
+       for _, c := range cc.Server.ClientMgr.List() {
                if c.Account.Login == login {
                        newT := NewTransaction(TranUserAccess, c.ID, NewField(FieldUserAccess, newAccessLvl))
                        res = append(res, newT)
 
                if c.Account.Login == login {
                        newT := NewTransaction(TranUserAccess, c.ID, NewField(FieldUserAccess, newAccessLvl))
                        res = append(res, newT)
 
-                       if c.Authorize(accessDisconUser) {
+                       if c.Authorize(AccessDisconUser) {
                                c.Flags.Set(UserFlagAdmin, 1)
                        } else {
                                c.Flags.Set(UserFlagAdmin, 0)
                                c.Flags.Set(UserFlagAdmin, 1)
                        } else {
                                c.Flags.Set(UserFlagAdmin, 0)
@@ -519,7 +511,7 @@ func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction) {
 
                        c.Account.Access = account.Access
 
 
                        c.Account.Access = account.Access
 
-                       cc.sendAll(
+                       cc.SendAll(
                                TranNotifyChangeUser,
                                NewField(FieldUserID, c.ID[:]),
                                NewField(FieldUserFlags, c.Flags[:]),
                                TranNotifyChangeUser,
                                NewField(FieldUserID, c.ID[:]),
                                NewField(FieldUserFlags, c.Flags[:]),
@@ -529,47 +521,44 @@ func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction) {
                }
        }
 
                }
        }
 
-       res = append(res, cc.NewReply(t))
-       return res
+       return append(res, cc.NewReply(t))
 }
 
 func HandleGetUser(cc *ClientConn, t *Transaction) (res []Transaction) {
 }
 
 func HandleGetUser(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessOpenUser) {
+       if !cc.Authorize(AccessOpenUser) {
                return cc.NewErrReply(t, "You are not allowed to view accounts.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to view accounts.")
        }
 
-       account := cc.Server.Accounts[string(t.GetField(FieldUserLogin).Data)]
+       account := cc.Server.AccountManager.Get(string(t.GetField(FieldUserLogin).Data))
        if account == nil {
                return cc.NewErrReply(t, "Account does not exist.")
        }
 
        if account == nil {
                return cc.NewErrReply(t, "Account does not exist.")
        }
 
-       res = append(res, cc.NewReply(t,
+       return append(res, cc.NewReply(t,
                NewField(FieldUserName, []byte(account.Name)),
                NewField(FieldUserLogin, encodeString(t.GetField(FieldUserLogin).Data)),
                NewField(FieldUserPassword, []byte(account.Password)),
                NewField(FieldUserAccess, account.Access[:]),
        ))
                NewField(FieldUserName, []byte(account.Name)),
                NewField(FieldUserLogin, encodeString(t.GetField(FieldUserLogin).Data)),
                NewField(FieldUserPassword, []byte(account.Password)),
                NewField(FieldUserAccess, account.Access[:]),
        ))
-       return res
 }
 
 func HandleListUsers(cc *ClientConn, t *Transaction) (res []Transaction) {
 }
 
 func HandleListUsers(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessOpenUser) {
+       if !cc.Authorize(AccessOpenUser) {
                return cc.NewErrReply(t, "You are not allowed to view accounts.")
        }
 
        var userFields []Field
                return cc.NewErrReply(t, "You are not allowed to view accounts.")
        }
 
        var userFields []Field
-       for _, acc := range cc.Server.Accounts {
-               accCopy := *acc
-               b, err := io.ReadAll(&accCopy)
+       for _, acc := range cc.Server.AccountManager.List() {
+               b, err := io.ReadAll(&acc)
                if err != nil {
                if err != nil {
-                       return res
+                       cc.logger.Error("Error reading account", "Account", acc.Login, "Err", err)
+                       continue
                }
 
                userFields = append(userFields, NewField(FieldData, b))
        }
 
                }
 
                userFields = append(userFields, NewField(FieldData, b))
        }
 
-       res = append(res, cc.NewReply(t, userFields...))
-       return res
+       return append(res, cc.NewReply(t, userFields...))
 }
 
 // HandleUpdateUser is used by the v1.5+ multi-user editor to perform account editing for multiple users at a time.
 }
 
 // HandleUpdateUser is used by the v1.5+ multi-user editor to perform account editing for multiple users at a time.
@@ -601,16 +590,37 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction) {
 
                // If there's only one subfield, that indicates this is a delete operation for the login in FieldData
                if len(subFields) == 1 {
 
                // If there's only one subfield, that indicates this is a delete operation for the login in FieldData
                if len(subFields) == 1 {
-                       if !cc.Authorize(accessDeleteUser) {
+                       if !cc.Authorize(AccessDeleteUser) {
                                return cc.NewErrReply(t, "You are not allowed to delete accounts.")
                        }
 
                        login := string(encodeString(getField(FieldData, &subFields).Data))
                                return cc.NewErrReply(t, "You are not allowed to delete accounts.")
                        }
 
                        login := string(encodeString(getField(FieldData, &subFields).Data))
+
                        cc.logger.Info("DeleteUser", "login", login)
 
                        cc.logger.Info("DeleteUser", "login", login)
 
-                       if err := cc.Server.DeleteUser(login); err != nil {
+                       if err := cc.Server.AccountManager.Delete(login); err != nil {
+                               cc.logger.Error("Error deleting account", "Err", err)
                                return res
                        }
                                return res
                        }
+
+                       for _, client := range cc.Server.ClientMgr.List() {
+                               if client.Account.Login == login {
+                                       //                                      "You are logged in with an account which was deleted."
+
+                                       res = append(res,
+                                               NewTransaction(TranServerMsg, [2]byte{},
+                                                       NewField(FieldData, []byte("You are logged in with an account which was deleted.")),
+                                                       NewField(FieldChatOptions, []byte{0}),
+                                               ),
+                                       )
+
+                                       go func(c *ClientConn) {
+                                               time.Sleep(3 * time.Second)
+                                               c.Disconnect()
+                                       }(client)
+                               }
+                       }
+
                        continue
                }
 
                        continue
                }
 
@@ -630,15 +640,15 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction) {
                }
 
                // Check if accountToUpdate has an existing account.  If so, we know we are updating an existing user.
                }
 
                // Check if accountToUpdate has an existing account.  If so, we know we are updating an existing user.
-               if acc, ok := cc.Server.Accounts[accountToUpdate]; ok {
+               if acc := cc.Server.AccountManager.Get(accountToUpdate); acc != nil {
                        if loginToRename != "" {
                                cc.logger.Info("RenameUser", "prevLogin", accountToUpdate, "newLogin", userLogin)
                        } else {
                                cc.logger.Info("UpdateUser", "login", accountToUpdate)
                        }
 
                        if loginToRename != "" {
                                cc.logger.Info("RenameUser", "prevLogin", accountToUpdate, "newLogin", userLogin)
                        } else {
                                cc.logger.Info("UpdateUser", "login", accountToUpdate)
                        }
 
-                       // account exists, so this is an update action
-                       if !cc.Authorize(accessModifyUser) {
+                       // Account exists, so this is an update action.
+                       if !cc.Authorize(AccessModifyUser) {
                                return cc.NewErrReply(t, "You are not allowed to modify accounts.")
                        }
 
                                return cc.NewErrReply(t, "You are not allowed to modify accounts.")
                        }
 
@@ -662,18 +672,15 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction) {
                                copy(acc.Access[:], getField(FieldUserAccess, &subFields).Data)
                        }
 
                                copy(acc.Access[:], getField(FieldUserAccess, &subFields).Data)
                        }
 
-                       err := cc.Server.UpdateUser(
-                               string(encodeString(getField(FieldData, &subFields).Data)),
-                               string(encodeString(getField(FieldUserLogin, &subFields).Data)),
-                               string(getField(FieldUserName, &subFields).Data),
-                               acc.Password,
-                               acc.Access,
-                       )
+                       acc.Name = string(getField(FieldUserName, &subFields).Data)
+
+                       err := cc.Server.AccountManager.Update(*acc, string(encodeString(getField(FieldUserLogin, &subFields).Data)))
+
                        if err != nil {
                                return res
                        }
                } else {
                        if err != nil {
                                return res
                        }
                } else {
-                       if !cc.Authorize(accessCreateUser) {
+                       if !cc.Authorize(AccessCreateUser) {
                                return cc.NewErrReply(t, "You are not allowed to create new accounts.")
                        }
 
                                return cc.NewErrReply(t, "You are not allowed to create new accounts.")
                        }
 
@@ -691,7 +698,9 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction) {
                                }
                        }
 
                                }
                        }
 
-                       err := cc.Server.NewUser(userLogin, string(getField(FieldUserName, &subFields).Data), string(getField(FieldUserPassword, &subFields).Data), newAccess)
+                       account := NewAccount(userLogin, string(getField(FieldUserName, &subFields).Data), string(getField(FieldUserPassword, &subFields).Data), newAccess)
+
+                       err := cc.Server.AccountManager.Create(*account)
                        if err != nil {
                                return cc.NewErrReply(t, "Cannot create account because there is already an account with that login.")
                        }
                        if err != nil {
                                return cc.NewErrReply(t, "Cannot create account because there is already an account with that login.")
                        }
@@ -703,18 +712,18 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction) {
 
 // HandleNewUser creates a new user account
 func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction) {
 
 // HandleNewUser creates a new user account
 func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessCreateUser) {
+       if !cc.Authorize(AccessCreateUser) {
                return cc.NewErrReply(t, "You are not allowed to create new accounts.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to create new accounts.")
        }
 
-       login := string(encodeString(t.GetField(FieldUserLogin).Data))
+       login := t.GetField(FieldUserLogin).DecodeObfuscatedString()
 
 
-       // If the account already dataFile, reply with an error
-       if _, ok := cc.Server.Accounts[login]; ok {
+       // If the account already exists, reply with an error.
+       if account := cc.Server.AccountManager.Get(login); account != nil {
                return cc.NewErrReply(t, "Cannot create account "+login+" because there is already an account with that login.")
        }
 
                return cc.NewErrReply(t, "Cannot create account "+login+" because there is already an account with that login.")
        }
 
-       newAccess := accessBitmap{}
+       var newAccess accessBitmap
        copy(newAccess[:], t.GetField(FieldUserAccess).Data)
 
        // Prevent account from creating new account with greater permission
        copy(newAccess[:], t.GetField(FieldUserAccess).Data)
 
        // Prevent account from creating new account with greater permission
@@ -726,7 +735,10 @@ func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction) {
                }
        }
 
                }
        }
 
-       if err := cc.Server.NewUser(login, string(t.GetField(FieldUserName).Data), string(t.GetField(FieldUserPassword).Data), newAccess); err != nil {
+       account := NewAccount(login, string(t.GetField(FieldUserName).Data), string(t.GetField(FieldUserPassword).Data), newAccess)
+
+       err := cc.Server.AccountManager.Create(*account)
+       if err != nil {
                return cc.NewErrReply(t, "Cannot create account because there is already an account with that login.")
        }
 
                return cc.NewErrReply(t, "Cannot create account because there is already an account with that login.")
        }
 
@@ -734,26 +746,43 @@ func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction) {
 }
 
 func HandleDeleteUser(cc *ClientConn, t *Transaction) (res []Transaction) {
 }
 
 func HandleDeleteUser(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessDeleteUser) {
+       if !cc.Authorize(AccessDeleteUser) {
                return cc.NewErrReply(t, "You are not allowed to delete accounts.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to delete accounts.")
        }
 
-       login := string(encodeString(t.GetField(FieldUserLogin).Data))
+       login := t.GetField(FieldUserLogin).DecodeObfuscatedString()
 
 
-       if err := cc.Server.DeleteUser(login); err != nil {
+       if err := cc.Server.AccountManager.Delete(login); err != nil {
+               cc.logger.Error("Error deleting account", "Err", err)
                return res
        }
 
                return res
        }
 
+       for _, client := range cc.Server.ClientMgr.List() {
+               if client.Account.Login == login {
+                       res = append(res,
+                               NewTransaction(TranServerMsg, client.ID,
+                                       NewField(FieldData, []byte("You are logged in with an account which was deleted.")),
+                                       NewField(FieldChatOptions, []byte{2}),
+                               ),
+                       )
+
+                       go func(c *ClientConn) {
+                               time.Sleep(2 * time.Second)
+                               c.Disconnect()
+                       }(client)
+               }
+       }
+
        return append(res, cc.NewReply(t))
 }
 
 // HandleUserBroadcast sends an Administrator Message to all connected clients of the server
 func HandleUserBroadcast(cc *ClientConn, t *Transaction) (res []Transaction) {
        return append(res, cc.NewReply(t))
 }
 
 // HandleUserBroadcast sends an Administrator Message to all connected clients of the server
 func HandleUserBroadcast(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessBroadcast) {
+       if !cc.Authorize(AccessBroadcast) {
                return cc.NewErrReply(t, "You are not allowed to send broadcast messages.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to send broadcast messages.")
        }
 
-       cc.sendAll(
+       cc.SendAll(
                TranServerMsg,
                NewField(FieldData, t.GetField(FieldData).Data),
                NewField(FieldChatOptions, []byte{0}),
                TranServerMsg,
                NewField(FieldData, t.GetField(FieldData).Data),
                NewField(FieldChatOptions, []byte{0}),
@@ -765,37 +794,51 @@ func HandleUserBroadcast(cc *ClientConn, t *Transaction) (res []Transaction) {
 // HandleGetClientInfoText returns user information for the specific user.
 //
 // Fields used in the request:
 // HandleGetClientInfoText returns user information for the specific user.
 //
 // Fields used in the request:
-// 103 User ID
+// 103 User Type
 //
 // Fields used in the reply:
 // 102 User Name
 // 101 Data            User info text string
 func HandleGetClientInfoText(cc *ClientConn, t *Transaction) (res []Transaction) {
 //
 // Fields used in the reply:
 // 102 User Name
 // 101 Data            User info text string
 func HandleGetClientInfoText(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessGetClientInfo) {
+       if !cc.Authorize(AccessGetClientInfo) {
                return cc.NewErrReply(t, "You are not allowed to get client info.")
        }
 
        clientID := t.GetField(FieldUserID).Data
 
                return cc.NewErrReply(t, "You are not allowed to get client info.")
        }
 
        clientID := t.GetField(FieldUserID).Data
 
-       clientConn := cc.Server.Clients[[2]byte(clientID)]
+       clientConn := cc.Server.ClientMgr.Get(ClientID(clientID))
        if clientConn == nil {
                return cc.NewErrReply(t, "User not found.")
        }
 
        if clientConn == nil {
                return cc.NewErrReply(t, "User not found.")
        }
 
-       res = append(res, cc.NewReply(t,
+       return append(res, cc.NewReply(t,
                NewField(FieldData, []byte(clientConn.String())),
                NewField(FieldUserName, clientConn.UserName),
        ))
                NewField(FieldData, []byte(clientConn.String())),
                NewField(FieldUserName, clientConn.UserName),
        ))
-       return res
 }
 
 func HandleGetUserNameList(cc *ClientConn, t *Transaction) (res []Transaction) {
 }
 
 func HandleGetUserNameList(cc *ClientConn, t *Transaction) (res []Transaction) {
-       return []Transaction{cc.NewReply(t, cc.Server.connectedUsers()...)}
+       var fields []Field
+       for _, c := range cc.Server.ClientMgr.List() {
+               b, err := io.ReadAll(&User{
+                       ID:    c.ID,
+                       Icon:  c.Icon,
+                       Flags: c.Flags[:],
+                       Name:  string(c.UserName),
+               })
+               if err != nil {
+                       return nil
+               }
+
+               fields = append(fields, NewField(FieldUsernameWithInfo, b))
+       }
+
+       return []Transaction{cc.NewReply(t, fields...)}
 }
 
 func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction) {
        if t.GetField(FieldUserName).Data != nil {
 }
 
 func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction) {
        if t.GetField(FieldUserName).Data != nil {
-               if cc.Authorize(accessAnyName) {
+               if cc.Authorize(AccessAnyName) {
                        cc.UserName = t.GetField(FieldUserName).Data
                } else {
                        cc.UserName = []byte(cc.Account.Name)
                        cc.UserName = t.GetField(FieldUserName).Data
                } else {
                        cc.UserName = []byte(cc.Account.Name)
@@ -805,7 +848,7 @@ func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction) {
        cc.Icon = t.GetField(FieldUserIconID).Data
 
        cc.logger = cc.logger.With("Name", string(cc.UserName))
        cc.Icon = t.GetField(FieldUserIconID).Data
 
        cc.logger = cc.logger.With("Name", string(cc.UserName))
-       cc.logger.Info("Login successful", "clientVersion", fmt.Sprintf("%v", func() int { i, _ := byteToInt(cc.Version); return i }()))
+       cc.logger.Info("Login successful")
 
        options := t.GetField(FieldOptions).Data
        optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
 
        options := t.GetField(FieldOptions).Data
        optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
@@ -824,7 +867,7 @@ func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction) {
                cc.AutoReply = t.GetField(FieldAutomaticResponse).Data
        }
 
                cc.AutoReply = t.GetField(FieldAutomaticResponse).Data
        }
 
-       trans := cc.notifyOthers(
+       trans := cc.NotifyOthers(
                NewTransaction(
                        TranNotifyChangeUser, [2]byte{0, 0},
                        NewField(FieldUserName, cc.UserName),
                NewTransaction(
                        TranNotifyChangeUser, [2]byte{0, 0},
                        NewField(FieldUserName, cc.UserName),
@@ -848,13 +891,10 @@ func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction) {
 // Fields used in this request:
 // 101 Data
 func HandleTranOldPostNews(cc *ClientConn, t *Transaction) (res []Transaction) {
 // Fields used in this request:
 // 101 Data
 func HandleTranOldPostNews(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessNewsPostArt) {
+       if !cc.Authorize(AccessNewsPostArt) {
                return cc.NewErrReply(t, "You are not allowed to post news.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to post news.")
        }
 
-       cc.Server.flatNewsMux.Lock()
-       defer cc.Server.flatNewsMux.Unlock()
-
        newsDateTemplate := defaultNewsDateFormat
        if cc.Server.Config.NewsDateFormat != "" {
                newsDateTemplate = cc.Server.Config.NewsDateFormat
        newsDateTemplate := defaultNewsDateFormat
        if cc.Server.Config.NewsDateFormat != "" {
                newsDateTemplate = cc.Server.Config.NewsDateFormat
@@ -868,32 +908,30 @@ func HandleTranOldPostNews(cc *ClientConn, t *Transaction) (res []Transaction) {
        newsPost := fmt.Sprintf(newsTemplate+"\r", cc.UserName, time.Now().Format(newsDateTemplate), t.GetField(FieldData).Data)
        newsPost = strings.ReplaceAll(newsPost, "\n", "\r")
 
        newsPost := fmt.Sprintf(newsTemplate+"\r", cc.UserName, time.Now().Format(newsDateTemplate), t.GetField(FieldData).Data)
        newsPost = strings.ReplaceAll(newsPost, "\n", "\r")
 
-       // update news in memory
-       cc.Server.FlatNews = append([]byte(newsPost), cc.Server.FlatNews...)
-
-       // update news on disk
-       if err := cc.Server.FS.WriteFile(filepath.Join(cc.Server.ConfigDir, "MessageBoard.txt"), cc.Server.FlatNews, 0644); err != nil {
-               return res
+       _, err := cc.Server.MessageBoard.Write([]byte(newsPost))
+       if err != nil {
+               cc.logger.Error("error writing news post", "err", err)
+               return nil
        }
 
        // Notify all clients of updated news
        }
 
        // Notify all clients of updated news
-       cc.sendAll(
+       cc.SendAll(
                TranNewMsg,
                NewField(FieldData, []byte(newsPost)),
        )
 
                TranNewMsg,
                NewField(FieldData, []byte(newsPost)),
        )
 
-       res = append(res, cc.NewReply(t))
-       return res
+       return append(res, cc.NewReply(t))
 }
 
 func HandleDisconnectUser(cc *ClientConn, t *Transaction) (res []Transaction) {
 }
 
 func HandleDisconnectUser(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessDisconUser) {
+       if !cc.Authorize(AccessDisconUser) {
                return cc.NewErrReply(t, "You are not allowed to disconnect users.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to disconnect users.")
        }
 
-       clientConn := cc.Server.Clients[[2]byte(t.GetField(FieldUserID).Data)]
+       clientID := [2]byte(t.GetField(FieldUserID).Data)
+       clientConn := cc.Server.ClientMgr.Get(clientID)
 
 
-       if clientConn.Authorize(accessCannotBeDiscon) {
+       if clientConn.Authorize(AccessCannotBeDiscon) {
                return cc.NewErrReply(t, clientConn.Account.Login+" is not allowed to be disconnected.")
        }
 
                return cc.NewErrReply(t, clientConn.Account.Login+" is not allowed to be disconnected.")
        }
 
@@ -914,7 +952,13 @@ func HandleDisconnectUser(cc *ClientConn, t *Transaction) (res []Transaction) {
                        ))
 
                        banUntil := time.Now().Add(tempBanDuration)
                        ))
 
                        banUntil := time.Now().Add(tempBanDuration)
-                       cc.Server.banList[strings.Split(clientConn.RemoteAddr, ":")[0]] = &banUntil
+                       ip := strings.Split(clientConn.RemoteAddr, ":")[0]
+
+                       err := cc.Server.BanList.Add(ip, &banUntil)
+                       if err != nil {
+                               cc.logger.Error("Error saving ban", "err", err)
+                               // TODO
+                       }
                case 2:
                        // send message: "You are permanently banned on this server"
                        cc.logger.Info("Disconnect & ban " + string(clientConn.UserName))
                case 2:
                        // send message: "You are permanently banned on this server"
                        cc.logger.Info("Disconnect & ban " + string(clientConn.UserName))
@@ -926,12 +970,12 @@ func HandleDisconnectUser(cc *ClientConn, t *Transaction) (res []Transaction) {
                                NewField(FieldChatOptions, []byte{0, 0}),
                        ))
 
                                NewField(FieldChatOptions, []byte{0, 0}),
                        ))
 
-                       cc.Server.banList[strings.Split(clientConn.RemoteAddr, ":")[0]] = nil
-               }
+                       ip := strings.Split(clientConn.RemoteAddr, ":")[0]
 
 
-               err := cc.Server.writeBanList()
-               if err != nil {
-                       return res
+                       err := cc.Server.BanList.Add(ip, nil)
+                       if err != nil {
+                               // TODO
+                       }
                }
        }
 
                }
        }
 
@@ -948,81 +992,67 @@ func HandleDisconnectUser(cc *ClientConn, t *Transaction) (res []Transaction) {
 // Fields used in the request:
 // 325 News path       (Optional)
 func HandleGetNewsCatNameList(cc *ClientConn, t *Transaction) (res []Transaction) {
 // Fields used in the request:
 // 325 News path       (Optional)
 func HandleGetNewsCatNameList(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessNewsReadArt) {
+       if !cc.Authorize(AccessNewsReadArt) {
                return cc.NewErrReply(t, "You are not allowed to read news.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to read news.")
        }
 
-       pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
-       cats := cc.Server.GetNewsCatByPath(pathStrs)
+       pathStrs, err := t.GetField(FieldNewsPath).DecodeNewsPath()
+       if err != nil {
 
 
-       // To store the keys in slice in sorted order
-       keys := make([]string, len(cats))
-       i := 0
-       for k := range cats {
-               keys[i] = k
-               i++
        }
        }
-       sort.Strings(keys)
-
-       var fieldData []Field
-       for _, k := range keys {
-               cat := cats[k]
 
 
-               b, _ := io.ReadAll(&cat)
+       var fields []Field
+       for _, cat := range cc.Server.ThreadedNewsMgr.GetCategories(pathStrs) {
+               b, err := io.ReadAll(&cat)
+               if err != nil {
+                       // TODO
+               }
 
 
-               fieldData = append(fieldData, NewField(FieldNewsCatListData15, b))
+               fields = append(fields, NewField(FieldNewsCatListData15, b))
        }
 
        }
 
-       res = append(res, cc.NewReply(t, fieldData...))
-       return res
+       return append(res, cc.NewReply(t, fields...))
 }
 
 func HandleNewNewsCat(cc *ClientConn, t *Transaction) (res []Transaction) {
 }
 
 func HandleNewNewsCat(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessNewsCreateCat) {
+       if !cc.Authorize(AccessNewsCreateCat) {
                return cc.NewErrReply(t, "You are not allowed to create news categories.")
        }
 
        name := string(t.GetField(FieldNewsCatName).Data)
                return cc.NewErrReply(t, "You are not allowed to create news categories.")
        }
 
        name := string(t.GetField(FieldNewsCatName).Data)
-       pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
-
-       cats := cc.Server.GetNewsCatByPath(pathStrs)
-       cats[name] = NewsCategoryListData15{
-               Name:     name,
-               Type:     [2]byte{0, 3},
-               Articles: map[uint32]*NewsArtData{},
-               SubCats:  make(map[string]NewsCategoryListData15),
+       pathStrs, err := t.GetField(FieldNewsPath).DecodeNewsPath()
+       if err != nil {
+               return res
        }
 
        }
 
-       if err := cc.Server.writeThreadedNews(); err != nil {
-               return res
+       err = cc.Server.ThreadedNewsMgr.CreateGrouping(pathStrs, name, NewsCategory)
+       if err != nil {
+               cc.logger.Error("error creating news category", "err", err)
        }
        }
-       res = append(res, cc.NewReply(t))
-       return res
+
+       return []Transaction{cc.NewReply(t)}
 }
 
 // Fields used in the request:
 // 322 News category Name
 // 325 News path
 func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction) {
 }
 
 // Fields used in the request:
 // 322 News category Name
 // 325 News path
 func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessNewsCreateFldr) {
+       if !cc.Authorize(AccessNewsCreateFldr) {
                return cc.NewErrReply(t, "You are not allowed to create news folders.")
        }
 
        name := string(t.GetField(FieldFileName).Data)
                return cc.NewErrReply(t, "You are not allowed to create news folders.")
        }
 
        name := string(t.GetField(FieldFileName).Data)
-       pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
-
-       cats := cc.Server.GetNewsCatByPath(pathStrs)
-       cats[name] = NewsCategoryListData15{
-               Name:     name,
-               Type:     [2]byte{0, 2},
-               Articles: map[uint32]*NewsArtData{},
-               SubCats:  make(map[string]NewsCategoryListData15),
-       }
-       if err := cc.Server.writeThreadedNews(); err != nil {
+       pathStrs, err := t.GetField(FieldNewsPath).DecodeNewsPath()
+       if err != nil {
                return res
        }
                return res
        }
-       res = append(res, cc.NewReply(t))
-       return res
+
+       err = cc.Server.ThreadedNewsMgr.CreateGrouping(pathStrs, name, NewsBundle)
+       if err != nil {
+               cc.logger.Error("error creating news bundle", "err", err)
+       }
+
+       return append(res, cc.NewReply(t))
 }
 
 // HandleGetNewsArtData gets the list of article names at the specified news path.
 }
 
 // HandleGetNewsArtData gets the list of article names at the specified news path.
@@ -1033,29 +1063,23 @@ func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction) {
 // Fields used in the reply:
 // 321 News article list data  Optional
 func HandleGetNewsArtNameList(cc *ClientConn, t *Transaction) (res []Transaction) {
 // Fields used in the reply:
 // 321 News article list data  Optional
 func HandleGetNewsArtNameList(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessNewsReadArt) {
+       if !cc.Authorize(AccessNewsReadArt) {
                return cc.NewErrReply(t, "You are not allowed to read news.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to read news.")
        }
 
-       pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
-
-       var cat NewsCategoryListData15
-       cats := cc.Server.ThreadedNews.Categories
-
-       for _, fp := range pathStrs {
-               cat = cats[fp]
-               cats = cats[fp].SubCats
+       pathStrs, err := t.GetField(FieldNewsPath).DecodeNewsPath()
+       if err != nil {
+               return res
        }
 
        }
 
-       nald := cat.GetNewsArtListData()
+       nald := cc.Server.ThreadedNewsMgr.ListArticles(pathStrs)
 
        b, err := io.ReadAll(&nald)
        if err != nil {
                return res
        }
 
 
        b, err := io.ReadAll(&nald)
        if err != nil {
                return res
        }
 
-       res = append(res, cc.NewReply(t, NewField(FieldNewsArtListData, b)))
-       return res
+       return append(res, cc.NewReply(t, NewField(FieldNewsArtListData, b)))
 }
 
 // HandleGetNewsArtData requests information about the specific news article.
 }
 
 // HandleGetNewsArtData requests information about the specific news article.
@@ -1063,40 +1087,35 @@ func HandleGetNewsArtNameList(cc *ClientConn, t *Transaction) (res []Transaction
 //
 // Request fields
 // 325 News path
 //
 // Request fields
 // 325 News path
-// 326 News article ID
+// 326 News article Type
 // 327 News article data flavor
 //
 // Fields used in the reply:
 // 328 News article title
 // 329 News article poster
 // 330 News article date
 // 327 News article data flavor
 //
 // Fields used in the reply:
 // 328 News article title
 // 329 News article poster
 // 330 News article date
-// 331 Previous article ID
-// 332 Next article ID
-// 335 Parent article ID
-// 336 First child article ID
+// 331 Previous article Type
+// 332 Next article Type
+// 335 Parent article Type
+// 336 First child article Type
 // 327 News article data flavor        "Should be “text/plain”
 // 333 News article data       Optional (if data flavor is “text/plain”)
 func HandleGetNewsArtData(cc *ClientConn, t *Transaction) (res []Transaction) {
 // 327 News article data flavor        "Should be “text/plain”
 // 333 News article data       Optional (if data flavor is “text/plain”)
 func HandleGetNewsArtData(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessNewsReadArt) {
+       if !cc.Authorize(AccessNewsReadArt) {
                return cc.NewErrReply(t, "You are not allowed to read news.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to read news.")
        }
 
-       var cat NewsCategoryListData15
-       cats := cc.Server.ThreadedNews.Categories
-
-       for _, fp := range ReadNewsPath(t.GetField(FieldNewsPath).Data) {
-               cat = cats[fp]
-               cats = cats[fp].SubCats
+       newsPath, err := t.GetField(FieldNewsPath).DecodeNewsPath()
+       if err != nil {
+               return res
        }
 
        }
 
-       // The official Hotline clients will send the article ID as 2 bytes if possible, but
-       // some third party clients such as Frogblast and Heildrun will always send 4 bytes
-       convertedID, err := byteToInt(t.GetField(FieldNewsArtID).Data)
+       convertedID, err := t.GetField(FieldNewsArtID).DecodeInt()
        if err != nil {
                return res
        }
 
        if err != nil {
                return res
        }
 
-       art := cat.Articles[uint32(convertedID)]
+       art := cc.Server.ThreadedNewsMgr.GetArticle(newsPath, uint32(convertedID))
        if art == nil {
                return append(res, cc.NewReply(t))
        }
        if art == nil {
                return append(res, cc.NewReply(t))
        }
@@ -1115,144 +1134,104 @@ func HandleGetNewsArtData(cc *ClientConn, t *Transaction) (res []Transaction) {
        return res
 }
 
        return res
 }
 
-// HandleDelNewsItem deletes an existing threaded news folder or category from the server.
+// HandleDelNewsItem deletes a threaded news folder or category.
 // Fields used in the request:
 // 325 News path
 // Fields used in the reply:
 // None
 func HandleDelNewsItem(cc *ClientConn, t *Transaction) (res []Transaction) {
 // Fields used in the request:
 // 325 News path
 // Fields used in the reply:
 // None
 func HandleDelNewsItem(cc *ClientConn, t *Transaction) (res []Transaction) {
-       pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
-
-       cats := cc.Server.ThreadedNews.Categories
-       delName := pathStrs[len(pathStrs)-1]
-       if len(pathStrs) > 1 {
-               for _, fp := range pathStrs[0 : len(pathStrs)-1] {
-                       cats = cats[fp].SubCats
-               }
+       pathStrs, err := t.GetField(FieldNewsPath).DecodeNewsPath()
+       if err != nil {
+               return res
        }
 
        }
 
-       if cats[delName].Type == [2]byte{0, 3} {
-               if !cc.Authorize(accessNewsDeleteCat) {
+       item := cc.Server.ThreadedNewsMgr.NewsItem(pathStrs)
+
+       if item.Type == [2]byte{0, 3} {
+               if !cc.Authorize(AccessNewsDeleteCat) {
                        return cc.NewErrReply(t, "You are not allowed to delete news categories.")
                }
        } else {
                        return cc.NewErrReply(t, "You are not allowed to delete news categories.")
                }
        } else {
-               if !cc.Authorize(accessNewsDeleteFldr) {
+               if !cc.Authorize(AccessNewsDeleteFldr) {
                        return cc.NewErrReply(t, "You are not allowed to delete news folders.")
                }
        }
 
                        return cc.NewErrReply(t, "You are not allowed to delete news folders.")
                }
        }
 
-       delete(cats, delName)
-
-       if err := cc.Server.writeThreadedNews(); err != nil {
+       err = cc.Server.ThreadedNewsMgr.DeleteNewsItem(pathStrs)
+       if err != nil {
                return res
        }
 
        return append(res, cc.NewReply(t))
 }
 
                return res
        }
 
        return append(res, cc.NewReply(t))
 }
 
+// HandleDelNewsArt deletes a threaded news article.
+// Request Fields
+// 325 News path
+// 326 News article Type
+// 337 News article recursive delete   - Delete child articles (1) or not (0)
 func HandleDelNewsArt(cc *ClientConn, t *Transaction) (res []Transaction) {
 func HandleDelNewsArt(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessNewsDeleteArt) {
+       if !cc.Authorize(AccessNewsDeleteArt) {
                return cc.NewErrReply(t, "You are not allowed to delete news articles.")
 
        }
 
                return cc.NewErrReply(t, "You are not allowed to delete news articles.")
 
        }
 
-       // Request Fields
-       // 325  News path
-       // 326  News article ID
-       // 337  News article – recursive delete       Delete child articles (1) or not (0)
-       pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
-       ID, err := byteToInt(t.GetField(FieldNewsArtID).Data)
+       pathStrs, err := t.GetField(FieldNewsPath).DecodeNewsPath()
        if err != nil {
                return res
        }
 
        if err != nil {
                return res
        }
 
-       // TODO: Delete recursive
-       cats := cc.Server.GetNewsCatByPath(pathStrs[:len(pathStrs)-1])
-
-       catName := pathStrs[len(pathStrs)-1]
-       cat := cats[catName]
+       articleID, err := t.GetField(FieldNewsArtID).DecodeInt()
+       if err != nil {
+               cc.logger.Error("error reading article Type", "err", err)
+               return
+       }
 
 
-       delete(cat.Articles, uint32(ID))
+       deleteRecursive := bytes.Equal([]byte{0, 1}, t.GetField(FieldNewsArtRecurseDel).Data)
 
 
-       cats[catName] = cat
-       if err := cc.Server.writeThreadedNews(); err != nil {
-               return res
+       err = cc.Server.ThreadedNewsMgr.DeleteArticle(pathStrs, uint32(articleID), deleteRecursive)
+       if err != nil {
+               cc.logger.Error("error deleting news article", "err", err)
        }
 
        }
 
-       res = append(res, cc.NewReply(t))
-       return res
+       return []Transaction{cc.NewReply(t)}
 }
 
 // Request fields
 // 325 News path
 }
 
 // Request fields
 // 325 News path
-// 326 News article ID                                                 ID of the parent article?
+// 326 News article Type                                                       Type of the parent article?
 // 328 News article title
 // 334 News article flags
 // 327 News article data flavor                Currently “text/plain”
 // 333 News article data
 func HandlePostNewsArt(cc *ClientConn, t *Transaction) (res []Transaction) {
 // 328 News article title
 // 334 News article flags
 // 327 News article data flavor                Currently “text/plain”
 // 333 News article data
 func HandlePostNewsArt(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessNewsPostArt) {
+       if !cc.Authorize(AccessNewsPostArt) {
                return cc.NewErrReply(t, "You are not allowed to post news articles.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to post news articles.")
        }
 
-       pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
-       cats := cc.Server.GetNewsCatByPath(pathStrs[:len(pathStrs)-1])
-
-       catName := pathStrs[len(pathStrs)-1]
-       cat := cats[catName]
-
-       artID, err := byteToInt(t.GetField(FieldNewsArtID).Data)
+       pathStrs, err := t.GetField(FieldNewsPath).DecodeNewsPath()
        if err != nil {
                return res
        }
        if err != nil {
                return res
        }
-       convertedArtID := uint32(artID)
-       bs := make([]byte, 4)
-       binary.BigEndian.PutUint32(bs, convertedArtID)
-
-       cc.Server.mux.Lock()
-       defer cc.Server.mux.Unlock()
-
-       newArt := NewsArtData{
-               Title:     string(t.GetField(FieldNewsArtTitle).Data),
-               Poster:    string(cc.UserName),
-               Date:      toHotlineTime(time.Now()),
-               ParentArt: [4]byte(bs),
-               DataFlav:  []byte("text/plain"),
-               Data:      string(t.GetField(FieldNewsArtData).Data),
-       }
-
-       var keys []int
-       for k := range cat.Articles {
-               keys = append(keys, int(k))
-       }
-
-       nextID := uint32(1)
-       if len(keys) > 0 {
-               sort.Ints(keys)
-               prevID := uint32(keys[len(keys)-1])
-               nextID = prevID + 1
-
-               binary.BigEndian.PutUint32(newArt.PrevArt[:], prevID)
-
-               // Set next article ID
-               binary.BigEndian.PutUint32(cat.Articles[prevID].NextArt[:], nextID)
-       }
-
-       // Update parent article with first child reply
-       parentID := convertedArtID
-       if parentID != 0 {
-               parentArt := cat.Articles[parentID]
 
 
-               if parentArt.FirstChildArt == [4]byte{0, 0, 0, 0} {
-                       binary.BigEndian.PutUint32(parentArt.FirstChildArt[:], nextID)
-               }
+       parentArticleID, err := t.GetField(FieldNewsArtID).DecodeInt()
+       if err != nil {
+               return res
        }
 
        }
 
-       cat.Articles[nextID] = &newArt
-
-       cats[catName] = cat
-       if err := cc.Server.writeThreadedNews(); err != nil {
-               return res
+       err = cc.Server.ThreadedNewsMgr.PostArticle(
+               pathStrs,
+               uint32(parentArticleID),
+               NewsArtData{
+                       Title:    string(t.GetField(FieldNewsArtTitle).Data),
+                       Poster:   string(cc.UserName),
+                       Date:     toHotlineTime(time.Now()),
+                       DataFlav: NewsFlavor,
+                       Data:     string(t.GetField(FieldNewsArtData).Data),
+               },
+       )
+       if err != nil {
+               cc.logger.Error("error posting news article", "err", err)
        }
 
        return append(res, cc.NewReply(t))
        }
 
        return append(res, cc.NewReply(t))
@@ -1260,17 +1239,22 @@ func HandlePostNewsArt(cc *ClientConn, t *Transaction) (res []Transaction) {
 
 // HandleGetMsgs returns the flat news data
 func HandleGetMsgs(cc *ClientConn, t *Transaction) (res []Transaction) {
 
 // HandleGetMsgs returns the flat news data
 func HandleGetMsgs(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessNewsReadArt) {
+       if !cc.Authorize(AccessNewsReadArt) {
                return cc.NewErrReply(t, "You are not allowed to read news.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to read news.")
        }
 
-       res = append(res, cc.NewReply(t, NewField(FieldData, cc.Server.FlatNews)))
+       _, _ = cc.Server.MessageBoard.Seek(0, 0)
 
 
-       return res
+       newsData, err := io.ReadAll(cc.Server.MessageBoard)
+       if err != nil {
+               // TODO
+       }
+
+       return append(res, cc.NewReply(t, NewField(FieldData, newsData)))
 }
 
 func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction) {
 }
 
 func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessDownloadFile) {
+       if !cc.Authorize(AccessDownloadFile) {
                return cc.NewErrReply(t, "You are not allowed to download files.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to download files.")
        }
 
@@ -1311,7 +1295,7 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction) {
                ft.fileResumeData = &frd
        }
 
                ft.fileResumeData = &frd
        }
 
-       // Optional field for when a HL v1.5+ client requests file preview
+       // Optional field for when a client requests file preview
        // Used only for TEXT, JPEG, GIFF, BMP or PICT files
        // The value will always be 2
        if t.GetField(FieldFileTransferOptions).Data != nil {
        // Used only for TEXT, JPEG, GIFF, BMP or PICT files
        // The value will always be 2
        if t.GetField(FieldFileTransferOptions).Data != nil {
@@ -1331,7 +1315,7 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction) {
 
 // Download all files from the specified folder and sub-folders
 func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction) {
 
 // Download all files from the specified folder and sub-folders
 func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessDownloadFile) {
+       if !cc.Authorize(AccessDownloadFile) {
                return cc.NewErrReply(t, "You are not allowed to download folders.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to download folders.")
        }
 
@@ -1348,7 +1332,6 @@ func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction) {
        if err != nil {
                return res
        }
        if err != nil {
                return res
        }
-       spew.Dump(itemCount)
 
        fileTransfer := cc.newFileTransfer(FolderDownload, t.GetField(FieldFileName).Data, t.GetField(FieldFilePath).Data, transferSize)
 
 
        fileTransfer := cc.newFileTransfer(FolderDownload, t.GetField(FieldFileName).Data, t.GetField(FieldFilePath).Data, transferSize)
 
@@ -1383,7 +1366,7 @@ func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction) {
        }
 
        // Handle special cases for Upload and Drop Box folders
        }
 
        // Handle special cases for Upload and Drop Box folders
-       if !cc.Authorize(accessUploadAnywhere) {
+       if !cc.Authorize(AccessUploadAnywhere) {
                if !fp.IsUploadDir() && !fp.IsDropbox() {
                        return cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload of the folder \"%v\" because you are only allowed to upload to the \"Uploads\" folder.", string(t.GetField(FieldFileName).Data)))
                }
                if !fp.IsUploadDir() && !fp.IsDropbox() {
                        return cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload of the folder \"%v\" because you are only allowed to upload to the \"Uploads\" folder.", string(t.GetField(FieldFileName).Data)))
                }
@@ -1408,7 +1391,7 @@ func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction) {
 // Used only to resume download, currently has value 2"
 // 108 File transfer size      "Optional used if download is not resumed"
 func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction) {
 // Used only to resume download, currently has value 2"
 // 108 File transfer size      "Optional used if download is not resumed"
 func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessUploadFile) {
+       if !cc.Authorize(AccessUploadFile) {
                return cc.NewErrReply(t, "You are not allowed to upload files.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to upload files.")
        }
 
@@ -1425,7 +1408,7 @@ func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction) {
        }
 
        // Handle special cases for Upload and Drop Box folders
        }
 
        // Handle special cases for Upload and Drop Box folders
-       if !cc.Authorize(accessUploadAnywhere) {
+       if !cc.Authorize(AccessUploadAnywhere) {
                if !fp.IsUploadDir() && !fp.IsDropbox() {
                        return cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload of the file \"%v\" because you are only allowed to upload to the \"Uploads\" folder.", string(fileName)))
                }
                if !fp.IsUploadDir() && !fp.IsDropbox() {
                        return cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload of the file \"%v\" because you are only allowed to upload to the \"Uploads\" folder.", string(fileName)))
                }
@@ -1474,24 +1457,24 @@ func HandleSetClientUserInfo(cc *ClientConn, t *Transaction) (res []Transaction)
        } else {
                cc.Icon = t.GetField(FieldUserIconID).Data
        }
        } else {
                cc.Icon = t.GetField(FieldUserIconID).Data
        }
-       if cc.Authorize(accessAnyName) {
+       if cc.Authorize(AccessAnyName) {
                cc.UserName = t.GetField(FieldUserName).Data
        }
 
                cc.UserName = t.GetField(FieldUserName).Data
        }
 
-       cc.flagsMU.Lock()
-       defer cc.flagsMU.Unlock()
-
        // the options field is only passed by the client versions > 1.2.3.
        options := t.GetField(FieldOptions).Data
        if options != nil {
                optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
        // the options field is only passed by the client versions > 1.2.3.
        options := t.GetField(FieldOptions).Data
        if options != nil {
                optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
-               flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(cc.Flags[:])))
 
 
-               flagBitmap.SetBit(flagBitmap, UserFlagRefusePM, optBitmap.Bit(UserOptRefusePM))
-               binary.BigEndian.PutUint16(cc.Flags[:], uint16(flagBitmap.Int64()))
+               //flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(cc.Flags[:])))
+               //flagBitmap.SetBit(flagBitmap, UserFlagRefusePM, optBitmap.Bit(UserOptRefusePM))
+               //binary.BigEndian.PutUint16(cc.Flags[:], uint16(flagBitmap.Int64()))
 
 
-               flagBitmap.SetBit(flagBitmap, UserFlagRefusePChat, optBitmap.Bit(UserOptRefuseChat))
-               binary.BigEndian.PutUint16(cc.Flags[:], uint16(flagBitmap.Int64()))
+               cc.Flags.Set(UserFlagRefusePM, optBitmap.Bit(UserOptRefusePM))
+               cc.Flags.Set(UserFlagRefusePChat, optBitmap.Bit(UserOptRefuseChat))
+               //
+               //flagBitmap.SetBit(flagBitmap, UserFlagRefusePChat, optBitmap.Bit(UserOptRefuseChat))
+               //binary.BigEndian.PutUint16(cc.Flags[:], uint16(flagBitmap.Int64()))
 
                // Check auto response
                if optBitmap.Bit(UserOptAutoResponse) == 1 {
 
                // Check auto response
                if optBitmap.Bit(UserOptAutoResponse) == 1 {
@@ -1501,7 +1484,7 @@ func HandleSetClientUserInfo(cc *ClientConn, t *Transaction) (res []Transaction)
                }
        }
 
                }
        }
 
-       for _, c := range cc.Server.Clients {
+       for _, c := range cc.Server.ClientMgr.List() {
                res = append(res, NewTransaction(
                        TranNotifyChangeUser,
                        c.ID,
                res = append(res, NewTransaction(
                        TranNotifyChangeUser,
                        c.ID,
@@ -1542,7 +1525,7 @@ func HandleGetFileNameList(cc *ClientConn, t *Transaction) (res []Transaction) {
        }
 
        // Handle special case for drop box folders
        }
 
        // Handle special case for drop box folders
-       if fp.IsDropbox() && !cc.Authorize(accessViewDropBoxes) {
+       if fp.IsDropbox() && !cc.Authorize(AccessViewDropBoxes) {
                return cc.NewErrReply(t, "You are not allowed to view drop boxes.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to view drop boxes.")
        }
 
@@ -1559,10 +1542,10 @@ func HandleGetFileNameList(cc *ClientConn, t *Transaction) (res []Transaction) {
 // =================================
 //     Hotline private chat flow
 // =================================
 // =================================
 //     Hotline private chat flow
 // =================================
-// 1. ClientA sends TranInviteNewChat to server with user ID to invite
+// 1. ClientA sends TranInviteNewChat to server with user Type to invite
 // 2. Server creates new ChatID
 // 3. Server sends TranInviteToChat to invitee
 // 2. Server creates new ChatID
 // 3. Server sends TranInviteToChat to invitee
-// 4. Server replies to ClientA with new Chat ID
+// 4. Server replies to ClientA with new Chat Type
 //
 // A dialog box pops up in the invitee client with options to accept or decline the invitation.
 // If Accepted is clicked:
 //
 // A dialog box pops up in the invitee client with options to accept or decline the invitation.
 // If Accepted is clicked:
@@ -1570,16 +1553,18 @@ func HandleGetFileNameList(cc *ClientConn, t *Transaction) (res []Transaction) {
 
 // HandleInviteNewChat invites users to new private chat
 func HandleInviteNewChat(cc *ClientConn, t *Transaction) (res []Transaction) {
 
 // HandleInviteNewChat invites users to new private chat
 func HandleInviteNewChat(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessOpenChat) {
+       if !cc.Authorize(AccessOpenChat) {
                return cc.NewErrReply(t, "You are not allowed to request private chat.")
        }
 
        // Client to Invite
        targetID := t.GetField(FieldUserID).Data
                return cc.NewErrReply(t, "You are not allowed to request private chat.")
        }
 
        // Client to Invite
        targetID := t.GetField(FieldUserID).Data
-       newChatID := cc.Server.NewPrivateChat(cc)
+
+       // Create a new chat with self as initial member.
+       newChatID := cc.Server.ChatMgr.New(cc)
 
        // Check if target user has "Refuse private chat" flag
 
        // Check if target user has "Refuse private chat" flag
-       targetClient := cc.Server.Clients[[2]byte(targetID)]
+       targetClient := cc.Server.ClientMgr.Get([2]byte(targetID))
        flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(targetClient.Flags[:])))
        if flagBitmap.Bit(UserFlagRefusePChat) == 1 {
                res = append(res,
        flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(targetClient.Flags[:])))
        if flagBitmap.Bit(UserFlagRefusePChat) == 1 {
                res = append(res,
@@ -1604,7 +1589,8 @@ func HandleInviteNewChat(cc *ClientConn, t *Transaction) (res []Transaction) {
                )
        }
 
                )
        }
 
-       res = append(res,
+       return append(
+               res,
                cc.NewReply(t,
                        NewField(FieldChatID, newChatID[:]),
                        NewField(FieldUserName, cc.UserName),
                cc.NewReply(t,
                        NewField(FieldChatID, newChatID[:]),
                        NewField(FieldUserName, cc.UserName),
@@ -1613,12 +1599,10 @@ func HandleInviteNewChat(cc *ClientConn, t *Transaction) (res []Transaction) {
                        NewField(FieldUserFlags, cc.Flags[:]),
                ),
        )
                        NewField(FieldUserFlags, cc.Flags[:]),
                ),
        )
-
-       return res
 }
 
 func HandleInviteToChat(cc *ClientConn, t *Transaction) (res []Transaction) {
 }
 
 func HandleInviteToChat(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessOpenChat) {
+       if !cc.Authorize(AccessOpenChat) {
                return cc.NewErrReply(t, "You are not allowed to request private chat.")
        }
 
                return cc.NewErrReply(t, "You are not allowed to request private chat.")
        }
 
@@ -1647,9 +1631,8 @@ func HandleInviteToChat(cc *ClientConn, t *Transaction) (res []Transaction) {
 
 func HandleRejectChatInvite(cc *ClientConn, t *Transaction) (res []Transaction) {
        chatID := [4]byte(t.GetField(FieldChatID).Data)
 
 func HandleRejectChatInvite(cc *ClientConn, t *Transaction) (res []Transaction) {
        chatID := [4]byte(t.GetField(FieldChatID).Data)
-       privChat := cc.Server.PrivateChats[chatID]
 
 
-       for _, c := range privChat.ClientConn {
+       for _, c := range cc.Server.ChatMgr.Members(chatID) {
                res = append(res,
                        NewTransaction(
                                TranChatMsg,
                res = append(res,
                        NewTransaction(
                                TranChatMsg,
@@ -1671,10 +1654,8 @@ func HandleRejectChatInvite(cc *ClientConn, t *Transaction) (res []Transaction)
 func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction) {
        chatID := t.GetField(FieldChatID).Data
 
 func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction) {
        chatID := t.GetField(FieldChatID).Data
 
-       privChat := cc.Server.PrivateChats[[4]byte(chatID)]
-
        // Send TranNotifyChatChangeUser to current members of the chat to inform of new user
        // Send TranNotifyChatChangeUser to current members of the chat to inform of new user
-       for _, c := range privChat.ClientConn {
+       for _, c := range cc.Server.ChatMgr.Members([4]byte(chatID)) {
                res = append(res,
                        NewTransaction(
                                TranNotifyChatChangeUser,
                res = append(res,
                        NewTransaction(
                                TranNotifyChatChangeUser,
@@ -1688,10 +1669,12 @@ func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction) {
                )
        }
 
                )
        }
 
-       privChat.ClientConn[cc.ID] = cc
+       cc.Server.ChatMgr.Join(ChatID(chatID), cc)
+
+       subject := cc.Server.ChatMgr.GetSubject(ChatID(chatID))
 
 
-       replyFields := []Field{NewField(FieldChatSubject, []byte(privChat.Subject))}
-       for _, c := range privChat.ClientConn {
+       replyFields := []Field{NewField(FieldChatSubject, []byte(subject))}
+       for _, c := range cc.Server.ChatMgr.Members([4]byte(chatID)) {
                b, err := io.ReadAll(&User{
                        ID:    c.ID,
                        Icon:  c.Icon,
                b, err := io.ReadAll(&User{
                        ID:    c.ID,
                        Icon:  c.Icon,
@@ -1704,8 +1687,7 @@ func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction) {
                replyFields = append(replyFields, NewField(FieldUsernameWithInfo, b))
        }
 
                replyFields = append(replyFields, NewField(FieldUsernameWithInfo, b))
        }
 
-       res = append(res, cc.NewReply(t, replyFields...))
-       return res
+       return append(res, cc.NewReply(t, replyFields...))
 }
 
 // HandleLeaveChat is sent from a v1.8+ Hotline client when the user exits a private chat
 }
 
 // HandleLeaveChat is sent from a v1.8+ Hotline client when the user exits a private chat
@@ -1716,15 +1698,10 @@ func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction) {
 func HandleLeaveChat(cc *ClientConn, t *Transaction) (res []Transaction) {
        chatID := t.GetField(FieldChatID).Data
 
 func HandleLeaveChat(cc *ClientConn, t *Transaction) (res []Transaction) {
        chatID := t.GetField(FieldChatID).Data
 
-       privChat, ok := cc.Server.PrivateChats[[4]byte(chatID)]
-       if !ok {
-               return res
-       }
-
-       delete(privChat.ClientConn, cc.ID)
+       cc.Server.ChatMgr.Leave([4]byte(chatID), cc.ID)
 
        // Notify members of the private chat that the user has left
 
        // Notify members of the private chat that the user has left
-       for _, c := range privChat.ClientConn {
+       for _, c := range cc.Server.ChatMgr.Members(ChatID(chatID)) {
                res = append(res,
                        NewTransaction(
                                TranNotifyChatDeleteUser,
                res = append(res,
                        NewTransaction(
                                TranNotifyChatDeleteUser,
@@ -1740,16 +1717,16 @@ func HandleLeaveChat(cc *ClientConn, t *Transaction) (res []Transaction) {
 
 // HandleSetChatSubject is sent from a v1.8+ Hotline client when the user sets a private chat subject
 // Fields used in the request:
 
 // HandleSetChatSubject is sent from a v1.8+ Hotline client when the user sets a private chat subject
 // Fields used in the request:
-// * 114       Chat ID
+// * 114       Chat Type
 // * 115       Chat subject
 // Reply is not expected.
 func HandleSetChatSubject(cc *ClientConn, t *Transaction) (res []Transaction) {
        chatID := t.GetField(FieldChatID).Data
 
 // * 115       Chat subject
 // Reply is not expected.
 func HandleSetChatSubject(cc *ClientConn, t *Transaction) (res []Transaction) {
        chatID := t.GetField(FieldChatID).Data
 
-       privChat := cc.Server.PrivateChats[[4]byte(chatID)]
-       privChat.Subject = string(t.GetField(FieldChatSubject).Data)
+       cc.Server.ChatMgr.SetSubject([4]byte(chatID), string(t.GetField(FieldChatSubject).Data))
 
 
-       for _, c := range privChat.ClientConn {
+       // Notify chat members of new subject.
+       for _, c := range cc.Server.ChatMgr.Members([4]byte(chatID)) {
                res = append(res,
                        NewTransaction(
                                TranNotifyChatSubject,
                res = append(res,
                        NewTransaction(
                                TranNotifyChatSubject,
@@ -1772,7 +1749,7 @@ func HandleSetChatSubject(cc *ClientConn, t *Transaction) (res []Transaction) {
 // Fields used in the reply:
 // None
 func HandleMakeAlias(cc *ClientConn, t *Transaction) (res []Transaction) {
 // Fields used in the reply:
 // None
 func HandleMakeAlias(cc *ClientConn, t *Transaction) (res []Transaction) {
-       if !cc.Authorize(accessMakeAlias) {
+       if !cc.Authorize(AccessMakeAlias) {
                return cc.NewErrReply(t, "You are not allowed to make aliases.")
        }
        fileName := t.GetField(FieldFileName).Data
                return cc.NewErrReply(t, "You are not allowed to make aliases.")
        }
        fileName := t.GetField(FieldFileName).Data
@@ -1806,7 +1783,7 @@ func HandleMakeAlias(cc *ClientConn, t *Transaction) (res []Transaction) {
 // 107 FieldRefNum                     Used later for transfer
 // 108 FieldTransferSize       Size of data to be downloaded
 func HandleDownloadBanner(cc *ClientConn, t *Transaction) (res []Transaction) {
 // 107 FieldRefNum                     Used later for transfer
 // 108 FieldTransferSize       Size of data to be downloaded
 func HandleDownloadBanner(cc *ClientConn, t *Transaction) (res []Transaction) {
-       ft := cc.newFileTransfer(bannerDownload, []byte{}, []byte{}, make([]byte, 4))
+       ft := cc.newFileTransfer(BannerDownload, []byte{}, []byte{}, make([]byte, 4))
        binary.BigEndian.PutUint32(ft.TransferSize, uint32(len(cc.Server.banner)))
 
        return append(res, cc.NewReply(t,
        binary.BigEndian.PutUint32(ft.TransferSize, uint32(len(cc.Server.banner)))
 
        return append(res, cc.NewReply(t,
index b7cc0afbc609ac78dbaeea3506f0029fa57b8bce..610e11399f7516467b844f0fe053598cc5596f76 100644 (file)
@@ -9,7 +9,6 @@ import (
        "os"
        "path/filepath"
        "strings"
        "os"
        "path/filepath"
        "strings"
-       "sync"
        "testing"
        "time"
 )
        "testing"
        "time"
 )
@@ -30,39 +29,63 @@ func TestHandleSetChatSubject(t *testing.T) {
                                cc: &ClientConn{
                                        UserName: []byte{0x00, 0x01},
                                        Server: &Server{
                                cc: &ClientConn{
                                        UserName: []byte{0x00, 0x01},
                                        Server: &Server{
-                                               PrivateChats: map[[4]byte]*PrivateChat{
-                                                       [4]byte{0, 0, 0, 1}: {
-                                                               Subject: "unset",
-                                                               ClientConn: map[[2]byte]*ClientConn{
-                                                                       [2]byte{0, 1}: {
-                                                                               Account: &Account{
-                                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
-                                                                               },
-                                                                               ID: [2]byte{0, 1},
+                                               ChatMgr: func() *MockChatManager {
+                                                       m := MockChatManager{}
+                                                       m.On("Members", ChatID{0x0, 0x0, 0x0, 0x1}).Return([]*ClientConn{
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
                                                                        },
                                                                        },
-                                                                       [2]byte{0, 2}: {
-                                                                               Account: &Account{
-                                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
-                                                                               },
-                                                                               ID: [2]byte{0, 2},
+                                                                       ID: [2]byte{0, 1},
+                                                               },
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
                                                                        },
                                                                        },
+                                                                       ID: [2]byte{0, 2},
                                                                },
                                                                },
-                                                       },
-                                               },
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 1}: {
-                                                               Account: &Account{
-                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                       })
+                                                       m.On("SetSubject", ChatID{0x0, 0x0, 0x0, 0x1}, "Test Subject")
+                                                       return &m
+                                               }(),
+                                               //PrivateChats: map[[4]byte]*PrivateChat{
+                                               //      [4]byte{0, 0, 0, 1}: {
+                                               //              Subject: "unset",
+                                               //              ClientConn: map[[2]byte]*ClientConn{
+                                               //                      [2]byte{0, 1}: {
+                                               //                              Account: &Account{
+                                               //                                      Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                               //                              },
+                                               //                              ID: [2]byte{0, 1},
+                                               //                      },
+                                               //                      [2]byte{0, 2}: {
+                                               //                              Account: &Account{
+                                               //                                      Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                               //                              },
+                                               //                              ID: [2]byte{0, 2},
+                                               //                      },
+                                               //              },
+                                               //      },
+                                               //},
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("List").Return([]*ClientConn{
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                                       },
+                                                                       ID: [2]byte{0, 1},
                                                                },
                                                                },
-                                                               ID: [2]byte{0, 1},
-                                                       },
-                                                       [2]byte{0, 2}: {
-                                                               Account: &Account{
-                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                                       },
+                                                                       ID: [2]byte{0, 2},
                                                                },
                                                                },
-                                                               ID: [2]byte{0, 2},
                                                        },
                                                        },
-                                               },
+                                                       )
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: Transaction{
                                        },
                                },
                                t: Transaction{
@@ -115,43 +138,44 @@ func TestHandleLeaveChat(t *testing.T) {
                want []Transaction
        }{
                {
                want []Transaction
        }{
                {
-                       name: "returns expected transactions",
+                       name: "when client 2 leaves chat",
                        args: args{
                                cc: &ClientConn{
                                        ID: [2]byte{0, 2},
                                        Server: &Server{
                        args: args{
                                cc: &ClientConn{
                                        ID: [2]byte{0, 2},
                                        Server: &Server{
-                                               PrivateChats: map[[4]byte]*PrivateChat{
-                                                       [4]byte{0, 0, 0, 1}: {
-                                                               ClientConn: map[[2]byte]*ClientConn{
-                                                                       [2]byte{0, 1}: {
-                                                                               Account: &Account{
-                                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
-                                                                               },
-                                                                               ID: [2]byte{0, 1},
-                                                                       },
-                                                                       [2]byte{0, 2}: {
-                                                                               Account: &Account{
-                                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
-                                                                               },
-                                                                               ID: [2]byte{0, 2},
+                                               ChatMgr: func() *MockChatManager {
+                                                       m := MockChatManager{}
+                                                       m.On("Members", ChatID{0x0, 0x0, 0x0, 0x1}).Return([]*ClientConn{
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
                                                                        },
                                                                        },
+                                                                       ID: [2]byte{0, 1},
                                                                },
                                                                },
-                                                       },
-                                               },
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 1}: {
-                                                               Account: &Account{
-                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                       })
+                                                       m.On("Leave", ChatID{0x0, 0x0, 0x0, 0x1}, [2]uint8{0x0, 0x2})
+                                                       m.On("GetSubject").Return("unset")
+                                                       return &m
+                                               }(),
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("Get").Return([]*ClientConn{
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                                       },
+                                                                       ID: [2]byte{0, 1},
                                                                },
                                                                },
-                                                               ID: [2]byte{0, 1},
-                                                       },
-                                                       [2]byte{0, 2}: {
-                                                               Account: &Account{
-                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                                       },
+                                                                       ID: [2]byte{0, 2},
                                                                },
                                                                },
-                                                               ID: [2]byte{0, 2},
                                                        },
                                                        },
-                                               },
+                                                       )
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: NewTransaction(TranDeleteUser, [2]byte{}, NewField(FieldChatID, []byte{0, 0, 0, 1})),
                                        },
                                },
                                t: NewTransaction(TranDeleteUser, [2]byte{}, NewField(FieldChatID, []byte{0, 0, 0, 1})),
@@ -194,20 +218,25 @@ func TestHandleGetUserNameList(t *testing.T) {
                                cc: &ClientConn{
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
                                cc: &ClientConn{
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 1}: {
-                                                               ID:       [2]byte{0, 1},
-                                                               Icon:     []byte{0, 2},
-                                                               Flags:    [2]byte{0, 3},
-                                                               UserName: []byte{0, 4},
-                                                       },
-                                                       [2]byte{0, 2}: {
-                                                               ID:       [2]byte{0, 2},
-                                                               Icon:     []byte{0, 2},
-                                                               Flags:    [2]byte{0, 3},
-                                                               UserName: []byte{0, 4},
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("List").Return([]*ClientConn{
+                                                               {
+                                                                       ID:       [2]byte{0, 1},
+                                                                       Icon:     []byte{0, 2},
+                                                                       Flags:    [2]byte{0, 3},
+                                                                       UserName: []byte{0, 4},
+                                                               },
+                                                               {
+                                                                       ID:       [2]byte{0, 2},
+                                                                       Icon:     []byte{0, 2},
+                                                                       Flags:    [2]byte{0, 3},
+                                                                       UserName: []byte{0, 4},
+                                                               },
                                                        },
                                                        },
-                                               },
+                                                       )
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: Transaction{},
                                        },
                                },
                                t: Transaction{},
@@ -255,26 +284,31 @@ func TestHandleChatSend(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessSendChat)
+                                                       bits.Set(AccessSendChat)
                                                        return bits
                                                }(),
                                        },
                                        UserName: []byte{0x00, 0x01},
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        UserName: []byte{0x00, 0x01},
                                        Server: &Server{
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 1}: {
-                                                               Account: &Account{
-                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("List").Return([]*ClientConn{
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                                       },
+                                                                       ID: [2]byte{0, 1},
                                                                },
                                                                },
-                                                               ID: [2]byte{0, 1},
-                                                       },
-                                                       [2]byte{0, 2}: {
-                                                               Account: &Account{
-                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                                       },
+                                                                       ID: [2]byte{0, 2},
                                                                },
                                                                },
-                                                               ID: [2]byte{0, 2},
                                                        },
                                                        },
-                                               },
+                                                       )
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: Transaction{
                                        },
                                },
                                t: Transaction{
@@ -305,32 +339,37 @@ func TestHandleChatSend(t *testing.T) {
                        },
                },
                {
                        },
                },
                {
-                       name: "treats Chat ID 00 00 00 00 as a public chat message",
+                       name: "treats Chat Type 00 00 00 00 as a public chat message",
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessSendChat)
+                                                       bits.Set(AccessSendChat)
                                                        return bits
                                                }(),
                                        },
                                        UserName: []byte{0x00, 0x01},
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        UserName: []byte{0x00, 0x01},
                                        Server: &Server{
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 1}: {
-                                                               Account: &Account{
-                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("List").Return([]*ClientConn{
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                                       },
+                                                                       ID: [2]byte{0, 1},
                                                                },
                                                                },
-                                                               ID: [2]byte{0, 1},
-                                                       },
-                                                       [2]byte{0, 2}: {
-                                                               Account: &Account{
-                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                                       },
+                                                                       ID: [2]byte{0, 2},
                                                                },
                                                                },
-                                                               ID: [2]byte{0, 2},
                                                        },
                                                        },
-                                               },
+                                                       )
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: Transaction{
                                        },
                                },
                                t: Transaction{
@@ -368,7 +407,7 @@ func TestHandleChatSend(t *testing.T) {
                                                }(),
                                        },
                                        Server: &Server{
                                                }(),
                                        },
                                        Server: &Server{
-                                               Accounts: map[string]*Account{},
+                                               //Accounts: map[string]*Account{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -393,26 +432,31 @@ func TestHandleChatSend(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessSendChat)
+                                                       bits.Set(AccessSendChat)
                                                        return bits
                                                }(),
                                        },
                                        UserName: []byte("Testy McTest"),
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        UserName: []byte("Testy McTest"),
                                        Server: &Server{
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 1}: {
-                                                               Account: &Account{
-                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("List").Return([]*ClientConn{
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                                       },
+                                                                       ID: [2]byte{0, 1},
                                                                },
                                                                },
-                                                               ID: [2]byte{0, 1},
-                                                       },
-                                                       [2]byte{0, 2}: {
-                                                               Account: &Account{
-                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                                       },
+                                                                       ID: [2]byte{0, 2},
                                                                },
                                                                },
-                                                               ID: [2]byte{0, 2},
                                                        },
                                                        },
-                                               },
+                                                       )
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: Transaction{
                                        },
                                },
                                t: Transaction{
@@ -450,26 +494,31 @@ func TestHandleChatSend(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessSendChat)
+                                                       bits.Set(AccessSendChat)
                                                        return bits
                                                }(),
                                        },
                                        UserName: []byte("Testy McTest"),
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        UserName: []byte("Testy McTest"),
                                        Server: &Server{
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 1}: {
-                                                               Account: &Account{
-                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("List").Return([]*ClientConn{
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                                       },
+                                                                       ID: [2]byte{0, 1},
                                                                },
                                                                },
-                                                               ID: [2]byte{0, 1},
-                                                       },
-                                                       [2]byte{0, 2}: {
-                                                               Account: &Account{
-                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                                       },
+                                                                       ID: [2]byte{0, 2},
                                                                },
                                                                },
-                                                               ID: [2]byte{0, 2},
                                                        },
                                                        },
-                                               },
+                                                       )
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: Transaction{
                                        },
                                },
                                t: Transaction{
@@ -497,35 +546,39 @@ func TestHandleChatSend(t *testing.T) {
                        },
                },
                {
                        },
                },
                {
-                       name: "only sends chat msg to clients with accessReadChat permission",
+                       name: "only sends chat msg to clients with AccessReadChat permission",
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessSendChat)
+                                                       bits.Set(AccessSendChat)
                                                        return bits
                                                }(),
                                        },
                                        UserName: []byte{0x00, 0x01},
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        UserName: []byte{0x00, 0x01},
                                        Server: &Server{
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 1}: {
-                                                               Account: &Account{
-                                                                       Access: func() accessBitmap {
-                                                                               var bits accessBitmap
-                                                                               bits.Set(accessReadChat)
-                                                                               return bits
-                                                                       }()},
-                                                               ID: [2]byte{0, 1},
-                                                       },
-                                                       [2]byte{0, 2}: {
-                                                               Account: &Account{
-                                                                       Access: accessBitmap{0, 0, 0, 0, 0, 0, 0, 0},
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("List").Return([]*ClientConn{
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: func() accessBitmap {
+                                                                                       var bits accessBitmap
+                                                                                       bits.Set(AccessReadChat)
+                                                                                       return bits
+                                                                               }(),
+                                                                       },
+                                                                       ID: [2]byte{0, 1},
+                                                               },
+                                                               {
+                                                                       Account: &Account{},
+                                                                       ID:      [2]byte{0, 2},
                                                                },
                                                                },
-                                                               ID: [2]byte{0, 2},
                                                        },
                                                        },
-                                               },
+                                                       )
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: Transaction{
                                        },
                                },
                                t: Transaction{
@@ -551,44 +604,50 @@ func TestHandleChatSend(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessSendChat)
+                                                       bits.Set(AccessSendChat)
                                                        return bits
                                                }(),
                                        },
                                        UserName: []byte{0x00, 0x01},
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        UserName: []byte{0x00, 0x01},
                                        Server: &Server{
-                                               PrivateChats: map[[4]byte]*PrivateChat{
-                                                       [4]byte{0, 0, 0, 1}: {
-                                                               ClientConn: map[[2]byte]*ClientConn{
-                                                                       [2]byte{0, 1}: {
-                                                                               ID: [2]byte{0, 1},
-                                                                       },
-                                                                       [2]byte{0, 2}: {
-                                                                               ID: [2]byte{0, 2},
-                                                                       },
+                                               ChatMgr: func() *MockChatManager {
+                                                       m := MockChatManager{}
+                                                       m.On("Members", ChatID{0x0, 0x0, 0x0, 0x1}).Return([]*ClientConn{
+                                                               {
+                                                                       ID: [2]byte{0, 1},
                                                                },
                                                                },
-                                                       },
-                                               },
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 1}: {
-                                                               Account: &Account{
-                                                                       Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                               {
+                                                                       ID: [2]byte{0, 2},
                                                                },
                                                                },
-                                                               ID: [2]byte{0, 1},
-                                                       },
-                                                       [2]byte{0, 2}: {
-                                                               Account: &Account{
-                                                                       Access: accessBitmap{0, 0, 0, 0, 0, 0, 0, 0},
+                                                       })
+                                                       m.On("GetSubject").Return("unset")
+                                                       return &m
+                                               }(),
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("List").Return([]*ClientConn{
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                                       },
+                                                                       ID: [2]byte{0, 1},
                                                                },
                                                                },
-                                                               ID: [2]byte{0, 2},
-                                                       },
-                                                       [2]byte{0, 3}: {
-                                                               Account: &Account{
-                                                                       Access: accessBitmap{0, 0, 0, 0, 0, 0, 0, 0},
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{0, 0, 0, 0, 0, 0, 0, 0},
+                                                                       },
+                                                                       ID: [2]byte{0, 2},
+                                                               },
+                                                               {
+                                                                       Account: &Account{
+                                                                               Access: accessBitmap{0, 0, 0, 0, 0, 0, 0, 0},
+                                                                       },
+                                                                       ID: [2]byte{0, 3},
                                                                },
                                                                },
-                                                               ID: [2]byte{0, 3},
                                                        },
                                                        },
-                                               },
+                                                       )
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: Transaction{
                                        },
                                },
                                t: Transaction{
@@ -643,7 +702,7 @@ func TestHandleGetFileInfo(t *testing.T) {
                                        ID: [2]byte{0x00, 0x01},
                                        Server: &Server{
                                                FS: &OSFileStore{},
                                        ID: [2]byte{0x00, 0x01},
                                        Server: &Server{
                                                FS: &OSFileStore{},
-                                               Config: &Config{
+                                               Config: Config{
                                                        FileRoot: func() string {
                                                                path, _ := os.Getwd()
                                                                return filepath.Join(path, "/test/config/Files")
                                                        FileRoot: func() string {
                                                                path, _ := os.Getwd()
                                                                return filepath.Join(path, "/test/config/Files")
@@ -734,13 +793,13 @@ func TestHandleNewFolder(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessCreateFolder)
+                                                       bits.Set(AccessCreateFolder)
                                                        return bits
                                                }(),
                                        },
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
-                                               Config: &Config{
+                                               Config: Config{
                                                        FileRoot: "/Files/",
                                                },
                                                FS: func() *MockFileStore {
                                                        FileRoot: "/Files/",
                                                },
                                                FS: func() *MockFileStore {
@@ -776,13 +835,13 @@ func TestHandleNewFolder(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessCreateFolder)
+                                                       bits.Set(AccessCreateFolder)
                                                        return bits
                                                }(),
                                        },
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
-                                               Config: &Config{
+                                               Config: Config{
                                                        FileRoot: "/Files",
                                                },
                                                FS: func() *MockFileStore {
                                                        FileRoot: "/Files",
                                                },
                                                FS: func() *MockFileStore {
@@ -812,13 +871,13 @@ func TestHandleNewFolder(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessCreateFolder)
+                                                       bits.Set(AccessCreateFolder)
                                                        return bits
                                                }(),
                                        },
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
-                                               Config: &Config{
+                                               Config: Config{
                                                        FileRoot: "/Files/",
                                                },
                                                FS: func() *MockFileStore {
                                                        FileRoot: "/Files/",
                                                },
                                                FS: func() *MockFileStore {
@@ -846,13 +905,13 @@ func TestHandleNewFolder(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessCreateFolder)
+                                                       bits.Set(AccessCreateFolder)
                                                        return bits
                                                }(),
                                        },
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
-                                               Config: &Config{
+                                               Config: Config{
                                                        FileRoot: "/Files/",
                                                },
                                                FS: func() *MockFileStore {
                                                        FileRoot: "/Files/",
                                                },
                                                FS: func() *MockFileStore {
@@ -882,13 +941,13 @@ func TestHandleNewFolder(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessCreateFolder)
+                                                       bits.Set(AccessCreateFolder)
                                                        return bits
                                                }(),
                                        },
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
-                                               Config: &Config{
+                                               Config: Config{
                                                        FileRoot: "/Files/",
                                                },
                                                FS: func() *MockFileStore {
                                                        FileRoot: "/Files/",
                                                },
                                                FS: func() *MockFileStore {
@@ -947,19 +1006,17 @@ func TestHandleUploadFile(t *testing.T) {
                        args: args{
                                cc: &ClientConn{
                                        Server: &Server{
                        args: args{
                                cc: &ClientConn{
                                        Server: &Server{
-                                               FS:            &OSFileStore{},
-                                               fileTransfers: map[[4]byte]*FileTransfer{},
-                                               Config: &Config{
+                                               FS:              &OSFileStore{},
+                                               FileTransferMgr: NewMemFileTransferMgr(),
+                                               Config: Config{
                                                        FileRoot: func() string { path, _ := os.Getwd(); return path + "/test/config/Files" }(),
                                                }},
                                                        FileRoot: func() string { path, _ := os.Getwd(); return path + "/test/config/Files" }(),
                                                }},
-                                       transfers: map[int]map[[4]byte]*FileTransfer{
-                                               FileUpload: {},
-                                       },
+                                       ClientFileTransferMgr: NewClientFileTransferMgr(),
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessUploadFile)
-                                                       bits.Set(accessUploadAnywhere)
+                                                       bits.Set(AccessUploadFile)
+                                                       bits.Set(AccessUploadAnywhere)
                                                        return bits
                                                }(),
                                        },
                                                        return bits
                                                }(),
                                        },
@@ -1043,12 +1100,12 @@ func TestHandleMakeAlias(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessMakeAlias)
+                                                       bits.Set(AccessMakeAlias)
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
-                                               Config: &Config{
+                                               Config: Config{
                                                        FileRoot: func() string {
                                                                path, _ := os.Getwd()
                                                                return path + "/test/config/Files"
                                                        FileRoot: func() string {
                                                                path, _ := os.Getwd()
                                                                return path + "/test/config/Files"
@@ -1089,12 +1146,12 @@ func TestHandleMakeAlias(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessMakeAlias)
+                                                       bits.Set(AccessMakeAlias)
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
-                                               Config: &Config{
+                                               Config: Config{
                                                        FileRoot: func() string {
                                                                path, _ := os.Getwd()
                                                                return path + "/test/config/Files"
                                                        FileRoot: func() string {
                                                                path, _ := os.Getwd()
                                                                return path + "/test/config/Files"
@@ -1136,13 +1193,10 @@ func TestHandleMakeAlias(t *testing.T) {
                                cc: &ClientConn{
                                        logger: NewTestLogger(),
                                        Account: &Account{
                                cc: &ClientConn{
                                        logger: NewTestLogger(),
                                        Account: &Account{
-                                               Access: func() accessBitmap {
-                                                       var bits accessBitmap
-                                                       return bits
-                                               }(),
+                                               Access: accessBitmap{},
                                        },
                                        Server: &Server{
                                        },
                                        Server: &Server{
-                                               Config: &Config{
+                                               Config: Config{
                                                        FileRoot: func() string {
                                                                path, _ := os.Getwd()
                                                                return path + "/test/config/Files"
                                                        FileRoot: func() string {
                                                                path, _ := os.Getwd()
                                                                return path + "/test/config/Files"
@@ -1203,19 +1257,21 @@ func TestHandleGetUser(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessOpenUser)
+                                                       bits.Set(AccessOpenUser)
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
-                                               Accounts: map[string]*Account{
-                                                       "guest": {
+                                               AccountManager: func() *MockAccountManager {
+                                                       m := MockAccountManager{}
+                                                       m.On("Get", "guest").Return(&Account{
                                                                Login:    "guest",
                                                                Name:     "Guest",
                                                                Password: "password",
                                                                Access:   accessBitmap{},
                                                                Login:    "guest",
                                                                Name:     "Guest",
                                                                Password: "password",
                                                                Access:   accessBitmap{},
-                                                       },
-                                               },
+                                                       })
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -1246,7 +1302,7 @@ func TestHandleGetUser(t *testing.T) {
                                                }(),
                                        },
                                        Server: &Server{
                                                }(),
                                        },
                                        Server: &Server{
-                                               Accounts: map[string]*Account{},
+                                               //Accounts: map[string]*Account{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -1271,12 +1327,16 @@ func TestHandleGetUser(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessOpenUser)
+                                                       bits.Set(AccessOpenUser)
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
-                                               Accounts: map[string]*Account{},
+                                               AccountManager: func() *MockAccountManager {
+                                                       m := MockAccountManager{}
+                                                       m.On("Get", "nonExistentUser").Return((*Account)(nil))
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -1316,29 +1376,26 @@ func TestHandleDeleteUser(t *testing.T) {
                wantRes []Transaction
        }{
                {
                wantRes []Transaction
        }{
                {
-                       name: "when user dataFile",
+                       name: "when user exists",
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessDeleteUser)
+                                                       bits.Set(AccessDeleteUser)
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
-                                               Accounts: map[string]*Account{
-                                                       "testuser": {
-                                                               Login:    "testuser",
-                                                               Name:     "Testy McTest",
-                                                               Password: "password",
-                                                               Access:   accessBitmap{},
-                                                       },
-                                               },
-                                               FS: func() *MockFileStore {
-                                                       mfs := &MockFileStore{}
-                                                       mfs.On("Remove", "Users/testuser.yaml").Return(nil)
-                                                       return mfs
+                                               AccountManager: func() *MockAccountManager {
+                                                       m := MockAccountManager{}
+                                                       m.On("Delete", "testuser").Return(nil)
+                                                       return &m
+                                               }(),
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("List").Return([]*ClientConn{}) // TODO
+                                                       return &m
                                                }(),
                                        },
                                },
                                                }(),
                                        },
                                },
@@ -1361,13 +1418,10 @@ func TestHandleDeleteUser(t *testing.T) {
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
-                                               Access: func() accessBitmap {
-                                                       var bits accessBitmap
-                                                       return bits
-                                               }(),
+                                               Access: accessBitmap{},
                                        },
                                        Server: &Server{
                                        },
                                        Server: &Server{
-                                               Accounts: map[string]*Account{},
+                                               //Accounts: map[string]*Account{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -1411,12 +1465,20 @@ func TestHandleGetMsgs(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessNewsReadArt)
+                                                       bits.Set(AccessNewsReadArt)
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
-                                               FlatNews: []byte("TEST"),
+                                               MessageBoard: func() *mockReadWriteSeeker {
+                                                       m := mockReadWriteSeeker{}
+                                                       m.On("Seek", int64(0), 0).Return(int64(0), nil)
+                                                       m.On("Read", mock.AnythingOfType("[]uint8")).Run(func(args mock.Arguments) {
+                                                               arg := args.Get(0).([]uint8)
+                                                               copy(arg, "TEST")
+                                                       }).Return(4, io.EOF)
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -1437,13 +1499,10 @@ func TestHandleGetMsgs(t *testing.T) {
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
-                                               Access: func() accessBitmap {
-                                                       var bits accessBitmap
-                                                       return bits
-                                               }(),
+                                               Access: accessBitmap{},
                                        },
                                        Server: &Server{
                                        },
                                        Server: &Server{
-                                               Accounts: map[string]*Account{},
+                                               //Accounts: map[string]*Account{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -1490,7 +1549,7 @@ func TestHandleNewUser(t *testing.T) {
                                                }(),
                                        },
                                        Server: &Server{
                                                }(),
                                        },
                                        Server: &Server{
-                                               Accounts: map[string]*Account{},
+                                               //Accounts: map[string]*Account{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -1514,22 +1573,26 @@ func TestHandleNewUser(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessCreateUser)
+                                                       bits.Set(AccessCreateUser)
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
-                                               Accounts: map[string]*Account{},
+                                               AccountManager: func() *MockAccountManager {
+                                                       m := MockAccountManager{}
+                                                       m.On("Get", "userB").Return((*Account)(nil))
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: NewTransaction(
                                        TranNewUser, [2]byte{0, 1},
                                        },
                                },
                                t: NewTransaction(
                                        TranNewUser, [2]byte{0, 1},
-                                       NewField(FieldUserLogin, []byte("userB")),
+                                       NewField(FieldUserLogin, encodeString([]byte("userB"))),
                                        NewField(
                                                FieldUserAccess,
                                                func() []byte {
                                                        var bits accessBitmap
                                        NewField(
                                                FieldUserAccess,
                                                func() []byte {
                                                        var bits accessBitmap
-                                                       bits.Set(accessDisconUser)
+                                                       bits.Set(AccessDisconUser)
                                                        return bits[:]
                                                }(),
                                        ),
                                                        return bits[:]
                                                }(),
                                        ),
@@ -1575,7 +1638,7 @@ func TestHandleListUsers(t *testing.T) {
                                                }(),
                                        },
                                        Server: &Server{
                                                }(),
                                        },
                                        Server: &Server{
-                                               Accounts: map[string]*Account{},
+                                               //Accounts: map[string]*Account{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -1599,19 +1662,23 @@ func TestHandleListUsers(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessOpenUser)
+                                                       bits.Set(AccessOpenUser)
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
-                                               Accounts: map[string]*Account{
-                                                       "guest": {
-                                                               Name:     "guest",
-                                                               Login:    "guest",
-                                                               Password: "zz",
-                                                               Access:   accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
-                                                       },
-                                               },
+                                               AccountManager: func() *MockAccountManager {
+                                                       m := MockAccountManager{}
+                                                       m.On("List").Return([]Account{
+                                                               {
+                                                                       Name:     "guest",
+                                                                       Login:    "guest",
+                                                                       Password: "zz",
+                                                                       Access:   accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
+                                                               },
+                                                       })
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -1680,23 +1747,20 @@ func TestHandleDownloadFile(t *testing.T) {
                        name: "with a valid file",
                        args: args{
                                cc: &ClientConn{
                        name: "with a valid file",
                        args: args{
                                cc: &ClientConn{
-                                       transfers: map[int]map[[4]byte]*FileTransfer{
-                                               FileDownload: {},
-                                       },
+                                       ClientFileTransferMgr: NewClientFileTransferMgr(),
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessDownloadFile)
+                                                       bits.Set(AccessDownloadFile)
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
-                                               FS:            &OSFileStore{},
-                                               fileTransfers: map[[4]byte]*FileTransfer{},
-                                               Config: &Config{
+                                               FS:              &OSFileStore{},
+                                               FileTransferMgr: NewMemFileTransferMgr(),
+                                               Config: Config{
                                                        FileRoot: func() string { path, _ := os.Getwd(); return path + "/test/config/Files" }(),
                                                },
                                                        FileRoot: func() string { path, _ := os.Getwd(); return path + "/test/config/Files" }(),
                                                },
-                                               Accounts: map[string]*Account{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -1722,12 +1786,11 @@ func TestHandleDownloadFile(t *testing.T) {
                        name: "when client requests to resume 1k test file at offset 256",
                        args: args{
                                cc: &ClientConn{
                        name: "when client requests to resume 1k test file at offset 256",
                        args: args{
                                cc: &ClientConn{
-                                       transfers: map[int]map[[4]byte]*FileTransfer{
-                                               FileDownload: {},
-                                       }, Account: &Account{
+                                       ClientFileTransferMgr: NewClientFileTransferMgr(),
+                                       Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessDownloadFile)
+                                                       bits.Set(AccessDownloadFile)
                                                        return bits
                                                }(),
                                        },
                                                        return bits
                                                }(),
                                        },
@@ -1751,11 +1814,11 @@ func TestHandleDownloadFile(t *testing.T) {
                                                //
                                                //      return mfs
                                                // }(),
                                                //
                                                //      return mfs
                                                // }(),
-                                               fileTransfers: map[[4]byte]*FileTransfer{},
-                                               Config: &Config{
+                                               FileTransferMgr: NewMemFileTransferMgr(),
+                                               Config: Config{
                                                        FileRoot: func() string { path, _ := os.Getwd(); return path + "/test/config/Files" }(),
                                                },
                                                        FileRoot: func() string { path, _ := os.Getwd(); return path + "/test/config/Files" }(),
                                                },
-                                               Accounts: map[string]*Account{},
+                                               //Accounts: map[string]*Account{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -1822,13 +1885,15 @@ func TestHandleUpdateUser(t *testing.T) {
                                cc: &ClientConn{
                                        logger: NewTestLogger(),
                                        Server: &Server{
                                cc: &ClientConn{
                                        logger: NewTestLogger(),
                                        Server: &Server{
+                                               AccountManager: func() *MockAccountManager {
+                                                       m := MockAccountManager{}
+                                                       m.On("Get", "bbb").Return((*Account)(nil))
+                                                       return &m
+                                               }(),
                                                Logger: NewTestLogger(),
                                        },
                                        Account: &Account{
                                                Logger: NewTestLogger(),
                                        },
                                        Account: &Account{
-                                               Access: func() accessBitmap {
-                                                       var bits accessBitmap
-                                                       return bits
-                                               }(),
+                                               Access: accessBitmap{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -1872,9 +1937,11 @@ func TestHandleUpdateUser(t *testing.T) {
                                        logger: NewTestLogger(),
                                        Server: &Server{
                                                Logger: NewTestLogger(),
                                        logger: NewTestLogger(),
                                        Server: &Server{
                                                Logger: NewTestLogger(),
-                                               Accounts: map[string]*Account{
-                                                       "bbb": {},
-                                               },
+                                               AccountManager: func() *MockAccountManager {
+                                                       m := MockAccountManager{}
+                                                       m.On("Get", "bbb").Return(&Account{})
+                                                       return &m
+                                               }(),
                                        },
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                        },
                                        Account: &Account{
                                                Access: func() accessBitmap {
@@ -1922,16 +1989,9 @@ func TestHandleUpdateUser(t *testing.T) {
                        args: args{
                                cc: &ClientConn{
                                        logger: NewTestLogger(),
                        args: args{
                                cc: &ClientConn{
                                        logger: NewTestLogger(),
-                                       Server: &Server{
-                                               Accounts: map[string]*Account{
-                                                       "bbb": {},
-                                               },
-                                       },
+                                       Server: &Server{},
                                        Account: &Account{
                                        Account: &Account{
-                                               Access: func() accessBitmap {
-                                                       var bits accessBitmap
-                                                       return bits
-                                               }(),
+                                               Access: accessBitmap{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -2050,23 +2110,26 @@ func TestHandleDisconnectUser(t *testing.T) {
                        args: args{
                                cc: &ClientConn{
                                        Server: &Server{
                        args: args{
                                cc: &ClientConn{
                                        Server: &Server{
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 1}: {
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("Get", ClientID{0x0, 0x1}).Return(&ClientConn{
                                                                Account: &Account{
                                                                        Login: "unnamed",
                                                                        Access: func() accessBitmap {
                                                                                var bits accessBitmap
                                                                Account: &Account{
                                                                        Login: "unnamed",
                                                                        Access: func() accessBitmap {
                                                                                var bits accessBitmap
-                                                                               bits.Set(accessCannotBeDiscon)
+                                                                               bits.Set(AccessCannotBeDiscon)
                                                                                return bits
                                                                        }(),
                                                                },
                                                        },
                                                                                return bits
                                                                        }(),
                                                                },
                                                        },
-                                               },
+                                                       )
+                                                       return &m
+                                               }(),
                                        },
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        },
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessDisconUser)
+                                                       bits.Set(AccessDisconUser)
                                                        return bits
                                                }(),
                                        },
                                                        return bits
                                                }(),
                                        },
@@ -2139,19 +2202,22 @@ func TestHandleSendInstantMsg(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessSendPrivMsg)
+                                                       bits.Set(AccessSendPrivMsg)
                                                        return bits
                                                }(),
                                        },
                                        ID:       [2]byte{0, 1},
                                        UserName: []byte("User1"),
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        ID:       [2]byte{0, 1},
                                        UserName: []byte("User1"),
                                        Server: &Server{
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 2}: {
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("Get", ClientID{0x0, 0x2}).Return(&ClientConn{
                                                                AutoReply: []byte(nil),
                                                                Flags:     [2]byte{0, 0},
                                                        },
                                                                AutoReply: []byte(nil),
                                                                Flags:     [2]byte{0, 0},
                                                        },
-                                               },
+                                                       )
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -2184,21 +2250,23 @@ func TestHandleSendInstantMsg(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessSendPrivMsg)
+                                                       bits.Set(AccessSendPrivMsg)
                                                        return bits
                                                }(),
                                        },
                                        ID:       [2]byte{0, 1},
                                        UserName: []byte("User1"),
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        ID:       [2]byte{0, 1},
                                        UserName: []byte("User1"),
                                        Server: &Server{
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 2}: {
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("Get", ClientID{0x0, 0x2}).Return(&ClientConn{
                                                                Flags:     [2]byte{0, 0},
                                                                ID:        [2]byte{0, 2},
                                                                UserName:  []byte("User2"),
                                                                AutoReply: []byte("autohai"),
                                                                Flags:     [2]byte{0, 0},
                                                                ID:        [2]byte{0, 2},
                                                                UserName:  []byte("User2"),
                                                                AutoReply: []byte("autohai"),
-                                                       },
-                                               },
+                                                       })
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -2239,20 +2307,23 @@ func TestHandleSendInstantMsg(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessSendPrivMsg)
+                                                       bits.Set(AccessSendPrivMsg)
                                                        return bits
                                                }(),
                                        },
                                        ID:       [2]byte{0, 1},
                                        UserName: []byte("User1"),
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        ID:       [2]byte{0, 1},
                                        UserName: []byte("User1"),
                                        Server: &Server{
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 2}: {
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("Get", ClientID{0x0, 0x2}).Return(&ClientConn{
                                                                Flags:    [2]byte{255, 255},
                                                                ID:       [2]byte{0, 2},
                                                                UserName: []byte("User2"),
                                                        },
                                                                Flags:    [2]byte{255, 255},
                                                                ID:       [2]byte{0, 2},
                                                                UserName: []byte("User2"),
                                                        },
-                                               },
+                                                       )
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -2308,7 +2379,7 @@ func TestHandleDeleteFile(t *testing.T) {
                                                }(),
                                        },
                                        Server: &Server{
                                                }(),
                                        },
                                        Server: &Server{
-                                               Config: &Config{
+                                               Config: Config{
                                                        FileRoot: func() string {
                                                                return "/fakeRoot/Files"
                                                        }(),
                                                        FileRoot: func() string {
                                                                return "/fakeRoot/Files"
                                                        }(),
@@ -2328,7 +2399,7 @@ func TestHandleDeleteFile(t *testing.T) {
 
                                                        return mfs
                                                }(),
 
                                                        return mfs
                                                }(),
-                                               Accounts: map[string]*Account{},
+                                               //Accounts: map[string]*Account{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -2359,12 +2430,12 @@ func TestHandleDeleteFile(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessDeleteFile)
+                                                       bits.Set(AccessDeleteFile)
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
-                                               Config: &Config{
+                                               Config: Config{
                                                        FileRoot: func() string {
                                                                return "/fakeRoot/Files"
                                                        }(),
                                                        FileRoot: func() string {
                                                                return "/fakeRoot/Files"
                                                        }(),
@@ -2389,7 +2460,7 @@ func TestHandleDeleteFile(t *testing.T) {
 
                                                        return mfs
                                                }(),
 
                                                        return mfs
                                                }(),
-                                               Accounts: map[string]*Account{},
+                                               //Accounts: map[string]*Account{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -2432,7 +2503,7 @@ func TestHandleGetFileNameList(t *testing.T) {
                wantRes []Transaction
        }{
                {
                wantRes []Transaction
        }{
                {
-                       name: "when FieldFilePath is a drop box, but user does not have accessViewDropBoxes ",
+                       name: "when FieldFilePath is a drop box, but user does not have AccessViewDropBoxes ",
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
@@ -2443,7 +2514,7 @@ func TestHandleGetFileNameList(t *testing.T) {
                                        },
                                        Server: &Server{
 
                                        },
                                        Server: &Server{
 
-                                               Config: &Config{
+                                               Config: Config{
                                                        FileRoot: func() string {
                                                                path, _ := os.Getwd()
                                                                return filepath.Join(path, "/test/config/Files/getFileNameListTestDir")
                                                        FileRoot: func() string {
                                                                path, _ := os.Getwd()
                                                                return filepath.Join(path, "/test/config/Files/getFileNameListTestDir")
@@ -2476,7 +2547,7 @@ func TestHandleGetFileNameList(t *testing.T) {
                        args: args{
                                cc: &ClientConn{
                                        Server: &Server{
                        args: args{
                                cc: &ClientConn{
                                        Server: &Server{
-                                               Config: &Config{
+                                               Config: Config{
                                                        FileRoot: func() string {
                                                                path, _ := os.Getwd()
                                                                return filepath.Join(path, "/test/config/Files/getFileNameListTestDir")
                                                        FileRoot: func() string {
                                                                path, _ := os.Getwd()
                                                                return filepath.Join(path, "/test/config/Files/getFileNameListTestDir")
@@ -2548,7 +2619,7 @@ func TestHandleGetClientInfoText(t *testing.T) {
                                                }(),
                                        },
                                        Server: &Server{
                                                }(),
                                        },
                                        Server: &Server{
-                                               Accounts: map[string]*Account{},
+                                               //Accounts: map[string]*Account{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -2575,36 +2646,33 @@ func TestHandleGetClientInfoText(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessGetClientInfo)
+                                                       bits.Set(AccessGetClientInfo)
                                                        return bits
                                                }(),
                                                Name:  "test",
                                                Login: "test",
                                        },
                                        Server: &Server{
                                                        return bits
                                                }(),
                                                Name:  "test",
                                                Login: "test",
                                        },
                                        Server: &Server{
-                                               Accounts: map[string]*Account{},
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 1}: {
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("Get", ClientID{0x0, 0x1}).Return(&ClientConn{
                                                                UserName:   []byte("Testy McTest"),
                                                                RemoteAddr: "1.2.3.4:12345",
                                                                Account: &Account{
                                                                        Access: func() accessBitmap {
                                                                                var bits accessBitmap
                                                                UserName:   []byte("Testy McTest"),
                                                                RemoteAddr: "1.2.3.4:12345",
                                                                Account: &Account{
                                                                        Access: func() accessBitmap {
                                                                                var bits accessBitmap
-                                                                               bits.Set(accessGetClientInfo)
+                                                                               bits.Set(AccessGetClientInfo)
                                                                                return bits
                                                                        }(),
                                                                        Name:  "test",
                                                                        Login: "test",
                                                                },
                                                        },
                                                                                return bits
                                                                        }(),
                                                                        Name:  "test",
                                                                        Login: "test",
                                                                },
                                                        },
-                                               },
-                                       },
-                                       transfers: map[int]map[[4]byte]*FileTransfer{
-                                               FileDownload:   {},
-                                               FileUpload:     {},
-                                               FolderDownload: {},
-                                               FolderUpload:   {},
+                                                       )
+                                                       return &m
+                                               }(),
                                        },
                                        },
+                                       ClientFileTransferMgr: ClientFileTransferMgr{},
                                },
                                t: NewTransaction(
                                        TranGetClientInfoText, [2]byte{0, 1},
                                },
                                t: NewTransaction(
                                        TranGetClientInfoText, [2]byte{0, 1},
@@ -2674,8 +2742,8 @@ func TestHandleTranAgreed(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessDisconUser)
-                                                       bits.Set(accessAnyName)
+                                                       bits.Set(AccessDisconUser)
+                                                       bits.Set(AccessAnyName)
                                                        return bits
                                                }()},
                                        Icon:    []byte{0, 1},
                                                        return bits
                                                }()},
                                        Icon:    []byte{0, 1},
@@ -2684,9 +2752,20 @@ func TestHandleTranAgreed(t *testing.T) {
                                        ID:      [2]byte{0, 1},
                                        logger:  NewTestLogger(),
                                        Server: &Server{
                                        ID:      [2]byte{0, 1},
                                        logger:  NewTestLogger(),
                                        Server: &Server{
-                                               Config: &Config{
+                                               Config: Config{
                                                        BannerFile: "banner.jpg",
                                                },
                                                        BannerFile: "banner.jpg",
                                                },
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("List").Return([]*ClientConn{
+                                                               //{
+                                                               //      ID:       [2]byte{0, 2},
+                                                               //      UserName: []byte("UserB"),
+                                                               //},
+                                                       },
+                                                       )
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -2731,7 +2810,7 @@ func TestHandleSetClientUserInfo(t *testing.T) {
                wantRes []Transaction
        }{
                {
                wantRes []Transaction
        }{
                {
-                       name: "when client does not have accessAnyName",
+                       name: "when client does not have AccessAnyName",
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
@@ -2744,11 +2823,15 @@ func TestHandleSetClientUserInfo(t *testing.T) {
                                        UserName: []byte("Guest"),
                                        Flags:    [2]byte{0, 1},
                                        Server: &Server{
                                        UserName: []byte("Guest"),
                                        Flags:    [2]byte{0, 1},
                                        Server: &Server{
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 1}: {
-                                                               ID: [2]byte{0, 1},
-                                                       },
-                                               },
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("List").Return([]*ClientConn{
+                                                               {
+                                                                       ID: [2]byte{0, 1},
+                                                               },
+                                                       })
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -2797,12 +2880,13 @@ func TestHandleDelNewsItem(t *testing.T) {
                                        },
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
                                        },
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
-                                               ThreadedNews: &ThreadedNews{Categories: map[string]NewsCategoryListData15{
-                                                       "test": {
-                                                               Type: [2]byte{0, 3},
-                                                               Name: "zz",
-                                                       },
-                                               }},
+                                               ThreadedNewsMgr: func() *mockThreadNewsMgr {
+                                                       m := mockThreadNewsMgr{}
+                                                       m.On("NewsItem", []string{"test"}).Return(NewsCategoryListData15{
+                                                               Type: NewsCategory,
+                                                       })
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -2837,12 +2921,13 @@ func TestHandleDelNewsItem(t *testing.T) {
                                        },
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
                                        },
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
-                                               ThreadedNews: &ThreadedNews{Categories: map[string]NewsCategoryListData15{
-                                                       "testcat": {
-                                                               Type: [2]byte{0, 2},
-                                                               Name: "test",
-                                                       },
-                                               }},
+                                               ThreadedNewsMgr: func() *mockThreadNewsMgr {
+                                                       m := mockThreadNewsMgr{}
+                                                       m.On("NewsItem", []string{"test"}).Return(NewsCategoryListData15{
+                                                               Type: NewsBundle,
+                                                       })
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -2875,24 +2960,18 @@ func TestHandleDelNewsItem(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessNewsDeleteFldr)
+                                                       bits.Set(AccessNewsDeleteFldr)
                                                        return bits
                                                }(),
                                        },
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        ID: [2]byte{0, 1},
                                        Server: &Server{
-                                               ConfigDir: "/fakeConfigRoot",
-                                               FS: func() *MockFileStore {
-                                                       mfs := &MockFileStore{}
-                                                       mfs.On("WriteFile", "/fakeConfigRoot/ThreadedNews.yaml", mock.Anything, mock.Anything).Return(nil, os.ErrNotExist)
-                                                       return mfs
+                                               ThreadedNewsMgr: func() *mockThreadNewsMgr {
+                                                       m := mockThreadNewsMgr{}
+                                                       m.On("NewsItem", []string{"test"}).Return(NewsCategoryListData15{Type: NewsBundle})
+                                                       m.On("DeleteNewsItem", []string{"test"}).Return(nil)
+                                                       return &m
                                                }(),
                                                }(),
-                                               ThreadedNews: &ThreadedNews{Categories: map[string]NewsCategoryListData15{
-                                                       "testcat": {
-                                                               Type: [2]byte{0, 2},
-                                                               Name: "test",
-                                                       },
-                                               }},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -2940,10 +3019,7 @@ func TestHandleTranOldPostNews(t *testing.T) {
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
-                                               Access: func() accessBitmap {
-                                                       var bits accessBitmap
-                                                       return bits
-                                               }(),
+                                               Access: accessBitmap{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -2968,18 +3044,29 @@ func TestHandleTranOldPostNews(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessNewsPostArt)
+                                                       bits.Set(AccessNewsPostArt)
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
-                                               FS: func() *MockFileStore {
-                                                       mfs := &MockFileStore{}
-                                                       mfs.On("WriteFile", "/fakeConfigRoot/MessageBoard.txt", mock.Anything, mock.Anything).Return(nil, os.ErrNotExist)
-                                                       return mfs
+                                               Config: Config{
+                                                       NewsDateFormat: "",
+                                               },
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("List").Return([]*ClientConn{})
+                                                       return &m
+                                               }(),
+                                               MessageBoard: func() *mockReadWriteSeeker {
+                                                       m := mockReadWriteSeeker{}
+                                                       m.On("Seek", int64(0), 0).Return(int64(0), nil)
+                                                       m.On("Read", mock.AnythingOfType("[]uint8")).Run(func(args mock.Arguments) {
+                                                               arg := args.Get(0).([]uint8)
+                                                               copy(arg, "TEST")
+                                                       }).Return(4, io.EOF)
+                                                       m.On("Write", mock.AnythingOfType("[]uint8")).Return(3, nil)
+                                                       return &m
                                                }(),
                                                }(),
-                                               ConfigDir: "/fakeConfigRoot",
-                                               Config:    &Config{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -3044,7 +3131,7 @@ func TestHandleInviteNewChat(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessOpenChat)
+                                                       bits.Set(AccessOpenChat)
                                                        return bits
                                                }(),
                                        },
                                                        return bits
                                                }(),
                                        },
@@ -3052,13 +3139,19 @@ func TestHandleInviteNewChat(t *testing.T) {
                                        Icon:     []byte{0, 1},
                                        Flags:    [2]byte{0, 0},
                                        Server: &Server{
                                        Icon:     []byte{0, 1},
                                        Flags:    [2]byte{0, 0},
                                        Server: &Server{
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 2}: {
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("Get", ClientID{0x0, 0x2}).Return(&ClientConn{
                                                                ID:       [2]byte{0, 2},
                                                                UserName: []byte("UserB"),
                                                                ID:       [2]byte{0, 2},
                                                                UserName: []byte("UserB"),
-                                                       },
-                                               },
-                                               PrivateChats: make(map[[4]byte]*PrivateChat),
+                                                       })
+                                                       return &m
+                                               }(),
+                                               ChatMgr: func() *MockChatManager {
+                                                       m := MockChatManager{}
+                                                       m.On("New", mock.AnythingOfType("*hotline.ClientConn")).Return(ChatID{0x52, 0xfd, 0xfc, 0x07})
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -3076,7 +3169,6 @@ func TestHandleInviteNewChat(t *testing.T) {
                                                NewField(FieldUserID, []byte{0, 1}),
                                        },
                                },
                                                NewField(FieldUserID, []byte{0, 1}),
                                        },
                                },
-
                                {
                                        clientID: [2]byte{0, 1},
                                        IsReply:  0x01,
                                {
                                        clientID: [2]byte{0, 1},
                                        IsReply:  0x01,
@@ -3098,7 +3190,7 @@ func TestHandleInviteNewChat(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessOpenChat)
+                                                       bits.Set(AccessOpenChat)
                                                        return bits
                                                }(),
                                        },
                                                        return bits
                                                }(),
                                        },
@@ -3106,14 +3198,21 @@ func TestHandleInviteNewChat(t *testing.T) {
                                        Icon:     []byte{0, 1},
                                        Flags:    [2]byte{0, 0},
                                        Server: &Server{
                                        Icon:     []byte{0, 1},
                                        Flags:    [2]byte{0, 0},
                                        Server: &Server{
-                                               Clients: map[[2]byte]*ClientConn{
-                                                       [2]byte{0, 2}: {
+                                               ClientMgr: func() *MockClientMgr {
+                                                       m := MockClientMgr{}
+                                                       m.On("Get", ClientID{0, 2}).Return(&ClientConn{
                                                                ID:       [2]byte{0, 2},
                                                                ID:       [2]byte{0, 2},
+                                                               Icon:     []byte{0, 1},
                                                                UserName: []byte("UserB"),
                                                                Flags:    [2]byte{255, 255},
                                                                UserName: []byte("UserB"),
                                                                Flags:    [2]byte{255, 255},
-                                                       },
-                                               },
-                                               PrivateChats: make(map[[4]byte]*PrivateChat),
+                                                       })
+                                                       return &m
+                                               }(),
+                                               ChatMgr: func() *MockChatManager {
+                                                       m := MockChatManager{}
+                                                       m.On("New", mock.AnythingOfType("*hotline.ClientConn")).Return(ChatID{0x52, 0xfd, 0xfc, 0x07})
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -3148,6 +3247,7 @@ func TestHandleInviteNewChat(t *testing.T) {
        }
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
        }
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
+
                        gotRes := HandleInviteNewChat(tt.args.cc, &tt.args.t)
 
                        tranAssertEqual(t, tt.wantRes, gotRes)
                        gotRes := HandleInviteNewChat(tt.args.cc, &tt.args.t)
 
                        tranAssertEqual(t, tt.wantRes, gotRes)
@@ -3167,28 +3267,73 @@ func TestHandleGetNewsArtData(t *testing.T) {
        }{
                {
                        name: "when user does not have required permission",
        }{
                {
                        name: "when user does not have required permission",
+                       args: args{
+                               cc: &ClientConn{Account: &Account{}},
+                               t: NewTransaction(
+                                       TranGetNewsArtData, [2]byte{0, 1},
+                               ),
+                       },
+                       wantRes: []Transaction{
+                               {
+                                       IsReply:   0x01,
+                                       ErrorCode: [4]byte{0, 0, 0, 1},
+                                       Fields: []Field{
+                                               NewField(FieldError, []byte("You are not allowed to read news.")),
+                                       },
+                               },
+                       },
+               },
+               {
+                       name: "when user has required permission",
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                        args: args{
                                cc: &ClientConn{
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
+                                                       bits.Set(AccessNewsReadArt)
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        Server: &Server{
-                                               Accounts: map[string]*Account{},
+                                               ThreadedNewsMgr: func() *mockThreadNewsMgr {
+                                                       m := mockThreadNewsMgr{}
+                                                       m.On("GetArticle", []string{"Example Category"}, uint32(1)).Return(&NewsArtData{
+                                                               Title:         "title",
+                                                               Poster:        "poster",
+                                                               Date:          [8]byte{},
+                                                               PrevArt:       [4]byte{0, 0, 0, 1},
+                                                               NextArt:       [4]byte{0, 0, 0, 2},
+                                                               ParentArt:     [4]byte{0, 0, 0, 3},
+                                                               FirstChildArt: [4]byte{0, 0, 0, 4},
+                                                               DataFlav:      []byte("text/plain"),
+                                                               Data:          "article data",
+                                                       })
+                                                       return &m
+                                               }(),
                                        },
                                },
                                t: NewTransaction(
                                        TranGetNewsArtData, [2]byte{0, 1},
                                        },
                                },
                                t: NewTransaction(
                                        TranGetNewsArtData, [2]byte{0, 1},
+                                       NewField(FieldNewsPath, []byte{
+                                               // Example Category
+                                               0x00, 0x01, 0x00, 0x00, 0x10, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x20, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79,
+                                       }),
+                                       NewField(FieldNewsArtID, []byte{0, 1}),
                                ),
                        },
                        wantRes: []Transaction{
                                {
                                ),
                        },
                        wantRes: []Transaction{
                                {
-                                       IsReply:   0x01,
-                                       ErrorCode: [4]byte{0, 0, 0, 1},
+                                       IsReply: 1,
                                        Fields: []Field{
                                        Fields: []Field{
-                                               NewField(FieldError, []byte("You are not allowed to read news.")),
+                                               NewField(FieldNewsArtTitle, []byte("title")),
+                                               NewField(FieldNewsArtPoster, []byte("poster")),
+                                               NewField(FieldNewsArtDate, []byte{0, 0, 0, 0, 0, 0, 0, 0}),
+                                               NewField(FieldNewsArtPrevArt, []byte{0, 0, 0, 1}),
+                                               NewField(FieldNewsArtNextArt, []byte{0, 0, 0, 2}),
+                                               NewField(FieldNewsArtParentArt, []byte{0, 0, 0, 3}),
+                                               NewField(FieldNewsArt1stChildArt, []byte{0, 0, 0, 4}),
+                                               NewField(FieldNewsArtDataFlav, []byte("text/plain")),
+                                               NewField(FieldNewsArtData, []byte("article data")),
                                        },
                                },
                        },
                                        },
                                },
                        },
@@ -3223,7 +3368,7 @@ func TestHandleGetNewsArtNameList(t *testing.T) {
                                                }(),
                                        },
                                        Server: &Server{
                                                }(),
                                        },
                                        Server: &Server{
-                                               Accounts: map[string]*Account{},
+                                               //Accounts: map[string]*Account{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -3242,74 +3387,54 @@ func TestHandleGetNewsArtNameList(t *testing.T) {
                                },
                        },
                },
                                },
                        },
                },
-               {
-                       name: "when user has required access",
-                       args: args{
-                               cc: &ClientConn{
-                                       Account: &Account{
-                                               Access: func() accessBitmap {
-                                                       var bits accessBitmap
-                                                       bits.Set(accessNewsReadArt)
-                                                       return bits
-                                               }(),
-                                       },
-                                       Server: &Server{
-                                               ThreadedNews: &ThreadedNews{
-                                                       Categories: map[string]NewsCategoryListData15{
-                                                               "Example Category": {
-                                                                       Type: [2]byte{0, 2},
-                                                                       Name: "",
-                                                                       Articles: map[uint32]*NewsArtData{
-                                                                               uint32(1): {
-                                                                                       Title:  "testTitle",
-                                                                                       Poster: "testPoster",
-                                                                                       Data:   "testBody",
-                                                                               },
-                                                                       },
-                                                                       SubCats:  nil,
-                                                                       GUID:     [16]byte{},
-                                                                       AddSN:    [4]byte{},
-                                                                       DeleteSN: [4]byte{},
-                                                               },
-                                                       },
-                                               },
-
-                                               //Accounts: map[string]*Account{
-                                               //      "guest": {
-                                               //              Name:     "guest",
-                                               //              Login:    "guest",
-                                               //              Password: "zz",
-                                               //              Access:   accessBitmap{255, 255, 255, 255, 255, 255, 255, 255},
-                                               //      },
-                                               //},
-                                       },
-                               },
-                               t: NewTransaction(
-                                       TranGetNewsArtNameList,
-                                       [2]byte{0, 1},
-                                       //  00000000  00 01 00 00 10 45 78 61  6d 70 6c 65 20 43 61 74  |.....Example Cat|
-                                       //  00000010  65 67 6f 72 79                                    |egory|
-                                       NewField(FieldNewsPath, []byte{
-                                               0x00, 0x01, 0x00, 0x00, 0x10, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x20, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79,
-                                       }),
-                               ),
-                       },
-                       wantRes: []Transaction{
-                               {
-                                       IsReply: 0x01,
-                                       Fields: []Field{
-                                               NewField(FieldNewsArtListData, []byte{
-                                                       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
-                                                       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
-                                                       0x09, 0x74, 0x65, 0x73, 0x74, 0x54, 0x69, 0x74, 0x6c, 0x65, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x50,
-                                                       0x6f, 0x73, 0x74, 0x65, 0x72, 0x0a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e,
-                                                       0x00, 0x08,
-                                               },
-                                               ),
-                                       },
-                               },
-                       },
-               },
+               //{
+               //      name: "when user has required access",
+               //      args: args{
+               //              cc: &ClientConn{
+               //                      Account: &Account{
+               //                              Access: func() accessBitmap {
+               //                                      var bits accessBitmap
+               //                                      bits.Set(AccessNewsReadArt)
+               //                                      return bits
+               //                              }(),
+               //                      },
+               //                      Server: &Server{
+               //                              ThreadedNewsMgr: func() *mockThreadNewsMgr {
+               //                                      m := mockThreadNewsMgr{}
+               //                                      m.On("ListArticles", []string{"Example Category"}).Return(NewsArtListData{
+               //                                              Name:        []byte("testTitle"),
+               //                                              NewsArtList: []byte{},
+               //                                      })
+               //                                      return &m
+               //                              }(),
+               //                      },
+               //              },
+               //              t: NewTransaction(
+               //                      TranGetNewsArtNameList,
+               //                      [2]byte{0, 1},
+               //                      //  00000000  00 01 00 00 10 45 78 61  6d 70 6c 65 20 43 61 74  |.....Example Cat|
+               //                      //  00000010  65 67 6f 72 79                                    |egory|
+               //                      NewField(FieldNewsPath, []byte{
+               //                              0x00, 0x01, 0x00, 0x00, 0x10, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x20, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79,
+               //                      }),
+               //              ),
+               //      },
+               //      wantRes: []Transaction{
+               //              {
+               //                      IsReply: 0x01,
+               //                      Fields: []Field{
+               //                              NewField(FieldNewsArtListData, []byte{
+               //                                      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
+               //                                      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+               //                                      0x09, 0x74, 0x65, 0x73, 0x74, 0x54, 0x69, 0x74, 0x6c, 0x65, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x50,
+               //                                      0x6f, 0x73, 0x74, 0x65, 0x72, 0x0a, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e,
+               //                                      0x00, 0x08,
+               //                              },
+               //                              ),
+               //                      },
+               //              },
+               //      },
+               //},
        }
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
        }
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
@@ -3341,7 +3466,7 @@ func TestHandleNewNewsFldr(t *testing.T) {
                                                }(),
                                        },
                                        Server: &Server{
                                                }(),
                                        },
                                        Server: &Server{
-                                               Accounts: map[string]*Account{},
+                                               //Accounts: map[string]*Account{},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -3367,26 +3492,18 @@ func TestHandleNewNewsFldr(t *testing.T) {
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessNewsCreateFldr)
+                                                       bits.Set(AccessNewsCreateFldr)
                                                        return bits
                                                }(),
                                        },
                                        logger: NewTestLogger(),
                                        ID:     [2]byte{0, 1},
                                        Server: &Server{
                                                        return bits
                                                }(),
                                        },
                                        logger: NewTestLogger(),
                                        ID:     [2]byte{0, 1},
                                        Server: &Server{
-                                               ConfigDir: "/fakeConfigRoot",
-                                               FS: func() *MockFileStore {
-                                                       mfs := &MockFileStore{}
-                                                       mfs.On("WriteFile", "/fakeConfigRoot/ThreadedNews.yaml", mock.Anything, mock.Anything).Return(nil)
-                                                       return mfs
+                                               ThreadedNewsMgr: func() *mockThreadNewsMgr {
+                                                       m := mockThreadNewsMgr{}
+                                                       m.On("CreateGrouping", []string{"test"}, "testFolder", NewsBundle).Return(nil)
+                                                       return &m
                                                }(),
                                                }(),
-                                               ThreadedNews: &ThreadedNews{Categories: map[string]NewsCategoryListData15{
-                                                       "test": {
-                                                               Type:    [2]byte{0, 2},
-                                                               Name:    "test",
-                                                               SubCats: make(map[string]NewsCategoryListData15),
-                                                       },
-                                               }},
                                        },
                                },
                                t: NewTransaction(
                                        },
                                },
                                t: NewTransaction(
@@ -3417,12 +3534,12 @@ func TestHandleNewNewsFldr(t *testing.T) {
                //                      Account: &Account{
                //                              Access: func() accessBitmap {
                //                                      var bits accessBitmap
                //                      Account: &Account{
                //                              Access: func() accessBitmap {
                //                                      var bits accessBitmap
-               //                                      bits.Set(accessNewsCreateFldr)
+               //                                      bits.Set(AccessNewsCreateFldr)
                //                                      return bits
                //                              }(),
                //                      },
                //                      logger: NewTestLogger(),
                //                                      return bits
                //                              }(),
                //                      },
                //                      logger: NewTestLogger(),
-               //                      ID:     [2]byte{0, 1},
+               //                      Type:     [2]byte{0, 1},
                //                      Server: &Server{
                //                              ConfigDir: "/fakeConfigRoot",
                //                              FS: func() *MockFileStore {
                //                      Server: &Server{
                //                              ConfigDir: "/fakeConfigRoot",
                //                              FS: func() *MockFileStore {
@@ -3538,32 +3655,16 @@ func TestHandlePostNewsArt(t *testing.T) {
                        args: args{
                                cc: &ClientConn{
                                        Server: &Server{
                        args: args{
                                cc: &ClientConn{
                                        Server: &Server{
-                                               FS: func() *MockFileStore {
-                                                       mfs := &MockFileStore{}
-                                                       mfs.On("WriteFile", "ThreadedNews.yaml", mock.Anything, mock.Anything).Return(nil, os.ErrNotExist)
-                                                       return mfs
+                                               ThreadedNewsMgr: func() *mockThreadNewsMgr {
+                                                       m := mockThreadNewsMgr{}
+                                                       m.On("PostArticle", []string{"www"}, uint32(0), mock.AnythingOfType("hotline.NewsArtData")).Return(nil)
+                                                       return &m
                                                }(),
                                                }(),
-                                               mux:             sync.Mutex{},
-                                               threadedNewsMux: sync.Mutex{},
-                                               ThreadedNews: &ThreadedNews{
-                                                       Categories: map[string]NewsCategoryListData15{
-                                                               "www": {
-                                                                       Type:       [2]byte{},
-                                                                       Name:       "www",
-                                                                       Articles:   map[uint32]*NewsArtData{},
-                                                                       SubCats:    nil,
-                                                                       GUID:       [16]byte{},
-                                                                       AddSN:      [4]byte{},
-                                                                       DeleteSN:   [4]byte{},
-                                                                       readOffset: 0,
-                                                               },
-                                                       },
-                                               },
                                        },
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
                                        },
                                        Account: &Account{
                                                Access: func() accessBitmap {
                                                        var bits accessBitmap
-                                                       bits.Set(accessNewsPostArt)
+                                                       bits.Set(AccessNewsPostArt)
                                                        return bits
                                                }(),
                                        },
                                                        return bits
                                                }(),
                                        },
index 820b08c7f1fb6bf09cbd447bc49217a4582afb6a..e7b535d0034d0d157184728d229189952f4464e3 100644 (file)
@@ -6,111 +6,6 @@ import (
        "testing"
 )
 
        "testing"
 )
 
-func TestReadFields(t *testing.T) {
-       type args struct {
-               paramCount []byte
-               buf        []byte
-       }
-       tests := []struct {
-               name    string
-               args    args
-               want    []Field
-               wantErr bool
-       }{
-               {
-                       name: "valid field data",
-                       args: args{
-                               paramCount: []byte{0x00, 0x02},
-                               buf: []byte{
-                                       0x00, 0x65, // ID: FieldData
-                                       0x00, 0x04, // Size: 2 bytes
-                                       0x01, 0x02, 0x03, 0x04, // Data
-                                       0x00, 0x66, // ID: FieldUserName
-                                       0x00, 0x02, // Size: 2 bytes
-                                       0x00, 0x01, // Data
-                               },
-                       },
-                       want: []Field{
-                               {
-                                       ID:        [2]byte{0x00, 0x65},
-                                       FieldSize: [2]byte{0x00, 0x04},
-                                       Data:      []byte{0x01, 0x02, 0x03, 0x04},
-                               },
-                               {
-                                       ID:        [2]byte{0x00, 0x66},
-                                       FieldSize: [2]byte{0x00, 0x02},
-                                       Data:      []byte{0x00, 0x01},
-                               },
-                       },
-                       wantErr: false,
-               },
-               {
-                       name: "empty bytes",
-                       args: args{
-                               paramCount: []byte{0x00, 0x00},
-                               buf:        []byte{},
-                       },
-                       want:    []Field(nil),
-                       wantErr: false,
-               },
-               {
-                       name: "when field size does not match data length",
-                       args: args{
-                               paramCount: []byte{0x00, 0x01},
-                               buf: []byte{
-                                       0x00, 0x65, // ID: FieldData
-                                       0x00, 0x04, // Size: 4 bytes
-                                       0x01, 0x02, 0x03, // Data
-                               },
-                       },
-                       want:    []Field{},
-                       wantErr: true,
-               },
-               {
-                       name: "when field size of second field does not match data length",
-                       args: args{
-                               paramCount: []byte{0x00, 0x01},
-                               buf: []byte{
-                                       0x00, 0x65, // ID: FieldData
-                                       0x00, 0x02, // Size: 2 bytes
-                                       0x01, 0x02, // Data
-                                       0x00, 0x65, // ID: FieldData
-                                       0x00, 0x04, // Size: 4 bytes
-                                       0x01, 0x02, 0x03, // Data
-                               },
-                       },
-                       want:    []Field{},
-                       wantErr: true,
-               },
-               {
-                       name: "when field data has extra bytes",
-                       args: args{
-                               paramCount: []byte{0x00, 0x01},
-                               buf: []byte{
-                                       0x00, 0x65, // ID: FieldData
-                                       0x00, 0x02, // Size: 2 bytes
-                                       0x01, 0x02, 0x03, // Data
-                               },
-                       },
-                       want:    []Field{},
-                       wantErr: true,
-               },
-       }
-       for _, tt := range tests {
-               t.Run(tt.name, func(t *testing.T) {
-                       got, err := ReadFields(tt.args.paramCount, tt.args.buf)
-                       if (err != nil) != tt.wantErr {
-                               t.Errorf("ReadFields() error = %v, wantErr %v", err, tt.wantErr)
-                               return
-                       }
-
-                       if !assert.Equal(t, tt.want, got) {
-                               t.Errorf("ReadFields() got = %v, want %v", got, tt.want)
-                       }
-               })
-       }
-}
-
 func Test_transactionScanner(t *testing.T) {
        type args struct {
                data []byte
 func Test_transactionScanner(t *testing.T) {
        type args struct {
                data []byte
@@ -462,7 +357,7 @@ func TestTransaction_Write(t1 *testing.T) {
                                ParamCount: [2]byte{0, 1},
                                Fields: []Field{
                                        {
                                ParamCount: [2]byte{0, 1},
                                Fields: []Field{
                                        {
-                                               ID:        FieldData,
+                                               Type:      FieldData,
                                                FieldSize: [2]byte{0, 3},
                                                Data:      []byte("hai"),
                                        },
                                                FieldSize: [2]byte{0, 3},
                                                Data:      []byte("hai"),
                                        },
index 1257f8a33825285d2501373e4b44f870c21fa416..5cb28c8e60fba1927470860ba22597485c28fe83 100644 (file)
@@ -9,7 +9,7 @@ import (
 
 type transfer struct {
        Protocol        [4]byte // "HTXF" 0x48545846
 
 type transfer struct {
        Protocol        [4]byte // "HTXF" 0x48545846
-       ReferenceNumber [4]byte // Unique ID generated for the transfer
+       ReferenceNumber [4]byte // Unique Type generated for the transfer
        DataSize        [4]byte // File size
        RSVD            [4]byte // Not implemented in Hotline Protocol
 }
        DataSize        [4]byte // File size
        RSVD            [4]byte // Not implemented in Hotline Protocol
 }
index 26625a2f59849060122ea3c216ab40a9f1444226..f3754d36328141114d5ef6dd8139b86ee91134ff 100644 (file)
@@ -15,7 +15,7 @@ const (
        UserFlagRefusePChat = 3 // User refuses private chat
 )
 
        UserFlagRefusePChat = 3 // User refuses private chat
 )
 
-// FieldOptions flags are sent from v1.5+ clients as part of TranAgreed
+// User options are sent from clients and represent options set in the client's preferences.
 const (
        UserOptRefusePM     = 0 // User has "Refuse private messages" pref set
        UserOptRefuseChat   = 1 // User has "Refuse private chat" pref set
 const (
        UserOptRefusePM     = 0 // User has "Refuse private messages" pref set
        UserOptRefuseChat   = 1 // User has "Refuse private chat" pref set
@@ -24,15 +24,15 @@ const (
 
 type UserFlags [2]byte
 
 
 type UserFlags [2]byte
 
-func (flag *UserFlags) IsSet(i int) bool {
-       flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(flag[:])))
+func (f *UserFlags) IsSet(i int) bool {
+       flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(f[:])))
        return flagBitmap.Bit(i) == 1
 }
 
        return flagBitmap.Bit(i) == 1
 }
 
-func (flag *UserFlags) Set(i int, newVal uint) {
-       flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(flag[:])))
+func (f *UserFlags) Set(i int, newVal uint) {
+       flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(f[:])))
        flagBitmap.SetBit(flagBitmap, i, newVal)
        flagBitmap.SetBit(flagBitmap, i, newVal)
-       binary.BigEndian.PutUint16(flag[:], uint16(flagBitmap.Int64()))
+       binary.BigEndian.PutUint16(f[:], uint16(flagBitmap.Int64()))
 }
 
 type User struct {
 }
 
 type User struct {
diff --git a/hotline/util.go b/hotline/util.go
deleted file mode 100644 (file)
index d37feb5..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-package hotline
-
-import (
-       "encoding/binary"
-       "errors"
-)
-
-func byteToInt(bytes []byte) (int, error) {
-       switch len(bytes) {
-       case 2:
-               return int(binary.BigEndian.Uint16(bytes)), nil
-       case 4:
-               return int(binary.BigEndian.Uint32(bytes)), nil
-       }
-
-       return 0, errors.New("unknown byte length")
-}
diff --git a/hotline/util_test.go b/hotline/util_test.go
deleted file mode 100644 (file)
index 30ae2b6..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-package hotline
-
-import (
-       "fmt"
-       "github.com/stretchr/testify/assert"
-       "testing"
-)
-
-func Test_byteToInt(t *testing.T) {
-       type args struct {
-               bytes []byte
-       }
-       tests := []struct {
-               name    string
-               args    args
-               want    int
-               wantErr assert.ErrorAssertionFunc
-       }{
-               {
-                       name:    "with 2 bytes of input",
-                       args:    args{bytes: []byte{0, 1}},
-                       want:    1,
-                       wantErr: assert.NoError,
-               },
-               {
-                       name:    "with 4 bytes of input",
-                       args:    args{bytes: []byte{0, 1, 0, 0}},
-                       want:    65536,
-                       wantErr: assert.NoError,
-               },
-               {
-                       name:    "with invalid number of bytes of input",
-                       args:    args{bytes: []byte{1, 0, 0, 0, 0, 0, 0, 0}},
-                       want:    0,
-                       wantErr: assert.Error,
-               },
-       }
-       for _, tt := range tests {
-               t.Run(tt.name, func(t *testing.T) {
-                       got, err := byteToInt(tt.args.bytes)
-                       if !tt.wantErr(t, err, fmt.Sprintf("byteToInt(%v)", tt.args.bytes)) {
-                               return
-                       }
-                       assert.Equalf(t, tt.want, got, "byteToInt(%v)", tt.args.bytes)
-               })
-       }
-}
diff --git a/internal/mobius/ban.go b/internal/mobius/ban.go
new file mode 100644 (file)
index 0000000..f78e3f3
--- /dev/null
@@ -0,0 +1,76 @@
+package mobius
+
+import (
+       "gopkg.in/yaml.v3"
+       "os"
+       "path/filepath"
+       "sync"
+       "time"
+)
+
+type BanFile struct {
+       banList  map[string]*time.Time
+       filePath string
+
+       sync.Mutex
+}
+
+func NewBanFile(path string) (*BanFile, error) {
+       bf := &BanFile{
+               filePath: path,
+               banList:  make(map[string]*time.Time),
+       }
+
+       err := bf.Load()
+
+       return bf, err
+}
+
+func (bf *BanFile) Load() error {
+       bf.Lock()
+       defer bf.Unlock()
+
+       bf.banList = make(map[string]*time.Time)
+
+       fh, err := os.Open(bf.filePath)
+       if err != nil {
+               if os.IsNotExist(err) {
+                       return nil
+               }
+               return err
+       }
+       defer fh.Close()
+
+       decoder := yaml.NewDecoder(fh)
+       err = decoder.Decode(&bf.banList)
+       if err != nil {
+               return err
+       }
+
+       return nil
+}
+
+func (bf *BanFile) Add(ip string, until *time.Time) error {
+       bf.Lock()
+       defer bf.Unlock()
+
+       bf.banList[ip] = until
+
+       out, err := yaml.Marshal(bf.banList)
+       if err != nil {
+               return err
+       }
+
+       return os.WriteFile(filepath.Join(bf.filePath), out, 0644)
+}
+
+func (bf *BanFile) IsBanned(ip string) (bool, *time.Time) {
+       bf.Lock()
+       defer bf.Unlock()
+
+       if until, ok := bf.banList[ip]; ok {
+               return true, until
+       }
+
+       return false, nil
+}
diff --git a/internal/mobius/config.go b/internal/mobius/config.go
new file mode 100644 (file)
index 0000000..e985dd7
--- /dev/null
@@ -0,0 +1,30 @@
+package mobius
+
+import (
+       "github.com/go-playground/validator/v10"
+       "github.com/jhalter/mobius/hotline"
+       "gopkg.in/yaml.v3"
+       "log"
+       "os"
+)
+
+func LoadConfig(path string) (*hotline.Config, error) {
+       var config hotline.Config
+
+       yamlFile, err := os.ReadFile(path)
+       if err != nil {
+               return nil, err
+       }
+       err = yaml.Unmarshal(yamlFile, &config)
+       if err != nil {
+               log.Fatalf("Unmarshal: %v", err)
+       }
+
+       validate := validator.New()
+       err = validate.Struct(config)
+       if err != nil {
+               return nil, err
+       }
+
+       return &config, nil
+}
diff --git a/internal/mobius/news.go b/internal/mobius/news.go
new file mode 100644 (file)
index 0000000..51e212d
--- /dev/null
@@ -0,0 +1,87 @@
+package mobius
+
+import (
+       "fmt"
+       "io"
+       "os"
+       "slices"
+       "sync"
+)
+
+type FlatNews struct {
+       mu sync.Mutex
+
+       data     []byte
+       filePath string
+
+       readOffset int // Internal offset to track read progress
+}
+
+func NewFlatNews(path string) (*FlatNews, error) {
+       data, err := os.ReadFile(path)
+       if err != nil {
+               return &FlatNews{}, err
+       }
+
+       return &FlatNews{
+               data:     data,
+               filePath: path,
+       }, nil
+}
+
+func (f *FlatNews) Reload() error {
+       f.mu.Lock()
+       defer f.mu.Unlock()
+
+       data, err := os.ReadFile(f.filePath)
+       if err != nil {
+               return err
+       }
+       f.data = data
+
+       return nil
+}
+
+// It returns the number of bytes read and any error encountered.
+func (f *FlatNews) Read(p []byte) (int, error) {
+       f.mu.Lock()
+       defer f.mu.Unlock()
+
+       if f.readOffset >= len(f.data) {
+               return 0, io.EOF // All bytes have been read
+       }
+
+       n := copy(p, f.data[f.readOffset:])
+
+       f.readOffset += n
+
+       return n, nil
+}
+
+// Write implements io.Writer for flat news.
+// p is guaranteed to contain the full data of a news post.
+func (f *FlatNews) Write(p []byte) (int, error) {
+       f.mu.Lock()
+       defer f.mu.Unlock()
+
+       f.data = slices.Concat(p, f.data)
+
+       tempFilePath := f.filePath + ".tmp"
+
+       if err := os.WriteFile(tempFilePath, f.data, 0644); err != nil {
+               return 0, fmt.Errorf("write to temporary file: %v", err)
+       }
+
+       // Atomically rename the temporary file to the final file path.
+       if err := os.Rename(tempFilePath, f.filePath); err != nil {
+               return 0, fmt.Errorf("rename temporary file to final file: %v", err)
+       }
+
+       return len(p), os.WriteFile(f.filePath, f.data, 0644)
+}
+
+func (f *FlatNews) Seek(offset int64, _ int) (int64, error) {
+       f.readOffset = int(offset)
+
+       return 0, nil
+}
diff --git a/internal/mobius/threaded_news.go b/internal/mobius/threaded_news.go
new file mode 100644 (file)
index 0000000..bae6779
--- /dev/null
@@ -0,0 +1,251 @@
+package mobius
+
+import (
+       "cmp"
+       "encoding/binary"
+       "fmt"
+       "github.com/jhalter/mobius/hotline"
+       "gopkg.in/yaml.v3"
+       "os"
+       "slices"
+       "sort"
+       "sync"
+)
+
+type ThreadedNewsYAML struct {
+       ThreadedNews hotline.ThreadedNews
+
+       filePath string
+
+       mu sync.Mutex
+}
+
+func NewThreadedNewsYAML(filePath string) (*ThreadedNewsYAML, error) {
+       tn := &ThreadedNewsYAML{filePath: filePath}
+
+       err := tn.Load()
+
+       return tn, err
+}
+
+func (n *ThreadedNewsYAML) CreateGrouping(newsPath []string, name string, t [2]byte) error {
+       n.mu.Lock()
+       defer n.mu.Unlock()
+
+       cats := n.getCatByPath(newsPath)
+       cats[name] = hotline.NewsCategoryListData15{
+               Name:     name,
+               Type:     t,
+               Articles: map[uint32]*hotline.NewsArtData{},
+               SubCats:  make(map[string]hotline.NewsCategoryListData15),
+       }
+
+       return n.writeFile()
+}
+
+func (n *ThreadedNewsYAML) NewsItem(newsPath []string) hotline.NewsCategoryListData15 {
+       n.mu.Lock()
+       defer n.mu.Unlock()
+
+       cats := n.ThreadedNews.Categories
+       delName := newsPath[len(newsPath)-1]
+       if len(newsPath) > 1 {
+               for _, fp := range newsPath[0 : len(newsPath)-1] {
+                       cats = cats[fp].SubCats
+               }
+       }
+
+       return cats[delName]
+}
+
+func (n *ThreadedNewsYAML) DeleteNewsItem(newsPath []string) error {
+       n.mu.Lock()
+       defer n.mu.Unlock()
+
+       cats := n.ThreadedNews.Categories
+       delName := newsPath[len(newsPath)-1]
+       if len(newsPath) > 1 {
+               for _, fp := range newsPath[0 : len(newsPath)-1] {
+                       cats = cats[fp].SubCats
+               }
+       }
+
+       delete(cats, delName)
+
+       return n.writeFile()
+}
+
+func (n *ThreadedNewsYAML) GetArticle(newsPath []string, articleID uint32) *hotline.NewsArtData {
+       n.mu.Lock()
+       defer n.mu.Unlock()
+
+       var cat hotline.NewsCategoryListData15
+       cats := n.ThreadedNews.Categories
+
+       for _, fp := range newsPath {
+               cat = cats[fp]
+               cats = cats[fp].SubCats
+       }
+
+       art := cat.Articles[articleID]
+       if art == nil {
+               return nil
+       }
+
+       return art
+}
+
+//
+//func (n *ThreadedNewsYAML) GetNewsCatByPath(paths []string) map[string]hotline.NewsCategoryListData15 {
+//     n.mu.Lock()
+//     defer n.mu.Unlock()
+//
+//     cats := n.getCatByPath(paths)
+//
+//     return cats
+//}
+
+func (n *ThreadedNewsYAML) GetCategories(paths []string) []hotline.NewsCategoryListData15 {
+       n.mu.Lock()
+       defer n.mu.Unlock()
+
+       var categories []hotline.NewsCategoryListData15
+       for _, c := range n.getCatByPath(paths) {
+               categories = append(categories, c)
+       }
+
+       slices.SortFunc(categories, func(a, b hotline.NewsCategoryListData15) int {
+               return cmp.Compare(
+                       a.Name,
+                       b.Name,
+               )
+       })
+
+       return categories
+}
+
+func (n *ThreadedNewsYAML) getCatByPath(paths []string) map[string]hotline.NewsCategoryListData15 {
+       cats := n.ThreadedNews.Categories
+       for _, path := range paths {
+               cats = cats[path].SubCats
+       }
+
+       return cats
+}
+
+func (n *ThreadedNewsYAML) PostArticle(newsPath []string, parentArticleID uint32, article hotline.NewsArtData) error {
+       n.mu.Lock()
+       defer n.mu.Unlock()
+
+       binary.BigEndian.PutUint32(article.ParentArt[:], parentArticleID)
+
+       cats := n.getCatByPath(newsPath[:len(newsPath)-1])
+
+       catName := newsPath[len(newsPath)-1]
+       cat := cats[catName]
+
+       var keys []int
+       for k := range cat.Articles {
+               keys = append(keys, int(k))
+       }
+
+       nextID := uint32(1)
+       if len(keys) > 0 {
+               sort.Ints(keys)
+               prevID := uint32(keys[len(keys)-1])
+               nextID = prevID + 1
+
+               binary.BigEndian.PutUint32(article.PrevArt[:], prevID)
+
+               // Set next article Type
+               binary.BigEndian.PutUint32(cat.Articles[prevID].NextArt[:], nextID)
+       }
+
+       // Update parent article with first child reply
+       parentID := parentArticleID
+       if parentID != 0 {
+               parentArt := cat.Articles[parentID]
+
+               if parentArt.FirstChildArt == [4]byte{0, 0, 0, 0} {
+                       binary.BigEndian.PutUint32(parentArt.FirstChildArt[:], nextID)
+               }
+       }
+
+       cat.Articles[nextID] = &article
+
+       cats[catName] = cat
+
+       return n.writeFile()
+}
+
+func (n *ThreadedNewsYAML) DeleteArticle(newsPath []string, articleID uint32, recursive bool) error {
+       n.mu.Lock()
+       defer n.mu.Unlock()
+
+       if recursive {
+               // TODO: Handle delete recursive
+       }
+
+       cats := n.getCatByPath(newsPath[:len(newsPath)-1])
+
+       catName := newsPath[len(newsPath)-1]
+
+       cat := cats[catName]
+       delete(cat.Articles, articleID)
+       cats[catName] = cat
+
+       return n.writeFile()
+}
+
+func (n *ThreadedNewsYAML) ListArticles(newsPath []string) hotline.NewsArtListData {
+       n.mu.Lock()
+       defer n.mu.Unlock()
+
+       var cat hotline.NewsCategoryListData15
+       cats := n.ThreadedNews.Categories
+
+       for _, fp := range newsPath {
+               cat = cats[fp]
+               cats = cats[fp].SubCats
+       }
+
+       return cat.GetNewsArtListData()
+}
+
+func (n *ThreadedNewsYAML) Load() error {
+       n.mu.Lock()
+       defer n.mu.Unlock()
+
+       fh, err := os.Open(n.filePath)
+       if err != nil {
+               return err
+       }
+       defer fh.Close()
+
+       n.ThreadedNews = hotline.ThreadedNews{}
+
+       decoder := yaml.NewDecoder(fh)
+       return decoder.Decode(&n.ThreadedNews)
+}
+
+func (n *ThreadedNewsYAML) writeFile() error {
+       out, err := yaml.Marshal(&n.ThreadedNews)
+       if err != nil {
+               return err
+       }
+
+       // Define a temporary file path in the same directory.
+       tempFilePath := n.filePath + ".tmp"
+
+       // Write the marshaled YAML to the temporary file.
+       if err := os.WriteFile(tempFilePath, out, 0644); err != nil {
+               return fmt.Errorf("write to temporary file: %v", err)
+       }
+
+       // Atomically rename the temporary file to the final file path.
+       if err := os.Rename(tempFilePath, n.filePath); err != nil {
+               return fmt.Errorf("rename temporary file to final file: %v", err)
+       }
+
+       return nil
+}