summaryrefslogtreecommitdiff
path: root/protocols/ICQ-WIM/src
diff options
context:
space:
mode:
authordartraiden <wowemuh@gmail.com>2023-01-02 21:10:29 +0300
committerdartraiden <wowemuh@gmail.com>2023-01-02 21:10:29 +0300
commit1979fd80424d16b2e489f9b57d01d9c7811d25a2 (patch)
tree960d42c5fe4a51f0fe2850bea91256e226bce221 /protocols/ICQ-WIM/src
parentadfbbb217d4f4a05acf198755f219a5223d31c27 (diff)
Update copyrights
Diffstat (limited to 'protocols/ICQ-WIM/src')
-rw-r--r--protocols/ICQ-WIM/src/groupchats.cpp584
-rw-r--r--protocols/ICQ-WIM/src/http.cpp770
-rw-r--r--protocols/ICQ-WIM/src/ignore.cpp164
-rw-r--r--protocols/ICQ-WIM/src/main.cpp204
-rw-r--r--protocols/ICQ-WIM/src/mra.cpp282
-rw-r--r--protocols/ICQ-WIM/src/options.cpp788
-rw-r--r--protocols/ICQ-WIM/src/poll.cpp818
-rw-r--r--protocols/ICQ-WIM/src/proto.cpp1356
-rw-r--r--protocols/ICQ-WIM/src/proto.h988
-rw-r--r--protocols/ICQ-WIM/src/server.cpp2440
-rw-r--r--protocols/ICQ-WIM/src/stdafx.cxx36
-rw-r--r--protocols/ICQ-WIM/src/stdafx.h218
-rw-r--r--protocols/ICQ-WIM/src/userinfo.cpp128
-rw-r--r--protocols/ICQ-WIM/src/utils.cpp792
-rw-r--r--protocols/ICQ-WIM/src/version.h2
15 files changed, 4785 insertions, 4785 deletions
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 &param)
-{
- 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 &param)
-{
- 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 &param)
+{
+ 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 &param)
+{
+ 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<CIcqProto>
-{
- CMPluginMra() : ACCPROTOPLUGIN<CIcqProto>("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<CIcqProto>(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<CIcqProto>
+{
+ CMPluginMra() : ACCPROTOPLUGIN<CIcqProto>("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<CIcqProto>(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 <http://www.gnu.org/licenses/>.
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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<IcqGroup> 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<IcqGroup> 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<CIcqProto>(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<CIcqProto>(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<CIcqProto> 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<CIcqProto>
-{
- 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<IcqCacheItem> 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<IcqOwnMessage> m_arOwnIds;
-
- OBJLIST<IcqGroup> 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<AsyncHttpRequest> 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<IcqCacheItem> 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<wchar_t*> m_szOwnId; // our own aim id
- CMOption<uint8_t> m_bHideGroupchats; // don't pop up group chat windows on startup
- CMOption<uint8_t> m_bUseTrayIcon; // use tray icon notifications
- CMOption<uint8_t> m_bErrorPopups; // display popups with errors
- CMOption<uint8_t> m_bLaunchMailbox; // launch browser to view email
- CMOption<uint32_t> m_iTimeDiff1; // set this status to m_iStatus1 after this interval of secs
- CMOption<uint32_t> m_iStatus1;
- CMOption<uint32_t> m_iTimeDiff2; // set this status to m_iStatus2 after this interval of secs
- CMOption<uint32_t> 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<CIcqProto>
-{
- 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<CIcqProto> 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<CIcqProto>
+{
+ 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<IcqCacheItem> 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<IcqOwnMessage> m_arOwnIds;
+
+ OBJLIST<IcqGroup> 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<AsyncHttpRequest> 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<IcqCacheItem> 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<wchar_t*> m_szOwnId; // our own aim id
+ CMOption<uint8_t> m_bHideGroupchats; // don't pop up group chat windows on startup
+ CMOption<uint8_t> m_bUseTrayIcon; // use tray icon notifications
+ CMOption<uint8_t> m_bErrorPopups; // display popups with errors
+ CMOption<uint8_t> m_bLaunchMailbox; // launch browser to view email
+ CMOption<uint32_t> m_iTimeDiff1; // set this status to m_iStatus1 after this interval of secs
+ CMOption<uint32_t> m_iStatus1;
+ CMOption<uint32_t> m_iTimeDiff2; // set this status to m_iStatus2 after this interval of secs
+ CMOption<uint32_t> 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<CIcqProto>
+{
+ 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 <http://www.gnu.org/licenses/>.
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <windows.h>
-
-// Standard includes
-#include <stdio.h>
-#include <time.h>
-#include <io.h>
-#include <malloc.h>
-#include <direct.h>
-#include <fcntl.h>
-#include <process.h>
-
-// Miranda IM SDK includes
-#include <newpluginapi.h> // This must be included first
-#include <m_avatars.h>
-#include <m_chat_int.h>
-#include <m_clistint.h>
-#include <m_contacts.h>
-#include <m_database.h>
-#include <m_gui.h>
-#include <m_idle.h>
-#include <m_icolib.h>
-#include <m_ignore.h>
-#include <m_json.h>
-#include <m_langpack.h>
-#include <m_message.h>
-#include <m_messagestate.h>
-#include <m_netlib.h>
-#include <m_protocols.h>
-#include <m_protosvc.h>
-#include <m_options.h>
-#include <m_popup.h>
-#include <m_skin.h>
-#include <m_smileyadd.h>
-#include <m_system.h>
-#include <m_timezones.h>
-#include <m_userinfo.h>
-#include <m_utils.h>
-
-#include <openssl/evp.h>
-#include <openssl/hmac.h>
-#include <openssl/rand.h>
-#include <openssl/sha.h>
-
-// 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 <windows.h>
+
+// Standard includes
+#include <stdio.h>
+#include <time.h>
+#include <io.h>
+#include <malloc.h>
+#include <direct.h>
+#include <fcntl.h>
+#include <process.h>
+
+// Miranda IM SDK includes
+#include <newpluginapi.h> // This must be included first
+#include <m_avatars.h>
+#include <m_chat_int.h>
+#include <m_clistint.h>
+#include <m_contacts.h>
+#include <m_database.h>
+#include <m_gui.h>
+#include <m_idle.h>
+#include <m_icolib.h>
+#include <m_ignore.h>
+#include <m_json.h>
+#include <m_langpack.h>
+#include <m_message.h>
+#include <m_messagestate.h>
+#include <m_netlib.h>
+#include <m_protocols.h>
+#include <m_protosvc.h>
+#include <m_options.h>
+#include <m_popup.h>
+#include <m_skin.h>
+#include <m_smileyadd.h>
+#include <m_system.h>
+#include <m_timezones.h>
+#include <m_userinfo.h>
+#include <m_utils.h>
+
+#include <openssl/evp.h>
+#include <openssl/hmac.h>
+#include <openssl/rand.h>
+#include <openssl/sha.h>
+
+// 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"