diff src/protocols/qq/qq.c @ 13870:983fd420e86b

[gaim-migrate @ 16340] Performed minor cleanup of the OpenQ codebase and patched it into the Gaim trunk as a prpl, providing basic QQ functionality. committer: Tailor Script <tailor@pidgin.im>
author Mark Huetsch <markhuetsch>
date Mon, 26 Jun 2006 02:58:54 +0000
parents
children 16102b9c5c4a
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/protocols/qq/qq.c	Mon Jun 26 02:58:54 2006 +0000
@@ -0,0 +1,1195 @@
+/**
+ * @file qq.c The QQ2003C protocol plugin
+ *
+ * gaim
+ *
+ * Copyright (C) 2004 Puzzlebird
+ *
+ * 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"
+
+#ifdef _WIN32
+#define random rand
+#endif
+
+#include "debug.h"
+#include "notify.h"
+#include "prefs.h"
+#include "request.h"
+#include "accountopt.h"
+#include "prpl.h"
+#include "gtkroomlist.h"
+#include "gtklog.h"
+#include "server.h"
+#include "util.h" 		/* GaimMenuAction, gaim_menu_action_new, gaim2beta2, gfhuang*/
+
+#include "utils.h"
+#include "buddy_info.h"
+#include "buddy_opt.h"
+#include "buddy_status.h"
+#include "char_conv.h"
+#include "group_find.h"		/* qq_group_find_member_by_channel_and_nickname */
+#include "group_im.h"		/* qq_send_packet_group_im */
+#include "group_info.h"		/* qq_send_cmd_group_get_group_info */
+#include "group_join.h"		/* qq_group_join */
+#include "group_opt.h"		/* qq_group_manage_members */
+#include "group.h"		/* chat_info, etc */
+#include "header_info.h"	/* qq_get_cmd_desc */
+#include "im.h"
+#include "infodlg.h"
+#include "keep_alive.h"
+#include "ip_location.h"	/* qq_ip_get_location */
+#include "login_logout.h"
+#include "qq_proxy.h"		/* qq_connect, qq_disconnect */
+#include "send_core.h"
+#include "qq.h"
+#include "send_file.h"
+#include "version.h"
+
+#define OPENQ_AUTHOR            "Puzzlebird"
+#define OPENQ_WEBSITE            "http://openq.sourceforge.net"
+#define QQ_TCP_QUERY_PORT       "8000"
+#define QQ_UDP_PORT             "8000"
+
+const gchar *udp_server_list[] = {
+	"sz.tencent.com",	// 61.144.238.145
+	"sz2.tencent.com",	// 61.144.238.146
+	"sz3.tencent.com",	// 202.104.129.251
+	"sz4.tencent.com",	// 202.104.129.254
+	"sz5.tencent.com",	// 61.141.194.203
+	"sz6.tencent.com",	// 202.104.129.252
+	"sz7.tencent.com",	// 202.104.129.253
+	"202.96.170.64",
+	"64.144.238.155",
+	"202.104.129.254"
+};
+const gint udp_server_amount = (sizeof(udp_server_list) / sizeof(udp_server_list[0]));
+
+
+const gchar *tcp_server_list[] = {
+	"tcpconn.tencent.com",	// 218.17.209.23
+	"tcpconn2.tencent.com",	// 218.18.95.153
+	"tcpconn3.tencent.com",	// 218.17.209.23
+	"tcpconn4.tencent.com",	// 218.18.95.153
+};
+const gint tcp_server_amount = (sizeof(tcp_server_list) / sizeof(tcp_server_list[0]));
+
+/*********** Prototypes ***********/
+
+static void _qq_login(GaimAccount * account);
+static void _qq_close(GaimConnection * gc);
+static const gchar *_qq_list_icon(GaimAccount * a, GaimBuddy * b);
+static gchar *_qq_status_text(GaimBuddy *b);
+static void _qq_tooltip_text(GaimBuddy *b, GString *tooltip, gboolean full);
+static void _qq_list_emblems(GaimBuddy * b, const char **se, const char **sw, const char **nw, const char **ne);
+static GList *_qq_away_states(GaimAccount *ga);
+static void _qq_set_away(GaimAccount *account, GaimStatus *status);
+static gint _qq_send_im(GaimConnection * gc, const gchar * who, const gchar * message, GaimMessageFlags flags); 
+static int _qq_chat_send(GaimConnection *gc, int channel, const char *message, GaimMessageFlags flags);
+static void _qq_get_info(GaimConnection * gc, const gchar * who);
+static void _qq_menu_get_my_info(GaimPluginAction * action);
+//static void _qq_menu_block_buddy(GaimBlistNode * node);
+static void _qq_menu_show_login_info(GaimPluginAction * action);
+static void _qq_menu_show_about(GaimPluginAction * action);
+static void _qq_menu_any_cmd_send_cb(GaimConnection * gc, GaimRequestFields * fields);
+static void _qq_menu_any_cmd(GaimPluginAction * action);
+static void _qq_menu_locate_ip_cb(GaimConnection * gc, GaimRequestFields * fields);
+static void _qq_menu_locate_ip(GaimPluginAction *action);
+static void _qq_menu_search_or_add_permanent_group(GaimPluginAction * action);
+static void _qq_menu_create_permanent_group(GaimPluginAction * action);
+static void _qq_menu_unsubscribe_group(GaimBlistNode * node);
+static void _qq_menu_manage_group(GaimBlistNode * node);
+static void _qq_menu_show_system_message(GaimPluginAction *action);
+//static void _qq_menu_send_file(GaimBlistNode * node, gpointer ignored);
+static GList *_qq_actions(GaimPlugin * plugin, gpointer context);
+static GList *_qq_chat_menu(GaimBlistNode *node);
+static GList *_qq_buddy_menu(GaimBlistNode * node);
+static void _qq_keep_alive(GaimConnection * gc);
+static void _qq_get_chat_buddy_info(GaimConnection * gc, gint channel, const gchar * who);
+static gchar *_qq_get_chat_buddy_real_name(GaimConnection * gc, gint channel, const gchar * who);
+//static GaimPluginPrefFrame *get_plugin_pref_frame(GaimPlugin * plugin);
+static void init_plugin(GaimPlugin * plugin);
+
+static void _qq_login(GaimAccount * account)
+{
+	const gchar *qq_server, *qq_port;
+	qq_data *qd;
+	GaimConnection *gc;
+	GaimPresence *presence;				//gfhuang
+	gboolean login_hidden, use_tcp;
+
+	g_return_if_fail(account != NULL);
+
+	gc = gaim_account_get_connection(account);
+	g_return_if_fail(gc != NULL);
+
+	gc->flags |= GAIM_CONNECTION_HTML | GAIM_CONNECTION_NO_BGCOLOR | GAIM_CONNECTION_AUTO_RESP;
+
+	qd = g_new0(qq_data, 1);
+	gc->proto_data = qd;
+
+	qq_server = gaim_account_get_string(account, "server", NULL);
+	qq_port = gaim_account_get_string(account, "port", NULL);
+	use_tcp = gaim_account_get_bool(account, "use_tcp", FALSE);
+//	login_hidden = gaim_account_get_bool(account, "hidden", FALSE);		gfhuang
+        presence = gaim_account_get_presence(account);
+        login_hidden = gaim_presence_is_status_primitive_active(presence, GAIM_STATUS_INVISIBLE);
+
+	qd->use_tcp = use_tcp;
+
+	if (login_hidden) {
+		qd->login_mode = QQ_LOGIN_MODE_HIDDEN;
+		gaim_debug(GAIM_DEBUG_INFO, "QQ", "Login in hidden mode\n");
+	}
+	else {
+		qd->login_mode = QQ_LOGIN_MODE_NORMAL;
+		gaim_debug(GAIM_DEBUG_INFO, "QQ", "Login in normal mode\n");
+	}
+
+	if (qq_server == NULL || strlen(qq_server) == 0)
+		qq_server = use_tcp ?
+		    tcp_server_list[random() % tcp_server_amount] : udp_server_list[random() % udp_server_amount];
+
+	if (qq_port == NULL || strtol(qq_port, NULL, 10) == 0)
+		qq_port = use_tcp ? QQ_TCP_QUERY_PORT : QQ_UDP_PORT;
+
+	gaim_connection_update_progress(gc, _("Connecting"), 0, QQ_CONNECT_STEPS);
+
+	if (qq_connect(account, qq_server, strtol(qq_port, NULL, 10), use_tcp, FALSE) < 0)
+		gaim_connection_error(gc, _("Unable to connect."));
+}
+
+/* directly goes for qq_disconnect */
+static void _qq_close(GaimConnection * gc)
+{
+	g_return_if_fail(gc != NULL);
+	qq_disconnect(gc);
+}
+
+/* returns the icon name for a buddy or protocol */
+static const gchar *_qq_list_icon(GaimAccount * a, GaimBuddy * b)
+{
+	gchar *filename;
+	qq_buddy *q_bud;
+	gchar icon_suffix;
+
+	/* do not use g_return_val_if_fail, as it is not assertion */
+	if (b == NULL || b->proto_data == NULL)
+		return "qq";
+
+	q_bud = (qq_buddy *) b->proto_data;
+	icon_suffix = get_suffix_from_status(q_bud->status);
+	filename = get_icon_name(q_bud->icon / 3 + 1, icon_suffix);
+
+	return filename;
+}
+
+
+/* a short status text beside buddy icon*/
+static gchar *_qq_status_text(GaimBuddy *b)
+{
+	qq_buddy *q_bud;
+//	gboolean show_info;
+	GString *status;
+	gchar *ret;
+
+//	show_info = gaim_prefs_get_bool("/plugins/prpl/qq/show_status_by_icon");
+//	if (!show_info)
+//		return NULL;
+
+	q_bud = (qq_buddy *) b->proto_data;
+	if (q_bud == NULL)
+		return NULL;
+
+	status = g_string_new("");
+
+	//by gfhuang
+	switch(q_bud->status) {
+	case QQ_BUDDY_OFFLINE:
+		g_string_append(status, "My Offline");
+		break;
+	case QQ_BUDDY_ONLINE_NORMAL:
+		return NULL;
+		break;
+	case QQ_BUDDY_ONLINE_OFFLINE:
+		g_string_append(status, "Online Offline");
+		break;
+	case QQ_BUDDY_ONLINE_AWAY:
+		g_string_append(status, "Away");
+		break;
+	case QQ_BUDDY_ONLINE_INVISIBLE:
+		g_string_append(status, "Invisible");
+		break;
+	default:
+		g_string_printf(status, "Unknown-%d", q_bud->status);
+	}	
+	/*
+	switch (q_bud->gender) {
+	case QQ_BUDDY_GENDER_GG:
+		g_string_append(status, " GG");
+		break;
+	case QQ_BUDDY_GENDER_MM:
+		g_string_append(status, " MM");
+		break;
+	case QQ_BUDDY_GENDER_UNKNOWN:
+		g_string_append(status, "^_*");
+		break;
+	default:
+		g_string_append(status, "^_^");
+	}			
+
+	g_string_append_printf(status, " Age: %d", q_bud->age);
+	g_string_append_printf(status, " Client: %04x", q_bud->client_version);
+	*/
+//	having_video = q_bud->comm_flag & QQ_COMM_FLAG_VIDEO;
+//	if (having_video)
+//		g_string_append(status, " (video)");
+
+	ret = status->str;
+	g_string_free(status, FALSE);
+
+	return ret;
+}
+
+
+/* a floating text when mouse is on the icon, show connection status here */
+static void _qq_tooltip_text(GaimBuddy *b, GString *tooltip, gboolean full)
+{
+	qq_buddy *q_bud;
+	gchar *country, *country_utf8, *city, *city_utf8;
+	guint32 ip_value;
+	gchar *ip_str;
+
+	g_return_if_fail(b != NULL);
+
+	q_bud = (qq_buddy *) b->proto_data;
+	g_return_if_fail(q_bud != NULL);
+
+//	if (is_online(q_bud->status)) //disable by gfhuang
+	{
+		ip_value = ntohl(*(guint32 *) (q_bud->ip));
+//		tooltip = g_string_new("");                     beta2, gfhuang
+		if (qq_ip_get_location(ip_value, &country, &city)) {
+			country_utf8 = qq_to_utf8(country, QQ_CHARSET_DEFAULT);
+			city_utf8 = qq_to_utf8(city, QQ_CHARSET_DEFAULT);
+			g_string_append_printf(tooltip, "\n%s, %s", country_utf8, city_utf8);
+			g_free(country);
+			g_free(city);
+			g_free(country_utf8);
+			g_free(city_utf8);
+		}
+		//memory leak fixed by gfhuang
+		ip_str = gen_ip_str(q_bud->ip);
+		g_string_append_printf(tooltip, "\n<b>%s Address:</b> %s:%d", (q_bud->comm_flag & QQ_COMM_FLAG_TCP_MODE)
+				       ? "TCP" : "UDP", ip_str, q_bud->port);
+		g_free(ip_str);
+		//added by gfhuang
+		g_string_append_printf(tooltip, "\n<b>Age:</b> %d", q_bud->age);
+        	switch (q_bud->gender) {
+	        case QQ_BUDDY_GENDER_GG:
+                	g_string_append(tooltip, "\n<b>Gender:</b> GG");
+        	        break;
+	        case QQ_BUDDY_GENDER_MM:
+                	g_string_append(tooltip, "\n<b>Gender:</b> MM");
+        	        break;
+	        case QQ_BUDDY_GENDER_UNKNOWN:
+                	g_string_append(tooltip, "\n<b>Gender:</b> UNKNOWN");
+        	        break;
+	        default:
+                	g_string_append_printf(tooltip, "\n<b>Gender:</b> ERROR(%d)", q_bud->gender);
+	        }                       /* switch gender */
+		g_string_append_printf(tooltip, "\n<b>Flag:</b> %01x", q_bud->flag1);
+		g_string_append_printf(tooltip, "\n<b>CommFlag:</b> %01x", q_bud->comm_flag);
+	        g_string_append_printf(tooltip, "\n<b>Client:</b> %04x", q_bud->client_version);
+	}
+}
+
+/* we can show tiny icons on the four corners of buddy icon, */
+static void _qq_list_emblems(GaimBuddy * b, const char **se, const char **sw, const char **nw, const char **ne) {   //add const by gfhuang
+	// each char ** are refering to filename in pixmaps/gaim/status/default/*png
+
+	qq_buddy *q_bud = b->proto_data;
+//        GaimPresence *presence;
+        const char *emblems[4] = { NULL, NULL, NULL, NULL };
+        int i = 0;
+
+/*        presence = gaim_buddy_get_presence(b);
+
+        if (!gaim_presence_is_online(presence))
+                emblems[i++] = "offline";
+        else if (gaim_presence_is_status_active(presence, "busy") ||
+                         gaim_presence_is_status_active(presence, "phone"))
+                emblems[i++] = "occupied";
+        else if (!gaim_presence_is_available(presence))
+                emblems[i++] = "away";
+*/
+
+        if (q_bud == NULL)
+        {
+                emblems[0] = "offline";
+        }
+        else
+        {
+		if (q_bud->comm_flag & QQ_COMM_FLAG_QQ_MEMBER)
+			emblems[i++] = "qq_member";
+                if (q_bud->comm_flag & QQ_COMM_FLAG_BIND_MOBILE)
+                        emblems[i++] = "wireless";
+		if (q_bud->comm_flag & QQ_COMM_FLAG_VIDEO)
+			emblems[i++] = "video";
+
+
+//                if (!(user->list_op & (1 << MSN_LIST_RL)))
+//                        emblems[i++] = "nr";
+        }
+
+        *se = emblems[0];
+        *sw = emblems[1];
+        *nw = emblems[2];
+        *ne = emblems[3];
+
+	return;
+}
+
+/* QQ away status (used to initiate QQ away packet) */
+//Rewritten by gfhuang
+static GList *_qq_away_states(GaimAccount *ga) 
+{
+	GaimStatusType *status;
+	GList *types = NULL;
+
+	status = gaim_status_type_new_full(GAIM_STATUS_AVAILABLE,
+			"available", _("QQ: Available"), FALSE, TRUE, FALSE);
+	types = g_list_append(types, status);
+
+        status = gaim_status_type_new_full(GAIM_STATUS_AWAY, 
+			"away", _("QQ: Away"), FALSE, TRUE, FALSE);
+        types = g_list_append(types, status);
+
+        status = gaim_status_type_new_full(GAIM_STATUS_INVISIBLE, /* HIDDEN, change to gaim2beta2, gfhuang */ 
+			"invisible", _("QQ: Invisible"), FALSE, TRUE, FALSE);
+        types = g_list_append(types, status);
+
+	status = gaim_status_type_new_full(GAIM_STATUS_OFFLINE,
+                        "offline", _("QQ: Offline"), FALSE, TRUE, FALSE);
+        types = g_list_append(types, status);
+
+	return types;
+}
+/*
+GList *_qq_away_states(GaimConnection * gc)
+{
+	GList *m;
+
+	g_return_val_if_fail(gc != NULL, NULL);
+
+	m = NULL;
+	m = g_list_append(m, _("QQ: Available"));
+	m = g_list_append(m, _("QQ: Away"));
+	m = g_list_append(m, _("QQ: Invisible"));
+	m = g_list_append(m, GAIM_AWAY_CUSTOM);       
+	return m;
+}
+*/
+
+
+
+/* initiate QQ away with proper change_status packet */
+//void _qq_set_away(GaimConnection * gc, const char *state, const char *msg)
+static void _qq_set_away(GaimAccount *account, GaimStatus *status)
+{
+	// by gfhuang
+	GaimConnection *gc = gaim_account_get_connection(account);
+	const char *state = gaim_status_get_id(status);
+
+	qq_data *qd;
+	
+
+	g_return_if_fail(gc != NULL && gc->proto_data != NULL);
+
+	qd = (qq_data *) gc->proto_data;
+
+	// by gfhuang
+	if(0 == strcmp(state, "available"))
+		qd->status = QQ_SELF_STATUS_AVAILABLE;
+	else if (0 == strcmp(state, "away"))
+		qd->status = QQ_SELF_STATUS_AWAY;
+	else if (0 == strcmp(state, "invisible"))
+		qd->status = QQ_SELF_STATUS_INVISIBLE;
+	else
+		qd->status = QQ_SELF_STATUS_AVAILABLE;
+
+/*	if (gc->away) {                //disable by gfhuang, 1.2006
+		g_free(gc->away);
+		gc->away = NULL;
+	}
+
+	if (msg) {
+		qd->status = QQ_SELF_STATUS_CUSTOM;
+		gc->away = g_strdup(msg);
+	} else if (state) {
+		gc->away = g_strdup("");
+		if (g_ascii_strcasecmp(state, _("QQ: Available")) == 0)
+			qd->status = QQ_SELF_STATUS_AVAILABLE;
+		else if (g_ascii_strcasecmp(state, _("QQ: Away")) == 0)
+			qd->status = QQ_SELF_STATUS_AWAY;
+		else if (g_ascii_strcasecmp(state, _("QQ: Invisible")) == 0)
+			qd->status = QQ_SELF_STATUS_INVISIBLE;
+		else if (g_ascii_strcasecmp(state, GAIM_AWAY_CUSTOM) == 0) {
+			if (gc->is_idle)
+				qd->status = QQ_SELF_STATUS_IDLE;
+			else
+				qd->status = QQ_SELF_STATUS_AVAILABLE;
+		}
+	} else if (gc->is_idle)
+		qd->status = QQ_SELF_STATUS_IDLE; 
+	else
+		qd->status = QQ_SELF_STATUS_AVAILABLE;
+*/
+	qq_send_packet_change_status(gc);
+}
+
+
+// IMPORTANT: GaimConvImFlags -> GaimMessageFlags    //gfhuang
+/* send an instance msg to a buddy */
+static gint _qq_send_im(GaimConnection * gc, const gchar * who, const gchar * message, GaimMessageFlags flags)
+{
+	gint type, to_uid;
+	gchar *msg, *msg_with_qq_smiley;
+	qq_data *qd;
+
+	g_return_val_if_fail(gc != NULL && gc->proto_data != NULL && who != NULL, -1);
+
+	qd = (qq_data *) gc->proto_data;
+
+	g_return_val_if_fail(strlen(message) <= QQ_MSG_IM_MAX, -E2BIG);
+
+	type = (flags == GAIM_MESSAGE_AUTO_RESP ? QQ_IM_AUTO_REPLY : QQ_IM_TEXT);
+	to_uid = gaim_name_to_uid(who);
+
+	/* if msg is to myself, bypass the network */
+	if (to_uid == qd->uid)
+		serv_got_im(gc, who, message, flags, time(NULL));
+	else {
+		msg = utf8_to_qq(message, QQ_CHARSET_DEFAULT);
+		msg_with_qq_smiley = gaim_smiley_to_qq(msg);
+		qq_send_packet_im(gc, to_uid, msg_with_qq_smiley, type);
+		g_free(msg);
+		g_free(msg_with_qq_smiley);
+	}
+
+	return 1;
+}
+
+/* send a chat msg to a QQ Qun */
+static int _qq_chat_send(GaimConnection *gc, int channel, const char *message, GaimMessageFlags flags/*gfhuang*/)
+{
+	gchar *msg, *msg_with_qq_smiley;
+	qq_group *group;
+
+	g_return_val_if_fail(gc != NULL && message != NULL, -1);
+	g_return_val_if_fail(strlen(message) <= QQ_MSG_IM_MAX, -E2BIG);
+
+	group = qq_group_find_by_channel(gc, channel);
+	g_return_val_if_fail(group != NULL, -1);
+
+	msg = utf8_to_qq(message, QQ_CHARSET_DEFAULT);
+	msg_with_qq_smiley = gaim_smiley_to_qq(msg);
+	qq_send_packet_group_im(gc, group, msg_with_qq_smiley);
+	g_free(msg);
+	g_free(msg_with_qq_smiley);
+
+	return 1;
+}
+
+/* send packet to get who's detailed information */
+static void _qq_get_info(GaimConnection * gc, const gchar * who)
+{
+	guint32 uid;
+	qq_data *qd;
+
+	g_return_if_fail(gc != NULL && gc->proto_data != NULL);
+	qd = gc->proto_data;
+	uid = gaim_name_to_uid(who);
+
+	if (uid <= 0) {
+		gaim_debug(GAIM_DEBUG_ERROR, "QQ", "Not valid QQid: %s\n", who);
+		gaim_notify_error(gc, NULL, _("Invalid name, please input in qq-xxxxxxxx format"), NULL);
+		return;
+	}
+
+	qq_send_packet_get_info(gc, uid, TRUE);	/* need to show up info window */
+}
+
+/* get my own information */
+static void _qq_menu_get_my_info(GaimPluginAction * action)
+{
+	GaimConnection *gc = (GaimConnection *) action->context;
+	qq_data *qd;
+
+	g_return_if_fail(gc != NULL && gc->proto_data != NULL);
+
+	qd = (qq_data *) gc->proto_data;
+	_qq_get_info(gc, uid_to_gaim_name(qd->uid));
+}
+
+/* remove a buddy from my list and remove myself from his list */
+/* TODO: re-enable this
+static void _qq_menu_block_buddy(GaimBlistNode * node)
+{
+	guint32 uid;
+	gc_and_uid *g;
+	GaimBuddy *buddy;
+	GaimConnection *gc;
+//	const gchar *who = param_who;	gfhuang
+	const gchar *who;
+
+	g_return_if_fail(GAIM_BLIST_NODE_IS_BUDDY(node));
+
+	buddy = (GaimBuddy *) node;
+	gc = gaim_account_get_connection(buddy->account);
+	who = buddy->name;
+	g_return_if_fail(gc != NULL && who != NULL);
+
+	uid = gaim_name_to_uid(who);
+	g_return_if_fail(uid > 0);
+
+	g = g_new0(gc_and_uid, 1);
+	g->gc = gc;
+	g->uid = uid;
+
+	gaim_request_action(gc, _("Block Buddy"),
+			    _("Are you sure to block this buddy?"), NULL,
+			    1, g, 2,
+			    _("Cancel"),
+			    G_CALLBACK(qq_do_nothing_with_gc_and_uid),
+			    _("Block"), G_CALLBACK(qq_block_buddy_with_gc_and_uid));
+}
+*/
+
+/* show a brief summary of what we get from login packet */
+static void _qq_menu_show_login_info(GaimPluginAction * action)
+{
+	GaimConnection *gc = (GaimConnection *) action->context;
+	qq_data *qd;
+	GString *info;
+
+	g_return_if_fail(gc != NULL && gc->proto_data != NULL);
+
+	qd = (qq_data *) gc->proto_data;
+	info = g_string_new("<html><body>\n");
+
+	g_string_append_printf(info, _("<b>Current Online</b>: %d<br>\n"), qd->all_online);
+	g_string_append_printf(info, _("<b>Last Refresh</b>: %s<br>\n"), ctime(&qd->last_get_online));
+
+	g_string_append(info, "<hr>\n");
+
+	g_string_append_printf(info, _("<b>Connection Mode</b>: %s<br>\n"), qd->use_tcp ? "TCP" : "UDP");
+	g_string_append_printf(info, _("<b>Server IP</b>: %s: %d<br>\n"), qd->server_ip, qd->server_port);
+	g_string_append_printf(info, _("<b>My Public IP</b>: %s<br>\n"), qd->my_ip);
+
+	g_string_append(info, "<hr>\n");
+	g_string_append(info, "<i>Information below may not be accurate</i><br>\n");
+
+	g_string_append_printf(info, _("<b>Login Time</b>: %s<br>\n"), ctime(&qd->login_time));
+	g_string_append_printf(info, _("<b>Last Login IP</b>: %s<br>\n"), qd->last_login_ip);
+	g_string_append_printf(info, _("<b>Last Login Time</b>: %s\n"), ctime(&qd->last_login_time));
+
+	g_string_append(info, "</body></html>");
+
+	gaim_notify_formatted(gc, NULL, _("Login Information"), NULL, info->str, NULL, NULL);
+
+	g_string_free(info, TRUE);
+}
+
+/* show about page about QQ plugin */
+static void _qq_menu_show_about(GaimPluginAction * action)
+{
+	GaimConnection *gc = (GaimConnection *) action->context;
+	qq_data *qd;
+	GString *info;
+	gchar *head;
+
+	g_return_if_fail(gc != NULL && gc->proto_data != NULL);
+
+	qd = (qq_data *) gc->proto_data;
+	info = g_string_new("<html><body>\n");
+
+	g_string_append_printf(info, _("<b>Author</b> : %s<br>\n"), OPENQ_AUTHOR);
+	g_string_append(info, "Copyright (c) 2004.  All rights reserved.<br><br>\n");
+
+	g_string_append(info, _("<p><b>Code Contributors</b><br>\n"));
+	g_string_append(info, "gfhuang   : patches for gaim 2.0.0beta2<br>\n");
+	g_string_append(info, "henryouly : file transfer, udp sock5 proxy and qq_show<br>\n");
+	g_string_append(info, "arfankai  : fixed bugs in char_conv.c<br>\n");
+	g_string_append(info, "rakescar  : provided filter for HTML tag<br>\n");
+	g_string_append(info, "yyw       : improved performance on PPC linux<br>\n");
+	g_string_append(info, "lvxiang   : provided ip to location original code<br><br>\n");
+
+	g_string_append(info, _("<p><b>Acknowledgement</b><br>\n"));
+	g_string_append(info, "Shufeng Tan : http://sf.net/projects/perl-oicq<br>\n");
+	g_string_append(info, "Jeff Ye : http://www.sinomac.com<br>\n");
+	g_string_append(info, "Hu Zheng : http://forlinux.yeah.net<br><br>\n");
+
+	g_string_append(info, "<p>And, my parents...\n");
+
+	g_string_append(info, "</body></html>");
+
+	head = g_strdup_printf("About QQ Plugin Ver %s", VERSION);
+	gaim_notify_formatted(gc, NULL, head, NULL, info->str, NULL, NULL);
+
+	g_free(head);
+	g_string_free(info, TRUE);
+}
+
+/* callback of sending any command to QQ server */
+static void _qq_menu_any_cmd_send_cb(GaimConnection * gc, GaimRequestFields * fields)
+{
+	GList *groups, *flds;
+	GaimRequestField *field;
+	const gchar *id, *value;
+	gchar *cmd_str, *data_str, **segments;
+	guint16 cmd;
+	guint8 *data;
+	gint i, data_len;
+
+	cmd_str = NULL;
+	data_str = NULL;
+	cmd = 0x00;
+	data = NULL;
+	data_len = 0;
+
+	for (groups = gaim_request_fields_get_groups(fields); groups; groups = groups->next) {
+		for (flds = gaim_request_field_group_get_fields(groups->data); flds; flds = flds->next) {
+			field = flds->data;
+			id = gaim_request_field_get_id(field);
+			value = gaim_request_field_string_get_value(field);
+
+			if (!g_ascii_strcasecmp(id, "cmd"))
+				cmd_str = g_strdup(value);
+			else if (!g_ascii_strcasecmp(id, "data"))
+				data_str = g_strdup(value);
+		}
+	}
+
+	if (cmd_str != NULL)
+		cmd = (guint16) strtol(cmd_str, NULL, 16);
+
+	if (data_str != NULL) {
+		if (NULL == (segments = split_data(data_str, strlen(data_str), ",", 0))) {
+			g_free(cmd_str);
+			g_free(data_str);
+			return;
+		}
+		for (data_len = 0; segments[data_len] != NULL; data_len++) {;
+		}
+		data = g_newa(guint8, data_len);
+		for (i = 0; i < data_len; i++)
+			data[i] = (guint8) strtol(segments[i], NULL, 16);
+		g_strfreev(segments);
+	}
+
+	if (cmd && data_len > 0) {
+		gaim_debug(GAIM_DEBUG_INFO, "QQ",
+			   "Send Any cmd: %s, data dump\n%s", qq_get_cmd_desc(cmd), hex_dump_to_str(data, data_len));
+		qq_send_cmd(gc, cmd, TRUE, 0, TRUE, data, data_len);
+	}
+
+	g_free(cmd_str);
+	g_free(data_str);
+}				
+
+/* send any command with data to QQ server, for testing and debuggin only */
+static void _qq_menu_any_cmd(GaimPluginAction * action)
+{
+	GaimConnection *gc = (GaimConnection *) action->context;
+	qq_data *qd;
+	const char *tips;
+	GaimRequestField *field;
+	GaimRequestFields *fields;
+	GaimRequestFieldGroup *group;
+
+	g_return_if_fail(gc != NULL && gc->proto_data != NULL);
+	qd = (qq_data *) gc->proto_data;
+
+	tips = _("Separate the value with \",\"\nAllow \"0x\" before each value");
+	fields = gaim_request_fields_new();
+	group = gaim_request_field_group_new(NULL);
+	gaim_request_fields_add_group(fields, group);
+
+	/* sample: 0x22 */
+	field = gaim_request_field_string_new("cmd", _("CMD Code"), NULL, FALSE);
+	gaim_request_field_group_add_field(group, field);
+	/* sample: 0x00,0x15,0xAB */
+	/*     or: 00,15,AB */
+	/* the delimit is ",", allow 0x before the value */
+	field = gaim_request_field_string_new("data", _("Raw Data"), NULL, FALSE);
+	gaim_request_field_group_add_field(group, field);
+
+	gaim_request_fields(gc, _("QQ Any Command"),
+			    _("Send Arbitrary Command"), tips, fields,
+			    _("Send"), G_CALLBACK(_qq_menu_any_cmd_send_cb), _("Cancel"), NULL, gc);
+}
+
+/* added by gfhuang */
+static void _qq_menu_locate_ip_cb(GaimConnection * gc, GaimRequestFields * fields)
+{
+        GList *groups, *flds;
+        GaimRequestField *field;
+        const gchar *id, *value;
+        gchar *ip_str = NULL, *ip_dupstr = NULL;
+	guint8 *ip;
+        gchar *country, *country_utf8, *city, *city_utf8;
+        guint32 ip_value;
+
+        for (groups = gaim_request_fields_get_groups(fields); groups && !ip_str; groups = groups->next) {
+                for (flds = gaim_request_field_group_get_fields(groups->data); flds && !ip_str; flds = flds->next) {
+                        field = flds->data;
+                        id = gaim_request_field_get_id(field);
+                        value = gaim_request_field_string_get_value(field);
+
+                        if (!g_ascii_strcasecmp(id, "ip")) {
+                                ip_str = g_strdup(value);
+				break;
+			}
+                }
+        }
+	
+	if(ip_str) {
+		ip = str_ip_gen(ip_str);
+		ip_dupstr = gen_ip_str(ip);
+        
+		ip_value = ntohl(*(guint32 *)ip);
+        	if (qq_ip_get_location(ip_value, &country, &city)) {
+                        country_utf8 = qq_to_utf8(country, QQ_CHARSET_DEFAULT);
+                        city_utf8 = qq_to_utf8(city, QQ_CHARSET_DEFAULT);
+			gaim_notify_info(gc, ip_dupstr, country_utf8, city_utf8);
+                        g_free(country);
+                        g_free(city);
+                        g_free(country_utf8);
+                        g_free(city_utf8);
+        	}
+		else 
+			gaim_notify_info(gc, ip_dupstr, "IP not found", NULL);
+		g_free(ip);
+		g_free(ip_dupstr);
+		g_free(ip_str);
+	}
+}
+
+/* added by gfhuang */
+static void _qq_menu_locate_ip(GaimPluginAction *action)
+{
+        GaimConnection *gc = (GaimConnection *) action->context;
+        GaimRequestField *field;
+        GaimRequestFields *fields;
+        GaimRequestFieldGroup *group;
+
+        g_return_if_fail(gc != NULL);
+
+        fields = gaim_request_fields_new();
+        group = gaim_request_field_group_new(NULL);
+        gaim_request_fields_add_group(fields, group);
+        
+	field = gaim_request_field_string_new("ip", _("IP Address"), NULL, FALSE);
+        gaim_request_field_group_add_field(group, field);
+        
+	gaim_request_fields(gc, _("Locate an IP"),
+			_("Locate an IP address"), NULL, fields,
+			 _("Check"), G_CALLBACK(_qq_menu_locate_ip_cb), _("Cancel"), NULL, gc);
+}
+
+static void _qq_menu_search_or_add_permanent_group(GaimPluginAction * action)
+{
+	gaim_gtk_roomlist_dialog_show();
+}
+
+static void _qq_menu_create_permanent_group(GaimPluginAction * action)
+{
+	GaimConnection *gc = (GaimConnection *) action->context;
+	g_return_if_fail(gc != NULL);
+	gaim_request_input(gc, _("Create QQ Qun"),
+			   _("Input Qun name here"),
+			   _("Only QQ member can create permanent Qun"),
+			   "OpenQ", FALSE, FALSE, NULL,
+			   _("Create"), G_CALLBACK(qq_group_create_with_name), _("Cancel"), NULL, gc);
+}
+
+static void _qq_menu_unsubscribe_group(GaimBlistNode * node) // by gfhuang, gpointer param_components)
+{
+//	GaimBuddy *buddy;		by gfhuang
+	GaimChat *chat = (GaimChat *)node;
+	GaimConnection *gc = gaim_account_get_connection(chat->account);
+	GHashTable *components = chat -> components;
+//	GHashTable *components = (GHashTable *) param_components;
+
+//	g_return_if_fail(GAIM_BLIST_NODE_IS_BUDDY(node));	bug! found by gfhuang
+	g_return_if_fail(GAIM_BLIST_NODE_IS_CHAT(node));
+
+//	buddy = (GaimBuddy *) node;
+//	gc = gaim_account_get_connection(buddy->account);
+
+	g_return_if_fail(gc != NULL && components != NULL);
+	qq_group_exit(gc, components);
+}
+
+static void _qq_menu_manage_group(GaimBlistNode * node) // by gfhuang, gpointer param_components)
+{
+//	GaimBuddy *buddy;		by gfhuang
+	GaimChat *chat = (GaimChat *)node;
+	GaimConnection *gc = gaim_account_get_connection(chat->account);
+	GHashTable *components = chat -> components;
+//	GHashTable *components = (GHashTable *) param_components;
+
+//	g_return_if_fail(GAIM_BLIST_NODE_IS_BUDDY(node));	bug! found by gfhuang
+	g_return_if_fail(GAIM_BLIST_NODE_IS_CHAT(node));
+
+//	buddy = (GaimBuddy *) node;
+//	gc = gaim_account_get_connection(buddy->account);
+
+	g_return_if_fail(gc != NULL && components != NULL);
+	qq_group_manage_group(gc, components);
+}
+
+static void _qq_menu_show_system_message(GaimPluginAction *action)
+{
+	GaimConnection *gc = (GaimConnection *) action->context;
+	g_return_if_fail ( gc != NULL );
+	gaim_gtk_log_show(GAIM_LOG_IM, "systemim", gaim_connection_get_account(gc));
+}
+
+/* TODO: re-enable this
+static void _qq_menu_send_file(GaimBlistNode * node, gpointer ignored)
+{
+	GaimBuddy *buddy;
+	GaimConnection *gc;
+	qq_buddy *q_bud;
+
+	g_return_if_fail (GAIM_BLIST_NODE_IS_BUDDY (node));
+	buddy = (GaimBuddy *) node;
+	q_bud = (qq_buddy *) buddy->proto_data;
+//	if (is_online (q_bud->status)) {
+	gc = gaim_account_get_connection (buddy->account);
+	g_return_if_fail (gc != NULL && gc->proto_data != NULL);
+	qq_send_file(gc, buddy->name, NULL);
+//	}
+}
+*/
+
+/* protocol related menus */
+static GList *_qq_actions(GaimPlugin * plugin, gpointer context)
+{
+	GList *m;
+	GaimPluginAction *act;
+
+	m = NULL;
+	act = gaim_plugin_action_new(_("Modify My Information"), _qq_menu_get_my_info);
+	m = g_list_append(m, act);
+
+	act = gaim_plugin_action_new(_("Show Login Information"), _qq_menu_show_login_info);
+	m = g_list_append(m, act);
+
+	act = gaim_plugin_action_new(_("Show System Message"), _qq_menu_show_system_message);
+	m = g_list_append(m, act);
+
+	act = gaim_plugin_action_new(_("Any QQ Command"), _qq_menu_any_cmd);
+	m = g_list_append(m, act);
+
+	act = gaim_plugin_action_new(_("Qun: Search a permanent Qun"), _qq_menu_search_or_add_permanent_group);
+	m = g_list_append(m, act);
+
+	act = gaim_plugin_action_new(_("Qun: Create a permanent Qun"), _qq_menu_create_permanent_group);
+	m = g_list_append(m, act);
+
+	act = gaim_plugin_action_new(_("Locate an IP"), _qq_menu_locate_ip);
+        m = g_list_append(m, act);
+	
+	act = gaim_plugin_action_new(_("About QQ Plugin"), _qq_menu_show_about);
+	m = g_list_append(m, act);
+
+	return m;
+}
+
+/* chat-related (QQ Qun) menu shown up with right-click */
+static GList *_qq_chat_menu(GaimBlistNode *node)	//gfhuang
+{
+	GList *m;
+	GaimMenuAction *act;
+
+	m = NULL;
+	act = gaim_menu_action_new(_("Exit this QQ Qun"), GAIM_CALLBACK(_qq_menu_unsubscribe_group), NULL, NULL);   //add NULL by gfhuang
+	m = g_list_append(m, act);
+
+	act = gaim_menu_action_new(_("Show Details"), GAIM_CALLBACK(_qq_menu_manage_group), NULL, NULL);  //add NULL by gfhuang
+	m = g_list_append(m, act);
+
+	return m;
+}
+/* buddy-related menu shown up with right-click */
+static GList *_qq_buddy_menu(GaimBlistNode * node)
+{
+	GList *m;
+	/*GaimBlistNodeAction->GaimMenuAction, gaim2beta2, gfhuang*/
+//	GaimMenuAction  *act;
+
+	if(GAIM_BLIST_NODE_IS_CHAT(node))		//by gfhuang
+		return _qq_chat_menu(node);
+	
+	m = NULL;
+	/*gaim_blist_node_action_new -> gaim_menu_action_new, gaim2beta2, gfhuang */
+
+/* TODO : not working, temp commented out by gfhuang 
+
+	act = gaim_menu_action_new(_("Block this buddy"), GAIM_CALLBACK(_qq_menu_block_buddy), NULL, NULL); //add NULL by gfhuang
+	m = g_list_append(m, act);
+//	if (q_bud && is_online(q_bud->status)) {
+		act = gaim_menu_action_new(_("Send File"), GAIM_CALLBACK(_qq_menu_send_file), NULL, NULL); //add NULL by gfhuang
+		m = g_list_append(m, act);
+//	}
+*/
+
+	return m;
+}
+
+
+static void _qq_keep_alive(GaimConnection * gc)
+{
+	qq_group *group;
+	qq_data *qd;
+	GList *list;
+
+	g_return_if_fail(gc != NULL);
+	if (NULL == (qd = (qq_data *) gc->proto_data))
+		return;
+
+	list = qd->groups;
+	while (list != NULL) {
+		group = (qq_group *) list->data;
+		if (group->my_status == QQ_GROUP_MEMBER_STATUS_IS_MEMBER ||
+		    group->my_status == QQ_GROUP_MEMBER_STATUS_IS_ADMIN)
+// no need to get info time and time again, online members enough, gfhuang
+//			qq_send_cmd_group_get_group_info(gc, group);
+			qq_send_cmd_group_get_online_member(gc, group);
+	
+		list = list->next;
+	}
+
+	qq_send_packet_keep_alive(gc);
+
+}
+
+/* convert chat nickname to qq-uid to get this buddy info */
+/* who is the nickname of buddy in QQ chat-room (Qun) */
+static void _qq_get_chat_buddy_info(GaimConnection * gc, gint channel, const gchar * who)
+{
+	gchar *gaim_name;
+	g_return_if_fail(gc != NULL && gc->proto_data != NULL && who != NULL);
+
+	gaim_name = qq_group_find_member_by_channel_and_nickname(gc, channel, who);
+	if (gaim_name != NULL)
+		_qq_get_info(gc, gaim_name);
+
+}
+
+/* convert chat nickname to qq-uid to invite individual IM to buddy */
+/* who is the nickname of buddy in QQ chat-room (Qun) */
+static gchar *_qq_get_chat_buddy_real_name(GaimConnection * gc, gint channel, const gchar * who)
+{
+	g_return_val_if_fail(gc != NULL && gc->proto_data != NULL && who != NULL, NULL);
+	return qq_group_find_member_by_channel_and_nickname(gc, channel, who);
+
+}
+
+void qq_function_not_implemented(GaimConnection * gc)
+{
+	gaim_notify_warning(gc, NULL, _("This function has not be implemented yet"), _("Please wait for new version"));
+}
+
+/*
+static GaimPluginPrefFrame *get_plugin_pref_frame(GaimPlugin * plugin)
+{
+	GaimPluginPrefFrame *frame;
+	GaimPluginPref *ppref;
+
+	frame = gaim_plugin_pref_frame_new();
+
+	ppref = gaim_plugin_pref_new_with_label(_("Convert IP to location"));
+	gaim_plugin_pref_frame_add(frame, ppref);
+
+	ppref = gaim_plugin_pref_new_with_name_and_label("/plugins/prpl/qq/ipfile", _("IP file"));
+	gaim_plugin_pref_frame_add(frame, ppref);
+
+	ppref = gaim_plugin_pref_new_with_label(_("Display Options"));
+	gaim_plugin_pref_frame_add(frame, ppref);
+
+	ppref = gaim_plugin_pref_new_with_name_and_label
+	    ("/plugins/prpl/qq/show_status_by_icon", _("Show gender/age information beside buddy icons"));
+	gaim_plugin_pref_frame_add(frame, ppref);
+
+	ppref = gaim_plugin_pref_new_with_name_and_label
+	    ("/plugins/prpl/qq/show_fake_video", _("Fake an video for GAIM QQ (re-login to activate)"));
+	gaim_plugin_pref_frame_add(frame, ppref);
+
+	ppref = gaim_plugin_pref_new_with_label(_("System Options"));
+	gaim_plugin_pref_frame_add(frame, ppref);
+
+	ppref = gaim_plugin_pref_new_with_name_and_label
+	    ("/plugins/prpl/qq/prompt_for_missing_packet", _("Prompt user for actions if there are missing packets"));
+	gaim_plugin_pref_frame_add(frame, ppref);
+
+	ppref = gaim_plugin_pref_new_with_name_and_label
+	    ("/plugins/prpl/qq/prompt_group_msg_on_recv", _("Pop up Qun chat window when receive Qun message"));
+	gaim_plugin_pref_frame_add(frame, ppref);
+
+	ppref = gaim_plugin_pref_new_with_name_and_label("/plugins/prpl/qq/datadir", _("OpenQ installed directory"));
+	gaim_plugin_pref_frame_add(frame, ppref);
+
+	return frame;
+}
+*/
+
+GaimPlugin *my_protocol = NULL;
+static GaimPluginProtocolInfo prpl_info	= {
+	OPT_PROTO_CHAT_TOPIC | OPT_PROTO_USE_POINTSIZE,
+	NULL,				/* user_splits	*/
+	NULL,				/* protocol_options */
+	NO_BUDDY_ICONS,			/* icon_spec */
+	_qq_list_icon,			/* list_icon */
+	_qq_list_emblems,		/* list_emblems */
+	_qq_status_text,		/* status_text	*/
+	_qq_tooltip_text,		/* tooltip_text */
+	_qq_away_states,		/* away_states	*/
+	_qq_buddy_menu,			/* blist_node_menu */
+	qq_chat_info,			/* chat_info */
+	NULL,				/* chat_info_defaults */
+	_qq_login,			/* login */
+	_qq_close,			/* close */
+	_qq_send_im,			/* send_im */
+	NULL,				/* set_info */
+	NULL,				/* send_typing	*/
+	_qq_get_info,			/* get_info */
+	_qq_set_away,			/* set_away */
+	NULL,				/* set_idle */
+	NULL,				/* change_passwd */
+	qq_add_buddy,			/* add_buddy */
+	NULL,				/* add_buddies	*/
+	qq_remove_buddy,		/* remove_buddy */
+	NULL,				/* remove_buddies */
+	NULL,				/* add_permit */
+	NULL,				/* add_deny */
+	NULL,				/* rem_permit */
+	NULL,				/* rem_deny */
+	NULL,				/* set_permit_deny */
+	qq_group_join,			/* join_chat */
+	NULL,				/* reject chat	invite */
+	NULL,				/* get_chat_name */
+	NULL,				/* chat_invite	*/
+	NULL,				/* chat_leave */
+	NULL,				/* chat_whisper */
+	_qq_chat_send,			/* chat_send */
+	_qq_keep_alive,			/* keepalive */
+	NULL,				/* register_user */
+	_qq_get_chat_buddy_info,	/* get_cb_info	*/
+	NULL,				/* get_cb_away	*/
+	NULL,				/* alias_buddy	*/
+	NULL,				/* group_buddy	*/
+	NULL,				/* rename_group */
+	NULL,				/* buddy_free */     
+	NULL,				/* convo_closed */
+	NULL,				/* normalize */
+	NULL,				/* set_buddy_icon */
+	NULL,				/* remove_group */
+	_qq_get_chat_buddy_real_name,	/* get_cb_real_name */
+	NULL,				/* set_chat_topic */
+	NULL,				/* find_blist_chat */
+	qq_roomlist_get_list,		/* roomlist_get_list */
+	qq_roomlist_cancel,		/* roomlist_cancel */
+	NULL,				/* roomlist_expand_category */
+	qq_can_receive_file,				/* can_receive_file */
+	qq_send_file,				/* send_file */
+	NULL,                           /* new xfer, by gfhuang */
+	NULL,				/* offline_message, gaim2beta2, gfhuang */
+	NULL,				/* GaimWhiteboardPrplOps, gaim2beta2, gfhuang */
+};
+
+/*
+static GaimPluginUiInfo prefs_info = {
+	get_plugin_pref_frame
+};
+*/
+
+static GaimPluginInfo info = {
+	GAIM_PLUGIN_MAGIC,
+	GAIM_MAJOR_VERSION,
+	GAIM_MINOR_VERSION,
+	GAIM_PLUGIN_PROTOCOL,		/**< type		*/
+	NULL,				/**< ui_requirement	*/
+	0,				/**< flags		*/
+	NULL,				/**< dependencies	*/
+	GAIM_PRIORITY_DEFAULT,		/**< priority		*/
+
+	"prpl-qq",			/**< id			*/
+	"QQ",				/**< name		*/
+	VERSION,			/**< version		*/
+					/**  summary		*/
+	N_("QQ Protocol	Plugin"),
+					/**  description	*/
+	N_("QQ Protocol	Plugin"),
+	OPENQ_AUTHOR,			/**< author		*/
+	OPENQ_WEBSITE,			/**< homepage		*/
+
+	NULL,				/**< load		*/
+	NULL,				/**< unload		*/
+	NULL,				/**< destroy		*/
+
+	NULL,				/**< ui_info		*/
+	&prpl_info,			/**< extra_info		*/
+	NULL,			/**< prefs_info		*/
+	_qq_actions
+};
+
+
+static void init_plugin(GaimPlugin * plugin)
+{
+	GaimAccountOption *option;
+
+	bindtextdomain(PACKAGE, LOCALEDIR);
+	bind_textdomain_codeset(PACKAGE, "UTF-8");
+
+	option = gaim_account_option_bool_new(_("Login in TCP"), "use_tcp", FALSE);
+	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
+
+	option = gaim_account_option_bool_new(_("Login Hidden"), "hidden", FALSE);
+	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
+
+	option = gaim_account_option_string_new(_("QQ Server"), "server", NULL);
+	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
+
+	option = gaim_account_option_string_new(_("QQ Port"), "port", NULL);
+	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
+
+	my_protocol = plugin;
+
+	gaim_prefs_add_none("/plugins/prpl/qq");
+	gaim_prefs_add_string("/plugins/prpl/qq/ipfile", "");
+	gaim_prefs_add_bool("/plugins/prpl/qq/show_status_by_icon", TRUE);
+	gaim_prefs_add_bool("/plugins/prpl/qq/show_fake_video", FALSE);
+	gaim_prefs_add_string("/plugins/prpl/qq/datadir", DATADIR);
+	gaim_prefs_add_bool("/plugins/prpl/qq/prompt_for_missing_packet", TRUE);
+	gaim_prefs_add_bool("/plugins/prpl/qq/prompt_group_msg_on_recv", TRUE);
+}
+
+GAIM_INIT_PLUGIN(qq, init_plugin, info);