Czat — przebieg integracji¶
Ścieżki REST są względne wobec /partner/v1/chat.
Uwierzytelnienie REST: x-client-id, x-api-key, x-user-id.
Zasada ogólna¶
Czat działa w dwóch warstwach:
- REST — wszystkie operacje na danych: pobranie konwersacji, odczyt historii, wysyłanie, usuwanie, tłumaczenie. Wywoływane z backendu partnera, kluczem API.
- Socket.IO — odbiór wiadomości na żywo oraz sygnały „przeczytano" i „pisze". Nawiązywane z przeglądarki, uwierzytelniane krótkożyciowym tokenem.
Podział wynika z ustalenia, że klucz API nigdy nie opuszcza backendu partnera. Połączenie socketowe z natury powstaje po stronie frontendu, więc dostaje osobne, wąskie poświadczenie.
Rozmowa toczy się między użytkownikiem a weterynarzem po naszej stronie. Odpowiedź
weterynarza przychodzi asynchronicznie — przez socket, a przy jego braku zdarzeniem
webhooka chat.message.received.
Pierwsza wiadomość wiąże użytkownika z placówką¶
Wysłanie pierwszej wiadomości w konwersacji dopina użytkownika oraz wszystkie jego zwierzaki do placówki obsługującej czat. Dzieje się to automatycznie i nie ma osobnego wywołania, którym można by to włączyć lub pominąć. Dzięki temu weterynarz widzi zwierzaka, o którym jest mowa, i ma wgląd w jego książeczkę zdrowia.
Trzy rzeczy wynikające z tego powiązania:
- dopięte zostają wszystkie zwierzaki użytkownika, nie tylko ten, którego dotyczy rozmowa
- od tej chwili weterynarz tej placówki może założyć wizytę w naszej sieci, która pojawi się użytkownikowi na osi czasu i w module wizyt
- powiązanie jest trwałe — nie znika po zakończeniu rozmowy
Krok 1 — konwersacja użytkownika¶
Każdy użytkownik ma najwyżej jedną konwersację. Endpoint zwraca istniejącą, a przy pierwszym wywołaniu ją zakłada. Kolejne wywołania zwracają zawsze tę samą.
{ "conversation": { "id": "…", "name": "Anna Kowalska", "active": true,
"readByUser": false, "readByVet": true, "createdAt": "…" } }
Identyfikator z tej odpowiedzi jest potrzebny we wszystkich pozostałych wywołaniach.
Krok 2 — historia wiadomości¶
{ "messages": [ { "id": "…", "conversationId": "…", "content": "Dzień dobry…",
"files": null, "isOwn": false, "createdAt": "…",
"Author": { "name": "lek. wet. Jan Nowak", "image_src": "…" } } ],
"nextCursor": "…" }
- wiadomości są zwracane od najnowszej
takejest opcjonalne (domyślnie 30, maksymalnie 100); stronicowanie przezcursorz polanextCursornextCursorrównenulloznacza koniec historiiisOwnmówi, czy autorem jest użytkownik zx-user-idfilesprzy odczycie jestnull, gdy wiadomość nie ma załączników (a nie pustą tablicą, którą zwraca samo wysłanie)
Dwie rzeczy warte uwagi:
Treść bywa tłumaczeniem. Dla cudzych wiadomości content zawiera tłumaczenie na
język użytkownika, jeśli takie powstało, a oryginał dopiero w jego braku. Własne
wiadomości zawsze wracają w oryginale.
Brak dostępu nie jest błędem. Jeśli konwersacja nie istnieje albo użytkownik nie
jest jej uczestnikiem, odpowiedź to pusta lista i nextCursor: null, a nie kod błędu.
Krok 3 — wysłanie wiadomości¶
contentmoże być puste, jeśli wysyłane są same plikifilesto identyfikatory plików (idz potwierdzenia wgrania), a nie ścieżki — czat jako jedyny moduł referencuje pliki przez identyfikator; szczegóły w dokumentacji plików- identyfikator jest sprawdzany przed zapisem wiadomości: nieznany kończy się
404, a plik wgrany w innym celu (np. jako zdjęcie zwierzaka) —400 - załączniki czatu wgrywa się z
purpose: "chat_attachment"i są prywatne, więc do ich wyświetlenia potrzebny jest podpisany adres
Wysłanie wiadomości samo rozgłasza ją socketem — nie trzeba nic dodatkowo robić.
Dwie rzeczy warte uwagi:
- Zdarzenie
new_messagedociera także do nadawcy, zprofileIdrównym jego własnemu. Klient, który dopisuje wiadomość lokalnie poPOSTi jednocześnie nasłuchujenew_message, pokaże ją dwa razy. Albo pomijamy zdarzenia własne (poprofileId), albo nie dopisujemy lokalnie i czekamy na socket. - Odpowiedź na
POSTzawiera wysłaną treść, więc nadaje się do optymistycznego dopisania w interfejsie — z zastrzeżeniem z punktu wyżej, żeby nie policzyć tej samej wiadomości dwa razy.
Usuwanie i tłumaczenie¶
Usunąć można wyłącznie własną wiadomość. Treść zostaje wyczyszczona, a wiadomość oznaczona jako usunięta — wpis pozostaje w historii.
Tłumaczenie zwraca gołego stringa z przetłumaczoną treścią (albo null), a nie
obiekt. Językiem docelowym jest ten z nagłówka x-language.
Warstwa czasu rzeczywistego¶
Token¶
Token jest ważny 5 minut — tyle na nawiązanie połączenia, nie na czas jego trwania. Dotyczy jednego użytkownika i nie daje dostępu do niczego poza socketem. Wydaje się go z backendu partnera i przekazuje do przeglądarki.
Połączenie¶
Wymagana jest biblioteka socket.io-client — natywny WebSocket nie jest zgodny
z protokołem Socket.IO i połączenie się nie nawiąże.
Tożsamość użytkownika bierze się wyłącznie z tokena. Nie trzeba przekazywać identyfikatora profilu w parametrach połączenia — zostanie zignorowany.
Dołączenie do konwersacji jest konieczne¶
Zdarzenia new_message i message_deleted trafiają tylko do uczestników pokoju
danej konwersacji. Bez join_conversation połączenie będzie zestawione, ale nowe
wiadomości nie przyjdą. Warto wywołać je zaraz po nawiązaniu połączenia i po każdym
ponownym połączeniu.
Zdarzenia¶
Wysyłane przez klienta:
| Zdarzenie | Ładunek | Znaczenie |
|---|---|---|
join_conversation |
{ conversationId } |
zapisanie się na wiadomości z konwersacji |
leave_conversation |
{ conversationId } |
wypisanie się |
message_read |
{ conversationId } |
oznaczenie konwersacji jako przeczytanej |
typing_start / typing_end |
{ conversationId } |
wskaźnik pisania |
Odbierane przez klienta:
| Zdarzenie | Znaczenie |
|---|---|
new_message |
nowa wiadomość w konwersacji, z treścią i autorem |
message_deleted |
{ messageId, profileId } |
typing_start / typing_end |
druga strona pisze |
conversations_updated |
{ conversationId, profileId } — sygnał, że w konwersacji coś się zmieniło, bez treści |
message_read nie ma odpowiednika w REST. Oznaczenie konwersacji jako
przeczytanej jest możliwe wyłącznie przez socket — bez niego weterynarz będzie widział
rozmowę jako nieodczytaną.
Najczęstsze przyczyny błędów¶
| Objaw | Przyczyna |
|---|---|
| Pusta lista wiadomości | Zły conversationId albo użytkownik nie jest uczestnikiem — nie zwracamy w tym przypadku błędu |
| Połączenie socketowe odrzucane | Token wygasł (ważny 5 minut) albo użyto natywnego WebSocket zamiast socket.io-client |
| Połączenie działa, ale wiadomości nie przychodzą | Nie wywołano join_conversation |
| Konwersacja stale nieprzeczytana u weterynarza | Nie wysyłane jest zdarzenie message_read |
| Nie da się usunąć wiadomości | Usuwać można wyłącznie własne wiadomości |
| Załącznik nie wyświetla się | W files wysłano path zamiast id, albo nie pobrano podpisanego adresu |
404 przy wysyłaniu wiadomości |
Nieznany identyfikator w files |
400 „was not uploaded as a chat attachment" |
Plik wgrano z innym purpose niż chat_attachment |
| Wiadomość pojawia się dwa razy | Nadawca też dostaje new_message — patrz wyżej |
Przykładowa pełna sekwencja¶
backend:
GET /partner/v1/chat/user-conversation → { conversation: { id } }
GET /partner/v1/chat/{conversationId}/messages?take=30 → historia
POST /partner/v1/chat/socket-token → { token, expiresIn: 300 }
przeglądarka:
io(SOCKET_URL, { auth: { token } })
socket.emit('join_conversation', { conversationId: id })
socket.on('new_message', …)
socket.emit('message_read', { conversationId: id })
wysłanie wiadomości (ponownie przez backend):
POST /partner/v1/chat/{conversationId}/messages { "content": "…", "files": [] }
Adres serwera Socket.IO podamy razem z poświadczeniami dla środowiska testowego.