// Package attendance fetches attendance CSV exports from Google Sheets. // No auth required — the sheet must be publicly readable. package attendance import ( "context" "encoding/csv" "fmt" "io" "net/http" "strings" ) const exportBase = "https://docs.google.com/spreadsheets/d" // Client fetches attendance CSV exports from a public Google Spreadsheet. type Client struct { http *http.Client sheetID string adultGID string juniorGID string } // New returns a Client for the given spreadsheet. // adultGID is typically "0"; juniorGID is the GID of the junior tab. func New(httpClient *http.Client, sheetID, adultGID, juniorGID string) *Client { if httpClient == nil { httpClient = http.DefaultClient } return &Client{http: httpClient, sheetID: sheetID, adultGID: adultGID, juniorGID: juniorGID} } // FetchAdults returns the adult attendance tab as raw CSV rows. func (c *Client) FetchAdults(ctx context.Context) ([][]string, error) { return c.fetch(ctx, c.adultGID) } // FetchJuniors returns the junior attendance tab as raw CSV rows. func (c *Client) FetchJuniors(ctx context.Context) ([][]string, error) { return c.fetch(ctx, c.juniorGID) } func (c *Client) fetch(ctx context.Context, gid string) ([][]string, error) { url := fmt.Sprintf("%s/%s/export?format=csv&gid=%s", exportBase, c.sheetID, gid) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } resp, err := c.http.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("attendance fetch: HTTP %d for gid=%s", resp.StatusCode, gid) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } r := csv.NewReader(strings.NewReader(string(body))) r.FieldsPerRecord = -1 // rows may have different lengths return r.ReadAll() }