diff --git a/api/endpoints.go b/api/endpoints.go index cf53b3c..c42e7f9 100644 --- a/api/endpoints.go +++ b/api/endpoints.go @@ -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 { diff --git a/api/main.go b/api/main.go index df54eb8..8324eae 100644 --- a/api/main.go +++ b/api/main.go @@ -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 diff --git a/api/ws/commands.go b/api/ws/commands.go new file mode 100644 index 0000000..aa303ea --- /dev/null +++ b/api/ws/commands.go @@ -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 +} diff --git a/api/ws/main.go b/api/ws/main.go index b0e58af..9a60603 100644 --- a/api/ws/main.go +++ b/api/ws/main.go @@ -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) { + } diff --git a/test/main.go b/test/main.go index af5abb3..bac039e 100644 --- a/test/main.go +++ b/test/main.go @@ -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) + // } }