package chat import ( "context" "encoding/json" "fmt" "time" "github.com/google/uuid" "github.com/redis/go-redis/v9" "go.uber.org/zap" ) const ( presenceTTL = 2 * time.Minute presenceKeyPrefix = "chat:presence:" ) type PresenceInfo struct { UserID uuid.UUID `json:"user_id"` Online bool `json:"online"` LastSeen time.Time `json:"last_seen"` } type ChatPresenceService struct { redis *redis.Client logger *zap.Logger } func NewChatPresenceService(redisClient *redis.Client, logger *zap.Logger) *ChatPresenceService { if logger == nil { logger = zap.NewNop() } return &ChatPresenceService{ redis: redisClient, logger: logger, } } func (s *ChatPresenceService) presenceKey(userID uuid.UUID) string { return fmt.Sprintf("%s%s", presenceKeyPrefix, userID.String()) } func (s *ChatPresenceService) SetOnline(ctx context.Context, userID uuid.UUID) error { if s.redis == nil { return nil } info := PresenceInfo{ UserID: userID, Online: true, LastSeen: time.Now(), } data, err := json.Marshal(info) if err != nil { return fmt.Errorf("marshal presence: %w", err) } if err := s.redis.Set(ctx, s.presenceKey(userID), data, presenceTTL).Err(); err != nil { s.logger.Warn("Failed to set online presence", zap.Error(err), zap.String("user_id", userID.String())) return fmt.Errorf("set presence: %w", err) } return nil } func (s *ChatPresenceService) SetOffline(ctx context.Context, userID uuid.UUID) error { if s.redis == nil { return nil } if err := s.redis.Del(ctx, s.presenceKey(userID)).Err(); err != nil { s.logger.Warn("Failed to delete presence", zap.Error(err), zap.String("user_id", userID.String())) return fmt.Errorf("delete presence: %w", err) } return nil } func (s *ChatPresenceService) Heartbeat(ctx context.Context, userID uuid.UUID) error { if s.redis == nil { return nil } info := PresenceInfo{ UserID: userID, Online: true, LastSeen: time.Now(), } data, err := json.Marshal(info) if err != nil { return fmt.Errorf("marshal presence: %w", err) } if err := s.redis.Set(ctx, s.presenceKey(userID), data, presenceTTL).Err(); err != nil { s.logger.Warn("Failed to heartbeat presence", zap.Error(err), zap.String("user_id", userID.String())) return fmt.Errorf("heartbeat presence: %w", err) } return nil } func (s *ChatPresenceService) GetPresence(ctx context.Context, userID uuid.UUID) (*PresenceInfo, error) { if s.redis == nil { return &PresenceInfo{UserID: userID, Online: false}, nil } data, err := s.redis.Get(ctx, s.presenceKey(userID)).Bytes() if err == redis.Nil { return &PresenceInfo{UserID: userID, Online: false}, nil } if err != nil { return nil, fmt.Errorf("get presence: %w", err) } var info PresenceInfo if err := json.Unmarshal(data, &info); err != nil { return nil, fmt.Errorf("unmarshal presence: %w", err) } return &info, nil }