summaryrefslogtreecommitdiff
Commit message (Expand)AuthorAgeFilesLines
* Default REGISTER-before-connect and email-required to ON•••These knobs were opt-in (env unset = off) for the irctest harness' benefit; the harness exercises both branches and is happy to set the env explicitly. But for the hosted papod deployment that papoweb is moving towards, we definitely want both: - REGISTER before connect: web clients can create an account in one CAP/NICK/USER round-trip instead of completing the IRC handshake twice (register, disconnect, log in). - Email required: enables password recovery and abuse signal; cost is one extra field at signup. Invert the env-var sense so production picks up the secure defaults with no env tweaks; tests that need the old behavior pin PAPOD_REG_BEFORE_CONNECT=0 / PAPOD_REG_EMAIL_REQUIRED=0. The irctest controller and papoweb's integration harness are updated separately to pin "0" explicitly so a future default flip doesn't silently change which branches they exercise. HEADmainEuAndreh26 hours1-2/+6
* doc/c10m.adocEuAndreh6 days1-0/+117
* Record topic setter + set-time so 333 reports the truth•••RPL_TOPICWHOTIME (333) was emitted on JOIN with the *joiner's* nick as the setter and System/currentTimeMillis as the set-time — neither of which has any relationship to who actually set the topic when. Add :papod.channel/topic-set-by (string) and :papod.channel/topic- set-at (long unix seconds) to the schema, populate them in the TOPIC handler, and read them in join-one!'s 333 emission (falling back to nick / now if absent, e.g. an empty channel snapshot imported without the new attrs). Surfaced by the chat.papo.im differential test (333 line diverged between fake-ircd's correctly-tracked setter and papod's broken output). EuAndreh8 days1-4/+18
* Fix JOIN race: pin NAMES/broadcast to atomic membership snapshot•••Two clients JOINing the same channel concurrently could each see the other in their own NAMES reply, and both could end up auto-opped on a freshly-created channel. The cause was that join-one! did: (swap! channels update handle (fnil conj #{}) nick) ... (get @channels handle) ;; for NAMES + broadcast + auto-op Between the swap! and the @channels read, a peer's swap! could land, so the read returned a set that already included them. Switch the swap to swap-vals!, capture members-before / members-after as a snapshot of state at the moment of our atomic update, and feed that snapshot to the broadcast loop, the metadata-2 push, the away- notify push, the auto-op decision, and the NAMES reply (via a new :members keyword on names-for). Auto-op now keys off (empty? members-before) instead of comparing the live atom to #{nick}. Surfaced and verified by the differential test in chat.papo.im, which previously had to insert quiesce checkpoints between concurrent JOINs to dodge the race; those checkpoints are no longer needed. EuAndreh9 days1-23/+34
* Bump welcome-banner counts after RPL_005 split•••The welcome burst grew to three RPL_005 lines a while back, but test_replies-for! / test_sasl / test_memoserv still asserted the old counts (7/7/8). Update them to 8/8/9 to match what the server actually emits. EuAndreh9 days1-3/+6
* Identify networks via PROXY v2 AUTHORITY; drop default network•••papod no longer pre-creates a "default network" at boot. Instead, each accepted connection is expected to begin with a PROXY protocol v2 header (RFC: haproxy.org proxy-protocol.txt) carrying the SNI in a PP2_TYPE_AUTHORITY (0x02) TLV — exactly what untls now injects. The authority is used to look up (or create) a network by name, and the resulting network-id is bound to the client at connect time rather than at registration time. Connections that arrive without a header and without the PAPOD_NETWORK_NAME fallback set are refused with "ERROR :Closing link: (No network)". PAPOD_NETWORK_NAME exists for environments where untls is not in the path (notably the integration test harness, where binder/wscat speak raw bytes); production deployments should leave it unset and let untls supply the SNI. Unit tests cover the parser: a valid header returns the AUTHORITY and leaves trailing bytes intact; non-PROXY input returns nil and preserves the stream; PROXY without an AUTHORITY TLV returns "". EuAndreh9 days2-18/+221
* Implement ADMIN, STATS, LINKS, TRACE, USERS, REHASH, CONNECT•••Previously these commands fell through to the catch-all 421 'Unknown command' branch. All of them are RFC1459 commands a modern client may probe; we now respond with the RFC numerics: - ADMIN: 256 + 257 + 258 + 259 administrative info burst. - STATS: 219 terminator always; the 'u' query returns 242 with a parsed uptime, the default returns a single 250 summary line. Other queries (m, l, o) are accepted with only the terminator (no records to report). - LINKS: 364 line for ourselves + 365 terminator (we don't link to other servers). - TRACE: 206 server entry + 262 terminator. - USERS: 392 header + 393 entries + 394 terminator, or 395 when nobody is logged in. - REHASH: 382 for opers, 481 otherwise. No actual config reload (papod reads only env vars at startup). - CONNECT: 481 for non-opers, 402 'no such server' otherwise (we do not link). Also added :started-at to the components map so STATS u can report uptime. EuAndreh9 days1-0/+149
* Implement VERSION, ISON, and CHGHOST••• - VERSION: emit 351 with version + server name, followed by the same three-line 005 ISUPPORT burst we send on welcome. - ISON: reply with 303, listing only those of the requested targets that have a live client entry. - CHGHOST (oper-only): introduce a per-client :host field (defaulting to 'localhost' when unset) and update both client-prefix and nuh-for-nick to use it. CHGHOST <target> <newuser> <newhost> mutates the target's :user :username and :host, replies 396 to the target itself, and broadcasts ':oldprefix CHGHOST newuser newhost' to peers in shared channels that negotiated the chghost cap. Non-opers get 481; missing params get 461; bad chars get 501. Was previously surfaced as gaps by the chat.papo.im integration suite; all three commands now respond with the RFC-mandated numerics. EuAndreh9 days1-4/+129
* Honor CAP REQ -cap and ignore unknown batch refs•••Two compliance gaps surfaced by the chat.papo.im integration suite: - CAP REQ :-echo-message previously matched no supported cap name (because of the leading '-'), so it was NAKed; even when a client sent a plain disable like above, papod silently kept the cap in the active set. We now split each REQ token by the '-' prefix, mark it as :add or :remove, and update the client's cap set accordingly. An REQ containing a mix of known and unknown caps is still NAKed atomically. - A message tagged with @batch=X referring to a batch that was never opened (e.g. because handle-batch-open! rejected the BATCH because of a missing target) used to fall through into normal command processing — so a malformed multiline batch would still deliver its inner PRIVMSG. Per IRCv3 we now silently drop those messages instead. EuAndreh9 days1-10/+24
* Honor userhost-in-names and account-tag caps end-to-end•••NAMES (RPL_NAMREPLY) now appends nick!user@host when the requesting client has negotiated userhost-in-names, both on JOIN and on the explicit NAMES command. account-tag is now applied even for unauthenticated senders: per the IRCv3 spec, the value is the literal '*' when no account is known. Previously we silently dropped the tag, which made the ACK meaningless for anonymous clients. EuAndreh9 days1-35/+38
* Implement draft/metadata-2 before-connect optional behavior•••Clients with draft/metadata-2 + batch can now METADATA * SET/GET/LIST/ CLEAR before completing registration. Pre-registration values are buffered on the client atom and replayed in a metadata BATCH at the end of the welcome burst, then merged into the global metadata atom under the registered nick. The value-suffix form ("draft/metadata-2=before-connect") is only sent in CAP LS 302 replies; CAP LS without a 302 version still emits the bare name to avoid IRCv3.1 clients seeing name=value capabilities. EuAndreh10 days1-4/+112
* Fix mute extban exemption, list query, ISUPPORT split, line limit•••- Mute extban exception (~m: in +e list) now matched against the speaker, not just account-based extban; new mute-except-matches? uses ~m:/~a:/plain mask types. - nuh-for-nick now uses the live client's actual ident instead of echoing the nick as user, so masks like *!*evan@* match correctly. - +b/+e/+I list queries (MODE #chan +b without args) no longer require channel operator status. - Ban/exception/invex lists now record setter+timestamp so 367/348/346 surfaces the correct setter nick. - Relax incoming line-length cap to 8190 bytes; AUTHENTICATE keeps its own 400-byte chunk limit, and TAGMSG keeps the 4094 tag-length cap. - Always reply 417 ERR_INPUTTOOLONG for over-long input (instead of FAIL VALUE_INVALID). - 905 ERR_SASLTOOLONG now uses the standard "<nick> :msg" format. - Split the welcome 005 burst across three lines so each stays within the 15-param numeric reply limit. EuAndreh10 days1-61/+82
* metadata-2: push subscribed values on JOIN•••When a subscriber joins a channel, the server now pushes any already-set metadata for the channel itself and for each existing member, gated on the subscription set. This is what irctest's testSubUserSetBeforeJoin and testSubChannelSetBeforeJoin want: a client that SUBs a key and then joins should receive the current value as if the SET had happened post-join, in a single METADATA push from the server. The push iterates (cons handle members) so the channel's own metadata is delivered first, then each member's. Self is excluded so the joiner doesn't echo their own keys back. Metadata propagation on later SETs is unchanged — notify-metadata-subs! still handles those. Brings the metadata_2 suite to 24 passed / 1 skipped (only metadata-before-connect remains, which we don't advertise). EuAndreh10 days1-0/+19
* Channel modes, channel-rename, extended-isupport, metadata-2•••Picks up the entire batch of optional irctest cases the previous gap analysis identified. ISUPPORT now advertises ACCOUNTEXTBAN=a, EXCEPTS=e, INVEX=I, EXTBAN=~,am; CHANMODES becomes beI,k,l,Cnt so the list-mask modes land in group A. The +b/+e/+I MODE handler is unified — same plumbing for mask add, mask remove, and list query (367/368, 348/349, 346/347 numerics). Each list lives in its own components atom (:chan-bans, :chan-excepts, :chan-invex). ban-matches? understands two extban prefixes carried by +b/+e masks: ~a:account matches the joiner's SASL account, while ~m:nick-mask is the mute extban that's skipped on JOIN and consulted by PRIVMSG via mute-matches?. JOIN: +i is bypassed by a matching +I; +b is bypassed by a matching +e; ~m: masks no longer block JOIN. PRIVMSG: +C drops non-ACTION CTCP to channels; ~m: bans drop non-op/non-voiced sends with 404, except for matching +e masks. draft/channel-rename: new RENAME command — chanop-only, refuses already-occupied targets, persists the new :papod.channel/name, and moves all in-memory channel state. Members with the cap receive RENAME; the rest get a synthesised PART+JOIN pair. draft/extended-isupport: advertised in CAP LS/REQ; no behaviour change yet (our 005 fits in two lines anyway). draft/metadata-2: METADATA command with GET / SET / LIST / SUB / UNSUB / SUBS / CLEAR. Each user/channel maps to a key→value table (:metadata atom) with per-nick subscriptions (:metadata-subs). GET/LIST responses are wrapped in a "metadata" BATCH; SET emits 761 or 766 directly. SET also fires notify-metadata-subs! so subscribers that share a channel with the target get a :nick METADATA push. WHOIS now emits 760 (RPL_WHOISKEYVALUE) for any key set on the target. Value-length and UTF-8 limits surface as FAIL METADATA VALUE_INVALID / INVALID_UTF8; the input-too-long handler also picks up the originating command name so those FAILs match the spec's expected shape instead of a bare 417. EuAndreh10 days1-70/+528
* Retry JOIN persistence on concurrent channel creation•••When two clients send JOIN for the same fresh channel name in parallel, both threads compute channel-eid=nil and add a new :papod.channel/... map to their transaction. The :papod.channel/name unique-value constraint then makes the second transact throw, which in turn propagated up through join-one! / handle-message! to client-loop's catch and dropped the losing client's connection. The persistence step is now wrapped in try/catch: on failure we re-read the channel from the latest db (it must have just been created by the other transaction) and retry the same write without the new-channel map. The in-memory updates that follow do not need to retry — :channels/:ops are coordinated through swap! atoms and already converge on the right state. Caught by the bot_mode tests (testBotChannelMessage, testBotWhox) where the third client's JOIN raced with the bot's; both now pass, and the in-tree kick / message_tags scenarios remain green. EuAndreh10 days1-28/+44
* IRCv3 bot-mode capability•••Advertise BOT=B in ISUPPORT and recognise the +B user mode. Three of the five irctest bot_mode cases now pass (testBotMode, testBotWhois, testBotPrivateMessage); the channel-join cases (testBotChannelMessage, testBotWhox) hit a pre-existing concurrent-JOIN race in papod that is unrelated to bot-mode and needs a separate fix. Implementation: - ISUPPORT 005 now advertises BOT=B alongside the other tokens. - handle-mode tracks a :bot? flag via +B/-B and surfaces it in the 221 RPL_UMODEIS summary alongside i/o/w/T. - WHOIS emits 335 RPL_WHOISBOT for any bot-flagged target. - WHO/WHOX flag column includes the BOT mode char (B) for bots. - PRIVMSGs originating from a bot now carry the IRCv3 'bot' message tag — added presence-style to out-tags so it propagates to every recipient with message-tags, both for channels and DMs. EuAndreh10 days1-1/+10
* WALLOPS, HELP and HELPOP commands•••Three Modern/RFC commands that irctest exercises and were previously returning 421 unknown: - WALLOPS: oper-only broadcast. Sender always echoes; recipients are any user with the new +w user mode. Non-opers get 481. - +w user mode: toggleable with MODE <nick> +w/-w; surfaced in 221 along with i/o/T. - HELP / HELPOP: returns a minimal RPL_HELPSTART (704) / RPL_HELPTXT (705) / RPL_ENDOFHELP (706) trio. The subject is echoed back so the test's case-insensitive subject matcher passes for HELP PRIVMSG, HELP unknown, etc. Tests covered: 2 in wallops.py + 6 in help.py (both HELP and HELPOP parametrized variants). EuAndreh10 days1-0/+42
* Wire account-registration before-connect and email-required configs•••Two env vars gate the optional draft/account-registration features: PAPOD_REG_BEFORE_CONNECT and PAPOD_REG_EMAIL_REQUIRED. When set, they extend the cap value with `before-connect` and/or `email-required` respectively, mirroring how the irctest controller config maps to the cap negotiation surface. Behavior changes in handle-register: - before-connect on: REGISTER is allowed pre-NICK/USER/CAP END. - email-required on: a `*` or blank email returns FAIL INVALID_EMAIL. - The handler now records the attempted nick in handle-nick (even on 433) so that REGISTER * can resolve the user's intended account name when their NICK was rejected. Combined with the new ACCOUNT_EXISTS check against the live :clients map, this prevents registration land-grabs of a nick already held by another connection. All 8 irctest account_registration cases now pass. EuAndreh12 days1-32/+77
* draft/relaymsg and draft/account-registration•••Both capabilities are advertised in CAP LS / accepted in CAP REQ. draft/relaymsg adds a RELAYMSG command for bridge bots: - RELAYMSG <channel> <fakenick> <message> - Validates fakenick (must contain '/', must not contain '!') with FAIL RELAYMSG INVALID_NICK - Authorization: chanop or sender holds draft/relaymsg cap; otherwise FAIL RELAYMSG PRIVS_NEEDED - Persists as user-message so chathistory replays the relayed line - Recipients with draft/relaymsg cap receive a draft/relaymsg=<sender> tag identifying the actual sender - Echoes to the sender via the existing labeled-response/echo-message pipeline so labels propagate naturally draft/account-registration adds a REGISTER command: - REGISTER <account|*> <email|*> <password> - account = '*' uses the current nick; any other value must match the current nick or fails with ACCOUNT_NAME_MUST_BE_NICK - Pre-connection-complete attempts fail with COMPLETE_CONNECTION_REQUIRED - Existing accounts fail with ACCOUNT_EXISTS - Successful registration confirms the cracha account and reports REGISTER SUCCESS The four basic irctest cases (RegisterTestCase x3 + RegisterBeforeConnectDisallowedTestCase) pass; before-connect and email-required configurations are not yet wired through the controller. EuAndreh12 days1-0/+155
* extended-monitor capability•••Watchers that negotiate extended-monitor (alongside the appropriate gating cap) now receive AWAY, SETNAME, and ACCOUNT events for monitored nicks even when they don't share a channel. The channel- member broadcast and the extended-monitor fan-out share a `seen` set so a watcher never gets a line twice. Implementation: notify-extended-monitors\! looks up the nick's watchers in :nick-monitors, filters by both extended-monitor and the gating cap (away-notify / setname / account-notify), and skips anyone already delivered to. handle-away, handle-setname, and broadcast-account- notify\! each call it after the existing channel-member loop. EuAndreh12 days1-43/+76
* MONITOR command and online-state notifications•••Adds the IRCv3 MONITOR command (+/-/C/L/S subcommands), advertised via the new ISUPPORT MONITOR=100 token. Per-client subscriptions live in :monitor-targets (set of lowercased nicks); a global :nick-monitors atom keyed by lowercased nick maps to the set of watching client atoms for fast reverse lookup. RPL_MONONLINE (730) is emitted to subscribed watchers when: - a client completes registration (welcome path), - a registered client changes nick to a different lowercased value. RPL_MONOFFLINE (731) is emitted when: - the canonical session for a nick disconnects (only if no sibling multi-session entry remains), - a registered client changes nick to a different lowercased value (the old nick). Case-only nick changes do not emit either, matching the spec. MONITOR + filters out invalid nicks (which would include hostmasks) and reports them as offline rather than rejecting the whole batch, so testMonitorForbidsMasks's RPL_MONOFFLINE branch is satisfied. Disconnect cleanup also drops the leaving session from every :nick-monitors entry it was watching, so we don't deliver to closed writers. EuAndreh12 days1-2/+151
* chghost, setname, invite-notify capabilities•••chghost: advertise the cap. We never rewrite hostnames currently (everything is "localhost"), so this is a passive declaration that clients negotiating the cap will see CHGHOST messages if/when host rewriting lands. setname: SETNAME :<realname> updates the client's stored realname and, for every shared-channel member that negotiated the cap, emits the ":nick\!user@host SETNAME :<realname>" notification. Length cap matches the existing TOPICLEN bound (390); empty/over-length names get FAIL SETNAME INVALID_REALNAME. invite-notify: INVITE delivery now also sends the INVITE line to every channel op (other than inviter/invitee) that negotiated the cap, so mod tooling can track invites without polling. Uses the same account-tag-aware delivery as the recipient path. EuAndreh12 days1-2/+62
* account-notify and account-tag•••account-notify: when a SASL auth completes mid-session (re-auth path), broadcast :nick ACCOUNT <account> to every other member of every shared channel that negotiated the cap. No-op on initial connection since the user has not joined any channel yet. account-tag: messages from authenticated senders carry an account=<name> tag for recipients with account-tag negotiated. The message-build path now precomputes "tagged + account" and "time-only + account" variants; clients with only account-tag (no message-tags or server-time) get a bare @account=<name> with no msgid/time. INVITE delivery follows the same rule. Both caps are now in CAP LS / REQ. EuAndreh12 days1-15/+68
* Advertise utf8only and draft/typing capabilities•••We already enforce UTF8ONLY (advertised in 005) and forward +typing client-only tags through TAGMSG, but the corresponding capabilities were never offered in CAP LS. Adding them lets clients negotiate the caps explicitly instead of inferring behavior. - utf8only: complementary to the ISUPPORT UTF8ONLY token - draft/typing: marks the server as forwarding the +typing tag EuAndreh12 days1-6/+9
* Multi-session SASL: shared-nick collisions and MARKREAD broadcast•••When two clients are SASL-authenticated to the same account, they may now share a nickname instead of getting a 433 ERR_NICKNAMEINUSE on the second NICK. The first session keeps the canonical :clients[nick] entry (so nick-keyed PRIVMSG delivery is unchanged); siblings register only in :account-clients[account]. Disconnect cleanup removes the per-account slot and only drops the :clients[nick] entry when the disconnecting session was the canonical one. handle-markread now fans out the MARKREAD line to every other live session of the same account that negotiated draft/read-marker, fixing draft/read-marker §"propagated to other sessions" (previously hung). Side effect: ZNC playback's reattach-to-same-nick flow no longer hangs either; the test now fails fast on the missing *playback bot rather than blocking on the second registration. EuAndreh12 days1-12/+68
* Auto-rejoin persistent channels after SASL reconnect; UTF-8 FAIL prefix•••Authenticated clients reconnecting via SASL are now auto-rejoined to every channel where they have a stored :papod.membership row, with the JOIN+TOPIC+MARKREAD+NAMES burst delivered as part of the welcome reply sequence. join-one\! handles the bookkeeping (skips duplicate membership rows, fires user-join events, updates in-memory :channels), and clients that negotiated draft/read-marker get their stored MARKREAD replayed. The INVALID_UTF8 reject now includes the server prefix (":<server> FAIL * INVALID_UTF8 ..."), so clients that look for ' FAIL ' to detect the rejection actually see it. Previously they'd time out waiting for either a 001 or an ERROR/FAIL with leading space. EuAndreh12 days1-8/+35
* SASL re-auth, WHOX %a, RFC1459 INVITE, +I bypass, tag forwarding fixes•••- WHOX %a now returns the user's :account (was hardcoded "0"). - INVITE accepts the RFC1459 "INVITE <channel> <nick>" param order in addition to Modern's "INVITE <nick> <channel>", and the relayed line preserves the input order. When the channel does not exist the inviter also receives an INVITE echo (RFC1459 deprecated, but tested). - AUTHENTICATE PLAIN is now allowed after registration and after a prior successful auth, matching SASL 3.2's "any time" rule. Removed the trailing "NOTICE Session ID:" line so it doesn't leak in front of the next AUTHENTICATE +. - PRIVMSG delivery splits client capabilities: clients with message-tags see the full tagged line (msgid + time + client-only +tags); clients with only server-time see just msgid + time, no client-only tags; clients with neither see the raw line. Same split applies to the echo-message reply. - deliver-to-client\! truncates only the body (post-tag) to 510 bytes so IRCv3 message-tag payloads up to 4094 bytes survive the wire. - +b ban check on JOIN is bypassed when an INVITE is pending for the user, mirroring Ergo's +I-style invite-exception. The invite is still consumed on successful JOIN. Unit test for the post-success AUTHENTICATE PLAIN path is updated to expect "AUTHENTICATE +" rather than 907. EuAndreh12 days2-46/+88
* Implement CHATHISTORY TARGETS, DM redaction, account-isolated DMs•••Adds the missing CHATHISTORY TARGETS subcommand returning channels and DM peers the user has activity in within an exclusive timestamp window, sorted by latest message time. REDACT now accepts DM targets: a sender can redact their own DM, and both sides receive the REDACT line if they have draft/message-redaction. DM events now record source-account and target-account at storage time. Chathistory queries filter to events whose stored account matches the requesting client's current SASL account, so a freshly-registered user who reuses a previously-anonymous nick does not see the prior identity's DMs (regression test for issue #833). DM history fetch is also case-insensitive on nick comparisons, and format-history-event renders DMs with the original target nick rather than the conversation peer, fixing replays where a self→peer reply was shown as peer→peer. The join-one\! refactor (returns echo + topic + markread + names lines in the reply vector) is retained from prior work; unit tests assert the returned replies instead of inspecting the writer bytestream so JOIN echoes can be wrapped in a labeled-response BATCH. EuAndreh13 days2-108/+289
* Implement TAGMSG persistence and BATCH multiline support•••TAGMSG persistence: - Persist user-tagmsg events when +draft/persist tag is set - Render TAGMSG events in chathistory with their original tags - Filter only shown to clients with draft/event-playback cap BATCH multiline (draft/multiline): - Parse BATCH +id type target / BATCH -id from clients - Accumulate @batch=id PRIVMSGs without immediate echo - On batch close, persist as one multiline user-message event, echo to sender as BATCH+/PRIVMSGs/BATCH- with original label - Relay to multiline-capable clients as BATCH structure - Relay to non-multiline clients as separate PRIVMSGs (first line gets msgid+time, rest get just time) - Forward client-only tags (+ prefix) and concat tag through the BATCH to recipients - Reject batch tag mismatches with FAIL MULTILINE_INVALID - Reject concat tag on blank message with FAIL MULTILINE_INVALID - Enforce max-bytes=4096 / max-lines=32 with FAIL responses - Preserve trailing whitespace in trailing param (split limit -1) EuAndreh13 days1-11/+345
* Support STATUSMSG (@#chan and +#chan) PRIVMSG targets•••Recognize @ and + prefixes on channel targets to filter delivery to ops or ops/voiced respectively. The prefixed target is preserved in the relayed PRIVMSG so clients can render it appropriately. EuAndreh13 days1-3/+35
* Implement +b channel ban mode with glob mask matching•••- Track per-channel ban masks in :chan-bans atom - MODE +b <mask> adds, -b removes (case-insensitive) - MODE +b without mask returns RPL_BANLIST + RPL_ENDOFBANLIST - JOIN checks ban list and returns ERR_BANNEDFROMCHAN (474) - Update CHANMODES isupport to b,k,l,nt EuAndreh13 days1-3/+73
* Reject non-UTF-8 messages with FAIL INVALID_UTF8•••Match UTF8ONLY isupport semantics by detecting U+FFFD replacement characters (introduced by InputStreamReader for invalid bytes) and returning FAIL with optional label tag for labeled-response. EuAndreh13 days1-0/+23
* SASL: support multi-chunk AUTHENTICATE per IRCv3 spec•••- Accumulate AUTHENTICATE chunks until '+' terminator or chunk < 400 chars - Reject single chunks > 400 with ERR_SASLTOOLONG (905) per spec - Cap accumulated buffer at 8KB to prevent abuse EuAndreh13 days1-8/+28
* WHOIS: send 330 RPL_WHOISACCOUNT, hide channels for invisible users•••- Track :account on SASL authentication so WHOIS can return RPL_WHOISACCOUNT - Filter channels in 319 RPL_WHOISCHANNELS by invisibility: hide channels of +i users from non-members EuAndreh13 days1-3/+15
* Implement CHATHISTORY subcommands, TOPIC events, REDACT cap filter•••Major chathistory work: - Add CHATHISTORY BEFORE/AFTER/BETWEEN/AROUND with msgid/timestamp anchors - Filter redacted messages from history per spec - Persist TOPIC events for event-playback support - Use historical timestamps in CHATHISTORY responses - Support DM history queries (msg-to-self and between users) - Add draft/event-playback capability REDACT improvements: - Only deliver REDACT to clients with draft/message-redaction cap - Add msgid/time tags to generic TAGMSG handler - FAIL responses include subcommand context per spec Tag-line refactoring: - Add tag-line-at helper for stored event timestamps - Share msg-at between event store and broadcast in PRIVMSG/TOPIC - Use ISO time with explicit Date instead of double iso-time call EuAndreh13 days2-74/+327
* MARKREAD on JOIN and DM targets•••When a client with draft/read-marker JOINs a channel, send a MARKREAD line with the stored timestamp (or '*' if none) before the closing RPL_ENDOFNAMES. Also support MARKREAD against direct-message targets (user nicknames, not just channels): per-(owner,target) markers are stored in a new in-memory atom :user-markers. EuAndreh13 days1-32/+61
* MARKREAD: monotonic timestamps and FAIL on missing params•••- SET now compares the incoming timestamp against the stored value and refuses to roll back: a smaller timestamp echoes the existing newer value, matching the read-marker monotonic rule. - Empty MARKREAD now responds with FAIL MARKREAD NEED_MORE_PARAMS instead of 461 to align with the cap's bespoke error format. EuAndreh13 days1-9/+24
* MARKREAD: timestamp= prefix on output, return * for unknown•••- GET on a target with no stored marker now returns '*' regardless of whether the channel exists, matching the read-marker spec. - SET only persists the bare ISO-8601 timestamp portion (avoiding the 'timestamp=timestamp=...' echo on subsequent GETs). - Reject malformed SET payloads with FAIL MARKREAD INVALID_PARAMS. EuAndreh13 days1-13/+26
* Normalize channel handles to lowercase in JOIN•••Joining #Foo and #foo now refer to the same channel, matching IRC casemapping (we advertise CASEMAPPING=ascii). The normalized name is what gets stored in the in-memory state and emitted in the JOIN broadcast. EuAndreh13 days1-2/+4
* Hide +s/+p channels from LIST for non-members•••LIST now skips channels with the +s (secret) or +p (private) flag when the requesting client is not a member, matching Modern's secret-channel rule. EuAndreh13 days1-0/+20
* Honor +n (no external messages) and +m (moderated) on PRIVMSG•••Sending PRIVMSG to a +n channel from outside the channel now returns 404 ERR_CANNOTSENDTOCHAN. Sending to a +m channel without op/voice returns the same numeric. EuAndreh13 days1-1/+33
* MODE channel: validate target/channel/key/limit, strip leading colon•••- Channel must exist (403) before checking op status (482). - MODE +o now validates that the target nick exists (401) and is on the channel (441) before changing ops. - MODE +k now rejects empty / whitespace keys with 696 ERR_INVALIDMODEPARAM and a placeholder '*' so clients see a 5-param numeric. - MODE +l now rejects non-positive / non-numeric values with 696 (same 5-param shape) and accepts only positive integers. - The trailing-form mode argument (e.g. ':0') has its leading colon stripped before parsing/validation, so MODE #chan +l :0 no longer emits a malformed reply. EuAndreh13 days1-56/+121
* Implement KILL command (oper-only)•••Non-opers receive 481 ERR_NOPRIVILEGES. Opers can KILL by nick: the target gets a KILL line, an ERROR line, and its socket is closed so the read loop unblocks immediately. EuAndreh13 days1-0/+35
* Filter non-message events from CHATHISTORY without event-playback•••When the client has the draft/event-playback capability we still emit JOIN/KICK/etc. in the historical batch; otherwise we now restrict the batch to user-message / user-edit / user-delete events, matching the chathistory spec. EuAndreh13 days1-1/+12
* REDACT: echo to sender, full nickmask prefix, FAIL format•••- Send REDACT back to sender (with echo-message-style behavior) - Use full nickmask prefix (foo!user@host) on outgoing REDACT - Strip incoming reason's leading colon to avoid '::reason' - Emit reason as a trailing colon-prefixed param - Use is-op? from in-memory ops alongside Datomic has-access? for the auth check, since auto-op only updates the in-memory state on JOIN - Fix FAIL INVALID_TARGET payload: drop the spurious second 'REDACT' token so params are [REDACT INVALID_TARGET <chan> <reason>] EuAndreh13 days1-9/+17
* Improve INVITE: validate target, channel membership, op status•••INVITE now returns: - 401 ERR_NOSUCHNICK if target user doesn't exist - 442 ERR_NOTONCHANNEL if inviter is not in the channel - 482 ERR_CHANOPRIVSNEEDED if channel is +i and inviter is not op The INVITE message delivered to the invitee now uses the full nickmask prefix (foo!user@host) rather than just the nick, so clients can match on prefix=StrRe('foo!.*'). EuAndreh13 days1-3/+24
* Track unregistered connections and honor MODE -o•••Add :n-unreg counter on the components map: incremented when a client connects, decremented when it registers or disconnects. LUSERS now reports the real unregistered count from this counter (previously always 0 because unregistered clients are not in :clients). Also handle the 'o' character in user-mode setting so MODE <self> -o actually clears :oper?, fixing the LuserOpers oper-count test. EuAndreh13 days3-13/+20
* Forward client tags through PRIVMSG to recipients•••Client-supplied tags starting with '+' (other than the special +reply tag) are now propagated to recipients in the relayed PRIVMSG and to the sender in the echoed PRIVMSG, matching the IRCv3 client-only message tags spec. EuAndreh13 days1-2/+12
* Reject overlong input with ERR_INPUTTOOLONG•••process-message! now rejects lines whose payload exceeds 510 bytes or whose tags exceed 4094 bytes with 417 ERR_INPUTTOOLONG instead of silently truncating, matching IRCv3 message-tags expectations. EuAndreh13 days1-3/+30
* Improve labeled-response, TAGMSG, and parser robustness•••- Move label tag insertion into existing tag prefix instead of emitting two separate @-prefixed sections. - Echo of PRIVMSG to sender now omits msgid/time tags unless the sender has message-tags or server-time capability. - Generic TAGMSG: forward to direct recipients (with message-tags cap), echo back to sender (with echo-message + message-tags), filter the label tag from the recipient copy. - Empty replies for labeled commands now produce a labeled ACK (without server prefix), matching IRCv3 labeled-response. - Accept silent PONG and parse loose command tokens (allow any non-space char) so labeled NONEXISTENT_COMMAND yields a labeled 421 instead of crashing the connection. EuAndreh13 days2-22/+92