LaputanMachines/vibersvp

GitHub: LaputanMachines/vibersvp

一个基于 Airtable 和 GitHub Actions 的志愿者排班 RSVP 工具,自动发送去重后的邮件和短信提醒,专为竞选活动的 canvassing 志愿者管理而设计。

Stars: 0 | Forks: 0

vibersvp — RSVP + reminders for canvassing volunteer shifts

# 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, 定时任务, 网络调试, 自动化, 自动笔记, 逆向工具, 通知提醒