@@ -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, ¤t_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");
@@ -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("&");
+ break;
+ case '<':
+ printf("<");
+ break;
+ case '>':
+ printf(">");
+ break;
+ case '"':
+ if (attribute) {
+ printf(""");
+ } else {
+ putchar('"');
+ }
+ break;
+ case '\n':
+ printf(" ");
+ break;
+ case '\r':
+ printf(" ");
+ break;
+ case '\t':
+ if (attribute) {
+ printf("	");
+ } 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");
+}