diff --git a/CHANGELOG.md b/CHANGELOG.md index f893e11..4d39a62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,24 @@ +2026.07.26 - Wikipedia search command. (0.43.0) + +- Added `!wp ` 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. diff --git a/CMakeLists.txt b/CMakeLists.txt index 12a3543..04043b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}") diff --git a/README.md b/README.md index bd09526..31497e5 100644 --- a/README.md +++ b/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, 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 ` (via YouTube oEmbed API) +- **Wikipedia Search**: Search Wikipedia with `!wp ` (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` | `: ` | 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 ` | Check when a user was last seen | | `!tell ` | Leave a message for an offline user | | `!yt ` | Show YouTube video title and channel name | +| `!wp ` | 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) | diff --git a/cmd_help.h b/cmd_help.h index fd531fe..5b7a7fe 100644 --- a/cmd_help.h +++ b/cmd_help.h @@ -104,6 +104,10 @@ int cmd_help(Session *s, const char *msg, const char *reply_target, const char * n = snprintf(p, rem, " !yt ,"); p += n; rem -= n; #endif +#ifdef HAS_CMD_WP + n = snprintf(p, rem, " !wp ,"); + p += n; rem -= n; +#endif #ifdef HAS_CMD_QUIT n = snprintf(p, rem, " !quit/!die (ops)"); p += n; rem -= n; diff --git a/cmd_wp.h b/cmd_wp.h new file mode 100644 index 0000000..8333b73 --- /dev/null +++ b/cmd_wp.h @@ -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 " 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 "); + 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 diff --git a/irc.h b/irc.h index 035601c..9cf02af 100644 --- a/irc.h +++ b/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 diff --git a/zynk.c b/zynk.c index 2bc0ba1..7f6ffe6 100644 --- a/zynk.c +++ b/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