diff --git a/.agents/setup b/.agents/setup new file mode 100755 index 0000000000000000000000000000000000000000..9a213acdeb20cedbf27c1791cccfd25035bc4170 --- /dev/null +++ b/.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 diff --git a/src/main.c b/src/main.c index 780e81c028d50cfe4b15e4ba13551b92c8a8ac0b..979318b0f70a3afd1d6df1b43ec4facf21f5c016 100644 --- a/src/main.c +++ b/src/main.c @@ -134,6 +134,7 @@ #include #include #include +#include #include #include #include @@ -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 -m message chat \n" + " xmppc -a -m mam list [MAM_FIELD=VALUE ...]\n" + " xmppc -a -m mam receive after-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" + " escaped body\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 -m mam receive after-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 Jabber ID\n"); printf(" -p / --pwd Passwort\n"); + printf(" Use '-' to read password from stdin\n"); printf(" -a / --account Account\n"); printf(" -m / --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 List messages from \n"); + printf(" list [field=value] List archived messages from \n"); + printf(" receive after-id= [timeout=300s] [max=5]\n"); printf("\n"); printf(" -m --mode discovery Service Discovery (XEP-0030)\n"); printf(" info info request for \n"); diff --git a/src/mode/mam.c b/src/mode/mam.c index ea5db27c240adf3fec5a3667798a521684673241..ad93c3e09273447e6884d4df6cbe8f5b62e3738c 100644 --- a/src/mode/mam.c +++ b/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=\n"); + _mam_options_free(options); + return false; + } + if (!options->after_id) { + logError(xmppc, "mam receive requires after-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(""); + _mam_print_escaped_text(body_text ? body_text : ""); + printf("\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 [with=] [start=