#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