1/*!
2 * @file File: mam.c
3 *
4 * vim: expandtab:ts=2:sts=2:sw=2
5 *
6 * @copyright
7 *
8 * Copyright (C) 2020 Anoxinon e.V.
9 *
10 * This file is part of xmppc.
11 *
12 * xmppc is free software: you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published by
14 * the Free Software Foundation, either version 3 of the License, or
15 * (at your option) any later version.
16 *
17 * xmppc is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with Foobar. If not, see <http://www.gnu.org/licenses/>.
24 *
25 * German
26 *
27 * Diese Datei ist Teil von xmppc.
28 *
29 * xmppc ist Freie Software: Sie können es unter den Bedingungen
30 * der GNU General Public License, wie von der Free Software Foundation,
31 * Version 3 der Lizenz oder (nach Ihrer Wahl) jeder neueren
32 * veröffentlichten Version, weiter verteilen und/oder modifizieren.
33 *
34 * xmppc wird in der Hoffnung, dass es nützlich sein wird, aber
35 * OHNE JEDE GEWÄHRLEISTUNG, bereitgestellt; sogar ohne die implizite
36 * Gewährleistung der MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK.
37 * Siehe die GNU General Public License für weitere Details.
38 *
39 * Sie sollten eine Kopie der GNU General Public License zusammen mit diesem
40 * Programm erhalten haben. Wenn nicht, siehe <https://www.gnu.org/licenses/>.
41 */
42
43#include "mam.h"
44#include "glib.h"
45#include "stdbool.h"
46#include "stdio.h"
47#include "stdlib.h"
48#include "string.h"
49
50#define MAM_NS "urn:xmpp:mam:2"
51#define FORWARD_NS "urn:xmpp:forward:0"
52#define DELAY_NS "urn:xmpp:delay"
53#define RSM_NS "http://jabber.org/protocol/rsm"
54#define SID_NS "urn:xmpp:sid:0"
55
56#define MAM_DEFAULT_MAX 5
57#define MAM_MAX_CAP 50
58#define MAM_DEFAULT_TIMEOUT_SECONDS 300
59#define MAM_POLL_PERIOD_MS 2000
60#define MAM_ARCHIVE_LOOKUP_TIMEOUT_SECONDS 15
61#define MAM_ARCHIVE_LOOKUP_POLL_MS 1000
62
63typedef struct {
64 char *with;
65 char *start;
66 char *end;
67 char *after_id;
68 char *before_id;
69 char *ids;
70 int max;
71 int timeout;
72 bool has_narrowing;
73} mam_options_t;
74
75typedef struct {
76 xmppc_t *xmppc;
77 mam_options_t options;
78 bool receive;
79 int messages_total;
80 gint64 deadline_us;
81 char *queryid;
82 bool done;
83} mam_run_t;
84
85typedef struct {
86 xmppc_t *xmppc;
87 char *with;
88 char *message_id;
89 char *start;
90 char *queryid;
91 char *archive_id;
92 gint64 deadline_us;
93 bool done;
94 MamArchiveLookupCallback callback;
95 void *userdata;
96} mam_archive_lookup_t;
97
98static mam_archive_lookup_t *mam_archive_lookup_active = NULL;
99
100static void _mam_options_init(mam_options_t *options);
101static void _mam_options_free(mam_options_t *options);
102static bool _mam_parse_terms(xmppc_t *xmppc, int argc, char *argv[], mam_options_t *options, bool receive);
103static void _mam_start_request(xmppc_t *xmppc, mam_options_t *options, bool pretty, bool receive);
104static void _mam_send_request(mam_run_t *run);
105static void _mam_send_query(xmppc_t *xmppc, mam_options_t *options, char **queryid, xmpp_handler finish_handler, void *userdata);
106static int _mam_finish(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
107static int _mam_receive_poll(xmpp_conn_t *const conn, void *const userdata);
108static int _mam_receive_timeout(xmpp_conn_t *const conn, void *const userdata);
109static int _mam_display_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
110static int _mam_display_pretty_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
111static void _mam_disconnect_and_free(xmpp_conn_t *conn, mam_run_t *run, bool delete_id_handler);
112static void _mam_send_archive_lookup_query(mam_archive_lookup_t *lookup);
113static int _mam_archive_lookup_result_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
114static int _mam_archive_lookup_finished(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
115static int _mam_archive_lookup_poll(xmpp_conn_t *const conn, void *const userdata);
116static int _mam_archive_lookup_timeout(xmpp_conn_t *const conn, void *const userdata);
117static bool _mam_archive_lookup_result_matches(mam_archive_lookup_t *lookup, xmpp_stanza_t *result);
118static bool _mam_has_origin_id(xmpp_stanza_t *message, const char *id);
119static void _mam_archive_lookup_done(xmpp_conn_t *conn, mam_archive_lookup_t *lookup, bool delete_id_handler);
120static void _mam_archive_lookup_free(mam_archive_lookup_t *lookup);
121static void _mam_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type);
122static void _mam_add_ids_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *ids);
123static void _mam_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text);
124static bool _mam_set_string_option(xmppc_t *xmppc, char **target, const char *key, const char *value, bool allow_empty);
125static bool _mam_parse_max(xmppc_t *xmppc, mam_options_t *options, const char *value);
126static bool _mam_parse_timeout(xmppc_t *xmppc, mam_options_t *options, const char *value);
127static bool _mam_parse_duration_seconds(const char *value, gint64 *seconds);
128static char *_mam_normalize_datetime(const char *value);
129static bool _mam_result_matches(mam_run_t *run, xmpp_stanza_t *result);
130static void _mam_print_compact_message(xmpp_stanza_t *result);
131static void _mam_print_escaped_attr(const char *text);
132static void _mam_print_escaped_text(const char *text);
133static void _mam_print_escaped(const char *text, bool attribute);
134static void _mam_usage(xmppc_t *xmppc);
135
136void mam_execute_command (xmppc_t *xmppc, int argc, char *argv[]) {
137 mam_options_t options;
138
139 if(argc >= 1) {
140 if (strcmp("list", argv[0]) == 0) {
141 if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, false)) {
142 _mam_start_request(xmppc, &options, false, false);
143 } else {
144 xmpp_disconnect(xmppc->conn);
145 }
146 return;
147 } else if (strcmp("pretty", argv[0]) == 0) {
148 if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, false)) {
149 _mam_start_request(xmppc, &options, true, false);
150 } else {
151 xmpp_disconnect(xmppc->conn);
152 }
153 return;
154 } else if (strcmp("receive", argv[0]) == 0) {
155 if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, true)) {
156 _mam_start_request(xmppc, &options, false, true);
157 } else {
158 xmpp_disconnect(xmppc->conn);
159 }
160 return;
161 }
162 }
163
164 _mam_usage(xmppc);
165 xmpp_disconnect(xmppc->conn);
166}
167
168static void _mam_options_init(mam_options_t *options) {
169 memset(options, 0, sizeof(*options));
170 options->max = MAM_DEFAULT_MAX;
171 options->timeout = MAM_DEFAULT_TIMEOUT_SECONDS;
172}
173
174static void _mam_options_free(mam_options_t *options) {
175 g_free(options->with);
176 g_free(options->start);
177 g_free(options->end);
178 g_free(options->after_id);
179 g_free(options->before_id);
180 g_free(options->ids);
181}
182
183static bool _mam_parse_terms(xmppc_t *xmppc, int argc, char *argv[], mam_options_t *options, bool receive) {
184 _mam_options_init(options);
185
186 for (int i = 0; i < argc; i++) {
187 char *term = argv[i];
188 char *equals = strchr(term, '=');
189
190 if (!equals) {
191 if (!_mam_set_string_option(xmppc, &options->with, "with", term, false)) {
192 _mam_options_free(options);
193 return false;
194 }
195 options->has_narrowing = true;
196 continue;
197 }
198
199 char *key = g_strndup(term, equals - term);
200 const char *value = equals + 1;
201
202 if (strcmp(key, "with") == 0) {
203 if (!_mam_set_string_option(xmppc, &options->with, key, value, false)) {
204 g_free(key);
205 _mam_options_free(options);
206 return false;
207 }
208 options->has_narrowing = true;
209 } else if (strcmp(key, "start") == 0) {
210 char *normalized = _mam_normalize_datetime(value);
211 if (!_mam_set_string_option(xmppc, &options->start, key, normalized, false)) {
212 g_free(normalized);
213 g_free(key);
214 _mam_options_free(options);
215 return false;
216 }
217 g_free(normalized);
218 options->has_narrowing = true;
219 } else if (strcmp(key, "end") == 0) {
220 char *normalized = _mam_normalize_datetime(value);
221 if (!_mam_set_string_option(xmppc, &options->end, key, normalized, false)) {
222 g_free(normalized);
223 g_free(key);
224 _mam_options_free(options);
225 return false;
226 }
227 g_free(normalized);
228 options->has_narrowing = true;
229 } else if (strcmp(key, "after-id") == 0) {
230 if (!_mam_set_string_option(xmppc, &options->after_id, key, value, false)) {
231 g_free(key);
232 _mam_options_free(options);
233 return false;
234 }
235 options->has_narrowing = true;
236 } else if (strcmp(key, "before-id") == 0) {
237 if (!_mam_set_string_option(xmppc, &options->before_id, key, value, true)) {
238 g_free(key);
239 _mam_options_free(options);
240 return false;
241 }
242 options->has_narrowing = true;
243 } else if (strcmp(key, "ids") == 0) {
244 if (!_mam_set_string_option(xmppc, &options->ids, key, value, false)) {
245 g_free(key);
246 _mam_options_free(options);
247 return false;
248 }
249 options->has_narrowing = true;
250 } else if (strcmp(key, "max") == 0) {
251 if (!_mam_parse_max(xmppc, options, value)) {
252 g_free(key);
253 _mam_options_free(options);
254 return false;
255 }
256 } else if (receive && strcmp(key, "timeout") == 0) {
257 if (!_mam_parse_timeout(xmppc, options, value)) {
258 g_free(key);
259 _mam_options_free(options);
260 return false;
261 }
262 } else {
263 logError(xmppc, "Unknown MAM field: %s\n", key);
264 g_free(key);
265 _mam_options_free(options);
266 return false;
267 }
268
269 g_free(key);
270 }
271
272 if (receive) {
273 if (!options->with) {
274 logError(xmppc, "mam receive requires a JID or with=<jid>\n");
275 _mam_options_free(options);
276 return false;
277 }
278 if (!options->after_id) {
279 logError(xmppc, "mam receive requires after-id=<mam-id>\n");
280 _mam_options_free(options);
281 return false;
282 }
283 } else if (!options->has_narrowing) {
284 logError(xmppc, "mam list requires a JID, with=, start=, end=, after-id=, before-id=, or ids=\n");
285 _mam_options_free(options);
286 return false;
287 }
288
289 if (!options->start && !options->end && !options->after_id && !options->before_id && !options->ids) {
290 char *start = _mam_normalize_datetime("-5m");
291 options->start = start;
292 }
293
294 return true;
295}
296
297static void _mam_start_request(xmppc_t *xmppc, mam_options_t *options, bool pretty, bool receive) {
298 mam_run_t *run = g_new0(mam_run_t, 1);
299 run->xmppc = xmppc;
300 run->options = *options;
301 run->receive = receive;
302 if (receive) {
303 unsigned long timeout_ms = (unsigned long)run->options.timeout * 1000;
304 run->deadline_us = g_get_monotonic_time() + ((gint64)run->options.timeout * G_USEC_PER_SEC);
305 xmpp_timed_handler_add(xmppc->conn, _mam_receive_timeout, timeout_ms, run);
306 }
307
308 if (pretty) {
309 xmpp_handler_add (xmppc->conn, _mam_display_pretty_message, NULL,"message",NULL, run);
310 } else {
311 xmpp_handler_add (xmppc->conn, _mam_display_message, NULL,"message",NULL, run);
312 }
313
314 _mam_send_request(run);
315}
316
317static void _mam_send_request(mam_run_t *run) {
318 _mam_send_query(run->xmppc, &run->options, &run->queryid, _mam_finish, run);
319}
320
321static void _mam_send_query(xmppc_t *xmppc, mam_options_t *options, char **queryid, xmpp_handler finish_handler, void *userdata) {
322 xmpp_ctx_t *ctx = xmppc->ctx;
323 char max[16];
324
325 g_snprintf(max, sizeof(max), "%d", options->max);
326
327 char* id = xmpp_uuid_gen(ctx);
328 g_free(*queryid);
329 *queryid = g_strdup(id);
330
331 xmpp_stanza_t *iq = xmpp_iq_new(ctx, "set", id);
332 xmpp_stanza_t *query = xmpp_stanza_new(ctx);
333 xmpp_stanza_set_name(query, "query");
334 xmpp_stanza_set_ns(query, MAM_NS);
335 xmpp_stanza_set_attribute(query, "queryid", id);
336
337 xmpp_stanza_t* x = xmpp_stanza_new(ctx);
338 xmpp_stanza_set_name(x,"x");
339 xmpp_stanza_set_ns(x, "jabber:x:data");
340 xmpp_stanza_set_type(x,"submit");
341 xmpp_stanza_add_child(query,x);
342
343 _mam_add_field(ctx, x, "FORM_TYPE", MAM_NS, "hidden");
344
345 if (options->with) {
346 _mam_add_field(ctx, x, "with", options->with, NULL);
347 }
348 if (options->start) {
349 _mam_add_field(ctx, x, "start", options->start, NULL);
350 }
351 if (options->end) {
352 _mam_add_field(ctx, x, "end", options->end, NULL);
353 }
354 if (options->ids) {
355 _mam_add_ids_field(ctx, x, options->ids);
356 }
357
358 xmpp_stanza_t *set = xmpp_stanza_new(ctx);
359 xmpp_stanza_set_name(set, "set");
360 xmpp_stanza_set_ns(set, RSM_NS);
361 _mam_add_text_child(ctx, set, "max", max);
362 if (options->after_id) {
363 _mam_add_text_child(ctx, set, "after", options->after_id);
364 }
365 if (options->before_id) {
366 _mam_add_text_child(ctx, set, "before", options->before_id);
367 }
368 xmpp_stanza_add_child(query, set);
369
370 xmpp_stanza_add_child(iq,query);
371 xmpp_id_handler_add(xmppc->conn, finish_handler, id, userdata);
372 xmpp_send(xmppc->conn,iq);
373
374 xmpp_stanza_release(iq);
375 xmpp_stanza_release(query);
376 xmpp_stanza_release(x);
377 xmpp_stanza_release(set);
378 free(id);
379}
380
381static int _mam_finish(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata){
382 mam_run_t *run = (mam_run_t *)userdata;
383 const char *type = xmpp_stanza_get_type(stanza);
384
385 if (run->done) {
386 return 0;
387 }
388
389 if (type && strcmp(type, "error") == 0) {
390 logError(run->xmppc, "MAM request failed\n");
391 _mam_disconnect_and_free(conn, run, false);
392 return 0;
393 }
394
395 xmpp_stanza_t *fin = xmpp_stanza_get_child_by_name(stanza,"fin");
396 if(fin) {
397 if (run->receive && run->messages_total == 0 && g_get_monotonic_time() < run->deadline_us) {
398 xmpp_timed_handler_add(conn, _mam_receive_poll, MAM_POLL_PERIOD_MS, run);
399 } else {
400 _mam_disconnect_and_free(conn, run, false);
401 }
402 }
403 return 0;
404}
405
406static int _mam_receive_poll(xmpp_conn_t *const conn, void *const userdata) {
407 mam_run_t *run = (mam_run_t *)userdata;
408
409 if (run->done) {
410 return 0;
411 }
412
413 if (g_get_monotonic_time() >= run->deadline_us) {
414 _mam_disconnect_and_free(conn, run, false);
415 return 0;
416 }
417
418 _mam_send_request(run);
419 return 0;
420}
421
422static int _mam_receive_timeout(xmpp_conn_t *const conn, void *const userdata) {
423 mam_run_t *run = (mam_run_t *)userdata;
424
425 if (!run->done) {
426 _mam_disconnect_and_free(conn, run, true);
427 }
428
429 return 0;
430}
431
432static void _mam_disconnect_and_free(xmpp_conn_t *conn, mam_run_t *run, bool delete_id_handler) {
433 if (run->done) {
434 return;
435 }
436
437 run->done = true;
438 xmpp_handler_delete(conn, _mam_display_message);
439 xmpp_handler_delete(conn, _mam_display_pretty_message);
440 xmpp_timed_handler_delete(conn, _mam_receive_poll);
441 xmpp_timed_handler_delete(conn, _mam_receive_timeout);
442 if (delete_id_handler && run->queryid) {
443 xmpp_id_handler_delete(conn, _mam_finish, run->queryid);
444 }
445 _mam_options_free(&run->options);
446 g_free(run->queryid);
447 g_free(run);
448 xmpp_disconnect(conn);
449}
450
451void mam_lookup_message_archive_id(xmppc_t *xmppc, const char *with, const char *message_id, MamArchiveLookupCallback callback, void *userdata) {
452 if (!with || !message_id) {
453 if (callback) {
454 callback(xmppc->conn, NULL, userdata);
455 }
456 return;
457 }
458
459 if (mam_archive_lookup_active) {
460 logError(xmppc, "MAM archive lookup already active\n");
461 if (callback) {
462 callback(xmppc->conn, NULL, userdata);
463 }
464 return;
465 }
466
467 mam_archive_lookup_t *lookup = g_new0(mam_archive_lookup_t, 1);
468 lookup->xmppc = xmppc;
469 lookup->with = g_strdup(with);
470 lookup->message_id = g_strdup(message_id);
471 lookup->start = _mam_normalize_datetime("-30s");
472 lookup->deadline_us = g_get_monotonic_time() + ((gint64)MAM_ARCHIVE_LOOKUP_TIMEOUT_SECONDS * G_USEC_PER_SEC);
473 lookup->callback = callback;
474 lookup->userdata = userdata;
475 mam_archive_lookup_active = lookup;
476
477 xmpp_handler_add(xmppc->conn, _mam_archive_lookup_result_handler, NULL, "message", NULL, lookup);
478 xmpp_timed_handler_add(xmppc->conn, _mam_archive_lookup_timeout, MAM_ARCHIVE_LOOKUP_TIMEOUT_SECONDS * 1000, lookup);
479 _mam_send_archive_lookup_query(lookup);
480}
481
482static void _mam_send_archive_lookup_query(mam_archive_lookup_t *lookup) {
483 mam_options_t options;
484 _mam_options_init(&options);
485 options.with = lookup->with;
486 options.start = lookup->start;
487 options.before_id = "";
488 options.max = MAM_MAX_CAP;
489
490 _mam_send_query(lookup->xmppc, &options, &lookup->queryid, _mam_archive_lookup_finished, lookup);
491}
492
493static int _mam_archive_lookup_result_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata) {
494 mam_archive_lookup_t *lookup = (mam_archive_lookup_t *)userdata;
495 xmpp_stanza_t* result = xmpp_stanza_get_child_by_name(stanza,"result");
496
497 if (!lookup->done && result && _mam_archive_lookup_result_matches(lookup, result)) {
498 const char *archive_id = xmpp_stanza_get_attribute(result, "id");
499 if (archive_id) {
500 g_free(lookup->archive_id);
501 lookup->archive_id = g_strdup(archive_id);
502 }
503 }
504
505 return 1;
506}
507
508static int _mam_archive_lookup_finished(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata) {
509 mam_archive_lookup_t *lookup = (mam_archive_lookup_t *)userdata;
510 const char *type = xmpp_stanza_get_type(stanza);
511
512 if (lookup->done) {
513 return 0;
514 }
515
516 if (lookup->archive_id) {
517 _mam_archive_lookup_done(conn, lookup, false);
518 return 0;
519 }
520
521 if (type && strcmp(type, "error") == 0) {
522 logError(lookup->xmppc, "MAM archive lookup failed\n");
523 _mam_archive_lookup_done(conn, lookup, false);
524 return 0;
525 }
526
527 if (g_get_monotonic_time() < lookup->deadline_us) {
528 xmpp_timed_handler_add(conn, _mam_archive_lookup_poll, MAM_ARCHIVE_LOOKUP_POLL_MS, lookup);
529 } else {
530 logError(lookup->xmppc, "MAM archive lookup timed out\n");
531 _mam_archive_lookup_done(conn, lookup, false);
532 }
533
534 return 0;
535}
536
537static int _mam_archive_lookup_poll(xmpp_conn_t *const conn, void *const userdata) {
538 mam_archive_lookup_t *lookup = (mam_archive_lookup_t *)userdata;
539
540 if (lookup->done) {
541 return 0;
542 }
543
544 if (g_get_monotonic_time() >= lookup->deadline_us) {
545 logError(lookup->xmppc, "MAM archive lookup timed out\n");
546 _mam_archive_lookup_done(conn, lookup, false);
547 return 0;
548 }
549
550 _mam_send_archive_lookup_query(lookup);
551 return 0;
552}
553
554static int _mam_archive_lookup_timeout(xmpp_conn_t *const conn, void *const userdata) {
555 mam_archive_lookup_t *lookup = (mam_archive_lookup_t *)userdata;
556
557 if (!lookup->done) {
558 logError(lookup->xmppc, "MAM archive lookup timed out\n");
559 _mam_archive_lookup_done(conn, lookup, true);
560 }
561
562 return 0;
563}
564
565static bool _mam_archive_lookup_result_matches(mam_archive_lookup_t *lookup, xmpp_stanza_t *result) {
566 const char *ns = xmpp_stanza_get_ns(result);
567 if (!ns || strcmp(MAM_NS, ns) != 0) {
568 return false;
569 }
570
571 const char *queryid = xmpp_stanza_get_attribute(result, "queryid");
572 if (queryid && lookup->queryid && strcmp(queryid, lookup->queryid) != 0) {
573 return false;
574 }
575
576 xmpp_stanza_t* forwarded = xmpp_stanza_get_child_by_ns(result, FORWARD_NS);
577 xmpp_stanza_t* message = forwarded ? xmpp_stanza_get_child_by_name(forwarded, "message") : NULL;
578 if (!message) {
579 return false;
580 }
581
582 const char *archived_message_id = xmpp_stanza_get_id(message);
583 if (archived_message_id && strcmp(archived_message_id, lookup->message_id) == 0) {
584 return true;
585 }
586
587 return _mam_has_origin_id(message, lookup->message_id);
588}
589
590static bool _mam_has_origin_id(xmpp_stanza_t *message, const char *id) {
591 for (xmpp_stanza_t *child = xmpp_stanza_get_children(message); child; child = xmpp_stanza_get_next(child)) {
592 const char *name = xmpp_stanza_get_name(child);
593 const char *ns = xmpp_stanza_get_ns(child);
594 const char *origin_id = xmpp_stanza_get_attribute(child, "id");
595
596 if (name && ns && origin_id && strcmp(name, "origin-id") == 0 && strcmp(ns, SID_NS) == 0 && strcmp(origin_id, id) == 0) {
597 return true;
598 }
599 }
600
601 return false;
602}
603
604static void _mam_archive_lookup_done(xmpp_conn_t *conn, mam_archive_lookup_t *lookup, bool delete_id_handler) {
605 if (lookup->done) {
606 return;
607 }
608
609 lookup->done = true;
610 xmpp_handler_delete(conn, _mam_archive_lookup_result_handler);
611 xmpp_timed_handler_delete(conn, _mam_archive_lookup_poll);
612 xmpp_timed_handler_delete(conn, _mam_archive_lookup_timeout);
613 if (delete_id_handler && lookup->queryid) {
614 xmpp_id_handler_delete(conn, _mam_archive_lookup_finished, lookup->queryid);
615 }
616 if (mam_archive_lookup_active == lookup) {
617 mam_archive_lookup_active = NULL;
618 }
619 if (lookup->callback) {
620 lookup->callback(conn, lookup->archive_id, lookup->userdata);
621 }
622 _mam_archive_lookup_free(lookup);
623}
624
625static void _mam_archive_lookup_free(mam_archive_lookup_t *lookup) {
626 g_free(lookup->with);
627 g_free(lookup->message_id);
628 g_free(lookup->start);
629 g_free(lookup->queryid);
630 g_free(lookup->archive_id);
631 g_free(lookup);
632}
633
634static void _mam_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type) {
635 xmpp_stanza_t* field = xmpp_stanza_new(ctx);
636 xmpp_stanza_set_name(field,"field");
637 xmpp_stanza_set_attribute(field, "var", var);
638 if (type) {
639 xmpp_stanza_set_type(field, type);
640 }
641
642 _mam_add_text_child(ctx, field, "value", value);
643 xmpp_stanza_add_child(x, field);
644 xmpp_stanza_release(field);
645}
646
647static void _mam_add_ids_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *ids) {
648 xmpp_stanza_t* field = xmpp_stanza_new(ctx);
649 xmpp_stanza_set_name(field,"field");
650 xmpp_stanza_set_attribute(field, "var", "ids");
651
652 char **parts = g_strsplit(ids, ",", -1);
653 for (int i = 0; parts[i] != NULL; i++) {
654 if (parts[i][0] != '\0') {
655 _mam_add_text_child(ctx, field, "value", parts[i]);
656 }
657 }
658 g_strfreev(parts);
659
660 xmpp_stanza_add_child(x, field);
661 xmpp_stanza_release(field);
662}
663
664static void _mam_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text) {
665 xmpp_stanza_t *child = xmpp_stanza_new(ctx);
666 xmpp_stanza_set_name(child, name);
667
668 xmpp_stanza_t *body = xmpp_stanza_new(ctx);
669 xmpp_stanza_set_text(body, text ? text : "");
670 xmpp_stanza_add_child(child, body);
671 xmpp_stanza_release(body);
672
673 xmpp_stanza_add_child(parent, child);
674 xmpp_stanza_release(child);
675}
676
677static bool _mam_set_string_option(xmppc_t *xmppc, char **target, const char *key, const char *value, bool allow_empty) {
678 if (*target) {
679 logError(xmppc, "Duplicate MAM field: %s\n", key);
680 return false;
681 }
682 if (!allow_empty && (!value || value[0] == '\0')) {
683 logError(xmppc, "MAM field requires a value: %s\n", key);
684 return false;
685 }
686 *target = g_strdup(value ? value : "");
687 return true;
688}
689
690static bool _mam_parse_max(xmppc_t *xmppc, mam_options_t *options, const char *value) {
691 char *end = NULL;
692 gint64 parsed = g_ascii_strtoll(value, &end, 10);
693
694 if (!value || value[0] == '\0' || !end || *end != '\0' || parsed < 1) {
695 logError(xmppc, "max must be a positive integer\n");
696 return false;
697 }
698
699 if (parsed > MAM_MAX_CAP) {
700 parsed = MAM_MAX_CAP;
701 }
702
703 options->max = (int)parsed;
704 return true;
705}
706
707static bool _mam_parse_timeout(xmppc_t *xmppc, mam_options_t *options, const char *value) {
708 gint64 seconds = 0;
709
710 if (!_mam_parse_duration_seconds(value, &seconds) || seconds < 0 || seconds > G_MAXINT || (guint64)seconds > ((guint64)G_MAXULONG / 1000)) {
711 logError(xmppc, "timeout must be a duration like 30s, 5m, or 1h\n");
712 return false;
713 }
714
715 options->timeout = (int)seconds;
716 return true;
717}
718
719static bool _mam_parse_duration_seconds(const char *value, gint64 *seconds) {
720 char *end = NULL;
721 gint64 parsed = 0;
722 gint64 multiplier = 1;
723
724 if (!value || value[0] == '\0') {
725 return false;
726 }
727
728 parsed = g_ascii_strtoll(value, &end, 10);
729 if (end == value || parsed < 0) {
730 return false;
731 }
732
733 if (*end == '\0') {
734 multiplier = 1;
735 } else if (end[1] == '\0') {
736 if (*end == 's') {
737 multiplier = 1;
738 } else if (*end == 'm') {
739 multiplier = 60;
740 } else if (*end == 'h') {
741 multiplier = 60 * 60;
742 } else if (*end == 'd') {
743 multiplier = 60 * 60 * 24;
744 } else {
745 return false;
746 }
747 } else {
748 return false;
749 }
750
751 if (parsed > G_MAXINT64 / multiplier) {
752 return false;
753 }
754
755 *seconds = parsed * multiplier;
756 return true;
757}
758
759static char *_mam_normalize_datetime(const char *value) {
760 if (value && value[0] == '-' && value[1] != '\0') {
761 gint64 seconds = 0;
762 if (_mam_parse_duration_seconds(value + 1, &seconds) && seconds > 0) {
763 GDateTime *now = g_date_time_new_now_utc();
764 GDateTime *then = g_date_time_add_seconds(now, -seconds);
765 char *formatted = g_date_time_format(then, "%Y-%m-%dT%H:%M:%SZ");
766 g_date_time_unref(then);
767 g_date_time_unref(now);
768 return formatted;
769 }
770 }
771
772 return g_strdup(value);
773}
774
775static bool _mam_result_matches(mam_run_t *run, xmpp_stanza_t *result) {
776 const char *ns = xmpp_stanza_get_ns(result);
777 if (!ns || strcmp(MAM_NS, ns) != 0) {
778 return false;
779 }
780
781 const char *queryid = xmpp_stanza_get_attribute(result, "queryid");
782 return !queryid || !run->queryid || strcmp(queryid, run->queryid) == 0;
783}
784
785static int _mam_display_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata) {
786 mam_run_t *run = (mam_run_t *)userdata;
787 xmpp_stanza_t* result = xmpp_stanza_get_child_by_name(stanza,"result");
788
789 if (result && _mam_result_matches(run, result) && run->messages_total < run->options.max) {
790 _mam_print_compact_message(result);
791 run->messages_total++;
792 }
793 return 1;
794}
795
796static int _mam_display_pretty_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata)
797{
798 mam_run_t *run = (mam_run_t *)userdata;
799 xmpp_stanza_t* result = xmpp_stanza_get_child_by_name(stanza,"result");
800
801 if( result && _mam_result_matches(run, result) && run->messages_total < run->options.max ) {
802
803 xmpp_stanza_t* forwarded = xmpp_stanza_get_child_by_ns(result, FORWARD_NS);
804 if (forwarded) {
805
806 xmpp_stanza_t* message = xmpp_stanza_get_child_by_name(forwarded, "message");
807 if (message) {
808 const char* const from = xmpp_stanza_get_from(message);
809 xmpp_stanza_t* body = xmpp_stanza_get_child_by_name(message, "body");
810
811 if (body) {
812 char *text = xmpp_stanza_get_text(body);
813 printf("%s: %s\n", from ? from : "", text ? text : "");
814 free(text);
815 run->messages_total++;
816 }
817 }
818 }
819 }
820
821 return 1;
822}
823
824static void _mam_print_compact_message(xmpp_stanza_t *result) {
825 const char *mam_id = xmpp_stanza_get_attribute(result, "id");
826 const char *message_id = "";
827 const char *stamp = "";
828 const char *from = "";
829 const char *to = "";
830 char *body_text = NULL;
831
832 xmpp_stanza_t* forwarded = xmpp_stanza_get_child_by_ns(result, FORWARD_NS);
833 xmpp_stanza_t* message = forwarded ? xmpp_stanza_get_child_by_name(forwarded, "message") : NULL;
834
835 if (forwarded) {
836 xmpp_stanza_t* delay = xmpp_stanza_get_child_by_ns(forwarded, DELAY_NS);
837 if (delay && xmpp_stanza_get_attribute(delay, "stamp")) {
838 stamp = xmpp_stanza_get_attribute(delay, "stamp");
839 }
840 }
841
842 if (message) {
843 xmpp_stanza_t* body = xmpp_stanza_get_child_by_name(message, "body");
844 if (xmpp_stanza_get_id(message)) {
845 message_id = xmpp_stanza_get_id(message);
846 }
847 if (xmpp_stanza_get_from(message)) {
848 from = xmpp_stanza_get_from(message);
849 }
850 if (xmpp_stanza_get_to(message)) {
851 to = xmpp_stanza_get_to(message);
852 }
853 if (body) {
854 body_text = xmpp_stanza_get_text(body);
855 }
856 }
857
858 printf("<message id=\"");
859 _mam_print_escaped_attr(mam_id ? mam_id : "");
860 printf("\" message-id=\"");
861 _mam_print_escaped_attr(message_id);
862 printf("\" stamp=\"");
863 _mam_print_escaped_attr(stamp);
864 printf("\" from=\"");
865 _mam_print_escaped_attr(from);
866 printf("\" to=\"");
867 _mam_print_escaped_attr(to);
868 printf("\"><body>");
869 _mam_print_escaped_text(body_text ? body_text : "");
870 printf("</body></message>\n");
871 fflush(stdout);
872
873 free(body_text);
874}
875
876static void _mam_print_escaped_attr(const char *text) {
877 _mam_print_escaped(text, true);
878}
879
880static void _mam_print_escaped_text(const char *text) {
881 _mam_print_escaped(text, false);
882}
883
884static void _mam_print_escaped(const char *text, bool attribute) {
885 const unsigned char *p = (const unsigned char *)(text ? text : "");
886
887 while (*p) {
888 switch (*p) {
889 case '&':
890 printf("&");
891 break;
892 case '<':
893 printf("<");
894 break;
895 case '>':
896 printf(">");
897 break;
898 case '"':
899 if (attribute) {
900 printf(""");
901 } else {
902 putchar('"');
903 }
904 break;
905 case '\n':
906 printf(" ");
907 break;
908 case '\r':
909 printf(" ");
910 break;
911 case '\t':
912 if (attribute) {
913 printf("	");
914 } else {
915 putchar('\t');
916 }
917 break;
918 default:
919 if (*p < 0x20) {
920 printf("&#x%02X;", *p);
921 } else {
922 putchar(*p);
923 }
924 break;
925 }
926 p++;
927 }
928}
929
930static void _mam_usage(xmppc_t *xmppc) {
931 logError(xmppc, "Usage:\n");
932 logError(xmppc, " mam list <jid> [with=<jid>] [start=<time>] [end=<time>] [after-id=<id>] [before-id=<id>] [ids=<id>[,<id>...]] [max=<n>]\n");
933 logError(xmppc, " mam receive <jid> after-id=<id> [timeout=300s] [max=5]\n");
934}