/* 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" ///////////////////////////////////////////////////////////////////////////////////////// // add / removes a friend from the server void CDiscordProto::AddFriend(SnowFlake id) { Push(new AsyncHttpRequest(this, REQUEST_PUT, CMStringA(FORMAT, "/users/@me/relationships/%lld", id), nullptr)); } void CDiscordProto::RemoveFriend(SnowFlake id) { Push(new AsyncHttpRequest(this, REQUEST_DELETE, CMStringA(FORMAT, "/users/@me/relationships/%lld", id), nullptr)); } ///////////////////////////////////////////////////////////////////////////////////////// // retrieves server history void CDiscordProto::RetrieveHistory(CDiscordUser *pUser, CDiscordHistoryOp iOp, SnowFlake msgid, int iLimit) { if (!pUser->hContact || getByte(pUser->hContact, DB_KEY_DONT_FETCH)) return; CMStringA szUrl(FORMAT, "/channels/%lld/messages", pUser->channelId); AsyncHttpRequest *pReq = new AsyncHttpRequest(this, REQUEST_GET, szUrl, &CDiscordProto::OnReceiveHistory); pReq << INT_PARAM("limit", iLimit); if (msgid) { switch (iOp) { case MSG_AFTER: pReq << INT64_PARAM("after", msgid); break; case MSG_BEFORE: pReq << INT64_PARAM("before", msgid); break; } } pReq->pUserInfo = pUser; Push(pReq); } static int compareMsgHistory(const JSONNode *p1, const JSONNode *p2) { return wcscmp((*p1)["id"].as_mstring(), (*p2)["id"].as_mstring()); } void CDiscordProto::OnReceiveHistory(MHttpResponse *pReply, AsyncHttpRequest *pReq) { CDiscordUser *pUser = (CDiscordUser*)pReq->pUserInfo; JsonReply root(pReply); if (!root) { if (root.error() == 403) // forbidden, don't try to read it anymore setByte(pUser->hContact, DB_KEY_DONT_FETCH, true); return; } SESSION_INFO *si = nullptr; if (!pUser->bIsPrivate) { si = Chat_Find(pUser->wszUsername, m_szModuleName); if (si == nullptr) { debugLogA("message to unknown channel %lld ignored", pUser->channelId); return; } } SnowFlake lastId = getId(pUser->hContact, DB_KEY_LASTMSGID); // as stored in a database LIST arNodes(10, compareMsgHistory); int iNumMessages = 0; for (auto &it : root.data()) { arNodes.insert(&it); iNumMessages++; } for (auto &it : arNodes) { auto &pNode = *it; CMStringW wszText = PrepareMessageText(pNode, pUser); if (wszText.IsEmpty()) continue; CMStringA szUserId = pNode["author"]["id"].as_mstring(); SnowFlake msgid = ::getId(pNode["id"]); SnowFlake authorid = _atoi64(szUserId); uint32_t dwTimeStamp = StringToDate(pNode["timestamp"].as_mstring()); DBEVENTINFO dbei = {}; dbei.szModule = m_szModuleName; dbei.flags = DBEF_UTF; dbei.eventType = EVENTTYPE_MESSAGE; dbei.timestamp = dwTimeStamp; if (authorid == m_ownId) dbei.flags |= DBEF_SENT; else dbei.flags &= ~DBEF_SENT; if (msgid <= pUser->lastReadId) dbei.flags |= DBEF_READ; else dbei.flags &= ~DBEF_READ; if (!pUser->bIsPrivate || pUser->bIsGroup) { dbei.szUserId = szUserId; ProcessChatUser(pUser, _atoi64(szUserId), pNode); } ptrA szBody(mir_utf8encodeW(wszText)); dbei.pBlob = szBody; dbei.cbBlob = (int)mir_strlen(szBody); char szMsgId[100]; _i64toa_s(msgid, szMsgId, _countof(szMsgId), 10); dbei.szId = szMsgId; db_event_replace(pUser->hContact, &dbei); if (lastId < msgid) lastId = msgid; } setId(pUser->hContact, DB_KEY_LASTMSGID, lastId); // if we fetched 99 messages, but have smth more to go, continue fetching if (iNumMessages == 99 && lastId < pUser->lastMsgId) RetrieveHistory(pUser, MSG_AFTER, lastId, 99); } ///////////////////////////////////////////////////////////////////////////////////////// // retrieves user info void CDiscordProto::OnReceiveGateway(MHttpResponse *pReply, AsyncHttpRequest *) { JsonReply root(pReply); if (!root) { ShutdownSession(); return; } auto &data = root.data(); m_szGateway = data["url"].as_mstring(); ForkThread(&CDiscordProto::GatewayThread, nullptr); } void CDiscordProto::OnReceiveMyInfo(MHttpResponse *pReply, AsyncHttpRequest*) { JsonReply root(pReply); if (!root) { ConnectionFailed(LOGINERR_WRONGPASSWORD); return; } auto &data = root.data(); SnowFlake id = ::getId(data["id"]); setId(0, DB_KEY_ID, id); setByte(0, DB_KEY_MFA, data["mfa_enabled"].as_bool()); setDword(0, DB_KEY_DISCR, _wtoi(data["discriminator"].as_mstring())); setWString(0, DB_KEY_NICK, data["username"].as_mstring()); m_ownId = id; m_szCookie = pReply->GetCookies(); CallService(MS_KS_ENABLEPROTOCOL, TRUE, LPARAM(m_szModuleName)); // launch gateway thread if (m_szGateway.IsEmpty()) Push(new AsyncHttpRequest(this, REQUEST_GET, "/gateway", &CDiscordProto::OnReceiveGateway)); else ForkThread(&CDiscordProto::GatewayThread, nullptr); CheckAvatarChange(0, data["avatar"].as_mstring()); } void CDiscordProto::RetrieveMyInfo() { Push(new AsyncHttpRequest(this, REQUEST_GET, "/users/@me", &CDiscordProto::OnReceiveMyInfo)); } ///////////////////////////////////////////////////////////////////////////////////////// void CDiscordProto::SetServerStatus(int iStatus) { if (GatewaySendStatus(iStatus, nullptr)) { int iOldStatus = m_iStatus; m_iStatus = iStatus; ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); } } ///////////////////////////////////////////////////////////////////////////////////////// // channels void CDiscordProto::OnReceiveCreateChannel(MHttpResponse *pReply, AsyncHttpRequest*) { JsonReply root(pReply); if (root) OnCommandChannelCreated(root.data()); } ///////////////////////////////////////////////////////////////////////////////////////// void CDiscordProto::OnReceiveMessageAck(MHttpResponse *pReply, AsyncHttpRequest*) { JsonReply root(pReply); if (!root) return; auto &data = root.data(); CMStringW wszToken(data["token"].as_mstring()); if (!wszToken.IsEmpty()) { JSONNode props; props.set_name("properties"); JSONNode reply; reply << props; reply << CHAR_PARAM("event", "ack_messages") << WCHAR_PARAM("token", data["token"].as_mstring()); Push(new AsyncHttpRequest(this, REQUEST_POST, "/track", nullptr, &reply)); } } ///////////////////////////////////////////////////////////////////////////////////////// #define RECAPTCHA_API_KEY "6Lef5iQTAAAAAKeIvIY-DeexoO3gj7ryl9rLMEnn" #define RECAPTCHA_SITE_URL "https://discord.com" void CDiscordProto::OnReceiveToken(MHttpResponse *pReply, AsyncHttpRequest*) { if (pReply->resultCode != 200) { JSONNode root = JSONNode::parse(pReply->body); if (root) { const JSONNode &captcha = root["captcha_key"].as_array(); if (captcha) { for (auto &it : captcha) { if (it.as_mstring() == "captcha-required") { MessageBoxW(NULL, TranslateT("The server requires you to enter the captcha. Miranda will redirect you to a browser now"), L"Discord", MB_OK | MB_ICONINFORMATION); Utils_OpenUrl("https://discord.com/app"); } } } for (auto &err: root["errors"]["email"]["_errors"]) { CMStringW code(err["code"].as_mstring()); CMStringW message(err["message"].as_mstring()); if (!code.IsEmpty() || !message.IsEmpty()) { POPUPDATAW popup; popup.lchIcon = IcoLib_GetIconByHandle(Skin_GetIconHandle(SKINICON_ERROR), true); wcscpy_s(popup.lpwzContactName, m_tszUserName); mir_snwprintf(popup.lpwzText, TranslateT("Connection failed.\n%s (%s)."), message.c_str(), code.c_str()); PUAddPopupW(&popup); } } } ConnectionFailed(LOGINERR_WRONGPASSWORD); return; } JsonReply root(pReply); if (!root) { ConnectionFailed(LOGINERR_NOSERVER); return; } auto &data = root.data(); if (auto &token = data["token"]) { CMStringA szToken = token.as_mstring(); m_szAccessToken = szToken.Detach(); setString(DB_KEY_TOKEN, m_szAccessToken); RetrieveMyInfo(); return; } if (data["mfa"].as_bool()) { m_szCookie = pReply->GetCookies(); ShowMfaDialog(data); } else { ConnectionFailed(LOGINERR_WRONGPASSWORD); debugLogA("Strange empty token received, exiting"); } }