mirror of
https://github.com/dsec-hub/dsec-discord-bot.git
synced 2026-09-22 15:53:56 +00:00
* COL-BOT-03: make /member_info expiry reply ephemeral and drop dead query
Restructure the membership lookup to `let Some(user_data) = ... else { ... }`
so the "Couldn't find info" branch now sends `.ephemeral(true)` like the other
two replies; whether a member's membership has lapsed is no longer announced to
the whole channel. Delete the commented-out `student_data` query that duplicated
member_data(). No embed fields or wording changed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE
* COL-BOT-05: parse all config once in AppState, remove per-event env reads
Finishes the AppState config hoisting (COR-12's groundwork was not present in
the repo, so this ticket establishes it): AppState now carries guild_id,
honeypot_channel_id, leetcode_channel_id, verified_role_id, logs_channel_id and
weather_token, all parsed once in AppState::new(). A missing or unparseable id
now produces one boot error naming every offending variable plus a pointer to
.env.example, instead of a per-event .expect() panic (a data race under the
multi-threaded runtime, since dotenv() calls the now-unsafe set_var).
- main.rs: new fields, required_u64() collector, fail-fast with combined message.
- events/message.rs: honeypot/create_leetcode_thread/on_message take &Data and
read channel/guild ids from AppState; dropped dotenv() and env::var.
- events/interaction_create.rs: deleted verified_role_id(); use
data.state.verified_role_id.
- commands/mods_only.rs: log_embed takes logs_channel_id: ChannelId; caller in
message.rs passes data.state.logs_channel_id.
- commands/weather.rs: WEATHER_TOKEN read once into AppState (non-fatal), passed
to get_weather — required so the new CI grep guard can be clean.
- .env.example: add LEETCODE_CHANNEL_ID.
- .github/workflows/ci.yml: add "No environment reads outside main.rs" guard.
grep -rn 'dotenv()\|env::var' src/ | grep -v '^src/main.rs:' now returns nothing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE
* COL-BOT-02: guard the honeypot, log failed bans, stop log_embed panicking
The honeypot banned whoever posted, unconditionally. Add guards so it never
bans a bot, a webhook, the bot itself, or anyone who can moderate
(ban_members / manage_messages / administrator, computed from the cached guild).
A failed ban now logs the user and error instead of being silent.
Remove the .expect("LOG FAIL") in log_embed: it runs inside a message handler,
so a transient Discord failure was a panic; it now eprintln!s and returns.
Decouple on_message so honeypot and the LeetCode thread each run and log
independently — a failure in one no longer skips the other, and neither
propagates out of the event handler.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE
* COL-BOT-04: clamp LeetCode thread names and add the repo's first tests
create_title_from_message dropped the two-character numbering prefix but a
first line of two chars or fewer left an empty name, and a line over 100 chars
exceeded Discord's limit — both produced a silent 400. Move the function to
module level (&str), clamp to 100 characters (on a char boundary, not bytes),
and fall back to "LeetCode discussion" when nothing is left. A failed
create_thread now logs instead of propagating. Add six unit tests covering
normal input, empty, a two-char line, a >100-char line, a multibyte first
character, and a multi-line message — the first tests in this repo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE
* COR-03: fix verification failing on a member's first attempt
The insert into dsec_discord_members deserialised the response into
Vec<DiscordMemberRow>, but without `.returning(...)` supabase-lib-rs never sends
`Prefer: return=representation`, so PostgREST returns a 201 with an empty body.
Parsing that empty body failed and the `?` aborted before add_role and before
any reply — so every member's first click died with "This interaction failed",
while the row was still written (which is why the second attempt worked). Add
`.returning("student_id,discord_id")` so the row comes back and parses.
Also wrap the post-modal verification work so any failure sends the user an
ephemeral "something went wrong" instead of a dead interaction (poise's on_error
has no handle to the modal submission), and set an explicit on_error on
FrameworkOptions that replies ephemerally on command errors and keeps the
default logging. The database-write-first ordering is left unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE
* NEW-UXA11Y-11: normalise names and student IDs before comparing them
Verification compared names with exact lowercased equality and never trimmed, so
a trailing space, a double space, or a missing middle name turned a real paid
member away — and a pasted "s123456789 " was looked up with the trailing space
and missed. Add normalise_name (trim + collapse whitespace), normalise_student_id
(strip whitespace and a leading "s"), and name_matches (equal, or every typed
word present in the roster name in order), and route the database path, the cache
path (cached_name_matches / cache_student) and the student-id lookup through them.
"Doe John" still does not match "John Doe" and an empty name matches nothing.
Update the modal placeholder to "As it appears on your DUSA membership". Four unit
tests added. The two failure embeds are left byte-identical (SEC-19 owns those).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE
* OPS-04: restart on crash, real logging, and a deploy that fails when the bot does
- docker-compose.yml: `restart: unless-stopped` so a panic, bad frame or VPS
reboot brings the bot back instead of leaving it dead. Comment explaining why
no healthcheck (slim runtime, no HTTP port).
- Cargo.toml: enable tracing-subscriber's env-filter feature (declared but never
initialised until now).
- main.rs: initialise tracing as the first statement in main(), defaulting to
`info` — serenity/poise/supabase logs now surface, without leaking student IDs
or the service email that the Supabase client emits at `debug`.
- deploy.yml: after `up -d`, assert the container is actually Running 30s later
and dump its logs and fail if not — `up -d` returns 0 on container creation,
not on a working process.
- Dockerfile: pin the builder to rust:1-bookworm to match the bookworm-slim
runtime, removing the trixie/bookworm glibc mismatch.
- README: document the info-level default and the RUST_LOG=debug PII footgun.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE
* NEW-UXA11Y-11: require first AND last name to match (close a verification bypass)
Codex review: name_matches accepted any non-empty ordered subset, so a single
common token ("John" or "Doe") matched "John Michael Doe" — a student id plus one
name token could claim the verified role for someone else. Now both the first and
last tokens must match, middle tokens the student typed must appear in the roster
in order (omitted middles still fine), and a single token, an arbitrary subset,
or a wrong first/last name is rejected. Add tests: single-token input, wrong
surname and wrong first name do NOT verify; full name and first+last with the
middle omitted do.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE
* OPS-04: make the deploy assertion catch a crash-looping bot
Codex review: with `restart: unless-stopped` a bot that panics at boot is
restarted, so a single `docker inspect` still reads Running while it crash-loops
— the deploy went green while broken. The assertion now waits for the bot to
reach READY (it logs "Logged in as ..." from the Ready handler) within a
stability window AND requires RestartCount == 0, breaking early and failing the
moment a restart is observed, dumping container logs on any failure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE
* COL-BOT-05: stop /weather panicking on missing token or error payloads
Codex review: a missing/blank WEATHER_TOKEN and invalid locations produced an API
error body that then hit as_str().unwrap() calls. Store the token as
Option<String> (None when missing or blank) and disable /weather with a friendly
ephemeral reply instead of running with an empty key. get_weather now returns the
HTTP status with the parsed body; a non-2xx surfaces the API's error message, and
every success field is read via JSON pointers with no unwrap() on external JSON.
The thumbnail is only set when an icon URL is actually present.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE
* COL-BOT-02: make the honeypot moderator guard fail safe
Codex review: the guard fell THROUGH to banning when the member lookup or the
permission calculation failed, and it ignored per-channel permission overwrites.
Now it computes effective permissions in the honeypot channel (honouring
overwrites via Guild::user_permissions_in) and ABSTAINS from banning whenever the
guild, the member, or the channel cannot be resolved — fail-safe: never ban when
identity or permissions cannot be established. Replaces the deprecated
Member::permissions call, so the #[allow(deprecated)] is gone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
15 lines
No EOL
361 B
TOML
15 lines
No EOL
361 B
TOML
[package]
|
|
name = "dsec_bot"
|
|
version = "0.1.0"
|
|
edition = "2024"
|
|
|
|
[dependencies]
|
|
dotenv = "0.15.0"
|
|
poise = "0.6.1"
|
|
reqwest = "0.12.24"
|
|
serde_json = "1.0.145"
|
|
serenity = "0.12"
|
|
tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread"] }
|
|
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
|
supabase-lib-rs = "0.5.3"
|
|
serde = "1.0.228" |