Initial commit
This commit is contained in:
96
cmd/web/handlers.go
Normal file
96
cmd/web/handlers.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"vkane.cz/tinyquiz/pkg/model"
|
||||
"vkane.cz/tinyquiz/pkg/model/ent"
|
||||
)
|
||||
|
||||
func (app *application) home(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
type homeData struct {
|
||||
Stats model.Stats
|
||||
templateData
|
||||
}
|
||||
td := &homeData{}
|
||||
setDefaultTemplateData(&td.templateData)
|
||||
|
||||
if stats, err := app.model.GetStats(r.Context()); err == nil {
|
||||
td.Stats = stats
|
||||
} else {
|
||||
app.serverError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
app.render(w, r, "home.page.tmpl.html", td)
|
||||
}
|
||||
|
||||
func (app *application) play(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
app.clientError(w, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var code = strings.TrimSpace(params.ByName("code"))
|
||||
var player = strings.ToLower(strings.TrimSpace(r.PostForm.Get("player")))
|
||||
|
||||
if len(player) < 1 {
|
||||
app.clientError(w, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if player, err := app.model.RegisterPlayer(player, code, r.Context()); err == nil {
|
||||
if session, err := player.Unwrap().QuerySession().Only(r.Context()); err == nil {
|
||||
if su, err := app.model.GetPlayersStateUpdate(session.ID, r.Context()); err == nil {
|
||||
app.rtClients.SendToAll(session.ID, su)
|
||||
} else {
|
||||
app.serverError(w, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
app.serverError(w, err)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/game/" + url.PathEscape(player.ID.String()), http.StatusSeeOther)
|
||||
return
|
||||
} else if errors.Is(err, model.NoSuchEntity) {
|
||||
app.clientError(w, http.StatusNotFound)
|
||||
return
|
||||
} else if errors.Is(err, model.ConstraintViolation) {
|
||||
app.clientError(w, http.StatusForbidden)
|
||||
return
|
||||
} else {
|
||||
app.serverError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (app *application) game(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
var playerUid uuid.UUID
|
||||
if uid, err := uuid.Parse(params.ByName("playerUid")); err == nil {
|
||||
playerUid = uid
|
||||
} else {
|
||||
app.clientError(w, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if player, err := app.model.GetPlayerWithSessionAndGame(playerUid, r.Context()); err == nil {
|
||||
type lobbyData struct {
|
||||
P *ent.Player
|
||||
templateData
|
||||
}
|
||||
td := &lobbyData{}
|
||||
setDefaultTemplateData(&td.templateData)
|
||||
td.P = player
|
||||
|
||||
app.render(w, r, "game.page.tmpl.html", td)
|
||||
} else {
|
||||
app.clientError(w, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
171
cmd/web/helpers.go
Normal file
171
cmd/web/helpers.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"html/template"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
"vkane.cz/tinyquiz/pkg/model"
|
||||
"vkane.cz/tinyquiz/pkg/model/ent"
|
||||
"vkane.cz/tinyquiz/pkg/rtcomm"
|
||||
)
|
||||
|
||||
func newTemplateCache(dir string) (map[string]*template.Template, error) {
|
||||
cache := map[string]*template.Template{}
|
||||
|
||||
pages, err := filepath.Glob(filepath.Join(dir, "*.page.tmpl.html"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, page := range pages {
|
||||
name := filepath.Base(page)
|
||||
|
||||
ts, err := template.ParseFiles(page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if layouts, err := filepath.Glob(filepath.Join(dir, "*.layout.tmpl.html")); err == nil && len(layouts) > 0 {
|
||||
fmt.Println(layouts)
|
||||
fmt.Println(filepath.Rel(dir, layouts[0]))
|
||||
ts, err = ts.ParseFiles(layouts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if partials, err := filepath.Glob(filepath.Join(dir, "*.partial.tmpl.html")); err == nil && len(partials) > 0 {
|
||||
ts, err = ts.ParseFiles(partials...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cache[name] = ts
|
||||
}
|
||||
|
||||
return cache, nil
|
||||
}
|
||||
|
||||
func (app *application) render(w http.ResponseWriter, r *http.Request, name string, td interface{}) {
|
||||
ts, ok := app.templateCache[name]
|
||||
if !ok {
|
||||
app.serverError(w, fmt.Errorf("The template %s does not exist", name))
|
||||
return
|
||||
}
|
||||
|
||||
err := ts.Execute(w, td)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) serverError(w http.ResponseWriter, err error) {
|
||||
trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack())
|
||||
app.errorLog.Output(2, trace)
|
||||
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func (app *application) clientError(w http.ResponseWriter, status int) {
|
||||
app.errorLog.Output(2, string(debug.Stack()))
|
||||
|
||||
http.Error(w, http.StatusText(status), status)
|
||||
}
|
||||
|
||||
func (app *application) notFound(w http.ResponseWriter) {
|
||||
app.clientError(w, http.StatusNotFound)
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1, // we will not be reading from the socket
|
||||
EnableCompression: true,
|
||||
}
|
||||
const suBufferSize = 100
|
||||
const writeDeadline = time.Second * 10
|
||||
const socketGCPeriod = writeDeadline
|
||||
|
||||
// TODO utilize request context
|
||||
func (app *application) processWebSocket(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||
var playerUid uuid.UUID
|
||||
if uid, err := uuid.Parse(params.ByName("playerUid")); err == nil {
|
||||
playerUid = uid
|
||||
} else {
|
||||
app.clientError(w, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var player *ent.Player
|
||||
if p, err := app.model.GetPlayerWithSessionAndGame(playerUid, r.Context()); err == nil {
|
||||
player = p
|
||||
} else if errors.Is(err, model.NoSuchEntity) {
|
||||
app.clientError(w, http.StatusNotFound)
|
||||
return
|
||||
} else {
|
||||
app.serverError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if c, err := upgrader.Upgrade(w, r, nil); err != nil {
|
||||
app.clientError(w, http.StatusBadRequest)
|
||||
return
|
||||
} else {
|
||||
defer c.Close()
|
||||
if tcp, ok := c.UnderlyingConn().(*net.TCPConn); ok {
|
||||
tcp.SetKeepAlivePeriod(writeDeadline)
|
||||
tcp.SetKeepAlive(true)
|
||||
} else {
|
||||
app.infoLog.Printf("could not set keepalive for %s\n", playerUid)
|
||||
}
|
||||
var ch = make(chan rtcomm.StateUpdate, suBufferSize)
|
||||
app.rtClients.AddClient(player.Edges.Session.ID, ch)
|
||||
defer app.rtClients.RemoveClient(player.Edges.Session.ID, ch)
|
||||
if su, err := app.model.GetFullStateUpdate(player.Edges.Session.ID, r.Context()); err == nil {
|
||||
select {
|
||||
case ch <- su:
|
||||
break
|
||||
default:
|
||||
app.infoLog.Printf("could not send initial StateUpdate to %s\n", playerUid.String())
|
||||
}
|
||||
} else {
|
||||
app.errorLog.Printf("failed getting initial StateUpdate for %s with %v\n", playerUid.String(), err)
|
||||
}
|
||||
var gcTicker = time.Tick(socketGCPeriod)
|
||||
var devnull [0]byte
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <- gcTicker:
|
||||
c.UnderlyingConn().SetWriteDeadline(time.Time{})
|
||||
if _, err := c.UnderlyingConn().Write(devnull[:]); err != nil {
|
||||
app.infoLog.Printf("closing broken (%v) connection of %s\n", err, playerUid.String())
|
||||
break loop
|
||||
}
|
||||
case su := <- ch:
|
||||
if err := c.SetWriteDeadline(time.Now().Add(writeDeadline)); err != nil {
|
||||
app.errorLog.Printf("setting write deadline for %s failed with %v\n", playerUid.String(), err)
|
||||
break loop
|
||||
}
|
||||
if err := c.WriteJSON(su); errors.Is(err, io.EOF) {
|
||||
break loop
|
||||
} else if err != nil {
|
||||
app.infoLog.Printf("sending message for %s failed with %v\n", playerUid.String(), err)
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
84
cmd/web/main.go
Normal file
84
cmd/web/main.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
"vkane.cz/tinyquiz/pkg/model"
|
||||
"vkane.cz/tinyquiz/pkg/model/ent"
|
||||
rtcomm "vkane.cz/tinyquiz/pkg/rtcomm"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type application struct {
|
||||
errorLog *log.Logger
|
||||
infoLog *log.Logger
|
||||
templateCache map[string]*template.Template
|
||||
model *model.Model
|
||||
rtClients *rtcomm.Clients
|
||||
}
|
||||
|
||||
type templateData struct {
|
||||
}
|
||||
|
||||
func setDefaultTemplateData(td *templateData) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
|
||||
errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
|
||||
|
||||
var app = application{
|
||||
errorLog: errorLog,
|
||||
infoLog: infoLog,
|
||||
rtClients: rtcomm.NewClients(),
|
||||
}
|
||||
|
||||
if tc, err := newTemplateCache("./ui/html/"); err == nil {
|
||||
app.templateCache = tc
|
||||
} else {
|
||||
errorLog.Fatal(err)
|
||||
}
|
||||
|
||||
if c, err := ent.Open("postgres", "host='127.0.0.1' sslmode=disable dbname=tinyquiz"); err == nil {
|
||||
if err := c.Schema.Create(context.Background()); err != nil {
|
||||
errorLog.Fatal(err)
|
||||
}
|
||||
app.model = model.NewModel(c)
|
||||
} else {
|
||||
errorLog.Fatal(err)
|
||||
}
|
||||
|
||||
//TODO remove debug print
|
||||
go func() {
|
||||
for range time.Tick(2*time.Second){
|
||||
sessions, clients := app.rtClients.Count()
|
||||
fmt.Printf("There are %d sessions with total of %d clients\n", sessions, clients)
|
||||
}
|
||||
}()
|
||||
|
||||
mux := httprouter.New()
|
||||
mux.GET("/", app.home)
|
||||
mux.POST("/play/:code", app.play)
|
||||
mux.POST("/organise/:code", app.play)
|
||||
mux.GET("/game/:playerUid", app.game)
|
||||
|
||||
mux.GET("/ws/:playerUid", app.processWebSocket)
|
||||
|
||||
mux.ServeFiles("/static/*filepath", http.Dir("./ui/static/"))
|
||||
|
||||
var srv = &http.Server{
|
||||
Addr: "127.0.0.1:8080",
|
||||
ErrorLog: errorLog,
|
||||
Handler: mux,
|
||||
}
|
||||
log.Println("Starting server on :8080")
|
||||
err := srv.ListenAndServe()
|
||||
log.Fatal(err)
|
||||
}
|
||||
Reference in New Issue
Block a user