# HG changeset patch # User Jeffrey Connelly # Date 1183605214 0 # Node ID 3c613fec5617d3300220066dbb622e8b30620463 # Parent 3aed9c8c6af73f23974e8e1ced29308424a487a0 Set status to current status when signing on. This makes the "Sign in as hidden" option no longer needed. diff -r 3aed9c8c6af7 -r 3c613fec5617 libpurple/protocols/myspace/CHANGES --- a/libpurple/protocols/myspace/CHANGES Thu Jul 05 00:29:25 2007 +0000 +++ b/libpurple/protocols/myspace/CHANGES Thu Jul 05 03:13:34 2007 +0000 @@ -3,6 +3,8 @@ (thanks to Scott Ellis, developing a MySpaceIM plugin for Miranda IM, for finding the idle status code.) * Time out if no data from server within a certain amount of time (keep alives). +* Remove "Sign on as hidden" option, and always set status to current status + when signing on. 2007-07-03 Jeff Connelly - 0.10 * On incoming instant messages, add support for: diff -r 3aed9c8c6af7 -r 3c613fec5617 libpurple/protocols/myspace/myspace.c --- a/libpurple/protocols/myspace/myspace.c Thu Jul 05 00:29:25 2007 +0000 +++ b/libpurple/protocols/myspace/myspace.c Thu Jul 05 03:13:34 2007 +0000 @@ -1,3087 +1,3081 @@ -/* MySpaceIM Protocol Plugin - * - * \author Jeff Connelly - * - * Copyright (C) 2007, Jeff Connelly - * - * Based on Purple's "C Plugin HOWTO" hello world example. - * - * Code also drawn from mockprpl: - * http://snarfed.org/space/purple+mock+protocol+plugin - * Copyright (C) 2004-2007, Ryan Barrett - * - * and some constructs also based on existing Purple plugins, which are: - * Copyright (C) 2003, Robbert Haarman - * Copyright (C) 2003, Ethan Blanton - * Copyright (C) 2000-2003, Rob Flynn - * Copyright (C) 1998-1999, Mark Spencer - * - * 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 - */ - -#define PURPLE_PLUGIN - -#include "message.h" -#include "persist.h" -#include "myspace.h" - -/** - * Load the plugin. - */ -gboolean -msim_load(PurplePlugin *plugin) -{ - /* If compiled to use RC4 from libpurple, check if it is really there. */ - if (!purple_ciphers_find_cipher("rc4")) - { - purple_debug_error("msim", "rc4 not in libpurple, but it is required - not loading MySpaceIM plugin!\n"); - purple_notify_error(plugin, _("Missing Cipher"), - _("The RC4 cipher could not be found"), - _("Upgrade " - "to a libpurple with RC4 support (>= 2.0.1). MySpaceIM " - "plugin will not be loaded.")); - return FALSE; - } - return TRUE; -} - -/** - * Get possible user status types. Based on mockprpl. - * - * @return GList of status types. - */ -GList * -msim_status_types(PurpleAccount *acct) -{ - GList *types; - PurpleStatusType *status; - - purple_debug_info("myspace", "returning status types\n"); - - types = NULL; - - status = purple_status_type_new_full(PURPLE_STATUS_AVAILABLE, NULL, NULL, FALSE, TRUE, FALSE); - types = g_list_append(types, status); - - status = purple_status_type_new_full(PURPLE_STATUS_AWAY, NULL, NULL, FALSE, TRUE, FALSE); - types = g_list_append(types, status); - - status = purple_status_type_new_full(PURPLE_STATUS_OFFLINE, NULL, NULL, FALSE, TRUE, FALSE); - types = g_list_append(types, status); - - status = purple_status_type_new_full(PURPLE_STATUS_INVISIBLE, NULL, NULL, FALSE, TRUE, FALSE); - types = g_list_append(types, status); - - return types; -} - -/** - * Return the icon name for a buddy and account. - * - * @param acct The account to find the icon for, or NULL for protocol icon. - * @param buddy The buddy to find the icon for, or NULL for the account icon. - * - * @return The base icon name string. - */ -const gchar * -msim_list_icon(PurpleAccount *acct, PurpleBuddy *buddy) -{ - /* Use a MySpace icon submitted by hbons at - * http://developer.pidgin.im/wiki/MySpaceIM. */ - return "myspace"; -} - -/* Replacement codes to be replaced with associated replacement text, - * used for protocol message escaping / unescaping. */ -static gchar* msim_replacement_code[] = { "/1", "/2", NULL }; -static gchar* msim_replacement_text[] = { "/", "\\", NULL }; - -/** - * Unescape or escape a protocol message. - * - * @param msg The message to be unescaped or escaped. WILL BE FREED. - * @param escape TRUE to escape, FALSE to unescape. - * - * @return The unescaped or escaped message. Caller must g_free(). - */ -gchar * -msim_unescape_or_escape(gchar *msg, gboolean escape) -{ - gchar *tmp, *code, *text; - guint i; - - /* Replace each code in msim_replacement_code with - * corresponding entry in msim_replacement_text. */ - for (i = 0; (code = msim_replacement_code[i]) - && (text = msim_replacement_text[i]); ++i) - { - if (escape) - { - tmp = str_replace(msg, text, code); - } - else - { - tmp = str_replace(msg, code, text); - } - g_free(msg); - msg = tmp; - } - - return msg; -} - -/** - * Escape a protocol message. - * - * @return The escaped message. Caller must g_free(). - */ -gchar * -msim_escape(const gchar *msg) -{ - return msim_unescape_or_escape(g_strdup(msg), TRUE); -} - -gchar * -msim_unescape(const gchar *msg) -{ - return msim_unescape_or_escape(g_strdup(msg), FALSE); -} - -/** - * Replace 'old' with 'new' in 'str'. - * - * @param str The original string. - * @param old The substring of 'str' to replace. - * @param new The replacement for 'old' within 'str'. - * - * @return A _new_ string, based on 'str', with 'old' replaced - * by 'new'. Must be g_free()'d by caller. - * - * This string replace method is based on - * http://mail.gnome.org/archives/gtk-app-devel-list/2000-July/msg00201.html - * - */ -gchar * -str_replace(const gchar *str, const gchar *old, const gchar *new) -{ - gchar **items; - gchar *ret; - - items = g_strsplit(str, old, -1); - ret = g_strjoinv(new, items); - g_free(items); - return ret; -} - -#ifdef MSIM_DEBUG_MSG -void -print_hash_item(gpointer key, gpointer value, gpointer user_data) -{ - purple_debug_info("msim", "%s=%s\n", (gchar *)key, (gchar *)value); -} -#endif - -/** - * Send raw data (given as a NUL-terminated string) to the server. - * - * @param session - * @param msg The raw data to send, in a NUL-terminated string. - * - * @return TRUE if succeeded, FALSE if not. - * - */ -gboolean -msim_send_raw(MsimSession *session, const gchar *msg) -{ - purple_debug_info("msim", "msim_send_raw: writing <%s>\n", msg); - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - return msim_send_really_raw(session->gc, msg, strlen(msg)) == - strlen(msg); -} - -/** Send raw data to the server, possibly with embedded NULs. - * - * Used in prpl_info struct, so that plugins can have the most possible - * control of what is sent over the connection. Inside this prpl, - * msim_send_raw() is used, since it sends NUL-terminated strings (easier). - * - * @param gc PurpleConnection - * @param buf Buffer to send - * @param total_bytes Size of buffer to send - * - * @return Bytes successfully sent, or -1 on error. - */ -int -msim_send_really_raw(PurpleConnection *gc, const char *buf, int total_bytes) -{ - int total_bytes_sent; - MsimSession *session; - - g_return_val_if_fail(gc != NULL, -1); - g_return_val_if_fail(buf != NULL, -1); - g_return_val_if_fail(total_bytes >= 0, -1); - - session = (MsimSession *)(gc->proto_data); - - g_return_val_if_fail(MSIM_SESSION_VALID(session), -1); - - /* Loop until all data is sent, or a failure occurs. */ - total_bytes_sent = 0; - do - { - int bytes_sent; - - bytes_sent = send(session->fd, buf + total_bytes_sent, - total_bytes - total_bytes_sent, 0); - - if (bytes_sent < 0) - { - purple_debug_info("msim", "msim_send_raw(%s): send() failed: %s\n", - buf, g_strerror(errno)); - return total_bytes_sent; - } - total_bytes_sent += bytes_sent; - - } while(total_bytes_sent < total_bytes); - - return total_bytes_sent; -} - - -/** - * Start logging in to the MSIM servers. - * - * @param acct Account information to use to login. - */ -void -msim_login(PurpleAccount *acct) -{ - PurpleConnection *gc; - const gchar *host; - int port; - - g_return_if_fail(acct != NULL); - g_return_if_fail(acct->username != NULL); - - purple_debug_info("myspace", "logging in %s\n", acct->username); - - gc = purple_account_get_connection(acct); - gc->proto_data = msim_session_new(acct); - - /* Passwords are limited in length. */ - if (strlen(acct->password) > MSIM_MAX_PASSWORD_LENGTH) - { - gchar *str; - - str = g_strdup_printf( - _("Sorry, passwords over %d characters in length (yours is " - "%d) are not supported by the MySpaceIM plugin."), - MSIM_MAX_PASSWORD_LENGTH, - (int)strlen(acct->password)); - - /* Notify an error message also, because this is important! */ - purple_notify_error(acct, g_strdup(_("MySpaceIM Error")), str, NULL); - - purple_connection_error(gc, str); - - g_free(str); - } - - /* 1. connect to server */ - purple_connection_update_progress(gc, _("Connecting"), - 0, /* which connection step this is */ - 4); /* total number of steps */ - - host = purple_account_get_string(acct, "server", MSIM_SERVER); - port = purple_account_get_int(acct, "port", MSIM_PORT); - - /* From purple.sf.net/api: - * """Note that this function name can be misleading--although it is called - * "proxy connect," it is used for establishing any outgoing TCP connection, - * whether through a proxy or not.""" */ - - /* Calls msim_connect_cb when connected. */ - if (purple_proxy_connect(gc, acct, host, port, msim_connect_cb, gc) == NULL) - { - /* TODO: try other ports if in auto mode, then save - * working port and try that first next time. */ - purple_connection_error(gc, _("Couldn't create socket")); - return; - } -} - -/** - * Process a login challenge, sending a response. - * - * @param session - * @param msg Login challenge message. - * - * @return TRUE if successful, FALSE if not - */ -gboolean -msim_login_challenge(MsimSession *session, MsimMessage *msg) -{ - PurpleAccount *account; - const gchar *response; - guint response_len; - gchar *nc; - gsize nc_len; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - g_return_val_if_fail(msim_msg_get_binary(msg, "nc", &nc, &nc_len), FALSE); - - account = session->account; - - g_return_val_if_fail(account != NULL, FALSE); - - purple_connection_update_progress(session->gc, _("Reading challenge"), 1, 4); - - purple_debug_info("msim", "nc is %d bytes, decoded\n", nc_len); - - if (nc_len != 0x40) - { - purple_debug_info("msim", "bad nc length: %x != 0x40\n", nc_len); - purple_connection_error(session->gc, _("Unexpected challenge length from server")); - return FALSE; - } - - purple_connection_update_progress(session->gc, _("Logging in"), 2, 4); - - response = msim_compute_login_response(nc, account->username, account->password, &response_len); - - g_free(nc); - - return msim_send(session, - "login2", MSIM_TYPE_INTEGER, MSIM_AUTH_ALGORITHM, - /* This is actually user's email address. */ - "username", MSIM_TYPE_STRING, g_strdup(account->username), - /* GString and gchar * response will be freed in msim_msg_free() in msim_send(). */ - "response", MSIM_TYPE_BINARY, g_string_new_len(response, response_len), - "clientver", MSIM_TYPE_INTEGER, MSIM_CLIENT_VERSION, - "reconn", MSIM_TYPE_INTEGER, 0, - "status", MSIM_TYPE_INTEGER, 100, - "id", MSIM_TYPE_INTEGER, 1, - NULL); -} - -/** - * Compute the base64'd login challenge response based on username, password, nonce, and IPs. - * - * @param nonce The base64 encoded nonce ('nc') field from the server. - * @param email User's email address (used as login name). - * @param password User's cleartext password. - * @param response_len Will be written with response length. - * - * @return Binary login challenge response, ready to send to the server. - * Must be g_free()'d when finished. NULL if error. - */ -const gchar * -msim_compute_login_response(const gchar nonce[2 * NONCE_SIZE], - const gchar *email, const gchar *password, guint *response_len) -{ - PurpleCipherContext *key_context; - PurpleCipher *sha1; - PurpleCipherContext *rc4; - - guchar hash_pw[HASH_SIZE]; - guchar key[HASH_SIZE]; - gchar *password_utf16le; - guchar *data; - guchar *data_out; - size_t data_len, data_out_len; - gsize conv_bytes_read, conv_bytes_written; - GError *conv_error; -#ifdef MSIM_DEBUG_LOGIN_CHALLENGE - int i; -#endif - - g_return_val_if_fail(nonce != NULL, NULL); - g_return_val_if_fail(email != NULL, NULL); - g_return_val_if_fail(password != NULL, NULL); - g_return_val_if_fail(response_len != NULL, NULL); - - /* Convert ASCII password to UTF16 little endian */ - purple_debug_info("msim", "converting password to UTF-16LE\n"); - conv_error = NULL; - password_utf16le = g_convert(password, -1, "UTF-16LE", "UTF-8", - &conv_bytes_read, &conv_bytes_written, &conv_error); - - g_return_val_if_fail(conv_bytes_read == strlen(password), NULL); - - if (conv_error != NULL) - { - purple_debug_error("msim", - "g_convert password UTF8->UTF16LE failed: %s", - conv_error->message); - g_error_free(conv_error); - return NULL; - } - - /* Compute password hash */ - purple_cipher_digest_region("sha1", (guchar *)password_utf16le, - conv_bytes_written, sizeof(hash_pw), hash_pw, NULL); - g_free(password_utf16le); - -#ifdef MSIM_DEBUG_LOGIN_CHALLENGE - purple_debug_info("msim", "pwhash = "); - for (i = 0; i < sizeof(hash_pw); i++) - purple_debug_info("msim", "%.2x ", hash_pw[i]); - purple_debug_info("msim", "\n"); -#endif - - /* key = sha1(sha1(pw) + nonce2) */ - sha1 = purple_ciphers_find_cipher("sha1"); - key_context = purple_cipher_context_new(sha1, NULL); - purple_cipher_context_append(key_context, hash_pw, HASH_SIZE); - purple_cipher_context_append(key_context, (guchar *)(nonce + NONCE_SIZE), NONCE_SIZE); - purple_cipher_context_digest(key_context, sizeof(key), key, NULL); - -#ifdef MSIM_DEBUG_LOGIN_CHALLENGE - purple_debug_info("msim", "key = "); - for (i = 0; i < sizeof(key); i++) - { - purple_debug_info("msim", "%.2x ", key[i]); - } - purple_debug_info("msim", "\n"); -#endif - - rc4 = purple_cipher_context_new_by_name("rc4", NULL); - - /* Note: 'key' variable is 0x14 bytes (from SHA-1 hash), - * but only first 0x10 used for the RC4 key. */ - purple_cipher_context_set_option(rc4, "key_len", (gpointer)0x10); - purple_cipher_context_set_key(rc4, key); - - /* TODO: obtain IPs of network interfaces */ - - /* rc4 encrypt: - * nonce1+email+IP list */ - - data_len = NONCE_SIZE + strlen(email) + MSIM_LOGIN_IP_LIST_LEN; - data = g_new0(guchar, data_len); - memcpy(data, nonce, NONCE_SIZE); - memcpy(data + NONCE_SIZE, email, strlen(email)); - memcpy(data + NONCE_SIZE + strlen(email), MSIM_LOGIN_IP_LIST, MSIM_LOGIN_IP_LIST_LEN); - - data_out = g_new0(guchar, data_len); - - purple_cipher_context_encrypt(rc4, (const guchar *)data, - data_len, data_out, &data_out_len); - purple_cipher_context_destroy(rc4); - - g_assert(data_out_len == data_len); - -#ifdef MSIM_DEBUG_LOGIN_CHALLENGE - purple_debug_info("msim", "response=<%s>\n", data_out); -#endif - - *response_len = data_out_len; - - return (const gchar *)data_out; -} - -/** - * Schedule an IM to be sent once the user ID is looked up. - * - * @param gc Connection. - * @param who A user id, email, or username to send the message to. - * @param message Instant message text to send. - * @param flags Flags. - * - * @return 1 if successful or postponed, -1 if failed - * - * Allows sending to a user by username, email address, or userid. If - * a username or email address is given, the userid must be looked up. - * This function does that by calling msim_postprocess_outgoing(). - */ -int -msim_send_im(PurpleConnection *gc, const gchar *who, const gchar *message, - PurpleMessageFlags flags) -{ - MsimSession *session; - - g_return_val_if_fail(gc != NULL, -1); - g_return_val_if_fail(who != NULL, -1); - g_return_val_if_fail(message != NULL, -1); - - /* 'flags' has many options, not used here. */ - - session = (MsimSession *)gc->proto_data; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), -1); - - if (msim_send_bm(session, who, message, MSIM_BM_INSTANT)) - { - /* Return 1 to have Purple show this IM as being sent, 0 to not. I always - * return 1 even if the message could not be sent, since I don't know if - * it has failed yet--because the IM is only sent after the userid is - * retrieved from the server (which happens after this function returns). - */ - /* TODO: maybe if message is delayed, don't echo to conv window, - * but do echo it to conv window manually once it is actually - * sent? Would be complicated. */ - return 1; - } else { - return -1; - } - /* - * In MySpace, you login with your email address, but don't talk to other - * users using their email address. So there is currently an asymmetry in the - * IM windows when using this plugin: - * - * you@example.com: hello - * some_other_user: what's going on? - * you@example.com: just coding a prpl - * - * TODO: Make the sent IM's appear as from the user's username, instead of - * their email address. Purple uses the login (in MSIM, the email)--change this. - */ -} - -/** Send a buddy message of a given type. - * - * @param session - * @param who Username to send message to. - * @param text Message text to send. Not freed; will be copied. - * @param type A MSIM_BM_* constant. - * - * @return TRUE if success, FALSE if fail. - * - * Buddy messages ('bm') include instant messages, action messages, status messages, etc. - * - */ -gboolean -msim_send_bm(MsimSession *session, const gchar *who, const gchar *text, - int type) -{ - gboolean rc; - MsimMessage *msg; - const gchar *from_username; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(who != NULL, FALSE); - g_return_val_if_fail(text != NULL, FALSE); - - from_username = session->account->username; - - purple_debug_info("msim", "sending %d message from %s to %s: %s\n", - type, from_username, who, text); - - msg = msim_msg_new(TRUE, - "bm", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(type), - "sesskey", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(session->sesskey), - /* 't' will be inserted here */ - "cv", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(MSIM_CLIENT_VERSION), - "msg", MSIM_TYPE_STRING, g_strdup(text), - NULL); - - rc = msim_postprocess_outgoing(session, msg, who, "t", "cv"); - - msim_msg_free(msg); - - return rc; -} - - -#ifdef MSIM_FONT_SIZE_WORKS -/** Convert a msim markup font height to points. */ -static guint -msim_font_height_to_point(guint height) -{ - /* See also: libpurple/protocols/bonjour/jabber.c - * _font_size_ichat_to_purple */ - switch (height) - { - case 11: return 8; - case 12: return 9; - case 13: return 10; - case 14: - case 15: return 11; - case 16: return 12; - case 17: - case 18: return 13; - case 19: return 14; - case 20: return 15; - case 21: return 16; - default: return 12; - } -} -#endif - -/** Convert the msim markup (font) tag into HTML. */ -static void -msim_markup_f_to_html(xmlnode *root, gchar **begin, gchar **end) -{ - const gchar *face, *height_str, *decor_str; - GString *gs_end, *gs_begin; - guint decor, height; - - face = xmlnode_get_attrib(root, "f"); - height_str = xmlnode_get_attrib(root, "h"); - decor_str = xmlnode_get_attrib(root, "s"); - - if (height_str) - height = atol(height_str); - else - height = 12; - - if (decor_str) - decor = atol(decor_str); - else - decor = 0; - - gs_begin = g_string_new(""); -#ifdef MSIM_FONT_SIZE_WORKS - /* TODO: get font size working */ - if (!face) - g_string_printf(gs_begin, "", - msim_font_height_to_point(height)); - else - g_string_printf(gs_begin, "", face, - msim_font_height_to_point(height)); -#else - if (face) - { - g_string_printf(gs_begin, "", face); - } else { - g_string_printf(gs_begin, ""); - } -#endif - - - /* No support for font-size CSS? */ - /* g_string_printf(gs_begin, "", face, - msim_font_height_to_point(height)); */ - - gs_end = g_string_new(""); - - if (decor & MSIM_TEXT_BOLD) - { - g_string_append(gs_begin, ""); - g_string_prepend(gs_end, ""); - } - - if (decor & MSIM_TEXT_ITALICS) - { - g_string_append(gs_begin, ""); - g_string_append(gs_end, ""); - } - - if (decor & MSIM_TEXT_UNDERLINE) - { - g_string_append(gs_begin, ""); - g_string_append(gs_end, ""); - } - - - *begin = gs_begin->str; - *end = gs_end->str; -} - -/** Convert a msim markup color to a color suitable for libpurple. - * - * @param msim Either a color name, or an rgb(x,y,z) code. - * - * @return A new string, either a color name or #rrggbb code. Must g_free(). - */ -static char * -msim_color_to_purple(const char *msim) -{ - guint red, green, blue; - - if (!msim) - return g_strdup("black"); - - if (sscanf(msim, "rgb(%d,%d,%d)", &red, &green, &blue) != 3) - { - /* Color name. */ - return g_strdup(msim); - } - /* TODO: rgba (alpha). */ - - return g_strdup_printf("#%.2x%.2x%.2x", red, green, blue); -} - -/** Convert the msim markup

(paragraph) tag into HTML. */ -static void -msim_markup_p_to_html(xmlnode *root, gchar **begin, gchar **end) -{ - /* Just pass through unchanged. - * - * Note: attributes currently aren't passed, if there are any. */ - *begin = g_strdup("

"); - *end = g_strdup("

"); -} - -/** Convert the msim markup tag (text color) into HTML. TODO: Test */ -static void -msim_markup_c_to_html(xmlnode *root, gchar **begin, gchar **end) -{ - const gchar *color; - gchar *purple_color; - - color = xmlnode_get_attrib(root, "v"); - if (!color) - { - purple_debug_info("msim", "msim_markup_c_to_html: tag w/o v attr"); - *begin = g_strdup(""); - *end = g_strdup(""); - /* TODO: log as unrecognized */ - return; - } - - purple_color = msim_color_to_purple(color); - - *begin = g_strdup_printf("", purple_color); - - g_free(purple_color); - - /* *begin = g_strdup_printf("", color); */ - *end = g_strdup(""); -} - -/** Convert the msim markup tag (background color) into HTML. TODO: Test */ -static void -msim_markup_b_to_html(xmlnode *root, gchar **begin, gchar **end) -{ - const gchar *color; - gchar *purple_color; - - color = xmlnode_get_attrib(root, "v"); - if (!color) - { - *begin = g_strdup(""); - *end = g_strdup(""); - purple_debug_info("msim", "msim_markup_b_to_html: w/o v attr"); - /* TODO: log as unrecognized. */ - return; - } - - purple_color = msim_color_to_purple(color); - - /* TODO: find out how to set background color. */ - *begin = g_strdup_printf("", - purple_color); - g_free(purple_color); - - *end = g_strdup("

"); -} - -/** Convert the msim markup tag (emoticon image) into HTML. TODO: Test */ -static void -msim_markup_i_to_html(xmlnode *root, gchar **begin, gchar **end) -{ - const gchar *name; - - name = xmlnode_get_attrib(root, "n"); - if (!name) - { - purple_debug_info("msim", "msim_markup_i_to_html: w/o n"); - *begin = g_strdup(""); - *end = g_strdup(""); - /* TODO: log as unrecognized */ - return; - } - - /* TODO: Support these emoticons: - * - * bigsmile growl mad scared tongue devil happy messed sidefrown upset - * frazzled heart nerd sinister wink geek laugh oops smirk worried - * googles mohawk pirate straight kiss - */ - - *begin = g_strdup_printf("", name); - *end = g_strdup("

"); -} - - -/** Convert an xmlnode of msim markup to an HTML string. - * @return An HTML string. Caller frees. - */ -static gchar * -msim_markup_xmlnode_to_html(xmlnode *root) -{ - xmlnode *node; - gchar *begin, *inner, *end; - gchar *final; - - if (!root || !root->name) - return g_strdup(""); - - purple_debug_info("msim", "msim_markup_xmlnode_to_html: got root=%s\n", - root->name); - - begin = inner = end = NULL; - - if (!strcmp(root->name, "f")) - { - msim_markup_f_to_html(root, &begin, &end); - } else if (!strcmp(root->name, "p")) { - msim_markup_p_to_html(root, &begin, &end); - } else if (!strcmp(root->name, "c")) { - msim_markup_c_to_html(root, &begin, &end); - } else if (!strcmp(root->name, "b")) { - msim_markup_b_to_html(root, &begin, &end); - } else if (!strcmp(root->name, "i")) { - msim_markup_i_to_html(root, &begin, &end); - } else { - purple_debug_info("msim", "msim_markup_xmlnode_to_html: " - "unknown tag name=%s, ignoring", root->name); - begin = g_strdup(""); - end = g_strdup(""); - } - - /* Loop over all child nodes. */ - for (node = root->child; node != NULL; node = node->next) - { - switch (node->type) - { - case XMLNODE_TYPE_ATTRIB: - /* Attributes handled above. */ - break; - - case XMLNODE_TYPE_TAG: - /* A tag or tag with attributes. Recursively descend. */ - inner = msim_markup_xmlnode_to_html(node); - g_return_val_if_fail(inner != NULL, NULL); - - purple_debug_info("msim", " ** node name=%s\n", node->name); - break; - - case XMLNODE_TYPE_DATA: - /* Literal text. */ - inner = g_new0(char, node->data_sz + 1); - strncpy(inner, node->data, node->data_sz); - inner[node->data_sz + 1] = 0; - - purple_debug_info("msim", " ** node data=%s\n", inner); - break; - - default: - purple_debug_info("msim", - "msim_markup_xmlnode_to_html: strange node\n"); - inner = g_strdup(""); - } - } - - final = g_strconcat(begin, inner, end, NULL); - g_free(begin); - g_free(inner); - g_free(end); - - purple_debug_info("msim", "msim_markup_xmlnode_to_gtkhtml: RETURNING %s\n", - final); - - return final; -} - -/** Convert MySpaceIM markup to Purple (HTML) markup. - * - * @return Purple markup string, must be g_free()'d. */ -gchar * -msim_markup_to_html(const gchar *raw) -{ - xmlnode *root; - gchar *str; - - root = xmlnode_from_str(raw, -1); - if (!root) - { - purple_debug_info("msim", "msim_markup_to_html: couldn't parse " - "%s as XML, returning raw\n", raw); - return g_strdup(raw); - } - - str = msim_markup_xmlnode_to_html(root); - purple_debug_info("msim", "msim_markup_to_html: returning %s\n", str); - - xmlnode_free(root); - - return g_strdup(str); -} - -/** - * Handle an incoming instant message. - * - * @param session The session - * @param msg Message from the server, containing 'f' (userid from) and 'msg'. - * Should also contain username in _username from preprocessing. - * - * @return TRUE if successful. - */ -gboolean -msim_incoming_im(MsimSession *session, MsimMessage *msg) -{ - gchar *username, *msg_msim_markup, *msg_purple_markup; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - username = msim_msg_get_string(msg, "_username"); - g_return_val_if_fail(username != NULL, FALSE); - - msg_msim_markup = msim_msg_get_string(msg, "msg"); - g_return_val_if_fail(msg_msim_markup != NULL, FALSE); - - msg_purple_markup = msim_markup_to_html(msg_msim_markup); - g_free(msg_msim_markup); - - serv_got_im(session->gc, username, msg_purple_markup, - PURPLE_MESSAGE_RECV, time(NULL)); - - g_free(username); - g_free(msg_purple_markup); - - return TRUE; -} - -/** - * Process unrecognized information. - * - * @param session - * @param msg An MsimMessage that was unrecognized, or NULL. - * @param note Information on what was unrecognized, or NULL. - */ -void -msim_unrecognized(MsimSession *session, MsimMessage *msg, gchar *note) -{ - /* TODO: Some more context, outwardly equivalent to a backtrace, - * for helping figure out what this msg is for. What was going on? - * But not too much information so that a user - * posting this dump reveals confidential information. - */ - - /* TODO: dump unknown msgs to file, so user can send them to me - * if they wish, to help add support for new messages (inspired - * by Alexandr Shutko, who maintains OSCAR protocol documentation). */ - - purple_debug_info("msim", "Unrecognized data on account for %s\n", - session->account->username); - if (note) - { - purple_debug_info("msim", "(Note: %s)\n", note); - } - - if (msg) - { - msim_msg_dump("Unrecognized message dump: %s\n", msg); - } -} - -/** - * Handle an incoming action message. - * - * @param session - * @param msg - * - * @return TRUE if successful. - * - * UNTESTED - */ -gboolean -msim_incoming_action(MsimSession *session, MsimMessage *msg) -{ - gchar *msg_text, *username; - gboolean rc; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - msg_text = msim_msg_get_string(msg, "msg"); - g_return_val_if_fail(msg_text != NULL, FALSE); - - username = msim_msg_get_string(msg, "_username"); - g_return_val_if_fail(username != NULL, FALSE); - - purple_debug_info("msim", "msim_incoming_action: action <%s> from <%d>\n", - msg_text, username); - - if (strcmp(msg_text, "%typing%") == 0) - { - /* TODO: find out if msim repeatedly sends typing messages, so we can - * give it a timeout. Right now, there does seem to be an inordinately - * amount of time between typing stopped-typing notifications. */ - serv_got_typing(session->gc, username, 0, PURPLE_TYPING); - rc = TRUE; - } else if (strcmp(msg_text, "%stoptyping%") == 0) { - serv_got_typing_stopped(session->gc, username); - rc = TRUE; - } else { - msim_unrecognized(session, msg, - "got to msim_incoming_action but unrecognized value for 'msg'"); - rc = FALSE; - } - - g_free(msg_text); - g_free(username); - - return rc; -} - -/** - * Handle when our user starts or stops typing to another user. - * - * @param gc - * @param name The buddy name to which our user is typing to - * @param state PURPLE_TYPING, PURPLE_TYPED, PURPLE_NOT_TYPING - * - * @return 0 - */ -unsigned int -msim_send_typing(PurpleConnection *gc, const gchar *name, - PurpleTypingState state) -{ - const gchar *typing_str; - MsimSession *session; - - g_return_val_if_fail(gc != NULL, 0); - g_return_val_if_fail(name != NULL, 0); - - session = (MsimSession *)gc->proto_data; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); - - switch (state) - { - case PURPLE_TYPING: - typing_str = "%typing%"; - break; - - case PURPLE_TYPED: - case PURPLE_NOT_TYPING: - default: - typing_str = "%stoptyping%"; - break; - } - - purple_debug_info("msim", "msim_send_typing(%s): %d (%s)\n", name, state, typing_str); - msim_send_bm(session, name, typing_str, MSIM_BM_ACTION); - return 0; -} - -/** Callback for msim_get_info(), for when user info is received. */ -void -msim_get_info_cb(MsimSession *session, MsimMessage *user_info_msg, gpointer data) -{ - GHashTable *body; - gchar *body_str; - MsimMessage *msg; - gchar *user; - PurpleNotifyUserInfo *user_info; - PurpleBuddy *buddy; - const gchar *str, *str2; - - g_return_if_fail(MSIM_SESSION_VALID(session)); - - /* Get user{name,id} from msim_get_info, passed as an MsimMessage for - orthogonality. */ - msg = (MsimMessage *)data; - g_return_if_fail(msg != NULL); - - user = msim_msg_get_string(msg, "user"); - if (!user) - { - purple_debug_info("msim", "msim_get_info_cb: no 'user' in msg"); - return; - } - - msim_msg_free(msg); - purple_debug_info("msim", "msim_get_info_cb: got for user: %s\n", user); - - body_str = msim_msg_get_string(user_info_msg, "body"); - g_return_if_fail(body_str != NULL); - body = msim_parse_body(body_str); - g_free(body_str); - - buddy = purple_find_buddy(session->account, user); - /* Note: don't assume buddy is non-NULL; will be if lookup random user - * not on blist. */ - - user_info = purple_notify_user_info_new(); - - /* Identification */ - purple_notify_user_info_add_pair(user_info, _("User"), user); - - /* note: g_hash_table_lookup does not create a new string! */ - str = g_hash_table_lookup(body, "UserID"); - if (str) - purple_notify_user_info_add_pair(user_info, _("User ID"), - g_strdup(str)); - - /* a/s/l...the vitals */ - str = g_hash_table_lookup(body, "Age"); - if (str) - purple_notify_user_info_add_pair(user_info, _("Age"), g_strdup(str)); - - str = g_hash_table_lookup(body, "Gender"); - if (str) - purple_notify_user_info_add_pair(user_info, _("Gender"), g_strdup(str)); - - str = g_hash_table_lookup(body, "Location"); - if (str) - purple_notify_user_info_add_pair(user_info, _("Location"), - g_strdup(str)); - - /* Other information */ - - /* Headline comes from buddy status messages */ - if (buddy) - { - str = purple_blist_node_get_string(&buddy->node, "Headline"); - if (str) - purple_notify_user_info_add_pair(user_info, "Headline", str); - } - - - str = g_hash_table_lookup(body, "BandName"); - str2 = g_hash_table_lookup(body, "SongName"); - if (str || str2) - { - purple_notify_user_info_add_pair(user_info, _("Song"), - g_strdup_printf("%s - %s", - str ? str : "Unknown Artist", - str2 ? str2 : "Unknown Song")); - } - - - /* Total friends only available if looked up by uid, not username. */ - str = g_hash_table_lookup(body, "TotalFriends"); - if (str) - purple_notify_user_info_add_pair(user_info, _("Total Friends"), - g_strdup(str)); - - purple_notify_userinfo(session->gc, user, user_info, NULL, NULL); - purple_debug_info("msim", "msim_get_info_cb: username=%s\n", user); - //purple_notify_user_info_destroy(user_info); - /* Do not free username, since it will be used by user_info. */ - - //g_hash_table_destroy(body); -} - -/** Retrieve a user's profile. */ -void -msim_get_info(PurpleConnection *gc, const gchar *user) -{ - PurpleBuddy *buddy; - MsimSession *session; - guint uid; - gchar *user_to_lookup; - MsimMessage *user_msg; - - g_return_if_fail(gc != NULL); - g_return_if_fail(user != NULL); - - session = (MsimSession *)gc->proto_data; - - g_return_if_fail(MSIM_SESSION_VALID(session)); - - /* Obtain uid of buddy. */ - buddy = purple_find_buddy(session->account, user); - if (buddy) - { - uid = purple_blist_node_get_int(&buddy->node, "UserID"); - if (!uid) - { - PurpleNotifyUserInfo *user_info; - - user_info = purple_notify_user_info_new(); - purple_notify_user_info_add_pair(user_info, NULL, - _("This buddy appears to not have a userid stored in the buddy list, can't look up. Is the user really on the buddy list?")); - - purple_notify_userinfo(session->gc, user, user_info, NULL, NULL); - purple_notify_user_info_destroy(user_info); - return; - } - - user_to_lookup = g_strdup_printf("%d", uid); - } else { - - /* Looking up buddy not on blist. Lookup by whatever user entered. */ - user_to_lookup = g_strdup(user); - } - - /* Pass the username to msim_get_info_cb(), because since we lookup - * by userid, the userinfo message will only contain the uid (not - * the username). - */ - user_msg = msim_msg_new(TRUE, - "user", MSIM_TYPE_STRING, g_strdup(user), - NULL); - purple_debug_info("msim", "msim_get_info, setting up lookup, user=%s\n", user); - - msim_lookup_user(session, user_to_lookup, msim_get_info_cb, user_msg); - - g_free(user_to_lookup); -} - -/** Set your status - callback for when user manually sets it. */ -void -msim_set_status(PurpleAccount *account, PurpleStatus *status) -{ - PurpleStatusType *type; - MsimSession *session; - guint status_code; - - session = (MsimSession *)account->gc->proto_data; - - g_return_if_fail(MSIM_SESSION_VALID(session)); - - type = purple_status_get_type(status); - - switch (purple_status_type_get_primitive(type)) - { - case PURPLE_STATUS_AVAILABLE: - status_code = MSIM_STATUS_CODE_ONLINE; - break; - - case PURPLE_STATUS_INVISIBLE: - status_code = MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN; - break; - - case PURPLE_STATUS_AWAY: - status_code = MSIM_STATUS_CODE_AWAY; - break; - - default: - purple_debug_info("msim", "msim_set_status: unknown " - "status interpreting as online"); - status_code = MSIM_STATUS_CODE_ONLINE; - break; - } - - - msim_set_status_code(session, status_code); -} - -/** Go idle. */ -void -msim_set_idle(PurpleConnection *gc, int time) -{ - MsimSession *session; - - g_return_if_fail(gc != NULL); - - session = (MsimSession *)gc->proto_data; - - g_return_if_fail(MSIM_SESSION_VALID(session)); - - if (time == 0) - { - /* Going back from idle. In msim, idle is mutually exclusive - * from the other states (you can only be away or idle, but not - * both, for example), so by going non-idle I go online. - */ - msim_set_status_code(session, MSIM_STATUS_CODE_ONLINE); - } else { - /* msim doesn't support idle time, so just go idle */ - msim_set_status_code(session, MSIM_STATUS_CODE_IDLE); - } -} - -/** Set status using an MSIM_STATUS_CODE_* value. (TODO: also set message) */ -void -msim_set_status_code(MsimSession *session, guint status_code) -{ - g_return_if_fail(MSIM_SESSION_VALID(session)); - - if (!msim_send(session, - "status", MSIM_TYPE_INTEGER, status_code, - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - "statstring", MSIM_TYPE_STRING, g_strdup(""), - "locstring", MSIM_TYPE_STRING, g_strdup(""))) - { - purple_debug_info("msim", "msim_set_status: failed to set status"); - } - -} - -/** After a uid is resolved to username, tag it with the username and submit for processing. - * - * @param session - * @param userinfo Response messsage to resolving request. - * @param data MsimMessage *, the message to attach information to. - */ -static void -msim_incoming_resolved(MsimSession *session, MsimMessage *userinfo, - gpointer data) -{ - gchar *body_str; - GHashTable *body; - gchar *username; - MsimMessage *msg; - - g_return_if_fail(MSIM_SESSION_VALID(session)); - g_return_if_fail(userinfo != NULL); - - body_str = msim_msg_get_string(userinfo, "body"); - g_return_if_fail(body_str != NULL); - body = msim_parse_body(body_str); - g_return_if_fail(body != NULL); - g_free(body_str); - - username = g_hash_table_lookup(body, "UserName"); - g_return_if_fail(username != NULL); - - msg = (MsimMessage *)data; - g_return_if_fail(msg != NULL); - - /* Special elements name beginning with '_', we'll use internally within the - * program (did not come from the wire). */ - msg = msim_msg_append(msg, "_username", MSIM_TYPE_STRING, g_strdup(username)); - - msim_process(session, msg); - - /* TODO: Free copy cloned from msim_preprocess_incoming(). */ - //XXX msim_msg_free(msg); - g_hash_table_destroy(body); -} - -#if 0 -/* Lookup a username by userid, from buddy list. - * - * @param wanted_uid - * - * @return Username of wanted_uid, if on blist, or NULL. Static string. - * - * XXX WARNING: UNKNOWN MEMORY CORRUPTION HERE! - */ -static const gchar * -msim_uid2username_from_blist(MsimSession *session, guint wanted_uid) -{ - GSList *buddies, *cur; - - buddies = purple_find_buddies(session->account, NULL); - - if (!buddies) - { - purple_debug_info("msim", "msim_uid2username_from_blist: no buddies?"); - return NULL; - } - - for (cur = buddies; cur != NULL; cur = g_slist_next(cur)) - { - PurpleBuddy *buddy; - //PurpleBlistNode *node; - guint uid; - const gchar *name; - - - /* See finch/gnthistory.c */ - buddy = cur->data; - //node = cur->data; - - uid = purple_blist_node_get_int(&buddy->node, "UserID"); - //uid = purple_blist_node_get_int(node, "UserID"); - - /* name = buddy->name; */ /* crash */ - /* name = PURPLE_BLIST_NODE_NAME(&buddy->node); */ /* crash */ - - /* XXX Is this right? Memory corruption here somehow. Happens only - * when return one of these values. */ - name = purple_buddy_get_name(buddy); /* crash */ - //name = purple_buddy_get_name((PurpleBuddy *)node); /* crash */ - /* return name; */ /* crash (with above) */ - - /* name = NULL; */ /* no crash */ - /* return NULL; */ /* no crash (with anything) */ - - /* crash = -*** glibc detected *** pidgin: realloc(): invalid pointer: 0x0000000000d2aec0 *** -======= Backtrace: ========= -/lib/libc.so.6(__libc_realloc+0x323)[0x2b7bfc012e03] -/usr/lib/libglib-2.0.so.0(g_realloc+0x31)[0x2b7bfba79a41] -/usr/lib/libgtk-x11-2.0.so.0(gtk_tree_path_append_index+0x3a)[0x2b7bfa110d5a] -/usr/lib/libgtk-x11-2.0.so.0[0x2b7bfa1287dc] -/usr/lib/libgtk-x11-2.0.so.0[0x2b7bfa128e56] -/usr/lib/libgtk-x11-2.0.so.0[0x2b7bfa128efd] -/usr/lib/libglib-2.0.so.0(g_main_context_dispatch+0x1b4)[0x2b7bfba72c84] -/usr/lib/libglib-2.0.so.0[0x2b7bfba75acd] -/usr/lib/libglib-2.0.so.0(g_main_loop_run+0x1ca)[0x2b7bfba75dda] -/usr/lib/libgtk-x11-2.0.so.0(gtk_main+0xa3)[0x2b7bfa0475f3] -pidgin(main+0x8be)[0x46b45e] -/lib/libc.so.6(__libc_start_main+0xf4)[0x2b7bfbfbf0c4] -pidgin(gtk_widget_grab_focus+0x39)[0x429ab9] - -or: - *** glibc detected *** /usr/local/bin/pidgin: malloc(): memory corruption (fast): 0x0000000000c10076 *** - (gdb) bt -#0 0x00002b4074ecd47b in raise () from /lib/libc.so.6 -#1 0x00002b4074eceda0 in abort () from /lib/libc.so.6 -#2 0x00002b4074f0453b in __fsetlocking () from /lib/libc.so.6 -#3 0x00002b4074f0c810 in free () from /lib/libc.so.6 -#4 0x00002b4074f0d6dd in malloc () from /lib/libc.so.6 -#5 0x00002b4074974b5b in g_malloc () from /usr/lib/libglib-2.0.so.0 -#6 0x00002b40749868bf in g_strdup () from /usr/lib/libglib-2.0.so.0 -#7 0x00002b407810969f in msim_parse ( - raw=0xd2a910 "\\bm\\100\\f\\3656574\\msg\\|s|0|ss|Offline") - at message.c:648 -#8 0x00002b407810889c in msim_input_cb (gc_uncasted=0xcf92c0, - source=, cond=) at myspace.c:1478 - - - Why is it crashing in msim_parse()'s g_strdup()? -*/ - purple_debug_info("msim", "msim_uid2username_from_blist: %s's uid=%d (want %d)\n", - name, uid, wanted_uid); - - if (uid == wanted_uid) - { - gchar *ret; - - ret = g_strdup(name); - - g_slist_free(buddies); - - return ret; - } - } - - g_slist_free(buddies); - return NULL; -} -#endif - -/** Preprocess incoming messages, resolving as needed, calling msim_process() when ready to process. - * - * @param session - * @param msg MsimMessage *, freed by caller. - */ -gboolean -msim_preprocess_incoming(MsimSession *session, MsimMessage *msg) -{ - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - if (msim_msg_get(msg, "bm") && msim_msg_get(msg, "f")) - { - guint uid; - const gchar *username; - - /* 'f' = userid message is from, in buddy messages */ - uid = msim_msg_get_integer(msg, "f"); - - /* TODO: Make caching work. Currently it is commented out because - * it crashes for unknown reasons, memory realloc error. */ -#if 0 - username = msim_uid2username_from_blist(session, uid); -#else - username = NULL; -#endif - - if (username) - { - /* Know username already, use it. */ - purple_debug_info("msim", "msim_preprocess_incoming: tagging with _username=%s\n", - username); - msg = msim_msg_append(msg, "_username", MSIM_TYPE_STRING, g_strdup(username)); - return msim_process(session, msg); - - } else { - gchar *from; - - /* Send lookup request. */ - /* XXX: where is msim_msg_get_string() freed? make _strdup and _nonstrdup. */ - purple_debug_info("msim", "msim_incoming: sending lookup, setting up callback\n"); - from = msim_msg_get_string(msg, "f"); - msim_lookup_user(session, from, msim_incoming_resolved, msim_msg_clone(msg)); - g_free(from); - - /* indeterminate */ - return TRUE; - } - } else { - /* Nothing to resolve - send directly to processing. */ - return msim_process(session, msg); - } -} - -/** Check if the connection is still alive, based on last communication. */ -gboolean -msim_check_alive(gpointer data) -{ - MsimSession *session; - time_t delta; - gchar *errmsg; - - session = (MsimSession *)data; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - - delta = time(NULL) - session->last_comm; - purple_debug_info("msim", "msim_check_alive: delta=%d\n", delta); - if (delta >= MSIM_KEEPALIVE_INTERVAL) - { - errmsg = g_strdup_printf(_("Connection to server lost (no data received within %d seconds)"), delta); - - purple_debug_info("msim", "msim_check_alive: %s > interval of %d, presumed dead\n", - errmsg, MSIM_KEEPALIVE_INTERVAL); - purple_connection_error(session->gc, errmsg); - - purple_notify_error(session->gc, NULL, errmsg, NULL); - - g_free(errmsg); - - return FALSE; - } - - return TRUE; -} - -/** Called when the session key arrives. */ -gboolean -msim_we_are_logged_on(MsimSession *session, MsimMessage *msg) -{ - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - purple_connection_update_progress(session->gc, _("Connected"), 3, 4); - - session->sesskey = msim_msg_get_integer(msg, "sesskey"); - purple_debug_info("msim", "SESSKEY=<%d>\n", session->sesskey); - - /* Comes with: proof,profileid,userid,uniquenick -- all same values - * some of the time, but can vary. This is our own user ID. */ - session->userid = msim_msg_get_integer(msg, "userid"); - - purple_connection_set_state(session->gc, PURPLE_CONNECTED); - - /* We now know are our own username, only after we're logged in.. - * which is weird, but happens because you login with your email - * address and not username. Will be freed in msim_session_destroy(). */ - session->username = msim_msg_get_string(msg, "uniquenick"); - -#ifdef MSIM_FAKE_SELF_ONLINE - /* Fake our self coming online. */ - purple_prpl_got_user_status(session->account, session->username, purple_primitive_get_id_from_type(PURPLE_STATUS_AVAILABLE), NULL); -#endif - - /* Set status depending on preference. */ - /* TODO: set status to current status, change status to hidden if sign on as hidden. - * Or remove this preference alltogether, and set status to current status? */ - msim_set_status_code(session, - purple_account_get_bool(session->account, "hidden", FALSE) - ? PURPLE_STATUS_INVISIBLE - : PURPLE_STATUS_AVAILABLE); - - purple_timeout_add(MSIM_KEEPALIVE_INTERVAL_CHECK, msim_check_alive, session); - - return TRUE; -} - -/** - * Process a message. - * - * @param session - * @param msg A message from the server, ready for processing (possibly with resolved username information attached). Caller frees. - * - * @return TRUE if successful. FALSE if processing failed. - */ -gboolean -msim_process(MsimSession *session, MsimMessage *msg) -{ - g_return_val_if_fail(session != NULL, FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - -#ifdef MSIM_DEBUG_MSG - { - msim_msg_dump("ready to process: %s\n", msg); - } -#endif - - if (msim_msg_get(msg, "nc")) - { - return msim_login_challenge(session, msg); - } else if (msim_msg_get(msg, "sesskey")) { - return msim_we_are_logged_on(session, msg); - } else if (msim_msg_get(msg, "bm")) { - guint bm; - - bm = msim_msg_get_integer(msg, "bm"); - switch (bm) - { - case MSIM_BM_STATUS: - return msim_status(session, msg); - case MSIM_BM_INSTANT: - return msim_incoming_im(session, msg); - case MSIM_BM_ACTION: - return msim_incoming_action(session, msg); - default: - /* Not really an IM, but show it for informational - * purposes during development. */ - return msim_incoming_im(session, msg); - } - } else if (msim_msg_get(msg, "rid")) { - return msim_process_reply(session, msg); - } else if (msim_msg_get(msg, "error")) { - return msim_error(session, msg); - } else if (msim_msg_get(msg, "ka")) { - /* TODO: Setup a timer, if keep-alive is not received within ~3 minutes, then - * disconnect the user. As it stands, if Internet connection goes out (this - * just happened here), msimprpl will appear to be connected forever, while - * other plugins (oscar, etc.) will time out. Msimprpl should timeout too. */ - purple_debug_info("msim", "msim_process: got keep alive\n"); - return TRUE; - } else { - msim_unrecognized(session, msg, "in msim_process"); - return FALSE; - } -} - -/** Store an field of information about a buddy. */ -void -msim_store_buddy_info_each(gpointer key, gpointer value, gpointer user_data) -{ - PurpleBuddy *buddy; - gchar *key_str, *value_str; - - buddy = (PurpleBuddy *)user_data; - key_str = (gchar *)key; - value_str = (gchar *)value; - - if (strcmp(key_str, "UserID") == 0 || - strcmp(key_str, "Age") == 0 || - strcmp(key_str, "TotalFriends") == 0) - { - /* Certain fields get set as integers, instead of strings, for - * convenience. May not be the best way to do it, but having at least - * UserID as an integer is convenient...until it overflows! */ - purple_blist_node_set_int(&buddy->node, key_str, atol(value_str)); - } else { - purple_blist_node_set_string(&buddy->node, key_str, value_str); - } -} - -/** Save buddy information to the buddy list from a user info reply message. - * - * @param session - * @param msg The user information reply, with any amount of information. - * - * The information is saved to the buddy's blist node, which ends up in blist.xml. - */ -gboolean -msim_store_buddy_info(MsimSession *session, MsimMessage *msg) -{ - GHashTable *body; - gchar *username, *body_str, *uid; - PurpleBuddy *buddy; - guint rid; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - rid = msim_msg_get_integer(msg, "rid"); - - g_return_val_if_fail(rid != 0, FALSE); - - body_str = msim_msg_get_string(msg, "body"); - g_return_val_if_fail(body_str != NULL, FALSE); - body = msim_parse_body(body_str); - g_free(body_str); - - /* TODO: implement a better hash-like interface, and use it. */ - username = g_hash_table_lookup(body, "UserName"); - - if (!username) - { - purple_debug_info("msim", - "msim_process_reply: not caching body, no UserName\n"); - return FALSE; - } - - uid = g_hash_table_lookup(body, "UserID"); - g_return_val_if_fail(uid, FALSE); - - purple_debug_info("msim", "associating uid %d with username %s\n", uid, username); - - buddy = purple_find_buddy(session->account, username); - if (buddy) - { - g_hash_table_foreach(body, msim_store_buddy_info_each, buddy); - } - - return TRUE; -} - -/** - * Process a persistance message reply from the server. - * - * @param session - * @param msg Message reply from server. - * - * @return TRUE if successful. - */ -gboolean -msim_process_reply(MsimSession *session, MsimMessage *msg) -{ - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - if (msim_msg_get(msg, "rid")) /* msim_lookup_user sets callback for here */ - { - MSIM_USER_LOOKUP_CB cb; - gpointer data; - guint rid; - - msim_store_buddy_info(session, msg); - - rid = msim_msg_get_integer(msg, "rid"); - - /* If a callback is registered for this userid lookup, call it. */ - cb = g_hash_table_lookup(session->user_lookup_cb, GUINT_TO_POINTER(rid)); - data = g_hash_table_lookup(session->user_lookup_cb_data, GUINT_TO_POINTER(rid)); - - if (cb) - { - purple_debug_info("msim", - "msim_process_body: calling callback now\n"); - /* Clone message, so that the callback 'cb' can use it (needs to free it also). */ - cb(session, msim_msg_clone(msg), data); - g_hash_table_remove(session->user_lookup_cb, GUINT_TO_POINTER(rid)); - g_hash_table_remove(session->user_lookup_cb_data, GUINT_TO_POINTER(rid)); - } else { - purple_debug_info("msim", - "msim_process_body: no callback for rid %d\n", rid); - } - } - - return TRUE; -} - -/** - * Handle an error from the server. - * - * @param session - * @param msg The message. - * - * @return TRUE if successfully reported error. - */ -gboolean -msim_error(MsimSession *session, MsimMessage *msg) -{ - gchar *errmsg, *full_errmsg; - guint err; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - err = msim_msg_get_integer(msg, "err"); - errmsg = msim_msg_get_string(msg, "errmsg"); - - full_errmsg = g_strdup_printf(_("Protocol error, code %d: %s"), err, - errmsg ? errmsg : "no 'errmsg' given"); - - g_free(errmsg); - - purple_debug_info("msim", "msim_error: %s\n", full_errmsg); - - purple_notify_error(session->account, g_strdup(_("MySpaceIM Error")), - full_errmsg, NULL); - - /* Destroy session if fatal. */ - if (msim_msg_get(msg, "fatal")) - { - purple_debug_info("msim", "fatal error, closing\n"); - purple_connection_error(session->gc, full_errmsg); - } - - return TRUE; -} - -/** - * Process incoming status messages. - * - * @param session - * @param msg Status update message. Caller frees. - * - * @return TRUE if successful. - */ -gboolean -msim_status(MsimSession *session, MsimMessage *msg) -{ - PurpleBuddyList *blist; - PurpleBuddy *buddy; - //PurpleStatus *status; - gchar **status_array; - GList *list; - gchar *status_headline; - gchar *status_str; - gint i, status_code, purple_status_code; - gchar *username; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - status_str = msim_msg_get_string(msg, "msg"); - g_return_val_if_fail(status_str != NULL, FALSE); - - msim_msg_dump("msim_status msg=%s\n", msg); - - /* Helpfully looked up by msim_incoming_resolve() for us. */ - username = msim_msg_get_string(msg, "_username"); - /* Note: DisplayName doesn't seem to be resolvable. It could be displayed on - * the buddy list, if the UserID was stored along with it. */ - - if (username == NULL) - { - g_free(status_str); - g_return_val_if_fail(NULL, FALSE); - } - - purple_debug_info("msim", - "msim_status_cb: updating status for <%s> to <%s>\n", - username, status_str); - - /* TODO: generic functions to split into a GList, part of MsimMessage */ - status_array = g_strsplit(status_str, "|", 0); - for (list = NULL, i = 0; - status_array[i]; - i++) - { - /* Note: this adds the 0th ordinal too, which might not be a value - * at all (the 0 in the 0|1|2|3... status fields, but 0 always appears blank). - */ - list = g_list_append(list, status_array[i]); - } - - /* Example fields: - * |s|0|ss|Offline - * |s|1|ss|:-)|ls||ip|0|p|0 - * - * TODO: write list support in MsimMessage, and use it here. - */ - - status_code = atoi(g_list_nth_data(list, MSIM_STATUS_ORDINAL_ONLINE)); - purple_debug_info("msim", "msim_status_cb: %s's status code = %d\n", username, status_code); - status_headline = g_list_nth_data(list, MSIM_STATUS_ORDINAL_HEADLINE); - - blist = purple_get_blist(); - - /* Add buddy if not found */ - buddy = purple_find_buddy(session->account, username); - if (!buddy) - { - purple_debug_info("msim", - "msim_status: making new buddy for %s\n", username); - buddy = purple_buddy_new(session->account, username, NULL); - - purple_blist_add_buddy(buddy, NULL, NULL, NULL); - - /* All buddies on list should have 'uid' integer associated with them. */ - purple_blist_node_set_int(&buddy->node, "UserID", msim_msg_get_integer(msg, "f")); - - msim_store_buddy_info(session, msg); - } else { - purple_debug_info("msim", "msim_status: found buddy %s\n", username); - } - - purple_blist_node_set_string(&buddy->node, "Headline", status_headline); - - /* Set user status */ - switch (status_code) - { - case MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN: - purple_status_code = PURPLE_STATUS_OFFLINE; - break; - - case MSIM_STATUS_CODE_ONLINE: - purple_status_code = PURPLE_STATUS_AVAILABLE; - break; - - case MSIM_STATUS_CODE_AWAY: - purple_status_code = PURPLE_STATUS_AWAY; - break; - - case MSIM_STATUS_CODE_IDLE: - /* will be handled below */ - purple_status_code = -1; - break; - - default: - purple_debug_info("msim", "msim_status_cb for %s, unknown status code %d, treating as available\n", - username, status_code); - purple_status_code = PURPLE_STATUS_AVAILABLE; - } - -#ifdef MSIM_FAKE_SELF_ONLINE - if (!strcmp(username, session->username) && purple_status_code == PURPLE_STATUS_OFFLINE) - { - /* Hack to ignore offline status notices on self. */ - } else if (status_code != MSIM_STATUS_CODE_IDLE) { -#endif - purple_prpl_got_user_status(session->account, username, purple_primitive_get_id_from_type(purple_status_code), NULL); -#ifdef MSIM_FAKE_SELF_ONLINE - } -#endif - - if (status_code == MSIM_STATUS_CODE_IDLE) - { - purple_debug_info("msim", "msim_status: got idle: %s\n", username); - purple_prpl_got_user_idle(session->account, username, TRUE, time(NULL)); - } else { - /* All other statuses indicate going back to non-idle. */ - purple_prpl_got_user_idle(session->account, username, FALSE, time(NULL)); - } - - g_strfreev(status_array); - g_free(status_str); - g_free(username); - g_list_free(list); - - return TRUE; -} - -/** Add a buddy to user's buddy list. */ -void -msim_add_buddy(PurpleConnection *gc, PurpleBuddy *buddy, PurpleGroup *group) -{ - MsimSession *session; - MsimMessage *msg; - /* MsimMessage *msg_blocklist; */ - - session = (MsimSession *)gc->proto_data; - purple_debug_info("msim", "msim_add_buddy: want to add %s to %s\n", buddy->name, - group ? group->name : "(no group)"); - - msg = msim_msg_new(TRUE, - "addbuddy", MSIM_TYPE_BOOLEAN, TRUE, - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - /* "newprofileid" will be inserted here with uid. */ - "reason", MSIM_TYPE_STRING, g_strdup(""), - NULL); - - if (!msim_postprocess_outgoing(session, msg, buddy->name, "newprofileid", "reason")) - { - purple_notify_error(NULL, NULL, _("Failed to add buddy"), _("'addbuddy' command failed.")); - msim_msg_free(msg); - return; - } - msim_msg_free(msg); - - /* TODO: if addbuddy fails ('error' message is returned), delete added buddy from - * buddy list since Purple adds it locally. */ - - /* TODO: Update blocklist. */ -#if 0 - msg_blocklist = msim_msg_new(TRUE, - "persist", MSIM_TYPE_INTEGER, 1, - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - "cmd", MSIM_TYPE_INTEGER, MSIM_CMD_BIT_ACTION | MSIM_CMD_PUT, - "dsn", MSIM_TYPE_INTEGER, MC_CONTACT_INFO_DSN, - "lid", MSIM_TYPE_INTEGER, MC_CONTACT_INFO_LID, - /* TODO: Use msim_new_reply_callback to get rid. */ - "rid", MSIM_TYPE_INTEGER, session->next_rid++, - "body", MSIM_TYPE_STRING, - g_strdup_printf("ContactID=\034" - "GroupName=%s\034" - "Position=1000\034" - "Visibility=1\034" - "NickName=\034" - "NameSelect=0", - "Friends" /*group->name*/ )); - - if (!msim_postprocess_outgoing(session, msg, buddy->name, "body", NULL)) - { - purple_notify_error(NULL, NULL, _("Failed to add buddy"), _("persist command failed")); - msim_msg_free(msg_blocklist); - return; - } - msim_msg_free(msg_blocklist); -#endif -} - -/** Perform actual postprocessing on a message, adding userid as specified. - * - * @param msg The message to postprocess. - * @param uid_before Name of field where to insert new field before, or NULL for end. - * @param uid_field_name Name of field to add uid to. - * @param uid The userid to insert. - * - * If the field named by uid_field_name already exists, then its string contents will - * be used for the field, except "" will be replaced by the userid. - * - * If the field named by uid_field_name does not exist, it will be added before the - * field named by uid_before, as an integer, with the userid. - * - * Does not handle sending, or scheduling userid lookup. For that, see msim_postprocess_outgoing(). - */ -MsimMessage * -msim_do_postprocessing(MsimMessage *msg, const gchar *uid_before, - const gchar *uid_field_name, guint uid) -{ - purple_debug_info("msim", "msim_do_postprocessing called with ufn=%s, ub=%s, uid=%d\n", - uid_field_name, uid_before, uid); - msim_msg_dump("msim_do_postprocessing msg: %s\n", msg); - - /* First, check - if the field already exists, treat it as a format string. */ - if (msim_msg_get(msg, uid_field_name)) - { - MsimMessageElement *elem; - gchar *fmt_string; - gchar *uid_str; - - /* Warning: this probably violates the encapsulation of MsimMessage */ - - elem = msim_msg_get(msg, uid_field_name); - g_return_val_if_fail(elem->type == MSIM_TYPE_STRING, NULL); - - /* Get the raw string, not with msim_msg_get_string() since that copies it. - * Want the original string so can free it. */ - fmt_string = (gchar *)(elem->data); - - uid_str = g_strdup_printf("%d", uid); - elem->data = str_replace(fmt_string, "", uid_str); - g_free(uid_str); - g_free(fmt_string); - - purple_debug_info("msim", "msim_postprocess_outgoing_cb: formatted new string, %s\n", - elem->data); - - } else { - /* Otherwise, insert new field into outgoing message. */ - msg = msim_msg_insert_before(msg, uid_before, uid_field_name, MSIM_TYPE_INTEGER, GUINT_TO_POINTER(uid)); - } - - return msg; -} - -/** Callback for msim_postprocess_outgoing() to add a userid to a message, and send it (once receiving userid). - * - * @param session - * @param userinfo The user information reply message, containing the user ID - * @param data The message to postprocess and send. - * - * The data message should contain these fields: - * - * _uid_field_name: string, name of field to add with userid from userinfo message - * _uid_before: string, name of field before field to insert, or NULL for end - * - * -*/ -void -msim_postprocess_outgoing_cb(MsimSession *session, MsimMessage *userinfo, gpointer data) -{ - gchar *body_str; - GHashTable *body; - gchar *uid, *uid_field_name, *uid_before; - MsimMessage *msg; - - msg = (MsimMessage *)data; - - msim_msg_dump("msim_postprocess_outgoing_cb() got msg=%s\n", msg); - - /* Obtain userid from userinfo message. */ - body_str = msim_msg_get_string(userinfo, "body"); - g_return_if_fail(body_str != NULL); - body = msim_parse_body(body_str); - g_free(body_str); - - uid = g_strdup(g_hash_table_lookup(body, "UserID")); - g_hash_table_destroy(body); - - uid_field_name = msim_msg_get_string(msg, "_uid_field_name"); - uid_before = msim_msg_get_string(msg, "_uid_before"); - - msg = msim_do_postprocessing(msg, uid_before, uid_field_name, atol(uid)); - - /* Send */ - if (!msim_msg_send(session, msg)) - { - purple_debug_info("msim", "msim_postprocess_outgoing_cb: sending failed for message: %s\n", msg); - } - - - /* Free field names AFTER sending message, because MsimMessage does NOT copy - * field names - instead, treats them as static strings (which they usually are). - */ - g_free(uid_field_name); - g_free(uid_before); - - //msim_msg_free(msg); -} - -/** Postprocess and send a message. - * - * @param session - * @param msg Message to postprocess. Will NOT be freed. - * @param username Username to resolve. Assumed to be a static string (will not be freed or copied). - * @param uid_field_name Name of new field to add, containing uid of username. Static string. - * @param uid_before Name of existing field to insert username field before. Static string. - * - * @return Postprocessed message. - */ -gboolean -msim_postprocess_outgoing(MsimSession *session, MsimMessage *msg, - const gchar *username, const gchar *uid_field_name, - const gchar *uid_before) -{ - PurpleBuddy *buddy; - guint uid; - gboolean rc; - - /* Store information for msim_postprocess_outgoing_cb(). */ - purple_debug_info("msim", "msim_postprocess_outgoing(u=%s,ufn=%s,ub=%s)\n", - username, uid_field_name, uid_before); - msim_msg_dump("msim_postprocess_outgoing: msg before=%s\n", msg); - msg = msim_msg_append(msg, "_username", MSIM_TYPE_STRING, g_strdup(username)); - msg = msim_msg_append(msg, "_uid_field_name", MSIM_TYPE_STRING, g_strdup(uid_field_name)); - msg = msim_msg_append(msg, "_uid_before", MSIM_TYPE_STRING, g_strdup(uid_before)); - - /* First, try the most obvious. If numeric userid is given, use that directly. */ - if (msim_is_userid(username)) - { - uid = atol(username); - } else { - /* Next, see if on buddy list and know uid. */ - buddy = purple_find_buddy(session->account, username); - if (buddy) - { - uid = purple_blist_node_get_int(&buddy->node, "UserID"); - } else { - uid = 0; - } - - if (!buddy || !uid) - { - /* Don't have uid offhand - need to ask for it, and wait until hear back before sending. */ - purple_debug_info("msim", ">>> msim_postprocess_outgoing: couldn't find username %s in blist\n", - username); - msim_msg_dump("msim_postprocess_outgoing - scheduling lookup, msg=%s\n", msg); - /* TODO: where is cloned message freed? Should be in _cb. */ - msim_lookup_user(session, username, msim_postprocess_outgoing_cb, msim_msg_clone(msg)); - return TRUE; /* not sure of status yet - haven't sent! */ - } - } - - /* Already have uid, postprocess and send msg immediately. */ - purple_debug_info("msim", "msim_postprocess_outgoing: found username %s has uid %d\n", - username, uid); - - msg = msim_do_postprocessing(msg, uid_before, uid_field_name, uid); - - msim_msg_dump("msim_postprocess_outgoing: msg after (uid immediate)=%s\n", msg); - - rc = msim_msg_send(session, msg); - - //msim_msg_free(msg); - - return rc; -} - -/** Remove a buddy from the user's buddy list. */ -void -msim_remove_buddy(PurpleConnection *gc, PurpleBuddy *buddy, PurpleGroup *group) -{ - MsimSession *session; - MsimMessage *delbuddy_msg; - MsimMessage *persist_msg; - MsimMessage *blocklist_msg; - - session = (MsimSession *)gc->proto_data; - - delbuddy_msg = msim_msg_new(TRUE, - "delbuddy", MSIM_TYPE_BOOLEAN, TRUE, - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - /* 'delprofileid' with uid will be inserted here. */ - NULL); - /* TODO: free msg */ - if (!msim_postprocess_outgoing(session, delbuddy_msg, buddy->name, "delprofileid", NULL)) - { - purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("'delbuddy' command failed")); - return; - } - - persist_msg = msim_msg_new(TRUE, - "persist", MSIM_TYPE_INTEGER, 1, - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - "cmd", MSIM_TYPE_INTEGER, MSIM_CMD_BIT_ACTION | MSIM_CMD_DELETE, - "dsn", MSIM_TYPE_INTEGER, MD_DELETE_BUDDY_DSN, - "lid", MSIM_TYPE_INTEGER, MD_DELETE_BUDDY_LID, - "uid", MSIM_TYPE_INTEGER, session->userid, - "rid", MSIM_TYPE_INTEGER, session->next_rid++, - /* will be replaced by postprocessing */ - "body", MSIM_TYPE_STRING, g_strdup("ContactID="), - NULL); - - /* TODO: free msg */ - if (!msim_postprocess_outgoing(session, persist_msg, buddy->name, "body", NULL)) - { - purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("persist command failed")); - return; - } - - blocklist_msg = msim_msg_new(TRUE, - "blocklist", MSIM_TYPE_BOOLEAN, TRUE, - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - /* TODO: MsimMessage lists */ - "idlist", MSIM_TYPE_STRING, g_strdup("a-||b-|"), - NULL); - - if (!msim_postprocess_outgoing(session, blocklist_msg, buddy->name, "idlist", NULL)) - { - purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("blocklist command failed")); - return; - } -} - -/** Return whether the buddy can be messaged while offline. - * - * The protocol supports offline messages in just the same way as online - * messages. - */ -gboolean -msim_offline_message(const PurpleBuddy *buddy) -{ - return TRUE; -} - -/** - * Callback when input available. - * - * @param gc_uncasted A PurpleConnection pointer. - * @param source File descriptor. - * @param cond PURPLE_INPUT_READ - * - * Reads the input, and calls msim_preprocess_incoming() to handle it. - */ -void -msim_input_cb(gpointer gc_uncasted, gint source, PurpleInputCondition cond) -{ - PurpleConnection *gc; - PurpleAccount *account; - MsimSession *session; - gchar *end; - int n; - - g_return_if_fail(gc_uncasted != NULL); - g_return_if_fail(source >= 0); /* Note: 0 is a valid fd */ - - gc = (PurpleConnection *)(gc_uncasted); - account = purple_connection_get_account(gc); - session = gc->proto_data; - - g_return_if_fail(cond == PURPLE_INPUT_READ); - g_return_if_fail(MSIM_SESSION_VALID(session)); - - /* Mark down that we got data, so don't timeout. */ - session->last_comm = time(NULL); - - /* Only can handle so much data at once... - * If this happens, try recompiling with a higher MSIM_READ_BUF_SIZE. - * Should be large enough to hold the largest protocol message. - */ - if (session->rxoff == MSIM_READ_BUF_SIZE) - { - purple_debug_error("msim", "msim_input_cb: %d-byte read buffer full!\n", - MSIM_READ_BUF_SIZE); - purple_connection_error(gc, _("Read buffer full")); - return; - } - - purple_debug_info("msim", "buffer at %d (max %d), reading up to %d\n", - session->rxoff, MSIM_READ_BUF_SIZE, - MSIM_READ_BUF_SIZE - session->rxoff); - - /* Read into buffer. On Win32, need recv() not read(). session->fd also holds - * the file descriptor, but it sometimes differs from the 'source' parameter. - */ - n = recv(session->fd, session->rxbuf + session->rxoff, MSIM_READ_BUF_SIZE - session->rxoff, 0); - - if (n < 0 && errno == EAGAIN) - { - return; - } - else if (n < 0) - { - purple_debug_error("msim", "msim_input_cb: read error, ret=%d, " - "error=%s, source=%d, fd=%d (%X))\n", - n, strerror(errno), source, session->fd, session->fd); - purple_connection_error(gc, _("Read error")); - return; - } - else if (n == 0) - { - purple_debug_info("msim", "msim_input_cb: server disconnected\n"); - purple_connection_error(gc, _("Server has disconnected")); - return; - } - - /* Null terminate */ - session->rxbuf[session->rxoff + n] = 0; - -#ifdef MSIM_CHECK_EMBEDDED_NULLS - /* Check for embedded NULs. I don't handle them, and they shouldn't occur. */ - if (strlen(session->rxbuf + session->rxoff) != n) - { - /* Occurs after login, but it is not a null byte. */ - purple_debug_info("msim", "msim_input_cb: strlen=%d, but read %d bytes" - "--null byte encountered?\n", - strlen(session->rxbuf + session->rxoff), n); - //purple_connection_error(gc, "Invalid message - null byte on input"); - return; - } -#endif - - session->rxoff += n; - purple_debug_info("msim", "msim_input_cb: read=%d\n", n); - -#ifdef MSIM_DEBUG_RXBUF - purple_debug_info("msim", "buf=<%s>\n", session->rxbuf); -#endif - - /* Look for \\final\\ end markers. If found, process message. */ - while((end = strstr(session->rxbuf, MSIM_FINAL_STRING))) - { - MsimMessage *msg; - -#ifdef MSIM_DEBUG_RXBUF - purple_debug_info("msim", "in loop: buf=<%s>\n", session->rxbuf); -#endif - *end = 0; - msg = msim_parse(g_strdup(session->rxbuf)); - if (!msg) - { - purple_debug_info("msim", "msim_input_cb: couldn't parse <%s>\n", - session->rxbuf); - purple_connection_error(gc, _("Unparseable message")); - } - else - { - /* Process message and then free it (processing function should - * clone message if it wants to keep it afterwards.) */ - if (!msim_preprocess_incoming(session, msg)) - { - msim_msg_dump("msim_input_cb: preprocessing message failed on msg: %s\n", msg); - } - msim_msg_free(msg); - } - - /* Move remaining part of buffer to beginning. */ - session->rxoff -= strlen(session->rxbuf) + strlen(MSIM_FINAL_STRING); - memmove(session->rxbuf, end + strlen(MSIM_FINAL_STRING), - MSIM_READ_BUF_SIZE - (end + strlen(MSIM_FINAL_STRING) - session->rxbuf)); - - /* Clear end of buffer */ - //memset(end, 0, MSIM_READ_BUF_SIZE - (end - session->rxbuf)); - } -} - -/* Setup a callback, to be called when a reply is received with the returned rid. - * - * @param cb The callback, an MSIM_USER_LOOKUP_CB. - * @param data Arbitrary user data to be passed to callback (probably an MsimMessage *). - * - * @return The request/reply ID, used to link replies with requests. Put the rid in your request. - * - * TODO: Make more generic and more specific: - * 1) MSIM_USER_LOOKUP_CB - make it for PERSIST_REPLY, not just user lookup - * 2) data - make it an MsimMessage? - */ -guint -msim_new_reply_callback(MsimSession *session, MSIM_USER_LOOKUP_CB cb, - gpointer data) -{ - guint rid; - - rid = session->next_rid++; - - g_hash_table_insert(session->user_lookup_cb, GUINT_TO_POINTER(rid), cb); - g_hash_table_insert(session->user_lookup_cb_data, GUINT_TO_POINTER(rid), data); - - return rid; -} - -/** - * Callback when connected. Sets up input handlers. - * - * @param data A PurpleConnection pointer. - * @param source File descriptor. - * @param error_message - */ -void -msim_connect_cb(gpointer data, gint source, const gchar *error_message) -{ - PurpleConnection *gc; - MsimSession *session; - - g_return_if_fail(data != NULL); - - gc = (PurpleConnection *)data; - session = (MsimSession *)gc->proto_data; - - if (source < 0) - { - purple_connection_error(gc, _("Couldn't connect to host")); - purple_connection_error(gc, g_strdup_printf( - _("Couldn't connect to host: %s (%d)"), - error_message ? error_message : "no message given", - source)); - return; - } - - session->fd = source; - - gc->inpa = purple_input_add(source, PURPLE_INPUT_READ, msim_input_cb, gc); - - -} - -/* Session methods */ - -/** - * Create a new MSIM session. - * - * @param acct The account to create the session from. - * - * @return Pointer to a new session. Free with msim_session_destroy. - */ -MsimSession * -msim_session_new(PurpleAccount *acct) -{ - MsimSession *session; - - g_return_val_if_fail(acct != NULL, NULL); - - session = g_new0(MsimSession, 1); - - session->magic = MSIM_SESSION_STRUCT_MAGIC; - session->account = acct; - session->gc = purple_account_get_connection(acct); - session->sesskey = 0; - session->userid = 0; - session->username = NULL; - session->fd = -1; - - /* TODO: Remove. */ - session->user_lookup_cb = g_hash_table_new_full(g_direct_hash, - g_direct_equal, NULL, NULL); /* do NOT free function pointers! (values) */ - session->user_lookup_cb_data = g_hash_table_new_full(g_direct_hash, - g_direct_equal, NULL, NULL);/* TODO: we don't know what the values are, - they could be integers inside gpointers - or strings, so I don't freed them. - Figure this out, once free cache. */ - session->rxoff = 0; - session->rxbuf = g_new0(gchar, MSIM_READ_BUF_SIZE); - session->next_rid = 1; - session->last_comm = time(NULL); - - return session; -} - -/** - * Free a session. - * - * @param session The session to destroy. - */ -void -msim_session_destroy(MsimSession *session) -{ - g_return_if_fail(MSIM_SESSION_VALID(session)); - - session->magic = -1; - - g_free(session->rxbuf); - g_free(session->username); - - /* TODO: Remove. */ - g_hash_table_destroy(session->user_lookup_cb); - g_hash_table_destroy(session->user_lookup_cb_data); - - g_free(session); -} - -/** - * Close the connection. - * - * @param gc The connection. - */ -void -msim_close(PurpleConnection *gc) -{ - MsimSession *session; - - if (gc == NULL) - return; - - session = (MsimSession *)gc->proto_data; - if (session == NULL) - return; - - gc->proto_data = NULL; - - if (!MSIM_SESSION_VALID(session)) - return; - - if (session->gc->inpa) - purple_input_remove(session->gc->inpa); - - msim_session_destroy(session); -} - - -/** - * Check if a string is a userid (all numeric). - * - * @param user The user id, email, or name. - * - * @return TRUE if is userid, FALSE if not. - */ -gboolean -msim_is_userid(const gchar *user) -{ - g_return_val_if_fail(user != NULL, FALSE); - - return strspn(user, "0123456789") == strlen(user); -} - -/** - * Check if a string is an email address (contains an @). - * - * @param user The user id, email, or name. - * - * @return TRUE if is an email, FALSE if not. - * - * This function is not intended to be used as a generic - * means of validating email addresses, but to distinguish - * between a user represented by an email address from - * other forms of identification. - */ -gboolean -msim_is_email(const gchar *user) -{ - g_return_val_if_fail(user != NULL, FALSE); - - return strchr(user, '@') != NULL; -} - - -/** - * Asynchronously lookup user information, calling callback when receive result. - * - * @param session - * @param user The user id, email address, or username. Not freed. - * @param cb Callback, called with user information when available. - * @param data An arbitray data pointer passed to the callback. - */ -/* TODO: change to not use callbacks */ -void -msim_lookup_user(MsimSession *session, const gchar *user, - MSIM_USER_LOOKUP_CB cb, gpointer data) -{ - gchar *field_name; - guint rid, cmd, dsn, lid; - - g_return_if_fail(MSIM_SESSION_VALID(session)); - g_return_if_fail(user != NULL); - g_return_if_fail(cb != NULL); - - purple_debug_info("msim", "msim_lookup_userid: " - "asynchronously looking up <%s>\n", user); - - msim_msg_dump("msim_lookup_user: data=%s\n", (MsimMessage *)data); - - /* Setup callback. Response will be associated with request using 'rid'. */ - rid = msim_new_reply_callback(session, cb, data); - - /* Send request */ - - cmd = MSIM_CMD_GET; - - if (msim_is_userid(user)) - { - field_name = "UserID"; - dsn = MG_MYSPACE_INFO_BY_ID_DSN; - lid = MG_MYSPACE_INFO_BY_ID_LID; - } else if (msim_is_email(user)) { - field_name = "Email"; - dsn = MG_MYSPACE_INFO_BY_STRING_DSN; - lid = MG_MYSPACE_INFO_BY_STRING_LID; - } else { - field_name = "UserName"; - dsn = MG_MYSPACE_INFO_BY_STRING_DSN; - lid = MG_MYSPACE_INFO_BY_STRING_LID; - } - - - g_return_if_fail(msim_send(session, - "persist", MSIM_TYPE_INTEGER, 1, - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - "cmd", MSIM_TYPE_INTEGER, 1, - "dsn", MSIM_TYPE_INTEGER, dsn, - "uid", MSIM_TYPE_INTEGER, session->userid, - "lid", MSIM_TYPE_INTEGER, lid, - "rid", MSIM_TYPE_INTEGER, rid, - /* TODO: dictionary field type */ - "body", MSIM_TYPE_STRING, - g_strdup_printf("%s=%s", field_name, user), - NULL)); -} - - -/** - * Obtain the status text for a buddy. - * - * @param buddy The buddy to obtain status text for. - * - * @return Status text, or NULL if error. Caller g_free()'s. - * - */ -char * -msim_status_text(PurpleBuddy *buddy) -{ - MsimSession *session; - const gchar *display_name, *headline; - - g_return_val_if_fail(buddy != NULL, NULL); - - session = (MsimSession *)buddy->account->gc->proto_data; - g_return_val_if_fail(MSIM_SESSION_VALID(session), NULL); - - display_name = headline = NULL; - - /* Retrieve display name and/or headline, depending on user preference. */ - if (purple_account_get_bool(session->account, "show_display_name", TRUE)) - { - display_name = purple_blist_node_get_string(&buddy->node, "DisplayName"); - } - - if (purple_account_get_bool(session->account, "show_headline", FALSE)) - { - headline = purple_blist_node_get_string(&buddy->node, "Headline"); - } - - /* Return appropriate combination of display name and/or headline, or neither. */ - - if (display_name && headline) - return g_strconcat(display_name, " ", headline, NULL); - - if (display_name) - return g_strdup(display_name); - - if (headline) - return g_strdup(headline); - - return NULL; -} - -/** - * Obtain the tooltip text for a buddy. - * - * @param buddy Buddy to obtain tooltip text on. - * @param user_info Variable modified to have the tooltip text. - * @param full TRUE if should obtain full tooltip text. - * - */ -void -msim_tooltip_text(PurpleBuddy *buddy, PurpleNotifyUserInfo *user_info, - gboolean full) -{ - const gchar *str, *str2; - gint n; - - g_return_if_fail(buddy != NULL); - g_return_if_fail(user_info != NULL); - - if (PURPLE_BUDDY_IS_ONLINE(buddy)) - { - MsimSession *session; - - session = (MsimSession *)buddy->account->gc->proto_data; - - g_return_if_fail(MSIM_SESSION_VALID(session)); - - /* TODO: if (full), do something different */ - - /* Useful to identify the account the tooltip refers to. - * Other prpls show this. */ - str = purple_blist_node_get_string(&buddy->node, "UserName"); - if (str) - purple_notify_user_info_add_pair(user_info, _("User Name"), str); - - /* a/s/l...the vitals */ - n = purple_blist_node_get_int(&buddy->node, "Age"); - if (n) - purple_notify_user_info_add_pair(user_info, _("Age"), - g_strdup_printf("%d", n)); - - str = purple_blist_node_get_string(&buddy->node, "Gender"); - if (str) - purple_notify_user_info_add_pair(user_info, _("Gender"), str); - - str = purple_blist_node_get_string(&buddy->node, "Location"); - if (str) - purple_notify_user_info_add_pair(user_info, _("Location"), str); - - /* Other information */ - str = purple_blist_node_get_string(&buddy->node, "Headline"); - if (str) - purple_notify_user_info_add_pair(user_info, _("Headline"), str); - - str = purple_blist_node_get_string(&buddy->node, "BandName"); - str2 = purple_blist_node_get_string(&buddy->node, "SongName"); - if (str || str2) - purple_notify_user_info_add_pair(user_info, _("Song"), - g_strdup_printf("%s - %s", - str ? str : _("Unknown Artist"), - str2 ? str2 : _("Unknown Song"))); - - n = purple_blist_node_get_int(&buddy->node, "TotalFriends"); - if (n) - purple_notify_user_info_add_pair(user_info, _("Total Friends"), - g_strdup_printf("%d", n)); - - } -} - -/** Callbacks called by Purple, to access this plugin. */ -PurplePluginProtocolInfo prpl_info = -{ - /* options */ - OPT_PROTO_USE_POINTSIZE /* specify font size in sane point size */ - /* | OPT_PROTO_MAIL_CHECK - TODO: myspace will notify of mail */ - /* | OPT_PROTO_IM_IMAGE - TODO: direct images. */ - , - NULL, /* user_splits */ - NULL, /* protocol_options */ - NO_BUDDY_ICONS, /* icon_spec - TODO: eventually should add this */ - msim_list_icon, /* list_icon */ - NULL, /* list_emblems */ - msim_status_text, /* status_text */ - msim_tooltip_text, /* tooltip_text */ - msim_status_types, /* status_types */ - NULL, /* blist_node_menu */ - NULL, /* chat_info */ - NULL, /* chat_info_defaults */ - msim_login, /* login */ - msim_close, /* close */ - msim_send_im, /* send_im */ - NULL, /* set_info */ - msim_send_typing, /* send_typing */ - msim_get_info, /* get_info */ - msim_set_status, /* set_status */ - msim_set_idle, /* set_idle */ - NULL, /* change_passwd */ - msim_add_buddy, /* add_buddy */ - NULL, /* add_buddies */ - msim_remove_buddy, /* remove_buddy */ - NULL, /* remove_buddies */ - NULL, /* add_permit */ - NULL, /* add_deny */ - NULL, /* rem_permit */ - NULL, /* rem_deny */ - NULL, /* set_permit_deny */ - NULL, /* join_chat */ - NULL, /* reject chat invite */ - NULL, /* get_chat_name */ - NULL, /* chat_invite */ - NULL, /* chat_leave */ - NULL, /* chat_whisper */ - NULL, /* chat_send */ - NULL, /* keepalive */ - NULL, /* register_user */ - NULL, /* get_cb_info */ - NULL, /* get_cb_away */ - NULL, /* alias_buddy */ - NULL, /* group_buddy */ - NULL, /* rename_group */ - NULL, /* buddy_free */ - NULL, /* convo_closed */ - NULL, /* normalize */ - NULL, /* set_buddy_icon */ - NULL, /* remove_group */ - NULL, /* get_cb_real_name */ - NULL, /* set_chat_topic */ - NULL, /* find_blist_chat */ - NULL, /* roomlist_get_list */ - NULL, /* roomlist_cancel */ - NULL, /* roomlist_expand_category */ - NULL, /* can_receive_file */ - NULL, /* send_file */ - NULL, /* new_xfer */ - msim_offline_message, /* offline_message */ - NULL, /* whiteboard_prpl_ops */ - msim_send_really_raw, /* send_raw */ - NULL, /* roomlist_room_serialize */ - NULL, /* _purple_reserved1 */ - NULL, /* _purple_reserved2 */ - NULL, /* _purple_reserved3 */ - NULL /* _purple_reserved4 */ -}; - - - -/** Based on MSN's plugin info comments. */ -PurplePluginInfo info = -{ - PURPLE_PLUGIN_MAGIC, - PURPLE_MAJOR_VERSION, - PURPLE_MINOR_VERSION, - PURPLE_PLUGIN_PROTOCOL, /**< type */ - NULL, /**< ui_requirement */ - 0, /**< flags */ - NULL, /**< dependencies */ - PURPLE_PRIORITY_DEFAULT, /**< priority */ - - "prpl-myspace", /**< id */ - "MySpaceIM", /**< name */ - "0.10", /**< version */ - /** summary */ - "MySpaceIM Protocol Plugin", - /** description */ - "MySpaceIM Protocol Plugin", - "Jeff Connelly ", /**< author */ - "http://developer.pidgin.im/wiki/MySpaceIM/", /**< homepage */ - - msim_load, /**< load */ - NULL, /**< unload */ - NULL, /**< destroy */ - NULL, /**< ui_info */ - &prpl_info, /**< extra_info */ - NULL, /**< prefs_info */ - - /* msim_actions */ - NULL, - - NULL, /**< reserved1 */ - NULL, /**< reserved2 */ - NULL, /**< reserved3 */ - NULL /**< reserved4 */ -}; - - -#ifdef MSIM_SELF_TEST -/** Test functions. - * Used to test or try out the internal workings of msimprpl. If you're reading - * this code for the first time, these functions can be instructive in how - * msimprpl is architected. - */ -void -msim_test_all(void) -{ - guint failures; - - - failures = 0; - failures += msim_test_xml(); - failures += msim_test_msg(); - failures += msim_test_escaping(); - - if (failures) - { - purple_debug_info("msim", "msim_test_all HAD FAILURES: %d\n", failures); - } - else - { - purple_debug_info("msim", "msim_test_all - all tests passed!\n"); - } - exit(0); -} - -int -msim_test_xml(void) -{ - gchar *msg_text; - xmlnode *root, *n; - guint failures; - char *s; - int len; - - failures = 0; - - msg_text = "

woo!xxxyyy

"; - - purple_debug_info("msim", "msim_test_xml: msg_text=%s\n", msg_text); - - root = xmlnode_from_str(msg_text, -1); - if (!root) - { - purple_debug_info("msim", "there is no root\n"); - exit(0); - } - - purple_debug_info("msim", "root name=%s, child name=%s\n", root->name, - root->child->name); - - purple_debug_info("msim", "last child name=%s\n", root->lastchild->name); - purple_debug_info("msim", "Root xml=%s\n", - xmlnode_to_str(root, &len)); - purple_debug_info("msim", "Child xml=%s\n", - xmlnode_to_str(root->child, &len)); - purple_debug_info("msim", "Lastchild xml=%s\n", - xmlnode_to_str(root->lastchild, &len)); - purple_debug_info("msim", "Next xml=%s\n", - xmlnode_to_str(root->next, &len)); - purple_debug_info("msim", "Next data=%s\n", - xmlnode_get_data(root->next)); - purple_debug_info("msim", "Child->next xml=%s\n", - xmlnode_to_str(root->child->next, &len)); - - for (n = root->child; n; n = n->next) - { - if (n->name) - { - purple_debug_info("msim", " ** n=%s\n",n->name); - } else { - purple_debug_info("msim", " ** n data=%s\n", n->data); - } - } - - purple_debug_info("msim", "root data=%s, child data=%s, child 'h'=%s\n", - xmlnode_get_data(root), - xmlnode_get_data(root->child), - xmlnode_get_attrib(root->child, "h")); - - - for (n = root->child; - n != NULL; - n = n->next) - { - purple_debug_info("msim", "next name=%s\n", n->name); - } - - s = xmlnode_to_str(root, &len); - s[len] = 0; - - purple_debug_info("msim", "str: %s\n", s); - g_free(s); - - xmlnode_free(root); - - exit(0); - - return failures; -} - -/** Test MsimMessage for basic functionality. */ -int -msim_test_msg(void) -{ - MsimMessage *msg, *msg_cloned; - gchar *packed, *packed_expected, *packed_cloned; - guint failures; - - failures = 0; - - purple_debug_info("msim", "\n\nTesting MsimMessage\n"); - msg = msim_msg_new(FALSE); /* Create a new, empty message. */ - - /* Append some new elements. */ - msg = msim_msg_append(msg, "bx", MSIM_TYPE_BINARY, g_string_new_len(g_strdup("XXX"), 3)); - msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v1")); - msg = msim_msg_append(msg, "k1", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(42)); - msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v43")); - msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v52/xxx\\yyy")); - msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v7")); - msim_msg_dump("msg debug str=%s\n", msg); - packed = msim_msg_pack(msg); - - purple_debug_info("msim", "msg packed=%s\n", packed); - - packed_expected = "\\bx\\WFhY\\k1\\v1\\k1\\42\\k1" - "\\v43\\k1\\v52/1xxx/2yyy\\k1\\v7\\final\\"; - - if (0 != strcmp(packed, packed_expected)) - { - purple_debug_info("msim", "!!!(%d), msim_msg_pack not what expected: %s != %s\n", - ++failures, packed, packed_expected); - } - - - msg_cloned = msim_msg_clone(msg); - packed_cloned = msim_msg_pack(msg_cloned); - - purple_debug_info("msim", "msg cloned=%s\n", packed_cloned); - if (0 != strcmp(packed, packed_cloned)) - { - purple_debug_info("msim", "!!!(%d), msim_msg_pack on cloned message not equal to original: %s != %s\n", - ++failures, packed_cloned, packed); - } - - g_free(packed); - g_free(packed_cloned); - msim_msg_free(msg_cloned); - msim_msg_free(msg); - - return failures; -} - -/** Test protocol-level escaping/unescaping. */ -int -msim_test_escaping(void) -{ - guint failures; - gchar *raw, *escaped, *unescaped, *expected; - - failures = 0; - - purple_debug_info("msim", "\n\nTesting escaping\n"); - - raw = "hello/world\\hello/world"; - - escaped = msim_escape(raw); - purple_debug_info("msim", "msim_test_escaping: raw=%s, escaped=%s\n", raw, escaped); - expected = "hello/1world/2hello/1world"; - if (0 != strcmp(escaped, expected)) - { - purple_debug_info("msim", "!!!(%d), msim_escape failed: %s != %s\n", - ++failures, escaped, expected); - } - - - unescaped = msim_unescape(escaped); - g_free(escaped); - purple_debug_info("msim", "msim_test_escaping: unescaped=%s\n", unescaped); - if (0 != strcmp(raw, unescaped)) - { - purple_debug_info("msim", "!!!(%d), msim_unescape failed: %s != %s\n", - ++failures, raw, unescaped); - } - - return failures; -} -#endif - -/** Initialize plugin. */ -void -init_plugin(PurplePlugin *plugin) -{ - PurpleAccountOption *option; -#ifdef MSIM_SELF_TEST - msim_test_all(); -#endif /* MSIM_SELF_TEST */ - - /* TODO: default to automatically try different ports. Make the user be - * able to set the first port to try (like LastConnectedPort in Windows client). */ - option = purple_account_option_string_new(_("Connect server"), "server", MSIM_SERVER); - prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); - - option = purple_account_option_int_new(_("Connect port"), "port", MSIM_PORT); - prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); - - option = purple_account_option_bool_new(_("Sign on as hidden"), "hidden", FALSE); - prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); - - option = purple_account_option_bool_new(_("Show display name in status text"), "show_display_name", TRUE); - prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); - - option = purple_account_option_bool_new(_("Show headline in status text"), "show_headline", TRUE); - prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); -} - -PURPLE_INIT_PLUGIN(myspace, init_plugin, info); +/* MySpaceIM Protocol Plugin + * + * \author Jeff Connelly + * + * Copyright (C) 2007, Jeff Connelly + * + * Based on Purple's "C Plugin HOWTO" hello world example. + * + * Code also drawn from mockprpl: + * http://snarfed.org/space/purple+mock+protocol+plugin + * Copyright (C) 2004-2007, Ryan Barrett + * + * and some constructs also based on existing Purple plugins, which are: + * Copyright (C) 2003, Robbert Haarman + * Copyright (C) 2003, Ethan Blanton + * Copyright (C) 2000-2003, Rob Flynn + * Copyright (C) 1998-1999, Mark Spencer + * + * 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 + */ + +#define PURPLE_PLUGIN + +#include "message.h" +#include "persist.h" +#include "myspace.h" + +/** + * Load the plugin. + */ +gboolean +msim_load(PurplePlugin *plugin) +{ + /* If compiled to use RC4 from libpurple, check if it is really there. */ + if (!purple_ciphers_find_cipher("rc4")) + { + purple_debug_error("msim", "rc4 not in libpurple, but it is required - not loading MySpaceIM plugin!\n"); + purple_notify_error(plugin, _("Missing Cipher"), + _("The RC4 cipher could not be found"), + _("Upgrade " + "to a libpurple with RC4 support (>= 2.0.1). MySpaceIM " + "plugin will not be loaded.")); + return FALSE; + } + return TRUE; +} + +/** + * Get possible user status types. Based on mockprpl. + * + * @return GList of status types. + */ +GList * +msim_status_types(PurpleAccount *acct) +{ + GList *types; + PurpleStatusType *status; + + purple_debug_info("myspace", "returning status types\n"); + + types = NULL; + + status = purple_status_type_new_full(PURPLE_STATUS_AVAILABLE, NULL, NULL, FALSE, TRUE, FALSE); + types = g_list_append(types, status); + + status = purple_status_type_new_full(PURPLE_STATUS_AWAY, NULL, NULL, FALSE, TRUE, FALSE); + types = g_list_append(types, status); + + status = purple_status_type_new_full(PURPLE_STATUS_OFFLINE, NULL, NULL, FALSE, TRUE, FALSE); + types = g_list_append(types, status); + + status = purple_status_type_new_full(PURPLE_STATUS_INVISIBLE, NULL, NULL, FALSE, TRUE, FALSE); + types = g_list_append(types, status); + + return types; +} + +/** + * Return the icon name for a buddy and account. + * + * @param acct The account to find the icon for, or NULL for protocol icon. + * @param buddy The buddy to find the icon for, or NULL for the account icon. + * + * @return The base icon name string. + */ +const gchar * +msim_list_icon(PurpleAccount *acct, PurpleBuddy *buddy) +{ + /* Use a MySpace icon submitted by hbons at + * http://developer.pidgin.im/wiki/MySpaceIM. */ + return "myspace"; +} + +/* Replacement codes to be replaced with associated replacement text, + * used for protocol message escaping / unescaping. */ +static gchar* msim_replacement_code[] = { "/1", "/2", NULL }; +static gchar* msim_replacement_text[] = { "/", "\\", NULL }; + +/** + * Unescape or escape a protocol message. + * + * @param msg The message to be unescaped or escaped. WILL BE FREED. + * @param escape TRUE to escape, FALSE to unescape. + * + * @return The unescaped or escaped message. Caller must g_free(). + */ +gchar * +msim_unescape_or_escape(gchar *msg, gboolean escape) +{ + gchar *tmp, *code, *text; + guint i; + + /* Replace each code in msim_replacement_code with + * corresponding entry in msim_replacement_text. */ + for (i = 0; (code = msim_replacement_code[i]) + && (text = msim_replacement_text[i]); ++i) + { + if (escape) + { + tmp = str_replace(msg, text, code); + } + else + { + tmp = str_replace(msg, code, text); + } + g_free(msg); + msg = tmp; + } + + return msg; +} + +/** + * Escape a protocol message. + * + * @return The escaped message. Caller must g_free(). + */ +gchar * +msim_escape(const gchar *msg) +{ + return msim_unescape_or_escape(g_strdup(msg), TRUE); +} + +gchar * +msim_unescape(const gchar *msg) +{ + return msim_unescape_or_escape(g_strdup(msg), FALSE); +} + +/** + * Replace 'old' with 'new' in 'str'. + * + * @param str The original string. + * @param old The substring of 'str' to replace. + * @param new The replacement for 'old' within 'str'. + * + * @return A _new_ string, based on 'str', with 'old' replaced + * by 'new'. Must be g_free()'d by caller. + * + * This string replace method is based on + * http://mail.gnome.org/archives/gtk-app-devel-list/2000-July/msg00201.html + * + */ +gchar * +str_replace(const gchar *str, const gchar *old, const gchar *new) +{ + gchar **items; + gchar *ret; + + items = g_strsplit(str, old, -1); + ret = g_strjoinv(new, items); + g_free(items); + return ret; +} + +#ifdef MSIM_DEBUG_MSG +void +print_hash_item(gpointer key, gpointer value, gpointer user_data) +{ + purple_debug_info("msim", "%s=%s\n", (gchar *)key, (gchar *)value); +} +#endif + +/** + * Send raw data (given as a NUL-terminated string) to the server. + * + * @param session + * @param msg The raw data to send, in a NUL-terminated string. + * + * @return TRUE if succeeded, FALSE if not. + * + */ +gboolean +msim_send_raw(MsimSession *session, const gchar *msg) +{ + purple_debug_info("msim", "msim_send_raw: writing <%s>\n", msg); + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + return msim_send_really_raw(session->gc, msg, strlen(msg)) == + strlen(msg); +} + +/** Send raw data to the server, possibly with embedded NULs. + * + * Used in prpl_info struct, so that plugins can have the most possible + * control of what is sent over the connection. Inside this prpl, + * msim_send_raw() is used, since it sends NUL-terminated strings (easier). + * + * @param gc PurpleConnection + * @param buf Buffer to send + * @param total_bytes Size of buffer to send + * + * @return Bytes successfully sent, or -1 on error. + */ +int +msim_send_really_raw(PurpleConnection *gc, const char *buf, int total_bytes) +{ + int total_bytes_sent; + MsimSession *session; + + g_return_val_if_fail(gc != NULL, -1); + g_return_val_if_fail(buf != NULL, -1); + g_return_val_if_fail(total_bytes >= 0, -1); + + session = (MsimSession *)(gc->proto_data); + + g_return_val_if_fail(MSIM_SESSION_VALID(session), -1); + + /* Loop until all data is sent, or a failure occurs. */ + total_bytes_sent = 0; + do + { + int bytes_sent; + + bytes_sent = send(session->fd, buf + total_bytes_sent, + total_bytes - total_bytes_sent, 0); + + if (bytes_sent < 0) + { + purple_debug_info("msim", "msim_send_raw(%s): send() failed: %s\n", + buf, g_strerror(errno)); + return total_bytes_sent; + } + total_bytes_sent += bytes_sent; + + } while(total_bytes_sent < total_bytes); + + return total_bytes_sent; +} + + +/** + * Start logging in to the MSIM servers. + * + * @param acct Account information to use to login. + */ +void +msim_login(PurpleAccount *acct) +{ + PurpleConnection *gc; + const gchar *host; + int port; + + g_return_if_fail(acct != NULL); + g_return_if_fail(acct->username != NULL); + + purple_debug_info("myspace", "logging in %s\n", acct->username); + + gc = purple_account_get_connection(acct); + gc->proto_data = msim_session_new(acct); + + /* Passwords are limited in length. */ + if (strlen(acct->password) > MSIM_MAX_PASSWORD_LENGTH) + { + gchar *str; + + str = g_strdup_printf( + _("Sorry, passwords over %d characters in length (yours is " + "%d) are not supported by the MySpaceIM plugin."), + MSIM_MAX_PASSWORD_LENGTH, + (int)strlen(acct->password)); + + /* Notify an error message also, because this is important! */ + purple_notify_error(acct, g_strdup(_("MySpaceIM Error")), str, NULL); + + purple_connection_error(gc, str); + + g_free(str); + } + + /* 1. connect to server */ + purple_connection_update_progress(gc, _("Connecting"), + 0, /* which connection step this is */ + 4); /* total number of steps */ + + host = purple_account_get_string(acct, "server", MSIM_SERVER); + port = purple_account_get_int(acct, "port", MSIM_PORT); + + /* From purple.sf.net/api: + * """Note that this function name can be misleading--although it is called + * "proxy connect," it is used for establishing any outgoing TCP connection, + * whether through a proxy or not.""" */ + + /* Calls msim_connect_cb when connected. */ + if (purple_proxy_connect(gc, acct, host, port, msim_connect_cb, gc) == NULL) + { + /* TODO: try other ports if in auto mode, then save + * working port and try that first next time. */ + purple_connection_error(gc, _("Couldn't create socket")); + return; + } +} + +/** + * Process a login challenge, sending a response. + * + * @param session + * @param msg Login challenge message. + * + * @return TRUE if successful, FALSE if not + */ +gboolean +msim_login_challenge(MsimSession *session, MsimMessage *msg) +{ + PurpleAccount *account; + const gchar *response; + guint response_len; + gchar *nc; + gsize nc_len; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + g_return_val_if_fail(msim_msg_get_binary(msg, "nc", &nc, &nc_len), FALSE); + + account = session->account; + + g_return_val_if_fail(account != NULL, FALSE); + + purple_connection_update_progress(session->gc, _("Reading challenge"), 1, 4); + + purple_debug_info("msim", "nc is %d bytes, decoded\n", nc_len); + + if (nc_len != 0x40) + { + purple_debug_info("msim", "bad nc length: %x != 0x40\n", nc_len); + purple_connection_error(session->gc, _("Unexpected challenge length from server")); + return FALSE; + } + + purple_connection_update_progress(session->gc, _("Logging in"), 2, 4); + + response = msim_compute_login_response(nc, account->username, account->password, &response_len); + + g_free(nc); + + return msim_send(session, + "login2", MSIM_TYPE_INTEGER, MSIM_AUTH_ALGORITHM, + /* This is actually user's email address. */ + "username", MSIM_TYPE_STRING, g_strdup(account->username), + /* GString and gchar * response will be freed in msim_msg_free() in msim_send(). */ + "response", MSIM_TYPE_BINARY, g_string_new_len(response, response_len), + "clientver", MSIM_TYPE_INTEGER, MSIM_CLIENT_VERSION, + "reconn", MSIM_TYPE_INTEGER, 0, + "status", MSIM_TYPE_INTEGER, 100, + "id", MSIM_TYPE_INTEGER, 1, + NULL); +} + +/** + * Compute the base64'd login challenge response based on username, password, nonce, and IPs. + * + * @param nonce The base64 encoded nonce ('nc') field from the server. + * @param email User's email address (used as login name). + * @param password User's cleartext password. + * @param response_len Will be written with response length. + * + * @return Binary login challenge response, ready to send to the server. + * Must be g_free()'d when finished. NULL if error. + */ +const gchar * +msim_compute_login_response(const gchar nonce[2 * NONCE_SIZE], + const gchar *email, const gchar *password, guint *response_len) +{ + PurpleCipherContext *key_context; + PurpleCipher *sha1; + PurpleCipherContext *rc4; + + guchar hash_pw[HASH_SIZE]; + guchar key[HASH_SIZE]; + gchar *password_utf16le; + guchar *data; + guchar *data_out; + size_t data_len, data_out_len; + gsize conv_bytes_read, conv_bytes_written; + GError *conv_error; +#ifdef MSIM_DEBUG_LOGIN_CHALLENGE + int i; +#endif + + g_return_val_if_fail(nonce != NULL, NULL); + g_return_val_if_fail(email != NULL, NULL); + g_return_val_if_fail(password != NULL, NULL); + g_return_val_if_fail(response_len != NULL, NULL); + + /* Convert ASCII password to UTF16 little endian */ + purple_debug_info("msim", "converting password to UTF-16LE\n"); + conv_error = NULL; + password_utf16le = g_convert(password, -1, "UTF-16LE", "UTF-8", + &conv_bytes_read, &conv_bytes_written, &conv_error); + + g_return_val_if_fail(conv_bytes_read == strlen(password), NULL); + + if (conv_error != NULL) + { + purple_debug_error("msim", + "g_convert password UTF8->UTF16LE failed: %s", + conv_error->message); + g_error_free(conv_error); + return NULL; + } + + /* Compute password hash */ + purple_cipher_digest_region("sha1", (guchar *)password_utf16le, + conv_bytes_written, sizeof(hash_pw), hash_pw, NULL); + g_free(password_utf16le); + +#ifdef MSIM_DEBUG_LOGIN_CHALLENGE + purple_debug_info("msim", "pwhash = "); + for (i = 0; i < sizeof(hash_pw); i++) + purple_debug_info("msim", "%.2x ", hash_pw[i]); + purple_debug_info("msim", "\n"); +#endif + + /* key = sha1(sha1(pw) + nonce2) */ + sha1 = purple_ciphers_find_cipher("sha1"); + key_context = purple_cipher_context_new(sha1, NULL); + purple_cipher_context_append(key_context, hash_pw, HASH_SIZE); + purple_cipher_context_append(key_context, (guchar *)(nonce + NONCE_SIZE), NONCE_SIZE); + purple_cipher_context_digest(key_context, sizeof(key), key, NULL); + +#ifdef MSIM_DEBUG_LOGIN_CHALLENGE + purple_debug_info("msim", "key = "); + for (i = 0; i < sizeof(key); i++) + { + purple_debug_info("msim", "%.2x ", key[i]); + } + purple_debug_info("msim", "\n"); +#endif + + rc4 = purple_cipher_context_new_by_name("rc4", NULL); + + /* Note: 'key' variable is 0x14 bytes (from SHA-1 hash), + * but only first 0x10 used for the RC4 key. */ + purple_cipher_context_set_option(rc4, "key_len", (gpointer)0x10); + purple_cipher_context_set_key(rc4, key); + + /* TODO: obtain IPs of network interfaces */ + + /* rc4 encrypt: + * nonce1+email+IP list */ + + data_len = NONCE_SIZE + strlen(email) + MSIM_LOGIN_IP_LIST_LEN; + data = g_new0(guchar, data_len); + memcpy(data, nonce, NONCE_SIZE); + memcpy(data + NONCE_SIZE, email, strlen(email)); + memcpy(data + NONCE_SIZE + strlen(email), MSIM_LOGIN_IP_LIST, MSIM_LOGIN_IP_LIST_LEN); + + data_out = g_new0(guchar, data_len); + + purple_cipher_context_encrypt(rc4, (const guchar *)data, + data_len, data_out, &data_out_len); + purple_cipher_context_destroy(rc4); + + g_assert(data_out_len == data_len); + +#ifdef MSIM_DEBUG_LOGIN_CHALLENGE + purple_debug_info("msim", "response=<%s>\n", data_out); +#endif + + *response_len = data_out_len; + + return (const gchar *)data_out; +} + +/** + * Schedule an IM to be sent once the user ID is looked up. + * + * @param gc Connection. + * @param who A user id, email, or username to send the message to. + * @param message Instant message text to send. + * @param flags Flags. + * + * @return 1 if successful or postponed, -1 if failed + * + * Allows sending to a user by username, email address, or userid. If + * a username or email address is given, the userid must be looked up. + * This function does that by calling msim_postprocess_outgoing(). + */ +int +msim_send_im(PurpleConnection *gc, const gchar *who, const gchar *message, + PurpleMessageFlags flags) +{ + MsimSession *session; + + g_return_val_if_fail(gc != NULL, -1); + g_return_val_if_fail(who != NULL, -1); + g_return_val_if_fail(message != NULL, -1); + + /* 'flags' has many options, not used here. */ + + session = (MsimSession *)gc->proto_data; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), -1); + + if (msim_send_bm(session, who, message, MSIM_BM_INSTANT)) + { + /* Return 1 to have Purple show this IM as being sent, 0 to not. I always + * return 1 even if the message could not be sent, since I don't know if + * it has failed yet--because the IM is only sent after the userid is + * retrieved from the server (which happens after this function returns). + */ + /* TODO: maybe if message is delayed, don't echo to conv window, + * but do echo it to conv window manually once it is actually + * sent? Would be complicated. */ + return 1; + } else { + return -1; + } + /* + * In MySpace, you login with your email address, but don't talk to other + * users using their email address. So there is currently an asymmetry in the + * IM windows when using this plugin: + * + * you@example.com: hello + * some_other_user: what's going on? + * you@example.com: just coding a prpl + * + * TODO: Make the sent IM's appear as from the user's username, instead of + * their email address. Purple uses the login (in MSIM, the email)--change this. + */ +} + +/** Send a buddy message of a given type. + * + * @param session + * @param who Username to send message to. + * @param text Message text to send. Not freed; will be copied. + * @param type A MSIM_BM_* constant. + * + * @return TRUE if success, FALSE if fail. + * + * Buddy messages ('bm') include instant messages, action messages, status messages, etc. + * + */ +gboolean +msim_send_bm(MsimSession *session, const gchar *who, const gchar *text, + int type) +{ + gboolean rc; + MsimMessage *msg; + const gchar *from_username; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(who != NULL, FALSE); + g_return_val_if_fail(text != NULL, FALSE); + + from_username = session->account->username; + + purple_debug_info("msim", "sending %d message from %s to %s: %s\n", + type, from_username, who, text); + + msg = msim_msg_new(TRUE, + "bm", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(type), + "sesskey", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(session->sesskey), + /* 't' will be inserted here */ + "cv", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(MSIM_CLIENT_VERSION), + "msg", MSIM_TYPE_STRING, g_strdup(text), + NULL); + + rc = msim_postprocess_outgoing(session, msg, who, "t", "cv"); + + msim_msg_free(msg); + + return rc; +} + + +#ifdef MSIM_FONT_SIZE_WORKS +/** Convert a msim markup font height to points. */ +static guint +msim_font_height_to_point(guint height) +{ + /* See also: libpurple/protocols/bonjour/jabber.c + * _font_size_ichat_to_purple */ + switch (height) + { + case 11: return 8; + case 12: return 9; + case 13: return 10; + case 14: + case 15: return 11; + case 16: return 12; + case 17: + case 18: return 13; + case 19: return 14; + case 20: return 15; + case 21: return 16; + default: return 12; + } +} +#endif + +/** Convert the msim markup (font) tag into HTML. */ +static void +msim_markup_f_to_html(xmlnode *root, gchar **begin, gchar **end) +{ + const gchar *face, *height_str, *decor_str; + GString *gs_end, *gs_begin; + guint decor, height; + + face = xmlnode_get_attrib(root, "f"); + height_str = xmlnode_get_attrib(root, "h"); + decor_str = xmlnode_get_attrib(root, "s"); + + if (height_str) + height = atol(height_str); + else + height = 12; + + if (decor_str) + decor = atol(decor_str); + else + decor = 0; + + gs_begin = g_string_new(""); +#ifdef MSIM_FONT_SIZE_WORKS + /* TODO: get font size working */ + if (!face) + g_string_printf(gs_begin, "", + msim_font_height_to_point(height)); + else + g_string_printf(gs_begin, "", face, + msim_font_height_to_point(height)); +#else + if (face) + { + g_string_printf(gs_begin, "", face); + } else { + g_string_printf(gs_begin, ""); + } +#endif + + + /* No support for font-size CSS? */ + /* g_string_printf(gs_begin, "", face, + msim_font_height_to_point(height)); */ + + gs_end = g_string_new(""); + + if (decor & MSIM_TEXT_BOLD) + { + g_string_append(gs_begin, ""); + g_string_prepend(gs_end, ""); + } + + if (decor & MSIM_TEXT_ITALICS) + { + g_string_append(gs_begin, ""); + g_string_append(gs_end, ""); + } + + if (decor & MSIM_TEXT_UNDERLINE) + { + g_string_append(gs_begin, ""); + g_string_append(gs_end, ""); + } + + + *begin = gs_begin->str; + *end = gs_end->str; +} + +/** Convert a msim markup color to a color suitable for libpurple. + * + * @param msim Either a color name, or an rgb(x,y,z) code. + * + * @return A new string, either a color name or #rrggbb code. Must g_free(). + */ +static char * +msim_color_to_purple(const char *msim) +{ + guint red, green, blue; + + if (!msim) + return g_strdup("black"); + + if (sscanf(msim, "rgb(%d,%d,%d)", &red, &green, &blue) != 3) + { + /* Color name. */ + return g_strdup(msim); + } + /* TODO: rgba (alpha). */ + + return g_strdup_printf("#%.2x%.2x%.2x", red, green, blue); +} + +/** Convert the msim markup

(paragraph) tag into HTML. */ +static void +msim_markup_p_to_html(xmlnode *root, gchar **begin, gchar **end) +{ + /* Just pass through unchanged. + * + * Note: attributes currently aren't passed, if there are any. */ + *begin = g_strdup("

"); + *end = g_strdup("

"); +} + +/** Convert the msim markup tag (text color) into HTML. TODO: Test */ +static void +msim_markup_c_to_html(xmlnode *root, gchar **begin, gchar **end) +{ + const gchar *color; + gchar *purple_color; + + color = xmlnode_get_attrib(root, "v"); + if (!color) + { + purple_debug_info("msim", "msim_markup_c_to_html: tag w/o v attr"); + *begin = g_strdup(""); + *end = g_strdup(""); + /* TODO: log as unrecognized */ + return; + } + + purple_color = msim_color_to_purple(color); + + *begin = g_strdup_printf("", purple_color); + + g_free(purple_color); + + /* *begin = g_strdup_printf("", color); */ + *end = g_strdup(""); +} + +/** Convert the msim markup tag (background color) into HTML. TODO: Test */ +static void +msim_markup_b_to_html(xmlnode *root, gchar **begin, gchar **end) +{ + const gchar *color; + gchar *purple_color; + + color = xmlnode_get_attrib(root, "v"); + if (!color) + { + *begin = g_strdup(""); + *end = g_strdup(""); + purple_debug_info("msim", "msim_markup_b_to_html: w/o v attr"); + /* TODO: log as unrecognized. */ + return; + } + + purple_color = msim_color_to_purple(color); + + /* TODO: find out how to set background color. */ + *begin = g_strdup_printf("", + purple_color); + g_free(purple_color); + + *end = g_strdup("

"); +} + +/** Convert the msim markup tag (emoticon image) into HTML. TODO: Test */ +static void +msim_markup_i_to_html(xmlnode *root, gchar **begin, gchar **end) +{ + const gchar *name; + + name = xmlnode_get_attrib(root, "n"); + if (!name) + { + purple_debug_info("msim", "msim_markup_i_to_html: w/o n"); + *begin = g_strdup(""); + *end = g_strdup(""); + /* TODO: log as unrecognized */ + return; + } + + /* TODO: Support these emoticons: + * + * bigsmile growl mad scared tongue devil happy messed sidefrown upset + * frazzled heart nerd sinister wink geek laugh oops smirk worried + * googles mohawk pirate straight kiss + */ + + *begin = g_strdup_printf("", name); + *end = g_strdup("

"); +} + + +/** Convert an xmlnode of msim markup to an HTML string. + * @return An HTML string. Caller frees. + */ +static gchar * +msim_markup_xmlnode_to_html(xmlnode *root) +{ + xmlnode *node; + gchar *begin, *inner, *end; + gchar *final; + + if (!root || !root->name) + return g_strdup(""); + + purple_debug_info("msim", "msim_markup_xmlnode_to_html: got root=%s\n", + root->name); + + begin = inner = end = NULL; + + if (!strcmp(root->name, "f")) + { + msim_markup_f_to_html(root, &begin, &end); + } else if (!strcmp(root->name, "p")) { + msim_markup_p_to_html(root, &begin, &end); + } else if (!strcmp(root->name, "c")) { + msim_markup_c_to_html(root, &begin, &end); + } else if (!strcmp(root->name, "b")) { + msim_markup_b_to_html(root, &begin, &end); + } else if (!strcmp(root->name, "i")) { + msim_markup_i_to_html(root, &begin, &end); + } else { + purple_debug_info("msim", "msim_markup_xmlnode_to_html: " + "unknown tag name=%s, ignoring", root->name); + begin = g_strdup(""); + end = g_strdup(""); + } + + /* Loop over all child nodes. */ + for (node = root->child; node != NULL; node = node->next) + { + switch (node->type) + { + case XMLNODE_TYPE_ATTRIB: + /* Attributes handled above. */ + break; + + case XMLNODE_TYPE_TAG: + /* A tag or tag with attributes. Recursively descend. */ + inner = msim_markup_xmlnode_to_html(node); + g_return_val_if_fail(inner != NULL, NULL); + + purple_debug_info("msim", " ** node name=%s\n", node->name); + break; + + case XMLNODE_TYPE_DATA: + /* Literal text. */ + inner = g_new0(char, node->data_sz + 1); + strncpy(inner, node->data, node->data_sz); + inner[node->data_sz + 1] = 0; + + purple_debug_info("msim", " ** node data=%s\n", inner); + break; + + default: + purple_debug_info("msim", + "msim_markup_xmlnode_to_html: strange node\n"); + inner = g_strdup(""); + } + } + + final = g_strconcat(begin, inner, end, NULL); + g_free(begin); + g_free(inner); + g_free(end); + + purple_debug_info("msim", "msim_markup_xmlnode_to_gtkhtml: RETURNING %s\n", + final); + + return final; +} + +/** Convert MySpaceIM markup to Purple (HTML) markup. + * + * @return Purple markup string, must be g_free()'d. */ +gchar * +msim_markup_to_html(const gchar *raw) +{ + xmlnode *root; + gchar *str; + + root = xmlnode_from_str(raw, -1); + if (!root) + { + purple_debug_info("msim", "msim_markup_to_html: couldn't parse " + "%s as XML, returning raw\n", raw); + return g_strdup(raw); + } + + str = msim_markup_xmlnode_to_html(root); + purple_debug_info("msim", "msim_markup_to_html: returning %s\n", str); + + xmlnode_free(root); + + return g_strdup(str); +} + +/** + * Handle an incoming instant message. + * + * @param session The session + * @param msg Message from the server, containing 'f' (userid from) and 'msg'. + * Should also contain username in _username from preprocessing. + * + * @return TRUE if successful. + */ +gboolean +msim_incoming_im(MsimSession *session, MsimMessage *msg) +{ + gchar *username, *msg_msim_markup, *msg_purple_markup; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + username = msim_msg_get_string(msg, "_username"); + g_return_val_if_fail(username != NULL, FALSE); + + msg_msim_markup = msim_msg_get_string(msg, "msg"); + g_return_val_if_fail(msg_msim_markup != NULL, FALSE); + + msg_purple_markup = msim_markup_to_html(msg_msim_markup); + g_free(msg_msim_markup); + + serv_got_im(session->gc, username, msg_purple_markup, + PURPLE_MESSAGE_RECV, time(NULL)); + + g_free(username); + g_free(msg_purple_markup); + + return TRUE; +} + +/** + * Process unrecognized information. + * + * @param session + * @param msg An MsimMessage that was unrecognized, or NULL. + * @param note Information on what was unrecognized, or NULL. + */ +void +msim_unrecognized(MsimSession *session, MsimMessage *msg, gchar *note) +{ + /* TODO: Some more context, outwardly equivalent to a backtrace, + * for helping figure out what this msg is for. What was going on? + * But not too much information so that a user + * posting this dump reveals confidential information. + */ + + /* TODO: dump unknown msgs to file, so user can send them to me + * if they wish, to help add support for new messages (inspired + * by Alexandr Shutko, who maintains OSCAR protocol documentation). */ + + purple_debug_info("msim", "Unrecognized data on account for %s\n", + session->account->username); + if (note) + { + purple_debug_info("msim", "(Note: %s)\n", note); + } + + if (msg) + { + msim_msg_dump("Unrecognized message dump: %s\n", msg); + } +} + +/** + * Handle an incoming action message. + * + * @param session + * @param msg + * + * @return TRUE if successful. + * + * UNTESTED + */ +gboolean +msim_incoming_action(MsimSession *session, MsimMessage *msg) +{ + gchar *msg_text, *username; + gboolean rc; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + msg_text = msim_msg_get_string(msg, "msg"); + g_return_val_if_fail(msg_text != NULL, FALSE); + + username = msim_msg_get_string(msg, "_username"); + g_return_val_if_fail(username != NULL, FALSE); + + purple_debug_info("msim", "msim_incoming_action: action <%s> from <%d>\n", + msg_text, username); + + if (strcmp(msg_text, "%typing%") == 0) + { + /* TODO: find out if msim repeatedly sends typing messages, so we can + * give it a timeout. Right now, there does seem to be an inordinately + * amount of time between typing stopped-typing notifications. */ + serv_got_typing(session->gc, username, 0, PURPLE_TYPING); + rc = TRUE; + } else if (strcmp(msg_text, "%stoptyping%") == 0) { + serv_got_typing_stopped(session->gc, username); + rc = TRUE; + } else { + msim_unrecognized(session, msg, + "got to msim_incoming_action but unrecognized value for 'msg'"); + rc = FALSE; + } + + g_free(msg_text); + g_free(username); + + return rc; +} + +/** + * Handle when our user starts or stops typing to another user. + * + * @param gc + * @param name The buddy name to which our user is typing to + * @param state PURPLE_TYPING, PURPLE_TYPED, PURPLE_NOT_TYPING + * + * @return 0 + */ +unsigned int +msim_send_typing(PurpleConnection *gc, const gchar *name, + PurpleTypingState state) +{ + const gchar *typing_str; + MsimSession *session; + + g_return_val_if_fail(gc != NULL, 0); + g_return_val_if_fail(name != NULL, 0); + + session = (MsimSession *)gc->proto_data; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); + + switch (state) + { + case PURPLE_TYPING: + typing_str = "%typing%"; + break; + + case PURPLE_TYPED: + case PURPLE_NOT_TYPING: + default: + typing_str = "%stoptyping%"; + break; + } + + purple_debug_info("msim", "msim_send_typing(%s): %d (%s)\n", name, state, typing_str); + msim_send_bm(session, name, typing_str, MSIM_BM_ACTION); + return 0; +} + +/** Callback for msim_get_info(), for when user info is received. */ +void +msim_get_info_cb(MsimSession *session, MsimMessage *user_info_msg, gpointer data) +{ + GHashTable *body; + gchar *body_str; + MsimMessage *msg; + gchar *user; + PurpleNotifyUserInfo *user_info; + PurpleBuddy *buddy; + const gchar *str, *str2; + + g_return_if_fail(MSIM_SESSION_VALID(session)); + + /* Get user{name,id} from msim_get_info, passed as an MsimMessage for + orthogonality. */ + msg = (MsimMessage *)data; + g_return_if_fail(msg != NULL); + + user = msim_msg_get_string(msg, "user"); + if (!user) + { + purple_debug_info("msim", "msim_get_info_cb: no 'user' in msg"); + return; + } + + msim_msg_free(msg); + purple_debug_info("msim", "msim_get_info_cb: got for user: %s\n", user); + + body_str = msim_msg_get_string(user_info_msg, "body"); + g_return_if_fail(body_str != NULL); + body = msim_parse_body(body_str); + g_free(body_str); + + buddy = purple_find_buddy(session->account, user); + /* Note: don't assume buddy is non-NULL; will be if lookup random user + * not on blist. */ + + user_info = purple_notify_user_info_new(); + + /* Identification */ + purple_notify_user_info_add_pair(user_info, _("User"), user); + + /* note: g_hash_table_lookup does not create a new string! */ + str = g_hash_table_lookup(body, "UserID"); + if (str) + purple_notify_user_info_add_pair(user_info, _("User ID"), + g_strdup(str)); + + /* a/s/l...the vitals */ + str = g_hash_table_lookup(body, "Age"); + if (str) + purple_notify_user_info_add_pair(user_info, _("Age"), g_strdup(str)); + + str = g_hash_table_lookup(body, "Gender"); + if (str) + purple_notify_user_info_add_pair(user_info, _("Gender"), g_strdup(str)); + + str = g_hash_table_lookup(body, "Location"); + if (str) + purple_notify_user_info_add_pair(user_info, _("Location"), + g_strdup(str)); + + /* Other information */ + + /* Headline comes from buddy status messages */ + if (buddy) + { + str = purple_blist_node_get_string(&buddy->node, "Headline"); + if (str) + purple_notify_user_info_add_pair(user_info, "Headline", str); + } + + + str = g_hash_table_lookup(body, "BandName"); + str2 = g_hash_table_lookup(body, "SongName"); + if (str || str2) + { + purple_notify_user_info_add_pair(user_info, _("Song"), + g_strdup_printf("%s - %s", + str ? str : "Unknown Artist", + str2 ? str2 : "Unknown Song")); + } + + + /* Total friends only available if looked up by uid, not username. */ + str = g_hash_table_lookup(body, "TotalFriends"); + if (str) + purple_notify_user_info_add_pair(user_info, _("Total Friends"), + g_strdup(str)); + + purple_notify_userinfo(session->gc, user, user_info, NULL, NULL); + purple_debug_info("msim", "msim_get_info_cb: username=%s\n", user); + //purple_notify_user_info_destroy(user_info); + /* Do not free username, since it will be used by user_info. */ + + //g_hash_table_destroy(body); +} + +/** Retrieve a user's profile. */ +void +msim_get_info(PurpleConnection *gc, const gchar *user) +{ + PurpleBuddy *buddy; + MsimSession *session; + guint uid; + gchar *user_to_lookup; + MsimMessage *user_msg; + + g_return_if_fail(gc != NULL); + g_return_if_fail(user != NULL); + + session = (MsimSession *)gc->proto_data; + + g_return_if_fail(MSIM_SESSION_VALID(session)); + + /* Obtain uid of buddy. */ + buddy = purple_find_buddy(session->account, user); + if (buddy) + { + uid = purple_blist_node_get_int(&buddy->node, "UserID"); + if (!uid) + { + PurpleNotifyUserInfo *user_info; + + user_info = purple_notify_user_info_new(); + purple_notify_user_info_add_pair(user_info, NULL, + _("This buddy appears to not have a userid stored in the buddy list, can't look up. Is the user really on the buddy list?")); + + purple_notify_userinfo(session->gc, user, user_info, NULL, NULL); + purple_notify_user_info_destroy(user_info); + return; + } + + user_to_lookup = g_strdup_printf("%d", uid); + } else { + + /* Looking up buddy not on blist. Lookup by whatever user entered. */ + user_to_lookup = g_strdup(user); + } + + /* Pass the username to msim_get_info_cb(), because since we lookup + * by userid, the userinfo message will only contain the uid (not + * the username). + */ + user_msg = msim_msg_new(TRUE, + "user", MSIM_TYPE_STRING, g_strdup(user), + NULL); + purple_debug_info("msim", "msim_get_info, setting up lookup, user=%s\n", user); + + msim_lookup_user(session, user_to_lookup, msim_get_info_cb, user_msg); + + g_free(user_to_lookup); +} + +/** Set your status - callback for when user manually sets it. */ +void +msim_set_status(PurpleAccount *account, PurpleStatus *status) +{ + PurpleStatusType *type; + MsimSession *session; + guint status_code; + + session = (MsimSession *)account->gc->proto_data; + + g_return_if_fail(MSIM_SESSION_VALID(session)); + + type = purple_status_get_type(status); + + switch (purple_status_type_get_primitive(type)) + { + case PURPLE_STATUS_AVAILABLE: + status_code = MSIM_STATUS_CODE_ONLINE; + break; + + case PURPLE_STATUS_INVISIBLE: + status_code = MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN; + break; + + case PURPLE_STATUS_AWAY: + status_code = MSIM_STATUS_CODE_AWAY; + break; + + default: + purple_debug_info("msim", "msim_set_status: unknown " + "status interpreting as online"); + status_code = MSIM_STATUS_CODE_ONLINE; + break; + } + + + msim_set_status_code(session, status_code); +} + +/** Go idle. */ +void +msim_set_idle(PurpleConnection *gc, int time) +{ + MsimSession *session; + + g_return_if_fail(gc != NULL); + + session = (MsimSession *)gc->proto_data; + + g_return_if_fail(MSIM_SESSION_VALID(session)); + + if (time == 0) + { + /* Going back from idle. In msim, idle is mutually exclusive + * from the other states (you can only be away or idle, but not + * both, for example), so by going non-idle I go online. + */ + msim_set_status_code(session, MSIM_STATUS_CODE_ONLINE); + } else { + /* msim doesn't support idle time, so just go idle */ + msim_set_status_code(session, MSIM_STATUS_CODE_IDLE); + } +} + +/** Set status using an MSIM_STATUS_CODE_* value. (TODO: also set message) */ +void +msim_set_status_code(MsimSession *session, guint status_code) +{ + g_return_if_fail(MSIM_SESSION_VALID(session)); + + if (!msim_send(session, + "status", MSIM_TYPE_INTEGER, status_code, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + "statstring", MSIM_TYPE_STRING, g_strdup(""), + "locstring", MSIM_TYPE_STRING, g_strdup(""))) + { + purple_debug_info("msim", "msim_set_status: failed to set status"); + } + +} + +/** After a uid is resolved to username, tag it with the username and submit for processing. + * + * @param session + * @param userinfo Response messsage to resolving request. + * @param data MsimMessage *, the message to attach information to. + */ +static void +msim_incoming_resolved(MsimSession *session, MsimMessage *userinfo, + gpointer data) +{ + gchar *body_str; + GHashTable *body; + gchar *username; + MsimMessage *msg; + + g_return_if_fail(MSIM_SESSION_VALID(session)); + g_return_if_fail(userinfo != NULL); + + body_str = msim_msg_get_string(userinfo, "body"); + g_return_if_fail(body_str != NULL); + body = msim_parse_body(body_str); + g_return_if_fail(body != NULL); + g_free(body_str); + + username = g_hash_table_lookup(body, "UserName"); + g_return_if_fail(username != NULL); + + msg = (MsimMessage *)data; + g_return_if_fail(msg != NULL); + + /* Special elements name beginning with '_', we'll use internally within the + * program (did not come from the wire). */ + msg = msim_msg_append(msg, "_username", MSIM_TYPE_STRING, g_strdup(username)); + + msim_process(session, msg); + + /* TODO: Free copy cloned from msim_preprocess_incoming(). */ + //XXX msim_msg_free(msg); + g_hash_table_destroy(body); +} + +#if 0 +/* Lookup a username by userid, from buddy list. + * + * @param wanted_uid + * + * @return Username of wanted_uid, if on blist, or NULL. Static string. + * + * XXX WARNING: UNKNOWN MEMORY CORRUPTION HERE! + */ +static const gchar * +msim_uid2username_from_blist(MsimSession *session, guint wanted_uid) +{ + GSList *buddies, *cur; + + buddies = purple_find_buddies(session->account, NULL); + + if (!buddies) + { + purple_debug_info("msim", "msim_uid2username_from_blist: no buddies?"); + return NULL; + } + + for (cur = buddies; cur != NULL; cur = g_slist_next(cur)) + { + PurpleBuddy *buddy; + //PurpleBlistNode *node; + guint uid; + const gchar *name; + + + /* See finch/gnthistory.c */ + buddy = cur->data; + //node = cur->data; + + uid = purple_blist_node_get_int(&buddy->node, "UserID"); + //uid = purple_blist_node_get_int(node, "UserID"); + + /* name = buddy->name; */ /* crash */ + /* name = PURPLE_BLIST_NODE_NAME(&buddy->node); */ /* crash */ + + /* XXX Is this right? Memory corruption here somehow. Happens only + * when return one of these values. */ + name = purple_buddy_get_name(buddy); /* crash */ + //name = purple_buddy_get_name((PurpleBuddy *)node); /* crash */ + /* return name; */ /* crash (with above) */ + + /* name = NULL; */ /* no crash */ + /* return NULL; */ /* no crash (with anything) */ + + /* crash = +*** glibc detected *** pidgin: realloc(): invalid pointer: 0x0000000000d2aec0 *** +======= Backtrace: ========= +/lib/libc.so.6(__libc_realloc+0x323)[0x2b7bfc012e03] +/usr/lib/libglib-2.0.so.0(g_realloc+0x31)[0x2b7bfba79a41] +/usr/lib/libgtk-x11-2.0.so.0(gtk_tree_path_append_index+0x3a)[0x2b7bfa110d5a] +/usr/lib/libgtk-x11-2.0.so.0[0x2b7bfa1287dc] +/usr/lib/libgtk-x11-2.0.so.0[0x2b7bfa128e56] +/usr/lib/libgtk-x11-2.0.so.0[0x2b7bfa128efd] +/usr/lib/libglib-2.0.so.0(g_main_context_dispatch+0x1b4)[0x2b7bfba72c84] +/usr/lib/libglib-2.0.so.0[0x2b7bfba75acd] +/usr/lib/libglib-2.0.so.0(g_main_loop_run+0x1ca)[0x2b7bfba75dda] +/usr/lib/libgtk-x11-2.0.so.0(gtk_main+0xa3)[0x2b7bfa0475f3] +pidgin(main+0x8be)[0x46b45e] +/lib/libc.so.6(__libc_start_main+0xf4)[0x2b7bfbfbf0c4] +pidgin(gtk_widget_grab_focus+0x39)[0x429ab9] + +or: + *** glibc detected *** /usr/local/bin/pidgin: malloc(): memory corruption (fast): 0x0000000000c10076 *** + (gdb) bt +#0 0x00002b4074ecd47b in raise () from /lib/libc.so.6 +#1 0x00002b4074eceda0 in abort () from /lib/libc.so.6 +#2 0x00002b4074f0453b in __fsetlocking () from /lib/libc.so.6 +#3 0x00002b4074f0c810 in free () from /lib/libc.so.6 +#4 0x00002b4074f0d6dd in malloc () from /lib/libc.so.6 +#5 0x00002b4074974b5b in g_malloc () from /usr/lib/libglib-2.0.so.0 +#6 0x00002b40749868bf in g_strdup () from /usr/lib/libglib-2.0.so.0 +#7 0x00002b407810969f in msim_parse ( + raw=0xd2a910 "\\bm\\100\\f\\3656574\\msg\\|s|0|ss|Offline") + at message.c:648 +#8 0x00002b407810889c in msim_input_cb (gc_uncasted=0xcf92c0, + source=, cond=) at myspace.c:1478 + + + Why is it crashing in msim_parse()'s g_strdup()? +*/ + purple_debug_info("msim", "msim_uid2username_from_blist: %s's uid=%d (want %d)\n", + name, uid, wanted_uid); + + if (uid == wanted_uid) + { + gchar *ret; + + ret = g_strdup(name); + + g_slist_free(buddies); + + return ret; + } + } + + g_slist_free(buddies); + return NULL; +} +#endif + +/** Preprocess incoming messages, resolving as needed, calling msim_process() when ready to process. + * + * @param session + * @param msg MsimMessage *, freed by caller. + */ +gboolean +msim_preprocess_incoming(MsimSession *session, MsimMessage *msg) +{ + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + if (msim_msg_get(msg, "bm") && msim_msg_get(msg, "f")) + { + guint uid; + const gchar *username; + + /* 'f' = userid message is from, in buddy messages */ + uid = msim_msg_get_integer(msg, "f"); + + /* TODO: Make caching work. Currently it is commented out because + * it crashes for unknown reasons, memory realloc error. */ +#if 0 + username = msim_uid2username_from_blist(session, uid); +#else + username = NULL; +#endif + + if (username) + { + /* Know username already, use it. */ + purple_debug_info("msim", "msim_preprocess_incoming: tagging with _username=%s\n", + username); + msg = msim_msg_append(msg, "_username", MSIM_TYPE_STRING, g_strdup(username)); + return msim_process(session, msg); + + } else { + gchar *from; + + /* Send lookup request. */ + /* XXX: where is msim_msg_get_string() freed? make _strdup and _nonstrdup. */ + purple_debug_info("msim", "msim_incoming: sending lookup, setting up callback\n"); + from = msim_msg_get_string(msg, "f"); + msim_lookup_user(session, from, msim_incoming_resolved, msim_msg_clone(msg)); + g_free(from); + + /* indeterminate */ + return TRUE; + } + } else { + /* Nothing to resolve - send directly to processing. */ + return msim_process(session, msg); + } +} + +/** Check if the connection is still alive, based on last communication. */ +gboolean +msim_check_alive(gpointer data) +{ + MsimSession *session; + time_t delta; + gchar *errmsg; + + session = (MsimSession *)data; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + + delta = time(NULL) - session->last_comm; + purple_debug_info("msim", "msim_check_alive: delta=%d\n", delta); + if (delta >= MSIM_KEEPALIVE_INTERVAL) + { + errmsg = g_strdup_printf(_("Connection to server lost (no data received within %d seconds)"), delta); + + purple_debug_info("msim", "msim_check_alive: %s > interval of %d, presumed dead\n", + errmsg, MSIM_KEEPALIVE_INTERVAL); + purple_connection_error(session->gc, errmsg); + + purple_notify_error(session->gc, NULL, errmsg, NULL); + + g_free(errmsg); + + return FALSE; + } + + return TRUE; +} + +/** Called when the session key arrives. */ +gboolean +msim_we_are_logged_on(MsimSession *session, MsimMessage *msg) +{ + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + purple_connection_update_progress(session->gc, _("Connected"), 3, 4); + + session->sesskey = msim_msg_get_integer(msg, "sesskey"); + purple_debug_info("msim", "SESSKEY=<%d>\n", session->sesskey); + + /* Comes with: proof,profileid,userid,uniquenick -- all same values + * some of the time, but can vary. This is our own user ID. */ + session->userid = msim_msg_get_integer(msg, "userid"); + + purple_connection_set_state(session->gc, PURPLE_CONNECTED); + + /* We now know are our own username, only after we're logged in.. + * which is weird, but happens because you login with your email + * address and not username. Will be freed in msim_session_destroy(). */ + session->username = msim_msg_get_string(msg, "uniquenick"); + +#ifdef MSIM_FAKE_SELF_ONLINE + /* Fake our self coming online. */ + purple_prpl_got_user_status(session->account, session->username, purple_primitive_get_id_from_type(PURPLE_STATUS_AVAILABLE), NULL); +#endif + + + /* Set status to current status. */ + msim_set_status(session->account, + purple_account_get_active_status(session->account)); + + purple_timeout_add(MSIM_KEEPALIVE_INTERVAL_CHECK, msim_check_alive, session); + + return TRUE; +} + +/** + * Process a message. + * + * @param session + * @param msg A message from the server, ready for processing (possibly with resolved username information attached). Caller frees. + * + * @return TRUE if successful. FALSE if processing failed. + */ +gboolean +msim_process(MsimSession *session, MsimMessage *msg) +{ + g_return_val_if_fail(session != NULL, FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + +#ifdef MSIM_DEBUG_MSG + { + msim_msg_dump("ready to process: %s\n", msg); + } +#endif + + if (msim_msg_get(msg, "nc")) + { + return msim_login_challenge(session, msg); + } else if (msim_msg_get(msg, "sesskey")) { + return msim_we_are_logged_on(session, msg); + } else if (msim_msg_get(msg, "bm")) { + guint bm; + + bm = msim_msg_get_integer(msg, "bm"); + switch (bm) + { + case MSIM_BM_STATUS: + return msim_status(session, msg); + case MSIM_BM_INSTANT: + return msim_incoming_im(session, msg); + case MSIM_BM_ACTION: + return msim_incoming_action(session, msg); + default: + /* Not really an IM, but show it for informational + * purposes during development. */ + return msim_incoming_im(session, msg); + } + } else if (msim_msg_get(msg, "rid")) { + return msim_process_reply(session, msg); + } else if (msim_msg_get(msg, "error")) { + return msim_error(session, msg); + } else if (msim_msg_get(msg, "ka")) { + /* TODO: Setup a timer, if keep-alive is not received within ~3 minutes, then + * disconnect the user. As it stands, if Internet connection goes out (this + * just happened here), msimprpl will appear to be connected forever, while + * other plugins (oscar, etc.) will time out. Msimprpl should timeout too. */ + purple_debug_info("msim", "msim_process: got keep alive\n"); + return TRUE; + } else { + msim_unrecognized(session, msg, "in msim_process"); + return FALSE; + } +} + +/** Store an field of information about a buddy. */ +void +msim_store_buddy_info_each(gpointer key, gpointer value, gpointer user_data) +{ + PurpleBuddy *buddy; + gchar *key_str, *value_str; + + buddy = (PurpleBuddy *)user_data; + key_str = (gchar *)key; + value_str = (gchar *)value; + + if (strcmp(key_str, "UserID") == 0 || + strcmp(key_str, "Age") == 0 || + strcmp(key_str, "TotalFriends") == 0) + { + /* Certain fields get set as integers, instead of strings, for + * convenience. May not be the best way to do it, but having at least + * UserID as an integer is convenient...until it overflows! */ + purple_blist_node_set_int(&buddy->node, key_str, atol(value_str)); + } else { + purple_blist_node_set_string(&buddy->node, key_str, value_str); + } +} + +/** Save buddy information to the buddy list from a user info reply message. + * + * @param session + * @param msg The user information reply, with any amount of information. + * + * The information is saved to the buddy's blist node, which ends up in blist.xml. + */ +gboolean +msim_store_buddy_info(MsimSession *session, MsimMessage *msg) +{ + GHashTable *body; + gchar *username, *body_str, *uid; + PurpleBuddy *buddy; + guint rid; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + rid = msim_msg_get_integer(msg, "rid"); + + g_return_val_if_fail(rid != 0, FALSE); + + body_str = msim_msg_get_string(msg, "body"); + g_return_val_if_fail(body_str != NULL, FALSE); + body = msim_parse_body(body_str); + g_free(body_str); + + /* TODO: implement a better hash-like interface, and use it. */ + username = g_hash_table_lookup(body, "UserName"); + + if (!username) + { + purple_debug_info("msim", + "msim_process_reply: not caching body, no UserName\n"); + return FALSE; + } + + uid = g_hash_table_lookup(body, "UserID"); + g_return_val_if_fail(uid, FALSE); + + purple_debug_info("msim", "associating uid %d with username %s\n", uid, username); + + buddy = purple_find_buddy(session->account, username); + if (buddy) + { + g_hash_table_foreach(body, msim_store_buddy_info_each, buddy); + } + + return TRUE; +} + +/** + * Process a persistance message reply from the server. + * + * @param session + * @param msg Message reply from server. + * + * @return TRUE if successful. + */ +gboolean +msim_process_reply(MsimSession *session, MsimMessage *msg) +{ + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + if (msim_msg_get(msg, "rid")) /* msim_lookup_user sets callback for here */ + { + MSIM_USER_LOOKUP_CB cb; + gpointer data; + guint rid; + + msim_store_buddy_info(session, msg); + + rid = msim_msg_get_integer(msg, "rid"); + + /* If a callback is registered for this userid lookup, call it. */ + cb = g_hash_table_lookup(session->user_lookup_cb, GUINT_TO_POINTER(rid)); + data = g_hash_table_lookup(session->user_lookup_cb_data, GUINT_TO_POINTER(rid)); + + if (cb) + { + purple_debug_info("msim", + "msim_process_body: calling callback now\n"); + /* Clone message, so that the callback 'cb' can use it (needs to free it also). */ + cb(session, msim_msg_clone(msg), data); + g_hash_table_remove(session->user_lookup_cb, GUINT_TO_POINTER(rid)); + g_hash_table_remove(session->user_lookup_cb_data, GUINT_TO_POINTER(rid)); + } else { + purple_debug_info("msim", + "msim_process_body: no callback for rid %d\n", rid); + } + } + + return TRUE; +} + +/** + * Handle an error from the server. + * + * @param session + * @param msg The message. + * + * @return TRUE if successfully reported error. + */ +gboolean +msim_error(MsimSession *session, MsimMessage *msg) +{ + gchar *errmsg, *full_errmsg; + guint err; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + err = msim_msg_get_integer(msg, "err"); + errmsg = msim_msg_get_string(msg, "errmsg"); + + full_errmsg = g_strdup_printf(_("Protocol error, code %d: %s"), err, + errmsg ? errmsg : "no 'errmsg' given"); + + g_free(errmsg); + + purple_debug_info("msim", "msim_error: %s\n", full_errmsg); + + purple_notify_error(session->account, g_strdup(_("MySpaceIM Error")), + full_errmsg, NULL); + + /* Destroy session if fatal. */ + if (msim_msg_get(msg, "fatal")) + { + purple_debug_info("msim", "fatal error, closing\n"); + purple_connection_error(session->gc, full_errmsg); + } + + return TRUE; +} + +/** + * Process incoming status messages. + * + * @param session + * @param msg Status update message. Caller frees. + * + * @return TRUE if successful. + */ +gboolean +msim_status(MsimSession *session, MsimMessage *msg) +{ + PurpleBuddyList *blist; + PurpleBuddy *buddy; + //PurpleStatus *status; + gchar **status_array; + GList *list; + gchar *status_headline; + gchar *status_str; + gint i, status_code, purple_status_code; + gchar *username; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + status_str = msim_msg_get_string(msg, "msg"); + g_return_val_if_fail(status_str != NULL, FALSE); + + msim_msg_dump("msim_status msg=%s\n", msg); + + /* Helpfully looked up by msim_incoming_resolve() for us. */ + username = msim_msg_get_string(msg, "_username"); + /* Note: DisplayName doesn't seem to be resolvable. It could be displayed on + * the buddy list, if the UserID was stored along with it. */ + + if (username == NULL) + { + g_free(status_str); + g_return_val_if_fail(NULL, FALSE); + } + + purple_debug_info("msim", + "msim_status: updating status for <%s> to <%s>\n", + username, status_str); + + /* TODO: generic functions to split into a GList, part of MsimMessage */ + status_array = g_strsplit(status_str, "|", 0); + for (list = NULL, i = 0; + status_array[i]; + i++) + { + /* Note: this adds the 0th ordinal too, which might not be a value + * at all (the 0 in the 0|1|2|3... status fields, but 0 always appears blank). + */ + list = g_list_append(list, status_array[i]); + } + + /* Example fields: + * |s|0|ss|Offline + * |s|1|ss|:-)|ls||ip|0|p|0 + * + * TODO: write list support in MsimMessage, and use it here. + */ + + status_code = atoi(g_list_nth_data(list, MSIM_STATUS_ORDINAL_ONLINE)); + purple_debug_info("msim", "msim_status: %s's status code = %d\n", username, status_code); + status_headline = g_list_nth_data(list, MSIM_STATUS_ORDINAL_HEADLINE); + + blist = purple_get_blist(); + + /* Add buddy if not found */ + buddy = purple_find_buddy(session->account, username); + if (!buddy) + { + purple_debug_info("msim", + "msim_status: making new buddy for %s\n", username); + buddy = purple_buddy_new(session->account, username, NULL); + + purple_blist_add_buddy(buddy, NULL, NULL, NULL); + + /* All buddies on list should have 'uid' integer associated with them. */ + purple_blist_node_set_int(&buddy->node, "UserID", msim_msg_get_integer(msg, "f")); + + msim_store_buddy_info(session, msg); + } else { + purple_debug_info("msim", "msim_status: found buddy %s\n", username); + } + + purple_blist_node_set_string(&buddy->node, "Headline", status_headline); + + /* Set user status */ + switch (status_code) + { + case MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN: + purple_status_code = PURPLE_STATUS_OFFLINE; + break; + + case MSIM_STATUS_CODE_ONLINE: + purple_status_code = PURPLE_STATUS_AVAILABLE; + break; + + case MSIM_STATUS_CODE_AWAY: + purple_status_code = PURPLE_STATUS_AWAY; + break; + + case MSIM_STATUS_CODE_IDLE: + /* will be handled below */ + purple_status_code = -1; + break; + + default: + purple_debug_info("msim", "msim_status for %s, unknown status code %d, treating as available\n", + username, status_code); + purple_status_code = PURPLE_STATUS_AVAILABLE; + } + +#ifdef MSIM_FAKE_SELF_ONLINE + if (!strcmp(username, session->username) && purple_status_code == PURPLE_STATUS_OFFLINE) + { + /* Hack to ignore offline status notices on self. */ + } else if (status_code != MSIM_STATUS_CODE_IDLE) { +#endif + purple_prpl_got_user_status(session->account, username, purple_primitive_get_id_from_type(purple_status_code), NULL); +#ifdef MSIM_FAKE_SELF_ONLINE + } +#endif + + if (status_code == MSIM_STATUS_CODE_IDLE) + { + purple_debug_info("msim", "msim_status: got idle: %s\n", username); + purple_prpl_got_user_idle(session->account, username, TRUE, time(NULL)); + } else { + /* All other statuses indicate going back to non-idle. */ + purple_prpl_got_user_idle(session->account, username, FALSE, time(NULL)); + } + + g_strfreev(status_array); + g_free(status_str); + g_free(username); + g_list_free(list); + + return TRUE; +} + +/** Add a buddy to user's buddy list. */ +void +msim_add_buddy(PurpleConnection *gc, PurpleBuddy *buddy, PurpleGroup *group) +{ + MsimSession *session; + MsimMessage *msg; + /* MsimMessage *msg_blocklist; */ + + session = (MsimSession *)gc->proto_data; + purple_debug_info("msim", "msim_add_buddy: want to add %s to %s\n", buddy->name, + group ? group->name : "(no group)"); + + msg = msim_msg_new(TRUE, + "addbuddy", MSIM_TYPE_BOOLEAN, TRUE, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + /* "newprofileid" will be inserted here with uid. */ + "reason", MSIM_TYPE_STRING, g_strdup(""), + NULL); + + if (!msim_postprocess_outgoing(session, msg, buddy->name, "newprofileid", "reason")) + { + purple_notify_error(NULL, NULL, _("Failed to add buddy"), _("'addbuddy' command failed.")); + msim_msg_free(msg); + return; + } + msim_msg_free(msg); + + /* TODO: if addbuddy fails ('error' message is returned), delete added buddy from + * buddy list since Purple adds it locally. */ + + /* TODO: Update blocklist. */ +#if 0 + msg_blocklist = msim_msg_new(TRUE, + "persist", MSIM_TYPE_INTEGER, 1, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + "cmd", MSIM_TYPE_INTEGER, MSIM_CMD_BIT_ACTION | MSIM_CMD_PUT, + "dsn", MSIM_TYPE_INTEGER, MC_CONTACT_INFO_DSN, + "lid", MSIM_TYPE_INTEGER, MC_CONTACT_INFO_LID, + /* TODO: Use msim_new_reply_callback to get rid. */ + "rid", MSIM_TYPE_INTEGER, session->next_rid++, + "body", MSIM_TYPE_STRING, + g_strdup_printf("ContactID=\034" + "GroupName=%s\034" + "Position=1000\034" + "Visibility=1\034" + "NickName=\034" + "NameSelect=0", + "Friends" /*group->name*/ )); + + if (!msim_postprocess_outgoing(session, msg, buddy->name, "body", NULL)) + { + purple_notify_error(NULL, NULL, _("Failed to add buddy"), _("persist command failed")); + msim_msg_free(msg_blocklist); + return; + } + msim_msg_free(msg_blocklist); +#endif +} + +/** Perform actual postprocessing on a message, adding userid as specified. + * + * @param msg The message to postprocess. + * @param uid_before Name of field where to insert new field before, or NULL for end. + * @param uid_field_name Name of field to add uid to. + * @param uid The userid to insert. + * + * If the field named by uid_field_name already exists, then its string contents will + * be used for the field, except "" will be replaced by the userid. + * + * If the field named by uid_field_name does not exist, it will be added before the + * field named by uid_before, as an integer, with the userid. + * + * Does not handle sending, or scheduling userid lookup. For that, see msim_postprocess_outgoing(). + */ +MsimMessage * +msim_do_postprocessing(MsimMessage *msg, const gchar *uid_before, + const gchar *uid_field_name, guint uid) +{ + purple_debug_info("msim", "msim_do_postprocessing called with ufn=%s, ub=%s, uid=%d\n", + uid_field_name, uid_before, uid); + msim_msg_dump("msim_do_postprocessing msg: %s\n", msg); + + /* First, check - if the field already exists, treat it as a format string. */ + if (msim_msg_get(msg, uid_field_name)) + { + MsimMessageElement *elem; + gchar *fmt_string; + gchar *uid_str; + + /* Warning: this probably violates the encapsulation of MsimMessage */ + + elem = msim_msg_get(msg, uid_field_name); + g_return_val_if_fail(elem->type == MSIM_TYPE_STRING, NULL); + + /* Get the raw string, not with msim_msg_get_string() since that copies it. + * Want the original string so can free it. */ + fmt_string = (gchar *)(elem->data); + + uid_str = g_strdup_printf("%d", uid); + elem->data = str_replace(fmt_string, "", uid_str); + g_free(uid_str); + g_free(fmt_string); + + purple_debug_info("msim", "msim_postprocess_outgoing_cb: formatted new string, %s\n", + elem->data); + + } else { + /* Otherwise, insert new field into outgoing message. */ + msg = msim_msg_insert_before(msg, uid_before, uid_field_name, MSIM_TYPE_INTEGER, GUINT_TO_POINTER(uid)); + } + + return msg; +} + +/** Callback for msim_postprocess_outgoing() to add a userid to a message, and send it (once receiving userid). + * + * @param session + * @param userinfo The user information reply message, containing the user ID + * @param data The message to postprocess and send. + * + * The data message should contain these fields: + * + * _uid_field_name: string, name of field to add with userid from userinfo message + * _uid_before: string, name of field before field to insert, or NULL for end + * + * +*/ +void +msim_postprocess_outgoing_cb(MsimSession *session, MsimMessage *userinfo, gpointer data) +{ + gchar *body_str; + GHashTable *body; + gchar *uid, *uid_field_name, *uid_before; + MsimMessage *msg; + + msg = (MsimMessage *)data; + + msim_msg_dump("msim_postprocess_outgoing_cb() got msg=%s\n", msg); + + /* Obtain userid from userinfo message. */ + body_str = msim_msg_get_string(userinfo, "body"); + g_return_if_fail(body_str != NULL); + body = msim_parse_body(body_str); + g_free(body_str); + + uid = g_strdup(g_hash_table_lookup(body, "UserID")); + g_hash_table_destroy(body); + + uid_field_name = msim_msg_get_string(msg, "_uid_field_name"); + uid_before = msim_msg_get_string(msg, "_uid_before"); + + msg = msim_do_postprocessing(msg, uid_before, uid_field_name, atol(uid)); + + /* Send */ + if (!msim_msg_send(session, msg)) + { + purple_debug_info("msim", "msim_postprocess_outgoing_cb: sending failed for message: %s\n", msg); + } + + + /* Free field names AFTER sending message, because MsimMessage does NOT copy + * field names - instead, treats them as static strings (which they usually are). + */ + g_free(uid_field_name); + g_free(uid_before); + + //msim_msg_free(msg); +} + +/** Postprocess and send a message. + * + * @param session + * @param msg Message to postprocess. Will NOT be freed. + * @param username Username to resolve. Assumed to be a static string (will not be freed or copied). + * @param uid_field_name Name of new field to add, containing uid of username. Static string. + * @param uid_before Name of existing field to insert username field before. Static string. + * + * @return Postprocessed message. + */ +gboolean +msim_postprocess_outgoing(MsimSession *session, MsimMessage *msg, + const gchar *username, const gchar *uid_field_name, + const gchar *uid_before) +{ + PurpleBuddy *buddy; + guint uid; + gboolean rc; + + /* Store information for msim_postprocess_outgoing_cb(). */ + purple_debug_info("msim", "msim_postprocess_outgoing(u=%s,ufn=%s,ub=%s)\n", + username, uid_field_name, uid_before); + msim_msg_dump("msim_postprocess_outgoing: msg before=%s\n", msg); + msg = msim_msg_append(msg, "_username", MSIM_TYPE_STRING, g_strdup(username)); + msg = msim_msg_append(msg, "_uid_field_name", MSIM_TYPE_STRING, g_strdup(uid_field_name)); + msg = msim_msg_append(msg, "_uid_before", MSIM_TYPE_STRING, g_strdup(uid_before)); + + /* First, try the most obvious. If numeric userid is given, use that directly. */ + if (msim_is_userid(username)) + { + uid = atol(username); + } else { + /* Next, see if on buddy list and know uid. */ + buddy = purple_find_buddy(session->account, username); + if (buddy) + { + uid = purple_blist_node_get_int(&buddy->node, "UserID"); + } else { + uid = 0; + } + + if (!buddy || !uid) + { + /* Don't have uid offhand - need to ask for it, and wait until hear back before sending. */ + purple_debug_info("msim", ">>> msim_postprocess_outgoing: couldn't find username %s in blist\n", + username); + msim_msg_dump("msim_postprocess_outgoing - scheduling lookup, msg=%s\n", msg); + /* TODO: where is cloned message freed? Should be in _cb. */ + msim_lookup_user(session, username, msim_postprocess_outgoing_cb, msim_msg_clone(msg)); + return TRUE; /* not sure of status yet - haven't sent! */ + } + } + + /* Already have uid, postprocess and send msg immediately. */ + purple_debug_info("msim", "msim_postprocess_outgoing: found username %s has uid %d\n", + username, uid); + + msg = msim_do_postprocessing(msg, uid_before, uid_field_name, uid); + + msim_msg_dump("msim_postprocess_outgoing: msg after (uid immediate)=%s\n", msg); + + rc = msim_msg_send(session, msg); + + //msim_msg_free(msg); + + return rc; +} + +/** Remove a buddy from the user's buddy list. */ +void +msim_remove_buddy(PurpleConnection *gc, PurpleBuddy *buddy, PurpleGroup *group) +{ + MsimSession *session; + MsimMessage *delbuddy_msg; + MsimMessage *persist_msg; + MsimMessage *blocklist_msg; + + session = (MsimSession *)gc->proto_data; + + delbuddy_msg = msim_msg_new(TRUE, + "delbuddy", MSIM_TYPE_BOOLEAN, TRUE, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + /* 'delprofileid' with uid will be inserted here. */ + NULL); + /* TODO: free msg */ + if (!msim_postprocess_outgoing(session, delbuddy_msg, buddy->name, "delprofileid", NULL)) + { + purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("'delbuddy' command failed")); + return; + } + + persist_msg = msim_msg_new(TRUE, + "persist", MSIM_TYPE_INTEGER, 1, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + "cmd", MSIM_TYPE_INTEGER, MSIM_CMD_BIT_ACTION | MSIM_CMD_DELETE, + "dsn", MSIM_TYPE_INTEGER, MD_DELETE_BUDDY_DSN, + "lid", MSIM_TYPE_INTEGER, MD_DELETE_BUDDY_LID, + "uid", MSIM_TYPE_INTEGER, session->userid, + "rid", MSIM_TYPE_INTEGER, session->next_rid++, + /* will be replaced by postprocessing */ + "body", MSIM_TYPE_STRING, g_strdup("ContactID="), + NULL); + + /* TODO: free msg */ + if (!msim_postprocess_outgoing(session, persist_msg, buddy->name, "body", NULL)) + { + purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("persist command failed")); + return; + } + + blocklist_msg = msim_msg_new(TRUE, + "blocklist", MSIM_TYPE_BOOLEAN, TRUE, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + /* TODO: MsimMessage lists */ + "idlist", MSIM_TYPE_STRING, g_strdup("a-||b-|"), + NULL); + + if (!msim_postprocess_outgoing(session, blocklist_msg, buddy->name, "idlist", NULL)) + { + purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("blocklist command failed")); + return; + } +} + +/** Return whether the buddy can be messaged while offline. + * + * The protocol supports offline messages in just the same way as online + * messages. + */ +gboolean +msim_offline_message(const PurpleBuddy *buddy) +{ + return TRUE; +} + +/** + * Callback when input available. + * + * @param gc_uncasted A PurpleConnection pointer. + * @param source File descriptor. + * @param cond PURPLE_INPUT_READ + * + * Reads the input, and calls msim_preprocess_incoming() to handle it. + */ +void +msim_input_cb(gpointer gc_uncasted, gint source, PurpleInputCondition cond) +{ + PurpleConnection *gc; + PurpleAccount *account; + MsimSession *session; + gchar *end; + int n; + + g_return_if_fail(gc_uncasted != NULL); + g_return_if_fail(source >= 0); /* Note: 0 is a valid fd */ + + gc = (PurpleConnection *)(gc_uncasted); + account = purple_connection_get_account(gc); + session = gc->proto_data; + + g_return_if_fail(cond == PURPLE_INPUT_READ); + g_return_if_fail(MSIM_SESSION_VALID(session)); + + /* Mark down that we got data, so don't timeout. */ + session->last_comm = time(NULL); + + /* Only can handle so much data at once... + * If this happens, try recompiling with a higher MSIM_READ_BUF_SIZE. + * Should be large enough to hold the largest protocol message. + */ + if (session->rxoff == MSIM_READ_BUF_SIZE) + { + purple_debug_error("msim", "msim_input_cb: %d-byte read buffer full!\n", + MSIM_READ_BUF_SIZE); + purple_connection_error(gc, _("Read buffer full")); + return; + } + + purple_debug_info("msim", "buffer at %d (max %d), reading up to %d\n", + session->rxoff, MSIM_READ_BUF_SIZE, + MSIM_READ_BUF_SIZE - session->rxoff); + + /* Read into buffer. On Win32, need recv() not read(). session->fd also holds + * the file descriptor, but it sometimes differs from the 'source' parameter. + */ + n = recv(session->fd, session->rxbuf + session->rxoff, MSIM_READ_BUF_SIZE - session->rxoff, 0); + + if (n < 0 && errno == EAGAIN) + { + return; + } + else if (n < 0) + { + purple_debug_error("msim", "msim_input_cb: read error, ret=%d, " + "error=%s, source=%d, fd=%d (%X))\n", + n, strerror(errno), source, session->fd, session->fd); + purple_connection_error(gc, _("Read error")); + return; + } + else if (n == 0) + { + purple_debug_info("msim", "msim_input_cb: server disconnected\n"); + purple_connection_error(gc, _("Server has disconnected")); + return; + } + + /* Null terminate */ + session->rxbuf[session->rxoff + n] = 0; + +#ifdef MSIM_CHECK_EMBEDDED_NULLS + /* Check for embedded NULs. I don't handle them, and they shouldn't occur. */ + if (strlen(session->rxbuf + session->rxoff) != n) + { + /* Occurs after login, but it is not a null byte. */ + purple_debug_info("msim", "msim_input_cb: strlen=%d, but read %d bytes" + "--null byte encountered?\n", + strlen(session->rxbuf + session->rxoff), n); + //purple_connection_error(gc, "Invalid message - null byte on input"); + return; + } +#endif + + session->rxoff += n; + purple_debug_info("msim", "msim_input_cb: read=%d\n", n); + +#ifdef MSIM_DEBUG_RXBUF + purple_debug_info("msim", "buf=<%s>\n", session->rxbuf); +#endif + + /* Look for \\final\\ end markers. If found, process message. */ + while((end = strstr(session->rxbuf, MSIM_FINAL_STRING))) + { + MsimMessage *msg; + +#ifdef MSIM_DEBUG_RXBUF + purple_debug_info("msim", "in loop: buf=<%s>\n", session->rxbuf); +#endif + *end = 0; + msg = msim_parse(g_strdup(session->rxbuf)); + if (!msg) + { + purple_debug_info("msim", "msim_input_cb: couldn't parse <%s>\n", + session->rxbuf); + purple_connection_error(gc, _("Unparseable message")); + } + else + { + /* Process message and then free it (processing function should + * clone message if it wants to keep it afterwards.) */ + if (!msim_preprocess_incoming(session, msg)) + { + msim_msg_dump("msim_input_cb: preprocessing message failed on msg: %s\n", msg); + } + msim_msg_free(msg); + } + + /* Move remaining part of buffer to beginning. */ + session->rxoff -= strlen(session->rxbuf) + strlen(MSIM_FINAL_STRING); + memmove(session->rxbuf, end + strlen(MSIM_FINAL_STRING), + MSIM_READ_BUF_SIZE - (end + strlen(MSIM_FINAL_STRING) - session->rxbuf)); + + /* Clear end of buffer */ + //memset(end, 0, MSIM_READ_BUF_SIZE - (end - session->rxbuf)); + } +} + +/* Setup a callback, to be called when a reply is received with the returned rid. + * + * @param cb The callback, an MSIM_USER_LOOKUP_CB. + * @param data Arbitrary user data to be passed to callback (probably an MsimMessage *). + * + * @return The request/reply ID, used to link replies with requests. Put the rid in your request. + * + * TODO: Make more generic and more specific: + * 1) MSIM_USER_LOOKUP_CB - make it for PERSIST_REPLY, not just user lookup + * 2) data - make it an MsimMessage? + */ +guint +msim_new_reply_callback(MsimSession *session, MSIM_USER_LOOKUP_CB cb, + gpointer data) +{ + guint rid; + + rid = session->next_rid++; + + g_hash_table_insert(session->user_lookup_cb, GUINT_TO_POINTER(rid), cb); + g_hash_table_insert(session->user_lookup_cb_data, GUINT_TO_POINTER(rid), data); + + return rid; +} + +/** + * Callback when connected. Sets up input handlers. + * + * @param data A PurpleConnection pointer. + * @param source File descriptor. + * @param error_message + */ +void +msim_connect_cb(gpointer data, gint source, const gchar *error_message) +{ + PurpleConnection *gc; + MsimSession *session; + + g_return_if_fail(data != NULL); + + gc = (PurpleConnection *)data; + session = (MsimSession *)gc->proto_data; + + if (source < 0) + { + purple_connection_error(gc, _("Couldn't connect to host")); + purple_connection_error(gc, g_strdup_printf( + _("Couldn't connect to host: %s (%d)"), + error_message ? error_message : "no message given", + source)); + return; + } + + session->fd = source; + + gc->inpa = purple_input_add(source, PURPLE_INPUT_READ, msim_input_cb, gc); + + +} + +/* Session methods */ + +/** + * Create a new MSIM session. + * + * @param acct The account to create the session from. + * + * @return Pointer to a new session. Free with msim_session_destroy. + */ +MsimSession * +msim_session_new(PurpleAccount *acct) +{ + MsimSession *session; + + g_return_val_if_fail(acct != NULL, NULL); + + session = g_new0(MsimSession, 1); + + session->magic = MSIM_SESSION_STRUCT_MAGIC; + session->account = acct; + session->gc = purple_account_get_connection(acct); + session->sesskey = 0; + session->userid = 0; + session->username = NULL; + session->fd = -1; + + /* TODO: Remove. */ + session->user_lookup_cb = g_hash_table_new_full(g_direct_hash, + g_direct_equal, NULL, NULL); /* do NOT free function pointers! (values) */ + session->user_lookup_cb_data = g_hash_table_new_full(g_direct_hash, + g_direct_equal, NULL, NULL);/* TODO: we don't know what the values are, + they could be integers inside gpointers + or strings, so I don't freed them. + Figure this out, once free cache. */ + session->rxoff = 0; + session->rxbuf = g_new0(gchar, MSIM_READ_BUF_SIZE); + session->next_rid = 1; + session->last_comm = time(NULL); + + return session; +} + +/** + * Free a session. + * + * @param session The session to destroy. + */ +void +msim_session_destroy(MsimSession *session) +{ + g_return_if_fail(MSIM_SESSION_VALID(session)); + + session->magic = -1; + + g_free(session->rxbuf); + g_free(session->username); + + /* TODO: Remove. */ + g_hash_table_destroy(session->user_lookup_cb); + g_hash_table_destroy(session->user_lookup_cb_data); + + g_free(session); +} + +/** + * Close the connection. + * + * @param gc The connection. + */ +void +msim_close(PurpleConnection *gc) +{ + MsimSession *session; + + if (gc == NULL) + return; + + session = (MsimSession *)gc->proto_data; + if (session == NULL) + return; + + gc->proto_data = NULL; + + if (!MSIM_SESSION_VALID(session)) + return; + + if (session->gc->inpa) + purple_input_remove(session->gc->inpa); + + msim_session_destroy(session); +} + + +/** + * Check if a string is a userid (all numeric). + * + * @param user The user id, email, or name. + * + * @return TRUE if is userid, FALSE if not. + */ +gboolean +msim_is_userid(const gchar *user) +{ + g_return_val_if_fail(user != NULL, FALSE); + + return strspn(user, "0123456789") == strlen(user); +} + +/** + * Check if a string is an email address (contains an @). + * + * @param user The user id, email, or name. + * + * @return TRUE if is an email, FALSE if not. + * + * This function is not intended to be used as a generic + * means of validating email addresses, but to distinguish + * between a user represented by an email address from + * other forms of identification. + */ +gboolean +msim_is_email(const gchar *user) +{ + g_return_val_if_fail(user != NULL, FALSE); + + return strchr(user, '@') != NULL; +} + + +/** + * Asynchronously lookup user information, calling callback when receive result. + * + * @param session + * @param user The user id, email address, or username. Not freed. + * @param cb Callback, called with user information when available. + * @param data An arbitray data pointer passed to the callback. + */ +/* TODO: change to not use callbacks */ +void +msim_lookup_user(MsimSession *session, const gchar *user, + MSIM_USER_LOOKUP_CB cb, gpointer data) +{ + gchar *field_name; + guint rid, cmd, dsn, lid; + + g_return_if_fail(MSIM_SESSION_VALID(session)); + g_return_if_fail(user != NULL); + g_return_if_fail(cb != NULL); + + purple_debug_info("msim", "msim_lookup_userid: " + "asynchronously looking up <%s>\n", user); + + msim_msg_dump("msim_lookup_user: data=%s\n", (MsimMessage *)data); + + /* Setup callback. Response will be associated with request using 'rid'. */ + rid = msim_new_reply_callback(session, cb, data); + + /* Send request */ + + cmd = MSIM_CMD_GET; + + if (msim_is_userid(user)) + { + field_name = "UserID"; + dsn = MG_MYSPACE_INFO_BY_ID_DSN; + lid = MG_MYSPACE_INFO_BY_ID_LID; + } else if (msim_is_email(user)) { + field_name = "Email"; + dsn = MG_MYSPACE_INFO_BY_STRING_DSN; + lid = MG_MYSPACE_INFO_BY_STRING_LID; + } else { + field_name = "UserName"; + dsn = MG_MYSPACE_INFO_BY_STRING_DSN; + lid = MG_MYSPACE_INFO_BY_STRING_LID; + } + + + g_return_if_fail(msim_send(session, + "persist", MSIM_TYPE_INTEGER, 1, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + "cmd", MSIM_TYPE_INTEGER, 1, + "dsn", MSIM_TYPE_INTEGER, dsn, + "uid", MSIM_TYPE_INTEGER, session->userid, + "lid", MSIM_TYPE_INTEGER, lid, + "rid", MSIM_TYPE_INTEGER, rid, + /* TODO: dictionary field type */ + "body", MSIM_TYPE_STRING, + g_strdup_printf("%s=%s", field_name, user), + NULL)); +} + + +/** + * Obtain the status text for a buddy. + * + * @param buddy The buddy to obtain status text for. + * + * @return Status text, or NULL if error. Caller g_free()'s. + * + */ +char * +msim_status_text(PurpleBuddy *buddy) +{ + MsimSession *session; + const gchar *display_name, *headline; + + g_return_val_if_fail(buddy != NULL, NULL); + + session = (MsimSession *)buddy->account->gc->proto_data; + g_return_val_if_fail(MSIM_SESSION_VALID(session), NULL); + + display_name = headline = NULL; + + /* Retrieve display name and/or headline, depending on user preference. */ + if (purple_account_get_bool(session->account, "show_display_name", TRUE)) + { + display_name = purple_blist_node_get_string(&buddy->node, "DisplayName"); + } + + if (purple_account_get_bool(session->account, "show_headline", FALSE)) + { + headline = purple_blist_node_get_string(&buddy->node, "Headline"); + } + + /* Return appropriate combination of display name and/or headline, or neither. */ + + if (display_name && headline) + return g_strconcat(display_name, " ", headline, NULL); + + if (display_name) + return g_strdup(display_name); + + if (headline) + return g_strdup(headline); + + return NULL; +} + +/** + * Obtain the tooltip text for a buddy. + * + * @param buddy Buddy to obtain tooltip text on. + * @param user_info Variable modified to have the tooltip text. + * @param full TRUE if should obtain full tooltip text. + * + */ +void +msim_tooltip_text(PurpleBuddy *buddy, PurpleNotifyUserInfo *user_info, + gboolean full) +{ + const gchar *str, *str2; + gint n; + + g_return_if_fail(buddy != NULL); + g_return_if_fail(user_info != NULL); + + if (PURPLE_BUDDY_IS_ONLINE(buddy)) + { + MsimSession *session; + + session = (MsimSession *)buddy->account->gc->proto_data; + + g_return_if_fail(MSIM_SESSION_VALID(session)); + + /* TODO: if (full), do something different */ + + /* Useful to identify the account the tooltip refers to. + * Other prpls show this. */ + str = purple_blist_node_get_string(&buddy->node, "UserName"); + if (str) + purple_notify_user_info_add_pair(user_info, _("User Name"), str); + + /* a/s/l...the vitals */ + n = purple_blist_node_get_int(&buddy->node, "Age"); + if (n) + purple_notify_user_info_add_pair(user_info, _("Age"), + g_strdup_printf("%d", n)); + + str = purple_blist_node_get_string(&buddy->node, "Gender"); + if (str) + purple_notify_user_info_add_pair(user_info, _("Gender"), str); + + str = purple_blist_node_get_string(&buddy->node, "Location"); + if (str) + purple_notify_user_info_add_pair(user_info, _("Location"), str); + + /* Other information */ + str = purple_blist_node_get_string(&buddy->node, "Headline"); + if (str) + purple_notify_user_info_add_pair(user_info, _("Headline"), str); + + str = purple_blist_node_get_string(&buddy->node, "BandName"); + str2 = purple_blist_node_get_string(&buddy->node, "SongName"); + if (str || str2) + purple_notify_user_info_add_pair(user_info, _("Song"), + g_strdup_printf("%s - %s", + str ? str : _("Unknown Artist"), + str2 ? str2 : _("Unknown Song"))); + + n = purple_blist_node_get_int(&buddy->node, "TotalFriends"); + if (n) + purple_notify_user_info_add_pair(user_info, _("Total Friends"), + g_strdup_printf("%d", n)); + + } +} + +/** Callbacks called by Purple, to access this plugin. */ +PurplePluginProtocolInfo prpl_info = +{ + /* options */ + OPT_PROTO_USE_POINTSIZE /* specify font size in sane point size */ + /* | OPT_PROTO_MAIL_CHECK - TODO: myspace will notify of mail */ + /* | OPT_PROTO_IM_IMAGE - TODO: direct images. */ + , + NULL, /* user_splits */ + NULL, /* protocol_options */ + NO_BUDDY_ICONS, /* icon_spec - TODO: eventually should add this */ + msim_list_icon, /* list_icon */ + NULL, /* list_emblems */ + msim_status_text, /* status_text */ + msim_tooltip_text, /* tooltip_text */ + msim_status_types, /* status_types */ + NULL, /* blist_node_menu */ + NULL, /* chat_info */ + NULL, /* chat_info_defaults */ + msim_login, /* login */ + msim_close, /* close */ + msim_send_im, /* send_im */ + NULL, /* set_info */ + msim_send_typing, /* send_typing */ + msim_get_info, /* get_info */ + msim_set_status, /* set_status */ + msim_set_idle, /* set_idle */ + NULL, /* change_passwd */ + msim_add_buddy, /* add_buddy */ + NULL, /* add_buddies */ + msim_remove_buddy, /* remove_buddy */ + NULL, /* remove_buddies */ + NULL, /* add_permit */ + NULL, /* add_deny */ + NULL, /* rem_permit */ + NULL, /* rem_deny */ + NULL, /* set_permit_deny */ + NULL, /* join_chat */ + NULL, /* reject chat invite */ + NULL, /* get_chat_name */ + NULL, /* chat_invite */ + NULL, /* chat_leave */ + NULL, /* chat_whisper */ + NULL, /* chat_send */ + NULL, /* keepalive */ + NULL, /* register_user */ + NULL, /* get_cb_info */ + NULL, /* get_cb_away */ + NULL, /* alias_buddy */ + NULL, /* group_buddy */ + NULL, /* rename_group */ + NULL, /* buddy_free */ + NULL, /* convo_closed */ + NULL, /* normalize */ + NULL, /* set_buddy_icon */ + NULL, /* remove_group */ + NULL, /* get_cb_real_name */ + NULL, /* set_chat_topic */ + NULL, /* find_blist_chat */ + NULL, /* roomlist_get_list */ + NULL, /* roomlist_cancel */ + NULL, /* roomlist_expand_category */ + NULL, /* can_receive_file */ + NULL, /* send_file */ + NULL, /* new_xfer */ + msim_offline_message, /* offline_message */ + NULL, /* whiteboard_prpl_ops */ + msim_send_really_raw, /* send_raw */ + NULL, /* roomlist_room_serialize */ + NULL, /* _purple_reserved1 */ + NULL, /* _purple_reserved2 */ + NULL, /* _purple_reserved3 */ + NULL /* _purple_reserved4 */ +}; + + + +/** Based on MSN's plugin info comments. */ +PurplePluginInfo info = +{ + PURPLE_PLUGIN_MAGIC, + PURPLE_MAJOR_VERSION, + PURPLE_MINOR_VERSION, + PURPLE_PLUGIN_PROTOCOL, /**< type */ + NULL, /**< ui_requirement */ + 0, /**< flags */ + NULL, /**< dependencies */ + PURPLE_PRIORITY_DEFAULT, /**< priority */ + + "prpl-myspace", /**< id */ + "MySpaceIM", /**< name */ + "0.10", /**< version */ + /** summary */ + "MySpaceIM Protocol Plugin", + /** description */ + "MySpaceIM Protocol Plugin", + "Jeff Connelly ", /**< author */ + "http://developer.pidgin.im/wiki/MySpaceIM/", /**< homepage */ + + msim_load, /**< load */ + NULL, /**< unload */ + NULL, /**< destroy */ + NULL, /**< ui_info */ + &prpl_info, /**< extra_info */ + NULL, /**< prefs_info */ + + /* msim_actions */ + NULL, + + NULL, /**< reserved1 */ + NULL, /**< reserved2 */ + NULL, /**< reserved3 */ + NULL /**< reserved4 */ +}; + + +#ifdef MSIM_SELF_TEST +/** Test functions. + * Used to test or try out the internal workings of msimprpl. If you're reading + * this code for the first time, these functions can be instructive in how + * msimprpl is architected. + */ +void +msim_test_all(void) +{ + guint failures; + + + failures = 0; + failures += msim_test_xml(); + failures += msim_test_msg(); + failures += msim_test_escaping(); + + if (failures) + { + purple_debug_info("msim", "msim_test_all HAD FAILURES: %d\n", failures); + } + else + { + purple_debug_info("msim", "msim_test_all - all tests passed!\n"); + } + exit(0); +} + +int +msim_test_xml(void) +{ + gchar *msg_text; + xmlnode *root, *n; + guint failures; + char *s; + int len; + + failures = 0; + + msg_text = "

woo!xxxyyy

"; + + purple_debug_info("msim", "msim_test_xml: msg_text=%s\n", msg_text); + + root = xmlnode_from_str(msg_text, -1); + if (!root) + { + purple_debug_info("msim", "there is no root\n"); + exit(0); + } + + purple_debug_info("msim", "root name=%s, child name=%s\n", root->name, + root->child->name); + + purple_debug_info("msim", "last child name=%s\n", root->lastchild->name); + purple_debug_info("msim", "Root xml=%s\n", + xmlnode_to_str(root, &len)); + purple_debug_info("msim", "Child xml=%s\n", + xmlnode_to_str(root->child, &len)); + purple_debug_info("msim", "Lastchild xml=%s\n", + xmlnode_to_str(root->lastchild, &len)); + purple_debug_info("msim", "Next xml=%s\n", + xmlnode_to_str(root->next, &len)); + purple_debug_info("msim", "Next data=%s\n", + xmlnode_get_data(root->next)); + purple_debug_info("msim", "Child->next xml=%s\n", + xmlnode_to_str(root->child->next, &len)); + + for (n = root->child; n; n = n->next) + { + if (n->name) + { + purple_debug_info("msim", " ** n=%s\n",n->name); + } else { + purple_debug_info("msim", " ** n data=%s\n", n->data); + } + } + + purple_debug_info("msim", "root data=%s, child data=%s, child 'h'=%s\n", + xmlnode_get_data(root), + xmlnode_get_data(root->child), + xmlnode_get_attrib(root->child, "h")); + + + for (n = root->child; + n != NULL; + n = n->next) + { + purple_debug_info("msim", "next name=%s\n", n->name); + } + + s = xmlnode_to_str(root, &len); + s[len] = 0; + + purple_debug_info("msim", "str: %s\n", s); + g_free(s); + + xmlnode_free(root); + + exit(0); + + return failures; +} + +/** Test MsimMessage for basic functionality. */ +int +msim_test_msg(void) +{ + MsimMessage *msg, *msg_cloned; + gchar *packed, *packed_expected, *packed_cloned; + guint failures; + + failures = 0; + + purple_debug_info("msim", "\n\nTesting MsimMessage\n"); + msg = msim_msg_new(FALSE); /* Create a new, empty message. */ + + /* Append some new elements. */ + msg = msim_msg_append(msg, "bx", MSIM_TYPE_BINARY, g_string_new_len(g_strdup("XXX"), 3)); + msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v1")); + msg = msim_msg_append(msg, "k1", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(42)); + msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v43")); + msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v52/xxx\\yyy")); + msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v7")); + msim_msg_dump("msg debug str=%s\n", msg); + packed = msim_msg_pack(msg); + + purple_debug_info("msim", "msg packed=%s\n", packed); + + packed_expected = "\\bx\\WFhY\\k1\\v1\\k1\\42\\k1" + "\\v43\\k1\\v52/1xxx/2yyy\\k1\\v7\\final\\"; + + if (0 != strcmp(packed, packed_expected)) + { + purple_debug_info("msim", "!!!(%d), msim_msg_pack not what expected: %s != %s\n", + ++failures, packed, packed_expected); + } + + + msg_cloned = msim_msg_clone(msg); + packed_cloned = msim_msg_pack(msg_cloned); + + purple_debug_info("msim", "msg cloned=%s\n", packed_cloned); + if (0 != strcmp(packed, packed_cloned)) + { + purple_debug_info("msim", "!!!(%d), msim_msg_pack on cloned message not equal to original: %s != %s\n", + ++failures, packed_cloned, packed); + } + + g_free(packed); + g_free(packed_cloned); + msim_msg_free(msg_cloned); + msim_msg_free(msg); + + return failures; +} + +/** Test protocol-level escaping/unescaping. */ +int +msim_test_escaping(void) +{ + guint failures; + gchar *raw, *escaped, *unescaped, *expected; + + failures = 0; + + purple_debug_info("msim", "\n\nTesting escaping\n"); + + raw = "hello/world\\hello/world"; + + escaped = msim_escape(raw); + purple_debug_info("msim", "msim_test_escaping: raw=%s, escaped=%s\n", raw, escaped); + expected = "hello/1world/2hello/1world"; + if (0 != strcmp(escaped, expected)) + { + purple_debug_info("msim", "!!!(%d), msim_escape failed: %s != %s\n", + ++failures, escaped, expected); + } + + + unescaped = msim_unescape(escaped); + g_free(escaped); + purple_debug_info("msim", "msim_test_escaping: unescaped=%s\n", unescaped); + if (0 != strcmp(raw, unescaped)) + { + purple_debug_info("msim", "!!!(%d), msim_unescape failed: %s != %s\n", + ++failures, raw, unescaped); + } + + return failures; +} +#endif + +/** Initialize plugin. */ +void +init_plugin(PurplePlugin *plugin) +{ + PurpleAccountOption *option; +#ifdef MSIM_SELF_TEST + msim_test_all(); +#endif /* MSIM_SELF_TEST */ + + /* TODO: default to automatically try different ports. Make the user be + * able to set the first port to try (like LastConnectedPort in Windows client). */ + option = purple_account_option_string_new(_("Connect server"), "server", MSIM_SERVER); + prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); + + option = purple_account_option_int_new(_("Connect port"), "port", MSIM_PORT); + prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); + + option = purple_account_option_bool_new(_("Show display name in status text"), "show_display_name", TRUE); + prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); + + option = purple_account_option_bool_new(_("Show headline in status text"), "show_headline", TRUE); + prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); +} + +PURPLE_INIT_PLUGIN(myspace, init_plugin, info);