Mercurial > pidgin.yaz
diff libpurple/protocols/gg/lib/libgadu.c @ 31860:93b08d43f684
matekm and kkszysiu collaborated on this patch to update our internal libgadu
to version 1.10.1.
author | John Bailey <rekkanoryo@rekkanoryo.org> |
---|---|
date | Thu, 24 Mar 2011 20:53:13 +0000 |
parents | a8cc50c2279f |
children | 3a90a59ddea2 |
line wrap: on
line diff
--- a/libpurple/protocols/gg/lib/libgadu.c Thu Mar 24 15:18:14 2011 +0000 +++ b/libpurple/protocols/gg/lib/libgadu.c Thu Mar 24 20:53:13 2011 +0000 @@ -1,7 +1,7 @@ -/* $Id: libgadu.c 878 2009-11-16 23:48:19Z wojtekka $ */ +/* $Id: libgadu.c 1062 2011-03-13 18:10:24Z wojtekka $ */ /* - * (C) Copyright 2001-2009 Wojtek Kaniewski <wojtekka@irc.pl> + * (C) Copyright 2001-2010 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> @@ -31,6 +31,7 @@ #include "libgadu.h" #include "libgadu-config.h" #include "libgadu-internal.h" +#include "libgadu-debug.h" #include <sys/types.h> @@ -51,6 +52,8 @@ #include "compat.h" #include "protocol.h" #include "resolver.h" +#include "encoding.h" +#include "session.h" #ifndef _WIN32 # include <errno.h> /* on Win32 this is included above */ @@ -64,51 +67,15 @@ #include <signal.h> #include <time.h> #include <unistd.h> +#ifdef GG_CONFIG_HAVE_GNUTLS +# include <gnutls/gnutls.h> +#endif #ifdef GG_CONFIG_HAVE_OPENSSL # include <openssl/err.h> # include <openssl/rand.h> #endif -#define GG_LIBGADU_VERSION "1.9.0" - -/** - * Poziom rejestracji informacji odpluskwiających. Zmienna jest maską bitową - * składającą się ze stałych \c GG_DEBUG_... - * - * \ingroup debug - */ -int gg_debug_level = 0; - -/** - * Funkcja, do której są przekazywane informacje odpluskwiające. Jeśli zarówno - * ten \c gg_debug_handler, jak i \c gg_debug_handler_session, są równe - * \c NULL, informacje są wysyłane do standardowego wyjścia błędu (\c stderr). - * - * \param level Poziom rejestracji - * \param format Format wiadomości (zgodny z \c printf) - * \param ap Lista argumentów (zgodna z \c printf) - * - * \note Funkcja jest przesłaniana przez \c gg_debug_handler_session. - * - * \ingroup debug - */ -void (*gg_debug_handler)(int level, const char *format, va_list ap) = NULL; - -/** - * Funkcja, do której są przekazywane informacje odpluskwiające. Jeśli zarówno - * ten \c gg_debug_handler, jak i \c gg_debug_handler_session, są równe - * \c NULL, informacje są wysyłane do standardowego wyjścia błędu. - * - * \param sess Sesja której dotyczy informacja lub \c NULL - * \param level Poziom rejestracji - * \param format Format wiadomości (zgodny z \c printf) - * \param ap Lista argumentów (zgodna z \c printf) - * - * \note Funkcja przesłania przez \c gg_debug_handler_session. - * - * \ingroup debug - */ -void (*gg_debug_handler_session)(struct gg_session *sess, int level, const char *format, va_list ap) = NULL; +#define GG_LIBGADU_VERSION "1.10.1" /** * Port gniazda nasłuchującego dla połączeń bezpośrednich. @@ -180,7 +147,7 @@ #ifdef __GNUC__ __attribute__ ((unused)) #endif -= "$Id: libgadu.c 923 2010-03-09 20:03:29Z wojtekka $"; += "$Id: libgadu.c 1062 2011-03-13 18:10:24Z wojtekka $"; #endif #endif /* DOXYGEN */ @@ -197,6 +164,37 @@ return GG_LIBGADU_VERSION; } +#ifdef GG_CONFIG_HAVE_UINT64_T +/** + * \internal Zamienia kolejność bajtów w 64-bitowym słowie. + * + * Ze względu na little-endianowość protokołu Gadu-Gadu, na maszynach + * big-endianowych odwraca kolejność bajtów w słowie. + * + * \param x Liczba do zamiany + * + * \return Liczba z odpowiednią kolejnością bajtów + * + * \ingroup helper + */ +uint64_t gg_fix64(uint64_t x) +{ +#ifndef GG_CONFIG_BIGENDIAN + return x; +#else + return (uint64_t) + (((x & (uint64_t) 0x00000000000000ffULL) << 56) | + ((x & (uint64_t) 0x000000000000ff00ULL) << 40) | + ((x & (uint64_t) 0x0000000000ff0000ULL) << 24) | + ((x & (uint64_t) 0x00000000ff000000ULL) << 8) | + ((x & (uint64_t) 0x000000ff00000000ULL) >> 8) | + ((x & (uint64_t) 0x0000ff0000000000ULL) >> 24) | + ((x & (uint64_t) 0x00ff000000000000ULL) >> 40) | + ((x & (uint64_t) 0xff00000000000000ULL) >> 56)); +#endif +} +#endif /* GG_CONFIG_HAVE_UINT64_T */ + /** * \internal Zamienia kolejność bajtów w 32-bitowym słowie. * @@ -280,7 +278,9 @@ /** * \internal Odbiera od serwera dane binarne. * - * Funkcja odbiera dane od serwera zajmując się TLS w razie konieczności. + * Funkcja odbiera dane od serwera zajmując się SSL/TLS w razie konieczności. + * Obsługuje EINTR, więc użytkownik nie musi się przejmować przerwanymi + * wywołaniami systemowymi. * * \param sess Struktura sesji * \param buf Bufor na danymi @@ -290,32 +290,135 @@ */ int gg_read(struct gg_session *sess, char *buf, int length) { - int res; +#ifdef GG_CONFIG_HAVE_GNUTLS + if (sess->ssl != NULL) { + for (;;) { + int res; + + res = gnutls_record_recv(GG_SESSION_GNUTLS(sess), buf, length); + + if (res < 0) { + if (!gnutls_error_is_fatal(res) || res == GNUTLS_E_INTERRUPTED) + continue; + + if (res == GNUTLS_E_AGAIN) + errno = EAGAIN; + else + errno = EINVAL; + + return -1; + } + + return res; + } + } +#endif #ifdef GG_CONFIG_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; + if (sess->ssl != NULL) { + for (;;) { + int res, err; + + res = SSL_read(sess->ssl, buf, length); + + if (res < 0) { + err = SSL_get_error(sess->ssl, res); + + if (err == SSL_ERROR_SYSCALL && errno == EINTR) + continue; + + if (err == SSL_ERROR_WANT_READ) + errno = EAGAIN; + else if (err != SSL_ERROR_SYSCALL) + errno = EINVAL; + + return -1; + } + + return res; } - } else + } #endif - res = read(sess->fd, buf, length); - - return res; + + return read(sess->fd, buf, length); } /** * \internal Wysyła do serwera dane binarne. * + * Funkcja wysyła dane do serwera zajmując się SSL/TLS w razie konieczności. + * Obsługuje EINTR, więc użytkownik nie musi się przejmować przerwanymi + * wywołaniami systemowymi. + * + * \note Funkcja nie zajmuje się buforowaniem wysyłanych danych (patrz + * gg_write()). + * + * \param sess Struktura sesji + * \param buf Bufor z danymi + * \param length Długość bufora + * + * \return To samo co funkcja systemowa \c write + */ +static int gg_write_common(struct gg_session *sess, const char *buf, int length) +{ +#ifdef GG_CONFIG_HAVE_GNUTLS + if (sess->ssl != NULL) { + for (;;) { + int res; + + res = gnutls_record_send(GG_SESSION_GNUTLS(sess), buf, length); + + if (res < 0) { + if (!gnutls_error_is_fatal(res) || res == GNUTLS_E_INTERRUPTED) + continue; + + if (res == GNUTLS_E_AGAIN) + errno = EAGAIN; + else + errno = EINVAL; + + return -1; + } + + return res; + } + } +#endif + +#ifdef GG_CONFIG_HAVE_OPENSSL + if (sess->ssl != NULL) { + for (;;) { + int res, err; + + res = SSL_write(sess->ssl, buf, length); + + if (res < 0) { + err = SSL_get_error(sess->ssl, res); + + if (err == SSL_ERROR_SYSCALL && errno == EINTR) + continue; + + if (err == SSL_ERROR_WANT_WRITE) + errno = EAGAIN; + else if (err != SSL_ERROR_SYSCALL) + errno = EINVAL; + + return -1; + } + + return res; + } + } +#endif + + return write(sess->fd, buf, length); +} + + + +/** + * \internal Wysyła do serwera dane binarne. + * * Funkcja wysyła dane do serwera zajmując się TLS w razie konieczności. * * \param sess Struktura sesji @@ -328,66 +431,41 @@ { int res = 0; -#ifdef GG_CONFIG_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; + if (!sess->async) { + int written = 0; + + while (written < length) { + res = gg_write_common(sess, buf + written, length - written); + + if (res == -1) + return -1; + + written += res; + res = written; } - } else -#endif - { - if (!sess->async) { - int written = 0; - - while (written < length) { - res = write(sess->fd, buf + written, length - written); - - if (res == -1) { - if (errno != EINTR) - break; - - continue; - } - - written += res; - res = written; + } else { + res = 0; + + if (sess->send_buf == NULL) { + res = gg_write_common(sess, buf, length); + + if (res == -1) + return -1; + } + + if (res < length) { + char *tmp; + + if (!(tmp = realloc(sess->send_buf, sess->send_left + length - res))) { + errno = ENOMEM; + return -1; } - } else { - if (!sess->send_buf) - res = write(sess->fd, buf, length); - else - res = 0; - - if (res == -1) { - if (errno != EAGAIN) - return res; - - res = 0; - } - - if (res < length) { - char *tmp; - - if (!(tmp = realloc(sess->send_buf, sess->send_left + length - res))) { - errno = ENOMEM; - return -1; - } - - sess->send_buf = tmp; - - memcpy(sess->send_buf + sess->send_left, buf + res, length - res); - - sess->send_left += length - res; - } + + sess->send_buf = tmp; + + memcpy(sess->send_buf + sess->send_left, buf + res, length - res); + + sess->send_left += length - res; } } @@ -443,11 +521,6 @@ } if (ret == -1) { - if (errno == EINTR) { - gg_debug_session(sess, GG_DEBUG_MISC, "// gg_recv_packet() header recv() interrupted system call, resuming\n"); - continue; - } - if (errno == EAGAIN) { gg_debug_session(sess, GG_DEBUG_MISC, "// gg_recv_packet() header recv() incomplete header received\n"); @@ -513,10 +586,7 @@ offset += ret; size -= ret; } else if (ret == -1) { - int errno2 = errno; - gg_debug_session(sess, GG_DEBUG_MISC, "// gg_recv_packet() body recv() failed (errno=%d, %s)\n", errno, strerror(errno)); - errno = errno2; if (errno == EAGAIN) { gg_debug_session(sess, GG_DEBUG_MISC, "// gg_recv_packet() %d bytes received, %d left\n", offset, size); @@ -525,23 +595,16 @@ sess->recv_done = offset; return NULL; } - if (errno != EINTR) { - free(buf); - return NULL; - } + + free(buf); + return NULL; } } sess->recv_left = 0; - if ((gg_debug_level & GG_DEBUG_DUMP)) { - unsigned int i; - - gg_debug_session(sess, GG_DEBUG_DUMP, "// gg_recv_packet(%.2x)", h.type); - for (i = 0; i < sizeof(h) + h.length; i++) - gg_debug_session(sess, GG_DEBUG_DUMP, " %.2x", (unsigned char) buf[i]); - gg_debug_session(sess, GG_DEBUG_DUMP, "\n"); - } + gg_debug_session(sess, GG_DEBUG_DUMP, "// gg_recv_packet(type=0x%.2x, length=%d)\n", h.type, h.length); + gg_debug_dump(sess, GG_DEBUG_DUMP, buf, sizeof(h) + h.length); return buf; } @@ -609,14 +672,8 @@ 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_session(sess, GG_DEBUG_DUMP, "// gg_send_packet(0x%.2x)", gg_fix32(h->type)); - for (i = 0; i < tmp_length; ++i) - gg_debug_session(sess, GG_DEBUG_DUMP, " %.2x", (unsigned char) tmp[i]); - gg_debug_session(sess, GG_DEBUG_DUMP, "\n"); - } + gg_debug_session(sess, GG_DEBUG_DUMP, "// gg_send_packet(type=0x%.2x, length=%d)\n", gg_fix32(h->type), gg_fix32(h->length)); + gg_debug_dump(sess, GG_DEBUG_DUMP, tmp, tmp_length); res = gg_write(sess, tmp, tmp_length); @@ -739,14 +796,22 @@ sess->server_addr = p->server_addr; sess->external_port = p->external_port; sess->external_addr = p->external_addr; - - sess->protocol_features = (p->protocol_features & ~(GG_FEATURE_STATUS77 | GG_FEATURE_MSG77)); - - if (!(p->protocol_features & GG_FEATURE_STATUS77)) - sess->protocol_features |= GG_FEATURE_STATUS80; - - if (!(p->protocol_features & GG_FEATURE_MSG77)) - sess->protocol_features |= GG_FEATURE_MSG80; + sess->client_port = p->client_port; + + if (p->protocol_features == 0) { + sess->protocol_features = GG_FEATURE_MSG80 | GG_FEATURE_STATUS80 | GG_FEATURE_DND_FFC | GG_FEATURE_IMAGE_DESCR | GG_FEATURE_UNKNOWN_100 | GG_FEATURE_USER_DATA | GG_FEATURE_MSG_ACK | GG_FEATURE_TYPING_NOTIFICATION; + } else { + sess->protocol_features = (p->protocol_features & ~(GG_FEATURE_STATUS77 | GG_FEATURE_MSG77)); + + if (!(p->protocol_features & GG_FEATURE_STATUS77)) + sess->protocol_features |= GG_FEATURE_STATUS80; + + if (!(p->protocol_features & GG_FEATURE_MSG77)) + sess->protocol_features |= GG_FEATURE_MSG80; + } + + if (!(sess->status_flags = p->status_flags)) + sess->status_flags = GG_STATUS_FLAG_UNKNOWN | GG_STATUS_FLAG_SPAM; sess->protocol_version = (p->protocol_version) ? p->protocol_version : GG_DEFAULT_PROTOCOL_VERSION; @@ -774,8 +839,8 @@ else max_length = GG_STATUS_DESCR_MAXSIZE_PRE_8_0; - if (sess->protocol_version >= 0x2d && p->encoding != GG_ENCODING_UTF8) - sess->initial_descr = gg_cp_to_utf8(p->status_descr); + if (sess->protocol_version >= 0x2d) + sess->initial_descr = gg_encoding_convert(p->status_descr, p->encoding, GG_ENCODING_UTF8, -1, -1); else sess->initial_descr = strdup(p->status_descr); @@ -791,7 +856,25 @@ } if (p->tls == 1) { -#ifdef GG_CONFIG_HAVE_OPENSSL +#ifdef GG_CONFIG_HAVE_GNUTLS + gg_session_gnutls_t *tmp; + + tmp = malloc(sizeof(gg_session_gnutls_t)); + + if (tmp == NULL) { + gg_debug(GG_DEBUG_MISC, "// gg_login() out of memory for GnuTLS session\n"); + goto fail; + } + + sess->ssl = tmp; + + gnutls_global_init(); + gnutls_certificate_allocate_credentials(&tmp->xcred); + gnutls_init(&tmp->session, GNUTLS_CLIENT); + gnutls_priority_set_direct(tmp->session, "NORMAL:-VERS-TLS", NULL); +// gnutls_priority_set_direct(tmp->session, "NONE:+VERS-SSL3.0:+AES-128-CBC:+RSA:+SHA1:+COMP-NULL", NULL); + gnutls_credentials_set(tmp->session, GNUTLS_CRD_CERTIFICATE, tmp->xcred); +#elif defined(GG_CONFIG_HAVE_OPENSSL) char buf[1024]; OpenSSL_add_ssl_algorithms(); @@ -810,7 +893,7 @@ RAND_seed((void *) &rstruct, sizeof(rstruct)); } - sess->ssl_ctx = SSL_CTX_new(TLSv1_client_method()); + sess->ssl_ctx = SSL_CTX_new(SSLv3_client_method()); if (!sess->ssl_ctx) { ERR_error_string_n(ERR_get_error(), buf, sizeof(buf)); @@ -850,10 +933,18 @@ if (!sess->server_addr) { if ((addr.s_addr = inet_addr(hostname)) == INADDR_NONE) { - if (gg_gethostbyname_real(hostname, &addr, 0) == -1) { + struct in_addr *addr_list = NULL; + int addr_count; + + if (gg_gethostbyname_real(hostname, &addr_list, &addr_count, 0) == -1 || addr_count == 0) { gg_debug(GG_DEBUG_MISC, "// gg_login() host \"%s\" not found\n", hostname); + free(addr_list); goto fail; } + + addr = addr_list[0]; + + free(addr_list); } } else { addr.s_addr = sess->server_addr; @@ -976,7 +1067,10 @@ * * \note Jeśli w buforze nadawczym połączenia z serwerem znajdują się jeszcze * dane (np. z powodu strat pakietów na łączu), prawdopodobnie zostaną one - * utracone przy zrywaniu połączenia. + * utracone przy zrywaniu połączenia. Aby mieć pewność, że opis statusu + * zostanie zachowany, należy ustawić stan \c GG_STATUS_NOT_AVAIL_DESCR + * za pomocą funkcji \c gg_change_status_descr() i poczekać na zdarzenie + * \c GG_EVENT_DISCONNECT_ACK. * * \param sess Struktura sesji * @@ -989,11 +1083,13 @@ gg_debug_session(sess, GG_DEBUG_FUNCTION, "** gg_logoff(%p);\n", sess); - if (GG_S_NA(sess->status)) - gg_change_status(sess, GG_STATUS_NOT_AVAIL); +#ifdef GG_CONFIG_HAVE_GNUTLS + if (sess->ssl != NULL) + gnutls_bye(GG_SESSION_GNUTLS(sess), GNUTLS_SHUT_RDWR); +#endif #ifdef GG_CONFIG_HAVE_OPENSSL - if (sess->ssl) + if (sess->ssl != NULL) SSL_shutdown(sess->ssl); #endif @@ -1005,6 +1101,18 @@ sess->fd = -1; } +#ifdef GG_CONFIG_HAVE_GNUTLS + if (sess->ssl != NULL) { + gg_session_gnutls_t *tmp; + + tmp = (gg_session_gnutls_t*) sess->ssl; + gnutls_deinit(tmp->session); + gnutls_certificate_free_credentials(tmp->xcred); + gnutls_global_deinit(); + free(sess->ssl); + } +#endif + if (sess->send_buf) { free(sess->send_buf); sess->send_buf = NULL; @@ -1103,7 +1211,7 @@ if (sess->protocol_version >= 0x2d) { if (descr != NULL && sess->encoding != GG_ENCODING_UTF8) { - new_descr = gg_cp_to_utf8(descr); + new_descr = gg_encoding_convert(descr, GG_ENCODING_CP1250, GG_ENCODING_UTF8, -1, -1); if (!new_descr) return -1; @@ -1140,7 +1248,7 @@ struct gg_new_status80 p; p.status = gg_fix32(status); - p.flags = gg_fix32(0x00800001); + p.flags = gg_fix32(sess->status_flags); p.description_size = gg_fix32(descr_len); res = gg_send_packet(sess, packet_type, @@ -1168,6 +1276,10 @@ } free(new_descr); + + if (GG_S_NA(status)) + sess->state = GG_STATE_DISCONNECTING; + return res; } @@ -1228,6 +1340,33 @@ } /** + * Funkcja zmieniająca flagi statusu. + * + * \param sess Struktura sesji + * \param flags Nowe flagi statusu + * + * \return 0 jeśli się powiodło, -1 w przypadku błędu + * + * \note Aby zmiany weszły w życie, należy ponownie ustawić status za pomocą + * funkcji z rodziny \c gg_change_status(). + * + * \ingroup status + */ +int gg_change_status_flags(struct gg_session *sess, int flags) +{ + gg_debug_session(sess, GG_DEBUG_FUNCTION, "** gg_change_status_flags(%p, 0x%08x);\n", sess, flags); + + if (sess == NULL) { + errno = EFAULT; + return -1; + } + + sess->status_flags = flags; + + return 0; +} + +/** * Wysyła wiadomość do użytkownika. * * Zwraca losowy numer sekwencyjny, który można zignorować albo wykorzystać @@ -1377,7 +1516,7 @@ format_idx += 3; - if ((attr & (GG_FONT_BOLD | GG_FONT_ITALIC | GG_FONT_UNDERLINE | GG_FONT_COLOR)) != 0) { + if ((attr & (GG_FONT_BOLD | GG_FONT_ITALIC | GG_FONT_UNDERLINE | GG_FONT_COLOR)) != 0 || (attr == 0 && old_attr != 0)) { if (char_pos != 0) { if ((old_attr & GG_FONT_UNDERLINE) != 0) gg_append(dst, &len, "</u>", 4); @@ -1388,7 +1527,8 @@ if ((old_attr & GG_FONT_BOLD) != 0) gg_append(dst, &len, "</b>", 4); - gg_append(dst, &len, "</span>", 7); + if (src[i] != 0) + gg_append(dst, &len, "</span>", 7); } if (((attr & GG_FONT_COLOR) != 0) && (format_idx + 3 <= format_len)) { @@ -1398,9 +1538,11 @@ color = (const unsigned char*) "\x00\x00\x00"; } - if (dst != NULL) - sprintf(&dst[len], span_fmt, color[0], color[1], color[2]); - len += span_len; + if (src[i] != 0) { + if (dst != NULL) + sprintf(&dst[len], span_fmt, color[0], color[1], color[2]); + len += span_len; + } } else if (char_pos == 0 && src[0] != 0) { if (dst != NULL) sprintf(&dst[len], span_fmt, 0, 0, 0); @@ -1543,13 +1685,13 @@ } if (sess->encoding == GG_ENCODING_UTF8) { - if (!(cp_msg = gg_utf8_to_cp((const char *) message))) + if (!(cp_msg = gg_encoding_convert((const char *) message, GG_ENCODING_UTF8, GG_ENCODING_CP1250, -1, -1))) return -1; utf_msg = (char*) message; } else { if (sess->protocol_version >= 0x2d) { - if (!(utf_msg = gg_cp_to_utf8((const char *) message))) + if (!(utf_msg = gg_encoding_convert((const char *) message, GG_ENCODING_CP1250, GG_ENCODING_UTF8, -1, -1))) return -1; } @@ -2192,6 +2334,47 @@ return gg_send_packet(sess, GG_USERLIST_REQUEST, &type, sizeof(type), request, len, NULL); } +/** + * Informuje rozmówcę o pisaniu wiadomości. + * + * \param sess Struktura sesji + * \param recipient Numer adresata + * \param length Długość wiadomości lub 0 jeśli jest pusta + * + * \return 0 jeśli się powiodło, -1 w przypadku błędu + * + * \ingroup messages + */ +int gg_typing_notification(struct gg_session *sess, uin_t recipient, int length){ + struct gg_typing_notification pkt; + uin_t uin; + + pkt.length = gg_fix16(length); + uin = gg_fix32(recipient); + memcpy(&pkt.uin, &uin, sizeof(uin_t)); + + return gg_send_packet(sess, GG_TYPING_NOTIFICATION, &pkt, sizeof(pkt), NULL); +} + +/** + * Rozłącza inną sesję multilogowania. + * + * \param gs Struktura sesji + * \param conn_id Sesja do rozłączenia + * + * \return 0 jeśli się powiodło, -1 w przypadku błędu + * + * \ingroup login + */ +int gg_multilogon_disconnect(struct gg_session *gs, gg_multilogon_id_t conn_id) +{ + struct gg_multilogon_disconnect pkt; + + pkt.conn_id = conn_id; + + return gg_send_packet(gs, GG_MULTILOGON_DISCONNECT, &pkt, sizeof(pkt), NULL); +} + /* @} */ /*