diff options
author | George Hazan <ghazan@miranda.im> | 2017-01-07 13:45:34 +0300 |
---|---|---|
committer | George Hazan <ghazan@miranda.im> | 2017-01-07 19:55:09 +0300 |
commit | e012650957335d5f4295441debf849b3b9b2f4ef (patch) | |
tree | 54438907b350f05b598ff16d9fbcb17c92672647 /protocols/Discord | |
parent | e59fe8f85cfa3e309a465dd92d17ebfc7093c5fb (diff) |
first version of Discord with working gateway connection
Diffstat (limited to 'protocols/Discord')
-rw-r--r-- | protocols/Discord/src/connection.cpp | 5 | ||||
-rw-r--r-- | protocols/Discord/src/dispatch.cpp | 64 | ||||
-rw-r--r-- | protocols/Discord/src/gateway.cpp | 135 | ||||
-rw-r--r-- | protocols/Discord/src/main.cpp | 4 | ||||
-rw-r--r-- | protocols/Discord/src/proto.h | 23 | ||||
-rw-r--r-- | protocols/Discord/src/server.cpp | 22 |
6 files changed, 198 insertions, 55 deletions
diff --git a/protocols/Discord/src/connection.cpp b/protocols/Discord/src/connection.cpp index bc48d402e4..7a7fb306a9 100644 --- a/protocols/Discord/src/connection.cpp +++ b/protocols/Discord/src/connection.cpp @@ -64,10 +64,6 @@ void CDiscordProto::OnLoggedIn() m_bOnline = true; SetServerStatus(m_iDesiredStatus); - Push(new AsyncHttpRequest(this, REQUEST_GET, "/users/@me/guilds", &CDiscordProto::OnReceiveGuilds)); - Push(new AsyncHttpRequest(this, REQUEST_GET, "/users/@me/channels", &CDiscordProto::OnReceiveChannels)); - Push(new AsyncHttpRequest(this, REQUEST_GET, "/users/@me/relationships", &CDiscordProto::OnReceiveFriends)); - if (m_szGateway.IsEmpty()) Push(new AsyncHttpRequest(this, REQUEST_GET, "/gateway", &CDiscordProto::OnReceiveGateway)); else @@ -91,6 +87,7 @@ void CDiscordProto::ShutdownSession() { debugLogA("CDiscordProto::ShutdownSession"); m_bTerminated = true; + m_iGatewaySeq = 0; if (m_hWorkerThread) SetEvent(m_evRequestsQueue); OnLoggedOut(); diff --git a/protocols/Discord/src/dispatch.cpp b/protocols/Discord/src/dispatch.cpp new file mode 100644 index 0000000000..f9cf006a09 --- /dev/null +++ b/protocols/Discord/src/dispatch.cpp @@ -0,0 +1,64 @@ +/* +Copyright © 2016-17 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" + +extern HWND g_hwndHeartbeat; + +void CALLBACK CDiscordProto::HeartbeatTimerProc(HWND, UINT, UINT_PTR id, DWORD) +{ + ((CDiscordProto*)id)->GatewaySendHeartbeat(); +} + +static void __stdcall sttStartTimer(void *param) +{ + CDiscordProto *ppro = (CDiscordProto*)param; + SetTimer(g_hwndHeartbeat, (UINT_PTR)param, ppro->getHeartbeatInterval(), &CDiscordProto::HeartbeatTimerProc); +} + +void CDiscordProto::OnCommandReady(const JSONNode &pRoot) +{ + GatewaySendHeartbeat(); + CallFunctionAsync(sttStartTimer, this); + + m_szGatewaySessionId = pRoot["session_id"].as_mstring(); + + const JSONNode &relations = pRoot["relationships"]; + for (auto it = relations.begin(); it != relations.end(); ++it) { + const JSONNode &p = *it; + + const JSONNode &user = p["user"]; + if (user) + PrepareUser(user); + } + + const JSONNode &channels = pRoot["private_channels"]; + for (auto it = channels.begin(); it != channels.end(); ++it) { + const JSONNode &p = *it; + + const JSONNode &user = p["recipient"]; + if (!user) + continue; + + CDiscordUser *pUser = PrepareUser(user); + pUser->lastMessageId = _wtoi64(p["last_message_id"].as_mstring()); + pUser->channelId = _wtoi64(p["id"].as_mstring()); + pUser->bIsPrivate = true; + + setId(pUser->hContact, DB_KEY_CHANNELID, pUser->channelId); + } +} diff --git a/protocols/Discord/src/gateway.cpp b/protocols/Discord/src/gateway.cpp index c8ed0121ce..b8049e3a1b 100644 --- a/protocols/Discord/src/gateway.cpp +++ b/protocols/Discord/src/gateway.cpp @@ -17,24 +17,34 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. #include "stdafx.h" -void CDiscordProto::OnReceiveGateway(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) +#pragma pack(4) + +struct CDiscordCommand { - if (pReply->resultCode != 200) { - ShutdownSession(); - return; - } + const wchar_t *szCommandId; + GatewayHandlerFunc pFunc; +} +static handlers[] = // these structures must me sorted alphabetically +{ + { L"READY", &CDiscordProto::OnCommandReady } +}; - JSONNode root = JSONNode::parse(pReply->pData); - if (!root) { - ShutdownSession(); - return; - } +static int __cdecl pSearchFunc(const void *p1, const void *p2) +{ + return wcscmp(((CDiscordCommand*)p1)->szCommandId, ((CDiscordCommand*)p2)->szCommandId); +} - m_szGateway = root["url"].as_mstring(); - ForkThread(&CDiscordProto::GatewayThread, NULL); +GatewayHandlerFunc CDiscordProto::GetHandler(const wchar_t *pwszCommand) +{ + CDiscordCommand tmp = { pwszCommand, NULL }; + CDiscordCommand *p = (CDiscordCommand*)bsearch(&tmp, handlers, _countof(handlers), sizeof(handlers[0]), pSearchFunc); + return (p != NULL) ? p->pFunc : NULL; } -void CDiscordProto::GatewaySend(int opCode, const JSONNode &pRoot) +////////////////////////////////////////////////////////////////////////////////////// +// sends a piece of JSON to a server via a websocket, masked + +void CDiscordProto::GatewaySend(const JSONNode &pRoot, int opCode) { if (m_hGatewayConnection == NULL) return; @@ -42,7 +52,9 @@ void CDiscordProto::GatewaySend(int opCode, const JSONNode &pRoot) json_string szText = pRoot.write(); BYTE header[20]; - size_t datalen, strLen = szText.length(); + size_t datalen; + uint64_t strLen = szText.length(); + header[0] = 0x80 + (opCode & 0x7F); if (strLen < 126) { header[1] = (strLen & 0xFF); @@ -67,15 +79,36 @@ void CDiscordProto::GatewaySend(int opCode, const JSONNode &pRoot) datalen = 10; } + union { + uLong dwMask; + Bytef arMask[4]; + }; + dwMask = crc32(rand(), (Bytef*)szText.c_str(), (uInt)szText.length()); + memcpy(header + datalen, arMask, _countof(arMask)); + datalen += _countof(arMask); + header[1] |= 0x80; + ptrA sendBuf((char*)mir_alloc(strLen + datalen)); memcpy(sendBuf, header, datalen); - if (strLen) + if (strLen) { memcpy(sendBuf.get() + datalen, szText.c_str(), strLen); + for (size_t i = 0; i < strLen; i++) + sendBuf[i + datalen] ^= arMask[i & 3]; + } Netlib_Send(m_hGatewayConnection, sendBuf, int(strLen + datalen), 0); } +////////////////////////////////////////////////////////////////////////////////////// +// gateway worker thread + void CDiscordProto::GatewayThread(void*) { + GatewayThreadWorker(); + ShutdownSession(); +} + +void CDiscordProto::GatewayThreadWorker() +{ // connect to the gateway server if (!mir_strncmp(m_szGateway, "wss://", 6)) m_szGateway.Delete(0, 6); @@ -96,13 +129,11 @@ void CDiscordProto::GatewayThread(void*) m_hGatewayConnection = (HANDLE)CallService(MS_NETLIB_OPENCONNECTION, (WPARAM)m_hGatewayNetlibUser, (LPARAM)&conn); if (m_hGatewayConnection == NULL) { debugLogA("Gateway connection failed to connect to %s:%d, exiting", m_szGateway.c_str(), conn.wPort); - LBL_Fatal: - ShutdownSession(); return; } { CMStringA szBuf; - szBuf.AppendFormat("GET https://%s/?v=6 HTTP/1.1\r\n", m_szGateway.c_str()); + szBuf.AppendFormat("GET https://%s/?encoding=json&v=6 HTTP/1.1\r\n", m_szGateway.c_str()); szBuf.AppendFormat("Host: %s\r\n", m_szGateway.c_str()); szBuf.AppendFormat("Upgrade: websocket\r\n"); szBuf.AppendFormat("Pragma: no-cache\r\n"); @@ -114,7 +145,7 @@ void CDiscordProto::GatewayThread(void*) szBuf.AppendFormat("\r\n"); if (Netlib_Send(m_hGatewayConnection, szBuf, szBuf.GetLength(), MSG_DUMPASTEXT) == SOCKET_ERROR) { debugLogA("Error establishing gateway connection to %s:%d, send failed", m_szGateway.c_str(), conn.wPort); - goto LBL_Fatal; + return; } } { @@ -122,13 +153,13 @@ void CDiscordProto::GatewayThread(void*) int bufSize = Netlib_Recv(m_hGatewayConnection, buf, _countof(buf), MSG_DUMPASTEXT); if (bufSize <= 0) { debugLogA("Error establishing gateway connection to %s:%d, read failed", m_szGateway.c_str(), conn.wPort); - goto LBL_Fatal; + return; } int status = 0; if (sscanf(buf, "HTTP/1.1 %d", &status) != 1 || status != 101) { debugLogA("Error establishing gateway connection to %s:%d, status %d", m_szGateway.c_str(), conn.wPort, status); - goto LBL_Fatal; + return; } } @@ -144,19 +175,8 @@ void CDiscordProto::GatewayThread(void*) if (m_bTerminated) break; - NETLIBSELECT sel = {}; - sel.cbSize = sizeof(sel); - sel.dwTimeout = 1000; - sel.hReadConns[0] = m_hGatewayConnection; - CallService(MS_NETLIB_SELECT, 0, (LPARAM)&sel); - { - int iInterval = GetTickCount() - m_dwLastHeartbeat; - if (m_iHartbeatInterval && iInterval > m_iHartbeatInterval) - GatewaySendHeartbeat(); - } - unsigned char buf[2048]; - int bufSize = Netlib_Recv(m_hGatewayConnection, (char*)buf+offset, _countof(buf) - offset, 0); + int bufSize = Netlib_Recv(m_hGatewayConnection, (char*)buf + offset, _countof(buf) - offset, MSG_DUMPASTEXT); if (bufSize == 0) { debugLogA("Gateway connection gracefully closed"); break; @@ -258,28 +278,56 @@ void CDiscordProto::GatewayThread(void*) Netlib_CloseHandle(m_hGatewayConnection); m_hGatewayConnection = NULL; - ShutdownSession(); } +////////////////////////////////////////////////////////////////////////////////////// +// handles server commands + void CDiscordProto::GatewayProcess(const JSONNode &pRoot) { int opCode = pRoot["op"].as_int(); switch (opCode) { + case 0: // 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 10: // hello m_iHartbeatInterval = pRoot["d"]["heartbeat_interval"].as_int(); - m_dwLastHeartbeat = GetTickCount(); - m_iGatewaySeq = 1; GatewaySendIdentify(); break; + + case 11: // heartbeat ack + break; + + default: + debugLogA("ACHTUNG! Unknown opcode: %d, report it to developer", opCode); } } +////////////////////////////////////////////////////////////////////////////////////// +// requests to be sent to a gateway + 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", 1) << INT_PARAM("d", m_iGatewaySeq); - GatewaySend(1, root); + GatewaySend(root); } void CDiscordProto::GatewaySendIdentify() @@ -291,14 +339,13 @@ void CDiscordProto::GatewaySendIdentify() Miranda_GetVersionText(szVersion, _countof(szVersion)); JSONNode props; props.set_name("properties"); - props << WCHAR_PARAM("$os", L"Windows") << CHAR_PARAM("$browser", "Miranda NG") << CHAR_PARAM("$device", "Miranda NG") - << CHAR_PARAM("$referrer", "") << CHAR_PARAM("$referring_domain", ""); + props << WCHAR_PARAM("os", wszOs) << CHAR_PARAM("browser", "Chrome") << CHAR_PARAM("device", "Miranda NG") + << CHAR_PARAM("referrer", "http://miranda-ng.org") << CHAR_PARAM("referring_domain", "miranda-ng.org"); - JSONNode shards(JSON_ARRAY); shards.set_name("shard"); - shards << INT_PARAM("", 1) << INT_PARAM("", 10); + 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 << CHAR_PARAM("token", m_szAccessToken) << BOOL_PARAM("compress", false) << INT_PARAM("large_threshold", 250); - root << props; // << shards; - GatewaySend(1, root); + root << INT_PARAM("op", 2) << payload; + GatewaySend(root); } diff --git a/protocols/Discord/src/main.cpp b/protocols/Discord/src/main.cpp index edfac36d0c..850d4a044e 100644 --- a/protocols/Discord/src/main.cpp +++ b/protocols/Discord/src/main.cpp @@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. HINSTANCE g_hInstance; int hLangpack = 0; +HWND g_hwndHeartbeat; PLUGININFOEX pluginInfo = { sizeof(PLUGININFOEX), @@ -68,6 +69,8 @@ extern "C" int __declspec(dllexport) Load(void) { mir_getLP(&pluginInfo); + g_hwndHeartbeat = CreateWindowEx(0, L"STATIC", NULL, 0, 0, 0, 0, 0, NULL, NULL, NULL, NULL); + PROTOCOLDESCRIPTOR pd = { 0 }; pd.cbSize = sizeof(pd); pd.szName = "Discord"; @@ -83,5 +86,6 @@ extern "C" int __declspec(dllexport) Load(void) extern "C" int __declspec(dllexport) Unload(void) { + DestroyWindow(g_hwndHeartbeat); return 0; } diff --git a/protocols/Discord/src/proto.h b/protocols/Discord/src/proto.h index aa4e36ef1c..73e48a806a 100644 --- a/protocols/Discord/src/proto.h +++ b/protocols/Discord/src/proto.h @@ -3,6 +3,7 @@ typedef __int64 SnowFlake; class CDiscordProto; typedef void (CDiscordProto::*HttpCallback)(NETLIBHTTPREQUEST*, struct AsyncHttpRequest*); +typedef void (CDiscordProto::*GatewayHandlerFunc)(const JSONNode&); struct AsyncHttpRequest : public NETLIBHTTPREQUEST, public MZeroedObject { @@ -129,13 +130,18 @@ class CDiscordProto : public PROTO<CDiscordProto> ////////////////////////////////////////////////////////////////////////////////////// // gateway - CMStringA m_szGateway; + CMStringA + m_szGateway, // gateway url + m_szGatewaySessionId; // current session id + HANDLE m_hGatewayNetlibUser, // the separate netlib user handle for gateways m_hGatewayConnection; // gateway connection void __cdecl GatewayThread(void*); - void GatewaySend(int opCode, const JSONNode&); + void CDiscordProto::GatewayThreadWorker(void); + + void GatewaySend(const JSONNode&, int opCode = 1); void GatewayProcess(const JSONNode&); void GatewaySendHeartbeat(void); @@ -143,9 +149,10 @@ class CDiscordProto : public PROTO<CDiscordProto> void OnReceiveGateway(NETLIBHTTPREQUEST*, AsyncHttpRequest*); - int m_iHartbeatInterval; - int m_iGatewaySeq; // gateway sequence number - DWORD m_dwLastHeartbeat; + GatewayHandlerFunc GetHandler(const wchar_t*); + + int m_iHartbeatInterval; // in milliseconds + int m_iGatewaySeq; // gateway sequence number ////////////////////////////////////////////////////////////////////////////////////// // options @@ -199,6 +206,9 @@ public: int __cdecl OnOptionsInit(WPARAM, LPARAM); int __cdecl OnSrmmEvent(WPARAM, LPARAM); + // dispatch commands + void OnCommandReady(const JSONNode&); + void OnLoggedIn(); void OnLoggedOut(); @@ -216,4 +226,7 @@ public: // Misc void SetServerStatus(int iStatus); + + static void CALLBACK HeartbeatTimerProc(HWND hwnd, UINT msg, UINT_PTR id, DWORD); + __forceinline int getHeartbeatInterval() const { return m_iHartbeatInterval; } }; diff --git a/protocols/Discord/src/server.cpp b/protocols/Discord/src/server.cpp index a5ac8e215a..855565d327 100644 --- a/protocols/Discord/src/server.cpp +++ b/protocols/Discord/src/server.cpp @@ -124,6 +124,26 @@ void CDiscordProto::OnReceiveUserInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpReques } ///////////////////////////////////////////////////////////////////////////////////////// +// finds a gateway address + +void CDiscordProto::OnReceiveGateway(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) +{ + if (pReply->resultCode != 200) { + ShutdownSession(); + return; + } + + JSONNode root = JSONNode::parse(pReply->pData); + if (!root) { + ShutdownSession(); + return; + } + + m_szGateway = root["url"].as_mstring(); + ForkThread(&CDiscordProto::GatewayThread, NULL); +} + +///////////////////////////////////////////////////////////////////////////////////////// void CDiscordProto::SetServerStatus(int iStatus) { @@ -224,5 +244,3 @@ LBL_Error: RetrieveUserInfo(NULL); } - -///////////////////////////////////////////////////////////////////////////////////////// |