/* 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 . */ #include "stdafx.h" ///////////////////////////////////////////////////////////////////////////////////////// // removes a friend from the server void CDiscordProto::RemoveFriend(SnowFlake id) { Push(new AsyncHttpRequest(this, REQUEST_DELETE, CMStringA(FORMAT, "/users/@me/relationships/%lld", id), NULL)); } ///////////////////////////////////////////////////////////////////////////////////////// // retrieves server history void CDiscordProto::RetrieveHistory(MCONTACT hContact, CDiscordHistoryOp iOp, SnowFlake msgid, int iLimit) { CDiscordUser *pUser = FindUser(getId(hContact, DB_KEY_ID)); if (pUser == NULL) return; CMStringA szUrl(FORMAT, "/channels/%lld/messages", pUser->channelId); AsyncHttpRequest *pReq = new AsyncHttpRequest(this, REQUEST_GET, szUrl, &CDiscordProto::OnReceiveHistory); pReq << INT_PARAM("limit", iLimit); 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(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) { CDiscordUser *pUser = (CDiscordUser*)pReq->pUserInfo; if (pReply->resultCode != 200) return; JSONNode root = JSONNode::parse(pReply->pData); if (!root) return; SESSION_INFO *si = nullptr; if (!pUser->bIsPrivate) { si = pci->SM_FindSession(pUser->wszUsername, m_szModuleName); if (si == NULL) { debugLogA("nessage to unknown channal %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.begin(); it != root.end(); ++it, ++iNumMessages) { JSONNode &p = *it; arNodes.insert(&p); } for (int i = 0; i < arNodes.getCount(); i++) { JSONNode &p = *arNodes[i]; CMStringW wszText = PrepareMessageText(p); CMStringW wszUserId = p["author"]["id"].as_mstring(); SnowFlake msgid = ::getId(p["id"]); SnowFlake authorid = _wtoi64(wszUserId); DWORD dwTimeStamp = StringToDate(p["timestamp"].as_mstring()); if (pUser->bIsPrivate) { DBEVENTINFO dbei = {}; dbei.szModule = m_szModuleName; dbei.flags = DBEF_UTF; dbei.eventType = EVENTTYPE_MESSAGE; 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; CMStringA szBody(ptrA(mir_utf8encodeW(wszText))); szBody.AppendFormat("%c%lld", 0, msgid); dbei.timestamp = dwTimeStamp; dbei.pBlob = (PBYTE)szBody.GetBuffer(); dbei.cbBlob = szBody.GetLength(); db_event_add(pUser->hContact, &dbei); } else { ParseSpecialChars(si, wszText); GCDEST gcd = { m_szModuleName, pUser->wszUsername, GC_EVENT_MESSAGE }; GCEVENT gce = { &gcd }; gce.dwFlags = GCEF_ADDTOLOG; gce.ptszUID = wszUserId; gce.ptszText = wszText; gce.time = dwTimeStamp; gce.bIsMe = authorid == m_ownId; Chat_Event(&gce); } 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->lastMessageId) RetrieveHistory(pUser->hContact, MSG_AFTER, lastId, 99); } ///////////////////////////////////////////////////////////////////////////////////////// // retrieves user info void CDiscordProto::RetrieveUserInfo(MCONTACT hContact) { CMStringA szUrl; if (hContact != 0) { SnowFlake id = getId(hContact, DB_KEY_ID); if (id == 0) return; szUrl.Format("/users/%lld", id); } else szUrl = "/users/@me"; AsyncHttpRequest *pReq = new AsyncHttpRequest(this, REQUEST_GET, szUrl, &CDiscordProto::OnReceiveUserInfo); pReq->pUserInfo = (void*)hContact; Push(pReq); } void CDiscordProto::OnReceiveUserInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) { MCONTACT hContact = (MCONTACT)pReq->pUserInfo; if (pReply->resultCode != 200) { if (hContact == NULL) ConnectionFailed(LOGINERR_WRONGPASSWORD); return; } JSONNode root = JSONNode::parse(pReply->pData); if (!root) { if (hContact == NULL) ConnectionFailed(LOGINERR_NOSERVER); return; } SnowFlake id = ::getId(root["id"]); setId(hContact, DB_KEY_ID, id); setByte(hContact, DB_KEY_MFA, root["mfa_enabled"].as_bool()); setDword(hContact, DB_KEY_DISCR, _wtoi(root["discriminator"].as_mstring())); setWString(hContact, DB_KEY_NICK, root["username"].as_mstring()); setWString(hContact, DB_KEY_EMAIL, root["email"].as_mstring()); if (hContact == NULL) { m_ownId = id; OnLoggedIn(); } else { CDiscordUser *pUser = FindUser(id); ProcessType(pUser, root); } CheckAvatarChange(hContact, root["avatar"].as_mstring()); } ///////////////////////////////////////////////////////////////////////////////////////// // 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) { if (!m_bOnline) return; if (iStatus == ID_STATUS_OFFLINE) Push(new AsyncHttpRequest(this, REQUEST_POST, "/auth/logout", NULL)); else { 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 root; root << CHAR_PARAM("status", pszStatus); Push(new AsyncHttpRequest(this, REQUEST_PATCH, "/users/@me/settings", NULL, &root)); } int iOldStatus = m_iStatus; m_iStatus = iStatus; ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); } void CDiscordProto::OnReceiveAuth(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) { MCONTACT hContact = (MCONTACT)pReq->pUserInfo; if (pReply->resultCode == 204) RetrieveUserInfo(hContact); } ///////////////////////////////////////////////////////////////////////////////////////// // channels void CDiscordProto::OnReceiveCreateChannel(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) { if (pReply->resultCode != 200) return; JSONNode root = JSONNode::parse(pReply->pData); if (root) OnCommandChannelCreated(root); } void CDiscordProto::OnReceiveChannels(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) { if (pReply->resultCode != 200) return; JSONNode root = JSONNode::parse(pReply->pData); if (!root) return; for (auto it = root.begin(); it != root.end(); ++it) { const JSONNode &p = *it; const JSONNode &user = p["recipient"]; if (!user) continue; CDiscordUser *pUser = PrepareUser(user); pUser->lastMessageId = ::getId(p["last_message_id"]); pUser->channelId = ::getId(p["id"]); pUser->bIsPrivate = p["is_private"].as_bool(); setId(pUser->hContact, DB_KEY_CHANNELID, pUser->channelId); } } ///////////////////////////////////////////////////////////////////////////////////////// void CDiscordProto::OnReceiveFriends(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) { if (pReply->resultCode != 200) return; JSONNode root = JSONNode::parse(pReply->pData); if (!root) return; for (auto it = root.begin(); it != root.end(); ++it) { JSONNode &p = *it; JSONNode &user = p["user"]; if (user) PrepareUser(user); } } ///////////////////////////////////////////////////////////////////////////////////////// void CDiscordProto::OnReceiveGuilds(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) { if (pReply->resultCode != 200) return; } ///////////////////////////////////////////////////////////////////////////////////////// void CDiscordProto::OnReceiveMessage(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) { MCONTACT hContact = (MCONTACT)pReq->pUserInfo; bool bSucceeded = true; if (pReply->resultCode != 200 && pReply->resultCode != 204) bSucceeded = false; JSONNode root = JSONNode::parse(pReply->pData); if (root) { SnowFlake newLastId = ::getId(root["id"]); SnowFlake oldLastId = getId(hContact, DB_KEY_LASTMSGID); // as stored in a database if (oldLastId < newLastId) setId(hContact, DB_KEY_LASTMSGID, newLastId); } ProtoBroadcastAck(hContact, ACKTYPE_MESSAGE, bSucceeded ? ACKRESULT_SUCCESS : ACKRESULT_FAILED, (HANDLE)pReq->m_iReqNum, 0); } ///////////////////////////////////////////////////////////////////////////////////////// void CDiscordProto::OnReceiveMessageAck(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) { if (pReply->resultCode != 200) return; JSONNode root = JSONNode::parse(pReply->pData); if (!root) return; CMStringW wszToken(root["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", root["token"].as_mstring()); Push(new AsyncHttpRequest(this, REQUEST_POST, "/track", NULL, &reply)); } } ///////////////////////////////////////////////////////////////////////////////////////// void CDiscordProto::OnReceiveToken(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) { if (pReply->resultCode != 200) { ConnectionFailed(LOGINERR_WRONGPASSWORD); return; } JSONNode root = JSONNode::parse(pReply->pData); if (!root) { LBL_Error: ConnectionFailed(LOGINERR_NOSERVER); return; } CMStringA szToken = root["token"].as_mstring(); if (szToken.IsEmpty()) goto LBL_Error; m_szAccessToken = szToken.Detach(); setString("AccessToken", m_szAccessToken); RetrieveUserInfo(NULL); }