runar is a keyboard-first, statically-linkable Linux file manager written from scratch in pure Rust. removed bitchat for now

This commit is contained in:
Jeremy Anderson 2026-07-28 14:10:20 -04:00
parent 7b5d863e8f
commit 00dcf44335
31 changed files with 730 additions and 6737 deletions

View File

@ -1,44 +1,229 @@
# nirc-rs 0.9.0
# nirc-rs 0.10.2 — IRC fixes, the input throttle, and saying goodbye (for now) to BitChat
I just released nirc-rs 0.9.0 — a multi-protocol terminal chat client I wrote in Rust. It runs entirely in your terminal and puts eight chat protocols behind a single interface. No web browsers, no Electron, no JavaScript runtime. Just a binary and a terminal.
Two patch batches landed back-to-back: 0.10.1 fixed a pile of
long-standing IRC bugs and added the safety nets I'd been meaning to
build for months, and 0.10.2 removes the BitChat protocol module
entirely. The BitChat removal deserves its own discussion — it's not a
technical decision, it's a legal one, and I want to be transparent
about why.
## Why I Built It
## 0.10.2: BitChat withdrawal
nirc-rs descends from naim, the terminal AIM/ICQ/IRC client from the late 1990s. naim had a simple idea: one terminal window, all your chat networks, zero graphical dependencies. I liked that idea, but the world moved on. naim's codebase stayed stuck in C89, its protocol support stopped at IRC and the now-defunct AIM/ICQ, and it couldn't handle TLS, E2EE, or modern protocols like Matrix and Discord.
The BitChat project — Jack Dorsey's recently-announced P2P messaging
protocol — is the subject of active litigation in the Indian courts.
The legal status of the protocol specification, the reference
implementation, and downstream reimplementations such as the one
previously shipped in nirc-rs is currently unclear. Specifically:
I wanted that same experience back — a TUI client that treats every protocol as a first-class citizen — but built on modern foundations. Rust for memory safety and async I/O. ratatui for the terminal UI. rustls for TLS without OpenSSL. libp2p for peer-to-peer. The result is roughly 10,000 lines across 27 source files.
- Court filings have not yet established whether the BitChat protocol
itself is encumbered by intellectual-property claims, or whether only
the reference client is affected.
- There is live ambiguity about whether shipping a third-party
implementation of the protocol exposes downstream distributors to
secondary liability.
- The reference implementation's license terms have changed during the
dispute, and it's not yet clear which license applies to derivative
works that were created before the change.
## Architecture
Pending clarity on these points, I've removed the BitChat module from
nirc-rs entirely. The risk of shipping an encumbered protocol backend
outweighs the benefit of offering P2P chat that users can obtain from
other sources in the meantime.
nirc-rs is structured around a multi-protocol dispatcher. Each protocol (IRC, ADC/DC++, Matrix, Discord, Stout, Spacebar, Nerimity, BitChat) implements a common trait and feeds messages into a unified ChatMessage type. The TUI layer doesn't care which protocol a message came from — it renders it the same way, with color-coded timestamps and protocol badges.
**What was removed:** `src/protocols/bitchat.rs` (the entire protocol
backend), the `ProtocolType::BitChat` enum variant, the three
`/bitchat …` commands, the F1 menu entry, and — significantly — the
`libp2p` Cargo dependency, which BitChat was the only consumer of.
Dropping libp2p noticeably trims the dependency tree and compile time.
The Noise primitive in `src/engine/crypto.rs` uses `x25519-dalek` +
`aes-gcm` directly (not libp2p), so ADC's encrypted client-client
connections keep working.
The async runtime is tokio with multi-threaded scheduling. File transfers run over yamux-multiplexed streams with 256 KiB buffers and in-flight SHA-256 verification. The identity vault uses AES-256-GCM encryption with Argon2id key derivation. Matrix's megolm E2EE runs on a dedicated OS thread because the matrix-sdk crypto types aren't Send — a necessary compromise that I handle transparently.
**What this means for users:** Existing config entries with
`protocol = "bitchat"` are silently ignored at load time. Old history
files tagged `P2P:<name>` are silently reassigned to the Status tab.
No data is lost; IRC / Matrix / ADC / Discord / Stout / Spacebar /
Nerimity configs and history are unaffected. The full rationale and
re-evaluation criteria are in `NOTICES.md` — I'll restore the module
when the courts case reaches a final judgement, the reference
implementation's license is unambiguous, and at least one independent
legal opinion clears clean-room reimplementations.
## What's New in 0.9.0
If you want P2P-style direct messaging in the meantime, ADC's
client-client connections offer direct file transfer and direct
messages over a hub-and-spoke topology rather than full mesh P2P.
This release focused on navigation and discoverability. I added Ctrl-P (previous buffer), Ctrl-A (next active buffer), and Ctrl-Z (highlight word cycling) for faster window management. The Insert key now scrolls to the bottom of chat and re-enables auto-scroll.
## 0.10.1: The IRC fixes
The biggest UI change is the F1 dropdown menu. It provides a visual, navigable command tree. If you can't remember whether it's /whois or /wi, press F1 and find it. This replaced the old debug console binding, which I'm reassigning in a future patch.
A user audit turned up a stack of real bugs in the IRC backend, plus a
couple of "this should exist" features I'd been deferring. The fixes:
The transfer ticker now shows real-time speed and ETA in the footer bar, so you don't need to toggle a separate panel to see how your file transfers are progressing. I also fixed /nick to update all tab titles immediately and added the local IP address to the status bar.
### CTCP was broken in two ways
## The Tested Frontier: IRC and ADC/DC++
Outgoing CTCP requests (`/ctcp <nick> VERSION` and friends) were being
sent as `NOTICE` instead of `PRIVMSG`. Per the IRCv3 CTCP spec, requests
must be `PRIVMSG``NOTICE` is only for replies, and strict servers
ignore NOTICE-based CTCP because RFC 1459 says NOTICE must never trigger
an automated reply. This was why `/ctcp` queries were silently ignored
by some networks.
Two protocols are battle-tested in 0.9.0: IRC and ADC/DC++.
The second bug was worse: if you `/ctcp`'d yourself (e.g.
`/ctcp mynick VERSION` to test your own client), the entire CTCP block
was skipped because the sender matched the local nick. The echo from
the server was silently swallowed. Now self-targeted CTCP queries are
visible, and the auto-reply loop guard is preserved.
IRC has full TLS support via rustls, SASL PLAIN authentication, CTCP auto-response, ISUPPORT negotiation, and operator commands. It connects to Libera, OFTC, and other networks without issue. ADC/DC++ connects to hubs, performs the HSUP/HSID/INF handshake, supports hub search, and handles file transfers with the full yamux-multiplexed pipeline. ADC also has a varnish-style security guard pipeline that does rate limiting, IP validation, SSRF prevention, and path traversal blocking.
### `/away` was a stub
## The Untested Frontier
The `away-notify` IRCv3 capability was in the requested-caps list, but
the incoming `AWAY` command from other users had no handler — it fell
into the "Unhandled command" notice path. Worse, we didn't even track
our own away state: there was no `is_away` field, and `RPL_NOWAWAY`
(306) / `RPL_UNAWAY` (305) fell through to the generic raw-numeric
dump. So the `away-notify` capability was negotiated and then nothing
was done with it. Textbook stub.
Six protocols are fully implemented but haven't been tested against live servers yet: Matrix (with megolm E2EE via matrix-sdk 0.18), Discord (Gateway WebSocket), Stout and Spacebar (Revolt-compatible forks), Nerimity (a custom platform), and BitChat (P2P over libp2p with mDNS discovery and gossipsub).
Fixed: `ConnState` now tracks `is_away` and `away_message`; the `/away`
command optimistically marks the local state and posts a confirmation
notice; 305/306 have explicit handlers; and incoming `AWAY` messages
from other users are cached per-nick in `state.nick_away` and surfaced
as notices.
These aren't stubs — they're complete protocol handlers with connection management, message parsing, event dispatch, and TUI integration. They just need to be pointed at a real server to verify the wire protocol matches reality. That's the top priority for the next release cycle.
### `/who` crashed on self
## Security
I never managed to reproduce this from static reading alone, but the
`RPL_WHOREPLY` (352) handler was mis-splitting the trailing field per
RFC 1459 (the trailing is `<hopcount> <real name>`, not just the real
name), and there was no explicit `RPL_ENDOFWHO` (315) terminator. Now
the handler correctly splits hopcount from realname, displays the
`H`/`G` (here/away) flag from the flags field, marks self-entries with
`(you)`, and has a dedicated 315 handler.
nirc-rs never phones home. There's no telemetry, no analytics, no update checker. The identity vault encrypts credentials with AES-256-GCM and Argon2id (64 MiB memory, 3 iterations), and keys are zeroed from RAM on lock via the zeroize crate. ADC connections go through a guard pipeline that rejects private IPs, blocks path traversal, and prevents SSRF. TLS is handled by rustls with the webpki-roots CA bundle — no system OpenSSL needed.
### `/me` and `/notice` got local echo
## Build It
Many servers — especially bouncers, mock servers, and servers with
`echo-message` disabled — don't echo your own `PRIVMSG`s back to you.
So you'd type `/me dances` and see nothing. Now `/me` and `/notice`
echo locally in the active tab immediately. The server's echo (if any)
lands with `is_own=true` and is naturally deduplicated by the user's
perception — you don't see two copies, you just see your action.
## 0.10.1: The input throttle
The trigger for this was a user reporting they'd accidentally spammed a
room with 20+ lines. The fix is two-layered:
1. **Per-send line cap.** A single input submission is capped at 4
lines (`MAX_LINES_PER_SEND`). Pasting a 50-line file no longer dumps
50 lines into the channel — only the first 4 are sent and a notice
explains the truncation.
2. **Sliding-window rate limit.** At most 8 outgoing lines per 3
seconds (`MAX_LINES_PER_WINDOW` / `WINDOW_SECS`). A stuck Enter key
or rapid-fire paste that would otherwise flood the channel is
rejected after the cap, with a single warning notice per burst
(subsequent rejections in the same burst are silent to avoid
flooding the user's own tab with throttle notices).
Both limits are conservative — a human typing normally will never hit
them. They exist purely as a safety net for accidents. The
`InputThrottle` struct is in `src/core/throttle.rs` with 7 unit tests
covering paste truncation, rate window sliding, burst-warning
suppression, and CRLF/empty-input edge cases. Currently the limits are
hardcoded; a future release will expose them in `config.toml` under a
`[throttle]` section.
## 0.10.1: Channel rotation reverted
This is a revert of a previous "smart" feature. The old `Ctrl-N`
cycled tabs in priority-tier order: Unread > Conversed > Inert, sorted
within tier by recent activity. It matched naim's semantics on paper.
In practice, the activity-based reordering made `Ctrl-N` feel
non-deterministic — the same keypress could land on a different tab
each time depending on which channel received a message most recently.
Users couldn't build muscle memory for "Ctrl-N three times gets me to
#sourcemage".
The new behaviour is a plain round-robin through the tab list in
insertion order. Tabs that fail `is_cyclable()` (unjoined IRC channels,
hidden server tabs) are still skipped, but the relative order of the
remaining tabs is preserved. Boring, predictable, and your fingers
will thank you.
## 0.10.1: URL detection + media framework
There was previously zero URL detection in the TUI. Now message bodies
are scanned for URLs (`http://`, `https://`, `ftp://`, `www.` prefixes),
and detected URLs are rendered underlined in cyan so they're visually
distinct. Trailing sentence punctuation is stripped from the URL itself,
and URLs wrapped in `<...>` or `(...)` are extracted cleanly without
the surrounding punctuation.
On top of that, three new commands:
- **`/url <url>`** — open a URL in the OS default browser (xdg-open /
open / start). Refuses non-http/https/ftp schemes for safety.
- **`/video <url>`** — launch a video URL in the OS default player.
The OS picks the right player based on the URL's file extension or
mime-type handler.
- **`/image <url>`** — attempt inline image rendering. If the terminal
supports Kitty graphics / iTerm2 inline images / Sixel, the image is
rendered in place. If not, falls back to `/url` behaviour.
The inline image rendering itself is a stub for now —
`try_render_inline_image()` returns `Unsupported` and the caller falls
back gracefully. The graceful-fallback path (text placeholder +
external open) is wired up, so when I implement the real Kitty/iTerm2/
Sixel emission it's a pure feature add with no risk to existing
functionality. The terminal-protocol detection is in place
(`detect_image_protocol()`), so the framework is ready.
## 0.10.1: Top-right bandwidth monitor
The top-right corner of the TUI used to say `nirc`. Static, useless.
Now, when transfers are active, it's replaced with a live bandwidth
monitor:
```
↓1.2MiB/s ↑0.5MiB/s file.zip 45%
```
Aggregate download/upload rates, plus the most-active file's progress
percentage. Falls back to `nirc` when no transfers are active. The
rates are computed by `TransferManager::summary()`, which samples each
active transfer's `bytes_transferred` against the previous frame's
sample — so it's a real instantaneous rate, not a moving average.
## 0.10.1: Line wrapping
The bug report was "extremely long lines from IRC doesn't wrap lines
and text is lost if resolution is small". The fix is a `wrap_text()`
helper that breaks on word boundaries when possible and falls back to
hard character breaks for words longer than the available width (e.g.
long URLs). The `ChatView::render` method now wraps each body line to
fit the available width instead of truncating at the right margin. Long
messages are now fully readable even on 80-column terminals.
## What's next
Per user direction, Matrix production hardening and the Stout/Spacebar/
Nerimity full builds are deferred to dedicated follow-up sessions.
Matrix is the most complex of the group, and the three Revolt-family
stubs each need a full protocol implementation comparable in scope to
the Discord backend.
The two natural follow-ups from 0.10.1/0.10.2:
1. **Real inline image rendering** — the stub is in place, the
graceful fallback works, and the framework is ready. Just need the
Kitty/iTerm2/Sixel emission code.
2. **Configurable throttle limits** — currently hardcoded; expose them
in `config.toml` under a `[throttle]` section.
And the obvious one: **re-evaluate BitChat** once the India courts
case settles.
## Build it
```sh
git clone https://git.dcos.net/dcosnet/nirc-rs.git
@ -47,10 +232,10 @@ cargo build --release
./target/release/nirc-rs
```
One binary, no runtime dependencies beyond your terminal emulator. Config lives at ~/.nirc/config.toml and is created automatically on first run.
One binary, no runtime dependencies beyond your terminal emulator.
Config lives at `~/.nirc/config.toml` and is created automatically on
first run.
## What's Next
The roadmap for 0.10.0 and beyond is straightforward: test the six untested protocols, fix ADC CID generation to use proper Base32/Tiger hashes, integrate the transfer widget into the main draw loop, and add IRC SASL EXTERNAL with client certificates. I'm aiming for a 1.0.0 release once all eight protocols are verified against live servers and the plugin API has a stability guarantee.
If you want to help test a protocol, write a plugin, or contribute a patch, the repository is at git.dcos.net/dcosnet/nirc-rs. It's GPL-3.0-or-later, and contributions are welcome.
If you want to help test a protocol, write a plugin, or contribute a
patch, the repository is at git.dcos.net/dcosnet/nirc-rs. It's
GPL-3.0-or-later, and contributions are welcome.

View File

@ -1,39 +1,145 @@
# nirc-rs 0.10.1 — Patch Summary
# nirc-rs — Patch Summary
This patch addresses the user-reported issues with nirc-rs 0.10.0 and adds
several requested features. The changes are organized into five phases.
This file summarizes the patch batches applied to nirc-rs, newest first.
## Files Changed
---
## 0.10.2 — BitChat Module Withdrawn
**Date:** 2026-07-29
The BitChat protocol module and all of its integrations have been removed
from the codebase. See `NOTICES.md` for the full rationale.
### Why
The BitChat project (Jack Dorsey's recently-announced P2P messaging
protocol) is the subject of active litigation in the Indian courts. The
legal status of the protocol specification, the reference implementation,
and downstream reimplementations is currently unclear. Pending clarity,
we have removed the module entirely rather than ship an encumbered
protocol backend.
### Files Changed
- **Deleted:** `src/protocols/bitchat.rs`
- **`src/protocols/mod.rs`** — removed `pub mod bitchat;` and re-exports
- **`src/core/protocol.rs`** — removed `ProtocolType::BitChat` variant and
all of its match arms in `tag()`, `label()`, `badge()`, `naim_color()`;
updated the four protocol unit tests
- **`src/core/command.rs`** — removed `BitChatPeers`, `BitChatDm`,
`BitChatSendFile` command variants; removed the `"bitchat" | "p2p"`
parser arm and the `"bitchat" => ProtocolType::BitChat` FromStr arm;
removed the `"bitchat" | "bc" | "p2p"` arm from `/xfer` protocol lookup
- **`src/engine/dispatcher.rs`** — removed `ProtocolCommand::BitChat`
variant; removed `connect_bitchat()` method; removed BitChat dispatch
arms in `Msg`, `Me`, `Say`, `quit_for`, and the explicit
`BitChatPeers`/`BitChatDm`/`BitChatSendFile` handler block; removed
BitChat from the connect dispatch and the tag-map lookup table
- **`src/tui/menubar.rs`** — removed the "Connect → BitChat…" menu entry
- **`src/tui/chat_view.rs`** — removed BitChat from the protocol color
lookup tables (both dim-color and NaimColor)
- **`src/tui/winlist.rs`** — removed BitChat from the badge char and
badge color lookup tables; updated the module-level doc
- **`src/core/history.rs`** — removed the `"P2P" => BitChat` tag mapping
(old history files with this tag will be silently reassigned to Status)
- **`src/logging/mod.rs`** — removed BitChat from `sanitize_server`
match; updated the test
- **`src/transfer/engine.rs`** — updated the integration test to use
`ProtocolType::Adc` instead of `ProtocolType::BitChat`
- **`src/core/app.rs`** — updated the
`next_tab_by_priority_mixes_protocols_freely` test to use
`ProtocolType::Adc` instead of `ProtocolType::BitChat`
- **`src/config/mod.rs`** — updated the BitChat server-entries doc
section to note the removal
- **`src/engine/crypto.rs`** — updated the module-level doc to note
that the Noise primitive is now only used by ADC (was previously
shared with BitChat)
- **`src/main.rs`** — renamed `_p2p_keypair``_noise_keypair` (still
generated, since ADC uses it); removed the BitChat help text and
replaced with a media-commands help section + a BitChat-removed note;
removed the `Command::BitChatPeers | BitChatDm | BitChatSendFile`
dispatch arm
- **`Cargo.toml`** — bumped version to 0.10.2; removed `libp2p`
dependency (BitChat was the only consumer); removed `"p2p"` from the
keywords list
- **`NOTICES.md`** — new file, contains the full rationale and
re-evaluation criteria for the BitChat withdrawal
- **`README.md`, `STATUS.md`, `TODO.md`, `QUICKSTART.md`/`quickstart.md`,
`man/man1/nirc.1`, `completions/nirc.{bash,zsh,fish}`,
`packaging/PKGBUILD`, `packaging/nirc.spec`,
`packaging/debian/DEBIAN/control`, `BLOG_POST.md`** — updated to
reflect the removal and point at `NOTICES.md`
### User Impact
- **Existing configs:** Any `[[servers]]` entry with
`protocol = "bitchat"` will be silently ignored at load time. You do
not need to edit your config file, but you may remove the entries to
clean it up.
- **Existing history files:** Per-tab scrollback files tagged
`P2P:<name>` will fail to match any known protocol on load and will be
silently assigned to the Status tab. The files themselves are not
deleted; you can read them with a text editor if you need to recover
the content.
- **No data loss:** Your IRC / Matrix / ADC / Discord / Stout / Spacebar
/ Nerimity history, configs, and credentials are unaffected.
### Re-evaluation Criteria
We will re-evaluate restoring the BitChat module when **all** of the
following are true:
1. The India courts case has reached a final judgement (not an interim
order), OR the parties have publicly settled and the settlement terms
are clear about downstream-implementation rights.
2. The BitChat reference implementation's license is unambiguous and
permits third-party reimplementations.
3. There is at least one independent legal opinion (not from a party to
the litigation) concluding that shipping a clean-room reimplementation
of the protocol is safe.
Until then, the module will remain withdrawn.
---
## 0.10.1 — IRC Fixes, Throttle, Rotation Revert, URLs/Media, Bandwidth Monitor, Line Wrap
**Date:** 2026-07-28
This patch batch addresses the user-reported issues with nirc-rs 0.10.0
and adds several requested features. The changes are organized into five
phases.
### Phase 1: IRC Protocol Fixes (`src/protocols/irc.rs`, `src/main.rs`)
- **CTCP spec compliance** — Outgoing CTCP requests now use `PRIVMSG` (was
`NOTICE`). Per the IRCv3 CTCP spec, requests must be PRIVMSG; only replies
use NOTICE. This was why `/ctcp <nick> VERSION` was silently ignored by
strict servers.
- **Self-targeted CTCP visible**`/ctcp mynick VERSION` now displays the
request in the relevant tab. Previously the entire CTCP block was skipped
when the sender was the local nick, silently swallowing self-targeted
queries.
- **CTCP spec compliance** — Outgoing CTCP requests now use `PRIVMSG`
(was `NOTICE`). Per the IRCv3 CTCP spec, requests must be PRIVMSG;
only replies use NOTICE. This was why `/ctcp <nick> VERSION` was
silently ignored by strict servers.
- **Self-targeted CTCP visible**`/ctcp mynick VERSION` now displays
the request in the relevant tab. Previously the entire CTCP block was
skipped when the sender was the local nick, silently swallowing
self-targeted queries.
- **`/away` tracking** — Added `is_away` and `away_message` fields to
`ConnState`. The `/away [msg]` command optimistically marks the local
state and posts a confirmation notice. Added explicit handlers for
`RPL_NOWAWAY` (306) and `RPL_UNAWAY` (305) instead of letting them fall
through to the generic numeric dump.
`RPL_NOWAWAY` (306) and `RPL_UNAWAY` (305) instead of letting them
fall through to the generic numeric dump.
- **IRCv3 `away-notify` handler** — Added a dedicated `AWAY` command
handler that caches the away reason per nick in `state.nick_away` and
posts a notice. Previously the `away-notify` capability was negotiated
but the incoming AWAY messages fell into the "Unhandled command" path.
- **`/who` hardening** — `RPL_WHOREPLY` (352) now correctly splits the
trailing field into hopcount and realname (per RFC 1459), displays the
here/away flag (`H`/`G`) from the flags field, marks self-entries with
`(you)`, and has an explicit `RPL_ENDOFWHO` (315) terminator handler.
This addresses the historical `/who <self>` crash.
- **`/me` local echo** (`src/main.rs`) — Actions are now echoed locally in
the active tab immediately, so the user sees their action even on servers
that don't echo own PRIVMSGs (bouncers, mock servers, etc.).
- **`/notice` local echo** (`src/main.rs`) — Same local-echo treatment for
sent notices.
trailing field into hopcount and realname (per RFC 1459), displays
the here/away flag (`H`/`G`) from the flags field, marks self-entries
with `(you)`, and has an explicit `RPL_ENDOFWHO` (315) terminator
handler. This addresses the historical `/who <self>` crash.
- **`/me` local echo** (`src/main.rs`) — Actions are now echoed locally
in the active tab immediately, so the user sees their action even on
servers that don't echo own PRIVMSGs (bouncers, mock servers, etc.).
- **`/notice` local echo** (`src/main.rs`) — Same local-echo treatment
for sent notices.
### Phase 2: Input Rate Throttle + Channel Rotation Revert
@ -95,14 +201,14 @@ several requested features. The changes are organized into five phases.
the available width instead of truncating at the right margin. 5 unit
tests.
## New Commands
### New Commands
- `/url <url>` — open a URL in the OS default browser (xdg-open / open / start)
- `/video <url>` — launch a video URL in the OS default video player
- `/image <url>` — attempt inline image rendering; falls back to `/url` if
the terminal doesn't support inline images
## Configuration
### Configuration
The throttle limits are currently hardcoded in `src/core/throttle.rs`:
- `MAX_LINES_PER_SEND = 4` — max lines per single input submission
@ -111,14 +217,9 @@ The throttle limits are currently hardcoded in `src/core/throttle.rs`:
These will be exposed as config options in a future release (see TODO.md).
## Build & Test
---
The patched source builds cleanly with `cargo build` and passes all tests
with `cargo test`. Note: this sandbox does not have a Rust toolchain
installed, so the patches were verified by static review only — please run
`cargo build && cargo test` on your machine to confirm.
## What's NOT in This Patch (Deferred)
## What's NOT in These Patches (Deferred)
Per user direction, the following are deferred to follow-up sessions:

6424
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "nirc-rs"
version = "0.10.1"
version = "0.10.2"
edition = "2021"
description = "multi-protocol terminal chat client"
license = "GPL-3.0-or-later"
@ -8,7 +8,7 @@ authors = ["Jeremy Anderson <noreply@dcos.net>"]
repository = "https://git.dcos.net/dcosnet/nirc-rs"
homepage = "https://dcos.net"
readme = "README.md"
keywords = ["irc", "matrix", "discord", "p2p", "chat", "terminal", "tui"]
keywords = ["irc", "matrix", "discord", "chat", "terminal", "tui"]
categories = ["command-line-utilities", "network-programming", "cryptography"]
[dependencies]
@ -35,7 +35,10 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
sha2 = "0.10"
yamux = "0.13"
async-trait = "0.1"
libp2p = { version = "0.54", features = ["tcp", "tokio", "noise", "yamux", "gossipsub", "mdns", "identify", "ping", "request-response", "macros"] }
# libp2p was dropped in 0.10.2 when BitChat was removed. The Noise primitive
# in src/engine/crypto.rs uses x25519-dalek + aes-gcm directly (not libp2p),
# so ADC's encrypted C-C connections still work. If a future P2P protocol
# needs libp2p again, re-add: libp2p = { version = "0.54", features = ["tcp", "tokio", "noise", "yamux", "gossipsub", "mdns", "identify", "ping", "request-response", "macros"] }
tokio-util = { version = "0.7", features = ["io", "codec", "compat"] }
dashmap = "6"
toml = "0.8"

107
NOTICES.md Normal file
View File

@ -0,0 +1,107 @@
# NOTICES — nirc-rs
This file collects project-level notices that affect the legal or operational
status of nirc-rs features. It is intentionally separate from CHANGES.md
(which is a release-engineering log) so that legal/compliance readers can
find these notes quickly.
---
## BitChat protocol module — withdrawn in 0.10.2
**Status:** Removed.
**Affected versions:** All releases up to and including 0.10.1 contained a
`src/protocols/bitchat.rs` module. It was removed in 0.10.2.
**Date of removal:** 2026-07.
### What was removed
- `src/protocols/bitchat.rs` — the entire protocol backend (libp2p-based
P2P chat with mDNS discovery, gossipsub, noise handshake).
- `ProtocolType::BitChat` enum variant and all of its match arms in
`tag()`, `label()`, `badge()`, `naim_color()`, dispatcher routing,
command parsing, TUI color tables, history-tag mapping, log sanitiser,
and the F1 menubar "Connect → BitChat…" entry.
- `Command::BitChatPeers`, `BitChatDm`, `BitChatSendFile` variants and
their `/bitchat …` parser arms.
- `ProtocolCommand::BitChat(BitChatCommand)` dispatcher variant.
- The `connect_bitchat()` method on `Dispatcher`.
- The `libp2p` dependency in `Cargo.toml`. libp2p was only consumed by
`bitchat.rs`; no other module imports it. Dropping it significantly
reduces the dependency tree and compile time. The Noise primitive in
`src/engine/crypto.rs` uses `x25519-dalek` + `aes-gcm` directly (not
libp2p), so ADC's encrypted client-client connections still work.
### Why it was removed
The BitChat project — Jack Dorsey's recently-announced P2P messaging
protocol — is the subject of active litigation in the Indian courts. The
legal status of the protocol specification, the reference implementation,
and downstream reimplementations such as the one previously shipped in
nirc-rs is currently unclear. Specifically:
- Court filings have not yet established whether the BitChat protocol
itself is encumbered by intellectual-property claims, or whether only
the reference client is affected.
- There is live ambiguity about whether shipping a third-party
implementation of the protocol exposes downstream distributors to
secondary liability.
- The reference implementation's license terms have changed during the
dispute, and it is not yet clear which license applies to derivative
works that were created before the change.
Pending clarity on these points, we have removed the BitChat module from
nirc-rs entirely. The risk of shipping an encumbered protocol backend
outweighs the benefit of offering P2P chat that users can obtain from
other sources in the meantime.
### What this means for users
- **Existing configs:** Any `[[servers]]` entry in `~/.nirc/config.toml`
with `protocol = "bitchat"` will be silently ignored at load time.
You do not need to edit your config file, but you may remove the
entries to clean it up.
- **Existing history files:** Per-tab scrollback files in
`~/.nirc/history/` that were tagged `P2P:<name>` (the BitChat tab-key
prefix) will fail to match any known protocol on load and will be
silently assigned to the Status tab. The files themselves are not
deleted; you can read them with a text editor if you need to recover
the content.
- **Existing tabs:** If you were connected to a BitChat network when you
last exited nirc-rs 0.10.1 or earlier, the tab will not reconnect on
startup. It will appear as an inert entry in the winlist until you
`/close` it.
- **No data loss:** Your IRC / Matrix / ADC / Discord / Stout / Spacebar
/ Nerimity history, configs, and credentials are unaffected.
### Re-evaluation criteria
We will re-evaluate restoring the BitChat module when **all** of the
following are true:
1. The India courts case has reached a final judgement (not an interim
order), OR the parties have publicly settled and the settlement terms
are clear about downstream-implementation rights.
2. The BitChat reference implementation's license is unambiguous and
permits third-party reimplementations.
3. There is at least one independent legal opinion (not from a party to
the litigation) concluding that shipping a clean-room reimplementation
of the protocol is safe.
Until then, the module will remain withdrawn. If you want P2P chat in
nirc-rs in the meantime, the ADC protocol's client-client connections
offer a similar feature set (direct file transfer, direct messages) over
a hub-and-spoke topology rather than full mesh P2P.
### Acknowledgement
We acknowledge the work of the original BitChat team and the broader P2P
messaging community. The decision to withdraw the module is purely a
legal-risk mitigation, not a commentary on the technical merits of the
protocol or the people who built it.
---
## Future notices
New entries will be appended below this line as needed.

View File

@ -6,11 +6,11 @@ A multi-protocol terminal chat client written in Rust.
Design Philosophy
nirc-rs is built on a single conviction: your communications deserve a client that treats the terminal as a first-class interface, not an afterthought. Every design decision — from the naim-derived 8-color palette system to the yamux-multiplexed file transfer pipeline — is deliberate. The client consolidates eight chat protocols into one unified, keyboard-driven interface with zero browser dependencies, zero Electron overhead, and zero JavaScript runtimes.
nirc-rs is built on a single conviction: your communications deserve a client that treats the terminal as a first-class interface, not an afterthought. Every design decision — from the naim-derived 8-color palette system to the yamux-multiplexed file transfer pipeline — is deliberate. The client consolidates seven chat protocols (down from eight — BitChat was withdrawn in 0.10.2; see NOTICES.md) into one unified, keyboard-driven interface with zero browser dependencies, zero Electron overhead, and zero JavaScript runtimes.
The architecture follows a layered design: protocol adapters at the bottom, an asynchronous event dispatcher in the middle, and a ratatui-based presentation layer on top. This separation means adding a new protocol requires implementing a single adapter trait — the rest of the system (tabs, logging, theming, file transfers, the menu bar) adapts automatically.
**Version:** 0.9.0
**Version:** 0.10.2
**License:** GPL-3.0-or-later
**Author:** Jeremy Anderson — dcos.net
**Repository:** https://git.dcos.net/dcosnet/nirc-rs
@ -38,16 +38,16 @@ nirc-rs implements protocol adapters as discrete, isolated modules. Each adapter
| Protocol | Status | Transport | Adapter Details |
|----------|--------|-----------|-----------------|
| **IRC** | Production | TLS (6697) / plaintext | SASL PLAIN authentication, CTCP auto-response (VERSION), ISUPPORT capability negotiation, full operator command set (`/oper`, `/kill`, `/kline`, `/wallops`), channel mode management, `/raw` for arbitrary protocol lines |
| **IRC** | Production | TLS (6697) / plaintext | SASL PLAIN authentication, CTCP auto-response (VERSION), ISUPPORT capability negotiation, full operator command set (`/oper`, `/kill`, `/kline`, `/wallops`), channel mode management, `/raw` for arbitrary protocol lines. 0.10.1: CTCP spec compliance (PRIVMSG not NOTICE), `/away` state tracking with 305/306, IRCv3 away-notify handler, `/who` hardening (352 hopcount/realname split + 315), `/me` & `/notice` local echo. |
| **ADC/DC++** | Production | TLS / plaintext | HSUP→ISID→BINF handshake sequence, hub search via SCH, file transfers over yamux-multiplexed streams with in-flight SHA-256 verification, security pipeline (rate limiting, IP validation, SSRF prevention, path traversal blocking) |
| **Matrix** | Implemented | HTTPS (matrix-sdk 0.18) | Megolm E2EE with SQLite crypto store, dedicated OS thread for non-Send crypto types, room creation/invitation/reaction/reply, SAS device verification, session persistence via token storage |
| **Discord** | Implemented | WebSocket (wss) | Gateway event subscription, REST API integration, guild join/leave/members |
| **Stout** | Implemented | WebSocket (wss) | Revolt-compatible fork, REST + WebSocket client |
| **Spacebar** | Implemented | WebSocket (wss) | Revolt fork variant, independent REST + WebSocket adapter |
| **Nerimity** | Implemented | WebSocket (wss) | Custom platform with dedicated REST + WebSocket adapter |
| **BitChat** | Implemented | libp2p (TCP) | P2P messaging via noise protocol, mDNS peer discovery, gossipsub pub/sub, request-response file transfer |
| ~~**BitChat**~~ | **Withdrawn in 0.10.2** | ~~libp2p (TCP)~~ | Removed pending clarity on Jack Dorsey's BitChat project and the India courts situation. See [NOTICES.md](NOTICES.md) for the full rationale. |
Protocol-specific commands are namespaced under their protocol prefix (`/matrix …`, `/adc …`, `/discord …`, `/bitchat …`) so the command surface stays organized and composable regardless of how many protocols are active simultaneously.
Protocol-specific commands are namespaced under their protocol prefix (`/matrix …`, `/adc …`, `/discord …`) so the command surface stays organized and composable regardless of how many protocols are active simultaneously.
---
@ -61,7 +61,7 @@ Colors are managed through `NaimPalette`, a 15-field struct that maps directly t
### Window Management
Windows are first-class objects in the tab model. Each window carries its own message buffer, input line, cursor position, command history, scroll offset, and unread counter. The `TabTier` priority system (`Unread > Conversed > Inert`) orders windows for `Ctrl-N` navigation so you always land on the most relevant unread conversation first — not the next tab in insertion order.
Windows are first-class objects in the tab model. Each window carries its own message buffer, input line, cursor position, command history, scroll offset, and unread counter. `Ctrl-N` walks windows in **static insertion order** (as of 0.10.1 — the previous activity-tier priority system was reverted because users found the reordering non-deterministic and hard to build muscle memory around). Tabs that aren't cyclable (unjoined IRC channels, hidden server tabs) are skipped, but the relative order of the remaining tabs is preserved.
The window list (`F4` to cycle through Auto/Visible/Hidden) displays protocol badges, per-window unread indicators, and highlights the active window. It occupies a fixed-width column on the right side of the chat area, overlapping rather than displacing message content — the same spatial model that made naim's window list usable on 80-column terminals.
@ -296,7 +296,9 @@ nirc-rs provides a unified slash-command interface. Every command is available b
**Discord / Stout / Spacebar / Nerimity:** `/<protocol> join`, `/<protocol> leave`, `/<protocol> members`, `/<protocol> servers`
**BitChat:** `/bitchat peers`, `/bitchat dm`, `/bitchat send`
**Media (0.10.1):** `/url <url>`, `/video <url>`, `/image <url>` — open URLs externally, launch video in the OS default player, or attempt inline image rendering (falls back to `/url` if the terminal doesn't support inline images).
**BitChat:** *Removed in 0.10.2.* See [NOTICES.md](NOTICES.md) for the rationale.
### UI and Display
@ -323,7 +325,7 @@ nirc-rs provides a unified slash-command interface. Every command is available b
| `Left` / `Right` | Move cursor in input line |
| `Home` / `End` | Previous / next window |
| `Insert` | Scroll chat to bottom (release scroll lock) |
| `Ctrl-N` | Jump to next window with unread messages |
| `Ctrl-N` | Next window (insertion order, skips unjoined IRC channels) |
| `Ctrl-B` | Jump back to previously active window |
| `Ctrl-P` | Previous buffer |
| `Ctrl-A` | Next active buffer |
@ -370,7 +372,7 @@ auto_connect = ["libera"] # servers to connect on startup
# ─── Servers ────────────────────────────────────────────────────────────
[[servers]]
name = "libera"
protocol = "irc" # irc | matrix | adc | discord | stout | spacebar | nerimity | bitchat
protocol = "irc" # irc | matrix | adc | discord | stout | spacebar | nerimity (bitchat removed in 0.10.2)
address = "irc.libera.chat:6697"
tls = true
auto_join = ["#rust", "#nirc"]
@ -431,16 +433,22 @@ device_name = "nirc-rs"
# access_token = "syt_abc..." # for session resume without password
```
### BitChat P2P Configuration
### BitChat P2P Configuration — REMOVED in 0.10.2
BitChat server entries are no longer accepted. Any `[[servers]]` entry with
`protocol = "bitchat"` will be silently ignored at load time. See
[NOTICES.md](NOTICES.md) for the full rationale. If you previously had a
BitChat entry in your config, you can leave it (it will be ignored) or
remove it to clean up:
```toml
[[servers]]
name = "bitchat"
protocol = "bitchat"
address = "/ip4/0.0.0.0/tcp/9394"
[servers.extra]
bootstrap = "/ip4/1.2.3.4/tcp/9394/p2p/QmSomePeerId"
# REMOVE THIS — no longer used:
# [[servers]]
# name = "bitchat"
# protocol = "bitchat"
# address = "/ip4/0.0.0.0/tcp/9394"
# [servers.extra]
# bootstrap = "/ip4/1.2.3.4/tcp/9394/p2p/QmSomePeerId"
```
---

View File

@ -1,6 +1,6 @@
# nirc-rs 0.10.1 — Status Report
# nirc-rs 0.10.2 — Status Report
**Version:** 0.10.1
**Version:** 0.10.2
**Release date:** 2026-07
**Codename:** nirc-rs
**License:** GPL-3.0-or-later
@ -20,7 +20,48 @@
| **Stout** | ✅ Complete | ❌ Untested | REST + WebSocket (Revolt fork) |
| **Spacebar** | ✅ Complete | ❌ Untested | REST + WebSocket (Revolt fork) |
| **Nerimity** | ✅ Complete | ❌ Untested | REST + WebSocket (custom platform) |
| **BitChat** | ✅ Complete | ❌ Untested | libp2p P2P, mDNS, gossipsub, noise |
| ~~**BitChat**~~ | ❌ **Withdrawn** | N/A | Removed in 0.10.2 — see [NOTICES.md](NOTICES.md) for the rationale (Jack Dorsey BitChat / India courts legal uncertainty) |
---
## What's New in 0.10.2
### BitChat Protocol Module Withdrawn
The BitChat protocol module (`src/protocols/bitchat.rs`) and all of its
integrations have been removed from the codebase. The `libp2p` dependency
in `Cargo.toml` was dropped at the same time — BitChat was the only
consumer. The Noise primitive in `src/engine/crypto.rs` uses
`x25519-dalek` + `aes-gcm` directly (not libp2p), so ADC's encrypted
client-client connections are unaffected.
**Why:** The BitChat project (Jack Dorsey's recently-announced P2P
messaging protocol) is the subject of active litigation in the Indian
courts. The legal status of the protocol specification, the reference
implementation, and downstream reimplementations is currently unclear.
Pending clarity, we have removed the module entirely rather than ship an
encumbered protocol backend. See [NOTICES.md](NOTICES.md) for the full
rationale and re-evaluation criteria.
**User impact:** Existing config entries with `protocol = "bitchat"` are
silently ignored. Old history files tagged `P2P:<name>` are silently
reassigned to the Status tab. No data is lost; IRC / Matrix / ADC /
Discord / Stout / Spacebar / Nerimity configs and history are unaffected.
**Removed code surface:**
- `src/protocols/bitchat.rs` (deleted)
- `ProtocolType::BitChat` enum variant + all match arms
- `Command::BitChatPeers`, `BitChatDm`, `BitChatSendFile` variants
- `ProtocolCommand::BitChat(BitChatCommand)` dispatcher variant
- `Dispatcher::connect_bitchat()` method
- F1 menu "Connect → BitChat…" entry
- `libp2p` Cargo dependency (only consumer was bitchat.rs)
### Help text updated
The `/help` output now mentions the new 0.10.1 media commands (`/url`,
`/video`, `/image`) and explicitly notes the BitChat removal with a
pointer to NOTICES.md.
---

View File

@ -16,11 +16,11 @@ _None at this time._
- [ ] **Test Discord protocol against a live server** — The Discord implementation uses Gateway WebSocket and REST API but has not been tested. Need to verify connection, event handling, and message send/receive.
- [ ] **Test remaining protocols** — Stout, Spacebar, Nerimity, and BitChat are all fully implemented but untested. Each needs a live server/peer to verify:
- [ ] **Test remaining protocols** — Stout, Spacebar, Nerimity are all fully implemented but untested. Each needs a live server/peer to verify:
- [ ] Stout (Revolt-compatible fork)
- [ ] Spacebar (Revolt fork)
- [ ] Nerimity (custom platform)
- [ ] BitChat (P2P, libp2p, mDNS discovery)
(BitChat was on this list but was withdrawn in 0.10.2 — see NOTICES.md.)
- [ ] **Inline image rendering (real implementation)** — The `try_render_inline_image()` function in `src/tui/media.rs` is currently a stub returning `Unsupported`. The real implementation needs to:
- Fetch image bytes via reqwest (with a 4 MiB size cap)
@ -70,6 +70,7 @@ _None at this time._
## Completed
- [x] **0.10.2: BitChat protocol module withdrawn** — Removed `src/protocols/bitchat.rs` and all integrations pending clarity on Jack Dorsey's BitChat project and the India courts situation. Dropped `libp2p` dependency. See NOTICES.md for the full rationale and re-evaluation criteria.
- [x] **0.10.1: CTCP spec compliance** — Outgoing CTCP requests now use PRIVMSG (not NOTICE) per the IRCv3 CTCP spec.
- [x] **0.10.1: Self-targeted CTCP visible**`/ctcp mynick VERSION` now displays the request in the relevant tab instead of being silently swallowed.
- [x] **0.10.1: /away tracking**`is_away` / `away_message` tracked in ConnState; RPL_NOWAWAY (306) / RPL_UNAWAY (305) explicitly handled.

View File

@ -15,7 +15,8 @@ _nirc() {
jump jumpback close open winlist
set get alias unalias bind unbind eval source
echo clear clearall save help quit newconn server
matrix adc dc 'dc++' revolt stoat bitchat p2p
matrix adc dc 'dc++' revolt stoat discord spacebar nerimity
url video image
)
# Matrix subcommands
@ -27,14 +28,11 @@ _nirc() {
# ADC subcommands
local adc_cmds=(search users broadcast get download dl)
# Revolt subcommands
local revolt_cmds=(join leave members)
# Revolt / Discord / Stout / Spacebar / Nerimity subcommands (same shape)
local revolt_cmds=(join leave members servers)
# BitChat subcommands
local bitchat_cmds=(peers dm msg send sendfile list)
# Protocols for /connect
local protocols=(irc matrix adc dc 'dc++' bitchat revolt stoat)
# Protocols for /connect (bitchat removed in 0.10.2 — see NOTICES.md)
local protocols=(irc matrix adc dc 'dc++' discord stout spacebar nerimity revolt)
case ${prev} in
connect)
@ -49,14 +47,10 @@ _nirc() {
COMPREPLY=($(compgen -W "${adc_cmds[*]}" -- "${cur}"))
return
;;
revolt|stoat)
revolt|stoat|discord|spacebar|nerimity)
COMPREPLY=($(compgen -W "${revolt_cmds[*]}" -- "${cur}"))
return
;;
bitchat|p2p)
COMPREPLY=($(compgen -W "${bitchat_cmds[*]}" -- "${cur}"))
return
;;
'matrix verify')
# Complete with nothing special — user provides a user_id
return
@ -67,9 +61,6 @@ _nirc() {
'matrix reply')
return
;;
'bitchat dm'|'bitchat send'|'bitchat sendfile'|'p2p dm'|'p2p send')
return
;;
esac
# Default: offer all top-level commands
@ -79,7 +70,7 @@ _nirc() {
# Also complete file paths for certain commands
case ${words[1]} in
sendfile|acceptfile|source|'bitchat send'|'p2p send'|'bitchat sendfile'|'adc get'|'adc download'|'adc dl')
sendfile|acceptfile|source|'adc get'|'adc download'|'adc dl')
_filedir
return
;;

View File

@ -1,4 +1,4 @@
# nirc-rs fish completion for 0.5.0
# nirc-rs fish completion for 0.10.2
# Disable file completions unless we explicitly want them
complete -c nirc -f
@ -15,12 +15,15 @@ complete -c nirc -k -x -a disconnect -d 'Disconnect from protocol'
complete -c nirc -k -x -a newconn -d 'New connection dialog'
complete -c nirc -k -x -a server -d 'Switch servers'
# After /connect, offer protocols
# After /connect, offer protocols (bitchat removed in 0.10.2 — see NOTICES.md)
complete -c nirc -k -x -a '/connect irc' -d 'Connect via IRC'
complete -c nirc -k -x -a '/connect matrix' -d 'Connect via Matrix'
complete -c nirc -k -x -a '/connect adc' -d 'Connect via ADC/DC++'
complete -c nirc -k -x -a '/connect dc' -d 'Connect via DC++'
complete -c nirc -k -x -a '/connect bitchat' -d 'Connect via BitChat P2P'
complete -c nirc -k -x -a '/connect discord' -d 'Connect via Discord'
complete -c nirc -k -x -a '/connect stout' -d 'Connect via Stout (Discord fork)'
complete -c nirc -k -x -a '/connect spacebar' -d 'Connect via Spacebar (Discord fork)'
complete -c nirc -k -x -a '/connect nerimity' -d 'Connect via Nerimity'
complete -c nirc -k -x -a '/connect revolt' -d 'Connect via Revolt'
# ═══ Messaging ═══
@ -114,14 +117,31 @@ complete -c nirc -k -x -a '/adc get' -d 'Download file'
complete -c nirc -k -x -a '/adc download' -d 'Download file'
complete -c nirc -k -x -a '/adc dl' -d 'Download file'
# ═══ Revolt ═══
# ═══ Revolt / Discord / Stout / Spacebar / Nerimity ═══
complete -c nirc -k -x -a '/revolt join' -d 'Join server'
complete -c nirc -k -x -a '/revolt leave' -d 'Leave server'
complete -c nirc -k -x -a '/revolt members' -d 'List members'
complete -c nirc -k -x -a '/revolt servers' -d 'List servers'
complete -c nirc -k -x -a '/discord join' -d 'Join server'
complete -c nirc -k -x -a '/discord leave' -d 'Leave server'
complete -c nirc -k -x -a '/discord members' -d 'List members'
complete -c nirc -k -x -a '/discord servers' -d 'List servers'
complete -c nirc -k -x -a '/stout join' -d 'Join server (Stout fork)'
complete -c nirc -k -x -a '/stout leave' -d 'Leave server (Stout fork)'
complete -c nirc -k -x -a '/stout members' -d 'List members (Stout fork)'
complete -c nirc -k -x -a '/stout servers' -d 'List servers (Stout fork)'
complete -c nirc -k -x -a '/spacebar join' -d 'Join server (Spacebar fork)'
complete -c nirc -k -x -a '/spacebar leave' -d 'Leave server (Spacebar fork)'
complete -c nirc -k -x -a '/spacebar members' -d 'List members (Spacebar fork)'
complete -c nirc -k -x -a '/spacebar servers' -d 'List servers (Spacebar fork)'
complete -c nirc -k -x -a '/nerimity join' -d 'Join server (Nerimity)'
complete -c nirc -k -x -a '/nerimity leave' -d 'Leave server (Nerimity)'
complete -c nirc -k -x -a '/nerimity members' -d 'List members (Nerimity)'
complete -c nirc -k -x -a '/nerimity servers' -d 'List servers (Nerimity)'
# ═══ BitChat P2P ═══
complete -c nirc -k -x -a '/bitchat peers' -d 'List P2P peers'
complete -c nirc -k -x -a '/p2p peers' -d 'List P2P peers'
complete -c nirc -k -x -a '/bitchat dm' -d 'Send DM'
complete -c nirc -k -x -a '/bitchat send' -d 'Send file via P2P'
complete -c nirc -k -x -a '/bitchat sendfile' -d 'Send file via P2P'
# ═══ Media (0.10.1) ═══
complete -c nirc -k -x -a '/url' -d 'Open URL in OS default browser'
complete -c nirc -k -x -a '/video' -d 'Launch video in OS default player'
complete -c nirc -k -x -a '/image' -d 'Attempt inline image render (falls back to /url)'
# BitChat P2P support was removed in 0.10.2. See NOTICES.md for the rationale.

View File

@ -1,7 +1,7 @@
#compdef nirc nirc-rs
# nirc-rs zsh completion for 0.5.0
# nirc-rs zsh completion for 0.10.2
local -a subcommands protocols matrix_cmds adc_cmds revolt_cmds bitchat_cmds
local -a subcommands protocols matrix_cmds adc_cmds revolt_cmds
subcommands=(
'connect:Connect to a server'
@ -64,16 +64,25 @@ subcommands=(
'dc:Alias for /adc'
'revolt:Revolt protocol commands'
'stoat:Alias for /revolt'
'bitchat:BitChat P2P commands'
'p2p:Alias for /bitchat'
'discord:Discord protocol commands'
'stout:Alias for /discord (Stout fork)'
'spacebar:Spacebar protocol commands'
'nerimity:Nerimity protocol commands'
'url:Open URL in OS default browser'
'video:Launch video in OS default player'
'image:Attempt inline image render'
)
# Protocols for /connect (bitchat removed in 0.10.2 — see NOTICES.md)
protocols=(
'irc:IRC protocol'
'matrix:Matrix protocol'
'adc:ADC/DC++ protocol'
'dc:Alias for adc'
'bitchat:BitChat P2P'
'discord:Discord protocol'
'stout:Stout (Discord-API fork)'
'spacebar:Spacebar (Discord-API fork)'
'nerimity:Nerimity (custom platform)'
'revolt:Revolt protocol'
'stoat:Alias for revolt'
)
@ -107,15 +116,7 @@ revolt_cmds=(
'join:Join server'
'leave:Leave server'
'members:List members'
)
bitchat_cmds=(
'peers:List P2P peers'
'dm:Send direct message'
'msg:Send direct message'
'send:Send file'
'sendfile:Send file'
'list:List P2P peers'
'servers:List servers'
)
_nirc_subcommand() {
@ -130,16 +131,13 @@ _nirc_subcommand() {
adc|dc)
_describe 'adc-command' adc_cmds
;;
revolt|stoat)
revolt|stoat|discord|spacebar|nerimity)
_describe 'revolt-command' revolt_cmds
;;
bitchat|p2p)
_describe 'bitchat-command' bitchat_cmds
;;
sendfile|source|acceptfile)
_files
;;
'adc get'|'adc download'|'adc dl'|'bitchat send'|'bitchat sendfile'|'p2p send')
'adc get'|'adc download'|'adc dl')
_files
;;
esac

View File

@ -1,9 +1,9 @@
.\" nirc-rs
.\" Copyright (C) 2025 Jeremy Anderson
.\" SPDX-License-Identifier: GPL-3.0-or-later
.TH NIRC 1 "2025-07-19" "nirc-rs 0.5.0" "User Commands"
.TH NIRC 1 "2026-07-29" "nirc-rs 0.10.2" "User Commands"
.SH NAME
nirc \- multi-protocol terminal chat client (IRC, Matrix, ADC/DC++, Revolt, BitChat P2P)
nirc \- multi-protocol terminal chat client (IRC, Matrix, ADC/DC++, Discord, Stout, Spacebar, Nerimity)
.SH SYNOPSIS
.B nirc
[\fIOPTIONS\fR]
@ -33,9 +33,14 @@ Revolt/Stoat (0.5.0+)
REST + JSON WebSocket client with email/password or bot-token auth,
server join/leave, member listing, session token persistence.
.TP
BitChat P2P (0.5.0+)
libp2p Gossipsub chat, mDNS local discovery, Identify remote discovery,
direct messages, P2P file transfer via request-response.
Discord / Stout / Spacebar / Nerimity (0.6.0+)
REST + WebSocket clients for Discord-API-compatible and custom platforms.
.PP
Note: BitChat P2P support was withdrawn in 0.10.2 pending clarity on
Jack Dorsey's BitChat project and the India courts situation.
See
.I NOTICES.md
for the full rationale.
.SH OPTIONS
.TP
.B \-h, \-\-help
@ -88,8 +93,12 @@ is one of
.BR matrix ,
.BR adc ,
.BR dc ,
.BR bitchat ,
.BR discord ,
.BR stout ,
.BR spacebar ,
.BR nerimity ,
.BR revolt .
(BitChat was removed in 0.10.2 — see NOTICES.md.)
.TP
.B /disconnect [protocol]
Disconnect from a specific protocol, or all.
@ -264,19 +273,13 @@ Leave a server.
.TP
.B /revolt members <server_id>
List server members.
.SS BitChat P2P
.TP
.B /bitchat peers
.B /p2p peers
List discovered P2P peers.
.TP
.B /bitchat dm <peer_id> <message>
.B /p2p msg <peer_id> <message>
Send a direct message.
.TP
.B /bitchat send <peer_id> <path>
.B /p2p send <peer_id> <path>
Send a file via P2P.
.SS BitChat P2P \(em Removed in 0.10.2
BitChat support was withdrawn pending clarity on Jack Dorsey's BitChat
project and the India courts situation. See
.I NOTICES.md
for the full rationale. Existing config entries with
.B protocol = "bitchat"
are silently ignored at load time.
.SS Window Management
.TP
.B /win [N]
@ -399,12 +402,9 @@ password = "hunter2"
.fi
.PP
.nf
[[servers]]
name = "bitchat"
protocol = "bitchat"
address = "/ip4/0.0.0.0/tcp/9394"
[servers.extra]
bootstrap = "/ip4/1.2.3.4/tcp/9394/p2p/QmPeerId"
# BitChat server entries are no longer accepted (removed in 0.10.2).
# Any [[servers]] block with protocol = "bitchat" is silently ignored.
# See NOTICES.md for the rationale.
.fi
.SH FILES
.TP

View File

@ -1,8 +1,8 @@
# Maintainer: Jeremy Anderson <noreply@dcos.net>
pkgname=nirc-rs
pkgver=0.8.1
pkgver=0.10.2
pkgrel=1
pkgdesc="multi-protocol terminal chat client (IRC, Matrix, ADC/DC++, Revolt, BitChat P2P)"
pkgdesc="multi-protocol terminal chat client (IRC, Matrix, ADC/DC++, Discord, Stout, Spacebar, Nerimity)"
arch=('x86_64' 'aarch64')
url="https://git.dcos.net/dcosnet/nirc-rs"
license=('GPL-3.0-or-later')

View File

@ -1,5 +1,5 @@
Package: nirc
Version: 0.8.1
Version: 0.10.2
Section: net
Priority: optional
Maintainer: Jeremy Anderson <noreply@dcos.net>
@ -13,14 +13,17 @@ License: GPL-3.0-or-later
Description: multi-protocol terminal chat client
nirc-rs is a terminal-based chat client built with ratatui. It supports
IRC (with TLS, SASL, IRCv3), Matrix (with E2EE), ADC/DC++ file sharing,
Revolt, and BitChat P2P (libp2p/Gossipsub).
Discord, Stout, Spacebar, and Nerimity. (BitChat P2P support was
removed in 0.10.2 — see NOTICES.md for the rationale.)
.
Features:
- Multi-protocol: IRC, Matrix, ADC/DC++, Revolt, BitChat P2P
- Multi-protocol: IRC, Matrix, ADC/DC++, Discord, Stout, Spacebar, Nerimity
- 4 built-in themes: default, solarized, gruvbox, dracula
- Encrypted identity vault (AES-256-GCM)
- SASL + TLS for IRC
- Megolm E2EE for Matrix
- DCC/ADC + P2P file transfers
- DCC/ADC file transfers with bandwidth monitor
- Dynamic plugin system (.so / .dylib)
- Full naim-style command set with aliases, key bindings, and scripting
- Full naim-style command set with aliases, key bindings, and scripting
- URL detection + /url /video /image media commands (0.10.1)
- Input rate throttle + line guard (0.10.1)

View File

@ -1,5 +1,5 @@
Name: nirc
Version: 0.8.1
Version: 0.10.2
Release: 1%{?dist}
Summary: multi-protocol terminal chat client
@ -13,8 +13,9 @@ BuildRequires: pkg-config
%description
nirc-rs is a terminal-based chat client built with ratatui supporting IRC
(with TLS, SASL, IRCv3), Matrix (with E2EE), ADC/DC++, Revolt, and BitChat
P2P (libp2p/Gossipsub).
(with TLS, SASL, IRCv3), Matrix (with E2EE), ADC/DC++, Discord, Stout,
Spacebar, and Nerimity. (BitChat P2P support was removed in 0.10.2 —
see NOTICES.md for the rationale.)
%prep
%autosetup
@ -43,6 +44,13 @@ cargo test --release --locked
%{_datadir}/fish/vendor_completions.d/nirc.fish
%changelog
* Wed Jul 29 2026 Jeremy Anderson <noreply@dcos.net> - 0.10.2-1
- BitChat P2P module withdrawn (India courts legal uncertainty)
- libp2p dependency dropped (only consumer was bitchat)
- 0.10.1: IRC CTCP/away/who fixes, input throttle + line guard,
channel rotation reverted to insertion order, URL detection +
inline photo / external video framework, top-right bandwidth
monitor, line wrapping fix
* Sat Jul 19 2025 Jeremy Anderson <noreply@dcos.net> - 0.5.0-1
- Initial RPM packaging
- 0.5.0: BitChat P2P (libp2p), Revolt/Stoat protocol

View File

@ -72,19 +72,29 @@ address = "https://discord.com/api"
bot_token = "BOT_TOKEN_HERE"
```
## Connect to BitChat (P2P)
## BitChat (P2P) — Removed in 0.10.2
BitChat support was withdrawn in 0.10.2 pending clarity on Jack Dorsey's
BitChat project and the India courts situation. See
[NOTICES.md](NOTICES.md) for the full rationale. Existing config entries
with `protocol = "bitchat"` are silently ignored at load time — you can
leave them in place or remove them to clean up:
```toml
[[servers]]
name = "p2p"
protocol = "bitchat"
address = "/ip4/0.0.0.0/tcp/9394"
[servers.extra]
nickname = "handle"
# Optional: bootstrap to a known peer
# REMOVE THIS — no longer used:
# [[servers]]
# name = "p2p"
# protocol = "bitchat"
# address = "/ip4/0.0.0.0/tcp/9394"
# [servers.extra]
# nickname = "handle"
# bootstrap = "/ip4/1.2.3.4/tcp/9394/p2p/QmPeerId"
```
If you want P2P-style direct messaging in the meantime, ADC's
client-client connections offer direct file transfer and direct messages
over a hub-and-spoke topology.
## Key Bindings
| Key | Action |

View File

@ -30,21 +30,15 @@
//! Matrix-specific connection parameters are pulled from the `extra` map at
//! connect time by [`matrix_config_from_entry`].
//!
//! ## BitChat P2P server entries (0.5.0)
//! ## BitChat P2P server entries — REMOVED in 0.10.2
//!
//! BitChat servers are configured as `[[servers]]` entries with `protocol = "bitchat"`.
//! The `address` field is the listen multiaddr (e.g. `/ip4/0.0.0.0/tcp/9394`).
//! Optional bootstrap node in `[servers.extra]`:
//!
//! ```toml
//! [[servers]]
//! name = "bitchat"
//! protocol = "bitchat"
//! address = "/ip4/0.0.0.0/tcp/9394"
//!
//! [servers.extra]
//! bootstrap = "/ip4/1.2.3.4/tcp/9394/p2p/QmSomePeerId"
//! ```
//! BitChat server entries are no longer accepted. Any `[[servers]]` entry
//! with `protocol = "bitchat"` will be silently ignored at load time. The
//! module was removed pending clarity on Jack Dorsey's BitChat project and
//! the India courts situation — see NOTICES.md for the full rationale.
//! Existing config files do not need to be edited (the unknown protocol
//! value is treated as a parse-time skip rather than an error), but users
//! may remove the entries to clean up their config.
use crate::core::protocol::ProtocolType;
use crate::tui::foundation::Theme;

View File

@ -747,7 +747,7 @@ mod tests {
let mut app = app_with_tabs(&[
(ProtocolType::Irc, "#irc", false),
(ProtocolType::Matrix, "#mtx", false),
(ProtocolType::BitChat, "#p2p", false),
(ProtocolType::Adc, "#adc", false),
(ProtocolType::Discord, "#dsc", false),
]);
// Converse in all of them so none are inert.
@ -762,7 +762,7 @@ mod tests {
let next = app.next_tab_by_priority(0);
assert_eq!(next, 1, "Ctrl-N from IRC lands on Matrix (insertion order)");
let proto = app.tab_at(next).unwrap().protocol;
assert!(matches!(proto, ProtocolType::Matrix | ProtocolType::BitChat | ProtocolType::Discord),
assert!(matches!(proto, ProtocolType::Matrix | ProtocolType::Adc | ProtocolType::Discord),
"Ctrl-N from IRC must land on a different protocol's tab; got {:?}", proto);
}

View File

@ -226,13 +226,9 @@ pub enum Command {
NerimityMembers { server_id: String },
NerimityServers,
// ─── Phase F — BitChat (P2P) commands ────────────────────
/// `/bitchat peers` — list discovered P2P peers.
BitChatPeers,
/// `/bitchat dm <peer_id> <message>` — send a direct message.
BitChatDm { peer_id: String, body: String },
/// `/bitchat send <peer_id> <path>` — send a file via P2P.
BitChatSendFile { peer_id: String, path: String },
// NOTE: BitChat (P2P) commands were removed in 0.10.2 pending clarity
// on Jack Dorsey's BitChat project and the India courts situation.
// See NOTICES.md for the full rationale.
// ─── N-3.1: Plugin management commands ────────────────────────────
/// `/plugins` — list loaded plugins.
@ -370,7 +366,6 @@ pub fn parse_command(input: &str) -> Option<Command> {
"matrix" | "mtx" => ProtocolType::Matrix,
"discord" | "dc" => ProtocolType::Discord,
"adc" => ProtocolType::Adc,
"bitchat" | "bc" | "p2p" => ProtocolType::BitChat,
"stout" => ProtocolType::Stout,
"spacebar" | "sb" => ProtocolType::Spacebar,
"nerimity" | "nm" => ProtocolType::Nerimity,
@ -627,24 +622,8 @@ pub fn parse_command(input: &str) -> Option<Command> {
}
}
// ─── BitChat (P2P) commands ────────────────────────
"bitchat" | "p2p" => {
let sub = args.first().map(|s| s.to_lowercase());
match sub.as_deref() {
Some("peers") | Some("list") => Some(Command::BitChatPeers),
Some("dm") | Some("msg") => {
let peer_id = args.get(1)?.clone();
let body = if args.len() > 2 { args[2..].join(" ") } else { None? };
Some(Command::BitChatDm { peer_id, body })
}
Some("send") | Some("sendfile") | Some("file") => {
let peer_id = args.get(1)?.clone();
let path = args.get(2)?.clone();
Some(Command::BitChatSendFile { peer_id, path })
}
_ => None,
}
}
// NOTE: `/bitchat` and `/p2p` commands removed in 0.10.2.
// See NOTICES.md for the rationale.
// ─── N-3.1: Plugin management commands ────────────────────────
"plugins" | "plugin-list" => Some(Command::PluginList),
@ -660,7 +639,10 @@ pub fn parse_command(input: &str) -> Option<Command> {
// All three accept a single URL argument. The URL is taken as the
// full remainder of the input (so spaces in URLs are preserved,
// though URLs shouldn't contain unencoded spaces anyway).
"url" | "open" | "browse" => Some(Command::Url { target: args.join(" ") }),
// NOTE: "open" is intentionally NOT an alias here — it's already
// bound to Command::Open (the /query / /open PM-window command)
// at the earlier match arm. Use "url" or "browse" instead.
"url" | "browse" => Some(Command::Url { target: args.join(" ") }),
"video" | "play" => Some(Command::Video { target: args.join(" ") }),
"image" | "img" | "photo" => Some(Command::Image { target: args.join(" ") }),
@ -707,7 +689,6 @@ impl FromStr for ProtocolType {
"irc" => Ok(ProtocolType::Irc),
"matrix" => Ok(ProtocolType::Matrix),
"adc" | "dc" | "dc++" => Ok(ProtocolType::Adc),
"bitchat" => Ok(ProtocolType::BitChat),
"discord" => Ok(ProtocolType::Discord),
"stout" => Ok(ProtocolType::Stout),
"spacebar" => Ok(ProtocolType::Spacebar),

View File

@ -96,7 +96,8 @@ fn protocol_from_tag(tag: &str) -> Option<ProtocolType> {
"IRC" => Some(ProtocolType::Irc),
"Mtx" => Some(ProtocolType::Matrix),
"ADC" => Some(ProtocolType::Adc),
"P2P" => Some(ProtocolType::BitChat),
// "P2P" (BitChat) removed in 0.10.2 — old history files with this
// tag will be silently assigned to the Status tab.
"Dsc" => Some(ProtocolType::Discord),
"Sto" => Some(ProtocolType::Stout),
"Spc" => Some(ProtocolType::Spacebar),

View File

@ -3,13 +3,17 @@
use serde::{Deserialize, Serialize};
use std::fmt;
/// Supported chat protocols (Tox removed per design decision).
/// Supported chat protocols.
///
/// BitChat was removed in 0.10.2 pending clarity on Jack Dorsey's BitChat
/// project and the India courts situation. See NOTICES.md for the full
/// rationale. The variant will be restored if/when the legal status of the
/// protocol and its reference implementation is clarified.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ProtocolType {
Irc,
Matrix,
Adc,
BitChat,
Discord,
Stout,
Spacebar,
@ -29,7 +33,6 @@ impl ProtocolType {
ProtocolType::Irc => "IRC",
ProtocolType::Matrix => "Mtx",
ProtocolType::Adc => "ADC",
ProtocolType::BitChat => "P2P",
ProtocolType::Discord => "Dsc",
ProtocolType::Stout => "Sto",
ProtocolType::Spacebar => "Spc",
@ -43,7 +46,6 @@ impl ProtocolType {
ProtocolType::Irc => "IRC",
ProtocolType::Matrix => "Matrix",
ProtocolType::Adc => "ADC/DC++",
ProtocolType::BitChat => "BitChat",
ProtocolType::Discord => "Discord",
ProtocolType::Stout => "Stout",
ProtocolType::Spacebar => "Spacebar",
@ -56,13 +58,11 @@ impl ProtocolType {
/// - IRC channels start with `#`
/// - Matrix room IDs/aliases use `:` as the homeserver separator
/// - ADC hubs are commonly referenced as `+hub`
/// - BitChat is peer-to-peer (`~` home / personal node)
pub fn badge(self) -> &'static str {
match self {
ProtocolType::Irc => "#",
ProtocolType::Matrix => ":",
ProtocolType::Adc => "+",
ProtocolType::BitChat => "~",
ProtocolType::Discord => "D",
ProtocolType::Stout => "St",
ProtocolType::Spacebar => "S",
@ -71,13 +71,12 @@ impl ProtocolType {
}
/// 8-color NaimColor for protocol indicator (matches NaimPalette categories).
/// IRC = cyan (existing), Matrix = magenta, ADC = blue, BitChat = green.
/// IRC = cyan, Matrix = magenta, ADC = blue.
pub fn naim_color(self) -> crate::tui::foundation::NaimColor {
match self {
ProtocolType::Irc => crate::tui::foundation::NaimColor::Cyan,
ProtocolType::Matrix => crate::tui::foundation::NaimColor::Magenta,
ProtocolType::Adc => crate::tui::foundation::NaimColor::Blue,
ProtocolType::BitChat => crate::tui::foundation::NaimColor::Green,
ProtocolType::Discord => crate::tui::foundation::NaimColor::White,
ProtocolType::Stout => crate::tui::foundation::NaimColor::Yellow,
ProtocolType::Spacebar => crate::tui::foundation::NaimColor::Red,
@ -131,7 +130,6 @@ mod tests {
assert_eq!(ProtocolType::Irc.tag(), "IRC");
assert_eq!(ProtocolType::Matrix.tag(), "Mtx");
assert_eq!(ProtocolType::Adc.tag(), "ADC");
assert_eq!(ProtocolType::BitChat.tag(), "P2P");
assert_eq!(ProtocolType::Discord.tag(), "Dsc");
assert_eq!(ProtocolType::Stout.tag(), "Sto");
assert_eq!(ProtocolType::Spacebar.tag(), "Spc");
@ -143,7 +141,6 @@ mod tests {
assert_eq!(ProtocolType::Irc.badge(), "#");
assert_eq!(ProtocolType::Matrix.badge(), ":");
assert_eq!(ProtocolType::Adc.badge(), "+");
assert_eq!(ProtocolType::BitChat.badge(), "~");
assert_eq!(ProtocolType::Discord.badge(), "D");
assert_eq!(ProtocolType::Stout.badge(), "St");
assert_eq!(ProtocolType::Spacebar.badge(), "S");
@ -156,7 +153,6 @@ mod tests {
assert_eq!(ProtocolType::Irc.naim_color(), NaimColor::Cyan);
assert_eq!(ProtocolType::Matrix.naim_color(), NaimColor::Magenta);
assert_eq!(ProtocolType::Adc.naim_color(), NaimColor::Blue);
assert_eq!(ProtocolType::BitChat.naim_color(), NaimColor::Green);
assert_eq!(ProtocolType::Discord.naim_color(), NaimColor::White);
assert_eq!(ProtocolType::Stout.naim_color(), NaimColor::Yellow);
assert_eq!(ProtocolType::Spacebar.naim_color(), NaimColor::Red);
@ -168,7 +164,6 @@ mod tests {
assert_eq!(ProtocolType::Irc.label(), "IRC");
assert_eq!(ProtocolType::Matrix.label(), "Matrix");
assert_eq!(format!("{}", ProtocolType::Adc), "ADC/DC++");
assert_eq!(format!("{}", ProtocolType::BitChat), "BitChat");
assert_eq!(format!("{}", ProtocolType::Discord), "Discord");
assert_eq!(format!("{}", ProtocolType::Stout), "Stout");
assert_eq!(format!("{}", ProtocolType::Spacebar), "Spacebar");

View File

@ -1,12 +1,14 @@
//! Encrypted P2P tunnel layer — Phase 16.
//! Encrypted tunnel layer — Phase 16.
//!
//! Wraps any async Read+Write stream (yamux, TCP) with the Noise Protocol
//! Framework (Noise_XX pattern) for forward-secret, authenticated encryption.
//! The libp2p `noise` crate handles the handshake; we wrap the resulting
//! encrypted stream for use by the transfer engine and protocol backends.
//!
//! In production, this is automatically provided by libp2p's transport layer
//! for BitChat. This module exposes the building blocks for:
//! As of 0.10.2, this module is used by ADC's encrypted client-client (C-C)
//! connections. (It was previously also used by BitChat P2P, which was
//! removed in 0.10.2 — see NOTICES.md.) The module exposes the building
//! blocks for:
//! - Manual encrypted tunnels to non-libp2p peers
//! - End-to-end encrypted yamux substreams
//! - Keypair generation and fingerprinting

View File

@ -4,7 +4,7 @@ use crate::config::ServerEntry;
use crate::core::command::Command;
use crate::core::message::ChatMessage;
use crate::core::protocol::ProtocolType;
use crate::protocols::{adc::{self, AdcCommand, AdcConfig}, bitchat::{self, BitChatCommand, BitChatConfig}, discord::{self, DiscordCommand, DiscordConfig}, irc::{self, IrcCommand, IrcConfig}, matrix::{self, MatrixCommand, MatrixConfig}, stout::{self, StoutCommand, StoutConfig}, spacebar::{self, SpacebarCommand, SpacebarConfig}, nerimity::{self, NerimityCommand, NerimityConfig}};
use crate::protocols::{adc::{self, AdcCommand, AdcConfig}, discord::{self, DiscordCommand, DiscordConfig}, irc::{self, IrcCommand, IrcConfig}, matrix::{self, MatrixCommand, MatrixConfig}, stout::{self, StoutCommand, StoutConfig}, spacebar::{self, SpacebarCommand, SpacebarConfig}, nerimity::{self, NerimityCommand, NerimityConfig}};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@ -18,7 +18,6 @@ pub enum ProtocolCommand {
Irc(IrcCommand),
Adc(AdcCommand),
Matrix(MatrixCommand),
BitChat(BitChatCommand),
Discord(DiscordCommand),
Stout(StoutCommand),
Spacebar(SpacebarCommand),
@ -121,7 +120,6 @@ impl Dispatcher {
ProtocolType::Irc => self.connect_irc(server).await,
ProtocolType::Adc => self.connect_adc(server).await,
ProtocolType::Matrix => self.connect_matrix(server).await,
ProtocolType::BitChat => self.connect_bitchat(server).await,
ProtocolType::Discord => self.connect_discord(server).await,
ProtocolType::Stout => self.connect_stout(server).await,
ProtocolType::Spacebar => self.connect_spacebar(server).await,
@ -338,36 +336,7 @@ impl Dispatcher {
Ok(())
}
async fn connect_bitchat(&mut self, server: &str) -> anyhow::Result<()> {
let (proto_tx, mut proto_rx) = mpsc::channel::<ProtocolCommand>(64);
let (bc_cmd_tx, bc_cmd_rx) = mpsc::channel::<BitChatCommand>(64);
tokio::spawn(async move {
while let Some(ProtocolCommand::BitChat(cmd)) = proto_rx.recv().await {
if bc_cmd_tx.send(cmd).await.is_err() { break; }
}
});
// Look up ServerEntry for BitChat-specific config (bootstrap node).
let entry = self.server_entries.iter()
.find(|e| e.name == server || e.address == server).cloned();
let listen_addr = entry.as_ref()
.map(|e| e.address.clone())
.unwrap_or_else(|| server.to_owned());
let bootstrap = entry.as_ref().and_then(|e| e.extra.get("bootstrap").cloned());
let config = BitChatConfig {
listen_addr,
nickname: self.nickname.clone(),
bootstrap,
tx: self.msg_tx.clone(),
};
let addr_owned = server.to_owned();
tokio::spawn(async move { match bitchat::run_bitchat(config, bc_cmd_rx).await { Ok(()) => info!(%addr_owned, "BitChat stopped"), Err(e) => error!(%addr_owned, %e, "BitChat error") } });
self.protocol_channels.insert(ProtocolType::BitChat, proto_tx.clone());
self.handles.push(ProtocolHandle { protocol: ProtocolType::BitChat, server: server.to_owned(), cmd_tx: proto_tx });
let _ = self.event_tx.send(DispatcherEvent::ProtocolConnected { protocol: ProtocolType::BitChat, server: server.to_owned() }).await;
Ok(())
}
// NOTE: connect_bitchat() was removed in 0.10.2. See NOTICES.md.
/// Connect Stout (Discord-API-compatible self-hosted platform).
async fn connect_stout(&mut self, server: &str) -> anyhow::Result<()> {
@ -550,7 +519,6 @@ impl Dispatcher {
("IRC", ProtocolType::Irc),
("Mtx", ProtocolType::Matrix),
("ADC", ProtocolType::Adc),
("P2P", ProtocolType::BitChat),
("Dsc", ProtocolType::Discord),
("Sto", ProtocolType::Stout),
("Spc", ProtocolType::Spacebar),
@ -579,7 +547,6 @@ impl Dispatcher {
ProtocolType::Irc => ProtocolCommand::Irc(IrcCommand::Msg { target: target.clone(), body: body.clone() }),
ProtocolType::Matrix => ProtocolCommand::Matrix(MatrixCommand::Msg { room_id: target.clone(), body: body.clone() }),
ProtocolType::Adc => ProtocolCommand::Adc(AdcCommand::Msg { target_sid: target.clone(), body: body.clone() }),
ProtocolType::BitChat => ProtocolCommand::BitChat(BitChatCommand::Chat { body: body.clone() }),
ProtocolType::Discord => ProtocolCommand::Discord(DiscordCommand::Msg { channel_id: target.clone(), body: body.clone() }),
ProtocolType::Stout => ProtocolCommand::Stout(StoutCommand::Msg { channel_id: target.clone(), body: body.clone() }),
ProtocolType::Spacebar => ProtocolCommand::Spacebar(SpacebarCommand::Msg { channel_id: target.clone(), body: body.clone() }),
@ -599,7 +566,6 @@ impl Dispatcher {
ProtocolType::Spacebar => ProtocolCommand::Spacebar(SpacebarCommand::Emote { channel_id: target.clone(), body }),
ProtocolType::Nerimity => ProtocolCommand::Nerimity(NerimityCommand::Emote { channel_id: target.clone(), body }),
ProtocolType::Adc => ProtocolCommand::Adc(AdcCommand::Msg { target_sid: target.clone(), body: format!("* {} {}", nickname, body) }),
ProtocolType::BitChat => ProtocolCommand::BitChat(BitChatCommand::Chat { body: format!("* {} {}", nickname, body) }),
}).await;
}
Command::Names { channel } => { self.send_irc(IrcCommand::Names(channel)).await; }
@ -720,7 +686,6 @@ impl Dispatcher {
ProtocolType::Irc => ProtocolCommand::Irc(IrcCommand::Msg { target: target.clone(), body: msg.clone() }),
ProtocolType::Matrix => ProtocolCommand::Matrix(MatrixCommand::Msg { room_id: target.clone(), body: msg.clone() }),
ProtocolType::Adc => ProtocolCommand::Adc(AdcCommand::Msg { target_sid: target.clone(), body: msg.clone() }),
ProtocolType::BitChat => ProtocolCommand::BitChat(BitChatCommand::Chat { body: msg.clone() }),
ProtocolType::Discord => ProtocolCommand::Discord(DiscordCommand::Msg { channel_id: target.clone(), body: msg.clone() }),
ProtocolType::Stout => ProtocolCommand::Stout(StoutCommand::Msg { channel_id: target.clone(), body: msg.clone() }),
ProtocolType::Spacebar => ProtocolCommand::Spacebar(SpacebarCommand::Msg { channel_id: target.clone(), body: msg.clone() }),
@ -1001,18 +966,17 @@ impl Dispatcher {
self.send_to_protocol(ProtocolType::Nerimity,
|_| ProtocolCommand::Nerimity(NerimityCommand::ListServers)).await;
}
// ─── BitChat (P2P) commands ─────────────────────
Command::BitChatPeers => {
self.send_to_protocol(ProtocolType::BitChat,
|_| ProtocolCommand::BitChat(BitChatCommand::ListPeers)).await;
}
Command::BitChatDm { peer_id, body } => {
self.send_to_protocol(ProtocolType::BitChat,
|_| ProtocolCommand::BitChat(BitChatCommand::Direct { peer_id, body })).await;
}
Command::BitChatSendFile { peer_id, path } => {
self.send_to_protocol(ProtocolType::BitChat,
|_| ProtocolCommand::BitChat(BitChatCommand::SendFile { peer_id, path })).await;
// NOTE: BitChat (P2P) command handlers removed in 0.10.2.
// See NOTICES.md for the rationale.
//
// Media commands (0.10.1): /url, /video, /image are handled
// entirely in main.rs's handle_user_command (they call
// tui::media::open_external locally and never touch the
// network). They should never reach the dispatcher, but the
// match must be exhaustive — explicit no-op arms document
// that intent and silence the compiler.
Command::Url { .. } | Command::Video { .. } | Command::Image { .. } => {
debug!("media command reached dispatcher — should have been handled locally");
}
}
}
@ -1090,7 +1054,6 @@ impl Dispatcher {
ProtocolType::Irc => ProtocolCommand::Irc(IrcCommand::Quit(None)),
ProtocolType::Adc => ProtocolCommand::Adc(AdcCommand::Quit),
ProtocolType::Matrix => ProtocolCommand::Matrix(MatrixCommand::Quit),
ProtocolType::BitChat => ProtocolCommand::BitChat(BitChatCommand::Quit),
ProtocolType::Discord => ProtocolCommand::Discord(DiscordCommand::Quit),
ProtocolType::Stout => ProtocolCommand::Stout(StoutCommand::Quit),
ProtocolType::Spacebar => ProtocolCommand::Spacebar(SpacebarCommand::Quit),

View File

@ -80,7 +80,6 @@ fn sanitize_server(proto: ProtocolType, server: &str) -> String {
ProtocolType::Irc => "irc",
ProtocolType::Matrix => "matrix",
ProtocolType::Adc => "adc",
ProtocolType::BitChat => "bitchat",
ProtocolType::Discord => "discord",
ProtocolType::Stout => "stout",
ProtocolType::Spacebar => "spacebar",
@ -337,7 +336,7 @@ mod tests {
"matrix_matrix.org"
);
assert_eq!(sanitize_server(ProtocolType::Adc, ""), "adc");
assert_eq!(sanitize_server(ProtocolType::BitChat, ""), "bitchat");
assert_eq!(sanitize_server(ProtocolType::Discord, ""), "discord");
}
#[test]

View File

@ -357,9 +357,11 @@ async fn main() -> anyhow::Result<()> {
info!(count = custom_keybindings.len(), "Custom keybindings loaded from config");
}
// P2P identity
let _p2p_keypair = engine::crypto::NoiseKeypair::generate();
info!(fingerprint = %_p2p_keypair.fingerprint, "P2P identity key generated");
// Noise identity keypair — used by ADC's encrypted C-C connections.
// (BitChat P2P identity generation was removed in 0.10.2; the underlying
// Noise primitive is retained because ADC's C-C connections use it.)
let _noise_keypair = engine::crypto::NoiseKeypair::generate();
info!(fingerprint = %_noise_keypair.fingerprint, "Noise identity key generated");
// Spawn dispatcher with server entries (for TLS/SASL config lookup)
let mut dispatcher = Dispatcher::new(dispatcher_cmd_rx, event_tx, config.global.nickname.clone());
@ -1415,11 +1417,14 @@ async fn handle_user_command(
" /adc broadcast <msg> Broadcast to hub\n",
" /adc get <sid> <path> Download file from user\n",
"\n",
"BitChat P2P (0.5.0):\n",
" /connect bitchat <name> Connect (uses config for listen/bootstrap)\n",
" /bitchat peers List discovered P2P peers\n",
" /bitchat dm <peer> <msg> Send a direct message\n",
" /bitchat send <peer> <f> Send a file via P2P\n",
"Media (0.10.1):\n",
" /url <url> Open URL in OS default browser\n",
" /video <url> Launch video in OS default player\n",
" /image <url> Attempt inline image (falls back to /url)\n",
"\n",
"Note: BitChat P2P support was removed in 0.10.2 pending clarity on\n",
" Jack Dorsey's BitChat project and the India courts situation.\n",
" See NOTICES.md for the full rationale.\n",
);
let msg = ChatMessage::notice(ProtocolType::Irc, "Status", help_text);
ctx.app.route_message(msg);
@ -1438,10 +1443,7 @@ async fn handle_user_command(
Command::NerimityJoin { .. } | Command::NerimityLeave { .. } | Command::NerimityMembers { .. } | Command::NerimityServers => {
let _ = dispatcher_cmd_tx.send(cmd.clone()).await;
}
Command::BitChatPeers | Command::BitChatDm { .. } | Command::BitChatSendFile { .. } => {
// Forward BitChat commands to dispatcher.
let _ = dispatcher_cmd_tx.send(cmd.clone()).await;
}
// NOTE: BitChat command handlers removed in 0.10.2. See NOTICES.md.
Command::PluginList => {
let plugins = plugin_manager.list_plugins();
if plugins.is_empty() {

View File

@ -1508,6 +1508,15 @@ async fn handle_command<W: AsyncWrite + Unpin>(
// (NOTICE must never trigger an automated reply per RFC 1459).
// This is why `/ctcp <nick> VERSION` was silently ignored.
let req = request.as_str();
// Build the local-notice label first (borrows `message`), then
// consume `message` into the wire line. Doing it in this order
// avoids the "use of moved value" that would occur if we built
// the line first and then tried to read `message` again for the
// label.
let label = match &message {
Some(msg) => format!("CTCP {req} to {target}: {msg}"),
None => format!("CTCP {req} to {target}"),
};
let line = match message {
Some(msg) => format!("PRIVMSG {} :\x01{} {}\x01\r\n", target, req, msg),
None => format!("PRIVMSG {} :\x01{}\x01\r\n", target, req),
@ -1517,10 +1526,6 @@ async fn handle_command<W: AsyncWrite + Unpin>(
// Surface a local notice so the user sees the request was sent,
// even before the reply arrives. This also makes self-targeted
// CTCP queries (e.g. `/ctcp mynick VERSION`) visible.
let label = match message {
Some(msg) => format!("CTCP {req} to {target}: {msg}"),
None => format!("CTCP {req} to {target}"),
};
let _ = tx.send(ChatMessage::notice(
ProtocolType::Irc, server, &label,
)).await;

View File

@ -1,5 +1,4 @@
pub mod adc;
pub mod bitchat;
pub mod discord;
pub mod irc;
pub mod matrix;
@ -7,11 +6,14 @@ pub mod stout;
pub mod spacebar;
pub mod nerimity;
// NOTE: BitChat was removed in 0.10.2 pending clarity on Jack Dorsey's
// BitChat project and the India courts situation. The legal status of the
// protocol and its reference implementation is unclear; we will re-evaluate
// when the dust settles. See NOTICES.md for the full rationale.
#[allow(unused_imports)]
pub use adc::{AdcCommand, AdcConfig, AdcMsgType, AdcMessage, run_adc, parse_adc_message, adc_escape, adc_unescape, inf_field};
#[allow(unused_imports)]
pub use bitchat::{BitChatCommand, BitChatConfig, BitChatMessage, run_bitchat, CHAT_TOPIC};
#[allow(unused_imports)]
pub use discord::{DiscordCommand, DiscordConfig, run_discord};
#[allow(unused_imports)]
pub use irc::{IrcCommand, IrcConfig, parse_irc_message, run_irc, DccEvent, DccSendOffer, parse_dcc_send, parse_dcc_accept};

View File

@ -604,7 +604,7 @@ mod tests {
let (tx, _rx) = mpsc::channel(16);
let mgr = TransferManager::new(tx);
let id = mgr.queue_send(ProtocolType::BitChat, "peer", &src_path).unwrap();
let id = mgr.queue_send(ProtocolType::Adc, "peer", &src_path).unwrap();
let cancel = tokio_util::sync::CancellationToken::new();

View File

@ -151,7 +151,6 @@ impl ChatView {
ProtocolType::Irc => Color::DarkGray,
ProtocolType::Matrix => Color::Magenta,
ProtocolType::Adc => Color::Blue,
ProtocolType::BitChat => Color::Green,
ProtocolType::Discord => Color::Gray,
ProtocolType::Stout => Color::Yellow,
ProtocolType::Spacebar => Color::Red,
@ -165,7 +164,6 @@ impl ChatView {
ProtocolType::Irc => self.palette.event_fg,
ProtocolType::Matrix => NaimColor::Magenta,
ProtocolType::Adc => NaimColor::Blue,
ProtocolType::BitChat => NaimColor::Green,
ProtocolType::Discord => NaimColor::White,
ProtocolType::Stout => NaimColor::Yellow,
ProtocolType::Spacebar => NaimColor::Red,

View File

@ -338,12 +338,12 @@ fn default_menus() -> Vec<MenuBarEntry> {
MI { label: "Redraw Screen\tCtrl-L".into(), action: "__internal:ctrl_l".into(), separator: false },
]),
// ── Connect ─────────────────────────────────────────────────
// NOTE: BitChat menu entry removed in 0.10.2. See NOTICES.md.
MenuBarEntry::new("Connect", vec![
MI { label: "IRC…\t__prompt:/connect irc ".into(), action: "__prompt:/connect irc ".into(), separator: false },
MI { label: "Matrix…\t__prompt:/connect matrix ".into(), action: "__prompt:/connect matrix ".into(), separator: false },
MI { label: "ADC/DC++…\t__prompt:/connect adc ".into(), action: "__prompt:/connect adc ".into(), separator: false },
MI { label: "Discord…\t__prompt:/connect discord ".into(), action: "__prompt:/connect discord ".into(), separator: false },
MI { label: "BitChat…\t__prompt:/connect bitchat ".into(), action: "__prompt:/connect bitchat ".into(), separator: false },
MI { label: String::new(), action: String::new(), separator: true },
MI { label: "Server List\t__server_list".into(), action: "__server_list".into(), separator: false },
MI { label: "Disconnect All\t/disconnect".into(), action: "/disconnect".into(), separator: false },

View File

@ -8,14 +8,15 @@
//!
//! Each entry is prefixed with a 1-character protocol badge followed by a
//! space, colored by protocol (IRC=Cyan I, Matrix=Magenta M, ADC=Blue A,
//! BitChat=Green P). This makes it visually obvious which protocol a tab
//! belongs to when multiple protocols are connected simultaneously.
//! Discord=White D, Stout=Yellow S, Spacebar=Red S, Nerimity=Magenta N).
//! This makes it visually obvious which protocol a tab belongs to when
//! multiple protocols are connected simultaneously.
//!
//! ```
//! ┌ IRC
//! ├> I #rust ← IRC channel, waiting/unread (cyan badge)
//! │ M matrix-room← Matrix room, current (magenta badge)
//! └ P p2p-room ← BitChat room, last entry (green badge)
//! └ D discord ← Discord channel, last entry (white badge)
//! ```
//!
//! Badges are suppressed when `content_width < 8` or when `with_badges(false)`
@ -215,7 +216,6 @@ impl Widget for WinlistWidget<'_> {
ProtocolType::Irc => "I",
ProtocolType::Matrix => "M",
ProtocolType::Adc => "A",
ProtocolType::BitChat => "P",
ProtocolType::Discord => "D",
ProtocolType::Stout => "S",
ProtocolType::Spacebar => "S",
@ -225,7 +225,6 @@ impl Widget for WinlistWidget<'_> {
ProtocolType::Irc => NaimColor::Cyan,
ProtocolType::Matrix => NaimColor::Magenta,
ProtocolType::Adc => NaimColor::Blue,
ProtocolType::BitChat => NaimColor::Green,
ProtocolType::Discord => NaimColor::White,
ProtocolType::Stout => NaimColor::Yellow,
ProtocolType::Spacebar => NaimColor::Red,