mirror of
https://github.com/kc1awv/rrcd.git
synced 2026-05-04 16:39:07 -07:00
Merge pull request #1 from kc1awv/handle_nick
Add nickname support to envelope protocol and update version to 0.1.1
This commit is contained in:
@@ -13,3 +13,7 @@ Initial public release.
|
||||
- Persistent config + room registry in TOML (`rrcd.toml`, `rooms.toml`)
|
||||
- Reduced lock contention by flushing outbound packets outside the shared state lock
|
||||
- Added small packaging metadata and README polish
|
||||
|
||||
## 0.1.1 - 2025-12-30
|
||||
|
||||
- Protocol extension: hub may attach an optional nickname (`K_NICK = 7`) to forwarded `MSG`/`NOTICE` envelopes based on the nickname provided in `HELLO`.
|
||||
|
||||
15
README.md
15
README.md
@@ -74,6 +74,21 @@ use a hub-local convention: if a client sends a `MSG`/`NOTICE` whose body is a
|
||||
string beginning with `/`, and the command is recognized, the hub treats it as a
|
||||
command and does not forward it.
|
||||
|
||||
Wire-level extensions (backwards-compatible):
|
||||
|
||||
- **Optional envelope nickname**: the hub may include an additional envelope key
|
||||
`K_NICK = 7` (string) when forwarding `MSG`/`NOTICE`. This is an optional
|
||||
hint associated with `K_SRC` so clients can display a human-friendly
|
||||
nickname instead of only the sender identity hash.
|
||||
|
||||
The hub learns this value from the client's `HELLO` body key
|
||||
`B_HELLO_NICK = 0` and treats it as the authoritative nickname for that
|
||||
link. Clients should treat `K_NICK` as optional and fall back to `K_SRC`
|
||||
when it is missing.
|
||||
|
||||
Nickname policy (current implementation): trimmed Unicode string, UTF-8
|
||||
encodable on the wire, maximum 32 characters.
|
||||
|
||||
Configure trusted operators and banned identities in the TOML config:
|
||||
|
||||
- `trusted_identities`: list of Reticulum Identity hashes (hex) allowed to run
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rrcd"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "Reticulum Relay Chat daemon (hub service)"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
__all__ = ["__version__"]
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__version__ = "0.1.1"
|
||||
|
||||
@@ -10,6 +10,8 @@ K_TS = 3
|
||||
K_SRC = 4
|
||||
K_ROOM = 5
|
||||
K_BODY = 6
|
||||
K_NICK = 7
|
||||
NICK_MAX_CHARS = 32
|
||||
|
||||
# Message types
|
||||
T_HELLO = 1
|
||||
|
||||
@@ -3,7 +3,19 @@ from __future__ import annotations
|
||||
import os
|
||||
import time
|
||||
|
||||
from .constants import K_BODY, K_ID, K_ROOM, K_SRC, K_T, K_TS, K_V, RRC_VERSION
|
||||
from .constants import (
|
||||
K_BODY,
|
||||
K_ID,
|
||||
K_NICK,
|
||||
K_ROOM,
|
||||
K_SRC,
|
||||
K_T,
|
||||
K_TS,
|
||||
K_V,
|
||||
NICK_MAX_CHARS,
|
||||
RRC_VERSION,
|
||||
)
|
||||
from .util import normalize_nick
|
||||
|
||||
|
||||
def now_ms() -> int:
|
||||
@@ -20,6 +32,7 @@ def make_envelope(
|
||||
src: bytes,
|
||||
room: str | None = None,
|
||||
body=None,
|
||||
nick: str | None = None,
|
||||
mid: bytes | None = None,
|
||||
ts: int | None = None,
|
||||
) -> dict:
|
||||
@@ -34,6 +47,10 @@ def make_envelope(
|
||||
env[K_ROOM] = room
|
||||
if body is not None:
|
||||
env[K_BODY] = body
|
||||
if nick is not None:
|
||||
n = normalize_nick(nick)
|
||||
if n is not None:
|
||||
env[K_NICK] = n
|
||||
return env
|
||||
|
||||
|
||||
@@ -81,3 +98,22 @@ def validate_envelope(env: dict) -> None:
|
||||
raise TypeError("room name must be a string")
|
||||
if room == "":
|
||||
raise ValueError("room name must not be empty")
|
||||
|
||||
if K_NICK in env:
|
||||
nick = env[K_NICK]
|
||||
if not isinstance(nick, str):
|
||||
raise TypeError("nickname must be a string")
|
||||
if nick.strip() == "":
|
||||
raise ValueError("nickname must not be empty")
|
||||
|
||||
# Require normalized form on the wire.
|
||||
if nick != nick.strip():
|
||||
raise ValueError("nickname must not have leading/trailing whitespace")
|
||||
if len(nick) > int(NICK_MAX_CHARS):
|
||||
raise ValueError("nickname too long")
|
||||
if "\n" in nick or "\r" in nick or "\x00" in nick:
|
||||
raise ValueError("nickname contains control characters")
|
||||
try:
|
||||
nick.encode("utf-8", "strict")
|
||||
except UnicodeError as e:
|
||||
raise ValueError(f"nickname is not valid UTF-8: {e}") from e
|
||||
|
||||
@@ -18,6 +18,7 @@ from .constants import (
|
||||
B_WELCOME_GREETING,
|
||||
B_WELCOME_HUB,
|
||||
K_BODY,
|
||||
K_NICK,
|
||||
K_ROOM,
|
||||
K_SRC,
|
||||
K_T,
|
||||
@@ -33,7 +34,7 @@ from .constants import (
|
||||
T_WELCOME,
|
||||
)
|
||||
from .envelope import make_envelope, validate_envelope
|
||||
from .util import expand_path
|
||||
from .util import expand_path, normalize_nick
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -2150,8 +2151,9 @@ class HubService:
|
||||
|
||||
if isinstance(body, dict):
|
||||
nick = body.get(B_HELLO_NICK)
|
||||
if isinstance(nick, str) and nick.strip():
|
||||
sess["nick"] = nick.strip()
|
||||
n = normalize_nick(nick)
|
||||
if n is not None:
|
||||
sess["nick"] = n
|
||||
|
||||
if self.identity is not None:
|
||||
sess["welcomed"] = True
|
||||
@@ -2347,6 +2349,15 @@ class HubService:
|
||||
env[K_SRC] = bytes(peer_hash) if isinstance(peer_hash, (bytes, bytearray)) else peer_hash
|
||||
env[K_ROOM] = r
|
||||
|
||||
# Backwards-compatible extension: hub can attach the nickname learned
|
||||
# from HELLO so clients can render a human-friendly name.
|
||||
nick = sess.get("nick")
|
||||
if isinstance(nick, str) and nick.strip():
|
||||
env[K_NICK] = nick.strip()
|
||||
else:
|
||||
# Prevent client-supplied spoofed nicknames.
|
||||
env.pop(K_NICK, None)
|
||||
|
||||
payload = encode(env)
|
||||
for other in list(self.rooms.get(r, set())):
|
||||
if other is link:
|
||||
|
||||
26
rrcd/util.py
26
rrcd/util.py
@@ -2,6 +2,32 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from .constants import NICK_MAX_CHARS
|
||||
|
||||
|
||||
def expand_path(p: str) -> str:
|
||||
return os.path.expanduser(os.path.expandvars(p))
|
||||
|
||||
|
||||
def normalize_nick(value) -> str | None:
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
|
||||
s = value.strip()
|
||||
if not s:
|
||||
return None
|
||||
|
||||
if len(s) > int(NICK_MAX_CHARS):
|
||||
return None
|
||||
|
||||
# Keep this conservative: avoid embedded newlines or NUL, which frequently
|
||||
# cause UI/log formatting issues.
|
||||
if "\n" in s or "\r" in s or "\x00" in s:
|
||||
return None
|
||||
|
||||
try:
|
||||
s.encode("utf-8", "strict")
|
||||
except UnicodeError:
|
||||
return None
|
||||
|
||||
return s
|
||||
|
||||
@@ -4,6 +4,7 @@ from rrcd.constants import (
|
||||
B_HELLO_NICK,
|
||||
K_BODY,
|
||||
K_ID,
|
||||
K_NICK,
|
||||
K_SRC,
|
||||
K_T,
|
||||
K_TS,
|
||||
@@ -19,6 +20,26 @@ def test_validate_accepts_make_envelope() -> None:
|
||||
validate_envelope(env)
|
||||
|
||||
|
||||
def test_validate_accepts_optional_nick_extension() -> None:
|
||||
env = make_envelope(T_HELLO, src=b"peer", body=None, nick="alice")
|
||||
assert env[K_NICK] == "alice"
|
||||
validate_envelope(env)
|
||||
|
||||
|
||||
def test_validate_rejects_nick_too_long() -> None:
|
||||
env = make_envelope(T_HELLO, src=b"peer", body=None)
|
||||
env[K_NICK] = "a" * 33
|
||||
with pytest.raises(ValueError):
|
||||
validate_envelope(env)
|
||||
|
||||
|
||||
def test_validate_rejects_nick_with_whitespace() -> None:
|
||||
env = make_envelope(T_HELLO, src=b"peer", body=None)
|
||||
env[K_NICK] = " alice "
|
||||
with pytest.raises(ValueError):
|
||||
validate_envelope(env)
|
||||
|
||||
|
||||
def test_validate_rejects_missing_required_key() -> None:
|
||||
env = make_envelope(T_HELLO, src=b"peer", body=None)
|
||||
env.pop(K_TS)
|
||||
@@ -67,3 +88,8 @@ def test_validate_rejects_wrong_field_types() -> None:
|
||||
env[K_TS] = "not-int"
|
||||
with pytest.raises(TypeError):
|
||||
validate_envelope(env)
|
||||
|
||||
env = make_envelope(T_HELLO, src=b"peer", body=None)
|
||||
env[K_NICK] = 123
|
||||
with pytest.raises(TypeError):
|
||||
validate_envelope(env)
|
||||
|
||||
Reference in New Issue
Block a user