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:
Johannes Findeisen 2026-07-26 00:23:38 +02:00
commit 19b07fa76f
7 changed files with 443 additions and 5 deletions

View file

@ -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.

View file

@ -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}")

View file

@ -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) |

View file

@ -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
View 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
View file

@ -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
View file

@ -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