1/*!
2 * @file message.c
3 *
4 * vim: expandtab:ts=2:sts=2:sw=2
5 *
6 * @authors
7 * Copyright (C) 2020 Anoxinon e.V.
8 *
9 * @copyright
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 "message.h"
44#include "glib.h"
45#include "stdbool.h"
46#include "stdlib.h"
47#include "string.h"
48
49#define MAM_NS "urn:xmpp:mam:2"
50#define FORWARD_NS "urn:xmpp:forward:0"
51#define RSM_NS "http://jabber.org/protocol/rsm"
52#define SID_NS "urn:xmpp:sid:0"
53
54#define MESSAGE_ARCHIVE_LOOKUP_TIMEOUT_SECONDS 15
55#define MESSAGE_ARCHIVE_LOOKUP_POLL_MS 1000
56
57typedef struct {
58 xmppc_t *xmppc;
59 char *to;
60 char *message_id;
61 char *start;
62 char *queryid;
63 char *archive_id;
64 gint64 deadline_us;
65 bool done;
66} message_archive_lookup_t;
67
68static void _message_send_text(xmppc_t *xmppc, char* to, char* message);
69static void _message_lookup_archive_id(xmppc_t *xmppc, const char *to, const char *message_id);
70static void _message_send_archive_query(message_archive_lookup_t *lookup);
71static int _message_archive_result_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
72static int _message_archive_query_finished(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
73static int _message_archive_query_poll(xmpp_conn_t *const conn, void *const userdata);
74static int _message_archive_lookup_timeout(xmpp_conn_t *const conn, void *const userdata);
75static bool _message_archive_result_matches(message_archive_lookup_t *lookup, xmpp_stanza_t *result);
76static bool _message_has_origin_id(xmpp_stanza_t *message, const char *id);
77static void _message_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text);
78static void _message_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type);
79static char *_message_lookup_start_time(void);
80static void _message_finish_sent(xmpp_conn_t *conn, message_archive_lookup_t *lookup);
81static void _message_print_sent(message_archive_lookup_t *lookup);
82static void _message_print_escaped_attr(const char *text);
83
84void message_execute_command(xmppc_t *xmppc, int argc, char *argv[]) {
85 if( argc == 3 && ( strcmp("chat", argv[0] ) == 0)) {
86 _message_send_text(xmppc, argv[1],argv[2]);
87 } else {
88 logError(xmppc, "Befehl unbekannt");
89 xmpp_disconnect(xmppc->conn);
90 }
91}
92
93void _message_send_text(xmppc_t *xmppc, char* to, char* text) {
94 xmpp_conn_t *conn = xmppc->conn;
95 xmpp_ctx_t *ctx = xmpp_conn_get_context(conn);
96 xmpp_stanza_t *message;
97 char* id = xmpp_uuid_gen(xmppc->ctx);
98 message = xmpp_message_new(ctx, "chat", to, id);
99 int res = xmpp_message_set_body(message, text);
100 if(res == 0) {
101 xmpp_stanza_t *origin = xmpp_stanza_new(ctx);
102 xmpp_stanza_set_name(origin, "origin-id");
103 xmpp_stanza_set_ns(origin, SID_NS);
104 xmpp_stanza_set_attribute(origin, "id", id);
105 xmpp_stanza_add_child(message, origin);
106 xmpp_stanza_release(origin);
107
108 xmpp_send(conn, message);
109 _message_lookup_archive_id(xmppc, to, id);
110 } else {
111 xmpp_disconnect(conn);
112 }
113 xmpp_stanza_release(message);
114 free(id);
115}
116
117static void _message_lookup_archive_id(xmppc_t *xmppc, const char *to, const char *message_id) {
118 message_archive_lookup_t *lookup = g_new0(message_archive_lookup_t, 1);
119 lookup->xmppc = xmppc;
120 lookup->to = g_strdup(to);
121 lookup->message_id = g_strdup(message_id);
122 lookup->start = _message_lookup_start_time();
123 lookup->deadline_us = g_get_monotonic_time() + ((gint64)MESSAGE_ARCHIVE_LOOKUP_TIMEOUT_SECONDS * G_USEC_PER_SEC);
124
125 xmpp_handler_add(xmppc->conn, _message_archive_result_handler, NULL, "message", NULL, lookup);
126 xmpp_timed_handler_add(xmppc->conn, _message_archive_lookup_timeout, MESSAGE_ARCHIVE_LOOKUP_TIMEOUT_SECONDS * 1000, lookup);
127 _message_send_archive_query(lookup);
128}
129
130static void _message_send_archive_query(message_archive_lookup_t *lookup) {
131 xmppc_t *xmppc = lookup->xmppc;
132 xmpp_ctx_t *ctx = xmppc->ctx;
133 char* id = xmpp_uuid_gen(ctx);
134 g_free(lookup->queryid);
135 lookup->queryid = g_strdup(id);
136
137 xmpp_stanza_t *iq = xmpp_iq_new(ctx, "set", id);
138 xmpp_stanza_t *query = xmpp_stanza_new(ctx);
139 xmpp_stanza_set_name(query, "query");
140 xmpp_stanza_set_ns(query, MAM_NS);
141 xmpp_stanza_set_attribute(query, "queryid", id);
142
143 xmpp_stanza_t* x = xmpp_stanza_new(ctx);
144 xmpp_stanza_set_name(x,"x");
145 xmpp_stanza_set_ns(x, "jabber:x:data");
146 xmpp_stanza_set_type(x,"submit");
147 xmpp_stanza_add_child(query,x);
148
149 _message_add_field(ctx, x, "FORM_TYPE", MAM_NS, "hidden");
150 _message_add_field(ctx, x, "with", lookup->to, NULL);
151 _message_add_field(ctx, x, "start", lookup->start, NULL);
152
153 xmpp_stanza_t *set = xmpp_stanza_new(ctx);
154 xmpp_stanza_set_name(set, "set");
155 xmpp_stanza_set_ns(set, RSM_NS);
156 _message_add_text_child(ctx, set, "max", "50");
157 _message_add_text_child(ctx, set, "before", "");
158 xmpp_stanza_add_child(query, set);
159
160 xmpp_stanza_add_child(iq, query);
161 xmpp_id_handler_add(xmppc->conn, _message_archive_query_finished, id, lookup);
162 xmpp_send(xmppc->conn, iq);
163
164 xmpp_stanza_release(iq);
165 xmpp_stanza_release(query);
166 xmpp_stanza_release(x);
167 xmpp_stanza_release(set);
168 free(id);
169}
170
171static int _message_archive_result_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata) {
172 message_archive_lookup_t *lookup = (message_archive_lookup_t *)userdata;
173 xmpp_stanza_t* result = xmpp_stanza_get_child_by_name(stanza,"result");
174
175 if (!lookup->done && result && _message_archive_result_matches(lookup, result)) {
176 const char *archive_id = xmpp_stanza_get_attribute(result, "id");
177 if (archive_id) {
178 g_free(lookup->archive_id);
179 lookup->archive_id = g_strdup(archive_id);
180 }
181 }
182
183 return 1;
184}
185
186static int _message_archive_query_finished(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata) {
187 message_archive_lookup_t *lookup = (message_archive_lookup_t *)userdata;
188 const char *type = xmpp_stanza_get_type(stanza);
189
190 if (lookup->done) {
191 return 0;
192 }
193
194 if (lookup->archive_id) {
195 _message_finish_sent(conn, lookup);
196 return 0;
197 }
198
199 if (type && strcmp(type, "error") == 0) {
200 logError(lookup->xmppc, "MAM archive lookup failed\n");
201 _message_finish_sent(conn, lookup);
202 return 0;
203 }
204
205 if (g_get_monotonic_time() < lookup->deadline_us) {
206 xmpp_timed_handler_add(conn, _message_archive_query_poll, MESSAGE_ARCHIVE_LOOKUP_POLL_MS, lookup);
207 } else {
208 logError(lookup->xmppc, "MAM archive lookup timed out\n");
209 _message_finish_sent(conn, lookup);
210 }
211
212 return 0;
213}
214
215static int _message_archive_query_poll(xmpp_conn_t *const conn, void *const userdata) {
216 message_archive_lookup_t *lookup = (message_archive_lookup_t *)userdata;
217
218 if (lookup->done) {
219 return 0;
220 }
221
222 if (g_get_monotonic_time() >= lookup->deadline_us) {
223 logError(lookup->xmppc, "MAM archive lookup timed out\n");
224 _message_finish_sent(conn, lookup);
225 return 0;
226 }
227
228 _message_send_archive_query(lookup);
229 return 0;
230}
231
232static int _message_archive_lookup_timeout(xmpp_conn_t *const conn, void *const userdata) {
233 message_archive_lookup_t *lookup = (message_archive_lookup_t *)userdata;
234
235 if (!lookup->done) {
236 logError(lookup->xmppc, "MAM archive lookup timed out\n");
237 _message_finish_sent(conn, lookup);
238 }
239
240 return 0;
241}
242
243static bool _message_archive_result_matches(message_archive_lookup_t *lookup, xmpp_stanza_t *result) {
244 const char *ns = xmpp_stanza_get_ns(result);
245 if (!ns || strcmp(MAM_NS, ns) != 0) {
246 return false;
247 }
248
249 const char *queryid = xmpp_stanza_get_attribute(result, "queryid");
250 if (queryid && lookup->queryid && strcmp(queryid, lookup->queryid) != 0) {
251 return false;
252 }
253
254 xmpp_stanza_t* forwarded = xmpp_stanza_get_child_by_ns(result, FORWARD_NS);
255 xmpp_stanza_t* message = forwarded ? xmpp_stanza_get_child_by_name(forwarded, "message") : NULL;
256 if (!message) {
257 return false;
258 }
259
260 const char *message_id = xmpp_stanza_get_id(message);
261 if (message_id && strcmp(message_id, lookup->message_id) == 0) {
262 return true;
263 }
264
265 return _message_has_origin_id(message, lookup->message_id);
266}
267
268static bool _message_has_origin_id(xmpp_stanza_t *message, const char *id) {
269 for (xmpp_stanza_t *child = xmpp_stanza_get_children(message); child; child = xmpp_stanza_get_next(child)) {
270 const char *name = xmpp_stanza_get_name(child);
271 const char *ns = xmpp_stanza_get_ns(child);
272 const char *origin_id = xmpp_stanza_get_attribute(child, "id");
273
274 if (name && ns && origin_id && strcmp(name, "origin-id") == 0 && strcmp(ns, SID_NS) == 0 && strcmp(origin_id, id) == 0) {
275 return true;
276 }
277 }
278
279 return false;
280}
281
282static void _message_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text) {
283 xmpp_stanza_t *child = xmpp_stanza_new(ctx);
284 xmpp_stanza_set_name(child, name);
285
286 xmpp_stanza_t *body = xmpp_stanza_new(ctx);
287 xmpp_stanza_set_text(body, text ? text : "");
288 xmpp_stanza_add_child(child, body);
289 xmpp_stanza_release(body);
290
291 xmpp_stanza_add_child(parent, child);
292 xmpp_stanza_release(child);
293}
294
295static void _message_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type) {
296 xmpp_stanza_t* field = xmpp_stanza_new(ctx);
297 xmpp_stanza_set_name(field,"field");
298 xmpp_stanza_set_attribute(field, "var", var);
299 if (type) {
300 xmpp_stanza_set_type(field, type);
301 }
302
303 _message_add_text_child(ctx, field, "value", value);
304 xmpp_stanza_add_child(x, field);
305 xmpp_stanza_release(field);
306}
307
308static char *_message_lookup_start_time(void) {
309 GDateTime *now = g_date_time_new_now_utc();
310 GDateTime *then = g_date_time_add_seconds(now, -30);
311 char *formatted = g_date_time_format(then, "%Y-%m-%dT%H:%M:%SZ");
312 g_date_time_unref(then);
313 g_date_time_unref(now);
314 return formatted;
315}
316
317static void _message_finish_sent(xmpp_conn_t *conn, message_archive_lookup_t *lookup) {
318 if (lookup->done) {
319 return;
320 }
321
322 lookup->done = true;
323 _message_print_sent(lookup);
324 xmpp_disconnect(conn);
325}
326
327static void _message_print_sent(message_archive_lookup_t *lookup) {
328 printf("<sent archive-id=\"");
329 _message_print_escaped_attr(lookup->archive_id ? lookup->archive_id : "");
330 printf("\" message-id=\"");
331 _message_print_escaped_attr(lookup->message_id);
332 printf("\" to=\"");
333 _message_print_escaped_attr(lookup->to);
334 printf("\"/>\n");
335 fflush(stdout);
336}
337
338static void _message_print_escaped_attr(const char *text) {
339 const unsigned char *p = (const unsigned char *)(text ? text : "");
340
341 while (*p) {
342 switch (*p) {
343 case '&':
344 printf("&");
345 break;
346 case '<':
347 printf("<");
348 break;
349 case '>':
350 printf(">");
351 break;
352 case '"':
353 printf(""");
354 break;
355 case '\n':
356 printf(" ");
357 break;
358 case '\r':
359 printf(" ");
360 break;
361 case '\t':
362 printf("	");
363 break;
364 default:
365 if (*p < 0x20) {
366 printf("&#x%02X;", *p);
367 } else {
368 putchar(*p);
369 }
370 break;
371 }
372 p++;
373 }
374}