Apollo Integration¶
Apollo is a third-party Discord bot that posts event embeds with RSVP buttons. ClanGuard captures those embeds, parses them into structured ApolloEvent records, and syncs them to Google Calendar so officers see the same schedule outside Discord.
Pipeline¶
flowchart TB
Apollo[Apollo posts event] --> Capture[ApolloMessageCaptureHandler]
Capture --> Log[(ApolloMessageLog)]
Log --> Worker[ApolloMessageParserWorker]
Worker --> Parser[ApolloEmbedParser]
Parser --> Event[(ApolloEvent)]
Event --> Cal[CalendarEvent + Google Calendar]
Apollo -- edits --> Capture
Capture -- updated --> Recon[ApolloReconciliationService]
Recon --> Event
Recon --> Cal
Components¶
| File | Role |
|---|---|
ApolloMessageCaptureHandler.cs |
Subscribes to MessageReceived and MessageUpdated. Filters by ApolloBotName. Persists raw messages to ApolloMessageLog. |
ApolloMessageLog.cs (entity) |
Raw audit log of every captured Apollo message. |
ApolloMessageParserWorker.cs |
Background worker that pulls unparsed ApolloMessageLog rows and runs them through the parser. |
ApolloEmbedParser.cs |
Pure parser. Takes Apollo embed JSON, returns a structured ApolloEvent. |
ApolloEmbedReconstructor.cs |
Inverse: rebuild a synthetic embed from a stored ApolloEvent (used for the calendar event description). |
ApolloEvent.cs (entity) |
Parsed event record: title, time, RSVP counts, etc. |
ApolloEventHandler.cs |
Coordinates the parsed-event → calendar sync. |
ApolloReconciliationService.cs |
Periodic reconciliation: re-parse, re-sync, drop calendar dupes. |
ApolloBackfillService.cs |
One-shot historical backfill of older Apollo messages on startup if the log is short. |
Why we cache 500 messages¶
Discord doesn't deliver MessageUpdated for messages outside the gateway cache. Apollo edits the same message repeatedly to reflect RSVP changes, so without a cache the bot would only see the original post and miss every update.
MessageCacheSize = 500 (set in Program.cs) is the compromise between memory use and reliable update capture across short bot restarts.
Capture flow¶
- Apollo posts an embed in
#events. ApolloMessageCaptureHandler.OnMessageReceivedfires:- Confirms
Author.Username == ApolloBotName. - Persists the full message JSON to
ApolloMessageLogwithParsedAt = NULL.
- Confirms
ApolloMessageParserWorkerpolls for unparsed rows.ApolloEmbedParserextracts:- Event title
- Start time (with timezone)
- End time (or duration → end)
- Attendee list per RSVP status (Accepted / Declined / Tentative / etc.)
- The result is upserted to
ApolloEvent(keyed by Apollo message ID). ApolloEventHandlersyncs to Google Calendar viaGoogleCalendarService. The calendar event's description includes a reconstructed text version of the embed for officers viewing the calendar directly.ApolloMessageLog.ParsedAtis stamped so the worker doesn't re-process.
When Apollo edits the message:
OnMessageUpdatedfires.- Fresh
ApolloMessageLogrow is appended (we keep the audit trail of every revision). - Worker re-parses;
ApolloEventis updated by Apollo message ID. - Calendar event is updated in place.
Reconciliation¶
ApolloReconciliationService runs on a schedule and:
- Re-parses any
ApolloMessageLogrows where parsing previously failed. - Detects orphan
CalendarEventrows (no matchingApolloEvent) and removes them. - Detects duplicate calendar events (same
ApolloEvent, multipleCalendarEventrows) and removes the extras.
/cleanup-calendar-dupes (MAJ+) triggers reconciliation manually.
Parser landmines¶
Smart punctuation crashes the parser
Curly quotes (" "), em-dashes (—), and other smart-punctuation characters in event titles have crashed ApolloEmbedParser historically. If a new Apollo embed format is introduced or a member uses unusual characters, parsing fails and the row stays unparsed in ApolloMessageLog.
Mitigation: check ApolloMessageParserWorker logs for parse failures. The fix is usually to broaden the parser's character handling, not to ask members to change their input.
Apollo can change its embed format
Apollo's embed structure has evolved before. If new events stop appearing, first thing to check is whether the embed shape has changed:
sql
SELECT MessageId, RawJson FROM ApolloMessageLog
ORDER BY CapturedAt DESC LIMIT 1;
Compare against the parser's expected fields.
Timestamp handling
Earlier versions of the parser had a bug with Apollo embed timestamps where the parser's date conversion misinterpreted the embed's epoch field. The fix landed during recent work — if events start showing up at obviously wrong times (off by a day or in the wrong year), regression-test against ApolloMessageLog raw JSON.
RSVP counts¶
ApolloEvent stores attendee user IDs per RSVP bucket. These feed into:
- The grouped event announcement
- Event attendance calculations (cross-referenced with
EventAttendancevoice presence)
Calendar event description¶
ApolloEmbedReconstructor rebuilds a markdown-style description from a stored ApolloEvent, used as the Google Calendar event description. This way officers viewing the calendar in Google get the same structured info they'd see in Discord, even though the calendar can't render Discord embed formatting.
Backfill¶
ApolloBackfillService runs once on startup. If ApolloMessageLog has fewer than N entries, it pulls the last N messages from the events channel where the author was Apollo, persists them, and lets the parser worker handle them. This is how a fresh bot deployment catches up on recent events.
Common operational questions¶
An event is missing from the calendar.
- Check
ApolloMessageLogfor the original message — was it captured? - If captured, check
ParsedAt— was it parsed? - If parsed, check
ApolloEventfor the row. - If
ApolloEventexists but the calendar event is missing, run/cleanup-calendar-dupes(which also re-syncs missing events).
An event is on the calendar twice.
Almost always Apollo edited the original and the previous calendar entry didn't get cleaned up. Run /cleanup-calendar-dupes. If duplicates persist, check CalendarEvent rows by ApolloEventId to see what's actually mapped.
RSVP counts are wrong.
Capture happens on MessageUpdated. If the bot was offline during an edit and the message has since fallen out of the 500-message cache, the latest state isn't captured. The next edit will refresh things. To force a re-fetch, an officer can edit the embed (Apollo will repost) or wait for the next reconciliation cycle.