package user import ( "context" "time" ) type ListFilter struct { ActiveOnly bool Search string Limit int } func (r *Repo) List(ctx context.Context, f ListFilter) ([]*User, error) { q := `SELECT ` + userCols + ` FROM users WHERE 1=1` args := []any{} if f.ActiveOnly { q += ` AND is_active = 1` } if f.Search != "" { q += ` AND (username LIKE ? COLLATE NOCASE OR pubkey LIKE ?)` args = append(args, "%"+f.Search+"%", "%"+f.Search+"%") } q += ` ORDER BY created_at DESC` if f.Limit > 0 { q += ` LIMIT ?` args = append(args, f.Limit) } rows, err := r.db.QueryContext(ctx, q, args...) if err != nil { return nil, err } return r.collect(rows) } func (r *Repo) ActiveByName(ctx context.Context) (map[string]string, error) { rows, err := r.db.QueryContext(ctx, `SELECT username, pubkey FROM users WHERE is_active = 1`) if err != nil { return nil, err } defer rows.Close() out := map[string]string{} for rows.Next() { var u, p string if err := rows.Scan(&u, &p); err != nil { return nil, err } out[u] = p } return out, rows.Err() } func (r *Repo) ListActivePubkeys(ctx context.Context) ([]string, error) { rows, err := r.db.QueryContext(ctx, `SELECT pubkey FROM users WHERE is_active = 1 ORDER BY pubkey`) if err != nil { return nil, err } defer rows.Close() out := make([]string, 0) for rows.Next() { var pk string if err := rows.Scan(&pk); err != nil { return nil, err } out = append(out, pk) } return out, rows.Err() } func (r *Repo) collect(rows interface { Next() bool Scan(...any) error Err() error Close() error }) ([]*User, error) { defer rows.Close() out := []*User{} for rows.Next() { u, err := scanUser(rows) if err != nil { return nil, err } out = append(out, u) } return out, rows.Err() } func (r *Repo) ListPendingReminders(ctx context.Context, days int, now time.Time) ([]*User, error) { low := now.Add(time.Duration(days)*24*time.Hour - 12*time.Hour).UTC().Format(time.RFC3339) high := now.Add(time.Duration(days)*24*time.Hour + 12*time.Hour).UTC().Format(time.RFC3339) rows, err := r.db.QueryContext(ctx, `SELECT `+userCols+` FROM users WHERE is_active = 1 AND subscription_type = 'yearly' AND expires_at BETWEEN ? AND ? AND (expiring_reminder_sent_at IS NULL OR expiring_reminder_sent_at < ?)`, low, high, now.Add(-23*time.Hour).UTC().Format(time.RFC3339)) if err != nil { return nil, err } return r.collect(rows) } func (r *Repo) ListExpired(ctx context.Context, now time.Time) ([]*User, error) { rows, err := r.db.QueryContext(ctx, `SELECT `+userCols+` FROM users WHERE is_active = 1 AND subscription_type = 'yearly' AND expires_at < ?`, now.UTC().Format(time.RFC3339)) if err != nil { return nil, err } return r.collect(rows) } func (r *Repo) ListGraceExpired(ctx context.Context, cutoff time.Time) ([]*User, error) { rows, err := r.db.QueryContext(ctx, `SELECT `+userCols+` FROM users WHERE is_active = 0 AND deactivated_at IS NOT NULL AND deactivated_at < ?`, cutoff.UTC().Format(time.RFC3339)) if err != nil { return nil, err } return r.collect(rows) } func (r *Repo) ListForSync(ctx context.Context, staleBefore time.Time) ([]*User, error) { rows, err := r.db.QueryContext(ctx, `SELECT `+userCols+` FROM users WHERE is_active = 1 AND manual_username = 0 AND (last_synced_at IS NULL OR last_synced_at < ?)`, staleBefore.UTC().Format(time.RFC3339)) if err != nil { return nil, err } return r.collect(rows) }