diff options
Diffstat (limited to 'protocols/Teams/src/teams_trouter.cpp')
-rw-r--r-- | protocols/Teams/src/teams_trouter.cpp | 342 |
1 files changed, 342 insertions, 0 deletions
diff --git a/protocols/Teams/src/teams_trouter.cpp b/protocols/Teams/src/teams_trouter.cpp new file mode 100644 index 0000000000..0e54b8edb6 --- /dev/null +++ b/protocols/Teams/src/teams_trouter.cpp @@ -0,0 +1,342 @@ +/* +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 <http://www.gnu.org/licenses/>. +*/ + +#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<CTeamsProto> 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<CTeamsProto>::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 &) {} |