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
 55#define MAM_DEFAULT_MAX 5
 56#define MAM_MAX_CAP 50
 57#define MAM_DEFAULT_TIMEOUT_SECONDS 300
 58#define MAM_POLL_PERIOD_MS 2000
 59
 60typedef struct {
 61  char *with;
 62  char *start;
 63  char *end;
 64  char *after_id;
 65  char *before_id;
 66  char *ids;
 67  int max;
 68  int timeout;
 69  bool has_narrowing;
 70} mam_options_t;
 71
 72typedef struct {
 73  xmppc_t *xmppc;
 74  mam_options_t options;
 75  bool receive;
 76  int messages_total;
 77  gint64 deadline_us;
 78  char *queryid;
 79} mam_run_t;
 80
 81static void _mam_options_init(mam_options_t *options);
 82static void _mam_options_free(mam_options_t *options);
 83static bool _mam_parse_terms(xmppc_t *xmppc, int argc, char *argv[], mam_options_t *options, bool receive);
 84static void _mam_start_request(xmppc_t *xmppc, mam_options_t *options, bool pretty, bool receive);
 85static void _mam_send_request(mam_run_t *run);
 86static int _mam_finish(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
 87static int _mam_receive_poll(xmpp_conn_t *const conn, void *const userdata);
 88static int _mam_display_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
 89static int _mam_display_pretty_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
 90static void _mam_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type);
 91static void _mam_add_ids_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *ids);
 92static void _mam_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text);
 93static bool _mam_set_string_option(xmppc_t *xmppc, char **target, const char *key, const char *value, bool allow_empty);
 94static bool _mam_parse_max(xmppc_t *xmppc, mam_options_t *options, const char *value);
 95static bool _mam_parse_timeout(xmppc_t *xmppc, mam_options_t *options, const char *value);
 96static bool _mam_parse_duration_seconds(const char *value, gint64 *seconds);
 97static char *_mam_normalize_datetime(const char *value);
 98static bool _mam_result_matches(mam_run_t *run, xmpp_stanza_t *result);
 99static void _mam_print_compact_message(xmpp_stanza_t *result);
100static void _mam_print_escaped_attr(const char *text);
101static void _mam_print_escaped_text(const char *text);
102static void _mam_print_escaped(const char *text, bool attribute);
103static void _mam_usage(xmppc_t *xmppc);
104
105void mam_execute_command (xmppc_t *xmppc, int argc, char *argv[]) {
106  mam_options_t options;
107
108  if(argc >= 1) {
109    if (strcmp("list", argv[0]) == 0) {
110      if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, false)) {
111        _mam_start_request(xmppc, &options, false, false);
112      } else {
113        xmpp_disconnect(xmppc->conn);
114      }
115      return;
116    } else if (strcmp("pretty", argv[0]) == 0) {
117      if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, false)) {
118        _mam_start_request(xmppc, &options, true, false);
119      } else {
120        xmpp_disconnect(xmppc->conn);
121      }
122      return;
123    } else if (strcmp("receive", argv[0]) == 0) {
124      if (_mam_parse_terms(xmppc, argc - 1, argv + 1, &options, true)) {
125        _mam_start_request(xmppc, &options, false, true);
126      } else {
127        xmpp_disconnect(xmppc->conn);
128      }
129      return;
130    }
131  }
132
133  _mam_usage(xmppc);
134  xmpp_disconnect(xmppc->conn);
135}
136
137static void _mam_options_init(mam_options_t *options) {
138  memset(options, 0, sizeof(*options));
139  options->max = MAM_DEFAULT_MAX;
140  options->timeout = MAM_DEFAULT_TIMEOUT_SECONDS;
141}
142
143static void _mam_options_free(mam_options_t *options) {
144  g_free(options->with);
145  g_free(options->start);
146  g_free(options->end);
147  g_free(options->after_id);
148  g_free(options->before_id);
149  g_free(options->ids);
150}
151
152static bool _mam_parse_terms(xmppc_t *xmppc, int argc, char *argv[], mam_options_t *options, bool receive) {
153  _mam_options_init(options);
154
155  for (int i = 0; i < argc; i++) {
156    char *term = argv[i];
157    char *equals = strchr(term, '=');
158
159    if (!equals) {
160      if (!_mam_set_string_option(xmppc, &options->with, "with", term, false)) {
161        _mam_options_free(options);
162        return false;
163      }
164      options->has_narrowing = true;
165      continue;
166    }
167
168    char *key = g_strndup(term, equals - term);
169    const char *value = equals + 1;
170
171    if (strcmp(key, "with") == 0) {
172      if (!_mam_set_string_option(xmppc, &options->with, key, value, false)) {
173        g_free(key);
174        _mam_options_free(options);
175        return false;
176      }
177      options->has_narrowing = true;
178    } else if (strcmp(key, "start") == 0) {
179      char *normalized = _mam_normalize_datetime(value);
180      if (!_mam_set_string_option(xmppc, &options->start, key, normalized, false)) {
181        g_free(normalized);
182        g_free(key);
183        _mam_options_free(options);
184        return false;
185      }
186      g_free(normalized);
187      options->has_narrowing = true;
188    } else if (strcmp(key, "end") == 0) {
189      char *normalized = _mam_normalize_datetime(value);
190      if (!_mam_set_string_option(xmppc, &options->end, key, normalized, false)) {
191        g_free(normalized);
192        g_free(key);
193        _mam_options_free(options);
194        return false;
195      }
196      g_free(normalized);
197      options->has_narrowing = true;
198    } else if (strcmp(key, "after-id") == 0) {
199      if (!_mam_set_string_option(xmppc, &options->after_id, key, value, false)) {
200        g_free(key);
201        _mam_options_free(options);
202        return false;
203      }
204      options->has_narrowing = true;
205    } else if (strcmp(key, "before-id") == 0) {
206      if (!_mam_set_string_option(xmppc, &options->before_id, key, value, true)) {
207        g_free(key);
208        _mam_options_free(options);
209        return false;
210      }
211      options->has_narrowing = true;
212    } else if (strcmp(key, "ids") == 0) {
213      if (!_mam_set_string_option(xmppc, &options->ids, key, value, false)) {
214        g_free(key);
215        _mam_options_free(options);
216        return false;
217      }
218      options->has_narrowing = true;
219    } else if (strcmp(key, "max") == 0) {
220      if (!_mam_parse_max(xmppc, options, value)) {
221        g_free(key);
222        _mam_options_free(options);
223        return false;
224      }
225    } else if (receive && strcmp(key, "timeout") == 0) {
226      if (!_mam_parse_timeout(xmppc, options, value)) {
227        g_free(key);
228        _mam_options_free(options);
229        return false;
230      }
231    } else {
232      logError(xmppc, "Unknown MAM field: %s\n", key);
233      g_free(key);
234      _mam_options_free(options);
235      return false;
236    }
237
238    g_free(key);
239  }
240
241  if (receive) {
242    if (!options->with) {
243      logError(xmppc, "mam receive requires a JID or with=<jid>\n");
244      _mam_options_free(options);
245      return false;
246    }
247    if (!options->after_id) {
248      logError(xmppc, "mam receive requires after-id=<mam-id>\n");
249      _mam_options_free(options);
250      return false;
251    }
252  } else if (!options->has_narrowing) {
253    logError(xmppc, "mam list requires a JID, with=, start=, end=, after-id=, before-id=, or ids=\n");
254    _mam_options_free(options);
255    return false;
256  }
257
258  if (!options->start && !options->end && !options->after_id && !options->before_id && !options->ids) {
259    char *start = _mam_normalize_datetime("-5m");
260    options->start = start;
261  }
262
263  return true;
264}
265
266static void _mam_start_request(xmppc_t *xmppc, mam_options_t *options, bool pretty, bool receive) {
267  mam_run_t *run = g_new0(mam_run_t, 1);
268  run->xmppc = xmppc;
269  run->options = *options;
270  run->receive = receive;
271  if (receive) {
272    run->deadline_us = g_get_monotonic_time() + ((gint64)run->options.timeout * G_USEC_PER_SEC);
273  }
274
275  if (pretty) {
276    xmpp_handler_add (xmppc->conn, _mam_display_pretty_message, NULL,"message",NULL, run);
277  } else {
278    xmpp_handler_add (xmppc->conn, _mam_display_message, NULL,"message",NULL, run);
279  }
280
281  _mam_send_request(run);
282}
283
284static void _mam_send_request(mam_run_t *run) {
285  xmppc_t *xmppc = run->xmppc;
286  xmpp_ctx_t *ctx = xmppc->ctx;
287  char max[16];
288
289  g_snprintf(max, sizeof(max), "%d", run->options.max);
290
291  char* id = xmpp_uuid_gen(ctx);
292  g_free(run->queryid);
293  run->queryid = g_strdup(id);
294
295  xmpp_stanza_t *iq = xmpp_iq_new(ctx, "set", id);
296  xmpp_stanza_t *query = xmpp_stanza_new(ctx);
297  xmpp_stanza_set_name(query, "query");
298  xmpp_stanza_set_ns(query, MAM_NS);
299  xmpp_stanza_set_attribute(query, "queryid", id);
300
301  xmpp_stanza_t* x = xmpp_stanza_new(ctx);
302  xmpp_stanza_set_name(x,"x");
303  xmpp_stanza_set_ns(x, "jabber:x:data");
304  xmpp_stanza_set_type(x,"submit");
305  xmpp_stanza_add_child(query,x);
306
307  _mam_add_field(ctx, x, "FORM_TYPE", MAM_NS, "hidden");
308
309  if (run->options.with) {
310    _mam_add_field(ctx, x, "with", run->options.with, NULL);
311  }
312  if (run->options.start) {
313    _mam_add_field(ctx, x, "start", run->options.start, NULL);
314  }
315  if (run->options.end) {
316    _mam_add_field(ctx, x, "end", run->options.end, NULL);
317  }
318  if (run->options.ids) {
319    _mam_add_ids_field(ctx, x, run->options.ids);
320  }
321
322  xmpp_stanza_t *set = xmpp_stanza_new(ctx);
323  xmpp_stanza_set_name(set, "set");
324  xmpp_stanza_set_ns(set, RSM_NS);
325  _mam_add_text_child(ctx, set, "max", max);
326  if (run->options.after_id) {
327    _mam_add_text_child(ctx, set, "after", run->options.after_id);
328  }
329  if (run->options.before_id) {
330    _mam_add_text_child(ctx, set, "before", run->options.before_id);
331  }
332  xmpp_stanza_add_child(query, set);
333
334  xmpp_stanza_add_child(iq,query);
335  xmpp_id_handler_add(xmppc->conn, _mam_finish, id, run);
336  xmpp_send(xmppc->conn,iq);
337
338  xmpp_stanza_release(iq);
339  xmpp_stanza_release(query);
340  xmpp_stanza_release(x);
341  xmpp_stanza_release(set);
342  free(id);
343}
344
345static int _mam_finish(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata){
346  mam_run_t *run = (mam_run_t *)userdata;
347  const char *type = xmpp_stanza_get_type(stanza);
348
349  if (type && strcmp(type, "error") == 0) {
350    logError(run->xmppc, "MAM request failed\n");
351    xmpp_disconnect(conn);
352    return 0;
353  }
354
355  xmpp_stanza_t *fin = xmpp_stanza_get_child_by_name(stanza,"fin");
356  if(fin) {
357    if (run->receive && run->messages_total == 0 && g_get_monotonic_time() < run->deadline_us) {
358      xmpp_timed_handler_add(conn, _mam_receive_poll, MAM_POLL_PERIOD_MS, run);
359    } else {
360      xmpp_disconnect(conn);
361    }
362  }
363  return 0;
364}
365
366static int _mam_receive_poll(xmpp_conn_t *const conn, void *const userdata) {
367  mam_run_t *run = (mam_run_t *)userdata;
368
369  if (g_get_monotonic_time() >= run->deadline_us) {
370    xmpp_disconnect(conn);
371    return 0;
372  }
373
374  _mam_send_request(run);
375  return 0;
376}
377
378static void _mam_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type) {
379  xmpp_stanza_t* field = xmpp_stanza_new(ctx);
380  xmpp_stanza_set_name(field,"field");
381  xmpp_stanza_set_attribute(field, "var", var);
382  if (type) {
383    xmpp_stanza_set_type(field, type);
384  }
385
386  _mam_add_text_child(ctx, field, "value", value);
387  xmpp_stanza_add_child(x, field);
388  xmpp_stanza_release(field);
389}
390
391static void _mam_add_ids_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *ids) {
392  xmpp_stanza_t* field = xmpp_stanza_new(ctx);
393  xmpp_stanza_set_name(field,"field");
394  xmpp_stanza_set_attribute(field, "var", "ids");
395
396  char **parts = g_strsplit(ids, ",", -1);
397  for (int i = 0; parts[i] != NULL; i++) {
398    if (parts[i][0] != '\0') {
399      _mam_add_text_child(ctx, field, "value", parts[i]);
400    }
401  }
402  g_strfreev(parts);
403
404  xmpp_stanza_add_child(x, field);
405  xmpp_stanza_release(field);
406}
407
408static void _mam_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text) {
409  xmpp_stanza_t *child = xmpp_stanza_new(ctx);
410  xmpp_stanza_set_name(child, name);
411
412  xmpp_stanza_t *body = xmpp_stanza_new(ctx);
413  xmpp_stanza_set_text(body, text ? text : "");
414  xmpp_stanza_add_child(child, body);
415  xmpp_stanza_release(body);
416
417  xmpp_stanza_add_child(parent, child);
418  xmpp_stanza_release(child);
419}
420
421static bool _mam_set_string_option(xmppc_t *xmppc, char **target, const char *key, const char *value, bool allow_empty) {
422  if (*target) {
423    logError(xmppc, "Duplicate MAM field: %s\n", key);
424    return false;
425  }
426  if (!allow_empty && (!value || value[0] == '\0')) {
427    logError(xmppc, "MAM field requires a value: %s\n", key);
428    return false;
429  }
430  *target = g_strdup(value ? value : "");
431  return true;
432}
433
434static bool _mam_parse_max(xmppc_t *xmppc, mam_options_t *options, const char *value) {
435  char *end = NULL;
436  gint64 parsed = g_ascii_strtoll(value, &end, 10);
437
438  if (!value || value[0] == '\0' || !end || *end != '\0' || parsed < 1) {
439    logError(xmppc, "max must be a positive integer\n");
440    return false;
441  }
442
443  if (parsed > MAM_MAX_CAP) {
444    parsed = MAM_MAX_CAP;
445  }
446
447  options->max = (int)parsed;
448  return true;
449}
450
451static bool _mam_parse_timeout(xmppc_t *xmppc, mam_options_t *options, const char *value) {
452  gint64 seconds = 0;
453
454  if (!_mam_parse_duration_seconds(value, &seconds) || seconds < 0 || seconds > G_MAXINT) {
455    logError(xmppc, "timeout must be a duration like 30s, 5m, or 1h\n");
456    return false;
457  }
458
459  options->timeout = (int)seconds;
460  return true;
461}
462
463static bool _mam_parse_duration_seconds(const char *value, gint64 *seconds) {
464  char *end = NULL;
465  gint64 parsed = 0;
466  gint64 multiplier = 1;
467
468  if (!value || value[0] == '\0') {
469    return false;
470  }
471
472  parsed = g_ascii_strtoll(value, &end, 10);
473  if (end == value || parsed < 0) {
474    return false;
475  }
476
477  if (*end == '\0') {
478    multiplier = 1;
479  } else if (end[1] == '\0') {
480    if (*end == 's') {
481      multiplier = 1;
482    } else if (*end == 'm') {
483      multiplier = 60;
484    } else if (*end == 'h') {
485      multiplier = 60 * 60;
486    } else if (*end == 'd') {
487      multiplier = 60 * 60 * 24;
488    } else {
489      return false;
490    }
491  } else {
492    return false;
493  }
494
495  *seconds = parsed * multiplier;
496  return true;
497}
498
499static char *_mam_normalize_datetime(const char *value) {
500  if (value && value[0] == '-' && value[1] != '\0') {
501    gint64 seconds = 0;
502    if (_mam_parse_duration_seconds(value + 1, &seconds) && seconds > 0) {
503      GDateTime *now = g_date_time_new_now_utc();
504      GDateTime *then = g_date_time_add_seconds(now, -seconds);
505      char *formatted = g_date_time_format(then, "%Y-%m-%dT%H:%M:%SZ");
506      g_date_time_unref(then);
507      g_date_time_unref(now);
508      return formatted;
509    }
510  }
511
512  return g_strdup(value);
513}
514
515static bool _mam_result_matches(mam_run_t *run, xmpp_stanza_t *result) {
516  const char *ns = xmpp_stanza_get_ns(result);
517  if (!ns || strcmp(MAM_NS, ns) != 0) {
518    return false;
519  }
520
521  const char *queryid = xmpp_stanza_get_attribute(result, "queryid");
522  return !queryid || !run->queryid || strcmp(queryid, run->queryid) == 0;
523}
524
525static int _mam_display_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata) {
526  mam_run_t *run = (mam_run_t *)userdata;
527  xmpp_stanza_t* result = xmpp_stanza_get_child_by_name(stanza,"result");
528
529  if (result && _mam_result_matches(run, result) && run->messages_total < run->options.max) {
530    _mam_print_compact_message(result);
531    run->messages_total++;
532  }
533  return 1;
534}
535
536static int _mam_display_pretty_message(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata)
537{
538  mam_run_t *run = (mam_run_t *)userdata;
539  xmpp_stanza_t* result = xmpp_stanza_get_child_by_name(stanza,"result");
540
541  if( result && _mam_result_matches(run, result) && run->messages_total < run->options.max ) {
542
543    xmpp_stanza_t* forwarded = xmpp_stanza_get_child_by_ns(result, FORWARD_NS);
544    if (forwarded) {
545
546      xmpp_stanza_t* message = xmpp_stanza_get_child_by_name(forwarded, "message");
547      if (message) {
548        const char* const from = xmpp_stanza_get_from(message);
549        xmpp_stanza_t* body = xmpp_stanza_get_child_by_name(message, "body");
550
551        if (body) {
552          char *text = xmpp_stanza_get_text(body);
553          printf("%s: %s\n", from ? from : "", text ? text : "");
554          free(text);
555          run->messages_total++;
556        }
557      }
558    }
559  }
560
561  return 1;
562}
563
564static void _mam_print_compact_message(xmpp_stanza_t *result) {
565  const char *mam_id = xmpp_stanza_get_attribute(result, "id");
566  const char *message_id = "";
567  const char *stamp = "";
568  const char *from = "";
569  const char *to = "";
570  char *body_text = NULL;
571
572  xmpp_stanza_t* forwarded = xmpp_stanza_get_child_by_ns(result, FORWARD_NS);
573  xmpp_stanza_t* message = forwarded ? xmpp_stanza_get_child_by_name(forwarded, "message") : NULL;
574
575  if (forwarded) {
576    xmpp_stanza_t* delay = xmpp_stanza_get_child_by_ns(forwarded, DELAY_NS);
577    if (delay && xmpp_stanza_get_attribute(delay, "stamp")) {
578      stamp = xmpp_stanza_get_attribute(delay, "stamp");
579    }
580  }
581
582  if (message) {
583    xmpp_stanza_t* body = xmpp_stanza_get_child_by_name(message, "body");
584    if (xmpp_stanza_get_id(message)) {
585      message_id = xmpp_stanza_get_id(message);
586    }
587    if (xmpp_stanza_get_from(message)) {
588      from = xmpp_stanza_get_from(message);
589    }
590    if (xmpp_stanza_get_to(message)) {
591      to = xmpp_stanza_get_to(message);
592    }
593    if (body) {
594      body_text = xmpp_stanza_get_text(body);
595    }
596  }
597
598  printf("<message id=\"");
599  _mam_print_escaped_attr(mam_id ? mam_id : "");
600  printf("\" message-id=\"");
601  _mam_print_escaped_attr(message_id);
602  printf("\" stamp=\"");
603  _mam_print_escaped_attr(stamp);
604  printf("\" from=\"");
605  _mam_print_escaped_attr(from);
606  printf("\" to=\"");
607  _mam_print_escaped_attr(to);
608  printf("\"><body>");
609  _mam_print_escaped_text(body_text ? body_text : "");
610  printf("</body></message>\n");
611  fflush(stdout);
612
613  free(body_text);
614}
615
616static void _mam_print_escaped_attr(const char *text) {
617  _mam_print_escaped(text, true);
618}
619
620static void _mam_print_escaped_text(const char *text) {
621  _mam_print_escaped(text, false);
622}
623
624static void _mam_print_escaped(const char *text, bool attribute) {
625  const unsigned char *p = (const unsigned char *)(text ? text : "");
626
627  while (*p) {
628    switch (*p) {
629      case '&':
630        printf("&amp;");
631        break;
632      case '<':
633        printf("&lt;");
634        break;
635      case '>':
636        printf("&gt;");
637        break;
638      case '"':
639        if (attribute) {
640          printf("&quot;");
641        } else {
642          putchar('"');
643        }
644        break;
645      case '\n':
646        printf("&#10;");
647        break;
648      case '\r':
649        printf("&#13;");
650        break;
651      case '\t':
652        if (attribute) {
653          printf("&#9;");
654        } else {
655          putchar('\t');
656        }
657        break;
658      default:
659        if (*p < 0x20) {
660          printf("&#x%02X;", *p);
661        } else {
662          putchar(*p);
663        }
664        break;
665    }
666    p++;
667  }
668}
669
670static void _mam_usage(xmppc_t *xmppc) {
671  logError(xmppc, "Usage:\n");
672  logError(xmppc, "  mam list <jid> [with=<jid>] [start=<time>] [end=<time>] [after-id=<id>] [before-id=<id>] [ids=<id>[,<id>...]] [max=<n>]\n");
673  logError(xmppc, "  mam receive <jid> after-id=<id> [timeout=300s] [max=5]\n");
674}