How it works Β· Get started Β· Architecture Β· Documentation Β· Changelog
Canvas tells you about a deadline once, in an email you probably didn't read. This bot keeps telling you until the work is in.
It reads your Canvas To Do list through the official API, sends you a ranked list every morning, and while anything is due within 24 hours it sends one combined reminder every 2 hours, until Canvas shows it submitted. It runs as a Telegram bot: there's nothing to install, and the buttons work from your lock screen.
Private beta: 6 students at several universities use it every day. A public release is planned. See the roadmap.
| βοΈ Ranked morning list | Everything due in the next 14 days, ranked by urgency, share of your final grade and estimated effort, with a one-line explanation for each item. It arrives at the time you choose, in your own time zone. |
| β° Reminders until submitted | One combined message every 2 hours while anything is due within 24 hours. Submitting in Canvas stops it automatically, usually within 10 minutes. |
| β One-tap buttons | Done for paper or in-class work Canvas can't see, Snooze 2h, Open in Canvas, and Undo within 10 minutes. |
| π Respects your sleep | Quiet hours you choose, an 11:30 PM warning about anything due overnight, and one overdue notice instead of endless nagging. |
| π Private by design | Read-only Canvas access, tokens encrypted at rest, your token message deleted from the chat, invite-only access, and /disconnect erases everything. |
| π§ Honest about stale data | If Canvas can't be reached, every message says how old its data is. It never claims "nothing due" when it doesn't know. |
flowchart LR
C[("Canvas LMS<br/>Planner API")] -- "every 10 min (30 min at night)<br/>ETag-cached, read-only" --> S[Sync]
S --> DB[("Your own<br/>SQLite file")]
DB --> P{{"Scheduler<br/>(every minute)"}}
P -- "8:00 AM" --> D["βοΈ Ranked morning list"]
P -- "every 2 h while<br/>due < 24 h" --> R["β° Reminder"]
P -- "23:30" --> O["π Overnight warning"]
P -- "once, when late" --> X["β Overdue notice"]
D & R & O & X --> T(["Telegram"])
T -- "β
Done Β· π€ Snooze Β· β©οΈ Undo" --> DB
An item counts as done when Canvas shows it submitted, graded (for paper or in-class work), excused or marked complete, or when you tap β Done. If its due date moves, an extension re-opens it automatically. The ranking is a transparent 100-point score:
pie showData
title The 100-point priority score
"Urgency: time left (0β50)" : 50
"Importance: share of grade (0β35)" : 35
"Effort: estimated hours vs. time left (0β15)" : 15
The full rules, with a timeline of a real reminder day, are in How it works.
|
You need an invite link from whoever runs the bot.
|
You need a Telegram account and an always-on Linux server (Google Cloud's free tier works).
|
| Command | What it does |
|---|---|
/today Β· /week |
What's due today, or in the next 7 days by priority |
/digest |
Your morning list, right now |
/sync Β· /status |
Check Canvas now Β· last sync, token health, next list and reminder |
/pause 3h |
Pause reminders (30m, until 18:00, off) |
/settings |
Time zone, morning time, quiet hours, Canvas token or address |
/email you@school.edu |
Also email the morning list and a heads-up 2 h before each deadline (/email off stops it) |
/disconnect |
Delete everything the bot knows about you |
/invite Β· /users Β· /remove N |
Owner only: invite links, user health (never anyone's assignments), removal |
flowchart TB
TG(["Telegram"]) <--> H
subgraph Bot["Bot container"]
H["Handlers<br/>invite-only gate Β· per-user ordering"] --> HUB["Hub<br/>sign-up Β· settings Β· owner commands"]
HUB <--> REG[("registry.db<br/>users Β· invites<br/>π encrypted tokens")]
HUB --> T3 & T2 & T1
subgraph T3["Tenant: β¦"]
N1["Sync"] --> N2[("β¦.db")] --> N3["Scheduler"]
end
subgraph T2["Tenant: Ben"]
B1["Sync β Ben's Canvas"] --> B2[("ben.db")] --> B3["Scheduler"]
end
subgraph T1["Tenant: Ana"]
A1["Sync β Ana's Canvas"] --> A2[("ana.db")] --> A3["Scheduler"]
end
end
- Isolation by construction. Every user gets their own database file, Canvas client and scheduler. Each Telegram update is routed by the sender's ID, so no code path can reach another user's data.
- Functional core, imperative shell. All scheduling and ranking is a pure function,
plan(now, snapshot, settings) β Plan, with no I/O. That makes it simple to test exhaustively. - No duplicate messages. Every message has a deterministic key, claimed atomically in SQLite before it's sent, so restarts, crashes and clock changes never send something twice.
- Fault isolation. Each user's tick runs on its own. One school's Canvas hanging never delays anyone else's reminders.
Architecture deep dive β (components, data model, sign-up state machine, delivery guarantees, failure modes)
| Area | Choice | Why |
|---|---|---|
| Language | Python 3.13 (asyncio), strict typing |
One process handles polling, scheduling and chat concurrently |
| Telegram | python-telegram-bot 22 | Mature async library: inline keyboards, deep links, concurrent updates |
| HTTP | httpx | Async, timeouts, mockable transport for tests |
| Storage | SQLite (WAL), versioned SQL migrations | Zero-ops, a file per user, atomic claims with UNIQUE |
| Config | Pydantic Settings | Validated environment, secrets wrapped so they're never echoed |
| Crypto | cryptography (Fernet) | Authenticated encryption for stored Canvas tokens |
| Time | zoneinfo + tzdata |
IANA zones, correct across daylight-saving changes |
| Packaging | uv (locked), Docker multi-stage build | Reproducible installs, small hardened image |
| Quality | pytest, Hypothesis, respx, time-machine, mypy --strict, Ruff (incl. Bandit rules), pip-audit |
See Testing |
| CI/CD | GitHub Actions (SHA-pinned), Dependabot | Lint, types, tests on 3.13 and 3.14, coverage gates, audit, Docker build |
| Hosting | Docker Compose on a Debian/Ubuntu VM | Runs at $0 on Google Cloud's free tier |
| 426 automated tests | Unit, property-based, integration, end-to-end and a 20-day minute-by-minute simulation |
| 94% line + branch coverage | 98% on the scheduling and ranking core (CI fails below 95%) |
0 mypy --strict errors |
Across 106 source and test files |
| 18 review findings fixed | From two independent whole-codebase reviews, each with a regression test |
| 28,800 simulated scheduler ticks | Across a daylight-saving change, against a hand-reviewed golden file |
Canvas tokens are as powerful as a password, so the bot treats them that way:
- Encrypted at rest: Fernet (AES-128-CBC + HMAC-SHA256), with the key stored outside the data volume.
- Never left in chat: a message containing a token is deleted at whatever step it was pasted.
- Never in logs: known secrets and token-shaped strings are redacted from every log line, including exceptions.
- Hardened Canvas client: read-only (GET only), HTTPS on port 443, host-pinned with no redirects or cross-host pagination, and private or internal addresses refused.
- Least-privilege container: non-root, read-only filesystem, no Linux capabilities, file-based secrets.
Security policy and threat model β
Important
Instructure's API policy says apps used by multiple people should use OAuth, and that asking other users to create and paste a personal token is not allowed. The shared bot does exactly that, so it's run as a small, invite-only private beta among friends who understand the trade-off. Before a public launch the plan is to use OAuth through participating schools, or a calendar-feed mode that needs no token (see the roadmap). Using your own token for your own data is the most defensible setup; check your school's IT policy if you're unsure.
βββ src/canvas_tracker/
β βββ domain/ # models, completion rule, 100-point priority score (pure)
β βββ planning/ # scheduler: digest, reminder cycles, quiet hours, send keys (pure)
β βββ render/ # message text, keyboards, 4096-char splitting, all user-facing copy
β βββ sources/ # read-only Canvas REST client, Planner and enrichment mappers
β βββ store/ # SQLite repository and migrations (one file per user)
β βββ runtime/ # tick loop, sync scheduling, secret-redacting logs
β βββ notify/ # Telegram and email (SMTP) delivery
β βββ bot/ # per-user commands and buttons
β βββ hub/ # multi-user layer: registry, encryption, sign-up, routing
βββ tests/ # unit Β· property Β· integration Β· simulation Β· contract
βββ deploy/ # one-command server installer
βββ spike/ # owner-run probes and a PII scrubber for real Canvas data
βββ docs/ # guides, architecture, design records, research
βββ Dockerfile Β· compose.yaml
βββ pyproject.toml Β· uv.lock
- Single-user MVP: digest, reminders, buttons, quiet hours, Docker (v0.1)
- Friends mode: shared invite-only bot, sign-up in chat, encrypted tokens, one-command install (v0.2)
- Beta feedback: tune ranking thresholds on real use; contract tests on scrubbed real Canvas data
- Calendar-feed mode for schools that block personal tokens (no token needed)
- OAuth with participating schools and stronger protection for a public release (per-user key wrapping, rate limiting, monitoring)
- Optional email: the morning list and a heads-up 2 hours before each deadline (any SMTP provider)
- Per-user reminder rhythm, localization (all copy already lives in one module)
| Joining guide | For students using a bot someone invited them to |
| Hosting guide | Set up, run, update and back up your own bot |
| How it works | Ranking, completion rule, reminder timeline, quiet hours |
| Architecture | Components, data model, state machine, guarantees |
| Testing | Test layers, simulation, coverage, how to run |
| Security | Threat model, controls, reporting a vulnerability |
| Design records | Product requirements and the friends-mode design |
| Background research | Canvas data access, channels, hosting and policy research |
Issues and pull requests are welcome. See CONTRIBUTING.md for setup (uv sync), the checks CI runs, and conventions. Please report security problems privately as described in SECURITY.md.
Apache-2.0. Not affiliated with Instructure or Telegram. Canvas is a trademark of Instructure, Inc.
Why Telegram and not SMS? A bot has to rent a phone number and register with US carriers (about $20 up front plus $7β8 a month), and the free email-to-text gateways have shut down. Telegram is free, works on every phone and has buttons.