aboutsummaryrefslogtreecommitdiff
path: root/xmppcb.c
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-05-21 18:56:19 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-05-21 18:56:19 +0200
commit688779fbc61d48046199d1f45549720e289ce417 (patch)
tree4ea8c13d96428176f97c0c27f665391c09a93098 /xmppcb.c
parentfb0fd67d6e7e55548c7a0a1ec67f608efffe3a94 (diff)
downloadxmppcb-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.c166
1 files changed, 164 insertions, 2 deletions
diff --git a/xmppcb.c b/xmppcb.c
index c934550..445345a 100644
--- a/xmppcb.c
+++ b/xmppcb.c
@@ -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 */