Conversation
* feat(cfg): add etc_clientblocker cfg keys + scripts.tbl entry (#81) Foundation commit for the AP/VE-based client blocker. Adds the cfg keys (oplevel, check_levels table, default_reason, max_pattern_len) to core/cfg_defaults.lua and the corresponding operator-facing section to examples/cfg/cfg.tbl. The scripts array gains an entry right after hub_inf_manager so the listener chain runs the structural validator BEFORE the policy filter - that ordering is load-bearing per §1a.2 and is called out in an inline comment. No behaviour change yet - the plugin file lands in the next commit. Part of #81. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(etc_clientblocker): bundled client AP/VE blocker plugin (#81) Promotes etc_clientblocker from luadch-ng/scripts (v0.2 by pulsar) into the core bundle and rewrites the persistence + operator surfaces: - patterns + per-pattern kick reasons live in scripts/data/etc_clientblocker.tbl (was hardcoded inline) - ADC commands: +addblocker <pattern> [reason] / +delblocker <pattern> / +blocker (list). Pattern is the first whitespace- token; everything after it is the kick reason. oplevel-gated via etc_clientblocker_oplevel. - HTTP API: GET /v1/clientblocker (read), POST + DELETE /v1/clientblocker (admin). DELETE takes the pattern as a URL-percent-encoded path var; matches the etc_aliases shape. - audit-fires client.block.kick on each block + client.block.add / .remove on each operator edit. Same actor/target conventions as cmd_ban / etc_aliases. - pattern validation at edit time: non-empty + length-capped via etc_clientblocker_max_pattern_len (default 200) + compile-probe via pcall(string.find, "", pat) so a bad pattern fails loud at POST time, not silent at onConnect kick time. - F-INF-1d nil-VE guard preserved from upstream v0.2: a client without a VE field has nothing to match against, so the check skips - mirrors "no rule applies" semantics for missing input. - operator levels (60..80) exempt by default via etc_clientblocker_check_levels so adding a self-matching pattern does not self-lockout. Public surface unchanged-style getters (resolve / get_patterns_tbl) follow the #239 / #238 rebind-hazard convention. Empty default .tbl ships with the bundle so the plugin loads as a no-op until the operator adds the first pattern. Part of #81. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(etc_clientblocker): unit test + smoke.yml wiring (#81) 61 checks across: - HTTP API: POST happy + default reason + bad_pattern (empty / over-length / compile-probe failure) + exists + GET sort + DELETE happy + not_found - check_clients onConnect listener: matching VE on covered level emits ISTA 231 + audit `client.block.kick`; matching VE on exempt operator level skips; non-matching VE skips; nil VE skips (F-INF-1d guard) - ADC handlers: +addblocker / +delblocker / +blocker hubcmd happy paths + level-denied + missing-args - resolve() input sanity (nil / number / empty) - +reload semantic: re-running onStart re-reads from disk Wired into both smoke.yml jobs (Linux + Windows msys2) next to the existing audit / aliases tests so a regression here fails the same CI gate. Part of #81. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(smoke): #81 etc_clientblocker end-to-end (test_clientblocker_81) Six-stage smoke test that owns the issue's primary acceptance criterion (blocked client -> kicked at connect): 1. POST /v1/clientblocker adds a unique smoke pattern 2. Connect with VE matching the pattern -> ISTA 231 + drop (the kick anchors on "ISTA 231" + the custom reason literal "smoke_blocked_81" to avoid the BINF-echo false-match noted in the #84 audit-test pattern 4) 3. Connect with a non-matching VE -> normal login 4. DELETE /v1/clientblocker/{pattern} -> 200 5. Reconnect with the previously-blocked VE -> normal login (verifies the in-memory patterns_tbl mutation took effect without a hub restart) 6. audit JSONL has client.block.{add,kick,remove} lines with the right meta.pattern field `_adc_login` gains optional `ve` / `ap` / `expect_kill` params. With expect_kill=True the helper watches for ISTA instead of the BINF echo / IGPA challenge - etc_clientblocker fires its kill on the onConnect listener BEFORE IGPA (verified at core/hub_dispatch.lua:589), so on a kick the password exchange never happens. Runs immediately after test_audit_log_84 so the same audit JSONL file is exercised by both checks. Idempotent: the pattern is unique to this test and a best-effort DELETE runs before the POST to clear any leftover from a prior crashed run. Part of #81. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(scripts,http): etc_clientblocker + /v1/clientblocker (#81) SCRIPTS.md: new etc_clientblocker section after etc_auditlog. Documents the cmds (+addblocker / +delblocker / +blocker), the data file under scripts/data/, the listener-chain placement (MUST sit AFTER hub_inf_manager), the operator-self-lockout footgun (operator levels exempt by default), the pattern-validation at edit time (length cap + pcall compile probe), the audit-event vocabulary (client.block.{add,remove,kick}), the HTTP triplet, the F-INF-1d nil-VE guard, and the cfg keys. HTTP_API.md: GET/POST/DELETE /v1/clientblocker rows in the "Bans + blacklist" catalog (the closest topical neighbour to a write-action on user identity), plus three new footnotes covering the request/response shape, error codes, and the cache-immediacy guarantee on DELETE. Part of #81. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(etc_clientblocker): two-pass review findings (#81) Addresses three issues found by the §1a.6 independent reviewer: BLOCKER B1 - HTTP DELETE was undeletable for patterns containing URL-special characters. The router captures `{pattern}` via `([^/]+)` with NO percent-decoding (verified at core/http_router.lua:282-298), so a pattern carrying `/`, `?`, `#` or `&` could be POSTed but never DELETEd (silent 404). The docs ALSO incorrectly claimed the router decodes path-vars. Fix: reject those four chars at POST/+addblocker time so the edit-time error is loud instead of silent at delete time. Real-world AP/VE strings are alphanumeric + `%+.-`, so this rules out zero legitimate patterns. Other Lua-pattern punctuation (`%`, `+`, `.`, `(`, `)`, `[`, `]`, `*`, `-`, `^`, `$`) is RFC-3986 URL-safe in path components and passes through unmodified. Docs corrected. CONCERN C1 - system-fired kick events used a novel `{ nick="<auto>", sid="<system>" }` actor shape that exists nowhere else in the codebase. Switched to the canonical string-shorthand actor (= the plugin name) which core/audit.lua's _snapshot_actor supports at line 106 (string -> `{nick=<string>, level=0, sid="", cid="", ip=""}`). Downstream filters now see a consistent `actor.nick = "etc_clientblocker"` for system-fired events vs operator names for the .add/.remove events. CONCERN C2 - reason-defaulting was duplicated across three call sites (do_add_pattern + ADC handler audit + HTTP handler audit). Extracted to a single `_effective_reason()` helper to prevent future drift. NIT N3 - check_clients iterated `pairs(patterns_tbl)` which has non-deterministic order. With two patterns matching the same VE, the kick reason was hash-order dependent. Switched to `util_spairs` so the alphabetically-first matching pattern always wins. format_list already used spairs - matches the convention. NIT N2 - long-pattern unit test now uses a realistic pattern body (mix of `%a` classes) over the 200-char cap rather than plain `aaaa...` so the cap branch is exercised independently of the compile-probe branch. Unit test grew from 61 to 70 checks. Smoke suite still green locally on Windows. Part of #81. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(etc_clientblocker): security review R1 - spairs orderedIndex leak (#81) The check_clients hot loop iterated patterns_tbl via `util_spairs` and returned PROCESSED mid-loop on the kick. `util.spairs` (core/util.lua:816-843) mutates the iterated table with an internal `orderedIndex` sorted-keys array on first call and only clears it on iterator exhaustion. Breaking out early leaves the artifact attached to `patterns_tbl`. Observable side effects pre-fix: - next persist() serialized `orderedIndex = {...}` into scripts/data/etc_clientblocker.tbl (filtered out on reload by the type guard, but the on-disk artifact is confusing) - next GET /v1/clientblocker emitted a spurious row with pattern="orderedIndex", breaking the documented response schema (response_schema says pattern is a string + reason is a string; orderedIndex's value is a Lua array of strings) - +blocker chat output showed `[orderedIndex] -> table: 0x...` No RCE / no auth bypass / no leak across users. Audit log shape and the kick semantics themselves are unaffected. The sandbox boundary still holds. Fix: snapshot the keys into a sorted local array BEFORE the iteration, then ipairs the array. Same deterministic ordering the original spairs call gave us (= cosmetically-consistent audit-attribution on a collision-with-multiple-matches) without the internal-state mutation. Regression test per §1a.7: the new unit test (10b) replaces the test stub's util.spairs with the REAL util.spairs impl from core/util.lua so the kick path exercises the same mutate-then- clear contract the production code sees. Verified the test FAILS on pre-fix code (`got="table: 0x..." want="nil"`) and PASSES on the post-fix snapshot+ipairs form. Unit count: 70 -> 71. Other plugins using `util.spairs` (cmd_ban, cmd_usercleaner, cmd_usersearch, etc_aliases, etc_trafficmanager) all iterate the spairs loop to completion - no early-return leak there. The footgun shape applies broadly though; documenting in a follow-up issue ([anti-pattern]: util.spairs + return inside loop body) would be worth doing if it ever resurfaces. Adversarial security review (general-purpose subagent) audited 12 additional surfaces (catastrophic Lua-pattern DoS, .tbl Lua code injection, HTTP path-traversal, pattern enumeration via timing, malformed-VE crash, pcall hiding bypass, forged audit via token comment, HUBOWNER self-lockout, onset-of-block timing, unbounded pattern count, UTF-8 in string.find, audit forgery via scriptname). All theoretical or non-issues under the existing Phase 7 / 7d / 7e / 8a guardrails; reasoning captured in the PR thread. Part of #81. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…#81) (#341) * feat(etc_clientblocker): Sopor patterns + opchat report + SBOT exempt (#81) Imports Sopor's etc_clientblocker.lua v0.4 contributions on top of the v0.10 promoted-to-core baseline. Functional changes (cfg-keyed, default-on for opt-out): - `etc_clientblocker_report*` family: on every block kick fire etc_report.send with a human-readable banner `[ CLIENT BLOCKER ]--> The user <nick> with IP <ip> is running <version> and is not allowed in this hub. Matching pattern: <p>` so staff see kicks live in op-chat (defaults: report=true, report_opchat=true, report_hubbot=false, llevel=60). The audit log already carries the same fields structured for forensics; this banner is for live operational awareness. Matches the sibling-plugin convention (etc_aliases / etc_msgmanager). - Level 55 (SBOT) added to the check_levels default with `false`. Bots have no BINF VE/AP to check against; the kick path should never reach them. Data changes: - scripts/data/etc_clientblocker.tbl (bundled default) now ships with 6 universally-malicious cheat/mod client patterns (CleanDC++, RSX++, CrZ++, SmVDC++, DC@fe++, FearDC). These are always-unwanted across DC hubs - operators almost never need them, so out-of-the-box blocking is the right default. Each entry is removable via `+delblocker <pattern>` for the rare operator who needs an exception. - examples/data/etc_clientblocker.tbl.example: 40-pattern curated list from Sopor's production hub. Blocks outdated stable releases of DC++ (0.0xx-0.8xx), AirDC++ (1.0-4.29 + Web 0.x- 2.14b + nano), EiskaltDC++ (<2.4.1), ApexDC++ (<1.6.4), ncdc (<1.18), Jucy (<0.86), plus legacy mods (StrgDC++, IceDC++, PDC++, PWDC++). Operators copy this over the bundled file if they want the broader policy. Sopor-isms normalised on import: - `%w` in his AirDC++ Web Client / nano patterns was used as if it were literal "w" / "n", but Lua's `%w` matches ANY word character. The patterns silently over-blocked any hypothetical "AirDC++Q 0.5" etc. Switched to literal `w`. The unit-test sweep `should_not_match` set covers the regression (AirDC++Q 0.5 must NOT match the Web Client pattern). - `%n` is fine as written - Lua 5.4 treats `%X` for non-class X as literal X (verified empirically). Left as-is for readability against the original source. Tests: - Unit: 71 -> 80 checks. New asserts cover report fire shape (opchat=true, hubbot=false, contains nick + pattern) and the SBOT-level-55-exempt path (no kill, no report, no audit fired). - Smoke: existing `test_clientblocker_81` extended with a 7th stage that connects with `VEFearDC/2.0` and asserts the hub kicks WITHOUT any operator configuration (= the bundled default works out of the box). Docs: docs/SCRIPTS.md gains the report behaviour, the bundled- default list, and the example-file reference. Part of #81 (the parent issue stays open until v3.2.0 ships). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(etc_clientblocker): two-pass review C1 + C2 + N2 (#81) Addresses the three findings from the §1a.6 review of the Sopor import: C1 (regression test for the %w -> w substitution): the original Sopor patterns used %w (Lua word-class, matches any letter/digit) where the intent was literal "w" for "AirDC++w" (Web Client). The substitution was made but never regression-tested. Per §1a.7 the test must FAIL on the unpatched pattern and PASS on the fix. Two two-direction assertions added at the end of the unit test: - ^AirDC%+%+w%s matches "AirDC++w 0.5" (positive) - ^AirDC%+%+w%s does NOT match "AirDC++Q 0.5" (negative) Verified the buggy %w version: positive passes, negative fails. The fixed literal-w: both pass. C2 (op-chat flood risk): documented in docs/SCRIPTS.md that the report fires synchronously on every kick and that flood-control relies on the upstream per-IP / per-CID connect rate-limit in core/ratelimit.lua. Operators with a noisy hub can flip etc_clientblocker_report=false and rely on the audit log alone. N2 (%n -> literal n in the example file): the example file had already replaced %n with literal n in the pattern source during the original Sopor import (the value was equivalent in Lua 5.4 but reads as if it were a character class). Updated the header rationale + an inline comment at the AirDC++ nano entry to make the normalisation explicit + match the %w fix narrative. Unit count: 80 -> 82 checks. Smoke still green. Part of #81. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(etc_clientblocker): catchall for unauthorised AirDC++ forks (#81) Aybo flagged that the legitimate-AirDC++ AP-suffix set is small and known (plain "AirDC++", "AirDC++w" for Web Client, "AirDC++n" for nano - that's it). Anything else - "AirDC++Q", "AirDC++3", "AirDC++pirate", random one-off forks operators in the wild keep spotting - is by definition modified. Adds one catchall pattern to examples/data/etc_clientblocker.tbl.example: ["^AirDC%+%+[^%snw]"] = "Modified / unauthorised AirDC++ fork is not allowed in this hub. Please download the official AirDC++ from https://airdcpp.net" Character class `[^%snw]` is the negative form: NOT whitespace AND NOT "n" AND NOT "w". Verified to leave plain AirDC++, AirDC++w, and AirDC++n alone while catching arbitrary fork suffixes incl. multi-char ("AirDC++Z-mod") and named ("AirDC++pirate") variants. 8 new regression-test assertions (3 legit must-not-match + 5 fork must-match cases). Unit count: 82 -> 90 checks. Bundled `scripts/data/etc_clientblocker.tbl` stays minimal-policy (6 well-known cheat clients). The fork catchall is operator- opt-in via the example file. Part of #81. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…#344) * feat(blockers): include target IP in op-chat reports across 4 plugins Surveyed all bundled blocker / kicker plugins and found four that fire opchat reports WITHOUT the target's IP address. Operators investigating a kick post-mortem had to cross-reference cmd.log / audit.log to recover it. Adds the IP consistently across all four: - etc_dhtblocker.lua: report_msg now carries the IP of the DHT-banned user. Format becomes "[ DHT BLOCKER ]--> User: %s | IP: %s | was banned for ..." - usr_nick_prefix.lua: previously fired no report at all (onConnect can't fire onFailedAuth without recursion). Adds a direct report.send (safe - no listener re-entry) with new cfg keys matching the etc_aliases convention: usr_nick_prefix_report / _report_hubbot / _report_opchat / _llevel (defaults: opchat=true, hubbot=false, llevel=60). Creates the missing scripts/lang/usr_nick_prefix.lang.{en,de} along the way. - etc_trafficmanager.lua: msg_op_report_block / msg_op_report_unblock gain an `IP: %s` field. Six call sites updated to derive `target_ip = (target and target:ip()) or msg_unknown` - offline-target blocks correctly fall back to the localised <unknown> string. - etc_msgmanager.lua: msg_report_block / msg_report_unblock gain `IP: %s`. is_online() extended to return target:ip() as a fourth value; four +msgmanager block/unblock branches threaded. `user:ip()` returns the TCP source IP - whatever family the client connected via (v4 or v6, never both - a single TCP connection has one source). For dual-stack users with HBRI (#214), the OTHER family is in the user INF but is not the connection source; the v4-or-v6 returned here is always the authoritative "where did this user actually connect from". All lang strings updated in both en and de. cfg_defaults + examples/cfg/cfg.tbl carry the new usr_nick_prefix_report* keys. Local lua54 + Windows MinGW smoke suite both green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(etc_dhtblocker): restore 'fuer' -> 'fuer' UTF-8 'fuer' (lang.de) Sorry, retyping the commit msg properly. Restore UTF-8 'fuer' regression in the de lang file. The previous commit downgraded 'fuer' to ASCII fallback 'fuer' on the report_msg line, while the rest of the same line (`aktiver`, `Funktion`) stayed UTF-8. Inconsistent + visibly worse in op- chat. §1a.6 review C1 - restore the umlaut. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
… fix) (#345) * Changed internal messages to English cmd_disconnect.lua * fix(cmd_disconnect): align english defaults with lang.en (#342) Two small fixes on top of Sopor's #342 to keep the source-code fallback strings exactly in sync with scripts/lang/cmd_disconnect.lang.en: - report_msg: "were disconnected" -> "was disconnected" (the subject %s is the singular user nick, lang.en already had the correct form). - msg_denied1..4 + msg_bot: trailing "!" -> "." (lang.en uses full stops; mixing punctuation between the source-default and the loaded lang file would look inconsistent if the lang file ever fails to load). The defaults only fire if cfg.loadlanguage fails (corrupt / missing lang file) so the visible effect on a healthy hub is zero - but per project i18n hygiene the source defaults should match the lang file character-for-character. Verified via a one-off script that diffs all 13 string-valued default fallbacks against the corresponding lang.en keys: 0 mismatches. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: I'm an OSK user, are you? <5789283+Sopor@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(cmd_disconnect): per-call TL override (closes #343) Sopor requested the ability to override the default TL30 on +disconnect, e.g. TL-1 for "permanent kick, don't auto-reconnect". ADC syntax extension (backward-compatible): +disconnect <NICK> [TL<SECONDS>] <REASON> Parser peeks at the second whitespace-token: if it matches `^TL(-?%d+)$` AND the integer is in -1..86400, that's the TL override and the reason starts at token 3. Otherwise the second token is treated as part of the reason and the cfg-default TL applies. The "TL" first-token-prefix is unambiguous against real-world disconnect reasons (operators don't write reasons starting with literal "TL<number>" against a hub bot). A TL<N> token with N out of bounds fails LOUD via msg_bad_tl ("Invalid TL value. Must be an integer between -1 and 86400.") - silent clamping would mask the operator's intent. HTTP API: `DELETE /v1/users/{sid}` body gains an optional `tl: integer optional (-1..86400)` field. Schema validation runs at the router layer (same min/max bounds), so a malformed body returns 400 before the handler fires. Response data now includes `tl` (the effective value used) alongside `reason`. cfg key: `cmd_disconnect_default_tl` (default 30, range -1..86400) lets the operator change the global default. ADC sites use the cfg default when no per-call TL token is present; HTTP body without a `tl` field falls through to the same path. Audit meta: every `user.kick` event from cmd_disconnect now carries `{ tl = <effective_tl> }` so the audit trail records which TL value the kick used. Plugin grew from 1.4 -> 1.5. Public surface gains `parse_tl_token(string) -> (n|nil|false, rest)` for unit tests. Tests: new `tests/unit/cmd_disconnect_tl_test.lua` (34 checks) exercises no-TL, valid TL (-1, 0, 30, 3600, 86400), out-of- bounds (-2, 86401, 999999), TL-prefix-but-not-numeric (TLabc, TL30abc, TL-alone), trailing-TL-in-reason, empty/ whitespace/nil/non-string inputs, and the "TL-only without reason" quirk. Wired into smoke.yml Linux + Windows blocks. Docs: docs/SCRIPTS.md cmd_disconnect entry documents the new syntax + cfg key + audit meta; docs/HTTP_API.md adds [^http-disconnect-1] footnote describing the body shape + response shape + audit emission. Lang en+de updated for help_usage / msg_usage / help_desc + the new msg_bad_tl error string. Local Windows MinGW smoke suite passes. Closes #343. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(cmd_disconnect): two-pass review NITs (#343) Two minor cleanups from the §1a.6 review of the original TL-override commit: NIT 1 - lang.de umlaut consistency. msg_bad_tl had "Ungueltiger" (ae-fold) while the rest of the de lang file uses proper umlauts (`höheres`, `Begründung`). Restored to "Ungültiger" so the file is consistent. NIT 2 - "+disconnect Bob TL30" (TL token but no reason) used to treat the literal string "TL30" as the kick reason. parse_tl_token now accepts both shapes: - "TL<N> <reason ...>" -> standard, reason populated - "TL<N>" -> reason becomes "", N still applies Operators should still type a reason in practice, but the previous quirk (where the TL prefix bled into the reason) was surprising. Out-of-bounds N still rejected without a reason. Unit test grew from 34 -> 39 checks: covers TL<N>-alone for both positive (TL30) and negative (TL-1) values, trailing whitespace, and the out-of-bounds-without-reason path. Smoke green locally. Part of #343. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Wraps the cfg.scripts entry for etc_clientblocker in the
`{ "name", enabled = true }` table form so the plugin becomes
listable via `GET /v1/plugins` and toggleable via
`PUT /v1/plugins/etc_clientblocker/enabled` per the #261
plugin-management endpoints.
Matches the existing convention used by etc_motd / etc_banner /
etc_chatlog / etc_records / etc_prometheus / hub_runtime /
bot_regchat / bot_session_chat / etc_userlogininfo / etc_dhtblocker
/ etc_regserver_announce in the same cfg.scripts array.
Default `enabled = true` preserves the current behaviour (plugin
loads on hub start). Operators who want to disable client-policy
filtering without removing the cfg entry can now flip it via
the API.
Listener-chain placement unchanged: still AFTER hub_inf_manager
per #81 invariant. Inline comment now also calls out the #261
toggleability.
Smoke green locally incl. the `HTTP API plugins (#261)` test
which exercises the toggle endpoint.
Part of #81.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…play + +delblocker by row index (#81) (#348) * fix(etc_clientblocker): testhub-sync defaults loss + AirDC++ list display (#81) Two issues reported during testhub validation of the v0.10 plugin. ISSUE A (BUNDLED DEFAULTS LOST ON SCRIPT-SYNC UPGRADE) When an existing testhub gets the new etc_clientblocker.lua via docker-compose script-sync, the sync copies scripts/*.lua but preserves scripts/data/ at its pre-upgrade state. On a hub that previously had the v0.0 empty .tbl stub, the v0.10 plugin loaded fine but found NO bundled defaults (CleanDC++, RSX++, CrZ++, SmVDC++, DC@fe++, FearDC) because they had been shipped IN the data file rather than in the plugin source. Fix: move BUNDLED_DEFAULTS into the plugin's Lua source. onStart now seeds them into the operator-managed .tbl when the loaded patterns_tbl is empty (covers both fresh install AND the script- sync scenario above). After the first seed the .tbl is non-empty on disk and subsequent +reload cycles use it as-is. Operator who genuinely wants zero patterns should disable the plugin via cfg.scripts; emptying the file just re-seeds on next reload. The bundled scripts/data/etc_clientblocker.tbl resets back to an empty stub with a comment explaining the seed mechanism. ISSUE B (`+blocker` LIST OUTPUT EMPTY IN AirDC++) The multi-line +blocker output was sent via 2-arg user:reply (BMSG / broadcast to main chat). DC++ rendered it correctly but AirDC++ (latest) showed no output. cmd_help and other multi- line operator-only output already use 3-arg user:reply (DMSG / hub-to-user private message), which is also more semantically correct - the pattern list is operator-only information, not something to broadcast to all users in main chat. Fix: switch on_listblocker to 3-arg user:reply. Single-line replies (+addblocker / +delblocker confirmations) stay on BMSG since those are short enough that all clients render them. TESTS Unit test grows from 90 -> 100 checks: - 21. Seed-on-empty: bundled count == 6, FearDC + CleanDC keys present, defaults persisted to disk. - 22. Seed-on-non-empty: operator .tbl with at least one entry leaves bundled defaults out (protects operator who has +delblocker'd a default from having it come back). - 23. +blocker reply: 3-arg DMSG verified via captured arity. Pre-existing tests adjusted: setup pre-populates _next_loaded with a `__test_sentinel__` entry so the seed-on-empty logic does not fire during setup; sentinel is cleared right after onStart so the existing test cases run on a clean empty map. Smoke green locally on Windows. Part of #81. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(etc_clientblocker): +delblocker by row index from +blocker output (#81) Sopor + Aybo testhub feedback: typing `+delblocker ^FearDC.+` to remove a bundled default is unintuitive because the `^` anchor is easy to miss / forget. UX fix: +blocker now numbers each row, and +delblocker accepts either the literal pattern OR the row number from the latest +blocker list. Mechanics: - format_list output now starts each line with `N. ` where N is the alphabetic-sort position (util_spairs already gives us a stable order across reloads). - +delblocker arg parser: if the operator-supplied token is a pure positive integer, resolve it as a 1-based index into the sorted pattern list. Out-of-range falls through to the existing literal-pattern path (which will not_found and tell the operator). - Literal-pattern delete still works for any string. Only pure-digit input is reinterpreted - a pattern that LITERALLY is a positive integer (extremely unlikely - real DC client AP/VE strings never look like "42") cannot be deleted via chat-cmd; documented edge case (edit the .tbl directly). Help / msg_usage / lang.en + lang.de now show `<pattern|N>` so the operator-facing syntax is self-documenting. Unit test grew from 100 -> 107 checks: 3-row sorted setup, delete by index 2 (middle), index 1 (post-shift), out-of-range (99) -> literal-fallback not_found, then literal-pattern delete works as before. Local smoke green incl. client blocker end-to-end. Part of #81. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…#349) Aybo testhub follow-up: after PR #348 shipped, the question came up "if I +delblocker every bundled default to keep the plugin active with zero patterns, do they come back on +reload?". The answer was yes - and that's not what an operator means by "delete all". Switch to Variante B: seed BUNDLED_DEFAULTS only when the .tbl file is MISSING on disk (util.loadtable returns nil), not when it exists with an empty patterns table. Concrete semantics: - First run / fresh install (file doesn't exist): -> seed 6 bundled defaults + persist - Operator empties the file deliberately (+delblocker all): -> no seed, stays empty across +reload cycles - Operator has at least one entry: -> use as-is (no merge with bundled) - Recovery from a deliberate empty: `rm scripts/data/etc_clientblocker.tbl` + +reload (triggers the file-missing branch) OR cherry-pick patterns from examples/data/etc_clientblocker.tbl.example. Side effect: scripts/data/etc_clientblocker.tbl is no longer committed to the repo as an empty stub. The data dir's other files (cmd_ban_bans.tbl etc) still exist; the clientblocker plugin auto-creates its file on first onStart via util.savetable. This also avoids the confusing "file looks empty in the repo but the docs say defaults are there" gap. Plugin bumped 0.10 -> 0.11 so the version string disambiguates which behavior the operator's hub is on (previous v0.10 had two distinct codepaths under the same version label across #341 and #348 - my mistake, not bumping then). Tests: - Test 21 inverted: now "seed-on-missing" instead of "seed-on-empty" (utility load returns nil, not {}) - New test 21b: "no seed when .tbl exists but is empty" - regression test for Aybo's empty-by-operator scenario - Test 22 unchanged (non-empty .tbl: use as-is) - Total 107 -> 110 checks Smoke (test_clientblocker_81 stage 7 "out-of-the-box FearDC block") now exercises the file-missing seed path: the smoke harness wipes the .tbl from the staging tree before hub start, onStart auto-creates it with defaults, FearDC connect gets kicked. Plugin header docstring + docs/SCRIPTS.md updated with the new recovery instructions. Part of #81. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced Jun 26, 2026
2 tasks
Aybook
added a commit
to luadch-ng/scripts
that referenced
this pull request
Jun 26, 2026
Plugin promoted into the luadch-ng/luadch core bundle via luadch-ng/luadch#81, shipped in the 9-PR arc culminating in luadch-ng/luadch#351. Operators get the blocker out of the box now, plus ADC `+blocker` CRUD, HTTP `/v1/clientblocker`, audit fires, opchat IP-in-report, and 40 bundled patterns - strictly better than the standalone v0.1 copy that lived here. Closes #27. - delete scripts/etc_clientblocker/ subdir (.lua + lang/) - IMPORT_NOTES.md: add Status line to the existing entry so the import history is preserved
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promote dev to master after testhub validation.
What's in this PR
Nine squash-merge commits from dev, three feature lines:
#81 etc_clientblocker arc (CLOSED)
feat(etc_clientblocker): bundled client AP/VE blocklist (#81) (#340)luadch-ng/scriptsinto core. ADC+blockeradd/del/list + HTTP/v1/clientblockerCRUD + audit fires + smoke + 110-check unit testhub_inf_manager; F-INF-1d nil-guard preservedfeat(etc_clientblocker): Sopor patterns + opchat report + SBOT exempt (#81) (#341)feat(cfg): make etc_clientblocker API-toggleable (#81 / #261) (#347)cfg.scriptsentry wrapped{ enabled = true }per HTTP API: GET /v1/plugins + PUT /v1/plugins/{name}/enabled (#82 Future Scope item 1) #261 toggle contractfix+feat(etc_clientblocker): testhub-sync defaults loss + AirDC++ display + +delblocker by row index (#81) (#348)BUNDLED_DEFAULTSmoved into plugin Lua source so script-sync upgrades don't lose them; multi-line+blockerreply switched to 3-arg DMSG (AirDC++ swallows multi-line BMSG); numbered list ++delblocker Nindex-delete UXfix(etc_clientblocker): seed defaults only when .tbl is missing (#81) (#349).tbl(operator deleted everything) is respected; defaults seed only on file-MISSING. Stubscripts/data/etc_clientblocker.tblremoved from repo so first-run path is the documented oneCross-plugin: IP in op-chat reports (#344)
feat(blockers): include target IP in op-chat reports across 4 plugins (#344)etc_dhtblocker,usr_nick_prefix,etc_trafficmanager(6 sites),etc_msgmanager(is_online()4th return) all reportnick + IPconsistentlycmd_disconnect (#342 / #343)
i18n(cmd_disconnect): English source defaults (Sopor's #342 + grammar fix) (#345)feat(cmd_disconnect): per-call TL override (closes #343) (#346)Revert "feat(cmd_disconnect): per-call TL override (closes #343) (#346)" (#350)Testhub validation
Maintainer pulled
ghcr.io/luadch-ng/luadch:devacross the arc:+blockeradd/list/del round-trip works in main chat (AirDC++ + DC++)+delblocker 3removes the 3rd row.tbldeletion -> +reload reseeds defaults; empty.tblstays empty across reloadsnick + IPon dht / nick-prefix / trafficmanager / msgmanager kicks+disconnect <nick> <reason>works post-revert (no TL parser path)Scope
Merge-commit promotion path per GitFlow A. All 9 dev commits go to master as a unit; no additional changes here.
Test plan
fbaf8ef):masterDocker image rebuild