diff options
Diffstat (limited to 'libpurple/protocols/jabber')
36 files changed, 11641 insertions, 0 deletions
diff --git a/libpurple/protocols/jabber/.todo b/libpurple/protocols/jabber/.todo new file mode 100644 index 0000000000..5f659a3e0f --- /dev/null +++ b/libpurple/protocols/jabber/.todo @@ -0,0 +1,47 @@ +<todo version="0.1.19"> + <note priority="verylow" time="1036043981" done="1089237837"> + *sigh* file transfer (do we really need/want this?) + <comment> + faceprint did this + </comment> + </note> + <note priority="high" time="1036043427"> + problem seeing buddies with long blist? + </note> + <note priority="medium" time="1036044198"> + Browsing + </note> + <note priority="medium" time="1036044416"> + Server Admin operations (messages, etc.) + </note> + <note priority="medium" time="1036044448"> + Add option for user info to be published or not in JUD. + </note> + <note priority="medium" time="1036044571"> + Show self on buddylist + <comment> + is this done? + </comment> + </note> + <note priority="medium" time="1036044583"> + Delete server account. + </note> + <note priority="medium" time="1036045649"> + Permit/Deny buddy support. + </note> + <note priority="medium" time="1036046413"> + a populate roster from local blist. most useful if you want to migrate a blist from one account to another, also useful if something freaky happens and the server blist is lost. + </note> + <note priority="medium" time="1037892911"> + info + <note priority="medium" time="1037893000"> + formatted. enhancement-request so that the birthday field in the setinfo form would split up into relevant fields allowing for a strict syntax (like year--month--day or so, perhaps even dropdown menus) + </note> + <note priority="low" time="1037890968"> + have set info pre-fill values from the server when no local vcard exists. this will help people migrating to gaim + </note> + </note> + <note priority="verylow" time="1036044192"> + Jabber Transports (having them show up on the buddy list should be fairly easy; having an appropriate right-click menu for them should also be somewhat easy. Providing a UI for adding transports should be rather difficult.) + </note> +</todo> diff --git a/libpurple/protocols/jabber/JEPS b/libpurple/protocols/jabber/JEPS new file mode 100644 index 0000000000..cd67248431 --- /dev/null +++ b/libpurple/protocols/jabber/JEPS @@ -0,0 +1,27 @@ +0045: IN PROGRESS + Multi-User Chat +0047: IN PROGRESS + In-Band Bytestreams +0060: NEED + Pub-Sub +0071: AWAITING FINAL SPEC + XHTML-IM +0073: NEED + Basic IM Protocol Suite +0080: NEED (Do we?) + Geographic Location Information +0084: NEED + User Avatars in Jabber +0085: NEED + Chat State Notifications +0089: WATCH + Generic Alerts +0093: NEED + Roster Item Exchange +0100: NEED + Gateway Interaction (Transports) +0115: WATCH + Client Capabilities +0117: NEED + Intermediate IM Protocol Suite + diff --git a/libpurple/protocols/jabber/Makefile.am b/libpurple/protocols/jabber/Makefile.am new file mode 100644 index 0000000000..5c6a15a7c7 --- /dev/null +++ b/libpurple/protocols/jabber/Makefile.am @@ -0,0 +1,67 @@ +EXTRA_DIST = \ + Makefile.mingw \ + win32/posix.uname.c \ + win32/utsname.h + +pkgdir = $(libdir)/gaim + +JABBERSOURCES = auth.c \ + auth.h \ + buddy.c \ + buddy.h \ + chat.c \ + chat.h \ + disco.c \ + disco.h \ + google.c \ + google.h \ + iq.c \ + iq.h \ + jabber.c \ + jabber.h \ + jutil.c \ + jutil.h \ + message.c \ + message.h \ + oob.c \ + oob.h \ + parser.c \ + parser.h \ + presence.c \ + presence.h \ + roster.c \ + roster.h \ + si.c \ + si.h \ + xdata.c \ + xdata.h + +AM_CFLAGS = $(st) + +libjabber_la_LDFLAGS = -module -avoid-version + +if STATIC_JABBER + +st = -DGAIM_STATIC_PRPL +noinst_LIBRARIES = libjabber.a +pkg_LTLIBRARIES = + +libjabber_a_SOURCES = $(JABBERSOURCES) +libjabber_a_CFLAGS = $(AM_CFLAGS) + +else + +st = +pkg_LTLIBRARIES = libjabber.la +noinst_LIBRARIES = + +libjabber_la_SOURCES = $(JABBERSOURCES) +libjabber_la_LIBADD = $(GLIB_LIBS) $(SASL_LIBS) $(LIBXML_LIBS) + +endif + +AM_CPPFLAGS = \ + -I$(top_srcdir)/libpurple \ + $(DEBUG_CFLAGS) \ + $(GLIB_CFLAGS) \ + $(LIBXML_CFLAGS) diff --git a/libpurple/protocols/jabber/Makefile.mingw b/libpurple/protocols/jabber/Makefile.mingw new file mode 100644 index 0000000000..1a59a8eaf2 --- /dev/null +++ b/libpurple/protocols/jabber/Makefile.mingw @@ -0,0 +1,96 @@ +# +# Makefile.mingw +# +# Description: Makefile for win32 (mingw) version of libjabber +# + +GAIM_TOP := ../../.. +include $(GAIM_TOP)/libgaim/win32/global.mak + +TARGET = libjabber +TYPE = PLUGIN + +# Static or Plugin... +ifeq ($(TYPE),STATIC) + DEFINES += -DSTATIC + DLL_INSTALL_DIR = $(GAIM_INSTALL_DIR) +else +ifeq ($(TYPE),PLUGIN) + DLL_INSTALL_DIR = $(GAIM_INSTALL_PLUGINS_DIR) +endif +endif + +## +## INCLUDE PATHS +## +INCLUDE_PATHS += -I. \ + -I./win32 \ + -I$(GTK_TOP)/include \ + -I$(GTK_TOP)/include/glib-2.0 \ + -I$(GTK_TOP)/lib/glib-2.0/include \ + -I$(LIBXML2_TOP)/include \ + -I$(GAIM_LIB_TOP) \ + -I$(GAIM_LIB_TOP)/win32 \ + -I$(GAIM_TOP) + +LIB_PATHS = -L$(GTK_TOP)/lib \ + -L$(LIBXML2_TOP)/lib \ + -L$(GAIM_LIB_TOP) + +## +## SOURCES, OBJECTS +## +C_SRC = auth.c \ + buddy.c \ + chat.c \ + disco.c \ + google.c \ + iq.c \ + jabber.c \ + jutil.c \ + message.c \ + oob.c \ + parser.c \ + presence.c \ + roster.c \ + si.c \ + xdata.c \ + win32/posix.uname.c + +OBJECTS = $(C_SRC:%.c=%.o) + +## +## LIBRARIES +## +LIBS = \ + -lglib-2.0 \ + -lxml2 \ + -lws2_32 \ + -lintl \ + -lgaim + +include $(GAIM_COMMON_RULES) + +## +## TARGET DEFINITIONS +## +.PHONY: all install clean + +all: $(TARGET).dll + +install: all $(DLL_INSTALL_DIR) + cp $(TARGET).dll $(DLL_INSTALL_DIR) + +$(OBJECTS): $(GAIM_CONFIG_H) + +$(TARGET).dll: $(GAIM_LIBGAIM_DLL).a $(OBJECTS) + $(CC) -shared $(OBJECTS) $(LIB_PATHS) $(LIBS) $(DLL_LD_FLAGS) -o $(TARGET).dll + +## +## CLEAN RULES +## +clean: + rm -f $(OBJECTS) + rm -f $(TARGET).dll + +include $(GAIM_COMMON_TARGETS) diff --git a/libpurple/protocols/jabber/auth.c b/libpurple/protocols/jabber/auth.c new file mode 100644 index 0000000000..9f2718463e --- /dev/null +++ b/libpurple/protocols/jabber/auth.c @@ -0,0 +1,800 @@ +/* + * gaim - Jabber Protocol Plugin + * + * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com> + * + * 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 + * + */ +#include "internal.h" + +#include "jutil.h" +#include "auth.h" +#include "xmlnode.h" +#include "jabber.h" +#include "iq.h" + +#include "debug.h" +#include "util.h" +#include "cipher.h" +#include "sslconn.h" +#include "request.h" + +static void auth_old_result_cb(JabberStream *js, xmlnode *packet, + gpointer data); + +gboolean +jabber_process_starttls(JabberStream *js, xmlnode *packet) +{ + xmlnode *starttls; + + if((starttls = xmlnode_get_child(packet, "starttls"))) { + if(gaim_ssl_is_supported()) { + jabber_send_raw(js, + "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>", -1); + return TRUE; + } else if(xmlnode_get_child(starttls, "required")) { + gaim_connection_error(js->gc, _("Server requires TLS/SSL for login. No TLS/SSL support found.")); + return TRUE; + } + } + + return FALSE; +} + +static void finish_plaintext_authentication(JabberStream *js) +{ + if(js->auth_type == JABBER_AUTH_PLAIN) { + xmlnode *auth; + GString *response; + gchar *enc_out; + + auth = xmlnode_new("auth"); + xmlnode_set_namespace(auth, "urn:ietf:params:xml:ns:xmpp-sasl"); + + xmlnode_set_attrib(auth, "xmlns:ga", "http://www.google.com/talk/protocol/auth"); + xmlnode_set_attrib(auth, "ga:client-uses-full-bind-result", "true"); + + response = g_string_new(""); + response = g_string_append_len(response, "\0", 1); + response = g_string_append(response, js->user->node); + response = g_string_append_len(response, "\0", 1); + response = g_string_append(response, + gaim_connection_get_password(js->gc)); + + enc_out = gaim_base64_encode((guchar *)response->str, response->len); + + xmlnode_set_attrib(auth, "mechanism", "PLAIN"); + xmlnode_insert_data(auth, enc_out, -1); + g_free(enc_out); + g_string_free(response, TRUE); + + jabber_send(js, auth); + xmlnode_free(auth); + } else if(js->auth_type == JABBER_AUTH_IQ_AUTH) { + JabberIq *iq; + xmlnode *query, *x; + + iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:auth"); + query = xmlnode_get_child(iq->node, "query"); + x = xmlnode_new_child(query, "username"); + xmlnode_insert_data(x, js->user->node, -1); + x = xmlnode_new_child(query, "resource"); + xmlnode_insert_data(x, js->user->resource, -1); + x = xmlnode_new_child(query, "password"); + xmlnode_insert_data(x, gaim_connection_get_password(js->gc), -1); + jabber_iq_set_callback(iq, auth_old_result_cb, NULL); + jabber_iq_send(iq); + } +} + +static void allow_plaintext_auth(GaimAccount *account) +{ + gaim_account_set_bool(account, "auth_plain_in_clear", TRUE); + + finish_plaintext_authentication(account->gc->proto_data); +} + +static void disallow_plaintext_auth(GaimAccount *account) +{ + gaim_connection_error(account->gc, _("Server requires plaintext authentication over an unencrypted stream")); +} + +#ifdef HAVE_CYRUS_SASL + +static void jabber_auth_start_cyrus(JabberStream *); + +/* Callbacks for Cyrus SASL */ + +static int jabber_sasl_cb_realm(void *ctx, int id, const char **avail, const char **result) +{ + JabberStream *js = (JabberStream *)ctx; + + if (id != SASL_CB_GETREALM || !result) return SASL_BADPARAM; + + *result = js->user->domain; + + return SASL_OK; +} + +static int jabber_sasl_cb_simple(void *ctx, int id, const char **res, unsigned *len) +{ + JabberStream *js = (JabberStream *)ctx; + + switch(id) { + case SASL_CB_AUTHNAME: + *res = js->user->node; + break; + case SASL_CB_USER: + *res = ""; + break; + default: + return SASL_BADPARAM; + } + if (len) *len = strlen((char *)*res); + return SASL_OK; +} + +static int jabber_sasl_cb_secret(sasl_conn_t *conn, void *ctx, int id, sasl_secret_t **secret) +{ + JabberStream *js = (JabberStream *)ctx; + const char *pw = gaim_account_get_password(js->gc->account); + size_t len; + static sasl_secret_t *x = NULL; + + if (!conn || !secret || id != SASL_CB_PASS) + return SASL_BADPARAM; + + len = strlen(pw); + x = (sasl_secret_t *) realloc(x, sizeof(sasl_secret_t) + len); + + if (!x) + return SASL_NOMEM; + + x->len = len; + strcpy((char*)x->data, pw); + + *secret = x; + return SASL_OK; +} + +static void allow_cyrus_plaintext_auth(GaimAccount *account) +{ + gaim_account_set_bool(account, "auth_plain_in_clear", TRUE); + + jabber_auth_start_cyrus(account->gc->proto_data); +} + +static void jabber_auth_start_cyrus(JabberStream *js) +{ + const char *clientout = NULL, *mech = NULL; + char *enc_out; + unsigned coutlen = 0; + xmlnode *auth; + sasl_security_properties_t secprops; + gboolean again; + gboolean plaintext = TRUE; + + /* Set up security properties and options */ + secprops.min_ssf = 0; + secprops.security_flags = SASL_SEC_NOANONYMOUS; + + if (!js->gsc) { + secprops.max_ssf = -1; + secprops.maxbufsize = 4096; + plaintext = gaim_account_get_bool(js->gc->account, "auth_plain_in_clear", FALSE); + if (!plaintext) + secprops.security_flags |= SASL_SEC_NOPLAINTEXT; + } else { + secprops.max_ssf = 0; + secprops.maxbufsize = 0; + plaintext = TRUE; + } + secprops.property_names = 0; + secprops.property_values = 0; + + do { + again = FALSE; + /* Use the user's domain for compatibility with the old + * DIGESTMD5 code. Note that this may cause problems where + * the user's domain doesn't match the FQDN of the jabber + * service + */ + + js->sasl_state = sasl_client_new("xmpp", js->user->domain, NULL, NULL, js->sasl_cb, 0, &js->sasl); + if (js->sasl_state==SASL_OK) { + sasl_setprop(js->sasl, SASL_SEC_PROPS, &secprops); + gaim_debug_info("sasl", "Mechs found: %s\n", js->sasl_mechs->str); + js->sasl_state = sasl_client_start(js->sasl, js->sasl_mechs->str, NULL, &clientout, &coutlen, &mech); + } + switch (js->sasl_state) { + /* Success */ + case SASL_OK: + case SASL_CONTINUE: + break; + case SASL_NOMECH: + /* No mechanisms do what we want. See if we can add + * plaintext ones to the list. */ + + if (!gaim_account_get_password(js->gc->account)) { + gaim_connection_error(js->gc, _("Server couldn't authenticate you without a password")); + return; + } else if (!plaintext) { + gaim_request_yes_no(js->gc, _("Plaintext Authentication"), + _("Plaintext Authentication"), + _("This server requires plaintext authentication over an unencrypted connection. Allow this and continue authentication?"), + 2, js->gc->account, + allow_cyrus_plaintext_auth, + disallow_plaintext_auth); + return; + } else { + gaim_connection_error(js->gc, _("Server does not use any supported authentication method")); + return; + } + /* not reached */ + break; + + /* Fatal errors. Give up and go home */ + case SASL_BADPARAM: + case SASL_NOMEM: + break; + + /* For everything else, fail the mechanism and try again */ + default: + gaim_debug_info("sasl", "sasl_state is %d, failing the mech and trying again\n", js->sasl_state); + + /* + * DAA: is this right? + * The manpage says that "mech" will contain the chosen mechanism on success. + * Presumably, if we get here that isn't the case and we shouldn't try again? + * I suspect that this never happens. + */ + if (mech && strlen(mech) > 0) { + char *pos; + if ((pos = strstr(js->sasl_mechs->str, mech))) { + g_string_erase(js->sasl_mechs, pos-js->sasl_mechs->str, strlen(mech)); + } + again = TRUE; + } + + sasl_dispose(&js->sasl); + } + } while (again); + + if (js->sasl_state == SASL_CONTINUE || js->sasl_state == SASL_OK) { + auth = xmlnode_new("auth"); + xmlnode_set_namespace(auth, "urn:ietf:params:xml:ns:xmpp-sasl"); + xmlnode_set_attrib(auth, "mechanism", mech); + if (clientout) { + if (coutlen == 0) { + xmlnode_insert_data(auth, "=", -1); + } else { + enc_out = gaim_base64_encode((unsigned char*)clientout, coutlen); + xmlnode_insert_data(auth, enc_out, -1); + g_free(enc_out); + } + } + jabber_send(js, auth); + xmlnode_free(auth); + } else { + gaim_connection_error(js->gc, "SASL authentication failed\n"); + } +} + +static int +jabber_sasl_cb_log(void *context, int level, const char *message) +{ + if(level <= SASL_LOG_TRACE) + gaim_debug_info("sasl", "%s\n", message); + + return SASL_OK; +} + +#endif + +void +jabber_auth_start(JabberStream *js, xmlnode *packet) +{ +#ifdef HAVE_CYRUS_SASL + int id; +#else + gboolean digest_md5 = FALSE, plain=FALSE; +#endif + + xmlnode *mechs, *mechnode; + + + if(js->registration) { + jabber_register_start(js); + return; + } + + mechs = xmlnode_get_child(packet, "mechanisms"); + + if(!mechs) { + gaim_connection_error(js->gc, _("Invalid response from server.")); + return; + } + +#ifdef HAVE_CYRUS_SASL + js->sasl_mechs = g_string_new(""); +#endif + + for(mechnode = xmlnode_get_child(mechs, "mechanism"); mechnode; + mechnode = xmlnode_get_next_twin(mechnode)) + { + char *mech_name = xmlnode_get_data(mechnode); +#ifdef HAVE_CYRUS_SASL + g_string_append(js->sasl_mechs, mech_name); + g_string_append_c(js->sasl_mechs, ' '); +#else + if(mech_name && !strcmp(mech_name, "DIGEST-MD5")) + digest_md5 = TRUE; + else if(mech_name && !strcmp(mech_name, "PLAIN")) + plain = TRUE; +#endif + g_free(mech_name); + } + +#ifdef HAVE_CYRUS_SASL + js->auth_type = JABBER_AUTH_CYRUS; + + /* Set up our callbacks structure */ + js->sasl_cb = g_new0(sasl_callback_t,6); + + id = 0; + js->sasl_cb[id].id = SASL_CB_GETREALM; + js->sasl_cb[id].proc = jabber_sasl_cb_realm; + js->sasl_cb[id].context = (void *)js; + id++; + + js->sasl_cb[id].id = SASL_CB_AUTHNAME; + js->sasl_cb[id].proc = jabber_sasl_cb_simple; + js->sasl_cb[id].context = (void *)js; + id++; + + js->sasl_cb[id].id = SASL_CB_USER; + js->sasl_cb[id].proc = jabber_sasl_cb_simple; + js->sasl_cb[id].context = (void *)js; + id++; + + if (gaim_account_get_password(js->gc->account)) { + js->sasl_cb[id].id = SASL_CB_PASS; + js->sasl_cb[id].proc = jabber_sasl_cb_secret; + js->sasl_cb[id].context = (void *)js; + id++; + } + + js->sasl_cb[id].id = SASL_CB_LOG; + js->sasl_cb[id].proc = jabber_sasl_cb_log; + js->sasl_cb[id].context = (void*)js; + id++; + + js->sasl_cb[id].id = SASL_CB_LIST_END; + + jabber_auth_start_cyrus(js); +#else + + if(digest_md5) { + xmlnode *auth; + + js->auth_type = JABBER_AUTH_DIGEST_MD5; + auth = xmlnode_new("auth"); + xmlnode_set_namespace(auth, "urn:ietf:params:xml:ns:xmpp-sasl"); + xmlnode_set_attrib(auth, "mechanism", "DIGEST-MD5"); + + jabber_send(js, auth); + xmlnode_free(auth); + } else if(plain) { + js->auth_type = JABBER_AUTH_PLAIN; + + if(js->gsc == NULL && !gaim_account_get_bool(js->gc->account, "auth_plain_in_clear", FALSE)) { + gaim_request_yes_no(js->gc, _("Plaintext Authentication"), + _("Plaintext Authentication"), + _("This server requires plaintext authentication over an unencrypted connection. Allow this and continue authentication?"), + 2, js->gc->account, allow_plaintext_auth, + disallow_plaintext_auth); + return; + } + finish_plaintext_authentication(js); + } else { + gaim_connection_error(js->gc, + _("Server does not use any supported authentication method")); + } +#endif +} + +static void auth_old_result_cb(JabberStream *js, xmlnode *packet, gpointer data) +{ + const char *type = xmlnode_get_attrib(packet, "type"); + + if(type && !strcmp(type, "result")) { + jabber_stream_set_state(js, JABBER_STREAM_CONNECTED); + } else { + char *msg = jabber_parse_error(js, packet); + xmlnode *error; + const char *err_code; + + if((error = xmlnode_get_child(packet, "error")) && + (err_code = xmlnode_get_attrib(error, "code")) && + !strcmp(err_code, "401")) { + js->gc->wants_to_die = TRUE; + } + + gaim_connection_error(js->gc, msg); + g_free(msg); + } +} + +static void auth_old_cb(JabberStream *js, xmlnode *packet, gpointer data) +{ + JabberIq *iq; + xmlnode *query, *x; + const char *type = xmlnode_get_attrib(packet, "type"); + const char *pw = gaim_connection_get_password(js->gc); + + if(!type) { + gaim_connection_error(js->gc, _("Invalid response from server.")); + return; + } else if(!strcmp(type, "error")) { + char *msg = jabber_parse_error(js, packet); + gaim_connection_error(js->gc, msg); + g_free(msg); + } else if(!strcmp(type, "result")) { + query = xmlnode_get_child(packet, "query"); + if(js->stream_id && xmlnode_get_child(query, "digest")) { + unsigned char hashval[20]; + char *s, h[41], *p; + int i; + + iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:auth"); + query = xmlnode_get_child(iq->node, "query"); + x = xmlnode_new_child(query, "username"); + xmlnode_insert_data(x, js->user->node, -1); + x = xmlnode_new_child(query, "resource"); + xmlnode_insert_data(x, js->user->resource, -1); + + x = xmlnode_new_child(query, "digest"); + s = g_strdup_printf("%s%s", js->stream_id, pw); + + gaim_cipher_digest_region("sha1", (guchar *)s, strlen(s), + sizeof(hashval), hashval, NULL); + + p = h; + for(i=0; i<20; i++, p+=2) + snprintf(p, 3, "%02x", hashval[i]); + xmlnode_insert_data(x, h, -1); + g_free(s); + jabber_iq_set_callback(iq, auth_old_result_cb, NULL); + jabber_iq_send(iq); + + } else if(xmlnode_get_child(query, "password")) { + if(js->gsc == NULL && !gaim_account_get_bool(js->gc->account, + "auth_plain_in_clear", FALSE)) { + gaim_request_yes_no(js->gc, _("Plaintext Authentication"), + _("Plaintext Authentication"), + _("This server requires plaintext authentication over an unencrypted connection. Allow this and continue authentication?"), + 2, js->gc->account, allow_plaintext_auth, + disallow_plaintext_auth); + return; + } + finish_plaintext_authentication(js); + } else { + gaim_connection_error(js->gc, + _("Server does not use any supported authentication method")); + return; + } + } +} + +void jabber_auth_start_old(JabberStream *js) +{ + JabberIq *iq; + xmlnode *query, *username; + + iq = jabber_iq_new_query(js, JABBER_IQ_GET, "jabber:iq:auth"); + + query = xmlnode_get_child(iq->node, "query"); + username = xmlnode_new_child(query, "username"); + xmlnode_insert_data(username, js->user->node, -1); + + jabber_iq_set_callback(iq, auth_old_cb, NULL); + + jabber_iq_send(iq); +} + +static GHashTable* parse_challenge(const char *challenge) +{ + GHashTable *ret = g_hash_table_new_full(g_str_hash, g_str_equal, + g_free, g_free); + char **pairs; + int i; + + pairs = g_strsplit(challenge, ",", -1); + + for(i=0; pairs[i]; i++) { + char **keyval = g_strsplit(pairs[i], "=", 2); + if(keyval[0] && keyval[1]) { + if(keyval[1][0] == '"' && keyval[1][strlen(keyval[1])-1] == '"') + g_hash_table_replace(ret, g_strdup(keyval[0]), g_strndup(keyval[1]+1, strlen(keyval[1])-2)); + else + g_hash_table_replace(ret, g_strdup(keyval[0]), g_strdup(keyval[1])); + } + g_strfreev(keyval); + } + + g_strfreev(pairs); + + return ret; +} + +static char * +generate_response_value(JabberID *jid, const char *passwd, const char *nonce, + const char *cnonce, const char *a2, const char *realm) +{ + GaimCipher *cipher; + GaimCipherContext *context; + guchar result[16]; + size_t a1len; + + gchar *a1, *convnode=NULL, *convpasswd = NULL, *ha1, *ha2, *kd, *x, *z; + + if((convnode = g_convert(jid->node, strlen(jid->node), "iso-8859-1", "utf-8", + NULL, NULL, NULL)) == NULL) { + convnode = g_strdup(jid->node); + } + if(passwd && ((convpasswd = g_convert(passwd, strlen(passwd), "iso-8859-1", + "utf-8", NULL, NULL, NULL)) == NULL)) { + convpasswd = g_strdup(passwd); + } + + cipher = gaim_ciphers_find_cipher("md5"); + context = gaim_cipher_context_new(cipher, NULL); + + x = g_strdup_printf("%s:%s:%s", convnode, realm, convpasswd ? convpasswd : ""); + gaim_cipher_context_append(context, (const guchar *)x, strlen(x)); + gaim_cipher_context_digest(context, sizeof(result), result, NULL); + + a1 = g_strdup_printf("xxxxxxxxxxxxxxxx:%s:%s", nonce, cnonce); + a1len = strlen(a1); + g_memmove(a1, result, 16); + + gaim_cipher_context_reset(context, NULL); + gaim_cipher_context_append(context, (const guchar *)a1, a1len); + gaim_cipher_context_digest(context, sizeof(result), result, NULL); + + ha1 = gaim_base16_encode(result, 16); + + gaim_cipher_context_reset(context, NULL); + gaim_cipher_context_append(context, (const guchar *)a2, strlen(a2)); + gaim_cipher_context_digest(context, sizeof(result), result, NULL); + + ha2 = gaim_base16_encode(result, 16); + + kd = g_strdup_printf("%s:%s:00000001:%s:auth:%s", ha1, nonce, cnonce, ha2); + + gaim_cipher_context_reset(context, NULL); + gaim_cipher_context_append(context, (const guchar *)kd, strlen(kd)); + gaim_cipher_context_digest(context, sizeof(result), result, NULL); + gaim_cipher_context_destroy(context); + + z = gaim_base16_encode(result, 16); + + g_free(convnode); + g_free(convpasswd); + g_free(x); + g_free(a1); + g_free(ha1); + g_free(ha2); + g_free(kd); + + return z; +} + +void +jabber_auth_handle_challenge(JabberStream *js, xmlnode *packet) +{ + + if(js->auth_type == JABBER_AUTH_DIGEST_MD5) { + char *enc_in = xmlnode_get_data(packet); + char *dec_in; + char *enc_out; + GHashTable *parts; + + if(!enc_in) { + gaim_connection_error(js->gc, _("Invalid response from server.")); + return; + } + + dec_in = (char *)gaim_base64_decode(enc_in, NULL); + gaim_debug(GAIM_DEBUG_MISC, "jabber", "decoded challenge (%d): %s\n", + strlen(dec_in), dec_in); + + parts = parse_challenge(dec_in); + + + if (g_hash_table_lookup(parts, "rspauth")) { + char *rspauth = g_hash_table_lookup(parts, "rspauth"); + + + if(rspauth && js->expected_rspauth && + !strcmp(rspauth, js->expected_rspauth)) { + jabber_send_raw(js, + "<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", + -1); + } else { + gaim_connection_error(js->gc, _("Invalid challenge from server")); + } + g_free(js->expected_rspauth); + } else { + /* assemble a response, and send it */ + /* see RFC 2831 */ + GString *response = g_string_new(""); + char *a2; + char *auth_resp; + char *buf; + char *cnonce; + char *realm; + char *nonce; + + /* we're actually supposed to prompt the user for a realm if + * the server doesn't send one, but that really complicates things, + * so i'm not gonna worry about it until is poses a problem to + * someone, or I get really bored */ + realm = g_hash_table_lookup(parts, "realm"); + if(!realm) + realm = js->user->domain; + + cnonce = g_strdup_printf("%x%u%x", g_random_int(), (int)time(NULL), + g_random_int()); + nonce = g_hash_table_lookup(parts, "nonce"); + + + a2 = g_strdup_printf("AUTHENTICATE:xmpp/%s", realm); + auth_resp = generate_response_value(js->user, + gaim_connection_get_password(js->gc), nonce, cnonce, a2, realm); + g_free(a2); + + a2 = g_strdup_printf(":xmpp/%s", realm); + js->expected_rspauth = generate_response_value(js->user, + gaim_connection_get_password(js->gc), nonce, cnonce, a2, realm); + g_free(a2); + + + g_string_append_printf(response, "username=\"%s\"", js->user->node); + g_string_append_printf(response, ",realm=\"%s\"", realm); + g_string_append_printf(response, ",nonce=\"%s\"", nonce); + g_string_append_printf(response, ",cnonce=\"%s\"", cnonce); + g_string_append_printf(response, ",nc=00000001"); + g_string_append_printf(response, ",qop=auth"); + g_string_append_printf(response, ",digest-uri=\"xmpp/%s\"", realm); + g_string_append_printf(response, ",response=%s", auth_resp); + g_string_append_printf(response, ",charset=utf-8"); + + g_free(auth_resp); + g_free(cnonce); + + enc_out = gaim_base64_encode((guchar *)response->str, response->len); + + gaim_debug(GAIM_DEBUG_MISC, "jabber", "decoded response (%d): %s\n", response->len, response->str); + + buf = g_strdup_printf("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>%s</response>", enc_out); + + jabber_send_raw(js, buf, -1); + + g_free(buf); + + g_free(enc_out); + + g_string_free(response, TRUE); + } + + g_free(enc_in); + g_free(dec_in); + g_hash_table_destroy(parts); + } +#ifdef HAVE_CYRUS_SASL + else if (js->auth_type == JABBER_AUTH_CYRUS) { + char *enc_in = xmlnode_get_data(packet); + unsigned char *dec_in; + char *enc_out; + const char *c_out; + unsigned int clen; + gsize declen; + xmlnode *response; + + dec_in = gaim_base64_decode(enc_in, &declen); + + js->sasl_state = sasl_client_step(js->sasl, (char*)dec_in, declen, + NULL, &c_out, &clen); + g_free(enc_in); + g_free(dec_in); + if (js->sasl_state != SASL_CONTINUE && js->sasl_state != SASL_OK) { + gaim_debug_error("jabber", "Error is %d : %s\n",js->sasl_state,sasl_errdetail(js->sasl)); + gaim_connection_error(js->gc, _("SASL error")); + return; + } else { + response = xmlnode_new("response"); + xmlnode_set_namespace(response, "urn:ietf:params:xml:ns:xmpp-sasl"); + if (c_out) { + enc_out = gaim_base64_encode((unsigned char*)c_out, clen); + xmlnode_insert_data(response, enc_out, -1); + g_free(enc_out); + } + jabber_send(js, response); + xmlnode_free(response); + } + } +#endif +} + +void jabber_auth_handle_success(JabberStream *js, xmlnode *packet) +{ + const char *ns = xmlnode_get_namespace(packet); +#ifdef HAVE_CYRUS_SASL + const int *x; +#endif + + if(!ns || strcmp(ns, "urn:ietf:params:xml:ns:xmpp-sasl")) { + gaim_connection_error(js->gc, _("Invalid response from server.")); + return; + } + +#ifdef HAVE_CYRUS_SASL + /* The SASL docs say that if the client hasn't returned OK yet, we + * should try one more round against it + */ + if (js->sasl_state != SASL_OK) { + char *enc_in = xmlnode_get_data(packet); + unsigned char *dec_in = NULL; + const char *c_out; + unsigned int clen; + gsize declen = 0; + + if(enc_in != NULL) + dec_in = gaim_base64_decode(enc_in, &declen); + + js->sasl_state = sasl_client_step(js->sasl, (char*)dec_in, declen, NULL, &c_out, &clen); + + g_free(enc_in); + g_free(dec_in); + + if (js->sasl_state != SASL_OK) { + /* This should never happen! */ + gaim_connection_error(js->gc, _("Invalid response from server.")); + } + } + /* If we've negotiated a security layer, we need to enable it */ + sasl_getprop(js->sasl, SASL_SSF, &x); + if (*x > 0) { + sasl_getprop(js->sasl, SASL_MAXOUTBUF, &x); + js->sasl_maxbuf = *x; + } +#endif + + jabber_stream_set_state(js, JABBER_STREAM_REINITIALIZING); +} + +void jabber_auth_handle_failure(JabberStream *js, xmlnode *packet) +{ + char *msg = jabber_parse_error(js, packet); + + if(!msg) { + gaim_connection_error(js->gc, _("Invalid response from server.")); + } else { + gaim_connection_error(js->gc, msg); + g_free(msg); + } +} diff --git a/libpurple/protocols/jabber/auth.h b/libpurple/protocols/jabber/auth.h new file mode 100644 index 0000000000..5daf9ae6de --- /dev/null +++ b/libpurple/protocols/jabber/auth.h @@ -0,0 +1,35 @@ +/** + * @file auth.h Authentication routines + * + * gaim + * + * Copyright (C) 2003 Nathan Walp <faceprint@faceprint.com> + * + * 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 + */ +#ifndef _GAIM_JABBER_AUTH_H_ +#define _GAIM_JABBER_AUTH_H_ + +#include "jabber.h" +#include "xmlnode.h" + +gboolean jabber_process_starttls(JabberStream *js, xmlnode *packet); +void jabber_auth_start(JabberStream *js, xmlnode *packet); +void jabber_auth_start_old(JabberStream *js); +void jabber_auth_handle_challenge(JabberStream *js, xmlnode *packet); +void jabber_auth_handle_success(JabberStream *js, xmlnode *packet); +void jabber_auth_handle_failure(JabberStream *js, xmlnode *packet); + +#endif /* _GAIM_JABBER_AUTH_H_ */ diff --git a/libpurple/protocols/jabber/buddy.c b/libpurple/protocols/jabber/buddy.c new file mode 100644 index 0000000000..d8a5c1fa17 --- /dev/null +++ b/libpurple/protocols/jabber/buddy.c @@ -0,0 +1,1799 @@ +/* + * gaim - Jabber Protocol Plugin + * + * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com> + * + * 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 + * + */ +#include "internal.h" +#include "cipher.h" +#include "debug.h" +#include "imgstore.h" +#include "prpl.h" +#include "notify.h" +#include "request.h" +#include "util.h" +#include "xmlnode.h" + +#include "buddy.h" +#include "chat.h" +#include "jabber.h" +#include "iq.h" +#include "presence.h" +#include "xdata.h" + +typedef struct { + long idle_seconds; +} JabberBuddyInfoResource; + +typedef struct { + JabberStream *js; + JabberBuddy *jb; + char *jid; + GSList *ids; + GHashTable *resources; + int timeout_handle; + char *vcard_text; + GSList *vcard_imgids; +} JabberBuddyInfo; + +void jabber_buddy_free(JabberBuddy *jb) +{ + g_return_if_fail(jb != NULL); + + if(jb->error_msg) + g_free(jb->error_msg); + while(jb->resources) + jabber_buddy_resource_free(jb->resources->data); + + g_free(jb); +} + +JabberBuddy *jabber_buddy_find(JabberStream *js, const char *name, + gboolean create) +{ + JabberBuddy *jb; + const char *realname; + + if (js->buddies == NULL) + return NULL; + + if(!(realname = jabber_normalize(js->gc->account, name))) + return NULL; + + jb = g_hash_table_lookup(js->buddies, realname); + + if(!jb && create) { + jb = g_new0(JabberBuddy, 1); + g_hash_table_insert(js->buddies, g_strdup(realname), jb); + } + + return jb; +} + + +JabberBuddyResource *jabber_buddy_find_resource(JabberBuddy *jb, + const char *resource) +{ + JabberBuddyResource *jbr = NULL; + GList *l; + + if(!jb) + return NULL; + + for(l = jb->resources; l; l = l->next) + { + if(!jbr && !resource) { + jbr = l->data; + } else if(!resource) { + if(((JabberBuddyResource *)l->data)->priority >= jbr->priority) + jbr = l->data; + } else if(((JabberBuddyResource *)l->data)->name) { + if(!strcmp(((JabberBuddyResource *)l->data)->name, resource)) { + jbr = l->data; + break; + } + } + } + + return jbr; +} + +JabberBuddyResource *jabber_buddy_track_resource(JabberBuddy *jb, const char *resource, + int priority, JabberBuddyState state, const char *status) +{ + JabberBuddyResource *jbr = jabber_buddy_find_resource(jb, resource); + + if(!jbr) { + jbr = g_new0(JabberBuddyResource, 1); + jbr->jb = jb; + jbr->name = g_strdup(resource); + jbr->capabilities = JABBER_CAP_XHTML; + jb->resources = g_list_append(jb->resources, jbr); + } + jbr->priority = priority; + jbr->state = state; + if(jbr->status) + g_free(jbr->status); + if (status) + jbr->status = g_markup_escape_text(status, -1); + else + jbr->status = NULL; + + return jbr; +} + +void jabber_buddy_resource_free(JabberBuddyResource *jbr) +{ + g_return_if_fail(jbr != NULL); + + jbr->jb->resources = g_list_remove(jbr->jb->resources, jbr); + + g_free(jbr->name); + g_free(jbr->status); + g_free(jbr->thread_id); + g_free(jbr->client.name); + g_free(jbr->client.version); + g_free(jbr->client.os); + g_free(jbr); +} + +void jabber_buddy_remove_resource(JabberBuddy *jb, const char *resource) +{ + JabberBuddyResource *jbr = jabber_buddy_find_resource(jb, resource); + + if(!jbr) + return; + + jabber_buddy_resource_free(jbr); +} + +const char *jabber_buddy_get_status_msg(JabberBuddy *jb) +{ + JabberBuddyResource *jbr; + + if(!jb) + return NULL; + + jbr = jabber_buddy_find_resource(jb, NULL); + + if(!jbr) + return NULL; + + return jbr->status; +} + +/******* + * This is the old vCard stuff taken from the old prpl. vCards, by definition + * are a temporary thing until jabber can get its act together and come up + * with a format for user information, hence the namespace of 'vcard-temp' + * + * Since I don't feel like putting that much work into something that's + * _supposed_ to go away, i'm going to just copy the kludgy old code here, + * and make it purdy when jabber comes up with a standards-track JEP to + * replace vcard-temp + * --Nathan + *******/ + +/*---------------------------------------*/ +/* Jabber "set info" (vCard) support */ +/*---------------------------------------*/ + +/* + * V-Card format: + * + * <vCard prodid='' version='' xmlns=''> + * <FN></FN> + * <N> + * <FAMILY/> + * <GIVEN/> + * </N> + * <NICKNAME/> + * <URL/> + * <ADR> + * <STREET/> + * <EXTADD/> + * <LOCALITY/> + * <REGION/> + * <PCODE/> + * <COUNTRY/> + * </ADR> + * <TEL/> + * <EMAIL/> + * <ORG> + * <ORGNAME/> + * <ORGUNIT/> + * </ORG> + * <TITLE/> + * <ROLE/> + * <DESC/> + * <BDAY/> + * </vCard> + * + * See also: + * + * http://docs.jabber.org/proto/html/vcard-temp.html + * http://www.vcard-xml.org/dtd/vCard-XML-v2-20010520.dtd + */ + +/* + * Cross-reference user-friendly V-Card entry labels to vCard XML tags + * and attributes. + * + * Order is (or should be) unimportant. For example: we have no way of + * knowing in what order real data will arrive. + * + * Format: Label, Pre-set text, "visible" flag, "editable" flag, XML tag + * name, XML tag's parent tag "path" (relative to vCard node). + * + * List is terminated by a NULL label pointer. + * + * Entries with no label text, but with XML tag and parent tag + * entries, are used by V-Card XML construction routines to + * "automagically" construct the appropriate XML node tree. + * + * Thoughts on future direction/expansion + * + * This is a "simple" vCard. + * + * It is possible for nodes other than the "vCard" node to have + * attributes. Should that prove necessary/desirable, add an + * "attributes" pointer to the vcard_template struct, create the + * necessary tag_attr structs, and add 'em to the vcard_dflt_data + * array. + * + * The above changes will (obviously) require changes to the vCard + * construction routines. + */ + +struct vcard_template { + char *label; /* label text pointer */ + char *text; /* entry text pointer */ + int visible; /* should entry field be "visible?" */ + int editable; /* should entry field be editable? */ + char *tag; /* tag text */ + char *ptag; /* parent tag "path" text */ + char *url; /* vCard display format if URL */ +} vcard_template_data[] = { + {N_("Full Name"), NULL, TRUE, TRUE, "FN", NULL, NULL}, + {N_("Family Name"), NULL, TRUE, TRUE, "FAMILY", "N", NULL}, + {N_("Given Name"), NULL, TRUE, TRUE, "GIVEN", "N", NULL}, + {N_("Nickname"), NULL, TRUE, TRUE, "NICKNAME", NULL, NULL}, + {N_("URL"), NULL, TRUE, TRUE, "URL", NULL, "<A HREF=\"%s\">%s</A>"}, + {N_("Street Address"), NULL, TRUE, TRUE, "STREET", "ADR", NULL}, + {N_("Extended Address"), NULL, TRUE, TRUE, "EXTADD", "ADR", NULL}, + {N_("Locality"), NULL, TRUE, TRUE, "LOCALITY", "ADR", NULL}, + {N_("Region"), NULL, TRUE, TRUE, "REGION", "ADR", NULL}, + {N_("Postal Code"), NULL, TRUE, TRUE, "PCODE", "ADR", NULL}, + {N_("Country"), NULL, TRUE, TRUE, "CTRY", "ADR", NULL}, + {N_("Telephone"), NULL, TRUE, TRUE, "NUMBER", "TEL", NULL}, + {N_("E-Mail"), NULL, TRUE, TRUE, "USERID", "EMAIL", "<A HREF=\"mailto:%s\">%s</A>"}, + {N_("Organization Name"), NULL, TRUE, TRUE, "ORGNAME", "ORG", NULL}, + {N_("Organization Unit"), NULL, TRUE, TRUE, "ORGUNIT", "ORG", NULL}, + {N_("Title"), NULL, TRUE, TRUE, "TITLE", NULL, NULL}, + {N_("Role"), NULL, TRUE, TRUE, "ROLE", NULL, NULL}, + {N_("Birthday"), NULL, TRUE, TRUE, "BDAY", NULL, NULL}, + {N_("Description"), NULL, TRUE, TRUE, "DESC", NULL, NULL}, + {"", NULL, TRUE, TRUE, "N", NULL, NULL}, + {"", NULL, TRUE, TRUE, "ADR", NULL, NULL}, + {"", NULL, TRUE, TRUE, "ORG", NULL, NULL}, + {NULL, NULL, 0, 0, NULL, NULL, NULL} +}; + +/* + * The "vCard" tag's attribute list... + */ +struct tag_attr { + char *attr; + char *value; +} vcard_tag_attr_list[] = { + {"prodid", "-//HandGen//NONSGML vGen v1.0//EN"}, + {"version", "2.0", }, + {"xmlns", "vcard-temp", }, + {NULL, NULL}, +}; + + +/* + * Insert a tag node into an xmlnode tree, recursively inserting parent tag + * nodes as necessary + * + * Returns pointer to inserted node + * + * Note to hackers: this code is designed to be re-entrant (it's recursive--it + * calls itself), so don't put any "static"s in here! + */ +static xmlnode *insert_tag_to_parent_tag(xmlnode *start, const char *parent_tag, const char *new_tag) +{ + xmlnode *x = NULL; + + /* + * If the parent tag wasn't specified, see if we can get it + * from the vCard template struct. + */ + if(parent_tag == NULL) { + struct vcard_template *vc_tp = vcard_template_data; + + while(vc_tp->label != NULL) { + if(strcmp(vc_tp->tag, new_tag) == 0) { + parent_tag = vc_tp->ptag; + break; + } + ++vc_tp; + } + } + + /* + * If we have a parent tag... + */ + if(parent_tag != NULL ) { + /* + * Try to get the parent node for a tag + */ + if((x = xmlnode_get_child(start, parent_tag)) == NULL) { + /* + * Descend? + */ + char *grand_parent = g_strdup(parent_tag); + char *parent; + + if((parent = strrchr(grand_parent, '/')) != NULL) { + *(parent++) = '\0'; + x = insert_tag_to_parent_tag(start, grand_parent, parent); + } else { + x = xmlnode_new_child(start, grand_parent); + } + g_free(grand_parent); + } else { + /* + * We found *something* to be the parent node. + * Note: may be the "root" node! + */ + xmlnode *y; + if((y = xmlnode_get_child(x, new_tag)) != NULL) { + return(y); + } + } + } + + /* + * insert the new tag into its parent node + */ + return(xmlnode_new_child((x == NULL? start : x), new_tag)); +} + +/* + * Send vCard info to Jabber server + */ +void jabber_set_info(GaimConnection *gc, const char *info) +{ + JabberIq *iq; + JabberStream *js = gc->proto_data; + xmlnode *vc_node; + char *avatar_file = NULL; + struct tag_attr *tag_attr; + + g_free(js->avatar_hash); + js->avatar_hash = NULL; + + /* + * Send only if there's actually any *information* to send + */ + vc_node = info ? xmlnode_from_str(info, -1) : NULL; + avatar_file = gaim_buddy_icons_get_full_path(gaim_account_get_buddy_icon(gc->account)); + + if(!vc_node) { + vc_node = xmlnode_new("vCard"); + for(tag_attr = vcard_tag_attr_list; tag_attr->attr != NULL; ++tag_attr) + xmlnode_set_attrib(vc_node, tag_attr->attr, tag_attr->value); + } + + if (vc_node->name && + !g_ascii_strncasecmp(vc_node->name, "vCard", 5)) { + GError *error = NULL; + gchar *avatar_data_tmp; + guchar *avatar_data; + gsize avatar_len; + + if(avatar_file && g_file_get_contents(avatar_file, &avatar_data_tmp, &avatar_len, &error)) { + xmlnode *photo, *binval; + gchar *enc; + int i; + unsigned char hashval[20]; + char *p, hash[41]; + + avatar_data = (guchar *) avatar_data_tmp; + photo = xmlnode_new_child(vc_node, "PHOTO"); + binval = xmlnode_new_child(photo, "BINVAL"); + enc = gaim_base64_encode(avatar_data, avatar_len); + + gaim_cipher_digest_region("sha1", (guchar *)avatar_data, + avatar_len, sizeof(hashval), + hashval, NULL); + + p = hash; + for(i=0; i<20; i++, p+=2) + snprintf(p, 3, "%02x", hashval[i]); + js->avatar_hash = g_strdup(hash); + + xmlnode_insert_data(binval, enc, -1); + g_free(enc); + g_free(avatar_data); + } else if (error != NULL) { + g_error_free(error); + } + g_free(avatar_file); + + iq = jabber_iq_new(js, JABBER_IQ_SET); + xmlnode_insert_child(iq->node, vc_node); + jabber_iq_send(iq); + } else { + xmlnode_free(vc_node); + } +} + +void jabber_set_buddy_icon(GaimConnection *gc, const char *iconfile) +{ + GaimPresence *gpresence; + GaimStatus *status; + + jabber_set_info(gc, gaim_account_get_user_info(gc->account)); + + gpresence = gaim_account_get_presence(gc->account); + status = gaim_presence_get_active_status(gpresence); + jabber_presence_send(gc->account, status); +} + +/* + * This is the callback from the "ok clicked" for "set vCard" + * + * Formats GSList data into XML-encoded string and returns a pointer + * to said string. + * + * g_free()'ing the returned string space is the responsibility of + * the caller. + */ +static void +jabber_format_info(GaimConnection *gc, GaimRequestFields *fields) +{ + xmlnode *vc_node; + GaimRequestField *field; + const char *text; + char *p; + const struct vcard_template *vc_tp; + struct tag_attr *tag_attr; + + vc_node = xmlnode_new("vCard"); + + for(tag_attr = vcard_tag_attr_list; tag_attr->attr != NULL; ++tag_attr) + xmlnode_set_attrib(vc_node, tag_attr->attr, tag_attr->value); + + for (vc_tp = vcard_template_data; vc_tp->label != NULL; vc_tp++) { + if (*vc_tp->label == '\0') + continue; + + field = gaim_request_fields_get_field(fields, vc_tp->tag); + text = gaim_request_field_string_get_value(field); + + + if (text != NULL && *text != '\0') { + xmlnode *xp; + + gaim_debug(GAIM_DEBUG_INFO, "jabber", + "Setting %s to '%s'\n", vc_tp->tag, text); + + if ((xp = insert_tag_to_parent_tag(vc_node, + NULL, vc_tp->tag)) != NULL) { + + xmlnode_insert_data(xp, text, -1); + } + } + } + + p = xmlnode_to_str(vc_node, NULL); + xmlnode_free(vc_node); + + gaim_account_set_user_info(gaim_connection_get_account(gc), p); + serv_set_info(gc, p); + + g_free(p); +} + +/* + * This gets executed by the proto action + * + * Creates a new GaimRequestFields struct, gets the XML-formatted user_info + * string (if any) into GSLists for the (multi-entry) edit dialog and + * calls the set_vcard dialog. + */ +void jabber_setup_set_info(GaimPluginAction *action) +{ + GaimConnection *gc = (GaimConnection *) action->context; + GaimRequestFields *fields; + GaimRequestFieldGroup *group; + GaimRequestField *field; + const struct vcard_template *vc_tp; + const char *user_info; + char *cdata = NULL; + xmlnode *x_vc_data = NULL; + + fields = gaim_request_fields_new(); + group = gaim_request_field_group_new(NULL); + gaim_request_fields_add_group(fields, group); + + /* + * Get existing, XML-formatted, user info + */ + if((user_info = gaim_account_get_user_info(gc->account)) != NULL) + x_vc_data = xmlnode_from_str(user_info, -1); + + /* + * Set up GSLists for edit with labels from "template," data from user info + */ + for(vc_tp = vcard_template_data; vc_tp->label != NULL; ++vc_tp) { + xmlnode *data_node; + if((vc_tp->label)[0] == '\0') + continue; + + if (x_vc_data != NULL) { + if(vc_tp->ptag == NULL) { + data_node = xmlnode_get_child(x_vc_data, vc_tp->tag); + } else { + gchar *tag = g_strdup_printf("%s/%s", vc_tp->ptag, vc_tp->tag); + data_node = xmlnode_get_child(x_vc_data, tag); + g_free(tag); + } + if(data_node) + cdata = xmlnode_get_data(data_node); + } + + if(strcmp(vc_tp->tag, "DESC") == 0) { + field = gaim_request_field_string_new(vc_tp->tag, + _(vc_tp->label), cdata, + TRUE); + } else { + field = gaim_request_field_string_new(vc_tp->tag, + _(vc_tp->label), cdata, + FALSE); + } + + g_free(cdata); + cdata = NULL; + + gaim_request_field_group_add_field(group, field); + } + + if(x_vc_data != NULL) + xmlnode_free(x_vc_data); + + gaim_request_fields(gc, _("Edit Jabber vCard"), + _("Edit Jabber vCard"), + _("All items below are optional. Enter only the " + "information with which you feel comfortable."), + fields, + _("Save"), G_CALLBACK(jabber_format_info), + _("Cancel"), NULL, + gc); +} + +/*---------------------------------------*/ +/* End Jabber "set info" (vCard) support */ +/*---------------------------------------*/ + +/****** + * end of that ancient crap that needs to die + ******/ + +static void jabber_buddy_info_destroy(JabberBuddyInfo *jbi) +{ + /* Remove the timeout, which would otherwise trigger jabber_buddy_get_info_timeout() */ + if (jbi->timeout_handle > 0) + gaim_timeout_remove(jbi->timeout_handle); + + g_free(jbi->jid); + g_hash_table_destroy(jbi->resources); + g_free(jbi->vcard_text); + g_free(jbi); +} + +static void jabber_buddy_info_show_if_ready(JabberBuddyInfo *jbi) +{ + char *resource_name, *tmp; + JabberBuddyResource *jbr; + JabberBuddyInfoResource *jbir = NULL; + GList *resources; + GaimNotifyUserInfo *user_info; + + /* not yet */ + if(jbi->ids) + return; + + user_info = gaim_notify_user_info_new(); + resource_name = jabber_get_resource(jbi->jid); + + if(resource_name) { + jbr = jabber_buddy_find_resource(jbi->jb, resource_name); + jbir = g_hash_table_lookup(jbi->resources, resource_name); + if(jbr) { + char *purdy = NULL; + if(jbr->status) + purdy = gaim_strdup_withhtml(jbr->status); + tmp = g_strdup_printf("%s%s%s", jabber_buddy_state_get_name(jbr->state), + (purdy ? ": " : ""), + (purdy ? purdy : "")); + gaim_notify_user_info_add_pair(user_info, _("Status"), tmp); + g_free(tmp); + g_free(purdy); + } else { + gaim_notify_user_info_add_pair(user_info, _("Status"), _("Unknown")); + } + if(jbir) { + if(jbir->idle_seconds > 0) { + gaim_notify_user_info_add_pair(user_info, _("Idle"), gaim_str_seconds_to_string(jbir->idle_seconds)); + } + } + if(jbr && jbr->client.name) { + tmp = g_strdup_printf("%s%s%s", jbr->client.name, + (jbr->client.version ? " " : ""), + (jbr->client.version ? jbr->client.version : "")); + gaim_notify_user_info_add_pair(user_info, _("Client"), tmp); + g_free(tmp); + + if(jbr->client.os) { + gaim_notify_user_info_add_pair(user_info, _("Operating System"), jbr->client.os); + } + } + } else { + for(resources = jbi->jb->resources; resources; resources = resources->next) { + char *purdy = NULL; + jbr = resources->data; + if(jbr->status) + purdy = gaim_strdup_withhtml(jbr->status); + if(jbr->name) + gaim_notify_user_info_add_pair(user_info, _("Resource"), jbr->name); + tmp = g_strdup_printf("%d", jbr->priority); + gaim_notify_user_info_add_pair(user_info, _("Priority"), tmp); + g_free(tmp); + + tmp = g_strdup_printf("%s%s%s", jabber_buddy_state_get_name(jbr->state), + (purdy ? ": " : ""), + (purdy ? purdy : "")); + gaim_notify_user_info_add_pair(user_info, _("Status"), tmp); + g_free(tmp); + g_free(purdy); + + if(jbr->name) + jbir = g_hash_table_lookup(jbi->resources, jbr->name); + + if(jbir) { + if(jbir->idle_seconds > 0) { + gaim_notify_user_info_add_pair(user_info, _("Idle"), gaim_str_seconds_to_string(jbir->idle_seconds)); + } + } + if(jbr && jbr->client.name) { + tmp = g_strdup_printf("%s%s%s", jbr->client.name, + (jbr->client.version ? " " : ""), + (jbr->client.version ? jbr->client.version : "")); + gaim_notify_user_info_add_pair(user_info, + _("Client"), tmp); + g_free(tmp); + + if(jbr->client.os) { + gaim_notify_user_info_add_pair(user_info, _("Operating System"), jbr->client.os); + } + } + } + } + + g_free(resource_name); + + if (jbi->vcard_text != NULL) { + gaim_notify_user_info_add_section_break(user_info); + /* Should this have some sort of label? */ + gaim_notify_user_info_add_pair(user_info, NULL, jbi->vcard_text); + } + + gaim_notify_userinfo(jbi->js->gc, jbi->jid, user_info, NULL, NULL); + gaim_notify_user_info_destroy(user_info); + + while(jbi->vcard_imgids) { + gaim_imgstore_unref(GPOINTER_TO_INT(jbi->vcard_imgids->data)); + jbi->vcard_imgids = g_slist_delete_link(jbi->vcard_imgids, jbi->vcard_imgids); + } + + jbi->js->pending_buddy_info_requests = g_slist_remove(jbi->js->pending_buddy_info_requests, jbi); + + jabber_buddy_info_destroy(jbi); +} + +static void jabber_buddy_info_remove_id(JabberBuddyInfo *jbi, const char *id) +{ + GSList *l = jbi->ids; + + if(!id) + return; + + while(l) { + if(!strcmp(id, l->data)) { + jbi->ids = g_slist_remove(jbi->ids, l->data); + return; + } + l = l->next; + } +} + +static void jabber_vcard_parse(JabberStream *js, xmlnode *packet, gpointer data) +{ + const char *id, *from; + GString *info_text; + char *bare_jid; + char *text; + xmlnode *vcard; + GaimBuddy *b; + JabberBuddyInfo *jbi = data; + + from = xmlnode_get_attrib(packet, "from"); + id = xmlnode_get_attrib(packet, "id"); + + if(!jbi) + return; + + jabber_buddy_info_remove_id(jbi, id); + + if(!from) + return; + + if(!jabber_buddy_find(js, from, FALSE)) + return; + + /* XXX: handle the error case */ + + bare_jid = jabber_get_bare_jid(from); + + b = gaim_find_buddy(js->gc->account, bare_jid); + + info_text = g_string_new(""); + + if((vcard = xmlnode_get_child(packet, "vCard")) || + (vcard = xmlnode_get_child_with_namespace(packet, "query", "vcard-temp"))) { + xmlnode *child; + for(child = vcard->child; child; child = child->next) + { + xmlnode *child2; + + if(child->type != XMLNODE_TYPE_TAG) + continue; + + text = xmlnode_get_data(child); + if(text && !strcmp(child->name, "FN")) { + g_string_append_printf(info_text, "<b>%s:</b> %s<br/>", + _("Full Name"), text); + } else if(!strcmp(child->name, "N")) { + for(child2 = child->child; child2; child2 = child2->next) + { + char *text2; + + if(child2->type != XMLNODE_TYPE_TAG) + continue; + + text2 = xmlnode_get_data(child2); + if(text2 && !strcmp(child2->name, "FAMILY")) { + g_string_append_printf(info_text, + "<b>%s:</b> %s<br/>", + _("Family Name"), text2); + } else if(text2 && !strcmp(child2->name, "GIVEN")) { + g_string_append_printf(info_text, + "<b>%s:</b> %s<br/>", + _("Given Name"), text2); + } else if(text2 && !strcmp(child2->name, "MIDDLE")) { + g_string_append_printf(info_text, + "<b>%s:</b> %s<br/>", + _("Middle Name"), text2); + } + g_free(text2); + } + } else if(text && !strcmp(child->name, "NICKNAME")) { + serv_got_alias(js->gc, from, text); + if(b) { + gaim_blist_node_set_string((GaimBlistNode*)b, "servernick", text); + } + g_string_append_printf(info_text, "<b>%s:</b> %s<br/>", + _("Nickname"), text); + } else if(text && !strcmp(child->name, "BDAY")) { + g_string_append_printf(info_text, "<b>%s:</b> %s<br/>", + _("Birthday"), text); + } else if(!strcmp(child->name, "ADR")) { + gboolean address_line_added = FALSE; + + for(child2 = child->child; child2; child2 = child2->next) + { + char *text2; + + if(child2->type != XMLNODE_TYPE_TAG) + continue; + + text2 = xmlnode_get_data(child2); + if (text2 == NULL) + continue; + + /* We do this here so that it's not added if all the child + * elements are empty. */ + if (!address_line_added) + { + g_string_append_printf(info_text, "<b>%s:</b><br/>", + _("Address")); + address_line_added = TRUE; + } + + if(!strcmp(child2->name, "POBOX")) { + g_string_append_printf(info_text, + " <b>%s:</b> %s<br/>", + _("P.O. Box"), text2); + } else if(!strcmp(child2->name, "EXTADR")) { + g_string_append_printf(info_text, + " <b>%s:</b> %s<br/>", + _("Extended Address"), text2); + } else if(!strcmp(child2->name, "STREET")) { + g_string_append_printf(info_text, + " <b>%s:</b> %s<br/>", + _("Street Address"), text2); + } else if(!strcmp(child2->name, "LOCALITY")) { + g_string_append_printf(info_text, + " <b>%s:</b> %s<br/>", + _("Locality"), text2); + } else if(!strcmp(child2->name, "REGION")) { + g_string_append_printf(info_text, + " <b>%s:</b> %s<br/>", + _("Region"), text2); + } else if(!strcmp(child2->name, "PCODE")) { + g_string_append_printf(info_text, + " <b>%s:</b> %s<br/>", + _("Postal Code"), text2); + } else if(!strcmp(child2->name, "CTRY") + || !strcmp(child2->name, "COUNTRY")) { + g_string_append_printf(info_text, + " <b>%s:</b> %s<br/>", + _("Country"), text2); + } + g_free(text2); + } + } else if(!strcmp(child->name, "TEL")) { + char *number; + if((child2 = xmlnode_get_child(child, "NUMBER"))) { + /* show what kind of number it is */ + number = xmlnode_get_data(child2); + if(number) { + g_string_append_printf(info_text, + "<b>%s:</b> %s<br/>", _("Telephone"), number); + g_free(number); + } + } else if((number = xmlnode_get_data(child))) { + /* lots of clients (including gaim) do this, but it's + * out of spec */ + g_string_append_printf(info_text, + "<b>%s:</b> %s<br/>", _("Telephone"), number); + g_free(number); + } + } else if(!strcmp(child->name, "EMAIL")) { + char *userid; + if((child2 = xmlnode_get_child(child, "USERID"))) { + /* show what kind of email it is */ + userid = xmlnode_get_data(child2); + if(userid) { + g_string_append_printf(info_text, + "<b>%s:</b> <a href='mailto:%s'>%s</a><br/>", + _("E-Mail"), userid, userid); + g_free(userid); + } + } else if((userid = xmlnode_get_data(child))) { + /* lots of clients (including gaim) do this, but it's + * out of spec */ + g_string_append_printf(info_text, + "<b>%s:</b> <a href='mailto:%s'>%s</a><br/>", + _("E-Mail"), userid, userid); + g_free(userid); + } + } else if(!strcmp(child->name, "ORG")) { + for(child2 = child->child; child2; child2 = child2->next) + { + char *text2; + + if(child2->type != XMLNODE_TYPE_TAG) + continue; + + text2 = xmlnode_get_data(child2); + if(text2 && !strcmp(child2->name, "ORGNAME")) { + g_string_append_printf(info_text, + "<b>%s:</b> %s<br/>", + _("Organization Name"), text2); + } else if(text2 && !strcmp(child2->name, "ORGUNIT")) { + g_string_append_printf(info_text, + "<b>%s:</b> %s<br/>", + _("Organization Unit"), text2); + } + g_free(text2); + } + } else if(text && !strcmp(child->name, "TITLE")) { + g_string_append_printf(info_text, "<b>%s:</b> %s<br/>", + _("Title"), text); + } else if(text && !strcmp(child->name, "ROLE")) { + g_string_append_printf(info_text, "<b>%s:</b> %s<br/>", + _("Role"), text); + } else if(text && !strcmp(child->name, "DESC")) { + g_string_append_printf(info_text, "<b>%s:</b> %s<br/>", + _("Description"), text); + } else if(!strcmp(child->name, "PHOTO") || + !strcmp(child->name, "LOGO")) { + char *bintext = NULL; + xmlnode *binval; + + if( ((binval = xmlnode_get_child(child, "BINVAL")) && + (bintext = xmlnode_get_data(binval))) || + (bintext = xmlnode_get_data(child))) { + gsize size; + guchar *data; + int i; + unsigned char hashval[20]; + char *p, hash[41]; + gboolean photo = (strcmp(child->name, "PHOTO") == 0); + + data = gaim_base64_decode(bintext, &size); + + jbi->vcard_imgids = g_slist_prepend(jbi->vcard_imgids, GINT_TO_POINTER(gaim_imgstore_add(data, size, "logo.png"))); + g_string_append_printf(info_text, + "<b>%s:</b> <img id='%d'><br/>", + photo ? _("Photo") : _("Logo"), + GPOINTER_TO_INT(jbi->vcard_imgids->data)); + + gaim_buddy_icons_set_for_user(js->gc->account, bare_jid, + data, size); + + gaim_cipher_digest_region("sha1", (guchar *)data, size, + sizeof(hashval), hashval, NULL); + p = hash; + for(i=0; i<20; i++, p+=2) + snprintf(p, 3, "%02x", hashval[i]); + gaim_blist_node_set_string((GaimBlistNode*)b, "avatar_hash", hash); + + g_free(data); + g_free(bintext); + } + } + g_free(text); + } + } + + jbi->vcard_text = gaim_strdup_withhtml(info_text->str); + g_string_free(info_text, TRUE); + g_free(bare_jid); + + jabber_buddy_info_show_if_ready(jbi); +} + + +static void jabber_buddy_info_resource_free(gpointer data) +{ + JabberBuddyInfoResource *jbri = data; + g_free(jbri); +} + +static void jabber_version_parse(JabberStream *js, xmlnode *packet, gpointer data) +{ + JabberBuddyInfo *jbi = data; + const char *type, *id, *from; + xmlnode *query; + char *resource_name; + + g_return_if_fail(jbi != NULL); + + type = xmlnode_get_attrib(packet, "type"); + id = xmlnode_get_attrib(packet, "id"); + from = xmlnode_get_attrib(packet, "from"); + + jabber_buddy_info_remove_id(jbi, id); + + if(!from) + return; + + resource_name = jabber_get_resource(from); + + if(resource_name) { + if(type && !strcmp(type, "result")) { + if((query = xmlnode_get_child(packet, "query"))) { + JabberBuddyResource *jbr = jabber_buddy_find_resource(jbi->jb, resource_name); + if(jbr) { + xmlnode *node; + if((node = xmlnode_get_child(query, "name"))) { + jbr->client.name = xmlnode_get_data(node); + } + if((node = xmlnode_get_child(query, "version"))) { + jbr->client.version = xmlnode_get_data(node); + } + if((node = xmlnode_get_child(query, "os"))) { + jbr->client.os = xmlnode_get_data(node); + } + } + } + } + g_free(resource_name); + } + + jabber_buddy_info_show_if_ready(jbi); +} + +static void jabber_last_parse(JabberStream *js, xmlnode *packet, gpointer data) +{ + JabberBuddyInfo *jbi = data; + xmlnode *query; + char *resource_name; + const char *type, *id, *from, *seconds; + + g_return_if_fail(jbi != NULL); + + type = xmlnode_get_attrib(packet, "type"); + id = xmlnode_get_attrib(packet, "id"); + from = xmlnode_get_attrib(packet, "from"); + + jabber_buddy_info_remove_id(jbi, id); + + if(!from) + return; + + resource_name = jabber_get_resource(from); + + if(resource_name) { + if(type && !strcmp(type, "result")) { + if((query = xmlnode_get_child(packet, "query"))) { + seconds = xmlnode_get_attrib(query, "seconds"); + if(seconds) { + char *end = NULL; + long sec = strtol(seconds, &end, 10); + if(end != seconds) { + JabberBuddyInfoResource *jbir = g_hash_table_lookup(jbi->resources, resource_name); + if(jbir) { + jbir->idle_seconds = sec; + } + } + } + } + } + g_free(resource_name); + } + + jabber_buddy_info_show_if_ready(jbi); +} + +void jabber_buddy_remove_all_pending_buddy_info_requests(JabberStream *js) +{ + if (js->pending_buddy_info_requests) + { + JabberBuddyInfo *jbi; + GSList *l = js->pending_buddy_info_requests; + while (l) { + jbi = l->data; + + g_slist_free(jbi->ids); + jabber_buddy_info_destroy(jbi); + + l = l->next; + } + + g_slist_free(js->pending_buddy_info_requests); + js->pending_buddy_info_requests = NULL; + } +} + +static gboolean jabber_buddy_get_info_timeout(gpointer data) +{ + JabberBuddyInfo *jbi = data; + + /* remove the pending callbacks */ + while(jbi->ids) { + char *id = jbi->ids->data; + jabber_iq_remove_callback_by_id(jbi->js, id); + jbi->ids = g_slist_remove(jbi->ids, id); + g_free(id); + } + + jbi->js->pending_buddy_info_requests = g_slist_remove(jbi->js->pending_buddy_info_requests, jbi); + jbi->timeout_handle = 0; + + jabber_buddy_info_show_if_ready(jbi); + + return FALSE; +} + +static void jabber_buddy_get_info_for_jid(JabberStream *js, const char *jid) +{ + JabberIq *iq; + xmlnode *vcard; + GList *resources; + JabberBuddy *jb; + JabberBuddyInfo *jbi; + + jb = jabber_buddy_find(js, jid, TRUE); + + /* invalid JID */ + if(!jb) + return; + + jbi = g_new0(JabberBuddyInfo, 1); + jbi->jid = g_strdup(jid); + jbi->js = js; + jbi->jb = jb; + jbi->resources = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, jabber_buddy_info_resource_free); + + iq = jabber_iq_new(js, JABBER_IQ_GET); + + xmlnode_set_attrib(iq->node, "to", jid); + vcard = xmlnode_new_child(iq->node, "vCard"); + xmlnode_set_namespace(vcard, "vcard-temp"); + + jabber_iq_set_callback(iq, jabber_vcard_parse, jbi); + jbi->ids = g_slist_prepend(jbi->ids, g_strdup(iq->id)); + + jabber_iq_send(iq); + + for(resources = jb->resources; resources; resources = resources->next) + { + JabberBuddyResource *jbr = resources->data; + JabberBuddyInfoResource *jbir; + char *full_jid; + + if ((strchr(jid, '/') == NULL) && (jbr->name != NULL)) { + full_jid = g_strdup_printf("%s/%s", jid, jbr->name); + } else { + full_jid = g_strdup(jid); + } + + if (jbr->name != NULL) + { + jbir = g_new0(JabberBuddyInfoResource, 1); + g_hash_table_insert(jbi->resources, g_strdup(jbr->name), jbir); + } + + if(!jbr->client.name) { + iq = jabber_iq_new_query(js, JABBER_IQ_GET, "jabber:iq:version"); + xmlnode_set_attrib(iq->node, "to", full_jid); + jabber_iq_set_callback(iq, jabber_version_parse, jbi); + jbi->ids = g_slist_prepend(jbi->ids, g_strdup(iq->id)); + jabber_iq_send(iq); + } + + iq = jabber_iq_new_query(js, JABBER_IQ_GET, "jabber:iq:last"); + xmlnode_set_attrib(iq->node, "to", full_jid); + jabber_iq_set_callback(iq, jabber_last_parse, jbi); + jbi->ids = g_slist_prepend(jbi->ids, g_strdup(iq->id)); + jabber_iq_send(iq); + + g_free(full_jid); + } + + js->pending_buddy_info_requests = g_slist_prepend(js->pending_buddy_info_requests, jbi); + jbi->timeout_handle = gaim_timeout_add(30000, jabber_buddy_get_info_timeout, jbi); +} + +void jabber_buddy_get_info(GaimConnection *gc, const char *who) +{ + JabberStream *js = gc->proto_data; + char *bare_jid = jabber_get_bare_jid(who); + + if(bare_jid) { + jabber_buddy_get_info_for_jid(js, bare_jid); + g_free(bare_jid); + } +} + +void jabber_buddy_get_info_chat(GaimConnection *gc, int id, + const char *resource) +{ + JabberStream *js = gc->proto_data; + JabberChat *chat = jabber_chat_find_by_id(js, id); + char *full_jid; + + if(!chat) + return; + + full_jid = g_strdup_printf("%s@%s/%s", chat->room, chat->server, resource); + jabber_buddy_get_info_for_jid(js, full_jid); + g_free(full_jid); +} + + +static void jabber_buddy_set_invisibility(JabberStream *js, const char *who, + gboolean invisible) +{ + GaimPresence *gpresence; + GaimAccount *account; + GaimStatus *status; + JabberBuddy *jb = jabber_buddy_find(js, who, TRUE); + xmlnode *presence; + JabberBuddyState state; + char *msg; + int priority; + + account = gaim_connection_get_account(js->gc); + gpresence = gaim_account_get_presence(account); + status = gaim_presence_get_active_status(gpresence); + + gaim_status_to_jabber(status, &state, &msg, &priority); + presence = jabber_presence_create(state, msg, priority); + + g_free(msg); + + xmlnode_set_attrib(presence, "to", who); + if(invisible) { + xmlnode_set_attrib(presence, "type", "invisible"); + jb->invisible |= JABBER_INVIS_BUDDY; + } else { + jb->invisible &= ~JABBER_INVIS_BUDDY; + } + + jabber_send(js, presence); + xmlnode_free(presence); +} + +static void jabber_buddy_make_invisible(GaimBlistNode *node, gpointer data) +{ + GaimBuddy *buddy; + GaimConnection *gc; + JabberStream *js; + + g_return_if_fail(GAIM_BLIST_NODE_IS_BUDDY(node)); + + buddy = (GaimBuddy *) node; + gc = gaim_account_get_connection(buddy->account); + js = gc->proto_data; + + jabber_buddy_set_invisibility(js, buddy->name, TRUE); +} + +static void jabber_buddy_make_visible(GaimBlistNode *node, gpointer data) +{ + GaimBuddy *buddy; + GaimConnection *gc; + JabberStream *js; + + g_return_if_fail(GAIM_BLIST_NODE_IS_BUDDY(node)); + + buddy = (GaimBuddy *) node; + gc = gaim_account_get_connection(buddy->account); + js = gc->proto_data; + + jabber_buddy_set_invisibility(js, buddy->name, FALSE); +} + +static void jabber_buddy_cancel_presence_notification(GaimBlistNode *node, + gpointer data) +{ + GaimBuddy *buddy; + GaimConnection *gc; + JabberStream *js; + + g_return_if_fail(GAIM_BLIST_NODE_IS_BUDDY(node)); + + buddy = (GaimBuddy *) node; + gc = gaim_account_get_connection(buddy->account); + js = gc->proto_data; + + /* I wonder if we should prompt the user before doing this */ + jabber_presence_subscription_set(js, buddy->name, "unsubscribed"); +} + +static void jabber_buddy_rerequest_auth(GaimBlistNode *node, gpointer data) +{ + GaimBuddy *buddy; + GaimConnection *gc; + JabberStream *js; + + g_return_if_fail(GAIM_BLIST_NODE_IS_BUDDY(node)); + + buddy = (GaimBuddy *) node; + gc = gaim_account_get_connection(buddy->account); + js = gc->proto_data; + + jabber_presence_subscription_set(js, buddy->name, "subscribe"); +} + + +static void jabber_buddy_unsubscribe(GaimBlistNode *node, gpointer data) +{ + GaimBuddy *buddy; + GaimConnection *gc; + JabberStream *js; + + g_return_if_fail(GAIM_BLIST_NODE_IS_BUDDY(node)); + + buddy = (GaimBuddy *) node; + gc = gaim_account_get_connection(buddy->account); + js = gc->proto_data; + + jabber_presence_subscription_set(js, buddy->name, "unsubscribe"); +} + + +static GList *jabber_buddy_menu(GaimBuddy *buddy) +{ + GaimConnection *gc = gaim_account_get_connection(buddy->account); + JabberStream *js = gc->proto_data; + JabberBuddy *jb = jabber_buddy_find(js, buddy->name, TRUE); + + GList *m = NULL; + GaimMenuAction *act; + + if(!jb) + return m; + + /* XXX: fix the NOT ME below */ + + if(js->protocol_version == JABBER_PROTO_0_9 /* && NOT ME */) { + if(jb->invisible & JABBER_INVIS_BUDDY) { + act = gaim_menu_action_new(_("Un-hide From"), + GAIM_CALLBACK(jabber_buddy_make_visible), + NULL, NULL); + } else { + act = gaim_menu_action_new(_("Temporarily Hide From"), + GAIM_CALLBACK(jabber_buddy_make_invisible), + NULL, NULL); + } + m = g_list_append(m, act); + } + + if(jb->subscription & JABBER_SUB_FROM /* && NOT ME */) { + act = gaim_menu_action_new(_("Cancel Presence Notification"), + GAIM_CALLBACK(jabber_buddy_cancel_presence_notification), + NULL, NULL); + m = g_list_append(m, act); + } + + if(!(jb->subscription & JABBER_SUB_TO)) { + act = gaim_menu_action_new(_("(Re-)Request authorization"), + GAIM_CALLBACK(jabber_buddy_rerequest_auth), + NULL, NULL); + m = g_list_append(m, act); + + } else /* if(NOT ME) */{ + + /* shouldn't this just happen automatically when the buddy is + removed? */ + act = gaim_menu_action_new(_("Unsubscribe"), + GAIM_CALLBACK(jabber_buddy_unsubscribe), + NULL, NULL); + m = g_list_append(m, act); + } + + return m; +} + +GList * +jabber_blist_node_menu(GaimBlistNode *node) +{ + if(GAIM_BLIST_NODE_IS_BUDDY(node)) { + return jabber_buddy_menu((GaimBuddy *) node); + } else { + return NULL; + } +} + + +const char * +jabber_buddy_state_get_name(JabberBuddyState state) +{ + switch(state) { + case JABBER_BUDDY_STATE_UNKNOWN: + return _("Unknown"); + case JABBER_BUDDY_STATE_ERROR: + return _("Error"); + case JABBER_BUDDY_STATE_UNAVAILABLE: + return _("Offline"); + case JABBER_BUDDY_STATE_ONLINE: + return _("Available"); + case JABBER_BUDDY_STATE_CHAT: + return _("Chatty"); + case JABBER_BUDDY_STATE_AWAY: + return _("Away"); + case JABBER_BUDDY_STATE_XA: + return _("Extended Away"); + case JABBER_BUDDY_STATE_DND: + return _("Do Not Disturb"); + } + + return _("Unknown"); +} + +JabberBuddyState jabber_buddy_status_id_get_state(const char *id) { + if(!id) + return JABBER_BUDDY_STATE_UNKNOWN; + if(!strcmp(id, "available")) + return JABBER_BUDDY_STATE_ONLINE; + if(!strcmp(id, "freeforchat")) + return JABBER_BUDDY_STATE_CHAT; + if(!strcmp(id, "away")) + return JABBER_BUDDY_STATE_AWAY; + if(!strcmp(id, "extended_away")) + return JABBER_BUDDY_STATE_XA; + if(!strcmp(id, "dnd")) + return JABBER_BUDDY_STATE_DND; + if(!strcmp(id, "offline")) + return JABBER_BUDDY_STATE_UNAVAILABLE; + if(!strcmp(id, "error")) + return JABBER_BUDDY_STATE_ERROR; + + return JABBER_BUDDY_STATE_UNKNOWN; +} + +JabberBuddyState jabber_buddy_show_get_state(const char *id) { + if(!id) + return JABBER_BUDDY_STATE_UNKNOWN; + if(!strcmp(id, "available")) + return JABBER_BUDDY_STATE_ONLINE; + if(!strcmp(id, "chat")) + return JABBER_BUDDY_STATE_CHAT; + if(!strcmp(id, "away")) + return JABBER_BUDDY_STATE_AWAY; + if(!strcmp(id, "xa")) + return JABBER_BUDDY_STATE_XA; + if(!strcmp(id, "dnd")) + return JABBER_BUDDY_STATE_DND; + if(!strcmp(id, "offline")) + return JABBER_BUDDY_STATE_UNAVAILABLE; + if(!strcmp(id, "error")) + return JABBER_BUDDY_STATE_ERROR; + + return JABBER_BUDDY_STATE_UNKNOWN; +} + +const char *jabber_buddy_state_get_show(JabberBuddyState state) { + switch(state) { + case JABBER_BUDDY_STATE_CHAT: + return "chat"; + case JABBER_BUDDY_STATE_AWAY: + return "away"; + case JABBER_BUDDY_STATE_XA: + return "xa"; + case JABBER_BUDDY_STATE_DND: + return "dnd"; + case JABBER_BUDDY_STATE_ONLINE: + return "available"; + case JABBER_BUDDY_STATE_UNKNOWN: + case JABBER_BUDDY_STATE_ERROR: + return NULL; + case JABBER_BUDDY_STATE_UNAVAILABLE: + return "offline"; + } + return NULL; +} + +const char *jabber_buddy_state_get_status_id(JabberBuddyState state) { + switch(state) { + case JABBER_BUDDY_STATE_CHAT: + return "freeforchat"; + case JABBER_BUDDY_STATE_AWAY: + return "away"; + case JABBER_BUDDY_STATE_XA: + return "extended_away"; + case JABBER_BUDDY_STATE_DND: + return "dnd"; + case JABBER_BUDDY_STATE_ONLINE: + return "available"; + case JABBER_BUDDY_STATE_UNKNOWN: + return "available"; + case JABBER_BUDDY_STATE_ERROR: + return "error"; + case JABBER_BUDDY_STATE_UNAVAILABLE: + return "offline"; + } + return NULL; +} + +static void user_search_result_add_buddy_cb(GaimConnection *gc, GList *row, void *user_data) +{ + /* XXX find out the jid */ + gaim_blist_request_add_buddy(gaim_connection_get_account(gc), + g_list_nth_data(row, 0), NULL, NULL); +} + +static void user_search_result_cb(JabberStream *js, xmlnode *packet, gpointer data) +{ + GaimNotifySearchResults *results; + GaimNotifySearchColumn *column; + xmlnode *x, *query, *item, *field; + + /* XXX error checking? */ + if(!(query = xmlnode_get_child(packet, "query"))) + return; + + results = gaim_notify_searchresults_new(); + if((x = xmlnode_get_child_with_namespace(query, "x", "jabber:x:data"))) { + xmlnode *reported; + gaim_debug_info("jabber", "new-skool\n"); + if((reported = xmlnode_get_child(x, "reported"))) { + xmlnode *field = xmlnode_get_child(reported, "field"); + while(field) { + /* XXX keep track of this order, use it below */ + const char *var = xmlnode_get_attrib(field, "var"); + const char *label = xmlnode_get_attrib(field, "label"); + if(var) { + column = gaim_notify_searchresults_column_new(label ? label : var); + gaim_notify_searchresults_column_add(results, column); + } + field = xmlnode_get_next_twin(field); + } + } + item = xmlnode_get_child(x, "item"); + while(item) { + GList *row = NULL; + field = xmlnode_get_child(item, "field"); + while(field) { + xmlnode *valuenode = xmlnode_get_child(field, "value"); + if(valuenode) { + char *value = xmlnode_get_data(valuenode); + row = g_list_append(row, value); + } + field = xmlnode_get_next_twin(field); + } + gaim_notify_searchresults_row_add(results, row); + + item = xmlnode_get_next_twin(item); + } + } else { + /* old skool */ + gaim_debug_info("jabber", "old-skool\n"); + + column = gaim_notify_searchresults_column_new(_("JID")); + gaim_notify_searchresults_column_add(results, column); + column = gaim_notify_searchresults_column_new(_("First Name")); + gaim_notify_searchresults_column_add(results, column); + column = gaim_notify_searchresults_column_new(_("Last Name")); + gaim_notify_searchresults_column_add(results, column); + column = gaim_notify_searchresults_column_new(_("Nickname")); + gaim_notify_searchresults_column_add(results, column); + column = gaim_notify_searchresults_column_new(_("E-Mail")); + gaim_notify_searchresults_column_add(results, column); + + for(item = xmlnode_get_child(query, "item"); item; item = xmlnode_get_next_twin(item)) { + const char *jid; + xmlnode *node; + GList *row = NULL; + + if(!(jid = xmlnode_get_attrib(item, "jid"))) + continue; + + row = g_list_append(row, g_strdup(jid)); + node = xmlnode_get_child(item, "first"); + row = g_list_append(row, node ? xmlnode_get_data(node) : NULL); + node = xmlnode_get_child(item, "last"); + row = g_list_append(row, node ? xmlnode_get_data(node) : NULL); + node = xmlnode_get_child(item, "nick"); + row = g_list_append(row, node ? xmlnode_get_data(node) : NULL); + node = xmlnode_get_child(item, "email"); + row = g_list_append(row, node ? xmlnode_get_data(node) : NULL); + gaim_debug_info("jabber", "row=%d\n", row); + gaim_notify_searchresults_row_add(results, row); + } + } + + gaim_notify_searchresults_button_add(results, GAIM_NOTIFY_BUTTON_ADD, + user_search_result_add_buddy_cb); + + gaim_notify_searchresults(js->gc, NULL, NULL, _("The following are the results of your search"), results, NULL, NULL); +} + +static void user_search_x_data_cb(JabberStream *js, xmlnode *result, gpointer data) +{ + xmlnode *query; + JabberIq *iq; + char *dir_server = data; + + iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:search"); + query = xmlnode_get_child(iq->node, "query"); + + xmlnode_insert_child(query, result); + + jabber_iq_set_callback(iq, user_search_result_cb, NULL); + xmlnode_set_attrib(iq->node, "to", dir_server); + jabber_iq_send(iq); + g_free(dir_server); +} + +struct user_search_info { + JabberStream *js; + char *directory_server; +}; + +static void user_search_cancel_cb(struct user_search_info *usi, GaimRequestFields *fields) +{ + g_free(usi->directory_server); + g_free(usi); +} + +static void user_search_cb(struct user_search_info *usi, GaimRequestFields *fields) +{ + JabberStream *js = usi->js; + JabberIq *iq; + xmlnode *query; + GList *groups, *flds; + + iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:search"); + query = xmlnode_get_child(iq->node, "query"); + + for(groups = gaim_request_fields_get_groups(fields); groups; groups = groups->next) { + for(flds = gaim_request_field_group_get_fields(groups->data); + flds; flds = flds->next) { + GaimRequestField *field = flds->data; + const char *id = gaim_request_field_get_id(field); + const char *value = gaim_request_field_string_get_value(field); + + if(value && (!strcmp(id, "first") || !strcmp(id, "last") || !strcmp(id, "nick") || !strcmp(id, "email"))) { + xmlnode *y = xmlnode_new_child(query, id); + xmlnode_insert_data(y, value, -1); + } + } + } + + jabber_iq_set_callback(iq, user_search_result_cb, NULL); + xmlnode_set_attrib(iq->node, "to", usi->directory_server); + jabber_iq_send(iq); + + g_free(usi->directory_server); + g_free(usi); +} + +#if 0 +/* This is for gettext only -- it will see this even though there's an #if 0. */ + +/* + * An incomplete list of server generated original language search + * comments for Jabber User Directories + * + * See discussion thread "Search comment for Jabber is not translatable" + * in gaim-i18n@lists.sourceforge.net (March 2006) + */ +static const char * jabber_user_dir_comments [] = { + /* current comment from Jabber User Directory users.jabber.org */ + N_("Find a contact by entering the search criteria in the given fields. " + "Note: Each field supports wild card searches (%)"), + NULL +}; +#endif + +static void user_search_fields_result_cb(JabberStream *js, xmlnode *packet, gpointer data) +{ + xmlnode *query, *x; + const char *from, *type; + + if(!(from = xmlnode_get_attrib(packet, "from"))) + return; + + if(!(type = xmlnode_get_attrib(packet, "type")) || !strcmp(type, "error")) { + char *msg = jabber_parse_error(js, packet); + + if(!msg) + msg = g_strdup(_("Unknown error")); + + gaim_notify_error(js->gc, _("Directory Query Failed"), + _("Could not query the directory server."), msg); + g_free(msg); + + return; + } + + + if(!(query = xmlnode_get_child(packet, "query"))) + return; + + if((x = xmlnode_get_child_with_namespace(query, "x", "jabber:x:data"))) { + jabber_x_data_request(js, x, user_search_x_data_cb, g_strdup(from)); + return; + } else { + struct user_search_info *usi; + xmlnode *instnode; + char *instructions = NULL; + GaimRequestFields *fields; + GaimRequestFieldGroup *group; + GaimRequestField *field; + + /* old skool */ + fields = gaim_request_fields_new(); + group = gaim_request_field_group_new(NULL); + gaim_request_fields_add_group(fields, group); + + if((instnode = xmlnode_get_child(query, "instructions"))) + { + char *tmp = xmlnode_get_data(instnode); + + if(tmp) + { + /* Try to translate the message (see static message + list in jabber_user_dir_comments[]) */ + instructions = g_strdup_printf(_("Server Instructions: %s"), _(tmp)); + g_free(tmp); + } + } + + if(!instructions) + { + instructions = g_strdup(_("Fill in one or more fields to search " + "for any matching Jabber users.")); + } + + if(xmlnode_get_child(query, "first")) { + field = gaim_request_field_string_new("first", _("First Name"), + NULL, FALSE); + gaim_request_field_group_add_field(group, field); + } + if(xmlnode_get_child(query, "last")) { + field = gaim_request_field_string_new("last", _("Last Name"), + NULL, FALSE); + gaim_request_field_group_add_field(group, field); + } + if(xmlnode_get_child(query, "nick")) { + field = gaim_request_field_string_new("nick", _("Nickname"), + NULL, FALSE); + gaim_request_field_group_add_field(group, field); + } + if(xmlnode_get_child(query, "email")) { + field = gaim_request_field_string_new("email", _("E-Mail Address"), + NULL, FALSE); + gaim_request_field_group_add_field(group, field); + } + + usi = g_new0(struct user_search_info, 1); + usi->js = js; + usi->directory_server = g_strdup(from); + + gaim_request_fields(js->gc, _("Search for Jabber users"), + _("Search for Jabber users"), instructions, fields, + _("Search"), G_CALLBACK(user_search_cb), + _("Cancel"), G_CALLBACK(user_search_cancel_cb), usi); + + g_free(instructions); + } +} + +static void jabber_user_search_ok(JabberStream *js, const char *directory) +{ + JabberIq *iq; + + /* XXX: should probably better validate the directory we're given */ + if(!directory || !*directory) { + gaim_notify_error(js->gc, _("Invalid Directory"), _("Invalid Directory"), NULL); + return; + } + + iq = jabber_iq_new_query(js, JABBER_IQ_GET, "jabber:iq:search"); + xmlnode_set_attrib(iq->node, "to", directory); + + jabber_iq_set_callback(iq, user_search_fields_result_cb, NULL); + + jabber_iq_send(iq); +} + +void jabber_user_search_begin(GaimPluginAction *action) +{ + GaimConnection *gc = (GaimConnection *) action->context; + JabberStream *js = gc->proto_data; + + gaim_request_input(gc, _("Enter a User Directory"), _("Enter a User Directory"), + _("Select a user directory to search"), + js->user_directories ? js->user_directories->data : "users.jabber.org", + FALSE, FALSE, NULL, + _("Search Directory"), GAIM_CALLBACK(jabber_user_search_ok), + _("Cancel"), NULL, js); +} + + + diff --git a/libpurple/protocols/jabber/buddy.h b/libpurple/protocols/jabber/buddy.h new file mode 100644 index 0000000000..db2b628aca --- /dev/null +++ b/libpurple/protocols/jabber/buddy.h @@ -0,0 +1,106 @@ +/** + * @file buddy.h Buddy handlers + * + * gaim + * + * Copyright (C) 2003 Nathan Walp <faceprint@faceprint.com> + * + * 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 + */ +#ifndef _GAIM_JABBER_BUDDY_H_ +#define _GAIM_JABBER_BUDDY_H_ + +#include "jabber.h" + +typedef enum { + JABBER_BUDDY_STATE_UNKNOWN = -2, + JABBER_BUDDY_STATE_ERROR = -1, + JABBER_BUDDY_STATE_UNAVAILABLE = 0, + JABBER_BUDDY_STATE_ONLINE, + JABBER_BUDDY_STATE_CHAT, + JABBER_BUDDY_STATE_AWAY, + JABBER_BUDDY_STATE_XA, + JABBER_BUDDY_STATE_DND +} JabberBuddyState; + +typedef struct _JabberBuddy { + GList *resources; + char *error_msg; + enum { + JABBER_INVISIBLE_NONE = 0, + JABBER_INVISIBLE_SERVER = 1 << 1, + JABBER_INVIS_BUDDY = 1 << 2 + } invisible; + enum { + JABBER_SUB_NONE = 0, + JABBER_SUB_PENDING = 1 << 1, + JABBER_SUB_TO = 1 << 2, + JABBER_SUB_FROM = 1 << 3, + JABBER_SUB_BOTH = (JABBER_SUB_TO | JABBER_SUB_FROM), + JABBER_SUB_REMOVE = 1 << 4 + } subscription; +} JabberBuddy; + +typedef struct _JabberBuddyResource { + JabberBuddy *jb; + char *name; + int priority; + JabberBuddyState state; + char *status; + JabberCapabilities capabilities; + char *thread_id; + enum { + JABBER_CHAT_STATES_UNKNOWN, + JABBER_CHAT_STATES_UNSUPPORTED, + JABBER_CHAT_STATES_SUPPORTED + } chat_states; + struct { + char *version; + char *name; + char *os; + } client; +} JabberBuddyResource; + +void jabber_buddy_free(JabberBuddy *jb); +JabberBuddy *jabber_buddy_find(JabberStream *js, const char *name, + gboolean create); +JabberBuddyResource *jabber_buddy_find_resource(JabberBuddy *jb, + const char *resource); +JabberBuddyResource *jabber_buddy_track_resource(JabberBuddy *jb, const char *resource, + int priority, JabberBuddyState state, const char *status); +void jabber_buddy_resource_free(JabberBuddyResource *jbr); +void jabber_buddy_remove_resource(JabberBuddy *jb, const char *resource); +const char *jabber_buddy_get_status_msg(JabberBuddy *jb); +void jabber_buddy_get_info(GaimConnection *gc, const char *who); +void jabber_buddy_get_info_chat(GaimConnection *gc, int id, + const char *resource); + +GList *jabber_blist_node_menu(GaimBlistNode *node); + +void jabber_set_info(GaimConnection *gc, const char *info); +void jabber_setup_set_info(GaimPluginAction *action); +void jabber_set_buddy_icon(GaimConnection *gc, const char *iconfile); + +const char *jabber_buddy_state_get_name(JabberBuddyState state); +const char *jabber_buddy_state_get_status_id(JabberBuddyState state); +const char *jabber_buddy_state_get_show(JabberBuddyState state); +JabberBuddyState jabber_buddy_status_id_get_state(const char *id); +JabberBuddyState jabber_buddy_show_get_state(const char *id); + +void jabber_user_search_begin(GaimPluginAction *); + +void jabber_buddy_remove_all_pending_buddy_info_requests(JabberStream *js); + +#endif /* _GAIM_JABBER_BUDDY_H_ */ diff --git a/libpurple/protocols/jabber/chat.c b/libpurple/protocols/jabber/chat.c new file mode 100644 index 0000000000..7346a15aa3 --- /dev/null +++ b/libpurple/protocols/jabber/chat.c @@ -0,0 +1,1013 @@ +/* + * gaim - Jabber Protocol Plugin + * + * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com> + * + * 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 + * + */ +#include "internal.h" +#include "debug.h" +#include "prpl.h" /* for proto_chat_entry */ +#include "notify.h" +#include "request.h" +#include "roomlist.h" +#include "util.h" + +#include "chat.h" +#include "iq.h" +#include "message.h" +#include "presence.h" +#include "xdata.h" + +GList *jabber_chat_info(GaimConnection *gc) +{ + GList *m = NULL; + struct proto_chat_entry *pce; + + pce = g_new0(struct proto_chat_entry, 1); + pce->label = _("_Room:"); + pce->identifier = "room"; + pce->required = TRUE; + m = g_list_append(m, pce); + + pce = g_new0(struct proto_chat_entry, 1); + pce->label = _("_Server:"); + pce->identifier = "server"; + pce->required = TRUE; + m = g_list_append(m, pce); + + pce = g_new0(struct proto_chat_entry, 1); + pce->label = _("_Handle:"); + pce->identifier = "handle"; + pce->required = TRUE; + m = g_list_append(m, pce); + + pce = g_new0(struct proto_chat_entry, 1); + pce->label = _("_Password:"); + pce->identifier = "password"; + pce->secret = TRUE; + m = g_list_append(m, pce); + + return m; +} + +GHashTable *jabber_chat_info_defaults(GaimConnection *gc, const char *chat_name) +{ + GHashTable *defaults; + JabberStream *js = gc->proto_data; + + defaults = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free); + + g_hash_table_insert(defaults, "handle", g_strdup(js->user->node)); + + if (js->chat_servers) + g_hash_table_insert(defaults, "server", g_strdup(js->chat_servers->data)); + else + g_hash_table_insert(defaults, "server", g_strdup("conference.jabber.org")); + + if (chat_name != NULL) { + JabberID *jid = jabber_id_new(chat_name); + if(jid) { + g_hash_table_insert(defaults, "room", g_strdup(jid->node)); + if(jid->domain) + g_hash_table_replace(defaults, "server", g_strdup(jid->domain)); + if(jid->resource) + g_hash_table_replace(defaults, "handle", g_strdup(jid->resource)); + jabber_id_free(jid); + } + } + + return defaults; +} + +JabberChat *jabber_chat_find(JabberStream *js, const char *room, + const char *server) +{ + JabberChat *chat = NULL; + char *room_jid; + + if(NULL != js->chats) + { + room_jid = g_strdup_printf("%s@%s", room, server); + + chat = g_hash_table_lookup(js->chats, jabber_normalize(NULL, room_jid)); + g_free(room_jid); + } + + return chat; +} + +struct _find_by_id_data { + int id; + JabberChat *chat; +}; + +static void find_by_id_foreach_cb(gpointer key, gpointer value, gpointer user_data) +{ + JabberChat *chat = value; + struct _find_by_id_data *fbid = user_data; + + if(chat->id == fbid->id) + fbid->chat = chat; +} + +JabberChat *jabber_chat_find_by_id(JabberStream *js, int id) +{ + JabberChat *chat; + struct _find_by_id_data *fbid = g_new0(struct _find_by_id_data, 1); + fbid->id = id; + g_hash_table_foreach(js->chats, find_by_id_foreach_cb, fbid); + chat = fbid->chat; + g_free(fbid); + return chat; +} + +JabberChat *jabber_chat_find_by_conv(GaimConversation *conv) +{ + GaimAccount *account = gaim_conversation_get_account(conv); + GaimConnection *gc = gaim_account_get_connection(account); + JabberStream *js = gc->proto_data; + int id = gaim_conv_chat_get_id(GAIM_CONV_CHAT(conv)); + + return jabber_chat_find_by_id(js, id); +} + +void jabber_chat_invite(GaimConnection *gc, int id, const char *msg, + const char *name) +{ + JabberStream *js = gc->proto_data; + JabberChat *chat; + xmlnode *message, *body, *x, *invite; + char *room_jid; + + chat = jabber_chat_find_by_id(js, id); + if(!chat) + return; + + message = xmlnode_new("message"); + + room_jid = g_strdup_printf("%s@%s", chat->room, chat->server); + + if(chat->muc) { + xmlnode_set_attrib(message, "to", room_jid); + x = xmlnode_new_child(message, "x"); + xmlnode_set_namespace(x, "http://jabber.org/protocol/muc#user"); + invite = xmlnode_new_child(x, "invite"); + xmlnode_set_attrib(invite, "to", name); + body = xmlnode_new_child(invite, "reason"); + xmlnode_insert_data(body, msg, -1); + } else { + xmlnode_set_attrib(message, "to", name); + body = xmlnode_new_child(message, "body"); + xmlnode_insert_data(body, msg, -1); + x = xmlnode_new_child(message, "x"); + xmlnode_set_attrib(x, "jid", room_jid); + xmlnode_set_namespace(x, "jabber:x:conference"); + } + + jabber_send(js, message); + xmlnode_free(message); + g_free(room_jid); +} + +void jabber_chat_member_free(JabberChatMember *jcm); + +char *jabber_get_chat_name(GHashTable *data) { + char *room, *server, *chat_name = NULL; + + room = g_hash_table_lookup(data, "room"); + server = g_hash_table_lookup(data, "server"); + + if (room && server) { + chat_name = g_strdup_printf("%s@%s", room, server); + } + return chat_name; +} + +void jabber_chat_join(GaimConnection *gc, GHashTable *data) +{ + JabberChat *chat; + char *room, *server, *handle, *passwd; + xmlnode *presence, *x; + char *tmp, *room_jid, *full_jid; + JabberStream *js = gc->proto_data; + GaimPresence *gpresence; + GaimStatus *status; + JabberBuddyState state; + char *msg; + int priority; + + room = g_hash_table_lookup(data, "room"); + server = g_hash_table_lookup(data, "server"); + handle = g_hash_table_lookup(data, "handle"); + passwd = g_hash_table_lookup(data, "password"); + + if(!room || !server) + return; + + if(!handle) + handle = js->user->node; + + if(!jabber_nodeprep_validate(room)) { + char *buf = g_strdup_printf(_("%s is not a valid room name"), room); + gaim_notify_error(gc, _("Invalid Room Name"), _("Invalid Room Name"), + buf); + g_free(buf); + return; + } else if(!jabber_nameprep_validate(server)) { + char *buf = g_strdup_printf(_("%s is not a valid server name"), server); + gaim_notify_error(gc, _("Invalid Server Name"), + _("Invalid Server Name"), buf); + g_free(buf); + return; + } else if(!jabber_resourceprep_validate(handle)) { + char *buf = g_strdup_printf(_("%s is not a valid room handle"), handle); + gaim_notify_error(gc, _("Invalid Room Handle"), + _("Invalid Room Handle"), buf); + } + + if(jabber_chat_find(js, room, server)) + return; + + tmp = g_strdup_printf("%s@%s", room, server); + room_jid = g_strdup(jabber_normalize(NULL, tmp)); + g_free(tmp); + + chat = g_new0(JabberChat, 1); + chat->js = gc->proto_data; + + chat->room = g_strdup(room); + chat->server = g_strdup(server); + chat->handle = g_strdup(handle); + + chat->members = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, + (GDestroyNotify)jabber_chat_member_free); + + g_hash_table_insert(js->chats, room_jid, chat); + + gpresence = gaim_account_get_presence(gc->account); + status = gaim_presence_get_active_status(gpresence); + + gaim_status_to_jabber(status, &state, &msg, &priority); + + presence = jabber_presence_create(state, msg, priority); + full_jid = g_strdup_printf("%s/%s", room_jid, handle); + xmlnode_set_attrib(presence, "to", full_jid); + g_free(full_jid); + g_free(msg); + + x = xmlnode_new_child(presence, "x"); + xmlnode_set_namespace(x, "http://jabber.org/protocol/muc"); + + if(passwd && *passwd) { + xmlnode *password = xmlnode_new_child(x, "password"); + xmlnode_insert_data(password, passwd, -1); + } + + jabber_send(js, presence); + xmlnode_free(presence); +} + +void jabber_chat_leave(GaimConnection *gc, int id) +{ + JabberStream *js = gc->proto_data; + JabberChat *chat = jabber_chat_find_by_id(js, id); + + + if(!chat) + return; + + jabber_chat_part(chat, NULL); + + chat->conv = NULL; +} + +void jabber_chat_destroy(JabberChat *chat) +{ + JabberStream *js = chat->js; + char *room_jid = g_strdup_printf("%s@%s", chat->room, chat->server); + + g_hash_table_remove(js->chats, jabber_normalize(NULL, room_jid)); + g_free(room_jid); +} + +void jabber_chat_free(JabberChat *chat) +{ + if(chat->config_dialog_handle) + gaim_request_close(chat->config_dialog_type, chat->config_dialog_handle); + + g_free(chat->room); + g_free(chat->server); + g_free(chat->handle); + g_hash_table_destroy(chat->members); + g_free(chat); +} + +gboolean jabber_chat_find_buddy(GaimConversation *conv, const char *name) +{ + return gaim_conv_chat_find_user(GAIM_CONV_CHAT(conv), name); +} + +char *jabber_chat_buddy_real_name(GaimConnection *gc, int id, const char *who) +{ + JabberStream *js = gc->proto_data; + JabberChat *chat; + + chat = jabber_chat_find_by_id(js, id); + + if(!chat) + return NULL; + + return g_strdup_printf("%s@%s/%s", chat->room, chat->server, who); +} + +static void jabber_chat_room_configure_x_data_cb(JabberStream *js, xmlnode *result, gpointer data) +{ + JabberChat *chat = data; + xmlnode *query; + JabberIq *iq; + char *to = g_strdup_printf("%s@%s", chat->room, chat->server); + + iq = jabber_iq_new_query(js, JABBER_IQ_SET, "http://jabber.org/protocol/muc#owner"); + xmlnode_set_attrib(iq->node, "to", to); + g_free(to); + + query = xmlnode_get_child(iq->node, "query"); + + xmlnode_insert_child(query, result); + + jabber_iq_send(iq); +} + +static void jabber_chat_room_configure_cb(JabberStream *js, xmlnode *packet, gpointer data) +{ + xmlnode *query, *x; + const char *type = xmlnode_get_attrib(packet, "type"); + const char *from = xmlnode_get_attrib(packet, "from"); + char *msg; + JabberChat *chat; + JabberID *jid; + + if(!type || !from) + return; + + + + if(!strcmp(type, "result")) { + jid = jabber_id_new(from); + + if(!jid) + return; + + chat = jabber_chat_find(js, jid->node, jid->domain); + jabber_id_free(jid); + + if(!chat) + return; + + if(!(query = xmlnode_get_child(packet, "query"))) + return; + + for(x = xmlnode_get_child(query, "x"); x; x = xmlnode_get_next_twin(x)) { + const char *xmlns; + if(!(xmlns = xmlnode_get_namespace(x))) + continue; + + if(!strcmp(xmlns, "jabber:x:data")) { + chat->config_dialog_type = GAIM_REQUEST_FIELDS; + chat->config_dialog_handle = jabber_x_data_request(js, x, jabber_chat_room_configure_x_data_cb, chat); + return; + } + } + } else if(!strcmp(type, "error")) { + char *msg = jabber_parse_error(js, packet); + + gaim_notify_error(js->gc, _("Configuration error"), _("Configuration error"), msg); + + if(msg) + g_free(msg); + return; + } + + msg = g_strdup_printf("Unable to configure room %s", from); + + gaim_notify_info(js->gc, _("Unable to configure"), _("Unable to configure"), msg); + g_free(msg); + +} + +void jabber_chat_request_room_configure(JabberChat *chat) { + JabberIq *iq; + char *room_jid; + + if(!chat) + return; + + chat->config_dialog_handle = NULL; + + if(!chat->muc) { + gaim_notify_error(chat->js->gc, _("Room Configuration Error"), _("Room Configuration Error"), + _("This room is not capable of being configured")); + return; + } + + iq = jabber_iq_new_query(chat->js, JABBER_IQ_GET, + "http://jabber.org/protocol/muc#owner"); + room_jid = g_strdup_printf("%s@%s", chat->room, chat->server); + + xmlnode_set_attrib(iq->node, "to", room_jid); + + jabber_iq_set_callback(iq, jabber_chat_room_configure_cb, NULL); + + jabber_iq_send(iq); + + g_free(room_jid); +} + +void jabber_chat_create_instant_room(JabberChat *chat) { + JabberIq *iq; + xmlnode *query, *x; + char *room_jid; + + if(!chat) + return; + + chat->config_dialog_handle = NULL; + + iq = jabber_iq_new_query(chat->js, JABBER_IQ_SET, + "http://jabber.org/protocol/muc#owner"); + query = xmlnode_get_child(iq->node, "query"); + x = xmlnode_new_child(query, "x"); + room_jid = g_strdup_printf("%s@%s", chat->room, chat->server); + + xmlnode_set_attrib(iq->node, "to", room_jid); + xmlnode_set_namespace(x, "jabber:x:data"); + xmlnode_set_attrib(x, "type", "submit"); + + jabber_iq_send(iq); + + g_free(room_jid); +} + +static void jabber_chat_register_x_data_result_cb(JabberStream *js, xmlnode *packet, gpointer data) +{ + const char *type = xmlnode_get_attrib(packet, "type"); + + if(type && !strcmp(type, "error")) { + char *msg = jabber_parse_error(js, packet); + + gaim_notify_error(js->gc, _("Registration error"), _("Registration error"), msg); + + if(msg) + g_free(msg); + return; + } +} + +static void jabber_chat_register_x_data_cb(JabberStream *js, xmlnode *result, gpointer data) +{ + JabberChat *chat = data; + xmlnode *query; + JabberIq *iq; + char *to = g_strdup_printf("%s@%s", chat->room, chat->server); + + iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:register"); + xmlnode_set_attrib(iq->node, "to", to); + g_free(to); + + query = xmlnode_get_child(iq->node, "query"); + + xmlnode_insert_child(query, result); + + jabber_iq_set_callback(iq, jabber_chat_register_x_data_result_cb, NULL); + + jabber_iq_send(iq); +} + +static void jabber_chat_register_cb(JabberStream *js, xmlnode *packet, gpointer data) +{ + xmlnode *query, *x; + const char *type = xmlnode_get_attrib(packet, "type"); + const char *from = xmlnode_get_attrib(packet, "from"); + char *msg; + JabberChat *chat; + JabberID *jid; + + if(!type || !from) + return; + + if(!strcmp(type, "result")) { + jid = jabber_id_new(from); + + if(!jid) + return; + + chat = jabber_chat_find(js, jid->node, jid->domain); + jabber_id_free(jid); + + if(!chat) + return; + + if(!(query = xmlnode_get_child(packet, "query"))) + return; + + for(x = xmlnode_get_child(query, "x"); x; x = xmlnode_get_next_twin(x)) { + const char *xmlns; + + if(!(xmlns = xmlnode_get_namespace(x))) + continue; + + if(!strcmp(xmlns, "jabber:x:data")) { + jabber_x_data_request(js, x, jabber_chat_register_x_data_cb, chat); + return; + } + } + } else if(!strcmp(type, "error")) { + char *msg = jabber_parse_error(js, packet); + + gaim_notify_error(js->gc, _("Registration error"), _("Registration error"), msg); + + if(msg) + g_free(msg); + return; + } + + msg = g_strdup_printf("Unable to configure room %s", from); + + gaim_notify_info(js->gc, _("Unable to configure"), _("Unable to configure"), msg); + g_free(msg); + +} + +void jabber_chat_register(JabberChat *chat) +{ + JabberIq *iq; + char *room_jid; + + if(!chat) + return; + + room_jid = g_strdup_printf("%s@%s", chat->room, chat->server); + + iq = jabber_iq_new_query(chat->js, JABBER_IQ_GET, "jabber:iq:register"); + xmlnode_set_attrib(iq->node, "to", room_jid); + g_free(room_jid); + + jabber_iq_set_callback(iq, jabber_chat_register_cb, NULL); + + jabber_iq_send(iq); +} + +/* merge this with the function below when we get everyone on the same page wrt /commands */ +void jabber_chat_change_topic(JabberChat *chat, const char *topic) +{ + if(topic && *topic) { + JabberMessage *jm; + jm = g_new0(JabberMessage, 1); + jm->js = chat->js; + jm->type = JABBER_MESSAGE_GROUPCHAT; + jm->subject = gaim_markup_strip_html(topic); + jm->to = g_strdup_printf("%s@%s", chat->room, chat->server); + jabber_message_send(jm); + jabber_message_free(jm); + } else { + const char *cur = gaim_conv_chat_get_topic(GAIM_CONV_CHAT(chat->conv)); + char *buf, *tmp, *tmp2; + + if(cur) { + tmp = g_markup_escape_text(cur, -1); + tmp2 = gaim_markup_linkify(tmp); + buf = g_strdup_printf(_("current topic is: %s"), tmp2); + g_free(tmp); + g_free(tmp2); + } else + buf = g_strdup(_("No topic is set")); + gaim_conv_chat_write(GAIM_CONV_CHAT(chat->conv), "", buf, + GAIM_MESSAGE_SYSTEM | GAIM_MESSAGE_NO_LOG, time(NULL)); + g_free(buf); + } + +} + +void jabber_chat_set_topic(GaimConnection *gc, int id, const char *topic) +{ + JabberStream *js = gc->proto_data; + JabberChat *chat = jabber_chat_find_by_id(js, id); + + if(!chat) + return; + + jabber_chat_change_topic(chat, topic); +} + + +void jabber_chat_change_nick(JabberChat *chat, const char *nick) +{ + xmlnode *presence; + char *full_jid; + GaimPresence *gpresence; + GaimStatus *status; + JabberBuddyState state; + char *msg; + int priority; + + if(!chat->muc) { + gaim_conv_chat_write(GAIM_CONV_CHAT(chat->conv), "", + _("Nick changing not supported in non-MUC chatrooms"), + GAIM_MESSAGE_SYSTEM, time(NULL)); + return; + } + + gpresence = gaim_account_get_presence(chat->js->gc->account); + status = gaim_presence_get_active_status(gpresence); + + gaim_status_to_jabber(status, &state, &msg, &priority); + + presence = jabber_presence_create(state, msg, priority); + full_jid = g_strdup_printf("%s@%s/%s", chat->room, chat->server, nick); + xmlnode_set_attrib(presence, "to", full_jid); + g_free(full_jid); + g_free(msg); + + jabber_send(chat->js, presence); + xmlnode_free(presence); +} + +void jabber_chat_part(JabberChat *chat, const char *msg) +{ + char *room_jid; + xmlnode *presence; + + room_jid = g_strdup_printf("%s@%s/%s", chat->room, chat->server, + chat->handle); + presence = xmlnode_new("presence"); + xmlnode_set_attrib(presence, "to", room_jid); + xmlnode_set_attrib(presence, "type", "unavailable"); + if(msg) { + xmlnode *status = xmlnode_new_child(presence, "status"); + xmlnode_insert_data(status, msg, -1); + } + jabber_send(chat->js, presence); + xmlnode_free(presence); + g_free(room_jid); +} + +static void roomlist_disco_result_cb(JabberStream *js, xmlnode *packet, gpointer data) +{ + xmlnode *query; + xmlnode *item; + const char *type; + + if(!js->roomlist) + return; + + if(!(type = xmlnode_get_attrib(packet, "type")) || strcmp(type, "result")) { + char *err = jabber_parse_error(js,packet); + gaim_notify_error(js->gc, _("Error"), + _("Error retrieving room list"), err); + gaim_roomlist_set_in_progress(js->roomlist, FALSE); + gaim_roomlist_unref(js->roomlist); + js->roomlist = NULL; + g_free(err); + return; + } + + if(!(query = xmlnode_get_child(packet, "query"))) { + char *err = jabber_parse_error(js, packet); + gaim_notify_error(js->gc, _("Error"), + _("Error retrieving room list"), err); + gaim_roomlist_set_in_progress(js->roomlist, FALSE); + gaim_roomlist_unref(js->roomlist); + js->roomlist = NULL; + g_free(err); + return; + } + + for(item = xmlnode_get_child(query, "item"); item; + item = xmlnode_get_next_twin(item)) { + const char *name; + GaimRoomlistRoom *room; + JabberID *jid; + + if(!(jid = jabber_id_new(xmlnode_get_attrib(item, "jid")))) + continue; + name = xmlnode_get_attrib(item, "name"); + + + room = gaim_roomlist_room_new(GAIM_ROOMLIST_ROOMTYPE_ROOM, jid->node, NULL); + gaim_roomlist_room_add_field(js->roomlist, room, jid->node); + gaim_roomlist_room_add_field(js->roomlist, room, jid->domain); + gaim_roomlist_room_add_field(js->roomlist, room, name ? name : ""); + gaim_roomlist_room_add(js->roomlist, room); + + jabber_id_free(jid); + } + gaim_roomlist_set_in_progress(js->roomlist, FALSE); + gaim_roomlist_unref(js->roomlist); + js->roomlist = NULL; +} + +static void roomlist_cancel_cb(JabberStream *js, const char *server) { + if(js->roomlist) { + gaim_roomlist_set_in_progress(js->roomlist, FALSE); + gaim_roomlist_unref(js->roomlist); + js->roomlist = NULL; + } +} + +static void roomlist_ok_cb(JabberStream *js, const char *server) +{ + JabberIq *iq; + + if(!js->roomlist) + return; + + if(!server || !*server) { + gaim_notify_error(js->gc, _("Invalid Server"), _("Invalid Server"), NULL); + return; + } + + gaim_roomlist_set_in_progress(js->roomlist, TRUE); + + iq = jabber_iq_new_query(js, JABBER_IQ_GET, "http://jabber.org/protocol/disco#items"); + + xmlnode_set_attrib(iq->node, "to", server); + + jabber_iq_set_callback(iq, roomlist_disco_result_cb, NULL); + + jabber_iq_send(iq); +} + +char *jabber_roomlist_room_serialize(GaimRoomlistRoom *room) +{ + + return g_strdup_printf("%s@%s", (char*)room->fields->data, (char*)room->fields->next->data); +} + +GaimRoomlist *jabber_roomlist_get_list(GaimConnection *gc) +{ + JabberStream *js = gc->proto_data; + GList *fields = NULL; + GaimRoomlistField *f; + + if(js->roomlist) + gaim_roomlist_unref(js->roomlist); + + js->roomlist = gaim_roomlist_new(gaim_connection_get_account(js->gc)); + + f = gaim_roomlist_field_new(GAIM_ROOMLIST_FIELD_STRING, "", "room", TRUE); + fields = g_list_append(fields, f); + + f = gaim_roomlist_field_new(GAIM_ROOMLIST_FIELD_STRING, "", "server", TRUE); + fields = g_list_append(fields, f); + + f = gaim_roomlist_field_new(GAIM_ROOMLIST_FIELD_STRING, _("Description"), "description", FALSE); + fields = g_list_append(fields, f); + + gaim_roomlist_set_fields(js->roomlist, fields); + + + gaim_request_input(gc, _("Enter a Conference Server"), _("Enter a Conference Server"), + _("Select a conference server to query"), + js->chat_servers ? js->chat_servers->data : "conference.jabber.org", + FALSE, FALSE, NULL, + _("Find Rooms"), GAIM_CALLBACK(roomlist_ok_cb), + _("Cancel"), GAIM_CALLBACK(roomlist_cancel_cb), js); + + return js->roomlist; +} + +void jabber_roomlist_cancel(GaimRoomlist *list) +{ + GaimConnection *gc; + JabberStream *js; + + gc = gaim_account_get_connection(list->account); + js = gc->proto_data; + + gaim_roomlist_set_in_progress(list, FALSE); + + if (js->roomlist == list) { + js->roomlist = NULL; + gaim_roomlist_unref(list); + } +} + +void jabber_chat_member_free(JabberChatMember *jcm) +{ + g_free(jcm->handle); + g_free(jcm->jid); + g_free(jcm); +} + +void jabber_chat_track_handle(JabberChat *chat, const char *handle, + const char *jid, const char *affiliation, const char *role) +{ + JabberChatMember *jcm = g_new0(JabberChatMember, 1); + + jcm->handle = g_strdup(handle); + jcm->jid = g_strdup(jid); + + g_hash_table_replace(chat->members, jcm->handle, jcm); + + /* XXX: keep track of role and affiliation */ +} + +void jabber_chat_remove_handle(JabberChat *chat, const char *handle) +{ + g_hash_table_remove(chat->members, handle); +} + +gboolean jabber_chat_ban_user(JabberChat *chat, const char *who, const char *why) +{ + JabberIq *iq; + JabberChatMember *jcm = g_hash_table_lookup(chat->members, who); + char *to; + xmlnode *query, *item, *reason; + + if(!jcm || !jcm->jid) + return FALSE; + + iq = jabber_iq_new_query(chat->js, JABBER_IQ_SET, + "http://jabber.org/protocol/muc#admin"); + + to = g_strdup_printf("%s@%s", chat->room, chat->server); + xmlnode_set_attrib(iq->node, "to", to); + g_free(to); + + query = xmlnode_get_child(iq->node, "query"); + item = xmlnode_new_child(query, "item"); + xmlnode_set_attrib(item, "jid", jcm->jid); + xmlnode_set_attrib(item, "affiliation", "outcast"); + if(why) { + reason = xmlnode_new_child(item, "reason"); + xmlnode_insert_data(reason, why, -1); + } + + jabber_iq_send(iq); + + return TRUE; +} + +gboolean jabber_chat_affiliate_user(JabberChat *chat, const char *who, const char *affiliation) +{ + char *to; + JabberIq *iq; + xmlnode *query, *item; + JabberChatMember *jcm; + + jcm = g_hash_table_lookup(chat->members, who); + + if (!jcm || !jcm->jid) + return FALSE; + + iq = jabber_iq_new_query(chat->js, JABBER_IQ_SET, + "http://jabber.org/protocol/muc#admin"); + + to = g_strdup_printf("%s@%s", chat->room, chat->server); + xmlnode_set_attrib(iq->node, "to", to); + g_free(to); + + query = xmlnode_get_child(iq->node, "query"); + item = xmlnode_new_child(query, "item"); + xmlnode_set_attrib(item, "jid", jcm->jid); + xmlnode_set_attrib(item, "affiliation", affiliation); + + jabber_iq_send(iq); + + return TRUE; +} + +gboolean jabber_chat_role_user(JabberChat *chat, const char *who, const char *role) +{ + char *to; + JabberIq *iq; + xmlnode *query, *item; + JabberChatMember *jcm; + + jcm = g_hash_table_lookup(chat->members, who); + + if (!jcm || !jcm->handle) + return FALSE; + + iq = jabber_iq_new_query(chat->js, JABBER_IQ_SET, + "http://jabber.org/protocol/muc#admin"); + + to = g_strdup_printf("%s@%s", chat->room, chat->server); + xmlnode_set_attrib(iq->node, "to", to); + g_free(to); + + query = xmlnode_get_child(iq->node, "query"); + item = xmlnode_new_child(query, "item"); + xmlnode_set_attrib(item, "nick", jcm->handle); + xmlnode_set_attrib(item, "role", role); + + jabber_iq_send(iq); + + return TRUE; +} + +gboolean jabber_chat_kick_user(JabberChat *chat, const char *who, const char *why) +{ + JabberIq *iq; + JabberChatMember *jcm = g_hash_table_lookup(chat->members, who); + char *to; + xmlnode *query, *item, *reason; + + if(!jcm || !jcm->jid) + return FALSE; + + iq = jabber_iq_new_query(chat->js, JABBER_IQ_SET, + "http://jabber.org/protocol/muc#admin"); + + to = g_strdup_printf("%s@%s", chat->room, chat->server); + xmlnode_set_attrib(iq->node, "to", to); + g_free(to); + + query = xmlnode_get_child(iq->node, "query"); + item = xmlnode_new_child(query, "item"); + xmlnode_set_attrib(item, "jid", jcm->jid); + xmlnode_set_attrib(item, "role", "none"); + if(why) { + reason = xmlnode_new_child(item, "reason"); + xmlnode_insert_data(reason, why, -1); + } + + jabber_iq_send(iq); + + return TRUE; +} + +static void jabber_chat_disco_traffic_cb(JabberStream *js, xmlnode *packet, gpointer data) +{ + JabberChat *chat; + xmlnode *query; + int id = GPOINTER_TO_INT(data); + + if(!(chat = jabber_chat_find_by_id(js, id))) + return; + + /* defaults, in case the conference server doesn't + * support this request */ + chat->xhtml = TRUE; + + if(xmlnode_get_child(packet, "error")) { + return; + } + + if(!(query = xmlnode_get_child(packet, "query"))) + return; + + /* disabling this until more MUC servers support + * announcing this + chat->xhtml = FALSE; + + for(x = xmlnode_get_child(query, "feature"); x; x = xmlnode_get_next_twin(x)) { + const char *var = xmlnode_get_attrib(x, "var"); + + if(var && !strcmp(var, "http://jabber.org/protocol/xhtml-im")) { + chat->xhtml = TRUE; + } + } + */ +} + +void jabber_chat_disco_traffic(JabberChat *chat) +{ + JabberIq *iq; + xmlnode *query; + char *room_jid; + + room_jid = g_strdup_printf("%s@%s", chat->room, chat->server); + + iq = jabber_iq_new_query(chat->js, JABBER_IQ_GET, + "http://jabber.org/protocol/disco#info"); + + xmlnode_set_attrib(iq->node, "to", room_jid); + + query = xmlnode_get_child(iq->node, "query"); + + xmlnode_set_attrib(query, "node", "http://jabber.org/protocol/muc#traffic"); + + jabber_iq_set_callback(iq, jabber_chat_disco_traffic_cb, GINT_TO_POINTER(chat->id)); + + jabber_iq_send(iq); + + g_free(room_jid); +} + + + diff --git a/libpurple/protocols/jabber/chat.h b/libpurple/protocols/jabber/chat.h new file mode 100644 index 0000000000..150aa1918f --- /dev/null +++ b/libpurple/protocols/jabber/chat.h @@ -0,0 +1,95 @@ +/** + * @file chat.h Chat stuff + * + * gaim + * + * Copyright (C) 2003 Nathan Walp <faceprint@faceprint.com> + * + * 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 + */ +#ifndef _GAIM_JABBER_CHAT_H_ +#define _GAIM_JABBER_CHAT_H_ + +#include "internal.h" +#include "connection.h" +#include "conversation.h" +#include "request.h" +#include "roomlist.h" + +#include "jabber.h" + +typedef struct _JabberChatMember { + char *handle; + char *jid; +} JabberChatMember; + + +typedef struct _JabberChat { + JabberStream *js; + char *room; + char *server; + char *handle; + int id; + GaimConversation *conv; + gboolean muc; + gboolean xhtml; + GaimRequestType config_dialog_type; + void *config_dialog_handle; + GHashTable *members; +} JabberChat; + +GList *jabber_chat_info(GaimConnection *gc); +GHashTable *jabber_chat_info_defaults(GaimConnection *gc, const char *chat_name); +char *jabber_get_chat_name(GHashTable *data); +void jabber_chat_join(GaimConnection *gc, GHashTable *data); +JabberChat *jabber_chat_find(JabberStream *js, const char *room, + const char *server); +JabberChat *jabber_chat_find_by_id(JabberStream *js, int id); +JabberChat *jabber_chat_find_by_conv(GaimConversation *conv); +void jabber_chat_destroy(JabberChat *chat); +void jabber_chat_free(JabberChat *chat); +gboolean jabber_chat_find_buddy(GaimConversation *conv, const char *name); +void jabber_chat_invite(GaimConnection *gc, int id, const char *message, + const char *name); +void jabber_chat_leave(GaimConnection *gc, int id); +char *jabber_chat_buddy_real_name(GaimConnection *gc, int id, const char *who); +void jabber_chat_request_room_configure(JabberChat *chat); +void jabber_chat_create_instant_room(JabberChat *chat); +void jabber_chat_register(JabberChat *chat); +void jabber_chat_change_topic(JabberChat *chat, const char *topic); +void jabber_chat_set_topic(GaimConnection *gc, int id, const char *topic); +void jabber_chat_change_nick(JabberChat *chat, const char *nick); +void jabber_chat_part(JabberChat *chat, const char *msg); +void jabber_chat_track_handle(JabberChat *chat, const char *handle, + const char *jid, const char *affiliation, const char *role); +void jabber_chat_remove_handle(JabberChat *chat, const char *handle); +gboolean jabber_chat_ban_user(JabberChat *chat, const char *who, + const char *why); +gboolean jabber_chat_affiliate_user(JabberChat *chat, const char *who, + const char *affiliation); +gboolean jabber_chat_role_user(JabberChat *chat, const char *who, + const char *role); +gboolean jabber_chat_kick_user(JabberChat *chat, const char *who, + const char *why); + +GaimRoomlist *jabber_roomlist_get_list(GaimConnection *gc); +void jabber_roomlist_cancel(GaimRoomlist *list); + +void jabber_chat_disco_traffic(JabberChat *chat); + +char *jabber_roomlist_room_serialize(GaimRoomlistRoom *room); + + +#endif /* _GAIM_JABBER_CHAT_H_ */ diff --git a/libpurple/protocols/jabber/disco.c b/libpurple/protocols/jabber/disco.c new file mode 100644 index 0000000000..905a6d6791 --- /dev/null +++ b/libpurple/protocols/jabber/disco.c @@ -0,0 +1,384 @@ +/* + * gaim - Jabber Protocol Plugin + * + * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com> + * + * 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 + * + */ + +#include "internal.h" +#include "prefs.h" +#include "debug.h" + +#include "buddy.h" +#include "google.h" +#include "iq.h" +#include "disco.h" +#include "jabber.h" +#include "presence.h" +#include "roster.h" + +struct _jabber_disco_info_cb_data { + gpointer data; + JabberDiscoInfoCallback *callback; +}; + +#define SUPPORT_FEATURE(x) \ + feature = xmlnode_new_child(query, "feature"); \ + xmlnode_set_attrib(feature, "var", x); + + +void jabber_disco_info_parse(JabberStream *js, xmlnode *packet) { + const char *from = xmlnode_get_attrib(packet, "from"); + const char *type = xmlnode_get_attrib(packet, "type"); + + if(!from || !type) + return; + + if(!strcmp(type, "get")) { + xmlnode *query, *identity, *feature; + JabberIq *iq; + + xmlnode *in_query; + const char *node = NULL; + + if((in_query = xmlnode_get_child(packet, "query"))) { + node = xmlnode_get_attrib(in_query, "node"); + } + + + iq = jabber_iq_new_query(js, JABBER_IQ_RESULT, + "http://jabber.org/protocol/disco#info"); + + jabber_iq_set_id(iq, xmlnode_get_attrib(packet, "id")); + + xmlnode_set_attrib(iq->node, "to", from); + query = xmlnode_get_child(iq->node, "query"); + + if(node) + xmlnode_set_attrib(query, "node", node); + + if(!node || !strcmp(node, CAPS0115_NODE "#" VERSION)) { + + identity = xmlnode_new_child(query, "identity"); + xmlnode_set_attrib(identity, "category", "client"); + xmlnode_set_attrib(identity, "type", "pc"); /* XXX: bot, console, + * handheld, pc, phone, + * web */ + xmlnode_set_attrib(identity, "name", PACKAGE); + + SUPPORT_FEATURE("jabber:iq:last") + SUPPORT_FEATURE("jabber:iq:oob") + SUPPORT_FEATURE("jabber:iq:time") + SUPPORT_FEATURE("jabber:iq:version") + SUPPORT_FEATURE("jabber:x:conference") + SUPPORT_FEATURE("http://jabber.org/protocol/bytestreams") + SUPPORT_FEATURE("http://jabber.org/protocol/disco#info") + SUPPORT_FEATURE("http://jabber.org/protocol/disco#items") +#if 0 + SUPPORT_FEATURE("http://jabber.org/protocol/ibb") +#endif + SUPPORT_FEATURE("http://jabber.org/protocol/muc") + SUPPORT_FEATURE("http://jabber.org/protocol/muc#user") + SUPPORT_FEATURE("http://jabber.org/protocol/si") + SUPPORT_FEATURE("http://jabber.org/protocol/si/profile/file-transfer") + SUPPORT_FEATURE("http://jabber.org/protocol/xhtml-im") + } else { + xmlnode *error, *inf; + + /* XXX: gross hack, implement jabber_iq_set_type or something */ + xmlnode_set_attrib(iq->node, "type", "error"); + iq->type = JABBER_IQ_ERROR; + + error = xmlnode_new_child(query, "error"); + xmlnode_set_attrib(error, "code", "404"); + xmlnode_set_attrib(error, "type", "cancel"); + inf = xmlnode_new_child(error, "item-not-found"); + xmlnode_set_namespace(inf, "urn:ietf:params:xml:ns:xmpp-stanzas"); + } + + jabber_iq_send(iq); + } else if(!strcmp(type, "result")) { + xmlnode *query = xmlnode_get_child(packet, "query"); + xmlnode *child; + JabberID *jid; + JabberBuddy *jb; + JabberBuddyResource *jbr = NULL; + JabberCapabilities capabilities = JABBER_CAP_NONE; + struct _jabber_disco_info_cb_data *jdicd; + + if((jid = jabber_id_new(from))) { + if(jid->resource && (jb = jabber_buddy_find(js, from, TRUE))) + jbr = jabber_buddy_find_resource(jb, jid->resource); + jabber_id_free(jid); + } + + if(jbr) + capabilities = jbr->capabilities; + + for(child = query->child; child; child = child->next) { + if(child->type != XMLNODE_TYPE_TAG) + continue; + + if(!strcmp(child->name, "identity")) { + const char *category = xmlnode_get_attrib(child, "category"); + const char *type = xmlnode_get_attrib(child, "type"); + if(!category || !type) + continue; + + if(!strcmp(category, "conference") && !strcmp(type, "text")) { + /* we found a groupchat or MUC server, add it to the list */ + /* XXX: actually check for protocol/muc or gc-1.0 support */ + js->chat_servers = g_list_append(js->chat_servers, g_strdup(from)); + } else if(!strcmp(category, "directory") && !strcmp(type, "user")) { + /* we found a JUD */ + js->user_directories = g_list_append(js->user_directories, g_strdup(from)); + } + + } else if(!strcmp(child->name, "feature")) { + const char *var = xmlnode_get_attrib(child, "var"); + if(!var) + continue; + + if(!strcmp(var, "http://jabber.org/protocol/si")) + capabilities |= JABBER_CAP_SI; + else if(!strcmp(var, "http://jabber.org/protocol/si/profile/file-transfer")) + capabilities |= JABBER_CAP_SI_FILE_XFER; + else if(!strcmp(var, "http://jabber.org/protocol/bytestreams")) + capabilities |= JABBER_CAP_BYTESTREAMS; + else if(!strcmp(var, "jabber:iq:search")) + capabilities |= JABBER_CAP_IQ_SEARCH; + else if(!strcmp(var, "jabber:iq:register")) + capabilities |= JABBER_CAP_IQ_REGISTER; + } + } + + capabilities |= JABBER_CAP_RETRIEVED; + + if(jbr) + jbr->capabilities = capabilities; + + if((jdicd = g_hash_table_lookup(js->disco_callbacks, from))) { + jdicd->callback(js, from, capabilities, jdicd->data); + g_hash_table_remove(js->disco_callbacks, from); + } + } else if(!strcmp(type, "error")) { + JabberID *jid; + JabberBuddy *jb; + JabberBuddyResource *jbr = NULL; + JabberCapabilities capabilities = JABBER_CAP_NONE; + struct _jabber_disco_info_cb_data *jdicd; + + if(!(jdicd = g_hash_table_lookup(js->disco_callbacks, from))) + return; + + if((jid = jabber_id_new(from))) { + if(jid->resource && (jb = jabber_buddy_find(js, from, TRUE))) + jbr = jabber_buddy_find_resource(jb, jid->resource); + jabber_id_free(jid); + } + + if(jbr) + capabilities = jbr->capabilities; + + jdicd->callback(js, from, capabilities, jdicd->data); + g_hash_table_remove(js->disco_callbacks, from); + } +} + +void jabber_disco_items_parse(JabberStream *js, xmlnode *packet) { + const char *from = xmlnode_get_attrib(packet, "from"); + const char *type = xmlnode_get_attrib(packet, "type"); + + if(type && !strcmp(type, "get")) { + JabberIq *iq = jabber_iq_new_query(js, JABBER_IQ_RESULT, + "http://jabber.org/protocol/disco#items"); + + jabber_iq_set_id(iq, xmlnode_get_attrib(packet, "id")); + + xmlnode_set_attrib(iq->node, "to", from); + jabber_iq_send(iq); + } +} + +static void +jabber_disco_finish_server_info_result_cb(JabberStream *js) +{ + GaimPresence *gpresence; + GaimStatus *status; + + if (!(js->server_caps & JABBER_CAP_GOOGLE_ROSTER)) { + /* If the server supports JABBER_CAP_GOOGLE_ROSTER; we will have already requested it */ + jabber_roster_request(js); + } + + /* Send initial presence; this will trigger receipt of presence for contacts on the roster */ + gpresence = gaim_account_get_presence(js->gc->account); + status = gaim_presence_get_active_status(gpresence); + jabber_presence_send(js->gc->account, status); +} + +static void +jabber_disco_server_info_result_cb(JabberStream *js, xmlnode *packet, gpointer data) +{ + xmlnode *query, *child; + const char *from = xmlnode_get_attrib(packet, "from"); + const char *type = xmlnode_get_attrib(packet, "type"); + + if((!from || !type) || + (strcmp(from, js->user->domain))) { + jabber_disco_finish_server_info_result_cb(js); + return; + } + + if(strcmp(type, "result")) { + /* A common way to get here is for the server not to support xmlns http://jabber.org/protocol/disco#info */ + jabber_disco_finish_server_info_result_cb(js); + return; + } + + query = xmlnode_get_child(packet, "query"); + + if (!query) { + jabber_disco_finish_server_info_result_cb(js); + return; + } + + for (child = xmlnode_get_child(query, "category"); child; + child = xmlnode_get_next_twin(child)) { + const char *category, *type, *name; + category = xmlnode_get_attrib(child, "category"); + if (!category || strcmp(category, "server")) + continue; + type = xmlnode_get_attrib(child, "type"); + if (!type || strcmp(type, "im")) + continue; + + name = xmlnode_get_attrib(child, "name"); + if (!name) + continue; + + g_free(js->server_name); + js->server_name = g_strdup(name); + if (!strcmp(name, "Google Talk")) + js->googletalk = TRUE; + } + + for (child = xmlnode_get_child(query, "feature"); child; + child = xmlnode_get_next_twin(child)) { + const char *var; + var = xmlnode_get_attrib(child, "var"); + if (!var) + continue; + + if (!strcmp("google:mail:notify", var)) { + js->server_caps |= JABBER_CAP_GMAIL_NOTIFY; + jabber_gmail_init(js); + } else if (!strcmp("google:roster", var)) { + js->server_caps |= JABBER_CAP_GOOGLE_ROSTER; + jabber_google_roster_init(js); + } + } + + jabber_disco_finish_server_info_result_cb(js); +} + +static void +jabber_disco_server_items_result_cb(JabberStream *js, xmlnode *packet, gpointer data) +{ + xmlnode *query, *child; + const char *from = xmlnode_get_attrib(packet, "from"); + const char *type = xmlnode_get_attrib(packet, "type"); + + if(!from || !type) + return; + + if(strcmp(from, js->user->domain)) + return; + + if(strcmp(type, "result")) + return; + + while(js->chat_servers) { + g_free(js->chat_servers->data); + js->chat_servers = g_list_delete_link(js->chat_servers, js->chat_servers); + } + + query = xmlnode_get_child(packet, "query"); + + for(child = xmlnode_get_child(query, "item"); child; + child = xmlnode_get_next_twin(child)) { + JabberIq *iq; + const char *jid; + + if(!(jid = xmlnode_get_attrib(child, "jid"))) + continue; + + iq = jabber_iq_new_query(js, JABBER_IQ_GET, "http://jabber.org/protocol/disco#info"); + xmlnode_set_attrib(iq->node, "to", jid); + jabber_iq_send(iq); + } +} + +void jabber_disco_items_server(JabberStream *js) +{ + JabberIq *iq = jabber_iq_new_query(js, JABBER_IQ_GET, + "http://jabber.org/protocol/disco#items"); + + xmlnode_set_attrib(iq->node, "to", js->user->domain); + + jabber_iq_set_callback(iq, jabber_disco_server_items_result_cb, NULL); + jabber_iq_send(iq); + + iq = jabber_iq_new_query(js, JABBER_IQ_GET, + "http://jabber.org/protocol/disco#info"); + xmlnode_set_attrib(iq->node, "to", js->user->domain); + jabber_iq_set_callback(iq, jabber_disco_server_info_result_cb, NULL); + jabber_iq_send(iq); +} + +void jabber_disco_info_do(JabberStream *js, const char *who, JabberDiscoInfoCallback *callback, gpointer data) +{ + JabberID *jid; + JabberBuddy *jb; + JabberBuddyResource *jbr = NULL; + struct _jabber_disco_info_cb_data *jdicd; + JabberIq *iq; + + if((jid = jabber_id_new(who))) { + if(jid->resource && (jb = jabber_buddy_find(js, who, TRUE))) + jbr = jabber_buddy_find_resource(jb, jid->resource); + jabber_id_free(jid); + } + + if(jbr && jbr->capabilities & JABBER_CAP_RETRIEVED) { + callback(js, who, jbr->capabilities, data); + return; + } + + jdicd = g_new0(struct _jabber_disco_info_cb_data, 1); + jdicd->data = data; + jdicd->callback = callback; + + g_hash_table_insert(js->disco_callbacks, g_strdup(who), jdicd); + + iq = jabber_iq_new_query(js, JABBER_IQ_GET, "http://jabber.org/protocol/disco#info"); + xmlnode_set_attrib(iq->node, "to", who); + + jabber_iq_send(iq); +} + + diff --git a/libpurple/protocols/jabber/disco.h b/libpurple/protocols/jabber/disco.h new file mode 100644 index 0000000000..550c591498 --- /dev/null +++ b/libpurple/protocols/jabber/disco.h @@ -0,0 +1,38 @@ +/** + * @file iq.h JabberID handlers + * + * gaim + * + * Copyright (C) 2003 Nathan Walp <faceprint@faceprint.com> + * + * 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 + */ +#ifndef _GAIM_JABBER_DISCO_H_ +#define _GAIM_JABBER_DISCO_H_ + +#include "jabber.h" + +typedef void (JabberDiscoInfoCallback)(JabberStream *js, const char *who, + JabberCapabilities capabilities, gpointer data); + +void jabber_disco_info_parse(JabberStream *js, xmlnode *packet); +void jabber_disco_items_parse(JabberStream *js, xmlnode *packet); + +void jabber_disco_items_server(JabberStream *js); + +void jabber_disco_info_do(JabberStream *js, const char *who, + JabberDiscoInfoCallback *callback, gpointer data); + +#endif /* _GAIM_JABBER_DISCO_H_ */ diff --git a/libpurple/protocols/jabber/google.c b/libpurple/protocols/jabber/google.c new file mode 100644 index 0000000000..8dd6211a77 --- /dev/null +++ b/libpurple/protocols/jabber/google.c @@ -0,0 +1,363 @@ + +/** + * Gaim is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 + */ + +#include "internal.h" +#include "debug.h" +#include "privacy.h" + +#include "buddy.h" +#include "google.h" +#include "jabber.h" +#include "presence.h" +#include "iq.h" + +static void +jabber_gmail_parse(JabberStream *js, xmlnode *packet, gpointer nul) +{ + const char *type = xmlnode_get_attrib(packet, "type"); + xmlnode *child; + xmlnode *message, *sender_node, *subject_node; + const char *from, *to, *subject, *url, *tid; + const char *in_str; + char *to_name; + int i, count = 1, returned_count; + + const char **tos, **froms, **subjects, **urls; + + if (strcmp(type, "result")) + return; + + child = xmlnode_get_child(packet, "mailbox"); + if (!child) + return; + + in_str = xmlnode_get_attrib(child, "total-matched"); + if (in_str && *in_str) + count = atoi(in_str); + + if (count == 0) + return; + + message = xmlnode_get_child(child, "mail-thread-info"); + + /* Loop once to see how many messages were returned so we can allocate arrays + * accordingly */ + if (!message) + return; + for (returned_count = 0; message; returned_count++, message=xmlnode_get_next_twin(message)); + + froms = g_new0(const char* , returned_count); + tos = g_new0(const char* , returned_count); + subjects = g_new0(const char* , returned_count); + urls = g_new0(const char* , returned_count); + + to = xmlnode_get_attrib(packet, "to"); + to_name = jabber_get_bare_jid(to); + url = xmlnode_get_attrib(child, "url"); + if (!url || !*url) + url = "http://www.gmail.com"; + + message= xmlnode_get_child(child, "mail-thread-info"); + for (i=0; message; message = xmlnode_get_next_twin(message), i++) { + subject_node = xmlnode_get_child(message, "subject"); + sender_node = xmlnode_get_child(message, "senders"); + sender_node = xmlnode_get_child(sender_node, "sender"); + + while (sender_node && (!xmlnode_get_attrib(sender_node, "unread") || + !strcmp(xmlnode_get_attrib(sender_node, "unread"),"0"))) + sender_node = xmlnode_get_next_twin(sender_node); + + if (!sender_node) { + i--; + continue; + } + + from = xmlnode_get_attrib(sender_node, "name"); + if (!from || !*from) + from = xmlnode_get_attrib(sender_node, "address"); + subject = xmlnode_get_data(subject_node); + /* + * url = xmlnode_get_attrib(message, "url"); + */ + tos[i] = (to_name != NULL ? to_name : ""); + froms[i] = (from != NULL ? from : ""); + subjects[i] = (subject != NULL ? subject : ""); + urls[i] = (url != NULL ? url : ""); + + tid = xmlnode_get_attrib(message, "tid"); + if (tid && + (js->gmail_last_tid == NULL || strcmp(tid, js->gmail_last_tid) > 0)) { + g_free(js->gmail_last_tid); + js->gmail_last_tid = g_strdup(tid); + } + } + + if (i>0) + gaim_notify_emails(js->gc, count, count == returned_count, subjects, froms, tos, + urls, NULL, NULL); + + g_free(to_name); + g_free(tos); + g_free(froms); + g_free(subjects); + g_free(urls); + + in_str = xmlnode_get_attrib(child, "result-time"); + if (in_str && *in_str) { + g_free(js->gmail_last_time); + js->gmail_last_time = g_strdup(in_str); + } +} + +void +jabber_gmail_poke(JabberStream *js, xmlnode *packet) +{ + const char *type; + xmlnode *query; + JabberIq *iq; + + /* bail if the user isn't interested */ + if (!gaim_account_get_check_mail(js->gc->account)) + return; + + type = xmlnode_get_attrib(packet, "type"); + + + /* Is this an initial incoming mail notification? If so, send a request for more info */ + if (strcmp(type, "set") || !xmlnode_get_child(packet, "new-mail")) + return; + + gaim_debug(GAIM_DEBUG_MISC, "jabber", + "Got new mail notification. Sending request for more info\n"); + + iq = jabber_iq_new_query(js, JABBER_IQ_GET, "google:mail:notify"); + jabber_iq_set_callback(iq, jabber_gmail_parse, NULL); + query = xmlnode_get_child(iq->node, "query"); + + if (js->gmail_last_time) + xmlnode_set_attrib(query, "newer-than-time", js->gmail_last_time); + if (js->gmail_last_tid) + xmlnode_set_attrib(query, "newer-than-tid", js->gmail_last_tid); + + jabber_iq_send(iq); + return; +} + +void jabber_gmail_init(JabberStream *js) { + JabberIq *iq; + + if (!gaim_account_get_check_mail(js->gc->account)) + return; + + iq = jabber_iq_new_query(js, JABBER_IQ_GET, "google:mail:notify"); + jabber_iq_set_callback(iq, jabber_gmail_parse, NULL); + jabber_iq_send(iq); +} + +void jabber_google_roster_init(JabberStream *js) +{ + JabberIq *iq; + xmlnode *query; + + iq = jabber_iq_new_query(js, JABBER_IQ_GET, "jabber:iq:roster"); + query = xmlnode_get_child(iq->node, "query"); + + xmlnode_set_attrib(query, "xmlns:gr", "google:roster"); + xmlnode_set_attrib(query, "gr:ext", "2"); + + jabber_iq_send(iq); +} + +void jabber_google_roster_outgoing(JabberStream *js, xmlnode *query, xmlnode *item) +{ + GaimAccount *account = gaim_connection_get_account(js->gc); + GSList *list = account->deny; + const char *jid = xmlnode_get_attrib(item, "jid"); + char *jid_norm = g_strdup(jabber_normalize(account, jid)); + + while (list) { + if (!strcmp(jid_norm, (char*)list->data)) { + xmlnode_set_attrib(query, "xmlns:gr", "google:roster"); + xmlnode_set_attrib(item, "gr:t", "B"); + xmlnode_set_attrib(query, "xmlns:gr", "google:roster"); + xmlnode_set_attrib(query, "gr:ext", "2"); + return; + } + list = list->next; + } + +} + +void jabber_google_roster_incoming(JabberStream *js, xmlnode *item) +{ + GaimAccount *account = gaim_connection_get_account(js->gc); + GSList *list = account->deny; + const char *jid = xmlnode_get_attrib(item, "jid"); + gboolean on_block_list = FALSE; + + char *jid_norm = g_strdup(jabber_normalize(account, jid)); + + const char *grt = xmlnode_get_attrib_with_namespace(item, "t", "google:roster"); + + while (list) { + if (!strcmp(jid_norm, (char*)list->data)) { + on_block_list = TRUE; + break; + } + list = list->next; + } + + if (!on_block_list && (grt && (*grt == 'B' || *grt == 'b'))) { + gaim_debug_info("jabber", "Blocking %s\n", jid_norm); + gaim_privacy_deny_add(account, jid_norm, TRUE); + } else if (on_block_list && (!grt || (*grt != 'B' && *grt != 'b' ))){ + gaim_debug_info("jabber", "Unblocking %s\n", jid_norm); + gaim_privacy_deny_remove(account, jid_norm, TRUE); + } +} + +void jabber_google_roster_add_deny(GaimConnection *gc, const char *who) +{ + JabberStream *js; + GSList *buddies; + JabberIq *iq; + xmlnode *query; + xmlnode *item; + xmlnode *group; + GaimBuddy *b; + JabberBuddy *jb; + + js = (JabberStream*)(gc->proto_data); + + if (!js || !js->server_caps & JABBER_CAP_GOOGLE_ROSTER) + return; + + jb = jabber_buddy_find(js, who, TRUE); + + buddies = gaim_find_buddies(js->gc->account, who); + if(!buddies) + return; + + b = buddies->data; + + iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:roster"); + + query = xmlnode_get_child(iq->node, "query"); + item = xmlnode_new_child(query, "item"); + + while(buddies) { + GaimGroup *g; + + b = buddies->data; + g = gaim_buddy_get_group(b); + + group = xmlnode_new_child(item, "group"); + xmlnode_insert_data(group, g->name, -1); + + buddies = buddies->next; + } + + iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:roster"); + + query = xmlnode_get_child(iq->node, "query"); + item = xmlnode_new_child(query, "item"); + + xmlnode_set_attrib(item, "jid", who); + xmlnode_set_attrib(item, "name", b->alias ? b->alias : ""); + xmlnode_set_attrib(item, "gr:t", "B"); + xmlnode_set_attrib(query, "xmlns:gr", "google:roster"); + xmlnode_set_attrib(query, "gr:ext", "2"); + + jabber_iq_send(iq); + + /* Synthesize a sign-off */ + if (jb) { + JabberBuddyResource *jbr; + GList *l = jb->resources; + while (l) { + jbr = l->data; + if (jbr && jbr->name) + { + gaim_debug(GAIM_DEBUG_MISC, "jabber", "Removing resource %s\n", jbr->name); + jabber_buddy_remove_resource(jb, jbr->name); + } + l = l->next; + } + } + gaim_prpl_got_user_status(gaim_connection_get_account(gc), who, "offline", NULL); +} + +void jabber_google_roster_rem_deny(GaimConnection *gc, const char *who) +{ + JabberStream *js; + GSList *buddies; + JabberIq *iq; + xmlnode *query; + xmlnode *item; + xmlnode *group; + GaimBuddy *b; + + g_return_if_fail(gc != NULL); + g_return_if_fail(who != NULL); + + js = (JabberStream*)(gc->proto_data); + + if (!js || !js->server_caps & JABBER_CAP_GOOGLE_ROSTER) + return; + + buddies = gaim_find_buddies(js->gc->account, who); + if(!buddies) + return; + + b = buddies->data; + + iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:roster"); + + query = xmlnode_get_child(iq->node, "query"); + item = xmlnode_new_child(query, "item"); + + while(buddies) { + GaimGroup *g; + + b = buddies->data; + g = gaim_buddy_get_group(b); + + group = xmlnode_new_child(item, "group"); + xmlnode_insert_data(group, g->name, -1); + + buddies = buddies->next; + } + + iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:roster"); + + query = xmlnode_get_child(iq->node, "query"); + item = xmlnode_new_child(query, "item"); + + xmlnode_set_attrib(item, "jid", who); + xmlnode_set_attrib(item, "name", b->alias ? b->alias : ""); + xmlnode_set_attrib(query, "xmlns:gr", "google:roster"); + xmlnode_set_attrib(query, "gr:ext", "2"); + + jabber_iq_send(iq); + + /* See if he's online */ + jabber_presence_subscription_set(js, who, "probe"); +} diff --git a/libpurple/protocols/jabber/google.h b/libpurple/protocols/jabber/google.h new file mode 100644 index 0000000000..ac342450bc --- /dev/null +++ b/libpurple/protocols/jabber/google.h @@ -0,0 +1,40 @@ +/** + * Gaim is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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 + */ + +#ifndef _GAIM_GOOGLE_H_ +#define _GAIM_GOOGLE_H_ + +/* This is a place for Google Talk-specific XMPP extensions to live + * such that they don't intermingle with code for the XMPP RFCs and XEPs :) */ + +#include "jabber.h" + +void jabber_gmail_init(JabberStream *js); +void jabber_gmail_poke(JabberStream *js, xmlnode *node); + +void jabber_google_roster_init(JabberStream *js); +void jabber_google_roster_outgoing(JabberStream *js, xmlnode *query, xmlnode *item); +void jabber_google_roster_incoming(JabberStream *js, xmlnode *item); +void jabber_google_roster_add_deny(GaimConnection *gc, const char *who); +void jabber_google_roster_rem_deny(GaimConnection *gc, const char *who); + + + +#endif /* _GAIM_GOOGLE_H_ */ diff --git a/libpurple/protocols/jabber/iq.c b/libpurple/protocols/jabber/iq.c new file mode 100644 index 0000000000..ce46831b41 --- /dev/null +++ b/libpurple/protocols/jabber/iq.c @@ -0,0 +1,336 @@ +/* + * gaim - Jabber Protocol Plugin + * + * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com> + * + * 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 + * + */ +#include "internal.h" +#include "debug.h" +#include "prefs.h" +#include "util.h" + +#include "buddy.h" +#include "disco.h" +#include "google.h" +#include "iq.h" +#include "oob.h" +#include "roster.h" +#include "si.h" + +#ifdef _WIN32 +#include "utsname.h" +#endif + +GHashTable *iq_handlers = NULL; + + +JabberIq *jabber_iq_new(JabberStream *js, JabberIqType type) +{ + JabberIq *iq; + + iq = g_new0(JabberIq, 1); + + iq->type = type; + + iq->node = xmlnode_new("iq"); + switch(iq->type) { + case JABBER_IQ_SET: + xmlnode_set_attrib(iq->node, "type", "set"); + break; + case JABBER_IQ_GET: + xmlnode_set_attrib(iq->node, "type", "get"); + break; + case JABBER_IQ_ERROR: + xmlnode_set_attrib(iq->node, "type", "error"); + break; + case JABBER_IQ_RESULT: + xmlnode_set_attrib(iq->node, "type", "result"); + break; + case JABBER_IQ_NONE: + /* this shouldn't ever happen */ + break; + } + + iq->js = js; + + if(type == JABBER_IQ_GET || type == JABBER_IQ_SET) { + iq->id = jabber_get_next_id(js); + xmlnode_set_attrib(iq->node, "id", iq->id); + } + + return iq; +} + +JabberIq *jabber_iq_new_query(JabberStream *js, JabberIqType type, + const char *xmlns) +{ + JabberIq *iq = jabber_iq_new(js, type); + xmlnode *query; + + query = xmlnode_new_child(iq->node, "query"); + xmlnode_set_namespace(query, xmlns); + + return iq; +} + +typedef struct _JabberCallbackData { + JabberIqCallback *callback; + gpointer data; +} JabberCallbackData; + +void +jabber_iq_set_callback(JabberIq *iq, JabberIqCallback *callback, gpointer data) +{ + iq->callback = callback; + iq->callback_data = data; +} + +void jabber_iq_set_id(JabberIq *iq, const char *id) +{ + if(iq->id) + g_free(iq->id); + + if(id) { + xmlnode_set_attrib(iq->node, "id", id); + iq->id = g_strdup(id); + } else { + xmlnode_remove_attrib(iq->node, "id"); + iq->id = NULL; + } +} + +void jabber_iq_send(JabberIq *iq) +{ + JabberCallbackData *jcd; + g_return_if_fail(iq != NULL); + + jabber_send(iq->js, iq->node); + + if(iq->id && iq->callback) { + jcd = g_new0(JabberCallbackData, 1); + jcd->callback = iq->callback; + jcd->data = iq->callback_data; + g_hash_table_insert(iq->js->iq_callbacks, g_strdup(iq->id), jcd); + } + + jabber_iq_free(iq); +} + +void jabber_iq_free(JabberIq *iq) +{ + g_return_if_fail(iq != NULL); + + g_free(iq->id); + xmlnode_free(iq->node); + g_free(iq); +} + +static void jabber_iq_last_parse(JabberStream *js, xmlnode *packet) +{ + JabberIq *iq; + const char *type; + const char *from; + const char *id; + xmlnode *query; + char *idle_time; + + type = xmlnode_get_attrib(packet, "type"); + from = xmlnode_get_attrib(packet, "from"); + id = xmlnode_get_attrib(packet, "id"); + + if(type && !strcmp(type, "get")) { + iq = jabber_iq_new_query(js, JABBER_IQ_RESULT, "jabber:iq:last"); + jabber_iq_set_id(iq, id); + xmlnode_set_attrib(iq->node, "to", from); + + query = xmlnode_get_child(iq->node, "query"); + + idle_time = g_strdup_printf("%ld", js->idle ? time(NULL) - js->idle : 0); + xmlnode_set_attrib(query, "seconds", idle_time); + g_free(idle_time); + + jabber_iq_send(iq); + } +} + +static void jabber_iq_time_parse(JabberStream *js, xmlnode *packet) +{ + const char *type, *from, *id; + JabberIq *iq; + xmlnode *query; + time_t now_t; + struct tm *now; + + time(&now_t); + now = localtime(&now_t); + + type = xmlnode_get_attrib(packet, "type"); + from = xmlnode_get_attrib(packet, "from"); + id = xmlnode_get_attrib(packet, "id"); + + if(type && !strcmp(type, "get")) { + const char *date; + + iq = jabber_iq_new_query(js, JABBER_IQ_RESULT, "jabber:iq:time"); + jabber_iq_set_id(iq, id); + xmlnode_set_attrib(iq->node, "to", from); + + query = xmlnode_get_child(iq->node, "query"); + + date = gaim_utf8_strftime("%Y%m%dT%T", now); + xmlnode_insert_data(xmlnode_new_child(query, "utc"), date, -1); + + date = gaim_utf8_strftime("%Z", now); + xmlnode_insert_data(xmlnode_new_child(query, "tz"), date, -1); + + date = gaim_utf8_strftime("%d %b %Y %T", now); + xmlnode_insert_data(xmlnode_new_child(query, "display"), date, -1); + + jabber_iq_send(iq); + } +} + +static void jabber_iq_version_parse(JabberStream *js, xmlnode *packet) +{ + JabberIq *iq; + const char *type, *from, *id; + xmlnode *query; + char *os = NULL; + + type = xmlnode_get_attrib(packet, "type"); + + if(type && !strcmp(type, "get")) { + + if(!gaim_prefs_get_bool("/plugins/prpl/jabber/hide_os")) { + struct utsname osinfo; + + uname(&osinfo); + os = g_strdup_printf("%s %s %s", osinfo.sysname, osinfo.release, + osinfo.machine); + } + + from = xmlnode_get_attrib(packet, "from"); + id = xmlnode_get_attrib(packet, "id"); + + iq = jabber_iq_new_query(js, JABBER_IQ_RESULT, "jabber:iq:version"); + xmlnode_set_attrib(iq->node, "to", from); + jabber_iq_set_id(iq, id); + + query = xmlnode_get_child(iq->node, "query"); + + xmlnode_insert_data(xmlnode_new_child(query, "name"), PACKAGE, -1); + xmlnode_insert_data(xmlnode_new_child(query, "version"), VERSION, -1); + if(os) { + xmlnode_insert_data(xmlnode_new_child(query, "os"), os, -1); + g_free(os); + } + + jabber_iq_send(iq); + } +} + +void jabber_iq_remove_callback_by_id(JabberStream *js, const char *id) +{ + g_hash_table_remove(js->iq_callbacks, id); +} + +void jabber_iq_parse(JabberStream *js, xmlnode *packet) +{ + JabberCallbackData *jcd; + xmlnode *query, *error, *x; + const char *xmlns; + const char *type, *id, *from; + JabberIqHandler *jih; + + query = xmlnode_get_child(packet, "query"); + type = xmlnode_get_attrib(packet, "type"); + from = xmlnode_get_attrib(packet, "from"); + id = xmlnode_get_attrib(packet, "id"); + + /* First, lets see if a special callback got registered */ + + if(type && (!strcmp(type, "result") || !strcmp(type, "error"))) { + if(id && *id && (jcd = g_hash_table_lookup(js->iq_callbacks, id))) { + jcd->callback(js, packet, jcd->data); + jabber_iq_remove_callback_by_id(js, id); + return; + } + } + + /* Apparently not, so lets see if we have a pre-defined handler */ + + if(type && query && (xmlns = xmlnode_get_namespace(query))) { + if((jih = g_hash_table_lookup(iq_handlers, xmlns))) { + jih(js, packet); + return; + } + } + + if(xmlnode_get_child_with_namespace(packet, "si", "http://jabber.org/protocol/si")) { + jabber_si_parse(js, packet); + return; + } + + if(xmlnode_get_child_with_namespace(packet, "new-mail", "google:mail:notify")) { + jabber_gmail_poke(js, packet); + return; + } + + /* If we get here, send the default error reply mandated by XMPP-CORE */ + if(type && (!strcmp(type, "set") || !strcmp(type, "get"))) { + JabberIq *iq = jabber_iq_new(js, JABBER_IQ_ERROR); + + xmlnode_free(iq->node); + iq->node = xmlnode_copy(packet); + xmlnode_set_attrib(iq->node, "to", from); + xmlnode_remove_attrib(iq->node, "from"); + xmlnode_set_attrib(iq->node, "type", "error"); + error = xmlnode_new_child(iq->node, "error"); + xmlnode_set_attrib(error, "type", "cancel"); + xmlnode_set_attrib(error, "code", "501"); + x = xmlnode_new_child(error, "feature-not-implemented"); + xmlnode_set_namespace(x, "urn:ietf:params:xml:ns:xmpp-stanzas"); + + jabber_iq_send(iq); + } +} + +void jabber_iq_register_handler(const char *xmlns, JabberIqHandler handlerfunc) +{ + g_hash_table_replace(iq_handlers, g_strdup(xmlns), handlerfunc); +} + +void jabber_iq_init(void) +{ + iq_handlers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + + jabber_iq_register_handler("jabber:iq:roster", jabber_roster_parse); + jabber_iq_register_handler("jabber:iq:oob", jabber_oob_parse); + jabber_iq_register_handler("http://jabber.org/protocol/bytestreams", jabber_bytestreams_parse); + jabber_iq_register_handler("jabber:iq:last", jabber_iq_last_parse); + jabber_iq_register_handler("jabber:iq:time", jabber_iq_time_parse); + jabber_iq_register_handler("jabber:iq:version", jabber_iq_version_parse); + jabber_iq_register_handler("http://jabber.org/protocol/disco#info", jabber_disco_info_parse); + jabber_iq_register_handler("http://jabber.org/protocol/disco#items", jabber_disco_items_parse); + jabber_iq_register_handler("jabber:iq:register", jabber_register_parse); +} + +void jabber_iq_uninit(void) +{ + g_hash_table_destroy(iq_handlers); +} + diff --git a/libpurple/protocols/jabber/iq.h b/libpurple/protocols/jabber/iq.h new file mode 100644 index 0000000000..acde9ed681 --- /dev/null +++ b/libpurple/protocols/jabber/iq.h @@ -0,0 +1,70 @@ +/** + * @file iq.h JabberID handlers + * + * gaim + * + * Copyright (C) 2003 Nathan Walp <faceprint@faceprint.com> + * + * 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 + */ +#ifndef _GAIM_JABBER_IQ_H_ +#define _GAIM_JABBER_IQ_H_ + +#include "jabber.h" + +typedef struct _JabberIq JabberIq; + +typedef enum { + JABBER_IQ_SET, + JABBER_IQ_GET, + JABBER_IQ_RESULT, + JABBER_IQ_ERROR, + JABBER_IQ_NONE +} JabberIqType; + +typedef void (JabberIqHandler)(JabberStream *js, xmlnode *packet); + +typedef void (JabberIqCallback)(JabberStream *js, xmlnode *packet, gpointer data); + +struct _JabberIq { + JabberIqType type; + char *id; + xmlnode *node; + + JabberIqCallback *callback; + gpointer callback_data; + + JabberStream *js; +}; + +JabberIq *jabber_iq_new(JabberStream *js, JabberIqType type); +JabberIq *jabber_iq_new_query(JabberStream *js, JabberIqType type, + const char *xmlns); + +void jabber_iq_parse(JabberStream *js, xmlnode *packet); + +void jabber_iq_remove_callback_by_id(JabberStream *js, const char *id); +void jabber_iq_set_callback(JabberIq *iq, JabberIqCallback *cb, gpointer data); +void jabber_iq_set_id(JabberIq *iq, const char *id); + +void jabber_iq_send(JabberIq *iq); +void jabber_iq_free(JabberIq *iq); + +void jabber_iq_init(void); +void jabber_iq_uninit(void); + +void jabber_iq_register_handler(const char *xmlns, JabberIqHandler *func); + +#endif /* _GAIM_JABBER_IQ_H_ */ diff --git a/libpurple/protocols/jabber/jabber.c b/libpurple/protocols/jabber/jabber.c new file mode 100644 index 0000000000..7c6ca5343f --- /dev/null +++ b/libpurple/protocols/jabber/jabber.c @@ -0,0 +1,2022 @@ +/* + * gaim - Jabber Protocol Plugin + * + * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com> + * + * 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 + * + */ +#include "internal.h" + +#include "account.h" +#include "accountopt.h" +#include "blist.h" +#include "cmds.h" +#include "connection.h" +#include "debug.h" +#include "dnssrv.h" +#include "message.h" +#include "notify.h" +#include "pluginpref.h" +#include "proxy.h" +#include "prpl.h" +#include "request.h" +#include "server.h" +#include "util.h" +#include "version.h" + +#include "auth.h" +#include "buddy.h" +#include "chat.h" +#include "disco.h" +#include "google.h" +#include "iq.h" +#include "jutil.h" +#include "message.h" +#include "parser.h" +#include "presence.h" +#include "jabber.h" +#include "roster.h" +#include "si.h" +#include "xdata.h" + +#define JABBER_CONNECT_STEPS (js->gsc ? 8 : 5) + +static GaimPlugin *my_protocol = NULL; + +static void jabber_stream_init(JabberStream *js) +{ + char *open_stream; + + open_stream = g_strdup_printf("<stream:stream to='%s' " + "xmlns='jabber:client' " + "xmlns:stream='http://etherx.jabber.org/streams' " + "version='1.0'>", + js->user->domain); + /* setup the parser fresh for each stream */ + jabber_parser_setup(js); + jabber_send_raw(js, open_stream, -1); + js->reinit = FALSE; + g_free(open_stream); +} + +static void +jabber_session_initialized_cb(JabberStream *js, xmlnode *packet, gpointer data) +{ + const char *type = xmlnode_get_attrib(packet, "type"); + if(type && !strcmp(type, "result")) { + jabber_stream_set_state(js, JABBER_STREAM_CONNECTED); + } else { + gaim_connection_error(js->gc, _("Error initializing session")); + } +} + +static void jabber_session_init(JabberStream *js) +{ + JabberIq *iq = jabber_iq_new(js, JABBER_IQ_SET); + xmlnode *session; + + jabber_iq_set_callback(iq, jabber_session_initialized_cb, NULL); + + session = xmlnode_new_child(iq->node, "session"); + xmlnode_set_namespace(session, "urn:ietf:params:xml:ns:xmpp-session"); + + jabber_iq_send(iq); +} + +static void jabber_bind_result_cb(JabberStream *js, xmlnode *packet, + gpointer data) +{ + const char *type = xmlnode_get_attrib(packet, "type"); + xmlnode *bind; + + if(type && !strcmp(type, "result") && + (bind = xmlnode_get_child_with_namespace(packet, "bind", "urn:ietf:params:xml:ns:xmpp-bind"))) { + xmlnode *jid; + char *full_jid; + if((jid = xmlnode_get_child(bind, "jid")) && (full_jid = xmlnode_get_data(jid))) { + JabberBuddy *my_jb = NULL; + jabber_id_free(js->user); + if(!(js->user = jabber_id_new(full_jid))) { + gaim_connection_error(js->gc, _("Invalid response from server.")); + } + if((my_jb = jabber_buddy_find(js, full_jid, TRUE))) + my_jb->subscription |= JABBER_SUB_BOTH; + g_free(full_jid); + } + } else { + char *msg = jabber_parse_error(js, packet); + gaim_connection_error(js->gc, msg); + g_free(msg); + } + + jabber_session_init(js); +} + +static void jabber_stream_features_parse(JabberStream *js, xmlnode *packet) +{ + if(xmlnode_get_child(packet, "starttls")) { + if(jabber_process_starttls(js, packet)) + return; + } + + if(js->registration) { + jabber_register_start(js); + } else if(xmlnode_get_child(packet, "mechanisms")) { + jabber_auth_start(js, packet); + } else if(xmlnode_get_child(packet, "bind")) { + xmlnode *bind, *resource; + JabberIq *iq = jabber_iq_new(js, JABBER_IQ_SET); + bind = xmlnode_new_child(iq->node, "bind"); + xmlnode_set_namespace(bind, "urn:ietf:params:xml:ns:xmpp-bind"); + resource = xmlnode_new_child(bind, "resource"); + xmlnode_insert_data(resource, js->user->resource, -1); + + jabber_iq_set_callback(iq, jabber_bind_result_cb, NULL); + + jabber_iq_send(iq); + } else /* if(xmlnode_get_child_with_namespace(packet, "auth")) */ { + /* If we get an empty stream:features packet, or we explicitly get + * an auth feature with namespace http://jabber.org/features/iq-auth + * we should revert back to iq:auth authentication, even though we're + * connecting to an XMPP server. */ + js->auth_type = JABBER_AUTH_IQ_AUTH; + jabber_stream_set_state(js, JABBER_STREAM_AUTHENTICATING); + } +} + +static void jabber_stream_handle_error(JabberStream *js, xmlnode *packet) +{ + char *msg = jabber_parse_error(js, packet); + + gaim_connection_error(js->gc, msg); + g_free(msg); +} + +static void tls_init(JabberStream *js); + +void jabber_process_packet(JabberStream *js, xmlnode *packet) +{ + gaim_signal_emit(my_protocol, "jabber-receiving-xmlnode", js->gc, &packet); + + /* if the signal leaves us with a null packet, we're done */ + if(NULL == packet) + return; + + if(!strcmp(packet->name, "iq")) { + jabber_iq_parse(js, packet); + } else if(!strcmp(packet->name, "presence")) { + jabber_presence_parse(js, packet); + } else if(!strcmp(packet->name, "message")) { + jabber_message_parse(js, packet); + } else if(!strcmp(packet->name, "stream:features")) { + jabber_stream_features_parse(js, packet); + } else if (!strcmp(packet->name, "features") && + !strcmp(xmlnode_get_namespace(packet), "http://etherx.jabber.org/streams")) { + jabber_stream_features_parse(js, packet); + } else if(!strcmp(packet->name, "stream:error")) { + jabber_stream_handle_error(js, packet); + } else if (!strcmp(packet->name, "error") && + !strcmp(xmlnode_get_namespace(packet), "http://etherx.jabber.org/streams")) { + jabber_stream_handle_error(js, packet); + } else if(!strcmp(packet->name, "challenge")) { + if(js->state == JABBER_STREAM_AUTHENTICATING) + jabber_auth_handle_challenge(js, packet); + } else if(!strcmp(packet->name, "success")) { + if(js->state == JABBER_STREAM_AUTHENTICATING) + jabber_auth_handle_success(js, packet); + } else if(!strcmp(packet->name, "failure")) { + if(js->state == JABBER_STREAM_AUTHENTICATING) + jabber_auth_handle_failure(js, packet); + } else if(!strcmp(packet->name, "proceed")) { + if(js->state == JABBER_STREAM_AUTHENTICATING && !js->gsc) + tls_init(js); + } else { + gaim_debug(GAIM_DEBUG_WARNING, "jabber", "Unknown packet: %s\n", + packet->name); + } +} + +static int jabber_do_send(JabberStream *js, const char *data, int len) +{ + int ret; + + if (js->gsc) + ret = gaim_ssl_write(js->gsc, data, len); + else + ret = write(js->fd, data, len); + + return ret; +} + +static void jabber_send_cb(gpointer data, gint source, GaimInputCondition cond) +{ + JabberStream *js = data; + int ret, writelen; + writelen = gaim_circ_buffer_get_max_read(js->write_buffer); + + if (writelen == 0) { + gaim_input_remove(js->writeh); + js->writeh = 0; + return; + } + + ret = jabber_do_send(js, js->write_buffer->outptr, writelen); + + if (ret < 0 && errno == EAGAIN) + return; + else if (ret <= 0) { + gaim_connection_error(js->gc, _("Write error")); + return; + } + + gaim_circ_buffer_mark_read(js->write_buffer, ret); +} + +void jabber_send_raw(JabberStream *js, const char *data, int len) +{ + int ret; + + /* because printing a tab to debug every minute gets old */ + if(strcmp(data, "\t")) + gaim_debug(GAIM_DEBUG_MISC, "jabber", "Sending%s: %s\n", + js->gsc ? " (ssl)" : "", data); + + /* If we've got a security layer, we need to encode the data, + * splitting it on the maximum buffer length negotiated */ + + gaim_signal_emit(my_protocol, "jabber-sending-text", js->gc, &data); + if (data == NULL) + return; + +#ifdef HAVE_CYRUS_SASL + if (js->sasl_maxbuf>0) { + int pos; + + if (!js->gsc && js->fd<0) + return; + pos = 0; + if (len == -1) + len = strlen(data); + while (pos < len) { + int towrite; + const char *out; + unsigned olen; + + if ((len - pos) < js->sasl_maxbuf) + towrite = len - pos; + else + towrite = js->sasl_maxbuf; + + sasl_encode(js->sasl, &data[pos], towrite, &out, &olen); + pos += towrite; + + if (js->writeh > 0) + ret = jabber_do_send(js, out, olen); + else { + ret = -1; + errno = EAGAIN; + } + + if (ret < 0 && errno != EAGAIN) + gaim_connection_error(js->gc, _("Write error")); + else if (ret < olen) { + if (ret < 0) + ret = 0; + if (js->writeh == 0) + js->writeh = gaim_input_add( + js->gsc ? js->gsc->fd : js->fd, + GAIM_INPUT_WRITE, + jabber_send_cb, js); + gaim_circ_buffer_append(js->write_buffer, + out + ret, olen - ret); + } + } + return; + } +#endif + + if (len == -1) + len = strlen(data); + + if (js->writeh == 0) + ret = jabber_do_send(js, data, len); + else { + ret = -1; + errno = EAGAIN; + } + + if (ret < 0 && errno != EAGAIN) + gaim_connection_error(js->gc, _("Write error")); + else if (ret < len) { + if (ret < 0) + ret = 0; + if (js->writeh == 0) + js->writeh = gaim_input_add( + js->gsc ? js->gsc->fd : js->fd, + GAIM_INPUT_WRITE, jabber_send_cb, js); + gaim_circ_buffer_append(js->write_buffer, + data + ret, len - ret); + } + return; +} + +static int jabber_prpl_send_raw(GaimConnection *gc, const char *buf, int len) +{ + JabberStream *js = (JabberStream*)gc->proto_data; + jabber_send_raw(js, buf, len); + return len; +} + +void jabber_send(JabberStream *js, xmlnode *packet) +{ + char *txt; + int len; + + gaim_signal_emit(my_protocol, "jabber-sending-xmlnode", js->gc, &packet); + + /* if we get NULL back, we're done processing */ + if(NULL == packet) + return; + + txt = xmlnode_to_str(packet, &len); + jabber_send_raw(js, txt, len); + g_free(txt); +} + +static void jabber_keepalive(GaimConnection *gc) +{ + jabber_send_raw(gc->proto_data, "\t", -1); +} + +static void +jabber_recv_cb_ssl(gpointer data, GaimSslConnection *gsc, + GaimInputCondition cond) +{ + GaimConnection *gc = data; + JabberStream *js = gc->proto_data; + int len; + static char buf[4096]; + + /* TODO: It should be possible to make this check unnecessary */ + if(!GAIM_CONNECTION_IS_VALID(gc)) { + gaim_ssl_close(gsc); + return; + } + + while((len = gaim_ssl_read(gsc, buf, sizeof(buf) - 1)) > 0) { + buf[len] = '\0'; + gaim_debug(GAIM_DEBUG_INFO, "jabber", "Recv (ssl)(%d): %s\n", len, buf); + jabber_parser_process(js, buf, len); + if(js->reinit) + jabber_stream_init(js); + } + + if(errno == EAGAIN) + return; + else + gaim_connection_error(gc, _("Read Error")); +} + +static void +jabber_recv_cb(gpointer data, gint source, GaimInputCondition condition) +{ + GaimConnection *gc = data; + JabberStream *js = gc->proto_data; + int len; + static char buf[4096]; + + if(!GAIM_CONNECTION_IS_VALID(gc)) + return; + + if((len = read(js->fd, buf, sizeof(buf) - 1)) > 0) { +#ifdef HAVE_CYRUS_SASL + if (js->sasl_maxbuf>0) { + const char *out; + unsigned int olen; + sasl_decode(js->sasl, buf, len, &out, &olen); + if (olen>0) { + gaim_debug(GAIM_DEBUG_INFO, "jabber", "RecvSASL (%u): %s\n", olen, out); + jabber_parser_process(js,out,olen); + if(js->reinit) + jabber_stream_init(js); + } + return; + } +#endif + buf[len] = '\0'; + gaim_debug(GAIM_DEBUG_INFO, "jabber", "Recv (%d): %s\n", len, buf); + jabber_parser_process(js, buf, len); + if(js->reinit) + jabber_stream_init(js); + } else if(errno == EAGAIN) { + return; + } else { + gaim_connection_error(gc, _("Read Error")); + } +} + +static void +jabber_login_callback_ssl(gpointer data, GaimSslConnection *gsc, + GaimInputCondition cond) +{ + GaimConnection *gc = data; + JabberStream *js; + + /* TODO: It should be possible to make this check unnecessary */ + if(!GAIM_CONNECTION_IS_VALID(gc)) { + gaim_ssl_close(gsc); + return; + } + + js = gc->proto_data; + + if(js->state == JABBER_STREAM_CONNECTING) + jabber_send_raw(js, "<?xml version='1.0' ?>", -1); + jabber_stream_set_state(js, JABBER_STREAM_INITIALIZING); + gaim_ssl_input_add(gsc, jabber_recv_cb_ssl, gc); +} + + +static void +jabber_login_callback(gpointer data, gint source, const gchar *error) +{ + GaimConnection *gc = data; + JabberStream *js = gc->proto_data; + + if (source < 0) { + gaim_connection_error(gc, _("Couldn't connect to host")); + return; + } + + js->fd = source; + + if(js->state == JABBER_STREAM_CONNECTING) + jabber_send_raw(js, "<?xml version='1.0' ?>", -1); + + jabber_stream_set_state(js, JABBER_STREAM_INITIALIZING); + gc->inpa = gaim_input_add(js->fd, GAIM_INPUT_READ, jabber_recv_cb, gc); +} + +static void +jabber_ssl_connect_failure(GaimSslConnection *gsc, GaimSslErrorType error, + gpointer data) +{ + GaimConnection *gc = data; + JabberStream *js = gc->proto_data; + + js->gsc = NULL; + + switch(error) { + case GAIM_SSL_CONNECT_FAILED: + gaim_connection_error(gc, _("Connection Failed")); + break; + case GAIM_SSL_HANDSHAKE_FAILED: + gaim_connection_error(gc, _("SSL Handshake Failed")); + break; + } +} + +static void tls_init(JabberStream *js) +{ + gaim_input_remove(js->gc->inpa); + js->gc->inpa = 0; + js->gsc = gaim_ssl_connect_fd(js->gc->account, js->fd, + jabber_login_callback_ssl, jabber_ssl_connect_failure, js->gc); +} + +static void jabber_login_connect(JabberStream *js, const char *server, int port) +{ + if (gaim_proxy_connect(js->gc, js->gc->account, server, + port, jabber_login_callback, js->gc) == NULL) + gaim_connection_error(js->gc, _("Unable to create socket")); +} + +static void srv_resolved_cb(GaimSrvResponse *resp, int results, gpointer data) +{ + JabberStream *js; + + js = data; + js->srv_query_data = NULL; + + if(results) { + jabber_login_connect(js, resp->hostname, resp->port); + g_free(resp); + } else { + jabber_login_connect(js, js->user->domain, + gaim_account_get_int(js->gc->account, "port", 5222)); + } +} + + + +static void +jabber_login(GaimAccount *account) +{ + GaimConnection *gc = gaim_account_get_connection(account); + const char *connect_server = gaim_account_get_string(account, + "connect_server", ""); + JabberStream *js; + JabberBuddy *my_jb = NULL; + + gc->flags |= GAIM_CONNECTION_HTML; + js = gc->proto_data = g_new0(JabberStream, 1); + js->gc = gc; + js->fd = -1; + js->iq_callbacks = g_hash_table_new_full(g_str_hash, g_str_equal, + g_free, g_free); + js->disco_callbacks = g_hash_table_new_full(g_str_hash, g_str_equal, + g_free, g_free); + js->buddies = g_hash_table_new_full(g_str_hash, g_str_equal, + g_free, (GDestroyNotify)jabber_buddy_free); + js->chats = g_hash_table_new_full(g_str_hash, g_str_equal, + g_free, (GDestroyNotify)jabber_chat_free); + js->chat_servers = g_list_append(NULL, g_strdup("conference.jabber.org")); + js->user = jabber_id_new(gaim_account_get_username(account)); + js->next_id = g_random_int(); + js->write_buffer = gaim_circ_buffer_new(512); + + if(!js->user) { + gaim_connection_error(gc, _("Invalid Jabber ID")); + return; + } + + if(!js->user->resource) { + char *me; + js->user->resource = g_strdup("Home"); + if(!js->user->node) { + js->user->node = js->user->domain; + js->user->domain = g_strdup("jabber.org"); + } + me = g_strdup_printf("%s@%s/%s", js->user->node, js->user->domain, + js->user->resource); + gaim_account_set_username(account, me); + g_free(me); + } + + if((my_jb = jabber_buddy_find(js, gaim_account_get_username(account), TRUE))) + my_jb->subscription |= JABBER_SUB_BOTH; + + jabber_stream_set_state(js, JABBER_STREAM_CONNECTING); + + /* if they've got old-ssl mode going, we probably want to ignore SRV lookups */ + if(gaim_account_get_bool(js->gc->account, "old_ssl", FALSE)) { + if(gaim_ssl_is_supported()) { + js->gsc = gaim_ssl_connect(js->gc->account, + connect_server[0] ? connect_server : js->user->domain, + gaim_account_get_int(account, "port", 5223), jabber_login_callback_ssl, + jabber_ssl_connect_failure, js->gc); + } else { + gaim_connection_error(js->gc, _("SSL support unavailable")); + } + } + + /* no old-ssl, so if they've specified a connect server, we'll use that, otherwise we'll + * invoke the magic of SRV lookups, to figure out host and port */ + if(!js->gsc) { + if(connect_server[0]) { + jabber_login_connect(js, connect_server, gaim_account_get_int(account, "port", 5222)); + } else { + js->srv_query_data = gaim_srv_resolve("xmpp-client", + "tcp", js->user->domain, srv_resolved_cb, js); + } + } +} + + +static gboolean +conn_close_cb(gpointer data) +{ + JabberStream *js = data; + GaimAccount *account = gaim_connection_get_account(js->gc); + + gaim_account_disconnect(account); + + return FALSE; +} + +static void +jabber_connection_schedule_close(JabberStream *js) +{ + gaim_timeout_add(0, conn_close_cb, js); +} + +static void +jabber_registration_result_cb(JabberStream *js, xmlnode *packet, gpointer data) +{ + const char *type = xmlnode_get_attrib(packet, "type"); + char *buf; + + if(!strcmp(type, "result")) { + buf = g_strdup_printf(_("Registration of %s@%s successful"), + js->user->node, js->user->domain); + gaim_notify_info(NULL, _("Registration Successful"), + _("Registration Successful"), buf); + g_free(buf); + } else { + char *msg = jabber_parse_error(js, packet); + + if(!msg) + msg = g_strdup(_("Unknown Error")); + + gaim_notify_error(NULL, _("Registration Failed"), + _("Registration Failed"), msg); + g_free(msg); + } + jabber_connection_schedule_close(js); +} + +static void +jabber_register_cb(JabberStream *js, GaimRequestFields *fields) +{ + GList *groups, *flds; + xmlnode *query, *y; + JabberIq *iq; + char *username; + + iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:register"); + query = xmlnode_get_child(iq->node, "query"); + + for(groups = gaim_request_fields_get_groups(fields); groups; + groups = groups->next) { + for(flds = gaim_request_field_group_get_fields(groups->data); + flds; flds = flds->next) { + GaimRequestField *field = flds->data; + const char *id = gaim_request_field_get_id(field); + const char *value = gaim_request_field_string_get_value(field); + + if(!strcmp(id, "username")) { + y = xmlnode_new_child(query, "username"); + } else if(!strcmp(id, "password")) { + y = xmlnode_new_child(query, "password"); + } else if(!strcmp(id, "name")) { + y = xmlnode_new_child(query, "name"); + } else if(!strcmp(id, "email")) { + y = xmlnode_new_child(query, "email"); + } else if(!strcmp(id, "nick")) { + y = xmlnode_new_child(query, "nick"); + } else if(!strcmp(id, "first")) { + y = xmlnode_new_child(query, "first"); + } else if(!strcmp(id, "last")) { + y = xmlnode_new_child(query, "last"); + } else if(!strcmp(id, "address")) { + y = xmlnode_new_child(query, "address"); + } else if(!strcmp(id, "city")) { + y = xmlnode_new_child(query, "city"); + } else if(!strcmp(id, "state")) { + y = xmlnode_new_child(query, "state"); + } else if(!strcmp(id, "zip")) { + y = xmlnode_new_child(query, "zip"); + } else if(!strcmp(id, "phone")) { + y = xmlnode_new_child(query, "phone"); + } else if(!strcmp(id, "url")) { + y = xmlnode_new_child(query, "url"); + } else if(!strcmp(id, "date")) { + y = xmlnode_new_child(query, "date"); + } else { + continue; + } + xmlnode_insert_data(y, value, -1); + if(!strcmp(id, "username")) { + if(js->user->node) + g_free(js->user->node); + js->user->node = g_strdup(value); + } + } + } + + username = g_strdup_printf("%s@%s/%s", js->user->node, js->user->domain, + js->user->resource); + gaim_account_set_username(js->gc->account, username); + g_free(username); + + jabber_iq_set_callback(iq, jabber_registration_result_cb, NULL); + + jabber_iq_send(iq); + +} + +static void +jabber_register_cancel_cb(JabberStream *js, GaimRequestFields *fields) +{ + jabber_connection_schedule_close(js); +} + +static void jabber_register_x_data_cb(JabberStream *js, xmlnode *result, gpointer data) +{ + xmlnode *query; + JabberIq *iq; + + iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:register"); + query = xmlnode_get_child(iq->node, "query"); + + xmlnode_insert_child(query, result); + + jabber_iq_set_callback(iq, jabber_registration_result_cb, NULL); + jabber_iq_send(iq); +} + +void jabber_register_parse(JabberStream *js, xmlnode *packet) +{ + const char *type; + if(!(type = xmlnode_get_attrib(packet, "type")) || strcmp(type, "result")) + return; + + if(js->registration) { + GaimRequestFields *fields; + GaimRequestFieldGroup *group; + GaimRequestField *field; + xmlnode *query, *x, *y; + char *instructions; + + /* get rid of the login thingy */ + gaim_connection_set_state(js->gc, GAIM_CONNECTED); + + query = xmlnode_get_child(packet, "query"); + + if(xmlnode_get_child(query, "registered")) { + gaim_notify_error(NULL, _("Already Registered"), + _("Already Registered"), NULL); + jabber_connection_schedule_close(js); + return; + } + + if((x = xmlnode_get_child_with_namespace(packet, "x", + "jabber:x:data"))) { + jabber_x_data_request(js, x, jabber_register_x_data_cb, NULL); + return; + } else if((x = xmlnode_get_child_with_namespace(packet, "x", + "jabber:x:oob"))) { + xmlnode *url; + + if((url = xmlnode_get_child(x, "url"))) { + char *href; + if((href = xmlnode_get_data(url))) { + gaim_notify_uri(NULL, href); + g_free(href); + js->gc->wants_to_die = TRUE; + jabber_connection_schedule_close(js); + return; + } + } + } + + /* as a last resort, use the old jabber:iq:register syntax */ + + fields = gaim_request_fields_new(); + group = gaim_request_field_group_new(NULL); + gaim_request_fields_add_group(fields, group); + + field = gaim_request_field_string_new("username", _("Username"), + js->user->node, FALSE); + gaim_request_field_group_add_field(group, field); + + field = gaim_request_field_string_new("password", _("Password"), + gaim_connection_get_password(js->gc), FALSE); + gaim_request_field_string_set_masked(field, TRUE); + gaim_request_field_group_add_field(group, field); + + if(xmlnode_get_child(query, "name")) { + field = gaim_request_field_string_new("name", _("Name"), + gaim_account_get_alias(js->gc->account), FALSE); + gaim_request_field_group_add_field(group, field); + } + if(xmlnode_get_child(query, "email")) { + field = gaim_request_field_string_new("email", _("E-mail"), + NULL, FALSE); + gaim_request_field_group_add_field(group, field); + } + if(xmlnode_get_child(query, "nick")) { + field = gaim_request_field_string_new("nick", _("Nickname"), + NULL, FALSE); + gaim_request_field_group_add_field(group, field); + } + if(xmlnode_get_child(query, "first")) { + field = gaim_request_field_string_new("first", _("First name"), + NULL, FALSE); + gaim_request_field_group_add_field(group, field); + } + if(xmlnode_get_child(query, "last")) { + field = gaim_request_field_string_new("last", _("Last name"), + NULL, FALSE); + gaim_request_field_group_add_field(group, field); + } + if(xmlnode_get_child(query, "address")) { + field = gaim_request_field_string_new("address", _("Address"), + NULL, FALSE); + gaim_request_field_group_add_field(group, field); + } + if(xmlnode_get_child(query, "city")) { + field = gaim_request_field_string_new("city", _("City"), + NULL, FALSE); + gaim_request_field_group_add_field(group, field); + } + if(xmlnode_get_child(query, "state")) { + field = gaim_request_field_string_new("state", _("State"), + NULL, FALSE); + gaim_request_field_group_add_field(group, field); + } + if(xmlnode_get_child(query, "zip")) { + field = gaim_request_field_string_new("zip", _("Postal code"), + NULL, FALSE); + gaim_request_field_group_add_field(group, field); + } + if(xmlnode_get_child(query, "phone")) { + field = gaim_request_field_string_new("phone", _("Phone"), + NULL, FALSE); + gaim_request_field_group_add_field(group, field); + } + if(xmlnode_get_child(query, "url")) { + field = gaim_request_field_string_new("url", _("URL"), + NULL, FALSE); + gaim_request_field_group_add_field(group, field); + } + if(xmlnode_get_child(query, "date")) { + field = gaim_request_field_string_new("date", _("Date"), + NULL, FALSE); + gaim_request_field_group_add_field(group, field); + } + + if((y = xmlnode_get_child(query, "instructions"))) + instructions = xmlnode_get_data(y); + else + instructions = g_strdup(_("Please fill out the information below " + "to register your new account.")); + + gaim_request_fields(js->gc, _("Register New Jabber Account"), + _("Register New Jabber Account"), instructions, fields, + _("Register"), G_CALLBACK(jabber_register_cb), + _("Cancel"), G_CALLBACK(jabber_register_cancel_cb), js); + + g_free(instructions); + } +} + +void jabber_register_start(JabberStream *js) +{ + JabberIq *iq; + + iq = jabber_iq_new_query(js, JABBER_IQ_GET, "jabber:iq:register"); + jabber_iq_send(iq); +} + +static void jabber_register_account(GaimAccount *account) +{ + GaimConnection *gc = gaim_account_get_connection(account); + JabberStream *js; + JabberBuddy *my_jb = NULL; + const char *connect_server = gaim_account_get_string(account, + "connect_server", ""); + const char *server; + + js = gc->proto_data = g_new0(JabberStream, 1); + js->gc = gc; + js->registration = TRUE; + js->iq_callbacks = g_hash_table_new_full(g_str_hash, g_str_equal, + g_free, g_free); + js->disco_callbacks = g_hash_table_new_full(g_str_hash, g_str_equal, + g_free, g_free); + js->user = jabber_id_new(gaim_account_get_username(account)); + js->next_id = g_random_int(); + + if(!js->user) { + gaim_connection_error(gc, _("Invalid Jabber ID")); + return; + } + + js->write_buffer = gaim_circ_buffer_new(512); + + if(!js->user->resource) { + char *me; + js->user->resource = g_strdup("Home"); + if(!js->user->node) { + js->user->node = js->user->domain; + js->user->domain = g_strdup("jabber.org"); + } + me = g_strdup_printf("%s@%s/%s", js->user->node, js->user->domain, + js->user->resource); + gaim_account_set_username(account, me); + g_free(me); + } + + if((my_jb = jabber_buddy_find(js, gaim_account_get_username(account), TRUE))) + my_jb->subscription |= JABBER_SUB_BOTH; + + server = connect_server[0] ? connect_server : js->user->domain; + + jabber_stream_set_state(js, JABBER_STREAM_CONNECTING); + + if(gaim_account_get_bool(account, "old_ssl", FALSE)) { + if(gaim_ssl_is_supported()) { + js->gsc = gaim_ssl_connect(account, server, + gaim_account_get_int(account, "port", 5222), + jabber_login_callback_ssl, jabber_ssl_connect_failure, gc); + } else { + gaim_connection_error(gc, _("SSL support unavailable")); + } + } + + if(!js->gsc) { + if (connect_server[0]) { + jabber_login_connect(js, server, + gaim_account_get_int(account, + "port", 5222)); + } else { + js->srv_query_data = gaim_srv_resolve("xmpp-client", + "tcp", + js->user->domain, + srv_resolved_cb, + js); + } + } +} + +static void jabber_close(GaimConnection *gc) +{ + JabberStream *js = gc->proto_data; + + /* Don't perform any actions on the ssl connection + * if we were forcibly disconnected because it will crash + * on some SSL backends. + */ + if (!gc->disconnect_timeout) + jabber_send_raw(js, "</stream:stream>", -1); + + if (js->srv_query_data) + gaim_srv_cancel(js->srv_query_data); + + if(js->gsc) { +#ifdef HAVE_OPENSSL + if (!gc->disconnect_timeout) +#endif + gaim_ssl_close(js->gsc); + } else if (js->fd > 0) { + if(js->gc->inpa) + gaim_input_remove(js->gc->inpa); + close(js->fd); + } + + jabber_buddy_remove_all_pending_buddy_info_requests(js); + + if(js->iq_callbacks) + g_hash_table_destroy(js->iq_callbacks); + if(js->disco_callbacks) + g_hash_table_destroy(js->disco_callbacks); + if(js->buddies) + g_hash_table_destroy(js->buddies); + if(js->chats) + g_hash_table_destroy(js->chats); + while(js->chat_servers) { + g_free(js->chat_servers->data); + js->chat_servers = g_list_delete_link(js->chat_servers, js->chat_servers); + } + while(js->user_directories) { + g_free(js->user_directories->data); + js->user_directories = g_list_delete_link(js->user_directories, js->user_directories); + } + if(js->stream_id) + g_free(js->stream_id); + if(js->user) + jabber_id_free(js->user); + if(js->avatar_hash) + g_free(js->avatar_hash); + gaim_circ_buffer_destroy(js->write_buffer); + if(js->writeh) + gaim_input_remove(js->writeh); +#ifdef HAVE_CYRUS_SASL + if(js->sasl) + sasl_dispose(&js->sasl); + if(js->sasl_mechs) + g_string_free(js->sasl_mechs, TRUE); + if(js->sasl_cb) + g_free(js->sasl_cb); +#endif + g_free(js->server_name); + g_free(js->gmail_last_time); + g_free(js->gmail_last_tid); + g_free(js); + + gc->proto_data = NULL; +} + +void jabber_stream_set_state(JabberStream *js, JabberStreamState state) +{ + js->state = state; + switch(state) { + case JABBER_STREAM_OFFLINE: + break; + case JABBER_STREAM_CONNECTING: + gaim_connection_update_progress(js->gc, _("Connecting"), 1, + JABBER_CONNECT_STEPS); + break; + case JABBER_STREAM_INITIALIZING: + gaim_connection_update_progress(js->gc, _("Initializing Stream"), + js->gsc ? 5 : 2, JABBER_CONNECT_STEPS); + jabber_stream_init(js); + break; + case JABBER_STREAM_AUTHENTICATING: + gaim_connection_update_progress(js->gc, _("Authenticating"), + js->gsc ? 6 : 3, JABBER_CONNECT_STEPS); + if(js->protocol_version == JABBER_PROTO_0_9 && js->registration) { + jabber_register_start(js); + } else if(js->auth_type == JABBER_AUTH_IQ_AUTH) { + jabber_auth_start_old(js); + } + break; + case JABBER_STREAM_REINITIALIZING: + gaim_connection_update_progress(js->gc, _("Re-initializing Stream"), + (js->gsc ? 7 : 4), JABBER_CONNECT_STEPS); + + /* The stream will be reinitialized later, in jabber_recv_cb_ssl() */ + js->reinit = TRUE; + + break; + case JABBER_STREAM_CONNECTED: + gaim_connection_set_state(js->gc, GAIM_CONNECTED); + jabber_disco_items_server(js); + break; + } +} + +char *jabber_get_next_id(JabberStream *js) +{ + return g_strdup_printf("gaim%x", js->next_id++); +} + + +static void jabber_idle_set(GaimConnection *gc, int idle) +{ + JabberStream *js = gc->proto_data; + + js->idle = idle ? time(NULL) - idle : idle; +} + +static const char *jabber_list_icon(GaimAccount *a, GaimBuddy *b) +{ + return "jabber"; +} + +static void jabber_list_emblems(GaimBuddy *b, const char **se, const char **sw, + const char **nw, const char **ne) +{ + JabberStream *js; + JabberBuddy *jb = NULL; + + if(!b->account->gc) + return; + js = b->account->gc->proto_data; + if(js) + jb = jabber_buddy_find(js, b->name, FALSE); + + if(!GAIM_BUDDY_IS_ONLINE(b)) { + if(jb && jb->error_msg) + *nw = "error"; + + if(jb && (jb->subscription & JABBER_SUB_PENDING || + !(jb->subscription & JABBER_SUB_TO))) + *se = "notauthorized"; + else + *se = "offline"; + } else { + GaimStatusType *status_type = gaim_status_get_type(gaim_presence_get_active_status(gaim_buddy_get_presence(b))); + GaimStatusPrimitive primitive = gaim_status_type_get_primitive(status_type); + + if(primitive > GAIM_STATUS_AVAILABLE) { + *se = gaim_status_type_get_id(status_type); + } + } +} + +static char *jabber_status_text(GaimBuddy *b) +{ + JabberBuddy *jb = jabber_buddy_find(b->account->gc->proto_data, b->name, + FALSE); + char *ret = NULL; + + if(jb && !GAIM_BUDDY_IS_ONLINE(b) && (jb->subscription & JABBER_SUB_PENDING || !(jb->subscription & JABBER_SUB_TO))) { + ret = g_strdup(_("Not Authorized")); + } else if(jb && !GAIM_BUDDY_IS_ONLINE(b) && jb->error_msg) { + ret = g_strdup(jb->error_msg); + } else { + char *stripped; + + if(!(stripped = gaim_markup_strip_html(jabber_buddy_get_status_msg(jb)))) { + GaimStatus *status = gaim_presence_get_active_status(gaim_buddy_get_presence(b)); + + if(!gaim_status_is_available(status)) + stripped = g_strdup(gaim_status_get_name(status)); + } + + if(stripped) { + ret = g_markup_escape_text(stripped, -1); + g_free(stripped); + } + } + + return ret; +} + +static void jabber_tooltip_text(GaimBuddy *b, GaimNotifyUserInfo *user_info, gboolean full) +{ + JabberBuddy *jb; + + g_return_if_fail(b != NULL); + g_return_if_fail(b->account != NULL); + g_return_if_fail(b->account->gc != NULL); + g_return_if_fail(b->account->gc->proto_data != NULL); + + jb = jabber_buddy_find(b->account->gc->proto_data, b->name, + FALSE); + + if(jb) { + JabberBuddyResource *jbr = NULL; + const char *sub; + GList *l; + + if (full) { + if(jb->subscription & JABBER_SUB_FROM) { + if(jb->subscription & JABBER_SUB_TO) + sub = _("Both"); + else if(jb->subscription & JABBER_SUB_PENDING) + sub = _("From (To pending)"); + else + sub = _("From"); + } else { + if(jb->subscription & JABBER_SUB_TO) + sub = _("To"); + else if(jb->subscription & JABBER_SUB_PENDING) + sub = _("None (To pending)"); + else + sub = _("None"); + } + + gaim_notify_user_info_add_pair(user_info, _("Subscription"), sub); + } + + for(l=jb->resources; l; l = l->next) { + char *text = NULL; + char *res = NULL; + char *label, *value; + const char *state; + + jbr = l->data; + + if(jbr->status) { + char *tmp; + text = gaim_strreplace(jbr->status, "\n", "<br />\n"); + tmp = gaim_markup_strip_html(text); + g_free(text); + text = g_markup_escape_text(tmp, -1); + g_free(tmp); + } + + if(jbr->name) + res = g_strdup_printf(" (%s)", jbr->name); + + state = jabber_buddy_state_get_name(jbr->state); + if (text != NULL && !gaim_utf8_strcasecmp(state, text)) { + g_free(text); + text = NULL; + } + + label = g_strdup_printf("%s%s", + _("Status"), (res ? res : "")); + value = g_strdup_printf("%s%s%s", + state, + (text ? ": " : ""), + (text ? text : "")); + + gaim_notify_user_info_add_pair(user_info, label, value); + + g_free(label); + g_free(value); + g_free(text); + g_free(res); + } + + if(!GAIM_BUDDY_IS_ONLINE(b) && jb->error_msg) { + gaim_notify_user_info_add_pair(user_info, _("Error"), jb->error_msg); + } + } +} + +static GList *jabber_status_types(GaimAccount *account) +{ + GaimStatusType *type; + GList *types = NULL; + GaimValue *priority_value; + + priority_value = gaim_value_new(GAIM_TYPE_INT); + gaim_value_set_int(priority_value, 1); + type = gaim_status_type_new_with_attrs(GAIM_STATUS_AVAILABLE, + jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_ONLINE), + NULL, TRUE, TRUE, FALSE, + "priority", _("Priority"), priority_value, + "message", _("Message"), gaim_value_new(GAIM_TYPE_STRING), + NULL); + types = g_list_append(types, type); + + priority_value = gaim_value_new(GAIM_TYPE_INT); + gaim_value_set_int(priority_value, 1); + type = gaim_status_type_new_with_attrs(GAIM_STATUS_AVAILABLE, + jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_CHAT), + _("Chatty"), TRUE, TRUE, FALSE, + "priority", _("Priority"), priority_value, + "message", _("Message"), gaim_value_new(GAIM_TYPE_STRING), + NULL); + types = g_list_append(types, type); + + priority_value = gaim_value_new(GAIM_TYPE_INT); + gaim_value_set_int(priority_value, 0); + type = gaim_status_type_new_with_attrs(GAIM_STATUS_AWAY, + jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_AWAY), + NULL, TRUE, TRUE, FALSE, + "priority", _("Priority"), priority_value, + "message", _("Message"), gaim_value_new(GAIM_TYPE_STRING), + NULL); + types = g_list_append(types, type); + + priority_value = gaim_value_new(GAIM_TYPE_INT); + gaim_value_set_int(priority_value, 0); + type = gaim_status_type_new_with_attrs(GAIM_STATUS_EXTENDED_AWAY, + jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_XA), + NULL, TRUE, TRUE, FALSE, + "priority", _("Priority"), priority_value, + "message", _("Message"), gaim_value_new(GAIM_TYPE_STRING), + NULL); + types = g_list_append(types, type); + + priority_value = gaim_value_new(GAIM_TYPE_INT); + gaim_value_set_int(priority_value, 0); + type = gaim_status_type_new_with_attrs(GAIM_STATUS_UNAVAILABLE, + jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_DND), + _("Do Not Disturb"), TRUE, TRUE, FALSE, + "priority", _("Priority"), priority_value, + "message", _("Message"), gaim_value_new(GAIM_TYPE_STRING), + NULL); + types = g_list_append(types, type); + + /* + if(js->protocol_version == JABBER_PROTO_0_9) + m = g_list_append(m, _("Invisible")); + */ + + type = gaim_status_type_new_with_attrs(GAIM_STATUS_OFFLINE, + jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_UNAVAILABLE), + NULL, FALSE, TRUE, FALSE, + "message", _("Message"), gaim_value_new(GAIM_TYPE_STRING), + NULL); + types = g_list_append(types, type); + + return types; +} + +static void +jabber_password_change_result_cb(JabberStream *js, xmlnode *packet, + gpointer data) +{ + const char *type; + + type = xmlnode_get_attrib(packet, "type"); + + if(type && !strcmp(type, "result")) { + gaim_notify_info(js->gc, _("Password Changed"), _("Password Changed"), + _("Your password has been changed.")); + } else { + char *msg = jabber_parse_error(js, packet); + + gaim_notify_error(js->gc, _("Error changing password"), + _("Error changing password"), msg); + g_free(msg); + } +} + +static void jabber_password_change_cb(JabberStream *js, + GaimRequestFields *fields) +{ + const char *p1, *p2; + JabberIq *iq; + xmlnode *query, *y; + + p1 = gaim_request_fields_get_string(fields, "password1"); + p2 = gaim_request_fields_get_string(fields, "password2"); + + if(strcmp(p1, p2)) { + gaim_notify_error(js->gc, NULL, _("New passwords do not match."), NULL); + return; + } + + iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:register"); + + xmlnode_set_attrib(iq->node, "to", js->user->domain); + + query = xmlnode_get_child(iq->node, "query"); + + y = xmlnode_new_child(query, "username"); + xmlnode_insert_data(y, js->user->node, -1); + y = xmlnode_new_child(query, "password"); + xmlnode_insert_data(y, p1, -1); + + jabber_iq_set_callback(iq, jabber_password_change_result_cb, NULL); + + jabber_iq_send(iq); + + gaim_account_set_password(js->gc->account, p1); +} + +static void jabber_password_change(GaimPluginAction *action) +{ + + GaimConnection *gc = (GaimConnection *) action->context; + JabberStream *js = gc->proto_data; + GaimRequestFields *fields; + GaimRequestFieldGroup *group; + GaimRequestField *field; + + fields = gaim_request_fields_new(); + group = gaim_request_field_group_new(NULL); + gaim_request_fields_add_group(fields, group); + + field = gaim_request_field_string_new("password1", _("Password"), + "", FALSE); + gaim_request_field_string_set_masked(field, TRUE); + gaim_request_field_group_add_field(group, field); + + field = gaim_request_field_string_new("password2", _("Password (again)"), + "", FALSE); + gaim_request_field_string_set_masked(field, TRUE); + gaim_request_field_group_add_field(group, field); + + gaim_request_fields(js->gc, _("Change Jabber Password"), + _("Change Jabber Password"), _("Please enter your new password"), + fields, _("OK"), G_CALLBACK(jabber_password_change_cb), + _("Cancel"), NULL, js); +} + +static GList *jabber_actions(GaimPlugin *plugin, gpointer context) +{ + GList *m = NULL; + GaimPluginAction *act; + + act = gaim_plugin_action_new(_("Set User Info..."), + jabber_setup_set_info); + m = g_list_append(m, act); + + /* if (js->protocol_options & CHANGE_PASSWORD) { */ + act = gaim_plugin_action_new(_("Change Password..."), + jabber_password_change); + m = g_list_append(m, act); + /* } */ + + act = gaim_plugin_action_new(_("Search for Users..."), + jabber_user_search_begin); + m = g_list_append(m, act); + + return m; +} + +static GaimChat *jabber_find_blist_chat(GaimAccount *account, const char *name) +{ + GaimBlistNode *gnode, *cnode; + JabberID *jid; + + if(!(jid = jabber_id_new(name))) + return NULL; + + for(gnode = gaim_get_blist()->root; gnode; gnode = gnode->next) { + for(cnode = gnode->child; cnode; cnode = cnode->next) { + GaimChat *chat = (GaimChat*)cnode; + const char *room, *server; + if(!GAIM_BLIST_NODE_IS_CHAT(cnode)) + continue; + + if(chat->account != account) + continue; + + if(!(room = g_hash_table_lookup(chat->components, "room"))) + continue; + if(!(server = g_hash_table_lookup(chat->components, "server"))) + continue; + + if(jid->node && jid->domain && + !g_utf8_collate(room, jid->node) && !g_utf8_collate(server, jid->domain)) { + jabber_id_free(jid); + return chat; + } + } + } + jabber_id_free(jid); + return NULL; +} + +static void jabber_convo_closed(GaimConnection *gc, const char *who) +{ + JabberStream *js = gc->proto_data; + JabberID *jid; + JabberBuddy *jb; + JabberBuddyResource *jbr; + + if(!(jid = jabber_id_new(who))) + return; + + if((jb = jabber_buddy_find(js, who, TRUE)) && + (jbr = jabber_buddy_find_resource(jb, jid->resource))) { + if(jbr->thread_id) { + g_free(jbr->thread_id); + jbr->thread_id = NULL; + } + } + + jabber_id_free(jid); +} + + +char *jabber_parse_error(JabberStream *js, xmlnode *packet) +{ + xmlnode *error; + const char *code = NULL, *text = NULL; + const char *xmlns = xmlnode_get_namespace(packet); + char *cdata = NULL; + + if((error = xmlnode_get_child(packet, "error"))) { + cdata = xmlnode_get_data(error); + code = xmlnode_get_attrib(error, "code"); + + /* Stanza errors */ + if(xmlnode_get_child(error, "bad-request")) { + text = _("Bad Request"); + } else if(xmlnode_get_child(error, "conflict")) { + text = _("Conflict"); + } else if(xmlnode_get_child(error, "feature-not-implemented")) { + text = _("Feature Not Implemented"); + } else if(xmlnode_get_child(error, "forbidden")) { + text = _("Forbidden"); + } else if(xmlnode_get_child(error, "gone")) { + text = _("Gone"); + } else if(xmlnode_get_child(error, "internal-server-error")) { + text = _("Internal Server Error"); + } else if(xmlnode_get_child(error, "item-not-found")) { + text = _("Item Not Found"); + } else if(xmlnode_get_child(error, "jid-malformed")) { + text = _("Malformed Jabber ID"); + } else if(xmlnode_get_child(error, "not-acceptable")) { + text = _("Not Acceptable"); + } else if(xmlnode_get_child(error, "not-allowed")) { + text = _("Not Allowed"); + } else if(xmlnode_get_child(error, "not-authorized")) { + text = _("Not Authorized"); + } else if(xmlnode_get_child(error, "payment-required")) { + text = _("Payment Required"); + } else if(xmlnode_get_child(error, "recipient-unavailable")) { + text = _("Recipient Unavailable"); + } else if(xmlnode_get_child(error, "redirect")) { + /* XXX */ + } else if(xmlnode_get_child(error, "registration-required")) { + text = _("Registration Required"); + } else if(xmlnode_get_child(error, "remote-server-not-found")) { + text = _("Remote Server Not Found"); + } else if(xmlnode_get_child(error, "remote-server-timeout")) { + text = _("Remote Server Timeout"); + } else if(xmlnode_get_child(error, "resource-constraint")) { + text = _("Server Overloaded"); + } else if(xmlnode_get_child(error, "service-unavailable")) { + text = _("Service Unavailable"); + } else if(xmlnode_get_child(error, "subscription-required")) { + text = _("Subscription Required"); + } else if(xmlnode_get_child(error, "unexpected-request")) { + text = _("Unexpected Request"); + } else if(xmlnode_get_child(error, "undefined-condition")) { + text = _("Unknown Error"); + } + } else if(xmlns && !strcmp(xmlns, "urn:ietf:params:xml:ns:xmpp-sasl")) { + if(xmlnode_get_child(packet, "aborted")) { + js->gc->wants_to_die = TRUE; + text = _("Authorization Aborted"); + } else if(xmlnode_get_child(packet, "incorrect-encoding")) { + text = _("Incorrect encoding in authorization"); + } else if(xmlnode_get_child(packet, "invalid-authzid")) { + js->gc->wants_to_die = TRUE; + text = _("Invalid authzid"); + } else if(xmlnode_get_child(packet, "invalid-mechanism")) { + js->gc->wants_to_die = TRUE; + text = _("Invalid Authorization Mechanism"); + } else if(xmlnode_get_child(packet, "mechanism-too-weak")) { + js->gc->wants_to_die = TRUE; + text = _("Authorization mechanism too weak"); + } else if(xmlnode_get_child(packet, "not-authorized")) { + js->gc->wants_to_die = TRUE; + text = _("Not Authorized"); + } else if(xmlnode_get_child(packet, "temporary-auth-failure")) { + text = _("Temporary Authentication Failure"); + } else { + js->gc->wants_to_die = TRUE; + text = _("Authentication Failure"); + } + } else if(!strcmp(packet->name, "stream:error")) { + if(xmlnode_get_child(packet, "bad-format")) { + text = _("Bad Format"); + } else if(xmlnode_get_child(packet, "bad-namespace-prefix")) { + text = _("Bad Namespace Prefix"); + } else if(xmlnode_get_child(packet, "conflict")) { + js->gc->wants_to_die = TRUE; + text = _("Resource Conflict"); + } else if(xmlnode_get_child(packet, "connection-timeout")) { + text = _("Connection Timeout"); + } else if(xmlnode_get_child(packet, "host-gone")) { + text = _("Host Gone"); + } else if(xmlnode_get_child(packet, "host-unknown")) { + text = _("Host Unknown"); + } else if(xmlnode_get_child(packet, "improper-addressing")) { + text = _("Improper Addressing"); + } else if(xmlnode_get_child(packet, "internal-server-error")) { + text = _("Internal Server Error"); + } else if(xmlnode_get_child(packet, "invalid-id")) { + text = _("Invalid ID"); + } else if(xmlnode_get_child(packet, "invalid-namespace")) { + text = _("Invalid Namespace"); + } else if(xmlnode_get_child(packet, "invalid-xml")) { + text = _("Invalid XML"); + } else if(xmlnode_get_child(packet, "nonmatching-hosts")) { + text = _("Non-matching Hosts"); + } else if(xmlnode_get_child(packet, "not-authorized")) { + text = _("Not Authorized"); + } else if(xmlnode_get_child(packet, "policy-violation")) { + text = _("Policy Violation"); + } else if(xmlnode_get_child(packet, "remote-connection-failed")) { + text = _("Remote Connection Failed"); + } else if(xmlnode_get_child(packet, "resource-constraint")) { + text = _("Resource Constraint"); + } else if(xmlnode_get_child(packet, "restricted-xml")) { + text = _("Restricted XML"); + } else if(xmlnode_get_child(packet, "see-other-host")) { + text = _("See Other Host"); + } else if(xmlnode_get_child(packet, "system-shutdown")) { + text = _("System Shutdown"); + } else if(xmlnode_get_child(packet, "undefined-condition")) { + text = _("Undefined Condition"); + } else if(xmlnode_get_child(packet, "unsupported-encoding")) { + text = _("Unsupported Encoding"); + } else if(xmlnode_get_child(packet, "unsupported-stanza-type")) { + text = _("Unsupported Stanza Type"); + } else if(xmlnode_get_child(packet, "unsupported-version")) { + text = _("Unsupported Version"); + } else if(xmlnode_get_child(packet, "xml-not-well-formed")) { + text = _("XML Not Well Formed"); + } else { + text = _("Stream Error"); + } + } + + if(text || cdata) { + char *ret = g_strdup_printf("%s%s%s", code ? code : "", + code ? ": " : "", text ? text : cdata); + g_free(cdata); + return ret; + } else { + return NULL; + } +} + +static GaimCmdRet jabber_cmd_chat_config(GaimConversation *conv, + const char *cmd, char **args, char **error, void *data) +{ + JabberChat *chat = jabber_chat_find_by_conv(conv); + jabber_chat_request_room_configure(chat); + return GAIM_CMD_RET_OK; +} + +static GaimCmdRet jabber_cmd_chat_register(GaimConversation *conv, + const char *cmd, char **args, char **error, void *data) +{ + JabberChat *chat = jabber_chat_find_by_conv(conv); + jabber_chat_register(chat); + return GAIM_CMD_RET_OK; +} + +static GaimCmdRet jabber_cmd_chat_topic(GaimConversation *conv, + const char *cmd, char **args, char **error, void *data) +{ + JabberChat *chat = jabber_chat_find_by_conv(conv); + jabber_chat_change_topic(chat, args ? args[0] : NULL); + return GAIM_CMD_RET_OK; +} + +static GaimCmdRet jabber_cmd_chat_nick(GaimConversation *conv, + const char *cmd, char **args, char **error, void *data) +{ + JabberChat *chat = jabber_chat_find_by_conv(conv); + + if(!args || !args[0]) + return GAIM_CMD_RET_FAILED; + + jabber_chat_change_nick(chat, args[0]); + return GAIM_CMD_RET_OK; +} + +static GaimCmdRet jabber_cmd_chat_part(GaimConversation *conv, + const char *cmd, char **args, char **error, void *data) +{ + JabberChat *chat = jabber_chat_find_by_conv(conv); + jabber_chat_part(chat, args ? args[0] : NULL); + return GAIM_CMD_RET_OK; +} + +static GaimCmdRet jabber_cmd_chat_ban(GaimConversation *conv, + const char *cmd, char **args, char **error, void *data) +{ + JabberChat *chat = jabber_chat_find_by_conv(conv); + + if(!args || !args[0]) + return GAIM_CMD_RET_FAILED; + + if(!jabber_chat_ban_user(chat, args[0], args[1])) { + *error = g_strdup_printf(_("Unable to ban user %s"), args[0]); + return GAIM_CMD_RET_FAILED; + } + + return GAIM_CMD_RET_OK; +} + +static GaimCmdRet jabber_cmd_chat_affiliate(GaimConversation *conv, + const char *cmd, char **args, char **error, void *data) +{ + JabberChat *chat = jabber_chat_find_by_conv(conv); + + if (!args || !args[0] || !args[1]) + return GAIM_CMD_RET_FAILED; + + if (strcmp(args[1], "owner") != 0 && + strcmp(args[1], "admin") != 0 && + strcmp(args[1], "member") != 0 && + strcmp(args[1], "outcast") != 0 && + strcmp(args[1], "none") != 0) { + *error = g_strdup_printf(_("Unknown affiliation: \"%s\""), args[1]); + return GAIM_CMD_RET_FAILED; + } + + if (!jabber_chat_affiliate_user(chat, args[0], args[1])) { + *error = g_strdup_printf(_("Unable to affiliate user %s as \"%s\""), args[0], args[1]); + return GAIM_CMD_RET_FAILED; + } + + return GAIM_CMD_RET_OK; +} + +static GaimCmdRet jabber_cmd_chat_role(GaimConversation *conv, + const char *cmd, char **args, char **error, void *data) +{ + JabberChat *chat; + + if (!args || !args[0] || !args[1]) + return GAIM_CMD_RET_FAILED; + + if (strcmp(args[1], "moderator") != 0 && + strcmp(args[1], "participant") != 0 && + strcmp(args[1], "visitor") != 0 && + strcmp(args[1], "none") != 0) { + *error = g_strdup_printf(_("Unknown role: \"%s\""), args[1]); + return GAIM_CMD_RET_FAILED; + } + + chat = jabber_chat_find_by_conv(conv); + + if (!jabber_chat_role_user(chat, args[0], args[1])) { + *error = g_strdup_printf(_("Unable to set role \"%s\" for user: %s"), + args[1], args[0]); + return GAIM_CMD_RET_FAILED; + } + + return GAIM_CMD_RET_OK; +} + +static GaimCmdRet jabber_cmd_chat_invite(GaimConversation *conv, + const char *cmd, char **args, char **error, void *data) +{ + if(!args || !args[0]) + return GAIM_CMD_RET_FAILED; + + jabber_chat_invite(gaim_conversation_get_gc(conv), + gaim_conv_chat_get_id(GAIM_CONV_CHAT(conv)), args[1] ? args[1] : "", + args[0]); + + return GAIM_CMD_RET_OK; +} + +static GaimCmdRet jabber_cmd_chat_join(GaimConversation *conv, + const char *cmd, char **args, char **error, void *data) +{ + JabberChat *chat = jabber_chat_find_by_conv(conv); + GHashTable *components; + + if(!args || !args[0]) + return GAIM_CMD_RET_FAILED; + + components = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, NULL); + + g_hash_table_replace(components, "room", args[0]); + g_hash_table_replace(components, "server", chat->server); + g_hash_table_replace(components, "handle", chat->handle); + if(args[1]) + g_hash_table_replace(components, "password", args[1]); + + jabber_chat_join(gaim_conversation_get_gc(conv), components); + + g_hash_table_destroy(components); + return GAIM_CMD_RET_OK; +} + +static GaimCmdRet jabber_cmd_chat_kick(GaimConversation *conv, + const char *cmd, char **args, char **error, void *data) +{ + JabberChat *chat = jabber_chat_find_by_conv(conv); + + if(!args || !args[0]) + return GAIM_CMD_RET_FAILED; + + if(!jabber_chat_kick_user(chat, args[0], args[1])) { + *error = g_strdup_printf(_("Unable to kick user %s"), args[0]); + return GAIM_CMD_RET_FAILED; + } + + return GAIM_CMD_RET_OK; +} + +static GaimCmdRet jabber_cmd_chat_msg(GaimConversation *conv, + const char *cmd, char **args, char **error, void *data) +{ + JabberChat *chat = jabber_chat_find_by_conv(conv); + char *who; + + who = g_strdup_printf("%s@%s/%s", chat->room, chat->server, args[0]); + + jabber_message_send_im(gaim_conversation_get_gc(conv), who, args[1], 0); + + g_free(who); + return GAIM_CMD_RET_OK; +} + +static gboolean jabber_offline_message(const GaimBuddy *buddy) +{ + return TRUE; +} + +static void jabber_register_commands(void) +{ + gaim_cmd_register("config", "", GAIM_CMD_P_PRPL, + GAIM_CMD_FLAG_CHAT | GAIM_CMD_FLAG_PRPL_ONLY, + "prpl-jabber", jabber_cmd_chat_config, + _("config: Configure a chat room."), NULL); + gaim_cmd_register("configure", "", GAIM_CMD_P_PRPL, + GAIM_CMD_FLAG_CHAT | GAIM_CMD_FLAG_PRPL_ONLY, + "prpl-jabber", jabber_cmd_chat_config, + _("configure: Configure a chat room."), NULL); + gaim_cmd_register("nick", "s", GAIM_CMD_P_PRPL, + GAIM_CMD_FLAG_CHAT | GAIM_CMD_FLAG_PRPL_ONLY, + "prpl-jabber", jabber_cmd_chat_nick, + _("nick <new nickname>: Change your nickname."), + NULL); + gaim_cmd_register("part", "s", GAIM_CMD_P_PRPL, + GAIM_CMD_FLAG_CHAT | GAIM_CMD_FLAG_PRPL_ONLY | + GAIM_CMD_FLAG_ALLOW_WRONG_ARGS, "prpl-jabber", + jabber_cmd_chat_part, _("part [room]: Leave the room."), + NULL); + gaim_cmd_register("register", "", GAIM_CMD_P_PRPL, + GAIM_CMD_FLAG_CHAT | GAIM_CMD_FLAG_PRPL_ONLY, + "prpl-jabber", jabber_cmd_chat_register, + _("register: Register with a chat room."), NULL); + /* XXX: there needs to be a core /topic cmd, methinks */ + gaim_cmd_register("topic", "s", GAIM_CMD_P_PRPL, + GAIM_CMD_FLAG_CHAT | GAIM_CMD_FLAG_PRPL_ONLY | + GAIM_CMD_FLAG_ALLOW_WRONG_ARGS, "prpl-jabber", + jabber_cmd_chat_topic, + _("topic [new topic]: View or change the topic."), + NULL); + gaim_cmd_register("ban", "ws", GAIM_CMD_P_PRPL, + GAIM_CMD_FLAG_CHAT | GAIM_CMD_FLAG_PRPL_ONLY | + GAIM_CMD_FLAG_ALLOW_WRONG_ARGS, "prpl-jabber", + jabber_cmd_chat_ban, + _("ban <user> [room]: Ban a user from the room."), + NULL); + gaim_cmd_register("affiliate", "ws", GAIM_CMD_P_PRPL, + GAIM_CMD_FLAG_CHAT | GAIM_CMD_FLAG_PRPL_ONLY | + GAIM_CMD_FLAG_ALLOW_WRONG_ARGS, "prpl-jabber", + jabber_cmd_chat_affiliate, + _("affiliate <user> <owner|admin|member|outcast|none>: Set a user's affiliation with the room."), + NULL); + gaim_cmd_register("role", "ws", GAIM_CMD_P_PRPL, + GAIM_CMD_FLAG_CHAT | GAIM_CMD_FLAG_PRPL_ONLY | + GAIM_CMD_FLAG_ALLOW_WRONG_ARGS, "prpl-jabber", + jabber_cmd_chat_role, + _("role <user> <moderator|participant|visitor|none>: Set a user's role in the room."), + NULL); + gaim_cmd_register("invite", "ws", GAIM_CMD_P_PRPL, + GAIM_CMD_FLAG_CHAT | GAIM_CMD_FLAG_PRPL_ONLY | + GAIM_CMD_FLAG_ALLOW_WRONG_ARGS, "prpl-jabber", + jabber_cmd_chat_invite, + _("invite <user> [message]: Invite a user to the room."), + NULL); + gaim_cmd_register("join", "ws", GAIM_CMD_P_PRPL, + GAIM_CMD_FLAG_CHAT | GAIM_CMD_FLAG_PRPL_ONLY | + GAIM_CMD_FLAG_ALLOW_WRONG_ARGS, "prpl-jabber", + jabber_cmd_chat_join, + _("join: <room> [server]: Join a chat on this server."), + NULL); + gaim_cmd_register("kick", "ws", GAIM_CMD_P_PRPL, + GAIM_CMD_FLAG_CHAT | GAIM_CMD_FLAG_PRPL_ONLY | + GAIM_CMD_FLAG_ALLOW_WRONG_ARGS, "prpl-jabber", + jabber_cmd_chat_kick, + _("kick <user> [room]: Kick a user from the room."), + NULL); + gaim_cmd_register("msg", "ws", GAIM_CMD_P_PRPL, + GAIM_CMD_FLAG_CHAT | GAIM_CMD_FLAG_PRPL_ONLY, + "prpl-jabber", jabber_cmd_chat_msg, + _("msg <user> <message>: Send a private message to another user."), + NULL); +} + +static GaimPluginProtocolInfo prpl_info = +{ + OPT_PROTO_CHAT_TOPIC | OPT_PROTO_UNIQUE_CHATNAME | OPT_PROTO_MAIL_CHECK, + NULL, /* user_splits */ + NULL, /* protocol_options */ + {"png,gif,jpeg", 32, 32, 96, 96, 8191, GAIM_ICON_SCALE_SEND | GAIM_ICON_SCALE_DISPLAY}, /* icon_spec */ + jabber_list_icon, /* list_icon */ + jabber_list_emblems, /* list_emblems */ + jabber_status_text, /* status_text */ + jabber_tooltip_text, /* tooltip_text */ + jabber_status_types, /* status_types */ + jabber_blist_node_menu, /* blist_node_menu */ + jabber_chat_info, /* chat_info */ + jabber_chat_info_defaults, /* chat_info_defaults */ + jabber_login, /* login */ + jabber_close, /* close */ + jabber_message_send_im, /* send_im */ + jabber_set_info, /* set_info */ + jabber_send_typing, /* send_typing */ + jabber_buddy_get_info, /* get_info */ + jabber_presence_send, /* set_away */ + jabber_idle_set, /* set_idle */ + NULL, /* change_passwd */ + jabber_roster_add_buddy, /* add_buddy */ + NULL, /* add_buddies */ + jabber_roster_remove_buddy, /* remove_buddy */ + NULL, /* remove_buddies */ + NULL, /* add_permit */ + jabber_google_roster_add_deny, /* add_deny */ + NULL, /* rem_permit */ + jabber_google_roster_rem_deny, /* rem_deny */ + NULL, /* set_permit_deny */ + jabber_chat_join, /* join_chat */ + NULL, /* reject_chat */ + jabber_get_chat_name, /* get_chat_name */ + jabber_chat_invite, /* chat_invite */ + jabber_chat_leave, /* chat_leave */ + NULL, /* chat_whisper */ + jabber_message_send_chat, /* chat_send */ + jabber_keepalive, /* keepalive */ + jabber_register_account, /* register_user */ + jabber_buddy_get_info_chat, /* get_cb_info */ + NULL, /* get_cb_away */ + jabber_roster_alias_change, /* alias_buddy */ + jabber_roster_group_change, /* group_buddy */ + jabber_roster_group_rename, /* rename_group */ + NULL, /* buddy_free */ + jabber_convo_closed, /* convo_closed */ + jabber_normalize, /* normalize */ + jabber_set_buddy_icon, /* set_buddy_icon */ + NULL, /* remove_group */ + jabber_chat_buddy_real_name, /* get_cb_real_name */ + jabber_chat_set_topic, /* set_chat_topic */ + jabber_find_blist_chat, /* find_blist_chat */ + jabber_roomlist_get_list, /* roomlist_get_list */ + jabber_roomlist_cancel, /* roomlist_cancel */ + NULL, /* roomlist_expand_category */ + NULL, /* can_receive_file */ + jabber_si_xfer_send, /* send_file */ + jabber_si_new_xfer, /* new_xfer */ + jabber_offline_message, /* offline_message */ + NULL, /* whiteboard_prpl_ops */ + jabber_prpl_send_raw, /* send_raw */ + jabber_roomlist_room_serialize, /* roomlist_room_serialize */ +}; + +static gboolean load_plugin(GaimPlugin *plugin) +{ + gaim_signal_register(plugin, "jabber-receiving-xmlnode", + gaim_marshal_VOID__POINTER_POINTER, NULL, 2, + gaim_value_new(GAIM_TYPE_SUBTYPE, GAIM_SUBTYPE_CONNECTION), + gaim_value_new_outgoing(GAIM_TYPE_SUBTYPE, GAIM_SUBTYPE_XMLNODE)); + + gaim_signal_register(plugin, "jabber-sending-xmlnode", + gaim_marshal_VOID__POINTER_POINTER, NULL, 2, + gaim_value_new(GAIM_TYPE_SUBTYPE, GAIM_SUBTYPE_CONNECTION), + gaim_value_new_outgoing(GAIM_TYPE_SUBTYPE, GAIM_SUBTYPE_XMLNODE)); + + gaim_signal_register(plugin, "jabber-sending-text", + gaim_marshal_VOID__POINTER_POINTER, NULL, 2, + gaim_value_new(GAIM_TYPE_SUBTYPE, GAIM_SUBTYPE_CONNECTION), + gaim_value_new_outgoing(GAIM_TYPE_STRING)); + + + return TRUE; +} + +static gboolean unload_plugin(GaimPlugin *plugin) +{ + gaim_signal_unregister(plugin, "jabber-receiving-xmlnode"); + + gaim_signal_unregister(plugin, "jabber-sending-xmlnode"); + + gaim_signal_unregister(plugin, "jabber-sending-text"); + + return TRUE; +} + +static GaimPluginInfo info = +{ + GAIM_PLUGIN_MAGIC, + GAIM_MAJOR_VERSION, + GAIM_MINOR_VERSION, + GAIM_PLUGIN_PROTOCOL, /**< type */ + NULL, /**< ui_requirement */ + 0, /**< flags */ + NULL, /**< dependencies */ + GAIM_PRIORITY_DEFAULT, /**< priority */ + + "prpl-jabber", /**< id */ + "Jabber", /**< name */ + VERSION, /**< version */ + /** summary */ + N_("Jabber Protocol Plugin"), + /** description */ + N_("Jabber Protocol Plugin"), + NULL, /**< author */ + GAIM_WEBSITE, /**< homepage */ + + load_plugin, /**< load */ + unload_plugin, /**< unload */ + NULL, /**< destroy */ + + NULL, /**< ui_info */ + &prpl_info, /**< extra_info */ + NULL, /**< prefs_info */ + jabber_actions +}; + +static void +init_plugin(GaimPlugin *plugin) +{ + GaimAccountUserSplit *split; + GaimAccountOption *option; + + split = gaim_account_user_split_new(_("Server"), "jabber.org", '@'); + prpl_info.user_splits = g_list_append(prpl_info.user_splits, split); + + split = gaim_account_user_split_new(_("Resource"), "Home", '/'); + prpl_info.user_splits = g_list_append(prpl_info.user_splits, split); + + option = gaim_account_option_bool_new(_("Force old (port 5223) SSL"), "old_ssl", FALSE); + prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, + option); + + option = gaim_account_option_bool_new( + _("Allow plaintext auth over unencrypted streams"), + "auth_plain_in_clear", FALSE); + prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, + option); + + option = gaim_account_option_int_new(_("Connect port"), "port", 5222); + prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, + option); + + option = gaim_account_option_string_new(_("Connect server"), + "connect_server", NULL); + prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, + option); + + my_protocol = plugin; + + gaim_prefs_remove("/plugins/prpl/jabber"); + + /* XXX - If any other plugin wants SASL this won't be good ... */ +#ifdef HAVE_CYRUS_SASL + sasl_client_init(NULL); +#endif + jabber_register_commands(); + + jabber_iq_init(); +} + +GAIM_INIT_PLUGIN(jabber, init_plugin, info); diff --git a/libpurple/protocols/jabber/jabber.h b/libpurple/protocols/jabber/jabber.h new file mode 100644 index 0000000000..412a2a4de5 --- /dev/null +++ b/libpurple/protocols/jabber/jabber.h @@ -0,0 +1,165 @@ +/** + * @file jabber.h + * + * gaim + * + * Copyright (C) 2003 Nathan Walp <faceprint@faceprint.com> + * + * 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 + */ +#ifndef _GAIM_JABBER_H_ +#define _GAIM_JABBER_H_ + +#include <libxml/parser.h> +#include <glib.h> +#include "circbuffer.h" +#include "connection.h" +#include "dnssrv.h" +#include "roomlist.h" +#include "sslconn.h" + +#include "jutil.h" +#include "xmlnode.h" + +#ifdef HAVE_CYRUS_SASL +#include <sasl/sasl.h> +#endif + +#define CAPS0115_NODE "http://gaim.sf.net/caps" + +typedef enum { + JABBER_CAP_NONE = 0, + JABBER_CAP_XHTML = 1 << 0, + JABBER_CAP_COMPOSING = 1 << 1, + JABBER_CAP_SI = 1 << 2, + JABBER_CAP_SI_FILE_XFER = 1 << 3, + JABBER_CAP_BYTESTREAMS = 1 << 4, + JABBER_CAP_IBB = 1 << 5, + JABBER_CAP_CHAT_STATES = 1 << 6, + JABBER_CAP_IQ_SEARCH = 1 << 7, + JABBER_CAP_IQ_REGISTER = 1 << 8, + + /* Google Talk extensions: + * http://code.google.com/apis/talk/jep_extensions/extensions.html + */ + JABBER_CAP_GMAIL_NOTIFY = 1 << 9, + JABBER_CAP_GOOGLE_ROSTER = 1 << 10, + + JABBER_CAP_RETRIEVED = 1 << 31 +} JabberCapabilities; + +typedef enum { + JABBER_STREAM_OFFLINE, + JABBER_STREAM_CONNECTING, + JABBER_STREAM_INITIALIZING, + JABBER_STREAM_AUTHENTICATING, + JABBER_STREAM_REINITIALIZING, + JABBER_STREAM_CONNECTED +} JabberStreamState; + +typedef struct _JabberStream +{ + int fd; + + GaimSrvQueryData *srv_query_data; + + xmlParserCtxt *context; + xmlnode *current; + + enum { + JABBER_PROTO_0_9, + JABBER_PROTO_1_0 + } protocol_version; + enum { + JABBER_AUTH_UNKNOWN, + JABBER_AUTH_DIGEST_MD5, + JABBER_AUTH_PLAIN, + JABBER_AUTH_IQ_AUTH, + JABBER_AUTH_CYRUS + } auth_type; + char *stream_id; + JabberStreamState state; + + /* SASL authentication */ + char *expected_rspauth; + + GHashTable *buddies; + gboolean roster_parsed; + + GHashTable *chats; + GList *chat_servers; + GaimRoomlist *roomlist; + GList *user_directories; + + GHashTable *iq_callbacks; + GHashTable *disco_callbacks; + int next_id; + + + GList *oob_file_transfers; + GList *file_transfers; + + time_t idle; + + JabberID *user; + GaimConnection *gc; + GaimSslConnection *gsc; + + gboolean registration; + + char *avatar_hash; + GSList *pending_avatar_requests; + + GSList *pending_buddy_info_requests; + + GaimCircBuffer *write_buffer; + guint writeh; + + gboolean reinit; + + JabberCapabilities server_caps; + gboolean googletalk; + char *server_name; + + char *gmail_last_time; + char *gmail_last_tid; + + /* OK, this stays at the end of the struct, so plugins can depend + * on the rest of the stuff being in the right place + */ +#ifdef HAVE_CYRUS_SASL + sasl_conn_t *sasl; + sasl_callback_t *sasl_cb; + int sasl_state; + int sasl_maxbuf; + GString *sasl_mechs; +#endif + +} JabberStream; + +void jabber_process_packet(JabberStream *js, xmlnode *packet); +void jabber_send(JabberStream *js, xmlnode *data); +void jabber_send_raw(JabberStream *js, const char *data, int len); + +void jabber_stream_set_state(JabberStream *js, JabberStreamState state); + +void jabber_register_parse(JabberStream *js, xmlnode *packet); +void jabber_register_start(JabberStream *js); + +char *jabber_get_next_id(JabberStream *js); + +char *jabber_parse_error(JabberStream *js, xmlnode *packet); + +#endif /* _GAIM_JABBER_H_ */ diff --git a/libpurple/protocols/jabber/jutil.c b/libpurple/protocols/jabber/jutil.c new file mode 100644 index 0000000000..025f6ba1d4 --- /dev/null +++ b/libpurple/protocols/jabber/jutil.c @@ -0,0 +1,235 @@ +/* + * gaim - Jabber Protocol Plugin + * + * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com> + * + * 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 + * + */ +#include "internal.h" +#include "server.h" +#include "util.h" + +#include "chat.h" +#include "presence.h" +#include "jutil.h" + +gboolean jabber_nodeprep_validate(const char *str) +{ + const char *c; + + if(!str) + return TRUE; + + if(strlen(str) > 1023) + return FALSE; + + c = str; + while(c && *c) { + gunichar ch = g_utf8_get_char(c); + if(ch == '\"' || ch == '&' || ch == '\'' || ch == '/' || ch == ':' || + ch == '<' || ch == '>' || ch == '@' || !g_unichar_isgraph(ch)) { + return FALSE; + } + c = g_utf8_next_char(c); + } + + return TRUE; +} + +gboolean jabber_nameprep_validate(const char *str) +{ + const char *c; + + if(!str) + return TRUE; + + if(strlen(str) > 1023) + return FALSE; + + c = str; + while(c && *c) { + gunichar ch = g_utf8_get_char(c); + if(!g_unichar_isgraph(ch)) + return FALSE; + + c = g_utf8_next_char(c); + } + + + return TRUE; +} + +gboolean jabber_resourceprep_validate(const char *str) +{ + const char *c; + + if(!str) + return TRUE; + + if(strlen(str) > 1023) + return FALSE; + + c = str; + while(c && *c) { + gunichar ch = g_utf8_get_char(c); + if(!g_unichar_isgraph(ch) && ch != ' ') + return FALSE; + + c = g_utf8_next_char(c); + } + + return TRUE; +} + + +JabberID* +jabber_id_new(const char *str) +{ + char *at; + char *slash; + JabberID *jid; + + if(!str || !g_utf8_validate(str, -1, NULL)) + return NULL; + + jid = g_new0(JabberID, 1); + + at = g_utf8_strchr(str, -1, '@'); + slash = g_utf8_strchr(str, -1, '/'); + + if(at) { + jid->node = g_utf8_normalize(str, at-str, G_NORMALIZE_NFKC); + if(slash) { + jid->domain = g_utf8_normalize(at+1, slash-(at+1), G_NORMALIZE_NFKC); + jid->resource = g_utf8_normalize(slash+1, -1, G_NORMALIZE_NFKC); + } else { + jid->domain = g_utf8_normalize(at+1, -1, G_NORMALIZE_NFKC); + } + } else { + if(slash) { + jid->domain = g_utf8_normalize(str, slash-str, G_NORMALIZE_NFKC); + jid->resource = g_utf8_normalize(slash+1, -1, G_NORMALIZE_NFKC); + } else { + jid->domain = g_utf8_normalize(str, -1, G_NORMALIZE_NFKC); + } + } + + + if(!jabber_nodeprep_validate(jid->node) || + !jabber_nameprep_validate(jid->domain) || + !jabber_resourceprep_validate(jid->resource)) { + jabber_id_free(jid); + return NULL; + } + + return jid; +} + +void +jabber_id_free(JabberID *jid) +{ + if(jid) { + if(jid->node) + g_free(jid->node); + if(jid->domain) + g_free(jid->domain); + if(jid->resource) + g_free(jid->resource); + g_free(jid); + } +} + + +char *jabber_get_resource(const char *in) +{ + JabberID *jid = jabber_id_new(in); + char *out; + + if(!jid) + return NULL; + + out = g_strdup(jid->resource); + jabber_id_free(jid); + + return out; +} + +char *jabber_get_bare_jid(const char *in) +{ + JabberID *jid = jabber_id_new(in); + char *out; + + if(!jid) + return NULL; + + out = g_strdup_printf("%s%s%s", jid->node ? jid->node : "", + jid->node ? "@" : "", jid->domain); + jabber_id_free(jid); + + return out; +} + +const char *jabber_normalize(const GaimAccount *account, const char *in) +{ + GaimConnection *gc = account ? account->gc : NULL; + JabberStream *js = gc ? gc->proto_data : NULL; + static char buf[3072]; /* maximum legal length of a jabber jid */ + JabberID *jid; + char *node, *domain; + + jid = jabber_id_new(in); + + if(!jid) + return NULL; + + node = jid->node ? g_utf8_strdown(jid->node, -1) : NULL; + domain = g_utf8_strdown(jid->domain, -1); + + + if(js && node && jid->resource && + jabber_chat_find(js, node, domain)) + g_snprintf(buf, sizeof(buf), "%s@%s/%s", node, domain, + jid->resource); + else + g_snprintf(buf, sizeof(buf), "%s%s%s", node ? node : "", + node ? "@" : "", domain); + + jabber_id_free(jid); + g_free(node); + g_free(domain); + + return buf; +} + +GaimConversation * +jabber_find_unnormalized_conv(const char *name, GaimAccount *account) +{ + GaimConversation *c = NULL; + GList *cnv; + + g_return_val_if_fail(name != NULL, NULL); + + for(cnv = gaim_get_conversations(); cnv; cnv = cnv->next) { + c = (GaimConversation*)cnv->data; + if(gaim_conversation_get_type(c) == GAIM_CONV_TYPE_IM && + !gaim_utf8_strcasecmp(name, gaim_conversation_get_name(c)) && + account == gaim_conversation_get_account(c)) + return c; + } + + return NULL; +} + diff --git a/libpurple/protocols/jabber/jutil.h b/libpurple/protocols/jabber/jutil.h new file mode 100644 index 0000000000..24570ab043 --- /dev/null +++ b/libpurple/protocols/jabber/jutil.h @@ -0,0 +1,50 @@ +/** + * @file jutil.h utility functions + * + * gaim + * + * Copyright (C) 2003 Nathan Walp <faceprint@faceprint.com> + * + * 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 + */ +#ifndef _GAIM_JABBER_JUTIL_H_ +#define _GAIM_JABBER_JUTIL_H_ + +#include "account.h" +#include "conversation.h" +#include "xmlnode.h" + + +typedef struct _JabberID { + char *node; + char *domain; + char *resource; +} JabberID; + +JabberID* jabber_id_new(const char *str); +void jabber_id_free(JabberID *jid); + +char *jabber_get_resource(const char *jid); +char *jabber_get_bare_jid(const char *jid); + +const char *jabber_normalize(const GaimAccount *account, const char *in); + +gboolean jabber_nodeprep_validate(const char *); +gboolean jabber_nameprep_validate(const char *); +gboolean jabber_resourceprep_validate(const char *); + +GaimConversation *jabber_find_unnormalized_conv(const char *name, GaimAccount *account); + +#endif /* _GAIM_JABBER_JUTIL_H_ */ diff --git a/libpurple/protocols/jabber/message.c b/libpurple/protocols/jabber/message.c new file mode 100644 index 0000000000..49ee35a0a9 --- /dev/null +++ b/libpurple/protocols/jabber/message.c @@ -0,0 +1,632 @@ +/* + * gaim - Jabber Protocol Plugin + * + * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com> + * + * 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 + * + */ +#include "internal.h" + +#include "debug.h" +#include "notify.h" +#include "server.h" +#include "util.h" + +#include "buddy.h" +#include "chat.h" +#include "message.h" +#include "xmlnode.h" + +void jabber_message_free(JabberMessage *jm) +{ + g_free(jm->from); + g_free(jm->to); + g_free(jm->id); + g_free(jm->subject); + g_free(jm->body); + g_free(jm->xhtml); + g_free(jm->password); + g_list_free(jm->etc); + + g_free(jm); +} + +static void handle_chat(JabberMessage *jm) +{ + JabberID *jid = jabber_id_new(jm->from); + char *from; + + JabberBuddy *jb; + JabberBuddyResource *jbr; + + if(!jid) + return; + + jb = jabber_buddy_find(jm->js, jm->from, TRUE); + jbr = jabber_buddy_find_resource(jb, jid->resource); + + if(jabber_find_unnormalized_conv(jm->from, jm->js->gc->account)) { + from = g_strdup(jm->from); + } else if(jid->node) { + if(jid->resource) { + GaimConversation *conv; + + from = g_strdup_printf("%s@%s", jid->node, jid->domain); + conv = gaim_find_conversation_with_account(GAIM_CONV_TYPE_IM, from, jm->js->gc->account); + if(conv) { + gaim_conversation_set_name(conv, jm->from); + } + g_free(from); + } + from = g_strdup(jm->from); + } else { + from = g_strdup(jid->domain); + } + + if(!jm->xhtml && !jm->body) { + if(JM_STATE_COMPOSING == jm->chat_state) + serv_got_typing(jm->js->gc, from, 0, GAIM_TYPING); + else if(JM_STATE_PAUSED == jm->chat_state) + serv_got_typing(jm->js->gc, from, 0, GAIM_TYPED); + else + serv_got_typing_stopped(jm->js->gc, from); + } else { + if(jbr) { + if(JM_TS_JEP_0085 == (jm->typing_style & JM_TS_JEP_0085)) { + jbr->chat_states = JABBER_CHAT_STATES_SUPPORTED; + } else { + jbr->chat_states = JABBER_CHAT_STATES_UNSUPPORTED; + } + + if(JM_TS_JEP_0022 == (jm->typing_style & JM_TS_JEP_0022)) { + jbr->capabilities |= JABBER_CAP_COMPOSING; + } + + if(jbr->thread_id) + g_free(jbr->thread_id); + jbr->thread_id = g_strdup(jbr->thread_id); + } + serv_got_im(jm->js->gc, from, jm->xhtml ? jm->xhtml : jm->body, 0, + jm->sent); + } + + + g_free(from); + jabber_id_free(jid); +} + +static void handle_headline(JabberMessage *jm) +{ + char *title; + GString *body = g_string_new(""); + GList *etc; + + title = g_strdup_printf(_("Message from %s"), jm->from); + + if(jm->xhtml) + g_string_append(body, jm->xhtml); + else if(jm->body) + g_string_append(body, jm->body); + + for(etc = jm->etc; etc; etc = etc->next) { + xmlnode *x = etc->data; + const char *xmlns = xmlnode_get_namespace(x); + if(xmlns && !strcmp(xmlns, "jabber:x:oob")) { + xmlnode *url, *desc; + char *urltxt, *desctxt; + + url = xmlnode_get_child(x, "url"); + desc = xmlnode_get_child(x, "desc"); + + if(!url || !desc) + continue; + + urltxt = xmlnode_get_data(url); + desctxt = xmlnode_get_data(desc); + + /* I'm all about ugly hacks */ + if(body->len && jm->body && !strcmp(body->str, jm->body)) + g_string_printf(body, "<a href='%s'>%s</a>", + urltxt, desctxt); + else + g_string_append_printf(body, "<br/><a href='%s'>%s</a>", + urltxt, desctxt); + + g_free(urltxt); + g_free(desctxt); + } + } + + gaim_notify_formatted(jm->js->gc, title, jm->subject ? jm->subject : title, + NULL, body->str, NULL, NULL); + + g_free(title); + g_string_free(body, TRUE); +} + +static void handle_groupchat(JabberMessage *jm) +{ + JabberID *jid = jabber_id_new(jm->from); + JabberChat *chat; + + if(!jid) + return; + + chat = jabber_chat_find(jm->js, jid->node, jid->domain); + + if(!chat) + return; + + if(jm->subject) { + gaim_conv_chat_set_topic(GAIM_CONV_CHAT(chat->conv), jid->resource, + jm->subject); + if(!jm->xhtml && !jm->body) { + char *msg, *tmp, *tmp2; + tmp = g_markup_escape_text(jm->subject, -1); + tmp2 = gaim_markup_linkify(tmp); + if(jid->resource) + msg = g_strdup_printf(_("%s has set the topic to: %s"), jid->resource, tmp2); + else + msg = g_strdup_printf(_("The topic is: %s"), tmp2); + gaim_conv_chat_write(GAIM_CONV_CHAT(chat->conv), "", msg, GAIM_MESSAGE_SYSTEM, jm->sent); + g_free(tmp); + g_free(tmp2); + g_free(msg); + } + } + + if(jm->xhtml || jm->body) { + if(jid->resource) + serv_got_chat_in(jm->js->gc, chat->id, jid->resource, + jm->delayed ? GAIM_MESSAGE_DELAYED : 0, + jm->xhtml ? jm->xhtml : jm->body, jm->sent); + else if(chat->muc) + gaim_conv_chat_write(GAIM_CONV_CHAT(chat->conv), "", + jm->xhtml ? jm->xhtml : jm->body, + GAIM_MESSAGE_SYSTEM, jm->sent); + } + + jabber_id_free(jid); +} + +static void handle_groupchat_invite(JabberMessage *jm) +{ + GHashTable *components; + JabberID *jid = jabber_id_new(jm->to); + + if(!jid) + return; + + components = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + + g_hash_table_replace(components, g_strdup("room"), g_strdup(jid->node)); + g_hash_table_replace(components, g_strdup("server"), g_strdup(jid->domain)); + g_hash_table_replace(components, g_strdup("handle"), + g_strdup(jm->js->user->node)); + g_hash_table_replace(components, g_strdup("password"), + g_strdup(jm->password)); + + jabber_id_free(jid); + serv_got_chat_invite(jm->js->gc, jm->to, jm->from, jm->body, components); +} + +static void handle_error(JabberMessage *jm) +{ + char *buf; + + if(!jm->body) + return; + + buf = g_strdup_printf(_("Message delivery to %s failed: %s"), + jm->from, jm->error ? jm->error : ""); + + gaim_notify_formatted(jm->js->gc, _("Jabber Message Error"), _("Jabber Message Error"), buf, + jm->xhtml ? jm->xhtml : jm->body, NULL, NULL); + + g_free(buf); +} + +void jabber_message_parse(JabberStream *js, xmlnode *packet) +{ + JabberMessage *jm; + const char *type; + xmlnode *child; + + jm = g_new0(JabberMessage, 1); + jm->js = js; + jm->sent = time(NULL); + jm->delayed = FALSE; + + type = xmlnode_get_attrib(packet, "type"); + + if(type) { + if(!strcmp(type, "normal")) + jm->type = JABBER_MESSAGE_NORMAL; + else if(!strcmp(type, "chat")) + jm->type = JABBER_MESSAGE_CHAT; + else if(!strcmp(type, "groupchat")) + jm->type = JABBER_MESSAGE_GROUPCHAT; + else if(!strcmp(type, "headline")) + jm->type = JABBER_MESSAGE_HEADLINE; + else if(!strcmp(type, "error")) + jm->type = JABBER_MESSAGE_ERROR; + else + jm->type = JABBER_MESSAGE_OTHER; + } else { + jm->type = JABBER_MESSAGE_NORMAL; + } + + jm->from = g_strdup(xmlnode_get_attrib(packet, "from")); + jm->to = g_strdup(xmlnode_get_attrib(packet, "to")); + jm->id = g_strdup(xmlnode_get_attrib(packet, "id")); + + for(child = packet->child; child; child = child->next) { + if(child->type != XMLNODE_TYPE_TAG) + continue; + + if(!strcmp(child->name, "subject")) { + if(!jm->subject) + jm->subject = xmlnode_get_data(child); + } else if(!strcmp(child->name, "thread")) { + if(!jm->thread_id) + jm->thread_id = xmlnode_get_data(child); + } else if(!strcmp(child->name, "body")) { + if(!jm->body) { + char *msg = xmlnode_to_str(child, NULL); + jm->body = gaim_strdup_withhtml(msg); + g_free(msg); + } + } else if(!strcmp(child->name, "html")) { + if(!jm->xhtml && xmlnode_get_child(child, "body")) + jm->xhtml = xmlnode_to_str(child, NULL); + } else if(!strcmp(child->name, "active")) { + jm->chat_state = JM_STATE_ACTIVE; + jm->typing_style |= JM_TS_JEP_0085; + } else if(!strcmp(child->name, "composing")) { + jm->chat_state = JM_STATE_COMPOSING; + jm->typing_style |= JM_TS_JEP_0085; + } else if(!strcmp(child->name, "paused")) { + jm->chat_state = JM_STATE_PAUSED; + jm->typing_style |= JM_TS_JEP_0085; + } else if(!strcmp(child->name, "inactive")) { + jm->chat_state = JM_STATE_INACTIVE; + jm->typing_style |= JM_TS_JEP_0085; + } else if(!strcmp(child->name, "gone")) { + jm->chat_state = JM_STATE_GONE; + jm->typing_style |= JM_TS_JEP_0085; + } else if(!strcmp(child->name, "error")) { + const char *code = xmlnode_get_attrib(child, "code"); + char *code_txt = NULL; + char *text = xmlnode_get_data(child); + + if(code) + code_txt = g_strdup_printf(_(" (Code %s)"), code); + + if(!jm->error) + jm->error = g_strdup_printf("%s%s", text ? text : "", + code_txt ? code_txt : ""); + + g_free(code_txt); + g_free(text); + } else if(!strcmp(child->name, "x")) { + const char *xmlns = xmlnode_get_namespace(child); + if(xmlns && !strcmp(xmlns, "jabber:x:event")) { + if(xmlnode_get_child(child, "composing")) { + if(jm->chat_state == JM_STATE_ACTIVE) + jm->chat_state = JM_STATE_COMPOSING; + jm->typing_style |= JM_TS_JEP_0022; + } + } else if(xmlns && !strcmp(xmlns, "jabber:x:delay")) { + const char *timestamp = xmlnode_get_attrib(child, "stamp"); + jm->delayed = TRUE; + if(timestamp) + jm->sent = gaim_str_to_time(timestamp, TRUE, NULL, NULL, NULL); + } else if(xmlns && !strcmp(xmlns, "jabber:x:conference") && + jm->type != JABBER_MESSAGE_GROUPCHAT_INVITE && + jm->type != JABBER_MESSAGE_ERROR) { + const char *jid = xmlnode_get_attrib(child, "jid"); + if(jid) { + jm->type = JABBER_MESSAGE_GROUPCHAT_INVITE; + g_free(jm->to); + jm->to = g_strdup(jid); + } + } else if(xmlns && !strcmp(xmlns, + "http://jabber.org/protocol/muc#user") && + jm->type != JABBER_MESSAGE_ERROR) { + xmlnode *invite = xmlnode_get_child(child, "invite"); + if(invite) { + xmlnode *reason, *password; + const char *jid = xmlnode_get_attrib(invite, "from"); + g_free(jm->to); + jm->to = jm->from; + jm->from = g_strdup(jid); + if((reason = xmlnode_get_child(invite, "reason"))) { + g_free(jm->body); + jm->body = xmlnode_get_data(reason); + } + if((password = xmlnode_get_child(child, "password"))) + jm->password = xmlnode_get_data(password); + + jm->type = JABBER_MESSAGE_GROUPCHAT_INVITE; + } + } else { + jm->etc = g_list_append(jm->etc, child); + } + } + } + + switch(jm->type) { + case JABBER_MESSAGE_NORMAL: + case JABBER_MESSAGE_CHAT: + handle_chat(jm); + break; + case JABBER_MESSAGE_HEADLINE: + handle_headline(jm); + break; + case JABBER_MESSAGE_GROUPCHAT: + handle_groupchat(jm); + break; + case JABBER_MESSAGE_GROUPCHAT_INVITE: + handle_groupchat_invite(jm); + break; + case JABBER_MESSAGE_ERROR: + handle_error(jm); + break; + case JABBER_MESSAGE_OTHER: + gaim_debug(GAIM_DEBUG_INFO, "jabber", + "Received message of unknown type: %s\n", type); + break; + } + jabber_message_free(jm); +} + +void jabber_message_send(JabberMessage *jm) +{ + xmlnode *message, *child; + const char *type = NULL; + + message = xmlnode_new("message"); + + switch(jm->type) { + case JABBER_MESSAGE_NORMAL: + type = "normal"; + break; + case JABBER_MESSAGE_CHAT: + case JABBER_MESSAGE_GROUPCHAT_INVITE: + type = "chat"; + break; + case JABBER_MESSAGE_HEADLINE: + type = "headline"; + break; + case JABBER_MESSAGE_GROUPCHAT: + type = "groupchat"; + break; + case JABBER_MESSAGE_ERROR: + type = "error"; + break; + case JABBER_MESSAGE_OTHER: + type = NULL; + break; + } + + if(type) + xmlnode_set_attrib(message, "type", type); + + if (jm->id) + xmlnode_set_attrib(message, "id", jm->id); + + xmlnode_set_attrib(message, "to", jm->to); + + if(jm->thread_id) { + child = xmlnode_new_child(message, "thread"); + xmlnode_insert_data(child, jm->thread_id, -1); + } + + if(JM_TS_JEP_0022 == (jm->typing_style & JM_TS_JEP_0022)) { + child = xmlnode_new_child(message, "x"); + xmlnode_set_namespace(child, "jabber:x:event"); + if(jm->chat_state == JM_STATE_COMPOSING || jm->body) + xmlnode_new_child(child, "composing"); + } + + if(JM_TS_JEP_0085 == (jm->typing_style & JM_TS_JEP_0085)) { + child = NULL; + switch(jm->chat_state) + { + case JM_STATE_ACTIVE: + child = xmlnode_new_child(message, "active"); + break; + case JM_STATE_COMPOSING: + child = xmlnode_new_child(message, "composing"); + break; + case JM_STATE_PAUSED: + child = xmlnode_new_child(message, "paused"); + break; + case JM_STATE_INACTIVE: + child = xmlnode_new_child(message, "inactive"); + break; + case JM_STATE_GONE: + child = xmlnode_new_child(message, "gone"); + break; + } + if(child) + xmlnode_set_namespace(child, "http://jabber.org/protocol/chatstates"); + } + + if(jm->subject) { + child = xmlnode_new_child(message, "subject"); + xmlnode_insert_data(child, jm->subject, -1); + } + + if(jm->body) { + child = xmlnode_new_child(message, "body"); + xmlnode_insert_data(child, jm->body, -1); + } + + if(jm->xhtml) { + child = xmlnode_from_str(jm->xhtml, -1); + if(child) { + xmlnode_insert_child(message, child); + } else { + gaim_debug(GAIM_DEBUG_ERROR, "jabber", + "XHTML translation/validation failed, returning: %s\n", + jm->xhtml); + } + } + + jabber_send(jm->js, message); + + xmlnode_free(message); +} + +int jabber_message_send_im(GaimConnection *gc, const char *who, const char *msg, + GaimMessageFlags flags) +{ + JabberMessage *jm; + JabberBuddy *jb; + JabberBuddyResource *jbr; + char *buf; + char *xhtml; + char *resource; + + if(!who || !msg) + return 0; + + resource = jabber_get_resource(who); + + jb = jabber_buddy_find(gc->proto_data, who, TRUE); + jbr = jabber_buddy_find_resource(jb, resource); + + g_free(resource); + + jm = g_new0(JabberMessage, 1); + jm->js = gc->proto_data; + jm->type = JABBER_MESSAGE_CHAT; + jm->chat_state = JM_STATE_ACTIVE; + jm->to = g_strdup(who); + jm->id = jabber_get_next_id(jm->js); + jm->chat_state = JM_STATE_ACTIVE; + + if(jbr) { + if(jbr->thread_id) + jm->thread_id = jbr->thread_id; + + if(jbr->chat_states != JABBER_CHAT_STATES_UNSUPPORTED) { + jm->typing_style |= JM_TS_JEP_0085; + /* if(JABBER_CHAT_STATES_UNKNOWN == jbr->chat_states) + jbr->chat_states = JABBER_CHAT_STATES_UNSUPPORTED; */ + } + + if(jbr->chat_states != JABBER_CHAT_STATES_SUPPORTED) + jm->typing_style |= JM_TS_JEP_0022; + } + + buf = g_strdup_printf("<html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'>%s</body></html>", msg); + + gaim_markup_html_to_xhtml(buf, &xhtml, &jm->body); + g_free(buf); + + if(!jbr || jbr->capabilities & JABBER_CAP_XHTML) + jm->xhtml = xhtml; + else + g_free(xhtml); + + jabber_message_send(jm); + jabber_message_free(jm); + return 1; +} + +int jabber_message_send_chat(GaimConnection *gc, int id, const char *msg, GaimMessageFlags flags) +{ + JabberChat *chat; + JabberMessage *jm; + JabberStream *js; + char *buf; + + if(!msg || !gc) + return 0; + + js = gc->proto_data; + chat = jabber_chat_find_by_id(js, id); + + if(!chat) + return 0; + + jm = g_new0(JabberMessage, 1); + jm->js = gc->proto_data; + jm->type = JABBER_MESSAGE_GROUPCHAT; + jm->to = g_strdup_printf("%s@%s", chat->room, chat->server); + jm->id = jabber_get_next_id(jm->js); + + buf = g_strdup_printf("<html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'>%s</body></html>", msg); + gaim_markup_html_to_xhtml(buf, &jm->xhtml, &jm->body); + g_free(buf); + + if(!chat->xhtml) { + g_free(jm->xhtml); + jm->xhtml = NULL; + } + + jabber_message_send(jm); + jabber_message_free(jm); + + return 1; +} + +unsigned int jabber_send_typing(GaimConnection *gc, const char *who, GaimTypingState state) +{ + JabberMessage *jm; + JabberBuddy *jb; + JabberBuddyResource *jbr; + char *resource = jabber_get_resource(who); + + jb = jabber_buddy_find(gc->proto_data, who, TRUE); + jbr = jabber_buddy_find_resource(jb, resource); + + g_free(resource); + + if(!jbr || !((jbr->capabilities & JABBER_CAP_COMPOSING) || (jbr->chat_states != JABBER_CHAT_STATES_UNSUPPORTED))) + return 0; + + /* TODO: figure out threading */ + jm = g_new0(JabberMessage, 1); + jm->js = gc->proto_data; + jm->type = JABBER_MESSAGE_CHAT; + jm->to = g_strdup(who); + jm->id = jabber_get_next_id(jm->js); + + if(GAIM_TYPING == state) + jm->chat_state = JM_STATE_COMPOSING; + else if(GAIM_TYPED == state) + jm->chat_state = JM_STATE_PAUSED; + else + jm->chat_state = JM_STATE_ACTIVE; + + if(jbr->chat_states != JABBER_CHAT_STATES_UNSUPPORTED) { + jm->typing_style |= JM_TS_JEP_0085; + /* if(JABBER_CHAT_STATES_UNKNOWN == jbr->chat_states) + jbr->chat_states = JABBER_CHAT_STATES_UNSUPPORTED; */ + } + + if(jbr->chat_states != JABBER_CHAT_STATES_SUPPORTED) + jm->typing_style |= JM_TS_JEP_0022; + + jabber_message_send(jm); + jabber_message_free(jm); + + return 0; +} + diff --git a/libpurple/protocols/jabber/message.h b/libpurple/protocols/jabber/message.h new file mode 100644 index 0000000000..2755d55d3f --- /dev/null +++ b/libpurple/protocols/jabber/message.h @@ -0,0 +1,77 @@ +/** + * @file message.h Message handlers + * + * gaim + * + * Copyright (C) 2003 Nathan Walp <faceprint@faceprint.com> + * + * 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 + */ +#ifndef _GAIM_JABBER_MESSAGE_H_ +#define _GAIM_JABBER_MESSAGE_H_ + +#include "jabber.h" +#include "xmlnode.h" + +typedef struct _JabberMessage { + JabberStream *js; + enum { + JABBER_MESSAGE_NORMAL, + JABBER_MESSAGE_CHAT, + JABBER_MESSAGE_GROUPCHAT, + JABBER_MESSAGE_HEADLINE, + JABBER_MESSAGE_ERROR, + JABBER_MESSAGE_GROUPCHAT_INVITE, + JABBER_MESSAGE_OTHER + } type; + time_t sent; + gboolean delayed; + char *id; + char *from; + char *to; + char *subject; + char *body; + char *xhtml; + char *password; + char *error; + char *thread_id; + enum { + JM_TS_NONE = 0, + JM_TS_JEP_0022 = 0x1, + JM_TS_JEP_0085 = 0x2 + } typing_style; + enum { + JM_STATE_ACTIVE, + JM_STATE_COMPOSING, + JM_STATE_PAUSED, + JM_STATE_INACTIVE, + JM_STATE_GONE + } chat_state; + GList *etc; +} JabberMessage; + +void jabber_message_free(JabberMessage *jm); + +void jabber_message_send(JabberMessage *jm); + +void jabber_message_parse(JabberStream *js, xmlnode *packet); +int jabber_message_send_im(GaimConnection *gc, const char *who, const char *msg, + GaimMessageFlags flags); +int jabber_message_send_chat(GaimConnection *gc, int id, const char *message, GaimMessageFlags flags); + +unsigned int jabber_send_typing(GaimConnection *gc, const char *who, GaimTypingState state); + + +#endif /* _GAIM_JABBER_MESSAGE_H_ */ diff --git a/libpurple/protocols/jabber/oob.c b/libpurple/protocols/jabber/oob.c new file mode 100644 index 0000000000..2295de9d02 --- /dev/null +++ b/libpurple/protocols/jabber/oob.c @@ -0,0 +1,242 @@ +/* + * gaim - Jabber Protocol Plugin + * + * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com> + * + * 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 + * + */ +#include "internal.h" +#include "debug.h" +#include "ft.h" +#include "util.h" + +#include "jabber.h" +#include "iq.h" +#include "oob.h" + +typedef struct _JabberOOBXfer { + char *address; + int port; + char *page; + + GString *headers; + + char *iq_id; + + JabberStream *js; + + gchar *write_buffer; + gsize written_len; + guint writeh; + +} JabberOOBXfer; + +static void jabber_oob_xfer_init(GaimXfer *xfer) +{ + JabberOOBXfer *jox = xfer->data; + gaim_xfer_start(xfer, -1, jox->address, jox->port); +} + +static void jabber_oob_xfer_free(GaimXfer *xfer) +{ + JabberOOBXfer *jox = xfer->data; + jox->js->oob_file_transfers = g_list_remove(jox->js->oob_file_transfers, + xfer); + + g_string_free(jox->headers, TRUE); + g_free(jox->address); + g_free(jox->page); + g_free(jox->iq_id); + g_free(jox->write_buffer); + if(jox->writeh) + gaim_input_remove(jox->writeh); + g_free(jox); + + xfer->data = NULL; +} + +static void jabber_oob_xfer_end(GaimXfer *xfer) +{ + JabberOOBXfer *jox = xfer->data; + JabberIq *iq; + + iq = jabber_iq_new(jox->js, JABBER_IQ_RESULT); + xmlnode_set_attrib(iq->node, "to", xfer->who); + jabber_iq_set_id(iq, jox->iq_id); + + jabber_iq_send(iq); + + jabber_oob_xfer_free(xfer); +} + +static void jabber_oob_xfer_request_send(gpointer data, gint source, GaimInputCondition cond) { + GaimXfer *xfer = data; + JabberOOBXfer *jox = xfer->data; + int len, total_len = strlen(jox->write_buffer); + + len = write(xfer->fd, jox->write_buffer + jox->written_len, + total_len - jox->written_len); + + if(len < 0 && errno == EAGAIN) + return; + else if(len < 0) { + gaim_debug(GAIM_DEBUG_ERROR, "jabber", "Write error on oob xfer!\n"); + gaim_input_remove(jox->writeh); + gaim_xfer_cancel_local(xfer); + } + jox->written_len += len; + + if(jox->written_len == total_len) { + gaim_input_remove(jox->writeh); + g_free(jox->write_buffer); + jox->write_buffer = NULL; + } +} + +static void jabber_oob_xfer_start(GaimXfer *xfer) +{ + JabberOOBXfer *jox = xfer->data; + + if(jox->write_buffer == NULL) { + jox->write_buffer = g_strdup_printf( + "GET /%s HTTP/1.1\r\nHost: %s\r\n\r\n", + jox->page, jox->address); + jox->written_len = 0; + } + + jox->writeh = gaim_input_add(xfer->fd, GAIM_INPUT_WRITE, + jabber_oob_xfer_request_send, xfer); + + jabber_oob_xfer_request_send(xfer, xfer->fd, GAIM_INPUT_WRITE); +} + +static gssize jabber_oob_xfer_read(guchar **buffer, GaimXfer *xfer) { + JabberOOBXfer *jox = xfer->data; + char test[2048]; + char *tmp, *lenstr; + int len; + + if((len = read(xfer->fd, test, sizeof(test))) > 0) { + jox->headers = g_string_append_len(jox->headers, test, len); + if((tmp = strstr(jox->headers->str, "\r\n\r\n"))) { + *tmp = '\0'; + lenstr = strstr(jox->headers->str, "Content-Length: "); + if(lenstr) { + int size; + sscanf(lenstr, "Content-Length: %d", &size); + gaim_xfer_set_size(xfer, size); + } + gaim_xfer_set_read_fnc(xfer, NULL); + + tmp += 4; + + *buffer = (unsigned char*) g_strdup(tmp); + return strlen(tmp); + } + return 0; + } else if (errno != EAGAIN) { + gaim_debug(GAIM_DEBUG_ERROR, "jabber", "Read error on oob xfer!\n"); + gaim_xfer_cancel_local(xfer); + } + + return 0; +} + +static void jabber_oob_xfer_recv_error(GaimXfer *xfer, const char *code) { + JabberOOBXfer *jox = xfer->data; + JabberIq *iq; + xmlnode *y, *z; + + iq = jabber_iq_new(jox->js, JABBER_IQ_ERROR); + xmlnode_set_attrib(iq->node, "to", xfer->who); + jabber_iq_set_id(iq, jox->iq_id); + y = xmlnode_new_child(iq->node, "error"); + xmlnode_set_attrib(y, "code", code); + if(!strcmp(code, "406")) { + z = xmlnode_new_child(y, "not-acceptable"); + xmlnode_set_attrib(y, "type", "modify"); + xmlnode_set_namespace(z, "urn:ietf:params:xml:ns:xmpp-stanzas"); + } else if(!strcmp(code, "404")) { + z = xmlnode_new_child(y, "not-found"); + xmlnode_set_attrib(y, "type", "cancel"); + xmlnode_set_namespace(z, "urn:ietf:params:xml:ns:xmpp-stanzas"); + } + jabber_iq_send(iq); + + jabber_oob_xfer_free(xfer); +} + +static void jabber_oob_xfer_recv_denied(GaimXfer *xfer) { + jabber_oob_xfer_recv_error(xfer, "406"); +} + +static void jabber_oob_xfer_recv_canceled(GaimXfer *xfer) { + jabber_oob_xfer_recv_error(xfer, "404"); +} + +void jabber_oob_parse(JabberStream *js, xmlnode *packet) { + JabberOOBXfer *jox; + GaimXfer *xfer; + char *filename; + char *url; + const char *type; + xmlnode *querynode, *urlnode; + + if(!(type = xmlnode_get_attrib(packet, "type")) || strcmp(type, "set")) + return; + + if(!(querynode = xmlnode_get_child(packet, "query"))) + return; + + if(!(urlnode = xmlnode_get_child(querynode, "url"))) + return; + + url = xmlnode_get_data(urlnode); + + jox = g_new0(JabberOOBXfer, 1); + gaim_url_parse(url, &jox->address, &jox->port, &jox->page, NULL, NULL); + g_free(url); + jox->js = js; + jox->headers = g_string_new(""); + jox->iq_id = g_strdup(xmlnode_get_attrib(packet, "id")); + + xfer = gaim_xfer_new(js->gc->account, GAIM_XFER_RECEIVE, + xmlnode_get_attrib(packet, "from")); + if (xfer) + { + xfer->data = jox; + + if(!(filename = g_strdup(g_strrstr(jox->page, "/")))) + filename = g_strdup(jox->page); + + gaim_xfer_set_filename(xfer, filename); + + g_free(filename); + + gaim_xfer_set_init_fnc(xfer, jabber_oob_xfer_init); + gaim_xfer_set_end_fnc(xfer, jabber_oob_xfer_end); + gaim_xfer_set_request_denied_fnc(xfer, jabber_oob_xfer_recv_denied); + gaim_xfer_set_cancel_recv_fnc(xfer, jabber_oob_xfer_recv_canceled); + gaim_xfer_set_read_fnc(xfer, jabber_oob_xfer_read); + gaim_xfer_set_start_fnc(xfer, jabber_oob_xfer_start); + + js->oob_file_transfers = g_list_append(js->oob_file_transfers, xfer); + + gaim_xfer_request(xfer); + } +} + + diff --git a/libpurple/protocols/jabber/oob.h b/libpurple/protocols/jabber/oob.h new file mode 100644 index 0000000000..bd5d3e0761 --- /dev/null +++ b/libpurple/protocols/jabber/oob.h @@ -0,0 +1,27 @@ +/** + * @file jutil.h utility functions + * + * gaim + * + * Copyright (C) 2003 Nathan Walp <faceprint@faceprint.com> + * + * 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 + */ +#ifndef _GAIM_JABBER_OOB_H_ +#define _GAIM_JABBER_OOB_H_ + +void jabber_oob_parse(JabberStream *js, xmlnode *packet); + +#endif /* _GAIM_JABBER_OOB_H_ */ diff --git a/libpurple/protocols/jabber/parser.c b/libpurple/protocols/jabber/parser.c new file mode 100644 index 0000000000..7ba794a4a5 --- /dev/null +++ b/libpurple/protocols/jabber/parser.c @@ -0,0 +1,195 @@ +/* + * gaim - Jabber XML parser stuff + * + * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com> + * + * 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 + * + */ +#include "internal.h" + +#include <libxml/parser.h> + +#include "connection.h" +#include "debug.h" +#include "jabber.h" +#include "parser.h" +#include "util.h" +#include "xmlnode.h" + +static void +jabber_parser_element_start_libxml(void *user_data, + const xmlChar *element_name, const xmlChar *prefix, const xmlChar *namespace, + int nb_namespaces, const xmlChar **namespaces, + int nb_attributes, int nb_defaulted, const xmlChar **attributes) +{ + JabberStream *js = user_data; + xmlnode *node; + int i; + + if(!element_name) { + return; + } else if(!xmlStrcmp(element_name, (xmlChar*) "stream")) { + js->protocol_version = JABBER_PROTO_0_9; + for(i=0; i < nb_attributes * 5; i += 5) { + int attrib_len = attributes[i+4] - attributes[i+3]; + char *attrib = g_malloc(attrib_len + 1); + memcpy(attrib, attributes[i+3], attrib_len); + attrib[attrib_len] = '\0'; + + if(!xmlStrcmp(attributes[i], (xmlChar*) "version") + && !strcmp(attrib, "1.0")) { + js->protocol_version = JABBER_PROTO_1_0; + g_free(attrib); + } else if(!xmlStrcmp(attributes[i], (xmlChar*) "id")) { + if(js->stream_id) + g_free(js->stream_id); + js->stream_id = attrib; + } + } + if(js->protocol_version == JABBER_PROTO_0_9) + js->auth_type = JABBER_AUTH_IQ_AUTH; + + if(js->state == JABBER_STREAM_INITIALIZING) + jabber_stream_set_state(js, JABBER_STREAM_AUTHENTICATING); + } else { + + if(js->current) + node = xmlnode_new_child(js->current, (const char*) element_name); + else + node = xmlnode_new((const char*) element_name); + xmlnode_set_namespace(node, (const char*) namespace); + + for(i=0; i < nb_attributes * 5; i+=5) { + char *txt; + int attrib_len = attributes[i+4] - attributes[i+3]; + char *attrib = g_malloc(attrib_len + 1); + char *attrib_ns = NULL; + + if (attributes[i+2]) { + attrib_ns = g_strdup(attributes[i+2]);; + } + + memcpy(attrib, attributes[i+3], attrib_len); + attrib[attrib_len] = '\0'; + + txt = attrib; + attrib = gaim_unescape_html(txt); + g_free(txt); + xmlnode_set_attrib_with_namespace(node, (const char*) attributes[i], attrib_ns, attrib); + g_free(attrib); + g_free(attrib_ns); + } + + js->current = node; + } +} + +static void +jabber_parser_element_end_libxml(void *user_data, const xmlChar *element_name, + const xmlChar *prefix, const xmlChar *namespace) +{ + JabberStream *js = user_data; + + if(!js->current) + return; + + if(js->current->parent) { + if(!xmlStrcmp((xmlChar*) js->current->name, element_name)) + js->current = js->current->parent; + } else { + xmlnode *packet = js->current; + js->current = NULL; + jabber_process_packet(js, packet); + xmlnode_free(packet); + } +} + +static void +jabber_parser_element_text_libxml(void *user_data, const xmlChar *text, int text_len) +{ + JabberStream *js = user_data; + + if(!js->current) + return; + + if(!text || !text_len) + return; + + xmlnode_insert_data(js->current, (const char*) text, text_len); +} + +static xmlSAXHandler jabber_parser_libxml = { + .internalSubset = NULL, + .isStandalone = NULL, + .hasInternalSubset = NULL, + .hasExternalSubset = NULL, + .resolveEntity = NULL, + .getEntity = NULL, + .entityDecl = NULL, + .notationDecl = NULL, + .attributeDecl = NULL, + .elementDecl = NULL, + .unparsedEntityDecl = NULL, + .setDocumentLocator = NULL, + .startDocument = NULL, + .endDocument = NULL, + .startElement = NULL, + .endElement = NULL, + .reference = NULL, + .characters = jabber_parser_element_text_libxml, + .ignorableWhitespace = NULL, + .processingInstruction = NULL, + .comment = NULL, + .warning = NULL, + .error = NULL, + .fatalError = NULL, + .getParameterEntity = NULL, + .cdataBlock = NULL, + .externalSubset = NULL, + .initialized = XML_SAX2_MAGIC, + ._private = NULL, + .startElementNs = jabber_parser_element_start_libxml, + .endElementNs = jabber_parser_element_end_libxml, + .serror = NULL +}; + +void +jabber_parser_setup(JabberStream *js) +{ + /* This seems backwards, but it makes sense. The libxml code creates + * the parser context when you try to use it (this way, it can figure + * out the encoding at creation time. So, setting up the parser is + * just a matter of destroying any current parser. */ + if (js->context) { + xmlParseChunk(js->context, NULL,0,1); + xmlFreeParserCtxt(js->context); + js->context = NULL; + } +} + + +void jabber_parser_process(JabberStream *js, const char *buf, int len) +{ + if (js->context == NULL) { + /* libxml inconsistently starts parsing on creating the + * parser, so do a ParseChunk right afterwards to force it. */ + js->context = xmlCreatePushParserCtxt(&jabber_parser_libxml, js, buf, len, NULL); + xmlParseChunk(js->context, "", 0, 0); + } else if (xmlParseChunk(js->context, buf, len, 0) < 0) { + gaim_connection_error(js->gc, _("XML Parse error")); + } +} + diff --git a/libpurple/protocols/jabber/parser.h b/libpurple/protocols/jabber/parser.h new file mode 100644 index 0000000000..29aa1eed0c --- /dev/null +++ b/libpurple/protocols/jabber/parser.h @@ -0,0 +1,30 @@ +/** + * @file parser.h XML parser functions + * + * gaim + * + * Copyright (C) 2003 Nathan Walp <faceprint@faceprint.com> + * + * 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 + */ +#ifndef _GAIM_JABBER_PARSER_H_ +#define _GAIM_JABBER_PARSER_H_ + +#include "jabber.h" + +void jabber_parser_setup(JabberStream *js); +void jabber_parser_process(JabberStream *js, const char *buf, int len); + +#endif /* _GAIM_JABBER_PARSER_H_ */ diff --git a/libpurple/protocols/jabber/presence.c b/libpurple/protocols/jabber/presence.c new file mode 100644 index 0000000000..9ac70dfc57 --- /dev/null +++ b/libpurple/protocols/jabber/presence.c @@ -0,0 +1,617 @@ +/* + * gaim - Jabber Protocol Plugin + * + * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com> + * + * 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 + * + */ +#include "internal.h" + +#include "cipher.h" +#include "debug.h" +#include "notify.h" +#include "request.h" +#include "server.h" +#include "status.h" +#include "util.h" + +#include "buddy.h" +#include "chat.h" +#include "presence.h" +#include "iq.h" +#include "jutil.h" +#include "xmlnode.h" + + +static void chats_send_presence_foreach(gpointer key, gpointer val, + gpointer user_data) +{ + JabberChat *chat = val; + xmlnode *presence = user_data; + char *chat_full_jid; + + if(!chat->conv) + return; + + chat_full_jid = g_strdup_printf("%s@%s/%s", chat->room, chat->server, + chat->handle); + + xmlnode_set_attrib(presence, "to", chat_full_jid); + jabber_send(chat->js, presence); + g_free(chat_full_jid); +} + +void jabber_presence_fake_to_self(JabberStream *js, const GaimStatus *gstatus) { + char *my_base_jid; + + if(!js->user) + return; + + my_base_jid = g_strdup_printf("%s@%s", js->user->node, js->user->domain); + if(gaim_find_buddy(js->gc->account, my_base_jid)) { + JabberBuddy *jb; + JabberBuddyResource *jbr; + if((jb = jabber_buddy_find(js, my_base_jid, TRUE))) { + JabberBuddyState state; + char *msg; + int priority; + + gaim_status_to_jabber(gstatus, &state, &msg, &priority); + + if (state == JABBER_BUDDY_STATE_UNAVAILABLE || state == JABBER_BUDDY_STATE_UNKNOWN) { + jabber_buddy_remove_resource(jb, js->user->resource); + } else { + jabber_buddy_track_resource(jb, js->user->resource, priority, state, msg); + } + if((jbr = jabber_buddy_find_resource(jb, NULL))) { + gaim_prpl_got_user_status(js->gc->account, my_base_jid, jabber_buddy_state_get_status_id(jbr->state), "priority", jbr->priority, jbr->status ? "message" : NULL, jbr->status, NULL); + } else { + gaim_prpl_got_user_status(js->gc->account, my_base_jid, "offline", msg ? "message" : NULL, msg, NULL); + } + + g_free(msg); + } + } + g_free(my_base_jid); +} + + +void jabber_presence_send(GaimAccount *account, GaimStatus *status) +{ + GaimConnection *gc = NULL; + JabberStream *js = NULL; + gboolean disconnected; + int primitive; + xmlnode *presence, *x, *photo; + char *stripped = NULL; + JabberBuddyState state; + int priority; + + if(!gaim_status_is_active(status)) + return; + + disconnected = gaim_account_is_disconnected(account); + primitive = gaim_status_type_get_primitive(gaim_status_get_type(status)); + + if(disconnected) + return; + + gc = gaim_account_get_connection(account); + js = gc->proto_data; + + gaim_status_to_jabber(status, &state, &stripped, &priority); + + + presence = jabber_presence_create(state, stripped, priority); + g_free(stripped); + + if(js->avatar_hash) { + x = xmlnode_new_child(presence, "x"); + xmlnode_set_namespace(x, "vcard-temp:x:update"); + photo = xmlnode_new_child(x, "photo"); + xmlnode_insert_data(photo, js->avatar_hash, -1); + } + + jabber_send(js, presence); + + g_hash_table_foreach(js->chats, chats_send_presence_foreach, presence); + xmlnode_free(presence); + + jabber_presence_fake_to_self(js, status); +} + +xmlnode *jabber_presence_create(JabberBuddyState state, const char *msg, int priority) +{ + xmlnode *show, *status, *presence, *pri, *c; + const char *show_string = NULL; + + presence = xmlnode_new("presence"); + + if(state == JABBER_BUDDY_STATE_UNAVAILABLE) + xmlnode_set_attrib(presence, "type", "unavailable"); + else if(state != JABBER_BUDDY_STATE_ONLINE && + state != JABBER_BUDDY_STATE_UNKNOWN && + state != JABBER_BUDDY_STATE_ERROR) + show_string = jabber_buddy_state_get_show(state); + + if(show_string) { + show = xmlnode_new_child(presence, "show"); + xmlnode_insert_data(show, show_string, -1); + } + + if(msg) { + status = xmlnode_new_child(presence, "status"); + xmlnode_insert_data(status, msg, -1); + } + + if(priority) { + char *pstr = g_strdup_printf("%d", priority); + pri = xmlnode_new_child(presence, "priority"); + xmlnode_insert_data(pri, pstr, -1); + g_free(pstr); + } + + /* JEP-0115 */ + c = xmlnode_new_child(presence, "c"); + xmlnode_set_namespace(c, "http://jabber.org/protocol/caps"); + xmlnode_set_attrib(c, "node", CAPS0115_NODE); + xmlnode_set_attrib(c, "ver", VERSION); + + return presence; +} + +struct _jabber_add_permit { + GaimConnection *gc; + JabberStream *js; + char *who; +}; + +static void authorize_add_cb(struct _jabber_add_permit *jap) +{ + jabber_presence_subscription_set(jap->gc->proto_data, jap->who, + "subscribed"); + g_free(jap->who); + g_free(jap); +} + +static void deny_add_cb(struct _jabber_add_permit *jap) +{ + jabber_presence_subscription_set(jap->gc->proto_data, jap->who, + "unsubscribed"); + + g_free(jap->who); + g_free(jap); +} + +static void jabber_vcard_parse_avatar(JabberStream *js, xmlnode *packet, gpointer blah) +{ + JabberBuddy *jb = NULL; + GaimBuddy *b = NULL; + xmlnode *vcard, *photo, *binval; + char *text; + guchar *data; + gsize size; + const char *from = xmlnode_get_attrib(packet, "from"); + + if(!from) + return; + + jb = jabber_buddy_find(js, from, TRUE); + + js->pending_avatar_requests = g_slist_remove(js->pending_avatar_requests, jb); + + if((vcard = xmlnode_get_child(packet, "vCard")) || + (vcard = xmlnode_get_child_with_namespace(packet, "query", "vcard-temp"))) { + if((photo = xmlnode_get_child(vcard, "PHOTO")) && + (( (binval = xmlnode_get_child(photo, "BINVAL")) && + (text = xmlnode_get_data(binval))) || + (text = xmlnode_get_data(photo)))) { + data = gaim_base64_decode(text, &size); + + gaim_buddy_icons_set_for_user(js->gc->account, from, data, size); + if((b = gaim_find_buddy(js->gc->account, from))) { + unsigned char hashval[20]; + char hash[41], *p; + int i; + + gaim_cipher_digest_region("sha1", data, size, + sizeof(hashval), hashval, NULL); + p = hash; + for(i=0; i<20; i++, p+=2) + snprintf(p, 3, "%02x", hashval[i]); + gaim_blist_node_set_string((GaimBlistNode*)b, "avatar_hash", hash); + } + g_free(data); + g_free(text); + } + } +} + +void jabber_presence_parse(JabberStream *js, xmlnode *packet) +{ + const char *from = xmlnode_get_attrib(packet, "from"); + const char *type = xmlnode_get_attrib(packet, "type"); + const char *real_jid = NULL; + const char *affiliation = NULL; + const char *role = NULL; + char *status = NULL; + int priority = 0; + JabberID *jid; + JabberChat *chat; + JabberBuddy *jb; + JabberBuddyResource *jbr = NULL, *found_jbr = NULL; + GaimConvChatBuddyFlags flags = GAIM_CBFLAGS_NONE; + gboolean delayed = FALSE; + GaimBuddy *b = NULL; + char *buddy_name; + JabberBuddyState state = JABBER_BUDDY_STATE_UNKNOWN; + xmlnode *y; + gboolean muc = FALSE; + char *avatar_hash = NULL; + + if(!(jb = jabber_buddy_find(js, from, TRUE))) + return; + + if(!(jid = jabber_id_new(from))) + return; + + if(jb->error_msg) { + g_free(jb->error_msg); + jb->error_msg = NULL; + } + + if(type && !strcmp(type, "error")) { + char *msg = jabber_parse_error(js, packet); + + state = JABBER_BUDDY_STATE_ERROR; + jb->error_msg = msg ? msg : g_strdup(_("Unknown Error in presence")); + } else if(type && !strcmp(type, "subscribe")) { + struct _jabber_add_permit *jap = g_new0(struct _jabber_add_permit, 1); + gboolean onlist = FALSE; + GaimBuddy *buddy = gaim_find_buddy(gaim_connection_get_account(js->gc), from); + JabberBuddy *jb = NULL; + + if (buddy) { + jb = jabber_buddy_find(js, from, TRUE); + if ((jb->subscription & JABBER_SUB_TO) == 0) + onlist = TRUE; + } + + jap->gc = js->gc; + jap->who = g_strdup(from); + jap->js = js; + + gaim_account_request_authorization(gaim_connection_get_account(js->gc), from, NULL, NULL, NULL, onlist, + G_CALLBACK(authorize_add_cb), G_CALLBACK(deny_add_cb), jap); + jabber_id_free(jid); + return; + } else if(type && !strcmp(type, "subscribed")) { + /* we've been allowed to see their presence, but we don't care */ + jabber_id_free(jid); + return; + } else if(type && !strcmp(type, "unsubscribe")) { + /* XXX I'm not sure this is the right way to handle this, it + * might be better to add "unsubscribe" to the presence status + * if lower down, but I'm not sure. */ + /* they are unsubscribing from our presence, we don't care */ + /* Well, maybe just a little, we might want/need to start + * acknowledging this (and the others) at some point. */ + jabber_id_free(jid); + return; + } else { + if((y = xmlnode_get_child(packet, "show"))) { + char *show = xmlnode_get_data(y); + state = jabber_buddy_show_get_state(show); + g_free(show); + } else { + state = JABBER_BUDDY_STATE_ONLINE; + } + } + + + for(y = packet->child; y; y = y->next) { + if(y->type != XMLNODE_TYPE_TAG) + continue; + + if(!strcmp(y->name, "status")) { + g_free(status); + status = xmlnode_get_data(y); + } else if(!strcmp(y->name, "priority")) { + char *p = xmlnode_get_data(y); + if(p) { + priority = atoi(p); + g_free(p); + } + } else if(!strcmp(y->name, "x")) { + const char *xmlns = xmlnode_get_namespace(y); + if(xmlns && !strcmp(xmlns, "jabber:x:delay")) { + /* XXX: compare the time. jabber:x:delay can happen on presence packets that aren't really and truly delayed */ + delayed = TRUE; + } else if(xmlns && !strcmp(xmlns, "http://jabber.org/protocol/muc#user")) { + xmlnode *z; + + muc = TRUE; + if((z = xmlnode_get_child(y, "status"))) { + const char *code = xmlnode_get_attrib(z, "code"); + if(code && !strcmp(code, "201")) { + if((chat = jabber_chat_find(js, jid->node, jid->domain))) { + chat->config_dialog_type = GAIM_REQUEST_ACTION; + chat->config_dialog_handle = + gaim_request_action(js->gc, + _("Create New Room"), + _("Create New Room"), + _("You are creating a new room. Would" + " you like to configure it, or" + " accept the default settings?"), + 1, chat, 2, _("_Configure Room"), + G_CALLBACK(jabber_chat_request_room_configure), + _("_Accept Defaults"), + G_CALLBACK(jabber_chat_create_instant_room)); + } + } + } + if((z = xmlnode_get_child(y, "item"))) { + real_jid = xmlnode_get_attrib(z, "jid"); + affiliation = xmlnode_get_attrib(z, "affiliation"); + role = xmlnode_get_attrib(z, "role"); + if(affiliation != NULL && !strcmp(affiliation, "owner")) + flags |= GAIM_CBFLAGS_FOUNDER; + if (role != NULL) { + if (!strcmp(role, "moderator")) + flags |= GAIM_CBFLAGS_OP; + else if (!strcmp(role, "participant")) + flags |= GAIM_CBFLAGS_VOICE; + } + } + } else if(xmlns && !strcmp(xmlns, "vcard-temp:x:update")) { + xmlnode *photo = xmlnode_get_child(y, "photo"); + if(photo) { + if(avatar_hash) + g_free(avatar_hash); + avatar_hash = xmlnode_get_data(photo); + } + } + } + } + + + if(jid->node && (chat = jabber_chat_find(js, jid->node, jid->domain))) { + static int i = 1; + char *room_jid = g_strdup_printf("%s@%s", jid->node, jid->domain); + + if(state == JABBER_BUDDY_STATE_ERROR) { + char *title, *msg = jabber_parse_error(js, packet); + + if(chat->conv) { + title = g_strdup_printf(_("Error in chat %s"), from); + serv_got_chat_left(js->gc, chat->id); + } else { + title = g_strdup_printf(_("Error joining chat %s"), from); + } + gaim_notify_error(js->gc, title, title, msg); + g_free(title); + g_free(msg); + + jabber_chat_destroy(chat); + jabber_id_free(jid); + g_free(status); + g_free(room_jid); + if(avatar_hash) + g_free(avatar_hash); + return; + } + + + if(type && !strcmp(type, "unavailable")) { + gboolean nick_change = FALSE; + + /* If we haven't joined the chat yet, we don't care that someone + * left, or it was us leaving after we closed the chat */ + if(!chat->conv) { + if(jid->resource && chat->handle && !strcmp(jid->resource, chat->handle)) + jabber_chat_destroy(chat); + jabber_id_free(jid); + g_free(status); + g_free(room_jid); + if(avatar_hash) + g_free(avatar_hash); + return; + } + + jabber_buddy_remove_resource(jb, jid->resource); + if(chat->muc) { + xmlnode *x; + for(x = xmlnode_get_child(packet, "x"); x; x = xmlnode_get_next_twin(x)) { + const char *xmlns, *nick, *code; + xmlnode *stat, *item; + if(!(xmlns = xmlnode_get_namespace(x)) || + strcmp(xmlns, "http://jabber.org/protocol/muc#user")) + continue; + if(!(stat = xmlnode_get_child(x, "status"))) + continue; + if(!(code = xmlnode_get_attrib(stat, "code"))) + continue; + if(!strcmp(code, "301")) { + /* XXX: we got banned */ + } else if(!strcmp(code, "303")) { + if(!(item = xmlnode_get_child(x, "item"))) + continue; + if(!(nick = xmlnode_get_attrib(item, "nick"))) + continue; + nick_change = TRUE; + if(!strcmp(jid->resource, chat->handle)) { + g_free(chat->handle); + chat->handle = g_strdup(nick); + } + gaim_conv_chat_rename_user(GAIM_CONV_CHAT(chat->conv), jid->resource, nick); + jabber_chat_remove_handle(chat, jid->resource); + break; + } else if(!strcmp(code, "307")) { + /* XXX: we got kicked */ + } else if(!strcmp(code, "321")) { + /* XXX: removed due to an affiliation change */ + } else if(!strcmp(code, "322")) { + /* XXX: removed because room is now members-only */ + } else if(!strcmp(code, "332")) { + /* XXX: removed due to system shutdown */ + } + } + } + if(!nick_change) { + if(!g_utf8_collate(jid->resource, chat->handle)) { + serv_got_chat_left(js->gc, chat->id); + jabber_chat_destroy(chat); + } else { + gaim_conv_chat_remove_user(GAIM_CONV_CHAT(chat->conv), jid->resource, + status); + jabber_chat_remove_handle(chat, jid->resource); + } + } + } else { + if(!chat->conv) { + chat->id = i++; + chat->muc = muc; + chat->conv = serv_got_joined_chat(js->gc, chat->id, room_jid); + gaim_conv_chat_set_nick(GAIM_CONV_CHAT(chat->conv), chat->handle); + + jabber_chat_disco_traffic(chat); + } + + jabber_buddy_track_resource(jb, jid->resource, priority, state, + status); + + jabber_chat_track_handle(chat, jid->resource, real_jid, affiliation, role); + + if(!jabber_chat_find_buddy(chat->conv, jid->resource)) + gaim_conv_chat_add_user(GAIM_CONV_CHAT(chat->conv), jid->resource, + real_jid, flags, !delayed); + else + gaim_conv_chat_user_set_flags(GAIM_CONV_CHAT(chat->conv), jid->resource, + flags); + } + g_free(room_jid); + } else { + buddy_name = g_strdup_printf("%s%s%s", jid->node ? jid->node : "", + jid->node ? "@" : "", jid->domain); + if((b = gaim_find_buddy(js->gc->account, buddy_name)) == NULL) { + gaim_debug_warning("jabber", "Got presence for unknown buddy %s on account %s (%x)", + buddy_name, gaim_account_get_username(js->gc->account), js->gc->account); + jabber_id_free(jid); + if(avatar_hash) + g_free(avatar_hash); + g_free(buddy_name); + g_free(status); + return; + } + + if(avatar_hash) { + const char *avatar_hash2 = gaim_blist_node_get_string((GaimBlistNode*)b, "avatar_hash"); + if(!avatar_hash2 || strcmp(avatar_hash, avatar_hash2)) { + JabberIq *iq; + xmlnode *vcard; + + /* XXX this is a crappy way of trying to prevent + * someone from spamming us with presence packets + * and causing us to DoS ourselves...what we really + * need is a queue system that can throttle itself, + * but i'm too tired to write that right now */ + if(!g_slist_find(js->pending_avatar_requests, jb)) { + + js->pending_avatar_requests = g_slist_prepend(js->pending_avatar_requests, jb); + + iq = jabber_iq_new(js, JABBER_IQ_GET); + xmlnode_set_attrib(iq->node, "to", buddy_name); + vcard = xmlnode_new_child(iq->node, "vCard"); + xmlnode_set_namespace(vcard, "vcard-temp"); + + jabber_iq_set_callback(iq, jabber_vcard_parse_avatar, NULL); + jabber_iq_send(iq); + } + } + } + + if(state == JABBER_BUDDY_STATE_ERROR || + (type && (!strcmp(type, "unavailable") || + !strcmp(type, "unsubscribed")))) { + GaimConversation *conv; + + jabber_buddy_remove_resource(jb, jid->resource); + if((conv = jabber_find_unnormalized_conv(from, js->gc->account))) + gaim_conversation_set_name(conv, buddy_name); + + } else { + jbr = jabber_buddy_track_resource(jb, jid->resource, priority, + state, status); + } + + if((found_jbr = jabber_buddy_find_resource(jb, NULL))) { + if(!jbr || jbr == found_jbr) { + gaim_prpl_got_user_status(js->gc->account, buddy_name, jabber_buddy_state_get_status_id(state), "priority", found_jbr->priority, found_jbr->status ? "message" : NULL, found_jbr->status, NULL); + } + } else { + gaim_prpl_got_user_status(js->gc->account, buddy_name, "offline", status ? "message" : NULL, status, NULL); + } + g_free(buddy_name); + } + g_free(status); + jabber_id_free(jid); + if(avatar_hash) + g_free(avatar_hash); +} + +void jabber_presence_subscription_set(JabberStream *js, const char *who, const char *type) +{ + xmlnode *presence = xmlnode_new("presence"); + + xmlnode_set_attrib(presence, "to", who); + xmlnode_set_attrib(presence, "type", type); + + jabber_send(js, presence); + xmlnode_free(presence); +} + +void gaim_status_to_jabber(const GaimStatus *status, JabberBuddyState *state, char **msg, int *priority) +{ + const char *status_id = NULL; + const char *formatted_msg = NULL; + + if(state) *state = JABBER_BUDDY_STATE_UNKNOWN; + if(msg) *msg = NULL; + if(priority) *priority = 0; + + if(!status) { + if(state) *state = JABBER_BUDDY_STATE_UNAVAILABLE; + } else { + if(state) { + status_id = gaim_status_get_id(status); + *state = jabber_buddy_status_id_get_state(status_id); + } + + if(msg) { + formatted_msg = gaim_status_get_attr_string(status, "message"); + + /* if the message is blank, then there really isn't a message */ + if(formatted_msg && !*formatted_msg) + formatted_msg = NULL; + + if(formatted_msg) + gaim_markup_html_to_xhtml(formatted_msg, NULL, msg); + } + + if(priority) + *priority = gaim_status_get_attr_int(status, "priority"); + } +} diff --git a/libpurple/protocols/jabber/presence.h b/libpurple/protocols/jabber/presence.h new file mode 100644 index 0000000000..3c65f3d823 --- /dev/null +++ b/libpurple/protocols/jabber/presence.h @@ -0,0 +1,37 @@ +/** + * @file presence.h Presence + * + * gaim + * + * Copyright (C) 2003 Nathan Walp <faceprint@faceprint.com> + * + * 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 + */ +#ifndef _GAIM_JABBER_PRESENCE_H_ +#define _GAIM_JABBER_PRESENCE_H_ + +#include "buddy.h" +#include "jabber.h" +#include "xmlnode.h" + +void jabber_presence_send(GaimAccount *account, GaimStatus *status); +xmlnode *jabber_presence_create(JabberBuddyState state, const char *msg, int priority); +void jabber_presence_parse(JabberStream *js, xmlnode *packet); +void jabber_presence_subscription_set(JabberStream *js, const char *who, + const char *type); +void jabber_presence_fake_to_self(JabberStream *js, const GaimStatus *status); +void gaim_status_to_jabber(const GaimStatus *status, JabberBuddyState *state, char **msg, int *priority); + +#endif /* _GAIM_JABBER_PRESENCE_H_ */ diff --git a/libpurple/protocols/jabber/roster.c b/libpurple/protocols/jabber/roster.c new file mode 100644 index 0000000000..d551466030 --- /dev/null +++ b/libpurple/protocols/jabber/roster.c @@ -0,0 +1,409 @@ +/* + * gaim - Jabber Protocol Plugin + * + * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com> + * + * 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 + * + */ +#include "internal.h" +#include "debug.h" +#include "server.h" +#include "util.h" + +#include "buddy.h" +#include "google.h" +#include "presence.h" +#include "roster.h" +#include "iq.h" + +#include <string.h> + + +void jabber_roster_request(JabberStream *js) +{ + JabberIq *iq; + + iq = jabber_iq_new_query(js, JABBER_IQ_GET, "jabber:iq:roster"); + + jabber_iq_send(iq); +} + +static void remove_gaim_buddies(JabberStream *js, const char *jid) +{ + GSList *buddies, *l; + + buddies = gaim_find_buddies(js->gc->account, jid); + + for(l = buddies; l; l = l->next) + gaim_blist_remove_buddy(l->data); + + g_slist_free(buddies); +} + +static void add_gaim_buddies_in_groups(JabberStream *js, const char *jid, + const char *alias, GSList *groups) +{ + GSList *buddies, *g2, *l; + gchar *my_bare_jid; + + buddies = gaim_find_buddies(js->gc->account, jid); + + g2 = groups; + + if(!groups) { + if(!buddies) + g2 = g_slist_append(g2, g_strdup(_("Buddies"))); + else + return; + } + + my_bare_jid = g_strdup_printf("%s@%s", js->user->node, js->user->domain); + + while(buddies) { + GaimBuddy *b = buddies->data; + GaimGroup *g = gaim_buddy_get_group(b); + + buddies = g_slist_remove(buddies, b); + + if((l = g_slist_find_custom(g2, g->name, (GCompareFunc)strcmp))) { + const char *servernick; + + if((servernick = gaim_blist_node_get_string((GaimBlistNode*)b, "servernick"))) + serv_got_alias(js->gc, jid, servernick); + + if(alias && (!b->alias || strcmp(b->alias, alias))) + gaim_blist_alias_buddy(b, alias); + g_free(l->data); + g2 = g_slist_delete_link(g2, l); + } else { + gaim_blist_remove_buddy(b); + } + } + + while(g2) { + GaimBuddy *b = gaim_buddy_new(js->gc->account, jid, alias); + GaimGroup *g = gaim_find_group(g2->data); + + if(!g) { + g = gaim_group_new(g2->data); + gaim_blist_add_group(g, NULL); + } + + gaim_blist_add_buddy(b, NULL, g, NULL); + gaim_blist_alias_buddy(b, alias); + + /* If we just learned about ourself, then fake our status, + * because we won't be receiving a normal presence message + * about ourself. */ + if(!strcmp(b->name, my_bare_jid)) { + GaimPresence *gpresence; + GaimStatus *status; + + gpresence = gaim_account_get_presence(js->gc->account); + status = gaim_presence_get_active_status(gpresence); + jabber_presence_fake_to_self(js, status); + } + + g_free(g2->data); + g2 = g_slist_delete_link(g2, g2); + } + + g_free(my_bare_jid); + g_slist_free(buddies); +} + +void jabber_roster_parse(JabberStream *js, xmlnode *packet) +{ + xmlnode *query, *item, *group; + const char *from = xmlnode_get_attrib(packet, "from"); + + if(from) { + char *from_norm; + gboolean invalid; + + from_norm = g_strdup(jabber_normalize(js->gc->account, from)); + + if(!from_norm) + return; + + invalid = g_utf8_collate(from_norm, + jabber_normalize(js->gc->account, + gaim_account_get_username(js->gc->account))); + + g_free(from_norm); + + if(invalid) + return; + } + + query = xmlnode_get_child(packet, "query"); + if(!query) + return; + + js->roster_parsed = TRUE; + + for(item = xmlnode_get_child(query, "item"); item; item = xmlnode_get_next_twin(item)) + { + const char *jid, *name, *subscription, *ask; + JabberBuddy *jb; + + subscription = xmlnode_get_attrib(item, "subscription"); + jid = xmlnode_get_attrib(item, "jid"); + name = xmlnode_get_attrib(item, "name"); + ask = xmlnode_get_attrib(item, "ask"); + + if(!jid) + continue; + + if(!(jb = jabber_buddy_find(js, jid, TRUE))) + continue; + + if(subscription) { + gint me = -1; + char *jid_norm; + const char *username; + + jid_norm = g_strdup(jabber_normalize(js->gc->account, jid)); + username = gaim_account_get_username(js->gc->account); + me = g_utf8_collate(jid_norm, + jabber_normalize(js->gc->account, + username)); + + if(me == 0) + jb->subscription = JABBER_SUB_BOTH; + else if(!strcmp(subscription, "none")) + jb->subscription = JABBER_SUB_NONE; + else if(!strcmp(subscription, "to")) + jb->subscription = JABBER_SUB_TO; + else if(!strcmp(subscription, "from")) + jb->subscription = JABBER_SUB_FROM; + else if(!strcmp(subscription, "both")) + jb->subscription = JABBER_SUB_BOTH; + else if(!strcmp(subscription, "remove")) + jb->subscription = JABBER_SUB_REMOVE; + /* XXX: if subscription is now "from" or "none" we need to + * fake a signoff, since we won't get any presence from them + * anymore */ + /* YYY: I was going to use this, but I'm not sure it's necessary + * anymore, but it's here in case it is. */ + /* + if ((jb->subscription & JABBER_SUB_FROM) || + (jb->subscription & JABBER_SUB_NONE)) { + gaim_prpl_got_user_status(js->gc->account, jid, "offline", NULL); + } + */ + } + + if(ask && !strcmp(ask, "subscribe")) + jb->subscription |= JABBER_SUB_PENDING; + else + jb->subscription &= ~JABBER_SUB_PENDING; + + if(jb->subscription == JABBER_SUB_REMOVE) { + remove_gaim_buddies(js, jid); + } else { + GSList *groups = NULL; + for(group = xmlnode_get_child(item, "group"); group; group = xmlnode_get_next_twin(group)) { + char *group_name; + + if(!(group_name = xmlnode_get_data(group))) + group_name = g_strdup(""); + + if (g_slist_find_custom(groups, group_name, (GCompareFunc)gaim_utf8_strcasecmp) == NULL) + groups = g_slist_append(groups, group_name); + } + if (js->server_caps & JABBER_CAP_GOOGLE_ROSTER) + jabber_google_roster_incoming(js, item); + add_gaim_buddies_in_groups(js, jid, name, groups); + } + } +} + +static void jabber_roster_update(JabberStream *js, const char *name, + GSList *grps) +{ + GaimBuddy *b; + GaimGroup *g; + GSList *groups = NULL, *l; + JabberIq *iq; + xmlnode *query, *item, *group; + + if(grps) { + groups = grps; + } else { + GSList *buddies = gaim_find_buddies(js->gc->account, name); + if(!buddies) + return; + while(buddies) { + b = buddies->data; + g = gaim_buddy_get_group(b); + groups = g_slist_append(groups, g->name); + buddies = g_slist_remove(buddies, b); + } + } + + if(!(b = gaim_find_buddy(js->gc->account, name))) + return; + + iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:roster"); + + query = xmlnode_get_child(iq->node, "query"); + item = xmlnode_new_child(query, "item"); + + xmlnode_set_attrib(item, "jid", name); + + xmlnode_set_attrib(item, "name", b->alias ? b->alias : ""); + + for(l = groups; l; l = l->next) { + group = xmlnode_new_child(item, "group"); + xmlnode_insert_data(group, l->data, -1); + } + + if(!grps) + g_slist_free(groups); + + if (js->server_caps & JABBER_CAP_GOOGLE_ROSTER) { + jabber_google_roster_outgoing(js, query, item); + xmlnode_set_attrib(query, "xmlns:gr", "google:roster"); + xmlnode_set_attrib(query, "gr:ext", "2"); + } + jabber_iq_send(iq); +} + +void jabber_roster_add_buddy(GaimConnection *gc, GaimBuddy *buddy, + GaimGroup *group) +{ + JabberStream *js = gc->proto_data; + char *who; + GSList *groups = NULL; + JabberBuddy *jb; + JabberBuddyResource *jbr; + char *my_bare_jid; + + if(!js->roster_parsed) + return; + + if(!(who = jabber_get_bare_jid(buddy->name))) + return; + + jb = jabber_buddy_find(js, buddy->name, FALSE); + + if(!jb || !(jb->subscription & JABBER_SUB_TO)) { + groups = g_slist_append(groups, group->name); + } + + jabber_roster_update(js, who, groups); + + my_bare_jid = g_strdup_printf("%s@%s", js->user->node, js->user->domain); + if(!strcmp(who, my_bare_jid)) { + GaimPresence *gpresence; + GaimStatus *status; + + gpresence = gaim_account_get_presence(js->gc->account); + status = gaim_presence_get_active_status(gpresence); + jabber_presence_fake_to_self(js, status); + } else if(!jb || !(jb->subscription & JABBER_SUB_TO)) { + jabber_presence_subscription_set(js, who, "subscribe"); + } else if((jbr =jabber_buddy_find_resource(jb, NULL))) { + gaim_prpl_got_user_status(gc->account, who, + jabber_buddy_state_get_status_id(jbr->state), + "priority", jbr->priority, jbr->status ? "message" : NULL, jbr->status, NULL); + } + + g_free(my_bare_jid); + g_free(who); +} + +void jabber_roster_alias_change(GaimConnection *gc, const char *name, const char *alias) +{ + GaimBuddy *b = gaim_find_buddy(gc->account, name); + + if(b != NULL) { + gaim_blist_alias_buddy(b, alias); + + jabber_roster_update(gc->proto_data, name, NULL); + } +} + +void jabber_roster_group_change(GaimConnection *gc, const char *name, + const char *old_group, const char *new_group) +{ + GSList *buddies, *groups = NULL; + GaimBuddy *b; + GaimGroup *g; + + if(!old_group || !new_group || !strcmp(old_group, new_group)) + return; + + buddies = gaim_find_buddies(gc->account, name); + while(buddies) { + b = buddies->data; + g = gaim_buddy_get_group(b); + if(!strcmp(g->name, old_group)) + groups = g_slist_append(groups, (char*)new_group); /* ick */ + else + groups = g_slist_append(groups, g->name); + buddies = g_slist_remove(buddies, b); + } + jabber_roster_update(gc->proto_data, name, groups); + g_slist_free(groups); +} + +void jabber_roster_group_rename(GaimConnection *gc, const char *old_name, + GaimGroup *group, GList *moved_buddies) +{ + GList *l; + for(l = moved_buddies; l; l = l->next) { + GaimBuddy *buddy = l->data; + jabber_roster_group_change(gc, buddy->name, old_name, group->name); + } +} + +void jabber_roster_remove_buddy(GaimConnection *gc, GaimBuddy *buddy, + GaimGroup *group) { + GSList *buddies = gaim_find_buddies(gc->account, buddy->name); + GSList *groups = NULL; + + buddies = g_slist_remove(buddies, buddy); + if(g_slist_length(buddies)) { + GaimBuddy *tmpbuddy; + GaimGroup *tmpgroup; + + while(buddies) { + tmpbuddy = buddies->data; + tmpgroup = gaim_buddy_get_group(tmpbuddy); + groups = g_slist_append(groups, tmpgroup->name); + buddies = g_slist_remove(buddies, tmpbuddy); + } + + jabber_roster_update(gc->proto_data, buddy->name, groups); + } else { + JabberIq *iq = jabber_iq_new_query(gc->proto_data, JABBER_IQ_SET, + "jabber:iq:roster"); + xmlnode *query = xmlnode_get_child(iq->node, "query"); + xmlnode *item = xmlnode_new_child(query, "item"); + + xmlnode_set_attrib(item, "jid", buddy->name); + xmlnode_set_attrib(item, "subscription", "remove"); + + jabber_iq_send(iq); + } + + if(buddies) + g_slist_free(buddies); + if(groups) + g_slist_free(groups); +} diff --git a/libpurple/protocols/jabber/roster.h b/libpurple/protocols/jabber/roster.h new file mode 100644 index 0000000000..798effa6c7 --- /dev/null +++ b/libpurple/protocols/jabber/roster.h @@ -0,0 +1,42 @@ +/** + * @file roster.h Roster manipulation + * + * gaim + * + * Copyright (C) 2003 Nathan Walp <faceprint@faceprint.com> + * + * 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 + */ +#ifndef _GAIM_JABBER_ROSTER_H_ +#define _GAIM_JABBER_ROSTER_H_ + +#include "jabber.h" + +void jabber_roster_request(JabberStream *js); + +void jabber_roster_parse(JabberStream *js, xmlnode *packet); + +void jabber_roster_add_buddy(GaimConnection *gc, GaimBuddy *buddy, + GaimGroup *group); +void jabber_roster_alias_change(GaimConnection *gc, const char *name, + const char *alias); +void jabber_roster_group_change(GaimConnection *gc, const char *name, + const char *old_group, const char *new_group); +void jabber_roster_group_rename(GaimConnection *gc, const char *old_name, + GaimGroup *group, GList *moved_buddies); +void jabber_roster_remove_buddy(GaimConnection *gc, GaimBuddy *buddy, + GaimGroup *group); + +#endif /* _GAIM_JABBER_ROSTER_H_ */ diff --git a/libpurple/protocols/jabber/si.c b/libpurple/protocols/jabber/si.c new file mode 100644 index 0000000000..637e48802e --- /dev/null +++ b/libpurple/protocols/jabber/si.c @@ -0,0 +1,974 @@ +/* + * gaim - Jabber Protocol Plugin + * + * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com> + * + * 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 + * + */ + +#include "blist.h" + +#include "internal.h" +#include "cipher.h" +#include "debug.h" +#include "ft.h" +#include "network.h" +#include "notify.h" + +#include "buddy.h" +#include "disco.h" +#include "jabber.h" +#include "iq.h" +#include "si.h" + +#include "si.h" + +struct bytestreams_streamhost { + char *jid; + char *host; + int port; +}; + +typedef struct _JabberSIXfer { + JabberStream *js; + + GaimProxyConnectData *connect_data; + GaimNetworkListenData *listen_data; + + gboolean accepted; + + char *stream_id; + char *iq_id; + + enum { + STREAM_METHOD_UNKNOWN = 0, + STREAM_METHOD_BYTESTREAMS = 2 << 1, + STREAM_METHOD_IBB = 2 << 2, + STREAM_METHOD_UNSUPPORTED = 2 << 31 + } stream_method; + + GList *streamhosts; + GaimProxyInfo *gpi; + + char *rxqueue; + size_t rxlen; + gsize rxmaxlen; +} JabberSIXfer; + +static GaimXfer* +jabber_si_xfer_find(JabberStream *js, const char *sid, const char *from) +{ + GList *xfers; + + if(!sid || !from) + return NULL; + + for(xfers = js->file_transfers; xfers; xfers = xfers->next) { + GaimXfer *xfer = xfers->data; + JabberSIXfer *jsx = xfer->data; + if(jsx->stream_id && xfer->who && + !strcmp(jsx->stream_id, sid) && !strcmp(xfer->who, from)) + return xfer; + } + + return NULL; +} + + +static void jabber_si_bytestreams_attempt_connect(GaimXfer *xfer); + +static void +jabber_si_bytestreams_connect_cb(gpointer data, gint source, const gchar *error_message) +{ + GaimXfer *xfer = data; + JabberSIXfer *jsx = xfer->data; + JabberIq *iq; + xmlnode *query, *su; + struct bytestreams_streamhost *streamhost = jsx->streamhosts->data; + + gaim_proxy_info_destroy(jsx->gpi); + jsx->connect_data = NULL; + + if(source < 0) { + jsx->streamhosts = g_list_remove(jsx->streamhosts, streamhost); + g_free(streamhost->jid); + g_free(streamhost->host); + g_free(streamhost); + jabber_si_bytestreams_attempt_connect(xfer); + return; + } + + iq = jabber_iq_new_query(jsx->js, JABBER_IQ_RESULT, "http://jabber.org/protocol/bytestreams"); + xmlnode_set_attrib(iq->node, "to", xfer->who); + jabber_iq_set_id(iq, jsx->iq_id); + query = xmlnode_get_child(iq->node, "query"); + su = xmlnode_new_child(query, "streamhost-used"); + xmlnode_set_attrib(su, "jid", streamhost->jid); + + jabber_iq_send(iq); + + gaim_xfer_start(xfer, source, NULL, -1); +} + +static void jabber_si_bytestreams_attempt_connect(GaimXfer *xfer) +{ + JabberSIXfer *jsx = xfer->data; + struct bytestreams_streamhost *streamhost; + char *dstaddr, *p; + int i; + unsigned char hashval[20]; + JabberID *dstjid; + + if(!jsx->streamhosts) { + JabberIq *iq = jabber_iq_new(jsx->js, JABBER_IQ_ERROR); + xmlnode *error, *inf; + + if(jsx->iq_id) + jabber_iq_set_id(iq, jsx->iq_id); + + xmlnode_set_attrib(iq->node, "to", xfer->who); + error = xmlnode_new_child(iq->node, "error"); + xmlnode_set_attrib(error, "code", "404"); + xmlnode_set_attrib(error, "type", "cancel"); + inf = xmlnode_new_child(error, "item-not-found"); + xmlnode_set_namespace(inf, "urn:ietf:params:xml:ns:xmpp-stanzas"); + + jabber_iq_send(iq); + + gaim_xfer_cancel_local(xfer); + + return; + } + + streamhost = jsx->streamhosts->data; + + dstjid = jabber_id_new(xfer->who); + + if(dstjid != NULL) { + jsx->gpi = gaim_proxy_info_new(); + gaim_proxy_info_set_type(jsx->gpi, GAIM_PROXY_SOCKS5); + gaim_proxy_info_set_host(jsx->gpi, streamhost->host); + gaim_proxy_info_set_port(jsx->gpi, streamhost->port); + + + + dstaddr = g_strdup_printf("%s%s@%s/%s%s@%s/%s", jsx->stream_id, dstjid->node, dstjid->domain, dstjid->resource, jsx->js->user->node, + jsx->js->user->domain, jsx->js->user->resource); + + gaim_cipher_digest_region("sha1", (guchar *)dstaddr, strlen(dstaddr), + sizeof(hashval), hashval, NULL); + g_free(dstaddr); + dstaddr = g_malloc(41); + p = dstaddr; + for(i=0; i<20; i++, p+=2) + snprintf(p, 3, "%02x", hashval[i]); + + jsx->connect_data = gaim_proxy_connect_socks5(NULL, jsx->gpi, + dstaddr, 0, + jabber_si_bytestreams_connect_cb, xfer); + g_free(dstaddr); + + jabber_id_free(dstjid); + } + + if (jsx->connect_data == NULL) + { + jsx->streamhosts = g_list_remove(jsx->streamhosts, streamhost); + g_free(streamhost->jid); + g_free(streamhost->host); + g_free(streamhost); + jabber_si_bytestreams_attempt_connect(xfer); + } +} + +void jabber_bytestreams_parse(JabberStream *js, xmlnode *packet) +{ + GaimXfer *xfer; + JabberSIXfer *jsx; + xmlnode *query, *streamhost; + const char *sid, *from, *type; + + if(!(type = xmlnode_get_attrib(packet, "type")) || strcmp(type, "set")) + return; + + if(!(from = xmlnode_get_attrib(packet, "from"))) + return; + + if(!(query = xmlnode_get_child(packet, "query"))) + return; + + if(!(sid = xmlnode_get_attrib(query, "sid"))) + return; + + if(!(xfer = jabber_si_xfer_find(js, sid, from))) + return; + + jsx = xfer->data; + + if(!jsx->accepted) + return; + + if(jsx->iq_id) + g_free(jsx->iq_id); + jsx->iq_id = g_strdup(xmlnode_get_attrib(packet, "id")); + + for(streamhost = xmlnode_get_child(query, "streamhost"); streamhost; + streamhost = xmlnode_get_next_twin(streamhost)) { + const char *jid, *host, *port; + int portnum; + + if((jid = xmlnode_get_attrib(streamhost, "jid")) && + (host = xmlnode_get_attrib(streamhost, "host")) && + (port = xmlnode_get_attrib(streamhost, "port")) && + (portnum = atoi(port))) { + struct bytestreams_streamhost *sh = g_new0(struct bytestreams_streamhost, 1); + sh->jid = g_strdup(jid); + sh->host = g_strdup(host); + sh->port = portnum; + jsx->streamhosts = g_list_append(jsx->streamhosts, sh); + } + } + + jabber_si_bytestreams_attempt_connect(xfer); +} + + +static void +jabber_si_xfer_bytestreams_send_read_again_resp_cb(gpointer data, gint source, + GaimInputCondition cond) +{ + GaimXfer *xfer = data; + JabberSIXfer *jsx = xfer->data; + int len; + + len = write(source, jsx->rxqueue + jsx->rxlen, jsx->rxmaxlen - jsx->rxlen); + if (len < 0 && errno == EAGAIN) + return; + else if (len < 0) { + gaim_input_remove(xfer->watcher); + xfer->watcher = 0; + g_free(jsx->rxqueue); + jsx->rxqueue = NULL; + close(source); + gaim_xfer_cancel_remote(xfer); + return; + } + jsx->rxlen += len; + + if (jsx->rxlen < jsx->rxmaxlen) + return; + + gaim_input_remove(xfer->watcher); + xfer->watcher = 0; + g_free(jsx->rxqueue); + jsx->rxqueue = NULL; + + gaim_xfer_start(xfer, source, NULL, -1); +} + +static void +jabber_si_xfer_bytestreams_send_read_again_cb(gpointer data, gint source, + GaimInputCondition cond) +{ + GaimXfer *xfer = data; + JabberSIXfer *jsx = xfer->data; + int i; + char buffer[256]; + int len; + char *dstaddr, *p; + unsigned char hashval[20]; + const char *host; + + gaim_debug_info("jabber", "in jabber_si_xfer_bytestreams_send_read_again_cb\n"); + + if(jsx->rxlen < 5) { + gaim_debug_info("jabber", "reading the first 5 bytes\n"); + len = read(source, buffer, 5 - jsx->rxlen); + if(len < 0 && errno == EAGAIN) + return; + else if(len <= 0) { + gaim_input_remove(xfer->watcher); + xfer->watcher = 0; + close(source); + gaim_xfer_cancel_remote(xfer); + return; + } + jsx->rxqueue = g_realloc(jsx->rxqueue, len + jsx->rxlen); + memcpy(jsx->rxqueue + jsx->rxlen, buffer, len); + jsx->rxlen += len; + return; + } else if(jsx->rxqueue[0] != 0x05 || jsx->rxqueue[1] != 0x01 || + jsx->rxqueue[3] != 0x03) { + gaim_debug_info("jabber", "invalid socks5 stuff\n"); + gaim_input_remove(xfer->watcher); + xfer->watcher = 0; + close(source); + gaim_xfer_cancel_remote(xfer); + return; + } else if(jsx->rxlen - 5 < jsx->rxqueue[4] + 2) { + gaim_debug_info("jabber", "reading umpteen more bytes\n"); + len = read(source, buffer, jsx->rxqueue[4] + 5 + 2 - jsx->rxlen); + if(len < 0 && errno == EAGAIN) + return; + else if(len <= 0) { + gaim_input_remove(xfer->watcher); + xfer->watcher = 0; + close(source); + gaim_xfer_cancel_remote(xfer); + return; + } + jsx->rxqueue = g_realloc(jsx->rxqueue, len + jsx->rxlen); + memcpy(jsx->rxqueue + jsx->rxlen, buffer, len); + jsx->rxlen += len; + } + + if(jsx->rxlen - 5 < jsx->rxqueue[4] + 2) + return; + + gaim_input_remove(xfer->watcher); + xfer->watcher = 0; + + dstaddr = g_strdup_printf("%s%s@%s/%s%s", jsx->stream_id, + jsx->js->user->node, jsx->js->user->domain, + jsx->js->user->resource, xfer->who); + + gaim_cipher_digest_region("sha1", (guchar *)dstaddr, strlen(dstaddr), + sizeof(hashval), hashval, NULL); + g_free(dstaddr); + dstaddr = g_malloc(41); + p = dstaddr; + for(i=0; i<20; i++, p+=2) + snprintf(p, 3, "%02x", hashval[i]); + + if(jsx->rxqueue[4] != 40 || strncmp(dstaddr, jsx->rxqueue+5, 40) || + jsx->rxqueue[45] != 0x00 || jsx->rxqueue[46] != 0x00) { + gaim_debug_error("jabber", "someone connected with the wrong info!\n"); + close(source); + gaim_xfer_cancel_remote(xfer); + return; + } + + g_free(jsx->rxqueue); + host = gaim_network_get_my_ip(jsx->js->fd); + + jsx->rxmaxlen = 5 + strlen(host) + 2; + jsx->rxqueue = g_malloc(jsx->rxmaxlen); + jsx->rxlen = 0; + + jsx->rxqueue[0] = 0x05; + jsx->rxqueue[1] = 0x00; + jsx->rxqueue[2] = 0x00; + jsx->rxqueue[3] = 0x03; + jsx->rxqueue[4] = strlen(host); + memcpy(jsx->rxqueue + 5, host, strlen(host)); + jsx->rxqueue[5+strlen(host)] = 0x00; + jsx->rxqueue[6+strlen(host)] = 0x00; + + xfer->watcher = gaim_input_add(source, GAIM_INPUT_WRITE, + jabber_si_xfer_bytestreams_send_read_again_resp_cb, xfer); + jabber_si_xfer_bytestreams_send_read_again_resp_cb(xfer, source, + GAIM_INPUT_WRITE); +} + +static void +jabber_si_xfer_bytestreams_send_read_response_cb(gpointer data, gint source, + GaimInputCondition cond) +{ + GaimXfer *xfer = data; + JabberSIXfer *jsx = xfer->data; + int len; + + len = write(source, jsx->rxqueue + jsx->rxlen, jsx->rxmaxlen - jsx->rxlen); + if (len < 0 && errno == EAGAIN) + return; + else if (len < 0) { + gaim_input_remove(xfer->watcher); + xfer->watcher = 0; + g_free(jsx->rxqueue); + jsx->rxqueue = NULL; + close(source); + gaim_xfer_cancel_remote(xfer); + return; + } + jsx->rxlen += len; + + if (jsx->rxlen < jsx->rxmaxlen) + return; + + gaim_input_remove(xfer->watcher); + xfer->watcher = 0; + + if (jsx->rxqueue[1] == 0x00) { + xfer->watcher = gaim_input_add(source, GAIM_INPUT_READ, + jabber_si_xfer_bytestreams_send_read_again_cb, xfer); + g_free(jsx->rxqueue); + jsx->rxqueue = NULL; + } else { + close(source); + gaim_xfer_cancel_remote(xfer); + } +} + +static void +jabber_si_xfer_bytestreams_send_read_cb(gpointer data, gint source, + GaimInputCondition cond) +{ + GaimXfer *xfer = data; + JabberSIXfer *jsx = xfer->data; + int i; + int len; + char buffer[256]; + + gaim_debug_info("jabber", "in jabber_si_xfer_bytestreams_send_read_cb\n"); + + xfer->fd = source; + + if(jsx->rxlen < 2) { + gaim_debug_info("jabber", "reading those first two bytes\n"); + len = read(source, buffer, 2 - jsx->rxlen); + if(len < 0 && errno == EAGAIN) + return; + else if(len <= 0) { + gaim_input_remove(xfer->watcher); + xfer->watcher = 0; + close(source); + gaim_xfer_cancel_remote(xfer); + return; + } + jsx->rxqueue = g_realloc(jsx->rxqueue, len + jsx->rxlen); + memcpy(jsx->rxqueue + jsx->rxlen, buffer, len); + jsx->rxlen += len; + return; + } else if(jsx->rxlen - 2 < jsx->rxqueue[1]) { + gaim_debug_info("jabber", "reading the next umpteen bytes\n"); + len = read(source, buffer, jsx->rxqueue[1] + 2 - jsx->rxlen); + if(len < 0 && errno == EAGAIN) + return; + else if(len <= 0) { + gaim_input_remove(xfer->watcher); + xfer->watcher = 0; + close(source); + gaim_xfer_cancel_remote(xfer); + return; + } + jsx->rxqueue = g_realloc(jsx->rxqueue, len + jsx->rxlen); + memcpy(jsx->rxqueue + jsx->rxlen, buffer, len); + jsx->rxlen += len; + } + + if(jsx->rxlen -2 < jsx->rxqueue[1]) + return; + + gaim_input_remove(xfer->watcher); + xfer->watcher = 0; + + gaim_debug_info("jabber", "checking to make sure we're socks FIVE\n"); + + if(jsx->rxqueue[0] != 0x05) { + close(source); + gaim_xfer_cancel_remote(xfer); + return; + } + + gaim_debug_info("jabber", "going to test %hhu different methods\n", jsx->rxqueue[1]); + + for(i=0; i<jsx->rxqueue[1]; i++) { + + gaim_debug_info("jabber", "testing %hhu\n", jsx->rxqueue[i+2]); + if(jsx->rxqueue[i+2] == 0x00) { + g_free(jsx->rxqueue); + jsx->rxlen = 0; + jsx->rxmaxlen = 2; + jsx->rxqueue = g_malloc(jsx->rxmaxlen); + jsx->rxqueue[0] = 0x05; + jsx->rxqueue[1] = 0x00; + xfer->watcher = gaim_input_add(source, GAIM_INPUT_WRITE, + jabber_si_xfer_bytestreams_send_read_response_cb, + xfer); + jabber_si_xfer_bytestreams_send_read_response_cb(xfer, + source, GAIM_INPUT_WRITE); + jsx->rxqueue = NULL; + jsx->rxlen = 0; + return; + } + } + + g_free(jsx->rxqueue); + jsx->rxlen = 0; + jsx->rxmaxlen = 2; + jsx->rxqueue = g_malloc(jsx->rxmaxlen); + jsx->rxqueue[0] = 0x05; + jsx->rxqueue[1] = 0xFF; + xfer->watcher = gaim_input_add(source, GAIM_INPUT_WRITE, + jabber_si_xfer_bytestreams_send_read_response_cb, xfer); + jabber_si_xfer_bytestreams_send_read_response_cb(xfer, + source, GAIM_INPUT_WRITE); +} + +static void +jabber_si_xfer_bytestreams_send_connected_cb(gpointer data, gint source, + GaimInputCondition cond) +{ + GaimXfer *xfer = data; + int acceptfd; + + gaim_debug_info("jabber", "in jabber_si_xfer_bytestreams_send_connected_cb\n"); + + acceptfd = accept(source, NULL, 0); + if(acceptfd == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) + return; + else if(acceptfd == -1) { + gaim_debug_warning("jabber", "accept: %s\n", strerror(errno)); + return; + } + + gaim_input_remove(xfer->watcher); + close(source); + + xfer->watcher = gaim_input_add(acceptfd, GAIM_INPUT_READ, + jabber_si_xfer_bytestreams_send_read_cb, xfer); +} + +static void +jabber_si_xfer_bytestreams_listen_cb(int sock, gpointer data) +{ + GaimXfer *xfer = data; + JabberSIXfer *jsx; + JabberIq *iq; + xmlnode *query, *streamhost; + char *jid, *port; + + jsx = xfer->data; + jsx->listen_data = NULL; + + if (gaim_xfer_get_status(xfer) == GAIM_XFER_STATUS_CANCEL_LOCAL) { + gaim_xfer_unref(xfer); + return; + } + + gaim_xfer_unref(xfer); + + if (sock < 0) { + gaim_xfer_cancel_local(xfer); + return; + } + + iq = jabber_iq_new_query(jsx->js, JABBER_IQ_SET, + "http://jabber.org/protocol/bytestreams"); + xmlnode_set_attrib(iq->node, "to", xfer->who); + query = xmlnode_get_child(iq->node, "query"); + + xmlnode_set_attrib(query, "sid", jsx->stream_id); + + streamhost = xmlnode_new_child(query, "streamhost"); + jid = g_strdup_printf("%s@%s/%s", jsx->js->user->node, + jsx->js->user->domain, jsx->js->user->resource); + xmlnode_set_attrib(streamhost, "jid", jid); + g_free(jid); + + /* XXX: shouldn't we use the public IP or something? here */ + xmlnode_set_attrib(streamhost, "host", + gaim_network_get_my_ip(jsx->js->fd)); + xfer->local_port = gaim_network_get_port_from_fd(sock); + port = g_strdup_printf("%hu", xfer->local_port); + xmlnode_set_attrib(streamhost, "port", port); + g_free(port); + + xfer->watcher = gaim_input_add(sock, GAIM_INPUT_READ, + jabber_si_xfer_bytestreams_send_connected_cb, xfer); + + /* XXX: insert proxies here */ + + /* XXX: callback to find out which streamhost they used, or see if they + * screwed it up */ + jabber_iq_send(iq); + +} + +static void +jabber_si_xfer_bytestreams_send_init(GaimXfer *xfer) +{ + JabberSIXfer *jsx; + + gaim_xfer_ref(xfer); + + jsx = xfer->data; + jsx->listen_data = gaim_network_listen_range(0, 0, SOCK_STREAM, + jabber_si_xfer_bytestreams_listen_cb, xfer); + if (jsx->listen_data == NULL) { + gaim_xfer_unref(xfer); + /* XXX: couldn't open a port, we're fscked */ + gaim_xfer_cancel_local(xfer); + return; + } + +} + +static void jabber_si_xfer_send_method_cb(JabberStream *js, xmlnode *packet, + gpointer data) +{ + GaimXfer *xfer = data; + xmlnode *si, *feature, *x, *field, *value; + + if(!(si = xmlnode_get_child_with_namespace(packet, "si", "http://jabber.org/protocol/si"))) { + gaim_xfer_cancel_remote(xfer); + return; + } + + if(!(feature = xmlnode_get_child_with_namespace(si, "feature", "http://jabber.org/protocol/feature-neg"))) { + gaim_xfer_cancel_remote(xfer); + return; + } + + if(!(x = xmlnode_get_child_with_namespace(feature, "x", "jabber:x:data"))) { + gaim_xfer_cancel_remote(xfer); + return; + } + + for(field = xmlnode_get_child(x, "field"); field; field = xmlnode_get_next_twin(field)) { + const char *var = xmlnode_get_attrib(field, "var"); + + if(var && !strcmp(var, "stream-method")) { + if((value = xmlnode_get_child(field, "value"))) { + char *val = xmlnode_get_data(value); + if(val && !strcmp(val, "http://jabber.org/protocol/bytestreams")) { + jabber_si_xfer_bytestreams_send_init(xfer); + g_free(val); + return; + } + g_free(val); + } + } + } + gaim_xfer_cancel_remote(xfer); +} + +static void jabber_si_xfer_send_request(GaimXfer *xfer) +{ + JabberSIXfer *jsx = xfer->data; + JabberIq *iq; + xmlnode *si, *file, *feature, *x, *field, *option, *value; + char buf[32]; + + xfer->filename = g_path_get_basename(xfer->local_filename); + + iq = jabber_iq_new(jsx->js, JABBER_IQ_SET); + xmlnode_set_attrib(iq->node, "to", xfer->who); + si = xmlnode_new_child(iq->node, "si"); + xmlnode_set_namespace(si, "http://jabber.org/protocol/si"); + jsx->stream_id = jabber_get_next_id(jsx->js); + xmlnode_set_attrib(si, "id", jsx->stream_id); + xmlnode_set_attrib(si, "profile", + "http://jabber.org/protocol/si/profile/file-transfer"); + + file = xmlnode_new_child(si, "file"); + xmlnode_set_namespace(file, + "http://jabber.org/protocol/si/profile/file-transfer"); + xmlnode_set_attrib(file, "name", xfer->filename); + g_snprintf(buf, sizeof(buf), "%" G_GSIZE_FORMAT, xfer->size); + xmlnode_set_attrib(file, "size", buf); + /* maybe later we'll do hash and date attribs */ + + feature = xmlnode_new_child(si, "feature"); + xmlnode_set_namespace(feature, + "http://jabber.org/protocol/feature-neg"); + x = xmlnode_new_child(feature, "x"); + xmlnode_set_namespace(x, "jabber:x:data"); + xmlnode_set_attrib(x, "type", "form"); + field = xmlnode_new_child(x, "field"); + xmlnode_set_attrib(field, "var", "stream-method"); + xmlnode_set_attrib(field, "type", "list-single"); + option = xmlnode_new_child(field, "option"); + value = xmlnode_new_child(option, "value"); + xmlnode_insert_data(value, "http://jabber.org/protocol/bytestreams", + -1); + /* + option = xmlnode_new_child(field, "option"); + value = xmlnode_new_child(option, "value"); + xmlnode_insert_data(value, "http://jabber.org/protocol/ibb", -1); + */ + + jabber_iq_set_callback(iq, jabber_si_xfer_send_method_cb, xfer); + + jabber_iq_send(iq); +} + +static void jabber_si_xfer_free(GaimXfer *xfer) +{ + JabberSIXfer *jsx = xfer->data; + JabberStream *js = jsx->js; + + js->file_transfers = g_list_remove(js->file_transfers, xfer); + + if (jsx->connect_data != NULL) + gaim_proxy_connect_cancel(jsx->connect_data); + if (jsx->listen_data != NULL) + gaim_network_listen_cancel(jsx->listen_data); + + g_free(jsx->stream_id); + g_free(jsx->iq_id); + /* XXX: free other stuff */ + g_free(jsx->rxqueue); + g_free(jsx); + xfer->data = NULL; +} + +static void jabber_si_xfer_cancel_send(GaimXfer *xfer) +{ + jabber_si_xfer_free(xfer); + gaim_debug(GAIM_DEBUG_INFO, "jabber", "in jabber_si_xfer_cancel_send\n"); +} + + +static void jabber_si_xfer_request_denied(GaimXfer *xfer) +{ + jabber_si_xfer_free(xfer); + gaim_debug(GAIM_DEBUG_INFO, "jabber", "in jabber_si_xfer_request_denied\n"); +} + + +static void jabber_si_xfer_cancel_recv(GaimXfer *xfer) +{ + jabber_si_xfer_free(xfer); + gaim_debug(GAIM_DEBUG_INFO, "jabber", "in jabber_si_xfer_cancel_recv\n"); +} + + +static void jabber_si_xfer_end(GaimXfer *xfer) +{ + jabber_si_xfer_free(xfer); +} + + +static void jabber_si_xfer_send_disco_cb(JabberStream *js, const char *who, + JabberCapabilities capabilities, gpointer data) +{ + GaimXfer *xfer = data; + + if(capabilities & JABBER_CAP_SI_FILE_XFER) { + jabber_si_xfer_send_request(xfer); + } else { + char *msg = g_strdup_printf(_("Unable to send file to %s, user does not support file transfers"), who); + gaim_notify_error(js->gc, _("File Send Failed"), + _("File Send Failed"), msg); + g_free(msg); + } +} + +static void jabber_si_xfer_init(GaimXfer *xfer) +{ + JabberSIXfer *jsx = xfer->data; + JabberIq *iq; + if(gaim_xfer_get_type(xfer) == GAIM_XFER_SEND) { + JabberBuddy *jb; + JabberBuddyResource *jbr = NULL; + + jb = jabber_buddy_find(jsx->js, xfer->who, TRUE); + /* XXX */ + if(!jb) + return; + + /* XXX: for now, send to the first resource available */ + if(g_list_length(jb->resources) >= 1) { + char **who_v = g_strsplit(xfer->who, "/", 2); + char *who; + + jbr = jabber_buddy_find_resource(jb, NULL); + who = g_strdup_printf("%s/%s", who_v[0], jbr->name); + g_strfreev(who_v); + g_free(xfer->who); + xfer->who = who; + jabber_disco_info_do(jsx->js, who, + jabber_si_xfer_send_disco_cb, xfer); + } else { + return; /* XXX: ick */ + } + } else { + xmlnode *si, *feature, *x, *field, *value; + + iq = jabber_iq_new(jsx->js, JABBER_IQ_RESULT); + xmlnode_set_attrib(iq->node, "to", xfer->who); + if(jsx->iq_id) + jabber_iq_set_id(iq, jsx->iq_id); + + jsx->accepted = TRUE; + + si = xmlnode_new_child(iq->node, "si"); + xmlnode_set_namespace(si, "http://jabber.org/protocol/si"); + + feature = xmlnode_new_child(si, "feature"); + xmlnode_set_namespace(feature, "http://jabber.org/protocol/feature-neg"); + + x = xmlnode_new_child(feature, "x"); + xmlnode_set_namespace(x, "jabber:x:data"); + xmlnode_set_attrib(x, "type", "submit"); + + field = xmlnode_new_child(x, "field"); + xmlnode_set_attrib(field, "var", "stream-method"); + + value = xmlnode_new_child(field, "value"); + if(jsx->stream_method & STREAM_METHOD_BYTESTREAMS) + xmlnode_insert_data(value, "http://jabber.org/protocol/bytestreams", -1); + /* + else if(jsx->stream_method & STREAM_METHOD_IBB) + xmlnode_insert_data(value, "http://jabber.org/protocol/ibb", -1); + */ + + jabber_iq_send(iq); + } +} + +GaimXfer *jabber_si_new_xfer(GaimConnection *gc, const char *who) +{ + JabberStream *js; + + GaimXfer *xfer; + JabberSIXfer *jsx; + + js = gc->proto_data; + + xfer = gaim_xfer_new(gc->account, GAIM_XFER_SEND, who); + if (xfer) + { + xfer->data = jsx = g_new0(JabberSIXfer, 1); + jsx->js = js; + + gaim_xfer_set_init_fnc(xfer, jabber_si_xfer_init); + gaim_xfer_set_cancel_send_fnc(xfer, jabber_si_xfer_cancel_send); + gaim_xfer_set_end_fnc(xfer, jabber_si_xfer_end); + + js->file_transfers = g_list_append(js->file_transfers, xfer); + } + + return xfer; +} + +void jabber_si_xfer_send(GaimConnection *gc, const char *who, const char *file) +{ + JabberStream *js; + + GaimXfer *xfer; + + js = gc->proto_data; + + if(!gaim_find_buddy(gc->account, who) || !jabber_buddy_find(js, who, FALSE)) + return; + + xfer = jabber_si_new_xfer(gc, who); + + if (file) + gaim_xfer_request_accepted(xfer, file); + else + gaim_xfer_request(xfer); +} + +void jabber_si_parse(JabberStream *js, xmlnode *packet) +{ + JabberSIXfer *jsx; + GaimXfer *xfer; + xmlnode *si, *file, *feature, *x, *field, *option, *value; + const char *stream_id, *filename, *filesize_c, *profile, *from; + size_t filesize = 0; + + if(!(si = xmlnode_get_child(packet, "si"))) + return; + + if(!(profile = xmlnode_get_attrib(si, "profile")) || + strcmp(profile, "http://jabber.org/protocol/si/profile/file-transfer")) + return; + + if(!(stream_id = xmlnode_get_attrib(si, "id"))) + return; + + if(!(file = xmlnode_get_child(si, "file"))) + return; + + if(!(filename = xmlnode_get_attrib(file, "name"))) + return; + + if((filesize_c = xmlnode_get_attrib(file, "size"))) + filesize = atoi(filesize_c); + + if(!(feature = xmlnode_get_child(si, "feature"))) + return; + + if(!(x = xmlnode_get_child_with_namespace(feature, "x", "jabber:x:data"))) + return; + + if(!(from = xmlnode_get_attrib(packet, "from"))) + return; + + /* if they've already sent us this file transfer with the same damn id + * then we're gonna ignore it, until I think of something better to do + * with it */ + if((xfer = jabber_si_xfer_find(js, stream_id, from))) + return; + + jsx = g_new0(JabberSIXfer, 1); + + for(field = xmlnode_get_child(x, "field"); field; field = xmlnode_get_next_twin(field)) { + const char *var = xmlnode_get_attrib(field, "var"); + if(var && !strcmp(var, "stream-method")) { + for(option = xmlnode_get_child(field, "option"); option; + option = xmlnode_get_next_twin(option)) { + if((value = xmlnode_get_child(option, "value"))) { + char *val; + if((val = xmlnode_get_data(value))) { + if(!strcmp(val, "http://jabber.org/protocol/bytestreams")) { + jsx->stream_method |= STREAM_METHOD_BYTESTREAMS; + /* + } else if(!strcmp(val, "http://jabber.org/protocol/ibb")) { + jsx->stream_method |= STREAM_METHOD_IBB; + */ + } + g_free(val); + } + } + } + } + } + + if(jsx->stream_method == STREAM_METHOD_UNKNOWN) { + g_free(jsx); + return; + } + + jsx->js = js; + jsx->stream_id = g_strdup(stream_id); + jsx->iq_id = g_strdup(xmlnode_get_attrib(packet, "id")); + + xfer = gaim_xfer_new(js->gc->account, GAIM_XFER_RECEIVE, from); + if (xfer) + { + xfer->data = jsx; + + gaim_xfer_set_filename(xfer, filename); + if(filesize > 0) + gaim_xfer_set_size(xfer, filesize); + + gaim_xfer_set_init_fnc(xfer, jabber_si_xfer_init); + gaim_xfer_set_request_denied_fnc(xfer, jabber_si_xfer_request_denied); + gaim_xfer_set_cancel_recv_fnc(xfer, jabber_si_xfer_cancel_recv); + gaim_xfer_set_end_fnc(xfer, jabber_si_xfer_end); + + js->file_transfers = g_list_append(js->file_transfers, xfer); + + gaim_xfer_request(xfer); + } +} + + diff --git a/libpurple/protocols/jabber/si.h b/libpurple/protocols/jabber/si.h new file mode 100644 index 0000000000..724a4dfe84 --- /dev/null +++ b/libpurple/protocols/jabber/si.h @@ -0,0 +1,34 @@ +/** + * @file jutil.h utility functions + * + * gaim + * + * Copyright (C) 2003 Nathan Walp <faceprint@faceprint.com> + * + * 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 + */ +#ifndef _GAIM_JABBER_SI_H_ +#define _GAIM_JABBER_SI_H_ + +#include "ft.h" + +#include "jabber.h" + +void jabber_bytestreams_parse(JabberStream *js, xmlnode *packet); +void jabber_si_parse(JabberStream *js, xmlnode *packet); +GaimXfer *jabber_si_new_xfer(GaimConnection *gc, const char *who); +void jabber_si_xfer_send(GaimConnection *gc, const char *who, const char *file); + +#endif /* _GAIM_JABBER_SI_H_ */ diff --git a/libpurple/protocols/jabber/win32/posix.uname.c b/libpurple/protocols/jabber/win32/posix.uname.c new file mode 100644 index 0000000000..a5ecb128ed --- /dev/null +++ b/libpurple/protocols/jabber/win32/posix.uname.c @@ -0,0 +1,135 @@ +/* + posix.uname.c - version 1.1 + Copyright (C) 1999, 2000 + Earnie Boyd and assigns + + Fills the utsname structure with the appropriate values. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; either version 2.1, 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 PARTICUALR PURPOSE. See the + GNU Lesser 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. + */ + +/* + Send bug reports to Earnie Boyd <earnie_boyd@yahoo.com> + */ + +#include "utsname.h" +#include <string.h> +#include <stdio.h> + +/* ANONYMOUS unions and structs are used from the windows header definitions. + These need to be defined for them to work correctly with gcc2.95.2-mingw. */ +/*#define _ANONYMOUS_STRUCT*/ +/*#define _ANONYMOUS_UNION*/ +#include <windows.h> +#include <_mingw.h> + +int +uname( struct utsname *uts ) +{ + DWORD sLength; + OSVERSIONINFO OS_version; + SYSTEM_INFO System_Info; + +/* XXX Should these be in the global runtime */ + enum WinOS {Win95, Win98, WinNT, unknown}; + int MingwOS; + + memset( uts, 0, sizeof ( *uts ) ); + OS_version.dwOSVersionInfoSize = sizeof( OSVERSIONINFO ); + + GetVersionEx ( &OS_version ); + GetSystemInfo ( &System_Info ); + + strcpy( uts->sysname, "MINGW_" ); + switch( OS_version.dwPlatformId ) + { + case VER_PLATFORM_WIN32_NT: + strcat( uts->sysname, "WinNT" ); + MingwOS = WinNT; + break; + case VER_PLATFORM_WIN32_WINDOWS: + switch ( OS_version.dwMinorVersion ) + { + case 0: + strcat( uts->sysname, "Win95" ); + MingwOS = Win95; + break; + case 10: + strcat( uts->sysname, "Win98" ); + MingwOS = Win98; + break; + default: + strcat( uts->sysname, "Win??" ); + MingwOS = unknown; + break; + } + break; + default: + strcat( uts->sysname, "Win??" ); + MingwOS = unknown; + break; + } + + sprintf( uts->version, "%i", __MINGW32_MAJOR_VERSION ); + sprintf( uts->release, "%i", __MINGW32_MINOR_VERSION ); + + switch( System_Info.wProcessorArchitecture ) + { + case PROCESSOR_ARCHITECTURE_PPC: + strcpy( uts->machine, "ppc" ); + break; + case PROCESSOR_ARCHITECTURE_ALPHA: + strcpy( uts->machine, "alpha" ); + break; + case PROCESSOR_ARCHITECTURE_MIPS: + strcpy( uts->machine, "mips" ); + break; + case PROCESSOR_ARCHITECTURE_INTEL: + /* dwProcessorType is only valid in Win95 and Win98 + wProcessorLevel is only valid in WinNT */ + switch( MingwOS ) + { + case Win95: + case Win98: + switch( System_Info.dwProcessorType ) + { + case PROCESSOR_INTEL_386: + case PROCESSOR_INTEL_486: + case PROCESSOR_INTEL_PENTIUM: + sprintf( uts->machine, "i%ld", System_Info.dwProcessorType ); + break; + default: + strcpy( uts->machine, "i386" ); + break; + } + break; + case WinNT: + sprintf( uts->machine, "i%d86", System_Info.wProcessorLevel ); + break; + default: + strcpy( uts->machine, "unknown" ); + break; + } + break; + default: + strcpy( uts->machine, "unknown" ); + break; + } + + sLength = sizeof ( uts->nodename ) - 1; + GetComputerNameA( uts->nodename, &sLength ); + return 1; +} + diff --git a/libpurple/protocols/jabber/win32/utsname.h b/libpurple/protocols/jabber/win32/utsname.h new file mode 100644 index 0000000000..bbfa9a6d0c --- /dev/null +++ b/libpurple/protocols/jabber/win32/utsname.h @@ -0,0 +1,23 @@ +#ifndef _SYS_UTSNAME_H +#define _SYS_UTSNAME_H + +#ifdef __cplusplus +extern "C" { +#endif + +struct utsname +{ + char sysname[20]; + char nodename[20]; + char release[20]; + char version[20]; + char machine[20]; +}; + +int uname (struct utsname *); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/libpurple/protocols/jabber/xdata.c b/libpurple/protocols/jabber/xdata.c new file mode 100644 index 0000000000..0a8ff52252 --- /dev/null +++ b/libpurple/protocols/jabber/xdata.c @@ -0,0 +1,348 @@ +/* + * gaim - Jabber Protocol Plugin + * + * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com> + * + * 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 + * + */ +#include "internal.h" +#include "request.h" +#include "server.h" + +#include "xdata.h" + +typedef enum { + JABBER_X_DATA_IGNORE = 0, + JABBER_X_DATA_TEXT_SINGLE, + JABBER_X_DATA_TEXT_MULTI, + JABBER_X_DATA_LIST_SINGLE, + JABBER_X_DATA_LIST_MULTI, + JABBER_X_DATA_BOOLEAN, + JABBER_X_DATA_JID_SINGLE +} jabber_x_data_field_type; + +struct jabber_x_data_data { + GHashTable *fields; + GSList *values; + jabber_x_data_cb cb; + gpointer user_data; + JabberStream *js; +}; + +static void jabber_x_data_ok_cb(struct jabber_x_data_data *data, GaimRequestFields *fields) { + xmlnode *result = xmlnode_new("x"); + jabber_x_data_cb cb = data->cb; + gpointer user_data = data->user_data; + JabberStream *js = data->js; + GList *groups, *flds; + + xmlnode_set_namespace(result, "jabber:x:data"); + xmlnode_set_attrib(result, "type", "submit"); + + for(groups = gaim_request_fields_get_groups(fields); groups; groups = groups->next) { + for(flds = gaim_request_field_group_get_fields(groups->data); flds; flds = flds->next) { + xmlnode *fieldnode, *valuenode; + GaimRequestField *field = flds->data; + const char *id = gaim_request_field_get_id(field); + jabber_x_data_field_type type = GPOINTER_TO_INT(g_hash_table_lookup(data->fields, id)); + + switch(type) { + case JABBER_X_DATA_TEXT_SINGLE: + case JABBER_X_DATA_JID_SINGLE: + { + const char *value = gaim_request_field_string_get_value(field); + fieldnode = xmlnode_new_child(result, "field"); + xmlnode_set_attrib(fieldnode, "var", id); + valuenode = xmlnode_new_child(fieldnode, "value"); + if(value) + xmlnode_insert_data(valuenode, value, -1); + break; + } + case JABBER_X_DATA_TEXT_MULTI: + { + char **pieces, **p; + const char *value = gaim_request_field_string_get_value(field); + fieldnode = xmlnode_new_child(result, "field"); + xmlnode_set_attrib(fieldnode, "var", id); + + pieces = g_strsplit(value, "\n", -1); + for(p = pieces; *p != NULL; p++) { + valuenode = xmlnode_new_child(fieldnode, "value"); + xmlnode_insert_data(valuenode, *p, -1); + } + g_strfreev(pieces); + } + break; + case JABBER_X_DATA_LIST_SINGLE: + case JABBER_X_DATA_LIST_MULTI: + { + const GList *selected = gaim_request_field_list_get_selected(field); + char *value; + fieldnode = xmlnode_new_child(result, "field"); + xmlnode_set_attrib(fieldnode, "var", id); + + while(selected) { + value = gaim_request_field_list_get_data(field, selected->data); + valuenode = xmlnode_new_child(fieldnode, "value"); + if(value) + xmlnode_insert_data(valuenode, value, -1); + selected = selected->next; + } + } + break; + case JABBER_X_DATA_BOOLEAN: + fieldnode = xmlnode_new_child(result, "field"); + xmlnode_set_attrib(fieldnode, "var", id); + valuenode = xmlnode_new_child(fieldnode, "value"); + if(gaim_request_field_bool_get_value(field)) + xmlnode_insert_data(valuenode, "1", -1); + else + xmlnode_insert_data(valuenode, "0", -1); + break; + case JABBER_X_DATA_IGNORE: + break; + } + } + } + + g_hash_table_destroy(data->fields); + while(data->values) { + g_free(data->values->data); + data->values = g_slist_delete_link(data->values, data->values); + } + g_free(data); + + cb(js, result, user_data); +} + +static void jabber_x_data_cancel_cb(struct jabber_x_data_data *data, GaimRequestFields *fields) { + xmlnode *result = xmlnode_new("x"); + jabber_x_data_cb cb = data->cb; + gpointer user_data = data->user_data; + JabberStream *js = data->js; + g_hash_table_destroy(data->fields); + while(data->values) { + g_free(data->values->data); + data->values = g_slist_delete_link(data->values, data->values); + } + g_free(data); + + xmlnode_set_namespace(result, "jabber:x:data"); + xmlnode_set_attrib(result, "type", "cancel"); + + cb(js, result, user_data); +} + +void *jabber_x_data_request(JabberStream *js, xmlnode *packet, jabber_x_data_cb cb, gpointer user_data) +{ + void *handle; + xmlnode *fn, *x; + GaimRequestFields *fields; + GaimRequestFieldGroup *group; + GaimRequestField *field; + + char *title = NULL; + char *instructions = NULL; + + struct jabber_x_data_data *data = g_new0(struct jabber_x_data_data, 1); + + data->fields = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + data->user_data = user_data; + data->cb = cb; + data->js = js; + + fields = gaim_request_fields_new(); + group = gaim_request_field_group_new(NULL); + gaim_request_fields_add_group(fields, group); + + for(fn = xmlnode_get_child(packet, "field"); fn; fn = xmlnode_get_next_twin(fn)) { + xmlnode *valuenode; + const char *type = xmlnode_get_attrib(fn, "type"); + const char *label = xmlnode_get_attrib(fn, "label"); + const char *var = xmlnode_get_attrib(fn, "var"); + char *value = NULL; + + if(!type) + continue; + + if(!var && strcmp(type, "fixed")) + continue; + if(!label) + label = var; + + if((valuenode = xmlnode_get_child(fn, "value"))) + value = xmlnode_get_data(valuenode); + + + /* XXX: handle <required/> */ + + if(!strcmp(type, "text-private")) { + if((valuenode = xmlnode_get_child(fn, "value"))) + value = xmlnode_get_data(valuenode); + + field = gaim_request_field_string_new(var, label, + value ? value : "", FALSE); + gaim_request_field_string_set_masked(field, TRUE); + gaim_request_field_group_add_field(group, field); + + g_hash_table_replace(data->fields, g_strdup(var), GINT_TO_POINTER(JABBER_X_DATA_TEXT_SINGLE)); + + if(value) + g_free(value); + } else if(!strcmp(type, "text-multi") || !strcmp(type, "jid-multi")) { + GString *str = g_string_new(""); + + for(valuenode = xmlnode_get_child(fn, "value"); valuenode; + valuenode = xmlnode_get_next_twin(valuenode)) { + + if(!(value = xmlnode_get_data(valuenode))) + continue; + + g_string_append_printf(str, "%s\n", value); + g_free(value); + } + + field = gaim_request_field_string_new(var, label, + str->str, TRUE); + gaim_request_field_group_add_field(group, field); + + g_hash_table_replace(data->fields, g_strdup(var), GINT_TO_POINTER(JABBER_X_DATA_TEXT_MULTI)); + + g_string_free(str, TRUE); + } else if(!strcmp(type, "list-single") || !strcmp(type, "list-multi")) { + xmlnode *optnode; + GList *selected = NULL; + + field = gaim_request_field_list_new(var, label); + + if(!strcmp(type, "list-multi")) { + gaim_request_field_list_set_multi_select(field, TRUE); + g_hash_table_replace(data->fields, g_strdup(var), + GINT_TO_POINTER(JABBER_X_DATA_LIST_MULTI)); + } else { + g_hash_table_replace(data->fields, g_strdup(var), + GINT_TO_POINTER(JABBER_X_DATA_LIST_SINGLE)); + } + + for(valuenode = xmlnode_get_child(fn, "value"); valuenode; + valuenode = xmlnode_get_next_twin(valuenode)) { + selected = g_list_prepend(selected, xmlnode_get_data(valuenode)); + } + + for(optnode = xmlnode_get_child(fn, "option"); optnode; + optnode = xmlnode_get_next_twin(optnode)) { + const char *lbl; + + if(!(valuenode = xmlnode_get_child(optnode, "value"))) + continue; + + if(!(value = xmlnode_get_data(valuenode))) + continue; + + if(!(lbl = xmlnode_get_attrib(optnode, "label"))) + label = value; + + data->values = g_slist_prepend(data->values, value); + + gaim_request_field_list_add(field, lbl, value); + if(g_list_find_custom(selected, value, (GCompareFunc)strcmp)) + gaim_request_field_list_add_selected(field, lbl); + } + gaim_request_field_group_add_field(group, field); + + while(selected) { + g_free(selected->data); + selected = g_list_delete_link(selected, selected); + } + + } else if(!strcmp(type, "boolean")) { + gboolean def = FALSE; + + if((valuenode = xmlnode_get_child(fn, "value"))) + value = xmlnode_get_data(valuenode); + + if(value && (!g_ascii_strcasecmp(value, "yes") || + !g_ascii_strcasecmp(value, "true") || !g_ascii_strcasecmp(value, "1"))) + def = TRUE; + + field = gaim_request_field_bool_new(var, label, def); + gaim_request_field_group_add_field(group, field); + + g_hash_table_replace(data->fields, g_strdup(var), GINT_TO_POINTER(JABBER_X_DATA_BOOLEAN)); + + if(value) + g_free(value); + } else if(!strcmp(type, "fixed") && value) { + if((valuenode = xmlnode_get_child(fn, "value"))) + value = xmlnode_get_data(valuenode); + + field = gaim_request_field_label_new("", value); + gaim_request_field_group_add_field(group, field); + + if(value) + g_free(value); + } else if(!strcmp(type, "hidden")) { + if((valuenode = xmlnode_get_child(fn, "value"))) + value = xmlnode_get_data(valuenode); + + field = gaim_request_field_string_new(var, "", value ? value : "", + FALSE); + gaim_request_field_set_visible(field, FALSE); + gaim_request_field_group_add_field(group, field); + + g_hash_table_replace(data->fields, g_strdup(var), GINT_TO_POINTER(JABBER_X_DATA_TEXT_SINGLE)); + + if(value) + g_free(value); + } else { /* text-single, jid-single, and the default */ + if((valuenode = xmlnode_get_child(fn, "value"))) + value = xmlnode_get_data(valuenode); + + field = gaim_request_field_string_new(var, label, + value ? value : "", FALSE); + gaim_request_field_group_add_field(group, field); + + if(!strcmp(type, "jid-single")) { + gaim_request_field_set_type_hint(field, "screenname"); + g_hash_table_replace(data->fields, g_strdup(var), GINT_TO_POINTER(JABBER_X_DATA_JID_SINGLE)); + } else { + g_hash_table_replace(data->fields, g_strdup(var), GINT_TO_POINTER(JABBER_X_DATA_TEXT_SINGLE)); + } + + if(value) + g_free(value); + } + } + + if((x = xmlnode_get_child(packet, "title"))) + title = xmlnode_get_data(x); + + if((x = xmlnode_get_child(packet, "instructions"))) + instructions = xmlnode_get_data(x); + + handle = gaim_request_fields(js->gc, title, title, instructions, fields, + _("OK"), G_CALLBACK(jabber_x_data_ok_cb), + _("Cancel"), G_CALLBACK(jabber_x_data_cancel_cb), data); + + if(title) + g_free(title); + if(instructions) + g_free(instructions); + + return handle; +} + + diff --git a/libpurple/protocols/jabber/xdata.h b/libpurple/protocols/jabber/xdata.h new file mode 100644 index 0000000000..09a1fdbae3 --- /dev/null +++ b/libpurple/protocols/jabber/xdata.h @@ -0,0 +1,31 @@ +/** + * @file xdata.h utility functions + * + * gaim + * + * Copyright (C) 2003 Nathan Walp <faceprint@faceprint.com> + * + * 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 + */ +#ifndef _GAIM_JABBER_XDATA_H_ +#define _GAIM_JABBER_XDATA_H_ + +#include "jabber.h" +#include "xmlnode.h" + +typedef void (*jabber_x_data_cb)(JabberStream *js, xmlnode *result, gpointer user_data); +void *jabber_x_data_request(JabberStream *js, xmlnode *packet, jabber_x_data_cb cb, gpointer user_data); + +#endif /* _GAIM_JABBER_XDATA_H_ */ |