/*
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);
}