Mercurial > pidgin.yaz
diff libpurple/protocols/myspace/myspace.c @ 25005:401f548e3544
propagate from branch 'im.pidgin.pidgin' (head df6eba32e5b6b34d7483cbfb7e9f2e4c836ac35f)
to branch 'org.darkrain42.pidgin.buddy-add' (head 6831808999a270f8c1a128c7430a73d3dc0bfae2)
author | Paul Aurich <paul@darkrain42.org> |
---|---|
date | Sun, 21 Dec 2008 18:32:37 +0000 |
parents | 125cac3e24ee 22fd7467f0cc |
children | ae544623840b |
line wrap: on
line diff
--- a/libpurple/protocols/myspace/myspace.c Sat Nov 29 18:46:49 2008 +0000 +++ b/libpurple/protocols/myspace/myspace.c Sun Dec 21 18:32:37 2008 +0000 @@ -1,4 +1,5 @@ -/* MySpaceIM Protocol Plugin +/** + * MySpaceIM Protocol Plugin * * \author Jeff Connelly * @@ -35,87 +36,423 @@ #include "myspace.h" -/* Internal functions */ - -#ifdef MSIM_DEBUG_MSG -static void print_hash_item(gpointer key, gpointer value, gpointer user_data); -#endif - -static int msim_send_really_raw(PurpleConnection *gc, const char *buf, int total_bytes); -static gboolean msim_login_challenge(MsimSession *session, MsimMessage *msg); -static gchar *msim_compute_login_response(const gchar nonce[2 * NONCE_SIZE], const gchar *email, const gchar *password, guint *response_len); - -static gboolean msim_incoming_bm_record_cv(MsimSession *session, MsimMessage *msg); -static gboolean msim_incoming_bm(MsimSession *session, MsimMessage *msg); -static gboolean msim_incoming_status(MsimSession *session, MsimMessage *msg); -static gboolean msim_incoming_im(MsimSession *session, MsimMessage *msg); -/* static gboolean msim_incoming_zap(MsimSession *session, MsimMessage *msg); - in zap.c */ -static gboolean msim_incoming_action(MsimSession *session, MsimMessage *msg); -static gboolean msim_incoming_media(MsimSession *session, MsimMessage *msg); -static gboolean msim_incoming_unofficial_client(MsimSession *session, - MsimMessage *msg); - -#ifdef MSIM_SEND_CLIENT_VERSION -static gboolean msim_send_unofficial_client(MsimSession *session, gchar *username); -#endif - -static void msim_get_info_cb(MsimSession *session, MsimMessage *userinfo, gpointer data); - -static void msim_set_status_code(MsimSession *session, guint code, gchar *statstring); - -static gboolean msim_process_server_info(MsimSession *session, MsimMessage *msg); -static gboolean msim_web_challenge(MsimSession *session, MsimMessage *msg); -static gboolean msim_process_reply(MsimSession *session, MsimMessage *msg); - -static gboolean msim_preprocess_incoming(MsimSession *session, MsimMessage *msg); - -#ifdef MSIM_USE_KEEPALIVE -static gboolean msim_check_alive(gpointer data); -#endif - -static gboolean msim_is_username_set(MsimSession *session, MsimMessage *msg); - -static gboolean msim_process(MsimSession *session, MsimMessage *msg); - -static MsimMessage *msim_do_postprocessing(MsimMessage *msg, const gchar *uid_field_name, const gchar *uid_before, guint uid); -static void msim_postprocess_outgoing_cb(MsimSession *session, MsimMessage *userinfo, gpointer data); -static gboolean msim_postprocess_outgoing(MsimSession *session, MsimMessage *msg, const gchar *username, const gchar *uid_field_name, const gchar *uid_before); - -static gboolean msim_error(MsimSession *session, MsimMessage *msg); - -static void msim_check_inbox_cb(MsimSession *session, MsimMessage *userinfo, gpointer data); -static gboolean msim_check_inbox(gpointer data); - -static void msim_input_cb(gpointer gc_uncasted, gint source, PurpleInputCondition cond); - - -static void msim_connect_cb(gpointer data, gint source, const gchar *error_message); - -static void msim_import_friends(PurplePluginAction *action); -static void msim_import_friends_cb(MsimSession *session, MsimMessage *reply, gpointer user_data); -static gboolean msim_get_contact_list(MsimSession *session, int what_to_do_after); - -static gboolean msim_uri_handler(const gchar *proto, const gchar *cmd, GHashTable *params); -static void msim_uri_handler_addContact_cb(MsimSession *session, MsimMessage *userinfo, gpointer data); -static void msim_uri_handler_sendIM_cb(MsimSession *session, MsimMessage *userinfo, gpointer data); - -/** - * Load the plugin. +#include "privacy.h" + +static void msim_set_status(PurpleAccount *account, PurpleStatus *status); +static void msim_set_idle(PurpleConnection *gc, int time); + +/** + * 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 "<uid>" 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(). + */ +static MsimMessage * +msim_do_postprocessing(MsimMessage *msg, const gchar *uid_before, + const gchar *uid_field_name, guint uid) +{ + MsimMessageElement *elem; + + /* First, check - if the field already exists, replace <uid> within it */ + if ((elem = msim_msg_get(msg, uid_field_name)) != NULL) { + gchar *fmt_string; + gchar *uid_str, *new_str; + + /* Get the packed element, flattening it. This allows <uid> to be + * replaced within nested data structures, since the replacement is done + * on the linear, packed data, not on a complicated data structure. + * + * For example, if the field was originally a dictionary or a list, you + * would have to iterate over all the items in it to see what needs to + * be replaced. But by packing it first, the <uid> marker is easily replaced + * just by a string replacement. + */ + fmt_string = msim_msg_pack_element_data(elem); + + uid_str = g_strdup_printf("%d", uid); + new_str = purple_strreplace(fmt_string, "<uid>", uid_str); + g_free(uid_str); + g_free(fmt_string); + + /* Free the old element data */ + msim_msg_free_element_data(elem->data); + + /* Replace it with our new data */ + elem->data = new_str; + elem->type = MSIM_TYPE_RAW; + + } 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 + */ +static void +msim_postprocess_outgoing_cb(MsimSession *session, const MsimMessage *userinfo, + gpointer data) +{ + gchar *uid_field_name, *uid_before, *username; + guint uid; + MsimMessage *msg, *body; + + msg = (MsimMessage *)data; + + /* Obtain userid from userinfo message. */ + body = msim_msg_get_dictionary(userinfo, "body"); + g_return_if_fail(body != NULL); + + uid = msim_msg_get_integer(body, "UserID"); + msim_msg_free(body); + + username = msim_msg_get_string(msg, "_username"); + + if (!uid) { + gchar *msg; + + msg = g_strdup_printf(_("No such user: %s"), username); + if (!purple_conv_present_error(username, session->account, msg)) { + purple_notify_error(NULL, NULL, _("User lookup"), msg); + } + + g_free(msg); + g_free(username); + /* TODO: free + * msim_msg_free(msg); + */ + return; + } + + 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, uid); + + /* Send */ + if (!msim_msg_send(session, msg)) { + msim_msg_dump("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); + g_free(username); + /* TODO: free + * 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 TRUE if successful. + */ +static 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; + + g_return_val_if_fail(msg != NULL, FALSE); + + /* Store information for msim_postprocess_outgoing_cb(). */ + 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 ? username : "(NULL)"); + /* 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 ? username : "(NULL)", uid); + + msg = msim_do_postprocessing(msg, uid_before, uid_field_name, uid); + + rc = msim_msg_send(session, msg); + + /* TODO: free + * msim_msg_free(msg); + */ + + return rc; +} + +/** + * 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_load(PurplePlugin *plugin) +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; + + g_return_val_if_fail(from_username != NULL, FALSE); + + purple_debug_info("msim", "sending %d message from %s to %s: %s\n", + type, from_username, who, text); + + msg = msim_msg_new( + "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; +} + +/** + * Lookup a username by userid, from buddy list. + * + * @param wanted_uid + * + * @return Username of wanted_uid, if on blist, or NULL. + * This is a static string, so don't free it. Copy it if needed. + * + */ +static const gchar * +msim_uid2username_from_blist(PurpleAccount *account, guint wanted_uid) +{ + GSList *buddies, *cur; + const gchar *ret; + + buddies = purple_find_buddies(account, NULL); + + if (!buddies) + { + purple_debug_info("msim", "msim_uid2username_from_blist: no buddies?\n"); + return NULL; + } + + ret = NULL; + + for (cur = buddies; cur != NULL; cur = g_slist_next(cur)) + { + PurpleBuddy *buddy; + guint uid; + const gchar *name; + + /* See finch/gnthistory.c */ + buddy = cur->data; + + uid = purple_blist_node_get_int(&buddy->node, "UserID"); + name = purple_buddy_get_name(buddy); + + if (uid == wanted_uid) + { + ret = name; + break; + } + } + + g_slist_free(buddies); + return ret; +} + +/** + * 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, or -1. + * Put the rid in your request, 'rid' field. + * + * 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) { - /* 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; + guint rid; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), -1); + + 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; +} + +/** + * 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. + */ +static 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"; +} + +/** + * 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. + */ +static char * +msim_status_text(PurpleBuddy *buddy) +{ + MsimSession *session; + MsimUser *user; + const gchar *display_name, *headline; + + g_return_val_if_fail(buddy != NULL, NULL); + + user = msim_get_user_from_buddy(buddy, TRUE); + + 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_headline", TRUE)) { + headline = user->headline; } - return TRUE; + + if (purple_account_get_bool(session->account, "show_display_name", FALSE)) { + display_name = user->display_name; + } + + /* Return appropriate combination of display name and/or headline, or neither. */ + + if (display_name && headline) { + return g_strconcat(display_name, " ", headline, NULL); + } else if (display_name) { + return g_strdup(display_name); + } else 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. + */ +static void +msim_tooltip_text(PurpleBuddy *buddy, PurpleNotifyUserInfo *user_info, + gboolean full) +{ + MsimUser *user; + + g_return_if_fail(buddy != NULL); + g_return_if_fail(user_info != NULL); + + user = msim_get_user_from_buddy(buddy, TRUE); + + 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? */ + + /* TODO: request information? have to figure out how to do + * the asynchronous lookup like oscar does (tooltip shows + * 'retrieving...' if not yet available, then changes when it is). + * + * Right now, only show what we have on hand. + */ + + /* Show abbreviated user info. */ + msim_append_user_info(session, user_info, user, FALSE); + } } /** @@ -123,7 +460,7 @@ * * @return GList of status types. */ -GList * +static GList * msim_status_types(PurpleAccount *acct) { GList *types; @@ -177,154 +514,141 @@ } /** - * 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. + * Compute the base64'd login challenge response based on username, password, nonce, and IPs. * - * @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"; -} - -#ifdef MSIM_DEBUG_MSG -static void -print_hash_item(gpointer key, gpointer value, gpointer user_data) -{ - purple_debug_info("msim", "%s=%s\n", - key ? (gchar *)key : "(NULL)", - value ? (gchar *)value : "(NULL)"); -} -#endif - -/** - * Send raw data (given as a NUL-terminated string) to the server. + * @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. * - * @param session - * @param msg The raw data to send, in a NUL-terminated string. - * - * @return TRUE if succeeded, FALSE if not. - * + * @return Binary login challenge response, ready to send to the server. + * Must be g_free()'d when finished. NULL if error. */ -gboolean -msim_send_raw(MsimSession *session, const gchar *msg) -{ - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - purple_debug_info("msim", "msim_send_raw: writing <%s>\n", msg); - - 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. - */ -static int -msim_send_really_raw(PurpleConnection *gc, const char *buf, int total_bytes) +static gchar * +msim_compute_login_response(const gchar nonce[2 * NONCE_SIZE], + const gchar *email, const gchar *password, guint *response_len) { - 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("msim", "logging in %s\n", acct->username); - - gc = purple_account_get_connection(acct); - gc->proto_data = msim_session_new(acct); - gc->flags |= PURPLE_CONNECTION_HTML | PURPLE_CONNECTION_NO_URLDESC; - - /* 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)) { - /* TODO: try other ports if in auto mode, then save - * working port and try that first next time. */ - purple_connection_error_reason (gc, - PURPLE_CONNECTION_ERROR_NETWORK_ERROR, - _("Couldn't create socket")); - return; + PurpleCipherContext *key_context; + PurpleCipher *sha1; + PurpleCipherContext *rc4; + + guchar hash_pw[HASH_SIZE]; + guchar key[HASH_SIZE]; + gchar *password_utf16le, *password_utf8_lc; + GString *data; + guchar *data_out; + size_t 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 password to lowercase (required for passwords containing + * uppercase characters). MySpace passwords are lowercase, + * see ticket #2066. */ + password_utf8_lc = g_utf8_strdown(password, -1); + + /* 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_utf8_lc, -1, "UTF-16LE", "UTF-8", + &conv_bytes_read, &conv_bytes_written, &conv_error); + g_free(password_utf8_lc); + + 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); + purple_cipher_context_destroy(key_context); + +#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 = g_string_new(NULL); + g_string_append_len(data, nonce, NONCE_SIZE); + g_string_append(data, email); + g_string_append_len(data, MSIM_LOGIN_IP_LIST, MSIM_LOGIN_IP_LIST_LEN); + + data_out = g_new0(guchar, data->len); + + purple_cipher_context_encrypt(rc4, (const guchar *)data->str, + data->len, data_out, &data_out_len); + purple_cipher_context_destroy(rc4); + + if (data_out_len != data->len) { + purple_debug_info("msim", "msim_compute_login_response: " + "data length mismatch: %" G_GSIZE_FORMAT " != %" + G_GSIZE_FORMAT "\n", data_out_len, data->len); + } + + g_string_free(data, TRUE); + +#ifdef MSIM_DEBUG_LOGIN_CHALLENGE + purple_debug_info("msim", "response=<%s>\n", data_out); +#endif + + *response_len = data_out_len; + + return (gchar *)data_out; } /** - * Process a login challenge, sending a response. + * Process a login challenge, sending a response. * - * @param session + * @param session * @param msg Login challenge message. * * @return TRUE if successful, FALSE if not */ -static gboolean -msim_login_challenge(MsimSession *session, MsimMessage *msg) +static gboolean +msim_login_challenge(MsimSession *session, MsimMessage *msg) { PurpleAccount *account; gchar *response; @@ -383,230 +707,552 @@ } /** - * 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. + * Process unrecognized information. * - * @return Binary login challenge response, ready to send to the server. - * Must be g_free()'d when finished. NULL if error. + * @param session + * @param msg An MsimMessage that was unrecognized, or NULL. + * @param note Information on what was unrecognized, or NULL. */ -static gchar * -msim_compute_login_response(const gchar nonce[2 * NONCE_SIZE], - const gchar *email, const gchar *password, guint *response_len) +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). + * + * Filed enhancement ticket for libpurple as #4688. + */ + + purple_debug_info("msim", "Unrecognized data on account for %s\n", + (session && session->account && session->account->username) ? + session->account->username : "(NULL)"); + if (note) { + purple_debug_info("msim", "(Note: %s)\n", note); + } + + if (msg) { + msim_msg_dump("Unrecognized message dump: %s\n", msg); + } +} + +/** Called when the session key arrives to check whether the user + * has a username, and set one if desired. */ +static gboolean +msim_is_username_set(MsimSession *session, MsimMessage *msg) { - PurpleCipherContext *key_context; - PurpleCipher *sha1; - PurpleCipherContext *rc4; - - guchar hash_pw[HASH_SIZE]; - guchar key[HASH_SIZE]; - gchar *password_utf16le, *password_utf8_lc; - 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 password to lowercase (required for passwords containing - * uppercase characters). MySpace passwords are lowercase, - * see ticket #2066. */ - password_utf8_lc = g_utf8_strdown(password, -1); - - /* 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_utf8_lc, -1, "UTF-16LE", "UTF-8", - &conv_bytes_read, &conv_bytes_written, &conv_error); - g_free(password_utf8_lc); - - 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; + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + g_return_val_if_fail(session->gc != NULL, FALSE); + + session->sesskey = msim_msg_get_integer(msg, "sesskey"); + purple_debug_info("msim", "SESSKEY=<%d>\n", session->sesskey); + + /* What is proof? Used to be uid, but now is 52 base64'd bytes... */ + + /* 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"); + + /* Save uid to account so this account can be looked up by uid. */ + purple_account_set_int(session->account, "uid", session->userid); + + /* Not sure what profileid is used for. */ + if (msim_msg_get_integer(msg, "profileid") != session->userid) { + msim_unrecognized(session, msg, + "Profile ID didn't match user ID, don't know why"); + } + + /* 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"); + + /* If user lacks a username, help them get one. */ + if (msim_msg_get_integer(msg, "uniquenick") == session->userid) { + purple_debug_info("msim_is_username_set", "no username is set\n"); + purple_request_yes_no(session->gc, + _("MySpaceIM - No Username Set"), + _("You appear to have no MySpace username."), + _("Would you like to set one now? (Note: THIS CANNOT BE CHANGED!)"), + 0, + session->account, + NULL, + NULL, + session->gc, + G_CALLBACK(msim_set_username_cb), + G_CALLBACK(msim_do_not_set_username_cb)); + purple_debug_info("msim_is_username_set","'username not set' alert prompted\n"); + return FALSE; + } + return TRUE; +} + +#ifdef MSIM_USE_KEEPALIVE +/** + * Check if the connection is still alive, based on last communication. + */ +static gboolean +msim_check_alive(gpointer data) +{ + MsimSession *session; + time_t delta; + + 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) { + purple_debug_info("msim", + "msim_check_alive: %zu > interval of %d, presumed dead\n", + delta, MSIM_KEEPALIVE_INTERVAL); + purple_connection_error_reason(session->gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Lost connection with server")); + + return FALSE; } - /* 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); - purple_cipher_context_destroy(key_context); - -#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"); + return TRUE; +} #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_free(data); - - if (data_out_len != data_len) { - purple_debug_info("msim", "msim_compute_login_response: " - "data length mismatch: %" G_GSIZE_FORMAT " != %" - G_GSIZE_FORMAT "\n", data_out_len, data_len); +/** + * Handle mail reply checks. + */ +static void +msim_check_inbox_cb(MsimSession *session, const MsimMessage *reply, gpointer data) +{ + MsimMessage *body; + guint old_inbox_status; + guint i, n; + const gchar *froms[5], *tos[5], *urls[5], *subjects[5]; + + /* Information for each new inbox message type. */ + static struct + { + const gchar *key; + guint bit; + const gchar *url; + const gchar *text; + } message_types[] = { + { "Mail", MSIM_INBOX_MAIL, "http://messaging.myspace.com/index.cfm?fuseaction=mail.inbox", NULL }, + { "BlogComment", MSIM_INBOX_BLOG_COMMENT, "http://blog.myspace.com/index.cfm?fuseaction=blog", NULL }, + { "ProfileComment", MSIM_INBOX_PROFILE_COMMENT, "http://home.myspace.com/index.cfm?fuseaction=user", NULL }, + { "FriendRequest", MSIM_INBOX_FRIEND_REQUEST, "http://messaging.myspace.com/index.cfm?fuseaction=mail.friendRequests", NULL }, + { "PictureComment", MSIM_INBOX_PICTURE_COMMENT, "http://home.myspace.com/index.cfm?fuseaction=user", NULL } + }; + + /* Can't write _()'d strings in array initializers. Workaround. */ + message_types[0].text = _("New mail messages"); + message_types[1].text = _("New blog comments"); + message_types[2].text = _("New profile comments"); + message_types[3].text = _("New friend requests!"); + message_types[4].text = _("New picture comments"); + + g_return_if_fail(reply != NULL); + + body = msim_msg_get_dictionary(reply, "body"); + + if (body == NULL) + return; + + old_inbox_status = session->inbox_status; + + n = 0; + + for (i = 0; i < sizeof(message_types) / sizeof(message_types[0]); ++i) { + const gchar *key; + guint bit; + + key = message_types[i].key; + bit = message_types[i].bit; + + if (msim_msg_get(body, key)) { + /* Notify only on when _changes_ from no mail -> has mail + * (edge triggered) */ + if (!(session->inbox_status & bit)) { + purple_debug_info("msim", "msim_check_inbox_cb: got %s, at %d\n", + key ? key : "(NULL)", n); + + subjects[n] = message_types[i].text; + froms[n] = _("MySpace"); + tos[n] = session->username; + /* TODO: append token, web challenge, so automatically logs in. + * Would also need to free strings because they won't be static + */ + urls[n] = message_types[i].url; + + ++n; + } else { + purple_debug_info("msim", + "msim_check_inbox_cb: already notified of %s\n", + key ? key : "(NULL)"); + } + + session->inbox_status |= bit; + } } -#ifdef MSIM_DEBUG_LOGIN_CHALLENGE - purple_debug_info("msim", "response=<%s>\n", data_out); -#endif - - *response_len = data_out_len; - - return (gchar *)data_out; + if (n) { + purple_debug_info("msim", + "msim_check_inbox_cb: notifying of %d\n", n); + + /* TODO: free strings with callback _if_ change to dynamic (w/ token) */ + purple_notify_emails(session->gc, /* handle */ + n, /* count */ + TRUE, /* detailed */ + subjects, froms, tos, urls, + NULL, /* PurpleNotifyCloseCallback cb */ + NULL); /* gpointer user_data */ + + } + + msim_msg_free(body); +} + +/** + * Send request to check if there is new mail. + */ +static gboolean +msim_check_inbox(gpointer data) +{ + MsimSession *session; + + session = (MsimSession *)data; + + if (!MSIM_SESSION_VALID(session)) { + purple_debug_info("msim", "msim_check_inbox: session invalid, stopping the mail check.\n"); + return FALSE; + } + + purple_debug_info("msim", "msim_check_inbox: checking mail\n"); + g_return_val_if_fail(msim_send(session, + "persist", MSIM_TYPE_INTEGER, 1, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + "cmd", MSIM_TYPE_INTEGER, MSIM_CMD_GET, + "dsn", MSIM_TYPE_INTEGER, MG_CHECK_MAIL_DSN, + "lid", MSIM_TYPE_INTEGER, MG_CHECK_MAIL_LID, + "uid", MSIM_TYPE_INTEGER, session->userid, + "rid", MSIM_TYPE_INTEGER, + msim_new_reply_callback(session, msim_check_inbox_cb, NULL), + "body", MSIM_TYPE_STRING, g_strdup(""), + NULL), TRUE); + + /* Always return true, so that we keep checking for mail. */ + return TRUE; } /** - * 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. + * Add contact from server to buddy list, after looking up username. + * Callback from msim_add_contact_from_server(). * - * @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(). + * @param data An MsimMessage * of the contact information. Will be freed. */ -int -msim_send_im(PurpleConnection *gc, const gchar *who, const gchar *message, - PurpleMessageFlags flags) +static void +msim_add_contact_from_server_cb(MsimSession *session, const MsimMessage *user_lookup_info, gpointer data) { - MsimSession *session; - gchar *message_msim; - int rc; - - 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); - - message_msim = html_to_msim_markup(session, message); - - if (msim_send_bm(session, who, message_msim, 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). - * If an error does occur, it should be logged to the IM window. - */ - rc = 1; + MsimMessage *contact_info, *user_lookup_info_body; + PurpleGroup *group; + PurpleBuddy *buddy; + MsimUser *user; + gchar *username, *group_name, *display_name; + guint uid, visibility; + + contact_info = (MsimMessage *)data; + purple_debug_info("msim_add_contact_from_server_cb", "contact_info addr=%p\n", contact_info); + uid = msim_msg_get_integer(contact_info, "ContactID"); + + if (!user_lookup_info) { + username = g_strdup(msim_uid2username_from_blist(session->account, uid)); + display_name = NULL; + g_return_if_fail(username != NULL); } else { - rc = -1; + user_lookup_info_body = msim_msg_get_dictionary(user_lookup_info, "body"); + username = msim_msg_get_string(user_lookup_info_body, "UserName"); + display_name = msim_msg_get_string(user_lookup_info_body, "DisplayName"); + msim_msg_free(user_lookup_info_body); + g_return_if_fail(username != NULL); + } + + purple_debug_info("msim_add_contact_from_server_cb", + "*** about to add/update username=%s\n", username); + + /* 1. Creates a new group, or gets existing group if it exists (or so + * the documentation claims). */ + group_name = msim_msg_get_string(contact_info, "GroupName"); + if (!group_name || (*group_name == '\0')) { + g_free(group_name); + group_name = g_strdup(_("IM Friends")); + purple_debug_info("myspace", "No GroupName specified, defaulting to '%s'.\n", group_name); + } + group = purple_find_group(group_name); + if (!group) { + group = purple_group_new(group_name); + /* Add group to beginning. See #2752. */ + purple_blist_add_group(group, NULL); + } + g_free(group_name); + + visibility = msim_msg_get_integer(contact_info, "Visibility"); + if (visibility == 2) { + /* This buddy is blocked (and therefore not on our buddy list */ + purple_privacy_deny_add(session->account, username, TRUE); + msim_msg_free(contact_info); + g_free(username); + g_free(display_name); + return; } - g_free(message_msim); - - return rc; + /* 2. Get or create buddy */ + buddy = purple_find_buddy(session->account, username); + if (!buddy) { + purple_debug_info("msim_add_contact_from_server_cb", + "creating new buddy: %s\n", username); + buddy = purple_buddy_new(session->account, username, NULL); + } + + /* TODO: use 'Position' in contact_info to take into account where buddy is */ + purple_blist_add_buddy(buddy, NULL, group, NULL /* insertion point */); + + if (strtol(username, NULL, 10) == uid) { + /* + * This user has not set their username! Set their server + * alias to their display name so that we don't see a bunch + * of numbers in the buddy list. + */ + if (display_name != NULL) { + purple_blist_node_set_string(&buddy->node, "DisplayName", display_name); + serv_got_alias(session->gc, username, display_name); + } else { + serv_got_alias(session->gc, username, + purple_blist_node_get_string(&buddy->node, "DisplayName")); + } + } + g_free(display_name); + + /* 3. Update buddy information */ + user = msim_get_user_from_buddy(buddy, TRUE); + + user->id = uid; + /* Keep track of the user ID across sessions */ + purple_blist_node_set_int(&buddy->node, "UserID", uid); + + /* Stores a few fields in the MsimUser, relevant to the buddy itself. + * AvatarURL, Headline, ContactID. */ + msim_store_user_info(session, contact_info, NULL); + + /* TODO: other fields, store in 'user' */ + msim_msg_free(contact_info); + + g_free(username); } -/** Send a buddy message of a given type. +/** + * Add first ContactID in contact_info to buddy's list. Used to add + * server-side buddies to client-side list. * - * @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. + * @return TRUE if added. + */ +static gboolean +msim_add_contact_from_server(MsimSession *session, MsimMessage *contact_info) +{ + guint uid; + const gchar *username; + + uid = msim_msg_get_integer(contact_info, "ContactID"); + g_return_val_if_fail(uid != 0, FALSE); + + /* Lookup the username, since NickName and IMName is unreliable */ + username = msim_uid2username_from_blist(session->account, uid); + if (!username) { + gchar *uid_str; + + uid_str = g_strdup_printf("%d", uid); + purple_debug_info("msim_add_contact_from_server", + "contact_info addr=%p\n", contact_info); + msim_lookup_user(session, uid_str, msim_add_contact_from_server_cb, (gpointer)msim_msg_clone(contact_info)); + g_free(uid_str); + } else { + msim_add_contact_from_server_cb(session, NULL, (gpointer)msim_msg_clone(contact_info)); + } + + /* Say that the contact was added, even if we're still looking up + * their username. */ + return TRUE; +} + +/** + * Called when contact list is received from server. + */ +static void +msim_got_contact_list(MsimSession *session, const MsimMessage *reply, gpointer user_data) +{ + MsimMessage *body, *body_node; + gchar *msg; + guint buddy_count; + + body = msim_msg_get_dictionary(reply, "body"); + if (!body) { + /* No friends. Not an error. */ + return; + } + + buddy_count = 0; + + for (body_node = body; + body_node != NULL; + body_node = msim_msg_get_next_element_node(body_node)) + { + MsimMessageElement *elem; + + elem = (MsimMessageElement *)body_node->data; + + if (g_str_equal(elem->name, "ContactID")) + { + /* Will look for first contact in body_node */ + if (msim_add_contact_from_server(session, body_node)) { + ++buddy_count; + } + } + } + + switch (GPOINTER_TO_UINT(user_data)) { + case MSIM_CONTACT_LIST_IMPORT_ALL_FRIENDS: + msg = g_strdup_printf(ngettext("%d buddy was added or updated from the server (including buddies already on the server-side list)", + "%d buddies were added or updated from the server (including buddies already on the server-side list)", + buddy_count), + buddy_count); + purple_notify_info(session->account, _("Add contacts from server"), msg, NULL); + g_free(msg); + break; + + case MSIM_CONTACT_LIST_IMPORT_TOP_FRIENDS: + /* TODO */ + break; + + case MSIM_CONTACT_LIST_INITIAL_FRIENDS: + /* Nothing */ + break; + } + + msim_msg_free(body); +} + +/** + * Get contact list, calling msim_got_contact_list() with + * what_to_do_after as user_data gpointer. * + * @param what_to_do_after should be one of the MSIM_CONTACT_LIST_* #defines. */ -gboolean -msim_send_bm(MsimSession *session, const gchar *who, const gchar *text, - int type) +static gboolean +msim_get_contact_list(MsimSession *session, int what_to_do_after) { - gboolean rc; - MsimMessage *msg; - const gchar *from_username; + return msim_send(session, + "persist", MSIM_TYPE_INTEGER, 1, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + "cmd", MSIM_TYPE_INTEGER, MSIM_CMD_GET, + "dsn", MSIM_TYPE_INTEGER, MG_LIST_ALL_CONTACTS_DSN, + "lid", MSIM_TYPE_INTEGER, MG_LIST_ALL_CONTACTS_LID, + "uid", MSIM_TYPE_INTEGER, session->userid, + "rid", MSIM_TYPE_INTEGER, + msim_new_reply_callback(session, msim_got_contact_list, GUINT_TO_POINTER(what_to_do_after)), + "body", MSIM_TYPE_STRING, g_strdup(""), + NULL); +} + +/** Called after username is set, if necessary and we're open for business. */ +gboolean msim_we_are_logged_on(MsimSession *session) +{ + MsimMessage *body; 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; - - g_return_val_if_fail(from_username != NULL, FALSE); - - purple_debug_info("msim", "sending %d message from %s to %s: %s\n", - type, from_username, who, text); - - msg = msim_msg_new( - "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), + + /* Set display name to username (otherwise will show email address) */ + purple_connection_set_display_name(session->gc, session->username); + + /* The session is now set up, ready to be connected. This emits the + * signedOn signal, so clients can now do anything with msimprpl, and + * we're ready for it (session key, userid, username all setup). */ + purple_connection_update_progress(session->gc, _("Connected"), 3, 4); + purple_connection_set_state(session->gc, PURPLE_CONNECTED); + + body = msim_msg_new( + "UserID", MSIM_TYPE_INTEGER, session->userid, + NULL); + + /* Request IM info about ourself. */ + msim_send(session, + "persist", MSIM_TYPE_INTEGER, 1, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + "cmd", MSIM_TYPE_INTEGER, MSIM_CMD_GET, + "dsn", MSIM_TYPE_INTEGER, MG_OWN_MYSPACE_INFO_DSN, + "lid", MSIM_TYPE_INTEGER, MG_OWN_MYSPACE_INFO_LID, + "rid", MSIM_TYPE_INTEGER, session->next_rid++, + "UserID", MSIM_TYPE_INTEGER, session->userid, + "body", MSIM_TYPE_DICTIONARY, body, + NULL); + + /* Request MySpace info about ourself. */ + msim_send(session, + "persist", MSIM_TYPE_INTEGER, 1, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + "cmd", MSIM_TYPE_INTEGER, MSIM_CMD_GET, + "dsn", MSIM_TYPE_INTEGER, MG_OWN_IM_INFO_DSN, + "lid", MSIM_TYPE_INTEGER, MG_OWN_IM_INFO_LID, + "rid", MSIM_TYPE_INTEGER, session->next_rid++, + "body", MSIM_TYPE_STRING, g_strdup(""), NULL); - rc = msim_postprocess_outgoing(session, msg, who, "t", "cv"); - - msim_msg_free(msg); - - return rc; + /* TODO: set options (persist cmd=514,dsn=1,lid=10) */ + /* TODO: set blocklist */ + + /* Notify servers of our current status. */ + purple_debug_info("msim", "msim_we_are_logged_on: notifying servers of status\n"); + msim_set_status(session->account, + purple_account_get_active_status(session->account)); + + /* TODO: setinfo */ + /* + body = msim_msg_new( + "TotalFriends", MSIM_TYPE_INTEGER, 666, + NULL); + msim_send(session, + "setinfo", MSIM_TYPE_BOOLEAN, TRUE, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + "info", MSIM_TYPE_DICTIONARY, body, + NULL); + */ + + /* Disable due to problems with timeouts. TODO: fix. */ +#ifdef MSIM_USE_KEEPALIVE + purple_timeout_add(MSIM_KEEPALIVE_INTERVAL_CHECK, + (GSourceFunc)msim_check_alive, session); +#endif + + /* Check mail if they want to. */ + if (purple_account_get_check_mail(session->account)) { + session->inbox_handle = purple_timeout_add(MSIM_MAIL_INTERVAL_CHECK, + (GSourceFunc)msim_check_inbox, session); + msim_check_inbox(session); + } + + msim_get_contact_list(session, MSIM_CONTACT_LIST_INITIAL_FRIENDS); + + return TRUE; } - -/** Record the client version in the buddy list, from an incoming message. */ +/** + * Record the client version in the buddy list, from an incoming message. + */ static gboolean msim_incoming_bm_record_cv(MsimSession *session, MsimMessage *msg) { @@ -639,44 +1285,183 @@ return ret; } -/** Handle an incoming buddy message. */ +#ifdef MSIM_SEND_CLIENT_VERSION +/** + * Send our client version to another unofficial client that understands it. + */ static gboolean -msim_incoming_bm(MsimSession *session, MsimMessage *msg) +msim_send_unofficial_client(MsimSession *session, gchar *username) +{ + gchar *our_info; + gboolean ret; + + our_info = g_strdup_printf("Libpurple %d.%d.%d - msimprpl %s", + PURPLE_MAJOR_VERSION, + PURPLE_MINOR_VERSION, + PURPLE_MICRO_VERSION, + MSIM_PRPL_VERSION_STRING); + + ret = msim_send_bm(session, username, our_info, MSIM_BM_UNOFFICIAL_CLIENT); + + return ret; +} +#endif + +/** + * Process incoming status messages. + * + * @param session + * @param msg Status update message. Caller frees. + * + * @return TRUE if successful. + */ +static gboolean +msim_incoming_status(MsimSession *session, MsimMessage *msg) { - guint bm; - - bm = msim_msg_get_integer(msg, "bm"); - - msim_incoming_bm_record_cv(session, msg); - - switch (bm) { - case MSIM_BM_STATUS: - return msim_incoming_status(session, msg); - case MSIM_BM_INSTANT: - return msim_incoming_im(session, msg); - case MSIM_BM_ACTION: - return msim_incoming_action(session, msg); - case MSIM_BM_MEDIA: - return msim_incoming_media(session, msg); - case MSIM_BM_UNOFFICIAL_CLIENT: - return msim_incoming_unofficial_client(session, msg); + PurpleBuddyList *blist; + MsimUser *user; + GList *list; + gchar *status_headline, *status_headline_escaped; + gint status_code, purple_status_code; + gchar *username; + gchar *unrecognized_msg; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + /* Helpfully looked up by msim_incoming_resolve() for us. */ + username = msim_msg_get_string(msg, "_username"); + g_return_val_if_fail(username != NULL, FALSE); + + { + gchar *ss; + + ss = msim_msg_get_string(msg, "msg"); + purple_debug_info("msim", + "msim_status: updating status for <%s> to <%s>\n", + username, ss ? ss : "(NULL)"); + g_free(ss); + } + + /* Example fields: + * |s|0|ss|Offline + * |s|1|ss|:-)|ls||ip|0|p|0 + */ + list = msim_msg_get_list(msg, "msg"); + + status_code = msim_msg_get_integer_from_element(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 = msim_msg_get_string_from_element(g_list_nth_data(list, MSIM_STATUS_ORDINAL_HEADLINE)); + + blist = purple_get_blist(); + + /* Add buddy if not found. + * TODO: Could this be responsible for #3444? */ + user = msim_find_user(session, username); + if (!user) { + PurpleBuddy *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); + + user = msim_get_user_from_buddy(buddy, TRUE); + user->id = msim_msg_get_integer(msg, "f"); + + /* Keep track of the user ID across sessions */ + purple_blist_node_set_int(&buddy->node, "UserID", user->id); + + msim_store_user_info(session, msg, NULL); + } else { + purple_debug_info("msim", "msim_status: found buddy %s\n", username); + } + + if (status_headline && strcmp(status_headline, "") != 0) { + /* The status headline is plaintext, but libpurple treats it as HTML, + * so escape any HTML characters to their entity equivalents. */ + status_headline_escaped = g_markup_escape_text(status_headline, -1); + } else { + status_headline_escaped = NULL; + } + + g_free(status_headline); + + /* don't copy; let the MsimUser own the headline, memory-wise */ + g_free(user->headline); + user->headline = status_headline_escaped; + + /* 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: + /* Treat idle as an available status. */ + purple_status_code = PURPLE_STATUS_AVAILABLE; + break; + default: - /* Not really an IM, but show it for informational - * purposes during development. */ - return msim_incoming_im(session, msg); + purple_debug_info("msim", "msim_incoming_status for %s, unknown status code %d, treating as available\n", + username, status_code); + purple_status_code = PURPLE_STATUS_AVAILABLE; + + unrecognized_msg = g_strdup_printf("msim_incoming_status, unrecognized status code: %d\n", + status_code); + msim_unrecognized(session, NULL, unrecognized_msg); + g_free(unrecognized_msg); + } + + purple_prpl_got_user_status(session->account, username, purple_primitive_get_id_from_type(purple_status_code), NULL); + + 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, 0); + } else { + /* All other statuses indicate going back to non-idle. */ + purple_prpl_got_user_idle(session->account, username, FALSE, 0); } + +#ifdef MSIM_SEND_CLIENT_VERSION + if (status_code == MSIM_STATUS_CODE_ONLINE) { + /* Secretly whisper to unofficial clients our own version as they come online */ + msim_send_unofficial_client(session, username); + } +#endif + + if (status_code != MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN) { + /* Get information when they come online. + * TODO: periodically refresh? + */ + purple_debug_info("msim_incoming_status", "%s came online, looking up\n", username); + msim_lookup_user(session, username, NULL, NULL); + } + + g_free(username); + msim_msg_list_free(list); + + return TRUE; } /** * Handle an incoming instant message. * * @param session The session - * @param msg Message from the server, containing 'f' (userid from) and 'msg'. + * @param msg Message from the server, containing 'f' (userid from) and 'msg'. * Should also contain username in _username from preprocessing. * * @return TRUE if successful. */ -static gboolean +static gboolean msim_incoming_im(MsimSession *session, MsimMessage *msg) { gchar *username, *msg_msim_markup, *msg_purple_markup; @@ -701,7 +1486,7 @@ g_free(username); return FALSE; } - + /* See if a conversation with their UID already exists...*/ conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, userid, session->account); if (conv) { @@ -730,50 +1515,14 @@ } /** - * 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). - * - * Filed enhancement ticket for libpurple as #4688. - */ - - purple_debug_info("msim", "Unrecognized data on account for %s\n", - (session && session->account && session->account->username) ? - session->account->username : "(NULL)"); - 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. - * */ -static gboolean +static gboolean msim_incoming_action(MsimSession *session, MsimMessage *msg) { gchar *msg_text, *username; @@ -819,7 +1568,7 @@ rc = TRUE; } else { - msim_unrecognized(session, msg, + msim_unrecognized(session, msg, "got to msim_incoming_action but unrecognized value for 'msg'"); rc = FALSE; } @@ -830,7 +1579,9 @@ return rc; } -/* Process an incoming media (message background?) message. */ +/** + * Process an incoming media (message background?) message. + */ static gboolean msim_incoming_media(MsimSession *session, MsimMessage *msg) { @@ -855,8 +1606,10 @@ return TRUE; } -/* Process an incoming "unofficial client" message. The plugin for - * Miranda IM sends this message with the plugin information. */ +/** + * Process an incoming "unofficial client" message. The plugin for + * Miranda IM sends this message with the plugin information. + */ static gboolean msim_incoming_unofficial_client(MsimSession *session, MsimMessage *msg) { @@ -873,7 +1626,7 @@ username, client_info); user = msim_find_user(session, username); - + g_return_val_if_fail(user != NULL, FALSE); if (user->client_info) { @@ -887,28 +1640,664 @@ return TRUE; } - -#ifdef MSIM_SEND_CLIENT_VERSION -/** Send our client version to another unofficial client that understands it. */ +/** + * Handle an incoming buddy message. + */ +static gboolean +msim_incoming_bm(MsimSession *session, MsimMessage *msg) +{ + guint bm; + + bm = msim_msg_get_integer(msg, "bm"); + + msim_incoming_bm_record_cv(session, msg); + + switch (bm) { + case MSIM_BM_STATUS: + return msim_incoming_status(session, msg); + case MSIM_BM_INSTANT: + return msim_incoming_im(session, msg); + case MSIM_BM_ACTION: + return msim_incoming_action(session, msg); + case MSIM_BM_MEDIA: + return msim_incoming_media(session, msg); + case MSIM_BM_UNOFFICIAL_CLIENT: + return msim_incoming_unofficial_client(session, msg); + default: + /* Not really an IM, but show it for informational + * purposes during development. */ + return msim_incoming_im(session, msg); + } +} + +/** + * Process the initial server information from the server. + */ +static gboolean +msim_process_server_info(MsimSession *session, MsimMessage *msg) +{ + MsimMessage *body; + + body = msim_msg_get_dictionary(msg, "body"); + g_return_val_if_fail(body != NULL, FALSE); + + /* Example body: +AdUnitRefreshInterval=10. +AlertPollInterval=360. +AllowChatRoomEmoticonSharing=False. +ChatRoomUserIDs=78744676;163733130;1300326231;123521495;142663391. +CurClientVersion=673. +EnableIMBrowse=True. +EnableIMStuffAvatars=False. +EnableIMStuffZaps=False. +MaxAddAllFriends=100. +MaxContacts=1000. +MinClientVersion=594. +MySpaceIM_ENGLISH=78744676. +MySpaceNowTimer=720. +PersistenceDataTimeout=900. +UseWebChallenge=1. +WebTicketGoHome=False + + Anything useful? TODO: use what is useful, and use it. +*/ + purple_debug_info("msim_process_server_info", + "maximum contacts: %d\n", + msim_msg_get_integer(body, "MaxContacts")); + + session->server_info = body; + /* session->server_info freed in msim_session_destroy */ + + return TRUE; +} + +/** + * Process a web challenge, used to login to the web site. + */ +static gboolean +msim_web_challenge(MsimSession *session, MsimMessage *msg) +{ + /* TODO: web challenge, store token. #2659. */ + return FALSE; +} + +/** + * Process a persistance message reply from the server. + * + * @param session + * @param msg Message reply from server. + * + * @return TRUE if successful. + * + * msim_lookup_user sets callback for here + */ +static gboolean +msim_process_reply(MsimSession *session, MsimMessage *msg) +{ + MSIM_USER_LOOKUP_CB cb; + gpointer data; + guint rid, cmd, dsn, lid; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + msim_store_user_info(session, msg, NULL); + + rid = msim_msg_get_integer(msg, "rid"); + cmd = msim_msg_get_integer(msg, "cmd"); + dsn = msim_msg_get_integer(msg, "dsn"); + lid = msim_msg_get_integer(msg, "lid"); + + /* Unsolicited messages */ + if (cmd == (MSIM_CMD_BIT_REPLY | MSIM_CMD_GET)) { + if (dsn == MG_SERVER_INFO_DSN && lid == MG_SERVER_INFO_LID) { + return msim_process_server_info(session, msg); + } else if (dsn == MG_WEB_CHALLENGE_DSN && lid == MG_WEB_CHALLENGE_LID) { + return msim_web_challenge(session, msg); + } + } + + /* 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_reply: calling callback now\n"); + /* Clone message, so that the callback 'cb' can use it (needs to free it also). */ + cb(session, 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_reply: 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. + */ static gboolean -msim_send_unofficial_client(MsimSession *session, gchar *username) +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 (sesskey=%d): %s\n", + session->sesskey, full_errmsg); + + /* Destroy session if fatal. */ + if (msim_msg_get(msg, "fatal")) { + PurpleConnectionError reason = PURPLE_CONNECTION_ERROR_NETWORK_ERROR; + purple_debug_info("msim", "fatal error, closing\n"); + + switch (err) { + case MSIM_ERROR_INCORRECT_PASSWORD: /* Incorrect password */ + reason = PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED; + if (!purple_account_get_remember_password(session->account)) + purple_account_set_password(session->account, NULL); +#ifdef MSIM_MAX_PASSWORD_LENGTH + if (session->account->password && (strlen(session->account->password) > MSIM_MAX_PASSWORD_LENGTH)) { + gchar *suggestion; + + suggestion = g_strdup_printf(_("%s Your password is " + "%d characters, greater than the " + "expected maximum length of %d for " + "MySpaceIM. Please shorten your " + "password at http://profileedit.myspace.com/index.cfm?fuseaction=accountSettings.changePassword and try again."), + full_errmsg, (int) + strlen(session->account->password), + MSIM_MAX_PASSWORD_LENGTH); + + /* Replace full_errmsg. */ + g_free(full_errmsg); + full_errmsg = suggestion; + } +#endif + break; + case MSIM_ERROR_LOGGED_IN_ELSEWHERE: /* Logged in elsewhere */ + reason = PURPLE_CONNECTION_ERROR_NAME_IN_USE; + if (!purple_account_get_remember_password(session->account)) + purple_account_set_password(session->account, NULL); + break; + } + purple_connection_error_reason (session->gc, reason, full_errmsg); + } else { + purple_notify_error(session->account, _("MySpaceIM Error"), full_errmsg, NULL); + } + + g_free(full_errmsg); + + 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. + */ +static gboolean +msim_process(MsimSession *session, MsimMessage *msg) +{ + g_return_val_if_fail(session != NULL, FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + if (msim_msg_get_integer(msg, "lc") == 1) { + return msim_login_challenge(session, msg); + } else if (msim_msg_get_integer(msg, "lc") == 2) { + /* return msim_we_are_logged_on(session, msg); */ + if (msim_is_username_set(session, msg)) { + return msim_we_are_logged_on(session); + } else { + /* No username is set... We'll wait for the callbacks to do their work */ + /* When they're all done, the last one will call msim_we_are_logged_on() and pick up where we left off */ + return FALSE; + } + } else if (msim_msg_get(msg, "bm")) { + return msim_incoming_bm(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")) { + return TRUE; + } else { + msim_unrecognized(session, msg, "in msim_process"); + return FALSE; + } +} + +/** + * 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, const MsimMessage *userinfo, + gpointer data) +{ + gchar *username; + MsimMessage *msg, *body; + + g_return_if_fail(MSIM_SESSION_VALID(session)); + g_return_if_fail(userinfo != NULL); + + body = msim_msg_get_dictionary(userinfo, "body"); + g_return_if_fail(body != NULL); + + username = msim_msg_get_string(body, "UserName"); + g_return_if_fail(username != NULL); + /* Note: username will be owned by 'msg' below. */ + + msg = (MsimMessage *)data; + g_return_if_fail(msg != NULL); + + /* TODO: more elegant solution than below. attach whole message? */ + /* Special elements name beginning with '_', we'll use internally within the + * program (did not come directly from the wire). */ + msg = msim_msg_append(msg, "_username", MSIM_TYPE_STRING, username); /* This makes 'msg' the owner of 'username' */ + + /* TODO: attach more useful information, like ImageURL */ + + msim_process(session, msg); + + /* TODO: Free copy cloned from msim_preprocess_incoming(). */ + /* msim_msg_free(msg); */ + msim_msg_free(body); +} + +/** + * Preprocess incoming messages, resolving as needed, calling + * msim_process() when ready to process. + * + * @param session + * @param msg MsimMessage *, freed by caller. + */ +static gboolean +msim_preprocess_incoming(MsimSession *session, MsimMessage *msg) { - gchar *our_info; - gboolean ret; - - our_info = g_strdup_printf("Libpurple %d.%d.%d - msimprpl %s", - PURPLE_MAJOR_VERSION, - PURPLE_MINOR_VERSION, - PURPLE_MICRO_VERSION, - MSIM_PRPL_VERSION_STRING); - - ret = msim_send_bm(session, username, our_info, MSIM_BM_UNOFFICIAL_CLIENT); - - return ret; + 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"); + + username = msim_uid2username_from_blist(session->account, uid); + + 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); + } } + +/** + * 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. + */ +static 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; + + /* libpurple/eventloop.h only defines these two */ + if (cond != PURPLE_INPUT_READ && cond != PURPLE_INPUT_WRITE) { + purple_debug_info("msim_input_cb", "unknown condition=%d\n", cond); + purple_connection_error_reason (gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Invalid input condition")); + return; + } + + g_return_if_fail(cond == PURPLE_INPUT_READ); + g_return_if_fail(MSIM_SESSION_VALID(session)); + + /* Mark down that we got data, so we don't timeout. */ + session->last_comm = time(NULL); + + /* If approaching end of buffer, reallocate some more memory. */ + if (session->rxsize < session->rxoff + MSIM_READ_BUF_SIZE) { + purple_debug_info("msim", + "msim_input_cb: %d-byte read buffer full, rxoff=%d, " "growing by %d bytes\n", + session->rxsize, session->rxoff, MSIM_READ_BUF_SIZE); + session->rxsize += MSIM_READ_BUF_SIZE; + session->rxbuf = g_realloc(session->rxbuf, session->rxsize); + + return; + } + + purple_debug_info("msim", "dynamic buffer at %d (max %d), reading up to %d\n", + session->rxoff, session->rxsize, + MSIM_READ_BUF_SIZE - session->rxoff - 1); + + /* 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, + session->rxsize - session->rxoff - 1, 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, g_strerror(errno), source, session->fd, session->fd); + purple_connection_error_reason (gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Read error")); + return; + } else if (n == 0) { + purple_debug_info("msim", "msim_input_cb: server disconnected\n"); + purple_connection_error_reason (gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Server has disconnected")); + return; + } + + if (n + session->rxoff > session->rxsize) { + purple_debug_info("msim_input_cb", "received %d bytes, pushing rxoff to %d, over buffer size of %d\n", + n, n + session->rxoff, session->rxsize); + purple_connection_error_reason (gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Read buffer full (2)")); + return; + } + + /* Null terminate */ + purple_debug_info("msim", "msim_input_cb: going to null terminate " + "at n=%d\n", n); + 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_reason (gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + "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(session->rxbuf); + if (!msg) { + purple_debug_info("msim", "msim_input_cb: couldn't parse rxbuf\n"); + purple_connection_error_reason (gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Unparseable message")); + break; + } 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), + session->rxsize - (end + strlen(MSIM_FINAL_STRING) - session->rxbuf)); + + /* Clear end of buffer + * memset(end, 0, MSIM_READ_BUF_SIZE - (end - session->rxbuf)); + */ + } +} + +/** + * Callback when connected. Sets up input handlers. + * + * @param data A PurpleConnection pointer. + * @param source File descriptor. + * @param error_message + */ +static 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_reason (gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + 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); +} + +/** + * Start logging in to the MSIM servers. + * + * @param acct Account information to use to login. + */ +static 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("msim", "logging in %s\n", acct->username); + + gc = purple_account_get_connection(acct); + gc->proto_data = msim_session_new(acct); + gc->flags |= PURPLE_CONNECTION_HTML | PURPLE_CONNECTION_NO_URLDESC; + + /* + * Lets wipe out our local list of blocked buddies. We'll get a + * list of all blocked buddies from the server, and we shouldn't + * have stuff in the local list that isn't on the server list. + */ + while (acct->deny != NULL) + purple_privacy_deny_remove(acct, acct->deny->data, TRUE); + + /* 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)) { + /* TODO: try other ports if in auto mode, then save + * working port and try that first next time. */ + purple_connection_error_reason (gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Couldn't create socket")); + return; + } +} + +/** + * Close the connection. + * + * @param gc The connection. + */ +static 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); + } + if (session->fd >= 0) { + close(session->fd); + session->fd = -1; + } + + msim_session_destroy(session); +} + +/** + * 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(). + */ +static int +msim_send_im(PurpleConnection *gc, const gchar *who, const gchar *message, + PurpleMessageFlags flags) +{ + MsimSession *session; + gchar *message_msim; + int rc; + + 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); + + message_msim = html_to_msim_markup(session, message); + + if (msim_send_bm(session, who, message_msim, 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). + * If an error does occur, it should be logged to the IM window. + */ + rc = 1; + } else { + rc = -1; + } + + g_free(message_msim); + + return rc; +} + +/** * Handle when our user starts or stops typing to another user. * * @param gc @@ -917,8 +2306,8 @@ * * @return 0 */ -unsigned int -msim_send_typing(PurpleConnection *gc, const gchar *name, +static unsigned int +msim_send_typing(PurpleConnection *gc, const gchar *name, PurpleTypingState state) { const gchar *typing_str; @@ -932,8 +2321,8 @@ g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); switch (state) { - case PURPLE_TYPING: - typing_str = "%typing%"; + case PURPLE_TYPING: + typing_str = "%typing%"; break; case PURPLE_TYPED: @@ -948,11 +2337,11 @@ return 0; } - - -/** Callback for msim_get_info(), for when user info is received. */ -static void -msim_get_info_cb(MsimSession *session, MsimMessage *user_info_msg, +/** + * Callback for msim_get_info(), for when user info is received. + */ +static void +msim_get_info_cb(MsimSession *session, const MsimMessage *user_info_msg, gpointer data) { MsimMessage *msg; @@ -962,7 +2351,7 @@ g_return_if_fail(MSIM_SESSION_VALID(session)); - /* Get user{name,id} from msim_get_info, passed as an MsimMessage for + /* Get user{name,id} from msim_get_info, passed as an MsimMessage for orthogonality. */ msg = (MsimMessage *)data; g_return_if_fail(msg != NULL); @@ -980,15 +2369,8 @@ if (!user) { /* User isn't on blist, create a temporary user to store info. */ - /* TODO: is this legit, or is it somehow responsible for #3444? */ - PurpleBuddy *buddy; - user = g_new0(MsimUser, 1); user->temporary_user = TRUE; - - buddy = purple_buddy_new(session->account, username, NULL); - user->buddy = buddy; - buddy->proto_data = (gpointer)user; } /* Update user structure with new information */ @@ -1005,7 +2387,6 @@ purple_notify_user_info_destroy(user_info); if (user->temporary_user) { - purple_blist_remove_buddy(user->buddy); g_free(user->client_info); g_free(user->gender); g_free(user->location); @@ -1018,15 +2399,15 @@ g_free(username); } -/** Retrieve a user's profile. +/** + * Retrieve a user's profile. * @param username Username, user ID, or email address to lookup. */ -void +static void msim_get_info(PurpleConnection *gc, const gchar *username) { MsimSession *session; MsimUser *user; - guint uid; gchar *user_to_lookup; MsimMessage *user_msg; @@ -1041,15 +2422,15 @@ user = msim_find_user(session, username); /* If is on buddy list, lookup by uid since it is faster. */ - if (user && (uid = purple_blist_node_get_int(&user->buddy->node, "UserID"))) { - user_to_lookup = g_strdup_printf("%d", uid); + if (user && user->id) { + user_to_lookup = g_strdup_printf("%d", user->id); } else { /* Looking up buddy not on blist. Lookup by whatever user entered. */ user_to_lookup = g_strdup(username); } /* Pass the username to msim_get_info_cb(), because since we lookup - * by userid, the userinfo message will only contain the uid (not + * by userid, the userinfo message will only contain the uid (not * the username) but it would be useful to display the username too. */ user_msg = msim_msg_new( @@ -1059,11 +2440,38 @@ msim_lookup_user(session, user_to_lookup, msim_get_info_cb, user_msg); - g_free(user_to_lookup); + g_free(user_to_lookup); } -/** Set your status - callback for when user manually sets it. */ -void +/** + * Set status using an MSIM_STATUS_CODE_* value. + * @param status_code An MSIM_STATUS_CODE_* value. + * @param statstring Status string, must be a dynamic string (will be freed by msim_send). + */ +static void +msim_set_status_code(MsimSession *session, guint status_code, gchar *statstring) +{ + g_return_if_fail(MSIM_SESSION_VALID(session)); + g_return_if_fail(statstring != NULL); + + purple_debug_info("msim", "msim_set_status_code: going to set status to code=%d,str=%s\n", + status_code, statstring); + + if (!msim_send(session, + "status", MSIM_TYPE_INTEGER, status_code, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + "statstring", MSIM_TYPE_STRING, statstring, + "locstring", MSIM_TYPE_STRING, g_strdup(""), + NULL)) + { + purple_debug_info("msim", "msim_set_status: failed to set status\n"); + } +} + +/** + * Set your status - callback for when user manually sets it. + */ +static void msim_set_status(PurpleAccount *account, PurpleStatus *status) { PurpleStatusType *type; @@ -1105,7 +2513,7 @@ "status interpreting as online"); status_code = MSIM_STATUS_CODE_ONLINE; - unrecognized_msg = g_strdup_printf("msim_set_status, unrecognized status type: %d\n", + unrecognized_msg = g_strdup_printf("msim_set_status, unrecognized status type: %d\n", purple_status_type_get_primitive(type)); msim_unrecognized(session, NULL, unrecognized_msg); g_free(unrecognized_msg); @@ -1126,11 +2534,12 @@ /* If we should be idle, set that status. Time is irrelevant here. */ if (purple_presence_is_idle(pres) && status_code != MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN) msim_set_idle(account->gc, 1); - } -/** Go idle. */ -void +/** + * Go idle. + */ +static void msim_set_idle(PurpleConnection *gc, int time) { MsimSession *session; @@ -1145,7 +2554,7 @@ status = purple_account_get_active_status(session->account); if (time == 0) { - /* Going back from idle. In msim, idle is mutually exclusive + /* 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 back to what * libpurple says I should be. @@ -1169,911 +2578,47 @@ } } -/** Set status using an MSIM_STATUS_CODE_* value. - * @param status_code An MSIM_STATUS_CODE_* value. - * @param statstring Status string, must be a dynamic string (will be freed by msim_send). - */ -static void -msim_set_status_code(MsimSession *session, guint status_code, gchar *statstring) -{ - g_return_if_fail(MSIM_SESSION_VALID(session)); - g_return_if_fail(statstring != NULL); - - purple_debug_info("msim", "msim_set_status_code: going to set status to code=%d,str=%s\n", - status_code, statstring); - - if (!msim_send(session, - "status", MSIM_TYPE_INTEGER, status_code, - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - "statstring", MSIM_TYPE_STRING, statstring, - "locstring", MSIM_TYPE_STRING, g_strdup(""), - NULL)) - { - purple_debug_info("msim", "msim_set_status: failed to set status\n"); - } - -} - -/** 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. +/** + * @return TRUE if everything was ok, FALSE if something went awry. */ -static void -msim_incoming_resolved(MsimSession *session, MsimMessage *userinfo, - gpointer data) -{ - gchar *username; - MsimMessage *msg, *body; - - g_return_if_fail(MSIM_SESSION_VALID(session)); - g_return_if_fail(userinfo != NULL); - - body = msim_msg_get_dictionary(userinfo, "body"); - g_return_if_fail(body != NULL); - - username = msim_msg_get_string(body, "UserName"); - g_return_if_fail(username != NULL); - /* Note: username will be owned by 'msg' below. */ - - msg = (MsimMessage *)data; - g_return_if_fail(msg != NULL); - - /* TODO: more elegant solution than below. attach whole message? */ - /* Special elements name beginning with '_', we'll use internally within the - * program (did not come directly from the wire). */ - msg = msim_msg_append(msg, "_username", MSIM_TYPE_STRING, username); /* This makes 'msg' the owner of 'username' */ - - /* TODO: attach more useful information, like ImageURL */ - - msim_process(session, msg); - - /* TODO: Free copy cloned from msim_preprocess_incoming(). */ - /* msim_msg_free(msg); */ - msim_msg_free(body); -} - -/* Lookup a username by userid, from buddy list. - * - * @param wanted_uid - * - * @return Username of wanted_uid, if on blist, or NULL. - * This is a static string, so don't free it. Copy it if needed. - * - */ -static const gchar * -msim_uid2username_from_blist(PurpleAccount *account, guint wanted_uid) +static gboolean +msim_update_blocklist_for_buddy(MsimSession *session, const char *name, gboolean allow, gboolean block) { - GSList *buddies, *cur; - const gchar *ret; - - buddies = purple_find_buddies(account, NULL); - - if (!buddies) - { - purple_debug_info("msim", "msim_uid2username_from_blist: no buddies?\n"); - return NULL; - } - - ret = NULL; - - for (cur = buddies; cur != NULL; cur = g_slist_next(cur)) - { - PurpleBuddy *buddy; - guint uid; - const gchar *name; - - /* See finch/gnthistory.c */ - buddy = cur->data; - - uid = purple_blist_node_get_int(&buddy->node, "UserID"); - name = purple_buddy_get_name(buddy); - - if (uid == wanted_uid) - { - ret = name; - break; - } - } - - g_slist_free(buddies); - return ret; -} - -/** Preprocess incoming messages, resolving as needed, calling msim_process() when ready to process. - * - * @param session - * @param msg MsimMessage *, freed by caller. - */ -static 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"); - - username = msim_uid2username_from_blist(session->account, uid); - - 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); - } -} - -#ifdef MSIM_USE_KEEPALIVE -/** Check if the connection is still alive, based on last communication. */ -static 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(ngettext("Connection to server lost (no data received within %d second)", - "Connection to server lost (no data received within %d seconds)", - (int)delta), - (int)delta); - - purple_debug_info("msim", "msim_check_alive: %s > interval of %d, presumed dead\n", - errmsg, MSIM_KEEPALIVE_INTERVAL); - purple_connection_error_reason(session->gc, - PURPLE_CONNECTION_ERROR_NETWORK_ERROR, errmsg); - - purple_notify_error(session->gc, NULL, errmsg, NULL); - - g_free(errmsg); - + MsimMessage *msg; + GList *list; + + list = NULL; + list = g_list_prepend(list, allow ? "a+" : "a-"); + list = g_list_prepend(list, "<uid>"); + list = g_list_prepend(list, block ? "b+" : "b-"); + list = g_list_prepend(list, "<uid>"); + list = g_list_reverse(list); + + msg = msim_msg_new( + "blocklist", MSIM_TYPE_BOOLEAN, TRUE, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + /* TODO: MsimMessage lists. Currently <uid> isn't replaced in lists. */ + /* "idlist", MSIM_TYPE_STRING, g_strdup("a-|<uid>|b-|<uid>"), */ + "idlist", MSIM_TYPE_LIST, list, + NULL); + + if (!msim_postprocess_outgoing(session, msg, name, "idlist", NULL)) { + purple_debug_error("myspace", + "blocklist command failed for %s, allow=%d, block=%d\n", + name, allow, block); + msim_msg_free(msg); return FALSE; } - return TRUE; -} -#endif - -/** Handle mail reply checks. */ -static void -msim_check_inbox_cb(MsimSession *session, MsimMessage *reply, gpointer data) -{ - MsimMessage *body; - guint old_inbox_status; - guint i, n; - const gchar *froms[5], *tos[5], *urls[5], *subjects[5]; - - /* Information for each new inbox message type. */ - static struct - { - const gchar *key; - guint bit; - const gchar *url; - const gchar *text; - } message_types[] = { - { "Mail", MSIM_INBOX_MAIL, "http://messaging.myspace.com/index.cfm?fuseaction=mail.inbox", NULL }, - { "BlogComment", MSIM_INBOX_BLOG_COMMENT, "http://blog.myspace.com/index.cfm?fuseaction=blog", NULL }, - { "ProfileComment", MSIM_INBOX_PROFILE_COMMENT, "http://home.myspace.com/index.cfm?fuseaction=user", NULL }, - { "FriendRequest", MSIM_INBOX_FRIEND_REQUEST, "http://messaging.myspace.com/index.cfm?fuseaction=mail.friendRequests", NULL }, - { "PictureComment", MSIM_INBOX_PICTURE_COMMENT, "http://home.myspace.com/index.cfm?fuseaction=user", NULL } - }; - - /* Can't write _()'d strings in array initializers. Workaround. */ - message_types[0].text = _("New mail messages"); - message_types[1].text = _("New blog comments"); - message_types[2].text = _("New profile comments"); - message_types[3].text = _("New friend requests!"); - message_types[4].text = _("New picture comments"); - - g_return_if_fail(reply != NULL); - - msim_msg_dump("msim_check_inbox_cb: reply=%s\n", reply); - - body = msim_msg_get_dictionary(reply, "body"); - - if (body == NULL) - return; - - old_inbox_status = session->inbox_status; - - n = 0; - - for (i = 0; i < sizeof(message_types) / sizeof(message_types[0]); ++i) { - const gchar *key; - guint bit; - - key = message_types[i].key; - bit = message_types[i].bit; - - if (msim_msg_get(body, key)) { - /* Notify only on when _changes_ from no mail -> has mail - * (edge triggered) */ - if (!(session->inbox_status & bit)) { - purple_debug_info("msim", "msim_check_inbox_cb: got %s, at %d\n", - key ? key : "(NULL)", n); - - subjects[n] = message_types[i].text; - froms[n] = _("MySpace"); - tos[n] = session->username; - /* TODO: append token, web challenge, so automatically logs in. - * Would also need to free strings because they won't be static - */ - urls[n] = message_types[i].url; - - ++n; - } else { - purple_debug_info("msim", - "msim_check_inbox_cb: already notified of %s\n", - key ? key : "(NULL)"); - } - - session->inbox_status |= bit; - } - } - - if (n) { - purple_debug_info("msim", - "msim_check_inbox_cb: notifying of %d\n", n); - - /* TODO: free strings with callback _if_ change to dynamic (w/ token) */ - purple_notify_emails(session->gc, /* handle */ - n, /* count */ - TRUE, /* detailed */ - subjects, froms, tos, urls, - NULL, /* PurpleNotifyCloseCallback cb */ - NULL); /* gpointer user_data */ - - } - - msim_msg_free(body); -} - -/* Send request to check if there is new mail. */ -static gboolean -msim_check_inbox(gpointer data) -{ - MsimSession *session; - - session = (MsimSession *)data; - - if (!MSIM_SESSION_VALID(session)) { - purple_debug_info("msim", "msim_check_inbox: session invalid, stopping the mail check.\n"); - return FALSE; - } - - purple_debug_info("msim", "msim_check_inbox: checking mail\n"); - g_return_val_if_fail(msim_send(session, - "persist", MSIM_TYPE_INTEGER, 1, - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - "cmd", MSIM_TYPE_INTEGER, MSIM_CMD_GET, - "dsn", MSIM_TYPE_INTEGER, MG_CHECK_MAIL_DSN, - "lid", MSIM_TYPE_INTEGER, MG_CHECK_MAIL_LID, - "uid", MSIM_TYPE_INTEGER, session->userid, - "rid", MSIM_TYPE_INTEGER, - msim_new_reply_callback(session, msim_check_inbox_cb, NULL), - "body", MSIM_TYPE_STRING, g_strdup(""), - NULL), TRUE); - - /* Always return true, so that we keep checking for mail. */ - return TRUE; -} - -#ifdef MSIM_CHECK_NEWER_VERSION -/** Callback for when a currentversion.txt has been downloaded. */ -static void -msim_check_newer_version_cb(PurpleUtilFetchUrlData *url_data, - gpointer user_data, - const gchar *url_text, - gsize len, - const gchar *error_message) -{ - GKeyFile *keyfile; - GError *error; - GString *data; - gchar *newest_filever; - - if (!url_text) { - purple_debug_info("msim_check_newer_version_cb", - "got error: %s\n", error_message); - return; - } - - purple_debug_info("msim_check_newer_version_cb", - "url_text=%s\n", url_text ? url_text : "(NULL)"); - - /* Prepend [group] so that GKeyFile can parse it (requires a group). */ - data = g_string_new(url_text); - purple_debug_info("msim", "data=%s\n", data->str - ? data->str : "(NULL)"); - data = g_string_prepend(data, "[group]\n"); - - purple_debug_info("msim", "data=%s\n", data->str - ? data->str : "(NULL)"); - - /* url_text is variable=data\n...†*/ - - /* Check FILEVER, 1.0.716.0. 716 is build, MSIM_CLIENT_VERSION */ - /* New (english) version can be downloaded from SETUPURL+SETUPFILE */ - - error = NULL; - keyfile = g_key_file_new(); - - /* Default list seperator is ;, but currentversion.txt doesn't have - * these, so set to an unused character to avoid parsing problems. */ - g_key_file_set_list_separator(keyfile, '\0'); - - g_key_file_load_from_data(keyfile, data->str, data->len, - G_KEY_FILE_NONE, &error); - g_string_free(data, TRUE); - - if (error != NULL) { - purple_debug_info("msim_check_newer_version_cb", - "couldn't parse, error: %d %d %s\n", - error->domain, error->code, error->message); - g_error_free(error); - return; - } - - gchar **ks; - guint n; - ks = g_key_file_get_keys(keyfile, "group", &n, NULL); - purple_debug_info("msim", "n=%d\n", n); - guint i; - for (i = 0; ks[i] != NULL; ++i) - { - purple_debug_info("msim", "%d=%s\n", i, ks[i]); - } - - newest_filever = g_key_file_get_string(keyfile, "group", - "FILEVER", &error); - - purple_debug_info("msim_check_newer_version_cb", - "newest filever: %s\n", newest_filever ? - newest_filever : "(NULL)"); - if (error != NULL) { - purple_debug_info("msim_check_newer_version_cb", - "error: %d %d %s\n", - error->domain, error->code, error->message); - g_error_free(error); - } - - g_key_file_free(keyfile); - - exit(0); -} -#endif - -/** Called when the session key arrives to check whether the user - * has a username, and set one if desired. */ -static gboolean -msim_is_username_set(MsimSession *session, MsimMessage *msg) -{ - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - g_return_val_if_fail(session->gc != NULL, FALSE); - - session->sesskey = msim_msg_get_integer(msg, "sesskey"); - purple_debug_info("msim", "SESSKEY=<%d>\n", session->sesskey); - - /* What is proof? Used to be uid, but now is 52 base64'd bytes... */ - - /* 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"); - - /* Save uid to account so this account can be looked up by uid. */ - purple_account_set_int(session->account, "uid", session->userid); - - /* Not sure what profileid is used for. */ - if (msim_msg_get_integer(msg, "profileid") != session->userid) { - msim_unrecognized(session, msg, - "Profile ID didn't match user ID, don't know why"); - } - - /* 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"); - - /* If user lacks a username, help them get one. */ - if (msim_msg_get_integer(msg, "uniquenick") == session->userid) { - purple_debug_info("msim_is_username_set", "no username is set\n"); - purple_request_yes_no(session->gc, - _("MySpaceIM - No Username Set"), - _("You appear to have no MySpace username."), - _("Would you like to set one now? (Note: THIS CANNOT BE CHANGED!)"), - 0, - session->account, - NULL, - NULL, - session->gc, - G_CALLBACK(msim_set_username_cb), - G_CALLBACK(msim_do_not_set_username_cb)); - purple_debug_info("msim_is_username_set","'username not set' alert prompted\n"); - return FALSE; - } - return TRUE; -} - -/** Called after username is set, if necessary and we're open for business. */ -gboolean msim_we_are_logged_on(MsimSession *session) -{ - MsimMessage *body; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - - /* The session is now set up, ready to be connected. This emits the - * signedOn signal, so clients can now do anything with msimprpl, and - * we're ready for it (session key, userid, username all setup). */ - purple_connection_update_progress(session->gc, _("Connected"), 3, 4); - purple_connection_set_state(session->gc, PURPLE_CONNECTED); - - /* Set display name to username (otherwise will show email address) */ - purple_connection_set_display_name(session->gc, session->username); - - body = msim_msg_new( - "UserID", MSIM_TYPE_INTEGER, session->userid, - NULL); - - /* Request IM info about ourself. */ - msim_send(session, - "persist", MSIM_TYPE_STRING, g_strdup("persist"), - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - "dsn", MSIM_TYPE_INTEGER, MG_OWN_MYSPACE_INFO_DSN, - "uid", MSIM_TYPE_INTEGER, session->userid, - "lid", MSIM_TYPE_INTEGER, MG_OWN_MYSPACE_INFO_LID, - "rid", MSIM_TYPE_INTEGER, session->next_rid++, - "body", MSIM_TYPE_DICTIONARY, body, - NULL); - - /* Request MySpace info about ourself. */ - msim_send(session, - "persist", MSIM_TYPE_STRING, g_strdup("persist"), - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - "dsn", MSIM_TYPE_INTEGER, MG_OWN_IM_INFO_DSN, - "uid", MSIM_TYPE_INTEGER, session->userid, - "lid", MSIM_TYPE_INTEGER, MG_OWN_IM_INFO_LID, - "rid", MSIM_TYPE_INTEGER, session->next_rid++, - "body", MSIM_TYPE_STRING, g_strdup(""), - NULL); - - /* TODO: set options (persist cmd=514,dsn=1,lid=10) */ - /* TODO: set blocklist */ - - /* Notify servers of our current status. */ - purple_debug_info("msim", "msim_we_are_logged_on: notifying servers of status\n"); - msim_set_status(session->account, - purple_account_get_active_status(session->account)); - - /* TODO: setinfo */ - /* - body = msim_msg_new( - "TotalFriends", MSIM_TYPE_INTEGER, 666, - NULL); - msim_send(session, - "setinfo", MSIM_TYPE_BOOLEAN, TRUE, - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - "info", MSIM_TYPE_DICTIONARY, body, - NULL); - */ - - /* Disable due to problems with timeouts. TODO: fix. */ -#ifdef MSIM_USE_KEEPALIVE - purple_timeout_add(MSIM_KEEPALIVE_INTERVAL_CHECK, - (GSourceFunc)msim_check_alive, session); -#endif - - /* Check mail if they want to. */ - if (purple_account_get_check_mail(session->account)) { - session->inbox_handle = purple_timeout_add(MSIM_MAIL_INTERVAL_CHECK, - (GSourceFunc)msim_check_inbox, session); - msim_check_inbox(session); - } - - msim_get_contact_list(session, MSIM_CONTACT_LIST_INITIAL_FRIENDS); + msim_msg_free(msg); 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. - */ -static 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_integer(msg, "lc") == 1) { - return msim_login_challenge(session, msg); - } else if (msim_msg_get_integer(msg, "lc") == 2) { - /* return msim_we_are_logged_on(session, msg); */ - if (msim_is_username_set(session, msg)) { - return msim_we_are_logged_on(session); - } else { - /* No username is set... We'll wait for the callbacks to do their work */ - /* When they're all done, the last one will call msim_we_are_logged_on() and pick up where we left off */ - return FALSE; - } - } else if (msim_msg_get(msg, "bm")) { - return msim_incoming_bm(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")) { - return TRUE; - } else { - msim_unrecognized(session, msg, "in msim_process"); - return FALSE; - } -} - -/** Process the initial server information from the server. */ -static gboolean -msim_process_server_info(MsimSession *session, MsimMessage *msg) -{ - MsimMessage *body; - - body = msim_msg_get_dictionary(msg, "body"); - g_return_val_if_fail(body != NULL, FALSE); - - /* Example body: -AdUnitRefreshInterval=10. -AlertPollInterval=360. -AllowChatRoomEmoticonSharing=False. -ChatRoomUserIDs=78744676;163733130;1300326231;123521495;142663391. -CurClientVersion=673. -EnableIMBrowse=True. -EnableIMStuffAvatars=False. -EnableIMStuffZaps=False. -MaxAddAllFriends=100. -MaxContacts=1000. -MinClientVersion=594. -MySpaceIM_ENGLISH=78744676. -MySpaceNowTimer=720. -PersistenceDataTimeout=900. -UseWebChallenge=1. -WebTicketGoHome=False - - Anything useful? TODO: use what is useful, and use it. -*/ - purple_debug_info("msim_process_server_info", - "maximum contacts: %d\n", - msim_msg_get_integer(body, "MaxContacts")); - - session->server_info = body; - /* session->server_info freed in msim_session_destroy */ - - return TRUE; -} - -/** Process a web challenge, used to login to the web site. */ -static gboolean -msim_web_challenge(MsimSession *session, MsimMessage *msg) -{ - /* TODO: web challenge, store token. #2659. */ - return FALSE; -} - -/** - * Process a persistance message reply from the server. - * - * @param session - * @param msg Message reply from server. - * - * @return TRUE if successful. - * - * msim_lookup_user sets callback for here - */ -static gboolean -msim_process_reply(MsimSession *session, MsimMessage *msg) -{ - MSIM_USER_LOOKUP_CB cb; - gpointer data; - guint rid, cmd, dsn, lid; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - msim_store_user_info(session, msg, NULL); - - rid = msim_msg_get_integer(msg, "rid"); - cmd = msim_msg_get_integer(msg, "cmd"); - dsn = msim_msg_get_integer(msg, "dsn"); - lid = msim_msg_get_integer(msg, "lid"); - - /* Unsolicited messages */ - if (cmd == (MSIM_CMD_BIT_REPLY | MSIM_CMD_GET)) { - if (dsn == MG_SERVER_INFO_DSN && lid == MG_SERVER_INFO_LID) { - return msim_process_server_info(session, msg); - } else if (dsn == MG_WEB_CHALLENGE_DSN && lid == MG_WEB_CHALLENGE_LID) { - return msim_web_challenge(session, msg); - } - } - - /* 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_reply: calling callback now\n"); - msim_msg_dump("for msg=%s\n", msg); - /* 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_reply: 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. + * Add a buddy to user's buddy list. */ -static 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 (sesskey=%d): %s\n", - session->sesskey, full_errmsg); - - /* Destroy session if fatal. */ - if (msim_msg_get(msg, "fatal")) { - PurpleConnectionError reason = PURPLE_CONNECTION_ERROR_NETWORK_ERROR; - purple_debug_info("msim", "fatal error, closing\n"); - - switch (err) { - case MSIM_ERROR_INCORRECT_PASSWORD: /* Incorrect password */ - reason = PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED; - if (!purple_account_get_remember_password(session->account)) - purple_account_set_password(session->account, NULL); -#ifdef MSIM_MAX_PASSWORD_LENGTH - if (session->account->password && (strlen(session->account->password) > MSIM_MAX_PASSWORD_LENGTH)) { - gchar *suggestion; - - suggestion = g_strdup_printf(_("%s Your password is " - "%d characters, greater than the " - "expected maximum length of %d for " - "MySpaceIM. Please shorten your " - "password at http://profileedit.myspace.com/index.cfm?fuseaction=accountSettings.changePassword and try again."), - full_errmsg, (int) - strlen(session->account->password), - MSIM_MAX_PASSWORD_LENGTH); - - /* Replace full_errmsg. */ - g_free(full_errmsg); - full_errmsg = suggestion; - } -#endif - break; - case MSIM_ERROR_LOGGED_IN_ELSEWHERE: /* Logged in elsewhere */ - reason = PURPLE_CONNECTION_ERROR_NAME_IN_USE; - if (!purple_account_get_remember_password(session->account)) - purple_account_set_password(session->account, NULL); - break; - } - purple_connection_error_reason (session->gc, reason, full_errmsg); - } else { - purple_notify_error(session->account, _("MySpaceIM Error"), full_errmsg, NULL); - } - - g_free(full_errmsg); - - return TRUE; -} - -/** - * Process incoming status messages. - * - * @param session - * @param msg Status update message. Caller frees. - * - * @return TRUE if successful. - */ -static gboolean -msim_incoming_status(MsimSession *session, MsimMessage *msg) -{ - PurpleBuddyList *blist; - MsimUser *user; - GList *list; - gchar *status_headline, *status_headline_escaped; - gint status_code, purple_status_code; - gchar *username; - gchar *unrecognized_msg; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != 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"); - g_return_val_if_fail(username != NULL, FALSE); - - { - gchar *ss; - - ss = msim_msg_get_string(msg, "msg"); - purple_debug_info("msim", - "msim_status: updating status for <%s> to <%s>\n", - username, ss ? ss : "(NULL)"); - g_free(ss); - } - - /* Example fields: - * |s|0|ss|Offline - * |s|1|ss|:-)|ls||ip|0|p|0 - */ - list = msim_msg_get_list(msg, "msg"); - - status_code = msim_msg_get_integer_from_element(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 = msim_msg_get_string_from_element(g_list_nth_data(list, MSIM_STATUS_ORDINAL_HEADLINE)); - - blist = purple_get_blist(); - - /* Add buddy if not found. - * TODO: Could this be responsible for #3444? */ - user = msim_find_user(session, username); - if (!user) { - PurpleBuddy *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); - - user = msim_get_user_from_buddy(buddy, TRUE); - - /* All buddies on list should have a UserID integer associated with them. */ - purple_blist_node_set_int(&buddy->node, "UserID", msim_msg_get_integer(msg, "f")); - - msim_store_user_info(session, msg, NULL); - } else { - purple_debug_info("msim", "msim_status: found buddy %s\n", username); - } - - if (status_headline && strcmp(status_headline, "") != 0) { - /* The status headline is plaintext, but libpurple treats it as HTML, - * so escape any HTML characters to their entity equivalents. */ - status_headline_escaped = g_markup_escape_text(status_headline, strlen(status_headline)); - } else { - status_headline_escaped = NULL; - } - - g_free(status_headline); - - if (user->headline) - g_free(user->headline); - - /* don't copy; let the MsimUser own the headline, memory-wise */ - user->headline = status_headline_escaped; - - /* 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: - /* Treat idle as an available status. */ - purple_status_code = PURPLE_STATUS_AVAILABLE; - break; - - default: - purple_debug_info("msim", "msim_incoming_status for %s, unknown status code %d, treating as available\n", - username, status_code); - purple_status_code = PURPLE_STATUS_AVAILABLE; - - unrecognized_msg = g_strdup_printf("msim_incoming_status, unrecognized status code: %d\n", - status_code); - msim_unrecognized(session, NULL, unrecognized_msg); - g_free(unrecognized_msg); - - } - - purple_prpl_got_user_status(session->account, username, purple_primitive_get_id_from_type(purple_status_code), NULL); - - 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)); - } - -#ifdef MSIM_SEND_CLIENT_VERSION - if (status_code == MSIM_STATUS_CODE_ONLINE) { - /* Secretly whisper to unofficial clients our own version as they come online */ - msim_send_unofficial_client(session, username); - } -#endif - - if (status_code != MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN) { - /* Get information when they come online. - * TODO: periodically refresh? - */ - purple_debug_info("msim_incoming_status", "%s came online, looking up\n", username); - msim_lookup_user(session, username, NULL, NULL); - } - - g_free(username); - msim_msg_list_free(list); - - return TRUE; -} - -/** Add a buddy to user's buddy list. */ -void +static void msim_add_buddy(PurpleConnection *gc, PurpleBuddy *buddy, PurpleGroup *group) { MsimSession *session; @@ -2086,7 +2631,7 @@ if (msim_get_user_from_buddy(buddy, FALSE) != NULL) return; - purple_debug_info("msim", "msim_add_buddy: want to add %s to %s\n", + purple_debug_info("msim", "msim_add_buddy: want to add %s to %s\n", buddy->name, (group && group->name) ? group->name : "(no group)"); msg = msim_msg_new( @@ -2102,7 +2647,7 @@ return; } msim_msg_free(msg); - + /* TODO: if addbuddy fails ('error' message is returned), delete added buddy from * buddy list since Purple adds it locally. */ @@ -2137,216 +2682,19 @@ } msim_msg_free(msg_persist); -} - -/** 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 "<uid>" 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(). - */ -static MsimMessage * -msim_do_postprocessing(MsimMessage *msg, const gchar *uid_before, - const gchar *uid_field_name, guint uid) -{ - MsimMessageElement *elem; - msim_msg_dump("msim_do_postprocessing msg: %s\n", msg); - - /* First, check - if the field already exists, replace <uid> within it */ - if ((elem = msim_msg_get(msg, uid_field_name)) != NULL) { - gchar *fmt_string; - gchar *uid_str, *new_str; - - /* Get the packed element, flattening it. This allows <uid> to be - * replaced within nested data structures, since the replacement is done - * on the linear, packed data, not on a complicated data structure. - * - * For example, if the field was originally a dictionary or a list, you - * would have to iterate over all the items in it to see what needs to - * be replaced. But by packing it first, the <uid> marker is easily replaced - * just by a string replacement. - */ - fmt_string = msim_msg_pack_element_data(elem); - - uid_str = g_strdup_printf("%d", uid); - new_str = purple_strreplace(fmt_string, "<uid>", uid_str); - g_free(uid_str); - g_free(fmt_string); - - /* Free the old element data */ - msim_msg_free_element_data(elem->data); - - /* Replace it with our new data */ - elem->data = new_str; - elem->type = MSIM_TYPE_RAW; - - } 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)); - } - - msim_msg_dump("msim_postprocess_outgoing_cb: postprocessed msg=%s\n", msg); - - return msg; + /* Add to allow list, remove from block list */ + msim_update_blocklist_for_buddy(session, buddy->name, TRUE, FALSE); } -/** 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 - * - * -*/ -static void -msim_postprocess_outgoing_cb(MsimSession *session, MsimMessage *userinfo, - gpointer data) -{ - gchar *uid_field_name, *uid_before, *username; - guint uid; - MsimMessage *msg, *body; - - msg = (MsimMessage *)data; - - msim_msg_dump("msim_postprocess_outgoing_cb() got msg=%s\n", msg); - - /* Obtain userid from userinfo message. */ - body = msim_msg_get_dictionary(userinfo, "body"); - g_return_if_fail(body != NULL); - - uid = msim_msg_get_integer(body, "UserID"); - msim_msg_free(body); - - username = msim_msg_get_string(msg, "_username"); - - if (!uid) { - gchar *msg; - - msg = g_strdup_printf(_("No such user: %s"), username); - if (!purple_conv_present_error(username, session->account, msg)) { - purple_notify_error(NULL, NULL, _("User lookup"), msg); - } - - g_free(msg); - g_free(username); - /* TODO: free - * msim_msg_free(msg); - */ - return; - } - - 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, uid); - - /* Send */ - if (!msim_msg_send(session, msg)) { - msim_msg_dump("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); - g_free(username); - /* TODO: free - * 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 TRUE if successful. +/** + * Remove a buddy from the user's buddy list. */ -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; - - g_return_val_if_fail(msg != NULL, FALSE); - - /* Store information for msim_postprocess_outgoing_cb(). */ - 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 ? username : "(NULL)"); - 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 ? username : "(NULL)", 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); - - /* TODO: free - * msim_msg_free(msg); - */ - - return rc; -} - -/** Remove a buddy from the user's buddy list. */ -void +static void msim_remove_buddy(PurpleConnection *gc, PurpleBuddy *buddy, PurpleGroup *group) { MsimSession *session; MsimMessage *delbuddy_msg; MsimMessage *persist_msg; - MsimMessage *blocklist_msg; - GList *blocklist_updates; session = (MsimSession *)gc->proto_data; @@ -2382,27 +2730,101 @@ } msim_msg_free(persist_msg); - blocklist_updates = NULL; - blocklist_updates = g_list_prepend(blocklist_updates, "a-"); - blocklist_updates = g_list_prepend(blocklist_updates, "<uid>"); - blocklist_updates = g_list_prepend(blocklist_updates, "b-"); - blocklist_updates = g_list_prepend(blocklist_updates, "<uid>"); - blocklist_updates = g_list_reverse(blocklist_updates); - - blocklist_msg = msim_msg_new( - "blocklist", MSIM_TYPE_BOOLEAN, TRUE, + /* + * Remove from our approve list and from our block list (this + * doesn't seem like it would be necessary, but the official client + * does it) + */ + if (!msim_update_blocklist_for_buddy(session, buddy->name, FALSE, FALSE)) + purple_notify_error(NULL, NULL, + _("Failed to remove buddy"), _("blocklist command failed")); +} + +/** + * Remove a buddy from the user's buddy list and add them to the block list. + */ +static void +msim_add_deny(PurpleConnection *gc, const char *name) +{ + MsimSession *session; + MsimMessage *msg, *body; + + session = (MsimSession *)gc->proto_data; + + /* Remove from buddy list */ + msg = msim_msg_new( + "delbuddy", MSIM_TYPE_BOOLEAN, TRUE, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + /* 'delprofileid' with uid will be inserted here. */ + NULL); + if (!msim_postprocess_outgoing(session, msg, name, "delprofileid", NULL)) + purple_debug_error("myspace", "delbuddy command failed\n"); + msim_msg_free(msg); + + /* Remove from our approve list and add to our block list */ + msim_update_blocklist_for_buddy(session, name, FALSE, TRUE); + + /* + * Add the buddy to our list of blocked contacts, so we know they + * are blocked if we log in with another client + */ + body = msim_msg_new( + "ContactID", MSIM_TYPE_STRING, g_strdup("<uid>"), + "Visibility", MSIM_TYPE_INTEGER, 2, + NULL); + msg = msim_msg_new( + "persist", MSIM_TYPE_INTEGER, 1, "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - /* TODO: MsimMessage lists. Currently <uid> isn't replaced in lists. */ - /* "idlist", MSIM_TYPE_STRING, g_strdup("a-|<uid>|b-|<uid>"), */ - "idlist", MSIM_TYPE_LIST, blocklist_updates, + "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, + "rid", MSIM_TYPE_INTEGER, session->next_rid++, + "body", MSIM_TYPE_DICTIONARY, body, NULL); - - if (!msim_postprocess_outgoing(session, blocklist_msg, buddy->name, "idlist", NULL)) { - purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("blocklist command failed")); - msim_msg_free(blocklist_msg); - return; - } - msim_msg_free(blocklist_msg); + if (!msim_postprocess_outgoing(session, msg, name, "body", NULL)) + purple_debug_error("myspace", "add to block list command failed\n"); + msim_msg_free(msg); + + /* + * TODO: MySpace doesn't allow blocked buddies on our buddy list, + * do they? If not then we need to remove the buddy from + * libpurple's buddy list. + */ +} + +/** + * Remove a buddy from the user's block list. + */ +static void +msim_rem_deny(PurpleConnection *gc, const char *name) +{ + MsimSession *session; + MsimMessage *msg, *body; + + session = (MsimSession *)gc->proto_data; + + /* + * Remove from our list of blocked contacts, so we know they + * are no longer blocked if we log in with another client + */ + body = msim_msg_new( + "ContactID", MSIM_TYPE_STRING, g_strdup("<uid>"), + NULL); + msg = msim_msg_new( + "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, MC_DELETE_CONTACT_INFO_DSN, + "lid", MSIM_TYPE_INTEGER, MC_DELETE_CONTACT_INFO_LID, + "rid", MSIM_TYPE_INTEGER, session->next_rid++, + "body", MSIM_TYPE_DICTIONARY, body, + NULL); + if (!msim_postprocess_outgoing(session, msg, name, "body", NULL)) + purple_debug_error("myspace", "remove from block list command failed\n"); + msim_msg_free(msg); + + /* Remove from our approve list and our block list */ + msim_update_blocklist_for_buddy(session, name, FALSE, FALSE); } /** @@ -2410,10 +2832,10 @@ * spaces, lowercases the string, and looks up user IDs to usernames. * Normalizing tom, TOM, Tom, and 6221 wil all return 'tom'. * - * Borrowed this code from oscar_normalize. Added checking for + * Borrowed this code from oscar_normalize. Added checking for * "if userid, get name before normalizing" */ -const char *msim_normalize(const PurpleAccount *account, const char *str) { +static const char *msim_normalize(const PurpleAccount *account, const char *str) { static char normalized[BUF_LEN]; char *tmp1, *tmp2; int i, j; @@ -2433,7 +2855,7 @@ username = msim_uid2username_from_blist((PurpleAccount *)account, id); if (!username) { /* Not in buddy list... scheisse... TODO: Manual Lookup! Bug #4631 */ - /* Note: manual lookup using msim_lookup_user() is a problem inside + /* Note: manual lookup using msim_lookup_user() is a problem inside * msim_normalize(), because msim_lookup_user() calls a callback function * when the user information has been looked up, but msim_normalize() expects * the result immediately. */ @@ -2471,6 +2893,92 @@ return normalized; } +/** + * Return whether the buddy can be messaged while offline. + * + * The protocol supports offline messages in just the same way as online + * messages. + */ +static gboolean +msim_offline_message(const PurpleBuddy *buddy) +{ + return TRUE; +} + +/** + * 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. + */ +/* + * TODO: This needs to do non-blocking writes and use a watcher to check + * when the fd is available to be written to. + */ +static 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; +} + +/** + * 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) +{ + size_t len; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + purple_debug_info("msim", "msim_send_raw: writing <%s>\n", msg); + len = strlen(msg); + + return msim_send_really_raw(session->gc, msg, len) == len; +} + static GHashTable * msim_get_account_text_table(PurpleAccount *unused) { @@ -2483,624 +2991,15 @@ return table; } -/** 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. - */ -static 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; - - /* libpurple/eventloop.h only defines these two */ - if (cond != PURPLE_INPUT_READ && cond != PURPLE_INPUT_WRITE) { - purple_debug_info("msim_input_cb", "unknown condition=%d\n", cond); - purple_connection_error_reason (gc, - PURPLE_CONNECTION_ERROR_NETWORK_ERROR, - _("Invalid input condition")); - return; - } - - g_return_if_fail(cond == PURPLE_INPUT_READ); - g_return_if_fail(MSIM_SESSION_VALID(session)); - - /* Mark down that we got data, so we don't timeout. */ - session->last_comm = time(NULL); - - /* If approaching end of buffer, reallocate some more memory. */ - if (session->rxsize < session->rxoff + MSIM_READ_BUF_SIZE) { - purple_debug_info("msim", - "msim_input_cb: %d-byte read buffer full, rxoff=%d, " "growing by %d bytes\n", - session->rxsize, session->rxoff, MSIM_READ_BUF_SIZE); - session->rxsize += MSIM_READ_BUF_SIZE; - session->rxbuf = g_realloc(session->rxbuf, session->rxsize); - - return; - } - - purple_debug_info("msim", "dynamic buffer at %d (max %d), reading up to %d\n", - session->rxoff, session->rxsize, - MSIM_READ_BUF_SIZE - session->rxoff - 1); - - /* 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, - session->rxsize - session->rxoff - 1, 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, g_strerror(errno), source, session->fd, session->fd); - purple_connection_error_reason (gc, - PURPLE_CONNECTION_ERROR_NETWORK_ERROR, - _("Read error")); - return; - } else if (n == 0) { - purple_debug_info("msim", "msim_input_cb: server disconnected\n"); - purple_connection_error_reason (gc, - PURPLE_CONNECTION_ERROR_NETWORK_ERROR, - _("Server has disconnected")); - return; - } - - if (n + session->rxoff > session->rxsize) { - purple_debug_info("msim_input_cb", "received %d bytes, pushing rxoff to %d, over buffer size of %d\n", - n, n + session->rxoff, session->rxsize); - purple_connection_error_reason (gc, - PURPLE_CONNECTION_ERROR_NETWORK_ERROR, - _("Read buffer full (2)")); - return; - } - - /* Null terminate */ - purple_debug_info("msim", "msim_input_cb: going to null terminate " - "at n=%d\n", n); - 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_reason (gc, - PURPLE_CONNECTION_ERROR_NETWORK_ERROR, - "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 rxbuf\n"); - purple_connection_error_reason (gc, - PURPLE_CONNECTION_ERROR_NETWORK_ERROR, - _("Unparseable message")); - break; - } 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), - session->rxsize - (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, or -1. - * Put the rid in your request, 'rid' field. - * - * 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; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), -1); - - 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 - */ -static 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_reason (gc, - PURPLE_CONNECTION_ERROR_NETWORK_ERROR, - 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); -} - - -/** - * 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); -} - - -/** - * 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. - * + * Callbacks called by Purple, to access this plugin. */ -char * -msim_status_text(PurpleBuddy *buddy) -{ - MsimSession *session; - MsimUser *user; - const gchar *display_name, *headline; - - g_return_val_if_fail(buddy != NULL, NULL); - - user = msim_get_user_from_buddy(buddy, TRUE); - - 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_headline", TRUE)) { - headline = user->headline; - } - - if (purple_account_get_bool(session->account, "show_display_name", FALSE)) { - display_name = user->display_name; - } - - /* Return appropriate combination of display name and/or headline, or neither. */ - - if (display_name && headline) { - return g_strconcat(display_name, " ", headline, NULL); - } else if (display_name) { - return g_strdup(display_name); - } else 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) -{ - MsimUser *user; - - g_return_if_fail(buddy != NULL); - g_return_if_fail(user_info != NULL); - - user = msim_get_user_from_buddy(buddy, TRUE); - - 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? */ - - /* TODO: request information? have to figure out how to do - * the asynchronous lookup like oscar does (tooltip shows - * 'retrieving...' if not yet available, then changes when it is). - * - * Right now, only show what we have on hand. - */ - - /* Show abbreviated user info. */ - msim_append_user_info(session, user_info, user, FALSE); - } -} - -/** Add contact from server to buddy list, after looking up username. - * Callback from msim_add_contact_from_server(). - * - * @param data An MsimMessage * of the contact information. Will be freed. - */ -static void -msim_add_contact_from_server_cb(MsimSession *session, MsimMessage *user_lookup_info, gpointer data) -{ - MsimMessage *contact_info, *user_lookup_info_body; - PurpleGroup *group; - PurpleBuddy *buddy; - MsimUser *user; - gchar *username, *group_name; - guint uid; - - contact_info = (MsimMessage *)data; - purple_debug_info("msim_add_contact_from_server_cb", "contact_info addr=%p\n", contact_info); - uid = msim_msg_get_integer(contact_info, "ContactID"); - - if (!user_lookup_info) { - username = g_strdup(msim_uid2username_from_blist(session->account, uid)); - g_return_if_fail(username != NULL); - } else { - user_lookup_info_body = msim_msg_get_dictionary(user_lookup_info, "body"); - username = msim_msg_get_string(user_lookup_info_body, "UserName"); - msim_msg_free(user_lookup_info_body); - g_return_if_fail(username != NULL); - } - - purple_debug_info("msim_add_contact_from_server_cb", - "*** about to add/update username=%s\n", username); - - /* 1. Creates a new group, or gets existing group if it exists (or so - * the documentation claims). */ - group_name = msim_msg_get_string(contact_info, "GroupName"); - if (!group_name || (*group_name == '\0')) { - g_free(group_name); - group_name = g_strdup(_("IM Friends")); - purple_debug_info("myspace", "No GroupName specified, defaulting to '%s'.\n", group_name); - } - group = purple_find_group(group_name); - if (!group) { - group = purple_group_new(group_name); - /* Add group to beginning. See #2752. */ - purple_blist_add_group(group, NULL); - } - g_free(group_name); - - /* 2. Get or create buddy */ - buddy = purple_find_buddy(session->account, username); - if (!buddy) { - purple_debug_info("msim_add_contact_from_server_cb", - "creating new buddy: %s\n", username); - buddy = purple_buddy_new(session->account, username, NULL); - } - - /* TODO: use 'Position' in contact_info to take into account where buddy is */ - purple_blist_add_buddy(buddy, NULL, group, NULL /* insertion point */); - - /* 3. Update buddy information */ - user = msim_get_user_from_buddy(buddy, TRUE); - - /* All buddies on list should have 'uid' integer associated with them. */ - purple_blist_node_set_int(&buddy->node, "UserID", uid); - - /* Stores a few fields in the MsimUser, relevant to the buddy itself. - * AvatarURL, Headline, ContactID. */ - msim_store_user_info(session, contact_info, NULL); - - /* TODO: other fields, store in 'user' */ - msim_msg_free(contact_info); - - g_free(username); -} - -/** Add first ContactID in contact_info to buddy's list. Used to add - * server-side buddies to client-side list. - * - * @return TRUE if added. - * */ -static gboolean -msim_add_contact_from_server(MsimSession *session, MsimMessage *contact_info) -{ - guint uid; - const gchar *username; - - uid = msim_msg_get_integer(contact_info, "ContactID"); - g_return_val_if_fail(uid != 0, FALSE); - - /* Lookup the username, since NickName and IMName is unreliable */ - username = msim_uid2username_from_blist(session->account, uid); - if (!username) { - gchar *uid_str; - - uid_str = g_strdup_printf("%d", uid); - purple_debug_info("msim_add_contact_from_server", - "contact_info addr=%p\n", contact_info); - msim_lookup_user(session, uid_str, msim_add_contact_from_server_cb, (gpointer)msim_msg_clone(contact_info)); - g_free(uid_str); - } else { - msim_add_contact_from_server_cb(session, NULL, (gpointer)msim_msg_clone(contact_info)); - } - - /* Say that the contact was added, even if we're still looking up - * their username. */ - return TRUE; -} - -/** Called when contact list is received from server. */ -static void -msim_got_contact_list(MsimSession *session, MsimMessage *reply, gpointer user_data) -{ - MsimMessage *body, *body_node; - gchar *msg; - guint buddy_count; - - msim_msg_dump("msim_got_contact_list: reply=%s", reply); - - body = msim_msg_get_dictionary(reply, "body"); - if (!body) { - /* No friends. Not an error. */ - return; - } - - buddy_count = 0; - - for (body_node = body; - body_node != NULL; - body_node = msim_msg_get_next_element_node(body_node)) - { - MsimMessageElement *elem; - - elem = (MsimMessageElement *)body_node->data; - - if (g_str_equal(elem->name, "ContactID")) - { - /* Will look for first contact in body_node */ - if (msim_add_contact_from_server(session, body_node)) { - ++buddy_count; - } - } - } - - switch (GPOINTER_TO_UINT(user_data)) { - case MSIM_CONTACT_LIST_IMPORT_ALL_FRIENDS: - msg = g_strdup_printf(ngettext("%d buddy was added or updated from the server (including buddies already on the server-side list)", - "%d buddies were added or updated from the server (including buddies already on the server-side list)", - buddy_count), - buddy_count); - purple_notify_info(session->account, _("Add contacts from server"), msg, NULL); - g_free(msg); - break; - - case MSIM_CONTACT_LIST_IMPORT_TOP_FRIENDS: - /* TODO */ - break; - - case MSIM_CONTACT_LIST_INITIAL_FRIENDS: - /* Nothing */ - break; - } - - msim_msg_free(body); -} - -/* Get contact list, calling msim_got_contact_list() with what_to_do_after as user_data gpointer. */ -static gboolean -msim_get_contact_list(MsimSession *session, int what_to_do_after) -{ - return msim_send(session, - "persist", MSIM_TYPE_INTEGER, 1, - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - "cmd", MSIM_TYPE_INTEGER, MSIM_CMD_GET, - "dsn", MSIM_TYPE_INTEGER, MG_LIST_ALL_CONTACTS_DSN, - "lid", MSIM_TYPE_INTEGER, MG_LIST_ALL_CONTACTS_LID, - "uid", MSIM_TYPE_INTEGER, session->userid, - "rid", MSIM_TYPE_INTEGER, - msim_new_reply_callback(session, msim_got_contact_list, GUINT_TO_POINTER(what_to_do_after)), - "body", MSIM_TYPE_STRING, g_strdup(""), - NULL); -} - - -/** Called when friends have been imported to buddy list on server. */ -static void -msim_import_friends_cb(MsimSession *session, MsimMessage *reply, gpointer user_data) -{ - MsimMessage *body; - gchar *completed; - msim_msg_dump("msim_import_friends_cb=%s", reply); - - /* Check if the friends were imported successfully. */ - body = msim_msg_get_dictionary(reply, "body"); - g_return_if_fail(body != NULL); - completed = msim_msg_get_string(body, "Completed"); - g_return_if_fail(body != NULL); - msim_msg_free(body); - if (!g_str_equal(completed, "True")) - { - purple_debug_info("msim_import_friends_cb", - "failed to import friends: %s", completed); - purple_notify_error(session->account, _("Add friends from MySpace.com"), - _("Importing friends failed"), NULL); - g_free(completed); - return; - } - g_free(completed); - - purple_debug_info("msim_import_friends_cb", - "added friends to server-side buddy list, requesting new contacts from server"); - - msim_get_contact_list(session, MSIM_CONTACT_LIST_IMPORT_ALL_FRIENDS); - - /* TODO: show, X friends have been added */ -} - -/** Import friends from myspace.com. */ -static void msim_import_friends(PurplePluginAction *action) -{ - PurpleConnection *gc; - MsimSession *session; - gchar *group_name; - - gc = (PurpleConnection *)action->context; - session = (MsimSession *)gc->proto_data; - - group_name = "MySpace Friends"; - - g_return_if_fail(msim_send(session, - "persist", MSIM_TYPE_INTEGER, 1, - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - "cmd", MSIM_TYPE_INTEGER, MSIM_CMD_PUT, - "dsn", MSIM_TYPE_INTEGER, MC_IMPORT_ALL_FRIENDS_DSN, - "lid", MSIM_TYPE_INTEGER, MC_IMPORT_ALL_FRIENDS_LID, - "uid", MSIM_TYPE_INTEGER, session->userid, - "rid", MSIM_TYPE_INTEGER, - msim_new_reply_callback(session, msim_import_friends_cb, NULL), - "body", MSIM_TYPE_STRING, - g_strdup_printf("GroupName=%s", group_name), - NULL)); - - -} - -/** Actions menu for account. */ -GList * -msim_actions(PurplePlugin *plugin, gpointer context) -{ - PurpleConnection *gc; - GList *menu; - PurplePluginAction *act; - - gc = (PurpleConnection *)context; - - menu = NULL; - -#if 0 - /* TODO: find out how */ - act = purple_plugin_action_new(_("Find people..."), msim_); - menu = g_list_append(menu, act); - - act = purple_plugin_action_new(_("Change IM name..."), NULL); - menu = g_list_append(menu, act); -#endif - - act = purple_plugin_action_new(_("Add friends from MySpace.com"), msim_import_friends); - menu = g_list_append(menu, act); - - return menu; -} - -/** Callbacks called by Purple, to access this plugin. */ static PurplePluginProtocolInfo prpl_info = { /* options */ OPT_PROTO_USE_POINTSIZE /* specify font size in sane point size */ | OPT_PROTO_MAIL_CHECK, - /* | OPT_PROTO_IM_IMAGE - TODO: direct images. */ + /* | OPT_PROTO_IM_IMAGE - TODO: direct images. */ NULL, /* user_splits */ NULL, /* protocol_options */ NO_BUDDY_ICONS, /* icon_spec - TODO: eventually should add this */ @@ -3126,9 +3025,9 @@ msim_remove_buddy, /* remove_buddy */ NULL, /* remove_buddies */ NULL, /* add_permit */ - NULL, /* add_deny */ + msim_add_deny, /* add_deny */ NULL, /* rem_permit */ - NULL, /* rem_deny */ + msim_rem_deny, /* rem_deny */ NULL, /* set_permit_deny */ NULL, /* join_chat */ NULL, /* reject chat invite */ @@ -3170,9 +3069,119 @@ msim_get_account_text_table, /* get_account_text_table */ }; - - -/** Based on MSN's plugin info comments. */ +/** + * Load the plugin. + */ +static 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; +} + +/** + * Called when friends have been imported to buddy list on server. + */ +static void +msim_import_friends_cb(MsimSession *session, const MsimMessage *reply, gpointer user_data) +{ + MsimMessage *body; + gchar *completed; + + /* Check if the friends were imported successfully. */ + body = msim_msg_get_dictionary(reply, "body"); + g_return_if_fail(body != NULL); + completed = msim_msg_get_string(body, "Completed"); + g_return_if_fail(body != NULL); + msim_msg_free(body); + if (!g_str_equal(completed, "True")) + { + purple_debug_info("msim_import_friends_cb", + "failed to import friends: %s", completed); + purple_notify_error(session->account, _("Add friends from MySpace.com"), + _("Importing friends failed"), NULL); + g_free(completed); + return; + } + g_free(completed); + + purple_debug_info("msim_import_friends_cb", + "added friends to server-side buddy list, requesting new contacts from server"); + + msim_get_contact_list(session, MSIM_CONTACT_LIST_IMPORT_ALL_FRIENDS); + + /* TODO: show, X friends have been added */ +} + +/** + * Import friends from myspace.com. + */ +static void msim_import_friends(PurplePluginAction *action) +{ + PurpleConnection *gc; + MsimSession *session; + gchar *group_name; + + gc = (PurpleConnection *)action->context; + session = (MsimSession *)gc->proto_data; + + group_name = "MySpace Friends"; + + g_return_if_fail(msim_send(session, + "persist", MSIM_TYPE_INTEGER, 1, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + "cmd", MSIM_TYPE_INTEGER, MSIM_CMD_PUT, + "dsn", MSIM_TYPE_INTEGER, MC_IMPORT_ALL_FRIENDS_DSN, + "lid", MSIM_TYPE_INTEGER, MC_IMPORT_ALL_FRIENDS_LID, + "uid", MSIM_TYPE_INTEGER, session->userid, + "rid", MSIM_TYPE_INTEGER, + msim_new_reply_callback(session, msim_import_friends_cb, NULL), + "body", MSIM_TYPE_STRING, + g_strdup_printf("GroupName=%s", group_name), + NULL)); +} + +/** + * Actions menu for account. + */ +static GList * +msim_actions(PurplePlugin *plugin, gpointer context) +{ + PurpleConnection *gc; + GList *menu; + PurplePluginAction *act; + + gc = (PurpleConnection *)context; + + menu = NULL; + +#if 0 + /* TODO: find out how */ + act = purple_plugin_action_new(_("Find people..."), msim_); + menu = g_list_append(menu, act); + + act = purple_plugin_action_new(_("Change IM name..."), NULL); + menu = g_list_append(menu, act); +#endif + + act = purple_plugin_action_new(_("Add friends from MySpace.com"), msim_import_friends); + menu = g_list_append(menu, act); + + return menu; +} + +/** + * Based on MSN's plugin info comments. + */ static PurplePluginInfo info = { PURPLE_PLUGIN_MAGIC, PURPLE_MAJOR_VERSION, @@ -3206,32 +3215,18 @@ NULL /**< reserved4 */ }; - #ifdef MSIM_SELF_TEST -/** Test functions. +/* + * 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 learning * how msimprpl is architected. */ -void -msim_test_all(void) { - guint failures; - - - failures = 0; - 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); -} - -/** Test MsimMessage for basic functionality. */ -int + +/** + * Test MsimMessage for basic functionality. + */ +static int msim_test_msg(void) { MsimMessage *msg, *msg_cloned, *msg2; @@ -3308,8 +3303,10 @@ return failures; } -/** Test protocol-level escaping/unescaping. */ -int +/** + * Test protocol-level escaping/unescaping. + */ +static int msim_test_escaping(void) { guint failures; @@ -3340,87 +3337,146 @@ return failures; } + +static void +msim_test_all(void) +{ + guint failures; + + failures = 0; + 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); +} #endif -static gboolean -msim_uri_handler(const gchar *proto, const gchar *cmd, GHashTable *params) +#ifdef MSIM_CHECK_NEWER_VERSION +/** + * Callback for when a currentversion.txt has been downloaded. + */ +static void +msim_check_newer_version_cb(PurpleUtilFetchUrlData *url_data, + gpointer user_data, + const gchar *url_text, + gsize len, + const gchar *error_message) { - PurpleAccount *account; - MsimSession *session; - GList *l; - gchar *uid_str, *cid_str; - guint uid, cid; - - if (g_ascii_strcasecmp(proto, "myim")) - return FALSE; - - /* Parameters are case-insensitive. */ - uid_str = g_hash_table_lookup(params, "uid"); - cid_str = g_hash_table_lookup(params, "cid"); - - uid = uid_str ? atol(uid_str) : 0; - cid = cid_str ? atol(cid_str) : 0; - - /* Need a contact. */ - g_return_val_if_fail(cid != 0, FALSE); - - /* TODO: if auto=true, "Add all the people on this page to my IM List!", on - * http://collect.myspace.com/index.cfm?fuseaction=im.friendslist. Don't need a cid. */ - - /* Convert numeric contact ID back to a string. Needed for looking up. Don't just - * directly use cid directly from parameters, because it might not be numeric. - * It is trivial to change this to allow cID to be a username, but that's not how - * the official MySpaceIM client works, so don't provide that functionality. */ - cid_str = g_strdup_printf("%d", cid); - - - /* Find our account with specified user id, or use first connected account if uid=0. */ - account = NULL; - l = purple_accounts_get_all(); - while (l) { - if (purple_account_is_connected(l->data) && - (uid == 0 || purple_account_get_int(l->data, "uid", 0) == uid)) { - account = l->data; - break; - } - l = l->next; + GKeyFile *keyfile; + GError *error; + GString *data; + gchar *newest_filever; + + if (!url_text) { + purple_debug_info("msim_check_newer_version_cb", + "got error: %s\n", error_message); + return; + } + + purple_debug_info("msim_check_newer_version_cb", + "url_text=%s\n", url_text ? url_text : "(NULL)"); + + /* Prepend [group] so that GKeyFile can parse it (requires a group). */ + data = g_string_new(url_text); + purple_debug_info("msim", "data=%s\n", data->str + ? data->str : "(NULL)"); + data = g_string_prepend(data, "[group]\n"); + + purple_debug_info("msim", "data=%s\n", data->str + ? data->str : "(NULL)"); + + /* url_text is variable=data\n...†*/ + + /* Check FILEVER, 1.0.716.0. 716 is build, MSIM_CLIENT_VERSION */ + /* New (english) version can be downloaded from SETUPURL+SETUPFILE */ + + error = NULL; + keyfile = g_key_file_new(); + + /* Default list seperator is ;, but currentversion.txt doesn't have + * these, so set to an unused character to avoid parsing problems. */ + g_key_file_set_list_separator(keyfile, '\0'); + + g_key_file_load_from_data(keyfile, data->str, data->len, + G_KEY_FILE_NONE, &error); + g_string_free(data, TRUE); + + if (error != NULL) { + purple_debug_info("msim_check_newer_version_cb", + "couldn't parse, error: %d %d %s\n", + error->domain, error->code, error->message); + g_error_free(error); + return; } - if (!account) { - purple_notify_error(NULL, _("myim URL handler"), - _("No suitable MySpaceIM account could be found to open this myim URL."), - _("Enable the proper MySpaceIM account and try again.")); - g_free(cid_str); - return FALSE; + gchar **ks; + guint n; + ks = g_key_file_get_keys(keyfile, "group", &n, NULL); + purple_debug_info("msim", "n=%d\n", n); + guint i; + for (i = 0; ks[i] != NULL; ++i) + { + purple_debug_info("msim", "%d=%s\n", i, ks[i]); + } + + newest_filever = g_key_file_get_string(keyfile, "group", + "FILEVER", &error); + + purple_debug_info("msim_check_newer_version_cb", + "newest filever: %s\n", newest_filever ? + newest_filever : "(NULL)"); + if (error != NULL) { + purple_debug_info("msim_check_newer_version_cb", + "error: %d %d %s\n", + error->domain, error->code, error->message); + g_error_free(error); } - session = (MsimSession *)account->gc->proto_data; - g_return_val_if_fail(session != NULL, FALSE); - - /* Lookup userid to username. TODO: push this down, to IM sending/contact - * adding functions. */ - - /* myim:sendIM?uID=USERID&cID=CONTACTID */ - if (!g_ascii_strcasecmp(cmd, "sendIM")) { - msim_lookup_user(session, cid_str, (MSIM_USER_LOOKUP_CB)msim_uri_handler_sendIM_cb, NULL); - g_free(cid_str); - return TRUE; - - /* myim:addContact?uID=USERID&cID=CONTACTID */ - } else if (!g_ascii_strcasecmp(cmd, "addContact")) { - msim_lookup_user(session, cid_str, (MSIM_USER_LOOKUP_CB)msim_uri_handler_addContact_cb, NULL); - g_free(cid_str); - return TRUE; + g_key_file_free(keyfile); + + exit(0); +} +#endif + +/** + Handle a myim:addContact command, after username has been looked up. + */ +static void +msim_uri_handler_addContact_cb(MsimSession *session, MsimMessage *userinfo, gpointer data) +{ + MsimMessage *body; + gchar *username; + + body = msim_msg_get_dictionary(userinfo, "body"); + username = msim_msg_get_string(body, "UserName"); + msim_msg_free(body); + + if (!username) { + guint uid; + + uid = msim_msg_get_integer(userinfo, "UserID"); + g_return_if_fail(uid != 0); + + username = g_strdup_printf("%d", uid); } - return FALSE; + purple_blist_request_add_buddy(session->account, username, _("Buddies"), NULL); + + g_free(username); } /* TODO: move uid->username resolving to IM sending and buddy adding functions, * so that user can manually add or IM by userid and username automatically * looked up if possible? */ - -/** Handle a myim:sendIM URI command, after username has been looked up. */ + +/** + * Handle a myim:sendIM URI command, after username has been looked up. + */ static void msim_uri_handler_sendIM_cb(MsimSession *session, MsimMessage *userinfo, gpointer data) { @@ -3454,35 +3510,85 @@ g_free(username); } -/** Handle a myim:addContact command, after username has been looked up. */ -static void -msim_uri_handler_addContact_cb(MsimSession *session, MsimMessage *userinfo, gpointer data) +static gboolean +msim_uri_handler(const gchar *proto, const gchar *cmd, GHashTable *params) { - MsimMessage *body; - gchar *username; - - body = msim_msg_get_dictionary(userinfo, "body"); - username = msim_msg_get_string(body, "UserName"); - msim_msg_free(body); - - if (!username) { - guint uid; - - uid = msim_msg_get_integer(userinfo, "UserID"); - g_return_if_fail(uid != 0); - - username = g_strdup_printf("%d", uid); + PurpleAccount *account; + MsimSession *session; + GList *l; + gchar *uid_str, *cid_str; + guint uid, cid; + + if (g_ascii_strcasecmp(proto, "myim")) + return FALSE; + + /* Parameters are case-insensitive. */ + uid_str = g_hash_table_lookup(params, "uid"); + cid_str = g_hash_table_lookup(params, "cid"); + + uid = uid_str ? atol(uid_str) : 0; + cid = cid_str ? atol(cid_str) : 0; + + /* Need a contact. */ + g_return_val_if_fail(cid != 0, FALSE); + + /* TODO: if auto=true, "Add all the people on this page to my IM List!", on + * http://collect.myspace.com/index.cfm?fuseaction=im.friendslist. Don't need a cid. */ + + /* Convert numeric contact ID back to a string. Needed for looking up. Don't just + * directly use cid directly from parameters, because it might not be numeric. + * It is trivial to change this to allow cID to be a username, but that's not how + * the official MySpaceIM client works, so don't provide that functionality. */ + cid_str = g_strdup_printf("%d", cid); + + + /* Find our account with specified user id, or use first connected account if uid=0. */ + account = NULL; + l = purple_accounts_get_all(); + while (l) { + if (purple_account_is_connected(l->data) && + (uid == 0 || purple_account_get_int(l->data, "uid", 0) == uid)) { + account = l->data; + break; + } + l = l->next; } - - purple_blist_request_add_buddy(session->account, username, _("Buddies"), NULL); - - g_free(username); + if (!account) { + purple_notify_error(NULL, _("myim URL handler"), + _("No suitable MySpaceIM account could be found to open this myim URL."), + _("Enable the proper MySpaceIM account and try again.")); + g_free(cid_str); + return FALSE; + } + + session = (MsimSession *)account->gc->proto_data; + g_return_val_if_fail(session != NULL, FALSE); + + /* Lookup userid to username. TODO: push this down, to IM sending/contact + * adding functions. */ + + /* myim:sendIM?uID=USERID&cID=CONTACTID */ + if (!g_ascii_strcasecmp(cmd, "sendIM")) { + msim_lookup_user(session, cid_str, (MSIM_USER_LOOKUP_CB)msim_uri_handler_sendIM_cb, NULL); + g_free(cid_str); + return TRUE; + + /* myim:addContact?uID=USERID&cID=CONTACTID */ + } else if (!g_ascii_strcasecmp(cmd, "addContact")) { + msim_lookup_user(session, cid_str, (MSIM_USER_LOOKUP_CB)msim_uri_handler_addContact_cb, NULL); + g_free(cid_str); + return TRUE; + } + + return FALSE; } -/** Initialize plugin. */ -void -init_plugin(PurplePlugin *plugin) +/** + * Initialize plugin. + */ +static void +init_plugin(PurplePlugin *plugin) { #ifdef MSIM_SELF_TEST msim_test_all(); @@ -3533,7 +3639,7 @@ #endif /* Code below only runs once. Based on oscar.c's oscar_init(). */ - if (initialized) + if (initialized) return; initialized = TRUE;