/* Copyright (C) 2025 Miranda NG team (https://miranda-ng.org) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation version 2 of the License. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "stdafx.h" #define TEAMS_TROUTER_TTL 86400 #define TEAMS_TROUTER_TCCV "2024.23.01.2" void CTeamsProto::OnTrouterSession(MHttpResponse *response, AsyncHttpRequest *pRequest) { if (!response || response->resultCode != 200) { LoginError(); return; } int iStart = 0; CMStringA szId = response->body.Tokenize(":", iStart); m_szTrouterUrl = pRequest->m_szUrl; m_szTrouterUrl.Replace("socket.io/1/", "socket.io/1/websocket/" + szId + "/"); ForkThread(&CTeamsProto::GatewayThread); } void CTeamsProto::OnTrouterInfo(MHttpResponse *response, AsyncHttpRequest *) { TeamsReply reply(response); if (reply.error()) { LoginError(); return; } auto &root = reply.data(); m_szTrouterSurl = root["surl"].as_mstring(); CMStringA ccid = root["ccid"].as_mstring(); CMStringA szUrl = root["socketio"].as_mstring(); szUrl += "socket.io/1/"; CreateContactSubscription(); auto *pReq = new AsyncHttpRequest(REQUEST_GET, HOST_OTHER, szUrl, &CTeamsProto::OnTrouterSession); pReq << CHAR_PARAM("v", "v4"); m_connectParams.destroy(); for (auto &it : root["connectparams"]) { m_connectParams.AddHeader(it.name(), it.as_string().c_str()); pReq << CHAR_PARAM(it.name(), it.as_string().c_str()); } pReq << CHAR_PARAM("tc", "{\"cv\":\"" TEAMS_TROUTER_TCCV "\",\"ua\":\"TeamsCDL\",\"hr\":\"\",\"v\":\"" TEAMS_CLIENTINFO_VERSION "\"}") << CHAR_PARAM("con_num", "1234567890123_1") << CHAR_PARAM("epid", m_szEndpoint) << BOOL_PARAM("auth", true) << INT_PARAM("timeout", 40); if (!ccid.IsEmpty()) pReq << CHAR_PARAM("ccid", ccid); PushRequest(pReq); } void CTeamsProto::StartTrouter() { auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_OTHER, "https://go.trouter.skype.com/v4/a", &CTeamsProto::OnTrouterInfo); pReq->m_szUrl.AppendFormat("?epid=%s", m_szEndpoint.c_str()); pReq->AddHeader("x-skypetoken", m_szSkypeToken); pReq->flags |= NLHRF_NODUMPHEADERS; PushRequest(pReq); } ///////////////////////////////////////////////////////////////////////////////////////// void CTeamsProto::StopTrouter() { m_impl.m_heartBeat.StopSafe(); if (m_ws) { TRouterSendActive(false); m_ws->terminate(); m_ws = nullptr; } } void CTeamsProto::GatewayThread(void *) { while (!m_isTerminated) GatewayThreadWorker(); } void CTeamsProto::GatewayThreadWorker() { m_ws = nullptr; MHttpHeaders headers; headers.AddHeader("x-skypetoken", m_szSkypeToken); headers.AddHeader("User-Agent", TEAMS_USER_AGENT); WebSocket ws(this); NLHR_PTR pReply(ws.connect(m_hTrouterNetlibUser, m_szTrouterUrl, &headers)); if (pReply) { if (pReply->resultCode == 101) { m_ws = &ws; iCommandId = 1; m_impl.m_heartBeat.StartSafe(30000); debugLogA("Websocket connection succeeded"); ws.run(); } else debugLogA("websocket connection failed: %d", pReply->resultCode); } else debugLogA("websocket connection failed"); StopTrouter(); } ///////////////////////////////////////////////////////////////////////////////////////// // TRouter send void CTeamsProto::TRouterSendJson(const JSONNode &node, int iReplyTo) { CMStringA szJson; if (iReplyTo == -1) { iCommandId++; szJson.Format("5:%d+::", iCommandId); } else szJson.Format("5:%d+::", iReplyTo); szJson += node.write().c_str(); if (m_ws) m_ws->sendText(szJson.c_str()); } void CTeamsProto::TRouterSendJson(const char *szName, const JSONNode *node, int iReplyTo) { JSONNode payload, args(JSON_ARRAY); payload << CHAR_PARAM("name", szName); if (node) { if (mir_strcmp(node->name(), "args")) { args.set_name("args"); args << *node; payload << args; } else payload << *node; } CMStringA szJson; if (iReplyTo == -1) { iCommandId++; szJson.Format("5:%d+::", iCommandId); } else szJson.Format("5:%d+::", iReplyTo); szJson += payload.write().c_str(); if (m_ws) m_ws->sendText(szJson.c_str()); } static char szSuffix[4] = { 'A', 'g', 'Q', 'w' }; void CTeamsProto::TRouterSendActive(bool bActive, int iReplyTo) { CMStringA cv; srand(time(0)); for (int i = 0; i < 21; i++) cv.AppendChar('a' + rand() % 26); cv.AppendChar(szSuffix[rand() % 4]); cv += ".0.1"; JSONNode payload; payload << CHAR_PARAM("state", bActive ? "active" : "inactive") << CHAR_PARAM("cv", cv); TRouterSendJson("user.activity", &payload, iReplyTo); } void CTeamsProto::TRouterRegister() { TRouterRegister("NextGenCalling", "DesktopNgc_2.3:SkypeNgc", m_szTrouterSurl + "NGCallManagerWin", nullptr); TRouterRegister("SkypeSpacesWeb", "SkypeSpacesWeb_2.3", m_szTrouterSurl + "SkypeSpacesWeb", nullptr); TRouterRegister("TeamsCDLWebWorker", "TeamsCDLWebWorker_2.3", m_szTrouterSurl, ""); TRouterRegister("TeamsCDLWebWorker", "TeamsCDLWebWorker_2.3", m_szTrouterSurl, "TFL"); } void CTeamsProto::TRouterRegister(const char *pszAppId, const char *pszKey, const char *pszPath, const char *pszContext) { JSONNode descr, reg, obj, trouter(JSON_ARRAY), transports; descr.set_name("clientDescription"); descr << CHAR_PARAM("appId", pszAppId) << CHAR_PARAM("aesKey", "") << CHAR_PARAM("languageId", "en-US") << CHAR_PARAM("platform", "edge") << CHAR_PARAM("templateKey", pszKey) << CHAR_PARAM("platformUIVersion", TEAMS_CLIENTINFO_VERSION); if (pszContext) descr << CHAR_PARAM("productContext", pszContext); obj << CHAR_PARAM("context", "") << CHAR_PARAM("path", pszPath) << INT_PARAM("ttl", TEAMS_TROUTER_TTL); trouter.set_name("TROUTER"); trouter << obj; transports.set_name("transports"); transports << trouter; reg.set_name("registration"); reg << descr << CHAR_PARAM("registrationId", m_szEndpoint) << CHAR_PARAM("nodeId", "") << transports; auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_OTHER, "https://edge.skype.com/registrar/prod/v2/registrations"); pReq->flags |= NLHRF_NODUMPHEADERS; pReq->AddHeader("Content-Type", "application/json"); pReq->AddHeader("X-Skypetoken", m_szSkypeToken); pReq->AddHeader("Authorization", "Bearer " + m_szAccessToken); pReq->m_szParam = reg.write().c_str(); PushRequest(pReq); } ///////////////////////////////////////////////////////////////////////////////////////// // TRouter receive void WebSocket::process(const uint8_t *buf, size_t cbLen) { Netlib_Dump(getConn(), buf, cbLen, false, 0); CMStringA payload((const char *)buf, (int)cbLen); p->TRouterProcess(payload); } static const char* skip3colons(const char *str, int *packet_id = nullptr) { int nColons = 3; for (const char *p = str; *p; p++) { if (*p == ':') { if (packet_id && nColons == 3) *packet_id = atoi(p+1); if (--nColons == 0) return p + 1; } } return str; } void CTeamsProto::TRouterProcess(const char *str) { switch (*str) { case '1': TRouterRegister(); break; case '3': if (auto packet = JSONNode::parse(skip3colons(str))) { std::string szBody(packet["body"].as_string()); auto message = JSONNode::parse(szBody.c_str()); if (message) { Netlib_Logf(m_hTrouterNetlibUser, "Got event:\n%s", message.write_formatted().c_str()); ProcessEvent(message); } JSONNode reply, &old = packet["headers"], headers; headers.set_name("headers"); headers << WCHAR_PARAM("MS-CV", old["MS-CV"].as_mstring()) << old["trouter-request"] << old["trouter-client"]; reply << WCHAR_PARAM("id", packet["id"].as_mstring()) << INT_PARAM("status", 200) << headers << CHAR_PARAM("body", ""); if (m_ws) m_ws->sendText(("3:::" + reply.write()).c_str()); } break; case '5': if (auto root = JSONNode::parse(skip3colons(str, &iCommandId))) { std::string szName(root["name"].as_string()); ProcessServerMessage(szName, iCommandId, root["args"]); } break; } } void CTeamsProto::ProcessEvent(const JSONNode &node) { if (auto &presence = node["presence"]) { for (auto &it : presence) ProcessUserPresence(it); return; } auto szType = node["type"].as_string(); if (szType == "EventMessage") { auto &resource = node["resource"]; auto szResourceType = node["resourceType"]; if (szResourceType == "ConversationUpdate") ProcessConversationUpdate(resource); else if (szResourceType == "NewMessage") ProcessNewMessage(resource); } } void CTeamsProto::ProcessUserPresence(const JSONNode &node) { debugLogA(__FUNCTION__); CMStringA skypename = node["mri"].as_mstring(); auto &presence = node["presence"]; std::string status = presence["availability"].as_string(); if (!skypename.IsEmpty()) { if (IsMe(skypename)) { int iNewStatus = TeamsToMirandaStatus(status.c_str()); if (iNewStatus == ID_STATUS_OFFLINE) return; int old_status = m_iStatus; m_iDesiredStatus = iNewStatus; m_iStatus = iNewStatus; if (old_status != iNewStatus) ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, iNewStatus); } else if (MCONTACT hContact = FindContact(skypename)) { SetContactStatus(hContact, TeamsToMirandaStatus(status.c_str())); if (auto &p = presence["lastActiveTime"]) setDword(hContact, "LastSeen", Utils_IsoToUnixTime(p.as_string().c_str())); if (auto &p = presence["deviceType"]) setWString(hContact, "MirVer", L"Teams (" + p.as_mstring() + L")"); } } } void CTeamsProto::ProcessServerMessage(const std::string &szName, int packetId, const JSONNode &args) { if (szName == "trouter.message_loss") TRouterSendJson("trouter.processed_message_loss", &args, packetId); else if (szName == "trouter.connected") TRouterSendActive(true, packetId); } void CTeamsProto::ProcessConversationUpdate(const JSONNode &node) { if (auto &properties = node["threadProperties"]) { CMStringW wszId(node["id"].as_mstring()); if (auto *si = Chat_Find(wszId, m_szModuleName)) if (getMStringW(si->hContact, "Version") != properties["version"].as_mstring()) GetChatInfo(wszId); } } void CTeamsProto::ProcessThreadUpdate(const JSONNode &) {}