zynk/irc.h
hanez 19b07fa76f Wikipedia search command (!wp), version 0.43.0
- Add !wp <search terms> command: searches Wikipedia via MediaWiki API
  (action=query&list=search), displays article titles, snippets, and links.
  Single-result mode fetches the first chapter via Extracts API (exchars=800);
  multi-result mode shows a numbered list with short snippets and per-result
  Read full article links.
- Add self-contained JSON parser helpers: wp_json_get_string(),
  wp_json_next_array_item(), wp_decode_json_string() with \uXXXX Unicode
  support.
- Add wp_url_encode() (RFC 3986), wp_codepoint_to_utf8(), wp_strip_html(),
  wp_collapse_spaces(), wp_hex_digit().
- Add WITH_WP CMake feature toggle (ON by default, HAS_CMD_WP compile def).
- Add !wp to the !help command listing.
- Add doc comments to all 9 functions in cmd_wp.h.
- Update CHANGELOG.md with 0.43.0 entry.
- Update README.md: add Wikipedia to features list, feature toggles table,
  IRC commands utilities table, and minimal build command.
2026-07-26 00:23:38 +02:00

819 lines
26 KiB
C

#ifndef IRC_H
#define IRC_H
#include "zynk.h"
#include "db.h"
/*
* valid_irc_word - Check if a string is a single valid IRC word.
*
* A valid IRC word is non-NULL, non-empty, contains no CRLF characters,
* and contains no spaces or tabs. Used to validate IRC nicknames,
* channel names, and command arguments before database operations.
*
* Parameters:
* s - The string to validate.
*
* Returns: 1 if the string is a valid single word, 0 otherwise.
*/
int valid_irc_word(const char *s) {
if (!s || !*s || has_crlf(s)) return 0;
for (const char *p = s; *p; p++)
if (*p == ' ' || *p == '\t') return 0;
return 1;
}
/* ---- channel op tracking ---- */
/*
* chans - Array of tracked channel entries.
*
* Holds up to MAX_CHANS channel records, each containing the channel name
* and its list of operator nicks. Populated via chan_get_or_add() when JOIN
* messages or NAMREPLY (353) responses are received.
*/
ChanEntry chans[MAX_CHANS];
/*
* nchans - Number of currently tracked channels.
*
* Tracks how many entries in the chans[] array are in use. Incremented by
* chan_get_or_add() when a new channel is registered.
*/
int nchans;
/*
* chan_get - Look up a channel entry by name.
*
* Searches the global chans[] array for a channel matching the given name
* (case-insensitive comparison). Used by all other chan_* functions to
* locate the channel record before modifying it.
*
* Parameters:
* name - The channel name (e.g. "#zynk") to look up.
*
* Returns: Pointer to the ChanEntry if found, NULL otherwise.
*/
ChanEntry *chan_get(const char *name) {
for (int i = 0; i < nchans; i++)
if (strcasecmp(chans[i].name, name) == 0) return &chans[i];
return NULL;
}
/*
* chan_get_or_add - Look up a channel entry, creating it if it doesn't exist.
*
* First tries chan_get(). If not found and there is room in the chans[] array
* (up to MAX_CHANS), allocates a new entry with the given name and an empty
* ops list. Automatically called when a JOIN message or NAMREPLY is received.
*
* Parameters:
* name - The channel name to find or register.
*
* Returns: Pointer to the ChanEntry (existing or newly created), or NULL if
* the channel array is full.
*/
ChanEntry *chan_get_or_add(const char *name) {
ChanEntry *c = chan_get(name);
if (!c && nchans < MAX_CHANS) {
c = &chans[nchans++];
snprintf(c->name, sizeof c->name, "%s", name);
c->nops = 0;
}
return c;
}
/*
* chan_is_op - Check whether a nick has operator status in a channel.
*
* Looks up the channel and searches its ops list for the given nick
* (case-insensitive). Used to gate privileged commands like !code,
* !reload, !restart, and zynk! overwrite.
*
* Parameters:
* name - The channel name.
* nick - The IRC nick to check.
*
* Returns: 1 if the nick is a channel op, 0 otherwise.
*/
int chan_is_op(const char *name, const char *nick) {
ChanEntry *c = chan_get(name);
if (!c) return 0;
for (int i = 0; i < c->nops; i++)
if (strcasecmp(c->ops[i], nick) == 0) return 1;
return 0;
}
/*
* chan_add_op - Add a nick to the operator list for a channel.
*
* Gets or creates the channel entry, then checks for duplicates before
* appending the nick to the ops array. Called when a NAMREPLY shows the
* nick with @, ~, or & prefix, or when a MODE +o is received.
*
* Parameters:
* name - The channel name.
* nick - The IRC nick to add as op.
*
* Returns: void. Silently ignores duplicates and full ops arrays.
*/
void chan_add_op(const char *name, const char *nick) {
ChanEntry *c = chan_get_or_add(name);
if (!c) return;
for (int i = 0; i < c->nops; i++)
if (strcasecmp(c->ops[i], nick) == 0) return;
if (c->nops < MAX_CHAN_OPS) {
snprintf(c->ops[c->nops], sizeof c->ops[0], "%s", nick);
c->nops++;
}
}
/*
* chan_del_op - Remove a nick from the operator list for a channel.
*
* Searches the channel's ops list for the nick (case-insensitive) and
* removes it by shifting subsequent entries down with memmove. Called on
* MODE -o, PART, KICK, and QUIT messages to keep the ops list current.
*
* Parameters:
* name - The channel name.
* nick - The IRC nick to remove from ops.
*
* Returns: void. Silently ignores if the nick is not found.
*/
void chan_del_op(const char *name, const char *nick) {
ChanEntry *c = chan_get(name);
if (!c) return;
for (int i = 0; i < c->nops; i++) {
if (strcasecmp(c->ops[i], nick) == 0) {
memmove(c->ops + i, c->ops + i + 1, (c->nops - i - 1) * sizeof c->ops[0]);
c->nops--;
return;
}
}
}
/*
* chan_rename_op - Rename a nick across all channel operator lists.
*
* Iterates all channels and replaces oldnick with newnick in every ops
* array where a match is found. Called when a NICK change message is
* received so that op tracking stays consistent across renames.
*
* Parameters:
* oldnick - The previous IRC nick.
* newnick - The new IRC nick to substitute.
*
* Returns: void.
*/
void chan_rename_op(const char *oldnick, const char *newnick) {
for (int i = 0; i < nchans; i++) {
for (int j = 0; j < chans[i].nops; j++) {
if (strcasecmp(chans[i].ops[j], oldnick) == 0) {
snprintf(chans[i].ops[j], sizeof chans[i].ops[0], "%s", newnick);
break;
}
}
}
}
/* ---- IRC protocol helpers ---- */
/*
* irc_join - Send a JOIN command to join an IRC channel.
*
* Sanitizes the channel name by stopping at spaces, colons, or CRLF
* to prevent protocol injection. Sends "JOIN #channel\r\n" via net_send.
*
* Parameters:
* s - The IRC session.
* chan - The channel name to join (e.g. "#zynk").
*
* Returns: void.
*/
void irc_join(Session *s, const char *chan) {
char tb[256]; size_t tl = 0;
if (chan) {
for (const unsigned char *p = (const unsigned char*)chan; *p && tl + 1 < sizeof tb; p++) {
if (*p == ' ' || *p == ':' || *p == '\r' || *p == '\n') break;
tb[tl++] = *p;
}
}
tb[tl] = 0;
if (tb[0]) net_send(s, "JOIN %s\r\n", tb);
}
/*
* irc_msg - Send a PRIVMSG to a target (channel or nick).
*
* Sanitizes the target name and message text. If the message contains
* any control characters (bytes < 0x20 or 0x7f), they are replaced with
* spaces to prevent IRC protocol injection. This is critical because
* AI-generated or user-provided text may contain embedded control codes.
* Sends "PRIVMSG target :text\r\n" via net_send.
*
* Parameters:
* s - The IRC session.
* t - The target (channel name or nick).
* txt - The message text to send.
*
* Returns: void.
*/
void irc_msg(Session *s, const char *t, const char *txt) {
char tb[256]; size_t tl = 0;
if (t) {
for (const unsigned char *p = (const unsigned char*)t; *p && tl + 1 < sizeof tb; p++) {
if (*p == ' ' || *p == ':' || *p == '\r' || *p == '\n') break;
tb[tl++] = *p;
}
}
tb[tl] = 0;
const char *send_txt = txt ? txt : "";
char *clean = NULL;
int need_clean = 0;
for (const unsigned char *p = (const unsigned char*)send_txt; *p; p++) {
if (*p < 0x20 || *p == 0x7f) { need_clean = 1; break; }
}
if (need_clean) {
clean = strdup(send_txt);
if (!clean) return;
for (char *p = clean; *p; p++) if ((unsigned char)*p < 0x20 || (unsigned char)*p == 0x7f) *p = ' ';
send_txt = clean;
}
if (!tb[0]) { free(clean); return; }
size_t maxtxt = 490 > tl ? 490 - tl : 0;
size_t tlen = strlen(send_txt);
if (tlen > maxtxt && maxtxt > 3) {
char *trunc = malloc(maxtxt + 1);
if (trunc) {
memcpy(trunc, send_txt, maxtxt - 3);
trunc[maxtxt - 3] = '.'; trunc[maxtxt - 2] = '.'; trunc[maxtxt - 1] = '.'; trunc[maxtxt] = 0;
send_txt = trunc;
free(clean); clean = trunc;
}
}
if (tb[0]) net_send(s, "PRIVMSG %s :%s\r\n", tb, send_txt);
free(clean);
}
/*
* irc_reply - Send a PRIVMSG with an automatic nick prefix for channels.
*
* Wraps irc_msg() to provide consistent addressing in channel conversations.
* When the target is a channel (starts with '#'), prepends "nick: " to the
* message so other users can see which user the bot is replying to. When the
* target is a private message (does not start with '#'), sends the text as-is
* without a prefix. Handles NULL and empty text safely.
*
* Parameters:
* s - The IRC session.
* target - The target channel name or nick.
* nick - The nick of the user who triggered the response.
* txt - The message text to send.
*
* Returns: void.
*/
void irc_reply(Session *s, const char *target, const char *nick, const char *txt) {
if (target && target[0] == '#' && nick && *nick) {
char prefixed[2048];
snprintf(prefixed, sizeof prefixed, "%s: %s", nick, txt);
irc_msg(s, target, prefixed);
} else {
irc_msg(s, target, txt ? txt : "");
}
}
/*
* irc_nick - Send a NICK command to change the bot's nickname.
*
* Sanitizes the nickname by stopping at spaces, colons, or CRLF.
* Sends "NICK newnick\r\n" via net_send. Also used on startup
* and when a nick collision (433/436) requires appending underscore.
*
* Parameters:
* s - The IRC session.
* nn - The new nickname to set.
*
* Returns: void.
*/
void irc_nick(Session *s, const char *nn) {
char tb[64]; size_t tl = 0;
if (nn) {
for (const unsigned char *p = (const unsigned char*)nn; *p && tl + 1 < sizeof tb; p++) {
if (*p == ' ' || *p == ':' || *p == '\r' || *p == '\n') break;
tb[tl++] = *p;
}
}
tb[tl] = 0;
if (tb[0]) net_send(s, "NICK %s\r\n", tb);
}
/*
* irc_pong - Send a PONG response to a server PING.
*
* Strips the leading colon from the server's ping token, sanitizes
* any control characters to spaces, and sends "PONG :token\r\n".
* Required to keep the connection alive and respond to keepalives.
*
* Parameters:
* s - The IRC session.
* r - The ping token from the server (may start with ':').
*
* Returns: void.
*/
static void irc_pong(Session *s, const char *r) {
if (*r == ':') r++;
char pb[256]; size_t pl = 0;
if (r) {
for (const unsigned char *p = (const unsigned char*)r; *p && pl + 1 < sizeof pb; p++) {
if (*p == '\r' || *p == '\n') break;
unsigned char c = *p;
if (c < 0x20 || c == 0x7f) c = ' ';
pb[pl++] = (char)c;
}
}
pb[pl] = 0;
net_send(s, "PONG :%s\r\n", pb);
}
/* ---- IRC protocol handlers ---- */
/*
* handle_privmsg - Dispatch incoming PRIVMSG to the appropriate command handler.
*
* Parses the PRIVMSG params to extract the target channel/nick and the message
* text. Ignores messages from the bot itself. Sets reply_target to the channel
* if it's a channel message, or to the sender's nick for PMs. Then tries each
* command handler in order: zynk, ping, version, help, quit, weather, forecast,
* stock, ai, code, reload, restart. If no command matches and the message was
* addressed to the bot by nick, falls through to cmd_greeting_or_chat.
*
* Parameters:
* s - The IRC session.
* params - The raw IRC params (target :message).
* src_nick - The nick of the message sender.
*
* Returns: void.
*/
static void handle_privmsg(Session *s, const char *params, const char *src_nick) {
const char *p = params;
const char *target = p;
p = strchr(p, ' '); if (!p) return;
char tgt[256]; size_t tl = p - target;
if (tl >= sizeof tgt) tl = sizeof tgt - 1;
memcpy(tgt, target, tl); tgt[tl] = 0;
while (*p == ' ') p++;
if (*p == ':') p++;
const char *msg = p;
if (strcasecmp(src_nick, s->nick) == 0) return;
seen_update(src_nick, tgt, "spoke");
tell_deliver(s, src_nick, (tgt[0] == '#') ? tgt : src_nick);
const char *reply_target = (tgt[0] == '#') ? tgt : src_nick;
#ifdef HAS_CMD_ZYNK
if (cmd_zynk(s, msg, reply_target, src_nick, tgt)) return;
#endif
#ifdef HAS_CMD_PING
if (cmd_ping(s, msg, reply_target, src_nick)) return;
#endif
#ifdef HAS_CMD_VERSION
if (cmd_version(s, msg, reply_target, src_nick)) return;
#endif
#ifdef HAS_CMD_UPTIME
if (cmd_uptime(s, msg, reply_target, src_nick)) return;
#endif
#ifdef HAS_CMD_HELP
if (cmd_help(s, msg, reply_target, src_nick)) return;
#endif
#ifdef HAS_CMD_QUIT
if (cmd_quit(s, msg, reply_target, src_nick, tgt)) return;
#endif
#ifdef HAS_CMD_WEATHER
if (cmd_weather(s, msg, reply_target, src_nick)) return;
#endif
#ifdef HAS_CMD_FORECAST
if (cmd_forecast(s, msg, reply_target, src_nick)) return;
#endif
#ifdef HAS_CMD_STOCK
if (cmd_stock(s, msg, reply_target, src_nick)) return;
#endif
#ifdef HAS_CMD_CALC
if (cmd_calc(s, msg, reply_target, src_nick)) return;
#endif
#ifdef HAS_CMD_TIME
if (cmd_time(s, msg, reply_target, src_nick)) return;
#endif
#ifdef HAS_CMD_SEEN
if (cmd_seen(s, msg, reply_target, src_nick)) return;
#endif
#ifdef HAS_CMD_TELL
if (cmd_tell(s, msg, reply_target, src_nick)) return;
#endif
#ifdef HAS_CMD_GITLOG
if (cmd_gitlog(s, msg, reply_target, src_nick)) return;
#endif
#ifdef HAS_CMD_CHANGELOG
if (cmd_changelog(s, msg, reply_target, src_nick)) return;
#endif
#ifdef HAS_CMD_AI
if (cmd_ai(s, msg, reply_target, src_nick)) return;
#endif
#ifdef HAS_CMD_CODE
if (cmd_code(s, msg, reply_target, src_nick, tgt)) return;
#endif
#ifdef HAS_CMD_REBUILD
if (cmd_rebuild(s, msg, reply_target, src_nick, tgt)) return;
#endif
#ifdef HAS_CMD_RELOAD
if (cmd_reload(s, msg, reply_target, src_nick, tgt)) return;
#endif
#ifdef HAS_CMD_RESTART
if (cmd_restart(s, msg, reply_target, src_nick, tgt)) return;
#endif
#ifdef HAS_CMD_YT
if (cmd_yt(s, msg, reply_target, src_nick)) return;
#endif
#ifdef HAS_CMD_WP
if (cmd_wp(s, msg, reply_target, src_nick)) return;
#endif
#ifdef HAS_CMD_GREETING_OR_CHAT
cmd_greeting_or_chat(s, msg, reply_target, src_nick);
#endif
if (msg[0] == '!' && msg[1] && isalpha((unsigned char)msg[1]))
irc_reply(s, reply_target, src_nick, "Not implemented...");
}
/*
* handle_namreply - Process RPL_NAMREPLY (353) to populate channel op lists.
*
* Parses the names list from the server's NAMREPLY, which contains nicks
* prefixed with status symbols: @ = channel op, ~ = owner, & = admin,
* % = halfop. For each nick with op-like prefix, adds it to the channel's
* op tracking list via chan_add_op(). This is how the bot discovers existing
* ops when it first joins a channel.
*
* Parameters:
* s - The IRC session.
* params - The raw IRC params (nick type #channel :names...).
*
* Returns: void.
*/
static void handle_namreply(Session *s, const char *params) {
(void)s;
char our[32], _chan[64];
const char *p = params;
/* params: "<nick> <type> <chan> :<nicks>" */
if (sscanf(p, "%31s %*s %63s", our, _chan) < 2) return;
p = strchr(p, ':');
if (!p) return;
p++;
char buf[32];
while (*p) {
while (*p == ' ') p++;
if (!*p) break;
int is_op = 0;
while (*p && (*p == '@' || *p == '~' || *p == '&' || *p == '%')) {
if (*p == '@' || *p == '~' || *p == '&') is_op = 1;
p++;
}
int i = 0;
while (*p && *p != ' ') {
if (i < 31) buf[i++] = *p;
p++;
}
buf[i] = 0;
if (is_op && buf[0]) chan_add_op(_chan, buf);
}
}
/*
* handle_mode - Process MODE changes to track channel op additions/removals.
*
* Parses MODE messages like "#chan +o nick" or "#chan -o nick". When +o is
* set, adds the nick to the channel's op list. When -o is set, removes it.
* Only processes channel modes that affect operator status.
*
* Parameters:
* s - The IRC session.
* params - The raw IRC params (channel modes [nick]).
*
* Returns: void.
*/
static void handle_mode(Session *s, const char *params) {
(void)s;
char _chan[64], _modes[32], _nick[32];
if (sscanf(params, "%63s %31s %31s", _chan, _modes, _nick) < 3) return;
if (_chan[0] == '#') {
if (_modes[0] == '+' && strchr(_modes, 'o'))
chan_add_op(_chan, _nick);
else if (_modes[0] == '-' && strchr(_modes, 'o'))
chan_del_op(_chan, _nick);
}
}
/*
* handle_join - Process JOIN messages to register channels and welcome users.
*
* When any user joins a channel, registers the channel in the tracking array
* via chan_get_or_add(). If the joining user is not the bot itself, sends a
* simple welcome message to the channel.
*
* Parameters:
* s - The IRC session.
* params - The raw IRC params (:#channel).
* src_nick - The nick of the user who joined.
*
* Returns: void.
*/
static void handle_join(Session *s, const char *params, const char *src_nick) {
const char *p = params;
if (*p == ':') p++;
char _chan[64];
if (sscanf(p, "%63s", _chan) == 1)
chan_get_or_add(_chan);
if (src_nick && strcasecmp(src_nick, s->nick) != 0) {
char welcome[256];
snprintf(welcome, sizeof welcome, "Welcome, %s! User zynk is an AI driven bot using OpenCode. You can ask the bot whatever you want... have fun! ;)", src_nick);
irc_msg(s, _chan, welcome);
}
}
/*
* handle_part - Process PART messages to clean up channel op tracking.
*
* When a user parts a channel, removes them from that channel's op list
* via chan_del_op(). Prevents stale op entries from accumulating.
*
* Parameters:
* params - The raw IRC params (#channel [message]).
* src_nick - The nick of the user who parted.
*
* Returns: void.
*/
static void handle_part(const char *params, const char *src_nick) {
char _chan[64];
if (sscanf(params, "%63s", _chan) >= 1)
chan_del_op(_chan, src_nick);
seen_update(src_nick, _chan, "left");
}
/*
* handle_kick - Process KICK messages to remove kicked users from op lists.
*
* When a user is kicked from a channel, removes them from that channel's
* op list via chan_del_op(). Prevents tracking a user who is no longer
* in the channel.
*
* Parameters:
* params - The raw IRC params (#channel victim [message]).
*
* Returns: void.
*/
static void handle_kick(const char *params) {
char _chan[64], _victim[32];
if (sscanf(params, "%63s %31s", _chan, _victim) >= 2)
chan_del_op(_chan, _victim);
}
/*
* handle_quit_net - Process QUIT messages to clean up op tracking.
*
* When any user quits the network, removes them from all channel op lists
* via chan_del_op(). Iterates all tracked channels.
*
* Parameters:
* src_nick - The nick of the user who quit.
*
* Returns: void.
*/
static void handle_quit_net(const char *src_nick) {
for (int i = 0; i < nchans; i++)
chan_del_op(chans[i].name, src_nick);
seen_update(src_nick, NULL, "quit");
}
/*
* handle_nick_change - Process NICK changes to update op tracking and bot state.
*
* Extracts the old nick from the message prefix, gets the new nick from
* params, then renames the nick across all channel op lists via
* chan_rename_op(). If the bot itself changed its nick (e.g. after a
* 433 collision), updates s->nick to match. Validates the new nick for
* safety (no CRLF, no control characters).
*
* Parameters:
* s - The IRC session.
* prefix - The IRC prefix containing the old nick (nick!user@host).
* params - The IRC params (:newnick).
*
* Returns: void.
*/
static void handle_nick_change(Session *s, const char *prefix, const char *params) {
char src_nick[64] = "";
const char *ex = strchr(prefix, '!');
size_t nlen = ex ? (size_t)(ex - prefix) : strlen(prefix);
if (nlen >= sizeof src_nick) nlen = sizeof src_nick - 1;
memcpy(src_nick, prefix, nlen); src_nick[nlen] = 0;
const char *newnick = params;
if (*newnick == ':') newnick++;
if (!*newnick || has_crlf(newnick) || has_ctl(newnick)) return;
chan_rename_op(src_nick, newnick);
if (strcasecmp(src_nick, s->nick) == 0) {
strncpy(s->nick, newnick, sizeof s->nick - 1);
s->nick[sizeof s->nick - 1] = 0;
}
}
/*
* handle_error_codes - Handle numeric IRC error codes and ERROR messages.
*
* Handles: 464 (password rejected - shuts down), 433/436 (nick collision -
* appends underscore to nick and retries), and ERROR (logs the message).
* These are server-level errors that require special handling beyond normal
* command dispatch.
*
* Parameters:
* s - The IRC session.
* cmd_buf - The IRC command/numeric as a string.
* params - The IRC params.
*
* Returns: void.
*/
static void handle_error_codes(Session *s, const char *cmd_buf, const char *params) {
if (strcmp(cmd_buf, "464") == 0) {
log_stamp(); fprintf(stderr, CLR_RED "Password rejected" CLR_RESET "\n"); running = 0; return;
}
if (strcmp(cmd_buf, "433") == 0 || strcmp(cmd_buf, "436") == 0) {
char newnick[33];
snprintf(newnick, sizeof newnick, "%s_", s->nick);
irc_nick(s, newnick);
strncpy(s->nick, newnick, sizeof s->nick - 1);
s->nick[sizeof s->nick - 1] = 0;
return;
}
if (strcmp(cmd_buf, "ERROR") == 0) {
const char *m = params;
if (*m == ':') m++;
log_stamp(); fprintf(stderr, CLR_RED "ERROR:" CLR_RESET " %s\n", m);
return;
}
}
/* ---- IRC dispatcher ---- */
/*
* irc_handle - Parse and dispatch a single complete IRC protocol message.
*
* Parses the IRC message format (:prefix COMMAND params :trailing) into
* its components. Handles server keepalives (PING/PONG), connection
* establishment (001), MOTD end (376/422 triggers channel joins), and
* routes numeric replies and commands to their respective handler
* functions. Extracts the source nick from the prefix for PRIVMSG,
* JOIN, PART, KICK, QUIT, and NICK commands. Also handles the code
* restart notification on MOTD end.
*
* Parameters:
* s - The IRC session.
* raw - The complete IRC line (without trailing \r\n).
*
* Returns: void.
*/
static void irc_handle(Session *s, const char *raw) {
if (!raw || !*raw) return;
const char *prefix = NULL, *cmd = raw;
char pfx_buf[512], cmd_buf[64];
pfx_buf[0] = cmd_buf[0] = 0;
if (*raw == ':') {
raw++;
const char *sp = strchr(raw, ' ');
if (!sp) return;
size_t plen = sp - raw;
if (plen >= sizeof pfx_buf) plen = sizeof pfx_buf - 1;
memcpy(pfx_buf, raw, plen); pfx_buf[plen] = 0;
prefix = pfx_buf;
cmd = sp + 1;
while (*cmd == ' ') cmd++;
if (!*cmd) return;
}
const char *params = strchr(cmd, ' ');
if (params) {
size_t clen = params - cmd;
if (clen >= sizeof cmd_buf) clen = sizeof cmd_buf - 1;
memcpy(cmd_buf, cmd, clen); cmd_buf[clen] = 0;
while (*params == ' ') params++;
} else {
size_t cl2 = strlen(cmd);
if (cl2 >= sizeof cmd_buf) cl2 = sizeof cmd_buf - 1;
memcpy(cmd_buf, cmd, cl2); cmd_buf[cl2] = 0;
params = "";
}
if (strcmp(cmd_buf, "PING") == 0) { irc_pong(s, params); return; }
char src_nick[64] = "";
if (prefix) {
const char *ex = strchr(prefix, '!');
size_t nlen = ex ? (size_t)(ex - prefix) : strlen(prefix);
if (nlen >= sizeof src_nick) nlen = sizeof src_nick - 1;
memcpy(src_nick, prefix, nlen); src_nick[nlen] = 0;
current_userhost[0] = 0;
if (ex) {
const char *uh = ex + 1;
size_t uhlen = strlen(uh);
if (uhlen >= sizeof current_userhost) uhlen = sizeof current_userhost - 1;
memcpy(current_userhost, uh, uhlen);
current_userhost[uhlen] = 0;
}
}
if (strcmp(cmd_buf, "001") == 0) {
s->connected = 1;
log_stamp(); fprintf(stderr, CLR_GREEN "Connected to %s" CLR_RESET "\n", s->host);
return;
}
if (strcmp(cmd_buf, "376") == 0 || strcmp(cmd_buf, "422") == 0) {
log_stamp(); fprintf(stderr, CLR_GREEN "MOTD end, joining channels..." CLR_RESET "\n");
char *copy = strdup(s->auto_join);
if (copy) {
char *save;
for (char *tok = strtok_r(copy, ",", &save); tok; tok = strtok_r(NULL, ",", &save)) {
while (*tok == ' ') tok++;
char *end = tok + strlen(tok);
while (end > tok && end[-1] == ' ') end--;
*end = 0;
if (*tok) { irc_join(s, tok); log_stamp(); fprintf(stderr, CLR_GREEN "Joining %s" CLR_RESET "\n", tok); }
}
free(copy);
}
if (s->code_restart) {
s->code_restart = 0;
char *c2 = strdup(s->auto_join);
if (c2) {
char *sv;
for (char *t = strtok_r(c2, ",", &sv); t; t = strtok_r(NULL, ",", &sv)) {
while (*t == ' ') t++;
char *e = t + strlen(t);
while (e > t && e[-1] == ' ') e--;
*e = 0;
if (*t) irc_msg(s, t, "Code update done, I'm back!");
}
free(c2);
}
}
return;
}
if (strcmp(cmd_buf, "PRIVMSG") == 0) { handle_privmsg(s, params, src_nick); return; }
if (strcmp(cmd_buf, "353") == 0) { handle_namreply(s, params); return; }
if (strcmp(cmd_buf, "MODE") == 0) { handle_mode(s, params); return; }
if (strcmp(cmd_buf, "JOIN") == 0 && prefix) { handle_join(s, params, src_nick); return; }
if (strcmp(cmd_buf, "PART") == 0 && prefix) { handle_part(params, src_nick); return; }
if (strcmp(cmd_buf, "KICK") == 0 && prefix) { handle_kick(params); return; }
if (strcmp(cmd_buf, "QUIT") == 0 && prefix) { handle_quit_net(src_nick); return; }
if (strcmp(cmd_buf, "NICK") == 0 && prefix) { handle_nick_change(s, prefix, params); return; }
handle_error_codes(s, cmd_buf, params);
}
/* ---- IRC line parser ---- */
/*
* irc_feed - Buffer incoming data and dispatch complete IRC lines.
*
* Accumulates raw bytes into the session's read buffer (s->rbuf) until
* a newline is found. On \n, NUL-terminates the buffer and passes the
* complete line to irc_handle(). Skips \r characters. Prevents buffer
* overflow by checking against RBUF_SZ. This implements the line-based
* protocol parsing required by IRC.
*
* Parameters:
* s - The IRC session with rbuf and rlen state.
* data - Raw bytes from the network read.
* len - Number of bytes in data.
*
* Returns: void.
*/
static void irc_feed(Session *s, const char *data, int len) {
for (int i = 0; i < len; i++) {
char c = data[i];
if (c == '\r') continue;
if (c == '\n') {
if (!s->dropping_line) {
if (s->poll_lines_left <= 0) {
s->rlen = 0;
s->dropping_line = 1;
continue;
}
s->rbuf[s->rlen] = 0;
if (s->rlen > 0) {
irc_handle(s, s->rbuf);
s->poll_lines_left--;
}
}
s->rlen = 0;
s->dropping_line = 0;
} else {
if (!s->dropping_line && s->rlen < RBUF_SZ - 1)
s->rbuf[s->rlen++] = c;
else if (s->rlen >= RBUF_SZ - 1) {
s->rlen = 0;
s->dropping_line = 1;
}
}
}
}
#endif