mirror of
https://github.com/supleed2/go-chat.git
synced 2024-12-22 14:15:49 +00:00
Initial Commit
This commit is contained in:
commit
6a94af7daf
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
# Project specific files
|
||||||
|
nicks.json
|
334
client/main.go
Normal file
334
client/main.go
Normal file
|
@ -0,0 +1,334 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
c "go-chat/common"
|
||||||
|
|
||||||
|
"github.com/alexflint/go-arg"
|
||||||
|
"github.com/charmbracelet/bubbles/help"
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
ws "nhooyr.io/websocket"
|
||||||
|
"nhooyr.io/websocket/wsjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
const manText string = `The current available commands are:
|
||||||
|
man
|
||||||
|
prints this message
|
||||||
|
mv <string>
|
||||||
|
set your nick
|
||||||
|
ls
|
||||||
|
get the available rooms
|
||||||
|
cd <string>
|
||||||
|
connect to a room
|
||||||
|
who
|
||||||
|
list users in the current room
|
||||||
|
moo
|
||||||
|
:)`
|
||||||
|
|
||||||
|
const mooText string = `
|
||||||
|
(__)
|
||||||
|
(oo)
|
||||||
|
/------\/
|
||||||
|
/ | ||
|
||||||
|
* /\---/\
|
||||||
|
~~ ~~
|
||||||
|
..."Have you mooed today?"...`
|
||||||
|
|
||||||
|
type showTim int
|
||||||
|
|
||||||
|
const (
|
||||||
|
off showTim = iota
|
||||||
|
short
|
||||||
|
full
|
||||||
|
)
|
||||||
|
|
||||||
|
type model struct {
|
||||||
|
history viewport.Model
|
||||||
|
msgs []c.SMsg
|
||||||
|
showTim showTim
|
||||||
|
tz time.Location
|
||||||
|
input textinput.Model
|
||||||
|
idStyle lipgloss.Style
|
||||||
|
pStyle lipgloss.Style
|
||||||
|
help help.Model
|
||||||
|
recvCh chan c.SMsg
|
||||||
|
sendCh chan c.CMsg
|
||||||
|
conn *ws.Conn
|
||||||
|
exitCh chan exit
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
Address string `arg:"positional" default:"localhost:8000" help:"address to connect to, without ws://" placeholder:"HOST[:PORT]"`
|
||||||
|
Timestamps showTim `arg:"-t" default:"off" help:"display timestamps of messages, ctrl+t to cycle after startup [off, short, full]" placeholder:"CHOICE"`
|
||||||
|
Nick *string `arg:"-n" help:"attempt to automatically set nick after connecting"`
|
||||||
|
Password *string `arg:"-p" help:"password, if required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var a args
|
||||||
|
arg.MustParse(&a)
|
||||||
|
|
||||||
|
conn, _, err := ws.Dial(ctx, "ws://"+a.Address, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer conn.Close(ws.StatusNormalClosure, "")
|
||||||
|
|
||||||
|
local, err := time.LoadLocation("Local")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := tea.NewProgram(initModel(ctx, conn, a, *local), tea.WithAltScreen(), tea.WithMouseCellMotion())
|
||||||
|
if _, err := p.Run(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initModel(ctx context.Context, conn *ws.Conn, a args, tz time.Location) model {
|
||||||
|
discCtx, disc := context.WithCancel(ctx)
|
||||||
|
exitCh := make(chan exit)
|
||||||
|
|
||||||
|
recvCh := make(chan c.SMsg)
|
||||||
|
go func() {
|
||||||
|
smsg := c.SMsg{}
|
||||||
|
for {
|
||||||
|
err := wsjson.Read(ctx, conn, &smsg)
|
||||||
|
if err != nil {
|
||||||
|
if ws.CloseStatus(err) != ws.StatusNormalClosure {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
disc()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recvCh <- smsg
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
sendCh := make(chan c.CMsg)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case cmsg := <-sendCh:
|
||||||
|
err := wsjson.Write(ctx, conn, cmsg)
|
||||||
|
if err != nil {
|
||||||
|
recvCh <- c.SMsg{Tim: time.Now(), Id: "system", Msg: fmt.Sprintf("wsjson error when sending message: %v", err)}
|
||||||
|
}
|
||||||
|
case <-discCtx.Done():
|
||||||
|
exitCh <- exit{}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ta := textinput.New()
|
||||||
|
ta.Placeholder = "Send a message (or a command with /)"
|
||||||
|
ta.Focus()
|
||||||
|
ta.CharLimit = 128
|
||||||
|
ta.Width = 60
|
||||||
|
|
||||||
|
vp := viewport.New(60, 5)
|
||||||
|
vp.KeyMap = viewport.KeyMap{
|
||||||
|
PageDown: key.NewBinding(
|
||||||
|
key.WithKeys("pgdown"),
|
||||||
|
key.WithHelp("pgdn", "page down"),
|
||||||
|
),
|
||||||
|
PageUp: key.NewBinding(
|
||||||
|
key.WithKeys("pgup"),
|
||||||
|
key.WithHelp("pgup", "page up"),
|
||||||
|
),
|
||||||
|
Up: key.NewBinding(
|
||||||
|
key.WithKeys("up"),
|
||||||
|
key.WithHelp("↑", "up"),
|
||||||
|
),
|
||||||
|
Down: key.NewBinding(
|
||||||
|
key.WithKeys("down"),
|
||||||
|
key.WithHelp("↓", "down"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Nick != nil {
|
||||||
|
login := *a.Nick
|
||||||
|
if a.Password != nil {
|
||||||
|
login += ":" + *a.Password
|
||||||
|
}
|
||||||
|
sendCh <- c.CMsg{Typ: c.Mv, Msg: login}
|
||||||
|
}
|
||||||
|
|
||||||
|
messages := []c.SMsg{{Tim: time.Now(), Id: "system", Msg: "Welcome to the chat room! Press Enter to send, /man for more info :)"}}
|
||||||
|
|
||||||
|
return model{
|
||||||
|
input: ta,
|
||||||
|
msgs: messages,
|
||||||
|
showTim: a.Timestamps,
|
||||||
|
tz: tz,
|
||||||
|
history: vp,
|
||||||
|
idStyle: lipgloss.NewStyle().Width(30),
|
||||||
|
pStyle: lipgloss.NewStyle().Bold(true),
|
||||||
|
help: help.New(),
|
||||||
|
recvCh: recvCh,
|
||||||
|
sendCh: sendCh,
|
||||||
|
conn: conn,
|
||||||
|
exitCh: exitCh,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Init() tea.Cmd {
|
||||||
|
return tea.Batch(
|
||||||
|
tea.SetWindowTitle("go-chat by 8bit"),
|
||||||
|
textinput.Blink,
|
||||||
|
getNextSMsg(m.recvCh),
|
||||||
|
getExitMsg(m.exitCh),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var tiCmd, vpCmd, clCmd, smCmd tea.Cmd
|
||||||
|
m.input, tiCmd = m.input.Update(msg)
|
||||||
|
m.history, vpCmd = m.history.Update(msg)
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case exit:
|
||||||
|
return m, tea.Quit
|
||||||
|
case c.SMsg:
|
||||||
|
m.msgs = append(m.msgs, msg)
|
||||||
|
m.history.SetContent(m.viewMessages())
|
||||||
|
m.history.GotoBottom()
|
||||||
|
smCmd = getNextSMsg(m.recvCh)
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.Type {
|
||||||
|
case tea.KeyCtrlC, tea.KeyEsc:
|
||||||
|
return m, tea.Quit
|
||||||
|
case tea.KeyCtrlT:
|
||||||
|
m.showTim = (m.showTim + 1) % 3
|
||||||
|
m.history.SetContent(m.viewMessages())
|
||||||
|
case tea.KeyEnter:
|
||||||
|
text := strings.TrimSpace(m.input.Value())
|
||||||
|
if text, ok := strings.CutPrefix(text, "/"); ok {
|
||||||
|
if text == "man" {
|
||||||
|
m.recvCh <- c.SMsg{Tim: time.Now(), Id: "system", Msg: manText}
|
||||||
|
} else if text, ok := strings.CutPrefix(text, "mv "); ok {
|
||||||
|
m.sendCh <- c.CMsg{Typ: c.Mv, Msg: text}
|
||||||
|
} else if text == "ls" {
|
||||||
|
m.sendCh <- c.CMsg{Typ: c.Ls, Msg: ""}
|
||||||
|
} else if text, ok := strings.CutPrefix(text, "cd "); ok {
|
||||||
|
m.sendCh <- c.CMsg{Typ: c.Cd, Msg: text}
|
||||||
|
} else if text == "who" {
|
||||||
|
m.sendCh <- c.CMsg{Typ: c.Who, Msg: ""}
|
||||||
|
} else if text, ok := strings.CutPrefix(text, "sudo "); ok {
|
||||||
|
m.sendCh <- c.CMsg{Typ: c.Sudo, Msg: text}
|
||||||
|
} else if text == "moo" {
|
||||||
|
m.recvCh <- c.SMsg{Tim: time.Now(), Id: "cow", Msg: mooText}
|
||||||
|
} else {
|
||||||
|
m.recvCh <- c.SMsg{Tim: time.Now(), Id: "system", Msg: "Unrecognised command, use /man for more info"}
|
||||||
|
}
|
||||||
|
} else if text != "" {
|
||||||
|
m.sendCh <- c.CMsg{Typ: c.Echo, Msg: text}
|
||||||
|
}
|
||||||
|
m.input.Reset()
|
||||||
|
}
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
clCmd = tea.Sequence(tea.ExitAltScreen, tea.EnterAltScreen)
|
||||||
|
m.history.Height = msg.Height - 2
|
||||||
|
m.history.Width = msg.Width
|
||||||
|
m.history.GotoBottom()
|
||||||
|
m.input.Width = msg.Width - 3
|
||||||
|
m.idStyle.Width(msg.Width)
|
||||||
|
m.help.Width = msg.Width - 1
|
||||||
|
m.history.SetContent(m.viewMessages())
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, tea.Batch(tiCmd, vpCmd, clCmd, smCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) View() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s\n%s\n%s",
|
||||||
|
m.history.View(),
|
||||||
|
m.input.View(),
|
||||||
|
m.help.View(m),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) ShortHelp() []key.Binding {
|
||||||
|
return []key.Binding{
|
||||||
|
m.history.KeyMap.PageDown,
|
||||||
|
m.history.KeyMap.PageUp,
|
||||||
|
m.history.KeyMap.Down,
|
||||||
|
m.history.KeyMap.Up,
|
||||||
|
key.NewBinding(
|
||||||
|
key.WithKeys("ctrl+t"),
|
||||||
|
key.WithHelp("ctrl+t", "toggle timestamps"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) FullHelp() [][]key.Binding {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) viewMessages() string {
|
||||||
|
s := ""
|
||||||
|
for i := range m.msgs {
|
||||||
|
prefix := ""
|
||||||
|
if m.showTim == short {
|
||||||
|
prefix += m.msgs[i].Tim.In(&m.tz).Format(time.TimeOnly) + " "
|
||||||
|
} else if m.showTim == full {
|
||||||
|
prefix += m.msgs[i].Tim.In(&m.tz).Format(time.DateTime) + " "
|
||||||
|
}
|
||||||
|
if m.msgs[i].Id == "system" {
|
||||||
|
prefix += m.pStyle.Foreground(lipgloss.Color("201")).Render("system:")
|
||||||
|
} else {
|
||||||
|
prefix += m.pStyle.Foreground(lipgloss.Color(prefixColor(m.msgs[i].Id))).Render(m.msgs[i].Id + ":")
|
||||||
|
}
|
||||||
|
s += m.idStyle.SetString(prefix).Render(m.msgs[i].Msg) + "\n"
|
||||||
|
}
|
||||||
|
return s[:len(s)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func prefixColor(s string) string {
|
||||||
|
if len(s) == 0 {
|
||||||
|
s = "missing"
|
||||||
|
}
|
||||||
|
return fmt.Sprint(uint(s[0]+s[len(s)-1]) % 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNextSMsg(c <-chan c.SMsg) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
return <-c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type exit struct{}
|
||||||
|
|
||||||
|
func getExitMsg(c <-chan exit) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
return <-c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (st *showTim) UnmarshalText(b []byte) error {
|
||||||
|
s := string(b)
|
||||||
|
switch s {
|
||||||
|
case "off":
|
||||||
|
*st = off
|
||||||
|
case "short":
|
||||||
|
*st = short
|
||||||
|
case "full":
|
||||||
|
*st = full
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid choice: %s [off, short, full]", s)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
25
common/types.go
Normal file
25
common/types.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type SMsg struct {
|
||||||
|
Tim time.Time
|
||||||
|
Id string
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CMsgT int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Sudo CMsgT = iota
|
||||||
|
Echo
|
||||||
|
Mv
|
||||||
|
Ls
|
||||||
|
Cd
|
||||||
|
Who
|
||||||
|
)
|
||||||
|
|
||||||
|
type CMsg struct {
|
||||||
|
Typ CMsgT
|
||||||
|
Msg string
|
||||||
|
}
|
32
go.mod
Normal file
32
go.mod
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
module go-chat
|
||||||
|
|
||||||
|
go 1.21.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alexflint/go-arg v1.4.3
|
||||||
|
github.com/charmbracelet/bubbles v0.17.1
|
||||||
|
github.com/charmbracelet/bubbletea v0.25.0
|
||||||
|
github.com/charmbracelet/lipgloss v0.9.1
|
||||||
|
nhooyr.io/websocket v1.8.10
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alexflint/go-scalar v1.2.0 // indirect
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/reflow v0.3.0 // indirect
|
||||||
|
github.com/muesli/termenv v0.15.2 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.4 // indirect
|
||||||
|
github.com/stretchr/testify v1.8.4 // indirect
|
||||||
|
golang.org/x/sync v0.6.0 // indirect
|
||||||
|
golang.org/x/sys v0.16.0 // indirect
|
||||||
|
golang.org/x/term v0.16.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
)
|
64
go.sum
Normal file
64
go.sum
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo=
|
||||||
|
github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA=
|
||||||
|
github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||||
|
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||||
|
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||||
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/charmbracelet/bubbles v0.17.1 h1:0SIyjOnkrsfDo88YvPgAWvZMwXe26TP6drRvmkjyUu4=
|
||||||
|
github.com/charmbracelet/bubbles v0.17.1/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o=
|
||||||
|
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
|
||||||
|
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
|
||||||
|
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
|
||||||
|
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
|
||||||
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||||
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||||
|
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||||
|
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||||
|
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
|
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||||
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
||||||
|
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
|
||||||
|
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
359
server/main.go
Normal file
359
server/main.go
Normal file
|
@ -0,0 +1,359 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
c "go-chat/common"
|
||||||
|
|
||||||
|
"github.com/alexflint/go-arg"
|
||||||
|
ws "nhooyr.io/websocket"
|
||||||
|
"nhooyr.io/websocket/wsjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type user struct {
|
||||||
|
room string
|
||||||
|
nick string
|
||||||
|
}
|
||||||
|
|
||||||
|
type conns struct {
|
||||||
|
sm sync.Mutex
|
||||||
|
cm map[*ws.Conn]user
|
||||||
|
}
|
||||||
|
|
||||||
|
type server struct {
|
||||||
|
admin string
|
||||||
|
logFn func(string, ...interface{})
|
||||||
|
conns *conns
|
||||||
|
rooms map[string]struct{}
|
||||||
|
rhist map[string][]c.SMsg
|
||||||
|
rhlen int
|
||||||
|
nickm map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log := log.New(os.Stderr, "ws server 🚀 ", log.LstdFlags|log.Lshortfile|log.Lmsgprefix)
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Admin string `arg:"-a" default:"8bit" help:"admin user nick, allows access to /sudo" placeholder:"NICK"`
|
||||||
|
HistLen uint `arg:"-h" default:"10" help:"set message history size" placeholder:"N"`
|
||||||
|
Port uint `arg:"positional" default:"0" help:"port to listen on, random available port if not set"`
|
||||||
|
NickMap *string `arg:"-n" help:"path to nick:pass JSON file" placeholder:"FILE"`
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
|
||||||
|
nickMap, err := loadNickMap(args.NickMap)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = run("localhost:"+fmt.Sprint(args.Port), nickMap, args.Admin, int(args.HistLen), log)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(addr string, nickMap map[string]string, admin string, rhlen int, log *log.Logger) error {
|
||||||
|
listener, err := net.Listen("tcp4", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("listening on ws://%v", listener.Addr())
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Handler: server{
|
||||||
|
admin: admin,
|
||||||
|
logFn: log.Printf,
|
||||||
|
conns: &conns{cm: make(map[*ws.Conn]user)},
|
||||||
|
rooms: map[string]struct{}{"general": {}, "test1": {}, "test2": {}},
|
||||||
|
rhist: make(map[string][]c.SMsg),
|
||||||
|
rhlen: rhlen,
|
||||||
|
nickm: nickMap,
|
||||||
|
},
|
||||||
|
ReadTimeout: 10 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
errch := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
errch <- server.Serve(listener)
|
||||||
|
}()
|
||||||
|
|
||||||
|
signals := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signals, os.Interrupt)
|
||||||
|
select {
|
||||||
|
case err := <-errch:
|
||||||
|
log.Printf("failed to serve: %v", err)
|
||||||
|
case signal := <-signals:
|
||||||
|
log.Printf("quitting: %v", signal)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
return server.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
conn, err := ws.Accept(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
s.logFn("%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.CloseNow()
|
||||||
|
|
||||||
|
if conn.Subprotocol() != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
port := strings.Split(r.RemoteAddr, ":")[1]
|
||||||
|
s.conns.sm.Lock()
|
||||||
|
s.conns.cm[conn] = user{room: "general", nick: port}
|
||||||
|
s.conns.sm.Unlock()
|
||||||
|
defer func() {
|
||||||
|
s.conns.sm.Lock()
|
||||||
|
delete(s.conns.cm, conn)
|
||||||
|
s.logFn("Remaining connections: %v", len(s.conns.cm))
|
||||||
|
s.conns.sm.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
s.logFn("connected: %v", r.RemoteAddr)
|
||||||
|
for i := range s.rhist["general"] {
|
||||||
|
wsjson.Write(ctx, conn, s.rhist["general"][i])
|
||||||
|
}
|
||||||
|
cmsg := c.CMsg{}
|
||||||
|
smsg := c.SMsg{Id: port}
|
||||||
|
for {
|
||||||
|
err := func(ctx context.Context, conn *ws.Conn) error {
|
||||||
|
err := wsjson.Read(ctx, conn, &cmsg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cmsg.Typ {
|
||||||
|
case c.Sudo:
|
||||||
|
s.logFn("(%v) sudo: %v", smsg.Id, cmsg.Msg)
|
||||||
|
if smsg.Id == s.admin {
|
||||||
|
cmd := strings.Split(cmsg.Msg, " ")
|
||||||
|
if len(cmd) == 2 {
|
||||||
|
if cmd[0] == "mk" {
|
||||||
|
if _, ok := s.rooms[cmd[1]]; ok {
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: fmt.Sprintf("Room exists: %v", cmd)})
|
||||||
|
} else {
|
||||||
|
s.rooms[cmd[1]] = struct{}{}
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: fmt.Sprintf("Created room: %v", cmd)})
|
||||||
|
}
|
||||||
|
} else if cmd[0] == "rm" {
|
||||||
|
if _, ok := s.rooms[cmd[1]]; ok && cmd[1] != "general" {
|
||||||
|
delete(s.rooms, cmd[1])
|
||||||
|
s.rhist[cmd[1]] = []c.SMsg{}
|
||||||
|
tim := time.Now()
|
||||||
|
s.conns.sm.Lock()
|
||||||
|
for cn, r := range s.conns.cm {
|
||||||
|
if r.room == cmd[1] {
|
||||||
|
r.room = "general"
|
||||||
|
s.conns.cm[cn] = r
|
||||||
|
wsjson.Write(ctx, cn, c.SMsg{Tim: tim, Id: "system", Msg: "room deleted, reconnected to general"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.conns.sm.Unlock()
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: fmt.Sprintf("Deleted room: %v", cmd)})
|
||||||
|
} else {
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: fmt.Sprintf("Room does not exist: %v", cmd)})
|
||||||
|
}
|
||||||
|
} else if cmd[0] == "yeet" {
|
||||||
|
found := false
|
||||||
|
s.conns.sm.Lock()
|
||||||
|
for c, r := range s.conns.cm {
|
||||||
|
if r.nick == cmd[1] {
|
||||||
|
c.Close(ws.StatusNormalClosure, "Kicked")
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.conns.sm.Unlock()
|
||||||
|
if found {
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: fmt.Sprintf("Yeet: %v", cmd[1])})
|
||||||
|
} else {
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: fmt.Sprintf("Not found: %v", cmd[1])})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: fmt.Sprintf("Invalid command: %v", cmd)})
|
||||||
|
}
|
||||||
|
} else if cmd[0] == "wc" {
|
||||||
|
s.conns.sm.Lock()
|
||||||
|
wc := len(s.conns.cm)
|
||||||
|
s.conns.sm.Unlock()
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: fmt.Sprintf("Online: %v", wc)})
|
||||||
|
} else if cmd[0] == "man" {
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: "Available commands: man, mk, rm, wc, yeet"})
|
||||||
|
} else {
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: fmt.Sprintf("Invalid command: %v", cmd)})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: "Unrecognised command, use /man for more info"})
|
||||||
|
}
|
||||||
|
case c.Echo:
|
||||||
|
s.logFn("(%v) echo: %v", smsg.Id, cmsg.Msg)
|
||||||
|
s.conns.sm.Lock()
|
||||||
|
room := s.conns.cm[conn].room
|
||||||
|
s.conns.sm.Unlock()
|
||||||
|
smsg.Tim = time.Now()
|
||||||
|
smsg.Msg = cmsg.Msg
|
||||||
|
if len(s.rhist[room]) < s.rhlen {
|
||||||
|
s.rhist[room] = append(s.rhist[room], smsg)
|
||||||
|
} else {
|
||||||
|
s.rhist[room] = append(s.rhist[room][1:], smsg)
|
||||||
|
}
|
||||||
|
s.conns.sm.Lock()
|
||||||
|
for c, r := range s.conns.cm {
|
||||||
|
if r.room == room {
|
||||||
|
wsjson.Write(ctx, c, &smsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.conns.sm.Unlock()
|
||||||
|
case c.Mv:
|
||||||
|
switch nick, valid := verifyNick(&s, cmsg.Msg); valid {
|
||||||
|
case nickOk:
|
||||||
|
s.logFn("(%v) mv: %v", smsg.Id, cmsg.Msg)
|
||||||
|
smsg.Id = nick
|
||||||
|
s.conns.sm.Lock()
|
||||||
|
u := s.conns.cm[conn]
|
||||||
|
u.nick = smsg.Id
|
||||||
|
s.conns.cm[conn] = u
|
||||||
|
s.conns.sm.Unlock()
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: fmt.Sprintf("nick set: %v", nick)})
|
||||||
|
case nickUsed:
|
||||||
|
s.logFn("(%v) mv used: %v", smsg.Id, cmsg.Msg)
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: fmt.Sprintf("nick in use: %v", cmsg.Msg)})
|
||||||
|
case nickInvalid:
|
||||||
|
s.logFn("(%v) mv invalid: %v", smsg.Id, cmsg.Msg)
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: fmt.Sprintf("invalid nick: %v", cmsg.Msg)})
|
||||||
|
}
|
||||||
|
case c.Ls:
|
||||||
|
s.logFn("(%v) ls", smsg.Id)
|
||||||
|
s.conns.sm.Lock()
|
||||||
|
room := s.conns.cm[conn].room
|
||||||
|
s.conns.sm.Unlock()
|
||||||
|
avRooms := ""
|
||||||
|
for r := range s.rooms {
|
||||||
|
avRooms += r + ", "
|
||||||
|
}
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: fmt.Sprintf("connected to: %v, available: %v", room, avRooms[:len(avRooms)-2])})
|
||||||
|
case c.Cd:
|
||||||
|
if _, ok := s.rooms[cmsg.Msg]; ok {
|
||||||
|
s.logFn("(%v) cd: %v", smsg.Id, cmsg.Msg)
|
||||||
|
s.conns.sm.Lock()
|
||||||
|
u := s.conns.cm[conn]
|
||||||
|
u.room = cmsg.Msg
|
||||||
|
s.conns.cm[conn] = u
|
||||||
|
s.conns.sm.Unlock()
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: fmt.Sprintf("connected to: %v", u.room)})
|
||||||
|
rhistSize := max(len(s.rhist[u.room])-10, 0)
|
||||||
|
recentHistory := s.rhist[u.room][rhistSize:]
|
||||||
|
for i := range recentHistory {
|
||||||
|
wsjson.Write(ctx, conn, recentHistory[i])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.logFn("(%v) cd invalid: %v", smsg.Id, cmsg.Msg)
|
||||||
|
wsjson.Write(ctx, conn, c.SMsg{Tim: time.Now(), Id: "system", Msg: fmt.Sprintf("unchanged, invalid room: %v", cmsg.Msg)})
|
||||||
|
}
|
||||||
|
case c.Who:
|
||||||
|
s.conns.sm.Lock()
|
||||||
|
room := s.conns.cm[conn].room
|
||||||
|
s.logFn("(%v) who: %v", smsg.Id, room)
|
||||||
|
users := fmt.Sprintf("users in %v: ", room)
|
||||||
|
for _, r := range s.conns.cm {
|
||||||
|
if r.room == room {
|
||||||
|
users += fmt.Sprintf("%v, ", r.nick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.conns.sm.Unlock()
|
||||||
|
wsjson.Write(ctx, conn, &c.SMsg{Tim: time.Now(), Id: "system", Msg: users[:len(users)-2]})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(ctx, conn)
|
||||||
|
|
||||||
|
if ws.CloseStatus(err) == ws.StatusNormalClosure {
|
||||||
|
s.logFn("disconnected: %v", r.RemoteAddr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.logFn("failed, addr %v: %v", r.RemoteAddr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type nickErr int
|
||||||
|
|
||||||
|
const (
|
||||||
|
nickOk nickErr = iota
|
||||||
|
nickUsed
|
||||||
|
nickInvalid
|
||||||
|
)
|
||||||
|
|
||||||
|
func verifyNick(s *server, n string) (string, nickErr) {
|
||||||
|
nick, pass, _ := strings.Cut(n, ":")
|
||||||
|
|
||||||
|
s.conns.sm.Lock()
|
||||||
|
defer s.conns.sm.Unlock()
|
||||||
|
for _, u := range s.conns.cm {
|
||||||
|
if u.nick == nick {
|
||||||
|
return "", nickUsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expPass, needAuth := s.nickm[nick]
|
||||||
|
|
||||||
|
if (!needAuth || pass == expPass) && alphanumeric(nick) {
|
||||||
|
return nick, nickOk
|
||||||
|
} else {
|
||||||
|
return "", nickInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadNickMap(m *string) (map[string]string, error) {
|
||||||
|
nm := make(map[string]string)
|
||||||
|
if m == nil {
|
||||||
|
return nm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := filepath.Abs(*m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(file, &nm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func alphanumeric(s string) bool {
|
||||||
|
for _, r := range s {
|
||||||
|
if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && (r < '0' || r > '9') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
Loading…
Reference in a new issue