Release 1.25.0 -- Buddy Idle Time, RTF
[siplcs.git] / src / core / sipe-groupchat.c
blob0d221c05d3c59fa4bc365c6c928655271ff04f14
1 /**
2 * @file sipe-groupchat.c
4 * pidgin-sipe
6 * Copyright (C) 2010-2019 SIPE Project <http://sipe.sourceforge.net/>
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 /**
24 * This module implements the OCS2007R2 Group Chat functionality
26 * Documentation references:
28 * Microsoft TechNet: Key Protocols and Windows Services Used by Group Chat
29 * <http://technet.microsoft.com/en-us/library/ee323484%28office.13%29.aspx>
30 * Microsoft TechNet: Group Chat Call Flows
31 * <http://technet.microsoft.com/en-us/library/ee323524%28office.13%29.aspx>
32 * Microsoft Office Communications Server 2007 R2 Technical Reference Guide
33 * <http://go.microsoft.com/fwlink/?LinkID=159649>
34 * Microsoft DevNet: [MS-XCCOSIP] Extensible Chat Control Over SIP
35 * <http://msdn.microsoft.com/en-us/library/hh624112.aspx>
36 * RFC 4028: Session Timers in the Session Initiation Protocol (SIP)
37 * <http://www.rfc-editor.org/rfc/rfc4028.txt>
40 * @TODO:
42 * -.cmd:getserverinfo
43 * <sib domain="<DOMAIN>" infoType="123" />
44 * rpl:getservinfo
45 * <sib infoType="123"
46 * serverTime="2010-09-14T14:26:17.6206356Z"
47 * searchLimit="999"
48 * messageSizeLimit="512"
49 * storySizeLimit="4096"
50 * rootUri="ma-cat://<DOMAIN>/<GUID>"
51 * dbVersion="3ea3a5a8-ef36-46cf-898f-7a5133931d63"
52 * />
54 * is there any information in there we would need/use?
56 * - cmd:getpref/rpl:getpref/cmd:setpref/rpl:setpref
57 * probably useless, as libpurple stores configuration locally
59 * can store base64 encoded "free text" in key/value fashion
60 * <cmd id="cmd:getpref" seqid="x">
61 * <data>
62 * <pref label="kedzie.GroupChannels"
63 * seqid="71"
64 * createdefault="true" />
65 * </data>
66 * </cmd>
67 * <cmd id="cmd:setpref" seqid="x">
68 * <data>
69 * <pref label="kedzie.GroupChannels"
70 * seqid="71"
71 * createdefault="false"
72 * content="<BASE64 text>" />
73 * </data>
74 * </cmd>
76 * use this to sync chats in buddy list on multiple clients?
78 * - cmd:getinv
79 * <inv inviteId="1" domain="<DOMAIN>" />
80 * rpl:getinv
81 * ???
83 * according to documentation should provide list of outstanding invites.
84 * [no log file examples]
85 * should we automatically join those channels or ask user to join/add?
87 * - chatserver_command_message()
88 * needs to support multiple <grpchat> nodes?
89 * [no log file examples]
91 * - create/delete chat rooms
92 * [no log file examples]
93 * are these related to this functionality?
95 * <cmd id="cmd:nodespermcreatechild" seqid="1">
96 * <data />
97 * </cmd>
98 * <rpl id="rpl:nodespermcreatechild" seqid="1">
99 * <commandid seqid="1" envid="xxx" />
100 * <resp code="200">SUCCESS_OK</resp>
101 * <data />
102 * </rpl>
104 * - file transfer (uses HTTPS PUT/GET via a filestore server)
105 * [no log file examples]
109 #ifdef HAVE_CONFIG_H
110 #include "config.h"
111 #endif
113 #include <stdlib.h>
114 #include <string.h>
116 #include <glib.h>
118 #include "sipe-common.h"
119 #include "sipmsg.h"
120 #include "sip-transport.h"
121 #include "sipe-backend.h"
122 #include "sipe-chat.h"
123 #include "sipe-core.h"
124 #include "sipe-core-private.h"
125 #include "sipe-dialog.h"
126 #include "sipe-groupchat.h"
127 #include "sipe-im.h"
128 #include "sipe-nls.h"
129 #include "sipe-schedule.h"
130 #include "sipe-session.h"
131 #include "sipe-utils.h"
132 #include "sipe-xml.h"
134 #define GROUPCHAT_RETRY_TIMEOUT 5*60 /* seconds */
137 * aib node - magic numbers?
139 * Example:
140 * <aib key="3984" value="0,1,2,3,4,5,7,9,10,12,13,14,15,16,17" />
141 * <aib key="12276" value="6,8,11" />
143 * "value" corresponds to the "id" attribute in uib nodes.
145 * @TODO: Confirm "guessed" meaning of the magic numbers:
146 * 3984 = normal users
147 * 12276 = channel operators
149 #define GROUPCHAT_AIB_KEY_USER "3984"
150 #define GROUPCHAT_AIB_KEY_CHANOP "12276"
152 struct sipe_groupchat {
153 struct sip_session *session;
154 gchar *domain;
155 GSList *join_queue;
156 GHashTable *uri_to_chat_session;
157 GHashTable *msgs;
158 guint envid;
159 guint expires;
160 gboolean connected;
163 struct sipe_groupchat_msg {
164 GHashTable *container;
165 struct sipe_chat_session *session;
166 gchar *content;
167 gchar *xccos;
168 guint envid;
171 /* GDestroyNotify */
172 static void sipe_groupchat_msg_free(gpointer data) {
173 struct sipe_groupchat_msg *msg = data;
174 g_free(msg->content);
175 g_free(msg->xccos);
176 g_free(msg);
179 /* GDestroyNotify */
180 static void sipe_groupchat_msg_remove(gpointer data) {
181 struct sipe_groupchat_msg *msg = data;
182 g_hash_table_remove(msg->container, &msg->envid);
185 static void sipe_groupchat_allocate(struct sipe_core_private *sipe_private)
187 struct sipe_groupchat *groupchat = g_new0(struct sipe_groupchat, 1);
189 groupchat->uri_to_chat_session = g_hash_table_new(g_str_hash, g_str_equal);
190 groupchat->msgs = g_hash_table_new_full(g_int_hash, g_int_equal,
191 NULL,
192 sipe_groupchat_msg_free);
193 groupchat->envid = rand();
194 groupchat->connected = FALSE;
195 sipe_private->groupchat = groupchat;
198 static void sipe_groupchat_free_join_queue(struct sipe_groupchat *groupchat)
200 sipe_utils_slist_free_full(groupchat->join_queue, g_free);
201 groupchat->join_queue = NULL;
204 void sipe_groupchat_free(struct sipe_core_private *sipe_private)
206 struct sipe_groupchat *groupchat = sipe_private->groupchat;
207 if (groupchat) {
208 sipe_groupchat_free_join_queue(groupchat);
209 g_hash_table_destroy(groupchat->msgs);
210 g_hash_table_destroy(groupchat->uri_to_chat_session);
211 g_free(groupchat->domain);
212 g_free(groupchat);
213 sipe_private->groupchat = NULL;
217 static struct sipe_groupchat_msg *generate_xccos_message(struct sipe_groupchat *groupchat,
218 const gchar *content)
220 struct sipe_groupchat_msg *msg = g_new0(struct sipe_groupchat_msg, 1);
222 msg->container = groupchat->msgs;
223 msg->envid = groupchat->envid++;
224 msg->xccos = g_strdup_printf("<xccos ver=\"1\" envid=\"%u\" xmlns=\"urn:parlano:xml:ns:xccos\">"
225 "%s"
226 "</xccos>",
227 msg->envid,
228 content);
230 g_hash_table_insert(groupchat->msgs, &msg->envid, msg);
232 return(msg);
236 * Create short-lived dialog with ocschat@<domain> (or user specified value)
237 * This initiates the Group Chat feature
239 void sipe_groupchat_init(struct sipe_core_private *sipe_private)
241 const gchar *setting = sipe_backend_setting(SIPE_CORE_PUBLIC,
242 SIPE_SETTING_GROUPCHAT_USER);
243 const gchar *persistent = sipe_private->persistentChatPool_uri;
244 gboolean user_set = !is_empty(setting);
245 gboolean provisioned = !is_empty(persistent);
246 gchar **parts = g_strsplit(user_set ? setting :
247 provisioned ? persistent :
248 sipe_private->username, "@", 2);
249 gboolean domain_found = !is_empty(parts[1]);
250 const gchar *user = "ocschat";
251 const gchar *domain = parts[domain_found ? 1 : 0];
252 gchar *chat_uri;
253 struct sip_session *session;
254 struct sipe_groupchat *groupchat;
256 /* User specified or provisioned URI is valid 'user@company.com' */
257 if ((user_set || provisioned) && domain_found && !is_empty(parts[0]))
258 user = parts[0];
260 SIPE_DEBUG_INFO("sipe_groupchat_init: username '%s' setting '%s' persistent '%s' split '%s'/'%s' GC user %s@%s",
261 sipe_private->username, setting ? setting : "(null)",
262 persistent ? persistent : "(null)",
263 parts[0], parts[1] ? parts[1] : "(null)", user, domain);
265 if (!sipe_private->groupchat)
266 sipe_groupchat_allocate(sipe_private);
267 groupchat = sipe_private->groupchat;
269 chat_uri = g_strdup_printf("sip:%s@%s", user, domain);
270 session = sipe_session_find_or_add_im(sipe_private,
271 chat_uri);
272 session->is_groupchat = TRUE;
273 sipe_im_invite(sipe_private, session, chat_uri,
274 NULL, NULL, NULL, FALSE);
276 g_free(groupchat->domain);
277 groupchat->domain = g_strdup(domain);
279 g_free(chat_uri);
280 g_strfreev(parts);
283 /* sipe_schedule_action */
284 static void groupchat_init_retry_cb(struct sipe_core_private *sipe_private,
285 SIPE_UNUSED_PARAMETER gpointer data)
287 sipe_groupchat_init(sipe_private);
290 static void groupchat_init_retry(struct sipe_core_private *sipe_private)
292 struct sipe_groupchat *groupchat = sipe_private->groupchat;
294 SIPE_DEBUG_INFO_NOFORMAT("groupchat_init_retry: trying again later...");
296 groupchat->session = NULL;
297 groupchat->connected = FALSE;
299 sipe_schedule_seconds(sipe_private,
300 "<+groupchat-retry>",
301 NULL,
302 GROUPCHAT_RETRY_TIMEOUT,
303 groupchat_init_retry_cb,
304 NULL);
307 void sipe_groupchat_invite_failed(struct sipe_core_private *sipe_private,
308 struct sip_session *session)
310 struct sipe_groupchat *groupchat = sipe_private->groupchat;
311 const gchar *setting = sipe_backend_setting(SIPE_CORE_PUBLIC,
312 SIPE_SETTING_GROUPCHAT_USER);
313 gboolean retry = FALSE;
315 if (groupchat->session) {
316 /* response to group chat server invite */
317 SIPE_DEBUG_ERROR_NOFORMAT("can't connect to group chat server!");
319 /* group chat server exists, but communication failed */
320 retry = TRUE;
321 } else {
322 /* response to initial invite */
323 SIPE_DEBUG_INFO_NOFORMAT("no group chat server found.");
326 sipe_session_close(sipe_private, session);
328 if (!is_empty(setting)) {
329 gchar *msg = g_strdup_printf(_("Group Chat Proxy setting is incorrect:\n\n\t%s\n\nPlease update your Account."),
330 setting);
331 sipe_backend_notify_error(SIPE_CORE_PUBLIC,
332 _("Couldn't find Group Chat server!"),
333 msg);
334 g_free(msg);
336 /* user specified group chat settings: we should retry */
337 retry = TRUE;
340 if (retry) {
341 groupchat_init_retry(sipe_private);
342 } else {
343 SIPE_DEBUG_INFO_NOFORMAT("disabling group chat feature.");
347 static gchar *generate_chanid_node(const gchar *uri, guint key)
349 /* ma-chan://<domain>/<value> */
350 gchar **parts = g_strsplit(uri, "/", 4);
351 gchar *chanid = NULL;
353 if (parts[2] && parts[3]) {
354 chanid = g_strdup_printf("<chanid key=\"%d\" domain=\"%s\" value=\"%s\"/>",
355 key, parts[2], parts[3]);
356 } else {
357 SIPE_DEBUG_ERROR("generate_chanid_node: mal-formed URI '%s'",
358 uri);
360 g_strfreev(parts);
362 return chanid;
365 /* TransCallback */
366 static void groupchat_update_cb(struct sipe_core_private *sipe_private,
367 gpointer data);
368 static gboolean groupchat_expired_session_response(struct sipe_core_private *sipe_private,
369 struct sipmsg *msg,
370 SIPE_UNUSED_PARAMETER struct transaction *trans)
372 struct sipe_groupchat *groupchat = sipe_private->groupchat;
374 /* 481 Call Leg Does Not Exist -> server dropped session */
375 if (msg->response == 481) {
376 struct sip_session *session = groupchat->session;
377 struct sip_dialog *dialog = sipe_dialog_find(session,
378 session->with);
380 if (dialog) {
381 /* close dialog from our side */
382 sip_transport_bye(sipe_private, dialog);
383 sipe_dialog_remove(session, session->with);
384 /* dialog is no longer valid */
387 /* re-initialize groupchat session */
388 groupchat->session = NULL;
389 groupchat->connected = FALSE;
390 sipe_groupchat_init(sipe_private);
391 } else {
392 sipe_schedule_seconds(sipe_private,
393 "<+groupchat-expires>",
394 NULL,
395 groupchat->expires,
396 groupchat_update_cb,
397 NULL);
400 return(TRUE);
403 /* sipe_schedule_action */
404 static void groupchat_update_cb(struct sipe_core_private *sipe_private,
405 SIPE_UNUSED_PARAMETER gpointer data)
407 struct sipe_groupchat *groupchat = sipe_private->groupchat;
409 if (groupchat->session) {
410 struct sip_dialog *dialog = sipe_dialog_find(groupchat->session,
411 groupchat->session->with);
413 if (dialog)
414 sip_transport_update(sipe_private,
415 dialog,
416 groupchat_expired_session_response);
420 static struct sipe_groupchat_msg *chatserver_command(struct sipe_core_private *sipe_private,
421 const gchar *cmd);
423 void sipe_groupchat_invite_response(struct sipe_core_private *sipe_private,
424 struct sip_dialog *dialog,
425 struct sipmsg *response)
427 struct sipe_groupchat *groupchat = sipe_private->groupchat;
429 SIPE_DEBUG_INFO_NOFORMAT("sipe_groupchat_invite_response");
431 if (!groupchat->session) {
432 /* response to initial invite */
433 struct sipe_groupchat_msg *msg = generate_xccos_message(groupchat,
434 "<cmd id=\"cmd:requri\" seqid=\"1\"><data/></cmd>");
435 const gchar *session_expires = sipmsg_find_header(response,
436 "Session-Expires");
438 sip_transport_info(sipe_private,
439 "Content-Type: text/plain\r\n",
440 msg->xccos,
441 dialog,
442 NULL);
443 sipe_groupchat_msg_remove(msg);
445 if (session_expires) {
446 groupchat->expires = strtoul(session_expires, NULL, 10);
448 if (groupchat->expires) {
449 SIPE_DEBUG_INFO("sipe_groupchat_invite_response: session expires in %d seconds",
450 groupchat->expires);
452 if (groupchat->expires > 10)
453 groupchat->expires -= 10;
454 sipe_schedule_seconds(sipe_private,
455 "<+groupchat-expires>",
456 NULL,
457 groupchat->expires,
458 groupchat_update_cb,
459 NULL);
463 } else {
464 /* response to group chat server invite */
465 gchar *invcmd;
467 SIPE_DEBUG_INFO_NOFORMAT("connection to group chat server established.");
469 groupchat->connected = TRUE;
471 /* Any queued joins? */
472 if (groupchat->join_queue) {
473 GString *cmd = g_string_new("<cmd id=\"cmd:bjoin\" seqid=\"1\">"
474 "<data>");
475 GSList *entry;
476 guint i = 0;
478 /* We used g_slist_prepend() to create the list */
479 groupchat->join_queue = entry = g_slist_reverse(groupchat->join_queue);
480 while (entry) {
481 gchar *chanid = generate_chanid_node(entry->data, i++);
482 g_string_append(cmd, chanid);
483 g_free(chanid);
484 entry = entry->next;
486 sipe_groupchat_free_join_queue(groupchat);
488 g_string_append(cmd, "</data></cmd>");
489 chatserver_command(sipe_private, cmd->str);
490 g_string_free(cmd, TRUE);
493 /* Request outstanding invites from server */
494 invcmd = g_strdup_printf("<cmd id=\"cmd:getinv\" seqid=\"1\">"
495 "<data>"
496 "<inv inviteId=\"1\" domain=\"%s\"/>"
497 "</data>"
498 "</cmd>", groupchat->domain);
499 chatserver_command(sipe_private, invcmd);
500 g_free(invcmd);
504 static void chatserver_command_error_notify(struct sipe_core_private *sipe_private,
505 struct sipe_chat_session *chat_session,
506 const gchar *content)
508 gchar *label = g_strdup_printf(_("This message was not delivered to chat room '%s'"),
509 chat_session->title);
510 gchar *errmsg = g_strdup_printf("%s:\n<font color=\"#888888\"></b>%s<b></font>",
511 label, content);
512 g_free(label);
513 sipe_backend_notify_message_error(SIPE_CORE_PUBLIC,
514 chat_session->backend,
515 NULL,
516 errmsg);
517 g_free(errmsg);
520 /* TransCallback */
521 static gboolean chatserver_command_response(struct sipe_core_private *sipe_private,
522 struct sipmsg *msg,
523 struct transaction *trans)
525 if (msg->response != 200) {
526 struct sipe_groupchat_msg *gmsg = trans->payload->data;
527 struct sipe_chat_session *chat_session = gmsg->session;
529 SIPE_DEBUG_INFO("chatserver_command_response: failure %d", msg->response);
531 if (chat_session)
532 chatserver_command_error_notify(sipe_private,
533 chat_session,
534 gmsg->content);
536 groupchat_expired_session_response(sipe_private, msg, trans);
538 return TRUE;
541 static struct sipe_groupchat_msg *chatserver_command(struct sipe_core_private *sipe_private,
542 const gchar *cmd)
544 struct sipe_groupchat *groupchat = sipe_private->groupchat;
545 struct sipe_groupchat_msg *msg = NULL;
547 if (groupchat->session) {
548 struct sip_dialog *dialog = sipe_dialog_find(groupchat->session,
549 groupchat->session->with);
551 if (dialog) {
552 struct transaction *trans;
554 msg = generate_xccos_message(groupchat, cmd);
555 trans = sip_transport_info(sipe_private,
556 "Content-Type: text/plain\r\n",
557 msg->xccos,
558 dialog,
559 chatserver_command_response);
561 if (trans) {
562 struct transaction_payload *payload = g_new0(struct transaction_payload, 1);
564 payload->destroy = sipe_groupchat_msg_remove;
565 payload->data = msg;
566 trans->payload = payload;
567 } else {
568 /* SIP transport is no longer valid - give up */
569 sipe_groupchat_msg_remove(msg);
570 msg = NULL;
575 return(msg);
578 static void chatserver_response_uri(struct sipe_core_private *sipe_private,
579 struct sip_session *session,
580 SIPE_UNUSED_PARAMETER guint result,
581 SIPE_UNUSED_PARAMETER const gchar *message,
582 const sipe_xml *xml)
584 const sipe_xml *uib = sipe_xml_child(xml, "uib");
585 const gchar *uri = sipe_xml_attribute(uib, "uri");
587 /* drop connection to ocschat@<domain> again */
588 sipe_session_close(sipe_private, session);
590 if (uri) {
591 struct sipe_groupchat *groupchat = sipe_private->groupchat;
593 SIPE_DEBUG_INFO("chatserver_response_uri: '%s'", uri);
595 groupchat->session = session = sipe_session_find_or_add_im(sipe_private,
596 uri);
598 session->is_groupchat = TRUE;
599 sipe_im_invite(sipe_private, session, uri, NULL, NULL, NULL, FALSE);
600 } else {
601 SIPE_DEBUG_WARNING_NOFORMAT("chatserver_response_uri: no server URI found!");
602 groupchat_init_retry(sipe_private);
606 static void chatserver_response_channel_search(struct sipe_core_private *sipe_private,
607 SIPE_UNUSED_PARAMETER struct sip_session *session,
608 guint result,
609 const gchar *message,
610 const sipe_xml *xml)
612 struct sipe_core_public *sipe_public = SIPE_CORE_PUBLIC;
614 if (result != 200) {
615 sipe_backend_notify_error(sipe_public,
616 _("Error retrieving room list"),
617 message);
618 } else {
619 const sipe_xml *chanib;
621 for (chanib = sipe_xml_child(xml, "chanib");
622 chanib;
623 chanib = sipe_xml_twin(chanib)) {
624 const gchar *name = sipe_xml_attribute(chanib, "name");
625 const gchar *desc = sipe_xml_attribute(chanib, "description");
626 const gchar *uri = sipe_xml_attribute(chanib, "uri");
627 const sipe_xml *node;
628 guint user_count = 0;
629 guint32 flags = 0;
631 /* information */
632 for (node = sipe_xml_child(chanib, "info");
633 node;
634 node = sipe_xml_twin(node)) {
635 const gchar *id = sipe_xml_attribute(node, "id");
636 gchar *data;
638 if (!id) continue;
640 data = sipe_xml_data(node);
641 if (data) {
642 if (sipe_strcase_equal(id, "urn:parlano:ma:info:ucnt")) {
643 user_count = g_ascii_strtoll(data, NULL, 10);
644 } else if (sipe_strcase_equal(id, "urn:parlano:ma:info:visibilty")) {
645 if (sipe_strcase_equal(data, "private")) {
646 flags |= SIPE_GROUPCHAT_ROOM_PRIVATE;
649 g_free(data);
653 /* properties */
654 for (node = sipe_xml_child(chanib, "prop");
655 node;
656 node = sipe_xml_twin(node)) {
657 const gchar *id = sipe_xml_attribute(node, "id");
658 gchar *data;
660 if (!id) continue;
662 data = sipe_xml_data(node);
663 if (data) {
664 gboolean value = sipe_strcase_equal(data, "true");
665 g_free(data);
667 if (value) {
668 guint32 add = 0;
669 if (sipe_strcase_equal(id, "urn:parlano:ma:prop:filepost")) {
670 add = SIPE_GROUPCHAT_ROOM_FILEPOST;
671 } else if (sipe_strcase_equal(id, "urn:parlano:ma:prop:invite")) {
672 add = SIPE_GROUPCHAT_ROOM_INVITE;
673 } else if (sipe_strcase_equal(id, "urn:parlano:ma:prop:logged")) {
674 add = SIPE_GROUPCHAT_ROOM_LOGGED;
676 flags |= add;
681 SIPE_DEBUG_INFO("group chat channel '%s': '%s' (%s) with %u users, flags 0x%x",
682 name, desc, uri, user_count, flags);
683 sipe_backend_groupchat_room_add(sipe_public,
684 uri, name, desc,
685 user_count, flags);
689 sipe_backend_groupchat_room_terminate(sipe_public);
692 static gboolean is_chanop(const sipe_xml *aib)
694 return sipe_strequal(sipe_xml_attribute(aib, "key"),
695 GROUPCHAT_AIB_KEY_CHANOP);
698 static void add_user(struct sipe_chat_session *chat_session,
699 const gchar *uri,
700 gboolean new, gboolean chanop)
702 SIPE_DEBUG_INFO("add_user: %s%s%s to room %s (%s)",
703 new ? "new " : "",
704 chanop ? "chanop " : "",
705 uri,
706 chat_session->title, chat_session->id);
707 sipe_backend_chat_add(chat_session->backend, uri, new);
708 if (chanop)
709 sipe_backend_chat_operator(chat_session->backend, uri);
712 static void chatserver_response_join(struct sipe_core_private *sipe_private,
713 SIPE_UNUSED_PARAMETER struct sip_session *session,
714 guint result,
715 const gchar *message,
716 const sipe_xml *xml)
718 if (result != 200) {
719 sipe_backend_notify_error(SIPE_CORE_PUBLIC,
720 _("Error joining chat room"),
721 message);
722 } else {
723 struct sipe_groupchat *groupchat = sipe_private->groupchat;
724 const sipe_xml *node;
725 GHashTable *user_ids = g_hash_table_new(g_str_hash, g_str_equal);
727 /* Extract user IDs & URIs and generate ID -> URI map */
728 for (node = sipe_xml_child(xml, "uib");
729 node;
730 node = sipe_xml_twin(node)) {
731 const gchar *id = sipe_xml_attribute(node, "id");
732 const gchar *uri = sipe_xml_attribute(node, "uri");
733 if (id && uri)
734 g_hash_table_insert(user_ids,
735 (gpointer) id,
736 (gpointer) uri);
739 /* Process channel data */
740 for (node = sipe_xml_child(xml, "chanib");
741 node;
742 node = sipe_xml_twin(node)) {
743 const gchar *uri = sipe_xml_attribute(node, "uri");
745 if (uri) {
746 struct sipe_chat_session *chat_session = g_hash_table_lookup(groupchat->uri_to_chat_session,
747 uri);
748 gboolean new = (chat_session == NULL);
749 const gchar *attr = sipe_xml_attribute(node, "name");
750 gchar *self = sip_uri_self(sipe_private);
751 const sipe_xml *aib;
753 if (new) {
754 chat_session = sipe_chat_create_session(SIPE_CHAT_TYPE_GROUPCHAT,
755 sipe_xml_attribute(node,
756 "uri"),
757 attr ? attr : "");
758 g_hash_table_insert(groupchat->uri_to_chat_session,
759 chat_session->id,
760 chat_session);
762 SIPE_DEBUG_INFO("joined room '%s' (%s)",
763 chat_session->title,
764 chat_session->id);
765 chat_session->backend = sipe_backend_chat_create(SIPE_CORE_PUBLIC,
766 chat_session,
767 chat_session->title,
768 self);
769 } else {
770 SIPE_DEBUG_INFO("rejoining room '%s' (%s)",
771 chat_session->title,
772 chat_session->id);
773 sipe_backend_chat_rejoin(SIPE_CORE_PUBLIC,
774 chat_session->backend,
775 self,
776 chat_session->title);
778 g_free(self);
780 attr = sipe_xml_attribute(node, "topic");
781 if (attr) {
782 sipe_backend_chat_topic(chat_session->backend,
783 attr);
786 /* Process user map for channel */
787 for (aib = sipe_xml_child(node, "aib");
788 aib;
789 aib = sipe_xml_twin(aib)) {
790 const gchar *value = sipe_xml_attribute(aib, "value");
791 gboolean chanop = is_chanop(aib);
792 gchar **ids = g_strsplit(value, ",", 0);
794 if (ids) {
795 gchar **uid = ids;
797 while (*uid) {
798 const gchar *uri = g_hash_table_lookup(user_ids,
799 *uid);
800 if (uri)
801 add_user(chat_session,
802 uri,
803 FALSE,
804 chanop);
805 uid++;
808 g_strfreev(ids);
812 /* Request last 25 entries from channel history */
813 self = g_strdup_printf("<cmd id=\"cmd:bccontext\" seqid=\"1\">"
814 "<data>"
815 "<chanib uri=\"%s\"/>"
816 "<bcq><last cnt=\"25\"/></bcq>"
817 "</data>"
818 "</cmd>", chat_session->id);
819 chatserver_command(sipe_private, self);
820 g_free(self);
824 g_hash_table_destroy(user_ids);
828 static void chatserver_grpchat_message(struct sipe_core_private *sipe_private,
829 const sipe_xml *grpchat);
831 static void chatserver_response_history(SIPE_UNUSED_PARAMETER struct sipe_core_private *sipe_private,
832 SIPE_UNUSED_PARAMETER struct sip_session *session,
833 SIPE_UNUSED_PARAMETER guint result,
834 SIPE_UNUSED_PARAMETER const gchar *message,
835 const sipe_xml *xml)
837 const sipe_xml *grpchat;
839 for (grpchat = sipe_xml_child(xml, "chanib/msg");
840 grpchat;
841 grpchat = sipe_xml_twin(grpchat))
842 if (sipe_strequal(sipe_xml_attribute(grpchat, "id"),
843 "grpchat"))
844 chatserver_grpchat_message(sipe_private, grpchat);
847 static void chatserver_response_part(struct sipe_core_private *sipe_private,
848 SIPE_UNUSED_PARAMETER struct sip_session *session,
849 guint result,
850 const gchar *message,
851 const sipe_xml *xml)
853 if (result != 200) {
854 SIPE_DEBUG_WARNING("chatserver_response_part: failed with %d: %s. Dropping room",
855 result, message);
856 } else {
857 struct sipe_groupchat *groupchat = sipe_private->groupchat;
858 const gchar *uri = sipe_xml_attribute(sipe_xml_child(xml, "chanib"),
859 "uri");
860 struct sipe_chat_session *chat_session;
862 if (uri &&
863 (chat_session = g_hash_table_lookup(groupchat->uri_to_chat_session,
864 uri))) {
866 SIPE_DEBUG_INFO("leaving room '%s' (%s)",
867 chat_session->title, chat_session->id);
869 g_hash_table_remove(groupchat->uri_to_chat_session,
870 uri);
871 sipe_chat_remove_session(chat_session);
873 } else {
874 SIPE_DEBUG_WARNING("chatserver_response_part: unknown chat room uri '%s'",
875 uri ? uri : "");
880 static void chatserver_notice_join(struct sipe_core_private *sipe_private,
881 SIPE_UNUSED_PARAMETER struct sip_session *session,
882 SIPE_UNUSED_PARAMETER guint result,
883 SIPE_UNUSED_PARAMETER const gchar *message,
884 const sipe_xml *xml)
886 struct sipe_groupchat *groupchat = sipe_private->groupchat;
887 const sipe_xml *uib;
889 for (uib = sipe_xml_child(xml, "uib");
890 uib;
891 uib = sipe_xml_twin(uib)) {
892 const gchar *uri = sipe_xml_attribute(uib, "uri");
894 if (uri) {
895 const sipe_xml *aib;
897 for (aib = sipe_xml_child(uib, "aib");
898 aib;
899 aib = sipe_xml_twin(aib)) {
900 const gchar *domain = sipe_xml_attribute(aib, "domain");
901 const gchar *path = sipe_xml_attribute(aib, "value");
903 if (domain && path) {
904 gchar *room_uri = g_strdup_printf("ma-chan://%s/%s",
905 domain, path);
906 struct sipe_chat_session *chat_session = g_hash_table_lookup(groupchat->uri_to_chat_session,
907 room_uri);
908 if (chat_session)
909 add_user(chat_session,
910 uri,
911 TRUE,
912 is_chanop(aib));
914 g_free(room_uri);
921 static void chatserver_notice_part(struct sipe_core_private *sipe_private,
922 SIPE_UNUSED_PARAMETER struct sip_session *session,
923 SIPE_UNUSED_PARAMETER guint result,
924 SIPE_UNUSED_PARAMETER const gchar *message,
925 const sipe_xml *xml)
927 struct sipe_groupchat *groupchat = sipe_private->groupchat;
928 const sipe_xml *chanib;
930 for (chanib = sipe_xml_child(xml, "chanib");
931 chanib;
932 chanib = sipe_xml_twin(chanib)) {
933 const gchar *room_uri = sipe_xml_attribute(chanib, "uri");
935 if (room_uri) {
936 struct sipe_chat_session *chat_session = g_hash_table_lookup(groupchat->uri_to_chat_session,
937 room_uri);
939 if (chat_session) {
940 const sipe_xml *uib;
942 for (uib = sipe_xml_child(chanib, "uib");
943 uib;
944 uib = sipe_xml_twin(uib)) {
945 const gchar *uri = sipe_xml_attribute(uib, "uri");
947 if (uri) {
948 SIPE_DEBUG_INFO("remove_user: %s from room %s (%s)",
949 uri,
950 chat_session->title,
951 chat_session->id);
952 sipe_backend_chat_remove(chat_session->backend,
953 uri);
961 static const struct response {
962 const gchar *key;
963 void (* const handler)(struct sipe_core_private *,
964 struct sip_session *,
965 guint result, const gchar *,
966 const sipe_xml *xml);
967 } response_table[] = {
968 { "rpl:requri", chatserver_response_uri },
969 { "rpl:chansrch", chatserver_response_channel_search },
970 { "rpl:join", chatserver_response_join },
971 { "rpl:bjoin", chatserver_response_join },
972 { "rpl:bccontext", chatserver_response_history },
973 { "rpl:part", chatserver_response_part },
974 { "ntc:join", chatserver_notice_join },
975 { "ntc:bjoin", chatserver_notice_join },
976 { "ntc:part", chatserver_notice_part },
977 { NULL, NULL }
980 /* Handles rpl:XXX & ntc:YYY */
981 static void chatserver_response(struct sipe_core_private *sipe_private,
982 const sipe_xml *reply,
983 struct sip_session *session)
985 do {
986 const sipe_xml *resp, *data;
987 const gchar *id;
988 gchar *message;
989 guint result = 500;
990 const struct response *r;
992 id = sipe_xml_attribute(reply, "id");
993 if (!id) {
994 SIPE_DEBUG_INFO_NOFORMAT("chatserver_response: no reply ID found!");
995 continue;
998 resp = sipe_xml_child(reply, "resp");
999 if (resp) {
1000 result = sipe_xml_int_attribute(resp, "code", 500);
1001 message = sipe_xml_data(resp);
1002 } else {
1003 message = g_strdup("");
1006 data = sipe_xml_child(reply, "data");
1008 SIPE_DEBUG_INFO("chatserver_response: '%s' result (%d) %s",
1009 id, result, message ? message : "");
1011 for (r = response_table; r->key; r++) {
1012 if (sipe_strcase_equal(id, r->key)) {
1013 (*r->handler)(sipe_private, session, result, message, data);
1014 /* session can be invalid now */
1015 session = NULL;
1016 break;
1019 if (!r->key) {
1020 SIPE_DEBUG_INFO_NOFORMAT("chatserver_response: ignoring unknown response");
1023 g_free(message);
1024 } while ((reply = sipe_xml_twin(reply)) != NULL);
1027 static void chatserver_grpchat_message(struct sipe_core_private *sipe_private,
1028 const sipe_xml *grpchat)
1030 struct sipe_groupchat *groupchat = sipe_private->groupchat;
1031 const gchar *uri = sipe_xml_attribute(grpchat, "chanUri");
1032 const gchar *from = sipe_xml_attribute(grpchat, "author");
1033 time_t when = sipe_utils_str_to_time(sipe_xml_attribute(grpchat, "ts"));
1034 gchar *text = sipe_xml_data(sipe_xml_child(grpchat, "chat"));
1035 struct sipe_chat_session *chat_session;
1036 gchar *escaped;
1038 if (!uri || !from) {
1039 SIPE_DEBUG_INFO("chatserver_grpchat_message: message '%s' received without chat room URI or author!",
1040 text ? text : "");
1041 g_free(text);
1042 return;
1045 chat_session = g_hash_table_lookup(groupchat->uri_to_chat_session,
1046 uri);
1047 if (!chat_session) {
1048 SIPE_DEBUG_INFO("chatserver_grpchat_message: message '%s' from '%s' received from unknown chat room '%s'!",
1049 text ? text : "", from, uri);
1050 g_free(text);
1051 return;
1054 /* libxml2 decodes all entities, but the backend expects HTML */
1055 escaped = g_markup_escape_text(text, -1);
1056 g_free(text);
1057 sipe_backend_chat_message(SIPE_CORE_PUBLIC, chat_session->backend,
1058 from, when, escaped);
1059 g_free(escaped);
1062 void process_incoming_info_groupchat(struct sipe_core_private *sipe_private,
1063 struct sipmsg *msg,
1064 struct sip_session *session)
1066 sipe_xml *xml = sipe_xml_parse(msg->body, msg->bodylen);
1067 const sipe_xml *node;
1068 const gchar *callid;
1069 struct sip_dialog *dialog;
1071 callid = sipmsg_find_call_id_header(msg);
1072 dialog = sipe_dialog_find(session, session->with);
1073 if (sipe_strequal(callid, dialog->callid)) {
1075 sip_transport_response(sipe_private, msg, 200, "OK", NULL);
1077 if (((node = sipe_xml_child(xml, "rpl")) != NULL) ||
1078 ((node = sipe_xml_child(xml, "ntc")) != NULL)) {
1079 chatserver_response(sipe_private, node, session);
1080 } else if ((node = sipe_xml_child(xml, "grpchat")) != NULL) {
1081 chatserver_grpchat_message(sipe_private, node);
1082 } else {
1083 SIPE_DEBUG_INFO_NOFORMAT("process_incoming_info_groupchat: ignoring unknown response");
1086 } else {
1088 * Our last session got disconnected without proper shutdown,
1089 * e.g. by Pidgin crashing or network connection loss. When
1090 * we reconnect to the group chat the server will send INFO
1091 * messages to the current *AND* the obsolete Call-ID, until
1092 * the obsolete session expires.
1094 * Ignore these INFO messages to avoid, e.g. duplicate texts,
1095 * and respond with an error so that the server knows that we
1096 * consider this dialog to be terminated.
1098 SIPE_DEBUG_INFO("process_incoming_info_groupchat: ignoring unsolicited INFO message to obsolete Call-ID: %s",
1099 callid);
1101 sip_transport_response(sipe_private, msg, 487, "Request Terminated", NULL);
1104 sipe_xml_free(xml);
1107 void sipe_groupchat_send(struct sipe_core_private *sipe_private,
1108 struct sipe_chat_session *chat_session,
1109 const gchar *what)
1111 struct sipe_groupchat *groupchat = sipe_private->groupchat;
1112 gchar *cmd, *self, *timestamp, *tmp;
1113 gchar **lines, **strvp;
1114 struct sipe_groupchat_msg *msg;
1116 if (!groupchat || !chat_session)
1117 return;
1119 SIPE_DEBUG_INFO("sipe_groupchat_send: '%s' to %s",
1120 what, chat_session->id);
1122 self = sip_uri_self(sipe_private);
1123 timestamp = sipe_utils_time_to_str(time(NULL));
1126 * 'what' is already XML-escaped, e.g.
1128 * " -> &quot;
1129 * > -> &gt;
1130 * < -> &lt;
1131 * & -> &amp;
1133 * Group Chat only accepts plain text, not full HTML. So we have to
1134 * strip all HTML tags and XML escape the text.
1136 * Line breaks are encoded as <br> and therefore need to be replaced
1137 * before stripping. In order to prevent HTML stripping to strip line
1138 * endings, we need to split the text into lines on <br>.
1140 lines = g_strsplit(what, "<br>", 0);
1141 for (strvp = lines; *strvp; strvp++) {
1142 /* replace array entry with HTML stripped & XML escaped version */
1143 gchar *stripped = sipe_backend_markup_strip_html(*strvp);
1144 gchar *escaped = g_markup_escape_text(stripped, -1);
1145 g_free(stripped);
1146 g_free(*strvp);
1147 *strvp = escaped;
1149 tmp = g_strjoinv("\r\n", lines);
1150 g_strfreev(lines);
1151 cmd = g_strdup_printf("<grpchat id=\"grpchat\" seqid=\"1\" chanUri=\"%s\" author=\"%s\" ts=\"%s\">"
1152 "<chat>%s</chat>"
1153 "</grpchat>",
1154 chat_session->id, self, timestamp, tmp);
1155 g_free(tmp);
1156 g_free(timestamp);
1157 g_free(self);
1158 msg = chatserver_command(sipe_private, cmd);
1159 g_free(cmd);
1161 if (msg) {
1162 msg->session = chat_session;
1163 msg->content = g_strdup(what);
1164 } else {
1165 chatserver_command_error_notify(sipe_private,
1166 chat_session,
1167 what);
1171 void sipe_groupchat_leave(struct sipe_core_private *sipe_private,
1172 struct sipe_chat_session *chat_session)
1174 struct sipe_groupchat *groupchat = sipe_private->groupchat;
1175 gchar *cmd;
1177 if (!groupchat || !chat_session)
1178 return;
1180 SIPE_DEBUG_INFO("sipe_groupchat_leave: %s", chat_session->id);
1182 cmd = g_strdup_printf("<cmd id=\"cmd:part\" seqid=\"1\">"
1183 "<data>"
1184 "<chanib uri=\"%s\"/>"
1185 "</data>"
1186 "</cmd>", chat_session->id);
1187 chatserver_command(sipe_private, cmd);
1188 g_free(cmd);
1191 gboolean sipe_core_groupchat_query_rooms(struct sipe_core_public *sipe_public)
1193 struct sipe_core_private *sipe_private = SIPE_CORE_PRIVATE;
1194 struct sipe_groupchat *groupchat = sipe_private->groupchat;
1196 if (!groupchat || !groupchat->connected)
1197 return FALSE;
1199 chatserver_command(sipe_private,
1200 "<cmd id=\"cmd:chansrch\" seqid=\"1\">"
1201 "<data>"
1202 "<qib qtype=\"BYNAME\" criteria=\"\" extended=\"false\"/>"
1203 "</data>"
1204 "</cmd>");
1206 return TRUE;
1209 void sipe_core_groupchat_join(struct sipe_core_public *sipe_public,
1210 const gchar *uri)
1212 struct sipe_core_private *sipe_private = SIPE_CORE_PRIVATE;
1213 struct sipe_groupchat *groupchat = sipe_private->groupchat;
1215 if (!g_str_has_prefix(uri, "ma-chan://"))
1216 return;
1218 if (!groupchat) {
1219 /* This happens when a user has set auto-join on a channel */
1220 sipe_groupchat_allocate(sipe_private);
1221 groupchat = sipe_private->groupchat;
1224 if (groupchat->connected) {
1225 struct sipe_chat_session *chat_session = g_hash_table_lookup(groupchat->uri_to_chat_session,
1226 uri);
1228 /* Already joined? */
1229 if (chat_session) {
1231 /* Yes, update backend session */
1232 SIPE_DEBUG_INFO("sipe_core_groupchat_join: show '%s' (%s)",
1233 chat_session->title,
1234 chat_session->id);
1235 sipe_backend_chat_show(chat_session->backend);
1237 } else {
1238 /* No, send command out directly */
1239 gchar *chanid = generate_chanid_node(uri, 0);
1240 if (chanid) {
1241 gchar *cmd = g_strdup_printf("<cmd id=\"cmd:join\" seqid=\"1\">"
1242 "<data>%s</data>"
1243 "</cmd>",
1244 chanid);
1245 SIPE_DEBUG_INFO("sipe_core_groupchat_join: join %s",
1246 uri);
1247 chatserver_command(sipe_private, cmd);
1248 g_free(cmd);
1249 g_free(chanid);
1252 } else {
1253 /* Add it to the queue but avoid duplicates */
1254 if (!g_slist_find_custom(groupchat->join_queue, uri,
1255 (GCompareFunc)g_strcmp0)) {
1256 SIPE_DEBUG_INFO_NOFORMAT("sipe_core_groupchat_join: URI queued");
1257 groupchat->join_queue = g_slist_prepend(groupchat->join_queue,
1258 g_strdup(uri));
1263 void sipe_groupchat_rejoin(struct sipe_core_private *sipe_private,
1264 struct sipe_chat_session *chat_session)
1266 struct sipe_groupchat *groupchat = sipe_private->groupchat;
1268 if (!groupchat) {
1269 /* First rejoined channel after reconnect will trigger this */
1270 sipe_groupchat_allocate(sipe_private);
1271 groupchat = sipe_private->groupchat;
1274 /* Remember "old" session, so that we don't recreate it at join */
1275 g_hash_table_insert(groupchat->uri_to_chat_session,
1276 chat_session->id,
1277 chat_session);
1278 sipe_core_groupchat_join(SIPE_CORE_PUBLIC, chat_session->id);
1282 Local Variables:
1283 mode: c
1284 c-file-style: "bsd"
1285 indent-tabs-mode: t
1286 tab-width: 8
1287 End: