LaputanMachines/vibersvp
GitHub: LaputanMachines/vibersvp
一个基于 Airtable 和 GitHub Actions 的志愿者排班 RSVP 工具,自动发送去重后的邮件和短信提醒,专为竞选活动的 canvassing 志愿者管理而设计。
Stars: 0 | Forks: 0
# vibersvp
用于 **Jack Sandor for Victoria** 竞选活动中 canvassing 志愿者排班的 RSVP + 提醒工具。
**Airtable** 是数据库、公开的 RSVP 表单以及候选人的 dashboard。
此 repo 是一个小型的 **Python worker**,用于执行 Airtable 无法做到的一件事:发送定时的、去重后的 **email + SMS** 提醒。它完全在 **GitHub Actions** 上运行——无需任何 self-hosted。
```
Volunteer → Airtable RSVP form ─┐
├→ Airtable base (Events, RSVPs, ReminderLog)
Jack → Airtable dashboard ──────┘ ▲
│ reads/writes
GitHub Actions cron (every 15m) → python -m vibersvp.run → Resend (email) + Twilio (SMS)
```
## 1. 设置 Airtable base
创建一个 base(任意名称)并包含三个 table。字段名称必须完全匹配。
### `Events`
| 字段 | 类型 | 备注 |
|---|---|---|
| `Name` | 单行文本 | 例如 "Fernwood door-knock" |
| `Start` | 包含时间的 Date | 班次开始时间;请设置合理的时区 |
| `End` | 包含时间的 Date | 可选 |
| `Location` | 单行文本 | |
| `Status` | 单选 | `Draft`, `Open`, `Cancelled`, `Completed` — 提醒仅对 `Open` 触发;当活动结束后,worker 会自动将其设置为 `Completed` |
| `Reminder offsets` | 单行文本 | 可选覆盖,例如 `24h,2h:sms`;留空 = 使用默认值。添加 `:email`/`:sms` 可将偏移量固定到某一渠道 |
| `Notes` | 长文本 | 可选;将包含在提醒中 |
### `RSVPs`(这是表单的 table)
| 字段 | 类型 | 备注 |
|---|---|---|
| `Name` | 单行文本 | |
| `Email` | Email | |
| `Phone` | 电话号码 | 理想情况下为 E.164 格式,例如 `+12505550123` |
| `Event` | 链接到 `Events` | |
| `Status` | 单选 | `Going`, `Not Going` — 只有 `Going` 才会收到提醒 |
| `Created` | 创建时间 | **new-RSVP alerts**(见下文)必需;否则为可选 |
### `ReminderLog`(由 worker 写入 — 请勿手动编辑)
| 字段 | 类型 |
|---|---|
| `Key` | 单行文本 |
| `RSVP` | 链接到 `RSVPs` |
| `Event` | 链接到 `Events` |
| `Offset` | 单行文本 |
| `Channel` | 单选 (`Email`, `SMS`) |
| `Sent at` | 包含时间的 Date |
| `Status` | 单选 (`Sent`, `Failed`) |
| `Provider message id` | 单行文本 |
| `Error` | 长文本 |
### 两个前端(无需代码)
- **RSVP 表单** — 在 `RSVPs` table 上创建一个 **Form** 视图,显示 Name、Email、Phone
和 Event(添加 CASL 措辞:发送方是谁 + 如何退订 / "reply STOP")。
与志愿者分享公开的表单链接。
- **Jack 的 dashboard** — 构建一个按 `Event` 分组的 **Interface**,显示 RSVP 计数
和名单。与 Jack 分享 **只读** 链接。
创建一个 **Personal Access Token**(Airtable → Builder hub → Personal access tokens),并赋予作用于此 base 的 `data.records:read`、`data.records:write` 和 `schema.bases:read` 权限。从你 base 的 API 文档中获取 base ID(`app…`)。
## 2. 配置
复制 `.env.example` → `.env` 并填写内容。有关每个变量的详情,请参阅该文件。
你至少需要 Airtable token + base ID 以及 Resend 密钥。**将 Twilio 变量留空**,直到你的号码通过验证 — SMS 将被直接跳过,email 仍能正常工作。
## 3. 部署到 GitHub Actions(无需 hosting)
工作流位于 `.github/workflows/reminders.yml` — 每 15 分钟运行一次 `python -m vibersvp.run --once`。
1. 将此 repo 推送到 GitHub。
2. **费用:** **公开** 的 repo 可获得无限的免费 Actions 分钟数(推荐使用 — 代码中不存在任何 secrets)。如果你将其保持为 **私有**,请将 cron 修改为 `*/30 * * * *` 以保持在每月免费分钟数限制内。
3. 在 **Settings → Secrets and variables → Actions** 中,添加:
- **Secrets:** `AIRTABLE_API_TOKEN`, `AIRTABLE_BASE_ID`, `RESEND_API_KEY`, `EMAIL_FROM`,
`EMAIL_REPLY_TO`,以及(稍后添加的)`TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TWILIO_FROM_NUMBER`,
`JACK_PHONE`(用于 new-RSVP alerts — 见下文)。
- **Variables:** `CAMPAIGN_NAME`, `CAMPAIGN_CONTACT`, `TIMEZONE`, `DEFAULT_REMINDER_OFFSETS`,
`EMAIL_FROM_NAME`, `NEW_RSVP_LOOKBACK`(可选;默认值为 `24h`)。
4. 从 **Actions** 标签页 (`workflow_dispatch`) 触发手动运行以进行测试,然后让计划任务接管。
### 电子邮件 (Resend)
创建一个 Resend 账户并 **验证你的发送域**(添加 DNS 记录),以确保提醒能正常发送到收件箱。将 `EMAIL_FROM` 设置为该域上的地址。
### SMS (Twilio) — 稍后启用
向加拿大号码发送 SMS 需要一个通过 **toll-free verification** 或 **A2P 10DLC** 注册(需要竞选活动的加拿大商业编号;审核需要数天至数周)的 Twilio 号码。在此之前,请保持 Twilio 变量未设置并仅运行 email。获得批准后,添加那三个 Twilio secrets — 无需更改代码。
### New-RSVP alerts(当有人报名时向 Jack 发送短信)
当志愿者 RSVP **`Going`** 时,worker 会向组织者发送一次短信。将 `JACK_PHONE` secret 设置为要通知的手机号码(E.164 格式,例如 `+12505550123`)。需要配置 Twilio(上述三个变量) — 若无配置,提醒将像一样被跳过。
- **每次 RSVP 仅一次。** 该 alert 复用 `ReminderLog` 的幂等性机制:它会写入一行数据,其中 `Offset = new-rsvp`,`Channel = SMS`,因此重新运行 cron 绝不会重复发送短信。
- **首次部署保护。** 只有 `Created` 时间在 `NEW_RSVP_LOOKBACK`(默认 `24h`)范围内的 RSVP 才会被视为“新”,因此开启此功能不会因为现有的历史 RSVP 数据而打扰组织者。这就是 `RSVPs` table 上的 `Created` 字段对此功能 **必需** 的原因 — 没有 `Created` 值的 RSVP 将被跳过。
- **无免打扰时段。** 与志愿者提醒不同,这些发送给组织者自身号码的运营 alert 会在白天或夜间立即发送。
若要禁用,请将 `JACK_PHONE` 保持未设置状态。
## 4. 本地开发与测试
```
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
# 离线单元测试(无网络,无 Airtable):
pytest
# 针对你的真实 base 进行 dry run — 记录 WOULD 发送什么,不进行任何发送,也不写入任何 log rows:
python -m vibersvp.run --once --dry-run
# 覆盖时钟以测试特定时刻:
python -m vibersvp.run --once --dry-run --now 2026-07-01T17:00:00Z
```
**端到端冒烟测试:** 创建一个大约在 2 小时和 24 小时后开始的测试活动,为你自己 RSVP(使用你自己的 email/phone),在不使用 `--dry-run` 的情况下运行,确认你能收到消息并且 `ReminderLog` 中出现了相应的行 — 然后再次运行并确认 **没有任何内容被重新发送**。
## 工作原理(安全模型)
- `scheduler.compute_due_reminders` 是 **纯粹的**:给定活动、RSVPs 和 `now`,它会返回发送窗口(`start − offset ≤ now < start`)处于开启状态的提醒。
- 每个提醒都有一个稳定的 **key**(`rsvp::offset::channel`)。在发送之前,worker 会根据 `ReminderLog` 检查该 key;发送之后,它会将 key 写回。因此该任务是 **幂等的** — 每 15 分钟运行一次是安全的,并且能够抵御 cron 漂移或漏发的情况。
- 每个偏移量都会选择各自的渠道:一个纯偏移量(`24h`)会在志愿者提供了联系信息的每一个渠道上进行提醒,而带有后缀的偏移量(`2h:sms`)则会固定在一个渠道上。默认的 `24h,2h:sms` 会通过 email + SMS 发送 24 小时提醒,并将 2 小时提醒仅作为文本发送。
- SMS 会在当地的免打扰时段(默认为上午 9 点至晚上 9 点)之外发送;延迟的短信会在发送窗口内由后续的运行提取并发送。
- 当有人 RSVP `Going` 时,组织者会收到一条一次性短信(`JACK_PHONE`),该短信通过相同的 `ReminderLog` key 机制进行去重,并受 `NEW_RSVP_LOOKBACK` 限制仅针对最近的 RSVP。
- 活动结束后,worker 会将其 `Status` 从 `Open` 更改为 `Completed`(如果已设置,则使用 `End`,否则使用 `Start`),以便 dashboard 反映已完成的内容。`Draft`、`Cancelled` 以及已经是 `Completed` 的活动将保持不变。
标签:Airtable, GitHub Actions, 定时任务, 网络调试, 自动化, 自动笔记, 逆向工具, 通知提醒