websocket base and command parser

This commit is contained in:
zomo
2026-01-06 23:43:14 -06:00
parent 81e8bca787
commit a6d932a560
5 changed files with 314 additions and 12 deletions

View File

@@ -90,6 +90,14 @@ func (server *ApiServer) loadEndpoints() {
c.AbortWithStatus(http.StatusBadRequest)
}
})
server.engine.Any("/ws", func(c *gin.Context) {
err := server.ws.Handle(c.Writer, c.Request)
if err != nil {
log.Printf("Error handing websocket connection from %s: %v", c.RemoteIP(), err)
c.AbortWithStatus(http.StatusInternalServerError)
}
})
}
type ServerInfo struct {

View File

@@ -2,6 +2,7 @@ package api
import (
"github.com/gin-gonic/gin"
"zomo.dev/largehadroncollider/api/ws"
"zomo.dev/largehadroncollider/db"
"zomo.dev/largehadroncollider/ttv"
"zomo.dev/largehadroncollider/util"
@@ -9,8 +10,12 @@ import (
func InitApiServer(conf *util.Config, dbConn *db.DBConn, twitchConn *ttv.TwitchConn) (*ApiServer, error) {
engine := gin.Default()
wsServer, err := ws.InitWSServer(dbConn, twitchConn)
if err != nil {
return nil, err
}
apiServer := &ApiServer{engine, conf, dbConn, twitchConn}
apiServer := &ApiServer{engine, wsServer, conf, dbConn, twitchConn}
apiServer.loadEndpoints()
@@ -19,6 +24,7 @@ func InitApiServer(conf *util.Config, dbConn *db.DBConn, twitchConn *ttv.TwitchC
type ApiServer struct {
engine *gin.Engine
ws *ws.WSServer
conf *util.Config
db *db.DBConn
twitch *ttv.TwitchConn

234
api/ws/commands.go Normal file
View File

@@ -0,0 +1,234 @@
package ws
import (
"errors"
"io"
"strings"
)
type Command struct {
Command string
Args []string
}
type parser struct {
cmd *Command
r *strings.Reader
arg string
args []string
}
type parserQuote rune
const (
parserQuoteSingle parserQuote = '\''
parserQuoteDouble parserQuote = '"'
)
type parserBracket rune
const (
parserBracketCurly parserBracket = '{'
parserBracketSquare parserBracket = '['
parserBracketTriangle parserBracket = '<'
)
var parserClosingBracket = map[parserBracket]rune{
parserBracketCurly: '}',
parserBracketSquare: ']',
parserBracketTriangle: '>',
}
func parseCommand(msg string) (*Command, error) {
cmd := &Command{}
r := strings.NewReader(msg)
parser := &parser{cmd: cmd, r: r}
return cmd, parser.parse()
}
func (p *parser) parse() error {
err := p.stateBase()
if err != nil {
return err
}
p.args = append(p.args, p.arg)
filteredArgs := []string{}
for _, arg := range p.args {
if arg != "" {
filteredArgs = append(filteredArgs, arg)
}
}
if len(p.args) >= 1 {
p.cmd.Command = p.args[0]
}
if len(p.args) >= 2 {
p.cmd.Args = p.args[1:]
}
return nil
}
func (p *parser) consume() (rune, bool, error) {
b := make([]byte, 1)
_, err := p.r.Read(b)
if err == io.EOF {
return 0, true, nil
}
if err != nil {
return 0, false, err
}
return rune(b[0]), false, nil
}
func (p *parser) push(r rune) {
p.arg += string(r)
}
func (p *parser) flush() {
p.args = append(p.args, p.arg)
p.arg = ""
}
func (p *parser) stateBase() error {
for {
ch, eof, err := p.consume()
if err != nil {
return err
}
if eof {
return nil
}
switch ch {
case '\\':
err := p.stateEscape()
if err != nil {
return err
}
case rune(parserQuoteDouble): fallthrough
case rune(parserQuoteSingle):
err := p.stateQuote(parserQuote(ch))
if err != nil {
return err
}
// special case, if the arg starts with a { then parse it with rough object mode
case rune(parserBracketCurly): fallthrough
case rune(parserBracketSquare): fallthrough
case rune(parserBracketTriangle):
if p.arg != "" {
p.push(ch)
} else {
err := p.stateBracket(parserBracket(ch))
if err != nil {
return err
}
}
case '\n': fallthrough
case '\t': fallthrough
case ' ':
p.flush()
default:
p.push(ch)
}
}
}
func (p *parser) stateQuote(quote parserQuote) error {
for {
ch, eof, err := p.consume()
if err != nil {
return err
}
if eof {
return errors.New("missing closing quote")
}
switch ch {
case '\\':
err := p.stateEscape()
if err != nil {
return err
}
case rune(quote):
return nil
default:
p.push(ch)
}
}
}
func (p *parser) stateBracket(bracket parserBracket) error {
p.push(rune(bracket))
for {
ch, eof, err := p.consume()
if err != nil {
return err
}
if eof {
return errors.New("missing closing bracket")
}
switch ch {
case '\\':
err := p.stateEscape()
if err != nil {
return err
}
case rune(parserQuoteDouble): fallthrough
case rune(parserQuoteSingle):
p.push(ch)
err := p.stateQuote(parserQuote(ch))
if err != nil {
return err
}
p.push(ch)
case rune(parserBracketCurly): fallthrough
case rune(parserBracketSquare): fallthrough
case rune(parserBracketTriangle):
err := p.stateBracket(parserBracket(ch))
if err != nil {
return err
}
case parserClosingBracket[bracket]:
p.push(ch)
return nil
default:
p.push(ch)
}
}
}
func (p *parser) stateEscape() error {
ch, eof, err := p.consume()
if err != nil {
return err
}
if eof {
return errors.New("invalid escape sequence before end of file")
}
switch ch {
case 'n':
p.arg += "\n"
case 't':
p.arg += "\t"
default:
p.push(ch)
}
return nil
}

View File

@@ -1,17 +1,68 @@
package ws
import (
"log"
"net/http"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
"zomo.dev/largehadroncollider/db"
"zomo.dev/largehadroncollider/ttv"
)
func InitWSServer(dbConn db.DBConn, twitchConn ttv.TwitchConn) (WSServer, error) {
return WSServer{}, nil
func InitWSServer(dbConn *db.DBConn, twitchConn *ttv.TwitchConn) (*WSServer, error) {
return &WSServer{}, nil
}
type WSServer struct {
}
func (wsServer *WSServer) Listen() {
// start web server itself
type WSConn struct {
events []string
}
func (wsServer *WSServer) Handle(w http.ResponseWriter, r *http.Request) error {
conn, _, _, err := ws.UpgradeHTTP(r, w)
if err != nil {
return err
}
authrorization := ""
go func() {
defer conn.Close()
for {
msg, _, err := wsutil.ReadClientData(conn)
if err != nil {
// TODO handle error better, print more conn info
log.Printf("Error reading data from ws connection %v", err)
continue
}
cmd, err := parseCommand(string(msg))
if err != nil {
log.Printf("Error parsing command data from ws connection %v", err)
continue
}
log.Printf("%+v", cmd)
if authrorization == "" {
// TODO authorize connection
continue
}
// TODO run with cmd
// TODO errors will be responded to the client
// if the response errors, log and exit the loop
}
}()
return nil
}
func runCommand(cmd *Command) {
}

View File

@@ -1,12 +1,15 @@
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
fmt.Println("Now:", now)
fmt.Println("Truncated:", now.Truncate(5*time.Minute).Add(5*time.Minute).Sub(now))
// cmd, err := parseCommand("abc \\\"def ghi\"\" [abc][] {\"abc\":'def'}")
// if err != nil {
// fmt.Printf("ERROR: %v\n", err)
// return
// }
// fmt.Printf("cmd: %s\n", cmd.Command)
// for _, arg := range cmd.Args {
// fmt.Printf("arg: %s\n", arg)
// }
}