// szybki kurs libgadu // (c) copyright 2001-2003 by wojtek kaniewski // robert j. wozny // tekst poprawiany ostatnio 2003-03-18 każda sesja jest opisywana przez ,,struct gg_session''. biblioteka może w ramach jednego procesu/wątku obsługiwać tyle sesji, na ile pozwolą zasoby. na początku deklarujemy: struct gg_session *blah; następnie będziemy się łączyć. przykład będzie dotyczył socketów nieblokujących, bo w większości aplikacji ciężko sobie pozwolić na zawieszanie programu na czas łączenia. struct gg_login_params p; memset(&p, 0, sizeof(p)); p.uin = 123456; p.password = "hasło"; p.async = 1; p.status = GG_STATUS_INVISIBLE; if (!(blah = gg_login(&p))) my_error(); jeśli uda się rozpocząć proces łączenia, dostajemy wskaźnik do struktury, inaczej NULL. wywołanie gg_login() powoduje uruchomienie drugiego procesu lub wątku w tle, który wywoła gethostbyname() i potokiem zwróci wynik. później połączy się z serwerem, wyśle, odbierze, połączy się ze wskazanym adresem IP, zaloguje się itd. jako że wszystko dzieję się w tle, klient musi sprawdzać cały czas podane deskryptory. pole ,,blah->fd'' zawiera deskryptor, a ,,blah->check'' jest bitmapą i zawiera GG_CHECK_READ i/lub GG_CHECK_WRITE jeśli mamy sprawdzić czy przyszły nowe dane i/lub możemy wysyłać. jeśli coś się wydarzy, wywołujemy ,,gg_watch_fd()'', a libgadu sobie już sprawdzi, co takiego się zdarzyło: while (1) { fd_set rd, wr, ex; FD_ZERO(&rd); FD_ZERO(&wr); FD_ZERO(&ex); if ((blah->check & GG_CHECK_READ)) FD_SET(blah->fd, &rd); if ((blah->check & GG_CHECK_WRITE)) FD_SET(blah->fd, &wr); FD_SET(blah->fd, &ex); if (select(blah->fd + 1, &rd, &wr, &ex, NULL) == -1) my_error(); if (FD_ISSET(blah->fd, &ex)) my_error(); if (FD_ISSET(blah->fd, &rd) || FD_ISSET(blah->fd, &wr)) my_handle_event(); } dla uproszczenia, nie ma tutaj obsługi timeoutów i tym podobnych dodatków. poza tym, jeśli program sprawdza też inne deskryptory (np. stdin dla klientów konsolowych), dobrze byłoby sprawdzić, czy dana sesja coś robi i nie sprawdzać ,,blah->fd'' jeśli ,,blah->state == GG_STATE_IDLE''. od czasu do czasu można dać serwerowi znać, że coś się dzieje, za pomocą... gg_ping(blah); ale to już wymaga implementacji timerów i liczenia czasu od ostatniego pinga. ,,blah->last_event'' mówi, kiedy dostaliśmy cokolwiek ostatnio od serwera. wszystkie pola struktury są opisane w pliku libgadu.h. wracając do obsługi deskryptorów -- jeśli klient zauważy, że coś się zmieniło na podanym sockecie, powinien wywołać ,,gg_watch_fd()'', która wszystkim się zajmie. zwraca ona wskaźnik do zaalokowanej struktury opisującej zdarzenie. po obejrzeniu należy zwolnić ją za pomocą ,,gg_event_free()''. w powyższym przykładzie jest wywoływana funkcja ,,my_handle_event()'', która może wyglądać tak: struct gg_event *e; if (!(e = gg_watch_fd(blah))) my_error(); switch (e->type) { case GG_EVENT_NONE: case GG_EVENT_PONG: /* olewamy */ break; case GG_EVENT_CONN_SUCCESS: printf("połączono!\n"); /* tutaj wysyłamy userlistę za pomocą gg_notify() */ break; case GG_EVENT_CONN_FAILED: printf("nie udało się\n"); /* powód w e->event.failure, stałe GG_FAILURE_... */ break; case GG_EVENT_MSG: printf("masz wiadomość!\n"); printf("od: %d\n", e->event.msg.sender); printf("treść: %s\n", e->event.msg.message); /* e->event.msg.class mówi czy rozmowa czy wiad. */ /* jeśli e->event.msg.sender równy 0, to mamy */ /* wiadomość systemową o numerze w msg.class */ break; case GG_EVENT_NOTIFY: printf("oto ludzie, którzy się pojawili: "); /* tutaj sprawdzanie tablicy e->event.notify */ break; case GG_EVENT_STATUS: printf("ktoś %d zmienił stan\n", e->event.status.uin); /* nowy stan w e->event.status.status */ break; case GG_EVENT_ACK: printf("wiadomość dotarła do %d.\n", e->event.ack.recipient); /* e->event.ack.status mówi czy dotarła do klienta */ /* czy leży na serwerze, stałe GG_ACK_... */ /* e->event.ack.seq to numerek wiadomości */ break; case GG_EVENT_PUBDIR50_REPLY: printf("znalazło kogoś\n"); /* opisane niżej */ break; } gg_event_free(e); przy okazji wiadomo, co oznaczają zdarzenia. część z nich można ignorować, jeśli robi się okrojonego klienta, np. wysyłającego jedną wiadomość z linii komend. po zalogowaniu należy wysłać serwerowi listę użytkowników, których mamy w liście kontaktów. ,,gg_notify()'' przyjmuje za argument tablicę zmiennych typu ,,uin_t''. w odpowiedzi dostaniemy GG_EVENT_NOTIFY i tablicę struktur ,,struct gg_notify_reply'', jeśli ktoś jest. po szczegóły odsyłam do libgadu.c, libgadu.h i źródeł konsolowego klienta. jeśli dodajemy lub usuwamy kogoś w trakcie działania, należy skorzystać z ,,gg_add_notify()'' lub ,,gg_remove_notify()''. jeśli chcemy korzystać z listy osób blokowanych lub takich, przed którymi się ukrywamy, należy korzystać z funkcji ,,gg_notify_ex()'', ,,gg_add_notify_ex()'' i ,,gg_remove_notify_ex()'', które biorą dodatkowy argument mówiący, jak traktować użytkownika. odpowiadają za to stałe GG_USER_NORMAL, GG_USER_BLOCKED i GG_USER_OFFLINE. żeby zmienić stan na zajęty lub dostępny, używamy ,,gg_change_status()'', ,,gg_change_status_descr()'' lub ,,gg_change_status_descr_time()''. wysyłanie wiadomości za pomocą ,,gg_send_message()''. parametr ,,class'' mówi, czy ma się pojawić w osobnym okienku (GG_CLASS_MSG) czy w okienku rozmowy (GG_CLASS_CHAT). funkcja zwraca numer sekwencyjny wiadomości, którego możemy użyć do potwierdzenia. wiadomość, która ma być sformatowana w odpowiedni sposób (pogrubienie, kursywa, kolory, itp.) wysyłamy za pomocą ,,gg_send_message_richtext()''. wiadomości konferencyjne wysyłamy funkcjami ,,gg_send_message_confer()'' lub ,,gg_send_message_confer_richtext()''. jeśli chcemy się wylogować, wywołujemy ,,gg_logoff()'' i potem zwalniamy pamięć związaną z sesją funkcją ,,gg_free_session()''. jeśli chcemy przypomnieć swoje hasło, wywołujemy funkcję ,,gg_remind_password2()'', a wynikową struktuję ,,gg_http'' traktujemy podobnie do ,,gg_session'': - sprawdzamy ->fd i ->check, - wywołujemy ,,gg_remind_passwd_watch_fd()'', gdy coś się dzieje. funkcja ta zwraca -1 w przypadku błędu. jeśli zwraca 0, wywołujemy ją, póki ->state nie będzie równe GG_STATE_DONE lub GG_STATE_ERROR. - po zakończeniu, wywołujemy ,,gg_remind_passwd_free()''. jeśli chcemy zmienić hasło, wywołujemy funkcję ,,gg_change_passwd3()'' i traktujemy podobnie wynikowe ,,gg_http''. do zarządzania listą kontaktów na serwerze służą funkcje ,,gg_userlist_get()'', ,,gg_userlsit_put()'' i ,,gg_userlist_remove()''. *** OBSŁUGA KATALOGU PUBLICZNEGO GG 5.0 (na podstawie listu na ekg-devel) skoro już działa, opiszę aktualne API. głównym założeniem była maksymalna niezależność od zmian w protokole, zmian nazw pól, dodawania nowych itd. zastosowane podejście może być trochę dziwne na pierwszy rzut oka, ale podpatrzyłem to w poważniejszych projektach (np. libdbi). jeśli chcemy szukać: gg_pubdir50_t req = gg_pubdir50_new(GG_PUBDIR50_SEARCH_REQUEST); if (!req) out_of_memory(); /* szukamy po numerku... */ gg_pubdir50_add(req, GG_PUBDIR50_UIN, "123456"); /* lub... */ gg_pubdir50_add(req, GG_PUBDIR50_FIRSTNAME, "Ania"); gg_pubdir50_add(req, GG_PUBDIR50_GENDER, GG_PUBDIR50_GENDER_FEMALE); /* lub... */ gg_pubdir50_add(req, GG_PUBDIR50_BIRTHYEAR, "1979 1985"); gg_pubdir50_add(req, GG_PUBDIR50_START, "0"); gg_pubdir50_add(req, GG_PUBDIR50_ACTIVE, GG_PUBDIR50_ACTIVE_TRUE); /* i w końcu... */ gg_pubdir50(sesja, req); /* i zwalniamy pamięć, albo sobie gdzieś zachowujemy. whatever */ gg_pubdir50_free(req); jak witać, gg_pubdir50_new() tworzy obiekt opisujący operację katalogu, gg_pubdir50_add() dodaje kolejne parametry. rodzaj parametru jest w rzeczywiści stałą tekstową, np. GG_PUBDIR50_UIN to "FmNumber". należy pamiętać, że wszystkie argumenty są tekstami. nie trzeba się bawić w ich alokowanie czy coś takiego. biblioteka sobie sama zapamięta. teksty muszą być oczywiście w CP1250. na końcu wywołujemy gg_pubdir50() i tyle. funkcja ta zwraca numer sekwencyjny wyszukiwania, który możemy sobie zachować dla późniejszych referencji. żeby otrzymać wynik, należy obsłużyć zdarzenia GG_EVENT_PUBDIR50_SEARCH_REPLY, GG_EVENT_PUBDIR50_WRITE i GG_EVENT_PUBDIR50_READ. dla przykładu, obsługa wyników wyszukiwania wygląda następująco: gg_search50_t res = zdarzenie->event.search50; int count = gg_search50_count(res); if (count < 1) { wiadomość("Nie znaleziono"); return; } for (int i = 0; i < count; i++) { const char *uin, *first, *nick, *born, *city, *status; uin = gg_pubdir50_get(res, i, GG_PUBDIR50_UIN); first = gg_pubdir50_get(res, i, GG_PUBDIR50_FIRSTNAME); nick = gg_pubdir50_get(res, i, GG_PUBDIR50_NICK); born = gg_pubdir50_get(res, i, GG_PUBDIR50_BIRTHYEAR); city = gg_pubdir50_get(res, i, GG_PUBDIR50_CITY); status = gg_pubdir50_get(res, i, GG_PUBDIR50_STATUS); printf("Numer: %s\nImię: %s\nPseudonim: %s\n" "Urodzony: %s\nMiejscowość: %s\n", uin, first, nick, born, city); switch ((status) ? atoi(status) : -1) { case GG_STATUS_AVAIL: printf("Dostępny\n"); break; case GG_STATUS_BUSY: printf("Zajęty\n"); break; default: printf("Niedostępny\n"); } printf("\n"); } gg_event_free(zdarzenie); jeśli chcemy wiedzieć, od jakiego numeru zacząć wyszukiwanie, żeby dostać dalszą część, używamy gg_pubdir50_next(). jeśli chcemy numer sekwencyjny, używamy gg_pubdir50_seq(). w żadnym wypadku nie można się odwoływać do pól gg_pubdir50_t, ponieważ mogą się zmieniać między wersjami biblioteki. dzięki odwoływaniu się przez funkcje, mamy pewność, że bez względu na zmiany API/ABI mamy to samo. dodatkowo, jeśli dojdą jakieś opcje wyszukiwania, nie trzeba w bibliotece niczego zmieniać -- żadnych struktur, itd. po prostu odwołujemy się do kolejnego pola przez gg_pubdir50_add() i gg_pubdir50_get(). *** BEZPOŚREDNIE POŁĄCZENIA gadu-gadu, w przeciwieństwie do irc, umożliwia połączenia w obie strony, bez względu na to, który klient nadaje, a który odbiera. do tego, jeśli obie strony wychodzą z tego samego adresu IP, serwer informuje ich o ich adresach wewnętrznych z tego samego LANu. mamy kilka możliwych sytuacji: a) mam publiczny lub niepubliczny adres IP i chcę wysłać plik do kogoś z publicznym adresem -- łączę się z jego klientem, przedstawiam się, mówię czego chcę i jeśli to zaakceptuje, zaczynam wysyłać plik. bardzo to przypomina zwykłe połączenia dcc klientów irc. b) mam publiczny adres IP i wysyłam plik do kogoś za maskaradą -- wysyłam do niego odpowiedni pakiet ctcp (client-to-client protocol). jest to pakiet klasy GG_CLASS_CTCP (0x10) o treści składającej się z jednego znaku o kodzie 0x02. druga strona, odebrawszy taki pakiet łączy się z nami, mówi, że proszono ją o połączenie i czeka na dalsze instrukcje. wtedy wysyłamy informację, że owszem, chcemy wysłać plik, mówimy jaki i jeśli druga strona to zaakceptuje, nadajemy. c) mam niepubliczny adres IP, tak samo jak i druga strona -- tutaj nawiązanie połączenia jest możliwe tylko i wyłącznie, gdy oba klienty znajdują się w tej samej sieci (tj. oba łączą się z serwerem GG z tego samego adresu zewnętrznego) i wygląda to wtedy identycznie jak w punkcie a). to, czy możemy się z kimś połączyć widać po porcie, jaki dostajemy w pakietach gg_notify_reply. jeśli jest mniejszy niż 10, połączenie nie jest możliwe, a wtedy wysyłamy pakiet ctcp za pomocą funkcji gg_dcc_request(). każde połączenie związanie z dcc opisywane jest przez strukturę gg_dcc. najważniejsze jest GG_SESSION_DCC_SOCKET, które odpowiada za przychodzące połączenia. tworzymy je przez: struct gg_dcc *socket = gg_dcc_socket_create(uin, port); if (!socket) błąd("nie mogę otworzyć socketu"); dodaj_do_listy_przeglądanych_deskryptorów(socket); port może wynosić 0, a wtedy libgadu samo weźmie pierwszy lepszy z brzegu. w razie powodzenia zwraca zaalokowaną strukturę gg_dcc, której najbardziej interesującym polem jest gg_dcc->port zawierające numer przyznanego portu. jeśli funkcja zwróci NULL, patrzymy na errno. EINVAL to niewłaściwie parametry, ENOMEM brak pamięci, a reszta możliwych błędów to te związane z socketami, typu EADDRINUSE gdy nie może wolnego portu znaleźć. teraz wypadałoby ustawić zmienną ,,gg_dcc_port'' i połączyć się z serwerem GG, żeby ogłosić swoje namiary. ogłaszany adres IP będzie brany z połączenia z serwerem. gg_dcc_port = socket->port; połącz_się_z_serwerem(); w każdym razie, gdy pojawi się coś na deskryptorze, wywołujemy: struct gg_event *event = gg_dcc_watch_fd(socket); if (!event) { usuń_z_listy_przeglądanych_deskryptorów(socket); gg_dcc_free(socket); błąd("poważny błąd"): } błąd jest zwracany tylko w naprawdę krytycznych sytuacjach, gdy brakuje pamięci, lub nie powiodła się operacja na socketach, która nie miała się nie powieść (i przy okazji dalsza zabawa jest kompletnie bezcelowa). jeśli błędu nie będzie, dostajemy informacje o zdarzeniu. w przypadku GG_SESSION_DCC_SOCKET mogą to być: 1) GG_EVENT_NONE -- nic ciekawego się nie wydarzyło. 2) GG_EVENT_DCC_ERROR -- wystąpił błąd, którego kod znajduje się w event->event.dcc_error. w przypadku tego typu sesji możliwy jest tylko GG_ERROR_DCC_HANDSHAKE, który mówi, że nie udało się nawiązać połączenia z klientem. 3) GG_EVENT_DCC_NEW -- nowe połączenie od klienta. w polu event->event.dcc_new jest struktura gg_dcc typu GG_SESSION_DCC, którą dodajemy do listy przeglądanych deskryptorów. w każdym z tych wypadków należy po sprawdzeniu zdarzenia wywołać funkcję: gg_event_free(socket->event); by zwolnić pamięć po zdarzeniu. gdy nadejdzie połączenie i dopiszemy je do listy przeglądanych deskryptorów, musimy zwracać uwagę na następujące zdarzenia: 1) GG_EVENT_NONE -- nic się nie zdarzyło. 2) GG_EVENT_DCC_CLIENT_ACCEPT -- klient się przedstawił i czeka na autoryzację połączenia. sprawdzamy gg_dcc->uin czy jest naszym numerem i czy gg_dcc->peer_uin jest na naszej liście kontaktów i czy chcemy z nim nawiązywać połączenie. jeśli nie, to po prostu usuwamy połączenie: if (!akceptujemy_połączenie(klient->uin, klient->peer_uin)) { usuń_z_listy_przeglądanych_deskryptorów(client); gg_dcc_free(klient); } 3) GG_EVENT_DCC_CALLBACK -- poprosiliśmy klienta, żeby się z nami połączył za pomocą gg_dcc_request() i on teraz pyta się, czego chcemy. zaraz po tym zdarzeniu należy wywołać funkcję: gg_dcc_set_type(klient, rodzaj_połączenia); gdzie rodzaj to GG_SESSION_DCC_SEND albo GG_SESSION_DCC_VOICE. jeśli wysyłamy plik, można od razu wywołać gg_dcc_fill_file_info(), ale nie jest to wymagane. kiedy przyjdzie pora, libgadu sama nas o to poprosi. 4) GG_EVENT_DCC_NEED_FILE_ACK -- klient chce wysłać nam plik. w strukturze gg_dcc->file_info znajdują się wszystkie informacje na temat pliku, jak jego nazwa, rozmiar, atrybuty, data i czas utworzenia itp. jeśli nie chcemy pliku, zamykamy połączenie w podobny sposób jak przy braku autoryzacji. libgadu jeszcze nie potrafi odpowiadać negatywnie na prośby połączeń dcc. jeśli chcemy plik, otwieramy plik do zapisu i numer jego deskryptora zapisujemy do gg_dcc->file_fd. dalej libgadu zajmie się transferem. 5) GG_EVENT_DCC_NEED_FILE_INFO -- wcześniej poprosiliśmy drugą stronę by się z nami połączyła, bo jest za maskaradą, a my chcemy wysłać plik. w tym wypadku możemy albo sami wypełnić strukturę gg_dcc->file_info, którą biblioteka wyśle drugiej stronie, albo skorzystać z funkcji gg_dcc_fill_file_info(). if (gg_dcc_fill_file_info(klient, nazwa_pliku)) { błąd("nie mogę otworzyć pliku"); usuń_z_listy_przeglądanych_deskryptorów(klient); gg_dcc_free(klient); } 6) GG_EVENT_DCC_DONE -- zakończono transfer, można już nie patrzeć na deskryptor i zwolnić pamięć po połączeniu. 7) GG_EVENT_DCC_ERROR -- błąd. możliwy kod błędu to GG_ERROR_DCC_HANDSHAKE gdy nie powiodło się ustanowienie połączenia z klientem, GG_ERROR_DCC_NET kiedy nie udało się wysłać lub odczytać czegoś z socketa, GG_ERROR_DCC_FILE gdy nie można było odczytać albo zapisać do pliku, GG_ERROR_DCC_EOF gdy plik lub połączenie zbyt wcześnie się skończy, GG_ERROR_DCC_REFUSED gdy użytkownik po drugiej stronie odmówił połączenia. tutaj również należy pamiętać o wywoływaniu gg_event_free(). jeśli chcemy sami wysłać plik, sprawdzamy najpierw, czy druga strona może przyjąć połączenie, patrząc na jej port. jeśli powyżej 10, możemy śmiało wywołać funkcję: struct gg_dcc *klient = gg_dcc_send_file(adres_ip, port, nasz_uin, jego_uin); if (!klient) błąd("nie można ustanowić połączenia"); zaraz potem możemy wywołać funkcję gg_dcc_fill_file_info() by uzupełnić informację o pliku... gg_dcc_fill_file_info(klient, nazwa_pliku); ...ale jeśli tego nie zrobimy teraz, biblioteka poprosi nas o to w odpowiedniej za pomocą zdarzenia GG_EVENT_DCC_NEED_FILE_INFO. jeśli port jest podejrzanie niski, znaczy że połączenie jest niemożliwe i wtedy wywołujemy funkcję: gg_dcc_request(sesja_gg, jego_uin); gdzie sesja_gg to nasza sesja GG (jakoś musimy wysłać wiadomość), a jego_uin to numer drugiej strony. spowoduje ona, że druga strona spróbuje się z nami połączyć, jeśli ma taką możliwość. gdy otrzymamy wiadomość klasy GG_CLASS_CTCP o treści 0x02 znaczy, że ktoś chce nam coś przesłać i mamy się z nim połączyć. wywołujemy wtedy: struct gg_dcc *klient = gg_dcc_get_file(adres_ip, port, nasz_uin, jego_uin); if (!klient) błąd("nie można ustanowić połączenia"); dalej tak samo, jak przy zwykłym odbieraniu pliku. $Id: api.txt,v 1.19 2003/04/15 13:12:55 szalik Exp $