diff --git a/src/main.c b/src/main.c index 979318b0f70a3afd1d6df1b43ec4facf21f5c016..8a9afae1b0cbd8a2ef59172c3cadc659baec5306 100644 --- a/src/main.c +++ b/src/main.c @@ -573,6 +573,12 @@ static int _agent_skill_command(int argc, char *argv[]) { " xmppc -a -m mam list [MAM_FIELD=VALUE ...]\n" " xmppc -a -m mam receive after-id= [timeout=300s] [max=5]\n" "\n" + "Sending prints one compact XML record after the sent stanza appears in MAM:\n" + " \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" diff --git a/src/mode/message.c b/src/mode/message.c index 52d2388034bd497fa0f5085f1524c6dfa6ebdcee..9faa6da671e67c58645940ad1f459d0491c3e71b 100644 --- a/src/mode/message.c +++ b/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("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("&"); + break; + case '<': + printf("<"); + break; + case '>': + printf(">"); + break; + case '"': + printf("""); + break; + case '\n': + printf(" "); + break; + case '\r': + printf(" "); + break; + case '\t': + printf(" "); + break; + default: + if (*p < 0x20) { + printf("&#x%02X;", *p); + } else { + putchar(*p); + } + break; + } + p++; + } +}