diff options
Diffstat (limited to '')
| -rw-r--r-- | internal/chat/chan.go | 69 | ||||
| -rw-r--r-- | internal/chat/chat.go | 178 | ||||
| -rw-r--r-- | internal/chat/message.go | 20 | ||||
| -rw-r--r-- | internal/chat/presence.go | 17 | ||||
| -rw-r--r-- | internal/chat/user.go | 35 |
5 files changed, 319 insertions, 0 deletions
diff --git a/internal/chat/chan.go b/internal/chat/chan.go new file mode 100644 index 0000000..ae78bb6 --- /dev/null +++ b/internal/chat/chan.go @@ -0,0 +1,69 @@ +package chat + +import ( + "context" + "log/slog" + "sync" + "time" +) + +type Channel struct { + Name string + Users map[*User]struct{} + Events chan any + mu sync.RWMutex +} + +func (c *Channel) Listen(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case ev := <-c.Events: + c.processEvent(ev) + } + } +} + +func (c *Channel) processEvent(ev any) { + slog.Info("channel event", slog.String("channel", c.Name), slog.Any("event", ev)) + + for u := range c.Users { + select { + case u.Events <- ev: + default: + } + } +} + +func (c *Channel) Join(u *User) { + c.mu.Lock() + defer c.mu.Unlock() + + if _, ok := c.Users[u]; ok { + return + } + c.Events <- UserJoined{ + User: u, + Time: time.Now(), + Chan: c, + } + + c.Users[u] = struct{}{} +} + +func (c *Channel) Leave(u *User) { + c.mu.Lock() + defer c.mu.Unlock() + + if _, ok := c.Users[u]; !ok { + return + } + c.Events <- UserLeft{ + User: u, + Time: time.Now(), + Chan: c, + } + + delete(c.Users, u) +} diff --git a/internal/chat/chat.go b/internal/chat/chat.go new file mode 100644 index 0000000..4a639ba --- /dev/null +++ b/internal/chat/chat.go @@ -0,0 +1,178 @@ +package chat + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + "go.neonxp.ru/qchat/internal/config" +) + +var ( + helpMessage = "Available commands:\n" + + "/join [chan] - change current channel to [chan]\n" + + "/chans - list all chans\n" + + "/users - list all online users\n" + + "ctrl+c - leave chat" +) + +type Chat struct { + cfg *config.Config + users map[*User]struct{} + channels map[*Channel]struct{} + wg sync.WaitGroup + mu sync.RWMutex +} + +func New(cfg *config.Config) *Chat { + return &Chat{ + cfg: cfg, + users: make(map[*User]struct{}, 32), + channels: make(map[*Channel]struct{}, 32), + wg: sync.WaitGroup{}, + mu: sync.RWMutex{}, + } +} + +func (c *Chat) Run(ctx context.Context) error { + for _, channel := range c.cfg.Channels { + c.NewChannel(ctx, channel.Name) + } + + c.wg.Wait() + + return nil +} + +func (c *Chat) NewUser(username, identify string) *User { + u := &User{ + Username: username, + Identify: identify, + Chans: map[string]*Channel{}, + CurrentChan: nil, + Events: make(chan any, 32), + mu: sync.RWMutex{}, + } + + ch := c.GetChannel("main") + if ch != nil { + u.JoinChan(ch) + } + + c.users[u] = struct{}{} + + u.Events <- SystemMessage{ + Message: fmt.Sprintf("Connected to %s chat server...", c.cfg.Server.Name), + } + + return u +} + +func (c *Chat) RemoveUser(u *User) { + c.mu.Lock() + defer c.mu.Unlock() + + for ch := range c.channels { + ch.Leave(u) + } + + close(u.Events) + + delete(c.users, u) +} + +func (c *Chat) NewChannel(ctx context.Context, name string) *Channel { + ch := &Channel{ + Name: name, + Users: make(map[*User]struct{}, 32), + Events: make(chan any), + mu: sync.RWMutex{}, + } + c.mu.Lock() + defer c.mu.Unlock() + c.channels[ch] = struct{}{} + c.wg.Go(func() { + ch.Listen(ctx) + }) + + return ch +} + +func (c *Chat) GetChannel(name string) *Channel { + for ch := range c.channels { + if ch.Name == name { + return ch + } + } + + return nil +} + +func (c *Chat) Message(user *User, message string) { + if user.CurrentChan == nil { + return + } + + user.CurrentChan.Events <- Message{ + User: user, + Message: message, + Time: time.Now(), + } +} + +func (c *Chat) SelfMessage(user *User, message string) { + if user.CurrentChan == nil { + return + } + + user.CurrentChan.Events <- SelfMessage{ + User: user, + Message: message, + } +} +func (c *Chat) SystemMessage(user *User, message string) { + user.Events <- SystemMessage{ + Message: message, + } +} + +func (c *Chat) Input(ctx context.Context, user *User, input string) { + cmd := strings.ToLower(input) + switch { + case strings.HasPrefix(cmd, "/me "): + c.SelfMessage(user, strings.TrimPrefix(input, "/me ")) + case cmd == "/help": + c.SystemMessage(user, helpMessage) + case cmd == "/list": + list := make([]string, 0, len(c.channels)) + for ch := range c.channels { + list = append(list, ch.Name) + } + c.SystemMessage(user, "Chans:\n"+strings.Join(list, "\n")) + case cmd == "/users": + list := make([]string, 0, len(c.users)) + for u := range c.users { + list = append(list, u.NUsername()) + } + c.SystemMessage(user, "Users:\n"+strings.Join(list, "\n")) + case strings.HasPrefix(cmd, "/join "): + newChanName := strings.TrimPrefix(input, "/join ") + var newChan *Channel + for ch := range c.channels { + if ch.Name == newChanName { + newChan = ch + break + } + } + if newChan == nil { + newChan = c.NewChannel(ctx, newChanName) + } + user.CurrentChan.Leave(user) + user.CurrentChan = newChan + newChan.Join(user) + default: + c.Message(user, input) + } +} diff --git a/internal/chat/message.go b/internal/chat/message.go new file mode 100644 index 0000000..081d640 --- /dev/null +++ b/internal/chat/message.go @@ -0,0 +1,20 @@ +package chat + +import ( + "time" +) + +type SystemMessage struct { + Message string +} + +type Message struct { + User *User + Time time.Time + Message string +} + +type SelfMessage struct { + User *User + Message string +} diff --git a/internal/chat/presence.go b/internal/chat/presence.go new file mode 100644 index 0000000..995f214 --- /dev/null +++ b/internal/chat/presence.go @@ -0,0 +1,17 @@ +package chat + +import ( + "time" +) + +type UserJoined struct { + User *User + Chan *Channel + Time time.Time +} + +type UserLeft struct { + User *User + Chan *Channel + Time time.Time +} diff --git a/internal/chat/user.go b/internal/chat/user.go new file mode 100644 index 0000000..e897f32 --- /dev/null +++ b/internal/chat/user.go @@ -0,0 +1,35 @@ +package chat + +import ( + "sync" +) + +type User struct { + Username string + Identify string + Chans map[string]*Channel + CurrentChan *Channel + Events chan any + mu sync.RWMutex +} + +func (u *User) JoinChan(channel *Channel) { + u.mu.Lock() + defer u.mu.Unlock() + + if _, ok := u.Chans[channel.Name]; ok { + return + } + + u.Chans[channel.Name] = channel + u.CurrentChan = channel + channel.Join(u) +} + +func (u *User) NUsername() string { + username := u.Username + identify := u.Identify + shortIdentify := identify[max(0, len(identify)-5):] + + return username[:min(len(username), 10)] + " (" + shortIdentify + ")" +} |
