zynk/cmd_time.h
hanez 3b97b4d902 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().
2026-07-25 20:37:07 +02:00

179 lines
6.5 KiB
C

#ifndef CMD_TIME_H
#define CMD_TIME_H
#include "zynk.h"
typedef struct { const char *name; int off; int dst_off; int dst_north; } TzEntry;
static const TzEntry tz_table[] = {
{"UTC", 0, 0, 0},
{"GMT", 0, 0, 0},
{"EST", -5*3600, -5*3600, 0},
{"CST", -6*3600, -6*3600, 0},
{"MST", -7*3600, -7*3600, 0},
{"PST", -8*3600, -8*3600, 0},
{"HST", -10*3600, -10*3600, 0},
{"US/Eastern", -5*3600, -4*3600, 1},
{"US/Central", -6*3600, -5*3600, 1},
{"US/Mountain", -7*3600, -6*3600, 1},
{"US/Pacific", -8*3600, -7*3600, 1},
{"US/Alaska", -9*3600, -8*3600, 1},
{"US/Honolulu", -10*3600, -10*3600, 0},
{"Europe/London", 0, 1*3600, 1},
{"Europe/Berlin", 1*3600, 2*3600, 1},
{"Europe/Paris", 1*3600, 2*3600, 1},
{"Europe/Moscow", 3*3600, 3*3600, 0},
{"Europe/Istanbul", 3*3600, 3*3600, 0},
{"Asia/Tokyo", 9*3600, 9*3600, 0},
{"Asia/Shanghai", 8*3600, 8*3600, 0},
{"Asia/Kolkata", 19800, 19800, 0},
{"Asia/Dubai", 4*3600, 4*3600, 0},
{"Asia/Singapore", 8*3600, 8*3600, 0},
{"Asia/Seoul", 9*3600, 9*3600, 0},
{"Asia/Hong_Kong", 8*3600, 8*3600, 0},
{"Asia/Taipei", 8*3600, 8*3600, 0},
{"Australia/Sydney", 10*3600, 11*3600, 2},
{"Australia/Melbourne", 10*3600, 11*3600, 2},
{"Australia/Brisbane", 10*3600, 10*3600, 0},
{"Australia/Perth", 8*3600, 8*3600, 0},
{"Australia/Adelaide", 930*60, 1030*60, 2},
{"Australia/Darwin", 930*60, 930*60, 0},
{"Pacific/Auckland", 12*3600, 13*3600, 2},
{"Pacific/Honolulu", -10*3600, -10*3600, 0},
{"Pacific/Fiji", 12*3600, 13*3600, 2},
{"Canada/Toronto", -5*3600, -4*3600, 1},
{"Canada/Vancouver", -8*3600, -7*3600, 1},
{"America/Sao_Paulo", -3*3600, -2*3600, 2},
{"America/Mexico_City", -6*3600, -5*3600, 1},
{"America/Argentina/Buenos_Aires", -3*3600, -3*3600, 0},
{"Africa/Cairo", 2*3600, 2*3600, 0},
{"Africa/Johannesburg", 2*3600, 2*3600, 0},
{"Africa/Lagos", 1*3600, 1*3600, 0},
{"Africa/Nairobi", 3*3600, 3*3600, 0},
{"Atlantic/Reykjavik", 0, 0, 0},
};
/*
* tz_lookup - Look up a timezone entry by name.
*
* Searches the built-in tz_table for a matching timezone name
* (case-insensitive). Returns a pointer to the entry if found, or NULL.
*
* Parameters:
* name - The timezone name to look up (e.g. "US/Eastern", "UTC").
*
* Returns: Pointer to the matching TzEntry, or NULL if not found.
*/
static const TzEntry *tz_lookup(const char *name) {
for (size_t i = 0; i < sizeof tz_table / sizeof tz_table[0]; i++)
if (strcasecmp(tz_table[i].name, name) == 0) return &tz_table[i];
return NULL;
}
/*
* tz_format - Format a timestamp with a UTC offset.
*
* Converts an epoch timestamp to a formatted date/time string using
* the given UTC offset. Writes the result as "YYYY-MM-DD HH:MM:SS" into buf.
*
* Parameters:
* epoch - The epoch timestamp to format.
* offset - UTC offset in seconds to apply before formatting.
* tzname - The timezone name (unused, reserved for future use).
* is_dst - Whether DST is in effect (unused, reserved for future use).
* buf - Output buffer for the formatted string.
* buflen - Size of the output buffer.
*
* Returns: void.
*/
static void tz_format(time_t epoch, int offset, const char *tzname, int is_dst, char *buf, size_t buflen) {
(void)tzname; (void)is_dst;
time_t local = epoch + offset;
struct tm tm;
gmtime_r(&local, &tm);
snprintf(buf, buflen, "%04d-%02d-%02d %02d:%02d:%02d",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec);
}
/*
* cmd_time - Handle the "!time [timezone]" command.
*
* Without arguments, returns the bot's local time. With a timezone
* argument, first tries the system setenv("TZ")/localtime_r() approach.
* If the system returns UTC for a non-UTC timezone (indicating missing
* tzdata), falls back to the built-in tz_table with hemisphere-aware
* DST rules. Rate-limited.
*
* Parameters:
* s - The IRC session.
* msg - The raw IRC message text.
* reply_target - Channel or nick to send the response to.
* src_nick - The nick of the user who issued the command.
*
* Returns: 1 if the command was matched, 0 otherwise.
*/
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, current_userhost) < 0) return 1;
const char *rest = msg[5] == ' ' ? msg + 6 : "";
while (*rest == ' ') rest++;
if (*rest) {
char tzname[64];
size_t tl = 0;
for (const char *p = rest; *p && tl < sizeof tzname - 1 && *p != ' '; p++)
tzname[tl++] = *p;
tzname[tl] = 0;
time_t now = time(NULL);
struct tm utc_tm;
gmtime_r(&now, &utc_tm);
char *old_tz = getenv("TZ");
char old_tz_buf[256] = "";
if (old_tz) { strncpy(old_tz_buf, old_tz, sizeof old_tz_buf - 1); old_tz_buf[sizeof old_tz_buf - 1] = 0; }
setenv("TZ", tzname, 1);
tzset();
struct tm local_tm;
localtime_r(&now, &local_tm);
if (old_tz_buf[0]) setenv("TZ", old_tz_buf, 1); else unsetenv("TZ");
tzset();
int sys_offset = (local_tm.tm_hour - utc_tm.tm_hour) * 3600 + (local_tm.tm_min - utc_tm.tm_min) * 60;
if (sys_offset > 12 * 3600) sys_offset -= 24 * 3600;
if (sys_offset < -12 * 3600) sys_offset += 24 * 3600;
int system_works = (sys_offset != 0 || strcasecmp(tzname, "UTC") == 0 || strcasecmp(tzname, "GMT") == 0);
if (system_works) {
char buf[64];
strftime(buf, sizeof buf, "%Y-%m-%d %H:%M:%S", &local_tm);
irc_reply(s, reply_target, src_nick, buf);
} else {
const TzEntry *tz = tz_lookup(tzname);
if (tz) {
int off = tz->off;
if (tz->dst_north && tz->off != tz->dst_off) {
int mon = utc_tm.tm_mon + 1;
if (tz->dst_north == 1)
off = (mon >= 4 && mon <= 10) ? tz->dst_off : tz->off;
else
off = (mon >= 10 || mon <= 3) ? tz->dst_off : tz->off;
}
char buf[128];
tz_format(now, off, tzname, off != tz->off, buf, sizeof buf);
irc_reply(s, reply_target, src_nick, buf);
} else {
char resp[256];
snprintf(resp, sizeof resp, "Unknown timezone: %s", tzname);
irc_reply(s, reply_target, src_nick, resp);
}
}
} else {
time_t now = time(NULL);
struct tm *tm = localtime(&now);
char buf[64];
strftime(buf, sizeof buf, "%Y-%m-%d %H:%M:%S", tm);
irc_reply(s, reply_target, src_nick, buf);
}
log_stamp(); fprintf(stderr, CLR_YELLOW "TIME from %s in %s" CLR_RESET "\n", src_nick, reply_target);
return 1;
}
#endif