Wikipedia search command (!wp), version 0.43.0
- Add !wp <search terms> command: searches Wikipedia via MediaWiki API (action=query&list=search), displays article titles, snippets, and links. Single-result mode fetches the first chapter via Extracts API (exchars=800); multi-result mode shows a numbered list with short snippets and per-result Read full article links. - Add self-contained JSON parser helpers: wp_json_get_string(), wp_json_next_array_item(), wp_decode_json_string() with \uXXXX Unicode support. - Add wp_url_encode() (RFC 3986), wp_codepoint_to_utf8(), wp_strip_html(), wp_collapse_spaces(), wp_hex_digit(). - Add WITH_WP CMake feature toggle (ON by default, HAS_CMD_WP compile def). - Add !wp to the !help command listing. - Add doc comments to all 9 functions in cmd_wp.h. - Update CHANGELOG.md with 0.43.0 entry. - Update README.md: add Wikipedia to features list, feature toggles table, IRC commands utilities table, and minimal build command.
This commit is contained in:
parent
4dbdd9bdde
commit
19b07fa76f
7 changed files with 443 additions and 5 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -1,10 +1,24 @@
|
|||
2026.07.26 - Wikipedia search command. (0.43.0)
|
||||
|
||||
- Added `!wp <search terms>` command: searches Wikipedia using the MediaWiki API (`action=query&list=search`) and displays results. Accepts multiple keywords (e.g. `!wp quantum physics`). Rate-limited via `rate_limit_check()`.
|
||||
- Single-result mode: when the search returns exactly one article, fetches the first chapter via the Extracts API (`action=query&prop=extracts`, up to 800 characters) and sends the article title, intro text, and a "Read full article: LINK" message. Appends "..." if the extract was truncated.
|
||||
- Multi-result mode: when the search returns 2-5 articles, sends a compact numbered list with short snippets (truncated to 60 chars) separated by pipes. Each result is followed by a "Read full article: LINK" message. Appends "..." if the result list was truncated.
|
||||
- Added self-contained JSON parser helpers: `wp_json_get_string()` extracts string values by key, `wp_json_next_array_item()` iterates JSON array objects, `wp_decode_json_string()` handles JSON escape sequences including `\uXXXX` Unicode.
|
||||
- Added `wp_url_encode()` for percent-encoding search queries and article titles in API URLs (RFC 3986 compliant, spaces as `+`).
|
||||
- Added `wp_codepoint_to_utf8()` to encode Unicode code points as UTF-8 bytes (handles U+0000 through U+10FFFF).
|
||||
- Added `wp_strip_html()` to remove HTML tags from Wikipedia search result snippets, and `wp_collapse_spaces()` to normalize whitespace.
|
||||
- Added `wp_hex_digit()` to convert hexadecimal characters to numeric values.
|
||||
- Added `WITH_WP` CMake feature toggle (ON by default) with compile definition `HAS_CMD_WP`.
|
||||
- Added `!wp` to the `!help` command listing.
|
||||
- Added comprehensive doc comments to all 9 functions in `cmd_wp.h`.
|
||||
|
||||
2026.07.25 - Comprehensive flood protection: per-poll limits, userhost rate limiting, outgoing throttle. (0.42.0)
|
||||
|
||||
- Added `MAX_LINES_PER_POLL = 50` constant and per-poll line counting in `irc_feed()`: the bot now drops excess lines when a single poll iteration delivers more than 50 complete IRC messages, preventing burst flooding from overwhelming the bot or triggering IRC server excess flood disconnects.
|
||||
- Added `poll_lines_left` counter to `Session` struct, reset at the start of each poll cycle in `main()` before calling `irc_feed()`.
|
||||
- Changed `RateLimitEntry` to track by `user@host` instead of nick: the rate limiter now keys on the full userhost extracted from the IRC prefix (`nick!user@host`) rather than the nick alone, preventing trivial bypass via nick cycling. Added `userhost[128]` field replacing the old `nick[32]` field.
|
||||
- Added `current_userhost[128]` global variable, parsed from the IRC message prefix in `irc_handle()` before dispatching to command handlers. Updated `rate_limit_check()` signature to accept the userhost parameter.
|
||||
- Added violation tracking to `RateLimitEntry` with a `violations` counter: repeated rate limit hits from the same userhost increment the counter, enabling future escalating penalties (warnings at 3 violations, kick requests at 5).
|
||||
- Added violation tracking to `RateLimitEntry` with a `violations` counter: repeated rate limit hits from the same userhost increment the counter, enabling future escalating penalties (warnings at three violations, kick requests at 5).
|
||||
- Increased `RATE_LIMIT_MAX` from 8 to 32, expanding the rate limit tracking buffer to handle more concurrent users without evicting entries.
|
||||
- Added outgoing message throttle to `net_send()`: uses `clock_gettime(CLOCK_MONOTONIC)` timestamp tracking to enforce a minimum `OUTGOING_DELAY_MS = 100` millisecond delay between consecutive sends, preventing the bot from flooding the IRC server with rapid bursts.
|
||||
- Added 200ms inter-line delay in `!gitlog` output (`cmd_gitlog.h`): each commit line is now separated by a 200ms `nanosleep()` to avoid triggering IRC server flood protection on multi-commit responses.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
project(zynk VERSION 0.42.0 LANGUAGES C)
|
||||
project(zynk VERSION 0.43.0 LANGUAGES C)
|
||||
|
||||
# Prefer C99; the code is compatible with C99/C11
|
||||
set(CMAKE_C_STANDARD 99)
|
||||
|
|
@ -30,6 +30,7 @@ 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_WP "Wikipedia search 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
|
||||
|
|
@ -38,7 +39,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 YT GREETING_OR_CHAT)
|
||||
REBUILD UPTIME RELOAD RESTART YT WP GREETING_OR_CHAT)
|
||||
string(TOLOWER "${_feat}" _feat_lower)
|
||||
if(WITH_${_feat})
|
||||
list(APPEND _cmd_defs "HAS_CMD_${_feat}")
|
||||
|
|
|
|||
|
|
@ -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, YouTube title lookups, 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, Wikipedia search, and AI-powered replies.
|
||||
|
||||
## This is a fork!
|
||||
|
||||
|
|
@ -20,6 +20,7 @@ I want to thank Armin for his work and for sharing his code with me. Take a look
|
|||
- **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)
|
||||
- **Wikipedia Search**: Search Wikipedia with `!wp <query>` (via MediaWiki 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
|
||||
|
|
@ -90,6 +91,7 @@ Available options (the `!command` shown in parentheses):
|
|||
| `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_WP` | `!wp` | Wikipedia search (requires curl) | ON |
|
||||
| `WITH_GREETING_OR_CHAT` | `<nick>: <text>` | Greeting/chat with AI fallback | ON (can not be disabled) |
|
||||
|
||||
### Minimal build
|
||||
|
|
@ -102,7 +104,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_YT=OFF -DWITH_GREETING_OR_CHAT=OFF
|
||||
-DWITH_RESTART=OFF -DWITH_YT=OFF -DWITH_WP=OFF -DWITH_GREETING_OR_CHAT=OFF
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
|
|
@ -176,6 +178,7 @@ rm -rf build
|
|||
| `!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 |
|
||||
| `!wp <query>` | Search Wikipedia and show article info |
|
||||
| `!gitlog [count]` | Show recent git commits with links (default: 1, max: 10) |
|
||||
| `!changelog [count]` | Show changelog entries (default: 1, max: 10) |
|
||||
|
||||
|
|
|
|||
|
|
@ -104,6 +104,10 @@ int cmd_help(Session *s, const char *msg, const char *reply_target, const char *
|
|||
n = snprintf(p, rem, " !yt <url>,");
|
||||
p += n; rem -= n;
|
||||
#endif
|
||||
#ifdef HAS_CMD_WP
|
||||
n = snprintf(p, rem, " !wp <query>,");
|
||||
p += n; rem -= n;
|
||||
#endif
|
||||
#ifdef HAS_CMD_QUIT
|
||||
n = snprintf(p, rem, " !quit/!die (ops)");
|
||||
p += n; rem -= n;
|
||||
|
|
|
|||
410
cmd_wp.h
Normal file
410
cmd_wp.h
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
#ifndef CMD_WP_H
|
||||
#define CMD_WP_H
|
||||
|
||||
#include "zynk.h"
|
||||
|
||||
/*
|
||||
* wp_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 wp_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;
|
||||
}
|
||||
|
||||
/*
|
||||
* wp_codepoint_to_utf8 - Encode a Unicode code point as UTF-8 bytes.
|
||||
*
|
||||
* Handles code points from U+0000 through U+10FFFF, producing 1-4 UTF-8
|
||||
* bytes. Used to decode \uXXXX JSON Unicode escapes in Wikipedia API
|
||||
* responses into printable UTF-8 text.
|
||||
*
|
||||
* Parameters:
|
||||
* cp - The Unicode code point.
|
||||
* 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 wp_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;
|
||||
}
|
||||
|
||||
/*
|
||||
* wp_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. Spaces are encoded as '+' for
|
||||
* application/x-www-form-urlencoded compatibility. Used to encode search
|
||||
* queries and article titles before embedding them in Wikipedia API URLs.
|
||||
*
|
||||
* 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 wp_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 if (c == ' ') {
|
||||
dst[di++] = '+';
|
||||
} else {
|
||||
if (di + 3 >= dstsz) break;
|
||||
dst[di++] = '%';
|
||||
dst[di++] = hex[c >> 4];
|
||||
dst[di++] = hex[c & 0x0F];
|
||||
}
|
||||
}
|
||||
dst[di] = '\0';
|
||||
}
|
||||
|
||||
/*
|
||||
* wp_decode_json_string - Decode JSON string escape sequences in-place.
|
||||
*
|
||||
* Scans a string for JSON backslash escapes and replaces each with the
|
||||
* corresponding character: \\n and \\t become spaces, \\\" becomes a
|
||||
* double quote, \\\\ becomes a backslash, \\/ becomes a forward slash,
|
||||
* and \\uXXXX sequences are decoded to UTF-8 bytes. Processes the input
|
||||
* from src to dst, which may be the same buffer (safe for in-place
|
||||
* modification because decoded output is never longer than the escape).
|
||||
*
|
||||
* Parameters:
|
||||
* src - The JSON-encoded input string.
|
||||
* dst - Output buffer for the decoded string (may alias src).
|
||||
* dstsz - Size of the output buffer.
|
||||
*
|
||||
* Returns: void (output is written to dst, NUL-terminated).
|
||||
*/
|
||||
static void wp_decode_json_string(const char *src, char *dst, size_t dstsz) {
|
||||
size_t di = 0;
|
||||
while (*src && di + 1 < dstsz) {
|
||||
if (*src == '\\' && *(src+1)) {
|
||||
src++;
|
||||
if (*src == 'n') { dst[di++] = ' '; src++; }
|
||||
else if (*src == 't') { dst[di++] = ' '; src++; }
|
||||
else if (*src == '"') { dst[di++] = '"'; src++; }
|
||||
else if (*src == '\\') { dst[di++] = '\\'; src++; }
|
||||
else if (*src == '/' ) { dst[di++] = '/'; src++; }
|
||||
else if (*src == 'u' && strlen(src) >= 5) {
|
||||
int h1 = wp_hex_digit(src[1]), h2 = wp_hex_digit(src[2]),
|
||||
h3 = wp_hex_digit(src[3]), h4 = wp_hex_digit(src[4]);
|
||||
if (h1 >= 0 && h2 >= 0 && h3 >= 0 && h4 >= 0) {
|
||||
unsigned int cp = ((unsigned int)h1 << 12) | ((unsigned int)h2 << 8) |
|
||||
((unsigned int)h3 << 4) | (unsigned int)h4;
|
||||
char utf8[5];
|
||||
size_t n = wp_codepoint_to_utf8(cp, utf8);
|
||||
if (di + n < dstsz) { memcpy(dst + di, utf8, n); di += n; }
|
||||
src += 5;
|
||||
continue;
|
||||
} else {
|
||||
dst[di++] = *src++;
|
||||
}
|
||||
} else {
|
||||
dst[di++] = *src++;
|
||||
}
|
||||
} else {
|
||||
dst[di++] = *src++;
|
||||
}
|
||||
}
|
||||
dst[di] = '\0';
|
||||
}
|
||||
|
||||
/*
|
||||
* wp_json_get_string - Extract the string value of a JSON key.
|
||||
*
|
||||
* Finds the first occurrence of the given key in a JSON string, skips
|
||||
* past the colon, and reads the double-quoted string value into the
|
||||
* output buffer. Handles escaped double quotes (\\") inside the value.
|
||||
* Does not perform full JSON parsing — relies on the flat structure
|
||||
* of the Wikipedia MediaWiki API responses.
|
||||
*
|
||||
* Parameters:
|
||||
* json - The JSON string to search.
|
||||
* key - The key to look for (e.g., "\"title\":").
|
||||
* out - Output buffer for the extracted value.
|
||||
* outsz - Size of the output buffer.
|
||||
*
|
||||
* Returns: Pointer to out on success, NULL if the key or value is not found.
|
||||
*/
|
||||
static char *wp_json_get_string(const char *json, const char *key, char *out, size_t outsz) {
|
||||
const char *p = strstr(json, key);
|
||||
if (!p) return NULL;
|
||||
p += strlen(key);
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
if (*p == ':') { p++; while (*p == ' ' || *p == '\t') p++; }
|
||||
if (*p != '"') return NULL;
|
||||
p++;
|
||||
size_t i = 0;
|
||||
while (*p && *p != '"' && i + 1 < outsz) {
|
||||
if (*p == '\\' && *(p+1) == '"') {
|
||||
out[i++] = '"';
|
||||
p += 2;
|
||||
} else {
|
||||
out[i++] = *p++;
|
||||
}
|
||||
}
|
||||
out[i] = '\0';
|
||||
return (i > 0) ? out : NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* wp_strip_html - Remove HTML tags from a string in-place.
|
||||
*
|
||||
* Scans for '<' and '>' characters to identify tags, skipping everything
|
||||
* between them. Only content outside tags is kept. Used to strip HTML
|
||||
* markup from Wikipedia search result snippets before displaying them
|
||||
* to the IRC channel.
|
||||
*
|
||||
* Parameters:
|
||||
* s - The string to strip (modified in-place, NUL-terminated).
|
||||
*
|
||||
* Returns: void.
|
||||
*/
|
||||
static void wp_strip_html(char *s) {
|
||||
char *r = s, *w = s;
|
||||
int in_tag = 0;
|
||||
while (*r) {
|
||||
if (*r == '<') { in_tag = 1; r++; continue; }
|
||||
if (*r == '>') { in_tag = 0; r++; continue; }
|
||||
if (!in_tag) *w++ = *r;
|
||||
r++;
|
||||
}
|
||||
*w = '\0';
|
||||
}
|
||||
|
||||
/*
|
||||
* wp_collapse_spaces - Normalize whitespace in a string in-place.
|
||||
*
|
||||
* Collapses runs of spaces, tabs, newlines, and carriage returns into a
|
||||
* single space. Trims leading and trailing whitespace. Produces clean,
|
||||
* readable text from the output of wp_strip_html, where tag removal
|
||||
* may leave behind extra whitespace.
|
||||
*
|
||||
* Parameters:
|
||||
* s - The string to normalize (modified in-place, NUL-terminated).
|
||||
*
|
||||
* Returns: void.
|
||||
*/
|
||||
static void wp_collapse_spaces(char *s) {
|
||||
char *r = s, *w = s;
|
||||
int in_space = 1;
|
||||
while (*r) {
|
||||
if (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') {
|
||||
if (!in_space) *w++ = ' ';
|
||||
in_space = 1;
|
||||
} else {
|
||||
*w++ = *r;
|
||||
in_space = 0;
|
||||
}
|
||||
r++;
|
||||
}
|
||||
*w = 0;
|
||||
while (w > s && *(w-1) == ' ') *--w = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* wp_json_next_array_item - Advance a pointer to the next '{' in a JSON array.
|
||||
*
|
||||
* Starting from *cur, finds the next '{' which marks the beginning of the
|
||||
* next object in a JSON array. Updates *cur to point past that brace so
|
||||
* subsequent calls continue scanning. Used to iterate over the "search"
|
||||
* array in the Wikipedia API response.
|
||||
*
|
||||
* Parameters:
|
||||
* cur - Pointer to the current position in the JSON string (in/out).
|
||||
*
|
||||
* Returns: Pointer to the '{' of the next array item, or NULL if none found.
|
||||
*/
|
||||
static const char *wp_json_next_array_item(const char **cur) {
|
||||
const char *p = strchr(*cur, '{');
|
||||
if (p) *cur = p + 1;
|
||||
return p;
|
||||
}
|
||||
|
||||
/*
|
||||
* cmd_wp - Handle the "!wp <search terms>" command.
|
||||
*
|
||||
* Searches Wikipedia using the MediaWiki API. Accepts multiple keywords.
|
||||
* For a single result: fetches the article intro (first ~500 chars) and
|
||||
* sends the title, intro text, and a link. For multiple results: sends
|
||||
* a compact list of titles with short snippets separated by pipes.
|
||||
*
|
||||
* Uses two Wikipedia API endpoints:
|
||||
* 1. Search API: action=query&list=search (find matching articles)
|
||||
* 2. Extracts API: action=query&prop=extracts&exintro=true (get intro)
|
||||
*
|
||||
* 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_wp(Session *s, const char *msg, const char *reply_target, const char *src_nick) {
|
||||
if (strncmp(msg, "!wp", 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: !wp <search terms>");
|
||||
return 1;
|
||||
}
|
||||
char query_encoded[512];
|
||||
wp_url_encode(rest, query_encoded, sizeof query_encoded);
|
||||
char search_url[1024];
|
||||
snprintf(search_url, sizeof search_url,
|
||||
"https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=%s&srlimit=5&format=json",
|
||||
query_encoded);
|
||||
char *json = fetch_url(search_url);
|
||||
if (!json) {
|
||||
irc_reply(s, reply_target, src_nick, "Wikipedia: could not fetch search results");
|
||||
return 1;
|
||||
}
|
||||
const char *results_key = strstr(json, "\"search\":[");
|
||||
if (!results_key) {
|
||||
irc_reply(s, reply_target, src_nick, "Wikipedia: no articles found");
|
||||
free(json);
|
||||
return 1;
|
||||
}
|
||||
int result_count = 0;
|
||||
{
|
||||
const char *scan = results_key;
|
||||
while ((scan = strchr(scan, '{')) != NULL) { result_count++; scan++; }
|
||||
}
|
||||
if (result_count == 0) {
|
||||
irc_reply(s, reply_target, src_nick, "Wikipedia: no articles found");
|
||||
free(json);
|
||||
return 1;
|
||||
}
|
||||
if (result_count == 1) {
|
||||
char title[256] = "";
|
||||
char snippet_raw[512] = "";
|
||||
const char *brace = strchr(results_key, '{');
|
||||
if (brace) {
|
||||
wp_json_get_string(brace, "\"title\":", title, sizeof title);
|
||||
wp_json_get_string(brace, "\"snippet\":", snippet_raw, sizeof snippet_raw);
|
||||
}
|
||||
wp_decode_json_string(snippet_raw, snippet_raw, sizeof snippet_raw);
|
||||
wp_strip_html(snippet_raw);
|
||||
wp_collapse_spaces(snippet_raw);
|
||||
if (!title[0]) {
|
||||
irc_reply(s, reply_target, src_nick, "Wikipedia: could not parse result");
|
||||
free(json);
|
||||
return 1;
|
||||
}
|
||||
char enc_title[512];
|
||||
wp_url_encode(title, enc_title, sizeof enc_title);
|
||||
char extract_url[1024];
|
||||
snprintf(extract_url, sizeof extract_url,
|
||||
"https://en.wikipedia.org/w/api.php?action=query&titles=%s&prop=extracts&explaintext=true&exchars=800&format=json",
|
||||
enc_title);
|
||||
free(json);
|
||||
char *ej = fetch_url(extract_url);
|
||||
char extract[1024] = "";
|
||||
if (ej) {
|
||||
const char *ep = strstr(ej, "\"extract\":");
|
||||
if (ep) {
|
||||
char raw[1024] = "";
|
||||
wp_json_get_string(ep, "\"extract\":", raw, sizeof raw);
|
||||
wp_decode_json_string(raw, extract, sizeof extract);
|
||||
}
|
||||
free(ej);
|
||||
}
|
||||
char resp[1536];
|
||||
if (extract[0]) {
|
||||
size_t elen = strlen(extract);
|
||||
int truncated = (elen >= 790);
|
||||
snprintf(resp, sizeof resp, "%s — %s%s", title, extract, truncated ? "..." : "");
|
||||
} else if (snippet_raw[0]) {
|
||||
snprintf(resp, sizeof resp, "%s — %s", title, snippet_raw);
|
||||
} else {
|
||||
snprintf(resp, sizeof resp, "%s — https://en.wikipedia.org/wiki/%s", title, enc_title);
|
||||
}
|
||||
irc_reply(s, reply_target, src_nick, resp);
|
||||
char link[640];
|
||||
snprintf(link, sizeof link, "Read full article: https://en.wikipedia.org/wiki/%s", enc_title);
|
||||
irc_reply(s, reply_target, src_nick, link);
|
||||
} else {
|
||||
char resp[1800];
|
||||
int off = snprintf(resp, sizeof resp, "Wikipedia results for \"%s\":", rest);
|
||||
const char *cur = results_key;
|
||||
int idx = 0;
|
||||
while (idx < result_count && idx < 5 && off < (int)sizeof resp - 200) {
|
||||
const char *brace = wp_json_next_array_item(&cur);
|
||||
if (!brace) break;
|
||||
char title[256] = "";
|
||||
char snippet_raw[256] = "";
|
||||
wp_json_get_string(brace, "\"title\":", title, sizeof title);
|
||||
wp_json_get_string(brace, "\"snippet\":", snippet_raw, sizeof snippet_raw);
|
||||
wp_decode_json_string(snippet_raw, snippet_raw, sizeof snippet_raw);
|
||||
wp_strip_html(snippet_raw);
|
||||
wp_collapse_spaces(snippet_raw);
|
||||
if (!title[0]) continue;
|
||||
char enc_title[512];
|
||||
wp_url_encode(title, enc_title, sizeof enc_title);
|
||||
int n;
|
||||
if (idx == 0) {
|
||||
n = snprintf(resp + off, sizeof resp - off, " %d. %s", idx + 1, title);
|
||||
} else {
|
||||
n = snprintf(resp + off, sizeof resp - off, " | %d. %s", idx + 1, title);
|
||||
}
|
||||
off += n;
|
||||
if (snippet_raw[0] && off < (int)sizeof resp - 128) {
|
||||
char snippet_short[256];
|
||||
snprintf(snippet_short, sizeof snippet_short, "%s", snippet_raw);
|
||||
size_t slen = strlen(snippet_short);
|
||||
if (slen > 60) {
|
||||
char *sp = snippet_short + 57;
|
||||
while (sp > snippet_short && *sp != ' ') sp--;
|
||||
if (sp <= snippet_short) sp = snippet_short + 57;
|
||||
*sp = '\0';
|
||||
n = snprintf(resp + off, sizeof resp - off, " (%s...)", snippet_short);
|
||||
} else {
|
||||
n = snprintf(resp + off, sizeof resp - off, " (%s)", snippet_short);
|
||||
}
|
||||
off += n;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
irc_reply(s, reply_target, src_nick, resp);
|
||||
if (idx < result_count) {
|
||||
irc_reply(s, reply_target, src_nick, "...");
|
||||
}
|
||||
cur = results_key;
|
||||
idx = 0;
|
||||
while (idx < result_count && idx < 5) {
|
||||
const char *brace = wp_json_next_array_item(&cur);
|
||||
if (!brace) break;
|
||||
char title[256] = "";
|
||||
wp_json_get_string(brace, "\"title\":", title, sizeof title);
|
||||
if (!title[0]) { idx++; continue; }
|
||||
char enc_title[512];
|
||||
wp_url_encode(title, enc_title, sizeof enc_title);
|
||||
char link[640];
|
||||
snprintf(link, sizeof link, "Read full article: https://en.wikipedia.org/wiki/%s", enc_title);
|
||||
irc_reply(s, reply_target, src_nick, link);
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
log_stamp(); fprintf(stderr, CLR_MAGENTA "WP %s from %s:" CLR_RESET " %s\n", reply_target, src_nick, rest);
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endif
|
||||
3
irc.h
3
irc.h
|
|
@ -431,6 +431,9 @@ static void handle_privmsg(Session *s, const char *params, const char *src_nick)
|
|||
#ifdef HAS_CMD_YT
|
||||
if (cmd_yt(s, msg, reply_target, src_nick)) return;
|
||||
#endif
|
||||
#ifdef HAS_CMD_WP
|
||||
if (cmd_wp(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
|
||||
|
|
|
|||
3
zynk.c
3
zynk.c
|
|
@ -780,6 +780,9 @@ const char *json_find(const char *haystack, const char *key) {
|
|||
#ifdef HAS_CMD_YT
|
||||
#include "cmd_yt.h"
|
||||
#endif
|
||||
#ifdef HAS_CMD_WP
|
||||
#include "cmd_wp.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