// Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package user import ( "context" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/util" "xorm.io/builder" "xorm.io/xorm/schemas" ) // Badge represents a user badge type Badge struct { ID int64 `xorm:"pk autoincr"` Slug string `xorm:"UNIQUE"` Description string ImageURL string } // UserBadge represents a user badge type UserBadge struct { //nolint:revive // export stutter ID int64 `xorm:"pk autoincr"` BadgeID int64 UserID int64 } // TableIndices implements xorm's TableIndices interface func (n *UserBadge) TableIndices() []*schemas.Index { indices := make([]*schemas.Index, 0, 1) ubUnique := schemas.NewIndex("unique_user_badge", schemas.UniqueType) ubUnique.AddColumn("user_id", "badge_id") indices = append(indices, ubUnique) return indices } func init() { db.RegisterModel(new(Badge)) db.RegisterModel(new(UserBadge)) } // GetUserBadges returns the user's badges. func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) { sess := db.GetEngine(ctx). Select("`badge`.*"). Join("INNER", "user_badge", "`user_badge`.badge_id=badge.id"). Where("user_badge.user_id=?", u.ID) badges := make([]*Badge, 0, 8) count, err := sess.FindAndCount(&badges) return badges, count, err } // GetBadgeUsersOptions contains options for getting users with a specific badge type GetBadgeUsersOptions struct { db.ListOptions BadgeSlug string } // GetBadgeUsers returns the users that have a specific badge with pagination support. func GetBadgeUsers(ctx context.Context, opts *GetBadgeUsersOptions) ([]*User, int64, error) { sess := db.GetEngine(ctx). Select("`user`.*"). Join("INNER", "user_badge", "`user_badge`.user_id=user.id"). Join("INNER", "badge", "`user_badge`.badge_id=badge.id"). Where("badge.slug=?", opts.BadgeSlug) if opts.Page > 0 { sess = db.SetSessionPagination(sess, opts) } users := make([]*User, 0, opts.PageSize) count, err := sess.FindAndCount(&users) return users, count, err } // CreateBadge creates a new badge. func CreateBadge(ctx context.Context, badge *Badge) error { exists, err := db.GetEngine(ctx).Where("slug = ?", badge.Slug).Exist(new(Badge)) if err != nil { return err } if exists { return util.NewAlreadyExistErrorf("badge already exists [slug: %s]", badge.Slug) } if _, err := db.GetEngine(ctx).Insert(badge); err != nil { // Handle race between existence check and insert. exists, existErr := db.GetEngine(ctx).Where("slug = ?", badge.Slug).Exist(new(Badge)) if existErr == nil && exists { return util.NewAlreadyExistErrorf("badge already exists [slug: %s]", badge.Slug) } return err } return nil } // GetBadge returns a specific badge func GetBadge(ctx context.Context, slug string) (*Badge, error) { badge := new(Badge) has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge) if err != nil { return nil, err } if !has { return nil, util.NewNotExistErrorf("badge does not exist [slug: %s]", slug) } return badge, nil } // UpdateBadge updates a badge based on its slug. func UpdateBadge(ctx context.Context, badge *Badge) error { _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Cols("description", "image_url").Update(badge) return err } // DeleteBadge deletes a badge and all associated user_badge entries. func DeleteBadge(ctx context.Context, badge *Badge) error { return db.WithTx(ctx, func(ctx context.Context) error { // First delete all user_badge entries for this badge if _, err := db.GetEngine(ctx). Where("badge_id = ?", badge.ID). Delete(&UserBadge{}); err != nil { return err } // Then delete the badge itself if _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge); err != nil { return err } return nil }) } // AddUserBadge adds a badge to a user. func AddUserBadge(ctx context.Context, u *User, badge *Badge) error { return AddUserBadges(ctx, u, []*Badge{badge}) } // AddUserBadges adds badges to a user. func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error { return db.WithTx(ctx, func(ctx context.Context) error { for _, badge := range badges { // hydrate badge and check if it exists has, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Get(badge) if err != nil { return err } else if !has { return util.NewNotExistErrorf("badge does not exist [slug: %s]", badge.Slug) } exists, err := db.GetEngine(ctx).Where("badge_id = ? AND user_id = ?", badge.ID, u.ID).Exist(new(UserBadge)) if err != nil { return err } if exists { return util.NewAlreadyExistErrorf("user badge already exists [user_id: %d, badge_id: %d]", u.ID, badge.ID) } if err := db.Insert(ctx, &UserBadge{ BadgeID: badge.ID, UserID: u.ID, }); err != nil { exists, existErr := db.GetEngine(ctx).Where("badge_id = ? AND user_id = ?", badge.ID, u.ID).Exist(new(UserBadge)) if existErr == nil && exists { return util.NewAlreadyExistErrorf("user badge already exists [user_id: %d, badge_id: %d]", u.ID, badge.ID) } return err } } return nil }) } // RemoveUserBadge removes a badge from a user. func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error { return RemoveUserBadges(ctx, u, []*Badge{badge}) } // RemoveUserBadges removes specific badges from a user. func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error { return db.WithTx(ctx, func(ctx context.Context) error { if len(badges) == 0 { return nil } badgeSlugs := make([]string, 0, len(badges)) for _, badge := range badges { badgeSlugs = append(badgeSlugs, badge.Slug) } var userBadges []UserBadge if err := db.GetEngine(ctx).Table("user_badge"). Join("INNER", "badge", "badge.id = `user_badge`.badge_id"). Where("`user_badge`.user_id = ?", u.ID).In("`badge`.slug", badgeSlugs). Find(&userBadges); err != nil { return err } userBadgeIDs := make([]int64, 0, len(userBadges)) for _, ub := range userBadges { userBadgeIDs = append(userBadgeIDs, ub.ID) } if len(userBadgeIDs) == 0 { return nil } if _, err := db.GetEngine(ctx).Table("user_badge").In("id", userBadgeIDs).Delete(); err != nil { return err } return nil }) } // RemoveAllUserBadges removes all badges from a user. func RemoveAllUserBadges(ctx context.Context, u *User) error { _, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{}) return err } // SearchBadgeOptions represents the options when finding badges type SearchBadgeOptions struct { db.ListOptions Keyword string Slug string ID int64 OrderBy db.SearchOrderBy } func (opts *SearchBadgeOptions) ToConds() builder.Cond { cond := builder.NewCond() if opts.Keyword != "" { keywordCond := builder.Or( db.BuildCaseInsensitiveLike("badge.slug", opts.Keyword), db.BuildCaseInsensitiveLike("badge.description", opts.Keyword), ) cond = cond.And(keywordCond) } if opts.ID > 0 { cond = cond.And(builder.Eq{"badge.id": opts.ID}) } if len(opts.Slug) > 0 { cond = cond.And(builder.Eq{"badge.slug": opts.Slug}) } return cond } func (opts *SearchBadgeOptions) ToOrders() string { return opts.OrderBy.String() } // SearchBadges returns badges based on the provided SearchBadgeOptions options func SearchBadges(ctx context.Context, opts *SearchBadgeOptions) ([]*Badge, int64, error) { return db.FindAndCount[Badge](ctx, opts) } // GetBadgeByID returns a specific badge by ID func GetBadgeByID(ctx context.Context, id int64) (*Badge, error) { badge := new(Badge) has, err := db.GetEngine(ctx).ID(id).Get(badge) if err != nil { return nil, err } if !has { return nil, util.NewNotExistErrorf("badge does not exist [id: %d]", id) } return badge, nil }