comparison libpurple/protocols/jabber/jabber.c @ 25987:c4fd9222dda1

propagate from branch 'im.pidgin.pidgin' (head 303af74a38e7b313d4fb0be4d4054a16cb13d819) to branch 'im.pidgin.cpw.darkrain42.xmpp.bosh' (head 650d82b8a5f0c8860804dd8004cd54badea48e1e)
author Paul Aurich <paul@darkrain42.org>
date Sat, 07 Mar 2009 01:59:40 +0000
parents 5f9a24d1c25e 97a4d71e0c3d
children 035ba9355361
comparison
equal deleted inserted replaced
25464:0e93bbb7f5ca 25987:c4fd9222dda1
40 #include "version.h" 40 #include "version.h"
41 #include "xmlnode.h" 41 #include "xmlnode.h"
42 42
43 #include "auth.h" 43 #include "auth.h"
44 #include "buddy.h" 44 #include "buddy.h"
45 #include "caps.h"
45 #include "chat.h" 46 #include "chat.h"
46 #include "data.h" 47 #include "data.h"
47 #include "disco.h" 48 #include "disco.h"
48 #include "google.h" 49 #include "google.h"
49 #include "iq.h" 50 #include "iq.h"
62 63
63 #define JABBER_CONNECT_STEPS (js->gsc ? 9 : 5) 64 #define JABBER_CONNECT_STEPS (js->gsc ? 9 : 5)
64 65
65 static PurplePlugin *my_protocol = NULL; 66 static PurplePlugin *my_protocol = NULL;
66 GList *jabber_features = NULL; 67 GList *jabber_features = NULL;
68 GList *jabber_identities = NULL;
67 69
68 static void jabber_unregister_account_cb(JabberStream *js); 70 static void jabber_unregister_account_cb(JabberStream *js);
69 static void try_srv_connect(JabberStream *js); 71 static void try_srv_connect(JabberStream *js);
70 72
71 static void jabber_stream_init(JabberStream *js) 73 static void jabber_stream_init(JabberStream *js)
87 static void 89 static void
88 jabber_session_initialized_cb(JabberStream *js, xmlnode *packet, gpointer data) 90 jabber_session_initialized_cb(JabberStream *js, xmlnode *packet, gpointer data)
89 { 91 {
90 const char *type = xmlnode_get_attrib(packet, "type"); 92 const char *type = xmlnode_get_attrib(packet, "type");
91 if(type && !strcmp(type, "result")) { 93 if(type && !strcmp(type, "result")) {
92 jabber_stream_set_state(js, JABBER_STREAM_CONNECTED); 94 jabber_disco_items_server(js);
93 if(js->unregistration) 95 if(js->unregistration)
94 jabber_unregister_account_cb(js); 96 jabber_unregister_account_cb(js);
95 } else { 97 } else {
96 purple_connection_error_reason (js->gc, 98 purple_connection_error_reason (js->gc,
97 PURPLE_CONNECTION_ERROR_NETWORK_ERROR, 99 PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
178 dot = '\0'; 180 dot = '\0';
179 181
180 return purple_strreplace(input, "__HOSTNAME__", hostname); 182 return purple_strreplace(input, "__HOSTNAME__", hostname);
181 } 183 }
182 184
183 static void jabber_stream_features_parse(JabberStream *js, xmlnode *packet) 185 void jabber_stream_features_parse(JabberStream *js, xmlnode *packet)
184 { 186 {
185 if(xmlnode_get_child(packet, "starttls")) { 187 if(xmlnode_get_child(packet, "starttls")) {
186 if(jabber_process_starttls(js, packet)) 188 if(jabber_process_starttls(js, packet))
187 189
188 return; 190 return;
418 } 420 }
419 return; 421 return;
420 } 422 }
421 #endif 423 #endif
422 424
423 do_jabber_send_raw(js, data, len); 425 if (len == -1)
426 len = strlen(data);
427
428 if (js->use_bosh)
429 jabber_bosh_connection_send_raw(js->bosh, data, len);
430 else
431 do_jabber_send_raw(js, data, len);
424 } 432 }
425 433
426 int jabber_prpl_send_raw(PurpleConnection *gc, const char *buf, int len) 434 int jabber_prpl_send_raw(PurpleConnection *gc, const char *buf, int len)
427 { 435 {
428 JabberStream *js = (JabberStream*)gc->proto_data; 436 JabberStream *js = (JabberStream*)gc->proto_data;
439 447
440 /* if we get NULL back, we're done processing */ 448 /* if we get NULL back, we're done processing */
441 if(NULL == packet) 449 if(NULL == packet)
442 return; 450 return;
443 451
444 txt = xmlnode_to_str(packet, &len); 452 if (js->use_bosh)
445 jabber_send_raw(js, txt, len); 453 jabber_bosh_connection_send(js->bosh, packet);
446 g_free(txt); 454 else {
455 txt = xmlnode_to_str(packet, &len);
456 jabber_send_raw(js, txt, len);
457 g_free(txt);
458 }
447 } 459 }
448 460
449 static void jabber_pong_cb(JabberStream *js, xmlnode *packet, gpointer unused) 461 static void jabber_pong_cb(JabberStream *js, xmlnode *packet, gpointer unused)
450 { 462 {
451 purple_timeout_remove(js->keepalive_timeout); 463 purple_timeout_remove(js->keepalive_timeout);
581 593
582 /* Tell the app that we're doing encryption */ 594 /* Tell the app that we're doing encryption */
583 jabber_stream_set_state(js, JABBER_STREAM_INITIALIZING_ENCRYPTION); 595 jabber_stream_set_state(js, JABBER_STREAM_INITIALIZING_ENCRYPTION);
584 } 596 }
585 597
598 static void
599 jabber_bosh_login_callback(PurpleBOSHConnection *conn)
600 {
601 purple_debug_info("jabber","YAY...BOSH connection established.\n");
602 }
603
604 static void
605 txt_resolved_cb(PurpleTxtResponse *resp, int results, gpointer data)
606 {
607 JabberStream *js = data;
608 int n;
609
610 js->srv_query_data = NULL;
611
612 if (results == 0) {
613 gchar *tmp;
614 tmp = g_strdup_printf(_("Could not find alternative XMPP connection methods after failing to connect directly.\n"));
615 purple_connection_error_reason (js->gc,
616 PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp);
617 g_free(tmp);
618 return;
619 }
620
621 for (n = 0; n < results; n++) {
622 gchar **token;
623 token = g_strsplit(resp[n].content, "=", 2);
624 if (!strcmp(token[0], "_xmpp-client-xbosh")) {
625 purple_debug_info("jabber","Found alternative connection method using %s at %s.\n", token[0], token[1]);
626 js->bosh = jabber_bosh_connection_init(js, token[1]);
627 js->use_bosh = TRUE;
628 g_strfreev(token);
629 break;
630 }
631 g_strfreev(token);
632 }
633 if (js->bosh) {
634 jabber_bosh_connection_connect(js->bosh);
635 } else {
636 purple_debug_info("jabber","Didn't find an alternative connection method.\n");
637 }
638 }
586 639
587 static void 640 static void
588 jabber_login_callback(gpointer data, gint source, const gchar *error) 641 jabber_login_callback(gpointer data, gint source, const gchar *error)
589 { 642 {
590 PurpleConnection *gc = data; 643 PurpleConnection *gc = data;
593 if (source < 0) { 646 if (source < 0) {
594 if (js->srv_rec != NULL) { 647 if (js->srv_rec != NULL) {
595 purple_debug_error("jabber", "Unable to connect to server: %s. Trying next SRV record.\n", error); 648 purple_debug_error("jabber", "Unable to connect to server: %s. Trying next SRV record.\n", error);
596 try_srv_connect(js); 649 try_srv_connect(js);
597 } else { 650 } else {
598 gchar *tmp; 651 purple_debug_info("jabber","Couldn't connect directly to %s. Trying to find alternative connection methods, like BOSH.\n", js->user->domain);
599 tmp = g_strdup_printf(_("Could not establish a connection with the server:\n%s"), 652 js->srv_query_data = purple_txt_resolve("_xmppconnect", js->user->domain, txt_resolved_cb, js);
600 error);
601 purple_connection_error_reason(gc,
602 PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp);
603 g_free(tmp);
604 } 653 }
605 return; 654 return;
606 } 655 }
607 656
608 g_free(js->srv_rec); 657 g_free(js->srv_rec);
726 js->user = jabber_id_new(purple_account_get_username(account)); 775 js->user = jabber_id_new(purple_account_get_username(account));
727 js->next_id = g_random_int(); 776 js->next_id = g_random_int();
728 js->write_buffer = purple_circ_buffer_new(512); 777 js->write_buffer = purple_circ_buffer_new(512);
729 js->old_length = 0; 778 js->old_length = 0;
730 js->keepalive_timeout = -1; 779 js->keepalive_timeout = -1;
731 js->certificate_CN = g_strdup(connect_server[0] ? connect_server : js->user ? js->user->domain : NULL);
732 780
733 if(!js->user) { 781 if(!js->user) {
734 purple_connection_error_reason (gc, 782 purple_connection_error_reason (gc,
735 PURPLE_CONNECTION_ERROR_INVALID_SETTINGS, 783 PURPLE_CONNECTION_ERROR_INVALID_SETTINGS,
736 _("Invalid XMPP ID")); 784 _("Invalid XMPP ID"));
746 794
747 if((my_jb = jabber_buddy_find(js, purple_account_get_username(account), TRUE))) 795 if((my_jb = jabber_buddy_find(js, purple_account_get_username(account), TRUE)))
748 my_jb->subscription |= JABBER_SUB_BOTH; 796 my_jb->subscription |= JABBER_SUB_BOTH;
749 797
750 jabber_stream_set_state(js, JABBER_STREAM_CONNECTING); 798 jabber_stream_set_state(js, JABBER_STREAM_CONNECTING);
799
800 /* TODO: Just use purple_url_parse? */
801 if (!g_ascii_strncasecmp(connect_server, "http://", 7) || !g_ascii_strncasecmp(connect_server, "https://", 8)) {
802 js->use_bosh = TRUE;
803 js->bosh = jabber_bosh_connection_init(js, connect_server);
804 if (!js->bosh) {
805 purple_connection_error_reason (js->gc,
806 PURPLE_CONNECTION_ERROR_INVALID_SETTINGS,
807 _("Malformed BOSH Connect Server"));
808 return;
809 }
810 jabber_bosh_connection_connect(js->bosh);
811 return;
812 } else {
813 js->certificate_CN = g_strdup(connect_server[0] ? connect_server : js->user->domain);
814 }
751 815
752 /* if they've got old-ssl mode going, we probably want to ignore SRV lookups */ 816 /* if they've got old-ssl mode going, we probably want to ignore SRV lookups */
753 if(purple_account_get_bool(js->gc->account, "old_ssl", FALSE)) { 817 if(purple_account_get_bool(js->gc->account, "old_ssl", FALSE)) {
754 if(purple_ssl_is_supported()) { 818 if(purple_ssl_is_supported()) {
755 js->gsc = purple_ssl_connect(js->gc->account, 819 js->gsc = purple_ssl_connect(js->gc->account,
756 js->certificate_CN, 820 js->certificate_CN,
757 purple_account_get_int(account, "port", 5223), jabber_login_callback_ssl, 821 purple_account_get_int(account, "port", 5223), jabber_login_callback_ssl,
758 jabber_ssl_connect_failure, js->gc); 822 jabber_ssl_connect_failure, js->gc);
823 if (!js->gsc) {
824 purple_connection_error_reason (js->gc,
825 PURPLE_CONNECTION_ERROR_NO_SSL_SUPPORT,
826 _("Unable to establish SSL connection"));
827 }
759 } else { 828 } else {
760 purple_connection_error_reason (js->gc, 829 purple_connection_error_reason (js->gc,
761 PURPLE_CONNECTION_ERROR_NO_SSL_SUPPORT, 830 PURPLE_CONNECTION_ERROR_NO_SSL_SUPPORT,
762 _("SSL support unavailable")); 831 _("SSL support unavailable"));
763 } 832 }
833
834 return;
764 } 835 }
765 836
766 /* no old-ssl, so if they've specified a connect server, we'll use that, otherwise we'll 837 /* no old-ssl, so if they've specified a connect server, we'll use that, otherwise we'll
767 * invoke the magic of SRV lookups, to figure out host and port */ 838 * invoke the magic of SRV lookups, to figure out host and port */
768 if(!js->gsc) { 839 if(connect_server[0]) {
769 if(connect_server[0]) { 840 jabber_login_connect(js, js->user->domain, connect_server, purple_account_get_int(account, "port", 5222), TRUE);
770 jabber_login_connect(js, js->user->domain, connect_server, purple_account_get_int(account, "port", 5222), TRUE); 841 } else {
771 } else { 842 js->srv_query_data = purple_srv_resolve("xmpp-client",
772 js->srv_query_data = purple_srv_resolve("xmpp-client", 843 "tcp", js->user->domain, srv_resolved_cb, js);
773 "tcp", js->user->domain, srv_resolved_cb, js);
774 }
775 } 844 }
776 } 845 }
777 846
778 847
779 static gboolean 848 static gboolean
1322 1391
1323 /* Don't perform any actions on the ssl connection 1392 /* Don't perform any actions on the ssl connection
1324 * if we were forcibly disconnected because it will crash 1393 * if we were forcibly disconnected because it will crash
1325 * on some SSL backends. 1394 * on some SSL backends.
1326 */ 1395 */
1327 if (!gc->disconnect_timeout) 1396 if (!gc->disconnect_timeout) {
1328 jabber_send_raw(js, "</stream:stream>", -1); 1397 if (js->use_bosh)
1398 jabber_bosh_connection_close(js->bosh);
1399 else
1400 jabber_send_raw(js, "</stream:stream>", -1);
1401 }
1329 1402
1330 if (js->srv_query_data) 1403 if (js->srv_query_data)
1331 purple_srv_cancel(js->srv_query_data); 1404 purple_srv_cancel(js->srv_query_data);
1332 1405
1333 if(js->gsc) { 1406 if(js->gsc) {
1339 if(js->gc->inpa) 1412 if(js->gc->inpa)
1340 purple_input_remove(js->gc->inpa); 1413 purple_input_remove(js->gc->inpa);
1341 close(js->fd); 1414 close(js->fd);
1342 } 1415 }
1343 1416
1417 if (js->bosh)
1418 jabber_bosh_connection_destroy(js->bosh);
1419
1344 jabber_buddy_remove_all_pending_buddy_info_requests(js); 1420 jabber_buddy_remove_all_pending_buddy_info_requests(js);
1345 1421
1346 jabber_parser_free(js); 1422 jabber_parser_free(js);
1347 1423
1348 if(js->iq_callbacks) 1424 if(js->iq_callbacks)
1380 1456
1381 g_free(js->stream_id); 1457 g_free(js->stream_id);
1382 if(js->user) 1458 if(js->user)
1383 jabber_id_free(js->user); 1459 jabber_id_free(js->user);
1384 g_free(js->avatar_hash); 1460 g_free(js->avatar_hash);
1461 g_free(js->caps_hash);
1385 1462
1386 purple_circ_buffer_destroy(js->write_buffer); 1463 purple_circ_buffer_destroy(js->write_buffer);
1387 if(js->writeh) 1464 if(js->writeh)
1388 purple_input_remove(js->writeh); 1465 purple_input_remove(js->writeh);
1389 #ifdef HAVE_CYRUS_SASL 1466 #ifdef HAVE_CYRUS_SASL
1472 1549
1473 break; 1550 break;
1474 case JABBER_STREAM_CONNECTED: 1551 case JABBER_STREAM_CONNECTED:
1475 /* now we can alert the core that we're ready to send status */ 1552 /* now we can alert the core that we're ready to send status */
1476 purple_connection_set_state(js->gc, PURPLE_CONNECTED); 1553 purple_connection_set_state(js->gc, PURPLE_CONNECTED);
1477 jabber_disco_items_server(js);
1478 break; 1554 break;
1479 } 1555 }
1480 } 1556 }
1481 1557
1482 char *jabber_get_next_id(JabberStream *js) 1558 char *jabber_get_next_id(JabberStream *js)
1590 xmlnode_set_attrib(item, "jid", who); 1666 xmlnode_set_attrib(item, "jid", who);
1591 1667
1592 jabber_iq_send(iq); 1668 jabber_iq_send(iq);
1593 } 1669 }
1594 1670
1595 void jabber_add_feature(const char *shortname, const char *namespace, JabberFeatureEnabled cb) { 1671 void jabber_add_feature(const char *namespace, JabberFeatureEnabled cb) {
1596 JabberFeature *feat; 1672 JabberFeature *feat;
1597 1673
1598 g_return_if_fail(shortname != NULL);
1599 g_return_if_fail(namespace != NULL); 1674 g_return_if_fail(namespace != NULL);
1600 1675
1601 feat = g_new0(JabberFeature,1); 1676 feat = g_new0(JabberFeature,1);
1602 feat->shortname = g_strdup(shortname);
1603 feat->namespace = g_strdup(namespace); 1677 feat->namespace = g_strdup(namespace);
1604 feat->is_enabled = cb; 1678 feat->is_enabled = cb;
1605 1679
1606 /* try to remove just in case it already exists in the list */ 1680 /* try to remove just in case it already exists in the list */
1607 jabber_remove_feature(shortname); 1681 jabber_remove_feature(namespace);
1608 1682
1609 jabber_features = g_list_append(jabber_features, feat); 1683 jabber_features = g_list_append(jabber_features, feat);
1610 } 1684 }
1611 1685
1612 void jabber_remove_feature(const char *shortname) { 1686 void jabber_remove_feature(const char *namespace) {
1613 GList *feature; 1687 GList *feature;
1614 for(feature = jabber_features; feature; feature = feature->next) { 1688 for(feature = jabber_features; feature; feature = feature->next) {
1615 JabberFeature *feat = (JabberFeature*)feature->data; 1689 JabberFeature *feat = (JabberFeature*)feature->data;
1616 if(!strcmp(feat->shortname, shortname)) { 1690 if(!strcmp(feat->namespace, namespace)) {
1617 g_free(feat->shortname);
1618 g_free(feat->namespace); 1691 g_free(feat->namespace);
1619
1620 g_free(feature->data); 1692 g_free(feature->data);
1621 jabber_features = g_list_delete_link(jabber_features, feature); 1693 jabber_features = g_list_delete_link(jabber_features, feature);
1622 break; 1694 break;
1623 } 1695 }
1696 }
1697 }
1698
1699 static void jabber_features_destroy(void)
1700 {
1701 while (jabber_features) {
1702 JabberFeature *feature = jabber_features->data;
1703 g_free(feature->namespace);
1704 g_free(feature);
1705 jabber_features = g_list_remove_link(jabber_features, jabber_features);
1706 }
1707 }
1708
1709 void jabber_add_identity(const gchar *category, const gchar *type, const gchar *lang, const gchar *name) {
1710 GList *identity;
1711 JabberIdentity *ident;
1712 /* both required according to XEP-0030 */
1713 g_return_if_fail(category != NULL);
1714 g_return_if_fail(type != NULL);
1715
1716 for(identity = jabber_identities; identity; identity = identity->next) {
1717 JabberIdentity *ident = (JabberIdentity*)identity->data;
1718 if (!strcmp(ident->category, category) &&
1719 !strcmp(ident->type, type) &&
1720 ((!ident->lang && !lang) || (ident->lang && lang && !strcmp(ident->lang, lang)))) {
1721 return;
1722 }
1723 }
1724
1725 ident = g_new0(JabberIdentity, 1);
1726 ident->category = g_strdup(category);
1727 ident->type = g_strdup(type);
1728 ident->lang = g_strdup(lang);
1729 ident->name = g_strdup(name);
1730 jabber_identities = g_list_append(jabber_identities, ident);
1731 }
1732
1733 static void jabber_identities_destroy(void)
1734 {
1735 while (jabber_identities) {
1736 JabberIdentity *id = jabber_identities->data;
1737 g_free(id->category);
1738 g_free(id->type);
1739 g_free(id->lang);
1740 g_free(id->name);
1741 g_free(id);
1742 jabber_identities = g_list_remove_link(jabber_identities, jabber_identities);
1624 } 1743 }
1625 } 1744 }
1626 1745
1627 const char *jabber_list_icon(PurpleAccount *a, PurpleBuddy *b) 1746 const char *jabber_list_icon(PurpleAccount *a, PurpleBuddy *b)
1628 { 1747 {
2673 PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS, 2792 PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS,
2674 "prpl-jabber", jabber_cmd_buzz, 2793 "prpl-jabber", jabber_cmd_buzz,
2675 _("buzz: Buzz a user to get their attention"), NULL); 2794 _("buzz: Buzz a user to get their attention"), NULL);
2676 } 2795 }
2677 2796
2797 /* IPC functions */
2798
2799 /**
2800 * IPC function for determining if a contact supports a certain feature.
2801 *
2802 * @param account The PurpleAccount
2803 * @param jid The full JID of the contact.
2804 * @param feature The feature's namespace.
2805 *
2806 * @return TRUE if supports feature; else FALSE.
2807 */
2808 static gboolean
2809 jabber_ipc_contact_has_feature(PurpleAccount *account, const gchar *jid,
2810 const gchar *feature)
2811 {
2812 PurpleConnection *gc = purple_account_get_connection(account);
2813 JabberStream *js;
2814 JabberBuddy *jb;
2815 JabberBuddyResource *jbr;
2816 gchar *resource;
2817
2818 if (!purple_account_is_connected(account))
2819 return FALSE;
2820 js = gc->proto_data;
2821
2822 if (!(resource = jabber_get_resource(jid)) ||
2823 !(jb = jabber_buddy_find(js, jid, FALSE)) ||
2824 !(jbr = jabber_buddy_find_resource(jb, resource))) {
2825 g_free(resource);
2826 return FALSE;
2827 }
2828
2829 g_free(resource);
2830
2831 return jabber_resource_has_capability(jbr, feature);
2832 }
2833
2834 static void
2835 jabber_ipc_add_feature(const gchar *feature)
2836 {
2837 if (!feature)
2838 return;
2839 jabber_add_feature(feature, 0);
2840
2841 /* send presence with new caps info for all connected accounts */
2842 jabber_caps_broadcast_change();
2843 }
2844
2678 void 2845 void
2679 jabber_init_plugin(PurplePlugin *plugin) 2846 jabber_init_plugin(PurplePlugin *plugin)
2680 { 2847 {
2681 my_protocol = plugin; 2848 my_protocol = plugin;
2682 } 2849
2850 jabber_add_identity("client", "pc", NULL, PACKAGE);
2851
2852 /* initialize jabber_features list */
2853 jabber_add_feature("jabber:iq:last", 0);
2854 jabber_add_feature("jabber:iq:oob", 0);
2855 jabber_add_feature("jabber:iq:time", 0);
2856 jabber_add_feature("urn:xmpp:time", 0);
2857 jabber_add_feature("jabber:iq:version", 0);
2858 jabber_add_feature("jabber:x:conference", 0);
2859 jabber_add_feature("http://jabber.org/protocol/bytestreams", 0);
2860 jabber_add_feature("http://jabber.org/protocol/caps", 0);
2861 jabber_add_feature("http://jabber.org/protocol/chatstates", 0);
2862 jabber_add_feature("http://jabber.org/protocol/disco#info", 0);
2863 jabber_add_feature("http://jabber.org/protocol/disco#items", 0);
2864 #if 0
2865 jabber_add_feature("http://jabber.org/protocol/ibb", 0);
2866 #endif
2867 jabber_add_feature("http://jabber.org/protocol/muc", 0);
2868 jabber_add_feature("http://jabber.org/protocol/muc#user", 0);
2869 jabber_add_feature("http://jabber.org/protocol/si", 0);
2870 jabber_add_feature("http://jabber.org/protocol/si/profile/file-transfer", 0);
2871 jabber_add_feature("http://jabber.org/protocol/xhtml-im", 0);
2872 jabber_add_feature("urn:xmpp:ping", 0);
2873
2874 /* IPC functions */
2875 purple_plugin_ipc_register(plugin, "contact_has_feature", PURPLE_CALLBACK(jabber_ipc_contact_has_feature),
2876 purple_marshal_BOOLEAN__POINTER_POINTER_POINTER,
2877 purple_value_new(PURPLE_TYPE_BOOLEAN), 3,
2878 purple_value_new(PURPLE_TYPE_SUBTYPE, PURPLE_SUBTYPE_ACCOUNT),
2879 purple_value_new(PURPLE_TYPE_STRING),
2880 purple_value_new(PURPLE_TYPE_STRING));
2881 purple_plugin_ipc_register(plugin, "add_feature", PURPLE_CALLBACK(jabber_ipc_add_feature),
2882 purple_marshal_VOID__POINTER,
2883 NULL, 1,
2884 purple_value_new(PURPLE_TYPE_STRING));
2885 }
2886
2887 void
2888 jabber_uninit_plugin(void)
2889 {
2890 jabber_features_destroy();
2891 jabber_identities_destroy();
2892 }