feat: add agent XMPP MAM workflow

Amolith created

Add the agent-skill command and installer, password stdin/pwd-cmd
support, compact MAM list output with field terms, receive polling via
MAM/RSM after-id, and message chat ID output. Also add orb setup
dependencies for future agents.

Amp-Thread-ID: https://ampcode.com/threads/T-019f2fe4-f392-7688-8b2f-8d40eb9fa4b8

Change summary

.agents/setup      |  16 +
src/main.c         | 183 ++++++++++++-
src/mode/mam.c     | 656 ++++++++++++++++++++++++++++++++++++++++++-----
src/mode/message.c |   8 
4 files changed, 762 insertions(+), 101 deletions(-)

Detailed changes

.agents/setup 🔗

@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if command -v apt-get >/dev/null 2>&1; then
+  sudo apt-get update
+  sudo apt-get install -y \
+    autoconf \
+    automake \
+    gcc \
+    libglib2.0-dev \
+    libgpgme-dev \
+    libstrophe-dev \
+    libtool \
+    make \
+    pkg-config
+fi

src/main.c 🔗

@@ -134,6 +134,7 @@
 #include <stdlib.h>
 #include <string.h>
 #include <glib.h>
+#include <glib/gstdio.h>
 #include <strophe.h>
 #include <errno.h>
 #include <termios.h>
@@ -315,6 +316,9 @@ void conn_handler(xmpp_conn_t *const conn, const xmpp_conn_event_t status,
 }
 
 static void _show_help();
+static int _agent_skill_command(int argc, char *argv[]);
+static char *_read_password_from_stdin(void);
+static char *_read_password_command(xmppc_t *xmppc, const char *command);
 
 /*!
  * \brief C-Main function
@@ -334,15 +338,15 @@ static void _show_help();
  */
 
 int main(int argc, char *argv[]) {
-#if XMPPC_DEVELOPMENT
-  printf("!!! WARNING: XMPPC is running in development mode !!!\n");
-#endif
-
   if(argc < 2) {
     _show_help();
     return EXIT_FAILURE;
   }
 
+  if (strcmp(argv[1], "agent-skill") == 0) {
+    return _agent_skill_command(argc, argv);
+  }
+
   INIT_XMPPC(xmppc);
 
   static int verbose_flag = 0;
@@ -399,8 +403,11 @@ int main(int argc, char *argv[]) {
         break;
 
       case 'p':
-        pwd = malloc(strlen(optarg) + 1);
-        strcpy(pwd, optarg);
+        if (strcmp(optarg, "-") == 0) {
+          pwd = _read_password_from_stdin();
+        } else {
+          pwd = g_strdup(optarg);
+        }
         break;
 
       case 'm':
@@ -447,19 +454,35 @@ int main(int argc, char *argv[]) {
       return EXIT_FAILURE;
     }
   }
-  error = NULL;
+  g_clear_error(&error);
 
-  if(jid == NULL && pwd == NULL) {
+  if(account != NULL || (jid == NULL && pwd == NULL)) {
     logInfo(&xmppc,"Loading default account\n");
     if( account == NULL ) {
       account = "default";
     }
-    jid = g_key_file_get_value (config_file, account, "jid" ,&error);
-    if( error ) {
-      logError(&xmppc, "No jid found in configuration file. %s\n", error->message);
-      return EXIT_FAILURE;
+    if (jid == NULL) {
+      jid = g_key_file_get_value (config_file, account, "jid" ,&error);
+      if( error ) {
+        logError(&xmppc, "No jid found in configuration file. %s\n", error->message);
+        return EXIT_FAILURE;
+      }
+      g_clear_error(&error);
+    }
+
+    if (pwd == NULL) {
+      pwd = g_key_file_get_value (config_file, account, "pwd", &error);
+      if (!pwd) {
+        g_clear_error(&error);
+        char *pwd_cmd = g_key_file_get_value(config_file, account, "pwd-cmd", &error);
+        if (pwd_cmd) {
+          pwd = _read_password_command(&xmppc, pwd_cmd);
+          g_free(pwd_cmd);
+        }
+        g_clear_error(&error);
+      }
+      g_clear_error(&error);
     }
-    pwd = g_key_file_get_value (config_file, account, "pwd" ,&error);
   }
 
   if ( !pwd ) {
@@ -476,7 +499,10 @@ int main(int argc, char *argv[]) {
     if (fgets(pwd, BUFSIZ, stdin) == NULL) {
         pwd[0] = '\0';
     } else {
-        pwd[strlen(pwd)-1] = '\0';
+        size_t pwd_len = strlen(pwd);
+        if (pwd_len > 0 && pwd[pwd_len - 1] == '\n') {
+          pwd[pwd_len - 1] = '\0';
+        }
     }
     tcsetattr(STDIN_FILENO, TCSANOW, &current_terminal);
     printf("\n");
@@ -511,7 +537,6 @@ int main(int argc, char *argv[]) {
   callback_t callback = {paramc, paramv, handler, &xmppc};
 
 #if XMPPC_DEVELOPMENT
-  printf("Warning: Developer-Mode: XMPP_CONN_FLAG_TRUST_TLS\n");
   xmpp_conn_set_flags(xmppc.conn, XMPP_CONN_FLAG_TRUST_TLS);
 #else
   xmpp_conn_set_flags(xmppc.conn, XMPP_CONN_FLAG_MANDATORY_TLS);
@@ -527,12 +552,131 @@ int main(int argc, char *argv[]) {
   xmpp_ctx_free(xmppc.ctx);
   xmpp_shutdown();
 
-  free(jid);
-  free(pwd);
+  g_free(jid);
+  g_free(pwd);
 
   return EXIT_SUCCESS;
 }
 
+static int _agent_skill_command(int argc, char *argv[]) {
+  const char *skill_text =
+    "xmppc agent-skill\n"
+    "\n"
+    "Use this when the user asks you to communicate over XMPP/Jabber with xmppc.\n"
+    "Before sending or receiving, read ~/.config/xmppc/AGENTS.md if it exists; it\n"
+    "contains the user's allowed accounts, preferred contacts, and local policy.\n"
+    "Never read, print, summarize, or expose ~/.config/xmppc.conf, passwords,\n"
+    "password commands, environment variables, or other secrets.\n"
+    "\n"
+    "Commands:\n"
+    "  xmppc -a <account> -m message chat <jid> <body>\n"
+    "  xmppc -a <account> -m mam list <jid> [MAM_FIELD=VALUE ...]\n"
+    "  xmppc -a <account> -m mam receive <jid> after-id=<mam-id> [timeout=300s] [max=5]\n"
+    "\n"
+    "MAM fields for list/receive are unordered key=value terms: with=, start=,\n"
+    "end=, after-id=, before-id=, ids=, max=. A bare JID is shorthand for with=.\n"
+    "A second bare argument is an error. start= accepts XMPP timestamps or relative\n"
+    "values like -5m; max defaults to 5 and is capped at 50.\n"
+    "\n"
+    "Message archive output is one compact XML record per line:\n"
+    "  <message id=\"mam-id\" message-id=\"stanza-id\" stamp=\"...\" from=\"...\" to=\"...\"><body>escaped body</body></message>\n"
+    "The id attribute is the MAM archive cursor. message-id is the stanza id.\n"
+    "Use the MAM id, not the stanza id, as receive after-id=.\n"
+    "\n"
+    "Receive behavior:\n"
+    "  xmppc -a <account> -m mam receive <jid> after-id=<mam-id>\n"
+    "blocks until at least one newer archived message is available or timeout is\n"
+    "reached. It polls with real MAM/RSM after-id queries and prints at most max\n"
+    "messages. If no message arrives before timeout, it exits without output.\n";
+
+  const char *pointer_skill =
+    "---\n"
+    "name: communicating-through-xmppc\n"
+    "description: \"Communicates with the user and other contacts over XMPP/Jabber using xmppc. Use when asked to send, list, or receive Jabber/XMPP messages.\"\n"
+    "---\n"
+    "\n"
+    "Run `xmppc agent-skill` and follow the instructions it prints.\n";
+
+  if (argc == 2) {
+    printf("%s", skill_text);
+    return EXIT_SUCCESS;
+  }
+
+  if (argc == 3 && strcmp(argv[2], "install") == 0) {
+    const char *home = g_get_home_dir();
+    if (!home) {
+      fprintf(stderr, "Could not determine home directory\n");
+      return EXIT_FAILURE;
+    }
+
+    char *skill_dir = g_build_filename(home, ".config", "agents", "skills", "communicating-through-xmppc", NULL);
+    char *skill_file = g_build_filename(skill_dir, "SKILL.md", NULL);
+
+    if (g_mkdir_with_parents(skill_dir, 0700) != 0) {
+      fprintf(stderr, "Could not create skill directory: %s\n", skill_dir);
+      g_free(skill_file);
+      g_free(skill_dir);
+      return EXIT_FAILURE;
+    }
+
+    GError *error = NULL;
+    if (!g_file_set_contents(skill_file, pointer_skill, -1, &error)) {
+      fprintf(stderr, "Could not write skill file: %s\n", error ? error->message : "unknown error");
+      g_clear_error(&error);
+      g_free(skill_file);
+      g_free(skill_dir);
+      return EXIT_FAILURE;
+    }
+
+    printf("Installed communicating-through-xmppc skill at %s\n", skill_file);
+    g_free(skill_file);
+    g_free(skill_dir);
+    return EXIT_SUCCESS;
+  }
+
+  fprintf(stderr, "Usage: xmppc agent-skill [install]\n");
+  return EXIT_FAILURE;
+}
+
+static char *_read_password_from_stdin(void) {
+  char buffer[BUFSIZ];
+  if (fgets(buffer, sizeof(buffer), stdin) == NULL) {
+    return g_strdup("");
+  }
+  g_strchomp(buffer);
+  return g_strdup(buffer);
+}
+
+static char *_read_password_command(xmppc_t *xmppc, const char *command) {
+  char *stdout_buf = NULL;
+  char *stderr_buf = NULL;
+  int exit_status = 0;
+  GError *error = NULL;
+
+  gboolean ok = g_spawn_command_line_sync(command, &stdout_buf, &stderr_buf, &exit_status, &error);
+  g_free(stderr_buf);
+
+  if (!ok) {
+    logError(xmppc, "pwd-cmd failed: %s\n", error ? error->message : "unknown error");
+    g_clear_error(&error);
+    g_free(stdout_buf);
+    return NULL;
+  }
+
+  if (exit_status != 0) {
+    logError(xmppc, "pwd-cmd failed\n");
+    g_free(stdout_buf);
+    return NULL;
+  }
+
+  if (!stdout_buf) {
+    return g_strdup("");
+  }
+
+  g_strchomp(stdout_buf);
+  return stdout_buf;
+}
+
 static void _show_help() {
 #ifdef XMPPC_DEVELOPMENT
   printf("%s - Development\n", PACKAGE_STRING);
@@ -544,8 +688,10 @@ static void _show_help() {
   printf("  -h / --help                 Display this information.\n");
   printf("  -j / --jid <jid>            Jabber ID\n");
   printf("  -p / --pwd <password>       Passwort\n");
+  printf("                               Use '-' to read password from stdin\n");
   printf("  -a / --account <account>    Account\n");
   printf("  -m / --mode <mode>          xmppc mode\n");
+  printf("  agent-skill [install]       Print or install the agent-facing xmppc skill\n");
   printf("\n");
   printf("Modes:\n");
   printf("  -m --mode roster            xmppc roster mode\n");
@@ -573,7 +719,8 @@ static void _show_help() {
   printf("    list                      List bookmarks\n");
   printf("\n");
   printf("  -m --mode mam               Message Archive Management (XEP-0313)\n");
-  printf("    list <jid>                List messages from <jid>\n");
+  printf("    list <jid> [field=value]  List archived messages from <jid>\n");
+  printf("    receive <jid> after-id=<id> [timeout=300s] [max=5]\n");
   printf("\n");
   printf("  -m --mode discovery         Service Discovery (XEP-0030)\n");
   printf("    info <jid>                info request for <jid>\n");

src/mode/mam.c 🔗

@@ -41,125 +41,506 @@
  */
 
 #include "mam.h"
-#include "stdio.h"
+#include "glib.h"
 #include "stdbool.h"
+#include "stdio.h"
 #include "stdlib.h"
 #include "string.h"
 
-static void _mam_request(xmppc_t *xmppc, char* to, bool pretty);
-static int _mam_show(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
+#define MAM_NS "urn:xmpp:mam:2"
+#define FORWARD_NS "urn:xmpp:forward:0"
+#define DELAY_NS "urn:xmpp:delay"
+#define RSM_NS "http://jabber.org/protocol/rsm"
+
+#define MAM_DEFAULT_MAX 5
+#define MAM_MAX_CAP 50
+#define MAM_DEFAULT_TIMEOUT_SECONDS 300
+#define MAM_POLL_PERIOD_MS 2000
+
+typedef struct {
+  char *with;
+  char *start;
+  char *end;
+  char *after_id;
+  char *before_id;
+  char *ids;
+  int max;
+  int timeout;
+  bool has_narrowing;
+} mam_options_t;
+
+typedef struct {
+  xmppc_t *xmppc;
+  mam_options_t options;
+  bool receive;
+  int messages_total;
+  gint64 deadline_us;
+  char *queryid;
+} mam_run_t;
+
+static void _mam_options_init(mam_options_t *options);
+static void _mam_options_free(mam_options_t *options);
+static bool _mam_parse_terms(xmppc_t *xmppc, int argc, char *argv[], mam_options_t *options, bool receive);
+static void _mam_start_request(xmppc_t *xmppc, mam_options_t *options, bool pretty, bool receive);
+static void _mam_send_request(mam_run_t *run);
+static int _mam_finish(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
+static int _mam_receive_poll(xmpp_conn_t *const conn, void *const userdata);
 static int _mam_display_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
 static int _mam_display_pretty_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
+static void _mam_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type);
+static void _mam_add_ids_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *ids);
+static void _mam_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text);
+static bool _mam_set_string_option(xmppc_t *xmppc, char **target, const char *key, const char *value, bool allow_empty);
+static bool _mam_parse_max(xmppc_t *xmppc, mam_options_t *options, const char *value);
+static bool _mam_parse_timeout(xmppc_t *xmppc, mam_options_t *options, const char *value);
+static bool _mam_parse_duration_seconds(const char *value, gint64 *seconds);
+static char *_mam_normalize_datetime(const char *value);
+static bool _mam_result_matches(mam_run_t *run, xmpp_stanza_t *result);
+static void _mam_print_compact_message(xmpp_stanza_t *result);
+static void _mam_print_escaped_attr(const char *text);
+static void _mam_print_escaped_text(const char *text);
+static void _mam_print_escaped(const char *text, bool attribute);
+static void _mam_usage(xmppc_t *xmppc);
 
 void mam_execute_command (xmppc_t *xmppc, int argc, char *argv[]) {
-  if(argc == 2) {
+  mam_options_t options;
+
+  if(argc >= 1) {
     if (strcmp("list", argv[0]) == 0) {
-      _mam_request(xmppc, argv[1], false);
+      if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, false)) {
+        _mam_start_request(xmppc, &options, false, false);
+      } else {
+        xmpp_disconnect(xmppc->conn);
+      }
       return;
     } else if (strcmp("pretty", argv[0]) == 0) {
-      _mam_request(xmppc, argv[1], true);
+      if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, false)) {
+        _mam_start_request(xmppc, &options, true, false);
+      } else {
+        xmpp_disconnect(xmppc->conn);
+      }
+      return;
+    } else if (strcmp("receive", argv[0]) == 0) {
+      if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, true)) {
+        _mam_start_request(xmppc, &options, false, true);
+      } else {
+        xmpp_disconnect(xmppc->conn);
+      }
       return;
     }
   }
 
-  printf("Command unbekannt!");
+  _mam_usage(xmppc);
   xmpp_disconnect(xmppc->conn);
 }
 
-static void _mam_request(xmppc_t *xmppc, char* to, bool pretty) {
+static void _mam_options_init(mam_options_t *options) {
+  memset(options, 0, sizeof(*options));
+  options->max = MAM_DEFAULT_MAX;
+  options->timeout = MAM_DEFAULT_TIMEOUT_SECONDS;
+}
+
+static void _mam_options_free(mam_options_t *options) {
+  g_free(options->with);
+  g_free(options->start);
+  g_free(options->end);
+  g_free(options->after_id);
+  g_free(options->before_id);
+  g_free(options->ids);
+}
+
+static bool _mam_parse_terms(xmppc_t *xmppc, int argc, char *argv[], mam_options_t *options, bool receive) {
+  _mam_options_init(options);
+
+  for (int i = 0; i < argc; i++) {
+    char *term = argv[i];
+    char *equals = strchr(term, '=');
+
+    if (!equals) {
+      if (!_mam_set_string_option(xmppc, &options->with, "with", term, false)) {
+        _mam_options_free(options);
+        return false;
+      }
+      options->has_narrowing = true;
+      continue;
+    }
+
+    char *key = g_strndup(term, equals - term);
+    const char *value = equals + 1;
+
+    if (strcmp(key, "with") == 0) {
+      if (!_mam_set_string_option(xmppc, &options->with, key, value, false)) {
+        g_free(key);
+        _mam_options_free(options);
+        return false;
+      }
+      options->has_narrowing = true;
+    } else if (strcmp(key, "start") == 0) {
+      char *normalized = _mam_normalize_datetime(value);
+      if (!_mam_set_string_option(xmppc, &options->start, key, normalized, false)) {
+        g_free(normalized);
+        g_free(key);
+        _mam_options_free(options);
+        return false;
+      }
+      g_free(normalized);
+      options->has_narrowing = true;
+    } else if (strcmp(key, "end") == 0) {
+      char *normalized = _mam_normalize_datetime(value);
+      if (!_mam_set_string_option(xmppc, &options->end, key, normalized, false)) {
+        g_free(normalized);
+        g_free(key);
+        _mam_options_free(options);
+        return false;
+      }
+      g_free(normalized);
+      options->has_narrowing = true;
+    } else if (strcmp(key, "after-id") == 0) {
+      if (!_mam_set_string_option(xmppc, &options->after_id, key, value, false)) {
+        g_free(key);
+        _mam_options_free(options);
+        return false;
+      }
+      options->has_narrowing = true;
+    } else if (strcmp(key, "before-id") == 0) {
+      if (!_mam_set_string_option(xmppc, &options->before_id, key, value, true)) {
+        g_free(key);
+        _mam_options_free(options);
+        return false;
+      }
+      options->has_narrowing = true;
+    } else if (strcmp(key, "ids") == 0) {
+      if (!_mam_set_string_option(xmppc, &options->ids, key, value, false)) {
+        g_free(key);
+        _mam_options_free(options);
+        return false;
+      }
+      options->has_narrowing = true;
+    } else if (strcmp(key, "max") == 0) {
+      if (!_mam_parse_max(xmppc, options, value)) {
+        g_free(key);
+        _mam_options_free(options);
+        return false;
+      }
+    } else if (receive && strcmp(key, "timeout") == 0) {
+      if (!_mam_parse_timeout(xmppc, options, value)) {
+        g_free(key);
+        _mam_options_free(options);
+        return false;
+      }
+    } else {
+      logError(xmppc, "Unknown MAM field: %s\n", key);
+      g_free(key);
+      _mam_options_free(options);
+      return false;
+    }
+
+    g_free(key);
+  }
+
+  if (receive) {
+    if (!options->with) {
+      logError(xmppc, "mam receive requires a JID or with=<jid>\n");
+      _mam_options_free(options);
+      return false;
+    }
+    if (!options->after_id) {
+      logError(xmppc, "mam receive requires after-id=<mam-id>\n");
+      _mam_options_free(options);
+      return false;
+    }
+  } else if (!options->has_narrowing) {
+    logError(xmppc, "mam list requires a JID, with=, start=, end=, after-id=, before-id=, or ids=\n");
+    _mam_options_free(options);
+    return false;
+  }
+
+  if (!options->start && !options->end && !options->after_id && !options->before_id && !options->ids) {
+    char *start = _mam_normalize_datetime("-5m");
+    options->start = start;
+  }
+
+  return true;
+}
+
+static void _mam_start_request(xmppc_t *xmppc, mam_options_t *options, bool pretty, bool receive) {
+  mam_run_t *run = g_new0(mam_run_t, 1);
+  run->xmppc = xmppc;
+  run->options = *options;
+  run->receive = receive;
+  if (receive) {
+    run->deadline_us = g_get_monotonic_time() + ((gint64)run->options.timeout * G_USEC_PER_SEC);
+  }
 
   if (pretty) {
-    xmpp_handler_add (xmppc->conn, _mam_display_pretty_message, NULL,"message",NULL, xmppc);
+    xmpp_handler_add (xmppc->conn, _mam_display_pretty_message, NULL,"message",NULL, run);
   } else {
-    xmpp_handler_add (xmppc->conn, _mam_display_message, NULL,"message",NULL, xmppc);
-  }
-
-    char* id = xmpp_uuid_gen(xmppc->ctx);
-    xmpp_stanza_t *iq = xmpp_iq_new(xmppc->ctx, "set", id);
-    // query
-    xmpp_stanza_t* query = xmpp_stanza_new(xmppc->ctx);
-    xmpp_stanza_set_name(query, "query");
-    xmpp_stanza_set_ns(query, "urn:xmpp:mam:2");
-
-  
-    xmpp_stanza_t* x = xmpp_stanza_new(xmppc->ctx);
-    xmpp_stanza_set_name(x,"x");
-    xmpp_stanza_set_ns(x, "jabber:x:data"),
-    xmpp_stanza_set_type(x,"submit");
-    
-    xmpp_stanza_add_child(query,x);
-
-    xmpp_stanza_t* f = xmpp_stanza_new(xmppc->ctx);
-    xmpp_stanza_set_name(f,"field");
-    xmpp_stanza_set_type(f,"hidden");
-    xmpp_stanza_set_attribute(f, "var", "FORM_TYPE");
-    xmpp_stanza_t* v = xmpp_stanza_new(xmppc->ctx);
-    xmpp_stanza_set_name(v,"value");
-    xmpp_stanza_t* b = xmpp_stanza_new(xmppc->ctx);
-    xmpp_stanza_set_text(b, "urn:xmpp:mam:2");
-    
-    xmpp_stanza_add_child(x,f);
-    xmpp_stanza_add_child(f,v);
-    xmpp_stanza_add_child(v,b);
-
-    xmpp_stanza_release(f);
-    xmpp_stanza_release(v);
-    xmpp_stanza_release(b);
-
-    f = xmpp_stanza_new(xmppc->ctx);
-    xmpp_stanza_set_name(f,"field");
-    xmpp_stanza_set_attribute(f, "var", "with");
-    v = xmpp_stanza_new(xmppc->ctx);
-    xmpp_stanza_set_name(v,"value");
-    b = xmpp_stanza_new(xmppc->ctx);
-    xmpp_stanza_set_text(b, to);
-    //xmpp_stanza_set_text(b, xmpp_conn_get_jid (xmppc->conn));
-
-    xmpp_stanza_add_child(x,f);
-    xmpp_stanza_add_child(f,v);
-    xmpp_stanza_add_child(v,b);
-
-    xmpp_stanza_add_child(iq,query);
-    xmpp_id_handler_add(xmppc->conn, _mam_show, id, xmppc);
-    xmpp_send(xmppc->conn,iq);
-
-    xmpp_stanza_release(iq);
-    xmpp_stanza_release(x);
-    xmpp_stanza_release(f);
-    xmpp_stanza_release(v);
-    xmpp_stanza_release(b);
-    xmpp_stanza_release(query);
-
-    free(id);
-}
-
-int _mam_show(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata){
+    xmpp_handler_add (xmppc->conn, _mam_display_message, NULL,"message",NULL, run);
+  }
+
+  _mam_send_request(run);
+}
+
+static void _mam_send_request(mam_run_t *run) {
+  xmppc_t *xmppc = run->xmppc;
+  xmpp_ctx_t *ctx = xmppc->ctx;
+  char max[16];
+
+  g_snprintf(max, sizeof(max), "%d", run->options.max);
+
+  char* id = xmpp_uuid_gen(ctx);
+  g_free(run->queryid);
+  run->queryid = g_strdup(id);
+
+  xmpp_stanza_t *iq = xmpp_iq_new(ctx, "set", id);
+  xmpp_stanza_t *query = xmpp_stanza_new(ctx);
+  xmpp_stanza_set_name(query, "query");
+  xmpp_stanza_set_ns(query, MAM_NS);
+  xmpp_stanza_set_attribute(query, "queryid", id);
+
+  xmpp_stanza_t* x = xmpp_stanza_new(ctx);
+  xmpp_stanza_set_name(x,"x");
+  xmpp_stanza_set_ns(x, "jabber:x:data");
+  xmpp_stanza_set_type(x,"submit");
+  xmpp_stanza_add_child(query,x);
+
+  _mam_add_field(ctx, x, "FORM_TYPE", MAM_NS, "hidden");
+
+  if (run->options.with) {
+    _mam_add_field(ctx, x, "with", run->options.with, NULL);
+  }
+  if (run->options.start) {
+    _mam_add_field(ctx, x, "start", run->options.start, NULL);
+  }
+  if (run->options.end) {
+    _mam_add_field(ctx, x, "end", run->options.end, NULL);
+  }
+  if (run->options.ids) {
+    _mam_add_ids_field(ctx, x, run->options.ids);
+  }
+
+  xmpp_stanza_t *set = xmpp_stanza_new(ctx);
+  xmpp_stanza_set_name(set, "set");
+  xmpp_stanza_set_ns(set, RSM_NS);
+  _mam_add_text_child(ctx, set, "max", max);
+  if (run->options.after_id) {
+    _mam_add_text_child(ctx, set, "after", run->options.after_id);
+  }
+  if (run->options.before_id) {
+    _mam_add_text_child(ctx, set, "before", run->options.before_id);
+  }
+  xmpp_stanza_add_child(query, set);
+
+  xmpp_stanza_add_child(iq,query);
+  xmpp_id_handler_add(xmppc->conn, _mam_finish, id, run);
+  xmpp_send(xmppc->conn,iq);
+
+  xmpp_stanza_release(iq);
+  xmpp_stanza_release(query);
+  xmpp_stanza_release(x);
+  xmpp_stanza_release(set);
+  free(id);
+}
+
+static int _mam_finish(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata){
+  mam_run_t *run = (mam_run_t *)userdata;
+  const char *type = xmpp_stanza_get_type(stanza);
+
+  if (type && strcmp(type, "error") == 0) {
+    logError(run->xmppc, "MAM request failed\n");
+    xmpp_disconnect(conn);
+    return 0;
+  }
+
   xmpp_stanza_t *fin = xmpp_stanza_get_child_by_name(stanza,"fin");
   if(fin) {
+    if (run->receive && run->messages_total == 0 && g_get_monotonic_time() < run->deadline_us) {
+      xmpp_timed_handler_add(conn, _mam_receive_poll, MAM_POLL_PERIOD_MS, run);
+    } else {
+      xmpp_disconnect(conn);
+    }
+  }
+  return 0;
+}
+
+static int _mam_receive_poll(xmpp_conn_t *const conn, void *const userdata) {
+  mam_run_t *run = (mam_run_t *)userdata;
+
+  if (g_get_monotonic_time() >= run->deadline_us) {
     xmpp_disconnect(conn);
+    return 0;
   }
-  return 1;
+
+  _mam_send_request(run);
+  return 0;
+}
+
+static void _mam_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type) {
+  xmpp_stanza_t* field = xmpp_stanza_new(ctx);
+  xmpp_stanza_set_name(field,"field");
+  xmpp_stanza_set_attribute(field, "var", var);
+  if (type) {
+    xmpp_stanza_set_type(field, type);
+  }
+
+  _mam_add_text_child(ctx, field, "value", value);
+  xmpp_stanza_add_child(x, field);
+  xmpp_stanza_release(field);
+}
+
+static void _mam_add_ids_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *ids) {
+  xmpp_stanza_t* field = xmpp_stanza_new(ctx);
+  xmpp_stanza_set_name(field,"field");
+  xmpp_stanza_set_attribute(field, "var", "ids");
+
+  char **parts = g_strsplit(ids, ",", -1);
+  for (int i = 0; parts[i] != NULL; i++) {
+    if (parts[i][0] != '\0') {
+      _mam_add_text_child(ctx, field, "value", parts[i]);
+    }
+  }
+  g_strfreev(parts);
+
+  xmpp_stanza_add_child(x, field);
+  xmpp_stanza_release(field);
+}
+
+static void _mam_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text) {
+  xmpp_stanza_t *child = xmpp_stanza_new(ctx);
+  xmpp_stanza_set_name(child, name);
+
+  xmpp_stanza_t *body = xmpp_stanza_new(ctx);
+  xmpp_stanza_set_text(body, text ? text : "");
+  xmpp_stanza_add_child(child, body);
+  xmpp_stanza_release(body);
+
+  xmpp_stanza_add_child(parent, child);
+  xmpp_stanza_release(child);
+}
+
+static bool _mam_set_string_option(xmppc_t *xmppc, char **target, const char *key, const char *value, bool allow_empty) {
+  if (*target) {
+    logError(xmppc, "Duplicate MAM field: %s\n", key);
+    return false;
+  }
+  if (!allow_empty && (!value || value[0] == '\0')) {
+    logError(xmppc, "MAM field requires a value: %s\n", key);
+    return false;
+  }
+  *target = g_strdup(value ? value : "");
+  return true;
+}
+
+static bool _mam_parse_max(xmppc_t *xmppc, mam_options_t *options, const char *value) {
+  char *end = NULL;
+  gint64 parsed = g_ascii_strtoll(value, &end, 10);
+
+  if (!value || value[0] == '\0' || !end || *end != '\0' || parsed < 1) {
+    logError(xmppc, "max must be a positive integer\n");
+    return false;
+  }
+
+  if (parsed > MAM_MAX_CAP) {
+    parsed = MAM_MAX_CAP;
+  }
+
+  options->max = (int)parsed;
+  return true;
+}
+
+static bool _mam_parse_timeout(xmppc_t *xmppc, mam_options_t *options, const char *value) {
+  gint64 seconds = 0;
+
+  if (!_mam_parse_duration_seconds(value, &seconds) || seconds < 0 || seconds > G_MAXINT) {
+    logError(xmppc, "timeout must be a duration like 30s, 5m, or 1h\n");
+    return false;
+  }
+
+  options->timeout = (int)seconds;
+  return true;
+}
+
+static bool _mam_parse_duration_seconds(const char *value, gint64 *seconds) {
+  char *end = NULL;
+  gint64 parsed = 0;
+  gint64 multiplier = 1;
+
+  if (!value || value[0] == '\0') {
+    return false;
+  }
+
+  parsed = g_ascii_strtoll(value, &end, 10);
+  if (end == value || parsed < 0) {
+    return false;
+  }
+
+  if (*end == '\0') {
+    multiplier = 1;
+  } else if (end[1] == '\0') {
+    if (*end == 's') {
+      multiplier = 1;
+    } else if (*end == 'm') {
+      multiplier = 60;
+    } else if (*end == 'h') {
+      multiplier = 60 * 60;
+    } else if (*end == 'd') {
+      multiplier = 60 * 60 * 24;
+    } else {
+      return false;
+    }
+  } else {
+    return false;
+  }
+
+  *seconds = parsed * multiplier;
+  return true;
+}
+
+static char *_mam_normalize_datetime(const char *value) {
+  if (value && value[0] == '-' && value[1] != '\0') {
+    gint64 seconds = 0;
+    if (_mam_parse_duration_seconds(value + 1, &seconds) && seconds > 0) {
+      GDateTime *now = g_date_time_new_now_utc();
+      GDateTime *then = g_date_time_add_seconds(now, -seconds);
+      char *formatted = g_date_time_format(then, "%Y-%m-%dT%H:%M:%SZ");
+      g_date_time_unref(then);
+      g_date_time_unref(now);
+      return formatted;
+    }
+  }
+
+  return g_strdup(value);
 }
 
+static bool _mam_result_matches(mam_run_t *run, xmpp_stanza_t *result) {
+  const char *ns = xmpp_stanza_get_ns(result);
+  if (!ns || strcmp(MAM_NS, ns) != 0) {
+    return false;
+  }
+
+  const char *queryid = xmpp_stanza_get_attribute(result, "queryid");
+  return !queryid || !run->queryid || strcmp(queryid, run->queryid) == 0;
+}
 
 static int _mam_display_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata) {
+  mam_run_t *run = (mam_run_t *)userdata;
   xmpp_stanza_t* result = xmpp_stanza_get_child_by_name(stanza,"result");
-  if( result && strcmp("urn:xmpp:mam:2", xmpp_stanza_get_ns(result)) == 0 ) {
-    char* s;
-    size_t len;
-    xmpp_stanza_to_text(xmpp_stanza_get_children(result) ,&s,&len);
-    printf("%s%s\x1b[m\n", ANSI_COLOR_YELLOW, s);
+
+  if (result && _mam_result_matches(run, result) && run->messages_total < run->options.max) {
+    _mam_print_compact_message(result);
+    run->messages_total++;
   }
   return 1;
 }
 
 static int _mam_display_pretty_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata)
 {
+  mam_run_t *run = (mam_run_t *)userdata;
   xmpp_stanza_t* result = xmpp_stanza_get_child_by_name(stanza,"result");
 
-  if( result && strcmp("urn:xmpp:mam:2", xmpp_stanza_get_ns(result)) == 0 ) {
+  if( result && _mam_result_matches(run, result) && run->messages_total < run->options.max ) {
 
-    xmpp_stanza_t* forwarded = xmpp_stanza_get_child_by_ns(result, "urn:xmpp:forward:0");
+    xmpp_stanza_t* forwarded = xmpp_stanza_get_child_by_ns(result, FORWARD_NS);
     if (forwarded) {
 
       xmpp_stanza_t* message = xmpp_stanza_get_child_by_name(forwarded, "message");
@@ -169,8 +550,9 @@ static int _mam_display_pretty_message(xmpp_conn_t *const conn, xmpp_stanza_t *c
 
         if (body) {
           char *text = xmpp_stanza_get_text(body);
-          printf("%s%s\x1b[m: %s\n", ANSI_COLOR_YELLOW, from, text);
+          printf("%s: %s\n", from ? from : "", text ? text : "");
           free(text);
+          run->messages_total++;
         }
       }
     }
@@ -178,3 +560,115 @@ static int _mam_display_pretty_message(xmpp_conn_t *const conn, xmpp_stanza_t *c
 
   return 1;
 }
+
+static void _mam_print_compact_message(xmpp_stanza_t *result) {
+  const char *mam_id = xmpp_stanza_get_attribute(result, "id");
+  const char *message_id = "";
+  const char *stamp = "";
+  const char *from = "";
+  const char *to = "";
+  char *body_text = NULL;
+
+  xmpp_stanza_t* forwarded = xmpp_stanza_get_child_by_ns(result, FORWARD_NS);
+  xmpp_stanza_t* message = forwarded ? xmpp_stanza_get_child_by_name(forwarded, "message") : NULL;
+
+  if (forwarded) {
+    xmpp_stanza_t* delay = xmpp_stanza_get_child_by_ns(forwarded, DELAY_NS);
+    if (delay && xmpp_stanza_get_attribute(delay, "stamp")) {
+      stamp = xmpp_stanza_get_attribute(delay, "stamp");
+    }
+  }
+
+  if (message) {
+    xmpp_stanza_t* body = xmpp_stanza_get_child_by_name(message, "body");
+    if (xmpp_stanza_get_id(message)) {
+      message_id = xmpp_stanza_get_id(message);
+    }
+    if (xmpp_stanza_get_from(message)) {
+      from = xmpp_stanza_get_from(message);
+    }
+    if (xmpp_stanza_get_to(message)) {
+      to = xmpp_stanza_get_to(message);
+    }
+    if (body) {
+      body_text = xmpp_stanza_get_text(body);
+    }
+  }
+
+  printf("<message id=\"");
+  _mam_print_escaped_attr(mam_id ? mam_id : "");
+  printf("\" message-id=\"");
+  _mam_print_escaped_attr(message_id);
+  printf("\" stamp=\"");
+  _mam_print_escaped_attr(stamp);
+  printf("\" from=\"");
+  _mam_print_escaped_attr(from);
+  printf("\" to=\"");
+  _mam_print_escaped_attr(to);
+  printf("\"><body>");
+  _mam_print_escaped_text(body_text ? body_text : "");
+  printf("</body></message>\n");
+  fflush(stdout);
+
+  free(body_text);
+}
+
+static void _mam_print_escaped_attr(const char *text) {
+  _mam_print_escaped(text, true);
+}
+
+static void _mam_print_escaped_text(const char *text) {
+  _mam_print_escaped(text, false);
+}
+
+static void _mam_print_escaped(const char *text, bool attribute) {
+  const unsigned char *p = (const unsigned char *)(text ? text : "");
+
+  while (*p) {
+    switch (*p) {
+      case '&':
+        printf("&amp;");
+        break;
+      case '<':
+        printf("&lt;");
+        break;
+      case '>':
+        printf("&gt;");
+        break;
+      case '"':
+        if (attribute) {
+          printf("&quot;");
+        } else {
+          putchar('"');
+        }
+        break;
+      case '\n':
+        printf("&#10;");
+        break;
+      case '\r':
+        printf("&#13;");
+        break;
+      case '\t':
+        if (attribute) {
+          printf("&#9;");
+        } else {
+          putchar('\t');
+        }
+        break;
+      default:
+        if (*p < 0x20) {
+          printf("&#x%02X;", *p);
+        } else {
+          putchar(*p);
+        }
+        break;
+    }
+    p++;
+  }
+}
+
+static void _mam_usage(xmppc_t *xmppc) {
+  logError(xmppc, "Usage:\n");
+  logError(xmppc, "  mam list <jid> [with=<jid>] [start=<time>] [end=<time>] [after-id=<id>] [before-id=<id>] [ids=<id>[,<id>...]] [max=<n>]\n");
+  logError(xmppc, "  mam receive <jid> after-id=<id> [timeout=300s] [max=5]\n");
+}

src/mode/message.c 🔗

@@ -41,6 +41,7 @@
  */
 
 #include "message.h"
+#include "stdlib.h"
 #include "string.h"
 
 static void _message_send_text(xmppc_t *xmppc, char* to, char* message);
@@ -62,8 +63,11 @@ void _message_send_text(xmppc_t *xmppc, char* to, char* text) {
   message = xmpp_message_new(xmpp_conn_get_context(conn), "chat", to, id);
   int res = xmpp_message_set_body(message, text);
   if(res == 0) {
-    xmpp_send(conn, message); 
+    xmpp_send(conn, message);
+    printf("%s\n", id);
+    fflush(stdout);
   }
+  xmpp_stanza_release(message);
+  free(id);
 }
 
-