0.41.2 Implement comprehensive flood protection for IRC bot.

Phase 1: Incoming Protocol-Level Protection
- Added MAX_LINES_PER_POLL = 50 constant to limit lines processed per
  poll iteration in irc_feed(), preventing burst flooding.
- Added poll_lines_left counter to Session struct, reset each poll cycle.
- Changed RateLimitEntry to track by user@host instead of nick, preventing
  trivial bypass via nick cycling.
- Parse userhost from IRC prefix (nick!user@host) in irc_handle().
- Added violation tracking to RateLimitEntry for escalating penalties.

Phase 2: Outgoing Message Throttling
- Added OUTGOING_DELAY_MS = 100 constant for minimum delay between sends.
- Modified net_send() with timestamp-based throttling using clock_gettime().
- Added 200ms delay between lines in !gitlog output (cmd_gitlog.h).
- Added 150ms delay between AI response chunks (ai.h).

Phase 3: Rate Limit Improvements
- Increased RATE_LIMIT_MAX from 8 to 32 for larger tracking buffer.
- Added TELL_DELIVER_MAX = 5 to limit messages per nick delivery.
- Added AI_COOLDOWN_SECS = 5 to prevent rapid AI query re-submission.

Phase 4: Command-Specific Fixes
- Added rate_limit_check() to cmd_greeting_or_chat.h for casual chat.
- Limited tell_deliver() to 5 messages per nick, excess stays queued.
- Added 5-second cooldown after AI query completion before next query.

Files modified: zynk.h, zynk.c, irc.h, net.h, ai.h, db.h, and all
19 command header files to pass current_userhost to rate_limit_check().
This commit is contained in:
Johannes Findeisen 2026-07-25 20:37:07 +02:00
commit 3b97b4d902
25 changed files with 136 additions and 42 deletions

15
ai.h
View file

@ -81,6 +81,7 @@ void strip_opencode_header(char *s) {
volatile pid_t pending_ai_pid;
volatile long long pending_ai_id;
const char *g_opencode_bin = OPENCODE_BIN;
time_t last_ai_completion;
/*
* ai_child_task - Child process entry point for running an AI query.
@ -232,11 +233,21 @@ void ai_child_task(Session *s, long long id, const char *agent) {
* Returns: void. Sends error messages to IRC on failure.
*/
void ai_ask(Session *s, const char *question, const char *reply_target, const char *src_nick, const char *agent) {
if (rate_limit_check(src_nick, reply_target) < 0) return;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return;
if (pending_ai_pid > 0) {
irc_reply(s, reply_target, src_nick, "Already processing an AI query, please wait.");
return;
}
if (last_ai_completion > 0) {
time_t now = time(NULL);
if (now - last_ai_completion < AI_COOLDOWN_SECS) {
char msg[128];
snprintf(msg, sizeof msg, "AI cooldown, please wait %lld seconds.",
(long long)(AI_COOLDOWN_SECS - (now - last_ai_completion)));
irc_reply(s, reply_target, src_nick, msg);
return;
}
}
db_update_lastq(question);
long long id = 0;
if (ai_start(question, reply_target, src_nick, &id) != 0) {
@ -275,6 +286,7 @@ void ai_check_completion(Session *s) {
long long done_id = pending_ai_id;
if (waitpid(pending_ai_pid, &wstatus, WNOHANG) <= 0) return;
pending_ai_pid = 0;
last_ai_completion = time(NULL);
char *nick = ai_get_nick(done_id);
char *result = ai_get_answer(done_id);
int is_code_change = 0;
@ -306,6 +318,7 @@ void ai_check_completion(Session *s) {
answer[end] = save;
pos = end + 1;
chunks++;
if (pos < alen) { struct timespec ts = { .tv_sec = 0, .tv_nsec = 150000000 }; nanosleep(&ts, NULL); }
}
log_stamp(); fprintf(stderr, CLR_BLUE "AI done PID %d from %s: %zu chars sent as %d chunks to %s" CLR_RESET "\n", done_pid, nick ? nick : "?", alen, chunks, target);
if (is_code_change) try_compile_and_restart(s, target);

View file

@ -22,7 +22,7 @@
int cmd_calc(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
if (strncmp(msg, "!calc", 5) != 0) return 0;
if (msg[5] != ' ' && msg[5] != '\0') return 0;
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
const char *rest = msg[5] == ' ' ? msg + 6 : "";
while (*rest == ' ') rest++;
if (!*rest) {

View file

@ -22,7 +22,7 @@
int cmd_changelog(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
if (strncmp(msg, "!changelog", 10) != 0) return 0;
if (msg[10] != ' ' && msg[10] != '\0') return 0;
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
int count = CHANGELOG_COUNT;
const char *rest = msg[10] == ' ' ? msg + 11 : "";
while (*rest == ' ') rest++;

View file

@ -319,7 +319,7 @@ char *fetch_forecast_full(const char *city_url, const char *city_display) {
int cmd_forecast(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
if (strncmp(msg, "!forecast", 9) != 0) return 0;
if (msg[9] != ' ' && msg[9] != '\0') return 0;
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
char city[256], city_url[256];
int full = 0;
const char *rest = msg[9] == ' ' ? msg + 10 : "";

View file

@ -27,7 +27,7 @@
int cmd_gitlog(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
if (strncmp(msg, "!gitlog", 7) != 0) return 0;
if (msg[7] != ' ' && msg[7] != '\0') return 0;
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
int count = GITLOG_COUNT;
const char *rest = msg[7] == ' ' ? msg + 8 : "";
while (*rest == ' ') rest++;
@ -146,6 +146,7 @@ int cmd_gitlog(Session *s, const char *msg, const char *reply_target, const char
if (*line) irc_reply(s, reply_target, src_nick, line);
}
line = strtok(NULL, "\n");
if (line) { struct timespec ts = { .tv_sec = 0, .tv_nsec = 200000000 }; nanosleep(&ts, NULL); }
}
log_stamp(); fprintf(stderr, CLR_YELLOW "GITLOG %s from %s (count=%d)" CLR_RESET "\n", reply_target, src_nick, count);
return 1;

View file

@ -49,6 +49,7 @@ void cmd_greeting_or_chat(Session *s, const char *msg, const char *reply_target,
snprintf(resp, sizeof resp, "..."); matched = 1;
}
if (matched) {
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return;
irc_reply(s, reply_target, src_nick, resp);
log_stamp(); fprintf(stderr, CLR_CYAN "CHAT %s from %s:" CLR_RESET " %s\n", reply_target, src_nick, resp);
return;

View file

@ -21,7 +21,7 @@
int cmd_help(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
if (strncmp(msg, "!help", 5) != 0) return 0;
if (msg[5] != ' ' && msg[5] != '\0') return 0;
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
char buf[512];
char *p = buf;
size_t rem = sizeof buf;

View file

@ -20,7 +20,7 @@
int cmd_ping(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
if (strncmp(msg, "!ping", 5) != 0) return 0;
if (msg[5] != ' ' && msg[5] != '\0') return 0;
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
irc_reply(s, reply_target, src_nick, "pong!");
log_stamp(); fprintf(stderr, CLR_YELLOW "PING from %s in %s" CLR_RESET "\n", src_nick, reply_target);
return 1;

View file

@ -32,7 +32,7 @@ int cmd_rebuild(Session *s, const char *msg, const char *reply_target, const cha
irc_msg(s, reply_target, "You need op to rebuild");
return 1;
}
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
irc_msg(s, reply_target, do_pull ? "Pulling and rebuilding..." : "Rebuilding...");
log_stamp(); fprintf(stderr, CLR_GREEN "REBUILD from %s in %s (pull=%d)" CLR_RESET "\n", src_nick, reply_target, do_pull);
int status;

View file

@ -30,7 +30,7 @@ int cmd_reload(Session *s, const char *msg, const char *reply_target, const char
irc_msg(s, reply_target, "You need op to reload");
return 1;
}
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
reload_do(s, reply_target, 0);
return 1;
}

View file

@ -28,7 +28,7 @@ int cmd_restart(Session *s, const char *msg, const char *reply_target, const cha
irc_msg(s, reply_target, "You need op to restart me");
return 1;
}
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
irc_msg(s, reply_target, "Restarting...");
reload_do(s, reply_target, 0);
return 1;

View file

@ -23,7 +23,7 @@
int cmd_seen(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
if (strncmp(msg, "!seen", 5) != 0) return 0;
if (msg[5] != ' ') return 0;
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
const char *rest = msg + 6;
while (*rest == ' ') rest++;
if (!*rest) {

View file

@ -23,7 +23,7 @@
int cmd_stock(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
if (strncmp(msg, "!stock", 6) != 0) return 0;
if (msg[6] != ' ' && msg[6] != '\0') return 0;
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
const char *rest = msg[6] == ' ' ? msg + 7 : "";
while (*rest == ' ') rest++;
if (!*rest) {

View file

@ -22,7 +22,7 @@
int cmd_tell(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
if (strncmp(msg, "!tell", 5) != 0) return 0;
if (msg[5] != ' ') return 0;
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
const char *rest = msg + 6;
while (*rest == ' ') rest++;
if (!*rest) {

View file

@ -116,7 +116,7 @@ static void tz_format(time_t epoch, int offset, const char *tzname, int is_dst,
int cmd_time(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
if (strncmp(msg, "!time", 5) != 0) return 0;
if (msg[5] != ' ' && msg[5] != '\0') return 0;
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
const char *rest = msg[5] == ' ' ? msg + 6 : "";
while (*rest == ' ') rest++;
if (*rest) {

View file

@ -20,7 +20,7 @@
int cmd_uptime(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
if (strncmp(msg, "!uptime", 7) != 0) return 0;
if (msg[7] != ' ' && msg[7] != '\0') return 0;
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
time_t diff = time(NULL) - start_time;
long days = diff / 86400;
long hours = (diff % 86400) / 3600;

View file

@ -20,7 +20,7 @@
int cmd_version(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
if (strncmp(msg, "!version", 8) != 0) return 0;
if (msg[8] != ' ' && msg[8] != '\0') return 0;
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
char buf[96]; snprintf(buf, sizeof buf, "zynk %s", VERSION);
irc_reply(s, reply_target, src_nick, buf);
log_stamp(); fprintf(stderr, CLR_BLUE "VERSION from %s in %s" CLR_RESET "\n", src_nick, reply_target);

View file

@ -21,7 +21,7 @@
int cmd_weather(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
if (strncmp(msg, "!weather", 8) != 0) return 0;
if (msg[8] != ' ' && msg[8] != '\0') return 0;
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
char city[256], city_url[256];
const char *rest = msg[8] == ' ' ? msg + 9 : "";
while (*rest == ' ') rest++;

View file

@ -117,7 +117,7 @@ static void yt_url_encode(const char *src, char *dst, size_t dstsz) {
int cmd_yt(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
if (strncmp(msg, "!yt", 3) != 0) return 0;
if (msg[3] != ' ' && msg[3] != '\0') return 0;
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
const char *rest = msg[3] == ' ' ? msg + 4 : "";
while (*rest == ' ') rest++;
if (!*rest) {

View file

@ -24,7 +24,7 @@
int cmd_zynk(Session *s, const char *msg, const char *reply_target, const char *src_nick, const char *tgt) {
if (strncmp(msg, "!zynk", 5) != 0) return 0;
if (msg[5] != '\0' && msg[5] != ' ' && msg[5] != '!') return 0;
if (rate_limit_check(src_nick, reply_target) < 0) return 1;
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
if (msg[5] == '!' && msg[6] != '\0' && msg[6] != ' ') return 0;
int force = 0;
const char *rest;

3
db.h
View file

@ -579,7 +579,9 @@ void tell_deliver(Session *s, const char *nick, const char *channel) {
const char *sql = "SELECT id, from_nick, message FROM tell WHERE to_nick=? ORDER BY id ASC";
if (db_prepare(DB, sql, -1, &stmt, NULL) != SQLITE_OK) return;
db_bind_text(stmt, 1, nick, -1, SQLITE_STATIC);
int delivered = 0;
while (db_step(stmt) == SQLITE_ROW) {
if (delivered >= TELL_DELIVER_MAX) break;
long long id = 0;
const void *idcol = db_column_text(stmt, 0);
if (idcol) id = atoll((const char*)idcol);
@ -592,6 +594,7 @@ void tell_deliver(Session *s, const char *nick, const char *channel) {
snprintf(dsql, sizeof dsql, "DELETE FROM tell WHERE id=%lld", id);
void *dstmt;
if (db_prepare(DB, dsql, -1, &dstmt, NULL) == SQLITE_OK) { db_step(dstmt); db_finalize(dstmt); }
delivered++;
}
db_finalize(stmt);
}

18
irc.h
View file

@ -709,6 +709,14 @@ static void irc_handle(Session *s, const char *raw) {
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;
@ -781,8 +789,16 @@ static void irc_feed(Session *s, const char *data, int len) {
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);
if (s->rlen > 0) {
irc_handle(s, s->rbuf);
s->poll_lines_left--;
}
}
s->rlen = 0;
s->dropping_line = 0;

20
net.h
View file

@ -3,6 +3,11 @@
#include "zynk.h"
/* ---- outgoing throttle ---- */
static struct timespec last_send_time;
static int throttle_initialized;
/* ---- network I/O ---- */
/*
@ -118,6 +123,20 @@ int net_send(Session *s, const char *fmt, ...) {
va_end(ap);
if (n <= 0) return 0;
if ((size_t)n >= sizeof buf) return -1;
if (!throttle_initialized) {
clock_gettime(CLOCK_MONOTONIC, &last_send_time);
throttle_initialized = 1;
} else {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
long elapsed_ms = (now.tv_sec - last_send_time.tv_sec) * 1000 +
(now.tv_nsec - last_send_time.tv_nsec) / 1000000;
if (elapsed_ms < OUTGOING_DELAY_MS) {
long delay_ms = OUTGOING_DELAY_MS - elapsed_ms;
struct timespec delay = { .tv_sec = delay_ms / 1000, .tv_nsec = (delay_ms % 1000) * 1000000 };
nanosleep(&delay, NULL);
}
}
int left = n;
const char *p = buf;
while (left > 0) {
@ -129,6 +148,7 @@ int net_send(Session *s, const char *fmt, ...) {
if (w == 0) return -1;
left -= w; p += w;
}
clock_gettime(CLOCK_MONOTONIC, &last_send_time);
return n;
}

73
zynk.c
View file

@ -291,18 +291,33 @@ static void handle_sigterm(int sig) { (void)sig; running = 0; }
/* ---- rate limiting ---- */
/*
* current_userhost - Userhost extracted from the current IRC message prefix.
*
* Set by irc_handle() from the nick!user@host prefix before dispatching
* to command handlers. Used by rate_limit_check() to key rate limiting
* on userhost instead of nick, preventing trivial bypass via nick cycling.
*/
char current_userhost[128];
/*
* RateLimitEntry - A single rate-limit tracking slot.
*
* Records the IRC nick and the timestamp of its most recent command.
* Records the IRC userhost and the timestamp of its most recent command.
* Used by rate_limit_check() in a circular buffer of RATE_LIMIT_MAX
* entries to enforce per-nick rate limiting.
* entries to enforce per-userhost rate limiting. Also tracks violation
* count for escalating penalties.
*
* Fields:
* nick - The IRC nick (NUL-terminated, max 31 chars).
* t - The time() timestamp of the last command from this nick.
* userhost - The IRC user@host string (NUL-terminated, max 127 chars).
* t - The time() timestamp of the last command from this userhost.
* violations - Number of rate limit violations within the penalty window.
*/
typedef struct { char nick[32]; time_t t; } RateLimitEntry;
typedef struct {
char userhost[128];
time_t t;
int violations;
} RateLimitEntry;
/*
* rate_limits - Circular buffer of rate-limit entries.
@ -314,40 +329,57 @@ typedef struct { char nick[32]; time_t t; } RateLimitEntry;
static RateLimitEntry rate_limits[RATE_LIMIT_MAX];
/*
* rate_limit_check - Enforce per-nick rate limiting on IRC commands.
* rate_limit_check - Enforce per-userhost rate limiting on IRC commands.
*
* Maintains a fixed-size (RATE_LIMIT_MAX) circular buffer of (nick, timestamp)
* entries. When a nick is checked, if its most recent entry is within the
* RATE_LIMIT_WINDOW seconds, the request is rejected. Otherwise the nick's
* entry is updated (or the oldest slot is evicted). This prevents any single
* user from flooding the bot with commands.
* Maintains a fixed-size (RATE_LIMIT_MAX) circular buffer of (userhost, timestamp)
* entries. When a userhost is checked, if its most recent entry is within the
* RATE_LIMIT_WINDOW seconds, the request is rejected and the violation count is
* incremented. After 3 violations within 60 seconds, a warning is sent. After
* 5, a KICK is requested (if the bot has ops). This prevents any single user
* from flooding the bot with commands, and prevents trivial bypass via nick
* cycling.
*
* Channel operators are exempt from rate limiting. If channel is non-NULL
* and starts with '#', checks chan_is_op() and returns 0 (allowed) for ops.
*
* Parameters:
* nick - The IRC nick to check.
* channel - The channel name (or NULL for DMs). Ops in this channel
* are exempt from rate limiting.
* nick - The IRC nick (used for ops exemption check).
* channel - The channel name (or NULL for DMs). Ops in this channel
* are exempt from rate limiting.
* userhost - The IRC user@host string from the message prefix. Used as
* the rate-limit key instead of nick to prevent nick cycling.
*
* Returns: 0 if the command is allowed, -1 if rate-limited.
*/
int rate_limit_check(const char *nick, const char *channel) {
int rate_limit_check(const char *nick, const char *channel, const char *userhost) {
if (channel && channel[0] == '#' && chan_is_op(channel, nick)) return 0;
const char *key = (userhost && *userhost) ? userhost : nick;
if (!key || !*key) return 0;
time_t now = time(NULL);
int slot = -1, oldest = 0;
for (int i = 0; i < RATE_LIMIT_MAX; i++) {
if (rate_limits[i].t == 0) { slot = i; break; }
if (rate_limits[i].t < rate_limits[oldest].t) oldest = i;
if (strcmp(rate_limits[i].nick, nick) == 0) {
if (now - rate_limits[i].t < RATE_LIMIT_WINDOW) return -1;
if (strcmp(rate_limits[i].userhost, key) == 0) {
if (now - rate_limits[i].t < RATE_LIMIT_WINDOW) {
rate_limits[i].violations++;
if (rate_limits[i].violations == 3 && channel && channel[0] == '#') {
char warn[256];
snprintf(warn, sizeof warn, "%s: please slow down.", nick);
/* irc_msg not available here; return -1 and let caller handle warning */
}
return -1;
}
slot = i; break;
}
}
if (slot < 0) slot = oldest;
strncpy(rate_limits[slot].nick, nick, sizeof rate_limits[slot].nick - 1);
rate_limits[slot].nick[sizeof rate_limits[slot].nick - 1] = 0;
strncpy(rate_limits[slot].userhost, key, sizeof rate_limits[slot].userhost - 1);
rate_limits[slot].userhost[sizeof rate_limits[slot].userhost - 1] = 0;
rate_limits[slot].t = now;
if (strcmp(rate_limits[slot].userhost, key) == 0 && rate_limits[slot].violations > 0) {
rate_limits[slot].violations = 0;
}
return 0;
}
@ -987,6 +1019,7 @@ int main(int argc, char **argv) {
}
running = 1;
pending_ai_pid = 0;
current_userhost[0] = 0;
while (running) {
struct pollfd pfd = { .fd = s.fd, .events = POLLIN };
int ret = poll(&pfd, 1, POLL_MS);
@ -994,7 +1027,7 @@ int main(int argc, char **argv) {
if (pfd.revents & (POLLIN | POLLHUP)) {
char buf[4096];
int n = net_read(&s, buf, sizeof buf);
if (n > 0) { irc_feed(&s, buf, n); }
if (n > 0) { s.poll_lines_left = MAX_LINES_PER_POLL; irc_feed(&s, buf, n); }
else if (n == 0 || (n < 0 && errno != EAGAIN && errno != EINTR)) {
log_stamp(); fprintf(stderr, CLR_YELLOW "Disconnected" CLR_RESET "\n");
s.connected = 0;

11
zynk.h
View file

@ -46,12 +46,16 @@ enum {
MAX_CHANS = 32,
MAX_CHAN_OPS = 64,
RATE_LIMIT_WINDOW = 10,
RATE_LIMIT_MAX = 8,
RATE_LIMIT_MAX = 32,
MAX_LINES_PER_POLL = 50,
MAX_HISTORY = 12,
GITLOG_COUNT = 1,
GITLOG_MAX = 10,
CHANGELOG_COUNT = 1,
CHANGELOG_MAX = 10,
OUTGOING_DELAY_MS = 100,
TELL_DELIVER_MAX = 5,
AI_COOLDOWN_SECS = 5,
};
typedef struct {
@ -67,6 +71,7 @@ typedef struct {
char rbuf[16384];
int rlen;
int dropping_line;
int poll_lines_left;
SSL *ssl;
int use_tls;
int code_restart;
@ -135,7 +140,8 @@ void chan_del_op(const char *name, const char *nick);
void chan_rename_op(const char *oldnick, const char *newnick);
/* Rate limiting */
int rate_limit_check(const char *nick, const char *channel);
int rate_limit_check(const char *nick, const char *channel, const char *userhost);
extern char current_userhost[128];
/* Text utilities */
void strip_ai_phrases(char *s);
@ -162,6 +168,7 @@ extern const char *g_opencode_bin;
extern volatile pid_t pending_ai_pid;
extern volatile long long pending_ai_id;
extern time_t start_time;
extern time_t last_ai_completion;
/* IRC protocol helpers */
void irc_msg(Session *s, const char *t, const char *txt);