mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 05:59:39 +02:00
Replace 21 separate NestJS Matrix bot processes (~2.1 GB RAM, ~4.2 GB Docker images) with a single Go binary using plugin architecture (8.6 MB binary, ~30 MB RAM). New services: - services/mana-matrix-bot/ — Go Matrix bot with 21 plugins (mautrix-go, Redis sessions) - services/mana-api-gateway-go/ — Go API gateway (rate limiting, API keys, credit billing) Deleted: - 21 services/matrix-*-bot/ directories - packages/bot-services/ and packages/matrix-bot-common/ - Legacy deploy scripts and CI build jobs Updated: - docker-compose.macmini.yml: new Go services, legacy bots removed - CI/CD: change detection + build jobs for Go services - Root package.json: new dev:matrix, build:matrix, test:matrix scripts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
174 lines
4.2 KiB
Go
174 lines
4.2 KiB
Go
package session
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
// RedisStore is a Redis-backed session manager.
|
|
// Sessions are shared across all plugins and persist across restarts.
|
|
type RedisStore struct {
|
|
client *redis.Client
|
|
prefix string
|
|
ttl time.Duration
|
|
}
|
|
|
|
// RedisConfig holds configuration for the Redis session store.
|
|
type RedisConfig struct {
|
|
Host string
|
|
Port int
|
|
Password string
|
|
Prefix string // key prefix (default: "mana-bot:session:")
|
|
TTL time.Duration // session TTL (default: 24h)
|
|
}
|
|
|
|
// NewRedisStore creates a new Redis session store.
|
|
func NewRedisStore(cfg RedisConfig) (*RedisStore, error) {
|
|
client := redis.NewClient(&redis.Options{
|
|
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
|
|
Password: cfg.Password,
|
|
DB: 0,
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
if err := client.Ping(ctx).Err(); err != nil {
|
|
return nil, fmt.Errorf("redis ping: %w", err)
|
|
}
|
|
|
|
prefix := cfg.Prefix
|
|
if prefix == "" {
|
|
prefix = "mana-bot:session:"
|
|
}
|
|
|
|
ttl := cfg.TTL
|
|
if ttl == 0 {
|
|
ttl = 24 * time.Hour
|
|
}
|
|
|
|
slog.Info("redis session store connected", "addr", fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
|
|
|
|
return &RedisStore{
|
|
client: client,
|
|
prefix: prefix,
|
|
ttl: ttl,
|
|
}, nil
|
|
}
|
|
|
|
// key builds the Redis key for a user's session data.
|
|
func (s *RedisStore) key(userID string) string {
|
|
return s.prefix + userID
|
|
}
|
|
|
|
// tokenKey builds the Redis key for a user's auth token.
|
|
func (s *RedisStore) tokenKey(userID string) string {
|
|
return s.prefix + "token:" + userID
|
|
}
|
|
|
|
// Get retrieves a session value.
|
|
func (s *RedisStore) Get(userID, key string) (any, bool) {
|
|
ctx := context.Background()
|
|
val, err := s.client.HGet(ctx, s.key(userID), key).Result()
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
|
|
// Try to unmarshal as JSON first
|
|
var result any
|
|
if err := json.Unmarshal([]byte(val), &result); err == nil {
|
|
return result, true
|
|
}
|
|
return val, true
|
|
}
|
|
|
|
// Set stores a session value.
|
|
func (s *RedisStore) Set(userID, key string, value any) {
|
|
ctx := context.Background()
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
slog.Error("redis session set: marshal failed", "error", err)
|
|
return
|
|
}
|
|
|
|
pipe := s.client.Pipeline()
|
|
pipe.HSet(ctx, s.key(userID), key, string(data))
|
|
pipe.Expire(ctx, s.key(userID), s.ttl)
|
|
if _, err := pipe.Exec(ctx); err != nil {
|
|
slog.Error("redis session set failed", "error", err)
|
|
}
|
|
}
|
|
|
|
// Delete removes a session value.
|
|
func (s *RedisStore) Delete(userID, key string) {
|
|
ctx := context.Background()
|
|
if err := s.client.HDel(ctx, s.key(userID), key).Err(); err != nil {
|
|
slog.Error("redis session delete failed", "error", err)
|
|
}
|
|
}
|
|
|
|
// GetToken returns the stored auth token for a user.
|
|
func (s *RedisStore) GetToken(userID string) (string, bool) {
|
|
ctx := context.Background()
|
|
|
|
// Get token and expiry from hash
|
|
vals, err := s.client.HMGet(ctx, s.tokenKey(userID), "token", "expires_at").Result()
|
|
if err != nil || len(vals) < 2 || vals[0] == nil {
|
|
return "", false
|
|
}
|
|
|
|
token, ok := vals[0].(string)
|
|
if !ok || token == "" {
|
|
return "", false
|
|
}
|
|
|
|
// Check expiry
|
|
if expiresStr, ok := vals[1].(string); ok && expiresStr != "" {
|
|
expiresAt, err := time.Parse(time.RFC3339, expiresStr)
|
|
if err == nil && time.Now().After(expiresAt) {
|
|
// Token expired, clean up
|
|
s.client.Del(ctx, s.tokenKey(userID))
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
return token, true
|
|
}
|
|
|
|
// SetToken stores an auth token with expiration.
|
|
func (s *RedisStore) SetToken(userID, token string, expiresAt time.Time) {
|
|
ctx := context.Background()
|
|
|
|
pipe := s.client.Pipeline()
|
|
pipe.HSet(ctx, s.tokenKey(userID), map[string]any{
|
|
"token": token,
|
|
"expires_at": expiresAt.Format(time.RFC3339),
|
|
})
|
|
|
|
// Set Redis TTL to match token expiry (plus buffer)
|
|
ttl := time.Until(expiresAt) + 1*time.Hour
|
|
if ttl < s.ttl {
|
|
ttl = s.ttl
|
|
}
|
|
pipe.Expire(ctx, s.tokenKey(userID), ttl)
|
|
|
|
if _, err := pipe.Exec(ctx); err != nil {
|
|
slog.Error("redis set token failed", "error", err)
|
|
}
|
|
}
|
|
|
|
// IsLoggedIn checks if a user has a valid token.
|
|
func (s *RedisStore) IsLoggedIn(userID string) bool {
|
|
_, ok := s.GetToken(userID)
|
|
return ok
|
|
}
|
|
|
|
// Close closes the Redis connection.
|
|
func (s *RedisStore) Close() error {
|
|
return s.client.Close()
|
|
}
|