/* 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 . */ #include "stdafx.h" ////////////////////////////////////////////////////////////////////////////////////// // sends a piece of JSON to a server via a websocket, masked bool CDiscordProto::GatewaySend(const JSONNode &pRoot) { if (m_ws == nullptr) return false; json_string szText = pRoot.write(); debugLogA("Gateway send: %s", szText.c_str()); m_ws->sendText(szText.c_str()); return true; } ////////////////////////////////////////////////////////////////////////////////////// // gateway worker thread void CDiscordProto::GatewayThread(void*) { while (GatewayThreadWorker()) ; ShutdownSession(); } bool CDiscordProto::GatewayThreadWorker() { bool bHasCookie = false; MHttpHeaders hdrs; hdrs.AddHeader("Origin", "https://discord.com"); if (!m_szWSCookie.IsEmpty()) { bHasCookie = true; hdrs.AddHeader("Cookie", m_szWSCookie); } JsonWebSocket ws(this); NLHR_PTR pReply(ws.connect(m_hGatewayNetlibUser, m_szGateway + "/?encoding=json&v=8", &hdrs)); if (pReply == nullptr) { debugLogA("Gateway connection failed, exiting"); return false; } m_szWSCookie = pReply->GetCookies(); 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 (!bHasCookie) return true; m_szWSCookie.Empty(); // don't use the same cookie twice } return false; } // succeeded! debugLogA("Gateway connection succeeded"); m_ws = &ws; ws.run(); m_ws = nullptr; return true; } ////////////////////////////////////////////////////////////////////////////////////// // handles server commands void JsonWebSocket::process(const JSONNode &json) { int opCode = json["op"].as_int(); switch (opCode) { case OPCODE_DISPATCH: // process incoming command { int iSeq = json["s"].as_int(); if (iSeq != 0) p->m_iGatewaySeq = iSeq; CMStringW wszCommand = json["t"].as_mstring(); p->debugLogA("got a server command to dispatch: %S", wszCommand.c_str()); GatewayHandlerFunc pFunc = p->GetHandler(wszCommand); if (pFunc) (p->*pFunc)(json["d"]); } break; case OPCODE_RECONNECT: // we need to reconnect asap p->debugLogA("we need to reconnect, leaving worker thread"); p->m_bTerminated = true; return; case OPCODE_INVALID_SESSION: // session invalidated if (json["d"].as_bool()) // session can be resumed p->GatewaySendResume(); else { Sleep(5000); // 5 seconds - recommended timeout p->GatewaySendIdentify(); } break; case OPCODE_HELLO: // hello p->m_iHartbeatInterval = json["d"]["heartbeat_interval"].as_int(); p->GatewaySendIdentify(); break; case OPCODE_HEARTBEAT_ACK: // heartbeat ack break; default: p->debugLogA("ACHTUNG! Unknown opcode: %d, report it to developer", opCode); } } ////////////////////////////////////////////////////////////////////////////////////// // 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->m_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) // 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; case ID_STATUS_OFFLINE: pszStatus = "offline"; 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); } ///////////////////////////////////////////////////////////////////////////////////////// bool CDiscordProto::GatewaySendVoice(JSONNode &payload) { payload.set_name("d"); JSONNode root; root << INT_PARAM("op", OPCODE_VOICE_UPDATE) << payload; return GatewaySend(root); }