summaryrefslogtreecommitdiff
path: root/protocols/Discord
diff options
context:
space:
mode:
authorGeorge Hazan <ghazan@miranda.im>2017-01-07 13:45:34 +0300
committerGeorge Hazan <ghazan@miranda.im>2017-01-07 19:55:09 +0300
commite012650957335d5f4295441debf849b3b9b2f4ef (patch)
tree54438907b350f05b598ff16d9fbcb17c92672647 /protocols/Discord
parente59fe8f85cfa3e309a465dd92d17ebfc7093c5fb (diff)
first version of Discord with working gateway connection
Diffstat (limited to 'protocols/Discord')
-rw-r--r--protocols/Discord/src/connection.cpp5
-rw-r--r--protocols/Discord/src/dispatch.cpp64
-rw-r--r--protocols/Discord/src/gateway.cpp135
-rw-r--r--protocols/Discord/src/main.cpp4
-rw-r--r--protocols/Discord/src/proto.h23
-rw-r--r--protocols/Discord/src/server.cpp22
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);
}
-
-/////////////////////////////////////////////////////////////////////////////////////////