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