The single, hashtag-driven feed for NUS campus life.
Students miss events because they're buried in 100+ muted Telegram channels. UniPulse fixes that: post any event with #unipulse in any group chat, and it lands in a single, searchable, personalised feed that every NUS student can follow.
Group chat UniPulse Bot Student
────────── ──────────── ───────
Post message Detects #unipulse /events
with #unipulse ──────────► Gemini AI parses ──────────► /find #sports
(+ optional title, date, /trending
image poster) location personalised
Saves to DB newsletter
Sends event card ◄────────────────────────
| Feature | Description |
|---|---|
| Smart event parsing | Paste any event announcement (or image poster) with #unipulse — Gemini AI extracts title, date, location, description automatically |
| Personalised feed | Subscribe to categories (#sports, #ai, #general, …) and get a daily digest at your chosen time |
| RSVP & reminders | Mark "Going" or "Interested"; automatic 24h and 1h reminders for events you're attending |
| Search | /find pizza (keyword) or /find #sports (category) |
| Trending | /trending shows events ranked by RSVP count |
| Google Calendar | One-tap "Add to Calendar" button on every event card |
| Moderation panel | /manage lists your events with Edit and Delete buttons — no UUID hunting required |
| Event editing | Correct AI parsing errors field-by-field without re-posting |
| NUS identity gate | Email verification via Supabase magic links (NUS domain only) |
- Runtime: Python 3.11+
- Web framework: FastAPI + uvicorn
- Bot SDK: python-telegram-bot v21 (webhook mode)
- Database: Supabase (PostgreSQL)
- Storage: Supabase Storage (event poster images)
- Auth: Supabase Auth (magic link OTP)
- AI: Google Gemini (event text + image OCR)
- Scheduler: APScheduler (reminders, daily digest, weekly newsletter)
- Deploy: Render / Heroku (Procfile included)
- Python 3.11+
- A Telegram bot token from @BotFather
- A Supabase project
- A Google AI Studio API key (Gemini)
- A public HTTPS URL for the webhook (e.g. a Render deploy, or
ngroklocally)
# 1. Clone
git clone https://github.com/your-org/unipulse.git
cd unipulse
# 2. Install dependencies
pip install -r requirements.txt
# 3. Configure environment
cp .env.example .env.local
# Fill in the values — see Environment Variables below
# 4. Run the database migrations
# Open your Supabase project → SQL Editor → paste and run migration.sql
# 5. Create the event-posters storage bucket in Supabase
# Dashboard → Storage → New bucket → name: event-posters → Public: on
# 6. Start the server
uvicorn app.main:app --reload --port 8000For local testing, expose port 8000 with ngrok:
ngrok http 8000
# Copy the HTTPS URL and set it as WEBHOOK_URL in .env.localCreate .env.local (copy from .env.example):
| Variable | Description |
|---|---|
TOKEN |
Telegram bot token from @BotFather |
WEBHOOK_URL |
Public HTTPS base URL (e.g. https://your-app.onrender.com) |
WEBHOOK_SECRET |
Any random secret string — used to validate incoming Telegram requests |
SUPABASE_URL |
Your Supabase project URL (https://xxxxx.supabase.co) |
SUPABASE_SECRET_KEY |
Supabase service role key (server-side only, never expose to clients) |
GEMINI_API_KEY |
Google AI Studio API key |
- Push the repo to GitHub.
- Create a new Web Service on Render.
- Set Build Command:
pip install -r requirements.txt - Set Start Command:
uvicorn app.main:app --host 0.0.0.0 --port $PORT - Add all six environment variables in the Render dashboard.
- After the first deploy, set
WEBHOOK_URLto your Render service URL and redeploy (the bot registers its own webhook on startup).
- Run
migration.sqlin the Supabase SQL Editor. This creates:pending_verificationstable (persistent magic-link state)upsert_rsvpRPC function (atomic RSVP toggle)created_atcolumn onevents(for DB-backed rate limiting)
- Create a Storage bucket named
event-posterswith Public access. - The following tables must exist (create them via the Supabase Table Editor or your own migration):
accounts,events,categories,event_categories,event_images,rsvps,reminders,account_categories
| Command | Description |
|---|---|
/start |
Welcome screen; shows onboarding on first login |
/verify |
Verify NUS identity via email magic link (DM only) |
/events |
Browse upcoming events (chronological) |
/trending |
Browse events by RSVP popularity |
/find <query> |
Search by keyword or #category |
/subscribe |
Manage category subscriptions |
/newslettertime HH:MM |
Set your daily digest delivery time (SGT) |
/manage |
View, edit, and delete your own events |
/edit <event_id> |
Edit a specific event field-by-field |
/delete <event_id> |
Soft-delete an event |
In any Telegram group where the bot is a member:
Hackathon Night — come build something cool!
Date: 15 March 2026, 6 PM
Venue: UTown Auditorium
Open to all NUS students.
#unipulse #tech #hackathon
The bot will:
- Detect
#unipulse - Extract category from the other hashtags (
tech) - Parse event details with Gemini AI
- Post a formatted event card with RSVP buttons back to the group
Attach an image poster and the bot will OCR it for any details missing from the text.
Requirements: you must be a verified NUS user (run /verify in a DM first). Limit: 5 posts per hour.
app/
├── main.py FastAPI app — webhook endpoint, auth callback
├── bot.py python-telegram-bot setup, handler registration
├── config.py Settings (env vars), timezone constant
├── handlers/
│ ├── start.py /start — welcome + first-login onboarding
│ ├── onboarding.py Post-verification welcome flow
│ ├── verify.py /verify — NUS email verification conversation
│ ├── parser.py #unipulse group message handler (AI parsing)
│ ├── browse.py /events, /trending
│ ├── find.py /find
│ ├── subscribe.py /subscribe — category subscription keyboard
│ ├── rsvp.py RSVP inline button callbacks
│ ├── remind.py Reminder inline button + creation
│ ├── moderation.py /manage — event list with edit/delete
│ ├── edit.py /edit — field-by-field event editing
│ ├── admin.py /delete command
│ └── newslettertime.py /newslettertime
├── services/
│ ├── supabase_client.py All database operations
│ ├── user_service.py Account & subscription queries
│ ├── event_card.py Event message formatting
│ ├── gemini.py Gemini AI event parsing
│ ├── calendar.py Google Calendar deep-link builder
│ └── scheduler.py APScheduler initialisation
├── jobs/
│ ├── reminders.py Send due reminders (every minute)
│ ├── digest.py Daily personalised newsletter
│ └── newsletter.py Weekly top-10 roundup (Sunday 6 PM SGT)
├── middleware/
│ └── rate_limit.py DB-backed rate limiting (5 posts/hour)
└── models/
└── schemas.py Pydantic models (ParsedEvent)
- Fork the repo and create a branch.
- Run the server locally with
uvicorn app.main:app --reload. - Test bot interactions via a test bot token pointing to your ngrok URL.
- Open a pull request with a clear description of the change.