////////////////////////////////////////////////////////////////////////////////
// Gadu-Gadu Plugin for Miranda IM
//
// Copyright (c) 2003-2009 Adam Strzelecki <ono+miranda@java.pl>
// Copyright (c) 2009-2011 Bartosz Białek
//
// 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 "gg.h"
#include <errno.h>
#include <io.h>

extern int gg_failno;

////////////////////////////////////////////////////////////
// Swap bits in DWORD
uint32_t swap32(uint32_t x)
{
	return (uint32_t)
		(((x & (uint32_t) 0x000000ffU) << 24) |
				((x & (uint32_t) 0x0000ff00U) << 8) |
				((x & (uint32_t) 0x00ff0000U) >> 8) |
				((x & (uint32_t) 0xff000000U) >> 24));
}

////////////////////////////////////////////////////////////
// Is online function
GGINLINE int gg_isonline(GGPROTO *gg)
{
	int isonline;
	EnterCriticalSection(&gg->sess_mutex);
	isonline = (gg->sess != NULL);
	LeaveCriticalSection(&gg->sess_mutex);

	return isonline;
}

////////////////////////////////////////////////////////////
// Send disconnect request and wait for server thread to die
void gg_disconnect(GGPROTO *gg)
{
	// If main loop go and send disconnect request
	if (gg_isonline(gg))
	{
		// Fetch proper status msg
		char *szMsg = NULL;

		// Loadup status
		if (DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_LEAVESTATUSMSG, GG_KEYDEF_LEAVESTATUSMSG))
		{
			DBVARIANT dbv;
			switch (DBGetContactSettingWord(NULL, GG_PROTO, GG_KEY_LEAVESTATUS, GG_KEYDEF_LEAVESTATUS))
			{
				case ID_STATUS_ONLINE:
					EnterCriticalSection(&gg->modemsg_mutex);
					szMsg = mir_strdup(gg->modemsg.online);
					LeaveCriticalSection(&gg->modemsg_mutex);
					if (!szMsg &&
						!DBGetContactSettingString(NULL, "SRAway", gg_status2db(ID_STATUS_ONLINE, "Default"), &dbv))
					{
						if (dbv.pszVal && *(dbv.pszVal))
							szMsg = mir_strdup(dbv.pszVal);
						DBFreeVariant(&dbv);
					}
					break;
				case ID_STATUS_AWAY:
					EnterCriticalSection(&gg->modemsg_mutex);
					szMsg = mir_strdup(gg->modemsg.away);
					LeaveCriticalSection(&gg->modemsg_mutex);
					if (!szMsg &&
						!DBGetContactSettingString(NULL, "SRAway", gg_status2db(ID_STATUS_AWAY, "Default"), &dbv))
					{
						if (dbv.pszVal && *(dbv.pszVal))
							szMsg = mir_strdup(dbv.pszVal);
						DBFreeVariant(&dbv);
					}
					break;
				case ID_STATUS_DND:
					EnterCriticalSection(&gg->modemsg_mutex);
					szMsg = mir_strdup(gg->modemsg.dnd);
					LeaveCriticalSection(&gg->modemsg_mutex);
					if (!szMsg &&
						!DBGetContactSettingString(NULL, "SRAway", gg_status2db(ID_STATUS_DND, "Default"), &dbv))
					{
						if (dbv.pszVal && *(dbv.pszVal))
							szMsg = mir_strdup(dbv.pszVal);
						DBFreeVariant(&dbv);
					}
					break;
				case ID_STATUS_FREECHAT:
					EnterCriticalSection(&gg->modemsg_mutex);
					szMsg = mir_strdup(gg->modemsg.freechat);
					LeaveCriticalSection(&gg->modemsg_mutex);
					if (!szMsg &&
						!DBGetContactSettingString(NULL, "SRAway", gg_status2db(ID_STATUS_FREECHAT, "Default"), &dbv))
					{
						if (dbv.pszVal && *(dbv.pszVal))
							szMsg = mir_strdup(dbv.pszVal);
						DBFreeVariant(&dbv);
					}
					break;
				case ID_STATUS_INVISIBLE:
					EnterCriticalSection(&gg->modemsg_mutex);
					szMsg = mir_strdup(gg->modemsg.invisible);
					LeaveCriticalSection(&gg->modemsg_mutex);
					if (!szMsg &&
						!DBGetContactSettingString(NULL, "SRAway", gg_status2db(ID_STATUS_INVISIBLE, "Default"), &dbv))
					{
						if (dbv.pszVal && *(dbv.pszVal))
							szMsg = mir_strdup(dbv.pszVal);
						DBFreeVariant(&dbv);
					}
					break;
				default:
					// Set last status
					EnterCriticalSection(&gg->modemsg_mutex);
					szMsg = mir_strdup(gg_getstatusmsg(gg, gg->proto.m_iStatus));
					LeaveCriticalSection(&gg->modemsg_mutex);
			}
		}

		EnterCriticalSection(&gg->sess_mutex);
		// Check if it has message
		if (szMsg)
		{
			gg_change_status_descr(gg->sess, GG_STATUS_NOT_AVAIL_DESCR, szMsg);
			mir_free(szMsg);
			// Wait for disconnection acknowledge
		}
		else
		{
			gg_change_status(gg->sess, GG_STATUS_NOT_AVAIL);
			// Send logoff immediately
			gg_logoff(gg->sess);
		}
		LeaveCriticalSection(&gg->sess_mutex);
	}
}

////////////////////////////////////////////////////////////
// DNS lookup function
uint32_t gg_dnslookup(GGPROTO *gg, char *host)
{
	uint32_t ip;
	struct hostent *he;

	ip = inet_addr(host);
	if(ip != INADDR_NONE)
	{
#ifdef DEBUGMODE
		gg_netlog(gg, "gg_dnslookup(): Parameter \"%s\" is already IP number.", host);
#endif
		return ip;
	}
	he = gethostbyname(host);
	if(he)
	{
		ip = *(uint32_t *) he->h_addr_list[0];
#ifdef DEBUGMODE
		gg_netlog(gg, "gg_dnslookup(): Parameter \"%s\" was resolved to %d.%d.%d.%d.", host,
			LOBYTE(LOWORD(ip)), HIBYTE(LOWORD(ip)), LOBYTE(HIWORD(ip)), HIBYTE(HIWORD(ip)));
#endif
		return ip;
	}
	gg_netlog(gg, "gg_dnslookup(): Cannot resolve hostname \"%s\".", host);
	return 0;
}

////////////////////////////////////////////////////////////
// Host list decoder
typedef struct
{
	char hostname[128];
	int port;
} GGHOST;
#define ISHOSTALPHA(a) (((a) >= '0' && (a) <= '9') || ((a) >= 'a' && (a) <= 'z') || (a) == '.' || (a) == '-')
int gg_decodehosts(char *var, GGHOST *hosts, int max)
{
	int hp = 0;
	char *hostname = NULL;
	char *portname = NULL;

	while(var && *var && hp < max)
	{
		if(ISHOSTALPHA(*var))
		{
			hostname = var;

			while(var && *var && ISHOSTALPHA(*var)) var ++;

			if(var && *var == ':' && var++ && *var && isdigit(*var))
			{
				*(var - 1) = 0;
				portname = var;
				while(var && *var && isdigit(*var)) var++;
				if(*var) { *var = 0; var ++; }
			}
			else
				if(*var) { *var = 0; var ++; }

			// Insert new item
			hosts[hp].hostname[127] = 0;
			strncpy(hosts[hp].hostname, hostname, 127);
			hosts[hp].port = portname ? atoi(portname) : 443;
			hp ++;

			// Zero the names
			hostname = NULL;
			portname = NULL;
		}
		else
			var ++;
	}
	return hp;
}

////////////////////////////////////////////////////////////
// Main connection session thread
void __cdecl gg_mainthread(GGPROTO *gg, void *empty)
{
	// Miranda variables
	NETLIBUSERSETTINGS nlus = {0};
	DBVARIANT dbv;
	// Gadu-Gadu variables
	struct gg_login_params p = {0};
	struct gg_event *e;
	struct gg_session *sess;
	// Host cycling variables
	int hostnum = 0, hostcount = 0;
	GGHOST hosts[64];
	// Gadu-gadu login errors
	static const struct tagReason { int type; char *str; } reason[] = {
		{ GG_FAILURE_RESOLVING, 	"Miranda was unable to resolve the name of the Gadu-Gadu server to its numeric address." },
		{ GG_FAILURE_CONNECTING,	"Miranda was unable to make a connection with a server. It is likely that the server is down, in which case you should wait for a while and try again later." },
		{ GG_FAILURE_INVALID,		"Received invalid server response." },
		{ GG_FAILURE_READING,		"The connection with the server was abortively closed during the connection attempt. You may have lost your local network connection." },
		{ GG_FAILURE_WRITING,		"The connection with the server was abortively closed during the connection attempt. You may have lost your local network connection." },
		{ GG_FAILURE_PASSWORD,		"Your Gadu-Gadu number and password combination was rejected by the Gadu-Gadu server. Please check login details at M->Options->Network->Gadu-Gadu and try again." },
		{ GG_FAILURE_404,			"Connecting to Gadu-Gadu hub failed." },
		{ GG_FAILURE_TLS,			"Cannot establish secure connection." },
		{ GG_FAILURE_NEED_EMAIL,	"Server disconnected asking you for changing your e-mail." },
		{ GG_FAILURE_INTRUDER,		"Too many login attempts with invalid password." },
		{ GG_FAILURE_UNAVAILABLE,	"Gadu-Gadu servers are now down. Try again later." },
		{ 0,						"Unknown" }
	};
	// Time deviation (300s)
	time_t timeDeviation = DBGetContactSettingWord(NULL, GG_PROTO, GG_KEY_TIMEDEVIATION, GG_KEYDEF_TIMEDEVIATION);

	gg_netlog(gg, "gg_mainthread(%x): Server Thread Starting", gg);
#ifdef DEBUGMODE
	gg_debug_level = GG_DEBUG_NET | GG_DEBUG_TRAFFIC | GG_DEBUG_FUNCTION | GG_DEBUG_MISC;
#else
	gg_debug_level = 0;
#endif

	// Broadcast that service is connecting
	gg_broadcastnewstatus(gg, ID_STATUS_CONNECTING);

	// Client version and misc settings
	p.client_version = GG_DEFAULT_CLIENT_VERSION;
	p.protocol_version = GG_DEFAULT_PROTOCOL_VERSION;
	p.protocol_features = GG_FEATURE_DND_FFC | GG_FEATURE_UNKNOWN_100 | GG_FEATURE_USER_DATA | GG_FEATURE_MSG_ACK | GG_FEATURE_TYPING_NOTIFICATION | GG_FEATURE_MULTILOGON;
	p.encoding = GG_ENCODING_CP1250;
	p.status_flags = GG_STATUS_FLAG_UNKNOWN;
	if (DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_SHOWLINKS, GG_KEYDEF_SHOWLINKS))
		p.status_flags |= GG_STATUS_FLAG_SPAM;

	// Use audio
	/* p.has_audio = 1; */

	// Use async connections
	/* p.async = 1; */

	// Send Era Omnix info if set
	p.era_omnix = DBGetContactSettingByte(NULL, GG_PROTO, "EraOmnix", 0);

	// Setup proxy
	nlus.cbSize = sizeof(nlus);
	if(CallService(MS_NETLIB_GETUSERSETTINGS, (WPARAM)gg->netlib, (LPARAM)&nlus))
	{
		if(nlus.useProxy)
			gg_netlog(gg, "gg_mainthread(%x): Using proxy %s:%d.", gg, nlus.szProxyServer, nlus.wProxyPort);
		gg_proxy_enabled = nlus.useProxy;
		gg_proxy_host = nlus.szProxyServer;
		gg_proxy_port = nlus.wProxyPort;
		if(nlus.useProxyAuth)
		{
			gg_proxy_username = nlus.szProxyAuthUser;
			gg_proxy_password = nlus.szProxyAuthPassword;
		}
		else
			gg_proxy_username = gg_proxy_password = NULL;
	}
	else
	{
		gg_netlog(gg, "gg_mainthread(%x): Failed loading proxy settings.", gg);
		gg_proxy_enabled = 0;
	}

	// Check out manual host setting
	if(DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_MANUALHOST, GG_KEYDEF_MANUALHOST))
	{
		if(!DBGetContactSettingString(NULL, GG_PROTO, GG_KEY_SERVERHOSTS, &dbv))
		{
			hostcount = gg_decodehosts(dbv.pszVal, hosts, 64);
			DBFreeVariant(&dbv);
		}
	}

	// Readup password
	if(!DBGetContactSettingString(NULL, GG_PROTO, GG_KEY_PASSWORD, &dbv))
	{
		CallService(MS_DB_CRYPT_DECODESTRING, strlen(dbv.pszVal) + 1, (LPARAM) dbv.pszVal);
		p.password = mir_strdup(dbv.pszVal);
		DBFreeVariant(&dbv);
	}
	else
	{
		gg_netlog(gg, "gg_mainthread(%x): No password specified. Exiting.", gg);
		gg_broadcastnewstatus(gg, ID_STATUS_OFFLINE);
		return;
	}

	// Readup number
	if(!(p.uin = DBGetContactSettingDword(NULL, GG_PROTO, GG_KEY_UIN, 0)))
	{
		gg_netlog(gg, "gg_mainthread(%x): No Gadu-Gadu number specified. Exiting.", gg);
		gg_broadcastnewstatus(gg, ID_STATUS_OFFLINE);
		mir_free(p.password);
		return;
	}

	// Readup SSL/TLS setting
	if(p.tls = DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_SSLCONN, GG_KEYDEF_SSLCONN))
		gg_netlog(gg, "gg_mainthread(%x): Using TLS/SSL for connections.", gg);

	// Gadu-Gadu accepts image sizes upto 255
	p.image_size = 255;

	////////////////////////////// DCC STARTUP /////////////////////////////
	// Uin is ok so startup dcc if not started already
	if(!gg->dcc)
	{
		gg->event = CreateEvent(NULL, TRUE, FALSE, NULL);
		gg_dccstart(gg);

		// Wait for DCC
#ifdef DEBUGMODE
		gg_netlog(gg, "gg_mainthread(%x): Waiting DCC service to start...", gg);
#endif
		while (WaitForSingleObjectEx(gg->event, INFINITE, TRUE) != WAIT_OBJECT_0);
		CloseHandle(gg->event); gg->event = NULL;
	}
	// Check if dcc is running and setup forwarding port
	if(gg->dcc && DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_FORWARDING, GG_KEYDEF_FORWARDING))
	{
		if(!DBGetContactSettingString(NULL, GG_PROTO, GG_KEY_FORWARDHOST, &dbv))
		{
			if(!(p.external_addr = gg_dnslookup(gg, dbv.pszVal)))
			{
				char error[128];
				mir_snprintf(error, sizeof(error), Translate("External direct connections hostname %s is invalid. Disabling external host forwarding."), dbv.pszVal);
				MessageBox(
					NULL,
					error,
					GG_PROTONAME,
					MB_OK | MB_ICONEXCLAMATION
				);
			}
			else
				gg_netlog(gg, "gg_mainthread(%x): Loading forwarding host %s and port %d.", dbv.pszVal, p.external_port, gg);
			if(p.external_addr)	p.external_port = DBGetContactSettingWord(NULL, GG_PROTO, GG_KEY_FORWARDPORT, GG_KEYDEF_FORWARDPORT);
			DBFreeVariant(&dbv);
		}
	}
	// Setup client port
	if(gg->dcc) p.client_port = gg->dcc->port;

retry:
	// Loadup startup status & description
	EnterCriticalSection(&gg->modemsg_mutex);
	p.status_descr = mir_strdup(gg_getstatusmsg(gg, gg->proto.m_iDesiredStatus));
	p.status = status_m2gg(gg, gg->proto.m_iDesiredStatus, p.status_descr != NULL);

	gg_netlog(gg, "gg_mainthread(%x): Connecting with number %d, status %d and description \"%s\".", gg, p.uin, gg->proto.m_iDesiredStatus,
				p.status_descr ? p.status_descr : "<none>");
	LeaveCriticalSection(&gg->modemsg_mutex);

	// Check manual hosts
	if(hostnum < hostcount)
	{
		if(!(p.server_addr = gg_dnslookup(gg, hosts[hostnum].hostname)))
		{
			char error[128];
			mir_snprintf(error, sizeof(error), Translate("Server hostname %s is invalid. Using default hostname provided by the network."), hosts[hostnum].hostname);
			MessageBox(
				NULL,
				error,
				GG_PROTONAME,
				MB_OK | MB_ICONEXCLAMATION
			);
		}
		else
		{
			p.server_port = hosts[hostnum].port;
			gg_netlog(gg, "gg_mainthread(%x): Connecting to manually specified host %s (%d.%d.%d.%d) and port %d.", gg,
				hosts[hostnum].hostname, LOBYTE(LOWORD(p.server_addr)), HIBYTE(LOWORD(p.server_addr)),
				LOBYTE(HIWORD(p.server_addr)), HIBYTE(HIWORD(p.server_addr)), p.server_port);
		}
	}
	else
		p.server_port = p.server_addr = 0;

	// Send login request
	if(!(sess = gg_login(&p)))
	{
#ifndef DEBUGMODE
		if(DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_SHOWCERRORS, GG_KEYDEF_SHOWCERRORS))
#endif
		{
			char error[128], *perror = NULL;
			int i;

			gg_broadcastnewstatus(gg, ID_STATUS_OFFLINE);
			// Lookup for error desciption
			if(errno == EACCES)
			{
				for(i = 0; reason[i].type; i++) if(reason[i].type == gg_failno)
				{
					perror = Translate(reason[i].str);
					break;
				}
			}
			if(!perror)
			{
				mir_snprintf(error, sizeof(error), Translate("Connection cannot be established because of error:\n\t%s"), strerror(errno));
				perror = error;
			}
#ifdef DEBUGMODE
			if(DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_SHOWCERRORS, GG_KEYDEF_SHOWCERRORS))
#endif
			MessageBox(
				NULL,
				perror,
				GG_PROTONAME,
				MB_OK | MB_ICONSTOP
			);
			gg_netlog(gg, "gg_mainthread(%x): %s", gg, perror);
		}

		// Reconnect to the next server on the list
		if(gg->proto.m_iDesiredStatus != ID_STATUS_OFFLINE
			&& errno == EACCES
			&& (gg_failno >= GG_FAILURE_RESOLVING && gg_failno != GG_FAILURE_PASSWORD && gg_failno != GG_FAILURE_INTRUDER && gg_failno != GG_FAILURE_UNAVAILABLE)
			&& (DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_ARECONNECT, GG_KEYDEF_ARECONNECT)
				|| (hostnum < hostcount - 1)))
		{
			if(hostnum < hostcount - 1) hostnum ++;
			gg_broadcastnewstatus(gg, ID_STATUS_CONNECTING);
			mir_free(p.status_descr);
			goto retry;
		}
		// We cannot do more about this
		EnterCriticalSection(&gg->modemsg_mutex);
		gg->proto.m_iDesiredStatus = ID_STATUS_OFFLINE;
		LeaveCriticalSection(&gg->modemsg_mutex);
	}
	else
	{
		// Successfully connected
		EnterCriticalSection(&gg->sess_mutex);
		gg->sess = sess;
		LeaveCriticalSection(&gg->sess_mutex);
		// Subscribe users status notifications
		gg_notifyall(gg);
		// Set startup status
		if (gg->proto.m_iDesiredStatus != status_gg2m(gg, p.status))
			gg_refreshstatus(gg, gg->proto.m_iDesiredStatus);
		else
		{
			gg_broadcastnewstatus(gg, gg->proto.m_iDesiredStatus);
			// Change status of the contact with our own UIN (if got yourself added to the contact list)
			gg_changecontactstatus(gg, p.uin, p.status, p.status_descr, 0, 0, 0, 0);
		}
		if (gg->check_first_conn) // First connection to the account
		{
			// Start search for user data
			gg_getinfo((PROTO_INTERFACE *)gg, NULL, 0);
			// Fetch user avatar
			gg_getuseravatar(gg);
			gg->check_first_conn = 0;
		}
	}

	//////////////////////////////////////////////////////////////////////////////////
	// Main loop
	while(gg_isonline(gg))
	{
		// Connection broken/closed
		if(!(e = gg_watch_fd(gg->sess)))
		{
			gg_netlog(gg, "gg_mainthread(%x): Connection closed.", gg);
			EnterCriticalSection(&gg->sess_mutex);
			gg_free_session(gg->sess);
			gg->sess = NULL;
			LeaveCriticalSection(&gg->sess_mutex);
			break;
		}
		else
			gg_netlog(gg, "gg_mainthread(%x): Event: %s", gg, ggdebug_eventtype(e));

		switch(e->type)
		{
			// Client connected
			case GG_EVENT_CONN_SUCCESS:
				// Nada
				break;

			// Client disconnected or connection failure
			case GG_EVENT_CONN_FAILED:
			case GG_EVENT_DISCONNECT:
				EnterCriticalSection(&gg->sess_mutex);
				gg_free_session(gg->sess);
				gg->sess = NULL;
				LeaveCriticalSection(&gg->sess_mutex);
				break;

			// Client allowed to disconnect
			case GG_EVENT_DISCONNECT_ACK:
				// Send logoff
				gg_logoff(gg->sess);
				break;

			// Received ackowledge
			case GG_EVENT_ACK:
				if(e->event.ack.seq && e->event.ack.recipient)
				{
					ProtoBroadcastAck(GG_PROTO, gg_getcontact(gg, (DWORD)e->event.ack.recipient, 0, 0, NULL),
						ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE) e->event.ack.seq, 0);
				}
				break;

			// Statuslist notify (deprecated)
			case GG_EVENT_NOTIFY:
			case GG_EVENT_NOTIFY_DESCR:
			{
				struct gg_notify_reply *n;

				n = (e->type == GG_EVENT_NOTIFY) ? e->event.notify : e->event.notify_descr.notify;

				for (; n->uin; n++)
				{
					char *descr = (e->type == GG_EVENT_NOTIFY_DESCR) ? e->event.notify_descr.descr : NULL;
					gg_changecontactstatus(gg, n->uin, n->status, descr, 0, n->remote_ip, n->remote_port, n->version);
				}
				break;
			}
			// Statuslist notify (version >= 6.0)
			case GG_EVENT_NOTIFY60:
			{
				uin_t uin = (uin_t)DBGetContactSettingDword(NULL, GG_PROTO, GG_KEY_UIN, 0);
				int i;
				for(i = 0; e->event.notify60[i].uin; i++) {
					if (e->event.notify60[i].uin == uin) continue;
					gg_changecontactstatus(gg, e->event.notify60[i].uin, e->event.notify60[i].status, e->event.notify60[i].descr,
						e->event.notify60[i].time, e->event.notify60[i].remote_ip, e->event.notify60[i].remote_port,
						e->event.notify60[i].version);
					gg_requestavatar(gg, gg_getcontact(gg, e->event.notify60[i].uin, 0, 0, NULL), 0);
				}
				break;
			}

			// Pubdir search reply && read own data reply
			case GG_EVENT_PUBDIR50_SEARCH_REPLY:
			case GG_EVENT_PUBDIR50_READ:
			case GG_EVENT_PUBDIR50_WRITE:
			{
				gg_pubdir50_t res = e->event.pubdir50;
				int i, count;

				if (e->type == GG_EVENT_PUBDIR50_SEARCH_REPLY)
				{
					gg_netlog(gg, "gg_mainthread(%x): Got user info.", gg);
					// Store next search UIN
					if (res->seq == GG_SEQ_SEARCH)
						gg->next_uin = gg_pubdir50_next(res);
				}
				else if (e->type == GG_EVENT_PUBDIR50_READ)
				{
					gg_netlog(gg, "gg_mainthread(%x): Got owner info.", gg);
				}
				else if (e->type == GG_EVENT_PUBDIR50_WRITE)
				{
					gg_netlog(gg, "gg_mainthread(%x): Public directory save succesful.", gg);
					// Update user details
					gg_getinfo((PROTO_INTERFACE *)gg, NULL, 0);
				}

				if ((count = gg_pubdir50_count(res)) > 0)
				{
					for (i = 0; i < count; i++)
					{
						// Loadup fields
						const char *__fmnumber = gg_pubdir50_get(res, i, GG_PUBDIR50_UIN);
						const char *__nick = gg_pubdir50_get(res, i, GG_PUBDIR50_NICKNAME);
						const char *__firstname = gg_pubdir50_get(res, i, GG_PUBDIR50_FIRSTNAME);
						const char *__lastname = gg_pubdir50_get(res, i, GG_PUBDIR50_LASTNAME);
						const char *__familyname = gg_pubdir50_get(res, i, GG_PUBDIR50_FAMILYNAME);
						const char *__birthyear = gg_pubdir50_get(res, i, GG_PUBDIR50_BIRTHYEAR);
						const char *__city = gg_pubdir50_get(res, i, GG_PUBDIR50_CITY);
						const char *__origincity = gg_pubdir50_get(res, i, GG_PUBDIR50_FAMILYCITY);
						const char *__gender = gg_pubdir50_get(res, i, GG_PUBDIR50_GENDER);
						const char *__status = gg_pubdir50_get(res, i, GG_PUBDIR50_STATUS);
						uin_t uin = __fmnumber ? atoi(__fmnumber) : 0;

						HANDLE hContact = (res->seq == GG_SEQ_CHINFO) ? NULL : gg_getcontact(gg, uin, 0, 0, NULL);
						gg_netlog(gg, "gg_mainthread(%x): Search result for uin %d, seq %d.", gg, uin, res->seq);
						if (res->seq == GG_SEQ_SEARCH)
						{
							char strFmt1[64];
							char strFmt2[64];
							GGSEARCHRESULT sr = {0};

							mir_snprintf(strFmt2, sizeof(strFmt2), "%s", (char *)CallService(MS_CLIST_GETSTATUSMODEDESCRIPTION, status_gg2m(gg, atoi(__status)), 0));
							if (__city)
							{
								mir_snprintf(strFmt1, sizeof(strFmt1), ", %s %s", Translate("City:"), __city);
								strncat(strFmt2, strFmt1, sizeof(strFmt2) - strlen(strFmt2));
							}
							if (__birthyear)
							{
								time_t t = time(NULL);
								struct tm *lt = localtime(&t);
								int br = atoi(__birthyear);

								if (br < (lt->tm_year + 1900) && br > 1900)
								{
									mir_snprintf(strFmt1, sizeof(strFmt1), ", %s %d", Translate("Age:"), (lt->tm_year + 1900) - br);
									strncat(strFmt2, strFmt1, sizeof(strFmt2) - strlen(strFmt2));
								}
							}

							sr.hdr.cbSize = sizeof(sr);
							sr.hdr.nick = (char *)__nick;
							sr.hdr.firstName = (char *)__firstname;
							sr.hdr.lastName = (char *)__lastname;
							sr.hdr.email = strFmt2;
							sr.hdr.id = _ultoa(uin, strFmt1, 10);
							sr.uin = uin;

							ProtoBroadcastAck(GG_PROTO, NULL, ACKTYPE_SEARCH, ACKRESULT_DATA, (HANDLE) 1, (LPARAM)&sr);
						}

						if (((res->seq == GG_SEQ_INFO || res->seq == GG_SEQ_GETNICK) && hContact != NULL)
							|| res->seq == GG_SEQ_CHINFO)
						{
							// Change nickname if it's not present
							if (__nick && (res->seq == GG_SEQ_GETNICK || res->seq == GG_SEQ_CHINFO))
								DBWriteContactSettingString(hContact, GG_PROTO, GG_KEY_NICK, __nick);

							if (__nick)
								DBWriteContactSettingString(hContact, GG_PROTO, "NickName", __nick);
							else if (res->seq == GG_SEQ_CHINFO)
								DBDeleteContactSetting(NULL, GG_PROTO, "NickName");

							// Change other info
							if (__city)
								DBWriteContactSettingString(hContact, GG_PROTO, "City", __city);
							else if (res->seq == GG_SEQ_CHINFO)
								DBDeleteContactSetting(NULL, GG_PROTO, "City");

							if (__firstname)
								DBWriteContactSettingString(hContact, GG_PROTO, "FirstName", __firstname);
							else if (res->seq == GG_SEQ_CHINFO)
								DBDeleteContactSetting(NULL, GG_PROTO, "FirstName");

							if (__lastname)
								DBWriteContactSettingString(hContact, GG_PROTO, "LastName", __lastname);
							else if (res->seq == GG_SEQ_CHINFO)
								DBDeleteContactSetting(NULL, GG_PROTO, "LastName");

							if (__familyname)
								DBWriteContactSettingString(hContact, GG_PROTO, "FamilyName", __familyname);
							else if (res->seq == GG_SEQ_CHINFO)
								DBDeleteContactSetting(NULL, GG_PROTO, "FamilyName");

							if (__origincity)
								DBWriteContactSettingString(hContact, GG_PROTO, "CityOrigin", __origincity);
							else if (res->seq == GG_SEQ_CHINFO)
								DBDeleteContactSetting(NULL, GG_PROTO, "CityOrigin");

							if (__birthyear)
							{
								time_t t = time(NULL);
								struct tm *lt = localtime(&t);
								int br = atoi(__birthyear);
								if (br > 0)
								{
									DBWriteContactSettingWord(hContact, GG_PROTO, "Age", (WORD)(lt->tm_year + 1900 - br));
									DBWriteContactSettingWord(hContact, GG_PROTO, "BirthYear", (WORD)br);
								}
							}
							else if (res->seq == GG_SEQ_CHINFO)
							{
								DBDeleteContactSetting(NULL, GG_PROTO, "Age");
								DBDeleteContactSetting(NULL, GG_PROTO, "BirthYear");
							}

							// Gadu-Gadu Male <-> Female
							if (__gender)
							{
								if (res->seq == GG_SEQ_CHINFO)
									DBWriteContactSettingByte(hContact, GG_PROTO, "Gender",
									(BYTE)(!strcmp(__gender, GG_PUBDIR50_GENDER_SET_MALE) ? 'M' :
										  (!strcmp(__gender, GG_PUBDIR50_GENDER_SET_FEMALE) ? 'F' : '?')));
								else
									DBWriteContactSettingByte(hContact, GG_PROTO, "Gender",
									(BYTE)(!strcmp(__gender, GG_PUBDIR50_GENDER_MALE) ? 'M' :
										  (!strcmp(__gender, GG_PUBDIR50_GENDER_FEMALE) ? 'F' : '?')));
							}
							else if (res->seq == GG_SEQ_CHINFO)
							{
								DBDeleteContactSetting(NULL, GG_PROTO, "Gender");
							}

							gg_netlog(gg, "gg_mainthread(%x): Setting user info for uin %d.", gg, uin);
							ProtoBroadcastAck(GG_PROTO, hContact, ACKTYPE_GETINFO, ACKRESULT_SUCCESS, (HANDLE) 1, 0);
						}
					}
				}
				if (res->seq == GG_SEQ_SEARCH)
					ProtoBroadcastAck(GG_PROTO, NULL, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE) 1, 0);
				break;
			}

			// Status (deprecated)
			case GG_EVENT_STATUS:
				gg_changecontactstatus(gg, e->event.status.uin, e->event.status.status, e->event.status.descr, 0, 0, 0, 0);
				break;

			// Status (version >= 6.0)
			case GG_EVENT_STATUS60:
				{
					HANDLE hContact = gg_getcontact(gg, e->event.status60.uin, 0, 0, NULL);
					int oldstatus = DBGetContactSettingWord(hContact, GG_PROTO, GG_KEY_STATUS, (WORD)ID_STATUS_OFFLINE);
					uin_t uin = (uin_t)DBGetContactSettingDword(NULL, GG_PROTO, GG_KEY_UIN, 0);

					if (e->event.status60.uin == uin)
					{
						// Status was changed by the user simultaneously logged on using different Miranda account or IM client
						int iStatus = status_gg2m(gg, e->event.status60.status);
						CallProtoService(GG_PROTO, PS_SETAWAYMSG, iStatus, (LPARAM)e->event.status60.descr);
						CallProtoService(GG_PROTO, PS_SETSTATUS, iStatus, 0);
					}

					gg_changecontactstatus(gg, e->event.status60.uin, e->event.status60.status, e->event.status60.descr,
						e->event.status60.time, e->event.status60.remote_ip, e->event.status60.remote_port, e->event.status60.version);

					if (oldstatus == ID_STATUS_OFFLINE && DBGetContactSettingWord(hContact, GG_PROTO, GG_KEY_STATUS, (WORD)ID_STATUS_OFFLINE) != ID_STATUS_OFFLINE)
						gg_requestavatar(gg, hContact, 0);
				}
				break;

			// Received userlist / or put info
			case GG_EVENT_USERLIST:
				switch (e->event.userlist.type)
				{
					case GG_USERLIST_GET_REPLY:
						if(e->event.userlist.reply)
						{
							gg_parsecontacts(gg, e->event.userlist.reply);
							MessageBox(
								NULL,
								Translate("List import successful."),
								GG_PROTONAME,
								MB_OK | MB_ICONINFORMATION
							);
						}
						break;

					case GG_USERLIST_PUT_REPLY:
						if(gg->list_remove)
							MessageBox(
								NULL,
								Translate("List remove successful."),
								GG_PROTONAME,
								MB_OK | MB_ICONINFORMATION
							);
						else
							MessageBox(
								NULL,
								Translate("List export successful."),
								GG_PROTONAME,
								MB_OK | MB_ICONINFORMATION
							);

						break;
				}
				break;

			// Received message
			case GG_EVENT_MSG:
				// This is CTCP request
				if((e->event.msg.msgclass & GG_CLASS_CTCP))
				{
					gg_dccconnect(gg, e->event.msg.sender);
				}
				// Check if not conference and block
				else if(!e->event.msg.recipients_count || gg->gc_enabled)
				{
					// Check if groupchat
					if(e->event.msg.recipients_count && gg->gc_enabled && !DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_IGNORECONF, GG_KEYDEF_IGNORECONF))
					{
						char *chat = gg_gc_getchat(gg, e->event.msg.sender, e->event.msg.recipients, e->event.msg.recipients_count);
						if(chat)
						{
							char id[32];
							GCDEST gcdest = {GG_PROTO, chat, GC_EVENT_MESSAGE};
							GCEVENT gcevent = {sizeof(GCEVENT), &gcdest};
							time_t t = time(NULL);

							UIN2ID(e->event.msg.sender, id);

							gcevent.pszUID = id;
							gcevent.pszText = e->event.msg.message;
							gcevent.pszNick = (char *) CallService(MS_CLIST_GETCONTACTDISPLAYNAME, (WPARAM) gg_getcontact(gg, e->event.msg.sender, 1, 0, NULL), 0);
							gcevent.time = (!(e->event.msg.msgclass & GG_CLASS_OFFLINE) || e->event.msg.time > (t - timeDeviation)) ? t : e->event.msg.time;
							gcevent.dwFlags = GCEF_ADDTOLOG;
							gg_netlog(gg, "gg_mainthread(%x): Conference message to room %s & id %s.", gg, chat, id);
							CallServiceSync(MS_GC_EVENT, 0, (LPARAM)&gcevent);
						}
					}
					// Check if not empty message ( who needs it? )
					else if(!e->event.msg.recipients_count && e->event.msg.message && *e->event.msg.message && strcmp(e->event.msg.message, "\xA0\0"))
					{
						CCSDATA ccs = {0};
						PROTORECVEVENT pre = {0};
						time_t t = time(NULL);
						ccs.szProtoService = PSR_MESSAGE;
						ccs.hContact = gg_getcontact(gg, e->event.msg.sender, 1, 0, NULL);
						ccs.lParam = (LPARAM)&pre;
						pre.timestamp = (!(e->event.msg.msgclass & GG_CLASS_OFFLINE) || e->event.msg.time > (t - timeDeviation)) ? t : e->event.msg.time;
						pre.szMessage = e->event.msg.message;
						CallService(MS_PROTO_CHAINRECV, 0, (LPARAM) &ccs);
					}

					// RichEdit format included (image)
					if(e->event.msg.formats_length
						&& DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_IMGRECEIVE, GG_KEYDEF_IMGRECEIVE))
					{
						char *formats = e->event.msg.formats;
						int len = 0, formats_len = e->event.msg.formats_length, add_ptr;

						while (len < formats_len)
						{
							add_ptr = sizeof(struct gg_msg_richtext_format);
							if (((struct gg_msg_richtext_format*)formats)->font & GG_FONT_IMAGE)
							{
								struct gg_msg_richtext_image *image = (struct gg_msg_richtext_image *)(formats + add_ptr);
								EnterCriticalSection(&gg->sess_mutex);
								gg_image_request(gg->sess, e->event.msg.sender, image->size, image->crc32);
								LeaveCriticalSection(&gg->sess_mutex);

								gg_netlog(gg, "gg_mainthread: image request sent!");
								add_ptr += sizeof(struct gg_msg_richtext_image);
							}
							if (((struct gg_msg_richtext_format*)formats)->font & GG_FONT_COLOR)
								add_ptr += sizeof(struct gg_msg_richtext_color);
							len += add_ptr;
							formats += add_ptr;
						}
					}
				}
				break;

			// Message sent from concurrent user session
			case GG_EVENT_MULTILOGON_MSG:
				if (e->event.multilogon_msg.recipients_count && gg->gc_enabled && !DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_IGNORECONF, GG_KEYDEF_IGNORECONF))
				{
					char *chat = gg_gc_getchat(gg, e->event.multilogon_msg.sender, e->event.multilogon_msg.recipients, e->event.multilogon_msg.recipients_count);
					if (chat)
					{
						char id[32];
						DBVARIANT dbv;
						GCDEST gcdest = {GG_PROTO, chat, GC_EVENT_MESSAGE};
						GCEVENT gcevent = {sizeof(GCEVENT), &gcdest};

						UIN2ID(DBGetContactSettingDword(NULL, GG_PROTO, GG_KEY_UIN, 0), id);

						gcevent.pszUID = id;
						gcevent.pszText = e->event.multilogon_msg.message;
						if (!DBGetContactSettingString(NULL, GG_PROTO, GG_KEY_NICK, &dbv))
							gcevent.pszNick = dbv.pszVal;
						else
							gcevent.pszNick = Translate("Me");
						gcevent.time = e->event.multilogon_msg.time;
						gcevent.bIsMe = 1;
						gcevent.dwFlags = GCEF_ADDTOLOG;
						gg_netlog(gg, "gg_mainthread(%x): Sent conference message to room %s.", gg, chat);
						CallServiceSync(MS_GC_EVENT, 0, (LPARAM)&gcevent);
						if (gcevent.pszNick == dbv.pszVal) DBFreeVariant(&dbv);
					}
				}
				else if (!e->event.multilogon_msg.recipients_count && e->event.multilogon_msg.message && *e->event.multilogon_msg.message
					&& strcmp(e->event.multilogon_msg.message, "\xA0\0"))
				{
					DBEVENTINFO dbei = {0};
					dbei.cbSize = sizeof(dbei);
					dbei.szModule = GG_PROTO;
					dbei.timestamp = (DWORD)e->event.multilogon_msg.time;
					dbei.flags = DBEF_SENT;
					dbei.eventType = EVENTTYPE_MESSAGE;
					dbei.cbBlob = (DWORD)strlen(e->event.multilogon_msg.message) + 1;
					dbei.pBlob = (PBYTE)e->event.multilogon_msg.message;
					CallService(MS_DB_EVENT_ADD, (WPARAM)gg_getcontact(gg, e->event.multilogon_msg.sender, 1, 0, NULL), (LPARAM)&dbei);
				}
				break;

			// Information on active concurrent sessions
			case GG_EVENT_MULTILOGON_INFO:
				{
					list_t l;
					int i;
					gg_netlog(gg, "gg_mainthread(): Concurrent sessions count: %d.", e->event.multilogon_info.count);
					EnterCriticalSection(&gg->sessions_mutex);
					for (l = gg->sessions; l; l = l->next)
					{
						struct gg_multilogon_session* sess = (struct gg_multilogon_session*)l->data;
						mir_free(sess->name);
						mir_free(sess);
					}
					list_destroy(gg->sessions, 0);
					gg->sessions = NULL;
					for (i = 0; i < e->event.multilogon_info.count; i++)
					{
						struct gg_multilogon_session* sess = mir_alloc(sizeof(struct gg_multilogon_session));
						memcpy(sess, &e->event.multilogon_info.sessions[i], sizeof(struct gg_multilogon_session));
						sess->name = mir_strdup(e->event.multilogon_info.sessions[i].name);
						list_add(&gg->sessions, sess, 0);
					}
					LeaveCriticalSection(&gg->sessions_mutex);
					gg_sessions_updatedlg(gg);
				}
				break;

			// Image reply sent
			case GG_EVENT_IMAGE_REPLY:
				// Get rid of empty image
				if(e->event.image_reply.size && e->event.image_reply.image)
				{
					HANDLE hContact = gg_getcontact(gg, e->event.image_reply.sender, 1, 0, NULL);
					void *img = (void *)gg_img_loadpicture(gg, e, 0);

					if(!img)
						break;

					if(DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_IMGMETHOD, GG_KEYDEF_IMGMETHOD) == 1 || gg_img_opened(gg, e->event.image_reply.sender))
					{
						gg_img_display(gg, hContact, img);
					}
					else if(DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_IMGMETHOD, GG_KEYDEF_IMGMETHOD) == 2)
					{
						gg_img_displayasmsg(gg, hContact, img);
					}
					else
					{
						CLISTEVENT cle = {0};
						char service[128];
						mir_snprintf(service, sizeof(service), GGS_RECVIMAGE, GG_PROTO);

						cle.cbSize = sizeof(cle);
						cle.hContact = hContact;
						cle.hIcon = LoadIconEx("image", FALSE);
						cle.flags = CLEF_URGENT;
						cle.hDbEvent = (HANDLE)"img";
						cle.lParam = (LPARAM)img;
						cle.pszService = service;
						cle.pszTooltip = Translate("Image received");
						CallService(MS_CLIST_ADDEVENT, 0, (LPARAM)&cle);
						ReleaseIconEx("image", FALSE);
					}
				}
				break;

			// Image send request
			case GG_EVENT_IMAGE_REQUEST:
				gg_img_sendonrequest(gg, e);
				break;

			// Incoming direct connection
			case GG_EVENT_DCC7_NEW:
				{
					struct gg_dcc7 *dcc7 = e->event.dcc7_new;
					gg_netlog(gg, "gg_mainthread(%x): Incoming direct connection.", gg);
					dcc7->contact = gg_getcontact(gg, dcc7->peer_uin, 0, 0, NULL);

					// Check if user is on the list and if it is my uin
					if(!dcc7->contact || DBGetContactSettingDword(NULL, GG_PROTO, GG_KEY_UIN, -1) != dcc7->uin) {
						gg_dcc7_free(dcc7);
						e->event.dcc7_new = NULL;
						break;
					}

					// Add to waiting transfers
					EnterCriticalSection(&gg->ft_mutex);
					list_add(&gg->transfers, dcc7, 0);
					LeaveCriticalSection(&gg->ft_mutex);

					//////////////////////////////////////////////////
					// Add file recv request
					{
						CCSDATA ccs;
						PROTORECVEVENT pre;
						char *szBlob;
						char *szFilename = dcc7->filename;
						char *szMsg = dcc7->filename;
						gg_netlog(gg, "gg_mainthread(%x): Client: %d, File ack filename \"%s\" size %d.", gg, dcc7->peer_uin,
							dcc7->filename, dcc7->size);
						// Make new ggtransfer struct
						szBlob = (char *)malloc(sizeof(DWORD) + strlen(szFilename) + strlen(szMsg) + 2);
						// Store current dcc
						*(PDWORD)szBlob = (DWORD)dcc7;
						// Store filename
						strcpy(szBlob + sizeof(DWORD), szFilename);
						// Store description
						strcpy(szBlob + sizeof(DWORD) + strlen(szFilename) + 1, szMsg);
						ccs.szProtoService = PSR_FILE;
						ccs.hContact = dcc7->contact;
						ccs.wParam = 0;
						ccs.lParam = (LPARAM)&pre;
						pre.flags = 0;
						pre.timestamp = time(NULL);
						pre.szMessage = szBlob;
						pre.lParam = 0;
						CallService(MS_PROTO_CHAINRECV, 0, (LPARAM)&ccs);
						free(szBlob);
					}
					e->event.dcc7_new = NULL;
				}
				break;

			// Direct connection rejected
			case GG_EVENT_DCC7_REJECT:
				{
					struct gg_dcc7 *dcc7 = e->event.dcc7_reject.dcc7;
					if (dcc7->type == GG_SESSION_DCC7_SEND)
					{
						gg_netlog(gg, "gg_mainthread(%x): File transfer denied by client %d (reason = %d).", gg, dcc7->peer_uin, e->event.dcc7_reject.reason);
						ProtoBroadcastAck(GG_PROTO, dcc7->contact, ACKTYPE_FILE, ACKRESULT_DENIED, dcc7, 0);

						// Remove from watches and free
						EnterCriticalSection(&gg->ft_mutex);
						list_remove(&gg->watches, dcc7, 0);
						LeaveCriticalSection(&gg->ft_mutex);
						gg_dcc7_free(dcc7);
					}
					else
					{
						gg_netlog(gg, "gg_mainthread(%x): File transfer aborted by client %d.", gg, dcc7->peer_uin);

						// Remove transfer from waiting list
						EnterCriticalSection(&gg->ft_mutex);
						list_remove(&gg->transfers, dcc7, 0);
						LeaveCriticalSection(&gg->ft_mutex);
					}
				}
				break;

			// Direct connection error
			case GG_EVENT_DCC7_ERROR:
				{
					struct gg_dcc7 *dcc7 = e->event.dcc7_error_ex.dcc7;
					switch (e->event.dcc7_error)
					{
						case GG_ERROR_DCC7_HANDSHAKE:
							gg_netlog(gg, "gg_mainthread(%x): Client: %d, Handshake error.", gg, dcc7 ? dcc7->peer_uin : 0);
							break;
						case GG_ERROR_DCC7_NET:
							gg_netlog(gg, "gg_mainthread(%x): Client: %d, Network error.", gg, dcc7 ? dcc7->peer_uin : 0);
							break;
						case GG_ERROR_DCC7_FILE:
							gg_netlog(gg, "gg_mainthread(%x): Client: %d, File read/write error.", gg, dcc7 ? dcc7->peer_uin : 0);
							break;
						case GG_ERROR_DCC7_EOF:
							gg_netlog(gg, "gg_mainthread(%x): Client: %d, End of file/connection error.", gg, dcc7 ? dcc7->peer_uin : 0);
							break;
						case GG_ERROR_DCC7_REFUSED:
							gg_netlog(gg, "gg_mainthread(%x): Client: %d, Connection refused error.", gg, dcc7 ? dcc7->peer_uin : 0);
							break;
						case GG_ERROR_DCC7_RELAY:
							gg_netlog(gg, "gg_mainthread(%x): Client: %d, Relay connection error.", gg, dcc7 ? dcc7->peer_uin : 0);
							break;
						default:
							gg_netlog(gg, "gg_mainthread(%x): Client: %d, Unknown error.", gg, dcc7 ? dcc7->peer_uin : 0);
					}
					if (!dcc7) break;

					// Remove from watches
					list_remove(&gg->watches, dcc7, 0);

					// Close file & fail
					if (dcc7->file_fd != -1)
					{
						_close(dcc7->file_fd);
						dcc7->file_fd = -1;
					}

					if (dcc7->contact)
						ProtoBroadcastAck(GG_PROTO, dcc7->contact, ACKTYPE_FILE, ACKRESULT_FAILED, dcc7, 0);

					// Free dcc
					gg_dcc7_free(dcc7);
				}
				break;

			case GG_EVENT_XML_ACTION:
				if (DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_ENABLEAVATARS, GG_KEYDEF_ENABLEAVATARS)) {
					HXML hXml;
					TCHAR *xmlAction;
					TCHAR *tag;

					xmlAction = gg_a2t(e->event.xml_action.data);
					tag = gg_a2t("events");
					hXml = xi.parseString(xmlAction, 0, tag);

					if (hXml != NULL) {
						HXML node;
						char *type, *sender;
						
						mir_free(tag);
						tag = gg_a2t("event/type");
						node = xi.getChildByPath(hXml, tag, 0);
						type = node != NULL ? gg_t2a(xi.getText(node)) : NULL;

						mir_free(tag);
						tag = gg_a2t("event/sender");
						node = xi.getChildByPath(hXml, tag, 0);
						sender = node != NULL ? gg_t2a(xi.getText(node)) : NULL;
						gg_netlog(gg, "gg_mainthread(%x): XML Action type: %s.", gg, type != NULL ? type : "unknown");
						// Avatar change notify
						if (type != NULL && !strcmp(type, "28")) {
							gg_netlog(gg, "gg_mainthread(%x): Client %s changed his avatar.", gg, sender);
							gg_requestavatar(gg, gg_getcontact(gg, atoi(sender), 0, 0, NULL), 0);
						}
						mir_free(type);
						mir_free(sender);
						xi.destroyNode(hXml);
					}
					mir_free(tag);
					mir_free(xmlAction);
				}
				break;

			case GG_EVENT_TYPING_NOTIFICATION:
				{
					HANDLE hContact = gg_getcontact(gg, e->event.typing_notification.uin, 0, 0, NULL);
#ifdef DEBUGMODE
					gg_netlog(gg, "gg_mainthread(%x): Typing notification from %d (%d).", gg,
						e->event.typing_notification.uin, e->event.typing_notification.length);
#endif
					CallService(MS_PROTO_CONTACTISTYPING, (WPARAM)hContact,
						e->event.typing_notification.length > 0 ? 7 : PROTOTYPE_CONTACTTYPING_OFF);
				}
				break;
		}
		// Free event struct
		gg_free_event(e);
	}

	gg_broadcastnewstatus(gg, ID_STATUS_OFFLINE);
	gg_setalloffline(gg);

	// If it was unwanted disconnection reconnect
	if(gg->proto.m_iDesiredStatus != ID_STATUS_OFFLINE
		&& DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_ARECONNECT, GG_KEYDEF_ARECONNECT))
	{
		gg_netlog(gg, "gg_mainthread(%x): Unintentional disconnection detected. Going to reconnect...", gg);
		hostnum = 0;
		gg_broadcastnewstatus(gg, ID_STATUS_CONNECTING);
		mir_free(p.status_descr);
		goto retry;
	}

	mir_free(p.password);
	mir_free(p.status_descr);

	// Destroy concurrent sessions list
	{
		list_t l;
		EnterCriticalSection(&gg->sessions_mutex);
		for (l = gg->sessions; l; l = l->next)
		{
			struct gg_multilogon_session* sess = (struct gg_multilogon_session*)l->data;
			mir_free(sess->name);
			mir_free(sess);
		}
		list_destroy(gg->sessions, 0);
		gg->sessions = NULL;
		LeaveCriticalSection(&gg->sessions_mutex);
	}

	// Stop dcc server
	gg->pth_dcc.dwThreadId = 0;
#ifdef DEBUGMODE
	gg_netlog(gg, "gg_mainthread(%x): Waiting until DCC Server Thread finished, if needed.", gg);
#endif
	gg_threadwait(gg, &gg->pth_dcc);

	gg_netlog(gg, "gg_mainthread(%x): Server Thread Ending", gg);
	return;
}

////////////////////////////////////////////////////////////
// Change status function
void gg_broadcastnewstatus(GGPROTO *gg, int newStatus)
{
	int oldStatus;

	EnterCriticalSection(&gg->modemsg_mutex);
	oldStatus = gg->proto.m_iStatus;
	if(oldStatus == newStatus)
	{
		LeaveCriticalSection(&gg->modemsg_mutex);
		return;
	}
	gg->proto.m_iStatus = newStatus;
	LeaveCriticalSection(&gg->modemsg_mutex);

	ProtoBroadcastAck(GG_PROTO, NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE) oldStatus, newStatus);

	gg_netlog(gg, "gg_broadcastnewstatus(): Broadcast new status: %d.", newStatus);
}

////////////////////////////////////////////////////////////
// When user is deleted
int gg_userdeleted(GGPROTO *gg, WPARAM wParam, LPARAM lParam)
{
	HANDLE hContact = (HANDLE) wParam;
	uin_t uin; int type;
	DBVARIANT dbv;

	if(!hContact) return 0;

	uin = (uin_t)DBGetContactSettingDword(hContact, GG_PROTO, GG_KEY_UIN, 0);
	type = DBGetContactSettingByte(hContact, GG_PROTO, "ChatRoom", 0);

	// Terminate conference if contact is deleted
	if(type && !DBGetContactSetting(hContact, GG_PROTO, "ChatRoomID", &dbv) && gg->gc_enabled)
	{
		GCDEST gcdest = {GG_PROTO, dbv.pszVal, GC_EVENT_CONTROL};
		GCEVENT gcevent = {sizeof(GCEVENT), &gcdest};
		GGGC *chat = gg_gc_lookup(gg, dbv.pszVal);

		gg_netlog(gg, "gg_gc_event(): Terminating chat %x, id %s from contact list...", chat, dbv.pszVal);
		if(chat)
		{
			// Destroy chat entry
			free(chat->recipients);
			list_remove(&gg->chats, chat, 1);
			// Terminate chat window / shouldn't cascade entry is deleted
			CallServiceSync(MS_GC_EVENT, SESSION_OFFLINE, (LPARAM)&gcevent);
			CallServiceSync(MS_GC_EVENT, SESSION_TERMINATE, (LPARAM)&gcevent);
		}

		DBFreeVariant(&dbv);
		return 0;
	}

	if(uin && gg_isonline(gg))
	{
		EnterCriticalSection(&gg->sess_mutex);
		gg_remove_notify_ex(gg->sess, uin, GG_USER_NORMAL);
		LeaveCriticalSection(&gg->sess_mutex);
	}

	return 0;
}

////////////////////////////////////////////////////////////
// When db settings changed
int gg_dbsettingchanged(GGPROTO *gg, WPARAM wParam, LPARAM lParam)
{
	DBCONTACTWRITESETTING *cws = (DBCONTACTWRITESETTING *) lParam;
	HANDLE hContact = (HANDLE) wParam;
	char *szProto = NULL;

	// Check if the contact is NULL or we are not online
	if(!hContact || !gg_isonline(gg))
		return 0;

	// Fetch protocol name and check if it's our
	if(!(szProto = (char *) CallService(MS_PROTO_GETCONTACTBASEPROTO, wParam, 0)) || strcmp(szProto, GG_PROTO))
		return 0;

	// If ignorance changed
	if(!strcmp(cws->szModule, "Ignore") && !strcmp(cws->szSetting, "Mask1"))
	{
		gg_notifyuser(gg, hContact, 1);
		return 0;
	}

	// Contact is being renamed
	if(gg->gc_enabled && !strcmp(cws->szModule, GG_PROTO) && !strcmp(cws->szSetting, GG_KEY_NICK)
		&& cws->value.pszVal)
	{
		// Groupchat window contact is being renamed
		DBVARIANT dbv;
		int type = DBGetContactSettingByte(hContact, GG_PROTO, "ChatRoom", 0);
		if(type && !DBGetContactSetting(hContact, GG_PROTO, "ChatRoomID", &dbv))
		{
			// Most important... check redundancy (fucking cascading)
			static int cascade = 0;
			if(!cascade && dbv.pszVal)
			{
				GCDEST gcdest = {GG_PROTO, dbv.pszVal, GC_EVENT_CHANGESESSIONAME};
				GCEVENT gcevent = {sizeof(GCEVENT), &gcdest};
				gcevent.pszText = cws->value.pszVal;
				gg_netlog(gg, "gg_dbsettingchanged(): Conference %s was renamed to %s.", dbv.pszVal, cws->value.pszVal);
				// Mark cascading
				/* FIXME */ cascade = 1;
				CallServiceSync(MS_GC_EVENT, 0, (LPARAM)&gcevent);
				/* FIXME */ cascade = 0;
			}
			DBFreeVariant(&dbv);
		}
		else
			// Change contact name on all chats
			gg_gc_changenick(gg, hContact, cws->value.pszVal);
	}

	// Contact list changes
	if(!strcmp(cws->szModule, "CList"))
	{
		// If name changed... change nick
		if(!strcmp(cws->szSetting, "MyHandle") && cws->value.type == DBVT_ASCIIZ && cws->value.pszVal)
			DBWriteContactSettingString(hContact, GG_PROTO, GG_KEY_NICK, cws->value.pszVal);

		// If not on list changed
		if(!strcmp(cws->szSetting, "NotOnList"))
		{
			if(DBGetContactSettingByte(hContact, "CList", "Hidden", 0))
				return 0;
			if(cws->value.type == DBVT_DELETED || (cws->value.type == DBVT_BYTE && cws->value.bVal == 0))
			{
				// Notify user normally this time if added to the list permanently
				DBDeleteContactSetting(hContact, GG_PROTO, GG_KEY_DELETEUSER); // What is it ?? I don't remember
				gg_notifyuser(gg, hContact, 1);
			}
		}
	}
	return 0;
}

////////////////////////////////////////////////////////////
// All users set offline
void gg_setalloffline(GGPROTO *gg)
{
	HANDLE hContact;
	char *szProto;

	gg_netlog(gg, "gg_setalloffline(): Setting buddies offline");
	DBWriteContactSettingWord(NULL, GG_PROTO, GG_KEY_STATUS, ID_STATUS_OFFLINE);
	hContact = (HANDLE) CallService(MS_DB_CONTACT_FINDFIRST, 0, 0);
	while (hContact)
	{
		szProto = (char *) CallService(MS_PROTO_GETCONTACTBASEPROTO, (WPARAM) hContact, 0);
		if(szProto != NULL && !strcmp(szProto, GG_PROTO))
		{
			DBWriteContactSettingWord(hContact, GG_PROTO, GG_KEY_STATUS, ID_STATUS_OFFLINE);
			// Clear IP and port settings
			DBDeleteContactSetting(hContact, GG_PROTO, GG_KEY_CLIENTIP);
			DBDeleteContactSetting(hContact, GG_PROTO, GG_KEY_CLIENTPORT);
			// Delete status descr
			DBDeleteContactSetting(hContact, "CList", GG_KEY_STATUSDESCR);
		}
		hContact = (HANDLE) CallService(MS_DB_CONTACT_FINDNEXT, (WPARAM) hContact, 0);
	}
#ifdef DEBUGMODE
	gg_netlog(gg, "gg_setalloffline(): End");
#endif
}

////////////////////////////////////////////////////////////
// All users set offline
void gg_notifyuser(GGPROTO *gg, HANDLE hContact, int refresh)
{
	uin_t uin;
	if(!hContact) return;
	if(gg_isonline(gg) && (uin = (uin_t)DBGetContactSettingDword(hContact, GG_PROTO, GG_KEY_UIN, 0)))
	{
		// Check if user should be invisible
		// Or be blocked ?
		if((DBGetContactSettingWord(hContact, GG_PROTO, GG_KEY_APPARENT, (WORD) ID_STATUS_ONLINE) == ID_STATUS_OFFLINE) ||
			DBGetContactSettingByte(hContact, "CList", "NotOnList", 0))
		{
			EnterCriticalSection(&gg->sess_mutex);
			if(refresh)
			{
				gg_remove_notify_ex(gg->sess, uin, GG_USER_NORMAL);
				gg_remove_notify_ex(gg->sess, uin, GG_USER_BLOCKED);
			}

			gg_add_notify_ex(gg->sess, uin, GG_USER_OFFLINE);
			LeaveCriticalSection(&gg->sess_mutex);
		}
		else if(DBGetContactSettingDword(hContact, "Ignore", "Mask1", (DWORD)0 ) & IGNOREEVENT_MESSAGE)
		{
			EnterCriticalSection(&gg->sess_mutex);
			if(refresh)
			{
				gg_remove_notify_ex(gg->sess, uin, GG_USER_OFFLINE);
			}

			gg_add_notify_ex(gg->sess, uin, GG_USER_BLOCKED);
			LeaveCriticalSection(&gg->sess_mutex);
		}
		else
		{
			EnterCriticalSection(&gg->sess_mutex);
			if(refresh)
			{
				gg_remove_notify_ex(gg->sess, uin, GG_USER_BLOCKED);
			}

			gg_add_notify_ex(gg->sess, uin, GG_USER_NORMAL);
			LeaveCriticalSection(&gg->sess_mutex);
		}
	}
}
void gg_notifyall(GGPROTO *gg)
{
	HANDLE hContact;
	char *szProto;
	int count = 0, cc = 0;
	uin_t *uins;
	char *types;

	gg_netlog(gg, "gg_notifyall(): Subscribing notification to all users");
	// Readup count
	hContact = (HANDLE) CallService(MS_DB_CONTACT_FINDFIRST, 0, 0);
	while (hContact)
	{
		szProto = (char *) CallService(MS_PROTO_GETCONTACTBASEPROTO, (WPARAM) hContact, 0);
		if(szProto != NULL && !strcmp(szProto, GG_PROTO)) count ++;
		hContact = (HANDLE) CallService(MS_DB_CONTACT_FINDNEXT, (WPARAM) hContact, 0);
	}

	// Readup list
	/* FIXME: If we have nothing on the list but we omit gg_notify_ex we have problem with receiving any contacts */
	if(count == 0)
	{
		if(gg_isonline(gg))
		{
			EnterCriticalSection(&gg->sess_mutex);
			gg_notify_ex(gg->sess, NULL, NULL, 0);
			LeaveCriticalSection(&gg->sess_mutex);
		}
		return;
	}
	uins = calloc(sizeof(uin_t), count);
	types = calloc(sizeof(char), count);

	hContact = (HANDLE) CallService(MS_DB_CONTACT_FINDFIRST, 0, 0);
	while (hContact && cc < count)
	{
		szProto = (char *) CallService(MS_PROTO_GETCONTACTBASEPROTO, (WPARAM) hContact, 0);
		if(szProto != NULL && !strcmp(szProto, GG_PROTO) && (uins[cc] = DBGetContactSettingDword(hContact, GG_PROTO, GG_KEY_UIN, 0)))
		{
			if((DBGetContactSettingWord(hContact, GG_PROTO, GG_KEY_APPARENT, (WORD) ID_STATUS_ONLINE) == ID_STATUS_OFFLINE) ||
				DBGetContactSettingByte(hContact, "CList", "NotOnList", 0))
				types[cc] = GG_USER_OFFLINE;
			else if(DBGetContactSettingDword(hContact, "Ignore", "Mask1", (DWORD)0 ) & IGNOREEVENT_MESSAGE)
				types[cc] = GG_USER_BLOCKED;
			else
				types[cc] = GG_USER_NORMAL;
			cc ++;
		}
		hContact = (HANDLE) CallService(MS_DB_CONTACT_FINDNEXT, (WPARAM) hContact, 0);
	}
	if(cc < count) count = cc;

	// Send notification
	if(gg_isonline(gg))
	{
		EnterCriticalSection(&gg->sess_mutex);
		gg_notify_ex(gg->sess, uins, types, count);
		LeaveCriticalSection(&gg->sess_mutex);
	}

	// Free variables
	free(uins); free(types);
}

////////////////////////////////////////////////////////////
// Get contact by uin
HANDLE gg_getcontact(GGPROTO *gg, uin_t uin, int create, int inlist, char *szNick)
{
	HANDLE hContact;
	char *szProto;

	// Look for contact in DB
	hContact = (HANDLE) CallService(MS_DB_CONTACT_FINDFIRST, 0, 0);
	while (hContact)
	{
		szProto = (char *) CallService(MS_PROTO_GETCONTACTBASEPROTO, (WPARAM) hContact, 0);
		if(szProto != NULL && !strcmp(szProto, GG_PROTO))
		{
			if((uin_t)DBGetContactSettingDword(hContact, GG_PROTO, GG_KEY_UIN, 0) == uin
				&& DBGetContactSettingByte(hContact, GG_PROTO, "ChatRoom", 0) == 0)
			{
				if(inlist)
				{
					DBDeleteContactSetting(hContact, "CList", "NotOnList");
					DBDeleteContactSetting(hContact, "CList", "Hidden");
				}
				return hContact;
			}
		}
		hContact = (HANDLE) CallService(MS_DB_CONTACT_FINDNEXT, (WPARAM) hContact, 0);
	}
	if(!create) return NULL;

	hContact = (HANDLE) CallService(MS_DB_CONTACT_ADD, 0, 0);

	if(!hContact)
	{
		gg_netlog(gg, "gg_getcontact(): Failed to create Gadu-Gadu contact %s", szNick);
		return NULL;
	}

	if(CallService(MS_PROTO_ADDTOCONTACT, (WPARAM) hContact, (LPARAM) GG_PROTO) != 0)
	{
		// For some reason we failed to register the protocol for this contact
		CallService(MS_DB_CONTACT_DELETE, (WPARAM) hContact, 0);
		gg_netlog(gg, "Failed to register GG contact %d", uin);
		return NULL;
	}

	gg_netlog(gg, "gg_getcontact(): Added buddy: %d", uin);
	if(!inlist)
	{
		DBWriteContactSettingByte(hContact, "CList", "NotOnList", 1);
		//DBWriteContactSettingByte(hContact, "CList", "Hidden", 1);
	}

	DBWriteContactSettingDword(hContact, GG_PROTO, GG_KEY_UIN, (DWORD) uin);
	DBWriteContactSettingWord(hContact, GG_PROTO, GG_KEY_STATUS, ID_STATUS_OFFLINE);

	// If nick specified use it
	if(szNick)
		DBWriteContactSettingString(hContact, GG_PROTO, GG_KEY_NICK, szNick);
	else if(gg_isonline(gg))
	{
		gg_pubdir50_t req;

		// Search for that nick
		if(req = gg_pubdir50_new(GG_PUBDIR50_SEARCH))
		{
			// Add uin and search it
			gg_pubdir50_add(req, GG_PUBDIR50_UIN, ditoa(uin));
			gg_pubdir50_seq_set(req, GG_SEQ_GETNICK);
			EnterCriticalSection(&gg->sess_mutex);
			gg_pubdir50(gg->sess, req);
			LeaveCriticalSection(&gg->sess_mutex);
			gg_pubdir50_free(req);
			DBWriteContactSettingString(hContact, GG_PROTO, GG_KEY_NICK, ditoa(uin));
			gg_netlog(gg, "gg_getcontact(): Search for nick on uin: %d", uin);
		}
	}

	// Add to notify list and pull avatar for the new contact
	if(gg_isonline(gg))
	{
		PROTO_AVATAR_INFORMATION pai = {0};

		EnterCriticalSection(&gg->sess_mutex);
		gg_add_notify_ex(gg->sess, uin, (char)(inlist ? GG_USER_NORMAL : GG_USER_OFFLINE));
		LeaveCriticalSection(&gg->sess_mutex);

		pai.cbSize = sizeof(pai);
		pai.hContact = hContact;
		gg_getavatarinfo(gg, (WPARAM)GAIF_FORCE, (LPARAM)&pai);

		// Change status of the contact with our own UIN (if got yourself added to the contact list)
		if (DBGetContactSettingDword(NULL, GG_PROTO, GG_KEY_UIN, 0) == uin)
		{
			char *szMsg;
			EnterCriticalSection(&gg->modemsg_mutex);
			szMsg = mir_strdup(gg_getstatusmsg(gg, gg->proto.m_iStatus));
			LeaveCriticalSection(&gg->modemsg_mutex);
			gg_changecontactstatus(gg, uin, status_m2gg(gg, gg->proto.m_iStatus, szMsg != NULL), szMsg, 0, 0, 0, 0);
			mir_free(szMsg);
		}
	}

	// TODO server side list & add buddy
	return hContact;
}

////////////////////////////////////////////////////////////
// Status conversion
int status_m2gg(GGPROTO *gg, int status, int descr)
{
	// check frends only
	int mask = DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_FRIENDSONLY, GG_KEYDEF_FRIENDSONLY) ? GG_STATUS_FRIENDS_MASK : 0;

	if(descr)
	{
		switch(status)
		{
			case ID_STATUS_OFFLINE:
				return GG_STATUS_NOT_AVAIL_DESCR | mask;

			case ID_STATUS_ONLINE:
				return GG_STATUS_AVAIL_DESCR | mask;

			case ID_STATUS_AWAY:
				return GG_STATUS_BUSY_DESCR | mask;

			case ID_STATUS_DND:
				return GG_STATUS_DND_DESCR | mask;

			case ID_STATUS_FREECHAT:
				return GG_STATUS_FFC_DESCR | mask;

			case ID_STATUS_INVISIBLE:
				return GG_STATUS_INVISIBLE_DESCR | mask;

			default:
				return GG_STATUS_BUSY_DESCR | mask;
		}
	}
	else
	{
		switch(status)
		{
			case ID_STATUS_OFFLINE:
				return GG_STATUS_NOT_AVAIL | mask;

			case ID_STATUS_ONLINE:
				return GG_STATUS_AVAIL | mask;

			case ID_STATUS_AWAY:
				return GG_STATUS_BUSY | mask;

			case ID_STATUS_DND:
				return GG_STATUS_DND | mask;

			case ID_STATUS_FREECHAT:
				return GG_STATUS_FFC | mask;

			case ID_STATUS_INVISIBLE:
				return GG_STATUS_INVISIBLE | mask;

			default:
				return GG_STATUS_BUSY | mask;
		}
	}
}
int status_gg2m(GGPROTO *gg, int status)
{
	// ignore additional flags
	status = GG_S(status);

	// when user has status description but is offline (show it invisible)
	if(status == GG_STATUS_NOT_AVAIL_DESCR && DBGetContactSettingByte(NULL, GG_PROTO, GG_KEY_SHOWINVISIBLE, GG_KEYDEF_SHOWINVISIBLE))
		return ID_STATUS_INVISIBLE;

	// rest of cases
	switch(status)
	{
		case GG_STATUS_NOT_AVAIL:
		case GG_STATUS_NOT_AVAIL_DESCR:
			return ID_STATUS_OFFLINE;

		case GG_STATUS_AVAIL:
		case GG_STATUS_AVAIL_DESCR:
			return ID_STATUS_ONLINE;

		case GG_STATUS_BUSY:
		case GG_STATUS_BUSY_DESCR:
			return ID_STATUS_AWAY;

		case GG_STATUS_DND:
		case GG_STATUS_DND_DESCR:
			return ID_STATUS_DND;

		case GG_STATUS_FFC:
		case GG_STATUS_FFC_DESCR:
			return ID_STATUS_FREECHAT;

		case GG_STATUS_INVISIBLE:
		case GG_STATUS_INVISIBLE_DESCR:
			return ID_STATUS_INVISIBLE;

		case GG_STATUS_BLOCKED:
			return ID_STATUS_NA;

		default:
			return ID_STATUS_OFFLINE;
	}
}

////////////////////////////////////////////////////////////
// Called when contact status is changed
void gg_changecontactstatus(GGPROTO *gg, uin_t uin, int status, const char *idescr, int time, uint32_t remote_ip, uint16_t remote_port, uint32_t version)
{
	HANDLE hContact = gg_getcontact(gg, uin, 0, 0, NULL);

	// Check if contact is on list
	if(!hContact) return;

	// Write contact status
	DBWriteContactSettingWord(hContact, GG_PROTO, GG_KEY_STATUS, (WORD)status_gg2m(gg, status));

	// Check if there's description and if it's not empty
	if(idescr && *idescr)
	{
		gg_netlog(gg, "gg_changecontactstatus(): Saving for %d status descr \"%s\".", uin, idescr);
		DBWriteContactSettingString(hContact, "CList", GG_KEY_STATUSDESCR, idescr);
	}
	else
		// Remove status if there's nothing
		DBDeleteContactSetting(hContact, "CList", GG_KEY_STATUSDESCR);

	// Store contact ip and port
	if(remote_ip) DBWriteContactSettingDword(hContact, GG_PROTO, GG_KEY_CLIENTIP, (DWORD) swap32(remote_ip));
	if(remote_port) DBWriteContactSettingWord(hContact, GG_PROTO, GG_KEY_CLIENTPORT, (WORD) remote_port);
	if(version)
	{
		char sversion[48];
		DBWriteContactSettingDword(hContact, GG_PROTO, GG_KEY_CLIENTVERSION, (DWORD) version);
		mir_snprintf(sversion, sizeof(sversion), "%sGadu-Gadu %s", (version & 0x00ffffff) > 0x2b ? "Nowe " : "", gg_version2string(version));
		DBWriteContactSettingString(hContact, GG_PROTO, "MirVer", sversion);
	}
}

////////////////////////////////////////////////////////////
// Returns GG client version string from packet version
const char *gg_version2string(int v)
{
	const char *pstr = "???";
	v &= 0x00ffffff;
	switch(v)
	{
		case 0x2e:
			pstr = "8.0 build 8283"; break;
		case 0x2d:
			pstr = "8.0 build 4881"; break;
		case 0x2b:
			pstr = "< 8.0"; break;
		case 0x2a:
			pstr = "7.7 build 3315"; break;
		case 0x29:
			pstr = "7.6 build 1688"; break;
		case 0x28:
			pstr = "7.5 build 2201"; break;
		case 0x27:
			pstr = "7.0 build 22"; break;
		case 0x26:
			pstr = "7.0 build 20"; break;
		case 0x25:
			pstr = "7.0 build 1"; break;
		case 0x24:
			pstr = "6.1 (155) / 7.6 (1359)"; break;
		case 0x22:
			pstr = "6.0 build 140"; break;
		case 0x21:
			pstr = "6.0 build 133"; break;
		case 0x20:
			pstr = "6.0b"; break;
		case 0x1e:
			pstr = "5.7b build 121"; break;
		case 0x1c:
			pstr = "5.7b"; break;
		case 0x1b:
			pstr = "5.0.5"; break;
		case 0x19:
			pstr = "5.0.3"; break;
		case 0x18:
			pstr = "5.0.0-1"; break;
		case 0x17:
			pstr = "4.9.2"; break;
		case 0x16:
			pstr = "4.9.1"; break;
		case 0x15:
			pstr = "4.8.9"; break;
		case 0x14:
			pstr = "4.8.1-3"; break;
		case 0x11:
			pstr = "4.6.1-10"; break;
		case 0x10:
			pstr = "4.5.15-22"; break;
		case 0x0f:
			pstr = "4.5.12"; break;
		case 0x0b:
			pstr = "4.0.25-30"; break;
		default:
			if (v < 0x0b)
				pstr = "< 4.0.25";
			else if (v > 0x2e)
				pstr = ">= 8.0";
			break;
	}
	return pstr;
}
