RTM WebSocket (Centrifuge)
src/prime/AsyncPrimeWs.ts — real-time канал від партнерського сайту (TalkyTimes) по кожній ТУ: повідомлення, листи, зміни лімітів, статуси. На основі Centrifugo/centrifuge-js.
UI: тригерить створення тасків оператора (жовті/червоні/сині). Партнерська дока: RTM Documentation (від розробників TU).
Підключення
- URL:
wss://talkytimes.com/rtm, транспортcentrifuge-jsonповерхws. - Авторизація: JWT
tld-tokenз cookie ТУ (беремо зgetCookie), плюс заголовокX-Requested-With. - Один інстанс
AsyncPrimeWsна одну ТУ (на пару lady+operator). Реконект — внутрішній у centrifuge.
Три джерела подій (важливо)
При конекті сервер сам підписує з’єднання на персональний серверний канал personal:#<ladyId> — саме туди йдуть УСІ події чату/листів/лімітів. Ми також вручну підписуємось на канали broadcast і online. centrifuge маршрутизує кожну публікацію рівно в одне місце за каналом:
| Джерело | Хендлер | Що реально носить |
|---|---|---|
personal:#<ladyId> (серверна підписка) | centrifuge.on('publication') | все — чат, листи, ліміти, статуси |
broadcast (клієнтська підписка) | broadcastSubscription.on('publication') | у спостереженні — порожньо |
online (клієнтська підписка) | onlineSubscription.on('publication') | у спостереженні — порожньо (presence) |
Одна подія не дублюється між джерелами (перевірено: кожен
eventIdв лог-каналіprocessрівно раз).broadcast/onlineза час спостереження не дали жодної публікації — кандидати на видалення.
Формат конверта
Сирий push: { push: { channel, pub: { data, offset } } }. centrifuge віддає нам ctx.data = вміст pub.data:
{ "data": { /* корисне навантаження */ }, "dateExpired": null, "id": "<uuid події>", "type": "<partner type>" }Виняток — лист: у нього немає data, натомість поле email на верхньому рівні (див. приклад нижче).
Події
Партнерський type парситься в наш локальний тип (src/prime/primeSocketParser/PrimeSocketParser.ts), далі обробляється в src/prime/components/tasks/socketTaskPrime.ts. Легенда: ✅ бачили наживо · ⬜ лише в типізації, наживо не бачили · ⚠️ парситься, але дія вимкнена/відсутня.
Партнерський type | Наш локальний тип | Дія | Бачили |
|---|---|---|---|
MessageSent | message | → таск «unanswered» + на фронт ttMessage | ✅ |
chat_DialogCreated | dialogCreated | → initSocketOpenLimitTask (активна) | ✅ |
chat_DialogLimitChanged | limitsOpened | парситься, але виклик таску закоментований | ✅ ⚠️ |
chat_DialogTyping | dialogTyping | нічого | ✅ |
email (поле email) | email | на фронт ttEmail; виклик таску закоментований | ✅ ⚠️ |
chat_MessageRead | messageRead | на фронт ttMessageRead | ✅ |
Blocked | Blocked | → видалити всі активні таски пари — зараз падає, див. баги | ✅ 🐞 |
platform_CorrespondenceLimitChanged | — (падає в type:null) | не обробляється (ліміт пошти) | ✅ ⚠️ |
chat_InterlocutorStatusChanged | — (нема в enum) | не обробляється (online/offline чоловіка) | ✅ ⚠️ |
chat_DialogHighlightingChanged | — | не обробляється — а саме тут limit_reload (див. нюанси) | ✅ ⚠️ |
chat_MessageVideoPurchased | — | не обробляється (платна дія: купівля відео) | ✅ ⚠️ |
gallery_PhotoProcessed | — | не обробляється (модерація фото ТЮ) | ✅ ⚠️ |
chat_RequestLimitChanged | — (нема в enum) | не обробляється (добовий ліміт chat-request) | ✅ ⚠️ |
chat_MessageDisplayAttributesApplied | — (нема в enum) | не обробляється (модерація: blur на повідомленні) | ✅ ⚠️ |
news-feed_PostModerated | — (нема в enum) | не обробляється (модерація поста стрічки) | ✅ ⚠️ |
correspondence_LetterUpdated | — (нема в enum) | не обробляється (доїхав переклад листа) | ✅ ⚠️ |
Unblocked | — | ігнор | ⬜ |
virtual-gift_GiftLimitUpdated | — | ігнор | ⬜ |
streaming_StreamStarted | — | ігнор | ⬜ |
video-call_SessionStarted / video-call_SessionStopped | — | ігнор | ⬜ |
Підтипи MessageSent (message.type)
Enum PrimeSocketChatMessageType. Парсер пропускає повідомлення лише якщо підтип є в enum (if (chatMessageType in PrimeSocketChatMessageType)) — усе інше мовчки відкидається.
| Підтип | content | Бачили |
|---|---|---|
message | { message } | ✅ |
photo | { id, key, url, text } — text (підпис) не був описаний | ✅ |
sticker | { id, url } | ✅ |
wink | {} (порожній) | ✅ |
likephoto | { idPhoto, url } | ✅ |
like_newsfeed_post | { idFeedPost, photos[], photosTotalCount, text } | ✅ |
reply_newsfeed_post | { idFeedPost, photos[], photosTotalCount, text, replyText } | ✅ ⚠️ (в enum закоментований — відкидається) |
virtual_gift | { id, message, imageSrc, animationSrc } | ⬜ |
Кожен message також стабільно несе isReported, isIcebreaker, hasTranslation, isTranslation — раніше не були в типізації.
Офіційний перелік подій (партнерська дока)
Джерело: https://tawnylb.com/platform/api-doc/schema (RTM endpoint). Повний перелік типів, які декларує партнер:
- gallery:
gallery_PhotoProcessed,gallery_VideoProcessed - chat:
chat_DialogCreated,chat_DialogLimitChanged,chat_DialogHighlightingChanged,chat_DialogTyping,chat_MessageRead,chat_MessageVideoPurchased - platform:
platform_AudioPurchased,platform_CorrespondenceLimitChanged,platform_RevenueAccrued(нарахування для менеджерів) - social:
Blocked,Unblocked - gift:
virtual-gift_GiftLimitUpdated - інше:
MessageSent,adglare(зони блокування банерів),mail(вхідний лист)
Розбіжності з нашим кодом / спостереженням
- Бачили наживо, але немає в офіційній доці партнера (прод-реєстр 20.07.2026):
chat_InterlocutorStatusChanged,chat_MessageDisplayAttributesApplied,chat_RequestLimitChanged,news-feed_PostModerated,correspondence_LetterUpdated. Тобто офіційна дока суттєво неповна → прод-реєстр унікальних типів виправданий, і це прямий аргумент у запиті партнеру задокументувати повний перелік подій. mailvsemail— дока називає подію листаmail, а в живому трафіку прийшлоtype: "email"(з полемemail). Звірити.- Schema drift у межах одного типу —
chat_DialogHighlightingChangedприходить у двох формах (зhighlightExpireDateі без). Ключ реєстру (type|shape) це ловить; жорстко покладатись на набір полів не можна. - Неконсистентний нейминг —
emailіcorrespondence_LetterUpdatedу snake_case (id_user_from), решта подій у camelCase (idUserFrom). - Є в доці, нема в нашій типізації:
gallery_VideoProcessed,platform_AudioPurchased,platform_RevenueAccrued,adglare. - Є в нашому enum, нема в доці:
streaming_StreamStarted,video-call_SessionStarted,video-call_SessionStopped— імовірно застарілі/перейменовані на боці партнера.
Партнерський gRPC/HTTP API (не сокет) —
https://tawnylb.com/platform/api-doc/proto, 24 сервіси (Dialog, Email, Icebreaker, Gift, TrustedUser, Media, Summarizer…). Окрема поверхня, не події RTM.
Живі приклади (personal:#18567229, 07.07.2026)
MessageSent — вінк (content порожній, type: "wink"):
{
"data": { "message": {
"id": 47819361272, "type": "wink", "content": {},
"idUserFrom": 83366724, "idUserTo": 18567229,
"dateCreated": "2026-07-07T12:33:29+00:00",
"isIcebreaker": false, "isReported": false,
"hasTranslation": false, "isTranslation": false,
"displayAttributes": []
}},
"dateExpired": null, "id": "47ac87ff-8c2f-4ac3-ae8b-d638e9b3fe2e", "type": "MessageSent"
}MessageSent — текстове (type: "message", content.message):
{
"data": { "message": {
"id": 47819363913, "type": "message", "content": { "message": "фівіф" },
"idUserFrom": 83366724, "idUserTo": 18567229,
"dateCreated": "2026-07-07T12:33:41+00:00", "isIcebreaker": false
}},
"dateExpired": null, "id": "cf2a7395-31f7-40d5-8055-de94fb967178", "type": "MessageSent"
}chat_DialogCreated — система створила діалог (несе idTrustedUser=lady, idRegularUser=man):
{
"data": { "idDialog": 8371546107, "idRegularUser": 83366724, "idTrustedUser": 18567229 },
"dateExpired": null, "id": "38a48c96-62e3-4fb0-9e6e-db79cc3206ad", "type": "chat_DialogCreated"
}chat_DialogLimitChanged — зміна ліміту чату (несе поточний limitLeft, без дельти):
{
"data": { "idInterlocutor": 83366724, "idUser": 18567229, "limitLeft": 10 },
"dateExpired": null, "id": "42252d03-35d2-4b5a-828d-045506324258", "type": "chat_DialogLimitChanged"
}platform_CorrespondenceLimitChanged — зміна ліміту пошти (та сама форма):
{
"data": { "idInterlocutor": 83366724, "idUser": 18567229, "limitLeft": 2 },
"dateExpired": null, "id": "b69b4669-8343-4943-87aa-2bbfd9857264", "type": "platform_CorrespondenceLimitChanged"
}chat_DialogTyping — чоловік друкує:
{
"data": { "idInterlocutor": 83366724 },
"dateExpired": null, "id": "71be2541-69f2-4b13-81f3-47d7667ea7db", "type": "chat_DialogTyping"
}chat_InterlocutorStatusChanged — статус чоловіка (несе status, isBookmarked):
{
"data": { "idInterlocutor": 162874321, "status": "online", "isBookmarked": false, "changedAt": "2026-07-07T12:34:27.051315203Z" },
"dateExpired": null, "id": "f760dec4-71d4-4c09-bd66-693e4554b85f", "type": "chat_InterlocutorStatusChanged"
}email — вхідний лист (конверт БЕЗ data, з полем email і dateExpired):
{
"email": {
"id": 9950743543, "id_correspondence": 4103352319,
"id_user_from": 83366724, "id_user_to": 18567229,
"title": "test test ...", "content": "<p>test test test</p>",
"countPhotos": 0, "countVideos": 0,
"dateCreated": "2026-07-07T12:34:03+00:00"
},
"dateExpired": "2026-07-07T12:36:03Z", "id": "927752b1-119b-43ed-938c-1a951e9d1773", "type": "email"
}Живі приклади — прод-реєстр (усі ТЮ, 20.07.2026)
Зібрано src/prime/socketTypeRegistry.ts на живому проді (лише унікальні type|shape), тому вибірка ширша за спостереження з однієї ТЮ вище.
chat_DialogHighlightingChanged — підсвітка діалогу. Форма плаває: при highlightType: "none" поля highlightExpireDate немає.
{ "data": { "idInterlocutor": 178957333, "highlightType": "limit_reload", "highlightExpireDate": "2026-07-20T10:28:10.811330274Z" }, "type": "chat_DialogHighlightingChanged" }
{ "data": { "idInterlocutor": 178957333, "highlightType": "none" }, "type": "chat_DialogHighlightingChanged" }Blocked — payload плоский, без вкладеного message:
{ "data": { "idUserFrom": 173893126, "idUserTo": 34550873 }, "type": "Blocked" }chat_MessageRead, chat_MessageVideoPurchased (платна дія — купівля відео):
{ "data": { "idInterlocutor": 167887968, "idMessage": 48067005602 }, "type": "chat_MessageRead" }
{ "data": { "idInterlocutor": 174351164, "idVideo": 9313430 }, "type": "chat_MessageVideoPurchased" }chat_RequestLimitChanged — добовий ліміт chat-request’ів (не привʼязаний до діалогу):
{ "data": { "type": "chat_request", "limitsAvailable": 1, "dateAvailable": "2026-07-21T00:00:00+03:00" }, "type": "chat_RequestLimitChanged" }chat_MessageDisplayAttributesApplied — модерація вже надісланого повідомлення (блюр):
{ "data": { "idMessage": 48067027424, "idUserFrom": 89519208, "idUserTo": 179358206,
"displayAttributes": { "effect": "inappropriateBlur" }, "dateCreated": "2026-07-20T09:56:27.419072157Z" },
"type": "chat_MessageDisplayAttributesApplied" }gallery_PhotoProcessed / news-feed_PostModerated — модерація контенту ТЮ:
{ "data": { "idPhoto": 80058033, "idUser": 95847828, "key": "shpzkl61lkp8oa89u",
"status": { "code": "approved", "description": "Approved" },
"tags": [{ "code": "special_plus", "description": "Special+" }],
"declineReasons": [], "comment": "", "dateProcessed": "2026-07-20T10:10:06+00:00" }, "type": "gallery_PhotoProcessed" }
{ "data": { "idPost": 21312680, "result": "approved_by_ai" }, "type": "news-feed_PostModerated" }correspondence_LetterUpdated — доїхав переклад уже доставленого листа. Поля в snake_case (як в email), решта подій — camelCase. Має dateExpired (+2 хв):
{ "data": { "id": 9992185802, "id_correspondence": 3576311657,
"id_user_from": 145125994, "id_user_to": 131954164,
"title": "My darling, finally a beautiful smile...", "content": "<p>…</p>",
"has_translation": true, "is_translation": true },
"dateExpired": "2026-07-20T11:15:11Z", "type": "correspondence_LetterUpdated" }MessageSent / reply_newsfeed_post — чоловік відповів на пост стрічки (підтипу не було в enum):
{ "data": { "message": { "id": 48068106836, "type": "reply_newsfeed_post",
"content": { "idFeedPost": 829648457, "photos": [{ "id": 77630539, "key": "shpzklfolvc8khbv5", "url": "…" }],
"photosTotalCount": 1, "text": "We desperately defend our ego from criticism…",
"replyText": "Voilà une belle photo de toi…" },
"idUserFrom": 120527807, "idUserTo": 6760244, "dateCreated": "2026-07-20T11:37:18+00:00",
"isIcebreaker": false, "isReported": false, "hasTranslation": false, "isTranslation": false } },
"type": "MessageSent" }Знайдені баги (за прод-реєстром 20.07.2026)
Blockedніколи не знімає таски. Парсер береdata.data.message.idUserFrom, але payloadBlockedплоский ({ idUserFrom, idUserTo }) — вкладеногоmessageнемає.data.data.message→undefined, звернення до.idUserFromкидає TypeError, який ловиться локальнимtry/catchі лише пише в логError in remove tasks on blocked dialog. ТобтоremoveTaskOnBlockFromManне виконується жодного разу. Правильно —data.data.idUserFrom(інтерфейсIPSPM_Blockedуже описує форму коректно). Побічно:manId_apiна початку парсера рахується якdata.data?.idInterlocutor || data.data?.message?.idUserFrom→ дляBlockedобидваundefined, тож пара не визначається.reply_newsfeed_postмовчки відкидається. Підтипу не було вPrimeSocketChatMessageType, а парсер пропускає лишеif (chatMessageType in PrimeSocketChatMessageType)— такіMessageSentне дають ні таску, ні відправки на фронт. Відповідь чоловіка на пост стрічки для нас зараз не існує, хоча парнийlike_newsfeed_postобробляється. Підтип додано в enum закоментованим — свідомо, бо цей enum є рантайм-гардом і розкоментування одразу вмикає обробку:initSocketUnansweredTask(нові таски операторам) + відправка на фронт. На profit/фаворитів не впливає —checkNeedToBookmarkпропускає лишеmessage/photo/sticker/virtual_gift. Перед вмиканням оцінити добовий обсяг таких подій заsocket-types.log, щоб не завалити операторів тасками.
Нюанси
chat_DialogLimitChangedшумний. Прилітає на кожну зміну ліміту — і коли чоловік пише (0→N), і коли пишемо ми (10→9→8…). У спостереженні 4 наші відповіді дали 4 події поспіль. Payload не містить дельти чи причини — з самої події не відрізнити «система відкрила ліміти» від «витратили ліміт». Тому >90% цих подій — шум переписки.- Чому socket-openLimit вимкнено. Коли він був увімкнений, кожен
chat_DialogLimitChangedтригеривinitSocketOpenLimitTask, а той робитьgetDialogs(партнерський API) до перевірки типу — тобто зайвий запит на кожну зміну ліміту × усі активні діалоги × усі ТУ. Історія комітів (14.03.2025) — серія вмик/вимк, залишили вимкненим. - Червоний OpenLimit ключиться на system-повідомлення. І поллінг
openLimitTaskPrime, іinitSocketOpenLimitTaskстворюють червоний таск лише колиdialog.lastMessage.type === system && messagesLeft. Надійний маркер «система відкрила ліміти» — це стан діалогу (RESTgetDialogs):lastMessage.type: "system"+highlightType: "limit_reload"+highlightExpireDate, а не сама сокет-подія. - ⚡
highlightType: "limit_reload"приходить і по сокету — подієюchat_DialogHighlightingChanged(прод-реєстр 20.07.2026), з тим самимhighlightExpireDate, що й у REST. Тобто маркер відкриття лімітів не обовʼязково тягнути поллінгомgetDialogs:chat_DialogHighlightingChangedзhighlightType: "limit_reload"— це точковий, нешумний сигнал (на відміну відchat_DialogLimitChanged, де >90% — шум переписки), аhighlightType: "none"закриває вікно. Подія зараз не обробляється (немає в enum-гілці парсера). Кандидат №1 замінити поллінг лімітів; треба перевірити на живому трафіку, що вона приходить у 100% випадків відкриття (і чи не приходить на інші види підсвітки). - Дедуп тасків — по
eventTriggerTask.id(=lastMessage.id; для system id=0 нормалізується в=<idUser><idInterlocutor>), тож поллінг і сокет не дублюють один одного для однакового стану. Плюс in-memorycreatedVipTaskKeysпроти дублю з VIP-таском (не переживає рестарт). - Лист по сокету бачимо, але таск не робимо — email-гілка кличе (закоментований)
initSocketOpenLimitTask; mail-таск реально створює лише поллінгmailTaskPrime(~40 сек). Дірка.
Зв’язки
- Парсинг:
src/prime/primeSocketParser/PrimeSocketParser.ts+ типиIPrimeSocketParser.interface.ts(PrimeSocketParsedMessageType,PrimeLocalSocketMessageType). - Обробка в таски:
src/prime/components/tasks/socketTaskPrime.ts. - Поллінг-аналог для лімітів:
src/prime/components/tasks/openLimitTaskPrime.ts. - Задокументовано за живими логами:
src/prime/rtmDebugLogger.ts(важкий дамп кожної події, лише поза продом —RTM_DEBUG = NODE_ENV !== 'production') таsrc/prime/socketTypeRegistry.ts(прод-безпечний реєстр унікальнихtype|shape→logs/socket-types.log).
Заповнити
- Чи прилітає
chat_DialogLimitChangedу момент, коли система (айсбрейкер) відкриває ліміти новому чоловіку (0→N з system-повідомленням)? — не спостерігали наживо. - Чи
chat_DialogHighlightingChangedзlimit_reloadпокриває 100% випадків відкриття лімітів (див. нюанси) — від цього залежить, чи можна прибрати поллінгgetDialogs. - Форма payload для все ще не бачених типів:
Unblocked,virtual-gift_GiftLimitUpdated,streaming_StreamStarted,video-call_SessionStarted/video-call_SessionStopped, а також підтипуvirtual_giftуMessageSent. - Що саме носять
broadcast/online(чи вони взагалі використовуються). - Повний перелік значень:
highlightType,status(chat_InterlocutorStatusChanged),result(news-feed_PostModerated),status.code/tags[].code(gallery_PhotoProcessed),effect(displayAttributes) — бачили лише по 1-2 значення кожного.
Вибірка 20.07.2026 обрізана на середині рядка (останній запис — тип на
Un…, найімовірнішеUnblocked). Реєстр продовжує писати лише новіtype|shape, тож повторний знімок за кілька діб добере рідкісні події.