websocket base and command parser
This commit is contained in:
@@ -90,6 +90,14 @@ func (server *ApiServer) loadEndpoints() {
|
|||||||
c.AbortWithStatus(http.StatusBadRequest)
|
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 {
|
type ServerInfo struct {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"zomo.dev/largehadroncollider/api/ws"
|
||||||
"zomo.dev/largehadroncollider/db"
|
"zomo.dev/largehadroncollider/db"
|
||||||
"zomo.dev/largehadroncollider/ttv"
|
"zomo.dev/largehadroncollider/ttv"
|
||||||
"zomo.dev/largehadroncollider/util"
|
"zomo.dev/largehadroncollider/util"
|
||||||
@@ -9,8 +10,12 @@ import (
|
|||||||
|
|
||||||
func InitApiServer(conf *util.Config, dbConn *db.DBConn, twitchConn *ttv.TwitchConn) (*ApiServer, error) {
|
func InitApiServer(conf *util.Config, dbConn *db.DBConn, twitchConn *ttv.TwitchConn) (*ApiServer, error) {
|
||||||
engine := gin.Default()
|
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()
|
apiServer.loadEndpoints()
|
||||||
|
|
||||||
@@ -19,6 +24,7 @@ func InitApiServer(conf *util.Config, dbConn *db.DBConn, twitchConn *ttv.TwitchC
|
|||||||
|
|
||||||
type ApiServer struct {
|
type ApiServer struct {
|
||||||
engine *gin.Engine
|
engine *gin.Engine
|
||||||
|
ws *ws.WSServer
|
||||||
conf *util.Config
|
conf *util.Config
|
||||||
db *db.DBConn
|
db *db.DBConn
|
||||||
twitch *ttv.TwitchConn
|
twitch *ttv.TwitchConn
|
||||||
|
|||||||
234
api/ws/commands.go
Normal file
234
api/ws/commands.go
Normal 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
|
||||||
|
}
|
||||||
@@ -1,17 +1,68 @@
|
|||||||
package ws
|
package ws
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gobwas/ws"
|
||||||
|
"github.com/gobwas/ws/wsutil"
|
||||||
"zomo.dev/largehadroncollider/db"
|
"zomo.dev/largehadroncollider/db"
|
||||||
"zomo.dev/largehadroncollider/ttv"
|
"zomo.dev/largehadroncollider/ttv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitWSServer(dbConn db.DBConn, twitchConn ttv.TwitchConn) (WSServer, error) {
|
func InitWSServer(dbConn *db.DBConn, twitchConn *ttv.TwitchConn) (*WSServer, error) {
|
||||||
return WSServer{}, nil
|
return &WSServer{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type WSServer struct {
|
type WSServer struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wsServer *WSServer) Listen() {
|
type WSConn struct {
|
||||||
// start web server itself
|
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) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
17
test/main.go
17
test/main.go
@@ -1,12 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
now := time.Now()
|
// cmd, err := parseCommand("abc \\\"def ghi\"\" [abc][] {\"abc\":'def'}")
|
||||||
fmt.Println("Now:", now)
|
// if err != nil {
|
||||||
fmt.Println("Truncated:", now.Truncate(5*time.Minute).Add(5*time.Minute).Sub(now))
|
// 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)
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user