chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,235 @@
package redisstore
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/redis/go-redis/v9"
"telesrv/internal/domain"
)
const DefaultUserCacheTTL = 5 * time.Minute
// UserCache stores viewer-independent base user rows in Redis.
type UserCache struct {
c *redis.Client
ttl time.Duration
}
func NewUserCache(c *redis.Client, ttl time.Duration) *UserCache {
if ttl <= 0 {
ttl = DefaultUserCacheTTL
}
return &UserCache{c: c, ttl: ttl}
}
func userBaseKey(id int64) string {
return fmt.Sprintf("user:base:%d", id)
}
type userBaseValue struct {
ID int64 `json:"id"`
AccessHash int64 `json:"access_hash"`
Phone string `json:"phone"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
About string `json:"about"`
Username string `json:"username"`
CountryCode string `json:"country_code"`
Verified bool `json:"verified"`
Support bool `json:"support"`
// bot 字段必须随缓存往返:丢失会让缓存命中路径把 bot 输出成普通用户,
// 污染客户端本地缓存(TDesktop 的 bot 标记不可逆)。
Bot bool `json:"bot,omitempty"`
BotInfoVersion int `json:"bot_info_version,omitempty"`
// premium / emoji status 同理必须随缓存往返:丢失会让缓存命中路径把
// 会员输出成非会员,跨路径状态漂移(与 bot 列同一坑位)。
PremiumUntil int `json:"premium_until,omitempty"`
EmojiStatusDocumentID int64 `json:"emoji_status_document_id,omitempty"`
EmojiStatusUntil int `json:"emoji_status_until,omitempty"`
// birthday / personal channel 同理必须随缓存往返:缓存命中路径丢失会让刚保存的
// 生日 / 个人频道在重新打开资料时归零(与 bot/premium 列同一坑位)。
BirthdayDay int `json:"birthday_day,omitempty"`
BirthdayMonth int `json:"birthday_month,omitempty"`
BirthdayYear int `json:"birthday_year,omitempty"`
PersonalChannelID int64 `json:"personal_channel_id,omitempty"`
ColorSet bool `json:"color_set,omitempty"`
Color int `json:"color,omitempty"`
ColorBackgroundEmojiID int64 `json:"color_background_emoji_id,omitempty"`
ProfileColorSet bool `json:"profile_color_set,omitempty"`
ProfileColor int `json:"profile_color,omitempty"`
ProfileColorBackgroundEmojiID int64 `json:"profile_color_background_emoji_id,omitempty"`
LastSeenAt int `json:"last_seen_at"`
}
func baseValueFromUser(u domain.User) userBaseValue {
return userBaseValue{
ID: u.ID,
AccessHash: u.AccessHash,
Phone: u.Phone,
FirstName: u.FirstName,
LastName: u.LastName,
About: u.About,
Username: u.Username,
CountryCode: u.CountryCode,
Verified: u.Verified,
Support: u.Support,
Bot: u.Bot,
BotInfoVersion: u.BotInfoVersion,
PremiumUntil: u.PremiumUntil,
EmojiStatusDocumentID: u.EmojiStatusDocumentID,
EmojiStatusUntil: u.EmojiStatusUntil,
BirthdayDay: u.Birthday.Day,
BirthdayMonth: u.Birthday.Month,
BirthdayYear: u.Birthday.Year,
PersonalChannelID: u.PersonalChannelID,
ColorSet: u.Color.HasColor,
Color: u.Color.Color,
ColorBackgroundEmojiID: u.Color.BackgroundEmojiID,
ProfileColorSet: u.ProfileColor.HasColor,
ProfileColor: u.ProfileColor.Color,
ProfileColorBackgroundEmojiID: u.ProfileColor.BackgroundEmojiID,
LastSeenAt: u.LastSeenAt,
}
}
func (v userBaseValue) user() domain.User {
return domain.User{
ID: v.ID,
AccessHash: v.AccessHash,
Phone: v.Phone,
FirstName: v.FirstName,
LastName: v.LastName,
About: v.About,
Username: v.Username,
CountryCode: v.CountryCode,
Verified: v.Verified,
Support: v.Support,
Bot: v.Bot,
BotInfoVersion: v.BotInfoVersion,
PremiumUntil: v.PremiumUntil,
EmojiStatusDocumentID: v.EmojiStatusDocumentID,
EmojiStatusUntil: v.EmojiStatusUntil,
Birthday: domain.Birthday{Day: v.BirthdayDay, Month: v.BirthdayMonth, Year: v.BirthdayYear},
PersonalChannelID: v.PersonalChannelID,
Color: domain.PeerColor{
HasColor: v.ColorSet,
Color: v.Color,
BackgroundEmojiID: v.ColorBackgroundEmojiID,
},
ProfileColor: domain.PeerColor{
HasColor: v.ProfileColorSet,
Color: v.ProfileColor,
BackgroundEmojiID: v.ProfileColorBackgroundEmojiID,
},
LastSeenAt: v.LastSeenAt,
}
}
func (s *UserCache) GetByIDs(ctx context.Context, ids []int64) (map[int64]domain.User, error) {
if s == nil || s.c == nil || len(ids) == 0 {
return map[int64]domain.User{}, nil
}
unique := uniqueCacheUserIDs(ids)
if len(unique) == 0 {
return map[int64]domain.User{}, nil
}
keys := make([]string, 0, len(unique))
for _, id := range unique {
keys = append(keys, userBaseKey(id))
}
values, err := s.c.MGet(ctx, keys...).Result()
if err != nil {
return nil, fmt.Errorf("redis mget user base: %w", err)
}
out := make(map[int64]domain.User, len(values))
corruptKeys := make([]string, 0)
for i, value := range values {
if value == nil {
continue
}
raw, ok := value.(string)
if !ok {
corruptKeys = append(corruptKeys, keys[i])
continue
}
var decoded userBaseValue
if err := json.Unmarshal([]byte(raw), &decoded); err != nil || decoded.ID == 0 || decoded.ID != unique[i] {
corruptKeys = append(corruptKeys, keys[i])
continue
}
out[decoded.ID] = decoded.user()
}
if len(corruptKeys) > 0 {
_ = s.c.Del(ctx, corruptKeys...).Err()
}
return out, nil
}
func (s *UserCache) PutMany(ctx context.Context, users []domain.User) error {
if s == nil || s.c == nil || len(users) == 0 {
return nil
}
pipe := s.c.Pipeline()
writes := 0
seen := make(map[int64]struct{}, len(users))
for _, u := range users {
if u.ID == 0 {
continue
}
if _, ok := seen[u.ID]; ok {
continue
}
seen[u.ID] = struct{}{}
raw, err := json.Marshal(baseValueFromUser(u))
if err != nil {
return fmt.Errorf("marshal user base cache: %w", err)
}
pipe.Set(ctx, userBaseKey(u.ID), raw, s.ttl)
writes++
}
if writes == 0 {
return nil
}
if _, err := pipe.Exec(ctx); err != nil {
return fmt.Errorf("redis set user base: %w", err)
}
return nil
}
func (s *UserCache) Delete(ctx context.Context, ids []int64) error {
if s == nil || s.c == nil || len(ids) == 0 {
return nil
}
unique := uniqueCacheUserIDs(ids)
if len(unique) == 0 {
return nil
}
keys := make([]string, 0, len(unique))
for _, id := range unique {
keys = append(keys, userBaseKey(id))
}
if err := s.c.Del(ctx, keys...).Err(); err != nil {
return fmt.Errorf("redis delete user base: %w", err)
}
return nil
}
func uniqueCacheUserIDs(ids []int64) []int64 {
out := make([]int64, 0, len(ids))
seen := make(map[int64]struct{}, len(ids))
for _, id := range ids {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
return out
}