package redis import ( "bytes" "compress/zlib" "context" "encoding/json" "fmt" "io" "strconv" "strings" "time" "github.com/mediocregopher/radix/v4" "github.com/elyby/chrly/model" ) var now = time.Now func New(ctx context.Context, addr string, poolSize int) (*Redis, error) { client, err := (radix.PoolConfig{Size: poolSize}).New(ctx, "tcp", addr) if err != nil { return nil, err } return &Redis{ client: client, context: ctx, }, nil } const accountIdToUsernameKey = "hash:username-to-account-id" // TODO: this should be actually "hash:user-id-to-username" const mojangUsernameToUuidKey = "hash:mojang-username-to-uuid" type Redis struct { client radix.Client context context.Context } func (db *Redis) FindSkinByUsername(username string) (*model.Skin, error) { var skin *model.Skin err := db.client.Do(db.context, radix.WithConn("", func(ctx context.Context, conn radix.Conn) error { var err error skin, err = findByUsername(ctx, conn, username) return err })) return skin, err } func findByUsername(ctx context.Context, conn radix.Conn, username string) (*model.Skin, error) { redisKey := buildUsernameKey(username) var encodedResult []byte err := conn.Do(ctx, radix.Cmd(&encodedResult, "GET", redisKey)) if err != nil { return nil, err } if len(encodedResult) == 0 { return nil, nil } result, err := zlibDecode(encodedResult) if err != nil { return nil, err } var skin *model.Skin err = json.Unmarshal(result, &skin) if err != nil { return nil, err } // Some old data causing issues in the production. // TODO: remove after investigation will be finished if skin.Uuid == "" { return nil, nil } skin.OldUsername = skin.Username return skin, nil } func (db *Redis) FindSkinByUserId(id int) (*model.Skin, error) { var skin *model.Skin err := db.client.Do(db.context, radix.WithConn("", func(ctx context.Context, conn radix.Conn) error { var err error skin, err = findByUserId(ctx, conn, id) return err })) return skin, err } func findByUserId(ctx context.Context, conn radix.Conn, id int) (*model.Skin, error) { var username string err := conn.Do(ctx, radix.FlatCmd(&username, "HGET", accountIdToUsernameKey, id)) if err != nil { return nil, err } if username == "" { return nil, nil } return findByUsername(ctx, conn, username) } func (db *Redis) SaveSkin(skin *model.Skin) error { return db.client.Do(db.context, radix.WithConn("", func(ctx context.Context, conn radix.Conn) error { return save(ctx, conn, skin) })) } func save(ctx context.Context, conn radix.Conn, skin *model.Skin) error { err := conn.Do(ctx, radix.Cmd(nil, "MULTI")) if err != nil { return err } // If user has changed username, then we must delete his old username record if skin.OldUsername != "" && skin.OldUsername != skin.Username { err = conn.Do(ctx, radix.Cmd(nil, "DEL", buildUsernameKey(skin.OldUsername))) if err != nil { return err } } // If this is a new record or if the user has changed username, we set the value in the hash table if skin.OldUsername != "" || skin.OldUsername != skin.Username { err = conn.Do(ctx, radix.FlatCmd(nil, "HSET", accountIdToUsernameKey, skin.UserId, skin.Username)) } str, _ := json.Marshal(skin) err = conn.Do(ctx, radix.FlatCmd(nil, "SET", buildUsernameKey(skin.Username), zlibEncode(str))) if err != nil { return err } err = conn.Do(ctx, radix.Cmd(nil, "EXEC")) if err != nil { return err } skin.OldUsername = skin.Username return nil } func (db *Redis) RemoveSkinByUserId(id int) error { return db.client.Do(db.context, radix.WithConn("", func(ctx context.Context, conn radix.Conn) error { return removeByUserId(ctx, conn, id) })) } func removeByUserId(ctx context.Context, conn radix.Conn, id int) error { record, err := findByUserId(ctx, conn, id) if err != nil { return err } err = conn.Do(ctx, radix.Cmd(nil, "MULTI")) if err != nil { return err } err = conn.Do(ctx, radix.FlatCmd(nil, "HDEL", accountIdToUsernameKey, id)) if err != nil { return err } if record != nil { err = conn.Do(ctx, radix.Cmd(nil, "DEL", buildUsernameKey(record.Username))) if err != nil { return err } } return conn.Do(ctx, radix.Cmd(nil, "EXEC")) } func (db *Redis) RemoveSkinByUsername(username string) error { return db.client.Do(db.context, radix.WithConn("", func(ctx context.Context, conn radix.Conn) error { return removeByUsername(ctx, conn, username) })) } func removeByUsername(ctx context.Context, conn radix.Conn, username string) error { record, err := findByUsername(ctx, conn, username) if err != nil { return err } if record == nil { return nil } err = conn.Do(ctx, radix.Cmd(nil, "MULTI")) if err != nil { return err } err = conn.Do(ctx, radix.Cmd(nil, "DEL", buildUsernameKey(record.Username))) if err != nil { return err } err = conn.Do(ctx, radix.FlatCmd(nil, "HDEL", accountIdToUsernameKey, record.UserId)) if err != nil { return err } return conn.Do(ctx, radix.Cmd(nil, "EXEC")) } func (db *Redis) GetUuidForMojangUsername(username string) (string, string, error) { var uuid string foundUsername := username err := db.client.Do(db.context, radix.WithConn("", func(ctx context.Context, conn radix.Conn) error { var err error uuid, foundUsername, err = findMojangUuidByUsername(ctx, conn, username) return err })) return uuid, foundUsername, err } func findMojangUuidByUsername(ctx context.Context, conn radix.Conn, username string) (string, string, error) { key := strings.ToLower(username) var result string err := conn.Do(ctx, radix.Cmd(&result, "HGET", mojangUsernameToUuidKey, key)) if err != nil { return "", "", err } if result == "" { return "", "", nil } parts := strings.Split(result, ":") partsCnt := len(parts) // https://github.com/elyby/chrly/issues/28 if partsCnt < 2 { err = conn.Do(ctx, radix.Cmd(nil, "HDEL", mojangUsernameToUuidKey, key)) if err != nil { return "", "", err } return "", "", fmt.Errorf("got unexpected response from the mojangUsernameToUuid hash: \"%s\"", result) } var casedUsername, uuid, rawTimestamp string if partsCnt == 2 { // Legacy, when original username wasn't stored casedUsername = username uuid = parts[0] rawTimestamp = parts[1] } else { casedUsername = parts[0] uuid = parts[1] rawTimestamp = parts[2] } timestamp, _ := strconv.ParseInt(rawTimestamp, 10, 64) storedAt := time.Unix(timestamp, 0) if storedAt.Add(time.Hour * 24 * 30).Before(now()) { err = conn.Do(ctx, radix.Cmd(nil, "HDEL", mojangUsernameToUuidKey, key)) if err != nil { return "", "", err } return "", "", nil } return uuid, casedUsername, nil } func (db *Redis) StoreMojangUuid(username string, uuid string) error { return db.client.Do(db.context, radix.WithConn("", func(ctx context.Context, conn radix.Conn) error { return storeMojangUuid(ctx, conn, username, uuid) })) } func storeMojangUuid(ctx context.Context, conn radix.Conn, username string, uuid string) error { value := fmt.Sprintf("%s:%s:%d", username, uuid, now().Unix()) err := conn.Do(ctx, radix.Cmd(nil, "HSET", mojangUsernameToUuidKey, strings.ToLower(username), value)) if err != nil { return err } return nil } func (db *Redis) Ping() error { return db.client.Do(db.context, radix.Cmd(nil, "PING")) } func buildUsernameKey(username string) string { return "username:" + strings.ToLower(username) } func zlibEncode(str []byte) []byte { var buff bytes.Buffer writer := zlib.NewWriter(&buff) _, _ = writer.Write(str) _ = writer.Close() return buff.Bytes() } func zlibDecode(bts []byte) ([]byte, error) { buff := bytes.NewReader(bts) reader, err := zlib.NewReader(buff) if err != nil { return nil, err } resultBuffer := new(bytes.Buffer) _, _ = io.Copy(resultBuffer, reader) _ = reader.Close() return resultBuffer.Bytes(), nil }