## What changed
`Network.validate()` now compares a locked network's host against the
configured
`defaults.host` **case-insensitively**.
## Why
In `server/models/network.ts`, `validate()` lowercases the incoming host
(hostnames are
case-insensitive):
```ts
this.host = cleanString(this.host).toLowerCase();
```
but the `lockNetwork` guard then compared that lowercased value against
the **raw** configured
default with a strict `!==`:
```ts
this.host !== Config.values.defaults.host
```
`Config.values.defaults.host` is never normalized on load, so when an
admin's `defaults.host`
contains uppercase letters (e.g. `irc.eXample.net`), a persisted network
whose stored host had
been lowercased (`irc.example.net`) no longer matched the default and
was rejected with:
> The hostname you specified (irc.example.net) is not allowed.
The block is guarded by `this.host.length > 0`, so it doesn't fire on
the first connect (locked
networks send an empty host there). It only bites **after a restart**,
when the persisted network
JSON carries the stored host — which is exactly the symptom reported in
#4733: the network never
reconnects.
## Fix
Compare against `Config.values.defaults.host.toLowerCase()`. One line;
no behavior change for
hosts that already matched.
## Tests
Added a regression test to `test/models/network.ts` (`#validate()`),
which runs in CI. It sets a
mixed-case `defaults.host` under `lockNetwork` + private mode and
asserts a network with the same
mixed-case host validates successfully. The test fails without the fix
(`validate()` returns
`false`) and passes with it.
- `yarn vitest run test/models/network.ts` — 24/24 pass.
- `tsc -p server/tsconfig.json`, eslint and prettier clean on the
changed files.
Fixes#4733
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Updated version of #4550. Closes#337, closes#872 (#337 can't be fully
addressed — tracking nick changes isn't really possible atm).
<img width="253" height="149" alt="image"
src="https://github.com/user-attachments/assets/8a961868-0aa9-406b-92e2-f9d4858e2d0d"
/>
Away support:
<img width="214" alt="image"
src="https://user-images.githubusercontent.com/8675906/165882385-6f928c8e-5a0b-4502-8bd3-3cd0bd70e0e0.png">
## Summary
Query windows show whether the person behind them is online, and whether
they are away, using
[MONITOR](https://ircv3.net/specs/extensions/monitor) rather than
polling. `Network` keeps the monitor list: queries are registered in
batched `MONITOR +` commands once RPL_ISUPPORT tells us the server
supports it, and one at a time as new query windows open. The limit
advertised in `MONITOR=<n>` is respected, with anything over it waiting
for a slot; `MONITOR=0` means no monitoring at all, a bare `MONITOR`
means no limit. A nick change on a monitored query swaps the target in
place so the query keeps its slot.
[extended-monitor](https://ircv3.net/specs/extensions/extended-monitor)
is requested too, so away, nick, account and chghost updates arrive for
monitored nicks we share no channel with. Away state for people we do
share a channel with comes from a WHO on join.
The indicator is a dot on the query in the sidebar and next to the nick
in the message bubble, with a tooltip and an `img` role so it isn't
colour-only, and it can be turned off under Appearance.
Known limitation: a server that enforces a monitor limit it doesn't
advertise will reject part of a batch with `ERR_MONLISTFULL`; the user
gets a message in the lobby, but the client's own list isn't reconciled
until reconnect.
## Test Plan
- Unit tests for the monitor list: add, case-insensitive dedupe,
queueing at the limit, removal draining the queue, batching, and the
rename path.
- Ran the server against a local IRCd and watched the wire: opening
queries sends `MONITOR + <nick>` each, a reconnect with four open
queries sends a single `MONITOR + bob,carol,dave,mallory`, 730/731 drive
the online/offline state, a rename sends `MONITOR - bob` then `MONITOR +
bob2`, `MONITOR=0` sends nothing at all, and a bare `MONITOR` behaves as
unlimited.
Closes#5052
Multi-line input is currently sent as one PRIVMSG per line, so a paste
arrives as N unrelated messages. On networks that support
[draft/multiline](https://ircv3.net/specs/extensions/multiline) it can
be sent as a single BATCH and stays one message everywhere.
## Summary
Sending goes through `sayMultiline()` in `plugins/inputs/msg.ts` when
the network advertises limits we can respect (`multilineLimits()`); if
the batch is over max-bytes/max-lines the send falls back to individual
messages, and the local echo follows whichever path was actually taken
so the sender sees what everyone else got. `Client.input()` stops
splitting on newlines when the target network has the cap. Inbound
batches arrive from irc-framework already reassembled and are flagged
`multiline` on the `Msg`, which drives line-by-line rendering (`parse()`
joins lines with `<br>`), one entry per message in the text logs, and a
first-non-empty-line notification preview. `MULTILINE_*` standard
replies get readable text.
No pin bump: `master` already points at the irc-framework commit that
carries multiline (kiwiirc/irc-framework#414).
## Test Plan
- New unit tests for the send path (batch, fallback on max-bytes, no
limits advertised, blank lines), the text log format, `parse()`, and the
standard replies.
- Ran the server against a local IRCd advertising `draft/multiline` and
confirmed on the wire: a 3-line message goes out as one `BATCH …
draft/multiline` with three frames; with the cap advertised bare (no
limits) or with `max-bytes` too small it degrades to three plain
PRIVMSGs and the local echo degrades with it; a CRLF paste sends clean
frames and echoes without stray `\r`; an inbound batch from another user
renders as one message.
- Not verified: rendering was checked through unit tests rather than in
a browser.
Relies on https://github.com/kiwiirc/irc-framework/pull/411https://ircv3.net/specs/client-tags/reply.html
Design feedback very welcome
<img width="1718" height="278" alt="Screenshot 2026-04-11 at 21-00-22
#thelounge — The Lounge"
src="https://github.com/user-attachments/assets/ba6e7c69-2477-4a7b-b196-901d19ca9cba"
/>
1. we lack a 'thread' view (irccloud, slack, etc). can be a followup if
we do want it
2. need to test scrolling to messages outside of the current buffer
3. due to lack of client support, i opted to auto prefill the input with
\`<targetNick>: \` so people can follow along even without reply support
4. CSS can definitely be improved (once a design is settled on)
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
https://github.com/thelounge/thelounge/pull/5044 added the type + value
to handles, but now we need to persist it in the database so e.g.
mentions work after a server restart.
`msgid` is optional
## What changed
- Match nickname tokens with Unicode letters and numbers while
preserving IRC nickname characters.
- Use Unicode-aware word boundaries for nickname highlights.
- Add regression coverage for Polish text, Unicode nicknames, and
existing IRC nickname characters.
## Why
The previous regular expressions used ASCII-only character classes. This
caused a one-character nickname such as `D` to be detected inside words
containing Polish diacritics, for example `dzięki`.
## Before / after
| Before (v4.5.2) | After (this PR) |
|:---:|:---:|
|

|

|
| Scenario | Before | After |
|---|---|---|
| Nick `Ądam42` appears in a message | Not recognized as a nick —
rendered as plain text (no color, not clickable) | Recognized and
colored like any other nick |
| `Ćadam` / `adamć` with your nick `adam` | Falsely highlighted as a
mention of you | Not highlighted |
| Genuine mention `hey adam` | Highlighted | Highlighted (no regression)
|
<sub>Screenshots are a faithful reproduction rendered with TheLounge's
real <code>client/css/style.css</code> and the actual old/new regexes
(not a live IRC capture).</sub>
## Validation
- Direct Unicode nickname extraction and boundary checks with Node.js 24
- Full Vitest/lint suite not run because repository dependencies were
unavailable and dependency installation stalled in the local environment
Fixes#4930
Ratified two days ago
https://ircv3.net/specs/client-tags/channel-context
> This specification defines a client-only message tag to indicate the
channel a private PRIVMSG or NOTICE should be displayed in.
Reported by AlexC on IRC:
> after upgrade [to 4.5.0] identd forwarding is broken
4.5.0 included 0e48014d (`ident: only respond if the ip,port tuples
match`). It correctly requires the addresses to match but the comparison
compares the IPs as strings, but an IP can have multiple variations.
A default `bind` unset ident server will look something like
`::ffff:127.0.0.1` while outgoing will be `127.0.0.1.` and those
obviously do not match in a string equality check.
closes#5101
Fixes#5103
Today, `getMessages()` filters by `(network, channel)` but sorts by
`time DESC, id DESC` and no index covers both (and a new one supersedes
`network_channel`). This has always been an issue but
https://github.com/thelounge/thelounge/pull/5055 made this code
synchronous which exposed it.
| | Before | After |
|--------|--------|--------|
| history load, 5 channels (4.4M rows total, busiest 3M) | 6.9s, event
loop blocked | 80ms |
| single channel with 3M messages | 4.6s | 18ms |
| query plan | index scan + temp B-tree sort | index only |
When sending a message that is too long, thelounge will split it up into
multiple PRIVMSGs. These will be saved to the sqlite database with
identical timestamps. Thelounge retrieves messages from the DB using
`ORDER BY time DESC`, which under certain circumstances causes these
messages to be loaded and displayed in the wrong order. This PR
addresses that by also ordering by id (only affects messages with
identical timestamps).
Before:
<img width="1050" height="107" alt="image"
src="https://github.com/user-attachments/assets/54fac085-a989-48fa-9538-1faecb09aa44"
/>
After:
<img width="1058" height="114" alt="image"
src="https://github.com/user-attachments/assets/0bb99db9-5a5d-481d-b799-87162d0535e8"
/>
Introduced in
551f85ea51
If you `/expand` (or `/collapse`) and reload, the state is not persisted
as its not stored on the server. We didn't notice as the client logic is
correct.
h/t @xPaw and claude for finding
Found by claude:
```
server/plugins/inputs/mute.ts — /mute slash command never persists. Missing client.save(). Mutes set via the command vanish on restart. The socket handler (server/server.ts:768-797) does save.
server/plugins/inputs/mute.ts:18 — /mute #Foo fails when channel was joined as #foo. Uses case-sensitive c.name === arg; IRC channel names are case-insensitive.
```
- Mentions were doing a bunch of mutations; not just uses derived state
- no need for a separate `ClientMention`
- added `extractInputHistory` for shared logic
- tests made by claude
Replace the manual deleteFolder() recursive implementation with
fs.rmSync({recursive: true, force: true}), which handles both files
and directories. This also resolves the ENOTDIR error caused by
.DS_Store and other non-directory entries in the storage folder.
It is possible to send actions (/me et al.) to a STATUSMSG target
(+#channel etc), even though thelounge does not currently support
sending that. Show the statusmsg indicator on such messages.
Apparently sqlite started validating this in certain versions.
Crashes on BSDs with:
SQLite version 3.50.4 2025-07-30 19:33:53
sqlite> SELECT msg, type, time, network, channel FROM messages WHERE type = "message";
Parse error: no such column: "message" - should this be a string literal in single-quotes?
time, network, channel FROM messages WHERE type = "message";
error here ---^
The spread operator will place the arguments, which can reach the call
stack limit if too many messages are being sent. This fix uses
`.concat()` to avoid the spread operator.
Fixes#5022
This has the problem that failing to get a modification trigger might
lead to corrupted state. The author tested it on their large instance
however and it seems to work very well.
So let's try if anyone hits any problems in the wild
Previously when any user config file was changed all users would be reloaded. This could be very expensive on installations with thousands of users. Since this triggered when any file was changed it would trigger when any user connected which can be quite frequent.
As a side-effect this removes the debouncing. This means that user changes take effect instantly rather than after 1s.
Since there is no longer a 1s delay it is extra important that files are written safely. To this end the `thelounge add <user>` command was updated to write the user file atomically. (The update path already did this.)
irc-framework has a funny fallback where it uses nick + server pw
in the sasl handshake, if account is undefined in the options.
This becomes a problem, as the nick might not actually be the account
(happened for znc users), so we need to set it to an empty object to really
turn it off.
LockNetwork is documented as:
> When set to `true`, users will not be able to modify host, port and TLS
> settings and will be limited to the configured network.
Looking at the view, that also includes the name field (for some reason).
When leaveMessage was added to the defaults, the white list for the LockedConfigNetDefaults
wasn't adjusted.
Rather than playing whack a mole, disallow the documented fields + name and export the rest.
Fixes: https://github.com/thelounge/thelounge/issues/4956
Installing a scoped npm package with thelounge install lead to an error,
because the original split that was used to split the version from the
package, split at the first @ from scoped packages.