feat: print archive cursor after sending messages

Amolith created

After sending a chat message, poll MAM for the archived forwarded stanza
by message id/origin-id and print a compact <sent archive-id=...
message-id=... to=.../> record. Add timeout handling so missing MAM
responses fall back without hanging, and update the agent-skill
instructions.

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

Change summary

src/main.c         |   6 
src/mode/message.c | 311 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 312 insertions(+), 5 deletions(-)

Detailed changes

src/main.c 🔗

@@ -573,6 +573,12 @@ static int _agent_skill_command(int argc, char *argv[]) {
     "  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"
+    "Sending prints one compact XML record after the sent stanza appears in MAM:\n"
+    "  <sent archive-id=\"mam-id\" message-id=\"stanza-id\" to=\"jid\"/>\n"
+    "Use archive-id as the receive after-id cursor when waiting for a reply. If\n"
+    "archive-id is empty, the message was sent but the archive cursor was not\n"
+    "resolved before timeout.\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"

src/mode/message.c 🔗

@@ -41,33 +41,334 @@
  */
 
 #include "message.h"
+#include "glib.h"
+#include "stdbool.h"
 #include "stdlib.h"
 #include "string.h"
 
+#define MAM_NS "urn:xmpp:mam:2"
+#define FORWARD_NS "urn:xmpp:forward:0"
+#define RSM_NS "http://jabber.org/protocol/rsm"
+#define SID_NS "urn:xmpp:sid:0"
+
+#define MESSAGE_ARCHIVE_LOOKUP_TIMEOUT_SECONDS 15
+#define MESSAGE_ARCHIVE_LOOKUP_POLL_MS 1000
+
+typedef struct {
+  xmppc_t *xmppc;
+  char *to;
+  char *message_id;
+  char *start;
+  char *queryid;
+  char *archive_id;
+  gint64 deadline_us;
+  bool done;
+} message_archive_lookup_t;
+
 static void _message_send_text(xmppc_t *xmppc, char* to, char* message);
+static void _message_lookup_archive_id(xmppc_t *xmppc, const char *to, const char *message_id);
+static void _message_send_archive_query(message_archive_lookup_t *lookup);
+static int _message_archive_result_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
+static int _message_archive_query_finished(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
+static int _message_archive_query_poll(xmpp_conn_t *const conn, void *const userdata);
+static int _message_archive_lookup_timeout(xmpp_conn_t *const conn, void *const userdata);
+static bool _message_archive_result_matches(message_archive_lookup_t *lookup, xmpp_stanza_t *result);
+static bool _message_has_origin_id(xmpp_stanza_t *message, const char *id);
+static void _message_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text);
+static void _message_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type);
+static char *_message_lookup_start_time(void);
+static void _message_finish_sent(xmpp_conn_t *conn, message_archive_lookup_t *lookup);
+static void _message_print_sent(message_archive_lookup_t *lookup);
+static void _message_print_escaped_attr(const char *text);
 
 void message_execute_command(xmppc_t *xmppc, int argc, char *argv[]) {
   if( argc == 3 && ( strcmp("chat", argv[0] ) == 0)) {
     _message_send_text(xmppc, argv[1],argv[2]);
   } else {
     logError(xmppc, "Befehl unbekannt");
+    xmpp_disconnect(xmppc->conn);
   }
-  sleep(10);
-  xmpp_disconnect(xmppc->conn);
 }
 
 void _message_send_text(xmppc_t *xmppc, char* to, char* text) {
   xmpp_conn_t *conn = xmppc->conn;
+  xmpp_ctx_t *ctx = xmpp_conn_get_context(conn);
   xmpp_stanza_t *message;
   char* id = xmpp_uuid_gen(xmppc->ctx);
-  message = xmpp_message_new(xmpp_conn_get_context(conn), "chat", to, id);
+  message = xmpp_message_new(ctx, "chat", to, id);
   int res = xmpp_message_set_body(message, text);
   if(res == 0) {
+    xmpp_stanza_t *origin = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(origin, "origin-id");
+    xmpp_stanza_set_ns(origin, SID_NS);
+    xmpp_stanza_set_attribute(origin, "id", id);
+    xmpp_stanza_add_child(message, origin);
+    xmpp_stanza_release(origin);
+
     xmpp_send(conn, message);
-    printf("%s\n", id);
-    fflush(stdout);
+    _message_lookup_archive_id(xmppc, to, id);
+  } else {
+    xmpp_disconnect(conn);
   }
   xmpp_stanza_release(message);
   free(id);
 }
 
+static void _message_lookup_archive_id(xmppc_t *xmppc, const char *to, const char *message_id) {
+  message_archive_lookup_t *lookup = g_new0(message_archive_lookup_t, 1);
+  lookup->xmppc = xmppc;
+  lookup->to = g_strdup(to);
+  lookup->message_id = g_strdup(message_id);
+  lookup->start = _message_lookup_start_time();
+  lookup->deadline_us = g_get_monotonic_time() + ((gint64)MESSAGE_ARCHIVE_LOOKUP_TIMEOUT_SECONDS * G_USEC_PER_SEC);
+
+  xmpp_handler_add(xmppc->conn, _message_archive_result_handler, NULL, "message", NULL, lookup);
+  xmpp_timed_handler_add(xmppc->conn, _message_archive_lookup_timeout, MESSAGE_ARCHIVE_LOOKUP_TIMEOUT_SECONDS * 1000, lookup);
+  _message_send_archive_query(lookup);
+}
+
+static void _message_send_archive_query(message_archive_lookup_t *lookup) {
+  xmppc_t *xmppc = lookup->xmppc;
+  xmpp_ctx_t *ctx = xmppc->ctx;
+  char* id = xmpp_uuid_gen(ctx);
+  g_free(lookup->queryid);
+  lookup->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);
+
+  _message_add_field(ctx, x, "FORM_TYPE", MAM_NS, "hidden");
+  _message_add_field(ctx, x, "with", lookup->to, NULL);
+  _message_add_field(ctx, x, "start", lookup->start, NULL);
+
+  xmpp_stanza_t *set = xmpp_stanza_new(ctx);
+  xmpp_stanza_set_name(set, "set");
+  xmpp_stanza_set_ns(set, RSM_NS);
+  _message_add_text_child(ctx, set, "max", "50");
+  _message_add_text_child(ctx, set, "before", "");
+  xmpp_stanza_add_child(query, set);
+
+  xmpp_stanza_add_child(iq, query);
+  xmpp_id_handler_add(xmppc->conn, _message_archive_query_finished, id, lookup);
+  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 _message_archive_result_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata) {
+  message_archive_lookup_t *lookup = (message_archive_lookup_t *)userdata;
+  xmpp_stanza_t* result = xmpp_stanza_get_child_by_name(stanza,"result");
+
+  if (!lookup->done && result && _message_archive_result_matches(lookup, result)) {
+    const char *archive_id = xmpp_stanza_get_attribute(result, "id");
+    if (archive_id) {
+      g_free(lookup->archive_id);
+      lookup->archive_id = g_strdup(archive_id);
+    }
+  }
+
+  return 1;
+}
+
+static int _message_archive_query_finished(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata) {
+  message_archive_lookup_t *lookup = (message_archive_lookup_t *)userdata;
+  const char *type = xmpp_stanza_get_type(stanza);
+
+  if (lookup->done) {
+    return 0;
+  }
+
+  if (lookup->archive_id) {
+    _message_finish_sent(conn, lookup);
+    return 0;
+  }
+
+  if (type && strcmp(type, "error") == 0) {
+    logError(lookup->xmppc, "MAM archive lookup failed\n");
+    _message_finish_sent(conn, lookup);
+    return 0;
+  }
+
+  if (g_get_monotonic_time() < lookup->deadline_us) {
+    xmpp_timed_handler_add(conn, _message_archive_query_poll, MESSAGE_ARCHIVE_LOOKUP_POLL_MS, lookup);
+  } else {
+    logError(lookup->xmppc, "MAM archive lookup timed out\n");
+    _message_finish_sent(conn, lookup);
+  }
+
+  return 0;
+}
+
+static int _message_archive_query_poll(xmpp_conn_t *const conn, void *const userdata) {
+  message_archive_lookup_t *lookup = (message_archive_lookup_t *)userdata;
+
+  if (lookup->done) {
+    return 0;
+  }
+
+  if (g_get_monotonic_time() >= lookup->deadline_us) {
+    logError(lookup->xmppc, "MAM archive lookup timed out\n");
+    _message_finish_sent(conn, lookup);
+    return 0;
+  }
+
+  _message_send_archive_query(lookup);
+  return 0;
+}
+
+static int _message_archive_lookup_timeout(xmpp_conn_t *const conn, void *const userdata) {
+  message_archive_lookup_t *lookup = (message_archive_lookup_t *)userdata;
+
+  if (!lookup->done) {
+    logError(lookup->xmppc, "MAM archive lookup timed out\n");
+    _message_finish_sent(conn, lookup);
+  }
+
+  return 0;
+}
+
+static bool _message_archive_result_matches(message_archive_lookup_t *lookup, 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");
+  if (queryid && lookup->queryid && strcmp(queryid, lookup->queryid) != 0) {
+    return false;
+  }
+
+  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 (!message) {
+    return false;
+  }
+
+  const char *message_id = xmpp_stanza_get_id(message);
+  if (message_id && strcmp(message_id, lookup->message_id) == 0) {
+    return true;
+  }
+
+  return _message_has_origin_id(message, lookup->message_id);
+}
+
+static bool _message_has_origin_id(xmpp_stanza_t *message, const char *id) {
+  for (xmpp_stanza_t *child = xmpp_stanza_get_children(message); child; child = xmpp_stanza_get_next(child)) {
+    const char *name = xmpp_stanza_get_name(child);
+    const char *ns = xmpp_stanza_get_ns(child);
+    const char *origin_id = xmpp_stanza_get_attribute(child, "id");
+
+    if (name && ns && origin_id && strcmp(name, "origin-id") == 0 && strcmp(ns, SID_NS) == 0 && strcmp(origin_id, id) == 0) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+static void _message_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 void _message_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);
+  }
+
+  _message_add_text_child(ctx, field, "value", value);
+  xmpp_stanza_add_child(x, field);
+  xmpp_stanza_release(field);
+}
+
+static char *_message_lookup_start_time(void) {
+  GDateTime *now = g_date_time_new_now_utc();
+  GDateTime *then = g_date_time_add_seconds(now, -30);
+  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;
+}
+
+static void _message_finish_sent(xmpp_conn_t *conn, message_archive_lookup_t *lookup) {
+  if (lookup->done) {
+    return;
+  }
+
+  lookup->done = true;
+  _message_print_sent(lookup);
+  xmpp_disconnect(conn);
+}
+
+static void _message_print_sent(message_archive_lookup_t *lookup) {
+  printf("<sent archive-id=\"");
+  _message_print_escaped_attr(lookup->archive_id ? lookup->archive_id : "");
+  printf("\" message-id=\"");
+  _message_print_escaped_attr(lookup->message_id);
+  printf("\" to=\"");
+  _message_print_escaped_attr(lookup->to);
+  printf("\"/>\n");
+  fflush(stdout);
+}
+
+static void _message_print_escaped_attr(const char *text) {
+  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 '"':
+        printf("&quot;");
+        break;
+      case '\n':
+        printf("&#10;");
+        break;
+      case '\r':
+        printf("&#13;");
+        break;
+      case '\t':
+        printf("&#9;");
+        break;
+      default:
+        if (*p < 0x20) {
+          printf("&#x%02X;", *p);
+        } else {
+          putchar(*p);
+        }
+        break;
+    }
+    p++;
+  }
+}