~xenrox/10man-api

04c80adcc87d60d24ad0e3900e93940bfe80403f — Thorben Günther 10 months ago f88d4c0
Implement mapvote in memory
M README.md => README.md +2 -0
@@ 13,4 13,6 @@ Your config file should be placed at `/etc/10man-api/config`.
connection-string postgresql://user:pass@localhost/db?sslmode=disable
# Comma separated list of steamID64 for users that should become an admin
admins STEAMID64_ADMIN1,STEAMID64_ADMIN2
# Comma separated list of maps
maps inferno,mirage
```

M config/config.go => config/config.go +10 -0
@@ 15,6 15,8 @@ var ConnectionString string
// Admins array of steamID64 of users that should become admins
var Admins []string

var Maps []string

func init() {
	cfg, err := scfg.Load(configPath)
	if err != nil {


@@ 36,6 38,14 @@ func init() {

			Admins = strings.Split(admins, ",")

		case "maps":
			var maps string
			if err := d.ParseParams(&maps); err != nil {
				log.Fatalf("could not parse maps: %v", err)
			}

			Maps = strings.Split(maps, ",")

		default:
			log.Fatalf("illegal config value %q", d.Name)
		}

M database/schema.go => database/schema.go +2 -1
@@ 37,5 37,6 @@ CREATE TABLE "Match" (
	elo1 SMALLINT NOT NULL,
	elo2 SMALLINT NOT NULL,
	winner VARCHAR(255),
	CONSTRAINT ck_winner CHECK(winner IN ('TEAM1', 'TEAM2'))
	CONSTRAINT ck_winner CHECK(winner IN ('TEAM1', 'TEAM2')),
	map VARCHAR(255)
);`

A functions/functions.go => functions/functions.go +12 -0
@@ 0,0 1,12 @@
package functions

// SliceContains checks if slice s contains element e and returns index
func SliceContains(s []string, e string) (bool, int) {
	for i, v := range s {
		if e == v {
			return true, i
		}
	}

	return false, 0
}

M go.mod => go.mod +1 -1
@@ 3,13 3,13 @@ module git.xenrox.net/~xenrox/10man-api
go 1.17

require (
	git.sr.ht/~emersion/go-scfg v0.0.0-20201019143924-142a8aa629fc
	github.com/99designs/gqlgen v0.14.0
	github.com/lib/pq v1.10.3
	github.com/vektah/gqlparser/v2 v2.2.0
)

require (
	git.sr.ht/~emersion/go-scfg v0.0.0-20201019143924-142a8aa629fc // indirect
	github.com/agnivade/levenshtein v1.1.1 // indirect
	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
	github.com/gorilla/websocket v1.4.2 // indirect

M graph/generated/generated.go => graph/generated/generated.go +254 -0
@@ 43,6 43,11 @@ type DirectiveRoot struct {
}

type ComplexityRoot struct {
	MapVote struct {
		Maps   func(childComplexity int) int
		Status func(childComplexity int) int
	}

	Mutation struct {
		CancelMatch func(childComplexity int) int
		CancelQueue func(childComplexity int, teamspeakID string) int


@@ 52,6 57,7 @@ type ComplexityRoot struct {
		FinishMatch func(childComplexity int, winner string) int
		StartQueue  func(childComplexity int, teamspeakID string) int
		UpdateUser  func(childComplexity int, id int, input model.UserInput) int
		VetoMap     func(childComplexity int, mapArg *string) int
	}

	Query struct {


@@ 85,6 91,7 @@ type MutationResolver interface {
	CreateMatch(ctx context.Context) (int, error)
	CancelMatch(ctx context.Context) (string, error)
	FinishMatch(ctx context.Context, winner string) (string, error)
	VetoMap(ctx context.Context, mapArg *string) (*model.MapVote, error)
}
type QueryResolver interface {
	UserBySteam(ctx context.Context, steamID string) (*model.User, error)


@@ 107,6 114,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
	_ = ec
	switch typeName + "." + field {

	case "MapVote.maps":
		if e.complexity.MapVote.Maps == nil {
			break
		}

		return e.complexity.MapVote.Maps(childComplexity), true

	case "MapVote.status":
		if e.complexity.MapVote.Status == nil {
			break
		}

		return e.complexity.MapVote.Status(childComplexity), true

	case "Mutation.cancelMatch":
		if e.complexity.Mutation.CancelMatch == nil {
			break


@@ 193,6 214,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.Mutation.UpdateUser(childComplexity, args["id"].(int), args["input"].(model.UserInput)), true

	case "Mutation.vetoMap":
		if e.complexity.Mutation.VetoMap == nil {
			break
		}

		args, err := ec.field_Mutation_vetoMap_args(context.TODO(), rawArgs)
		if err != nil {
			return 0, false
		}

		return e.complexity.Mutation.VetoMap(childComplexity, args["map"].(*string)), true

	case "Query.getTeams":
		if e.complexity.Query.GetTeams == nil {
			break


@@ 371,6 404,12 @@ type Teams {
    team2: [User!]!
}

type MapVote {
    maps: [String!]!
    # status: TEAM1, TEAM2, DONE
    status: String!
}

type Query {
    userBySteam(steamID: String!): User
    userByTS(teamspeakID: String!): User


@@ 400,6 439,7 @@ type Mutation {
    createMatch: Int!
    cancelMatch: String!
    finishMatch(winner: String!): String!
    vetoMap(map: String): MapVote!
}
`, BuiltIn: false},
}


@@ 508,6 548,21 @@ func (ec *executionContext) field_Mutation_updateUser_args(ctx context.Context, 
	return args, nil
}

func (ec *executionContext) field_Mutation_vetoMap_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
	var err error
	args := map[string]interface{}{}
	var arg0 *string
	if tmp, ok := rawArgs["map"]; ok {
		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("map"))
		arg0, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
		if err != nil {
			return nil, err
		}
	}
	args["map"] = arg0
	return args, nil
}

func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
	var err error
	args := map[string]interface{}{}


@@ 606,6 661,76 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg

// region    **************************** field.gotpl *****************************

func (ec *executionContext) _MapVote_maps(ctx context.Context, field graphql.CollectedField, obj *model.MapVote) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	fc := &graphql.FieldContext{
		Object:     "MapVote",
		Field:      field,
		Args:       nil,
		IsMethod:   false,
		IsResolver: false,
	}

	ctx = graphql.WithFieldContext(ctx, fc)
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return obj.Maps, nil
	})
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	if resTmp == nil {
		if !graphql.HasFieldError(ctx, fc) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	res := resTmp.([]string)
	fc.Result = res
	return ec.marshalNString2ᚕstringᚄ(ctx, field.Selections, res)
}

func (ec *executionContext) _MapVote_status(ctx context.Context, field graphql.CollectedField, obj *model.MapVote) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	fc := &graphql.FieldContext{
		Object:     "MapVote",
		Field:      field,
		Args:       nil,
		IsMethod:   false,
		IsResolver: false,
	}

	ctx = graphql.WithFieldContext(ctx, fc)
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return obj.Status, nil
	})
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	if resTmp == nil {
		if !graphql.HasFieldError(ctx, fc) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	res := resTmp.(string)
	fc.Result = res
	return ec.marshalNString2string(ctx, field.Selections, res)
}

func (ec *executionContext) _Mutation_createUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {


@@ 925,6 1050,48 @@ func (ec *executionContext) _Mutation_finishMatch(ctx context.Context, field gra
	return ec.marshalNString2string(ctx, field.Selections, res)
}

func (ec *executionContext) _Mutation_vetoMap(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	fc := &graphql.FieldContext{
		Object:     "Mutation",
		Field:      field,
		Args:       nil,
		IsMethod:   true,
		IsResolver: true,
	}

	ctx = graphql.WithFieldContext(ctx, fc)
	rawArgs := field.ArgumentMap(ec.Variables)
	args, err := ec.field_Mutation_vetoMap_args(ctx, rawArgs)
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	fc.Args = args
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return ec.resolvers.Mutation().VetoMap(rctx, args["map"].(*string))
	})
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	if resTmp == nil {
		if !graphql.HasFieldError(ctx, fc) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	res := resTmp.(*model.MapVote)
	fc.Result = res
	return ec.marshalNMapVote2ᚖgitᚗxenroxᚗnetᚋאxenroxᚋ10manᚑapiᚋgraphᚋmodelᚐMapVote(ctx, field.Selections, res)
}

func (ec *executionContext) _Query_userBySteam(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {


@@ 2649,6 2816,38 @@ func (ec *executionContext) unmarshalInputUserInput(ctx context.Context, obj int

// region    **************************** object.gotpl ****************************

var mapVoteImplementors = []string{"MapVote"}

func (ec *executionContext) _MapVote(ctx context.Context, sel ast.SelectionSet, obj *model.MapVote) graphql.Marshaler {
	fields := graphql.CollectFields(ec.OperationContext, sel, mapVoteImplementors)

	out := graphql.NewFieldSet(fields)
	var invalids uint32
	for i, field := range fields {
		switch field.Name {
		case "__typename":
			out.Values[i] = graphql.MarshalString("MapVote")
		case "maps":
			out.Values[i] = ec._MapVote_maps(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
			}
		case "status":
			out.Values[i] = ec._MapVote_status(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
			}
		default:
			panic("unknown field " + strconv.Quote(field.Name))
		}
	}
	out.Dispatch()
	if invalids > 0 {
		return graphql.Null
	}
	return out
}

var mutationImplementors = []string{"Mutation"}

func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {


@@ 2701,6 2900,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
			if out.Values[i] == graphql.Null {
				invalids++
			}
		case "vetoMap":
			out.Values[i] = ec._Mutation_vetoMap(ctx, field)
			if out.Values[i] == graphql.Null {
				invalids++
			}
		default:
			panic("unknown field " + strconv.Quote(field.Name))
		}


@@ 3141,6 3345,20 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti
	return res
}

func (ec *executionContext) marshalNMapVote2gitᚗxenroxᚗnetᚋאxenroxᚋ10manᚑapiᚋgraphᚋmodelᚐMapVote(ctx context.Context, sel ast.SelectionSet, v model.MapVote) graphql.Marshaler {
	return ec._MapVote(ctx, sel, &v)
}

func (ec *executionContext) marshalNMapVote2ᚖgitᚗxenroxᚗnetᚋאxenroxᚋ10manᚑapiᚋgraphᚋmodelᚐMapVote(ctx context.Context, sel ast.SelectionSet, v *model.MapVote) graphql.Marshaler {
	if v == nil {
		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	return ec._MapVote(ctx, sel, v)
}

func (ec *executionContext) unmarshalNNewUser2gitᚗxenroxᚗnetᚋאxenroxᚋ10manᚑapiᚋgraphᚋmodelᚐNewUser(ctx context.Context, v interface{}) (model.NewUser, error) {
	res, err := ec.unmarshalInputNewUser(ctx, v)
	return res, graphql.ErrorOnPath(ctx, err)


@@ 3161,6 3379,42 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S
	return res
}

func (ec *executionContext) unmarshalNString2ᚕstringᚄ(ctx context.Context, v interface{}) ([]string, error) {
	var vSlice []interface{}
	if v != nil {
		if tmp1, ok := v.([]interface{}); ok {
			vSlice = tmp1
		} else {
			vSlice = []interface{}{v}
		}
	}
	var err error
	res := make([]string, len(vSlice))
	for i := range vSlice {
		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i))
		res[i], err = ec.unmarshalNString2string(ctx, vSlice[i])
		if err != nil {
			return nil, err
		}
	}
	return res, nil
}

func (ec *executionContext) marshalNString2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler {
	ret := make(graphql.Array, len(v))
	for i := range v {
		ret[i] = ec.marshalNString2string(ctx, sel, v[i])
	}

	for _, e := range ret {
		if e == graphql.Null {
			return graphql.Null
		}
	}

	return ret
}

func (ec *executionContext) marshalNTeams2gitᚗxenroxᚗnetᚋאxenroxᚋ10manᚑapiᚋgraphᚋmodelᚐTeams(ctx context.Context, sel ast.SelectionSet, v model.Teams) graphql.Marshaler {
	return ec._Teams(ctx, sel, &v)
}

M graph/model/models_gen.go => graph/model/models_gen.go +5 -0
@@ 2,6 2,11 @@

package model

type MapVote struct {
	Maps   []string `json:"maps"`
	Status string   `json:"status"`
}

type NewUser struct {
	SteamID     string  `json:"steamID"`
	TeamspeakID string  `json:"teamspeakID"`

M graph/schema.graphqls => graph/schema.graphqls +7 -0
@@ 13,6 13,12 @@ type Teams {
    team2: [User!]!
}

type MapVote {
    maps: [String!]!
    # status: TEAM1, TEAM2, DONE
    status: String!
}

type Query {
    userBySteam(steamID: String!): User
    userByTS(teamspeakID: String!): User


@@ 42,4 48,5 @@ type Mutation {
    createMatch: Int!
    cancelMatch: String!
    finishMatch(winner: String!): String!
    vetoMap(map: String): MapVote!
}

M graph/schema.resolvers.go => graph/schema.resolvers.go +66 -0
@@ 10,15 10,22 @@ import (
	"errors"
	"fmt"
	"io/ioutil"
	"math/rand"
	"net/http"
	"time"

	"git.xenrox.net/~xenrox/10man-api/config"
	"git.xenrox.net/~xenrox/10man-api/database"
	"git.xenrox.net/~xenrox/10man-api/functions"
	"git.xenrox.net/~xenrox/10man-api/graph/generated"
	"git.xenrox.net/~xenrox/10man-api/graph/model"
	"git.xenrox.net/~xenrox/10man-api/logic"
)

// save current map vote
var voteMaps []string
var voteStatus string

func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (string, error) {
	type Elo struct {
		Elo int `json:"Elo"`


@@ 306,6 313,16 @@ func (r *mutationResolver) CreateMatch(ctx context.Context) (int, error) {
		return -1, err
	}

	// initialize map vote (maps and random voter)
	voteMaps = config.Maps
	rand.Seed(time.Now().UTC().UnixNano())
	voter := rand.Intn(2)
	if voter == 0 {
		voteStatus = "TEAM1"
	} else {
		voteStatus = "TEAM2"
	}

	return matchID, nil
}



@@ 388,6 405,55 @@ func (r *mutationResolver) FinishMatch(ctx context.Context, winner string) (stri
	return "FinishSuccess", nil
}

func (r *mutationResolver) VetoMap(ctx context.Context, mapArg *string) (*model.MapVote, error) {
	var mapVote model.MapVote

	if mapArg == nil {
		// return all remaining maps
		mapVote.Maps = voteMaps
		mapVote.Status = voteStatus

		return &mapVote, nil
	}

	if voteStatus == "DONE" {
		return nil, errors.New("vote is done")
	}

	length := len(voteMaps)

	// check if voted map is in remaining
	contains, i := functions.SliceContains(voteMaps, *mapArg)
	if !contains {
		return nil, errors.New("no legal vote")
	}
	voteMaps[i] = voteMaps[length-1]
	voteMaps = voteMaps[:length-1]

	if length == 2 {
		voteStatus = "DONE"

		// save in database
		query := `
			UPDATE "Match"
			SET map = $1
			WHERE status = 'ongoing'`
		_, err := database.DB.Exec(query, voteMaps[0])
		if err != nil {
			return nil, fmt.Errorf("failed to update match map: %w", err)
		}
	} else if voteStatus == "TEAM1" {
		voteStatus = "TEAM2"
	} else if voteStatus == "TEAM2" {
		voteStatus = "TEAM1"
	}

	mapVote.Maps = voteMaps
	mapVote.Status = voteStatus

	return &mapVote, nil
}

func (r *queryResolver) UserBySteam(ctx context.Context, steamID string) (*model.User, error) {
	var user model.User
	user.SteamID = steamID