| Commit message (Expand) | Author | Age | Files | Lines |
| * | 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.
HEADmain | EuAndreh | 26 hours | 1 | -2/+6 |
| * | doc/c10m.adoc | EuAndreh | 6 days | 1 | -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).
| EuAndreh | 8 days | 1 | -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.
| EuAndreh | 9 days | 1 | -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.
| EuAndreh | 9 days | 1 | -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 "".
| EuAndreh | 9 days | 2 | -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.
| EuAndreh | 9 days | 1 | -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.
| EuAndreh | 9 days | 1 | -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.
| EuAndreh | 9 days | 1 | -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.
| EuAndreh | 9 days | 1 | -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.
| EuAndreh | 10 days | 1 | -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.
| EuAndreh | 10 days | 1 | -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).
| EuAndreh | 10 days | 1 | -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.
| EuAndreh | 10 days | 1 | -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.
| EuAndreh | 10 days | 1 | -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.
| EuAndreh | 10 days | 1 | -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).
| EuAndreh | 10 days | 1 | -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.
| EuAndreh | 12 days | 1 | -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.
| EuAndreh | 12 days | 1 | -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.
| EuAndreh | 12 days | 1 | -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.
| EuAndreh | 12 days | 1 | -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.
| EuAndreh | 12 days | 1 | -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.
| EuAndreh | 12 days | 1 | -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
| EuAndreh | 12 days | 1 | -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.
| EuAndreh | 12 days | 1 | -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.
| EuAndreh | 12 days | 1 | -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.
| EuAndreh | 12 days | 2 | -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.
| EuAndreh | 13 days | 2 | -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)
| EuAndreh | 13 days | 1 | -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.
| EuAndreh | 13 days | 1 | -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
| EuAndreh | 13 days | 1 | -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.
| EuAndreh | 13 days | 1 | -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
| EuAndreh | 13 days | 1 | -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
| EuAndreh | 13 days | 1 | -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
| EuAndreh | 13 days | 2 | -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.
| EuAndreh | 13 days | 1 | -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.
| EuAndreh | 13 days | 1 | -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.
| EuAndreh | 13 days | 1 | -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.
| EuAndreh | 13 days | 1 | -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.
| EuAndreh | 13 days | 1 | -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.
| EuAndreh | 13 days | 1 | -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.
| EuAndreh | 13 days | 1 | -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.
| EuAndreh | 13 days | 1 | -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.
| EuAndreh | 13 days | 1 | -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>]
| EuAndreh | 13 days | 1 | -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!.*').
| EuAndreh | 13 days | 1 | -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.
| EuAndreh | 13 days | 3 | -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.
| EuAndreh | 13 days | 1 | -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.
| EuAndreh | 13 days | 1 | -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.
| EuAndreh | 13 days | 2 | -22/+92 |