Something went wrong. Try again.
An event companion and onboarding experience for the ATmosphere (alpha) atmo.quest
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231package api
import ( "log/slog" "net/http" "time"
"github.com/bluesky-social/indigo/atproto/syntax"
"atmoquest/internal/checkin" "atmoquest/internal/event")
// eventResponse is one item in the events list.type eventResponse struct { Token string `json:"token"` Name string `json:"name"` Description string `json:"description"` StartDate string `json:"startDate"` EndDate string `json:"endDate"` Location string `json:"location"` Lat float64 `json:"lat,omitempty"` Lng float64 `json:"lng,omitempty"` Radius float64 `json:"radius,omitempty"` CheckinCount int `json:"checkinCount"`}
// eventDetailResponse is the full event detail.type eventDetailResponse struct { eventResponse Attendees []eventAttendee `json:"attendees"` Stats eventStats `json:"stats"` IsCheckedIn bool `json:"isCheckedIn"`}
type eventAttendee struct { DID string `json:"did"` Handle string `json:"handle"` DisplayName string `json:"displayName"`}
type eventStats struct { UniqueConnectors int `json:"uniqueConnectors"` TotalCheckins int `json:"totalCheckins"` AttendeeCount int `json:"attendeeCount"`}
// ListEvents returns all events.// GET /api/v1/eventsfunc (h *Handlers) ListEvents(w http.ResponseWriter, r *http.Request) { events, err := event.ListAll(r.Context(), h.DB) if err != nil { slog.Warn("api: list events", "err", err) writeJSON(w, http.StatusOK, []eventResponse{}) return }
resp := make([]eventResponse, 0, len(events)) for _, ev := range events { token := event.QRTokenForURI(r.Context(), h.DB, ev.URI) er := eventResponse{ Token: token, Name: ev.Name, StartDate: ev.StartTime.Format(time.RFC3339), EndDate: ev.EndTime.Format(time.RFC3339), Location: ev.Location, } if ev.Geofence != nil { er.Lat = ev.Geofence.Lat er.Lng = ev.Geofence.Lng er.Radius = float64(ev.Geofence.RadiusMeters) } resp = append(resp, er) }
writeJSON(w, http.StatusOK, resp)}
// GetEvent returns full event detail.// GET /api/v1/events/{token}func (h *Handlers) GetEvent(w http.ResponseWriter, r *http.Request) { token := r.PathValue("token") if token == "" { writeError(w, http.StatusBadRequest, "missing event token") return }
ev, err := event.LookupByQRToken(r.Context(), h.DB, token) if err != nil { writeError(w, http.StatusNotFound, "event not found") return }
stats, _ := event.GetStats(r.Context(), h.DB, ev.URI)
attendeeDIDs, _ := checkin.ListAttendeesForEvent(r.Context(), h.DB, ev.URI) attendees := make([]eventAttendee, 0, len(attendeeDIDs)) for _, d := range attendeeDIDs { attendees = append(attendees, eventAttendee{ DID: d, Handle: h.resolveHandle(r, d), }) }
// Check if the current user is checked in (optional — no auth required for viewing). isCheckedIn := false didStr, _ := h.Auth.Sessions.Get(r) if didStr != "" { if did, err := syntax.ParseDID(didStr); err == nil { _, isCheckedIn, _ = checkin.Current(r.Context(), h.DB, did) } }
er := eventResponse{ Token: token, Name: ev.Name, StartDate: ev.StartTime.Format(time.RFC3339), EndDate: ev.EndTime.Format(time.RFC3339), Location: ev.Location, } if ev.Geofence != nil { er.Lat = ev.Geofence.Lat er.Lng = ev.Geofence.Lng er.Radius = float64(ev.Geofence.RadiusMeters) }
writeJSON(w, http.StatusOK, eventDetailResponse{ eventResponse: er, Attendees: attendees, Stats: eventStats{ UniqueConnectors: stats.UniqueConnectors, TotalCheckins: stats.TotalCheckins, AttendeeCount: len(attendeeDIDs), }, IsCheckedIn: isCheckedIn, })}
// GetEventStats returns just the live stats for an event (polling endpoint).// GET /api/v1/events/{token}/statsfunc (h *Handlers) GetEventStats(w http.ResponseWriter, r *http.Request) { token := r.PathValue("token") ev, err := event.LookupByQRToken(r.Context(), h.DB, token) if err != nil { writeError(w, http.StatusNotFound, "event not found") return }
stats, _ := event.GetStats(r.Context(), h.DB, ev.URI) attendeeDIDs, _ := checkin.ListAttendeesForEvent(r.Context(), h.DB, ev.URI)
w.Header().Set("Cache-Control", "no-cache") writeJSON(w, http.StatusOK, eventStats{ UniqueConnectors: stats.UniqueConnectors, TotalCheckins: stats.TotalCheckins, AttendeeCount: len(attendeeDIDs), })}
// Checkin checks the authenticated user into an event.// POST /api/v1/events/{token}/checkinfunc (h *Handlers) Checkin(w http.ResponseWriter, r *http.Request) { did, sess, ok := h.requireSession(w, r) if !ok { return }
token := r.PathValue("token") ev, err := event.LookupByQRToken(r.Context(), h.DB, token) if err != nil { writeError(w, http.StatusNotFound, "event not found") return }
if !ev.IsOngoing(time.Now()) { writeError(w, http.StatusBadRequest, "event is not currently active") return }
recordURI, err := checkin.Put(r.Context(), sess, h.DB, ev.URI, time.Now()) if err != nil { slog.Warn("api: checkin", "did", did, "event", ev.URI, "err", err) writeError(w, http.StatusInternalServerError, "failed to check in") return }
writeJSON(w, http.StatusCreated, map[string]string{"uri": recordURI})}
// FlushEvents batch-processes queued event tokens.// POST /api/v1/events/flushfunc (h *Handlers) FlushEvents(w http.ResponseWriter, r *http.Request) { did, sess, ok := h.requireSession(w, r) if !ok { return }
var body struct { Tokens []string `json:"tokens"` } if err := decodeBody(r, &body); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return }
written, skipped, errs := 0, 0, 0 for _, token := range body.Tokens { ev, err := event.LookupByQRToken(r.Context(), h.DB, token) if err != nil { skipped++ continue } if !ev.IsOngoing(time.Now()) { skipped++ continue } if _, err := checkin.Put(r.Context(), sess, h.DB, ev.URI, time.Now()); err != nil { slog.Warn("api: flush event", "did", did, "token", token, "err", err) errs++ continue } written++ }
writeJSON(w, http.StatusOK, map[string]int{ "written": written, "skipped": skipped, "errors": errs, })}