Initial commit

This commit is contained in:
Vojtěch Káně
2020-12-03 23:07:44 +01:00
commit 28e22e3422
24 changed files with 1526 additions and 0 deletions

96
cmd/web/handlers.go Normal file
View 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
View 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
View 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)
}