From 268f7293fd9f71d2c27a34e74516bce89efd3f0b Mon Sep 17 00:00:00 2001 From: Nguyen Thanh Quang Date: Wed, 7 Feb 2024 11:19:04 +0700 Subject: [PATCH] :sparkles: (judge) live submission observer --- cache/stores/problems.go | 2 +- cache/stores/users.go | 581 +++++++++++++---------- config/config.go | 63 +-- db/models/contest/submission.go | 52 -- db/models/post/comment.go | 17 - db/models/post/post.go | 16 - db/models/user/role.go | 15 - db/models/user/user.go | 43 -- db/{models => schema}/contest/contest.go | 4 +- db/{models => schema}/contest/problem.go | 10 +- db/schema/contest/submission.go | 51 ++ db/schema/post/comment.go | 17 + db/schema/post/post.go | 16 + db/{models => schema}/user/oauth.go | 5 +- db/schema/user/org.go | 32 ++ db/schema/user/role.go | 25 + db/schema/user/user.go | 26 + db/seed/seed.go | 10 +- go.mod | 40 +- go.sum | 91 ++-- judge/judge.go | 2 +- judge/{worker.go => observer.go} | 128 ++--- judge/result.go | 23 +- permission/permission.go | 11 +- routes/apex/status.go | 2 +- routes/auth/login.go | 2 +- routes/auth/register.go | 23 +- routes/oauth/index.go | 3 +- routes/oauth/unlink.go | 2 +- routes/oauth/validate.go | 3 +- routes/posts/index.go | 2 +- routes/problems/index.go | 2 +- routes/problems/submit.go | 51 +- routes/submissions/cancel_submission.go | 2 +- routes/submissions/index.go | 8 +- routes/submissions/source.dynamic.go | 8 +- routes/submissions/submission.dynamic.go | 34 +- routes/user/api_key.go | 2 +- routes/user/hover_card.dynamic.go | 2 - routes/user/index.go | 2 +- routes/users/index.go | 19 +- server/http/context.go | 1 + server/http/method.go | 8 +- server/middlewares/auth.go | 20 +- server/server.go | 6 + server/session/paseto.go | 2 +- storage/submission.go | 21 +- utils/crypto/hash.go | 2 +- 48 files changed, 845 insertions(+), 662 deletions(-) delete mode 100644 db/models/contest/submission.go delete mode 100644 db/models/post/comment.go delete mode 100644 db/models/post/post.go delete mode 100644 db/models/user/role.go delete mode 100644 db/models/user/user.go rename db/{models => schema}/contest/contest.go (87%) rename db/{models => schema}/contest/problem.go (79%) create mode 100644 db/schema/contest/submission.go create mode 100644 db/schema/post/comment.go create mode 100644 db/schema/post/post.go rename db/{models => schema}/user/oauth.go (69%) create mode 100644 db/schema/user/org.go create mode 100644 db/schema/user/role.go create mode 100644 db/schema/user/user.go rename judge/{worker.go => observer.go} (56%) diff --git a/cache/stores/problems.go b/cache/stores/problems.go index 0514a86..7d95641 100644 --- a/cache/stores/problems.go +++ b/cache/stores/problems.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/ArcticOJ/blizzard/v0/cache" "github.com/ArcticOJ/blizzard/v0/db" - "github.com/ArcticOJ/blizzard/v0/db/models/contest" + "github.com/ArcticOJ/blizzard/v0/db/schema/contest" "github.com/ArcticOJ/blizzard/v0/rejson" "time" ) diff --git a/cache/stores/users.go b/cache/stores/users.go index 9cd062b..f242b99 100644 --- a/cache/stores/users.go +++ b/cache/stores/users.go @@ -2,18 +2,13 @@ package stores import ( "context" - "crypto/md5" - "errors" "fmt" "github.com/ArcticOJ/blizzard/v0/cache" "github.com/ArcticOJ/blizzard/v0/db" - "github.com/ArcticOJ/blizzard/v0/db/models/user" + "github.com/ArcticOJ/blizzard/v0/db/schema/user" "github.com/ArcticOJ/blizzard/v0/logger" "github.com/ArcticOJ/blizzard/v0/rejson" - "github.com/ArcticOJ/blizzard/v0/utils/numeric" "github.com/google/uuid" - "github.com/redis/go-redis/v9" - "github.com/tmthrgd/go-hex" "github.com/uptrace/bun" "golang.org/x/sync/singleflight" "strings" @@ -28,12 +23,13 @@ type userStore struct { } const ( - defaultUserKey = "blizzard::user[%s]" - defaultHandleToIdResolver = "blizzard::user_id[%s]" - defaultUserListKey = "blizzard::user_list" - defaultUserTtl = time.Hour * 48 - - DefaultUserPageSize = 25 + //defaultUserKey = "blizzard::user[%s]" + //defaultHandleToIdResolver = "blizzard::user_id[%s]" + defaultUserListKey = "blizzard::user_list" + defaultUserTtl = time.Hour * 48 + //defaultAvatarHashKey = "blizzard::avatar_hash[%s]" + defaultApiKeyResolverKey = "blizzard::api_key[%s]" + DefaultUserPageSize = 25 ) func init() { @@ -41,268 +37,347 @@ func init() { Users.populateUserList(context.Background()) } -func (s *userStore) populateUserList(c context.Context) { - _, e, _ := s.s.Do("populate", func() (interface{}, error) { - if Users.j.Exists(c, defaultUserListKey).Val() == 0 { - var ( - ids []string - ratings []uint16 - ) - // TODO: use app's context for this - if _e := db.Database.NewSelect().Model((*user.User)(nil)).Column("id", "rating").Scan(context.Background(), &ids, &ratings); _e != nil { - return nil, _e - } - var m []redis.Z - for i := range ids { - m = append(m, redis.Z{ - Score: float64(numeric.CompressUint16(ratings[i], 0)), - Member: ids[i], - }) - } - if _e := Users.j.ZAdd(context.Background(), defaultUserListKey, m...).Err(); _e != nil { - return nil, _e - } - } - return nil, nil - }) - logger.Panic(e, "failed to populate user cache") -} - -func (s *userStore) loadOne(ctx context.Context, id uuid.UUID, handle string) (u *user.User) { - u = new(user.User) - q := db.Database.NewSelect().Model(u).ExcludeColumn("password", "api_key") - if id == uuid.Nil { - q = q.Where("handle = ?", handle) - } else { - q = q.Where("id = ?", id) - } - if e := q.Relation("Connections", func(query *bun.SelectQuery) *bun.SelectQuery { - return query.Where("show_in_profile = true") - }).Relation("Roles", func(query *bun.SelectQuery) *bun.SelectQuery { - return query.Order("priority ASC").Column("name", "icon", "color") - }).Scan(ctx); e != nil { - return nil - } - if strings.TrimSpace(u.Email) != "" { - h := md5.Sum([]byte(u.Email)) - u.Avatar = hex.EncodeToString(h[:]) - } - if len(u.Roles) > 0 { - u.TopRole = &u.Roles[0] - } - return -} - -func (s *userStore) loadMulti(ctx context.Context, users []user.User) (u []user.User) { - if db.Database.NewSelect(). - // users are not selected by order of ids but by order in the database, so I have to use this hack to ensure stable order - // might not be performance-wise but at least it does the trick lol - With("inp", db.Database.NewValues(&users).Column("id").WithOrder()). - Model(&u).ExcludeColumn("password", "api_key"). - Table("inp"). - Where("\"user\".id = inp.id"). - OrderExpr("inp._order"). - Relation("Connections", func(query *bun.SelectQuery) *bun.SelectQuery { - return query.Where("show_in_profile = true") - }). - Relation("Roles", func(query *bun.SelectQuery) *bun.SelectQuery { - return query.Order("priority ASC").Column("name", "icon", "color") - }).Scan(ctx) != nil { - return nil +func (s *userStore) ResolveApiKey(ctx context.Context, apiKey string) (uid uuid.UUID) { + if e := s.j.Get(ctx, fmt.Sprintf(defaultApiKeyResolverKey, apiKey)).Scan(&uid); e == nil { + return } - for i := range u { - if strings.TrimSpace(u[i].Email) != "" { - h := md5.Sum([]byte(u[i].Email)) - u[i].Avatar = hex.EncodeToString(h[:]) - } - if len(u[i].Roles) > 0 { - u[i].TopRole = &u[i].Roles[0] - } + if db.Database.NewSelect().Model((*user.User)(nil)).Where("api_key = ?", apiKey).Column("id").Scan(ctx, &uid) == nil && uid != uuid.Nil { + s.j.Set(ctx, fmt.Sprintf(defaultApiKeyResolverKey, apiKey), uid, defaultUserTtl) } return } -func (s *userStore) UserExists(ctx context.Context, id uuid.UUID) bool { - s.populateUserList(ctx) - return s.j.ZScore(ctx, defaultUserListKey, id.String()).Err() == nil +func (s *userStore) UserExists(ctx context.Context, uid uuid.UUID) bool { + return s.j.SIsMember(ctx, defaultUserListKey, uid.String()).Val() } -func (s *userStore) fallbackOne(ctx context.Context, id uuid.UUID, handle string) *user.User { - u := s.loadOne(ctx, id, handle) - if u != nil && u.ID != uuid.Nil { - handle = u.Handle - s.j.JTxPipelined(ctx, func(r *rejson.ReJSON) error { - if e := r.Set(ctx, fmt.Sprintf(defaultHandleToIdResolver, handle), u.ID.String(), defaultUserTtl).Err(); e != nil { - return e - } - k := fmt.Sprintf(defaultUserKey, u.ID) - if e := r.JSONSet(ctx, k, "$", u); e != nil { - return e - } - return r.Expire(ctx, k, defaultUserTtl).Err() - }) - return u - } - return nil -} - -func (s *userStore) fallbackMulti(ctx context.Context, users []user.User) (u []user.User) { - u = s.loadMulti(ctx, users) - s.j.JTxPipelined(ctx, func(r *rejson.ReJSON) error { - for _, _u := range u { - if _u.ID != uuid.Nil { - k := fmt.Sprintf(defaultUserKey, _u.ID) - if r.Set(ctx, fmt.Sprintf(defaultHandleToIdResolver, _u.Handle), _u.ID.String(), defaultUserTtl).Err() == nil && - r.JSONSet(ctx, k, "$", _u) == nil { - r.Expire(ctx, k, defaultUserTtl) - } +func (s *userStore) populateUserList(ctx context.Context) { + _, e, _ := s.s.Do("populate", func() (interface{}, error) { + if s.j.Exists(ctx, defaultUserListKey).Val() == 0 { + var ids []interface{} + if _e := db.Database.NewSelect().Model((*user.User)(nil)).Column("id").Scan(ctx, &ids); _e != nil { + return nil, _e } + s.j.SAdd(ctx, defaultUserListKey, ids...) } - return nil + return nil, nil }) - return + logger.Panic(e, "failed to populate user cache") } -func (s *userStore) Get(ctx context.Context, id uuid.UUID, handle string) *user.User { - if handle == "" && id == uuid.Nil { - return nil - } - if id == uuid.Nil { - if _id, e := s.j.Get(ctx, fmt.Sprintf(defaultHandleToIdResolver, handle)).Result(); e == nil && _id != "" && _id != uuid.Nil.String() { - id, e = uuid.Parse(_id) - if e != nil { - return s.fallbackOne(ctx, id, "") - } - goto getFromCache - } - return s.fallbackOne(ctx, uuid.Nil, handle) - } - if !s.UserExists(ctx, id) { - return nil - } -getFromCache: - r := s.j.JSONGet(ctx, fmt.Sprintf(defaultUserKey, id)) - _u := rejson.Unmarshal[user.User](r) - if _u == nil { - return s.fallbackOne(ctx, id, "") - } - return _u +func (s *userStore) Add(uid string) { + s.j.SAdd(context.Background(), defaultUserListKey, uid) } -func userToMinimalUser(u *user.User) *user.MinimalUser { - if u == nil { - return nil - } - var topRole interface{} = nil - if len(u.Roles) > 0 { - topRole = u.Roles[0] - } - return &user.MinimalUser{ - ID: u.ID.String(), - DisplayName: u.DisplayName, - Handle: u.Handle, - Avatar: u.Avatar, - Organization: u.Organization, - TopRole: topRole, - Rating: u.Rating, - } -} +// +//func (s *userStore) loadOne(ctx context.Context, id uuid.UUID, handle string) (u *user.User) { +// u = new(user.User) +// q := db.Database.NewSelect().Model(u).ExcludeColumn("password", "api_key") +// if id == uuid.Nil { +// q = q.Where("handle = ?", handle) +// } else { +// q = q.Where("id = ?", id) +// } +// if e := q.Relation("Connections", func(query *bun.SelectQuery) *bun.SelectQuery { +// return query.Where("show_in_profile = true") +// }).Relation("Roles", func(query *bun.SelectQuery) *bun.SelectQuery { +// return query.Order("priority ASC").Column("name", "icon", "color") +// }).Relation("Organizations").Scan(ctx); e != nil { +// return nil +// } +// if strings.TrimSpace(u.Email) != "" { +// h := md5.Sum([]byte(u.Email)) +// u.Avatar = hex.EncodeToString(h[:]) +// } +// if len(u.Roles) > 0 { +// u.Role = &u.Roles[0] +// } +// return +//} +// +//func (s *userStore) loadMulti(ctx context.Context, users []user.User) (u []user.User) { +// if db.Database.NewSelect(). +// // users are not selected by order of ids but by order in the database, so I have to use this hack to ensure stable order +// // might not be performance-wise but at least it does the trick lol +// With("inp", db.Database.NewValues(&users).Column("id").WithOrder()). +// Model(&u).ExcludeColumn("password", "api_key"). +// Table("inp"). +// Where("\"user\".id = inp.id"). +// OrderExpr("inp._order"). +// Relation("Connections", func(query *bun.SelectQuery) *bun.SelectQuery { +// return query.Where("show_in_profile = true") +// }). +// Relation("Roles", func(query *bun.SelectQuery) *bun.SelectQuery { +// return query.Order("priority ASC").Column("name", "icon", "color") +// }). +// Relation("Organizations").Scan(ctx) != nil { +// return nil +// } +// for i := range u { +// if strings.TrimSpace(u[i].Email) != "" { +// h := md5.Sum([]byte(u[i].Email)) +// u[i].Avatar = hex.EncodeToString(h[:]) +// } +// if len(u[i].Roles) > 0 { +// u[i].Role = &u[i].Roles[0] +// } +// } +// return +//} +// +//func (s *userStore) UserExists(ctx context.Context, id uuid.UUID) bool { +// s.populateUserList(ctx) +// return s.j.ZScore(ctx, defaultUserListKey, id.String()).Err() == nil +//} +// +//func (s *userStore) fallbackOne(ctx context.Context, id uuid.UUID, handle string) *user.User { +// u := s.loadOne(ctx, id, handle) +// if u != nil && u.ID != uuid.Nil { +// handle = u.Handle +// s.j.JTxPipelined(ctx, func(r *rejson.ReJSON) error { +// if e := r.Set(ctx, fmt.Sprintf(defaultHandleToIdResolver, handle), u.ID.String(), defaultUserTtl).Err(); e != nil { +// return e +// } +// k := fmt.Sprintf(defaultUserKey, u.ID) +// if e := r.JSONSet(ctx, k, "$", u); e != nil { +// return e +// } +// return r.Expire(ctx, k, defaultUserTtl).Err() +// }) +// return u +// } +// return nil +//} +// +//func (s *userStore) fallbackMulti(ctx context.Context, users []user.User) (u []user.User) { +// u = s.loadMulti(ctx, users) +// s.j.JTxPipelined(ctx, func(r *rejson.ReJSON) error { +// for _, _u := range u { +// if _u.ID != uuid.Nil { +// k := fmt.Sprintf(defaultUserKey, _u.ID) +// if r.Set(ctx, fmt.Sprintf(defaultHandleToIdResolver, _u.Handle), _u.ID.String(), defaultUserTtl).Err() == nil && +// r.JSONSet(ctx, k, "$", _u) == nil { +// r.Expire(ctx, k, defaultUserTtl) +// } +// } +// } +// return nil +// }) +// return +//} +// +//func (s *userStore) Get(ctx context.Context, id uuid.UUID, handle string) *user.User { +// if handle == "" && id == uuid.Nil { +// return nil +// } +// if id == uuid.Nil { +// if _id, e := s.j.Get(ctx, fmt.Sprintf(defaultHandleToIdResolver, handle)).Result(); e == nil && _id != "" && _id != uuid.Nil.String() { +// id, e = uuid.Parse(_id) +// if e != nil { +// return s.fallbackOne(ctx, id, "") +// } +// goto getFromCache +// } +// return s.fallbackOne(ctx, uuid.Nil, handle) +// } +// if !s.UserExists(ctx, id) { +// return nil +// } +//getFromCache: +// r := s.j.JSONGet(ctx, fmt.Sprintf(defaultUserKey, id)) +// _u := rejson.Unmarshal[user.User](r) +// if _u == nil { +// return s.fallbackOne(ctx, id, "") +// } +// return _u +//} +// +//func userToMinimalUser(u *user.User) *user.MinimalUser { +// if u == nil { +// return nil +// } +// var ( +// topRole interface{} = nil +// org interface{} = nil +// ) +// if len(u.Roles) > 0 { +// topRole = u.Roles[0] +// } +// if len(u.Organizations) > 0 { +// org = u.Organizations[0] +// } +// return &user.MinimalUser{ +// ID: u.ID.String(), +// DisplayName: u.DisplayName, +// Handle: u.Handle, +// Avatar: u.Avatar, +// Role: topRole, +// Organization: org, +// Rating: u.Rating, +// } +//} +// +//func (s *userStore) GetMinimal(ctx context.Context, id uuid.UUID) *user.MinimalUser { +// if id == uuid.Nil { +// return nil +// } +// if !s.UserExists(ctx, id) { +// return nil +// } +// r := s.j.JSONGet(ctx, fmt.Sprintf(defaultUserKey, id), "$..['id','displayName','handle','organization','avatar','topRole','rating']") +// res := rejson.Unmarshal[[]interface{}](r) +// if res == nil || len(*res) != 7 { +// return userToMinimalUser(s.fallbackOne(ctx, id, "")) +// } +// _res := *res +// return &user.MinimalUser{ +// ID: _res[0].(string), +// DisplayName: _res[1].(string), +// Handle: _res[2].(string), +// Organization: _res[3], +// Avatar: _res[4].(string), +// Role: _res[5], +// Rating: uint16(_res[6].(float64)), +// } +//} +// +//func (s *userStore) UserCount(ctx context.Context) uint32 { +// return uint32(s.j.ZCard(ctx, defaultUserListKey).Val()) +//} +// +//func (s *userStore) GetPage(ctx context.Context, page uint32, rev bool) (res []user.MinimalUser) { +// order := "desc" +// if rev { +// order = "asc" +// } +// r, e, _ := s.s.Do(fmt.Sprintf("page-%d-%s", page, order), func() (interface{}, error) { +// s.populateUserList(ctx) +// u := s.j.ZRangeArgs(context.Background(), redis.ZRangeArgs{ +// Key: defaultUserListKey, +// Start: "-inf", +// Stop: "+inf", +// ByScore: true, +// // the leaderboard should be descending by default, so rev should make it ascending instead of descending +// Rev: !rev, +// Offset: int64(page * DefaultUserPageSize), +// Count: DefaultUserPageSize, +// }).Val() +// if len(u) == 0 { +// return nil, errors.New("no users") +// } +// var toGet []interface{} +// for _, z := range u { +// toGet = append(toGet, fmt.Sprintf(defaultUserKey, z)) +// } +// if r := s.j.JSONMGet(ctx, "$..['id','displayName','handle','organization','avatar','topRole','rating']", toGet...); len(r) > 0 { +// res = make([]user.MinimalUser, len(r)) +// var ( +// toLoad []user.User +// indices []int +// ) +// for i := range r { +// _u := rejson.Unmarshal[[]interface{}](r[i]) +// if _u == nil || len(*_u) != 7 { +// toLoad = append(toLoad, user.User{ +// ID: uuid.MustParse(u[i]), +// }) +// indices = append(indices, i) +// continue +// } +// usr := *_u +// res[i] = user.MinimalUser{ +// ID: usr[0].(string), +// DisplayName: usr[1].(string), +// Handle: usr[2].(string), +// Organization: usr[3], +// Avatar: usr[4].(string), +// Role: usr[5], +// Rating: uint16(usr[6].(float64)), +// } +// } +// if len(toLoad) > 0 { +// ul := s.fallbackMulti(ctx, toLoad) +// for i, c := range ul { +// if _u := userToMinimalUser(&c); _u != nil { +// res[indices[i]] = *_u +// } +// } +// } +// return res, nil +// } +// return nil, nil +// }) +// if e != nil { +// return +// } +// if _r, ok := r.([]user.MinimalUser); ok { +// r = _r +// } +// return +//} -func (s *userStore) GetMinimal(ctx context.Context, id uuid.UUID) *user.MinimalUser { - if id == uuid.Nil { - return nil - } - if !s.UserExists(ctx, id) { - return nil - } - r := s.j.JSONGet(ctx, fmt.Sprintf(defaultUserKey, id), "$..['id','displayName','handle','organization','avatar','topRole','rating']") - res := rejson.Unmarshal[[]interface{}](r) - if res == nil || len(*res) != 7 { - return userToMinimalUser(s.fallbackOne(ctx, id, "")) - } - _res := *res - return &user.MinimalUser{ - ID: _res[0].(string), - DisplayName: _res[1].(string), - Handle: _res[2].(string), - Organization: _res[3].(string), - Avatar: _res[4].(string), - TopRole: _res[5], - Rating: uint16(_res[6].(float64)), +func (*userStore) GetPage(ctx context.Context, page uint32) (n int, users []user.User) { + var e error + n, e = db.Database.NewSelect(). + Model(&users). + Column("id", "display_name", "handle", "email", "banned_since", "rating"). + ColumnExpr("MD5(email) AS avatar"). + Relation("Roles", func(query *bun.SelectQuery) *bun.SelectQuery { + // get the role with the highest priority + return query.Column("name", "color", "icon").Order("priority DESC").Limit(1) + }). + Relation("Organizations", func(query *bun.SelectQuery) *bun.SelectQuery { + // get the organization with the earliest date of joining + return query.Order("joined_at ASC").Limit(1). + ExcludeColumn("description") + }). + Offset(int((page - 1) * DefaultUserPageSize)). + Limit(DefaultUserPageSize). + Order("rating DESC"). + ScanAndCount(ctx) + if e != nil { + logger.Blizzard.Error().Err(e). + Uint32("page", page). + Msg("error querying for users") + return -1, nil } + return } -func (s *userStore) UserCount(ctx context.Context) uint32 { - return uint32(s.j.ZCard(ctx, defaultUserListKey).Val()) -} +//func (*userStore) UserExists(ctx context.Context, id uuid.UUID) bool { +// exists, _ := db.Database.NewSelect().Model(&user.User{ +// ID: id, +// }).WherePK().Exists(ctx) +// return exists +//} -func (s *userStore) GetPage(ctx context.Context, page uint32, rev bool) (res []user.MinimalUser) { - order := "desc" - if rev { - order = "asc" +func (*userStore) Get(ctx context.Context, id uuid.UUID, handle string, columns ...string) (u *user.User) { + u = new(user.User) + if len(columns) == 0 { + columns = []string{"id", "display_name", "handle", "email", "registered_at", "banned_since", "rating"} } - r, e, _ := s.s.Do(fmt.Sprintf("page-%d-%s", page, order), func() (interface{}, error) { - s.populateUserList(ctx) - u := s.j.ZRangeArgs(context.Background(), redis.ZRangeArgs{ - Key: defaultUserListKey, - Start: "-inf", - Stop: "+inf", - ByScore: true, - // the leaderboard should be descending by default, so rev should make it ascending instead of descending - Rev: !rev, - Offset: int64(page * DefaultUserPageSize), - Count: DefaultUserPageSize, - }).Val() - if len(u) == 0 { - return nil, errors.New("no users") - } - var toGet []interface{} - for _, z := range u { - toGet = append(toGet, fmt.Sprintf(defaultUserKey, z)) - } - if r := s.j.JSONMGet(ctx, "$..['id','displayName','handle','organization','avatar','topRole','rating']", toGet...); len(r) > 0 { - res = make([]user.MinimalUser, len(r)) - var ( - toLoad []user.User - indices []int - ) - for i := range r { - _u := rejson.Unmarshal[[]interface{}](r[i]) - if _u == nil || len(*_u) != 7 { - toLoad = append(toLoad, user.User{ - ID: uuid.MustParse(u[i]), - }) - indices = append(indices, i) - continue - } - usr := *_u - res[i] = user.MinimalUser{ - ID: usr[0].(string), - DisplayName: usr[1].(string), - Handle: usr[2].(string), - Organization: usr[3].(string), - Avatar: usr[4].(string), - TopRole: usr[5], - Rating: uint16(usr[6].(float64)), - } - } - if len(toLoad) > 0 { - ul := s.fallbackMulti(ctx, toLoad) - for i, c := range ul { - if _u := userToMinimalUser(&c); _u != nil { - res[indices[i]] = *_u - } - } - } - return res, nil - } - return nil, nil - }) - if e != nil { - return + handle = strings.TrimSpace(handle) + if id == uuid.Nil && handle == "" { + return nil } - if _r, ok := r.([]user.MinimalUser); ok { - r = _r + if e := db.Database.NewSelect(). + Model(u). + Column(columns...). + ColumnExpr("MD5(email) AS avatar"). + Relation("Roles", func(query *bun.SelectQuery) *bun.SelectQuery { + return query.Order("priority DESC") + }). + Relation("Organizations", func(query *bun.SelectQuery) *bun.SelectQuery { + return query.Order("joined_at ASC") + }). + Where("id = ?", id). + WhereOr("handle = ?", handle). + Scan(ctx); e != nil { + logger.Blizzard.Error().Err(e). + Stringer("id", id). + Str("handle", handle). + Msg("error querying for user") + return nil } return } diff --git a/config/config.go b/config/config.go index f52bed3..d310db1 100644 --- a/config/config.go +++ b/config/config.go @@ -5,44 +5,47 @@ import ( "github.com/rs/zerolog" "gopkg.in/yaml.v3" "os" + "strings" ) type ( storageType string config struct { - Host string - Port uint16 - Brand string - Blizzard blizzardConfig - Polar polarConfig - Debug bool `yaml:"-"` - Orca orcaConfig + Host string `yaml:"host"` + Port uint16 `yaml:"port"` + Brand string `yaml:"brand"` + Blizzard blizzardConfig `yaml:"blizzard"` + Polar polarConfig `yaml:"polar"` + Debug bool `yaml:"-"` + Orca orcaConfig `yaml:"orca"` } polarConfig struct { - Port uint16 - Password string + Port uint16 `yaml:"port"` + Secret string `yaml:"secret"` + CertFile string `yaml:"certFile"` + KeyFile string `yaml:"keyFile"` } blizzardConfig struct { - PrivateKey string `yaml:"privateKey"` - EnableCORS bool `json:"enableCors"` - RateLimit uint32 `yaml:"rateLimit"` - Database databaseConfig - Storage map[storageType]string - OAuth map[string]oauthProvider - Dragonfly dragonflyConfig + Secret string `yaml:"secret"` + EnableCORS bool `json:"enableCors"` + RateLimit uint32 `yaml:"rateLimit"` + Database databaseConfig `yaml:"database"` + Storage map[storageType]string `yaml:"storage"` + OAuth map[string]oauthProvider `yaml:"oauth"` + Dragonfly dragonflyConfig `yaml:"dragonfly"` } orcaConfig struct { - Token string - Guild string + Token string `yaml:"token"` + Guild string `yaml:"guild"` } dragonflyConfig struct { - Host string - Port uint16 - Password string + Host string `yaml:"host"` + Port uint16 `yaml:"port"` + Password string `yaml:"password"` } oauthProvider struct { @@ -51,12 +54,12 @@ type ( } databaseConfig struct { - Host string - Port uint16 - Username string - Password string - Name string - Secure bool + Host string `yaml:"host"` + Port uint16 `yaml:"port"` + Username string `yaml:"username"` + Password string `yaml:"password"` + Name string `yaml:"name"` + Secure bool `yaml:"secure"` } ) @@ -70,7 +73,11 @@ const ( ) func init() { - b, e := os.ReadFile("arctic.yml") + confPath := strings.TrimSpace(os.Getenv("ARCTIC_CONFIG_PATH")) + if confPath == "" { + confPath = "arctic.yml" + } + b, e := os.ReadFile(confPath) logger.Panic(e, "could not read config file") logger.Panic(yaml.Unmarshal(b, &Config), "failed to parse config file") Config.Debug = os.Getenv("ENV") == "dev" diff --git a/db/models/contest/submission.go b/db/models/contest/submission.go deleted file mode 100644 index 525452a..0000000 --- a/db/models/contest/submission.go +++ /dev/null @@ -1,52 +0,0 @@ -package contest - -import ( - "github.com/ArcticOJ/blizzard/v0/db/models/user" - "github.com/google/uuid" - "time" -) - -type ( - Submission struct { - ID uint32 `bun:",pk,autoincrement" json:"id" json:"-"` - AuthorID uuid.UUID `bun:",type:uuid" json:"-"` - // file extension of the source code, we're using extension instead of full path because source code file name pattern is static except for the extension - Extension string `bun:",notnull" json:"extension"` - ProblemID string `json:"problemId"` - Problem *Problem `bun:"rel:belongs-to,join:problem_id=id" json:"problem,omitempty"` - Runtime string `json:"runtime"` - SubmittedAt time.Time `bun:",nullzero,notnull,default:'now()'" json:"submittedAt"` - Results []CaseResult `json:"results"` - TimeTaken float32 `json:"timeTaken"` - TotalMemory uint64 `json:"totalMemory"` - Verdict Verdict `json:"verdict"` - Points float64 `json:"points"` - CompilerOutput string `json:"compilerOutput"` - Author *user.User `bun:"rel:belongs-to,join:author_id=id" json:"author,omitempty"` - } - - Verdict = string - CaseResult struct { - ID uint16 `json:"id"` - Message string `json:"message,omitempty"` - Verdict Verdict `json:"verdict"` - Memory uint32 `json:"memory"` - Duration float32 `json:"duration"` - } -) - -const ( - VerdictNone Verdict = "" - VerdictAccepted = "AC" - VerdictPartiallyAccepted = "PA" - VerdictWrongAnswer = "WA" - VerdictInternalError = "IE" - VerdictRejected = "RJ" - VerdictCancelled = "CL" - VerdictRuntimeError = "RTE" - VerdictTimeLimitExceeded = "TLE" - VerdictMemoryLimitExceeded = "MLE" - VerdictOutputLimitExceeded = "OLE" - VerdictStackLimitExceeded = "SLE" - VerdictCompileError = "CE" -) diff --git a/db/models/post/comment.go b/db/models/post/comment.go deleted file mode 100644 index 203a420..0000000 --- a/db/models/post/comment.go +++ /dev/null @@ -1,17 +0,0 @@ -package post - -import ( - "github.com/ArcticOJ/blizzard/v0/db/models/user" - "github.com/google/uuid" - "time" -) - -type Comment struct { - ID uint32 `bun:",pk,autoincrement" json:"id"` - CommentedAt *time.Time `bun:",nullzero,notnull,default:'now()'" json:"commentedAt,omitempty"` - AuthorID uuid.UUID `bun:",type:uuid" json:"-"` - Author *user.User `bun:"rel:has-one,join:author_id=id" json:"author,omitempty"` - PostID uint32 - Post *Post `bun:"rel:belongs-to,join:post_id=id"` - Content string -} diff --git a/db/models/post/post.go b/db/models/post/post.go deleted file mode 100644 index 997ee29..0000000 --- a/db/models/post/post.go +++ /dev/null @@ -1,16 +0,0 @@ -package post - -import ( - "github.com/ArcticOJ/blizzard/v0/db/models/user" - "github.com/google/uuid" - "time" -) - -type Post struct { - ID uint32 `bun:",pk,autoincrement" json:"id"` - Title string `bun:",notnull" json:"title"` - PostedAt *time.Time `bun:",nullzero,notnull,default:'now()'" json:"postedAt,omitempty"` - AuthorID uuid.UUID `bun:",type:uuid" json:"authorID,omitempty"` - Author *user.User `bun:"rel:has-one,join:author_id=id" json:"-"` - Comments []Comment `bun:"rel:has-many,join:id=author_id" json:"comments,omitempty"` -} diff --git a/db/models/user/role.go b/db/models/user/role.go deleted file mode 100644 index 3c0c6b1..0000000 --- a/db/models/user/role.go +++ /dev/null @@ -1,15 +0,0 @@ -package user - -import ( - "github.com/ArcticOJ/blizzard/v0/permission" -) - -type Role struct { - ID uint16 `bun:",pk,autoincrement" json:"id,omitempty"` - Name string `bun:",unique,notnull" json:"name,omitempty"` - Permissions permission.Permission `bun:",default:0" json:"permissions,omitempty"` - Icon string `json:"icon"` - Color string `json:"color"` - Priority uint16 `bun:",notnull,default:1000" json:"priority,omitempty"` - Members []User `bun:"m2m:user_to_roles,join:Role=User" json:"members,omitempty"` -} diff --git a/db/models/user/user.go b/db/models/user/user.go deleted file mode 100644 index 8d13804..0000000 --- a/db/models/user/user.go +++ /dev/null @@ -1,43 +0,0 @@ -package user - -import ( - "github.com/google/uuid" - "time" -) - -type ( - MinimalUser struct { - ID string `json:"id,omitempty"` - DisplayName string `json:"displayName,omitempty"` - Handle string `json:"handle,omitempty"` - Avatar string `json:"avatar,omitempty"` - Organization string `json:"organization,omitempty"` - TopRole interface{} `json:"topRole,omitempty"` - Rating uint16 `json:"rating"` - } - User struct { - ID uuid.UUID `bun:",pk,unique,type:uuid,default:gen_random_uuid()" json:"id"` - DisplayName string `json:"displayName"` - Handle string `bun:",notnull,unique" json:"handle"` - Email string `bun:",notnull,unique" json:"-"` - EmailVerified bool `bun:",default:false" json:"emailVerified,omitempty"` - Avatar string `bun:"-" json:"avatar"` - Password string `bun:",notnull" json:"-"` - Organization string `json:"organization"` - RegisteredAt *time.Time `bun:",nullzero,notnull,default:'now()'" json:"registeredAt,omitempty"` - ApiKey string `json:"-"` - Connections []OAuthConnection `bun:"rel:has-many,join:id=user_id" json:"connections"` - Roles []Role `bun:"m2m:user_to_roles,join:User=Role" json:"roles"` - TopRole *Role `bun:"-" json:"topRole"` - DeletedAt *time.Time `bun:",soft_delete,nullzero" json:"deletedAt,omitempty"` - ProblemsSolved uint16 `bun:",scanonly" json:"problemsSolved"` - Rating uint16 `bun:",default:0" json:"rating"` - } - - UserToRole struct { - RoleID uint16 `bun:",pk"` - Role *Role `bun:"rel:belongs-to,join:role_id=id"` - UserID uuid.UUID `bun:",pk,type:uuid"` - User *User `bun:"rel:belongs-to,join:user_id=id"` - } -) diff --git a/db/models/contest/contest.go b/db/schema/contest/contest.go similarity index 87% rename from db/models/contest/contest.go rename to db/schema/contest/contest.go index 39aaeca..b7ed490 100644 --- a/db/models/contest/contest.go +++ b/db/schema/contest/contest.go @@ -1,12 +1,12 @@ package contest import ( - "github.com/ArcticOJ/blizzard/v0/db/models/user" + "github.com/ArcticOJ/blizzard/v0/db/schema/user" ) type ( Contest struct { - ID uint32 `bun:",pk,autoincrement" json:"id"` + ID uint32 `bun:",pk" json:"id"` Title string `bun:",notnull" json:"title"` Tags []string `bun:",array" json:"tags"` Organizers []user.User `bun:"m2m:contest_to_organizers,join:Contest=User"` diff --git a/db/models/contest/problem.go b/db/schema/contest/problem.go similarity index 79% rename from db/models/contest/problem.go rename to db/schema/contest/problem.go index 350ec97..2cb4c77 100644 --- a/db/models/contest/problem.go +++ b/db/schema/contest/problem.go @@ -1,7 +1,7 @@ package contest import ( - "github.com/ArcticOJ/blizzard/v0/db/models/user" + "github.com/ArcticOJ/blizzard/v0/db/schema/user" "github.com/google/uuid" ) @@ -11,14 +11,14 @@ type ( Tags []string `bun:",array" json:"tags"` Source string `json:"source"` AuthorID uuid.UUID `bun:",type:uuid" json:"authorID,omitempty"` - Author *user.User `bun:"rel:has-one,join:author_id=id" json:"-"` + Author *user.User `bun:",rel:has-one,join:author_id=id" json:"-"` Title string `bun:",notnull" json:"title,omitempty"` ContentType ContentType `json:"contentType"` - Content interface{} `bun:"type:jsonb" json:"content,omitempty"` - Constraints *Constraints `bun:"embed:" json:"constraints,omitempty"` + Content interface{} `bun:",type:jsonb" json:"content,omitempty"` + Constraints *Constraints `bun:",embed:" json:"constraints,omitempty"` TestCount uint16 `bun:",notnull" json:"testCount,omitempty"` PointsPerTest float64 `bun:",default:1" json:"pointPerTest,omitempty"` - Submissions []Submission `bun:"rel:has-many,join:id=problem_id" json:"submissions,omitempty"` + Submissions []Submission `bun:",rel:has-many,join:id=problem_id" json:"submissions,omitempty"` } SampleTestCases struct { diff --git a/db/schema/contest/submission.go b/db/schema/contest/submission.go new file mode 100644 index 0000000..0614bd3 --- /dev/null +++ b/db/schema/contest/submission.go @@ -0,0 +1,51 @@ +package contest + +import ( + "github.com/ArcticOJ/blizzard/v0/db/schema/user" + "github.com/google/uuid" + "time" +) + +type ( + Submission struct { + ID uint32 `bun:",pk,autoincrement" json:"id" json:"-"` + AuthorID uuid.UUID `bun:",type:uuid" json:"-"` + FileName string `bun:",notnull" json:"-"` + ProblemID string `json:"problemId"` + Problem *Problem `bun:",rel:belongs-to,join:problem_id=id" json:"problem,omitempty"` + Runtime string `json:"runtime"` + SubmittedAt time.Time `bun:",nullzero,notnull,default:current_timestamp" json:"submittedAt"` + Results []CaseResult `json:"results"` + TimeTaken float32 `json:"timeTaken"` + TotalMemory uint64 `json:"totalMemory"` + Verdict Verdict `json:"verdict"` + Points float64 `json:"points"` + CompilerOutput string `json:"compilerOutput"` + Author *user.User `bun:",rel:belongs-to,join:author_id=id" json:"author,omitempty"` + } + + Verdict = string + CaseResult struct { + ID uint16 `json:"id"` + Message string `json:"message,omitempty"` + Verdict Verdict `json:"verdict"` + Memory uint32 `json:"memory"` + Duration float32 `json:"duration"` + } +) + +const ( + VerdictNone Verdict = "" + VerdictAccepted Verdict = "AC" + VerdictPartiallyAccepted Verdict = "PA" + VerdictWrongAnswer Verdict = "WA" + VerdictInternalError Verdict = "IE" + VerdictRejected Verdict = "RJ" + VerdictCancelled Verdict = "CL" + VerdictRuntimeError Verdict = "RTE" + VerdictTimeLimitExceeded Verdict = "TLE" + VerdictMemoryLimitExceeded Verdict = "MLE" + VerdictOutputLimitExceeded Verdict = "OLE" + VerdictStackLimitExceeded Verdict = "SLE" + VerdictCompileError Verdict = "CE" +) diff --git a/db/schema/post/comment.go b/db/schema/post/comment.go new file mode 100644 index 0000000..ba39362 --- /dev/null +++ b/db/schema/post/comment.go @@ -0,0 +1,17 @@ +package post + +import ( + "github.com/ArcticOJ/blizzard/v0/db/schema/user" + "github.com/google/uuid" + "time" +) + +type Comment struct { + ID uint32 `bun:",pk,autoincrement" json:"id"` + CommentedAt *time.Time `bun:",notnull,default:current_timestamp" json:"commentedAt,omitempty"` + AuthorID uuid.UUID `bun:",type:uuid" json:"-"` + Author *user.User `bun:",rel:has-one,join:author_id=id" json:"author,omitempty"` + PostID uint32 + Post *Post `bun:",rel:belongs-to,join:post_id=id"` + Content string +} diff --git a/db/schema/post/post.go b/db/schema/post/post.go new file mode 100644 index 0000000..33ec93f --- /dev/null +++ b/db/schema/post/post.go @@ -0,0 +1,16 @@ +package post + +import ( + "github.com/ArcticOJ/blizzard/v0/db/schema/user" + "github.com/google/uuid" + "time" +) + +type Post struct { + ID uint32 `bun:",pk,autoincrement" json:"id"` + Title string `bun:",notnull" json:"title"` + PostedAt *time.Time `bun:",notnull,default:current_timestamp" json:"postedAt,omitempty"` + AuthorID uuid.UUID `bun:",type:uuid" json:"authorID,omitempty"` + Author *user.User `bun:",rel:has-one,join:author_id=id" json:"-"` + Comments []Comment `bun:",rel:has-many,join:id=author_id" json:"comments,omitempty"` +} diff --git a/db/models/user/oauth.go b/db/schema/user/oauth.go similarity index 69% rename from db/models/user/oauth.go rename to db/schema/user/oauth.go index 9135081..68e1489 100644 --- a/db/models/user/oauth.go +++ b/db/schema/user/oauth.go @@ -8,10 +8,9 @@ import ( ) type OAuthConnection struct { - ID string `bun:",pk" json:"-"` - Provider string `bun:",pk,unique:provider,notnull" json:"provider"` + Provider string `bun:",pk,notnull" json:"provider"` Username string `bun:",notnull" json:"username"` - UserID uuid.UUID `bun:",type:uuid,unique:provider" json:"-"` + UserID uuid.UUID `bun:",pk,type:uuid,notnull" json:"-"` ShowInProfile bool `bun:",default:true" json:"-"` } diff --git a/db/schema/user/org.go b/db/schema/user/org.go new file mode 100644 index 0000000..d9dfd0f --- /dev/null +++ b/db/schema/user/org.go @@ -0,0 +1,32 @@ +package user + +import ( + "github.com/google/uuid" + "time" +) + +type ( + Organization struct { + ID string `bun:",pk" json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Members []User `bun:",m2m:org_memberships,join:Organization=User" json:"members,omitempty"` + } + + OrgRole uint8 + + OrgMembership struct { + OrgID string `bun:",pk"` + Organization *Organization `bun:",rel:belongs-to,join:org_id=id"` + Role OrgRole `bun:",default:0"` + JoinedAt time.Time `bun:",notnull,default:current_timestamp" json:"joinedAt"` + UserID uuid.UUID `bun:",pk,type:uuid"` + User *User `bun:",rel:belongs-to,join:user_id=id"` + } +) + +const ( + OrgMember OrgRole = iota + OrgAdmin + OrgOwner +) diff --git a/db/schema/user/role.go b/db/schema/user/role.go new file mode 100644 index 0000000..6c09499 --- /dev/null +++ b/db/schema/user/role.go @@ -0,0 +1,25 @@ +package user + +import ( + "github.com/ArcticOJ/blizzard/v0/permission" + "github.com/google/uuid" +) + +type ( + Role struct { + ID uint16 `bun:",pk,autoincrement" json:"id,omitempty"` + Name string `bun:",unique,notnull" json:"name,omitempty"` + Permissions permission.Permission `bun:",default:0" json:"permissions,omitempty"` + Icon string `json:"icon"` + Color string `json:"color"` + Priority uint16 `bun:",notnull,default:1000,unique" json:"priority,omitempty"` + Members []User `bun:",m2m:role_memberships,join:Role=User" json:"members,omitempty"` + } + + RoleMembership struct { + RoleID uint16 `bun:",pk"` + Role *Role `bun:",rel:belongs-to,join:role_id=id"` + UserID uuid.UUID `bun:",pk,type:uuid"` + User *User `bun:",rel:belongs-to,join:user_id=id"` + } +) diff --git a/db/schema/user/user.go b/db/schema/user/user.go new file mode 100644 index 0000000..cf8bb03 --- /dev/null +++ b/db/schema/user/user.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/google/uuid" + "time" +) + +type ( + User struct { + ID uuid.UUID `bun:",pk,type:uuid,default:gen_random_uuid()" json:"id"` + DisplayName string `json:"displayName"` + Handle string `bun:",notnull,unique" json:"handle"` + Email string `bun:",notnull,unique" json:"-"` + EmailVerified bool `bun:",default:false" json:"emailVerified,omitempty"` + // TODO: cache avatar hashes + Avatar string `bun:",scanonly" json:"avatar"` + Password string `bun:",notnull" json:"-"` + Organizations []Organization `bun:",m2m:org_memberships,join:User=Organization" json:"organizations,omitempty"` + RegisteredAt *time.Time `bun:",notnull,default:current_timestamp" json:"registeredAt,omitempty"` + ApiKey string `json:"-"` + Connections []OAuthConnection `bun:",rel:has-many,join:id=user_id" json:"connections,omitempty"` + Roles []Role `bun:",m2m:role_memberships,join:User=Role" json:"roles,omitempty"` + BannedSince *time.Time `bun:",soft_delete" json:"deletedAt,omitempty"` + Rating uint16 `bun:",default:0" json:"rating"` + } +) diff --git a/db/seed/seed.go b/db/seed/seed.go index 32e2a9f..e428c73 100644 --- a/db/seed/seed.go +++ b/db/seed/seed.go @@ -2,22 +2,24 @@ package seed import ( "context" - "github.com/ArcticOJ/blizzard/v0/db/models/contest" - "github.com/ArcticOJ/blizzard/v0/db/models/post" - "github.com/ArcticOJ/blizzard/v0/db/models/user" + "github.com/ArcticOJ/blizzard/v0/db/schema/contest" + "github.com/ArcticOJ/blizzard/v0/db/schema/post" + "github.com/ArcticOJ/blizzard/v0/db/schema/user" "github.com/uptrace/bun" ) var intermediaryModels = []any{ (*contest.ContestToOrganizer)(nil), (*contest.ContestToProblem)(nil), - (*user.UserToRole)(nil), + (*user.RoleMembership)(nil), + (*user.OrgMembership)(nil), } var models = []any{ (*user.User)(nil), (*user.OAuthConnection)(nil), (*user.Role)(nil), + (*user.Organization)(nil), (*contest.Contest)(nil), (*contest.Problem)(nil), diff --git a/go.mod b/go.mod index 327f6ba..ab31871 100644 --- a/go.mod +++ b/go.mod @@ -4,24 +4,25 @@ go 1.21 require ( aidanwoods.dev/go-paseto v1.5.1 - github.com/go-co-op/gocron/v2 v2.0.0-rc3 + entgo.io/ent v0.12.5 + github.com/go-co-op/gocron/v2 v2.2.4 github.com/go-playground/validator/v10 v10.16.0 - github.com/google/uuid v1.4.0 + github.com/google/uuid v1.6.0 github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa - github.com/labstack/echo/v4 v4.11.3 - github.com/matthewhartstonge/argon2 v0.3.4 + github.com/labstack/echo/v4 v4.11.4 + github.com/matthewhartstonge/argon2 v1.0.0 github.com/mhmtszr/concurrent-swiss-map v1.0.5 github.com/mitchellh/mapstructure v1.5.0 github.com/ravener/discord-oauth2 v0.0.0-20230514095040-ae65713199b3 - github.com/redis/go-redis/v9 v9.3.0 - github.com/rs/zerolog v1.31.0 + github.com/redis/go-redis/v9 v9.4.0 + github.com/rs/zerolog v1.32.0 github.com/stretchr/testify v1.8.4 github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc - github.com/uptrace/bun v1.1.16 - github.com/uptrace/bun/dialect/pgdialect v1.1.16 + github.com/uptrace/bun v1.1.17 + github.com/uptrace/bun/dialect/pgdialect v1.1.17 github.com/uptrace/bun/driver/pgdriver v1.1.16 - golang.org/x/oauth2 v0.12.0 - golang.org/x/sync v0.3.0 + golang.org/x/oauth2 v0.16.0 + golang.org/x/sync v0.6.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -38,24 +39,23 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect github.com/kr/pretty v0.3.1 // indirect - github.com/labstack/gommon v0.4.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect - golang.org/x/time v0.3.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect mellium.im/sasl v0.3.1 // indirect ) diff --git a/go.sum b/go.sum index 9f2d29f..2c3b01a 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ aidanwoods.dev/go-paseto v1.5.1 h1:IvT7wk7jmeTff6wyk7RlS6uAjUIAKU4MU2hkqr95lCo= aidanwoods.dev/go-paseto v1.5.1/go.mod h1:9J13iCMdWrkfK1AxAg9QDHLaDMYSEP1ldbFiR+DfmVc= aidanwoods.dev/go-result v0.1.0 h1:y/BMIRX6q3HwaorX1Wzrjo3WUdiYeyWbvGe18hKS3K8= aidanwoods.dev/go-result v0.1.0/go.mod h1:yridkWghM7AXSFA6wzx0IbsurIm1Lhuro3rYef8FBHM= +entgo.io/ent v0.12.5 h1:KREM5E4CSoej4zeGa88Ou/gfturAnpUv0mzAjch1sj4= +entgo.io/ent v0.12.5/go.mod h1:Y3JVAjtlIk8xVZYSn3t3mf8xlZIn5SAOXZQxD6kKI+Q= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -17,8 +19,10 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/go-co-op/gocron/v2 v2.0.0-rc3 h1:6HZhwq4rKJUyYH2FcbXeU2n/7zSjTVZMnCFSLQ8N6hQ= -github.com/go-co-op/gocron/v2 v2.0.0-rc3/go.mod h1:3SLoqKnyORFVN0VvFFb1383hM4WD9XHBPn9aUUp7sQs= +github.com/go-co-op/gocron/v2 v2.1.1 h1:vQPaVzCFUbfNTKjLYPCUiLlgE3mJ78XfYCo+CTfutHs= +github.com/go-co-op/gocron/v2 v2.1.1/go.mod h1:0MfNAXEchzeSH1vtkZrTAcSMWqyL435kL6CA4b0bjrg= +github.com/go-co-op/gocron/v2 v2.2.4 h1:fL6a8/U+BJQ9UbaeqKxua8wY02w4ftKZsxPzLSNOCKk= +github.com/go-co-op/gocron/v2 v2.2.4/go.mod h1:igssOwzZkfcnu3m2kwnCf/mYj4SmhP9ecSgmYjCOHkk= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -37,36 +41,34 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM= -github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= -github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= -github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= +github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/matthewhartstonge/argon2 v0.3.4 h1:4TU3B1XWTKoZtTd7z8+fklURHWw6a5du+M7mD35CsXA= -github.com/matthewhartstonge/argon2 v0.3.4/go.mod h1:aLGFnNR9awGSbndX9GCFP3JXn67Sgui7F0pHpJur+vg= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/matthewhartstonge/argon2 v1.0.0 h1:e65fkae6O8Na6YTy2HAccUbXR+GQHOnpQxeWGqWCRIw= +github.com/matthewhartstonge/argon2 v1.0.0/go.mod h1:Fm4FHZxdxCM6hg21Jkz3YZVKnU7VnTlqDQ3ghS/Myok= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mhmtszr/concurrent-swiss-map v1.0.5 h1:kTtd7fXymclRnwNofVI+hFq8pFndehmuXAvlZOVFq/s= github.com/mhmtszr/concurrent-swiss-map v1.0.5/go.mod h1:F6QETL48Qn7jEJ3ZPt7EqRZjAAZu7lRQeQGIzXuUIDc= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -79,6 +81,8 @@ github.com/ravener/discord-oauth2 v0.0.0-20230514095040-ae65713199b3 h1:x3Lgcvuj github.com/ravener/discord-oauth2 v0.0.0-20230514095040-ae65713199b3/go.mod h1:P/mZMYLZ87lqRSECEWsOqywGrO1hlZkk9RTwEw35IP4= github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0= github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk= +github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= @@ -86,11 +90,12 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= +github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -100,48 +105,59 @@ github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYm github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= github.com/uptrace/bun v1.1.16 h1:cn9cgEMFwcyYRsQLfxCRMUxyK1WaHwOVrR3TvzEFZ/A= github.com/uptrace/bun v1.1.16/go.mod h1:7HnsMRRvpLFUcquJxp22JO8PsWKpFQO/gNXqqsuGWg8= +github.com/uptrace/bun v1.1.17 h1:qxBaEIo0hC/8O3O6GrMDKxqyT+mw5/s0Pn/n6xjyGIk= +github.com/uptrace/bun v1.1.17/go.mod h1:hATAzivtTIRsSJR4B8AXR+uABqnQxr3myKDKEf5iQ9U= github.com/uptrace/bun/dialect/pgdialect v1.1.16 h1:eUPZ+YCJ69BA+W1X1ZmpOJSkv1oYtinr0zCXf7zCo5g= github.com/uptrace/bun/dialect/pgdialect v1.1.16/go.mod h1:KQjfx/r6JM0OXfbv0rFrxAbdkPD7idK8VitnjIV9fZI= +github.com/uptrace/bun/dialect/pgdialect v1.1.17 h1:NsvFVHAx1Az6ytlAD/B6ty3cVE6j9Yp82bjqd9R9hOs= +github.com/uptrace/bun/dialect/pgdialect v1.1.17/go.mod h1:fLBDclNc7nKsZLzNjFL6BqSdgJzbj2HdnyOnLoDvAME= github.com/uptrace/bun/driver/pgdriver v1.1.16 h1:b/NiSXk6Ldw7KLfMLbOqIkm4odHd7QiNOCPLqPFJjK4= github.com/uptrace/bun/driver/pgdriver v1.1.16/go.mod h1:Rmfbc+7lx1z/umjMyAxkOHK81LgnGj71XC5YpA6k1vU= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= +golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= -golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= +golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= @@ -154,7 +170,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo= diff --git a/judge/judge.go b/judge/judge.go index 78ebbac..c586439 100644 --- a/judge/judge.go +++ b/judge/judge.go @@ -1,7 +1,7 @@ package judge import ( - "github.com/ArcticOJ/blizzard/v0/db/models/contest" + "github.com/ArcticOJ/blizzard/v0/db/schema/contest" "github.com/ArcticOJ/polar/v0/types" ) diff --git a/judge/worker.go b/judge/observer.go similarity index 56% rename from judge/worker.go rename to judge/observer.go index 17ee8cc..4e1f72b 100644 --- a/judge/worker.go +++ b/judge/observer.go @@ -4,29 +4,26 @@ import ( "container/list" "context" "errors" - "fmt" - "github.com/ArcticOJ/blizzard/v0/core/errs" "github.com/ArcticOJ/blizzard/v0/db" - "github.com/ArcticOJ/blizzard/v0/db/models/contest" + "github.com/ArcticOJ/blizzard/v0/db/schema/contest" "github.com/ArcticOJ/blizzard/v0/logger" - "github.com/ArcticOJ/blizzard/v0/storage" "github.com/ArcticOJ/polar/v0" "github.com/ArcticOJ/polar/v0/types" "github.com/google/uuid" csmap "github.com/mhmtszr/concurrent-swiss-map" "github.com/mitchellh/mapstructure" "github.com/uptrace/bun" - "io" "math" "sync" ) -var Worker *worker +var Observer *observer type ( - worker struct { + observer struct { ctx context.Context // subscribers + m sync.RWMutex sm *csmap.CsMap[uint32, *submissionSubscribers] p *polar.Polar } @@ -40,7 +37,7 @@ func getPendingSubmissions(ctx context.Context) (r []types.Submission) { var s []contest.Submission if db.Database.NewSelect().Model(&s).Relation("Problem", func(query *bun.SelectQuery) *bun.SelectQuery { return query.ExcludeColumn("tags", "source", "author_id", "title", "content_type", "content") - }).Order("submitted_at DESC").Where("results IS ?", nil).Scan(ctx) != nil { + }).Order("submitted_at DESC").Where("verdict = ?", "").Scan(ctx) != nil { return nil } r = make([]types.Submission, len(s)) @@ -49,7 +46,7 @@ func getPendingSubmissions(ctx context.Context) (r []types.Submission) { r[i] = types.Submission{ AuthorID: _s.AuthorID.String(), ID: _s.ID, - SourcePath: fmt.Sprintf("%d.%s", _s.ID, _s.Extension), + SourcePath: _s.FileName, Runtime: _s.Runtime, ProblemID: _s.ProblemID, TestCount: _s.Problem.TestCount, @@ -68,16 +65,18 @@ func getPendingSubmissions(ctx context.Context) (r []types.Submission) { } func Init(ctx context.Context) { - Worker = &worker{ + Observer = &observer{ ctx: ctx, - sm: csmap.Create[uint32, *submissionSubscribers](), + sm: csmap.Create[uint32, *submissionSubscribers](csmap.WithCustomHasher[uint32, *submissionSubscribers](func(key uint32) uint64 { + return uint64(key) + }), csmap.WithShardCount[uint32, *submissionSubscribers](1)), } - Worker.p = polar.NewPolar(ctx, Worker.handleResult) - Worker.p.Populate(getPendingSubmissions(ctx)) - Worker.p.StartServer() + Observer.p = polar.NewPolar(ctx, Observer.handleResult) + Observer.p.Populate(getPendingSubmissions(ctx)) + Observer.p.StartServer() } -func (w *worker) commitToDb(id uint32, cases []contest.CaseResult, fr types.FinalResult, v contest.Verdict, p float64) { +func (o *observer) commitToDb(id uint32, cases []contest.CaseResult, fr types.FinalResult, v contest.Verdict, p float64) { s := contest.Submission{ ID: id, Results: cases, @@ -91,40 +90,35 @@ func (w *worker) commitToDb(id uint32, cases []contest.CaseResult, fr types.Fina s.TotalMemory += uint64(r.Memory) s.TimeTaken += r.Duration } - logger.Panic(db.Database.RunInTx(w.ctx, nil, func(ctx context.Context, tx bun.Tx) error { - _, e := tx.NewUpdate().Model(&s).WherePK().Column("results", "verdict", "points", "compiler_output").Returning("NULL").Exec(w.ctx) - return e - }), "tx") + if _, e := db.Database.NewUpdate().Model(&s).WherePK().Column("results", "verdict", "points", "compiler_output").Returning("NULL").Exec(o.ctx); e != nil { + logger.Blizzard.Error().Err(e).Uint32("id", id).Msg("could not commit results to database") + } } -func (w *worker) Enqueue(sub types.Submission, subscribe bool, path string, f io.Reader) (chan interface{}, *list.Element, error) { +func (o *observer) Enqueue(sub types.Submission, subscribe bool) (chan interface{}, *list.Element, error) { var ( c chan interface{} element *list.Element ) - if !w.p.RuntimeAvailable(sub.Runtime) { - return nil, nil, errs.JudgeNotAvailable - } - if e := storage.Submission.Write(path, f); e != nil { - return nil, nil, e - } - w.sm.Store(sub.ID, &submissionSubscribers{ + o.sm.Store(sub.ID, &submissionSubscribers{ l: list.New(), }) if subscribe { - c, element = w.Subscribe(sub) + c, element = o.Subscribe(sub, func() interface{} { + return nil + }) } - return c, element, w.p.Push(sub, false) + return c, element, o.p.Push(sub, false) } -func (w *worker) Cancel(id uint32, userId uuid.UUID) error { - if !w.p.Cancel(id, userId.String()) { +func (o *observer) Cancel(id uint32, userId uuid.UUID) error { + if !o.p.Cancel(id, userId.String()) { return errors.New("could not cancel specified submission") } return nil } -func (w *worker) handleResult(id uint32) func(t types.ResultType, data interface{}) bool { +func (o *observer) handleResult(id uint32) func(t types.ResultType, data interface{}) bool { lastNonAcVerdict := types.CaseVerdictAccepted return func(t types.ResultType, data interface{}) bool { switch t { @@ -136,23 +130,23 @@ func (w *worker) handleResult(id uint32) func(t types.ResultType, data interface if _r.Verdict != types.CaseVerdictAccepted { lastNonAcVerdict = _r.Verdict } - w.publish(id, _r.CaseID, _r) + o.publish(id, _r.CaseID, _r) case types.ResultFinal: var _r types.FinalResult if mapstructure.Decode(data, &_r) != nil { return false } _r.LastNonACVerdict = lastNonAcVerdict - w.publish(id, math.MaxUint16, _r) + o.publish(id, math.MaxUint16, _r) return true - case types.ResultAnnouncement: - w.publish(id, math.MaxUint16, data.(uint16)) + case types.ResultAck: + o.publish(id, math.MaxUint16, uint16(0)) } return false } } -func (w *worker) publish(id uint32, cid uint16, data interface{}) { +func (o *observer) publish(id uint32, cid uint16, data interface{}) { var d *response = nil switch r := data.(type) { case types.CaseResult: @@ -163,7 +157,7 @@ func (w *worker) publish(id uint32, cid uint16, data interface{}) { Memory: r.Memory, Duration: r.Duration, } - w.p.UpdateResult(id, cr) + o.p.UpdateResult(id, cr) d = &response{ Type: typeCase, Data: cr, @@ -172,28 +166,26 @@ func (w *worker) publish(id uint32, cid uint16, data interface{}) { fv, p := getFinalVerdict(r) d = &response{ Type: typeFinal, - Data: fResult{ + Data: finalJudgement{ CompilerOutput: r.CompilerOutput, Verdict: fv, Points: p, - MaxPoints: r.MaxPoints, }, } - w.commitToDb(id, w.p.GetResult(id), r, fv, p) - defer w.DestroySubscribers(id) + o.commitToDb(id, o.p.GetResults(id), r, fv, p) + defer o.DestroySubscribers(id) case uint16: d = &response{ - Type: typeAnnouncement, - Data: data, + Type: typeAck, } } if d != nil { - subscribers, ok := w.sm.Load(id) - if ok { + if subscribers, ok := o.sm.Load(id); ok { subscribers.m.RLock() for v := subscribers.l.Front(); v != nil; v = v.Next() { select { case v.Value.(chan interface{}) <- d: + default: } } subscribers.m.RUnlock() @@ -201,35 +193,45 @@ func (w *worker) publish(id uint32, cid uint16, data interface{}) { } } -func (w *worker) DestroySubscribers(id uint32) { - subscribers, ok := w.sm.Load(id) +func (o *observer) DestroySubscribers(id uint32) { + subscribers, ok := o.sm.Load(id) if !ok { return } subscribers.m.Lock() - w.sm.Delete(id) - subscribers.m.Unlock() + defer subscribers.m.Unlock() + o.sm.Delete(id) // iterate over linked list and then close + delete all subscribers for v := subscribers.l.Front(); v != nil; v = v.Next() { close(v.Value.(chan interface{})) - subscribers.l.Remove(v) } + subscribers.l.Init() + subscribers = nil } -func (w *worker) Subscribe(sub types.Submission) (chan interface{}, *list.Element) { - subscribers, ok := w.sm.Load(sub.ID) +func (o *observer) Subscribe(sub types.Submission, getData func() interface{}) (chan interface{}, *list.Element) { + subscribers, ok := o.sm.Load(sub.ID) if !ok { return nil, nil } subscribers.m.Lock() + // acknowledgement + n test case results + final result = n + 2 + c := make(chan interface{}, sub.TestCount+2) + c <- response{ + Type: typeManifest, + Data: manifest{ + SubmissionID: sub.ID, + TestCount: sub.TestCount, + MaxPoints: float64(sub.TestCount) * sub.PointsPerTest, + AdditionalData: getData(), + }, + } defer subscribers.m.Unlock() - // maximum number of messages = compile announcement + n test case announcements + n test case results + final result = (n + 1) * 2 - c := make(chan interface{}, (sub.TestCount+1)*2) return c, subscribers.l.PushBack(c) } -func (w *worker) Unsubscribe(id uint32, e *list.Element) { - subscribers, ok := w.sm.Load(id) +func (o *observer) Unsubscribe(id uint32, e *list.Element) { + subscribers, ok := o.sm.Load(id) if !ok { return } @@ -239,10 +241,14 @@ func (w *worker) Unsubscribe(id uint32, e *list.Element) { subscribers.l.Remove(e) } -func (w *worker) RuntimeSupported(rt string) bool { - return w.p.RuntimeAvailable(rt) +func (o *observer) RuntimeSupported(rt string) bool { + return o.p.RuntimeAvailable(rt) +} + +func (o *observer) GetJudges() map[string]*polar.JudgeObj { + return o.p.GetJudges() } -func (w *worker) GetJudges() map[string]*polar.JudgeObj { - return w.p.GetJudges() +func (o *observer) GetResults(id uint32) []contest.CaseResult { + return o.p.GetResults(id) } diff --git a/judge/result.go b/judge/result.go index f0b8a09..bdf90ae 100644 --- a/judge/result.go +++ b/judge/result.go @@ -1,14 +1,15 @@ package judge import ( - "github.com/ArcticOJ/blizzard/v0/db/models/contest" + "github.com/ArcticOJ/blizzard/v0/db/schema/contest" "github.com/ArcticOJ/polar/v0/types" ) const ( - typeAnnouncement responseType = "announcement" - typeCase = "case" - typeFinal = "final" + typeManifest responseType = "manifest" + typeAck responseType = "ack" + typeCase responseType = "case" + typeFinal responseType = "final" ) type ( @@ -16,14 +17,20 @@ type ( // response with type to distinguish response types response struct { Type responseType `json:"type"` - Data interface{} `json:"data"` + Data interface{} `json:"data,omitempty"` } - // final result for responding to clients - fResult struct { + manifest struct { + SubmissionID uint32 `json:"submissionId"` + TestCount uint16 `json:"testCount"` + MaxPoints float64 `json:"maxPoints"` + // for initial payload + AdditionalData interface{} `json:"additionalData,omitempty"` + } + // final judgement for responding to clients + finalJudgement struct { CompilerOutput string `json:"compilerOutput"` Verdict contest.Verdict `json:"verdict"` Points float64 `json:"points"` - MaxPoints float64 `json:"maxPoints"` } ) diff --git a/permission/permission.go b/permission/permission.go index bb4e15d..59d35d8 100644 --- a/permission/permission.go +++ b/permission/permission.go @@ -4,8 +4,9 @@ type Permission = uint32 const ( // Superuser has the right to ban users, create posts, and all permissions below - Superuser Permission = 1 << iota - CreateContest + Superuser Permission = 1 << iota // administrators + ManageUsers // moderators + CreateContest // contest organizers CreateProblem ) @@ -17,9 +18,11 @@ func StringToPermission(p string) Permission { switch p { case "superuser": return Superuser - case "createcontest": + case "manage_users": + return ManageUsers + case "create_contest": return CreateContest - case "createproblem": + case "create_problem": return CreateProblem } return 0 diff --git a/routes/apex/status.go b/routes/apex/status.go index 4778b4f..8f7f74e 100644 --- a/routes/apex/status.go +++ b/routes/apex/status.go @@ -6,5 +6,5 @@ import ( ) func Status(ctx *http.Context) http.Response { - return ctx.Respond(judge.Worker.GetJudges()) + return ctx.Respond(judge.Observer.GetJudges()) } diff --git a/routes/auth/login.go b/routes/auth/login.go index 7d44554..d091106 100644 --- a/routes/auth/login.go +++ b/routes/auth/login.go @@ -2,7 +2,7 @@ package auth import ( "github.com/ArcticOJ/blizzard/v0/db" - "github.com/ArcticOJ/blizzard/v0/db/models/user" + "github.com/ArcticOJ/blizzard/v0/db/schema/user" "github.com/ArcticOJ/blizzard/v0/server/http" "github.com/matthewhartstonge/argon2" "strings" diff --git a/routes/auth/register.go b/routes/auth/register.go index fcdf4de..d54a718 100644 --- a/routes/auth/register.go +++ b/routes/auth/register.go @@ -1,13 +1,13 @@ package auth import ( + "github.com/ArcticOJ/blizzard/v0/cache/stores" "github.com/ArcticOJ/blizzard/v0/core" "github.com/ArcticOJ/blizzard/v0/db" - "github.com/ArcticOJ/blizzard/v0/db/models/user" + "github.com/ArcticOJ/blizzard/v0/db/schema/user" "github.com/ArcticOJ/blizzard/v0/server/http" "github.com/jackc/pgerrcode" "github.com/uptrace/bun/driver/pgdriver" - "slices" "strings" ) @@ -19,8 +19,6 @@ type registerRequest struct { Organization string `json:"organization,omitempty"` } -var blacklistedHandles = []string{"edit"} - // TODO: Validate req before processing func Register(ctx *http.Context) http.Response { @@ -33,21 +31,20 @@ func Register(ctx *http.Context) http.Response { return ctx.InternalServerError("Could not crypto provided password.") } handle := strings.TrimSpace(strings.ToLower(req.Handle)) - if slices.Contains(blacklistedHandles, handle) { - return ctx.Bad("Blacklisted handle, please try another one.") - } + var uid string _, err := db.Database.NewInsert().Model(&user.User{ - DisplayName: req.DisplayName, - Handle: handle, - Email: strings.TrimSpace(strings.ToLower(req.Email)), - Password: string(r), - Organization: req.Organization, - }).Returning("NULL").Exec(ctx.Request().Context()) + DisplayName: req.DisplayName, + Handle: handle, + Email: strings.TrimSpace(strings.ToLower(req.Email)), + Password: string(r), + //Organizations: req.Organization, + }).Returning("id").Exec(ctx.Request().Context(), &uid) if err != nil { if err, ok := err.(pgdriver.Error); ok && err.Field('C') == pgerrcode.UniqueViolation { return ctx.Forbid("User with the same email or handle already exists.") } return ctx.InternalServerError("Request failed with unexpected error.") } + stores.Users.Add(uid) return ctx.Success() } diff --git a/routes/oauth/index.go b/routes/oauth/index.go index dc35c57..6a56b69 100644 --- a/routes/oauth/index.go +++ b/routes/oauth/index.go @@ -2,7 +2,7 @@ package oauth import ( "github.com/ArcticOJ/blizzard/v0/db" - "github.com/ArcticOJ/blizzard/v0/db/models/user" + "github.com/ArcticOJ/blizzard/v0/db/schema/user" "github.com/ArcticOJ/blizzard/v0/logger/debug" "github.com/ArcticOJ/blizzard/v0/oauth" "github.com/ArcticOJ/blizzard/v0/server/http" @@ -27,7 +27,6 @@ func Index(ctx *http.Context) http.Response { debug.Dump(c) for _, p := range c { m[p.Provider] = connection{ - ID: p.ID, Username: p.Username, } } diff --git a/routes/oauth/unlink.go b/routes/oauth/unlink.go index b5bdc1e..08590d9 100644 --- a/routes/oauth/unlink.go +++ b/routes/oauth/unlink.go @@ -2,7 +2,7 @@ package oauth import ( "github.com/ArcticOJ/blizzard/v0/db" - "github.com/ArcticOJ/blizzard/v0/db/models/user" + "github.com/ArcticOJ/blizzard/v0/db/schema/user" "github.com/ArcticOJ/blizzard/v0/logger/debug" "github.com/ArcticOJ/blizzard/v0/oauth" "github.com/ArcticOJ/blizzard/v0/server/http" diff --git a/routes/oauth/validate.go b/routes/oauth/validate.go index 08c12be..d57f779 100644 --- a/routes/oauth/validate.go +++ b/routes/oauth/validate.go @@ -4,7 +4,7 @@ import ( "crypto/hmac" "errors" "github.com/ArcticOJ/blizzard/v0/db" - "github.com/ArcticOJ/blizzard/v0/db/models/user" + "github.com/ArcticOJ/blizzard/v0/db/schema/user" "github.com/ArcticOJ/blizzard/v0/logger/debug" "github.com/ArcticOJ/blizzard/v0/oauth" "github.com/ArcticOJ/blizzard/v0/oauth/providers" @@ -28,7 +28,6 @@ func HandleLink(ctx *http.Context, provider string, res *providers.UserInfo) htt if _, e := db.Database.NewInsert().Model(&user.OAuthConnection{ UserID: uuid, Username: res.Username, - ID: res.ID, Provider: provider, }).Exec(ctx.Request().Context()); e != nil { var err pgdriver.Error diff --git a/routes/posts/index.go b/routes/posts/index.go index 0fdec32..3a11a36 100644 --- a/routes/posts/index.go +++ b/routes/posts/index.go @@ -2,7 +2,7 @@ package posts import ( "github.com/ArcticOJ/blizzard/v0/db" - "github.com/ArcticOJ/blizzard/v0/db/models/post" + "github.com/ArcticOJ/blizzard/v0/db/schema/post" "github.com/ArcticOJ/blizzard/v0/server/http" ) diff --git a/routes/problems/index.go b/routes/problems/index.go index c0e7f76..78e89fe 100644 --- a/routes/problems/index.go +++ b/routes/problems/index.go @@ -2,7 +2,7 @@ package problems import ( "github.com/ArcticOJ/blizzard/v0/db" - "github.com/ArcticOJ/blizzard/v0/db/models/contest" + "github.com/ArcticOJ/blizzard/v0/db/schema/contest" "github.com/ArcticOJ/blizzard/v0/server/http" ) diff --git a/routes/problems/submit.go b/routes/problems/submit.go index 4e6b5d7..796f904 100644 --- a/routes/problems/submit.go +++ b/routes/problems/submit.go @@ -1,4 +1,4 @@ -// TODO: complete submit route +// TODO: finalize submit route package problems @@ -6,7 +6,7 @@ import ( "container/list" "context" "github.com/ArcticOJ/blizzard/v0/db" - "github.com/ArcticOJ/blizzard/v0/db/models/contest" + "github.com/ArcticOJ/blizzard/v0/db/schema/contest" "github.com/ArcticOJ/blizzard/v0/judge" "github.com/ArcticOJ/blizzard/v0/server/http" "github.com/ArcticOJ/blizzard/v0/storage" @@ -42,22 +42,17 @@ func getExt(fileName string) string { return strings.ToLower(strings.TrimLeft(path.Ext(fileName), ".")) } -func createSubmission(ctx context.Context, userId uuid.UUID, problem, runtime string, ext string) (*contest.Submission, func() error, func() error) { +func createSubmission(ctx context.Context, userId uuid.UUID, problem, runtime string, fileName string) (*contest.Submission, error) { sub := &contest.Submission{ AuthorID: userId, ProblemID: problem, Runtime: runtime, - Extension: ext, + FileName: path.Base(fileName), } - tx, e := db.Database.Begin() - if e != nil { - return nil, nil, nil - } - if _, e = tx.NewInsert().Model(sub).Returning("id, submitted_at", "author_id").Exec(ctx); e != nil { - tx.Rollback() - return nil, nil, nil + if _, e := db.Database.NewInsert().Model(sub).Returning("id, submitted_at, author_id").Exec(ctx); e != nil { + return nil, e } - return sub, tx.Rollback, tx.Commit + return sub, nil } func Submit(ctx *http.Context) http.Response { @@ -92,41 +87,43 @@ func Submit(ctx *http.Context) http.Response { if len(problem.Constraints.AllowedRuntimes) > 0 && !slices.Contains(problem.Constraints.AllowedRuntimes, rt) { return ctx.Bad("This runtime is not allowed by current problem.") } - if !judge.Worker.RuntimeSupported(rt) { + if !judge.Observer.RuntimeSupported(rt) { return ctx.InternalServerError("No judge server is available to handle this submission.") } - dbSub, rollback, commit := createSubmission(ctx.Request().Context(), ctx.GetUUID(), problem.ID, rt, ext) + // create a random file name for this submission + fName := storage.Submission.Create(ext) + // try writing source code to previously generated file + e, rollback := storage.Submission.Write(fName, f) + if e != nil { + return ctx.InternalServerError("Failed to write uploaded source code to disk.") + } + // write to database + dbSub, _ := createSubmission(ctx.Request().Context(), ctx.GetUUID(), problem.ID, rt, fName) if dbSub == nil { - return ctx.Bad("Failed to create submission!") + rollback() + return ctx.InternalServerError("Failed to commit current submission to database.") } - // generate a path for storing source code for this submission - p := storage.Submission.Create(dbSub.ID, ext) // convert submission model to a polar-compatible one - sub := prepare(dbSub.ID, p, rt, dbSub.AuthorID, &problem) - if res, element, e = judge.Worker.Enqueue(sub, shouldStream, p, f); e != nil { - judge.Worker.DestroySubscribers(sub.ID) + sub := prepare(dbSub.ID, fName, rt, dbSub.AuthorID, &problem) + if res, element, e = judge.Observer.Enqueue(sub, shouldStream); e != nil { rollback() + judge.Observer.DestroySubscribers(sub.ID) return ctx.InternalServerError("Failed to process your submission.") } - if commit() != nil { - return ctx.InternalServerError("Could not save submission to database.") - } if shouldStream && res != nil { stream := ctx.StreamResponse() done := ctx.Request().Context().Done() for { select { case <-done: - judge.Worker.Unsubscribe(sub.ID, element) + judge.Observer.Unsubscribe(sub.ID, element) return nil case r, more := <-res: - if !more { + if !more || stream.Write(r) != nil { return nil } - stream.Write(r) } } - return ctx.Success() } else { return ctx.Respond(dbSub.ID) } diff --git a/routes/submissions/cancel_submission.go b/routes/submissions/cancel_submission.go index b296ad8..74d7bf4 100644 --- a/routes/submissions/cancel_submission.go +++ b/routes/submissions/cancel_submission.go @@ -17,5 +17,5 @@ func CancelSubmission(ctx *http.Context) http.Response { if e != nil { return ctx.Bad("Invalid ID.") } - return ctx.Respond(judge.Worker.Cancel(uint32(id), ctx.GetUUID())) + return ctx.Respond(judge.Observer.Cancel(uint32(id), ctx.GetUUID())) } diff --git a/routes/submissions/index.go b/routes/submissions/index.go index c7c8839..fe01580 100644 --- a/routes/submissions/index.go +++ b/routes/submissions/index.go @@ -2,13 +2,17 @@ package submissions import ( "github.com/ArcticOJ/blizzard/v0/db" - "github.com/ArcticOJ/blizzard/v0/db/models/contest" + "github.com/ArcticOJ/blizzard/v0/db/schema/contest" "github.com/ArcticOJ/blizzard/v0/server/http" ) func Submissions(ctx *http.Context) http.Response { var submissions []contest.Submission - if db.Database.NewSelect().Model(&submissions).Column("id", "runtime", "submitted_at", "verdict", "points", "problem_id", "time_taken", "total_memory").Scan(ctx.Request().Context()) != nil { + if db.Database.NewSelect(). + Model(&submissions). + Column("id", "runtime", "submitted_at", "verdict", "points", "problem_id", "time_taken", "total_memory"). + Order("submitted_at DESC"). + Scan(ctx.Request().Context()) != nil { return ctx.InternalServerError("Could not fetch submissions.") } return ctx.Respond(submissions) diff --git a/routes/submissions/source.dynamic.go b/routes/submissions/source.dynamic.go index 5a8a6ff..19a1731 100644 --- a/routes/submissions/source.dynamic.go +++ b/routes/submissions/source.dynamic.go @@ -2,9 +2,10 @@ package submissions import ( "github.com/ArcticOJ/blizzard/v0/db" - "github.com/ArcticOJ/blizzard/v0/db/models/contest" + "github.com/ArcticOJ/blizzard/v0/db/schema/contest" "github.com/ArcticOJ/blizzard/v0/server/http" "github.com/ArcticOJ/blizzard/v0/storage" + "path" ) func Source(ctx *http.Context) http.Response { @@ -13,13 +14,14 @@ func Source(ctx *http.Context) http.Response { } id := ctx.Param("submission") s := new(contest.Submission) - if db.Database.NewSelect().Model(s).Where("id = ?", id).Column("id", "author_id", "extension", "problem_id").Scan(ctx.Request().Context()) != nil { + if db.Database.NewSelect().Model(s).Where("id = ?", id).Column("id", "author_id", "file_name", "problem_id").Scan(ctx.Request().Context()) != nil { return ctx.NotFound("Submission not found.") } if s.AuthorID != ctx.GetUUID() { return ctx.Unauthorized() } - if e := ctx.Inline(storage.Submission.GetPath(s.ID, s.Extension), s.ProblemID+"."+s.Extension); e != nil { + downloadFileName := s.ProblemID + "." + path.Ext(s.FileName) + if e := ctx.Inline(storage.Submission.GetPath(s.FileName), downloadFileName); e != nil { return ctx.InternalServerError("Failed to load source code.") } return nil diff --git a/routes/submissions/submission.dynamic.go b/routes/submissions/submission.dynamic.go index 1b286d9..99f00ba 100644 --- a/routes/submissions/submission.dynamic.go +++ b/routes/submissions/submission.dynamic.go @@ -2,12 +2,17 @@ package submissions import ( "github.com/ArcticOJ/blizzard/v0/db" - "github.com/ArcticOJ/blizzard/v0/db/models/contest" + "github.com/ArcticOJ/blizzard/v0/db/schema/contest" + "github.com/ArcticOJ/blizzard/v0/judge" "github.com/ArcticOJ/blizzard/v0/server/http" + "github.com/ArcticOJ/polar/v0/types" "github.com/uptrace/bun" ) func Submission(ctx *http.Context) http.Response { + if ctx.RequireAuth() { + return nil + } id := ctx.Param("submission") s := new(contest.Submission) if db.Database.NewSelect().Model(s).Where("submission.id = ?", id).Relation("Problem", func(query *bun.SelectQuery) *bun.SelectQuery { @@ -17,5 +22,30 @@ func Submission(ctx *http.Context) http.Response { }).Scan(ctx.Request().Context()) != nil { return ctx.NotFound("Submission not found.") } - return ctx.Respond(s) + if s.AuthorID != ctx.GetUUID() { + return ctx.Unauthorized() + } + resChan, element := judge.Observer.Subscribe(types.Submission{ + ID: s.ID, + TestCount: s.Problem.TestCount, + PointsPerTest: s.Problem.PointsPerTest, + }, func() interface{} { + return judge.Observer.GetResults(s.ID) + }) + if resChan == nil { + return ctx.Respond(s) + } + stream := ctx.StreamResponse() + done := ctx.Request().Context().Done() + for { + select { + case <-done: + judge.Observer.Unsubscribe(s.ID, element) + return nil + case r, more := <-resChan: + if !more || stream.Write(r) != nil { + return nil + } + } + } } diff --git a/routes/user/api_key.go b/routes/user/api_key.go index 2b728c2..1947d48 100644 --- a/routes/user/api_key.go +++ b/routes/user/api_key.go @@ -3,7 +3,7 @@ package user import ( "encoding/base64" "github.com/ArcticOJ/blizzard/v0/db" - "github.com/ArcticOJ/blizzard/v0/db/models/user" + "github.com/ArcticOJ/blizzard/v0/db/schema/user" "github.com/ArcticOJ/blizzard/v0/server/http" "github.com/ArcticOJ/blizzard/v0/utils" "github.com/labstack/echo/v4" diff --git a/routes/user/hover_card.dynamic.go b/routes/user/hover_card.dynamic.go index 37206c1..57791dc 100644 --- a/routes/user/hover_card.dynamic.go +++ b/routes/user/hover_card.dynamic.go @@ -4,11 +4,9 @@ import ( "github.com/ArcticOJ/blizzard/v0/cache/stores" "github.com/ArcticOJ/blizzard/v0/server/http" "github.com/google/uuid" - "time" ) func HoverCard(ctx *http.Context) http.Response { - time.Sleep(time.Second * 5) handle := ctx.Param("handle") u := stores.Users.Get(ctx.Request().Context(), uuid.Nil, handle) if u == nil || u.ID == uuid.Nil { diff --git a/routes/user/index.go b/routes/user/index.go index c80470e..990d17f 100644 --- a/routes/user/index.go +++ b/routes/user/index.go @@ -9,7 +9,7 @@ func Index(ctx *http.Context) http.Response { if ctx.RequireAuth() { return nil } - if u := stores.Users.GetMinimal(ctx.Request().Context(), ctx.GetUUID()); u != nil { + if u := stores.Users.Get(ctx.Request().Context(), ctx.GetUUID(), ""); u != nil { return ctx.Respond(u) } return ctx.NotFound("User not found.") diff --git a/routes/users/index.go b/routes/users/index.go index bc30fb7..49538be 100644 --- a/routes/users/index.go +++ b/routes/users/index.go @@ -2,27 +2,32 @@ package users import ( "github.com/ArcticOJ/blizzard/v0/cache/stores" - "github.com/ArcticOJ/blizzard/v0/db/models/user" + "github.com/ArcticOJ/blizzard/v0/db/schema/user" "github.com/ArcticOJ/blizzard/v0/server/http" "github.com/ArcticOJ/blizzard/v0/types" + "math" "strconv" ) func Index(ctx *http.Context) http.Response { page := ctx.QueryParam("page") var ( - p uint32 = 1 - users []user.MinimalUser + p uint32 = 1 ) if _p, e := strconv.ParseUint(page, 10, 32); e == nil && _p > 0 { p = uint32(_p) } - c := ctx.Request().Context() - if users = stores.Users.GetPage(c, p-1, ctx.QueryParam("reversed") == "true"); users == nil { + n, users := stores.Users.GetPage(ctx.Request().Context(), p) + if n == -1 { return ctx.InternalServerError("Could not fetch users.") } - return ctx.Respond(types.Paginateable[user.MinimalUser]{ - Count: stores.Users.UserCount(c), + // coerce page in 1 - max page count + p = min(p, uint32(math.Ceil(float64(n)/float64(stores.DefaultUserPageSize)))) + //if users = stores.Users.GetPage(c, p-1, ctx.QueryParam("reversed") == "true"); users == nil { + // return ctx.InternalServerError("Could not fetch users.") + //} + return ctx.Respond(types.Paginateable[user.User]{ + Count: uint32(n), CurrentPage: p, PageSize: stores.DefaultUserPageSize, Data: users, diff --git a/server/http/context.go b/server/http/context.go index 21df7b6..aaa51ca 100644 --- a/server/http/context.go +++ b/server/http/context.go @@ -37,6 +37,7 @@ func (ctx Context) Arr(arr ...interface{}) Response { func (ctx Context) StreamResponse() *ResponseStream { r := ctx.Response() h := r.Header() + h.Set("X-Streamed", "true") h.Set("Transfer-Encoding", "chunked") h.Set("Connection", "keep-alive") r.WriteHeader(http.StatusOK) diff --git a/server/http/method.go b/server/http/method.go index e397bd8..8799b0e 100644 --- a/server/http/method.go +++ b/server/http/method.go @@ -6,10 +6,10 @@ type Method = string const ( Get Method = http.MethodGet - Post = http.MethodPost - Patch = http.MethodPatch - Delete = http.MethodDelete - Put = http.MethodPut + Post Method = http.MethodPost + Patch Method = http.MethodPatch + Delete Method = http.MethodDelete + Put Method = http.MethodPut ) func MethodFromString(method string) Method { diff --git a/server/middlewares/auth.go b/server/middlewares/auth.go index fd10d39..be74b5d 100644 --- a/server/middlewares/auth.go +++ b/server/middlewares/auth.go @@ -12,16 +12,16 @@ import ( func Authentication() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - //if authHeader := c.Request().Header.Get("Authorization"); strings.HasPrefix(authHeader, "Bearer") { - // authToken := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer")) - // if len(authToken) > 0 { - // var usr user.User - // if e := db.Database.NewSelect().Model(&usr).Where("api_key = ?", authToken).Column("id").Scan(c.Request().Context()); e == nil { - // c.Set("user", usr.ID) - // return next(c) - // } - // } - //} + // TODO: implement a key -> id resolver / user validator + if authHeader := c.Request().Header.Get("Authorization"); strings.HasPrefix(authHeader, "Bearer") { + authToken := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer")) + if len(authToken) > 0 { + if uid := stores.Users.ResolveApiKey(c.Request().Context(), authToken); uid != uuid.Nil { + c.Set("id", uid) + return next(c) + } + } + } ctx := &http.Context{ Context: c, } diff --git a/server/server.go b/server/server.go index 76db815..78675c3 100644 --- a/server/server.go +++ b/server/server.go @@ -35,6 +35,12 @@ func Register(e *echo.Echo) { } if config.Config.Debug { g.Use(middleware.BodyDump(func(c echo.Context, req, res []byte) { + if len(req) > 128 { + req = []byte("") + } + if len(res) > 128 { + res = []byte("") + } logger.Blizzard.Debug().Str("url", c.Request().RequestURI).Bytes("req", req).Bytes("res", res).Msg("body") })) g.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ diff --git a/server/session/paseto.go b/server/session/paseto.go index 33ca02f..ed7b9ca 100644 --- a/server/session/paseto.go +++ b/server/session/paseto.go @@ -15,7 +15,7 @@ var ( func init() { var e error - key, e = paseto.V4SymmetricKeyFromHex(config.Config.Blizzard.PrivateKey) + key, e = paseto.V4SymmetricKeyFromHex(config.Config.Blizzard.Secret) logger.Panic(e, "failed to decode hex-encoded private key from config, please regenerate another one using cmd/generator") parser = paseto.NewParser() } diff --git a/storage/submission.go b/storage/submission.go index 75038be..7c3b049 100644 --- a/storage/submission.go +++ b/storage/submission.go @@ -3,6 +3,7 @@ package storage import ( "fmt" "github.com/ArcticOJ/blizzard/v0/config" + "github.com/ArcticOJ/blizzard/v0/utils" "io" "os" "path" @@ -16,19 +17,25 @@ func init() { Submission = submissionStorage{root: config.Config.Blizzard.Storage[config.Submissions]} } -func (s submissionStorage) Create(id uint32, ext string) string { - return path.Join(s.root, fmt.Sprintf("%d.%s", id, ext)) +func (s submissionStorage) Create(ext string) string { + rnd := utils.Rand(8, "") + if rnd == "" { + return "" + } + return path.Join(s.root, fmt.Sprintf("%s.%s", rnd, ext)) } -func (submissionStorage) Write(path string, f io.Reader) error { +func (submissionStorage) Write(path string, f io.Reader) (error, func()) { file, e := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0755) if e != nil { - return e + return e, nil } _, e = io.Copy(file, f) - return e + return e, func() { + os.Remove(path) + } } -func (s submissionStorage) GetPath(id uint32, ext string) string { - return path.Join(s.root, fmt.Sprintf("%d.%s", id, ext)) +func (s submissionStorage) GetPath(name string) string { + return path.Join(s.root, name) } diff --git a/utils/crypto/hash.go b/utils/crypto/hash.go index 16a2ca2..51b0bb4 100644 --- a/utils/crypto/hash.go +++ b/utils/crypto/hash.go @@ -10,7 +10,7 @@ import ( var hmacHash hash.Hash func init() { - hmacHash = hmac.New(sha256.New, []byte(config.Config.Blizzard.PrivateKey)) + hmacHash = hmac.New(sha256.New, []byte(config.Config.Blizzard.Secret)) } func Hash(buf []byte) []byte {