package main import ( "context" "encoding/json" "fmt" "html/template" "io" "log" "math/rand" "net/http" "strings" glws "git.alfi.li/gamelang/frontend/webserver" glwss "git.alfi.li/gamelang/frontend/wsserver" bingoman "git.alfi.li/gamelang/games/bingo/bingoManager" gamelangpb "git.alfi.li/gamelang/protobuf/gamelang" glbingopb "git.alfi.li/gamelang/protobuf/gamelang-bingo" userman "git.alfi.li/gamelang/systems/usermanager" worldman "git.alfi.li/gamelang/systems/worldmanager" "github.com/labstack/echo/v4" "gopkg.in/gcfg.v1" ) // Template wraps go's html template type Template struct { templates *template.Template } // Render renders a Template func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { return t.templates.ExecuteTemplate(w, name, data) } func initTemplates(e *echo.Echo) { t := &Template{ templates: template.Must(template.ParseGlob("public/*.html")), } e.Renderer = t } type BingoConfig struct { Http struct { ListenIP string ListenPort string } WS struct { ListenIP string ListenPort string } Manager struct { UserManager string WorldManager string BingoManager string } } func getDefaultConfig() BingoConfig { bc := BingoConfig{} bc.Http.ListenIP = "127.0.0.1" bc.Http.ListenPort = "8080" bc.WS.ListenIP = "127.0.0.1" bc.WS.ListenPort = "8081" bc.Manager.UserManager = "localhost:9090" bc.Manager.WorldManager = "localhost:9091" bc.Manager.BingoManager = "localhost:9092" return bc } type BingoGame struct { um *userman.UserManager //gamelangpb.UserServiceClient wm *worldman.WorldManager //gamelangpb.WorldServiceClient bingomanager *bingoman.BingoManager webserver *glws.Webserver wssserver *glwss.WSServer notichan chan glbingopb.Event config BingoConfig } func (bg *BingoGame) Init(config BingoConfig) { //init userclient //userconn, err := grpc.Dial(config.Manager.UserManager, grpc.WithInsecure()) //if err != nil { // panic(err.Error()) //} //defer userconn.Close() bg.um = userman.NewUserManager([]string{}) //gamelangpb.NewUserServiceClient(userconn) //init wm //worldconn, err := grpc.Dial(config.Manager.WorldManager, grpc.WithInsecure()) //if err != nil { // panic(err.Error()) //} //defer worldconn.Close() bg.wm = worldman.NewWorldManager([]string{}) //gamelangpb.NewWorldServiceClient(worldconn) bg.notichan = make(chan glbingopb.Event) bg.bingomanager = bingoman.NewBingoManager([]string{}, bg.notichan) go bg.workNotifications() //init webserver wsConfig := glws.Config{} wsConfig.Http.ListenIP = config.Http.ListenIP wsConfig.Http.ListenPort = config.Http.ListenPort bg.webserver = glws.NewWebserver(wsConfig) } func (bg *BingoGame) workNotifications() { for { log.Print("waiting for notification") event := <-bg.notichan log.Println("new notification", event) eventJson, err := json.Marshal(event) if err != nil { log.Printf("workNotifications - err marshalling event \"%s\"\n%s", event, err.Error()) } notify := glwss.WSMsg{Mtype: "game:event", Payload: string(eventJson)} worldgamename := strings.Split(event.Gamename, "\uffff") worldname := worldgamename[0] gamename := worldgamename[1] // if the game is a common game or its a win event all players have to be notified if gamename == "common" || event.Type == "Win" { world := gamelangpb.World{} worlds := bg.wm.List() for _, world := range worlds { if world.GetName() == worldname { log.Println("sending world\n", world) break } } //world, err := bg.wm.GetWorld(context.Background(), &gamelangpb.World{Name: worldname}) if err != nil { log.Printf("workNotifications - err getting world msg \"%s\"\n%s", worldname, err.Error()) return } for _, user := range world.Users { go bg.wssserver.Write(user.Name, notify) } } else { bg.wssserver.Write(event.Username, notify) } } } // RenderEndpoints returns the endpoints that are served by the webserver func (bg *BingoGame) RenderEndpoints() []glws.Endpoint { endpoints := []glws.Endpoint{} endpoints = append(endpoints, glws.Endpoint{"GET", "/", func(ctx echo.Context) error { log.Print("/: get") ok, user := bg.webserver.CheckSess(ctx) if !ok { log.Print("/: not found") return ctx.Redirect(301, "/login") } data := struct { Username string Worlds []gamelangpb.World Items []string }{ Username: user.Name, Worlds: func() []gamelangpb.World { worlds := bg.wm.List() return worlds }(), Items: []string{ "user", "world", }, } log.Printf("/: user \"%v\" found", user.Name) return ctx.Render(http.StatusOK, "index", data) }}) endpoints = append(endpoints, glws.Endpoint{"GET", "/user", func(ctx echo.Context) error { log.Print("/user: get") log.Print("/user: sending register form") return ctx.Render(http.StatusOK, "registerform", "") }}) // POST to /user creates a user endpoints = append(endpoints, glws.Endpoint{"POST", "/user", func(ctx echo.Context) error { username := ctx.FormValue("username") password := ctx.FormValue("password") log.Printf("new user request: %v", username) newUser := gamelangpb.User{Name: username, Password: []byte(password)} id := rand.Uint32() name := newUser.GetName() _, err := bg.um.Create(id, name, password) //user, err := bg.userclient.CreateUser(context.Background(), &newUser) if err != nil { return ctx.String(501, "user creation failed") } bg.webserver.NewSession(newUser, ctx) log.Printf("new user created: %v", username) return ctx.Redirect(302, "/") }}) endpoints = append(endpoints, glws.Endpoint{"GET", "/login", func(ctx echo.Context) error { log.Print("/login: get") // check if the login page was called with failed parameter (set by /login POST) failedBefore := false if value := ctx.FormValue("failed"); value == "1" { failedBefore = true } ok, user := bg.webserver.CheckSess(ctx) if ok { // logged in log.Printf("/login: user %v logged in", user.Name) return ctx.String(http.StatusOK, "hello, "+user.Name+"\nyou are logged in.") } // not logged in log.Print("/login: not logged in") return ctx.Render(http.StatusOK, "loginform", failedBefore) }}) endpoints = append(endpoints, glws.Endpoint{"POST", "/login", func(ctx echo.Context) error { log.Print("/login: post") username := ctx.FormValue("username") password := ctx.FormValue("password") // if the session is already set, the user is already logged in ok, _ := bg.webserver.CheckSess(ctx) if ok { return ctx.String(http.StatusOK, "you are already logged in.") } log.Printf("user login attempt: %v", username) //checkuser := gamelangpb.User{Name: username, Password: []byte(password)} user, ok := bg.um.Check(username, password) //bg.userclient.CheckUser(context.Background(), &checkuser) if ok { //err == nil { bg.webserver.NewSession(user, ctx) log.Printf("user login suceeded: %v", username) return ctx.String(http.StatusOK, "hello, "+username+"\nyou are now logged in.") } log.Printf("user login failed: %v\n%s", username) return ctx.Redirect(302, "/login?failed=1") }}) endpoints = append(endpoints, glws.Endpoint{"GET", "/admin", func(ctx echo.Context) error { log.Print("/admin: get") ok, user := bg.webserver.CheckSess(ctx) if !ok { log.Print("/admin: not logged in") return ctx.String(http.StatusUnauthorized, "not logged in") } log.Println("/admin:", user.Name) if user.Admin != true && user.Name != "admin" { log.Println("/admin: user is not an admin", user.Name) return ctx.String(http.StatusUnauthorized, "you are not an admin") } data := struct { Users []gamelangpb.User Sessions map[string]gamelangpb.User Worlds []gamelangpb.World }{ Users: func() []gamelangpb.User { users := bg.um.List() return users }(), Sessions: bg.webserver.ListSess(), Worlds: func() []gamelangpb.World { worlds := bg.wm.List() return worlds }(), } err := ctx.Render(http.StatusOK, "admin", data) if err != nil { log.Println(err) } return err }}) // worldbuilder // Get to /World returns register form endpoints = append(endpoints, glws.Endpoint{"GET", "/world", func(ctx echo.Context) error { ok, _ := bg.webserver.CheckSess(ctx) if !ok { log.Print("/World: not logged in") return ctx.String(http.StatusUnauthorized, "not logged in") } data := struct { Worlds []gamelangpb.World }{ Worlds: func() []gamelangpb.World { worlds := bg.wm.List() return worlds }(), } err := ctx.Render(http.StatusOK, "worldlobby", data) if err != nil { log.Print(err) } return err }}) endpoints = append(endpoints, glws.Endpoint{"POST", "/world", func(ctx echo.Context) error { ok, _ := bg.webserver.CheckSess(ctx) if !ok { log.Print("/World: not logged in") return ctx.String(http.StatusUnauthorized, "not logged in") } worldname := ctx.FormValue("worldname") world := &gamelangpb.World{Name: worldname} lobbygame := CraftLobbygame(fmt.Sprintf("%s\uffff%s", world.GetName(), "lobby")) world.Games = []string{lobbygame.Name} log.Println("/World: creating world_\n", world) id := rand.Uint32() world.Id = id _, err := bg.wm.Create(*world) if err != nil { log.Println("error creating world", err) return ctx.String(http.StatusInternalServerError, "error creating world") } err = bg.bingomanager.Create(lobbygame) if err != nil { log.Println("error creating world", err) return ctx.String(http.StatusInternalServerError, "error creating world") } return ctx.Redirect(http.StatusFound, "/world") }}) endpoints = append(endpoints, glws.Endpoint{"POST", "/world/join", func(ctx echo.Context) error { log.Print("join attemt") ok, user := bg.webserver.CheckSess(ctx) if !ok { log.Print("/World/join: not logged in") return ctx.String(http.StatusUnauthorized, "not logged in") } worldname := ctx.FormValue("worldname") log.Println("joining", user.Name, "to", worldname) joinworld := gamelangpb.World{Name: worldname, Users: []*gamelangpb.User{}} joinworld.Users = append(joinworld.Users, &user) err := bg.wm.Join(worldname, &user) if err != nil { log.Printf("error joining %s to %s\n%s", user.Name, worldname, err.Error()) return ctx.String(501, "join failed") } log.Println("join of", user.Name, "to", worldname, "complete") return ctx.Redirect(302, "/world") }}) return endpoints } //RenderHandler renders the endpoints that will be served via websocket func (bg *BingoGame) RenderHandler() []glwss.Handler { handler := []glwss.Handler{} handler = append(handler, glwss.Handler{MType: "echo", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) { log.Printf("echo from \"%v\" with \"%s\"", user, msg.Payload) remsg := glwss.WSMsg{Mtype: "echo", Payload: msg.Payload} return remsg, nil }}) handler = append(handler, glwss.Handler{MType: "world:create", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) { createWorld := gamelangpb.World{Name: msg.Payload} lobbygame := CraftLobbygame(fmt.Sprintf("%s\uffff%s", createWorld.GetName(), "lobby")) createWorld.Games = []string{lobbygame.Name} id := rand.Uint32() createWorld.Id = id _, err := bg.wm.Create(*&createWorld) if err != nil { remsg := glwss.WSMsg{Mtype: "world:create", Payload: "failed"} return remsg, err } err = bg.bingomanager.Create(lobbygame) if err != nil { remsg := glwss.WSMsg{Mtype: "world:create", Payload: "failed"} return remsg, err } log.Println("world:create created Lobbygame:\n", lobbygame) createworldJSON, _ := json.Marshal(createWorld) remsg := glwss.WSMsg{Mtype: "world:create", Payload: string(createworldJSON)} return remsg, nil }}) handler = append(handler, glwss.Handler{MType: "world:remove", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) { err := bg.wm.Remove(msg.Payload) if err != nil { remsg := glwss.WSMsg{Mtype: "world:remove", Payload: "failed"} return remsg, err } remsg := glwss.WSMsg{Mtype: "world:remove", Payload: "success"} return remsg, nil }}) handler = append(handler, glwss.Handler{MType: "world:join", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) { worldname := msg.Payload err := bg.wm.Join(worldname, &user) if err != nil { remsg := glwss.WSMsg{Mtype: "world:join", Payload: "failed"} return remsg, err } remsg := glwss.WSMsg{Mtype: "world:join", Payload: "success"} return remsg, nil }}) handler = append(handler, glwss.Handler{MType: "world:list", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) { log.Print("world:list") worldlist := bg.wm.List() worldlistJSON, err := json.Marshal(worldlist) if err != nil { log.Print(err.Error()) msg := glwss.WSMsg{Mtype: "world:list", Payload: "failed"} return msg, err } log.Print("world:list", "sending list") remsg := glwss.WSMsg{Mtype: "world:list", Payload: string(worldlistJSON)} return remsg, nil }}) handler = append(handler, glwss.Handler{MType: "game:get", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) { log.Println("game:get new get request:\n\"", msg.Payload, "\"") var world gamelangpb.World worldname := msg.Payload worlds := bg.wm.List() for _, world = range worlds { if world.GetName() == worldname { log.Println("sending world\n", world) break } } log.Println("game:get got world:\n\"", world, "\"") lobbygame := glbingopb.Bingo{} commongame := glbingopb.Bingo{} usergame := glbingopb.Bingo{} for _, game := range world.GetGames() { log.Println("searching games", game) if game == fmt.Sprintf("%s\uffff%s", world.GetName(), "lobby") { log.Println("found lobbygame", game) searchGame := glbingopb.Bingo{Name: game} lobbygame = bg.bingomanager.Get(searchGame) } if game == fmt.Sprintf("%s\uffff%s", world.GetName(), "common") { log.Println("found commongame", game) searchGame := glbingopb.Bingo{Name: game} commongame = bg.bingomanager.Get(searchGame) } if user.Name == strings.Split(game, "\uffff")[1] { log.Println("found usergame", game) searchGame := glbingopb.Bingo{Name: game} usergame = bg.bingomanager.Get(searchGame) break } } returngame := glbingopb.Bingo{} if usergame.Name == fmt.Sprintf("%s\uffff%s", world.GetName(), user.Name) { log.Println("sending usergame", usergame.Name) returngame = usergame } else if commongame.Name == fmt.Sprintf("%s\uffff%s", world.GetName(), "common") { log.Println("sending commongame", commongame.Name) returngame = commongame } else if lobbygame.Name == fmt.Sprintf("%s\uffff%s", world.GetName(), "lobby") { log.Println("sending lobbygame", lobbygame.Name) returngame = lobbygame } else { // if get returned empty game returngame = CraftLobbygame(fmt.Sprintf("%s\uffff%s", world.GetName(), "lobby")) bg.bingomanager.Create(returngame) log.Print("could not find lobbygame, so created one") } gameJSON, err := json.Marshal(returngame) if err != nil { remsg := glwss.WSMsg{Mtype: "game:get", Payload: "failed"} return remsg, err } log.Println("game:get sending game:\n", string(gameJSON)) remsg := glwss.WSMsg{Mtype: "game:get", Payload: string(gameJSON)} return remsg, nil }}) handler = append(handler, glwss.Handler{MType: "game:start", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) { log.Println("game:start new start request:\n", msg.Payload) startgame := glbingopb.Bingo{} err := json.Unmarshal([]byte(msg.Payload), &startgame) if err != nil { log.Printf("game:start - err unmarshalling startgame \"%s\"\n%s", msg, err.Error()) remsg := glwss.WSMsg{Mtype: "game:start", Payload: "failed"} return remsg, err } searchworld := gamelangpb.World{Name: strings.Split(startgame.Name, "\uffff")[0]} startworld, err := bg.wm.GetWorld(context.Background(), &searchworld) if err != nil { log.Printf("game:start - err unmarshalling startworld \"%s\"\n%s", msg, err.Error()) remsg := glwss.WSMsg{Mtype: "game:start", Payload: "failed"} return remsg, err } // start game for _, option := range startgame.Options { if option.Key == "common game" && option.Bool { newGame := bg.bingomanager.CraftGame(rand.Uint32(), fmt.Sprintf("%s\uffffcommon", startworld.Name), startgame.Textlist, startgame.Numrows, startgame.Numcols, startgame.Options...) bg.bingomanager.Create(newGame) addworld := gamelangpb.World{Name: startworld.Name, Games: []string{newGame.Name}} bg.wm.AddGameWorld(context.Background(), &addworld) jsonGame, err := json.Marshal(newGame) if err != nil { log.Printf("write: err marshalling game \"%s\"\n%s", msg, err.Error()) } remsg := glwss.WSMsg{Mtype: "game:get", Payload: string(jsonGame)} if err != nil { log.Printf("write: err marshalling msg \"%s\"\n%s", msg, err.Error()) } for _, user := range startworld.Users { go bg.wssserver.Write(user.Name, remsg) } } } remsg := glwss.WSMsg{Mtype: "game:start", Payload: "sucess"} return remsg, nil }}) handler = append(handler, glwss.Handler{MType: "game:mod", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) { modgame := &glbingopb.Bingo{} err := json.Unmarshal([]byte(msg.Payload), modgame) if err != nil { log.Printf("game:mod - err unmarshalling msg \"%s\"\n%s", msg, err.Error()) remsg := glwss.WSMsg{Mtype: "game:mod", Payload: "failed"} return remsg, nil } bg.bingomanager.Modify(*modgame) searchworld := gamelangpb.World{Name: strings.Split(modgame.Name, "\uffff")[0]} world, err := bg.wm.GetWorld(context.Background(), &searchworld) if err != nil { log.Printf("game:mod - err getting world msg \"%s\"\n%s", msg, err.Error()) remsg := glwss.WSMsg{Mtype: "game:mod", Payload: "failed"} return remsg, nil } notify := glwss.WSMsg{Mtype: "game:get", Payload: msg.Payload} for _, user := range world.Users { go bg.wssserver.Write(user.Name, notify) } bg.bingomanager.CheckWin() remsg := glwss.WSMsg{Mtype: "game:mod", Payload: "success"} return remsg, nil }}) handler = append(handler, glwss.Handler{MType: "game:event", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) { newevent := &glbingopb.Event{} err := json.Unmarshal([]byte(msg.Payload), newevent) if err != nil { log.Printf("game:event - err unmarshalling msg \"%s\"\n%s", msg, err.Error()) remsg := glwss.WSMsg{Mtype: "game:event", Payload: "failed"} return remsg, nil } newevent.Username = user.Name log.Println("game:event new event", newevent) worldgamename := strings.Split(newevent.Gamename, "\uffff") worldname := worldgamename[0] gamename := worldgamename[1] // if the game is not a common game add all games from that world to neighborgames if gamename != "common" { world, err := bg.wm.GetWorld(context.Background(), &gamelangpb.World{Name: worldname}) if err != nil { log.Printf("workNotifications - err getting world msg \"%s\"\n%s", worldname, err.Error()) remsg := glwss.WSMsg{Mtype: "game:mod", Payload: "failed"} return remsg, nil } for _, game := range world.Games { if strings.Split(game, "\uffff")[1] != "lobby" { newevent.Neighborgames = append(newevent.Neighborgames, game) } } } err = bg.bingomanager.HandleEvent(*newevent) if err != nil { if err.Error() == "game finished" { reevent := glbingopb.Event{Type: "Win", Gamename: newevent.Gamename} eventJson, err := json.Marshal(reevent) if err != nil { log.Printf("workNotifications - err marshalling event \"%s\"\n%s", reevent, err.Error()) } notify := glwss.WSMsg{Mtype: "game:event", Payload: string(eventJson)} return notify, nil } return glwss.WSMsg{Mtype: "game:event", Payload: "failed"}, err } remsg := glwss.WSMsg{Mtype: "game:event", Payload: "success"} bg.bingomanager.CheckWin() return remsg, nil }}) return handler } func CraftLobbygame(name string) glbingopb.Bingo { lobbygame := glbingopb.Bingo{Name: name} lobbygame.Options = append(lobbygame.Options, &glbingopb.Option{Key: "unique fields", Bool: true, Type: 1}) lobbygame.Options = append(lobbygame.Options, &glbingopb.Option{Key: "common game", Bool: true, Type: 1}) lobbygame.Numcols = 5 lobbygame.Numrows = 5 lobbygame.Textlist = []string{"word"} return lobbygame } func main() { var config BingoConfig err := gcfg.ReadFileInto(&config, "config") if err != nil { if strings.Count(err.Error(), "no such file or directory") > 0 { config = getDefaultConfig() } else { log.Fatal(err) } } log.Print("config read") log.Printf("%+v", config) bingogame := BingoGame{} bingogame.Init(config) bingogame.webserver.InitEndpoints(bingogame.RenderEndpoints()) go bingogame.webserver.Run() bingogame.wssserver = glwss.NewWSServer([]string{}, bingogame.userclient) handler := bingogame.RenderHandler() keys := "" for _, k := range handler { keys = fmt.Sprintf("%s\n%v", keys, k) } log.Println("available MTypes:\n", keys, "\n----") bingogame.wssserver.RegisterHandler(handler) http.HandleFunc("/ws", bingogame.wssserver.WebsocketHandler) log.Println("websocket listening on", fmt.Sprintf("%s:%s", config.WS.ListenIP, config.WS.ListenPort)) log.Print(http.ListenAndServe(fmt.Sprintf("%s:%s", config.WS.ListenIP, config.WS.ListenPort), nil).Error()) }