Mercurial > pidgin.yaz
changeset 19242:f60192409e7f
merge of '0ae78c5d891099ade8e599dc2087ff9411b98d5f'
and '615e30de07cd1a72f9af2a7949cd9b4ebdab63a0'
author | Jeffrey Connelly <jaconnel@calpoly.edu> |
---|---|
date | Tue, 14 Aug 2007 02:45:41 +0000 |
parents | 0651af21ceee (diff) 6ea5602643ea (current diff) |
children | a242e014ab8c |
files | libpurple/protocols/myspace/myspace.c pidgin/sounds/Makefile.am pidgin/sounds/Makefile.mingw pidgin/sounds/alert.wav pidgin/sounds/login.wav pidgin/sounds/logout.wav pidgin/sounds/receive.wav pidgin/sounds/send.wav |
diffstat | 203 files changed, 7992 insertions(+), 4786 deletions(-) [+] |
line wrap: on
line diff
--- a/COPYRIGHT Tue Aug 14 02:45:06 2007 +0000 +++ b/COPYRIGHT Tue Aug 14 02:45:41 2007 +0000 @@ -278,6 +278,7 @@ Joao Luís Marques Pinto Aleksander Piotrowski Julien Pivotto +Eric Polino <aluink@gmail.com> Ari Pollak Robey Pointer Eric Polino
--- a/ChangeLog Tue Aug 14 02:45:06 2007 +0000 +++ b/ChangeLog Tue Aug 14 02:45:41 2007 +0000 @@ -5,10 +5,18 @@ * Added an account action to open your inbox in the yahoo prpl. * Added support for Unicode status messages in Yahoo. * Server-stored aliases for Yahoo. (John Moody) + * Fixed support for Yahoo! doodling. + * Bonjour plugin uses native Avahi instead of Howl + * Bonjour plugin supports Buddy Icons Pidgin: * Show current outgoing conversation formatting on the font label on the toolbar + * Slim new redesign of conversation tabs to maximize number of + conversations that can fit in a window + + Finch: + * Sound support (Eric Polino) version 2.1.0 (07/28/2007): libpurple:
--- a/ChangeLog.API Tue Aug 14 02:45:06 2007 +0000 +++ b/ChangeLog.API Tue Aug 14 02:45:41 2007 +0000 @@ -1,5 +1,15 @@ Pidgin and Finch: The Pimpin' Penguin IM Clients That're Good for the Soul +Version 2.1.1 (x/x/x): + libpurple: + Changed: + * PurpleAccountUiOps.request_authorize's authorize_cb and + deny_cb parameters now correctly have type + PurpleAccountRequestAuthorizationCb rather than GCallback. + (You'll want to change your UI's implementation's signature + to avoid warnings, and then remove some now-redundant casts + back to the proper type.) + version 2.1.0 (7/28/2007): libpurple: Added:
--- a/ChangeLog.win32 Tue Aug 14 02:45:06 2007 +0000 +++ b/ChangeLog.win32 Tue Aug 14 02:45:41 2007 +0000 @@ -9,6 +9,9 @@ Apple Bonjour for Windows from: http://www.apple.com/support/downloads/bonjourforwindows.html +version 2.0.1 (5/24/2007): + * No changes + version 2.0.0 (5/3/2007): * URI Handler support added via `pidgin.exe --protocolhandler=` * Running a second instance will popup the Buddy List, if possible.
--- a/Doxyfile.in Tue Aug 14 02:45:06 2007 +0000 +++ b/Doxyfile.in Tue Aug 14 02:45:41 2007 +0000 @@ -300,7 +300,7 @@ # by member name. If set to NO (the default) the members will appear in # declaration order. -SORT_BRIEF_DOCS = YES +SORT_BRIEF_DOCS = NO # If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be # sorted by fully-qualified names, including namespaces. If set to
--- a/Makefile.am Tue Aug 14 02:45:06 2007 +0000 +++ b/Makefile.am Tue Aug 14 02:45:41 2007 +0000 @@ -42,7 +42,7 @@ GNT_DIR=finch endif -SUBDIRS = libpurple doc $(GNT_DIR) $(GTK_DIR) m4macros po +SUBDIRS = libpurple doc $(GNT_DIR) $(GTK_DIR) m4macros po share docs: Doxyfile if HAVE_DOXYGEN
--- a/Makefile.mingw Tue Aug 14 02:45:06 2007 +0000 +++ b/Makefile.mingw Tue Aug 14 02:45:41 2007 +0000 @@ -67,6 +67,7 @@ $(MAKE) -C $(PURPLE_TOP) -f $(MINGW_MAKEFILE) install $(MAKE) -C $(PIDGIN_TOP) -f $(MINGW_MAKEFILE) install $(MAKE) -C $(PURPLE_PO_TOP) -f $(MINGW_MAKEFILE) install + $(MAKE) -C share -f $(MINGW_MAKEFILE) install create_release_install_dir: install rm -rf $(PIDGIN_INSTALL_DIR).release
--- a/configure.ac Tue Aug 14 02:45:06 2007 +0000 +++ b/configure.ac Tue Aug 14 02:45:41 2007 +0000 @@ -54,9 +54,9 @@ m4_define([gnt_lt_current], [1]) m4_define([gnt_major_version], [2]) -m4_define([gnt_minor_version], [0]) +m4_define([gnt_minor_version], [1]) m4_define([gnt_micro_version], [0]) -m4_define([gnt_version_suffix], []) +m4_define([gnt_version_suffix], [devel]) m4_define([gnt_version], [gnt_major_version.gnt_minor_version.gnt_micro_version]) m4_define([gnt_display_version], gnt_version[]m4_ifdef([gnt_version_suffix],[gnt_version_suffix])) @@ -2090,6 +2090,8 @@ AC_DEFINE(DEBUG, 1, [Define if debugging is enabled.]) fi +AM_CONDITIONAL(PURPLE_AVAILABLE, true) + AC_OUTPUT([Makefile Doxyfile doc/Makefile @@ -2168,7 +2170,6 @@ pidgin/plugins/perl/Makefile pidgin/plugins/perl/common/Makefile.PL pidgin/plugins/ticker/Makefile - pidgin/sounds/Makefile libpurple/example/Makefile libpurple/gconf/Makefile libpurple/purple.pc @@ -2202,6 +2203,8 @@ libpurple/protocols/zephyr/Makefile libpurple/tests/Makefile libpurple/version.h + share/Makefile + share/sounds/Makefile finch/Makefile finch/libgnt/Makefile finch/libgnt/gnt.pc
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/ui-ops.dox Tue Aug 14 02:45:41 2007 +0000 @@ -0,0 +1,24 @@ +/** @page ui-ops UiOps structures + + When implementing a UI for libpurple, you need to fill in various UiOps + structures: + + - #PurpleAccountUiOps + - #PurpleBlistUiOps + - #PurpleConnectionUiOps + - #PurpleConversationUiOps + - #PurpleCoreUiOps + - #PurpleDebugUiOps + - #PurpleDnsQueryUiOps + - #PurpleEventLoopUiOps + - #PurpleIdleUiOps + - #PurpleNotifyUiOps + - #PurplePrivacyUiOps + - #PurpleRequestUiOps + - #PurpleRoomlistUiOps + - #PurpleSoundUiOps + - #PurpleWhiteboardUiOps + - #PurpleXferUiOps + + */ +// vim: ft=c.doxygen
--- a/finch/Makefile.am Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/Makefile.am Tue Aug 14 02:45:41 2007 +0000 @@ -60,6 +60,7 @@ $(GLIB_LIBS) \ $(LIBXML_LIBS) \ $(GNT_LIBS) \ + $(GSTREAMER_LIBS) \ ./libgnt/libgnt.la \ $(top_builddir)/libpurple/libpurple.la @@ -77,4 +78,5 @@ $(GLIB_CFLAGS) \ $(DBUS_CFLAGS) \ $(LIBXML_CFLAGS) \ + $(GSTREAMER_CFLAGS) \ $(GNT_CFLAGS)
--- a/finch/finch.c Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/finch.c Tue Aug 14 02:45:41 2007 +0000 @@ -55,16 +55,10 @@ purple_debug_set_ui_ops(finch_debug_get_ui_ops()); } -/* XXX: this "leaks" a hashtable on shutdown. I'll let - * the finch guys decide if they want to go through the trouble - * of properly freeing it, since their quit function doesn't - * live in this file */ - static GHashTable *ui_info = NULL; - static GHashTable *finch_ui_get_info() { - if(NULL == ui_info) { + if (ui_info == NULL) { ui_info = g_hash_table_new(g_str_hash, g_str_equal); g_hash_table_insert(ui_info, "name", (char*)_("Finch")); @@ -74,12 +68,20 @@ return ui_info; } +static void +finch_quit(void) +{ + gnt_ui_uninit(); + if (ui_info) + g_hash_table_destroy(ui_info); +} + static PurpleCoreUiOps core_ops = { finch_prefs_init, debug_init, gnt_ui_init, - gnt_ui_uninit, + finch_quit, finch_ui_get_info, /* padding */ @@ -396,18 +398,29 @@ return 1; } -int main(int argc, char **argv) +static gboolean gnt_start(int *argc, char ***argv) +{ + /* Initialize the libpurple stuff */ + if (!init_libpurple(*argc, *argv)) + return FALSE; + + purple_blist_show(); + return TRUE; +} + +int main(int argc, char *argv[]) { signal(SIGPIPE, SIG_IGN); g_set_prgname("Finch"); +#if GLIB_CHECK_VERSION(2,2,0) g_set_application_name(_("Finch")); +#endif - /* Initialize the libpurple stuff */ - if (!init_libpurple(argc, argv)) - return 0; - - purple_blist_show(); + gnt_init(); + + gnt_start(&argc, &argv); + gnt_main(); #ifdef STANDALONE
--- a/finch/finch.h Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/finch.h Tue Aug 14 02:45:41 2007 +0000 @@ -27,3 +27,4 @@ #define FINCH_UI "gnt-purple" +#define FINCH_PREFS_ROOT "/finch"
--- a/finch/gntaccount.c Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/gntaccount.c Tue Aug 14 02:45:41 2007 +0000 @@ -912,9 +912,15 @@ } static void * -finch_request_authorize(PurpleAccount *account, const char *remote_user, - const char *id, const char *alias, const char *message, gboolean on_list, - GCallback auth_cb, GCallback deny_cb, void *user_data) +finch_request_authorize(PurpleAccount *account, + const char *remote_user, + const char *id, + const char *alias, + const char *message, + gboolean on_list, + PurpleAccountRequestAuthorizationCb auth_cb, + PurpleAccountRequestAuthorizationCb deny_cb, + void *user_data) { char *buffer; PurpleConnection *gc; @@ -941,8 +947,8 @@ GList *iter; auth_and_add *aa = g_new(auth_and_add, 1); - aa->auth_cb = (PurpleAccountRequestAuthorizationCb)auth_cb; - aa->deny_cb = (PurpleAccountRequestAuthorizationCb)deny_cb; + aa->auth_cb = auth_cb; + aa->deny_cb = deny_cb; aa->data = user_data; aa->username = g_strdup(remote_user); aa->alias = g_strdup(alias);
--- a/finch/gntconv.h Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/gntconv.h Tue Aug 14 02:45:41 2007 +0000 @@ -30,6 +30,9 @@ #include "conversation.h" +/* Grabs the conv out of a PurpleConverstation */ +#define FINCH_CONV(conv) ((FinchConv *)(conv)->ui_data) + /*************************************************************************** * @name GNT Conversations API ***************************************************************************/
--- a/finch/gntsound.c Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/gntsound.c Tue Aug 14 02:45:41 2007 +0000 @@ -23,28 +23,1052 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "internal.h" +#include "finch.h" + +#ifdef _WIN32 +#include <windows.h> +#include <mmsystem.h> +#endif + +#ifdef USE_GSTREAMER +#include <gst/gst.h> +#endif /* USE_GSTREAMER */ + +#include "debug.h" +#include "notify.h" +#include "prefs.h" +#include "sound.h" +#include "util.h" + +#include "gntbox.h" +#include "gntwindow.h" +#include "gntcombobox.h" +#include "gntlabel.h" +#include "gntconv.h" #include "gntsound.h" +#include "gntwidget.h" +#include "gntentry.h" +#include "gntcheckbox.h" +#include "gntline.h" +#include "gntslider.h" +#include "gnttree.h" +#include "gntfilesel.h" + +typedef struct { + PurpleSoundEventID id; + char *label; + char *pref; + char *def; + char *file; +} FinchSoundEvent; + +typedef struct { + GntWidget *method; + GntWidget *command; + GntWidget *conv_focus; + GntWidget *while_status; + GntWidget *volume; + GntWidget *events; + GntWidget *window; + GntWidget *selector; + + GntWidget *profiles; + GntWidget *new_profile; + gchar * original_profile; +} SoundPrefDialog; + +#define DEFAULT_PROFILE "default" + +static SoundPrefDialog *pref_dialog; + +#define PLAY_SOUND_TIMEOUT 15000 + +static guint mute_login_sounds_timeout = 0; +static gboolean mute_login_sounds = FALSE; + +#ifdef USE_GSTREAMER +static gboolean gst_init_failed; +#endif /* USE_GSTREAMER */ + +static FinchSoundEvent sounds[PURPLE_NUM_SOUNDS] = { + {PURPLE_SOUND_BUDDY_ARRIVE, N_("Buddy logs in"), "login", "login.wav", NULL}, + {PURPLE_SOUND_BUDDY_LEAVE, N_("Buddy logs out"), "logout", "logout.wav", NULL}, + {PURPLE_SOUND_RECEIVE, N_("Message received"), "im_recv", "receive.wav", NULL}, + {PURPLE_SOUND_FIRST_RECEIVE, N_("Message received begins conversation"), "first_im_recv", "receive.wav", NULL}, + {PURPLE_SOUND_SEND, N_("Message sent"), "send_im", "send.wav", NULL}, + {PURPLE_SOUND_CHAT_JOIN, N_("Person enters chat"), "join_chat", "login.wav", NULL}, + {PURPLE_SOUND_CHAT_LEAVE, N_("Person leaves chat"), "left_chat", "logout.wav", NULL}, + {PURPLE_SOUND_CHAT_YOU_SAY, N_("You talk in chat"), "send_chat_msg", "send.wav", NULL}, + {PURPLE_SOUND_CHAT_SAY, N_("Others talk in chat"), "chat_msg_recv", "receive.wav", NULL}, + {PURPLE_SOUND_POUNCE_DEFAULT, NULL, "pounce_default", "alert.wav", NULL}, + {PURPLE_SOUND_CHAT_NICK, N_("Someone says your screen name in chat"), "nick_said", "alert.wav", NULL} +}; + +const char * +finch_sound_get_active_profile() +{ + return purple_prefs_get_string(FINCH_PREFS_ROOT "/sound/actprofile"); +} -const char *finch_sound_get_active_profile(void) +/* This method creates a pref name based on the current active profile. + * So if "Home" is the current active profile the pref name + * [FINCH_PREFS_ROOT "/sound/profiles/Home/$NAME"] is created. + */ +static gchar * +make_pref(const char *name) +{ + static char pref_string[512]; + g_snprintf(pref_string, sizeof(pref_string), + FINCH_PREFS_ROOT "/sound/profiles/%s%s", finch_sound_get_active_profile(), name); + return pref_string; +} + + +static gboolean +unmute_login_sounds_cb(gpointer data) +{ + mute_login_sounds = FALSE; + mute_login_sounds_timeout = 0; + return FALSE; +} + +static gboolean +chat_nick_matches_name(PurpleConversation *conv, const char *aname) { - return NULL; + PurpleConvChat *chat = NULL; + char *nick = NULL; + char *name = NULL; + gboolean ret = FALSE; + chat = purple_conversation_get_chat_data(conv); + + if (chat == NULL) + return ret; + + nick = g_strdup(purple_normalize(conv->account, chat->nick)); + name = g_strdup(purple_normalize(conv->account, aname)); + + if (g_utf8_collate(nick, name) == 0) + ret = TRUE; + + g_free(nick); + g_free(name); + + return ret; +} + +/* + * play a sound event for a conversation, honoring make_sound flag + * of conversation and checking for focus if conv_focus pref is set + */ +static void +play_conv_event(PurpleConversation *conv, PurpleSoundEventID event) +{ + /* If we should not play the sound for some reason, then exit early */ + if (conv != NULL) + { + FinchConv *gntconv; + gboolean has_focus; + + gntconv = FINCH_CONV(conv); + + has_focus = purple_conversation_has_focus(conv); + + if (has_focus && !purple_prefs_get_bool(make_pref("/conv_focus"))) + { + return; + } + } + + purple_sound_play_event(event, conv ? purple_conversation_get_account(conv) : NULL); +} + +static void +buddy_state_cb(PurpleBuddy *buddy, PurpleSoundEventID event) +{ + purple_sound_play_event(event, purple_buddy_get_account(buddy)); +} + +static void +im_msg_received_cb(PurpleAccount *account, char *sender, + char *message, PurpleConversation *conv, + PurpleMessageFlags flags, PurpleSoundEventID event) +{ + if (flags & PURPLE_MESSAGE_DELAYED) + return; + + if (conv == NULL) { + purple_sound_play_event(PURPLE_SOUND_FIRST_RECEIVE, account); + } else { + play_conv_event(conv, event); + } } -void finch_sound_set_active_profile(const char *name) +static void +im_msg_sent_cb(PurpleAccount *account, const char *receiver, + const char *message, PurpleSoundEventID event) +{ + PurpleConversation *conv = purple_find_conversation_with_account( + PURPLE_CONV_TYPE_ANY, receiver, account); + play_conv_event(conv, event); +} + +static void +chat_buddy_join_cb(PurpleConversation *conv, const char *name, + PurpleConvChatBuddyFlags flags, gboolean new_arrival, + PurpleSoundEventID event) +{ + if (new_arrival && !chat_nick_matches_name(conv, name)) + play_conv_event(conv, event); +} + +static void +chat_buddy_left_cb(PurpleConversation *conv, const char *name, + const char *reason, PurpleSoundEventID event) +{ + if (!chat_nick_matches_name(conv, name)) + play_conv_event(conv, event); +} + +static void +chat_msg_sent_cb(PurpleAccount *account, const char *message, + int id, PurpleSoundEventID event) +{ + PurpleConnection *conn = purple_account_get_connection(account); + PurpleConversation *conv = NULL; + + if (conn!=NULL) + conv = purple_find_chat(conn, id); + + play_conv_event(conv, event); +} + +static void +chat_msg_received_cb(PurpleAccount *account, char *sender, + char *message, PurpleConversation *conv, + PurpleMessageFlags flags, PurpleSoundEventID event) +{ + PurpleConvChat *chat; + + if (flags & PURPLE_MESSAGE_DELAYED) + return; + + chat = purple_conversation_get_chat_data(conv); + g_return_if_fail(chat != NULL); + + if (purple_conv_chat_is_user_ignored(chat, sender)) + return; + + if (chat_nick_matches_name(conv, sender)) + return; + + if (flags & PURPLE_MESSAGE_NICK || purple_utf8_has_word(message, chat->nick)) + play_conv_event(conv, PURPLE_SOUND_CHAT_NICK); + else + play_conv_event(conv, event); +} + +/* + * We mute sounds for the 10 seconds after you log in so that + * you don't get flooded with sounds when the blist shows all + * your buddies logging in. + */ +static void +account_signon_cb(PurpleConnection *gc, gpointer data) +{ + if (mute_login_sounds_timeout != 0) + g_source_remove(mute_login_sounds_timeout); + mute_login_sounds = TRUE; + mute_login_sounds_timeout = purple_timeout_add_seconds(10, unmute_login_sounds_cb, NULL); +} + +static void * +finch_sound_get_handle() +{ + static int handle; + + return &handle; +} + + +/* This gets called when the active profile changes */ +static void +initialize_profile(const char *name, PurplePrefType type, gconstpointer val, gpointer null) { + if (purple_prefs_exists(make_pref(""))) + return; + + purple_prefs_add_none(make_pref("")); + purple_prefs_add_none(make_pref("/enabled")); + purple_prefs_add_none(make_pref("/file")); + purple_prefs_add_bool(make_pref("/enabled/login"), TRUE); + purple_prefs_add_path(make_pref("/file/login"), ""); + purple_prefs_add_bool(make_pref("/enabled/logout"), TRUE); + purple_prefs_add_path(make_pref("/file/logout"), ""); + purple_prefs_add_bool(make_pref("/enabled/im_recv"), TRUE); + purple_prefs_add_path(make_pref("/file/im_recv"), ""); + purple_prefs_add_bool(make_pref("/enabled/first_im_recv"), FALSE); + purple_prefs_add_path(make_pref("/file/first_im_recv"), ""); + purple_prefs_add_bool(make_pref("/enabled/send_im"), TRUE); + purple_prefs_add_path(make_pref("/file/send_im"), ""); + purple_prefs_add_bool(make_pref("/enabled/join_chat"), FALSE); + purple_prefs_add_path(make_pref("/file/join_chat"), ""); + purple_prefs_add_bool(make_pref("/enabled/left_chat"), FALSE); + purple_prefs_add_path(make_pref("/file/left_chat"), ""); + purple_prefs_add_bool(make_pref("/enabled/send_chat_msg"), FALSE); + purple_prefs_add_path(make_pref("/file/send_chat_msg"), ""); + purple_prefs_add_bool(make_pref("/enabled/chat_msg_recv"), FALSE); + purple_prefs_add_path(make_pref("/file/chat_msg_recv"), ""); + purple_prefs_add_bool(make_pref("/enabled/nick_said"), FALSE); + purple_prefs_add_path(make_pref("/file/nick_said"), ""); + purple_prefs_add_bool(make_pref("/enabled/pounce_default"), TRUE); + purple_prefs_add_path(make_pref("/file/pounce_default"), ""); + purple_prefs_add_bool(make_pref("/conv_focus"), TRUE); + purple_prefs_add_bool(make_pref("/mute"), FALSE); + purple_prefs_add_path(make_pref("/command"), ""); + purple_prefs_add_string(make_pref("/method"), "automatic"); + purple_prefs_add_int(make_pref("/volume"), 50); +} + +static void +finch_sound_init(void) +{ + void *gnt_sound_handle = finch_sound_get_handle(); + void *blist_handle = purple_blist_get_handle(); + void *conv_handle = purple_conversations_get_handle(); +#ifdef USE_GSTREAMER + GError *error = NULL; +#endif + + purple_signal_connect(purple_connections_get_handle(), "signed-on", + gnt_sound_handle, PURPLE_CALLBACK(account_signon_cb), + NULL); + + purple_prefs_add_none(FINCH_PREFS_ROOT "/sound"); + purple_prefs_add_string(FINCH_PREFS_ROOT "/sound/actprofile", DEFAULT_PROFILE); + purple_prefs_add_none(FINCH_PREFS_ROOT "/sound/profiles"); + + purple_prefs_connect_callback(gnt_sound_handle, FINCH_PREFS_ROOT "/sound/actprofile", initialize_profile, NULL); + purple_prefs_trigger_callback(FINCH_PREFS_ROOT "/sound/actprofile"); + + +#ifdef USE_GSTREAMER + purple_debug_info("sound", "Initializing sound output drivers.\n"); + if ((gst_init_failed = !gst_init_check(NULL, NULL, &error))) { + purple_notify_error(NULL, _("GStreamer Failure"), + _("GStreamer failed to initialize."), + error ? error->message : ""); + if (error) { + g_error_free(error); + error = NULL; + } + } +#endif /* USE_GSTREAMER */ + + purple_signal_connect(blist_handle, "buddy-signed-on", + gnt_sound_handle, PURPLE_CALLBACK(buddy_state_cb), + GINT_TO_POINTER(PURPLE_SOUND_BUDDY_ARRIVE)); + purple_signal_connect(blist_handle, "buddy-signed-off", + gnt_sound_handle, PURPLE_CALLBACK(buddy_state_cb), + GINT_TO_POINTER(PURPLE_SOUND_BUDDY_LEAVE)); + purple_signal_connect(conv_handle, "received-im-msg", + gnt_sound_handle, PURPLE_CALLBACK(im_msg_received_cb), + GINT_TO_POINTER(PURPLE_SOUND_RECEIVE)); + purple_signal_connect(conv_handle, "sent-im-msg", + gnt_sound_handle, PURPLE_CALLBACK(im_msg_sent_cb), + GINT_TO_POINTER(PURPLE_SOUND_SEND)); + purple_signal_connect(conv_handle, "chat-buddy-joined", + gnt_sound_handle, PURPLE_CALLBACK(chat_buddy_join_cb), + GINT_TO_POINTER(PURPLE_SOUND_CHAT_JOIN)); + purple_signal_connect(conv_handle, "chat-buddy-left", + gnt_sound_handle, PURPLE_CALLBACK(chat_buddy_left_cb), + GINT_TO_POINTER(PURPLE_SOUND_CHAT_LEAVE)); + purple_signal_connect(conv_handle, "sent-chat-msg", + gnt_sound_handle, PURPLE_CALLBACK(chat_msg_sent_cb), + GINT_TO_POINTER(PURPLE_SOUND_CHAT_YOU_SAY)); + purple_signal_connect(conv_handle, "received-chat-msg", + gnt_sound_handle, PURPLE_CALLBACK(chat_msg_received_cb), + GINT_TO_POINTER(PURPLE_SOUND_CHAT_SAY)); +} + +static void +finch_sound_uninit(void) +{ +#ifdef USE_GSTREAMER + if (!gst_init_failed) + gst_deinit(); +#endif + + purple_signals_disconnect_by_handle(finch_sound_get_handle()); } -GList *finch_sound_get_profiles(void) +#ifdef USE_GSTREAMER +static gboolean +bus_call (GstBus *bus, GstMessage *msg, gpointer data) +{ + GstElement *play = data; + GError *err = NULL; + + switch (GST_MESSAGE_TYPE (msg)) { + case GST_MESSAGE_EOS: + gst_element_set_state(play, GST_STATE_NULL); + gst_object_unref(GST_OBJECT(play)); + break; + case GST_MESSAGE_ERROR: + gst_message_parse_error(msg, &err, NULL); + purple_debug_error("gstreamer", "%s\n", err->message); + g_error_free(err); + break; + case GST_MESSAGE_WARNING: + gst_message_parse_warning(msg, &err, NULL); + purple_debug_warning("gstreamer", "%s\n", err->message); + g_error_free(err); + break; + default: + break; + } + return TRUE; +} +#endif + +static void +finch_sound_play_file(const char *filename) +{ + const char *method; +#ifdef USE_GSTREAMER + float volume; + char *uri; + GstElement *sink = NULL; + GstElement *play = NULL; + GstBus *bus = NULL; +#endif + if (purple_prefs_get_bool(make_pref("/mute"))) + return; + + method = purple_prefs_get_string(make_pref("/method")); + + if (!strcmp(method, "none")) { + return; + } else if (!strcmp(method, "beep")) { + beep(); + return; + } + + if (!g_file_test(filename, G_FILE_TEST_EXISTS)) { + purple_debug_error("gntsound", "sound file (%s) does not exist.\n", filename); + return; + } + +#ifndef _WIN32 + if (!strcmp(method, "custom")) { + const char *sound_cmd; + char *command; + char *esc_filename; + GError *error = NULL; + + sound_cmd = purple_prefs_get_path(make_pref("/command")); + + if (!sound_cmd || *sound_cmd == '\0') { + purple_debug_error("gntsound", + "'Command' sound method has been chosen, " + "but no command has been set."); + return; + } + + esc_filename = g_shell_quote(filename); + + if (strstr(sound_cmd, "%s")) + command = purple_strreplace(sound_cmd, "%s", esc_filename); + else + command = g_strdup_printf("%s %s", sound_cmd, esc_filename); + + if (!g_spawn_command_line_async(command, &error)) { + purple_debug_error("gntsound", "sound command could not be launched: %s\n", error->message); + g_error_free(error); + } + + g_free(esc_filename); + g_free(command); + return; + } +#ifdef USE_GSTREAMER + if (gst_init_failed) /* Perhaps do beep instead? */ + return; + volume = (float)(CLAMP(purple_prefs_get_int(make_pref("/volume")), 0, 100)) / 50; + if (!strcmp(method, "automatic")) { + if (purple_running_gnome()) { + sink = gst_element_factory_make("gconfaudiosink", "sink"); + } + if (!sink) + sink = gst_element_factory_make("autoaudiosink", "sink"); + if (!sink) { + purple_debug_error("sound", "Unable to create GStreamer audiosink.\n"); + return; + } + } else if (!strcmp(method, "esd")) { + sink = gst_element_factory_make("esdsink", "sink"); + if (!sink) { + purple_debug_error("sound", "Unable to create GStreamer audiosink.\n"); + return; + } + } else if (!strcmp(method, "alsa")) { + sink = gst_element_factory_make("alsasink", "sink"); + if (!sink) { + purple_debug_error("sound", "Unable to create GStreamer audiosink.\n"); + return; + } + } else { + purple_debug_error("sound", "Unknown sound method '%s'\n", method); + return; + } + + play = gst_element_factory_make("playbin", "play"); + + if (play == NULL) { + return; + } + + uri = g_strdup_printf("file://%s", filename); + + g_object_set(G_OBJECT(play), "uri", uri, + "volume", volume, + "audio-sink", sink, NULL); + + bus = gst_pipeline_get_bus(GST_PIPELINE(play)); + gst_bus_add_watch(bus, bus_call, play); + + gst_element_set_state(play, GST_STATE_PLAYING); + + gst_object_unref(bus); + g_free(uri); + +#else /* USE_GSTREAMER */ + beep(); + return; +#endif /* USE_GSTREAMER */ +#else /* _WIN32 */ + purple_debug_info("sound", "Playing %s\n", filename); + + if (G_WIN32_HAVE_WIDECHAR_API ()) { + wchar_t *wc_filename = g_utf8_to_utf16(filename, + -1, NULL, NULL, NULL); + if (!PlaySoundW(wc_filename, NULL, SND_ASYNC | SND_FILENAME)) + purple_debug(PURPLE_DEBUG_ERROR, "sound", "Error playing sound.\n"); + g_free(wc_filename); + } else { + char *l_filename = g_locale_from_utf8(filename, + -1, NULL, NULL, NULL); + if (!PlaySoundA(l_filename, NULL, SND_ASYNC | SND_FILENAME)) + purple_debug(PURPLE_DEBUG_ERROR, "sound", "Error playing sound.\n"); + g_free(l_filename); + } +#endif /* _WIN32 */ +} + +static void +finch_sound_play_event(PurpleSoundEventID event) { - return NULL; + char *enable_pref; + char *file_pref; + if ((event == PURPLE_SOUND_BUDDY_ARRIVE) && mute_login_sounds) + return; + + if (event >= PURPLE_NUM_SOUNDS) { + purple_debug_error("sound", "got request for unknown sound: %d\n", event); + return; + } + + enable_pref = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s/enabled/%s", + finch_sound_get_active_profile(), + sounds[event].pref); + file_pref = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s/file/%s", finch_sound_get_active_profile(), sounds[event].pref); + + /* check NULL for sounds that don't have an option, ie buddy pounce */ + if (purple_prefs_get_bool(enable_pref)) { + char *filename = g_strdup(purple_prefs_get_path(file_pref)); + if (!filename || !strlen(filename)) { + g_free(filename); + /* XXX Consider creating a constant for "sounds/purple" to be shared with Pidgin */ + filename = g_build_filename(DATADIR, "sounds", "purple", sounds[event].def, NULL); + } + + purple_sound_play_file(filename, NULL); + g_free(filename); + } + + g_free(enable_pref); + g_free(file_pref); +} + +GList * +finch_sound_get_profiles() +{ + GList *list = NULL, *iter; + iter = purple_prefs_get_children_names(FINCH_PREFS_ROOT "/sound/profiles"); + while (iter) { + list = g_list_append(list, g_strdup(strrchr(iter->data, '/') + 1)); + g_free(iter->data); + iter = g_list_delete_link(iter, iter); + } + return list; +} + +/* This will also create it if it doesn't exist */ +void +finch_sound_set_active_profile(const char *name) +{ + purple_prefs_set_string(FINCH_PREFS_ROOT "/sound/actprofile", name); +} + +static gboolean +finch_sound_profile_exists(const char *name) +{ + gchar * tmp; + gboolean ret = purple_prefs_exists(tmp = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s", name)); + g_free(tmp); + return ret; +} + +static void +save_cb(GntWidget *button, gpointer win) +{ + GList * itr; + + purple_prefs_set_string(make_pref("/method"), gnt_combo_box_get_selected_data(GNT_COMBO_BOX(pref_dialog->method))); + purple_prefs_set_path(make_pref("/command"), gnt_entry_get_text(GNT_ENTRY(pref_dialog->command))); + purple_prefs_set_bool(make_pref("/conv_focus"), gnt_check_box_get_checked(GNT_CHECK_BOX(pref_dialog->conv_focus))); + purple_prefs_set_int("/purple/sound/while_status", GPOINTER_TO_INT(gnt_combo_box_get_selected_data(GNT_COMBO_BOX(pref_dialog->while_status)))); + purple_prefs_set_int(make_pref("/volume"), gnt_slider_get_value(GNT_SLIDER(pref_dialog->volume))); + + for (itr = gnt_tree_get_rows(GNT_TREE(pref_dialog->events)); itr; itr = itr->next) { + FinchSoundEvent * event = &sounds[GPOINTER_TO_INT(itr->data)]; + char * filepref = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s/file/%s", finch_sound_get_active_profile(), event->pref); + char * boolpref = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s/enabled/%s", finch_sound_get_active_profile(), event->pref); + purple_prefs_set_bool(boolpref, gnt_tree_get_choice(GNT_TREE(pref_dialog->events), itr->data)); + purple_prefs_set_path(filepref, event->file ? event->file : ""); + g_free(filepref); + g_free(boolpref); + } + gnt_widget_destroy(GNT_WIDGET(win)); +} + +static void +file_cb(GntFileSel *w, const char *path, const char *file, gpointer data) +{ + FinchSoundEvent *event = data; + + g_free(event->file); + event->file = g_strdup(path); + + gnt_tree_change_text(GNT_TREE(pref_dialog->events), GINT_TO_POINTER(event->id), 1, file); + gnt_tree_set_choice(GNT_TREE(pref_dialog->events), GINT_TO_POINTER(event->id), TRUE); + + gnt_widget_destroy(GNT_WIDGET(w)); +} + +static void +test_cb(GntWidget *button, gpointer null) +{ + PurpleSoundEventID id = GPOINTER_TO_INT(gnt_tree_get_selection_data(GNT_TREE(pref_dialog->events))); + FinchSoundEvent * event = &sounds[id]; + char *enabled, *file, *tmpfile; + gboolean temp_value; + + enabled = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s/enabled/%s", + finch_sound_get_active_profile(), event->pref); + file = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s/file/%s", + finch_sound_get_active_profile(), event->pref); + + temp_value = purple_prefs_get_bool(enabled); + tmpfile = g_strdup(purple_prefs_get_string(file)); + + purple_prefs_set_string(file, event->file); + if (!temp_value) purple_prefs_set_bool(enabled, TRUE); + + purple_sound_play_event(id, NULL); + + if (!temp_value) purple_prefs_set_bool(enabled, FALSE); + purple_prefs_set_string(file, tmpfile); + + g_free(enabled); + g_free(file); + g_free(tmpfile); +} + +static void +reset_cb(GntWidget *button, gpointer null) +{ + /* Don't dereference this pointer ! */ + gpointer key = gnt_tree_get_selection_data(GNT_TREE(pref_dialog->events)); + + FinchSoundEvent * event = &sounds[GPOINTER_TO_INT(key)]; + g_free(event->file); + event->file = NULL; + gnt_tree_change_text(GNT_TREE(pref_dialog->events), key, 1, _("(default)")); +} + + +static void +choose_cb(GntWidget *button, gpointer null) +{ + GntWidget *w = gnt_file_sel_new(); + GntFileSel *sel = GNT_FILE_SEL(w); + PurpleSoundEventID id = GPOINTER_TO_INT(gnt_tree_get_selection_data(GNT_TREE(pref_dialog->events))); + FinchSoundEvent * event = &sounds[id]; + char *path = NULL; + + gnt_box_set_title(GNT_BOX(w), _("Select Sound File ...")); + gnt_file_sel_set_current_location(sel, + (event && event->file) ? (path = g_path_get_dirname(event->file)) + : purple_home_dir()); + + g_signal_connect_swapped(G_OBJECT(sel->cancel), "activate", G_CALLBACK(gnt_widget_destroy), sel); + g_signal_connect(G_OBJECT(sel), "file_selected", G_CALLBACK(file_cb), event); + g_signal_connect_swapped(G_OBJECT(sel), "destroy", G_CALLBACK(g_nullify_pointer), &pref_dialog->selector); + + /* If there's an already open file-selector, close that one. */ + if (pref_dialog->selector) + gnt_widget_destroy(pref_dialog->selector); + + pref_dialog->selector = w; + + gnt_widget_show(w); + g_free(path); } -PurpleSoundUiOps *finch_sound_get_ui_ops(void) +static void +release_pref_dialog(GntBindable *data, gpointer null) +{ + GList * itr; + for (itr = gnt_tree_get_rows(GNT_TREE(pref_dialog->events)); itr; itr = itr->next) { + PurpleSoundEventID id = GPOINTER_TO_INT(itr->data); + FinchSoundEvent * e = &sounds[id]; + g_free(e->file); + e->file = NULL; + } + if (pref_dialog->selector) + gnt_widget_destroy(pref_dialog->selector); + g_free(pref_dialog); + pref_dialog = NULL; +} + +static void +load_pref_window(const char * profile) { - return NULL; + gint i; + + finch_sound_set_active_profile(profile); + + gnt_combo_box_set_selected(GNT_COMBO_BOX(pref_dialog->method), (gchar *)purple_prefs_get_string(make_pref("/method"))); + + gnt_entry_set_text(GNT_ENTRY(pref_dialog->command), purple_prefs_get_path(make_pref("/command"))); + + gnt_check_box_set_checked(GNT_CHECK_BOX(pref_dialog->conv_focus), purple_prefs_get_bool(make_pref("/conv_focus"))); + + gnt_combo_box_set_selected(GNT_COMBO_BOX(pref_dialog->while_status), GINT_TO_POINTER(purple_prefs_get_int("/purple" "/sound/while_status"))); + + gnt_slider_set_value(GNT_SLIDER(pref_dialog->volume), CLAMP(purple_prefs_get_int(make_pref("/volume")), 0, 100)); + + for (i = 0; i < PURPLE_NUM_SOUNDS; i++) { + FinchSoundEvent * event = &sounds[i]; + gchar *boolpref; + gchar *filepref, *basename = NULL; + const char * profile = finch_sound_get_active_profile(); + + filepref = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s/file/%s", profile, event->pref); + + g_free(event->file); + event->file = g_strdup(purple_prefs_get_path(filepref)); + + g_free(filepref); + if (event->label == NULL) { + continue; + } + + boolpref = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s/enabled/%s", profile, event->pref); + + gnt_tree_change_text(GNT_TREE(pref_dialog->events), GINT_TO_POINTER(i), 0, event->label); + gnt_tree_change_text(GNT_TREE(pref_dialog->events), GINT_TO_POINTER(i), 1, + event->file[0] ? (basename = g_path_get_basename(event->file)) : _("(default)")); + g_free(basename); + + gnt_tree_set_choice(GNT_TREE(pref_dialog->events), GINT_TO_POINTER(i), purple_prefs_get_bool(boolpref)); + + g_free(boolpref); + } + + gnt_tree_set_selected(GNT_TREE(pref_dialog->profiles), (gchar *)finch_sound_get_active_profile()); + + gnt_widget_draw(pref_dialog->window); +} + +static void +reload_pref_window(const char *profile) +{ + if (!strcmp(profile, finch_sound_get_active_profile())) + return; + load_pref_window(profile); +} + +static void +prof_del_cb(GntWidget *button, gpointer null) +{ + const char * profile = gnt_entry_get_text(GNT_ENTRY(pref_dialog->new_profile)); + gchar * pref; + + if (!strcmp(profile, DEFAULT_PROFILE)) + return; + + pref = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s", profile); + purple_prefs_remove(pref); + g_free(pref); + + if (!strcmp(pref_dialog->original_profile, profile)) { + g_free(pref_dialog->original_profile); + pref_dialog->original_profile = g_strdup(DEFAULT_PROFILE); + } + + if(!strcmp(profile, finch_sound_get_active_profile())) + reload_pref_window(DEFAULT_PROFILE); + + gnt_tree_remove(GNT_TREE(pref_dialog->profiles), (gchar *) profile); +} + +static void +prof_add_cb(GntButton *button, GntEntry * entry) +{ + const char * profile = gnt_entry_get_text(entry); + GntTreeRow * row; + if (!finch_sound_profile_exists(profile)) { + gpointer key = g_strdup(profile); + row = gnt_tree_create_row(GNT_TREE(pref_dialog->profiles), profile); + gnt_tree_add_row_after(GNT_TREE(pref_dialog->profiles), key, + row, + NULL, NULL); + gnt_entry_set_text(entry, ""); + gnt_tree_set_selected(GNT_TREE(pref_dialog->profiles), key); + finch_sound_set_active_profile(key); + } else + reload_pref_window(profile); +} + +static void +prof_load_cb(GntTree *tree, gpointer oldkey, gpointer newkey, gpointer null) +{ + reload_pref_window(newkey); +} + +static void +cancel_cb(GntButton *button, gpointer win) +{ + finch_sound_set_active_profile(pref_dialog->original_profile); + gnt_widget_destroy(GNT_WIDGET(win)); } -void finch_sounds_show_all(void) +void +finch_sounds_show_all(void) { + GntWidget *box, *tmpbox, *splitbox, *cmbox, *slider; + GntWidget *entry; + GntWidget *chkbox; + GntWidget *button; + GntWidget *label; + GntWidget *tree; + GntWidget *win; + + gint i; + GList *itr, *list; + + if (pref_dialog) { + gnt_window_present(pref_dialog->window); + return; + } + + pref_dialog = g_new0(SoundPrefDialog, 1); + + pref_dialog->original_profile = g_strdup(finch_sound_get_active_profile()); + + pref_dialog->window = win = gnt_window_box_new(FALSE, TRUE); + gnt_box_set_pad(GNT_BOX(win), 0); + gnt_box_set_toplevel(GNT_BOX(win), TRUE); + gnt_box_set_title(GNT_BOX(win), _("Sound Preferences")); + gnt_box_set_fill(GNT_BOX(win), TRUE); + gnt_box_set_alignment(GNT_BOX(win), GNT_ALIGN_MID); + + /* Profiles */ + splitbox = gnt_hbox_new(FALSE); + gnt_box_set_pad(GNT_BOX(splitbox), 0); + gnt_box_set_alignment(GNT_BOX(splitbox), GNT_ALIGN_TOP); + + box = gnt_vbox_new(FALSE); + gnt_box_set_pad(GNT_BOX(box), 0); + gnt_box_add_widget(GNT_BOX(box), gnt_label_new_with_format(_("Profiles"), GNT_TEXT_FLAG_BOLD)); + pref_dialog->profiles = tree = gnt_tree_new(); + gnt_tree_set_hash_fns(GNT_TREE(tree), g_str_hash, g_str_equal, g_free); + gnt_tree_set_compare_func(GNT_TREE(tree), (GCompareFunc)g_ascii_strcasecmp); + g_signal_connect(G_OBJECT(tree), "selection-changed", G_CALLBACK(prof_load_cb), NULL); + + itr = list = finch_sound_get_profiles(); + for (; itr; itr = itr->next) { + /* Do not free itr->data. It's the stored as a key for the tree, and will + * be freed when the tree is destroyed. */ + gnt_tree_add_row_after(GNT_TREE(tree), itr->data, + gnt_tree_create_row(GNT_TREE(tree), itr->data), NULL, NULL); + } + g_list_free(list); + + gnt_box_add_widget(GNT_BOX(box), tree); + + pref_dialog->new_profile = entry = gnt_entry_new(""); + gnt_box_add_widget(GNT_BOX(box), entry); + + tmpbox = gnt_hbox_new(FALSE); + button = gnt_button_new("Add"); + g_signal_connect(G_OBJECT(button), "activate", G_CALLBACK(prof_add_cb), entry); + gnt_box_add_widget(GNT_BOX(tmpbox), button); + button = gnt_button_new("Delete"); + g_signal_connect(G_OBJECT(button), "activate", G_CALLBACK(prof_del_cb), NULL); + gnt_box_add_widget(GNT_BOX(tmpbox), button); + gnt_box_add_widget(GNT_BOX(box), tmpbox); + gnt_box_add_widget(GNT_BOX(splitbox), box); + + gnt_box_add_widget(GNT_BOX(splitbox), gnt_vline_new()); + + /* Sound method */ + + box = gnt_vbox_new(FALSE); + gnt_box_set_pad(GNT_BOX(box), 0); + + pref_dialog->method = cmbox = gnt_combo_box_new(); + gnt_tree_set_hash_fns(GNT_TREE(GNT_COMBO_BOX(cmbox)->dropdown), g_str_hash, g_str_equal, NULL); + gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), "automatic", _("Automatic")); + gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), "alsa", "ALSA"); + gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), "esd", "ESD"); + gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), "beep", _("Console Beep")); + gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), "custom", _("Command")); + gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), "nosound", _("No Sound")); + + label = gnt_label_new_with_format(_("Sound Method"), GNT_TEXT_FLAG_BOLD); + gnt_box_add_widget(GNT_BOX(box), label); + tmpbox = gnt_hbox_new(TRUE); + gnt_box_set_fill(GNT_BOX(tmpbox), FALSE); + gnt_box_set_pad(GNT_BOX(tmpbox), 0); + gnt_box_add_widget(GNT_BOX(tmpbox), gnt_label_new(_("Method: "))); + gnt_box_add_widget(GNT_BOX(tmpbox), cmbox); + gnt_box_add_widget(GNT_BOX(box), tmpbox); + + tmpbox = gnt_hbox_new(TRUE); + gnt_box_set_pad(GNT_BOX(tmpbox), 0); + gnt_box_set_fill(GNT_BOX(tmpbox), FALSE); + gnt_box_add_widget(GNT_BOX(tmpbox), gnt_label_new(_("Sound Command\n(%s for filename)"))); + pref_dialog->command = entry = gnt_entry_new(""); + gnt_box_add_widget(GNT_BOX(tmpbox), entry); + gnt_box_add_widget(GNT_BOX(box), tmpbox); + + gnt_box_add_widget(GNT_BOX(box), gnt_line_new(FALSE)); + + /* Sound options */ + gnt_box_add_widget(GNT_BOX(box), gnt_label_new_with_format(_("Sound Options"), GNT_TEXT_FLAG_BOLD)); + pref_dialog->conv_focus = chkbox = gnt_check_box_new(_("Sounds when conversation has focus")); + gnt_box_add_widget(GNT_BOX(box), chkbox); + + tmpbox = gnt_hbox_new(TRUE); + gnt_box_set_pad(GNT_BOX(tmpbox), 0); + gnt_box_set_fill(GNT_BOX(tmpbox), FALSE); + gnt_box_add_widget(GNT_BOX(tmpbox), gnt_label_new("Enable Sounds:")); + pref_dialog->while_status = cmbox = gnt_combo_box_new(); + gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), GINT_TO_POINTER(3), _("Always")); + gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), GINT_TO_POINTER(1), _("Only when available")); + gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), GINT_TO_POINTER(2), _("Only when not available")); + gnt_box_add_widget(GNT_BOX(tmpbox), cmbox); + gnt_box_add_widget(GNT_BOX(box), tmpbox); + + tmpbox = gnt_hbox_new(TRUE); + gnt_box_set_pad(GNT_BOX(tmpbox), 0); + gnt_box_set_fill(GNT_BOX(tmpbox), FALSE); + gnt_box_add_widget(GNT_BOX(tmpbox), gnt_label_new(_("Volume(0-100):"))); + + pref_dialog->volume = slider = gnt_slider_new(FALSE, 100, 0); + gnt_slider_set_step(GNT_SLIDER(slider), 5); + label = gnt_label_new(""); + gnt_slider_reflect_label(GNT_SLIDER(slider), GNT_LABEL(label)); + gnt_box_set_pad(GNT_BOX(tmpbox), 1); + gnt_box_add_widget(GNT_BOX(tmpbox), slider); + gnt_box_add_widget(GNT_BOX(tmpbox), label); + gnt_box_add_widget(GNT_BOX(box), tmpbox); + gnt_box_add_widget(GNT_BOX(splitbox), box); + + gnt_box_add_widget(GNT_BOX(win), splitbox); + + gnt_box_add_widget(GNT_BOX(win), gnt_hline_new()); + + /* Sound events */ + gnt_box_add_widget(GNT_BOX(win), gnt_label_new_with_format(_("Sound Events"), GNT_TEXT_FLAG_BOLD)); + pref_dialog->events = tree = gnt_tree_new_with_columns(2); + gnt_tree_set_column_titles(GNT_TREE(tree), _("Event"), _("File")); + gnt_tree_set_show_title(GNT_TREE(tree), TRUE); + + for (i = 0; i < PURPLE_NUM_SOUNDS; i++) { + FinchSoundEvent * event = &sounds[i]; + + if (event->label == NULL) { + continue; + } + + gnt_tree_add_choice(GNT_TREE(tree), GINT_TO_POINTER(i), + gnt_tree_create_row(GNT_TREE(tree), "", ""), + NULL, NULL); + } + + gnt_tree_adjust_columns(GNT_TREE(tree)); + gnt_box_add_widget(GNT_BOX(win), tree); + + box = gnt_hbox_new(FALSE); + button = gnt_button_new(_("Test")); + g_signal_connect(G_OBJECT(button), "activate", G_CALLBACK(test_cb), NULL); + gnt_box_add_widget(GNT_BOX(box), button); + button = gnt_button_new(_("Reset")); + g_signal_connect(G_OBJECT(button), "activate", G_CALLBACK(reset_cb), NULL); + gnt_box_add_widget(GNT_BOX(box), button); + button = gnt_button_new(_("Choose...")); + g_signal_connect(G_OBJECT(button), "activate", G_CALLBACK(choose_cb), NULL); + gnt_box_add_widget(GNT_BOX(box), button); + gnt_box_add_widget(GNT_BOX(win), box); + + gnt_box_add_widget(GNT_BOX(win), gnt_line_new(FALSE)); + + /* Add new stuff before this */ + box = gnt_hbox_new(FALSE); + gnt_box_set_fill(GNT_BOX(box), TRUE); + button = gnt_button_new(_("Save")); + g_signal_connect(G_OBJECT(button), "activate", G_CALLBACK(save_cb), win); + gnt_box_add_widget(GNT_BOX(box), button); + button = gnt_button_new(_("Cancel")); + g_signal_connect(G_OBJECT(button), "activate", G_CALLBACK(cancel_cb), win); + gnt_box_add_widget(GNT_BOX(box), button); + gnt_box_add_widget(GNT_BOX(win), box); + + g_signal_connect(G_OBJECT(win), "destroy", G_CALLBACK(release_pref_dialog), NULL); + + load_pref_window(finch_sound_get_active_profile()); + + gnt_widget_show(win); +} + +static PurpleSoundUiOps sound_ui_ops = +{ + finch_sound_init, + finch_sound_uninit, + finch_sound_play_file, + finch_sound_play_event, + NULL, + NULL, + NULL, + NULL +}; + +PurpleSoundUiOps * +finch_sound_get_ui_ops(void) +{ + return &sound_ui_ops; }
--- a/finch/gntui.c Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/gntui.c Tue Aug 14 02:45:41 2007 +0000 @@ -35,6 +35,7 @@ #include "gntprefs.h" #include "gntrequest.h" #include "gntstatus.h" +#include "gntsound.h" #include <prefs.h> @@ -58,6 +59,9 @@ finch_blist_init(); purple_blist_set_ui_ops(finch_blist_get_ui_ops()); + /* Initialize sound */ + purple_sound_set_ui_ops(finch_sound_get_ui_ops()); + /* Now the conversations */ finch_conversation_init(); purple_conversations_set_ui_ops(finch_conv_get_ui_ops()); @@ -80,6 +84,7 @@ gnt_register_action(_("Debug Window"), finch_debug_window_show); gnt_register_action(_("File Transfers"), finch_xfer_dialog_show); gnt_register_action(_("Plugins"), finch_plugins_show_all); + gnt_register_action(_("Sounds"), finch_sounds_show_all); gnt_register_action(_("Preferences"), finch_prefs_show_all); gnt_register_action(_("Statuses"), finch_savedstatus_show_all);
--- a/finch/libgnt/configure.ac Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/configure.ac Tue Aug 14 02:45:41 2007 +0000 @@ -26,7 +26,7 @@ m4_define([gnt_lt_current], [1]) m4_define([gnt_major_version], [2]) -m4_define([gnt_minor_version], [0]) +m4_define([gnt_minor_version], [1]) m4_define([gnt_micro_version], [0]) m4_define([gnt_version_suffix], [devel]) m4_define([gnt_version], @@ -36,6 +36,7 @@ AC_INIT([libgnt], [gnt_display_version], [devel@pidgin.im]) AC_CANONICAL_SYSTEM AM_CONFIG_HEADER(config.h) +AC_CONFIG_AUX_DIR([.]) AM_INIT_AUTOMAKE(AC_PACKAGE_NAME, AC_PACKAGE_VERSION) GNT_MAJOR_VERSION=gnt_major_version @@ -314,6 +315,8 @@ AC_DEFINE(NO_LIBXML, 1, [Do not have libxml2.]) fi +AM_CONDITIONAL(PURPLE_AVAILABLE, false) + AC_OUTPUT([Makefile gnt.pc wms/Makefile
--- a/finch/libgnt/gnt.h Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/gnt.h Tue Aug 14 02:45:41 2007 +0000 @@ -157,4 +157,3 @@ void (*callback)(int status, gpointer data), gpointer data); gboolean gnt_is_refugee(void); -
--- a/finch/libgnt/gntbindable.c Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/gntbindable.c Tue Aug 14 02:45:41 2007 +0000 @@ -20,13 +20,182 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ +#include <string.h> + #include "gntbindable.h" #include "gntstyle.h" #include "gnt.h" #include "gntutils.h" +#include "gnttextview.h" +#include "gnttree.h" +#include "gntbox.h" +#include "gntbutton.h" +#include "gntwindow.h" +#include "gntlabel.h" static GObjectClass *parent_class = NULL; +static struct +{ + char * okeys; /* Old keystrokes */ + char * keys; /* New Keystrokes being bound to the action */ + GntBindableClass * klass; /* Class of the object that's getting keys rebound */ + char * name; /* The name of the action */ + GList * params; /* The list of paramaters */ +} rebind_info; + +static void +gnt_bindable_free_rebind_info() +{ + g_free(rebind_info.name); + g_free(rebind_info.keys); + g_free(rebind_info.okeys); +} + +static void +gnt_bindable_rebinding_cancel(GntWidget *button, gpointer data) +{ + gnt_bindable_free_rebind_info(); + gnt_widget_destroy(GNT_WIDGET(data)); +} + +static void +gnt_bindable_rebinding_rebind(GntWidget *button, gpointer data) +{ + if (rebind_info.keys) { + gnt_bindable_register_binding(rebind_info.klass, + NULL, + rebind_info.okeys, + rebind_info.params); + gnt_bindable_register_binding(rebind_info.klass, + rebind_info.name, + rebind_info.keys, + rebind_info.params); + } + gnt_bindable_free_rebind_info(); + gnt_widget_destroy(GNT_WIDGET(data)); +} + +static gboolean +gnt_bindable_rebinding_grab_key(GntBindable *bindable, const char *text, gpointer data) +{ + GntTextView *textview = GNT_TEXT_VIEW(data); + char *new_text; + const char *tmp; + + if (text && *text) { + /* Rebinding tab or enter for something is probably not that great an idea */ + if (!strcmp(text, GNT_KEY_CTRL_I) || !strcmp(text, GNT_KEY_ENTER)) { + return FALSE; + } + + tmp = gnt_key_lookup(text); + new_text = g_strdup_printf("KEY: \"%s\"", tmp); + gnt_text_view_clear(textview); + gnt_text_view_append_text_with_flags(textview, new_text, GNT_TEXT_FLAG_NORMAL); + g_free(new_text); + + g_free(rebind_info.keys); + rebind_info.keys = g_strdup(text); + + return TRUE; + } + return FALSE; +} +static void +gnt_bindable_rebinding_activate(GntBindable *data, gpointer bindable) +{ + const char *widget_name = g_type_name(G_OBJECT_TYPE(bindable)); + char *keys; + GntWidget *key_textview; + GntWidget *label; + GntWidget *bind_button, *cancel_button; + GntWidget *button_box; + GList *current_row_data; + char *tmp; + GntWidget *win = gnt_window_new(); + GntTree *tree = GNT_TREE(data); + GntWidget *vbox = gnt_box_new(FALSE, TRUE); + + rebind_info.klass = GNT_BINDABLE_GET_CLASS(bindable); + + current_row_data = gnt_tree_get_selection_text_list(tree); + rebind_info.name = g_strdup(g_list_nth_data(current_row_data, 1)); + + keys = gnt_tree_get_selection_data(tree); + rebind_info.okeys = g_strdup(gnt_key_translate(keys)); + + rebind_info.params = NULL; + + g_list_foreach(current_row_data, (GFunc)g_free, NULL); + g_list_free(current_row_data); + + gnt_box_set_alignment(GNT_BOX(vbox), GNT_ALIGN_MID); + + gnt_box_set_title(GNT_BOX(win), "Key Capture"); + + tmp = g_strdup_printf("Type the new bindings for %s in a %s.", rebind_info.name, widget_name); + label = gnt_label_new(tmp); + g_free(tmp); + gnt_box_add_widget(GNT_BOX(vbox), label); + + tmp = g_strdup_printf("KEY: \"%s\"", keys); + key_textview = gnt_text_view_new(); + gnt_widget_set_size(key_textview, key_textview->priv.x, 2); + gnt_text_view_append_text_with_flags(GNT_TEXT_VIEW(key_textview), tmp, GNT_TEXT_FLAG_NORMAL); + g_free(tmp); + gnt_widget_set_name(key_textview, "keystroke"); + gnt_box_add_widget(GNT_BOX(vbox), key_textview); + + g_signal_connect(G_OBJECT(win), "key_pressed", G_CALLBACK(gnt_bindable_rebinding_grab_key), key_textview); + + button_box = gnt_box_new(FALSE, FALSE); + + bind_button = gnt_button_new("BIND"); + gnt_widget_set_name(bind_button, "bind"); + gnt_box_add_widget(GNT_BOX(button_box), bind_button); + + cancel_button = gnt_button_new("Cancel"); + gnt_widget_set_name(cancel_button, "cancel"); + gnt_box_add_widget(GNT_BOX(button_box), cancel_button); + + g_signal_connect(G_OBJECT(bind_button), "activate", G_CALLBACK(gnt_bindable_rebinding_rebind), win); + g_signal_connect(G_OBJECT(cancel_button), "activate", G_CALLBACK(gnt_bindable_rebinding_cancel), win); + + gnt_box_add_widget(GNT_BOX(vbox), button_box); + + gnt_box_add_widget(GNT_BOX(win), vbox); + gnt_widget_show(win); +} + +typedef struct +{ + GHashTable *hash; + GntTree *tree; +} BindingView; + +static void +add_binding(gpointer key, gpointer value, gpointer data) +{ + BindingView *bv = data; + GntBindableActionParam *act = value; + const char *name = g_hash_table_lookup(bv->hash, act->action); + if (name && *name) { + const char *k = gnt_key_lookup(key); + if (!k) + k = key; + gnt_tree_add_row_after(bv->tree, (gpointer)k, + gnt_tree_create_row(bv->tree, k, name), NULL, NULL); + } +} + +static void +add_action(gpointer key, gpointer value, gpointer data) +{ + BindingView *bv = data; + g_hash_table_insert(bv->hash, value, key); +} + static void gnt_bindable_class_init(GntBindableClass *klass) { @@ -88,7 +257,7 @@ { static GType type = 0; - if(type == 0) { + if (type == 0) { static const GTypeInfo info = { sizeof(GntBindableClass), (GBaseInitFunc)duplicate_hashes, /* base_init */ @@ -184,7 +353,7 @@ action = g_hash_table_lookup(klass->actions, name); if (!action) { - g_printerr("GntWidget: Invalid action name %s for %s\n", + g_printerr("GntBindable: Invalid action name %s for %s\n", name, g_type_name(G_OBJECT_CLASS_TYPE(klass))); if (list) g_list_free(list); @@ -251,4 +420,55 @@ g_free(param); } +GntBindable * gnt_bindable_bindings_view(GntBindable *bind) +{ + GntBindable *tree = GNT_BINDABLE(gnt_tree_new_with_columns(2)); + GntBindableClass *klass = GNT_BINDABLE_CLASS(GNT_BINDABLE_GET_CLASS(bind)); + GHashTable *hash = g_hash_table_new(g_direct_hash, g_direct_equal); + BindingView bv = {hash, GNT_TREE(tree)}; + gnt_tree_set_compare_func(bv.tree, (GCompareFunc)g_utf8_collate); + g_hash_table_foreach(klass->actions, add_action, &bv); + g_hash_table_foreach(klass->bindings, add_binding, &bv); + if (GNT_TREE(tree)->list == NULL) { + gnt_widget_destroy(GNT_WIDGET(tree)); + tree = NULL; + } else + gnt_tree_adjust_columns(bv.tree); + g_hash_table_destroy(hash); + + return tree; +} + +static void +reset_binding_window(GntBindableClass *window, gpointer k) +{ + GntBindableClass *klass = GNT_BINDABLE_CLASS(k); + klass->help_window = NULL; +} + +gboolean +gnt_bindable_build_help_window(GntBindable *bindable) +{ + GntWidget *tree; + GntBindableClass *klass = GNT_BINDABLE_GET_CLASS(bindable); + char *title; + + tree = GNT_WIDGET(gnt_bindable_bindings_view(bindable)); + + klass->help_window = GNT_BINDABLE(gnt_window_new()); + title = g_strdup_printf("Bindings for %s", g_type_name(G_OBJECT_TYPE(bindable))); + gnt_box_set_title(GNT_BOX(klass->help_window), title); + if (tree) { + g_signal_connect(G_OBJECT(tree), "activate", G_CALLBACK(gnt_bindable_rebinding_activate), bindable); + gnt_box_add_widget(GNT_BOX(klass->help_window), tree); + } else + gnt_box_add_widget(GNT_BOX(klass->help_window), gnt_label_new("This widget has no customizable bindings.")); + + g_signal_connect(G_OBJECT(klass->help_window), "destroy", G_CALLBACK(reset_binding_window), klass); + gnt_widget_show(GNT_WIDGET(klass->help_window)); + g_free(title); + + return TRUE; +} +
--- a/finch/libgnt/gntbindable.h Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/gntbindable.h Tue Aug 14 02:45:41 2007 +0000 @@ -57,7 +57,8 @@ GHashTable *actions; /* name -> Action */ GHashTable *bindings; /* key -> ActionParam */ - void (*gnt_reserved1)(void); + GntBindable * help_window; + void (*gnt_reserved2)(void); void (*gnt_reserved3)(void); void (*gnt_reserved4)(void); @@ -150,6 +151,31 @@ */ gboolean gnt_bindable_perform_action_named(GntBindable *bindable, const char *name, ...); +/** +* Returns a GntTree populated with "key" -> "binding" for the widget. +*/ +/** +* +* @param widget +* +* @return +*/ +GntBindable * gnt_bindable_bindings_view(GntBindable *bind); + +/** + * + * Builds a window that list the key bindings for a GntBindable object. From this window a user can select a listing to rebind a new key for the given action. + * + */ +/** + * + * @param bindable + * + * @return + */ + +gboolean gnt_bindable_build_help_window(GntBindable *bindable); + G_END_DECLS #endif /* GNT_BINDABLE_H */
--- a/finch/libgnt/gntcombobox.h Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/gntcombobox.h Tue Aug 14 02:45:41 2007 +0000 @@ -69,7 +69,8 @@ G_BEGIN_DECLS /** - * + * + * Get the GType for GntComboBox * * @return */ @@ -77,44 +78,55 @@ /** * + * Create a new GntComboBox * - * @return + * @return A new GntComboBox */ GntWidget * gnt_combo_box_new(void); /** * - * @param box - * @param key - * @param text + * Add an entry + * + * @param box The GntComboBox + * @param key The data + * @param text The text to display */ void gnt_combo_box_add_data(GntComboBox *box, gpointer key, const char *text); /** + * + * Remove an entry * - * @param box - * @param key + * @param box The GntComboBox + * @param key The data to be removed */ void gnt_combo_box_remove(GntComboBox *box, gpointer key); /** * - * @param box + * Remove all entries + * + * @param box The GntComboBox */ void gnt_combo_box_remove_all(GntComboBox *box); /** * - * @param box + * Get the data that is currently selected * - * @return + * @param box The GntComboBox + * + * @return The data of the currently selected entry */ gpointer gnt_combo_box_get_selected_data(GntComboBox *box); /** * - * @param box - * @param key + * Set the current selection to a specific entry + * + * @param box The GntComboBox + * @param key The data to be set to */ void gnt_combo_box_set_selected(GntComboBox *box, gpointer key);
--- a/finch/libgnt/gntfilesel.c Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/gntfilesel.c Tue Aug 14 02:45:41 2007 +0000 @@ -355,6 +355,7 @@ } else if (strcmp(str, "..") == 0) { gnt_tree_set_selected(tree, dir); } + gnt_bindable_perform_action_named(GNT_BINDABLE(tree), "end-search", NULL); g_free(dir); g_free(str); g_free(path); @@ -495,10 +496,11 @@ if (!sel->multiselect) return FALSE; tree = sel->dirsonly ? sel->dirs : sel->files; - if (!gnt_widget_has_focus(tree)) + if (!gnt_widget_has_focus(tree) || + gnt_tree_is_searching(GNT_TREE(tree))) return FALSE; - file = gnt_tree_get_selection_data(sel->dirsonly ? GNT_TREE(sel->dirs) : GNT_TREE(sel->files)); + file = gnt_tree_get_selection_data(GNT_TREE(tree)); str = gnt_file_sel_get_selected_file(sel); if ((find = g_list_find_custom(sel->tags, str, (GCompareFunc)g_utf8_collate)) != NULL) { @@ -526,7 +528,8 @@ if (!sel->multiselect) return FALSE; tree = sel->dirsonly ? sel->dirs : sel->files; - if (!gnt_widget_has_focus(tree)) + if (!gnt_widget_has_focus(tree) || + gnt_tree_is_searching(GNT_TREE(tree))) return FALSE; g_list_foreach(sel->tags, (GFunc)g_free, NULL); @@ -547,6 +550,9 @@ if (!gnt_widget_has_focus(sel->dirs) && !gnt_widget_has_focus(sel->files)) return FALSE; + if (gnt_tree_is_searching(GNT_TREE(sel->dirs)) || + gnt_tree_is_searching(GNT_TREE(sel->files))) + return FALSE; path = g_build_filename(sel->current, "..", NULL); dir = g_path_get_basename(sel->current);
--- a/finch/libgnt/gntline.c Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/gntline.c Tue Aug 14 02:45:41 2007 +0000 @@ -24,6 +24,12 @@ enum { + PROP_0, + PROP_VERTICAL +}; + +enum +{ SIGS = 1, }; @@ -65,14 +71,57 @@ } static void +gnt_line_set_property(GObject *obj, guint prop_id, const GValue *value, + GParamSpec *spec) +{ + GntLine *line = GNT_LINE(obj); + switch (prop_id) { + case PROP_VERTICAL: + line->vertical = g_value_get_boolean(value); + if (line->vertical) { + GNT_WIDGET_SET_FLAGS(line, GNT_WIDGET_GROW_Y); + } else { + GNT_WIDGET_SET_FLAGS(line, GNT_WIDGET_GROW_X); + } + break; + default: + break; + } +} + +static void +gnt_line_get_property(GObject *obj, guint prop_id, GValue *value, + GParamSpec *spec) +{ + GntLine *line = GNT_LINE(obj); + switch (prop_id) { + case PROP_VERTICAL: + g_value_set_boolean(value, line->vertical); + break; + default: + break; + } +} + +static void gnt_line_class_init(GntLineClass *klass) { + GObjectClass *gclass = G_OBJECT_CLASS(klass); parent_class = GNT_WIDGET_CLASS(klass); parent_class->draw = gnt_line_draw; parent_class->map = gnt_line_map; parent_class->size_request = gnt_line_size_request; - GNTDEBUG; + gclass->set_property = gnt_line_set_property; + gclass->get_property = gnt_line_get_property; + g_object_class_install_property(gclass, + PROP_VERTICAL, + g_param_spec_boolean("vertical", "Vertical", + "Whether it's a vertical line or a horizontal one.", + TRUE, + G_PARAM_READWRITE|G_PARAM_STATIC_NAME|G_PARAM_STATIC_NICK|G_PARAM_STATIC_BLURB + ) + ); } static void @@ -118,20 +167,7 @@ GntWidget *gnt_line_new(gboolean vertical) { - GntWidget *widget = g_object_new(GNT_TYPE_LINE, NULL); - GntLine *line = GNT_LINE(widget); - - line->vertical = vertical; - - if (vertical) - { - GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_GROW_Y); - } - else - { - GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_GROW_X); - } - + GntWidget *widget = g_object_new(GNT_TYPE_LINE, "vertical", vertical, NULL); return widget; }
--- a/finch/libgnt/gntmain.c Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/gntmain.c Tue Aug 14 02:45:41 2007 +0000 @@ -252,6 +252,18 @@ if (HOLDING_ESCAPE) keys[0] = '\033'; k = keys; + +#if 0 + /* I am not sure what's happening here. If this actually does something, + * then this needs to go in gnt_keys_refine. */ + if (*k < 0) { /* Alt not sending ESC* */ + *(k + 1) = 128 - *k; + *k = 27; + *(k + 2) = 0; + rd++; + } +#endif + while (rd) { char back; int p; @@ -517,7 +529,8 @@ void gnt_screen_release(GntWidget *widget) { - gnt_wm_window_close(wm, widget); + if (wm) + gnt_wm_window_close(wm, widget); } void gnt_screen_update(GntWidget *widget) @@ -564,7 +577,9 @@ void gnt_quit() { - g_hash_table_destroy(wm->nodes); /* XXX: */ + g_object_unref(G_OBJECT(wm)); + wm = NULL; + update_panels(); doupdate(); gnt_uninit_colors();
--- a/finch/libgnt/gntmenu.c Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/gntmenu.c Tue Aug 14 02:45:41 2007 +0000 @@ -247,11 +247,14 @@ int current = menu->selected; if (menu->submenu) { - do menu = menu->submenu; while (menu->submenu); - return (gnt_widget_key_pressed(GNT_WIDGET(menu), text)); + GntMenu *sub = menu; + do sub = sub->submenu; while (sub->submenu); + if (gnt_widget_key_pressed(GNT_WIDGET(sub), text)) + return TRUE; } - if (text[0] == 27 && text[1] == 0) { + if ((text[0] == 27 && text[1] == 0) || + (menu->type != GNT_MENU_TOPLEVEL && strcmp(text, GNT_KEY_LEFT) == 0)) { /* Escape closes menu */ GntMenu *par = menu->parentmenu; if (par != NULL) { @@ -271,11 +274,17 @@ menu->selected++; if (menu->selected >= g_list_length(menu->list)) menu->selected = 0; - } else if (strcmp(text, GNT_KEY_ENTER) == 0) { + } else if (strcmp(text, GNT_KEY_ENTER) == 0 || + strcmp(text, GNT_KEY_DOWN) == 0) { gnt_widget_activate(widget); } if (current != menu->selected) { + GntMenu *sub = menu->submenu; + while (sub) { + gnt_widget_hide(GNT_WIDGET(sub)); + sub = sub->submenu; + } gnt_widget_draw(widget); return TRUE; } @@ -283,6 +292,12 @@ if (text[1] == '\0') { if (check_for_trigger(menu, text[0])) return TRUE; + } else if (strcmp(text, GNT_KEY_RIGHT) == 0) { + GntMenuItem *item = gnt_tree_get_selection_data(GNT_TREE(menu)); + if (item && item->submenu) { + menuitem_activate(menu, item); + return TRUE; + } } return org_key_pressed(widget, text); }
--- a/finch/libgnt/gnttextview.c Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/gnttextview.c Tue Aug 14 02:45:41 2007 +0000 @@ -68,17 +68,31 @@ int i = 0; GList *lines; int rows, scrcol; + int comp = 0; /* Used for top-aligned text */ gboolean has_scroll = !(view->flags & GNT_TEXT_VIEW_NO_SCROLL); wbkgd(widget->window, COLOR_PAIR(GNT_COLOR_NORMAL)); werase(widget->window); + if ((view->flags & GNT_TEXT_VIEW_TOP_ALIGN) && + g_list_length(view->list) < widget->priv.height) { + GList *now = view->list; + comp = widget->priv.height - g_list_length(view->list); + view->list = g_list_nth_prev(view->list, comp); + if (!view->list) { + view->list = g_list_first(now); + comp = widget->priv.height - g_list_length(view->list); + } else { + comp = 0; + } + } + for (i = 0, lines = view->list; i < widget->priv.height && lines; i++, lines = lines->next) { GList *iter; GntTextLine *line = lines->data; - wmove(widget->window, widget->priv.height - 1 - i, 0); + wmove(widget->window, widget->priv.height - 1 - i - comp, 0); for (iter = line->segments; iter; iter = iter->next) { @@ -398,7 +412,7 @@ static void gnt_text_view_size_changed(GntWidget *widget, int w, int h) { - if (w != widget->priv.width) { + if (w != widget->priv.width && GNT_WIDGET_IS_FLAG_SET(widget, GNT_WIDGET_MAPPED)) { gnt_text_view_reflow(GNT_TEXT_VIEW(widget)); } } @@ -422,11 +436,16 @@ gnt_text_view_init(GTypeInstance *instance, gpointer class) { GntWidget *widget = GNT_WIDGET(instance); - - GNT_WIDGET_SET_FLAGS(GNT_WIDGET(instance), GNT_WIDGET_GROW_Y | GNT_WIDGET_GROW_X); + GntTextView *view = GNT_TEXT_VIEW(widget); + GntTextLine *line = g_new0(GntTextLine, 1); + GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_NO_BORDER | GNT_WIDGET_NO_SHADOW | + GNT_WIDGET_GROW_Y | GNT_WIDGET_GROW_X); widget->priv.minw = 5; widget->priv.minh = 2; + view->string = g_string_new(NULL); + view->list = g_list_append(view->list, line); + GNTDEBUG; } @@ -464,13 +483,6 @@ GntWidget *gnt_text_view_new() { GntWidget *widget = g_object_new(GNT_TYPE_TEXT_VIEW, NULL); - GntTextView *view = GNT_TEXT_VIEW(widget); - GntTextLine *line = g_new0(GntTextLine, 1); - - GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_NO_BORDER | GNT_WIDGET_NO_SHADOW); - - view->string = g_string_new(NULL); - view->list = g_list_append(view->list, line); return widget; }
--- a/finch/libgnt/gnttextview.h Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/gnttextview.h Tue Aug 14 02:45:41 2007 +0000 @@ -47,9 +47,11 @@ typedef struct _GntTextViewPriv GntTextViewPriv; typedef struct _GntTextViewClass GntTextViewClass; -typedef enum _GntTextViewFlag { +typedef enum +{ GNT_TEXT_VIEW_NO_SCROLL = 1 << 0, GNT_TEXT_VIEW_WRAP_CHAR = 1 << 1, + GNT_TEXT_VIEW_TOP_ALIGN = 1 << 2, } GntTextViewFlag; struct _GntTextView
--- a/finch/libgnt/gnttree.c Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/gnttree.c Tue Aug 14 02:45:41 2007 +0000 @@ -450,7 +450,7 @@ if (COLUMN_INVISIBLE(tree, i)) { continue; } - mvwaddstr(widget->window, pos, x + 1, tree->columns[i].title); + mvwaddnstr(widget->window, pos, x + (x != pos), tree->columns[i].title, tree->columns[i].width); NEXT_X; } if (pos) @@ -768,6 +768,7 @@ g_string_free(tree->priv->search, TRUE); tree->priv->search = NULL; tree->priv->search_timeout = 0; + GNT_WIDGET_UNSET_FLAGS(GNT_WIDGET(tree), GNT_WIDGET_DISABLE_ACTIONS); } } @@ -1053,7 +1054,9 @@ GntTree *tree = GNT_TREE(widget); tree->show_separator = TRUE; tree->priv = g_new0(GntTreePriv, 1); - GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_GROW_X | GNT_WIDGET_GROW_Y | GNT_WIDGET_CAN_TAKE_FOCUS); + GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_GROW_X | GNT_WIDGET_GROW_Y | + GNT_WIDGET_CAN_TAKE_FOCUS | GNT_WIDGET_NO_SHADOW); + gnt_widget_set_take_focus(widget, TRUE); widget->priv.minw = 4; widget->priv.minh = 1; GNTDEBUG; @@ -1605,9 +1608,6 @@ "columns", col, NULL); - GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_NO_SHADOW); - gnt_widget_set_take_focus(widget, TRUE); - return widget; }
--- a/finch/libgnt/gntutils.h Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/gntutils.h Tue Aug 14 02:45:41 2007 +0000 @@ -40,6 +40,7 @@ */ void gnt_util_get_text_bound(const char *text, int *width, int *height); +/* excluding *end */ /** * Get the onscreen width of a string, or a substring. *
--- a/finch/libgnt/gntwm.c Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/gntwm.c Tue Aug 14 02:45:41 2007 +0000 @@ -39,6 +39,8 @@ #include "gntmarshal.h" #include "gnt.h" #include "gntbox.h" +#include "gntbutton.h" +#include "gntentry.h" #include "gntlabel.h" #include "gntmenu.h" #include "gnttextview.h" @@ -84,6 +86,7 @@ static time_t last_active_time; static gboolean idle_update; static GList *act = NULL; /* list of WS with unseen activitiy */ +static gboolean ignore_keys = FALSE; static GList * g_list_bring_to_front(GList *list, gpointer data) @@ -485,35 +488,6 @@ return TRUE; } -static gboolean -help_for_widget(GntBindable *bindable, GList *null) -{ - GntWM *wm = GNT_WM(bindable); - GntWidget *widget, *tree, *win, *active; - char *title; - - if (!wm->cws->ordered) - return TRUE; - - widget = wm->cws->ordered->data; - if (!GNT_IS_BOX(widget)) - return TRUE; - active = GNT_BOX(widget)->active; - - tree = gnt_widget_bindings_view(active); - win = gnt_window_new(); - title = g_strdup_printf("Bindings for %s", g_type_name(G_OBJECT_TYPE(active))); - gnt_box_set_title(GNT_BOX(win), title); - if (tree) - gnt_box_add_widget(GNT_BOX(win), tree); - else - gnt_box_add_widget(GNT_BOX(win), gnt_label_new("This widget has no customizable bindings.")); - - gnt_widget_show(win); - - return TRUE; -} - static void destroy__list(GntWidget *widget, GntWM *wm) { @@ -684,6 +658,8 @@ {'j', "┘"}, {'a', "▒"}, {'n', "┼"}, + {'w', "┬"}, + {'v', "┴"}, {'\0', NULL} }; @@ -841,6 +817,7 @@ shift_right(GntBindable *bindable, GList *null) { GntWM *wm = GNT_WM(bindable); + if (wm->_list.window) return TRUE; @@ -1136,10 +1113,106 @@ return TRUE; } +static gboolean +ignore_keys_start(GntBindable *bindable, GList *n) +{ + GntWM *wm = GNT_WM(bindable); + + if(!wm->menu && !wm->_list.window && wm->mode == GNT_KP_MODE_NORMAL){ + ignore_keys = TRUE; + return TRUE; + } + return FALSE; +} + +static gboolean +ignore_keys_end(GntBindable *bindable, GList *n) +{ + return ignore_keys ? !(ignore_keys = FALSE) : FALSE; +} + +static gboolean +help_for_bindable(GntWM *wm, GntBindable *bindable) +{ + gboolean ret = TRUE; + GntBindableClass *klass = GNT_BINDABLE_GET_CLASS(bindable); + + if (klass->help_window) { + gnt_wm_raise_window(wm, GNT_WIDGET(klass->help_window)); + } else { + ret = gnt_bindable_build_help_window(bindable); + } + return ret; +} + +static gboolean +help_for_wm(GntBindable *bindable, GList *null) +{ + return help_for_bindable(GNT_WM(bindable),bindable); +} + +static gboolean +help_for_window(GntBindable *bindable, GList *null) +{ + GntWM *wm = GNT_WM(bindable); + GntWidget *widget; + + if(!wm->cws->ordered) + return FALSE; + + widget = wm->cws->ordered->data; + + return help_for_bindable(wm,GNT_BINDABLE(widget)); +} + +static gboolean +help_for_widget(GntBindable *bindable, GList *null) +{ + GntWM *wm = GNT_WM(bindable); + GntWidget *widget; + + if (!wm->cws->ordered) + return TRUE; + + widget = wm->cws->ordered->data; + if (!GNT_IS_BOX(widget)) + return TRUE; + + return help_for_bindable(wm, GNT_BINDABLE(GNT_BOX(widget)->active)); +} + +static void +accumulate_windows(gpointer window, gpointer node, gpointer p) +{ + GList *list = *(GList**)p; + list = g_list_prepend(list, window); + *(GList**)p = list; +} + +static void +gnt_wm_destroy(GObject *obj) +{ + GntWM *wm = GNT_WM(obj); + GList *list = NULL; + g_hash_table_foreach(wm->nodes, accumulate_windows, &list); + g_list_foreach(list, (GFunc)gnt_widget_destroy, NULL); + g_list_free(list); + g_hash_table_destroy(wm->nodes); + wm->nodes = NULL; + + while (wm->workspaces) { + g_object_unref(wm->workspaces->data); + wm->workspaces = g_list_delete_link(wm->workspaces, wm->workspaces); + } +} + static void gnt_wm_class_init(GntWMClass *klass) { int i; + GObjectClass *gclass = G_OBJECT_CLASS(klass); + + gclass->dispose = gnt_wm_destroy; klass->new_window = gnt_wm_new_window_real; klass->decorate_window = NULL; @@ -1291,8 +1364,16 @@ "\033" "T", NULL); gnt_bindable_class_register_action(GNT_BINDABLE_CLASS(klass), "workspace-list", workspace_list, "\033" "s", NULL); - gnt_bindable_class_register_action(GNT_BINDABLE_CLASS(klass), "toggle-clipboard", - toggle_clipboard, "\033" "C", NULL); + gnt_bindable_class_register_action(GNT_BINDABLE_CLASS(klass), "toggle-clipboard", toggle_clipboard, + "\033" "C", NULL); + gnt_bindable_class_register_action(GNT_BINDABLE_CLASS(klass), "help-for-wm", help_for_wm, + "\033" "\\", NULL); + gnt_bindable_class_register_action(GNT_BINDABLE_CLASS(klass), "help-for-window", help_for_window, + "\033" "|", NULL); + gnt_bindable_class_register_action(GNT_BINDABLE_CLASS(klass), "ignore-keys-start", ignore_keys_start, + GNT_KEY_CTRL_G, NULL); + gnt_bindable_class_register_action(GNT_BINDABLE_CLASS(klass), "ignore-keys-end", ignore_keys_end, + "\033" GNT_KEY_CTRL_G, NULL); gnt_style_read_actions(G_OBJECT_CLASS_TYPE(klass), GNT_BINDABLE_CLASS(klass)); @@ -1679,6 +1760,14 @@ keys = gnt_bindable_remap_keys(GNT_BINDABLE(wm), keys); idle_update = TRUE; + if(ignore_keys){ + if(keys && !strcmp(keys, "\033" GNT_KEY_CTRL_G)){ + if(gnt_bindable_perform_action_key(GNT_BINDABLE(wm), keys)){ + return TRUE; + } + } + return wm->cws->ordered ? gnt_widget_key_pressed(GNT_WIDGET(wm->cws->ordered->data), keys) : FALSE; + } if (gnt_bindable_perform_action_key(GNT_BINDABLE(wm), keys)) { return TRUE; @@ -1897,7 +1986,7 @@ if (!node) return; - + if (widget != wm->_list.window && !GNT_IS_MENU(widget) && wm->cws->ordered->data != widget) { GntWidget *w = wm->cws->ordered->data; @@ -1978,3 +2067,4 @@ wm->event_stack = set; } +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/finch/libgnt/pygnt/Makefile.am Tue Aug 14 02:45:41 2007 +0000 @@ -0,0 +1,40 @@ +EXTRA_DIST = gendef.sh + +pg_LTLIBRARIES = gnt.la + +pgdir = $(libdir) + +sources = \ + gnt.def \ + gnt.override \ + gntbox.override \ + gntfilesel.override \ + gnttree.override \ + gntwidget.override + +gnt_la_SOURCES = gnt.c common.c common.h gntmodule.c + +gnt_la_LDFLAGS = -module -avoid-version \ + `pkg-config --libs pygobject-2.0` + +gnt_la_LIBADD = \ + $(GLIB_LIBS) \ + ../libgnt.la + +AM_CPPFLAGS = \ + -I../ \ + $(GLIB_CFLAGS) \ + $(GNT_CFLAGS) \ + -I/usr/include/python2.4 \ + `pkg-config --cflags pygobject-2.0` + +CLEANFILES = gnt.def gnt.c gnt.defe + +gnt.def: $(srcdir)/../*.h + $(srcdir)/gendef.sh + +gnt.c: $(sources) + pygtk-codegen-2.0 --prefix gnt \ + --override gnt.override \ + gnt.def > $@ +
--- a/finch/libgnt/pygnt/Makefile.make Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/pygnt/Makefile.make Tue Aug 14 02:45:41 2007 +0000 @@ -10,5 +10,7 @@ --override gnt.override \ gnt.def > $@ +#python codegen/codegen.py --prefix gnt \ + clean: @rm *.so *.o gnt.c
--- a/finch/libgnt/pygnt/dbus-gnt Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/pygnt/dbus-gnt Tue Aug 14 02:45:41 2007 +0000 @@ -17,13 +17,14 @@ convwins = {} -def buddysignedon(): +def buddysignedon(buddy): pass def conv_closed(conv): key = get_dict_key(conv) stuff = convwins[key] stuff[0].destroy() + # if a conv window is closed, then reopened, this thing crashes convwins[key] = None def wrote_msg(account, who, msg, conv, flags): @@ -31,10 +32,13 @@ tv = stuff[1] tv.append_text_with_flags("\n", 0) tv.append_text_with_flags(strftime("(%X) "), 8) - tv.append_text_with_flags(who + ": ", 1) - tv.append_text_with_flags(msg, 0) + if flags & 3: + tv.append_text_with_flags(who + ": ", 1) + tv.append_text_with_flags(msg, 0) + stuff[0].set_urgent() + else: + tv.append_text_with_flags(msg, 8) tv.scroll(0) - stuff[0].set_urgent() bus = dbus.SessionBus() obj = bus.get_object("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject") @@ -72,6 +76,9 @@ purple.PurpleConvChatSend(chatdata, entry.get_text()) entry.clear() +def conv_window_destroyed(win, key): + del convwins[key] + def show_conversation(conv): key = get_dict_key(conv) if key in convwins: @@ -91,27 +98,28 @@ tv.clear() win.show() convwins[key] = [win, tv, entry] + win.connect("destroy", conv_window_destroyed, key) return convwins[key] def show_buddylist(): - win = gnt.Window() - tree = gnt.Tree() - tree.set_property("columns", 1) - win.add_widget(tree) - node = purple.PurpleBlistGetRoot() - while node: - if purple.PurpleBlistNodeIsGroup(node): - sys.stderr.write(str(node) + "\n") - tree.add_row_after(str(node), ["asd", ""], None, None) - #tree.add_row_after(node, [str(purple.PurpleGroupGetName(node)), ""], None, None) - #tree.add_row_after(node, ["aasd", ""], None, None) - elif purple.PurpleBlistNodeIsContact(node): - buddy = purple.PurpleContactGetPriorityBuddy(node) - group = purple.PurpleBuddyGetGroup(buddy) - #tree.add_row_after(node, [str(purple.PurpleBuddyGetName(buddy)), ""], group, None) + win = gnt.Window() + tree = gnt.Tree() + tree.set_property("columns", 1) + win.add_widget(tree) + node = purple.PurpleBlistGetRoot() + while node: + if purple.PurpleBlistNodeIsGroup(node): + sys.stderr.write(str(node) + "\n") + tree.add_row_after(str(node), ["asd", ""], None, None) + #tree.add_row_after(node, [str(purple.PurpleGroupGetName(node)), ""], None, None) + #tree.add_row_after(node, ["aasd", ""], None, None) + elif purple.PurpleBlistNodeIsContact(node): + buddy = purple.PurpleContactGetPriorityBuddy(node) + group = purple.PurpleBuddyGetGroup(buddy) + #tree.add_row_after(node, [str(purple.PurpleBuddyGetName(buddy)), ""], group, None) - node = purple.PurpleBlistNodeNext(node, False) - win.show() + node = purple.PurpleBlistNodeNext(node, False) + win.show() gnt.gnt_init()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/finch/libgnt/pygnt/example/rss/gnthtml.py Tue Aug 14 02:45:41 2007 +0000 @@ -0,0 +1,87 @@ +#!/usr/bin/env python + +""" +gr - An RSS-reader built using libgnt and feedparser. + +Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@pidgin.im> + +This application is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This application is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this application; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +USA +""" + +""" +This file defines GParser, which is a simple HTML parser to display HTML +in a GntTextView nicely. +""" + +import sgmllib +import gnt + +class GParser(sgmllib.SGMLParser): + def __init__(self, view): + sgmllib.SGMLParser.__init__(self, False) + self.link = None + self.view = view + self.flag = gnt.TEXT_FLAG_NORMAL + + def parse(self, s): + self.feed(s) + self.close() + + def unknown_starttag(self, tag, attrs): + if tag in ["b", "i", "blockquote", "strong"]: + self.flag = self.flag | gnt.TEXT_FLAG_BOLD + elif tag in ["p", "hr", "br"]: + self.view.append_text_with_flags("\n", self.flag) + else: + print tag + + def unknown_endtag(self, tag): + if tag in ["b", "i", "blockquote", "strong"]: + self.flag = self.flag & ~gnt.TEXT_FLAG_BOLD + elif tag in ["p", "hr", "br"]: + self.view.append_text_with_flags("\n", self.flag) + else: + print tag + + def start_u(self, attrs): + self.flag = self.flag | gnt.TEXT_FLAG_UNDERLINE + + def end_u(self): + self.flag = self.flag & ~gnt.TEXT_FLAG_UNDERLINE + + def start_a(self, attributes): + for name, value in attributes: + if name == "href": + self.link = value + + def do_img(self, attrs): + for name, value in attrs: + if name == 'src': + self.view.append_text_with_flags("[img:" + value + "]", self.flag) + + def end_a(self): + if not self.link: + return + self.view.append_text_with_flags(" (", self.flag) + self.view.append_text_with_flags(self.link, self.flag | gnt.TEXT_FLAG_UNDERLINE) + self.view.append_text_with_flags(")", self.flag) + self.link = None + + def handle_data(self, data): + if len(data.strip()) == 0: + return + self.view.append_text_with_flags(data, self.flag) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/finch/libgnt/pygnt/example/rss/gntrss-ui.py Tue Aug 14 02:45:41 2007 +0000 @@ -0,0 +1,400 @@ +#!/usr/bin/env python + +""" +gr - An RSS-reader built using libgnt and feedparser. + +Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@pidgin.im> + +This application is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This application is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this application; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +USA +""" + +""" +This file deals with the UI part (gnt) of the application + +TODO: + - Allow showing feeds of only selected 'category' and/or 'priority'. A different + window should be used to change such filtering. + - Display details of each item in its own window. + - Add search capability, and allow searching only in title/body. Also allow + filtering in the search results. + - Show the data and time for feed items (probably in a separate column .. perhaps not) + - Have a simple way to add a feed. + - Allow renaming a feed. +""" + +import gntrss +import gnthtml +import gnt +import gobject +import sys + +__version__ = "0.0.1alpha" +__author__ = "Sadrul Habib Chowdhury (sadrul@pidgin.im)" +__copyright__ = "Copyright 2007, Sadrul Habib Chowdhury" +__license__ = "GPL" # see full license statement above + +gnt.gnt_init() + +class RssTree(gnt.Tree): + __gsignals__ = { + 'active_changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,)) + } + + __gntbindings__ = { + 'jump-next-unread' : ('jump_next_unread', 'J') + } + + def jump_next_unread(self, null): + first = None + next = None + all = self.get_rows() + for item in all: + if item.unread: + if next: + first = item + break + elif not first and self.active != item: + first = item + if self.active == item: + next = item + if first: + self.set_active(first) + self.set_selected(first) + return True + + def __init__(self): + self.active = None + gnt.Tree.__init__(self) + gnt.set_flag(self, 8) # remove borders + self.connect('key_pressed', self.do_key_pressed) + + def set_active(self, active): + if self.active == active: + return + if self.active: + flag = gnt.TEXT_FLAG_NORMAL + if self.active.unread: + flag = flag | gnt.TEXT_FLAG_BOLD + self.set_row_flags(self.active, flag) + old = self.active + self.active = active + flag = gnt.TEXT_FLAG_UNDERLINE + if self.active.unread: + flag = flag | gnt.TEXT_FLAG_BOLD + self.set_row_flags(self.active, flag) + self.emit('active_changed', old) + + def do_key_pressed(self, null, text): + if text == '\r': + now = self.get_selection_data() + self.set_active(now) + return True + return False + +gobject.type_register(RssTree) +gnt.register_bindings(RssTree) + +win = gnt.Box(homo = False, vert = True) +win.set_toplevel(True) +win.set_title("GntRss") +win.set_pad(0) + +# +# [[[ Generic feed/item callbacks +# +def feed_item_added(feed, item): + add_feed_item(item) + +def add_feed(feed): + if not feed.get_data('gntrss-connected'): + feed.connect('added', feed_item_added) + feed.connect('notify', update_feed_title) + feed.set_data('gntrss-connected', True) + feeds.add_row_after(feed, [feed.title, str(feed.unread)], None, None) + +def remove_item(item, feed): + items.remove(item) + +def update_feed_item(item, property): + if property.name == 'unread': + if feeds.active == item.parent: + flag = 0 + if item == items.active: + flag = gnt.TEXT_FLAG_UNDERLINE + if item.unread: + flag = flag | gnt.TEXT_FLAG_BOLD + else: + flag = flag | gnt.TEXT_FLAG_NORMAL + items.set_row_flags(item, flag) + + unread = item.parent.unread + if item.unread: + unread = unread + 1 + else: + unread = unread - 1 + item.parent.set_property('unread', unread) + +def add_feed_item(item): + currentfeed = feeds.active + if item.parent != currentfeed: + return + months = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + dt = str(item.date_parsed[2]) + "." + months[item.date_parsed[1]] + "." + str(item.date_parsed[0]) + items.add_row_after(item, [str(item.title), dt], None, None) + if item.unread: + items.set_row_flags(item, gnt.TEXT_FLAG_BOLD) + if not item.get_data('gntrss-connected'): + item.set_data('gntrss-connected', True) + # this needs to happen *without* having to add the item in the tree + item.connect('notify', update_feed_item) + item.connect('delete', remove_item) + +# +# ]]] Generic feed/item callbacks +# + + +#### +# [[[ The list of feeds +### + +# 'Add Feed' dialog +add_feed_win = None +def add_feed_win_closed(win): + global add_feed_win + add_feed_win = None + +def add_new_feed(): + global add_feed_win + + if add_feed_win: + gnt.gnt_window_present(add_feed_win) + return + win = gnt.Window() + win.set_title("New Feed") + + box = gnt.Box(False, False) + label = gnt.Label("Link") + box.add_widget(label) + entry = gnt.Entry("") + entry.set_size(40, 1) + box.add_widget(entry) + + win.add_widget(box) + win.show() + add_feed_win = win + add_feed_win.connect("destroy", add_feed_win_closed) + +# +# The active row in the feed-list has changed. Update the feed-item table. +def feed_active_changed(tree, old): + items.remove_all() + if not tree.active: + return + update_items_title() + for item in tree.active.items: + add_feed_item(item) + win.give_focus_to_child(items) + +# +# Check for the action keys and decide how to deal with them. +def feed_key_pressed(tree, text): + if tree.is_searching(): + return + if text == 'r': + feed = tree.get_selection_data() + tree.perform_action_key('j') + #tree.perform_action('move-down') + feed.refresh() + elif text == 'R': + feeds = tree.get_rows() + for feed in feeds: + feed.refresh() + elif text == 'm': + feed = tree.get_selection_data() + if feed: + feed.mark_read() + feed.set_property('unread', 0) + elif text == 'a': + add_new_feed() + else: + return False + return True + +feeds = RssTree() +feeds.set_property('columns', 2) +feeds.set_col_width(0, 20) +feeds.set_col_width(1, 6) +feeds.set_column_resizable(0, False) +feeds.set_column_resizable(1, False) +feeds.set_column_is_right_aligned(1, True) +feeds.set_show_separator(False) +feeds.set_column_title(0, "Feeds") +feeds.set_show_title(True) + +feeds.connect('active_changed', feed_active_changed) +feeds.connect('key_pressed', feed_key_pressed) +gnt.unset_flag(feeds, 256) # Fix the width + +#### +# ]]] The list of feeds +### + +#### +# [[[ The list of items in the feed +#### + +# +# The active item in the feed-item list has changed. Update the +# summary content. +def item_active_changed(tree, old): + details.clear() + if not tree.active: + return + item = tree.active + details.append_text_with_flags(str(item.title) + "\n", gnt.TEXT_FLAG_BOLD) + details.append_text_with_flags("Link: ", gnt.TEXT_FLAG_BOLD) + details.append_text_with_flags(str(item.link) + "\n", gnt.TEXT_FLAG_UNDERLINE) + details.append_text_with_flags("Date: ", gnt.TEXT_FLAG_BOLD) + details.append_text_with_flags(str(item.date) + "\n", gnt.TEXT_FLAG_NORMAL) + details.append_text_with_flags("\n", gnt.TEXT_FLAG_NORMAL) + parser = gnthtml.GParser(details) + parser.parse(str(item.summary)) + item.mark_unread(False) + + if old and old.unread: # If the last selected item is marked 'unread', then make sure it's bold + items.set_row_flags(old, gnt.TEXT_FLAG_BOLD) + +# +# Look for action keys in the feed-item list. +def item_key_pressed(tree, text): + if tree.is_searching(): + return + current = tree.get_selection_data() + if text == 'M': # Mark all of the items 'read' + feed = feeds.active + if feed: + feed.mark_read() + elif text == 'm': # Mark the current item 'read' + current.mark_unread(False) + tree.perform_action_key('j') + elif text == 'U': # Mark the current item 'unread' + current.mark_unread(True) + elif text == 'd': + current.remove() + tree.perform_action_key('j') + else: + return False + return True + +items = RssTree() +items.set_property('columns', 2) +items.set_col_width(0, 40) +items.set_col_width(1, 11) +items.set_column_resizable(1, False) +items.set_column_title(0, "Items") +items.set_column_title(1, "Date") +items.set_show_title(True) +items.connect('key_pressed', item_key_pressed) +items.connect('active_changed', item_active_changed) + +#### +# ]]] The list of items in the feed +#### + +# +# Update the title of the items list depending on the selection in the feed list +def update_items_title(): + feed = feeds.active + if feed: + items.set_column_title(0, str(feed.title) + ": " + str(feed.unread) + "(" + str(len(feed.items)) + ")") + else: + items.set_column_title(0, "Items") + items.draw() + +# The container on the top +line = gnt.Line(vertical = False) + +# The textview to show the details of a feed +details = gnt.TextView() +details.set_take_focus(True) +details.set_flag(gnt.TEXT_VIEW_TOP_ALIGN) +details.attach_scroll_widget(details) + +# Make it look nice +s = feeds.get_size() +size = gnt.screen_size() +size[0] = size[0] - s[0] +items.set_size(size[0], size[1] / 2) +details.set_size(size[0], size[1] / 2) + +# Category tree +cat = gnt.Tree() +cat.set_property('columns', 1) +cat.set_column_title(0, 'Category') +cat.set_show_title(True) +gnt.set_flag(cat, 8) # remove borders + +box = gnt.Box(homo = False, vert = False) +box.set_pad(0) + +vbox = gnt.Box(homo = False, vert = True) +vbox.set_pad(0) +vbox.add_widget(feeds) +vbox.add_widget(gnt.Line(False)) +vbox.add_widget(cat) +box.add_widget(vbox) + +box.add_widget(gnt.Line(True)) + +vbox = gnt.Box(homo = False, vert = True) +vbox.set_pad(0) +vbox.add_widget(items) +vbox.add_widget(gnt.Line(False)) +vbox.add_widget(details) +box.add_widget(vbox) + +win.add_widget(box) +win.show() + +def update_feed_title(feed, property): + if property.name == 'title': + if feed.customtitle: + title = feed.customtitle + else: + title = feed.title + feeds.change_text(feed, 0, title) + elif property.name == 'unread': + feeds.change_text(feed, 1, str(feed.unread) + "(" + str(len(feed.items)) + ")") + flag = 0 + if feeds.active == feed: + flag = gnt.TEXT_FLAG_UNDERLINE + update_items_title() + if feed.unread > 0: + flag = flag | gnt.TEXT_FLAG_BOLD + feeds.set_row_flags(feed, flag) + +# populate everything +for feed in gntrss.feeds: + feed.refresh() + feed.set_auto_refresh(True) + add_feed(feed) + +gnt.gnt_register_action("Stuff", add_new_feed) +gnt.gnt_main() + +gnt.gnt_quit() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/finch/libgnt/pygnt/example/rss/gntrss.py Tue Aug 14 02:45:41 2007 +0000 @@ -0,0 +1,250 @@ +#!/usr/bin/env python + +""" +gr - An RSS-reader built using libgnt and feedparser. + +Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@pidgin.im> + +This application is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This application is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this application; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +USA +""" + +""" +This file deals with the rss parsing part (feedparser) of the application +""" + +import os +import tempfile, urllib2 +import feedparser +import gobject +import sys +import time + +## +# The FeedItem class. It will update emit 'delete' signal when it's +# destroyed. +## +class FeedItem(gobject.GObject): + __gproperties__ = { + 'unread' : (gobject.TYPE_BOOLEAN, 'read', + 'The unread state of the item.', + False, gobject.PARAM_READWRITE) + } + __gsignals__ = { + 'delete' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,)) + } + def __init__(self, item, parent): + self.__gobject_init__() + try: + "Apparently some feed items don't have any dates in them" + self.date = item['date'] + self.date_parsed = item['date_parsed'] + except: + item['date'] = self.date = time.ctime() + self.date_parsed = feedparser._parse_date(self.date) + + self.title = item['title'].encode('utf8') + self.summary = item['summary'].encode('utf8') + self.link = item['link'] + self.parent = parent + self.unread = True + + def remove(self): + self.emit('delete', self.parent) + if self.unread: + self.parent.set_property('unread', self.parent.unread - 1) + + def do_set_property(self, property, value): + if property.name == 'unread': + self.unread = value + + def mark_unread(self, unread): + if self.unread == unread: + return + self.set_property('unread', unread) + +gobject.type_register(FeedItem) + +def item_hash(item): + return str(item['title']) + +""" +The Feed class. It will update the 'link', 'title', 'desc' and 'items' +attributes if/when they are updated (triggering 'notify::<attr>' signal) + +TODO: + - Add a 'count' attribute + - Each feed will have a 'uidata', which will be its display window + - Look into 'category'. Is it something that feed defines, or the user? + - Have separate refresh times for each feed. + - Have 'priority' for each feed. (somewhat like category, perhaps?) +""" +class Feed(gobject.GObject): + __gproperties__ = { + 'link' : (gobject.TYPE_STRING, 'link', + 'The web page this feed is associated with.', + '...', gobject.PARAM_READWRITE), + 'title' : (gobject.TYPE_STRING, 'title', + 'The title of the feed.', + '...', gobject.PARAM_READWRITE), + 'desc' : (gobject.TYPE_STRING, 'description', + 'The description for the feed.', + '...', gobject.PARAM_READWRITE), + 'items' : (gobject.TYPE_POINTER, 'items', + 'The items in the feed.', gobject.PARAM_READWRITE), + 'unread' : (gobject.TYPE_INT, 'unread', + 'Number of unread items in the feed.', 0, 10000, 0, gobject.PARAM_READWRITE) + } + __gsignals__ = { + 'added' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,)) + } + + def __init__(self, feed): + self.__gobject_init__() + url = feed['link'] + name = feed['name'] + self.url = url # The url of the feed itself + self.link = url # The web page associated with the feed + self.desc = url + self.title = (name, url)[not name] + self.customtitle = name + self.unread = 0 + self.items = [] + self.hash = {} + self.pending = False + self._refresh = {'time' : 30, 'id' : 0} + + def do_set_property(self, property, value): + if property.name == 'link': + self.link = value + elif property.name == 'desc': + self.desc = value + elif property.name == 'title': + self.title = value + elif property.name == 'unread': + self.unread = value + pass + + def set_result(self, result): + # XXX Look at result['bozo'] first, and emit some signal that the UI can use + # to indicate (dim the row?) that the feed has invalid XML format or something + + try: + channel = result['channel'] + self.set_property('link', channel['link']) + self.set_property('desc', channel['description']) + self.set_property('title', channel['title']) + items = result['items'] + except: + items = () + + tmp = {} + for item in self.items: + tmp[hash(item)] = item + + unread = self.unread + for item in items: + try: + exist = self.hash[item_hash(item)] + del tmp[hash(exist)] + except: + itm = FeedItem(item, self) + self.items.append(itm) + self.emit('added', itm) + self.hash[item_hash(item)] = itm + unread = unread + 1 + + if unread != self.unread: + self.set_property('unread', unread) + + for hv in tmp: + self.items.remove(tmp[hv]) + tmp[hv].remove() + "Also notify the UI about the count change" + + self.pending = False + return False + + def refresh(self): + if self.pending: + return + self.pending = True + FeedReader(self).run() + return True + + def mark_read(self): + for item in self.items: + item.mark_unread(False) + + def set_auto_refresh(self, auto): + if auto: + if self._refresh['id']: + return + if self._refresh['time'] < 1: + self._refresh['time'] = 1 + self.id = gobject.timeout_add(self._refresh['time'] * 1000 * 60, self.refresh) + else: + if not self._refresh['id']: + return + gobject.source_remove(self._refresh['id']) + self._refresh['id'] = 0 + +gobject.type_register(Feed) + +""" +The FeedReader updates a Feed. It fork()s off a child to avoid blocking. +""" +class FeedReader: + def __init__(self, feed): + self.feed = feed + + def reap_child(self, pid, status): + result = feedparser.parse(self.tmpfile.name) + self.tmpfile.close() + self.feed.set_result(result) + + def run(self): + self.tmpfile = tempfile.NamedTemporaryFile() + self.pid = os.fork() + if self.pid == 0: + tmp = urllib2.urlopen(self.feed.url) + content = tmp.read() + tmp.close() + self.tmpfile.write(content) + self.tmpfile.flush() + # Do NOT close tmpfile here + os._exit(os.EX_OK) + gobject.child_watch_add(self.pid, self.reap_child) + +feeds = [] +urls = ( + {'name': '/.', + 'link': "http://rss.slashdot.org/Slashdot/slashdot"}, + {'name': 'KernelTrap', + 'link': "http://kerneltrap.org/node/feed"}, + {'name': None, + 'link': "http://pidgin.im/rss.php"}, + {'name': "F1", + 'link': "http://www.formula1.com/rss/news/latest.rss"}, + {'name': "Freshmeat", + 'link': "http://www.pheedo.com/f/freshmeatnet_announcements_unix"}, + {'name': "Cricinfo", + 'link': "http://www.cricinfo.com/rss/livescores.xml"} +) + +for url in urls: + feed = Feed(url) + feeds.append(feed) +
--- a/finch/libgnt/pygnt/gendef.sh Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/pygnt/gendef.sh Tue Aug 14 02:45:41 2007 +0000 @@ -28,7 +28,7 @@ gnt.h" # Generate the def file -rm gnt.def +rm -f gnt.def for file in $FILES do python /usr/share/pygtk/2.0/codegen/h2def.py ../$file >> gnt.def @@ -43,6 +43,7 @@ GNT_TYPE_KEY_PRESS_MODE GNT_TYPE_ENTRY_FLAG GNT_TYPE_TEXT_FORMAT_FLAGS +GNT_TYPE_TEXT_VIEW_FLAG " for enum in $ENUMS
--- a/finch/libgnt/pygnt/gnt.override Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/pygnt/gnt.override Tue Aug 14 02:45:41 2007 +0000 @@ -29,8 +29,10 @@ #include "common.h" %% include + gntbox.override gntfilesel.override gnttree.override + gntwidget.override %% modulename gnt %% @@ -39,3 +41,154 @@ ignore-glob *_get_gtype %% +define set_flag +static PyObject * +_wrap_set_flag(PyGObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = {"flags", NULL}; + PyGObject *widget; + int flags; + + if (!PyArg_ParseTuple(args, "O!i:gnt.set_flag", &PyGntWidget_Type, &widget, + &flags)) { + return NULL; + } + + GNT_WIDGET_SET_FLAGS(widget->obj, flags); + + Py_INCREF(Py_None); + return Py_None; +} +%% +define unset_flag +static PyObject * +_wrap_unset_flag(PyGObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = {"flags", NULL}; + PyGObject *widget; + int flags; + + if (!PyArg_ParseTuple(args, "O!i:gnt.unset_flag", &PyGntWidget_Type, &widget, + &flags)) { + return NULL; + } + + GNT_WIDGET_UNSET_FLAGS(widget->obj, flags); + + Py_INCREF(Py_None); + return Py_None; +} +%% +define screen_size noargs +static PyObject * +_wrap_screen_size(PyObject *self) +{ + PyObject *list = PyList_New(0); + + if (list == NULL) + return NULL; + + PyList_Append(list, PyInt_FromLong((long)getmaxx(stdscr))); + PyList_Append(list, PyInt_FromLong((long)getmaxy(stdscr))); + + return list; +} +%% +override gnt_register_action +static GHashTable *actions; + + + +static PyObject * +_wrap_gnt_register_action(PyGObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = {"name", "callback", NULL}; + PyGObject *callback; + GClosure *closure; + char *name; + + if (!PyArg_ParseTuple(args, "sO:gnt.gnt_register_action", &name, &callback)) { + return NULL; + } + + if (!PyCallable_Check(callback)) { + PyErr_SetString(PyExc_TypeError, "the callback must be callable ... doh!"); + return NULL; + } + + gnt_register_action(name, callback->obj); + + Py_INCREF(Py_None); + return Py_None; +} +%% +define register_bindings + +static gboolean +pygnt_binding_callback(GntBindable *bindable, GList *list) +{ + PyObject *wrapper = pygobject_new(G_OBJECT(bindable)); + PyObject_CallMethod(wrapper, list->data, "O", Py_None); + Py_DECREF(wrapper); + return TRUE; +} + +static PyObject * +_wrap_register_bindings(PyObject *self, PyObject *args) +{ + PyTypeObject *class; + int pos = 0; + PyObject *key, *value, *gbindings; + GntBindableClass *bindable; + + if (!PyArg_ParseTuple(args, "O!:gnt.register_bindings", + &PyType_Type, &class)) { + /* Make sure it's a GntBindableClass subclass */ + PyErr_SetString(PyExc_TypeError, + "argument must be a GntBindable subclass"); + return NULL; + } + + gbindings = PyDict_GetItemString(class->tp_dict, "__gntbindings__"); + if (!gbindings) + goto end; + + if (!PyDict_Check(gbindings)) { + PyErr_SetString(PyExc_TypeError, + "__gntbindings__ attribute not a dict!"); + return NULL; + } + + bindable = g_type_class_ref(pyg_type_from_object((PyObject *)class)); + while (PyDict_Next(gbindings, &pos, &key, &value)) { + const char *trigger, *callback, *name; + GList *list = NULL; + + if (!PyString_Check(key)) { + PyErr_SetString(PyExc_TypeError, + "__gntbindings__ keys must be strings"); + g_type_class_unref(bindable); + return NULL; + } + name = PyString_AsString(key); + + if (!PyTuple_Check(value) || + !PyArg_ParseTuple(value, "ss", &callback, &trigger)) { + PyErr_SetString(PyExc_TypeError, + "__gntbindings__ values must be (callback, trigger) tupples"); + g_type_class_unref(bindable); + return NULL; + } + + gnt_bindable_class_register_action(bindable, name, pygnt_binding_callback, + trigger, g_strdup(callback), NULL); + } + if (gbindings) + PyDict_DelItemString(class->tp_dict, "__gntbindings__"); + g_type_class_unref(bindable); + +end: + Py_INCREF(Py_None); + return Py_None; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/finch/libgnt/pygnt/gntbox.override Tue Aug 14 02:45:41 2007 +0000 @@ -0,0 +1,38 @@ +/** + * pygnt- Python bindings for the GNT toolkit. + * Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@pidgin.im> + * + * gntbox.override: overrides for the box widget. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +%% +override gnt_box_add_widget kwargs +static PyObject * +_wrap_gnt_box_add_widget(PyGObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = { "widget", NULL }; + PyGObject *widget; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!:GntBox.add_widget", kwlist, &PyGntWidget_Type, &widget)) + return NULL; + + gnt_box_add_widget(GNT_BOX(self->obj), GNT_WIDGET(widget->obj)); + Py_INCREF(widget); + + Py_INCREF(Py_None); + return Py_None; +}
--- a/finch/libgnt/pygnt/gntmodule.c Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/pygnt/gntmodule.c Tue Aug 14 02:45:41 2007 +0000 @@ -9,11 +9,12 @@ PyObject *m, *d; init_pygobject (); - + m = Py_InitModule ("gnt", gnt_functions); d = PyModule_GetDict (m); gnt_register_classes (d); + gnt_add_constants(m, "GNT_"); if (PyErr_Occurred ()) { Py_FatalError ("can't initialise module sad");
--- a/finch/libgnt/pygnt/gnttree.override Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/pygnt/gnttree.override Tue Aug 14 02:45:41 2007 +0000 @@ -20,7 +20,7 @@ * USA */ %% -headrs +headers #include "common.h" %% ignore @@ -49,7 +49,7 @@ return NULL; } while (list) { - PyObject *obj = pyg_pointer_new(G_TYPE_POINTER, list->data); + PyObject *obj = list->data; PyList_Append(py_list, obj); Py_DECREF(obj); list = list->next; @@ -100,4 +100,79 @@ Py_INCREF(Py_None); return Py_None; } +%% +override gnt_tree_get_selection_data noargs +static PyObject * +_wrap_gnt_tree_get_selection_data(PyGObject *self) +{ + PyObject *ret = gnt_tree_get_selection_data(GNT_TREE(self->obj)); + if (!ret) + ret = Py_None; + Py_INCREF(ret); + return ret; +} +%% +override gnt_tree_change_text +static PyObject * +_wrap_gnt_tree_change_text(PyGObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = { "key", "colno", "text", NULL }; + char *text; + int colno; + gpointer key; + if (!PyArg_ParseTupleAndKeywords(args, kwargs,"Ois:GntTree.change_text", kwlist, &key, &colno, &text)) + return NULL; + + gnt_tree_change_text(GNT_TREE(self->obj), key, colno, text); + + Py_INCREF(Py_None); + return Py_None; +} +%% +override gnt_tree_set_row_flags +static PyObject * +_wrap_gnt_tree_set_row_flags(PyGObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = { "key", "flag", NULL }; + int flag; + gpointer key; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs,"Oi:GntTree.set_row_flags", kwlist, &key, &flag)) + return NULL; + + gnt_tree_set_row_flags(GNT_TREE(self->obj), key, flag); + + Py_INCREF(Py_None); + return Py_None; +} +%% +override gnt_tree_remove +static PyObject * +_wrap_gnt_tree_remove(PyGObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = { "key", NULL }; + gpointer key; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs,"O:GntTree.remove", kwlist, &key)) + return NULL; + + gnt_tree_remove(GNT_TREE(self->obj), key); + + Py_INCREF(Py_None); + return Py_None; +} +%% +override gnt_tree_set_selected +static PyObject * +_wrap_gnt_tree_set_selected(PyGObject *self, PyObject *args) +{ + gpointer key; + if (!PyArg_ParseTuple(args, "O:GntTree.set_selected", &key)) { + return NULL; + } + gnt_tree_set_selected(GNT_TREE(self->obj), key); + Py_INCREF(Py_None); + return Py_None; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/finch/libgnt/pygnt/gntwidget.override Tue Aug 14 02:45:41 2007 +0000 @@ -0,0 +1,36 @@ +/** + * pygnt- Python bindings for the GNT toolkit. + * Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@pidgin.im> + * + * gntwidget.override: overrides for generic widgets. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +%% +override gnt_widget_get_size args +static PyObject * +_wrap_gnt_widget_get_size(PyGObject *self) +{ + PyObject *list = PyList_New(0); + int x = 0, y = 0; + + gnt_widget_get_size(GNT_WIDGET(self->obj), &x, &y); + PyList_Append(list, PyInt_FromLong((long)x)); + PyList_Append(list, PyInt_FromLong((long)y)); + + return list; +} +
--- a/finch/libgnt/pygnt/test.py Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/pygnt/test.py Tue Aug 14 02:45:41 2007 +0000 @@ -1,18 +1,59 @@ #!/usr/bin/python +import gobject import gnt +class MyObject(gobject.GObject): + __gproperties__ = { + 'mytype': (gobject.TYPE_INT, 'mytype', 'the type of the object', + 0, 10000, 0, gobject.PARAM_READWRITE), + 'string': (gobject.TYPE_STRING, 'string property', 'the string', + None, gobject.PARAM_READWRITE), + 'gobject': (gobject.TYPE_OBJECT, 'object property', 'the object', + gobject.PARAM_READWRITE), + } + + def __init__(self, type = 'string', value = None): + self.__gobject_init__() + self.set_property(type, value) + + def do_set_property(self, pspec, value): + if pspec.name == 'string': + self.string = value + self.type = gobject.TYPE_STRING + elif pspec.name == 'gobject': + self.gobject = value + self.type = gobject.TYPE_OBJECT + else: + raise AttributeError, 'unknown property %s' % pspec.name + def do_get_property(self, pspec): + if pspec.name == 'string': + return self.string + elif pspec.name == 'gobject': + return self.gobject + elif pspec.name == 'mytype': + return self.type + else: + raise AttributeError, 'unknown property %s' % pspec.name +gobject.type_register(MyObject) + def button_activate(button, tree): - list = tree.get_selection_text_list() - str = "" - for i in list: - str = str + i - entry.set_text("clicked!!!" + str) + list = tree.get_selection_text_list() + ent = tree.get_selection_data() + if ent.type == gobject.TYPE_STRING: + str = "" + for i in list: + str = str + i + entry.set_text("clicked!!!" + str) + elif ent.type == gobject.TYPE_OBJECT: + ent.gobject.set_text("mwhahaha!!!") gnt.gnt_init() win = gnt.Window() entry = gnt.Entry("") +obj = MyObject() +obj.set_property('gobject', entry) win.add_widget(entry) win.set_title("Entry") @@ -27,12 +68,20 @@ # so random non-string values can be used as the key for a row in a GntTree! last = None for i in range(1, 100): - tree.add_row_after(i, [str(i), ""], None, i-1) -tree.add_row_after(entry, ["asd"], None, None) -tree.add_row_after("b", ["123", ""], entry, None) + key = MyObject('string', str(i)) + tree.add_row_after(key, [str(i)], None, last) + last = key + +tree.add_row_after(MyObject('gobject', entry), ["asd"], None, None) +tree.add_row_after(MyObject('string', "b"), ["123"], MyObject('gobject', entry), None) button.connect("activate", button_activate, tree) +tv = gnt.TextView() + +win.add_widget(tv) +tv.append_text_with_flags("What up!!", gnt.TEXT_FLAG_BOLD) + win.show() gnt.gnt_main()
--- a/finch/libgnt/wms/Makefile.am Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/wms/Makefile.am Tue Aug 14 02:45:41 2007 +0000 @@ -1,10 +1,16 @@ +if PURPLE_AVAILABLE +# These custom wms depend on libpurple +purple_wms = s.la irssi.la +else +purple_wms = +endif + s_la_LDFLAGS = -module -avoid-version irssi_la_LDFLAGS = -module -avoid-version plugin_LTLIBRARIES = \ - s.la \ - irssi.la - + $(purple_wms) + plugindir = $(libdir)/gnt irssi_la_SOURCES = irssi.c
--- a/finch/libgnt/wms/s.c Tue Aug 14 02:45:06 2007 +0000 +++ b/finch/libgnt/wms/s.c Tue Aug 14 02:45:41 2007 +0000 @@ -15,6 +15,10 @@ #define TYPE_S (s_get_gtype()) +#ifdef _S +#undef _S +#endif + typedef struct _S { GntWM inherit;
--- a/libpurple/Makefile.am Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/Makefile.am Tue Aug 14 02:45:41 2007 +0000 @@ -148,7 +148,7 @@ # purple dbus server dbus_sources = dbus-server.c dbus-useful.c -dbus_headers = dbus-bindings.h dbus-purple.h dbus-server.h dbus-useful.h dbus-define-api.h +dbus_headers = dbus-bindings.h dbus-purple.h dbus-server.h dbus-useful.h dbus-define-api.h dbus-types.h dbus_exported = dbus-useful.h dbus-define-api.h account.h blist.h buddyicon.h \ connection.h conversation.h core.h ft.h log.h notify.h prefs.h roomlist.h \
--- a/libpurple/account.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/account.c Tue Aug 14 02:45:41 2007 +0000 @@ -1130,8 +1130,8 @@ void * purple_account_request_authorization(PurpleAccount *account, const char *remote_user, - const char *id, const char *alias, const char *message, gboolean on_list, - GCallback auth_cb, GCallback deny_cb, void *user_data) + const char *id, const char *alias, const char *message, gboolean on_list, + PurpleAccountRequestAuthorizationCb auth_cb, PurpleAccountRequestAuthorizationCb deny_cb, void *user_data) { PurpleAccountUiOps *ui_ops; PurpleAccountRequestInfo *info; @@ -1146,8 +1146,8 @@ info->type = PURPLE_ACCOUNT_REQUEST_AUTHORIZATION; info->account = account; info->ui_handle = ui_ops->request_authorize(account, remote_user, id, alias, message, - on_list, auth_cb, deny_cb, user_data); - + on_list, auth_cb, deny_cb, user_data); + handles = g_list_append(handles, info); return info->ui_handle; }
--- a/libpurple/account.h Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/account.h Tue Aug 14 02:45:41 2007 +0000 @@ -51,20 +51,50 @@ PURPLE_ACCOUNT_REQUEST_AUTHORIZATION = 0 /* Account authorization request */ } PurpleAccountRequestType; + +/** Account UI operations, used to notify the user of status changes and when + * buddies add this account to their buddy lists. + */ struct _PurpleAccountUiOps { - /* A buddy we already have added us to their buddy list. */ - void (*notify_added)(PurpleAccount *account, const char *remote_user, - const char *id, const char *alias, + /** A buddy who is already on this account's buddy list added this account + * to their buddy list. + */ + void (*notify_added)(PurpleAccount *account, + const char *remote_user, + const char *id, + const char *alias, const char *message); - void (*status_changed)(PurpleAccount *account, PurpleStatus *status); - /* Someone we don't have on our list added us. Will prompt to add them. */ - void (*request_add)(PurpleAccount *account, const char *remote_user, - const char *id, const char *alias, + + /** This account's status changed. */ + void (*status_changed)(PurpleAccount *account, + PurpleStatus *status); + + /** Someone we don't have on our list added us; prompt to add them. */ + void (*request_add)(PurpleAccount *account, + const char *remote_user, + const char *id, + const char *alias, const char *message); - void *(*request_authorize)(PurpleAccount *account, const char *remote_user, const char *id, - const char *alias, const char *message, gboolean on_list, - GCallback authorize_cb, GCallback deny_cb, void *user_data); + + /** Prompt for authorization when someone adds this account to their buddy + * list. To authorize them to see this account's presence, call \a + * authorize_cb (\a user_data); otherwise call \a deny_cb (\a user_data); + * @return a UI-specific handle, as passed to #close_account_request. + */ + void *(*request_authorize)(PurpleAccount *account, + const char *remote_user, + const char *id, + const char *alias, + const char *message, + gboolean on_list, + PurpleAccountRequestAuthorizationCb authorize_cb, + PurpleAccountRequestAuthorizationCb deny_cb, + void *user_data); + + /** Close a pending request for authorization. \a ui_handle is a handle + * as returned by #request_authorize. + */ void (*close_account_request)(void *ui_handle); void (*_purple_reserved1)(void); @@ -193,7 +223,7 @@ /** * Notifies the user that a remote user has wants to add the local user - * to his or her buddy list and requires authorization to d oso. + * to his or her buddy list and requires authorization to do so. * * This will present a dialog informing the user of this and ask if the * user authorizes or denies the remote user from adding him. @@ -212,7 +242,7 @@ */ void *purple_account_request_authorization(PurpleAccount *account, const char *remote_user, const char *id, const char *alias, const char *message, gboolean on_list, - GCallback auth_cb, GCallback deny_cb, void *user_data); + PurpleAccountRequestAuthorizationCb auth_cb, PurpleAccountRequestAuthorizationCb deny_cb, void *user_data); /** * Close account requests registered for the given PurpleAccount
--- a/libpurple/connection.h Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/connection.h Tue Aug 14 02:45:41 2007 +0000 @@ -60,15 +60,52 @@ #include "plugin.h" #include "status.h" +/** Connection UI operations. Used to notify the user of changes to + * connections, such as being disconnected, and to respond to the + * underlying network connection appearing and disappearing. UIs should + * call #purple_connections_set_ui_ops() with an instance of this struct. + * + * @see @ref ui-ops + */ typedef struct { - void (*connect_progress)(PurpleConnection *gc, const char *text, - size_t step, size_t step_count); + /** When an account is connecting, this operation is called to notify + * the UI of what is happening, as well as which @a step out of @a + * step_count has been reached (which might be displayed as a progress + * bar). + */ + void (*connect_progress)(PurpleConnection *gc, + const char *text, + size_t step, + size_t step_count); + /** Called when a connection is established (just before the + * @ref signed-on signal). + */ void (*connected)(PurpleConnection *gc); + /** Called when a connection is ended (between the @ref signing-off + * and @ref signed-off signals). + */ void (*disconnected)(PurpleConnection *gc); + /** Used to display connection-specific notices. (Pidgin's Gtk user + * interface implements this as a no-op; #purple_connection_notice(), + * which uses this operation, is not used by any of the protocols + * shipped with libpurple.) + */ void (*notice)(PurpleConnection *gc, const char *text); + /** Called when an error causes a connection to be disconnected. + * Called before #disconnected. + * @param text a localized error message. + */ void (*report_disconnect)(PurpleConnection *gc, const char *text); + /** Called when libpurple discovers that the computer's network + * connection is active. On Linux, this uses Network Manager if + * available; on Windows, it uses Win32's network change notification + * infrastructure. + */ void (*network_connected)(); + /** Called when libpurple discovers that the computer's network + * connection has gone away. + */ void (*network_disconnected)(); void (*_purple_reserved1)(void);
--- a/libpurple/conversation.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/conversation.c Tue Aug 14 02:45:41 2007 +0000 @@ -1106,7 +1106,7 @@ c = purple_conv_im_get_conversation(im); - /* Raise the window, if specified in prefs. */ + /* Pass this on to either the ops structure or the default write func. */ if (c->ui_ops != NULL && c->ui_ops->write_im != NULL) c->ui_ops->write_im(c, who, message, flags, mtime); else
--- a/libpurple/conversation.h Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/conversation.h Tue Aug 14 02:45:41 2007 +0000 @@ -149,27 +149,74 @@ */ struct _PurpleConversationUiOps { + /** Called when @a conv is created (but before the @ref + * conversation-created signal is emitted). + */ void (*create_conversation)(PurpleConversation *conv); + + /** Called just before @a conv is freed. */ void (*destroy_conversation)(PurpleConversation *conv); + /** Write a message to a chat. If this field is @c NULL, libpurple will + * fall back to using #write_conv. + * @see purple_conv_chat_write() + */ void (*write_chat)(PurpleConversation *conv, const char *who, const char *message, PurpleMessageFlags flags, time_t mtime); + /** Write a message to an IM conversation. If this field is @c NULL, + * libpurple will fall back to using #write_conv. + * @see purple_conv_im_write() + */ void (*write_im)(PurpleConversation *conv, const char *who, const char *message, PurpleMessageFlags flags, time_t mtime); - void (*write_conv)(PurpleConversation *conv, const char *name, const char *alias, - const char *message, PurpleMessageFlags flags, + /** Write a message to a conversation. This is used rather than + * the chat- or im-specific ops for generic messages, such as system + * messages like "x is now know as y". + * @see purple_conversation_write() + */ + void (*write_conv)(PurpleConversation *conv, + const char *name, + const char *alias, + const char *message, + PurpleMessageFlags flags, time_t mtime); - void (*chat_add_users)(PurpleConversation *conv, GList *cbuddies, gboolean new_arrivals); - + /** Add @a cbuddies to a chat. + * @param cbuddies A @C GList of #PurpleConvChatBuddy structs. + * @param new_arrivals Whether join notices should be shown. + * (Join notices are actually written to the + * conversation by #purple_conv_chat_add_users().) + */ + void (*chat_add_users)(PurpleConversation *conv, + GList *cbuddies, + gboolean new_arrivals); + /** Rename the user in this chat named @a old_name to @a new_name. (The + * rename message is written to the conversation by libpurple.) + * @param new_alias @a new_name's new alias, if they have one. + * @see purple_conv_chat_add_users() + */ void (*chat_rename_user)(PurpleConversation *conv, const char *old_name, const char *new_name, const char *new_alias); + /** Remove @a users from a chat. + * @param users A @C GList of <tt>const char *</tt>s. + * @see purple_conv_chat_rename_user() + */ void (*chat_remove_users)(PurpleConversation *conv, GList *users); + /** Called when a user's flags are changed. + * @see purple_conv_chat_user_set_flags() + */ void (*chat_update_user)(PurpleConversation *conv, const char *user); + /** Present this conversation to the user; for example, by displaying + * the IM dialog. + */ void (*present)(PurpleConversation *conv); + /** If this UI has a concept of focus (as in a windowing system) and + * this conversation has the focus, return @c TRUE; otherwise, return + * @c FALSE. + */ gboolean (*has_focus)(PurpleConversation *conv); /* Custom Smileys */ @@ -178,6 +225,11 @@ const guchar *data, gsize size); void (*custom_smiley_close)(PurpleConversation *conv, const char *smile); + /** Prompt the user for confirmation to send @a message. This function + * should arrange for the message to be sent if the user accepts. If + * this field is @c NULL, libpurple will fall back to using + * #purple_request_action(). + */ void (*send_confirm)(PurpleConversation *conv, const char *message); void (*_purple_reserved1)(void);
--- a/libpurple/dbus-analyze-functions.py Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/dbus-analyze-functions.py Tue Aug 14 02:45:41 2007 +0000 @@ -39,6 +39,7 @@ "purple_prefs_get_string_list", "purple_uri_list_extract_filenames", "purple_uri_list_extract_uris", + "purple_prefs_get_children_names", ] # This is a list of functions that return a GList* or GSList* that should
--- a/libpurple/log.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/log.c Tue Aug 14 02:45:41 2007 +0000 @@ -718,9 +718,9 @@ if (tmp < start) g_string_append_len(newmsg, tmp, start - tmp); - idstr = g_datalist_get_data(&attributes, "id"); + if ((idstr = g_datalist_get_data(&attributes, "id")) != NULL) + imgid = atoi(idstr); - imgid = atoi(idstr); if (imgid != 0) { FILE *image_file; @@ -735,6 +735,7 @@ if (image == NULL) { /* This should never happen. */ + /* This *does* happen for failed Direct-IMs -DAA */ g_string_free(newmsg, TRUE); g_return_val_if_reached((char *)msg); }
--- a/libpurple/plugins/perl/perl-handlers.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/plugins/perl/perl-handlers.c Tue Aug 14 02:45:41 2007 +0000 @@ -22,6 +22,7 @@ gchar *hvname; PurplePlugin *plugin; PurplePerlScript *gps; + STRLEN na; dSP; plugin = action->plugin; @@ -45,9 +46,16 @@ XPUSHs(purple_perl_bless_object(gps->plugin, "Purple::Plugin")); PUTBACK; - call_sv(*callback, G_VOID | G_DISCARD); + call_sv(*callback, G_EVAL | G_VOID | G_DISCARD); + SPAGAIN; + if (SvTRUE(ERRSV)) { + purple_debug_error("perl", + "Perl plugin action function exited abnormally: %s\n", + SvPV(ERRSV, na)); + } + PUTBACK; FREETMPS; LEAVE; @@ -59,6 +67,7 @@ GList *l = NULL; PurplePerlScript *gps; int i = 0, count = 0; + STRLEN na; dSP; gps = (PurplePerlScript *)plugin->info->extra_info; @@ -77,10 +86,16 @@ XPUSHs(&PL_sv_undef); PUTBACK; - count = call_pv(gps->plugin_action_sub, G_ARRAY); + count = call_pv(gps->plugin_action_sub, G_EVAL | G_ARRAY); SPAGAIN; + if (SvTRUE(ERRSV)) { + purple_debug_error("perl", + "Perl plugin actions lookup exited abnormally: %s\n", + SvPV(ERRSV, na)); + } + if (count == 0) croak("The plugin_actions sub didn't return anything.\n"); @@ -113,6 +128,7 @@ MAGIC *mg; GtkWidget *ret; PurplePerlScript *gps; + STRLEN na; dSP; gps = (PurplePerlScript *)plugin->info->extra_info; @@ -120,13 +136,19 @@ ENTER; SAVETMPS; - count = call_pv(gps->gtk_prefs_sub, G_SCALAR | G_NOARGS); + count = call_pv(gps->gtk_prefs_sub, G_EVAL | G_SCALAR | G_NOARGS); if (count != 1) croak("call_pv: Did not return the correct number of values.\n"); /* the frame was created in a perl sub and is returned */ SPAGAIN; + if (SvTRUE(ERRSV)) { + purple_debug_error("perl", + "Perl gtk plugin frame init exited abnormally: %s\n", + SvPV(ERRSV, na)); + } + /* We have a Gtk2::Frame on top of the stack */ sv = POPs; @@ -150,6 +172,7 @@ int count; PurplePerlScript *gps; PurplePluginPrefFrame *ret_frame; + STRLEN na; dSP; gps = (PurplePerlScript *)plugin->info->extra_info; @@ -161,10 +184,16 @@ PUSHMARK(SP); PUTBACK; - count = call_pv(gps->prefs_sub, G_SCALAR | G_NOARGS); + count = call_pv(gps->prefs_sub, G_EVAL | G_SCALAR | G_NOARGS); SPAGAIN; + if (SvTRUE(ERRSV)) { + purple_debug_error("perl", + "Perl plugin prefs frame init exited abnormally: %s\n", + SvPV(ERRSV, na)); + } + if (count != 1) croak("call_pv: Did not return the correct number of values.\n"); /* the frame was created in a perl sub and is returned */ @@ -215,6 +244,7 @@ { PurplePerlTimeoutHandler *handler = (PurplePerlTimeoutHandler *)data; gboolean ret = FALSE; + STRLEN na; dSP; ENTER; @@ -225,6 +255,12 @@ call_sv(handler->callback, G_EVAL | G_SCALAR); SPAGAIN; + if (SvTRUE(ERRSV)) { + purple_debug_error("perl", + "Perl timeout function exited abnormally: %s\n", + SvPV(ERRSV, na)); + } + ret = POPi; PUTBACK; @@ -285,7 +321,7 @@ else ret_val = purple_perl_data_from_sv(ret_value, POPs); } else { - call_sv(handler->callback, G_SCALAR); + call_sv(handler->callback, G_EVAL | G_SCALAR); SPAGAIN; } @@ -501,6 +537,7 @@ gchar **args, gchar **error, void *data) { int i = 0, count, ret_value = PURPLE_CMD_RET_OK; + STRLEN na; SV *cmdSV, *tmpSV, *convSV; PurplePerlCmdHandler *handler = (PurplePerlCmdHandler *)data; @@ -532,11 +569,17 @@ } PUTBACK; - count = call_sv(handler->callback, G_EVAL|G_SCALAR); + count = call_sv(handler->callback, G_EVAL | G_SCALAR); if (count != 1) croak("call_sv: Did not return the correct number of values.\n"); + if (SvTRUE(ERRSV)) { + purple_debug_error("perl", + "Perl plugin command function exited abnormally: %s\n", + SvPV(ERRSV, na)); + } + SPAGAIN; ret_value = POPi;
--- a/libpurple/prefs.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/prefs.c Tue Aug 14 02:45:41 2007 +0000 @@ -1341,7 +1341,6 @@ list = g_list_append(list, g_strdup_printf("%s%s%s", name, sep, child->name)); } return list; - } void
--- a/libpurple/protocols/bonjour/Makefile.am Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/bonjour/Makefile.am Tue Aug 14 02:45:41 2007 +0000 @@ -22,9 +22,9 @@ if MDNS_AVAHI BONJOURSOURCES += mdns_avahi.c else - if MDNS_HOWL +if MDNS_HOWL BONJOURSOURCES += mdns_howl.c - endif +endif endif AM_CFLAGS = $(st) @@ -42,9 +42,9 @@ if MDNS_AVAHI libbonjour_a_LIBADD += $(AVAHI_LIBS) else - if MDNS_HOWL +if MDNS_HOWL libbonjour_a_LIBADD += $(HOWL_LIBS) - endif +endif endif else @@ -56,9 +56,9 @@ if MDNS_AVAHI libbonjour_la_LIBADD += $(AVAHI_LIBS) else - if MDNS_HOWL +if MDNS_HOWL libbonjour_la_LIBADD += $(HOWL_LIBS) - endif +endif endif endif @@ -74,8 +74,8 @@ if MDNS_AVAHI AM_CPPFLAGS += $(AVAHI_CFLAGS) else - if MDNS_HOWL +if MDNS_HOWL AM_CPPFLAGS += $(HOWL_CFLAGS) - endif +endif endif
--- a/libpurple/protocols/bonjour/bonjour.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/bonjour/bonjour.c Tue Aug 14 02:45:41 2007 +0000 @@ -138,6 +138,8 @@ return; } + bonjour_dns_sd_update_buddy_icon(bd->dns_sd_data); + /* Create a group for bonjour buddies */ bonjour_group = purple_group_new(BONJOUR_GROUP_NAME); purple_blist_add_group(bonjour_group, NULL); @@ -283,17 +285,33 @@ bb->conversation = NULL; } +static +void bonjour_set_buddy_icon(PurpleConnection *conn, PurpleStoredImage *img) +{ + BonjourData *bd = conn->proto_data; + bonjour_dns_sd_update_buddy_icon(bd->dns_sd_data); +} + + static char * bonjour_status_text(PurpleBuddy *buddy) { - PurplePresence *presence; + const PurplePresence *presence; + const PurpleStatus *status; + const char *message; + gchar *ret = NULL; presence = purple_buddy_get_presence(buddy); + status = purple_presence_get_active_status(presence); - if (purple_presence_is_online(presence) && !purple_presence_is_available(presence)) - return g_strdup(_("Away")); + message = purple_status_get_attr_string(status, "message"); - return NULL; + if (message != NULL) { + ret = g_markup_escape_text(message, -1); + purple_util_chrreplace(ret, '\n', ' '); + } + + return ret; } static void @@ -301,6 +319,7 @@ { PurplePresence *presence; PurpleStatus *status; + BonjourBuddy *bb = buddy->proto_data; const char *status_description; const char *message; @@ -318,6 +337,23 @@ purple_notify_user_info_add_pair(user_info, _("Status"), status_description); if (message != NULL) purple_notify_user_info_add_pair(user_info, _("Message"), message); + + /* Only show first/last name if there is a nickname set (to avoid duplication) */ + if (bb->nick != NULL) { + if (bb->first != NULL) + purple_notify_user_info_add_pair(user_info, _("First name"), bb->first); + if (bb->first != NULL) + purple_notify_user_info_add_pair(user_info, _("Last name"), bb->last); + } + + if (bb->email != NULL) + purple_notify_user_info_add_pair(user_info, _("E-Mail"), bb->email); + + if (bb->AIM != NULL) + purple_notify_user_info_add_pair(user_info, _("AIM Account"), bb->AIM); + + if (bb->jid!= NULL) + purple_notify_user_info_add_pair(user_info, _("XMPP Account"), bb->jid); } static gboolean @@ -339,10 +375,9 @@ OPT_PROTO_NO_PASSWORD, NULL, /* user_splits */ NULL, /* protocol_options */ - /* {"png", 0, 0, 96, 96, 0, PURPLE_ICON_SCALE_DISPLAY}, */ /* icon_spec */ - NO_BUDDY_ICONS, /* not yet */ /* icon_spec */ + {"png,gif,jpeg", 0, 0, 96, 96, 65535, PURPLE_ICON_SCALE_DISPLAY}, /* icon_spec */ bonjour_list_icon, /* list_icon */ - NULL, /* list_emblem */ + NULL, /* list_emblem */ bonjour_status_text, /* status_text */ bonjour_tooltip_text, /* tooltip_text */ bonjour_status_types, /* status_types */ @@ -384,7 +419,7 @@ NULL, /* buddy_free */ bonjour_convo_closed, /* convo_closed */ NULL, /* normalize */ - NULL, /* set_buddy_icon */ + bonjour_set_buddy_icon, /* set_buddy_icon */ NULL, /* remove_group */ NULL, /* get_cb_real_name */ NULL, /* set_chat_topic */ @@ -412,11 +447,11 @@ PURPLE_PLUGIN_MAGIC, PURPLE_MAJOR_VERSION, PURPLE_MINOR_VERSION, - PURPLE_PLUGIN_PROTOCOL, /**< type */ + PURPLE_PLUGIN_PROTOCOL, /**< type */ NULL, /**< ui_requirement */ 0, /**< flags */ NULL, /**< dependencies */ - PURPLE_PRIORITY_DEFAULT, /**< priority */ + PURPLE_PRIORITY_DEFAULT, /**< priority */ "prpl-bonjour", /**< id */ "Bonjour", /**< name */ @@ -426,10 +461,10 @@ /** description */ N_("Bonjour Protocol Plugin"), NULL, /**< author */ - PURPLE_WEBSITE, /**< homepage */ + PURPLE_WEBSITE, /**< homepage */ NULL, /**< load */ - plugin_unload, /**< unload */ + plugin_unload, /**< unload */ NULL, /**< destroy */ NULL, /**< ui_info */ @@ -533,7 +568,7 @@ { default_firstname = g_strndup(fullname, splitpoint - fullname); tmp = &splitpoint[1]; - + /* The last name may be followed by a comma and additional data. * Only use the last name itself. */
--- a/libpurple/protocols/bonjour/buddy.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/bonjour/buddy.c Tue Aug 14 02:45:41 2007 +0000 @@ -18,6 +18,7 @@ #include <stdlib.h> #include "internal.h" +#include "cipher.h" #include "buddy.h" #include "account.h" #include "blist.h" @@ -41,6 +42,26 @@ return buddy; } +#define _B_CLR(x) g_free(x); x = NULL; + +void clear_bonjour_buddy_values(BonjourBuddy *buddy) { + + _B_CLR(buddy->first) + _B_CLR(buddy->email); + _B_CLR(buddy->ext); + _B_CLR(buddy->jid); + _B_CLR(buddy->last); + _B_CLR(buddy->msg); + _B_CLR(buddy->nick); + _B_CLR(buddy->node); + _B_CLR(buddy->phsh); + _B_CLR(buddy->status); + _B_CLR(buddy->vc); + _B_CLR(buddy->ver); + _B_CLR(buddy->AIM); + +} + void set_bonjour_buddy_value(BonjourBuddy* buddy, const char *record_key, const char *value, uint32_t len){ gchar **fld = NULL; @@ -106,11 +127,10 @@ PurpleBuddy *buddy; PurpleGroup *group; PurpleAccount *account = bonjour_buddy->account; - const char *status_id, *first, *last, *old_hash, *new_hash; - gchar *alias = NULL; + const char *status_id, *old_hash, *new_hash; /* Translate between the Bonjour status and the Purple status */ - if (g_ascii_strcasecmp("dnd", bonjour_buddy->status) == 0) + if (bonjour_buddy->status != NULL && g_ascii_strcasecmp("dnd", bonjour_buddy->status) == 0) status_id = BONJOUR_STATUS_ID_AWAY; else status_id = BONJOUR_STATUS_ID_AVAILABLE; @@ -138,15 +158,21 @@ } /* Create the alias for the buddy using the first and the last name */ - first = bonjour_buddy->first; - last = bonjour_buddy->last; - if ((first && *first) || (last && *last)) - alias = g_strdup_printf("%s%s%s", - (first && *first ? first : ""), - (first && *first && last && *last ? " " : ""), - (last && *last ? last : "")); - serv_got_alias(purple_account_get_connection(account), buddy->name, alias); - g_free(alias); + if (bonjour_buddy->nick) + serv_got_alias(purple_account_get_connection(account), buddy->name, bonjour_buddy->nick); + else { + gchar *alias = NULL; + const char *first, *last; + first = bonjour_buddy->first; + last = bonjour_buddy->last; + if ((first && *first) || (last && *last)) + alias = g_strdup_printf("%s%s%s", + (first && *first ? first : ""), + (first && *first && last && *last ? " " : ""), + (last && *last ? last : "")); + serv_got_alias(purple_account_get_connection(account), buddy->name, alias); + g_free(alias); + } /* Set the user's status */ if (bonjour_buddy->msg != NULL) @@ -166,12 +192,46 @@ new_hash = (bonjour_buddy->phsh && *(bonjour_buddy->phsh)) ? bonjour_buddy->phsh : NULL; if (new_hash && (!old_hash || strcmp(old_hash, new_hash) != 0)) { /* Look up the new icon data */ + /* TODO: Make sure the hash assigned to the retrieved buddy icon is the same + * as what we looked up. */ bonjour_dns_sd_retrieve_buddy_icon(bonjour_buddy); - } else + } else if (!new_hash) purple_buddy_icons_set_for_user(account, buddy->name, NULL, 0, NULL); } /** + * We got the buddy icon data; deal with it + */ +void bonjour_buddy_got_buddy_icon(BonjourBuddy *buddy, gconstpointer data, gsize len) { + /* Recalculate the hash instead of using the current phsh to make sure it is accurate for the icon. */ + int i; + gchar *enc; + char *p, hash[41]; + unsigned char hashval[20]; + + if (data == NULL || len == 0) + return; + + enc = purple_base64_encode(data, len); + + purple_cipher_digest_region("sha1", data, + len, sizeof(hashval), + hashval, NULL); + + p = hash; + for(i=0; i<20; i++, p+=2) + snprintf(p, 3, "%02x", hashval[i]); + + purple_debug_info("bonjour", "Got buddy icon for %s icon hash='%s' phsh='%s'.\n", buddy->name, + hash, buddy->phsh ? buddy->phsh : "(null)"); + + purple_buddy_icons_set_for_user(buddy->account, buddy->name, + g_memdup(data, len), len, hash); + + g_free(enc); +} + +/** * Deletes a buddy from memory. */ void @@ -179,8 +239,6 @@ { g_free(buddy->name); g_free(buddy->ip); - g_free(buddy->full_service_name); - g_free(buddy->first); g_free(buddy->phsh); g_free(buddy->status);
--- a/libpurple/protocols/bonjour/buddy.h Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/bonjour/buddy.h Tue Aug 14 02:45:41 2007 +0000 @@ -29,7 +29,6 @@ gchar *name; /* TODO: Remove and just use the hostname */ gchar *ip; - gchar *full_service_name; gint port_p2pj; gchar *first; @@ -76,9 +75,15 @@ BonjourBuddy *bonjour_buddy_new(const gchar *name, PurpleAccount *account); /** + * Clear any existing values from the buddy. + * This is called before updating so that we can notice removals + */ +void clear_bonjour_buddy_values(BonjourBuddy *buddy); + +/** * Sets a value in the BonjourBuddy struct, destroying the old value */ -void set_bonjour_buddy_value(BonjourBuddy* buddy, const char *record_key, const char *value, uint32_t len); +void set_bonjour_buddy_value(BonjourBuddy *buddy, const char *record_key, const char *value, uint32_t len); /** * Check if all the compulsory buddy data is present. @@ -91,6 +96,11 @@ void bonjour_buddy_add_to_purple(BonjourBuddy *buddy); /** + * We got the buddy icon data; deal with it + */ +void bonjour_buddy_got_buddy_icon(BonjourBuddy *buddy, gconstpointer data, gsize len); + +/** * Deletes a buddy from memory. */ void bonjour_buddy_delete(BonjourBuddy *buddy);
--- a/libpurple/protocols/bonjour/issues.txt Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/bonjour/issues.txt Tue Aug 14 02:45:41 2007 +0000 @@ -3,6 +3,5 @@ ========================================== * Status changes don't work -* Avatars * File transfers * Typing notifications
--- a/libpurple/protocols/bonjour/mdns_avahi.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/bonjour/mdns_avahi.c Tue Aug 14 02:45:41 2007 +0000 @@ -19,6 +19,7 @@ #include "mdns_interface.h" #include "debug.h" #include "buddy.h" +#include "bonjour.h" #include <avahi-client/client.h> #include <avahi-client/lookup.h> @@ -32,14 +33,25 @@ #include <avahi-glib/glib-malloc.h> #include <avahi-glib/glib-watch.h> +/* For some reason, this is missing from the Avahi type defines */ +#ifndef AVAHI_DNS_TYPE_NULL +#define AVAHI_DNS_TYPE_NULL 0x0A +#endif + /* data used by avahi bonjour implementation */ typedef struct _avahi_session_impl_data { AvahiClient *client; AvahiGLibPoll *glib_poll; AvahiServiceBrowser *sb; AvahiEntryGroup *group; + AvahiEntryGroup *buddy_icon_group; } AvahiSessionImplData; +typedef struct _avahi_buddy_impl_data { + AvahiServiceResolver *resolver; + AvahiRecordBrowser *buddy_icon_rec_browser; +} AvahiBuddyImplData; + static void _resolver_callback(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, AvahiResolverEvent event, const char *name, const char *type, const char *domain, @@ -59,11 +71,14 @@ case AVAHI_RESOLVER_FAILURE: purple_debug_error("bonjour", "_resolve_callback - Failure: %s\n", avahi_strerror(avahi_client_errno(avahi_service_resolver_get_client(r)))); + avahi_service_resolver_free(r); break; case AVAHI_RESOLVER_FOUND: /* create a buddy record */ buddy = bonjour_buddy_new(name, account); + ((AvahiBuddyImplData *)buddy->mdns_impl_data)->resolver = r; + /* Get the ip as a string */ buddy->ip = g_malloc(AVAHI_ADDRESS_STR_MAX); avahi_address_snprint(buddy->ip, AVAHI_ADDRESS_STR_MAX, a); @@ -71,6 +86,7 @@ buddy->port_p2pj = port; /* Obtain the parameters from the text_record */ + clear_bonjour_buddy_values(buddy); l = txt; while (l != NULL) { ret = avahi_string_list_get_pair(l, &key, &value, &size); @@ -95,7 +111,6 @@ purple_debug_info("bonjour", "Unrecognized Service Resolver event: %d.\n", event); } - avahi_service_resolver_free(r); } static void @@ -145,6 +160,30 @@ } static void +_buddy_icon_group_cb(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata) { + BonjourDnsSd *data = userdata; + AvahiSessionImplData *idata = data->mdns_impl_data; + + g_return_if_fail(g == idata->buddy_icon_group || idata->buddy_icon_group == NULL); + + switch(state) { + case AVAHI_ENTRY_GROUP_ESTABLISHED: + purple_debug_info("bonjour", "Successfully registered buddy icon data.\n"); + case AVAHI_ENTRY_GROUP_COLLISION: + purple_debug_error("bonjour", "Collision registering buddy icon data.\n"); + break; + case AVAHI_ENTRY_GROUP_FAILURE: + purple_debug_error("bonjour", "Error registering buddy icon data: %s\n.", + avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g)))); + break; + case AVAHI_ENTRY_GROUP_UNCOMMITED: + case AVAHI_ENTRY_GROUP_REGISTERING: + break; + } + +} + +static void _entry_group_cb(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata) { AvahiSessionImplData *idata = userdata; @@ -170,6 +209,31 @@ } +static void +_buddy_icon_record_cb(AvahiRecordBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, + AvahiBrowserEvent event, const char *name, uint16_t clazz, uint16_t type, + const void *rdata, size_t size, AvahiLookupResultFlags flags, void *userdata) { + BonjourBuddy *buddy = userdata; + AvahiBuddyImplData *idata = buddy->mdns_impl_data; + + switch (event) { + case AVAHI_BROWSER_NEW: + bonjour_buddy_got_buddy_icon(buddy, rdata, size); + break; + case AVAHI_BROWSER_REMOVE: + case AVAHI_BROWSER_CACHE_EXHAUSTED: + case AVAHI_BROWSER_ALL_FOR_NOW: + case AVAHI_BROWSER_FAILURE: + purple_debug_error("bonjour", "Error rerieving buddy icon record: %s\n", + avahi_strerror(avahi_client_errno(avahi_record_browser_get_client(b)))); + break; + } + + /* Stop listening */ + avahi_record_browser_free(idata->buddy_icon_rec_browser); + idata->buddy_icon_rec_browser = NULL; +} + /**************************** * mdns_interface functions * ****************************/ @@ -203,10 +267,8 @@ return TRUE; } -gboolean _mdns_publish(BonjourDnsSd *data, PublishType type) { +gboolean _mdns_publish(BonjourDnsSd *data, PublishType type, GSList *records) { int publish_result = 0; - char portstring[6]; - const char *jid, *aim, *email; AvahiSessionImplData *idata = data->mdns_impl_data; AvahiStringList *lst = NULL; @@ -223,44 +285,11 @@ } } - /* Convert the port to a string */ - snprintf(portstring, sizeof(portstring), "%d", data->port_p2pj); - - jid = purple_account_get_string(data->account, "jid", NULL); - aim = purple_account_get_string(data->account, "AIM", NULL); - email = purple_account_get_string(data->account, "email", NULL); - - /* We should try to follow XEP-0174, but some clients have "issues", so we humor them. - * See http://telepathy.freedesktop.org/wiki/SalutInteroperability - */ - - /* Needed by iChat */ - lst = avahi_string_list_add_pair(lst,"txtvers", "1"); - /* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */ - lst = avahi_string_list_add_pair(lst, "1st", data->first); - /* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */ - lst = avahi_string_list_add_pair(lst, "last", data->last); - /* Needed by Adium */ - lst = avahi_string_list_add_pair(lst, "port.p2pj", portstring); - /* Needed by iChat, Gaim/Pidgin <= 2.0.1 */ - lst = avahi_string_list_add_pair(lst, "status", data->status); - /* Currently always set to "!" since we don't support AV and wont ever be in a conference */ - lst = avahi_string_list_add_pair(lst, "vc", data->vc); - lst = avahi_string_list_add_pair(lst, "ver", VERSION); - if (email != NULL && *email != '\0') - lst = avahi_string_list_add_pair(lst, "email", email); - if (jid != NULL && *jid != '\0') - lst = avahi_string_list_add_pair(lst, "jid", jid); - /* Nonstandard, but used by iChat */ - if (aim != NULL && *aim != '\0') - lst = avahi_string_list_add_pair(lst, "AIM", aim); - if (data->msg != NULL && *data->msg != '\0') - lst = avahi_string_list_add_pair(lst, "msg", data->msg); - if (data->phsh != NULL && *data->phsh != '\0') - lst = avahi_string_list_add_pair(lst, "phsh", data->phsh); - - /* TODO: ext, nick, node */ - + while (records) { + PurpleKeyValuePair *kvp = records->data; + lst = avahi_string_list_add_pair(lst, kvp->key, kvp->value); + records = records->next; + } /* Publish the service */ switch (type) { @@ -290,7 +319,8 @@ return FALSE; } - if ((publish_result = avahi_entry_group_commit(idata->group)) < 0) { + if (type == PUBLISH_START + && (publish_result = avahi_entry_group_commit(idata->group)) < 0) { purple_debug_error("bonjour", "Failed to commit " ICHAT_SERVICE " service. Error: %s\n", avahi_strerror(publish_result)); @@ -317,9 +347,71 @@ return TRUE; } -/* This is done differently than with Howl/Apple Bonjour */ -guint _mdns_register_to_mainloop(BonjourDnsSd *data) { - return 0; +gboolean _mdns_set_buddy_icon_data(BonjourDnsSd *data, gconstpointer avatar_data, gsize avatar_len) { + AvahiSessionImplData *idata = data->mdns_impl_data; + + if (idata == NULL || idata->client == NULL) + return FALSE; + + if (avatar_data != NULL) { + gboolean new_group = FALSE; + gchar *svc_name; + int ret; + AvahiPublishFlags flags = 0; + + if (idata->buddy_icon_group == NULL) { + purple_debug_info("bonjour", "Setting new buddy icon.\n"); + new_group = TRUE; + + idata->buddy_icon_group = avahi_entry_group_new(idata->client, + _buddy_icon_group_cb, data); + } else { + purple_debug_info("bonjour", "Updating existing buddy icon.\n"); + flags |= AVAHI_PUBLISH_UPDATE; + } + + if (idata->buddy_icon_group == NULL) { + purple_debug_error("bonjour", + "Unable to initialize the buddy icon group (%s).\n", + avahi_strerror(avahi_client_errno(idata->client))); + return FALSE; + } + + svc_name = g_strdup_printf("%s." ICHAT_SERVICE "local", + purple_account_get_username(data->account)); + + ret = avahi_entry_group_add_record(idata->buddy_icon_group, AVAHI_IF_UNSPEC, + AVAHI_PROTO_UNSPEC, flags, svc_name, + AVAHI_DNS_CLASS_IN, AVAHI_DNS_TYPE_NULL, 120, avatar_data, avatar_len); + + g_free(svc_name); + + if (ret < 0) { + purple_debug_error("bonjour", + "Failed to register buddy icon. Error: %s\n", avahi_strerror(ret)); + if (new_group) { + avahi_entry_group_free(idata->buddy_icon_group); + idata->buddy_icon_group = NULL; + } + return FALSE; + } + + if (new_group && (ret = avahi_entry_group_commit(idata->buddy_icon_group)) < 0) { + purple_debug_error("bonjour", + "Failed to commit buddy icon group. Error: %s\n", avahi_strerror(ret)); + if (new_group) { + avahi_entry_group_free(idata->buddy_icon_group); + idata->buddy_icon_group = NULL; + } + return FALSE; + } + } else if (idata->buddy_icon_group != NULL) { + purple_debug_info("bonjour", "Removing existing buddy icon.\n"); + avahi_entry_group_free(idata->buddy_icon_group); + idata->buddy_icon_group = NULL; + } + + return TRUE; } void _mdns_stop(BonjourDnsSd *data) { @@ -340,10 +432,50 @@ } void _mdns_init_buddy(BonjourBuddy *buddy) { + buddy->mdns_impl_data = g_new0(AvahiBuddyImplData, 1); } void _mdns_delete_buddy(BonjourBuddy *buddy) { + AvahiBuddyImplData *idata = buddy->mdns_impl_data; + + g_return_if_fail(idata != NULL); + + if (idata->buddy_icon_rec_browser != NULL) + avahi_record_browser_free(idata->buddy_icon_rec_browser); + + if (idata->resolver != NULL) + avahi_service_resolver_free(idata->resolver); + + g_free(idata); + + buddy->mdns_impl_data = NULL; } -void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy) { +void _mdns_retrieve_buddy_icon(BonjourBuddy* buddy) { + PurpleConnection *conn = purple_account_get_connection(buddy->account); + BonjourData *bd = conn->proto_data; + AvahiSessionImplData *session_idata = bd->dns_sd_data->mdns_impl_data; + AvahiBuddyImplData *idata = buddy->mdns_impl_data; + gchar *name; + + g_return_if_fail(idata != NULL); + + if (idata->buddy_icon_rec_browser != NULL) + avahi_record_browser_free(idata->buddy_icon_rec_browser); + + purple_debug_info("bonjour", "Retrieving buddy icon for '%s'.\n", buddy->name); + + name = g_strdup_printf("%s." ICHAT_SERVICE "local", buddy->name); + idata->buddy_icon_rec_browser = avahi_record_browser_new(session_idata->client, AVAHI_IF_UNSPEC, + AVAHI_PROTO_UNSPEC, name, AVAHI_DNS_CLASS_IN, AVAHI_DNS_TYPE_NULL, 0, + _buddy_icon_record_cb, buddy); + g_free(name); + + if (!idata->buddy_icon_rec_browser) { + purple_debug_error("bonjour", + "Unable to initialize buddy icon record browser. Error: %s\n.", + avahi_strerror(avahi_client_errno(session_idata->client))); + } + } +
--- a/libpurple/protocols/bonjour/mdns_common.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/bonjour/mdns_common.c Tue Aug 14 02:45:41 2007 +0000 @@ -17,30 +17,27 @@ #include <string.h> #include "internal.h" +#include "cipher.h" +#include "debug.h" + #include "mdns_common.h" #include "mdns_interface.h" #include "bonjour.h" #include "buddy.h" -#include "debug.h" /** * Allocate space for the dns-sd data. */ -BonjourDnsSd * -bonjour_dns_sd_new() -{ +BonjourDnsSd * bonjour_dns_sd_new() { BonjourDnsSd *data = g_new0(BonjourDnsSd, 1); - return data; } /** * Deallocate the space of the dns-sd data. */ -void -bonjour_dns_sd_free(BonjourDnsSd *data) -{ +void bonjour_dns_sd_free(BonjourDnsSd *data) { g_free(data->first); g_free(data->last); g_free(data->phsh); @@ -50,12 +47,90 @@ g_free(data); } +static GSList *generate_presence_txt_records(BonjourDnsSd *data) { + GSList *ret = NULL; + PurpleKeyValuePair *kvp; + char portstring[6]; + const char *jid, *aim, *email; + + /* Convert the port to a string */ + snprintf(portstring, sizeof(portstring), "%d", data->port_p2pj); + + jid = purple_account_get_string(data->account, "jid", NULL); + aim = purple_account_get_string(data->account, "AIM", NULL); + email = purple_account_get_string(data->account, "email", NULL); + +#define _M_ADD_R(k, v) \ + kvp = g_new0(PurpleKeyValuePair, 1); \ + kvp->key = g_strdup(k); \ + kvp->value = g_strdup(v); \ + ret = g_slist_prepend(ret, kvp); \ + + /* We should try to follow XEP-0174, but some clients have "issues", so we humor them. + * See http://telepathy.freedesktop.org/wiki/SalutInteroperability + */ + + /* Needed by iChat */ + _M_ADD_R("txtvers", "1") + /* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */ + _M_ADD_R("1st", data->first) + /* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */ + _M_ADD_R("last", data->last) + /* Needed by Adium */ + _M_ADD_R("port.p2pj", portstring) + /* Needed by iChat, Gaim/Pidgin <= 2.0.1 */ + _M_ADD_R("status", data->status) + _M_ADD_R("node", "libpurple") + _M_ADD_R("ver", VERSION) + /* Currently always set to "!" since we don't support AV and wont ever be in a conference */ + _M_ADD_R("vc", data->vc) + if (email != NULL && *email != '\0') { + _M_ADD_R("email", email) + } + if (jid != NULL && *jid != '\0') { + _M_ADD_R("jid", jid) + } + /* Nonstandard, but used by iChat */ + if (aim != NULL && *aim != '\0') { + _M_ADD_R("AIM", aim) + } + if (data->msg != NULL && *data->msg != '\0') { + _M_ADD_R("msg", data->msg) + } + if (data->phsh != NULL && *data->phsh != '\0') { + _M_ADD_R("phsh", data->phsh) + } + + /* TODO: ext, nick */ + return ret; +} + +static void free_presence_txt_records(GSList *lst) { + PurpleKeyValuePair *kvp; + while(lst) { + kvp = lst->data; + g_free(kvp->key); + g_free(kvp->value); + g_free(kvp); + lst = g_slist_remove(lst, lst->data); + } +} + +static gboolean publish_presence(BonjourDnsSd *data, PublishType type) { + GSList *txt_records; + gboolean ret; + + txt_records = generate_presence_txt_records(data); + ret = _mdns_publish(data, type, txt_records); + free_presence_txt_records(txt_records); + + return ret; +} + /** * Send a new dns-sd packet updating our status. */ -void -bonjour_dns_sd_send_status(BonjourDnsSd *data, const char *status, const char *status_message) -{ +void bonjour_dns_sd_send_status(BonjourDnsSd *data, const char *status, const char *status_message) { g_free(data->status); g_free(data->msg); @@ -63,26 +138,78 @@ data->msg = g_strdup(status_message); /* Update our text record with the new status */ - _mdns_publish(data, PUBLISH_UPDATE); /* <--We must control the errors */ + publish_presence(data, PUBLISH_UPDATE); +} + +/** + * Retrieve the buddy icon blob + */ +void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy) { + _mdns_retrieve_buddy_icon(buddy); +} + +void bonjour_dns_sd_update_buddy_icon(BonjourDnsSd *data) { + PurpleStoredImage *img; + + if ((img = purple_buddy_icons_find_account_icon(data->account))) { + gconstpointer avatar_data; + gsize avatar_len; + + avatar_data = purple_imgstore_get_data(img); + avatar_len = purple_imgstore_get_size(img); + + if (_mdns_set_buddy_icon_data(data, avatar_data, avatar_len)) { + int i; + gchar *enc; + char *p, hash[41]; + unsigned char hashval[20]; + + enc = purple_base64_encode(avatar_data, avatar_len); + + purple_cipher_digest_region("sha1", avatar_data, + avatar_len, sizeof(hashval), + hashval, NULL); + + p = hash; + for(i=0; i<20; i++, p+=2) + snprintf(p, 3, "%02x", hashval[i]); + + g_free(data->phsh); + data->phsh = g_strdup(hash); + + g_free(enc); + + /* Update our TXT record */ + publish_presence(data, PUBLISH_UPDATE); + } + + purple_imgstore_unref(img); + } else { + /* We need to do this regardless of whether data->phsh is set so that we + * cancel any icons that are currently in the process of being set */ + _mdns_set_buddy_icon_data(data, NULL, 0); + if (data->phsh != NULL) { + /* Clear the buddy icon */ + g_free(data->phsh); + data->phsh = NULL; + /* Update our TXT record */ + publish_presence(data, PUBLISH_UPDATE); + } + } } /** * Advertise our presence within the dns-sd daemon and start browsing * for other bonjour peers. */ -gboolean -bonjour_dns_sd_start(BonjourDnsSd *data) -{ - PurpleConnection *gc; - - gc = purple_account_get_connection(data->account); +gboolean bonjour_dns_sd_start(BonjourDnsSd *data) { /* Initialize the dns-sd data and session */ if (!_mdns_init_session(data)) return FALSE; /* Publish our bonjour IM client at the mDNS daemon */ - if (!_mdns_publish(data, PUBLISH_START)) + if (!publish_presence(data, PUBLISH_START)) return FALSE; /* Advise the daemon that we are waiting for connections */ @@ -91,11 +218,6 @@ return FALSE; } - - /* Get the socket that communicates with the mDNS daemon and bind it to a */ - /* callback that will handle the dns_sd packets */ - gc->inpa = _mdns_register_to_mainloop(data); - return TRUE; } @@ -103,14 +225,6 @@ * Unregister the "_presence._tcp" service at the mDNS daemon. */ -void -bonjour_dns_sd_stop(BonjourDnsSd *data) -{ - PurpleConnection *gc; - +void bonjour_dns_sd_stop(BonjourDnsSd *data) { _mdns_stop(data); - - gc = purple_account_get_connection(data->account); - if (gc->inpa > 0) - purple_input_remove(gc->inpa); }
--- a/libpurple/protocols/bonjour/mdns_common.h Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/bonjour/mdns_common.h Tue Aug 14 02:45:41 2007 +0000 @@ -42,6 +42,11 @@ void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy); /** + * Deal with a buddy icon update + */ +void bonjour_dns_sd_update_buddy_icon(BonjourDnsSd *data); + +/** * Advertise our presence within the dns-sd daemon and start * browsing for other bonjour peers. */
--- a/libpurple/protocols/bonjour/mdns_howl.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/bonjour/mdns_howl.c Tue Aug 14 02:45:41 2007 +0000 @@ -26,6 +26,7 @@ typedef struct _howl_impl_data { sw_discovery session; sw_discovery_oid session_id; + guint session_handler; } HowlSessionImplData; static sw_result HOWL_API @@ -84,6 +85,7 @@ /* Obtain the parameters from the text_record */ if ((text_record_len > 0) && (text_record) && (*text_record != '\0')) { + clear_bonjour_buddy_values(buddy); sw_text_record_iterator_init(&iterator, text_record, text_record_len); while (sw_text_record_iterator_next(iterator, key, (sw_octet *)value, &value_length) == SW_OKAY) set_bonjour_buddy_value(buddy, key, value, value_length); @@ -192,11 +194,9 @@ } -gboolean _mdns_publish(BonjourDnsSd *data, PublishType type) { +gboolean _mdns_publish(BonjourDnsSd *data, PublishType type, GSList *records) { sw_text_record dns_data; sw_result publish_result = SW_OKAY; - char portstring[6]; - const char *jid, *aim, *email; HowlSessionImplData *idata = data->mdns_impl_data; g_return_val_if_fail(idata != NULL, FALSE); @@ -207,43 +207,11 @@ return FALSE; } - /* Convert the port to a string */ - snprintf(portstring, sizeof(portstring), "%d", data->port_p2pj); - - jid = purple_account_get_string(data->account, "jid", NULL); - aim = purple_account_get_string(data->account, "AIM", NULL); - email = purple_account_get_string(data->account, "email", NULL); - - /* We should try to follow XEP-0174, but some clients have "issues", so we humor them. - * See http://telepathy.freedesktop.org/wiki/SalutInteroperability - */ - - /* Needed by iChat */ - sw_text_record_add_key_and_string_value(dns_data, "txtvers", "1"); - /* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */ - sw_text_record_add_key_and_string_value(dns_data, "1st", data->first); - /* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */ - sw_text_record_add_key_and_string_value(dns_data, "last", data->last); - /* Needed by Adium */ - sw_text_record_add_key_and_string_value(dns_data, "port.p2pj", portstring); - /* Needed by iChat, Gaim/Pidgin <= 2.0.1 */ - sw_text_record_add_key_and_string_value(dns_data, "status", data->status); - /* Currently always set to "!" since we don't support AV and wont ever be in a conference */ - sw_text_record_add_key_and_string_value(dns_data, "vc", data->vc); - sw_text_record_add_key_and_string_value(dns_data, "ver", VERSION); - if (email != NULL && *email != '\0') - sw_text_record_add_key_and_string_value(dns_data, "email", email); - if (jid != NULL && *jid != '\0') - sw_text_record_add_key_and_string_value(dns_data, "jid", jid); - /* Nonstandard, but used by iChat */ - if (aim != NULL && *aim != '\0') - sw_text_record_add_key_and_string_value(dns_data, "AIM", aim); - if (data->msg != NULL && *data->msg != '\0') - sw_text_record_add_key_and_string_value(dns_data, "msg", data->msg); - if (data->phsh != NULL && *data->phsh != '\0') - sw_text_record_add_key_and_string_value(dns_data, "phsh", data->phsh); - - /* TODO: ext, nick, node */ + while (records) { + PurpleKeyValuePair *kvp = records->data; + sw_text_record_add_key_and_string_value(dns_data, kvp->key, kvp->value); + records = records->next; + } /* Publish the service */ switch (type) { @@ -276,17 +244,18 @@ g_return_val_if_fail(idata != NULL, FALSE); - return (sw_discovery_browse(idata->session, 0, ICHAT_SERVICE, NULL, _browser_reply, - data->account, &session_id) == SW_OKAY); + if (sw_discovery_browse(idata->session, 0, ICHAT_SERVICE, NULL, _browser_reply, + data->account, &session_id) == SW_OKAY) { + idata->session_handler = purple_input_add(sw_discovery_socket(idata->session), + PURPLE_INPUT_READ, _mdns_handle_event, idata->session); + return TRUE; + } + + return FALSE; } -guint _mdns_register_to_mainloop(BonjourDnsSd *data) { - HowlSessionImplData *idata = data->mdns_impl_data; - - g_return_val_if_fail(idata != NULL, 0); - - return purple_input_add(sw_discovery_socket(idata->session), - PURPLE_INPUT_READ, _mdns_handle_event, idata->session); +gboolean _mdns_set_buddy_icon_data(BonjourDnsSd *data, gconstpointer avatar_data, gsize avatar_len) { + return FALSE; } void _mdns_stop(BonjourDnsSd *data) { @@ -297,6 +266,8 @@ sw_discovery_cancel(idata->session, idata->session_id); + purple_input_remove(idata->session_handler); + /* TODO: should this really be g_free()'d ??? */ g_free(idata->session); @@ -311,5 +282,7 @@ void _mdns_delete_buddy(BonjourBuddy *buddy) { } -void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy) { +void _mdns_retrieve_buddy_icon(BonjourBuddy* buddy) { } + +
--- a/libpurple/protocols/bonjour/mdns_interface.h Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/bonjour/mdns_interface.h Tue Aug 14 02:45:41 2007 +0000 @@ -22,19 +22,18 @@ gboolean _mdns_init_session(BonjourDnsSd *data); -gboolean _mdns_publish(BonjourDnsSd *data, PublishType type); +gboolean _mdns_publish(BonjourDnsSd *data, PublishType type, GSList *records); gboolean _mdns_browse(BonjourDnsSd *data); -guint _mdns_register_to_mainloop(BonjourDnsSd *data); +void _mdns_stop(BonjourDnsSd *data); -void _mdns_stop(BonjourDnsSd *data); +gboolean _mdns_set_buddy_icon_data(BonjourDnsSd *data, gconstpointer avatar_data, gsize avatar_len); void _mdns_init_buddy(BonjourBuddy *buddy); void _mdns_delete_buddy(BonjourBuddy *buddy); -/* This doesn't quite belong here, but there really isn't any shared functionality */ -void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy); +void _mdns_retrieve_buddy_icon(BonjourBuddy* buddy); #endif
--- a/libpurple/protocols/bonjour/mdns_win32.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/bonjour/mdns_win32.c Tue Aug 14 02:45:41 2007 +0000 @@ -21,12 +21,14 @@ #include "mdns_interface.h" #include "dns_sd_proxy.h" #include "dnsquery.h" +#include "mdns_common.h" /* data structure for the resolve callback */ typedef struct _ResolveCallbackArgs { DNSServiceRef resolver; guint resolver_handler; + gchar *full_service_name; PurpleDnsQueryData *query; @@ -35,10 +37,12 @@ /* data used by win32 bonjour implementation */ typedef struct _win32_session_impl_data { - DNSServiceRef advertisement; - DNSServiceRef browser; + DNSServiceRef presence_svc; + DNSServiceRef browser_svc; + DNSRecordRef buddy_icon_rec; - guint advertisement_handler; /* hack... windows bonjour is broken, so we have to have this */ + guint presence_handler; + guint browser_handler; } Win32SessionImplData; typedef struct _win32_buddy_impl_data { @@ -60,6 +64,7 @@ uint8_t txt_len; int i; + clear_bonjour_buddy_values(buddy); for (i = 0; buddy_TXT_records[i] != NULL; i++) { txt_entry = TXTRecordGetValuePtr(record_len, record, buddy_TXT_records[i], &txt_len); if (txt_entry != NULL) @@ -68,7 +73,7 @@ } static void DNSSD_API -_mdns_text_record_query_callback(DNSServiceRef DNSServiceRef, DNSServiceFlags flags, +_mdns_record_query_callback(DNSServiceRef DNSServiceRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, const char *fullname, uint16_t rrtype, uint16_t rrclass, uint16_t rdlen, const void *rdata, uint32_t ttl, void *context) @@ -90,8 +95,7 @@ g_return_if_fail(idata != NULL); - purple_buddy_icons_set_for_user(buddy->account, buddy->name, - g_memdup(rdata, rdlen), rdlen, buddy->phsh); + bonjour_buddy_got_buddy_icon(buddy, rdata, rdlen); /* We've got what we need; stop listening */ purple_input_remove(idata->null_query_handler); @@ -120,14 +124,16 @@ /* finally, set up the continuous txt record watcher, and add the buddy to purple */ - if (kDNSServiceErr_NoError == DNSServiceQueryRecord(&idata->txt_query, 0, 0, buddy->full_service_name, - kDNSServiceType_TXT, kDNSServiceClass_IN, _mdns_text_record_query_callback, buddy)) { - int fd = DNSServiceRefSockFD(idata->txt_query); - idata->txt_query_handler = purple_input_add(fd, PURPLE_INPUT_READ, _mdns_handle_event, idata->txt_query); + if (kDNSServiceErr_NoError == DNSServiceQueryRecord(&idata->txt_query, kDNSServiceFlagsLongLivedQuery, + kDNSServiceInterfaceIndexAny, args->full_service_name, kDNSServiceType_TXT, + kDNSServiceClass_IN, _mdns_record_query_callback, buddy)) { + + purple_debug_info("bonjour", "Found buddy %s at %s:%d\n", buddy->name, buddy->ip, buddy->port_p2pj); + + idata->txt_query_handler = purple_input_add(DNSServiceRefSockFD(idata->txt_query), + PURPLE_INPUT_READ, _mdns_handle_event, idata->txt_query); bonjour_buddy_add_to_purple(buddy); - - purple_debug_info("bonjour", "Found buddy %s at %s:%d\n", buddy->name, buddy->ip, buddy->port_p2pj); } else bonjour_buddy_delete(buddy); @@ -138,6 +144,7 @@ /* free the remaining args memory */ purple_dnsquery_destroy(args->query); + g_free(args->full_service_name); g_free(args); } @@ -165,13 +172,14 @@ _mdns_parse_text_record(args->buddy, txtRecord, txtLen); /* set more arguments, and start the host resolver */ - args->buddy->full_service_name = g_strdup(fullname); + args->full_service_name = g_strdup(fullname); if (!(args->query = purple_dnsquery_a(hosttarget, port, _mdns_resolve_host_callback, args))) { purple_debug_error("bonjour", "service resolver - host resolution failed.\n"); bonjour_buddy_delete(args->buddy); + g_free(args->full_service_name); g_free(args); } } @@ -180,11 +188,11 @@ static void DNSSD_API _mdns_service_register_callback(DNSServiceRef sdRef, DNSServiceFlags flags, DNSServiceErrorType errorCode, - const char *name, const char *regtype, const char *domain, void *context) -{ - /* we don't actually care about anything said in this callback - this is only here because Bonjour for windows is broken */ + const char *name, const char *regtype, const char *domain, void *context) { + + /* TODO: deal with collision */ if (kDNSServiceErr_NoError != errorCode) - purple_debug_error("bonjour", "service advertisement - callback error.\n"); + purple_debug_error("bonjour", "service advertisement - callback error (%d).\n", errorCode); else purple_debug_info("bonjour", "service advertisement - callback.\n"); } @@ -235,61 +243,23 @@ return TRUE; } -gboolean _mdns_publish(BonjourDnsSd *data, PublishType type) { +gboolean _mdns_publish(BonjourDnsSd *data, PublishType type, GSList *records) { TXTRecordRef dns_data; - char portstring[6]; gboolean ret = TRUE; - const char *jid, *aim, *email; - DNSServiceErrorType set_ret; + DNSServiceErrorType set_ret = kDNSServiceErr_NoError; Win32SessionImplData *idata = data->mdns_impl_data; g_return_val_if_fail(idata != NULL, FALSE); TXTRecordCreate(&dns_data, 256, NULL); - /* Convert the port to a string */ - snprintf(portstring, sizeof(portstring), "%d", data->port_p2pj); - - jid = purple_account_get_string(data->account, "jid", NULL); - aim = purple_account_get_string(data->account, "AIM", NULL); - email = purple_account_get_string(data->account, "email", NULL); - - /* We should try to follow XEP-0174, but some clients have "issues", so we humor them. - * See http://telepathy.freedesktop.org/wiki/SalutInteroperability - */ - - /* Needed by iChat */ - set_ret = TXTRecordSetValue(&dns_data, "txtvers", 1, "1"); - /* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */ - if (set_ret == kDNSServiceErr_NoError) - set_ret = TXTRecordSetValue(&dns_data, "1st", strlen(data->first), data->first); - /* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */ - if (set_ret == kDNSServiceErr_NoError) - set_ret = TXTRecordSetValue(&dns_data, "last", strlen(data->last), data->last); - /* Needed by Adium */ - if (set_ret == kDNSServiceErr_NoError) - set_ret = TXTRecordSetValue(&dns_data, "port.p2pj", strlen(portstring), portstring); - /* Needed by iChat, Gaim/Pidgin <= 2.0.1 */ - if (set_ret == kDNSServiceErr_NoError) - set_ret = TXTRecordSetValue(&dns_data, "status", strlen(data->status), data->status); - if (set_ret == kDNSServiceErr_NoError) - set_ret = TXTRecordSetValue(&dns_data, "ver", strlen(VERSION), VERSION); - /* Currently always set to "!" since we don't support AV and wont ever be in a conference */ - if (set_ret == kDNSServiceErr_NoError) - set_ret = TXTRecordSetValue(&dns_data, "vc", strlen(data->vc), data->vc); - if (set_ret == kDNSServiceErr_NoError && email != NULL && *email != '\0') - set_ret = TXTRecordSetValue(&dns_data, "email", strlen(email), email); - if (set_ret == kDNSServiceErr_NoError && jid != NULL && *jid != '\0') - set_ret = TXTRecordSetValue(&dns_data, "jid", strlen(jid), jid); - /* Nonstandard, but used by iChat */ - if (set_ret == kDNSServiceErr_NoError && aim != NULL && *aim != '\0') - set_ret = TXTRecordSetValue(&dns_data, "AIM", strlen(aim), aim); - if (set_ret == kDNSServiceErr_NoError && data->msg != NULL && *data->msg != '\0') - set_ret = TXTRecordSetValue(&dns_data, "msg", strlen(data->msg), data->msg); - if (set_ret == kDNSServiceErr_NoError && data->phsh != NULL && *data->phsh != '\0') - set_ret = TXTRecordSetValue(&dns_data, "phsh", strlen(data->phsh), data->phsh); - - /* TODO: ext, nick, node */ + while (records) { + PurpleKeyValuePair *kvp = records->data; + set_ret = TXTRecordSetValue(&dns_data, kvp->key, strlen(kvp->value), kvp->value); + if (set_ret != kDNSServiceErr_NoError) + break; + records = records->next; + } if (set_ret != kDNSServiceErr_NoError) { purple_debug_error("bonjour", "Unable to allocate memory for text record.\n"); @@ -301,14 +271,15 @@ switch (type) { case PUBLISH_START: - purple_debug_info("bonjour", "Registering service on port %d\n", data->port_p2pj); - err = DNSServiceRegister(&idata->advertisement, 0, 0, purple_account_get_username(data->account), ICHAT_SERVICE, + purple_debug_info("bonjour", "Registering presence on port %d\n", data->port_p2pj); + err = DNSServiceRegister(&idata->presence_svc, 0, 0, purple_account_get_username(data->account), ICHAT_SERVICE, NULL, NULL, htons(data->port_p2pj), TXTRecordGetLength(&dns_data), TXTRecordGetBytesPtr(&dns_data), _mdns_service_register_callback, NULL); break; case PUBLISH_UPDATE: - err = DNSServiceUpdateRecord(idata->advertisement, NULL, 0, TXTRecordGetLength(&dns_data), TXTRecordGetBytesPtr(&dns_data), 0); + purple_debug_info("bonjour", "Updating presence.\n"); + err = DNSServiceUpdateRecord(idata->presence_svc, NULL, 0, TXTRecordGetLength(&dns_data), TXTRecordGetBytesPtr(&dns_data), 0); break; } @@ -316,9 +287,12 @@ purple_debug_error("bonjour", "Failed to publish presence service.\n"); ret = FALSE; } else if (type == PUBLISH_START) { - /* hack: Bonjour on windows is broken. We don't care about the callback but we have to listen anyway */ - gint fd = DNSServiceRefSockFD(idata->advertisement); - idata->advertisement_handler = purple_input_add(fd, PURPLE_INPUT_READ, _mdns_handle_event, idata->advertisement); + /* We need to do this because according to the Apple docs: + * "the client is responsible for ensuring that DNSServiceProcessResult() is called + * whenever there is a reply from the daemon - the daemon may terminate its connection + * with a client that does not process the daemon's responses */ + idata->presence_handler = purple_input_add(DNSServiceRefSockFD(idata->presence_svc), + PURPLE_INPUT_READ, _mdns_handle_event, idata->presence_svc); } } @@ -332,37 +306,64 @@ g_return_val_if_fail(idata != NULL, FALSE); - return (DNSServiceBrowse(&idata->browser, 0, 0, ICHAT_SERVICE, NULL, + if (DNSServiceBrowse(&idata->browser_svc, 0, 0, ICHAT_SERVICE, NULL, _mdns_service_browse_callback, data->account) - == kDNSServiceErr_NoError); -} + == kDNSServiceErr_NoError) { + idata->browser_handler = purple_input_add(DNSServiceRefSockFD(idata->browser_svc), + PURPLE_INPUT_READ, _mdns_handle_event, idata->browser_svc); + return TRUE; + } -guint _mdns_register_to_mainloop(BonjourDnsSd *data) { - Win32SessionImplData *idata = data->mdns_impl_data; - - g_return_val_if_fail(idata != NULL, 0); - - return purple_input_add(DNSServiceRefSockFD(idata->browser), - PURPLE_INPUT_READ, _mdns_handle_event, idata->browser); + return FALSE; } void _mdns_stop(BonjourDnsSd *data) { Win32SessionImplData *idata = data->mdns_impl_data; - if (idata == NULL || idata->advertisement == NULL || idata->browser == NULL) + if (idata == NULL) return; - /* hack: for win32, we need to stop listening to the advertisement pipe too */ - purple_input_remove(idata->advertisement_handler); + if (idata->presence_svc != NULL) { + purple_input_remove(idata->presence_handler); + DNSServiceRefDeallocate(idata->presence_svc); + } - DNSServiceRefDeallocate(idata->advertisement); - DNSServiceRefDeallocate(idata->browser); + if (idata->browser_svc != NULL) { + purple_input_remove(idata->browser_handler); + DNSServiceRefDeallocate(idata->browser_svc); + } g_free(idata); data->mdns_impl_data = NULL; } +gboolean _mdns_set_buddy_icon_data(BonjourDnsSd *data, gconstpointer avatar_data, gsize avatar_len) { + Win32SessionImplData *idata = data->mdns_impl_data; + DNSServiceErrorType err = kDNSServiceErr_NoError; + + g_return_val_if_fail(idata != NULL, FALSE); + + if (avatar_data != NULL && idata->buddy_icon_rec == NULL) { + purple_debug_info("bonjour", "Setting new buddy icon.\n"); + err = DNSServiceAddRecord(idata->presence_svc, &idata->buddy_icon_rec, + 0, kDNSServiceType_NULL, avatar_len, avatar_data, 0); + } else if (avatar_data != NULL) { + purple_debug_info("bonjour", "Updating existing buddy icon.\n"); + err = DNSServiceUpdateRecord(idata->presence_svc, idata->buddy_icon_rec, + 0, avatar_len, avatar_data, 0); + } else if (idata->buddy_icon_rec != NULL) { + purple_debug_info("bonjour", "Removing existing buddy icon.\n"); + DNSServiceRemoveRecord(idata->presence_svc, idata->buddy_icon_rec, 0); + idata->buddy_icon_rec = NULL; + } + + if (err != kDNSServiceErr_NoError) + purple_debug_error("bonjour", "Error (%d) setting buddy icon record.\n", err); + + return (err == kDNSServiceErr_NoError); +} + void _mdns_init_buddy(BonjourBuddy *buddy) { buddy->mdns_impl_data = g_new0(Win32BuddyImplData, 1); } @@ -387,8 +388,9 @@ buddy->mdns_impl_data = NULL; } -void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy) { +void _mdns_retrieve_buddy_icon(BonjourBuddy* buddy) { Win32BuddyImplData *idata = buddy->mdns_impl_data; + char svc_name[kDNSServiceMaxDomainName]; g_return_if_fail(idata != NULL); @@ -400,10 +402,11 @@ idata->null_query = NULL; } - if (kDNSServiceErr_NoError == DNSServiceQueryRecord(&idata->null_query, 0, 0, buddy->full_service_name, - kDNSServiceType_NULL, kDNSServiceClass_IN, _mdns_text_record_query_callback, buddy)) { - int fd = DNSServiceRefSockFD(idata->null_query); - idata->null_query_handler = purple_input_add(fd, PURPLE_INPUT_READ, _mdns_handle_event, idata->null_query); + DNSServiceConstructFullName(svc_name, buddy->name, ICHAT_SERVICE, "local"); + if (kDNSServiceErr_NoError == DNSServiceQueryRecord(&idata->null_query, 0, kDNSServiceInterfaceIndexAny, svc_name, + kDNSServiceType_NULL, kDNSServiceClass_IN, _mdns_record_query_callback, buddy)) { + idata->null_query_handler = purple_input_add(DNSServiceRefSockFD(idata->null_query), + PURPLE_INPUT_READ, _mdns_handle_event, idata->null_query); } }
--- a/libpurple/protocols/jabber/auth.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/jabber/auth.c Tue Aug 14 02:45:41 2007 +0000 @@ -296,7 +296,7 @@ purple_request_yes_no(js->gc, _("Plaintext Authentication"), _("Plaintext Authentication"), msg, - 2, js->gc->account, NULL, NULL, NULL, + 2, js->gc->account, NULL, NULL, js->gc->account, allow_cyrus_plaintext_auth, disallow_plaintext_auth); g_free(msg); @@ -682,11 +682,11 @@ gchar *a1, *convnode=NULL, *convpasswd = NULL, *ha1, *ha2, *kd, *x, *z; - if((convnode = g_convert(jid->node, strlen(jid->node), "iso-8859-1", "utf-8", + if((convnode = g_convert(jid->node, -1, "iso-8859-1", "utf-8", NULL, NULL, NULL)) == NULL) { convnode = g_strdup(jid->node); } - if(passwd && ((convpasswd = g_convert(passwd, strlen(passwd), "iso-8859-1", + if(passwd && ((convpasswd = g_convert(passwd, -1, "iso-8859-1", "utf-8", NULL, NULL, NULL)) == NULL)) { convpasswd = g_strdup(passwd); }
--- a/libpurple/protocols/jabber/jabber.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/jabber/jabber.c Tue Aug 14 02:45:41 2007 +0000 @@ -1718,7 +1718,7 @@ static PurpleCmdRet jabber_cmd_chat_role(PurpleConversation *conv, const char *cmd, char **args, char **error, void *data) { - JabberChat *chat; + JabberChat *chat = jabber_chat_find_by_conv(conv); if (!chat || !args || !args[0] || !args[1]) return PURPLE_CMD_RET_FAILED; @@ -1731,8 +1731,6 @@ return PURPLE_CMD_RET_FAILED; } - chat = jabber_chat_find_by_conv(conv); - if (!jabber_chat_role_user(chat, args[0], args[1])) { *error = g_strdup_printf(_("Unable to set role \"%s\" for user: %s"), args[1], args[0]);
--- a/libpurple/protocols/jabber/presence.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/jabber/presence.c Tue Aug 14 02:45:41 2007 +0000 @@ -193,16 +193,18 @@ char *who; }; -static void authorize_add_cb(struct _jabber_add_permit *jap) +static void authorize_add_cb(gpointer data) { + struct _jabber_add_permit *jap = data; jabber_presence_subscription_set(jap->gc->proto_data, jap->who, "subscribed"); g_free(jap->who); g_free(jap); } -static void deny_add_cb(struct _jabber_add_permit *jap) +static void deny_add_cb(gpointer data) { + struct _jabber_add_permit *jap = data; jabber_presence_subscription_set(jap->gc->proto_data, jap->who, "unsubscribed"); @@ -305,7 +307,7 @@ jap->js = js; purple_account_request_authorization(purple_connection_get_account(js->gc), from, NULL, NULL, NULL, onlist, - G_CALLBACK(authorize_add_cb), G_CALLBACK(deny_add_cb), jap); + authorize_add_cb, deny_add_cb, jap); jabber_id_free(jid); return; } else if(type && !strcmp(type, "subscribed")) {
--- a/libpurple/protocols/msn/userlist.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/msn/userlist.c Tue Aug 14 02:45:41 2007 +0000 @@ -38,8 +38,9 @@ * Callbacks **************************************************************************/ static void -msn_accept_add_cb(MsnPermitAdd *pa) +msn_accept_add_cb(gpointer data) { + MsnPermitAdd *pa = data; MsnSession *session = pa->gc->proto_data; MsnUserList *userlist = session->userlist; @@ -51,8 +52,9 @@ } static void -msn_cancel_add_cb(MsnPermitAdd *pa) +msn_cancel_add_cb(gpointer data) { + MsnPermitAdd *pa = data; MsnSession *session = pa->gc->proto_data; MsnUserList *userlist = session->userlist; @@ -75,7 +77,7 @@ purple_account_request_authorization(purple_connection_get_account(gc), passport, NULL, friendly, NULL, purple_find_buddy(purple_connection_get_account(gc), passport) != NULL, - G_CALLBACK(msn_accept_add_cb), G_CALLBACK(msn_cancel_add_cb), pa); + msn_accept_add_cb, msn_cancel_add_cb, pa); } /**************************************************************************
--- a/libpurple/protocols/myspace/myspace.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/myspace/myspace.c Tue Aug 14 02:45:41 2007 +0000 @@ -1,4107 +1,4107 @@ -/* MySpaceIM Protocol Plugin - * - * \author Jeff Connelly - * - * Copyright (C) 2007, Jeff Connelly <jeff2@soc.pidgin.im> - * - * Based on Purple's "C Plugin HOWTO" hello world example. - * - * Code also drawn from mockprpl: - * http://snarfed.org/space/purple+mock+protocol+plugin - * Copyright (C) 2004-2007, Ryan Barrett <mockprpl@ryanb.org> - * - * and some constructs also based on existing Purple plugins, which are: - * Copyright (C) 2003, Robbert Haarman <purple@inglorion.net> - * Copyright (C) 2003, Ethan Blanton <eblanton@cs.purdue.edu> - * Copyright (C) 2000-2003, Rob Flynn <rob@tgflinux.com> - * Copyright (C) 1998-1999, Mark Spencer <markster@marko.net> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -#define PURPLE_PLUGIN - -#include "message.h" -#include "persist.h" -#include "myspace.h" - - -/* Loosely based on Miranda plugin by Scott Ellis, formatting.cpp, - * https://server.scottellis.com.au/websvn/filedetails.php?repname=Miranda+Plugins&path=%2FMySpace%2Fformatting.cpp&rev=0&sc=0 */ - -/* The names in in emoticon_names (for <i n=whatever>) map to corresponding - * entries in emoticon_symbols (for the ASCII representation of the emoticon). - * - * Multiple emoticon symbols in Pidgin can map to one name. List the - * canonical form, as inserted by the "Smile!" dialog, first. For example, - * :) comes before :-), because although both are recognized as 'happy', - * the first is inserted by the smiley button. - * - * Note that symbols are case-sensitive in Pidgin -- :-X is not :-x. */ -static struct MSIM_EMOTICON -{ - gchar *name; - gchar *symbol; -} msim_emoticons[] = { - /* Unfortunately, this list duplicates much of the file - * pidgin/pidgin/pixmaps/emotes/default/22/default.theme.in, because - * that file is part of Pidgin, but we're part of libpurple. - */ - { "bigsmile", ":D" }, - { "bigsmile", ":-D" }, - { "devil", "}:)" }, - { "frazzled", ":Z" }, - { "geek", "B)" }, - { "googles", "%)" }, - { "growl", ":E" }, - { "laugh", ":))" }, /* Must be before ':)' */ - { "happy", ":)" }, - { "happy", ":-)" }, - { "happi", ":)" }, - { "heart", ":X" }, - { "mohawk", "-:" }, - { "mad", "X(" }, - { "messed", "X)" }, - { "nerd", "Q)" }, - { "oops", ":G" }, - { "pirate", "P)" }, - { "scared", ":O" }, - { "sidefrown", ":{" }, - { "sinister", ":B" }, - { "smirk", ":," }, - { "straight", ":|" }, - { "tongue", ":P" }, - { "tongue", ":p" }, - { "tongy", ":P" }, - { "upset", "B|" }, - { "wink", ";-)" }, - { "wink", ";)" }, - { "winc", ";)" }, - { "worried", ":[" }, - { "kiss", ":x" }, - { NULL, NULL } -}; - -/* Internal functions */ -static void msim_send_zap(PurpleBlistNode *node, gpointer zap_num_ptr); - -#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 const gchar *msim_compute_login_response( - const gchar nonce[2 * NONCE_SIZE], const gchar *email, - const gchar *password, guint *response_len); -static gboolean msim_send_bm(MsimSession *session, const gchar *who, - const gchar *text, int type); - -static guint msim_point_to_purple_size(MsimSession *session, guint point); -static guint msim_purple_size_to_point(MsimSession *session, guint size); -static guint msim_height_to_point(MsimSession *session, guint height); -static guint msim_point_to_height(MsimSession *session, guint point); - -static void msim_unrecognized(MsimSession *session, MsimMessage *msg, gchar *note); - -static void msim_markup_tag_to_html(MsimSession *, xmlnode *root, - gchar **begin, gchar **end); -static void html_tag_to_msim_markup(MsimSession *, xmlnode *root, - gchar **begin, gchar **end); -static gchar *msim_convert_xml(MsimSession *, const gchar *raw, - MSIM_XMLNODE_CONVERT f); -static gchar *msim_convert_smileys_to_markup(gchar *before); - -/* High-level msim markup <=> html conversion functions. */ -static gchar *msim_markup_to_html(MsimSession *, const gchar *raw); -static gchar *html_to_msim_markup(MsimSession *, const gchar *raw); - -static MsimUser *msim_get_user_from_buddy(PurpleBuddy *buddy); -static MsimUser *msim_find_user(MsimSession *session, const gchar *username); - -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); -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 gchar *msim_format_now_playing(gchar *band, gchar *song); - -static void msim_set_status_code(MsimSession *session, guint code, - gchar *statstring); - -static void msim_append_user_info(MsimSession *session, PurpleNotifyUserInfo *user_info, MsimUser *user, gboolean full); - -static void msim_downloaded_buddy_icon(PurpleUtilFetchUrlData *url_data, gpointer user_data, const gchar *url_text, - gsize len, const gchar *error_message); - -static void msim_store_user_info_each(gpointer key, gpointer value, - gpointer user_data); -static gboolean msim_store_user_info(MsimSession *session, MsimMessage *msg, MsimUser *user); -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_we_are_logged_on(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 guint msim_new_reply_callback(MsimSession *session, - MSIM_USER_LOOKUP_CB cb, gpointer data); - -static void msim_connect_cb(gpointer data, gint source, - const gchar *error_message); - -static gboolean msim_is_userid(const gchar *user); -static gboolean msim_is_email(const gchar *user); - -static void msim_lookup_user(MsimSession *session, const gchar *user, - MSIM_USER_LOOKUP_CB cb, gpointer data); - -double msim_round(double round); - -/* round is part of C99, but sometimes is unavailable before then. - * Based on http://forums.belution.com/en/cpp/000/050/13.shtml - */ -double msim_round(double value) -{ - if (value < 0) { - return -(floor(-value + 0.5)); - } else { - return floor( value + 0.5); - } -} - -/** - * Load the plugin. - */ -gboolean -msim_load(PurplePlugin *plugin) -{ - /* If compiled to use RC4 from libpurple, check if it is really there. */ - if (!purple_ciphers_find_cipher("rc4")) { - purple_debug_error("msim", "rc4 not in libpurple, but it is required - not loading MySpaceIM plugin!\n"); - purple_notify_error(plugin, _("Missing Cipher"), - _("The RC4 cipher could not be found"), - _("Upgrade " - "to a libpurple with RC4 support (>= 2.0.1). MySpaceIM " - "plugin will not be loaded.")); - return FALSE; - } - return TRUE; -} - -/** - * Get possible user status types. Based on mockprpl. - * - * @return GList of status types. - */ -GList * -msim_status_types(PurpleAccount *acct) -{ - GList *types; - PurpleStatusType *status; - - purple_debug_info("myspace", "returning status types\n"); - - types = NULL; - - /* Statuses are almost all the same. Define a macro to reduce code repetition. */ -#define _MSIM_ADD_NEW_STATUS(prim) status = \ - purple_status_type_new_with_attrs( \ - prim, /* PurpleStatusPrimitive */ \ - NULL, /* id - use default */ \ - NULL, /* name - use default */ \ - TRUE, /* savable */ \ - TRUE, /* user_settable */ \ - FALSE, /* not independent */ \ - \ - /* Attributes - each status can have a message. */ \ - "message", \ - _("Message"), \ - purple_value_new(PURPLE_TYPE_STRING), \ - NULL); \ - \ - \ - types = g_list_append(types, status) - - - _MSIM_ADD_NEW_STATUS(PURPLE_STATUS_AVAILABLE); - _MSIM_ADD_NEW_STATUS(PURPLE_STATUS_AWAY); - _MSIM_ADD_NEW_STATUS(PURPLE_STATUS_OFFLINE); - _MSIM_ADD_NEW_STATUS(PURPLE_STATUS_INVISIBLE); - - - return types; -} - -/** Zap someone. Callback from msim_blist_node_menu zap menu. */ -static void -msim_send_zap(PurpleBlistNode *node, gpointer zap_num_ptr) -{ - PurpleBuddy *buddy; - PurpleConnection *gc; - MsimSession *session; - gchar *username, *zap_string, *zap_text; - guint zap; - const gchar *zap_gerund[10]; - - if (!PURPLE_BLIST_NODE_IS_BUDDY(node)) { - /* Only know about buddies for now. */ - return; - } - - zap_gerund[0] = _("Zapping"); - zap_gerund[1] = _("Whacking"); - zap_gerund[2] = _("Torching"); - zap_gerund[3] = _("Smooching"); - zap_gerund[4] = _("Hugging"); - zap_gerund[5] = _("Bslapping"); - zap_gerund[6] = _("Goosing"); - zap_gerund[7] = _("Hi-fiving"); - zap_gerund[8] = _("Punking"); - zap_gerund[9] = _("Raspberry'ing"); - - g_return_if_fail(PURPLE_BLIST_NODE_IS_BUDDY(node)); - - buddy = (PurpleBuddy *)node; - gc = purple_account_get_connection(buddy->account); - g_return_if_fail(gc != NULL); - - session = (MsimSession *)gc->proto_data; - g_return_if_fail(session != NULL); - - username = buddy->name; - g_return_if_fail(username != NULL); - - zap = GPOINTER_TO_INT(zap_num_ptr); - zap_string = g_strdup_printf("!!!ZAP_SEND!!!=RTE_BTN_ZAPS_%d", zap); - zap_text = g_strdup_printf("*** %s! ***", zap_gerund[zap]); - - serv_got_im(session->gc, username, zap_text, - PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_SYSTEM, time(NULL)); - - if (!msim_send_bm(session, username, zap_string, MSIM_BM_ACTION)) { - purple_debug_info("msim_send_zap", "msim_send_bm failed: zapping %s with %s", - username, zap_string); - } - - g_free(zap_string); - g_free(zap_text); - return; -} - - -/** Return menu, if any, for a buddy list node. */ -GList * -msim_blist_node_menu(PurpleBlistNode *node) -{ - GList *menu, *zap_menu; - PurpleMenuAction *act; - const gchar *zap_names[10]; - guint i; - - if (!PURPLE_BLIST_NODE_IS_BUDDY(node)) { - /* Only know about buddies for now. */ - return NULL; - } - - /* Names from official client. */ - zap_names[0] = _("zap"); - zap_names[1] = _("whack"); - zap_names[2] = _("torch"); - zap_names[3] = _("smooch"); - zap_names[4] = _("hug"); - zap_names[5] = _("bslap"); - zap_names[6] = _("goose"); - zap_names[7] = _("hi-five"); - zap_names[8] = _("punk'd"); - zap_names[9] = _("raspberry"); - - menu = zap_menu = NULL; - - /* TODO: move to / command, or better yet new API */ - for (i = 0; i < sizeof(zap_names) / sizeof(zap_names[0]); ++i) { - act = purple_menu_action_new(zap_names[i], PURPLE_CALLBACK(msim_send_zap), - GUINT_TO_POINTER(i), NULL); - zap_menu = g_list_append(zap_menu, act); - } - - act = purple_menu_action_new(_("Zap"), NULL, NULL, zap_menu); - menu = g_list_append(menu, act); - - return menu; -} - -/** - * Return the icon name for a buddy and account. - * - * @param acct The account to find the icon for, or NULL for protocol icon. - * @param buddy The buddy to find the icon for, or NULL for the account icon. - * - * @return The base icon name string. - */ -const gchar * -msim_list_icon(PurpleAccount *acct, PurpleBuddy *buddy) -{ - /* Use a MySpace icon submitted by hbons at - * http://developer.pidgin.im/wiki/MySpaceIM. */ - return "myspace"; -} - -/** - * Replace 'old' with 'new' in 'str'. - * - * @param str The original string. - * @param old The substring of 'str' to replace. - * @param new The replacement for 'old' within 'str'. - * - * @return A _new_ string, based on 'str', with 'old' replaced - * by 'new'. Must be g_free()'d by caller. - * - * This string replace method is based on - * http://mail.gnome.org/archives/gtk-app-devel-list/2000-July/msg00201.html - * - */ -gchar * -str_replace(const gchar *str, const gchar *old, const gchar *new) -{ - gchar **items; - gchar *ret; - - items = g_strsplit(str, old, -1); - ret = g_strjoinv(new, items); - g_free(items); - return ret; -} - -#ifdef MSIM_DEBUG_MSG -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 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) -{ - 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) -{ - 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; - - /* Passwords are limited in length. */ - if (strlen(acct->password) > MSIM_MAX_PASSWORD_LENGTH) { - gchar *str; - - str = g_strdup_printf( - _("Sorry, passwords over %d characters in length (yours is " - "%d) are not supported by MySpace."), - MSIM_MAX_PASSWORD_LENGTH, - (int)strlen(acct->password)); - - /* Notify an error message also, because this is important! */ - purple_notify_error(acct, g_strdup(_("MySpaceIM Error")), str, NULL); - - purple_connection_error(gc, str); - - g_free(str); - } - - /* 1. connect to server */ - purple_connection_update_progress(gc, _("Connecting"), - 0, /* which connection step this is */ - 4); /* total number of steps */ - - host = purple_account_get_string(acct, "server", MSIM_SERVER); - port = purple_account_get_int(acct, "port", MSIM_PORT); - - /* From purple.sf.net/api: - * """Note that this function name can be misleading--although it is called - * "proxy connect," it is used for establishing any outgoing TCP connection, - * whether through a proxy or not.""" */ - - /* Calls msim_connect_cb when connected. */ - if (!purple_proxy_connect(gc, acct, host, port, msim_connect_cb, gc)) { - /* TODO: try other ports if in auto mode, then save - * working port and try that first next time. */ - purple_connection_error(gc, _("Couldn't create socket")); - return; - } -} - -/** - * Process a login challenge, sending a response. - * - * @param session - * @param msg Login challenge message. - * - * @return TRUE if successful, FALSE if not - */ -static gboolean -msim_login_challenge(MsimSession *session, MsimMessage *msg) -{ - PurpleAccount *account; - const gchar *response; - guint response_len; - gchar *nc; - gsize nc_len; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - g_return_val_if_fail(msim_msg_get_binary(msg, "nc", &nc, &nc_len), FALSE); - - account = session->account; - - g_return_val_if_fail(account != NULL, FALSE); - - purple_connection_update_progress(session->gc, _("Reading challenge"), 1, 4); - - purple_debug_info("msim", "nc is %d bytes, decoded\n", nc_len); - - if (nc_len != MSIM_AUTH_CHALLENGE_LENGTH) { - purple_debug_info("msim", "bad nc length: %x != 0x%x\n", nc_len, MSIM_AUTH_CHALLENGE_LENGTH); - purple_connection_error(session->gc, _("Unexpected challenge length from server")); - return FALSE; - } - - purple_connection_update_progress(session->gc, _("Logging in"), 2, 4); - - response_len = 0; - response = msim_compute_login_response(nc, account->username, account->password, &response_len); - - g_free(nc); - - return msim_send(session, - "login2", MSIM_TYPE_INTEGER, MSIM_AUTH_ALGORITHM, - /* This is actually user's email address. */ - "username", MSIM_TYPE_STRING, g_strdup(account->username), - /* GString and gchar * response will be freed in msim_msg_free() in msim_send(). */ - "response", MSIM_TYPE_BINARY, g_string_new_len(response, response_len), - "clientver", MSIM_TYPE_INTEGER, MSIM_CLIENT_VERSION, - "langid", MSIM_TYPE_INTEGER, MSIM_LANGUAGE_ID_ENGLISH, - "imlang", MSIM_TYPE_STRING, g_strdup(MSIM_LANGUAGE_NAME_ENGLISH), - "reconn", MSIM_TYPE_INTEGER, 0, - "status", MSIM_TYPE_INTEGER, 100, - "id", MSIM_TYPE_INTEGER, 1, - NULL); -} - -/** - * Compute the base64'd login challenge response based on username, password, nonce, and IPs. - * - * @param nonce The base64 encoded nonce ('nc') field from the server. - * @param email User's email address (used as login name). - * @param password User's cleartext password. - * @param response_len Will be written with response length. - * - * @return Binary login challenge response, ready to send to the server. - * Must be g_free()'d when finished. NULL if error. - */ -static const gchar * -msim_compute_login_response(const gchar nonce[2 * NONCE_SIZE], - const gchar *email, const gchar *password, guint *response_len) -{ - PurpleCipherContext *key_context; - PurpleCipher *sha1; - PurpleCipherContext *rc4; - - guchar hash_pw[HASH_SIZE]; - guchar key[HASH_SIZE]; - gchar *password_utf16le, *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; - } - - /* Compute password hash */ - purple_cipher_digest_region("sha1", (guchar *)password_utf16le, - conv_bytes_written, sizeof(hash_pw), hash_pw, NULL); - g_free(password_utf16le); - -#ifdef MSIM_DEBUG_LOGIN_CHALLENGE - purple_debug_info("msim", "pwhash = "); - for (i = 0; i < sizeof(hash_pw); i++) - purple_debug_info("msim", "%.2x ", hash_pw[i]); - purple_debug_info("msim", "\n"); -#endif - - /* key = sha1(sha1(pw) + nonce2) */ - sha1 = purple_ciphers_find_cipher("sha1"); - key_context = purple_cipher_context_new(sha1, NULL); - purple_cipher_context_append(key_context, hash_pw, HASH_SIZE); - purple_cipher_context_append(key_context, (guchar *)(nonce + NONCE_SIZE), NONCE_SIZE); - purple_cipher_context_digest(key_context, sizeof(key), key, NULL); - -#ifdef MSIM_DEBUG_LOGIN_CHALLENGE - purple_debug_info("msim", "key = "); - for (i = 0; i < sizeof(key); i++) { - purple_debug_info("msim", "%.2x ", key[i]); - } - purple_debug_info("msim", "\n"); -#endif - - rc4 = purple_cipher_context_new_by_name("rc4", NULL); - - /* Note: 'key' variable is 0x14 bytes (from SHA-1 hash), - * but only first 0x10 used for the RC4 key. */ - purple_cipher_context_set_option(rc4, "key_len", (gpointer)0x10); - purple_cipher_context_set_key(rc4, key); - - /* TODO: obtain IPs of network interfaces */ - - /* rc4 encrypt: - * nonce1+email+IP list */ - - data_len = NONCE_SIZE + strlen(email) + MSIM_LOGIN_IP_LIST_LEN; - data = g_new0(guchar, data_len); - memcpy(data, nonce, NONCE_SIZE); - memcpy(data + NONCE_SIZE, email, strlen(email)); - memcpy(data + NONCE_SIZE + strlen(email), MSIM_LOGIN_IP_LIST, MSIM_LOGIN_IP_LIST_LEN); - - data_out = g_new0(guchar, data_len); - - purple_cipher_context_encrypt(rc4, (const guchar *)data, - data_len, data_out, &data_out_len); - purple_cipher_context_destroy(rc4); - - g_assert(data_out_len == data_len); - -#ifdef MSIM_DEBUG_LOGIN_CHALLENGE - purple_debug_info("msim", "response=<%s>\n", data_out); -#endif - - *response_len = data_out_len; - - return (const gchar *)data_out; -} - -/** - * Schedule an IM to be sent once the user ID is looked up. - * - * @param gc Connection. - * @param who A user id, email, or username to send the message to. - * @param message Instant message text to send. - * @param flags Flags. - * - * @return 1 if successful or postponed, -1 if failed - * - * Allows sending to a user by username, email address, or userid. If - * a username or email address is given, the userid must be looked up. - * This function does that by calling msim_postprocess_outgoing(). - */ -int -msim_send_im(PurpleConnection *gc, const gchar *who, const gchar *message, - PurpleMessageFlags flags) -{ - MsimSession *session; - 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). - */ - /* TODO: maybe if message is delayed, don't echo to conv window, - * but do echo it to conv window manually once it is actually - * sent? Would be complicated. */ - rc = 1; - } else { - rc = -1; - } - - g_free(message_msim); - - /* - * In MySpace, you login with your email address, but don't talk to other - * users using their email address. So there is currently an asymmetry in the - * IM windows when using this plugin: - * - * you@example.com: hello - * some_other_user: what's going on? - * you@example.com: just coding a prpl - * - * TODO: Make the sent IM's appear as from the user's username, instead of - * their email address. Purple uses the login (in MSIM, the email)--change this. - */ - - 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. - * - */ -static 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; -} - -/* Indexes of this array + 1 map HTML font size to scale of normal font size. * - * Based on _point_sizes from libpurple/gtkimhtml.c - * 1 2 3 4 5 6 7 */ -static gdouble _font_scale[] = { .85, .95, 1, 1.2, 1.44, 1.728, 2.0736 }; - -#define MAX_FONT_SIZE 7 /* Purple maximum font size */ -#define POINTS_PER_INCH 72 /* How many pt's in an inch */ - -/** Convert typographical font point size to HTML font size. - * Based on libpurple/gtkimhtml.c */ -static guint -msim_point_to_purple_size(MsimSession *session, guint point) -{ - guint size, this_point, base; - gdouble scale; - - base = purple_account_get_int(session->account, "base_font_size", MSIM_BASE_FONT_POINT_SIZE); - - for (size = 0; - size < sizeof(_font_scale) / sizeof(_font_scale[0]); - ++size) { - scale = _font_scale[CLAMP(size, 1, MAX_FONT_SIZE) - 1]; - this_point = (guint)msim_round(scale * base); - - if (this_point >= point) { - purple_debug_info("msim", "msim_point_to_purple_size: %d pt -> size=%d\n", - point, size); - return size; - } - } - - /* No HTML font size was this big; return largest possible. */ - return this_point; -} - -/** Convert HTML font size to point size. */ -static guint -msim_purple_size_to_point(MsimSession *session, guint size) -{ - gdouble scale; - guint point; - guint base; - - scale = _font_scale[CLAMP(size, 1, MAX_FONT_SIZE) - 1]; - - base = purple_account_get_int(session->account, "base_font_size", MSIM_BASE_FONT_POINT_SIZE); - - point = (guint)msim_round(scale * base); - - purple_debug_info("msim", "msim_purple_size_to_point: size=%d -> %d pt\n", - size, point); - - return point; -} - -/** Convert a msim markup font pixel height to the more usual point size, for incoming messages. */ -static guint -msim_height_to_point(MsimSession *session, guint height) -{ - guint dpi; - - dpi = purple_account_get_int(session->account, "port", MSIM_DEFAULT_DPI); - - return (guint)msim_round((POINTS_PER_INCH * 1. / dpi) * height); - - /* See also: libpurple/protocols/bonjour/jabber.c - * _font_size_ichat_to_purple */ -} - -/** Convert point size to msim pixel height font size specification, for outgoing messages. */ -static guint -msim_point_to_height(MsimSession *session, guint point) -{ - guint dpi; - - dpi = purple_account_get_int(session->account, "port", MSIM_DEFAULT_DPI); - - return (guint)msim_round((dpi * 1. / POINTS_PER_INCH) * point); -} - -/** Convert the msim markup <f> (font) tag into HTML. */ -static void -msim_markup_f_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) -{ - const gchar *face, *height_str, *decor_str; - GString *gs_end, *gs_begin; - guint decor, height; - - face = xmlnode_get_attrib(root, "f"); - height_str = xmlnode_get_attrib(root, "h"); - decor_str = xmlnode_get_attrib(root, "s"); - - if (height_str) { - height = atol(height_str); - } else { - height = 12; - } - - if (decor_str) { - decor = atol(decor_str); - } else { - decor = 0; - } - - gs_begin = g_string_new(""); - /* TODO: get font size working */ - if (height && !face) { - g_string_printf(gs_begin, "<font size='%d'>", - msim_point_to_purple_size(session, msim_height_to_point(session, height))); - } else if (height && face) { - g_string_printf(gs_begin, "<font face='%s' size='%d'>", face, - msim_point_to_purple_size(session, msim_height_to_point(session, height))); - } else { - g_string_printf(gs_begin, "<font>"); - } - - /* No support for font-size CSS? */ - /* g_string_printf(gs_begin, "<span style='font-family: %s; font-size: %dpt'>", face, - msim_height_to_point(height)); */ - - gs_end = g_string_new("</font>"); - - if (decor & MSIM_TEXT_BOLD) { - g_string_append(gs_begin, "<b>"); - g_string_prepend(gs_end, "</b>"); - } - - if (decor & MSIM_TEXT_ITALIC) { - g_string_append(gs_begin, "<i>"); - g_string_append(gs_end, "</i>"); - } - - if (decor & MSIM_TEXT_UNDERLINE) { - g_string_append(gs_begin, "<u>"); - g_string_append(gs_end, "</u>"); - } - - - *begin = gs_begin->str; - *end = gs_end->str; -} - -/** Convert a msim markup color to a color suitable for libpurple. - * - * @param msim Either a color name, or an rgb(x,y,z) code. - * - * @return A new string, either a color name or #rrggbb code. Must g_free(). - */ -static char * -msim_color_to_purple(const char *msim) -{ - guint red, green, blue; - - if (!msim) { - return g_strdup("black"); - } - - if (sscanf(msim, "rgb(%d,%d,%d)", &red, &green, &blue) != 3) { - /* Color name. */ - return g_strdup(msim); - } - /* TODO: rgba (alpha). */ - - return g_strdup_printf("#%.2x%.2x%.2x", red, green, blue); -} - -/** Convert the msim markup <a> (anchor) tag into HTML. */ -static void -msim_markup_a_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) -{ - const gchar *href; - - href = xmlnode_get_attrib(root, "h"); - if (!href) { - href = ""; - } - - *begin = g_strdup_printf("<a href=\"%s\">%s", href, href); - *end = g_strdup("</a>"); -} - -/** Convert the msim markup <p> (paragraph) tag into HTML. */ -static void -msim_markup_p_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) -{ - /* Just pass through unchanged. - * - * Note: attributes currently aren't passed, if there are any. */ - *begin = g_strdup("<p>"); - *end = g_strdup("</p>"); -} - -/** Convert the msim markup <c> tag (text color) into HTML. TODO: Test */ -static void -msim_markup_c_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) -{ - const gchar *color; - gchar *purple_color; - - color = xmlnode_get_attrib(root, "v"); - if (!color) { - purple_debug_info("msim", "msim_markup_c_to_html: <c> tag w/o v attr"); - *begin = g_strdup(""); - *end = g_strdup(""); - /* TODO: log as unrecognized */ - return; - } - - purple_color = msim_color_to_purple(color); - - *begin = g_strdup_printf("<font color='%s'>", purple_color); - - g_free(purple_color); - - /* *begin = g_strdup_printf("<span style='color: %s'>", color); */ - *end = g_strdup("</font>"); -} - -/** Convert the msim markup <b> tag (background color) into HTML. TODO: Test */ -static void -msim_markup_b_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) -{ - const gchar *color; - gchar *purple_color; - - color = xmlnode_get_attrib(root, "v"); - if (!color) { - *begin = g_strdup(""); - *end = g_strdup(""); - purple_debug_info("msim", "msim_markup_b_to_html: <b> w/o v attr"); - /* TODO: log as unrecognized. */ - return; - } - - purple_color = msim_color_to_purple(color); - - /* TODO: find out how to set background color. */ - *begin = g_strdup_printf("<span style='background-color: %s'>", - purple_color); - g_free(purple_color); - - *end = g_strdup("</p>"); -} - -/** Convert the msim markup <i> tag (emoticon image) into HTML. */ -static void -msim_markup_i_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) -{ - const gchar *name; - guint i; - struct MSIM_EMOTICON *emote; - - name = xmlnode_get_attrib(root, "n"); - if (!name) { - purple_debug_info("msim", "msim_markup_i_to_html: <i> w/o n"); - *begin = g_strdup(""); - *end = g_strdup(""); - /* TODO: log as unrecognized */ - return; - } - - /* Find and use canonical form of smiley symbol. */ - for (i = 0; (emote = &msim_emoticons[i]) && emote->name != NULL; ++i) { - if (!strcmp(name, emote->name)) { - *begin = g_strdup(emote->symbol); - *end = g_strdup(""); - return; - } - } - - /* Couldn't find it, sorry. Try to degrade gracefully. */ - *begin = g_strdup_printf("**%s**", name); - *end = g_strdup(""); -} - -/** Convert an individual msim markup tag to HTML. */ -static void -msim_markup_tag_to_html(MsimSession *session, xmlnode *root, gchar **begin, - gchar **end) -{ - if (!strcmp(root->name, "f")) { - msim_markup_f_to_html(session, root, begin, end); - } else if (!strcmp(root->name, "a")) { - msim_markup_a_to_html(session, root, begin, end); - } else if (!strcmp(root->name, "p")) { - msim_markup_p_to_html(session, root, begin, end); - } else if (!strcmp(root->name, "c")) { - msim_markup_c_to_html(session, root, begin, end); - } else if (!strcmp(root->name, "b")) { - msim_markup_b_to_html(session, root, begin, end); - } else if (!strcmp(root->name, "i")) { - msim_markup_i_to_html(session, root, begin, end); - } else { - purple_debug_info("msim", "msim_markup_tag_to_html: " - "unknown tag name=%s, ignoring", - (root && root->name) ? root->name : "(NULL)"); - *begin = g_strdup(""); - *end = g_strdup(""); - } -} - -/** Convert an individual HTML tag to msim markup. */ -static void -html_tag_to_msim_markup(MsimSession *session, xmlnode *root, gchar **begin, - gchar **end) -{ - /* TODO: Coalesce nested tags into one <f> tag! - * Currently, the 's' value will be overwritten when b/i/u is nested - * within another one, and only the inner-most formatting will be - * applied to the text. */ - if (!strcmp(root->name, "root")) { - *begin = g_strdup(""); - *end = g_strdup(""); - } else if (!strcmp(root->name, "b") || !strcmp(root->name, "B")) { - *begin = g_strdup_printf("<f s='%d'>", MSIM_TEXT_BOLD); - *end = g_strdup("</f>"); - } else if (!strcmp(root->name, "i") || !strcmp(root->name, "I")) { - *begin = g_strdup_printf("<f s='%d'>", MSIM_TEXT_ITALIC); - *end = g_strdup("</f>"); - } else if (!strcmp(root->name, "u") || !strcmp(root->name, "U")) { - *begin = g_strdup_printf("<f s='%d'>", MSIM_TEXT_UNDERLINE); - *end = g_strdup("</f>"); - } else if (!strcmp(root->name, "a") || !strcmp(root->name, "A")) { - const gchar *href, *link_text; - - href = xmlnode_get_attrib(root, "href"); - - if (!href) { - href = xmlnode_get_attrib(root, "HREF"); - } - - link_text = xmlnode_get_data(root); - - if (href) { - if (!strcmp(link_text, href)) { - /* Purple gives us: <a href="URL">URL</a> - * Translate to <a h='URL' /> - * Displayed as text of URL with link to URL - */ - *begin = g_strdup_printf("<a h='%s' />", href); - } else { - /* But if we get: <a href="URL">text</a> - * Translate to: text: <a h='URL' /> - * - * Because official client only supports self-closed <a> - * tags; you can't change the link text. - */ - *begin = g_strdup_printf("%s: <a h='%s' />", link_text, href); - } - } else { - *begin = g_strdup("<a />"); - } - - /* Sorry, kid. MySpace doesn't support you within <a> tags. */ - xmlnode_free(root->child); - root->child = NULL; - - *end = g_strdup(""); - } else if (!strcmp(root->name, "font") || !strcmp(root->name, "FONT")) { - const gchar *size; - const gchar *face; - - size = xmlnode_get_attrib(root, "size"); - face = xmlnode_get_attrib(root, "face"); - - if (face && size) { - *begin = g_strdup_printf("<f f='%s' h='%d'>", face, - msim_point_to_height(session, - msim_purple_size_to_point(session, atoi(size)))); - } else if (face) { - *begin = g_strdup_printf("<f f='%s'>", face); - } else if (size) { - *begin = g_strdup_printf("<f h='%d'>", - msim_point_to_height(session, - msim_purple_size_to_point(session, atoi(size)))); - } else { - *begin = g_strdup("<f>"); - } - - *end = g_strdup("</f>"); - - /* TODO: color (bg uses <body>), emoticons */ - } else { - *begin = g_strdup_printf("[%s]", root->name); - *end = g_strdup_printf("[/%s]", root->name); - } -} - -/** Convert an xmlnode of msim markup or HTML to an HTML string or msim markup. - * - * @param f Function to convert tags. - * - * @return An HTML string. Caller frees. - */ -static gchar * -msim_convert_xmlnode(MsimSession *session, xmlnode *root, MSIM_XMLNODE_CONVERT f) -{ - xmlnode *node; - gchar *begin, *inner, *end; - GString *final; - - if (!root || !root->name) { - return g_strdup(""); - } - - purple_debug_info("msim", "msim_convert_xmlnode: got root=%s\n", - root->name); - - begin = inner = end = NULL; - - final = g_string_new(""); - - f(session, root, &begin, &end); - - g_string_append(final, begin); - - /* Loop over all child nodes. */ - for (node = root->child; node != NULL; node = node->next) { - switch (node->type) { - case XMLNODE_TYPE_ATTRIB: - /* Attributes handled above. */ - break; - - case XMLNODE_TYPE_TAG: - /* A tag or tag with attributes. Recursively descend. */ - inner = msim_convert_xmlnode(session, node, f); - g_return_val_if_fail(inner != NULL, NULL); - - purple_debug_info("msim", " ** node name=%s\n", - (node && node->name) ? node->name : "(NULL)"); - break; - - case XMLNODE_TYPE_DATA: - /* Literal text. */ - inner = g_new0(char, node->data_sz + 1); - strncpy(inner, node->data, node->data_sz); - inner[node->data_sz] = 0; - - purple_debug_info("msim", " ** node data=%s\n", - inner ? inner : "(NULL)"); - break; - - default: - purple_debug_info("msim", - "msim_convert_xmlnode: strange node\n"); - inner = g_strdup(""); - } - - if (inner) { - g_string_append(final, inner); - } - } - - /* TODO: Note that msim counts each piece of text enclosed by <f> as - * a paragraph and will display each on its own line. You actually have - * to _nest_ <f> tags to intersperse different text in one paragraph! - * Comment out this line below to see. */ - g_string_append(final, end); - - purple_debug_info("msim", "msim_markup_xmlnode_to_gtkhtml: RETURNING %s\n", - (final && final->str) ? final->str : "(NULL)"); - - return final->str; -} - -/** Convert XML to something based on MSIM_XMLNODE_CONVERT. */ -static gchar * -msim_convert_xml(MsimSession *session, const gchar *raw, MSIM_XMLNODE_CONVERT f) -{ - xmlnode *root; - gchar *str; - gchar *enclosed_raw; - - g_return_val_if_fail(raw != NULL, NULL); - - /* Enclose text in one root tag, to try to make it valid XML for parsing. */ - enclosed_raw = g_strconcat("<root>", raw, "</root>", NULL); - - root = xmlnode_from_str(enclosed_raw, -1); - - if (!root) { - purple_debug_info("msim", "msim_markup_to_html: couldn't parse " - "%s as XML, returning raw: %s\n", enclosed_raw, raw); - /* TODO: msim_unrecognized */ - g_free(enclosed_raw); - return g_strdup(raw); - } - - g_free(enclosed_raw); - - str = msim_convert_xmlnode(session, root, f); - g_return_val_if_fail(str != NULL, NULL); - purple_debug_info("msim", "msim_markup_to_html: returning %s\n", str); - - xmlnode_free(root); - - return str; -} - -/** Convert plaintext smileys to <i> markup tags. - * - * @param before Original text with ASCII smileys. Will be freed. - * @return A new string with <i> tags, if applicable. Must be g_free()'d. - */ -static gchar * -msim_convert_smileys_to_markup(gchar *before) -{ - gchar *old, *new, *replacement; - guint i; - struct MSIM_EMOTICON *emote; - - old = before; - new = NULL; - - for (i = 0; (emote = &msim_emoticons[i]) && emote->name != NULL; ++i) { - gchar *name, *symbol; - - name = emote->name; - symbol = emote->symbol; - - replacement = g_strdup_printf("<i n=\"%s\"/>", name); - - purple_debug_info("msim", "msim_convert_smileys_to_markup: %s->%s\n", - symbol ? symbol : "(NULL)", - replacement ? replacement : "(NULL)"); - new = str_replace(old, symbol, replacement); - - g_free(replacement); - g_free(old); - - old = new; - } - - return new; -} - - -/** High-level function to convert MySpaceIM markup to Purple (HTML) markup. - * - * @return Purple markup string, must be g_free()'d. */ -static gchar * -msim_markup_to_html(MsimSession *session, const gchar *raw) -{ - return msim_convert_xml(session, raw, - (MSIM_XMLNODE_CONVERT)(msim_markup_tag_to_html)); -} - -/** High-level function to convert Purple (HTML) to MySpaceIM markup. - * - * @return HTML markup string, must be g_free()'d. */ -static gchar * -html_to_msim_markup(MsimSession *session, const gchar *raw) -{ - gchar *markup; - - markup = msim_convert_xml(session, raw, - (MSIM_XMLNODE_CONVERT)(html_tag_to_msim_markup)); - - if (purple_account_get_bool(session->account, "emoticons", TRUE)) { - /* Frees markup and allocates a new one. */ - markup = msim_convert_smileys_to_markup(markup); - } - - return markup; -} - -/** Get the MsimUser from a PurpleBuddy, creating it if needed. */ -static MsimUser * -msim_get_user_from_buddy(PurpleBuddy *buddy) -{ - MsimUser *user; - - if (!buddy) { - return NULL; - } - - if (!buddy->proto_data) { - /* TODO: where is this freed? */ - user = g_new0(MsimUser, 1); - user->buddy = buddy; - buddy->proto_data = (gpointer)user; - purple_debug_info("msim_get_user_from_buddy", - "creating new user for %s to %X\n", - buddy->name, buddy->proto_data); - } - - user = (MsimUser *)(buddy->proto_data); - - return user; -} - -/** Find and return an MsimUser * representing a user on the buddy list, or NULL. */ -static MsimUser * -msim_find_user(MsimSession *session, const gchar *username) -{ - PurpleBuddy *buddy; - MsimUser *user; - - buddy = purple_find_buddy(session->account, username); - if (!buddy) { - return NULL; - } - - user = msim_get_user_from_buddy(buddy); - - return user; -} - - -/** Record the client version in the buddy list, from an incoming message. */ -static gboolean -msim_incoming_bm_record_cv(MsimSession *session, MsimMessage *msg) -{ - gchar *username, *cv; - gboolean ret; - MsimUser *user; - - username = msim_msg_get_string(msg, "_username"); - cv = msim_msg_get_string(msg, "cv"); - - g_return_val_if_fail(username != NULL, FALSE); - if (!cv) { - /* No client version to record, don't worry about it. */ - return FALSE; - } - - user = msim_find_user(session, username); - - if (user) { - user->client_cv = atol(cv); - ret = TRUE; - } else { - ret = FALSE; - } - - g_free(username); - g_free(cv); - - return ret; -} - -/** 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); - } -} - -/** - * Handle an incoming instant message. - * - * @param session The session - * @param msg Message from the server, containing 'f' (userid from) and 'msg'. - * Should also contain username in _username from preprocessing. - * - * @return TRUE if successful. - */ -static gboolean -msim_incoming_im(MsimSession *session, MsimMessage *msg) -{ - gchar *username, *msg_msim_markup, *msg_purple_markup; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - username = msim_msg_get_string(msg, "_username"); - g_return_val_if_fail(username != NULL, FALSE); - - msg_msim_markup = msim_msg_get_string(msg, "msg"); - g_return_val_if_fail(msg_msim_markup != NULL, FALSE); - - msg_purple_markup = msim_markup_to_html(session, msg_msim_markup); - g_free(msg_msim_markup); - - serv_got_im(session->gc, username, msg_purple_markup, - PURPLE_MESSAGE_RECV, time(NULL)); - - g_free(username); - g_free(msg_purple_markup); - - return TRUE; -} - -/** - * Process unrecognized information. - * - * @param session - * @param msg An MsimMessage that was unrecognized, or NULL. - * @param note Information on what was unrecognized, or NULL. - */ -static void -msim_unrecognized(MsimSession *session, MsimMessage *msg, gchar *note) -{ - /* TODO: Some more context, outwardly equivalent to a backtrace, - * for helping figure out what this msg is for. What was going on? - * But not too much information so that a user - * posting this dump reveals confidential information. - */ - - /* TODO: dump unknown msgs to file, so user can send them to me - * if they wish, to help add support for new messages (inspired - * by Alexandr Shutko, who maintains OSCAR protocol documentation). */ - - purple_debug_info("msim", "Unrecognized data on account for %s\n", - session->account->username ? 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); - } -} - -/** Process an incoming zap. */ -static gboolean -msim_incoming_zap(MsimSession *session, MsimMessage *msg) -{ - gchar *msg_text, *username, *zap_text; - gint zap; - const gchar *zap_past_tense[10]; - - zap_past_tense[0] = _("zapped"); - zap_past_tense[1] = _("whacked"); - zap_past_tense[2] = _("torched"); - zap_past_tense[3] = _("smooched"); - zap_past_tense[4] = _("hugged"); - zap_past_tense[5] = _("bslapped"); - zap_past_tense[6] = _("goosed"); - zap_past_tense[7] = _("hi-fived"); - zap_past_tense[8] = _("punk'd"); - zap_past_tense[9] = _("raspberried"); - - msg_text = msim_msg_get_string(msg, "msg"); - username = msim_msg_get_string(msg, "_username"); - - g_return_val_if_fail(msg_text != NULL, FALSE); - g_return_val_if_fail(username != NULL, FALSE); - - g_return_val_if_fail(sscanf(msg_text, "!!!ZAP_SEND!!!=RTE_BTN_ZAPS_%d", &zap) == 1, FALSE); - - zap = CLAMP(zap, 0, sizeof(zap_past_tense) / sizeof(zap_past_tense[0])); - - zap_text = g_strdup_printf(_("*** You have been %s! ***"), zap_past_tense[zap]); - - serv_got_im(session->gc, username, zap_text, - PURPLE_MESSAGE_RECV | PURPLE_MESSAGE_SYSTEM, time(NULL)); - - g_free(zap_text); - g_free(msg_text); - g_free(username); - - return TRUE; -} - -/** - * Handle an incoming action message. - * - * @param session - * @param msg - * - * @return TRUE if successful. - * - */ -static gboolean -msim_incoming_action(MsimSession *session, MsimMessage *msg) -{ - gchar *msg_text, *username; - gboolean rc; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - msg_text = msim_msg_get_string(msg, "msg"); - g_return_val_if_fail(msg_text != NULL, FALSE); - - username = msim_msg_get_string(msg, "_username"); - g_return_val_if_fail(username != NULL, FALSE); - - purple_debug_info("msim", "msim_incoming_action: action <%s> from <%d>\n", - msg_text, username); - - if (strcmp(msg_text, "%typing%") == 0) { - /* TODO: find out if msim repeatedly sends typing messages, so we can - * give it a timeout. Right now, there does seem to be an inordinately - * amount of time between typing stopped-typing notifications. */ - serv_got_typing(session->gc, username, 0, PURPLE_TYPING); - rc = TRUE; - } else if (strcmp(msg_text, "%stoptyping%") == 0) { - serv_got_typing_stopped(session->gc, username); - rc = TRUE; - } else if (strstr(msg_text, "!!!ZAP_SEND!!!=RTE_BTN_ZAPS_")) { - rc = msim_incoming_zap(session, msg); - } else { - msim_unrecognized(session, msg, - "got to msim_incoming_action but unrecognized value for 'msg'"); - rc = FALSE; - } - - g_free(msg_text); - g_free(username); - - return rc; -} - -/* Process an incoming media (buddy icon) message. */ -static gboolean -msim_incoming_media(MsimSession *session, MsimMessage *msg) -{ - gchar *username, *text; - - username = msim_msg_get_string(msg, "_username"); - text = msim_msg_get_string(msg, "msg"); - - g_return_val_if_fail(username != NULL, FALSE); - g_return_val_if_fail(text != NULL, FALSE); - - purple_debug_info("msim", "msim_incoming_media: from %s, got msg=%s\n", username, text); - - /* Media messages are sent when the user opens a window to someone. - * Tell libpurple they started typing and stopped typing, to inform the Psychic - * Mode plugin so it too can open a window to the user. */ - serv_got_typing(session->gc, username, 0, PURPLE_TYPING); - serv_got_typing_stopped(session->gc, username); - - g_free(username); - - return TRUE; -} - -/* 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) -{ - MsimUser *user; - gchar *username, *client_info; - - username = msim_msg_get_string(msg, "_username"); - client_info = msim_msg_get_string(msg, "msg"); - - g_return_val_if_fail(username != NULL, FALSE); - g_return_val_if_fail(client_info != NULL, FALSE); - - purple_debug_info("msim", "msim_incoming_unofficial_client: %s is using client %s\n", - username, client_info); - - user = msim_find_user(session, username); - - g_return_val_if_fail(user != NULL, FALSE); - - if (user->client_info) { - g_free(user->client_info); - } - user->client_info = client_info; - - g_free(username); - /* Do not free client_info - the MsimUser now owns it. */ - - return TRUE; -} - - -#ifdef MSIM_SEND_CLIENT_VERSION -/** Send our client version to another unofficial client that understands it. */ -static gboolean -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 - -/** - * Handle when our user starts or stops typing to another user. - * - * @param gc - * @param name The buddy name to which our user is typing to - * @param state PURPLE_TYPING, PURPLE_TYPED, PURPLE_NOT_TYPING - * - * @return 0 - */ -unsigned int -msim_send_typing(PurpleConnection *gc, const gchar *name, - PurpleTypingState state) -{ - const gchar *typing_str; - MsimSession *session; - - g_return_val_if_fail(gc != NULL, 0); - g_return_val_if_fail(name != NULL, 0); - - session = (MsimSession *)gc->proto_data; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); - - switch (state) { - case PURPLE_TYPING: - typing_str = "%typing%"; - break; - - case PURPLE_TYPED: - case PURPLE_NOT_TYPING: - default: - typing_str = "%stoptyping%"; - break; - } - - purple_debug_info("msim", "msim_send_typing(%s): %d (%s)\n", name, state, typing_str); - msim_send_bm(session, name, typing_str, MSIM_BM_ACTION); - return 0; -} - -/** Format the "now playing" indicator, showing the artist and song. - * @return Return a new string (must be g_free()'d), or NULL. - */ -static gchar * -msim_format_now_playing(gchar *band, gchar *song) -{ - if ((band && strlen(band)) || (song && strlen(song))) { - return g_strdup_printf("%s - %s", - (band && strlen(band)) ? band : "Unknown Artist", - (song && strlen(song)) ? song : "Unknown Song"); - } else { - return NULL; - } -} - -/** Append user information to a PurpleNotifyUserInfo, given an MsimUser. - * Used by msim_tooltip_text() and msim_get_info_cb() to show a user's profile. - */ -static void -msim_append_user_info(MsimSession *session, PurpleNotifyUserInfo *user_info, MsimUser *user, gboolean full) -{ - gchar *str; - guint uid; - guint cv; - - /* Useful to identify the account the tooltip refers to. - * Other prpls show this. */ - if (user->username) { - purple_notify_user_info_add_pair(user_info, _("User"), user->username); - } - - uid = purple_blist_node_get_int(&user->buddy->node, "UserID"); - - if (full) { - purple_notify_user_info_add_pair(user_info, _("User ID"), g_strdup_printf("%d", uid)); - } - - - /* a/s/l...the vitals */ - if (user->age) { - purple_notify_user_info_add_pair(user_info, _("Age"), - g_strdup_printf("%d", user->age)); - } - - if (user->gender && strlen(user->gender)) { - purple_notify_user_info_add_pair(user_info, _("Gender"), user->gender); - } - - if (user->location && strlen(user->location)) { - purple_notify_user_info_add_pair(user_info, _("Location"), user->location); - } - - /* Other information */ - if (user->headline && strlen(user->headline)) { - purple_notify_user_info_add_pair(user_info, _("Headline"), user->headline); - } - - str = msim_format_now_playing(user->band_name, user->song_name); - if (str && strlen(str)) { - purple_notify_user_info_add_pair(user_info, _("Song"), str); - } - - /* Note: total friends only available if looked up by uid, not username. */ - if (user->total_friends) { - purple_notify_user_info_add_pair(user_info, _("Total Friends"), - g_strdup_printf("%d", user->total_friends)); - } - - if (full) { - /* Client information */ - - str = user->client_info; - cv = user->client_cv; - - if (str && cv != 0) { - purple_notify_user_info_add_pair(user_info, _("Client Version"), - g_strdup_printf("%s (build %d)", str, cv)); - } else if (str) { - purple_notify_user_info_add_pair(user_info, _("Client Version"), - g_strdup(str)); - } else if (cv) { - purple_notify_user_info_add_pair(user_info, _("Client Version"), - g_strdup_printf("Build %d", cv)); - } - } -} - -/** Callback for msim_get_info(), for when user info is received. */ -static void -msim_get_info_cb(MsimSession *session, MsimMessage *user_info_msg, - gpointer data) -{ - MsimMessage *msg; - gchar *username; - PurpleNotifyUserInfo *user_info; - MsimUser *user; - gboolean temporary_user; - - g_return_if_fail(MSIM_SESSION_VALID(session)); - - /* Get user{name,id} from msim_get_info, passed as an MsimMessage for - orthogonality. */ - msg = (MsimMessage *)data; - g_return_if_fail(msg != NULL); - - username = msim_msg_get_string(msg, "user"); - if (!username) { - purple_debug_info("msim", "msim_get_info_cb: no 'user' in msg"); - return; - } - - msim_msg_free(msg); - purple_debug_info("msim", "msim_get_info_cb: got for user: %s\n", username); - - user = msim_find_user(session, username); - - if (!user) { - /* User isn't on blist, create a temporary user to store info. */ - temporary_user = TRUE; - user = g_new0(MsimUser, 1); - } else { - temporary_user = FALSE; - } - - /* Update user structure with new information */ - msim_store_user_info(session, user_info_msg, user); - - user_info = purple_notify_user_info_new(); - - /* Append data from MsimUser to PurpleNotifyUserInfo for display, full */ - msim_append_user_info(session, user_info, user, TRUE); - - purple_notify_userinfo(session->gc, username, user_info, NULL, NULL); - purple_debug_info("msim", "msim_get_info_cb: username=%s\n", username); - - purple_notify_user_info_destroy(user_info); - /* TODO: do not free username, since it will be used by user_info? */ - - if (temporary_user) { - g_free(user->client_info); - g_free(user->gender); - g_free(user->location); - g_free(user->headline); - g_free(user->display_name); - g_free(user->username); - g_free(user->band_name); - g_free(user->song_name); - g_free(user->image_url); - g_free(user); - } - -} - -/** Retrieve a user's profile. - * @param username Username, user ID, or email address to lookup. - */ -void -msim_get_info(PurpleConnection *gc, const gchar *username) -{ - MsimSession *session; - MsimUser *user; - guint uid; - gchar *user_to_lookup; - MsimMessage *user_msg; - - g_return_if_fail(gc != NULL); - g_return_if_fail(username != NULL); - - session = (MsimSession *)gc->proto_data; - - g_return_if_fail(MSIM_SESSION_VALID(session)); - - /* Obtain uid of buddy. */ - 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); - } 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 - * the username) but it would be useful to display the username too. - */ - user_msg = msim_msg_new( - "user", MSIM_TYPE_STRING, g_strdup(username), - NULL); - purple_debug_info("msim", "msim_get_info, setting up lookup, user=%s\n", username); - - msim_lookup_user(session, user_to_lookup, msim_get_info_cb, user_msg); - - g_free(user_to_lookup); -} - -/** Set your status - callback for when user manually sets it. */ -void -msim_set_status(PurpleAccount *account, PurpleStatus *status) -{ - PurpleStatusType *type; - MsimSession *session; - guint status_code; - const gchar *statstring; - - session = (MsimSession *)account->gc->proto_data; - - g_return_if_fail(MSIM_SESSION_VALID(session)); - - type = purple_status_get_type(status); - - switch (purple_status_type_get_primitive(type)) { - case PURPLE_STATUS_AVAILABLE: - purple_debug_info("msim", "msim_set_status: available (%d->%d)\n", PURPLE_STATUS_AVAILABLE, - MSIM_STATUS_CODE_ONLINE); - status_code = MSIM_STATUS_CODE_ONLINE; - break; - - case PURPLE_STATUS_INVISIBLE: - purple_debug_info("msim", "msim_set_status: invisible (%d->%d)\n", PURPLE_STATUS_INVISIBLE, - MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN); - status_code = MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN; - break; - - case PURPLE_STATUS_AWAY: - purple_debug_info("msim", "msim_set_status: away (%d->%d)\n", PURPLE_STATUS_AWAY, - MSIM_STATUS_CODE_AWAY); - status_code = MSIM_STATUS_CODE_AWAY; - break; - - default: - purple_debug_info("msim", "msim_set_status: unknown " - "status interpreting as online"); - status_code = MSIM_STATUS_CODE_ONLINE; - break; - } - - statstring = purple_status_get_attr_string(status, "message"); - - if (!statstring) { - statstring = g_strdup(""); - } - - msim_set_status_code(session, status_code, g_strdup(statstring)); -} - -/** Go idle. */ -void -msim_set_idle(PurpleConnection *gc, int time) -{ - MsimSession *session; - - g_return_if_fail(gc != NULL); - - session = (MsimSession *)gc->proto_data; - - g_return_if_fail(MSIM_SESSION_VALID(session)); - - if (time == 0) { - /* Going back from idle. In msim, idle is mutually exclusive - * from the other states (you can only be away or idle, but not - * both, for example), so by going non-idle I go online. - */ - /* TODO: find out how to keep old status string? */ - msim_set_status_code(session, MSIM_STATUS_CODE_ONLINE, g_strdup("")); - } else { - /* msim doesn't support idle time, so just go idle */ - msim_set_status_code(session, MSIM_STATUS_CODE_IDLE, g_strdup("")); - } -} - -/** 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"); - } - -} - -/** After a uid is resolved to username, tag it with the username and submit for processing. - * - * @param session - * @param userinfo Response messsage to resolving request. - * @param data MsimMessage *, the message to attach information to. - */ -static void -msim_incoming_resolved(MsimSession *session, MsimMessage *userinfo, - gpointer data) -{ - gchar *body_str; - GHashTable *body; - gchar *username; - MsimMessage *msg; - - g_return_if_fail(MSIM_SESSION_VALID(session)); - g_return_if_fail(userinfo != NULL); - - body_str = msim_msg_get_string(userinfo, "body"); - g_return_if_fail(body_str != NULL); - body = msim_parse_body(body_str); - g_return_if_fail(body != NULL); - g_free(body_str); - - username = g_hash_table_lookup(body, "UserName"); - g_return_if_fail(username != NULL); - - - msg = (MsimMessage *)data; - g_return_if_fail(msg != NULL); - - /* 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, g_strdup(username)); - - /* TODO: attach more useful information, like ImageURL */ - - msim_process(session, msg); - - /* TODO: Free copy cloned from msim_preprocess_incoming(). */ - //XXX msim_msg_free(msg); - g_hash_table_destroy(body); -} - -/* Lookup a username by userid, from buddy list. - * - * @param wanted_uid - * - * @return Username of wanted_uid, if on blist, or NULL. Static string. - * - */ -static const gchar * -msim_uid2username_from_blist(MsimSession *session, guint wanted_uid) -{ - GSList *buddies, *cur; - gchar *ret; - - buddies = purple_find_buddies(session->account, NULL); - - if (!buddies) - { - purple_debug_info("msim", "msim_uid2username_from_blist: no buddies?"); - 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 = g_strdup(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, 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(_("Connection to server lost (no data received within %d seconds)"), (int)delta); - - purple_debug_info("msim", "msim_check_alive: %s > interval of %d, presumed dead\n", - errmsg, MSIM_KEEPALIVE_INTERVAL); - purple_connection_error(session->gc, errmsg); - - purple_notify_error(session->gc, NULL, errmsg, NULL); - - g_free(errmsg); - - return FALSE; - } - - return TRUE; -} -#endif - -/** Handle mail reply checks. */ -static void -msim_check_inbox_cb(MsimSession *session, MsimMessage *reply, gpointer data) -{ - GHashTable *body; - gchar *body_str; - GString *notification; - guint old_inbox_status; - guint i, n; - const gchar *froms[5], *tos[5], *urls[5], *subjects[5]; - - /* Three parallel arrays for each new inbox message type. */ - static const gchar *inbox_keys[] = - { - "Mail", - "BlogComment", - "ProfileComment", - "FriendRequest", - "PictureComment" - }; - - static const guint inbox_bits[] = - { - MSIM_INBOX_MAIL, - MSIM_INBOX_BLOG_COMMENT, - MSIM_INBOX_PROFILE_COMMENT, - MSIM_INBOX_FRIEND_REQUEST, - MSIM_INBOX_PICTURE_COMMENT - }; - - static const gchar *inbox_urls[] = - { - "http://messaging.myspace.com/index.cfm?fuseaction=mail.inbox", - "http://blog.myspace.com/index.cfm?fuseaction=blog", - "http://home.myspace.com/index.cfm?fuseaction=user", - "http://messaging.myspace.com/index.cfm?fuseaction=mail.friendRequests", - "http://home.myspace.com/index.cfm?fuseaction=user" - }; - - static const gchar *inbox_text[5]; - - /* Can't write _()'d strings in array initializers. Workaround. */ - inbox_text[0] = _("New mail messages"); - inbox_text[1] = _("New blog comments"); - inbox_text[2] = _("New profile comments"); - inbox_text[3] = _("New friend requests!"); - inbox_text[4] = _("New picture comments"); - - g_return_if_fail(reply != NULL); - - msim_msg_dump("msim_check_inbox_cb: reply=%s\n", reply); - - body_str = msim_msg_get_string(reply, "body"); - g_return_if_fail(body_str != NULL); - - body = msim_parse_body(body_str); - g_free(body_str); - - notification = g_string_new(""); - - old_inbox_status = session->inbox_status; - - n = 0; - - for (i = 0; i < sizeof(inbox_keys) / sizeof(inbox_keys[0]); ++i) { - const gchar *key; - guint bit; - - key = inbox_keys[i]; - bit = inbox_bits[i]; - - if (g_hash_table_lookup(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] = inbox_text[i]; - 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] = inbox_urls[i]; - - ++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 */ - - } - - g_hash_table_destroy(body); -} - -/* Send request to check if there is new mail. */ -static gboolean -msim_check_inbox(gpointer data) -{ - MsimSession *session; - - session = (MsimSession *)data; - - 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; -} - -/** Called when the session key arrives. */ -static gboolean -msim_we_are_logged_on(MsimSession *session, MsimMessage *msg) -{ - MsimMessage *body; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != 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"); - - /* 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"); - - /* 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); - - - /* Additional post-connect operations */ - - - if (msim_msg_get_integer(msg, "uniquenick") == session->userid) { - purple_debug_info("msim_we_are_logged_on", "TODO: pick 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 - - purple_timeout_add(MSIM_MAIL_INTERVAL_CHECK, - (GSourceFunc)msim_check_inbox, session); - - msim_check_inbox(session); - - return TRUE; -} - -/** - * Process a message. - * - * @param session - * @param msg A message from the server, ready for processing (possibly with resolved username information attached). Caller frees. - * - * @return TRUE if successful. FALSE if processing failed. - */ -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); - } 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; - } -} - -/** Callback for when a buddy icon finished being downloaded. */ -static void -msim_downloaded_buddy_icon(PurpleUtilFetchUrlData *url_data, - gpointer user_data, - const gchar *url_text, - gsize len, - const gchar *error_message) -{ - MsimUser *user; - - user = (MsimUser *)user_data; - - purple_debug_info("msim_downloaded_buddy_icon", - "Downloaded %d bytes\n", len); - - purple_buddy_icons_set_for_user(user->buddy->account, - user->buddy->name, - (gchar *)url_text, len, - /* Use URL itself as buddy icon "checksum" */ - user->image_url); -} - -/** Store a field of information about a buddy. */ -static void -msim_store_user_info_each(gpointer key, gpointer value, gpointer user_data) -{ - MsimUser *user; - gchar *key_str, *value_str; - - user = (MsimUser *)user_data; - key_str = (gchar *)key; - value_str = (gchar *)value; - - if (!strcmp(key_str, "UserID")) { - /* Save to buddy list, if it exists, for quick cached uid lookup with msim_uid2username_from_blist(). */ - if (user->buddy) - { - purple_debug_info("msim", "associating uid %s with username %s\n", key_str, user->buddy->name); - purple_blist_node_set_int(&user->buddy->node, "UserID", atol(value_str)); - } - /* Need to store in MsimUser, too? What if not on blist? */ - } else if (!strcmp(key_str, "Age")) { - user->age = atol(value_str); - } else if (!strcmp(key_str, "Gender")) { - user->gender = g_strdup(value_str); - } else if (!strcmp(key_str, "Location")) { - user->location = g_strdup(value_str); - } else if (!strcmp(key_str, "TotalFriends")) { - user->total_friends = atol(value_str); - } else if (!strcmp(key_str, "DisplayName")) { - user->display_name = g_strdup(value_str); - } else if (!strcmp(key_str, "BandName")) { - user->band_name = g_strdup(value_str); - } else if (!strcmp(key_str, "SongName")) { - user->song_name = g_strdup(value_str); - } else if (!strcmp(key_str, "UserName")) { - /* Ignore because PurpleBuddy knows this already */ - ; - } else if (!strcmp(key_str, "ImageURL")) { - const gchar *previous_url; - - user->image_url = g_strdup(value_str); - - previous_url = purple_buddy_icons_get_checksum_for_user(user->buddy); - - /* Only download if URL changed */ - if (!previous_url || strcmp(previous_url, user->image_url)) { - purple_util_fetch_url(user->image_url, TRUE, NULL, TRUE, msim_downloaded_buddy_icon, (gpointer)user); - } - } else { - /* TODO: other fields in MsimUser */ - gchar *msg; - - msg = g_strdup_printf("msim_store_user_info_each: unknown field %s=%s", - key_str, value_str); - - msim_unrecognized(NULL, NULL, msg); - - g_free(msg); - } -} - -/** Save buddy information to the buddy list from a user info reply message. - * - * @param session - * @param msg The user information reply, with any amount of information. - * @param user The structure to save to, or NULL to save in PurpleBuddy->proto_data. - * - * Variable information is saved to the passed MsimUser structure. Permanent - * information (UserID) is stored in the blist node of the buddy list (and - * ends up in blist.xml, persisted to disk) if it exists. - * - * If the function has no buddy information, this function - * is a no-op (and returns FALSE). - * - */ -static gboolean -msim_store_user_info(MsimSession *session, MsimMessage *msg, MsimUser *user) -{ - GHashTable *body; - gchar *username, *body_str; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - body_str = msim_msg_get_string(msg, "body"); - if (!body_str) { - return FALSE; - } - - g_return_val_if_fail(body_str != NULL, FALSE); - body = msim_parse_body(body_str); - g_free(body_str); - - - /* TODO: implement a better hash-like interface, and use it. */ - username = g_hash_table_lookup(body, "UserName"); - - if (!username) { - purple_debug_info("msim", - "msim_process_reply: not caching body, no UserName\n"); - g_hash_table_destroy(body); - return FALSE; - } - - /* Null user = find and store in PurpleBuddy's proto_data */ - if (!user) { - user = msim_find_user(session, username); - g_return_val_if_fail(user != NULL, FALSE); - } - - g_hash_table_foreach(body, msim_store_user_info_each, user); - - if (msim_msg_get_integer(msg, "dsn") == MG_OWN_IM_INFO_DSN && - msim_msg_get_integer(msg, "lid") == MG_OWN_IM_INFO_LID) { - /* TODO: do something with our own IM info, if we need it for some - * specific purpose. Otherwise it is available on the buddy list, - * if the user has themselves as their own buddy. - * - * However, much of the info is already available in MsimSession, - * stored in msim_we_are_logged_on(). */ - } else if (msim_msg_get_integer(msg, "dsn") == MG_OWN_MYSPACE_INFO_DSN && - msim_msg_get_integer(msg, "lid") == MG_OWN_MYSPACE_INFO_LID) { - /* TODO: same as above, but for MySpace info. */ - } - - g_hash_table_destroy(body); - - return TRUE; -} - -/** Process the initial server information from the server. */ -static gboolean -msim_process_server_info(MsimSession *session, MsimMessage *msg) -{ - gchar *body_str; - GHashTable *body; - - body_str = msim_msg_get_string(msg, "body"); - g_return_val_if_fail(body_str != NULL, FALSE); - body = msim_parse_body(body_str); - g_free(body_str); - 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: %s\n", - g_hash_table_lookup(body, "MaxContacts") ? - g_hash_table_lookup(body, "MaxContacts") : "(NULL)"); - - 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 */ - 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_body: calling callback now\n"); - /* Clone message, so that the callback 'cb' can use it (needs to free it also). */ - cb(session, msim_msg_clone(msg), data); - g_hash_table_remove(session->user_lookup_cb, GUINT_TO_POINTER(rid)); - g_hash_table_remove(session->user_lookup_cb_data, GUINT_TO_POINTER(rid)); - } else { - purple_debug_info("msim", - "msim_process_body: no callback for rid %d\n", rid); - } - - return TRUE; -} - -/** - * Handle an error from the server. - * - * @param session - * @param msg The message. - * - * @return TRUE if successfully reported error. - */ -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); - - purple_notify_error(session->account, g_strdup(_("MySpaceIM Error")), - full_errmsg, NULL); - - /* Destroy session if fatal. */ - if (msim_msg_get(msg, "fatal")) { - purple_debug_info("msim", "fatal error, closing\n"); - purple_connection_error(session->gc, full_errmsg); - } - - return TRUE; -} - -/** - * Process incoming status messages. - * - * @param session - * @param msg Status update message. Caller frees. - * - * @return TRUE if successful. - */ -static gboolean -msim_incoming_status(MsimSession *session, MsimMessage *msg) -{ - PurpleBuddyList *blist; - MsimUser *user; - GList *list; - gchar *status_headline; - gint status_code, purple_status_code; - gchar *username; - - g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); - g_return_val_if_fail(msg != NULL, FALSE); - - 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 = atoi(g_list_nth_data(list, MSIM_STATUS_ORDINAL_ONLINE)); - purple_debug_info("msim", "msim_status: %s's status code = %d\n", username, status_code); - status_headline = g_list_nth_data(list, MSIM_STATUS_ORDINAL_HEADLINE); - - blist = purple_get_blist(); - - /* Add buddy if not found */ - 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); - - /* All buddies on list should have 'uid' integer associated with them. */ - purple_blist_node_set_int(&buddy->node, "UserID", msim_msg_get_integer(msg, "f")); - - msim_store_user_info(session, msg, NULL); - } else { - purple_debug_info("msim", "msim_status: found buddy %s\n", username); - } - - user->headline = g_strdup(status_headline); - - /* Set user status */ - switch (status_code) { - case MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN: - purple_status_code = PURPLE_STATUS_OFFLINE; - break; - - case MSIM_STATUS_CODE_ONLINE: - purple_status_code = PURPLE_STATUS_AVAILABLE; - break; - - case MSIM_STATUS_CODE_AWAY: - purple_status_code = PURPLE_STATUS_AWAY; - break; - - case MSIM_STATUS_CODE_IDLE: - /* will be handled below */ - purple_status_code = -1; - break; - - default: - purple_debug_info("msim", "msim_status for %s, unknown status code %d, treating as available\n", - username, status_code); - purple_status_code = PURPLE_STATUS_AVAILABLE; - } - - 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 - - g_free(username); - msim_msg_list_free(list); - - return TRUE; -} - -/** Add a buddy to user's buddy list. */ -void -msim_add_buddy(PurpleConnection *gc, PurpleBuddy *buddy, PurpleGroup *group) -{ - MsimSession *session; - MsimMessage *msg; - /* MsimMessage *msg_persist; */ - MsimMessage *body; - - session = (MsimSession *)gc->proto_data; - 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( - "addbuddy", MSIM_TYPE_BOOLEAN, TRUE, - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - /* "newprofileid" will be inserted here with uid. */ - "reason", MSIM_TYPE_STRING, g_strdup(""), - NULL); - - if (!msim_postprocess_outgoing(session, msg, buddy->name, "newprofileid", "reason")) { - purple_notify_error(NULL, NULL, _("Failed to add buddy"), _("'addbuddy' command failed.")); - msim_msg_free(msg); - return; - } - msim_msg_free(msg); - - /* TODO: if addbuddy fails ('error' message is returned), delete added buddy from - * buddy list since Purple adds it locally. */ - - body = msim_msg_new( - "ContactID", MSIM_TYPE_STRING, g_strdup("<uid>"), - "GroupName", MSIM_TYPE_STRING, g_strdup(group->name), - "Position", MSIM_TYPE_INTEGER, 1000, - "Visibility", MSIM_TYPE_INTEGER, 1, - "NickName", MSIM_TYPE_STRING, g_strdup(""), - "NameSelect", MSIM_TYPE_INTEGER, 0, - NULL); - - /* TODO: Update blocklist. */ - -#if 0 - msg_persist = msim_msg_new( - "persist", MSIM_TYPE_INTEGER, 1, - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - "cmd", MSIM_TYPE_INTEGER, MSIM_CMD_BIT_ACTION | MSIM_CMD_PUT, - "dsn", MSIM_TYPE_INTEGER, MC_CONTACT_INFO_DSN, - "lid", MSIM_TYPE_INTEGER, MC_CONTACT_INFO_LID, - /* TODO: Use msim_new_reply_callback to get rid. */ - "rid", MSIM_TYPE_INTEGER, session->next_rid++, - "body", MSIM_TYPE_DICTIONARY, body, - NULL); - - if (!msim_postprocess_outgoing(session, msg_persist, buddy->name, "body", NULL)) - { - purple_notify_error(NULL, NULL, _("Failed to add buddy"), _("persist command failed")); - msim_msg_free(msg_persist); - return; - } - msim_msg_free(msg_persist); -#endif - -} - -/** Perform actual postprocessing on a message, adding userid as specified. - * - * @param msg The message to postprocess. - * @param uid_before Name of field where to insert new field before, or NULL for end. - * @param uid_field_name Name of field to add uid to. - * @param uid The userid to insert. - * - * If the field named by uid_field_name already exists, then its string contents will - * be used for the field, except "<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) -{ - msim_msg_dump("msim_do_postprocessing msg: %s\n", msg); - - /* First, check - if the field already exists, replace <uid> within it */ - if (msim_msg_get(msg, uid_field_name)) { - MsimMessageElement *elem; - gchar *fmt_string; - gchar *uid_str, *new_str; - - /* Warning: this is a delicate, but safe, operation */ - - elem = msim_msg_get(msg, uid_field_name); - - /* 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 = str_replace(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; -} - -/** 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 *body_str; - GHashTable *body; - gchar *uid, *uid_field_name, *uid_before; - MsimMessage *msg; - - msg = (MsimMessage *)data; - - msim_msg_dump("msim_postprocess_outgoing_cb() got msg=%s\n", msg); - - /* Obtain userid from userinfo message. */ - body_str = msim_msg_get_string(userinfo, "body"); - g_return_if_fail(body_str != NULL); - body = msim_parse_body(body_str); - g_free(body_str); - - uid = g_strdup(g_hash_table_lookup(body, "UserID")); - g_hash_table_destroy(body); - - uid_field_name = msim_msg_get_string(msg, "_uid_field_name"); - uid_before = msim_msg_get_string(msg, "_uid_before"); - - msg = msim_do_postprocessing(msg, uid_before, uid_field_name, atol(uid)); - - /* Send */ - if (!msim_msg_send(session, msg)) { - 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_hash_table_destroy(body); - - //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. - */ -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); - - //msim_msg_free(msg); - - return rc; -} - -/** Remove a buddy from the user's buddy list. */ -void -msim_remove_buddy(PurpleConnection *gc, PurpleBuddy *buddy, PurpleGroup *group) -{ - MsimSession *session; - MsimMessage *delbuddy_msg; - MsimMessage *persist_msg; - MsimMessage *blocklist_msg; - GList *blocklist_updates; - - session = (MsimSession *)gc->proto_data; - - delbuddy_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, delbuddy_msg, buddy->name, "delprofileid", NULL)) { - purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("'delbuddy' command failed")); - msim_msg_free(delbuddy_msg); - return; - } - msim_msg_free(delbuddy_msg); - - persist_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, MD_DELETE_BUDDY_DSN, - "lid", MSIM_TYPE_INTEGER, MD_DELETE_BUDDY_LID, - "uid", MSIM_TYPE_INTEGER, session->userid, - "rid", MSIM_TYPE_INTEGER, session->next_rid++, - /* <uid> will be replaced by postprocessing */ - "body", MSIM_TYPE_STRING, g_strdup("ContactID=<uid>"), - NULL); - - if (!msim_postprocess_outgoing(session, persist_msg, buddy->name, "body", NULL)) { - purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("persist command failed")); - msim_msg_free(persist_msg); - return; - } - 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, - "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, - 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); -} - -/** 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(gc, _("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 don't timeout. */ - session->last_comm = time(NULL); - - /* Only can handle so much data at once... - * If this happens, try recompiling with a higher MSIM_READ_BUF_SIZE. - * Should be large enough to hold the largest protocol message. - */ - if (session->rxoff >= MSIM_READ_BUF_SIZE) { - purple_debug_error("msim", - "msim_input_cb: %d-byte read buffer full! rxoff=%d\n", - MSIM_READ_BUF_SIZE, session->rxoff); - purple_connection_error(gc, _("Read buffer full")); - return; - } - - purple_debug_info("msim", "buffer at %d (max %d), reading up to %d\n", - session->rxoff, MSIM_READ_BUF_SIZE, - MSIM_READ_BUF_SIZE - session->rxoff); - - /* Read into buffer. On Win32, need recv() not read(). session->fd also holds - * the file descriptor, but it sometimes differs from the 'source' parameter. - */ - n = recv(session->fd, session->rxbuf + session->rxoff, MSIM_READ_BUF_SIZE - session->rxoff, 0); - - if (n < 0 && errno == EAGAIN) { - return; - } else if (n < 0) { - purple_debug_error("msim", "msim_input_cb: read error, ret=%d, " - "error=%s, source=%d, fd=%d (%X))\n", - n, strerror(errno), source, session->fd, session->fd); - purple_connection_error(gc, _("Read error")); - return; - } else if (n == 0) { - purple_debug_info("msim", "msim_input_cb: server disconnected\n"); - purple_connection_error(gc, _("Server has disconnected")); - return; - } - - if (n + session->rxoff >= MSIM_READ_BUF_SIZE) { - purple_debug_info("msim_input_cb", "received %d bytes, pushing rxoff to %d, over buffer size of %d\n", - n, n + session->rxoff, MSIM_READ_BUF_SIZE); - /* TODO: g_realloc like msn, yahoo, irc, jabber? */ - purple_connection_error(gc, _("Read buffer full")); - } - - /* 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(gc, "Invalid message - null byte on input"); - return; - } -#endif - - session->rxoff += n; - purple_debug_info("msim", "msim_input_cb: read=%d\n", n); - -#ifdef MSIM_DEBUG_RXBUF - purple_debug_info("msim", "buf=<%s>\n", session->rxbuf); -#endif - - /* Look for \\final\\ end markers. If found, process message. */ - while((end = strstr(session->rxbuf, MSIM_FINAL_STRING))) { - MsimMessage *msg; - -#ifdef MSIM_DEBUG_RXBUF - purple_debug_info("msim", "in loop: buf=<%s>\n", session->rxbuf); -#endif - *end = 0; - msg = msim_parse(g_strdup(session->rxbuf)); - if (!msg) { - purple_debug_info("msim", "msim_input_cb: couldn't parse rxbuf\n"); - purple_connection_error(gc, _("Unparseable message")); - } else { - /* Process message and then free it (processing function should - * clone message if it wants to keep it afterwards.) */ - if (!msim_preprocess_incoming(session, msg)) { - msim_msg_dump("msim_input_cb: preprocessing message failed on msg: %s\n", msg); - } - msim_msg_free(msg); - } - - /* Move remaining part of buffer to beginning. */ - session->rxoff -= strlen(session->rxbuf) + strlen(MSIM_FINAL_STRING); - memmove(session->rxbuf, end + strlen(MSIM_FINAL_STRING), - MSIM_READ_BUF_SIZE - (end + strlen(MSIM_FINAL_STRING) - session->rxbuf)); - - /* Clear end of buffer */ - //memset(end, 0, MSIM_READ_BUF_SIZE - (end - session->rxbuf)); - } -} - -/* Setup a callback, to be called when a reply is received with the returned rid. - * - * @param cb The callback, an MSIM_USER_LOOKUP_CB. - * @param data Arbitrary user data to be passed to callback (probably an MsimMessage *). - * - * @return The request/reply ID, used to link replies with requests, 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? - */ -static 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(gc, _("Couldn't connect to host")); - purple_connection_error(gc, g_strdup_printf( - _("Couldn't connect to host: %s (%d)"), - error_message ? error_message : "no message given", - source)); - return; - } - - session->fd = source; - - gc->inpa = purple_input_add(source, PURPLE_INPUT_READ, msim_input_cb, gc); -} - -/* Session methods */ - -/** - * Create a new MSIM session. - * - * @param acct The account to create the session from. - * - * @return Pointer to a new session. Free with msim_session_destroy. - */ -MsimSession * -msim_session_new(PurpleAccount *acct) -{ - MsimSession *session; - - g_return_val_if_fail(acct != NULL, NULL); - - session = g_new0(MsimSession, 1); - - session->magic = MSIM_SESSION_STRUCT_MAGIC; - session->account = acct; - session->gc = purple_account_get_connection(acct); - session->sesskey = 0; - session->userid = 0; - session->username = NULL; - session->fd = -1; - - /* TODO: Remove. */ - session->user_lookup_cb = g_hash_table_new_full(g_direct_hash, - g_direct_equal, NULL, NULL); /* do NOT free function pointers! (values) */ - session->user_lookup_cb_data = g_hash_table_new_full(g_direct_hash, - g_direct_equal, NULL, NULL);/* TODO: we don't know what the values are, - they could be integers inside gpointers - or strings, so I don't freed them. - Figure this out, once free cache. */ - - /* Created in msim_process_server_info() */ - session->server_info = NULL; - - session->rxoff = 0; - session->rxbuf = g_new0(gchar, MSIM_READ_BUF_SIZE); - session->next_rid = 1; - session->last_comm = time(NULL); - session->inbox_status = 0; - - return session; -} - -/** - * Free a session. - * - * @param session The session to destroy. - */ -void -msim_session_destroy(MsimSession *session) -{ - g_return_if_fail(MSIM_SESSION_VALID(session)); - - session->magic = -1; - - g_free(session->rxbuf); - g_free(session->username); - - /* TODO: Remove. */ - g_hash_table_destroy(session->user_lookup_cb); - g_hash_table_destroy(session->user_lookup_cb_data); - - if (session->server_info) { - g_hash_table_destroy(session->server_info); - } - - g_free(session); -} - -/** - * Close the connection. - * - * @param gc The connection. - */ -void -msim_close(PurpleConnection *gc) -{ - MsimSession *session; - - if (gc == NULL) { - return; - } - - session = (MsimSession *)gc->proto_data; - if (session == NULL) - return; - - gc->proto_data = NULL; - - if (!MSIM_SESSION_VALID(session)) { - return; - } - - if (session->gc->inpa) { - purple_input_remove(session->gc->inpa); - } - - msim_session_destroy(session); -} - - -/** - * Check if a string is a userid (all numeric). - * - * @param user The user id, email, or name. - * - * @return TRUE if is userid, FALSE if not. - */ -static gboolean -msim_is_userid(const gchar *user) -{ - g_return_val_if_fail(user != NULL, FALSE); - - return strspn(user, "0123456789") == strlen(user); -} - -/** - * Check if a string is an email address (contains an @). - * - * @param user The user id, email, or name. - * - * @return TRUE if is an email, FALSE if not. - * - * This function is not intended to be used as a generic - * means of validating email addresses, but to distinguish - * between a user represented by an email address from - * other forms of identification. - */ -static gboolean -msim_is_email(const gchar *user) -{ - g_return_val_if_fail(user != NULL, FALSE); - - return strchr(user, '@') != NULL; -} - - -/** - * Asynchronously lookup user information, calling callback when receive result. - * - * @param session - * @param user The user id, email address, or username. Not freed. - * @param cb Callback, called with user information when available. - * @param data An arbitray data pointer passed to the callback. - */ -/* TODO: change to not use callbacks */ -static void -msim_lookup_user(MsimSession *session, const gchar *user, - MSIM_USER_LOOKUP_CB cb, gpointer data) -{ - MsimMessage *body; - gchar *field_name; - guint rid, cmd, dsn, lid; - - g_return_if_fail(MSIM_SESSION_VALID(session)); - g_return_if_fail(user != NULL); - g_return_if_fail(cb != NULL); - - purple_debug_info("msim", "msim_lookup_userid: " - "asynchronously looking up <%s>\n", user); - - msim_msg_dump("msim_lookup_user: data=%s\n", (MsimMessage *)data); - - /* Setup callback. Response will be associated with request using 'rid'. */ - rid = msim_new_reply_callback(session, cb, data); - - /* Send request */ - - cmd = MSIM_CMD_GET; - - if (msim_is_userid(user)) { - field_name = "UserID"; - dsn = MG_MYSPACE_INFO_BY_ID_DSN; - lid = MG_MYSPACE_INFO_BY_ID_LID; - } else if (msim_is_email(user)) { - field_name = "Email"; - dsn = MG_MYSPACE_INFO_BY_STRING_DSN; - lid = MG_MYSPACE_INFO_BY_STRING_LID; - } else { - field_name = "UserName"; - dsn = MG_MYSPACE_INFO_BY_STRING_DSN; - lid = MG_MYSPACE_INFO_BY_STRING_LID; - } - - body = msim_msg_new( - field_name, MSIM_TYPE_STRING, g_strdup(user), - NULL); - - g_return_if_fail(msim_send(session, - "persist", MSIM_TYPE_INTEGER, 1, - "sesskey", MSIM_TYPE_INTEGER, session->sesskey, - "cmd", MSIM_TYPE_INTEGER, 1, - "dsn", MSIM_TYPE_INTEGER, dsn, - "uid", MSIM_TYPE_INTEGER, session->userid, - "lid", MSIM_TYPE_INTEGER, lid, - "rid", MSIM_TYPE_INTEGER, rid, - "body", MSIM_TYPE_DICTIONARY, body, - NULL)); -} - - -/** - * Obtain the status text for a buddy. - * - * @param buddy The buddy to obtain status text for. - * - * @return Status text, or NULL if error. Caller g_free()'s. - * - */ -char * -msim_status_text(PurpleBuddy *buddy) -{ - MsimSession *session; - MsimUser *user; - const gchar *display_name, *headline; - - g_return_val_if_fail(buddy != NULL, NULL); - - user = msim_get_user_from_buddy(buddy); - - session = (MsimSession *)buddy->account->gc->proto_data; - g_return_val_if_fail(MSIM_SESSION_VALID(session), NULL); - - display_name = headline = NULL; - - /* Retrieve display name and/or headline, depending on user preference. */ - if (purple_account_get_bool(session->account, "show_display_name", TRUE)) { - display_name = user->display_name; - } - - if (purple_account_get_bool(session->account, "show_headline", FALSE)) { - headline = user->headline; - } - - /* 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); - - 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); - } -} - -/** 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(_("Import friends..."), NULL); - menu = g_list_append(menu, act); - - act = purple_plugin_action_new(_("Change IM name..."), NULL); - menu = g_list_append(menu, act); -#endif - - return menu; -} - -/** Callbacks called by Purple, to access this plugin. */ -PurplePluginProtocolInfo prpl_info = { - /* options */ - OPT_PROTO_USE_POINTSIZE /* specify font size in sane point size */ - | OPT_PROTO_MAIL_CHECK, - - /* | OPT_PROTO_IM_IMAGE - TODO: direct images. */ - NULL, /* user_splits */ - NULL, /* protocol_options */ - NO_BUDDY_ICONS, /* icon_spec - TODO: eventually should add this */ - msim_list_icon, /* list_icon */ - NULL, /* list_emblems */ - msim_status_text, /* status_text */ - msim_tooltip_text, /* tooltip_text */ - msim_status_types, /* status_types */ - msim_blist_node_menu, /* blist_node_menu */ - NULL, /* chat_info */ - NULL, /* chat_info_defaults */ - msim_login, /* login */ - msim_close, /* close */ - msim_send_im, /* send_im */ - NULL, /* set_info */ - msim_send_typing, /* send_typing */ - msim_get_info, /* get_info */ - msim_set_status, /* set_status */ - msim_set_idle, /* set_idle */ - NULL, /* change_passwd */ - msim_add_buddy, /* add_buddy */ - NULL, /* add_buddies */ - msim_remove_buddy, /* remove_buddy */ - NULL, /* remove_buddies */ - NULL, /* add_permit */ - NULL, /* add_deny */ - NULL, /* rem_permit */ - NULL, /* rem_deny */ - NULL, /* set_permit_deny */ - NULL, /* join_chat */ - NULL, /* reject chat invite */ - NULL, /* get_chat_name */ - NULL, /* chat_invite */ - NULL, /* chat_leave */ - NULL, /* chat_whisper */ - NULL, /* chat_send */ - NULL, /* keepalive */ - NULL, /* register_user */ - NULL, /* get_cb_info */ - NULL, /* get_cb_away */ - NULL, /* alias_buddy */ - NULL, /* group_buddy */ - NULL, /* rename_group */ - NULL, /* buddy_free */ - NULL, /* convo_closed */ - NULL, /* normalize */ - NULL, /* set_buddy_icon */ - NULL, /* remove_group */ - NULL, /* get_cb_real_name */ - NULL, /* set_chat_topic */ - NULL, /* find_blist_chat */ - NULL, /* roomlist_get_list */ - NULL, /* roomlist_cancel */ - NULL, /* roomlist_expand_category */ - NULL, /* can_receive_file */ - NULL, /* send_file */ - NULL, /* new_xfer */ - msim_offline_message, /* offline_message */ - NULL, /* whiteboard_prpl_ops */ - msim_send_really_raw, /* send_raw */ - NULL, /* roomlist_room_serialize */ - NULL, /* _purple_reserved1 */ - NULL, /* _purple_reserved2 */ - NULL, /* _purple_reserved3 */ - NULL /* _purple_reserved4 */ -}; - - - -/** Based on MSN's plugin info comments. */ -PurplePluginInfo info = { - PURPLE_PLUGIN_MAGIC, - PURPLE_MAJOR_VERSION, - PURPLE_MINOR_VERSION, - PURPLE_PLUGIN_PROTOCOL, /**< type */ - NULL, /**< ui_requirement */ - 0, /**< flags */ - NULL, /**< dependencies */ - PURPLE_PRIORITY_DEFAULT, /**< priority */ - - "prpl-myspace", /**< id */ - "MySpaceIM", /**< name */ - MSIM_PRPL_VERSION_STRING, /**< version */ - /** summary */ - "MySpaceIM Protocol Plugin", - /** description */ - "MySpaceIM Protocol Plugin", - "Jeff Connelly <jeff2@soc.pidgin.im>", /**< author */ - "http://developer.pidgin.im/wiki/MySpaceIM/", /**< homepage */ - - msim_load, /**< load */ - NULL, /**< unload */ - NULL, /**< destroy */ - NULL, /**< ui_info */ - &prpl_info, /**< extra_info */ - NULL, /**< prefs_info */ - msim_actions, /**< msim_actions */ - NULL, /**< reserved1 */ - NULL, /**< reserved2 */ - NULL, /**< reserved3 */ - NULL /**< reserved4 */ -}; - - -#ifdef MSIM_SELF_TEST -/** Test functions. - * Used to test or try out the internal workings of msimprpl. If you're reading - * this code for the first time, these functions can be instructive in 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 -msim_test_msg(void) -{ - MsimMessage *msg, *msg_cloned, *msg2; - GList *list; - gchar *packed, *packed_expected, *packed_cloned; - guint failures; - - failures = 0; - - purple_debug_info("msim", "\n\nTesting MsimMessage\n"); - msg = msim_msg_new(NULL); /* Create a new, empty message. */ - - /* Append some new elements. */ - msg = msim_msg_append(msg, "bx", MSIM_TYPE_BINARY, g_string_new_len(g_strdup("XXX"), 3)); - msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v1")); - msg = msim_msg_append(msg, "k1", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(42)); - msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v43")); - msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v52/xxx\\yyy")); - msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v7")); - msim_msg_dump("msg debug str=%s\n", msg); - packed = msim_msg_pack(msg); - - purple_debug_info("msim", "msg packed=%s\n", packed); - - packed_expected = "\\bx\\WFhY\\k1\\v1\\k1\\42\\k1" - "\\v43\\k1\\v52/1xxx/2yyy\\k1\\v7\\final\\"; - - if (0 != strcmp(packed, packed_expected)) { - purple_debug_info("msim", "!!!(%d), msim_msg_pack not what expected: %s != %s\n", - ++failures, packed, packed_expected); - } - - - msg_cloned = msim_msg_clone(msg); - packed_cloned = msim_msg_pack(msg_cloned); - - purple_debug_info("msim", "msg cloned=%s\n", packed_cloned); - if (0 != strcmp(packed, packed_cloned)) { - purple_debug_info("msim", "!!!(%d), msim_msg_pack on cloned message not equal to original: %s != %s\n", - ++failures, packed_cloned, packed); - } - - g_free(packed); - g_free(packed_cloned); - msim_msg_free(msg_cloned); - msim_msg_free(msg); - - /* Try some of the more advanced functionality */ - list = NULL; - - list = g_list_prepend(list, "item3"); - list = g_list_prepend(list, "item2"); - list = g_list_prepend(list, "item1"); - list = g_list_prepend(list, "item0"); - - msg = msim_msg_new(NULL); - msg = msim_msg_append(msg, "string", MSIM_TYPE_STRING, g_strdup("string value")); - msg = msim_msg_append(msg, "raw", MSIM_TYPE_RAW, g_strdup("raw value")); - msg = msim_msg_append(msg, "integer", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(3140)); - msg = msim_msg_append(msg, "boolean", MSIM_TYPE_BOOLEAN, GUINT_TO_POINTER(FALSE)); - msg = msim_msg_append(msg, "list", MSIM_TYPE_LIST, list); - - msim_msg_dump("msg with list=%s\n", msg); - purple_debug_info("msim", "msg with list packed=%s\n", msim_msg_pack(msg)); - - msg2 = msim_msg_new(NULL); - msg2 = msim_msg_append(msg2, "outer", MSIM_TYPE_STRING, g_strdup("outer value")); - msg2 = msim_msg_append(msg2, "body", MSIM_TYPE_DICTIONARY, msg); - msim_msg_dump("msg with dict=%s\n", msg2); /* msg2 now 'owns' msg */ - purple_debug_info("msim", "msg with dict packed=%s\n", msim_msg_pack(msg2)); - - msim_msg_free(msg2); - - return failures; -} - -/** Test protocol-level escaping/unescaping. */ -int -msim_test_escaping(void) -{ - guint failures; - gchar *raw, *escaped, *unescaped, *expected; - - failures = 0; - - purple_debug_info("msim", "\n\nTesting escaping\n"); - - raw = "hello/world\\hello/world"; - - escaped = msim_escape(raw); - purple_debug_info("msim", "msim_test_escaping: raw=%s, escaped=%s\n", raw, escaped); - expected = "hello/1world/2hello/1world"; - if (0 != strcmp(escaped, expected)) { - purple_debug_info("msim", "!!!(%d), msim_escape failed: %s != %s\n", - ++failures, escaped, expected); - } - - - unescaped = msim_unescape(escaped); - g_free(escaped); - purple_debug_info("msim", "msim_test_escaping: unescaped=%s\n", unescaped); - if (0 != strcmp(raw, unescaped)) { - purple_debug_info("msim", "!!!(%d), msim_unescape failed: %s != %s\n", - ++failures, raw, unescaped); - } - - return failures; -} -#endif - -/** Initialize plugin. */ -void -init_plugin(PurplePlugin *plugin) -{ - PurpleAccountOption *option; -#ifdef MSIM_SELF_TEST - msim_test_all(); - exit(0); -#endif /* MSIM_SELF_TEST */ - - - /* TODO: default to automatically try different ports. Make the user be - * able to set the first port to try (like LastConnectedPort in Windows client). */ - option = purple_account_option_string_new(_("Connect server"), "server", MSIM_SERVER); - prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); - - option = purple_account_option_int_new(_("Connect port"), "port", MSIM_PORT); - prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); - -#ifdef MSIM_USER_WANTS_TO_CONFIGURE_STATUS_TEXT - option = purple_account_option_bool_new(_("Show display name in status text"), "show_display_name", TRUE); - prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); - - option = purple_account_option_bool_new(_("Show headline in status text"), "show_headline", TRUE); - prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); -#endif - -#ifdef MSIM_USER_WANTS_TO_DISABLE_EMOTICONS - option = purple_account_option_bool_new(_("Send emoticons"), "emoticons", FALSE); - prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); -#endif - -#ifdef MSIM_USER_REALLY_CARES_ABOUT_PRECISE_FONT_SIZES - option = purple_account_option_int_new(_("Screen resolution (dots per inch)"), "dpi", MSIM_DEFAULT_DPI); - prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); - - option = purple_account_option_int_new(_("Base font size (points)"), "base_font_size", MSIM_BASE_FONT_POINT_SIZE); - prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); -#endif - - /* TODO: /zap command. Problem with this is that there are different kinds of zaps, - * and the selection is best made available in a drop-down menu, instead of forcing - * the user to type the kind of zap and memorizing available zaps (or putting it in the - * help menu). A new "attention" API, for zap/buzz/nudge (different protocols) will - * solve this. */ -#if 0 - purple_cmd_register("zap", /* cmd */ - "w", /* args - accept a single word */ - PURPLE_CMD_P_PRPL, /* priority */ - PURPLE_CMD_FLAG_IM | PURPLE_CMD_FLAG_PRPL_ONLY, /* flags */ - "prpl-myspace", /* prpl_id */ - msim_cmd_zap, /* func */ - _("zap: zap a user to get their attention"), /* helpstr */ - NULL); /* data */ -#endif -} - -PURPLE_INIT_PLUGIN(myspace, init_plugin, info); +/* MySpaceIM Protocol Plugin + * + * \author Jeff Connelly + * + * Copyright (C) 2007, Jeff Connelly <jeff2@soc.pidgin.im> + * + * Based on Purple's "C Plugin HOWTO" hello world example. + * + * Code also drawn from mockprpl: + * http://snarfed.org/space/purple+mock+protocol+plugin + * Copyright (C) 2004-2007, Ryan Barrett <mockprpl@ryanb.org> + * + * and some constructs also based on existing Purple plugins, which are: + * Copyright (C) 2003, Robbert Haarman <purple@inglorion.net> + * Copyright (C) 2003, Ethan Blanton <eblanton@cs.purdue.edu> + * Copyright (C) 2000-2003, Rob Flynn <rob@tgflinux.com> + * Copyright (C) 1998-1999, Mark Spencer <markster@marko.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#define PURPLE_PLUGIN + +#include "message.h" +#include "persist.h" +#include "myspace.h" + + +/* Loosely based on Miranda plugin by Scott Ellis, formatting.cpp, + * https://server.scottellis.com.au/websvn/filedetails.php?repname=Miranda+Plugins&path=%2FMySpace%2Fformatting.cpp&rev=0&sc=0 */ + +/* The names in in emoticon_names (for <i n=whatever>) map to corresponding + * entries in emoticon_symbols (for the ASCII representation of the emoticon). + * + * Multiple emoticon symbols in Pidgin can map to one name. List the + * canonical form, as inserted by the "Smile!" dialog, first. For example, + * :) comes before :-), because although both are recognized as 'happy', + * the first is inserted by the smiley button. + * + * Note that symbols are case-sensitive in Pidgin -- :-X is not :-x. */ +static struct MSIM_EMOTICON +{ + gchar *name; + gchar *symbol; +} msim_emoticons[] = { + /* Unfortunately, this list duplicates much of the file + * pidgin/pidgin/pixmaps/emotes/default/22/default.theme.in, because + * that file is part of Pidgin, but we're part of libpurple. + */ + { "bigsmile", ":D" }, + { "bigsmile", ":-D" }, + { "devil", "}:)" }, + { "frazzled", ":Z" }, + { "geek", "B)" }, + { "googles", "%)" }, + { "growl", ":E" }, + { "laugh", ":))" }, /* Must be before ':)' */ + { "happy", ":)" }, + { "happy", ":-)" }, + { "happi", ":)" }, + { "heart", ":X" }, + { "mohawk", "-:" }, + { "mad", "X(" }, + { "messed", "X)" }, + { "nerd", "Q)" }, + { "oops", ":G" }, + { "pirate", "P)" }, + { "scared", ":O" }, + { "sidefrown", ":{" }, + { "sinister", ":B" }, + { "smirk", ":," }, + { "straight", ":|" }, + { "tongue", ":P" }, + { "tongue", ":p" }, + { "tongy", ":P" }, + { "upset", "B|" }, + { "wink", ";-)" }, + { "wink", ";)" }, + { "winc", ";)" }, + { "worried", ":[" }, + { "kiss", ":x" }, + { NULL, NULL } +}; + +/* Internal functions */ +static void msim_send_zap(PurpleBlistNode *node, gpointer zap_num_ptr); + +#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 const gchar *msim_compute_login_response( + const gchar nonce[2 * NONCE_SIZE], const gchar *email, + const gchar *password, guint *response_len); +static gboolean msim_send_bm(MsimSession *session, const gchar *who, + const gchar *text, int type); + +static guint msim_point_to_purple_size(MsimSession *session, guint point); +static guint msim_purple_size_to_point(MsimSession *session, guint size); +static guint msim_height_to_point(MsimSession *session, guint height); +static guint msim_point_to_height(MsimSession *session, guint point); + +static void msim_unrecognized(MsimSession *session, MsimMessage *msg, gchar *note); + +static void msim_markup_tag_to_html(MsimSession *, xmlnode *root, + gchar **begin, gchar **end); +static void html_tag_to_msim_markup(MsimSession *, xmlnode *root, + gchar **begin, gchar **end); +static gchar *msim_convert_xml(MsimSession *, const gchar *raw, + MSIM_XMLNODE_CONVERT f); +static gchar *msim_convert_smileys_to_markup(gchar *before); + +/* High-level msim markup <=> html conversion functions. */ +static gchar *msim_markup_to_html(MsimSession *, const gchar *raw); +static gchar *html_to_msim_markup(MsimSession *, const gchar *raw); + +static MsimUser *msim_get_user_from_buddy(PurpleBuddy *buddy); +static MsimUser *msim_find_user(MsimSession *session, const gchar *username); + +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); +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 gchar *msim_format_now_playing(gchar *band, gchar *song); + +static void msim_set_status_code(MsimSession *session, guint code, + gchar *statstring); + +static void msim_append_user_info(MsimSession *session, PurpleNotifyUserInfo *user_info, MsimUser *user, gboolean full); + +static void msim_downloaded_buddy_icon(PurpleUtilFetchUrlData *url_data, gpointer user_data, const gchar *url_text, + gsize len, const gchar *error_message); + +static void msim_store_user_info_each(gpointer key, gpointer value, + gpointer user_data); +static gboolean msim_store_user_info(MsimSession *session, MsimMessage *msg, MsimUser *user); +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_we_are_logged_on(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 guint msim_new_reply_callback(MsimSession *session, + MSIM_USER_LOOKUP_CB cb, gpointer data); + +static void msim_connect_cb(gpointer data, gint source, + const gchar *error_message); + +static gboolean msim_is_userid(const gchar *user); +static gboolean msim_is_email(const gchar *user); + +static void msim_lookup_user(MsimSession *session, const gchar *user, + MSIM_USER_LOOKUP_CB cb, gpointer data); + +double msim_round(double round); + +/* round is part of C99, but sometimes is unavailable before then. + * Based on http://forums.belution.com/en/cpp/000/050/13.shtml + */ +double msim_round(double value) +{ + if (value < 0) { + return -(floor(-value + 0.5)); + } else { + return floor( value + 0.5); + } +} + +/** + * Load the plugin. + */ +gboolean +msim_load(PurplePlugin *plugin) +{ + /* If compiled to use RC4 from libpurple, check if it is really there. */ + if (!purple_ciphers_find_cipher("rc4")) { + purple_debug_error("msim", "rc4 not in libpurple, but it is required - not loading MySpaceIM plugin!\n"); + purple_notify_error(plugin, _("Missing Cipher"), + _("The RC4 cipher could not be found"), + _("Upgrade " + "to a libpurple with RC4 support (>= 2.0.1). MySpaceIM " + "plugin will not be loaded.")); + return FALSE; + } + return TRUE; +} + +/** + * Get possible user status types. Based on mockprpl. + * + * @return GList of status types. + */ +GList * +msim_status_types(PurpleAccount *acct) +{ + GList *types; + PurpleStatusType *status; + + purple_debug_info("myspace", "returning status types\n"); + + types = NULL; + + /* Statuses are almost all the same. Define a macro to reduce code repetition. */ +#define _MSIM_ADD_NEW_STATUS(prim) status = \ + purple_status_type_new_with_attrs( \ + prim, /* PurpleStatusPrimitive */ \ + NULL, /* id - use default */ \ + NULL, /* name - use default */ \ + TRUE, /* savable */ \ + TRUE, /* user_settable */ \ + FALSE, /* not independent */ \ + \ + /* Attributes - each status can have a message. */ \ + "message", \ + _("Message"), \ + purple_value_new(PURPLE_TYPE_STRING), \ + NULL); \ + \ + \ + types = g_list_append(types, status) + + + _MSIM_ADD_NEW_STATUS(PURPLE_STATUS_AVAILABLE); + _MSIM_ADD_NEW_STATUS(PURPLE_STATUS_AWAY); + _MSIM_ADD_NEW_STATUS(PURPLE_STATUS_OFFLINE); + _MSIM_ADD_NEW_STATUS(PURPLE_STATUS_INVISIBLE); + + + return types; +} + +/** Zap someone. Callback from msim_blist_node_menu zap menu. */ +static void +msim_send_zap(PurpleBlistNode *node, gpointer zap_num_ptr) +{ + PurpleBuddy *buddy; + PurpleConnection *gc; + MsimSession *session; + gchar *username, *zap_string, *zap_text; + guint zap; + const gchar *zap_gerund[10]; + + if (!PURPLE_BLIST_NODE_IS_BUDDY(node)) { + /* Only know about buddies for now. */ + return; + } + + zap_gerund[0] = _("Zapping"); + zap_gerund[1] = _("Whacking"); + zap_gerund[2] = _("Torching"); + zap_gerund[3] = _("Smooching"); + zap_gerund[4] = _("Hugging"); + zap_gerund[5] = _("Bslapping"); + zap_gerund[6] = _("Goosing"); + zap_gerund[7] = _("Hi-fiving"); + zap_gerund[8] = _("Punking"); + zap_gerund[9] = _("Raspberry'ing"); + + g_return_if_fail(PURPLE_BLIST_NODE_IS_BUDDY(node)); + + buddy = (PurpleBuddy *)node; + gc = purple_account_get_connection(buddy->account); + g_return_if_fail(gc != NULL); + + session = (MsimSession *)gc->proto_data; + g_return_if_fail(session != NULL); + + username = buddy->name; + g_return_if_fail(username != NULL); + + zap = GPOINTER_TO_INT(zap_num_ptr); + zap_string = g_strdup_printf("!!!ZAP_SEND!!!=RTE_BTN_ZAPS_%d", zap); + zap_text = g_strdup_printf("*** %s! ***", zap_gerund[zap]); + + serv_got_im(session->gc, username, zap_text, + PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_SYSTEM, time(NULL)); + + if (!msim_send_bm(session, username, zap_string, MSIM_BM_ACTION)) { + purple_debug_info("msim_send_zap", "msim_send_bm failed: zapping %s with %s", + username, zap_string); + } + + g_free(zap_string); + g_free(zap_text); + return; +} + + +/** Return menu, if any, for a buddy list node. */ +GList * +msim_blist_node_menu(PurpleBlistNode *node) +{ + GList *menu, *zap_menu; + PurpleMenuAction *act; + const gchar *zap_names[10]; + guint i; + + if (!PURPLE_BLIST_NODE_IS_BUDDY(node)) { + /* Only know about buddies for now. */ + return NULL; + } + + /* Names from official client. */ + zap_names[0] = _("zap"); + zap_names[1] = _("whack"); + zap_names[2] = _("torch"); + zap_names[3] = _("smooch"); + zap_names[4] = _("hug"); + zap_names[5] = _("bslap"); + zap_names[6] = _("goose"); + zap_names[7] = _("hi-five"); + zap_names[8] = _("punk'd"); + zap_names[9] = _("raspberry"); + + menu = zap_menu = NULL; + + /* TODO: move to / command, or better yet new API */ + for (i = 0; i < sizeof(zap_names) / sizeof(zap_names[0]); ++i) { + act = purple_menu_action_new(zap_names[i], PURPLE_CALLBACK(msim_send_zap), + GUINT_TO_POINTER(i), NULL); + zap_menu = g_list_append(zap_menu, act); + } + + act = purple_menu_action_new(_("Zap"), NULL, NULL, zap_menu); + menu = g_list_append(menu, act); + + return menu; +} + +/** + * Return the icon name for a buddy and account. + * + * @param acct The account to find the icon for, or NULL for protocol icon. + * @param buddy The buddy to find the icon for, or NULL for the account icon. + * + * @return The base icon name string. + */ +const gchar * +msim_list_icon(PurpleAccount *acct, PurpleBuddy *buddy) +{ + /* Use a MySpace icon submitted by hbons at + * http://developer.pidgin.im/wiki/MySpaceIM. */ + return "myspace"; +} + +/** + * Replace 'old' with 'new' in 'str'. + * + * @param str The original string. + * @param old The substring of 'str' to replace. + * @param new The replacement for 'old' within 'str'. + * + * @return A _new_ string, based on 'str', with 'old' replaced + * by 'new'. Must be g_free()'d by caller. + * + * This string replace method is based on + * http://mail.gnome.org/archives/gtk-app-devel-list/2000-July/msg00201.html + * + */ +gchar * +str_replace(const gchar *str, const gchar *old, const gchar *new) +{ + gchar **items; + gchar *ret; + + items = g_strsplit(str, old, -1); + ret = g_strjoinv(new, items); + g_free(items); + return ret; +} + +#ifdef MSIM_DEBUG_MSG +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 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) +{ + 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) +{ + 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; + + /* Passwords are limited in length. */ + if (strlen(acct->password) > MSIM_MAX_PASSWORD_LENGTH) { + gchar *str; + + str = g_strdup_printf( + _("Sorry, passwords over %d characters in length (yours is " + "%d) are not supported by MySpace."), + MSIM_MAX_PASSWORD_LENGTH, + (int)strlen(acct->password)); + + /* Notify an error message also, because this is important! */ + purple_notify_error(acct, g_strdup(_("MySpaceIM Error")), str, NULL); + + purple_connection_error(gc, str); + + g_free(str); + } + + /* 1. connect to server */ + purple_connection_update_progress(gc, _("Connecting"), + 0, /* which connection step this is */ + 4); /* total number of steps */ + + host = purple_account_get_string(acct, "server", MSIM_SERVER); + port = purple_account_get_int(acct, "port", MSIM_PORT); + + /* From purple.sf.net/api: + * """Note that this function name can be misleading--although it is called + * "proxy connect," it is used for establishing any outgoing TCP connection, + * whether through a proxy or not.""" */ + + /* Calls msim_connect_cb when connected. */ + if (!purple_proxy_connect(gc, acct, host, port, msim_connect_cb, gc)) { + /* TODO: try other ports if in auto mode, then save + * working port and try that first next time. */ + purple_connection_error(gc, _("Couldn't create socket")); + return; + } +} + +/** + * Process a login challenge, sending a response. + * + * @param session + * @param msg Login challenge message. + * + * @return TRUE if successful, FALSE if not + */ +static gboolean +msim_login_challenge(MsimSession *session, MsimMessage *msg) +{ + PurpleAccount *account; + const gchar *response; + guint response_len; + gchar *nc; + gsize nc_len; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + g_return_val_if_fail(msim_msg_get_binary(msg, "nc", &nc, &nc_len), FALSE); + + account = session->account; + + g_return_val_if_fail(account != NULL, FALSE); + + purple_connection_update_progress(session->gc, _("Reading challenge"), 1, 4); + + purple_debug_info("msim", "nc is %d bytes, decoded\n", nc_len); + + if (nc_len != MSIM_AUTH_CHALLENGE_LENGTH) { + purple_debug_info("msim", "bad nc length: %x != 0x%x\n", nc_len, MSIM_AUTH_CHALLENGE_LENGTH); + purple_connection_error(session->gc, _("Unexpected challenge length from server")); + return FALSE; + } + + purple_connection_update_progress(session->gc, _("Logging in"), 2, 4); + + response_len = 0; + response = msim_compute_login_response(nc, account->username, account->password, &response_len); + + g_free(nc); + + return msim_send(session, + "login2", MSIM_TYPE_INTEGER, MSIM_AUTH_ALGORITHM, + /* This is actually user's email address. */ + "username", MSIM_TYPE_STRING, g_strdup(account->username), + /* GString and gchar * response will be freed in msim_msg_free() in msim_send(). */ + "response", MSIM_TYPE_BINARY, g_string_new_len(response, response_len), + "clientver", MSIM_TYPE_INTEGER, MSIM_CLIENT_VERSION, + "langid", MSIM_TYPE_INTEGER, MSIM_LANGUAGE_ID_ENGLISH, + "imlang", MSIM_TYPE_STRING, g_strdup(MSIM_LANGUAGE_NAME_ENGLISH), + "reconn", MSIM_TYPE_INTEGER, 0, + "status", MSIM_TYPE_INTEGER, 100, + "id", MSIM_TYPE_INTEGER, 1, + NULL); +} + +/** + * Compute the base64'd login challenge response based on username, password, nonce, and IPs. + * + * @param nonce The base64 encoded nonce ('nc') field from the server. + * @param email User's email address (used as login name). + * @param password User's cleartext password. + * @param response_len Will be written with response length. + * + * @return Binary login challenge response, ready to send to the server. + * Must be g_free()'d when finished. NULL if error. + */ +static const gchar * +msim_compute_login_response(const gchar nonce[2 * NONCE_SIZE], + const gchar *email, const gchar *password, guint *response_len) +{ + PurpleCipherContext *key_context; + PurpleCipher *sha1; + PurpleCipherContext *rc4; + + guchar hash_pw[HASH_SIZE]; + guchar key[HASH_SIZE]; + gchar *password_utf16le, *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; + } + + /* Compute password hash */ + purple_cipher_digest_region("sha1", (guchar *)password_utf16le, + conv_bytes_written, sizeof(hash_pw), hash_pw, NULL); + g_free(password_utf16le); + +#ifdef MSIM_DEBUG_LOGIN_CHALLENGE + purple_debug_info("msim", "pwhash = "); + for (i = 0; i < sizeof(hash_pw); i++) + purple_debug_info("msim", "%.2x ", hash_pw[i]); + purple_debug_info("msim", "\n"); +#endif + + /* key = sha1(sha1(pw) + nonce2) */ + sha1 = purple_ciphers_find_cipher("sha1"); + key_context = purple_cipher_context_new(sha1, NULL); + purple_cipher_context_append(key_context, hash_pw, HASH_SIZE); + purple_cipher_context_append(key_context, (guchar *)(nonce + NONCE_SIZE), NONCE_SIZE); + purple_cipher_context_digest(key_context, sizeof(key), key, NULL); + +#ifdef MSIM_DEBUG_LOGIN_CHALLENGE + purple_debug_info("msim", "key = "); + for (i = 0; i < sizeof(key); i++) { + purple_debug_info("msim", "%.2x ", key[i]); + } + purple_debug_info("msim", "\n"); +#endif + + rc4 = purple_cipher_context_new_by_name("rc4", NULL); + + /* Note: 'key' variable is 0x14 bytes (from SHA-1 hash), + * but only first 0x10 used for the RC4 key. */ + purple_cipher_context_set_option(rc4, "key_len", (gpointer)0x10); + purple_cipher_context_set_key(rc4, key); + + /* TODO: obtain IPs of network interfaces */ + + /* rc4 encrypt: + * nonce1+email+IP list */ + + data_len = NONCE_SIZE + strlen(email) + MSIM_LOGIN_IP_LIST_LEN; + data = g_new0(guchar, data_len); + memcpy(data, nonce, NONCE_SIZE); + memcpy(data + NONCE_SIZE, email, strlen(email)); + memcpy(data + NONCE_SIZE + strlen(email), MSIM_LOGIN_IP_LIST, MSIM_LOGIN_IP_LIST_LEN); + + data_out = g_new0(guchar, data_len); + + purple_cipher_context_encrypt(rc4, (const guchar *)data, + data_len, data_out, &data_out_len); + purple_cipher_context_destroy(rc4); + + g_assert(data_out_len == data_len); + +#ifdef MSIM_DEBUG_LOGIN_CHALLENGE + purple_debug_info("msim", "response=<%s>\n", data_out); +#endif + + *response_len = data_out_len; + + return (const gchar *)data_out; +} + +/** + * Schedule an IM to be sent once the user ID is looked up. + * + * @param gc Connection. + * @param who A user id, email, or username to send the message to. + * @param message Instant message text to send. + * @param flags Flags. + * + * @return 1 if successful or postponed, -1 if failed + * + * Allows sending to a user by username, email address, or userid. If + * a username or email address is given, the userid must be looked up. + * This function does that by calling msim_postprocess_outgoing(). + */ +int +msim_send_im(PurpleConnection *gc, const gchar *who, const gchar *message, + PurpleMessageFlags flags) +{ + MsimSession *session; + 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). + */ + /* TODO: maybe if message is delayed, don't echo to conv window, + * but do echo it to conv window manually once it is actually + * sent? Would be complicated. */ + rc = 1; + } else { + rc = -1; + } + + g_free(message_msim); + + /* + * In MySpace, you login with your email address, but don't talk to other + * users using their email address. So there is currently an asymmetry in the + * IM windows when using this plugin: + * + * you@example.com: hello + * some_other_user: what's going on? + * you@example.com: just coding a prpl + * + * TODO: Make the sent IM's appear as from the user's username, instead of + * their email address. Purple uses the login (in MSIM, the email)--change this. + */ + + 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. + * + */ +static 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; +} + +/* Indexes of this array + 1 map HTML font size to scale of normal font size. * + * Based on _point_sizes from libpurple/gtkimhtml.c + * 1 2 3 4 5 6 7 */ +static gdouble _font_scale[] = { .85, .95, 1, 1.2, 1.44, 1.728, 2.0736 }; + +#define MAX_FONT_SIZE 7 /* Purple maximum font size */ +#define POINTS_PER_INCH 72 /* How many pt's in an inch */ + +/** Convert typographical font point size to HTML font size. + * Based on libpurple/gtkimhtml.c */ +static guint +msim_point_to_purple_size(MsimSession *session, guint point) +{ + guint size, this_point, base; + gdouble scale; + + base = purple_account_get_int(session->account, "base_font_size", MSIM_BASE_FONT_POINT_SIZE); + + for (size = 0; + size < sizeof(_font_scale) / sizeof(_font_scale[0]); + ++size) { + scale = _font_scale[CLAMP(size, 1, MAX_FONT_SIZE) - 1]; + this_point = (guint)msim_round(scale * base); + + if (this_point >= point) { + purple_debug_info("msim", "msim_point_to_purple_size: %d pt -> size=%d\n", + point, size); + return size; + } + } + + /* No HTML font size was this big; return largest possible. */ + return this_point; +} + +/** Convert HTML font size to point size. */ +static guint +msim_purple_size_to_point(MsimSession *session, guint size) +{ + gdouble scale; + guint point; + guint base; + + scale = _font_scale[CLAMP(size, 1, MAX_FONT_SIZE) - 1]; + + base = purple_account_get_int(session->account, "base_font_size", MSIM_BASE_FONT_POINT_SIZE); + + point = (guint)msim_round(scale * base); + + purple_debug_info("msim", "msim_purple_size_to_point: size=%d -> %d pt\n", + size, point); + + return point; +} + +/** Convert a msim markup font pixel height to the more usual point size, for incoming messages. */ +static guint +msim_height_to_point(MsimSession *session, guint height) +{ + guint dpi; + + dpi = purple_account_get_int(session->account, "port", MSIM_DEFAULT_DPI); + + return (guint)msim_round((POINTS_PER_INCH * 1. / dpi) * height); + + /* See also: libpurple/protocols/bonjour/jabber.c + * _font_size_ichat_to_purple */ +} + +/** Convert point size to msim pixel height font size specification, for outgoing messages. */ +static guint +msim_point_to_height(MsimSession *session, guint point) +{ + guint dpi; + + dpi = purple_account_get_int(session->account, "port", MSIM_DEFAULT_DPI); + + return (guint)msim_round((dpi * 1. / POINTS_PER_INCH) * point); +} + +/** Convert the msim markup <f> (font) tag into HTML. */ +static void +msim_markup_f_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) +{ + const gchar *face, *height_str, *decor_str; + GString *gs_end, *gs_begin; + guint decor, height; + + face = xmlnode_get_attrib(root, "f"); + height_str = xmlnode_get_attrib(root, "h"); + decor_str = xmlnode_get_attrib(root, "s"); + + if (height_str) { + height = atol(height_str); + } else { + height = 12; + } + + if (decor_str) { + decor = atol(decor_str); + } else { + decor = 0; + } + + gs_begin = g_string_new(""); + /* TODO: get font size working */ + if (height && !face) { + g_string_printf(gs_begin, "<font size='%d'>", + msim_point_to_purple_size(session, msim_height_to_point(session, height))); + } else if (height && face) { + g_string_printf(gs_begin, "<font face='%s' size='%d'>", face, + msim_point_to_purple_size(session, msim_height_to_point(session, height))); + } else { + g_string_printf(gs_begin, "<font>"); + } + + /* No support for font-size CSS? */ + /* g_string_printf(gs_begin, "<span style='font-family: %s; font-size: %dpt'>", face, + msim_height_to_point(height)); */ + + gs_end = g_string_new("</font>"); + + if (decor & MSIM_TEXT_BOLD) { + g_string_append(gs_begin, "<b>"); + g_string_prepend(gs_end, "</b>"); + } + + if (decor & MSIM_TEXT_ITALIC) { + g_string_append(gs_begin, "<i>"); + g_string_append(gs_end, "</i>"); + } + + if (decor & MSIM_TEXT_UNDERLINE) { + g_string_append(gs_begin, "<u>"); + g_string_append(gs_end, "</u>"); + } + + + *begin = gs_begin->str; + *end = gs_end->str; +} + +/** Convert a msim markup color to a color suitable for libpurple. + * + * @param msim Either a color name, or an rgb(x,y,z) code. + * + * @return A new string, either a color name or #rrggbb code. Must g_free(). + */ +static char * +msim_color_to_purple(const char *msim) +{ + guint red, green, blue; + + if (!msim) { + return g_strdup("black"); + } + + if (sscanf(msim, "rgb(%d,%d,%d)", &red, &green, &blue) != 3) { + /* Color name. */ + return g_strdup(msim); + } + /* TODO: rgba (alpha). */ + + return g_strdup_printf("#%.2x%.2x%.2x", red, green, blue); +} + +/** Convert the msim markup <a> (anchor) tag into HTML. */ +static void +msim_markup_a_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) +{ + const gchar *href; + + href = xmlnode_get_attrib(root, "h"); + if (!href) { + href = ""; + } + + *begin = g_strdup_printf("<a href=\"%s\">%s", href, href); + *end = g_strdup("</a>"); +} + +/** Convert the msim markup <p> (paragraph) tag into HTML. */ +static void +msim_markup_p_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) +{ + /* Just pass through unchanged. + * + * Note: attributes currently aren't passed, if there are any. */ + *begin = g_strdup("<p>"); + *end = g_strdup("</p>"); +} + +/** Convert the msim markup <c> tag (text color) into HTML. TODO: Test */ +static void +msim_markup_c_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) +{ + const gchar *color; + gchar *purple_color; + + color = xmlnode_get_attrib(root, "v"); + if (!color) { + purple_debug_info("msim", "msim_markup_c_to_html: <c> tag w/o v attr"); + *begin = g_strdup(""); + *end = g_strdup(""); + /* TODO: log as unrecognized */ + return; + } + + purple_color = msim_color_to_purple(color); + + *begin = g_strdup_printf("<font color='%s'>", purple_color); + + g_free(purple_color); + + /* *begin = g_strdup_printf("<span style='color: %s'>", color); */ + *end = g_strdup("</font>"); +} + +/** Convert the msim markup <b> tag (background color) into HTML. TODO: Test */ +static void +msim_markup_b_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) +{ + const gchar *color; + gchar *purple_color; + + color = xmlnode_get_attrib(root, "v"); + if (!color) { + *begin = g_strdup(""); + *end = g_strdup(""); + purple_debug_info("msim", "msim_markup_b_to_html: <b> w/o v attr"); + /* TODO: log as unrecognized. */ + return; + } + + purple_color = msim_color_to_purple(color); + + /* TODO: find out how to set background color. */ + *begin = g_strdup_printf("<span style='background-color: %s'>", + purple_color); + g_free(purple_color); + + *end = g_strdup("</p>"); +} + +/** Convert the msim markup <i> tag (emoticon image) into HTML. */ +static void +msim_markup_i_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) +{ + const gchar *name; + guint i; + struct MSIM_EMOTICON *emote; + + name = xmlnode_get_attrib(root, "n"); + if (!name) { + purple_debug_info("msim", "msim_markup_i_to_html: <i> w/o n"); + *begin = g_strdup(""); + *end = g_strdup(""); + /* TODO: log as unrecognized */ + return; + } + + /* Find and use canonical form of smiley symbol. */ + for (i = 0; (emote = &msim_emoticons[i]) && emote->name != NULL; ++i) { + if (!strcmp(name, emote->name)) { + *begin = g_strdup(emote->symbol); + *end = g_strdup(""); + return; + } + } + + /* Couldn't find it, sorry. Try to degrade gracefully. */ + *begin = g_strdup_printf("**%s**", name); + *end = g_strdup(""); +} + +/** Convert an individual msim markup tag to HTML. */ +static void +msim_markup_tag_to_html(MsimSession *session, xmlnode *root, gchar **begin, + gchar **end) +{ + if (!strcmp(root->name, "f")) { + msim_markup_f_to_html(session, root, begin, end); + } else if (!strcmp(root->name, "a")) { + msim_markup_a_to_html(session, root, begin, end); + } else if (!strcmp(root->name, "p")) { + msim_markup_p_to_html(session, root, begin, end); + } else if (!strcmp(root->name, "c")) { + msim_markup_c_to_html(session, root, begin, end); + } else if (!strcmp(root->name, "b")) { + msim_markup_b_to_html(session, root, begin, end); + } else if (!strcmp(root->name, "i")) { + msim_markup_i_to_html(session, root, begin, end); + } else { + purple_debug_info("msim", "msim_markup_tag_to_html: " + "unknown tag name=%s, ignoring", + (root && root->name) ? root->name : "(NULL)"); + *begin = g_strdup(""); + *end = g_strdup(""); + } +} + +/** Convert an individual HTML tag to msim markup. */ +static void +html_tag_to_msim_markup(MsimSession *session, xmlnode *root, gchar **begin, + gchar **end) +{ + /* TODO: Coalesce nested tags into one <f> tag! + * Currently, the 's' value will be overwritten when b/i/u is nested + * within another one, and only the inner-most formatting will be + * applied to the text. */ + if (!purple_utf8_strcasecmp(root->name, "root")) { + *begin = g_strdup(""); + *end = g_strdup(""); + } else if (!purple_utf8_strcasecmp(root->name, "b")) { + *begin = g_strdup_printf("<f s='%d'>", MSIM_TEXT_BOLD); + *end = g_strdup("</f>"); + } else if (!purple_utf8_strcasecmp(root->name, "i")) { + *begin = g_strdup_printf("<f s='%d'>", MSIM_TEXT_ITALIC); + *end = g_strdup("</f>"); + } else if (!purple_utf8_strcasecmp(root->name, "u")) { + *begin = g_strdup_printf("<f s='%d'>", MSIM_TEXT_UNDERLINE); + *end = g_strdup("</f>"); + } else if (!purple_utf8_strcasecmp(root->name, "a")) { + const gchar *href, *link_text; + + href = xmlnode_get_attrib(root, "href"); + + if (!href) { + href = xmlnode_get_attrib(root, "HREF"); + } + + link_text = xmlnode_get_data(root); + + if (href) { + if (!strcmp(link_text, href)) { + /* Purple gives us: <a href="URL">URL</a> + * Translate to <a h='URL' /> + * Displayed as text of URL with link to URL + */ + *begin = g_strdup_printf("<a h='%s' />", href); + } else { + /* But if we get: <a href="URL">text</a> + * Translate to: text: <a h='URL' /> + * + * Because official client only supports self-closed <a> + * tags; you can't change the link text. + */ + *begin = g_strdup_printf("%s: <a h='%s' />", link_text, href); + } + } else { + *begin = g_strdup("<a />"); + } + + /* Sorry, kid. MySpace doesn't support you within <a> tags. */ + xmlnode_free(root->child); + root->child = NULL; + + *end = g_strdup(""); + } else if (!purple_utf8_strcasecmp(root->name, "font")) { + const gchar *size; + const gchar *face; + + size = xmlnode_get_attrib(root, "size"); + face = xmlnode_get_attrib(root, "face"); + + if (face && size) { + *begin = g_strdup_printf("<f f='%s' h='%d'>", face, + msim_point_to_height(session, + msim_purple_size_to_point(session, atoi(size)))); + } else if (face) { + *begin = g_strdup_printf("<f f='%s'>", face); + } else if (size) { + *begin = g_strdup_printf("<f h='%d'>", + msim_point_to_height(session, + msim_purple_size_to_point(session, atoi(size)))); + } else { + *begin = g_strdup("<f>"); + } + + *end = g_strdup("</f>"); + + /* TODO: color (bg uses <body>), emoticons */ + } else { + *begin = g_strdup_printf("[%s]", root->name); + *end = g_strdup_printf("[/%s]", root->name); + } +} + +/** Convert an xmlnode of msim markup or HTML to an HTML string or msim markup. + * + * @param f Function to convert tags. + * + * @return An HTML string. Caller frees. + */ +static gchar * +msim_convert_xmlnode(MsimSession *session, xmlnode *root, MSIM_XMLNODE_CONVERT f) +{ + xmlnode *node; + gchar *begin, *inner, *end; + GString *final; + + if (!root || !root->name) { + return g_strdup(""); + } + + purple_debug_info("msim", "msim_convert_xmlnode: got root=%s\n", + root->name); + + begin = inner = end = NULL; + + final = g_string_new(""); + + f(session, root, &begin, &end); + + g_string_append(final, begin); + + /* Loop over all child nodes. */ + for (node = root->child; node != NULL; node = node->next) { + switch (node->type) { + case XMLNODE_TYPE_ATTRIB: + /* Attributes handled above. */ + break; + + case XMLNODE_TYPE_TAG: + /* A tag or tag with attributes. Recursively descend. */ + inner = msim_convert_xmlnode(session, node, f); + g_return_val_if_fail(inner != NULL, NULL); + + purple_debug_info("msim", " ** node name=%s\n", + (node && node->name) ? node->name : "(NULL)"); + break; + + case XMLNODE_TYPE_DATA: + /* Literal text. */ + inner = g_new0(char, node->data_sz + 1); + strncpy(inner, node->data, node->data_sz); + inner[node->data_sz] = 0; + + purple_debug_info("msim", " ** node data=%s\n", + inner ? inner : "(NULL)"); + break; + + default: + purple_debug_info("msim", + "msim_convert_xmlnode: strange node\n"); + inner = g_strdup(""); + } + + if (inner) { + g_string_append(final, inner); + } + } + + /* TODO: Note that msim counts each piece of text enclosed by <f> as + * a paragraph and will display each on its own line. You actually have + * to _nest_ <f> tags to intersperse different text in one paragraph! + * Comment out this line below to see. */ + g_string_append(final, end); + + purple_debug_info("msim", "msim_markup_xmlnode_to_gtkhtml: RETURNING %s\n", + (final && final->str) ? final->str : "(NULL)"); + + return final->str; +} + +/** Convert XML to something based on MSIM_XMLNODE_CONVERT. */ +static gchar * +msim_convert_xml(MsimSession *session, const gchar *raw, MSIM_XMLNODE_CONVERT f) +{ + xmlnode *root; + gchar *str; + gchar *enclosed_raw; + + g_return_val_if_fail(raw != NULL, NULL); + + /* Enclose text in one root tag, to try to make it valid XML for parsing. */ + enclosed_raw = g_strconcat("<root>", raw, "</root>", NULL); + + root = xmlnode_from_str(enclosed_raw, -1); + + if (!root) { + purple_debug_info("msim", "msim_markup_to_html: couldn't parse " + "%s as XML, returning raw: %s\n", enclosed_raw, raw); + /* TODO: msim_unrecognized */ + g_free(enclosed_raw); + return g_strdup(raw); + } + + g_free(enclosed_raw); + + str = msim_convert_xmlnode(session, root, f); + g_return_val_if_fail(str != NULL, NULL); + purple_debug_info("msim", "msim_markup_to_html: returning %s\n", str); + + xmlnode_free(root); + + return str; +} + +/** Convert plaintext smileys to <i> markup tags. + * + * @param before Original text with ASCII smileys. Will be freed. + * @return A new string with <i> tags, if applicable. Must be g_free()'d. + */ +static gchar * +msim_convert_smileys_to_markup(gchar *before) +{ + gchar *old, *new, *replacement; + guint i; + struct MSIM_EMOTICON *emote; + + old = before; + new = NULL; + + for (i = 0; (emote = &msim_emoticons[i]) && emote->name != NULL; ++i) { + gchar *name, *symbol; + + name = emote->name; + symbol = emote->symbol; + + replacement = g_strdup_printf("<i n=\"%s\"/>", name); + + purple_debug_info("msim", "msim_convert_smileys_to_markup: %s->%s\n", + symbol ? symbol : "(NULL)", + replacement ? replacement : "(NULL)"); + new = str_replace(old, symbol, replacement); + + g_free(replacement); + g_free(old); + + old = new; + } + + return new; +} + + +/** High-level function to convert MySpaceIM markup to Purple (HTML) markup. + * + * @return Purple markup string, must be g_free()'d. */ +static gchar * +msim_markup_to_html(MsimSession *session, const gchar *raw) +{ + return msim_convert_xml(session, raw, + (MSIM_XMLNODE_CONVERT)(msim_markup_tag_to_html)); +} + +/** High-level function to convert Purple (HTML) to MySpaceIM markup. + * + * @return HTML markup string, must be g_free()'d. */ +static gchar * +html_to_msim_markup(MsimSession *session, const gchar *raw) +{ + gchar *markup; + + markup = msim_convert_xml(session, raw, + (MSIM_XMLNODE_CONVERT)(html_tag_to_msim_markup)); + + if (purple_account_get_bool(session->account, "emoticons", TRUE)) { + /* Frees markup and allocates a new one. */ + markup = msim_convert_smileys_to_markup(markup); + } + + return markup; +} + +/** Get the MsimUser from a PurpleBuddy, creating it if needed. */ +static MsimUser * +msim_get_user_from_buddy(PurpleBuddy *buddy) +{ + MsimUser *user; + + if (!buddy) { + return NULL; + } + + if (!buddy->proto_data) { + /* TODO: where is this freed? */ + user = g_new0(MsimUser, 1); + user->buddy = buddy; + buddy->proto_data = (gpointer)user; + purple_debug_info("msim_get_user_from_buddy", + "creating new user for %s to %X\n", + buddy->name, buddy->proto_data); + } + + user = (MsimUser *)(buddy->proto_data); + + return user; +} + +/** Find and return an MsimUser * representing a user on the buddy list, or NULL. */ +static MsimUser * +msim_find_user(MsimSession *session, const gchar *username) +{ + PurpleBuddy *buddy; + MsimUser *user; + + buddy = purple_find_buddy(session->account, username); + if (!buddy) { + return NULL; + } + + user = msim_get_user_from_buddy(buddy); + + return user; +} + + +/** Record the client version in the buddy list, from an incoming message. */ +static gboolean +msim_incoming_bm_record_cv(MsimSession *session, MsimMessage *msg) +{ + gchar *username, *cv; + gboolean ret; + MsimUser *user; + + username = msim_msg_get_string(msg, "_username"); + cv = msim_msg_get_string(msg, "cv"); + + g_return_val_if_fail(username != NULL, FALSE); + if (!cv) { + /* No client version to record, don't worry about it. */ + return FALSE; + } + + user = msim_find_user(session, username); + + if (user) { + user->client_cv = atol(cv); + ret = TRUE; + } else { + ret = FALSE; + } + + g_free(username); + g_free(cv); + + return ret; +} + +/** 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); + } +} + +/** + * Handle an incoming instant message. + * + * @param session The session + * @param msg Message from the server, containing 'f' (userid from) and 'msg'. + * Should also contain username in _username from preprocessing. + * + * @return TRUE if successful. + */ +static gboolean +msim_incoming_im(MsimSession *session, MsimMessage *msg) +{ + gchar *username, *msg_msim_markup, *msg_purple_markup; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + username = msim_msg_get_string(msg, "_username"); + g_return_val_if_fail(username != NULL, FALSE); + + msg_msim_markup = msim_msg_get_string(msg, "msg"); + g_return_val_if_fail(msg_msim_markup != NULL, FALSE); + + msg_purple_markup = msim_markup_to_html(session, msg_msim_markup); + g_free(msg_msim_markup); + + serv_got_im(session->gc, username, msg_purple_markup, + PURPLE_MESSAGE_RECV, time(NULL)); + + g_free(username); + g_free(msg_purple_markup); + + return TRUE; +} + +/** + * Process unrecognized information. + * + * @param session + * @param msg An MsimMessage that was unrecognized, or NULL. + * @param note Information on what was unrecognized, or NULL. + */ +static void +msim_unrecognized(MsimSession *session, MsimMessage *msg, gchar *note) +{ + /* TODO: Some more context, outwardly equivalent to a backtrace, + * for helping figure out what this msg is for. What was going on? + * But not too much information so that a user + * posting this dump reveals confidential information. + */ + + /* TODO: dump unknown msgs to file, so user can send them to me + * if they wish, to help add support for new messages (inspired + * by Alexandr Shutko, who maintains OSCAR protocol documentation). */ + + purple_debug_info("msim", "Unrecognized data on account for %s\n", + session->account->username ? 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); + } +} + +/** Process an incoming zap. */ +static gboolean +msim_incoming_zap(MsimSession *session, MsimMessage *msg) +{ + gchar *msg_text, *username, *zap_text; + gint zap; + const gchar *zap_past_tense[10]; + + zap_past_tense[0] = _("zapped"); + zap_past_tense[1] = _("whacked"); + zap_past_tense[2] = _("torched"); + zap_past_tense[3] = _("smooched"); + zap_past_tense[4] = _("hugged"); + zap_past_tense[5] = _("bslapped"); + zap_past_tense[6] = _("goosed"); + zap_past_tense[7] = _("hi-fived"); + zap_past_tense[8] = _("punk'd"); + zap_past_tense[9] = _("raspberried"); + + msg_text = msim_msg_get_string(msg, "msg"); + username = msim_msg_get_string(msg, "_username"); + + g_return_val_if_fail(msg_text != NULL, FALSE); + g_return_val_if_fail(username != NULL, FALSE); + + g_return_val_if_fail(sscanf(msg_text, "!!!ZAP_SEND!!!=RTE_BTN_ZAPS_%d", &zap) == 1, FALSE); + + zap = CLAMP(zap, 0, sizeof(zap_past_tense) / sizeof(zap_past_tense[0])); + + zap_text = g_strdup_printf(_("*** You have been %s! ***"), zap_past_tense[zap]); + + serv_got_im(session->gc, username, zap_text, + PURPLE_MESSAGE_RECV | PURPLE_MESSAGE_SYSTEM, time(NULL)); + + g_free(zap_text); + g_free(msg_text); + g_free(username); + + return TRUE; +} + +/** + * Handle an incoming action message. + * + * @param session + * @param msg + * + * @return TRUE if successful. + * + */ +static gboolean +msim_incoming_action(MsimSession *session, MsimMessage *msg) +{ + gchar *msg_text, *username; + gboolean rc; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + msg_text = msim_msg_get_string(msg, "msg"); + g_return_val_if_fail(msg_text != NULL, FALSE); + + username = msim_msg_get_string(msg, "_username"); + g_return_val_if_fail(username != NULL, FALSE); + + purple_debug_info("msim", "msim_incoming_action: action <%s> from <%d>\n", + msg_text, username); + + if (strcmp(msg_text, "%typing%") == 0) { + /* TODO: find out if msim repeatedly sends typing messages, so we can + * give it a timeout. Right now, there does seem to be an inordinately + * amount of time between typing stopped-typing notifications. */ + serv_got_typing(session->gc, username, 0, PURPLE_TYPING); + rc = TRUE; + } else if (strcmp(msg_text, "%stoptyping%") == 0) { + serv_got_typing_stopped(session->gc, username); + rc = TRUE; + } else if (strstr(msg_text, "!!!ZAP_SEND!!!=RTE_BTN_ZAPS_")) { + rc = msim_incoming_zap(session, msg); + } else { + msim_unrecognized(session, msg, + "got to msim_incoming_action but unrecognized value for 'msg'"); + rc = FALSE; + } + + g_free(msg_text); + g_free(username); + + return rc; +} + +/* Process an incoming media (buddy icon) message. */ +static gboolean +msim_incoming_media(MsimSession *session, MsimMessage *msg) +{ + gchar *username, *text; + + username = msim_msg_get_string(msg, "_username"); + text = msim_msg_get_string(msg, "msg"); + + g_return_val_if_fail(username != NULL, FALSE); + g_return_val_if_fail(text != NULL, FALSE); + + purple_debug_info("msim", "msim_incoming_media: from %s, got msg=%s\n", username, text); + + /* Media messages are sent when the user opens a window to someone. + * Tell libpurple they started typing and stopped typing, to inform the Psychic + * Mode plugin so it too can open a window to the user. */ + serv_got_typing(session->gc, username, 0, PURPLE_TYPING); + serv_got_typing_stopped(session->gc, username); + + g_free(username); + + return TRUE; +} + +/* 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) +{ + MsimUser *user; + gchar *username, *client_info; + + username = msim_msg_get_string(msg, "_username"); + client_info = msim_msg_get_string(msg, "msg"); + + g_return_val_if_fail(username != NULL, FALSE); + g_return_val_if_fail(client_info != NULL, FALSE); + + purple_debug_info("msim", "msim_incoming_unofficial_client: %s is using client %s\n", + username, client_info); + + user = msim_find_user(session, username); + + g_return_val_if_fail(user != NULL, FALSE); + + if (user->client_info) { + g_free(user->client_info); + } + user->client_info = client_info; + + g_free(username); + /* Do not free client_info - the MsimUser now owns it. */ + + return TRUE; +} + + +#ifdef MSIM_SEND_CLIENT_VERSION +/** Send our client version to another unofficial client that understands it. */ +static gboolean +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 + +/** + * Handle when our user starts or stops typing to another user. + * + * @param gc + * @param name The buddy name to which our user is typing to + * @param state PURPLE_TYPING, PURPLE_TYPED, PURPLE_NOT_TYPING + * + * @return 0 + */ +unsigned int +msim_send_typing(PurpleConnection *gc, const gchar *name, + PurpleTypingState state) +{ + const gchar *typing_str; + MsimSession *session; + + g_return_val_if_fail(gc != NULL, 0); + g_return_val_if_fail(name != NULL, 0); + + session = (MsimSession *)gc->proto_data; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); + + switch (state) { + case PURPLE_TYPING: + typing_str = "%typing%"; + break; + + case PURPLE_TYPED: + case PURPLE_NOT_TYPING: + default: + typing_str = "%stoptyping%"; + break; + } + + purple_debug_info("msim", "msim_send_typing(%s): %d (%s)\n", name, state, typing_str); + msim_send_bm(session, name, typing_str, MSIM_BM_ACTION); + return 0; +} + +/** Format the "now playing" indicator, showing the artist and song. + * @return Return a new string (must be g_free()'d), or NULL. + */ +static gchar * +msim_format_now_playing(gchar *band, gchar *song) +{ + if ((band && strlen(band)) || (song && strlen(song))) { + return g_strdup_printf("%s - %s", + (band && strlen(band)) ? band : "Unknown Artist", + (song && strlen(song)) ? song : "Unknown Song"); + } else { + return NULL; + } +} + +/** Append user information to a PurpleNotifyUserInfo, given an MsimUser. + * Used by msim_tooltip_text() and msim_get_info_cb() to show a user's profile. + */ +static void +msim_append_user_info(MsimSession *session, PurpleNotifyUserInfo *user_info, MsimUser *user, gboolean full) +{ + gchar *str; + guint uid; + guint cv; + + /* Useful to identify the account the tooltip refers to. + * Other prpls show this. */ + if (user->username) { + purple_notify_user_info_add_pair(user_info, _("User"), user->username); + } + + uid = purple_blist_node_get_int(&user->buddy->node, "UserID"); + + if (full) { + purple_notify_user_info_add_pair(user_info, _("User ID"), g_strdup_printf("%d", uid)); + } + + + /* a/s/l...the vitals */ + if (user->age) { + purple_notify_user_info_add_pair(user_info, _("Age"), + g_strdup_printf("%d", user->age)); + } + + if (user->gender && strlen(user->gender)) { + purple_notify_user_info_add_pair(user_info, _("Gender"), user->gender); + } + + if (user->location && strlen(user->location)) { + purple_notify_user_info_add_pair(user_info, _("Location"), user->location); + } + + /* Other information */ + if (user->headline && strlen(user->headline)) { + purple_notify_user_info_add_pair(user_info, _("Headline"), user->headline); + } + + str = msim_format_now_playing(user->band_name, user->song_name); + if (str && strlen(str)) { + purple_notify_user_info_add_pair(user_info, _("Song"), str); + } + + /* Note: total friends only available if looked up by uid, not username. */ + if (user->total_friends) { + purple_notify_user_info_add_pair(user_info, _("Total Friends"), + g_strdup_printf("%d", user->total_friends)); + } + + if (full) { + /* Client information */ + + str = user->client_info; + cv = user->client_cv; + + if (str && cv != 0) { + purple_notify_user_info_add_pair(user_info, _("Client Version"), + g_strdup_printf("%s (build %d)", str, cv)); + } else if (str) { + purple_notify_user_info_add_pair(user_info, _("Client Version"), + g_strdup(str)); + } else if (cv) { + purple_notify_user_info_add_pair(user_info, _("Client Version"), + g_strdup_printf("Build %d", cv)); + } + } +} + +/** Callback for msim_get_info(), for when user info is received. */ +static void +msim_get_info_cb(MsimSession *session, MsimMessage *user_info_msg, + gpointer data) +{ + MsimMessage *msg; + gchar *username; + PurpleNotifyUserInfo *user_info; + MsimUser *user; + gboolean temporary_user; + + g_return_if_fail(MSIM_SESSION_VALID(session)); + + /* Get user{name,id} from msim_get_info, passed as an MsimMessage for + orthogonality. */ + msg = (MsimMessage *)data; + g_return_if_fail(msg != NULL); + + username = msim_msg_get_string(msg, "user"); + if (!username) { + purple_debug_info("msim", "msim_get_info_cb: no 'user' in msg"); + return; + } + + msim_msg_free(msg); + purple_debug_info("msim", "msim_get_info_cb: got for user: %s\n", username); + + user = msim_find_user(session, username); + + if (!user) { + /* User isn't on blist, create a temporary user to store info. */ + temporary_user = TRUE; + user = g_new0(MsimUser, 1); + } else { + temporary_user = FALSE; + } + + /* Update user structure with new information */ + msim_store_user_info(session, user_info_msg, user); + + user_info = purple_notify_user_info_new(); + + /* Append data from MsimUser to PurpleNotifyUserInfo for display, full */ + msim_append_user_info(session, user_info, user, TRUE); + + purple_notify_userinfo(session->gc, username, user_info, NULL, NULL); + purple_debug_info("msim", "msim_get_info_cb: username=%s\n", username); + + purple_notify_user_info_destroy(user_info); + /* TODO: do not free username, since it will be used by user_info? */ + + if (temporary_user) { + g_free(user->client_info); + g_free(user->gender); + g_free(user->location); + g_free(user->headline); + g_free(user->display_name); + g_free(user->username); + g_free(user->band_name); + g_free(user->song_name); + g_free(user->image_url); + g_free(user); + } + +} + +/** Retrieve a user's profile. + * @param username Username, user ID, or email address to lookup. + */ +void +msim_get_info(PurpleConnection *gc, const gchar *username) +{ + MsimSession *session; + MsimUser *user; + guint uid; + gchar *user_to_lookup; + MsimMessage *user_msg; + + g_return_if_fail(gc != NULL); + g_return_if_fail(username != NULL); + + session = (MsimSession *)gc->proto_data; + + g_return_if_fail(MSIM_SESSION_VALID(session)); + + /* Obtain uid of buddy. */ + 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); + } 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 + * the username) but it would be useful to display the username too. + */ + user_msg = msim_msg_new( + "user", MSIM_TYPE_STRING, g_strdup(username), + NULL); + purple_debug_info("msim", "msim_get_info, setting up lookup, user=%s\n", username); + + msim_lookup_user(session, user_to_lookup, msim_get_info_cb, user_msg); + + g_free(user_to_lookup); +} + +/** Set your status - callback for when user manually sets it. */ +void +msim_set_status(PurpleAccount *account, PurpleStatus *status) +{ + PurpleStatusType *type; + MsimSession *session; + guint status_code; + const gchar *statstring; + + session = (MsimSession *)account->gc->proto_data; + + g_return_if_fail(MSIM_SESSION_VALID(session)); + + type = purple_status_get_type(status); + + switch (purple_status_type_get_primitive(type)) { + case PURPLE_STATUS_AVAILABLE: + purple_debug_info("msim", "msim_set_status: available (%d->%d)\n", PURPLE_STATUS_AVAILABLE, + MSIM_STATUS_CODE_ONLINE); + status_code = MSIM_STATUS_CODE_ONLINE; + break; + + case PURPLE_STATUS_INVISIBLE: + purple_debug_info("msim", "msim_set_status: invisible (%d->%d)\n", PURPLE_STATUS_INVISIBLE, + MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN); + status_code = MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN; + break; + + case PURPLE_STATUS_AWAY: + purple_debug_info("msim", "msim_set_status: away (%d->%d)\n", PURPLE_STATUS_AWAY, + MSIM_STATUS_CODE_AWAY); + status_code = MSIM_STATUS_CODE_AWAY; + break; + + default: + purple_debug_info("msim", "msim_set_status: unknown " + "status interpreting as online"); + status_code = MSIM_STATUS_CODE_ONLINE; + break; + } + + statstring = purple_status_get_attr_string(status, "message"); + + if (!statstring) { + statstring = g_strdup(""); + } + + msim_set_status_code(session, status_code, g_strdup(statstring)); +} + +/** Go idle. */ +void +msim_set_idle(PurpleConnection *gc, int time) +{ + MsimSession *session; + + g_return_if_fail(gc != NULL); + + session = (MsimSession *)gc->proto_data; + + g_return_if_fail(MSIM_SESSION_VALID(session)); + + if (time == 0) { + /* Going back from idle. In msim, idle is mutually exclusive + * from the other states (you can only be away or idle, but not + * both, for example), so by going non-idle I go online. + */ + /* TODO: find out how to keep old status string? */ + msim_set_status_code(session, MSIM_STATUS_CODE_ONLINE, g_strdup("")); + } else { + /* msim doesn't support idle time, so just go idle */ + msim_set_status_code(session, MSIM_STATUS_CODE_IDLE, g_strdup("")); + } +} + +/** 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"); + } + +} + +/** After a uid is resolved to username, tag it with the username and submit for processing. + * + * @param session + * @param userinfo Response messsage to resolving request. + * @param data MsimMessage *, the message to attach information to. + */ +static void +msim_incoming_resolved(MsimSession *session, MsimMessage *userinfo, + gpointer data) +{ + gchar *body_str; + GHashTable *body; + gchar *username; + MsimMessage *msg; + + g_return_if_fail(MSIM_SESSION_VALID(session)); + g_return_if_fail(userinfo != NULL); + + body_str = msim_msg_get_string(userinfo, "body"); + g_return_if_fail(body_str != NULL); + body = msim_parse_body(body_str); + g_return_if_fail(body != NULL); + g_free(body_str); + + username = g_hash_table_lookup(body, "UserName"); + g_return_if_fail(username != NULL); + + + msg = (MsimMessage *)data; + g_return_if_fail(msg != NULL); + + /* 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, g_strdup(username)); + + /* TODO: attach more useful information, like ImageURL */ + + msim_process(session, msg); + + /* TODO: Free copy cloned from msim_preprocess_incoming(). */ + //XXX msim_msg_free(msg); + g_hash_table_destroy(body); +} + +/* Lookup a username by userid, from buddy list. + * + * @param wanted_uid + * + * @return Username of wanted_uid, if on blist, or NULL. Static string. + * + */ +static const gchar * +msim_uid2username_from_blist(MsimSession *session, guint wanted_uid) +{ + GSList *buddies, *cur; + gchar *ret; + + buddies = purple_find_buddies(session->account, NULL); + + if (!buddies) + { + purple_debug_info("msim", "msim_uid2username_from_blist: no buddies?"); + 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 = g_strdup(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, 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(_("Connection to server lost (no data received within %d seconds)"), (int)delta); + + purple_debug_info("msim", "msim_check_alive: %s > interval of %d, presumed dead\n", + errmsg, MSIM_KEEPALIVE_INTERVAL); + purple_connection_error(session->gc, errmsg); + + purple_notify_error(session->gc, NULL, errmsg, NULL); + + g_free(errmsg); + + return FALSE; + } + + return TRUE; +} +#endif + +/** Handle mail reply checks. */ +static void +msim_check_inbox_cb(MsimSession *session, MsimMessage *reply, gpointer data) +{ + GHashTable *body; + gchar *body_str; + GString *notification; + guint old_inbox_status; + guint i, n; + const gchar *froms[5], *tos[5], *urls[5], *subjects[5]; + + /* Three parallel arrays for each new inbox message type. */ + static const gchar *inbox_keys[] = + { + "Mail", + "BlogComment", + "ProfileComment", + "FriendRequest", + "PictureComment" + }; + + static const guint inbox_bits[] = + { + MSIM_INBOX_MAIL, + MSIM_INBOX_BLOG_COMMENT, + MSIM_INBOX_PROFILE_COMMENT, + MSIM_INBOX_FRIEND_REQUEST, + MSIM_INBOX_PICTURE_COMMENT + }; + + static const gchar *inbox_urls[] = + { + "http://messaging.myspace.com/index.cfm?fuseaction=mail.inbox", + "http://blog.myspace.com/index.cfm?fuseaction=blog", + "http://home.myspace.com/index.cfm?fuseaction=user", + "http://messaging.myspace.com/index.cfm?fuseaction=mail.friendRequests", + "http://home.myspace.com/index.cfm?fuseaction=user" + }; + + static const gchar *inbox_text[5]; + + /* Can't write _()'d strings in array initializers. Workaround. */ + inbox_text[0] = _("New mail messages"); + inbox_text[1] = _("New blog comments"); + inbox_text[2] = _("New profile comments"); + inbox_text[3] = _("New friend requests!"); + inbox_text[4] = _("New picture comments"); + + g_return_if_fail(reply != NULL); + + msim_msg_dump("msim_check_inbox_cb: reply=%s\n", reply); + + body_str = msim_msg_get_string(reply, "body"); + g_return_if_fail(body_str != NULL); + + body = msim_parse_body(body_str); + g_free(body_str); + + notification = g_string_new(""); + + old_inbox_status = session->inbox_status; + + n = 0; + + for (i = 0; i < sizeof(inbox_keys) / sizeof(inbox_keys[0]); ++i) { + const gchar *key; + guint bit; + + key = inbox_keys[i]; + bit = inbox_bits[i]; + + if (g_hash_table_lookup(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] = inbox_text[i]; + 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] = inbox_urls[i]; + + ++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 */ + + } + + g_hash_table_destroy(body); +} + +/* Send request to check if there is new mail. */ +static gboolean +msim_check_inbox(gpointer data) +{ + MsimSession *session; + + session = (MsimSession *)data; + + 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; +} + +/** Called when the session key arrives. */ +static gboolean +msim_we_are_logged_on(MsimSession *session, MsimMessage *msg) +{ + MsimMessage *body; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != 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"); + + /* 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"); + + /* 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); + + + /* Additional post-connect operations */ + + + if (msim_msg_get_integer(msg, "uniquenick") == session->userid) { + purple_debug_info("msim_we_are_logged_on", "TODO: pick 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 + + purple_timeout_add(MSIM_MAIL_INTERVAL_CHECK, + (GSourceFunc)msim_check_inbox, session); + + msim_check_inbox(session); + + return TRUE; +} + +/** + * Process a message. + * + * @param session + * @param msg A message from the server, ready for processing (possibly with resolved username information attached). Caller frees. + * + * @return TRUE if successful. FALSE if processing failed. + */ +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); + } 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; + } +} + +/** Callback for when a buddy icon finished being downloaded. */ +static void +msim_downloaded_buddy_icon(PurpleUtilFetchUrlData *url_data, + gpointer user_data, + const gchar *url_text, + gsize len, + const gchar *error_message) +{ + MsimUser *user; + + user = (MsimUser *)user_data; + + purple_debug_info("msim_downloaded_buddy_icon", + "Downloaded %d bytes\n", len); + + purple_buddy_icons_set_for_user(user->buddy->account, + user->buddy->name, + (gchar *)url_text, len, + /* Use URL itself as buddy icon "checksum" */ + user->image_url); +} + +/** Store a field of information about a buddy. */ +static void +msim_store_user_info_each(gpointer key, gpointer value, gpointer user_data) +{ + MsimUser *user; + gchar *key_str, *value_str; + + user = (MsimUser *)user_data; + key_str = (gchar *)key; + value_str = (gchar *)value; + + if (!strcmp(key_str, "UserID")) { + /* Save to buddy list, if it exists, for quick cached uid lookup with msim_uid2username_from_blist(). */ + if (user->buddy) + { + purple_debug_info("msim", "associating uid %s with username %s\n", key_str, user->buddy->name); + purple_blist_node_set_int(&user->buddy->node, "UserID", atol(value_str)); + } + /* Need to store in MsimUser, too? What if not on blist? */ + } else if (!strcmp(key_str, "Age")) { + user->age = atol(value_str); + } else if (!strcmp(key_str, "Gender")) { + user->gender = g_strdup(value_str); + } else if (!strcmp(key_str, "Location")) { + user->location = g_strdup(value_str); + } else if (!strcmp(key_str, "TotalFriends")) { + user->total_friends = atol(value_str); + } else if (!strcmp(key_str, "DisplayName")) { + user->display_name = g_strdup(value_str); + } else if (!strcmp(key_str, "BandName")) { + user->band_name = g_strdup(value_str); + } else if (!strcmp(key_str, "SongName")) { + user->song_name = g_strdup(value_str); + } else if (!strcmp(key_str, "UserName")) { + /* Ignore because PurpleBuddy knows this already */ + ; + } else if (!strcmp(key_str, "ImageURL")) { + const gchar *previous_url; + + user->image_url = g_strdup(value_str); + + previous_url = purple_buddy_icons_get_checksum_for_user(user->buddy); + + /* Only download if URL changed */ + if (!previous_url || strcmp(previous_url, user->image_url)) { + purple_util_fetch_url(user->image_url, TRUE, NULL, TRUE, msim_downloaded_buddy_icon, (gpointer)user); + } + } else { + /* TODO: other fields in MsimUser */ + gchar *msg; + + msg = g_strdup_printf("msim_store_user_info_each: unknown field %s=%s", + key_str, value_str); + + msim_unrecognized(NULL, NULL, msg); + + g_free(msg); + } +} + +/** Save buddy information to the buddy list from a user info reply message. + * + * @param session + * @param msg The user information reply, with any amount of information. + * @param user The structure to save to, or NULL to save in PurpleBuddy->proto_data. + * + * Variable information is saved to the passed MsimUser structure. Permanent + * information (UserID) is stored in the blist node of the buddy list (and + * ends up in blist.xml, persisted to disk) if it exists. + * + * If the function has no buddy information, this function + * is a no-op (and returns FALSE). + * + */ +static gboolean +msim_store_user_info(MsimSession *session, MsimMessage *msg, MsimUser *user) +{ + GHashTable *body; + gchar *username, *body_str; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + body_str = msim_msg_get_string(msg, "body"); + if (!body_str) { + return FALSE; + } + + g_return_val_if_fail(body_str != NULL, FALSE); + body = msim_parse_body(body_str); + g_free(body_str); + + + /* TODO: implement a better hash-like interface, and use it. */ + username = g_hash_table_lookup(body, "UserName"); + + if (!username) { + purple_debug_info("msim", + "msim_process_reply: not caching body, no UserName\n"); + g_hash_table_destroy(body); + return FALSE; + } + + /* Null user = find and store in PurpleBuddy's proto_data */ + if (!user) { + user = msim_find_user(session, username); + g_return_val_if_fail(user != NULL, FALSE); + } + + g_hash_table_foreach(body, msim_store_user_info_each, user); + + if (msim_msg_get_integer(msg, "dsn") == MG_OWN_IM_INFO_DSN && + msim_msg_get_integer(msg, "lid") == MG_OWN_IM_INFO_LID) { + /* TODO: do something with our own IM info, if we need it for some + * specific purpose. Otherwise it is available on the buddy list, + * if the user has themselves as their own buddy. + * + * However, much of the info is already available in MsimSession, + * stored in msim_we_are_logged_on(). */ + } else if (msim_msg_get_integer(msg, "dsn") == MG_OWN_MYSPACE_INFO_DSN && + msim_msg_get_integer(msg, "lid") == MG_OWN_MYSPACE_INFO_LID) { + /* TODO: same as above, but for MySpace info. */ + } + + g_hash_table_destroy(body); + + return TRUE; +} + +/** Process the initial server information from the server. */ +static gboolean +msim_process_server_info(MsimSession *session, MsimMessage *msg) +{ + gchar *body_str; + GHashTable *body; + + body_str = msim_msg_get_string(msg, "body"); + g_return_val_if_fail(body_str != NULL, FALSE); + body = msim_parse_body(body_str); + g_free(body_str); + 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: %s\n", + g_hash_table_lookup(body, "MaxContacts") ? + g_hash_table_lookup(body, "MaxContacts") : "(NULL)"); + + 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 */ + 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_body: calling callback now\n"); + /* Clone message, so that the callback 'cb' can use it (needs to free it also). */ + cb(session, msim_msg_clone(msg), data); + g_hash_table_remove(session->user_lookup_cb, GUINT_TO_POINTER(rid)); + g_hash_table_remove(session->user_lookup_cb_data, GUINT_TO_POINTER(rid)); + } else { + purple_debug_info("msim", + "msim_process_body: no callback for rid %d\n", rid); + } + + return TRUE; +} + +/** + * Handle an error from the server. + * + * @param session + * @param msg The message. + * + * @return TRUE if successfully reported error. + */ +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); + + purple_notify_error(session->account, g_strdup(_("MySpaceIM Error")), + full_errmsg, NULL); + + /* Destroy session if fatal. */ + if (msim_msg_get(msg, "fatal")) { + purple_debug_info("msim", "fatal error, closing\n"); + purple_connection_error(session->gc, full_errmsg); + } + + return TRUE; +} + +/** + * Process incoming status messages. + * + * @param session + * @param msg Status update message. Caller frees. + * + * @return TRUE if successful. + */ +static gboolean +msim_incoming_status(MsimSession *session, MsimMessage *msg) +{ + PurpleBuddyList *blist; + MsimUser *user; + GList *list; + gchar *status_headline; + gint status_code, purple_status_code; + gchar *username; + + g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); + g_return_val_if_fail(msg != NULL, FALSE); + + 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 = atoi(g_list_nth_data(list, MSIM_STATUS_ORDINAL_ONLINE)); + purple_debug_info("msim", "msim_status: %s's status code = %d\n", username, status_code); + status_headline = g_list_nth_data(list, MSIM_STATUS_ORDINAL_HEADLINE); + + blist = purple_get_blist(); + + /* Add buddy if not found */ + 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); + + /* All buddies on list should have 'uid' integer associated with them. */ + purple_blist_node_set_int(&buddy->node, "UserID", msim_msg_get_integer(msg, "f")); + + msim_store_user_info(session, msg, NULL); + } else { + purple_debug_info("msim", "msim_status: found buddy %s\n", username); + } + + user->headline = g_strdup(status_headline); + + /* Set user status */ + switch (status_code) { + case MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN: + purple_status_code = PURPLE_STATUS_OFFLINE; + break; + + case MSIM_STATUS_CODE_ONLINE: + purple_status_code = PURPLE_STATUS_AVAILABLE; + break; + + case MSIM_STATUS_CODE_AWAY: + purple_status_code = PURPLE_STATUS_AWAY; + break; + + case MSIM_STATUS_CODE_IDLE: + /* will be handled below */ + purple_status_code = -1; + break; + + default: + purple_debug_info("msim", "msim_status for %s, unknown status code %d, treating as available\n", + username, status_code); + purple_status_code = PURPLE_STATUS_AVAILABLE; + } + + 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 + + g_free(username); + msim_msg_list_free(list); + + return TRUE; +} + +/** Add a buddy to user's buddy list. */ +void +msim_add_buddy(PurpleConnection *gc, PurpleBuddy *buddy, PurpleGroup *group) +{ + MsimSession *session; + MsimMessage *msg; + /* MsimMessage *msg_persist; */ + MsimMessage *body; + + session = (MsimSession *)gc->proto_data; + 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( + "addbuddy", MSIM_TYPE_BOOLEAN, TRUE, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + /* "newprofileid" will be inserted here with uid. */ + "reason", MSIM_TYPE_STRING, g_strdup(""), + NULL); + + if (!msim_postprocess_outgoing(session, msg, buddy->name, "newprofileid", "reason")) { + purple_notify_error(NULL, NULL, _("Failed to add buddy"), _("'addbuddy' command failed.")); + msim_msg_free(msg); + return; + } + msim_msg_free(msg); + + /* TODO: if addbuddy fails ('error' message is returned), delete added buddy from + * buddy list since Purple adds it locally. */ + + body = msim_msg_new( + "ContactID", MSIM_TYPE_STRING, g_strdup("<uid>"), + "GroupName", MSIM_TYPE_STRING, g_strdup(group->name), + "Position", MSIM_TYPE_INTEGER, 1000, + "Visibility", MSIM_TYPE_INTEGER, 1, + "NickName", MSIM_TYPE_STRING, g_strdup(""), + "NameSelect", MSIM_TYPE_INTEGER, 0, + NULL); + + /* TODO: Update blocklist. */ + +#if 0 + msg_persist = msim_msg_new( + "persist", MSIM_TYPE_INTEGER, 1, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + "cmd", MSIM_TYPE_INTEGER, MSIM_CMD_BIT_ACTION | MSIM_CMD_PUT, + "dsn", MSIM_TYPE_INTEGER, MC_CONTACT_INFO_DSN, + "lid", MSIM_TYPE_INTEGER, MC_CONTACT_INFO_LID, + /* TODO: Use msim_new_reply_callback to get rid. */ + "rid", MSIM_TYPE_INTEGER, session->next_rid++, + "body", MSIM_TYPE_DICTIONARY, body, + NULL); + + if (!msim_postprocess_outgoing(session, msg_persist, buddy->name, "body", NULL)) + { + purple_notify_error(NULL, NULL, _("Failed to add buddy"), _("persist command failed")); + msim_msg_free(msg_persist); + return; + } + msim_msg_free(msg_persist); +#endif + +} + +/** Perform actual postprocessing on a message, adding userid as specified. + * + * @param msg The message to postprocess. + * @param uid_before Name of field where to insert new field before, or NULL for end. + * @param uid_field_name Name of field to add uid to. + * @param uid The userid to insert. + * + * If the field named by uid_field_name already exists, then its string contents will + * be used for the field, except "<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) +{ + msim_msg_dump("msim_do_postprocessing msg: %s\n", msg); + + /* First, check - if the field already exists, replace <uid> within it */ + if (msim_msg_get(msg, uid_field_name)) { + MsimMessageElement *elem; + gchar *fmt_string; + gchar *uid_str, *new_str; + + /* Warning: this is a delicate, but safe, operation */ + + elem = msim_msg_get(msg, uid_field_name); + + /* 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 = str_replace(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; +} + +/** 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 *body_str; + GHashTable *body; + gchar *uid, *uid_field_name, *uid_before; + MsimMessage *msg; + + msg = (MsimMessage *)data; + + msim_msg_dump("msim_postprocess_outgoing_cb() got msg=%s\n", msg); + + /* Obtain userid from userinfo message. */ + body_str = msim_msg_get_string(userinfo, "body"); + g_return_if_fail(body_str != NULL); + body = msim_parse_body(body_str); + g_free(body_str); + + uid = g_strdup(g_hash_table_lookup(body, "UserID")); + g_hash_table_destroy(body); + + uid_field_name = msim_msg_get_string(msg, "_uid_field_name"); + uid_before = msim_msg_get_string(msg, "_uid_before"); + + msg = msim_do_postprocessing(msg, uid_before, uid_field_name, atol(uid)); + + /* Send */ + if (!msim_msg_send(session, msg)) { + 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_hash_table_destroy(body); + + //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. + */ +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); + + //msim_msg_free(msg); + + return rc; +} + +/** Remove a buddy from the user's buddy list. */ +void +msim_remove_buddy(PurpleConnection *gc, PurpleBuddy *buddy, PurpleGroup *group) +{ + MsimSession *session; + MsimMessage *delbuddy_msg; + MsimMessage *persist_msg; + MsimMessage *blocklist_msg; + GList *blocklist_updates; + + session = (MsimSession *)gc->proto_data; + + delbuddy_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, delbuddy_msg, buddy->name, "delprofileid", NULL)) { + purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("'delbuddy' command failed")); + msim_msg_free(delbuddy_msg); + return; + } + msim_msg_free(delbuddy_msg); + + persist_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, MD_DELETE_BUDDY_DSN, + "lid", MSIM_TYPE_INTEGER, MD_DELETE_BUDDY_LID, + "uid", MSIM_TYPE_INTEGER, session->userid, + "rid", MSIM_TYPE_INTEGER, session->next_rid++, + /* <uid> will be replaced by postprocessing */ + "body", MSIM_TYPE_STRING, g_strdup("ContactID=<uid>"), + NULL); + + if (!msim_postprocess_outgoing(session, persist_msg, buddy->name, "body", NULL)) { + purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("persist command failed")); + msim_msg_free(persist_msg); + return; + } + 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, + "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, + 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); +} + +/** 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(gc, _("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 don't timeout. */ + session->last_comm = time(NULL); + + /* Only can handle so much data at once... + * If this happens, try recompiling with a higher MSIM_READ_BUF_SIZE. + * Should be large enough to hold the largest protocol message. + */ + if (session->rxoff >= MSIM_READ_BUF_SIZE) { + purple_debug_error("msim", + "msim_input_cb: %d-byte read buffer full! rxoff=%d\n", + MSIM_READ_BUF_SIZE, session->rxoff); + purple_connection_error(gc, _("Read buffer full")); + return; + } + + purple_debug_info("msim", "buffer at %d (max %d), reading up to %d\n", + session->rxoff, MSIM_READ_BUF_SIZE, + MSIM_READ_BUF_SIZE - session->rxoff); + + /* Read into buffer. On Win32, need recv() not read(). session->fd also holds + * the file descriptor, but it sometimes differs from the 'source' parameter. + */ + n = recv(session->fd, session->rxbuf + session->rxoff, MSIM_READ_BUF_SIZE - session->rxoff, 0); + + if (n < 0 && errno == EAGAIN) { + return; + } else if (n < 0) { + purple_debug_error("msim", "msim_input_cb: read error, ret=%d, " + "error=%s, source=%d, fd=%d (%X))\n", + n, strerror(errno), source, session->fd, session->fd); + purple_connection_error(gc, _("Read error")); + return; + } else if (n == 0) { + purple_debug_info("msim", "msim_input_cb: server disconnected\n"); + purple_connection_error(gc, _("Server has disconnected")); + return; + } + + if (n + session->rxoff >= MSIM_READ_BUF_SIZE) { + purple_debug_info("msim_input_cb", "received %d bytes, pushing rxoff to %d, over buffer size of %d\n", + n, n + session->rxoff, MSIM_READ_BUF_SIZE); + /* TODO: g_realloc like msn, yahoo, irc, jabber? */ + purple_connection_error(gc, _("Read buffer full")); + } + + /* 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(gc, "Invalid message - null byte on input"); + return; + } +#endif + + session->rxoff += n; + purple_debug_info("msim", "msim_input_cb: read=%d\n", n); + +#ifdef MSIM_DEBUG_RXBUF + purple_debug_info("msim", "buf=<%s>\n", session->rxbuf); +#endif + + /* Look for \\final\\ end markers. If found, process message. */ + while((end = strstr(session->rxbuf, MSIM_FINAL_STRING))) { + MsimMessage *msg; + +#ifdef MSIM_DEBUG_RXBUF + purple_debug_info("msim", "in loop: buf=<%s>\n", session->rxbuf); +#endif + *end = 0; + msg = msim_parse(g_strdup(session->rxbuf)); + if (!msg) { + purple_debug_info("msim", "msim_input_cb: couldn't parse rxbuf\n"); + purple_connection_error(gc, _("Unparseable message")); + } else { + /* Process message and then free it (processing function should + * clone message if it wants to keep it afterwards.) */ + if (!msim_preprocess_incoming(session, msg)) { + msim_msg_dump("msim_input_cb: preprocessing message failed on msg: %s\n", msg); + } + msim_msg_free(msg); + } + + /* Move remaining part of buffer to beginning. */ + session->rxoff -= strlen(session->rxbuf) + strlen(MSIM_FINAL_STRING); + memmove(session->rxbuf, end + strlen(MSIM_FINAL_STRING), + MSIM_READ_BUF_SIZE - (end + strlen(MSIM_FINAL_STRING) - session->rxbuf)); + + /* Clear end of buffer */ + //memset(end, 0, MSIM_READ_BUF_SIZE - (end - session->rxbuf)); + } +} + +/* Setup a callback, to be called when a reply is received with the returned rid. + * + * @param cb The callback, an MSIM_USER_LOOKUP_CB. + * @param data Arbitrary user data to be passed to callback (probably an MsimMessage *). + * + * @return The request/reply ID, used to link replies with requests, 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? + */ +static 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(gc, _("Couldn't connect to host")); + purple_connection_error(gc, g_strdup_printf( + _("Couldn't connect to host: %s (%d)"), + error_message ? error_message : "no message given", + source)); + return; + } + + session->fd = source; + + gc->inpa = purple_input_add(source, PURPLE_INPUT_READ, msim_input_cb, gc); +} + +/* Session methods */ + +/** + * Create a new MSIM session. + * + * @param acct The account to create the session from. + * + * @return Pointer to a new session. Free with msim_session_destroy. + */ +MsimSession * +msim_session_new(PurpleAccount *acct) +{ + MsimSession *session; + + g_return_val_if_fail(acct != NULL, NULL); + + session = g_new0(MsimSession, 1); + + session->magic = MSIM_SESSION_STRUCT_MAGIC; + session->account = acct; + session->gc = purple_account_get_connection(acct); + session->sesskey = 0; + session->userid = 0; + session->username = NULL; + session->fd = -1; + + /* TODO: Remove. */ + session->user_lookup_cb = g_hash_table_new_full(g_direct_hash, + g_direct_equal, NULL, NULL); /* do NOT free function pointers! (values) */ + session->user_lookup_cb_data = g_hash_table_new_full(g_direct_hash, + g_direct_equal, NULL, NULL);/* TODO: we don't know what the values are, + they could be integers inside gpointers + or strings, so I don't freed them. + Figure this out, once free cache. */ + + /* Created in msim_process_server_info() */ + session->server_info = NULL; + + session->rxoff = 0; + session->rxbuf = g_new0(gchar, MSIM_READ_BUF_SIZE); + session->next_rid = 1; + session->last_comm = time(NULL); + session->inbox_status = 0; + + return session; +} + +/** + * Free a session. + * + * @param session The session to destroy. + */ +void +msim_session_destroy(MsimSession *session) +{ + g_return_if_fail(MSIM_SESSION_VALID(session)); + + session->magic = -1; + + g_free(session->rxbuf); + g_free(session->username); + + /* TODO: Remove. */ + g_hash_table_destroy(session->user_lookup_cb); + g_hash_table_destroy(session->user_lookup_cb_data); + + if (session->server_info) { + g_hash_table_destroy(session->server_info); + } + + g_free(session); +} + +/** + * Close the connection. + * + * @param gc The connection. + */ +void +msim_close(PurpleConnection *gc) +{ + MsimSession *session; + + if (gc == NULL) { + return; + } + + session = (MsimSession *)gc->proto_data; + if (session == NULL) + return; + + gc->proto_data = NULL; + + if (!MSIM_SESSION_VALID(session)) { + return; + } + + if (session->gc->inpa) { + purple_input_remove(session->gc->inpa); + } + + msim_session_destroy(session); +} + + +/** + * Check if a string is a userid (all numeric). + * + * @param user The user id, email, or name. + * + * @return TRUE if is userid, FALSE if not. + */ +static gboolean +msim_is_userid(const gchar *user) +{ + g_return_val_if_fail(user != NULL, FALSE); + + return strspn(user, "0123456789") == strlen(user); +} + +/** + * Check if a string is an email address (contains an @). + * + * @param user The user id, email, or name. + * + * @return TRUE if is an email, FALSE if not. + * + * This function is not intended to be used as a generic + * means of validating email addresses, but to distinguish + * between a user represented by an email address from + * other forms of identification. + */ +static gboolean +msim_is_email(const gchar *user) +{ + g_return_val_if_fail(user != NULL, FALSE); + + return strchr(user, '@') != NULL; +} + + +/** + * Asynchronously lookup user information, calling callback when receive result. + * + * @param session + * @param user The user id, email address, or username. Not freed. + * @param cb Callback, called with user information when available. + * @param data An arbitray data pointer passed to the callback. + */ +/* TODO: change to not use callbacks */ +static void +msim_lookup_user(MsimSession *session, const gchar *user, + MSIM_USER_LOOKUP_CB cb, gpointer data) +{ + MsimMessage *body; + gchar *field_name; + guint rid, cmd, dsn, lid; + + g_return_if_fail(MSIM_SESSION_VALID(session)); + g_return_if_fail(user != NULL); + g_return_if_fail(cb != NULL); + + purple_debug_info("msim", "msim_lookup_userid: " + "asynchronously looking up <%s>\n", user); + + msim_msg_dump("msim_lookup_user: data=%s\n", (MsimMessage *)data); + + /* Setup callback. Response will be associated with request using 'rid'. */ + rid = msim_new_reply_callback(session, cb, data); + + /* Send request */ + + cmd = MSIM_CMD_GET; + + if (msim_is_userid(user)) { + field_name = "UserID"; + dsn = MG_MYSPACE_INFO_BY_ID_DSN; + lid = MG_MYSPACE_INFO_BY_ID_LID; + } else if (msim_is_email(user)) { + field_name = "Email"; + dsn = MG_MYSPACE_INFO_BY_STRING_DSN; + lid = MG_MYSPACE_INFO_BY_STRING_LID; + } else { + field_name = "UserName"; + dsn = MG_MYSPACE_INFO_BY_STRING_DSN; + lid = MG_MYSPACE_INFO_BY_STRING_LID; + } + + body = msim_msg_new( + field_name, MSIM_TYPE_STRING, g_strdup(user), + NULL); + + g_return_if_fail(msim_send(session, + "persist", MSIM_TYPE_INTEGER, 1, + "sesskey", MSIM_TYPE_INTEGER, session->sesskey, + "cmd", MSIM_TYPE_INTEGER, 1, + "dsn", MSIM_TYPE_INTEGER, dsn, + "uid", MSIM_TYPE_INTEGER, session->userid, + "lid", MSIM_TYPE_INTEGER, lid, + "rid", MSIM_TYPE_INTEGER, rid, + "body", MSIM_TYPE_DICTIONARY, body, + NULL)); +} + + +/** + * Obtain the status text for a buddy. + * + * @param buddy The buddy to obtain status text for. + * + * @return Status text, or NULL if error. Caller g_free()'s. + * + */ +char * +msim_status_text(PurpleBuddy *buddy) +{ + MsimSession *session; + MsimUser *user; + const gchar *display_name, *headline; + + g_return_val_if_fail(buddy != NULL, NULL); + + user = msim_get_user_from_buddy(buddy); + + session = (MsimSession *)buddy->account->gc->proto_data; + g_return_val_if_fail(MSIM_SESSION_VALID(session), NULL); + + display_name = headline = NULL; + + /* Retrieve display name and/or headline, depending on user preference. */ + if (purple_account_get_bool(session->account, "show_display_name", TRUE)) { + display_name = user->display_name; + } + + if (purple_account_get_bool(session->account, "show_headline", FALSE)) { + headline = user->headline; + } + + /* 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); + + 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); + } +} + +/** 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(_("Import friends..."), NULL); + menu = g_list_append(menu, act); + + act = purple_plugin_action_new(_("Change IM name..."), NULL); + menu = g_list_append(menu, act); +#endif + + return menu; +} + +/** Callbacks called by Purple, to access this plugin. */ +PurplePluginProtocolInfo prpl_info = { + /* options */ + OPT_PROTO_USE_POINTSIZE /* specify font size in sane point size */ + | OPT_PROTO_MAIL_CHECK, + + /* | OPT_PROTO_IM_IMAGE - TODO: direct images. */ + NULL, /* user_splits */ + NULL, /* protocol_options */ + NO_BUDDY_ICONS, /* icon_spec - TODO: eventually should add this */ + msim_list_icon, /* list_icon */ + NULL, /* list_emblems */ + msim_status_text, /* status_text */ + msim_tooltip_text, /* tooltip_text */ + msim_status_types, /* status_types */ + msim_blist_node_menu, /* blist_node_menu */ + NULL, /* chat_info */ + NULL, /* chat_info_defaults */ + msim_login, /* login */ + msim_close, /* close */ + msim_send_im, /* send_im */ + NULL, /* set_info */ + msim_send_typing, /* send_typing */ + msim_get_info, /* get_info */ + msim_set_status, /* set_status */ + msim_set_idle, /* set_idle */ + NULL, /* change_passwd */ + msim_add_buddy, /* add_buddy */ + NULL, /* add_buddies */ + msim_remove_buddy, /* remove_buddy */ + NULL, /* remove_buddies */ + NULL, /* add_permit */ + NULL, /* add_deny */ + NULL, /* rem_permit */ + NULL, /* rem_deny */ + NULL, /* set_permit_deny */ + NULL, /* join_chat */ + NULL, /* reject chat invite */ + NULL, /* get_chat_name */ + NULL, /* chat_invite */ + NULL, /* chat_leave */ + NULL, /* chat_whisper */ + NULL, /* chat_send */ + NULL, /* keepalive */ + NULL, /* register_user */ + NULL, /* get_cb_info */ + NULL, /* get_cb_away */ + NULL, /* alias_buddy */ + NULL, /* group_buddy */ + NULL, /* rename_group */ + NULL, /* buddy_free */ + NULL, /* convo_closed */ + NULL, /* normalize */ + NULL, /* set_buddy_icon */ + NULL, /* remove_group */ + NULL, /* get_cb_real_name */ + NULL, /* set_chat_topic */ + NULL, /* find_blist_chat */ + NULL, /* roomlist_get_list */ + NULL, /* roomlist_cancel */ + NULL, /* roomlist_expand_category */ + NULL, /* can_receive_file */ + NULL, /* send_file */ + NULL, /* new_xfer */ + msim_offline_message, /* offline_message */ + NULL, /* whiteboard_prpl_ops */ + msim_send_really_raw, /* send_raw */ + NULL, /* roomlist_room_serialize */ + NULL, /* _purple_reserved1 */ + NULL, /* _purple_reserved2 */ + NULL, /* _purple_reserved3 */ + NULL /* _purple_reserved4 */ +}; + + + +/** Based on MSN's plugin info comments. */ +PurplePluginInfo info = { + PURPLE_PLUGIN_MAGIC, + PURPLE_MAJOR_VERSION, + PURPLE_MINOR_VERSION, + PURPLE_PLUGIN_PROTOCOL, /**< type */ + NULL, /**< ui_requirement */ + 0, /**< flags */ + NULL, /**< dependencies */ + PURPLE_PRIORITY_DEFAULT, /**< priority */ + + "prpl-myspace", /**< id */ + "MySpaceIM", /**< name */ + MSIM_PRPL_VERSION_STRING, /**< version */ + /** summary */ + "MySpaceIM Protocol Plugin", + /** description */ + "MySpaceIM Protocol Plugin", + "Jeff Connelly <jeff2@soc.pidgin.im>", /**< author */ + "http://developer.pidgin.im/wiki/MySpaceIM/", /**< homepage */ + + msim_load, /**< load */ + NULL, /**< unload */ + NULL, /**< destroy */ + NULL, /**< ui_info */ + &prpl_info, /**< extra_info */ + NULL, /**< prefs_info */ + msim_actions, /**< msim_actions */ + NULL, /**< reserved1 */ + NULL, /**< reserved2 */ + NULL, /**< reserved3 */ + NULL /**< reserved4 */ +}; + + +#ifdef MSIM_SELF_TEST +/** Test functions. + * Used to test or try out the internal workings of msimprpl. If you're reading + * this code for the first time, these functions can be instructive in 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 +msim_test_msg(void) +{ + MsimMessage *msg, *msg_cloned, *msg2; + GList *list; + gchar *packed, *packed_expected, *packed_cloned; + guint failures; + + failures = 0; + + purple_debug_info("msim", "\n\nTesting MsimMessage\n"); + msg = msim_msg_new(NULL); /* Create a new, empty message. */ + + /* Append some new elements. */ + msg = msim_msg_append(msg, "bx", MSIM_TYPE_BINARY, g_string_new_len(g_strdup("XXX"), 3)); + msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v1")); + msg = msim_msg_append(msg, "k1", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(42)); + msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v43")); + msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v52/xxx\\yyy")); + msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v7")); + msim_msg_dump("msg debug str=%s\n", msg); + packed = msim_msg_pack(msg); + + purple_debug_info("msim", "msg packed=%s\n", packed); + + packed_expected = "\\bx\\WFhY\\k1\\v1\\k1\\42\\k1" + "\\v43\\k1\\v52/1xxx/2yyy\\k1\\v7\\final\\"; + + if (0 != strcmp(packed, packed_expected)) { + purple_debug_info("msim", "!!!(%d), msim_msg_pack not what expected: %s != %s\n", + ++failures, packed, packed_expected); + } + + + msg_cloned = msim_msg_clone(msg); + packed_cloned = msim_msg_pack(msg_cloned); + + purple_debug_info("msim", "msg cloned=%s\n", packed_cloned); + if (0 != strcmp(packed, packed_cloned)) { + purple_debug_info("msim", "!!!(%d), msim_msg_pack on cloned message not equal to original: %s != %s\n", + ++failures, packed_cloned, packed); + } + + g_free(packed); + g_free(packed_cloned); + msim_msg_free(msg_cloned); + msim_msg_free(msg); + + /* Try some of the more advanced functionality */ + list = NULL; + + list = g_list_prepend(list, "item3"); + list = g_list_prepend(list, "item2"); + list = g_list_prepend(list, "item1"); + list = g_list_prepend(list, "item0"); + + msg = msim_msg_new(NULL); + msg = msim_msg_append(msg, "string", MSIM_TYPE_STRING, g_strdup("string value")); + msg = msim_msg_append(msg, "raw", MSIM_TYPE_RAW, g_strdup("raw value")); + msg = msim_msg_append(msg, "integer", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(3140)); + msg = msim_msg_append(msg, "boolean", MSIM_TYPE_BOOLEAN, GUINT_TO_POINTER(FALSE)); + msg = msim_msg_append(msg, "list", MSIM_TYPE_LIST, list); + + msim_msg_dump("msg with list=%s\n", msg); + purple_debug_info("msim", "msg with list packed=%s\n", msim_msg_pack(msg)); + + msg2 = msim_msg_new(NULL); + msg2 = msim_msg_append(msg2, "outer", MSIM_TYPE_STRING, g_strdup("outer value")); + msg2 = msim_msg_append(msg2, "body", MSIM_TYPE_DICTIONARY, msg); + msim_msg_dump("msg with dict=%s\n", msg2); /* msg2 now 'owns' msg */ + purple_debug_info("msim", "msg with dict packed=%s\n", msim_msg_pack(msg2)); + + msim_msg_free(msg2); + + return failures; +} + +/** Test protocol-level escaping/unescaping. */ +int +msim_test_escaping(void) +{ + guint failures; + gchar *raw, *escaped, *unescaped, *expected; + + failures = 0; + + purple_debug_info("msim", "\n\nTesting escaping\n"); + + raw = "hello/world\\hello/world"; + + escaped = msim_escape(raw); + purple_debug_info("msim", "msim_test_escaping: raw=%s, escaped=%s\n", raw, escaped); + expected = "hello/1world/2hello/1world"; + if (0 != strcmp(escaped, expected)) { + purple_debug_info("msim", "!!!(%d), msim_escape failed: %s != %s\n", + ++failures, escaped, expected); + } + + + unescaped = msim_unescape(escaped); + g_free(escaped); + purple_debug_info("msim", "msim_test_escaping: unescaped=%s\n", unescaped); + if (0 != strcmp(raw, unescaped)) { + purple_debug_info("msim", "!!!(%d), msim_unescape failed: %s != %s\n", + ++failures, raw, unescaped); + } + + return failures; +} +#endif + +/** Initialize plugin. */ +void +init_plugin(PurplePlugin *plugin) +{ + PurpleAccountOption *option; +#ifdef MSIM_SELF_TEST + msim_test_all(); + exit(0); +#endif /* MSIM_SELF_TEST */ + + + /* TODO: default to automatically try different ports. Make the user be + * able to set the first port to try (like LastConnectedPort in Windows client). */ + option = purple_account_option_string_new(_("Connect server"), "server", MSIM_SERVER); + prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); + + option = purple_account_option_int_new(_("Connect port"), "port", MSIM_PORT); + prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); + +#ifdef MSIM_USER_WANTS_TO_CONFIGURE_STATUS_TEXT + option = purple_account_option_bool_new(_("Show display name in status text"), "show_display_name", TRUE); + prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); + + option = purple_account_option_bool_new(_("Show headline in status text"), "show_headline", TRUE); + prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); +#endif + +#ifdef MSIM_USER_WANTS_TO_DISABLE_EMOTICONS + option = purple_account_option_bool_new(_("Send emoticons"), "emoticons", FALSE); + prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); +#endif + +#ifdef MSIM_USER_REALLY_CARES_ABOUT_PRECISE_FONT_SIZES + option = purple_account_option_int_new(_("Screen resolution (dots per inch)"), "dpi", MSIM_DEFAULT_DPI); + prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); + + option = purple_account_option_int_new(_("Base font size (points)"), "base_font_size", MSIM_BASE_FONT_POINT_SIZE); + prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); +#endif + + /* TODO: /zap command. Problem with this is that there are different kinds of zaps, + * and the selection is best made available in a drop-down menu, instead of forcing + * the user to type the kind of zap and memorizing available zaps (or putting it in the + * help menu). A new "attention" API, for zap/buzz/nudge (different protocols) will + * solve this. */ +#if 0 + purple_cmd_register("zap", /* cmd */ + "w", /* args - accept a single word */ + PURPLE_CMD_P_PRPL, /* priority */ + PURPLE_CMD_FLAG_IM | PURPLE_CMD_FLAG_PRPL_ONLY, /* flags */ + "prpl-myspace", /* prpl_id */ + msim_cmd_zap, /* func */ + _("zap: zap a user to get their attention"), /* helpstr */ + NULL); /* data */ +#endif +} + +PURPLE_INIT_PLUGIN(myspace, init_plugin, info);
--- a/libpurple/protocols/oscar/flap_connection.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/oscar/flap_connection.c Tue Aug 14 02:45:41 2007 +0000 @@ -129,8 +129,8 @@ new_current = rateclass_get_new_current(conn, rateclass, &now); + /* (Add 100ms padding to account for inaccuracies in the calculation) */ if (new_current < rateclass->alert + 100) - /* (Add 100ms padding to account for inaccuracies in the calculation) */ /* Not ready to send this SNAC yet--keep waiting. */ return TRUE; @@ -186,9 +186,9 @@ gettimeofday(&now, NULL); new_current = rateclass_get_new_current(conn, rateclass, &now); + /* (Add 100ms padding to account for inaccuracies in the calculation) */ if (new_current < rateclass->alert + 100) { - /* (Add 100ms padding to account for inaccuracies in the calculation) */ enqueue = TRUE; } else
--- a/libpurple/protocols/oscar/oscar.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/oscar/oscar.c Tue Aug 14 02:45:41 2007 +0000 @@ -161,7 +161,6 @@ static int purple_conv_chat_info_update (OscarData *, FlapConnection *, FlapFrame *, ...); static int purple_conv_chat_incoming_msg(OscarData *, FlapConnection *, FlapFrame *, ...); static int purple_email_parseupdate(OscarData *, FlapConnection *, FlapFrame *, ...); -static int purple_icon_error (OscarData *, FlapConnection *, FlapFrame *, ...); static int purple_icon_parseicon (OscarData *, FlapConnection *, FlapFrame *, ...); static int oscar_icon_req (OscarData *, FlapConnection *, FlapFrame *, ...); static int purple_parse_msgack (OscarData *, FlapConnection *, FlapFrame *, ...); @@ -195,7 +194,7 @@ static int purple_ssi_authreply (OscarData *, FlapConnection *, FlapFrame *, ...); static int purple_ssi_gotadded (OscarData *, FlapConnection *, FlapFrame *, ...); -static gboolean purple_icon_timerfunc(gpointer data); +static void purple_icons_fetch(PurpleConnection *gc); static void recent_buddies_cb(const char *name, PurplePrefType type, gconstpointer value, gpointer data); void oscar_set_info(PurpleConnection *gc, const char *info); @@ -483,7 +482,7 @@ /* Attempt to send as ASCII */ if (oscar_charset_check(from) == AIM_CHARSET_ASCII) { - *msg = g_convert(from, strlen(from), "ASCII", "UTF-8", NULL, &msglen, NULL); + *msg = g_convert(from, -1, "ASCII", "UTF-8", NULL, &msglen, NULL); *charset = AIM_CHARSET_ASCII; *charsubset = 0x0000; *msglen_int = msglen; @@ -505,7 +504,7 @@ b = purple_find_buddy(account, destsn); if ((b != NULL) && (PURPLE_BUDDY_IS_ONLINE(b))) { - *msg = g_convert(from, strlen(from), "UCS-2BE", "UTF-8", NULL, &msglen, NULL); + *msg = g_convert(from, -1, "UCS-2BE", "UTF-8", NULL, &msglen, NULL); if (*msg != NULL) { *charset = AIM_CHARSET_UNICODE; @@ -528,7 +527,7 @@ * XXX - We need a way to only attempt to convert if we KNOW "from" * can be converted to "charsetstr" */ - *msg = g_convert(from, strlen(from), charsetstr, "UTF-8", NULL, &msglen, NULL); + *msg = g_convert(from, -1, charsetstr, "UTF-8", NULL, &msglen, NULL); if (*msg != NULL) { *charset = AIM_CHARSET_CUSTOM; *charsubset = 0x0000; @@ -539,7 +538,7 @@ /* * Nothing else worked, so send as UCS-2BE. */ - *msg = g_convert(from, strlen(from), "UCS-2BE", "UTF-8", NULL, &msglen, &err); + *msg = g_convert(from, -1, "UCS-2BE", "UTF-8", NULL, &msglen, &err); if (*msg != NULL) { *charset = AIM_CHARSET_UNICODE; *charsubset = 0x0000; @@ -1156,8 +1155,7 @@ od->iconconnecting = FALSE; - if (od->icontimer == 0) - od->icontimer = purple_timeout_add(100, purple_icon_timerfunc, gc); + purple_icons_fetch(gc); } static int @@ -1203,7 +1201,6 @@ oscar_data_addhandler(od, SNAC_FAMILY_AUTH, 0x0003, purple_parse_auth_resp, 0); oscar_data_addhandler(od, SNAC_FAMILY_AUTH, 0x0007, purple_parse_login, 0); oscar_data_addhandler(od, SNAC_FAMILY_AUTH, SNAC_SUBTYPE_AUTH_SECURID_REQUEST, purple_parse_auth_securid_request, 0); - oscar_data_addhandler(od, SNAC_FAMILY_BART, SNAC_SUBTYPE_BART_ERROR, purple_icon_error, 0); oscar_data_addhandler(od, SNAC_FAMILY_BART, SNAC_SUBTYPE_BART_RESPONSE, purple_icon_parseicon, 0); oscar_data_addhandler(od, SNAC_FAMILY_BOS, 0x0001, purple_parse_genericerr, 0); oscar_data_addhandler(od, SNAC_FAMILY_BOS, 0x0003, purple_bosrights, 0); @@ -1873,13 +1870,12 @@ saved_b16 = purple_buddy_icons_get_checksum_for_user(b); if (!b16 || !saved_b16 || strcmp(b16, saved_b16)) { - GSList *cur = od->requesticon; - while (cur && aim_sncmp((char *)cur->data, info->sn)) - cur = cur->next; - if (!cur) { - od->requesticon = g_slist_append(od->requesticon, g_strdup(purple_normalize(account, info->sn))); - if (od->icontimer == 0) - od->icontimer = purple_timeout_add(500, purple_icon_timerfunc, gc); + if (g_slist_find_custom(od->requesticon, info->sn, + (GCompareFunc)aim_sncmp) == NULL) + { + od->requesticon = g_slist_prepend(od->requesticon, + g_strdup(purple_normalize(account, info->sn))); + purple_icons_fetch(gc); } } g_free(b16); @@ -2265,8 +2261,9 @@ /* When other people ask you for authorization */ static void -purple_auth_grant(struct name_data *data) +purple_auth_grant(gpointer cbdata) { + struct name_data *data = cbdata; PurpleConnection *gc = data->gc; OscarData *od = gc->proto_data; @@ -2286,8 +2283,9 @@ } static void -purple_auth_dontgrant_msgprompt(struct name_data *data) +purple_auth_dontgrant_msgprompt(gpointer cbdata) { + struct name_data *data = cbdata; purple_request_input(data->gc, NULL, _("Authorization Denied Message:"), NULL, _("No reason given."), TRUE, FALSE, NULL, _("_OK"), G_CALLBACK(purple_auth_dontgrant), @@ -2408,8 +2406,8 @@ purple_account_request_authorization(account, sn, NULL, NULL, reason, purple_find_buddy(account, sn) != NULL, - G_CALLBACK(purple_auth_grant), - G_CALLBACK(purple_auth_dontgrant_msgprompt), data); + purple_auth_grant, + purple_auth_dontgrant_msgprompt, data); g_free(reason); } } break; @@ -3237,24 +3235,8 @@ return 1; } -static int purple_icon_error(OscarData *od, FlapConnection *conn, FlapFrame *fr, ...) { - PurpleConnection *gc = od->gc; - char *sn; - - sn = od->requesticon->data; - purple_debug_misc("oscar", "removing %s from hash table\n", sn); - od->requesticon = g_slist_remove(od->requesticon, sn); - g_free(sn); - - if (od->icontimer == 0) - od->icontimer = purple_timeout_add(500, purple_icon_timerfunc, gc); - - return 1; -} - static int purple_icon_parseicon(OscarData *od, FlapConnection *conn, FlapFrame *fr, ...) { PurpleConnection *gc = od->gc; - GSList *cur; va_list ap; char *sn; guint8 iconcsumtype, *iconcsum, *icon; @@ -3280,38 +3262,23 @@ g_free(b16); } - cur = od->requesticon; - while (cur) { - char *cursn = cur->data; - if (!aim_sncmp(cursn, sn)) { - od->requesticon = g_slist_remove(od->requesticon, cursn); - g_free(cursn); - cur = od->requesticon; - } else - cur = cur->next; - } - - if (od->icontimer == 0) - od->icontimer = purple_timeout_add(250, purple_icon_timerfunc, gc); - return 1; } -static gboolean purple_icon_timerfunc(gpointer data) { - PurpleConnection *gc = data; +static void +purple_icons_fetch(PurpleConnection *gc) +{ OscarData *od = gc->proto_data; aim_userinfo_t *userinfo; FlapConnection *conn; - od->icontimer = 0; - conn = flap_connection_getbytype(od, SNAC_FAMILY_BART); if (!conn) { if (!od->iconconnecting) { aim_srv_requestnew(od, SNAC_FAMILY_BART); od->iconconnecting = TRUE; } - return FALSE; + return; } if (od->set_icon) { @@ -3322,32 +3289,24 @@ } else { purple_debug_info("oscar", "Uploading icon to icon server\n"); - aim_bart_upload(od, purple_imgstore_get_data(img), + aim_bart_upload(od, purple_imgstore_get_data(img), purple_imgstore_get_size(img)); purple_imgstore_unref(img); } od->set_icon = FALSE; } - if (!od->requesticon) { - purple_debug_misc("oscar", - "no more icons to request\n"); - return FALSE; - } - - userinfo = aim_locate_finduserinfo(od, (char *)od->requesticon->data); - if ((userinfo != NULL) && (userinfo->iconcsumlen > 0)) { - aim_bart_request(od, od->requesticon->data, userinfo->iconcsumtype, userinfo->iconcsum, userinfo->iconcsumlen); - return FALSE; - } else { - gchar *sn = od->requesticon->data; - od->requesticon = g_slist_remove(od->requesticon, sn); - g_free(sn); - } - - od->icontimer = purple_timeout_add(100, purple_icon_timerfunc, gc); - - return FALSE; + while (od->requesticon != NULL) + { + userinfo = aim_locate_finduserinfo(od, (char *)od->requesticon->data); + if ((userinfo != NULL) && (userinfo->iconcsumlen > 0)) + aim_bart_request(od, od->requesticon->data, userinfo->iconcsumtype, userinfo->iconcsum, userinfo->iconcsumlen); + + g_free(od->requesticon->data); + od->requesticon = g_slist_delete_link(od->requesticon, od->requesticon); + } + + purple_debug_misc("oscar", "no more icons to request\n"); } /* @@ -4389,10 +4348,10 @@ charset = oscar_charset_check(str); if (charset == AIM_CHARSET_UNICODE) { - encoded = g_convert(str, strlen(str), "UCS-2BE", "UTF-8", NULL, ret_len, NULL); + encoded = g_convert(str, -1, "UCS-2BE", "UTF-8", NULL, ret_len, NULL); *encoding = "unicode-2-0"; } else if (charset == AIM_CHARSET_CUSTOM) { - encoded = g_convert(str, strlen(str), "ISO-8859-1", "UTF-8", NULL, ret_len, NULL); + encoded = g_convert(str, -1, "ISO-8859-1", "UTF-8", NULL, ret_len, NULL); *encoding = "iso-8859-1"; } else { encoded = g_strdup(str); @@ -5213,8 +5172,8 @@ purple_account_request_authorization(account, sn, NULL, (buddy ? purple_buddy_get_alias_only(buddy) : NULL), - reason, buddy != NULL, G_CALLBACK(purple_auth_grant), - G_CALLBACK(purple_auth_dontgrant_msgprompt), data); + reason, buddy != NULL, purple_auth_grant, + purple_auth_dontgrant_msgprompt, data); g_free(reason); return 1;
--- a/libpurple/protocols/oscar/oscar.h Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/oscar/oscar.h Tue Aug 14 02:45:41 2007 +0000 @@ -449,7 +449,6 @@ GSList *requesticon; gboolean icq; - guint icontimer; guint getblisttimer; guint getinfotimer;
--- a/libpurple/protocols/oscar/oscar_data.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/oscar/oscar_data.c Tue Aug 14 02:45:41 2007 +0000 @@ -95,8 +95,6 @@ g_free(od->email); g_free(od->newp); g_free(od->oldp); - if (od->icontimer > 0) - purple_timeout_remove(od->icontimer); if (od->getblisttimer > 0) purple_timeout_remove(od->getblisttimer); if (od->getinfotimer > 0)
--- a/libpurple/protocols/yahoo/util.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/yahoo/util.c Tue Aug 14 02:45:41 2007 +0000 @@ -61,7 +61,7 @@ else to_codeset = purple_account_get_string(purple_connection_get_account(gc), "local_charset", "ISO-8859-1"); - ret = g_convert_with_fallback(str, strlen(str), to_codeset, "UTF-8", "?", NULL, NULL, NULL); + ret = g_convert_with_fallback(str, -1, to_codeset, "UTF-8", "?", NULL, NULL, NULL); if (ret) return ret; else @@ -92,7 +92,7 @@ else from_codeset = purple_account_get_string(purple_connection_get_account(gc), "local_charset", "ISO-8859-1"); - ret = g_convert_with_fallback(str, strlen(str), "UTF-8", from_codeset, NULL, NULL, NULL, NULL); + ret = g_convert_with_fallback(str, -1, "UTF-8", from_codeset, NULL, NULL, NULL, NULL); if (ret) return ret;
--- a/libpurple/protocols/yahoo/yahoo.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/yahoo/yahoo.c Tue Aug 14 02:45:41 2007 +0000 @@ -953,7 +953,8 @@ }; static void -yahoo_buddy_add_authorize_cb(struct yahoo_add_request *add_req) { +yahoo_buddy_add_authorize_cb(gpointer data) { + struct yahoo_add_request *add_req = data; g_free(add_req->id); g_free(add_req->who); g_free(add_req->msg); @@ -997,7 +998,8 @@ } static void -yahoo_buddy_add_deny_reason_cb(struct yahoo_add_request *add_req) { +yahoo_buddy_add_deny_reason_cb(gpointer data) { + struct yahoo_add_request *add_req = data; purple_request_input(add_req->gc, NULL, _("Authorization denied message:"), NULL, _("No reason given."), TRUE, FALSE, NULL, _("OK"), G_CALLBACK(yahoo_buddy_add_deny_cb), @@ -1042,8 +1044,8 @@ */ purple_account_request_authorization(purple_connection_get_account(gc), add_req->who, add_req->id, NULL, add_req->msg, purple_find_buddy(purple_connection_get_account(gc),add_req->who) != NULL, - G_CALLBACK(yahoo_buddy_add_authorize_cb), - G_CALLBACK(yahoo_buddy_add_deny_reason_cb), + yahoo_buddy_add_authorize_cb, + yahoo_buddy_add_deny_reason_cb, add_req); } else { g_free(add_req->id); @@ -3681,8 +3683,18 @@ group2 = yahoo_string_encode(gc, group, NULL); pkt = yahoo_packet_new(YAHOO_SERVICE_ADDBUDDY, YAHOO_STATUS_AVAILABLE, 0); - yahoo_packet_hash(pkt, "ssss", 1, purple_connection_get_display_name(gc), - 7, buddy->name, 65, group2, 14, ""); + yahoo_packet_hash(pkt, "ssssssssss", + 14, "", + 65, group2, + 97, "1", + 1, purple_connection_get_display_name(gc), + 302, "319", + 300, "319", + 7, buddy->name, + 334, "0", + 301, "319", + 303, "319" + ); yahoo_packet_send_and_free(pkt, yd); g_free(group2); } @@ -3819,16 +3831,12 @@ return; } - /* Step 1: Add buddy to new group. */ - pkt = yahoo_packet_new(YAHOO_SERVICE_ADDBUDDY, YAHOO_STATUS_AVAILABLE, 0); - yahoo_packet_hash(pkt, "ssss", 1, purple_connection_get_display_name(gc), - 7, who, 65, gpn, 14, ""); + pkt = yahoo_packet_new(YAHOO_SERVICE_CHGRP_15, YAHOO_STATUS_AVAILABLE, 0); + yahoo_packet_hash(pkt, "ssssssss", 1, purple_connection_get_display_name(gc), + 302, "240", 300, "240", 7, who, 224, gpo, 264, gpn, 301, + "240", 303, "240"); yahoo_packet_send_and_free(pkt, yd); - /* Step 2: Remove buddy from old group */ - pkt = yahoo_packet_new(YAHOO_SERVICE_REMBUDDY, YAHOO_STATUS_AVAILABLE, 0); - yahoo_packet_hash(pkt, "sss", 1, purple_connection_get_display_name(gc), 7, who, 65, gpo); - yahoo_packet_send_and_free(pkt, yd); g_free(gpn); g_free(gpo); }
--- a/libpurple/protocols/yahoo/yahoo_packet.c Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/yahoo/yahoo_packet.c Tue Aug 14 02:45:41 2007 +0000 @@ -223,6 +223,11 @@ GSList *l = pkt->hash; int pos = 0; + /* This is only called from one place, and the list is + * always backwards */ + + l = g_slist_reverse(l); + while (l) { struct yahoo_pair *pair = l->data; gchar buf[100];
--- a/libpurple/protocols/yahoo/yahoo_packet.h Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/protocols/yahoo/yahoo_packet.h Tue Aug 14 02:45:41 2007 +0000 @@ -98,6 +98,7 @@ YAHOO_SERVICE_AVATAR_UPDATE = 0xc7, YAHOO_SERVICE_VERIFY_ID_EXISTS = 0xc8, YAHOO_SERVICE_AUDIBLE = 0xd0, + YAHOO_SERVICE_CHGRP_15 = 0xe7, YAHOO_SERVICE_STATUS_15 = 0xf0, YAHOO_SERVICE_LIST_15 = 0Xf1, YAHOO_SERVICE_WEBLOGIN = 0x0226,
--- a/libpurple/win32/global.mak Tue Aug 14 02:45:06 2007 +0000 +++ b/libpurple/win32/global.mak Tue Aug 14 02:45:41 2007 +0000 @@ -40,7 +40,6 @@ PIDGIN_IDLETRACK_TOP := $(PIDGIN_TOP)/win32/IdleTracker PIDGIN_PIXMAPS_TOP := $(PIDGIN_TOP)/pixmaps PIDGIN_PLUGINS_TOP := $(PIDGIN_TOP)/plugins -PIDGIN_SOUNDS_TOP := $(PIDGIN_TOP)/sounds PURPLE_PO_TOP := $(PIDGIN_TREE_TOP)/po PURPLE_PROTOS_TOP := $(PURPLE_TOP)/protocols
--- a/pidgin/Makefile.am Tue Aug 14 02:45:06 2007 +0000 +++ b/pidgin/Makefile.am Tue Aug 14 02:45:41 2007 +0000 @@ -65,7 +65,7 @@ pkgconfigdir = $(libdir)/pkgconfig pkgconfig_DATA = pidgin.pc -SUBDIRS = pixmaps plugins sounds +SUBDIRS = pixmaps plugins bin_PROGRAMS = pidgin
--- a/pidgin/Makefile.mingw Tue Aug 14 02:45:06 2007 +0000 +++ b/pidgin/Makefile.mingw Tue Aug 14 02:45:41 2007 +0000 @@ -146,7 +146,6 @@ install: install_shallow all $(MAKE) -C $(PIDGIN_PLUGINS_TOP) -f $(MINGW_MAKEFILE) install $(MAKE) -C $(PIDGIN_PIXMAPS_TOP) -f $(MINGW_MAKEFILE) install - $(MAKE) -C $(PIDGIN_SOUNDS_TOP) -f $(MINGW_MAKEFILE) install $(MAKE) -C $(PIDGIN_IDLETRACK_TOP) -f $(MINGW_MAKEFILE) install win32/pidgin_dll_rc.rc: win32/pidgin_dll_rc.rc.in $(PIDGIN_TREE_TOP)/VERSION
--- a/pidgin/gtkaccount.c Tue Aug 14 02:45:06 2007 +0000 +++ b/pidgin/gtkaccount.c Tue Aug 14 02:45:41 2007 +0000 @@ -2546,9 +2546,15 @@ } static void * -pidgin_accounts_request_authorization(PurpleAccount *account, const char *remote_user, - const char *id, const char *alias, const char *message, gboolean on_list, - GCallback auth_cb, GCallback deny_cb, void *user_data) +pidgin_accounts_request_authorization(PurpleAccount *account, + const char *remote_user, + const char *id, + const char *alias, + const char *message, + gboolean on_list, + PurpleAccountRequestAuthorizationCb auth_cb, + PurpleAccountRequestAuthorizationCb deny_cb, + void *user_data) { char *buffer; PurpleConnection *gc; @@ -2574,8 +2580,8 @@ if (!on_list) { struct auth_and_add *aa = g_new0(struct auth_and_add, 1); - aa->auth_cb = (PurpleAccountRequestAuthorizationCb)auth_cb; - aa->deny_cb = (PurpleAccountRequestAuthorizationCb)deny_cb; + aa->auth_cb = auth_cb; + aa->deny_cb = deny_cb; aa->data = user_data; aa->username = g_strdup(remote_user); aa->alias = g_strdup(alias);
--- a/pidgin/gtkblist.c Tue Aug 14 02:45:06 2007 +0000 +++ b/pidgin/gtkblist.c Tue Aug 14 02:45:41 2007 +0000 @@ -2608,6 +2608,7 @@ gboolean tooltip_top = FALSE; struct _pidgin_blist_node *gtknode; GdkRectangle mon_size; + int sig; if (node == NULL) return; @@ -2618,7 +2619,6 @@ */ pidgin_blist_tooltip_destroy(); - gtkblist->tipwindow = gtk_window_new(GTK_WINDOW_POPUP); if(PURPLE_BLIST_NODE_IS_CHAT(node) || PURPLE_BLIST_NODE_IS_BUDDY(node)) { @@ -2673,7 +2673,6 @@ G_CALLBACK(pidgin_blist_paint_tip), NULL); gtk_widget_ensure_style (gtkblist->tipwindow); - #if GTK_CHECK_VERSION(2,2,0) gdk_display_get_pointer(gdk_display_get_default(), &screen, &x, &y, NULL); mon_num = gdk_screen_get_monitor_at_point(screen, x, y); @@ -2722,6 +2721,10 @@ gtk_window_move(GTK_WINDOW(gtkblist->tipwindow), x, y); gtk_widget_show(gtkblist->tipwindow); + /* Hide the tooltip when the widget is destroyed */ + sig = g_signal_connect(G_OBJECT(widget), "destroy", G_CALLBACK(pidgin_blist_tooltip_destroy), NULL); + g_signal_connect_swapped(G_OBJECT(gtkblist->tipwindow), "destroy", G_CALLBACK(g_source_remove), GINT_TO_POINTER(sig)); + return; }
--- a/pidgin/gtkconv.c Tue Aug 14 02:45:06 2007 +0000 +++ b/pidgin/gtkconv.c Tue Aug 14 02:45:41 2007 +0000 @@ -213,8 +213,8 @@ * Callbacks **************************************************************************/ -static gint -close_conv_cb(GtkWidget *w, PidginConversation *gtkconv) +static gboolean +close_conv_cb(GtkWidget *w, GdkEventButton *event, PidginConversation *gtkconv) { GList *list = g_list_copy(gtkconv->convs); @@ -1331,7 +1331,7 @@ { PidginWindow *win = data; - close_conv_cb(NULL, PIDGIN_CONVERSATION(pidgin_conv_window_get_active_conversation(win))); + close_conv_cb(NULL, NULL, PIDGIN_CONVERSATION(pidgin_conv_window_get_active_conversation(win))); } static void @@ -6256,7 +6256,7 @@ (fields & PIDGIN_CONV_SET_TITLE) || (fields & PIDGIN_CONV_TOPIC)) { - char *title, *truncate = NULL, truncchar; + char *title, *truncate = NULL, truncchar = '\0'; PurpleConvIm *im = NULL; PurpleAccount *account = purple_conversation_get_account(conv); PurpleBuddy *buddy = NULL; @@ -6294,11 +6294,13 @@ } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) { PurpleConvChat *chat = PURPLE_CONV_CHAT(conv); const char *topic = purple_conv_chat_get_topic(chat); + char *esc = topic ? g_markup_escape_text(topic, -1) : NULL; markup = g_strdup_printf("%s%s<span color='%s' size='smaller'>%s</span>", purple_conversation_get_title(conv), - topic ? "\n" : "", + esc ? "\n" : "", pidgin_get_dim_grey_string(gtkconv->infopane), - topic ? topic : ""); + esc ? esc : ""); + g_free(esc); } gtk_list_store_set(gtkconv->infopane_model, &(gtkconv->infopane_iter), CONV_TEXT_COLUMN, markup, -1); @@ -6320,13 +6322,13 @@ style = "color=\"#c4a000\""; } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_NICK) { atk_object_set_description(accessibility_obj, _("Nick Said")); - style = "color=\"#204a87\" style=\"italic\" weight=\"bold\""; + style = "color=\"#204a87\" weight=\"bold\""; } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT) { atk_object_set_description(accessibility_obj, _("Unread Messages")); style = "color=\"#cc0000\" weight=\"bold\""; } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) { atk_object_set_description(accessibility_obj, _("New Event")); - style = "color=\"#888a85\" style=\"italic\""; + style = "color=\"#888a85\" weight=\"bold\""; } else { style = ""; } @@ -6599,6 +6601,7 @@ event = gtk_event_box_new(); gtk_container_add(GTK_CONTAINER(gtkconv->u.im->icon_container), event); + gtk_event_box_set_visible_window(GTK_EVENT_BOX(event), FALSE); gtk_widget_add_events(event, GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK); g_signal_connect(G_OBJECT(event), "button-press-event", @@ -7495,7 +7498,7 @@ _("Confirm close"), GTK_WINDOW(gtkwin->window), GTK_DIALOG_MODAL, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, - PIDGIN_STOCK_CLOSE_TABS, GTK_RESPONSE_OK, NULL); + GTK_STOCK_CLOSE, GTK_RESPONSE_OK, NULL); gtk_dialog_set_default_response(GTK_DIALOG(warn_close_dialog), GTK_RESPONSE_OK); @@ -7787,7 +7790,7 @@ return FALSE; gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, tab_clicked); - close_conv_cb(NULL, gtkconv); + close_conv_cb(NULL, NULL, gtkconv); return TRUE; } @@ -8046,7 +8049,7 @@ if (gconv != gtkconv) { - close_conv_cb(NULL, gconv); + close_conv_cb(NULL, NULL, gconv); } } } @@ -8058,7 +8061,7 @@ gtkconv = g_object_get_data(menu, "clicked_tab"); if (gtkconv) - close_conv_cb(NULL, gtkconv); + close_conv_cb(NULL, NULL, gtkconv); } static gboolean @@ -8336,11 +8339,6 @@ if (gdk_window_get_state(w->window) & GDK_WINDOW_STATE_MAXIMIZED) return FALSE; - /* don't save if nothing changed */ - if (x == purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x") && - y == purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y")) - return FALSE; /* carry on normally */ - /* don't save off-screen positioning */ if (x + event->width < 0 || y + event->height < 0 || @@ -8388,10 +8386,10 @@ static void pidgin_conv_restore_position(PidginWindow *win) { pidgin_conv_set_position_size(win, - purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/x"), - purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/y"), - purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/width"), - purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/height")); + purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"), + purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"), + purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"), + purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height")); } PidginWindow * @@ -8544,6 +8542,20 @@ gtkconv->tab_cont)); } +static gboolean +close_button_left_cb(GtkWidget *widget, GdkEventCrossing *event, GtkLabel *label) +{ + gtk_label_set_markup(label, "×"); + return FALSE; +} + +static gboolean +close_button_entered_cb(GtkWidget *widget, GdkEventCrossing *event, GtkLabel *label) +{ + gtk_label_set_markup(label, "<b>×</b>"); + return FALSE; +} + void pidgin_conv_window_add_gtkconv(PidginWindow *win, PidginConversation *gtkconv) { @@ -8553,7 +8565,6 @@ GtkWidget *close_image; PurpleConversationType conv_type; const gchar *tmp_lab; - gint close_button_width, close_button_height, focus_width, focus_pad; conv_type = purple_conversation_get_type(conv); @@ -8565,29 +8576,19 @@ /* Close button. */ - gtkconv->close = gtk_button_new(); - gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &close_button_width, &close_button_height); - if (gtk_check_version(2, 4, 2) == NULL) { - /* Need to account for extra padding around the gtkbutton */ - gtk_widget_style_get(GTK_WIDGET(gtkconv->close), - "focus-line-width", &focus_width, - "focus-padding", &focus_pad, - NULL); - close_button_width += (focus_width + focus_pad) * 2; - close_button_height += (focus_width + focus_pad) * 2; - } - gtk_widget_set_size_request(GTK_WIDGET(gtkconv->close), - close_button_width, close_button_height); - - gtk_button_set_relief(GTK_BUTTON(gtkconv->close), GTK_RELIEF_NONE); - close_image = gtk_image_new_from_stock(GTK_STOCK_CLOSE, GTK_ICON_SIZE_MENU); + gtkconv->close = gtk_event_box_new(); + gtk_event_box_set_visible_window(GTK_EVENT_BOX(gtkconv->close), FALSE); + gtk_widget_set_events(gtkconv->close, GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK); + close_image = gtk_label_new("×"); + g_signal_connect(G_OBJECT(gtkconv->close), "enter-notify-event", G_CALLBACK(close_button_entered_cb), close_image); + g_signal_connect(G_OBJECT(gtkconv->close), "leave-notify-event", G_CALLBACK(close_button_left_cb), close_image); gtk_widget_show(close_image); gtk_container_add(GTK_CONTAINER(gtkconv->close), close_image); gtk_tooltips_set_tip(gtkconv->tooltips, gtkconv->close, _("Close conversation"), NULL); - g_signal_connect(G_OBJECT(gtkconv->close), "clicked", - G_CALLBACK(close_conv_cb), gtkconv); + g_signal_connect(G_OBJECT(gtkconv->close), "button-press-event", + G_CALLBACK(close_conv_cb), gtkconv); #if !GTK_CHECK_VERSION(2,6,0) /* @@ -8608,7 +8609,7 @@ gtkconv->tab_label = gtk_label_new(tmp_lab = purple_conversation_get_title(conv)); gtkconv->menu_tabby = gtk_hbox_new(FALSE, PIDGIN_HIG_BOX_SPACE); - gtkconv->menu_label = gtk_label_new(purple_conversation_get_title(gtkconv->active_conv)); + gtkconv->menu_label = gtk_label_new(tmp_lab); gtk_box_pack_start(GTK_BOX(gtkconv->menu_tabby), gtkconv->menu_icon, FALSE, FALSE, 0); gtk_widget_show_all(gtkconv->menu_icon); @@ -8618,7 +8619,6 @@ gtk_misc_set_alignment(GTK_MISC(gtkconv->menu_label), 0, 0); gtk_widget_show(gtkconv->menu_tabby); - gtk_widget_set_size_request(gtkconv->menu_tabby, 0, -1); if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) pidgin_conv_update_buddy_icon(conv); @@ -8633,9 +8633,6 @@ if (pidgin_conv_window_get_gtkconv_count(win) == 1) { /* Er, bug in notebooks? Switch to the page manually. */ gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), 0); - - gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), - purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs")); } else { gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), TRUE); } @@ -8734,6 +8731,12 @@ !tabs_side && !angle && pidgin_conv_window_get_gtkconv_count(win) > 1, TRUE, GTK_PACK_START); + if (pidgin_conv_window_get_gtkconv_count(win) == 1) + gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), + !purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons") || + purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_LEFT || + purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_RIGHT); + /* show the widgets */ /* gtk_widget_show(gtkconv->icon); */ gtk_widget_show(gtkconv->tab_label); @@ -8757,12 +8760,6 @@ gtk_notebook_remove_page(GTK_NOTEBOOK(win->notebook), index); - /* go back to tabless */ - if (pidgin_conv_window_get_gtkconv_count(win) <= 2) { - gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), - purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs")); - } - win->gtkconvs = g_list_remove(win->gtkconvs, gtkconv); if (win->gtkconvs && win->gtkconvs->next == NULL) @@ -9041,6 +9038,7 @@ PidginWindow *win; win = pidgin_conv_window_new(); + g_signal_connect(G_OBJECT(win->window), "configure_event", G_CALLBACK(gtk_conv_configure_cb), NULL);
--- a/pidgin/gtkdocklet.c Tue Aug 14 02:45:06 2007 +0000 +++ b/pidgin/gtkdocklet.c Tue Aug 14 02:45:41 2007 +0000 @@ -636,7 +636,7 @@ purple_prefs_add_none(PIDGIN_PREFS_ROOT "/docklet"); purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/docklet/blink", FALSE); - purple_prefs_add_string(PIDGIN_PREFS_ROOT "/docklet/show", "pending"); + purple_prefs_add_string(PIDGIN_PREFS_ROOT "/docklet/show", "always"); purple_prefs_connect_callback(docklet_handle, PIDGIN_PREFS_ROOT "/docklet/show", docklet_show_pref_changed_cb, NULL);
--- a/pidgin/gtkimhtml.c Tue Aug 14 02:45:06 2007 +0000 +++ b/pidgin/gtkimhtml.c Tue Aug 14 02:45:41 2007 +0000 @@ -1037,7 +1037,7 @@ { char *tmp; - if (text == NULL) + if (text == NULL || !(*text)) return; tmp = g_markup_escape_text(text, -1); @@ -1053,7 +1053,7 @@ if (!gtk_text_view_get_editable(GTK_TEXT_VIEW(imhtml))) return; - if (imhtml->wbfo || selection_data->length < 0) { + if (imhtml->wbfo || selection_data->length <= 0) { gtk_clipboard_request_text(clipboard, paste_plaintext_received_cb, imhtml); return; } else {
--- a/pidgin/gtkimhtmltoolbar.c Tue Aug 14 02:45:06 2007 +0000 +++ b/pidgin/gtkimhtmltoolbar.c Tue Aug 14 02:45:41 2007 +0000 @@ -939,7 +939,7 @@ *y -= widget->allocation.height; } -static void pidgin_menu_clicked(GtkWidget *button, GdkEventButton *event, GtkMenu *menu) +static void pidgin_menu_clicked(GtkWidget *button, GtkMenu *menu) { gtk_widget_show_all(GTK_WIDGET(menu)); gtk_menu_popup(menu, NULL, NULL, menu_position_func, button, 0, gtk_get_current_event_time()); @@ -1096,6 +1096,13 @@ g_signal_handlers_unblock_by_func(G_OBJECT(item), G_CALLBACK(gtk_button_clicked), button); } +static void +enable_markup(GtkWidget *widget, gpointer null) +{ + if (GTK_IS_LABEL(widget)) + g_object_set(G_OBJECT(widget), "use-markup", TRUE, NULL); +} + static void gtk_imhtmltoolbar_init (GtkIMHtmlToolbar *toolbar) { GtkWidget *hbox = GTK_WIDGET(toolbar); @@ -1114,14 +1121,17 @@ GtkWidget **button; gboolean check; } buttons[] = { - {_("_Bold"), &toolbar->bold, TRUE}, - {_("_Italic"), &toolbar->italic, TRUE}, - {_("_Underline"), &toolbar->underline, TRUE}, - {_("_Larger"), &toolbar->larger_size, TRUE}, + {_("<b>_Bold</b>"), &toolbar->bold, TRUE}, + {_("<i>_Italic</i>"), &toolbar->italic, TRUE}, + {_("<u>_Underline</u>"), &toolbar->underline, TRUE}, + {_("<span size='larger'>_Larger</span>"), &toolbar->larger_size, TRUE}, #if 0 {_("_Normal"), &toolbar->normal_size, TRUE}, #endif - {_("_Smaller"), &toolbar->smaller_size, TRUE}, + {_("<span size='smaller'>_Smaller</span>"), &toolbar->smaller_size, TRUE}, + /* If we want to show the formatting for the following items, we would + * need to update them when formatting changes. The above items don't need + * no updating nor nothin' */ {_("_Font face"), &toolbar->font, TRUE}, {_("Foreground _color"), &toolbar->fgcolor, TRUE}, {_("Bac_kground color"), &toolbar->bgcolor, TRUE}, @@ -1175,9 +1185,11 @@ gtk_menu_shell_append(GTK_MENU_SHELL(font_menu), menuitem); g_signal_connect(G_OBJECT(old), "notify::sensitive", G_CALLBACK(button_sensitiveness_changed), menuitem); + gtk_container_foreach(GTK_CONTAINER(menuitem), (GtkCallback)enable_markup, NULL); } - g_signal_connect(G_OBJECT(font_button), "button-press-event", G_CALLBACK(pidgin_menu_clicked), font_menu); + g_signal_connect_swapped(G_OBJECT(font_button), "button-press-event", G_CALLBACK(gtk_widget_activate), font_button); + g_signal_connect(G_OBJECT(font_button), "activate", G_CALLBACK(pidgin_menu_clicked), font_menu); g_signal_connect(G_OBJECT(font_menu), "deactivate", G_CALLBACK(pidgin_menu_deactivate), font_button); /* Sep */ @@ -1218,7 +1230,8 @@ g_signal_connect(G_OBJECT(toolbar->link), "notify::sensitive", G_CALLBACK(button_sensitiveness_changed), menuitem); - g_signal_connect(G_OBJECT(insert_button), "button-press-event", G_CALLBACK(pidgin_menu_clicked), insert_menu); + g_signal_connect_swapped(G_OBJECT(insert_button), "button-press-event", G_CALLBACK(gtk_widget_activate), insert_button); + g_signal_connect(G_OBJECT(insert_button), "activate", G_CALLBACK(pidgin_menu_clicked), insert_menu); g_signal_connect(G_OBJECT(insert_menu), "deactivate", G_CALLBACK(pidgin_menu_deactivate), insert_button); toolbar->sml = NULL; }
--- a/pidgin/gtkmain.c Tue Aug 14 02:45:06 2007 +0000 +++ b/pidgin/gtkmain.c Tue Aug 14 02:45:41 2007 +0000 @@ -695,7 +695,9 @@ return 1; } +#if GLIB_CHECK_VERSION(2,2,0) g_set_application_name(_("Pidgin")); +#endif /* glib-2.0 >= 2.2.0 */ #ifdef _WIN32 winpidgin_init(hint);
--- a/pidgin/gtknotify.c Tue Aug 14 02:45:06 2007 +0000 +++ b/pidgin/gtknotify.c Tue Aug 14 02:45:41 2007 +0000 @@ -274,6 +274,7 @@ gtk_label_set_markup(GTK_LABEL(label), label_text); gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); + gtk_label_set_selectable(GTK_LABEL(label), TRUE); gtk_misc_set_alignment(GTK_MISC(label), 0, 0); gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0); @@ -609,6 +610,7 @@ gtk_label_set_markup(GTK_LABEL(label), label_text); gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); + gtk_label_set_selectable(GTK_LABEL(label), TRUE); gtk_misc_set_alignment(GTK_MISC(label), 0, 0); gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0); gtk_widget_show(label); @@ -626,6 +628,7 @@ button = gtk_button_new_from_stock(GTK_STOCK_CLOSE); gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE, 0); gtk_widget_show(button); + gtk_widget_grab_focus(button); g_signal_connect_swapped(G_OBJECT(button), "clicked", G_CALLBACK(gtk_widget_destroy), window);
--- a/pidgin/gtksound.c Tue Aug 14 02:45:06 2007 +0000 +++ b/pidgin/gtksound.c Tue Aug 14 02:45:41 2007 +0000 @@ -543,7 +543,8 @@ char *filename = g_strdup(purple_prefs_get_path(file_pref)); if(!filename || !strlen(filename)) { g_free(filename); - filename = g_build_filename(DATADIR, "sounds", "pidgin", sounds[event].def, NULL); + /* XXX Consider creating a constant for "sounds/purple" to be shared with Finch */ + filename = g_build_filename(DATADIR, "sounds", "purple", sounds[event].def, NULL); } purple_sound_play_file(filename, NULL);
--- a/pidgin/gtkutils.c Tue Aug 14 02:45:06 2007 +0000 +++ b/pidgin/gtkutils.c Tue Aug 14 02:45:41 2007 +0000 @@ -2047,11 +2047,11 @@ entry.entry.buddy->account, entry.entry.buddy->name ); +#else + item->data = g_strdup(entry.entry.buddy->name); + g_completion_add_items(data->completion, item); +#endif /* NEW_STYLE_COMPLETION */ } -#else - item->data = g_strdup(buddy->name); - g_completion_add_items(data->completion, item); -#endif /* NEW_STYLE_COMPLETION */ } } }
--- a/pidgin/pixmaps/Makefile.am Tue Aug 14 02:45:06 2007 +0000 +++ b/pidgin/pixmaps/Makefile.am Tue Aug 14 02:45:41 2007 +0000 @@ -12,7 +12,7 @@ pidgin.ico pidginbuttonpixdir = $(datadir)/pixmaps/pidgin/buttons -pidginbuttonpix_DATA = edit.png pause.png +pidginbuttonpix_DATA = edit.png pause.png pidgindistpixdir = $(datadir)/pixmaps/pidgin pidgindistpix_DATA = logo.png arrow-down.xpm arrow-left.xpm arrow-right.xpm arrow-up.xpm
--- a/pidgin/plugins/win32/transparency/win2ktrans.c Tue Aug 14 02:45:06 2007 +0000 +++ b/pidgin/plugins/win32/transparency/win2ktrans.c Tue Aug 14 02:45:41 2007 +0000 @@ -274,9 +274,11 @@ gtk_window_get_size(GTK_WINDOW(win), &width, &height); gtk_box_pack_start(GTK_BOX(vbox), slider_box, FALSE, FALSE, 0); +#if 0 /*Now that we save window sizes, don't resize it or else it causes windows to grow*/ /* Make window taller so we don't slowly collapse its message area */ gtk_window_resize(GTK_WINDOW(win), width, (height + slidereq.height)); +#endif /* Add window to list, to track that it has a slider */ slidwin = g_new0(slider_win, 1); slidwin->win = win; @@ -292,6 +294,7 @@ slider_win *slidwin = (slider_win*) tmp->data; if (slidwin != NULL && GTK_IS_WINDOW(slidwin->win)) { +#if 0 GtkRequisition slidereq; gint width, height; /* Figure out how tall the slider was */ @@ -300,12 +303,13 @@ gtk_window_get_size( GTK_WINDOW(slidwin->win), &width, &height); - +#endif gtk_widget_destroy(slidwin->slider); - +#if 0 gtk_window_resize( GTK_WINDOW(slidwin->win), width, (height - slidereq.height)); +#endif } g_free(slidwin); tmp = tmp->next;
--- a/pidgin/sounds/Makefile.am Tue Aug 14 02:45:06 2007 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,12 +0,0 @@ -soundsdir = $(datadir)/sounds/pidgin -sounds_DATA = \ - alert.wav \ - login.wav \ - logout.wav \ - receive.wav \ - send.wav - -EXTRA_DIST = \ - Makefile.mingw \ - $(sounds_DATA) -
--- a/pidgin/sounds/Makefile.mingw Tue Aug 14 02:45:06 2007 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -# -# Makefile.mingw -# -# Description: Makefile for win32 (mingw) version of Pidgin sounds -# - -PIDGIN_TREE_TOP := ../.. -include $(PIDGIN_TREE_TOP)/libpurple/win32/global.mak - -datadir := $(PIDGIN_INSTALL_DIR) -include ./Makefile.am - -.PHONY: install - -install: - if test '$(sounds_DATA)'; then \ - mkdir -p $(soundsdir); \ - cp $(sounds_DATA) $(soundsdir); \ - fi; -
--- a/po/POTFILES.in Tue Aug 14 02:45:06 2007 +0000 +++ b/po/POTFILES.in Tue Aug 14 02:45:41 2007 +0000 @@ -11,6 +11,7 @@ finch/gntpounce.c finch/gntprefs.c finch/gntrequest.c +finch/gntsound.c finch/gntstatus.c finch/gntui.c finch/libgnt/gntbox.c
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/Makefile.am Tue Aug 14 02:45:41 2007 +0000 @@ -0,0 +1,4 @@ + +SUBDIRS = sounds + +EXTRA_DIST = Makefile.mingw
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/Makefile.mingw Tue Aug 14 02:45:41 2007 +0000 @@ -0,0 +1,19 @@ +# +# Makefile.mingw +# +# Description: Makefile for win32 (mingw) version +# + +PIDGIN_TREE_TOP := .. +include $(PIDGIN_TREE_TOP)/libpurple/win32/global.mak + +include ./Makefile.am + +.PHONY: install clean + +install: + if test '$(SUBDIRS)'; then \ + list='$(SUBDIRS)'; for subdir in $$list; do \ + $(MAKE) -C $$subdir -f $(MINGW_MAKEFILE) install || exit 1 ;\ + done; \ + fi;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/sounds/Makefile.am Tue Aug 14 02:45:41 2007 +0000 @@ -0,0 +1,12 @@ +soundsdir = $(datadir)/sounds/purple +sounds_DATA = \ + alert.wav \ + login.wav \ + logout.wav \ + receive.wav \ + send.wav + +EXTRA_DIST = \ + Makefile.mingw \ + $(sounds_DATA) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/share/sounds/Makefile.mingw Tue Aug 14 02:45:41 2007 +0000 @@ -0,0 +1,20 @@ +# +# Makefile.mingw +# +# Description: Makefile for win32 (mingw) version of Pidgin sounds +# + +PIDGIN_TREE_TOP := ../.. +include $(PIDGIN_TREE_TOP)/libpurple/win32/global.mak + +datadir := $(PIDGIN_INSTALL_DIR) +include ./Makefile.am + +.PHONY: install + +install: + if test '$(sounds_DATA)'; then \ + mkdir -p $(soundsdir); \ + cp $(sounds_DATA) $(soundsdir); \ + fi; +