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

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