Skip to content

Commit 97cff8c

Browse files
committed
fix: trim README short-msg-flood section and clarify approved-user guard
Two refinements from post-implementation review: - README.md: drop the trigger-formula code block, locator-derivation paragraph, separate "When triggered" paragraph, dedicated "Rollout note", and split-out "Naturally terse legitimate users" paragraph. The section now matches the brevity of neighboring checker sections (single intro, one Important callout, Configure with). Implementation detail belongs in CLAUDE.md (already there). - lib/tgspam/detector.go: add a comment on the approved-user fast-bail in isShortMsgFlood explaining why it exists. In production main.go forces FirstMessageOnly=true whenever FirstMessagesCount > 0, so approved users short-circuit at the pre-approved branch in Check and never reach this method. The guard is defensive against library consumers that construct Detector with FirstMessageOnly=false and FirstMessagesCount > 0. Related to #399
1 parent 90cf51d commit 97cff8c

2 files changed

Lines changed: 6 additions & 21 deletions

File tree

README.md

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -206,31 +206,12 @@ Configure with:
206206

207207
**Short-message flood detection**
208208

209-
This option is disabled by default. When enabled, the bot bans an unapproved user who has accumulated too many short messages without graduating to "approved" status. It targets the evasion pattern where a spammer probes a channel by posting innocuous short messages ("hi", "hello", "yo", "ok"): each individual message is too short for content checks (`--min-msg-len`), and the messages are different so the duplicate detector does not trigger.
209+
This option is disabled by default. When enabled, the bot bans an unapproved user who has accumulated too many short messages without graduating to "approved" status. This catches spammers who probe a channel with innocuous one-word messages ("hi", "hello", "yo") that individually evade content-based checks and the duplicate detector.
210210

211-
The trigger formula derives the count from existing state without any new in-memory tracking:
212-
213-
```
214-
(messages_from_user - approved_count) >= max-short-msg-count
215-
AND user is still unapproved (approved_count < first-messages-count)
216-
AND the current message is short (length < min-msg-len)
217-
```
218-
219-
`messages_from_user` comes from the on-disk locator (the same store that powers duplicate detection), and `approved_count` is the in-memory count of long ham messages that contributed to user approval. The difference — messages that did *not* graduate the user — is overwhelmingly short messages.
220-
221-
When triggered, the result follows the standard spam pipeline: ban + delete the current message + delete the prior messages via `ExtraDeleteIDs` cleanup. `--training`, `--dry`, and `--soft-ban` all intercept the action just like with any other check.
222-
223-
**Important**: this check returns immediately when triggered and bypasses LLM consensus (`--llm.consensus`) by design. The signal is behavioral, not content-based; an LLM looking at a single short message has no information that would justify overriding the count. Operators who expect consensus to apply uniformly should be aware of this asymmetry.
224-
225-
**Important**: this check requires the detector's first-message evaluation path to be active. By default (`--first-messages-count=1`) it is active and no extra configuration is needed. `--paranoid` mode is incompatible — it disables the first-message path entirely and clears the count, leaving this check no window to operate in. The bot will refuse to start with `--max-short-msg-count > 0` together with `--paranoid`.
226-
227-
**Naturally terse legitimate users**: a user posting only short messages during their first few messages can trip this check before they graduate. The risk is bounded to the evaluation window (`--first-messages-count` messages) — once approved, the check skips entirely for that user's lifetime. Recommended baselines: `--max-short-msg-count >= 3` paired with a low `--first-messages-count` (1 or 2).
228-
229-
**Rollout note**: on first activation against a populated database, existing unapproved users with N or more rows in the locator will trip on their next message. This is intended (those users are exhibiting the pattern), but be aware of it during rollout.
211+
**Important**: this check requires the first-message evaluation path (`--first-messages-count > 0` or `--first-message-only`); `--paranoid` mode is incompatible and rejected at startup. The risk window for naturally terse legitimate users is bounded to the evaluation period; once approved, the check skips for the rest of that user's lifetime.
230212

231213
Configure with:
232214
- `--max-short-msg-count=, [$MAX_SHORT_MSG_COUNT]` (default: 0, disabled) - Ban after N short messages from an unapproved user
233-
- Pair with `--first-messages-count` and a meaningful `--min-msg-len` (paranoid mode is incompatible)
234215

235216
Recommended config: `--max-short-msg-count=3 --first-messages-count=2 --min-msg-len=50`.
236217

lib/tgspam/detector.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,6 +1073,10 @@ func (d *Detector) isShortMsgFlood(req spamcheck.Request) spamcheck.Response {
10731073
return notSpam("message not short")
10741074
}
10751075
approvedCount := d.approvedUsers[req.UserID].Count
1076+
// defensive: in app/main.go FirstMessageOnly is forced true whenever FirstMessagesCount > 0,
1077+
// so approved users short-circuit at the pre-approved branch in Check and never reach this.
1078+
// the guard matters for library consumers that construct Detector with FirstMessageOnly=false
1079+
// and FirstMessagesCount > 0, where approved users would otherwise fall through to here.
10761080
if approvedCount >= d.FirstMessagesCount {
10771081
return notSpam("user already approved")
10781082
}

0 commit comments

Comments
 (0)