Implement session creation
This commit is contained in:
@@ -71,6 +71,38 @@ func (app *application) play(w http.ResponseWriter, r *http.Request, params http
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *application) createSession(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(r.PostForm.Get("code"))
|
||||||
|
var player = strings.ToLower(strings.TrimSpace(r.PostForm.Get("organiser")))
|
||||||
|
|
||||||
|
if len(player) < 1 {
|
||||||
|
app.clientError(w, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s, p, err := app.model.CreateSession(player, code, time.Now(), r.Context()); err == nil {
|
||||||
|
if su, err := app.model.GetPlayersStateUpdate(s.ID, r.Context()); err == nil {
|
||||||
|
app.rtClients.SendToAll(s.ID, su)
|
||||||
|
http.Redirect(w, r, "/game/"+url.PathEscape(p.ID.String()), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
app.serverError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if errors.Is(err, model.NoSuchEntity) {
|
||||||
|
app.clientError(w, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
app.serverError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (app *application) game(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
func (app *application) game(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
var playerUid uuid.UUID
|
var playerUid uuid.UUID
|
||||||
if uid, err := uuid.Parse(params.ByName("playerUid")); err == nil {
|
if uid, err := uuid.Parse(params.ByName("playerUid")); err == nil {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func main() {
|
|||||||
mux := httprouter.New()
|
mux := httprouter.New()
|
||||||
mux.GET("/", app.home)
|
mux.GET("/", app.home)
|
||||||
mux.POST("/play/:code", app.play)
|
mux.POST("/play/:code", app.play)
|
||||||
mux.POST("/organise/:code", app.play)
|
mux.POST("/session", app.createSession)
|
||||||
mux.GET("/game/:playerUid", app.game)
|
mux.GET("/game/:playerUid", app.game)
|
||||||
mux.POST("/game/:playerUid/rpc/next", app.nextQuestion)
|
mux.POST("/game/:playerUid/rpc/next", app.nextQuestion)
|
||||||
mux.POST("/game/:playerUid/answers/:choiceUid", app.answer)
|
mux.POST("/game/:playerUid/answers/:choiceUid", app.answer)
|
||||||
|
|||||||
67
pkg/codeGenerator/codeGenerator.go
Normal file
67
pkg/codeGenerator/codeGenerator.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package codeGenerator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
// some symbols are missing to prevent ambiguities
|
||||||
|
var alphabet = [...]byte{
|
||||||
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||||
|
'1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a code, that can be guessed after 256^randomLength attempts on average.
|
||||||
|
// randomLength can be at most 8 (= 64b of randomness)
|
||||||
|
// See GenerateCode for more details
|
||||||
|
func GenerateRandomCode(incremental uint64, randomLength uint8) ([]byte, error) {
|
||||||
|
var buf [8]byte
|
||||||
|
if _, err := rand.Read(buf[:randomLength]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var random uint64
|
||||||
|
for i := uint8(0); i < randomLength; i++ {
|
||||||
|
random |= uint64(buf[i]) << i * 8
|
||||||
|
}
|
||||||
|
return GenerateCode(incremental, random), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a code (a textual identifier) that is compact and easy for humans to deal with.
|
||||||
|
// It exists for all possible values of the arguments, but the smaller they are, the shorter the code.
|
||||||
|
// The code is guaranteed to be unique, if at least one of the arguments is unique.
|
||||||
|
//
|
||||||
|
// The typical usage is to use easy to predict yet easy to make unique value for incremental
|
||||||
|
// (to ensure uniqueness) and a random value for random to make the code unpredictable.
|
||||||
|
//
|
||||||
|
// Beware the code is not guaranteed to be parsable back to incremental and random arguments.
|
||||||
|
func GenerateCode(incremental uint64, random uint64) []byte {
|
||||||
|
var incrementalNumerals = countNumerals(incremental)
|
||||||
|
var randomNumerals = countNumerals(random)
|
||||||
|
|
||||||
|
var res = make([]byte, incrementalNumerals+randomNumerals)
|
||||||
|
genCode(random, res)
|
||||||
|
genCode(incremental, res[:incrementalNumerals])
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count the bytes needed to stored encoded x
|
||||||
|
func countNumerals(x uint64) (numerals uint64) {
|
||||||
|
for i := x; i > 0; i /= uint64(len(alphabet)) {
|
||||||
|
numerals++
|
||||||
|
}
|
||||||
|
if numerals == 0 {
|
||||||
|
numerals = 1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write encoded x to res from its end
|
||||||
|
func genCode(x uint64, res []byte) {
|
||||||
|
if x == 0 {
|
||||||
|
res[len(res)-1] = alphabet[0]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := len(res) - 1; x > 0 && i >= 0; i-- {
|
||||||
|
res[i] = alphabet[x%uint64(len(alphabet))]
|
||||||
|
x /= uint64(len(alphabet))
|
||||||
|
}
|
||||||
|
}
|
||||||
20
pkg/codeGenerator/codeGenerator_test.go
Normal file
20
pkg/codeGenerator/codeGenerator_test.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package codeGenerator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateCode(t *testing.T) {
|
||||||
|
test := func(incremental uint64, random uint64, expected []byte) {
|
||||||
|
if actual := GenerateCode(incremental, random); !bytes.Equal(actual, expected) {
|
||||||
|
t.Errorf("GenerateCode(%d, %d) returned %#v while %#v was expected", incremental, random, actual, expected)
|
||||||
|
} else if len(actual) != cap(actual) {
|
||||||
|
t.Errorf("GenerateCode(%d, %d) returned slice with capacity %d, while its length is %d. Potential memory waste", incremental, random, cap(actual), len(actual))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test(0, 0, []byte{'A', 'A'})
|
||||||
|
test(32, 31, []byte{'B', 'A', '9'})
|
||||||
|
const maxUint64 = ^uint64(0)
|
||||||
|
test(maxUint64, maxUint64, []byte{'S', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', 'S', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9'})
|
||||||
|
}
|
||||||
10
pkg/model/ent/schema/codesSequence.go
Normal file
10
pkg/model/ent/schema/codesSequence.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"entgo.io/ent"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provides a DBMS independent way of obtaining unique values
|
||||||
|
type CodesSequence struct {
|
||||||
|
ent.Schema
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ func (Game) Fields() []ent.Field {
|
|||||||
field.Text("name").MaxLen(64),
|
field.Text("name").MaxLen(64),
|
||||||
field.Time("created").Immutable(),
|
field.Time("created").Immutable(),
|
||||||
field.Text("author").MaxLen(64),
|
field.Text("author").MaxLen(64),
|
||||||
|
field.Text("code").MinLen(1).Unique(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func (Session) Fields() []ent.Field {
|
|||||||
field.UUID("id", uuid.Nil).Immutable(),
|
field.UUID("id", uuid.Nil).Immutable(),
|
||||||
field.Time("created").Immutable(),
|
field.Time("created").Immutable(),
|
||||||
field.Time("started").Nillable().Optional(),
|
field.Time("started").Nillable().Optional(),
|
||||||
field.String("code").MinLen(6).MaxLen(6).Immutable().Unique(),
|
field.String("code").MinLen(1).Immutable().Unique(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"time"
|
"time"
|
||||||
|
"vkane.cz/tinyquiz/pkg/codeGenerator"
|
||||||
"vkane.cz/tinyquiz/pkg/model/ent"
|
"vkane.cz/tinyquiz/pkg/model/ent"
|
||||||
"vkane.cz/tinyquiz/pkg/model/ent/answer"
|
"vkane.cz/tinyquiz/pkg/model/ent/answer"
|
||||||
"vkane.cz/tinyquiz/pkg/model/ent/askedquestion"
|
"vkane.cz/tinyquiz/pkg/model/ent/askedquestion"
|
||||||
@@ -84,6 +85,41 @@ func (m *Model) RegisterPlayer(playerName string, sessionCode string, now time.T
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) CreateSession(organiserName string, gameCode string, now time.Time, c context.Context) (*ent.Session, *ent.Player, error) {
|
||||||
|
tx, err := m.c.BeginTx(c, &sql.TxOptions{
|
||||||
|
Isolation: sql.LevelReadCommitted,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
if gameId, err := tx.Game.Query().Where(game.Code(gameCode)).OnlyID(c); err == nil {
|
||||||
|
if incremental, err := m.getCodeIncremental(c); err == nil {
|
||||||
|
if code, err := codeGenerator.GenerateRandomCode(incremental, codeRandomPartLength); err == nil {
|
||||||
|
if s, err := tx.Session.Create().SetID(uuid.New()).SetCreated(now).SetCode(string(code)).SetGameID(gameId).Save(c); err == nil {
|
||||||
|
if p, err := tx.Player.Create().SetID(uuid.New()).SetJoined(now).SetName(organiserName).SetSession(s).SetOrganiser(true).Save(c); err == nil {
|
||||||
|
err := tx.Commit()
|
||||||
|
return s, p, err
|
||||||
|
} else {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
} else if ent.IsNotFound(err) {
|
||||||
|
return nil, nil, NoSuchEntity
|
||||||
|
} else {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) GetPlayerWithSessionAndGame(uid uuid.UUID, c context.Context) (*ent.Player, error) {
|
func (m *Model) GetPlayerWithSessionAndGame(uid uuid.UUID, c context.Context) (*ent.Player, error) {
|
||||||
tx, err := m.c.BeginTx(c, &sql.TxOptions{
|
tx, err := m.c.BeginTx(c, &sql.TxOptions{
|
||||||
Isolation: sql.LevelRepeatableRead,
|
Isolation: sql.LevelRepeatableRead,
|
||||||
@@ -268,3 +304,13 @@ func (m *Model) SaveAnswer(playerId uuid.UUID, choiceId uuid.UUID, now time.Time
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const codeRandomPartLength uint8 = 3
|
||||||
|
|
||||||
|
func (m *Model) getCodeIncremental(c context.Context) (uint64, error) {
|
||||||
|
if c, err := m.c.CodesSequence.Create().Save(c); err == nil {
|
||||||
|
return uint64(c.ID), nil
|
||||||
|
} else {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func newTestModelWithData(t *testing.T) *Model {
|
|||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
var gamesC = []*ent.GameCreate{
|
var gamesC = []*ent.GameCreate{
|
||||||
tx.Game.Create().SetID(uuid.MustParse("cab48de7-bba3-4873-9335-eec4aaaae1e9")).SetName("5th grade knowledge test").SetCreated(time.Unix(1613387448, 0)).SetAuthor("Adam Smith PhD."),
|
tx.Game.Create().SetID(uuid.MustParse("cab48de7-bba3-4873-9335-eec4aaaae1e9")).SetName("5th grade knowledge test").SetCode("abcdef").SetCreated(time.Unix(1613387448, 0)).SetAuthor("Adam Smith PhD."),
|
||||||
}
|
}
|
||||||
|
|
||||||
games := tx.Game.CreateBulk(gamesC...).SaveX(c)
|
games := tx.Game.CreateBulk(gamesC...).SaveX(c)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h1>Začít hrát</h1>
|
<h1>Začít hrát</h1>
|
||||||
<form id="play" method="post" action="/play">
|
<form id="play" method="post" action="/session">
|
||||||
<label>Kód kvízu: <input type="text" name="code" placeholder="Kód kvizu"></label>
|
<label>Kód kvízu: <input type="text" name="code" placeholder="Kód kvizu"></label>
|
||||||
<label>Jméno organizátora: <input type="text" name="organiser" placeholder="Jméno"></label>
|
<label>Jméno organizátora: <input type="text" name="organiser" placeholder="Jméno"></label>
|
||||||
<input type="submit" value="Začit hrát">
|
<input type="submit" value="Začit hrát">
|
||||||
|
|||||||
Reference in New Issue
Block a user