notify() — Notification Abstraction¶
Stub only. The function exists but does not send — Discord DM and email paths are not yet implemented.
Purpose¶
A shared utility that sends a message to a member through the appropriate channel based on their membership state. Prevents the bot from hardcoding Discord DM calls throughout the codebase, and provides a hook for the P3 email path without requiring refactoring.
Interface¶
await notify(person: GristPerson, message: str, subject: str = None) -> NotifyResult
person— a resolved$Peoplerow (or equivalent object withdiscord_idandnewsletter_email)message— plain text or markdown bodysubject— used for email only; ignored for Discord DMs- Returns a
NotifyResultindicating which channel was used and whether it succeeded
Dispatch Logic¶
if person has a linked Discord_users row with a known snowflake:
→ send Discord DM
→ log result
elif person has a newsletter_email (preferred_email_address or initial_email_address):
→ send email via Proton SMTP [P3 — not implemented initially]
→ log result
else:
→ log as undeliverable
→ post alert to BOT_ALERTS_CHANNEL_ID
For the initial build, only the Discord DM path is implemented. The email branch is a stub that logs "email notify not yet implemented" and returns a failed result. This stub must exist from day one so callers don't need to change when email is added.
Usage Throughout the Codebase¶
Every place the bot needs to contact a member should go through notify() — never call discord.User.send() directly in command handlers.
Current callers:
- /rsvp accept — "your RSVP has been accepted"
- /rsvp waitlist — "you're on the waitlist"
- /onboard — buddy assignment notification to the buddy (P2)
- /roster — referral confirmation DM to referrer
- /roster — unvetted Discord welcome DM and matching flow
- Future /treasurer renewals — dues renewal reminders
Implementation Notes¶
- Requires
GUILD_MEMBERSprivileged intent to DM members who haven't DMed the bot first — already flagged as a day-one requirement - If the Discord DM fails (user has DMs disabled): log the failure, post a fallback alert to the organizing thread or
#records-committeeindicating the DM couldn't be delivered, do not silently swallow the error - Discord rate limits DMs — use
asyncio.sleepbetween bulk DM operations (e.g. notifying all accepted RSVPs at once). Space calls at least 1 second apart. - The
subjectparameter is reserved for email and ignored by the Discord path — always pass it for calls that will eventually support email so callers don't need updating
NotifyResult¶
@dataclass
class NotifyResult:
success: bool
channel: str # "discord_dm" | "email" | "undeliverable"
error: str | None = None
Callers should log failures but not raise — a failed notification should not abort the underlying operation (e.g. RSVP acceptance still succeeds even if the DM fails).
P3 Email Path¶
When email is implemented:
- Use
aiosmtplibwith Proton SMTP relay (smtp.protonmail.com:587, STARTTLS) fromaddress: org email address (configured viaPROTON_SMTP_USER)toaddress:$People.newsletter_email(already implements priority logic: preferred → initial → national email)- Templates in
/templates/notify/— one per notification type, same{{placeholder}}syntax as event templates subjectparam maps to email subject line
Do not implement until Discord path is confirmed stable and P3 non-Discord member onboarding is scoped.