258 lines
6.6 KiB
Go
258 lines
6.6 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"sort"
|
|
"time"
|
|
|
|
codequestpb "github.com/hhhapz/codequest/api/v1"
|
|
"github.com/hhhapz/codequest/models"
|
|
"github.com/hhhapz/codequest/question"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
var (
|
|
Start time.Time
|
|
End time.Time
|
|
)
|
|
|
|
func active() bool {
|
|
now := time.Now()
|
|
return Start.Before(now) && End.After(now)
|
|
}
|
|
|
|
type QuestService struct {
|
|
codequestpb.UnsafeQuestServiceServer
|
|
|
|
questStore QuestStore
|
|
userStore UserStore
|
|
cooldownStore CooldownStore
|
|
}
|
|
|
|
func (qs *QuestService) Questions(ctx context.Context, req *codequestpb.QuestionsRequest) (*codequestpb.QuestionsResponse, error) {
|
|
u := User(ctx)
|
|
|
|
questions, err := qs.questStore.Questions(ctx, u)
|
|
switch {
|
|
case errors.As(err, &models.UserError{}):
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
case err != nil:
|
|
return nil, status.Errorf(codes.Internal, "could not get questions: internal error")
|
|
}
|
|
|
|
if !active() {
|
|
questions = nil
|
|
}
|
|
|
|
res := new(codequestpb.QuestionsResponse)
|
|
|
|
for _, q := range questions {
|
|
res.Questions = append(res.Questions, &codequestpb.Question{
|
|
ID: q.Question.ID,
|
|
Title: q.Name,
|
|
Difficulty: codequestpb.Difficulty(q.Level),
|
|
Part1: &codequestpb.PartData{
|
|
Completed: q.Part1.Completed,
|
|
PointsWorth: int32(q.Part1.PointsWorth),
|
|
},
|
|
Part2: &codequestpb.PartData{
|
|
Completed: q.Part2.Completed,
|
|
PointsWorth: int32(q.Part2.PointsWorth),
|
|
},
|
|
})
|
|
}
|
|
|
|
q := res.Questions
|
|
sort.Slice(q, func(i, j int) bool {
|
|
var iSolved, jSolved int
|
|
switch {
|
|
case q[i].Part2.Completed:
|
|
iSolved = 2
|
|
case q[i].Part1.Completed:
|
|
iSolved = 1
|
|
}
|
|
switch {
|
|
case q[j].Part2.Completed:
|
|
jSolved = 2
|
|
case q[j].Part1.Completed:
|
|
jSolved = 1
|
|
}
|
|
|
|
switch {
|
|
// if progress on questions is the same, sort alphabetically
|
|
case iSolved == jSolved:
|
|
return q[i].Text < q[j].Text
|
|
|
|
// if both parts completed, always put at the end
|
|
case iSolved == 2:
|
|
return false
|
|
case jSolved == 2:
|
|
return true
|
|
|
|
// show partway completed ones before those not started
|
|
case iSolved == 1:
|
|
return true
|
|
case jSolved == 1:
|
|
return false
|
|
}
|
|
return false
|
|
})
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (qs *QuestService) QuestionByID(ctx context.Context, req *codequestpb.QuestionByIDRequest) (*codequestpb.Question, error) {
|
|
u := User(ctx)
|
|
|
|
if !active() {
|
|
return nil, status.Errorf(codes.NotFound, "the contest is not active")
|
|
}
|
|
|
|
question, err := qs.questStore.Question(ctx, u, req.ID)
|
|
switch {
|
|
case errors.As(err, &models.UserError{}):
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
case err != nil:
|
|
log.Printf("could not fetch question(%q): %v", req.ID, err)
|
|
return nil, status.Errorf(codes.Internal, "could not get questions: internal error")
|
|
}
|
|
|
|
content := new(bytes.Buffer)
|
|
err = question.Text.Execute(content, question)
|
|
if err != nil {
|
|
log.Printf("could gen write question %s template: %v", req.ID, err)
|
|
return nil, status.Errorf(codes.Internal, "could not get question: internal error")
|
|
}
|
|
|
|
q := &codequestpb.Question{
|
|
ID: question.Question.ID,
|
|
Title: question.Name,
|
|
Text: content.String(),
|
|
Part1: &codequestpb.PartData{
|
|
Completed: question.Part1.Completed,
|
|
PointsWorth: int32(question.Part1.PointsWorth),
|
|
},
|
|
Part2: &codequestpb.PartData{
|
|
Completed: question.Part2.Completed,
|
|
PointsWorth: int32(question.Part2.PointsWorth),
|
|
},
|
|
}
|
|
|
|
return q, nil
|
|
}
|
|
|
|
func (qs *QuestService) QuestionInput(ctx context.Context, req *codequestpb.QuestionInputRequest) (*codequestpb.QuestionInput, error) {
|
|
u := User(ctx)
|
|
|
|
if !active() {
|
|
return nil, status.Errorf(codes.NotFound, "the contest is not active")
|
|
}
|
|
|
|
question, err := qs.questStore.Question(ctx, u, req.ID)
|
|
switch {
|
|
case errors.As(err, &models.UserError{}):
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
case err != nil:
|
|
log.Printf("could not get question %s: %v", req.ID, err)
|
|
return nil, status.Errorf(codes.Internal, "could not get question: internal error")
|
|
}
|
|
|
|
inp := question.Question.Generate(u)
|
|
return &codequestpb.QuestionInput{
|
|
ID: question.Question.ID,
|
|
Input: inp,
|
|
}, nil
|
|
}
|
|
|
|
func (qs *QuestService) Submit(ctx context.Context, req *codequestpb.SubmitRequest) (*codequestpb.SubmitResponse, error) {
|
|
u := User(ctx)
|
|
|
|
if !active() {
|
|
return nil, status.Errorf(codes.NotFound, "the contest is not active")
|
|
}
|
|
|
|
if req.Part != 1 && req.Part != 2 {
|
|
return nil, status.Errorf(codes.NotFound, "invalid part number")
|
|
}
|
|
|
|
q, err := qs.questStore.Question(ctx, u, req.ID)
|
|
switch {
|
|
case errors.As(err, &models.UserError{}):
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
case err != nil:
|
|
return nil, status.Errorf(codes.Internal, "could not get questions: internal error")
|
|
}
|
|
|
|
completed, attempts, err := qs.questStore.Submissions(ctx, u, req.ID, question.Part(req.Part))
|
|
switch {
|
|
case errors.As(err, &models.UserError{}):
|
|
return nil, status.Errorf(codes.InvalidArgument, "%v", err)
|
|
case err != nil:
|
|
log.Printf("could not get submissions: %v", err)
|
|
return nil, status.Errorf(codes.Internal, "could not get submissions: internal error")
|
|
}
|
|
|
|
if completed {
|
|
return nil, status.Errorf(codes.AlreadyExists, "question %s (part %d) already completed", req.ID, req.Part)
|
|
}
|
|
|
|
// TODO: cooldowns
|
|
t := qs.cooldownStore.Check(u.ID, q.Question.ID)
|
|
if t != 0 {
|
|
return nil, status.Errorf(
|
|
codes.FailedPrecondition,
|
|
"you must wait %s before trying this question again", t.Truncate(time.Second),
|
|
)
|
|
}
|
|
|
|
ok := q.Question.Validate(u, question.Part(req.Part), req.Body.Answer)
|
|
|
|
var points int
|
|
if ok {
|
|
points = 5000 - (attempts * 100)
|
|
if points < 2000 {
|
|
points = 2000
|
|
}
|
|
} else {
|
|
qs.cooldownStore.NewAttempt(u.ID, q.Question.ID, attempts)
|
|
}
|
|
|
|
qa := &models.QuestionAttempt{
|
|
UserID: u.ID,
|
|
QuestionID: q.Question.ID,
|
|
QuestionPart: int(req.Part),
|
|
Correct: ok,
|
|
PointsAwarded: points,
|
|
Answer: req.Body.Answer,
|
|
Code: req.Body.Code,
|
|
SubmittedAt: models.NewTime(time.Now()),
|
|
}
|
|
qs.questStore.AddSubmission(ctx, qa)
|
|
|
|
res := &codequestpb.SubmitResponse{
|
|
Correct: qa.Correct,
|
|
Points: int32(qa.PointsAwarded),
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (qs *QuestService) Leaderboard(ctx context.Context, req *codequestpb.LeaderboardRequest) (*codequestpb.LeaderboardResponse, error) {
|
|
entries, err := qs.questStore.Leaderboard(ctx)
|
|
if err != nil {
|
|
log.Printf("could not get leaderboard: %v", err)
|
|
return nil, status.Errorf(codes.Internal, "could not get leaderboard: internal error")
|
|
}
|
|
var lbr codequestpb.LeaderboardResponse
|
|
for _, le := range entries {
|
|
lbr.Leaderboard = append(lbr.Leaderboard, &codequestpb.LeaderboardEntry{
|
|
Username: le.Name,
|
|
Points: int32(le.Points),
|
|
})
|
|
}
|
|
return &lbr, nil
|
|
}
|