aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author2025-12-31 14:15:42 +0300
committer2025-12-31 14:15:42 +0300
commit8c0b8ad931045c70845f662238230edb81f8cd94 (patch)
tree3bfa44e03bae220fc1233c02d716d0baa27efa3d
parentнемного поправил отображение (diff)
downloadqchat-8c0b8ad931045c70845f662238230edb81f8cd94.tar.gz
qchat-8c0b8ad931045c70845f662238230edb81f8cd94.tar.bz2
qchat-8c0b8ad931045c70845f662238230edb81f8cd94.tar.xz
qchat-8c0b8ad931045c70845f662238230edb81f8cd94.zip
Сделал корректную работу с терминалом и историю
-rw-r--r--Dockerfile3
-rw-r--r--internal/chat/chan.go38
-rw-r--r--internal/chat/chat.go17
-rw-r--r--internal/chat/logs.go44
-rw-r--r--internal/chat/presence.go4
-rw-r--r--internal/server/client.go72
-rw-r--r--internal/server/conn.go26
-rw-r--r--internal/server/display.go83
-rw-r--r--internal/server/event.go133
9 files changed, 287 insertions, 133 deletions
diff --git a/Dockerfile b/Dockerfile
index 77d54d6..102353b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,7 +3,7 @@ FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
ARG TARGETPLATFORM
ARG BUILDPLATFORM
-RUN apk add --no-cache git
+RUN apk add --no-cache tzdata git
WORKDIR /app
@@ -23,6 +23,7 @@ LABEL maintainer="neonxp" \
description="qChat - quick ssh chat" \
version="1.0"
+COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /app /app
ENV TZ=Europe/Moscow
diff --git a/internal/chat/chan.go b/internal/chat/chan.go
index bc2b70f..be9ae71 100644
--- a/internal/chat/chan.go
+++ b/internal/chat/chan.go
@@ -11,6 +11,7 @@ type Channel struct {
Name string
Users map[*User]struct{}
Events chan any
+ Log Logs
mu sync.RWMutex
}
@@ -21,6 +22,7 @@ func (c *Channel) Listen(ctx context.Context) {
return
case ev := <-c.Events:
c.processEvent(ev)
+ c.processLog(ev)
}
}
}
@@ -42,6 +44,37 @@ func (c *Channel) processEvent(ev any) {
}
}
+func (c *Channel) processLog(ev any) {
+ switch ev := ev.(type) {
+ case UserJoined:
+ c.Log.Append(LogEntry{
+ Username: ev.User.NUsername(),
+ Time: ev.Time,
+ Type: TypeJoined,
+ })
+ case UserLeft:
+ c.Log.Append(LogEntry{
+ Username: ev.User.NUsername(),
+ Time: ev.Time,
+ Type: TypeLeft,
+ })
+ case Message:
+ c.Log.Append(LogEntry{
+ Username: ev.User.NUsername(),
+ Time: ev.Time,
+ Message: ev.Message,
+ Type: TypeMessage,
+ })
+ case SelfMessage:
+ c.Log.Append(LogEntry{
+ Username: ev.User.NUsername(),
+ Time: time.Now(),
+ Message: "/me " + ev.Message,
+ Type: TypeMessage,
+ })
+ }
+}
+
func (c *Channel) Join(u *User) {
c.mu.Lock()
defer c.mu.Unlock()
@@ -56,6 +89,11 @@ func (c *Channel) Join(u *User) {
}
c.Users[u] = struct{}{}
+
+ // Отправляем при подключении последние 20 сообщений
+ u.Events <- UserLogs{
+ Logs: c.Log.Get(),
+ }
}
func (c *Channel) Leave(u *User) {
diff --git a/internal/chat/chat.go b/internal/chat/chat.go
index 4a639ba..1639d6b 100644
--- a/internal/chat/chat.go
+++ b/internal/chat/chat.go
@@ -15,6 +15,12 @@ var (
"/join [chan] - change current channel to [chan]\n" +
"/chans - list all chans\n" +
"/users - list all online users\n" +
+ "/me [message] - display message like from third person\n" +
+ "Formatting:\n" +
+ "- *Bold*\n" +
+ "- +Italic+\n" +
+ "- -Striked-\n" +
+ "- _Underline_\n" +
"ctrl+c - leave chat"
)
@@ -55,18 +61,18 @@ func (c *Chat) NewUser(username, identify string) *User {
Events: make(chan any, 32),
mu: sync.RWMutex{},
}
+ u.Events <- SystemMessage{
+ Message: fmt.Sprintf("Connected to %s chat server...\nType /help command for list available commands.", c.cfg.Server.Name),
+ }
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
}
@@ -170,8 +176,7 @@ func (c *Chat) Input(ctx context.Context, user *User, input string) {
newChan = c.NewChannel(ctx, newChanName)
}
user.CurrentChan.Leave(user)
- user.CurrentChan = newChan
- newChan.Join(user)
+ user.JoinChan(newChan)
default:
c.Message(user, input)
}
diff --git a/internal/chat/logs.go b/internal/chat/logs.go
new file mode 100644
index 0000000..09d60fe
--- /dev/null
+++ b/internal/chat/logs.go
@@ -0,0 +1,44 @@
+package chat
+
+import "time"
+
+type Logs struct {
+ entries [20]LogEntry
+ start int
+}
+
+func (l *Logs) Append(le LogEntry) {
+ l.entries[l.start] = le
+ l.start++
+ if l.start == len(l.entries) {
+ l.start = 0
+ }
+}
+
+func (l *Logs) Get() []LogEntry {
+ result := make([]LogEntry, 0, len(l.entries))
+ for i := l.start; i < len(l.entries); i++ {
+ result = append(result, l.entries[i])
+ }
+ for i := 0; i < l.start; i++ {
+ result = append(result, l.entries[i])
+ }
+
+ return result
+}
+
+type LogEntry struct {
+ Username string
+ Time time.Time
+ Message string
+ Type LogEntryType
+}
+
+type LogEntryType int
+
+const (
+ TypeNone LogEntryType = iota
+ TypeJoined
+ TypeLeft
+ TypeMessage
+)
diff --git a/internal/chat/presence.go b/internal/chat/presence.go
index 995f214..1057a11 100644
--- a/internal/chat/presence.go
+++ b/internal/chat/presence.go
@@ -15,3 +15,7 @@ type UserLeft struct {
Chan *Channel
Time time.Time
}
+
+type UserLogs struct {
+ Logs []LogEntry
+}
diff --git a/internal/server/client.go b/internal/server/client.go
index 698f7fa..8a23c8b 100644
--- a/internal/server/client.go
+++ b/internal/server/client.go
@@ -2,18 +2,30 @@ package server
import (
"context"
+ "encoding/binary"
"io"
"log/slog"
+ "sync"
- "go.neonxp.ru/qchat/internal/chat"
+ "golang.org/x/crypto/ssh"
"golang.org/x/term"
)
-func (s *Server) serveClient(ctx context.Context, rw io.ReadWriteCloser, user *chat.User) {
- t := term.NewTerminal(rw, "[] ")
- // TODO resize terminal to user size
+func (s *Server) serveClient(
+ ctx context.Context,
+ conn *ssh.ServerConn,
+ channel ssh.Channel,
+ requests <-chan *ssh.Request,
+) {
+ wg := sync.WaitGroup{}
- go func() {
+ identify := conn.Permissions.ExtraData["identify"].(string)
+ user := s.chat.NewUser(conn.User(), identify)
+
+ t := term.NewTerminal(channel, "[] ")
+
+ // Обработка ввода пользователя
+ wg.Go(func() {
for {
select {
case <-ctx.Done():
@@ -31,6 +43,8 @@ func (s *Server) serveClient(ctx context.Context, rw io.ReadWriteCloser, user *c
slog.Error("failed read line", slog.Any("err", err))
}
+ conn.Close()
+
return
}
if len(line) == 0 {
@@ -39,20 +53,42 @@ func (s *Server) serveClient(ctx context.Context, rw io.ReadWriteCloser, user *c
s.chat.Input(ctx, user, line)
}
- }()
+ })
- for message := range user.Events {
- switch message := message.(type) {
- case chat.Message:
- displayMessage(t, message, user)
- case chat.SelfMessage:
- displaySelfMessage(t, message)
- case chat.SystemMessage:
- displaySystemMessage(t, message)
- case chat.UserJoined:
- displayUserJoined(t, message)
- case chat.UserLeft:
- displayUserLeft(t, message)
+ wg.Go(func() {
+ for req := range requests {
+ switch req.Type {
+ case "pty-req":
+ termLen := req.Payload[3]
+ w, h := parseDims(req.Payload[termLen+4:])
+ t.SetSize(w, h)
+ req.Reply(true, nil)
+ case "window-change":
+ w, h := parseDims(req.Payload)
+ t.SetSize(w, h)
+ req.Reply(true, nil)
+ case "shell":
+ req.Reply(len(req.Payload) == 0, nil)
+ default:
+ req.Reply(false, nil)
+ }
+ slog.Debug(
+ "req",
+ slog.String("type", req.Type),
+ slog.Bool("want-reply", req.WantReply),
+ slog.String("payload", string(req.Payload)),
+ )
}
+ })
+
+ for message := range user.Events {
+ processUserEvent(message, t, user)
}
}
+
+func parseDims(b []byte) (int, int) {
+ w := binary.BigEndian.Uint32(b)
+ h := binary.BigEndian.Uint32(b[4:])
+
+ return int(w), int(h)
+}
diff --git a/internal/server/conn.go b/internal/server/conn.go
index 1069b0b..347b4eb 100644
--- a/internal/server/conn.go
+++ b/internal/server/conn.go
@@ -35,31 +35,7 @@ func (s *Server) serveConn(ctx context.Context, nConn net.Conn, config *ssh.Serv
}
wg.Go(func() {
- for req := range requests {
- switch req.Type {
- case "pty-req":
- req.Reply(true, nil)
- case "shell":
- req.Reply(true, nil)
- default:
- req.Reply(false, nil)
- }
- slog.Debug(
- "req",
- slog.String("type", req.Type),
- slog.Bool("want-reply", req.WantReply),
- slog.String("payload", string(req.Payload)),
- )
- }
- })
-
- wg.Go(func() {
- identify := conn.Permissions.ExtraData["identify"].(string)
- user := s.chat.NewUser(conn.User(), identify)
- slog.Info("joined", slog.String("user", user.NUsername()))
- s.serveClient(ctx, channel, user)
- slog.Info("disconnected", slog.String("user", user.NUsername()))
- conn.Close()
+ s.serveClient(ctx, conn, channel, requests)
})
}
diff --git a/internal/server/display.go b/internal/server/display.go
deleted file mode 100644
index c3450ad..0000000
--- a/internal/server/display.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package server
-
-import (
- "fmt"
- "io"
- "regexp"
- "strings"
-
- "go.neonxp.ru/qchat/internal/chat"
-)
-
-var replacements = []replacement{
- {
- From: *regexp.MustCompile(`\*(.+?)\*`),
- To: fmt.Sprintf("%s$1%s", escapeCodes[Bold], escapeCodes[Reset]),
- },
- {
- From: *regexp.MustCompile(`_(.+?)_`),
- To: fmt.Sprintf("%s$1%s", escapeCodes[Italic], escapeCodes[Reset]),
- },
-}
-
-func displayMessage(t io.Writer, message chat.Message, user *chat.User) {
- msg := message.Message
- mentionFrom := "@" + user.Username
- mentionTo := string(escapeCodes[Blink]) + string(escapeCodes[Underline]) + "@" + user.Username + string(escapeCodes[Reset])
- if strings.Contains(msg, mentionFrom) {
- msg = strings.ReplaceAll(msg, mentionFrom, mentionTo)
- msg = "\x07\a" + msg
- }
-
- for _, r := range replacements {
- msg = r.From.ReplaceAllString(msg, r.To)
- }
-
- fmt.Fprintf(t, "%s%15s%s [%s]%s: %s\n",
- escapeCodes[Blue],
- message.User.NUsername(),
- escapeCodes[Green],
- message.Time.Format("15:04:05"),
- escapeCodes[Reset],
- msg,
- )
-}
-
-func displaySystemMessage(t io.Writer, message chat.SystemMessage) {
- fmt.Fprintf(t, "%s* %s %s\n",
- escapeCodes[Green],
- message.Message,
- escapeCodes[Reset],
- )
-}
-func displaySelfMessage(t io.Writer, message chat.SelfMessage) {
- fmt.Fprintf(t, "%s* %s %s%s\n",
- escapeCodes[Blue],
- message.User.NUsername(),
- message.Message,
- escapeCodes[Reset],
- )
-}
-
-func displayUserJoined(t io.Writer, presence chat.UserJoined) {
- fmt.Fprintf(t, "%s* %s joined to %s chan%s\n",
- escapeCodes[Green],
- presence.User.NUsername(),
- presence.Chan.Name,
- escapeCodes[Reset],
- )
-}
-
-func displayUserLeft(t io.Writer, presence chat.UserLeft) {
- fmt.Fprintf(t, "%s* %s left %s chan%s\n",
- escapeCodes[Red],
- presence.User.NUsername(),
- presence.Chan.Name,
- escapeCodes[Reset],
- )
-}
-
-type replacement struct {
- From regexp.Regexp
- To string
-}
diff --git a/internal/server/event.go b/internal/server/event.go
new file mode 100644
index 0000000..d006d22
--- /dev/null
+++ b/internal/server/event.go
@@ -0,0 +1,133 @@
+package server
+
+import (
+ "fmt"
+ "io"
+ "regexp"
+ "strings"
+
+ "go.neonxp.ru/qchat/internal/chat"
+)
+
+var replacements = []replacement{
+ {
+ From: *regexp.MustCompile(`\*(.+?)\*`),
+ To: fmt.Sprintf("%s$1%s", escapeCodes[Bold], escapeCodes[Reset]),
+ },
+ {
+ From: *regexp.MustCompile(`\+(.+?)\+`),
+ To: fmt.Sprintf("%s$1%s", escapeCodes[Italic], escapeCodes[Reset]),
+ },
+ {
+ From: *regexp.MustCompile(`\-(.+?)\-`),
+ To: fmt.Sprintf("%s$1%s", escapeCodes[Strike], escapeCodes[Reset]),
+ },
+ {
+ From: *regexp.MustCompile(`_(.+?)_`),
+ To: fmt.Sprintf("%s$1%s", escapeCodes[Underline], escapeCodes[Reset]),
+ },
+}
+
+func processUserEvent(message any, w io.Writer, user *chat.User) {
+ switch message := message.(type) {
+ case chat.Message:
+ displayMessage(w, message, user)
+ case chat.SelfMessage:
+ displaySelfMessage(w, message)
+ case chat.SystemMessage:
+ displaySystemMessage(w, message)
+ case chat.UserJoined:
+ displayUserJoined(w, message)
+ case chat.UserLeft:
+ displayUserLeft(w, message)
+ case chat.UserLogs:
+ displayUserLogs(w, message)
+ }
+}
+
+func displayMessage(w io.Writer, message chat.Message, user *chat.User) {
+ msg := message.Message
+ mentionFrom := "@" + user.Username
+ mentionTo := string(escapeCodes[Yellow]) + string(escapeCodes[Underline]) + "@" + user.Username + string(escapeCodes[Reset])
+ if strings.Contains(msg, mentionFrom) {
+ msg = strings.ReplaceAll(msg, mentionFrom, mentionTo)
+ msg = "\x07\a" + msg
+ }
+
+ for _, r := range replacements {
+ msg = r.From.ReplaceAllString(msg, r.To)
+ }
+
+ fmt.Fprintf(w, "%s%18s%s [%s]%s: %s\n",
+ escapeCodes[Blue],
+ message.User.NUsername(),
+ escapeCodes[Green],
+ message.Time.Format("15:04:05"),
+ escapeCodes[Reset],
+ msg,
+ )
+}
+
+func displaySystemMessage(w io.Writer, message chat.SystemMessage) {
+ fmt.Fprintf(w, "%s* %s %s\n",
+ escapeCodes[Green],
+ message.Message,
+ escapeCodes[Reset],
+ )
+}
+func displaySelfMessage(w io.Writer, message chat.SelfMessage) {
+ fmt.Fprintf(w, "%s* %s %s%s\n",
+ escapeCodes[Blue],
+ message.User.NUsername(),
+ message.Message,
+ escapeCodes[Reset],
+ )
+}
+
+func displayUserJoined(w io.Writer, presence chat.UserJoined) {
+ fmt.Fprintf(w, "%s* %s joined to %s chan%s\n",
+ escapeCodes[Green],
+ presence.User.NUsername(),
+ presence.Chan.Name,
+ escapeCodes[Reset],
+ )
+}
+
+func displayUserLeft(w io.Writer, presence chat.UserLeft) {
+ fmt.Fprintf(w, "%s* %s left %s chan%s\n",
+ escapeCodes[Red],
+ presence.User.NUsername(),
+ presence.Chan.Name,
+ escapeCodes[Reset],
+ )
+}
+func displayUserLogs(w io.Writer, message chat.UserLogs) {
+ messages := make([]string, 0, len(message.Logs))
+ for _, m := range message.Logs {
+ switch m.Type {
+ case chat.TypeNone:
+ continue
+ case chat.TypeJoined:
+ messages = append(messages, fmt.Sprintf("* %18s [%s]: joined", m.Username, m.Time.Format("15:04:05")))
+ case chat.TypeLeft:
+ messages = append(messages, fmt.Sprintf("* %18s [%s]: left", m.Username, m.Time.Format("15:04:05")))
+ case chat.TypeMessage:
+ messages = append(messages, fmt.Sprintf("%18s [%s]: %s", m.Username, m.Time.Format("15:04:05"), m.Message))
+ }
+ }
+
+ if len(messages) == 0 {
+ return
+ }
+
+ fmt.Fprintf(w, "%s* Last channel messages:\n%s%s\n",
+ escapeCodes[Blue],
+ strings.Join(messages, "\n"),
+ escapeCodes[Reset],
+ )
+}
+
+type replacement struct {
+ From regexp.Regexp
+ To string
+}