view src/protocols/gg/lib/libgadu.c @ 12007:8724718d387f

[gaim-migrate @ 14300] Further updates to gg from Bartosz Oler. This makes the login and token retrieval processes asynchronous. There are also some encoding fixes and various cleanups. There are some changes from me to make it work on win32. committer: Tailor Script <tailor@pidgin.im>
author Daniel Atallah <daniel.atallah@gmail.com>
date Tue, 08 Nov 2005 19:45:09 +0000
parents 3c536224f0d0
children 9cbc5967fbfd
line wrap: on
line source

/* $Id: libgadu.c 14300 2005-11-08 19:45:09Z datallah $ */

/*
 *  (C) Copyright 2001-2003 Wojtek Kaniewski <wojtekka@irc.pl>
 *                          Robert J. Woźny <speedy@ziew.org>
 *                          Arkadiusz Miśkiewicz <arekm@pld-linux.org>
 *                          Tomasz Chiliński <chilek@chilan.com>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License Version
 *  2.1 as published by the Free Software Foundation.
 *
 *  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 Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser 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 <sys/types.h>
#ifndef _WIN32
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#ifdef sun
#  include <sys/filio.h>
#endif
#else
#include <io.h>
#include <fcntl.h>
#include <errno.h>
#define SHUT_RDWR SD_BOTH
#endif

#include "libgadu-config.h"

#include <errno.h>
#ifndef _WIN32
#include <netdb.h>
#endif
#ifdef __GG_LIBGADU_HAVE_PTHREAD
#  include <pthread.h>
#endif
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#ifdef __GG_LIBGADU_HAVE_OPENSSL
#  include <openssl/err.h>
#  include <openssl/rand.h>
#endif

#include "compat.h"
#include "libgadu.h"

int gg_debug_level = 0;
void (*gg_debug_handler)(int level, const char *format, va_list ap) = NULL;

int gg_dcc_port = 0;
unsigned long gg_dcc_ip = 0;

unsigned long gg_local_ip = 0;
/*
 * zmienne opisujące parametry proxy http.
 */
char *gg_proxy_host = NULL;
int gg_proxy_port = 0;
int gg_proxy_enabled = 0;
int gg_proxy_http_only = 0;
char *gg_proxy_username = NULL;
char *gg_proxy_password = NULL;

#ifndef lint 
static char rcsid[]
#ifdef __GNUC__
__attribute__ ((unused))
#endif
= "$Id: libgadu.c 14300 2005-11-08 19:45:09Z datallah $";
#endif 

#ifdef _WIN32
/**
 *  Deal with the fact that you can't select() on a win32 file fd.
 *  This makes it practically impossible to tie into gaim's event loop.
 *
 *  -This is thanks to Tor Lillqvist.
 *  XXX - Move this to where the rest of the the win32 compatiblity stuff goes when we push the changes back to libgadu.
 */
static int
socket_pipe (int *fds)
{
	SOCKET temp, socket1 = -1, socket2 = -1;
	struct sockaddr_in saddr;
	int len;
	u_long arg;
	fd_set read_set, write_set;
	struct timeval tv;

	temp = socket(AF_INET, SOCK_STREAM, 0);

	if (temp == INVALID_SOCKET) {
		goto out0;
	}

	arg = 1;
	if (ioctlsocket(temp, FIONBIO, &arg) == SOCKET_ERROR) {
		goto out0;
	}

	memset(&saddr, 0, sizeof(saddr));
	saddr.sin_family = AF_INET;
	saddr.sin_port = 0;
	saddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

	if (bind(temp, (struct sockaddr *)&saddr, sizeof (saddr))) {
		goto out0;
	}

	if (listen(temp, 1) == SOCKET_ERROR) {
		goto out0;
	}

	len = sizeof(saddr);
	if (getsockname(temp, (struct sockaddr *)&saddr, &len)) {
		goto out0;
	}

	socket1 = socket(AF_INET, SOCK_STREAM, 0);

	if (socket1 == INVALID_SOCKET) {
		goto out0;
	}

	arg = 1;
	if (ioctlsocket(socket1, FIONBIO, &arg) == SOCKET_ERROR) {
		goto out1;
	}

	if (connect(socket1, (struct sockaddr  *)&saddr, len) != SOCKET_ERROR ||
			WSAGetLastError() != WSAEWOULDBLOCK) {
		goto out1;
	}

	FD_ZERO(&read_set);
	FD_SET(temp, &read_set);

	tv.tv_sec = 0;
	tv.tv_usec = 0;

	if (select(0, &read_set, NULL, NULL, NULL) == SOCKET_ERROR) {
		goto out1;
	}

	if (!FD_ISSET(temp, &read_set)) {
		goto out1;
	}

	socket2 = accept(temp, (struct sockaddr *) &saddr, &len);
	if (socket2 == INVALID_SOCKET) {
		goto out1;
	}

	FD_ZERO(&write_set);
	FD_SET(socket1, &write_set);

	tv.tv_sec = 0;
	tv.tv_usec = 0;

	if (select(0, NULL, &write_set, NULL, NULL) == SOCKET_ERROR) {
		goto out2;
	}

	if (!FD_ISSET(socket1, &write_set)) {
		goto out2;
	}

	arg = 0;
	if (ioctlsocket(socket1, FIONBIO, &arg) == SOCKET_ERROR) {
		goto out2;
	}

	arg = 0;
	if (ioctlsocket(socket2, FIONBIO, &arg) == SOCKET_ERROR) {
		goto out2;
	}

	fds[0] = socket1;
	fds[1] = socket2;

	closesocket (temp);

	return 0;

out2:
	closesocket (socket2);
out1:
	closesocket (socket1);
out0:
	closesocket (temp);
	errno = EIO;            /* XXX */

	return -1;
}
#endif

/*
 * gg_libgadu_version()
 *
 * zwraca wersję libgadu.
 *
 *  - brak
 *
 * wersja libgadu.
 */
const char *gg_libgadu_version()
{
	return GG_LIBGADU_VERSION;
}

/*
 * gg_fix32()
 *
 * zamienia kolejność bajtów w liczbie 32-bitowej tak, by odpowiadała
 * kolejności bajtów w protokole GG. ze względu na LE-owość serwera,
 * zamienia tylko na maszynach BE-wych.
 *
 *  - x - liczba do zamiany
 *
 * liczba z odpowiednią kolejnością bajtów.
 */
uint32_t gg_fix32(uint32_t x)
{
#ifndef __GG_LIBGADU_BIGENDIAN
	return x;
#else
	return (uint32_t)
		(((x & (uint32_t) 0x000000ffU) << 24) |
		((x & (uint32_t) 0x0000ff00U) << 8) |
		((x & (uint32_t) 0x00ff0000U) >> 8) |
		((x & (uint32_t) 0xff000000U) >> 24));
#endif
}

/*
 * gg_fix16()
 *
 * zamienia kolejność bajtów w liczbie 16-bitowej tak, by odpowiadała
 * kolejności bajtów w protokole GG. ze względu na LE-owość serwera,
 * zamienia tylko na maszynach BE-wych.
 *
 *  - x - liczba do zamiany
 *
 * liczba z odpowiednią kolejnością bajtów.
 */
uint16_t gg_fix16(uint16_t x)
{
#ifndef __GG_LIBGADU_BIGENDIAN
	return x;
#else
	return (uint16_t)
		(((x & (uint16_t) 0x00ffU) << 8) |
		((x & (uint16_t) 0xff00U) >> 8));
#endif
}

/* 
 * gg_login_hash() // funkcja wewnętrzna
 * 
 * liczy hash z hasła i danego seeda.
 * 
 *  - password - hasło do hashowania
 *  - seed - wartość podana przez serwer
 *
 * hash.
 */
unsigned int gg_login_hash(const unsigned char *password, unsigned int seed)
{
	unsigned int x, y, z;

	y = seed;

	for (x = 0; *password; password++) {
		x = (x & 0xffffff00) | *password;
		y ^= x;
		y += x;
		x <<= 8;
		y ^= x;
		x <<= 8;
		y -= x;
		x <<= 8;
		y ^= x;

		z = y & 0x1F;
		y = (y << z) | (y >> (32 - z));
	}

	return y;
}

#ifndef _WIN32

/*
 * gg_resolve() // funkcja wewnętrzna
 *
 * tworzy potok, forkuje się i w drugim procesie zaczyna resolvować 
 * podanego hosta. zapisuje w sesji deskryptor potoku. jeśli coś tam
 * będzie gotowego, znaczy, że można wczytać struct in_addr. jeśli
 * nie znajdzie, zwraca INADDR_NONE.
 *
 *  - fd - wskaźnik gdzie wrzucić deskryptor
 *  - pid - gdzie wrzucić pid procesu potomnego
 *  - hostname - nazwa hosta do zresolvowania
 *
 * 0, -1.
 */
int gg_resolve(int *fd, int *pid, const char *hostname)
{
	int pipes[2], res;
	struct in_addr a;
	int errno2;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_resolve(%p, %p, \"%s\");\n", fd, pid, hostname);
	
	if (!fd || !pid) {
		errno = EFAULT;
		return -1;
	}

	if (pipe(pipes) == -1)
		return -1;

	if ((res = fork()) == -1) {
		errno2 = errno;
		close(pipes[0]);
		close(pipes[1]);
		errno = errno2;
		return -1;
	}

	if (!res) {
		if ((a.s_addr = inet_addr(hostname)) == INADDR_NONE) {
			struct in_addr *hn;
		
			if (!(hn = gg_gethostbyname(hostname)))
				a.s_addr = INADDR_NONE;
			else {
				a.s_addr = hn->s_addr;
				free(hn);
			}
		}

		write(pipes[1], &a, sizeof(a));

		exit(0);
	}

	close(pipes[1]);

	*fd = pipes[0];
	*pid = res;

	return 0;
}
#endif

#ifdef __GG_LIBGADU_HAVE_PTHREAD

struct gg_resolve_pthread_data {
	char *hostname;
	int fd;
};

static void *gg_resolve_pthread_thread(void *arg)
{
	struct gg_resolve_pthread_data *d = arg;
	struct in_addr a;

	pthread_detach(pthread_self());

	if ((a.s_addr = inet_addr(d->hostname)) == INADDR_NONE) {
		struct in_addr *hn;
		
		if (!(hn = gg_gethostbyname(d->hostname)))
			a.s_addr = INADDR_NONE;
		else {
			a.s_addr = hn->s_addr;
			free(hn);
		}
	}

	write(d->fd, &a, sizeof(a));
	close(d->fd);

	free(d->hostname);
	d->hostname = NULL;

	free(d);

	pthread_exit(NULL);

	return NULL;	/* żeby kompilator nie marudził */
}

/*
 * gg_resolve_pthread() // funkcja wewnętrzna
 *
 * tworzy potok, nowy wątek i w nim zaczyna resolvować podanego hosta.
 * zapisuje w sesji deskryptor potoku. jeśli coś tam będzie gotowego,
 * znaczy, że można wczytać struct in_addr. jeśli nie znajdzie, zwraca
 * INADDR_NONE.
 *
 *  - fd - wskaźnik do zmiennej przechowującej desktyptor resolvera
 *  - resolver - wskaźnik do wskaźnika resolvera
 *  - hostname - nazwa hosta do zresolvowania
 *
 * 0, -1.
 */
int gg_resolve_pthread(int *fd, void **resolver, const char *hostname)
{
	struct gg_resolve_pthread_data *d = NULL;
	pthread_t *tmp;
	int pipes[2], new_errno;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_resolve_pthread(%p, %p, \"%s\");\n", fd, resolver, hostname);
	
	if (!resolver || !fd || !hostname) {
		gg_debug(GG_DEBUG_MISC, "// gg_resolve_pthread() invalid arguments\n");
		errno = EFAULT;
		return -1;
	}

	if (!(tmp = malloc(sizeof(pthread_t)))) {
		gg_debug(GG_DEBUG_MISC, "// gg_resolve_pthread() out of memory for pthread id\n");
		return -1;
	}
	
	if (pipe(pipes) == -1) {
		gg_debug(GG_DEBUG_MISC, "// gg_resolve_pthread() unable to create pipes (errno=%d, %s)\n", errno, strerror(errno));
		free(tmp);
		return -1;
	}

	if (!(d = malloc(sizeof(*d)))) {
		gg_debug(GG_DEBUG_MISC, "// gg_resolve_pthread() out of memory\n");
		new_errno = errno;
		goto cleanup;
	}
	
	d->hostname = NULL;

	if (!(d->hostname = strdup(hostname))) {
		gg_debug(GG_DEBUG_MISC, "// gg_resolve_pthread() out of memory\n");
		new_errno = errno;
		goto cleanup;
	}

	d->fd = pipes[1];

	if (pthread_create(tmp, NULL, gg_resolve_pthread_thread, d)) {
		gg_debug(GG_DEBUG_MISC, "// gg_resolve_phread() unable to create thread\n");
		new_errno = errno;
		goto cleanup;
	}

	gg_debug(GG_DEBUG_MISC, "// gg_resolve_pthread() %p\n", tmp);

	*resolver = tmp;

	*fd = pipes[0];

	return 0;

cleanup:
	if (d) {
		free(d->hostname);
		free(d);
	}

	close(pipes[0]);
	close(pipes[1]);

	free(tmp);

	errno = new_errno;

	return -1;
}

#elif defined _WIN32

struct gg_resolve_win32thread_data {
	char *hostname;
	int fd;
};

static DWORD WINAPI gg_resolve_win32thread_thread(LPVOID arg)
{
	struct gg_resolve_win32thread_data *d = arg;
	struct in_addr a;

	if ((a.s_addr = inet_addr(d->hostname)) == INADDR_NONE) {
		struct in_addr *hn;
		
		if (!(hn = gg_gethostbyname(d->hostname)))
			a.s_addr = INADDR_NONE;
		else {
			a.s_addr = hn->s_addr;
			free(hn);
		}
	}

	write(d->fd, &a, sizeof(a));
	close(d->fd);

	free(d->hostname);
	d->hostname = NULL;

	free(d);

	return 0;
}


int gg_resolve_win32thread(int *fd, void **resolver, const char *hostname)
{
	struct gg_resolve_win32thread_data *d = NULL;
	HANDLE h;
	DWORD dwTId;
	int pipes[2], new_errno;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_resolve_win32thread(%p, %p, \"%s\");\n", fd, resolver, hostname);
	
	if (!resolver || !fd || !hostname) {
		gg_debug(GG_DEBUG_MISC, "// gg_resolve_win32thread() invalid arguments\n");
		errno = EFAULT;
		return -1;
	}

	if (socket_pipe(pipes) == -1) {
		gg_debug(GG_DEBUG_MISC, "// gg_resolve_win32thread() unable to create pipes (errno=%d, %s)\n", errno, strerror(errno));
		return -1;
	}

	if (!(d = malloc(sizeof(*d)))) {
		gg_debug(GG_DEBUG_MISC, "// gg_resolve_win32thread() out of memory\n");
		new_errno = GetLastError();
		goto cleanup;
	}

	d->hostname = NULL;

	if (!(d->hostname = strdup(hostname))) {
		gg_debug(GG_DEBUG_MISC, "// gg_resolve_win32thread() out of memory\n");
		new_errno = GetLastError();
		goto cleanup;
	}

	d->fd = pipes[1];

	h = CreateThread(NULL, 0, gg_resolve_win32thread_thread,
		d, 0, &dwTId);

	if (h == NULL) {
		gg_debug(GG_DEBUG_MISC, "// gg_resolve_win32thread() unable to create thread\n");
		new_errno = GetLastError();
		goto cleanup;
	}

	*resolver = h;
	*fd = pipes[0];

	return 0;

cleanup:
	if (d) {
		free(d->hostname);
		free(d);
	}

	close(pipes[0]);
	close(pipes[1]);

	errno = new_errno;

	return -1;

}
#endif

/*
 * gg_read() // funkcja pomocnicza
 *
 * czyta z gniazda określoną ilość bajtów. bierze pod uwagę, czy mamy
 * połączenie zwykłe czy TLS.
 *
 *  - sess - sesja,
 *  - buf - bufor,
 *  - length - ilość bajtów,
 *
 * takie same wartości jak read().
 */
int gg_read(struct gg_session *sess, char *buf, int length)
{
	int res;

#ifdef __GG_LIBGADU_HAVE_OPENSSL
	if (sess->ssl) {
		int err;

		res = SSL_read(sess->ssl, buf, length);

		if (res < 0) {
			err = SSL_get_error(sess->ssl, res);

			if (err == SSL_ERROR_WANT_READ)
				errno = EAGAIN;

			return -1;
		}
	} else
#endif
		res = read(sess->fd, buf, length);

	return res;
}

/*
 * gg_write() // funkcja pomocnicza
 *
 * zapisuje do gniazda określoną ilość bajtów. bierze pod uwagę, czy mamy
 * połączenie zwykłe czy TLS.
 *
 *  - sess - sesja,
 *  - buf - bufor,
 *  - length - ilość bajtów,
 *
 * takie same wartości jak write().
 */
int gg_write(struct gg_session *sess, const char *buf, int length)
{
	int res = 0;

#ifdef __GG_LIBGADU_HAVE_OPENSSL
	if (sess->ssl) {
		int err;

		res = SSL_write(sess->ssl, buf, length);

		if (res < 0) {
			err = SSL_get_error(sess->ssl, res);

			if (err == SSL_ERROR_WANT_WRITE)
				errno = EAGAIN;

			return -1;
		}
	} else
#endif
	{
		int written = 0;
		
		while (written < length) {
			res = write(sess->fd, buf + written, length - written);

			if (res == -1) {
				if (errno == EAGAIN)
					continue;
				else
					break;
			} else {
				written += res;
				res = written;
			}
		}
	}

	return res;
}

/*
 * gg_recv_packet() // funkcja wewnętrzna
 *
 * odbiera jeden pakiet i zwraca wskaźnik do niego. pamięć po nim
 * należy zwolnić za pomocą free().
 *
 *  - sess - opis sesji
 *
 * w przypadku błędu NULL, kod błędu w errno. należy zwrócić uwagę, że gdy
 * połączenie jest nieblokujące, a kod błędu wynosi EAGAIN, nie udało się
 * odczytać całego pakietu i nie należy tego traktować jako błąd.
 */
void *gg_recv_packet(struct gg_session *sess)
{
	struct gg_header h;
	char *buf = NULL;
	int ret = 0;
	unsigned int offset, size = 0;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_recv_packet(%p);\n", sess);
	
	if (!sess) {
		errno = EFAULT;
		return NULL;
	}

	if (sess->recv_left < 1) {
		if (sess->header_buf) {
			memcpy(&h, sess->header_buf, sess->header_done);
			gg_debug(GG_DEBUG_MISC, "// gg_recv_packet() header recv: resuming last read (%d bytes left)\n", sizeof(h) - sess->header_done);
			free(sess->header_buf);
			sess->header_buf = NULL;
		} else
			sess->header_done = 0;

		while (sess->header_done < sizeof(h)) {
			ret = gg_read(sess, (char*) &h + sess->header_done, sizeof(h) - sess->header_done);

			gg_debug(GG_DEBUG_MISC, "// gg_recv_packet() header recv(%d,%p,%d) = %d\n", sess->fd, &h + sess->header_done, sizeof(h) - sess->header_done, ret);

			if (!ret) {
				errno = ECONNRESET;
				gg_debug(GG_DEBUG_MISC, "// gg_recv_packet() header recv() failed: connection broken\n");
				return NULL;
			}

			if (ret == -1) {
				if (errno == EINTR) {
					gg_debug(GG_DEBUG_MISC, "// gg_recv_packet() header recv() interrupted system call, resuming\n");
					continue;
				}

				if (errno == EAGAIN) {
					gg_debug(GG_DEBUG_MISC, "// gg_recv_packet() header recv() incomplete header received\n");

					if (!(sess->header_buf = malloc(sess->header_done))) {
						gg_debug(GG_DEBUG_MISC, "// gg_recv_packet() header recv() not enough memory\n");
						return NULL;
					}

					memcpy(sess->header_buf, &h, sess->header_done);

					return NULL;
				}

				gg_debug(GG_DEBUG_MISC, "// gg_recv_packet() header recv() failed: errno=%d, %s\n", errno, strerror(errno));

				return NULL;
			}

			sess->header_done += ret;

		}

		h.type = gg_fix32(h.type);
		h.length = gg_fix32(h.length);
	} else
		memcpy(&h, sess->recv_buf, sizeof(h));
	
	/* jakieś sensowne limity na rozmiar pakietu */
	if (h.length > 65535) {
		gg_debug(GG_DEBUG_MISC, "// gg_recv_packet() invalid packet length (%d)\n", h.length);
		errno = ERANGE;
		return NULL;
	}

	if (sess->recv_left > 0) {
		gg_debug(GG_DEBUG_MISC, "// gg_recv_packet() resuming last gg_recv_packet()\n");
		size = sess->recv_left;
		offset = sess->recv_done;
		buf = sess->recv_buf;
	} else {
		if (!(buf = malloc(sizeof(h) + h.length + 1))) {
			gg_debug(GG_DEBUG_MISC, "// gg_recv_packet() not enough memory for packet data\n");
			return NULL;
		}

		memcpy(buf, &h, sizeof(h));

		offset = 0;
		size = h.length;
	}

	while (size > 0) {
		ret = gg_read(sess, buf + sizeof(h) + offset, size);
		gg_debug(GG_DEBUG_MISC, "// gg_recv_packet() body recv(%d,%p,%d) = %d\n", sess->fd, buf + sizeof(h) + offset, size, ret);
		if (!ret) {
			gg_debug(GG_DEBUG_MISC, "// gg_recv_packet() body recv() failed: connection broken\n");
			errno = ECONNRESET;
			return NULL;
		}
		if (ret > -1 && ret <= size) {
			offset += ret;
			size -= ret;
		} else if (ret == -1) {	
			int errno2 = errno;

			gg_debug(GG_DEBUG_MISC, "// gg_recv_packet() body recv() failed (errno=%d, %s)\n", errno, strerror(errno));
			errno = errno2;

			if (errno == EAGAIN) {
				gg_debug(GG_DEBUG_MISC, "// gg_recv_packet() %d bytes received, %d left\n", offset, size);
				sess->recv_buf = buf;
				sess->recv_left = size;
				sess->recv_done = offset;
				return NULL;
			}
			if (errno != EINTR) {
				free(buf);
				return NULL;
			}
		}
	}

	sess->recv_left = 0;

	if ((gg_debug_level & GG_DEBUG_DUMP)) {
		unsigned int i;

		gg_debug(GG_DEBUG_DUMP, "// gg_recv_packet(%.2x)", h.type);
		for (i = 0; i < sizeof(h) + h.length; i++) 
			gg_debug(GG_DEBUG_DUMP, " %.2x", (unsigned char) buf[i]);
		gg_debug(GG_DEBUG_DUMP, "\n");
	}

	return buf;
}

/*
 * gg_send_packet() // funkcja wewnętrzna
 *
 * konstruuje pakiet i wysyła go do serwera.
 *
 *  - sock - deskryptor gniazda
 *  - type - typ pakietu
 *  - payload_1 - pierwsza część pakietu
 *  - payload_length_1 - długość pierwszej części
 *  - payload_2 - druga część pakietu
 *  - payload_length_2 - długość drugiej części
 *  - ... - kolejne części pakietu i ich długości
 *  - NULL - końcowym parametr (konieczny!)
 *
 * jeśli się powiodło, zwraca 0, w przypadku błędu -1. jeśli errno == ENOMEM,
 * zabrakło pamięci. inaczej był błąd przy wysyłaniu pakietu. dla errno == 0
 * nie wysłano całego pakietu.
 */
int gg_send_packet(struct gg_session *sess, int type, ...)
{
	struct gg_header *h;
	char *tmp;
	unsigned int tmp_length;
	void *payload;
	unsigned int payload_length;
	va_list ap;
	int res;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_send_packet(%p, 0x%.2x, ...)\n", sess, type);

	tmp_length = sizeof(struct gg_header);

	if (!(tmp = malloc(tmp_length))) {
		gg_debug(GG_DEBUG_MISC, "// gg_send_packet() not enough memory for packet header\n");
		return -1;
	}

	va_start(ap, type);

	payload = va_arg(ap, void *);

	while (payload) {
		char *tmp2;

		payload_length = va_arg(ap, unsigned int);

		if (!(tmp2 = realloc(tmp, tmp_length + payload_length))) {
			gg_debug(GG_DEBUG_MISC, "// gg_send_packet() not enough memory for payload\n");
			free(tmp);
			va_end(ap);
			return -1;
		}

		tmp = tmp2;
		
		memcpy(tmp + tmp_length, payload, payload_length);
		tmp_length += payload_length;

		payload = va_arg(ap, void *);
	}

	va_end(ap);

	h = (struct gg_header*) tmp;
	h->type = gg_fix32(type);
	h->length = gg_fix32(tmp_length - sizeof(struct gg_header));

	if ((gg_debug_level & GG_DEBUG_DUMP)) {
		unsigned int i;

		gg_debug(GG_DEBUG_DUMP, "// gg_send_packet(0x%.2x)", gg_fix32(h->type));
		for (i = 0; i < tmp_length; ++i)
			gg_debug(GG_DEBUG_DUMP, " %.2x", (unsigned char) tmp[i]);
		gg_debug(GG_DEBUG_DUMP, "\n");
	}
	
	if ((res = gg_write(sess, tmp, tmp_length)) < tmp_length) {
		gg_debug(GG_DEBUG_MISC, "// gg_send_packet() write() failed. res = %d, errno = %d (%s)\n", res, errno, strerror(errno));
		free(tmp);
		return -1;
	}
	
	free(tmp);	
	return 0;
}

/*
 * gg_session_callback() // funkcja wewnętrzna
 *
 * wywoływany z gg_session->callback, wykonuje gg_watch_fd() i pakuje
 * do gg_session->event jego wynik.
 */
static int gg_session_callback(struct gg_session *s)
{
	if (!s) {
		errno = EFAULT;
		return -1;
	}

	return ((s->event = gg_watch_fd(s)) != NULL) ? 0 : -1;
}

/*
 * gg_login()
 *
 * rozpoczyna procedurę łączenia się z serwerem. resztę obsługuje się przez
 * gg_watch_fd().
 *
 * UWAGA! program musi obsłużyć SIGCHLD, jeśli łączy się asynchronicznie,
 * żeby poprawnie zamknąć proces resolvera.
 *
 *  - p - struktura opisująca początkowy stan. wymagane pola: uin, 
 *    password
 *
 * w przypadku błędu NULL, jeśli idzie dobrze (async) albo poszło
 * dobrze (sync), zwróci wskaźnik do zaalokowanej struct gg_session.
 */
struct gg_session *gg_login(const struct gg_login_params *p)
{
	struct gg_session *sess = NULL;
	char *hostname;
	int port;

	if (!p) {
		gg_debug(GG_DEBUG_FUNCTION, "** gg_login(%p);\n", p);
		errno = EFAULT;
		return NULL;
	}

	gg_debug(GG_DEBUG_FUNCTION, "** gg_login(%p: [uin=%u, async=%d, ...]);\n", p, p->uin, p->async);

	if (!(sess = malloc(sizeof(struct gg_session)))) {
		gg_debug(GG_DEBUG_MISC, "// gg_login() not enough memory for session data\n");
		goto fail;
	}

	memset(sess, 0, sizeof(struct gg_session));

	if (!p->password || !p->uin) {
		gg_debug(GG_DEBUG_MISC, "// gg_login() invalid arguments. uin and password needed\n");
		errno = EFAULT;
		goto fail;
	}

	if (!(sess->password = strdup(p->password))) {
		gg_debug(GG_DEBUG_MISC, "// gg_login() not enough memory for password\n");
		goto fail;
	}

	if (p->status_descr && !(sess->initial_descr = strdup(p->status_descr))) {
		gg_debug(GG_DEBUG_MISC, "// gg_login() not enough memory for status\n");
		goto fail;
	}

	sess->uin = p->uin;
	sess->state = GG_STATE_RESOLVING;
	sess->check = GG_CHECK_READ;
	sess->timeout = GG_DEFAULT_TIMEOUT;
	sess->async = p->async;
	sess->type = GG_SESSION_GG;
	sess->initial_status = p->status;
	sess->callback = gg_session_callback;
	sess->destroy = gg_free_session;
	sess->port = (p->server_port) ? p->server_port : ((gg_proxy_enabled) ? GG_HTTPS_PORT : GG_DEFAULT_PORT);
	sess->server_addr = p->server_addr;
	sess->external_port = p->external_port;
	sess->external_addr = p->external_addr;
	sess->protocol_version = (p->protocol_version) ? p->protocol_version : GG_DEFAULT_PROTOCOL_VERSION;
	if (p->era_omnix)
		sess->protocol_version |= GG_ERA_OMNIX_MASK;
	if (p->has_audio)
		sess->protocol_version |= GG_HAS_AUDIO_MASK;
	sess->client_version = (p->client_version) ? strdup(p->client_version) : NULL;
	sess->last_sysmsg = p->last_sysmsg;
	sess->image_size = p->image_size;
	sess->pid = -1;

	if (p->tls == 1) {
#ifdef __GG_LIBGADU_HAVE_OPENSSL
		char buf[1024];

		OpenSSL_add_ssl_algorithms();

		if (!RAND_status()) {
			char rdata[1024];
			struct {
				time_t time;
				void *ptr;
			} rstruct;

			time(&rstruct.time);
			rstruct.ptr = (void *) &rstruct;			

			RAND_seed((void *) rdata, sizeof(rdata));
			RAND_seed((void *) &rstruct, sizeof(rstruct));
		}

		sess->ssl_ctx = SSL_CTX_new(TLSv1_client_method());

		if (!sess->ssl_ctx) {
			ERR_error_string_n(ERR_get_error(), buf, sizeof(buf));
			gg_debug(GG_DEBUG_MISC, "// gg_login() SSL_CTX_new() failed: %s\n", buf);
			goto fail;
		}

		SSL_CTX_set_verify(sess->ssl_ctx, SSL_VERIFY_NONE, NULL);

		sess->ssl = SSL_new(sess->ssl_ctx);

		if (!sess->ssl) {
			ERR_error_string_n(ERR_get_error(), buf, sizeof(buf));
			gg_debug(GG_DEBUG_MISC, "// gg_login() SSL_new() failed: %s\n", buf);
			goto fail;
		}
#else
		gg_debug(GG_DEBUG_MISC, "// gg_login() client requested TLS but no support compiled in\n");
#endif
	}
	
	if (gg_proxy_enabled) {
		hostname = gg_proxy_host;
		sess->proxy_port = port = gg_proxy_port;
	} else {
		hostname = GG_APPMSG_HOST;
		port = GG_APPMSG_PORT;
	}

	if (!p->async) {
		struct in_addr a;

		if (!p->server_addr || !p->server_port) {
			if ((a.s_addr = inet_addr(hostname)) == INADDR_NONE) {
				struct in_addr *hn;
	
				if (!(hn = gg_gethostbyname(hostname))) {
					gg_debug(GG_DEBUG_MISC, "// gg_login() host \"%s\" not found\n", hostname);
					goto fail;
				} else {
					a.s_addr = hn->s_addr;
					free(hn);
				}
			}
		} else {
			a.s_addr = p->server_addr;
			port = p->server_port;
		}

		sess->hub_addr = a.s_addr;

		if (gg_proxy_enabled)
			sess->proxy_addr = a.s_addr;

		if ((sess->fd = gg_connect(&a, port, 0)) == -1) {
			gg_debug(GG_DEBUG_MISC, "// gg_login() connection failed (errno=%d, %s)\n", errno, strerror(errno));
			goto fail;
		}

		if (p->server_addr && p->server_port)
			sess->state = GG_STATE_CONNECTING_GG;
		else
			sess->state = GG_STATE_CONNECTING_HUB;

		while (sess->state != GG_STATE_CONNECTED) {
			struct gg_event *e;

			if (!(e = gg_watch_fd(sess))) {
				gg_debug(GG_DEBUG_MISC, "// gg_login() critical error in gg_watch_fd()\n");
				goto fail;
			}

			if (e->type == GG_EVENT_CONN_FAILED) {
				errno = EACCES;
				gg_debug(GG_DEBUG_MISC, "// gg_login() could not login\n");
				gg_event_free(e);
				goto fail;
			}

			gg_event_free(e);
		}

		return sess;
	}
	
	if (!sess->server_addr || gg_proxy_enabled) {
#ifdef __GG_LIBGADU_HAVE_PTHREAD
		if (gg_resolve_pthread(&sess->fd, &sess->resolver, hostname)) {
#elif defined _WIN32
		if (gg_resolve_win32thread(&sess->fd, &sess->resolver, hostname)) {
#else
		if (gg_resolve(&sess->fd, &sess->pid, hostname)) {
#endif
			gg_debug(GG_DEBUG_MISC, "// gg_login() resolving failed (errno=%d, %s)\n", errno, strerror(errno));
			goto fail;
		}
	} else {
		if ((sess->fd = gg_connect(&sess->server_addr, sess->port, sess->async)) == -1) {
			gg_debug(GG_DEBUG_MISC, "// gg_login() direct connection failed (errno=%d, %s)\n", errno, strerror(errno));
			goto fail;
		}
		sess->state = GG_STATE_CONNECTING_GG;
		sess->check = GG_CHECK_WRITE;
	}

	return sess;

fail:
	if (sess) {
		if (sess->password)
			free(sess->password);
		if (sess->initial_descr)
			free(sess->initial_descr);
		free(sess);
	}
	
	return NULL;
}

/* 
 * gg_free_session()
 *
 * próbuje zamknąć połączenia i zwalnia pamięć zajmowaną przez sesję.
 *
 *  - sess - opis sesji
 */
void gg_free_session(struct gg_session *sess)
{
	if (!sess)
		return;

	/* XXX dopisać zwalnianie i zamykanie wszystkiego, co mogło zostać */

	if (sess->password)
		free(sess->password);
	
	if (sess->initial_descr)
		free(sess->initial_descr);

	if (sess->client_version)
		free(sess->client_version);

	if (sess->header_buf)
		free(sess->header_buf);

#ifdef __GG_LIBGADU_HAVE_OPENSSL
	if (sess->ssl)
		SSL_free(sess->ssl);

	if (sess->ssl_ctx)
		SSL_CTX_free(sess->ssl_ctx);
#endif

#ifdef __GG_LIBGADU_HAVE_PTHREAD
	if (sess->resolver) {
		pthread_cancel(*((pthread_t*) sess->resolver));
		free(sess->resolver);
		sess->resolver = NULL;
	}
#elif defined _WIN32
	if (sess->resolver) {
		HANDLE h = sess->resolver;
		TerminateThread(h, 0);
		CloseHandle(h);
		sess->resolver = NULL;
	}
#else
	if (sess->pid != -1)
		waitpid(sess->pid, NULL, WNOHANG);
#endif

	if (sess->fd != -1)
		close(sess->fd);

	while (sess->images)
		gg_image_queue_remove(sess, sess->images, 1);

	free(sess);
}

/*
 * gg_change_status()
 *
 * zmienia status użytkownika. przydatne do /away i /busy oraz /quit.
 *
 *  - sess - opis sesji
 *  - status - nowy status użytkownika
 *
 * 0, -1.
 */
int gg_change_status(struct gg_session *sess, int status)
{
	struct gg_new_status p;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_change_status(%p, %d);\n", sess, status);

	if (!sess) {
		errno = EFAULT;
		return -1;
	}

	if (sess->state != GG_STATE_CONNECTED) {
		errno = ENOTCONN;
		return -1;
	}

	p.status = gg_fix32(status);

	sess->status = status;

	return gg_send_packet(sess, GG_NEW_STATUS, &p, sizeof(p), NULL);
}

/*
 * gg_change_status_descr()
 *
 * zmienia status użytkownika na opisowy.
 *
 *  - sess - opis sesji
 *  - status - nowy status użytkownika
 *  - descr - opis statusu
 *
 * 0, -1.
 */
int gg_change_status_descr(struct gg_session *sess, int status, const char *descr)
{
	struct gg_new_status p;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_change_status_descr(%p, %d, \"%s\");\n", sess, status, descr);

	if (!sess || !descr) {
		errno = EFAULT;
		return -1;
	}

	if (sess->state != GG_STATE_CONNECTED) {
		errno = ENOTCONN;
		return -1;
	}

	p.status = gg_fix32(status);

	sess->status = status;

	return gg_send_packet(sess, GG_NEW_STATUS, &p, sizeof(p), descr, (strlen(descr) > GG_STATUS_DESCR_MAXSIZE) ? GG_STATUS_DESCR_MAXSIZE : strlen(descr), NULL);
}

/*
 * gg_change_status_descr_time()
 *
 * zmienia status użytkownika na opisowy z godziną powrotu.
 *
 *  - sess - opis sesji
 *  - status - nowy status użytkownika
 *  - descr - opis statusu
 *  - time - czas w formacie uniksowym
 *
 * 0, -1.
 */
int gg_change_status_descr_time(struct gg_session *sess, int status, const char *descr, int time)
{
	struct gg_new_status p;
	uint32_t newtime;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_change_status_descr_time(%p, %d, \"%s\", %d);\n", sess, status, descr, time);

	if (!sess || !descr || !time) {
		errno = EFAULT;
		return -1;
	}

	if (sess->state != GG_STATE_CONNECTED) {
		errno = ENOTCONN;
		return -1;
	}

	p.status = gg_fix32(status);

	sess->status = status;

	newtime = gg_fix32(time);

	return gg_send_packet(sess, GG_NEW_STATUS, &p, sizeof(p), descr, (strlen(descr) > GG_STATUS_DESCR_MAXSIZE) ? GG_STATUS_DESCR_MAXSIZE : strlen(descr), &newtime, sizeof(newtime), NULL);
}

/*
 * gg_logoff()
 *
 * wylogowuje użytkownika i zamyka połączenie, ale nie zwalnia pamięci.
 *
 *  - sess - opis sesji
 */
void gg_logoff(struct gg_session *sess)
{
	if (!sess)
		return;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_logoff(%p);\n", sess);

	if (GG_S_NA(sess->status & ~GG_STATUS_FRIENDS_MASK))
		gg_change_status(sess, GG_STATUS_NOT_AVAIL);

#ifdef __GG_LIBGADU_HAVE_OPENSSL
	if (sess->ssl)
		SSL_shutdown(sess->ssl);
#endif

#ifdef __GG_LIBGADU_HAVE_PTHREAD
	if (sess->resolver) {
		pthread_cancel(*((pthread_t*) sess->resolver));
		free(sess->resolver);
		sess->resolver = NULL;
	}
#elif defined _WIN32
	if (sess->resolver) {
		HANDLE h = sess->resolver;
		TerminateThread(h, 0);
		CloseHandle(h);
		sess->resolver = NULL;
	}
#else
	if (sess->pid != -1) {
		waitpid(sess->pid, NULL, WNOHANG);
		sess->pid = -1;
	}
#endif
	
	if (sess->fd != -1) {
		shutdown(sess->fd, SHUT_RDWR);
		close(sess->fd);
		sess->fd = -1;
	}
}

/*
 * gg_image_request()
 *
 * wysyła żądanie wysłania obrazka o podanych parametrach.
 *
 *  - sess - opis sesji
 *  - recipient - numer adresata
 *  - size - rozmiar obrazka
 *  - crc32 - suma kontrolna obrazka
 *
 * 0/-1
 */
int gg_image_request(struct gg_session *sess, uin_t recipient, int size, uint32_t crc32)
{
	struct gg_send_msg s;
	struct gg_msg_image_request r;
	char dummy = 0;
	int res;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_image_request(%p, %d, %u, 0x%.4x);\n", sess, recipient, size, crc32);

	if (!sess) {
		errno = EFAULT;
		return -1;
	}
	
	if (sess->state != GG_STATE_CONNECTED) {
		errno = ENOTCONN;
		return -1;
	}

	if (size < 0) {
		errno = EINVAL;
		return -1;
	}

	s.recipient = gg_fix32(recipient);
	s.seq = gg_fix32(0);
	s.msgclass = gg_fix32(GG_CLASS_MSG);

	r.flag = 0x04;
	r.size = gg_fix32(size);
	r.crc32 = gg_fix32(crc32);
	
	res = gg_send_packet(sess, GG_SEND_MSG, &s, sizeof(s), &dummy, 1, &r, sizeof(r), NULL);

	if (!res) {
		struct gg_image_queue *q = malloc(sizeof(*q));
		char *buf;

		if (!q) {
			gg_debug(GG_DEBUG_MISC, "// gg_image_request() not enough memory for image queue\n");
			return -1;
		}

		buf = malloc(size);
		if (size && !buf)
		{
			gg_debug(GG_DEBUG_MISC, "// gg_image_request() not enough memory for image\n");
			free(q);
			return -1;
		}

		memset(q, 0, sizeof(*q));

		q->sender = recipient;
		q->size = size;
		q->crc32 = crc32;
		q->image = buf;

		if (!sess->images)
			sess->images = q;
		else {
			struct gg_image_queue *qq;

			for (qq = sess->images; qq->next; qq = qq->next)
				;

			qq->next = q;
		}
	}

	return res;
}

/*
 * gg_image_reply()
 *
 * wysyła żądany obrazek.
 *
 *  - sess - opis sesji
 *  - recipient - numer adresata
 *  - filename - nazwa pliku
 *  - image - bufor z obrazkiem
 *  - size - rozmiar obrazka
 *
 * 0/-1
 */
int gg_image_reply(struct gg_session *sess, uin_t recipient, const char *filename, const char *image, int size)
{
	struct gg_msg_image_reply *r;
	struct gg_send_msg s;
	const char *tmp;
	char buf[1910];
	int res = -1;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_image_reply(%p, %d, \"%s\", %p, %d);\n", sess, recipient, filename, image, size);

	if (!sess || !filename || !image) {
		errno = EFAULT;
		return -1;
	}

	if (sess->state != GG_STATE_CONNECTED) {
		errno = ENOTCONN;
		return -1;
	}

	if (size < 0) {
		errno = EINVAL;
		return -1;
	}

	/* wytnij ścieżki, zostaw tylko nazwę pliku */
	while ((tmp = strrchr(filename, '/')) || (tmp = strrchr(filename, '\\')))
		filename = tmp + 1;

	if (strlen(filename) < 1 || strlen(filename) > 1024) {
		errno = EINVAL;
		return -1;
	}
	
	s.recipient = gg_fix32(recipient);
	s.seq = gg_fix32(0);
	s.msgclass = gg_fix32(GG_CLASS_MSG);

	buf[0] = 0;
	r = (void*) &buf[1];

	r->flag = 0x05;
	r->size = gg_fix32(size);
	r->crc32 = gg_fix32(gg_crc32(0, image, size));

	while (size > 0) {
		int buflen, chunklen;
		
		/* \0 + struct gg_msg_image_reply */
		buflen = sizeof(struct gg_msg_image_reply) + 1;

		/* w pierwszym kawałku jest nazwa pliku */
		if (r->flag == 0x05) {
			strcpy(buf + buflen, filename);
			buflen += strlen(filename) + 1;
		}

		chunklen = (size >= sizeof(buf) - buflen) ? (sizeof(buf) - buflen) : size;

		memcpy(buf + buflen, image, chunklen);
		size -= chunklen;
		image += chunklen;
		
		res = gg_send_packet(sess, GG_SEND_MSG, &s, sizeof(s), buf, buflen + chunklen, NULL);

		if (res == -1)
			break;

		r->flag = 0x06;
	}

	return res;
}

/*
 * gg_send_message_ctcp()
 *
 * wysyła wiadomość do innego użytkownika. zwraca losowy numer
 * sekwencyjny, który można zignorować albo wykorzystać do potwierdzenia.
 *
 *  - sess - opis sesji
 *  - msgclass - rodzaj wiadomości
 *  - recipient - numer adresata
 *  - message - treść wiadomości
 *  - message_len - długość
 *
 * numer sekwencyjny wiadomości lub -1 w przypadku błędu.
 */
int gg_send_message_ctcp(struct gg_session *sess, int msgclass, uin_t recipient, const unsigned char *message, int message_len)
{
	struct gg_send_msg s;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_send_message_ctcp(%p, %d, %u, ...);\n", sess, msgclass, recipient);

	if (!sess) {
		errno = EFAULT;
		return -1;
	}
	
	if (sess->state != GG_STATE_CONNECTED) {
		errno = ENOTCONN;
		return -1;
	}

	s.recipient = gg_fix32(recipient);
	s.seq = gg_fix32(0);
	s.msgclass = gg_fix32(msgclass);
	
	return gg_send_packet(sess, GG_SEND_MSG, &s, sizeof(s), message, message_len, NULL);
}

/*
 * gg_send_message()
 *
 * wysyła wiadomość do innego użytkownika. zwraca losowy numer
 * sekwencyjny, który można zignorować albo wykorzystać do potwierdzenia.
 *
 *  - sess - opis sesji
 *  - msgclass - rodzaj wiadomości
 *  - recipient - numer adresata
 *  - message - treść wiadomości
 *
 * numer sekwencyjny wiadomości lub -1 w przypadku błędu.
 */
int gg_send_message(struct gg_session *sess, int msgclass, uin_t recipient, const unsigned char *message)
{
	gg_debug(GG_DEBUG_FUNCTION, "** gg_send_message(%p, %d, %u, %p)\n", sess, msgclass, recipient, message);

	return gg_send_message_richtext(sess, msgclass, recipient, message, NULL, 0);
}

/*
 * gg_send_message_richtext()
 *
 * wysyła kolorową wiadomość do innego użytkownika. zwraca losowy numer
 * sekwencyjny, który można zignorować albo wykorzystać do potwierdzenia.
 *
 *  - sess - opis sesji
 *  - msgclass - rodzaj wiadomości
 *  - recipient - numer adresata
 *  - message - treść wiadomości
 *  - format - informacje o formatowaniu
 *  - formatlen - długość informacji o formatowaniu
 *
 * numer sekwencyjny wiadomości lub -1 w przypadku błędu.
 */
int gg_send_message_richtext(struct gg_session *sess, int msgclass, uin_t recipient, const unsigned char *message, const unsigned char *format, int formatlen)
{
	struct gg_send_msg s;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_send_message_richtext(%p, %d, %u, %p, %p, %d);\n", sess, msgclass, recipient, message, format, formatlen);

	if (!sess) {
		errno = EFAULT;
		return -1;
	}

	if (sess->state != GG_STATE_CONNECTED) {
		errno = ENOTCONN;
		return -1;
	}

	if (!message) {
		errno = EFAULT;
		return -1;
	}
	
	s.recipient = gg_fix32(recipient);
	if (!sess->seq)
		sess->seq = 0x01740000 | (rand() & 0xffff);
	s.seq = gg_fix32(sess->seq);
	s.msgclass = gg_fix32(msgclass);
	sess->seq += (rand() % 0x300) + 0x300;
	
	if (gg_send_packet(sess, GG_SEND_MSG, &s, sizeof(s), message, strlen(message) + 1, format, formatlen, NULL) == -1)
		return -1;

	return gg_fix32(s.seq);
}

/*
 * gg_send_message_confer()
 *
 * wysyła wiadomość do kilku użytkownikow (konferencja). zwraca losowy numer
 * sekwencyjny, który można zignorować albo wykorzystać do potwierdzenia.
 *
 *  - sess - opis sesji
 *  - msgclass - rodzaj wiadomości
 *  - recipients_count - ilość adresatów
 *  - recipients - numerki adresatów
 *  - message - treść wiadomości
 *
 * numer sekwencyjny wiadomości lub -1 w przypadku błędu.
 */
int gg_send_message_confer(struct gg_session *sess, int msgclass, int recipients_count, uin_t *recipients, const unsigned char *message)
{
	gg_debug(GG_DEBUG_FUNCTION, "** gg_send_message_confer(%p, %d, %d, %p, %p);\n", sess, msgclass, recipients_count, recipients, message);

	return gg_send_message_confer_richtext(sess, msgclass, recipients_count, recipients, message, NULL, 0);
}

/*
 * gg_send_message_confer_richtext()
 *
 * wysyła kolorową wiadomość do kilku użytkownikow (konferencja). zwraca
 * losowy numer sekwencyjny, który można zignorować albo wykorzystać do
 * potwierdzenia.
 *
 *  - sess - opis sesji
 *  - msgclass - rodzaj wiadomości
 *  - recipients_count - ilość adresatów
 *  - recipients - numerki adresatów
 *  - message - treść wiadomości
 *  - format - informacje o formatowaniu
 *  - formatlen - długość informacji o formatowaniu
 *
 * numer sekwencyjny wiadomości lub -1 w przypadku błędu.
 */
int gg_send_message_confer_richtext(struct gg_session *sess, int msgclass, int recipients_count, uin_t *recipients, const unsigned char *message, const unsigned char *format, int formatlen)
{
	struct gg_send_msg s;
	struct gg_msg_recipients r;
	int i, j, k;
	uin_t *recps;
		
	gg_debug(GG_DEBUG_FUNCTION, "** gg_send_message_confer_richtext(%p, %d, %d, %p, %p, %p, %d);\n", sess, msgclass, recipients_count, recipients, message, format, formatlen);

	if (!sess) {
		errno = EFAULT;
		return -1;
	}

	if (sess->state != GG_STATE_CONNECTED) {
		errno = ENOTCONN;
		return -1;
	}

	if (!message || recipients_count <= 0 || recipients_count > 0xffff || !recipients) {
		errno = EINVAL;
		return -1;
	}
	
	r.flag = 0x01;
	r.count = gg_fix32(recipients_count - 1);
	
	if (!sess->seq)
		sess->seq = 0x01740000 | (rand() & 0xffff);
	s.seq = gg_fix32(sess->seq);
	s.msgclass = gg_fix32(msgclass);

	recps = malloc(sizeof(uin_t) * recipients_count);
	if (!recps)
		return -1;

	for (i = 0; i < recipients_count; i++) {
	 
		s.recipient = gg_fix32(recipients[i]);
		
		for (j = 0, k = 0; j < recipients_count; j++)
			if (recipients[j] != recipients[i]) {
				recps[k] = gg_fix32(recipients[j]);
				k++;
			}
				
		if (!i)
			sess->seq += (rand() % 0x300) + 0x300;
		
		if (gg_send_packet(sess, GG_SEND_MSG, &s, sizeof(s), message, strlen(message) + 1, &r, sizeof(r), recps, (recipients_count - 1) * sizeof(uin_t), format, formatlen, NULL) == -1) {
			free(recps);
			return -1;
		}
	}

	free(recps);
	
	return gg_fix32(s.seq);
}

/*
 * gg_ping()
 *
 * wysyła do serwera pakiet ping.
 *
 *  - sess - opis sesji
 *
 * 0, -1.
 */
int gg_ping(struct gg_session *sess)
{
	gg_debug(GG_DEBUG_FUNCTION, "** gg_ping(%p);\n", sess);

	if (!sess) {
		errno = EFAULT;
		return -1;
	}

	if (sess->state != GG_STATE_CONNECTED) {
		errno = ENOTCONN;
		return -1;
	}

	return gg_send_packet(sess, GG_PING, NULL);
}

/*
 * gg_notify_ex()
 *
 * wysyła serwerowi listę kontaktów (wraz z odpowiadającymi im typami userów),
 * dzięki czemu wie, czyj stan nas interesuje.
 *
 *  - sess - opis sesji
 *  - userlist - wskaźnik do tablicy numerów
 *  - types - wskaźnik do tablicy typów użytkowników
 *  - count - ilość numerków
 *
 * 0, -1.
 */
int gg_notify_ex(struct gg_session *sess, uin_t *userlist, char *types, int count)
{
	struct gg_notify *n;
	uin_t *u;
	char *t;
	int i, res = 0;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_notify_ex(%p, %p, %p, %d);\n", sess, userlist, types, count);
	
	if (!sess) {
		errno = EFAULT;
		return -1;
	}
	
	if (sess->state != GG_STATE_CONNECTED) {
		errno = ENOTCONN;
		return -1;
	}

	if (!userlist || !count)
		return gg_send_packet(sess, GG_LIST_EMPTY, NULL);
	
	while (count > 0) {
		int part_count, packet_type;
		
		if (count > 400) {
			part_count = 400;
			packet_type = GG_NOTIFY_FIRST;
		} else {
			part_count = count;
			packet_type = GG_NOTIFY_LAST;
		}

		if (!(n = (struct gg_notify*) malloc(sizeof(*n) * part_count)))
			return -1;
	
		for (u = userlist, t = types, i = 0; i < part_count; u++, t++, i++) { 
			n[i].uin = gg_fix32(*u);
			n[i].dunno1 = *t;
		}
	
		if (gg_send_packet(sess, packet_type, n, sizeof(*n) * part_count, NULL) == -1) {
			free(n);
			res = -1;
			break;
		}

		count -= part_count;
		userlist += part_count;
		types += part_count;

		free(n);
	}

	return res;
}

/*
 * gg_notify()
 *
 * wysyła serwerowi listę kontaktów, dzięki czemu wie, czyj stan nas
 * interesuje.
 *
 *  - sess - opis sesji
 *  - userlist - wskaźnik do tablicy numerów
 *  - count - ilość numerków
 *
 * 0, -1.
 */
int gg_notify(struct gg_session *sess, uin_t *userlist, int count)
{
	struct gg_notify *n;
	uin_t *u;
	int i, res = 0;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_notify(%p, %p, %d);\n", sess, userlist, count);
	
	if (!sess) {
		errno = EFAULT;
		return -1;
	}
	
	if (sess->state != GG_STATE_CONNECTED) {
		errno = ENOTCONN;
		return -1;
	}

	if (!userlist || !count)
		return gg_send_packet(sess, GG_LIST_EMPTY, NULL);
	
	while (count > 0) {
		int part_count, packet_type;
		
		if (count > 400) {
			part_count = 400;
			packet_type = GG_NOTIFY_FIRST;
		} else {
			part_count = count;
			packet_type = GG_NOTIFY_LAST;
		}
			
		if (!(n = (struct gg_notify*) malloc(sizeof(*n) * part_count)))
			return -1;
	
		for (u = userlist, i = 0; i < part_count; u++, i++) { 
			n[i].uin = gg_fix32(*u);
			n[i].dunno1 = GG_USER_NORMAL;
		}
	
		if (gg_send_packet(sess, packet_type, n, sizeof(*n) * part_count, NULL) == -1) {
			res = -1;
			free(n);
			break;
		}

		free(n);

		userlist += part_count;
		count -= part_count;
	}

	return res;
}

/*
 * gg_add_notify_ex()
 *
 * dodaje do listy kontaktów dany numer w trakcie połączenia.
 * dodawanemu użytkownikowi określamy jego typ (patrz protocol.html)
 *
 *  - sess - opis sesji
 *  - uin - numer
 *  - type - typ
 *
 * 0, -1.
 */
int gg_add_notify_ex(struct gg_session *sess, uin_t uin, char type)
{
	struct gg_add_remove a;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_add_notify_ex(%p, %u, %d);\n", sess, uin, type);
	
	if (!sess) {
		errno = EFAULT;
		return -1;
	}

	if (sess->state != GG_STATE_CONNECTED) {
		errno = ENOTCONN;
		return -1;
	}
	
	a.uin = gg_fix32(uin);
	a.dunno1 = type;
	
	return gg_send_packet(sess, GG_ADD_NOTIFY, &a, sizeof(a), NULL);
}

/*
 * gg_add_notify()
 *
 * dodaje do listy kontaktów dany numer w trakcie połączenia.
 *
 *  - sess - opis sesji
 *  - uin - numer
 *
 * 0, -1.
 */
int gg_add_notify(struct gg_session *sess, uin_t uin)
{
	return gg_add_notify_ex(sess, uin, GG_USER_NORMAL);
}

/*
 * gg_remove_notify_ex()
 *
 * usuwa z listy kontaktów w trakcie połączenia.
 * usuwanemu użytkownikowi określamy jego typ (patrz protocol.html)
 *
 *  - sess - opis sesji
 *  - uin - numer
 *  - type - typ
 *
 * 0, -1.
 */
int gg_remove_notify_ex(struct gg_session *sess, uin_t uin, char type)
{
	struct gg_add_remove a;

	gg_debug(GG_DEBUG_FUNCTION, "** gg_remove_notify_ex(%p, %u, %d);\n", sess, uin, type);
	
	if (!sess) {
		errno = EFAULT;
		return -1;
	}

	if (sess->state != GG_STATE_CONNECTED) {
		errno = ENOTCONN;
		return -1;
	}

	a.uin = gg_fix32(uin);
	a.dunno1 = type;
	
	return gg_send_packet(sess, GG_REMOVE_NOTIFY, &a, sizeof(a), NULL);
}

/*
 * gg_remove_notify()
 *
 * usuwa z listy kontaktów w trakcie połączenia.
 *
 *  - sess - opis sesji
 *  - uin - numer
 *
 * 0, -1.
 */
int gg_remove_notify(struct gg_session *sess, uin_t uin)
{
	return gg_remove_notify_ex(sess, uin, GG_USER_NORMAL);
}

/*
 * gg_userlist_request()
 *
 * wysyła żądanie/zapytanie listy kontaktów na serwerze.
 *
 *  - sess - opis sesji
 *  - type - rodzaj zapytania/żądania
 *  - request - treść zapytania/żądania (może być NULL)
 *
 * 0, -1
 */
int gg_userlist_request(struct gg_session *sess, char type, const char *request)
{
	int len;

	if (!sess) {
		errno = EFAULT;
		return -1;
	}
	
	if (sess->state != GG_STATE_CONNECTED) {
		errno = ENOTCONN;
		return -1;
	}

	if (!request) {
		sess->userlist_blocks = 1;
		return gg_send_packet(sess, GG_USERLIST_REQUEST, &type, sizeof(type), NULL);
	}
	
	len = strlen(request);

	sess->userlist_blocks = 0;

	while (len > 2047) {
		sess->userlist_blocks++;

		if (gg_send_packet(sess, GG_USERLIST_REQUEST, &type, sizeof(type), request, 2047, NULL) == -1)
			return -1;

		if (type == GG_USERLIST_PUT)
			type = GG_USERLIST_PUT_MORE;

		request += 2047;
		len -= 2047;
	}

	sess->userlist_blocks++;

	return gg_send_packet(sess, GG_USERLIST_REQUEST, &type, sizeof(type), request, len, NULL);
}

/*
 * Local variables:
 * c-indentation-style: k&r
 * c-basic-offset: 8
 * indent-tabs-mode: notnil
 * End:
 *
 * vim: shiftwidth=8:
 */