mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
250 lines
7.0 KiB
Go
250 lines
7.0 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package user
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/git/models/db"
|
|
"github.com/hanzoai/git/modules/cache"
|
|
"github.com/hanzoai/git/modules/json"
|
|
setting_module "github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/util"
|
|
|
|
"github.com/hanzoai/builder"
|
|
"github.com/hanzoai/xorm/convert"
|
|
)
|
|
|
|
// Setting is a key value store of user settings
|
|
type Setting struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
UserID int64 `xorm:"index unique(key_userid)"` // to load all of someone's settings
|
|
SettingKey string `xorm:"varchar(255) index unique(key_userid)"` // ensure key is always lowercase
|
|
SettingValue string `xorm:"text"`
|
|
}
|
|
|
|
// TableName sets the table name for the settings struct
|
|
func (s *Setting) TableName() string {
|
|
return "user_setting"
|
|
}
|
|
|
|
func init() {
|
|
db.RegisterModel(new(Setting))
|
|
}
|
|
|
|
// ErrUserSettingIsNotExist represents an error that a setting is not exist with special key
|
|
type ErrUserSettingIsNotExist struct {
|
|
Key string
|
|
}
|
|
|
|
// Error implements error
|
|
func (err ErrUserSettingIsNotExist) Error() string {
|
|
return fmt.Sprintf("Setting[%s] is not exist", err.Key)
|
|
}
|
|
|
|
func (err ErrUserSettingIsNotExist) Unwrap() error {
|
|
return util.ErrNotExist
|
|
}
|
|
|
|
// genSettingCacheKey returns the cache key for some configuration
|
|
func genSettingCacheKey(userID int64, key string) string {
|
|
return fmt.Sprintf("user_%d.setting.%s", userID, key)
|
|
}
|
|
|
|
// GetSetting returns the setting value via the key
|
|
func GetSetting(ctx context.Context, uid int64, key string) (string, error) {
|
|
return cache.GetString(genSettingCacheKey(uid, key), func() (string, error) {
|
|
res, err := GetSettingNoCache(ctx, uid, key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return res.SettingValue, nil
|
|
})
|
|
}
|
|
|
|
// GetSettingNoCache returns specific setting without using the cache
|
|
func GetSettingNoCache(ctx context.Context, uid int64, key string) (*Setting, error) {
|
|
v, err := GetSettings(ctx, uid, []string{key})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(v) == 0 {
|
|
return nil, ErrUserSettingIsNotExist{key}
|
|
}
|
|
return v[key], nil
|
|
}
|
|
|
|
// GetSettings returns specific settings from user
|
|
func GetSettings(ctx context.Context, uid int64, keys []string) (map[string]*Setting, error) {
|
|
settings := make([]*Setting, 0, len(keys))
|
|
if err := db.GetEngine(ctx).
|
|
Where("user_id=?", uid).
|
|
And(builder.In("setting_key", keys)).
|
|
Find(&settings); err != nil {
|
|
return nil, err
|
|
}
|
|
settingsMap := make(map[string]*Setting)
|
|
for _, s := range settings {
|
|
settingsMap[s.SettingKey] = s
|
|
}
|
|
return settingsMap, nil
|
|
}
|
|
|
|
// GetUserAllSettings returns all settings from user
|
|
func GetUserAllSettings(ctx context.Context, uid int64) (map[string]*Setting, error) {
|
|
settings := make([]*Setting, 0, 5)
|
|
if err := db.GetEngine(ctx).
|
|
Where("user_id=?", uid).
|
|
Find(&settings); err != nil {
|
|
return nil, err
|
|
}
|
|
settingsMap := make(map[string]*Setting)
|
|
for _, s := range settings {
|
|
settingsMap[s.SettingKey] = s
|
|
}
|
|
return settingsMap, nil
|
|
}
|
|
|
|
func validateUserSettingKey(key string) error {
|
|
if len(key) == 0 {
|
|
return errors.New("setting key must be set")
|
|
}
|
|
if strings.ToLower(key) != key {
|
|
return errors.New("setting key should be lowercase")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetUserSetting gets a specific setting for a user
|
|
func GetUserSetting(ctx context.Context, userID int64, key string, def ...string) (string, error) {
|
|
if err := validateUserSettingKey(key); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
setting, has, err := db.Get[Setting](ctx, builder.Eq{"user_id": userID, "setting_key": key})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !has {
|
|
if len(def) == 1 {
|
|
return def[0], nil
|
|
}
|
|
return "", nil
|
|
}
|
|
return setting.SettingValue, nil
|
|
}
|
|
|
|
// DeleteUserSetting deletes a specific setting for a user
|
|
func DeleteUserSetting(ctx context.Context, userID int64, key string) error {
|
|
if err := validateUserSettingKey(key); err != nil {
|
|
return err
|
|
}
|
|
|
|
cache.Remove(genSettingCacheKey(userID, key))
|
|
_, err := db.GetEngine(ctx).Delete(&Setting{UserID: userID, SettingKey: key})
|
|
|
|
return err
|
|
}
|
|
|
|
// SetUserSetting updates a users' setting for a specific key
|
|
func SetUserSetting(ctx context.Context, userID int64, key, value string) error {
|
|
if err := validateUserSettingKey(key); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := upsertUserSettingValue(ctx, userID, key, value); err != nil {
|
|
return err
|
|
}
|
|
|
|
cc := cache.GetCache()
|
|
if cc != nil {
|
|
return cc.Put(genSettingCacheKey(userID, key), value, setting_module.CacheService.TTLSeconds())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func upsertUserSettingValue(ctx context.Context, userID int64, key, value string) error {
|
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
e := db.GetEngine(ctx)
|
|
|
|
// here we use a general method to do a safe upsert for different databases (and most transaction levels)
|
|
// 1. try to UPDATE the record and acquire the transaction write lock
|
|
// if UPDATE returns non-zero rows are changed, OK, the setting is saved correctly
|
|
// if UPDATE returns "0 rows changed", two possibilities: (a) record doesn't exist (b) value is not changed
|
|
// 2. do a SELECT to check if the row exists or not (we already have the transaction lock)
|
|
// 3. if the row doesn't exist, do an INSERT (we are still protected by the transaction lock, so it's safe)
|
|
//
|
|
// to optimize the SELECT in step 2, we can use an extra column like `revision=revision+1`
|
|
// to make sure the UPDATE always returns a non-zero value for existing (unchanged) records.
|
|
|
|
res, err := e.Exec("UPDATE user_setting SET setting_value=? WHERE setting_key=? AND user_id=?", value, key, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows, _ := res.RowsAffected()
|
|
if rows > 0 {
|
|
// the existing row is updated, so we can return
|
|
return nil
|
|
}
|
|
|
|
// in case the value isn't changed, update would return 0 rows changed, so we need this check
|
|
has, err := e.Exist(&Setting{UserID: userID, SettingKey: key})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if has {
|
|
return nil
|
|
}
|
|
|
|
// if no existing row, insert a new row
|
|
_, err = e.Insert(&Setting{UserID: userID, SettingKey: key, SettingValue: value})
|
|
return err
|
|
})
|
|
}
|
|
|
|
func GetUserSettingJSON[T any](ctx context.Context, userID int64, key string, def T) (ret T, _ error) {
|
|
ret = def
|
|
str, err := GetUserSetting(ctx, userID, key)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
|
|
conv, ok := any(&ret).(convert.ConversionFrom)
|
|
if !ok {
|
|
conv, ok = any(ret).(convert.ConversionFrom)
|
|
}
|
|
if ok {
|
|
if err := conv.FromDB(util.UnsafeStringToBytes(str)); err != nil {
|
|
return ret, err
|
|
}
|
|
} else {
|
|
if str == "" {
|
|
return ret, nil
|
|
}
|
|
err = json.Unmarshal(util.UnsafeStringToBytes(str), &ret)
|
|
}
|
|
return ret, err
|
|
}
|
|
|
|
func SetUserSettingJSON[T any](ctx context.Context, userID int64, key string, val T) (err error) {
|
|
conv, ok := any(&val).(convert.ConversionTo)
|
|
if !ok {
|
|
conv, ok = any(val).(convert.ConversionTo)
|
|
}
|
|
var bs []byte
|
|
if ok {
|
|
bs, err = conv.ToDB()
|
|
} else {
|
|
bs, err = json.Marshal(val)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return SetUserSetting(ctx, userID, key, util.UnsafeBytesToString(bs))
|
|
}
|