/**
* @file sipe-groupchat.c
*
* pidgin-sipe
*
* Copyright (C) 2010-2017 SIPE Project <http://sipe.sourceforge.net/>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/**
* This module implements the OCS2007R2 Group Chat functionality
*
* Documentation references:
*
* Microsoft TechNet: Key Protocols and Windows Services Used by Group Chat
* <http://technet.microsoft.com/en-us/library/ee323484%28office.13%29.aspx>
* Microsoft TechNet: Group Chat Call Flows
* <http://technet.microsoft.com/en-us/library/ee323524%28office.13%29.aspx>
* Microsoft Office Communications Server 2007 R2 Technical Reference Guide
* <http://go.microsoft.com/fwlink/?LinkID=159649>
* Microsoft DevNet: [MS-XCCOSIP] Extensible Chat Control Over SIP
* <http://msdn.microsoft.com/en-us/library/hh624112.aspx>
* RFC 4028: Session Timers in the Session Initiation Protocol (SIP)
* <http://www.rfc-editor.org/rfc/rfc4028.txt>
*
*
* @TODO:
*
* -.cmd:getserverinfo
* <sib domain="<DOMAIN>" infoType="123" />
* rpl:getservinfo
* <sib infoType="123"
* serverTime="2010-09-14T14:26:17.6206356Z"
* searchLimit="999"
* messageSizeLimit="512"
* storySizeLimit="4096"
* rootUri="ma-cat://<DOMAIN>/<GUID>"
* dbVersion="3ea3a5a8-ef36-46cf-898f-7a5133931d63"
* />
*
* is there any information in there we would need/use?
*
* - cmd:getpref/rpl:getpref/cmd:setpref/rpl:setpref
* probably useless, as libpurple stores configuration locally
*
* can store base64 encoded "free text" in key/value fashion
* <cmd id="cmd:getpref" seqid="x">
* <data>
* <pref label="kedzie.GroupChannels"
* seqid="71"
* createdefault="true" />
* </data>
* </cmd>
* <cmd id="cmd:setpref" seqid="x">
* <data>
* <pref label="kedzie.GroupChannels"
* seqid="71"
* createdefault="false"
* content="<BASE64 text>" />
* </data>
* </cmd>
*
* use this to sync chats in buddy list on multiple clients?
*
* - cmd:getinv
* <inv inviteId="1" domain="<DOMAIN>" />
* rpl:getinv
* ???
*
* according to documentation should provide list of outstanding invites.
* [no log file examples]
* should we automatically join those channels or ask user to join/add?
*
* - chatserver_command_message()
* needs to support multiple <grpchat> nodes?
* [no log file examples]
*
* - create/delete chat rooms
* [no log file examples]
* are these related to this functionality?
*
* <cmd id="cmd:nodespermcreatechild" seqid="1">
* <data />
* </cmd>
* <rpl id="rpl:nodespermcreatechild" seqid="1">
* <commandid seqid="1" envid="xxx" />
* <resp code="200">SUCCESS_OK</resp>
* <data />
* </rpl>
*
* - file transfer (uses HTTPS PUT/GET via a filestore server)
* [no log file examples]
*
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <stdlib.h>
#include <string.h>
#include <glib.h>
#include "sipe-common.h"
#include "sipmsg.h"
#include "sip-transport.h"
#include "sipe-backend.h"
#include "sipe-chat.h"
#include "sipe-core.h"
#include "sipe-core-private.h"
#include "sipe-dialog.h"
#include "sipe-groupchat.h"
#include "sipe-im.h"
#include "sipe-nls.h"
#include "sipe-schedule.h"
#include "sipe-session.h"
#include "sipe-utils.h"
#include "sipe-xml.h"
#define GROUPCHAT_RETRY_TIMEOUT 5*60 /* seconds */
/**
* aib node - magic numbers?
*
* Example:
* <aib key="3984" value="0,1,2,3,4,5,7,9,10,12,13,14,15,16,17" />
* <aib key="12276" value="6,8,11" />
*
* "value" corresponds to the "id" attribute in uib nodes.
*
* @TODO: Confirm "guessed" meaning of the magic numbers:
* 3984 = normal users
* 12276 = channel operators
*/
#define GROUPCHAT_AIB_KEY_USER "3984"
#define GROUPCHAT_AIB_KEY_CHANOP "12276"
struct sipe_groupchat {
struct sip_session *session;
gchar *domain;
GSList *join_queue;
GHashTable *uri_to_chat_session;
GHashTable *msgs;
guint envid;
guint expires;
gboolean connected;
};
struct sipe_groupchat_msg {
GHashTable *container;
struct sipe_chat_session *session;
gchar *content;
gchar *xccos;
guint envid;
};
/* GDestroyNotify */
static void sipe_groupchat_msg_free(gpointer data) {
struct sipe_groupchat_msg *msg = data;
g_free(msg->content);
g_free(msg->xccos);
g_free(msg);
}
/* GDestroyNotify */
static void sipe_groupchat_msg_remove(gpointer data) {
struct sipe_groupchat_msg *msg = data;
g_hash_table_remove(msg->container, &msg->envid);
}
static void sipe_groupchat_allocate(struct sipe_core_private *sipe_private)
{
struct sipe_groupchat *groupchat = g_new0(struct sipe_groupchat, 1);
groupchat->uri_to_chat_session = g_hash_table_new(g_str_hash, g_str_equal);
groupchat->msgs = g_hash_table_new_full(g_int_hash, g_int_equal,
NULL,
sipe_groupchat_msg_free);
groupchat->envid = rand();
groupchat->connected = FALSE;
sipe_private->groupchat = groupchat;
}
static void sipe_groupchat_free_join_queue(struct sipe_groupchat *groupchat)
{
sipe_utils_slist_free_full(groupchat->join_queue, g_free);
groupchat->join_queue = NULL;
}
void sipe_groupchat_free(struct sipe_core_private *sipe_private)
{
struct sipe_groupchat *groupchat = sipe_private->groupchat;
if (groupchat) {
sipe_groupchat_free_join_queue(groupchat);
g_hash_table_destroy(groupchat->msgs);
g_hash_table_destroy(groupchat->uri_to_chat_session);
g_free(groupchat->domain);
g_free(groupchat);
sipe_private->groupchat = NULL;
}
}
static struct sipe_groupchat_msg *generate_xccos_message(struct sipe_groupchat *groupchat,
const gchar *content)
{
struct sipe_groupchat_msg *msg = g_new0(struct sipe_groupchat_msg, 1);
msg->container = groupchat->msgs;
msg->envid = groupchat->envid++;
msg->xccos = g_strdup_printf("<xccos ver=\"1\" envid=\"%u\" xmlns=\"urn:parlano:xml:ns:xccos\">"
"%s"
"</xccos>",
msg->envid,
content);
g_hash_table_insert(groupchat->msgs, &msg->envid, msg);
return(msg);
}
/**
* Create short-lived dialog with ocschat@<domain> (or user specified value)
* This initiates the Group Chat feature
*/
void sipe_groupchat_init(struct sipe_core_private *sipe_private)
{
const gchar *setting = sipe_backend_setting(SIPE_CORE_PUBLIC,
SIPE_SETTING_GROUPCHAT_USER);
const gchar *persistent = sipe_private->persistentChatPool_uri;
gboolean user_set = !is_empty(setting);
gboolean provisioned = !is_empty(persistent);
gchar **parts = g_strsplit(user_set ? setting :
provisioned ? persistent :
sipe_private->username, "@", 2);
gboolean domain_found = !is_empty(parts[1]);
const gchar *user = "ocschat";
const gchar *domain = parts[domain_found ? 1 : 0];
gchar *chat_uri;
struct sip_session *session;
struct sipe_groupchat *groupchat;
/* User specified or provisioned URI is valid 'user@company.com' */
if ((user_set || provisioned) && domain_found && !is_empty(parts[0]))
user = parts[0];
SIPE_DEBUG_INFO("sipe_groupchat_init: username '%s' setting '%s' persistent '%s' split '%s'/'%s' GC user %s@%s",
sipe_private->username, setting ? setting : "(null)",
persistent ? persistent : "(null)",
parts[0], parts[1] ? parts[1] : "(null)", user, domain);
if (!sipe_private->groupchat)
sipe_groupchat_allocate(sipe_private);
groupchat = sipe_private->groupchat;
chat_uri = g_strdup_printf("sip:%s@%s", user, domain);
session = sipe_session_find_or_add_im(sipe_private,
chat_uri);
session->is_groupchat = TRUE;
sipe_im_invite(sipe_private, session, chat_uri,
NULL, NULL, NULL, FALSE);
g_free(groupchat->domain);
groupchat->domain = g_strdup(domain);
g_free(chat_uri);
g_strfreev(parts);
}
/* sipe_schedule_action */
static void groupchat_init_retry_cb(struct sipe_core_private *sipe_private,
SIPE_UNUSED_PARAMETER gpointer data)
{
sipe_groupchat_init(sipe_private);
}
static void groupchat_init_retry(struct sipe_core_private *sipe_private)
{
struct sipe_groupchat *groupchat = sipe_private->groupchat;
SIPE_DEBUG_INFO_NOFORMAT("groupchat_init_retry: trying again later...");
groupchat->session = NULL;
groupchat->connected = FALSE;
sipe_schedule_seconds(sipe_private,
"<+groupchat-retry>",
NULL,
GROUPCHAT_RETRY_TIMEOUT,
groupchat_init_retry_cb,
NULL);
}
void sipe_groupchat_invite_failed(struct sipe_core_private *sipe_private,
struct sip_session *session)
{
struct sipe_groupchat *groupchat = sipe_private->groupchat;
const gchar *setting = sipe_backend_setting(SIPE_CORE_PUBLIC,
SIPE_SETTING_GROUPCHAT_USER);
gboolean retry = FALSE;
if (groupchat->session) {
/* response to group chat server invite */
SIPE_DEBUG_ERROR_NOFORMAT("can't connect to group chat server!");
/* group chat server exists, but communication failed */
retry = TRUE;
} else {
/* response to initial invite */
SIPE_DEBUG_INFO_NOFORMAT("no group chat server found.");
}
sipe_session_close(sipe_private, session);
if (!is_empty(setting)) {
gchar *msg = g_strdup_printf(_("Group Chat Proxy setting is incorrect:\n\n\t%s\n\nPlease update your Account."),
setting);
sipe_backend_notify_error(SIPE_CORE_PUBLIC,
_("Couldn't find Group Chat server!"),
msg);
g_free(msg);
/* user specified group chat settings: we should retry */
retry = TRUE;
}
if (retry) {
groupchat_init_retry(sipe_private);
} else {
SIPE_DEBUG_INFO_NOFORMAT("disabling group chat feature.");
}
}
static gchar *generate_chanid_node(const gchar *uri, guint key)
{
/* ma-chan://<domain>/<value> */
gchar **parts = g_strsplit(uri, "/", 4);
gchar *chanid = NULL;
if (parts[2] && parts[3]) {
chanid = g_strdup_printf("<chanid key=\"%d\" domain=\"%s\" value=\"%s\"/>",
key, parts[2], parts[3]);
} else {
SIPE_DEBUG_ERROR("generate_chanid_node: mal-formed URI '%s'",
uri);
}
g_strfreev(parts);
return chanid;
}
/* TransCallback */
static void groupchat_update_cb(struct sipe_core_private *sipe_private,
gpointer data);
static gboolean groupchat_expired_session_response(struct sipe_core_private *sipe_private,
struct sipmsg *msg,
SIPE_UNUSED_PARAMETER struct transaction *trans)
{
struct sipe_groupchat *groupchat = sipe_private->groupchat;
/* 481 Call Leg Does Not Exist -> server dropped session */
if (msg->response == 481) {
struct sip_session *session = groupchat->session;
struct sip_dialog *dialog = sipe_dialog_find(session,
session->with);
if (dialog) {
/* close dialog from our side */
sip_transport_bye(sipe_private, dialog);
sipe_dialog_remove(session, session->with);
/* dialog is no longer valid */
}
/* re-initialize groupchat session */
groupchat->session = NULL;
groupchat->connected = FALSE;
sipe_groupchat_init(sipe_private);
} else {
sipe_schedule_seconds(sipe_private,
"<+groupchat-expires>",
NULL,
groupchat->expires,
groupchat_update_cb,
NULL);
}
return(TRUE);
}
/* sipe_schedule_action */
static void groupchat_update_cb(struct sipe_core_private *sipe_private,
SIPE_UNUSED_PARAMETER gpointer data)
{
struct sipe_groupchat *groupchat = sipe_private->groupchat;
if (groupchat->session) {
struct sip_dialog *dialog = sipe_dialog_find(groupchat->session,
groupchat->session->with);
if (dialog)
sip_transport_update(sipe_private,
dialog,
groupchat_expired_session_response);
}
}
static struct sipe_groupchat_msg *chatserver_command(struct sipe_core_private *sipe_private,
const gchar *cmd);
void sipe_groupchat_invite_response(struct sipe_core_private *sipe_private,
struct sip_dialog *dialog,
struct sipmsg *response)
{
struct sipe_groupchat *groupchat = sipe_private->groupchat;
SIPE_DEBUG_INFO_NOFORMAT("sipe_groupchat_invite_response");
if (!groupchat->session) {
/* response to initial invite */
struct sipe_groupchat_msg *msg = generate_xccos_message(groupchat,
"<cmd id=\"cmd:requri\" seqid=\"1\"><data/></cmd>");
const gchar *session_expires = sipmsg_find_header(response,
"Session-Expires");
sip_transport_info(sipe_private,
"Content-Type: text/plain\r\n",
msg->xccos,
dialog,
NULL);
sipe_groupchat_msg_remove(msg);
if (session_expires) {
groupchat->expires = strtoul(session_expires, NULL, 10);
if (groupchat->expires) {
SIPE_DEBUG_INFO("sipe_groupchat_invite_response: session expires in %d seconds",
groupchat->expires);
if (groupchat->expires > 10)
groupchat->expires -= 10;
sipe_schedule_seconds(sipe_private,
"<+groupchat-expires>",
NULL,
groupchat->expires,
groupchat_update_cb,
NULL);
}
}
} else {
/* response to group chat server invite */
gchar *invcmd;
SIPE_DEBUG_INFO_NOFORMAT("connection to group chat server established.");
groupchat->connected = TRUE;
/* Any queued joins? */
if (groupchat->join_queue) {
GString *cmd = g_string_new("<cmd id=\"cmd:bjoin\" seqid=\"1\">"
"<data>");
GSList *entry;
guint i = 0;
/* We used g_slist_prepend() to create the list */
groupchat->join_queue = entry = g_slist_reverse(groupchat->join_queue);
while (entry) {
gchar *chanid = generate_chanid_node(entry->data, i++);
g_string_append(cmd, chanid);
g_free(chanid);
entry = entry->next;
}
sipe_groupchat_free_join_queue(groupchat);
g_string_append(cmd, "</data></cmd>");
chatserver_command(sipe_private, cmd->str);
g_string_free(cmd, TRUE);
}
/* Request outstanding invites from server */
invcmd = g_strdup_printf("<cmd id=\"cmd:getinv\" seqid=\"1\">"
"<data>"
"<inv inviteId=\"1\" domain=\"%s\"/>"
"</data>"
"</cmd>", groupchat->domain);
chatserver_command(sipe_private, invcmd);
g_free(invcmd);
}
}
static void chatserver_command_error_notify(struct sipe_core_private *sipe_private,
struct sipe_chat_session *chat_session,
const gchar *content)
{
gchar *label = g_strdup_printf(_("This message was not delivered to chat room '%s'"),
chat_session->title);
gchar *errmsg = g_strdup_printf("%s:\n<font color=\"#888888\"></b>%s<b></font>",
label, content);
g_free(label);
sipe_backend_notify_message_error(SIPE_CORE_PUBLIC,
chat_session->backend,
NULL,
errmsg);
g_free(errmsg);
}
/* TransCallback */
static gboolean chatserver_command_response(struct sipe_core_private *sipe_private,
struct sipmsg *msg,
struct transaction *trans)
{
if (msg->response != 200) {
struct sipe_groupchat_msg *gmsg = trans->payload->data;
struct sipe_chat_session *chat_session = gmsg->session;
SIPE_DEBUG_INFO("chatserver_command_response: failure %d", msg->response);
if (chat_session)
chatserver_command_error_notify(sipe_private,
chat_session,
gmsg->content);
groupchat_expired_session_response(sipe_private, msg, trans);
}
return TRUE;
}
static struct sipe_groupchat_msg *chatserver_command(struct sipe_core_private *sipe_private,
const gchar *cmd)
{
struct sipe_groupchat *groupchat = sipe_private->groupchat;
struct sipe_groupchat_msg *msg = NULL;
if (groupchat->session) {
struct sip_dialog *dialog = sipe_dialog_find(groupchat->session,
groupchat->session->with);
if (dialog) {
struct transaction *trans;
msg = generate_xccos_message(groupchat, cmd);
trans = sip_transport_info(sipe_private,
"Content-Type: text/plain\r\n",
msg->xccos,
dialog,
chatserver_command_response);
if (trans) {
struct transaction_payload *payload = g_new0(struct transaction_payload, 1);
payload->destroy = sipe_groupchat_msg_remove;
payload->data = msg;
trans->payload = payload;
} else {
/* SIP transport is no longer valid - give up */
sipe_groupchat_msg_remove(msg);
msg = NULL;
}
}
}
return(msg);
}
static void chatserver_response_uri(struct sipe_core_private *sipe_private,
struct sip_session *session,
SIPE_UNUSED_PARAMETER guint result,
SIPE_UNUSED_PARAMETER const gchar *message,
const sipe_xml *xml)
{
const sipe_xml *uib = sipe_xml_child(xml, "uib");
const gchar *uri = sipe_xml_attribute(uib, "uri");
/* drop connection to ocschat@<domain> again */
sipe_session_close(sipe_private, session);
if (uri) {
struct sipe_groupchat *groupchat = sipe_private->groupchat;
SIPE_DEBUG_INFO("chatserver_response_uri: '%s'", uri);
groupchat->session = session = sipe_session_find_or_add_im(sipe_private,
uri);
session->is_groupchat = TRUE;
sipe_im_invite(sipe_private, session, uri, NULL, NULL, NULL, FALSE);
} else {
SIPE_DEBUG_WARNING_NOFORMAT("chatserver_response_uri: no server URI found!");
groupchat_init_retry(sipe_private);
}
}
static void chatserver_response_channel_search(struct sipe_core_private *sipe_private,
SIPE_UNUSED_PARAMETER struct sip_session *session,
guint result,
const gchar *message,
const sipe_xml *xml)
{
struct sipe_core_public *sipe_public = SIPE_CORE_PUBLIC;
if (result != 200) {
sipe_backend_notify_error(sipe_public,
_("Error retrieving room list"),
message);
} else {
const sipe_xml *chanib;
for (chanib = sipe_xml_child(xml, "chanib");
chanib;
chanib = sipe_xml_twin(chanib)) {
const gchar *name = sipe_xml_attribute(chanib, "name");
const gchar *desc = sipe_xml_attribute(chanib, "description");
const gchar *uri = sipe_xml_attribute(chanib, "uri");
const sipe_xml *node;
guint user_count = 0;
guint32 flags = 0;
/* information */
for (node = sipe_xml_child(chanib, "info");
node;
node = sipe_xml_twin(node)) {
const gchar *id = sipe_xml_attribute(node, "id");
gchar *data;
if (!id) continue;
data = sipe_xml_data(node);
if (data) {
if (sipe_strcase_equal(id, "urn:parlano:ma:info:ucnt")) {
user_count = g_ascii_strtoll(data, NULL, 10);
} else if (sipe_strcase_equal(id, "urn:parlano:ma:info:visibilty")) {
if (sipe_strcase_equal(data, "private")) {
flags |= SIPE_GROUPCHAT_ROOM_PRIVATE;
}
}
g_free(data);
}
}
/* properties */
for (node = sipe_xml_child(chanib, "prop");
node;
node = sipe_xml_twin(node)) {
const gchar *id = sipe_xml_attribute(node, "id");
gchar *data;
if (!id) continue;
data = sipe_xml_data(node);
if (data) {
gboolean value = sipe_strcase_equal(data, "true");
g_free(data);
if (value) {
guint32 add = 0;
if (sipe_strcase_equal(id, "urn:parlano:ma:prop:filepost")) {
add = SIPE_GROUPCHAT_ROOM_FILEPOST;
} else if (sipe_strcase_equal(id, "urn:parlano:ma:prop:invite")) {
add = SIPE_GROUPCHAT_ROOM_INVITE;
} else if (sipe_strcase_equal(id, "urn:parlano:ma:prop:logged")) {
add = SIPE_GROUPCHAT_ROOM_LOGGED;
}
flags |= add;
}
}
}
SIPE_DEBUG_INFO("group chat channel '%s': '%s' (%s) with %u users, flags 0x%x",
name, desc, uri, user_count, flags);
sipe_backend_groupchat_room_add(sipe_public,
uri, name, desc,
user_count, flags);
}
}
sipe_backend_groupchat_room_terminate(sipe_public);
}
static gboolean is_chanop(const sipe_xml *aib)
{
return sipe_strequal(sipe_xml_attribute(aib, "key"),
GROUPCHAT_AIB_KEY_CHANOP);
}
static void add_user(struct sipe_chat_session *chat_session,
const gchar *uri,
gboolean new, gboolean chanop)
{
SIPE_DEBUG_INFO("add_user: %s%s%s to room %s (%s)",
new ? "new " : "",
chanop ? "chanop " : "",
uri,
chat_session->title, chat_session->id);
sipe_backend_chat_add(chat_session->backend, uri, new);
if (chanop)
sipe_backend_chat_operator(chat_session->backend, uri);
}
static void chatserver_response_join(struct sipe_core_private *sipe_private,
SIPE_UNUSED_PARAMETER struct sip_session *session,
guint result,
const gchar *message,
const sipe_xml *xml)
{
if (result != 200) {
sipe_backend_notify_error(SIPE_CORE_PUBLIC,
_("Error joining chat room"),
message);
} else {
struct sipe_groupchat *groupchat = sipe_private->groupchat;
const sipe_xml *node;
GHashTable *user_ids = g_hash_table_new(g_str_hash, g_str_equal);
/* Extract user IDs & URIs and generate ID -> URI map */
for (node = sipe_xml_child(xml, "uib");
node;
node = sipe_xml_twin(node)) {
const gchar *id = sipe_xml_attribute(node, "id");
const gchar *uri = sipe_xml_attribute(node, "uri");
if (id && uri)
g_hash_table_insert(user_ids,
(gpointer) id,
(gpointer) uri);
}
/* Process channel data */
for (node = sipe_xml_child(xml, "chanib");
node;
node = sipe_xml_twin(node)) {
const gchar *uri = sipe_xml_attribute(node, "uri");
if (uri) {
struct sipe_chat_session *chat_session = g_hash_table_lookup(groupchat->uri_to_chat_session,
uri);
gboolean new = (chat_session == NULL);
const gchar *attr = sipe_xml_attribute(node, "name");
gchar *self = sip_uri_self(sipe_private);
const sipe_xml *aib;
if (new) {
chat_session = sipe_chat_create_session(SIPE_CHAT_TYPE_GROUPCHAT,
sipe_xml_attribute(node,
"uri"),
attr ? attr : "");
g_hash_table_insert(groupchat->uri_to_chat_session,
chat_session->id,
chat_session);
SIPE_DEBUG_INFO("joined room '%s' (%s)",
chat_session->title,
chat_session->id);
chat_session->backend = sipe_backend_chat_create(SIPE_CORE_PUBLIC,
chat_session,
chat_session->title,
self);
} else {
SIPE_DEBUG_INFO("rejoining room '%s' (%s)",
chat_session->title,
chat_session->id);
sipe_backend_chat_rejoin(SIPE_CORE_PUBLIC,
chat_session->backend,
self,
chat_session->title);
}
g_free(self);
attr = sipe_xml_attribute(node, "topic");
if (attr) {
sipe_backend_chat_topic(chat_session->backend,
attr);
}
/* Process user map for channel */
for (aib = sipe_xml_child(node, "aib");
aib;
aib = sipe_xml_twin(aib)) {
const gchar *value = sipe_xml_attribute(aib, "value");
gboolean chanop = is_chanop(aib);
gchar **ids = g_strsplit(value, ",", 0);
if (ids) {
gchar **uid = ids;
while (*uid) {
const gchar *uri = g_hash_table_lookup(user_ids,
*uid);
if (uri)
add_user(chat_session,
uri,
FALSE,
chanop);
uid++;
}
g_strfreev(ids);
}
}
/* Request last 25 entries from channel history */
self = g_strdup_printf("<cmd id=\"cmd:bccontext\" seqid=\"1\">"
"<data>"
"<chanib uri=\"%s\"/>"
"<bcq><last cnt=\"25\"/></bcq>"
"</data>"
"</cmd>", chat_session->id);
chatserver_command(sipe_private, self);
g_free(self);
}
}
g_hash_table_destroy(user_ids);
}
}
static void chatserver_grpchat_message(struct sipe_core_private *sipe_private,
const sipe_xml *grpchat);
static void chatserver_response_history(SIPE_UNUSED_PARAMETER struct sipe_core_private *sipe_private,
SIPE_UNUSED_PARAMETER struct sip_session *session,
SIPE_UNUSED_PARAMETER guint result,
SIPE_UNUSED_PARAMETER const gchar *message,
const sipe_xml *xml)
{
const sipe_xml *grpchat;
for (grpchat = sipe_xml_child(xml, "chanib/msg");
grpchat;
grpchat = sipe_xml_twin(grpchat))
if (sipe_strequal(sipe_xml_attribute(grpchat, "id"),
"grpchat"))
chatserver_grpchat_message(sipe_private, grpchat);
}
static void chatserver_response_part(struct sipe_core_private *sipe_private,
SIPE_UNUSED_PARAMETER struct sip_session *session,
guint result,
const gchar *message,
const sipe_xml *xml)
{
if (result != 200) {
SIPE_DEBUG_WARNING("chatserver_response_part: failed with %d: %s. Dropping room",
result, message);
} else {
struct sipe_groupchat *groupchat = sipe_private->groupchat;
const gchar *uri = sipe_xml_attribute(sipe_xml_child(xml, "chanib"),
"uri");
struct sipe_chat_session *chat_session;
if (uri &&
(chat_session = g_hash_table_lookup(groupchat->uri_to_chat_session,
uri))) {
SIPE_DEBUG_INFO("leaving room '%s' (%s)",
chat_session->title, chat_session->id);
g_hash_table_remove(groupchat->uri_to_chat_session,
uri);
sipe_chat_remove_session(chat_session);
} else {
SIPE_DEBUG_WARNING("chatserver_response_part: unknown chat room uri '%s'",
uri ? uri : "");
}
}
}
static void chatserver_notice_join(struct sipe_core_private *sipe_private,
SIPE_UNUSED_PARAMETER struct sip_session *session,
SIPE_UNUSED_PARAMETER guint result,
SIPE_UNUSED_PARAMETER const gchar *message,
const sipe_xml *xml)
{
struct sipe_groupchat *groupchat = sipe_private->groupchat;
const sipe_xml *uib;
for (uib = sipe_xml_child(xml, "uib");
uib;
uib = sipe_xml_twin(uib)) {
const gchar *uri = sipe_xml_attribute(uib, "uri");
if (uri) {
const sipe_xml *aib;
for (aib = sipe_xml_child(uib, "aib");
aib;
aib = sipe_xml_twin(aib)) {
const gchar *domain = sipe_xml_attribute(aib, "domain");
const gchar *path = sipe_xml_attribute(aib, "value");
if (domain && path) {
gchar *room_uri = g_strdup_printf("ma-chan://%s/%s",
domain, path);
struct sipe_chat_session *chat_session = g_hash_table_lookup(groupchat->uri_to_chat_session,
room_uri);
if (chat_session)
add_user(chat_session,
uri,
TRUE,
is_chanop(aib));
g_free(room_uri);
}
}
}
}
}
static void chatserver_notice_part(struct sipe_core_private *sipe_private,
SIPE_UNUSED_PARAMETER struct sip_session *session,
SIPE_UNUSED_PARAMETER guint result,
SIPE_UNUSED_PARAMETER const gchar *message,
const sipe_xml *xml)
{
struct sipe_groupchat *groupchat = sipe_private->groupchat;
const sipe_xml *chanib;
for (chanib = sipe_xml_child(xml, "chanib");
chanib;
chanib = sipe_xml_twin(chanib)) {
const gchar *room_uri = sipe_xml_attribute(chanib, "uri");
if (room_uri) {
struct sipe_chat_session *chat_session = g_hash_table_lookup(groupchat->uri_to_chat_session,
room_uri);
if (chat_session) {
const sipe_xml *uib;
for (uib = sipe_xml_child(chanib, "uib");
uib;
uib = sipe_xml_twin(uib)) {
const gchar *uri = sipe_xml_attribute(uib, "uri");
if (uri) {
SIPE_DEBUG_INFO("remove_user: %s from room %s (%s)",
uri,
chat_session->title,
chat_session->id);
sipe_backend_chat_remove(chat_session->backend,
uri);
}
}
}
}
}
}
static const struct response {
const gchar *key;
void (* const handler)(struct sipe_core_private *,
struct sip_session *,
guint result, const gchar *,
const sipe_xml *xml);
} response_table[] = {
{ "rpl:requri", chatserver_response_uri },
{ "rpl:chansrch", chatserver_response_channel_search },
{ "rpl:join", chatserver_response_join },
{ "rpl:bjoin", chatserver_response_join },
{ "rpl:bccontext", chatserver_response_history },
{ "rpl:part", chatserver_response_part },
{ "ntc:join", chatserver_notice_join },
{ "ntc:bjoin", chatserver_notice_join },
{ "ntc:part", chatserver_notice_part },
{ NULL, NULL }
};
/* Handles rpl:XXX & ntc:YYY */
static void chatserver_response(struct sipe_core_private *sipe_private,
const sipe_xml *reply,
struct sip_session *session)
{
do {
const sipe_xml *resp, *data;
const gchar *id;
gchar *message;
guint result = 500;
const struct response *r;
id = sipe_xml_attribute(reply, "id");
if (!id) {
SIPE_DEBUG_INFO_NOFORMAT("chatserver_response: no reply ID found!");
continue;
}
resp = sipe_xml_child(reply, "resp");
if (resp) {
result = sipe_xml_int_attribute(resp, "code", 500);
message = sipe_xml_data(resp);
} else {
message = g_strdup("");
}
data = sipe_xml_child(reply, "data");
SIPE_DEBUG_INFO("chatserver_response: '%s' result (%d) %s",
id, result, message ? message : "");
for (r = response_table; r->key; r++) {
if (sipe_strcase_equal(id, r->key)) {
(*r->handler)(sipe_private, session, result, message, data);
/* session can be invalid now */
session = NULL;
break;
}
}
if (!r->key) {
SIPE_DEBUG_INFO_NOFORMAT("chatserver_response: ignoring unknown response");
}
g_free(message);
} while ((reply = sipe_xml_twin(reply)) != NULL);
}
static void chatserver_grpchat_message(struct sipe_core_private *sipe_private,
const sipe_xml *grpchat)
{
struct sipe_groupchat *groupchat = sipe_private->groupchat;
const gchar *uri = sipe_xml_attribute(grpchat, "chanUri");
const gchar *from = sipe_xml_attribute(grpchat, "author");
time_t when = sipe_utils_str_to_time(sipe_xml_attribute(grpchat, "ts"));
gchar *text = sipe_xml_data(sipe_xml_child(grpchat, "chat"));
struct sipe_chat_session *chat_session;
gchar *escaped;
if (!uri || !from) {
SIPE_DEBUG_INFO("chatserver_grpchat_message: message '%s' received without chat room URI or author!",
text ? text : "");
g_free(text);
return;
}
chat_session = g_hash_table_lookup(groupchat->uri_to_chat_session,
uri);
if (!chat_session) {
SIPE_DEBUG_INFO("chatserver_grpchat_message: message '%s' from '%s' received from unknown chat room '%s'!",
text ? text : "", from, uri);
g_free(text);
return;
}
/* libxml2 decodes all entities, but the backend expects HTML */
escaped = g_markup_escape_text(text, -1);
g_free(text);
sipe_backend_chat_message(SIPE_CORE_PUBLIC, chat_session->backend,
from, when, escaped);
g_free(escaped);
}
void process_incoming_info_groupchat(struct sipe_core_private *sipe_private,
struct sipmsg *msg,
struct sip_session *session)
{
sipe_xml *xml = sipe_xml_parse(msg->body, msg->bodylen);
const sipe_xml *node;
const gchar *callid;
struct sip_dialog *dialog;
callid = sipmsg_find_header(msg, "Call-ID");
dialog = sipe_dialog_find(session, session->with);
if (sipe_strequal(callid, dialog->callid)) {
sip_transport_response(sipe_private, msg, 200, "OK", NULL);
if (((node = sipe_xml_child(xml, "rpl")) != NULL) ||
((node = sipe_xml_child(xml, "ntc")) != NULL)) {
chatserver_response(sipe_private, node, session);
} else if ((node = sipe_xml_child(xml, "grpchat")) != NULL) {
chatserver_grpchat_message(sipe_private, node);
} else {
SIPE_DEBUG_INFO_NOFORMAT("process_incoming_info_groupchat: ignoring unknown response");
}
} else {
/*
* Our last session got disconnected without proper shutdown,
* e.g. by Pidgin crashing or network connection loss. When
* we reconnect to the group chat the server will send INFO
* messages to the current *AND* the obsolete Call-ID, until
* the obsolete session expires.
*
* Ignore these INFO messages to avoid, e.g. duplicate texts,
* and respond with an error so that the server knows that we
* consider this dialog to be terminated.
*/
SIPE_DEBUG_INFO("process_incoming_info_groupchat: ignoring unsolicited INFO message to obsolete Call-ID: %s",
callid);
sip_transport_response(sipe_private, msg, 487, "Request Terminated", NULL);
}
sipe_xml_free(xml);
}
void sipe_groupchat_send(struct sipe_core_private *sipe_private,
struct sipe_chat_session *chat_session,
const gchar *what)
{
struct sipe_groupchat *groupchat = sipe_private->groupchat;
gchar *cmd, *self, *timestamp, *tmp;
gchar **lines, **strvp;
struct sipe_groupchat_msg *msg;
if (!groupchat || !chat_session)
return;
SIPE_DEBUG_INFO("sipe_groupchat_send: '%s' to %s",
what, chat_session->id);
self = sip_uri_self(sipe_private);
timestamp = sipe_utils_time_to_str(time(NULL));
/**
* 'what' is already XML-escaped, e.g.
*
* " -> "
* > -> >
* < -> <
* & -> &
*
* Group Chat only accepts plain text, not full HTML. So we have to
* strip all HTML tags and XML escape the text.
*
* Line breaks are encoded as <br> and therefore need to be replaced
* before stripping. In order to prevent HTML stripping to strip line
* endings, we need to split the text into lines on <br>.
*/
lines = g_strsplit(what, "<br>", 0);
for (strvp = lines; *strvp; strvp++) {
/* replace array entry with HTML stripped & XML escaped version */
gchar *stripped = sipe_backend_markup_strip_html(*strvp);
gchar *escaped = g_markup_escape_text(stripped, -1);
g_free(stripped);
g_free(*strvp);
*strvp = escaped;
}
tmp = g_strjoinv("\r\n", lines);
g_strfreev(lines);
cmd = g_strdup_printf("<grpchat id=\"grpchat\" seqid=\"1\" chanUri=\"%s\" author=\"%s\" ts=\"%s\">"
"<chat>%s</chat>"
"</grpchat>",
chat_session->id, self, timestamp, tmp);
g_free(tmp);
g_free(timestamp);
g_free(self);
msg = chatserver_command(sipe_private, cmd);
g_free(cmd);
if (msg) {
msg->session = chat_session;
msg->content = g_strdup(what);
} else {
chatserver_command_error_notify(sipe_private,
chat_session,
what);
}
}
void sipe_groupchat_leave(struct sipe_core_private *sipe_private,
struct sipe_chat_session *chat_session)
{
struct sipe_groupchat *groupchat = sipe_private->groupchat;
gchar *cmd;
if (!groupchat || !chat_session)
return;
SIPE_DEBUG_INFO("sipe_groupchat_leave: %s", chat_session->id);
cmd = g_strdup_printf("<cmd id=\"cmd:part\" seqid=\"1\">"
"<data>"
"<chanib uri=\"%s\"/>"
"</data>"
"</cmd>", chat_session->id);
chatserver_command(sipe_private, cmd);
g_free(cmd);
}
gboolean sipe_core_groupchat_query_rooms(struct sipe_core_public *sipe_public)
{
struct sipe_core_private *sipe_private = SIPE_CORE_PRIVATE;
struct sipe_groupchat *groupchat = sipe_private->groupchat;
if (!groupchat || !groupchat->connected)
return FALSE;
chatserver_command(sipe_private,
"<cmd id=\"cmd:chansrch\" seqid=\"1\">"
"<data>"
"<qib qtype=\"BYNAME\" criteria=\"\" extended=\"false\"/>"
"</data>"
"</cmd>");
return TRUE;
}
void sipe_core_groupchat_join(struct sipe_core_public *sipe_public,
const gchar *uri)
{
struct sipe_core_private *sipe_private = SIPE_CORE_PRIVATE;
struct sipe_groupchat *groupchat = sipe_private->groupchat;
if (!g_str_has_prefix(uri, "ma-chan://"))
return;
if (!groupchat) {
/* This happens when a user has set auto-join on a channel */
sipe_groupchat_allocate(sipe_private);
groupchat = sipe_private->groupchat;
}
if (groupchat->connected) {
struct sipe_chat_session *chat_session = g_hash_table_lookup(groupchat->uri_to_chat_session,
uri);
/* Already joined? */
if (chat_session) {
/* Yes, update backend session */
SIPE_DEBUG_INFO("sipe_core_groupchat_join: show '%s' (%s)",
chat_session->title,
chat_session->id);
sipe_backend_chat_show(chat_session->backend);
} else {
/* No, send command out directly */
gchar *chanid = generate_chanid_node(uri, 0);
if (chanid) {
gchar *cmd = g_strdup_printf("<cmd id=\"cmd:join\" seqid=\"1\">"
"<data>%s</data>"
"</cmd>",
chanid);
SIPE_DEBUG_INFO("sipe_core_groupchat_join: join %s",
uri);
chatserver_command(sipe_private, cmd);
g_free(cmd);
g_free(chanid);
}
}
} else {
/* Add it to the queue but avoid duplicates */
if (!g_slist_find_custom(groupchat->join_queue, uri,
(GCompareFunc)g_strcmp0)) {
SIPE_DEBUG_INFO_NOFORMAT("sipe_core_groupchat_join: URI queued");
groupchat->join_queue = g_slist_prepend(groupchat->join_queue,
g_strdup(uri));
}
}
}
void sipe_groupchat_rejoin(struct sipe_core_private *sipe_private,
struct sipe_chat_session *chat_session)
{
struct sipe_groupchat *groupchat = sipe_private->groupchat;
if (!groupchat) {
/* First rejoined channel after reconnect will trigger this */
sipe_groupchat_allocate(sipe_private);
groupchat = sipe_private->groupchat;
}
/* Remember "old" session, so that we don't recreate it at join */
g_hash_table_insert(groupchat->uri_to_chat_session,
chat_session->id,
chat_session);
sipe_core_groupchat_join(SIPE_CORE_PUBLIC, chat_session->id);
}
/*
Local Variables:
mode: c
c-file-style: "bsd"
indent-tabs-mode: t
tab-width: 8
End:
*/