diff options
author | George Hazan <ghazan@miranda.im> | 2023-01-11 10:28:49 +0300 |
---|---|---|
committer | George Hazan <ghazan@miranda.im> | 2023-01-11 10:28:49 +0300 |
commit | 9ab83c6bb239d88e54ae1ce7a8af49432543443a (patch) | |
tree | fe7dd11cdbba3f6ca71eb102dce126666a043744 /protocols/Discord/src/gateway.cpp | |
parent | 605743d6e763b3aa2868133f50f99f15293a9a29 (diff) |
Discord: not included into the build, but adapted for the current core version
Diffstat (limited to 'protocols/Discord/src/gateway.cpp')
-rw-r--r-- | protocols/Discord/src/gateway.cpp | 346 |
1 files changed, 346 insertions, 0 deletions
diff --git a/protocols/Discord/src/gateway.cpp b/protocols/Discord/src/gateway.cpp new file mode 100644 index 0000000000..82c3b70eb5 --- /dev/null +++ b/protocols/Discord/src/gateway.cpp @@ -0,0 +1,346 @@ +/* +Copyright © 2016-22 Miranda NG team + +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, either version 2 of the License, or +(at your option) any later version. + +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" + +////////////////////////////////////////////////////////////////////////////////////// +// sends a piece of JSON to a server via a websocket, masked + +bool CDiscordProto::GatewaySend(const JSONNode &pRoot) +{ + if (m_hGatewayConnection == nullptr) + return false; + + json_string szText = pRoot.write(); + debugLogA("Gateway send: %s", szText.c_str()); + WebSocket_SendText(m_hGatewayConnection, szText.c_str()); + return true; +} + +////////////////////////////////////////////////////////////////////////////////////// +// gateway worker thread + +void CDiscordProto::GatewayThread(void*) +{ + while (GatewayThreadWorker()) + ; + ShutdownSession(); +} + +bool CDiscordProto::GatewayThreadWorker() +{ + NETLIBHTTPHEADER hdrs[] = + { + { "Origin", "https://discord.com" }, + { 0, 0 }, + { 0, 0 }, + }; + + if (!m_szWSCookie.IsEmpty()) { + hdrs[1].szName = "Cookie"; + hdrs[1].szValue = m_szWSCookie.GetBuffer(); + } + + NLHR_PTR pReply(WebSocket_Connect(m_hGatewayNetlibUser, m_szGateway + "/?encoding=json&v=8", hdrs)); + if (pReply == nullptr) { + debugLogA("Gateway connection failed, exiting"); + return false; + } + + if (auto *pszNewCookie = Netlib_GetHeader(pReply, "Set-Cookie")) { + char *p = strchr(pszNewCookie, ';'); + if (p) *p = 0; + + m_szWSCookie = pszNewCookie; + } + + if (pReply->resultCode != 101) { + // if there's no cookie & Miranda is bounced with error 404, simply apply the cookie and try again + if (pReply->resultCode == 404) { + if (hdrs[1].szName == nullptr) + return true; + + m_szWSCookie.Empty(); // don't use the same cookie twice + } + return false; + } + + // succeeded! + debugLogA("Gateway connection succeeded"); + m_hGatewayConnection = pReply->nlc; + + bool bExit = false; + int offset = 0; + MBinBuffer netbuf; + + while (!bExit) { + if (m_bTerminated) + break; + + unsigned char buf[2048]; + int bufSize = Netlib_Recv(m_hGatewayConnection, (char*)buf + offset, _countof(buf) - offset, MSG_NODUMP); + if (bufSize == 0) { + debugLogA("Gateway connection gracefully closed"); + bExit = !m_bTerminated; + break; + } + if (bufSize < 0) { + debugLogA("Gateway connection error, exiting"); + break; + } + + WSHeader hdr; + if (!WebSocket_InitHeader(hdr, buf, bufSize)) { + offset += bufSize; + continue; + } + offset = 0; + + debugLogA("Got packet: buffer = %d, opcode = %d, headerSize = %d, final = %d, masked = %d", bufSize, hdr.opCode, hdr.headerSize, hdr.bIsFinal, hdr.bIsMasked); + + // we have some additional data, not only opcode + if ((size_t)bufSize > hdr.headerSize) { + size_t currPacketSize = bufSize - hdr.headerSize; + netbuf.append(buf, bufSize); + while (currPacketSize < hdr.payloadSize) { + int result = Netlib_Recv(m_hGatewayConnection, (char*)buf, _countof(buf), MSG_NODUMP); + if (result == 0) { + debugLogA("Gateway connection gracefully closed"); + bExit = !m_bTerminated; + break; + } + if (result < 0) { + debugLogA("Gateway connection error, exiting"); + break; + } + currPacketSize += result; + netbuf.append(buf, result); + } + } + + // read all payloads from the current buffer, one by one + size_t prevSize = 0; + while (true) { + switch (hdr.opCode) { + case 0: // text packet + case 1: // binary packet + case 2: // continuation + if (hdr.bIsFinal) { + // process a packet here + CMStringA szJson((char*)netbuf.data() + hdr.headerSize, (int)hdr.payloadSize); + debugLogA("JSON received:\n%s", szJson.c_str()); + JSONNode root = JSONNode::parse(szJson); + if (root) + bExit = GatewayProcess(root); + } + break; + + case 8: // close + debugLogA("server required to exit"); + bExit = true; // simply reconnect, don't exit + break; + + case 9: // ping + debugLogA("ping received"); + Netlib_Send(m_hGatewayConnection, (char*)buf + hdr.headerSize, bufSize - int(hdr.headerSize), 0); + break; + } + + if (hdr.bIsFinal) + netbuf.remove(hdr.headerSize + hdr.payloadSize); + + if (netbuf.length() == 0) + break; + + // if we have not enough data for header, continue reading + if (!WebSocket_InitHeader(hdr, netbuf.data(), netbuf.length())) + break; + + // if we have not enough data for data, continue reading + if (hdr.headerSize + hdr.payloadSize > netbuf.length()) + break; + + debugLogA("Got inner packet: buffer = %d, opcode = %d, headerSize = %d, payloadSize = %d, final = %d, masked = %d", netbuf.length(), hdr.opCode, hdr.headerSize, hdr.payloadSize, hdr.bIsFinal, hdr.bIsMasked); + if (prevSize == netbuf.length()) { + netbuf.remove(prevSize); + debugLogA("dropping current packet, exiting"); + break; + } + + prevSize = netbuf.length(); + } + } + + Netlib_CloseHandle(m_hGatewayConnection); + m_hGatewayConnection = nullptr; + return bExit; +} + +////////////////////////////////////////////////////////////////////////////////////// +// handles server commands + +bool CDiscordProto::GatewayProcess(const JSONNode &pRoot) +{ + int opCode = pRoot["op"].as_int(); + switch (opCode) { + case OPCODE_DISPATCH: // process incoming command + { + int iSeq = pRoot["s"].as_int(); + if (iSeq != 0) + m_iGatewaySeq = iSeq; + + CMStringW wszCommand = pRoot["t"].as_mstring(); + debugLogA("got a server command to dispatch: %S", wszCommand.c_str()); + + GatewayHandlerFunc pFunc = GetHandler(wszCommand); + if (pFunc) + (this->*pFunc)(pRoot["d"]); + } + break; + + case OPCODE_RECONNECT: // we need to reconnect asap + debugLogA("we need to reconnect, leaving worker thread"); + return true; + + case OPCODE_INVALID_SESSION: // session invalidated + if (pRoot["d"].as_bool()) // session can be resumed + GatewaySendResume(); + else { + Sleep(5000); // 5 seconds - recommended timeout + GatewaySendIdentify(); + } + break; + + case OPCODE_HELLO: // hello + m_iHartbeatInterval = pRoot["d"]["heartbeat_interval"].as_int(); + + GatewaySendIdentify(); + break; + + case OPCODE_HEARTBEAT_ACK: // heartbeat ack + break; + + default: + debugLogA("ACHTUNG! Unknown opcode: %d, report it to developer", opCode); + } + + return false; +} + +////////////////////////////////////////////////////////////////////////////////////// +// requests to be sent to a gateway + +void CDiscordProto::GatewaySendGuildInfo(CDiscordGuild *pGuild) +{ + if (!pGuild->arChannels.getCount()) + return; + + JSONNode a1(JSON_ARRAY); a1 << INT_PARAM("", 0) << INT_PARAM("", 99); + + CMStringA szId(FORMAT, "%lld", pGuild->arChannels[0]->id); + JSONNode chl(JSON_ARRAY); chl.set_name(szId.c_str()); chl << a1; + + JSONNode channels; channels.set_name("channels"); channels << chl; + + JSONNode payload; payload.set_name("d"); + payload << SINT64_PARAM("guild_id", pGuild->id) << BOOL_PARAM("typing", true) << BOOL_PARAM("activities", true) << BOOL_PARAM("presences", true) << channels; + + JSONNode root; + root << INT_PARAM("op", OPCODE_REQUEST_SYNC_CHANNEL) << payload; + GatewaySend(root); +} + +void CDiscordProto::GatewaySendHeartbeat() +{ + // we don't send heartbeat packets until we get logged in + if (!m_iHartbeatInterval || !m_iGatewaySeq) + return; + + JSONNode root; + root << INT_PARAM("op", OPCODE_HEARTBEAT) << INT_PARAM("d", m_iGatewaySeq); + GatewaySend(root); +} + +void CDiscordProto::GatewaySendIdentify() +{ + if (m_szAccessToken == nullptr) { + ConnectionFailed(LOGINERR_WRONGPASSWORD); + return; + } + + char szOs[256]; + OS_GetDisplayString(szOs, _countof(szOs)); + + char szVersion[256]; + Miranda_GetVersionText(szVersion, _countof(szVersion)); + + JSONNode props; props.set_name("properties"); + props << CHAR_PARAM("os", szOs) << CHAR_PARAM("browser", "Chrome") << CHAR_PARAM("device", szVersion) + << CHAR_PARAM("referrer", "https://miranda-ng.org") << CHAR_PARAM("referring_domain", "miranda-ng.org"); + + JSONNode payload; payload.set_name("d"); + payload << CHAR_PARAM("token", m_szAccessToken) << props << BOOL_PARAM("compress", false) << INT_PARAM("large_threshold", 250); + + JSONNode root; + root << INT_PARAM("op", OPCODE_IDENTIFY) << payload; + GatewaySend(root); +} + +void CDiscordProto::GatewaySendResume() +{ + char szRandom[40]; + uint8_t random[16]; + Utils_GetRandom(random, _countof(random)); + bin2hex(random, _countof(random), szRandom); + + JSONNode root; + root << CHAR_PARAM("token", szRandom) << CHAR_PARAM("session_id", m_szGatewaySessionId) << INT_PARAM("seq", m_iGatewaySeq); + GatewaySend(root); +} + +bool CDiscordProto::GatewaySendStatus(int iStatus, const wchar_t *pwszStatusText) +{ + if (iStatus == ID_STATUS_OFFLINE) { + Push(new AsyncHttpRequest(this, REQUEST_POST, "/auth/logout", nullptr)); + return true; + } + + const char *pszStatus; + switch (iStatus) { + case ID_STATUS_AWAY: + case ID_STATUS_NA: + pszStatus = "idle"; break; + case ID_STATUS_DND: + pszStatus = "dnd"; break; + case ID_STATUS_INVISIBLE: + pszStatus = "invisible"; break; + default: + pszStatus = "online"; break; + } + + JSONNode payload; payload.set_name("d"); + payload << INT64_PARAM("since", __int64(time(0)) * 1000) << BOOL_PARAM("afk", true) << CHAR_PARAM("status", pszStatus); + if (pwszStatusText == nullptr) + payload << CHAR_PARAM("game", nullptr); + else { + JSONNode game; game.set_name("game"); game << WCHAR_PARAM("name", pwszStatusText) << INT_PARAM("type", 0); + payload << game; + } + + JSONNode root; root << INT_PARAM("op", OPCODE_STATUS_UPDATE) << payload; + return GatewaySend(root); +} |