Skip to content

Commit

Permalink
Add registration to single-player tournaments
Browse files Browse the repository at this point in the history
  • Loading branch information
thehowl committed Apr 30, 2018
1 parent 09b1427 commit 24a4aaa
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 12 deletions.
2 changes: 2 additions & 0 deletions http/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"zxq.co/ripple/misirlou-api/http"
)

// Home returns the homepage of the Misirlou API, which simply gives the
// software URL and the ID of the Ripple user, if a Session is present.
func Home(c *http.Context) {
c.WriteString("Misirlou API 2.0\nhttps://zxq.co/ripple/misirlou-api\n")
s := c.Session()
Expand Down
10 changes: 8 additions & 2 deletions http/api/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package api
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"time"

"golang.org/x/oauth2"
Expand Down Expand Up @@ -91,9 +93,13 @@ func OAuthFinish(c *http.Context) {
return
}

// token is a random set of characters which is stored in DB as a sha256 hash
token := randomStr(30)
token256 := sha256.Sum256([]byte(token))

// Save session
sess := &models.Session{
ID: randomStr(15),
ID: hex.EncodeToString(token256[:]),
UserID: u.ID,
AccessToken: code.AccessToken,
}
Expand All @@ -103,7 +109,7 @@ func OAuthFinish(c *http.Context) {
return
}

c.Redirect(302, c.StoreTokensURL+"?session="+sess.ID+"&access="+sess.AccessToken)
c.Redirect(302, c.StoreTokensURL+"?session="+token+"&access="+sess.AccessToken)
}

func init() {
Expand Down
133 changes: 133 additions & 0 deletions http/api/teams.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api
import (
"zxq.co/ripple/misirlou-api/http"
"zxq.co/ripple/misirlou-api/models"
"zxq.co/x/ripple"
)

// Teams fetches the teams that are playing in the given tournament ID.
Expand Down Expand Up @@ -41,6 +42,137 @@ func Team(c *http.Context) {
c.SetJSON(team, team == nil)
}

// CreateTeamData is the JSON data that is passed to CreateTeam.
type CreateTeamData struct {
Tournament models.ID `json:"tournament"`
Members []int `json:"members"`
Name string `json:"name"`
}

// CreateTeam creates a new team and verifies that the team can actually
// take part in the tournament.
func CreateTeam(c *http.Context) {
// Get session data
sess := c.Session()
if sess == nil {
c.SetCode(401)
c.WriteString("Missing or invalid access token.")
return
}

// Get client; fetch current user.
cl := sess.RippleClient()
user, err := validateUser(cl)
if err != nil {
c.Error(err)
return
}

// Get the data submitted by the user.
var d CreateTeamData
err = c.JSON(&d)
if err != nil {
c.Error(err)
return
}
tourn, err := userCanRegister(c.DB, d.Tournament, user.ID)
if err != nil {
c.Error(err)
return
}

// Single-user tournament; we force the team name and members.
if tourn.TeamSize == 1 {
t := &models.Team{
Name: user.Username,
Tournament: tourn.ID,
Captain: user.ID,
}
err = c.DB.CreateTeam(t)
if err != nil {
c.Error(err)
return
}
err = c.DB.AddTeamMembers([]models.TeamMember{{
Team: t.ID,
User: user.ID,
Attributes: models.TeamAttributeCaptain,
}})
if err != nil {
c.Error(err)
return
}
c.SetHeader("Location", c.BaseURL+"/teams/"+t.ID.String())
c.SetJSONWithCode(t, 201)
return
}

c.WriteString("Not implemented")
}

// validateUser checks that the user has a valid access token and that the user
// has privileges UserNormal and UserPublic. (is not banned/locked/restricted).
func validateUser(cl *ripple.Client) (*ripple.User, error) {
user, err := cl.User(ripple.Self)
if err != nil {
return nil, err
}

if user == nil {
return nil, &http.ResponseError{
Code: 401,
Message: "Access token is invalid - did you revoke the token?",
}
}
if user.Privileges&3 != 3 {
return nil, &http.ResponseError{
Code: 403,
Message: "You don't have the required privileges to register for a tournament.",
}
}

return user, nil
}

func userCanRegister(db *models.DB, tournID models.ID, uid int) (*models.Tournament, error) {
tourn, err := db.Tournament(tournID)
if err != nil {
return nil, err
}
if tourn == nil || tourn.Status == models.StatusOrganising {
return nil, &http.ResponseError{
Code: 404,
Message: "That tournament does not exist.",
}
}

// Check whether user in another team of this tournament.
in, err := db.UserInTournament(tourn.ID, uid)
if err != nil {
return nil, err
}
if in {
return nil, &http.ResponseError{
Code: 409,
Message: "You are already in this tournament",
}
}

// Check whether we're busy with another tournament
busy, err := db.UserIsBusy(tourn, uid)
if err != nil {
return nil, err
}
if busy {
return nil, &http.ResponseError{
Code: 409,
Message: "You can't join another tournament that overlaps with a tournament you're already in!",
}
}

return tourn, nil
}

// TeamMembers retrieves all the members of a team.
func TeamMembers(c *http.Context) {
members, err := c.DB.TeamMembers(c.ParamID("id"), c.QueryInt("p"))
Expand All @@ -53,6 +185,7 @@ func TeamMembers(c *http.Context) {

func init() {
http.GET("/tournaments/:id/teams", TeamsInTournament)
http.POST("/tournaments/:id/teams", CreateTeam)
http.GET("/teams", Teams)
http.GET("/teams/:id", Team)
http.GET("/teams/:id/members", TeamMembers)
Expand Down
18 changes: 17 additions & 1 deletion http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,26 @@ func (c *Context) reportError(err error) {
fmt.Fprintln(os.Stderr, err)
}

// ResponseError can be passed to Error, and instead of returning a 500, it will
// return a response with the code and the message specified.
type ResponseError struct {
Code int
Message string
}

func (re *ResponseError) Error() string {
return re.Message
}

// Error closes the request with a 500 code and prints the error to stderr.
func (c *Context) Error(err error) {
if re, ok := err.(*ResponseError); ok {
c.SetCode(re.Code)
c.WriteString(re.Message + "\n")
return
}
c.SetCode(500)
c.WriteString("Internal Server Error")
c.WriteString("Internal Server Error\n")
c.reportError(err)
}

Expand Down
31 changes: 31 additions & 0 deletions models/id.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package models

import (
"encoding/base64"
"reflect"
"sync/atomic"
"time"

"github.com/jinzhu/gorm"
)

// ID is the ID of a single resource. IDs are similar to Snowflake IDs, in that
Expand Down Expand Up @@ -118,3 +121,31 @@ func (i ID) String() string {
func (i ID) Time() time.Time {
return time.Unix(0, int64((i<<4)&timeBits))
}

// Register gorm callback for generating IDs when they are blank.
func init() {
gorm.DefaultCallback.Create().Before("gorm:before_create").
Register("snowflake:generate_id", generateID)
}

var idType = reflect.TypeOf(ID(0))

func generateID(scope *gorm.Scope) {
if scope.HasError() {
return
}
field, ok := scope.FieldByName("ID")
if !ok {
return
}
if !field.IsBlank {
return
}
if !idType.AssignableTo(field.Field.Type()) {
return
}
err := field.Set(GenerateID())
if err != nil {
scope.Err(err)
}
}
15 changes: 14 additions & 1 deletion models/session.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package models

import "errors"
import (
"errors"

"zxq.co/x/ripple"
)

// Session represents a single user session of an user who has authenticated
// on Misirlou.
Expand Down Expand Up @@ -28,3 +32,12 @@ func (db *DB) SetSession(sess *Session) error {
}
return db.db.Save(sess).Error
}

// RippleClient obtains a new ripple.Client to fetch information about the
// user.
func (s Session) RippleClient() *ripple.Client {
return &ripple.Client{
IsBearer: true,
Token: s.AccessToken,
}
}
38 changes: 32 additions & 6 deletions models/team.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ package models
type Team struct {
ID ID `json:"id"`
Name string `json:"name"`
Tournament int `json:"tournament"`
Tournament ID `json:"tournament"`
Captain int `json:"captain"`
}

// TODO: we should check that the team's tournament status is not 0.

// TeamFilters are options that can be passed to Teams for filtering teams.
type TeamFilters struct {
Tournament ID
Expand Down Expand Up @@ -41,12 +39,28 @@ func (db *DB) Team(id ID) (*Team, error) {
return &t, nil
}

// CreateTeam creates a new team, which is to say, registers in a tournament.
func (db *DB) CreateTeam(t *Team) error {
return db.db.Create(t).Error
}

// TeamAttributes represents the attributes a user may have inside of a team,
// such as being invited, a member, or a captain.
type TeamAttributes int

// Various TeamAttributes a team member might have.
const (
TeamAttributeInvited TeamAttributes = iota
TeamAttributeMember
TeamAttributeCaptain
)

// TeamMember represents a member of a team and information about their
// relationship to the team.
type TeamMember struct {
Team int `json:"team"`
User int `json:"user"`
Attributes int `json:"attributes"`
Team ID `json:"team"`
User int `json:"user"`
Attributes TeamAttributes `json:"attributes"`
}

// TableName returns the correct table name so that it can correctly be used
Expand All @@ -62,3 +76,15 @@ func (db *DB) TeamMembers(teamID ID, page int) ([]TeamMember, error) {
Find(&members, "team = ?", teamID).Error
return members, err
}

// AddTeamMembers adds the given team members to the database.
func (db *DB) AddTeamMembers(ms []TeamMember) error {
// not using for-range because this way we can keep the reference
// straight into the slice
for i := 0; i < len(ms); i++ {
if err := db.db.Create(&ms[i]).Error; err != nil {
return err
}
}
return nil
}
Loading

0 comments on commit 24a4aaa

Please sign in to comment.