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().
186 lines
6.2 KiB
C
186 lines
6.2 KiB
C
#ifndef CMD_YT_H
|
|
#define CMD_YT_H
|
|
|
|
#include "zynk.h"
|
|
|
|
/*
|
|
* yt_hex_digit - Convert a hexadecimal character to its numeric value.
|
|
*
|
|
* Parameters:
|
|
* c - A character ('0'-'9', 'a'-'f', or 'A'-'F').
|
|
*
|
|
* Returns: The numeric value (0-15), or -1 if not a valid hex digit.
|
|
*/
|
|
static int yt_hex_digit(char c) {
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* yt_codepoint_to_utf8 - Encode a Unicode code point as UTF-8.
|
|
*
|
|
* Parameters:
|
|
* cp - The Unicode code point (U+0000 to U+10FFFF).
|
|
* out - Output buffer for the UTF-8 bytes (at least 4 bytes needed).
|
|
*
|
|
* Returns: The number of UTF-8 bytes written (1-4).
|
|
*/
|
|
static size_t yt_codepoint_to_utf8(unsigned int cp, char *out) {
|
|
if (cp < 0x80) { out[0] = (char)cp; return 1; }
|
|
if (cp < 0x800) { out[0] = (char)(0xC0 | (cp >> 6)); out[1] = (char)(0x80 | (cp & 0x3F)); return 2; }
|
|
if (cp < 0x10000) { out[0] = (char)(0xE0 | (cp >> 12)); out[1] = (char)(0x80 | ((cp >> 6) & 0x3F)); out[2] = (char)(0x80 | (cp & 0x3F)); return 3; }
|
|
out[0] = (char)(0xF0 | (cp >> 18)); out[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
|
|
out[2] = (char)(0x80 | ((cp >> 6) & 0x3F)); out[3] = (char)(0x80 | (cp & 0x3F)); return 4;
|
|
}
|
|
|
|
/*
|
|
* yt_decode_unicode_escapes - Decode \\uXXXX JSON Unicode escapes in-place.
|
|
*
|
|
* Scans a string for JSON Unicode escape sequences (e.g. \\u0027 for single
|
|
* quote) and replaces each with the corresponding UTF-8 bytes. Safe to use
|
|
* in-place because UTF-8 encoding of any code point is never longer than the
|
|
* 6-byte \\uXXXX escape.
|
|
*
|
|
* Parameters:
|
|
* s - The string to decode (modified in-place, must be writable).
|
|
*
|
|
* Returns: void.
|
|
*/
|
|
static void yt_decode_unicode_escapes(char *s) {
|
|
char *r = s, *w = s;
|
|
while (*r) {
|
|
if (r[0] == '\\' && r[1] == 'u' && yt_hex_digit(r[2]) >= 0 && yt_hex_digit(r[3]) >= 0 &&
|
|
yt_hex_digit(r[4]) >= 0 && yt_hex_digit(r[5]) >= 0) {
|
|
unsigned int cp = ((unsigned int)yt_hex_digit(r[2]) << 12) | ((unsigned int)yt_hex_digit(r[3]) << 8) |
|
|
((unsigned int)yt_hex_digit(r[4]) << 4) | (unsigned int)yt_hex_digit(r[5]);
|
|
char utf8[5];
|
|
size_t n = yt_codepoint_to_utf8(cp, utf8);
|
|
memcpy(w, utf8, n);
|
|
w += n;
|
|
r += 6;
|
|
} else {
|
|
*w++ = *r++;
|
|
}
|
|
}
|
|
*w = '\0';
|
|
}
|
|
|
|
/*
|
|
* yt_url_encode - Percent-encode a string for safe use in a URL query parameter.
|
|
*
|
|
* Encodes all characters except unreserved characters (alphanumerics, '-', '_',
|
|
* '.', '~') using RFC 3986 percent-encoding. Used to encode the YouTube video
|
|
* URL before embedding it in the oEmbed API request.
|
|
*
|
|
* Parameters:
|
|
* src - The input string to encode.
|
|
* dst - Output buffer for the encoded string.
|
|
* dstsz - Size of the output buffer.
|
|
*
|
|
* Returns: void (output is written to dst, NUL-terminated).
|
|
*/
|
|
static void yt_url_encode(const char *src, char *dst, size_t dstsz) {
|
|
static const char hex[] = "0123456789ABCDEF";
|
|
size_t di = 0;
|
|
for (; *src && di + 4 < dstsz; src++) {
|
|
unsigned char c = (unsigned char)*src;
|
|
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
|
|
dst[di++] = c;
|
|
} else {
|
|
dst[di++] = '%';
|
|
dst[di++] = hex[c >> 4];
|
|
dst[di++] = hex[c & 0x0F];
|
|
}
|
|
}
|
|
dst[di] = '\0';
|
|
}
|
|
|
|
/*
|
|
* cmd_yt - Handle the "!yt <url>" command.
|
|
*
|
|
* Fetches the title and author of a YouTube video using the YouTube oEmbed
|
|
* API (https://www.youtube.com/oembed). The video URL is percent-encoded
|
|
* and passed as a query parameter. The JSON response is parsed for the
|
|
* "title" and "author_name" fields, and the result is formatted as
|
|
* "Title — Author" and sent to IRC. 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_yt(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
|
|
if (strncmp(msg, "!yt", 3) != 0) return 0;
|
|
if (msg[3] != ' ' && msg[3] != '\0') return 0;
|
|
if (rate_limit_check(src_nick, reply_target, current_userhost) < 0) return 1;
|
|
const char *rest = msg[3] == ' ' ? msg + 4 : "";
|
|
while (*rest == ' ') rest++;
|
|
if (!*rest) {
|
|
irc_reply(s, reply_target, src_nick, "Usage: !yt <youtube-url>");
|
|
return 1;
|
|
}
|
|
char url_buf[512];
|
|
int i = 0;
|
|
while (rest[i] && rest[i] != ' ' && i < 500) { url_buf[i] = rest[i]; i++; }
|
|
url_buf[i] = '\0';
|
|
if (strncmp(url_buf, "http://", 7) != 0 && strncmp(url_buf, "https://", 8) != 0) {
|
|
irc_reply(s, reply_target, src_nick, "Please provide a valid URL");
|
|
return 1;
|
|
}
|
|
char encoded[1024];
|
|
yt_url_encode(url_buf, encoded, sizeof encoded);
|
|
char api_url[1536];
|
|
snprintf(api_url, sizeof api_url, "https://www.youtube.com/oembed?url=%s&format=json", encoded);
|
|
char *json = fetch_url(api_url);
|
|
if (!json) {
|
|
irc_reply(s, reply_target, src_nick, "YouTube: could not fetch video info");
|
|
return 1;
|
|
}
|
|
const char *title_s = json_find(json, "\"title\":");
|
|
char title[256];
|
|
if (title_s && title_s[0] == '"') {
|
|
title_s++;
|
|
const char *tq = strchr(title_s, '"');
|
|
if (tq) {
|
|
size_t tlen = tq - title_s;
|
|
if (tlen > 255) tlen = 255;
|
|
memcpy(title, title_s, tlen);
|
|
title[tlen] = '\0';
|
|
yt_decode_unicode_escapes(title);
|
|
} else {
|
|
snprintf(title, sizeof title, "(parse error)");
|
|
}
|
|
} else {
|
|
snprintf(title, sizeof title, "(no title found)");
|
|
}
|
|
const char *author_s = json_find(json, "\"author_name\":");
|
|
char resp[512];
|
|
if (author_s && author_s[0] == '"') {
|
|
author_s++;
|
|
const char *aq = strchr(author_s, '"');
|
|
if (aq) {
|
|
size_t alen = aq - author_s;
|
|
if (alen > 127) alen = 127;
|
|
char author[128];
|
|
memcpy(author, author_s, alen);
|
|
author[alen] = '\0';
|
|
yt_decode_unicode_escapes(author);
|
|
snprintf(resp, sizeof resp, "%s — %s", title, author);
|
|
} else {
|
|
snprintf(resp, sizeof resp, "%s", title);
|
|
}
|
|
} else {
|
|
snprintf(resp, sizeof resp, "%s", title);
|
|
}
|
|
free(json);
|
|
irc_reply(s, reply_target, src_nick, resp);
|
|
log_stamp(); fprintf(stderr, CLR_MAGENTA "YT %s from %s:" CLR_RESET " %s\n", reply_target, src_nick, resp);
|
|
return 1;
|
|
}
|
|
|
|
#endif
|