Skip to content

Conversation

@MrLenin
Copy link
Contributor

@MrLenin MrLenin commented Jan 1, 2026

Note: This PR depends on #82 (security-hardening) being merged first.

Summary

Comprehensive IRCv3.2+ capability implementations including ratified specs and draft extensions. This is a major feature branch with 86 commits across 88 files (~26,700 lines added).

IRCv3 Capabilities Implemented

Ratified Specifications

Capability Description
cap-302 CAP LS 302, CAP NEW/DEL, SASL re-authentication
server-time Message timestamps via time tag
echo-message Echo sent messages back to client
account-tag Account info in message tags
chghost Host change notifications to channel members
invite-notify Invite notifications to channel members
labeled-response Request/response correlation via label tag
batch Message batching for netjoin/netsplit, chathistory
message-tags Client-only tags, TAGMSG command, msgid generation
standard-replies FAIL/WARN/NOTE response format
setname Real name changes (SETNAME command, P10 SE token)

Draft Extensions

Capability Description
draft/chathistory LMDB-backed message history with BEFORE/AFTER/AROUND/LATEST/BETWEEN
draft/message-redaction Message deletion with chathistory integration
draft/account-registration Pre-connection account registration via REGISTER command
draft/multiline Multi-line message support with configurable batch timeout
draft/read-marker Read position tracking (MARKREAD command)
draft/channel-rename Channel rename support
draft/event-playback Event replay for chathistory (JOIN/PART/QUIT/etc)
draft/metadata-2 User/channel metadata with LMDB persistence and visibility controls
draft/pre-away AWAY command before registration completes
draft/extended-isupport Extended ISUPPORT tokens
draft/no-implicit-names Suppress automatic NAMES reply on JOIN
draft/webpush Web push notifications for mobile/web clients

P10 Protocol Extensions

Token Description
SE SETNAME - propagate real name changes
BT BATCH - batch markers for netjoin/netsplit
ML MULTILINE - multi-line message propagation
MDQ METADATAQUERY - on-demand metadata sync between servers

Storage & Performance

LMDB Persistence

  • Chathistory: Persistent message storage with configurable retention
  • Metadata: Key-value storage for user/channel metadata
  • Zstd Compression: Transparent compression for stored data
  • TTL-based Expiry: Automatic cleanup of expired entries
  • ISO 8601 timestamps for clients, Unix internally

S2S Federation

  • Cross-server chathistory queries with numeric-based routing
  • Metadata synchronization via MDQ protocol
  • Multi-hop routing support for disconnected servers

X3 Integration

  • MARKREAD routing through X3 for authoritative storage
  • Metadata cache-through for X3-managed data
  • Account metadata loading on login
  • (Pre-)Away presence aggregation

Additional Features

CMocka Unit Testing Framework

  • make test target for automated testing
  • Test suites for: ircd_string, ircd_match, ircd_chattr, numnicks, ircd_in_addr, ircd_compress, ircd_cloaking, dbuf, ircd_crypt, crule, history

WebSocket Support

  • Native WebSocket transport (draft spec)
  • Binary and text frame support

Private Message History

  • Per-user opt-in consent system (FEAT_CHATHISTORY_PM_REQUIRE_CONSENT)
  • Normalized PM target handling for consistent storage

Operational Features

  • /STATS chathistory - Storage statistics
  • /STATS metadata - Metadata queue statistics
  • FEAT_AWAY_THROTTLE - Rate limiting for AWAY changes
  • FEAT_REGISTER_SERVER - Configurable registration target

Configuration

New features in ircd.conf:

FEAT_CHATHISTORY = TRUE
FEAT_CHATHISTORY_MAXLIMIT = 1000
FEAT_CHATHISTORY_PM = TRUE
FEAT_CHATHISTORY_PM_REQUIRE_CONSENT = TRUE
FEAT_METADATA = TRUE
FEAT_METADATA_MAXSUBS = 100
FEAT_AWAY_THROTTLE = 60

Build Requirements

  • LMDB library (liblmdb-dev)
  • Zstd library (libzstd-dev)
  • CMocka for tests (libcmocka-dev)

Test Plan

  • CAP 302 negotiation and capability advertisement
  • Message tag propagation (client-only and S2S)
  • Chathistory storage, retrieval, and federation
  • Metadata storage, sync, and visibility
  • Multiline batching with timeout
  • Standard-replies backwards compatibility
  • CMocka unit tests pass (legacy + new)
  • LMDB persistence across restarts
  • Full integration testing on live network
  • WebSocket client connectivity

Breaking Changes

None - all features are additive and backward compatible.

🤖 Generated with Claude Code

MrLenin and others added 30 commits January 1, 2026 03:32
Phase 1 - CAP 302 Foundation:
- Add cli_capab_version to track CAP negotiation version (0, 301, 302)
- Parse version parameter in CAP LS (e.g., "CAP LS 302")
- Support capability values for CAP 302+ clients
- Add multi-line CAP LS with '*' continuation for long capability lists

Phase 2 - SASL 3.2 Enhancements:
- Add cap-notify capability (CAP_CAPNOTIFY) with FEAT_CAP_cap_notify
- Advertise SASL mechanisms: sasl=PLAIN,EXTERNAL,OAUTHBEARER
- Allow post-registration AUTHENTICATE for OAuth token refresh:
  - Remove IsSASLComplete blocker, add ClearSASLComplete macro
  - Reset SASL state (agent, cookie, timer) for new auth attempt
- Send AC (ACCOUNT) after successful reauth for registered users:
  - Notify channel members with account-notify capability
  - Propagate to other servers using correct format based on
    FEAT_EXTENDED_ACCOUNTS setting (R/M subtype vs plain format)

This enables OAUTHBEARER token refresh without new P10 protocol commands -
reuses existing SASL 'S' (Start) subcmd for backwards compatibility.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement the server-time capability (IRCv3.2) that adds @time= tags
to messages for clients that request it. Timestamps are ISO 8601 format
with millisecond precision.

Changes:
- Add CAP_SERVERTIME capability to capab.h
- Add FEAT_CAP_server_time feature flag (default: TRUE)
- Add format_server_time() helper for ISO 8601 timestamps
- Update sendcmdto_channel_* functions to build dual message buffers
  (with and without @time tag) based on client capability
- Update sendcmdto_common_channels_* functions similarly

Functions updated:
- sendcmdto_channel_butserv_butone() - Channel messages
- sendcmdto_channel_capab_butserv_butone() - Capability-filtered channel
- sendcmdto_common_channels_butone() - Common channel notifications
- sendcmdto_common_channels_capab_butone() - Filtered common channels
- sendcmdto_channel_butone() - PRIVMSG/NOTICE to channels

Example output for server-time clients:
@time=2025-12-23T12:30:00.123Z :nick!user@host PRIVMSG #chan :message

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements the IRCv3 echo-message capability which echoes PRIVMSG and
NOTICE messages back to the sender. This is useful for clients to
confirm message delivery and maintain consistent message display.

Changes:
- Add CAP_ECHOMSG to capab.h
- Add FEAT_CAP_echo_message feature flag (default TRUE)
- Register echo-message in capability list in m_cap.c
- Modify relay functions in ircd_relay.c to echo back:
  - relay_channel_message() - channel PRIVMSG
  - relay_channel_notice() - channel NOTICE
  - relay_private_message() - private PRIVMSG
  - relay_private_notice() - private NOTICE

Private message echoes include sptr != acptr check to avoid
duplicate when messaging self.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements the IRCv3 account-tag capability which includes the sender's
account name in message tags (@account=accountname or @account=* for
not logged in).

Changes:
- Add CAP_ACCOUNTTAG to capab.h
- Add FEAT_CAP_account_tag feature flag (default TRUE)
- Register account-tag in capability list in m_cap.c
- Refactor send.c message tag handling:
  - Add format_message_tags() to build combined @time;@account tags
  - Add wants_message_tags() helper for capability checks
  - Rename mb_st to mb_tags for clarity
  - Update 5 send functions to use combined tag handling

The implementation combines server-time and account-tag into a single
tag string, sending both to clients that request either capability
(per IRCv3 spec, clients ignore unknown tags).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements the IRCv3 chghost capability which notifies clients
when a user's hostname or username changes, instead of showing
QUIT/JOIN messages.

Changes:
- Add CAP_CHGHOST capability and FEAT_CAP_chghost feature flag
- Add CMD_CHGHOST to msg.h
- Add SKIP_CHGHOST flag for send functions to skip chghost clients
- Modify hide_hostmask() and unhide_hostmask() in s_user.c to:
  * Send CHGHOST to clients with the capability
  * Skip chghost clients when doing QUIT+JOIN workaround
- Update send.c to handle SKIP_CHGHOST flag

Format: :nick!olduser@old.host CHGHOST newuser new.host

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements the invite-notify capability that notifies channel members
when someone is invited to the channel.

Changes:
- Add CAP_INVITENOTIFY to capab.h
- Add FEAT_CAP_invite_notify feature flag (default: TRUE)
- Register capability in m_cap.c
- Send INVITE notification to channel members with capability
  in both m_invite() (local) and ms_invite() (server) handlers

Format: :inviter!user@host INVITE invitee #channel

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement IRCv3.2 labeled-response capability that allows clients to
correlate commands with server responses using @Label tags.

Changes:
- Add CAP_LABELEDRESP capability and FEAT_CAP_labeled_response feature
- Store label per-connection in con_label[64] field
- Parse @Label=value from client message tags in parse_client()
- Add format_message_tags_for() for recipient-specific tag generation
- Add sendcmdto_one_tags() for sending messages with tags
- Modify send_reply() to include @Label and @time tags
- Update echo-message calls to use sendcmdto_one_tags()

The label is cleared at the start of each command and included in all
responses to that command. Labels are client-side only (no P10 changes).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement IRCv3.2 batch capability for grouping related server responses.
This is required for proper labeled-response support on multi-response
commands.

Changes:
- Add CAP_BATCH capability and FEAT_CAP_batch feature flag
- Add batch state fields to connection (con_batch_id, con_batch_seq)
- Add MSG_BATCH command definition
- Implement send_batch_start() to start a batch with label tag
- Implement send_batch_end() to end an active batch
- Implement has_active_batch() helper function
- Update format_message_tags_for() to use @Batch tag when active
- Update send_reply() to use @Batch instead of @Label when batched

The batch BATCH +id type message includes @Label tag for labeled-response
integration. Messages within a batch use @Batch=id instead of @Label.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement setname capability allowing users to change their realname
(GECOS field) mid-session per IRCv3 specification.

Changes:
- Add CAP_SETNAME capability and FEAT_CAP_setname feature flag
- New P10 token: SE (SN was taken by SVSNICK)
- New m_setname.c with m_setname() and ms_setname() handlers
- P10 format: [USER_NUMERIC] SE :[NEW_REALNAME]
- Notify channel members with setname capability
- Propagate changes S2S

IRCv3 spec: https://ircv3.net/specs/extensions/setname

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 14: Add @bot message tag for users with +B mode
Phase 15: Implement standard-replies capability (FAIL/WARN/NOTE)
Phase 16: Add msgid tag with unique ID generation
Phase 17: Implement TAGMSG command (P10 token: TM)
Phase 13a: Add S2S tag parser foundation (backward compatible)

Features:
- @bot tag added to messages from bot-mode users
- send_fail/warn/note() functions for structured error responses
- Message IDs: <server_numeric>-<startup_ts>-<counter> format
- TAGMSG command for tag-only messages (typing indicators)
- S2S parser silently skips @tags prefix for compatibility

New files:
- ircd/m_tagmsg.c: TAGMSG command handlers

New capabilities:
- standard-replies (CAP_STANDARDREPLIES)

New feature flags:
- FEAT_CAP_standard_replies (default: TRUE)
- FEAT_MSGID (default: TRUE)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add full support for client-only tags (+typing, +reply, etc.) in TAGMSG:

Infrastructure:
- Add con_client_tags[512] field to Connection struct for temp storage
- Extract client-only tags (prefixed with +) in parse_client()
- Add format_message_tags_with_client() for formatting tags to clients
- Add sendcmdto_one_client_tags() for user-targeted TAGMSG
- Add sendcmdto_channel_client_tags() for channel TAGMSG relay

TAGMSG changes:
- m_tagmsg: Extract client tags from cli_client_tags(sptr)
- m_tagmsg: Use new send functions for local delivery with tags
- m_tagmsg: Propagate tags S2S via P10 format: TM @+tag=val #channel
- ms_tagmsg: Parse incoming S2S tags from @+tag=val first parameter
- ms_tagmsg: Relay to local clients and propagate to other servers

P10 Format:
  NUMERIC TM @+typing=active #channel
  NUMERIC TM @+typing=active;+reply=msgid ABAAB

This enables typing indicators and other client-only tags to work
across server boundaries.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…OTICE

Add server-to-server message tag support (FEAT_P10_MESSAGE_TAGS):

Parser changes (parse.c):
- Modified parse_server() to extract @time and @msgid tags from incoming
  S2S messages and store them in cli_s2s_time() and cli_s2s_msgid()
- Tags are preserved during message relay for consistency across network

Client storage (client.h):
- Added con_s2s_time[32] and con_s2s_msgid[64] fields to Connection struct
- Added accessor macros cli_s2s_time() and cli_s2s_msgid()

Send functions (send.c):
- Added format_s2s_tags() function to generate/preserve @time;@msgid tags
- Modified sendcmdto_channel_butone() to include S2S tags in server buffers
- Modified sendcmdto_one() to add S2S tags for PRIVMSG/NOTICE to servers

Build fixes:
- Fixed circular dependency between capab.h and client.h by making capab.h
  self-contained with its own FLAGSET macros and forward declaration
- Renamed MSG_BATCH to MSG_BATCH_CMD to avoid conflict with system socket.h
- Fixed m_tagmsg.c function calls to use MSG_TAGMSG instead of CMD_TAGMSG
  for functions that don't take a token parameter

Feature flag:
- FEAT_P10_MESSAGE_TAGS (default: FALSE) controls S2S tag propagation
- When enabled, all PRIVMSG/NOTICE messages between servers include tags
- Tags are preserved from incoming messages or generated fresh if absent

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…netsplit

Add server-to-server BATCH coordination for netjoin and netsplit events:

- Add ms_batch() handler for BT P10 command (m_batch.c)
- Register BATCH command for servers in parse.c
- Add S2S batch tracking fields to client.h:
  - con_s2s_batch_id: active batch ID from server
  - con_s2s_batch_type: batch type (netjoin, netsplit)
- Add send_s2s_batch_start() and send_s2s_batch_end() to send.c
- Propagate batch markers to local clients with batch capability
- Fix FLAGSET_NBITS redefinition warnings in client.h

P10 format:
  [SERVER] BT +batchid type [server1 server2]  # Start batch
  [SERVER] BT -batchid                          # End batch

Batch types: netjoin, netsplit

Future work: Hook into END_OF_BURST and SQUIT handlers to automatically
trigger batches during net events.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Automatically send IRCv3 batch markers to local clients when:
- Netjoin: Server reconnects (junction detected in m_server.c)
  - Batch start on SetBurst/SetJunction
  - Batch end on END_OF_BURST
- Netsplit: Server disconnects (exit_client in s_misc.c)
  - Batch start/end wraps exit_downlinks

New functions:
- send_netjoin_batch_start/end: Track batch on server struct
- send_netsplit_batch_start/end: Use caller-provided batch ID

Batch ID stored on struct Server for netjoin (persists across burst).
Netsplit uses local variable since it's immediate.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Per IRCv3 spec, all messages inside a batch MUST include the @Batch=id
tag. This commit adds:

- Global active_network_batch_id tracking for network events
- set_active_network_batch() and get_active_network_batch() functions
- format_message_tags_with_network_batch() for @Batch tag formatting
- Modified sendcmdto_common_channels_butone() to use batch tags for
  clients with CAP_BATCH capability during network events

Netsplit: set_active_network_batch() called before exit_downlinks()
Netjoin: set in send_netjoin_batch_start(), cleared in send_netjoin_batch_end()

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add send_fail() calls with CAP_STANDARDREPLIES check to TAGMSG, SETNAME,
and AUTHENTICATE error paths. Clients with standard-replies capability
receive structured FAIL messages in addition to traditional numerics.

Error codes added:
- TAGMSG: NEED_MORE_PARAMS, INVALID_TARGET, CANNOT_SEND
- SETNAME: DISABLED, NEED_MORE_PARAMS
- AUTHENTICATE: TOO_LONG, SASL_FAIL

IRCv3 spec: https://ircv3.net/specs/extensions/standard-replies

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add sasl_server_available() check to CAP LS handling. SASL is now only
advertised when:
- FEAT_SASL_SERVER="*" and at least one server is connected, OR
- The specific configured SASL server is connected

This prevents clients from attempting SASL authentication when X3/services
are not available, improving the connection experience.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add SaslMechanisms global to store mechanism list received from services
- Add set_sasl_mechanisms() and get_sasl_mechanisms() functions
- Handle SASL * * M :mechanisms broadcast in ms_sasl()
- Use dynamic mechanism list in CAP LS 302 instead of hardcoded value
- Falls back to static value if no broadcast received

This allows X3 to announce which SASL mechanisms it actually supports
(PLAIN, EXTERNAL, OAUTHBEARER) based on its configuration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, broadcast functions like sendcmdto_common_channels_butone()
and sendcmdto_channel_butone() built a single tagged message with all
available tags (server-time, account-tag, bot) and sent it to any client
that requested any tag capability.

This meant a client requesting only server-time would also receive
@account tags, and vice versa. While technically compliant (clients must
ignore unknown tags per IRCv3 spec), it was wasteful and imprecise.

New implementation:
- Added format_message_tags_ex() with explicit TAGS_* flags control
- Added get_client_tag_flags() to determine which tags each client wants
- Updated broadcast functions to use per-capability message buffer cache
- Each unique combination of capabilities gets its own cached message
- Only tags the client actually requested are included

Functions updated:
- sendcmdto_common_channels_butone()
- sendcmdto_common_channels_capab_butone()
- sendcmdto_channel_butserv_butone()
- sendcmdto_channel_capab_butserv_butone()
- sendcmdto_channel_butone()

Performance: Uses lazy caching - message buffers are only built when
first needed for a given tag combination, then reused for all clients
with the same capabilities.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove unused format_message_tags() and format_message_tags_with_network_batch()
functions that were replaced by the per-capability message buffer caching.

The new implementation uses format_message_tags_ex() with TAGS_* flags and
get_client_tag_flags() to build per-client message buffers.

Update wants_message_tags() comment to clarify it's now only used for
TAGMSG filtering.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add support for the IRCv3 draft/no-implicit-names extension which allows
clients to suppress automatic NAMES replies after JOIN. This reduces
bandwidth for mobile clients and clients joining many channels.

Changes:
- Add CAP_DRAFT_NOIMPLICITNAMES to capab.h
- Add FEAT_CAP_draft_no_implicit_names feature flag (default: TRUE)
- Add capability to m_cap.c negotiation list
- Skip do_names() in m_join.c when capability is negotiated
- Skip do_names() in m_svsjoin.c when capability is negotiated

Spec: https://ircv3.net/specs/extensions/no-implicit-names

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add support for the IRCv3 draft/extended-isupport extension which allows
clients to request ISUPPORT (005) tokens before completing registration.
This enables early feature discovery during capability negotiation.

Changes:
- Add CAP_DRAFT_EXTISUPPORT to capab.h
- Add FEAT_CAP_draft_extended_isupport feature flag (default: TRUE)
- Add MSG_ISUPPORT/TOK_ISUPPORT/CMD_ISUPPORT to msg.h
- Add m_isupport declaration to handlers.h
- Create new ircd/m_isupport.c command handler
- Register ISUPPORT command in parse.c with MFLG_UNREG
- Add m_isupport.c to Makefile.in

The handler reuses send_supported() from s_user.c. Requires the
draft/extended-isupport capability to be negotiated; otherwise
returns ERR_UNKNOWNCOMMAND.

Spec: https://ircv3.net/specs/extensions/extended-isupport

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements IRCv3 draft/pre-away extension which allows clients to set
their away status before completing connection registration. Useful for
bouncers and mobile clients that connect in the background.

Changes:
- Added CAP_DRAFT_PREAWAY to capab.h
- Added FEAT_CAP_draft_pre_away feature flag (default: TRUE)
- Added con_pre_away and con_pre_away_msg fields to Connection struct
- Added mu_away handler for unregistered clients
- Apply pre-away state in register_user() after connection completes
- AWAY * sets away without message (hidden connection - not broadcast)
- Normal AWAY :message is broadcast to servers after registration

Specification: https://ircv3.net/specs/extensions/pre-away

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement the draft/multiline IRCv3 extension which allows clients to
send multi-line messages as a single unit, solving the code pasting
problem that drives users to Discord/Slack/Matrix.

Features:
- Capability with dynamic value: draft/multiline=max-bytes=4096,max-lines=24
- Client BATCH command handling for draft/multiline type
- Message collection with @Batch= tag interception in PRIVMSG
- Support for draft/multiline-concat tag for line joining
- Batch delivery to supporting clients with proper tags
- Fallback delivery as individual messages for non-supporting clients
- Echo-message support for multiline batches
- IRCv3 standard-replies (FAIL) for error handling
- Configurable limits via MULTILINE_MAX_BYTES and MULTILINE_MAX_LINES

Files modified:
- include/capab.h: Added CAP_DRAFT_MULTILINE
- include/ircd_features.h/c: Added multiline feature flags
- include/client.h: Added batch state fields to Connection struct
- include/handlers.h: Added m_batch declaration
- ircd/m_cap.c: Added capability with dynamic value generation
- ircd/parse.c: Added @Batch and draft/multiline-concat tag parsing
- ircd/m_batch.c: Added client batch handler and delivery logic
- ircd/m_privmsg.c: Added batch interception for multiline messages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds native WebSocket (RFC 6455) support directly in Nefarious, achieving
feature parity with Ergo, InspIRCd, and UnrealIRCd for browser-based clients.

Implementation details:
- New websocket.c with handshake, frame encode/decode functions
- Supports binary.ircv3.net and text.ircv3.net subprotocols
- Integrates with existing event loop (no threading required)
- Uses OpenSSL for SHA1/Base64 (no new dependencies)
- Works with existing SSL/TLS infrastructure
- Gated by FEAT_DRAFT_WEBSOCKET feature flag (enabled by default)

Configuration:
  Port { port = 8080; websocket = yes; ssl = yes; };

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add Makefile dependency rules for files added during the IRCv3 upgrade:
- m_batch.o (draft/multiline)
- m_isupport.o (draft/extended-isupport)
- m_setname.o (setname capability)
- m_tagmsg.o (message-tags)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements IRCv3 draft/chathistory extension for message history:
- LMDB backend for zero-copy reads and MVCC concurrency
- All CHATHISTORY subcommands: LATEST, BEFORE, AFTER, AROUND, BETWEEN, TARGETS
- Channel message storage (enabled by default)
- Private message storage (opt-in via CHATHISTORY_PRIVATE feature)
- Message reference formats: timestamp= and msgid=
- Proper batch responses with server-time and msgid tags
- ISUPPORT tokens: CHATHISTORY, MSGREFTYPES

Configuration:
  CAP_draft_chathistory = TRUE     # Enable capability
  CHATHISTORY_MAX = 100            # Max messages per query
  CHATHISTORY_DB = "history"       # LMDB database directory
  CHATHISTORY_PRIVATE = FALSE      # Enable DM history

Build requires: liblmdb-dev (--with-lmdb or --disable-lmdb)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add REDACT command (P10 token: RD) for message deletion
- Integrate with LMDB chathistory for message lookup/deletion
- Authorization: own messages (time-limited), chanops, opers
- Configurable time windows: REDACT_WINDOW (300s default), REDACT_OPER_WINDOW
- Disabled by default (draft spec) - enable with CAP_draft_message_redaction
- Propagate to channel members with capability and to other servers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement IRCv3 draft/account-registration extension for direct account
registration via IRC protocol. Uses XQUERY/XREPLY as fallback mechanism
for backward compatibility with older X3 versions.

New commands:
- REGISTER <account> <email|*> <password> - Register an account
- VERIFY <account> <code> - Verify registration with code
- REGREPLY (S2S) - Response from services

P10 tokens: RG (REGISTER), VF (VERIFY), RR (REGREPLY)

Feature flag CAP_draft_account_registration disabled by default since
this is a draft specification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add batch timeout handling per IRCv3 client-batch specification:

- Add con_ml_batch_start timestamp to track when batch started
- Add FEAT_CLIENT_BATCH_TIMEOUT (default 30 seconds)
- Add check_client_batch_timeout() called from check_pings()
- Send FAIL BATCH TIMEOUT when batch exceeds timeout

When a client opens a batch and doesn't close it within the timeout,
the server sends FAIL BATCH TIMEOUT and discards collected messages.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
MrLenin and others added 2 commits January 13, 2026 03:10
- Changed multiline separator from \n to \x1F (Unit Separator) in m_batch.c
  to avoid base64 encoding overhead in P10 federation
- Updated m_chathistory.c to handle both \x1F (new) and \n (legacy) data:
  - ch_needs_encoding() still triggers base64 for \n (legacy compatibility)
  - send_history_message() splits on \x1F for client delivery
- Added 3-tier fallback for multiline chathistory delivery:
  - Tier 1: Client has multiline cap → nested draft/multiline batch
  - Tier 2: Client has chathistory batch only → separate PRIVMSGs with shared msgid
  - Tier 3: No batch support → truncate to first line
- Skip U-lined servers (services) in chathistory federation since they don't
  store chat history, fixing empty batch timeout issue

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a client sends a multiline batch with a label tag and has
labeled-response capability enabled, the echoed BATCH +id message
should include the original label. This allows clients to correlate
the server's echo response with their original request.

Per IRCv3 labeled-response spec: "The label tag MUST be included in
any response to a labeled message".

Note: S2S relay via P10 doesn't carry labels (they're C2S/S2C only).
Recipients don't receive labels (they didn't send the original).
PM echo sends individual PRIVMSGs not BATCH, so no label there.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
MrLenin and others added 27 commits January 13, 2026 06:03
- Changed max-value-bytes from 1024 to 300 (IRC messages limited to 512 bytes)
- Changed error code from VALUE_TOO_LONG to VALUE_INVALID per IRCv3 spec
- Added visibility checks for private metadata in GET/LIST commands
- Added RPL_METADATAEND (762) numeric for proper METADATA LIST termination
- Added chathistory retention advertisement in CAP LS (retention=Xd)
- Excluded *.o and *.a files from Docker context to prevent stale builds

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When X3 responds with an error for non-existent accounts/channels,
immediately send FAIL TARGET_INVALID to the waiting client instead of
letting the request timeout after 30 seconds.

Protocol: Services send `MD <target> * ! :NOTARGET` where `!` is the
error visibility marker.

- Added METADATA_VIS_ERROR constant
- ms_metadata() parses `!` visibility and handles error response
- metadata_handle_response() sends FAIL TARGET_INVALID for errors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The metadata_x3_is_available() check was preventing MDQ queries
from being sent when X3 was connected but hadn't sent any MD
messages yet. The heartbeat flag was only set when receiving MD
from X3, which doesn't happen when querying for non-existent
accounts.

The find_services_server() check is sufficient and authoritative
for determining if X3 is available to handle metadata queries.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, METADATA GET for non-existent channels would only check
if the channel existed in memory, and skip querying X3/LMDB for
registered channel metadata. This caused the code to fall through
to RPL_KEYNOTSET instead of querying X3.

Now non-existent channels (where FindChannel returns NULL) will:
1. Check LMDB cache for registered channel metadata
2. Query X3 if not in cache (which returns NOTARGET for unregistered)
3. IRCd sends FAIL TARGET_INVALID when NOTARGET received

Also added null check before calling metadata_set_channel() since
target_channel may be NULL for non-existent channels.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
MetadataRequest.target was using ACCOUNTLEN (15 chars) which is too
short for channel names (up to 200 chars) and longer test targets.
This caused target truncation and failed request matching when X3
returned NOTARGET responses.

Changed to CHANNELLEN (200 chars) to accommodate all valid targets.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The channel structure allocated only enough space for the original
channel name. This prevented RENAME from working when the new name
was longer than the old name, returning "New channel name is too long".

Fix by reallocating the channel structure when needed, updating all
pointers:
- Hash table entry
- Global channel linked list (prev/next)
- All Membership->channel pointers
- User invite lists (cli_user->invited->value.chptr)
- Pending destruct event

The in-place rename path is preserved for the common case where the
new name fits in the existing allocation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Per IRCv3 spec, REDACT should be forwarded "in the same way as PRIVMSG
messages." This means echo-message behavior applies: the sender should
only receive their own REDACT back if they have the echo-message
capability negotiated.

Previously, the sender was always skipped. Now:
- Send to all channel members with draft/message-redaction capability
- Echo to sender only if they also have echo-message capability
- Skip remote clients (MyUser check)

This fixes tests that expect REDACT echo when both capabilities are
negotiated.
When rename_channel() reallocates the channel structure for a longer
name, the old pointer becomes invalid. Callers were continuing to use
the stale pointer, causing use-after-free bugs.

Changed rename_channel() signature to take struct Channel** so it can
update the caller's pointer when reallocation occurs. All callers in
m_rename.c updated to pass &chptr.

This fixes RENAME tests that were failing because the response used
the freed channel name instead of the new name.
The msgid format is <server>-<startup_time>-<counter>, where the
timestamp is the server's startup time, NOT the message creation time.

This caused REDACT_WINDOW_EXPIRED errors for all messages if the
server had been running for more than REDACT_WINDOW seconds.

Now we:
1. Look up the message in history first
2. Use msg->timestamp (actual message time) for window check
3. Fall back to msgid parsing only when history is unavailable

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, invalid timestamp= parameters were silently treated as
GET queries (returning the stored marker or *). Now the server properly
returns FAIL MARKREAD INVALID_PARAMS when timestamp format is invalid.

This matches expected behavior per IRCv3 read-marker spec where timestamp
must be in ISO 8601 format.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Messages from U-lined servers (services like X3) are now sent with
priority, ensuring they're flushed immediately instead of being
batched with the 1KB threshold.

This fixes delays of 30+ seconds when services respond to client
queries - the responses would sit in the send buffer until enough
data accumulated to trigger a flush.

Changes:
- Add is_from_uline() helper to check if client is from U-lined server
- Add FEAT_FLUSH_ULINE_IMMEDIATE feature flag (default TRUE)
- Set prio=1 for messages from U-lined servers in sendcmdto_one*
- Flush immediately in send_buffer when prio is set

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
OpenSSL can decrypt multiple IRC messages from a single TLS record into
an internal buffer. After SSL_read() returns, there may be more data
waiting that won't trigger epoll (since the kernel socket appears empty).

This caused intermittent message delays when multiple IRC commands arrived
in the same TLS record - subsequent messages would wait until new network
data arrived to wake up the event loop.

Changes:
- Add ssl_pending() wrapper to check for buffered SSL data
- In read_packet(), loop while SSL_pending() > 0 to drain all buffered data
- Handle client, server, and handshake connections appropriately

For client connections: accumulate all SSL data in receive queue before processing
For server/handshake connections: process each chunk then check for more

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add freelists to reduce malloc/free overhead for frequently
allocated structures:

- DLink: Used for channel member lists, recycled via freelist
- MetadataRequest: Used for MDQ queries to X3, pooled with mdq_alloc/mdq_free

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove hardcoded Valgrind from CMD
- Add NEFARIOUS_VALGRIND=1 environment check in entrypoint
- Default to normal execution without Valgrind overhead

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 1 of chathistory federation - infrastructure only, no behavior change.

- Add ChathistoryAd structure for tracking server storage capabilities
- Add CH A message parser for storage/retention advertisements:
  - CH A S <retention> - Storage capability (at BURST)
  - CH A R <retention> - Retention update (on REHASH)
  - CH A F/+/- - Channel sync (stubbed for Phase 3)
- Add helper functions:
  - has_chathistory_advertisement() - check if server stores history
  - server_retention_days() - get retention policy
  - clear_server_ad() - cleanup on SQUIT
- Hook clear_server_ad into exit_one_client() for proper cleanup

This enables future phases:
- Phase 2: Send CH A S at BURST
- Phase 3: Use advertisements to route federation queries
- Phase 4: Write forwarding (CH W) for storage gaps

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 2 of chathistory federation - advertisement sending.

- Send CH A S <retention> to newly linked servers after END_OF_BURST_ACK
- Only advertise if FEAT_CAP_draft_chathistory is enabled
- Enables remote servers to know our storage capability and retention

This allows federation routing to skip servers without history storage.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Only send federation queries to servers that have advertised
chathistory storage capability (CH A S). This reduces unnecessary
traffic and latency by skipping non-storage servers.

Changes:
- Added server_retention_covers() to check if server's retention
  window covers a given query timestamp
- Added count_storage_servers() to count only CH A S servers
- Modified start_fed_query() to:
  - Parse timestamp from reference for retention filtering
  - Only count/query servers with has_chathistory_advertisement()
  - Apply retention window filtering
- Modified ms_chathistory() CH Q propagation to filter by
  advertisements and retention

No backward compatibility with non-CH-A servers (new feature).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add FEAT_CHATHISTORY_STORE to separate the "storage" concern from
the "CAP advertisement" concern. This enables relay-only servers
that advertise draft/chathistory to clients but forward queries
to storage servers.

- Added FEAT_CHATHISTORY_STORE feature flag (default TRUE)
- Updated all storage write paths to check CHATHISTORY_STORE:
  - ircd_relay.c: store_channel_history(), store_private_history()
  - channel.c: store_channel_event()
  - m_kick.c: store_kick_event()
  - m_topic.c: store_topic_event()
  - ircd.c: history_purge_callback(), main() init
- Updated CH A S sending to check CHATHISTORY_STORE (not CAP)
- Updated s_misc.c clear_server_ad() call

Server role matrix:
- CAP=TRUE, STORE=TRUE: Full storage server (default)
- CAP=TRUE, STORE=FALSE: Relay server (forwards to STORE)
- CAP=FALSE: No chathistory participation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement write forwarding for non-storage servers to relay messages
to storage-capable servers for persistence.

Protocol additions:
- CH W <target> <msgid> <timestamp> <type> <text> - plain text forwarding
- CH WB <target> <msgid> <timestamp> <type> <base64> - chunked base64

Features:
- CHATHISTORY_WRITE_FORWARD feature flag to enable forwarding
- CHATHISTORY_STORE_REGISTERED to store registered channel history
- history_has_msgid() for msgid-based deduplication
- Automatic chunking for multiline/large content (>400 bytes)
- Storage decision: registered channels OR has local users

The write forwarding finds the nearest storage server by checking
for chathistory advertisement (CH A S) on connected servers.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement automatic eviction and storage monitoring to prevent
database full conditions with graceful degradation.

Features:
- history_db_utilization() - returns current usage percentage
- history_storage_state() - returns NORMAL/WARNING/CRITICAL/SUSPENDED
- history_evict_to_target() - evicts oldest messages to target %
- history_maintenance_tick() - periodic check with auto-eviction

Feature flags:
- CHATHISTORY_HIGH_WATERMARK (85%) - triggers eviction
- CHATHISTORY_LOW_WATERMARK (75%) - eviction target
- CHATHISTORY_MAINTENANCE_INTERVAL (300s)
- CHATHISTORY_EVICT_BATCH_SIZE (1000)

Enhanced STATS H output now shows:
- Database size and utilization percentage
- Storage state (NORMAL/WARNING/CRITICAL/SUSPENDED)
- Watermark settings
- Last eviction timestamp and count

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…EHASH

Phase 7 implementation items:

Emergency eviction (MDB_MAP_FULL recovery):
- Add history_emergency_evict() function for inline recovery
- Called automatically when LMDB returns MDB_MAP_FULL error
- Evicts batch of 500 oldest messages and retries operation
- Logs warning when triggered

STATS A for chathistory advertisements:
- New chathistory_report_ads() function for /STATS A
- Shows all servers that have advertised storage capability
- Reports retention days and last update time for each
- Shows local storage status

CH A R on REHASH:
- Add feature_notify_chathistory_retention() callback
- Automatically broadcasts retention changes to peer servers
- Triggered when CHATHISTORY_RETENTION is changed via SET/REHASH

CH W trust model refinement:
- Store forwarded messages even without local channel users
- Trust forwarding server's decision on what should be stored
- Handles edge cases like registered channels during netjoin

Compilation fixes:
- Add #include <time.h> to history.h and handlers.h
- Add forward declarations for struct Channel and struct StatDesc
- Fix DEBUG_INFO -> DEBUG_DEBUG (DEBUG_INFO doesn't exist)
- Add forward declarations for is_ulined_server() and has_chathistory_advertisement()
- Fix int_to_base64_str() to use inttobase64() instead of convert2n[]

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add local-interest check to store_channel_history() to prevent storing
messages when the server is just relaying P10 traffic with no local users.

Storage only happens when:
- The sender is local (our user sent the message)
- The channel has local users (we have interest in the conversation)

This prevents hub servers from redundantly storing messages they're just
passing through. Messages reach STORE servers without local users via
CH W forwarding from non-STORE servers, which has its own storage logic
in process_write_forward().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement channel-level advertisements for targeted query routing:

- CH A F: Full channel sync at burst, chunked if >512 bytes
- CH A +: Incremental add when first message stored for a channel
- Query filtering: Only query servers that advertise having the target
- history_enumerate_channels(): Callback-based channel enumeration
- history_has_channel(): Check if channel has stored history
- broadcast_channel_advertisement(): Notify all peers of new channel

This optimizes federation by avoiding queries to servers that don't
have history for the requested channel.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When channels are emptied by eviction or retention purge, broadcast
CH A - to remove them from peer servers' routing tables:

- Add history_channel_removed_cb callback type for notifications
- Add history_set_channel_removed_callback() to register callback
- Add history_channel_has_messages() to check for actual messages
- Add history_cleanup_empty_targets() called after eviction/purge
- Implement remove_server_channel_ad() for CH A - parsing
- Update CH A - parsing to call remove_server_channel_ad()
- Add broadcast_channel_removal() for outbound CH A -
- Register callback via chathistory_init_callbacks() on startup

This ensures federation routing tables stay accurate as history
ages out, preventing queries to servers that no longer have the data.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When responding to CH Q queries, send compressed LMDB data directly
with CH Z instead of decompressing first. Reduces S2S bandwidth.

- Add raw_content/raw_content_len to HistoryMessage struct
- Preserve raw compressed bytes during query when USE_ZSTD enabled
- send_ch_response() uses Z flag when compressed data fits single msg
- Receiver decodes base64, decompresses, then processes normally
- Falls back to CH B chunking if compressed data too large

Protocol: CH Z <reqid> <msgid> <ts> <type> <sender> <account> :<b64_zstd>

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Change callback to accept array of channels instead of single channel
- Batch channel removals during eviction/purge into single messages
- Parse multiple space-separated channels in +/- receivers
- Reduces S2S traffic during bulk eviction

Protocol: CH A - :#chan1 #chan2 #chan3 (space-separated)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant