package jitsi import ( "context" "database/sql" "log/slog" "time" ) type Queriable interface { ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row } type Repository struct { Db *sql.DB q Queriable } func NewRepository(db *sql.DB) (*Repository, error) { return &Repository{Db: db, q: db}, nil } func (r *Repository) WithTx(tx *sql.Tx) *Repository { return &Repository{Db: r.Db, q: tx} } func (r *Repository) RecordJoin(ctx context.Context, userId int64, room string, at time.Time) error { query := "INSERT INTO jitsi_room_history(user_id, room_name, last_joined_at) VALUES(?, ?, ?) ON CONFLICT(user_id, room_name) DO UPDATE SET last_joined_at = excluded.last_joined_at" _, err := r.q.ExecContext(ctx, query, userId, room, at.UTC().Format(time.RFC3339)) if err != nil { slog.Error("record room join", slog.String("op", "repo.RecordJoin"), slog.Int64("user_id", userId), slog.String("room", room), slog.Any("error", err)) return ErrInternal } return nil } func (r *Repository) List(ctx context.Context, userId int64, limit, offset int64) ([]*RoomHistory, error) { query := "SELECT user_id, room_name, last_joined_at FROM jitsi_room_history WHERE user_id = ? ORDER BY last_joined_at DESC" args := []any{userId} if limit > 0 { query += " LIMIT ? OFFSET ?" args = append(args, limit, offset) } rows, err := r.q.QueryContext(ctx, query, args...) if err != nil { slog.Error("list room history", slog.String("op", "repo.List"), slog.Int64("user_id", userId), slog.Any("error", err)) return nil, ErrInternal } defer rows.Close() var history []*RoomHistory for rows.Next() { entry := &RoomHistory{} var lastJoinedAt string err := rows.Scan(&entry.UserId, &entry.RoomName, &lastJoinedAt) if err != nil { slog.Error("scan room history", slog.String("op", "repo.List"), slog.Int64("user_id", userId), slog.Any("error", err)) return nil, ErrInternal } parsed, err := time.Parse(time.RFC3339, lastJoinedAt) if err != nil { slog.Error("parse last_joined_at", slog.String("op", "repo.List"), slog.Int64("user_id", userId), slog.String("room", entry.RoomName), slog.Any("error", err)) return nil, ErrInternal } entry.LastJoinedAt = parsed history = append(history, entry) } err = rows.Err() if err != nil { slog.Error("iterate room history", slog.String("op", "repo.List"), slog.Int64("user_id", userId), slog.Any("error", err)) return nil, ErrInternal } return history, nil } func (r *Repository) DeleteRoom(ctx context.Context, userId int64, room string) error { query := "DELETE FROM jitsi_room_history WHERE user_id = ? AND room_name = ?" res, err := r.q.ExecContext(ctx, query, userId, room) if err != nil { slog.Error("delete room", slog.String("op", "repo.DeleteRoom"), slog.Int64("user_id", userId), slog.String("room", room), slog.Any("error", err)) return ErrInternal } affected, err := res.RowsAffected() if err != nil { slog.Error("rows affected", slog.String("op", "repo.DeleteRoom"), slog.Int64("user_id", userId), slog.String("room", room), slog.Any("error", err)) return ErrInternal } if affected == 0 { return ErrNotFound } return nil } func (r *Repository) DeleteAll(ctx context.Context, userId int64) error { query := "DELETE FROM jitsi_room_history WHERE user_id = ?" _, err := r.q.ExecContext(ctx, query, userId) if err != nil { slog.Error("delete all rooms", slog.String("op", "repo.DeleteAll"), slog.Int64("user_id", userId), slog.Any("error", err)) return ErrInternal } return nil }