Mercurial > pidgin.yaz
changeset 27052:413006df9828
propagate from branch 'im.pidgin.pidgin' (head 13ac492a493b4d31c8b29905174b43a533304300)
to branch 'im.pidgin.cpw.darkrain42.xmpp.disco' (head e73974597bbcc75504a88eac45d6893c182d058e)
author | Paul Aurich <paul@darkrain42.org> |
---|---|
date | Wed, 29 Apr 2009 23:20:51 +0000 |
parents | 836a36564ff5 (diff) 464dbfad4474 (current diff) |
children | 58ff88dba33a |
files | libpurple/protocols/jabber/disco.c libpurple/protocols/jabber/jabber.c libpurple/protocols/jabber/jabber.h libpurple/protocols/jabber/libxmpp.c libpurple/prpl.h pidgin/gtkblist.c |
diffstat | 117 files changed, 6449 insertions(+), 2448 deletions(-) [+] |
line wrap: on
line diff
--- a/AUTHORS Sat Apr 18 06:52:59 2009 +0000 +++ b/AUTHORS Wed Apr 29 23:20:51 2009 +0000 @@ -9,6 +9,7 @@ ------------------ Daniel 'datallah' Atallah - Developer +Paul 'darkrain42' Aurich - Developer John 'rekkanoryo' Bailey - Developer Ethan 'Paco-Paco' Blanton - Developer Thomas Butter - Developer @@ -24,6 +25,7 @@ Bartosz Oler - Developer Etan 'deryni' Reisner - Developer Tim 'marv' Ringenbach - Developer +Michael 'Maiku' Ruprecht - Developer, voice and video Elliott 'QuLogic' Sales de Andrade - Developer Luke 'LSchiere' Schierer - Support Megan 'Cae' Schneider - support/QA @@ -35,7 +37,6 @@ Crazy Patch Writers: ------------------- -Paul 'darkrain42' Aurich Marcus 'malu' Lundblad Dennis 'EvilDennisR' Ristuccia Peter 'Fmoo' Ruibal
--- a/COPYRIGHT Sat Apr 18 06:52:59 2009 +0000 +++ b/COPYRIGHT Wed Apr 29 23:20:51 2009 +0000 @@ -267,6 +267,7 @@ Paolo Maggi Sulabh Mahajan Willian T. Mahan +Tobias Markmann Kris Marsh Fidel Martinez Lalo Martins
--- a/ChangeLog Sat Apr 18 06:52:59 2009 +0000 +++ b/ChangeLog Wed Apr 29 23:20:51 2009 +0000 @@ -4,28 +4,54 @@ General: * Theme support in libpurple thanks to Justin Rodriguez's summer of code project. With some minor additions and clean ups from Paul Aurich. + * Voice & Video framework in libpurple, thanks to Mike Ruprecht's summer + of code project in 2008. * It should no longer be possible to end up with duplicates of buddies in a group on the buddy list. * Removed the unmaintained and unneeded toc protocol plugin. * Fixed NTLM authentication on big-endian systems. - * Dragging a buddy onto a chat pops up a chat-invitation dialog. - (Carlos Bederian) libpurple: * Various memory cleanups when unloading libpurple. (Nick Hebner) + * Report idle time 'From last message sent' should work properly. XMPP: * Add voice & video support with Jingle (XEP-0166, 0167, 0176, & 0177), and voice support with GTalk and GMail. (Mike "Maiku" Ruprecht) * Add support for in-band bytestreams for file transfers (XEP-0047). - * Add support for sending and receiving attentions (equivalent to "buzz" - and "nudge") using the command /buzz (XEP-0224). + * Add support for sending and receiving attentions (equivalent to "buzz" + and "nudge") using the command /buzz. (XEP-0224) + * Support for connecting using BOSH. (Tobias Markmann) * A buddy's local time is displayed in the Get Info dialog if the remote client supports it. + * The set_chat_topic function can unset the chat topic. + * Fix crash on connection with recent gstreamer0.10-plugins-bad. + * Don't create a new conversation window for incoming messages of + type 'headline'. + * The Ad-Hoc commands associated with our server are now always shown at + login. + * Support showing and reporting idle times in the buddy list. (XEP-0256) + * Support most recent version of User Avatar. (XEP-0084 v1.1) + * Updated Entity Capabilities support. (Tobias Markmann) IRC: * Correctly handle WHOIS for users who are joined to a large number of channels. + * Notify the user if a /nick command fails, rather than trying + fallback nicks. + + MSN: + * Fix a race condition in the SOAP code that caused mysterious crashes. + (Thanks to Florian Quèze for noticing the cause) + * Fix a regression in 2.5.5 that caused numerous "Friendly name changes + too rapidly" pop-ups at login. + + Yahoo: + * P2P file transfers. (Sulabh Mahajan) + * MSN Interoperability by adding MSN buddies as 'msn/user@example.com'. + (Sulabh Mahajan) + * Sending text messages (address to +<countrycode><phone number>). + (Sulabh Mahajan) Pidgin: * Added -f command line option to tell Pidgin to ignore NetworkManager @@ -38,9 +64,14 @@ the next line. * Created a unified Buddy Pounce notification window for all pounces where "Pop up a notification" is selected, which avoids having a - new dialog box every time a pounce is triggered. (Jorge Villaseñor) + new dialog box every time a pounce is triggered. (Jorge Villaseñor) * The New Account dialog is now broken into three tabs. Proxy configuration has been moved from the Advanced tab to the new tab. + * Dragging a buddy onto a chat pops up a chat-invitation dialog. + (Carlos Bederian) + * The nicks of the persons who leave the chatroom are italicized in the + chat's conversation history. The nicks are un-italicized when they + rejoin. Finch: * The hardware cursor is updated correctly. This will be useful
--- a/ChangeLog.API Sat Apr 18 06:52:59 2009 +0000 +++ b/ChangeLog.API Wed Apr 29 23:20:51 2009 +0000 @@ -9,6 +9,7 @@ * PURPLE_CONTACT * PURPLE_BUDDY * PURPLE_CHAT + * account-actions-changed (see account-signals.dox) * purple_buddy_destroy * purple_buddy_get_protocol_data * purple_buddy_set_protocol_data @@ -32,6 +33,7 @@ * purple_network_get_stun_ip * purple_network_get_turn_ip * purple_prpl_get_media_caps + * purple_prpl_got_account_actions * purple_prpl_initiate_media * purple_request_field_get_group * purple_request_field_get_ui_data @@ -77,6 +79,11 @@ * pidgin_sound_is_customized * pidgin_utils_init, pidgin_utils_uninit * pidgin_notify_pounce_add + * PidginBlistTheme, PidginBlistThemeLoader API + * PidginIconTheme, PidginStatusIconTheme, PidginIconThemeLoader + API + * pidgin_stock_id_from_status_primitive + * pidgin_stock_id_from_presence libgnt: Added:
--- a/doc/TCL-HOWTO.dox Sat Apr 18 06:52:59 2009 +0000 +++ b/doc/TCL-HOWTO.dox Wed Apr 29 23:20:51 2009 +0000 @@ -173,6 +173,7 @@ purple::connection displayname gc purple::connection handle purple::connection list +purple::connection state @endcode @c purple::connection is a collection of subcommands pertaining to @@ -192,6 +193,9 @@ this list are appropriate as @c gc arguments to the other @c purple::connection subcommands or other commands requiring a gc. + @c state returns the PurpleConnectionState of this account as one of + the strings "connected", "disconnected", or "connecting". + @code purple::conv_send account who text @endcode
--- a/doc/account-signals.dox Sat Apr 18 06:52:59 2009 +0000 +++ b/doc/account-signals.dox Wed Apr 29 23:20:51 2009 +0000 @@ -9,6 +9,7 @@ @signal account-setting-info @signal account-set-info @signal account-status-changed + @signal account-actions-changed @signal account-alias-changed @signal account-authorization-requested @signal account-authorization-denied @@ -97,6 +98,15 @@ @param new The status after change. @endsignaldef + @signaldef account-actions-changed + @signalproto +void (*account_actions_changed)(PurpleAccount *account); + @endsignalproto + @signaldesc + Emitted when the account actions are changed after initial connection. + @param account The account whose actions changed. + @endsignaldef + @signaldef account-alias-changed @signalproto void (*account_alias_changed)(PurpleAccount *account, const char *old);
--- a/doc/pidgin.1.in Sat Apr 18 06:52:59 2009 +0000 +++ b/doc/pidgin.1.in Wed Apr 29 23:20:51 2009 +0000 @@ -602,7 +602,9 @@ .br Daniel 'datallah' Atallah (developer) .br - John 'rekkanoryo' Bailey (developer) + Paul 'darkrain42' Aurich (developer) +.br + John 'rekkanoryo' Bailey (developer and bugmaster) .br Ethan 'Paco-Paco' Blanton (developer) .br @@ -632,6 +634,8 @@ .br Tim 'marv' Ringenbach (developer) <\fImarv_sf@users.sf.net\fR> .br + Michael 'Maiku' Ruprecht (developer, voice and video) +.br Elliott 'QuLogic' Sales de Andrade (developer) .br Luke 'LSchiere' Schierer (support)
--- a/finch/gntblist.c Sat Apr 18 06:52:59 2009 +0000 +++ b/finch/gntblist.c Wed Apr 29 23:20:51 2009 +0000 @@ -935,7 +935,7 @@ else if (PURPLE_BLIST_NODE_IS_GROUP(node)) return purple_group_get_name((PurpleGroup*)node); - snprintf(text, sizeof(text) - 1, "%s %s", status, name); + g_snprintf(text, sizeof(text) - 1, "%s %s", status, name); return text; } @@ -2642,7 +2642,7 @@ char menuid[128]; FinchBlistManager *manager = iter->data; GntMenuItem *item = gnt_menuitem_new(_(manager->name)); - snprintf(menuid, sizeof(menuid), "grouping-%s", manager->id); + g_snprintf(menuid, sizeof(menuid), "grouping-%s", manager->id); gnt_menuitem_set_id(GNT_MENU_ITEM(item), menuid); gnt_menu_add_item(GNT_MENU(subsub), item); g_object_set_data_full(G_OBJECT(item), "grouping-id", g_strdup(manager->id), g_free); @@ -3123,6 +3123,8 @@ PURPLE_CALLBACK(reconstruct_accounts_menu), NULL); purple_signal_connect(purple_connections_get_handle(), "signed-off", finch_blist_get_handle(), PURPLE_CALLBACK(reconstruct_accounts_menu), NULL); + purple_signal_connect(purple_accounts_get_handle(), "account-actions-changed", finch_blist_get_handle(), + PURPLE_CALLBACK(reconstruct_accounts_menu), NULL); purple_signal_connect(purple_blist_get_handle(), "buddy-status-changed", finch_blist_get_handle(), PURPLE_CALLBACK(buddy_status_changed), ggblist); purple_signal_connect(purple_blist_get_handle(), "buddy-idle-changed", finch_blist_get_handle(),
--- a/finch/gntnotify.c Sat Apr 18 06:52:59 2009 +0000 +++ b/finch/gntnotify.c Wed Apr 29 23:20:51 2009 +0000 @@ -263,7 +263,7 @@ userinfo_hash(PurpleAccount *account, const char *who) { char key[256]; - snprintf(key, sizeof(key), "%s - %s", purple_account_get_username(account), purple_normalize(account, who)); + g_snprintf(key, sizeof(key), "%s - %s", purple_account_get_username(account), purple_normalize(account, who)); return g_utf8_strup(key, -1); }
--- a/libpurple/account.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/account.c Wed Apr 29 23:20:51 2009 +0000 @@ -2742,6 +2742,10 @@ purple_value_new(PURPLE_TYPE_SUBTYPE, PURPLE_SUBTYPE_STATUS)); + purple_signal_register(handle, "account-actions-changed", + purple_marshal_VOID__POINTER, NULL, 1, + purple_value_new(PURPLE_TYPE_SUBTYPE, PURPLE_SUBTYPE_ACCOUNT)); + purple_signal_register(handle, "account-alias-changed", purple_marshal_VOID__POINTER_POINTER, NULL, 2, purple_value_new(PURPLE_TYPE_SUBTYPE,
--- a/libpurple/blist.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/blist.h Wed Apr 29 23:20:51 2009 +0000 @@ -118,8 +118,8 @@ /** * A Buddy list node. This can represent a group, a buddy, or anything else. - * This is a base class for struct buddy and struct group and for anything - * else that wants to put itself in the buddy list. */ + * This is a base class for PurpleBuddy, PurpleContact, PurpleGroup, and for + * anything else that wants to put itself in the buddy list. */ struct _PurpleBlistNode { PurpleBlistNodeType type; /**< The type of node this is */ PurpleBlistNode *prev; /**< The sibling before this buddy. */ @@ -207,7 +207,7 @@ PurpleBlistNode *node); /**< This will update a node in the buddy list. */ void (*remove)(PurpleBuddyList *list, PurpleBlistNode *node); /**< This removes a node from the list */ - void (*destroy)(PurpleBuddyList *list); /**< When the list gets destroyed, this gets called to destroy the UI. */ + void (*destroy)(PurpleBuddyList *list); /**< When the list is destroyed, this is called to destroy the UI. */ void (*set_visible)(PurpleBuddyList *list, gboolean show); /**< Hides or unhides the buddy list */ void (*request_add_buddy)(PurpleAccount *account, const char *username, @@ -276,7 +276,7 @@ * * @since 2.6.0 */ -void *purple_blist_get_ui_data(void); +gpointer purple_blist_get_ui_data(void); /** * Sets the UI data for the list. @@ -285,7 +285,7 @@ * * @since 2.6.0 */ -void purple_blist_set_ui_data(void *ui_data); +void purple_blist_set_ui_data(gpointer ui_data); /** * Returns the next node of a given node. This function is to be used to iterate @@ -360,7 +360,7 @@ * @return The UI data. * @since 2.6.0 */ -void *purple_blist_node_get_ui_data(const PurpleBlistNode *node); +gpointer purple_blist_node_get_ui_data(const PurpleBlistNode *node); /** * Sets the UI data of a given node. @@ -370,7 +370,7 @@ * * @since 2.6.0 */ -void purple_blist_node_set_ui_data(PurpleBlistNode *node, void *ui_data); +void purple_blist_node_set_ui_data(PurpleBlistNode *node, gpointer ui_data); /** * Shows the buddy list, creating a new one if necessary. @@ -393,6 +393,8 @@ /** * Updates a buddy's status. * + * This should only be called from within Purple. + * * @param buddy The buddy whose status has changed. * @param old_status The status from which we are changing. */ @@ -499,12 +501,19 @@ void purple_blist_add_chat(PurpleChat *chat, PurpleGroup *group, PurpleBlistNode *node); /** - * Creates a new buddy + * Creates a new buddy. + * + * This function only creates the PurpleBuddy. Use purple_blist_add_buddy + * to add the buddy to the list and purple_account_add_buddy to sync up + * with the server. * * @param account The account this buddy will get added to * @param name The name of the new buddy * @param alias The alias of the new buddy (or NULL if unaliased) * @return A newly allocated buddy + * + * @see purple_account_add_buddy + * @see purple_blist_add_buddy */ PurpleBuddy *purple_buddy_new(PurpleAccount *account, const char *name, const char *alias); @@ -618,7 +627,7 @@ * Creates a new group * * You can't have more than one group with the same name. Sorry. If you pass - * this the * name of a group that already exists, it will return that group. + * this the name of a group that already exists, it will return that group. * * @param name The name of the new group * @return A new group struct @@ -727,18 +736,22 @@ /** * Removes a buddy from the buddy list and frees the memory allocated to it. - * This doesn't actually try to remove the buddy from the server list, nor does - * it clean up the prpl_data. + * This doesn't actually try to remove the buddy from the server list. * * @param buddy The buddy to be removed + * + * @see purple_account_remove_buddy */ void purple_blist_remove_buddy(PurpleBuddy *buddy); /** * Removes a contact, and any buddies it contains, and frees the memory - * allocated to it. + * allocated to it. This calls purple_blist_remove_buddy and therefore + * doesn't remove the buddies from the server list. * * @param contact The contact to be removed + * + * @see purple_blist_remove_buddy */ void purple_blist_remove_contact(PurpleContact *contact); @@ -850,7 +863,7 @@ * Finds all PurpleBuddy structs given a name and an account * * @param account The account this buddy belongs to - * @param name The buddy's name (or NULL to return all buddies in the account) + * @param name The buddy's name (or NULL to return all buddies for the account) * * @return A GSList of buddies (which must be freed), or NULL if the buddy doesn't exist */ @@ -945,7 +958,7 @@ const char *purple_group_get_name(PurpleGroup *group); /** - * Called when an account gets signed on. Tells the UI to update all the + * Called when an account connects. Tells the UI to update all the * buddies. * * @param account The account @@ -954,7 +967,7 @@ /** - * Called when an account gets signed off. Sets the presence of all the buddies to 0 + * Called when an account disconnects. Sets the presence of all the buddies to 0 * and tells the UI to update them. * * @param account The account
--- a/libpurple/conversation.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/conversation.c Wed Apr 29 23:20:51 2009 +0000 @@ -1477,11 +1477,11 @@ return; if (!(flags & PURPLE_MESSAGE_WHISPER)) { - char *str; - - str = g_strdup(purple_normalize(account, who)); - - if (purple_strequal(str, purple_normalize(account, chat->nick))) { + const char *str; + + str = purple_normalize(account, who); + + if (purple_strequal(str, chat->nick)) { flags |= PURPLE_MESSAGE_SEND; } else { flags |= PURPLE_MESSAGE_RECV; @@ -1489,8 +1489,6 @@ if (purple_utf8_has_word(message, chat->nick)) flags |= PURPLE_MESSAGE_NICK; } - - g_free(str); } /* Pass this on to either the ops structure or the default write func. */
--- a/libpurple/core.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/core.c Wed Apr 29 23:20:51 2009 +0000 @@ -231,9 +231,9 @@ purple_conversations_uninit(); purple_connections_uninit(); purple_buddy_icons_uninit(); - purple_accounts_uninit(); purple_savedstatuses_uninit(); purple_status_uninit(); + purple_accounts_uninit(); purple_sound_uninit(); purple_theme_manager_uninit(); purple_xfers_uninit();
--- a/libpurple/dnssrv.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/dnssrv.c Wed Apr 29 23:20:51 2009 +0000 @@ -33,12 +33,18 @@ #ifndef T_SRV #define T_SRV 33 #endif -#else +#ifndef T_TXT +#define T_TXT 16 +#endif +#else /* WIN32 */ #include <windns.h> /* Missing from the mingw headers */ #ifndef DNS_TYPE_SRV # define DNS_TYPE_SRV 33 #endif +#ifndef DNS_TYPE_TXT +# define DNS_TYPE_TXT 16 +#endif #endif #include "dnssrv.h" @@ -59,10 +65,19 @@ DNS_FREE_TYPE FreeType) = NULL; #endif +struct _PurpleTxtResponse { + char *content; +}; + struct _PurpleSrvQueryData { - PurpleSrvCallback cb; + union { + PurpleSrvCallback srv; + PurpleTxtCallback txt; + } cb; + gpointer extradata; guint handle; + int type; #ifdef _WIN32 GThread *resolver; char *query; @@ -74,6 +89,11 @@ #endif }; +typedef struct _PurpleSrvInternalQuery { + int type; + char query[256]; +} PurpleSrvInternalQuery; + static gint responsecompare(gconstpointer ar, gconstpointer br) { @@ -99,6 +119,7 @@ { GList *ret = NULL; PurpleSrvResponse *srvres; + PurpleTxtResponse *txtres; queryans answer; int size; int qdcount; @@ -107,23 +128,22 @@ guchar *cp; gchar name[256]; guint16 type, dlen, pref, weight, port; - gchar query[256]; + PurpleSrvInternalQuery query; #ifdef HAVE_SIGNAL_H purple_restore_default_signal_handlers(); #endif - if (read(in, query, 256) <= 0) { + if (read(in, &query, sizeof(query)) <= 0) { close(out); close(in); _exit(0); } - size = res_query( query, C_IN, T_SRV, (u_char*)&answer, sizeof( answer)); - + size = res_query( query.query, C_IN, query.type, (u_char*)&answer, sizeof( answer)); + qdcount = ntohs(answer.hdr.qdcount); ancount = ntohs(answer.hdr.ancount); - cp = (guchar*)&answer + sizeof(HEADER); end = (guchar*)&answer + size; @@ -138,17 +158,14 @@ size = dn_expand((unsigned char*)&answer, end, cp, name, 256); if(size < 0) goto end; - cp += size; - GETSHORT(type,cp); /* skip ttl and class since we already know it */ cp += 6; GETSHORT(dlen,cp); - - if (type == T_SRV) { + if (query.type == T_SRV) { GETSHORT(pref,cp); GETSHORT(weight,cp); @@ -168,6 +185,11 @@ srvres->weight = weight; ret = g_list_insert_sorted(ret, srvres, responsecompare); + } else if (query.type == T_TXT) { + txtres = g_new0(PurpleTxtResponse, 1); + txtres->content = g_strndup((gchar*)(++cp), dlen-1); + ret = g_list_append(ret, txtres); + cp += dlen - 1; } else { cp += dlen; } @@ -176,11 +198,13 @@ end: size = g_list_length(ret); /* TODO: Check return value */ - write(out, &size, sizeof(int)); + write(out, &(query.type), sizeof(query.type)); + write(out, &size, sizeof(size)); while (ret != NULL) { /* TODO: Check return value */ - write(out, ret->data, sizeof(PurpleSrvResponse)); + if (query.type == T_SRV) write(out, ret->data, sizeof(PurpleSrvResponse)); + if (query.type == T_TXT) write(out, ret->data, sizeof(PurpleTxtResponse)); g_free(ret->data); ret = g_list_remove(ret, ret->data); } @@ -195,39 +219,67 @@ resolved(gpointer data, gint source, PurpleInputCondition cond) { int size; + int type; PurpleSrvQueryData *query_data = (PurpleSrvQueryData*)data; - PurpleSrvResponse *res; - PurpleSrvResponse *tmp; int i; - PurpleSrvCallback cb = query_data->cb; int status; - - if (read(source, &size, sizeof(int)) == sizeof(int)) - { - ssize_t red; - purple_debug_info("dnssrv","found %d SRV entries\n", size); - tmp = res = g_new0(PurpleSrvResponse, size); - for (i = 0; i < size; i++) { - red = read(source, tmp++, sizeof(PurpleSrvResponse)); - if (red != sizeof(PurpleSrvResponse)) { - purple_debug_error("dnssrv","unable to read srv " - "response: %s\n", g_strerror(errno)); + + if (read(source, &type, sizeof(type)) == sizeof(type)) { + if (type == T_SRV) { + PurpleSrvResponse *res; + PurpleSrvResponse *tmp; + PurpleSrvCallback cb = query_data->cb.srv; + if (read(source, &size, sizeof(int)) == sizeof(int)) { + ssize_t red; + purple_debug_info("dnssrv","found %d SRV entries\n", size); + tmp = res = g_new0(PurpleSrvResponse, size); + for (i = 0; i < size; i++) { + red = read(source, tmp++, sizeof(PurpleSrvResponse)); + if (red != sizeof(PurpleSrvResponse)) { + purple_debug_error("dnssrv","unable to read srv " + "response: %s\n", g_strerror(errno)); + size = 0; + g_free(res); + res = NULL; + } + } + } else { + purple_debug_info("dnssrv","found 0 SRV entries; errno is %i\n", errno); size = 0; - g_free(res); res = NULL; } + cb(res, size, query_data->extradata); + } else if (type == T_TXT) { + GSList *responses = NULL; + PurpleTxtResponse *res; + PurpleTxtCallback cb = query_data->cb.txt; + if (read(source, &size, sizeof(int)) == sizeof(int)) { + ssize_t red; + purple_debug_info("dnssrv","found %d TXT entries\n", size); + res = g_new0(PurpleTxtResponse, 1); + for (i = 0; i < size; i++) { + red = read(source, res, sizeof(PurpleTxtResponse)); + if (red != sizeof(PurpleTxtResponse)) { + purple_debug_error("dnssrv","unable to read txt " + "response: %s\n", g_strerror(errno)); + size = 0; + g_free(res); + g_slist_foreach(responses, (GFunc)purple_txt_response_destroy, NULL); + g_slist_free(responses); + responses = NULL; + break; + } + } + } else { + purple_debug_info("dnssrv","found 0 TXT entries; errno is %i\n", errno); + } + cb(responses, query_data->extradata); + } else { + purple_debug_info("dnssrv","type unknown of DNS result entry; errno is %i\n", errno); } } - else - { - purple_debug_info("dnssrv","found 0 SRV entries; errno is %i\n", errno); - size = 0; - res = NULL; - } - cb(res, size, query_data->extradata); waitpid(query_data->pid, &status, 0); - purple_srv_cancel(query_data); } @@ -241,32 +293,43 @@ PurpleSrvResponse *srvres = NULL; int size = 0; PurpleSrvQueryData *query_data = data; - if(query_data->error_message != NULL) purple_debug_error("dnssrv", query_data->error_message); else { - PurpleSrvResponse *srvres_tmp = NULL; - GSList *lst = query_data->results; + if (query_data->type == DNS_TYPE_SRV) { + PurpleSrvResponse *srvres_tmp = NULL; + GSList *lst = query_data->results; + + size = g_slist_length(lst); - size = g_slist_length(lst); + if(query_data->cb.srv && size > 0) + srvres_tmp = srvres = g_new0(PurpleSrvResponse, size); + while (lst) { + if(query_data->cb.srv) + memcpy(srvres_tmp++, lst->data, sizeof(PurpleSrvResponse)); + g_free(lst->data); + lst = g_slist_remove(lst, lst->data); + } + + query_data->results = NULL; - if(query_data->cb && size > 0) - srvres_tmp = srvres = g_new0(PurpleSrvResponse, size); - while (lst) { - if(query_data->cb) - memcpy(srvres_tmp++, lst->data, sizeof(PurpleSrvResponse)); - g_free(lst->data); - lst = g_slist_remove(lst, lst->data); + purple_debug_info("dnssrv", "found %d SRV entries\n", size); + + if(query_data->cb.srv) query_data->cb.srv(srvres, size, query_data->extradata); + } else if (query_data->type == DNS_TYPE_TXT) { + GSList *lst = query_data->results; + + purple_debug_info("dnssrv", "found %d TXT entries\n", g_slist_length(lst)); + + if (query_data->cb.txt) { + query_data->results = NULL; + query_data->cb.txt(lst, query_data->extradata); + } + } else { + purple_debug_error("dnssrv", "unknown query type"); } - - query_data->results = NULL; - - purple_debug_info("dnssrv", "found %d SRV entries\n", size); } - if(query_data->cb) - query_data->cb(srvres, size, query_data->extradata); - query_data->resolver = NULL; query_data->handle = 0; @@ -279,40 +342,76 @@ res_thread(gpointer data) { PDNS_RECORD dr = NULL; - int type = DNS_TYPE_SRV; + int type; DNS_STATUS ds; PurpleSrvQueryData *query_data = data; - + type = query_data->type; ds = MyDnsQuery_UTF8(query_data->query, type, DNS_QUERY_STANDARD, NULL, &dr, NULL); if (ds != ERROR_SUCCESS) { gchar *msg = g_win32_error_message(ds); - query_data->error_message = g_strdup_printf("Couldn't look up SRV record. %s (%lu).\n", msg, ds); + if (type == DNS_TYPE_SRV) { + query_data->error_message = g_strdup_printf("Couldn't look up SRV record. %s (%lu).\n", msg, ds); + } else if (type == DNS_TYPE_TXT) { + query_data->error_message = g_strdup_printf("Couldn't look up TXT record. %s (%lu).\n", msg, ds); + } g_free(msg); } else { - PDNS_RECORD dr_tmp; - GSList *lst = NULL; - DNS_SRV_DATA *srv_data; - PurpleSrvResponse *srvres; + if (type == DNS_TYPE_SRV) { + PDNS_RECORD dr_tmp; + GSList *lst = NULL; + DNS_SRV_DATA *srv_data; + PurpleSrvResponse *srvres; + + for (dr_tmp = dr; dr_tmp != NULL; dr_tmp = dr_tmp->pNext) { + /* Discard any incorrect entries. I'm not sure if this is necessary */ + if (dr_tmp->wType != type || strcmp(dr_tmp->pName, query_data->query) != 0) { + continue; + } - for (dr_tmp = dr; dr_tmp != NULL; dr_tmp = dr_tmp->pNext) { - /* Discard any incorrect entries. I'm not sure if this is necessary */ - if (dr_tmp->wType != type || strcmp(dr_tmp->pName, query_data->query) != 0) { - continue; + srv_data = &dr_tmp->Data.SRV; + srvres = g_new0(PurpleSrvResponse, 1); + strncpy(srvres->hostname, srv_data->pNameTarget, 255); + srvres->hostname[255] = '\0'; + srvres->pref = srv_data->wPriority; + srvres->port = srv_data->wPort; + srvres->weight = srv_data->wWeight; + + lst = g_slist_insert_sorted(lst, srvres, responsecompare); } - srv_data = &dr_tmp->Data.SRV; - srvres = g_new0(PurpleSrvResponse, 1); - strncpy(srvres->hostname, srv_data->pNameTarget, 255); - srvres->hostname[255] = '\0'; - srvres->pref = srv_data->wPriority; - srvres->port = srv_data->wPort; - srvres->weight = srv_data->wWeight; + MyDnsRecordListFree(dr, DnsFreeRecordList); + query_data->results = lst; + } else if (type == DNS_TYPE_TXT) { + PDNS_RECORD dr_tmp; + GSList *lst = NULL; + DNS_TXT_DATA *txt_data; + PurpleTxtResponse *txtres; + + for (dr_tmp = dr; dr_tmp != NULL; dr_tmp = dr_tmp->pNext) { + GString *s; + int i; + + /* Discard any incorrect entries. I'm not sure if this is necessary */ + if (dr_tmp->wType != type || strcmp(dr_tmp->pName, query_data->query) != 0) { + continue; + } - lst = g_slist_insert_sorted(lst, srvres, responsecompare); - } + txt_data = &dr_tmp->Data.TXT; + txtres = g_new0(PurpleTxtResponse, 1); + + s = g_string_new(""); + for (i = 0; i < txt_data->dwStringCount; ++i) + s = g_string_append(s, txt_data->pStringArray[i]); + txtres->content = g_string_free(s, FALSE); - MyDnsRecordListFree(dr, DnsFreeRecordList); - query_data->results = lst; + lst = g_slist_append(lst, txtres); + } + + MyDnsRecordListFree(dr, DnsFreeRecordList); + query_data->results = lst; + } else { + + } } /* back to main thread */ @@ -331,6 +430,7 @@ char *query; PurpleSrvQueryData *query_data; #ifndef _WIN32 + PurpleSrvInternalQuery internal_query; int in[2], out[2]; int pid; #else @@ -377,11 +477,16 @@ close(out[1]); close(in[0]); - if (write(in[1], query, strlen(query)+1) < 0) + internal_query.type = T_SRV; + strncpy(internal_query.query, query, 255); + + if (write(in[1], &internal_query, sizeof(internal_query)) < 0) purple_debug_error("dnssrv", "Could not write to SRV resolver\n"); + query_data = g_new0(PurpleSrvQueryData, 1); - query_data->cb = cb; + query_data->type = T_SRV; + query_data->cb.srv = cb; query_data->extradata = extradata; query_data->pid = pid; query_data->fd_out = out[0]; @@ -400,7 +505,8 @@ } query_data = g_new0(PurpleSrvQueryData, 1); - query_data->cb = cb; + query_data->type = DNS_TYPE_SRV; + query_data->cb.srv = cb; query_data->query = query; query_data->extradata = extradata; @@ -424,6 +530,104 @@ #endif } +PurpleSrvQueryData *purple_txt_resolve(const char *owner, const char *domain, PurpleTxtCallback cb, gpointer extradata) +{ + char *query; + PurpleSrvQueryData *query_data; +#ifndef _WIN32 + PurpleSrvInternalQuery internal_query; + int in[2], out[2]; + int pid; +#else + GError* err = NULL; + static gboolean initialized = FALSE; +#endif + + query = g_strdup_printf("%s.%s", owner, domain); + purple_debug_info("dnssrv","querying TXT record for %s\n", query); + +#ifndef _WIN32 + if(pipe(in) || pipe(out)) { + purple_debug_error("dnssrv", "Could not create pipe\n"); + g_free(query); + cb(NULL, extradata); + return NULL; + } + + pid = fork(); + if (pid == -1) { + purple_debug_error("dnssrv", "Could not create process!\n"); + cb(NULL, extradata); + g_free(query); + return NULL; + } + + /* Child */ + if (pid == 0) + { + g_free(query); + + close(out[0]); + close(in[1]); + resolve(in[0], out[1]); + /* resolve() does not return */ + } + + close(out[1]); + close(in[0]); + + internal_query.type = T_TXT; + strncpy(internal_query.query, query, 255); + + if (write(in[1], &internal_query, sizeof(internal_query)) < 0) + purple_debug_error("dnssrv", "Could not write to TXT resolver\n"); + + query_data = g_new0(PurpleSrvQueryData, 1); + query_data->type = T_TXT; + query_data->cb.txt = cb; + query_data->extradata = extradata; + query_data->pid = pid; + query_data->fd_out = out[0]; + query_data->fd_in = in[1]; + query_data->handle = purple_input_add(out[0], PURPLE_INPUT_READ, resolved, query_data); + + g_free(query); + + return query_data; +#else + if (!initialized) { + MyDnsQuery_UTF8 = (void*) wpurple_find_and_loadproc("dnsapi.dll", "DnsQuery_UTF8"); + MyDnsRecordListFree = (void*) wpurple_find_and_loadproc( + "dnsapi.dll", "DnsRecordListFree"); + initialized = TRUE; + } + + query_data = g_new0(PurpleSrvQueryData, 1); + query_data->type = DNS_TYPE_TXT; + query_data->cb.txt = cb; + query_data->query = query; + query_data->extradata = extradata; + + if (!MyDnsQuery_UTF8 || !MyDnsRecordListFree) + query_data->error_message = g_strdup("System missing DNS API (Requires W2K+)\n"); + else { + query_data->resolver = g_thread_create(res_thread, query_data, FALSE, &err); + if (query_data->resolver == NULL) { + query_data->error_message = g_strdup_printf("TXT thread create failure: %s\n", (err && err->message) ? err->message : ""); + g_error_free(err); + } + } + + /* The query isn't going to happen, so finish the TXT lookup now. + * Asynchronously call the callback since stuff may not expect + * the callback to be called before this returns */ + if (query_data->error_message != NULL) + query_data->handle = purple_timeout_add(0, res_main_thread_cb, query_data); + + return query_data; +#endif +} + void purple_srv_cancel(PurpleSrvQueryData *query_data) { @@ -437,7 +641,7 @@ * just set the callback to NULL and let the DNS lookup * finish. */ - query_data->cb = NULL; + query_data->cb.srv = NULL; return; } g_free(query_data->query); @@ -448,3 +652,25 @@ #endif g_free(query_data); } + +void +purple_txt_cancel(PurpleSrvQueryData *query_data) +{ + purple_srv_cancel(query_data); +} + +const gchar * +purple_txt_response_get_content(PurpleTxtResponse *resp) +{ + g_return_val_if_fail(resp != NULL, NULL); + + return resp->content; +} + +void purple_txt_response_destroy(PurpleTxtResponse *resp) +{ + g_return_if_fail(resp != NULL); + + g_free(resp->content); + g_free(resp); +}
--- a/libpurple/dnssrv.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/dnssrv.h Wed Apr 29 23:20:51 2009 +0000 @@ -28,8 +28,11 @@ extern "C" { #endif +typedef struct _PurpleSrvQueryData PurpleSrvQueryData; typedef struct _PurpleSrvResponse PurpleSrvResponse; -typedef struct _PurpleSrvQueryData PurpleSrvQueryData; +typedef struct _PurpleTxtResponse PurpleTxtResponse; + +#include <glib.h> struct _PurpleSrvResponse { char hostname[256]; @@ -41,6 +44,14 @@ typedef void (*PurpleSrvCallback)(PurpleSrvResponse *resp, int results, gpointer data); /** + * Callback that returns the data retrieved from a DNS TXT lookup. + * + * @param responses A GSList of PurpleTxtResponse objects. + * @param data The extra data passed to purple_txt_resolve. + */ +typedef void (*PurpleTxtCallback)(GSList *responses, gpointer data); + +/** * Queries an SRV record. * * @param protocol Name of the protocol (e.g. "sip") @@ -58,6 +69,43 @@ */ void purple_srv_cancel(PurpleSrvQueryData *query_data); +/** + * Queries an TXT record. + * + * @param owner Name of the protocol (e.g. "_xmppconnect") + * @param domain Domain name to query (e.g. "blubb.com") + * @param cb A callback which will be called with the results + * @param extradata Extra data to be passed to the callback + * + * @since 2.6.0 + */ +PurpleSrvQueryData *purple_txt_resolve(const char *owner, const char *domain, PurpleTxtCallback cb, gpointer extradata); + +/** + * Cancel an TXT DNS query. + * + * @param query_data The request to cancel. + * @since 2.6.0 + */ +void purple_txt_cancel(PurpleSrvQueryData *query_data); + +/** + * Get the value of the current TXT record. + * + * @param resp The TXT response record + * @returns The value of the current TXT record. + * @since 2.6.0 + */ +const gchar *purple_txt_response_get_content(PurpleTxtResponse *resp); + +/** + * Destroy a TXT DNS response object. + * + * @param response The PurpleTxtResponse to destroy. + * @since 2.6.0 + */ +void purple_txt_response_destroy(PurpleTxtResponse *resp); + #ifdef __cplusplus } #endif
--- a/libpurple/idle.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/idle.c Wed Apr 29 23:20:51 2009 +0000 @@ -187,9 +187,8 @@ purple_savedstatus_set_idleaway(TRUE); no_away = FALSE; } - else if (!no_away && time_idle < away_seconds) + else if (purple_savedstatus_is_idleaway() && time_idle < away_seconds) { - no_away = TRUE; purple_savedstatus_set_idleaway(FALSE); if (time_until_next_idle_event == 0 || (away_seconds - time_idle) < time_until_next_idle_event) time_until_next_idle_event = away_seconds - time_idle;
--- a/libpurple/media.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/media.c Wed Apr 29 23:20:51 2009 +0000 @@ -1850,7 +1850,7 @@ #endif GList * -purple_media_get_session_names(PurpleMedia *media) +purple_media_get_session_ids(PurpleMedia *media) { #ifdef USE_VV g_return_val_if_fail(PURPLE_IS_MEDIA(media), NULL); @@ -2663,12 +2663,13 @@ } GList * -purple_media_get_local_candidates(PurpleMedia *media, const gchar *sess_id, const gchar *name) +purple_media_get_local_candidates(PurpleMedia *media, const gchar *sess_id, + const gchar *participant) { #ifdef USE_VV PurpleMediaStream *stream; g_return_val_if_fail(PURPLE_IS_MEDIA(media), NULL); - stream = purple_media_get_stream(media, sess_id, name); + stream = purple_media_get_stream(media, sess_id, participant); return purple_media_candidate_list_from_fs(stream->local_candidates); #else return NULL; @@ -2677,20 +2678,21 @@ void purple_media_add_remote_candidates(PurpleMedia *media, const gchar *sess_id, - const gchar *name, GList *remote_candidates) + const gchar *participant, + GList *remote_candidates) { #ifdef USE_VV PurpleMediaStream *stream; GError *err = NULL; g_return_if_fail(PURPLE_IS_MEDIA(media)); - stream = purple_media_get_stream(media, sess_id, name); + stream = purple_media_get_stream(media, sess_id, participant); if (stream == NULL) { purple_debug_error("media", "purple_media_add_remote_candidates: " "couldn't find stream %s %s.\n", - sess_id, name); + sess_id, participant); return; } @@ -2716,12 +2718,12 @@ GList * purple_media_get_active_local_candidates(PurpleMedia *media, - const gchar *sess_id, const gchar *name) + const gchar *sess_id, const gchar *participant) { #ifdef USE_VV PurpleMediaStream *stream; g_return_val_if_fail(PURPLE_IS_MEDIA(media), NULL); - stream = purple_media_get_stream(media, sess_id, name); + stream = purple_media_get_stream(media, sess_id, participant); return purple_media_candidate_list_from_fs( stream->active_local_candidates); #else @@ -2731,12 +2733,12 @@ GList * purple_media_get_active_remote_candidates(PurpleMedia *media, - const gchar *sess_id, const gchar *name) + const gchar *sess_id, const gchar *participant) { #ifdef USE_VV PurpleMediaStream *stream; g_return_val_if_fail(PURPLE_IS_MEDIA(media), NULL); - stream = purple_media_get_stream(media, sess_id, name); + stream = purple_media_get_stream(media, sess_id, participant); return purple_media_candidate_list_from_fs( stream->active_remote_candidates); #else @@ -2746,7 +2748,8 @@ #endif gboolean -purple_media_set_remote_codecs(PurpleMedia *media, const gchar *sess_id, const gchar *name, GList *codecs) +purple_media_set_remote_codecs(PurpleMedia *media, const gchar *sess_id, + const gchar *participant, GList *codecs) { #ifdef USE_VV PurpleMediaStream *stream; @@ -2755,7 +2758,7 @@ GError *err = NULL; g_return_val_if_fail(PURPLE_IS_MEDIA(media), FALSE); - stream = purple_media_get_stream(media, sess_id, name); + stream = purple_media_get_stream(media, sess_id, participant); if (stream == NULL) return FALSE; @@ -3029,7 +3032,7 @@ stream->session->id, stream->participant); } - iter = purple_media_get_session_names(media); + iter = purple_media_get_session_ids(media); for (; iter; iter = g_list_delete_link(iter, iter)) { gchar *session_name = iter->data; purple_media_manager_remove_output_windows(
--- a/libpurple/media.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/media.h Wed Apr 29 23:20:51 2009 +0000 @@ -351,15 +351,15 @@ void purple_media_codec_list_free(GList *codecs); /** - * Gets a list of session names. + * Gets a list of session IDs. * - * @param media The media session to retrieve session names from. + * @param media The media session from which to retrieve session IDs. * - * @return GList of session names. + * @return GList of session IDs. The caller must free the list. * * @since 2.6.0 */ -GList *purple_media_get_session_names(PurpleMedia *media); +GList *purple_media_get_session_ids(PurpleMedia *media); /** * Gets the PurpleAccount this media session is on. @@ -495,14 +495,14 @@ * * @param media The media object to find the session in. * @param sess_id The session id of the session find the stream in. - * @param name The name of the remote user to add the candidates for. + * @param participant The name of the remote user to add the candidates for. * @param remote_candidates The remote candidates to add. * * @since 2.6.0 */ void purple_media_add_remote_candidates(PurpleMedia *media, const gchar *sess_id, - const gchar *name, + const gchar *participant, GList *remote_candidates); /** @@ -510,13 +510,13 @@ * * @param media The media object to find the session in. * @param sess_id The session id of the session to find the stream in. - * @param name The name of the remote user to get the candidates from. + * @param participant The name of the remote user to get the candidates from. * * @since 2.6.0 */ GList *purple_media_get_local_candidates(PurpleMedia *media, const gchar *sess_id, - const gchar *name); + const gchar *participant); #if 0 /* @@ -529,24 +529,26 @@ * * @param media The media object to find the session in. * @param sess_id The session id of the session to find the stream in. - * @param name The name of the remote user to get the active candidate from. + * @param participant The name of the remote user to get the active candidate + * from. * * @return The active candidates retrieved. */ GList *purple_media_get_active_local_candidates(PurpleMedia *media, - const gchar *sess_id, const gchar *name); + const gchar *sess_id, const gchar *participant); /** * Gets the active remote candidates for the stream. * * @param media The media object to find the session in. * @param sess_id The session id of the session to find the stream in. - * @param name The name of the remote user to get the remote candidate from. + * @param participant The name of the remote user to get the remote candidate + * from. * * @return The remote candidates retrieved. */ GList *purple_media_get_active_remote_candidates(PurpleMedia *media, - const gchar *sess_id, const gchar *name); + const gchar *sess_id, const gchar *participant); #endif /** @@ -554,14 +556,14 @@ * * @param media The media object to find the session in. * @param sess_id The session id of the session find the stream in. - * @param name The name of the remote user to get the candidates from. + * @param participant The name of the remote user to set the candidates from. * * @return @c TRUE The codecs were set successfully, or @c FALSE otherwise. * * @since 2.6.0 */ gboolean purple_media_set_remote_codecs(PurpleMedia *media, const gchar *sess_id, - const gchar *name, GList *codecs); + const gchar *participant, GList *codecs); /** * Returns whether or not the candidates for set of streams are prepared
--- a/libpurple/mediamanager.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/mediamanager.h Wed Apr 29 23:20:51 2009 +0000 @@ -52,7 +52,7 @@ #endif /**************************************************************************/ -/** @cname Media Manager API */ +/** @name Media Manager API */ /**************************************************************************/ /*@{*/
--- a/libpurple/plugin.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/plugin.h Wed Apr 29 23:20:51 2009 +0000 @@ -105,6 +105,20 @@ void *ui_info; /**< Used only by UI-specific plugins to build a preference screen with a custom UI */ void *extra_info; PurplePluginUiInfo *prefs_info; /**< Used by any plugin to display preferences. If #ui_info has been specified, this will be ignored. */ + + /** + * This callback has a different use depending on whether this + * plugin type is PURPLE_PLUGIN_STANDARD or PURPLE_PLUGIN_PROTOCOL. + * + * If PURPLE_PLUGIN_STANDARD then the list of actions will show up + * in the Tools menu, under a submenu with the name of the plugin. + * context will be NULL. + * + * If PURPLE_PLUGIN_PROTOCOL then the list of actions will show up + * in the Accounts menu, under a submenu with the name of the + * account. context will be set to the PurpleConnection for that + * account. This callback will only be called for online accounts. + */ GList *(*actions)(PurplePlugin *plugin, gpointer context); void (*_purple_reserved1)(void);
--- a/libpurple/plugins/perl/perl-common.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/plugins/perl/perl-common.c Wed Apr 29 23:20:51 2009 +0000 @@ -403,7 +403,7 @@ static SV * purple_perl_sv_from_subtype(const PurpleValue *value, void *arg) { - const char *stash = NULL; + const char *stash = "Purple"; /* ? */ switch (purple_value_get_subtype(value)) { case PURPLE_SUBTYPE_ACCOUNT: @@ -442,6 +442,9 @@ case PURPLE_SUBTYPE_STATUS: stash = "Purple::Status"; break; + case PURPLE_SUBTYPE_SAVEDSTATUS: + stash = "Purple::SavedStatus"; + break; case PURPLE_SUBTYPE_LOG: stash = "Purple::Log"; break; @@ -451,10 +454,19 @@ case PURPLE_SUBTYPE_XMLNODE: stash = "Purple::XMLNode"; break; - - default: - stash = "Purple"; /* ? */ - } + case PURPLE_SUBTYPE_USERINFO: + stash = "Purple::NotifyUserInfo"; + break; + case PURPLE_SUBTYPE_STORED_IMAGE: + stash = "Purple::StoredImage"; + break; + case PURPLE_SUBTYPE_CERTIFICATEPOOL: + stash = "Purple::Certificate::Pool"; + break; + case PURPLE_SUBTYPE_UNKNOWN: + stash = "Purple::Unknown"; + break; + } return sv_2mortal(purple_perl_bless_object(arg, stash)); }
--- a/libpurple/plugins/statenotify.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/plugins/statenotify.c Wed Apr 29 23:20:51 2009 +0000 @@ -71,9 +71,9 @@ void *data) { if (purple_prefs_get_bool("/plugins/core/statenotify/notify_idle")) { - if (idle) { + if (idle && !old_idle) { write_status(buddy, _("%s has become idle.")); - } else { + } else if (!idle && old_idle) { write_status(buddy, _("%s is no longer idle.")); } }
--- a/libpurple/plugins/tcl/tcl_cmds.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/plugins/tcl/tcl_cmds.c Wed Apr 29 23:20:51 2009 +0000 @@ -683,8 +683,9 @@ int tcl_cmd_connection(ClientData unused, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[]) { Tcl_Obj *list, *elem; - const char *cmds[] = { "account", "displayname", "handle", "list", NULL }; - enum { CMD_CONN_ACCOUNT, CMD_CONN_DISPLAYNAME, CMD_CONN_HANDLE, CMD_CONN_LIST } cmd; + const char *cmds[] = { "account", "displayname", "handle", "list", "state", NULL }; + enum { CMD_CONN_ACCOUNT, CMD_CONN_DISPLAYNAME, CMD_CONN_HANDLE, + CMD_CONN_LIST, CMD_CONN_STATE } cmd; int error; GList *cur; PurpleConnection *gc; @@ -739,6 +740,25 @@ } Tcl_SetObjResult(interp, list); break; + case CMD_CONN_STATE: + if (objc != 3) { + Tcl_WrongNumArgs(interp, 2, objv, "gc"); + return TCL_ERROR; + } + if ((gc = tcl_validate_gc(objv[2], interp)) == NULL) + return TCL_ERROR; + switch (purple_connection_get_state(gc)) { + case PURPLE_DISCONNECTED: + Tcl_SetObjResult(interp, Tcl_NewStringObj("disconnected", -1)); + break; + case PURPLE_CONNECTED: + Tcl_SetObjResult(interp, Tcl_NewStringObj("connected", -1)); + break; + case PURPLE_CONNECTING: + Tcl_SetObjResult(interp, Tcl_NewStringObj("connecting", -1)); + break; + } + break; } return TCL_OK;
--- a/libpurple/protocols/bonjour/parser.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/bonjour/parser.c Wed Apr 29 23:20:51 2009 +0000 @@ -153,6 +153,18 @@ xmlnode_insert_data(bconv->current, (const char*) text, text_len); } +static void +bonjour_parser_structured_error_handler(void *user_data, xmlErrorPtr error) +{ + BonjourJabberConversation *bconv = user_data; + + purple_debug_error("jabber", "XML parser error for BonjourJabberConversation %p: " + "Domain %i, code %i, level %i: %s", + bconv, + error->domain, error->code, error->level, + (error->message ? error->message : "(null)\n")); +} + static xmlSAXHandler bonjour_parser_libxml = { NULL, /*internalSubset*/ NULL, /*isStandalone*/ @@ -185,7 +197,7 @@ NULL, /*_private*/ bonjour_parser_element_start_libxml, /*startElementNs*/ bonjour_parser_element_end_libxml, /*endElementNs*/ - NULL /*serror*/ + bonjour_parser_structured_error_handler /*serror*/ }; void
--- a/libpurple/protocols/irc/msgs.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/irc/msgs.c Wed Apr 29 23:20:51 2009 +0000 @@ -1004,10 +1004,25 @@ void irc_msg_nickused(struct irc_conn *irc, const char *name, const char *from, char **args) { char *newnick, *buf, *end; + PurpleConnection *gc = purple_account_get_connection(irc->account); if (!args || !args[1]) return; + if (gc && purple_connection_get_state(gc) == PURPLE_CONNECTED) { + /* We only want to do the following dance if the connection + has not been successfully completed. If it has, just + notify the user that their /nick command didn't go. */ + buf = g_strdup_printf(_("The nickname \"%s\" is already being used."), + irc->reqnick); + purple_notify_error(gc, _("Nickname in use"), + _("Nickname in use"), buf); + g_free(buf); + g_free(irc->reqnick); + irc->reqnick = NULL; + return; + } + if (strlen(args[1]) < strlen(irc->reqnick) || irc->nickused) newnick = g_strdup(args[1]); else
--- a/libpurple/protocols/jabber/Makefile.am Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/Makefile.am Wed Apr 29 23:20:51 2009 +0000 @@ -9,6 +9,8 @@ auth.h \ buddy.c \ buddy.h \ + bosh.c \ + bosh.h \ chat.c \ chat.h \ data.c \ @@ -61,6 +63,8 @@ adhoccommands.h \ pep.c \ pep.h \ + useravatar.c \ + useravatar.h \ usermood.c \ usermood.h \ usernick.c \
--- a/libpurple/protocols/jabber/Makefile.mingw Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/Makefile.mingw Wed Apr 29 23:20:51 2009 +0000 @@ -46,6 +46,7 @@ adhoccommands.c \ auth.c \ buddy.c \ + bosh.c \ caps.c \ chat.c \ data.c \ @@ -70,6 +71,7 @@ presence.c \ roster.c \ si.c \ + useravatar.c \ usermood.c \ usernick.c \ usertune.c \
--- a/libpurple/protocols/jabber/adhoccommands.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/adhoccommands.c Wed Apr 29 23:20:51 2009 +0000 @@ -39,30 +39,18 @@ GList *actionslist; } JabberAdHocActionInfo; -void jabber_adhoc_disco_result_cb(JabberStream *js, const char *from, - JabberIqType type, const char *id, - xmlnode *packet, gpointer data) +static void +jabber_adhoc_got_buddy_list(JabberStream *js, const char *from, xmlnode *query) { - const char *node; - xmlnode *query, *item; - JabberID *jabberid; + JabberID *jid; JabberBuddy *jb; JabberBuddyResource *jbr = NULL; - - if (type == JABBER_IQ_ERROR) - return; + xmlnode *item; - query = xmlnode_get_child_with_namespace(packet,"query","http://jabber.org/protocol/disco#items"); - if(!query) - return; - node = xmlnode_get_attrib(query,"node"); - if(!node || strcmp(node, "http://jabber.org/protocol/commands")) - return; - - if((jabberid = jabber_id_new(from))) { - if(jabberid->resource && (jb = jabber_buddy_find(js, from, TRUE))) - jbr = jabber_buddy_find_resource(jb, jabberid->resource); - jabber_id_free(jabberid); + if ((jid = jabber_id_new(from))) { + if (jid->resource && (jb = jabber_buddy_find(js, from, TRUE))) + jbr = jabber_buddy_find_resource(jb, jid->resource); + jabber_id_free(jid); } if(!jbr) @@ -96,11 +84,31 @@ } } +void +jabber_adhoc_disco_result_cb(JabberStream *js, const char *from, + JabberIqType type, const char *id, + xmlnode *packet, gpointer data) +{ + xmlnode *query; + const char *node; + + if (type == JABBER_IQ_ERROR) + return; + + query = xmlnode_get_child_with_namespace(packet, "query", "http://jabber.org/protocol/disco#items"); + if (!query) + return; + node = xmlnode_get_attrib(query, "node"); + if (!purple_strequal(node, "http://jabber.org/protocol/commands")) + return; + + jabber_adhoc_got_buddy_list(js, from, query); +} + static void jabber_adhoc_parse(JabberStream *js, const char *from, JabberIqType type, const char *id, xmlnode *packet, gpointer data); - static void do_adhoc_action_cb(JabberStream *js, xmlnode *result, const char *actionhandle, gpointer user_data) { xmlnode *command; GList *action; @@ -224,11 +232,8 @@ } static void -jabber_adhoc_server_got_list_cb(JabberStream *js, const char *from, - JabberIqType type, const char *id, - xmlnode *packet, gpointer data) +jabber_adhoc_got_server_list(JabberStream *js, const char *from, xmlnode *query) { - xmlnode *query = xmlnode_get_child_with_namespace(packet, "query", "http://jabber.org/protocol/disco#items"); xmlnode *item; if(!query) @@ -258,6 +263,29 @@ js->commands = g_list_append(js->commands,cmd); } + + if (js->state == JABBER_STREAM_CONNECTED) + purple_prpl_got_account_actions(purple_connection_get_account(js->gc)); +} + +static void +jabber_adhoc_server_got_list_cb(JabberStream *js, const char *from, + JabberIqType type, const char *id, + xmlnode *packet, gpointer data) +{ + xmlnode *query = xmlnode_get_child_with_namespace(packet, "query", "http://jabber.org/protocol/disco#items"); + + jabber_adhoc_got_server_list(js, from, query); + +} + +void jabber_adhoc_got_list(JabberStream *js, const char *from, xmlnode *query) +{ + if (purple_strequal(from, js->user->domain)) { + jabber_adhoc_got_server_list(js, from, query); + } else { + jabber_adhoc_got_buddy_list(js, from, query); + } } void jabber_adhoc_server_get_list(JabberStream *js) {
--- a/libpurple/protocols/jabber/adhoccommands.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/adhoccommands.h Wed Apr 29 23:20:51 2009 +0000 @@ -34,6 +34,8 @@ void jabber_adhoc_execute_action(PurpleBlistNode *node, gpointer data); +void jabber_adhoc_got_list(JabberStream *js, const char *from, xmlnode *query); + void jabber_adhoc_server_get_list(JabberStream *js); void jabber_adhoc_init_server_commands(JabberStream *js, GList **m);
--- a/libpurple/protocols/jabber/auth.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/auth.c Wed Apr 29 23:20:51 2009 +0000 @@ -30,9 +30,10 @@ #include "util.h" #include "xmlnode.h" +#include "auth.h" +#include "disco.h" +#include "jabber.h" #include "jutil.h" -#include "auth.h" -#include "jabber.h" #include "iq.h" #include "notify.h" @@ -282,7 +283,7 @@ secprops.min_ssf = 0; secprops.security_flags = SASL_SEC_NOANONYMOUS; - if (!js->gsc) { + if (!jabber_stream_is_ssl(js)) { secprops.max_ssf = -1; secprops.maxbufsize = 4096; plaintext = purple_account_get_bool(js->gc->account, "auth_plain_in_clear", FALSE); @@ -545,7 +546,7 @@ } else if(plain) { js->auth_type = JABBER_AUTH_PLAIN; - if(js->gsc == NULL && !purple_account_get_bool(js->gc->account, "auth_plain_in_clear", FALSE)) { + if(!jabber_stream_is_ssl(js) && !purple_account_get_bool(js->gc->account, "auth_plain_in_clear", FALSE)) { char *msg = g_strdup_printf(_("%s requires plaintext authentication over an unencrypted connection. Allow this and continue authentication?"), js->gc->account->username); purple_request_yes_no(js->gc, _("Plaintext Authentication"), @@ -572,7 +573,7 @@ xmlnode *packet, gpointer data) { if (type == JABBER_IQ_RESULT) { - jabber_stream_set_state(js, JABBER_STREAM_CONNECTED); + jabber_disco_items_server(js); } else { PurpleConnectionError reason = PURPLE_CONNECTION_ERROR_NETWORK_ERROR; char *msg = jabber_parse_error(js, packet, &reason); @@ -659,7 +660,7 @@ jabber_iq_send(iq); } else if(xmlnode_get_child(query, "password")) { - if(js->gsc == NULL && !purple_account_get_bool(js->gc->account, + if(!jabber_stream_is_ssl(js) && !purple_account_get_bool(js->gc->account, "auth_plain_in_clear", FALSE)) { char *msg = g_strdup_printf(_("%s requires plaintext authentication over an unencrypted connection. Allow this and continue authentication?"), js->gc->account->username);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/protocols/jabber/bosh.c Wed Apr 29 23:20:51 2009 +0000 @@ -0,0 +1,907 @@ +/* + * purple - Jabber Protocol Plugin + * + * Copyright (C) 2008, Tobias Markmann <tmarkmann@googlemail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + * + */ +#include "internal.h" +#include "circbuffer.h" +#include "core.h" +#include "cipher.h" +#include "debug.h" +#include "prpl.h" +#include "util.h" +#include "xmlnode.h" + +#include "bosh.h" + +#define MAX_HTTP_CONNECTIONS 2 +#define MAX_FAILED_CONNECTIONS 3 + +typedef struct _PurpleHTTPConnection PurpleHTTPConnection; + +typedef void (*PurpleBOSHConnectionConnectFunction)(PurpleBOSHConnection *conn); +typedef void (*PurpleBOSHConnectionReceiveFunction)(PurpleBOSHConnection *conn, xmlnode *node); + +static char *bosh_useragent = NULL; + +typedef enum { + PACKET_TERMINATE, + PACKET_STREAM_RESTART, + PACKET_NORMAL, +} PurpleBOSHPacketType; + +struct _PurpleBOSHConnection { + JabberStream *js; + gboolean pipelining; + PurpleHTTPConnection *connections[MAX_HTTP_CONNECTIONS]; + unsigned short failed_connections; + + gboolean ready; + gboolean ssl; + + /* decoded URL */ + char *host; + int port; + char *path; + + /* Must be big enough to hold 2^53 - 1 */ + guint64 rid; + char *sid; + + unsigned int inactivity_timer; + int max_inactivity; + int wait; + + PurpleCircBuffer *pending; + int max_requests; + int requests; + + PurpleBOSHConnectionConnectFunction connect_cb; + PurpleBOSHConnectionReceiveFunction receive_cb; +}; + +struct _PurpleHTTPConnection { + PurpleBOSHConnection *bosh; + PurpleSslConnection *psc; + int fd; + guint readh; + guint writeh; + + PurpleCircBuffer *write_buffer; + + gboolean ready; + int requests; /* number of outstanding HTTP requests */ + + GString *buf; + gboolean headers_done; + gsize handled_len; + gsize body_len; + +}; + +static void http_connection_connect(PurpleHTTPConnection *conn); +static void http_connection_send_request(PurpleHTTPConnection *conn, + const GString *req); + +void jabber_bosh_init(void) +{ + GHashTable *ui_info = purple_core_get_ui_info(); + const char *ui_name = NULL; + const char *ui_version = NULL; + + if (ui_info) { + ui_name = g_hash_table_lookup(ui_info, "name"); + ui_version = g_hash_table_lookup(ui_info, "version"); + } + + if (ui_name) + bosh_useragent = g_strdup_printf("%s%s%s (libpurple " VERSION ")", + ui_name, ui_version ? " " : "", + ui_version ? ui_version : ""); + else + bosh_useragent = g_strdup("libpurple " VERSION); +} + +void jabber_bosh_uninit(void) +{ + g_free(bosh_useragent); + bosh_useragent = NULL; +} + +static PurpleHTTPConnection* +jabber_bosh_http_connection_init(PurpleBOSHConnection *bosh) +{ + PurpleHTTPConnection *conn = g_new0(PurpleHTTPConnection, 1); + conn->bosh = bosh; + conn->fd = -1; + conn->ready = FALSE; + + conn->write_buffer = purple_circ_buffer_new(0 /* default grow size */); + + return conn; +} + +static void +jabber_bosh_http_connection_destroy(PurpleHTTPConnection *conn) +{ + if (conn->buf) + g_string_free(conn->buf, TRUE); + + if (conn->write_buffer) + purple_circ_buffer_destroy(conn->write_buffer); + if (conn->readh) + purple_input_remove(conn->readh); + if (conn->writeh) + purple_input_remove(conn->writeh); + if (conn->psc) + purple_ssl_close(conn->psc); + if (conn->fd >= 0) + close(conn->fd); + + purple_proxy_connect_cancel_with_handle(conn); + + g_free(conn); +} + +PurpleBOSHConnection* +jabber_bosh_connection_init(JabberStream *js, const char *url) +{ + PurpleBOSHConnection *conn; + char *host, *path, *user, *passwd; + int port; + + if (!purple_url_parse(url, &host, &port, &path, &user, &passwd)) { + purple_debug_info("jabber", "Unable to parse given URL.\n"); + return NULL; + } + + conn = g_new0(PurpleBOSHConnection, 1); + conn->host = host; + conn->port = port; + conn->path = g_strdup_printf("/%s", path); + g_free(path); + conn->pipelining = TRUE; + + if ((user && user[0] != '\0') || (passwd && passwd[0] != '\0')) { + purple_debug_info("jabber", "Ignoring unexpected username and password " + "in BOSH URL.\n"); + } + + g_free(user); + g_free(passwd); + + conn->js = js; + + /* + * Random 64-bit integer masked off by 2^52 - 1. + * + * This should produce a random integer in the range [0, 2^52). It's + * unlikely we'll send enough packets in one session to overflow the rid. + */ + conn->rid = ((guint64)g_random_int() << 32) | g_random_int(); + conn->rid &= 0xFFFFFFFFFFFFFLL; + + conn->pending = purple_circ_buffer_new(0 /* default grow size */); + + conn->ready = FALSE; + if (purple_strcasestr(url, "https://") != NULL) + conn->ssl = TRUE; + else + conn->ssl = FALSE; + + conn->connections[0] = jabber_bosh_http_connection_init(conn); + + return conn; +} + +void +jabber_bosh_connection_destroy(PurpleBOSHConnection *conn) +{ + int i; + + g_free(conn->host); + g_free(conn->path); + + if (conn->inactivity_timer) + purple_timeout_remove(conn->inactivity_timer); + + purple_circ_buffer_destroy(conn->pending); + + for (i = 0; i < MAX_HTTP_CONNECTIONS; ++i) { + if (conn->connections[i]) + jabber_bosh_http_connection_destroy(conn->connections[i]); + } + + g_free(conn); +} + +gboolean jabber_bosh_connection_is_ssl(PurpleBOSHConnection *conn) +{ + return conn->ssl; +} + +static PurpleHTTPConnection * +find_available_http_connection(PurpleBOSHConnection *conn) +{ + int i; + + /* Easy solution: Does everyone involved support pipelining? Hooray! Just use + * one TCP connection! */ + if (conn->pipelining) + return conn->connections[0]; + + /* First loop, look for a connection that's ready */ + for (i = 0; i < MAX_HTTP_CONNECTIONS; ++i) { + if (conn->connections[i] && conn->connections[i]->ready && + conn->connections[i]->requests == 0) + return conn->connections[i]; + } + + /* Second loop, look for one that's NULL and create a new connection */ + for (i = 0; i < MAX_HTTP_CONNECTIONS; ++i) { + if (!conn->connections[i]) { + conn->connections[i] = jabber_bosh_http_connection_init(conn); + + http_connection_connect(conn->connections[i]); + return NULL; + } + } + + /* None available. */ + return NULL; +} + +static void +jabber_bosh_connection_send(PurpleBOSHConnection *conn, PurpleBOSHPacketType type, + const char *data) +{ + PurpleHTTPConnection *chosen; + GString *packet = NULL; + + chosen = find_available_http_connection(conn); + + if (type != PACKET_NORMAL && !chosen) { + /* + * For non-ordinary traffic, we don't want to 'buffer' it, so use the first + * connection. + */ + chosen = conn->connections[0]; + + if (!chosen->ready) + purple_debug_warning("jabber", "First BOSH connection wasn't ready. Bad " + "things may happen.\n"); + } + + if (type == PACKET_NORMAL && (!chosen || + (conn->max_requests > 0 && conn->requests == conn->max_requests))) { + /* + * For normal data, send up to max_requests requests at a time or there is no + * connection ready (likely, we're currently opening a second connection and + * will send these packets when connected). + */ + if (data) { + int len = data ? strlen(data) : 0; + purple_circ_buffer_append(conn->pending, data, len); + } + return; + } + + packet = g_string_new(""); + + g_string_printf(packet, "<body " + "rid='%" G_GUINT64_FORMAT "' " + "sid='%s' " + "to='%s' " + "xml:lang='en' " + "xmlns='http://jabber.org/protocol/httpbind' " + "xmlns:xmpp='urn:xmpp:xbosh'", + ++conn->rid, + conn->sid, + conn->js->user->domain); + + if (type == PACKET_STREAM_RESTART) + packet = g_string_append(packet, " xmpp:restart='true'/>"); + else { + gsize read_amt; + if (type == PACKET_TERMINATE) + packet = g_string_append(packet, " type='terminate'"); + + packet = g_string_append_c(packet, '>'); + + while ((read_amt = purple_circ_buffer_get_max_read(conn->pending)) > 0) { + packet = g_string_append_len(packet, conn->pending->outptr, read_amt); + purple_circ_buffer_mark_read(conn->pending, read_amt); + } + + if (data) + packet = g_string_append(packet, data); + packet = g_string_append(packet, "</body>"); + } + + http_connection_send_request(chosen, packet); +} + +void jabber_bosh_connection_close(PurpleBOSHConnection *conn) +{ + jabber_bosh_connection_send(conn, PACKET_TERMINATE, NULL); +} + +static void jabber_bosh_connection_stream_restart(PurpleBOSHConnection *conn) { + jabber_bosh_connection_send(conn, PACKET_STREAM_RESTART, NULL); +} + +static gboolean jabber_bosh_connection_error_check(PurpleBOSHConnection *conn, xmlnode *node) { + const char *type; + + type = xmlnode_get_attrib(node, "type"); + + if (type != NULL && !strcmp(type, "terminate")) { + conn->ready = FALSE; + purple_connection_error_reason (conn->js->gc, + PURPLE_CONNECTION_ERROR_OTHER_ERROR, + _("The BOSH connection manager terminated your session.")); + return TRUE; + } + return FALSE; +} + +static gboolean +bosh_inactivity_cb(gpointer data) +{ + PurpleBOSHConnection *bosh = data; + + jabber_bosh_connection_send(bosh, PACKET_NORMAL, NULL); + return TRUE; +} + +static void jabber_bosh_connection_received(PurpleBOSHConnection *conn, xmlnode *node) { + xmlnode *child; + JabberStream *js = conn->js; + + g_return_if_fail(node != NULL); + if (jabber_bosh_connection_error_check(conn, node)) + return; + + child = node->child; + while (child != NULL) { + /* jabber_process_packet might free child */ + xmlnode *next = child->next; + if (child->type == XMLNODE_TYPE_TAG) { + if (!strcmp(child->name, "iq")) { + if (xmlnode_get_child(child, "session")) + conn->ready = TRUE; + } + + jabber_process_packet(js, &child); + } + + child = next; + } +} + +static void auth_response_cb(PurpleBOSHConnection *conn, xmlnode *node) { + xmlnode *child; + + g_return_if_fail(node != NULL); + if (jabber_bosh_connection_error_check(conn, node)) + return; + + child = node->child; + while(child != NULL && child->type != XMLNODE_TYPE_TAG) { + child = child->next; + } + + /* We're only expecting one XML node here, so only process the first one */ + if (child != NULL && child->type == XMLNODE_TYPE_TAG) { + JabberStream *js = conn->js; + if (!strcmp(child->name, "success")) { + jabber_bosh_connection_stream_restart(conn); + jabber_process_packet(js, &child); + conn->receive_cb = jabber_bosh_connection_received; + } else { + js->state = JABBER_STREAM_AUTHENTICATING; + jabber_process_packet(js, &child); + } + } else { + purple_debug_warning("jabber", "Received unexepcted empty BOSH packet.\n"); + } +} + +static void boot_response_cb(PurpleBOSHConnection *conn, xmlnode *node) { + const char *sid, *version; + const char *inactivity, *requests; + xmlnode *packet; + + g_return_if_fail(node != NULL); + if (jabber_bosh_connection_error_check(conn, node)) + return; + + sid = xmlnode_get_attrib(node, "sid"); + version = xmlnode_get_attrib(node, "ver"); + + inactivity = xmlnode_get_attrib(node, "inactivity"); + requests = xmlnode_get_attrib(node, "requests"); + + if (sid) { + conn->sid = g_strdup(sid); + } else { + purple_connection_error_reason(conn->js->gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("No session ID given")); + return; + } + + if (version) { + const char *dot = strstr(version, "."); + int major = atoi(version); + int minor = atoi(dot + 1); + + purple_debug_info("jabber", "BOSH connection manager version %s\n", version); + + if (major != 1 || minor < 6) { + purple_connection_error_reason(conn->js->gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Unsupported version of BOSH protocol")); + return; + } + } else { + purple_debug_info("jabber", "Missing version in BOSH initiation\n"); + } + + if (inactivity) { + conn->max_inactivity = atoi(inactivity); + if (conn->max_inactivity <= 2) { + purple_debug_warning("jabber", "Ignoring bogusly small inactivity: %s\n", + inactivity); + conn->max_inactivity = 0; + } else { + /* TODO: Integrate this with jabber.c keepalive checks... */ + conn->inactivity_timer = purple_timeout_add_seconds( + conn->max_inactivity - 2 /* rounding */, bosh_inactivity_cb, + conn); + } + } + + if (requests) + conn->max_requests = atoi(requests); + + /* FIXME: Depending on receiving features might break with some hosts */ + packet = xmlnode_get_child(node, "features"); + conn->js->use_bosh = TRUE; + conn->receive_cb = auth_response_cb; + jabber_stream_features_parse(conn->js, packet); +} + +static void jabber_bosh_connection_boot(PurpleBOSHConnection *conn) { + GString *buf = g_string_new(""); + + g_string_printf(buf, "<body content='text/xml; charset=utf-8' " + "secure='true' " + "to='%s' " + "xml:lang='en' " + "xmpp:version='1.0' " + "ver='1.6' " + "xmlns:xmpp='urn:xmpp:bosh' " + "rid='%" G_GUINT64_FORMAT "' " +/* TODO: This should be adjusted/adjustable automatically according to + * realtime network behavior */ + "wait='60' " + "hold='1' " + "xmlns='http://jabber.org/protocol/httpbind'/>", + conn->js->user->domain, + ++conn->rid); + + conn->receive_cb = boot_response_cb; + http_connection_send_request(conn->connections[0], buf); + g_string_free(buf, TRUE); +} + +static void +http_received_cb(const char *data, int len, PurpleBOSHConnection *conn) +{ + if (conn->failed_connections) + /* We've got some data, so reset the number of failed connections */ + conn->failed_connections = 0; + + if (conn->receive_cb) { + xmlnode *node = xmlnode_from_str(data, len); + + purple_debug_info("jabber", "RecvBOSH %s(%d): %s\n", + conn->ssl ? "(ssl)" : "", len, data); + + if (node) { + conn->receive_cb(conn, node); + xmlnode_free(node); + } else { + purple_debug_warning("jabber", "BOSH: Received invalid XML\n"); + } + } else { + g_return_if_reached(); + } +} + +void jabber_bosh_connection_send_raw(PurpleBOSHConnection *conn, + const char *data) +{ + jabber_bosh_connection_send(conn, PACKET_NORMAL, data); +} + +static void +connection_common_established_cb(PurpleHTTPConnection *conn) +{ + /* Indicate we're ready and reset some variables */ + conn->ready = TRUE; + conn->requests = 0; + if (conn->buf) { + g_string_free(conn->buf, TRUE); + conn->buf = NULL; + } + conn->headers_done = FALSE; + conn->handled_len = conn->body_len = 0; + + if (conn->bosh->ready) { + purple_debug_info("jabber", "BOSH session already exists. Trying to reuse it.\n"); + if (conn->bosh->pending->bufused > 0) { + /* Send the pending data */ + jabber_bosh_connection_send(conn->bosh, PACKET_NORMAL, NULL); + } +#if 0 + conn->bosh->receive_cb = jabber_bosh_connection_received; + if (conn->bosh->connect_cb) + conn->bosh->connect_cb(conn->bosh); +#endif + } else + jabber_bosh_connection_boot(conn->bosh); +} + +void jabber_bosh_connection_refresh(PurpleBOSHConnection *conn) +{ + jabber_bosh_connection_send(conn, PACKET_NORMAL, NULL); +} + +static void http_connection_disconnected(PurpleHTTPConnection *conn) +{ + /* + * Well, then. Fine! I never liked you anyway, server! I was cheating on you + * with AIM! + */ + conn->ready = FALSE; + if (conn->psc) { + purple_ssl_close(conn->psc); + conn->psc = NULL; + } else if (conn->fd >= 0) { + close(conn->fd); + conn->fd = -1; + } + + if (conn->readh) { + purple_input_remove(conn->readh); + conn->readh = 0; + } + + if (conn->writeh) { + purple_input_remove(conn->writeh); + conn->writeh = 0; + } + + if (conn->bosh->pipelining) + /* Hmmmm, fall back to multiple connections */ + conn->bosh->pipelining = FALSE; + + if (++conn->bosh->failed_connections == MAX_FAILED_CONNECTIONS) { + purple_connection_error_reason(conn->bosh->js->gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Unable to establish a connection with the server")); + } else { + /* No! Please! Take me back. It was me, not you! I was weak! */ + http_connection_connect(conn); + } +} + +void jabber_bosh_connection_connect(PurpleBOSHConnection *bosh) { + PurpleHTTPConnection *conn = bosh->connections[0]; + http_connection_connect(conn); +} + +static void +jabber_bosh_http_connection_process(PurpleHTTPConnection *conn) +{ + const char *cursor; + + cursor = conn->buf->str + conn->handled_len; + + if (!conn->headers_done) { + const char *content_length = purple_strcasestr(cursor, "\r\nContent-Length"); + const char *end_of_headers = purple_strcasestr(cursor, "\r\n\r\n"); + + /* Make sure Content-Length is in headers, not body */ + if (content_length && content_length < end_of_headers) { + char *sep = strstr(content_length, ": "); + int len = atoi(sep + 2); + if (len == 0) + purple_debug_warning("jabber", "Found mangled Content-Length header.\n"); + + conn->body_len = len; + } + + if (end_of_headers) { + conn->headers_done = TRUE; + conn->handled_len = end_of_headers - conn->buf->str + 4; + cursor = end_of_headers + 4; + } else { + conn->handled_len = conn->buf->len; + return; + } + } + + /* Have we handled everything in the buffer? */ + if (conn->handled_len >= conn->buf->len) + return; + + /* Have we read all that the Content-Length promised us? */ + if (conn->buf->len - conn->handled_len < conn->body_len) + return; + + --conn->requests; + --conn->bosh->requests; + + http_received_cb(conn->buf->str + conn->handled_len, conn->body_len, + conn->bosh); + + if (conn->bosh->ready && + (conn->bosh->requests == 0 || conn->bosh->pending->bufused > 0)) { + jabber_bosh_connection_send(conn->bosh, PACKET_NORMAL, NULL); + purple_debug_misc("jabber", "BOSH: Sending an empty request\n"); + } + + g_string_free(conn->buf, TRUE); + conn->buf = NULL; + conn->headers_done = FALSE; + conn->handled_len = conn->body_len = 0; +} + +/* + * Common code for reading, called from http_connection_read_cb_ssl and + * http_connection_read_cb. + */ +static void +http_connection_read(PurpleHTTPConnection *conn) +{ + char buffer[1025]; + int cnt, count = 0; + + if (!conn->buf) + conn->buf = g_string_new(""); + + /* Read once to prime cnt before the loop */ + if (conn->psc) + cnt = purple_ssl_read(conn->psc, buffer, sizeof(buffer)); + else + cnt = read(conn->fd, buffer, sizeof(buffer)); + while (cnt > 0) { + count += cnt; + g_string_append_len(conn->buf, buffer, cnt); + + if (conn->psc) + cnt = purple_ssl_read(conn->psc, buffer, sizeof(buffer)); + else + cnt = read(conn->fd, buffer, sizeof(buffer)); + } + + if (cnt == 0 || (cnt < 0 && errno != EAGAIN)) { + if (cnt < 0) + purple_debug_info("jabber", "bosh read=%d, errno=%d\n", cnt, errno); + else + purple_debug_info("jabber", "bosh server closed the connection\n"); + + /* + * If the socket is closed, the processing really needs to know about + * it. Handle that now. + */ + http_connection_disconnected(conn); + + /* Process what we do have */ + } + + + jabber_bosh_http_connection_process(conn); +} + +static void +http_connection_read_cb(gpointer data, gint fd, PurpleInputCondition condition) +{ + PurpleHTTPConnection *conn = data; + + http_connection_read(conn); +} + +static void +http_connection_read_cb_ssl(gpointer data, PurpleSslConnection *psc, + PurpleInputCondition cond) +{ + PurpleHTTPConnection *conn = data; + + http_connection_read(conn); +} + +static void +ssl_connection_established_cb(gpointer data, PurpleSslConnection *psc, + PurpleInputCondition cond) +{ + PurpleHTTPConnection *conn = data; + + purple_ssl_input_add(psc, http_connection_read_cb_ssl, conn); + connection_common_established_cb(conn); +} + +static void +ssl_connection_error_cb(PurpleSslConnection *gsc, PurpleSslErrorType error, + gpointer data) +{ + PurpleHTTPConnection *conn = data; + + /* sslconn frees the connection on error */ + conn->psc = NULL; + + purple_connection_ssl_error(conn->bosh->js->gc, error); +} + +static void +connection_established_cb(gpointer data, gint source, const gchar *error) +{ + PurpleHTTPConnection *conn = data; + PurpleConnection *gc = conn->bosh->js->gc; + + if (source < 0) { + gchar *tmp; + tmp = g_strdup_printf(_("Could not establish a connection with the server:\n%s"), + error); + purple_connection_error_reason(gc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp); + g_free(tmp); + return; + } + + conn->fd = source; + conn->readh = purple_input_add(conn->fd, PURPLE_INPUT_READ, + http_connection_read_cb, conn); + connection_common_established_cb(conn); +} + +static void http_connection_connect(PurpleHTTPConnection *conn) +{ + PurpleBOSHConnection *bosh = conn->bosh; + PurpleConnection *gc = bosh->js->gc; + PurpleAccount *account = purple_connection_get_account(gc); + + if (bosh->ssl) { + if (purple_ssl_is_supported()) { + conn->psc = purple_ssl_connect(account, bosh->host, bosh->port, + ssl_connection_established_cb, + ssl_connection_error_cb, + conn); + if (!conn->psc) { + purple_connection_error_reason(gc, + PURPLE_CONNECTION_ERROR_NO_SSL_SUPPORT, + _("Unable to establish SSL connection")); + } + } else { + purple_connection_error_reason(gc, + PURPLE_CONNECTION_ERROR_NO_SSL_SUPPORT, + _("SSL support unavailable")); + } + } else if (purple_proxy_connect(conn, account, bosh->host, bosh->port, + connection_established_cb, conn) == NULL) { + purple_connection_error_reason(gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Unable to create socket")); + } +} + +static int +http_connection_do_send(PurpleHTTPConnection *conn, const char *data, int len) +{ + int ret; + + if (conn->psc) + ret = purple_ssl_write(conn->psc, data, len); + else + ret = write(conn->fd, data, len); + + return ret; +} + +static void +http_connection_send_cb(gpointer data, gint source, PurpleInputCondition cond) +{ + PurpleHTTPConnection *conn = data; + int ret; + int writelen = purple_circ_buffer_get_max_read(conn->write_buffer); + + if (writelen == 0) { + purple_input_remove(conn->writeh); + conn->writeh = 0; + return; + } + + ret = http_connection_do_send(conn, conn->write_buffer->outptr, writelen); + + if (ret < 0 && errno == EAGAIN) + return; + else if (ret <= 0) { + /* + * TODO: Handle this better. Probably requires a PurpleBOSHConnection + * buffer that stores what is "being sent" until the + * PurpleHTTPConnection reports it is fully sent. + */ + purple_connection_error_reason(conn->bosh->js->gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Write error")); + return; + } + + purple_circ_buffer_mark_read(conn->write_buffer, ret); +} + +static void +http_connection_send_request(PurpleHTTPConnection *conn, const GString *req) +{ + char *data; + int ret; + size_t len; + + data = g_strdup_printf("POST %s HTTP/1.1\r\n" + "Host: %s\r\n" + "User-Agent: %s\r\n" + "Content-Encoding: text/xml; charset=utf-8\r\n" + "Content-Length: %" G_GSIZE_FORMAT "\r\n\r\n" + "%s", + conn->bosh->path, conn->bosh->host, bosh_useragent, + req->len, req->str); + + len = strlen(data); + + ++conn->requests; + ++conn->bosh->requests; + + if (conn->writeh == 0) + ret = http_connection_do_send(conn, data, len); + else { + ret = -1; + errno = EAGAIN; + } + + if (ret < 0 && errno != EAGAIN) { + /* + * TODO: Handle this better. Probably requires a PurpleBOSHConnection + * buffer that stores what is "being sent" until the + * PurpleHTTPConnection reports it is fully sent. + */ + purple_connection_error_reason(conn->bosh->js->gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Write error")); + return; + } else if (ret < len) { + if (ret < 0) + ret = 0; + if (conn->writeh == 0) + conn->writeh = purple_input_add(conn->psc ? conn->psc->fd : conn->fd, + PURPLE_INPUT_WRITE, http_connection_send_cb, conn); + purple_circ_buffer_append(conn->write_buffer, data + ret, len - ret); + } +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/protocols/jabber/bosh.h Wed Apr 29 23:20:51 2009 +0000 @@ -0,0 +1,41 @@ +/** + * @file bosh.h Bidirectional-streams over Synchronous HTTP (BOSH) (XEP-0124 and XEP-0206) + * + * purple + * + * Copyright (C) 2008, Tobias Markmann <tmarkmann@googlemail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ +#ifndef PURPLE_JABBER_BOSH_H_ +#define PURPLE_JABBER_BOSH_H_ + +typedef struct _PurpleBOSHConnection PurpleBOSHConnection; + +#include "jabber.h" + +void jabber_bosh_init(void); +void jabber_bosh_uninit(void); + +PurpleBOSHConnection* jabber_bosh_connection_init(JabberStream *js, const char *url); +void jabber_bosh_connection_destroy(PurpleBOSHConnection *conn); + +gboolean jabber_bosh_connection_is_ssl(PurpleBOSHConnection *conn); + +void jabber_bosh_connection_connect(PurpleBOSHConnection *conn); +void jabber_bosh_connection_close(PurpleBOSHConnection *conn); +void jabber_bosh_connection_send_raw(PurpleBOSHConnection *conn, const char *data); +void jabber_bosh_connection_refresh(PurpleBOSHConnection *conn); +#endif /* PURPLE_JABBER_BOSH_H_ */
--- a/libpurple/protocols/jabber/buddy.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/buddy.c Wed Apr 29 23:20:51 2009 +0000 @@ -32,12 +32,11 @@ #include "jabber.h" #include "iq.h" #include "presence.h" +#include "useravatar.h" #include "xdata.h" #include "pep.h" #include "adhoccommands.h" -#define MAX_HTTP_BUDDYICON_BYTES (200 * 1024) - typedef struct { long idle_seconds; } JabberBuddyInfoResource; @@ -98,36 +97,41 @@ for(l = jb->resources; l; l = l->next) { - if(!jbr && !resource) { - jbr = l->data; - } else if(!resource) { - if(((JabberBuddyResource *)l->data)->priority > jbr->priority) - jbr = l->data; - else if(((JabberBuddyResource *)l->data)->priority == jbr->priority) { + JabberBuddyResource *tmp = (JabberBuddyResource *) l->data; + if (!jbr && !resource) { + jbr = tmp; + } else if (!resource) { + if (tmp->priority > jbr->priority) + jbr = tmp; + else if (tmp->priority == jbr->priority) { /* Determine if this resource is more available than the one we've currently chosen */ - switch(((JabberBuddyResource *)l->data)->state) { + switch(tmp->state) { case JABBER_BUDDY_STATE_ONLINE: case JABBER_BUDDY_STATE_CHAT: /* This resource is online/chatty. Prefer to one which isn't either. */ - if ((jbr->state != JABBER_BUDDY_STATE_ONLINE) && (jbr->state != JABBER_BUDDY_STATE_CHAT)) - jbr = l->data; + if (((jbr->state != JABBER_BUDDY_STATE_ONLINE) && (jbr->state != JABBER_BUDDY_STATE_CHAT)) + || (jbr->idle && !tmp->idle) + || (jbr->idle && tmp->idle && tmp->idle > jbr->idle)) + jbr = tmp; break; case JABBER_BUDDY_STATE_AWAY: case JABBER_BUDDY_STATE_DND: /* This resource is away/dnd. Prefer to one which is extended away, unavailable, or unknown. */ - if ((jbr->state == JABBER_BUDDY_STATE_XA) || (jbr->state == JABBER_BUDDY_STATE_UNAVAILABLE) || + if (((jbr->state == JABBER_BUDDY_STATE_XA) || (jbr->state == JABBER_BUDDY_STATE_UNAVAILABLE) || (jbr->state == JABBER_BUDDY_STATE_UNKNOWN) || (jbr->state == JABBER_BUDDY_STATE_ERROR)) - jbr = l->data; + || (jbr->idle && !tmp->idle) + || (jbr->idle && tmp->idle && tmp->idle > jbr->idle)) + jbr = tmp; break; case JABBER_BUDDY_STATE_XA: /* This resource is extended away. That's better than unavailable or unknown. */ if ((jbr->state == JABBER_BUDDY_STATE_UNAVAILABLE) || (jbr->state == JABBER_BUDDY_STATE_UNKNOWN) || (jbr->state == JABBER_BUDDY_STATE_ERROR)) - jbr = l->data; + jbr = tmp; break; case JABBER_BUDDY_STATE_UNAVAILABLE: /* This resource is unavailable. That's better than unknown. */ if ((jbr->state == JABBER_BUDDY_STATE_UNKNOWN) || (jbr->state == JABBER_BUDDY_STATE_ERROR)) - jbr = l->data; + jbr = tmp; break; case JABBER_BUDDY_STATE_UNKNOWN: case JABBER_BUDDY_STATE_ERROR: @@ -135,9 +139,9 @@ break; } } - } else if(((JabberBuddyResource *)l->data)->name) { - if(!strcmp(((JabberBuddyResource *)l->data)->name, resource)) { - jbr = l->data; + } else if(tmp->name) { + if(!strcmp(tmp->name, resource)) { + jbr = tmp; break; } } @@ -181,8 +185,10 @@ jbr->commands = g_list_delete_link(jbr->commands, jbr->commands); } - jabber_caps_free_clientinfo(jbr->caps); - + if (jbr->caps.exts) { + g_list_foreach(jbr->caps.exts, (GFunc)g_free, NULL); + g_list_free(jbr->caps.exts); + } g_free(jbr->name); g_free(jbr->status); g_free(jbr->thread_id); @@ -202,21 +208,6 @@ jabber_buddy_resource_free(jbr); } -const char *jabber_buddy_get_status_msg(JabberBuddy *jb) -{ - JabberBuddyResource *jbr; - - if(!jb) - return NULL; - - jbr = jabber_buddy_find_resource(jb, NULL); - - if(!jbr) - return NULL; - - return jbr->status; -} - /******* * This is the old vCard stuff taken from the old prpl. vCards, by definition * are a temporary thing until jabber can get its act together and come up @@ -485,134 +476,25 @@ iq = jabber_iq_new(js, JABBER_IQ_SET); xmlnode_insert_child(iq->node, vc_node); jabber_iq_send(iq); + + /* Send presence to update vcard-temp:x:update */ + jabber_presence_send(js, FALSE); } } void jabber_set_buddy_icon(PurpleConnection *gc, PurpleStoredImage *img) { - PurplePresence *gpresence; - PurpleStatus *status; - - if(((JabberStream*)purple_connection_get_protocol_data(gc))->pep) { - /* XEP-0084: User Avatars */ - if(img) { - /* - * TODO: This is pretty gross. The Jabber PRPL really shouldn't - * do voodoo to try to determine the image type, height - * and width. - */ - /* A PNG header, including the IHDR, but nothing else */ - const struct { - guchar signature[8]; /* must be hex 89 50 4E 47 0D 0A 1A 0A */ - struct { - guint32 length; /* must be 0x0d */ - guchar type[4]; /* must be 'I' 'H' 'D' 'R' */ - guint32 width; - guint32 height; - guchar bitdepth; - guchar colortype; - guchar compression; - guchar filter; - guchar interlace; - } ihdr; - } *png = purple_imgstore_get_data(img); /* ATTN: this is in network byte order! */ - - /* check if the data is a valid png file (well, at least to some extend) */ - if(png->signature[0] == 0x89 && - png->signature[1] == 0x50 && - png->signature[2] == 0x4e && - png->signature[3] == 0x47 && - png->signature[4] == 0x0d && - png->signature[5] == 0x0a && - png->signature[6] == 0x1a && - png->signature[7] == 0x0a && - ntohl(png->ihdr.length) == 0x0d && - png->ihdr.type[0] == 'I' && - png->ihdr.type[1] == 'H' && - png->ihdr.type[2] == 'D' && - png->ihdr.type[3] == 'R') { - /* parse PNG header to get the size of the image (yes, this is required) */ - guint32 width = ntohl(png->ihdr.width); - guint32 height = ntohl(png->ihdr.height); - xmlnode *publish, *item, *data, *metadata, *info; - char *lengthstring, *widthstring, *heightstring; - - /* compute the sha1 hash */ - char *hash = jabber_calculate_data_sha1sum(purple_imgstore_get_data(img), purple_imgstore_get_size(img)); - char *base64avatar; - - publish = xmlnode_new("publish"); - xmlnode_set_attrib(publish,"node",AVATARNAMESPACEDATA); - - item = xmlnode_new_child(publish, "item"); - xmlnode_set_attrib(item, "id", hash); - - data = xmlnode_new_child(item, "data"); - xmlnode_set_namespace(data,AVATARNAMESPACEDATA); + PurpleAccount *account = purple_connection_get_account(gc); - base64avatar = purple_base64_encode(purple_imgstore_get_data(img), purple_imgstore_get_size(img)); - xmlnode_insert_data(data,base64avatar,-1); - g_free(base64avatar); - - /* publish the avatar itself */ - jabber_pep_publish((JabberStream*)purple_connection_get_protocol_data(gc), publish); - - /* next step: publish the metadata */ - publish = xmlnode_new("publish"); - xmlnode_set_attrib(publish,"node",AVATARNAMESPACEMETA); - - item = xmlnode_new_child(publish, "item"); - xmlnode_set_attrib(item, "id", hash); - - metadata = xmlnode_new_child(item, "metadata"); - xmlnode_set_namespace(metadata,AVATARNAMESPACEMETA); - - info = xmlnode_new_child(metadata, "info"); - xmlnode_set_attrib(info, "id", hash); - xmlnode_set_attrib(info, "type", "image/png"); - lengthstring = g_strdup_printf("%u", (unsigned)purple_imgstore_get_size(img)); - xmlnode_set_attrib(info, "bytes", lengthstring); - g_free(lengthstring); - widthstring = g_strdup_printf("%u", width); - xmlnode_set_attrib(info, "width", widthstring); - g_free(widthstring); - heightstring = g_strdup_printf("%u", height); - xmlnode_set_attrib(info, "height", heightstring); - g_free(heightstring); + /* Publish the avatar as specified in XEP-0084 */ + jabber_avatar_set(gc->proto_data, img); + /* Set the image in our vCard */ + jabber_set_info(gc, purple_account_get_user_info(account)); - /* publish the metadata */ - jabber_pep_publish((JabberStream*)purple_connection_get_protocol_data(gc), publish); - - g_free(hash); - } else { - purple_debug_error("jabber", "jabber_set_buddy_icon received non-png data"); - } - } else { - /* remove the metadata */ - xmlnode *metadata, *item; - xmlnode *publish = xmlnode_new("publish"); - xmlnode_set_attrib(publish,"node",AVATARNAMESPACEMETA); - - item = xmlnode_new_child(publish, "item"); - - metadata = xmlnode_new_child(item, "metadata"); - xmlnode_set_namespace(metadata,AVATARNAMESPACEMETA); - - xmlnode_new_child(metadata, "stop"); - - /* publish the metadata */ - jabber_pep_publish((JabberStream*)gc->proto_data, publish); - } - } - - /* vCard avatars do not have an image type requirement so update our - * vCard avatar regardless of image type for those poor older clients - */ - jabber_set_info(gc, purple_account_get_user_info(gc->account)); - - gpresence = purple_account_get_presence(gc->account); - status = purple_presence_get_active_status(gpresence); - jabber_presence_send(gc->account, status); + /* TODO: Fake image to ourselves, since a number of servers do not echo + * back our presence to us. To do this without uselessly copying the data + * of the image, we need purple_buddy_icons_set_for_user_image (i.e. takes + * an existing icon/stored image). */ } /* @@ -1187,9 +1069,8 @@ JabberIqType type, const char *id, xmlnode *packet, gpointer data) { - xmlnode *vcard; - char *txt; - PurpleStoredImage *img; + xmlnode *vcard, *photo, *binval; + char *txt, *vcard_hash = NULL; if (type == JABBER_IQ_ERROR) { purple_debug_warning("jabber", "Server returned error while retrieving vCard"); @@ -1209,10 +1090,29 @@ js->vcard_fetched = TRUE; - if(NULL != (img = purple_buddy_icons_find_account_icon(js->gc->account))) { - jabber_set_buddy_icon(js->gc, img); - purple_imgstore_unref(img); + if (vcard && (photo = xmlnode_get_child(vcard, "PHOTO")) && + (binval = xmlnode_get_child(photo, "BINVAL"))) { + gsize size; + char *bintext = xmlnode_get_data(binval); + guchar *data = purple_base64_decode(bintext, &size); + g_free(bintext); + + if (data) { + vcard_hash = jabber_calculate_data_sha1sum(data, size); + g_free(data); + } } + + /* Republish our vcard if the photo is different than the server's */ + if (!purple_strequal(vcard_hash, js->initial_avatar_hash)) { + PurpleAccount *account = purple_connection_get_account(js->gc); + jabber_set_info(js->gc, purple_account_get_user_info(account)); + } else if (js->initial_avatar_hash) { + /* Our photo is in the vcard, so advertise vcard-temp updates */ + js->avatar_hash = g_strdup(js->initial_avatar_hash); + } + + g_free(vcard_hash); } void jabber_vcard_fetch_mine(JabberStream *js) @@ -1460,127 +1360,6 @@ jabber_buddy_info_show_if_ready(jbi); } -typedef struct _JabberBuddyAvatarUpdateURLInfo { - JabberStream *js; - char *from; - char *id; -} JabberBuddyAvatarUpdateURLInfo; - -static void do_buddy_avatar_update_fromurl(PurpleUtilFetchUrlData *url_data, gpointer user_data, const gchar *url_text, gsize len, const gchar *error_message) { - JabberBuddyAvatarUpdateURLInfo *info = user_data; - if(!url_text) { - purple_debug(PURPLE_DEBUG_ERROR, "jabber", - "do_buddy_avatar_update_fromurl got error \"%s\"", error_message); - return; - } - - purple_buddy_icons_set_for_user(purple_connection_get_account(info->js->gc), info->from, (void*)url_text, len, info->id); - g_free(info->from); - g_free(info->id); - g_free(info); -} - -static void do_buddy_avatar_update_data(JabberStream *js, const char *from, xmlnode *items) { - xmlnode *item, *data; - const char *checksum; - char *b64data; - void *img; - size_t size; - if(!items) - return; - - item = xmlnode_get_child(items, "item"); - if(!item) - return; - - data = xmlnode_get_child_with_namespace(item,"data",AVATARNAMESPACEDATA); - if(!data) - return; - - checksum = xmlnode_get_attrib(item,"id"); - if(!checksum) - return; - - b64data = xmlnode_get_data(data); - if(!b64data) - return; - - img = purple_base64_decode(b64data, &size); - if(!img) { - g_free(b64data); - return; - } - - purple_buddy_icons_set_for_user(purple_connection_get_account(js->gc), from, img, size, checksum); - g_free(b64data); -} - -void jabber_buddy_avatar_update_metadata(JabberStream *js, const char *from, xmlnode *items) { - PurpleBuddy *buddy = purple_find_buddy(purple_connection_get_account(js->gc), from); - const char *checksum; - xmlnode *item, *metadata; - if(!buddy) - return; - - checksum = purple_buddy_icons_get_checksum_for_user(buddy); - item = xmlnode_get_child(items,"item"); - metadata = xmlnode_get_child_with_namespace(item, "metadata", AVATARNAMESPACEMETA); - if(!metadata) - return; - /* check if we have received a stop */ - if(xmlnode_get_child(metadata, "stop")) { - purple_buddy_icons_set_for_user(purple_connection_get_account(js->gc), from, NULL, 0, NULL); - } else { - xmlnode *info, *goodinfo = NULL; - gboolean has_children = FALSE; - - /* iterate over all info nodes to get one we can use */ - for(info = metadata->child; info; info = info->next) { - if(info->type == XMLNODE_TYPE_TAG) - has_children = TRUE; - if(info->type == XMLNODE_TYPE_TAG && !strcmp(info->name,"info")) { - const char *type = xmlnode_get_attrib(info,"type"); - const char *id = xmlnode_get_attrib(info,"id"); - - if(checksum && id && !strcmp(id, checksum)) { - /* we already have that avatar, so we don't have to do anything */ - goodinfo = NULL; - break; - } - /* We'll only pick the png one for now. It's a very nice image format anyways. */ - if(type && id && !goodinfo && !strcmp(type, "image/png")) - goodinfo = info; - } - } - if(has_children == FALSE) { - purple_buddy_icons_set_for_user(purple_connection_get_account(js->gc), from, NULL, 0, NULL); - } else if(goodinfo) { - const char *url = xmlnode_get_attrib(goodinfo, "url"); - const char *id = xmlnode_get_attrib(goodinfo,"id"); - - /* the avatar might either be stored in a pep node, or on a HTTP/HTTPS URL */ - if(!url) - jabber_pep_request_item(js, from, AVATARNAMESPACEDATA, id, do_buddy_avatar_update_data); - else { - PurpleUtilFetchUrlData *url_data; - JabberBuddyAvatarUpdateURLInfo *info = g_new0(JabberBuddyAvatarUpdateURLInfo, 1); - info->js = js; - - url_data = purple_util_fetch_url_len(url, TRUE, NULL, TRUE, - MAX_HTTP_BUDDYICON_BYTES, - do_buddy_avatar_update_fromurl, info); - if (url_data) { - info->from = g_strdup(from); - info->id = g_strdup(id); - js->url_datas = g_slist_prepend(js->url_datas, url_data); - } else - g_free(info); - - } - } - } -} - static void jabber_buddy_info_resource_free(gpointer data) { JabberBuddyInfoResource *jbri = data; @@ -1653,12 +1432,49 @@ if(seconds) { char *end = NULL; long sec = strtol(seconds, &end, 10); - if(end != seconds) { + JabberBuddy *jb = NULL; + char *resource = NULL; + char *buddy_name = NULL; + JabberBuddyResource *jbr = NULL; + + if(end != seconds) { JabberBuddyInfoResource *jbir = g_hash_table_lookup(jbi->resources, resource_name); if(jbir) { jbir->idle_seconds = sec; } } + /* Update the idle time of the buddy resource, if we got it. + This will correct the value when a server doesn't mark + delayed presence and we got the presence when signing on */ + jb = jabber_buddy_find(js, from, FALSE); + if (jb) { + resource = jabber_get_resource(from); + buddy_name = jabber_get_bare_jid(from); + /* if the resource already has an idle time set, we + must have gotten it originally from a presence. In + this case we update it. Otherwise don't update it, to + avoid setting an idle and not getting informed about + the resource getting unidle */ + if (resource && buddy_name) { + jbr = jabber_buddy_find_resource(jb, resource); + + if (jbr->idle) { + if (sec) { + jbr->idle = time(NULL) - sec; + } else { + jbr->idle = 0; + } + + if (jbr == + jabber_buddy_find_resource(jb, NULL)) { + purple_prpl_got_user_idle(js->gc->account, + buddy_name, jbr->idle, jbr->idle); + } + } + } + g_free(resource); + g_free(buddy_name); + } } } } @@ -1850,7 +1666,7 @@ } if (jbr->tz_off == PURPLE_NO_TZ_OFF && - (!jbr->caps || + (!jbr->caps.info || jabber_resource_has_capability(jbr, "urn:xmpp:time"))) { xmlnode *child; iq = jabber_iq_new(js, JABBER_IQ_GET); @@ -2589,23 +2405,35 @@ gboolean jabber_resource_has_capability(const JabberBuddyResource *jbr, const gchar *cap) { - const GList *iter = NULL; + const GList *node = NULL; + const JabberCapsNodeExts *exts; - if (!jbr->caps) { + if (!jbr->caps.info) { purple_debug_error("jabber", "Unable to find caps: nothing known about buddy\n"); return FALSE; } - for (iter = jbr->caps->features ; iter ; iter = g_list_next(iter)) { - if (strcmp(iter->data, cap) == 0) { - purple_debug_info("jabber", "Found cap: %s\n", (char *)iter->data); - return TRUE; + node = g_list_find_custom(jbr->caps.info->features, cap, (GCompareFunc)strcmp); + if (!node && jbr->caps.exts && jbr->caps.info->exts) { + const GList *ext; + exts = jbr->caps.info->exts; + /* Walk through all the enabled caps, checking each list for the cap. + * Don't check it twice, though. */ + for (ext = jbr->caps.exts; ext && !node; ext = ext->next) { + GList *features = g_hash_table_lookup(exts->exts, ext->data); + if (features) + node = g_list_find_custom(features, cap, (GCompareFunc)strcmp); } } - purple_debug_info("jabber", "Cap %s not found\n", cap); - return FALSE; + /* TODO: Are these messages actually useful? */ + if (node) + purple_debug_info("jabber", "Found cap: %s\n", cap); + else + purple_debug_info("jabber", "Cap %s not found\n", cap); + + return (node != NULL); } gboolean
--- a/libpurple/protocols/jabber/buddy.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/buddy.h Wed Apr 29 23:20:51 2009 +0000 @@ -36,9 +36,6 @@ #include "jabber.h" #include "caps.h" -#define AVATARNAMESPACEDATA "http://www.xmpp.org/extensions/xep-0084.html#ns-data" -#define AVATARNAMESPACEMETA "http://www.xmpp.org/extensions/xep-0084.html#ns-metadata" - typedef struct _JabberBuddy { GList *resources; char *error_msg; @@ -69,6 +66,7 @@ int priority; JabberBuddyState state; char *status; + time_t idle; JabberCapabilities capabilities; char *thread_id; enum { @@ -83,7 +81,10 @@ } client; /* tz_off == PURPLE_NO_TZ_OFF when unset */ long tz_off; - JabberCapsClientInfo *caps; + struct { + JabberCapsClientInfo *info; + GList *exts; + } caps; GList *commands; } JabberBuddyResource; @@ -96,7 +97,6 @@ int priority, JabberBuddyState state, const char *status); void jabber_buddy_resource_free(JabberBuddyResource *jbr); void jabber_buddy_remove_resource(JabberBuddy *jb, const char *resource); -const char *jabber_buddy_get_status_msg(JabberBuddy *jb); void jabber_buddy_get_info(PurpleConnection *gc, const char *who); GList *jabber_blist_node_menu(PurpleBlistNode *node); @@ -104,7 +104,6 @@ void jabber_set_info(PurpleConnection *gc, const char *info); void jabber_setup_set_info(PurplePluginAction *action); void jabber_set_buddy_icon(PurpleConnection *gc, PurpleStoredImage *img); -void jabber_buddy_avatar_update_metadata(JabberStream *js, const char *from, xmlnode *items); const char *jabber_buddy_state_get_name(JabberBuddyState state); const char *jabber_buddy_state_get_status_id(JabberBuddyState state);
--- a/libpurple/protocols/jabber/caps.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/caps.c Wed Apr 29 23:20:51 2009 +0000 @@ -21,99 +21,221 @@ #include "internal.h" +#include "debug.h" #include "caps.h" -#include <string.h> -#include "internal.h" +#include "cipher.h" +#include "iq.h" +#include "presence.h" #include "util.h" -#include "iq.h" #define JABBER_CAPS_FILENAME "xmpp-caps.xml" -static GHashTable *capstable = NULL; /* JabberCapsKey -> JabberCapsValue */ +typedef struct _JabberDataFormField { + gchar *var; + GList *values; +} JabberDataFormField; typedef struct _JabberCapsKey { char *node; char *ver; + char *hash; } JabberCapsKey; -typedef struct _JabberCapsValueExt { - GList *identities; /* JabberCapsIdentity */ - GList *features; /* char * */ -} JabberCapsValueExt; +static GHashTable *capstable = NULL; /* JabberCapsKey -> JabberCapsClientInfo */ +static GHashTable *nodetable = NULL; /* char *node -> JabberCapsNodeExts */ +static guint save_timer = 0; + +/** + * Processes a query-node and returns a JabberCapsClientInfo object with all relevant info. + * + * @param query A query object. + * @return A JabberCapsClientInfo object. + */ +static JabberCapsClientInfo *jabber_caps_parse_client_info(xmlnode *query); + +/* Free a GList of allocated char* */ +static void +free_string_glist(GList *list) +{ + g_list_foreach(list, (GFunc)g_free, NULL); + g_list_free(list); +} + +static JabberCapsNodeExts* +jabber_caps_node_exts_ref(JabberCapsNodeExts *exts) +{ + g_return_val_if_fail(exts != NULL, NULL); -typedef struct _JabberCapsValue { - GList *identities; /* JabberCapsIdentity */ - GList *features; /* char * */ - GHashTable *ext; /* char * -> JabberCapsValueExt */ -} JabberCapsValue; + ++exts->ref; + return exts; +} + +static void +jabber_caps_node_exts_unref(JabberCapsNodeExts *exts) +{ + if (exts == NULL) + return; + + g_return_if_fail(exts->ref != 0); + + if (--exts->ref != 0) + return; -static guint jabber_caps_hash(gconstpointer key) { - const JabberCapsKey *name = key; - guint nodehash = g_str_hash(name->node); - guint verhash = g_str_hash(name->ver); + g_hash_table_destroy(exts->exts); + g_free(exts); +} - return nodehash ^ verhash; +static guint jabber_caps_hash(gconstpointer data) { + const JabberCapsKey *key = data; + guint nodehash = g_str_hash(key->node); + guint verhash = g_str_hash(key->ver); + /* + * 'hash' was optional in XEP-0115 v1.4 and g_str_hash crashes on NULL >:O. + * Okay, maybe I've played too much Zelda, but that looks like + * a Deku Shrub... + */ + guint hashhash = (key->hash ? g_str_hash(key->hash) : 0); + return nodehash ^ verhash ^ hashhash; } static gboolean jabber_caps_compare(gconstpointer v1, gconstpointer v2) { const JabberCapsKey *name1 = v1; const JabberCapsKey *name2 = v2; - return strcmp(name1->node,name2->node) == 0 && strcmp(name1->ver,name2->ver) == 0; + return g_str_equal(name1->node, name2->node) && + g_str_equal(name1->ver, name2->ver) && + purple_strequal(name1->hash, name2->hash); } -static void jabber_caps_destroy_key(gpointer key) { - JabberCapsKey *keystruct = key; - g_free(keystruct->node); - g_free(keystruct->ver); - g_free(keystruct); +void jabber_caps_destroy_key(gpointer data) { + JabberCapsKey *key = data; + g_free(key->node); + g_free(key->ver); + g_free(key->hash); + g_free(key); } -static void jabber_caps_destroy_value(gpointer value) { - JabberCapsValue *valuestruct = value; - while(valuestruct->identities) { - JabberCapsIdentity *id = valuestruct->identities->data; +static void +jabber_caps_client_info_destroy(JabberCapsClientInfo *info) +{ + if (info == NULL) + return; + + while(info->identities) { + JabberIdentity *id = info->identities->data; g_free(id->category); g_free(id->type); g_free(id->name); + g_free(id->lang); g_free(id); - - valuestruct->identities = g_list_delete_link(valuestruct->identities,valuestruct->identities); + info->identities = g_list_delete_link(info->identities, info->identities); } - while(valuestruct->features) { - g_free(valuestruct->features->data); - valuestruct->features = g_list_delete_link(valuestruct->features,valuestruct->features); + + free_string_glist(info->features); + free_string_glist(info->forms); + + jabber_caps_node_exts_unref(info->exts); + + g_free(info); +} + +/* NOTE: Takes a reference to the exts, unref it if you don't really want to + * keep it around. */ +static JabberCapsNodeExts* +jabber_caps_find_exts_by_node(const char *node) +{ + JabberCapsNodeExts *exts; + if (NULL == (exts = g_hash_table_lookup(nodetable, node))) { + exts = g_new0(JabberCapsNodeExts, 1); + exts->exts = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, + (GDestroyNotify)free_string_glist); + g_hash_table_insert(nodetable, g_strdup(node), jabber_caps_node_exts_ref(exts)); } - g_hash_table_destroy(valuestruct->ext); - g_free(valuestruct); + + return jabber_caps_node_exts_ref(exts); +} + +static void +exts_to_xmlnode(gconstpointer key, gconstpointer value, gpointer user_data) +{ + const char *identifier = key; + const GList *features = value, *node; + xmlnode *client = user_data, *ext, *feature; + + ext = xmlnode_new_child(client, "ext"); + xmlnode_set_attrib(ext, "identifier", identifier); + + for (node = features; node; node = node->next) { + feature = xmlnode_new_child(ext, "feature"); + xmlnode_set_attrib(feature, "var", (const gchar *)node->data); + } } -static void jabber_caps_ext_destroy_value(gpointer value) { - JabberCapsValueExt *valuestruct = value; - while(valuestruct->identities) { - JabberCapsIdentity *id = valuestruct->identities->data; - g_free(id->category); - g_free(id->type); - g_free(id->name); - g_free(id); +static void jabber_caps_store_client(gpointer key, gpointer value, gpointer user_data) { + JabberCapsKey *clientinfo = key; + JabberCapsClientInfo *props = value; + xmlnode *root = user_data; + xmlnode *client = xmlnode_new_child(root, "client"); + GList *iter; - valuestruct->identities = g_list_delete_link(valuestruct->identities,valuestruct->identities); + xmlnode_set_attrib(client, "node", clientinfo->node); + xmlnode_set_attrib(client, "ver", clientinfo->ver); + if (clientinfo->hash) + xmlnode_set_attrib(client, "hash", clientinfo->hash); + for(iter = props->identities; iter; iter = g_list_next(iter)) { + JabberIdentity *id = iter->data; + xmlnode *identity = xmlnode_new_child(client, "identity"); + xmlnode_set_attrib(identity, "category", id->category); + xmlnode_set_attrib(identity, "type", id->type); + if (id->name) + xmlnode_set_attrib(identity, "name", id->name); + if (id->lang) + xmlnode_set_attrib(identity, "lang", id->lang); } - while(valuestruct->features) { - g_free(valuestruct->features->data); - valuestruct->features = g_list_delete_link(valuestruct->features,valuestruct->features); + + for(iter = props->features; iter; iter = g_list_next(iter)) { + const char *feat = iter->data; + xmlnode *feature = xmlnode_new_child(client, "feature"); + xmlnode_set_attrib(feature, "var", feat); } - g_free(valuestruct); + + for(iter = props->forms; iter; iter = g_list_next(iter)) { + /* FIXME: See #7814 */ + xmlnode *xdata = iter->data; + xmlnode_insert_child(client, xmlnode_copy(xdata)); + } + + /* TODO: Ideally, only save this once-per-node... */ + if (props->exts) + g_hash_table_foreach(props->exts->exts, (GHFunc)exts_to_xmlnode, client); } -static void jabber_caps_load(void); +static gboolean +do_jabber_caps_store(gpointer data) +{ + char *str; + int length = 0; + xmlnode *root = xmlnode_new("capabilities"); + g_hash_table_foreach(capstable, jabber_caps_store_client, root); + str = xmlnode_to_formatted_str(root, &length); + xmlnode_free(root); + purple_util_write_data_to_file(JABBER_CAPS_FILENAME, str, length); + g_free(str); -void jabber_caps_init(void) { - capstable = g_hash_table_new_full(jabber_caps_hash, jabber_caps_compare, jabber_caps_destroy_key, jabber_caps_destroy_value); - jabber_caps_load(); + save_timer = 0; + return FALSE; } -static void jabber_caps_load(void) { +static void +schedule_caps_save(void) +{ + if (save_timer == 0) + save_timer = purple_timeout_add_seconds(5, do_jabber_caps_store, NULL); +} + +static void +jabber_caps_load(void) +{ xmlnode *capsdata = purple_util_read_xml_from_file(JABBER_CAPS_FILENAME, "XMPP capabilities cache"); xmlnode *client; @@ -130,11 +252,17 @@ continue; if(!strcmp(client->name, "client")) { JabberCapsKey *key = g_new0(JabberCapsKey, 1); - JabberCapsValue *value = g_new0(JabberCapsValue, 1); + JabberCapsClientInfo *value = g_new0(JabberCapsClientInfo, 1); xmlnode *child; + JabberCapsNodeExts *exts = NULL; key->node = g_strdup(xmlnode_get_attrib(client,"node")); key->ver = g_strdup(xmlnode_get_attrib(client,"ver")); - value->ext = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, jabber_caps_ext_destroy_value); + key->hash = g_strdup(xmlnode_get_attrib(client,"hash")); + + /* v1.3 capabilities */ + if (key->hash == NULL) + exts = jabber_caps_find_exts_by_node(key->node); + for(child = client->child; child; child = child->next) { if(child->type != XMLNODE_TYPE_TAG) continue; @@ -147,438 +275,656 @@ const char *category = xmlnode_get_attrib(child, "category"); const char *type = xmlnode_get_attrib(child, "type"); const char *name = xmlnode_get_attrib(child, "name"); + const char *lang = xmlnode_get_attrib(child, "lang"); + JabberIdentity *id; - JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1); + if (!category || !type) + continue; + + id = g_new0(JabberIdentity, 1); id->category = g_strdup(category); id->type = g_strdup(type); id->name = g_strdup(name); - + id->lang = g_strdup(lang); + value->identities = g_list_append(value->identities,id); - } else if(!strcmp(child->name,"ext")) { + } else if(!strcmp(child->name,"x")) { + /* FIXME: See #7814 -- this will cause problems if anyone + * ever actually specifies forms. In fact, for this to + * work properly, that bug needs to be fixed in + * xmlnode_from_str, not the output version... */ + value->forms = g_list_append(value->forms, xmlnode_copy(child)); + } else if (!strcmp(child->name, "ext") && key->hash != NULL) { + purple_debug_warning("jabber", "Ignoring exts when reading new-style caps\n"); + } else if (!strcmp(child->name, "ext")) { + /* TODO: Do we care about reading in the identities listed here? */ const char *identifier = xmlnode_get_attrib(child, "identifier"); - if(identifier) { - xmlnode *extchild; + xmlnode *node; + GList *features = NULL; + + if (!identifier) + continue; - JabberCapsValueExt *extvalue = g_new0(JabberCapsValueExt, 1); - - for(extchild = child->child; extchild; extchild = extchild->next) { - if(extchild->type != XMLNODE_TYPE_TAG) + for (node = child->child; node; node = node->next) { + if (node->type != XMLNODE_TYPE_TAG) + continue; + if (!strcmp(node->name, "feature")) { + const char *var = xmlnode_get_attrib(node, "var"); + if (!var) continue; - if(!strcmp(extchild->name,"feature")) { - const char *var = xmlnode_get_attrib(extchild, "var"); - if(!var) - continue; - extvalue->features = g_list_append(extvalue->features,g_strdup(var)); - } else if(!strcmp(extchild->name,"identity")) { - const char *category = xmlnode_get_attrib(extchild, "category"); - const char *type = xmlnode_get_attrib(extchild, "type"); - const char *name = xmlnode_get_attrib(extchild, "name"); + features = g_list_prepend(features, g_strdup(var)); + } + } - JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1); - id->category = g_strdup(category); - id->type = g_strdup(type); - id->name = g_strdup(name); - - extvalue->identities = g_list_append(extvalue->identities,id); - } - } - g_hash_table_replace(value->ext, g_strdup(identifier), extvalue); - } + if (features) { + g_hash_table_insert(exts->exts, g_strdup(identifier), + features); + } else + purple_debug_warning("jabber", "Caps ext %s had no features.\n", + identifier); } } + + value->exts = exts; g_hash_table_replace(capstable, key, value); + } } xmlnode_free(capsdata); } -static void jabber_caps_store_ext(gpointer key, gpointer value, gpointer user_data) { - const char *extname = key; - JabberCapsValueExt *props = value; - xmlnode *root = user_data; - xmlnode *ext = xmlnode_new_child(root,"ext"); - GList *iter; - - xmlnode_set_attrib(ext,"identifier",extname); - - for(iter = props->identities; iter; iter = g_list_next(iter)) { - JabberCapsIdentity *id = iter->data; - xmlnode *identity = xmlnode_new_child(ext, "identity"); - xmlnode_set_attrib(identity, "category", id->category); - xmlnode_set_attrib(identity, "type", id->type); - if (id->name) - xmlnode_set_attrib(identity, "name", id->name); - } - - for(iter = props->features; iter; iter = g_list_next(iter)) { - const char *feat = iter->data; - xmlnode *feature = xmlnode_new_child(ext, "feature"); - xmlnode_set_attrib(feature, "var", feat); - } -} - -static void jabber_caps_store_client(gpointer key, gpointer value, gpointer user_data) { - JabberCapsKey *clientinfo = key; - JabberCapsValue *props = value; - xmlnode *root = user_data; - xmlnode *client = xmlnode_new_child(root,"client"); - GList *iter; - - xmlnode_set_attrib(client,"node",clientinfo->node); - xmlnode_set_attrib(client,"ver",clientinfo->ver); - - for(iter = props->identities; iter; iter = g_list_next(iter)) { - JabberCapsIdentity *id = iter->data; - xmlnode *identity = xmlnode_new_child(client, "identity"); - xmlnode_set_attrib(identity, "category", id->category); - xmlnode_set_attrib(identity, "type", id->type); - if (id->name) - xmlnode_set_attrib(identity, "name", id->name); - } - - for(iter = props->features; iter; iter = g_list_next(iter)) { - const char *feat = iter->data; - xmlnode *feature = xmlnode_new_child(client, "feature"); - xmlnode_set_attrib(feature, "var", feat); - } - - g_hash_table_foreach(props->ext,jabber_caps_store_ext,client); -} - -static void jabber_caps_store(void) { - char *str; - xmlnode *root = xmlnode_new("capabilities"); - g_hash_table_foreach(capstable, jabber_caps_store_client, root); - str = xmlnode_to_formatted_str(root, NULL); - xmlnode_free(root); - purple_util_write_data_to_file(JABBER_CAPS_FILENAME, str, -1); - g_free(str); +void jabber_caps_init(void) +{ + nodetable = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)jabber_caps_node_exts_unref); + capstable = g_hash_table_new_full(jabber_caps_hash, jabber_caps_compare, jabber_caps_destroy_key, (GDestroyNotify)jabber_caps_client_info_destroy); + jabber_caps_load(); } -/* this function assumes that all information is available locally */ -static JabberCapsClientInfo *jabber_caps_collect_info(const char *node, const char *ver, GList *ext) { - JabberCapsClientInfo *result; - JabberCapsKey *key = g_new0(JabberCapsKey, 1); - JabberCapsValue *caps; - GList *iter; - - key->node = (char *)node; - key->ver = (char *)ver; - - caps = g_hash_table_lookup(capstable,key); - - g_free(key); - - if (caps == NULL) - return NULL; - - result = g_new0(JabberCapsClientInfo, 1); - - /* join all information */ - for(iter = caps->identities; iter; iter = g_list_next(iter)) { - JabberCapsIdentity *id = iter->data; - JabberCapsIdentity *newid = g_new0(JabberCapsIdentity, 1); - newid->category = g_strdup(id->category); - newid->type = g_strdup(id->type); - newid->name = g_strdup(id->name); - - result->identities = g_list_append(result->identities,newid); - } - for(iter = caps->features; iter; iter = g_list_next(iter)) { - const char *feat = iter->data; - char *newfeat = g_strdup(feat); - - result->features = g_list_append(result->features,newfeat); +void jabber_caps_uninit(void) +{ + if (save_timer != 0) { + purple_timeout_remove(save_timer); + save_timer = 0; + do_jabber_caps_store(NULL); } - - for(iter = ext; iter; iter = g_list_next(iter)) { - const char *extname = iter->data; - JabberCapsValueExt *extinfo = g_hash_table_lookup(caps->ext,extname); - - if(extinfo) { - GList *iter2; - for(iter2 = extinfo->identities; iter2; iter2 = g_list_next(iter2)) { - JabberCapsIdentity *id = iter2->data; - JabberCapsIdentity *newid = g_new0(JabberCapsIdentity, 1); - newid->category = g_strdup(id->category); - newid->type = g_strdup(id->type); - newid->name = g_strdup(id->name); - - result->identities = g_list_append(result->identities,newid); - } - for(iter2 = extinfo->features; iter2; iter2 = g_list_next(iter2)) { - const char *feat = iter2->data; - char *newfeat = g_strdup(feat); - - result->features = g_list_append(result->features,newfeat); - } - } - } - return result; -} - -void jabber_caps_free_clientinfo(JabberCapsClientInfo *clientinfo) { - if(!clientinfo) - return; - while(clientinfo->identities) { - JabberCapsIdentity *id = clientinfo->identities->data; - g_free(id->category); - g_free(id->type); - g_free(id->name); - g_free(id); - - clientinfo->identities = g_list_delete_link(clientinfo->identities,clientinfo->identities); - } - while(clientinfo->features) { - char *feat = clientinfo->features->data; - g_free(feat); - - clientinfo->features = g_list_delete_link(clientinfo->features,clientinfo->features); - } - - g_free(clientinfo); + g_hash_table_destroy(capstable); + g_hash_table_destroy(nodetable); + capstable = NULL; } typedef struct _jabber_caps_cbplususerdata { + guint ref; + jabber_caps_get_info_cb cb; - gpointer user_data; + gpointer cb_data; char *who; char *node; char *ver; - GList *ext; - unsigned extOutstanding; + char *hash; + + JabberCapsClientInfo *info; + + GList *exts; + guint extOutstanding; + JabberCapsNodeExts *node_exts; } jabber_caps_cbplususerdata; -typedef struct jabber_ext_userdata { - jabber_caps_cbplususerdata *userdata; - char *node; -} jabber_ext_userdata; +static jabber_caps_cbplususerdata* +cbplususerdata_ref(jabber_caps_cbplususerdata *data) +{ + g_return_val_if_fail(data != NULL, NULL); -static void jabber_caps_get_info_check_completion(jabber_caps_cbplususerdata *userdata) { - if(userdata->extOutstanding == 0) { - userdata->cb(jabber_caps_collect_info(userdata->node, userdata->ver, userdata->ext), userdata->user_data); - g_free(userdata->who); - g_free(userdata->node); - g_free(userdata->ver); - while(userdata->ext) { - g_free(userdata->ext->data); - userdata->ext = g_list_delete_link(userdata->ext,userdata->ext); - } - g_free(userdata); - } + ++data->ref; + return data; } -static void jabber_caps_ext_iqcb(JabberStream *js, const char *from, - JabberIqType type, const char *id, - xmlnode *packet, gpointer data) +static void +cbplususerdata_unref(jabber_caps_cbplususerdata *data) { - /* collect data and fetch all exts */ - xmlnode *query = xmlnode_get_child_with_namespace(packet, "query", "http://jabber.org/protocol/disco#info"); - jabber_ext_userdata *extuserdata = data; - jabber_caps_cbplususerdata *userdata = extuserdata->userdata; - const char *node = extuserdata->node; - - --userdata->extOutstanding; - - /* TODO: Better error handling */ + if (data == NULL) + return; - if(node && query) { - const char *key; - JabberCapsValue *client; - xmlnode *child; - JabberCapsValueExt *value = g_new0(JabberCapsValueExt, 1); - JabberCapsKey *clientkey = g_new0(JabberCapsKey, 1); + g_return_if_fail(data->ref != 0); - clientkey->node = userdata->node; - clientkey->ver = userdata->ver; - - client = g_hash_table_lookup(capstable, clientkey); - - g_free(clientkey); + if (--data->ref > 0) + return; - /* split node by #, key either points to \0 or the correct ext afterwards */ - for(key = node; key[0] != '\0'; ++key) { - if(key[0] == '#') { - ++key; - break; - } - } + g_free(data->who); + g_free(data->node); + g_free(data->ver); + g_free(data->hash); - for(child = query->child; child; child = child->next) { - if(child->type != XMLNODE_TYPE_TAG) - continue; - if(!strcmp(child->name,"feature")) { - const char *var = xmlnode_get_attrib(child, "var"); - if(!var) - continue; - value->features = g_list_append(value->features,g_strdup(var)); - } else if(!strcmp(child->name,"identity")) { - const char *category = xmlnode_get_attrib(child, "category"); - const char *type = xmlnode_get_attrib(child, "type"); - const char *name = xmlnode_get_attrib(child, "name"); - - JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1); - id->category = g_strdup(category); - id->type = g_strdup(type); - id->name = g_strdup(name); - - value->identities = g_list_append(value->identities,id); - } - } - g_hash_table_replace(client->ext, g_strdup(key), value); - - jabber_caps_store(); - } - - g_free(extuserdata->node); - g_free(extuserdata); - jabber_caps_get_info_check_completion(userdata); + /* If we have info here, it's already in the capstable, so don't free it */ + if (data->exts) + free_string_glist(data->exts); + if (data->node_exts) + jabber_caps_node_exts_unref(data->node_exts); + g_free(data); } -static void jabber_caps_client_iqcb(JabberStream *js, const char *from, - JabberIqType type, const char *id, - xmlnode *packet, gpointer data) +static void +jabber_caps_get_info_complete(jabber_caps_cbplususerdata *userdata) { - /* collect data and fetch all exts */ + userdata->cb(userdata->info, userdata->exts, userdata->cb_data); + userdata->info = NULL; + userdata->exts = NULL; + + if (userdata->ref != 1) + purple_debug_warning("jabber", "Lost a reference to caps cbdata: %d\n", + userdata->ref); +} + +static void +jabber_caps_client_iqcb(JabberStream *js, const char *from, JabberIqType type, + const char *id, xmlnode *packet, gpointer data) +{ xmlnode *query = xmlnode_get_child_with_namespace(packet, "query", "http://jabber.org/protocol/disco#info"); jabber_caps_cbplususerdata *userdata = data; - - /* TODO: Better error checking! */ - - if (query) { - JabberCapsValue *value = g_new0(JabberCapsValue, 1); - JabberCapsKey *key = g_new0(JabberCapsKey, 1); - xmlnode *child; - GList *iter; - - value->ext = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, jabber_caps_ext_destroy_value); + JabberCapsClientInfo *info = NULL, *value; + JabberCapsKey key; - key->node = g_strdup(userdata->node); - key->ver = g_strdup(userdata->ver); + if (!query || type == JABBER_IQ_ERROR) { + /* Any outstanding exts will be dealt with via ref-counting */ + userdata->cb(NULL, NULL, userdata->cb_data); + cbplususerdata_unref(userdata); + return; + } - for(child = query->child; child; child = child->next) { - if(child->type != XMLNODE_TYPE_TAG) - continue; - if(!strcmp(child->name,"feature")) { - const char *var = xmlnode_get_attrib(child, "var"); - if(!var) - continue; - value->features = g_list_append(value->features, g_strdup(var)); - } else if(!strcmp(child->name,"identity")) { - const char *category = xmlnode_get_attrib(child, "category"); - const char *type = xmlnode_get_attrib(child, "type"); - const char *name = xmlnode_get_attrib(child, "name"); + /* check hash */ + info = jabber_caps_parse_client_info(query); - JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1); - id->category = g_strdup(category); - id->type = g_strdup(type); - id->name = g_strdup(name); - - value->identities = g_list_append(value->identities,id); - } + /* Only validate if these are v1.5 capabilities */ + if (userdata->hash) { + gchar *hash = NULL; + if (!strcmp(userdata->hash, "sha-1")) { + hash = jabber_caps_calculate_hash(info, "sha1"); + } else if (!strcmp(userdata->hash, "md5")) { + hash = jabber_caps_calculate_hash(info, "md5"); } - g_hash_table_replace(capstable, key, value); - jabber_caps_store(); - - /* fetch all exts */ - for(iter = userdata->ext; iter; iter = g_list_next(iter)) { - JabberIq *iq = jabber_iq_new_query(js, JABBER_IQ_GET, - "http://jabber.org/protocol/disco#info"); - xmlnode *query = xmlnode_get_child_with_namespace(iq->node, - "query", "http://jabber.org/protocol/disco#info"); - char *node = g_strdup_printf("%s#%s", userdata->node, (const char*)iter->data); - jabber_ext_userdata *ext_data = g_new0(jabber_ext_userdata, 1); - ext_data->node = node; - ext_data->userdata = userdata; + if (!hash || strcmp(hash, userdata->ver)) { + purple_debug_warning("jabber", "Could not validate caps info from %s\n", + xmlnode_get_attrib(packet, "from")); - xmlnode_set_attrib(query, "node", node); - xmlnode_set_attrib(iq->node, "to", userdata->who); - - jabber_iq_set_callback(iq, jabber_caps_ext_iqcb, ext_data); - jabber_iq_send(iq); + userdata->cb(NULL, NULL, userdata->cb_data); + jabber_caps_client_info_destroy(info); + cbplususerdata_unref(userdata); + g_free(hash); + return; } - } else - /* Don't wait for the ext discoveries; they aren't going to happen */ - userdata->extOutstanding = 0; + g_free(hash); + } + + if (!userdata->hash && userdata->node_exts) { + /* If the ClientInfo doesn't have information about the exts, give them + * ours (along with our ref) */ + info->exts = userdata->node_exts; + userdata->node_exts = NULL; + } + + key.node = userdata->node; + key.ver = userdata->ver; + key.hash = userdata->hash; - jabber_caps_get_info_check_completion(userdata); + /* Use the copy of this data already in the table if it exists or insert + * a new one if we need to */ + if ((value = g_hash_table_lookup(capstable, &key))) { + jabber_caps_client_info_destroy(info); + info = value; + } else { + JabberCapsKey *n_key = g_new(JabberCapsKey, 1); + n_key->node = userdata->node; + n_key->ver = userdata->ver; + n_key->hash = userdata->hash; + userdata->node = userdata->ver = userdata->hash = NULL; + + /* The capstable gets a reference */ + g_hash_table_insert(capstable, n_key, info); + schedule_caps_save(); + } + + userdata->info = info; + + if (userdata->extOutstanding == 0) + jabber_caps_get_info_complete(userdata); + + cbplususerdata_unref(userdata); } -void jabber_caps_get_info(JabberStream *js, const char *who, const char *node, const char *ver, const char *ext, jabber_caps_get_info_cb cb, gpointer user_data) { - JabberCapsValue *client; - JabberCapsKey *key = g_new0(JabberCapsKey, 1); - char *originalext = g_strdup(ext); - jabber_caps_cbplususerdata *userdata = g_new0(jabber_caps_cbplususerdata, 1); +typedef struct { + const char *name; + jabber_caps_cbplususerdata *data; +} ext_iq_data; + +static void +jabber_caps_ext_iqcb(JabberStream *js, const char *from, JabberIqType type, + const char *id, xmlnode *packet, gpointer data) +{ + xmlnode *query = xmlnode_get_child_with_namespace(packet, "query", + "http://jabber.org/protocol/disco#info"); + xmlnode *child; + ext_iq_data *userdata = data; + GList *features = NULL; + JabberCapsNodeExts *node_exts; + + if (!query || type == JABBER_IQ_ERROR) { + cbplususerdata_unref(userdata->data); + g_free(userdata); + return; + } + + /* So, we decrement this after checking for an error, which means that + * if there *is* an error, we'll never call the callback passed to + * jabber_caps_get_info. We will still free all of our data, though. + */ + --userdata->data->extOutstanding; + + for (child = xmlnode_get_child(query, "feature"); child; + child = xmlnode_get_next_twin(child)) { + const char *var = xmlnode_get_attrib(child, "var"); + if (var) + features = g_list_prepend(features, g_strdup(var)); + } + + node_exts = (userdata->data->info ? userdata->data->info->exts : + userdata->data->node_exts); + g_hash_table_insert(node_exts->exts, g_strdup(userdata->name), features); + schedule_caps_save(); + + /* Are we done? */ + if (userdata->data->info && userdata->data->extOutstanding == 0) + jabber_caps_get_info_complete(userdata->data); + + cbplususerdata_unref(userdata->data); + g_free(userdata); +} + +void jabber_caps_get_info(JabberStream *js, const char *who, const char *node, + const char *ver, const char *hash, const char *ext, + jabber_caps_get_info_cb cb, gpointer user_data) +{ + JabberCapsClientInfo *info; + JabberCapsKey key; + jabber_caps_cbplususerdata *userdata; + + if (ext && *ext && hash) + purple_debug_info("jabber", "Ignoring exts in new-style caps from %s\n", + who); + + /* Using this in a read-only fashion, so the cast is OK */ + key.node = (char *)node; + key.ver = (char *)ver; + key.hash = (char *)hash; + + info = g_hash_table_lookup(capstable, &key); + if (info && hash) { + /* v1.5 - We already have all the information we care about */ + cb(info, NULL, user_data); + return; + } + + userdata = g_new0(jabber_caps_cbplususerdata, 1); + /* This ref is given to fetching the basic node#ver info if we need it + * or unrefed at the bottom of this function */ + cbplususerdata_ref(userdata); userdata->cb = cb; - userdata->user_data = user_data; + userdata->cb_data = user_data; userdata->who = g_strdup(who); userdata->node = g_strdup(node); userdata->ver = g_strdup(ver); - - if(originalext) { - int i; - gchar **splat = g_strsplit(originalext, " ", 0); - for(i =0; splat[i]; i++) { - userdata->ext = g_list_append(userdata->ext, splat[i]); - ++userdata->extOutstanding; - } - g_free(splat); - } - g_free(originalext); + userdata->hash = g_strdup(hash); - key->node = (char *)node; - key->ver = (char *)ver; - - client = g_hash_table_lookup(capstable, key); + if (info) { + userdata->info = info; + } else { + /* If we don't have the basic information about the client, we need + * to fetch it. */ + JabberIq *iq; + xmlnode *query; + char *nodever; - g_free(key); - - if(!client) { - JabberIq *iq = jabber_iq_new_query(js,JABBER_IQ_GET,"http://jabber.org/protocol/disco#info"); - xmlnode *query = xmlnode_get_child_with_namespace(iq->node,"query","http://jabber.org/protocol/disco#info"); - char *nodever = g_strdup_printf("%s#%s", node, ver); + iq = jabber_iq_new_query(js, JABBER_IQ_GET, + "http://jabber.org/protocol/disco#info"); + query = xmlnode_get_child_with_namespace(iq->node, "query", + "http://jabber.org/protocol/disco#info"); + nodever = g_strdup_printf("%s#%s", node, ver); xmlnode_set_attrib(query, "node", nodever); g_free(nodever); xmlnode_set_attrib(iq->node, "to", who); - jabber_iq_set_callback(iq,jabber_caps_client_iqcb,userdata); + jabber_iq_set_callback(iq, jabber_caps_client_iqcb, userdata); jabber_iq_send(iq); - } else { - GList *iter; - /* fetch unknown exts only */ - for(iter = userdata->ext; iter; iter = g_list_next(iter)) { - JabberCapsValueExt *extvalue = g_hash_table_lookup(client->ext, (const char*)iter->data); - JabberIq *iq; - xmlnode *query; - char *nodever; - jabber_ext_userdata *ext_data; + } + + /* Are there any exts that we don't recognize? */ + if (ext && *ext && !hash) { + JabberCapsNodeExts *node_exts; + gchar **splat = g_strsplit(ext, " ", 0); + int i; + + if (info) { + if (info->exts) + node_exts = info->exts; + else + node_exts = info->exts = jabber_caps_find_exts_by_node(node); + } else + /* We'll put it in later once we have the client info */ + node_exts = userdata->node_exts = jabber_caps_find_exts_by_node(node); - if(extvalue) { - /* we already have this ext, don't bother with it */ - --userdata->extOutstanding; - continue; + for (i = 0; splat[i]; ++i) { + userdata->exts = g_list_prepend(userdata->exts, splat[i]); + /* Look it up if we don't already know what it means */ + if (!g_hash_table_lookup(node_exts->exts, splat[i])) { + JabberIq *iq; + xmlnode *query; + char *nodeext; + ext_iq_data *cbdata = g_new(ext_iq_data, 1); + + cbdata->name = splat[i]; + cbdata->data = cbplususerdata_ref(userdata); + + iq = jabber_iq_new_query(js, JABBER_IQ_GET, + "http://jabber.org/protocol/disco#info"); + query = xmlnode_get_child_with_namespace(iq->node, "query", + "http://jabber.org/protocol/disco#info"); + nodeext = g_strdup_printf("%s#%s", node, splat[i]); + xmlnode_set_attrib(query, "node", nodeext); + g_free(nodeext); + xmlnode_set_attrib(iq->node, "to", who); + + jabber_iq_set_callback(iq, jabber_caps_ext_iqcb, cbdata); + jabber_iq_send(iq); + + ++userdata->extOutstanding; } - - ext_data = g_new0(jabber_ext_userdata, 1); + splat[i] = NULL; + } + /* All the strings are now part of the GList, so don't need + * g_strfreev. */ + g_free(splat); + } - iq = jabber_iq_new_query(js,JABBER_IQ_GET,"http://jabber.org/protocol/disco#info"); - query = xmlnode_get_child_with_namespace(iq->node,"query","http://jabber.org/protocol/disco#info"); - nodever = g_strdup_printf("%s#%s", node, (const char*)iter->data); - xmlnode_set_attrib(query, "node", nodever); - xmlnode_set_attrib(iq->node, "to", who); + if (userdata->info && userdata->extOutstanding == 0) { + jabber_caps_get_info_complete(userdata); + cbplususerdata_unref(userdata); + } +} - ext_data->node = nodever; - ext_data->userdata = userdata; +static gint +jabber_identity_compare(gconstpointer a, gconstpointer b) +{ + const JabberIdentity *ac; + const JabberIdentity *bc; + gint cat_cmp; + gint typ_cmp; + + ac = a; + bc = b; - jabber_iq_set_callback(iq, jabber_caps_ext_iqcb, ext_data); - jabber_iq_send(iq); + if ((cat_cmp = strcmp(ac->category, bc->category)) == 0) { + if ((typ_cmp = strcmp(ac->type, bc->type)) == 0) { + if (!ac->lang && !bc->lang) { + return 0; + } else if (ac->lang && !bc->lang) { + return 1; + } else if (!ac->lang && bc->lang) { + return -1; + } else { + return strcmp(ac->lang, bc->lang); + } + } else { + return typ_cmp; } - /* maybe we have all data available anyways? This is the ideal case where no network traffic is necessary */ - jabber_caps_get_info_check_completion(userdata); + } else { + return cat_cmp; } } +static gchar *jabber_caps_get_formtype(const xmlnode *x) { + xmlnode *formtypefield; + formtypefield = xmlnode_get_child(x, "field"); + while (formtypefield && strcmp(xmlnode_get_attrib(formtypefield, "var"), "FORM_TYPE")) formtypefield = xmlnode_get_next_twin(formtypefield); + formtypefield = xmlnode_get_child(formtypefield, "value"); + return xmlnode_get_data(formtypefield);; +} + +static gint +jabber_xdata_compare(gconstpointer a, gconstpointer b) +{ + const xmlnode *aformtypefield = a; + const xmlnode *bformtypefield = b; + char *aformtype; + char *bformtype; + int result; + + aformtype = jabber_caps_get_formtype(aformtypefield); + bformtype = jabber_caps_get_formtype(bformtypefield); + + result = strcmp(aformtype, bformtype); + g_free(aformtype); + g_free(bformtype); + return result; +} + +static JabberCapsClientInfo *jabber_caps_parse_client_info(xmlnode *query) +{ + xmlnode *child; + JabberCapsClientInfo *info; + + if (!query || strcmp(query->xmlns, "http://jabber.org/protocol/disco#info")) + return 0; + + info = g_new0(JabberCapsClientInfo, 1); + + for(child = query->child; child; child = child->next) { + if (child->type != XMLNODE_TYPE_TAG) + continue; + if (!strcmp(child->name,"identity")) { + /* parse identity */ + const char *category = xmlnode_get_attrib(child, "category"); + const char *type = xmlnode_get_attrib(child, "type"); + const char *name = xmlnode_get_attrib(child, "name"); + const char *lang = xmlnode_get_attrib(child, "lang"); + JabberIdentity *id; + + if (!category || !type) + continue; + + id = g_new0(JabberIdentity, 1); + id->category = g_strdup(category); + id->type = g_strdup(type); + id->name = g_strdup(name); + id->lang = g_strdup(lang); + + info->identities = g_list_append(info->identities, id); + } else if (!strcmp(child->name, "feature")) { + /* parse feature */ + const char *var = xmlnode_get_attrib(child, "var"); + if (var) + info->features = g_list_prepend(info->features, g_strdup(var)); + } else if (!strcmp(child->name, "x")) { + if (child->xmlns && !strcmp(child->xmlns, "jabber:x:data")) { + /* x-data form */ + xmlnode *dataform = xmlnode_copy(child); + info->forms = g_list_append(info->forms, dataform); + } + } + } + return info; +} + +static gint jabber_caps_xdata_field_compare(gconstpointer a, gconstpointer b) +{ + const JabberDataFormField *ac = a; + const JabberDataFormField *bc = b; + + return strcmp(ac->var, bc->var); +} + +static GList* jabber_caps_xdata_get_fields(const xmlnode *x) +{ + GList *fields = NULL; + xmlnode *field; + + if (!x) + return NULL; + + for (field = xmlnode_get_child(x, "field"); field; field = xmlnode_get_next_twin(field)) { + xmlnode *value; + JabberDataFormField *xdatafield = g_new0(JabberDataFormField, 1); + xdatafield->var = g_strdup(xmlnode_get_attrib(field, "var")); + + for (value = xmlnode_get_child(field, "value"); value; value = xmlnode_get_next_twin(value)) { + gchar *val = xmlnode_get_data(value); + xdatafield->values = g_list_append(xdatafield->values, val); + } + + xdatafield->values = g_list_sort(xdatafield->values, (GCompareFunc)strcmp); + fields = g_list_append(fields, xdatafield); + } + + fields = g_list_sort(fields, jabber_caps_xdata_field_compare); + return fields; +} + +static GString* +jabber_caps_verification_append(GString *verification, const gchar *string) +{ + verification = g_string_append(verification, string); + return g_string_append_c(verification, '<'); +} + +gchar *jabber_caps_calculate_hash(JabberCapsClientInfo *info, const char *hash) +{ + GList *node; + GString *verification; + PurpleCipherContext *context; + guint8 checksum[20]; + gsize checksum_size = 20; + gboolean success; + + if (!info || !(context = purple_cipher_context_new_by_name(hash, NULL))) + return NULL; + + /* sort identities, features and x-data forms */ + info->identities = g_list_sort(info->identities, jabber_identity_compare); + info->features = g_list_sort(info->features, (GCompareFunc)strcmp); + info->forms = g_list_sort(info->forms, jabber_xdata_compare); + + verification = g_string_new(""); + + /* concat identities to the verification string */ + for (node = info->identities; node; node = node->next) { + JabberIdentity *id = (JabberIdentity*)node->data; + + g_string_append_printf(verification, "%s/%s/%s/%s<", id->category, + id->type, id->lang ? id->lang : "", id->name); + } + + /* concat features to the verification string */ + for (node = info->features; node; node = node->next) { + verification = jabber_caps_verification_append(verification, node->data); + } + + /* concat x-data forms to the verification string */ + for(node = info->forms; node; node = node->next) { + xmlnode *data = (xmlnode *)node->data; + gchar *formtype = jabber_caps_get_formtype(data); + GList *fields = jabber_caps_xdata_get_fields(data); + + /* append FORM_TYPE's field value to the verification string */ + verification = jabber_caps_verification_append(verification, formtype); + g_free(formtype); + + while (fields) { + GList *value; + JabberDataFormField *field = (JabberDataFormField*)fields->data; + + if (strcmp(field->var, "FORM_TYPE")) { + /* Append the "var" attribute */ + verification = jabber_caps_verification_append(verification, field->var); + /* Append <value/> elements' cdata */ + for(value = field->values; value; value = value->next) { + verification = jabber_caps_verification_append(verification, value->data); + g_free(value->data); + } + } + + g_free(field->var); + g_list_free(field->values); + + fields = g_list_delete_link(fields, fields); + } + } + + /* generate hash */ + purple_cipher_context_append(context, (guchar*)verification->str, verification->len); + + success = purple_cipher_context_digest(context, verification->len, + checksum, &checksum_size); + + g_string_free(verification, TRUE); + purple_cipher_context_destroy(context); + + return (success ? purple_base64_encode(checksum, checksum_size) : NULL); +} + +void jabber_caps_calculate_own_hash(JabberStream *js) { + JabberCapsClientInfo info; + GList *iter = 0; + GList *features = 0; + + if (!jabber_identities && !jabber_features) { + /* This really shouldn't ever happen */ + purple_debug_warning("jabber", "No features or identities, cannot calculate own caps hash.\n"); + g_free(js->caps_hash); + js->caps_hash = NULL; + return; + } + + /* build the currently-supported list of features */ + if (jabber_features) { + for (iter = jabber_features; iter; iter = iter->next) { + JabberFeature *feat = iter->data; + if(!feat->is_enabled || feat->is_enabled(js, feat->namespace)) { + features = g_list_append(features, feat->namespace); + } + } + } + + info.features = features; + info.identities = jabber_identities; + info.forms = NULL; + + g_free(js->caps_hash); + js->caps_hash = jabber_caps_calculate_hash(&info, "sha1"); + g_list_free(features); +} + +const gchar* jabber_caps_get_own_hash(JabberStream *js) +{ + if (!js->caps_hash) + jabber_caps_calculate_own_hash(js); + + return js->caps_hash; +} + +void jabber_caps_broadcast_change() +{ + GList *node, *accounts = purple_accounts_get_all_active(); + + for (node = accounts; node; node = node->next) { + PurpleAccount *account = node->data; + const char *prpl_id = purple_account_get_protocol_id(account); + if (!strcmp("prpl-jabber", prpl_id) && purple_account_is_connected(account)) { + PurpleConnection *gc = purple_account_get_connection(account); + jabber_presence_send(gc->proto_data, TRUE); + } + } + + g_list_free(accounts); +} +
--- a/libpurple/protocols/jabber/caps.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/caps.h Wed Apr 29 23:20:51 2009 +0000 @@ -26,24 +26,77 @@ #include "jabber.h" -/* Implementation of XEP-0115 */ +/* Implementation of XEP-0115 - Entity Capabilities */ -typedef struct _JabberCapsIdentity { - char *category; - char *type; - char *name; -} JabberCapsIdentity; +typedef struct _JabberCapsNodeExts JabberCapsNodeExts; struct _JabberCapsClientInfo { - GList *identities; /* JabberCapsIdentity */ + GList *identities; /* JabberIdentity */ GList *features; /* char * */ + GList *forms; /* xmlnode * */ + JabberCapsNodeExts *exts; +}; + +/* + * This stores a set of exts "known" for a specific node (which indicates + * a specific client -- for reference, Pidgin, Finch, Meebo, et al share one + * node.) In XEP-0115 v1.3, exts are used for features that may or may not be + * present at a given time (PEP things, buzz might be disabled, etc). + * + * This structure is shared among all JabberCapsClientInfo instances matching + * a specific node (if the capstable key->hash == NULL, which indicates that + * the ClientInfo is using v1.3 caps as opposed to v1.5 caps). + * + * It's only exposed so that jabber_resource_has_capability can use it. + * Everyone else, STAY AWAY! + */ +struct _JabberCapsNodeExts { + guint ref; + GHashTable *exts; /* char *ext_name -> GList *features */ }; -typedef void (*jabber_caps_get_info_cb)(JabberCapsClientInfo *info, gpointer user_data); +typedef void (*jabber_caps_get_info_cb)(JabberCapsClientInfo *info, GList *exts, gpointer user_data); void jabber_caps_init(void); +void jabber_caps_uninit(void); -void jabber_caps_get_info(JabberStream *js, const char *who, const char *node, const char *ver, const char *ext, jabber_caps_get_info_cb cb, gpointer user_data); -void jabber_caps_free_clientinfo(JabberCapsClientInfo *clientinfo); +void jabber_caps_destroy_key(gpointer value); + +/** + * Main entity capabilites function to get the capabilities of a contact. + * + * The callback will be called synchronously if we already have the + * capabilities for the specified (node,ver,hash) (and, if exts are specified, + * if we know what each means) + */ +void jabber_caps_get_info(JabberStream *js, const char *who, const char *node, + const char *ver, const char *hash, + const char *ext, jabber_caps_get_info_cb cb, + gpointer user_data); + +/** + * Takes a JabberCapsClientInfo pointer and returns the caps hash according to + * XEP-0115 Version 1.5. + * + * @param info A JabberCapsClientInfo pointer. + * @param hash Hash cipher to be used. Either sha-1 or md5. + * @return The base64 encoded SHA-1 hash; must be freed by caller + */ +gchar *jabber_caps_calculate_hash(JabberCapsClientInfo *info, const char *hash); + +/** + * Calculate SHA1 hash for own featureset. + */ +void jabber_caps_calculate_own_hash(JabberStream *js); + +/** Get the current caps hash. + * @ret hash +**/ +const gchar* jabber_caps_get_own_hash(JabberStream *js); + +/** + * Broadcast a new calculated hash using a <presence> stanza. + */ +void jabber_caps_broadcast_change(void); #endif /* PURPLE_JABBER_CAPS_H_ */
--- a/libpurple/protocols/jabber/chat.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/chat.c Wed Apr 29 23:20:51 2009 +0000 @@ -597,37 +597,25 @@ /* merge this with the function below when we get everyone on the same page wrt /commands */ void jabber_chat_change_topic(JabberChat *chat, const char *topic) { - if(topic && *topic) { - JabberMessage *jm; - jm = g_new0(JabberMessage, 1); - jm->js = chat->js; - jm->type = JABBER_MESSAGE_GROUPCHAT; - jm->subject = purple_markup_strip_html(topic); - jm->to = g_strdup_printf("%s@%s", chat->room, chat->server); - jabber_message_send(jm); - jabber_message_free(jm); - } else { - const char *cur = purple_conv_chat_get_topic(PURPLE_CONV_CHAT(chat->conv)); - char *buf, *tmp, *tmp2; + JabberMessage *jm; + + jm = g_new0(JabberMessage, 1); + jm->js = chat->js; + jm->type = JABBER_MESSAGE_GROUPCHAT; + jm->to = g_strdup_printf("%s@%s", chat->room, chat->server); - if(cur) { - tmp = g_markup_escape_text(cur, -1); - tmp2 = purple_markup_linkify(tmp); - buf = g_strdup_printf(_("current topic is: %s"), tmp2); - g_free(tmp); - g_free(tmp2); - } else - buf = g_strdup(_("No topic is set")); - purple_conv_chat_write(PURPLE_CONV_CHAT(chat->conv), "", buf, - PURPLE_MESSAGE_SYSTEM | PURPLE_MESSAGE_NO_LOG, time(NULL)); - g_free(buf); - } + if (topic && *topic) + jm->subject = purple_markup_strip_html(topic); + else + jm->subject = g_strdup(""); + jabber_message_send(jm); + jabber_message_free(jm); } void jabber_chat_set_topic(PurpleConnection *gc, int id, const char *topic) { - JabberStream *js = gc->proto_data; + JabberStream *js = purple_connection_get_protocol_data(gc); JabberChat *chat = jabber_chat_find_by_id(js, id); if(!chat)
--- a/libpurple/protocols/jabber/data.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/data.c Wed Apr 29 23:20:51 2009 +0000 @@ -247,4 +247,5 @@ g_hash_table_destroy(local_data_by_alt); g_hash_table_destroy(local_data_by_cid); g_hash_table_destroy(remote_data_by_cid); + local_data_by_alt = local_data_by_cid = remote_data_by_cid = NULL; }
--- a/libpurple/protocols/jabber/disco.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/disco.c Wed Apr 29 23:20:51 2009 +0000 @@ -22,21 +22,18 @@ #include "internal.h" #include "prefs.h" #include "debug.h" -#include "request.h" -#include "notify.h" -#include "libpurple/disco.h" +#include "adhoccommands.h" #include "buddy.h" +#include "disco.h" #include "google.h" #include "iq.h" -#include "disco.h" #include "jabber.h" #include "jingle/jingle.h" +#include "pep.h" #include "presence.h" #include "roster.h" -#include "pep.h" -#include "adhoccommands.h" -#include "xdata.h" +#include "useravatar.h" struct _jabber_disco_info_cb_data { gpointer data; @@ -110,7 +107,8 @@ void jabber_disco_info_parse(JabberStream *js, const char *from, JabberIqType type, const char *id, - xmlnode *in_query) { + xmlnode *in_query) +{ if(!from) return; @@ -119,6 +117,10 @@ xmlnode *query, *identity, *feature; JabberIq *iq; const char *node = xmlnode_get_attrib(in_query, "node"); + char *node_uri = NULL; + + /* create custom caps node URI */ + node_uri = g_strconcat(CAPS0115_NODE, "#", jabber_caps_get_own_hash(js), NULL); iq = jabber_iq_new_query(js, JABBER_IQ_RESULT, "http://jabber.org/protocol/disco#info"); @@ -138,90 +140,55 @@ * handheld, pc, phone, * web */ xmlnode_set_attrib(identity, "name", PACKAGE); + } - SUPPORT_FEATURE("jabber:iq:last") - SUPPORT_FEATURE("jabber:iq:oob") - SUPPORT_FEATURE("jabber:iq:time") - SUPPORT_FEATURE("urn:xmpp:time") - SUPPORT_FEATURE("jabber:iq:version") - SUPPORT_FEATURE("jabber:x:conference") - SUPPORT_FEATURE("http://jabber.org/protocol/bytestreams") - SUPPORT_FEATURE("http://jabber.org/protocol/disco#info") - SUPPORT_FEATURE("http://jabber.org/protocol/disco#items") - SUPPORT_FEATURE("http://jabber.org/protocol/ibb"); - SUPPORT_FEATURE("http://jabber.org/protocol/muc") - SUPPORT_FEATURE("http://jabber.org/protocol/muc#user") - SUPPORT_FEATURE("http://jabber.org/protocol/si") - SUPPORT_FEATURE("http://jabber.org/protocol/si/profile/file-transfer") - SUPPORT_FEATURE("http://jabber.org/protocol/xhtml-im") - SUPPORT_FEATURE("urn:xmpp:ping") - - if(!node) { /* non-caps disco#info, add all enabled extensions */ - GList *features; - for(features = jabber_features; features; features = features->next) { - JabberFeature *feat = (JabberFeature*)features->data; - if(feat->is_enabled == NULL || feat->is_enabled(js, feat->shortname, feat->namespace) == TRUE) - SUPPORT_FEATURE(feat->namespace); + if(!node || !strcmp(node, node_uri)) { + GList *features, *identities; + for(identities = jabber_identities; identities; identities = identities->next) { + JabberIdentity *ident = (JabberIdentity*)identities->data; + identity = xmlnode_new_child(query, "identity"); + xmlnode_set_attrib(identity, "category", ident->category); + xmlnode_set_attrib(identity, "type", ident->type); + if (ident->lang) + xmlnode_set_attrib(identity, "xml:lang", ident->lang); + if (ident->name) + xmlnode_set_attrib(identity, "name", ident->name); + } + for(features = jabber_features; features; features = features->next) { + JabberFeature *feat = (JabberFeature*)features->data; + if (!feat->is_enabled || feat->is_enabled(js, feat->namespace)) { + feature = xmlnode_new_child(query, "feature"); + xmlnode_set_attrib(feature, "var", feat->namespace); } } #ifdef USE_VV - } else if (node && !strcmp(node, CAPS0115_NODE "#voice-v1")) { - SUPPORT_FEATURE("http://www.google.com/xmpp/protocol/session"); - SUPPORT_FEATURE("http://www.google.com/xmpp/protocol/voice/v1"); - SUPPORT_FEATURE(JINGLE); - SUPPORT_FEATURE(JINGLE_APP_RTP_SUPPORT_AUDIO); - SUPPORT_FEATURE(JINGLE_APP_RTP_SUPPORT_VIDEO); - SUPPORT_FEATURE(JINGLE_TRANSPORT_RAWUDP); - SUPPORT_FEATURE(JINGLE_TRANSPORT_ICEUDP); + } else if (g_str_equal(node, CAPS0115_NODE "#" "voice-v1")) { + /* + * HUGE HACK! We advertise this ext (see jabber_presence_create_js + * where we add <c/> to the <presence/>) for the Google Talk + * clients that don't actually check disco#info features. + * + * This specific feature is redundant but is what + * node='http://mail.google.com/xmpp/client/caps', ver='1.1' + * advertises as 'voice-v1'. + */ + xmlnode *feature = xmlnode_new_child(query, "feature"); + xmlnode_set_attrib(feature, "var", "http://www.google.com/xmpp/protocol/voice/v1"); #endif } else { - const char *ext = NULL; - unsigned pos; - unsigned nodelen = strlen(node); - unsigned capslen = strlen(CAPS0115_NODE); - /* do a basic plausability check */ - if(nodelen > capslen+1) { - /* verify that the string is CAPS0115#<ext> and get the pointer to the ext part */ - for(pos = 0; pos < capslen+1; ++pos) { - if(pos == capslen) { - if(node[pos] == '#') - ext = &node[pos+1]; - else - break; - } else if(node[pos] != CAPS0115_NODE[pos]) - break; - } + xmlnode *error, *inf; + + /* XXX: gross hack, implement jabber_iq_set_type or something */ + xmlnode_set_attrib(iq->node, "type", "error"); + iq->type = JABBER_IQ_ERROR; - if(ext != NULL) { - /* look for that ext */ - GList *features; - for(features = jabber_features; features; features = features->next) { - JabberFeature *feat = (JabberFeature*)features->data; - if(!strcmp(feat->shortname, ext)) { - SUPPORT_FEATURE(feat->namespace); - break; - } - } - if(features == NULL) - ext = NULL; - } - } - - if(ext == NULL) { - xmlnode *error, *inf; - - /* XXX: gross hack, implement jabber_iq_set_type or something */ - xmlnode_set_attrib(iq->node, "type", "error"); - iq->type = JABBER_IQ_ERROR; - - error = xmlnode_new_child(query, "error"); - xmlnode_set_attrib(error, "code", "404"); - xmlnode_set_attrib(error, "type", "cancel"); - inf = xmlnode_new_child(error, "item-not-found"); - xmlnode_set_namespace(inf, "urn:ietf:params:xml:ns:xmpp-stanzas"); - } + error = xmlnode_new_child(query, "error"); + xmlnode_set_attrib(error, "code", "404"); + xmlnode_set_attrib(error, "type", "cancel"); + inf = xmlnode_new_child(error, "item-not-found"); + xmlnode_set_namespace(inf, "urn:ietf:params:xml:ns:xmpp-stanzas"); } - + g_free(node_uri); jabber_iq_send(iq); } else if (type == JABBER_IQ_SET) { /* wtf? seriously. wtf‽ */ @@ -259,6 +226,7 @@ JabberBuddy *jb; JabberBuddyResource *jbr = NULL; JabberCapabilities capabilities = JABBER_CAP_NONE; + struct _jabber_disco_info_cb_data *jdicd; if((jid = jabber_id_new(from))) { if(jid->resource && (jb = jabber_buddy_find(js, from, TRUE))) @@ -360,7 +328,8 @@ void jabber_disco_items_parse(JabberStream *js, const char *from, JabberIqType type, const char *id, - xmlnode *query) { + xmlnode *query) +{ if(type == JABBER_IQ_GET) { JabberIq *iq = jabber_iq_new_query(js, JABBER_IQ_RESULT, "http://jabber.org/protocol/disco#items"); @@ -384,16 +353,21 @@ { const char *ft_proxies; + /* + * This *should* happen only if the server supports vcard-temp, but there + * are apparently some servers that don't advertise it even though they + * support it. + */ jabber_vcard_fetch_mine(js); + if (js->pep) + jabber_avatar_fetch_mine(js); + if (!(js->server_caps & JABBER_CAP_GOOGLE_ROSTER)) { /* If the server supports JABBER_CAP_GOOGLE_ROSTER; we will have already requested it */ jabber_roster_request(js); } - /* Send initial presence; this will trigger receipt of presence for contacts on the roster */ - jabber_presence_send(js->gc->account, NULL); - if (js->server_caps & JABBER_CAP_ADHOC) { /* The server supports ad-hoc commands, so let's request the list */ jabber_adhoc_server_get_list(js);
--- a/libpurple/protocols/jabber/jabber.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/jabber.c Wed Apr 29 23:20:51 2009 +0000 @@ -28,6 +28,7 @@ #include "conversation.h" #include "debug.h" #include "dnssrv.h" +#include "imgstore.h" #include "message.h" #include "notify.h" #include "pluginpref.h" @@ -36,12 +37,14 @@ #include "prpl.h" #include "request.h" #include "server.h" +#include "status.h" #include "util.h" #include "version.h" #include "xmlnode.h" #include "auth.h" #include "buddy.h" +#include "caps.h" #include "chat.h" #include "data.h" #include "disco.h" @@ -66,6 +69,7 @@ static PurplePlugin *my_protocol = NULL; GList *jabber_features = NULL; +GList *jabber_identities = NULL; static void jabber_unregister_account_cb(JabberStream *js); static void try_srv_connect(JabberStream *js); @@ -79,6 +83,10 @@ "xmlns:stream='http://etherx.jabber.org/streams' " "version='1.0'>", js->user->domain); + if (js->reinit) + /* Close down the current stream to keep the XML parser happy */ + jabber_parser_close_stream(js); + /* setup the parser fresh for each stream */ jabber_parser_setup(js); jabber_send_raw(js, open_stream, -1); @@ -92,7 +100,7 @@ xmlnode *packet, gpointer data) { if (type == JABBER_IQ_RESULT) { - jabber_stream_set_state(js, JABBER_STREAM_CONNECTED); + jabber_disco_items_server(js); if(js->unregistration) jabber_unregister_account_cb(js); } else { @@ -183,13 +191,13 @@ return purple_strreplace(input, "__HOSTNAME__", hostname); } -static void jabber_stream_features_parse(JabberStream *js, xmlnode *packet) +void jabber_stream_features_parse(JabberStream *js, xmlnode *packet) { if(xmlnode_get_child(packet, "starttls")) { if(jabber_process_starttls(js, packet)) return; - } else if(purple_account_get_bool(js->gc->account, "require_tls", FALSE) && !js->gsc) { + } else if(purple_account_get_bool(js->gc->account, "require_tls", FALSE) && !jabber_stream_is_ssl(js)) { purple_connection_error_reason (js->gc, PURPLE_CONNECTION_ERROR_ENCRYPTION_ERROR, _("You require encryption, but it is not available on this server.")); @@ -382,7 +390,7 @@ } purple_debug(PURPLE_DEBUG_MISC, "jabber", "Sending%s: %s%s%s\n", - js->gsc ? " (ssl)" : "", text ? text : data, + jabber_stream_is_ssl(js) ? " (ssl)" : "", text ? text : data, last_part ? "password removed" : "", last_part ? last_part : ""); @@ -423,7 +431,13 @@ } #endif - do_jabber_send_raw(js, data, len); + if (len == -1) + len = strlen(data); + + if (js->use_bosh) + jabber_bosh_connection_send_raw(js->bosh, data); + else + do_jabber_send_raw(js, data, len); } int jabber_prpl_send_raw(PurpleConnection *gc, const char *buf, int len) @@ -575,6 +589,49 @@ jabber_stream_set_state(js, JABBER_STREAM_INITIALIZING_ENCRYPTION); } +static void +txt_resolved_cb(GSList *responses, gpointer data) +{ + JabberStream *js = data; + + js->srv_query_data = NULL; + + if (responses == NULL) { + gchar *tmp; + tmp = g_strdup_printf(_("Could not find alternative XMPP connection methods after failing to connect directly.\n")); + purple_connection_error_reason (js->gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp); + g_free(tmp); + return; + } + + while (responses) { + PurpleTxtResponse *resp = responses->data; + gchar **token; + token = g_strsplit(purple_txt_response_get_content(resp), "=", 2); + if (!strcmp(token[0], "_xmpp-client-xbosh")) { + purple_debug_info("jabber","Found alternative connection method using %s at %s.\n", token[0], token[1]); + js->bosh = jabber_bosh_connection_init(js, token[1]); + js->use_bosh = TRUE; + g_strfreev(token); + break; + } + g_strfreev(token); + purple_txt_response_destroy(resp); + responses = g_slist_delete_link(responses, responses); + } + + if (js->bosh) { + jabber_bosh_connection_connect(js->bosh); + } else { + purple_debug_info("jabber","Didn't find an alternative connection method.\n"); + } + + if (responses) { + g_slist_foreach(responses, (GFunc)purple_txt_response_destroy, NULL); + g_slist_free(responses); + } +} static void jabber_login_callback(gpointer data, gint source, const gchar *error) @@ -587,12 +644,8 @@ purple_debug_error("jabber", "Unable to connect to server: %s. Trying next SRV record.\n", error); try_srv_connect(js); } else { - gchar *tmp; - tmp = g_strdup_printf(_("Could not establish a connection with the server:\n%s"), - error); - purple_connection_error_reason(gc, - PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp); - g_free(tmp); + purple_debug_info("jabber","Couldn't connect directly to %s. Trying to find alternative connection methods, like BOSH.\n", js->user->domain); + js->srv_query_data = purple_txt_resolve("_xmppconnect", js->user->domain, txt_resolved_cb, js); } return; } @@ -628,6 +681,9 @@ static void tls_init(JabberStream *js) { + /* Close down the current stream to keep the XML parser happy */ + jabber_parser_close_stream(js); + purple_input_remove(js->gc->inpa); js->gc->inpa = 0; js->gsc = purple_ssl_connect_with_host_fd(js->gc->account, js->fd, @@ -700,6 +756,8 @@ const char *connect_server = purple_account_get_string(account, "connect_server", ""); JabberStream *js; + PurplePresence *presence; + PurpleStoredImage *image; JabberBuddy *my_jb = NULL; gc->flags |= PURPLE_CONNECTION_HTML | @@ -718,12 +776,20 @@ js->write_buffer = purple_circ_buffer_new(512); js->old_length = 0; js->keepalive_timeout = -1; - js->certificate_CN = g_strdup(connect_server[0] ? connect_server : js->user ? js->user->domain : NULL); + /* Set the default protocol version to 1.0. Overridden in parser.c. */ + js->protocol_version = JABBER_PROTO_1_0; js->sessions = NULL; js->stun_ip = NULL; js->stun_port = 0; js->stun_query = NULL; + /* if we are idle, set idle-ness on the stream (this could happen if we get + disconnected and the reconnects while being idle. I don't think it makes + sense to do this when registering a new account... */ + presence = purple_account_get_presence(account); + if (purple_presence_is_idle(presence)) + js->idle = purple_presence_get_idle_time(presence); + if(!js->user) { purple_connection_error_reason (gc, PURPLE_CONNECTION_ERROR_INVALID_SETTINGS, @@ -738,11 +804,38 @@ return; } + /* + * Calculate the avatar hash for our current image so we know (when we + * fetch our vCard and PEP avatar) if we should send our avatar to the + * server. + */ + if ((image = purple_buddy_icons_find_account_icon(account))) { + js->initial_avatar_hash = jabber_calculate_data_sha1sum(purple_imgstore_get_data(image), + purple_imgstore_get_size(image)); + purple_imgstore_unref(image); + } + if((my_jb = jabber_buddy_find(js, purple_account_get_username(account), TRUE))) my_jb->subscription |= JABBER_SUB_BOTH; jabber_stream_set_state(js, JABBER_STREAM_CONNECTING); + /* TODO: Just use purple_url_parse? */ + if (!g_ascii_strncasecmp(connect_server, "http://", 7) || !g_ascii_strncasecmp(connect_server, "https://", 8)) { + js->use_bosh = TRUE; + js->bosh = jabber_bosh_connection_init(js, connect_server); + if (!js->bosh) { + purple_connection_error_reason (js->gc, + PURPLE_CONNECTION_ERROR_INVALID_SETTINGS, + _("Malformed BOSH Connect Server")); + return; + } + jabber_bosh_connection_connect(js->bosh); + return; + } else { + js->certificate_CN = g_strdup(connect_server[0] ? connect_server : js->user->domain); + } + /* if they've got old-ssl mode going, we probably want to ignore SRV lookups */ if(purple_account_get_bool(js->gc->account, "old_ssl", FALSE)) { if(purple_ssl_is_supported()) { @@ -750,22 +843,27 @@ js->certificate_CN, purple_account_get_int(account, "port", 5223), jabber_login_callback_ssl, jabber_ssl_connect_failure, js->gc); + if (!js->gsc) { + purple_connection_error_reason (js->gc, + PURPLE_CONNECTION_ERROR_NO_SSL_SUPPORT, + _("Unable to establish SSL connection")); + } } else { purple_connection_error_reason (js->gc, PURPLE_CONNECTION_ERROR_NO_SSL_SUPPORT, _("SSL support unavailable")); } + + return; } /* no old-ssl, so if they've specified a connect server, we'll use that, otherwise we'll * invoke the magic of SRV lookups, to figure out host and port */ - if(!js->gsc) { - if(connect_server[0]) { - jabber_login_connect(js, js->user->domain, connect_server, purple_account_get_int(account, "port", 5222), TRUE); - } else { - js->srv_query_data = purple_srv_resolve("xmpp-client", - "tcp", js->user->domain, srv_resolved_cb, js); - } + if(connect_server[0]) { + jabber_login_connect(js, js->user->domain, connect_server, purple_account_get_int(account, "port", 5222), TRUE); + } else { + js->srv_query_data = purple_srv_resolve("xmpp-client", + "tcp", js->user->domain, srv_resolved_cb, js); } } @@ -1220,30 +1318,51 @@ jabber_stream_set_state(js, JABBER_STREAM_CONNECTING); + /* TODO: Just use purple_url_parse? */ + if (!g_ascii_strncasecmp(connect_server, "http://", 7) || !g_ascii_strncasecmp(connect_server, "https://", 8)) { + js->use_bosh = TRUE; + js->bosh = jabber_bosh_connection_init(js, connect_server); + if (!js->bosh) { + purple_connection_error_reason (js->gc, + PURPLE_CONNECTION_ERROR_INVALID_SETTINGS, + _("Malformed BOSH Connect Server")); + return; + } + jabber_bosh_connection_connect(js->bosh); + return; + } else { + js->certificate_CN = g_strdup(connect_server[0] ? connect_server : js->user->domain); + } + if(purple_account_get_bool(account, "old_ssl", FALSE)) { if(purple_ssl_is_supported()) { js->gsc = purple_ssl_connect(account, server, purple_account_get_int(account, "port", 5222), jabber_login_callback_ssl, jabber_ssl_connect_failure, gc); + if (!js->gsc) { + purple_connection_error_reason (js->gc, + PURPLE_CONNECTION_ERROR_NO_SSL_SUPPORT, + _("Unable to establish SSL connection")); + } } else { purple_connection_error_reason (gc, PURPLE_CONNECTION_ERROR_NO_SSL_SUPPORT, _("SSL support unavailable")); } + + return; } - if(!js->gsc) { - if (connect_server[0]) { - jabber_login_connect(js, js->user->domain, server, - purple_account_get_int(account, - "port", 5222), TRUE); - } else { - js->srv_query_data = purple_srv_resolve("xmpp-client", - "tcp", - js->user->domain, - srv_resolved_cb, - js); - } + if (connect_server[0]) { + jabber_login_connect(js, js->user->domain, server, + purple_account_get_int(account, + "port", 5222), TRUE); + } else { + js->srv_query_data = purple_srv_resolve("xmpp-client", + "tcp", + js->user->domain, + srv_resolved_cb, + js); } } @@ -1315,6 +1434,11 @@ jabber_unregister_account_cb(js); } +/* TODO: As Will pointed out in IRC, after being notified by the core to + * shutdown, we should async. wait for the server to send us the stream + * termination before destorying everything. That seems like it would require + * changing the semantics of prpl->close(), so it's a good idea for 3.0.0. + */ void jabber_close(PurpleConnection *gc) { JabberStream *js = gc->proto_data; @@ -1326,8 +1450,12 @@ * if we were forcibly disconnected because it will crash * on some SSL backends. */ - if (!gc->disconnect_timeout) - jabber_send_raw(js, "</stream:stream>", -1); + if (!gc->disconnect_timeout) { + if (js->use_bosh) + jabber_bosh_connection_close(js->bosh); + else + jabber_send_raw(js, "</stream:stream>", -1); + } if (js->srv_query_data) purple_srv_cancel(js->srv_query_data); @@ -1343,6 +1471,9 @@ close(js->fd); } + if (js->bosh) + jabber_bosh_connection_destroy(js->bosh); + jabber_buddy_remove_all_pending_buddy_info_requests(js); jabber_parser_free(js); @@ -1381,7 +1512,9 @@ g_free(js->stream_id); if(js->user) jabber_id_free(js->user); + g_free(js->initial_avatar_hash); g_free(js->avatar_hash); + g_free(js->caps_hash); purple_circ_buffer_destroy(js->write_buffer); if(js->writeh) @@ -1484,7 +1617,6 @@ case JABBER_STREAM_CONNECTED: /* now we can alert the core that we're ready to send status */ purple_connection_set_state(js->gc, PURPLE_CONNECTED); - jabber_disco_items_server(js); break; } } @@ -1500,6 +1632,10 @@ JabberStream *js = gc->proto_data; js->idle = idle ? time(NULL) - idle : idle; + + /* send out an updated prescence */ + purple_debug_info("jabber", "sending updated presence for idle\n"); + jabber_presence_send(js, FALSE); } static void jabber_blocklist_parse(JabberStream *js, const char *from, @@ -1604,31 +1740,27 @@ jabber_iq_send(iq); } -void jabber_add_feature(const char *shortname, const char *namespace, JabberFeatureEnabled cb) { +void jabber_add_feature(const char *namespace, JabberFeatureEnabled cb) { JabberFeature *feat; - g_return_if_fail(shortname != NULL); g_return_if_fail(namespace != NULL); feat = g_new0(JabberFeature,1); - feat->shortname = g_strdup(shortname); feat->namespace = g_strdup(namespace); feat->is_enabled = cb; /* try to remove just in case it already exists in the list */ - jabber_remove_feature(shortname); + jabber_remove_feature(namespace); jabber_features = g_list_append(jabber_features, feat); } -void jabber_remove_feature(const char *shortname) { +void jabber_remove_feature(const char *namespace) { GList *feature; for(feature = jabber_features; feature; feature = feature->next) { JabberFeature *feat = (JabberFeature*)feature->data; - if(!strcmp(feat->shortname, shortname)) { - g_free(feat->shortname); + if(!strcmp(feat->namespace, namespace)) { g_free(feat->namespace); - g_free(feature->data); jabber_features = g_list_delete_link(jabber_features, feature); break; @@ -1636,6 +1768,59 @@ } } +static void jabber_features_destroy(void) +{ + while (jabber_features) { + JabberFeature *feature = jabber_features->data; + g_free(feature->namespace); + g_free(feature); + jabber_features = g_list_remove_link(jabber_features, jabber_features); + } +} + +void jabber_add_identity(const gchar *category, const gchar *type, const gchar *lang, const gchar *name) { + GList *identity; + JabberIdentity *ident; + /* both required according to XEP-0030 */ + g_return_if_fail(category != NULL); + g_return_if_fail(type != NULL); + + for(identity = jabber_identities; identity; identity = identity->next) { + JabberIdentity *ident = (JabberIdentity*)identity->data; + if (!strcmp(ident->category, category) && + !strcmp(ident->type, type) && + ((!ident->lang && !lang) || (ident->lang && lang && !strcmp(ident->lang, lang)))) { + return; + } + } + + ident = g_new0(JabberIdentity, 1); + ident->category = g_strdup(category); + ident->type = g_strdup(type); + ident->lang = g_strdup(lang); + ident->name = g_strdup(name); + jabber_identities = g_list_append(jabber_identities, ident); +} + +static void jabber_identities_destroy(void) +{ + while (jabber_identities) { + JabberIdentity *id = jabber_identities->data; + g_free(id->category); + g_free(id->type); + g_free(id->lang); + g_free(id->name); + g_free(id); + jabber_identities = g_list_remove_link(jabber_identities, jabber_identities); + } +} + +gboolean jabber_stream_is_ssl(JabberStream *js) +{ + return (js->bosh && jabber_bosh_connection_is_ssl(js->bosh)) || + (!js->bosh && js->gsc); +} + const char *jabber_list_icon(PurpleAccount *a, PurpleBuddy *b) { return "jabber"; @@ -1677,10 +1862,11 @@ } else if(jb && !PURPLE_BUDDY_IS_ONLINE(b) && jb->error_msg) { ret = g_strdup(jb->error_msg); } else { + PurplePresence *presence = purple_buddy_get_presence(b); + PurpleStatus *status =purple_presence_get_active_status(presence); char *stripped; - if(!(stripped = purple_markup_strip_html(jabber_buddy_get_status_msg(jb)))) { - PurplePresence *presence = purple_buddy_get_presence(b); + if(!(stripped = purple_markup_strip_html(purple_status_get_attr_string(status, "message")))) { if (purple_presence_is_status_primitive_active(presence, PURPLE_STATUS_TUNE)) { PurpleStatus *status = purple_presence_get_status(presence, "tune"); stripped = g_strdup(purple_status_get_attr_string(status, PURPLE_TUNE_TITLE)); @@ -1696,6 +1882,56 @@ return ret; } +static void +jabber_tooltip_add_resource_text(JabberBuddyResource *jbr, + PurpleNotifyUserInfo *user_info, gboolean multiple_resources) +{ + char *text = NULL; + char *res = NULL; + char *label, *value; + const char *state; + + if(jbr->status) { + char *tmp; + text = purple_strreplace(jbr->status, "\n", "<br />\n"); + tmp = purple_markup_strip_html(text); + g_free(text); + text = g_markup_escape_text(tmp, -1); + g_free(tmp); + } + + if(jbr->name) + res = g_strdup_printf(" (%s)", jbr->name); + + state = jabber_buddy_state_get_name(jbr->state); + if (text != NULL && !purple_utf8_strcasecmp(state, text)) { + g_free(text); + text = NULL; + } + + label = g_strdup_printf("%s%s", _("Status"), (res ? res : "")); + value = g_strdup_printf("%s%s%s", state, (text ? ": " : ""), (text ? text : "")); + + purple_notify_user_info_add_pair(user_info, label, value); + g_free(label); + g_free(value); + g_free(text); + + /* if the resource is idle, show that */ + /* only show it if there is more than one resource available for + the buddy, since the "general" idleness will be shown anyway, + this way we can see see the idleness of lower-priority resources */ + if (jbr->idle && multiple_resources) { + gchar *idle_str = + purple_str_seconds_to_string(time(NULL) - jbr->idle); + label = g_strdup_printf("%s%s", _("Idle"), (res ? res : "")); + purple_notify_user_info_add_pair(user_info, label, idle_str); + g_free(idle_str); + g_free(label); + } + g_free(res); +} + void jabber_tooltip_text(PurpleBuddy *b, PurpleNotifyUserInfo *user_info, gboolean full) { JabberBuddy *jb; @@ -1719,28 +1955,28 @@ const char *sub; GList *l; const char *mood; - + gboolean multiple_resources = + jb->resources && g_list_next(jb->resources); + JabberBuddyResource *top_jbr = jabber_buddy_find_resource(jb, NULL); + + /* resource-specific info for the top resource */ + if (top_jbr) { + jabber_tooltip_add_resource_text(top_jbr, user_info, + multiple_resources); + } + + for(l=jb->resources; l; l = l->next) { + jbr = l->data; + /* the remaining resources */ + if (jbr != top_jbr) { + jabber_tooltip_add_resource_text(jbr, user_info, + multiple_resources); + } + } + if (full) { PurpleStatus *status; - if(jb->subscription & JABBER_SUB_FROM) { - if(jb->subscription & JABBER_SUB_TO) - sub = _("Both"); - else if(jb->subscription & JABBER_SUB_PENDING) - sub = _("From (To pending)"); - else - sub = _("From"); - } else { - if(jb->subscription & JABBER_SUB_TO) - sub = _("To"); - else if(jb->subscription & JABBER_SUB_PENDING) - sub = _("None (To pending)"); - else - sub = _("None"); - } - - purple_notify_user_info_add_pair(user_info, _("Subscription"), sub); - status = purple_presence_get_active_status(presence); mood = purple_status_get_attr_string(status, "mood"); if(mood != NULL) { @@ -1765,47 +2001,25 @@ g_free(playing); } } - } - - for(l=jb->resources; l; l = l->next) { - char *text = NULL; - char *res = NULL; - char *label, *value; - const char *state; - - jbr = l->data; - - if(jbr->status) { - char *tmp; - text = purple_strreplace(jbr->status, "\n", "<br />\n"); - tmp = purple_markup_strip_html(text); - g_free(text); - text = g_markup_escape_text(tmp, -1); - g_free(tmp); + + if(jb->subscription & JABBER_SUB_FROM) { + if(jb->subscription & JABBER_SUB_TO) + sub = _("Both"); + else if(jb->subscription & JABBER_SUB_PENDING) + sub = _("From (To pending)"); + else + sub = _("From"); + } else { + if(jb->subscription & JABBER_SUB_TO) + sub = _("To"); + else if(jb->subscription & JABBER_SUB_PENDING) + sub = _("None (To pending)"); + else + sub = _("None"); } - if(jbr->name) - res = g_strdup_printf(" (%s)", jbr->name); - - state = jabber_buddy_state_get_name(jbr->state); - if (text != NULL && !purple_utf8_strcasecmp(state, text)) { - g_free(text); - text = NULL; - } - - label = g_strdup_printf("%s%s", - _("Status"), (res ? res : "")); - value = g_strdup_printf("%s%s%s", - state, - (text ? ": " : ""), - (text ? text : "")); - - purple_notify_user_info_add_pair(user_info, label, value); - - g_free(label); - g_free(value); - g_free(text); - g_free(res); + purple_notify_user_info_add_pair(user_info, _("Subscription"), sub); + } if(!PURPLE_BUDDY_IS_ONLINE(b) && jb->error_msg) { @@ -2298,7 +2512,25 @@ if (!chat) return PURPLE_CMD_RET_FAILED; - jabber_chat_change_topic(chat, args ? args[0] : NULL); + if (args && args[0] && *args[0]) + jabber_chat_change_topic(chat, args[0]); + else { + const char *cur = purple_conv_chat_get_topic(PURPLE_CONV_CHAT(conv)); + char *buf, *tmp, *tmp2; + + if (cur) { + tmp = g_markup_escape_text(cur, -1); + tmp2 = purple_markup_linkify(tmp); + buf = g_strdup_printf(_("current topic is: %s"), tmp2); + g_free(tmp); + g_free(tmp2); + } else + buf = g_strdup(_("No topic is set")); + purple_conv_chat_write(PURPLE_CONV_CHAT(conv), "", buf, + PURPLE_MESSAGE_SYSTEM | PURPLE_MESSAGE_NO_LOG, time(NULL)); + g_free(buf); + } + return PURPLE_CMD_RET_OK; } @@ -2606,6 +2838,24 @@ } #ifdef USE_VV +gboolean +jabber_audio_enabled(JabberStream *js, const char *namespace) +{ + PurpleMediaManager *manager = purple_media_manager_get(); + PurpleMediaCaps caps = purple_media_manager_get_ui_caps(manager); + + return (caps & (PURPLE_MEDIA_CAPS_AUDIO | PURPLE_MEDIA_CAPS_AUDIO_SINGLE_DIRECTION)); +} + +static gboolean +jabber_video_enabled(JabberStream *js, const char *namespace) +{ + PurpleMediaManager *manager = purple_media_manager_get(); + PurpleMediaCaps caps = purple_media_manager_get_ui_caps(manager); + + return (caps & (PURPLE_MEDIA_CAPS_VIDEO | PURPLE_MEDIA_CAPS_VIDEO_SINGLE_DIRECTION)); +} + typedef struct { PurpleAccount *account; gchar *who; @@ -2961,8 +3211,109 @@ _("buzz: Buzz a user to get their attention"), NULL); } +/* IPC functions */ + +/** + * IPC function for determining if a contact supports a certain feature. + * + * @param account The PurpleAccount + * @param jid The full JID of the contact. + * @param feature The feature's namespace. + * + * @return TRUE if supports feature; else FALSE. + */ +static gboolean +jabber_ipc_contact_has_feature(PurpleAccount *account, const gchar *jid, + const gchar *feature) +{ + PurpleConnection *gc = purple_account_get_connection(account); + JabberStream *js; + JabberBuddy *jb; + JabberBuddyResource *jbr; + gchar *resource; + + if (!purple_account_is_connected(account)) + return FALSE; + js = gc->proto_data; + + if (!(resource = jabber_get_resource(jid)) || + !(jb = jabber_buddy_find(js, jid, FALSE)) || + !(jbr = jabber_buddy_find_resource(jb, resource))) { + g_free(resource); + return FALSE; + } + + g_free(resource); + + return jabber_resource_has_capability(jbr, feature); +} + +static void +jabber_ipc_add_feature(const gchar *feature) +{ + if (!feature) + return; + jabber_add_feature(feature, 0); + + /* send presence with new caps info for all connected accounts */ + jabber_caps_broadcast_change(); +} + void jabber_init_plugin(PurplePlugin *plugin) { - my_protocol = plugin; + my_protocol = plugin; + + jabber_add_identity("client", "pc", NULL, PACKAGE); + + /* initialize jabber_features list */ + jabber_add_feature("jabber:iq:last", 0); + jabber_add_feature("jabber:iq:oob", 0); + jabber_add_feature("jabber:iq:time", 0); + jabber_add_feature("urn:xmpp:time", 0); + jabber_add_feature("jabber:iq:version", 0); + jabber_add_feature("jabber:x:conference", 0); + jabber_add_feature("http://jabber.org/protocol/bytestreams", 0); + jabber_add_feature("http://jabber.org/protocol/caps", 0); + jabber_add_feature("http://jabber.org/protocol/chatstates", 0); + jabber_add_feature("http://jabber.org/protocol/disco#info", 0); + jabber_add_feature("http://jabber.org/protocol/disco#items", 0); + jabber_add_feature("http://jabber.org/protocol/ibb", 0); + jabber_add_feature("http://jabber.org/protocol/muc", 0); + jabber_add_feature("http://jabber.org/protocol/muc#user", 0); + jabber_add_feature("http://jabber.org/protocol/si", 0); + jabber_add_feature("http://jabber.org/protocol/si/profile/file-transfer", 0); + jabber_add_feature("http://jabber.org/protocol/xhtml-im", 0); + jabber_add_feature("urn:xmpp:ping", 0); + + /* Jingle features! */ + jabber_add_feature(JINGLE, 0); + jabber_add_feature(JINGLE_TRANSPORT_RAWUDP, 0); + +#ifdef USE_VV + jabber_add_feature("http://www.google.com/xmpp/protocol/session", jabber_audio_enabled); + jabber_add_feature("http://www.google.com/xmpp/protocol/voice/v1", jabber_audio_enabled); + jabber_add_feature(JINGLE_APP_RTP_SUPPORT_AUDIO, jabber_audio_enabled); + jabber_add_feature(JINGLE_APP_RTP_SUPPORT_VIDEO, jabber_video_enabled); + jabber_add_feature(JINGLE_TRANSPORT_ICEUDP, 0); +#endif + + /* IPC functions */ + purple_plugin_ipc_register(plugin, "contact_has_feature", PURPLE_CALLBACK(jabber_ipc_contact_has_feature), + purple_marshal_BOOLEAN__POINTER_POINTER_POINTER, + purple_value_new(PURPLE_TYPE_BOOLEAN), 3, + purple_value_new(PURPLE_TYPE_SUBTYPE, PURPLE_SUBTYPE_ACCOUNT), + purple_value_new(PURPLE_TYPE_STRING), + purple_value_new(PURPLE_TYPE_STRING)); + purple_plugin_ipc_register(plugin, "add_feature", PURPLE_CALLBACK(jabber_ipc_add_feature), + purple_marshal_VOID__POINTER, + NULL, 1, + purple_value_new(PURPLE_TYPE_STRING)); } + +void +jabber_uninit_plugin(void) +{ + jabber_features_destroy(); + jabber_identities_destroy(); +}
--- a/libpurple/protocols/jabber/jabber.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/jabber.h Wed Apr 29 23:20:51 2009 +0000 @@ -66,12 +66,13 @@ #include "jutil.h" #include "xmlnode.h" #include "buddy.h" +#include "bosh.h" #ifdef HAVE_CYRUS_SASL #include <sasl/sasl.h> #endif -#define CAPS0115_NODE "http://pidgin.im/caps" +#define CAPS0115_NODE "http://pidgin.im/" /* Index into attention_types list */ #define JABBER_BUZZ 0 @@ -159,6 +160,7 @@ GList *file_transfers; time_t idle; + time_t old_idle; JabberID *user; PurpleConnection *gc; @@ -166,6 +168,7 @@ gboolean registration; + char *initial_avatar_hash; char *avatar_hash; GSList *pending_avatar_requests; @@ -211,6 +214,9 @@ gboolean vcard_fetched; + /* Entity Capabilities hash */ + char *caps_hash; + /* does the local server support PEP? */ gboolean pep; @@ -238,10 +244,15 @@ /* A purple timeout tag for the keepalive */ int keepalive_timeout; - + PurpleSrvResponse *srv_rec; guint srv_rec_idx; guint max_srv_rec_idx; + + /* BOSH stuff */ + gboolean use_bosh; + PurpleBOSHConnection *bosh; + /** * This linked list contains PurpleUtilFetchUrlData structs * for when we lookup buddy icons from a url @@ -264,15 +275,22 @@ char *last_disco_server; }; -typedef gboolean (JabberFeatureEnabled)(JabberStream *js, const gchar *shortname, const gchar *namespace); +typedef gboolean (JabberFeatureEnabled)(JabberStream *js, const gchar *namespace); typedef struct _JabberFeature { - gchar *shortname; gchar *namespace; JabberFeatureEnabled *is_enabled; } JabberFeature; +typedef struct _JabberIdentity +{ + gchar *category; + gchar *type; + gchar *name; + gchar *lang; +} JabberIdentity; + typedef struct _JabberBytestreamsStreamhost { char *jid; char *host; @@ -282,7 +300,9 @@ /* what kind of additional features as returned from disco#info are supported? */ extern GList *jabber_features; +extern GList *jabber_identities; +void jabber_stream_features_parse(JabberStream *js, xmlnode *packet); void jabber_process_packet(JabberStream *js, xmlnode **packet); void jabber_send(JabberStream *js, xmlnode *data); void jabber_send_raw(JabberStream *js, const char *data, int len); @@ -304,8 +324,24 @@ */ char *jabber_parse_error(JabberStream *js, xmlnode *packet, PurpleConnectionError *reason); -void jabber_add_feature(const gchar *shortname, const gchar *namespace, JabberFeatureEnabled cb); /* cb may be NULL */ -void jabber_remove_feature(const gchar *shortname); +void jabber_add_feature(const gchar *namespace, JabberFeatureEnabled cb); /* cb may be NULL */ +void jabber_remove_feature(const gchar *namespace); + +/** Adds an identitiy to this jabber library instance. For list of valid values vistit the + * webiste of the XMPP Registrar ( http://www.xmpp.org/registrar/disco-categories.html#client ). + * @param category the category of the identity. + * @param type the type of the identity. + * @param language the language localization of the name. Can be NULL. + * @param name the name of the identity. + */ +void jabber_add_identity(const gchar *category, const gchar *type, const gchar *lang, const gchar *name); + +/** + * Returns true if this connection is over a secure (SSL) stream. Use this + * instead of checking js->gsc because BOSH stores its PurpleSslConnection + * members in its own data structure. + */ +gboolean jabber_stream_is_ssl(JabberStream *js); /** PRPL functions */ const char *jabber_list_icon(PurpleAccount *a, PurpleBuddy *b); @@ -330,10 +366,15 @@ gboolean jabber_offline_message(const PurpleBuddy *buddy); int jabber_prpl_send_raw(PurpleConnection *gc, const char *buf, int len); GList *jabber_actions(PurplePlugin *plugin, gpointer context); + +gboolean jabber_audio_enabled(JabberStream *js, const char *unused); gboolean jabber_initiate_media(PurpleAccount *account, const char *who, PurpleMediaSessionType type); PurpleMediaCaps jabber_get_media_caps(PurpleAccount *account, const char *who); + void jabber_register_commands(void); + void jabber_init_plugin(PurplePlugin *plugin); +void jabber_uninit_plugin(void); #endif /* PURPLE_JABBER_H_ */
--- a/libpurple/protocols/jabber/jingle/content.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/jingle/content.c Wed Apr 29 23:20:51 2009 +0000 @@ -391,7 +391,13 @@ jingle_content_parse(xmlnode *content) { const gchar *type = xmlnode_get_namespace(xmlnode_get_child(content, "description")); - return JINGLE_CONTENT_CLASS(g_type_class_ref(jingle_get_type(type)))->parse(content); + GType jingle_type = jingle_get_type(type); + + if (jingle_type != G_TYPE_NONE) { + return JINGLE_CONTENT_CLASS(g_type_class_ref(jingle_type))->parse(content); + } else { + return NULL; + } } static xmlnode *
--- a/libpurple/protocols/jabber/jingle/session.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/jingle/session.c Wed Apr 29 23:20:51 2009 +0000 @@ -363,17 +363,18 @@ g_hash_table_lookup(js->sessions, sid) : NULL; } +#if GLIB_CHECK_VERSION(2,4,0) static gboolean find_by_jid_ghr(gpointer key, gpointer value, gpointer user_data) { JingleSession *session = (JingleSession *)value; const gchar *jid = user_data; - gboolean use_bare = strchr(jid, '/') == NULL; + gboolean use_bare = g_utf8_strchr(jid, -1, '/') == NULL; gchar *remote_jid = jingle_session_get_remote_jid(session); gchar *cmp_jid = use_bare ? jabber_get_bare_jid(remote_jid) : g_strdup(remote_jid); g_free(remote_jid); - if (!strcmp(jid, cmp_jid)) { + if (g_str_equal(jid, cmp_jid)) { g_free(cmp_jid); return TRUE; } @@ -382,12 +383,58 @@ return FALSE; } +#else /* GLIB_CHECK_VERSION 2.4.0 */ + +/* Ugly code; g_hash_table_find version above is much nicer */ +struct session_find_jid +{ + const gchar *jid; + JingleSession *ret; + gboolean use_bare; +}; + +static void find_by_jid_ghr(gpointer key, gpointer value, gpointer user_data) +{ + JingleSession *session = (JingleSession *)value; + struct session_find_jid *data = user_data; + gchar *remote_jid; + gchar *cmp_jid; + + if (data->ret != NULL) + return; + + remote_jid = jingle_session_get_remote_jid(session); + cmp_jid = data->use_bare ? jabber_get_bare_jid(remote_jid) + : g_strdup(remote_jid); + g_free(remote_jid); + + if (g_str_equal(data->jid, cmp_jid)) + data->ret = session; + + g_free(cmp_jid); +} +#endif /* GLIB_CHECK_VERSION 2.4.0 */ + JingleSession * jingle_session_find_by_jid(JabberStream *js, const gchar *jid) { +#if GLIB_CHECK_VERSION(2,4,0) return js->sessions != NULL ? g_hash_table_find(js->sessions, find_by_jid_ghr, (gpointer)jid) : NULL; +#else + struct session_find_jid data; + + if (js->sessions == NULL) + return NULL; + + data.jid = jid; + data.ret = NULL; + data.use_bare = g_utf8_strchr(jid, -1, '/') == NULL; + + g_hash_table_foreach(js->sessions, find_by_jid_ghr, &data); + return data.ret; +#endif } static xmlnode *
--- a/libpurple/protocols/jabber/libxmpp.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/libxmpp.c Wed Apr 29 23:20:51 2009 +0000 @@ -71,7 +71,7 @@ jabber_set_info, /* set_info */ jabber_send_typing, /* send_typing */ jabber_buddy_get_info, /* get_info */ - jabber_presence_send, /* set_status */ + jabber_set_status, /* set_status */ jabber_idle_set, /* set_idle */ NULL, /* change_passwd */ jabber_roster_add_buddy, /* add_buddy */ @@ -154,9 +154,18 @@ purple_signal_unregister(plugin, "jabber-sending-text"); + /* reverse order of init_plugin */ + jabber_bosh_uninit(); jabber_data_uninit(); jabber_si_uninit(); jabber_ibb_uninit(); + /* PEP things should be uninit via jabber_pep_uninit, not here */ + jabber_pep_uninit(); + jabber_caps_uninit(); + jabber_iq_uninit(); + + /* Stay on target...stay on target... Almost there... */ + jabber_uninit_plugin(); return TRUE; } @@ -280,27 +289,23 @@ #endif jabber_register_commands(); + /* reverse order of unload_plugin */ jabber_iq_init(); + jabber_caps_init(); + /* PEP things should be init via jabber_pep_init, not here */ jabber_pep_init(); + jabber_data_init(); + jabber_bosh_init(); - jabber_tune_init(); - jabber_caps_init(); - - jabber_data_init(); - + #warning implement adding and retrieving own features via IPC API jabber_ibb_init(); jabber_si_init(); - jabber_add_feature("avatarmeta", AVATARNAMESPACEMETA, jabber_pep_namespace_only_when_pep_enabled_cb); - jabber_add_feature("avatardata", AVATARNAMESPACEDATA, jabber_pep_namespace_only_when_pep_enabled_cb); - jabber_add_feature("buzz", XEP_0224_NAMESPACE, + jabber_add_feature("http://www.xmpp.org/extensions/xep-0224.html#ns", jabber_buzz_isenabled); - jabber_add_feature("bob", XEP_0231_NAMESPACE, - jabber_custom_smileys_isenabled); - jabber_add_feature("ibb", XEP_0047_NAMESPACE, NULL); - - jabber_pep_register_handler("avatar", AVATARNAMESPACEMETA, jabber_buddy_avatar_update_metadata); + jabber_add_feature(XEP_0231_NAMESPACE, jabber_custom_smileys_isenabled); + jabber_add_feature(XEP_0047_NAMESPACE, NULL); }
--- a/libpurple/protocols/jabber/message.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/message.c Wed Apr 29 23:20:51 2009 +0000 @@ -24,6 +24,7 @@ #include "notify.h" #include "server.h" #include "util.h" +#include "adhoccommands.h" #include "buddy.h" #include "chat.h" #include "data.h" @@ -597,8 +598,11 @@ /* The following tests expect xmlns != NULL */ continue; } else if(!strcmp(child->name, "subject") && !strcmp(xmlns,"jabber:client")) { - if(!jm->subject) + if(!jm->subject) { jm->subject = xmlnode_get_data(child); + if(!jm->subject) + jm->subject = g_strdup(""); + } } else if(!strcmp(child->name, "thread") && !strcmp(xmlns,"jabber:client")) { if(!jm->thread_id) jm->thread_id = xmlnode_get_data(child); @@ -626,24 +630,28 @@ purple_debug_info("jabber", "found %d smileys\n", g_list_length(smiley_refs)); - if (jm->type == JABBER_MESSAGE_GROUPCHAT) { - JabberID *jid = jabber_id_new(jm->from); - JabberChat *chat = NULL; + if (smiley_refs) { + if (jm->type == JABBER_MESSAGE_GROUPCHAT) { + JabberID *jid = jabber_id_new(jm->from); + JabberChat *chat = NULL; - if (jid) { - chat = jabber_chat_find(js, jid->node, jid->domain); - if (chat) conv = chat->conv; - } + if (jid) { + chat = jabber_chat_find(js, jid->node, jid->domain); + if (chat) conv = chat->conv; + } - jabber_id_free(jid); - } else { - conv = - purple_find_conversation_with_account(PURPLE_CONV_TYPE_ANY, - who, account); - if (!conv) { - /* we need to create the conversation here */ - conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, - account, who); + jabber_id_free(jid); + } else if (jm->type == JABBER_MESSAGE_NORMAL || + jm->type == JABBER_MESSAGE_CHAT) { + conv = + purple_find_conversation_with_account(PURPLE_CONV_TYPE_ANY, + who, account); + if (!conv) { + /* we need to create the conversation here */ + conv = + purple_conversation_new(PURPLE_CONV_TYPE_IM, + account, who); + } } } @@ -675,7 +683,7 @@ TRUE)) { const JabberData *data = jabber_data_find_remote_by_cid(cid); - /* if data is already known, we add write it immediatly */ + /* if data is already known, we write it immediatly */ if (data) { purple_debug_info("jabber", "data is already known\n"); @@ -773,6 +781,12 @@ } else { jm->etc = g_list_append(jm->etc, child); } + } else if (g_str_equal(child->name, "query")) { + const char *node = xmlnode_get_attrib(child, "node"); + if (purple_strequal(xmlns, "http://jabber.org/protocol/disco#items") + && purple_strequal(node, "http://jabber.org/protocol/commands")) { + jabber_adhoc_got_list(js, jm->from, child); + } } } @@ -1229,12 +1243,11 @@ jabber_message_free(jm); } -gboolean jabber_buzz_isenabled(JabberStream *js, const gchar *shortname, const gchar *namespace) { +gboolean jabber_buzz_isenabled(JabberStream *js, const gchar *namespace) { return js->allowBuzz; } -gboolean jabber_custom_smileys_isenabled(JabberStream *js, const gchar *shortname, - const gchar *namespace) +gboolean jabber_custom_smileys_isenabled(JabberStream *js, const gchar *namespace) { const PurpleConnection *gc = js->gc; PurpleAccount *account = purple_connection_get_account(gc);
--- a/libpurple/protocols/jabber/message.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/message.h Wed Apr 29 23:20:51 2009 +0000 @@ -80,9 +80,8 @@ unsigned int jabber_send_typing(PurpleConnection *gc, const char *who, PurpleTypingState state); void jabber_message_conv_closed(JabberStream *js, const char *who); -gboolean jabber_buzz_isenabled(JabberStream *js, const gchar *shortname, const gchar *namespace); +gboolean jabber_buzz_isenabled(JabberStream *js, const gchar *namespace); -gboolean jabber_custom_smileys_isenabled(JabberStream *js, const gchar *shortname, - const gchar *namespace); +gboolean jabber_custom_smileys_isenabled(JabberStream *js, const const gchar *namespace); #endif /* PURPLE_JABBER_MESSAGE_H_ */
--- a/libpurple/protocols/jabber/parser.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/parser.c Wed Apr 29 23:20:51 2009 +0000 @@ -207,6 +207,12 @@ jabber_parser_free(js); } +void +jabber_parser_close_stream(JabberStream *js) +{ + xmlParseChunk(js->context, "</stream:stream>", 16 /* length */, 0); +} + void jabber_parser_free(JabberStream *js) { if (js->context) { xmlParseChunk(js->context, NULL,0,1); @@ -226,8 +232,17 @@ xmlParseChunk(js->context, "", 0, 0); } else if ((ret = xmlParseChunk(js->context, buf, len, 0)) != XML_ERR_OK) { xmlError *err = xmlCtxtGetLastError(js->context); + /* + * libxml2 uses a global setting to determine whether or not to store + * warnings. Other libraries may set this, which causes err to be + * NULL. See #8136 for details. + */ + xmlErrorLevel level = XML_ERR_WARNING; - switch (err->level) { + if (err) + level = err->level; + + switch (level) { case XML_ERR_NONE: purple_debug_info("jabber", "xmlParseChunk returned info %i\n", ret); break;
--- a/libpurple/protocols/jabber/parser.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/parser.h Wed Apr 29 23:20:51 2009 +0000 @@ -25,6 +25,7 @@ #include "jabber.h" void jabber_parser_setup(JabberStream *js); +void jabber_parser_close_stream(JabberStream *js); void jabber_parser_free(JabberStream *js); void jabber_parser_process(JabberStream *js, const char *buf, int len);
--- a/libpurple/protocols/jabber/pep.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/pep.c Wed Apr 29 23:20:51 2009 +0000 @@ -24,8 +24,10 @@ #include "pep.h" #include "iq.h" #include <string.h> +#include "useravatar.h" #include "usermood.h" #include "usernick.h" +#include "usertune.h" static GHashTable *pep_handlers = NULL; @@ -34,20 +36,28 @@ pep_handlers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); /* register PEP handlers */ + jabber_avatar_init(); jabber_mood_init(); + jabber_tune_init(); jabber_nick_init(); } } +void jabber_pep_uninit(void) { + /* any PEP handlers that need to clean things up go here */ + g_hash_table_destroy(pep_handlers); + pep_handlers = NULL; +} + void jabber_pep_init_actions(GList **m) { /* register the PEP-specific actions */ jabber_mood_init_action(m); jabber_nick_init_action(m); } -void jabber_pep_register_handler(const char *shortname, const char *xmlns, JabberPEPHandler handlerfunc) { +void jabber_pep_register_handler(const char *xmlns, JabberPEPHandler handlerfunc) { gchar *notifyns = g_strdup_printf("%s+notify", xmlns); - jabber_add_feature(shortname, notifyns, NULL); /* receiving PEPs is always supported */ + jabber_add_feature(notifyns, NULL); /* receiving PEPs is always supported */ g_free(notifyns); g_hash_table_replace(pep_handlers, g_strdup(xmlns), handlerfunc); } @@ -88,7 +98,7 @@ jabber_iq_send(iq); } -gboolean jabber_pep_namespace_only_when_pep_enabled_cb(JabberStream *js, const gchar *shortname, const gchar *namespace) { +gboolean jabber_pep_namespace_only_when_pep_enabled_cb(JabberStream *js, const gchar *namespace) { return js->pep; } @@ -96,7 +106,12 @@ /* this may be called even when the own server doesn't support pep! */ JabberPEPHandler *jph; GList *itemslist; - char *jid = jabber_get_bare_jid(jm->from); + char *jid; + + if (jm->type != JABBER_MESSAGE_EVENT) + return; + + jid = jabber_get_bare_jid(jm->from); for(itemslist = jm->eventitems; itemslist; itemslist = itemslist->next) { xmlnode *items = (xmlnode*)itemslist->data; @@ -110,6 +125,25 @@ g_free(jid); } +void jabber_pep_delete_node(JabberStream *js, const gchar *node) +{ + JabberIq *iq; + xmlnode *pubsub, *del; + + g_return_if_fail(node != NULL); + g_return_if_fail(js->pep); + + iq = jabber_iq_new(js, JABBER_IQ_SET); + + pubsub = xmlnode_new_child(iq->node, "pubsub"); + xmlnode_set_namespace(pubsub, "http://jabber.org/protocol/pubsub#owner"); + + del = xmlnode_new_child(pubsub, "delete"); + xmlnode_set_attrib(del, "node", node); + + jabber_iq_send(iq); +} + void jabber_pep_publish(JabberStream *js, xmlnode *publish) { JabberIq *iq; xmlnode *pubsub;
--- a/libpurple/protocols/jabber/pep.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/pep.h Wed Apr 29 23:20:51 2009 +0000 @@ -27,6 +27,7 @@ #include "buddy.h" void jabber_pep_init(void); +void jabber_pep_uninit(void); void jabber_pep_init_actions(GList **m); @@ -42,11 +43,10 @@ * Registers a callback for PEP events. Also automatically announces this receiving capability via disco#info. * Don't forget to use jabber_add_feature when supporting the sending of PEP events of this type. * - * @parameter shortname A short name for this feature for XEP-0115. It has no semantic meaning, it just has to be unique. - * @parameter xmlns The namespace for this event + * @parameter xmlns The namespace for this event * @parameter handlerfunc The callback to be used when receiving an event with this namespace */ -void jabber_pep_register_handler(const char *shortname, const char *xmlns, JabberPEPHandler handlerfunc); +void jabber_pep_register_handler(const char *xmlns, JabberPEPHandler handlerfunc); /* * Request a specific item from another PEP node. @@ -64,16 +64,20 @@ /* * Default callback that can be used for namespaces which should only be enabled when PEP is supported * - * @parameter js The JabberStream struct for this connection - * @parameter shortname The namespace's shortname (for caps), ignored. + * @parameter js The JabberStream struct for this connection * @parameter namespace The namespace that's queried, ignored. * * @returns TRUE when PEP is enabled, FALSE otherwise */ -gboolean jabber_pep_namespace_only_when_pep_enabled_cb(JabberStream *js, const gchar *shortname, const gchar *namespace); +gboolean jabber_pep_namespace_only_when_pep_enabled_cb(JabberStream *js, const gchar *namespace); void jabber_handle_event(JabberMessage *jm); +/** + * Delete the specified PEP node. + */ +void jabber_pep_delete_node(JabberStream *js, const gchar *node); + /* * Publishes PEP item(s) *
--- a/libpurple/protocols/jabber/presence.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/presence.c Wed Apr 29 23:20:51 2009 +0000 @@ -93,11 +93,31 @@ g_free(my_base_jid); } - -void jabber_presence_send(PurpleAccount *account, PurpleStatus *status) +void jabber_set_status(PurpleAccount *account, PurpleStatus *status) { - PurpleConnection *gc = NULL; - JabberStream *js = NULL; + PurpleConnection *gc; + JabberStream *js; + + if (!purple_account_is_connected(account)) + return; + + if (!purple_status_is_active(status)) + return; + + if (purple_status_is_exclusive(status) && !purple_status_is_active(status)) { + /* An exclusive status can't be deactivated. You should just + * activate some other exclusive status. */ + return; + } + + gc = purple_account_get_connection(account); + js = purple_connection_get_protocol_data(gc); + jabber_presence_send(js, FALSE); +} + +void jabber_presence_send(JabberStream *js, gboolean force) +{ + PurpleAccount *account; xmlnode *presence, *x, *photo; char *stripped = NULL; JabberBuddyState state; @@ -106,28 +126,11 @@ int length = -1; gboolean allowBuzz; PurplePresence *p; - PurpleStatus *tune; - - if (purple_account_is_disconnected(account)) - return; - - p = purple_account_get_presence(account); - if (NULL == status) { - status = purple_presence_get_active_status(p); - } + PurpleStatus *status, *tune; - if (purple_status_is_exclusive(status)) { - /* An exclusive status can't be deactivated. You should just - * activate some other exclusive status. */ - if (!purple_status_is_active(status)) - return; - } else { - /* Work with the exclusive status. */ - status = purple_presence_get_active_status(p); - } - - gc = purple_account_get_connection(account); - js = gc->proto_data; + account = purple_connection_get_account(js->gc); + p = purple_account_get_presence(account); + status = purple_presence_get_active_status(p); /* we don't want to send presence before we've gotten our roster */ if(!js->roster_parsed) { @@ -141,25 +144,35 @@ allowBuzz = purple_status_get_attr_boolean(status,"buzz"); /* changing the buzz state has to trigger a re-broadcasting of the presence for caps */ - if (js->googletalk && stripped == NULL && purple_presence_is_status_primitive_active(p, PURPLE_STATUS_TUNE)) { - tune = purple_presence_get_status(p, "tune"); + tune = purple_presence_get_status(p, "tune"); + if (js->googletalk && !stripped && purple_status_is_active(tune)) { stripped = jabber_google_presence_outgoing(tune); } #define CHANGED(a,b) ((!a && b) || (a && a[0] == '\0' && b && b[0] != '\0') || \ (a && !b) || (a && a[0] != '\0' && b && b[0] == '\0') || (a && b && strcmp(a,b))) /* check if there are any differences to the <presence> and send them in that case */ - if (allowBuzz != js->allowBuzz || js->old_state != state || CHANGED(js->old_msg, stripped) || - js->old_priority != priority || CHANGED(js->old_avatarhash, js->avatar_hash)) { + if (force || allowBuzz != js->allowBuzz || js->old_state != state || + CHANGED(js->old_msg, stripped) || js->old_priority != priority || + CHANGED(js->old_avatarhash, js->avatar_hash) || js->old_idle != js->idle) { + /* Need to update allowBuzz before creating the presence (with caps) */ js->allowBuzz = allowBuzz; presence = jabber_presence_create_js(js, state, stripped, priority); - if(js->avatar_hash) { - x = xmlnode_new_child(presence, "x"); - xmlnode_set_namespace(x, "vcard-temp:x:update"); + /* Per XEP-0153 4.1, we must always send the <x> */ + x = xmlnode_new_child(presence, "x"); + xmlnode_set_namespace(x, "vcard-temp:x:update"); + /* + * FIXME: Per XEP-0153 4.3.2 bullet 2, we must not publish our + * image hash if another resource has logged in and updated the + * vcard avatar. Requires changes in jabber_presence_parse. + */ + if (js->vcard_fetched) { + /* Always publish a <photo>; it's empty if we have no image. */ photo = xmlnode_new_child(x, "photo"); - xmlnode_insert_data(photo, js->avatar_hash, -1); + if (js->avatar_hash) + xmlnode_insert_data(photo, js->avatar_hash, -1); } jabber_send(js, presence); @@ -177,12 +190,12 @@ js->old_avatarhash = g_strdup(js->avatar_hash); js->old_state = state; js->old_priority = priority; + js->old_idle = js->idle; } g_free(stripped); /* next, check if there are any changes to the tune values */ - tune = purple_presence_get_status(p, "tune"); - if (tune && purple_status_is_active(tune)) { + if (purple_status_is_active(tune)) { artist = purple_status_get_attr_string(tune, PURPLE_TUNE_ARTIST); title = purple_status_get_attr_string(tune, PURPLE_TUNE_TITLE); source = purple_status_get_attr_string(tune, PURPLE_TUNE_ALBUM); @@ -259,47 +272,38 @@ g_free(pstr); } + /* if we are idle and not offline, include idle */ + if (js->idle && state != JABBER_BUDDY_STATE_UNAVAILABLE) { + xmlnode *query = xmlnode_new_child(presence, "query"); + gchar seconds[10]; + g_snprintf(seconds, 10, "%d", (int) (time(NULL) - js->idle)); + + xmlnode_set_namespace(query, "jabber:iq:last"); + xmlnode_set_attrib(query, "seconds", seconds); + } + /* JEP-0115 */ + /* calculate hash */ + jabber_caps_calculate_own_hash(js); + /* create xml */ c = xmlnode_new_child(presence, "c"); xmlnode_set_namespace(c, "http://jabber.org/protocol/caps"); xmlnode_set_attrib(c, "node", CAPS0115_NODE); - xmlnode_set_attrib(c, "ver", VERSION); -#ifdef USE_VV - /* Make sure this is 'voice-v1', or you won't be able to talk to Google Talk */ - xmlnode_set_attrib(c, "ext", "voice-v1"); -#endif - - if(js != NULL) { - /* add the extensions */ - char extlist[1024]; - unsigned remaining = 1023; /* one less for the \0 */ - GList *feature; - - extlist[0] = '\0'; - for(feature = jabber_features; feature && remaining > 0; feature = feature->next) { - JabberFeature *feat = (JabberFeature*)feature->data; - unsigned featlen; + xmlnode_set_attrib(c, "hash", "sha-1"); + xmlnode_set_attrib(c, "ver", jabber_caps_get_own_hash(js)); - if(feat->is_enabled != NULL && feat->is_enabled(js, feat->shortname, feat->namespace) == FALSE) - continue; /* skip this feature */ - - featlen = strlen(feat->shortname); - - /* cut off when we don't have any more space left in our buffer (too bad) */ - if(featlen > remaining) - break; - - strncat(extlist,feat->shortname,remaining); - remaining -= featlen; - if(feature->next) { /* no space at the end */ - strncat(extlist," ",remaining); - --remaining; - } - } - /* did we add anything? */ - if(remaining < 1023) - xmlnode_set_attrib(c, "ext", extlist); - } +#ifdef USE_VV + /* + * MASSIVE HUGE DISGUSTING HACK + * This is a huge hack. As far as I can tell, Google Talk's gmail client + * doesn't bother to check the actual features we advertise; they + * just assume that if we specify a 'voice-v1' ext (ignoring that + * these are to be assigned no semantic value), we support receiving voice + * calls. + */ + if (jabber_audio_enabled(js, NULL /* unused */)) + xmlnode_set_attrib(c, "ext", "voice-v1"); +#endif return presence; } @@ -338,8 +342,6 @@ JabberBuddy *jb = NULL; xmlnode *vcard, *photo, *binval; char *text; - guchar *data; - gsize size; if(!from) return; @@ -354,10 +356,13 @@ (( (binval = xmlnode_get_child(photo, "BINVAL")) && (text = xmlnode_get_data(binval))) || (text = xmlnode_get_data(photo)))) { - char *hash; + guchar *data; + gchar *hash; + gsize size; data = purple_base64_decode(text, &size); hash = jabber_calculate_data_sha1sum(data, size); + purple_buddy_icons_set_for_user(js->gc->account, from, data, size, hash); g_free(hash); g_free(text); @@ -371,39 +376,41 @@ char *from; } JabberPresenceCapabilities; -static void jabber_presence_set_capabilities(JabberCapsClientInfo *info, gpointer user_data) { - JabberPresenceCapabilities *userdata = user_data; - JabberID *jid; +static void +jabber_presence_set_capabilities(JabberCapsClientInfo *info, GList *exts, + JabberPresenceCapabilities *userdata) +{ JabberBuddyResource *jbr; - GList *iter; + char *resource = g_utf8_strrchr(userdata->from, -1, '/'); + resource += 1; - jid = jabber_id_new(userdata->from); - jbr = jabber_buddy_find_resource(userdata->jb, jid->resource); - jabber_id_free(jid); - - if(!jbr) { + jbr = jabber_buddy_find_resource(userdata->jb, resource); + if (!jbr) { g_free(userdata->from); g_free(userdata); + if (exts) { + g_list_foreach(exts, (GFunc)g_free, NULL); + g_list_free(exts); + } return; } - if(jbr->caps) - jabber_caps_free_clientinfo(jbr->caps); - jbr->caps = info; + /* Any old jbr->caps.info is owned by the caps code */ + if (jbr->caps.exts) { + g_list_foreach(jbr->caps.exts, (GFunc)g_free, NULL); + g_list_free(jbr->caps.exts); + } - if (info) { - for(iter = info->features; iter; iter = g_list_next(iter)) { - if(!strcmp((const char*)iter->data, "http://jabber.org/protocol/commands")) { - JabberIq *iq = jabber_iq_new_query(userdata->js, JABBER_IQ_GET, "http://jabber.org/protocol/disco#items"); - xmlnode *query = xmlnode_get_child_with_namespace(iq->node,"query","http://jabber.org/protocol/disco#items"); - xmlnode_set_attrib(iq->node, "to", userdata->from); - xmlnode_set_attrib(query, "node", "http://jabber.org/protocol/commands"); + jbr->caps.info = info; + jbr->caps.exts = exts; - jabber_iq_set_callback(iq, jabber_adhoc_disco_result_cb, NULL); - jabber_iq_send(iq); - break; - } - } + if (jabber_resource_has_capability(jbr, "http://jabber.org/protocol/commands")) { + JabberIq *iq = jabber_iq_new_query(userdata->js, JABBER_IQ_GET, "http://jabber.org/protocol/disco#items"); + xmlnode *query = xmlnode_get_child_with_namespace(iq->node, "query", "http://jabber.org/protocol/disco#items"); + xmlnode_set_attrib(iq->node, "to", userdata->from); + xmlnode_set_attrib(query, "node", "http://jabber.org/protocol/commands"); + jabber_iq_set_callback(iq, jabber_adhoc_disco_result_cb, NULL); + jabber_iq_send(iq); } g_free(userdata->from); @@ -425,6 +432,7 @@ JabberBuddyResource *jbr = NULL, *found_jbr = NULL; PurpleConvChatBuddyFlags flags = PURPLE_CBFLAGS_NONE; gboolean delayed = FALSE; + const gchar *stamp = NULL; /* from <delayed/> element */ PurpleBuddy *b = NULL; char *buddy_name; JabberBuddyState state = JABBER_BUDDY_STATE_UNKNOWN; @@ -432,6 +440,7 @@ gboolean muc = FALSE; char *avatar_hash = NULL; xmlnode *caps = NULL; + int idle = 0; if(!(jb = jabber_buddy_find(js, from, TRUE))) return; @@ -514,12 +523,14 @@ } else if(!strcmp(y->name, "delay") && !strcmp(xmlns, "urn:xmpp:delay")) { /* XXX: compare the time. jabber:x:delay can happen on presence packets that aren't really and truly delayed */ delayed = TRUE; + stamp = xmlnode_get_attrib(y, "stamp"); } else if(!strcmp(y->name, "c") && !strcmp(xmlns, "http://jabber.org/protocol/caps")) { caps = y; /* store for later, when creating buddy resource */ } else if(!strcmp(y->name, "x")) { if(!strcmp(xmlns, "jabber:x:delay")) { /* XXX: compare the time. jabber:x:delay can happen on presence packets that aren't really and truly delayed */ delayed = TRUE; + stamp = xmlnode_get_attrib(y, "stamp"); } else if(!strcmp(xmlns, "http://jabber.org/protocol/muc#user")) { xmlnode *z; @@ -570,13 +581,29 @@ avatar_hash = xmlnode_get_data(photo); } } + } else if (!strcmp(y->name, "query") && + !strcmp(xmlnode_get_namespace(y), "jabber:iq:last")) { + /* resource has specified idle */ + const gchar *seconds = xmlnode_get_attrib(y, "seconds"); + if (seconds) { + /* we may need to take "delayed" into account here */ + idle = atoi(seconds); + } } } + if (idle && delayed && stamp) { + /* if we have a delayed presence, we need to add the delay to the idle + value */ + time_t offset = time(NULL) - purple_str_to_time(stamp, TRUE, NULL, NULL, + NULL); + purple_debug_info("jabber", "got delay %s yielding %ld s offset\n", + stamp, offset); + idle += offset; + } if(jid->node && (chat = jabber_chat_find(js, jid->node, jid->domain))) { static int i = 1; - char *room_jid = g_strdup_printf("%s@%s", jid->node, jid->domain); if(state == JABBER_BUDDY_STATE_ERROR) { char *title, *msg = jabber_parse_error(js, packet, NULL); @@ -598,7 +625,6 @@ jabber_chat_destroy(chat); jabber_id_free(jid); g_free(status); - g_free(room_jid); g_free(avatar_hash); return; } @@ -615,7 +641,6 @@ jabber_chat_destroy(chat); jabber_id_free(jid); g_free(status); - g_free(room_jid); g_free(avatar_hash); return; } @@ -671,12 +696,14 @@ } } else { if(!chat->conv) { + char *room_jid = g_strdup_printf("%s@%s", jid->node, jid->domain); chat->id = i++; chat->muc = muc; chat->conv = serv_got_joined_chat(js->gc, chat->id, room_jid); purple_conv_chat_set_nick(PURPLE_CONV_CHAT(chat->conv), chat->handle); jabber_chat_disco_traffic(chat); + g_free(room_jid); } jabber_buddy_track_resource(jb, jid->resource, priority, state, @@ -691,7 +718,6 @@ purple_conv_chat_user_set_flags(PURPLE_CONV_CHAT(chat->conv), jid->resource, flags); } - g_free(room_jid); } else { buddy_name = g_strdup_printf("%s%s%s", jid->node ? jid->node : "", jid->node ? "@" : "", jid->domain); @@ -747,29 +773,43 @@ } else { jbr = jabber_buddy_track_resource(jb, jid->resource, priority, state, status); - if(caps) { - const char *node = xmlnode_get_attrib(caps,"node"); - const char *ver = xmlnode_get_attrib(caps,"ver"); - const char *ext = xmlnode_get_attrib(caps,"ext"); - - if(node && ver) { - JabberPresenceCapabilities *userdata = g_new0(JabberPresenceCapabilities, 1); - userdata->js = js; - userdata->jb = jb; - userdata->from = g_strdup(from); - jabber_caps_get_info(js, from, node, ver, ext, jabber_presence_set_capabilities, userdata); - } + if (idle) { + jbr->idle = time(NULL) - idle; + } else { + jbr->idle = 0; } } if((found_jbr = jabber_buddy_find_resource(jb, NULL))) { jabber_google_presence_incoming(js, buddy_name, found_jbr); purple_prpl_got_user_status(js->gc->account, buddy_name, jabber_buddy_state_get_status_id(found_jbr->state), "priority", found_jbr->priority, "message", found_jbr->status, NULL); + purple_prpl_got_user_idle(js->gc->account, buddy_name, found_jbr->idle, found_jbr->idle); } else { purple_prpl_got_user_status(js->gc->account, buddy_name, "offline", status ? "message" : NULL, status, NULL); } g_free(buddy_name); } + + if (caps && (!type || g_str_equal(type, "available"))) { + /* handle Entity Capabilities (XEP-0115) */ + const char *node = xmlnode_get_attrib(caps, "node"); + const char *ver = xmlnode_get_attrib(caps, "ver"); + const char *hash = xmlnode_get_attrib(caps, "hash"); + const char *ext = xmlnode_get_attrib(caps, "ext"); + + /* v1.3 uses: node, ver, and optionally ext. + * v1.5 uses: node, ver, and hash. */ + if (node && *node && ver && *ver) { + JabberPresenceCapabilities *userdata = g_new0(JabberPresenceCapabilities, 1); + userdata->js = js; + userdata->jb = jb; + userdata->from = g_strdup(from); + jabber_caps_get_info(js, from, node, ver, hash, ext, + (jabber_caps_get_info_cb)jabber_presence_set_capabilities, + userdata); + } + } + g_free(status); jabber_id_free(jid); g_free(avatar_hash);
--- a/libpurple/protocols/jabber/presence.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/presence.h Wed Apr 29 23:20:51 2009 +0000 @@ -26,7 +26,17 @@ #include "jabber.h" #include "xmlnode.h" -void jabber_presence_send(PurpleAccount *account, PurpleStatus *status); +void jabber_set_status(PurpleAccount *account, PurpleStatus *status); + +/** + * Send a full presence stanza. + * + * @param js A JabberStream object. + * @param force Force sending the presence stanza, irrespective of whether + * the contents seem to have changed. + */ +void jabber_presence_send(JabberStream *js, gboolean force); + xmlnode *jabber_presence_create(JabberBuddyState state, const char *msg, int priority); /* DEPRECATED */ xmlnode *jabber_presence_create_js(JabberStream *js, JabberBuddyState state, const char *msg, int priority); void jabber_presence_parse(JabberStream *js, xmlnode *packet);
--- a/libpurple/protocols/jabber/roster.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/roster.c Wed Apr 29 23:20:51 2009 +0000 @@ -256,11 +256,12 @@ js->currently_parsing_roster_push = FALSE; /* if we're just now parsing the roster for the first time, - * then now would be the time to send our initial presence */ + * then now would be the time to declare ourselves connected and + * send our initial presence */ if(!js->roster_parsed) { js->roster_parsed = TRUE; - - jabber_presence_send(js->gc->account, NULL); + jabber_presence_send(js, TRUE); + jabber_stream_set_state(js, JABBER_STREAM_CONNECTED); } }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/protocols/jabber/useravatar.c Wed Apr 29 23:20:51 2009 +0000 @@ -0,0 +1,373 @@ +/* + * purple - Jabber Protocol Plugin + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "internal.h" + +#include "useravatar.h" +#include "pep.h" +#include "debug.h" + +#define MAX_HTTP_BUDDYICON_BYTES (200 * 1024) + +static void update_buddy_metadata(JabberStream *js, const char *from, xmlnode *items); + +void jabber_avatar_init(void) +{ + jabber_pep_register_handler(NS_AVATAR_0_12_METADATA, + update_buddy_metadata); + + jabber_add_feature(NS_AVATAR_1_1_METADATA, + jabber_pep_namespace_only_when_pep_enabled_cb); + jabber_add_feature(NS_AVATAR_1_1_DATA, + jabber_pep_namespace_only_when_pep_enabled_cb); + + jabber_pep_register_handler(NS_AVATAR_1_1_METADATA, + update_buddy_metadata); +} + +static void +remove_avatar_0_12_nodes(JabberStream *js) +{ + jabber_pep_delete_node(js, NS_AVATAR_0_12_METADATA); + jabber_pep_delete_node(js, NS_AVATAR_0_12_DATA); +} + +void jabber_avatar_set(JabberStream *js, PurpleStoredImage *img) +{ + xmlnode *publish, *metadata, *item; + + if (!js->pep) + return; + + remove_avatar_0_12_nodes(js); + + if (!img) { + publish = xmlnode_new("publish"); + xmlnode_set_attrib(publish, "node", NS_AVATAR_1_1_METADATA); + + item = xmlnode_new_child(publish, "item"); + metadata = xmlnode_new_child(item, "metadata"); + xmlnode_set_namespace(metadata, NS_AVATAR_1_1_METADATA); + + /* publish */ + jabber_pep_publish(js, publish); + } else { + /* + * TODO: This is pretty gross. The Jabber PRPL really shouldn't + * do voodoo to try to determine the image type, height + * and width. + */ + /* A PNG header, including the IHDR, but nothing else */ + const struct { + guchar signature[8]; /* must be hex 89 50 4E 47 0D 0A 1A 0A */ + struct { + guint32 length; /* must be 0x0d */ + guchar type[4]; /* must be 'I' 'H' 'D' 'R' */ + guint32 width; + guint32 height; + guchar bitdepth; + guchar colortype; + guchar compression; + guchar filter; + guchar interlace; + } ihdr; + } *png = purple_imgstore_get_data(img); /* ATTN: this is in network byte order! */ + + /* check if the data is a valid png file (well, at least to some extent) */ + if(png->signature[0] == 0x89 && + png->signature[1] == 0x50 && + png->signature[2] == 0x4e && + png->signature[3] == 0x47 && + png->signature[4] == 0x0d && + png->signature[5] == 0x0a && + png->signature[6] == 0x1a && + png->signature[7] == 0x0a && + ntohl(png->ihdr.length) == 0x0d && + png->ihdr.type[0] == 'I' && + png->ihdr.type[1] == 'H' && + png->ihdr.type[2] == 'D' && + png->ihdr.type[3] == 'R') { + /* parse PNG header to get the size of the image (yes, this is required) */ + guint32 width = ntohl(png->ihdr.width); + guint32 height = ntohl(png->ihdr.height); + xmlnode *data, *info; + char *lengthstring, *widthstring, *heightstring; + + /* compute the sha1 hash */ + char *hash = jabber_calculate_data_sha1sum(purple_imgstore_get_data(img), + purple_imgstore_get_size(img)); + char *base64avatar = purple_base64_encode(purple_imgstore_get_data(img), + purple_imgstore_get_size(img)); + + publish = xmlnode_new("publish"); + xmlnode_set_attrib(publish, "node", NS_AVATAR_1_1_DATA); + + item = xmlnode_new_child(publish, "item"); + xmlnode_set_attrib(item, "id", hash); + + data = xmlnode_new_child(item, "data"); + xmlnode_set_namespace(data, NS_AVATAR_1_1_DATA); + + xmlnode_insert_data(data, base64avatar, -1); + /* publish the avatar itself */ + jabber_pep_publish(js, publish); + + g_free(base64avatar); + + lengthstring = g_strdup_printf("%" G_GSIZE_FORMAT, + purple_imgstore_get_size(img)); + widthstring = g_strdup_printf("%u", width); + heightstring = g_strdup_printf("%u", height); + + /* publish the metadata */ + publish = xmlnode_new("publish"); + xmlnode_set_attrib(publish, "node", NS_AVATAR_1_1_METADATA); + + item = xmlnode_new_child(publish, "item"); + xmlnode_set_attrib(item, "id", hash); + + metadata = xmlnode_new_child(item, "metadata"); + xmlnode_set_namespace(metadata, NS_AVATAR_1_1_METADATA); + + info = xmlnode_new_child(metadata, "info"); + xmlnode_set_attrib(info, "id", hash); + xmlnode_set_attrib(info, "type", "image/png"); + xmlnode_set_attrib(info, "bytes", lengthstring); + xmlnode_set_attrib(info, "width", widthstring); + xmlnode_set_attrib(info, "height", heightstring); + + jabber_pep_publish(js, publish); + + g_free(lengthstring); + g_free(widthstring); + g_free(heightstring); + g_free(hash); + } else { + purple_debug_error("jabber", "Cannot set PEP avatar to non-PNG data\n"); + } + } +} + +static void +do_got_own_avatar_cb(JabberStream *js, const char *from, xmlnode *items) +{ + xmlnode *item = NULL, *metadata = NULL, *info = NULL; + PurpleAccount *account = purple_connection_get_account(js->gc); + const char *server_hash = NULL; + const char *ns; + + if ((item = xmlnode_get_child(items, "item")) && + (metadata = xmlnode_get_child(item, "metadata")) && + (info = xmlnode_get_child(metadata, "info"))) { + server_hash = xmlnode_get_attrib(info, "id"); + } + + if (!metadata) + return; + + ns = xmlnode_get_namespace(metadata); + if (!ns) + return; + + /* + * We no longer publish avatars to the older namespace. If there is one + * there, delete it. + */ + if (g_str_equal(ns, NS_AVATAR_0_12_METADATA) && server_hash) { + remove_avatar_0_12_nodes(js); + return; + } + + /* Publish ours if it's different than the server's */ + if (!purple_strequal(server_hash, js->initial_avatar_hash)) { + PurpleStoredImage *img = purple_buddy_icons_find_account_icon(account); + jabber_avatar_set(js, img); + purple_imgstore_unref(img); + } +} + +void jabber_avatar_fetch_mine(JabberStream *js) +{ + char *jid = g_strdup_printf("%s@%s", js->user->node, js->user->domain); + jabber_pep_request_item(js, jid, NS_AVATAR_0_12_METADATA, NULL, + do_got_own_avatar_cb); + jabber_pep_request_item(js, jid, NS_AVATAR_1_1_METADATA, NULL, + do_got_own_avatar_cb); + g_free(jid); +} + +typedef struct _JabberBuddyAvatarUpdateURLInfo { + JabberStream *js; + char *from; + char *id; +} JabberBuddyAvatarUpdateURLInfo; + +static void +do_buddy_avatar_update_fromurl(PurpleUtilFetchUrlData *url_data, + gpointer user_data, const gchar *url_text, + gsize len, const gchar *error_message) +{ + JabberBuddyAvatarUpdateURLInfo *info = user_data; + if(!url_text) { + purple_debug(PURPLE_DEBUG_ERROR, "jabber", + "do_buddy_avatar_update_fromurl got error \"%s\"", + error_message); + goto out; + } + + purple_buddy_icons_set_for_user(purple_connection_get_account(info->js->gc), info->from, (void*)url_text, len, info->id); + +out: + g_free(info->from); + g_free(info->id); + g_free(info); +} + +static void +do_buddy_avatar_update_data(JabberStream *js, const char *from, xmlnode *items) +{ + xmlnode *item, *data; + const char *checksum, *ns; + char *b64data; + void *img; + size_t size; + if(!items) + return; + + item = xmlnode_get_child(items, "item"); + if(!item) + return; + + data = xmlnode_get_child(item, "data"); + if(!data) + return; + + ns = xmlnode_get_namespace(data); + /* Make sure the namespace is one of the two valid possibilities */ + if (!ns || (!g_str_equal(ns, NS_AVATAR_0_12_DATA) && + !g_str_equal(ns, NS_AVATAR_1_1_DATA))) + return; + + checksum = xmlnode_get_attrib(item,"id"); + if(!checksum) + return; + + b64data = xmlnode_get_data(data); + if(!b64data) + return; + + img = purple_base64_decode(b64data, &size); + if(!img) { + g_free(b64data); + return; + } + + purple_buddy_icons_set_for_user(purple_connection_get_account(js->gc), from, img, size, checksum); + g_free(b64data); +} + +static void +update_buddy_metadata(JabberStream *js, const char *from, xmlnode *items) +{ + PurpleBuddy *buddy = purple_find_buddy(purple_connection_get_account(js->gc), from); + const char *checksum, *ns; + xmlnode *item, *metadata; + if(!buddy) + return; + + if (!items) + return; + + item = xmlnode_get_child(items,"item"); + if (!item) + return; + + metadata = xmlnode_get_child(item, "metadata"); + if(!metadata) + return; + + ns = xmlnode_get_namespace(metadata); + /* Make sure the namespace is one of the two valid possibilities */ + if (!ns || (!g_str_equal(ns, NS_AVATAR_0_12_METADATA) && + !g_str_equal(ns, NS_AVATAR_1_1_METADATA))) + return; + + checksum = purple_buddy_icons_get_checksum_for_user(buddy); + + /* check if we have received a stop */ + if(xmlnode_get_child(metadata, "stop")) { + purple_buddy_icons_set_for_user(purple_connection_get_account(js->gc), from, NULL, 0, NULL); + } else { + xmlnode *info, *goodinfo = NULL; + gboolean has_children = FALSE; + + /* iterate over all info nodes to get one we can use */ + for(info = metadata->child; info; info = info->next) { + if(info->type == XMLNODE_TYPE_TAG) + has_children = TRUE; + if(info->type == XMLNODE_TYPE_TAG && !strcmp(info->name,"info")) { + const char *type = xmlnode_get_attrib(info,"type"); + const char *id = xmlnode_get_attrib(info,"id"); + + if(checksum && id && !strcmp(id, checksum)) { + /* we already have that avatar, so we don't have to do anything */ + goodinfo = NULL; + break; + } + /* We'll only pick the png one for now. It's a very nice image format anyways. */ + if(type && id && !goodinfo && !strcmp(type, "image/png")) + goodinfo = info; + } + } + if(has_children == FALSE) { + purple_buddy_icons_set_for_user(purple_connection_get_account(js->gc), from, NULL, 0, NULL); + } else if(goodinfo) { + const char *url = xmlnode_get_attrib(goodinfo, "url"); + const char *id = xmlnode_get_attrib(goodinfo,"id"); + + /* the avatar might either be stored in a pep node, or on a HTTP(S) URL */ + if(!url) { + const char *data_ns; + data_ns = (g_str_equal(ns, NS_AVATAR_0_12_METADATA) ? + NS_AVATAR_0_12_DATA : NS_AVATAR_1_1_DATA); + jabber_pep_request_item(js, from, data_ns, id, + do_buddy_avatar_update_data); + } else { + PurpleUtilFetchUrlData *url_data; + JabberBuddyAvatarUpdateURLInfo *info = g_new0(JabberBuddyAvatarUpdateURLInfo, 1); + info->js = js; + + url_data = purple_util_fetch_url_len(url, TRUE, NULL, TRUE, + MAX_HTTP_BUDDYICON_BYTES, + do_buddy_avatar_update_fromurl, info); + if (url_data) { + info->from = g_strdup(from); + info->id = g_strdup(id); + js->url_datas = g_slist_prepend(js->url_datas, url_data); + } else + g_free(info); + + } + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/protocols/jabber/useravatar.h Wed Apr 29 23:20:51 2009 +0000 @@ -0,0 +1,43 @@ +/* + * purple - Jabber Protocol Plugin + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef _PURPLE_JABBER_USERAVATAR_H_ +#define _PURPLE_JABBER_USERAVATAR_H_ + +#include "jabber.h" +#include "imgstore.h" + +/* Implementation of XEP-0084 */ + +#define NS_AVATAR_0_12_DATA "http://www.xmpp.org/extensions/xep-0084.html#ns-data" +#define NS_AVATAR_0_12_METADATA "http://www.xmpp.org/extensions/xep-0084.html#ns-metadata" + +#define NS_AVATAR_1_1_DATA "urn:xmpp:avatar:data" +#define NS_AVATAR_1_1_METADATA "urn:xmpp:avatar:metadata" + +void jabber_avatar_init(void); +void jabber_avatar_set(JabberStream *js, PurpleStoredImage *img); + +void jabber_avatar_fetch_mine(JabberStream *js); + +#endif /* _PURPLE_JABBER_USERAVATAR_H_ */
--- a/libpurple/protocols/jabber/usermood.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/usermood.c Wed Apr 29 23:20:51 2009 +0000 @@ -141,8 +141,8 @@ } void jabber_mood_init(void) { - jabber_add_feature("mood", "http://jabber.org/protocol/mood", jabber_pep_namespace_only_when_pep_enabled_cb); - jabber_pep_register_handler("moodn", "http://jabber.org/protocol/mood", jabber_mood_cb); + jabber_add_feature("http://jabber.org/protocol/mood", jabber_pep_namespace_only_when_pep_enabled_cb); + jabber_pep_register_handler("http://jabber.org/protocol/mood", jabber_mood_cb); } static void do_mood_set_from_fields(PurpleConnection *gc, PurpleRequestFields *fields) {
--- a/libpurple/protocols/jabber/usernick.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/usernick.c Wed Apr 29 23:20:51 2009 +0000 @@ -92,8 +92,8 @@ } void jabber_nick_init(void) { - jabber_add_feature("nick", "http://jabber.org/protocol/nick", jabber_pep_namespace_only_when_pep_enabled_cb); - jabber_pep_register_handler("nickn", "http://jabber.org/protocol/nick", jabber_nick_cb); + jabber_add_feature("http://jabber.org/protocol/nick", jabber_pep_namespace_only_when_pep_enabled_cb); + jabber_pep_register_handler("http://jabber.org/protocol/nick", jabber_nick_cb); } void jabber_nick_init_action(GList **m) {
--- a/libpurple/protocols/jabber/usertune.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/jabber/usertune.c Wed Apr 29 23:20:51 2009 +0000 @@ -109,8 +109,8 @@ } void jabber_tune_init(void) { - jabber_add_feature("tune", "http://jabber.org/protocol/tune", jabber_pep_namespace_only_when_pep_enabled_cb); - jabber_pep_register_handler("tunen", "http://jabber.org/protocol/tune", jabber_tune_cb); + jabber_add_feature("http://jabber.org/protocol/tune", jabber_pep_namespace_only_when_pep_enabled_cb); + jabber_pep_register_handler("http://jabber.org/protocol/tune", jabber_tune_cb); } void jabber_tune_set(PurpleConnection *gc, const PurpleJabberTuneInfo *tuneinfo) {
--- a/libpurple/protocols/oscar/family_chatnav.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/oscar/family_chatnav.c Wed Apr 29 23:20:51 2009 +0000 @@ -44,6 +44,8 @@ if (snac2->family != SNAC_FAMILY_CHATNAV) { purple_debug_warning("oscar", "chatnav error: received response that maps to corrupt request (fam=%04x)\n", snac2->family); + g_free(snac2->data); + g_free(snac2); return 0; } @@ -462,6 +464,8 @@ if (snac2->family != SNAC_FAMILY_CHATNAV) { purple_debug_misc("oscar", "faim: chatnav_parse_info: received response that maps to corrupt request! (fam=%04x)\n", snac2->family); + g_free(snac2->data); + g_free(snac2); return 0; }
--- a/libpurple/protocols/oscar/family_locate.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/oscar/family_locate.c Wed Apr 29 23:20:51 2009 +0000 @@ -963,11 +963,14 @@ if ((snac2->family != SNAC_FAMILY_LOCATE) && (snac2->type != 0x0015)) { purple_debug_misc("oscar", "locate error: received response from invalid request! %d\n", snac2->family); + g_free(snac2->data); + g_free(snac2); return 0; } if (!(bn = snac2->data)) { purple_debug_misc("oscar", "locate error: received response from request without a buddy name!\n"); + g_free(snac2); return 0; }
--- a/libpurple/protocols/qq/ChangeLog Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/qq/ChangeLog Wed Apr 29 23:20:51 2009 +0000 @@ -1,3 +1,6 @@ +2009.04.23 - flos <lonicerae(at)gmail.com> + * Fixed a bug of updating buddy who is not in user's buddy list + 2009.02.25 - flos <lonicerae(at)gmail.com> * Changed text 'ZipCode' to 'Postal Code'
--- a/libpurple/protocols/qq/buddy_info.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/qq/buddy_info.c Wed Apr 29 23:20:51 2009 +0000 @@ -191,7 +191,7 @@ } switch (field_infos[index].type) { case QQ_FIELD_BOOL: - purple_notify_user_info_add_pair(user_info, field_infos[index].text, + purple_notify_user_info_add_pair(user_info, _(field_infos[index].text), strtol(segments[index], NULL, 10) ? _("True") : _("False")); break; case QQ_FIELD_CHOICE: @@ -200,7 +200,7 @@ choice_num = 0; } - purple_notify_user_info_add_pair(user_info, field_infos[index].text, field_infos[index].choice[choice_num]); + purple_notify_user_info_add_pair(user_info, _(field_infos[index].text), field_infos[index].choice[choice_num]); break; case QQ_FIELD_LABEL: case QQ_FIELD_STRING: @@ -208,7 +208,7 @@ default: if (strlen(segments[index]) != 0) { utf8_value = qq_to_utf8(segments[index], QQ_CHARSET_DEFAULT); - purple_notify_user_info_add_pair(user_info, field_infos[index].text, utf8_value); + purple_notify_user_info_add_pair(user_info, _(field_infos[index].text), utf8_value); g_free(utf8_value); } break; @@ -348,18 +348,18 @@ utf8_value = qq_to_utf8(segments[index], QQ_CHARSET_DEFAULT); if (field_infos[index].type == QQ_FIELD_STRING) { field = purple_request_field_string_new( - field_infos[index].id, field_infos[index].text, utf8_value, FALSE); + field_infos[index].id, _(field_infos[index].text), utf8_value, FALSE); } else { field = purple_request_field_string_new( - field_infos[index].id, field_infos[index].text, utf8_value, TRUE); + field_infos[index].id, _(field_infos[index].text), utf8_value, TRUE); } purple_request_field_group_add_field(group, field); g_free(utf8_value); break; case QQ_FIELD_BOOL: field = purple_request_field_bool_new( - field_infos[index].id, field_infos[index].text, - strtol(segments[index], NULL, 10) ? TRUE : FALSE); + field_infos[index].id, _(field_infos[index].text), + strtol(segments[index], NULL, 10) ? TRUE : FALSE); purple_request_field_group_add_field(group, field); break; case QQ_FIELD_CHOICE: @@ -374,7 +374,7 @@ } } field = purple_request_field_choice_new( - field_infos[index].id, field_infos[index].text, choice_num); + field_infos[index].id, _(field_infos[index].text), choice_num); for (i = 0; i < field_infos[index].choice_size; i++) { purple_request_field_choice_add(field, field_infos[index].choice[i]); } @@ -606,21 +606,21 @@ /* after getting info or modify myself, refresh the buddy list accordingly */ static void update_buddy_info(PurpleConnection *gc, gchar **segments) { - PurpleBuddy *buddy; - qq_data *qd; - qq_buddy_data *bd; + PurpleBuddy *buddy = NULL; + qq_data *qd = NULL; + qq_buddy_data *bd = NULL; guint32 uid; gchar *who; gchar *alias_utf8; + PurpleAccount *account = purple_connection_get_account(gc); - qd = (qq_data *)purple_connection_get_protocol_data(gc); uid = strtoul(segments[QQ_INFO_UID], NULL, 10); who = uid_to_purple_name(uid); - qq_filter_str(segments[QQ_INFO_NICK]); alias_utf8 = qq_to_utf8(segments[QQ_INFO_NICK], QQ_CHARSET_DEFAULT); + if (uid == qd->uid) { /* it is me */ purple_debug_info("QQ", "Got my info\n"); qd->my_icon = strtol(segments[QQ_INFO_FACE], NULL, 10); @@ -631,12 +631,14 @@ buddy = qq_buddy_find_or_new(gc, uid); } else { buddy = purple_find_buddy(gc->account, who); + /* purple_debug_info("QQ", "buddy=%p\n", (void*)buddy); */ } /* if the buddy is null, the api will catch it and return null here */ bd = purple_buddy_get_protocol_data(buddy); + /* purple_debug_info("QQ", "bd=%p\n", (void*)bd); */ - if (buddy == NULL || bd) { + if (bd == NULL || buddy == NULL) { g_free(who); g_free(alias_utf8); return; @@ -646,6 +648,7 @@ bd->age = strtol(segments[QQ_INFO_AGE], NULL, 10); bd->gender = strtol(segments[QQ_INFO_GENDER], NULL, 10); bd->face = strtol(segments[QQ_INFO_FACE], NULL, 10); + if (alias_utf8 != NULL) { if (bd->nickname) g_free(bd->nickname); bd->nickname = g_strdup(alias_utf8);
--- a/libpurple/protocols/qq/qq.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/qq/qq.c Wed Apr 29 23:20:51 2009 +0000 @@ -674,8 +674,8 @@ g_string_append(info, "wd<br>\n"); g_string_append(info, "x6719620<br>\n"); g_string_append(info, "netelk<br>\n"); - g_string_append(info, "and more, please let me know... thank you!<br>\n"); - g_string_append(info, "<br>\n"); + g_string_append(info, _("and more, please let me know... thank you!))")); + g_string_append(info, "<br>\n<br>\n"); g_string_append(info, _("<p><i>And, all the boys in the backroom...</i><br>\n")); g_string_append(info, _("<i>Feel free to join us!</i> :)")); g_string_append(info, "</body></html>");
--- a/libpurple/protocols/yahoo/yahoo.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/protocols/yahoo/yahoo.c Wed Apr 29 23:20:51 2009 +0000 @@ -2705,13 +2705,20 @@ } /* remove timeout */ - purple_timeout_remove(yd->yahoo_p2p_server_timeout_handle); - yd->yahoo_p2p_server_timeout_handle = 0; + if (yd->yahoo_p2p_server_timeout_handle) { + purple_timeout_remove(yd->yahoo_p2p_server_timeout_handle); + yd->yahoo_p2p_server_timeout_handle = 0; + } /* remove watcher and close p2p server */ - purple_input_remove(yd->yahoo_p2p_server_watcher); - close(yd->yahoo_local_p2p_server_fd); - yd->yahoo_local_p2p_server_fd = -1; + if (yd->yahoo_p2p_server_watcher) { + purple_input_remove(yd->yahoo_p2p_server_watcher); + yd->yahoo_p2p_server_watcher = 0; + } + if (yd->yahoo_local_p2p_server_fd >= 0) { + close(yd->yahoo_local_p2p_server_fd); + yd->yahoo_local_p2p_server_fd = -1; + } /* Add an Input Read event to the file descriptor */ p2p_data->input_event = purple_input_add(acceptfd, PURPLE_INPUT_READ, yahoo_p2p_read_pkt_cb, data); @@ -3769,13 +3776,20 @@ yahoo_c_leave(gc, 1); /* 1 = YAHOO_CHAT_ID */ purple_timeout_remove(yd->yahoo_p2p_timer); - if(yd->yahoo_p2p_server_timeout_handle != 0) + if(yd->yahoo_p2p_server_timeout_handle != 0) { purple_timeout_remove(yd->yahoo_p2p_server_timeout_handle); + yd->yahoo_p2p_server_timeout_handle = 0; + } /* close p2p server if it is waiting for a peer to connect */ - purple_input_remove(yd->yahoo_p2p_server_watcher); - close(yd->yahoo_local_p2p_server_fd); - yd->yahoo_local_p2p_server_fd = -1; + if (yd->yahoo_p2p_server_watcher) { + purple_input_remove(yd->yahoo_p2p_server_watcher); + yd->yahoo_p2p_server_watcher = 0; + } + if (yd->yahoo_local_p2p_server_fd >= 0) { + close(yd->yahoo_local_p2p_server_fd); + yd->yahoo_local_p2p_server_fd = -1; + } g_hash_table_destroy(yd->sms_carrier); g_hash_table_destroy(yd->peers); @@ -4468,7 +4482,6 @@ " bytes, %ld characters. Max is %d bytes, %d chars." " Message is '%s'.\n", lenb, lenc, YAHOO_MAX_MESSAGE_LENGTH_BYTES, YAHOO_MAX_MESSAGE_LENGTH_CHARS, msg2); - yahoo_packet_free(pkt); g_free(msg); g_free(msg2); return -E2BIG;
--- a/libpurple/prpl.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/prpl.c Wed Apr 29 23:20:51 2009 +0000 @@ -182,6 +182,17 @@ } void +purple_prpl_got_account_actions(PurpleAccount *account) +{ + + g_return_if_fail(account != NULL); + g_return_if_fail(purple_account_is_connected(account)); + + purple_signal_emit(purple_accounts_get_handle(), "account-actions-changed", + account); +} + +void purple_prpl_got_user_idle(PurpleAccount *account, const char *name, gboolean idle, time_t idle_time) {
--- a/libpurple/prpl.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/prpl.h Wed Apr 29 23:20:51 2009 +0000 @@ -667,6 +667,20 @@ const char *status_id, ...) G_GNUC_NULL_TERMINATED; /** + * Notifies Purple that our account's actions have changed. This is only + * called after the initial connection. Emits the account-actions-changed + * signal. + * + * This is meant to be called from protocol plugins. + * + * @param account The account. + * + * @see account-actions-changed + * @since 2.6.0 + */ +void purple_prpl_got_account_actions(PurpleAccount *account); + +/** * Notifies Purple that a buddy's idle state and time have changed. * * This is meant to be called from protocol plugins.
--- a/libpurple/savedstatuses.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/savedstatuses.c Wed Apr 29 23:20:51 2009 +0000 @@ -870,15 +870,15 @@ /* Don't need to do anything */ return; - /* Changing our status makes us un-idle */ - if (!idleaway) - purple_idle_touch(); - old = purple_savedstatus_get_current(); saved_status = idleaway ? purple_savedstatus_get_idleaway() : purple_savedstatus_get_default(); purple_prefs_set_bool("/purple/savedstatus/isidleaway", idleaway); + /* Changing our status makes us un-idle */ + if (!idleaway) + purple_idle_touch(); + if (idleaway && (purple_savedstatus_get_type(old) != PURPLE_STATUS_AVAILABLE)) /* Our global status is already "away," so don't change anything */ return;
--- a/libpurple/server.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/server.c Wed Apr 29 23:20:51 2009 +0000 @@ -940,7 +940,8 @@ return; /* Did I send the message? */ - if (purple_strequal(purple_conv_chat_get_nick(chat), who)) { + if (purple_strequal(purple_conv_chat_get_nick(chat), + purple_normalize(purple_conversation_get_account(conv), who))) { flags |= PURPLE_MESSAGE_SEND; flags &= ~PURPLE_MESSAGE_RECV; /* Just in case some prpl sets it! */ } else {
--- a/libpurple/smiley.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/smiley.h Wed Apr 29 23:20:51 2009 +0000 @@ -61,44 +61,41 @@ /*@{*/ /** - * GObject foo. + * GObject-fu. * @internal. */ GType purple_smiley_get_type(void); /** - * Creates a new custom smiley structure and populates it. + * Creates a new custom smiley from a PurpleStoredImage. * - * If a custom smiley with the informed shortcut already exist, it + * If a custom smiley with the given shortcut already exists, it * will be automaticaly returned. * * @param img The image associated with the smiley. - * @param shortcut The custom smiley associated shortcut. + * @param shortcut The associated shortcut (e.g. "(homer)"). * - * @return The custom smiley structure filled up. + * @return The custom smiley. */ PurpleSmiley * purple_smiley_new(PurpleStoredImage *img, const char *shortcut); /** - * Creates a new custom smiley structure and populates it. + * Creates a new custom smiley, reading the image data from a file. * - * The data is retrieved from an already existent file. - * - * If a custom smiley with the informed shortcut already exist, it + * If a custom smiley with the given shortcut already exists, it * will be automaticaly returned. * - * @param shortcut The custom smiley associated shortcut. - * @param filepath The image file to be imported to a - * new custom smiley. + * @param shortcut The associated shortcut (e.g. "(homer)"). + * @param filepath The image file. * - * @return The custom smiley structure filled up. + * @return The custom smiley. */ PurpleSmiley * purple_smiley_new_from_file(const char *shortcut, const char *filepath); /** - * Destroy the custom smiley and release the associated resources. + * Destroys the custom smiley and release the associated resources. * * @param smiley The custom smiley. */ @@ -109,32 +106,28 @@ * Changes the custom smiley's shortcut. * * @param smiley The custom smiley. - * @param shortcut The custom smiley associated shortcut. + * @param shortcut The new shortcut. A custom smiley with this shortcut + * cannot already be in use. * - * @return TRUE whether the shortcut is not associated with another - * custom smiley and the parameters are valid. FALSE otherwise. + * @return TRUE if the shortcut was changed. FALSE otherwise. */ gboolean purple_smiley_set_shortcut(PurpleSmiley *smiley, const char *shortcut); /** - * Changes the custom smiley's data. - * - * When the filename controling is made outside this API, the param - * #keepfilename must be TRUE. - * Otherwise, the file and filename will be regenerated, and the - * old one will be removed. + * Changes the custom smiley's image data. * * @param smiley The custom smiley. - * @param smiley_data The custom smiley data. - * @param smiley_data_len The custom smiley data length. + * @param smiley_data The custom smiley data, which the smiley code + * takes ownership of and will free. + * @param smiley_data_len The length of the data in @a smiley_data. */ void purple_smiley_set_data(PurpleSmiley *smiley, guchar *smiley_data, size_t smiley_data_len); /** - * Returns the custom smiley's associated shortcut. + * Returns the custom smiley's associated shortcut (e.g. "(homer)"). * * @param smiley The custom smiley. * @@ -155,11 +148,11 @@ * Returns the PurpleStoredImage with the reference counter incremented. * * The returned PurpleStoredImage reference counter must be decremented - * after use. + * when the caller is done using it. * * @param smiley The custom smiley. * - * @return A PurpleStoredImage reference. + * @return A PurpleStoredImage. */ PurpleStoredImage *purple_smiley_get_stored_image(const PurpleSmiley *smiley); @@ -167,7 +160,7 @@ * Returns the custom smiley's data. * * @param smiley The custom smiley. - * @param len If not @c NULL, the length of the icon data returned + * @param len If not @c NULL, the length of the image data returned * will be set in the location pointed to by this. * * @return A pointer to the custom smiley data. @@ -194,6 +187,8 @@ * directly. If you find yourself wanting to use this function, think * very long and hard about it, and then don't. * + * Think some more. + * * @param smiley The custom smiley. * * @return A full path to the file, or @c NULL under various conditions. @@ -210,7 +205,8 @@ /*@{*/ /** - * Returns a list of all custom smileys. The caller should free the list. + * Returns a list of all custom smileys. The caller is responsible for freeing + * the list. * * @return A list of all custom smileys. */ @@ -218,23 +214,21 @@ purple_smileys_get_all(void); /** - * Returns the custom smiley given it's shortcut. + * Returns a custom smiley given its shortcut. * * @param shortcut The custom smiley's shortcut. * - * @return The custom smiley (with a reference for the caller) if found, - * or @c NULL if not found. + * @return The custom smiley if found, or @c NULL if not found. */ PurpleSmiley * purple_smileys_find_by_shortcut(const char *shortcut); /** - * Returns the custom smiley given it's checksum. + * Returns a custom smiley given its checksum. * * @param checksum The custom smiley's checksum. * - * @return The custom smiley (with a reference for the caller) if found, - * or @c NULL if not found. + * @return The custom smiley if found, or @c NULL if not found. */ PurpleSmiley * purple_smileys_find_by_checksum(const char *checksum); @@ -242,10 +236,9 @@ /** * Returns the directory used to store custom smiley cached files. * - * The default directory is PURPLEDIR/smileys, unless otherwise specified - * by purple_buddy_icons_set_cache_dir(). + * The default directory is PURPLEDIR/custom_smiley. * - * @return The directory to store custom smyles cached files to. + * @return The directory in which to store custom smileys cached files. */ const char *purple_smileys_get_storing_dir(void);
--- a/libpurple/sound-theme.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/sound-theme.h Wed Apr 29 23:20:51 2009 +0000 @@ -73,6 +73,7 @@ /** * Returns a copy of the filename for the sound event. * + * @param theme The theme. * @param event The purple sound event to look up. * * @returns The filename of the sound event. @@ -83,6 +84,7 @@ /** * Returns a copy of the directory and filename for the sound event * + * @param theme The theme. * @param event The purple sound event to look up * * @returns The directory + '/' + filename of the sound event. This is @@ -94,8 +96,9 @@ /** * Sets the filename for a given sound event * - * @param event the purple sound event to look up - * @param filename the name of the file to be used for the event + * @param theme The theme. + * @param event the purple sound event to look up + * @param filename the name of the file to be used for the event */ void purple_sound_theme_set_file(PurpleSoundTheme *theme, const gchar *event,
--- a/libpurple/theme-loader.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/theme-loader.h Wed Apr 29 23:20:51 2009 +0000 @@ -82,7 +82,8 @@ /** * Creates a new PurpleTheme * - * @param dir The directory containing the theme + * @param loader The theme loader + * @param dir The directory containing the theme * * @returns A PurpleTheme containing the information from the directory */
--- a/libpurple/theme-manager.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/theme-manager.h Wed Apr 29 23:20:51 2009 +0000 @@ -1,5 +1,5 @@ /** - * @file thememanager.h Theme Manager API + * @file theme-manager.h Theme Manager API */ /*
--- a/libpurple/xmlnode.c Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/xmlnode.c Wed Apr 29 23:20:51 2009 +0000 @@ -647,6 +647,28 @@ purple_debug_error("xmlnode", "Error parsing xml file: %s", errmsg); } +static void +xmlnode_parser_structural_error_libxml(void *user_data, xmlErrorPtr error) +{ + struct _xmlnode_parser_data *xpd = user_data; + + if (error && (error->level == XML_ERR_ERROR || + error->level == XML_ERR_FATAL)) { + xpd->error = TRUE; + purple_debug_error("xmlnode", "XML parser error for xmlnode %p: " + "Domain %i, code %i, level %i: %s", + user_data, error->domain, error->code, error->level, + error->message ? error->message : "(null)\n"); + } else if (error) + purple_debug_warning("xmlnode", "XML parser error for xmlnode %p: " + "Domain %i, code %i, level %i: %s", + user_data, error->domain, error->code, error->level, + error->message ? error->message : "(null)\n"); + else + purple_debug_warning("xmlnode", "XML parser error for xmlnode %p\n", + user_data); +} + static xmlSAXHandler xmlnode_parser_libxml = { NULL, /* internalSubset */ NULL, /* isStandalone */ @@ -679,7 +701,7 @@ NULL, /* _private */ xmlnode_parser_element_start_libxml, /* startElementNs */ xmlnode_parser_element_end_libxml, /* endElementNs */ - NULL, /* serror */ + xmlnode_parser_structural_error_libxml, /* serror */ }; xmlnode *
--- a/libpurple/xmlnode.h Sat Apr 18 06:52:59 2009 +0000 +++ b/libpurple/xmlnode.h Wed Apr 29 23:20:51 2009 +0000 @@ -335,11 +335,14 @@ * root node of an XML document will parse the entire document * into a tree of nodes, and return the xmlnode of the root. * - * @param str The string of xml. - * @param description The description of the file being parsed - * @process The utility that is calling xmlnode_from_file + * @param dir The directory where the file is located + * @param filename The filename + * @param description A description of the file being parsed. Displayed to + * the user if the file cannot be read. + * @param process The subsystem that is calling xmlnode_from_file. Used as + * the category for debugging. * - * @return The new node. + * @return The new node or NULL if an error occurred. * * @since 2.6.0 */
--- a/pidgin/gtkaccount.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkaccount.c Wed Apr 29 23:20:51 2009 +0000 @@ -413,7 +413,11 @@ if (dialog->protocol_menu != NULL) { +#if GTK_CHECK_VERSION(2,12,0) + g_object_ref(G_OBJECT(dialog->protocol_menu)); +#else gtk_widget_ref(dialog->protocol_menu); +#endif hbox = g_object_get_data(G_OBJECT(dialog->protocol_menu), "container"); gtk_container_remove(GTK_CONTAINER(hbox), dialog->protocol_menu); } @@ -440,13 +444,21 @@ { dialog->protocol_menu = pidgin_protocol_option_menu_new( dialog->protocol_id, G_CALLBACK(set_account_protocol_cb), dialog); +#if GTK_CHECK_VERSION(2,12,0) + g_object_ref(G_OBJECT(dialog->protocol_menu)); +#else gtk_widget_ref(dialog->protocol_menu); +#endif } hbox = add_pref_box(dialog, vbox, _("Pro_tocol:"), dialog->protocol_menu); g_object_set_data(G_OBJECT(dialog->protocol_menu), "container", hbox); +#if GTK_CHECK_VERSION(2,12,0) + g_object_unref(G_OBJECT(dialog->protocol_menu)); +#else gtk_widget_unref(dialog->protocol_menu); +#endif /* Username */ dialog->username_entry = gtk_entry_new();
--- a/pidgin/gtkblist-theme-loader.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkblist-theme-loader.c Wed Apr 29 23:20:51 2009 +0000 @@ -38,6 +38,22 @@ * Buddy List Theme Builder *****************************************************************************/ +static PidginThemeFont * +pidgin_theme_font_parse(xmlnode *node) +{ + const char *font; + const char *colordesc; + GdkColor color; + + font = xmlnode_get_attrib(node, "font"); + + if ((colordesc = xmlnode_get_attrib(node, "color")) == NULL || + !gdk_color_parse(colordesc, &color)) + gdk_color_parse(DEFAULT_TEXT_COLOR, &color); + + return pidgin_theme_font_new(font, &color); +} + static PurpleTheme * pidgin_blist_loader_build(const gchar *dir) { @@ -46,10 +62,35 @@ const gchar *temp; gboolean success = TRUE; GdkColor bgcolor, expanded_bgcolor, collapsed_bgcolor, contact_color; - GdkColor color; - FontColorPair expanded, collapsed, contact, online, away, offline, idle, message, message_nick_said, status; + PidginThemeFont *expanded, *collapsed, *contact, *online, *away, *offline, *idle, *message, *message_nick_said, *status; PidginBlistLayout layout; PidginBlistTheme *theme; + int i; + struct { + const char *tag; + PidginThemeFont **font; + } lookups[] = { + {"contact_text", &contact}, + {"online_text", &online}, + {"away_text", &away}, + {"offline_text", &offline}, + {"idle_text", &idle}, + {"message_text", &message}, + {"message_nick_said_text", &message_nick_said}, + {"status_text", &status}, + {NULL, NULL} + }; + + expanded = NULL; + collapsed = NULL; + contact = NULL; + online = NULL; + away = NULL; + offline = NULL; + idle = NULL; + message = NULL; + message_nick_said = NULL; + status = NULL; /* Find the theme file */ g_return_val_if_fail(dir != NULL, NULL); @@ -76,11 +117,7 @@ if ((success = (success && (sub_node = xmlnode_get_child(root_node, "groups")) != NULL && (sub_sub_node = xmlnode_get_child(sub_node, "expanded")) != NULL))) { - expanded.font = xmlnode_get_attrib(sub_sub_node, "font"); - - if ((temp = xmlnode_get_attrib(sub_sub_node, "text_color")) != NULL && gdk_color_parse(temp, &color)) - expanded.color = temp; - else expanded.color = DEFAULT_TEXT_COLOR; + expanded = pidgin_theme_font_parse(sub_sub_node); if ((temp = xmlnode_get_attrib(sub_sub_node, "background")) != NULL && gdk_color_parse(temp, &expanded_bgcolor)) gdk_colormap_alloc_color(gdk_colormap_get_system(), &expanded_bgcolor, FALSE, TRUE); @@ -90,11 +127,7 @@ if ((success = (success && sub_node != NULL && (sub_sub_node = xmlnode_get_child(sub_node, "collapsed")) != NULL))) { - collapsed.font = xmlnode_get_attrib(sub_sub_node, "font"); - - if((temp = xmlnode_get_attrib(sub_sub_node, "text_color")) != NULL && gdk_color_parse(temp, &color)) - collapsed.color = temp; - else collapsed.color = DEFAULT_TEXT_COLOR; + collapsed = pidgin_theme_font_parse(sub_sub_node); if ((temp = xmlnode_get_attrib(sub_sub_node, "background")) != NULL && gdk_color_parse(temp, &collapsed_bgcolor)) gdk_colormap_alloc_color(gdk_colormap_get_system(), &collapsed_bgcolor, FALSE, TRUE); @@ -121,60 +154,13 @@ memset(&contact_color, 0, sizeof(GdkColor)); } - if ((success = (success && sub_node != NULL && (sub_sub_node = xmlnode_get_child(sub_node, "contact_text")) != NULL))) { - contact.font = xmlnode_get_attrib(sub_sub_node, "font"); - if(gdk_color_parse(temp = xmlnode_get_attrib(sub_sub_node, "color"), &color)) - contact.color = temp; - else contact.color = DEFAULT_TEXT_COLOR; - } - - if ((success = (success && sub_node != NULL && (sub_sub_node = xmlnode_get_child(sub_node, "online_text")) != NULL))) { - online.font = xmlnode_get_attrib(sub_sub_node, "font"); - if(gdk_color_parse(temp = xmlnode_get_attrib(sub_sub_node, "color"), &color)) - online.color = temp; - else online.color = DEFAULT_TEXT_COLOR; - } - - if ((success = (success && sub_node != NULL && (sub_sub_node = xmlnode_get_child(sub_node, "away_text")) != NULL))) { - away.font = xmlnode_get_attrib(sub_sub_node, "font"); - if(gdk_color_parse(temp = xmlnode_get_attrib(sub_sub_node, "color"), &color)) - away.color = temp; - else away.color = DEFAULT_TEXT_COLOR; - } - - if ((success = (success && sub_node != NULL && (sub_sub_node = xmlnode_get_child(sub_node, "offline_text")) != NULL))) { - offline.font = xmlnode_get_attrib(sub_sub_node, "font"); - if(gdk_color_parse(temp = xmlnode_get_attrib(sub_sub_node, "color"), &color)) - offline.color = temp; - else offline.color = DEFAULT_TEXT_COLOR; - } - - if ((success = (success && sub_node != NULL && (sub_sub_node = xmlnode_get_child(sub_node, "idle_text")) != NULL))) { - idle.font = xmlnode_get_attrib(sub_sub_node, "font"); - if(gdk_color_parse(temp = xmlnode_get_attrib(sub_sub_node, "color"), &color)) - idle.color = temp; - else idle.color = DEFAULT_TEXT_COLOR; - } - - if ((success = (success && sub_node != NULL && (sub_sub_node = xmlnode_get_child(sub_node, "message_text")) != NULL))) { - message.font = xmlnode_get_attrib(sub_sub_node, "font"); - if(gdk_color_parse(temp = xmlnode_get_attrib(sub_sub_node, "color"), &color)) - message.color = temp; - else message.color = DEFAULT_TEXT_COLOR; - } - - if ((success = (success && sub_node != NULL && (sub_sub_node = xmlnode_get_child(sub_node, "message_nick_said_text")) != NULL))) { - message_nick_said.font = xmlnode_get_attrib(sub_sub_node, "font"); - if(gdk_color_parse(temp = xmlnode_get_attrib(sub_sub_node, "color"), &color)) - message_nick_said.color = temp; - else message_nick_said.color = DEFAULT_TEXT_COLOR; - } - - if ((success = (success && sub_node != NULL && (sub_sub_node = xmlnode_get_child(sub_node, "status_text")) != NULL))) { - status.font = xmlnode_get_attrib(sub_sub_node, "font"); - if(gdk_color_parse(temp = xmlnode_get_attrib(sub_sub_node, "color"), &color)) - status.color = temp; - else status.color = DEFAULT_TEXT_COLOR; + for (i = 0; success && lookups[i].tag; i++) { + if ((success = (sub_node != NULL && + (sub_sub_node = xmlnode_get_child(sub_node, lookups[i].tag)) != NULL))) { + *(lookups[i].font) = pidgin_theme_font_parse(sub_sub_node); + } else { + *(lookups[i].font) = NULL; + } } /* name is required for theme manager */ @@ -191,18 +177,27 @@ "background-color", &bgcolor, "layout", &layout, "expanded-color", &expanded_bgcolor, - "expanded-text", &expanded, + "expanded-text", expanded, "collapsed-color", &collapsed_bgcolor, - "collapsed-text", &collapsed, + "collapsed-text", collapsed, "contact-color", &contact_color, - "contact", &contact, - "online", &online, - "away", &away, - "offline", &offline, - "idle", &idle, - "message", &message, - "message_nick_said", &message_nick_said, - "status", &status, NULL); + "contact", contact, + "online", online, + "away", away, + "offline", offline, + "idle", idle, + "message", message, + "message_nick_said", message_nick_said, + "status", status, NULL); + + for (i = 0; lookups[i].tag; i++) { + if (*lookups[i].font) { + pidgin_theme_font_free(*lookups[i].font); + } + } + + pidgin_theme_font_free(expanded); + pidgin_theme_font_free(collapsed); xmlnode_free(root_node); g_free(data);
--- a/pidgin/gtkblist-theme-loader.h Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkblist-theme-loader.h Wed Apr 29 23:20:51 2009 +0000 @@ -1,5 +1,5 @@ /** - * @file gtkblist-loader.h Pidgin Buddy List Theme Loader Class API + * @file gtkblist-theme-loader.h Pidgin Buddy List Theme Loader Class API */ /* pidgin
--- a/pidgin/gtkblist-theme.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkblist-theme.c Wed Apr 29 23:20:51 2009 +0000 @@ -20,6 +20,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA */ +#include "internal.h" #include "gtkblist-theme.h" #define PIDGIN_BLIST_THEME_GET_PRIVATE(Gobject) \ @@ -37,27 +38,34 @@ /* groups */ GdkColor *expanded_color; - FontColorPair *expanded; + PidginThemeFont *expanded; GdkColor *collapsed_color; - FontColorPair *collapsed; + PidginThemeFont *collapsed; /* buddy */ GdkColor *contact_color; - FontColorPair *contact; + PidginThemeFont *contact; - FontColorPair *online; - FontColorPair *away; - FontColorPair *offline; - FontColorPair *idle; - FontColorPair *message; - FontColorPair *message_nick_said; + PidginThemeFont *online; + PidginThemeFont *away; + PidginThemeFont *offline; + PidginThemeFont *idle; + PidginThemeFont *message; + PidginThemeFont *message_nick_said; - FontColorPair *status; + PidginThemeFont *status; } PidginBlistThemePrivate; +struct _PidginThemeFont +{ + gchar *font; + gchar color[10]; + GdkColor *gdkcolor; +}; + /****************************************************************************** * Globals *****************************************************************************/ @@ -92,23 +100,83 @@ * Helpers *****************************************************************************/ +PidginThemeFont * +pidgin_theme_font_new(const gchar *face, GdkColor *color) +{ + PidginThemeFont *font = g_new0(PidginThemeFont, 1); + font->font = g_strdup(face); + if (color) + pidgin_theme_font_set_color(font, color); + return font; +} + void -free_font_and_color(FontColorPair *pair) +pidgin_theme_font_free(PidginThemeFont *pair) { if (pair != NULL) { - g_free((gchar *)pair->font); - g_free((gchar *)pair->color); + g_free(pair->font); + if (pair->gdkcolor) + gdk_color_free(pair->gdkcolor); g_free(pair); } } -static FontColorPair * -copy_font_and_color(const FontColorPair *pair) +static PidginThemeFont * +copy_font_and_color(const PidginThemeFont *pair) +{ + PidginThemeFont *copy = g_new0(PidginThemeFont, 1); + copy->font = g_strdup(pair->font); + strncpy(copy->color, pair->color, sizeof(copy->color) - 1); + if (pair->gdkcolor) + copy->gdkcolor = gdk_color_copy(pair->gdkcolor); + return copy; +} + +void +pidgin_theme_font_set_font_face(PidginThemeFont *font, const gchar *face) +{ + g_return_if_fail(font); + g_return_if_fail(face); + + g_free(font->font); + font->font = g_strdup(face); +} + +void +pidgin_theme_font_set_color(PidginThemeFont *font, const GdkColor *color) { - FontColorPair *copy = g_new0(FontColorPair, 1); - copy->font = g_strdup(pair->font); - copy->color = g_strdup(pair->color); - return copy; + g_return_if_fail(font); + + if (font->gdkcolor) + gdk_color_free(font->gdkcolor); + + font->gdkcolor = color ? gdk_color_copy(color) : NULL; + if (color) + g_snprintf(font->color, sizeof(font->color), + "#%02x%02x%02x", color->red >> 8, color->green >> 8, color->blue >> 8); + else + font->color[0] = '\0'; +} + +const gchar * +pidgin_theme_font_get_font_face(PidginThemeFont *font) +{ + g_return_val_if_fail(font, NULL); + return font->font; +} + +const GdkColor * +pidgin_theme_font_get_color(PidginThemeFont *font) +{ + g_return_val_if_fail(font, NULL); + return font->gdkcolor; +} + +const gchar * +pidgin_theme_font_get_color_describe(PidginThemeFont *font) +{ + g_return_val_if_fail(font, NULL); + return font->color[0] ? font->color : NULL; } /****************************************************************************** @@ -130,7 +198,7 @@ switch (param_id) { case PROP_BACKGROUND_COLOR: - g_value_set_pointer(value, pidgin_blist_theme_get_background_color(theme)); + g_value_set_boxed(value, pidgin_blist_theme_get_background_color(theme)); break; case PROP_OPACITY: g_value_set_double(value, pidgin_blist_theme_get_opacity(theme)); @@ -139,19 +207,19 @@ g_value_set_pointer(value, pidgin_blist_theme_get_layout(theme)); break; case PROP_EXPANDED_COLOR: - g_value_set_pointer(value, pidgin_blist_theme_get_expanded_background_color(theme)); + g_value_set_boxed(value, pidgin_blist_theme_get_expanded_background_color(theme)); break; case PROP_EXPANDED_TEXT: g_value_set_pointer(value, pidgin_blist_theme_get_expanded_text_info(theme)); break; case PROP_COLLAPSED_COLOR: - g_value_set_pointer(value, pidgin_blist_theme_get_collapsed_background_color(theme)); + g_value_set_boxed(value, pidgin_blist_theme_get_collapsed_background_color(theme)); break; case PROP_COLLAPSED_TEXT: g_value_set_pointer(value, pidgin_blist_theme_get_collapsed_text_info(theme)); break; case PROP_CONTACT_COLOR: - g_value_set_pointer(value, pidgin_blist_theme_get_contact_color(theme)); + g_value_set_boxed(value, pidgin_blist_theme_get_contact_color(theme)); break; case PROP_CONTACT: g_value_set_pointer(value, pidgin_blist_theme_get_contact_text_info(theme)); @@ -191,7 +259,7 @@ switch (param_id) { case PROP_BACKGROUND_COLOR: - pidgin_blist_theme_set_background_color(theme, g_value_get_pointer(value)); + pidgin_blist_theme_set_background_color(theme, g_value_get_boxed(value)); break; case PROP_OPACITY: pidgin_blist_theme_set_opacity(theme, g_value_get_double(value)); @@ -200,19 +268,19 @@ pidgin_blist_theme_set_layout(theme, g_value_get_pointer(value)); break; case PROP_EXPANDED_COLOR: - pidgin_blist_theme_set_expanded_background_color(theme, g_value_get_pointer(value)); + pidgin_blist_theme_set_expanded_background_color(theme, g_value_get_boxed(value)); break; case PROP_EXPANDED_TEXT: pidgin_blist_theme_set_expanded_text_info(theme, g_value_get_pointer(value)); break; case PROP_COLLAPSED_COLOR: - pidgin_blist_theme_set_collapsed_background_color(theme, g_value_get_pointer(value)); + pidgin_blist_theme_set_collapsed_background_color(theme, g_value_get_boxed(value)); break; case PROP_COLLAPSED_TEXT: pidgin_blist_theme_set_collapsed_text_info(theme, g_value_get_pointer(value)); break; case PROP_CONTACT_COLOR: - pidgin_blist_theme_set_contact_color(theme, g_value_get_pointer(value)); + pidgin_blist_theme_set_contact_color(theme, g_value_get_boxed(value)); break; case PROP_CONTACT: pidgin_blist_theme_set_contact_text_info(theme, g_value_get_pointer(value)); @@ -252,25 +320,29 @@ priv = PIDGIN_BLIST_THEME_GET_PRIVATE(obj); /* Buddy List */ - gdk_color_free(priv->bgcolor); + if (priv->bgcolor) + gdk_color_free(priv->bgcolor); g_free(priv->layout); /* Group */ - gdk_color_free(priv->expanded_color); - free_font_and_color(priv->expanded); - gdk_color_free(priv->collapsed_color); - free_font_and_color(priv->collapsed); + if (priv->expanded_color) + gdk_color_free(priv->expanded_color); + pidgin_theme_font_free(priv->expanded); + if (priv->collapsed_color) + gdk_color_free(priv->collapsed_color); + pidgin_theme_font_free(priv->collapsed); /* Buddy */ - gdk_color_free(priv->contact_color); - free_font_and_color(priv->contact); - free_font_and_color(priv->online); - free_font_and_color(priv->away); - free_font_and_color(priv->offline); - free_font_and_color(priv->idle); - free_font_and_color(priv->message); - free_font_and_color(priv->message_nick_said); - free_font_and_color(priv->status); + if (priv->contact_color) + gdk_color_free(priv->contact_color); + pidgin_theme_font_free(priv->contact); + pidgin_theme_font_free(priv->online); + pidgin_theme_font_free(priv->away); + pidgin_theme_font_free(priv->offline); + pidgin_theme_font_free(priv->idle); + pidgin_theme_font_free(priv->message); + pidgin_theme_font_free(priv->message_nick_said); + pidgin_theme_font_free(priv->status); g_free(priv); @@ -290,81 +362,81 @@ obj_class->finalize = pidgin_blist_theme_finalize; /* Buddy List */ - pspec = g_param_spec_pointer("background-color", "Background Color", - "The background color for the buddy list", - G_PARAM_READWRITE); + pspec = g_param_spec_boxed("background-color", _("Background Color"), + _("The background color for the buddy list"), + GDK_TYPE_COLOR, G_PARAM_READWRITE); g_object_class_install_property(obj_class, PROP_BACKGROUND_COLOR, pspec); - pspec = g_param_spec_pointer("layout", "Layout", - "The layout of icons, name, and status of the blist", + pspec = g_param_spec_pointer("layout", _("Layout"), + _("The layout of icons, name, and status of the blist"), G_PARAM_READWRITE); g_object_class_install_property(obj_class, PROP_LAYOUT, pspec); /* Group */ - pspec = g_param_spec_pointer("expanded-color", "Expanded Background Color", - "The background color of an expanded group", - G_PARAM_READWRITE); + pspec = g_param_spec_boxed("expanded-color", _("Expanded Background Color"), + _("The background color of an expanded group"), + GDK_TYPE_COLOR, G_PARAM_READWRITE); g_object_class_install_property(obj_class, PROP_EXPANDED_COLOR, pspec); - pspec = g_param_spec_pointer("expanded-text", "Expanded Text", - "The text information for when a group is expanded", + pspec = g_param_spec_pointer("expanded-text", _("Expanded Text"), + _("The text information for when a group is expanded"), G_PARAM_READWRITE); g_object_class_install_property(obj_class, PROP_EXPANDED_TEXT, pspec); - pspec = g_param_spec_pointer("collapsed-color", "Collapsed Background Color", - "The background color of a collapsed group", - G_PARAM_READWRITE); + pspec = g_param_spec_boxed("collapsed-color", _("Collapsed Background Color"), + _("The background color of a collapsed group"), + GDK_TYPE_COLOR, G_PARAM_READWRITE); g_object_class_install_property(obj_class, PROP_COLLAPSED_COLOR, pspec); - pspec = g_param_spec_pointer("collapsed-text", "Collapsed Text", - "The text information for when a group is collapsed", + pspec = g_param_spec_pointer("collapsed-text", _("Collapsed Text"), + _("The text information for when a group is collapsed"), G_PARAM_READWRITE); g_object_class_install_property(obj_class, PROP_COLLAPSED_TEXT, pspec); /* Buddy */ - pspec = g_param_spec_pointer("contact-color", "Contact/Chat Background Color", - "The background color of a contact or chat", - G_PARAM_READWRITE); + pspec = g_param_spec_boxed("contact-color", _("Contact/Chat Background Color"), + _("The background color of a contact or chat"), + GDK_TYPE_COLOR, G_PARAM_READWRITE); g_object_class_install_property(obj_class, PROP_CONTACT_COLOR, pspec); - pspec = g_param_spec_pointer("contact", "Contact Text", - "The text information for when a contact is expanded", + pspec = g_param_spec_pointer("contact", _("Contact Text"), + _("The text information for when a contact is expanded"), G_PARAM_READWRITE); g_object_class_install_property(obj_class, PROP_CONTACT, pspec); - pspec = g_param_spec_pointer("online", "On-line Text", - "The text information for when a buddy is online", + pspec = g_param_spec_pointer("online", _("On-line Text"), + _("The text information for when a buddy is online"), G_PARAM_READWRITE); g_object_class_install_property(obj_class, PROP_ONLINE, pspec); - pspec = g_param_spec_pointer("away", "Away Text", - "The text information for when a buddy is away", + pspec = g_param_spec_pointer("away", _("Away Text"), + _("The text information for when a buddy is away"), G_PARAM_READWRITE); g_object_class_install_property(obj_class, PROP_AWAY, pspec); - pspec = g_param_spec_pointer("offline", "Off-line Text", - "The text information for when a buddy is off-line", + pspec = g_param_spec_pointer("offline", _("Off-line Text"), + _("The text information for when a buddy is off-line"), G_PARAM_READWRITE); g_object_class_install_property(obj_class, PROP_OFFLINE, pspec); - pspec = g_param_spec_pointer("idle", "Idle Text", - "The text information for when a buddy is idle", + pspec = g_param_spec_pointer("idle", _("Idle Text"), + _("The text information for when a buddy is idle"), G_PARAM_READWRITE); g_object_class_install_property(obj_class, PROP_IDLE, pspec); - pspec = g_param_spec_pointer("message", "Message Text", - "The text information for when a buddy has an unread message", + pspec = g_param_spec_pointer("message", _("Message Text"), + _("The text information for when a buddy has an unread message"), G_PARAM_READWRITE); g_object_class_install_property(obj_class, PROP_MESSAGE, pspec); - pspec = g_param_spec_pointer("message_nick_said", "Message (Nick Said) Text", - "The text information for when a chat has an unread message that mentions your nick", + pspec = g_param_spec_pointer("message_nick_said", _("Message (Nick Said) Text"), + _("The text information for when a chat has an unread message that mentions your nick"), G_PARAM_READWRITE); g_object_class_install_property(obj_class, PROP_MESSAGE_NICK_SAID, pspec); - pspec = g_param_spec_pointer("status", "Status Text", - "The text information for a buddy's status", + pspec = g_param_spec_pointer("status", _("Status Text"), + _("The text information for a buddy's status"), G_PARAM_READWRITE); g_object_class_install_property(obj_class, PROP_STATUS, pspec); } @@ -447,7 +519,7 @@ return priv->expanded_color; } -FontColorPair * +PidginThemeFont * pidgin_blist_theme_get_expanded_text_info(PidginBlistTheme *theme) { PidginBlistThemePrivate *priv; @@ -471,7 +543,7 @@ return priv->collapsed_color; } -FontColorPair * +PidginThemeFont * pidgin_blist_theme_get_collapsed_text_info(PidginBlistTheme *theme) { PidginBlistThemePrivate *priv; @@ -495,7 +567,7 @@ return priv->contact_color; } -FontColorPair * +PidginThemeFont * pidgin_blist_theme_get_contact_text_info(PidginBlistTheme *theme) { PidginBlistThemePrivate *priv; @@ -507,7 +579,7 @@ return priv->contact; } -FontColorPair * +PidginThemeFont * pidgin_blist_theme_get_online_text_info(PidginBlistTheme *theme) { PidginBlistThemePrivate *priv; @@ -519,7 +591,7 @@ return priv->online; } -FontColorPair * +PidginThemeFont * pidgin_blist_theme_get_away_text_info(PidginBlistTheme *theme) { PidginBlistThemePrivate *priv; @@ -531,7 +603,7 @@ return priv->away; } -FontColorPair * +PidginThemeFont * pidgin_blist_theme_get_offline_text_info(PidginBlistTheme *theme) { PidginBlistThemePrivate *priv; @@ -543,7 +615,7 @@ return priv->offline; } -FontColorPair * +PidginThemeFont * pidgin_blist_theme_get_idle_text_info(PidginBlistTheme *theme) { PidginBlistThemePrivate *priv; @@ -555,7 +627,7 @@ return priv->idle; } -FontColorPair * +PidginThemeFont * pidgin_blist_theme_get_unread_message_text_info(PidginBlistTheme *theme) { PidginBlistThemePrivate *priv; @@ -567,7 +639,7 @@ return priv->message; } -FontColorPair * +PidginThemeFont * pidgin_blist_theme_get_unread_message_nick_said_text_info(PidginBlistTheme *theme) { PidginBlistThemePrivate *priv; @@ -579,7 +651,7 @@ return priv->message_nick_said; } -FontColorPair * +PidginThemeFont * pidgin_blist_theme_get_status_text_info(PidginBlistTheme *theme) { PidginBlistThemePrivate *priv; @@ -601,7 +673,8 @@ priv = PIDGIN_BLIST_THEME_GET_PRIVATE(G_OBJECT(theme)); - gdk_color_free(priv->bgcolor); + if (priv->bgcolor) + gdk_color_free(priv->bgcolor); priv->bgcolor = gdk_color_copy(color); } @@ -639,12 +712,13 @@ priv = PIDGIN_BLIST_THEME_GET_PRIVATE(G_OBJECT(theme)); - gdk_color_free(priv->expanded_color); + if (priv->expanded_color) + gdk_color_free(priv->expanded_color); priv->expanded_color = gdk_color_copy(color); } void -pidgin_blist_theme_set_expanded_text_info(PidginBlistTheme *theme, const FontColorPair *pair) +pidgin_blist_theme_set_expanded_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair) { PidginBlistThemePrivate *priv; @@ -652,7 +726,7 @@ priv = PIDGIN_BLIST_THEME_GET_PRIVATE(G_OBJECT(theme)); - free_font_and_color(priv->expanded); + pidgin_theme_font_free(priv->expanded); priv->expanded = copy_font_and_color(pair); } @@ -665,12 +739,13 @@ priv = PIDGIN_BLIST_THEME_GET_PRIVATE(G_OBJECT(theme)); - gdk_color_free(priv->collapsed_color); + if (priv->collapsed_color) + gdk_color_free(priv->collapsed_color); priv->collapsed_color = gdk_color_copy(color); } void -pidgin_blist_theme_set_collapsed_text_info(PidginBlistTheme *theme, const FontColorPair *pair) +pidgin_blist_theme_set_collapsed_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair) { PidginBlistThemePrivate *priv; @@ -678,7 +753,7 @@ priv = PIDGIN_BLIST_THEME_GET_PRIVATE(G_OBJECT(theme)); - free_font_and_color(priv->collapsed); + pidgin_theme_font_free(priv->collapsed); priv->collapsed = copy_font_and_color(pair); } @@ -691,12 +766,13 @@ priv = PIDGIN_BLIST_THEME_GET_PRIVATE(G_OBJECT(theme)); - gdk_color_free(priv->contact_color); + if (priv->contact_color) + gdk_color_free(priv->contact_color); priv->contact_color = gdk_color_copy(color); } void -pidgin_blist_theme_set_contact_text_info(PidginBlistTheme *theme, const FontColorPair *pair) +pidgin_blist_theme_set_contact_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair) { PidginBlistThemePrivate *priv; @@ -704,12 +780,12 @@ priv = PIDGIN_BLIST_THEME_GET_PRIVATE(G_OBJECT(theme)); - free_font_and_color(priv->contact); + pidgin_theme_font_free(priv->contact); priv->contact = copy_font_and_color(pair); } void -pidgin_blist_theme_set_online_text_info(PidginBlistTheme *theme, const FontColorPair *pair) +pidgin_blist_theme_set_online_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair) { PidginBlistThemePrivate *priv; @@ -717,12 +793,12 @@ priv = PIDGIN_BLIST_THEME_GET_PRIVATE(G_OBJECT(theme)); - free_font_and_color(priv->online); + pidgin_theme_font_free(priv->online); priv->online = copy_font_and_color(pair); } void -pidgin_blist_theme_set_away_text_info(PidginBlistTheme *theme, const FontColorPair *pair) +pidgin_blist_theme_set_away_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair) { PidginBlistThemePrivate *priv; @@ -730,12 +806,12 @@ priv = PIDGIN_BLIST_THEME_GET_PRIVATE(G_OBJECT(theme)); - free_font_and_color(priv->away); + pidgin_theme_font_free(priv->away); priv->away = copy_font_and_color(pair); } void -pidgin_blist_theme_set_offline_text_info(PidginBlistTheme *theme, const FontColorPair *pair) +pidgin_blist_theme_set_offline_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair) { PidginBlistThemePrivate *priv; @@ -743,12 +819,12 @@ priv = PIDGIN_BLIST_THEME_GET_PRIVATE(G_OBJECT(theme)); - free_font_and_color(priv->offline); + pidgin_theme_font_free(priv->offline); priv->offline = copy_font_and_color(pair); } void -pidgin_blist_theme_set_idle_text_info(PidginBlistTheme *theme, const FontColorPair *pair) +pidgin_blist_theme_set_idle_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair) { PidginBlistThemePrivate *priv; @@ -756,12 +832,12 @@ priv = PIDGIN_BLIST_THEME_GET_PRIVATE(G_OBJECT(theme)); - free_font_and_color(priv->idle); + pidgin_theme_font_free(priv->idle); priv->idle = copy_font_and_color(pair); } void -pidgin_blist_theme_set_unread_message_text_info(PidginBlistTheme *theme, const FontColorPair *pair) +pidgin_blist_theme_set_unread_message_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair) { PidginBlistThemePrivate *priv; @@ -769,12 +845,12 @@ priv = PIDGIN_BLIST_THEME_GET_PRIVATE(G_OBJECT(theme)); - free_font_and_color(priv->message); + pidgin_theme_font_free(priv->message); priv->message = copy_font_and_color(pair); } void -pidgin_blist_theme_set_unread_message_nick_said_text_info(PidginBlistTheme *theme, const FontColorPair *pair) +pidgin_blist_theme_set_unread_message_nick_said_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair) { PidginBlistThemePrivate *priv; @@ -782,12 +858,12 @@ priv = PIDGIN_BLIST_THEME_GET_PRIVATE(G_OBJECT(theme)); - free_font_and_color(priv->message_nick_said); + pidgin_theme_font_free(priv->message_nick_said); priv->message_nick_said = copy_font_and_color(pair); } void -pidgin_blist_theme_set_status_text_info(PidginBlistTheme *theme, const FontColorPair *pair) +pidgin_blist_theme_set_status_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair) { PidginBlistThemePrivate *priv; @@ -795,6 +871,6 @@ priv = PIDGIN_BLIST_THEME_GET_PRIVATE(G_OBJECT(theme)); - free_font_and_color(priv->status); + pidgin_theme_font_free(priv->status); priv->status = copy_font_and_color(pair); }
--- a/pidgin/gtkblist-theme.h Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkblist-theme.h Wed Apr 29 23:20:51 2009 +0000 @@ -59,12 +59,15 @@ PurpleThemeClass parent_class; }; +#if 0 typedef struct { const gchar *font; const gchar *color; -} FontColorPair; +} PidginThemeFont; +#endif +typedef struct _PidginThemeFont PidginThemeFont; typedef struct { @@ -78,13 +81,68 @@ } PidginBlistLayout; /**************************************************************************/ -/** @name FontColorPair API */ +/** @name PidginThemeFont API */ /**************************************************************************/ /** + * Create a new PidginThemeFont. + * + * @param face The font face + * @param color The color of the font + * + * @return A newly created PidginThemeFont + */ +PidginThemeFont * pidgin_theme_font_new(const gchar *face, GdkColor *color); + +/** * Frees a font and color pair + * + * @param font The theme font + */ +void pidgin_theme_font_free(PidginThemeFont *font); + +/** + * Set the font-face of a PidginThemeFont. + * + * @param font The PidginThemeFont + * @param face The font-face */ -void free_font_and_color(FontColorPair *pair); +void pidgin_theme_font_set_font_face(PidginThemeFont *font, const gchar *face); + +/** + * Set the color of a PidginThemeFont. + * + * @param font The PidginThemeFont + * @param color The color + */ +void pidgin_theme_font_set_color(PidginThemeFont *font, const GdkColor *color); + +/** + * Get the font-face of a PidginThemeFont. + * + * @param font The PidginThemeFont + * + * @return The font-face, or NULL if none is set. + */ +const gchar * pidgin_theme_font_get_font_face(PidginThemeFont *font); + +/** + * Get the color of a PidginThemeFont as a GdkColor object. + * + * @param font The PidginThemeFont + * + * @return The color, or NULL if none is set. + */ +const GdkColor * pidgin_theme_font_get_color(PidginThemeFont *font); + +/** + * Get the color of a PidginThemeFont. + * + * @param font The PidginThemeFont + * + * @return The color, or NULL if none is set. + */ +const gchar * pidgin_theme_font_get_color_describe(PidginThemeFont *font); /**************************************************************************/ /** @name Purple Buddy List Theme API */ @@ -102,6 +160,8 @@ /** * Returns the background color of the buddy list. * + * @param theme The PidginBlist theme. + * * @returns A gdk color. */ GdkColor *pidgin_blist_theme_get_background_color(PidginBlistTheme *theme); @@ -110,6 +170,8 @@ * Returns the opacity of the buddy list window * (0.0 or clear to 1.0 fully opaque). * + * @param theme The PidginBlist theme. + * * @returns The opacity */ gdouble pidgin_blist_theme_get_opacity(PidginBlistTheme *theme); @@ -117,6 +179,8 @@ /** * Returns the layout to be used with the buddy list. * + * @param theme The PidginBlist theme. + * * @returns The buddy list layout. */ PidginBlistLayout *pidgin_blist_theme_get_layout(PidginBlistTheme *theme); @@ -124,6 +188,8 @@ /** * Returns the background color to be used with expanded groups. * + * @param theme The PidginBlist theme. + * * @returns A gdk color. */ GdkColor *pidgin_blist_theme_get_expanded_background_color(PidginBlistTheme *theme); @@ -131,13 +197,17 @@ /** * Returns the text font and color to be used with expanded groups. * + * @param theme The PidginBlist theme. + * * @returns A font and color pair. */ - FontColorPair *pidgin_blist_theme_get_expanded_text_info(PidginBlistTheme *theme); + PidginThemeFont *pidgin_blist_theme_get_expanded_text_info(PidginBlistTheme *theme); /** * Returns the background color to be used with collapsed groups. * + * @param theme The PidginBlist theme. + * * @returns A gdk color. */ GdkColor *pidgin_blist_theme_get_collapsed_background_color(PidginBlistTheme *theme); @@ -145,13 +215,17 @@ /** * Returns the text font and color to be used with collapsed groups. * + * @param theme The PidginBlist theme. + * * @returns A font and color pair. */ - FontColorPair *pidgin_blist_theme_get_collapsed_text_info(PidginBlistTheme *theme); + PidginThemeFont *pidgin_blist_theme_get_collapsed_text_info(PidginBlistTheme *theme); /** * Returns the colors to be used for contacts and chats. * + * @param theme The PidginBlist theme. + * * @returns A gdkcolor for contacts and chats. */ GdkColor *pidgin_blist_theme_get_contact_color(PidginBlistTheme *theme); @@ -159,65 +233,82 @@ /** * Returns the text font and color to be used for expanded contacts. * + * @param theme The PidginBlist theme. + * * @returns A font and color pair. */ - FontColorPair *pidgin_blist_theme_get_contact_text_info(PidginBlistTheme *theme); + PidginThemeFont *pidgin_blist_theme_get_contact_text_info(PidginBlistTheme *theme); /** * Returns the text font and color to be used for online buddies. * + * @param theme The PidginBlist theme. + * * @returns A font and color pair. */ - FontColorPair *pidgin_blist_theme_get_online_text_info(PidginBlistTheme *theme); + PidginThemeFont *pidgin_blist_theme_get_online_text_info(PidginBlistTheme *theme); /** * Returns the text font and color to be used for away and idle buddies. * + * @param theme The PidginBlist theme. + * * @returns A font and color pair. */ - FontColorPair *pidgin_blist_theme_get_away_text_info(PidginBlistTheme *theme); + PidginThemeFont *pidgin_blist_theme_get_away_text_info(PidginBlistTheme *theme); /** * Returns the text font and color to be used for offline buddies. * + * @param theme The PidginBlist theme. + * * @returns A font and color pair. */ - FontColorPair *pidgin_blist_theme_get_offline_text_info(PidginBlistTheme *theme); + PidginThemeFont *pidgin_blist_theme_get_offline_text_info(PidginBlistTheme *theme); /** * Returns the text font and color to be used for idle buddies. * + * @param theme The PidginBlist theme. + * * @returns A font and color pair. */ - FontColorPair *pidgin_blist_theme_get_idle_text_info(PidginBlistTheme *theme); + PidginThemeFont *pidgin_blist_theme_get_idle_text_info(PidginBlistTheme *theme); /** * Returns the text font and color to be used for buddies with unread messages. * + * @param theme The PidginBlist theme. + * * @returns A font and color pair. */ - FontColorPair *pidgin_blist_theme_get_unread_message_text_info(PidginBlistTheme *theme); + PidginThemeFont *pidgin_blist_theme_get_unread_message_text_info(PidginBlistTheme *theme); /** * Returns the text font and color to be used for chats with unread messages * that mention your nick. * + * @param theme The PidginBlist theme. + * * @returns A font and color pair. */ - FontColorPair *pidgin_blist_theme_get_unread_message_nick_said_text_info(PidginBlistTheme *theme); + PidginThemeFont *pidgin_blist_theme_get_unread_message_nick_said_text_info(PidginBlistTheme *theme); /** * Returns the text font and color to be used for a buddy's status message. * + * @param theme The PidginBlist theme. + * * @returns A font and color pair. */ - FontColorPair *pidgin_blist_theme_get_status_text_info(PidginBlistTheme *theme); + PidginThemeFont *pidgin_blist_theme_get_status_text_info(PidginBlistTheme *theme); /* Set Methods */ /** * Sets the background color to be used for this buddy list theme. * + * @param theme The PidginBlist theme. * @param color The new background color. */ void pidgin_blist_theme_set_background_color(PidginBlistTheme *theme, const GdkColor *color); @@ -225,6 +316,7 @@ /** * Sets the opacity to be used for this buddy list theme. * + * @param theme The PidginBlist theme. * @param opacity The new opacity setting. */ void pidgin_blist_theme_set_opacity(PidginBlistTheme *theme, gdouble opacity); @@ -232,6 +324,7 @@ /** * Sets the buddy list layout to be used for this buddy list theme. * + * @param theme The PidginBlist theme. * @param layout The new layout. */ void pidgin_blist_theme_set_layout(PidginBlistTheme *theme, const PidginBlistLayout *layout); @@ -239,6 +332,7 @@ /** * Sets the background color to be used for expanded groups. * + * @param theme The PidginBlist theme. * @param color The new background color. */ void pidgin_blist_theme_set_expanded_background_color(PidginBlistTheme *theme, const GdkColor *color); @@ -246,13 +340,15 @@ /** * Sets the text color and font to be used for expanded groups. * - * @param pair The new text font at color pair. + * @param theme The PidginBlist theme. + * @param pair The new text font and color pair. */ -void pidgin_blist_theme_set_expanded_text_info(PidginBlistTheme *theme, const FontColorPair *pair); +void pidgin_blist_theme_set_expanded_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair); /** * Sets the background color to be used for collapsed groups. * + * @param theme The PidginBlist theme. * @param color The new background color. */ void pidgin_blist_theme_set_collapsed_background_color(PidginBlistTheme *theme, const GdkColor *color); @@ -260,13 +356,15 @@ /** * Sets the text color and font to be used for expanded groups. * - * @param pair The new text font at color pair. + * @param theme The PidginBlist theme. + * @param pair The new text font and color pair. */ -void pidgin_blist_theme_set_collapsed_text_info(PidginBlistTheme *theme, const FontColorPair *pair); +void pidgin_blist_theme_set_collapsed_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair); /** * Sets the background color to be used for contacts and chats. * + * @param theme The PidginBlist theme. * @param color The color to use for contacts and chats. */ void pidgin_blist_theme_set_contact_color(PidginBlistTheme *theme, const GdkColor *color); @@ -274,59 +372,67 @@ /** * Sets the text color and font to be used for expanded contacts. * - * @param pair The new text font at color pair. + * @param theme The PidginBlist theme. + * @param pair The new text font and color pair. */ -void pidgin_blist_theme_set_contact_text_info(PidginBlistTheme *theme, const FontColorPair *pair); +void pidgin_blist_theme_set_contact_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair); /** * Sets the text color and font to be used for online buddies. * - * @param pair The new text font at color pair. + * @param theme The PidginBlist theme. + * @param pair The new text font and color pair. */ -void pidgin_blist_theme_set_online_text_info(PidginBlistTheme *theme, const FontColorPair *pair); +void pidgin_blist_theme_set_online_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair); /** * Sets the text color and font to be used for away and idle buddies. * - * @param pair The new text font at color pair. + * @param theme The PidginBlist theme. + * @param pair The new text font and color pair. */ -void pidgin_blist_theme_set_away_text_info(PidginBlistTheme *theme, const FontColorPair *pair); +void pidgin_blist_theme_set_away_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair); /** * Sets the text color and font to be used for offline buddies. * - * @param pair The new text font at color pair. + * @param theme The PidginBlist theme. + * @param pair The new text font and color pair. */ -void pidgin_blist_theme_set_offline_text_info(PidginBlistTheme *theme, const FontColorPair *pair); +void pidgin_blist_theme_set_offline_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair); /** * Sets the text color and font to be used for idle buddies. * - * @param pair The new text font at color pair. + * @param theme The PidginBlist theme. + * @param pair The new text font and color pair. */ -void pidgin_blist_theme_set_idle_text_info(PidginBlistTheme *theme, const FontColorPair *pair); +void pidgin_blist_theme_set_idle_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair); /** * Sets the text color and font to be used for buddies with unread messages. * - * @param pair The new text font at color pair. + * @param theme The PidginBlist theme. + * @param pair The new text font and color pair. */ -void pidgin_blist_theme_set_unread_message_text_info(PidginBlistTheme *theme, const FontColorPair *pair); +void pidgin_blist_theme_set_unread_message_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair); /** * Sets the text color and font to be used for a chat with unread messages * that mention your nick. * - * @param pair The new text font at color pair. + * @param theme The PidginBlist theme. + * @param pair The new text font and color pair. */ -void pidgin_blist_theme_set_unread_message_nick_said_text_info(PidginBlistTheme *theme, const FontColorPair *pair); +void pidgin_blist_theme_set_unread_message_nick_said_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair); /** * Sets the text color and font to be used for buddy status messages. * - * @param pair The new text font at color pair. + * @param theme The PidginBlist theme. + * @param pair The new text font and color pair. */ -void pidgin_blist_theme_set_status_text_info(PidginBlistTheme *theme, const FontColorPair *pair); +void pidgin_blist_theme_set_status_text_info(PidginBlistTheme *theme, const PidginThemeFont *pair); G_END_DECLS #endif /* PIDGIN_BLIST_THEME_H */
--- a/pidgin/gtkblist.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkblist.c Wed Apr 29 23:20:51 2009 +0000 @@ -3904,6 +3904,24 @@ return ret; } +static const char * +theme_font_get_color_default(PidginThemeFont *font, const char *def) +{ + const char *ret; + if (!font || !(ret = pidgin_theme_font_get_color_describe(font))) + ret = def; + return ret; +} + +static const char * +theme_font_get_face_default(PidginThemeFont *font, const char *def) +{ + const char *ret; + if (!font || !(ret = pidgin_theme_font_get_font_face(font))) + ret = def; + return ret; +} + gchar * pidgin_blist_get_name_markup(PurpleBuddy *b, gboolean selected, gboolean aliased) { @@ -3918,7 +3936,7 @@ PurpleConversation *conv = find_conversation_with_buddy(b); gboolean hidden_conv = FALSE; gboolean biglist = purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/blist/show_buddy_icons"); - FontColorPair *pair = NULL; + PidginThemeFont *statusfont = NULL, *namefont = NULL; PidginBlistTheme *theme; if (conv != NULL) { @@ -4036,46 +4054,29 @@ /* choose the colors of the text */ theme = pidgin_blist_get_theme(); - - if (purple_presence_is_idle(presence)) { - if (theme) - pair = pidgin_blist_theme_get_idle_text_info(theme); - status_color = name_color = (pair != NULL && pair->color != NULL) ? pair->color : "dim grey"; - status_font = name_font = (pair != NULL && pair->font != NULL) ? pair->font : ""; - - } else if (!purple_presence_is_online(presence)) { - if (theme) - pair = pidgin_blist_theme_get_offline_text_info(theme); - name_color = (pair != NULL && pair->color != NULL) ? pair->color : NULL; - name_font = (pair != NULL && pair->font != NULL) ? pair->font : ""; - - if (theme) - pair = pidgin_blist_theme_get_status_text_info(theme); - status_color = (pair != NULL && pair->color != NULL) ? pair->color : "dim grey"; - status_font = (pair != NULL && pair->font != NULL) ? pair->font : ""; - - } else if (purple_presence_is_available(presence)) { - if (theme) - pair = pidgin_blist_theme_get_online_text_info(theme); - name_color = (pair != NULL && pair->color != NULL) ? pair->color : NULL; - name_font = (pair != NULL && pair->font != NULL) ? pair->font : ""; - - if (theme) - pair = pidgin_blist_theme_get_status_text_info(theme); - status_color = (pair != NULL && pair->color != NULL) ? pair->color : "dim grey"; - status_font = (pair != NULL && pair->font != NULL) ? pair->font : ""; - - } else { - if (theme) - pair = pidgin_blist_theme_get_away_text_info(theme); - name_color = (pair != NULL && pair->color != NULL) ? pair->color : NULL; - name_font = (pair != NULL && pair->font != NULL) ? pair->font : ""; - - if (theme) - pair = pidgin_blist_theme_get_status_text_info(theme); - status_color = (pair != NULL && pair->color != NULL) ? pair->color : "dim grey"; - status_font = (pair != NULL && pair->font != NULL) ? pair->font : ""; - } + name_color = NULL; + + if (theme) { + if (purple_presence_is_idle(presence)) { + namefont = statusfont = pidgin_blist_theme_get_idle_text_info(theme); + name_color = "dim grey"; + } else if (!purple_presence_is_online(presence)) { + namefont = pidgin_blist_theme_get_offline_text_info(theme); + statusfont = pidgin_blist_theme_get_status_text_info(theme); + } else if (purple_presence_is_available(presence)) { + namefont = pidgin_blist_theme_get_online_text_info(theme); + statusfont = pidgin_blist_theme_get_status_text_info(theme); + } else { + namefont = pidgin_blist_theme_get_away_text_info(theme); + statusfont = pidgin_blist_theme_get_status_text_info(theme); + } + } + + name_color = theme_font_get_color_default(namefont, name_color); + name_font = theme_font_get_face_default(namefont, ""); + + status_color = theme_font_get_color_default(statusfont, "dim grey"); + status_font = theme_font_get_face_default(statusfont, ""); if (aliased && selected) { if (theme) { @@ -4656,6 +4657,12 @@ } static void +account_actions_changed(PurpleAccount *account, gpointer data) +{ + pidgin_blist_update_accounts_menu(); +} + +static void account_status_changed(PurpleAccount *account, PurpleStatus *old, PurpleStatus *new, PidginBuddyList *gtkblist) { @@ -5841,6 +5848,8 @@ purple_signal_connect(handle, "account-error-changed", gtkblist, PURPLE_CALLBACK(update_account_error_state), gtkblist); + purple_signal_connect(handle, "account-actions-changed", gtkblist, + PURPLE_CALLBACK(account_actions_changed), NULL); handle = pidgin_account_get_handle(); purple_signal_connect(handle, "account-modified", gtkblist, @@ -6201,7 +6210,7 @@ char *mark, *esc; PurpleBlistNode *selected_node = NULL; GtkTreeIter iter; - FontColorPair *pair; + PidginThemeFont *pair; gchar const *text_color, *text_font; PidginBlistTheme *theme; @@ -6228,8 +6237,8 @@ pair = pidgin_blist_theme_get_collapsed_text_info(theme); - text_color = (selected || pair == NULL || pair->color == NULL) ? NULL : pair->color; - text_font = (pair == NULL || pair->font == NULL) ? "" : pair->font; + text_color = selected ? NULL : theme_font_get_color_default(pair, NULL); + text_font = theme_font_get_face_default(pair, ""); esc = g_markup_escape_text(group->name, -1); if (text_color) { @@ -6287,7 +6296,7 @@ if (idle_secs > 0) { - FontColorPair *pair = NULL; + PidginThemeFont *pair = NULL; const gchar *textcolor; time_t t; int ihrs, imin; @@ -6296,18 +6305,18 @@ ihrs = (t - idle_secs) / 3600; imin = ((t - idle_secs) / 60) % 60; - if (!selected && theme != NULL && (pair = pidgin_blist_theme_get_idle_text_info(theme)) != NULL && pair->color != NULL) - textcolor = pair->color; + if (!selected && theme != NULL && (pair = pidgin_blist_theme_get_idle_text_info(theme)) != NULL) + textcolor = pidgin_theme_font_get_color_describe(pair); else textcolor = NULL; if (textcolor) { idle = g_strdup_printf("<span color='%s' font_desc='%s'>%d:%02d</span>", - textcolor, (pair == NULL || pair->font == NULL) ? "" : pair->font, + textcolor, theme_font_get_face_default(pair, ""), ihrs, imin); } else { idle = g_strdup_printf("<span font_desc='%s'>%d:%02d</span>", - (pair == NULL || pair->font == NULL) ? "" : pair->font, + theme_font_get_face_default(pair, ""), ihrs, imin); } } @@ -6392,7 +6401,7 @@ const gchar *fg_color, *font; GdkColor *color = NULL; PidginBlistTheme *theme = pidgin_blist_get_theme(); - FontColorPair *pair; + PidginThemeFont *pair; gboolean selected = (gtkblist->selected_node == cnode); mark = g_markup_escape_text(purple_contact_get_alias(contact), -1); @@ -6405,8 +6414,8 @@ color = pidgin_blist_theme_get_contact_color(theme); } - font = (pair == NULL || pair->font == NULL) ? "" : pair->font; - fg_color = (selected || pair == NULL || pair->color == NULL) ? NULL : pair->color; + font = theme_font_get_face_default(pair, ""); + fg_color = selected ? NULL : theme_font_get_color_default(pair, NULL); if (fg_color) { tmp = g_strdup_printf("<span font_desc='%s' color='%s'>%s</span>", @@ -6503,7 +6512,7 @@ PurpleConversation *conv; gboolean hidden = FALSE; GdkColor *bgcolor = NULL; - FontColorPair *pair; + PidginThemeFont *pair; PidginBlistTheme *theme; gboolean selected = (gtkblist->selected_node == node); gboolean nick_said = FALSE; @@ -6541,12 +6550,10 @@ else pair = pidgin_blist_theme_get_online_text_info(theme); - font = (pair == NULL || pair->font == NULL) ? "" : pair->font; - if (selected || pair == NULL || pair->color == NULL) + font = theme_font_get_face_default(pair, ""); + if (selected || !(color = theme_font_get_color_default(pair, NULL))) /* nick_said color is the same as gtkconv:tab-label-attention */ color = (nick_said ? "#006aff" : NULL); - else - color = pair->color; if (color) { tmp = g_strdup_printf("<span font_desc='%s' color='%s' weight='%s'>%s</span>", @@ -6635,7 +6642,7 @@ purple_signals_disconnect_by_handle(gtkblist); if (gtkblist->headline_close) - gdk_pixbuf_unref(gtkblist->headline_close); + g_object_unref(G_OBJECT(gtkblist->headline_close)); gtk_widget_destroy(gtkblist->window);
--- a/pidgin/gtkconv.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkconv.c Wed Apr 29 23:20:51 2009 +0000 @@ -2512,13 +2512,49 @@ return get_prpl_icon_list(account); } +static const char * +pidgin_conv_get_icon_stock(PurpleConversation *conv) +{ + PurpleAccount *account = NULL; + const char *stock = NULL; + + g_return_val_if_fail(conv != NULL, NULL); + + account = purple_conversation_get_account(conv); + g_return_val_if_fail(account != NULL, NULL); + + /* Use the buddy icon, if possible */ + if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) { + const char *name = NULL; + PurpleBuddy *b; + name = purple_conversation_get_name(conv); + b = purple_find_buddy(account, name); + if (b != NULL) { + PurplePresence *p = purple_buddy_get_presence(b); + PurpleStatus *active = purple_presence_get_active_status(p); + PurpleStatusType *type = purple_status_get_type(active); + PurpleStatusPrimitive prim = purple_status_type_get_primitive(type); + stock = pidgin_stock_id_from_status_primitive(prim); + } else { + stock = PIDGIN_STOCK_STATUS_PERSON; + } + } else { + stock = PIDGIN_STOCK_STATUS_CHAT; + } + + return stock; +} + static GdkPixbuf * pidgin_conv_get_icon(PurpleConversation *conv, GtkWidget *parent, const char *icon_size) { PurpleAccount *account = NULL; const char *name = NULL; + const char *stock = NULL; GdkPixbuf *status = NULL; PurpleBlistUiOps *ops = purple_blist_get_ui_ops(); + GtkIconSize size; + g_return_val_if_fail(conv != NULL, NULL); account = purple_conversation_get_account(conv); @@ -2531,40 +2567,17 @@ if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) { PurpleBuddy *b = purple_find_buddy(account, name); if (b != NULL) { - PurplePresence *p = purple_buddy_get_presence(b); /* I hate this hack. It fixes a bug where the pending message icon * displays in the conv tab even though it shouldn't. * A better solution would be great. */ if (ops && ops->update) ops->update(NULL, (PurpleBlistNode*)b); - - /* XXX Seanegan: We really need a util function to return a pixbuf for a Presence to avoid all this switching */ - if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_AWAY)) - status = pidgin_create_status_icon(PURPLE_STATUS_AWAY, parent, icon_size); - else if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_EXTENDED_AWAY)) - status = pidgin_create_status_icon(PURPLE_STATUS_EXTENDED_AWAY, parent, icon_size); - else if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_OFFLINE)) - status = pidgin_create_status_icon(PURPLE_STATUS_OFFLINE, parent, icon_size); - else if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_AVAILABLE)) - status = pidgin_create_status_icon(PURPLE_STATUS_AVAILABLE, parent, icon_size); - else if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_INVISIBLE)) - status = pidgin_create_status_icon(PURPLE_STATUS_INVISIBLE, parent, icon_size); - else if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_UNAVAILABLE)) - status = pidgin_create_status_icon(PURPLE_STATUS_UNAVAILABLE, parent, icon_size); - } - } - - /* If they don't have a buddy icon, then use the PRPL icon */ - if (status == NULL) { - GtkIconSize size = gtk_icon_size_from_name(icon_size); - if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) { - status = gtk_widget_render_icon (parent, PIDGIN_STOCK_STATUS_PERSON, - size, "GtkWidget"); - } else { - status = gtk_widget_render_icon (parent, PIDGIN_STOCK_STATUS_CHAT, - size, "GtkWidget"); - } - } + } + } + + stock = pidgin_conv_get_icon_stock(conv); + size = gtk_icon_size_from_name(icon_size); + status = gtk_widget_render_icon (parent, stock, size, "GtkWidget"); return status; } @@ -2582,9 +2595,9 @@ PidginConversation *gtkconv; PidginWindow *win; GList *l; - GdkPixbuf *status = NULL; - GdkPixbuf *infopane_status = NULL; GdkPixbuf *emblem = NULL; + const char *status = NULL; + const char *infopane_status = NULL; g_return_if_fail(conv != NULL); @@ -2593,8 +2606,7 @@ if (conv != gtkconv->active_conv) return; - status = pidgin_conv_get_tab_icon(conv, TRUE); - infopane_status = pidgin_conv_get_tab_icon(conv, FALSE); + status = infopane_status = pidgin_conv_get_icon_stock(conv); if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) { PurpleBuddy *b = purple_find_buddy(conv->account, conv->name); @@ -2604,8 +2616,8 @@ g_return_if_fail(status != NULL); - gtk_image_set_from_pixbuf(GTK_IMAGE(gtkconv->icon), status); - gtk_image_set_from_pixbuf(GTK_IMAGE(gtkconv->menu_icon), status); + g_object_set(G_OBJECT(gtkconv->icon), "stock", status, NULL); + g_object_set(G_OBJECT(gtkconv->menu_icon), "stock", status, NULL); gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model), &(gtkconv->infopane_iter), @@ -2633,9 +2645,6 @@ gtk_widget_queue_resize(gtkconv->infopane); gtk_widget_queue_draw(gtkconv->infopane); - if (status != NULL) - g_object_unref(status); - if (pidgin_conv_window_is_active_conversation(conv) && (purple_conversation_get_type(conv) != PURPLE_CONV_TYPE_IM || gtkconv->u.im->anim == NULL)) @@ -3071,16 +3080,13 @@ PurpleConversation *conv = (PurpleConversation*)l->data; PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv); - GtkWidget *icon = gtk_image_new(); - GdkPixbuf *pbuf = pidgin_conv_get_icon(conv, icon, PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC); + GtkWidget *icon = gtk_image_new_from_stock(pidgin_conv_get_icon_stock(conv), + gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC)); GtkWidget *item; gchar *text = g_strdup_printf("%s (%d)", gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)), gtkconv->unseen_count); - gtk_image_set_from_pixbuf(GTK_IMAGE(icon), pbuf); - g_object_unref(pbuf); - item = gtk_image_menu_item_new_with_label(text); gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), icon); g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(unseen_conv_menu_cb), conv); @@ -3941,12 +3947,9 @@ update_send_to_selection(win); } -static GdkPixbuf * +static const char * get_chat_buddy_status_icon(PurpleConvChat *chat, const char *name, PurpleConvChatBuddyFlags flags) { - PidginConversation *gtkconv = PIDGIN_CONVERSATION(chat->conv); - GdkPixbuf *pixbuf, *scale, *scale2; - char *filename; const char *image = NULL; if (flags & PURPLE_CBFLAGS_FOUNDER) { @@ -3962,28 +3965,7 @@ } else { return NULL; } - - pixbuf = gtk_widget_render_icon (gtkconv->tab_cont, image, gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL), - "GtkTreeView"); - - if (!pixbuf) - return NULL; - - scale = gdk_pixbuf_scale_simple(pixbuf, 16, 16, GDK_INTERP_BILINEAR); - g_object_unref(pixbuf); - - if (flags && purple_conv_chat_is_user_ignored(chat, name)) { -/* TODO: the .../status/default directory isn't installed, should it be? */ - filename = g_build_filename(DATADIR, "pixmaps", "pidgin", "status", "default", "ignored.png", NULL); - pixbuf = gdk_pixbuf_new_from_file(filename, NULL); - g_free(filename); - scale2 = gdk_pixbuf_scale_simple(pixbuf, 16, 16, GDK_INTERP_BILINEAR); - g_object_unref(pixbuf); - gdk_pixbuf_composite(scale2, scale, 0, 0, 16, 16, 0, 0, 1, 1, GDK_INTERP_BILINEAR, 192); - g_object_unref(scale2); - } - - return scale; + return image; } static void @@ -3995,7 +3977,7 @@ PurpleConnection *gc; PurplePluginProtocolInfo *prpl_info; GtkListStore *ls; - GdkPixbuf *pixbuf; + const char *stock; GtkTreeIter iter; gboolean is_me = FALSE; gboolean is_buddy; @@ -4017,7 +3999,7 @@ ls = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list))); - pixbuf = get_chat_buddy_status_icon(chat, name, flags); + stock = get_chat_buddy_status_icon(chat, name, flags); if (!strcmp(chat->nick, purple_normalize(conv->account, old_name != NULL ? old_name : name))) is_me = TRUE; @@ -4034,6 +4016,11 @@ "send-name"); g_object_get(tag, "foreground-gdk", &color, NULL); } else { + GtkTextTag *tag; + if ((tag = get_buddy_tag(conv, name, 0, FALSE))) + g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL); + if ((tag = get_buddy_tag(conv, name, PURPLE_MESSAGE_NICK, FALSE))) + g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL); color = (GdkColor*)get_nick_color(gtkconv, name); } @@ -4047,7 +4034,7 @@ * Inserting in the "wrong" location has no visible ill effects. - F.P. */ -1, /* "row" */ - CHAT_USERS_ICON_COLUMN, pixbuf, + CHAT_USERS_ICON_STOCK_COLUMN, stock, CHAT_USERS_ALIAS_COLUMN, alias, CHAT_USERS_ALIAS_KEY_COLUMN, alias_key, CHAT_USERS_NAME_COLUMN, name, @@ -4058,7 +4045,7 @@ #else gtk_list_store_append(ls, &iter); gtk_list_store_set(ls, &iter, - CHAT_USERS_ICON_COLUMN, pixbuf, + CHAT_USERS_ICON_STOCK_COLUMN, stock, CHAT_USERS_ALIAS_COLUMN, alias, CHAT_USERS_ALIAS_KEY_COLUMN, alias_key, CHAT_USERS_NAME_COLUMN, name, @@ -4068,8 +4055,6 @@ -1); #endif - if (pixbuf) - g_object_unref(pixbuf); if (is_me && color) gdk_color_free(color); g_free(alias_key); @@ -4716,16 +4701,18 @@ ls = gtk_list_store_new(CHAT_USERS_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT, - GDK_TYPE_COLOR, G_TYPE_INT); + GDK_TYPE_COLOR, G_TYPE_INT, G_TYPE_STRING); gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(ls), CHAT_USERS_ALIAS_KEY_COLUMN, sort_chat_users, NULL, NULL); list = gtk_tree_view_new_with_model(GTK_TREE_MODEL(ls)); rend = gtk_cell_renderer_pixbuf_new(); - + g_object_set(G_OBJECT(rend), + "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL), + NULL); col = gtk_tree_view_column_new_with_attributes(NULL, rend, - "pixbuf", CHAT_USERS_ICON_COLUMN, NULL); + "stock-id", CHAT_USERS_ICON_STOCK_COLUMN, NULL); gtk_tree_view_column_set_sizing(col, GTK_TREE_VIEW_COLUMN_AUTOSIZE); gtk_tree_view_append_column(GTK_TREE_VIEW(list), col); ul_width = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width"); @@ -4752,7 +4739,7 @@ "foreground-set", TRUE, "weight-set", TRUE, NULL); - g_object_set(G_OBJECT(rend), "editable", TRUE, NULL); + g_object_set(G_OBJECT(rend), "editable", TRUE, NULL); col = gtk_tree_view_column_new_with_attributes(NULL, rend, "text", CHAT_USERS_ALIAS_COLUMN, @@ -4843,7 +4830,7 @@ pidgin_conv_create_tooltip, NULL); gtkconv->infopane = gtk_cell_view_new(); - gtkconv->infopane_model = gtk_list_store_new(CONV_NUM_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING, GDK_TYPE_PIXBUF, GDK_TYPE_PIXBUF); + gtkconv->infopane_model = gtk_list_store_new(CONV_NUM_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, GDK_TYPE_PIXBUF, GDK_TYPE_PIXBUF); gtk_cell_view_set_model(GTK_CELL_VIEW(gtkconv->infopane), GTK_TREE_MODEL(gtkconv->infopane_model)); g_object_unref(gtkconv->infopane_model); @@ -4866,8 +4853,10 @@ rend = gtk_cell_renderer_pixbuf_new(); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE); - gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "pixbuf", CONV_ICON_COLUMN, NULL); - g_object_set(rend, "xalign", 0.0, "xpad", 6, "ypad", 0, NULL); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "stock-id", CONV_ICON_COLUMN, NULL); + g_object_set(rend, "xalign", 0.0, "xpad", 6, "ypad", 0, + "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL), + NULL); rend = gtk_cell_renderer_text_new(); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, TRUE); @@ -6063,6 +6052,7 @@ PurpleConvChatBuddy *cbuddy; GtkTreeIter iter; GtkTreeModel *model; + GtkTextTag *tag; int f = 1; chat = PURPLE_CONV_CHAT(conv); @@ -6090,6 +6080,11 @@ g_free(val); } + if ((tag = get_buddy_tag(conv, old_name, 0, FALSE))) + g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL); + if ((tag = get_buddy_tag(conv, old_name, PURPLE_MESSAGE_NICK, FALSE))) + g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL); + if (!purple_conv_chat_find_user(chat, old_name)) return; @@ -6112,6 +6107,7 @@ char tmp[BUF_LONG]; int num_users; gboolean f; + GtkTextTag *tag; chat = PURPLE_CONV_CHAT(conv); gtkconv = PIDGIN_CONVERSATION(conv); @@ -6144,6 +6140,11 @@ g_free(val); } while (f); + + if ((tag = get_buddy_tag(conv, l->data, 0, FALSE))) + g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL); + if ((tag = get_buddy_tag(conv, l->data, PURPLE_MESSAGE_NICK, FALSE))) + g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL); } g_snprintf(tmp, sizeof(tmp), @@ -9417,6 +9418,12 @@ /* Status icon. */ gtkconv->icon = gtk_image_new(); gtkconv->menu_icon = gtk_image_new(); + g_object_set(G_OBJECT(gtkconv->icon), + "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC), + NULL); + g_object_set(G_OBJECT(gtkconv->menu_icon), + "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC), + NULL); gtk_widget_show(gtkconv->icon); update_tab_icon(conv);
--- a/pidgin/gtkconv.h Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkconv.h Wed Apr 29 23:20:51 2009 +0000 @@ -51,6 +51,7 @@ CHAT_USERS_FLAGS_COLUMN, CHAT_USERS_COLOR_COLUMN, CHAT_USERS_WEIGHT_COLUMN, + CHAT_USERS_ICON_STOCK_COLUMN, /** @since 2.6.0 */ CHAT_USERS_COLUMNS };
--- a/pidgin/gtkdialogs.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkdialogs.c Wed Apr 29 23:20:51 2009 +0000 @@ -73,6 +73,7 @@ /* Order: Alphabetical by Last Name */ static const struct developer developers[] = { {"Daniel 'datallah' Atallah", NULL, NULL}, + {"Paul 'darkrain42' Aurich", NULL, NULL }, {"John 'rekkanoryo' Bailey", N_("bug master"), "rekkanoryo@pidgin.im"}, {"Ethan 'Paco-Paco' Blanton", NULL, NULL}, {"Hylke Bons", N_("artist"), "h.bons@student.rug.nl"}, @@ -90,6 +91,7 @@ {"Bartosz Oler", NULL, NULL}, {"Etan 'deryni' Reisner", NULL, NULL}, {"Tim 'marv' Ringenbach", NULL, NULL}, + {"Michael 'Maiku' Ruprecht", N_("voice and video"), NULL}, {"Elliott 'QuLogic' Sales de Andrade", NULL, NULL}, {"Luke 'LSchiere' Schierer", N_("support"), "lschiere@users.sf.net"}, {"Evan Schoenberg", NULL, NULL}, @@ -101,7 +103,6 @@ /* Order: Alphabetical by Last Name */ static const struct developer patch_writers[] = { - {"Paul 'darkrain42' Aurich", NULL, NULL }, {"Marcus 'malu' Lundblad", NULL, NULL}, {"Dennis 'EvilDennisR' Ristuccia", N_("Senior Contributor/QA"), NULL}, {"Peter 'Fmoo' Ruibal", NULL, NULL}, @@ -427,7 +428,7 @@ #endif gtk_widget_destroy(logo); logo = gtk_image_new_from_pixbuf(pixbuf); - gdk_pixbuf_unref(pixbuf); + g_object_unref(G_OBJECT(pixbuf)); /* Insert the logo */ obj = gtk_widget_get_accessible(logo); tmp = g_strconcat(PIDGIN_NAME, " " DISPLAY_VERSION, NULL);
--- a/pidgin/gtkdocklet.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkdocklet.c Wed Apr 29 23:20:51 2009 +0000 @@ -482,7 +482,7 @@ } static GtkWidget * -new_menu_item_with_status_icon(GtkWidget *menu, const char *str, PurpleStatusPrimitive primitive, GtkSignalFunc sf, gpointer data, guint accel_key, guint accel_mods, char *mod) +new_menu_item_with_status_icon(GtkWidget *menu, const char *str, PurpleStatusPrimitive primitive, GCallback cb, gpointer data, guint accel_key, guint accel_mods, char *mod) { GtkWidget *menuitem; GdkPixbuf *pixbuf; @@ -493,8 +493,8 @@ if (menu) gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); - if (sf) - g_signal_connect(G_OBJECT(menuitem), "activate", sf, data); + if (cb) + g_signal_connect(G_OBJECT(menuitem), "activate", cb, data); pixbuf = pidgin_create_status_icon(primitive, menu, PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL); image = gtk_image_new_from_pixbuf(pixbuf);
--- a/pidgin/gtkicon-theme-loader.h Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkicon-theme-loader.h Wed Apr 29 23:20:51 2009 +0000 @@ -1,5 +1,5 @@ /** - * @file gtkicon-loader.h Pidgin Icon Theme Loader Class API + * @file gtkicon-theme-loader.h Pidgin Icon Theme Loader Class API */ /* purple
--- a/pidgin/gtkicon-theme.h Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkicon-theme.h Wed Apr 29 23:20:51 2009 +0000 @@ -1,5 +1,5 @@ /** - * @file icon-theme.h Pidgin Icon Theme Class API + * @file gtkicon-theme.h Pidgin Icon Theme Class API */ /* pidgin @@ -72,6 +72,7 @@ /** * Returns a copy of the filename for the icon event or NULL if it is not set * + * @param theme the theme * @param event the pidgin icon event to look up * * @returns the filename of the icon event @@ -82,6 +83,7 @@ /** * Sets the filename for a given icon id, setting the icon to NULL will remove the icon from the theme * + * @param theme the theme * @param icon_id a string representing what the icon is to be used for * @param filename the name of the file to be used for the given id */
--- a/pidgin/gtkimhtml.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkimhtml.c Wed Apr 29 23:20:51 2009 +0000 @@ -782,7 +782,7 @@ gc, TRUE, visible_rect.x, visible_rect.y, visible_rect.width, visible_rect.height); - gdk_gc_unref(gc); + g_object_unref(G_OBJECT(gc)); if (GTK_WIDGET_CLASS (parent_class)->expose_event) return (* GTK_WIDGET_CLASS (parent_class)->expose_event) @@ -873,7 +873,7 @@ !gtk_text_iter_begins_tag(&cur, NULL)); } - gdk_gc_unref(gc); + g_object_unref(G_OBJECT(gc)); if (GTK_WIDGET_CLASS (parent_class)->expose_event) return (* GTK_WIDGET_CLASS (parent_class)->expose_event) @@ -1384,7 +1384,7 @@ gtk_widget_destroy(imhtml->tip_window); } if(imhtml->tip_timer) - gtk_timeout_remove(imhtml->tip_timer); + g_source_remove(imhtml->tip_timer); for(scalables = imhtml->scalables; scalables; scalables = scalables->next) { struct scalable_data *sd = scalables->data; @@ -1451,7 +1451,7 @@ GObjectClass *gobject_class; object_class = (GtkObjectClass*) klass; gobject_class = (GObjectClass*) klass; - parent_class = gtk_type_class(GTK_TYPE_TEXT_VIEW); + parent_class = g_type_class_ref(GTK_TYPE_TEXT_VIEW); signals[URL_CLICKED] = g_signal_new("url_clicked", G_TYPE_FROM_CLASS(gobject_class), G_SIGNAL_RUN_FIRST,
--- a/pidgin/gtkimhtml.h Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkimhtml.h Wed Apr 29 23:20:51 2009 +0000 @@ -40,13 +40,13 @@ **************************************************************************/ /*@{*/ -#define GTK_TYPE_IMHTML (gtk_imhtml_get_type ()) -#define GTK_IMHTML(obj) (GTK_CHECK_CAST ((obj), GTK_TYPE_IMHTML, GtkIMHtml)) -#define GTK_IMHTML_CLASS(klass) (GTK_CHECK_CLASS_CAST ((klass), GTK_TYPE_IMHTML, GtkIMHtmlClass)) -#define GTK_IS_IMHTML(obj) (GTK_CHECK_TYPE ((obj), GTK_TYPE_IMHTML)) -#define GTK_IS_IMHTML_CLASS(klass) (GTK_CHECK_CLASS_TYPE ((klass), GTK_TYPE_IMHTML)) +#define GTK_TYPE_IMHTML (gtk_imhtml_get_type()) +#define GTK_IMHTML(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GTK_TYPE_IMHTML, GtkIMHtml)) +#define GTK_IMHTML_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GTK_TYPE_IMHTML, GtkIMHtmlClass)) +#define GTK_IS_IMHTML(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTK_TYPE_IMHTML)) +#define GTK_IS_IMHTML_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GTK_TYPE_IMHTML)) #define GTK_IMHTML_SCALABLE(obj) ((GtkIMHtmlScalable *)obj) -#define GTK_IMHTML_ANIMATION(obj) ((GtkIMHtmlAnimation *)obj) +#define GTK_IMHTML_ANIMATION(obj) ((GtkIMHtmlAnimation *)obj) typedef struct _GtkIMHtml GtkIMHtml; typedef struct _GtkIMHtmlClass GtkIMHtmlClass;
--- a/pidgin/gtkimhtmltoolbar.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkimhtmltoolbar.c Wed Apr 29 23:20:51 2009 +0000 @@ -1198,7 +1198,7 @@ GObjectClass *gobject_class; object_class = (GtkObjectClass*) class; gobject_class = (GObjectClass*) class; - parent_class = gtk_type_class(GTK_TYPE_HBOX); + parent_class = g_type_class_ref(GTK_TYPE_HBOX); gobject_class->finalize = gtk_imhtmltoolbar_finalize; purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations/toolbar");
--- a/pidgin/gtkimhtmltoolbar.h Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkimhtmltoolbar.h Wed Apr 29 23:20:51 2009 +0000 @@ -32,11 +32,11 @@ #define DEFAULT_FONT_FACE "Helvetica 12" -#define GTK_TYPE_IMHTMLTOOLBAR (gtk_imhtmltoolbar_get_type ()) -#define GTK_IMHTMLTOOLBAR(obj) (GTK_CHECK_CAST ((obj), GTK_TYPE_IMHTMLTOOLBAR, GtkIMHtmlToolbar)) -#define GTK_IMHTMLTOOLBAR_CLASS(klass) (GTK_CHECK_CLASS_CAST ((klass), GTK_TYPE_IMHTMLTOOLBAR, GtkIMHtmlToolbarClass)) -#define GTK_IS_IMHTMLTOOLBAR(obj) (GTK_CHECK_TYPE ((obj), GTK_TYPE_IMHTMLTOOLBAR)) -#define GTK_IS_IMHTMLTOOLBAR_CLASS(klass) (GTK_CHECK_CLASS_TYPE ((klass), GTK_TYPE_IMHTMLTOOLBAR)) +#define GTK_TYPE_IMHTMLTOOLBAR (gtk_imhtmltoolbar_get_type()) +#define GTK_IMHTMLTOOLBAR(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GTK_TYPE_IMHTMLTOOLBAR, GtkIMHtmlToolbar)) +#define GTK_IMHTMLTOOLBAR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GTK_TYPE_IMHTMLTOOLBAR, GtkIMHtmlToolbarClass)) +#define GTK_IS_IMHTMLTOOLBAR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTK_TYPE_IMHTMLTOOLBAR)) +#define GTK_IS_IMHTMLTOOLBAR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GTK_TYPE_IMHTMLTOOLBAR)) typedef struct _GtkIMHtmlToolbar GtkIMHtmlToolbar; typedef struct _GtkIMHtmlToolbarClass GtkIMHtmlToolbarClass;
--- a/pidgin/gtkmenutray.h Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkmenutray.h Wed Apr 29 23:20:51 2009 +0000 @@ -26,12 +26,12 @@ #include <gtk/gtk.h> -#define PIDGIN_TYPE_MENU_TRAY (pidgin_menu_tray_get_gtype()) -#define PIDGIN_MENU_TRAY(obj) (GTK_CHECK_CAST((obj), PIDGIN_TYPE_MENU_TRAY, PidginMenuTray)) -#define PIDGIN_MENU_TRAY_CLASS(klass) (GTK_CHECK_CLASS_CAST((klass), PIDGIN_TYPE_MENU_TRAY, PidginMenuTrayClass)) -#define PIDGIN_IS_MENU_TRAY(obj) (GTK_CHECK_TYPE((obj), PIDGIN_TYPE_MENU_TRAY)) -#define PIDGIN_IS_MENU_TRAY_CLASS(klass) (GTK_CHECK_CLASS_TYPE((klass), PIDGIN_TYPE_MENU_TRAY)) -#define PIDGIN_MENU_TRAY_GET_CLASS(obj) (GTK_CHECK_GET_CLASS((obj), PIDGIN_TYPE_MENU_TRAY, PidginMenuTrayClass)) +#define PIDGIN_TYPE_MENU_TRAY (pidgin_menu_tray_get_gtype()) +#define PIDGIN_MENU_TRAY(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), PIDGIN_TYPE_MENU_TRAY, PidginMenuTray)) +#define PIDGIN_MENU_TRAY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), PIDGIN_TYPE_MENU_TRAY, PidginMenuTrayClass)) +#define PIDGIN_IS_MENU_TRAY(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), PIDGIN_TYPE_MENU_TRAY)) +#define PIDGIN_IS_MENU_TRAY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), PIDGIN_TYPE_MENU_TRAY)) +#define PIDGIN_MENU_TRAY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), PIDGIN_TYPE_MENU_TRAY, PidginMenuTrayClass)) typedef struct _PidginMenuTray PidginMenuTray; typedef struct _PidginMenuTrayClass PidginMenuTrayClass;
--- a/pidgin/gtknotify.h Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtknotify.h Wed Apr 29 23:20:51 2009 +0000 @@ -32,8 +32,10 @@ /** * Adds a buddy pounce to the buddy pounce dialog * + * @param account The account + * @param pounce The pounce * @param alias The buddy alias - * @param event Event description + * @param event Event description * @param message Pounce message * @param date Pounce date */
--- a/pidgin/gtkprefs.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkprefs.c Wed Apr 29 23:20:51 2009 +0000 @@ -638,7 +638,7 @@ gtk_list_store_set(prefs_sound_themes, &iter, 0, pixbuf, 2, purple_theme_get_name(theme), -1); if (pixbuf != NULL) - gdk_pixbuf_unref(pixbuf); + g_object_unref(G_OBJECT(pixbuf)); } else if (PIDGIN_IS_BLIST_THEME(theme) || PIDGIN_IS_STATUS_ICON_THEME(theme)){ GtkListStore *store; @@ -665,7 +665,7 @@ g_free(markup); if (pixbuf != NULL) - gdk_pixbuf_unref(pixbuf); + g_object_unref(G_OBJECT(pixbuf)); } } @@ -702,7 +702,7 @@ gtk_list_store_set(prefs_status_icon_themes, &iter, 0, pixbuf, 1, "<b>(Default)</b> - None\n<span color='dim grey'>" "The default Pidgin status icon theme</span>", 2, "", -1); - gdk_pixbuf_unref(pixbuf); + g_object_unref(G_OBJECT(pixbuf)); } /* builds a theme combo box from a list store with colums: icon preview, markup, theme name */
--- a/pidgin/gtksavedstatuses.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtksavedstatuses.c Wed Apr 29 23:20:51 2009 +0000 @@ -398,23 +398,7 @@ static const gchar * get_stock_icon_from_primitive(PurpleStatusPrimitive type) { - switch (type) { - case PURPLE_STATUS_AVAILABLE: - return PIDGIN_STOCK_STATUS_AVAILABLE; - case PURPLE_STATUS_AWAY: - return PIDGIN_STOCK_STATUS_AWAY; - case PURPLE_STATUS_EXTENDED_AWAY: - return PIDGIN_STOCK_STATUS_XA; - case PURPLE_STATUS_INVISIBLE: - return PIDGIN_STOCK_STATUS_INVISIBLE; - case PURPLE_STATUS_OFFLINE: - return PIDGIN_STOCK_STATUS_OFFLINE; - case PURPLE_STATUS_UNAVAILABLE: - return PIDGIN_STOCK_STATUS_BUSY; - default: - /* this shouldn't happen */ - return NULL; - } + return pidgin_stock_id_from_status_primitive(type); } static void @@ -1503,16 +1487,19 @@ gtk_size_group_add_widget(sg, label); dialog->model = gtk_list_store_new(SUBSTATUS_NUM_COLUMNS, - GDK_TYPE_PIXBUF, + G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(dialog->model)); dialog->box = GTK_COMBO_BOX(combo); rend = GTK_CELL_RENDERER(gtk_cell_renderer_pixbuf_new()); + g_object_set(G_OBJECT(rend), + "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL), + NULL); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), rend, FALSE); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), rend, - "pixbuf", SUBSTATUS_COLUMN_ICON, NULL); + "stock-id", SUBSTATUS_COLUMN_ICON, NULL); rend = GTK_CELL_RENDERER(gtk_cell_renderer_text_new()); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), rend, TRUE); @@ -1574,8 +1561,8 @@ for (list = purple_account_get_status_types(account); list; list = list->next) { PurpleStatusType *status_type; - GdkPixbuf *pixbuf; const char *id, *name; + PurpleStatusPrimitive prim; status_type = list->data; @@ -1588,17 +1575,15 @@ continue; id = purple_status_type_get_id(status_type); - pixbuf = pidgin_create_status_icon(purple_status_type_get_primitive(status_type), combo, PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL); + prim = purple_status_type_get_primitive(status_type); name = purple_status_type_get_name(status_type); gtk_list_store_append(dialog->model, &iter); gtk_list_store_set(dialog->model, &iter, - SUBSTATUS_COLUMN_ICON, pixbuf, + SUBSTATUS_COLUMN_ICON, pidgin_stock_id_from_status_primitive(prim), SUBSTATUS_COLUMN_STATUS_ID, id, SUBSTATUS_COLUMN_STATUS_NAME, name, -1); - if (pixbuf != NULL) - g_object_unref(pixbuf); if ((status_id != NULL) && !strcmp(status_id, id)) { gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter); @@ -1705,18 +1690,15 @@ { GtkTreeIter iter; gboolean currently_selected = FALSE; - GdkPixbuf *pixbuf = pidgin_create_status_icon(primitive, w, PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL); gtk_list_store_append(model, &iter); gtk_list_store_set(model, &iter, SS_MENU_TYPE_COLUMN, SS_MENU_ENTRY_TYPE_PRIMITIVE, - SS_MENU_ICON_COLUMN, pixbuf, + SS_MENU_ICON_COLUMN, pidgin_stock_id_from_status_primitive(primitive), SS_MENU_TEXT_COLUMN, purple_primitive_get_name_from_type(primitive), SS_MENU_DATA_COLUMN, GINT_TO_POINTER(primitive), SS_MENU_EMBLEM_VISIBLE_COLUMN, FALSE, -1); - if (pixbuf != NULL) - g_object_unref(pixbuf); if (purple_savedstatus_is_transient(current_status) && !purple_savedstatus_has_substatuses(current_status) @@ -1730,23 +1712,20 @@ pidgin_status_menu_update_iter(GtkWidget *combobox, GtkListStore *store, GtkTreeIter *iter, PurpleSavedStatus *status) { - GdkPixbuf *pixbuf; + PurpleStatusPrimitive primitive; if (store == NULL) store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combobox))); - pixbuf = pidgin_create_status_icon(purple_savedstatus_get_type(status), - combobox, PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL); + primitive = purple_savedstatus_get_type(status); gtk_list_store_set(store, iter, SS_MENU_TYPE_COLUMN, SS_MENU_ENTRY_TYPE_SAVEDSTATUS, - SS_MENU_ICON_COLUMN, pixbuf, + SS_MENU_ICON_COLUMN, pidgin_stock_id_from_status_primitive(primitive), SS_MENU_TEXT_COLUMN, purple_savedstatus_get_title(status), SS_MENU_DATA_COLUMN, GINT_TO_POINTER(purple_savedstatus_get_creation_time(status)), SS_MENU_EMBLEM_COLUMN, GTK_STOCK_SAVE, SS_MENU_EMBLEM_VISIBLE_COLUMN, TRUE, -1); - if (pixbuf) - g_object_unref(G_OBJECT(pixbuf)); } static gboolean @@ -1828,7 +1807,7 @@ GtkCellRenderer *icon_rend; GtkCellRenderer *emblem_rend; - model = gtk_list_store_new(SS_MENU_NUM_COLUMNS, G_TYPE_INT, GDK_TYPE_PIXBUF, + model = gtk_list_store_new(SS_MENU_NUM_COLUMNS, G_TYPE_INT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_STRING, G_TYPE_BOOLEAN); combobox = gtk_combo_box_new(); @@ -1875,10 +1854,13 @@ gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combobox), icon_rend, FALSE); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combobox), text_rend, TRUE); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combobox), emblem_rend, FALSE); - gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combobox), icon_rend, "pixbuf", SS_MENU_ICON_COLUMN, NULL); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combobox), icon_rend, "stock-id", SS_MENU_ICON_COLUMN, NULL); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combobox), text_rend, "markup", SS_MENU_TEXT_COLUMN, NULL); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combobox), emblem_rend, "stock-id", SS_MENU_EMBLEM_COLUMN, "visible", SS_MENU_EMBLEM_VISIBLE_COLUMN, NULL); + g_object_set(G_OBJECT(icon_rend), + "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL), + NULL); gtk_combo_box_set_active(GTK_COMBO_BOX(combobox), index); g_signal_connect(G_OBJECT(combobox), "changed", G_CALLBACK(status_menu_cb), callback);
--- a/pidgin/gtksmiley.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtksmiley.c Wed Apr 29 23:20:51 2009 +0000 @@ -74,7 +74,7 @@ gtk_widget_destroy(smiley->parent); g_free(smiley->filename); if (smiley->custom_pixbuf) - gdk_pixbuf_unref(smiley->custom_pixbuf); + g_object_unref(G_OBJECT(smiley->custom_pixbuf)); g_free(smiley); } @@ -344,7 +344,7 @@ pixbuf = gdk_pixbuf_new_from_file_at_scale(filename, 64, 64, FALSE, NULL); gtk_image_set_from_pixbuf(GTK_IMAGE(s->smiley_image), pixbuf); if (pixbuf) - gdk_pixbuf_unref(pixbuf); + g_object_unref(G_OBJECT(pixbuf)); gtk_widget_grab_focus(s->smile); } @@ -459,8 +459,8 @@ pidgin_smiley_editor_set_image(PidginSmiley *editor, GdkPixbuf *image) { if (editor->custom_pixbuf) - gdk_pixbuf_unref(editor->custom_pixbuf); - editor->custom_pixbuf = image ? gdk_pixbuf_ref(image) : NULL; + g_object_unref(G_OBJECT(editor->custom_pixbuf)); + editor->custom_pixbuf = image ? g_object_ref(G_OBJECT(image)) : NULL; if (image) gtk_image_set_from_pixbuf(GTK_IMAGE(editor->smiley_image), image); }
--- a/pidgin/gtksourceundomanager.h Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtksourceundomanager.h Wed Apr 29 23:20:51 2009 +0000 @@ -28,12 +28,12 @@ #include <gtk/gtk.h> -#define GTK_SOURCE_TYPE_UNDO_MANAGER (gtk_source_undo_manager_get_type ()) -#define GTK_SOURCE_UNDO_MANAGER(obj) (GTK_CHECK_CAST ((obj), GTK_SOURCE_TYPE_UNDO_MANAGER, GtkSourceUndoManager)) -#define GTK_SOURCE_UNDO_MANAGER_CLASS(klass) (GTK_CHECK_CLASS_CAST ((klass), GTK_SOURCE_TYPE_UNDO_MANAGER, GtkSourceUndoManagerClass)) -#define GTK_SOURCE_IS_UNDO_MANAGER(obj) (GTK_CHECK_TYPE ((obj), GTK_SOURCE_TYPE_UNDO_MANAGER)) -#define GTK_SOURCE_IS_UNDO_MANAGER_CLASS(klass) (GTK_CHECK_CLASS_TYPE ((klass), GTK_SOURCE_TYPE_UNDO_MANAGER)) -#define GTK_SOURCE_UNDO_MANAGER_GET_CLASS(obj) (GTK_CHECK_GET_CLASS ((obj), GTK_SOURCE_TYPE_UNDO_MANAGER, GtkSourceUndoManagerClass)) +#define GTK_SOURCE_TYPE_UNDO_MANAGER (gtk_source_undo_manager_get_type()) +#define GTK_SOURCE_UNDO_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GTK_SOURCE_TYPE_UNDO_MANAGER, GtkSourceUndoManager)) +#define GTK_SOURCE_UNDO_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GTK_SOURCE_TYPE_UNDO_MANAGER, GtkSourceUndoManagerClass)) +#define GTK_SOURCE_IS_UNDO_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTK_SOURCE_TYPE_UNDO_MANAGER)) +#define GTK_SOURCE_IS_UNDO_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GTK_SOURCE_TYPE_UNDO_MANAGER)) +#define GTK_SOURCE_UNDO_MANAGER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), GTK_SOURCE_TYPE_UNDO_MANAGER, GtkSourceUndoManagerClass)) typedef struct _GtkSourceUndoManager GtkSourceUndoManager;
--- a/pidgin/gtkstatus-icon-theme.h Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkstatus-icon-theme.h Wed Apr 29 23:20:51 2009 +0000 @@ -1,5 +1,5 @@ /** - * @file status_icon-theme.h Pidgin Icon Theme Class API + * @file gtkstatus-icon-theme.h Pidgin Icon Theme Class API */ /* pidgin
--- a/pidgin/gtkstatusbox.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkstatusbox.c Wed Apr 29 23:20:51 2009 +0000 @@ -98,6 +98,9 @@ /** A PidginStatusBoxItemType */ TYPE_COLUMN, + /** This is the stock-id for the icon. */ + ICON_STOCK_COLUMN, + /** * This is a GdkPixbuf (the other columns are strings). * This column is visible. @@ -142,7 +145,51 @@ PROP_ICON_SEL, }; -GtkContainerClass *parent_class = NULL; +static char *typing_stock_ids[7] = { + PIDGIN_STOCK_ANIMATION_TYPING0, + PIDGIN_STOCK_ANIMATION_TYPING1, + PIDGIN_STOCK_ANIMATION_TYPING2, + PIDGIN_STOCK_ANIMATION_TYPING3, + PIDGIN_STOCK_ANIMATION_TYPING4, + NULL +}; + +static char *connecting_stock_ids[] = { + PIDGIN_STOCK_ANIMATION_CONNECT0, + PIDGIN_STOCK_ANIMATION_CONNECT1, + PIDGIN_STOCK_ANIMATION_CONNECT2, + PIDGIN_STOCK_ANIMATION_CONNECT3, + PIDGIN_STOCK_ANIMATION_CONNECT4, + PIDGIN_STOCK_ANIMATION_CONNECT5, + PIDGIN_STOCK_ANIMATION_CONNECT6, + PIDGIN_STOCK_ANIMATION_CONNECT7, + PIDGIN_STOCK_ANIMATION_CONNECT8, + PIDGIN_STOCK_ANIMATION_CONNECT9, + PIDGIN_STOCK_ANIMATION_CONNECT10, + PIDGIN_STOCK_ANIMATION_CONNECT11, + PIDGIN_STOCK_ANIMATION_CONNECT12, + PIDGIN_STOCK_ANIMATION_CONNECT13, + PIDGIN_STOCK_ANIMATION_CONNECT14, + PIDGIN_STOCK_ANIMATION_CONNECT15, + PIDGIN_STOCK_ANIMATION_CONNECT16, + PIDGIN_STOCK_ANIMATION_CONNECT17, + PIDGIN_STOCK_ANIMATION_CONNECT18, + PIDGIN_STOCK_ANIMATION_CONNECT19, + PIDGIN_STOCK_ANIMATION_CONNECT20, + PIDGIN_STOCK_ANIMATION_CONNECT21, + PIDGIN_STOCK_ANIMATION_CONNECT22, + PIDGIN_STOCK_ANIMATION_CONNECT23, + PIDGIN_STOCK_ANIMATION_CONNECT24, + PIDGIN_STOCK_ANIMATION_CONNECT25, + PIDGIN_STOCK_ANIMATION_CONNECT26, + PIDGIN_STOCK_ANIMATION_CONNECT27, + PIDGIN_STOCK_ANIMATION_CONNECT28, + PIDGIN_STOCK_ANIMATION_CONNECT29, + PIDGIN_STOCK_ANIMATION_CONNECT30, + NULL +}; + +static GtkContainerClass *parent_class = NULL; static void pidgin_status_box_class_init (PidginStatusBoxClass *klass); static void pidgin_status_box_init (PidginStatusBox *status_box); @@ -536,12 +583,12 @@ for (i = 0; i < G_N_ELEMENTS(statusbox->connecting_pixbufs); i++) { if (statusbox->connecting_pixbufs[i] != NULL) - gdk_pixbuf_unref(statusbox->connecting_pixbufs[i]); + g_object_unref(G_OBJECT(statusbox->connecting_pixbufs[i])); } for (i = 0; i < G_N_ELEMENTS(statusbox->typing_pixbufs); i++) { if (statusbox->typing_pixbufs[i] != NULL) - gdk_pixbuf_unref(statusbox->typing_pixbufs[i]); + g_object_unref(G_OBJECT(statusbox->typing_pixbufs[i])); } g_object_unref(G_OBJECT(statusbox->store)); @@ -599,32 +646,6 @@ ); } -static GdkPixbuf * -pidgin_status_box_get_pixbuf(PidginStatusBox *status_box, PurpleStatusPrimitive prim) -{ - GdkPixbuf *pixbuf; - GtkIconSize icon_size = gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL); - if (prim == PURPLE_STATUS_UNAVAILABLE) - pixbuf = gtk_widget_render_icon (GTK_WIDGET(status_box), PIDGIN_STOCK_STATUS_BUSY, - icon_size, "PidginStatusBox"); - else if (prim == PURPLE_STATUS_AWAY) - pixbuf = gtk_widget_render_icon (GTK_WIDGET(status_box), PIDGIN_STOCK_STATUS_AWAY, - icon_size, "PidginStatusBox"); - else if (prim == PURPLE_STATUS_EXTENDED_AWAY) - pixbuf = gtk_widget_render_icon (GTK_WIDGET(status_box), PIDGIN_STOCK_STATUS_XA, - icon_size, "PidginStatusBox"); - else if (prim == PURPLE_STATUS_INVISIBLE) - pixbuf = gtk_widget_render_icon (GTK_WIDGET(status_box), PIDGIN_STOCK_STATUS_INVISIBLE, - icon_size, "PidginStatusBox"); - else if (prim == PURPLE_STATUS_OFFLINE) - pixbuf = gtk_widget_render_icon (GTK_WIDGET(status_box), PIDGIN_STOCK_STATUS_OFFLINE, - icon_size, "PidginStatusBox"); - else - pixbuf = gtk_widget_render_icon (GTK_WIDGET(status_box), PIDGIN_STOCK_STATUS_AVAILABLE, - icon_size, "PidginStatusBox"); - return pixbuf; -} - /** * This updates the text displayed on the status box so that it shows * the current status. This is the only function in this file that @@ -638,7 +659,8 @@ char aa_color[8]; PurpleSavedStatus *saved_status; char *primary, *secondary, *text; - GdkPixbuf *pixbuf, *emblem = NULL; + const char *stock = NULL; + GdkPixbuf *emblem = NULL; GtkTreePath *path; gboolean account_status = FALSE; PurpleAccount *acct = (status_box->account) ? status_box->account : status_box->token_status_account; @@ -714,21 +736,21 @@ /* Pixbuf */ if (status_box->typing != 0) - pixbuf = status_box->typing_pixbufs[status_box->typing_index]; + stock = typing_stock_ids[status_box->typing_index]; else if (status_box->connecting) - pixbuf = status_box->connecting_pixbufs[status_box->connecting_index]; + stock = connecting_stock_ids[status_box->connecting_index]; else { PurpleStatusType *status_type; PurpleStatusPrimitive prim; if (account_status) { - status_type = purple_status_get_type(purple_account_get_active_status(acct)); + status_type = purple_status_get_type(purple_account_get_active_status(acct)); prim = purple_status_type_get_primitive(status_type); } else { - prim = purple_savedstatus_get_type(saved_status); + prim = purple_savedstatus_get_type(saved_status); } - pixbuf = pidgin_status_box_get_pixbuf(status_box, prim); + stock = pidgin_stock_id_from_status_primitive(prim); } if (status_box->account != NULL) { @@ -750,13 +772,11 @@ * really need to be a list store?) */ gtk_list_store_set(status_box->store, &(status_box->iter), - ICON_COLUMN, pixbuf, + ICON_STOCK_COLUMN, (gpointer)stock, TEXT_COLUMN, text, EMBLEM_COLUMN, emblem, EMBLEM_VISIBLE_COLUMN, (emblem != NULL), -1); - if ((status_box->typing == 0) && (!status_box->connecting)) - g_object_unref(pixbuf); g_free(text); if (emblem) g_object_unref(emblem); @@ -924,7 +944,6 @@ add_popular_statuses(PidginStatusBox *statusbox) { GList *list, *cur; - GdkPixbuf *pixbuf; list = purple_savedstatuses_get_popular(6); if (list == NULL) @@ -944,9 +963,6 @@ /* Get an appropriate status icon */ prim = purple_savedstatus_get_type(saved); - - pixbuf = pidgin_status_box_get_pixbuf(statusbox, prim); - if (purple_savedstatus_is_transient(saved)) { /* @@ -967,11 +983,9 @@ } pidgin_status_box_add(statusbox, type, - pixbuf, purple_savedstatus_get_title(saved), stripped, + NULL, purple_savedstatus_get_title(saved), stripped, GINT_TO_POINTER(purple_savedstatus_get_creation_time(saved))); g_free(stripped); - if (pixbuf != NULL) - g_object_unref(G_OBJECT(pixbuf)); } g_list_free(list); @@ -1032,7 +1046,6 @@ { /* Per-account */ GList *l; - GdkPixbuf *pixbuf; for (l = purple_account_get_status_types(account); l != NULL; l = l->next) { @@ -1045,22 +1058,17 @@ prim = purple_status_type_get_primitive(status_type); - pixbuf = pidgin_status_box_get_pixbuf(status_box, prim); - pidgin_status_box_add(PIDGIN_STATUS_BOX(status_box), - PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, pixbuf, + PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, NULL, purple_status_type_get_name(status_type), NULL, GINT_TO_POINTER(purple_status_type_get_primitive(status_type))); - if (pixbuf != NULL) - g_object_unref(pixbuf); } } static void pidgin_status_box_regenerate(PidginStatusBox *status_box) { - GdkPixbuf *pixbuf, *pixbuf2, *pixbuf3, *pixbuf4, *pixbuf5; GtkIconSize icon_size; icon_size = gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL); @@ -1075,8 +1083,6 @@ if (status_box->account == NULL) { - pixbuf = gtk_widget_render_icon (GTK_WIDGET(status_box->vbox), PIDGIN_STOCK_STATUS_AVAILABLE, - icon_size, "PidginStatusBox"); /* Do all the currently enabled accounts have the same statuses? * If so, display them instead of our global list. */ @@ -1084,25 +1090,11 @@ add_account_statuses(status_box, status_box->token_status_account); } else { /* Global */ - pixbuf2 = gtk_widget_render_icon (GTK_WIDGET(status_box->vbox), PIDGIN_STOCK_STATUS_AWAY, - icon_size, "PidginStatusBox"); - pixbuf3 = gtk_widget_render_icon (GTK_WIDGET(status_box->vbox), PIDGIN_STOCK_STATUS_OFFLINE, - icon_size, "PidginStatusBox"); - pixbuf4 = gtk_widget_render_icon (GTK_WIDGET(status_box->vbox), PIDGIN_STOCK_STATUS_INVISIBLE, - icon_size, "PidginStatusBox"); - pixbuf5 = gtk_widget_render_icon (GTK_WIDGET(status_box->vbox), PIDGIN_STOCK_STATUS_BUSY, - icon_size, "PidginStatusBox"); - - pidgin_status_box_add(PIDGIN_STATUS_BOX(status_box), PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, pixbuf, _("Available"), NULL, GINT_TO_POINTER(PURPLE_STATUS_AVAILABLE)); - pidgin_status_box_add(PIDGIN_STATUS_BOX(status_box), PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, pixbuf2, _("Away"), NULL, GINT_TO_POINTER(PURPLE_STATUS_AWAY)); - pidgin_status_box_add(PIDGIN_STATUS_BOX(status_box), PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, pixbuf5, _("Do not disturb"), NULL, GINT_TO_POINTER(PURPLE_STATUS_UNAVAILABLE)); - pidgin_status_box_add(PIDGIN_STATUS_BOX(status_box), PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, pixbuf4, _("Invisible"), NULL, GINT_TO_POINTER(PURPLE_STATUS_INVISIBLE)); - pidgin_status_box_add(PIDGIN_STATUS_BOX(status_box), PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, pixbuf3, _("Offline"), NULL, GINT_TO_POINTER(PURPLE_STATUS_OFFLINE)); - - if (pixbuf2) g_object_unref(G_OBJECT(pixbuf2)); - if (pixbuf3) g_object_unref(G_OBJECT(pixbuf3)); - if (pixbuf4) g_object_unref(G_OBJECT(pixbuf4)); - if (pixbuf5) g_object_unref(G_OBJECT(pixbuf5)); + pidgin_status_box_add(PIDGIN_STATUS_BOX(status_box), PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, NULL, _("Available"), NULL, GINT_TO_POINTER(PURPLE_STATUS_AVAILABLE)); + pidgin_status_box_add(PIDGIN_STATUS_BOX(status_box), PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, NULL, _("Away"), NULL, GINT_TO_POINTER(PURPLE_STATUS_AWAY)); + pidgin_status_box_add(PIDGIN_STATUS_BOX(status_box), PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, NULL, _("Do not disturb"), NULL, GINT_TO_POINTER(PURPLE_STATUS_UNAVAILABLE)); + pidgin_status_box_add(PIDGIN_STATUS_BOX(status_box), PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, NULL, _("Invisible"), NULL, GINT_TO_POINTER(PURPLE_STATUS_INVISIBLE)); + pidgin_status_box_add(PIDGIN_STATUS_BOX(status_box), PIDGIN_STATUS_BOX_TYPE_PRIMITIVE, NULL, _("Offline"), NULL, GINT_TO_POINTER(PURPLE_STATUS_OFFLINE)); } add_popular_statuses(status_box); @@ -1110,7 +1102,6 @@ pidgin_status_box_add_separator(PIDGIN_STATUS_BOX(status_box)); pidgin_status_box_add(PIDGIN_STATUS_BOX(status_box), PIDGIN_STATUS_BOX_TYPE_CUSTOM, NULL, _("New status..."), NULL, NULL); pidgin_status_box_add(PIDGIN_STATUS_BOX(status_box), PIDGIN_STATUS_BOX_TYPE_SAVED, NULL, _("Saved statuses..."), NULL, NULL); - if (pixbuf) g_object_unref(G_OBJECT(pixbuf)); status_menu_refresh_iter(status_box); pidgin_status_box_refresh(status_box); @@ -1202,45 +1193,26 @@ for (i = 0; i < G_N_ELEMENTS(status_box->connecting_pixbufs); i++) { if (status_box->connecting_pixbufs[i] != NULL) - gdk_pixbuf_unref(status_box->connecting_pixbufs[i]); + g_object_unref(G_OBJECT(status_box->connecting_pixbufs[i])); + if (connecting_stock_ids[i]) + status_box->connecting_pixbufs[i] = gtk_widget_render_icon (GTK_WIDGET(status_box->vbox), + connecting_stock_ids[i], icon_size, "PidginStatusBox"); + else + status_box->connecting_pixbufs[i] = NULL; } - status_box->connecting_index = 0; -#define CACHE_ANIMATION_CONNECT(index) \ - status_box->connecting_pixbufs[index] = gtk_widget_render_icon (GTK_WIDGET(status_box->vbox),\ - PIDGIN_STOCK_ANIMATION_CONNECT ## index, icon_size, "PidginStatusBox") - - CACHE_ANIMATION_CONNECT(0); - CACHE_ANIMATION_CONNECT(1); - CACHE_ANIMATION_CONNECT(2); - CACHE_ANIMATION_CONNECT(3); - CACHE_ANIMATION_CONNECT(4); - CACHE_ANIMATION_CONNECT(5); - CACHE_ANIMATION_CONNECT(6); - CACHE_ANIMATION_CONNECT(7); - CACHE_ANIMATION_CONNECT(8); - -#undef CACHE_ANIMATION_CONNECT for (i = 0; i < G_N_ELEMENTS(status_box->typing_pixbufs); i++) { if (status_box->typing_pixbufs[i] != NULL) - gdk_pixbuf_unref(status_box->typing_pixbufs[i]); + g_object_unref(G_OBJECT(status_box->typing_pixbufs[i])); + if (typing_stock_ids[i]) + status_box->typing_pixbufs[i] = gtk_widget_render_icon (GTK_WIDGET(status_box->vbox), + typing_stock_ids[i], icon_size, "PidginStatusBox"); + else + status_box->typing_pixbufs[i] = NULL; } - status_box->typing_index = 0; - -#define CACHE_ANIMATION_TYPING(index) \ - status_box->typing_pixbufs[index] = gtk_widget_render_icon (GTK_WIDGET(status_box->vbox), \ - PIDGIN_STOCK_ANIMATION_TYPING ## index, icon_size, "PidginStatusBox") - - CACHE_ANIMATION_TYPING(0); - CACHE_ANIMATION_TYPING(1); - CACHE_ANIMATION_TYPING(2); - CACHE_ANIMATION_TYPING(3); - CACHE_ANIMATION_TYPING(4); - -#undef CACHE_ANIMATION_TYPING } static void account_enabled_cb(PurpleAccount *acct, PidginStatusBox *status_box) @@ -1778,9 +1750,9 @@ status_box->vsep = gtk_vseparator_new(); status_box->arrow = gtk_arrow_new (GTK_ARROW_DOWN, GTK_SHADOW_NONE); - status_box->store = gtk_list_store_new(NUM_COLUMNS, G_TYPE_INT, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING, + status_box->store = gtk_list_store_new(NUM_COLUMNS, G_TYPE_INT, G_TYPE_STRING, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER, GDK_TYPE_PIXBUF, G_TYPE_BOOLEAN); - status_box->dropdown_store = gtk_list_store_new(NUM_COLUMNS, G_TYPE_INT, GDK_TYPE_PIXBUF, G_TYPE_STRING, + status_box->dropdown_store = gtk_list_store_new(NUM_COLUMNS, G_TYPE_INT, G_TYPE_STRING, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_STRING, G_TYPE_BOOLEAN); gtk_cell_view_set_model(GTK_CELL_VIEW(status_box->cell_view), GTK_TREE_MODEL(status_box->store)); @@ -1855,7 +1827,7 @@ gtk_tree_view_column_pack_start(status_box->column, icon_rend, FALSE); gtk_tree_view_column_pack_start(status_box->column, text_rend, TRUE); gtk_tree_view_column_pack_start(status_box->column, emblem_rend, FALSE); - gtk_tree_view_column_set_attributes(status_box->column, icon_rend, "pixbuf", ICON_COLUMN, NULL); + gtk_tree_view_column_set_attributes(status_box->column, icon_rend, "stock-id", ICON_STOCK_COLUMN, NULL); gtk_tree_view_column_set_attributes(status_box->column, text_rend, "markup", TEXT_COLUMN, NULL); gtk_tree_view_column_set_attributes(status_box->column, emblem_rend, "stock-id", EMBLEM_COLUMN, "visible", EMBLEM_VISIBLE_COLUMN, NULL); gtk_container_add(GTK_CONTAINER(status_box->scrolled_window), status_box->tree_view); @@ -1874,7 +1846,7 @@ gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(status_box->cell_view), status_box->icon_rend, FALSE); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(status_box->cell_view), status_box->text_rend, TRUE); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(status_box->cell_view), emblem_rend, FALSE); - gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(status_box->cell_view), status_box->icon_rend, "pixbuf", ICON_COLUMN, NULL); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(status_box->cell_view), status_box->icon_rend, "stock-id", ICON_STOCK_COLUMN, NULL); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(status_box->cell_view), status_box->text_rend, "markup", TEXT_COLUMN, NULL); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(status_box->cell_view), emblem_rend, "pixbuf", EMBLEM_COLUMN, "visible", EMBLEM_VISIBLE_COLUMN, NULL); #if GTK_CHECK_VERSION(2, 6, 0) @@ -2133,7 +2105,8 @@ * * @param status_box The status box itself. * @param type A PidginStatusBoxItemType. - * @param pixbuf The icon to associate with this row in the menu. + * @param pixbuf The icon to associate with this row in the menu. The + * function will try to decide a pixbuf if none is given. * @param title The title of this item. For the primitive entries, * this is something like "Available" or "Away." For * the saved statuses, this is something like @@ -2149,10 +2122,12 @@ * creation timestamp. */ void -pidgin_status_box_add(PidginStatusBox *status_box, PidginStatusBoxItemType type, GdkPixbuf *pixbuf, const char *title, const char *desc, gpointer data) +pidgin_status_box_add(PidginStatusBox *status_box, PidginStatusBoxItemType type, GdkPixbuf *pixbuf, + const char *title, const char *desc, gpointer data) { GtkTreeIter iter; char *text; + const char *stock = NULL; if (desc == NULL) { @@ -2179,10 +2154,25 @@ g_free(escaped_desc); } + if (!pixbuf) { + PurpleStatusPrimitive prim = PURPLE_STATUS_UNSET; + if (type == PIDGIN_STATUS_BOX_TYPE_PRIMITIVE) { + prim = GPOINTER_TO_INT(data); + } else if (type == PIDGIN_STATUS_BOX_TYPE_SAVED_POPULAR || + type == PIDGIN_STATUS_BOX_TYPE_POPULAR) { + PurpleSavedStatus *saved = purple_savedstatus_find_by_creation_time(GPOINTER_TO_INT(data)); + if (saved) { + prim = purple_savedstatus_get_type(saved); + } + } + + stock = pidgin_stock_id_from_status_primitive(prim); + } + gtk_list_store_append(status_box->dropdown_store, &iter); gtk_list_store_set(status_box->dropdown_store, &iter, TYPE_COLUMN, type, - ICON_COLUMN, pixbuf, + ICON_STOCK_COLUMN, stock, TEXT_COLUMN, text, TITLE_COLUMN, title, DESC_COLUMN, desc, @@ -2303,20 +2293,16 @@ { if (!status_box) return; - if (status_box->connecting_index == 8) + if (!connecting_stock_ids[++status_box->connecting_index]) status_box->connecting_index = 0; - else - status_box->connecting_index++; pidgin_status_box_refresh(status_box); } static void pidgin_status_box_pulse_typing(PidginStatusBox *status_box) { - if (status_box->typing_index == 4) + if (!typing_stock_ids[++status_box->typing_index]) status_box->typing_index = 0; - else - status_box->typing_index++; pidgin_status_box_refresh(status_box); }
--- a/pidgin/gtkutils.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkutils.c Wed Apr 29 23:20:51 2009 +0000 @@ -358,7 +358,7 @@ } GtkWidget *pidgin_new_check_item(GtkWidget *menu, const char *str, - GtkSignalFunc sf, gpointer data, gboolean checked) + GCallback cb, gpointer data, gboolean checked) { GtkWidget *menuitem; menuitem = gtk_check_menu_item_new_with_mnemonic(str); @@ -368,8 +368,8 @@ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem), checked); - if (sf) - g_signal_connect(G_OBJECT(menuitem), "activate", sf, data); + if (cb) + g_signal_connect(G_OBJECT(menuitem), "activate", cb, data); gtk_widget_show_all(menuitem); @@ -439,7 +439,7 @@ } -GtkWidget *pidgin_new_item_from_stock(GtkWidget *menu, const char *str, const char *icon, GtkSignalFunc sf, gpointer data, guint accel_key, guint accel_mods, char *mod) +GtkWidget *pidgin_new_item_from_stock(GtkWidget *menu, const char *str, const char *icon, GCallback cb, gpointer data, guint accel_key, guint accel_mods, char *mod) { GtkWidget *menuitem; /* @@ -456,8 +456,8 @@ if (menu) gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem); - if (sf) - g_signal_connect(G_OBJECT(menuitem), "activate", sf, data); + if (cb) + g_signal_connect(G_OBJECT(menuitem), "activate", cb, data); if (icon != NULL) { image = gtk_image_new_from_stock(icon, gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL)); @@ -1464,7 +1464,7 @@ str); g_free(str); - return; + break; } buddy = purple_find_buddy(data->account, data->who); @@ -1494,7 +1494,7 @@ g_error_free(err); g_free(str); - return; + break; } id = purple_imgstore_add_with_id(filedata, size, data->filename); @@ -1627,7 +1627,7 @@ _("Set as buddy icon"), DND_BUDDY_ICON, (ft ? _("Send image file") : _("Insert in message")), (ft ? DND_FILE_TRANSFER : DND_IM_IMAGE), NULL); - gdk_pixbuf_unref(pb); + g_object_unref(G_OBJECT(pb)); return; } @@ -1715,29 +1715,67 @@ { GtkIconSize icon_size = gtk_icon_size_from_name(size); GdkPixbuf *pixbuf = NULL; - - if (prim == PURPLE_STATUS_UNAVAILABLE) - pixbuf = gtk_widget_render_icon (w, PIDGIN_STOCK_STATUS_BUSY, - icon_size, "GtkWidget"); - else if (prim == PURPLE_STATUS_AWAY) - pixbuf = gtk_widget_render_icon (w, PIDGIN_STOCK_STATUS_AWAY, - icon_size, "GtkWidget"); - else if (prim == PURPLE_STATUS_EXTENDED_AWAY) - pixbuf = gtk_widget_render_icon (w, PIDGIN_STOCK_STATUS_XA, - icon_size, "GtkWidget"); - else if (prim == PURPLE_STATUS_INVISIBLE) - pixbuf = gtk_widget_render_icon (w, PIDGIN_STOCK_STATUS_INVISIBLE, - icon_size, "GtkWidget"); - else if (prim == PURPLE_STATUS_OFFLINE) - pixbuf = gtk_widget_render_icon (w, PIDGIN_STOCK_STATUS_OFFLINE, - icon_size, "GtkWidget"); - else - pixbuf = gtk_widget_render_icon (w, PIDGIN_STOCK_STATUS_AVAILABLE, - icon_size, "GtkWidget"); + const char *stock = pidgin_stock_id_from_status_primitive(prim); + + pixbuf = gtk_widget_render_icon (w, stock ? stock : PIDGIN_STOCK_STATUS_AVAILABLE, + icon_size, "GtkWidget"); return pixbuf; - } +static const char * +stock_id_from_status_primitive_idle(PurpleStatusPrimitive prim, gboolean idle) +{ + const char *stock = NULL; + switch (prim) { + case PURPLE_STATUS_UNSET: + stock = NULL; + break; + case PURPLE_STATUS_UNAVAILABLE: + stock = idle ? PIDGIN_STOCK_STATUS_BUSY_I : PIDGIN_STOCK_STATUS_BUSY; + break; + case PURPLE_STATUS_AWAY: + stock = idle ? PIDGIN_STOCK_STATUS_AWAY_I : PIDGIN_STOCK_STATUS_AWAY; + break; + case PURPLE_STATUS_EXTENDED_AWAY: + stock = idle ? PIDGIN_STOCK_STATUS_XA_I : PIDGIN_STOCK_STATUS_XA; + break; + case PURPLE_STATUS_INVISIBLE: + stock = PIDGIN_STOCK_STATUS_INVISIBLE; + break; + case PURPLE_STATUS_OFFLINE: + stock = idle ? PIDGIN_STOCK_STATUS_OFFLINE_I : PIDGIN_STOCK_STATUS_OFFLINE; + break; + default: + stock = idle ? PIDGIN_STOCK_STATUS_AVAILABLE_I : PIDGIN_STOCK_STATUS_AVAILABLE; + break; + } + return stock; +} + +const char * +pidgin_stock_id_from_status_primitive(PurpleStatusPrimitive prim) +{ + return stock_id_from_status_primitive_idle(prim, FALSE); +} + +const char * +pidgin_stock_id_from_presence(PurplePresence *presence) +{ + PurpleStatus *status; + PurpleStatusType *type; + PurpleStatusPrimitive prim; + gboolean idle; + + g_return_val_if_fail(presence, NULL); + + status = purple_presence_get_active_status(presence); + type = purple_status_get_type(status); + prim = purple_status_type_get_primitive(type); + + idle = purple_presence_is_idle(presence); + + return stock_id_from_status_primitive_idle(prim, idle); +} GdkPixbuf * pidgin_create_prpl_icon(PurpleAccount *account, PidginPrplIconSize size) @@ -2943,7 +2981,7 @@ #endif } -GSList *minidialogs = NULL; +static GSList *minidialogs = NULL; static void * pidgin_utils_get_handle(void)
--- a/pidgin/gtkutils.h Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkutils.h Wed Apr 29 23:20:51 2009 +0000 @@ -233,14 +233,14 @@ * * @param menu The menu to which to append the check menu item. * @param str The title to use for the newly created menu item. - * @param sf A function to call when the menu item is activated. + * @param cb A function to call when the menu item is activated. * @param data Data to pass to the signal function. * @param checked The initial state of the check item * * @return The newly created menu item. */ GtkWidget *pidgin_new_check_item(GtkWidget *menu, const char *str, - GtkSignalFunc sf, gpointer data, gboolean checked); + GCallback cb, gpointer data, gboolean checked); /** * Creates a menu item. @@ -249,7 +249,7 @@ * @param str The title for the menu item. * @param icon An icon to place to the left of the menu item, * or @c NULL for no icon. - * @param sf A function to call when the menu item is activated. + * @param cb A function to call when the menu item is activated. * @param data Data to pass to the signal function. * @param accel_key Something. * @param accel_mods Something. @@ -258,7 +258,7 @@ * @return The newly created menu item. */ GtkWidget *pidgin_new_item_from_stock(GtkWidget *menu, const char *str, - const char *icon, GtkSignalFunc sf, + const char *icon, GCallback cb, gpointer data, guint accel_key, guint accel_mods, char *mod); @@ -569,6 +569,27 @@ */ GdkPixbuf * pidgin_create_status_icon(PurpleStatusPrimitive primitive, GtkWidget *w, const char *size); +/** + * Returns an appropriate stock-id for a status primitive. + * + * @param prim The status primitive + * + * @return The stock-id + * + * @since 2.6.0 + */ +const char *pidgin_stock_id_from_status_primitive(PurpleStatusPrimitive prim); + +/** + * Returns an appropriate stock-id for a PurplePresence. + * + * @param presence The presence. + * + * @return The stock-id + * + * @since 2.6.0 + */ +const char *pidgin_stock_id_from_presence(PurplePresence *presence); /** * Append a PurpleMenuAction to a menu.
--- a/pidgin/gtkwhiteboard.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/gtkwhiteboard.c Wed Apr 29 23:20:51 2009 +0000 @@ -624,7 +624,7 @@ update_rect.x, update_rect.y, update_rect.width, update_rect.height); - gdk_gc_unref(gfx_con); + g_object_unref(G_OBJECT(gfx_con)); } /* Uses Bresenham's algorithm (as provided by Wikipedia) */
--- a/pidgin/pidginstock.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/pidginstock.c Wed Apr 29 23:20:51 2009 +0000 @@ -320,7 +320,7 @@ } static gchar * -find_icon_file(PidginStatusIconTheme *theme, const gchar *size, SizedStockIcon sized_icon, gboolean rtl) +find_icon_file(PidginIconTheme *theme, const gchar *size, SizedStockIcon sized_icon, gboolean rtl) { const gchar *file, *dir; gchar *file_full = NULL; @@ -352,7 +352,7 @@ } static void -add_sized_icon(GtkIconSet *iconset, GtkIconSize sizeid, PidginStatusIconTheme *theme, +add_sized_icon(GtkIconSet *iconset, GtkIconSize sizeid, PidginIconTheme *theme, const char *size, SizedStockIcon sized_icon, gboolean translucent) { char *filename; @@ -409,6 +409,16 @@ } } +static void +reload_settings(void) +{ +#if GTK_CHECK_VERSION(2,4,0) + GtkSettings *setting = NULL; + setting = gtk_settings_get_default(); + gtk_rc_reset_styles(setting); +#endif +} + /***************************************************************************** * Public API functions *****************************************************************************/ @@ -447,9 +457,9 @@ translucent = gtk_icon_set_new(); #define ADD_SIZED_ICON(name, size) if (sized_status_icons[i].name) { \ - add_sized_icon(normal, name, theme, size, sized_status_icons[i], FALSE); \ + add_sized_icon(normal, name, PIDGIN_ICON_THEME(theme), size, sized_status_icons[i], FALSE); \ if (sized_status_icons[i].translucent_name) \ - add_sized_icon(translucent, name, theme, size, sized_status_icons[i], TRUE); \ + add_sized_icon(translucent, name, PIDGIN_ICON_THEME(theme), size, sized_status_icons[i], TRUE); \ } ADD_SIZED_ICON(microscopic, "11"); ADD_SIZED_ICON(extra_small, "16"); @@ -471,52 +481,45 @@ gtk_widget_destroy(win); g_object_unref(G_OBJECT(icon_factory)); + reload_settings(); } void -pidgin_stock_init(void) +pidgin_stock_load_stock_icon_theme(PidginStockIconTheme *theme) { GtkIconFactory *icon_factory; - size_t i; + gint i; GtkWidget *win; - PidginIconThemeLoader *loader; - const gchar *path = NULL; - - if (stock_initted) - return; - stock_initted = TRUE; + if (theme != NULL) { + purple_prefs_set_string(PIDGIN_PREFS_ROOT "/stock/icon-theme", + purple_theme_get_name(PURPLE_THEME(theme))); + purple_prefs_set_path(PIDGIN_PREFS_ROOT "/stock/icon-theme-dir", + purple_theme_get_dir(PURPLE_THEME(theme))); + } + else { + purple_prefs_set_string(PIDGIN_PREFS_ROOT "/stock/icon-theme", ""); + purple_prefs_set_path(PIDGIN_PREFS_ROOT "/stock/icon-theme-dir", ""); + } - /* Setup the status icon theme */ - loader = g_object_new(PIDGIN_TYPE_ICON_THEME_LOADER, "type", "status-icon", NULL); - purple_theme_manager_register_type(PURPLE_THEME_LOADER(loader)); - purple_prefs_add_string(PIDGIN_PREFS_ROOT "/status/icon-theme", ""); - purple_prefs_add_path(PIDGIN_PREFS_ROOT "/status/icon-theme-dir", ""); - - /* Setup the icon factory. */ icon_factory = gtk_icon_factory_new(); gtk_icon_factory_add_default(icon_factory); - /* Er, yeah, a hack, but it works. :) */ win = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_widget_realize(win); /* All non-sized icons */ - for (i = 0; i < G_N_ELEMENTS(stock_icons); i++) - { + for (i = 0; i < G_N_ELEMENTS(stock_icons); i++) { GtkIconSource *source; GtkIconSet *iconset; gchar *filename; - if (stock_icons[i].dir == NULL) - { + if (stock_icons[i].dir == NULL) { /* GTK+ Stock icon */ iconset = gtk_style_lookup_icon_set(gtk_widget_get_style(win), stock_icons[i].filename); - } - else - { + } else { filename = find_file(stock_icons[i].dir, stock_icons[i].filename); if (filename == NULL) @@ -540,21 +543,13 @@ gtk_icon_set_unref(iconset); } - /* register custom icon sizes */ - microscopic = gtk_icon_size_register(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC, 11, 11); - extra_small = gtk_icon_size_register(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL, 16, 16); - small = gtk_icon_size_register(PIDGIN_ICON_SIZE_TANGO_SMALL, 22, 22); - medium = gtk_icon_size_register(PIDGIN_ICON_SIZE_TANGO_MEDIUM, 32, 32); - large = gtk_icon_size_register(PIDGIN_ICON_SIZE_TANGO_LARGE, 48, 48); - huge = gtk_icon_size_register(PIDGIN_ICON_SIZE_TANGO_HUGE, 64, 64); - /* All non-status sized icons */ for (i = 0; i < G_N_ELEMENTS(sized_stock_icons); i++) { GtkIconSet *iconset = gtk_icon_set_new(); #define ADD_SIZED_ICON(name, size) if (sized_stock_icons[i].name) \ - add_sized_icon(iconset, name, NULL, size, sized_stock_icons[i], FALSE); + add_sized_icon(iconset, name, PIDGIN_ICON_THEME(theme), size, sized_stock_icons[i], FALSE); ADD_SIZED_ICON(microscopic, "11"); ADD_SIZED_ICON(extra_small, "16"); ADD_SIZED_ICON(small, "22"); @@ -569,6 +564,40 @@ gtk_widget_destroy(win); g_object_unref(G_OBJECT(icon_factory)); + reload_settings(); +} + +void +pidgin_stock_init(void) +{ + PidginIconThemeLoader *loader, *stockloader; + const gchar *path = NULL; + + if (stock_initted) + return; + + stock_initted = TRUE; + + /* Setup the status icon theme */ + loader = g_object_new(PIDGIN_TYPE_ICON_THEME_LOADER, "type", "status-icon", NULL); + purple_theme_manager_register_type(PURPLE_THEME_LOADER(loader)); + purple_prefs_add_string(PIDGIN_PREFS_ROOT "/status/icon-theme", ""); + purple_prefs_add_path(PIDGIN_PREFS_ROOT "/status/icon-theme-dir", ""); + + stockloader = g_object_new(PIDGIN_TYPE_ICON_THEME_LOADER, "type", "stock-icon", NULL); + purple_theme_manager_register_type(PURPLE_THEME_LOADER(stockloader)); + purple_prefs_add_string(PIDGIN_PREFS_ROOT "/stock/icon-theme", ""); + purple_prefs_add_path(PIDGIN_PREFS_ROOT "/stock/icon-theme-dir", ""); + + /* register custom icon sizes */ + microscopic = gtk_icon_size_register(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC, 11, 11); + extra_small = gtk_icon_size_register(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL, 16, 16); + small = gtk_icon_size_register(PIDGIN_ICON_SIZE_TANGO_SMALL, 22, 22); + medium = gtk_icon_size_register(PIDGIN_ICON_SIZE_TANGO_MEDIUM, 32, 32); + large = gtk_icon_size_register(PIDGIN_ICON_SIZE_TANGO_LARGE, 48, 48); + huge = gtk_icon_size_register(PIDGIN_ICON_SIZE_TANGO_HUGE, 64, 64); + + pidgin_stock_load_stock_icon_theme(NULL); /* Pre-load Status icon theme - this avoids a bug with displaying the correct icon in the tray, theme is destroyed after*/ if (purple_prefs_get_string(PIDGIN_PREFS_ROOT "/icon/status/theme") && @@ -583,3 +612,31 @@ /* Register the stock items. */ gtk_stock_add_static(stock_items, G_N_ELEMENTS(stock_items)); } + +static void +pidgin_stock_icon_theme_class_init(PidginStockIconThemeClass *klass) +{ +} + +GType +pidgin_stock_icon_theme_get_type(void) +{ + static GType type = 0; + if (type == 0) { + static const GTypeInfo info = { + sizeof (PidginStockIconThemeClass), + NULL, /* base_init */ + NULL, /* base_finalize */ + (GClassInitFunc)pidgin_stock_icon_theme_class_init, /* class_init */ + NULL, /* class_finalize */ + NULL, /* class_data */ + sizeof (PidginStockIconTheme), + 0, /* n_preallocs */ + NULL, + NULL, /* value table */ + }; + type = g_type_register_static(PIDGIN_TYPE_ICON_THEME, + "PidginStockIconTheme", &info, 0); + } + return type; +}
--- a/pidgin/pidginstock.h Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/pidginstock.h Wed Apr 29 23:20:51 2009 +0000 @@ -185,15 +185,54 @@ #define PIDGIN_ICON_SIZE_TANGO_HUGE "pidgin-icon-size-tango-huge" /** + * extends PidginIconTheme (gtkicon-theme.h) + * A pidgin stock icon theme. + * This object represents a Pidgin stock icon theme. + * + * PidginStockIconTheme is a PidginIconTheme Object. + */ +typedef struct _PidginStockIconTheme PidginStockIconTheme; +typedef struct _PidginStockIconThemeClass PidginStockIconThemeClass; + +#define PIDGIN_TYPE_STOCK_ICON_THEME (pidgin_stock_icon_theme_get_type ()) +#define PIDGIN_STOCK_ICON_THEME(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), PIDGIN_TYPE_STOCK_ICON_THEME, PidginStockIconTheme)) +#define PIDGIN_STOCK_ICON_THEME_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), PIDGIN_TYPE_STOCK_ICON_THEME, PidginStockIconThemeClass)) +#define PIDGIN_IS_STOCK_ICON_THEME(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), PIDGIN_TYPE_STOCK_ICON_THEME)) +#define PIDGIN_IS_STOCK_ICON_THEME_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), PIDGIN_TYPE_STOCK_ICON_THEME)) +#define PIDGIN_STOCK_ICON_THEME_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), PIDGIN_TYPE_STOCK_ICON_THEME, PidginStockIconThemeClass)) + +struct _PidginStockIconTheme +{ + PidginIconTheme parent; +}; + +struct _PidginStockIconThemeClass +{ + PidginIconThemeClass parent_class; +}; + +G_BEGIN_DECLS + +/** + * GObject foo. + * @internal. + */ +GType pidgin_stock_icon_theme_get_type(void); + +/** * Loades all of the icons from the status icon theme into Pidgin stock * * @param theme the theme to load, or null to load all the default icons */ void pidgin_stock_load_status_icon_theme(PidginStatusIconTheme *theme); + +void pidgin_stock_load_stock_icon_theme(PidginStockIconTheme *theme); + /** * Sets up the purple stock repository. */ void pidgin_stock_init(void); +G_END_DECLS #endif /* _PIDGIN_STOCK_H_ */
--- a/pidgin/plugins/Makefile.am Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/plugins/Makefile.am Wed Apr 29 23:20:51 2009 +0000 @@ -43,6 +43,7 @@ relnot_la_LDFLAGS = -module -avoid-version sendbutton_la_LDFLAGS = -module -avoid-version spellchk_la_LDFLAGS = -module -avoid-version +themeedit_la_LDFLAGS = -module -avoid-version timestamp_la_LDFLAGS = -module -avoid-version timestamp_format_la_LDFLAGS = -module -avoid-version xmppconsole_la_LDFLAGS = -module -avoid-version @@ -61,6 +62,7 @@ relnot.la \ sendbutton.la \ spellchk.la \ + themeedit.la \ timestamp.la \ timestamp_format.la \ xmppconsole.la @@ -82,6 +84,7 @@ relnot_la_SOURCES = relnot.c sendbutton_la_SOURCES = sendbutton.c spellchk_la_SOURCES = spellchk.c +themeedit_la_SOURCES = themeedit.c themeedit-icon.c themeedit-icon.h timestamp_la_SOURCES = timestamp.c timestamp_format_la_SOURCES = timestamp_format.c xmppconsole_la_SOURCES = xmppconsole.c @@ -99,6 +102,7 @@ relnot_la_LIBADD = $(GLIB_LIBS) sendbutton_la_LIBADD = $(GTK_LIBS) spellchk_la_LIBADD = $(GTK_LIBS) +themeedit_la_LIBADD = $(GTK_LIBS) timestamp_la_LIBADD = $(GTK_LIBS) timestamp_format_la_LIBADD = $(GTK_LIBS) xmppconsole_la_LIBADD = $(GTK_LIBS)
--- a/pidgin/plugins/contact_priority.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/plugins/contact_priority.c Wed Apr 29 23:20:51 2009 +0000 @@ -31,7 +31,7 @@ select_account(GtkWidget *widget, PurpleAccount *account, gpointer data) { gtk_spin_button_set_value(GTK_SPIN_BUTTON(data), - (gdouble)purple_account_get_int(account, "score", 0)); + (gdouble)purple_account_get_int(account, "score", 0)); } static void @@ -142,18 +142,18 @@ spin = gtk_spin_button_new((GtkAdjustment *)adj, 1, 0); optmenu = pidgin_account_option_menu_new(NULL, TRUE, - G_CALLBACK(select_account), - NULL, spin); + G_CALLBACK(select_account), + NULL, spin); gtk_box_pack_start(GTK_BOX(hbox), optmenu, FALSE, FALSE, 0); /* this is where we set up the spin button we made above */ account = g_object_get_data(G_OBJECT(gtk_menu_get_active(GTK_MENU(gtk_option_menu_get_menu(GTK_OPTION_MENU(optmenu))))), - "account"); + "account"); gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin), - (gdouble)purple_account_get_int(account, "score", 0)); + (gdouble)purple_account_get_int(account, "score", 0)); gtk_spin_button_set_adjustment(GTK_SPIN_BUTTON(spin), GTK_ADJUSTMENT(adj)); g_signal_connect(G_OBJECT(spin), "value-changed", - G_CALLBACK(account_update), optmenu); + G_CALLBACK(account_update), optmenu); gtk_box_pack_start(GTK_BOX(hbox), spin, FALSE, FALSE, 0); gtk_widget_show_all(ret); @@ -178,29 +178,29 @@ PURPLE_PLUGIN_MAGIC, PURPLE_MAJOR_VERSION, PURPLE_MINOR_VERSION, - PURPLE_PLUGIN_STANDARD, /**< type */ + PURPLE_PLUGIN_STANDARD, /**< type */ PIDGIN_PLUGIN_TYPE, /**< ui_requirement */ - 0, /**< flags */ - NULL, /**< dependencies */ - PURPLE_PRIORITY_DEFAULT, /**< priority */ + 0, /**< flags */ + NULL, /**< dependencies */ + PURPLE_PRIORITY_DEFAULT, /**< priority */ - CONTACT_PRIORITY_PLUGIN_ID, /**< id */ - N_("Contact Priority"), /**< name */ - DISPLAY_VERSION, /**< version */ + CONTACT_PRIORITY_PLUGIN_ID, /**< id */ + N_("Contact Priority"), /**< name */ + DISPLAY_VERSION, /**< version */ /**< summary */ N_("Allows for controlling the values associated with different buddy states."), /**< description */ N_("Allows for changing the point values of idle/away/offline states for buddies in contact priority computations."), - "Etan Reisner <deryni@eden.rutgers.edu>", /**< author */ - PURPLE_WEBSITE, /**< homepage */ + "Etan Reisner <deryni@eden.rutgers.edu>", /**< author */ + PURPLE_WEBSITE, /**< homepage */ - NULL, /**< load */ - NULL, /**< unload */ - NULL, /**< destroy */ - &ui_info, /**< ui_info */ - NULL, /**< extra_info */ - NULL, /**< prefs_info */ - NULL, /**< actions */ + NULL, /**< load */ + NULL, /**< unload */ + NULL, /**< destroy */ + &ui_info, /**< ui_info */ + NULL, /**< extra_info */ + NULL, /**< prefs_info */ + NULL, /**< actions */ /* padding */ NULL,
--- a/pidgin/plugins/markerline.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/plugins/markerline.c Wed Apr 29 23:20:51 2009 +0000 @@ -84,7 +84,7 @@ gdk_gc_set_rgb_fg_color(gc, &red); gdk_draw_line(event->window, gc, 0, y, visible_rect.width, y); - gdk_gc_unref(gc); + g_object_unref(G_OBJECT(gc)); } return FALSE; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pidgin/plugins/themeedit-icon.c Wed Apr 29 23:20:51 2009 +0000 @@ -0,0 +1,312 @@ +/* Pidgin + * + * Pidgin is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ +#include "internal.h" +#include "pidgin.h" +#include "debug.h" +#include "version.h" + +#include "theme-manager.h" + +#include "gtkblist.h" +#include "gtkblist-theme.h" +#include "gtkutils.h" +#include "gtkplugin.h" + +#include "pidginstock.h" +#include "themeedit-icon.h" + +typedef enum +{ + FLAG_SIZE_MICROSOPIC = 0, + FLAG_SIZE_EXTRA_SMALL, + FLAG_SIZE_SMALL, + FLAG_SIZE_MEDIUM, + FLAG_SIZE_LARGE, + FLAG_SIZE_HUGE, + FLAG_SIZE_NONE, +} SectionFlags; + +#define SECTION_FLAGS_ALL (0x3f) + +static const char *stocksizes [] = { + [FLAG_SIZE_MICROSOPIC] = PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC, + [FLAG_SIZE_EXTRA_SMALL] = PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL, + [FLAG_SIZE_SMALL] = PIDGIN_ICON_SIZE_TANGO_SMALL, + [FLAG_SIZE_MEDIUM] = PIDGIN_ICON_SIZE_TANGO_MEDIUM, + [FLAG_SIZE_LARGE] = PIDGIN_ICON_SIZE_TANGO_LARGE, + [FLAG_SIZE_HUGE] = PIDGIN_ICON_SIZE_TANGO_HUGE, + [FLAG_SIZE_NONE] = NULL, +}; + +static const struct options { + const char *stockid; + const char *text; +} statuses[] = { + {PIDGIN_STOCK_STATUS_AVAILABLE, N_("Available")}, + {PIDGIN_STOCK_STATUS_AWAY, N_("Away")}, + {PIDGIN_STOCK_STATUS_XA, N_("Extended Away")}, + {PIDGIN_STOCK_STATUS_BUSY, N_("Busy")}, + {PIDGIN_STOCK_STATUS_OFFLINE, N_("Offline")}, + {PIDGIN_STOCK_STATUS_LOGIN, N_("Just logged in")}, + {PIDGIN_STOCK_STATUS_LOGOUT, N_("Just logged out")}, + {PIDGIN_STOCK_STATUS_PERSON, N_("Icon for Contact/\nIcon for Unknown person")}, + {PIDGIN_STOCK_STATUS_CHAT, N_("Icon for Chat")}, + {NULL, NULL} +}, chatemblems[] = { + {PIDGIN_STOCK_STATUS_IGNORED, N_("Ignored")}, + {PIDGIN_STOCK_STATUS_FOUNDER, N_("Founder")}, + {PIDGIN_STOCK_STATUS_OPERATOR, N_("Operator")}, + {PIDGIN_STOCK_STATUS_HALFOP, N_("Half Operator")}, + {PIDGIN_STOCK_STATUS_VOICE, N_("Voice")}, + {NULL, NULL} +}, dialogicons[] = { + {PIDGIN_STOCK_DIALOG_AUTH, N_("Authorization dialog")}, + {PIDGIN_STOCK_DIALOG_ERROR, N_("Error dialog")}, + {PIDGIN_STOCK_DIALOG_INFO, N_("Information dialog")}, + {PIDGIN_STOCK_DIALOG_MAIL, N_("Mail dialog")}, + {PIDGIN_STOCK_DIALOG_QUESTION, N_("Question dialog")}, + {PIDGIN_STOCK_DIALOG_WARNING, N_("Warning dialog")}, + {NULL, NULL}, + {PIDGIN_STOCK_DIALOG_COOL, N_("What kind of dialog is this?")}, +}; + +static const struct { + const char *heading; + const struct options *options; + SectionFlags flags; +} sections[] = { + {N_("Status Icons"), statuses, SECTION_FLAGS_ALL ^ (1 << FLAG_SIZE_HUGE)}, + {N_("Chatroom Emblems"), chatemblems, FLAG_SIZE_SMALL}, + {N_("Dialog Icons"), dialogicons, (1 << FLAG_SIZE_EXTRA_SMALL) | (1 << FLAG_SIZE_HUGE)}, + {NULL, NULL, 0} +}; + +static PidginStatusIconTheme * +create_icon_theme(GtkWidget *window) +{ + int s, i, j; + char *dirname = "/tmp"; /* FIXME */ + PidginStatusIconTheme *theme = g_object_new(PIDGIN_TYPE_STATUS_ICON_THEME, "type", "status-icon", + "author", getlogin(), + "directory", dirname, + NULL); + + for (s = 0; sections[s].heading; s++) { + GtkWidget *vbox = g_object_get_data(G_OBJECT(window), sections[s].heading); + for (i = 0; sections[s].options[i].stockid; i++) { + GtkWidget *image = g_object_get_data(G_OBJECT(vbox), sections[s].options[i].stockid); + GdkPixbuf *pixbuf = g_object_get_data(G_OBJECT(image), "pixbuf"); + if (!pixbuf) + continue; + pidgin_icon_theme_set_icon(PIDGIN_ICON_THEME(theme), sections[s].options[i].stockid, + sections[s].options[i].stockid); + for (j = 0; stocksizes[j]; j++) { + int width, height; + GtkIconSize iconsize; + char size[8]; + char *name; + GdkPixbuf *scale; + GError *error = NULL; + + if (!(sections[s].flags & (1 << j))) + continue; + + iconsize = gtk_icon_size_from_name(stocksizes[j]); + gtk_icon_size_lookup(iconsize, &width, &height); + g_snprintf(size, sizeof(size), "%d", width); + + if (i == 0) { + name = g_build_filename(dirname, size, NULL); + purple_build_dir(name, S_IRUSR | S_IWUSR | S_IXUSR); + g_free(name); + } + + name = g_build_filename(dirname, size, sections[s].options[i].stockid, NULL); + scale = gdk_pixbuf_scale_simple(pixbuf, width, height, GDK_INTERP_BILINEAR); + gdk_pixbuf_save(scale, name, "png", &error, "compression", "9", NULL); + g_free(name); + g_object_unref(G_OBJECT(scale)); + if (error) + g_error_free(error); + } + } + } + return theme; +} + +static void +use_icon_theme(GtkWidget *w, GtkWidget *window) +{ + /* I don't quite understand the icon-theme stuff. For example, I don't + * know why PidginIconTheme needs to be abstract, or how PidginStatusIconTheme + * would be different from other PidginIconTheme's (e.g. PidginStockIconTheme) + * etc., but anyway, this works for now. + * + * Here's an interesting note: A PidginStatusIconTheme can be used for both + * stock and status icons. Like I said, I don't quite know how they could be + * different. So I am going to just keep it as it is, for now anyway, until I + * have the time to dig through this, or someone explains this stuff to me + * clearly. + * -- Sad + */ + PidginStatusIconTheme *theme = create_icon_theme(window); + pidgin_stock_load_status_icon_theme(PIDGIN_STATUS_ICON_THEME(theme)); + pidgin_stock_load_stock_icon_theme((PidginStockIconTheme *)theme); + pidgin_blist_refresh(purple_get_blist()); + g_object_unref(theme); +} + +#ifdef NOT_SADRUL +static void +save_icon_theme(GtkWidget *w, GtkWidget *window) +{ + /* TODO: SAVE! */ + gtk_widget_destroy(window); +} +#endif + +static void +close_icon_theme(GtkWidget *w, GtkWidget *window) +{ + gtk_widget_destroy(window); +} + +static void +stock_icon_selected(const char *filename, gpointer image) +{ + GError *error = NULL; + GdkPixbuf *scale; + int i; + GdkPixbuf *pixbuf; + + if (!filename) + return; + + pixbuf = gdk_pixbuf_new_from_file(filename, &error); + if (error || !pixbuf) { + purple_debug_error("theme-editor-icon", "Unable to load icon file '%s' (%s)\n", + filename, error ? error->message : "Reason unknown"); + if (error) + g_error_free(error); + return; + } + + scale = gdk_pixbuf_scale_simple(pixbuf, 16, 16, GDK_INTERP_BILINEAR); + gtk_image_set_from_pixbuf(GTK_IMAGE(image), scale); + g_object_unref(G_OBJECT(scale)); + + /* Update the size previews */ + for (i = 0; stocksizes[i]; i++) { + int width, height; + GtkIconSize iconsize; + GtkWidget *prev = g_object_get_data(G_OBJECT(image), stocksizes[i]); + if (!prev) + continue; + iconsize = gtk_icon_size_from_name(stocksizes[i]); + gtk_icon_size_lookup(iconsize, &width, &height); + scale = gdk_pixbuf_scale_simple(pixbuf, width, height, GDK_INTERP_BILINEAR); + gtk_image_set_from_pixbuf(GTK_IMAGE(prev), scale); + g_object_unref(G_OBJECT(scale)); + } + + /* Save the original pixbuf so we can use it for resizing later */ + g_object_set_data_full(G_OBJECT(image), "pixbuf", pixbuf, + (GDestroyNotify)g_object_unref); +} + +static gboolean +change_stock_image(GtkWidget *widget, GdkEventButton *event, GtkWidget *image) +{ + GtkWidget *win = pidgin_buddy_icon_chooser_new(GTK_WINDOW(gtk_widget_get_toplevel(widget)), + stock_icon_selected, image); + gtk_widget_show_all(win); + + return TRUE; +} + +void pidgin_icon_theme_edit(PurplePluginAction *unused) +{ + GtkWidget *dialog; + GtkWidget *box, *vbox; + GtkWidget *notebook; + GtkSizeGroup *sizegroup; + int s, i, j; + dialog = pidgin_create_dialog(_("Pidgin Icon Theme Editor"), 0, "theme-editor-icon", FALSE); + box = pidgin_dialog_get_vbox_with_properties(GTK_DIALOG(dialog), FALSE, PIDGIN_HIG_BOX_SPACE); + + notebook = gtk_notebook_new(); + gtk_box_pack_start(GTK_BOX(box), notebook, TRUE, TRUE, PIDGIN_HIG_BOX_SPACE); + sizegroup = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + + for (s = 0; sections[s].heading; s++) { + const char *heading = sections[s].heading; + + box = gtk_vbox_new(FALSE, 0); + gtk_notebook_append_page(GTK_NOTEBOOK(notebook), box, gtk_label_new(heading)); + + vbox = pidgin_make_frame(box, heading); + g_object_set_data(G_OBJECT(dialog), heading, vbox); + + for (i = 0; sections[s].options[i].stockid; i++) { + const char *id = sections[s].options[i].stockid; + const char *text = _(sections[s].options[i].text); + + GtkWidget *hbox = gtk_hbox_new(FALSE, PIDGIN_HIG_CAT_SPACE); + GtkWidget *label = gtk_label_new(text); + GtkWidget *image = gtk_image_new_from_stock(id, + gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL)); + GtkWidget *ebox = gtk_event_box_new(); + gtk_container_add(GTK_CONTAINER(ebox), image); + gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5); + + g_signal_connect(G_OBJECT(ebox), "button-press-event", G_CALLBACK(change_stock_image), image); + g_object_set_data(G_OBJECT(image), "property-name", (gpointer)id); + + gtk_size_group_add_widget(sizegroup, label); + gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(hbox), ebox, FALSE, FALSE, 0); + + for (j = 0; stocksizes[j]; j++) { + GtkWidget *sh; + + if (!(sections[s].flags & (1 << j))) + continue; + + sh = gtk_image_new_from_stock(id, gtk_icon_size_from_name(stocksizes[j])); + gtk_box_pack_start(GTK_BOX(hbox), sh, FALSE, FALSE, 0); + g_object_set_data(G_OBJECT(image), stocksizes[j], sh); + } + + gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0); + + g_object_set_data(G_OBJECT(vbox), id, image); + } + } + +#ifdef NOT_SADRUL + pidgin_dialog_add_button(GTK_DIALOG(dialog), GTK_STOCK_SAVE, G_CALLBACK(save_icon_theme), dialog); +#endif + pidgin_dialog_add_button(GTK_DIALOG(dialog), GTK_STOCK_APPLY, G_CALLBACK(use_icon_theme), dialog); + pidgin_dialog_add_button(GTK_DIALOG(dialog), GTK_STOCK_CLOSE, G_CALLBACK(close_icon_theme), dialog); + gtk_widget_show_all(dialog); + g_object_unref(sizegroup); +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pidgin/plugins/themeedit-icon.h Wed Apr 29 23:20:51 2009 +0000 @@ -0,0 +1,2 @@ +void pidgin_icon_theme_edit(PurplePluginAction *); +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pidgin/plugins/themeedit.c Wed Apr 29 23:20:51 2009 +0000 @@ -0,0 +1,362 @@ +/* Pidgin + * + * Pidgin is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA + */ +#include "internal.h" +#include "pidgin.h" +#include "version.h" + +#include "theme-manager.h" + +#include "gtkblist.h" +#include "gtkblist-theme.h" +#include "gtkutils.h" +#include "gtkplugin.h" + +#define PLUGIN_ID "gtk-theme-editor" + +#include "themeedit-icon.h" + +static gboolean +prop_type_is_color(PidginBlistTheme *theme, const char *prop) +{ + PidginBlistThemeClass *klass = PIDGIN_BLIST_THEME_GET_CLASS(theme); + GParamSpec *spec = g_object_class_find_property(G_OBJECT_CLASS(klass), prop); + + return G_IS_PARAM_SPEC_BOXED(spec); +} + +#ifdef NOT_SADRUL +static void +save_blist_theme(GtkWidget *w, GtkWidget *window) +{ + /* TODO: SAVE! */ + gtk_widget_destroy(window); +} +#endif + +static void +close_blist_theme(GtkWidget *w, GtkWidget *window) +{ + gtk_widget_destroy(window); +} + +static void +theme_color_selected(GtkDialog *dialog, gint response, const char *prop) +{ + if (response == GTK_RESPONSE_OK) { + GdkColor color; + PidginBlistTheme *theme; + + gtk_color_selection_get_current_color(GTK_COLOR_SELECTION(GTK_COLOR_SELECTION_DIALOG(dialog)->colorsel), &color); + + theme = pidgin_blist_get_theme(); + + if (prop_type_is_color(theme, prop)) { + g_object_set(G_OBJECT(theme), prop, &color, NULL); + } else { + PidginThemeFont *font = NULL; + g_object_get(G_OBJECT(theme), prop, &font, NULL); + if (!font) { + font = pidgin_theme_font_new(NULL, &color); + g_object_set(G_OBJECT(theme), prop, font, NULL); + pidgin_theme_font_free(font); + } else { + pidgin_theme_font_set_color(font, &color); + } + } + pidgin_blist_set_theme(theme); + } + + gtk_widget_destroy(GTK_WIDGET(dialog)); +} + +static void +theme_font_face_selected(GtkWidget *dialog, gint response, gpointer font) +{ + if (response == GTK_RESPONSE_OK || response == GTK_RESPONSE_APPLY) { + const char *fontname = gtk_font_selection_dialog_get_font_name(GTK_FONT_SELECTION_DIALOG(dialog)); + pidgin_theme_font_set_font_face(font, fontname); + pidgin_blist_refresh(purple_get_blist()); + } + gtk_widget_destroy(dialog); +} + +static void +theme_font_select_face(GtkWidget *widget, gpointer prop) +{ + GtkWidget *dialog; + PidginBlistTheme *theme; + PidginThemeFont *font = NULL; + const char *face; + + theme = pidgin_blist_get_theme(); + g_object_get(G_OBJECT(theme), prop, &font, NULL); + + if (!font) { + font = pidgin_theme_font_new(NULL, NULL); + g_object_set(G_OBJECT(theme), prop, font, NULL); + pidgin_theme_font_free(font); + g_object_get(G_OBJECT(theme), prop, &font, NULL); + } + + face = pidgin_theme_font_get_font_face(font); + dialog = gtk_font_selection_dialog_new(_("Select Font")); + if (face && *face) + gtk_font_selection_set_font_name(GTK_FONT_SELECTION(GTK_FONT_SELECTION_DIALOG(dialog)->fontsel), + face); + g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(theme_font_face_selected), + font); + gtk_widget_show_all(dialog); +} + +static void +theme_color_select(GtkWidget *widget, gpointer prop) +{ + GtkWidget *dialog; + PidginBlistTheme *theme; + const GdkColor *color = NULL; + + theme = pidgin_blist_get_theme(); + + if (prop_type_is_color(theme, prop)) { + g_object_get(G_OBJECT(theme), prop, &color, NULL); + } else { + PidginThemeFont *pair = NULL; + g_object_get(G_OBJECT(theme), prop, &pair, NULL); + if (pair) + color = pidgin_theme_font_get_color(pair); + } + + dialog = gtk_color_selection_dialog_new(_("Select Color")); + if (color) + gtk_color_selection_set_current_color(GTK_COLOR_SELECTION(GTK_COLOR_SELECTION_DIALOG(dialog)->colorsel), + color); + g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(theme_color_selected), + prop); + + gtk_widget_show_all(dialog); +} + +static GtkWidget * +pidgin_theme_create_color_selector(const char *text, const char *blurb, const char *prop, + GtkSizeGroup *sizegroup) +{ + GtkWidget *color; + GtkWidget *hbox, *label; + + hbox = gtk_hbox_new(FALSE, PIDGIN_HIG_CAT_SPACE); + + label = gtk_label_new(_(text)); + gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5); + gtk_size_group_add_widget(sizegroup, label); + gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0); +#if GTK_CHECK_VERSION(2, 12, 0) + gtk_widget_set_tooltip_text(label, blurb); +#endif + + color = pidgin_pixbuf_button_from_stock("", GTK_STOCK_SELECT_COLOR, + PIDGIN_BUTTON_HORIZONTAL); + g_signal_connect(G_OBJECT(color), "clicked", G_CALLBACK(theme_color_select), + (gpointer)prop); + gtk_box_pack_start(GTK_BOX(hbox), color, FALSE, FALSE, 0); + + return hbox; +} + +static GtkWidget * +pidgin_theme_create_font_selector(const char *text, const char *blurb, const char *prop, + GtkSizeGroup *sizegroup) +{ + GtkWidget *color, *font; + GtkWidget *hbox, *label; + + hbox = gtk_hbox_new(FALSE, PIDGIN_HIG_CAT_SPACE); + + label = gtk_label_new(_(text)); + gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5); + gtk_size_group_add_widget(sizegroup, label); + gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0); +#if GTK_CHECK_VERSION(2, 12, 0) + gtk_widget_set_tooltip_text(label, blurb); +#endif + + font = pidgin_pixbuf_button_from_stock("", GTK_STOCK_SELECT_FONT, + PIDGIN_BUTTON_HORIZONTAL); + g_signal_connect(G_OBJECT(font), "clicked", G_CALLBACK(theme_font_select_face), + (gpointer)prop); + gtk_box_pack_start(GTK_BOX(hbox), font, FALSE, FALSE, 0); + + color = pidgin_pixbuf_button_from_stock("", GTK_STOCK_SELECT_COLOR, + PIDGIN_BUTTON_HORIZONTAL); + g_signal_connect(G_OBJECT(color), "clicked", G_CALLBACK(theme_color_select), + (gpointer)prop); + gtk_box_pack_start(GTK_BOX(hbox), color, FALSE, FALSE, 0); + + return hbox; +} + +static void +pidgin_blist_theme_edit(PurplePluginAction *unused) +{ + GtkWidget *dialog; + GtkWidget *box; + GtkSizeGroup *group; + PidginBlistTheme *theme; + GObjectClass *klass; + int i, j; + static struct { + const char *header; + const char *props[12]; + } sections[] = { + {N_("Contact"), { + "contact-color", + "contact", + "online", + "away", + "offline", + "idle", + "message", + "message_nick_said", + "status", + NULL + } + }, + {N_("Group"), { + "expanded-color", + "expanded-text", + "collapsed-color", + "collapsed-text", + NULL + } + }, + { NULL, { } } + }; + + dialog = pidgin_create_dialog(_("Pidgin Buddylist Theme Editor"), 0, "theme-editor-blist", FALSE); + box = pidgin_dialog_get_vbox_with_properties(GTK_DIALOG(dialog), FALSE, PIDGIN_HIG_BOX_SPACE); + + theme = pidgin_blist_get_theme(); + if (!theme) { + theme = g_object_new(PIDGIN_TYPE_BLIST_THEME, "type", "blist", + "author", getlogin(), + NULL); + pidgin_blist_set_theme(theme); + } + klass = G_OBJECT_CLASS(PIDGIN_BLIST_THEME_GET_CLASS(theme)); + + group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + for (i = 0; sections[i].header; i++) { + GtkWidget *vbox; + GtkWidget *hbox; + GParamSpec *spec; + + vbox = pidgin_make_frame(box, _(sections[i].header)); + for (j = 0; sections[i].props[j]; j++) { + const char *label; + const char *blurb; + spec = g_object_class_find_property(klass, sections[i].props[j]); + label = g_param_spec_get_nick(spec); + blurb = g_param_spec_get_blurb(spec); + if (G_IS_PARAM_SPEC_BOXED(spec)) { + hbox = pidgin_theme_create_color_selector(label, blurb, + sections[i].props[j], group); + gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0); + } else { + hbox = pidgin_theme_create_font_selector(label, blurb, + sections[i].props[j], group); + gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0); + } + } + } + + gtk_dialog_set_has_separator(GTK_DIALOG(dialog), TRUE); +#ifdef NOT_SADRUL + pidgin_dialog_add_button(GTK_DIALOG(dialog), GTK_STOCK_SAVE, G_CALLBACK(save_blist_theme), dialog); +#endif + pidgin_dialog_add_button(GTK_DIALOG(dialog), GTK_STOCK_CLOSE, G_CALLBACK(close_blist_theme), dialog); + + gtk_widget_show_all(dialog); + + g_object_unref(group); +} + +static gboolean +plugin_load(PurplePlugin *plugin) +{ + return TRUE; +} + +static GList * +actions(PurplePlugin *plugin, gpointer context) +{ + GList *l = NULL; + PurplePluginAction *act = NULL; + + act = purple_plugin_action_new(_("Edit Buddylist Theme"), pidgin_blist_theme_edit); + l = g_list_append(l, act); + act = purple_plugin_action_new(_("Edit Icon Theme"), pidgin_icon_theme_edit); + l = g_list_append(l, act); + + return l; +} + +static PurplePluginInfo info = +{ + PURPLE_PLUGIN_MAGIC, + PURPLE_MAJOR_VERSION, + PURPLE_MINOR_VERSION, + PURPLE_PLUGIN_STANDARD, /**< type */ + PIDGIN_PLUGIN_TYPE, /**< ui_requirement */ + 0, /**< flags */ + NULL, /**< dependencies */ + PURPLE_PRIORITY_DEFAULT, /**< priority */ + + PLUGIN_ID, /**< id */ + N_("Pidgin Theme Editor"), /**< name */ + DISPLAY_VERSION, /**< version */ + /** summary */ + N_("Pidgin Theme Editor."), + /** description */ + N_("Pidgin Theme Editor"), + "Sadrul Habib Chowdhury <imadil@gmail.com>", /**< author */ + PURPLE_WEBSITE, /**< homepage */ + + plugin_load, /**< load */ + NULL, /**< unload */ + NULL, /**< destroy */ + + NULL, /**< ui_info */ + NULL, /**< extra_info */ + NULL, + actions, + + /* padding */ + NULL, + NULL, + NULL, + NULL +}; + +static void +init_plugin(PurplePlugin *plugin) +{ +} + +PURPLE_INIT_PLUGIN(themeeditor, init_plugin, info)
--- a/pidgin/plugins/ticker/gtkticker.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/plugins/ticker/gtkticker.c Wed Apr 29 23:20:51 2009 +0000 @@ -41,7 +41,7 @@ gboolean include_internals, GtkCallback callback, gpointer callback_data); -static GtkType gtk_ticker_child_type (GtkContainer *container); +static GType gtk_ticker_child_type (GtkContainer *container); static GtkContainerClass *parent_class = NULL; @@ -97,7 +97,7 @@ widget_class = (GtkWidgetClass*) class; container_class = (GtkContainerClass*) class; - parent_class = gtk_type_class (GTK_TYPE_CONTAINER); + parent_class = g_type_class_ref (GTK_TYPE_CONTAINER); gobject_class->finalize = gtk_ticker_finalize; @@ -112,7 +112,7 @@ container_class->child_type = gtk_ticker_child_type; } -static GtkType gtk_ticker_child_type (GtkContainer *container) +static GType gtk_ticker_child_type (GtkContainer *container) { return GTK_TYPE_WIDGET; }
--- a/pidgin/plugins/ticker/gtkticker.h Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/plugins/ticker/gtkticker.h Wed Apr 29 23:20:51 2009 +0000 @@ -33,11 +33,11 @@ extern "C" { #endif /* __cplusplus */ -#define GTK_TYPE_TICKER (gtk_ticker_get_type ()) -#define GTK_TICKER(obj) (GTK_CHECK_CAST ((obj), GTK_TYPE_TICKER, GtkTicker)) -#define GTK_TICKER_CLASS(klass) (GTK_CHECK_CLASS_CAST ((klass), GTK_TYPE_TICKER, GtkTickerClass)) -#define GTK_IS_TICKER(obj) (GTK_CHECK_TYPE ((obj), GTK_TYPE_TICKER)) -#define GTK_IS_TICKER_CLASS(klass) (GTK_CHECK_CLASS_TYPE ((klass), GTK_TYPE_TICKER)) +#define GTK_TYPE_TICKER (gtk_ticker_get_type()) +#define GTK_TICKER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GTK_TYPE_TICKER, GtkTicker)) +#define GTK_TICKER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GTK_TYPE_TICKER, GtkTickerClass)) +#define GTK_IS_TICKER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTK_TYPE_TICKER)) +#define GTK_IS_TICKER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GTK_TYPE_TICKER)) typedef struct _GtkTicker GtkTicker; @@ -72,7 +72,7 @@ }; -GtkType gtk_ticker_get_type (void); +GType gtk_ticker_get_type (void); GtkWidget* gtk_ticker_new (void); void gtk_ticker_add (GtkTicker *ticker, GtkWidget *widget);
--- a/pidgin/plugins/ticker/ticker.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/plugins/ticker/ticker.c Wed Apr 29 23:20:51 2009 +0000 @@ -37,6 +37,7 @@ #include "gtkblist.h" #include "gtkplugin.h" #include "gtkutils.h" +#include "pidginstock.h" #include "gtkticker.h" @@ -53,7 +54,7 @@ guint timeout; } TickerData; -GList *tickerbuds = NULL; +static GList *tickerbuds = NULL; static void buddy_ticker_update_contact(PurpleContact *contact); @@ -91,9 +92,10 @@ PurpleContact *contact = user_data; PurpleBuddy *b = purple_contact_get_priority_buddy(contact); - purple_conversation_new(PURPLE_CONV_TYPE_IM, - purple_buddy_get_account(b), - purple_buddy_get_name(b)); + PurpleConversation *conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, + purple_buddy_get_account(b), + purple_buddy_get_name(b)); + purple_conversation_present(conv); return TRUE; } @@ -107,20 +109,27 @@ return NULL; } -static void buddy_ticker_set_pixmap(PurpleContact *c) { +static void buddy_ticker_set_pixmap(PurpleContact *c) +{ TickerData *td = buddy_ticker_find_contact(c); - GdkPixbuf *pixbuf; + PurpleBuddy *buddy; + PurplePresence *presence; + const char *stock; if(!td) return; - if(!td->icon) + buddy = purple_contact_get_priority_buddy(c); + presence = purple_buddy_get_presence(buddy); + stock = pidgin_stock_id_from_presence(presence); + if(!td->icon) { td->icon = gtk_image_new(); - - pixbuf = pidgin_blist_get_status_icon((PurpleBlistNode*)c, - PIDGIN_STATUS_ICON_SMALL); - gtk_image_set_from_pixbuf(GTK_IMAGE(td->icon), pixbuf); - g_object_unref(G_OBJECT(pixbuf)); + g_object_set(G_OBJECT(td->icon), "stock", stock, + "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC), + NULL); + } else { + g_object_set(G_OBJECT(td->icon), "stock", stock, NULL); + } } static gboolean buddy_ticker_set_pixmap_cb(gpointer data) {
--- a/pidgin/plugins/win32/transparency/win2ktrans.c Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/plugins/win32/transparency/win2ktrans.c Wed Apr 29 23:20:51 2009 +0000 @@ -182,7 +182,7 @@ /* On slider val change, update window's transparency level */ g_signal_connect(GTK_OBJECT(slider), "value-changed", - GTK_SIGNAL_FUNC(change_alpha), win); + G_CALLBACK(change_alpha), win); gtk_box_pack_start(GTK_BOX(hbox), slider, FALSE, TRUE, 5); @@ -563,7 +563,7 @@ button = pidgin_prefs_checkbox(_("_IM window transparency"), OPT_WINTRANS_IM_ENABLED, imtransbox); g_signal_connect(GTK_OBJECT(button), "clicked", - GTK_SIGNAL_FUNC(update_convs_wintrans), + G_CALLBACK(update_convs_wintrans), (gpointer) OPT_WINTRANS_IM_ENABLED); trans_box = gtk_vbox_new(FALSE, 18); @@ -572,12 +572,12 @@ gtk_widget_show(trans_box); g_signal_connect(GTK_OBJECT(button), "clicked", - GTK_SIGNAL_FUNC(pidgin_toggle_sensitive), trans_box); + G_CALLBACK(pidgin_toggle_sensitive), trans_box); button = pidgin_prefs_checkbox(_("_Show slider bar in IM window"), OPT_WINTRANS_IM_SLIDER, trans_box); g_signal_connect(GTK_OBJECT(button), "clicked", - GTK_SIGNAL_FUNC(update_convs_wintrans), + G_CALLBACK(update_convs_wintrans), (gpointer) OPT_WINTRANS_IM_SLIDER); button = pidgin_prefs_checkbox( @@ -587,7 +587,7 @@ button = pidgin_prefs_checkbox(_("Always on top"), OPT_WINTRANS_IM_ONTOP, trans_box); g_signal_connect(GTK_OBJECT(button), "clicked", - GTK_SIGNAL_FUNC(update_convs_wintrans), + G_CALLBACK(update_convs_wintrans), (gpointer) OPT_WINTRANS_IM_ONTOP); gtk_box_pack_start(GTK_BOX(imtransbox), trans_box, FALSE, FALSE, 5); @@ -604,9 +604,9 @@ gtk_widget_set_usize(GTK_WIDGET(slider), 200, -1); g_signal_connect(GTK_OBJECT(slider), "value-changed", - GTK_SIGNAL_FUNC(alpha_change), NULL); + G_CALLBACK(alpha_change), NULL); g_signal_connect(GTK_OBJECT(slider), "focus-out-event", - GTK_SIGNAL_FUNC(alpha_pref_set_int), + G_CALLBACK(alpha_pref_set_int), (gpointer) OPT_WINTRANS_IM_ALPHA); gtk_box_pack_start(GTK_BOX(hbox), slider, FALSE, TRUE, 5); @@ -620,7 +620,7 @@ button = pidgin_prefs_checkbox(_("_Buddy List window transparency"), OPT_WINTRANS_BL_ENABLED, bltransbox); g_signal_connect(GTK_OBJECT(button), "clicked", - GTK_SIGNAL_FUNC(set_blist_trans), + G_CALLBACK(set_blist_trans), (gpointer) OPT_WINTRANS_BL_ENABLED); trans_box = gtk_vbox_new(FALSE, 18); @@ -628,14 +628,14 @@ gtk_widget_set_sensitive(GTK_WIDGET(trans_box), FALSE); gtk_widget_show(trans_box); g_signal_connect(GTK_OBJECT(button), "clicked", - GTK_SIGNAL_FUNC(pidgin_toggle_sensitive), trans_box); + G_CALLBACK(pidgin_toggle_sensitive), trans_box); button = pidgin_prefs_checkbox( _("Remove Buddy List window transparency on focus"), OPT_WINTRANS_BL_ONFOCUS, trans_box); button = pidgin_prefs_checkbox(_("Always on top"), OPT_WINTRANS_BL_ONTOP, trans_box); g_signal_connect(GTK_OBJECT(button), "clicked", - GTK_SIGNAL_FUNC(set_blist_trans), + G_CALLBACK(set_blist_trans), (gpointer) OPT_WINTRANS_BL_ONTOP); gtk_box_pack_start(GTK_BOX(bltransbox), trans_box, FALSE, FALSE, 5); @@ -652,9 +652,9 @@ gtk_widget_set_usize(GTK_WIDGET(slider), 200, -1); g_signal_connect(GTK_OBJECT(slider), "value-changed", - GTK_SIGNAL_FUNC(bl_alpha_change), NULL); + G_CALLBACK(bl_alpha_change), NULL); g_signal_connect(GTK_OBJECT(slider), "focus-out-event", - GTK_SIGNAL_FUNC(alpha_pref_set_int), + G_CALLBACK(alpha_pref_set_int), (gpointer) OPT_WINTRANS_BL_ALPHA); gtk_box_pack_start(GTK_BOX(hbox), slider, FALSE, TRUE, 5);
--- a/pidgin/win32/nsis/pidgin-installer.nsi Sat Apr 18 06:52:59 2009 +0000 +++ b/pidgin/win32/nsis/pidgin-installer.nsi Wed Apr 29 23:20:51 2009 +0000 @@ -813,6 +813,7 @@ ; Shortcuts.. Delete "$DESKTOP\Pidgin.lnk" + Delete "$SMPROGRAMS\Pidgin.lnk" Goto done
--- a/po/POTFILES.in Sat Apr 18 06:52:59 2009 +0000 +++ b/po/POTFILES.in Wed Apr 29 23:20:51 2009 +0000 @@ -86,6 +86,7 @@ libpurple/protocols/irc/parse.c libpurple/protocols/jabber/adhoccommands.c libpurple/protocols/jabber/auth.c +libpurple/protocols/jabber/bosh.c libpurple/protocols/jabber/buddy.c libpurple/protocols/jabber/chat.c libpurple/protocols/jabber/jabber.c
--- a/po/de.po Sat Apr 18 06:52:59 2009 +0000 +++ b/po/de.po Wed Apr 29 23:20:51 2009 +0000 @@ -11,10 +11,10 @@ msgstr "" "Project-Id-Version: de\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-04-16 16:28+0200\n" -"PO-Revision-Date: 2009-04-16 16:27+0200\n" -"Last-Translator: Björn Voigt <bjoern@cs.tu-berlin.de>\n" -"Language-Team: German <de@li.org>\n" +"POT-Creation-Date: 2009-04-26 12:11+0200\n" +"PO-Revision-Date: 2009-04-26 12:11+0200\n" +"Last-Translator: Jochen Kemnade <jochenkemnade@web.de>\n" +"Language-Team: Deutsch <de@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -902,6 +902,8 @@ #, c-format msgid "%s is trying to start an unsupported media session type with you." msgstr "" +"%s versucht, einen nicht unterstützten Typ von Medien-Sitzung mit Ihnen zu " +"starten." msgid "You have rejected the call." msgstr "Sie haben den Anruf abgelehnt." @@ -1564,22 +1566,25 @@ "\n" "Fetching TinyURL..." msgstr "" +"\n" +"Hole TinyURL..." msgid "Only create TinyURL for urls of this length or greater" -msgstr "" +msgstr "TinyURL nur für URLs mit mindestens dieser Länge generieren" msgid "TinyURL (or other) address prefix" msgstr "" -#, fuzzy msgid "TinyURL" -msgstr "URL anpassen" +msgstr "TinyURL" msgid "TinyURL plugin" -msgstr "" +msgstr "TinyURL-Plugin" msgid "When receiving a message with URL(s), TinyURL for easier copying" msgstr "" +"URLs aus erhaltenen Nachrichten zum einfacheren Kopieren in TinyURLs " +"umwandeln" msgid "accounts" msgstr "Konten" @@ -1829,9 +1834,8 @@ msgid "%s left the room (%s)." msgstr "%s hat den Raum verlassen (%s)." -#, fuzzy msgid "Invite to chat" -msgstr "Zur Konferenz einladen" +msgstr "Zum Chat einladen" #. Put our happy label in it. msgid "" @@ -3105,6 +3109,7 @@ msgid "Add to chat..." msgstr "Zum Chat hinzufügen..." +#. Global msgid "Available" msgstr "Verfügbar" @@ -3451,6 +3456,17 @@ "Ihr gewählter Kontoname wurde vom Server abgelehnt. Er enthält vermutlich " "ungültige Zeichen." +#. We only want to do the following dance if the connection +#. has not been successfully completed. If it has, just +#. notify the user that their /nick command didn't go. +#, c-format +msgid "The nickname \"%s\" is already being used." +msgstr "Der Spitzname \"%s\" existiert bereits." + +#, fuzzy +msgid "Nickname in use" +msgstr "Spitzname" + msgid "Cannot change nick" msgstr "Kann den Spitznamen nicht ändern" @@ -3797,9 +3813,8 @@ msgid "Operating System" msgstr "Betriebssystem" -#, fuzzy msgid "Local Time" -msgstr "Lokale Datei:" +msgstr "Lokale Zeit" msgid "Last Activity" msgstr "Letzte Aktivität" @@ -4546,36 +4561,38 @@ msgid "%s has buzzed you!" msgstr "%s hat bei Ihnen angeklopft!" -#, fuzzy, c-format +#, c-format msgid "Unable to initiate media with %s: invalid JID" -msgstr "Kann die Nachricht an %s nicht senden, ungültige JID" - -#, fuzzy, c-format +msgstr "Medien-Sitzung mit %s konnte nicht gestartet werden: ungültige JID" + +#, c-format msgid "Unable to initiate media with %s: user is not online" -msgstr "Kann die Datei nicht an %s senden, Benutzer ist nicht online" - -#, fuzzy, c-format +msgstr "" +"Medien-Sitzung mit %s konnte nicht gestartet werden: Benutzer ist nicht " +"online" + +#, c-format msgid "Unable to initiate media with %s: not subscribed to user presence" msgstr "" -"Kann die Datei nicht an %s senden, Anwesenheit des Benutzers nicht abonniert" - -#, fuzzy +"Medien-Sitzung mit %s konnte nicht gestartet werden: Anwesenheit des " +"Benutzers nicht abonniert" + msgid "Media Initiation Failed" -msgstr "Registrierung fehlgeschlagen" - -#, fuzzy, c-format +msgstr "Medien-Initiierung fehlgeschlagen" + +#, c-format msgid "" "Please select the resource of %s with which you would like to start a media " "session." msgstr "" -"Bitte wählen Sie die Ressource von %s, an die Sie eine Datei schicken möchten" +"Bitte wählen Sie die Ressource von %s, mir der Sie eine Medien-Sitzung " +"starten möchten" msgid "Select a Resource" msgstr "Wählen Sie eine Ressource" -#, fuzzy msgid "Initiate Media" -msgstr "Initiiere _Chat" +msgstr "Initiiere Medien" msgid "config: Configure a chat room." msgstr "config: Konfiguriere einen Chatraum." @@ -7392,9 +7409,8 @@ msgid "_Modify" msgstr "_Bearbeiten" -#, fuzzy msgid "Memo Modify" -msgstr "Bearbeiten" +msgstr "Memo bearbeiten" msgid "Server says:" msgstr "Server meldet:" @@ -10939,21 +10955,17 @@ msgid "/Conversation/Clea_r Scrollback" msgstr "/Unterhaltung/_Leeren" -#, fuzzy msgid "/Conversation/M_edia" -msgstr "/Unterhaltung/Me_hr" - -#, fuzzy +msgstr "/Unterhaltung/M_edien" + msgid "/Conversation/Media/_Audio Call" -msgstr "/Unterhaltung/Me_hr" - -#, fuzzy +msgstr "/Unterhaltung/Medien/_Audio-Anruf" + msgid "/Conversation/Media/_Video Call" -msgstr "/Unterhaltung/Me_hr" - -#, fuzzy +msgstr "/Unterhaltung/Medien/_Video-Anruf" + msgid "/Conversation/Media/Audio\\/Video _Call" -msgstr "/Unterhaltung/Betrachte _Mitschnitt" +msgstr "/Unterhaltung/Medien/A_udio-\\/Video-Anruf" msgid "/Conversation/Se_nd File..." msgstr "/Unterhaltung/Datei _senden..." @@ -11027,17 +11039,14 @@ msgid "/Conversation/View Log" msgstr "/Unterhaltung/Betrachte Mitschnitt" -#, fuzzy msgid "/Conversation/Media/Audio Call" -msgstr "/Unterhaltung/Mehr" - -#, fuzzy +msgstr "/Unterhaltung/Medien/Audio-Anruf" + msgid "/Conversation/Media/Video Call" -msgstr "/Unterhaltung/Betrachte Mitschnitt" - -#, fuzzy +msgstr "/Unterhaltung/Medien/Video-Anruf" + msgid "/Conversation/Media/Audio\\/Video Call" -msgstr "/Unterhaltung/Mehr" +msgstr "/Unterhaltung/Medien/Audio-\\/Video-Anruf" msgid "/Conversation/Send File..." msgstr "/Unterhaltung/Datei senden ..." @@ -12194,11 +12203,11 @@ #, c-format msgid "%s wishes to start an audio/video session with you." -msgstr "" +msgstr "%s möchte eine Audio-/Video-Sitzung mit Ihnen starten." #, c-format msgid "%s wishes to start a video session with you." -msgstr "" +msgstr "%s möchte eine Video-Sitzung mit Ihnen starten." #, c-format msgid "%s has %d new message." @@ -12949,16 +12958,14 @@ msgstr "_Bild:" #. Shortcut text -#, fuzzy msgid "S_hortcut text:" -msgstr "Tastenkombination" +msgstr "_Verknüpfter Text:" msgid "Smiley" msgstr "Smiley" -#, fuzzy msgid "Shortcut Text" -msgstr "Tastenkombination" +msgstr "Verknüpfter Text" msgid "Custom Smiley Manager" msgstr "Verwaltung für benutzerdefinierte Smileys"
--- a/po/fi.po Sat Apr 18 06:52:59 2009 +0000 +++ b/po/fi.po Wed Apr 29 23:20:51 2009 +0000 @@ -1,7 +1,7 @@ # Pidgin Finnish translation # Copyright (C) 2002 Tero Kuusela <teroajk@subdimension.com> # Copyright (C) 2003-2005 Arto Alakulju <arto@alakulju.net> -# Copyright (C) 2005-2008 Timo Jyrinki <timo.jyrinki@iki.fi> +# Copyright (C) 2005-2009 Timo Jyrinki <timo.jyrinki@iki.fi> # # This file is distributed under the same license as the Pidgin package. # @@ -10,8 +10,8 @@ msgstr "" "Project-Id-Version: Pidgin\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-01-07 13:30+0200\n" -"PO-Revision-Date: 2009-01-23 19:58+0200\n" +"POT-Creation-Date: 2009-04-28 16:32+0300\n" +"PO-Revision-Date: 2009-04-28 16:31+0300\n" "Last-Translator: Timo Jyrinki <timo.jyrinki@iki.fi>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -34,7 +34,7 @@ "Usage: %s [OPTION]...\n" "\n" " -c, --config=DIR use DIR for config files\n" -" -d, --debug print debugging messages to stdout\n" +" -d, --debug print debugging messages to stderr\n" " -h, --help display this help and exit\n" " -n, --nologin don't automatically login\n" " -v, --version display the current version and exit\n" @@ -43,7 +43,7 @@ "Käyttö: %s [VALITSIN]...\n" "\n" " -c, --config=HAK käytä hakemistoa HAK asetustiedostoille\n" -" -d, --debug kirjoita virheenjäljitysviestit putkeen stdout\n" +" -d, --debug kirjoita virheenjäljitysviestit putkeen stderr\n" " -h, --help näytä tämä ohje ja poistu\n" " -n, --nologin älä kirjaudu automaattisesti\n" " -v, --version näytä nykyinen versionumero ja poistu\n" @@ -608,19 +608,6 @@ msgid "Send To" msgstr "Lähetä käyttäjälle" -msgid "Invite message" -msgstr "Kutsuviesti" - -msgid "Invite" -msgstr "Kutsu" - -msgid "" -"Please enter the name of the user you wish to invite,\n" -"along with an optional invite message." -msgstr "" -"Anna kutsuttavan käyttäjän nimi sekä vapaaehtoinen \n" -"kutsuviesti." - msgid "Conversation" msgstr "Keskustelu" @@ -880,6 +867,40 @@ msgid "System Log" msgstr "Järjestelmäloki" +msgid "Calling ... " +msgstr "Soitetaan..." + +msgid "Hangup" +msgstr "Katkaise" + +#. Number of actions +msgid "Accept" +msgstr "Hyväksy" + +msgid "Reject" +msgstr "Kieltäydy" + +msgid "Call in progress." +msgstr "Puhelu käynnissä." + +msgid "The call has been terminated." +msgstr "Puhelu on päättynyt." + +#, c-format +msgid "%s wishes to start an audio session with you." +msgstr "%s haluaa aloittaa ääni-istunnon kanssasi." + +#, c-format +msgid "%s is trying to start an unsupported media session type with you." +msgstr "" +"%s yrittää aloittaa kanssasi mediaistuntoa, jonka tyyppi ei ole tuettu." + +msgid "You have rejected the call." +msgstr "Olet hylännyt puhelun." + +msgid "call: Make an audio call." +msgstr "call: Tee äänipuhelu." + msgid "Emails" msgstr "Sähköpostit" @@ -914,6 +935,9 @@ msgid "IM" msgstr "Pikaviesti" +msgid "Invite" +msgstr "Kutsu" + msgid "(none)" msgstr "(ei mitään)" @@ -1118,7 +1142,6 @@ msgid "%s has sent you a message. (%s)" msgstr "%s on lähettämässä sinulle viestiä. (%s)" -#, c-format msgid "Unknown pounce event. Please report this!" msgstr "Tuntematon ilmoitinviesti. Raportoi tästä!" @@ -1164,7 +1187,6 @@ msgid "Change status to" msgstr "Vaihda tila seuraavaksi" -#. Conversations msgid "Conversations" msgstr "Keskustelut" @@ -1529,6 +1551,31 @@ msgid "Lastlog plugin." msgstr "Lastlog-liitännäinen." +#, c-format +msgid "" +"\n" +"Fetching TinyURL..." +msgstr "" +"\n" +"Noudetaan TinyURL-osoitetta..." + +msgid "Only create TinyURL for urls of this length or greater" +msgstr "Luo TinyURL vain tämän pituisille tai pidemmille osoitteille" + +msgid "TinyURL (or other) address prefix" +msgstr "TinyURL:n (tai muun) osoite-etuliite" + +msgid "TinyURL" +msgstr "TinyURL" + +msgid "TinyURL plugin" +msgstr "TinyURL-liitännäinen" + +msgid "When receiving a message with URL(s), TinyURL for easier copying" +msgstr "" +"Vastaanotettaessa viestiä jossa on URL-osoitteita, käytä TinyURL-palvelua " +"helpompaa kopiomista varten" + msgid "accounts" msgstr "käyttäjätilit" @@ -1629,13 +1676,6 @@ msgid "SSL Certificate Verification" msgstr "SSL-varmenteen tarkistus" -#. Number of actions -msgid "Accept" -msgstr "Hyväksy" - -msgid "Reject" -msgstr "Kieltäydy" - msgid "_View Certificate..." msgstr "_Näytä varmenne..." @@ -1781,6 +1821,15 @@ msgid "%s left the room (%s)." msgstr "%s poistui huoneesta (%s)." +msgid "Invite to chat" +msgstr "Kutsu keskusteluun" + +#. Put our happy label in it. +msgid "" +"Please enter the name of the user you wish to invite, along with an optional " +"invite message." +msgstr "Anna kutsuttavan käyttäjän nimi sekä vapaaehtoinen viesti." + #, c-format msgid "Failed to get connection: %s" msgstr "Yhteyden saaminen epäonnistui: %s" @@ -2617,6 +2666,31 @@ msgid "Do not ask. Always save in pounce." msgstr "Älä kysy. Tallenna aina ilmoittimeen." +msgid "One Time Password" +msgstr "Kertasalasana" + +#. *< type +#. *< ui_requirement +#. *< flags +#. *< dependencies +#. *< priority +#. *< id +msgid "One Time Password Support" +msgstr "Kertasalasanan tuki" + +#. *< name +#. *< version +#. * summary +msgid "Enforce that passwords are used only once." +msgstr "Pakota käyttöön salasanat, joita käytetään vain kerran." + +#. * description +msgid "" +"Allows you to enforce on a per-account basis that passwords not being saved " +"are only used in a single successful connection.\n" +"Note: The account password must not be saved for this to work." +msgstr "" + #. *< type #. *< ui_requirement #. *< flags @@ -2826,7 +2900,6 @@ msgstr "" "Paikalliseen mDNS-palvelimeen ei voi luoda yhteyttä. Onko se käynnissä?" -#. Creating the options for the protocol msgid "First name" msgstr "Etunimi" @@ -2858,6 +2931,10 @@ msgid "Purple Person" msgstr "Purple-henkilö" +#. Creating the options for the protocol +msgid "Local Port" +msgstr "Paikallinen portti" + msgid "Bonjour" msgstr "Bonjour" @@ -3014,6 +3091,7 @@ msgid "Add to chat..." msgstr "Lisää ryhmäkeskusteluun..." +#. Global msgid "Available" msgstr "Tavoitettavissa" @@ -3360,6 +3438,16 @@ "Palvelin hylkäsi valitsemasi tilinimen. Siinä on todennäköisesti kiellettyjä " "merkkejä." +#. We only want to do the following dance if the connection +#. has not been successfully completed. If it has, just +#. notify the user that their /nick command didn't go. +#, c-format +msgid "The nickname \"%s\" is already being used." +msgstr "Kutsumanimi ”%s” on jo käytössä." + +msgid "Nickname in use" +msgstr "Kutsumanimi on käytössä" + msgid "Cannot change nick" msgstr "Ei kyetty muuttamaan kutsumanimeä" @@ -3695,6 +3783,9 @@ msgid "Operating System" msgstr "Käyttöjärjestelmä" +msgid "Local Time" +msgstr "Paikallinen aika" + msgid "Last Activity" msgstr "Viimeisin aktiivisena olo" @@ -4139,6 +4230,12 @@ msgid "Not Authorized" msgstr "Ei valtuuksia" +msgid "Mood" +msgstr "Mieliala" + +msgid "Now Listening" +msgstr "Kuuntelee nyt" + msgid "Both" msgstr "molemmille" @@ -4160,12 +4257,6 @@ msgid "Subscription" msgstr "Tilailmoitus" -msgid "Mood" -msgstr "Mieliala" - -msgid "Now Listening" -msgstr "Kuuntelee nyt" - msgid "Mood Text" msgstr "Mielialan teksti" @@ -4404,17 +4495,25 @@ msgstr "Käyttäjää %s ei voi pingata." #, c-format -msgid "Unable to buzz, because there is nothing known about user %s." +msgid "Unable to buzz, because there is nothing known about %s." msgstr "Äänimerkkiä ei voi lähettää, koska mitään ei tiedetä käyttäjästä %s." #, c-format -msgid "Unable to buzz, because user %s might be offline." +msgid "Unable to buzz, because %s might be offline." msgstr "" "Äänimerkkiä ei voi lähettää, koska käyttäjä %s voi olla poissa linjoilta." #, c-format -msgid "Unable to buzz, because the user %s does not support it." -msgstr "Äänimerkkiä ei voi lähettää, koska käyttäjä %s ei tue sitä." +msgid "" +"Unable to buzz, because %s does not support it or does not wish to receive " +"buzzes now." +msgstr "" +"Äänimerkkiä ei voi lähettää, koska käyttäjä %s ei tue sitä tai ei halua " +"vastaanottaa äänimerkkejä tällä hetkellä." + +#, c-format +msgid "Buzzing %s..." +msgstr "Töötätään tuttavalle %s..." #. Yahoo only supports one attention command: the 'buzz'. #. This is index number YAHOO_BUZZ. @@ -4426,8 +4525,33 @@ msgstr "%s on töötännyt sinulle." #, c-format -msgid "Buzzing %s..." -msgstr "Töötätään tuttavalle %s..." +msgid "Unable to initiate media with %s: invalid JID" +msgstr "Mediaa ei voi alustaa käyttäjän %s kanssa: virheellinen JID." + +#, c-format +msgid "Unable to initiate media with %s: user is not online" +msgstr "Mediaa ei voi alustaa käyttäjän %s kanssa: käyttäjä ei ole linjoilla" + +#, c-format +msgid "Unable to initiate media with %s: not subscribed to user presence" +msgstr "" +"Mediaa ei voi alustaa käyttäjän %s kanssa: käyttäjän läsnäolotilaa ei ole " +"tilattu" + +msgid "Media Initiation Failed" +msgstr "Median alustus epäonnistui" + +#, c-format +msgid "" +"Please select the resource of %s with which you would like to start a media " +"session." +msgstr "Valitse käyttäjän %s sijainti jonne haluat aloittaa mediaistunnon." + +msgid "Select a Resource" +msgstr "Valitse sijainti" + +msgid "Initiate Media" +msgstr "Aloita median käyttö" msgid "config: Configure a chat room." msgstr "config: Konfiguroi ryhmäkeskusteluhuone." @@ -4582,6 +4706,18 @@ msgid "Error in chat %s" msgstr "Virhe ryhmäkeskustelussa: %s" +msgid "An error occured on the in-band bytestream transfer\n" +msgstr "Kaistansisäisessä tavuvirtasiirrossa tapahtui virhe\n" + +msgid "Transfer was closed." +msgstr "Siirto suljettiin." + +msgid "Failed to open the file" +msgstr "Tiedoston avaaminen epäonnistui" + +msgid "Failed to open in-band bytestream" +msgstr "Kaistansisäisen tavuvirran avaaminen epäonnistui" + #, c-format msgid "Unable to send file to %s, user does not support file transfers" msgstr "" @@ -4607,9 +4743,6 @@ msgid "Please select the resource of %s to which you would like to send a file" msgstr "Valitse käyttäjän %s sijainti johon haluat lähettää tiedoston" -msgid "Select a Resource" -msgstr "Valitse sijainti" - msgid "Edit User Mood" msgstr "Muuta käyttäjän mielialaa" @@ -4644,9 +4777,6 @@ msgid "Select an action" msgstr "Valitse toiminto" -msgid "Unable to retrieve MSN Address Book" -msgstr "MSN-osoitekirjaa ei onnistuttu noutamaan" - #. only notify the user about problems adding to the friends list #. * maybe we should do something else for other lists, but it probably #. * won't cause too many problems if we just ignore it @@ -6415,7 +6545,7 @@ "kirjaimella ja sisältää vain kirjaimia, numeroita ja välilyöntejä, tai " "sisältää vain numeroita." -#. Unregistered screen name +#. Unregistered username #. uid is not exist msgid "Invalid username." msgstr "Epäkelpo käyttäjänimi." @@ -6431,7 +6561,7 @@ msgid "The AOL Instant Messenger service is temporarily unavailable." msgstr "AOL-pikaviestipalvelu ei tilapäisesti ole käytössä." -#. screen name connecting too frequently +#. username connecting too frequently #. IP address connecting too frequently msgid "" "You have been connecting and disconnecting too frequently. Wait ten minutes " @@ -6611,7 +6741,7 @@ msgstr[0] "Et saanut %hu viestiä %s:lta tuntemattomasta syystä." msgstr[1] "Et saanut %hu viestiä %s:lta tuntemattomasta syystä." -#. Data is assumed to be the destination sn +#. Data is assumed to be the destination bn #, c-format msgid "Unable to send message: %s" msgstr "Viestiä ei voi lähettää: %s" @@ -6923,6 +7053,7 @@ msgid "Get AIM Info" msgstr "Hae AIM-tiedot" +#. We only do this if the user is in our buddy list msgid "Edit Buddy Comment" msgstr "Muokkaa kommenttia" @@ -7203,6 +7334,34 @@ msgid "Could not change buddy information." msgstr "Tuttavan tietojen muuttaminen ei onnistunut." +msgid "Mobile" +msgstr "Liikkeellä" + +msgid "Note" +msgstr "Huomautus" + +#. callback +msgid "Buddy Memo" +msgstr "Tuttavamuistio" + +msgid "Change his/her memo as you like" +msgstr "Muuta henkilön muistiota halutulla tavalla" + +msgid "_Modify" +msgstr "_Muokkaa" + +msgid "Memo Modify" +msgstr "Muistion muokkaaminen" + +msgid "Server says:" +msgstr "Palvelin sanoo:" + +msgid "Your request was accepted." +msgstr "Pyyntösi hyväksyttiin." + +msgid "Your request was rejected." +msgstr "Pyyntösi hylättiin." + #, c-format msgid "%u requires verification" msgstr "%u pyytää valtuutusta" @@ -7511,6 +7670,12 @@ msgid "<p><b>Acknowledgement</b>:<br>\n" msgstr "<p><b>Tunnustus</b>:<br>\n" +msgid "<p><b>Scrupulous Testers</b>:<br>\n" +msgstr "<p><b>Tunnolliset testaajat</b>:<br>\n" + +msgid "and more, please let me know... thank you!))" +msgstr "ja muuta, kerro minulle... kiitos!))" + msgid "<p><i>And, all the boys in the backroom...</i><br>\n" msgstr "<p><i>Ja, kaikki pojat takahuoneessa...</i><br>\n" @@ -7536,6 +7701,9 @@ msgid "About OpenQ" msgstr "Tietoja OpenQ:sta" +msgid "Modify Buddy Memo" +msgstr "Muokkaa tuttavan muistiota" + #. *< type #. *< ui_requirement #. *< flags @@ -7573,6 +7741,9 @@ msgid "Show server news" msgstr "Näytä palvelinuutiset" +msgid "Show chat room when msg comes" +msgstr "Näytä keskusteluhuone viestin tullessa" + msgid "Keep alive interval (seconds)" msgstr "Jatkuvan yhteydenpidon aikaväli (sekunneissa)" @@ -7640,7 +7811,6 @@ "Tuntematon vastauskoosi kirjauduttaessa sisään (0x%02X):\n" "%s" -#. we didn't successfully connect. tdt->toc_fd is valid here msgid "Unable to connect." msgstr "Yhteyden muodostaminen epäonnistui." @@ -8542,9 +8712,6 @@ msgid "Unit" msgstr "Yksikkö" -msgid "Note" -msgstr "Huomautus" - msgid "Join Chat" msgstr "Liity ryhmäkeskusteluun" @@ -9232,194 +9399,12 @@ msgstr "Todennus/verkkoalue" #, c-format -msgid "Looking up %s" -msgstr "Etsitään %s" - -#, c-format -msgid "Connect to %s failed" -msgstr "%s: yhteyden muodostaminen epäonnistui" - -#, c-format -msgid "Signon: %s" -msgstr "Kirjautuminen: %s" - -#, c-format -msgid "Unable to write file %s." -msgstr "Ei kyetty kirjoittamaan tiedostoa %s." - -#, c-format -msgid "Unable to read file %s." -msgstr "Ei kyetty lukemaan tiedostoa %s." - -#, c-format -msgid "Message too long, last %s bytes truncated." -msgstr "Viesti on liian pitkä, viimeiset %s tavua katkaistu." - -#, c-format -msgid "%s not currently logged in." -msgstr "%s ei ole parhaillaan kirjautuneena sisään." - -#, c-format -msgid "Warning of %s not allowed." -msgstr "%s:n varoittaminen ei ole sallittua." - -#, c-format -msgid "A message has been dropped, you are exceeding the server speed limit." -msgstr "Viesti on hylätty, ylität palvelimen nopeusrajan." - -#, c-format -msgid "Chat in %s is not available." -msgstr "Ryhmäkeskustelu %s ei ole käytettävissä." - -#, c-format -msgid "You are sending messages too fast to %s." -msgstr "Lähetät viestejä %s:lle liian nopeasti." - -#, c-format -msgid "You missed an IM from %s because it was too big." -msgstr "Et saanut %s:n pikaviestiä koska se oli liian suuri." - -#, c-format -msgid "You missed an IM from %s because it was sent too fast." -msgstr "Et saanut %s:n pikaviestiä koska se lähetettiin liian nopeasti." - -#, c-format -msgid "Failure." -msgstr "Epäonnistuminen." - -#, c-format -msgid "Too many matches." -msgstr "Liian monta tulosta." - -#, c-format -msgid "Need more qualifiers." -msgstr "Tarvitaan lisää määritteitä." - -#, c-format -msgid "Dir service temporarily unavailable." -msgstr "Hakemistopalvelu ei tilapäisesti ole käytettävissä." - -#, c-format -msgid "Email lookup restricted." -msgstr "Sähköpostin katsominen rajoitettu." - -#, c-format -msgid "Keyword ignored." -msgstr "Avainsanasta ei välitetty." - -#, c-format -msgid "No keywords." -msgstr "Ei avainsanoja." - -#, c-format -msgid "User has no directory information." -msgstr "Käyttäjällä ei ole hakemistotietoja." - -#, c-format -msgid "Country not supported." -msgstr "Maa ei tuettu." - -#, c-format -msgid "Failure unknown: %s." -msgstr "Tunnistamaton epäonnistuminen: %s." - -#, c-format -msgid "Incorrect username or password." -msgstr "Virheellinen käyttäjänimi tai salasana." - -#, c-format -msgid "The service is temporarily unavailable." -msgstr "Palvelu ei tilapäisesti ole käytössä." - -#, c-format -msgid "Your warning level is currently too high to log in." -msgstr "Varoitustasosi on parhaillaan liian korkea kirjautuaksesi sisään." - -#, c-format -msgid "" -"You have been connecting and disconnecting too frequently. Wait ten minutes " -"and try again. If you continue to try, you will need to wait even longer." -msgstr "" -"Olet ottanut ja katkaissut yhteyden liian tiheään. Odota kymmenen minuuttia " -"ja yritä uudestaan. Jos jatkat yrittämistä, joudut odottamaan vielä " -"pidempään." - -#, c-format -msgid "An unknown signon error has occurred: %s." -msgstr "Tuntematon sisäänkirjautumisvirhe esiintyi: %s." - -#, c-format -msgid "An unknown error, %d, has occurred. Info: %s" -msgstr "Tuntematon virhe, %d, esiintyi. Tiedot: %s" - -msgid "Invalid Groupname" -msgstr "Epäkelpo ryhmän nimi" - -msgid "Connection Closed" -msgstr "Yhteys suljettu" - -msgid "Waiting for reply..." -msgstr "Odotetaan vastausta..." - -msgid "TOC has come back from its pause. You may now send messages again." -msgstr "TOC on palannut tauoltaan. Voit lähettää viestejä jälleen." - -msgid "Password Change Successful" -msgstr "Salasanan vaihto onnistui" - -msgid "_Group:" -msgstr "_Ryhmä:" - -msgid "Get Dir Info" -msgstr "Hae hakemistotiedot" - -msgid "Set Dir Info" -msgstr "Aseta hakemistotiedot" - -#, c-format -msgid "Could not open %s for writing!" -msgstr "%s:n avaaminen kirjoitusta varten epäonnistui!" - -msgid "File transfer failed; other side probably canceled." -msgstr "" -"Tiedostonsiirto epäonnistui. Toinen osapuoli luultavasti katkaisi siirron." - -msgid "Could not connect for transfer." -msgstr "Yhteyttä siirtoa varten ei voi muodostaa." - -msgid "Could not write file header. The file will not be transferred." -msgstr "Tiedosto-otsikkoa ei voi kirjoittaa. Tiedostoa ei siirretä." - -msgid "Save As..." -msgstr "Tallenna nimellä..." - -#, c-format -msgid "%s requests %s to accept %d file: %s (%.2f %s)%s%s" -msgid_plural "%s requests %s to accept %d files: %s (%.2f %s)%s%s" -msgstr[0] "%s pyytää %s hyväksymään %d tiedoston: %s (%.2f %s)%s%s" -msgstr[1] "%s pyytää %s hyväksymään %d tiedostot: %s (%.2f %s)%s%s" - -#, c-format -msgid "%s requests you to send them a file" -msgstr "%s pyytää sinua lähettämään hänelle tiedoston" - -#. *< type -#. *< ui_requirement -#. *< flags -#. *< dependencies -#. *< priority -#. *< id -#. *< name -#. *< version -#. * summary -#. * description -msgid "TOC Protocol Plugin" -msgstr "TOC-yhteyskäytäntöliitännäinen" - -#, c-format msgid "%s has sent you a webcam invite, which is not yet supported." msgstr "%s on lähettänyt webkamera-kutsun, mikä ei ole vielä tuettuna." +msgid "Your SMS was not delivered" +msgstr "Tekstiviestiä (SMS) ei välitetty" + msgid "Your Yahoo! message did not get sent." msgstr "Yahoo!-viestiäsi ei lähetetty." @@ -10019,9 +10004,6 @@ msgid "Extended away" msgstr "Pidennetty poissaolo" -msgid "Mobile" -msgstr "Liikkeellä" - msgid "Listening to music" msgstr "Kuuntelee musiikkia" @@ -10063,18 +10045,6 @@ msgid "%x %X" msgstr "%x %X" -#, c-format -msgid "Error Reading %s" -msgstr "Virhe luettaessa %s" - -#, c-format -msgid "" -"An error was encountered reading your %s. They have not been loaded, and " -"the old file has been renamed to %s~." -msgstr "" -"%s:n lukemisessa tapahtui virhe. Niitä ei ladattu ja vanha tiedosto on " -"nimetty uudelleen nimellä %s~." - msgid "Calculating..." msgstr "Lasketaan..." @@ -10150,6 +10120,14 @@ msgstr "Kohteeseen %s ei voi yhdistää: %s" #, c-format +msgid "" +"Unable to connect to %s: Server requires TLS/SSL, but no TLS/SSL support was " +"found." +msgstr "" +"Palvelimelle %s ei voi yhdistää: palvelin vaatii TSL/SSL-tuen, mutta TLS/SSL-" +"tukea ei löydy." + +#, c-format msgid " - %s" msgstr " - %s" @@ -10182,6 +10160,18 @@ msgid "Address already in use." msgstr "Osoite on jo käytössä." +#, c-format +msgid "Error Reading %s" +msgstr "Virhe luettaessa %s" + +#, c-format +msgid "" +"An error was encountered reading your %s. The file has not been loaded, and " +"the old file has been renamed to %s~." +msgstr "" +"%s:n lukemisessa tapahtui virhe. Tiedostoa ei ladattu ja vanha tiedosto on " +"nimetty uudelleen nimelle %s~." + msgid "Internet Messenger" msgstr "Pikaviestin" @@ -10224,10 +10214,8 @@ msgid "Use this buddy _icon for this account:" msgstr "Käytä tätä tuttavakuvaketta tälle käyttäjät_ilille:" -#. Build the protocol options frame. -#, c-format -msgid "%s Options" -msgstr "%s-valinnat" +msgid "_Advanced" +msgstr "_Lisäasetukset" msgid "Use GNOME Proxy Settings" msgstr "Käytä Gnomen välipalvelinasetuksia" @@ -10262,9 +10250,6 @@ msgid "you can see the butterflies mating" msgstr "voit nähdä perhosten parittelevan" -msgid "Proxy Options" -msgstr "Välipalvelinvalinnat" - msgid "Proxy _type:" msgstr "Välipalvelimen _tyyppi:" @@ -10292,8 +10277,8 @@ msgid "Create _this new account on the server" msgstr "Luo _tämä uusi käyttäjätili palvelimelle" -msgid "_Advanced" -msgstr "_Lisäasetukset" +msgid "_Proxy" +msgstr "_Välipalvelin" msgid "Enabled" msgstr "Käytössä" @@ -10368,6 +10353,15 @@ msgid "I_M" msgstr "_Pikaviesti" +msgid "_Audio Call" +msgstr "_Äänipuhelu" + +msgid "Audio/_Video Call" +msgstr "Ääni/_videopuhelu" + +msgid "_Video Call" +msgstr "_Videopuhelu" + msgid "_Send File..." msgstr "_Lähetä tiedosto..." @@ -10504,6 +10498,9 @@ msgid "/Tools/_Certificates" msgstr "/Työkalut/_Varmenteet" +msgid "/Tools/Custom Smile_ys" +msgstr "/Työkalut/Omat h_ymiöt" + msgid "/Tools/Plu_gins" msgstr "/Työkalut/_Liitännäiset" @@ -10513,9 +10510,6 @@ msgid "/Tools/Pr_ivacy" msgstr "/Työkalut/Yks_ityisyys" -msgid "/Tools/Smile_y" -msgstr "/Työkalut/H_ymiö" - msgid "/Tools/_File Transfers" msgstr "/Työkalut/_Tiedostonsiirrot..." @@ -10633,8 +10627,8 @@ msgid "By status" msgstr "Tilan mukaan" -msgid "By log size" -msgstr "Lokin koon mukaan" +msgid "By recent log activity" +msgstr "Viimeisimpien lokikirjauksien mukaan" #, c-format msgid "%s disconnected" @@ -10650,6 +10644,9 @@ msgid "Re-enable" msgstr "Ota uudelleen käyttöön" +msgid "SSL FAQs" +msgstr "SSL-UKK:t" + msgid "Welcome back!" msgstr "Tervetuloa takaisin." @@ -10740,6 +10737,9 @@ msgid "A_lias:" msgstr "_Lempinimi:" +msgid "_Group:" +msgstr "_Ryhmä:" + msgid "Auto_join when account becomes online." msgstr "Liity automaattisesti kun käyttä_jätili pääsee linjoille." @@ -10792,12 +10792,6 @@ msgid "Invite Buddy Into Chat Room" msgstr "Kutsu tuttava keskusteluhuoneeseen" -#. Put our happy label in it. -msgid "" -"Please enter the name of the user you wish to invite, along with an optional " -"invite message." -msgstr "Anna kutsuttavan käyttäjän nimi sekä vapaaehtoinen viesti." - msgid "_Buddy:" msgstr "_Tuttava:" @@ -10872,6 +10866,18 @@ msgid "/Conversation/Clea_r Scrollback" msgstr "/Keskustelu/T_yhjennä takaisinvieritys" +msgid "/Conversation/M_edia" +msgstr "/Keskustelu/M_edia" + +msgid "/Conversation/Media/_Audio Call" +msgstr "/Keskustelu/Media/_Äänipuhelu" + +msgid "/Conversation/Media/_Video Call" +msgstr "/Keskustelu/Media/_Videopuhelu" + +msgid "/Conversation/Media/Audio\\/Video _Call" +msgstr "/Keskustelu/Media/Ääni\\/video_puhelu" + msgid "/Conversation/Se_nd File..." msgstr "/Keskustelu/_Lähetä tiedosto..." @@ -10944,6 +10950,15 @@ msgid "/Conversation/View Log" msgstr "/Keskustelu/Näytä loki..." +msgid "/Conversation/Media/Audio Call" +msgstr "/Keskustelu/Media/Äänipuhelu" + +msgid "/Conversation/Media/Video Call" +msgstr "/Keskustelu/Media/Videopuhelu" + +msgid "/Conversation/Media/Audio\\/Video Call" +msgstr "/Keskustelu/Media/Ääni\\/videopuhelu" + msgid "/Conversation/Send File..." msgstr "/Keskustelu/Lähetä tiedosto..." @@ -11127,6 +11142,9 @@ msgid "Ka-Hing Cheung" msgstr "Ka-Hing Cheung" +msgid "voice and video" +msgstr "ääni ja video" + msgid "support" msgstr "tuki" @@ -11266,6 +11284,9 @@ msgid "Ubuntu Georgian Translators" msgstr "Ubuntun georgian kääntäjät" +msgid "Khmer" +msgstr "khmeeri" + msgid "Kannada" msgstr "kannada" @@ -11287,6 +11308,9 @@ msgid "Macedonian" msgstr "makedonia" +msgid "Mongolian" +msgstr "mongolia" + msgid "Bokmål Norwegian" msgstr "kirjanorja" @@ -11402,9 +11426,32 @@ "anna ohjelmalle minkäänlaista takuuta.<BR><BR>" #, c-format -msgid "<FONT SIZE=\"4\">IRC:</FONT> #pidgin on irc.freenode.net<BR><BR>" -msgstr "" -"<FONT SIZE=\"4\">IRC:</FONT> #pidgin palvelimella irc.freenode.net<BR><BR>" +msgid "" +"<FONT SIZE=\"4\">FAQ:</FONT> <A HREF=\"http://developer.pidgin.im/wiki/FAQ" +"\">http://developer.pidgin.im/wiki/FAQ</A><BR/><BR/>" +msgstr "" +"<FONT SIZE=\"4\">UKK:</FONT> <A HREF=\"http://developer.pidgin.im/wiki/FAQ" +"\">http://developer.pidgin.im/wiki/FAQ</A><BR/><BR/>" + +#, c-format +msgid "" +"<FONT SIZE=\"4\">Help via e-mail:</FONT> <A HREF=\"mailto:support@pidgin.im" +"\">support@pidgin.im</A><BR/><BR/>" +msgstr "" +"<FONT SIZE=\"4\">Apua sähköpostitse (engl.):</FONT> <A HREF=\"mailto:" +"support@pidgin.im\">support@pidgin.im</A><BR/><BR/>" + +#, c-format +msgid "" +"<FONT SIZE=\"4\">IRC Channel:</FONT> #pidgin on irc.freenode.net<BR><BR>" +msgstr "" +"<FONT SIZE=\"4\">IRC-kanava:</FONT> #pidgin palvelimella irc.freenode." +"net<BR><BR>" + +#, c-format +msgid "<FONT SIZE=\"4\">XMPP MUC:</FONT> devel@conference.pidgin.im<BR><BR>" +msgstr "" +"<FONT SIZE=\"4\">XMPP-keskustelu:</FONT> devel@conference.pidgin.im<BR><BR>" msgid "Current Developers" msgstr "Nykyiset kehittäjät" @@ -11704,15 +11751,6 @@ msgid "Enable typing notification" msgstr "Ota kirjoittamishuomautus käyttöön" -msgid "_Copy Email Address" -msgstr "_Kopioi sähköpostiosoite" - -msgid "_Open Link in Browser" -msgstr "_Avaa linkki selaimessa" - -msgid "_Copy Link Location" -msgstr "_Kopioi linkin osoite" - msgid "" "<span size='larger' weight='bold'>Unrecognized file type</span>\n" "\n" @@ -11964,6 +12002,7 @@ "\n" " -c, --config=DIR use DIR for config files\n" " -d, --debug print debugging messages to stdout\n" +" -f, --force-online force online, regardless of network status\n" " -h, --help display this help and exit\n" " -m, --multiple do not ensure single instance\n" " -n, --nologin don't automatically login\n" @@ -11978,6 +12017,7 @@ "\n" " -c, --config=HAK käytä hakemistoa HAK asetustiedostoille\n" " -d, --debug kirjoita virheenjäljitysviestit putkeen stdout\n" +" -f, --force-online pakota ”tavoitettavissa” riippumatta verkon tilasta\n" " -h, --help näytä tämä ohje ja poistu\n" " -m, --multiple älä pitäydy vain yhdessä instanssissa\n" " -n, --nologin älä kirjaudu automaattisesti\n" @@ -11994,6 +12034,7 @@ "\n" " -c, --config=DIR use DIR for config files\n" " -d, --debug print debugging messages to stdout\n" +" -f, --force-online force online, regardless of network status\n" " -h, --help display this help and exit\n" " -m, --multiple do not ensure single instance\n" " -n, --nologin don't automatically login\n" @@ -12007,6 +12048,7 @@ "\n" " -c, --config=HAK käytä hakemistoa HAK asetustiedostoille\n" " -d, --debug kirjoita virheenjäljitysviestit putkeen stdout\n" +" -f, --force-online pakota ”tavoitettavissa” riippumatta verkon tilasta\n" " -h, --help näytä tämä ohje ja poistu\n" " -m, --multiple älä pitäydy vain yhdessä instanssissa\n" " -n, --nologin älä kirjaudu automaattisesti\n" @@ -12048,11 +12090,26 @@ msgid "Pidgin" msgstr "Pidgin" -msgid "Open All Messages" -msgstr "Avaa kaikki viestit" - -msgid "<span weight=\"bold\" size=\"larger\">You have mail!</span>" -msgstr "<span weight=\"bold\" size=\"larger\">Sinulle on postia!</span>" +#, c-format +msgid "Exiting because another libpurple client is already running.\n" +msgstr "Poistutaan, koska toinen libpurple-asiakas on jo käynnissä.\n" + +msgid "/_Media" +msgstr "/_Media" + +msgid "/Media/_Hangup" +msgstr "/Media/_Katkaise" + +msgid "Calling..." +msgstr "Soitetaan..." + +#, c-format +msgid "%s wishes to start an audio/video session with you." +msgstr "%s haluaa aloittaa ääni/videoistunnon kanssasi." + +#, c-format +msgid "%s wishes to start a video session with you." +msgstr "%s haluaa aloittaa videoistunnon kanssasi." #, c-format msgid "%s has %d new message." @@ -12081,6 +12138,24 @@ "The 'Manual' browser command has been chosen, but no command has been set." msgstr "Oma selainkomento -asetus valittu, mutta komentoa ei ole asetettu." +msgid "Open All Messages" +msgstr "Avaa kaikki viestit" + +msgid "<span weight=\"bold\" size=\"larger\">You have mail!</span>" +msgstr "<span weight=\"bold\" size=\"larger\">Sinulle on postia!</span>" + +msgid "New Pounces" +msgstr "Uusi ilmoituksia" + +msgid "Dismiss" +msgstr "Hylkää" + +msgid "<span weight=\"bold\" size=\"larger\">You have pounced!</span>" +msgstr "<span weight=\"bold\" size=\"larger\">Uusi ilmoitus</span>" + +msgid "No message" +msgstr "Ei viestiä" + msgid "The following plugins will be unloaded." msgstr "Seuraavat liitännäiset otetaan pois käytöstä." @@ -12129,6 +12204,9 @@ msgid "Select a file" msgstr "Valitse tiedosto" +msgid "Modify Buddy Pounce" +msgstr "Muokkaa tuttavailmoitinta" + #. Create the "Pounce on Whom" frame. msgid "Pounce on Whom" msgstr "Kenestä ilmoitetaan" @@ -12199,6 +12277,50 @@ msgid "Pounce Target" msgstr "Ilmoituksen kohde" +#, c-format +msgid "Started typing" +msgstr "alkoi kirjoittaa" + +#, c-format +msgid "Paused while typing" +msgstr "keskeytti kirjoittamisen" + +#, c-format +msgid "Signed on" +msgstr "kirjautui sisään" + +#, c-format +msgid "Returned from being idle" +msgstr "palasi oltuaan jouten" + +#, c-format +msgid "Returned from being away" +msgstr "palasi oltuaan poissa" + +#, c-format +msgid "Stopped typing" +msgstr "lopetti kirjoittamisen" + +#, c-format +msgid "Signed off" +msgstr "kirjautui ulos" + +#, c-format +msgid "Became idle" +msgstr "muuttui jouten olevaksi" + +#, c-format +msgid "Went away" +msgstr "muuttui poissa olevaksi" + +#, c-format +msgid "Sent a message" +msgstr "lähetti viestin" + +#, c-format +msgid "Unknown.... Please report this!" +msgstr "Tuntematon... Raportoi tästä!" + msgid "Smiley theme failed to unpack." msgstr "Hymiöteeman purkaminen epäonnistui." @@ -12221,6 +12343,11 @@ msgid "Cl_ose conversations with the Escape key" msgstr "S_ulje keskustelut Escape-näppäimellä" +#. Buddy List Themes +msgid "Buddy List Theme" +msgstr "Tuttavaluettelon teema" + +#. System Tray msgid "System Tray Icon" msgstr "Ilmoitusalueen kuvake" @@ -12331,9 +12458,6 @@ msgid "Cannot start browser configuration program." msgstr "Selaimen asetusohjelmaa ei voi käynnistää." -msgid "ST_UN server:" -msgstr "ST_UN-palvelin:" - msgid "<span style=\"italic\">Example: stunserver.org</span>" msgstr "<span style=\"italic\">Esimerkki: stunserver.org</span>" @@ -12358,6 +12482,10 @@ msgid "_End port:" msgstr "Viimeinen _portti:" +#. TURN server +msgid "Relay Server (TURN)" +msgstr "Edelleenvälityspalvelin (TURN)" + msgid "Proxy Server & Browser" msgstr "Välipalvelin & selain" @@ -12386,6 +12514,10 @@ msgid "No proxy" msgstr "Ei välipalvelinta" +#. This is a global option that affects SOCKS4 usage even with account-specific proxy settings +msgid "Use remote DNS with SOCKS4 proxies" +msgstr "Käytä etä-DNS:ää SOCKS4-välipalvelimien kanssa" + msgid "_User:" msgstr "_Käyttäjä:" @@ -12544,12 +12676,12 @@ msgid "Auto-away" msgstr "Automaattinen poissaoloasetus" +msgid "_Minutes before becoming idle:" +msgstr "_Minuutteja ennen jouten olevaksi asettamista:" + msgid "Change status when _idle" msgstr "Vaihda tila, kun ollaan _jouten" -msgid "_Minutes before becoming idle:" -msgstr "_Minuutteja ennen jouten olevaksi asettamista:" - msgid "Change _status to:" msgstr "Vaihda tila seuraavaksi:" @@ -12697,6 +12829,12 @@ msgid "Status for %s" msgstr "%s:n tila" +#. +#. * TODO: We should enable/disable the add button based on +#. * whether the user has entered all required data. That +#. * would eliminate the need for this check and provide a +#. * better user experience. +#. msgid "Custom Smiley" msgstr "Oma hymiö" @@ -12706,14 +12844,14 @@ msgid "Please provide a shortcut to associate with the smiley." msgstr "Syötä hymiöön liitettävä oikotie." +#, fuzzy, c-format +msgid "" +"A custom smiley for '%s' already exists. Please use a different shortcut." +msgstr "Valitulle oikotielle on jo oma hymiö. Valitse toisenlainen oikotie." + msgid "Duplicate Shortcut" msgstr "Monista oikotie" -msgid "" -"A custom smiley for the selected shortcut already exists. Please specify a " -"different shortcut." -msgstr "Valitulle oikotielle on jo oma hymiö. Valitse toisenlainen oikotie." - msgid "Please select an image for the smiley." msgstr "Valitse hymiölle kuva." @@ -12723,16 +12861,19 @@ msgid "Add Smiley" msgstr "Lisää hymiö" -msgid "Smiley _Image" -msgstr "Hymiön kuva" - -#. Smiley shortcut -msgid "Smiley S_hortcut" -msgstr "Hymiön _oikotie" +msgid "_Image:" +msgstr "Ku_va:" + +#. Shortcut text +msgid "S_hortcut text:" +msgstr "Oi_kotien teksti:" msgid "Smiley" msgstr "Hymiö" +msgid "Shortcut Text" +msgstr "Oikotien teksti" + msgid "Custom Smiley Manager" msgstr "Omien hymiöiden hallinta" @@ -12858,6 +12999,15 @@ "Kuvaa \"%s\" ei voi ladata: syy ei ole tiedossa, mahdollisesti vioittunut " "kuvatiedosto" +msgid "_Open Link" +msgstr "_Avaa linkki" + +msgid "_Copy Link Location" +msgstr "_Kopioi linkin osoite" + +msgid "_Copy Email Address" +msgstr "_Kopioi sähköpostiosoite" + msgid "Save File" msgstr "Tallenna tiedosto" @@ -13832,9 +13982,6 @@ msgid "Only when docked" msgstr "Vain telakoituna" -msgid "_Flash window when chat messages are received" -msgstr "_Vilkuta ikkunaa ryhmäkeskusteluviestien saapuessa" - msgid "Windows Pidgin Options" msgstr "Windows Pidgin -valinnat" @@ -13886,6 +14033,185 @@ "Tätä liitännäistä voidaan käyttää XMPP-palvelimien tai -asiakasohjelmien " "virheenjäljitykseen." +#~ msgid "Invite message" +#~ msgstr "Kutsuviesti" + +#~ msgid "" +#~ "Please enter the name of the user you wish to invite,\n" +#~ "along with an optional invite message." +#~ msgstr "" +#~ "Anna kutsuttavan käyttäjän nimi sekä vapaaehtoinen \n" +#~ "kutsuviesti." + +#~ msgid "Unable to retrieve MSN Address Book" +#~ msgstr "MSN-osoitekirjaa ei onnistuttu noutamaan" + +#~ msgid "Looking up %s" +#~ msgstr "Etsitään %s" + +#~ msgid "Connect to %s failed" +#~ msgstr "%s: yhteyden muodostaminen epäonnistui" + +#~ msgid "Signon: %s" +#~ msgstr "Kirjautuminen: %s" + +#~ msgid "Unable to write file %s." +#~ msgstr "Ei kyetty kirjoittamaan tiedostoa %s." + +#~ msgid "Unable to read file %s." +#~ msgstr "Ei kyetty lukemaan tiedostoa %s." + +#~ msgid "Message too long, last %s bytes truncated." +#~ msgstr "Viesti on liian pitkä, viimeiset %s tavua katkaistu." + +#~ msgid "%s not currently logged in." +#~ msgstr "%s ei ole parhaillaan kirjautuneena sisään." + +#~ msgid "Warning of %s not allowed." +#~ msgstr "%s:n varoittaminen ei ole sallittua." + +#~ msgid "" +#~ "A message has been dropped, you are exceeding the server speed limit." +#~ msgstr "Viesti on hylätty, ylität palvelimen nopeusrajan." + +#~ msgid "Chat in %s is not available." +#~ msgstr "Ryhmäkeskustelu %s ei ole käytettävissä." + +#~ msgid "You are sending messages too fast to %s." +#~ msgstr "Lähetät viestejä %s:lle liian nopeasti." + +#~ msgid "You missed an IM from %s because it was too big." +#~ msgstr "Et saanut %s:n pikaviestiä koska se oli liian suuri." + +#~ msgid "You missed an IM from %s because it was sent too fast." +#~ msgstr "Et saanut %s:n pikaviestiä koska se lähetettiin liian nopeasti." + +#~ msgid "Failure." +#~ msgstr "Epäonnistuminen." + +#~ msgid "Too many matches." +#~ msgstr "Liian monta tulosta." + +#~ msgid "Need more qualifiers." +#~ msgstr "Tarvitaan lisää määritteitä." + +#~ msgid "Dir service temporarily unavailable." +#~ msgstr "Hakemistopalvelu ei tilapäisesti ole käytettävissä." + +#~ msgid "Email lookup restricted." +#~ msgstr "Sähköpostin katsominen rajoitettu." + +#~ msgid "Keyword ignored." +#~ msgstr "Avainsanasta ei välitetty." + +#~ msgid "No keywords." +#~ msgstr "Ei avainsanoja." + +#~ msgid "User has no directory information." +#~ msgstr "Käyttäjällä ei ole hakemistotietoja." + +#~ msgid "Country not supported." +#~ msgstr "Maa ei tuettu." + +#~ msgid "Failure unknown: %s." +#~ msgstr "Tunnistamaton epäonnistuminen: %s." + +#~ msgid "Incorrect username or password." +#~ msgstr "Virheellinen käyttäjänimi tai salasana." + +#~ msgid "The service is temporarily unavailable." +#~ msgstr "Palvelu ei tilapäisesti ole käytössä." + +#~ msgid "Your warning level is currently too high to log in." +#~ msgstr "Varoitustasosi on parhaillaan liian korkea kirjautuaksesi sisään." + +#~ msgid "" +#~ "You have been connecting and disconnecting too frequently. Wait ten " +#~ "minutes and try again. If you continue to try, you will need to wait " +#~ "even longer." +#~ msgstr "" +#~ "Olet ottanut ja katkaissut yhteyden liian tiheään. Odota kymmenen " +#~ "minuuttia ja yritä uudestaan. Jos jatkat yrittämistä, joudut odottamaan " +#~ "vielä pidempään." + +#~ msgid "An unknown signon error has occurred: %s." +#~ msgstr "Tuntematon sisäänkirjautumisvirhe esiintyi: %s." + +#~ msgid "An unknown error, %d, has occurred. Info: %s" +#~ msgstr "Tuntematon virhe, %d, esiintyi. Tiedot: %s" + +#~ msgid "Invalid Groupname" +#~ msgstr "Epäkelpo ryhmän nimi" + +#~ msgid "Connection Closed" +#~ msgstr "Yhteys suljettu" + +#~ msgid "Waiting for reply..." +#~ msgstr "Odotetaan vastausta..." + +#~ msgid "TOC has come back from its pause. You may now send messages again." +#~ msgstr "TOC on palannut tauoltaan. Voit lähettää viestejä jälleen." + +#~ msgid "Password Change Successful" +#~ msgstr "Salasanan vaihto onnistui" + +#~ msgid "Get Dir Info" +#~ msgstr "Hae hakemistotiedot" + +#~ msgid "Set Dir Info" +#~ msgstr "Aseta hakemistotiedot" + +#~ msgid "Could not open %s for writing!" +#~ msgstr "%s:n avaaminen kirjoitusta varten epäonnistui!" + +#~ msgid "File transfer failed; other side probably canceled." +#~ msgstr "" +#~ "Tiedostonsiirto epäonnistui. Toinen osapuoli luultavasti katkaisi siirron." + +#~ msgid "Could not connect for transfer." +#~ msgstr "Yhteyttä siirtoa varten ei voi muodostaa." + +#~ msgid "Could not write file header. The file will not be transferred." +#~ msgstr "Tiedosto-otsikkoa ei voi kirjoittaa. Tiedostoa ei siirretä." + +#~ msgid "Save As..." +#~ msgstr "Tallenna nimellä..." + +#~ msgid "%s requests %s to accept %d file: %s (%.2f %s)%s%s" +#~ msgid_plural "%s requests %s to accept %d files: %s (%.2f %s)%s%s" +#~ msgstr[0] "%s pyytää %s hyväksymään %d tiedoston: %s (%.2f %s)%s%s" +#~ msgstr[1] "%s pyytää %s hyväksymään %d tiedostot: %s (%.2f %s)%s%s" + +#~ msgid "%s requests you to send them a file" +#~ msgstr "%s pyytää sinua lähettämään hänelle tiedoston" + +#~ msgid "TOC Protocol Plugin" +#~ msgstr "TOC-yhteyskäytäntöliitännäinen" + +#~ msgid "%s Options" +#~ msgstr "%s-valinnat" + +#~ msgid "Proxy Options" +#~ msgstr "Välipalvelinvalinnat" + +#~ msgid "By log size" +#~ msgstr "Lokin koon mukaan" + +#~ msgid "_Open Link in Browser" +#~ msgstr "_Avaa linkki selaimessa" + +#~ msgid "ST_UN server:" +#~ msgstr "ST_UN-palvelin:" + +#~ msgid "Smiley _Image" +#~ msgstr "Hymiön kuva" + +#~ msgid "Smiley S_hortcut" +#~ msgstr "Hymiön _oikotie" + +#~ msgid "_Flash window when chat messages are received" +#~ msgstr "_Vilkuta ikkunaa ryhmäkeskusteluviestien saapuessa" + #~ msgid "" #~ "You may be disconnected shortly. You may want to use TOC until this is " #~ "fixed. Check %s for updates." @@ -14834,9 +15160,6 @@ #~ msgid "minutes." #~ msgstr "minuuttia." -#~ msgid "Nickname: %s\n" -#~ msgstr "Kutsumanimi: %s\n" - #~ msgid "" #~ "\n" #~ "Idle: %s" @@ -15350,9 +15673,6 @@ #~ msgid "Calling %s" #~ msgstr "Soitetaan %s" -#~ msgid "End Call" -#~ msgstr "Lopeta puhelu" - #~ msgid "Receiving call from %s" #~ msgstr "Puhelu tulossa käyttäjältä %s" @@ -15594,9 +15914,6 @@ #~ msgid "_Idle" #~ msgstr "_on jouten" -#~ msgid "Retur_n from idle" -#~ msgstr "palaa oltuaan _jouten" - #~ msgid "Bro_wse..." #~ msgstr "_Selaa" @@ -15632,9 +15949,6 @@ #~ msgstr[0] "(%d viesti)" #~ msgstr[1] "(%d viestiä)" -#~ msgid "(1 message)" -#~ msgstr "(1 viesti)" - #~ msgid "Hide Disconnect Errors" #~ msgstr "Piilota yhteydenkatkaisuvirheilmoitukset"