zynk/cmd_forecast.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

376 lines
14 KiB
C

#ifndef CMD_FORECAST_H
#define CMD_FORECAST_H
#include "zynk.h"
/*
* parse_forecast_day - Parse a single day object from the wttr.in JSON.
*
* Extracts date, avg temperature, and weather condition from one day
* object within the "weather" array. Used by both fetch_forecast() and
* fetch_forecast_full().
*
* Parameters:
* p - Pointer to the start of the day JSON object.
* obj_end - Pointer to one past the end of the day object.
* date_out - Buffer to receive the parsed date string (YYYY-MM-DD).
* temp_out - Buffer to receive the average temperature.
* cond_out - Buffer to receive the weather condition string.
*
* Returns: 1 on success, 0 on failure.
*/
static int parse_forecast_day(const char *p, const char *obj_end,
char *date_out, size_t date_sz,
char *temp_out, size_t temp_sz,
char *cond_out, size_t cond_sz) {
date_out[0] = temp_out[0] = cond_out[0] = 0;
const char *db = strstr(p, "\"date\": \"");
if (db && db < obj_end) { db += 9; const char *dq = strchr(db, '"'); if (dq && dq < obj_end) { size_t l = dq - db; if (l >= date_sz) l = date_sz - 1; memcpy(date_out, db, l); date_out[l] = 0; } }
const char *tb = strstr(p, "\"avgtempC\": \"");
if (tb && tb < obj_end) { tb += 13; const char *tq = strchr(tb, '"'); if (tq && tq < obj_end) { size_t l = tq - tb; if (l >= temp_sz) l = temp_sz - 1; memcpy(temp_out, tb, l); temp_out[l] = 0; } }
const char *hb = strstr(p, "\"hourly\"");
if (hb && hb < obj_end) {
hb = strchr(hb, '[');
if (hb && hb < obj_end) {
hb++;
const char *vb = strstr(hb, "\"value\": \"");
if (vb && vb < obj_end) { vb += 9; const char *vq = strchr(vb, '"'); if (vq && vb < obj_end) { size_t l = vq - vb; if (l >= cond_sz) l = cond_sz - 1; memcpy(cond_out, vb, l); cond_out[l] = 0; while (l > 0 && cond_out[l-1] == ' ') cond_out[--l] = 0; } }
}
}
return (date_out[0] != 0) ? 1 : 0;
}
/*
* find_day_obj - Find the i-th day object in the weather JSON array.
*
* Scans the JSON string for the i-th '{' ... '}' block at the top level
* of the "weather" array. Sets obj_end to one past the closing '}'.
*
* Parameters:
* p - Pointer into the JSON string (start of the weather array).
* idx - Zero-based index of the day object to find (0, 1, or 2).
* obj_end - Output pointer set to one past the closing '}' of the
* matched day object.
*
* Returns: Pointer to the opening '{' of the matched day object, or NULL
* if fewer than idx+1 day objects exist.
*/
static const char *find_day_obj(const char *p, int idx, const char **obj_end) {
int count = 0;
while (*p) {
if (*p == '{') {
if (count == idx) {
*obj_end = p + 1;
int depth = 1;
while (**obj_end && depth > 0) { if (**obj_end == '{') depth++; else if (**obj_end == '}') depth--; (*obj_end)++; }
return p;
}
count++;
}
p++;
}
return NULL;
}
/*
* to_24h - Convert a 12-hour AM/PM time string to 24-hour format.
*
* Parses a time string in "HH:MM AM" or "HH:MM PM" format (as returned
* by wttr.in's astronomy fields) and overwrites the buffer with the
* equivalent 24-hour "HH:MM" string. Handles midnight (12 AM -> 00)
* and noon (12 PM -> 12) correctly. If the string is too short or
* does not end with a recognized AM/PM suffix, the buffer is left
* unchanged.
*
* Parameters:
* buf - Buffer containing the null-terminated 12-hour time string.
* Modified in place on success.
* sz - Size of the buffer.
*/
static void to_24h(char *buf, size_t sz) {
size_t len = strlen(buf);
if (len < 5) return;
char mm[4];
memcpy(mm, buf + 3, 3);
const char *ampm = buf + len - 2;
if ((ampm[0] == 'A' || ampm[0] == 'a') && (ampm[1] == 'M' || ampm[1] == 'm')) {
int h = (buf[0] - '0') * 10 + (buf[1] - '0');
if (h == 12) h = 0;
snprintf(buf, sz, "%02d:%s", h, mm);
} else if ((ampm[0] == 'P' || ampm[0] == 'p') && (ampm[1] == 'M' || ampm[1] == 'm')) {
int h = (buf[0] - '0') * 10 + (buf[1] - '0');
if (h != 12) h += 12;
snprintf(buf, sz, "%02d:%s", h, mm);
}
}
/*
* extract_astronomy - Extract sunrise/sunset/moonrise/moonset from a day object.
*
* Parameters:
* p, obj_end - The day object boundaries.
* sunrise, sunset - Buffers for sunrise/sunset strings.
* moonrise, moonset - Buffers for moonrise/moonset strings.
*/
static void extract_astronomy(const char *p, const char *obj_end,
char *sunrise, size_t sr_sz,
char *sunset, size_t ss_sz,
char *moonrise, size_t mr_sz,
char *moonset, size_t ms_sz) {
sunrise[0] = sunset[0] = moonrise[0] = moonset[0] = 0;
const char *ab = strstr(p, "\"astronomy\"");
if (!ab || ab >= obj_end) return;
ab = strchr(ab, '[');
if (!ab || ab >= obj_end) return;
ab++;
const char *ae = ab;
int depth = 1;
while (*ae && depth > 0) { if (*ae == '[') depth++; else if (*ae == ']') depth--; ae++; }
const char *fields[][2] = {
{ "\"sunrise\": \"", NULL }, { "\"sunset\": \"", NULL },
{ "\"moonrise\": \"", NULL }, { "\"moonset\": \"", NULL },
};
size_t sizes[] = { sr_sz, ss_sz, mr_sz, ms_sz };
char *dsts[] = { sunrise, sunset, moonrise, moonset };
for (int f = 0; f < 4; f++) {
const char *fb = strstr(ab, fields[f][0]);
if (fb && fb < ae) {
fb += strlen(fields[f][0]);
const char *fq = strchr(fb, '"');
if (fq && fq < ae) {
size_t l = fq - fb;
if (l >= sizes[f]) l = sizes[f] - 1;
memcpy(dsts[f], fb, l);
dsts[f][l] = 0;
}
}
}
}
/*
* fetch_forecast - Fetch and parse a 3-day weather forecast from wttr.in.
*
* Fetches the JSON forecast (format=j1) from wttr.in for the given city,
* then manually parses the "weather" array to extract date, average
* temperature, and weather condition for up to 3 days. Dates are formatted
* as "Mon DD Mon" (e.g. "Mon 19 Jul"). Returns a single-line summary
* string like "Forecast for Berlin: Mon 19 Jul: 22°C Partly cloudy / ...".
*
* Parameters:
* city_url - URL-encoded city name for the API request.
* city_display - Human-readable city name for the response.
*
* Returns: A newly allocated forecast string, or NULL/ error message on
* failure. Caller must free() the result.
*/
char *fetch_forecast(const char *city_url, const char *city_display) {
char url[512];
snprintf(url, sizeof url, "https://wttr.in/%s?format=j1", city_url);
char *json = fetch_url(url);
if (!json) return NULL;
char *p = strstr(json, "\"weather\"");
if (!p) {
char *err = NULL;
size_t jlen = strlen(json);
if (jlen > 400) jlen = 400;
if (asprintf(&err, "Forecast parse error (response: %.*s)", (int)jlen, json) < 0) err = NULL;
free(json);
return err;
}
p = strchr(p, '[');
if (!p) { free(json); return NULL; }
p++;
char line[3][256], fmt_date[3][32];
int ndays = 0;
for (int i = 0; i < 3; i++) {
const char *obj_end = NULL;
const char *day = find_day_obj(p, 0, &obj_end);
if (!day) break;
char date[16]="", temp[8]="", cond[64]="";
parse_forecast_day(day, obj_end, date, sizeof date, temp, sizeof temp, cond, sizeof cond);
struct tm tm = {0};
if (strptime(date, "%Y-%m-%d", &tm)) { mktime(&tm); strftime(fmt_date[ndays], sizeof fmt_date[ndays], "%a %d %b", &tm); }
else snprintf(fmt_date[ndays], sizeof fmt_date[ndays], "%s", date);
snprintf(line[ndays], sizeof line[ndays], "%s: %s\u00b0C %s", fmt_date[ndays], temp, cond);
ndays++;
p = (char*)obj_end;
}
free(json);
if (!ndays) return NULL;
char *out = NULL;
if (ndays == 1) { if (asprintf(&out, "Forecast for %s: %s", city_display, line[0]) < 0) out = NULL; }
else if (ndays == 2) { if (asprintf(&out, "Forecast for %s: %s / %s", city_display, line[0], line[1]) < 0) out = NULL; }
else { if (asprintf(&out, "Forecast for %s: %s / %s / %s", city_display, line[0], line[1], line[2]) < 0) out = NULL; }
if (out) collapse_spaces(out);
return out;
}
/*
* fetch_forecast_full - Fetch a detailed 3-day forecast from wttr.in.
*
* Like fetch_forecast(), but returns per-day detail lines including the
* weather description, sunrise/sunset, and moonrise/moonset times.
* Each day is returned as a separate line in a single string, separated
* by newlines. Caller must free() the result.
*
* Parameters:
* city_url - URL-encoded city name for the API request.
* city_display - Human-readable city name for the response.
*
* Returns: A newly allocated multi-line forecast string, or NULL/error.
*/
char *fetch_forecast_full(const char *city_url, const char *city_display) {
(void)city_display;
char url[512];
snprintf(url, sizeof url, "https://wttr.in/%s?format=j1", city_url);
char *json = fetch_url(url);
if (!json) return NULL;
char *p = strstr(json, "\"weather\"");
if (!p) {
char *err = NULL;
size_t jlen = strlen(json);
if (jlen > 400) jlen = 400;
if (asprintf(&err, "Forecast parse error (response: %.*s)", (int)jlen, json) < 0) err = NULL;
free(json);
return err;
}
p = strchr(p, '[');
if (!p) { free(json); return NULL; }
p++;
char *out = NULL;
size_t out_cap = 0;
size_t out_len = 0;
for (int i = 0; i < 3; i++) {
const char *obj_end = NULL;
const char *day = find_day_obj(p, 0, &obj_end);
if (!day) break;
char date[16]="", temp[8]="", cond[64]="";
parse_forecast_day(day, obj_end, date, sizeof date, temp, sizeof temp, cond, sizeof cond);
char desc[128]="";
char *hb = strstr((char*)day, "\"hourly\"");
if (hb && hb < obj_end) {
hb = strchr(hb, '[');
if (hb && hb < obj_end) {
hb++;
char *db = strstr(hb, "\"weatherDesc\"");
if (db && db < obj_end) {
db = strchr(db, '[');
if (db && db < obj_end) {
db++;
char *vb = strstr(db, "\"value\": \"");
if (vb && vb < obj_end) { vb += 10; char *vq = strchr(vb, '"'); if (vq && vq < obj_end) { size_t l = vq - vb; if (l > sizeof desc - 1) l = sizeof desc - 1; memcpy(desc, vb, l); desc[l] = 0; while (l > 0 && desc[l-1] == ' ') desc[--l] = 0; } }
}
}
}
}
char sunrise[32]="", sunset[32]="", moonrise[32]="", moonset[32]="";
extract_astronomy(day, obj_end, sunrise, sizeof sunrise, sunset, sizeof sunset, moonrise, sizeof moonrise, moonset, sizeof moonset);
to_24h(sunrise, sizeof sunrise);
to_24h(sunset, sizeof sunset);
to_24h(moonrise, sizeof moonrise);
to_24h(moonset, sizeof moonset);
struct tm tm = {0};
char fmt_date[32];
if (strptime(date, "%Y-%m-%d", &tm)) { mktime(&tm); strftime(fmt_date, sizeof fmt_date, "%a %d %b", &tm); }
else snprintf(fmt_date, sizeof fmt_date, "%s", date);
char line[512];
snprintf(line, sizeof line, "%s: %s\u00b0C %s (%s) | Sun %s - %s | Moon %s - %s",
fmt_date, temp, cond, desc, sunrise, sunset, moonrise, moonset);
collapse_spaces(line);
size_t llen = strlen(line);
size_t need = out_len + llen + 2;
if (need > out_cap) {
size_t new_cap = out_cap ? out_cap * 2 : 2048;
if (new_cap < need) new_cap = need;
char *tmp = realloc(out, new_cap);
if (!tmp) { free(out); free(json); return NULL; }
out = tmp;
out_cap = new_cap;
}
if (out_len > 0) { out[out_len++] = '\n'; }
memcpy(out + out_len, line, llen);
out_len += llen;
out[out_len] = 0;
p = (char*)obj_end;
}
free(json);
if (!out || out_len == 0) return NULL;
return out;
}
/*
* cmd_forecast - Handle the "!forecast [city] [full]" command.
*
* Fetches a 3-day weather forecast from wttr.in using the JSON API.
* Delegates to fetch_forecast() for the standard summary, or
* fetch_forecast_full() when "full" is passed as an argument. The full
* mode includes weather description, sunrise/sunset, and moonrise/moonset
* for each day, sent as separate IRC messages. Defaults to Berlin if no
* city is specified. Rate-limited.
*
* Parameters:
* s - The IRC session.
* msg - The raw message text.
* reply_target - Channel or nick to reply to.
* src_nick - The nick of the user who sent the command.
*
* Returns: 1 if matched, 0 otherwise.
*/
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, current_userhost) < 0) return 1;
char city[256], city_url[256];
int full = 0;
const char *rest = msg[9] == ' ' ? msg + 10 : "";
while (*rest == ' ') rest++;
char args[512];
strncpy(args, rest, sizeof args - 1);
args[sizeof args - 1] = 0;
char *full_ptr = strstr(args, "full");
if (full_ptr) {
size_t plen = full_ptr - args;
if (plen > 0 && args[plen - 1] == ' ') {
full = 1;
args[plen - 1] = 0;
} else if (plen == 0) {
full = 1;
args[0] = 0;
}
}
while (*args == ' ') { memmove(args, args + 1, strlen(args)); }
size_t alen = strlen(args);
while (alen > 0 && args[alen - 1] == ' ') { args[--alen] = 0; }
normalize_city(args, city, sizeof city, city_url, sizeof city_url);
if (full) {
char *body = fetch_forecast_full(city_url, city);
if (body) {
char *line = strtok(body, "\n");
while (line) {
irc_reply(s, reply_target, src_nick, line);
line = strtok(NULL, "\n");
if (line) {
struct timespec ts_msg = { .tv_sec = 0, .tv_nsec = 200000000 };
nanosleep(&ts_msg, NULL);
}
}
free(body);
} else {
irc_reply(s, reply_target, src_nick, "Forecast: could not fetch data");
}
} else {
char *body = fetch_forecast(city_url, city);
char resp[1024];
if (body) {
snprintf(resp, sizeof resp, "%s", body);
free(body);
} else {
snprintf(resp, sizeof resp, "Forecast: could not fetch data");
}
irc_reply(s, reply_target, src_nick, resp);
}
log_stamp(); fprintf(stderr, CLR_MAGENTA "FORECAST %s from %s (full=%d):" CLR_RESET " %s\n", reply_target, src_nick, full, city);
return 1;
}
#endif