Skip to content

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 $People row (or equivalent object with discord_id and newsletter_email)
  • message — plain text or markdown body
  • subject — used for email only; ignored for Discord DMs
  • Returns a NotifyResult indicating 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_MEMBERS privileged 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-committee indicating the DM couldn't be delivered, do not silently swallow the error
  • Discord rate limits DMs — use asyncio.sleep between bulk DM operations (e.g. notifying all accepted RSVPs at once). Space calls at least 1 second apart.
  • The subject parameter 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 aiosmtplib with Proton SMTP relay (smtp.protonmail.com:587, STARTTLS)
  • from address: org email address (configured via PROTON_SMTP_USER)
  • to address: $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
  • subject param maps to email subject line

Do not implement until Discord path is confirmed stable and P3 non-Discord member onboarding is scoped.