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().
111 lines
4.3 KiB
C
111 lines
4.3 KiB
C
#ifndef CMD_STOCK_H
|
|
#define CMD_STOCK_H
|
|
|
|
#include "zynk.h"
|
|
|
|
/*
|
|
* cmd_stock - Handle the "!stock <SYMBOL>[,SYMBOL...]" command.
|
|
*
|
|
* Fetches real-time stock quotes from Yahoo Finance's chart API for up to
|
|
* 3 comma-separated ticker symbols. Parses the JSON response to extract
|
|
* current price, previous close, change/percent, day high/low, and volume.
|
|
* Formats each symbol as "SYMB (Name): $price +/-change (+/-pct%) H:high L:low Vol:volM".
|
|
* Multiple symbols are separated by " | ". 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_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, current_userhost) < 0) return 1;
|
|
const char *rest = msg[6] == ' ' ? msg + 7 : "";
|
|
while (*rest == ' ') rest++;
|
|
if (!*rest) {
|
|
irc_reply(s, reply_target, src_nick, "Usage: !stock <SYMBOL>[,SYMBOL...] (max 3, e.g. !stock AAPL or !stock AAPL,MSFT,GOOGL)");
|
|
return 1;
|
|
}
|
|
char buf[2048];
|
|
buf[0] = 0;
|
|
int count = 0;
|
|
char symbols[3][16];
|
|
const char *p = rest;
|
|
while (*p && count < 3) {
|
|
while (*p == ' ' || *p == ',') p++;
|
|
if (!*p) break;
|
|
int i = 0;
|
|
while (*p && *p != ' ' && *p != ',' && i < 15) symbols[count][i++] = toupper((unsigned char)*p++);
|
|
symbols[count][i] = 0;
|
|
for (int j = 0; symbols[count][j]; j++) {
|
|
unsigned char c = (unsigned char)symbols[count][j];
|
|
if (!isalnum(c) && c != '.' && c != '-' && c != '^' && c != '=') {
|
|
symbols[count][0] = 0;
|
|
break;
|
|
}
|
|
}
|
|
count++;
|
|
}
|
|
for (int si = 0; si < count; si++) {
|
|
if (!symbols[si][0]) continue;
|
|
char url[512];
|
|
snprintf(url, sizeof url, "https://query1.finance.yahoo.com/v8/finance/chart/%s?interval=1d&range=1d", symbols[si]);
|
|
char *json = fetch_url(url);
|
|
if (!json) {
|
|
char tmp[128];
|
|
snprintf(tmp, sizeof tmp, "%s: fetch error", symbols[si]);
|
|
if (buf[0]) strncat(buf, " | ", sizeof buf - strlen(buf) - 1);
|
|
strncat(buf, tmp, sizeof buf - strlen(buf) - 1);
|
|
continue;
|
|
}
|
|
const char *rp_s = json_find(json, "\"regularMarketPrice\":");
|
|
const char *pc_s = json_find(json, "\"chartPreviousClose\":");
|
|
const char *hi_s = json_find(json, "\"high\":");
|
|
const char *lo_s = json_find(json, "\"low\":");
|
|
const char *vo_s = json_find(json, "\"volume\":");
|
|
const char *nm_s = json_find(json, "\"shortName\":");
|
|
if (!rp_s || !pc_s || !hi_s || !lo_s || !vo_s) {
|
|
char tmp[480];
|
|
size_t jlen = strlen(json);
|
|
if (jlen > 400) jlen = 400;
|
|
snprintf(tmp, sizeof tmp, "%s: parse error (response: %.*s)", symbols[si], (int)jlen, json);
|
|
if (buf[0]) strncat(buf, " | ", sizeof buf - strlen(buf) - 1);
|
|
strncat(buf, tmp, sizeof buf - strlen(buf) - 1);
|
|
free(json); continue;
|
|
}
|
|
double price = strtod(rp_s, NULL);
|
|
double prev_close = strtod(pc_s, NULL);
|
|
double high = strtod(hi_s, NULL);
|
|
double low = strtod(lo_s, NULL);
|
|
long volume = strtol(vo_s, NULL, 10);
|
|
double change = price - prev_close;
|
|
double pct = (prev_close != 0) ? (change / prev_close) * 100.0 : 0.0;
|
|
char name[32] = "";
|
|
if (nm_s && nm_s[0] == '"') {
|
|
nm_s++;
|
|
const char *nq = strchr(nm_s, '"');
|
|
if (nq) { size_t nl = nq - nm_s; if (nl > 31) nl = 31; memcpy(name, nm_s, nl); name[nl] = 0; }
|
|
}
|
|
char tmp[384];
|
|
if (name[0])
|
|
snprintf(tmp, sizeof tmp, "%s (%s): $%.2f %+.2f (%+.2f%%) H:%.2f L:%.2f Vol:%ldM",
|
|
symbols[si], name, price, change, pct, high, low, volume / 1000000);
|
|
else
|
|
snprintf(tmp, sizeof tmp, "%s: $%.2f %+.2f (%+.2f%%) H:%.2f L:%.2f Vol:%ldM",
|
|
symbols[si], price, change, pct, high, low, volume / 1000000);
|
|
if (buf[0]) strncat(buf, " | ", sizeof buf - strlen(buf) - 1);
|
|
strncat(buf, tmp, sizeof buf - strlen(buf) - 1);
|
|
free(json);
|
|
}
|
|
if (!buf[0]) snprintf(buf, sizeof buf, "Stock: could not fetch data");
|
|
collapse_spaces(buf);
|
|
irc_reply(s, reply_target, src_nick, buf);
|
|
log_stamp(); fprintf(stderr, CLR_MAGENTA "STOCK %s from %s:" CLR_RESET " %s\n", reply_target, src_nick, buf);
|
|
return 1;
|
|
}
|
|
|
|
#endif
|