summaryrefslogtreecommitdiff
path: root/protocols/ICQ-WIM/src/server.cpp
diff options
context:
space:
mode:
authorGeorge Hazan <ghazan@miranda.im>2019-01-16 18:06:52 +0300
committerGeorge Hazan <ghazan@miranda.im>2019-01-16 18:06:52 +0300
commitaa514a9f95c8cd95c01952250f7cdb670a66af9a (patch)
treeecb84e10fc3af008e1a8ac9027f1b526634c8d65 /protocols/ICQ-WIM/src/server.cpp
parent4f0a4ae4724fdfe65f63ee41dc1790ccbb0668f7 (diff)
fixes #1761 (Rename Icq10 folder and ICQ/2018 in version.h to ICQ-WIM)
Diffstat (limited to 'protocols/ICQ-WIM/src/server.cpp')
-rw-r--r--protocols/ICQ-WIM/src/server.cpp859
1 files changed, 859 insertions, 0 deletions
diff --git a/protocols/ICQ-WIM/src/server.cpp b/protocols/ICQ-WIM/src/server.cpp
new file mode 100644
index 0000000000..d77daf4075
--- /dev/null
+++ b/protocols/ICQ-WIM/src/server.cpp
@@ -0,0 +1,859 @@
+// -----------------------------------------------------------------------------
+// ICQ plugin for Miranda NG
+// -----------------------------------------------------------------------------
+// Copyright © 2018-19 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, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+// -----------------------------------------------------------------------------
+
+#include "stdafx.h"
+
+#pragma comment(lib, "libeay32.lib")
+
+void CIcqProto::CheckAvatarChange(MCONTACT hContact, const JSONNode &ev)
+{
+ CMStringW wszIconId(ev["iconId"].as_mstring());
+ CMStringW oldIconID(getMStringW(hContact, "IconId"));
+ if (wszIconId != oldIconID) {
+ setWString(hContact, "IconId", wszIconId);
+
+ CMStringA szUrl(ev["buddyIcon"].as_mstring());
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, szUrl, &CIcqProto::OnReceiveAvatar);
+ pReq->hContact = hContact;
+ Push(pReq);
+ }
+}
+
+void CIcqProto::CheckLastId(MCONTACT hContact, const JSONNode &ev)
+{
+ __int64 msgId = _wtoi64(ev["histMsgId"].as_mstring());
+ __int64 lastId = getId(hContact, DB_KEY_LASTMSGID);
+ if (msgId > lastId)
+ setId(hContact, DB_KEY_LASTMSGID, msgId);
+}
+
+void CIcqProto::CheckNickChange(MCONTACT hContact, const JSONNode &ev)
+{
+ // if we already set a nick, ignore change when friendly names aren't used
+ if (!m_bUseFriendly && !getMStringW(hContact, "Nick").IsEmpty())
+ return;
+
+ CMStringW wszNick = ev["friendly"].as_mstring();
+ if (m_bUseFriendly || !wszNick.IsEmpty())
+ setWString(hContact, "Nick", wszNick);
+}
+
+MCONTACT CIcqProto::CheckOwnMessage(const CMStringA &reqId, const CMStringA &msgId, bool bRemove)
+{
+ for (auto &own: m_arOwnIds) {
+ if (!mir_strcmp(reqId, own->m_guid)) {
+ ProtoBroadcastAck(own->m_hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)own->m_msgid, (LPARAM)msgId.c_str());
+ MCONTACT ret = own->m_hContact;
+ if (bRemove)
+ m_arOwnIds.remove(m_arOwnIds.indexOf(&own));
+ return ret;
+ }
+ }
+ return 0;
+}
+
+void CIcqProto::CheckPassword()
+{
+ char mirVer[100];
+ Miranda_GetVersionText(mirVer, _countof(mirVer));
+
+ m_szAToken = getMStringA(DB_KEY_ATOKEN);
+ m_iRClientId = getDword(DB_KEY_RCLIENTID);
+ m_szSessionKey = getMStringA(DB_KEY_SESSIONKEY);
+ if (m_szAToken.IsEmpty() || m_szSessionKey.IsEmpty()) {
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, "https://api.login.icq.net/auth/clientLogin", &CIcqProto::OnCheckPassword);
+ pReq << CHAR_PARAM("clientName", "Miranda NG") << CHAR_PARAM("clientVersion", mirVer) << CHAR_PARAM("devId", ICQ_APP_ID)
+ << CHAR_PARAM("f", "json") << CHAR_PARAM("tokenType", "longTerm") << INT_PARAM("s", m_dwUin) << WCHAR_PARAM("pwd", m_szPassword);
+ pReq->flags |= NLHRF_NODUMPSEND;
+ Push(pReq);
+ }
+ else StartSession();
+}
+
+void CIcqProto::ConnectionFailed(int iReason)
+{
+ debugLogA("ConnectionFailed -> reason %d", iReason);
+
+ ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, nullptr, iReason);
+ ShutdownSession();
+}
+
+void CIcqProto::MoveContactToGroup(MCONTACT hContact, const wchar_t *pwszGroup, const wchar_t *pwszNewGroup)
+{
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/moveBuddy");
+ pReq << CHAR_PARAM("f", "json") << CHAR_PARAM("aimsid", m_aimsid) << CHAR_PARAM("r", pReq->m_reqId)
+ << CHAR_PARAM("buddy", GetUserId(hContact)) << WCHAR_PARAM("group", pwszGroup) << WCHAR_PARAM("newGroup", pwszNewGroup);
+ Push(pReq);
+}
+
+void CIcqProto::OnLoggedIn()
+{
+ debugLogA("CIcqProto::OnLoggedIn");
+ m_bOnline = true;
+ SetServerStatus(m_iDesiredStatus);
+ RetrieveUserInfo(0);
+}
+
+void CIcqProto::OnLoggedOut()
+{
+ debugLogA("CIcqProto::OnLoggedOut");
+ m_bOnline = false;
+
+ ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE);
+ m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE;
+
+ setAllContactStatuses(ID_STATUS_OFFLINE, false);
+}
+
+MCONTACT CIcqProto::ParseBuddyInfo(const JSONNode &buddy, MCONTACT hContact)
+{
+ // user chat?
+ if (buddy["userType"].as_mstring() == "interop") {
+ CMStringW wszChatId(buddy["aimId"].as_mstring());
+ CMStringW wszChatName(buddy["friendly"].as_mstring());
+
+ SESSION_INFO *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, wszChatId, wszChatName);
+ if (si == nullptr)
+ return INVALID_CONTACT_ID;
+
+ Chat_AddGroup(si, TranslateT("admin"));
+ Chat_AddGroup(si, TranslateT("member"));
+ Chat_Control(m_szModuleName, wszChatId, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE);
+ Chat_Control(m_szModuleName, wszChatId, SESSION_ONLINE);
+ return si->hContact;
+ }
+
+ DWORD dwUin = _wtol(buddy["aimId"].as_mstring());
+
+ if (hContact == -1) {
+ hContact = CreateContact(dwUin, false);
+ FindContactByUIN(dwUin)->m_bInList = true;
+ }
+
+ CMStringW str(buddy["state"].as_mstring());
+ setDword(hContact, "Status", StatusFromString(str));
+
+ const JSONNode &profile = buddy["profile"];
+ if (profile) {
+ str = profile["firstName"].as_mstring();
+ if (!str.IsEmpty())
+ setWString(hContact, "FirstName", str);
+
+ str = profile["lastName"].as_mstring();
+ if (!str.IsEmpty())
+ setWString(hContact, "LastName", str);
+
+ str = profile["friendlyName"].as_mstring();
+ if (!str.IsEmpty())
+ if (!m_bUseFriendly || getMStringW("Nick").IsEmpty())
+ setWString(hContact, "Nick", str);
+
+ time_t birthDate = profile["birthDate"].as_int();
+ if (birthDate != 0) {
+ struct tm *timeinfo = localtime(&birthDate);
+ if (timeinfo != nullptr) {
+ setWord(hContact, "BirthDay", timeinfo->tm_mday);
+ setWord(hContact, "BirthMonth", timeinfo->tm_mon+1);
+ setWord(hContact, "BirthYear", timeinfo->tm_year+1900);
+ }
+ }
+
+ str = profile["gender"].as_mstring();
+ if (!str.IsEmpty()) {
+ if (str == "male")
+ setByte(hContact, "Gender", 'M');
+ else if (str == "female")
+ setByte(hContact, "Gender", 'F');
+ }
+
+ for (auto &it : profile["homeAddress"]) {
+ str = it["city"].as_mstring();
+ if (!str.IsEmpty())
+ setWString(hContact, "City", str);
+
+ str = it["state"].as_mstring();
+ if (!str.IsEmpty())
+ setWString(hContact, "State", str);
+
+ str = it["country"].as_mstring();
+ if (!str.IsEmpty())
+ setWString(hContact, "Country", str);
+ }
+ }
+
+ CheckNickChange(hContact, buddy);
+
+ int lastLogin = buddy["lastseen"].as_int();
+ if (lastLogin)
+ setDword(hContact, "LastSeen", lastLogin);
+
+ str = buddy["statusMsg"].as_mstring();
+ if (str.IsEmpty())
+ db_unset(hContact, "CList", "StatusMsg");
+ else
+ db_set_ws(hContact, "CList", "StatusMsg", str);
+
+ CheckAvatarChange(hContact, buddy);
+ return hContact;
+}
+
+void CIcqProto::ParseMessage(MCONTACT hContact, __int64 &lastMsgId, const JSONNode &it)
+{
+ CMStringA szMsgId(it["msgId"].as_mstring());
+ __int64 msgId = _atoi64(szMsgId);
+ if (msgId > lastMsgId)
+ lastMsgId = msgId;
+
+ CMStringW type(it["mediaType"].as_mstring());
+ if (type != "text" && !type.IsEmpty())
+ return;
+
+ CMStringW wszText(it["text"].as_mstring());
+ if (isChatRoom(hContact)) {
+ CMStringA reqId(it["reqId"].as_mstring());
+ CheckOwnMessage(reqId, szMsgId, true);
+
+ CMStringW wszSender(it["chat"]["sender"].as_mstring());
+ CMStringW wszChatId(getMStringW(hContact, "ChatRoomID"));
+
+ GCEVENT gce = { m_szModuleName, wszChatId, GC_EVENT_MESSAGE };
+ gce.dwFlags = GCEF_ADDTOLOG;
+ gce.ptszUID = wszSender;
+ gce.ptszText = wszText;
+ gce.time = it["time"].as_int();
+ gce.bIsMe = _wtoi(wszSender) == (int)m_dwUin;
+ Chat_Event(&gce);
+ }
+ else {
+ // skip own messages, just set the server msgid
+ CMStringA reqId(it["reqId"].as_mstring());
+ if (CheckOwnMessage(reqId, szMsgId, true))
+ return;
+
+ // ignore duplicates
+ MEVENT hDbEvent = db_event_getById(m_szModuleName, szMsgId);
+ if (hDbEvent != 0)
+ return;
+
+ bool bIsOutgoing = it["outgoing"].as_bool();
+ ptrA szUtf(mir_utf8encodeW(wszText));
+
+ PROTORECVEVENT pre = {};
+ pre.flags = (bIsOutgoing) ? PREF_SENT : 0;
+ pre.szMsgId = szMsgId;
+ pre.timestamp = it["time"].as_int();
+ pre.szMessage = szUtf;
+ ProtoChainRecvMsg(hContact, &pre);
+ }
+}
+
+bool CIcqProto::RefreshRobustToken()
+{
+ if (!m_szRToken.IsEmpty())
+ return true;
+
+ bool bRet = false;
+ auto *tmp = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER "/genToken");
+
+ time_t ts = time(0);
+ tmp << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("k", ICQ_APP_ID)
+ << CHAR_PARAM("nonce", CMStringA(FORMAT, "%d-%d", ts, rand() % 10)) << INT_PARAM("ts", ts);
+ CalcHash(tmp);
+ tmp->flags |= NLHRF_PERSISTENT;
+ tmp->nlc = m_ConnPool[CONN_RAPI].s;
+ tmp->dataLength = tmp->m_szParam.GetLength();
+ tmp->pData = tmp->m_szParam.Detach();
+ tmp->szUrl = tmp->m_szUrl.GetBuffer();
+
+ CMStringA szAgent(FORMAT, "%d Mail.ru Windows ICQ (version 10.0.1999)", DWORD(m_dwUin));
+ tmp->AddHeader("User-Agent", szAgent);
+
+ NETLIBHTTPREQUEST *reply = Netlib_HttpTransaction(m_hNetlibUser, tmp);
+ if (reply != nullptr) {
+ m_ConnPool[CONN_RAPI].s = reply->nlc;
+
+ RobustReply result(reply);
+ if (result.error() == 20000) {
+ const JSONNode &results = result.results();
+ m_szRToken = results["authToken"].as_mstring();
+
+ // now add this token
+ auto *add = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER, &CIcqProto::OnAddClient);
+ JSONNode request, params; params.set_name("params");
+ request << CHAR_PARAM("method", "addClient") << CHAR_PARAM("reqId", add->m_reqId) << CHAR_PARAM("authToken", m_szRToken) << params;
+ add->m_szParam = ptrW(json_write(&request));
+ add->pUserInfo = &bRet;
+ ExecuteRequest(add);
+ }
+
+ Netlib_FreeHttpRequest(reply);
+ }
+ else m_ConnPool[CONN_RAPI].s = nullptr;
+
+ delete tmp;
+ return bRet;
+}
+
+void CIcqProto::RetrieveUserInfo(MCONTACT hContact)
+{
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/presence/get", &CIcqProto::OnGetUserInfo);
+ pReq->flags |= NLHRF_NODUMPSEND;
+ pReq->hContact = hContact;
+ pReq << CHAR_PARAM("f", "json") << CHAR_PARAM("aimsid", m_aimsid) << INT_PARAM("mdir", 1) << CHAR_PARAM("t", GetUserId(hContact));
+ Push(pReq);
+}
+
+void CIcqProto::RetrieveUserHistory(MCONTACT hContact, __int64 startMsgId, __int64 endMsgId)
+{
+ if (startMsgId == 0)
+ startMsgId = -1;
+ if (endMsgId == 0)
+ endMsgId = -1;
+
+ if (startMsgId >= endMsgId)
+ return;
+
+ auto *pReq = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER, &CIcqProto::OnGetUserHistory);
+ pReq->flags |= NLHRF_NODUMPSEND;
+ pReq->hContact = hContact;
+
+ JSONNode request, params; params.set_name("params");
+ params << CHAR_PARAM("sn", GetUserId(hContact)) << INT64_PARAM("fromMsgId", startMsgId);
+ if (endMsgId != -1)
+ params << INT64_PARAM("tillMsgId", endMsgId);
+ params << INT_PARAM("count", 1000) << CHAR_PARAM("aimSid", m_aimsid) << CHAR_PARAM("patchVersion", "1") << CHAR_PARAM("language", "ru-ru");
+ request << CHAR_PARAM("method", "getHistory") << CHAR_PARAM("reqId", pReq->m_reqId) << CHAR_PARAM("authToken", m_szRToken)
+ << INT_PARAM("clientId", m_iRClientId) << params;
+ pReq->m_szParam = ptrW(json_write(&request));
+ Push(pReq);
+}
+
+void CIcqProto::SetServerStatus(int iStatus)
+{
+ const char *szStatus = "online";
+ int invisible = 0;
+
+ switch (iStatus) {
+ case ID_STATUS_OFFLINE: szStatus = "offline"; break;
+ case ID_STATUS_NA: szStatus = "occupied"; break;
+ case ID_STATUS_AWAY:
+ case ID_STATUS_DND: szStatus = "away"; break;
+ case ID_STATUS_INVISIBLE:
+ invisible = 1;
+ }
+
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/presence/setState");
+ pReq->flags |= NLHRF_NODUMPSEND;
+ pReq << CHAR_PARAM("f", "json") << CHAR_PARAM("aimsid", m_aimsid) << CHAR_PARAM("r", pReq->m_reqId)
+ << CHAR_PARAM("view", szStatus) << INT_PARAM("invisible", invisible);
+ Push(pReq);
+
+ int iOldStatus = m_iStatus; m_iStatus = iStatus;
+ ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus);
+}
+
+void CIcqProto::ShutdownSession()
+{
+ if (m_bTerminated)
+ return;
+
+ debugLogA("CIcqProto::ShutdownSession");
+
+ // shutdown all resources
+ if (m_hWorkerThread)
+ SetEvent(m_evRequestsQueue);
+
+ OnLoggedOut();
+
+ for (auto &it : m_ConnPool)
+ if (it.s)
+ Netlib_Shutdown(it.s);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+#define CAPS "094613504c7f11d18222444553540000,094613514c7f11d18222444553540000,094613534c7f11d18222444553540000,094613544c7f11d18222444553540000,094613594c7f11d18222444553540000,0946135b4c7f11d18222444553540000,0946135a4c7f11d18222444553540000"
+#define EVENTS "myInfo,presence,buddylist,typing,dataIM,userAddedToBuddyList,webrtcMsg,mchat,hist,hiddenChat,diff,permitDeny,imState,notification,apps"
+#define FIELDS "aimId,buddyIcon,bigBuddyIcon,iconId,bigIconId,largeIconId,displayId,friendly,offlineMsg,state,statusMsg,userType,phoneNumber,cellNumber,smsNumber,workNumber,otherNumber,capabilities,ssl,abPhoneNumber,moodIcon,lastName,abPhones,abContactName,lastseen,mute,livechat,official"
+
+void CIcqProto::StartSession()
+{
+ ptrA szDeviceId(getStringA("DeviceId"));
+ if (szDeviceId == nullptr) {
+ UUID deviceId;
+ UuidCreate(&deviceId);
+ RPC_CSTR szId;
+ UuidToStringA(&deviceId, &szId);
+ szDeviceId = mir_strdup((char*)szId);
+ setString("DeviceId", szDeviceId);
+ RpcStringFreeA(&szId);
+ }
+
+ int ts = time(0);
+ CMStringA nonce(FORMAT, "%d-2", ts);
+
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/aim/startSession", &CIcqProto::OnStartSession);
+
+ pReq << CHAR_PARAM("a", m_szAToken) << INT_PARAM("activeTimeout", 180) << CHAR_PARAM("assertCaps", CAPS)
+ << INT_PARAM("buildNumber", __BUILD_NUM) << CHAR_PARAM("deviceId", szDeviceId) << CHAR_PARAM("events", EVENTS)
+ << CHAR_PARAM("f", "json") << CHAR_PARAM("imf", "plain") << CHAR_PARAM("inactiveView", "offline")
+ << CHAR_PARAM("includePresenceFields", FIELDS) << CHAR_PARAM("invisible", "false")
+ << CHAR_PARAM("k", ICQ_APP_ID) << INT_PARAM("mobile", 0) << CHAR_PARAM("nonce", nonce) << CHAR_PARAM("r", pReq->m_reqId)
+ << INT_PARAM("rawMsg", 0) << INT_PARAM("sessionTimeout", 7776000) << INT_PARAM("ts", ts) << CHAR_PARAM("view", "online");
+
+ CalcHash(pReq);
+
+ Push(pReq);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+void CIcqProto::OnAddBuddy(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq)
+{
+ JsonReply root(pReply);
+ if (root.error() == 200) {
+ RetrieveUserInfo(pReq->hContact);
+ db_unset(pReq->hContact, "CList", "NotOnList");
+ }
+}
+
+void CIcqProto::OnAddClient(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq)
+{
+ bool *pRet = (bool*)pReq->pUserInfo;
+
+ RobustReply reply(pReply);
+ if (reply.error() != 20000) {
+ *pRet = false;
+ return;
+ }
+
+ const JSONNode &results = reply.results();
+ m_iRClientId = results["clientId"].as_int();
+ setDword(DB_KEY_RCLIENTID, m_iRClientId);
+ *pRet = true;
+}
+
+void CIcqProto::OnCheckPassword(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*)
+{
+ JsonReply root(pReply);
+ switch (root.error()) {
+ case 200:
+ break;
+
+ case 330:
+ case 440:
+ ConnectionFailed(LOGINERR_WRONGPASSWORD);
+ return;
+
+ default:
+ ConnectionFailed(LOGINERR_WRONGPROTOCOL);
+ return;
+ }
+
+ JSONNode &data = root.data();
+ m_szAToken = data["token"]["a"].as_mstring();
+ m_szAToken = ptrA(mir_urlDecode(m_szAToken));
+ setString(DB_KEY_ATOKEN, m_szAToken);
+
+ CMStringA szSessionSecret = data["sessionSecret"].as_mstring();
+ CMStringA szPassTemp = m_szPassword;
+
+ unsigned int len;
+ BYTE hashOut[MIR_SHA256_HASH_SIZE];
+ HMAC(EVP_sha256(), szPassTemp, szPassTemp.GetLength(), (BYTE*)szSessionSecret.c_str(), szSessionSecret.GetLength(), hashOut, &len);
+ m_szSessionKey = ptrA(mir_base64_encode(hashOut, sizeof(hashOut)));
+ setString(DB_KEY_SESSIONKEY, m_szSessionKey);
+
+ CMStringA szUin = data["loginId"].as_mstring();
+ if (szUin)
+ m_dwUin = atoi(szUin);
+
+ StartSession();
+}
+
+void CIcqProto::OnGetUserHistory(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq)
+{
+ RobustReply root(pReply);
+ if (root.error() != 20000)
+ return;
+
+ __int64 lastMsgId = getId(pReq->hContact, DB_KEY_LASTMSGID);
+
+ const JSONNode &results = root.results();
+ for (auto &it : results["messages"])
+ ParseMessage(pReq->hContact, lastMsgId, it);
+
+ setId(pReq->hContact, DB_KEY_LASTMSGID, lastMsgId);
+}
+
+void CIcqProto::OnGetUserInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq)
+{
+ JsonReply root(pReply);
+ if (root.error() != 200) {
+ ProtoBroadcastAck(pReq->hContact, ACKTYPE_GETINFO, ACKRESULT_FAILED, nullptr);
+ return;
+ }
+
+ const JSONNode &data = root.data();
+ for (auto &it : data["users"])
+ ParseBuddyInfo(it, pReq->hContact);
+
+ ProtoBroadcastAck(pReq->hContact, ACKTYPE_GETINFO, ACKRESULT_SUCCESS, nullptr);
+}
+
+void CIcqProto::OnStartSession(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*)
+{
+ JsonReply root(pReply);
+ switch (root.error()) {
+ case 200:
+ break;
+
+ case 401:
+ if (root.detail() == 1002) { // session expired
+ delSetting(DB_KEY_ATOKEN);
+ delSetting(DB_KEY_SESSIONKEY);
+ CheckPassword();
+ }
+ else ConnectionFailed(LOGINERR_WRONGPASSWORD);
+ return;
+
+ default:
+ ConnectionFailed(LOGINERR_WRONGPROTOCOL);
+ return;
+ }
+
+ JSONNode &data = root.data();
+ m_fetchBaseURL = data["fetchBaseURL"].as_mstring();
+ m_aimsid = data["aimsid"].as_mstring();
+
+ OnLoggedIn();
+
+ for (auto &it : data["events"])
+ ProcessEvent(it);
+
+ if (m_hPollThread == nullptr)
+ m_hPollThread = ForkThreadEx(&CIcqProto::PollThread, 0, 0);
+}
+
+void CIcqProto::OnReceiveAvatar(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq)
+{
+ PROTO_AVATAR_INFORMATION ai = {};
+ ai.hContact = pReq->hContact;
+
+ if (pReply->resultCode != 200 || pReply->pData == nullptr) {
+LBL_Error:
+ ProtoBroadcastAck(pReq->hContact, ACKTYPE_AVATAR, ACKRESULT_FAILED, HANDLE(&ai), 0);
+ return;
+ }
+
+ const wchar_t *pwszExtension;
+ ai.format = ProtoGetBufferFormat(pReply->pData, &pwszExtension);
+ setByte(pReq->hContact, "AvatarType", ai.format);
+ GetAvatarFileName(pReq->hContact, ai.filename, _countof(ai.filename));
+
+ FILE *out = _wfopen(ai.filename, L"wb");
+ if (out == nullptr)
+ goto LBL_Error;
+
+ fwrite(pReply->pData, pReply->dataLength, 1, out);
+ fclose(out);
+
+ if (pReq->hContact != 0) {
+ ProtoBroadcastAck(pReq->hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, HANDLE(&ai), 0);
+ debugLogW(L"Broadcast new avatar: %s", ai.filename);
+ }
+ else CallService(MS_AV_REPORTMYAVATARCHANGED, (WPARAM)m_szModuleName, 0);
+}
+
+void CIcqProto::OnSearchResults(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq)
+{
+ RobustReply root(pReply);
+ if (root.error() != 20000) {
+ ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_FAILED, (HANDLE)pReq, 0);
+ return;
+ }
+
+ const JSONNode &results = root.results();
+
+ PROTOSEARCHRESULT psr = {};
+ psr.cbSize = sizeof(psr);
+ psr.flags = PSR_UNICODE;
+ for (auto &it : results["data"]) {
+ const JSONNode &anketa = it["anketa"];
+
+ CMStringW wszId = it["sn"].as_mstring();
+ CMStringW wszNick = anketa["nickname"].as_mstring();
+ CMStringW wszFirst = anketa["firstName"].as_mstring();
+ CMStringW wszLast = anketa["lastName"].as_mstring();
+
+ psr.id.w = wszId.GetBuffer();
+ psr.nick.w = wszNick.GetBuffer();
+ psr.firstName.w = wszFirst.GetBuffer();
+ psr.lastName.w = wszLast.GetBuffer();
+ ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_DATA, (HANDLE)pReq, LPARAM(&psr));
+ }
+
+ ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)pReq);
+}
+
+void CIcqProto::OnSendMessage(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq)
+{
+ IcqOwnMessage *ownMsg = (IcqOwnMessage*)pReq->pUserInfo;
+
+ JsonReply root(pReply);
+ if (root.error() != 200) {
+ ProtoBroadcastAck(ownMsg->m_hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, (HANDLE)ownMsg->m_msgid, 0);
+ for (auto &it : m_arOwnIds) {
+ if (it == ownMsg) {
+ m_arOwnIds.remove(m_arOwnIds.indexOf(&it));
+ break;
+ }
+ }
+ }
+
+ const JSONNode &data = root.data();
+ CMStringA reqId(root.requestId());
+ CMStringA msgId(data["histMsgId"].as_mstring());
+ CheckOwnMessage(reqId, msgId, false);
+ CheckLastId(ownMsg->m_hContact, data);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+void CIcqProto::ProcessBuddyList(const JSONNode &ev)
+{
+ bool bEnableMenu = false;
+
+ for (auto &it : ev["groups"]) {
+ CMStringW szGroup = it["name"].as_mstring();
+ bool bCreated = false;
+
+ for (auto &buddy : it["buddies"]) {
+ MCONTACT hContact = ParseBuddyInfo(buddy);
+ if (hContact == INVALID_CONTACT_ID)
+ continue;
+
+ setWString(hContact, "IcqGroup", szGroup);
+
+ CMStringW mirGroup(db_get_sm(hContact, "CList", "Group"));
+ if (mirGroup != szGroup)
+ bEnableMenu = true;
+
+ if (mirGroup.IsEmpty()) {
+ if (!bCreated) {
+ Clist_GroupCreate(0, szGroup);
+ bCreated = true;
+ }
+
+ db_set_ws(hContact, "CList", "Group", szGroup);
+ }
+ }
+ }
+
+ if (bEnableMenu)
+ Menu_ShowItem(m_hUploadGroups, true);
+
+ for (auto &it : m_arCache)
+ if (!it->m_bInList)
+ db_set_b(it->m_hContact, "CList", "NotOnList", 1);
+}
+
+void CIcqProto::ProcessDiff(const JSONNode &ev)
+{
+ for (auto &block : ev) {
+ CMStringW szType = block["type"].as_mstring();
+ if (szType != "updated" && szType != "created")
+ continue;
+
+ for (auto &it : block["data"]) {
+ CMStringW szGroup = it["name"].as_mstring();
+ bool bCreated = false;
+
+ for (auto &buddy : it["buddies"]) {
+ MCONTACT hContact = ParseBuddyInfo(buddy);
+ if (hContact == INVALID_CONTACT_ID)
+ continue;
+
+ setWString(hContact, "IcqGroup", szGroup);
+
+ if (db_get_sm(hContact, "CList", "Group").IsEmpty()) {
+ if (!bCreated) {
+ Clist_GroupCreate(0, szGroup);
+ bCreated = true;
+ }
+
+ db_set_ws(hContact, "CList", "Group", szGroup);
+ }
+ }
+ }
+ }
+}
+
+void CIcqProto::ProcessEvent(const JSONNode &ev)
+{
+ const JSONNode &pData = ev["eventData"];
+ CMStringW szType = ev["type"].as_mstring();
+ if (szType == L"buddylist")
+ ProcessBuddyList(pData);
+ else if (szType == L"diff")
+ ProcessDiff(pData);
+ else if (szType == L"histDlgState")
+ ProcessHistData(pData);
+ else if (szType == L"imState")
+ ProcessImState(pData);
+ else if (szType == L"mchat")
+ ProcessGroupChat(pData);
+ else if (szType == L"myInfo")
+ ProcessMyInfo(pData);
+ else if (szType == L"presence")
+ ProcessPresence(pData);
+ else if (szType == L"typing")
+ ProcessTyping(pData);
+}
+
+void CIcqProto::ProcessHistData(const JSONNode &ev)
+{
+ MCONTACT hContact;
+
+ CMStringW wszId(ev["sn"].as_mstring());
+ if (IsChat(wszId)) {
+ SESSION_INFO *si = g_chatApi.SM_FindSession(wszId, m_szModuleName);
+ if (si == nullptr)
+ return;
+
+ hContact = si->hContact;
+
+ if (si->arUsers.getCount() == 0) {
+ __int64 srvInfoVer = _wtoi64(ev["mchatState"]["infoVersion"].as_mstring());
+ __int64 srvMembersVer = _wtoi64(ev["mchatState"]["membersVersion"].as_mstring());
+ if (srvInfoVer != getId(hContact, "InfoVersion") || srvMembersVer != getId(hContact, "MembersVersion")) {
+ auto *pReq = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER, &CIcqProto::OnGetChatInfo);
+ JSONNode request, params; params.set_name("params");
+ params << WCHAR_PARAM("sn", wszId) << INT_PARAM("memberLimit", 100) << CHAR_PARAM("aimSid", m_aimsid);
+ request << CHAR_PARAM("method", "getChatInfo") << CHAR_PARAM("reqId", pReq->m_reqId) << CHAR_PARAM("authToken", m_szRToken) << INT_PARAM("clientId", m_iRClientId) << params;
+ pReq->m_szParam = ptrW(json_write(&request));
+ pReq->pUserInfo = si;
+ Push(pReq);
+ }
+ else LoadChatInfo(si);
+ }
+ }
+ else hContact = CreateContact(_wtol(wszId), true);
+
+ __int64 lastMsgId = getId(hContact, DB_KEY_LASTMSGID);
+ __int64 srvLastId = _wtoi64(ev["lastMsgId"].as_mstring());
+ __int64 srvUnreadId = _wtoi64(ev["yours"]["lastRead"].as_mstring());
+
+ // on first start we don't load history not to create dups
+ if (lastMsgId == 0)
+ setId(hContact, DB_KEY_LASTMSGID, srvLastId);
+ // or load missing messages if any
+ else if (ev["unreadCnt"].as_int() > 0)
+ RetrieveUserHistory(hContact, min(srvUnreadId, lastMsgId), srvLastId);
+
+ for (auto &it : ev["tail"]["messages"])
+ ParseMessage(hContact, lastMsgId, it);
+ setId(hContact, DB_KEY_LASTMSGID, lastMsgId);
+}
+
+void CIcqProto::ProcessImState(const JSONNode &ev)
+{
+ for (auto &it : ev["imStates"]) {
+ if (it["state"].as_mstring() != L"sent")
+ continue;
+
+ CMStringA reqId(it["sendReqId"].as_mstring());
+ CMStringA msgId(it["histMsgId"].as_mstring());
+ MCONTACT hContact = CheckOwnMessage(reqId, msgId, false);
+ if (hContact)
+ CheckLastId(hContact, ev);
+ }
+}
+
+void CIcqProto::ProcessMyInfo(const JSONNode &ev)
+{
+ CheckNickChange(0, ev);
+ CheckAvatarChange(0, ev);
+}
+
+void CIcqProto::ProcessPresence(const JSONNode &ev)
+{
+ DWORD dwUin = _wtol(ev["aimId"].as_mstring());
+
+ IcqCacheItem *pCache = FindContactByUIN(dwUin);
+ if (pCache) {
+ setDword(pCache->m_hContact, "Status", StatusFromString(ev["state"].as_mstring()));
+
+ CheckNickChange(pCache->m_hContact, ev);
+ CheckAvatarChange(pCache->m_hContact, ev);
+ }
+}
+
+void CIcqProto::ProcessTyping(const JSONNode &ev)
+{
+ DWORD dwUin = _wtol(ev["aimId"].as_mstring());
+ CMStringW wszStatus = ev["typingStatus"].as_mstring();
+
+ IcqCacheItem *pCache = FindContactByUIN(dwUin);
+ if (pCache) {
+ if (wszStatus == "typing")
+ CallService(MS_PROTO_CONTACTISTYPING, pCache->m_hContact, 60);
+ else
+ CallService(MS_PROTO_CONTACTISTYPING, pCache->m_hContact, PROTOTYPE_CONTACTTYPING_OFF);
+ }
+}
+
+void CIcqProto::OnFetchEvents(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*)
+{
+ JsonReply root(pReply);
+ if (root.error() != 200) {
+ ShutdownSession();
+ return;
+ }
+
+ JSONNode &data = root.data();
+ m_fetchBaseURL = data["fetchBaseURL"].as_mstring();
+
+ for (auto &it : data["events"])
+ ProcessEvent(it);
+}
+
+void __cdecl CIcqProto::PollThread(void*)
+{
+ debugLogA("Polling thread started");
+ bool bFirst = true;
+
+ while (m_bOnline) {
+ CMStringA szUrl = m_fetchBaseURL;
+ if (bFirst) {
+ bFirst = false;
+ szUrl.Append("&first=1");
+ }
+ else szUrl.Append("&timeout=25000");
+
+ auto *pReq = new AsyncHttpRequest(CONN_FETCH, REQUEST_GET, szUrl, &CIcqProto::OnFetchEvents);
+ if (!bFirst)
+ pReq->timeout = 62000;
+ ExecuteRequest(pReq);
+ }
+
+ debugLogA("Polling thread ended");
+ m_hPollThread = nullptr;
+}