IRC: join after NickServ login, keyed auto-join, SASL fallback

- With NickServ, auto-join waits for the logged-in reply (900) or 2 s
  at most, so registered-only IRC channels let us in
- A failed SASL login falls back to NickServ (its own password, or the
  SASL account and password)
- IDENTIFY names the account explicitly, and once logged in on a
  fallback nick, REGAIN takes ours back from a stale session
- Auto-join entries take keys ("#private key, #public"); keys from
  /join are reused when rejoining; keyed channels go first in JOIN
- Serial irc dump: whole Buffers, and which login is configured
  (never the secrets)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-03 19:48:16 +02:00
co-authored by Claude Opus 5.5
parent 83eb791924
commit 1f0c1b2e14
9 changed files with 263 additions and 41 deletions
+27 -6
View File
@@ -28,7 +28,7 @@ void test_save_and_reload() {
c.nick = "clement";
c.saslUser = "clement";
c.saslPassword = "s3cret";
c.autojoin = {"#roro", "#meshtastic"};
c.autojoin = {{"#roro", ""}, {"#meshtastic", "k3y"}};
TEST_ASSERT_TRUE(c.save().empty());
}
IrcConfig again(store);
@@ -36,7 +36,8 @@ void test_save_and_reload() {
TEST_ASSERT_EQUAL_STRING("clement", again.nick.c_str());
TEST_ASSERT_EQUAL_STRING("s3cret", again.saslPassword.c_str());
TEST_ASSERT_EQUAL(2, again.autojoin.size());
TEST_ASSERT_EQUAL_STRING("#meshtastic", again.autojoin[1].c_str());
TEST_ASSERT_EQUAL_STRING("#meshtastic", again.autojoin[1].channel.c_str());
TEST_ASSERT_EQUAL_STRING("k3y", again.autojoin[1].key.c_str());
}
void test_invalid_values_are_refused_with_a_reason() {
@@ -54,14 +55,32 @@ void test_invalid_values_are_refused_with_a_reason() {
c.host = "";
TEST_ASSERT_FALSE(c.save().empty());
c.host = "irc.libera.chat";
c.autojoin = {"roro"}; // IRC channels start with # or &
c.autojoin = {{"roro", ""}}; // IRC channels start with # or &
TEST_ASSERT_FALSE(c.save().empty());
}
void test_autojoin_text_round_trip() {
TEST_ASSERT_EQUAL(2, IrcConfig::parseChannels("#a, #b").size());
TEST_ASSERT_EQUAL(2, IrcConfig::parseChannels("#a #b").size());
TEST_ASSERT_EQUAL_STRING("#a #b", IrcConfig::formatChannels({"#a", "#b"}).c_str());
TEST_ASSERT_EQUAL(2, IrcConfig::parseChannels("#a #b").size()); // the older, space-separated form
TEST_ASSERT_EQUAL_STRING("#a, #b", IrcConfig::formatChannels({{"#a", ""}, {"#b", ""}}).c_str());
}
void test_autojoin_entries_can_carry_a_key() {
auto list = IrcConfig::parseChannels("#private s3cret, #public &local");
TEST_ASSERT_EQUAL(3, list.size());
TEST_ASSERT_EQUAL_STRING("#private", list[0].channel.c_str());
TEST_ASSERT_EQUAL_STRING("s3cret", list[0].key.c_str());
TEST_ASSERT_EQUAL_STRING("", list[1].key.c_str());
TEST_ASSERT_EQUAL_STRING("&local", list[2].channel.c_str());
TEST_ASSERT_EQUAL_STRING("#private s3cret, #public, &local", IrcConfig::formatChannels(list).c_str());
}
void test_a_key_without_a_channel_is_refused() {
MemoryStore store;
IrcConfig c(store);
c.load("x");
c.autojoin = IrcConfig::parseChannels("orphan #a");
TEST_ASSERT_FALSE(c.validate().empty());
}
void test_copy_settings_from_a_draft() {
@@ -70,7 +89,7 @@ void test_copy_settings_from_a_draft() {
live.load("x");
IrcConfig draft = live;
draft.host = "irc.example.org";
draft.autojoin = {"#a"};
draft.autojoin = {{"#a", ""}};
TEST_ASSERT_EQUAL_STRING("irc.libera.chat", live.host.c_str()); // the draft is independent
live.copySettingsFrom(draft);
TEST_ASSERT_EQUAL_STRING("irc.example.org", live.host.c_str());
@@ -84,5 +103,7 @@ int main() {
RUN_TEST(test_invalid_values_are_refused_with_a_reason);
RUN_TEST(test_autojoin_text_round_trip);
RUN_TEST(test_copy_settings_from_a_draft);
RUN_TEST(test_autojoin_entries_can_carry_a_key);
RUN_TEST(test_a_key_without_a_channel_is_refused);
return UNITY_END();
}