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().
376 lines
14 KiB
C
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
|