Add !yt YouTube title lookup command via oEmbed API. (0.41.0)
Added new !yt command (cmd_yt.h) that fetches the title and channel name of a YouTube video using the YouTube oEmbed API. The video URL is percent-encoded via a new yt_url_encode() helper before being passed as a query parameter. The JSON response is parsed for 'title' and 'author_name' fields using the existing json_find() helper, and the result is formatted as 'Title - Channel'. - New file: cmd_yt.h with yt_url_encode() and cmd_yt() command handler - CMakeLists.txt: added WITH_YT feature toggle (ON by default) - zynk.c: added conditional #include for cmd_yt.h - irc.h: added cmd_yt() dispatch in handle_privmsg() - cmd_help.h: added !yt <url> to the help listing - zynk.c: added doc comments to json_find() function - cmd_yt.h: added doc comments to yt_url_encode() and cmd_yt() - CHANGELOG.md: added 0.41.0 entry documenting all changes - README.md: updated feature summary, features list, feature toggles table, minimal build command, and IRC commands utilities table
This commit is contained in:
parent
084a41cfbd
commit
d59d4dc46c
7 changed files with 186 additions and 27 deletions
|
|
@ -1,3 +1,11 @@
|
|||
2026.07.25 - YouTube title lookup command, json_find docs. (0.41.0)
|
||||
|
||||
- Added `!yt <url>` command: fetches the title and channel name of a YouTube video using the YouTube oEmbed API (`https://www.youtube.com/oembed`). The video URL is percent-encoded via a new `yt_url_encode()` helper before being passed as a query parameter. The JSON response is parsed for `title` and `author_name` fields using the existing `json_find()` helper, and the result is formatted as `"Title — Channel"`.
|
||||
- Added `WITH_YT` CMake feature toggle (ON by default) with compile definition `HAS_CMD_YT`.
|
||||
- Added `!yt` to the `!help` command listing.
|
||||
- Added comprehensive doc comments to `yt_url_encode()` and `cmd_yt()` in `cmd_yt.h`, and to `json_find()` in `zynk.c`.
|
||||
- Updated README.md: added YouTube title lookup to the feature summary, features list, feature toggles table, minimal build command, and IRC commands utilities table.
|
||||
|
||||
2026.07.24 - Reduced MAX_HISTORY, moved LOAD_FUNC macro, added irc_reply docs. (0.40.9)
|
||||
|
||||
- Reduced `MAX_HISTORY` from 100 to 12 to decrease AI context memory usage.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
project(zynk VERSION 0.40.9 LANGUAGES C)
|
||||
project(zynk VERSION 0.41.0 LANGUAGES C)
|
||||
|
||||
# Prefer C99; the code is compatible with C99/C11
|
||||
set(CMAKE_C_STANDARD 99)
|
||||
|
|
@ -29,6 +29,7 @@ option(WITH_REBUILD "Rebuild command (ops only)" OFF)
|
|||
option(WITH_UPTIME "Uptime command" ON)
|
||||
option(WITH_RELOAD "Hot-reload command (ops only)" ON)
|
||||
option(WITH_RESTART "Restart command (ops only)" ON)
|
||||
option(WITH_YT "YouTube title command (requires curl)" ON)
|
||||
option(WITH_GREETING_OR_CHAT "Greeting/chat with AI fallback" ON)
|
||||
|
||||
# Map WITH_* options to HAS_CMD_* compile definitions and print feature status
|
||||
|
|
@ -37,7 +38,7 @@ message(STATUS "Feature configuration for zynk (${PROJECT_VERSION}):")
|
|||
message(STATUS "=============================================================================")
|
||||
foreach(_feat ZYNK PING VERSION HELP QUIT WEATHER FORECAST STOCK
|
||||
CALC TIME SEEN TELL GITLOG CHANGELOG AI CODE
|
||||
REBUILD UPTIME RELOAD RESTART GREETING_OR_CHAT)
|
||||
REBUILD UPTIME RELOAD RESTART YT GREETING_OR_CHAT)
|
||||
string(TOLOWER "${_feat}" _feat_lower)
|
||||
if(WITH_${_feat})
|
||||
list(APPEND _cmd_defs "HAS_CMD_${_feat}")
|
||||
|
|
|
|||
53
README.md
53
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# zynk - An IRC bot
|
||||
|
||||
An IRC bot in C. Features a key-value store backed by SQLite3, weather lookups via wttr.in, stock quotes, calculator, time, user tracking, and AI-powered replies.
|
||||
An IRC bot in C. Features a key-value store backed by SQLite3, weather lookups via wttr.in, stock quotes, calculator, time, user tracking, YouTube title lookups, and AI-powered replies.
|
||||
|
||||
## This is a fork!
|
||||
|
||||
|
|
@ -19,6 +19,7 @@ I want to thank Armin for his work and for sharing his code with me. Take a look
|
|||
- **Offline Messages**: Leave messages for offline users with `!tell`
|
||||
- **AI Queries**: Ask questions via `!ai` or by addressing the bot; powered by `opencode`
|
||||
- **Code Changes**: AI-powered code editing with auto-recompile and hot reload
|
||||
- **YouTube Titles**: Look up video titles with `!yt <url>` (via YouTube oEmbed API)
|
||||
- **Chat Greetings**: Responds to `hi`, `hello`, `hey`, `bye`, and more
|
||||
- **Hot Reload**: Reload or restart without leaving channels, losing your nick, or dropping voice mode
|
||||
- **TLS**: Secure connections via OpenSSL
|
||||
|
|
@ -66,29 +67,30 @@ cmake --build build
|
|||
|
||||
Available options (the `!command` shown in parentheses):
|
||||
|
||||
| Option | Command | Description | Default |
|
||||
|-------------------------|------------------|----------------------------------|--------------------------|
|
||||
| `WITH_ZYNK` | `!zynk` | Key-value store | ON |
|
||||
| `WITH_PING` | `!ping` | Ping/pong | ON |
|
||||
| `WITH_VERSION` | `!version` | Version info | ON |
|
||||
| `WITH_HELP` | `!help` | Help listing | ON |
|
||||
| `WITH_QUIT` | `!quit` / `!die` | Shut down the bot | ON |
|
||||
| `WITH_WEATHER` | `!weather` | Weather lookup (requires curl) | ON |
|
||||
| `WITH_FORECAST` | `!forecast` | 3-day forecast (requires curl) | ON |
|
||||
| `WITH_STOCK` | `!stock` | Stock quotes (requires curl) | ON |
|
||||
| `WITH_CALC` | `!calc` | Calculator (requires bc) | ON |
|
||||
| `WITH_TIME` | `!time` | Time/timezone lookup | ON |
|
||||
| `WITH_SEEN` | `!seen` | User tracking | ON |
|
||||
| `WITH_TELL` | `!tell` | Offline messages | ON |
|
||||
| `WITH_GITLOG` | `!gitlog` | Recent git commits | ON |
|
||||
| `WITH_CHANGELOG` | `!changelog` | Changelog entries | ON |
|
||||
| `WITH_AI` | `!ai` | AI questions (requires opencode) | ON (can not be disabled) |
|
||||
| `WITH_CODE` | `!code` | AI code changes (ops only) | OFF |
|
||||
| `WITH_REBUILD` | `!rebuild` | Rebuild & restart (ops only) | OFF |
|
||||
| `WITH_UPTIME` | `!uptime` | Uptime display | ON |
|
||||
| `WITH_RELOAD` | `!reload` | Hot reload (ops only) | ON |
|
||||
| `WITH_RESTART` | `!restart` | Restart (ops only) | ON |
|
||||
| `WITH_GREETING_OR_CHAT` | `<nick>: <text>` | Greeting/chat with AI fallback | ON (can not be disabled) |
|
||||
| Option | Command | Description | Default |
|
||||
|-------------------------|------------------|--------------------------------------|--------------------------|
|
||||
| `WITH_ZYNK` | `!zynk` | Key-value store | ON |
|
||||
| `WITH_PING` | `!ping` | Ping/pong | ON |
|
||||
| `WITH_VERSION` | `!version` | Version info | ON |
|
||||
| `WITH_HELP` | `!help` | Help listing | ON |
|
||||
| `WITH_QUIT` | `!quit` / `!die` | Shut down the bot | ON |
|
||||
| `WITH_WEATHER` | `!weather` | Weather lookup (requires curl) | ON |
|
||||
| `WITH_FORECAST` | `!forecast` | 3-day forecast (requires curl) | ON |
|
||||
| `WITH_STOCK` | `!stock` | Stock quotes (requires curl) | ON |
|
||||
| `WITH_CALC` | `!calc` | Calculator (requires bc) | ON |
|
||||
| `WITH_TIME` | `!time` | Time/timezone lookup | ON |
|
||||
| `WITH_SEEN` | `!seen` | User tracking | ON |
|
||||
| `WITH_TELL` | `!tell` | Offline messages | ON |
|
||||
| `WITH_GITLOG` | `!gitlog` | Recent git commits | ON |
|
||||
| `WITH_CHANGELOG` | `!changelog` | Changelog entries | ON |
|
||||
| `WITH_AI` | `!ai` | AI questions (requires opencode) | ON (can not be disabled) |
|
||||
| `WITH_CODE` | `!code` | AI code changes (ops only) | OFF |
|
||||
| `WITH_REBUILD` | `!rebuild` | Rebuild & restart (ops only) | OFF |
|
||||
| `WITH_UPTIME` | `!uptime` | Uptime display | ON |
|
||||
| `WITH_RELOAD` | `!reload` | Hot reload (ops only) | ON |
|
||||
| `WITH_RESTART` | `!restart` | Restart (ops only) | ON |
|
||||
| `WITH_YT` | `!yt` | YouTube title lookup (requires curl) | ON |
|
||||
| `WITH_GREETING_OR_CHAT` | `<nick>: <text>` | Greeting/chat with AI fallback | ON (can not be disabled) |
|
||||
|
||||
### Minimal build
|
||||
|
||||
|
|
@ -100,7 +102,7 @@ cmake -B build \
|
|||
-DWITH_STOCK=OFF -DWITH_CALC=OFF -DWITH_TIME=OFF -DWITH_SEEN=OFF \
|
||||
-DWITH_TELL=OFF -DWITH_GITLOG=OFF -DWITH_CHANGELOG=OFF -DWITH_AI=OFF \
|
||||
-DWITH_CODE=OFF -DWITH_REBUILD=OFF -DWITH_UPTIME=OFF -DWITH_RELOAD=OFF \
|
||||
-DWITH_RESTART=OFF -DWITH_GREETING_OR_CHAT=OFF
|
||||
-DWITH_RESTART=OFF -DWITH_YT=OFF -DWITH_GREETING_OR_CHAT=OFF
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
|
|
@ -173,6 +175,7 @@ rm -rf build
|
|||
| `!time [timezone]` | Show current time (see below) |
|
||||
| `!seen <nick>` | Check when a user was last seen |
|
||||
| `!tell <nick> <message>` | Leave a message for an offline user |
|
||||
| `!yt <url>` | Show YouTube video title and channel name |
|
||||
| `!gitlog [count]` | Show recent git commits with links (default: 1, max: 10) |
|
||||
| `!changelog [count]` | Show changelog entries (default: 1, max: 10) |
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,10 @@ int cmd_help(Session *s, const char *msg, const char *reply_target, const char *
|
|||
n = snprintf(p, rem, " !restart (ops),");
|
||||
p += n; rem -= n;
|
||||
#endif
|
||||
#ifdef HAS_CMD_YT
|
||||
n = snprintf(p, rem, " !yt <url>,");
|
||||
p += n; rem -= n;
|
||||
#endif
|
||||
#ifdef HAS_CMD_QUIT
|
||||
n = snprintf(p, rem, " !quit/!die (ops)");
|
||||
p += n; rem -= n;
|
||||
|
|
|
|||
120
cmd_yt.h
Normal file
120
cmd_yt.h
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#ifndef CMD_YT_H
|
||||
#define CMD_YT_H
|
||||
|
||||
#include "zynk.h"
|
||||
|
||||
/*
|
||||
* 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) < 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';
|
||||
} 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';
|
||||
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
|
||||
3
irc.h
3
irc.h
|
|
@ -414,6 +414,9 @@ static void handle_privmsg(Session *s, const char *params, const char *src_nick)
|
|||
#ifdef HAS_CMD_RESTART
|
||||
if (cmd_restart(s, msg, reply_target, src_nick, tgt)) return;
|
||||
#endif
|
||||
#ifdef HAS_CMD_YT
|
||||
if (cmd_yt(s, msg, reply_target, src_nick)) return;
|
||||
#endif
|
||||
#ifdef HAS_CMD_GREETING_OR_CHAT
|
||||
cmd_greeting_or_chat(s, msg, reply_target, src_nick);
|
||||
#endif
|
||||
|
|
|
|||
20
zynk.c
20
zynk.c
|
|
@ -644,6 +644,23 @@ int try_compile_and_restart(Session *s, const char *target) {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* json_find - Locate a key in a flat JSON string and return a pointer to its value.
|
||||
*
|
||||
* Searches for the first occurrence of key in haystack using strstr(), then
|
||||
* skips past the key, any whitespace, an optional colon, more whitespace, and
|
||||
* any leading '[' or space characters to return a pointer to the value. This
|
||||
* is a simple helper for parsing flat JSON responses (e.g. from Yahoo Finance
|
||||
* or YouTube oEmbed APIs) without a full JSON parser. Not suitable for nested
|
||||
* or complex JSON structures.
|
||||
*
|
||||
* Parameters:
|
||||
* haystack - The JSON string to search (must be NUL-terminated).
|
||||
* key - The JSON key to find, including quotes (e.g. "\"title\":").
|
||||
*
|
||||
* Returns: A pointer into haystack at the start of the value, or NULL if the
|
||||
* key is not found.
|
||||
*/
|
||||
const char *json_find(const char *haystack, const char *key) {
|
||||
const char *p = strstr(haystack, key);
|
||||
if (!p) return NULL;
|
||||
|
|
@ -714,6 +731,9 @@ const char *json_find(const char *haystack, const char *key) {
|
|||
#ifdef HAS_CMD_RESTART
|
||||
#include "cmd_restart.h"
|
||||
#endif
|
||||
#ifdef HAS_CMD_YT
|
||||
#include "cmd_yt.h"
|
||||
#endif
|
||||
#ifdef HAS_CMD_GREETING_OR_CHAT
|
||||
#include "cmd_greeting_or_chat.h"
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue