From 1979fd80424d16b2e489f9b57d01d9c7811d25a2 Mon Sep 17 00:00:00 2001 From: dartraiden Date: Mon, 2 Jan 2023 21:10:29 +0300 Subject: Update copyrights --- protocols/ICQ-WIM/src/groupchats.cpp | 584 ++++---- protocols/ICQ-WIM/src/http.cpp | 770 +++++------ protocols/ICQ-WIM/src/ignore.cpp | 164 +-- protocols/ICQ-WIM/src/main.cpp | 204 +-- protocols/ICQ-WIM/src/mra.cpp | 282 ++-- protocols/ICQ-WIM/src/options.cpp | 788 +++++------ protocols/ICQ-WIM/src/poll.cpp | 818 ++++++------ protocols/ICQ-WIM/src/proto.cpp | 1356 +++++++++---------- protocols/ICQ-WIM/src/proto.h | 988 +++++++------- protocols/ICQ-WIM/src/server.cpp | 2440 +++++++++++++++++----------------- protocols/ICQ-WIM/src/stdafx.cxx | 36 +- protocols/ICQ-WIM/src/stdafx.h | 218 +-- protocols/ICQ-WIM/src/userinfo.cpp | 128 +- protocols/ICQ-WIM/src/utils.cpp | 792 +++++------ protocols/ICQ-WIM/src/version.h | 2 +- 15 files changed, 4785 insertions(+), 4785 deletions(-) (limited to 'protocols/ICQ-WIM/src') diff --git a/protocols/ICQ-WIM/src/groupchats.cpp b/protocols/ICQ-WIM/src/groupchats.cpp index e92811baa1..807bec394a 100644 --- a/protocols/ICQ-WIM/src/groupchats.cpp +++ b/protocols/ICQ-WIM/src/groupchats.cpp @@ -1,292 +1,292 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-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, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- - -#include "stdafx.h" - -void CIcqProto::LoadChatInfo(SESSION_INFO *si) -{ - int memberCount = getDword(si->hContact, "MemberCount"); - for (int i = 0; i < memberCount; i++) { - char buf[100]; - mir_snprintf(buf, "m%d", i); - ptrW szSetting(getWStringA(si->hContact, buf)); - JSONNode *node = json_parse(T2Utf(szSetting)); - if (node == nullptr) - continue; - - CMStringW nick((*node)["nick"].as_mstring()); - CMStringW role((*node)["role"].as_mstring()); - CMStringW sn((*node)["sn"].as_mstring()); - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_JOIN }; - gce.dwFlags = GCEF_SILENT; - gce.pszID.w = si->ptszID; - gce.pszNick.w = nick; - gce.pszUID.w = sn; - gce.time = ::time(0); - gce.bIsMe = sn == m_szOwnId; - gce.pszStatus.w = TranslateW(role); - Chat_Event(&gce); - - json_delete(node); - } -} - -void CIcqProto::OnGetChatInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - SESSION_INFO *si = (SESSION_INFO*)pReq->pUserInfo; - - RobustReply root(pReply); - if (root.error() != 20000) - return; - - int n = 0; - char buf[100]; - const JSONNode &results = root.results(); - for (auto &it : results["members"]) { - mir_snprintf(buf, "m%d", n++); - - CMStringW friendly = it["friendly"].as_mstring(); - CMStringW role = it["role"].as_mstring(); - CMStringW sn = it["sn"].as_mstring(); - - JSONNode member; - member << WCHAR_PARAM("nick", friendly) << WCHAR_PARAM("role", role) << WCHAR_PARAM("sn", sn); - ptrW text(json_write(&member)); - setWString(si->hContact, buf, text); - } - - setDword(si->hContact, "MemberCount", n); - setId(si->hContact, "InfoVersion", _wtoi64(results["infoVersion"].as_mstring())); - setId(si->hContact, "MembersVersion", _wtoi64(results["membersVersion"].as_mstring())); - - LoadChatInfo(si); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Invitation dialog - -class CGroupchatInviteDlg : public CIcqDlgBase -{ - CCtrlClc m_clc; - SESSION_INFO *m_si; - - void FilterList(CCtrlClc*) - { - for (auto &hContact : Contacts()) { - char *proto = Proto_GetBaseAccountName(hContact); - if (mir_strcmp(proto, m_proto->m_szModuleName) || m_proto->isChatRoom(hContact)) - if (HANDLE hItem = m_clc.FindContact(hContact)) - m_clc.DeleteItem(hItem); - } - } - - void ResetListOptions(CCtrlClc*) - { - m_clc.SetHideEmptyGroups(1); - m_clc.SetHideOfflineRoot(1); - } - -public: - CGroupchatInviteDlg(CIcqProto *ppro, SESSION_INFO *si) : - CIcqDlgBase(ppro, IDD_GROUPCHAT_INVITE), - m_si(si), - m_clc(this, IDC_CLIST) - { - m_clc.OnNewContact = - m_clc.OnListRebuilt = Callback(this, &CGroupchatInviteDlg::FilterList); - m_clc.OnOptionsChanged = Callback(this, &CGroupchatInviteDlg::ResetListOptions); - } - - bool OnInitDialog() override - { - SetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE, - GetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE) | CLS_SHOWHIDDEN | CLS_HIDEOFFLINE | CLS_CHECKBOXES | CLS_HIDEEMPTYGROUPS | CLS_USEGROUPS | CLS_GREYALTERNATE | CLS_GROUPCHECKBOXES); - m_clc.SendMsg(CLM_SETEXSTYLE, CLS_EX_DISABLEDRAGDROP | CLS_EX_TRACKSELECT, 0); - - ResetListOptions(&m_clc); - FilterList(&m_clc); - return true; - } - - bool OnApply() override - { - CMStringW szMembers; - for (auto &hContact : m_proto->AccContacts()) { - if (m_proto->isChatRoom(hContact)) - continue; - - if (HANDLE hItem = m_clc.FindContact(hContact)) { - if (m_clc.GetCheck(hItem)) { - if (!szMembers.IsEmpty()) - szMembers.AppendChar(','); - szMembers.Append(m_proto->GetUserId(hContact)); - } - } - } - - m_proto->Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/mchat/AddChat") - << AIMSID(m_proto) << WCHAR_PARAM("chat_id", m_si->ptszID) << WCHAR_PARAM("members", szMembers)); - return true; - } -}; - -void CIcqProto::InviteUserToChat(SESSION_INFO *si) -{ - CGroupchatInviteDlg dlg(this, si); - if (si->pDlg) - dlg.SetParent(((CDlgBase*)si->pDlg)->GetHwnd()); - dlg.DoModal(); -} - -void CIcqProto::LeaveDestroyChat(SESSION_INFO *si) -{ - Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/hideChat") - << AIMSID(this) << WCHAR_PARAM("buddy", si->ptszID) << INT64_PARAM("lastMsgId", getId(si->hContact, DB_KEY_LASTMSGID))); - - Chat_Terminate(si->pszModule, si->ptszID, true); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Group chats - -static gc_item sttLogListItems[] = -{ - { LPGENW("&Invite a user"), IDM_INVITE, MENU_ITEM }, - { nullptr, 0, MENU_SEPARATOR }, - { LPGENW("&Leave/destroy chat"), IDM_LEAVE, MENU_ITEM } -}; - -int CIcqProto::GroupchatMenuHook(WPARAM, LPARAM lParam) -{ - GCMENUITEMS *gcmi = (GCMENUITEMS*)lParam; - if (gcmi == nullptr) - return 0; - - if (mir_strcmpi(gcmi->pszModule, m_szModuleName)) - return 0; - - SESSION_INFO *si = g_chatApi.SM_FindSession(gcmi->pszID, gcmi->pszModule); - if (si == nullptr) - return 0; - - if (gcmi->Type == MENU_ON_LOG) - Chat_AddMenuItems(gcmi->hMenu, _countof(sttLogListItems), sttLogListItems, &g_plugin); - - return 0; -} - -int CIcqProto::GroupchatEventHook(WPARAM, LPARAM lParam) -{ - GCHOOK *gch = (GCHOOK*)lParam; - if (gch == nullptr) - return 0; - - if (mir_strcmpi(gch->si->pszModule, m_szModuleName)) - return 0; - - SESSION_INFO *si = g_chatApi.SM_FindSession(gch->si->ptszID, gch->si->pszModule); - if (si == nullptr) - return 1; - - switch (gch->iType) { - case GC_USER_MESSAGE: - rtrimw(gch->ptszText); - if (!mir_wstrlen(gch->ptszText)) - break; - - if (m_bOnline) { - wchar_t *wszText = NEWWSTR_ALLOCA(gch->ptszText); - Chat_UnescapeTags(wszText); - SendMsg(si->hContact, 0, T2Utf(wszText)); - } - break; - - case GC_USER_PRIVMESS: - Chat_SendPrivateMessage(gch); - break; - - case GC_USER_LOGMENU: - Chat_ProcessLogMenu(si, gch->dwData); - break; - } - - return 1; -} - -void CIcqProto::Chat_ProcessLogMenu(SESSION_INFO *si, int iChoice) -{ - switch (iChoice) { - case IDM_INVITE: - InviteUserToChat(si); - break; - - case IDM_LEAVE: - LeaveDestroyChat(si); - break; - } -} - -void CIcqProto::Chat_SendPrivateMessage(GCHOOK *gch) -{ - MCONTACT hContact; - auto *pCache = FindContactByUIN(gch->ptszUID); - if (pCache == nullptr) { - hContact = CreateContact(gch->ptszUID, true); - setWString(hContact, "Nick", gch->ptszNick); - Contact::Hide(hContact); - db_set_dw(hContact, "Ignore", "Mask1", 0); - } - else hContact = pCache->m_hContact; - - CallService(MS_MSG_SENDMESSAGE, hContact, 0); -} - -void CIcqProto::ProcessGroupChat(const JSONNode &ev) -{ - for (auto &it : ev["mchats"]) { - CMStringW wszId(it["sender"].as_mstring()); - SESSION_INFO *si = g_chatApi.SM_FindSession(wszId, m_szModuleName); - if (si == nullptr) - continue; - - CMStringW method(it["method"].as_mstring()); - GCEVENT gce = { m_szModuleName, 0, (method == "add_members") ? GC_EVENT_JOIN : GC_EVENT_PART }; - gce.pszID.w = si->ptszID; - - int iStart = 0; - CMStringW members(it["members"].as_mstring()); - while (true) { - CMStringW member = members.Tokenize(L",", iStart); - if (member.IsEmpty()) - break; - - auto *pCache = FindContactByUIN(member); - if (pCache == nullptr) - continue; - - gce.pszNick.w = Clist_GetContactDisplayName(pCache->m_hContact); - gce.pszUID.w = member; - gce.time = ::time(0); - gce.bIsMe = member == m_szOwnId; - Chat_Event(&gce); - } - } -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 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" + +void CIcqProto::LoadChatInfo(SESSION_INFO *si) +{ + int memberCount = getDword(si->hContact, "MemberCount"); + for (int i = 0; i < memberCount; i++) { + char buf[100]; + mir_snprintf(buf, "m%d", i); + ptrW szSetting(getWStringA(si->hContact, buf)); + JSONNode *node = json_parse(T2Utf(szSetting)); + if (node == nullptr) + continue; + + CMStringW nick((*node)["nick"].as_mstring()); + CMStringW role((*node)["role"].as_mstring()); + CMStringW sn((*node)["sn"].as_mstring()); + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_JOIN }; + gce.dwFlags = GCEF_SILENT; + gce.pszID.w = si->ptszID; + gce.pszNick.w = nick; + gce.pszUID.w = sn; + gce.time = ::time(0); + gce.bIsMe = sn == m_szOwnId; + gce.pszStatus.w = TranslateW(role); + Chat_Event(&gce); + + json_delete(node); + } +} + +void CIcqProto::OnGetChatInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + SESSION_INFO *si = (SESSION_INFO*)pReq->pUserInfo; + + RobustReply root(pReply); + if (root.error() != 20000) + return; + + int n = 0; + char buf[100]; + const JSONNode &results = root.results(); + for (auto &it : results["members"]) { + mir_snprintf(buf, "m%d", n++); + + CMStringW friendly = it["friendly"].as_mstring(); + CMStringW role = it["role"].as_mstring(); + CMStringW sn = it["sn"].as_mstring(); + + JSONNode member; + member << WCHAR_PARAM("nick", friendly) << WCHAR_PARAM("role", role) << WCHAR_PARAM("sn", sn); + ptrW text(json_write(&member)); + setWString(si->hContact, buf, text); + } + + setDword(si->hContact, "MemberCount", n); + setId(si->hContact, "InfoVersion", _wtoi64(results["infoVersion"].as_mstring())); + setId(si->hContact, "MembersVersion", _wtoi64(results["membersVersion"].as_mstring())); + + LoadChatInfo(si); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Invitation dialog + +class CGroupchatInviteDlg : public CIcqDlgBase +{ + CCtrlClc m_clc; + SESSION_INFO *m_si; + + void FilterList(CCtrlClc*) + { + for (auto &hContact : Contacts()) { + char *proto = Proto_GetBaseAccountName(hContact); + if (mir_strcmp(proto, m_proto->m_szModuleName) || m_proto->isChatRoom(hContact)) + if (HANDLE hItem = m_clc.FindContact(hContact)) + m_clc.DeleteItem(hItem); + } + } + + void ResetListOptions(CCtrlClc*) + { + m_clc.SetHideEmptyGroups(1); + m_clc.SetHideOfflineRoot(1); + } + +public: + CGroupchatInviteDlg(CIcqProto *ppro, SESSION_INFO *si) : + CIcqDlgBase(ppro, IDD_GROUPCHAT_INVITE), + m_si(si), + m_clc(this, IDC_CLIST) + { + m_clc.OnNewContact = + m_clc.OnListRebuilt = Callback(this, &CGroupchatInviteDlg::FilterList); + m_clc.OnOptionsChanged = Callback(this, &CGroupchatInviteDlg::ResetListOptions); + } + + bool OnInitDialog() override + { + SetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE, + GetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE) | CLS_SHOWHIDDEN | CLS_HIDEOFFLINE | CLS_CHECKBOXES | CLS_HIDEEMPTYGROUPS | CLS_USEGROUPS | CLS_GREYALTERNATE | CLS_GROUPCHECKBOXES); + m_clc.SendMsg(CLM_SETEXSTYLE, CLS_EX_DISABLEDRAGDROP | CLS_EX_TRACKSELECT, 0); + + ResetListOptions(&m_clc); + FilterList(&m_clc); + return true; + } + + bool OnApply() override + { + CMStringW szMembers; + for (auto &hContact : m_proto->AccContacts()) { + if (m_proto->isChatRoom(hContact)) + continue; + + if (HANDLE hItem = m_clc.FindContact(hContact)) { + if (m_clc.GetCheck(hItem)) { + if (!szMembers.IsEmpty()) + szMembers.AppendChar(','); + szMembers.Append(m_proto->GetUserId(hContact)); + } + } + } + + m_proto->Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/mchat/AddChat") + << AIMSID(m_proto) << WCHAR_PARAM("chat_id", m_si->ptszID) << WCHAR_PARAM("members", szMembers)); + return true; + } +}; + +void CIcqProto::InviteUserToChat(SESSION_INFO *si) +{ + CGroupchatInviteDlg dlg(this, si); + if (si->pDlg) + dlg.SetParent(((CDlgBase*)si->pDlg)->GetHwnd()); + dlg.DoModal(); +} + +void CIcqProto::LeaveDestroyChat(SESSION_INFO *si) +{ + Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/hideChat") + << AIMSID(this) << WCHAR_PARAM("buddy", si->ptszID) << INT64_PARAM("lastMsgId", getId(si->hContact, DB_KEY_LASTMSGID))); + + Chat_Terminate(si->pszModule, si->ptszID, true); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Group chats + +static gc_item sttLogListItems[] = +{ + { LPGENW("&Invite a user"), IDM_INVITE, MENU_ITEM }, + { nullptr, 0, MENU_SEPARATOR }, + { LPGENW("&Leave/destroy chat"), IDM_LEAVE, MENU_ITEM } +}; + +int CIcqProto::GroupchatMenuHook(WPARAM, LPARAM lParam) +{ + GCMENUITEMS *gcmi = (GCMENUITEMS*)lParam; + if (gcmi == nullptr) + return 0; + + if (mir_strcmpi(gcmi->pszModule, m_szModuleName)) + return 0; + + SESSION_INFO *si = g_chatApi.SM_FindSession(gcmi->pszID, gcmi->pszModule); + if (si == nullptr) + return 0; + + if (gcmi->Type == MENU_ON_LOG) + Chat_AddMenuItems(gcmi->hMenu, _countof(sttLogListItems), sttLogListItems, &g_plugin); + + return 0; +} + +int CIcqProto::GroupchatEventHook(WPARAM, LPARAM lParam) +{ + GCHOOK *gch = (GCHOOK*)lParam; + if (gch == nullptr) + return 0; + + if (mir_strcmpi(gch->si->pszModule, m_szModuleName)) + return 0; + + SESSION_INFO *si = g_chatApi.SM_FindSession(gch->si->ptszID, gch->si->pszModule); + if (si == nullptr) + return 1; + + switch (gch->iType) { + case GC_USER_MESSAGE: + rtrimw(gch->ptszText); + if (!mir_wstrlen(gch->ptszText)) + break; + + if (m_bOnline) { + wchar_t *wszText = NEWWSTR_ALLOCA(gch->ptszText); + Chat_UnescapeTags(wszText); + SendMsg(si->hContact, 0, T2Utf(wszText)); + } + break; + + case GC_USER_PRIVMESS: + Chat_SendPrivateMessage(gch); + break; + + case GC_USER_LOGMENU: + Chat_ProcessLogMenu(si, gch->dwData); + break; + } + + return 1; +} + +void CIcqProto::Chat_ProcessLogMenu(SESSION_INFO *si, int iChoice) +{ + switch (iChoice) { + case IDM_INVITE: + InviteUserToChat(si); + break; + + case IDM_LEAVE: + LeaveDestroyChat(si); + break; + } +} + +void CIcqProto::Chat_SendPrivateMessage(GCHOOK *gch) +{ + MCONTACT hContact; + auto *pCache = FindContactByUIN(gch->ptszUID); + if (pCache == nullptr) { + hContact = CreateContact(gch->ptszUID, true); + setWString(hContact, "Nick", gch->ptszNick); + Contact::Hide(hContact); + db_set_dw(hContact, "Ignore", "Mask1", 0); + } + else hContact = pCache->m_hContact; + + CallService(MS_MSG_SENDMESSAGE, hContact, 0); +} + +void CIcqProto::ProcessGroupChat(const JSONNode &ev) +{ + for (auto &it : ev["mchats"]) { + CMStringW wszId(it["sender"].as_mstring()); + SESSION_INFO *si = g_chatApi.SM_FindSession(wszId, m_szModuleName); + if (si == nullptr) + continue; + + CMStringW method(it["method"].as_mstring()); + GCEVENT gce = { m_szModuleName, 0, (method == "add_members") ? GC_EVENT_JOIN : GC_EVENT_PART }; + gce.pszID.w = si->ptszID; + + int iStart = 0; + CMStringW members(it["members"].as_mstring()); + while (true) { + CMStringW member = members.Tokenize(L",", iStart); + if (member.IsEmpty()) + break; + + auto *pCache = FindContactByUIN(member); + if (pCache == nullptr) + continue; + + gce.pszNick.w = Clist_GetContactDisplayName(pCache->m_hContact); + gce.pszUID.w = member; + gce.time = ::time(0); + gce.bIsMe = member == m_szOwnId; + Chat_Event(&gce); + } + } +} diff --git a/protocols/ICQ-WIM/src/http.cpp b/protocols/ICQ-WIM/src/http.cpp index 0d15a71b9f..79910dad11 100644 --- a/protocols/ICQ-WIM/src/http.cpp +++ b/protocols/ICQ-WIM/src/http.cpp @@ -1,385 +1,385 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-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, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- - -#include "stdafx.h" - -#pragma comment(lib, "Rpcrt4.lib") - -void CIcqProto::DropQueue() -{ - mir_cslock lck(m_csHttpQueue); - - while (m_arHttpQueue.getCount()) { - auto *pReq = m_arHttpQueue[0]; - m_arHttpQueue.remove(0); - delete pReq; - } -} - -bool CIcqProto::IsQueueEmpty() -{ - mir_cslock lck(m_csHttpQueue); - return m_arHttpQueue.getCount() == 0; -} - -void __cdecl CIcqProto::ServerThread(void*) -{ - memset(&m_ConnPool, 0, sizeof(m_ConnPool)); - m_bTerminated = false; - - debugLogA("CIcqProto::WorkerThread: %s", "entering"); - - while (true) { - WaitForSingleObject(m_evRequestsQueue, 1000); - if (m_bTerminated) - break; - - while (true) { - bool bNeedSleep = false; - AsyncHttpRequest *pReq; - { - mir_cslock lck(m_csHttpQueue); - if (m_arHttpQueue.getCount() == 0) - break; - - pReq = m_arHttpQueue[0]; - m_arHttpQueue.remove(0); - bNeedSleep = (m_arHttpQueue.getCount() > 1); - } - if (m_bTerminated) - break; - - ExecuteRequest(pReq); - if (bNeedSleep) - Sleep(200); - } - - int ts = time(0); - for (auto &it : m_ConnPool) { - int idx = int(&it - m_ConnPool); - if (idx == CONN_FETCH) - continue; - - if (it.s && it.lastTs + it.timeout < ts) { - debugLogA("Socket #1 (%p) expired", idx, it.s); - Netlib_CloseHandle(it.s); - it.s = nullptr; - it.lastTs = 0; - } - } - } - - m_hWorkerThread = nullptr; - for (auto &it : m_ConnPool) { - if (it.s) - Netlib_CloseHandle(it.s); - it.s = nullptr; - it.lastTs = it.timeout = 0; - } - - debugLogA("CIcqProto::WorkerThread: %s", "leaving"); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -AsyncRapiRequest::AsyncRapiRequest(CIcqProto *ppro, const char *pszMethod, MTHttpRequestHandler pFunc) : - AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER, pFunc) -{ - params.set_name("params"); - - if (ppro->getByte(DB_KEY_PHONEREG)) { - m_szUrl.AppendChar('/'); - m_szUrl.Append(pszMethod); - - AddHeader("Content-Type", "application/json"); - request << CHAR_PARAM("aimsid", ppro->m_aimsid); - } - else request << CHAR_PARAM("method", pszMethod); -} - -void AsyncRapiRequest::OnPush() -{ - request << CHAR_PARAM("reqId", m_reqId) << params; - - m_szParam = ptrW(json_write(&request)); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -AsyncHttpRequest::AsyncHttpRequest(IcqConnection conn, int iType, const char *szUrl, MTHttpRequestHandler pFunc) : - m_conn(conn) -{ - flags = NLHRF_HTTP11 | NLHRF_SSL | NLHRF_DUMPASTEXT; - requestType = iType; - m_szUrl = szUrl; - m_pFunc = pFunc; - timeout = 10000; - - GUID packetId; - UuidCreate(&packetId); - - RPC_CSTR szId; - UuidToStringA(&packetId, &szId); - strncpy_s(m_reqId, (char*)szId, _TRUNCATE); - RpcStringFreeA(&szId); - - if (iType == REQUEST_POST) { - AddHeader("Content-Type", "application/x-www-form-urlencoded"); - - dataLength = m_szParam.GetLength(); - pData = m_szParam.Detach(); - } -} - -void AsyncHttpRequest::ReplaceJsonParam(const JSONNode &n) -{ - auto *szNodeName = n.name(); - - JSONNode root = JSONNode::parse(m_szParam); - JSONNode& old = root.at(szNodeName); - if (old) - old = n; - else - root.push_back(n); - m_szParam = root.write().c_str(); - - replaceStr(pData, nullptr); - dataLength = 0; -} - -bool CIcqProto::ExecuteRequest(AsyncHttpRequest *pReq) -{ - CMStringA str; - - pReq->szUrl = pReq->m_szUrl.GetBuffer(); - if (!pReq->m_szParam.IsEmpty()) { - if (pReq->requestType == REQUEST_GET) { - str.Format("%s?%s", pReq->m_szUrl.c_str(), pReq->m_szParam.c_str()); - pReq->szUrl = str.GetBuffer(); - } - else { - pReq->dataLength = pReq->m_szParam.GetLength(); - pReq->pData = mir_strdup(pReq->m_szParam); - } - } - - // replace credentials inside JSON body for pure RAPI requests - if (pReq->m_conn == CONN_RAPI && !mir_strcmp(pReq->szUrl, ICQ_ROBUST_SERVER) && !getByte(DB_KEY_PHONEREG)) { - CMStringA szAgent(FORMAT, "%S Mail.ru Windows ICQ (version 10.0.1999)", (wchar_t*)m_szOwnId); - pReq->AddHeader("User-Agent", szAgent); - pReq->AddHeader("Content-Type", "application/json"); - - if (m_szRToken.IsEmpty()) { - if (!RefreshRobustToken(pReq)) { - delete pReq; - return false; - } - } - - if (m_iRClientId) - pReq->ReplaceJsonParam(JSONNode("clientId", m_iRClientId)); - pReq->ReplaceJsonParam(JSONNode("authToken", m_szRToken)); - pReq->dataLength = pReq->m_szParam.GetLength(); - pReq->pData = mir_strdup(pReq->m_szParam); - } - - debugLogA("Executing request %s:\n%s", pReq->m_reqId, pReq->szUrl); - - if (pReq->m_conn != CONN_NONE) { - pReq->flags |= NLHRF_PERSISTENT; - pReq->nlc = m_ConnPool[pReq->m_conn].s; - m_ConnPool[pReq->m_conn].lastTs = time(0); - } - - bool bRet; - NLHR_PTR reply(Netlib_HttpTransaction(m_hNetlibUser, pReq)); - if (reply != nullptr) { - if (pReq->m_conn != CONN_NONE) { - auto &conn = m_ConnPool[pReq->m_conn]; - conn.s = reply->nlc; - conn.timeout = 0; - if (auto *pszHdr = Netlib_GetHeader(reply, "Keep-Alive")) { - int timeout; - if (1 == sscanf(pszHdr, "timeout=%d", &timeout)) - conn.timeout = timeout; - } - } - - if (pReq->m_conn == CONN_RAPI && reply->pData && strstr(reply->pData, "\"code\": 40201")) { - RobustReply r(reply); - if (r.error() == 40201) { // robust token expired - m_szRToken.Empty(); - - // if token refresh succeeded, replace it in the query and push request back - if (!RefreshRobustToken(pReq)) { - delete pReq; - return false; - } - - Push(pReq); - return true; - } - } - - if (pReq->m_pFunc != nullptr) - (this->*(pReq->m_pFunc))(reply, pReq); - - bRet = true; - } - else { - debugLogA("Request %s failed", pReq->m_reqId); - - if (IsStatusConnecting(m_iStatus)) - ConnectionFailed(LOGINERR_NONETWORK); - - if (pReq->m_conn != CONN_NONE) - m_ConnPool[pReq->m_conn].s = nullptr; - - bRet = false; - } - - delete pReq; - return bRet; -} - -void CIcqProto::Push(MHttpRequest *p) -{ - AsyncHttpRequest *pReq = (AsyncHttpRequest*)p; - - pReq->OnPush(); - { - mir_cslock lck(m_csHttpQueue); - m_arHttpQueue.insert(pReq); - } - - SetEvent(m_evRequestsQueue); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -AsyncHttpRequest* operator<<(AsyncHttpRequest *pReq, const AIMSID ¶m) -{ - pReq << CHAR_PARAM("f", "json") << CHAR_PARAM("aimsid", param.m_ppro->m_aimsid) << CHAR_PARAM("r", pReq->m_reqId); - #ifndef _DEBUG - pReq->flags |= NLHRF_NODUMPSEND; - #endif - return pReq; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -MHttpRequest* operator<<(MHttpRequest *pReq, const GROUP_PARAM ¶m) -{ - if (param.wszValue) { - CMStringW tmp(param.wszValue); - tmp.Replace(L"\\", L">"); - tmp.Replace(L"/", L">"); - pReq << WCHAR_PARAM(param.szName, tmp); - } - return pReq; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -JsonReply::JsonReply(NETLIBHTTPREQUEST *pReply) -{ - if (pReply == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = pReply->resultCode; - if (m_errorCode != 200) - return; - - m_root = json_parse(pReply->pData); - if (m_root == nullptr) { - m_errorCode = 500; - return; - } - - JSONNode &response = (*m_root)["response"]; - m_errorCode = response["statusCode"].as_int(); - m_requestId = response["requestId"].as_mstring(); - m_detailCode = response["statusDetailCode"].as_int(); - m_data = &response["data"]; -} - -JsonReply::~JsonReply() -{ - json_delete(m_root); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -FileReply::FileReply(NETLIBHTTPREQUEST *pReply) -{ - if (pReply == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = pReply->resultCode; - if (m_errorCode != 200) - return; - - m_root = json_parse(pReply->pData); - if (m_root == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = (*m_root)["status"].as_int(); - m_data = &(*m_root)["data"]; -} - -FileReply::~FileReply() -{ - json_delete(m_root); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -RobustReply::RobustReply(NETLIBHTTPREQUEST *pReply) -{ - if (pReply == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = pReply->resultCode; - if (m_errorCode != 200) - return; - - m_root = json_parse(pReply->pData); - if (m_root == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = (*m_root)["status"]["code"].as_int(); - m_result = &(*m_root)["result"]; - m_results = &(*m_root)["results"]; -} - -RobustReply::~RobustReply() -{ - json_delete(m_root); -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 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, "Rpcrt4.lib") + +void CIcqProto::DropQueue() +{ + mir_cslock lck(m_csHttpQueue); + + while (m_arHttpQueue.getCount()) { + auto *pReq = m_arHttpQueue[0]; + m_arHttpQueue.remove(0); + delete pReq; + } +} + +bool CIcqProto::IsQueueEmpty() +{ + mir_cslock lck(m_csHttpQueue); + return m_arHttpQueue.getCount() == 0; +} + +void __cdecl CIcqProto::ServerThread(void*) +{ + memset(&m_ConnPool, 0, sizeof(m_ConnPool)); + m_bTerminated = false; + + debugLogA("CIcqProto::WorkerThread: %s", "entering"); + + while (true) { + WaitForSingleObject(m_evRequestsQueue, 1000); + if (m_bTerminated) + break; + + while (true) { + bool bNeedSleep = false; + AsyncHttpRequest *pReq; + { + mir_cslock lck(m_csHttpQueue); + if (m_arHttpQueue.getCount() == 0) + break; + + pReq = m_arHttpQueue[0]; + m_arHttpQueue.remove(0); + bNeedSleep = (m_arHttpQueue.getCount() > 1); + } + if (m_bTerminated) + break; + + ExecuteRequest(pReq); + if (bNeedSleep) + Sleep(200); + } + + int ts = time(0); + for (auto &it : m_ConnPool) { + int idx = int(&it - m_ConnPool); + if (idx == CONN_FETCH) + continue; + + if (it.s && it.lastTs + it.timeout < ts) { + debugLogA("Socket #1 (%p) expired", idx, it.s); + Netlib_CloseHandle(it.s); + it.s = nullptr; + it.lastTs = 0; + } + } + } + + m_hWorkerThread = nullptr; + for (auto &it : m_ConnPool) { + if (it.s) + Netlib_CloseHandle(it.s); + it.s = nullptr; + it.lastTs = it.timeout = 0; + } + + debugLogA("CIcqProto::WorkerThread: %s", "leaving"); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +AsyncRapiRequest::AsyncRapiRequest(CIcqProto *ppro, const char *pszMethod, MTHttpRequestHandler pFunc) : + AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER, pFunc) +{ + params.set_name("params"); + + if (ppro->getByte(DB_KEY_PHONEREG)) { + m_szUrl.AppendChar('/'); + m_szUrl.Append(pszMethod); + + AddHeader("Content-Type", "application/json"); + request << CHAR_PARAM("aimsid", ppro->m_aimsid); + } + else request << CHAR_PARAM("method", pszMethod); +} + +void AsyncRapiRequest::OnPush() +{ + request << CHAR_PARAM("reqId", m_reqId) << params; + + m_szParam = ptrW(json_write(&request)); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +AsyncHttpRequest::AsyncHttpRequest(IcqConnection conn, int iType, const char *szUrl, MTHttpRequestHandler pFunc) : + m_conn(conn) +{ + flags = NLHRF_HTTP11 | NLHRF_SSL | NLHRF_DUMPASTEXT; + requestType = iType; + m_szUrl = szUrl; + m_pFunc = pFunc; + timeout = 10000; + + GUID packetId; + UuidCreate(&packetId); + + RPC_CSTR szId; + UuidToStringA(&packetId, &szId); + strncpy_s(m_reqId, (char*)szId, _TRUNCATE); + RpcStringFreeA(&szId); + + if (iType == REQUEST_POST) { + AddHeader("Content-Type", "application/x-www-form-urlencoded"); + + dataLength = m_szParam.GetLength(); + pData = m_szParam.Detach(); + } +} + +void AsyncHttpRequest::ReplaceJsonParam(const JSONNode &n) +{ + auto *szNodeName = n.name(); + + JSONNode root = JSONNode::parse(m_szParam); + JSONNode& old = root.at(szNodeName); + if (old) + old = n; + else + root.push_back(n); + m_szParam = root.write().c_str(); + + replaceStr(pData, nullptr); + dataLength = 0; +} + +bool CIcqProto::ExecuteRequest(AsyncHttpRequest *pReq) +{ + CMStringA str; + + pReq->szUrl = pReq->m_szUrl.GetBuffer(); + if (!pReq->m_szParam.IsEmpty()) { + if (pReq->requestType == REQUEST_GET) { + str.Format("%s?%s", pReq->m_szUrl.c_str(), pReq->m_szParam.c_str()); + pReq->szUrl = str.GetBuffer(); + } + else { + pReq->dataLength = pReq->m_szParam.GetLength(); + pReq->pData = mir_strdup(pReq->m_szParam); + } + } + + // replace credentials inside JSON body for pure RAPI requests + if (pReq->m_conn == CONN_RAPI && !mir_strcmp(pReq->szUrl, ICQ_ROBUST_SERVER) && !getByte(DB_KEY_PHONEREG)) { + CMStringA szAgent(FORMAT, "%S Mail.ru Windows ICQ (version 10.0.1999)", (wchar_t*)m_szOwnId); + pReq->AddHeader("User-Agent", szAgent); + pReq->AddHeader("Content-Type", "application/json"); + + if (m_szRToken.IsEmpty()) { + if (!RefreshRobustToken(pReq)) { + delete pReq; + return false; + } + } + + if (m_iRClientId) + pReq->ReplaceJsonParam(JSONNode("clientId", m_iRClientId)); + pReq->ReplaceJsonParam(JSONNode("authToken", m_szRToken)); + pReq->dataLength = pReq->m_szParam.GetLength(); + pReq->pData = mir_strdup(pReq->m_szParam); + } + + debugLogA("Executing request %s:\n%s", pReq->m_reqId, pReq->szUrl); + + if (pReq->m_conn != CONN_NONE) { + pReq->flags |= NLHRF_PERSISTENT; + pReq->nlc = m_ConnPool[pReq->m_conn].s; + m_ConnPool[pReq->m_conn].lastTs = time(0); + } + + bool bRet; + NLHR_PTR reply(Netlib_HttpTransaction(m_hNetlibUser, pReq)); + if (reply != nullptr) { + if (pReq->m_conn != CONN_NONE) { + auto &conn = m_ConnPool[pReq->m_conn]; + conn.s = reply->nlc; + conn.timeout = 0; + if (auto *pszHdr = Netlib_GetHeader(reply, "Keep-Alive")) { + int timeout; + if (1 == sscanf(pszHdr, "timeout=%d", &timeout)) + conn.timeout = timeout; + } + } + + if (pReq->m_conn == CONN_RAPI && reply->pData && strstr(reply->pData, "\"code\": 40201")) { + RobustReply r(reply); + if (r.error() == 40201) { // robust token expired + m_szRToken.Empty(); + + // if token refresh succeeded, replace it in the query and push request back + if (!RefreshRobustToken(pReq)) { + delete pReq; + return false; + } + + Push(pReq); + return true; + } + } + + if (pReq->m_pFunc != nullptr) + (this->*(pReq->m_pFunc))(reply, pReq); + + bRet = true; + } + else { + debugLogA("Request %s failed", pReq->m_reqId); + + if (IsStatusConnecting(m_iStatus)) + ConnectionFailed(LOGINERR_NONETWORK); + + if (pReq->m_conn != CONN_NONE) + m_ConnPool[pReq->m_conn].s = nullptr; + + bRet = false; + } + + delete pReq; + return bRet; +} + +void CIcqProto::Push(MHttpRequest *p) +{ + AsyncHttpRequest *pReq = (AsyncHttpRequest*)p; + + pReq->OnPush(); + { + mir_cslock lck(m_csHttpQueue); + m_arHttpQueue.insert(pReq); + } + + SetEvent(m_evRequestsQueue); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +AsyncHttpRequest* operator<<(AsyncHttpRequest *pReq, const AIMSID ¶m) +{ + pReq << CHAR_PARAM("f", "json") << CHAR_PARAM("aimsid", param.m_ppro->m_aimsid) << CHAR_PARAM("r", pReq->m_reqId); + #ifndef _DEBUG + pReq->flags |= NLHRF_NODUMPSEND; + #endif + return pReq; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +MHttpRequest* operator<<(MHttpRequest *pReq, const GROUP_PARAM ¶m) +{ + if (param.wszValue) { + CMStringW tmp(param.wszValue); + tmp.Replace(L"\\", L">"); + tmp.Replace(L"/", L">"); + pReq << WCHAR_PARAM(param.szName, tmp); + } + return pReq; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +JsonReply::JsonReply(NETLIBHTTPREQUEST *pReply) +{ + if (pReply == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = pReply->resultCode; + if (m_errorCode != 200) + return; + + m_root = json_parse(pReply->pData); + if (m_root == nullptr) { + m_errorCode = 500; + return; + } + + JSONNode &response = (*m_root)["response"]; + m_errorCode = response["statusCode"].as_int(); + m_requestId = response["requestId"].as_mstring(); + m_detailCode = response["statusDetailCode"].as_int(); + m_data = &response["data"]; +} + +JsonReply::~JsonReply() +{ + json_delete(m_root); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +FileReply::FileReply(NETLIBHTTPREQUEST *pReply) +{ + if (pReply == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = pReply->resultCode; + if (m_errorCode != 200) + return; + + m_root = json_parse(pReply->pData); + if (m_root == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = (*m_root)["status"].as_int(); + m_data = &(*m_root)["data"]; +} + +FileReply::~FileReply() +{ + json_delete(m_root); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +RobustReply::RobustReply(NETLIBHTTPREQUEST *pReply) +{ + if (pReply == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = pReply->resultCode; + if (m_errorCode != 200) + return; + + m_root = json_parse(pReply->pData); + if (m_root == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = (*m_root)["status"]["code"].as_int(); + m_result = &(*m_root)["result"]; + m_results = &(*m_root)["results"]; +} + +RobustReply::~RobustReply() +{ + json_delete(m_root); +} diff --git a/protocols/ICQ-WIM/src/ignore.cpp b/protocols/ICQ-WIM/src/ignore.cpp index d2d6dfb36a..a2ce8c098f 100644 --- a/protocols/ICQ-WIM/src/ignore.cpp +++ b/protocols/ICQ-WIM/src/ignore.cpp @@ -1,82 +1,82 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-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, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- -// Server permissions - -#include "stdafx.h" - -void CIcqProto::GetPermitDeny() -{ - Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/preference/getPermitDeny", &CIcqProto::OnGetPermitDeny) << AIMSID(this)); -} - -void CIcqProto::OnGetPermitDeny(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) -{ - JsonReply root(pReply); - if (root.error() == 200) - ProcessPermissions(root.data()); -} - -void CIcqProto::ProcessPermissions(const JSONNode &ev) -{ - { mir_cslock lck(m_csCache); - for (auto &it : m_arCache) - it->m_iApparentMode = 0; - } - - for (auto &it : ev["allows"]) { - auto *p = FindContactByUIN(it.as_mstring()); - if (p) - p->m_iApparentMode = ID_STATUS_ONLINE; - } - - m_bIgnoreListEmpty = true; - for (auto &it : ev["ignores"]) { - CMStringW wszId(it.as_mstring()); - auto *p = FindContactByUIN(wszId); - if (p == nullptr) { - CreateContact(wszId, false); - p = FindContactByUIN(wszId); - } - p->m_iApparentMode = ID_STATUS_OFFLINE; - Contact::Hide(p->m_hContact); - m_bIgnoreListEmpty = false; - } - - { mir_cslock lck(m_csCache); - for (auto &it : m_arCache) { - int oldMode = getDword(it->m_hContact, "ApparentMode"); - if (oldMode != it->m_iApparentMode) { - if (it->m_iApparentMode == 0) - delSetting(it->m_hContact, "ApparentMode"); - else - setDword(it->m_hContact, "ApparentMode", it->m_iApparentMode); - } - } - } -} - -void CIcqProto::SetPermitDeny(const CMStringW &userId, bool bAllow) -{ - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/preference/setPermitDeny") - << AIMSID(this) << WCHAR_PARAM((bAllow) ? "pdIgnoreRemove" : "pdIgnore", userId); - if (!m_bIgnoreListEmpty) - pReq << CHAR_PARAM("pdMode", "denySome"); - Push(pReq); -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 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. +// ----------------------------------------------------------------------------- +// Server permissions + +#include "stdafx.h" + +void CIcqProto::GetPermitDeny() +{ + Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/preference/getPermitDeny", &CIcqProto::OnGetPermitDeny) << AIMSID(this)); +} + +void CIcqProto::OnGetPermitDeny(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) +{ + JsonReply root(pReply); + if (root.error() == 200) + ProcessPermissions(root.data()); +} + +void CIcqProto::ProcessPermissions(const JSONNode &ev) +{ + { mir_cslock lck(m_csCache); + for (auto &it : m_arCache) + it->m_iApparentMode = 0; + } + + for (auto &it : ev["allows"]) { + auto *p = FindContactByUIN(it.as_mstring()); + if (p) + p->m_iApparentMode = ID_STATUS_ONLINE; + } + + m_bIgnoreListEmpty = true; + for (auto &it : ev["ignores"]) { + CMStringW wszId(it.as_mstring()); + auto *p = FindContactByUIN(wszId); + if (p == nullptr) { + CreateContact(wszId, false); + p = FindContactByUIN(wszId); + } + p->m_iApparentMode = ID_STATUS_OFFLINE; + Contact::Hide(p->m_hContact); + m_bIgnoreListEmpty = false; + } + + { mir_cslock lck(m_csCache); + for (auto &it : m_arCache) { + int oldMode = getDword(it->m_hContact, "ApparentMode"); + if (oldMode != it->m_iApparentMode) { + if (it->m_iApparentMode == 0) + delSetting(it->m_hContact, "ApparentMode"); + else + setDword(it->m_hContact, "ApparentMode", it->m_iApparentMode); + } + } + } +} + +void CIcqProto::SetPermitDeny(const CMStringW &userId, bool bAllow) +{ + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/preference/setPermitDeny") + << AIMSID(this) << WCHAR_PARAM((bAllow) ? "pdIgnoreRemove" : "pdIgnore", userId); + if (!m_bIgnoreListEmpty) + pReq << CHAR_PARAM("pdMode", "denySome"); + Push(pReq); +} diff --git a/protocols/ICQ-WIM/src/main.cpp b/protocols/ICQ-WIM/src/main.cpp index 68245fff9c..b445bc7b29 100644 --- a/protocols/ICQ-WIM/src/main.cpp +++ b/protocols/ICQ-WIM/src/main.cpp @@ -1,102 +1,102 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-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, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- - -#include "stdafx.h" - -bool g_bSecureIM, g_bMessageState; - -///////////////////////////////////////////////////////////////////////////////////////// - -static PLUGININFOEX pluginInfoEx = { - sizeof(PLUGININFOEX), - __PLUGIN_NAME, - PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), - __DESCRIPTION, - __AUTHOR, - __COPYRIGHT, - __AUTHORWEB, - UNICODE_AWARE, - { 0xEFB2355B, 0x82B3, 0x4759, { 0xb7, 0xd8, 0x95, 0xf8, 0xe9, 0x50, 0x62, 0x91 } } // {EFB2355B-82B3-4759-B7D8-95F8E9506291} -}; - -CMPlugin g_plugin; - -///////////////////////////////////////////////////////////////////////////////////////// - -struct CMPluginMra : public ACCPROTOPLUGIN -{ - CMPluginMra() : ACCPROTOPLUGIN("MRA", pluginInfoEx) - { - SetUniqueId(DB_KEY_ID); - } - - void Register() - { - m_hInst = g_plugin.getInst(); - RegisterProtocol(PROTOTYPE_PROTOCOL, g_plugin.fnInit, g_plugin.fnUninit); - } -} -static g_pluginMra; - -///////////////////////////////////////////////////////////////////////////////////////// - -CMPlugin::CMPlugin() : - ACCPROTOPLUGIN(MODULENAME, pluginInfoEx) -{ - SetUniqueId(DB_KEY_ID); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST }; - -///////////////////////////////////////////////////////////////////////////////////////// - -static int ModuleLoad(WPARAM, LPARAM) -{ - g_bSecureIM = ServiceExists("SecureIM/IsContactSecured"); - g_bMessageState = ServiceExists(MS_MESSAGESTATE_UPDATE); - return 0; -} - -static int OnModulesLoaded(WPARAM, LPARAM) -{ - ModuleLoad(0, 0); - return 0; -} - -static IconItem iconList[] = -{ - { LPGEN("E-mail"), "icq_email", IDI_INBOX }, - { LPGEN("E-mail notification"), "icq_email_notif", IDI_MAIL_NOTIFY } -}; - -int CMPlugin::Load() -{ - // register the second instance of this plugin as MRA - g_pluginMra.Register(); - - registerIcon("Protocols/ICQ", iconList, "ICQ"); - - HookEvent(ME_SYSTEM_MODULELOAD, ModuleLoad); - HookEvent(ME_SYSTEM_MODULEUNLOAD, ModuleLoad); - HookEvent(ME_SYSTEM_MODULESLOADED, OnModulesLoaded); - return 0; -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 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" + +bool g_bSecureIM, g_bMessageState; + +///////////////////////////////////////////////////////////////////////////////////////// + +static PLUGININFOEX pluginInfoEx = { + sizeof(PLUGININFOEX), + __PLUGIN_NAME, + PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), + __DESCRIPTION, + __AUTHOR, + __COPYRIGHT, + __AUTHORWEB, + UNICODE_AWARE, + { 0xEFB2355B, 0x82B3, 0x4759, { 0xb7, 0xd8, 0x95, 0xf8, 0xe9, 0x50, 0x62, 0x91 } } // {EFB2355B-82B3-4759-B7D8-95F8E9506291} +}; + +CMPlugin g_plugin; + +///////////////////////////////////////////////////////////////////////////////////////// + +struct CMPluginMra : public ACCPROTOPLUGIN +{ + CMPluginMra() : ACCPROTOPLUGIN("MRA", pluginInfoEx) + { + SetUniqueId(DB_KEY_ID); + } + + void Register() + { + m_hInst = g_plugin.getInst(); + RegisterProtocol(PROTOTYPE_PROTOCOL, g_plugin.fnInit, g_plugin.fnUninit); + } +} +static g_pluginMra; + +///////////////////////////////////////////////////////////////////////////////////////// + +CMPlugin::CMPlugin() : + ACCPROTOPLUGIN(MODULENAME, pluginInfoEx) +{ + SetUniqueId(DB_KEY_ID); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST }; + +///////////////////////////////////////////////////////////////////////////////////////// + +static int ModuleLoad(WPARAM, LPARAM) +{ + g_bSecureIM = ServiceExists("SecureIM/IsContactSecured"); + g_bMessageState = ServiceExists(MS_MESSAGESTATE_UPDATE); + return 0; +} + +static int OnModulesLoaded(WPARAM, LPARAM) +{ + ModuleLoad(0, 0); + return 0; +} + +static IconItem iconList[] = +{ + { LPGEN("E-mail"), "icq_email", IDI_INBOX }, + { LPGEN("E-mail notification"), "icq_email_notif", IDI_MAIL_NOTIFY } +}; + +int CMPlugin::Load() +{ + // register the second instance of this plugin as MRA + g_pluginMra.Register(); + + registerIcon("Protocols/ICQ", iconList, "ICQ"); + + HookEvent(ME_SYSTEM_MODULELOAD, ModuleLoad); + HookEvent(ME_SYSTEM_MODULEUNLOAD, ModuleLoad); + HookEvent(ME_SYSTEM_MODULESLOADED, OnModulesLoaded); + return 0; +} diff --git a/protocols/ICQ-WIM/src/mra.cpp b/protocols/ICQ-WIM/src/mra.cpp index f5dcaae595..0c1422ddab 100644 --- a/protocols/ICQ-WIM/src/mra.cpp +++ b/protocols/ICQ-WIM/src/mra.cpp @@ -1,141 +1,141 @@ -/* -Copyright (C) 2012-22 Miranda NG team (https://miranda-ng.org) - -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 version 2 -of the License. - -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" - -void CIcqProto::SendMrimLogin(NETLIBHTTPREQUEST *pReply) -{ - if (pReply) { - for (int i=0; i < pReply->headersCount; i++) { - if (!mir_strcmpi(pReply->headers[i].szName, "Set-Cookie")) { - char *p = strchr(pReply->headers[i].szValue, ';'); - if (p) *p = 0; - if (!m_szMraCookie.IsEmpty()) - m_szMraCookie.Append("; "); - - m_szMraCookie.Append(pReply->headers[i].szValue); - } - } - } - - auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, "https://icqapilogin.mail.ru/auth/mrimLogin", &CIcqProto::OnCheckMrimLogin); - pReq->AddHeader("User-Agent", NETLIB_USER_AGENT); - if (!m_szMraCookie.IsEmpty()) - pReq->AddHeader("Cookie", m_szMraCookie); - pReq << CHAR_PARAM("clientName", "webagent") << INT_PARAM("clientVersion", 711) << CHAR_PARAM("devId", MRA_APP_ID) - << CHAR_PARAM("r", "91640-1626423568") << CHAR_PARAM("f", "json"); -#ifndef _DEBUG - pReq->flags |= NLHRF_NODUMPSEND; -#endif - Push(pReq); -} - -void CIcqProto::OnCheckMrimLogin(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) -{ - JsonReply root(pReply); - switch (root.error()) { - case 200: - case 302: - break; - - case 460: // no cookies at all, obtain them via MRA auth site - { - CMStringW uin(m_szOwnId); - - int iStart = 0; - CMStringW wszLogin = uin.Tokenize(L"@", iStart); - CMStringW wszDomain = uin.Tokenize(L"@", iStart); - - auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, "https://auth.mail.ru/cgi-bin/auth?from=splash", &CIcqProto::OnCheckMraAuth); - pReq->AddHeader("User-Agent", NETLIB_USER_AGENT); - if (!m_szMraCookie.IsEmpty()) - pReq->AddHeader("Cookie", m_szMraCookie); - pReq << WCHAR_PARAM("Domain", wszDomain) << WCHAR_PARAM("Login", wszLogin) << CHAR_PARAM("Password", m_szPassword) - << INT_PARAM("new_auth_form", 1) << INT_PARAM("saveauth", 1); -#ifndef _DEBUG - pReq->flags |= NLHRF_NODUMPSEND; -#endif - Push(pReq); - } - return; - - case 462: // bad cookies, refresh them - if (!m_bError462) { - m_bError462 = true; - - auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, "https://auth.mail.ru/sdc?JSONP_call=jscb_tmp_c85825&from=https%3A%2F%2Ficqapilogin.mail.ru%2Fauth%2FmrimLogin", &CIcqProto::OnCheckMraAuthFinal); - pReq->flags |= NLHRF_REDIRECT; - pReq->AddHeader("Cookie", m_szMraCookie); - pReq->AddHeader("Referer", "https://webagent.mail.ru/"); - pReq->AddHeader("User-Agent", NETLIB_USER_AGENT); - Push(pReq); - return; - } - - m_bError462 = false; - __fallthrough; - - default: - ConnectionFailed(LOGINERR_WRONGPROTOCOL, root.error()); - return; - } - - JSONNode &data = root.data(); - m_szAToken = data["token"]["a"].as_mstring(); - mir_urlDecode(m_szAToken.GetBuffer()); - setString(DB_KEY_ATOKEN, m_szAToken); - - m_szSessionKey = data["sessionKey"].as_mstring(); - - CMStringW szUin = data["loginId"].as_mstring(); - if (szUin) - m_szOwnId = szUin; - - int srvTS = data["hostTime"].as_int(); - m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; - - StartSession(); -} - -void CIcqProto::OnCheckMraAuth(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) -{ - JsonReply root(pReply); - switch (root.error()) { - case 200: - case 302: - m_szMraCookie.Empty(); - SendMrimLogin(pReply); - break; - - default: - ConnectionFailed(LOGINERR_WRONGPROTOCOL, root.error()); - } -} - -void CIcqProto::OnCheckMraAuthFinal(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) -{ - switch (pReply->resultCode) { - case 200: - case 302: - // accumulate sdcs cookie and resend request - SendMrimLogin(pReply); - break; - - default: - ConnectionFailed(LOGINERR_WRONGPROTOCOL, 500); - } -} +/* +Copyright (C) 2012-23 Miranda NG team (https://miranda-ng.org) + +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 version 2 +of the License. + +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" + +void CIcqProto::SendMrimLogin(NETLIBHTTPREQUEST *pReply) +{ + if (pReply) { + for (int i=0; i < pReply->headersCount; i++) { + if (!mir_strcmpi(pReply->headers[i].szName, "Set-Cookie")) { + char *p = strchr(pReply->headers[i].szValue, ';'); + if (p) *p = 0; + if (!m_szMraCookie.IsEmpty()) + m_szMraCookie.Append("; "); + + m_szMraCookie.Append(pReply->headers[i].szValue); + } + } + } + + auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, "https://icqapilogin.mail.ru/auth/mrimLogin", &CIcqProto::OnCheckMrimLogin); + pReq->AddHeader("User-Agent", NETLIB_USER_AGENT); + if (!m_szMraCookie.IsEmpty()) + pReq->AddHeader("Cookie", m_szMraCookie); + pReq << CHAR_PARAM("clientName", "webagent") << INT_PARAM("clientVersion", 711) << CHAR_PARAM("devId", MRA_APP_ID) + << CHAR_PARAM("r", "91640-1626423568") << CHAR_PARAM("f", "json"); +#ifndef _DEBUG + pReq->flags |= NLHRF_NODUMPSEND; +#endif + Push(pReq); +} + +void CIcqProto::OnCheckMrimLogin(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) +{ + JsonReply root(pReply); + switch (root.error()) { + case 200: + case 302: + break; + + case 460: // no cookies at all, obtain them via MRA auth site + { + CMStringW uin(m_szOwnId); + + int iStart = 0; + CMStringW wszLogin = uin.Tokenize(L"@", iStart); + CMStringW wszDomain = uin.Tokenize(L"@", iStart); + + auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, "https://auth.mail.ru/cgi-bin/auth?from=splash", &CIcqProto::OnCheckMraAuth); + pReq->AddHeader("User-Agent", NETLIB_USER_AGENT); + if (!m_szMraCookie.IsEmpty()) + pReq->AddHeader("Cookie", m_szMraCookie); + pReq << WCHAR_PARAM("Domain", wszDomain) << WCHAR_PARAM("Login", wszLogin) << CHAR_PARAM("Password", m_szPassword) + << INT_PARAM("new_auth_form", 1) << INT_PARAM("saveauth", 1); +#ifndef _DEBUG + pReq->flags |= NLHRF_NODUMPSEND; +#endif + Push(pReq); + } + return; + + case 462: // bad cookies, refresh them + if (!m_bError462) { + m_bError462 = true; + + auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, "https://auth.mail.ru/sdc?JSONP_call=jscb_tmp_c85825&from=https%3A%2F%2Ficqapilogin.mail.ru%2Fauth%2FmrimLogin", &CIcqProto::OnCheckMraAuthFinal); + pReq->flags |= NLHRF_REDIRECT; + pReq->AddHeader("Cookie", m_szMraCookie); + pReq->AddHeader("Referer", "https://webagent.mail.ru/"); + pReq->AddHeader("User-Agent", NETLIB_USER_AGENT); + Push(pReq); + return; + } + + m_bError462 = false; + __fallthrough; + + default: + ConnectionFailed(LOGINERR_WRONGPROTOCOL, root.error()); + return; + } + + JSONNode &data = root.data(); + m_szAToken = data["token"]["a"].as_mstring(); + mir_urlDecode(m_szAToken.GetBuffer()); + setString(DB_KEY_ATOKEN, m_szAToken); + + m_szSessionKey = data["sessionKey"].as_mstring(); + + CMStringW szUin = data["loginId"].as_mstring(); + if (szUin) + m_szOwnId = szUin; + + int srvTS = data["hostTime"].as_int(); + m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; + + StartSession(); +} + +void CIcqProto::OnCheckMraAuth(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) +{ + JsonReply root(pReply); + switch (root.error()) { + case 200: + case 302: + m_szMraCookie.Empty(); + SendMrimLogin(pReply); + break; + + default: + ConnectionFailed(LOGINERR_WRONGPROTOCOL, root.error()); + } +} + +void CIcqProto::OnCheckMraAuthFinal(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) +{ + switch (pReply->resultCode) { + case 200: + case 302: + // accumulate sdcs cookie and resend request + SendMrimLogin(pReply); + break; + + default: + ConnectionFailed(LOGINERR_WRONGPROTOCOL, 500); + } +} diff --git a/protocols/ICQ-WIM/src/options.cpp b/protocols/ICQ-WIM/src/options.cpp index 7a53219a3d..2de6641efc 100644 --- a/protocols/ICQ-WIM/src/options.cpp +++ b/protocols/ICQ-WIM/src/options.cpp @@ -1,394 +1,394 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-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, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- - -#include "stdafx.h" - -class CIcqEnterLoginDlg : public CIcqDlgBase -{ - CCtrlEdit edtPass; - CCtrlCheck chkSave; - -public: - CIcqEnterLoginDlg(CIcqProto *ppro) : - CIcqDlgBase(ppro, IDD_LOGINPW), - edtPass(this, IDC_PASSWORD), - chkSave(this, IDC_SAVEPASS) - { - } - - bool OnInitDialog() override - { - m_proto->m_bDlgActive = true; - chkSave.SetState(m_proto->getBool("RememberPass")); - Window_SetIcon_IcoLib(m_hwnd, m_proto->m_hProtoIcon); - return true; - } - - bool OnApply() override - { - m_proto->setByte("RememberPass", m_proto->m_bRememberPwd = chkSave.GetState()); - m_proto->m_szPassword = ptrA(edtPass.GetTextU()); - EndModal(true); - return true; - } - - void OnDestroy() override - { - m_proto->m_bDlgActive = false; - } -}; - -bool CIcqProto::RetrievePassword() -{ - // if we registered via phone (i.e., server holds the password), we don't need to enter it - if (getByte(DB_KEY_PHONEREG)) - return true; - - if (!m_szPassword.IsEmpty() && m_bRememberPwd) - return true; - - m_szPassword = getMStringA("Password"); - if (!m_szPassword.IsEmpty()) { - m_bRememberPwd = true; - return true; - } - - if (m_bDlgActive) - return false; - - return CIcqEnterLoginDlg(this).DoModal(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -struct CIcqRegistrationDlg : public CIcqDlgBase -{ - CMStringA szTrans, szMsisdn; - int iErrorCode = 0; - - CCtrlEdit edtPhone, edtCode; - CCtrlButton btnSendSms; - - CIcqRegistrationDlg(CIcqProto *ppro) : - CIcqDlgBase(ppro, IDD_REGISTER), - edtPhone(this, IDC_PHONE), - edtCode(this, IDC_CODE), - btnSendSms(this, IDC_SENDSMS) - { - btnSendSms.OnClick = Callback(this, &CIcqRegistrationDlg::onClick_SendSms); - edtPhone.OnChange = Callback(this, &CIcqRegistrationDlg::onChange_Phone); - } - - bool OnApply() override - { - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, "https://u.icq.net/api/v60/smsreg/loginWithPhoneNumber.php", &CIcqProto::OnLoginViaPhone); - pReq << CHAR_PARAM("locale", "en") << CHAR_PARAM("msisdn", szMsisdn) << CHAR_PARAM("trans_id", szTrans) << CHAR_PARAM("k", m_proto->appId()) - << CHAR_PARAM("r", pReq->m_reqId) << CHAR_PARAM("f", "json") << WCHAR_PARAM("sms_code", ptrW(edtCode.GetText())) << INT_PARAM("create_account", 1); - pReq->pUserInfo = this; - - SetCursor(LoadCursor(0, IDC_WAIT)); - m_proto->ExecuteRequest(pReq); - SetCursor(LoadCursor(0, IDC_ARROW)); - - if (iErrorCode != 200) - return false; - - EndDialog(m_hwnd, 1); - return true; - } - - void onChange_Phone(CCtrlEdit*) - { - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, "https://u.icq.net/api/v60/smsapi/fcgi-bin/smsphoneinfo", &CIcqProto::OnCheckPhone); - pReq << CHAR_PARAM("service", "icq_registration") << CHAR_PARAM("info", "typing_check,score,iso_country_code") << CHAR_PARAM("lang", "ru") - << WCHAR_PARAM("phone", ptrW(edtPhone.GetText())) << CHAR_PARAM("id", pReq->m_reqId); - pReq->pUserInfo = this; - m_proto->Push(pReq); - } - - void onClick_SendSms(CCtrlButton*) - { - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, "https://u.icq.net/api/v60/smsreg/requestPhoneValidation.php", &CIcqProto::OnValidateSms); - pReq << CHAR_PARAM("locale", "en") << CHAR_PARAM("msisdn", szMsisdn) << CHAR_PARAM("r", pReq->m_reqId) - << CHAR_PARAM("smsFormatType", "human") << CHAR_PARAM("k", m_proto->appId()); - pReq->pUserInfo = this; - m_proto->Push(pReq); - } -}; - -void CIcqProto::OnCheckPhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - if (pReply == nullptr || pReply->resultCode != 200) - return; - - CIcqRegistrationDlg *pDlg = (CIcqRegistrationDlg*)pReq->pUserInfo; - pDlg->btnSendSms.Disable(); - pDlg->edtCode.Disable(); - - JSONROOT root(pReply->pData); - CMStringW wszStatus((*root)["status"].as_mstring()); - if (wszStatus != L"OK") { - pDlg->edtCode.SetText((*root)["printable"].as_mstring()); - return; - } - - CMStringA szPhoneNumber((*root)["typing_check"]["modified_phone_number"].as_mstring()); - CMStringA szPrefix((*root)["modified_prefix"].as_mstring()); - - auto *pNew = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, "https://www.icq.com/smsreg/normalizePhoneNumber.php", &CIcqProto::OnNormalizePhone); - pNew << CHAR_PARAM("countryCode", szPrefix) << CHAR_PARAM("phoneNumber", szPhoneNumber.c_str() + szPrefix.GetLength()) - << CHAR_PARAM("k", appId()) << CHAR_PARAM("r", pReq->m_reqId); - pNew->pUserInfo = pDlg; - Push(pNew); -} - -void CIcqProto::OnNormalizePhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - CIcqRegistrationDlg *pDlg = (CIcqRegistrationDlg*)pReq->pUserInfo; - - JsonReply root(pReply); - pDlg->iErrorCode = root.error(); - if (root.error() != 200) - return; - - const JSONNode &data = root.data(); - pDlg->szMsisdn = data["msisdn"].as_mstring(); - pDlg->btnSendSms.Enable(); -} - -void CIcqProto::OnValidateSms(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - JsonReply root(pReply); - if (root.error() != 200) - return; - - CIcqRegistrationDlg *pDlg = (CIcqRegistrationDlg*)pReq->pUserInfo; - const JSONNode &data = root.data(); - pDlg->szTrans = data["trans_id"].as_mstring(); - - pDlg->edtCode.Enable(); - pDlg->edtCode.SetText(L""); -} - -void CIcqProto::OnLoginViaPhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - CIcqRegistrationDlg *pDlg = (CIcqRegistrationDlg*)pReq->pUserInfo; - - JsonReply root(pReply); - pDlg->iErrorCode = root.error(); - if (root.error() != 200) - return; - - const JSONNode &data = root.data(); - m_szAToken = data["token"]["a"].as_mstring(); - mir_urlDecode(m_szAToken.GetBuffer()); - setString(DB_KEY_ATOKEN, m_szAToken); - - int srvTS = data["hostTime"].as_int(); - m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; - - m_szSessionKey = data["sessionKey"].as_mstring(); - setString(DB_KEY_SESSIONKEY, m_szSessionKey); - - m_szOwnId = data["loginId"].as_mstring(); - setByte(DB_KEY_PHONEREG, 1); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -class CIcqOptionsDlg : public CIcqDlgBase -{ - CCtrlEdit edtUin, edtPassword; - CCtrlCheck chkHideChats, chkTrayIcon, chkLaunchMailbox, chkShowErrorPopups; - CCtrlButton btnCreate; - CMStringW wszOldPass; - -public: - CIcqOptionsDlg(CIcqProto *ppro, int iDlgID, bool bFullDlg) : - CIcqDlgBase(ppro, iDlgID), - edtUin(this, IDC_UIN), - btnCreate(this, IDC_REGISTER), - edtPassword(this, IDC_PASSWORD), - chkTrayIcon(this, IDC_USETRAYICON), - chkHideChats(this, IDC_HIDECHATS), - chkLaunchMailbox(this, IDC_LAUNCH_MAILBOX), - chkShowErrorPopups(this, IDC_SHOWERRORPOPUPS) - { - btnCreate.OnClick = Callback(this, &CIcqOptionsDlg::onClick_Register); - - CreateLink(edtUin, ppro->m_szOwnId); - if (bFullDlg) { - CreateLink(chkHideChats, ppro->m_bHideGroupchats); - CreateLink(chkTrayIcon, ppro->m_bUseTrayIcon); - CreateLink(chkLaunchMailbox, ppro->m_bLaunchMailbox); - CreateLink(chkShowErrorPopups, ppro->m_bErrorPopups); - - chkTrayIcon.OnChange = Callback(this, &CIcqOptionsDlg::onChange_Tray); - } - } - - bool OnInitDialog() override - { - if (m_proto->m_isMra) - btnCreate.Hide(); - else - SetDlgItemText(m_hwnd, IDC_UIN_LABEL, TranslateT("UIN:")); - - wszOldPass = m_proto->getMStringW("Password"); - edtPassword.SetText(wszOldPass); - return true; - } - - bool OnApply() override - { - ptrW wszPass(edtPassword.GetText()); - if (wszPass) - m_proto->setWString("Password", wszPass); - else - m_proto->delSetting("Password"); - - if (wszOldPass != wszPass) { - m_proto->delSetting(DB_KEY_ATOKEN); - m_proto->delSetting(DB_KEY_SESSIONKEY); - m_proto->delSetting(DB_KEY_PHONEREG); - } - - if (mir_wstrlen(wszPass)) { - m_proto->m_szPassword = T2Utf(wszPass).get(); - m_proto->m_bRememberPwd = true; - } - else m_proto->m_bRememberPwd = m_proto->getByte("RememberPass"); - - return true; - } - - void onChange_Tray(CCtrlCheck*) - { - chkLaunchMailbox.Enable(chkTrayIcon.GetState()); - } - - void onClick_Register(CCtrlButton*) - { - CIcqRegistrationDlg dlg(m_proto); - dlg.SetParent(m_hwnd); - if (dlg.DoModal()) // force exit to avoid data corruption - PostMessage(m_hwndParent, WM_COMMAND, MAKELONG(IDCANCEL, BN_CLICKED), 0); - } -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// Advanced options - -class CIcqOptionsAdv : public CIcqDlgBase -{ - CCtrlEdit edtDiff1, edtDiff2; - CCtrlSpin spin1, spin2; - CCtrlCombo cmbStatus1, cmbStatus2; - -public: - CIcqOptionsAdv(CIcqProto *ppro) : - CIcqDlgBase(ppro, IDD_OPTIONS_ADV), - spin1(this, IDC_SPIN1, 32000), - spin2(this, IDC_SPIN2, 32000), - edtDiff1(this, IDC_DIFF1), - edtDiff2(this, IDC_DIFF2), - cmbStatus1(this, IDC_STATUS1), - cmbStatus2(this, IDC_STATUS2) - { - edtDiff1.OnChange = Callback(this, &CIcqOptionsAdv::onChange_Timeout1); - edtDiff2.OnChange = Callback(this, &CIcqOptionsAdv::onChange_Timeout2); - - CreateLink(spin1, ppro->m_iTimeDiff1); - CreateLink(spin2, ppro->m_iTimeDiff2); - } - - bool OnInitDialog() override - { - if (cmbStatus1.GetHwnd()) { - for (uint32_t iStatus = ID_STATUS_OFFLINE; iStatus <= ID_STATUS_MAX; iStatus++) { - int idx = cmbStatus1.AddString(Clist_GetStatusModeDescription(iStatus, 0)); - cmbStatus1.SetItemData(idx, iStatus); - if (iStatus == m_proto->m_iStatus1) - cmbStatus1.SetCurSel(idx); - - idx = cmbStatus2.AddString(Clist_GetStatusModeDescription(iStatus, 0)); - cmbStatus2.SetItemData(idx, iStatus); - if (iStatus == m_proto->m_iStatus2) - cmbStatus2.SetCurSel(idx); - } - } - - return true; - } - - bool OnApply() override - { - if (cmbStatus1.GetHwnd()) { - m_proto->m_iStatus1 = cmbStatus1.GetCurData(); - m_proto->m_iStatus2 = cmbStatus2.GetCurData(); - } - - return true; - } - - void onChange_Timeout1(CCtrlEdit*) - { - bool bEnabled = edtDiff1.GetInt() != 0; - spin2.Enable(bEnabled); - edtDiff2.Enable(bEnabled); - cmbStatus1.Enable(bEnabled); - cmbStatus2.Enable(bEnabled && edtDiff2.GetInt() != 0); - } - - void onChange_Timeout2(CCtrlEdit*) - { - bool bEnabled = edtDiff2.GetInt() != 0; - cmbStatus2.Enable(bEnabled); - } -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// Services - -INT_PTR CIcqProto::CreateAccMgrUI(WPARAM, LPARAM hwndParent) -{ - CIcqOptionsDlg *pDlg = new CIcqOptionsDlg(this, IDD_OPTIONS_ACCMGR, false); - pDlg->SetParent((HWND)hwndParent); - pDlg->Create(); - return (INT_PTR)pDlg->GetHwnd(); -} - -int CIcqProto::OnOptionsInit(WPARAM wParam, LPARAM) -{ - OPTIONSDIALOGPAGE odp = {}; - odp.szTitle.w = m_tszUserName; - odp.flags = ODPF_UNICODE | ODPF_BOLDGROUPS; - odp.szGroup.w = LPGENW("Network"); - odp.position = 1; - - odp.szTab.w = LPGENW("General"); - odp.pDialog = new CIcqOptionsDlg(this, IDD_OPTIONS_FULL, true); - g_plugin.addOptions(wParam, &odp); - - odp.szTab.w = LPGENW("Advanced"); - odp.pDialog = new CIcqOptionsAdv(this); - g_plugin.addOptions(wParam, &odp); - return 0; -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 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" + +class CIcqEnterLoginDlg : public CIcqDlgBase +{ + CCtrlEdit edtPass; + CCtrlCheck chkSave; + +public: + CIcqEnterLoginDlg(CIcqProto *ppro) : + CIcqDlgBase(ppro, IDD_LOGINPW), + edtPass(this, IDC_PASSWORD), + chkSave(this, IDC_SAVEPASS) + { + } + + bool OnInitDialog() override + { + m_proto->m_bDlgActive = true; + chkSave.SetState(m_proto->getBool("RememberPass")); + Window_SetIcon_IcoLib(m_hwnd, m_proto->m_hProtoIcon); + return true; + } + + bool OnApply() override + { + m_proto->setByte("RememberPass", m_proto->m_bRememberPwd = chkSave.GetState()); + m_proto->m_szPassword = ptrA(edtPass.GetTextU()); + EndModal(true); + return true; + } + + void OnDestroy() override + { + m_proto->m_bDlgActive = false; + } +}; + +bool CIcqProto::RetrievePassword() +{ + // if we registered via phone (i.e., server holds the password), we don't need to enter it + if (getByte(DB_KEY_PHONEREG)) + return true; + + if (!m_szPassword.IsEmpty() && m_bRememberPwd) + return true; + + m_szPassword = getMStringA("Password"); + if (!m_szPassword.IsEmpty()) { + m_bRememberPwd = true; + return true; + } + + if (m_bDlgActive) + return false; + + return CIcqEnterLoginDlg(this).DoModal(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +struct CIcqRegistrationDlg : public CIcqDlgBase +{ + CMStringA szTrans, szMsisdn; + int iErrorCode = 0; + + CCtrlEdit edtPhone, edtCode; + CCtrlButton btnSendSms; + + CIcqRegistrationDlg(CIcqProto *ppro) : + CIcqDlgBase(ppro, IDD_REGISTER), + edtPhone(this, IDC_PHONE), + edtCode(this, IDC_CODE), + btnSendSms(this, IDC_SENDSMS) + { + btnSendSms.OnClick = Callback(this, &CIcqRegistrationDlg::onClick_SendSms); + edtPhone.OnChange = Callback(this, &CIcqRegistrationDlg::onChange_Phone); + } + + bool OnApply() override + { + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, "https://u.icq.net/api/v60/smsreg/loginWithPhoneNumber.php", &CIcqProto::OnLoginViaPhone); + pReq << CHAR_PARAM("locale", "en") << CHAR_PARAM("msisdn", szMsisdn) << CHAR_PARAM("trans_id", szTrans) << CHAR_PARAM("k", m_proto->appId()) + << CHAR_PARAM("r", pReq->m_reqId) << CHAR_PARAM("f", "json") << WCHAR_PARAM("sms_code", ptrW(edtCode.GetText())) << INT_PARAM("create_account", 1); + pReq->pUserInfo = this; + + SetCursor(LoadCursor(0, IDC_WAIT)); + m_proto->ExecuteRequest(pReq); + SetCursor(LoadCursor(0, IDC_ARROW)); + + if (iErrorCode != 200) + return false; + + EndDialog(m_hwnd, 1); + return true; + } + + void onChange_Phone(CCtrlEdit*) + { + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, "https://u.icq.net/api/v60/smsapi/fcgi-bin/smsphoneinfo", &CIcqProto::OnCheckPhone); + pReq << CHAR_PARAM("service", "icq_registration") << CHAR_PARAM("info", "typing_check,score,iso_country_code") << CHAR_PARAM("lang", "ru") + << WCHAR_PARAM("phone", ptrW(edtPhone.GetText())) << CHAR_PARAM("id", pReq->m_reqId); + pReq->pUserInfo = this; + m_proto->Push(pReq); + } + + void onClick_SendSms(CCtrlButton*) + { + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, "https://u.icq.net/api/v60/smsreg/requestPhoneValidation.php", &CIcqProto::OnValidateSms); + pReq << CHAR_PARAM("locale", "en") << CHAR_PARAM("msisdn", szMsisdn) << CHAR_PARAM("r", pReq->m_reqId) + << CHAR_PARAM("smsFormatType", "human") << CHAR_PARAM("k", m_proto->appId()); + pReq->pUserInfo = this; + m_proto->Push(pReq); + } +}; + +void CIcqProto::OnCheckPhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + if (pReply == nullptr || pReply->resultCode != 200) + return; + + CIcqRegistrationDlg *pDlg = (CIcqRegistrationDlg*)pReq->pUserInfo; + pDlg->btnSendSms.Disable(); + pDlg->edtCode.Disable(); + + JSONROOT root(pReply->pData); + CMStringW wszStatus((*root)["status"].as_mstring()); + if (wszStatus != L"OK") { + pDlg->edtCode.SetText((*root)["printable"].as_mstring()); + return; + } + + CMStringA szPhoneNumber((*root)["typing_check"]["modified_phone_number"].as_mstring()); + CMStringA szPrefix((*root)["modified_prefix"].as_mstring()); + + auto *pNew = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, "https://www.icq.com/smsreg/normalizePhoneNumber.php", &CIcqProto::OnNormalizePhone); + pNew << CHAR_PARAM("countryCode", szPrefix) << CHAR_PARAM("phoneNumber", szPhoneNumber.c_str() + szPrefix.GetLength()) + << CHAR_PARAM("k", appId()) << CHAR_PARAM("r", pReq->m_reqId); + pNew->pUserInfo = pDlg; + Push(pNew); +} + +void CIcqProto::OnNormalizePhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + CIcqRegistrationDlg *pDlg = (CIcqRegistrationDlg*)pReq->pUserInfo; + + JsonReply root(pReply); + pDlg->iErrorCode = root.error(); + if (root.error() != 200) + return; + + const JSONNode &data = root.data(); + pDlg->szMsisdn = data["msisdn"].as_mstring(); + pDlg->btnSendSms.Enable(); +} + +void CIcqProto::OnValidateSms(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + JsonReply root(pReply); + if (root.error() != 200) + return; + + CIcqRegistrationDlg *pDlg = (CIcqRegistrationDlg*)pReq->pUserInfo; + const JSONNode &data = root.data(); + pDlg->szTrans = data["trans_id"].as_mstring(); + + pDlg->edtCode.Enable(); + pDlg->edtCode.SetText(L""); +} + +void CIcqProto::OnLoginViaPhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + CIcqRegistrationDlg *pDlg = (CIcqRegistrationDlg*)pReq->pUserInfo; + + JsonReply root(pReply); + pDlg->iErrorCode = root.error(); + if (root.error() != 200) + return; + + const JSONNode &data = root.data(); + m_szAToken = data["token"]["a"].as_mstring(); + mir_urlDecode(m_szAToken.GetBuffer()); + setString(DB_KEY_ATOKEN, m_szAToken); + + int srvTS = data["hostTime"].as_int(); + m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; + + m_szSessionKey = data["sessionKey"].as_mstring(); + setString(DB_KEY_SESSIONKEY, m_szSessionKey); + + m_szOwnId = data["loginId"].as_mstring(); + setByte(DB_KEY_PHONEREG, 1); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +class CIcqOptionsDlg : public CIcqDlgBase +{ + CCtrlEdit edtUin, edtPassword; + CCtrlCheck chkHideChats, chkTrayIcon, chkLaunchMailbox, chkShowErrorPopups; + CCtrlButton btnCreate; + CMStringW wszOldPass; + +public: + CIcqOptionsDlg(CIcqProto *ppro, int iDlgID, bool bFullDlg) : + CIcqDlgBase(ppro, iDlgID), + edtUin(this, IDC_UIN), + btnCreate(this, IDC_REGISTER), + edtPassword(this, IDC_PASSWORD), + chkTrayIcon(this, IDC_USETRAYICON), + chkHideChats(this, IDC_HIDECHATS), + chkLaunchMailbox(this, IDC_LAUNCH_MAILBOX), + chkShowErrorPopups(this, IDC_SHOWERRORPOPUPS) + { + btnCreate.OnClick = Callback(this, &CIcqOptionsDlg::onClick_Register); + + CreateLink(edtUin, ppro->m_szOwnId); + if (bFullDlg) { + CreateLink(chkHideChats, ppro->m_bHideGroupchats); + CreateLink(chkTrayIcon, ppro->m_bUseTrayIcon); + CreateLink(chkLaunchMailbox, ppro->m_bLaunchMailbox); + CreateLink(chkShowErrorPopups, ppro->m_bErrorPopups); + + chkTrayIcon.OnChange = Callback(this, &CIcqOptionsDlg::onChange_Tray); + } + } + + bool OnInitDialog() override + { + if (m_proto->m_isMra) + btnCreate.Hide(); + else + SetDlgItemText(m_hwnd, IDC_UIN_LABEL, TranslateT("UIN:")); + + wszOldPass = m_proto->getMStringW("Password"); + edtPassword.SetText(wszOldPass); + return true; + } + + bool OnApply() override + { + ptrW wszPass(edtPassword.GetText()); + if (wszPass) + m_proto->setWString("Password", wszPass); + else + m_proto->delSetting("Password"); + + if (wszOldPass != wszPass) { + m_proto->delSetting(DB_KEY_ATOKEN); + m_proto->delSetting(DB_KEY_SESSIONKEY); + m_proto->delSetting(DB_KEY_PHONEREG); + } + + if (mir_wstrlen(wszPass)) { + m_proto->m_szPassword = T2Utf(wszPass).get(); + m_proto->m_bRememberPwd = true; + } + else m_proto->m_bRememberPwd = m_proto->getByte("RememberPass"); + + return true; + } + + void onChange_Tray(CCtrlCheck*) + { + chkLaunchMailbox.Enable(chkTrayIcon.GetState()); + } + + void onClick_Register(CCtrlButton*) + { + CIcqRegistrationDlg dlg(m_proto); + dlg.SetParent(m_hwnd); + if (dlg.DoModal()) // force exit to avoid data corruption + PostMessage(m_hwndParent, WM_COMMAND, MAKELONG(IDCANCEL, BN_CLICKED), 0); + } +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// Advanced options + +class CIcqOptionsAdv : public CIcqDlgBase +{ + CCtrlEdit edtDiff1, edtDiff2; + CCtrlSpin spin1, spin2; + CCtrlCombo cmbStatus1, cmbStatus2; + +public: + CIcqOptionsAdv(CIcqProto *ppro) : + CIcqDlgBase(ppro, IDD_OPTIONS_ADV), + spin1(this, IDC_SPIN1, 32000), + spin2(this, IDC_SPIN2, 32000), + edtDiff1(this, IDC_DIFF1), + edtDiff2(this, IDC_DIFF2), + cmbStatus1(this, IDC_STATUS1), + cmbStatus2(this, IDC_STATUS2) + { + edtDiff1.OnChange = Callback(this, &CIcqOptionsAdv::onChange_Timeout1); + edtDiff2.OnChange = Callback(this, &CIcqOptionsAdv::onChange_Timeout2); + + CreateLink(spin1, ppro->m_iTimeDiff1); + CreateLink(spin2, ppro->m_iTimeDiff2); + } + + bool OnInitDialog() override + { + if (cmbStatus1.GetHwnd()) { + for (uint32_t iStatus = ID_STATUS_OFFLINE; iStatus <= ID_STATUS_MAX; iStatus++) { + int idx = cmbStatus1.AddString(Clist_GetStatusModeDescription(iStatus, 0)); + cmbStatus1.SetItemData(idx, iStatus); + if (iStatus == m_proto->m_iStatus1) + cmbStatus1.SetCurSel(idx); + + idx = cmbStatus2.AddString(Clist_GetStatusModeDescription(iStatus, 0)); + cmbStatus2.SetItemData(idx, iStatus); + if (iStatus == m_proto->m_iStatus2) + cmbStatus2.SetCurSel(idx); + } + } + + return true; + } + + bool OnApply() override + { + if (cmbStatus1.GetHwnd()) { + m_proto->m_iStatus1 = cmbStatus1.GetCurData(); + m_proto->m_iStatus2 = cmbStatus2.GetCurData(); + } + + return true; + } + + void onChange_Timeout1(CCtrlEdit*) + { + bool bEnabled = edtDiff1.GetInt() != 0; + spin2.Enable(bEnabled); + edtDiff2.Enable(bEnabled); + cmbStatus1.Enable(bEnabled); + cmbStatus2.Enable(bEnabled && edtDiff2.GetInt() != 0); + } + + void onChange_Timeout2(CCtrlEdit*) + { + bool bEnabled = edtDiff2.GetInt() != 0; + cmbStatus2.Enable(bEnabled); + } +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// Services + +INT_PTR CIcqProto::CreateAccMgrUI(WPARAM, LPARAM hwndParent) +{ + CIcqOptionsDlg *pDlg = new CIcqOptionsDlg(this, IDD_OPTIONS_ACCMGR, false); + pDlg->SetParent((HWND)hwndParent); + pDlg->Create(); + return (INT_PTR)pDlg->GetHwnd(); +} + +int CIcqProto::OnOptionsInit(WPARAM wParam, LPARAM) +{ + OPTIONSDIALOGPAGE odp = {}; + odp.szTitle.w = m_tszUserName; + odp.flags = ODPF_UNICODE | ODPF_BOLDGROUPS; + odp.szGroup.w = LPGENW("Network"); + odp.position = 1; + + odp.szTab.w = LPGENW("General"); + odp.pDialog = new CIcqOptionsDlg(this, IDD_OPTIONS_FULL, true); + g_plugin.addOptions(wParam, &odp); + + odp.szTab.w = LPGENW("Advanced"); + odp.pDialog = new CIcqOptionsAdv(this); + g_plugin.addOptions(wParam, &odp); + return 0; +} diff --git a/protocols/ICQ-WIM/src/poll.cpp b/protocols/ICQ-WIM/src/poll.cpp index d7ca7298ad..6fca78728f 100644 --- a/protocols/ICQ-WIM/src/poll.cpp +++ b/protocols/ICQ-WIM/src/poll.cpp @@ -1,409 +1,409 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-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, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- -// Long-poll thread and its item handlers - -#include "stdafx.h" - -void CIcqProto::ProcessBuddyList(const JSONNode &ev) -{ - m_arGroups.destroy(); - - LIST tmpGroups(10); - bool bEnableMenu = false; - - for (auto &it : ev["groups"]) { - auto *pGroup = new IcqGroup(it["id"].as_int(), it["name"].as_mstring()); - debugLogA("new group: id=%d, level=%d, name=%S", pGroup->id, pGroup->level, pGroup->wszName.c_str()); - if (pGroup->level != 0) { - for (auto &p : tmpGroups.rev_iter()) { - if (p->level == pGroup->level-1) { - pGroup->wszName = p->wszName + L"\\" + pGroup->wszName; - debugLogA("Group name fixed as %S", pGroup->wszName.c_str()); - break; - } - } - } - tmpGroups.insert(pGroup); - m_arGroups.insert(pGroup); - - bool bCreated = false; - - for (auto &buddy : it["buddies"]) { - MCONTACT hContact = ParseBuddyInfo(buddy); - if (hContact == INVALID_CONTACT_ID) - continue; - - setWString(hContact, "IcqGroup", pGroup->wszName); - - if (!bCreated) { - Clist_GroupCreate(0, pGroup->wszName); - bCreated = true; - } - - ptrW mirGroup(Clist_GetGroup(hContact)); - if (mir_wstrcmp(mirGroup, pGroup->wszName)) - bEnableMenu = true; - - if (!mirGroup) - Clist_SetGroup(hContact, pGroup->wszName); - } - } - - if (bEnableMenu) - Menu_ShowItem(m_hUploadGroups, true); - - for (auto &it : m_arCache) - if (!it->m_bInList && !getBool(it->m_hContact, "IcqDeleted")) - Contact::RemoveFromList(it->m_hContact); -} - -void CIcqProto::ProcessDiff(const JSONNode &ev) -{ - for (auto &block : ev) { - CMStringW szType = block["type"].as_mstring(); - if (szType != "updated" && szType != "created" && szType != "deleted") - continue; - - for (auto &it : block["data"]) { - int grpId = it["id"].as_int(); - CMStringW wszName = it["name"].as_mstring(); - - auto *pGroup = m_arGroups.find((IcqGroup *)&grpId); - if (pGroup == nullptr) { - if (szType != "created") { - debugLogA("Group %d isn't found", grpId); - continue; - } - - pGroup = new IcqGroup(grpId, wszName); - m_arGroups.insert(pGroup); - } - else { - pGroup->wszSrvName = wszName; - pGroup->SetName(wszName); - } - - bool bCreated = false, bDeleted = (szType == "deleted"); - - for (auto &buddy : it["buddies"]) { - if (bDeleted) - continue; - - MCONTACT hContact = ParseBuddyInfo(buddy, true); - if (hContact == INVALID_CONTACT_ID) - continue; - - setWString(hContact, "IcqGroup", pGroup->wszName); - - if (!bCreated) { - Clist_GroupCreate(0, pGroup->wszName); - bCreated = true; - } - - ptrW wszGroup(Clist_GetGroup(hContact)); - if (!wszGroup) - Clist_SetGroup(hContact, pGroup->wszName); - } - - if (bDeleted) - m_arGroups.remove(pGroup); - } - - RefreshGroups(); - } -} - -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"notification") - ProcessNotification(pData); - else if (szType == L"permitDeny") - ProcessPermissions(pData); - else if (szType == L"presence") - ProcessPresence(pData); - else if (szType == L"sessionEnded") - ProcessSessionEnd(pData); - else if (szType == L"typing") - ProcessTyping(pData); -} - -void CIcqProto::ProcessHistData(const JSONNode &ev) -{ - MCONTACT hContact; - bool bVeryBeginning = m_bFirstBos; - - CMStringW wszId(ev["sn"].as_mstring()); - auto *pCache = FindContactByUIN(wszId); // might be NULL for groupchats - - 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 AsyncRapiRequest(this, "getChatInfo", &CIcqProto::OnGetChatInfo); - pReq->params << WCHAR_PARAM("sn", wszId) << INT_PARAM("memberLimit", 100) << CHAR_PARAM("aimSid", m_aimsid); - pReq->pUserInfo = si; - Push(pReq); - } - else LoadChatInfo(si); - } - } - else { - hContact = CreateContact(wszId, true); - - // for temporary contacts that just gonna be created - if (pCache == nullptr) { - bVeryBeginning = true; - pCache = FindContactByUIN(wszId); - } - } - - // restore reading from the previous point, if we just installed Miranda - __int64 lastMsgId = getId(hContact, DB_KEY_LASTMSGID); - if (lastMsgId == 0) { - lastMsgId = _wtoi64(ev["yours"]["lastRead"].as_mstring()); - setId(hContact, DB_KEY_LASTMSGID, lastMsgId); - } - - __int64 patchVersion = _wtoi64(ev["patchVersion"].as_mstring()); - setId(hContact, DB_KEY_PATCHVER, patchVersion); - - __int64 srvLastId = _wtoi64(ev["lastMsgId"].as_mstring()); - - // we load history in the very beginning or if the previous message - if (bVeryBeginning) { - if (pCache) { - debugLogA("Setting cache = %lld for %d", srvLastId, hContact); - pCache->m_iProcessedMsgId = srvLastId; - } - - if (srvLastId > lastMsgId) { - debugLogA("We need to retrieve history for %S: %lld > %lld", wszId.c_str(), srvLastId, lastMsgId); - RetrieveUserHistory(hContact, lastMsgId, false); - } - } - else { - if (!(pCache && pCache->m_iProcessedMsgId >= srvLastId)) { - if (pCache) - debugLogA("Proceeding with cache for %d: %lld < %lld", hContact, pCache->m_iProcessedMsgId, srvLastId); - else - debugLogA("Proceeding with empty cache for %d", hContact); - - for (auto &it : ev["tail"]["messages"]) - ParseMessage(hContact, lastMsgId, it, false, true); - - setId(hContact, DB_KEY_LASTMSGID, lastMsgId); - if (pCache) { - pCache->m_iProcessedMsgId = lastMsgId; - debugLogA("Setting second cache = %lld for %d", srvLastId, hContact); - } - } - } - - // check remote read - if (g_bMessageState) { - __int64 srvRemoteRead = _wtoi64(ev["theirs"]["lastRead"].as_mstring()); - __int64 lastRemoteRead = getId(hContact, DB_KEY_REMOTEREAD); - if (srvRemoteRead > lastRemoteRead) { - setId(hContact, DB_KEY_REMOTEREAD, srvRemoteRead); - - if (g_bMessageState) - CallService(MS_MESSAGESTATE_UPDATE, hContact, MRD_TYPE_READ); - } - } -} - -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) -{ - if (auto &var = ev["friendly"]) - setWString("Nick", var.as_mstring()); - - if (auto &var = ev["attachedPhoneNumber"]) - setWString(DB_KEY_PHONE, var.as_mstring()); - - CheckAvatarChange(0, ev); -} - -void CIcqProto::ProcessNotification(const JSONNode &ev) -{ - for (auto &fld : ev["fields"]) { - const JSONNode &email = fld["mailbox.newMessage"]; - if (email) { - JSONROOT root(email.as_string().c_str()); - CMStringW wszFrom((*root)["from"].as_mstring()); - CMStringW wszSubj((*root)["subject"].as_mstring()); - m_unreadEmails = (*root)["unreadCount"].as_int(); - debugLogW(L"You received e-mail (%d) from <%s>: <%s>", m_unreadEmails, wszFrom.c_str(), wszSubj.c_str()); - - CMStringW wszMessage(FORMAT, TranslateT("You received e-mail from %s: %s"), wszFrom.c_str(), wszSubj.c_str()); - EmailNotification(wszMessage); - } - - const JSONNode &status = fld["mailbox.status"]; - if (status) { - int iOldCount = m_unreadEmails; - - JSONROOT root(status.as_string().c_str()); - m_szMailBox = (*root)["email"].as_mstring(); - m_unreadEmails = (*root)["unreadCount"].as_int(); - - // we've read/removed some messages from server - if (iOldCount > m_unreadEmails) { - g_clistApi.pfnRemoveEvent(0, ICQ_FAKE_EVENT_ID); - return; - } - - // we notify about initial mail count only during login - if (m_bFirstBos && m_unreadEmails > 0) { - CMStringW wszMessage(FORMAT, TranslateT("You have %d unread emails"), m_unreadEmails); - EmailNotification(wszMessage); - } - } - } -} - -void CIcqProto::ProcessPresence(const JSONNode &ev) -{ - CMStringW aimId = ev["aimId"].as_mstring(); - - IcqCacheItem *pCache = FindContactByUIN(aimId); - if (pCache == nullptr) - return; - - int iNewStatus = StatusFromPresence(ev, pCache->m_hContact); - if (iNewStatus == -1) - iNewStatus = ID_STATUS_OFFLINE; - - // major crutch dedicated to the official client behaviour to go offline - // when its window gets closed. we change the status of a contact to the - // first chosen one from options and initialize a timer - if (iNewStatus == ID_STATUS_OFFLINE) { - if (m_iTimeDiff1) { - iNewStatus = m_iStatus1; - pCache->m_timer1 = time(0); - } - } - // if a client returns back online, we clear timers not to play with statuses anymore - else pCache->m_timer1 = pCache->m_timer2 = 0; - - setWord(pCache->m_hContact, "Status", iNewStatus); - - Json2string(pCache->m_hContact, ev, "friendly", "Nick", true); - CheckAvatarChange(pCache->m_hContact, ev); -} - -void CIcqProto::ProcessSessionEnd(const JSONNode &/*ev*/) -{ - m_szRToken.Empty(); - m_iRClientId = 0; - delSetting(DB_KEY_RCLIENTID); - - ShutdownSession(); -} - -void CIcqProto::ProcessTyping(const JSONNode &ev) -{ - CMStringW aimId = ev["aimId"].as_mstring(); - CMStringW wszStatus = ev["typingStatus"].as_mstring(); - - IcqCacheItem *pCache = FindContactByUIN(aimId); - 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"); - m_bFirstBos = true; - - while (m_bOnline && !m_fetchBaseURL.IsEmpty()) { - CMStringA szUrl = m_fetchBaseURL; - if (m_bFirstBos) - szUrl.Append("&first=1"); - else - szUrl.Append("&timeout=25000"); - - auto *pReq = new AsyncHttpRequest(CONN_FETCH, REQUEST_GET, szUrl, &CIcqProto::OnFetchEvents); - if (!m_bFirstBos) - pReq->timeout = 62000; - - if (!ExecuteRequest(pReq)) { - ShutdownSession(); - break; - } - - m_bFirstBos = false; - } - - debugLogA("Polling thread ended"); -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 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. +// ----------------------------------------------------------------------------- +// Long-poll thread and its item handlers + +#include "stdafx.h" + +void CIcqProto::ProcessBuddyList(const JSONNode &ev) +{ + m_arGroups.destroy(); + + LIST tmpGroups(10); + bool bEnableMenu = false; + + for (auto &it : ev["groups"]) { + auto *pGroup = new IcqGroup(it["id"].as_int(), it["name"].as_mstring()); + debugLogA("new group: id=%d, level=%d, name=%S", pGroup->id, pGroup->level, pGroup->wszName.c_str()); + if (pGroup->level != 0) { + for (auto &p : tmpGroups.rev_iter()) { + if (p->level == pGroup->level-1) { + pGroup->wszName = p->wszName + L"\\" + pGroup->wszName; + debugLogA("Group name fixed as %S", pGroup->wszName.c_str()); + break; + } + } + } + tmpGroups.insert(pGroup); + m_arGroups.insert(pGroup); + + bool bCreated = false; + + for (auto &buddy : it["buddies"]) { + MCONTACT hContact = ParseBuddyInfo(buddy); + if (hContact == INVALID_CONTACT_ID) + continue; + + setWString(hContact, "IcqGroup", pGroup->wszName); + + if (!bCreated) { + Clist_GroupCreate(0, pGroup->wszName); + bCreated = true; + } + + ptrW mirGroup(Clist_GetGroup(hContact)); + if (mir_wstrcmp(mirGroup, pGroup->wszName)) + bEnableMenu = true; + + if (!mirGroup) + Clist_SetGroup(hContact, pGroup->wszName); + } + } + + if (bEnableMenu) + Menu_ShowItem(m_hUploadGroups, true); + + for (auto &it : m_arCache) + if (!it->m_bInList && !getBool(it->m_hContact, "IcqDeleted")) + Contact::RemoveFromList(it->m_hContact); +} + +void CIcqProto::ProcessDiff(const JSONNode &ev) +{ + for (auto &block : ev) { + CMStringW szType = block["type"].as_mstring(); + if (szType != "updated" && szType != "created" && szType != "deleted") + continue; + + for (auto &it : block["data"]) { + int grpId = it["id"].as_int(); + CMStringW wszName = it["name"].as_mstring(); + + auto *pGroup = m_arGroups.find((IcqGroup *)&grpId); + if (pGroup == nullptr) { + if (szType != "created") { + debugLogA("Group %d isn't found", grpId); + continue; + } + + pGroup = new IcqGroup(grpId, wszName); + m_arGroups.insert(pGroup); + } + else { + pGroup->wszSrvName = wszName; + pGroup->SetName(wszName); + } + + bool bCreated = false, bDeleted = (szType == "deleted"); + + for (auto &buddy : it["buddies"]) { + if (bDeleted) + continue; + + MCONTACT hContact = ParseBuddyInfo(buddy, true); + if (hContact == INVALID_CONTACT_ID) + continue; + + setWString(hContact, "IcqGroup", pGroup->wszName); + + if (!bCreated) { + Clist_GroupCreate(0, pGroup->wszName); + bCreated = true; + } + + ptrW wszGroup(Clist_GetGroup(hContact)); + if (!wszGroup) + Clist_SetGroup(hContact, pGroup->wszName); + } + + if (bDeleted) + m_arGroups.remove(pGroup); + } + + RefreshGroups(); + } +} + +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"notification") + ProcessNotification(pData); + else if (szType == L"permitDeny") + ProcessPermissions(pData); + else if (szType == L"presence") + ProcessPresence(pData); + else if (szType == L"sessionEnded") + ProcessSessionEnd(pData); + else if (szType == L"typing") + ProcessTyping(pData); +} + +void CIcqProto::ProcessHistData(const JSONNode &ev) +{ + MCONTACT hContact; + bool bVeryBeginning = m_bFirstBos; + + CMStringW wszId(ev["sn"].as_mstring()); + auto *pCache = FindContactByUIN(wszId); // might be NULL for groupchats + + 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 AsyncRapiRequest(this, "getChatInfo", &CIcqProto::OnGetChatInfo); + pReq->params << WCHAR_PARAM("sn", wszId) << INT_PARAM("memberLimit", 100) << CHAR_PARAM("aimSid", m_aimsid); + pReq->pUserInfo = si; + Push(pReq); + } + else LoadChatInfo(si); + } + } + else { + hContact = CreateContact(wszId, true); + + // for temporary contacts that just gonna be created + if (pCache == nullptr) { + bVeryBeginning = true; + pCache = FindContactByUIN(wszId); + } + } + + // restore reading from the previous point, if we just installed Miranda + __int64 lastMsgId = getId(hContact, DB_KEY_LASTMSGID); + if (lastMsgId == 0) { + lastMsgId = _wtoi64(ev["yours"]["lastRead"].as_mstring()); + setId(hContact, DB_KEY_LASTMSGID, lastMsgId); + } + + __int64 patchVersion = _wtoi64(ev["patchVersion"].as_mstring()); + setId(hContact, DB_KEY_PATCHVER, patchVersion); + + __int64 srvLastId = _wtoi64(ev["lastMsgId"].as_mstring()); + + // we load history in the very beginning or if the previous message + if (bVeryBeginning) { + if (pCache) { + debugLogA("Setting cache = %lld for %d", srvLastId, hContact); + pCache->m_iProcessedMsgId = srvLastId; + } + + if (srvLastId > lastMsgId) { + debugLogA("We need to retrieve history for %S: %lld > %lld", wszId.c_str(), srvLastId, lastMsgId); + RetrieveUserHistory(hContact, lastMsgId, false); + } + } + else { + if (!(pCache && pCache->m_iProcessedMsgId >= srvLastId)) { + if (pCache) + debugLogA("Proceeding with cache for %d: %lld < %lld", hContact, pCache->m_iProcessedMsgId, srvLastId); + else + debugLogA("Proceeding with empty cache for %d", hContact); + + for (auto &it : ev["tail"]["messages"]) + ParseMessage(hContact, lastMsgId, it, false, true); + + setId(hContact, DB_KEY_LASTMSGID, lastMsgId); + if (pCache) { + pCache->m_iProcessedMsgId = lastMsgId; + debugLogA("Setting second cache = %lld for %d", srvLastId, hContact); + } + } + } + + // check remote read + if (g_bMessageState) { + __int64 srvRemoteRead = _wtoi64(ev["theirs"]["lastRead"].as_mstring()); + __int64 lastRemoteRead = getId(hContact, DB_KEY_REMOTEREAD); + if (srvRemoteRead > lastRemoteRead) { + setId(hContact, DB_KEY_REMOTEREAD, srvRemoteRead); + + if (g_bMessageState) + CallService(MS_MESSAGESTATE_UPDATE, hContact, MRD_TYPE_READ); + } + } +} + +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) +{ + if (auto &var = ev["friendly"]) + setWString("Nick", var.as_mstring()); + + if (auto &var = ev["attachedPhoneNumber"]) + setWString(DB_KEY_PHONE, var.as_mstring()); + + CheckAvatarChange(0, ev); +} + +void CIcqProto::ProcessNotification(const JSONNode &ev) +{ + for (auto &fld : ev["fields"]) { + const JSONNode &email = fld["mailbox.newMessage"]; + if (email) { + JSONROOT root(email.as_string().c_str()); + CMStringW wszFrom((*root)["from"].as_mstring()); + CMStringW wszSubj((*root)["subject"].as_mstring()); + m_unreadEmails = (*root)["unreadCount"].as_int(); + debugLogW(L"You received e-mail (%d) from <%s>: <%s>", m_unreadEmails, wszFrom.c_str(), wszSubj.c_str()); + + CMStringW wszMessage(FORMAT, TranslateT("You received e-mail from %s: %s"), wszFrom.c_str(), wszSubj.c_str()); + EmailNotification(wszMessage); + } + + const JSONNode &status = fld["mailbox.status"]; + if (status) { + int iOldCount = m_unreadEmails; + + JSONROOT root(status.as_string().c_str()); + m_szMailBox = (*root)["email"].as_mstring(); + m_unreadEmails = (*root)["unreadCount"].as_int(); + + // we've read/removed some messages from server + if (iOldCount > m_unreadEmails) { + g_clistApi.pfnRemoveEvent(0, ICQ_FAKE_EVENT_ID); + return; + } + + // we notify about initial mail count only during login + if (m_bFirstBos && m_unreadEmails > 0) { + CMStringW wszMessage(FORMAT, TranslateT("You have %d unread emails"), m_unreadEmails); + EmailNotification(wszMessage); + } + } + } +} + +void CIcqProto::ProcessPresence(const JSONNode &ev) +{ + CMStringW aimId = ev["aimId"].as_mstring(); + + IcqCacheItem *pCache = FindContactByUIN(aimId); + if (pCache == nullptr) + return; + + int iNewStatus = StatusFromPresence(ev, pCache->m_hContact); + if (iNewStatus == -1) + iNewStatus = ID_STATUS_OFFLINE; + + // major crutch dedicated to the official client behaviour to go offline + // when its window gets closed. we change the status of a contact to the + // first chosen one from options and initialize a timer + if (iNewStatus == ID_STATUS_OFFLINE) { + if (m_iTimeDiff1) { + iNewStatus = m_iStatus1; + pCache->m_timer1 = time(0); + } + } + // if a client returns back online, we clear timers not to play with statuses anymore + else pCache->m_timer1 = pCache->m_timer2 = 0; + + setWord(pCache->m_hContact, "Status", iNewStatus); + + Json2string(pCache->m_hContact, ev, "friendly", "Nick", true); + CheckAvatarChange(pCache->m_hContact, ev); +} + +void CIcqProto::ProcessSessionEnd(const JSONNode &/*ev*/) +{ + m_szRToken.Empty(); + m_iRClientId = 0; + delSetting(DB_KEY_RCLIENTID); + + ShutdownSession(); +} + +void CIcqProto::ProcessTyping(const JSONNode &ev) +{ + CMStringW aimId = ev["aimId"].as_mstring(); + CMStringW wszStatus = ev["typingStatus"].as_mstring(); + + IcqCacheItem *pCache = FindContactByUIN(aimId); + 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"); + m_bFirstBos = true; + + while (m_bOnline && !m_fetchBaseURL.IsEmpty()) { + CMStringA szUrl = m_fetchBaseURL; + if (m_bFirstBos) + szUrl.Append("&first=1"); + else + szUrl.Append("&timeout=25000"); + + auto *pReq = new AsyncHttpRequest(CONN_FETCH, REQUEST_GET, szUrl, &CIcqProto::OnFetchEvents); + if (!m_bFirstBos) + pReq->timeout = 62000; + + if (!ExecuteRequest(pReq)) { + ShutdownSession(); + break; + } + + m_bFirstBos = false; + } + + debugLogA("Polling thread ended"); +} diff --git a/protocols/ICQ-WIM/src/proto.cpp b/protocols/ICQ-WIM/src/proto.cpp index 84839ad923..5a86c896d8 100644 --- a/protocols/ICQ-WIM/src/proto.cpp +++ b/protocols/ICQ-WIM/src/proto.cpp @@ -1,678 +1,678 @@ -// ---------------------------------------------------------------------------80 -// ICQ plugin for Miranda Instant Messenger -// ________________________________________ -// -// Copyright © 2000-2001 Richard Hughes, Roland Rabien, Tristan Van de Vreede -// Copyright © 2001-2002 Jon Keating, Richard Hughes -// Copyright © 2002-2004 Martin Öberg, Sam Kothari, Robert Rainwater -// Copyright © 2004-2010 Joe Kucera, George Hazan -// Copyright © 2012-2022 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. -// ----------------------------------------------------------------------------- -// DESCRIPTION: -// -// Protocol Interface Implementation -// ----------------------------------------------------------------------------- - -#include "stdafx.h" - -#include "m_icolib.h" - -#pragma warning(disable:4355) - -static int CompareCache(const IcqCacheItem *p1, const IcqCacheItem *p2) -{ - return mir_wstrcmp(p1->m_aimid, p2->m_aimid); -} - -CIcqProto::CIcqProto(const char *aProtoName, const wchar_t *aUserName) : - PROTO(aProtoName, aUserName), - m_impl(*this), - m_arHttpQueue(10), - m_arOwnIds(1, PtrKeySortT), - m_arCache(20, &CompareCache), - m_arGroups(10, NumericKeySortT), - m_arMarkReadQueue(10, NumericKeySortT), - m_evRequestsQueue(CreateEvent(nullptr, FALSE, FALSE, nullptr)), - m_szOwnId(this, DB_KEY_ID), - m_iStatus1(this, "Status1", ID_STATUS_AWAY), - m_iStatus2(this, "Status2", ID_STATUS_NA), - m_iTimeDiff1(this, "TimeDiff1", 0), - m_iTimeDiff2(this, "TimeDiff2", 0), - m_bHideGroupchats(this, "HideChats", true), - m_bUseTrayIcon(this, "UseTrayIcon", false), - m_bErrorPopups(this, "ShowErrorPopups", true), - m_bLaunchMailbox(this, "LaunchMailbox", true) -{ - db_set_resident(m_szModuleName, DB_KEY_IDLE); - db_set_resident(m_szModuleName, DB_KEY_ONLINETS); - - m_isMra = !stricmp(Proto_GetAccount(m_szModuleName)->szProtoName, "MRA"); - - // services - CreateProtoService(PS_CREATEACCMGRUI, &CIcqProto::CreateAccMgrUI); - - CreateProtoService(PS_GETAVATARCAPS, &CIcqProto::GetAvatarCaps); - CreateProtoService(PS_GETAVATARINFO, &CIcqProto::GetAvatarInfo); - CreateProtoService(PS_GETMYAVATAR, &CIcqProto::GetAvatar); - CreateProtoService(PS_SETMYAVATAR, &CIcqProto::SetAvatar); - - CreateProtoService(PS_MENU_LOADHISTORY, &CIcqProto::OnMenuLoadHistory); - CreateProtoService(PS_GETUNREADEMAILCOUNT, &CIcqProto::GetEmailCount); - CreateProtoService(PS_GOTO_INBOX, &CIcqProto::GotoInbox); - - // events - HookProtoEvent(ME_CLIST_GROUPCHANGE, &CIcqProto::OnGroupChange); - HookProtoEvent(ME_DB_EVENT_MARKED_READ, &CIcqProto::OnDbEventRead); - HookProtoEvent(ME_GC_EVENT, &CIcqProto::GroupchatEventHook); - HookProtoEvent(ME_GC_BUILDMENU, &CIcqProto::GroupchatMenuHook); - HookProtoEvent(ME_OPT_INITIALISE, &CIcqProto::OnOptionsInit); - - // group chats - GCREGISTER gcr = {}; - gcr.dwFlags = GC_TYPNOTIF | GC_CHANMGR; - gcr.ptszDispName = m_tszUserName; - gcr.pszModule = m_szModuleName; - Chat_Register(&gcr); - - // netlib handle - NETLIBUSER nlu = {}; - nlu.szSettingsModule = m_szModuleName; - nlu.flags = NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; - nlu.szDescriptiveName.w = m_tszUserName; - m_hNetlibUser = Netlib_RegisterUser(&nlu); - - // this was previously an old ICQ account - ptrW wszUin(GetUIN(0)); - if (wszUin != nullptr) { - delSetting("UIN"); - - m_szOwnId = wszUin; - - for (auto &it : AccContacts()) - delSetting(it, "e-mail"); - } - // this was previously an old MRA account - else { - CMStringW wszEmail(getMStringW("e-mail")); - if (!wszEmail.IsEmpty()) { - m_szOwnId = wszEmail; - delSetting("e-mail"); - } - } - - m_hWorkerThread = ForkThreadEx(&CIcqProto::ServerThread, nullptr, nullptr); -} - -CIcqProto::~CIcqProto() -{ - ::CloseHandle(m_evRequestsQueue); -} - -//////////////////////////////////////////////////////////////////////////////////////// -// OnModulesLoadedEx - performs hook registration - -void CIcqProto::OnModulesLoaded() -{ - InitContactCache(); - - HookProtoEvent(ME_USERINFO_INITIALISE, &CIcqProto::OnUserInfoInit); - - // load custom smilies - CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers\\*.png", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName); - SMADD_CONT cont = { 2, m_szModuleName, wszPath }; - CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); -} - -void CIcqProto::OnShutdown() -{ - m_bTerminated = true; -} - -void CIcqProto::OnContactAdded(MCONTACT hContact) -{ - CMStringW wszId(getMStringW(hContact, DB_KEY_ID)); - if (!wszId.IsEmpty() && !FindContactByUIN(wszId)) { - mir_cslock l(m_csCache); - m_arCache.insert(new IcqCacheItem(wszId, hContact)); - } -} - -void CIcqProto::OnContactDeleted(MCONTACT hContact) -{ - CMStringW szId(GetUserId(hContact)); - if (!isChatRoom(hContact)) - m_arCache.remove(FindContactByUIN(szId)); - - Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/removeBuddy") - << AIMSID(this) << WCHAR_PARAM("buddy", szId) << INT_PARAM("allGroups", 1)); -} - -void CIcqProto::OnEventEdited(MCONTACT, MEVENT) -{ - -} - -INT_PTR CIcqProto::OnMenuLoadHistory(WPARAM hContact, LPARAM) -{ - delSetting(hContact, DB_KEY_LASTMSGID); - - RetrieveUserHistory(hContact, 1, true); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::OnBuildProtoMenu() -{ - CMenuItem mi(&g_plugin); - mi.root = Menu_GetProtocolRoot(this); - mi.flags = CMIF_UNMOVABLE; - - // "Bookmarks..." - mi.pszService = "/UploadGroups"; - CreateProtoService(mi.pszService, &CIcqProto::UploadGroups); - mi.name.a = LPGEN("Synchronize server groups"); - mi.position = 200001; - mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_GROUP); - m_hUploadGroups = Menu_AddProtoMenuItem(&mi, m_szModuleName); - - mi.pszService = "/EditGroups"; - CreateProtoService(mi.pszService, &CIcqProto::EditGroups); - mi.name.a = LPGEN("Edit server groups"); - mi.position = 200002; - mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_GROUP); - Menu_AddProtoMenuItem(&mi, m_szModuleName); - - mi.pszService = "/EditProfile"; - CreateProtoService(mi.pszService, &CIcqProto::EditProfile); - mi.name.a = LPGEN("Edit my web profile"); - mi.position = 210001; - mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_MIRANDAWEB); - Menu_AddProtoMenuItem(&mi, m_szModuleName); - - Menu_ShowItem(m_hUploadGroups, false); -} - -INT_PTR CIcqProto::UploadGroups(WPARAM, LPARAM) -{ - for (auto &it : AccContacts()) { - if (isChatRoom(it)) - continue; - - ptrW wszIcqGroup(getWStringA(it, "IcqGroup")); - if (wszIcqGroup == nullptr) - continue; - - ptrW wszMirGroup(Clist_GetGroup(it)); - if (!wszMirGroup) - wszMirGroup = mir_wstrdup(L"General"); - if (mir_wstrcmp(wszIcqGroup, wszMirGroup)) - MoveContactToGroup(it, wszIcqGroup, wszMirGroup); - } - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -class CGroupEditDlg : public CIcqDlgBase -{ - CCtrlListView groups; - -public: - - static CGroupEditDlg *pDlg; - - CGroupEditDlg(CIcqProto *ppro) : - CIcqDlgBase(ppro, IDD_EDITGROUPS), - groups(this, IDC_GROUPS) - { - groups.OnBuildMenu = Callback(this, &CGroupEditDlg::onMenu); - } - - void RefreshGroups() - { - for (auto &it : m_proto->m_arGroups.rev_iter()) - groups.AddItem(it->wszName, 0, (LPARAM)it); - } - - bool OnInitDialog() override - { - pDlg = this; - groups.AddColumn(0, TranslateT("Name"), 300); - RefreshGroups(); - return true; - } - - void OnDestroy() override - { - pDlg = nullptr; - } - - void onMenu(void *) - { - int cur = groups.GetSelectionMark(); - if (cur == -1) - return; - - IcqGroup *pGroup = (IcqGroup *)groups.GetItemData(cur); - - HMENU hMenu = CreatePopupMenu(); - AppendMenu(hMenu, MF_STRING, 1, TranslateT("Rename")); - AppendMenu(hMenu, MF_STRING, 2, TranslateT("Delete")); - - POINT pt; - GetCursorPos(&pt); - int cmd = TrackPopupMenu(hMenu, TPM_RIGHTBUTTON | TPM_RETURNCMD, pt.x, pt.y, 0, m_hwnd, nullptr); - DestroyMenu(hMenu); - - if (cmd == 1) { // rename - ENTER_STRING es = {}; - es.szModuleName = m_proto->m_szModuleName; - es.caption = TranslateT("Enter new group name"); - if (!EnterString(&es)) - return; - - m_proto->Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/renameGroup") - << AIMSID(m_proto) << WCHAR_PARAM("oldGroup", pGroup->wszSrvName) << GROUP_PARAM("newGroup", es.ptszResult)); - - mir_free(es.ptszResult); - } - else if (cmd == 2) { // delete - m_proto->Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/removeGroup") - << AIMSID(m_proto) << WCHAR_PARAM("group", pGroup->wszSrvName)); - } - } -}; - -CGroupEditDlg *CGroupEditDlg::pDlg = nullptr; - -INT_PTR CIcqProto::EditGroups(WPARAM, LPARAM) -{ - (new CGroupEditDlg(this))->Show(); - return 0; -} - -INT_PTR CIcqProto::EditProfile(WPARAM, LPARAM) -{ - if (mir_wstrlen(m_szOwnId)) - Utils_OpenUrlW(CMStringW(FORMAT, L"https://icq.com/people/%s/edit/", (wchar_t*)m_szOwnId)); - return 0; -} - -void RefreshGroups(void) -{ - if (CGroupEditDlg::pDlg != nullptr) - CGroupEditDlg::pDlg->RefreshGroups(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -INT_PTR CIcqProto::GetEmailCount(WPARAM, LPARAM) -{ - if (!m_bOnline) - return 0; - return m_unreadEmails; -} - -INT_PTR CIcqProto::GotoInbox(WPARAM, LPARAM) -{ - Utils_OpenUrl("https://e.mail.ru/messages/inbox"); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::SendMarkRead() -{ - mir_cslock lck(m_csMarkReadQueue); - while (m_arMarkReadQueue.getCount()) { - IcqCacheItem *pUser = m_arMarkReadQueue[0]; - - auto *pReq = new AsyncRapiRequest(this, "setDlgStateWim"); - pReq->params << WCHAR_PARAM("sn", GetUserId(pUser->m_hContact)) << INT64_PARAM("lastRead", getId(pUser->m_hContact, DB_KEY_LASTMSGID)); - Push(pReq); - - m_arMarkReadQueue.remove(0); - } -} - -int CIcqProto::OnDbEventRead(WPARAM, LPARAM hDbEvent) -{ - MCONTACT hContact = db_event_getContact(hDbEvent); - if (!hContact) - return 0; - - // filter out only events of my protocol - const char *szProto = Proto_GetBaseAccountName(hContact); - if (mir_strcmp(szProto, m_szModuleName)) - return 0; - - MarkAsRead(hContact); - return 0; -} - -int CIcqProto::OnGroupChange(WPARAM hContact, LPARAM lParam) -{ - if (!m_bOnline) - return 0; - - CLISTGROUPCHANGE *pParam = (CLISTGROUPCHANGE*)lParam; - if (hContact == 0) { // whole group is changed - if (pParam->pszOldName == nullptr) { - for (auto &it : m_arGroups) - if (it->wszName == pParam->pszNewName) - return 0; - - Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/addGroup") - << AIMSID(this) << GROUP_PARAM("group", pParam->pszNewName)); - } - else if (pParam->pszNewName == nullptr) { - Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/removeGroup") - << AIMSID(this) << GROUP_PARAM("group", pParam->pszOldName)); - } - else { - Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/renameGroup") - << AIMSID(this) << GROUP_PARAM("oldGroup", pParam->pszOldName) << GROUP_PARAM("newGroup", pParam->pszNewName)); - } - } - else MoveContactToGroup(hContact, ptrW(getWStringA(hContact, "IcqGroup")), pParam->pszNewName); - - return 0; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// PS_AddToList - adds a contact to the contact list - -MCONTACT CIcqProto::AddToList(int, PROTOSEARCHRESULT *psr) -{ - if (mir_wstrlen(psr->id.w) == 0) - return 0; - - MCONTACT hContact = CreateContact(psr->id.w, true); - if (psr->nick.w) - setWString(hContact, "Nick", psr->nick.w); - if (psr->firstName.w) - setWString(hContact, "FirstName", psr->firstName.w); - if (psr->lastName.w) - setWString(hContact, "LastName", psr->lastName.w); - - return hContact; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// PSR_AUTH - -int CIcqProto::AuthRecv(MCONTACT, PROTORECVEVENT *pre) -{ - return Proto_AuthRecv(m_szModuleName, pre); -} - -//////////////////////////////////////////////////////////////////////////////////////// -// PSS_AUTHREQUEST - -int CIcqProto::AuthRequest(MCONTACT hContact, const wchar_t* szMessage) -{ - ptrW wszGroup(Clist_GetGroup(hContact)); - if (!wszGroup) - wszGroup = mir_wstrdup(L"General"); - - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/buddylist/addBuddy", &CIcqProto::OnAddBuddy); - pReq << AIMSID(this) << WCHAR_PARAM("authorizationMsg", szMessage) << WCHAR_PARAM("buddy", GetUserId(hContact)) << WCHAR_PARAM("group", wszGroup) << INT_PARAM("preAuthorized", 1); - pReq->hContact = hContact; - Push(pReq); - return 0; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// File operations - -HANDLE CIcqProto::FileAllow(MCONTACT, HANDLE hTransfer, const wchar_t *pwszSavePath) -{ - if (!m_bOnline) - return nullptr; - - auto *ft = (IcqFileTransfer *)hTransfer; - ft->m_wszFileName.Insert(0, pwszSavePath); - ft->pfts.szCurrentFile.w = ft->m_wszFileName.GetBuffer(); - - auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_GET, ft->m_szHost, &CIcqProto::OnFileRecv); - pReq->pUserInfo = ft; - pReq->AddHeader("Sec-Fetch-User", "?1"); - pReq->AddHeader("Sec-Fetch-Site", "cross-site"); - pReq->AddHeader("Sec-Fetch-Mode", "navigate"); - Push(pReq); - - return hTransfer; -} - -int CIcqProto::FileCancel(MCONTACT hContact, HANDLE hTransfer) -{ - ProtoBroadcastAck(hContact, ACKTYPE_FILE, ACKRESULT_FAILED, hTransfer); - - auto *ft = (IcqFileTransfer *)hTransfer; - if (ft->pfts.currentFileTime != 0) - ft->m_bCanceled = true; - else - delete ft; - return 0; -} - -int CIcqProto::FileResume(HANDLE hTransfer, int, const wchar_t *szFilename) -{ - auto *ft = (IcqFileTransfer *)hTransfer; - if (!m_bOnline || ft == nullptr) - return 1; - - if (szFilename != nullptr) { - ft->m_wszFileName = szFilename; - ft->pfts.szCurrentFile.w = ft->m_wszFileName.GetBuffer(); - } - - ::SetEvent(ft->hWaitEvent); - return 0; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// GetCaps - return protocol capabilities bits - -INT_PTR CIcqProto::GetCaps(int type, MCONTACT) -{ - INT_PTR nReturn = 0; - - switch (type) { - case PFLAGNUM_1: - nReturn = PF1_IM | PF1_AUTHREQ | PF1_BASICSEARCH | PF1_ADDSEARCHRES | /*PF1_SEARCHBYNAME | TODO */ - PF1_VISLIST | PF1_FILE | PF1_CONTACT | PF1_SERVERCLIST; - break; - - case PFLAGNUM_2: - return PF2_ONLINE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND | PF2_INVISIBLE; - - case PFLAGNUM_3: - return PF2_ONLINE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND | PF2_INVISIBLE; - - case PFLAGNUM_5: - return PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND | PF2_INVISIBLE; - - case PFLAGNUM_4: - nReturn = PF4_FORCEAUTH | PF4_SUPPORTIDLE | PF4_OFFLINEFILES | PF4_IMSENDOFFLINE | PF4_SUPPORTTYPING | PF4_AVATARS | PF4_SERVERMSGID | PF4_READNOTIFY; - break; - - case PFLAG_UNIQUEIDTEXT: - return (INT_PTR)TranslateT("UIN/e-mail/phone"); - } - - return nReturn; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// GetInfo - retrieves a contact info - -int CIcqProto::GetInfo(MCONTACT hContact, int) -{ - RetrieveUserInfo(hContact); - return 0; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// SearchBasic - searches the contact by UID - -HANDLE CIcqProto::SearchBasic(const wchar_t *pszSearch) -{ - if (!m_bOnline) - return nullptr; - - auto *pReq = new AsyncRapiRequest(this, "search", &CIcqProto::OnSearchResults); - pReq->params << WCHAR_PARAM(*pszSearch == '+' ? "phonenum" : "keyword", pszSearch); - Push(pReq); - - return pReq; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// SendFile - sends a file - -HANDLE CIcqProto::SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) -{ - // we can't send more than one file at a time - if (ppszFiles[1] != 0) - return nullptr; - - struct _stat statbuf; - if (_wstat(ppszFiles[0], &statbuf)) { - debugLogW(L"'%s' is an invalid filename", ppszFiles[0]); - return nullptr; - } - - int iFileId = _wopen(ppszFiles[0], _O_RDONLY | _O_BINARY, _S_IREAD); - if (iFileId < 0) - return nullptr; - - auto *pTransfer = new IcqFileTransfer(hContact, ppszFiles[0]); - pTransfer->pfts.totalFiles = 1; - pTransfer->pfts.currentFileSize = pTransfer->pfts.totalBytes = statbuf.st_size; - pTransfer->m_fileId = iFileId; - if (mir_wstrlen(szDescription)) - pTransfer->m_wszDescr = szDescription; - - auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_GET, "https://files.icq.com/files/init", &CIcqProto::OnFileInit); - pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("client", "icq") << CHAR_PARAM("f", "json") << WCHAR_PARAM("filename", pTransfer->m_wszShortName) - << CHAR_PARAM("k", appId()) << INT_PARAM("size", statbuf.st_size) << INT_PARAM("ts", TS()); - CalcHash(pReq); - pReq->pUserInfo = pTransfer; - Push(pReq); - - return pTransfer; // Failure -} - -//////////////////////////////////////////////////////////////////////////////////////// -// PS_SendMessage - sends a message - -int CIcqProto::SendMsg(MCONTACT hContact, int, const char *pszSrc) -{ - CMStringA szUserid(GetUserId(hContact)); - if (szUserid.IsEmpty()) - return 0; - - int id = InterlockedIncrement(&m_msgId); - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/im/sendIM", &CIcqProto::OnSendMessage); - - auto *pOwn = new IcqOwnMessage(hContact, id, pReq->m_reqId); - pReq->pUserInfo = pOwn; - { - mir_cslock lck(m_csOwnIds); - m_arOwnIds.insert(pOwn); - } - - pReq << AIMSID(this) << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("k", appId()) << CHAR_PARAM("mentions", "") - << CHAR_PARAM("message", pszSrc) << CHAR_PARAM("offlineIM", "true") << CHAR_PARAM("t", szUserid) << INT_PARAM("ts", TS()); - Push(pReq); - return id; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// PS_SetStatus - sets the protocol status - -int CIcqProto::SetStatus(int iNewStatus) -{ - debugLogA("CIcqProto::SetStatus iNewStatus = %d, m_iStatus = %d, m_iDesiredStatus = %d m_hWorkerThread = %p", iNewStatus, m_iStatus, m_iDesiredStatus, m_hWorkerThread); - - if (iNewStatus == m_iStatus) - return 0; - - m_iDesiredStatus = iNewStatus; - int iOldStatus = m_iStatus; - - // go offline - if (iNewStatus == ID_STATUS_OFFLINE) { - if (m_bOnline) - SetServerStatus(ID_STATUS_OFFLINE); - - m_iStatus = m_iDesiredStatus; - setAllContactStatuses(ID_STATUS_OFFLINE, false); - - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); - } - // not logged in? come on - else if (!m_bOnline && !IsStatusConnecting(m_iStatus)) { - m_iStatus = ID_STATUS_CONNECTING; - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); - - if (mir_wstrlen(m_szOwnId) == 0) { - debugLogA("Thread ended, UIN/password are not configured"); - ConnectionFailed(LOGINERR_BADUSERID); - return 0; - } - - if (!RetrievePassword()) { - debugLogA("Thread ended, password is not configured"); - ConnectionFailed(LOGINERR_BADUSERID); - return 0; - } - - CheckPassword(); - } - else if (m_bOnline) { - debugLogA("setting server online status to %d", iNewStatus); - SetServerStatus(iNewStatus); - } - - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// PS_UserIsTyping - sends a UTN notification - -int CIcqProto::UserIsTyping(MCONTACT hContact, int type) -{ - Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/im/setTyping") - << AIMSID(this) << WCHAR_PARAM("t", GetUserId(hContact)) << CHAR_PARAM("typingStatus", (type == PROTOTYPE_SELFTYPING_ON) ? "typing" : "typed")); - return 0; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// PS_SetApparentMode - sets the visibility status - -int CIcqProto::SetApparentMode(MCONTACT hContact, int iMode) -{ - int oldMode = getWord(hContact, "ApparentMode"); - if (oldMode != iMode) { - setWord(hContact, "ApparentMode", iMode); - SetPermitDeny(GetUserId(hContact), iMode != ID_STATUS_OFFLINE); - } - return 0; -} +// ---------------------------------------------------------------------------80 +// ICQ plugin for Miranda Instant Messenger +// ________________________________________ +// +// Copyright © 2000-2001 Richard Hughes, Roland Rabien, Tristan Van de Vreede +// Copyright © 2001-2002 Jon Keating, Richard Hughes +// Copyright © 2002-2004 Martin Öberg, Sam Kothari, Robert Rainwater +// Copyright © 2004-2010 Joe Kucera, George Hazan +// Copyright © 2012-2023 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. +// ----------------------------------------------------------------------------- +// DESCRIPTION: +// +// Protocol Interface Implementation +// ----------------------------------------------------------------------------- + +#include "stdafx.h" + +#include "m_icolib.h" + +#pragma warning(disable:4355) + +static int CompareCache(const IcqCacheItem *p1, const IcqCacheItem *p2) +{ + return mir_wstrcmp(p1->m_aimid, p2->m_aimid); +} + +CIcqProto::CIcqProto(const char *aProtoName, const wchar_t *aUserName) : + PROTO(aProtoName, aUserName), + m_impl(*this), + m_arHttpQueue(10), + m_arOwnIds(1, PtrKeySortT), + m_arCache(20, &CompareCache), + m_arGroups(10, NumericKeySortT), + m_arMarkReadQueue(10, NumericKeySortT), + m_evRequestsQueue(CreateEvent(nullptr, FALSE, FALSE, nullptr)), + m_szOwnId(this, DB_KEY_ID), + m_iStatus1(this, "Status1", ID_STATUS_AWAY), + m_iStatus2(this, "Status2", ID_STATUS_NA), + m_iTimeDiff1(this, "TimeDiff1", 0), + m_iTimeDiff2(this, "TimeDiff2", 0), + m_bHideGroupchats(this, "HideChats", true), + m_bUseTrayIcon(this, "UseTrayIcon", false), + m_bErrorPopups(this, "ShowErrorPopups", true), + m_bLaunchMailbox(this, "LaunchMailbox", true) +{ + db_set_resident(m_szModuleName, DB_KEY_IDLE); + db_set_resident(m_szModuleName, DB_KEY_ONLINETS); + + m_isMra = !stricmp(Proto_GetAccount(m_szModuleName)->szProtoName, "MRA"); + + // services + CreateProtoService(PS_CREATEACCMGRUI, &CIcqProto::CreateAccMgrUI); + + CreateProtoService(PS_GETAVATARCAPS, &CIcqProto::GetAvatarCaps); + CreateProtoService(PS_GETAVATARINFO, &CIcqProto::GetAvatarInfo); + CreateProtoService(PS_GETMYAVATAR, &CIcqProto::GetAvatar); + CreateProtoService(PS_SETMYAVATAR, &CIcqProto::SetAvatar); + + CreateProtoService(PS_MENU_LOADHISTORY, &CIcqProto::OnMenuLoadHistory); + CreateProtoService(PS_GETUNREADEMAILCOUNT, &CIcqProto::GetEmailCount); + CreateProtoService(PS_GOTO_INBOX, &CIcqProto::GotoInbox); + + // events + HookProtoEvent(ME_CLIST_GROUPCHANGE, &CIcqProto::OnGroupChange); + HookProtoEvent(ME_DB_EVENT_MARKED_READ, &CIcqProto::OnDbEventRead); + HookProtoEvent(ME_GC_EVENT, &CIcqProto::GroupchatEventHook); + HookProtoEvent(ME_GC_BUILDMENU, &CIcqProto::GroupchatMenuHook); + HookProtoEvent(ME_OPT_INITIALISE, &CIcqProto::OnOptionsInit); + + // group chats + GCREGISTER gcr = {}; + gcr.dwFlags = GC_TYPNOTIF | GC_CHANMGR; + gcr.ptszDispName = m_tszUserName; + gcr.pszModule = m_szModuleName; + Chat_Register(&gcr); + + // netlib handle + NETLIBUSER nlu = {}; + nlu.szSettingsModule = m_szModuleName; + nlu.flags = NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; + nlu.szDescriptiveName.w = m_tszUserName; + m_hNetlibUser = Netlib_RegisterUser(&nlu); + + // this was previously an old ICQ account + ptrW wszUin(GetUIN(0)); + if (wszUin != nullptr) { + delSetting("UIN"); + + m_szOwnId = wszUin; + + for (auto &it : AccContacts()) + delSetting(it, "e-mail"); + } + // this was previously an old MRA account + else { + CMStringW wszEmail(getMStringW("e-mail")); + if (!wszEmail.IsEmpty()) { + m_szOwnId = wszEmail; + delSetting("e-mail"); + } + } + + m_hWorkerThread = ForkThreadEx(&CIcqProto::ServerThread, nullptr, nullptr); +} + +CIcqProto::~CIcqProto() +{ + ::CloseHandle(m_evRequestsQueue); +} + +//////////////////////////////////////////////////////////////////////////////////////// +// OnModulesLoadedEx - performs hook registration + +void CIcqProto::OnModulesLoaded() +{ + InitContactCache(); + + HookProtoEvent(ME_USERINFO_INITIALISE, &CIcqProto::OnUserInfoInit); + + // load custom smilies + CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers\\*.png", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName); + SMADD_CONT cont = { 2, m_szModuleName, wszPath }; + CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); +} + +void CIcqProto::OnShutdown() +{ + m_bTerminated = true; +} + +void CIcqProto::OnContactAdded(MCONTACT hContact) +{ + CMStringW wszId(getMStringW(hContact, DB_KEY_ID)); + if (!wszId.IsEmpty() && !FindContactByUIN(wszId)) { + mir_cslock l(m_csCache); + m_arCache.insert(new IcqCacheItem(wszId, hContact)); + } +} + +void CIcqProto::OnContactDeleted(MCONTACT hContact) +{ + CMStringW szId(GetUserId(hContact)); + if (!isChatRoom(hContact)) + m_arCache.remove(FindContactByUIN(szId)); + + Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/removeBuddy") + << AIMSID(this) << WCHAR_PARAM("buddy", szId) << INT_PARAM("allGroups", 1)); +} + +void CIcqProto::OnEventEdited(MCONTACT, MEVENT) +{ + +} + +INT_PTR CIcqProto::OnMenuLoadHistory(WPARAM hContact, LPARAM) +{ + delSetting(hContact, DB_KEY_LASTMSGID); + + RetrieveUserHistory(hContact, 1, true); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::OnBuildProtoMenu() +{ + CMenuItem mi(&g_plugin); + mi.root = Menu_GetProtocolRoot(this); + mi.flags = CMIF_UNMOVABLE; + + // "Bookmarks..." + mi.pszService = "/UploadGroups"; + CreateProtoService(mi.pszService, &CIcqProto::UploadGroups); + mi.name.a = LPGEN("Synchronize server groups"); + mi.position = 200001; + mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_GROUP); + m_hUploadGroups = Menu_AddProtoMenuItem(&mi, m_szModuleName); + + mi.pszService = "/EditGroups"; + CreateProtoService(mi.pszService, &CIcqProto::EditGroups); + mi.name.a = LPGEN("Edit server groups"); + mi.position = 200002; + mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_GROUP); + Menu_AddProtoMenuItem(&mi, m_szModuleName); + + mi.pszService = "/EditProfile"; + CreateProtoService(mi.pszService, &CIcqProto::EditProfile); + mi.name.a = LPGEN("Edit my web profile"); + mi.position = 210001; + mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_MIRANDAWEB); + Menu_AddProtoMenuItem(&mi, m_szModuleName); + + Menu_ShowItem(m_hUploadGroups, false); +} + +INT_PTR CIcqProto::UploadGroups(WPARAM, LPARAM) +{ + for (auto &it : AccContacts()) { + if (isChatRoom(it)) + continue; + + ptrW wszIcqGroup(getWStringA(it, "IcqGroup")); + if (wszIcqGroup == nullptr) + continue; + + ptrW wszMirGroup(Clist_GetGroup(it)); + if (!wszMirGroup) + wszMirGroup = mir_wstrdup(L"General"); + if (mir_wstrcmp(wszIcqGroup, wszMirGroup)) + MoveContactToGroup(it, wszIcqGroup, wszMirGroup); + } + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +class CGroupEditDlg : public CIcqDlgBase +{ + CCtrlListView groups; + +public: + + static CGroupEditDlg *pDlg; + + CGroupEditDlg(CIcqProto *ppro) : + CIcqDlgBase(ppro, IDD_EDITGROUPS), + groups(this, IDC_GROUPS) + { + groups.OnBuildMenu = Callback(this, &CGroupEditDlg::onMenu); + } + + void RefreshGroups() + { + for (auto &it : m_proto->m_arGroups.rev_iter()) + groups.AddItem(it->wszName, 0, (LPARAM)it); + } + + bool OnInitDialog() override + { + pDlg = this; + groups.AddColumn(0, TranslateT("Name"), 300); + RefreshGroups(); + return true; + } + + void OnDestroy() override + { + pDlg = nullptr; + } + + void onMenu(void *) + { + int cur = groups.GetSelectionMark(); + if (cur == -1) + return; + + IcqGroup *pGroup = (IcqGroup *)groups.GetItemData(cur); + + HMENU hMenu = CreatePopupMenu(); + AppendMenu(hMenu, MF_STRING, 1, TranslateT("Rename")); + AppendMenu(hMenu, MF_STRING, 2, TranslateT("Delete")); + + POINT pt; + GetCursorPos(&pt); + int cmd = TrackPopupMenu(hMenu, TPM_RIGHTBUTTON | TPM_RETURNCMD, pt.x, pt.y, 0, m_hwnd, nullptr); + DestroyMenu(hMenu); + + if (cmd == 1) { // rename + ENTER_STRING es = {}; + es.szModuleName = m_proto->m_szModuleName; + es.caption = TranslateT("Enter new group name"); + if (!EnterString(&es)) + return; + + m_proto->Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/renameGroup") + << AIMSID(m_proto) << WCHAR_PARAM("oldGroup", pGroup->wszSrvName) << GROUP_PARAM("newGroup", es.ptszResult)); + + mir_free(es.ptszResult); + } + else if (cmd == 2) { // delete + m_proto->Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/removeGroup") + << AIMSID(m_proto) << WCHAR_PARAM("group", pGroup->wszSrvName)); + } + } +}; + +CGroupEditDlg *CGroupEditDlg::pDlg = nullptr; + +INT_PTR CIcqProto::EditGroups(WPARAM, LPARAM) +{ + (new CGroupEditDlg(this))->Show(); + return 0; +} + +INT_PTR CIcqProto::EditProfile(WPARAM, LPARAM) +{ + if (mir_wstrlen(m_szOwnId)) + Utils_OpenUrlW(CMStringW(FORMAT, L"https://icq.com/people/%s/edit/", (wchar_t*)m_szOwnId)); + return 0; +} + +void RefreshGroups(void) +{ + if (CGroupEditDlg::pDlg != nullptr) + CGroupEditDlg::pDlg->RefreshGroups(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR CIcqProto::GetEmailCount(WPARAM, LPARAM) +{ + if (!m_bOnline) + return 0; + return m_unreadEmails; +} + +INT_PTR CIcqProto::GotoInbox(WPARAM, LPARAM) +{ + Utils_OpenUrl("https://e.mail.ru/messages/inbox"); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::SendMarkRead() +{ + mir_cslock lck(m_csMarkReadQueue); + while (m_arMarkReadQueue.getCount()) { + IcqCacheItem *pUser = m_arMarkReadQueue[0]; + + auto *pReq = new AsyncRapiRequest(this, "setDlgStateWim"); + pReq->params << WCHAR_PARAM("sn", GetUserId(pUser->m_hContact)) << INT64_PARAM("lastRead", getId(pUser->m_hContact, DB_KEY_LASTMSGID)); + Push(pReq); + + m_arMarkReadQueue.remove(0); + } +} + +int CIcqProto::OnDbEventRead(WPARAM, LPARAM hDbEvent) +{ + MCONTACT hContact = db_event_getContact(hDbEvent); + if (!hContact) + return 0; + + // filter out only events of my protocol + const char *szProto = Proto_GetBaseAccountName(hContact); + if (mir_strcmp(szProto, m_szModuleName)) + return 0; + + MarkAsRead(hContact); + return 0; +} + +int CIcqProto::OnGroupChange(WPARAM hContact, LPARAM lParam) +{ + if (!m_bOnline) + return 0; + + CLISTGROUPCHANGE *pParam = (CLISTGROUPCHANGE*)lParam; + if (hContact == 0) { // whole group is changed + if (pParam->pszOldName == nullptr) { + for (auto &it : m_arGroups) + if (it->wszName == pParam->pszNewName) + return 0; + + Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/addGroup") + << AIMSID(this) << GROUP_PARAM("group", pParam->pszNewName)); + } + else if (pParam->pszNewName == nullptr) { + Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/removeGroup") + << AIMSID(this) << GROUP_PARAM("group", pParam->pszOldName)); + } + else { + Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/renameGroup") + << AIMSID(this) << GROUP_PARAM("oldGroup", pParam->pszOldName) << GROUP_PARAM("newGroup", pParam->pszNewName)); + } + } + else MoveContactToGroup(hContact, ptrW(getWStringA(hContact, "IcqGroup")), pParam->pszNewName); + + return 0; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// PS_AddToList - adds a contact to the contact list + +MCONTACT CIcqProto::AddToList(int, PROTOSEARCHRESULT *psr) +{ + if (mir_wstrlen(psr->id.w) == 0) + return 0; + + MCONTACT hContact = CreateContact(psr->id.w, true); + if (psr->nick.w) + setWString(hContact, "Nick", psr->nick.w); + if (psr->firstName.w) + setWString(hContact, "FirstName", psr->firstName.w); + if (psr->lastName.w) + setWString(hContact, "LastName", psr->lastName.w); + + return hContact; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// PSR_AUTH + +int CIcqProto::AuthRecv(MCONTACT, PROTORECVEVENT *pre) +{ + return Proto_AuthRecv(m_szModuleName, pre); +} + +//////////////////////////////////////////////////////////////////////////////////////// +// PSS_AUTHREQUEST + +int CIcqProto::AuthRequest(MCONTACT hContact, const wchar_t* szMessage) +{ + ptrW wszGroup(Clist_GetGroup(hContact)); + if (!wszGroup) + wszGroup = mir_wstrdup(L"General"); + + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/buddylist/addBuddy", &CIcqProto::OnAddBuddy); + pReq << AIMSID(this) << WCHAR_PARAM("authorizationMsg", szMessage) << WCHAR_PARAM("buddy", GetUserId(hContact)) << WCHAR_PARAM("group", wszGroup) << INT_PARAM("preAuthorized", 1); + pReq->hContact = hContact; + Push(pReq); + return 0; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// File operations + +HANDLE CIcqProto::FileAllow(MCONTACT, HANDLE hTransfer, const wchar_t *pwszSavePath) +{ + if (!m_bOnline) + return nullptr; + + auto *ft = (IcqFileTransfer *)hTransfer; + ft->m_wszFileName.Insert(0, pwszSavePath); + ft->pfts.szCurrentFile.w = ft->m_wszFileName.GetBuffer(); + + auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_GET, ft->m_szHost, &CIcqProto::OnFileRecv); + pReq->pUserInfo = ft; + pReq->AddHeader("Sec-Fetch-User", "?1"); + pReq->AddHeader("Sec-Fetch-Site", "cross-site"); + pReq->AddHeader("Sec-Fetch-Mode", "navigate"); + Push(pReq); + + return hTransfer; +} + +int CIcqProto::FileCancel(MCONTACT hContact, HANDLE hTransfer) +{ + ProtoBroadcastAck(hContact, ACKTYPE_FILE, ACKRESULT_FAILED, hTransfer); + + auto *ft = (IcqFileTransfer *)hTransfer; + if (ft->pfts.currentFileTime != 0) + ft->m_bCanceled = true; + else + delete ft; + return 0; +} + +int CIcqProto::FileResume(HANDLE hTransfer, int, const wchar_t *szFilename) +{ + auto *ft = (IcqFileTransfer *)hTransfer; + if (!m_bOnline || ft == nullptr) + return 1; + + if (szFilename != nullptr) { + ft->m_wszFileName = szFilename; + ft->pfts.szCurrentFile.w = ft->m_wszFileName.GetBuffer(); + } + + ::SetEvent(ft->hWaitEvent); + return 0; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// GetCaps - return protocol capabilities bits + +INT_PTR CIcqProto::GetCaps(int type, MCONTACT) +{ + INT_PTR nReturn = 0; + + switch (type) { + case PFLAGNUM_1: + nReturn = PF1_IM | PF1_AUTHREQ | PF1_BASICSEARCH | PF1_ADDSEARCHRES | /*PF1_SEARCHBYNAME | TODO */ + PF1_VISLIST | PF1_FILE | PF1_CONTACT | PF1_SERVERCLIST; + break; + + case PFLAGNUM_2: + return PF2_ONLINE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND | PF2_INVISIBLE; + + case PFLAGNUM_3: + return PF2_ONLINE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND | PF2_INVISIBLE; + + case PFLAGNUM_5: + return PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND | PF2_INVISIBLE; + + case PFLAGNUM_4: + nReturn = PF4_FORCEAUTH | PF4_SUPPORTIDLE | PF4_OFFLINEFILES | PF4_IMSENDOFFLINE | PF4_SUPPORTTYPING | PF4_AVATARS | PF4_SERVERMSGID | PF4_READNOTIFY; + break; + + case PFLAG_UNIQUEIDTEXT: + return (INT_PTR)TranslateT("UIN/e-mail/phone"); + } + + return nReturn; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// GetInfo - retrieves a contact info + +int CIcqProto::GetInfo(MCONTACT hContact, int) +{ + RetrieveUserInfo(hContact); + return 0; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// SearchBasic - searches the contact by UID + +HANDLE CIcqProto::SearchBasic(const wchar_t *pszSearch) +{ + if (!m_bOnline) + return nullptr; + + auto *pReq = new AsyncRapiRequest(this, "search", &CIcqProto::OnSearchResults); + pReq->params << WCHAR_PARAM(*pszSearch == '+' ? "phonenum" : "keyword", pszSearch); + Push(pReq); + + return pReq; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// SendFile - sends a file + +HANDLE CIcqProto::SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) +{ + // we can't send more than one file at a time + if (ppszFiles[1] != 0) + return nullptr; + + struct _stat statbuf; + if (_wstat(ppszFiles[0], &statbuf)) { + debugLogW(L"'%s' is an invalid filename", ppszFiles[0]); + return nullptr; + } + + int iFileId = _wopen(ppszFiles[0], _O_RDONLY | _O_BINARY, _S_IREAD); + if (iFileId < 0) + return nullptr; + + auto *pTransfer = new IcqFileTransfer(hContact, ppszFiles[0]); + pTransfer->pfts.totalFiles = 1; + pTransfer->pfts.currentFileSize = pTransfer->pfts.totalBytes = statbuf.st_size; + pTransfer->m_fileId = iFileId; + if (mir_wstrlen(szDescription)) + pTransfer->m_wszDescr = szDescription; + + auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_GET, "https://files.icq.com/files/init", &CIcqProto::OnFileInit); + pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("client", "icq") << CHAR_PARAM("f", "json") << WCHAR_PARAM("filename", pTransfer->m_wszShortName) + << CHAR_PARAM("k", appId()) << INT_PARAM("size", statbuf.st_size) << INT_PARAM("ts", TS()); + CalcHash(pReq); + pReq->pUserInfo = pTransfer; + Push(pReq); + + return pTransfer; // Failure +} + +//////////////////////////////////////////////////////////////////////////////////////// +// PS_SendMessage - sends a message + +int CIcqProto::SendMsg(MCONTACT hContact, int, const char *pszSrc) +{ + CMStringA szUserid(GetUserId(hContact)); + if (szUserid.IsEmpty()) + return 0; + + int id = InterlockedIncrement(&m_msgId); + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/im/sendIM", &CIcqProto::OnSendMessage); + + auto *pOwn = new IcqOwnMessage(hContact, id, pReq->m_reqId); + pReq->pUserInfo = pOwn; + { + mir_cslock lck(m_csOwnIds); + m_arOwnIds.insert(pOwn); + } + + pReq << AIMSID(this) << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("k", appId()) << CHAR_PARAM("mentions", "") + << CHAR_PARAM("message", pszSrc) << CHAR_PARAM("offlineIM", "true") << CHAR_PARAM("t", szUserid) << INT_PARAM("ts", TS()); + Push(pReq); + return id; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// PS_SetStatus - sets the protocol status + +int CIcqProto::SetStatus(int iNewStatus) +{ + debugLogA("CIcqProto::SetStatus iNewStatus = %d, m_iStatus = %d, m_iDesiredStatus = %d m_hWorkerThread = %p", iNewStatus, m_iStatus, m_iDesiredStatus, m_hWorkerThread); + + if (iNewStatus == m_iStatus) + return 0; + + m_iDesiredStatus = iNewStatus; + int iOldStatus = m_iStatus; + + // go offline + if (iNewStatus == ID_STATUS_OFFLINE) { + if (m_bOnline) + SetServerStatus(ID_STATUS_OFFLINE); + + m_iStatus = m_iDesiredStatus; + setAllContactStatuses(ID_STATUS_OFFLINE, false); + + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); + } + // not logged in? come on + else if (!m_bOnline && !IsStatusConnecting(m_iStatus)) { + m_iStatus = ID_STATUS_CONNECTING; + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); + + if (mir_wstrlen(m_szOwnId) == 0) { + debugLogA("Thread ended, UIN/password are not configured"); + ConnectionFailed(LOGINERR_BADUSERID); + return 0; + } + + if (!RetrievePassword()) { + debugLogA("Thread ended, password is not configured"); + ConnectionFailed(LOGINERR_BADUSERID); + return 0; + } + + CheckPassword(); + } + else if (m_bOnline) { + debugLogA("setting server online status to %d", iNewStatus); + SetServerStatus(iNewStatus); + } + + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// PS_UserIsTyping - sends a UTN notification + +int CIcqProto::UserIsTyping(MCONTACT hContact, int type) +{ + Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/im/setTyping") + << AIMSID(this) << WCHAR_PARAM("t", GetUserId(hContact)) << CHAR_PARAM("typingStatus", (type == PROTOTYPE_SELFTYPING_ON) ? "typing" : "typed")); + return 0; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// PS_SetApparentMode - sets the visibility status + +int CIcqProto::SetApparentMode(MCONTACT hContact, int iMode) +{ + int oldMode = getWord(hContact, "ApparentMode"); + if (oldMode != iMode) { + setWord(hContact, "ApparentMode", iMode); + SetPermitDeny(GetUserId(hContact), iMode != ID_STATUS_OFFLINE); + } + return 0; +} diff --git a/protocols/ICQ-WIM/src/proto.h b/protocols/ICQ-WIM/src/proto.h index b909a0d74a..20636cc4c6 100644 --- a/protocols/ICQ-WIM/src/proto.h +++ b/protocols/ICQ-WIM/src/proto.h @@ -1,494 +1,494 @@ -// ---------------------------------------------------------------------------80 -// ICQ plugin for Miranda Instant Messenger -// ________________________________________ -// -// Copyright © 2000-2001 Richard Hughes, Roland Rabien, Tristan Van de Vreede -// Copyright © 2001-2002 Jon Keating, Richard Hughes -// Copyright © 2002-2004 Martin Öberg, Sam Kothari, Robert Rainwater -// Copyright © 2004-2010 Joe Kucera, George Hazan -// Copyright © 2012-2022 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. -// ----------------------------------------------------------------------------- -// DESCRIPTION: -// -// Protocol Interface declarations -// ----------------------------------------------------------------------------- - -#ifndef _ICQ_PROTO_H_ -#define _ICQ_PROTO_H_ - -#include "m_system.h" -#include "m_protoint.h" - -#define MRA_APP_ID "ic1pzYNtEU6dDnEQ" -#define ICQ_APP_ID "ic1nmMjqg7Yu-0hL" -#define ICQ_API_SERVER "https://u.icq.net/wim" -#define ICQ_FILE_SERVER "https://u.icq.net/files/api/v1.1" -#define ICQ_FAKE_EVENT_ID 0xBABAEB -#define ICQ_ROBUST_SERVER "https://u.icq.net/rapi" - -#define PS_DUMMY "/DoNothing" -#define PS_GOTO_INBOX "/GotoInbox" - -#define WIM_CAP_VOIP_VOICE "094613504c7f11d18222444553540000" -#define WIM_CAP_VOIP_VIDEO "094613514c7f11d18222444553540000" -#define WIM_CAP_FILETRANSFER "094613434c7f11d18222444553540000" -#define WIM_CAP_UNIQ_REQ_ID "094613534c7f11d18222444553540000" -#define WIM_CAP_EMOJI "094613544c7f11d18222444553540000" -#define WIM_CAP_MENTIONS "0946135b4c7f11d18222444553540000" -#define WIM_CAP_MAIL_NOTIFICATIONS "094613594c7f11d18222444553540000" -#define WIM_CAP_INTRO_DLG_STATE "0946135a4c7f11d18222444553540000" - -#define NG_CAP_SECUREIM "4d69724e47536563757265494d000000" - -typedef CProtoDlgBase CIcqDlgBase; - -struct AIMSID -{ - AIMSID(CIcqProto *_ppro) : - m_ppro(_ppro) - {} - - CIcqProto *m_ppro; -}; - -enum ChatMenuItems -{ - IDM_INVITE = 10, IDM_LEAVE -}; - -struct IcqFileInfo -{ - IcqFileInfo(const std::string &pszUrl, const CMStringW &pwszDescr, uint32_t dwSize) : - szUrl(pszUrl.c_str()), - wszDescr(pwszDescr), - dwFileSize(dwSize) - {} - - CMStringA szUrl; - CMStringW wszDescr; - uint32_t dwFileSize; - bool bIsSticker = false; -}; - -struct IcqGroup -{ - IcqGroup(int _p1, const CMStringW &_p2) : - id(_p1), - wszSrvName(_p2) - { - SetName(_p2); - } - - int id; - int level; - CMStringW wszName, wszSrvName; - - void SetName(const CMStringW &str) - { - wszName = str; - level = wszName.SpanIncluding(L">").GetLength(); - if (level != 0) - wszName.Delete(0, level); - wszName.Replace(L">", L"\\"); - } -}; - -struct IcqCacheItem : public MZeroedObject -{ - IcqCacheItem(const CMStringW &wszId, MCONTACT _contact) : - m_aimid(wszId), - m_hContact(_contact) - {} - - CMStringW m_aimid; - MCONTACT m_hContact; - bool m_bInList; - __int64 m_iProcessedMsgId; - int m_iApparentMode; - time_t m_timer1, m_timer2; -}; - -struct IcqOwnMessage -{ - IcqOwnMessage(MCONTACT _hContact, int _msgid, const char *guid) - : m_hContact(_hContact), m_msgid(_msgid) - { - strncpy_s(m_guid, guid, _TRUNCATE); - } - - MCONTACT m_hContact; - int m_msgid; - char m_guid[50]; -}; - -struct IcqConn -{ - HNETLIBCONN s; - int lastTs, timeout; -}; - -struct IcqFileTransfer : public MZeroedObject -{ - // create an object for receiving - IcqFileTransfer(MCONTACT hContact, const char *pszUrl) : - m_szHost(pszUrl) - { - pfts.hContact = hContact; - pfts.totalFiles = 1; - pfts.flags = PFTS_UNICODE | PFTS_RECEIVING; - - ptrW pwszFileName(mir_utf8decodeW(pszUrl)); - if (pwszFileName == nullptr) - pwszFileName = mir_a2u(pszUrl); - - const wchar_t *p = wcsrchr(pwszFileName, '/'); - m_wszFileName = (p == nullptr) ? pwszFileName : p + 1; - m_wszShortName = m_wszFileName; - } - - // create an object for sending - IcqFileTransfer(MCONTACT hContact, const wchar_t *pwszFileName) : - m_wszFileName(pwszFileName) - { - pfts.flags = PFTS_UNICODE | PFTS_SENDING; - pfts.hContact = hContact; - pfts.szCurrentFile.w = m_wszFileName.GetBuffer(); - - const wchar_t *p = wcsrchr(pfts.szCurrentFile.w, '\\'); - if (pwszFileName != nullptr) - p++; - else - p = pfts.szCurrentFile.w; - m_wszShortName = p; - } - - ~IcqFileTransfer() - { - if (m_fileId >= 0) - _close(m_fileId); - } - - bool m_bCanceled = false, m_bStarted = false; - int m_fileId = -1; - CMStringA m_szHost; - CMStringW m_wszFileName, m_wszDescr; - const wchar_t *m_wszShortName; - PROTOFILETRANSFERSTATUS pfts; - HANDLE hWaitEvent; - - void FillHeaders(AsyncHttpRequest *pReq) - { - pReq->AddHeader("Content-Type", "application/octet-stream"); - pReq->AddHeader("Content-Disposition", CMStringA(FORMAT, "attachment; filename=\"%s\"", T2Utf(m_wszShortName).get())); - - uint32_t dwPortion = pfts.currentFileSize - pfts.currentFileProgress; - if (dwPortion > 1000000) - dwPortion = 1000000; - - pReq->AddHeader("Content-Range", CMStringA(FORMAT, "bytes %lld-%lld/%lld", pfts.currentFileProgress, pfts.currentFileProgress + dwPortion - 1, pfts.currentFileSize)); - pReq->AddHeader("Content-Length", CMStringA(FORMAT, "%d", dwPortion)); - - pReq->dataLength = dwPortion; - pReq->pData = (char*)mir_alloc(dwPortion); - _lseek(m_fileId, pfts.currentFileProgress, SEEK_SET); - _read(m_fileId, pReq->pData, dwPortion); - - pfts.currentFileProgress += dwPortion; - pfts.totalProgress += dwPortion; - } -}; - -class CIcqProto : public PROTO -{ - friend struct AsyncRapiRequest; - - class CIcqProtoImpl - { - friend class CIcqProto; - - CIcqProto &m_proto; - CTimer m_heartBeat, m_markRead; - - void OnHeartBeat(CTimer *) { - m_proto.CheckStatus(); - } - - void OnMarkRead(CTimer *pTimer) { - m_proto.SendMarkRead(); - pTimer->Stop(); - } - - CIcqProtoImpl(CIcqProto &pro) : - m_proto(pro), - m_markRead(Miranda_GetSystemWindow(), UINT_PTR(this)), - m_heartBeat(Miranda_GetSystemWindow(), UINT_PTR(this) + 1) - { - m_markRead.OnEvent = Callback(this, &CIcqProtoImpl::OnMarkRead); - m_heartBeat.OnEvent = Callback(this, &CIcqProtoImpl::OnHeartBeat); - } - } m_impl; - - friend struct CIcqRegistrationDlg; - friend class CGroupchatInviteDlg; - friend class CEditIgnoreListDlg; - friend class CIcqEnterLoginDlg; - friend class CIcqOptionsDlg; - friend class CGroupEditDlg; - - friend AsyncHttpRequest* operator <<(AsyncHttpRequest*, const AIMSID&); - - bool m_bOnline, m_bTerminated, m_bFirstBos, m_isMra, m_bError462; - int m_iTimeShift; - - MCONTACT CheckOwnMessage(const CMStringA &reqId, const CMStringA &msgId, bool bRemove); - void CheckPassword(void); - void ConnectionFailed(int iReason, int iErrorCode = 0); - void EmailNotification(const wchar_t *pwszText); - void GetPermitDeny(); - wchar_t* GetUIN(MCONTACT hContact); - void MarkAsRead(MCONTACT hContact); - void MoveContactToGroup(MCONTACT hContact, const wchar_t *pwszGroup, const wchar_t *pwszNewGroup); - bool RetrievePassword(); - void RetrieveUserHistory(MCONTACT, __int64 startMsgId, bool bCreateRead); - void RetrieveUserInfo(MCONTACT hContact); - void SendMrimLogin(NETLIBHTTPREQUEST *pReply); - void SetServerStatus(int iNewStatus); - void ShutdownSession(void); - void StartSession(void); - - void CheckAvatarChange(MCONTACT hContact, const JSONNode&); - IcqFileInfo* CheckFile(MCONTACT hContact, CMStringW &wszFileName, bool &bIsFile); - void CheckLastId(MCONTACT hContact, const JSONNode&); - void Json2int(MCONTACT, const JSONNode&, const char *szJson, const char *szSetting, bool bIsPartial); - void Json2string(MCONTACT, const JSONNode&, const char *szJson, const char *szSetting, bool bIsPartial); - MCONTACT ParseBuddyInfo(const JSONNode &buddy, MCONTACT hContact = INVALID_CONTACT_ID, bool bIsPartial = false); - void ParseMessage(MCONTACT hContact, __int64 &lastMsgId, const JSONNode &msg, bool bCreateRead, bool bLocalTime); - int StatusFromPresence(const JSONNode &presence, MCONTACT hContact); - - void OnLoggedIn(void); - void OnLoggedOut(void); - - mir_cs m_csMarkReadQueue; - LIST m_arMarkReadQueue; - void SendMarkRead(); - - __int64 getId(MCONTACT hContact, const char *szSetting); - void setId(MCONTACT hContact, const char *szSetting, __int64 iValue); - - void OnAddBuddy(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnAddClient(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnCheckMraAuth(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnCheckMraAuthFinal(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnCheckMrimLogin(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnCheckPassword(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnCheckPhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnFetchEvents(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnFileContinue(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnFileInit(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnFileInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnFileRecv(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnGenToken(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnGetChatInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnGetPermitDeny(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnGetSticker(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnGetUserHistory(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnGetUserInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnLoginViaPhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnNormalizePhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnReceiveAvatar(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnSearchResults(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnSendMessage(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnSessionEnd(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnStartSession(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnValidateSms(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - - void ProcessBuddyList(const JSONNode &pRoot); - void ProcessDiff(const JSONNode &pRoot); - void ProcessEvent(const JSONNode &pRoot); - void ProcessGroupChat(const JSONNode &pRoot); - void ProcessHistData(const JSONNode &pRoot); - void ProcessImState(const JSONNode &pRoot); - void ProcessMyInfo(const JSONNode &pRoot); - void ProcessNotification(const JSONNode &pRoot); - void ProcessPermissions(const JSONNode &pRoot); - void ProcessPresence(const JSONNode &pRoot); - void ProcessSessionEnd(const JSONNode &pRoot); - void ProcessTyping(const JSONNode &pRoot); - - IcqConn m_ConnPool[CONN_LAST]; - CMStringA m_szPassword; - CMStringA m_szSessionKey; - CMStringA m_szAToken; - CMStringA m_szRToken; - CMStringA m_fetchBaseURL; - CMStringA m_aimsid; - CMStringA m_szMraCookie; - LONG m_msgId = 1; - int m_iRClientId; - HGENMENU m_hUploadGroups; - - mir_cs m_csOwnIds; - OBJLIST m_arOwnIds; - - OBJLIST m_arGroups; - - int m_unreadEmails = -1; - CMStringA m_szMailBox; - - bool m_bIgnoreListEmpty = true; - bool m_bRememberPwd; // store password in a database - bool m_bDlgActive; - - //////////////////////////////////////////////////////////////////////////////////////// - // group chats - - int __cdecl GroupchatEventHook(WPARAM, LPARAM); - int __cdecl GroupchatMenuHook(WPARAM, LPARAM); - - void Chat_ProcessLogMenu(SESSION_INFO *si, int); - void Chat_SendPrivateMessage(GCHOOK *gch); - - void InviteUserToChat(SESSION_INFO *si); - void LeaveDestroyChat(SESSION_INFO *si); - void LoadChatInfo(SESSION_INFO *si); - - //////////////////////////////////////////////////////////////////////////////////////// - // http queue - - mir_cs m_csHttpQueue; - HANDLE m_evRequestsQueue; - LIST m_arHttpQueue; - - void CalcHash(AsyncHttpRequest*); - void DropQueue(); - bool ExecuteRequest(AsyncHttpRequest*); - bool IsQueueEmpty(); - void Push(MHttpRequest*); - bool RefreshRobustToken(AsyncHttpRequest *pReq); - - //////////////////////////////////////////////////////////////////////////////////////// - // cache - - mir_cs m_csCache; - OBJLIST m_arCache; - - void InitContactCache(void); - IcqCacheItem* FindContactByUIN(const CMStringW &pwszId); - MCONTACT CreateContact(const CMStringW &pwszId, bool bTemporary); - - void GetAvatarFileName(MCONTACT hContact, wchar_t *pszDest, size_t cbLen); - - //////////////////////////////////////////////////////////////////////////////////////// - // threads - - HANDLE m_hWorkerThread; - void __cdecl ServerThread(void*); - void __cdecl PollThread(void*); - - //////////////////////////////////////////////////////////////////////////////////////// - // services - - INT_PTR __cdecl GetAvatar(WPARAM, LPARAM); - INT_PTR __cdecl GetAvatarCaps(WPARAM, LPARAM); - INT_PTR __cdecl GetAvatarInfo(WPARAM, LPARAM); - INT_PTR __cdecl SetAvatar(WPARAM, LPARAM); - - INT_PTR __cdecl CreateAccMgrUI(WPARAM, LPARAM); - INT_PTR __cdecl EditGroups(WPARAM, LPARAM); - INT_PTR __cdecl EditProfile(WPARAM, LPARAM); - INT_PTR __cdecl GetEmailCount(WPARAM, LPARAM); - INT_PTR __cdecl GotoInbox(WPARAM, LPARAM); - INT_PTR __cdecl UploadGroups(WPARAM, LPARAM); - - INT_PTR __cdecl OnMenuLoadHistory(WPARAM, LPARAM); - - //////////////////////////////////////////////////////////////////////////////////////// - // events - - int __cdecl OnGroupChange(WPARAM, LPARAM); - int __cdecl OnDbEventRead(WPARAM, LPARAM); - int __cdecl OnOptionsInit(WPARAM, LPARAM); - int __cdecl OnUserInfoInit(WPARAM, LPARAM); - - //////////////////////////////////////////////////////////////////////////////////////// - // PROTO_INTERFACE - - MCONTACT AddToList( int flags, PROTOSEARCHRESULT *psr) override; - - int AuthRecv(MCONTACT, PROTORECVEVENT *pre) override; - int AuthRequest(MCONTACT hContact, const wchar_t *szMessage) override; - - INT_PTR GetCaps(int type, MCONTACT hContact = NULL) override; - int GetInfo(MCONTACT hContact, int infoType) override; - - HANDLE SearchBasic(const wchar_t *id) override; - - HANDLE FileAllow(MCONTACT hContact, HANDLE hTransfer, const wchar_t *szPath) override; - int FileCancel(MCONTACT hContact, HANDLE hTransfer) override; - int FileResume(HANDLE hTransfer, int action, const wchar_t *szFilename) override; - - HANDLE SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) override; - int SendMsg(MCONTACT hContact, int flags, const char *msg) override; - - int SetApparentMode(MCONTACT hContact, int mode) override; - int SetStatus(int iNewStatus) override; - - int UserIsTyping(MCONTACT hContact, int type) override; - - void OnBuildProtoMenu(void) override; - void OnContactAdded(MCONTACT) override; - void OnContactDeleted(MCONTACT) override; - void OnEventEdited(MCONTACT, MEVENT) override; - void OnModulesLoaded() override; - void OnShutdown() override; - -public: - CIcqProto(const char*, const wchar_t*); - ~CIcqProto(); - - CMOption m_szOwnId; // our own aim id - CMOption m_bHideGroupchats; // don't pop up group chat windows on startup - CMOption m_bUseTrayIcon; // use tray icon notifications - CMOption m_bErrorPopups; // display popups with errors - CMOption m_bLaunchMailbox; // launch browser to view email - CMOption m_iTimeDiff1; // set this status to m_iStatus1 after this interval of secs - CMOption m_iStatus1; - CMOption m_iTimeDiff2; // set this status to m_iStatus2 after this interval of secs - CMOption m_iStatus2; - - void CheckStatus(void); - CMStringW GetUserId(MCONTACT); - - __forceinline int TS() const - { return time(0) - m_iTimeShift; - } - - __forceinline const char *appId() const - { return (m_isMra) ? MRA_APP_ID : ICQ_APP_ID; - } - - void SetPermitDeny(const CMStringW &userId, bool bAllow); -}; - -struct CMPlugin : public ACCPROTOPLUGIN -{ - CMPlugin(); - - int Load() override; -}; - -#endif +// ---------------------------------------------------------------------------80 +// ICQ plugin for Miranda Instant Messenger +// ________________________________________ +// +// Copyright © 2000-2001 Richard Hughes, Roland Rabien, Tristan Van de Vreede +// Copyright © 2001-2002 Jon Keating, Richard Hughes +// Copyright © 2002-2004 Martin Öberg, Sam Kothari, Robert Rainwater +// Copyright © 2004-2010 Joe Kucera, George Hazan +// Copyright © 2012-2023 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. +// ----------------------------------------------------------------------------- +// DESCRIPTION: +// +// Protocol Interface declarations +// ----------------------------------------------------------------------------- + +#ifndef _ICQ_PROTO_H_ +#define _ICQ_PROTO_H_ + +#include "m_system.h" +#include "m_protoint.h" + +#define MRA_APP_ID "ic1pzYNtEU6dDnEQ" +#define ICQ_APP_ID "ic1nmMjqg7Yu-0hL" +#define ICQ_API_SERVER "https://u.icq.net/wim" +#define ICQ_FILE_SERVER "https://u.icq.net/files/api/v1.1" +#define ICQ_FAKE_EVENT_ID 0xBABAEB +#define ICQ_ROBUST_SERVER "https://u.icq.net/rapi" + +#define PS_DUMMY "/DoNothing" +#define PS_GOTO_INBOX "/GotoInbox" + +#define WIM_CAP_VOIP_VOICE "094613504c7f11d18222444553540000" +#define WIM_CAP_VOIP_VIDEO "094613514c7f11d18222444553540000" +#define WIM_CAP_FILETRANSFER "094613434c7f11d18222444553540000" +#define WIM_CAP_UNIQ_REQ_ID "094613534c7f11d18222444553540000" +#define WIM_CAP_EMOJI "094613544c7f11d18222444553540000" +#define WIM_CAP_MENTIONS "0946135b4c7f11d18222444553540000" +#define WIM_CAP_MAIL_NOTIFICATIONS "094613594c7f11d18222444553540000" +#define WIM_CAP_INTRO_DLG_STATE "0946135a4c7f11d18222444553540000" + +#define NG_CAP_SECUREIM "4d69724e47536563757265494d000000" + +typedef CProtoDlgBase CIcqDlgBase; + +struct AIMSID +{ + AIMSID(CIcqProto *_ppro) : + m_ppro(_ppro) + {} + + CIcqProto *m_ppro; +}; + +enum ChatMenuItems +{ + IDM_INVITE = 10, IDM_LEAVE +}; + +struct IcqFileInfo +{ + IcqFileInfo(const std::string &pszUrl, const CMStringW &pwszDescr, uint32_t dwSize) : + szUrl(pszUrl.c_str()), + wszDescr(pwszDescr), + dwFileSize(dwSize) + {} + + CMStringA szUrl; + CMStringW wszDescr; + uint32_t dwFileSize; + bool bIsSticker = false; +}; + +struct IcqGroup +{ + IcqGroup(int _p1, const CMStringW &_p2) : + id(_p1), + wszSrvName(_p2) + { + SetName(_p2); + } + + int id; + int level; + CMStringW wszName, wszSrvName; + + void SetName(const CMStringW &str) + { + wszName = str; + level = wszName.SpanIncluding(L">").GetLength(); + if (level != 0) + wszName.Delete(0, level); + wszName.Replace(L">", L"\\"); + } +}; + +struct IcqCacheItem : public MZeroedObject +{ + IcqCacheItem(const CMStringW &wszId, MCONTACT _contact) : + m_aimid(wszId), + m_hContact(_contact) + {} + + CMStringW m_aimid; + MCONTACT m_hContact; + bool m_bInList; + __int64 m_iProcessedMsgId; + int m_iApparentMode; + time_t m_timer1, m_timer2; +}; + +struct IcqOwnMessage +{ + IcqOwnMessage(MCONTACT _hContact, int _msgid, const char *guid) + : m_hContact(_hContact), m_msgid(_msgid) + { + strncpy_s(m_guid, guid, _TRUNCATE); + } + + MCONTACT m_hContact; + int m_msgid; + char m_guid[50]; +}; + +struct IcqConn +{ + HNETLIBCONN s; + int lastTs, timeout; +}; + +struct IcqFileTransfer : public MZeroedObject +{ + // create an object for receiving + IcqFileTransfer(MCONTACT hContact, const char *pszUrl) : + m_szHost(pszUrl) + { + pfts.hContact = hContact; + pfts.totalFiles = 1; + pfts.flags = PFTS_UNICODE | PFTS_RECEIVING; + + ptrW pwszFileName(mir_utf8decodeW(pszUrl)); + if (pwszFileName == nullptr) + pwszFileName = mir_a2u(pszUrl); + + const wchar_t *p = wcsrchr(pwszFileName, '/'); + m_wszFileName = (p == nullptr) ? pwszFileName : p + 1; + m_wszShortName = m_wszFileName; + } + + // create an object for sending + IcqFileTransfer(MCONTACT hContact, const wchar_t *pwszFileName) : + m_wszFileName(pwszFileName) + { + pfts.flags = PFTS_UNICODE | PFTS_SENDING; + pfts.hContact = hContact; + pfts.szCurrentFile.w = m_wszFileName.GetBuffer(); + + const wchar_t *p = wcsrchr(pfts.szCurrentFile.w, '\\'); + if (pwszFileName != nullptr) + p++; + else + p = pfts.szCurrentFile.w; + m_wszShortName = p; + } + + ~IcqFileTransfer() + { + if (m_fileId >= 0) + _close(m_fileId); + } + + bool m_bCanceled = false, m_bStarted = false; + int m_fileId = -1; + CMStringA m_szHost; + CMStringW m_wszFileName, m_wszDescr; + const wchar_t *m_wszShortName; + PROTOFILETRANSFERSTATUS pfts; + HANDLE hWaitEvent; + + void FillHeaders(AsyncHttpRequest *pReq) + { + pReq->AddHeader("Content-Type", "application/octet-stream"); + pReq->AddHeader("Content-Disposition", CMStringA(FORMAT, "attachment; filename=\"%s\"", T2Utf(m_wszShortName).get())); + + uint32_t dwPortion = pfts.currentFileSize - pfts.currentFileProgress; + if (dwPortion > 1000000) + dwPortion = 1000000; + + pReq->AddHeader("Content-Range", CMStringA(FORMAT, "bytes %lld-%lld/%lld", pfts.currentFileProgress, pfts.currentFileProgress + dwPortion - 1, pfts.currentFileSize)); + pReq->AddHeader("Content-Length", CMStringA(FORMAT, "%d", dwPortion)); + + pReq->dataLength = dwPortion; + pReq->pData = (char*)mir_alloc(dwPortion); + _lseek(m_fileId, pfts.currentFileProgress, SEEK_SET); + _read(m_fileId, pReq->pData, dwPortion); + + pfts.currentFileProgress += dwPortion; + pfts.totalProgress += dwPortion; + } +}; + +class CIcqProto : public PROTO +{ + friend struct AsyncRapiRequest; + + class CIcqProtoImpl + { + friend class CIcqProto; + + CIcqProto &m_proto; + CTimer m_heartBeat, m_markRead; + + void OnHeartBeat(CTimer *) { + m_proto.CheckStatus(); + } + + void OnMarkRead(CTimer *pTimer) { + m_proto.SendMarkRead(); + pTimer->Stop(); + } + + CIcqProtoImpl(CIcqProto &pro) : + m_proto(pro), + m_markRead(Miranda_GetSystemWindow(), UINT_PTR(this)), + m_heartBeat(Miranda_GetSystemWindow(), UINT_PTR(this) + 1) + { + m_markRead.OnEvent = Callback(this, &CIcqProtoImpl::OnMarkRead); + m_heartBeat.OnEvent = Callback(this, &CIcqProtoImpl::OnHeartBeat); + } + } m_impl; + + friend struct CIcqRegistrationDlg; + friend class CGroupchatInviteDlg; + friend class CEditIgnoreListDlg; + friend class CIcqEnterLoginDlg; + friend class CIcqOptionsDlg; + friend class CGroupEditDlg; + + friend AsyncHttpRequest* operator <<(AsyncHttpRequest*, const AIMSID&); + + bool m_bOnline, m_bTerminated, m_bFirstBos, m_isMra, m_bError462; + int m_iTimeShift; + + MCONTACT CheckOwnMessage(const CMStringA &reqId, const CMStringA &msgId, bool bRemove); + void CheckPassword(void); + void ConnectionFailed(int iReason, int iErrorCode = 0); + void EmailNotification(const wchar_t *pwszText); + void GetPermitDeny(); + wchar_t* GetUIN(MCONTACT hContact); + void MarkAsRead(MCONTACT hContact); + void MoveContactToGroup(MCONTACT hContact, const wchar_t *pwszGroup, const wchar_t *pwszNewGroup); + bool RetrievePassword(); + void RetrieveUserHistory(MCONTACT, __int64 startMsgId, bool bCreateRead); + void RetrieveUserInfo(MCONTACT hContact); + void SendMrimLogin(NETLIBHTTPREQUEST *pReply); + void SetServerStatus(int iNewStatus); + void ShutdownSession(void); + void StartSession(void); + + void CheckAvatarChange(MCONTACT hContact, const JSONNode&); + IcqFileInfo* CheckFile(MCONTACT hContact, CMStringW &wszFileName, bool &bIsFile); + void CheckLastId(MCONTACT hContact, const JSONNode&); + void Json2int(MCONTACT, const JSONNode&, const char *szJson, const char *szSetting, bool bIsPartial); + void Json2string(MCONTACT, const JSONNode&, const char *szJson, const char *szSetting, bool bIsPartial); + MCONTACT ParseBuddyInfo(const JSONNode &buddy, MCONTACT hContact = INVALID_CONTACT_ID, bool bIsPartial = false); + void ParseMessage(MCONTACT hContact, __int64 &lastMsgId, const JSONNode &msg, bool bCreateRead, bool bLocalTime); + int StatusFromPresence(const JSONNode &presence, MCONTACT hContact); + + void OnLoggedIn(void); + void OnLoggedOut(void); + + mir_cs m_csMarkReadQueue; + LIST m_arMarkReadQueue; + void SendMarkRead(); + + __int64 getId(MCONTACT hContact, const char *szSetting); + void setId(MCONTACT hContact, const char *szSetting, __int64 iValue); + + void OnAddBuddy(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnAddClient(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnCheckMraAuth(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnCheckMraAuthFinal(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnCheckMrimLogin(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnCheckPassword(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnCheckPhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnFetchEvents(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnFileContinue(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnFileInit(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnFileInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnFileRecv(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnGenToken(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnGetChatInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnGetPermitDeny(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnGetSticker(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnGetUserHistory(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnGetUserInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnLoginViaPhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnNormalizePhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnReceiveAvatar(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnSearchResults(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnSendMessage(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnSessionEnd(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnStartSession(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnValidateSms(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + + void ProcessBuddyList(const JSONNode &pRoot); + void ProcessDiff(const JSONNode &pRoot); + void ProcessEvent(const JSONNode &pRoot); + void ProcessGroupChat(const JSONNode &pRoot); + void ProcessHistData(const JSONNode &pRoot); + void ProcessImState(const JSONNode &pRoot); + void ProcessMyInfo(const JSONNode &pRoot); + void ProcessNotification(const JSONNode &pRoot); + void ProcessPermissions(const JSONNode &pRoot); + void ProcessPresence(const JSONNode &pRoot); + void ProcessSessionEnd(const JSONNode &pRoot); + void ProcessTyping(const JSONNode &pRoot); + + IcqConn m_ConnPool[CONN_LAST]; + CMStringA m_szPassword; + CMStringA m_szSessionKey; + CMStringA m_szAToken; + CMStringA m_szRToken; + CMStringA m_fetchBaseURL; + CMStringA m_aimsid; + CMStringA m_szMraCookie; + LONG m_msgId = 1; + int m_iRClientId; + HGENMENU m_hUploadGroups; + + mir_cs m_csOwnIds; + OBJLIST m_arOwnIds; + + OBJLIST m_arGroups; + + int m_unreadEmails = -1; + CMStringA m_szMailBox; + + bool m_bIgnoreListEmpty = true; + bool m_bRememberPwd; // store password in a database + bool m_bDlgActive; + + //////////////////////////////////////////////////////////////////////////////////////// + // group chats + + int __cdecl GroupchatEventHook(WPARAM, LPARAM); + int __cdecl GroupchatMenuHook(WPARAM, LPARAM); + + void Chat_ProcessLogMenu(SESSION_INFO *si, int); + void Chat_SendPrivateMessage(GCHOOK *gch); + + void InviteUserToChat(SESSION_INFO *si); + void LeaveDestroyChat(SESSION_INFO *si); + void LoadChatInfo(SESSION_INFO *si); + + //////////////////////////////////////////////////////////////////////////////////////// + // http queue + + mir_cs m_csHttpQueue; + HANDLE m_evRequestsQueue; + LIST m_arHttpQueue; + + void CalcHash(AsyncHttpRequest*); + void DropQueue(); + bool ExecuteRequest(AsyncHttpRequest*); + bool IsQueueEmpty(); + void Push(MHttpRequest*); + bool RefreshRobustToken(AsyncHttpRequest *pReq); + + //////////////////////////////////////////////////////////////////////////////////////// + // cache + + mir_cs m_csCache; + OBJLIST m_arCache; + + void InitContactCache(void); + IcqCacheItem* FindContactByUIN(const CMStringW &pwszId); + MCONTACT CreateContact(const CMStringW &pwszId, bool bTemporary); + + void GetAvatarFileName(MCONTACT hContact, wchar_t *pszDest, size_t cbLen); + + //////////////////////////////////////////////////////////////////////////////////////// + // threads + + HANDLE m_hWorkerThread; + void __cdecl ServerThread(void*); + void __cdecl PollThread(void*); + + //////////////////////////////////////////////////////////////////////////////////////// + // services + + INT_PTR __cdecl GetAvatar(WPARAM, LPARAM); + INT_PTR __cdecl GetAvatarCaps(WPARAM, LPARAM); + INT_PTR __cdecl GetAvatarInfo(WPARAM, LPARAM); + INT_PTR __cdecl SetAvatar(WPARAM, LPARAM); + + INT_PTR __cdecl CreateAccMgrUI(WPARAM, LPARAM); + INT_PTR __cdecl EditGroups(WPARAM, LPARAM); + INT_PTR __cdecl EditProfile(WPARAM, LPARAM); + INT_PTR __cdecl GetEmailCount(WPARAM, LPARAM); + INT_PTR __cdecl GotoInbox(WPARAM, LPARAM); + INT_PTR __cdecl UploadGroups(WPARAM, LPARAM); + + INT_PTR __cdecl OnMenuLoadHistory(WPARAM, LPARAM); + + //////////////////////////////////////////////////////////////////////////////////////// + // events + + int __cdecl OnGroupChange(WPARAM, LPARAM); + int __cdecl OnDbEventRead(WPARAM, LPARAM); + int __cdecl OnOptionsInit(WPARAM, LPARAM); + int __cdecl OnUserInfoInit(WPARAM, LPARAM); + + //////////////////////////////////////////////////////////////////////////////////////// + // PROTO_INTERFACE + + MCONTACT AddToList( int flags, PROTOSEARCHRESULT *psr) override; + + int AuthRecv(MCONTACT, PROTORECVEVENT *pre) override; + int AuthRequest(MCONTACT hContact, const wchar_t *szMessage) override; + + INT_PTR GetCaps(int type, MCONTACT hContact = NULL) override; + int GetInfo(MCONTACT hContact, int infoType) override; + + HANDLE SearchBasic(const wchar_t *id) override; + + HANDLE FileAllow(MCONTACT hContact, HANDLE hTransfer, const wchar_t *szPath) override; + int FileCancel(MCONTACT hContact, HANDLE hTransfer) override; + int FileResume(HANDLE hTransfer, int action, const wchar_t *szFilename) override; + + HANDLE SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) override; + int SendMsg(MCONTACT hContact, int flags, const char *msg) override; + + int SetApparentMode(MCONTACT hContact, int mode) override; + int SetStatus(int iNewStatus) override; + + int UserIsTyping(MCONTACT hContact, int type) override; + + void OnBuildProtoMenu(void) override; + void OnContactAdded(MCONTACT) override; + void OnContactDeleted(MCONTACT) override; + void OnEventEdited(MCONTACT, MEVENT) override; + void OnModulesLoaded() override; + void OnShutdown() override; + +public: + CIcqProto(const char*, const wchar_t*); + ~CIcqProto(); + + CMOption m_szOwnId; // our own aim id + CMOption m_bHideGroupchats; // don't pop up group chat windows on startup + CMOption m_bUseTrayIcon; // use tray icon notifications + CMOption m_bErrorPopups; // display popups with errors + CMOption m_bLaunchMailbox; // launch browser to view email + CMOption m_iTimeDiff1; // set this status to m_iStatus1 after this interval of secs + CMOption m_iStatus1; + CMOption m_iTimeDiff2; // set this status to m_iStatus2 after this interval of secs + CMOption m_iStatus2; + + void CheckStatus(void); + CMStringW GetUserId(MCONTACT); + + __forceinline int TS() const + { return time(0) - m_iTimeShift; + } + + __forceinline const char *appId() const + { return (m_isMra) ? MRA_APP_ID : ICQ_APP_ID; + } + + void SetPermitDeny(const CMStringW &userId, bool bAllow); +}; + +struct CMPlugin : public ACCPROTOPLUGIN +{ + CMPlugin(); + + int Load() override; +}; + +#endif diff --git a/protocols/ICQ-WIM/src/server.cpp b/protocols/ICQ-WIM/src/server.cpp index 167aabfee9..114ad5ed99 100644 --- a/protocols/ICQ-WIM/src/server.cpp +++ b/protocols/ICQ-WIM/src/server.cpp @@ -1,1220 +1,1220 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-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, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- - -#include "stdafx.h" - -#pragma comment(lib, "libcrypto.lib") - -void CIcqProto::CheckAvatarChange(MCONTACT hContact, const JSONNode &ev) -{ - CMStringW wszIconId(ev["bigIconId"].as_mstring()); - if (wszIconId.IsEmpty()) - wszIconId = ev["iconId"].as_mstring(); - - if (!wszIconId.IsEmpty()) { - CMStringW oldIconID(getMStringW(hContact, "IconId")); - if (wszIconId == oldIconID) { - wchar_t wszFullName[MAX_PATH]; - GetAvatarFileName(hContact, wszFullName, _countof(wszFullName)); - if (_waccess(wszFullName, 0) == 0) - return; - } - - setWString(hContact, "IconId", wszIconId); - - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/expressions/get", &CIcqProto::OnReceiveAvatar); - pReq << CHAR_PARAM("f", "native") << WCHAR_PARAM("t", GetUserId(hContact)) << CHAR_PARAM("type", "bigBuddyIcon"); - pReq->hContact = hContact; - Push(pReq); - } - else delSetting(hContact, "IconId"); -} - -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); -} - -MCONTACT CIcqProto::CheckOwnMessage(const CMStringA &reqId, const CMStringA &msgId, bool bRemove) -{ - IcqOwnMessage *pOwn = nullptr; - { - mir_cslock lck(m_csOwnIds); - for (auto &it : m_arOwnIds) { - if (reqId == it->m_guid) { - pOwn = it; - break; - } - } - } - - if (pOwn == nullptr) - return 0; - - ProtoBroadcastAck(pOwn->m_hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)pOwn->m_msgid, (LPARAM)msgId.c_str()); - - MCONTACT ret = pOwn->m_hContact; - if (bRemove) { - mir_cslock lck(m_csOwnIds); - m_arOwnIds.remove(pOwn); - } - return ret; -} - -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()) { - StartSession(); - return; - } - - if (m_isMra) { - m_bError462 = false; - SendMrimLogin(nullptr); - } - else { - 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", appId()) - << CHAR_PARAM("f", "json") << CHAR_PARAM("tokenType", "longTerm") << WCHAR_PARAM("s", m_szOwnId) << CHAR_PARAM("pwd", m_szPassword); - #ifndef _DEBUG - pReq->flags |= NLHRF_NODUMPSEND; - #endif - Push(pReq); - } -} - -IcqFileInfo* CIcqProto::CheckFile(MCONTACT hContact, CMStringW &wszText, bool &bIsFile) -{ - CMStringW wszUrl(wszText.Mid(26)); - int idx = wszUrl.Find(' '); - if (idx != -1) - wszUrl.Truncate(idx); - - bIsFile = false; - IcqFileInfo *pFileInfo = nullptr; - - // is it already downloaded sticker? - CMStringW wszLoadedPath(FORMAT, L"%s\\%S\\Stickers\\STK{%s}.png", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName, wszUrl.c_str()); - if (!_waccess(wszLoadedPath, 0)) { - pFileInfo = (IcqFileInfo *)this; - wszText.Format(L"STK{%s}", wszUrl.c_str()); - } - else { - // download file info - CMStringA szUrl(FORMAT, ICQ_FILE_SERVER "/info/%S/", wszUrl.c_str()); - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, szUrl, &CIcqProto::OnFileInfo); - pReq->hContact = hContact; - pReq->pUserInfo = &pFileInfo; - pReq << CHAR_PARAM("aimsid", m_aimsid) << CHAR_PARAM("previews", "192,600,xlarge"); - if (!ExecuteRequest(pReq)) - return nullptr; - - // is it a sticker? - if (pFileInfo->bIsSticker) { - if (ServiceExists(MS_SMILEYADD_LOADCONTACTSMILEYS)) { - auto *pNew = new AsyncHttpRequest(CONN_NONE, REQUEST_GET, pFileInfo->szUrl, &CIcqProto::OnGetSticker); - pNew->flags |= NLHRF_NODUMP | NLHRF_SSL | NLHRF_HTTP11 | NLHRF_REDIRECT; - pNew->pUserInfo = wszUrl.GetBuffer(); - pNew->AddHeader("Sec-Fetch-User", "?1"); - pNew->AddHeader("Sec-Fetch-Site", "cross-site"); - pNew->AddHeader("Sec-Fetch-Mode", "navigate"); - if (!ExecuteRequest(pNew)) - return nullptr; - - wszText.Format(L"STK{%s}", wszUrl.c_str()); - delete pFileInfo; - } - else wszText = TranslateT("SmileyAdd plugin required to support stickers"); - } - else bIsFile = true; - } - - return pFileInfo; -} - -void CIcqProto::CheckStatus() -{ - time_t now = time(0); - int diff1 = m_iTimeDiff1, diff2 = m_iTimeDiff2; - - for (auto &it : m_arCache) { - // this contact is really offline and is on the first timer - // if the first timer is expired, we clear it and look for the second status - if (diff1 && it->m_timer1 && now - it->m_timer1 > diff1) { - it->m_timer1 = 0; - - // if the second timer is set up, activate it - if (m_iTimeDiff2) { - setWord(it->m_hContact, "Status", m_iStatus2); - it->m_timer2 = now; - } - // if the second timer is not set, simply mark a contact as offline - else setWord(it->m_hContact, "Status", ID_STATUS_OFFLINE); - continue; - } - - // if the second timer is expired, set status to offline - if (diff2 && it->m_timer2 && now - it->m_timer2 > m_iTimeDiff2) { - it->m_timer2 = 0; - setWord(it->m_hContact, "Status", ID_STATUS_OFFLINE); - } - } -} - -void CIcqProto::ConnectionFailed(int iReason, int iErrorCode) -{ - debugLogA("ConnectionFailed -> reason %d", iReason); - - if (m_bErrorPopups) { - POPUPDATAW Popup = {}; - Popup.lchIcon = IcoLib_GetIconByHandle(Skin_GetIconHandle(SKINICON_ERROR), true); - wcscpy_s(Popup.lpwzContactName, m_tszUserName); - switch (iReason) { - case LOGINERR_BADUSERID: - mir_snwprintf(Popup.lpwzText, TranslateT("You have not entered a login or password.\nConfigure this in Options -> Network -> ICQ and try again.")); - break; - case LOGINERR_WRONGPASSWORD: - mir_snwprintf(Popup.lpwzText, TranslateT("Connection failed.\nYour login or password was rejected (%d)."), iErrorCode); - break; - case LOGINERR_NONETWORK: - case LOGINERR_NOSERVER: - mir_snwprintf(Popup.lpwzText, TranslateT("Connection failed.\nThe server is temporarily unavailable (%d)."), iErrorCode); - break; - default: - mir_snwprintf(Popup.lpwzText, TranslateT("Connection failed.\nUnknown error during sign on: %d"), iErrorCode); - break; - } - PUAddPopupW(&Popup); - } - - ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, nullptr, iReason); - ShutdownSession(); -} - -void CIcqProto::MoveContactToGroup(MCONTACT hContact, const wchar_t *pwszGroup, const wchar_t *pwszNewGroup) -{ - // otherwise we'll get a server error - if (!mir_wstrlen(pwszGroup)) - return; - - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/moveBuddy") << AIMSID(this) << WCHAR_PARAM("buddy", GetUserId(hContact)) - << GROUP_PARAM("group", pwszGroup); - if (mir_wstrlen(pwszNewGroup)) - pReq << GROUP_PARAM("newGroup", pwszNewGroup); - Push(pReq); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::OnLoggedIn() -{ - debugLogA("CIcqProto::OnLoggedIn"); - m_bOnline = true; - m_impl.m_heartBeat.Start(1000); - - for (auto &it : m_arCache) - it->m_timer1 = it->m_timer2 = 0; - - SetServerStatus(m_iDesiredStatus); - RetrieveUserInfo(0); - GetPermitDeny(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::OnLoggedOut() -{ - debugLogA("CIcqProto::OnLoggedOut"); - m_bOnline = false; - m_impl.m_heartBeat.Stop(); - - for (auto &it : m_arCache) - it->m_timer1 = it->m_timer2 = 0; - - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE); - m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; - - setAllContactStatuses(ID_STATUS_OFFLINE, false); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::MarkAsRead(MCONTACT hContact) -{ - if (!m_bOnline) - return; - - m_impl.m_markRead.Start(200); - - auto *pCache = FindContactByUIN(GetUserId(hContact)); - if (pCache) { - mir_cslock lck(m_csMarkReadQueue); - if (m_arMarkReadQueue.indexOf(pCache) == -1) - m_arMarkReadQueue.insert(pCache); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -MCONTACT CIcqProto::ParseBuddyInfo(const JSONNode &buddy, MCONTACT hContact, bool bIsPartial) -{ - // user chat? - CMStringW wszId(buddy["aimId"].as_mstring()); - if (IsChat(wszId)) { - CMStringW wszChatId(buddy["aimId"].as_mstring()); - CMStringW wszChatName(buddy["friendly"].as_mstring()); - - auto *pContact = FindContactByUIN(wszId); - if (pContact && pContact->m_iApparentMode == ID_STATUS_OFFLINE) - return INVALID_CONTACT_ID; - - auto *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; - } - - bool bIgnored = !IsValidType(buddy); - if (hContact == INVALID_CONTACT_ID) { - if (bIgnored) - return INVALID_CONTACT_ID; - - hContact = CreateContact(wszId, false); - FindContactByUIN(wszId)->m_bInList = true; - } - else if (bIgnored) { - db_delete_contact(hContact); - return INVALID_CONTACT_ID; - } - - CMStringA szVer; - bool bVersionDetected = false, bSecureIM = false; - - for (auto &it : buddy["capabilities"]) { - CMStringW wszCap(it.as_mstring()); - if (wszCap.GetLength() != 32) - continue; - - uint8_t cap[16]; - hex2binW(wszCap, cap, sizeof(cap)); - if (!memcmp(cap, "MiNG", 4)) { // Miranda - int v[4]; - if (4 == swscanf(wszCap.c_str() + 16, L"%04x%04x%04x%04x", &v[0], &v[1], &v[2], &v[3])) { - szVer.Format("Miranda NG %d.%d.%d.%d (ICQ %d.%d.%d.%d)", v[0], v[1], v[2], v[3], cap[4], cap[5], cap[6], cap[7]); - setString(hContact, "MirVer", szVer); - bVersionDetected = true; - } - } - else if (wszCap == _A2W(NG_CAP_SECUREIM)) { - bSecureIM = bVersionDetected = true; - } - else if (!memcmp(cap, "Mod by Mikanoshi", 16)) { - szVer = "R&Q build by Mikanoshi"; - bVersionDetected = true; - } - else if (!memcmp(cap, "Mandarin IM", 11)) { - szVer = "Mandarin IM"; - bVersionDetected = true; - } - } - - if (bVersionDetected) { - if (bSecureIM) - szVer.Append(" + SecureIM"); - setString(hContact, "MirVer", szVer); - } - else delSetting(hContact, "MirVer"); - - const JSONNode &var = buddy["friendly"]; - if (var) - setWString(hContact, "Nick", var.as_mstring()); - - if (buddy["deleted"].as_bool()) { - setByte(hContact, "IcqDeleted", 1); - Contact::PutOnList(hContact); - } - - Json2string(hContact, buddy, "emailId", "Email", bIsPartial); - Json2string(hContact, buddy, "cellNumber", "Cellular", bIsPartial); - Json2string(hContact, buddy, "workNumber", "CompanyPhone", bIsPartial); - - // we shall not remove existing phone number anyhow - Json2string(hContact, buddy, "phoneNumber", DB_KEY_PHONE, true); - - Json2int(hContact, buddy, "official", "Official", bIsPartial); - Json2int(hContact, buddy, "onlineTime", DB_KEY_ONLINETS, bIsPartial); - Json2int(hContact, buddy, "idleTime", "IdleTS", bIsPartial); - Json2int(hContact, buddy, "memberSince", DB_KEY_MEMBERSINCE, bIsPartial); - - int iStatus = StatusFromPresence(buddy, hContact); - if (iStatus > 0) - setWord(hContact, "Status", iStatus); - - const JSONNode &profile = buddy["profile"]; - if (profile) { - Json2string(hContact, profile, "friendlyName", DB_KEY_ICQNICK, bIsPartial); - Json2string(hContact, profile, "firstName", "FirstName", bIsPartial); - Json2string(hContact, profile, "lastName", "LastName", bIsPartial); - Json2string(hContact, profile, "aboutMe", DB_KEY_ABOUT, bIsPartial); - - ptrW wszNick(getWStringA(hContact, "Nick")); - if (!wszNick) { - CMStringW srvNick = profile["friendlyName"].as_mstring(); - if (!srvNick.IsEmpty()) - setWString(hContact, "Nick", srvNick); - } - - 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); - } - } - - CMStringW 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"]) { - Json2string(hContact, it, "city", "City", bIsPartial); - Json2string(hContact, it, "state", "State", bIsPartial); - Json2string(hContact, it, "country", "Country", bIsPartial); - } - } - - CMStringW 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, bool bCreateRead, bool bLocalTime) -{ - CMStringA szMsgId(it["msgId"].as_mstring()); - __int64 msgId = _atoi64(szMsgId); - if (msgId > lastMsgId) - lastMsgId = msgId; - - CMStringW wszText; - const JSONNode &sticker = it["sticker"]; - if (sticker) { - CMStringW wszUrl, wszSticker(sticker["id"].as_mstring()); - int iCollectionId, iStickerId; - if (2 == swscanf(wszSticker, L"ext:%d:sticker:%d", &iCollectionId, &iStickerId)) - wszUrl.Format(L"https://c.icq.com/store/stickers/%d/%d/medium", iCollectionId, iStickerId); - else - wszUrl = TranslateT("Unknown sticker"); - wszText.Format(L"%s\n%s", TranslateT("User sent a sticker:"), wszUrl.c_str()); - } - else { - wszText = it["text"].as_mstring(); - wszText.TrimRight(); - - // user added you - if (it["class"].as_mstring() == L"event" && it["eventTypeId"].as_mstring() == L"27:33000") { - if (bLocalTime) { - CMStringA id = getMStringA(hContact, DB_KEY_ID); - int pos = id.Find('@'); - CMStringA nick = (pos == -1) ? id : id.Left(pos); - DB::AUTH_BLOB blob(hContact, nick, nullptr, nullptr, id, nullptr); - - PROTORECVEVENT pre = {}; - pre.timestamp = (uint32_t)time(0); - pre.lParam = blob.size(); - pre.szMessage = blob; - ProtoChainRecv(hContact, PSR_AUTH, 0, (LPARAM)&pre); - } - return; - } - } - - int iMsgTime = (bLocalTime) ? time(0) : it["time"].as_int(); - bool bIsOutgoing = it["outgoing"].as_bool(), bIsFileTransfer = false; - IcqFileInfo *pFileInfo = nullptr; - - if (!bCreateRead && !bIsOutgoing && wszText.Left(26) == L"https://files.icq.net/get/") { - pFileInfo = CheckFile(hContact, wszText, bIsFileTransfer); - if (!pFileInfo) - return; - - for (auto &jt : it["parts"]) { - CMStringW wszDescr(jt["captionedContent"]["caption"].as_mstring()); - if (!wszDescr.IsEmpty()) - pFileInfo->wszDescr = wszDescr; - } - } - - 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")); - - if (bIsFileTransfer) { - wszText = pFileInfo->szUrl; - if (!pFileInfo->wszDescr) - wszText.AppendFormat(L"\r\n%s", pFileInfo->wszDescr.c_str()); - delete pFileInfo; - } - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE }; - gce.pszID.w = wszChatId; - gce.dwFlags = GCEF_ADDTOLOG; - gce.pszUID.w = wszSender; - gce.pszText.w = wszText; - gce.time = iMsgTime; - gce.bIsMe = wszSender == m_szOwnId; - Chat_Event(&gce); - return; - } - - // skip own messages, just set the server msgid - CMStringA reqId(it["reqId"].as_mstring()); - if (CheckOwnMessage(reqId, szMsgId, true)) { - debugLogA("Skipping our own message %s", szMsgId.c_str()); - return; - } - - // ignore duplicates - MEVENT hDbEvent = db_event_getById(m_szModuleName, szMsgId); - if (hDbEvent != 0) { - debugLogA("Message %s already exists", szMsgId.c_str()); - return; - } - - // convert a file info into Miranda's file transfer - if (bIsFileTransfer) { - auto *ft = new IcqFileTransfer(hContact, pFileInfo->szUrl); - ft->pfts.totalBytes = ft->pfts.currentFileSize = pFileInfo->dwFileSize; - ft->pfts.szCurrentFile.w = ft->m_wszFileName.GetBuffer(); - - PROTORECVFILE pre = {}; - pre.dwFlags = PRFF_UNICODE; - pre.fileCount = 1; - pre.timestamp = iMsgTime; - pre.files.w = &ft->m_wszShortName; - pre.descr.w = pFileInfo->wszDescr; - pre.lParam = (LPARAM)ft; - ProtoChainRecvFile(hContact, &pre); - - delete pFileInfo; - return; - } - - // suppress notifications for already loaded/processed messages - __int64 storedLastId = getId(hContact, DB_KEY_LASTMSGID); - if (msgId <= storedLastId) { - debugLogA("Parsing old/processed message with id %lld < %lld, setting CR to true", msgId, storedLastId); - bCreateRead = true; - } - - debugLogA("Adding message %d:%lld (CR=%d)", hContact, msgId, bCreateRead); - - ptrA szUtf(mir_utf8encodeW(wszText)); - - PROTORECVEVENT pre = {}; - if (bIsOutgoing) pre.flags |= PREF_SENT; - if (bCreateRead) pre.flags |= PREF_CREATEREAD; - pre.szMsgId = szMsgId; - pre.timestamp = iMsgTime; - pre.szMessage = szUtf; - ProtoChainRecvMsg(hContact, &pre); -} - -bool CIcqProto::RefreshRobustToken(AsyncHttpRequest *pOrigReq) -{ - if (!m_szRToken.IsEmpty()) - return true; - - auto *pReq = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER "/genToken", &CIcqProto::OnGenToken); - #ifndef _DEBUG - pReq->flags |= NLHRF_NODUMPSEND; - #endif - - int ts = TS(); - pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("k", appId()) << CHAR_PARAM("nonce", CMStringA(FORMAT, "%d-%d", ts, rand() % 10)) << INT_PARAM("ts", ts); - CalcHash(pReq); - - CMStringA szAgent(FORMAT, "%S Mail.ru Windows ICQ (version 10.0.1999)", (wchar_t*)m_szOwnId); - pReq->AddHeader("User-Agent", szAgent); - if (!ExecuteRequest(pReq)) { -LBL_Error: - (this->*(pOrigReq->m_pFunc))(nullptr, pOrigReq); - return false; - } - if (m_szRToken.IsEmpty()) - goto LBL_Error; - - // now add this token - bool bRet = false; - pReq = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER "/addClient", &CIcqProto::OnAddClient); - #ifndef _DEBUG - pReq->flags |= NLHRF_NODUMPSEND; - #endif - pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("f", "json") << CHAR_PARAM("k", appId()) << INT_PARAM("ts", ts) - << CHAR_PARAM("client", "icq") << CHAR_PARAM("reqId", pReq->m_reqId) << CHAR_PARAM("authToken", m_szRToken); - pReq->pUserInfo = &bRet; - if (!ExecuteRequest(pReq)) - goto LBL_Error; - - return bRet; -} - -void CIcqProto::RetrieveUserInfo(MCONTACT hContact) -{ - auto *pReq = new AsyncRapiRequest(this, "getUserInfo", &CIcqProto::OnGetUserInfo); - pReq->params << WCHAR_PARAM("sn", GetUserId(hContact)); - pReq->hContact = hContact; - Push(pReq); -} - -void CIcqProto::RetrieveUserHistory(MCONTACT hContact, __int64 startMsgId, bool bCreateRead) -{ - if (startMsgId == 0) - startMsgId = -1; - - __int64 patchVer = getId(hContact, DB_KEY_PATCHVER); - if (patchVer == 0) - patchVer = 1; - - auto *pReq = new AsyncRapiRequest(this, "getHistory", &CIcqProto::OnGetUserHistory); - #ifndef _DEBUG - pReq->flags |= NLHRF_NODUMPSEND; - #endif - pReq->hContact = hContact; - pReq->pUserInfo = (bCreateRead) ? pReq : 0; - pReq->params << WCHAR_PARAM("sn", GetUserId(hContact)) << INT64_PARAM("fromMsgId", startMsgId) << INT_PARAM("count", 1000) - << SINT64_PARAM("patchVersion", patchVer) << CHAR_PARAM("language", "ru-ru"); - 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; - } - - Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/presence/setState") - << AIMSID(this) << CHAR_PARAM("view", szStatus) << INT_PARAM("invisible", invisible)); - - if (iStatus == ID_STATUS_OFFLINE && !getByte(DB_KEY_PHONEREG)) { - auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_GET, ICQ_API_SERVER "/aim/endSession", &CIcqProto::OnSessionEnd); - pReq << AIMSID(this) << INT_PARAM("invalidateToken", 1); - ExecuteRequest(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 - DropQueue(); - - if (m_hWorkerThread) - SetEvent(m_evRequestsQueue); - - OnLoggedOut(); - - for (auto &it : m_ConnPool) { - if (it.s) { - Netlib_Shutdown(it.s); - it.s = nullptr; - } - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -#define EVENTS "myInfo,presence,buddylist,typing,dataIM,userAddedToBuddyList,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 = TS(); - CMStringA nonce(FORMAT, "%d-2", ts); - CMStringA caps(WIM_CAP_UNIQ_REQ_ID "," WIM_CAP_EMOJI "," WIM_CAP_MAIL_NOTIFICATIONS "," WIM_CAP_INTRO_DLG_STATE); - if (g_bSecureIM) { - caps.AppendChar(','); - caps.Append(NG_CAP_SECUREIM); - } - - MFileVersion v; - Miranda_GetFileVersion(&v); - caps.AppendFormat(",%02x%02x%02x%02x%02x%02x%02x%02x%04x%04x%04x%04x", 'M', 'i', 'N', 'G', - __MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM, v[0], v[1], v[2], v[3]); - - 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", appId()) << 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) - return; - - CMStringW wszId = getMStringW(pReq->hContact, DB_KEY_ID); - for (auto &it : root.data()["results"]) { - if (it["buddy"].as_mstring() != wszId) - continue; - - switch (int iResultCode = it["resultCode"].as_int()) { - case 0: // success - case 3: // already in contact list - break; - - default: - debugLogA("Contact %d failed to add: error %d", pReq->hContact, iResultCode); - - POPUPDATAW Popup = {}; - Popup.lchIcon = IcoLib_GetIconByHandle(Skin_GetIconHandle(SKINICON_ERROR)); - wcsncpy_s(Popup.lpwzText, TranslateT("Buddy addition failed"), _TRUNCATE); - wcsncpy_s(Popup.lpwzContactName, Clist_GetContactDisplayName(pReq->hContact), _TRUNCATE); - Popup.iSeconds = 20; - PUAddPopupW(&Popup); - - // Contact::RemoveFromList(pReq->hContact); - } - - RetrieveUserInfo(pReq->hContact); - Contact::PutOnList(pReq->hContact); - } -} - -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, root.error()); - return; - - default: - ConnectionFailed(LOGINERR_WRONGPROTOCOL, root.error()); - return; - } - - JSONNode &data = root.data(); - m_szAToken = data["token"]["a"].as_mstring(); - mir_urlDecode(m_szAToken.GetBuffer()); - setString(DB_KEY_ATOKEN, m_szAToken); - - CMStringA szSessionSecret = data["sessionSecret"].as_mstring(); - CMStringA szPassTemp = m_szPassword; - - unsigned int len; - uint8_t hashOut[MIR_SHA256_HASH_SIZE]; - HMAC(EVP_sha256(), szPassTemp, szPassTemp.GetLength(), (uint8_t*)szSessionSecret.c_str(), szSessionSecret.GetLength(), hashOut, &len); - m_szSessionKey = ptrA(mir_base64_encode(hashOut, sizeof(hashOut))); - setString(DB_KEY_SESSIONKEY, m_szSessionKey); - - CMStringW szUin = data["loginId"].as_mstring(); - if (szUin) - m_szOwnId = szUin; - - int srvTS = data["hostTime"].as_int(); - m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; - - StartSession(); -} - -void CIcqProto::OnFileContinue(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pOld) -{ - IcqFileTransfer *pTransfer = (IcqFileTransfer*)pOld->pUserInfo; - if (pTransfer->m_bCanceled) { -LBL_Error: - ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, pTransfer); - delete pTransfer; - return; - } - - switch (pReply->resultCode) { - case 200: // final ok - case 206: // partial ok - break; - - default: - goto LBL_Error; - } - - // file transfer succeeded? - if (pTransfer->pfts.currentFileProgress == pTransfer->pfts.currentFileSize) { - FileReply root(pReply); - if (root.error() != 200) - goto LBL_Error; - - ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, pTransfer); - - const JSONNode &data = root.data(); - CMStringW wszUrl(data["static_url"].as_mstring()); - - JSONNode bundle, contents; contents.set_name("captionedContent"); - contents << WCHAR_PARAM("caption", pTransfer->m_wszDescr) << WCHAR_PARAM("url", wszUrl); - bundle << CHAR_PARAM("mediaType", "text") << CHAR_PARAM("text", "") << contents; - CMStringW wszParts(FORMAT, L"[%s]", ptrW(json_write(&bundle)).get()); - - if (!pTransfer->m_wszDescr.IsEmpty()) - wszUrl += L" " + pTransfer->m_wszDescr; - - int id = InterlockedIncrement(&m_msgId); - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/im/sendIM", &CIcqProto::OnSendMessage); - - auto *pOwn = new IcqOwnMessage(pTransfer->pfts.hContact, id, pReq->m_reqId); - pReq->pUserInfo = pOwn; - { - mir_cslock lck(m_csOwnIds); - m_arOwnIds.insert(pOwn); - } - - pReq << AIMSID(this) << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("k", appId()) << CHAR_PARAM("mentions", "") << WCHAR_PARAM("message", wszUrl) - << CHAR_PARAM("offlineIM", "true") << WCHAR_PARAM("parts", wszParts) << WCHAR_PARAM("t", GetUserId(pTransfer->pfts.hContact)) << INT_PARAM("ts", TS()); - Push(pReq); - - delete pTransfer; - return; - } - - // else send the next portion - auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, pTransfer->m_szHost, &CIcqProto::OnFileContinue); - pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("client", "icq") << CHAR_PARAM("k", appId()) << INT_PARAM("ts", TS()); - CalcHash(pReq); - pReq->m_szUrl.AppendChar('?'); - pReq->m_szUrl += pReq->m_szParam; pReq->m_szParam.Empty(); - pReq->pUserInfo = pTransfer; - pTransfer->FillHeaders(pReq); - Push(pReq); - - pTransfer->pfts.currentFileTime = time(0); - ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_DATA, pTransfer, (LPARAM)&pTransfer->pfts); -} - -void CIcqProto::OnFileInit(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pOld) -{ - IcqFileTransfer *pTransfer = (IcqFileTransfer*)pOld->pUserInfo; - if (pTransfer->m_bCanceled) { -LBL_Error: - ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, pTransfer); - delete pTransfer; - return; - } - - FileReply root(pReply); - if (root.error() != 200) - goto LBL_Error; - - ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_INITIALISING, pTransfer); - pTransfer->pfts.currentFileTime = time(0); - - const JSONNode &data = root.data(); - CMStringW wszHost(data["host"].as_mstring()); - CMStringW wszUrl(data["url"].as_mstring()); - pTransfer->m_szHost = L"https://" + wszHost + wszUrl; - - auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, pTransfer->m_szHost, &CIcqProto::OnFileContinue); - pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("client", "icq") << CHAR_PARAM("k", appId()) << INT_PARAM("ts", TS()); - CalcHash(pReq); - pReq->m_szUrl.AppendChar('?'); - pReq->m_szUrl += pReq->m_szParam; pReq->m_szParam.Empty(); - pReq->pUserInfo = pTransfer; - pTransfer->FillHeaders(pReq); - Push(pReq); - - ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_DATA, pTransfer, (LPARAM)&pTransfer->pfts); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Support for stickers - -void CIcqProto::OnGetSticker(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - if (pReply->resultCode != 200) { - debugLogA("Error getting sticker: %d", pReply->resultCode); - return; - } - - CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName); - CreateDirectoryTreeW(wszPath); - - CMStringW wszFileName(FORMAT, L"%s\\STK{%s}.png", wszPath.c_str(), pReq->pUserInfo); - FILE *out = _wfopen(wszFileName, L"wb"); - fwrite(pReply->pData, 1, pReply->dataLength, out); - fclose(out); - - SMADD_CONT cont = { 1, m_szModuleName, wszFileName }; - CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// File info request - -void CIcqProto::OnFileInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - IcqFileInfo **res = (IcqFileInfo **)pReq->pUserInfo; - *res = nullptr; - - RobustReply root(pReply); - if (root.error() != 200) - return; - - auto &pData = root.result(); - auto &pInfo = pData["info"] ; - std::string szUrl(pInfo["dlink"].as_string()); - if (szUrl.empty()) - return; - - MarkAsRead(pReq->hContact); - - bool bIsSticker; - CMStringW wszDescr(pInfo["file_name"].as_mstring()); - if (!mir_wstrncmp(wszDescr, L"dnld", 4)) { - bIsSticker = true; - - std::string szPreview = pData["previews"]["192"].as_string(); - if (!szPreview.empty()) - szUrl = szPreview; - } - else bIsSticker = false; - - mir_urlDecode(&*szUrl.begin()); - - *res = new IcqFileInfo(szUrl, wszDescr, pInfo["file_size"].as_int()); - res[0]->bIsSticker = bIsSticker; -} - -void CIcqProto::OnFileRecv(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - auto *ft = (IcqFileTransfer*)pReq->pUserInfo; - - if (pReply->resultCode != 200) { -LBL_Error: - FileCancel(pReq->hContact, ft); - return; - } - - ft->hWaitEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); - if (ProtoBroadcastAck(ft->pfts.hContact, ACKTYPE_FILE, ACKRESULT_FILERESUME, ft, (LPARAM)&ft->pfts)) - WaitForSingleObject(ft->hWaitEvent, INFINITE); - CloseHandle(ft->hWaitEvent); - - debugLogW(L"Saving to [%s]", ft->pfts.szCurrentFile.w); - int fileId = _wopen(ft->pfts.szCurrentFile.w, _O_BINARY | _O_CREAT | _O_TRUNC | _O_WRONLY, _S_IREAD | _S_IWRITE); - if (fileId == -1) { - debugLogW(L"Cannot open [%s] for writing", ft->pfts.szCurrentFile.w); - goto LBL_Error; - } - - int result = _write(fileId, pReply->pData, pReply->dataLength); - _close(fileId); - if (result != pReply->dataLength) { - debugLogW(L"Error writing data into [%s]", ft->pfts.szCurrentFile.w); - goto LBL_Error; - } - - ft->pfts.totalProgress += pReply->dataLength; - ft->pfts.currentFileProgress += pReply->dataLength; - ProtoBroadcastAck(ft->pfts.hContact, ACKTYPE_FILE, ACKRESULT_DATA, ft, (LPARAM)&ft->pfts); - - ProtoBroadcastAck(ft->pfts.hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, ft); - delete ft; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::OnGenToken(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) -{ - RobustReply root(pReply); - if (root.error() != 20000) - return; - - auto &results = root.results(); - m_szRToken = results["authToken"].as_mstring(); -} - -void CIcqProto::OnGetUserHistory(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - RobustReply root(pReply); - if (root.error() != 20000) - return; - - __int64 lastMsgId = getId(pReq->hContact, DB_KEY_LASTMSGID); - - int count = 0; - auto &results = root.results(); - for (auto &it : results["messages"]) { - ParseMessage(pReq->hContact, lastMsgId, it, pReq->pUserInfo != nullptr, false); - count++; - } - - setId(pReq->hContact, DB_KEY_LASTMSGID, lastMsgId); - - if (count >= 999) - RetrieveUserHistory(pReq->hContact, lastMsgId, pReq->pUserInfo != nullptr); -} - -void CIcqProto::OnGetUserInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - RobustReply root(pReply); - if (root.error() != 20000) { - ProtoBroadcastAck(pReq->hContact, ACKTYPE_GETINFO, ACKRESULT_FAILED, nullptr); - return; - } - - ParseBuddyInfo(root.results(), pReq->hContact, true); - - ProtoBroadcastAck(pReq->hContact, ACKTYPE_GETINFO, ACKRESULT_SUCCESS, nullptr); -} - -void CIcqProto::OnStartSession(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) -{ - JsonReply root(pReply); - switch (root.error()) { - case 200: - break; - - case 451: - // session forcibly closed from site - delSetting(DB_KEY_ATOKEN); - delSetting(DB_KEY_SESSIONKEY); - CheckPassword(); - return; - - case 401: - delSetting(DB_KEY_ATOKEN); - delSetting(DB_KEY_SESSIONKEY); - if (root.detail() == 1002) // session expired - CheckPassword(); - else - ConnectionFailed(LOGINERR_WRONGPASSWORD, root.error()); - return; - - case 400: - if (root.detail() == 1015 && m_iTimeShift == 0) { // wrong timestamp - JSONNode &data = root.data(); - int srvTS = data["ts"].as_int(); - m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; - StartSession(); - return; - } - __fallthrough; - - default: - ConnectionFailed(LOGINERR_WRONGPROTOCOL, root.error()); - return; - } - - JSONNode &data = root.data(); - m_fetchBaseURL = data["fetchBaseURL"].as_mstring(); - m_aimsid = data["aimsid"].as_mstring(); - - ProcessMyInfo(data["myInfo"]); - - int srvTS = data["ts"].as_int(); - m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; - - OnLoggedIn(); - - for (auto &it : data["events"]) - ProcessEvent(it); - - ForkThread(&CIcqProto::PollThread); -} - -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 char *szContentType = Netlib_GetHeader(pReply, "Content-Type"); - if (szContentType == nullptr) - szContentType = "image/jpeg"; - - ai.format = ProtoGetAvatarFormatByMimeType(szContentType); - 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 ReportSelfAvatarChanged(); -} - -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["persons"]) { - CMStringW wszId = it["sn"].as_mstring(); - if (wszId == m_szOwnId) - continue; - - CMStringW wszNick = it["friendly"].as_mstring(); - CMStringW wszFirst = it["firstName"].as_mstring(); - CMStringW wszLast = it["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); - - mir_cslock lck(m_csOwnIds); - m_arOwnIds.remove(ownMsg); - } - - if (g_bMessageState) - CallService(MS_MESSAGESTATE_UPDATE, ownMsg->m_hContact, MRD_TYPE_DELIVERED); - - 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::OnSessionEnd(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) -{ - JsonReply root(pReply); - if (root.error() == 200) { - m_szAToken.Empty(); - delSetting(DB_KEY_ATOKEN); - - m_szSessionKey.Empty(); - delSetting(DB_KEY_SESSIONKEY); - - ShutdownSession(); - } -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 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, "libcrypto.lib") + +void CIcqProto::CheckAvatarChange(MCONTACT hContact, const JSONNode &ev) +{ + CMStringW wszIconId(ev["bigIconId"].as_mstring()); + if (wszIconId.IsEmpty()) + wszIconId = ev["iconId"].as_mstring(); + + if (!wszIconId.IsEmpty()) { + CMStringW oldIconID(getMStringW(hContact, "IconId")); + if (wszIconId == oldIconID) { + wchar_t wszFullName[MAX_PATH]; + GetAvatarFileName(hContact, wszFullName, _countof(wszFullName)); + if (_waccess(wszFullName, 0) == 0) + return; + } + + setWString(hContact, "IconId", wszIconId); + + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/expressions/get", &CIcqProto::OnReceiveAvatar); + pReq << CHAR_PARAM("f", "native") << WCHAR_PARAM("t", GetUserId(hContact)) << CHAR_PARAM("type", "bigBuddyIcon"); + pReq->hContact = hContact; + Push(pReq); + } + else delSetting(hContact, "IconId"); +} + +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); +} + +MCONTACT CIcqProto::CheckOwnMessage(const CMStringA &reqId, const CMStringA &msgId, bool bRemove) +{ + IcqOwnMessage *pOwn = nullptr; + { + mir_cslock lck(m_csOwnIds); + for (auto &it : m_arOwnIds) { + if (reqId == it->m_guid) { + pOwn = it; + break; + } + } + } + + if (pOwn == nullptr) + return 0; + + ProtoBroadcastAck(pOwn->m_hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)pOwn->m_msgid, (LPARAM)msgId.c_str()); + + MCONTACT ret = pOwn->m_hContact; + if (bRemove) { + mir_cslock lck(m_csOwnIds); + m_arOwnIds.remove(pOwn); + } + return ret; +} + +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()) { + StartSession(); + return; + } + + if (m_isMra) { + m_bError462 = false; + SendMrimLogin(nullptr); + } + else { + 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", appId()) + << CHAR_PARAM("f", "json") << CHAR_PARAM("tokenType", "longTerm") << WCHAR_PARAM("s", m_szOwnId) << CHAR_PARAM("pwd", m_szPassword); + #ifndef _DEBUG + pReq->flags |= NLHRF_NODUMPSEND; + #endif + Push(pReq); + } +} + +IcqFileInfo* CIcqProto::CheckFile(MCONTACT hContact, CMStringW &wszText, bool &bIsFile) +{ + CMStringW wszUrl(wszText.Mid(26)); + int idx = wszUrl.Find(' '); + if (idx != -1) + wszUrl.Truncate(idx); + + bIsFile = false; + IcqFileInfo *pFileInfo = nullptr; + + // is it already downloaded sticker? + CMStringW wszLoadedPath(FORMAT, L"%s\\%S\\Stickers\\STK{%s}.png", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName, wszUrl.c_str()); + if (!_waccess(wszLoadedPath, 0)) { + pFileInfo = (IcqFileInfo *)this; + wszText.Format(L"STK{%s}", wszUrl.c_str()); + } + else { + // download file info + CMStringA szUrl(FORMAT, ICQ_FILE_SERVER "/info/%S/", wszUrl.c_str()); + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, szUrl, &CIcqProto::OnFileInfo); + pReq->hContact = hContact; + pReq->pUserInfo = &pFileInfo; + pReq << CHAR_PARAM("aimsid", m_aimsid) << CHAR_PARAM("previews", "192,600,xlarge"); + if (!ExecuteRequest(pReq)) + return nullptr; + + // is it a sticker? + if (pFileInfo->bIsSticker) { + if (ServiceExists(MS_SMILEYADD_LOADCONTACTSMILEYS)) { + auto *pNew = new AsyncHttpRequest(CONN_NONE, REQUEST_GET, pFileInfo->szUrl, &CIcqProto::OnGetSticker); + pNew->flags |= NLHRF_NODUMP | NLHRF_SSL | NLHRF_HTTP11 | NLHRF_REDIRECT; + pNew->pUserInfo = wszUrl.GetBuffer(); + pNew->AddHeader("Sec-Fetch-User", "?1"); + pNew->AddHeader("Sec-Fetch-Site", "cross-site"); + pNew->AddHeader("Sec-Fetch-Mode", "navigate"); + if (!ExecuteRequest(pNew)) + return nullptr; + + wszText.Format(L"STK{%s}", wszUrl.c_str()); + delete pFileInfo; + } + else wszText = TranslateT("SmileyAdd plugin required to support stickers"); + } + else bIsFile = true; + } + + return pFileInfo; +} + +void CIcqProto::CheckStatus() +{ + time_t now = time(0); + int diff1 = m_iTimeDiff1, diff2 = m_iTimeDiff2; + + for (auto &it : m_arCache) { + // this contact is really offline and is on the first timer + // if the first timer is expired, we clear it and look for the second status + if (diff1 && it->m_timer1 && now - it->m_timer1 > diff1) { + it->m_timer1 = 0; + + // if the second timer is set up, activate it + if (m_iTimeDiff2) { + setWord(it->m_hContact, "Status", m_iStatus2); + it->m_timer2 = now; + } + // if the second timer is not set, simply mark a contact as offline + else setWord(it->m_hContact, "Status", ID_STATUS_OFFLINE); + continue; + } + + // if the second timer is expired, set status to offline + if (diff2 && it->m_timer2 && now - it->m_timer2 > m_iTimeDiff2) { + it->m_timer2 = 0; + setWord(it->m_hContact, "Status", ID_STATUS_OFFLINE); + } + } +} + +void CIcqProto::ConnectionFailed(int iReason, int iErrorCode) +{ + debugLogA("ConnectionFailed -> reason %d", iReason); + + if (m_bErrorPopups) { + POPUPDATAW Popup = {}; + Popup.lchIcon = IcoLib_GetIconByHandle(Skin_GetIconHandle(SKINICON_ERROR), true); + wcscpy_s(Popup.lpwzContactName, m_tszUserName); + switch (iReason) { + case LOGINERR_BADUSERID: + mir_snwprintf(Popup.lpwzText, TranslateT("You have not entered a login or password.\nConfigure this in Options -> Network -> ICQ and try again.")); + break; + case LOGINERR_WRONGPASSWORD: + mir_snwprintf(Popup.lpwzText, TranslateT("Connection failed.\nYour login or password was rejected (%d)."), iErrorCode); + break; + case LOGINERR_NONETWORK: + case LOGINERR_NOSERVER: + mir_snwprintf(Popup.lpwzText, TranslateT("Connection failed.\nThe server is temporarily unavailable (%d)."), iErrorCode); + break; + default: + mir_snwprintf(Popup.lpwzText, TranslateT("Connection failed.\nUnknown error during sign on: %d"), iErrorCode); + break; + } + PUAddPopupW(&Popup); + } + + ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, nullptr, iReason); + ShutdownSession(); +} + +void CIcqProto::MoveContactToGroup(MCONTACT hContact, const wchar_t *pwszGroup, const wchar_t *pwszNewGroup) +{ + // otherwise we'll get a server error + if (!mir_wstrlen(pwszGroup)) + return; + + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/moveBuddy") << AIMSID(this) << WCHAR_PARAM("buddy", GetUserId(hContact)) + << GROUP_PARAM("group", pwszGroup); + if (mir_wstrlen(pwszNewGroup)) + pReq << GROUP_PARAM("newGroup", pwszNewGroup); + Push(pReq); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::OnLoggedIn() +{ + debugLogA("CIcqProto::OnLoggedIn"); + m_bOnline = true; + m_impl.m_heartBeat.Start(1000); + + for (auto &it : m_arCache) + it->m_timer1 = it->m_timer2 = 0; + + SetServerStatus(m_iDesiredStatus); + RetrieveUserInfo(0); + GetPermitDeny(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::OnLoggedOut() +{ + debugLogA("CIcqProto::OnLoggedOut"); + m_bOnline = false; + m_impl.m_heartBeat.Stop(); + + for (auto &it : m_arCache) + it->m_timer1 = it->m_timer2 = 0; + + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE); + m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; + + setAllContactStatuses(ID_STATUS_OFFLINE, false); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::MarkAsRead(MCONTACT hContact) +{ + if (!m_bOnline) + return; + + m_impl.m_markRead.Start(200); + + auto *pCache = FindContactByUIN(GetUserId(hContact)); + if (pCache) { + mir_cslock lck(m_csMarkReadQueue); + if (m_arMarkReadQueue.indexOf(pCache) == -1) + m_arMarkReadQueue.insert(pCache); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +MCONTACT CIcqProto::ParseBuddyInfo(const JSONNode &buddy, MCONTACT hContact, bool bIsPartial) +{ + // user chat? + CMStringW wszId(buddy["aimId"].as_mstring()); + if (IsChat(wszId)) { + CMStringW wszChatId(buddy["aimId"].as_mstring()); + CMStringW wszChatName(buddy["friendly"].as_mstring()); + + auto *pContact = FindContactByUIN(wszId); + if (pContact && pContact->m_iApparentMode == ID_STATUS_OFFLINE) + return INVALID_CONTACT_ID; + + auto *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; + } + + bool bIgnored = !IsValidType(buddy); + if (hContact == INVALID_CONTACT_ID) { + if (bIgnored) + return INVALID_CONTACT_ID; + + hContact = CreateContact(wszId, false); + FindContactByUIN(wszId)->m_bInList = true; + } + else if (bIgnored) { + db_delete_contact(hContact); + return INVALID_CONTACT_ID; + } + + CMStringA szVer; + bool bVersionDetected = false, bSecureIM = false; + + for (auto &it : buddy["capabilities"]) { + CMStringW wszCap(it.as_mstring()); + if (wszCap.GetLength() != 32) + continue; + + uint8_t cap[16]; + hex2binW(wszCap, cap, sizeof(cap)); + if (!memcmp(cap, "MiNG", 4)) { // Miranda + int v[4]; + if (4 == swscanf(wszCap.c_str() + 16, L"%04x%04x%04x%04x", &v[0], &v[1], &v[2], &v[3])) { + szVer.Format("Miranda NG %d.%d.%d.%d (ICQ %d.%d.%d.%d)", v[0], v[1], v[2], v[3], cap[4], cap[5], cap[6], cap[7]); + setString(hContact, "MirVer", szVer); + bVersionDetected = true; + } + } + else if (wszCap == _A2W(NG_CAP_SECUREIM)) { + bSecureIM = bVersionDetected = true; + } + else if (!memcmp(cap, "Mod by Mikanoshi", 16)) { + szVer = "R&Q build by Mikanoshi"; + bVersionDetected = true; + } + else if (!memcmp(cap, "Mandarin IM", 11)) { + szVer = "Mandarin IM"; + bVersionDetected = true; + } + } + + if (bVersionDetected) { + if (bSecureIM) + szVer.Append(" + SecureIM"); + setString(hContact, "MirVer", szVer); + } + else delSetting(hContact, "MirVer"); + + const JSONNode &var = buddy["friendly"]; + if (var) + setWString(hContact, "Nick", var.as_mstring()); + + if (buddy["deleted"].as_bool()) { + setByte(hContact, "IcqDeleted", 1); + Contact::PutOnList(hContact); + } + + Json2string(hContact, buddy, "emailId", "Email", bIsPartial); + Json2string(hContact, buddy, "cellNumber", "Cellular", bIsPartial); + Json2string(hContact, buddy, "workNumber", "CompanyPhone", bIsPartial); + + // we shall not remove existing phone number anyhow + Json2string(hContact, buddy, "phoneNumber", DB_KEY_PHONE, true); + + Json2int(hContact, buddy, "official", "Official", bIsPartial); + Json2int(hContact, buddy, "onlineTime", DB_KEY_ONLINETS, bIsPartial); + Json2int(hContact, buddy, "idleTime", "IdleTS", bIsPartial); + Json2int(hContact, buddy, "memberSince", DB_KEY_MEMBERSINCE, bIsPartial); + + int iStatus = StatusFromPresence(buddy, hContact); + if (iStatus > 0) + setWord(hContact, "Status", iStatus); + + const JSONNode &profile = buddy["profile"]; + if (profile) { + Json2string(hContact, profile, "friendlyName", DB_KEY_ICQNICK, bIsPartial); + Json2string(hContact, profile, "firstName", "FirstName", bIsPartial); + Json2string(hContact, profile, "lastName", "LastName", bIsPartial); + Json2string(hContact, profile, "aboutMe", DB_KEY_ABOUT, bIsPartial); + + ptrW wszNick(getWStringA(hContact, "Nick")); + if (!wszNick) { + CMStringW srvNick = profile["friendlyName"].as_mstring(); + if (!srvNick.IsEmpty()) + setWString(hContact, "Nick", srvNick); + } + + 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); + } + } + + CMStringW 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"]) { + Json2string(hContact, it, "city", "City", bIsPartial); + Json2string(hContact, it, "state", "State", bIsPartial); + Json2string(hContact, it, "country", "Country", bIsPartial); + } + } + + CMStringW 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, bool bCreateRead, bool bLocalTime) +{ + CMStringA szMsgId(it["msgId"].as_mstring()); + __int64 msgId = _atoi64(szMsgId); + if (msgId > lastMsgId) + lastMsgId = msgId; + + CMStringW wszText; + const JSONNode &sticker = it["sticker"]; + if (sticker) { + CMStringW wszUrl, wszSticker(sticker["id"].as_mstring()); + int iCollectionId, iStickerId; + if (2 == swscanf(wszSticker, L"ext:%d:sticker:%d", &iCollectionId, &iStickerId)) + wszUrl.Format(L"https://c.icq.com/store/stickers/%d/%d/medium", iCollectionId, iStickerId); + else + wszUrl = TranslateT("Unknown sticker"); + wszText.Format(L"%s\n%s", TranslateT("User sent a sticker:"), wszUrl.c_str()); + } + else { + wszText = it["text"].as_mstring(); + wszText.TrimRight(); + + // user added you + if (it["class"].as_mstring() == L"event" && it["eventTypeId"].as_mstring() == L"27:33000") { + if (bLocalTime) { + CMStringA id = getMStringA(hContact, DB_KEY_ID); + int pos = id.Find('@'); + CMStringA nick = (pos == -1) ? id : id.Left(pos); + DB::AUTH_BLOB blob(hContact, nick, nullptr, nullptr, id, nullptr); + + PROTORECVEVENT pre = {}; + pre.timestamp = (uint32_t)time(0); + pre.lParam = blob.size(); + pre.szMessage = blob; + ProtoChainRecv(hContact, PSR_AUTH, 0, (LPARAM)&pre); + } + return; + } + } + + int iMsgTime = (bLocalTime) ? time(0) : it["time"].as_int(); + bool bIsOutgoing = it["outgoing"].as_bool(), bIsFileTransfer = false; + IcqFileInfo *pFileInfo = nullptr; + + if (!bCreateRead && !bIsOutgoing && wszText.Left(26) == L"https://files.icq.net/get/") { + pFileInfo = CheckFile(hContact, wszText, bIsFileTransfer); + if (!pFileInfo) + return; + + for (auto &jt : it["parts"]) { + CMStringW wszDescr(jt["captionedContent"]["caption"].as_mstring()); + if (!wszDescr.IsEmpty()) + pFileInfo->wszDescr = wszDescr; + } + } + + 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")); + + if (bIsFileTransfer) { + wszText = pFileInfo->szUrl; + if (!pFileInfo->wszDescr) + wszText.AppendFormat(L"\r\n%s", pFileInfo->wszDescr.c_str()); + delete pFileInfo; + } + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE }; + gce.pszID.w = wszChatId; + gce.dwFlags = GCEF_ADDTOLOG; + gce.pszUID.w = wszSender; + gce.pszText.w = wszText; + gce.time = iMsgTime; + gce.bIsMe = wszSender == m_szOwnId; + Chat_Event(&gce); + return; + } + + // skip own messages, just set the server msgid + CMStringA reqId(it["reqId"].as_mstring()); + if (CheckOwnMessage(reqId, szMsgId, true)) { + debugLogA("Skipping our own message %s", szMsgId.c_str()); + return; + } + + // ignore duplicates + MEVENT hDbEvent = db_event_getById(m_szModuleName, szMsgId); + if (hDbEvent != 0) { + debugLogA("Message %s already exists", szMsgId.c_str()); + return; + } + + // convert a file info into Miranda's file transfer + if (bIsFileTransfer) { + auto *ft = new IcqFileTransfer(hContact, pFileInfo->szUrl); + ft->pfts.totalBytes = ft->pfts.currentFileSize = pFileInfo->dwFileSize; + ft->pfts.szCurrentFile.w = ft->m_wszFileName.GetBuffer(); + + PROTORECVFILE pre = {}; + pre.dwFlags = PRFF_UNICODE; + pre.fileCount = 1; + pre.timestamp = iMsgTime; + pre.files.w = &ft->m_wszShortName; + pre.descr.w = pFileInfo->wszDescr; + pre.lParam = (LPARAM)ft; + ProtoChainRecvFile(hContact, &pre); + + delete pFileInfo; + return; + } + + // suppress notifications for already loaded/processed messages + __int64 storedLastId = getId(hContact, DB_KEY_LASTMSGID); + if (msgId <= storedLastId) { + debugLogA("Parsing old/processed message with id %lld < %lld, setting CR to true", msgId, storedLastId); + bCreateRead = true; + } + + debugLogA("Adding message %d:%lld (CR=%d)", hContact, msgId, bCreateRead); + + ptrA szUtf(mir_utf8encodeW(wszText)); + + PROTORECVEVENT pre = {}; + if (bIsOutgoing) pre.flags |= PREF_SENT; + if (bCreateRead) pre.flags |= PREF_CREATEREAD; + pre.szMsgId = szMsgId; + pre.timestamp = iMsgTime; + pre.szMessage = szUtf; + ProtoChainRecvMsg(hContact, &pre); +} + +bool CIcqProto::RefreshRobustToken(AsyncHttpRequest *pOrigReq) +{ + if (!m_szRToken.IsEmpty()) + return true; + + auto *pReq = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER "/genToken", &CIcqProto::OnGenToken); + #ifndef _DEBUG + pReq->flags |= NLHRF_NODUMPSEND; + #endif + + int ts = TS(); + pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("k", appId()) << CHAR_PARAM("nonce", CMStringA(FORMAT, "%d-%d", ts, rand() % 10)) << INT_PARAM("ts", ts); + CalcHash(pReq); + + CMStringA szAgent(FORMAT, "%S Mail.ru Windows ICQ (version 10.0.1999)", (wchar_t*)m_szOwnId); + pReq->AddHeader("User-Agent", szAgent); + if (!ExecuteRequest(pReq)) { +LBL_Error: + (this->*(pOrigReq->m_pFunc))(nullptr, pOrigReq); + return false; + } + if (m_szRToken.IsEmpty()) + goto LBL_Error; + + // now add this token + bool bRet = false; + pReq = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER "/addClient", &CIcqProto::OnAddClient); + #ifndef _DEBUG + pReq->flags |= NLHRF_NODUMPSEND; + #endif + pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("f", "json") << CHAR_PARAM("k", appId()) << INT_PARAM("ts", ts) + << CHAR_PARAM("client", "icq") << CHAR_PARAM("reqId", pReq->m_reqId) << CHAR_PARAM("authToken", m_szRToken); + pReq->pUserInfo = &bRet; + if (!ExecuteRequest(pReq)) + goto LBL_Error; + + return bRet; +} + +void CIcqProto::RetrieveUserInfo(MCONTACT hContact) +{ + auto *pReq = new AsyncRapiRequest(this, "getUserInfo", &CIcqProto::OnGetUserInfo); + pReq->params << WCHAR_PARAM("sn", GetUserId(hContact)); + pReq->hContact = hContact; + Push(pReq); +} + +void CIcqProto::RetrieveUserHistory(MCONTACT hContact, __int64 startMsgId, bool bCreateRead) +{ + if (startMsgId == 0) + startMsgId = -1; + + __int64 patchVer = getId(hContact, DB_KEY_PATCHVER); + if (patchVer == 0) + patchVer = 1; + + auto *pReq = new AsyncRapiRequest(this, "getHistory", &CIcqProto::OnGetUserHistory); + #ifndef _DEBUG + pReq->flags |= NLHRF_NODUMPSEND; + #endif + pReq->hContact = hContact; + pReq->pUserInfo = (bCreateRead) ? pReq : 0; + pReq->params << WCHAR_PARAM("sn", GetUserId(hContact)) << INT64_PARAM("fromMsgId", startMsgId) << INT_PARAM("count", 1000) + << SINT64_PARAM("patchVersion", patchVer) << CHAR_PARAM("language", "ru-ru"); + 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; + } + + Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/presence/setState") + << AIMSID(this) << CHAR_PARAM("view", szStatus) << INT_PARAM("invisible", invisible)); + + if (iStatus == ID_STATUS_OFFLINE && !getByte(DB_KEY_PHONEREG)) { + auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_GET, ICQ_API_SERVER "/aim/endSession", &CIcqProto::OnSessionEnd); + pReq << AIMSID(this) << INT_PARAM("invalidateToken", 1); + ExecuteRequest(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 + DropQueue(); + + if (m_hWorkerThread) + SetEvent(m_evRequestsQueue); + + OnLoggedOut(); + + for (auto &it : m_ConnPool) { + if (it.s) { + Netlib_Shutdown(it.s); + it.s = nullptr; + } + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +#define EVENTS "myInfo,presence,buddylist,typing,dataIM,userAddedToBuddyList,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 = TS(); + CMStringA nonce(FORMAT, "%d-2", ts); + CMStringA caps(WIM_CAP_UNIQ_REQ_ID "," WIM_CAP_EMOJI "," WIM_CAP_MAIL_NOTIFICATIONS "," WIM_CAP_INTRO_DLG_STATE); + if (g_bSecureIM) { + caps.AppendChar(','); + caps.Append(NG_CAP_SECUREIM); + } + + MFileVersion v; + Miranda_GetFileVersion(&v); + caps.AppendFormat(",%02x%02x%02x%02x%02x%02x%02x%02x%04x%04x%04x%04x", 'M', 'i', 'N', 'G', + __MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM, v[0], v[1], v[2], v[3]); + + 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", appId()) << 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) + return; + + CMStringW wszId = getMStringW(pReq->hContact, DB_KEY_ID); + for (auto &it : root.data()["results"]) { + if (it["buddy"].as_mstring() != wszId) + continue; + + switch (int iResultCode = it["resultCode"].as_int()) { + case 0: // success + case 3: // already in contact list + break; + + default: + debugLogA("Contact %d failed to add: error %d", pReq->hContact, iResultCode); + + POPUPDATAW Popup = {}; + Popup.lchIcon = IcoLib_GetIconByHandle(Skin_GetIconHandle(SKINICON_ERROR)); + wcsncpy_s(Popup.lpwzText, TranslateT("Buddy addition failed"), _TRUNCATE); + wcsncpy_s(Popup.lpwzContactName, Clist_GetContactDisplayName(pReq->hContact), _TRUNCATE); + Popup.iSeconds = 20; + PUAddPopupW(&Popup); + + // Contact::RemoveFromList(pReq->hContact); + } + + RetrieveUserInfo(pReq->hContact); + Contact::PutOnList(pReq->hContact); + } +} + +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, root.error()); + return; + + default: + ConnectionFailed(LOGINERR_WRONGPROTOCOL, root.error()); + return; + } + + JSONNode &data = root.data(); + m_szAToken = data["token"]["a"].as_mstring(); + mir_urlDecode(m_szAToken.GetBuffer()); + setString(DB_KEY_ATOKEN, m_szAToken); + + CMStringA szSessionSecret = data["sessionSecret"].as_mstring(); + CMStringA szPassTemp = m_szPassword; + + unsigned int len; + uint8_t hashOut[MIR_SHA256_HASH_SIZE]; + HMAC(EVP_sha256(), szPassTemp, szPassTemp.GetLength(), (uint8_t*)szSessionSecret.c_str(), szSessionSecret.GetLength(), hashOut, &len); + m_szSessionKey = ptrA(mir_base64_encode(hashOut, sizeof(hashOut))); + setString(DB_KEY_SESSIONKEY, m_szSessionKey); + + CMStringW szUin = data["loginId"].as_mstring(); + if (szUin) + m_szOwnId = szUin; + + int srvTS = data["hostTime"].as_int(); + m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; + + StartSession(); +} + +void CIcqProto::OnFileContinue(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pOld) +{ + IcqFileTransfer *pTransfer = (IcqFileTransfer*)pOld->pUserInfo; + if (pTransfer->m_bCanceled) { +LBL_Error: + ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, pTransfer); + delete pTransfer; + return; + } + + switch (pReply->resultCode) { + case 200: // final ok + case 206: // partial ok + break; + + default: + goto LBL_Error; + } + + // file transfer succeeded? + if (pTransfer->pfts.currentFileProgress == pTransfer->pfts.currentFileSize) { + FileReply root(pReply); + if (root.error() != 200) + goto LBL_Error; + + ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, pTransfer); + + const JSONNode &data = root.data(); + CMStringW wszUrl(data["static_url"].as_mstring()); + + JSONNode bundle, contents; contents.set_name("captionedContent"); + contents << WCHAR_PARAM("caption", pTransfer->m_wszDescr) << WCHAR_PARAM("url", wszUrl); + bundle << CHAR_PARAM("mediaType", "text") << CHAR_PARAM("text", "") << contents; + CMStringW wszParts(FORMAT, L"[%s]", ptrW(json_write(&bundle)).get()); + + if (!pTransfer->m_wszDescr.IsEmpty()) + wszUrl += L" " + pTransfer->m_wszDescr; + + int id = InterlockedIncrement(&m_msgId); + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/im/sendIM", &CIcqProto::OnSendMessage); + + auto *pOwn = new IcqOwnMessage(pTransfer->pfts.hContact, id, pReq->m_reqId); + pReq->pUserInfo = pOwn; + { + mir_cslock lck(m_csOwnIds); + m_arOwnIds.insert(pOwn); + } + + pReq << AIMSID(this) << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("k", appId()) << CHAR_PARAM("mentions", "") << WCHAR_PARAM("message", wszUrl) + << CHAR_PARAM("offlineIM", "true") << WCHAR_PARAM("parts", wszParts) << WCHAR_PARAM("t", GetUserId(pTransfer->pfts.hContact)) << INT_PARAM("ts", TS()); + Push(pReq); + + delete pTransfer; + return; + } + + // else send the next portion + auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, pTransfer->m_szHost, &CIcqProto::OnFileContinue); + pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("client", "icq") << CHAR_PARAM("k", appId()) << INT_PARAM("ts", TS()); + CalcHash(pReq); + pReq->m_szUrl.AppendChar('?'); + pReq->m_szUrl += pReq->m_szParam; pReq->m_szParam.Empty(); + pReq->pUserInfo = pTransfer; + pTransfer->FillHeaders(pReq); + Push(pReq); + + pTransfer->pfts.currentFileTime = time(0); + ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_DATA, pTransfer, (LPARAM)&pTransfer->pfts); +} + +void CIcqProto::OnFileInit(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pOld) +{ + IcqFileTransfer *pTransfer = (IcqFileTransfer*)pOld->pUserInfo; + if (pTransfer->m_bCanceled) { +LBL_Error: + ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, pTransfer); + delete pTransfer; + return; + } + + FileReply root(pReply); + if (root.error() != 200) + goto LBL_Error; + + ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_INITIALISING, pTransfer); + pTransfer->pfts.currentFileTime = time(0); + + const JSONNode &data = root.data(); + CMStringW wszHost(data["host"].as_mstring()); + CMStringW wszUrl(data["url"].as_mstring()); + pTransfer->m_szHost = L"https://" + wszHost + wszUrl; + + auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, pTransfer->m_szHost, &CIcqProto::OnFileContinue); + pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("client", "icq") << CHAR_PARAM("k", appId()) << INT_PARAM("ts", TS()); + CalcHash(pReq); + pReq->m_szUrl.AppendChar('?'); + pReq->m_szUrl += pReq->m_szParam; pReq->m_szParam.Empty(); + pReq->pUserInfo = pTransfer; + pTransfer->FillHeaders(pReq); + Push(pReq); + + ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_DATA, pTransfer, (LPARAM)&pTransfer->pfts); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Support for stickers + +void CIcqProto::OnGetSticker(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + if (pReply->resultCode != 200) { + debugLogA("Error getting sticker: %d", pReply->resultCode); + return; + } + + CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName); + CreateDirectoryTreeW(wszPath); + + CMStringW wszFileName(FORMAT, L"%s\\STK{%s}.png", wszPath.c_str(), pReq->pUserInfo); + FILE *out = _wfopen(wszFileName, L"wb"); + fwrite(pReply->pData, 1, pReply->dataLength, out); + fclose(out); + + SMADD_CONT cont = { 1, m_szModuleName, wszFileName }; + CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// File info request + +void CIcqProto::OnFileInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + IcqFileInfo **res = (IcqFileInfo **)pReq->pUserInfo; + *res = nullptr; + + RobustReply root(pReply); + if (root.error() != 200) + return; + + auto &pData = root.result(); + auto &pInfo = pData["info"] ; + std::string szUrl(pInfo["dlink"].as_string()); + if (szUrl.empty()) + return; + + MarkAsRead(pReq->hContact); + + bool bIsSticker; + CMStringW wszDescr(pInfo["file_name"].as_mstring()); + if (!mir_wstrncmp(wszDescr, L"dnld", 4)) { + bIsSticker = true; + + std::string szPreview = pData["previews"]["192"].as_string(); + if (!szPreview.empty()) + szUrl = szPreview; + } + else bIsSticker = false; + + mir_urlDecode(&*szUrl.begin()); + + *res = new IcqFileInfo(szUrl, wszDescr, pInfo["file_size"].as_int()); + res[0]->bIsSticker = bIsSticker; +} + +void CIcqProto::OnFileRecv(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + auto *ft = (IcqFileTransfer*)pReq->pUserInfo; + + if (pReply->resultCode != 200) { +LBL_Error: + FileCancel(pReq->hContact, ft); + return; + } + + ft->hWaitEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (ProtoBroadcastAck(ft->pfts.hContact, ACKTYPE_FILE, ACKRESULT_FILERESUME, ft, (LPARAM)&ft->pfts)) + WaitForSingleObject(ft->hWaitEvent, INFINITE); + CloseHandle(ft->hWaitEvent); + + debugLogW(L"Saving to [%s]", ft->pfts.szCurrentFile.w); + int fileId = _wopen(ft->pfts.szCurrentFile.w, _O_BINARY | _O_CREAT | _O_TRUNC | _O_WRONLY, _S_IREAD | _S_IWRITE); + if (fileId == -1) { + debugLogW(L"Cannot open [%s] for writing", ft->pfts.szCurrentFile.w); + goto LBL_Error; + } + + int result = _write(fileId, pReply->pData, pReply->dataLength); + _close(fileId); + if (result != pReply->dataLength) { + debugLogW(L"Error writing data into [%s]", ft->pfts.szCurrentFile.w); + goto LBL_Error; + } + + ft->pfts.totalProgress += pReply->dataLength; + ft->pfts.currentFileProgress += pReply->dataLength; + ProtoBroadcastAck(ft->pfts.hContact, ACKTYPE_FILE, ACKRESULT_DATA, ft, (LPARAM)&ft->pfts); + + ProtoBroadcastAck(ft->pfts.hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, ft); + delete ft; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::OnGenToken(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) +{ + RobustReply root(pReply); + if (root.error() != 20000) + return; + + auto &results = root.results(); + m_szRToken = results["authToken"].as_mstring(); +} + +void CIcqProto::OnGetUserHistory(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + RobustReply root(pReply); + if (root.error() != 20000) + return; + + __int64 lastMsgId = getId(pReq->hContact, DB_KEY_LASTMSGID); + + int count = 0; + auto &results = root.results(); + for (auto &it : results["messages"]) { + ParseMessage(pReq->hContact, lastMsgId, it, pReq->pUserInfo != nullptr, false); + count++; + } + + setId(pReq->hContact, DB_KEY_LASTMSGID, lastMsgId); + + if (count >= 999) + RetrieveUserHistory(pReq->hContact, lastMsgId, pReq->pUserInfo != nullptr); +} + +void CIcqProto::OnGetUserInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + RobustReply root(pReply); + if (root.error() != 20000) { + ProtoBroadcastAck(pReq->hContact, ACKTYPE_GETINFO, ACKRESULT_FAILED, nullptr); + return; + } + + ParseBuddyInfo(root.results(), pReq->hContact, true); + + ProtoBroadcastAck(pReq->hContact, ACKTYPE_GETINFO, ACKRESULT_SUCCESS, nullptr); +} + +void CIcqProto::OnStartSession(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) +{ + JsonReply root(pReply); + switch (root.error()) { + case 200: + break; + + case 451: + // session forcibly closed from site + delSetting(DB_KEY_ATOKEN); + delSetting(DB_KEY_SESSIONKEY); + CheckPassword(); + return; + + case 401: + delSetting(DB_KEY_ATOKEN); + delSetting(DB_KEY_SESSIONKEY); + if (root.detail() == 1002) // session expired + CheckPassword(); + else + ConnectionFailed(LOGINERR_WRONGPASSWORD, root.error()); + return; + + case 400: + if (root.detail() == 1015 && m_iTimeShift == 0) { // wrong timestamp + JSONNode &data = root.data(); + int srvTS = data["ts"].as_int(); + m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; + StartSession(); + return; + } + __fallthrough; + + default: + ConnectionFailed(LOGINERR_WRONGPROTOCOL, root.error()); + return; + } + + JSONNode &data = root.data(); + m_fetchBaseURL = data["fetchBaseURL"].as_mstring(); + m_aimsid = data["aimsid"].as_mstring(); + + ProcessMyInfo(data["myInfo"]); + + int srvTS = data["ts"].as_int(); + m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; + + OnLoggedIn(); + + for (auto &it : data["events"]) + ProcessEvent(it); + + ForkThread(&CIcqProto::PollThread); +} + +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 char *szContentType = Netlib_GetHeader(pReply, "Content-Type"); + if (szContentType == nullptr) + szContentType = "image/jpeg"; + + ai.format = ProtoGetAvatarFormatByMimeType(szContentType); + 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 ReportSelfAvatarChanged(); +} + +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["persons"]) { + CMStringW wszId = it["sn"].as_mstring(); + if (wszId == m_szOwnId) + continue; + + CMStringW wszNick = it["friendly"].as_mstring(); + CMStringW wszFirst = it["firstName"].as_mstring(); + CMStringW wszLast = it["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); + + mir_cslock lck(m_csOwnIds); + m_arOwnIds.remove(ownMsg); + } + + if (g_bMessageState) + CallService(MS_MESSAGESTATE_UPDATE, ownMsg->m_hContact, MRD_TYPE_DELIVERED); + + 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::OnSessionEnd(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) +{ + JsonReply root(pReply); + if (root.error() == 200) { + m_szAToken.Empty(); + delSetting(DB_KEY_ATOKEN); + + m_szSessionKey.Empty(); + delSetting(DB_KEY_SESSIONKEY); + + ShutdownSession(); + } +} diff --git a/protocols/ICQ-WIM/src/stdafx.cxx b/protocols/ICQ-WIM/src/stdafx.cxx index d265a4c02e..8c570f6949 100644 --- a/protocols/ICQ-WIM/src/stdafx.cxx +++ b/protocols/ICQ-WIM/src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright (C) 2012-22 Miranda NG team (https://miranda-ng.org) - -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 version 2 -of the License. - -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" +/* +Copyright (C) 2012-23 Miranda NG team (https://miranda-ng.org) + +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 version 2 +of the License. + +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" diff --git a/protocols/ICQ-WIM/src/stdafx.h b/protocols/ICQ-WIM/src/stdafx.h index 465a385f02..b45810a867 100644 --- a/protocols/ICQ-WIM/src/stdafx.h +++ b/protocols/ICQ-WIM/src/stdafx.h @@ -1,109 +1,109 @@ -// ---------------------------------------------------------------------------80 -// ICQ plugin for Miranda Instant Messenger -// ________________________________________ -// -// Copyright © 2000-2001 Richard Hughes, Roland Rabien, Tristan Van de Vreede -// Copyright © 2001-2002 Jon Keating, Richard Hughes -// Copyright © 2002-2004 Martin Öberg, Sam Kothari, Robert Rainwater -// Copyright © 2004-2010 Joe Kucera -// Copyright © 2012-2022 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. -// ----------------------------------------------------------------------------- -// DESCRIPTION: -// -// Includes all header files that should be precompiled to speed up compilation. -// ----------------------------------------------------------------------------- - -#pragma once - -// Windows includes -#include - -// Standard includes -#include -#include -#include -#include -#include -#include -#include - -// Miranda IM SDK includes -#include // This must be included first -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -// Project resources -#include "resource.h" - -// ICQ plugin includes -#include "version.h" - -#define MODULENAME "ICQ" - -#define DB_KEY_ID "aimId" -#define DB_KEY_IDLE "IdleTS" -#define DB_KEY_ABOUT "About" -#define DB_KEY_PHONE "Phone" -#define DB_KEY_ATOKEN "AToken" -#define DB_KEY_ICQNICK "IcqNick" -#define DB_KEY_PHONEREG "PhoneReg" -#define DB_KEY_LASTSEEN "LastSeen" -#define DB_KEY_ONLINETS "OnlineTS" -#define DB_KEY_PATCHVER "PatchVersion" -#define DB_KEY_RCLIENTID "RClientID" -#define DB_KEY_LASTMSGID "LastMsgId" -#define DB_KEY_REMOTEREAD "RemoteReadId" -#define DB_KEY_SESSIONKEY "SessionKey" -#define DB_KEY_MEMBERSINCE "MemberSince" - -#include "http.h" -#include "proto.h" - -bool IsChat(const CMStringW &aimid); -bool IsValidType(const JSONNode &aimid); - -void RefreshGroups(void); -wchar_t* time2text(time_t time); - -extern bool g_bSecureIM, g_bMessageState; +// ---------------------------------------------------------------------------80 +// ICQ plugin for Miranda Instant Messenger +// ________________________________________ +// +// Copyright © 2000-2001 Richard Hughes, Roland Rabien, Tristan Van de Vreede +// Copyright © 2001-2002 Jon Keating, Richard Hughes +// Copyright © 2002-2004 Martin Öberg, Sam Kothari, Robert Rainwater +// Copyright © 2004-2010 Joe Kucera +// Copyright © 2012-2023 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. +// ----------------------------------------------------------------------------- +// DESCRIPTION: +// +// Includes all header files that should be precompiled to speed up compilation. +// ----------------------------------------------------------------------------- + +#pragma once + +// Windows includes +#include + +// Standard includes +#include +#include +#include +#include +#include +#include +#include + +// Miranda IM SDK includes +#include // This must be included first +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// Project resources +#include "resource.h" + +// ICQ plugin includes +#include "version.h" + +#define MODULENAME "ICQ" + +#define DB_KEY_ID "aimId" +#define DB_KEY_IDLE "IdleTS" +#define DB_KEY_ABOUT "About" +#define DB_KEY_PHONE "Phone" +#define DB_KEY_ATOKEN "AToken" +#define DB_KEY_ICQNICK "IcqNick" +#define DB_KEY_PHONEREG "PhoneReg" +#define DB_KEY_LASTSEEN "LastSeen" +#define DB_KEY_ONLINETS "OnlineTS" +#define DB_KEY_PATCHVER "PatchVersion" +#define DB_KEY_RCLIENTID "RClientID" +#define DB_KEY_LASTMSGID "LastMsgId" +#define DB_KEY_REMOTEREAD "RemoteReadId" +#define DB_KEY_SESSIONKEY "SessionKey" +#define DB_KEY_MEMBERSINCE "MemberSince" + +#include "http.h" +#include "proto.h" + +bool IsChat(const CMStringW &aimid); +bool IsValidType(const JSONNode &aimid); + +void RefreshGroups(void); +wchar_t* time2text(time_t time); + +extern bool g_bSecureIM, g_bMessageState; diff --git a/protocols/ICQ-WIM/src/userinfo.cpp b/protocols/ICQ-WIM/src/userinfo.cpp index 887d9c2d92..61f661a31b 100644 --- a/protocols/ICQ-WIM/src/userinfo.cpp +++ b/protocols/ICQ-WIM/src/userinfo.cpp @@ -1,64 +1,64 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-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, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- - -#include "stdafx.h" - -struct IcqUserInfoDlg : public CUserInfoPageDlg -{ - CIcqProto *ppro; - - IcqUserInfoDlg(CIcqProto *_ppro) : - CUserInfoPageDlg(g_plugin, IDD_INFO_ICQ), - ppro(_ppro) - { - } - - bool OnRefresh() override - { - SetDlgItemTextW(m_hwnd, IDC_UIN, ppro->GetUserId(m_hContact)); - SetDlgItemTextW(m_hwnd, IDC_NICK, ppro->getMStringW(m_hContact, DB_KEY_ICQNICK)); - SetDlgItemTextW(m_hwnd, IDC_PHONE, ppro->getMStringW(m_hContact, DB_KEY_PHONE)); - - SetDlgItemTextW(m_hwnd, IDC_IDLETIME, time2text(ppro->getDword(m_hContact, DB_KEY_IDLE))); - SetDlgItemTextW(m_hwnd, IDC_LASTSEEN, time2text(ppro->getDword(m_hContact, DB_KEY_LASTSEEN))); - SetDlgItemTextW(m_hwnd, IDC_MEMBERSINCE, time2text(ppro->getDword(m_hContact, DB_KEY_MEMBERSINCE))); - SetDlgItemTextW(m_hwnd, IDC_ONLINESINCE, time2text(time(0) - ppro->getDword(m_hContact, DB_KEY_ONLINETS))); - return false; - } -}; - -int CIcqProto::OnUserInfoInit(WPARAM wParam, LPARAM hContact) -{ - if (hContact && mir_strcmp(Proto_GetBaseAccountName(hContact), m_szModuleName)) - return 0; - - if (isChatRoom(hContact)) - return 0; - - USERINFOPAGE uip = {}; - uip.flags = ODPF_UNICODE | ODPF_USERINFOTAB | ODPF_DONTTRANSLATE | ODPF_ICON; - uip.dwInitParam = (LPARAM)Skin_GetProtoIcon(m_szModuleName, ID_STATUS_ONLINE); - uip.szTitle.w = L"ICQ"; - uip.szGroup.w = m_tszUserName; - uip.position = -1900000000; - uip.pDialog = new IcqUserInfoDlg(this); - g_plugin.addUserInfo(wParam, &uip); - return 0; -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 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" + +struct IcqUserInfoDlg : public CUserInfoPageDlg +{ + CIcqProto *ppro; + + IcqUserInfoDlg(CIcqProto *_ppro) : + CUserInfoPageDlg(g_plugin, IDD_INFO_ICQ), + ppro(_ppro) + { + } + + bool OnRefresh() override + { + SetDlgItemTextW(m_hwnd, IDC_UIN, ppro->GetUserId(m_hContact)); + SetDlgItemTextW(m_hwnd, IDC_NICK, ppro->getMStringW(m_hContact, DB_KEY_ICQNICK)); + SetDlgItemTextW(m_hwnd, IDC_PHONE, ppro->getMStringW(m_hContact, DB_KEY_PHONE)); + + SetDlgItemTextW(m_hwnd, IDC_IDLETIME, time2text(ppro->getDword(m_hContact, DB_KEY_IDLE))); + SetDlgItemTextW(m_hwnd, IDC_LASTSEEN, time2text(ppro->getDword(m_hContact, DB_KEY_LASTSEEN))); + SetDlgItemTextW(m_hwnd, IDC_MEMBERSINCE, time2text(ppro->getDword(m_hContact, DB_KEY_MEMBERSINCE))); + SetDlgItemTextW(m_hwnd, IDC_ONLINESINCE, time2text(time(0) - ppro->getDword(m_hContact, DB_KEY_ONLINETS))); + return false; + } +}; + +int CIcqProto::OnUserInfoInit(WPARAM wParam, LPARAM hContact) +{ + if (hContact && mir_strcmp(Proto_GetBaseAccountName(hContact), m_szModuleName)) + return 0; + + if (isChatRoom(hContact)) + return 0; + + USERINFOPAGE uip = {}; + uip.flags = ODPF_UNICODE | ODPF_USERINFOTAB | ODPF_DONTTRANSLATE | ODPF_ICON; + uip.dwInitParam = (LPARAM)Skin_GetProtoIcon(m_szModuleName, ID_STATUS_ONLINE); + uip.szTitle.w = L"ICQ"; + uip.szGroup.w = m_tszUserName; + uip.position = -1900000000; + uip.pDialog = new IcqUserInfoDlg(this); + g_plugin.addUserInfo(wParam, &uip); + return 0; +} diff --git a/protocols/ICQ-WIM/src/utils.cpp b/protocols/ICQ-WIM/src/utils.cpp index 4fa65d0302..dfa73b8fb4 100644 --- a/protocols/ICQ-WIM/src/utils.cpp +++ b/protocols/ICQ-WIM/src/utils.cpp @@ -1,396 +1,396 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-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, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- - -#include "stdafx.h" - -void CIcqProto::InitContactCache() -{ - mir_cslock l(m_csCache); - for (auto &it : AccContacts()) { - if (isChatRoom(it)) - continue; - - // that was previously an ICQ contact - ptrW wszUin(GetUIN(it)); - if (wszUin != nullptr) { - delSetting(it, "UIN"); - setWString(it, DB_KEY_ID, wszUin); - } - // that was previously a MRA contact - else { - CMStringW wszEmail(getMStringW(it, "e-mail")); - if (!wszEmail.IsEmpty()) { - delSetting(it, "e-mail"); - setWString(it, DB_KEY_ID, wszEmail); - } - } - - CMStringW wszId = GetUserId(it); - auto *pCache = FindContactByUIN(wszId); - if (pCache == nullptr) { - pCache = new IcqCacheItem(wszId, it); - m_arCache.insert(pCache); - } - pCache->m_iProcessedMsgId = getId(it, DB_KEY_LASTMSGID); - } -} - -IcqCacheItem* CIcqProto::FindContactByUIN(const CMStringW &wszId) -{ - IcqCacheItem tmp(wszId, -1); - - mir_cslock l(m_csCache); - return m_arCache.find(&tmp); -} - -wchar_t* CIcqProto::GetUIN(MCONTACT hContact) -{ - DBVARIANT dbv = {}; - if (!db_get(hContact, m_szModuleName, "UIN", &dbv)) { - switch (dbv.type) { - case DBVT_DWORD: - wchar_t buf[40], *ret; - _itow_s(dbv.dVal, buf, 10); - return mir_wstrdup(buf); - - case DBVT_ASCIIZ: - ret = mir_a2u(dbv.pszVal); - db_free(&dbv); - return ret; - - case DBVT_UTF8: - ret = mir_utf8decodeW(dbv.pszVal); - db_free(&dbv); - return ret; - - case DBVT_WCHAR: - return dbv.pwszVal; - } - db_free(&dbv); - } - return nullptr; -} - -MCONTACT CIcqProto::CreateContact(const CMStringW &wszId, bool bTemporary) -{ - auto *pCache = FindContactByUIN(wszId); - if (pCache != nullptr) - return pCache->m_hContact; - - MCONTACT hContact = db_add_contact(); - setWString(hContact, DB_KEY_ID, wszId); - Proto_AddToContact(hContact, m_szModuleName); - RetrieveUserInfo(hContact); - - if (bTemporary) - Contact::RemoveFromList(hContact); - - return hContact; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::CalcHash(AsyncHttpRequest *pReq) -{ - CMStringA hashData(FORMAT, "%s&%s&%s", - pReq->requestType == REQUEST_POST ? "POST" : "GET", - mir_urlEncode(pReq->m_szUrl).c_str(), mir_urlEncode(pReq->m_szParam).c_str()); - - unsigned int len; - uint8_t hashOut[MIR_SHA256_HASH_SIZE]; - HMAC(EVP_sha256(), m_szSessionKey, m_szSessionKey.GetLength(), (uint8_t*)hashData.c_str(), hashData.GetLength(), hashOut, &len); - pReq << CHAR_PARAM("sig_sha256", ptrA(mir_base64_encode(hashOut, sizeof(hashOut)))); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::Json2int(MCONTACT hContact, const JSONNode &node, const char *szJson, const char *szSetting, bool bIsPartial) -{ - const JSONNode &var = node[szJson]; - if (var) - setDword(hContact, szSetting, var.as_int()); - else if (!bIsPartial) - delSetting(hContact, szSetting); -} - -void CIcqProto::Json2string(MCONTACT hContact, const JSONNode &node, const char *szJson, const char *szSetting, bool bIsPartial) -{ - const JSONNode &var = node[szJson]; - if (var) { - CMStringW wszStr(var.as_mstring()); - if (wszStr == L"[deleted]") { - setByte(hContact, "IcqDeleted", 1); - Contact::PutOnList(hContact); - } - else setWString(hContact, szSetting, wszStr); - } - else if (!bIsPartial) - delSetting(hContact, szSetting); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Avatars - -void CIcqProto::GetAvatarFileName(MCONTACT hContact, wchar_t* pszDest, size_t cbLen) -{ - int tPathLen = mir_snwprintf(pszDest, cbLen, L"%s\\%S", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName); - CreateDirectoryTreeW(pszDest); - pszDest[tPathLen++] = '\\'; - - CMStringW wszFileName(getMStringW(hContact, "IconId")); - const wchar_t* szFileType = ProtoGetAvatarExtension(getByte(hContact, "AvatarType", PA_FORMAT_PNG)); - mir_snwprintf(pszDest + tPathLen, MAX_PATH - tPathLen, L"%s%s", wszFileName.c_str(), szFileType); -} - -INT_PTR __cdecl CIcqProto::GetAvatar(WPARAM wParam, LPARAM lParam) -{ - wchar_t *buf = (wchar_t*)wParam; - int size = (int)lParam; - if (buf == nullptr || size <= 0) - return -1; - - GetAvatarFileName(0, buf, size); - return 0; -} - -INT_PTR __cdecl CIcqProto::GetAvatarCaps(WPARAM wParam, LPARAM lParam) -{ - switch (wParam) { - case AF_MAXSIZE: - ((POINT*)lParam)->x = -1; - ((POINT*)lParam)->y = -1; - return 0; - - case AF_FORMATSUPPORTED: // nobody - return 1; - - case AF_DELAYAFTERFAIL: - return 10 * 60 * 1000; - - case AF_ENABLED: - case AF_FETCHIFPROTONOTVISIBLE: - case AF_FETCHIFCONTACTOFFLINE: - return 1; - } - return 0; -} - -INT_PTR __cdecl CIcqProto::GetAvatarInfo(WPARAM, LPARAM lParam) -{ - PROTO_AVATAR_INFORMATION* pai = (PROTO_AVATAR_INFORMATION*)lParam; - - ptrW szIconId(getWStringA(pai->hContact, "IconId")); - if (szIconId == nullptr) { - debugLogA("No avatar"); - return GAIR_NOAVATAR; - } - - GetAvatarFileName(pai->hContact, pai->filename, _countof(pai->filename)); - pai->format = getByte(pai->hContact, "AvatarType", 0); - - if (::_waccess(pai->filename, 0) == 0) - return GAIR_SUCCESS; - - debugLogA("No avatar"); - return GAIR_NOAVATAR; -} - -INT_PTR __cdecl CIcqProto::SetAvatar(WPARAM, LPARAM lParam) -{ - wchar_t* pwszFileName = (wchar_t*)lParam; - - wchar_t wszOldName[MAX_PATH]; - GetAvatarFileName(0, wszOldName, _countof(wszOldName)); - _wremove(wszOldName); - - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/expressions/upload"); - pReq->m_szUrl.AppendFormat("?f=json&aimsid=%s&r=%s&type=largeBuddyIcon", mir_urlEncode(m_aimsid.c_str()).c_str(), pReq->m_reqId); - - if (pwszFileName == nullptr) - delSetting("AvatarHash"); - else { - int fileId = _wopen(pwszFileName, _O_RDONLY | _O_BINARY, _S_IREAD); - if (fileId < 0) { - delete pReq; - return 1; - } - - unsigned dwSize = (unsigned)_filelengthi64(fileId); - char* pData = (char*)mir_alloc(dwSize); - if (pData == nullptr) { - _close(fileId); - delete pReq; - return 2; - } - - _read(fileId, pData, dwSize); - _close(fileId); - - pReq->pData = pData; - pReq->dataLength = dwSize; - - int iAvatarType = ProtoGetBufferFormat(pData); - if (iAvatarType == PA_FORMAT_UNKNOWN) { - delete pReq; - delete pData; - return 3; - } - - pReq->AddHeader("Content-Type", ProtoGetAvatarMimeType(iAvatarType)); - } - Push(pReq); - - return 0; // TODO -} - -///////////////////////////////////////////////////////////////////////////////////////// - -CMStringW CIcqProto::GetUserId(MCONTACT hContact) -{ - if (isChatRoom(hContact)) - return getMStringW(hContact, "ChatRoomID"); - - return getMStringW(hContact, DB_KEY_ID); -} - -bool IsChat(const CMStringW &aimid) -{ - return aimid.Right(11) == "@chat.agent"; -} - -bool IsValidType(const JSONNode &n) -{ - auto type = n["userType"].as_string(); - return type == "icq" || type == "aim" || type == "interop" || type == ""; -} - -int CIcqProto::StatusFromPresence(const JSONNode &presence, MCONTACT hContact) -{ - CMStringW wszStatus = presence["state"].as_mstring(); - int iStatus; - if (wszStatus == L"online") - iStatus = ID_STATUS_ONLINE; - else if (wszStatus == L"offline") - iStatus = ID_STATUS_OFFLINE; - else if (wszStatus == L"n/a") - iStatus = ID_STATUS_NA; - else if (wszStatus == L"away") - iStatus = ID_STATUS_AWAY; - else if (wszStatus == L"occupied") - iStatus = ID_STATUS_OCCUPIED; - else if (wszStatus == L"dnd") - iStatus = ID_STATUS_DND; - else - iStatus = -1; - - int iLastSeen = presence["lastseen"].as_int(); - if (iLastSeen != 0) - setDword(hContact, DB_KEY_LASTSEEN, iLastSeen); - else if (getDword(hContact, DB_KEY_ONLINETS)) - iStatus = ID_STATUS_ONLINE; - - return iStatus; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -__int64 CIcqProto::getId(MCONTACT hContact, const char *szSetting) -{ - DBVARIANT dbv; - dbv.type = DBVT_BLOB; - if (db_get(hContact, m_szModuleName, szSetting, &dbv)) - return 0; - - __int64 result = (dbv.cpbVal == sizeof(__int64)) ? *(__int64*)dbv.pbVal : 0; - db_free(&dbv); - return result; -} - -void CIcqProto::setId(MCONTACT hContact, const char *szSetting, __int64 iValue) -{ - __int64 oldVal = getId(hContact, szSetting); - if (oldVal != iValue) - db_set_blob(hContact, m_szModuleName, szSetting, &iValue, sizeof(iValue)); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -wchar_t* time2text(time_t ts) -{ - if (ts == 0) - return L""; - - static wchar_t buf[100]; - TimeZone_PrintTimeStamp(NULL, ts, L"D t", buf, _countof(buf), 0); - return buf; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static LRESULT CALLBACK PopupDlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - switch (message) { - case WM_CONTEXTMENU: - PUDeletePopup(hWnd); - break; - - case WM_COMMAND: - CIcqProto *ppro = (CIcqProto*)PUGetPluginData(hWnd); - CallProtoService(ppro->m_szModuleName, PS_GOTO_INBOX); - PUDeletePopup(hWnd); - break; - } - - return DefWindowProc(hWnd, message, wParam, lParam); -} - -void CIcqProto::EmailNotification(const wchar_t *pwszText) -{ - POPUPDATAW Popup = {}; - Popup.lchIcon = g_plugin.getIcon(IDI_INBOX); - wcsncpy_s(Popup.lpwzText, pwszText, _TRUNCATE); - wcsncpy_s(Popup.lpwzContactName, m_tszUserName, _TRUNCATE); - Popup.iSeconds = 20; - Popup.PluginData = this; - Popup.PluginWindowProc = PopupDlgProc; - PUAddPopupW(&Popup); - - if (m_bUseTrayIcon) { - char szServiceFunction[MAX_PATH]; - if (m_bLaunchMailbox) - mir_snprintf(szServiceFunction, "%s%s", m_szModuleName, PS_GOTO_INBOX); - else - mir_snprintf(szServiceFunction, "%s%s", m_szModuleName, PS_DUMMY); - - int i = 0; - while (CLISTEVENT *pcle = g_clistApi.pfnGetEvent(-1, i++)) - if (!mir_strcmp(pcle->pszService, szServiceFunction)) - return; - - CLISTEVENT cle = {}; - cle.hDbEvent = ICQ_FAKE_EVENT_ID; - cle.moduleName = m_szModuleName; - cle.hIcon = g_plugin.getIcon(IDI_INBOX); - cle.flags = CLEF_UNICODE | CLEF_PROTOCOLGLOBAL; - cle.pszService = szServiceFunction; - cle.szTooltip.w = pwszText; - g_clistApi.pfnAddEvent(&cle); - } -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 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" + +void CIcqProto::InitContactCache() +{ + mir_cslock l(m_csCache); + for (auto &it : AccContacts()) { + if (isChatRoom(it)) + continue; + + // that was previously an ICQ contact + ptrW wszUin(GetUIN(it)); + if (wszUin != nullptr) { + delSetting(it, "UIN"); + setWString(it, DB_KEY_ID, wszUin); + } + // that was previously a MRA contact + else { + CMStringW wszEmail(getMStringW(it, "e-mail")); + if (!wszEmail.IsEmpty()) { + delSetting(it, "e-mail"); + setWString(it, DB_KEY_ID, wszEmail); + } + } + + CMStringW wszId = GetUserId(it); + auto *pCache = FindContactByUIN(wszId); + if (pCache == nullptr) { + pCache = new IcqCacheItem(wszId, it); + m_arCache.insert(pCache); + } + pCache->m_iProcessedMsgId = getId(it, DB_KEY_LASTMSGID); + } +} + +IcqCacheItem* CIcqProto::FindContactByUIN(const CMStringW &wszId) +{ + IcqCacheItem tmp(wszId, -1); + + mir_cslock l(m_csCache); + return m_arCache.find(&tmp); +} + +wchar_t* CIcqProto::GetUIN(MCONTACT hContact) +{ + DBVARIANT dbv = {}; + if (!db_get(hContact, m_szModuleName, "UIN", &dbv)) { + switch (dbv.type) { + case DBVT_DWORD: + wchar_t buf[40], *ret; + _itow_s(dbv.dVal, buf, 10); + return mir_wstrdup(buf); + + case DBVT_ASCIIZ: + ret = mir_a2u(dbv.pszVal); + db_free(&dbv); + return ret; + + case DBVT_UTF8: + ret = mir_utf8decodeW(dbv.pszVal); + db_free(&dbv); + return ret; + + case DBVT_WCHAR: + return dbv.pwszVal; + } + db_free(&dbv); + } + return nullptr; +} + +MCONTACT CIcqProto::CreateContact(const CMStringW &wszId, bool bTemporary) +{ + auto *pCache = FindContactByUIN(wszId); + if (pCache != nullptr) + return pCache->m_hContact; + + MCONTACT hContact = db_add_contact(); + setWString(hContact, DB_KEY_ID, wszId); + Proto_AddToContact(hContact, m_szModuleName); + RetrieveUserInfo(hContact); + + if (bTemporary) + Contact::RemoveFromList(hContact); + + return hContact; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::CalcHash(AsyncHttpRequest *pReq) +{ + CMStringA hashData(FORMAT, "%s&%s&%s", + pReq->requestType == REQUEST_POST ? "POST" : "GET", + mir_urlEncode(pReq->m_szUrl).c_str(), mir_urlEncode(pReq->m_szParam).c_str()); + + unsigned int len; + uint8_t hashOut[MIR_SHA256_HASH_SIZE]; + HMAC(EVP_sha256(), m_szSessionKey, m_szSessionKey.GetLength(), (uint8_t*)hashData.c_str(), hashData.GetLength(), hashOut, &len); + pReq << CHAR_PARAM("sig_sha256", ptrA(mir_base64_encode(hashOut, sizeof(hashOut)))); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::Json2int(MCONTACT hContact, const JSONNode &node, const char *szJson, const char *szSetting, bool bIsPartial) +{ + const JSONNode &var = node[szJson]; + if (var) + setDword(hContact, szSetting, var.as_int()); + else if (!bIsPartial) + delSetting(hContact, szSetting); +} + +void CIcqProto::Json2string(MCONTACT hContact, const JSONNode &node, const char *szJson, const char *szSetting, bool bIsPartial) +{ + const JSONNode &var = node[szJson]; + if (var) { + CMStringW wszStr(var.as_mstring()); + if (wszStr == L"[deleted]") { + setByte(hContact, "IcqDeleted", 1); + Contact::PutOnList(hContact); + } + else setWString(hContact, szSetting, wszStr); + } + else if (!bIsPartial) + delSetting(hContact, szSetting); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Avatars + +void CIcqProto::GetAvatarFileName(MCONTACT hContact, wchar_t* pszDest, size_t cbLen) +{ + int tPathLen = mir_snwprintf(pszDest, cbLen, L"%s\\%S", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName); + CreateDirectoryTreeW(pszDest); + pszDest[tPathLen++] = '\\'; + + CMStringW wszFileName(getMStringW(hContact, "IconId")); + const wchar_t* szFileType = ProtoGetAvatarExtension(getByte(hContact, "AvatarType", PA_FORMAT_PNG)); + mir_snwprintf(pszDest + tPathLen, MAX_PATH - tPathLen, L"%s%s", wszFileName.c_str(), szFileType); +} + +INT_PTR __cdecl CIcqProto::GetAvatar(WPARAM wParam, LPARAM lParam) +{ + wchar_t *buf = (wchar_t*)wParam; + int size = (int)lParam; + if (buf == nullptr || size <= 0) + return -1; + + GetAvatarFileName(0, buf, size); + return 0; +} + +INT_PTR __cdecl CIcqProto::GetAvatarCaps(WPARAM wParam, LPARAM lParam) +{ + switch (wParam) { + case AF_MAXSIZE: + ((POINT*)lParam)->x = -1; + ((POINT*)lParam)->y = -1; + return 0; + + case AF_FORMATSUPPORTED: // nobody + return 1; + + case AF_DELAYAFTERFAIL: + return 10 * 60 * 1000; + + case AF_ENABLED: + case AF_FETCHIFPROTONOTVISIBLE: + case AF_FETCHIFCONTACTOFFLINE: + return 1; + } + return 0; +} + +INT_PTR __cdecl CIcqProto::GetAvatarInfo(WPARAM, LPARAM lParam) +{ + PROTO_AVATAR_INFORMATION* pai = (PROTO_AVATAR_INFORMATION*)lParam; + + ptrW szIconId(getWStringA(pai->hContact, "IconId")); + if (szIconId == nullptr) { + debugLogA("No avatar"); + return GAIR_NOAVATAR; + } + + GetAvatarFileName(pai->hContact, pai->filename, _countof(pai->filename)); + pai->format = getByte(pai->hContact, "AvatarType", 0); + + if (::_waccess(pai->filename, 0) == 0) + return GAIR_SUCCESS; + + debugLogA("No avatar"); + return GAIR_NOAVATAR; +} + +INT_PTR __cdecl CIcqProto::SetAvatar(WPARAM, LPARAM lParam) +{ + wchar_t* pwszFileName = (wchar_t*)lParam; + + wchar_t wszOldName[MAX_PATH]; + GetAvatarFileName(0, wszOldName, _countof(wszOldName)); + _wremove(wszOldName); + + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/expressions/upload"); + pReq->m_szUrl.AppendFormat("?f=json&aimsid=%s&r=%s&type=largeBuddyIcon", mir_urlEncode(m_aimsid.c_str()).c_str(), pReq->m_reqId); + + if (pwszFileName == nullptr) + delSetting("AvatarHash"); + else { + int fileId = _wopen(pwszFileName, _O_RDONLY | _O_BINARY, _S_IREAD); + if (fileId < 0) { + delete pReq; + return 1; + } + + unsigned dwSize = (unsigned)_filelengthi64(fileId); + char* pData = (char*)mir_alloc(dwSize); + if (pData == nullptr) { + _close(fileId); + delete pReq; + return 2; + } + + _read(fileId, pData, dwSize); + _close(fileId); + + pReq->pData = pData; + pReq->dataLength = dwSize; + + int iAvatarType = ProtoGetBufferFormat(pData); + if (iAvatarType == PA_FORMAT_UNKNOWN) { + delete pReq; + delete pData; + return 3; + } + + pReq->AddHeader("Content-Type", ProtoGetAvatarMimeType(iAvatarType)); + } + Push(pReq); + + return 0; // TODO +} + +///////////////////////////////////////////////////////////////////////////////////////// + +CMStringW CIcqProto::GetUserId(MCONTACT hContact) +{ + if (isChatRoom(hContact)) + return getMStringW(hContact, "ChatRoomID"); + + return getMStringW(hContact, DB_KEY_ID); +} + +bool IsChat(const CMStringW &aimid) +{ + return aimid.Right(11) == "@chat.agent"; +} + +bool IsValidType(const JSONNode &n) +{ + auto type = n["userType"].as_string(); + return type == "icq" || type == "aim" || type == "interop" || type == ""; +} + +int CIcqProto::StatusFromPresence(const JSONNode &presence, MCONTACT hContact) +{ + CMStringW wszStatus = presence["state"].as_mstring(); + int iStatus; + if (wszStatus == L"online") + iStatus = ID_STATUS_ONLINE; + else if (wszStatus == L"offline") + iStatus = ID_STATUS_OFFLINE; + else if (wszStatus == L"n/a") + iStatus = ID_STATUS_NA; + else if (wszStatus == L"away") + iStatus = ID_STATUS_AWAY; + else if (wszStatus == L"occupied") + iStatus = ID_STATUS_OCCUPIED; + else if (wszStatus == L"dnd") + iStatus = ID_STATUS_DND; + else + iStatus = -1; + + int iLastSeen = presence["lastseen"].as_int(); + if (iLastSeen != 0) + setDword(hContact, DB_KEY_LASTSEEN, iLastSeen); + else if (getDword(hContact, DB_KEY_ONLINETS)) + iStatus = ID_STATUS_ONLINE; + + return iStatus; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +__int64 CIcqProto::getId(MCONTACT hContact, const char *szSetting) +{ + DBVARIANT dbv; + dbv.type = DBVT_BLOB; + if (db_get(hContact, m_szModuleName, szSetting, &dbv)) + return 0; + + __int64 result = (dbv.cpbVal == sizeof(__int64)) ? *(__int64*)dbv.pbVal : 0; + db_free(&dbv); + return result; +} + +void CIcqProto::setId(MCONTACT hContact, const char *szSetting, __int64 iValue) +{ + __int64 oldVal = getId(hContact, szSetting); + if (oldVal != iValue) + db_set_blob(hContact, m_szModuleName, szSetting, &iValue, sizeof(iValue)); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +wchar_t* time2text(time_t ts) +{ + if (ts == 0) + return L""; + + static wchar_t buf[100]; + TimeZone_PrintTimeStamp(NULL, ts, L"D t", buf, _countof(buf), 0); + return buf; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static LRESULT CALLBACK PopupDlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) { + case WM_CONTEXTMENU: + PUDeletePopup(hWnd); + break; + + case WM_COMMAND: + CIcqProto *ppro = (CIcqProto*)PUGetPluginData(hWnd); + CallProtoService(ppro->m_szModuleName, PS_GOTO_INBOX); + PUDeletePopup(hWnd); + break; + } + + return DefWindowProc(hWnd, message, wParam, lParam); +} + +void CIcqProto::EmailNotification(const wchar_t *pwszText) +{ + POPUPDATAW Popup = {}; + Popup.lchIcon = g_plugin.getIcon(IDI_INBOX); + wcsncpy_s(Popup.lpwzText, pwszText, _TRUNCATE); + wcsncpy_s(Popup.lpwzContactName, m_tszUserName, _TRUNCATE); + Popup.iSeconds = 20; + Popup.PluginData = this; + Popup.PluginWindowProc = PopupDlgProc; + PUAddPopupW(&Popup); + + if (m_bUseTrayIcon) { + char szServiceFunction[MAX_PATH]; + if (m_bLaunchMailbox) + mir_snprintf(szServiceFunction, "%s%s", m_szModuleName, PS_GOTO_INBOX); + else + mir_snprintf(szServiceFunction, "%s%s", m_szModuleName, PS_DUMMY); + + int i = 0; + while (CLISTEVENT *pcle = g_clistApi.pfnGetEvent(-1, i++)) + if (!mir_strcmp(pcle->pszService, szServiceFunction)) + return; + + CLISTEVENT cle = {}; + cle.hDbEvent = ICQ_FAKE_EVENT_ID; + cle.moduleName = m_szModuleName; + cle.hIcon = g_plugin.getIcon(IDI_INBOX); + cle.flags = CLEF_UNICODE | CLEF_PROTOCOLGLOBAL; + cle.pszService = szServiceFunction; + cle.szTooltip.w = pwszText; + g_clistApi.pfnAddEvent(&cle); + } +} diff --git a/protocols/ICQ-WIM/src/version.h b/protocols/ICQ-WIM/src/version.h index c1cb84630e..d5270a8de9 100644 --- a/protocols/ICQ-WIM/src/version.h +++ b/protocols/ICQ-WIM/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "ICQ protocol support for Miranda NG." #define __AUTHOR "George Hazan" #define __AUTHORWEB "https://miranda-ng.org/p/ICQ" -#define __COPYRIGHT "© 2018-22 George Hazan" +#define __COPYRIGHT "© 2018-23 George Hazan" -- cgit v1.2.3