From 688779fbc61d48046199d1f45549720e289ce417 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 21 May 2026 18:56:19 +0200 Subject: Added support for user generated shell scripts: - create a director with shell scripts - call them within the chat with @ - four sample scripts --- xmppcb.c | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 164 insertions(+), 2 deletions(-) (limited to 'xmppcb.c') diff --git a/xmppcb.c b/xmppcb.c index c934550..445345a 100644 --- a/xmppcb.c +++ b/xmppcb.c @@ -12,6 +12,7 @@ #endif #include +#include #include #include #include @@ -22,9 +23,11 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -46,6 +49,7 @@ #define REQBUF 140000 #define HTTPBUF 98304 #define LLMOUT 8192 +#define CMDOUT 4096 #define CTX_FILEBUF 131072 #define CTX_MAXLINES 4000 @@ -822,13 +826,38 @@ static time_t parse_period(const char *tok) { } static const char *help_text(void) { - return + static char buf[2048]; + int n = snprintf(buf, sizeof buf, "Commands:\n" " " CMD_PREFIX " summarize [period] - summarise the chat\n" " " CMD_PREFIX " tasks [period] - extract action items\n" " " CMD_PREFIX " - ask anything\n" "\n" - " Period: 1h 1d 2d 1w 2w 1m (default: last 50 messages)"; + " Period: 1h 1d 2d 1w 2w 1m (default: last 50 messages)"); + + /* append the script commands found in CMD_DIR */ + DIR *d = opendir(CMD_DIR); + if (d) { + struct dirent *e; + int first = 1; + while ((e = readdir(d)) != NULL && n > 0 && n < (int)sizeof buf - 80) { + if (e->d_name[0] == '.') continue; + char path[512]; + struct stat stb; + snprintf(path, sizeof path, "%s/%s", CMD_DIR, e->d_name); + if (stat(path, &stb) != 0 || !S_ISREG(stb.st_mode)) continue; + if (access(path, X_OK) != 0) continue; + if (first) { + n += snprintf(buf + n, sizeof buf - (size_t)n, + "\n\nScript commands:"); + first = 0; + } + n += snprintf(buf + n, sizeof buf - (size_t)n, + "\n @%s", e->d_name); + } + closedir(d); + } + return buf; } static void muc_send(Conn *c, const char *room, const char *text) { @@ -962,6 +991,138 @@ static void handle_command(Conn *c, const char *room, const char *body) { } } +/* ------------------------------------------------------------------ */ +/* @scriptname commands */ +/* ------------------------------------------------------------------ */ + +/* Run `path` with `args` passed as a single argv element - no shell is + * involved, so the user's text cannot be interpreted as a command. + * Captures combined stdout+stderr into out (NUL-terminated, capped at + * outsz). The child is SIGKILLed after CMD_TIMEOUT_SEC seconds. + * Returns 1 if the script exited 0, else 0. */ +static int run_script(const char *path, const char *args, + char *out, size_t outsz) { + int pfd[2]; + out[0] = '\0'; + if (pipe(pfd) != 0) { + snprintf(out, outsz, "cmd: pipe failed"); + return 0; + } + pid_t pid = fork(); + if (pid < 0) { + close(pfd[0]); + close(pfd[1]); + snprintf(out, outsz, "cmd: fork failed"); + return 0; + } + if (pid == 0) { /* child */ + dup2(pfd[1], STDOUT_FILENO); + dup2(pfd[1], STDERR_FILENO); + close(pfd[0]); + close(pfd[1]); + if (args && *args) + execl(path, path, args, (char *)NULL); + else + execl(path, path, (char *)NULL); + _exit(127); /* exec failed */ + } + + close(pfd[1]); /* parent */ + size_t len = 0; + time_t deadline = time(NULL) + CMD_TIMEOUT_SEC; + int killed = 0, truncated = 0; + + /* read output until EOF, the buffer fills, or the deadline passes */ + for (;;) { + fd_set rf; + FD_ZERO(&rf); + FD_SET(pfd[0], &rf); + struct timeval tv = { 1, 0 }; + int s = select(pfd[0] + 1, &rf, NULL, NULL, &tv); + if (s < 0) { + if (errno == EINTR) continue; + break; + } + if (s > 0) { + ssize_t r = read(pfd[0], out + len, outsz - 1 - len); + if (r > 0) { + len += (size_t)r; + if (len >= outsz - 1) { truncated = 1; break; } + } else { + break; /* EOF or error */ + } + } + if (time(NULL) >= deadline) { killed = 1; break; } + } + out[len] = '\0'; + if (truncated && outsz > 32) + memcpy(out + outsz - 16, " ...[cut]", 10); + + /* The child may still be running (timeout, or it outran the buffer). + * Kill it, then reap with WNOHANG + a deadline so a child that closed + * its output but kept running cannot hang the bot on waitpid(). */ + if (killed || truncated) + kill(pid, SIGKILL); + close(pfd[0]); + int status = 0; + for (;;) { + pid_t w = waitpid(pid, &status, WNOHANG); + if (w != 0) break; /* reaped, or error */ + if (time(NULL) >= deadline) { + kill(pid, SIGKILL); + killed = 1; + } + struct timespec ts = { 0, 50000000 }; /* 50 ms */ + nanosleep(&ts, NULL); + } + rtrim(out); + + if (killed) { + snprintf(out, outsz, "cmd: timed out after %d s", CMD_TIMEOUT_SEC); + return 0; + } + if (!out[0]) + snprintf(out, outsz, "(no output)"); + return WIFEXITED(status) && WEXITSTATUS(status) == 0; +} + +/* Parse a @scriptname message and run CMD_DIR/, posting its + * output back to the room. Unknown commands are ignored silently so the + * bot does not react to @-mentions of people. */ +static void handle_script_command(Conn *c, const char *room, + const char *body) { + while (*body == ' ' || *body == '\t') body++; + if (*body != '@') return; + if (strncasecmp(body, CMD_PREFIX, strlen(CMD_PREFIX)) == 0) + return; /* that is the @bot prefix */ + body++; /* skip '@' */ + + /* command name: alphanumerics, '-' and '_' only (no path traversal) */ + char name[64]; + size_t n = 0; + while (body[n] && !isspace((unsigned char)body[n])) { + unsigned char ch = (unsigned char)body[n]; + if (!isalnum(ch) && ch != '-' && ch != '_') return; + if (n >= sizeof name - 1) return; + name[n] = (char)ch; + n++; + } + if (n == 0) return; + name[n] = '\0'; + + const char *args = body + n; + while (*args == ' ' || *args == '\t') args++; + + char path[256]; + snprintf(path, sizeof path, "%s/%s", CMD_DIR, name); + if (access(path, X_OK) != 0) return; /* not a command: stay silent */ + + fprintf(stderr, "xmppcb: running %s\n", path); + static char out[CMDOUT]; + run_script(path, args, out, sizeof out); + muc_send(c, room, out); +} + /* ------------------------------------------------------------------ */ /* stanza dispatch */ /* ------------------------------------------------------------------ */ @@ -992,6 +1153,7 @@ static void dispatch(Conn *c, const char *st) { history_append(room, nick, bdy); handle_command(c, room, bdy); + handle_script_command(c, room, bdy); } else if (strcmp(tag, "iq") == 0) { if (strstr(st, "urn:xmpp:ping")) { /* answer pings */ -- cgit v1.3