aboutsummaryrefslogtreecommitdiff
path: root/internal/chat
diff options
context:
space:
mode:
author2025-12-30 19:33:39 +0300
committer2025-12-30 19:33:39 +0300
commitd317e8f6df0e0e16445db606da1d683a6b35f531 (patch)
tree4b80de04e17a137cff2dc309508b5f841f48c994 /internal/chat
downloadqchat-d317e8f6df0e0e16445db606da1d683a6b35f531.tar.gz
qchat-d317e8f6df0e0e16445db606da1d683a6b35f531.tar.bz2
qchat-d317e8f6df0e0e16445db606da1d683a6b35f531.tar.xz
qchat-d317e8f6df0e0e16445db606da1d683a6b35f531.zip
начальный коммит
Diffstat (limited to '')
-rw-r--r--internal/chat/chan.go69
-rw-r--r--internal/chat/chat.go178
-rw-r--r--internal/chat/message.go20
-rw-r--r--internal/chat/presence.go17
-rw-r--r--internal/chat/user.go35
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 + ")"
+}