mam.c

  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("&amp;");
891        break;
892      case '<':
893        printf("&lt;");
894        break;
895      case '>':
896        printf("&gt;");
897        break;
898      case '"':
899        if (attribute) {
900          printf("&quot;");
901        } else {
902          putchar('"');
903        }
904        break;
905      case '\n':
906        printf("&#10;");
907        break;
908      case '\r':
909        printf("&#13;");
910        break;
911      case '\t':
912        if (attribute) {
913          printf("&#9;");
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}