diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-05-21 18:56:19 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-05-21 18:56:19 +0200 |
| commit | 688779fbc61d48046199d1f45549720e289ce417 (patch) | |
| tree | 4ea8c13d96428176f97c0c27f665391c09a93098 /xmppcb.c | |
| parent | fb0fd67d6e7e55548c7a0a1ec67f608efffe3a94 (diff) | |
| download | xmppcb-688779fbc61d48046199d1f45549720e289ce417.tar.gz xmppcb-688779fbc61d48046199d1f45549720e289ce417.zip | |
Added support for user generated shell scripts:
- create a director with shell scripts
- call them within the chat with @<script-name>
- four sample scripts
Diffstat (limited to 'xmppcb.c')
| -rw-r--r-- | xmppcb.c | 166 |
1 files changed, 164 insertions, 2 deletions
@@ -12,6 +12,7 @@ #endif #include <ctype.h> +#include <dirent.h> #include <errno.h> #include <netdb.h> #include <poll.h> @@ -22,9 +23,11 @@ #include <strings.h> #include <time.h> #include <unistd.h> +#include <sys/select.h> #include <sys/socket.h> #include <sys/stat.h> #include <sys/types.h> +#include <sys/wait.h> #include <openssl/err.h> #include <openssl/ssl.h> @@ -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 " <question> - 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) { @@ -963,6 +992,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/<scriptname>, 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 */ |
