M1 step 4a: IRC protocol core (host-tested)

lib/irc: IrcMessage parse/serialize, base64, ReconnectPolicy
(5 s .. 5 min), IrcConfig (validated, persisted) and IrcSession:
registration with SASL PLAIN or NickServ, nick fallback, PING, Buffers
capped at 50 lines with unread counts, Mentions, CTCP ACTION/VERSION,
topics, /names on request only, others' join/part/quit hidden, user
commands, and rejoining after a pause. No networking: the session
returns lines to send, Log entries and Notifications.

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-02 15:03:21 +02:00
co-authored by Claude Opus 5.5
parent d3948ccc3a
commit 311eb38723
11 changed files with 1142 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
#pragma once
#include <cstdint>
#include <deque>
#include <set>
#include <string>
#include <vector>
#include "irc_config.h"
#include "irc_message.h"
namespace roro {
struct IrcLine {
enum class Kind { Message, Action, Notice, Info, Own, OwnAction };
int64_t utc; // -1 if the clock wasn't set
Kind kind;
std::string nick;
std::string text;
};
// One IRC conversation (see Buffer in CONTEXT.md).
struct IrcBuffer {
enum class Type { Server, Channel, Query };
std::string name;
Type type;
std::deque<IrcLine> lines; // the most recent kLinesPerBuffer; full history goes to Logs
int unread = 0;
bool mentioned = false;
bool joined = false;
std::string topic;
};
struct IrcLogEntry {
std::string buffer;
IrcLine line;
};
// What the session wants done: lines to send, Log entries, Notification texts.
struct IrcEffects {
std::vector<std::string> send;
std::vector<IrcLogEntry> logs;
std::vector<std::string> notifications;
};
// The IRC protocol, without any networking: registration (SASL PLAIN or NickServ), Buffers,
// unread counts, Mentions, the user's commands, and rejoining after a pause. The IRC Service
// feeds it received lines and carries out the effects.
class IrcSession {
public:
static constexpr size_t kLinesPerBuffer = 50;
static constexpr int kMaxBuffers = 12;
explicit IrcSession(const IrcConfig& config);
void connected(int64_t utc);
void disconnected(int64_t utc, const std::string& reason);
void receive(const std::string& raw, int64_t utc);
void input(int buffer, const std::string& text, int64_t utc);
// The Buffer the user is looking at (-1: none); it neither counts unread nor notifies.
void setViewing(int buffer);
bool registered() const { return registered_; }
bool quitRequested() const { return quit_; }
const std::string& nick() const { return nick_; }
int bufferCount() const { return static_cast<int>(buffers_.size()); }
const IrcBuffer& buffer(int i) const { return buffers_[i]; }
int totalUnread() const;
IrcEffects takeEffects();
private:
void send(const std::string& line) { effects_.send.push_back(line); }
int findBuffer(const std::string& name) const;
int bufferFor(const std::string& name, IrcBuffer::Type type); // creates if needed and possible
void add(int buffer, IrcLine::Kind kind, const std::string& nick, const std::string& text, int64_t utc,
bool countsAsMention = false);
void info(int buffer, const std::string& text, int64_t utc) { add(buffer, IrcLine::Kind::Info, "", text, utc); }
bool mentionsMe(const std::string& text) const;
void onPrivmsg(const IrcMessage& m, int64_t utc, bool notice);
void onWelcome(const IrcMessage& m, int64_t utc);
void command(int buffer, const std::string& text, int64_t utc);
void say(int buffer, const std::string& text, int64_t utc, bool action);
const IrcConfig& config_;
std::vector<IrcBuffer> buffers_;
std::string nick_;
bool registered_ = false;
bool quit_ = false;
int viewing_ = -1;
std::set<std::string> namesRequested_;
std::vector<std::string> rejoin_; // IRC channels to join again after a reconnect
IrcEffects effects_;
};
} // namespace roro