尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗Every public McMaster event in one place, starting with the ones that have free food. A worker reads club Instagram posts, an LLM pulls out the events, and an admin approves them; only confident, double-checked events skip the wait.
Campus life is scattered across hundreds of club Instagram accounts. Unless you follow the right pages and catch the right story, you miss things. Cushion puts every public campus event in one place, organized and searchable: a search engine for things to do.
It's built for one moment. You've got downtime tomorrow and want something to do. You open Cushion, say when you're free, and see everything happening in that window: free food, socials, games, talks, sports, shows. Finding something fun should take seconds, not an evening of scrolling.
What that means for how it's built:
| Folder | What it is |
|---|---|
worker/ | Python worker: fetches posts, stores them and their images, extracts events |
src/ | Next.js site: public pages, admin review queue, and club list |
supabase/migrations/ | Database schema and row level security |
campus.config.json | School-specific values (name, timezone, campus buildings), shared by both |
worker/fetchers/profile_page.py);
if every attempt is blocked, Apify's Instagram Scraper is the fallback.post-images bucket. Reels and other videos are saved but not read: clubs
post a flyer for anything they promote, so a reel mostly duplicates it
(SKIP_REELS=false reads them again).worker/extractors/two_stage.py).worker/places.py): the same
room at about the same time is one event whatever it's called, and a similar
name in another room or building, or hours apart, isn't (another club's is a
separate event; the same club's waits for a person, since it may be a room
change or a second session). Copies sort themselves out (a copy of a
published event, the less complete of two pending copies, a teaser once the
dated post arrives, and a date a newer post moved are rejected with a note;
a club's post of an event already listed on Rubric isn't a copy, since the
merge joins the two), and pending events expire when their date passes. Everything else waits as
pending at /admin. See
worker/auto_approve.py; AUTO_APPROVE_MIN_CONFIDENCE=none turns
auto-approval off.worker/rubric.py) at
most one request a second, and maps each Rubric club to our Instagram
account, by the Instagram link the club gave Rubric or by name (unsure
links wait at /admin/clubs). worker/merge.py then matches Rubric events
to Instagram ones (same club, same day, starting within an hour; the same
room counts for a match, another room against) and takes
each field from the more trustworthy source: Rubric for time, place, price
and tickets; Instagram for free food, description and image. Rubric events
with no Instagram match publish on their own once they pass a duplicate
check and their club is on our list (otherwise they wait); they're off
campus at a street address, on campus at a named campus place. The merge
runs every cycle, so a club's post joins its Rubric listing within 15
minutes of being published. A published
Rubric event stays up until a person takes it down: when it starts, when it
leaves Rubric (flagged, with "Check before you go"), and while an Instagram
post of it waits for review (they merge once that post is approved).
Disagreements and unsure matches wait at ("Needs a look"); a
deleted post or an event gone from Rubric is flagged, never deleted.
(the default) saves the merge; only prints it
( for a range).Every request is logged: fetch_logs (club, backend, status, bytes) and
llm_usage (tokens and estimated cost; python usage.py for totals).
Needs Python 3.10+ and worker/.env (copy worker/.env.example; keys are never committed).
cd worker
python -m venv .venv
.venv\Scripts\python -m pip install -r requirements.txt
.venv\Scripts\python check_setup.py # verifies keys, database, Claude, proxy, images
.venv\Scripts\python run.py # one cycle
Clone the repo, copy worker/.env into the clone, then from an administrator
PowerShell in the worker folder:
powershell -ExecutionPolicy Bypass -File deploy\setup-windows.ps1
It installs the packages, runs check_setup.py, and registers a scheduled task
that runs every 15 minutes (never overlapping itself) with daily logs in
worker/logs/.
Needs Node 20+ and .env.local (copy .env.example).
npm install
npm run dev # http://localhost:3000/admin
Public pages have three ways to send feedback, all opening the same form (a
bottom sheet on phones, a small dialog on desktop; it never opens by itself):
"Something wrong with this event?" on event pages, "Missing an event? Tell
us." under the lists on / and /week, and "Send feedback" in the footer.
Messages land in the feedback table, which visitors can add to but never
read, and are listed at /admin/feedback with a type filter and a Done box.
Spam guards: a hidden honeypot field, and at most 3 messages per browser per
hour (a cookie only the server reads).
Visitor counts come from Vercel Web Analytics (<Analytics /> in the public
layout: no cookies, nothing personal). Turn it on once under the project's
Analytics tab in Vercel.
/adminRUBRIC_MERGE=applyRUBRIC_MERGE=reportpython merge.py --from 2026-10-19 --to 2026-10-25worker/recheck.py) Nothing cancels an event by itself: the
worker flags it for /admin ("Needs a look"), where a person sets
Cancelled or Postponed. Cancelled events stay listed with a red badge and a
struck-through title until their date passes, so people who saw them learn
they're off. Flags come from: an edited caption (saved on every fetch, then
re-read and compared: date, time, place, status); a new post saying
cancelled, postponed, rescheduled or "no longer happening" (flags that
club's upcoming events); the daily check of every event in the next 14 days
(fetches the club's profile, or opens a post too old to show there; a
deleted or unavailable post flags its events); Rubric (a listing gone or
titled cancelled); and two "This got cancelled" reports from visitors (a
database trigger). Event pages show "Last checked" from when the post or
Rubric listing was last seen. While a flag waits (deleted post, a post about
a cancellation, a listing gone from Rubric, an edited post, visitors'
reports), the event shows "Check
before you go" with the reason; every event page also says clubs post
last-minute changes in their stories, with a link to the club's profile
(stories can't be read). "Save & clear flags" in the admin dismisses an
event's flags for good. First run: python recheck.py --every-club.worker/registration.py): required, recommended
("RSVP appreciated"), drop in, or unknown (the default: the source doesn't
say). The caption wins (Claude reads it; a Rubric-only event uses its
rejected Instagram copies' captions). Rubric only makes it required when
its listing says to register, is capped or sold out, or has a paid ticket;
a free ticket alone doesn't, since clubs use them as headcounts for drop-in
events (a caption saying drop in next to a free ticket is just drop in).
When the caption says drop in but Rubric has a paid ticket, a cap, sells
out or says to register, the caption wins and the event is flagged. Cards show "RSVP" (required) or "Drop in";
event pages show "Registration required: [where]" with a button, or a
lighter "RSVP appreciated". New and changed events are judged each cycle;
python registration.py --all --apply judges every upcoming event again.worker/bio_links.py): every profile fetch saves the links
in the club's bio, and reads the link-in-bio page they lead to (Linktree
and the like) when the bio changes or once a day. A bio linking one Rubric
club page (or its events) confirms that Rubric club is this club; a bio
link to a Rubric event makes it count as the same event as that club's
post that day; and a "link in bio" registration gets the link whose title
names the event, so the button opens the actual form. First run:
python bio_links.py --fetch-upcoming.worker/communities.py): clubs and events are tagged
with the communities they're for, from the list in campus.config.json
(Pride, women, each faith, culture by region, each program). A club's tags
(from its name and account, read by Sonnet once) apply to all its events;
an event adds its own from its words (a women-only class, Diwali, an
engineering night). /communities lists upcoming events per community,
including events only for that group's members. python communities.py --all tags everything again.