summaryrefslogtreecommitdiff
path: root/protocols/ICQ-WIM
diff options
context:
space:
mode:
authorGeorge Hazan <ghazan@miranda.im>2019-01-16 18:06:52 +0300
committerGeorge Hazan <ghazan@miranda.im>2019-01-16 18:06:52 +0300
commitaa514a9f95c8cd95c01952250f7cdb670a66af9a (patch)
treeecb84e10fc3af008e1a8ac9027f1b526634c8d65 /protocols/ICQ-WIM
parent4f0a4ae4724fdfe65f63ee41dc1790ccbb0668f7 (diff)
fixes #1761 (Rename Icq10 folder and ICQ/2018 in version.h to ICQ-WIM)
Diffstat (limited to 'protocols/ICQ-WIM')
-rw-r--r--protocols/ICQ-WIM/CMakeLists.txt5
-rw-r--r--protocols/ICQ-WIM/ICQ-WIM.vcxproj28
-rw-r--r--protocols/ICQ-WIM/ICQ-WIM.vcxproj.filters4
-rw-r--r--protocols/ICQ-WIM/proto_icq/CMakeLists.txt2
-rw-r--r--protocols/ICQ-WIM/proto_icq/Proto_ICQ.vcxproj28
-rw-r--r--protocols/ICQ-WIM/proto_icq/Proto_ICQ.vcxproj.filters4
-rw-r--r--protocols/ICQ-WIM/proto_icq/res/Away.icobin0 -> 5430 bytes
-rw-r--r--protocols/ICQ-WIM/proto_icq/res/DND.icobin0 -> 5430 bytes
-rw-r--r--protocols/ICQ-WIM/proto_icq/res/FFC.icobin0 -> 5430 bytes
-rw-r--r--protocols/ICQ-WIM/proto_icq/res/Invisible.icobin0 -> 5430 bytes
-rw-r--r--protocols/ICQ-WIM/proto_icq/res/NA.icobin0 -> 5430 bytes
-rw-r--r--protocols/ICQ-WIM/proto_icq/res/Occupied.icobin0 -> 5430 bytes
-rw-r--r--protocols/ICQ-WIM/proto_icq/res/Offline.icobin0 -> 5430 bytes
-rw-r--r--protocols/ICQ-WIM/proto_icq/res/Online.icobin0 -> 5430 bytes
-rw-r--r--protocols/ICQ-WIM/proto_icq/res/Phone.icobin0 -> 5430 bytes
-rw-r--r--protocols/ICQ-WIM/proto_icq/res/Proto_ICQ.rc77
-rw-r--r--protocols/ICQ-WIM/proto_icq/src/resource.h24
-rw-r--r--protocols/ICQ-WIM/res/resources.rc208
-rw-r--r--protocols/ICQ-WIM/res/version.rc9
-rw-r--r--protocols/ICQ-WIM/src/groupchats.cpp303
-rw-r--r--protocols/ICQ-WIM/src/http.cpp289
-rw-r--r--protocols/ICQ-WIM/src/http.h51
-rw-r--r--protocols/ICQ-WIM/src/main.cpp77
-rw-r--r--protocols/ICQ-WIM/src/options.cpp226
-rw-r--r--protocols/ICQ-WIM/src/proto.cpp479
-rw-r--r--protocols/ICQ-WIM/src/proto.h263
-rw-r--r--protocols/ICQ-WIM/src/resource.h29
-rw-r--r--protocols/ICQ-WIM/src/server.cpp859
-rw-r--r--protocols/ICQ-WIM/src/stdafx.cxx18
-rw-r--r--protocols/ICQ-WIM/src/stdafx.h95
-rw-r--r--protocols/ICQ-WIM/src/utils.cpp239
-rw-r--r--protocols/ICQ-WIM/src/version.h13
32 files changed, 3330 insertions, 0 deletions
diff --git a/protocols/ICQ-WIM/CMakeLists.txt b/protocols/ICQ-WIM/CMakeLists.txt
new file mode 100644
index 0000000000..bb361358c2
--- /dev/null
+++ b/protocols/ICQ-WIM/CMakeLists.txt
@@ -0,0 +1,5 @@
+file(GLOB SOURCES "src/*.h" "src/*.cpp" "res/*.rc")
+set(TARGET ICQ)
+include(${CMAKE_SOURCE_DIR}/cmake/plugin.cmake)
+target_link_libraries(${TARGET} comctl32.lib ws2_32.lib)
+add_subdirectory(proto_icq)
diff --git a/protocols/ICQ-WIM/ICQ-WIM.vcxproj b/protocols/ICQ-WIM/ICQ-WIM.vcxproj
new file mode 100644
index 0000000000..98fb8261e0
--- /dev/null
+++ b/protocols/ICQ-WIM/ICQ-WIM.vcxproj
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ItemGroup Label="ProjectConfigurations">
+ <ProjectConfiguration Include="Debug|Win32">
+ <Configuration>Debug</Configuration>
+ <Platform>Win32</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Debug|x64">
+ <Configuration>Debug</Configuration>
+ <Platform>x64</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Release|Win32">
+ <Configuration>Release</Configuration>
+ <Platform>Win32</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Release|x64">
+ <Configuration>Release</Configuration>
+ <Platform>x64</Platform>
+ </ProjectConfiguration>
+ </ItemGroup>
+ <PropertyGroup Label="Globals">
+ <ProjectName>ICQ</ProjectName>
+ <ProjectGuid>{EFB2355B-82B3-4759-B7D8-95F8E9506291}</ProjectGuid>
+ </PropertyGroup>
+ <ImportGroup Label="PropertySheets">
+ <Import Project="$(ProjectDir)..\..\build\vc.common\plugin.props" />
+ </ImportGroup>
+</Project> \ No newline at end of file
diff --git a/protocols/ICQ-WIM/ICQ-WIM.vcxproj.filters b/protocols/ICQ-WIM/ICQ-WIM.vcxproj.filters
new file mode 100644
index 0000000000..fcae13a9d8
--- /dev/null
+++ b/protocols/ICQ-WIM/ICQ-WIM.vcxproj.filters
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(ProjectDir)..\..\build\vc.common\common.filters" />
+</Project> \ No newline at end of file
diff --git a/protocols/ICQ-WIM/proto_icq/CMakeLists.txt b/protocols/ICQ-WIM/proto_icq/CMakeLists.txt
new file mode 100644
index 0000000000..596ac21624
--- /dev/null
+++ b/protocols/ICQ-WIM/proto_icq/CMakeLists.txt
@@ -0,0 +1,2 @@
+set(TARGET Proto_ICQ)
+include(${CMAKE_SOURCE_DIR}/cmake/icons.cmake) \ No newline at end of file
diff --git a/protocols/ICQ-WIM/proto_icq/Proto_ICQ.vcxproj b/protocols/ICQ-WIM/proto_icq/Proto_ICQ.vcxproj
new file mode 100644
index 0000000000..21763bbc0b
--- /dev/null
+++ b/protocols/ICQ-WIM/proto_icq/Proto_ICQ.vcxproj
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ItemGroup Label="ProjectConfigurations">
+ <ProjectConfiguration Include="Debug|Win32">
+ <Configuration>Debug</Configuration>
+ <Platform>Win32</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Debug|x64">
+ <Configuration>Debug</Configuration>
+ <Platform>x64</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Release|Win32">
+ <Configuration>Release</Configuration>
+ <Platform>Win32</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Release|x64">
+ <Configuration>Release</Configuration>
+ <Platform>x64</Platform>
+ </ProjectConfiguration>
+ </ItemGroup>
+ <PropertyGroup Label="Globals">
+ <ProjectName>Proto_ICQ</ProjectName>
+ <ProjectGuid>{DB3B0449-E576-4BBB-8B08-AB9E914D39CA}</ProjectGuid>
+ </PropertyGroup>
+ <ImportGroup Label="PropertySheets">
+ <Import Project="$(ProjectDir)..\..\..\build\vc.common\icons.props" />
+ </ImportGroup>
+</Project> \ No newline at end of file
diff --git a/protocols/ICQ-WIM/proto_icq/Proto_ICQ.vcxproj.filters b/protocols/ICQ-WIM/proto_icq/Proto_ICQ.vcxproj.filters
new file mode 100644
index 0000000000..28f81e7f1b
--- /dev/null
+++ b/protocols/ICQ-WIM/proto_icq/Proto_ICQ.vcxproj.filters
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(ProjectDir)..\..\..\build\vc.common\common.filters" />
+</Project> \ No newline at end of file
diff --git a/protocols/ICQ-WIM/proto_icq/res/Away.ico b/protocols/ICQ-WIM/proto_icq/res/Away.ico
new file mode 100644
index 0000000000..248a3e9916
--- /dev/null
+++ b/protocols/ICQ-WIM/proto_icq/res/Away.ico
Binary files differ
diff --git a/protocols/ICQ-WIM/proto_icq/res/DND.ico b/protocols/ICQ-WIM/proto_icq/res/DND.ico
new file mode 100644
index 0000000000..4833160eac
--- /dev/null
+++ b/protocols/ICQ-WIM/proto_icq/res/DND.ico
Binary files differ
diff --git a/protocols/ICQ-WIM/proto_icq/res/FFC.ico b/protocols/ICQ-WIM/proto_icq/res/FFC.ico
new file mode 100644
index 0000000000..e7ec4d3ae2
--- /dev/null
+++ b/protocols/ICQ-WIM/proto_icq/res/FFC.ico
Binary files differ
diff --git a/protocols/ICQ-WIM/proto_icq/res/Invisible.ico b/protocols/ICQ-WIM/proto_icq/res/Invisible.ico
new file mode 100644
index 0000000000..6a337c2926
--- /dev/null
+++ b/protocols/ICQ-WIM/proto_icq/res/Invisible.ico
Binary files differ
diff --git a/protocols/ICQ-WIM/proto_icq/res/NA.ico b/protocols/ICQ-WIM/proto_icq/res/NA.ico
new file mode 100644
index 0000000000..ec0621dc9f
--- /dev/null
+++ b/protocols/ICQ-WIM/proto_icq/res/NA.ico
Binary files differ
diff --git a/protocols/ICQ-WIM/proto_icq/res/Occupied.ico b/protocols/ICQ-WIM/proto_icq/res/Occupied.ico
new file mode 100644
index 0000000000..04ea2a5855
--- /dev/null
+++ b/protocols/ICQ-WIM/proto_icq/res/Occupied.ico
Binary files differ
diff --git a/protocols/ICQ-WIM/proto_icq/res/Offline.ico b/protocols/ICQ-WIM/proto_icq/res/Offline.ico
new file mode 100644
index 0000000000..af862168cd
--- /dev/null
+++ b/protocols/ICQ-WIM/proto_icq/res/Offline.ico
Binary files differ
diff --git a/protocols/ICQ-WIM/proto_icq/res/Online.ico b/protocols/ICQ-WIM/proto_icq/res/Online.ico
new file mode 100644
index 0000000000..2e33305a76
--- /dev/null
+++ b/protocols/ICQ-WIM/proto_icq/res/Online.ico
Binary files differ
diff --git a/protocols/ICQ-WIM/proto_icq/res/Phone.ico b/protocols/ICQ-WIM/proto_icq/res/Phone.ico
new file mode 100644
index 0000000000..74c80b66ed
--- /dev/null
+++ b/protocols/ICQ-WIM/proto_icq/res/Phone.ico
Binary files differ
diff --git a/protocols/ICQ-WIM/proto_icq/res/Proto_ICQ.rc b/protocols/ICQ-WIM/proto_icq/res/Proto_ICQ.rc
new file mode 100644
index 0000000000..33d8faf243
--- /dev/null
+++ b/protocols/ICQ-WIM/proto_icq/res/Proto_ICQ.rc
@@ -0,0 +1,77 @@
+// Microsoft Visual C++ generated resource script.
+//
+#include "..\src\resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "afxres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// Russian (Russia) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_RUS)
+LANGUAGE LANG_RUSSIAN, SUBLANG_DEFAULT
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "..\\src\\resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""afxres.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Icon
+//
+
+// Icon with lowest ID value placed first to ensure application icon
+// remains consistent on all systems.
+IDI_ICON1 ICON "Offline.ico"
+IDI_ICON2 ICON "Online.ico"
+IDI_ICON3 ICON "Away.ico"
+IDI_ICON4 ICON "Invisible.ico"
+IDI_ICON5 ICON "NA.ico"
+IDI_ICON6 ICON "DND.ico"
+IDI_ICON7 ICON "Occupied.ico"
+IDI_ICON8 ICON "FFC.ico"
+IDI_ICON9 ICON "Phone.ico"
+#endif // Russian (Russia) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
+
diff --git a/protocols/ICQ-WIM/proto_icq/src/resource.h b/protocols/ICQ-WIM/proto_icq/src/resource.h
new file mode 100644
index 0000000000..c74e04f59e
--- /dev/null
+++ b/protocols/ICQ-WIM/proto_icq/src/resource.h
@@ -0,0 +1,24 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by Proto_ICQ.rc
+//
+#define IDI_ICON1 105
+#define IDI_ICON2 104
+#define IDI_ICON3 128
+#define IDI_ICON4 130
+#define IDI_ICON5 131
+#define IDI_ICON6 158
+#define IDI_ICON7 159
+#define IDI_ICON8 129
+#define IDI_ICON9 1002
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 110
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1001
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/protocols/ICQ-WIM/res/resources.rc b/protocols/ICQ-WIM/res/resources.rc
new file mode 100644
index 0000000000..a792fb924f
--- /dev/null
+++ b/protocols/ICQ-WIM/res/resources.rc
@@ -0,0 +1,208 @@
+// Microsoft Visual C++ generated resource script.
+//
+#include "..\src\resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// Russian (Russia) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_RUS)
+LANGUAGE LANG_RUSSIAN, SUBLANG_DEFAULT
+#pragma code_page(1251)
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Dialog
+//
+
+IDD_REGISTER DIALOGEX 0, 0, 316, 111
+STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
+EXSTYLE WS_EX_APPWINDOW
+CAPTION "Account registration"
+FONT 8, "MS Shell Dlg", 400, 0, 0x1
+BEGIN
+ DEFPUSHBUTTON "OK",IDOK,205,90,50,14
+ PUSHBUTTON "Cancel",IDCANCEL,259,90,50,14
+ LTEXT "Enter full phone number",IDC_STATIC,7,7,302,14
+ EDITTEXT IDC_PHONE,7,18,302,14,ES_AUTOHSCROLL
+ PUSHBUTTON "Send me registration code via SMS",IDC_SENDSMS,49,37,221,14,BS_CENTER | WS_DISABLED
+ LTEXT "Enter registration code",IDC_STATIC,7,58,302,8
+ EDITTEXT IDC_CODE,7,70,302,14,ES_AUTOHSCROLL | WS_DISABLED
+END
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// DESIGNINFO
+//
+
+#ifdef APSTUDIO_INVOKED
+GUIDELINES DESIGNINFO
+BEGIN
+ IDD_REGISTER, DIALOG
+ BEGIN
+ LEFTMARGIN, 7
+ RIGHTMARGIN, 309
+ TOPMARGIN, 7
+ BOTTOMMARGIN, 104
+ END
+END
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// AFX_DIALOG_LAYOUT
+//
+
+IDD_REGISTER AFX_DIALOG_LAYOUT
+BEGIN
+ 0
+END
+
+#endif // Russian (Russia) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+#pragma code_page(1252)
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Dialog
+//
+
+IDD_OPTIONS_ACCMGR DIALOGEX 0, 0, 186, 68
+STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD
+EXSTYLE WS_EX_CONTROLPARENT
+FONT 8, "MS Shell Dlg", 0, 0, 0x1
+BEGIN
+ LTEXT "ICQ number:",IDC_STATIC,0,0,53,12
+ EDITTEXT IDC_UIN,54,0,131,12,ES_AUTOHSCROLL
+ LTEXT "Password:",IDC_STATIC,0,16,53,12
+ EDITTEXT IDC_PASSWORD,54,16,131,12,ES_PASSWORD | ES_AUTOHSCROLL
+END
+
+IDD_OPTIONS_FULL DIALOGEX 0, 0, 310, 98
+STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD
+EXSTYLE WS_EX_CONTROLPARENT
+FONT 8, "MS Shell Dlg", 0, 0, 0x1
+BEGIN
+ GROUPBOX "ICQ",IDC_STATIC,4,0,302,74
+ RTEXT "ICQ number:",IDC_STATIC,12,14,51,8
+ EDITTEXT IDC_UIN,68,12,106,12,ES_AUTOHSCROLL
+ RTEXT "Password:",IDC_STATIC,12,28,51,8
+ EDITTEXT IDC_PASSWORD,68,26,106,12,ES_PASSWORD | ES_AUTOHSCROLL
+ CONTROL "Use friendly names instead of contact nicks",IDC_USEFRIENDLY,
+ "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,43,285,10
+ PUSHBUTTON "Create a new ICQ account",IDC_REGISTER,183,78,122,14
+ CONTROL "Hide group chats on startup",IDC_HIDECHATS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,57,285,10
+END
+
+IDD_GROUPCHAT_INVITE DIALOGEX 0, 0, 215, 263
+STYLE DS_SETFONT | DS_MODALFRAME | DS_SETFOREGROUND | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
+CAPTION "Send group chat invitation"
+FONT 8, "MS Shell Dlg", 0, 0, 0x1
+BEGIN
+ CONTROL "",IDC_CLIST,"CListControl",WS_TABSTOP | 0x1,7,7,201,231,WS_EX_CLIENTEDGE
+ DEFPUSHBUTTON "&Invite",IDOK,104,243,50,14
+ PUSHBUTTON "Cancel",IDCANCEL,158,243,50,14
+END
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// DESIGNINFO
+//
+
+#ifdef APSTUDIO_INVOKED
+GUIDELINES DESIGNINFO
+BEGIN
+ IDD_OPTIONS_ACCMGR, DIALOG
+ BEGIN
+ END
+
+ IDD_OPTIONS_FULL, DIALOG
+ BEGIN
+ BOTTOMMARGIN, 78
+ END
+
+ IDD_GROUPCHAT_INVITE, DIALOG
+ BEGIN
+ END
+END
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// AFX_DIALOG_LAYOUT
+//
+
+IDD_OPTIONS_FULL AFX_DIALOG_LAYOUT
+BEGIN
+ 0
+END
+
+IDD_OPTIONS_ACCMGR AFX_DIALOG_LAYOUT
+BEGIN
+ 0
+END
+
+IDD_GROUPCHAT_INVITE AFX_DIALOG_LAYOUT
+BEGIN
+ 0
+END
+
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "..\\src\\resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winres.h""\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
+
diff --git a/protocols/ICQ-WIM/res/version.rc b/protocols/ICQ-WIM/res/version.rc
new file mode 100644
index 0000000000..5a5ddd63ed
--- /dev/null
+++ b/protocols/ICQ-WIM/res/version.rc
@@ -0,0 +1,9 @@
+// Microsoft Visual C++ generated resource script.
+//
+#ifdef APSTUDIO_INVOKED
+#error this file is not editable by Microsoft Visual C++
+#endif //APSTUDIO_INVOKED
+
+#include "..\src\version.h"
+
+#include "..\..\build\Version.rc"
diff --git a/protocols/ICQ-WIM/src/groupchats.cpp b/protocols/ICQ-WIM/src/groupchats.cpp
new file mode 100644
index 0000000000..5d4253bc15
--- /dev/null
+++ b/protocols/ICQ-WIM/src/groupchats.cpp
@@ -0,0 +1,303 @@
+// -----------------------------------------------------------------------------
+// ICQ plugin for Miranda NG
+// -----------------------------------------------------------------------------
+// Copyright © 2018-19 Miranda NG team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+// -----------------------------------------------------------------------------
+
+#include "stdafx.h"
+
+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, si->ptszID, GC_EVENT_JOIN };
+ gce.dwFlags = GCEF_SILENT;
+ gce.ptszNick = nick;
+ gce.ptszUID = sn;
+ gce.time = ::time(0);
+ gce.bIsMe = _wtoi(sn) == (int)m_dwUin;
+ gce.ptszStatus = 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 CProtoDlgBase<CIcqProto>
+{
+ typedef CProtoDlgBase<CIcqProto> CSuper;
+
+ CCtrlClc m_clc;
+ SESSION_INFO *m_si;
+
+ void FilterList(CCtrlClc*)
+ {
+ for (auto &hContact : Contacts()) {
+ char *proto = GetContactProto(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.SetBkBitmap(0, nullptr);
+ m_clc.SetBkColor(GetSysColor(COLOR_WINDOW));
+ m_clc.SetGreyoutFlags(0);
+ m_clc.SetLeftMargin(4);
+ m_clc.SetIndent(10);
+ m_clc.SetHideEmptyGroups(1);
+ m_clc.SetHideOfflineRoot(1);
+ for (int i = 0; i <= FONTID_MAX; i++)
+ m_clc.SetTextColor(i, GetSysColor(COLOR_WINDOWTEXT));
+ }
+
+public:
+ CGroupchatInviteDlg(CIcqProto *ppro, SESSION_INFO *si) :
+ CSuper(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
+ {
+ CMStringA 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));
+ }
+ }
+ }
+
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/mchat/AddChat");
+ pReq << CHAR_PARAM("f", "json") << WCHAR_PARAM("chat_id", m_si->ptszID) << CHAR_PARAM("aimsid", m_proto->m_aimsid) << CHAR_PARAM("r", pReq->m_reqId) << CHAR_PARAM("members", szMembers);
+ m_proto->Push(pReq);
+ 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)
+{
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/hideChat");
+ pReq << CHAR_PARAM("f", "json") << CHAR_PARAM("aimsid", m_aimsid) << WCHAR_PARAM("buddy", si->ptszID)
+ << CHAR_PARAM("r", pReq->m_reqId) << INT64_PARAM("lastMsgId", getId(si->hContact, DB_KEY_LASTMSGID));
+ Push(pReq);
+
+ 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->pszModule, m_szModuleName))
+ return 0;
+
+ SESSION_INFO *si = g_chatApi.SM_FindSession(gch->ptszID, gch->pszModule);
+ if (si == nullptr)
+ return 0;
+
+ 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 0;
+}
+
+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;
+ DWORD dwUin = _wtoi(gch->ptszUID);
+ auto *pCache = FindContactByUIN(dwUin);
+ if (pCache == nullptr) {
+ hContact = CreateContact(dwUin, true);
+ setWString(hContact, "Nick", gch->ptszNick);
+ db_set_b(hContact, "CList", "Hidden", 1);
+ 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, si->ptszID, (method == "add_members") ? GC_EVENT_JOIN : GC_EVENT_PART };
+
+ int iStart = 0;
+ CMStringW members(it["members"].as_mstring());
+ while (true) {
+ CMStringW member = members.Tokenize(L",", iStart);
+ if (member.IsEmpty())
+ break;
+
+ auto *pCache = FindContactByUIN(_wtoi(member));
+ if (pCache == nullptr)
+ continue;
+
+ gce.ptszNick = Clist_GetContactDisplayName(pCache->m_hContact);
+ gce.ptszUID = member;
+ gce.time = ::time(0);
+ gce.bIsMe = _wtoi(member) == (int)m_dwUin;
+ Chat_Event(&gce);
+ }
+ }
+}
diff --git a/protocols/ICQ-WIM/src/http.cpp b/protocols/ICQ-WIM/src/http.cpp
new file mode 100644
index 0000000000..e0bd34d17a
--- /dev/null
+++ b/protocols/ICQ-WIM/src/http.cpp
@@ -0,0 +1,289 @@
+// -----------------------------------------------------------------------------
+// ICQ plugin for Miranda NG
+// -----------------------------------------------------------------------------
+// Copyright © 2018-19 Miranda NG team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+// -----------------------------------------------------------------------------
+
+#include "stdafx.h"
+
+#pragma comment(lib, "Rpcrt4.lib")
+
+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");
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+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;
+
+ 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 char *paramName, const char *newValue)
+{
+ CMStringA szOldValue(FORMAT, "\"%s\":\"", paramName);
+ int idx = m_szParam.Find(szOldValue);
+ if (idx == -1)
+ return;
+
+ idx += szOldValue.GetLength();
+ int iEnd = m_szParam.Find("\"", idx);
+ if (iEnd == -1)
+ return;
+
+ szOldValue += m_szParam.Mid(idx, iEnd + 1 - idx);
+
+ CMStringA szNewValue(FORMAT, "\"%s\":\"%s\"", paramName, newValue);
+ m_szParam.Replace(szOldValue, szNewValue);
+
+ replaceStr(pData, nullptr);
+ dataLength = 0;
+}
+
+void 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);
+ }
+ }
+
+ if (pReq->m_conn == CONN_RAPI) {
+ CMStringA szAgent(FORMAT, "%d Mail.ru Windows ICQ (version 10.0.1999)", DWORD(m_dwUin));
+ pReq->AddHeader("User-Agent", szAgent);
+
+ if (m_szRToken.IsEmpty()) {
+ if (!RefreshRobustToken()) {
+ delete pReq;
+ return;
+ }
+
+ pReq->ReplaceJsonParam("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);
+ }
+
+ NETLIBHTTPREQUEST *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;
+ for (int i = 0; i < reply->headersCount; i++) {
+ if (!mir_strcmp(reply->headers[i].szName, "Keep-Alive")) {
+ int timeout;
+ if (1 == sscanf(reply->headers[i].szValue, "timeout=%d", &timeout))
+ conn.timeout = timeout;
+ break;
+ }
+ }
+ }
+
+ 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->ReplaceJsonParam("authToken", m_szRToken);
+ Push(pReq);
+ }
+ else delete pReq;
+ return;
+ }
+ }
+
+ if (pReq->m_pFunc != nullptr)
+ (this->*(pReq->m_pFunc))(reply, pReq);
+
+ Netlib_FreeHttpRequest(reply);
+ }
+ else {
+ debugLogA("Request %s failed", pReq->m_reqId);
+
+ if (pReq->m_conn != CONN_NONE) {
+ if (IsStatusConnecting(m_iStatus))
+ ConnectionFailed(LOGINERR_NONETWORK);
+ m_ConnPool[pReq->m_conn].s = nullptr;
+ }
+ }
+
+ delete pReq;
+}
+
+void CIcqProto::Push(MHttpRequest *p)
+{
+ AsyncHttpRequest *pReq = (AsyncHttpRequest*)p;
+
+ pReq->timeout = 10000;
+ {
+ mir_cslock lck(m_csHttpQueue);
+ m_arHttpQueue.insert(pReq);
+ }
+
+ SetEvent(m_evRequestsQueue);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+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);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+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_results = &(*m_root)["results"];
+}
+
+RobustReply::~RobustReply()
+{
+ json_delete(m_root);
+}
diff --git a/protocols/ICQ-WIM/src/http.h b/protocols/ICQ-WIM/src/http.h
new file mode 100644
index 0000000000..bdcac34f50
--- /dev/null
+++ b/protocols/ICQ-WIM/src/http.h
@@ -0,0 +1,51 @@
+
+class CIcqProto;
+
+enum IcqConnection
+{
+ CONN_NONE = -1, CONN_MAIN = 0, CONN_FETCH = 1, CONN_RAPI = 2, CONN_LAST = 3
+};
+
+struct AsyncHttpRequest : public MTHttpRequest<CIcqProto>
+{
+ IcqConnection m_conn;
+ MCONTACT hContact;
+ char m_reqId[50];
+
+ AsyncHttpRequest(IcqConnection, int type, const char *szUrl, MTHttpRequestHandler pFunc = nullptr);
+
+ void ReplaceJsonParam(const char *paramName, const char *newValue);
+};
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+class JsonReply
+{
+ JSONNode *m_root = nullptr;
+ int m_errorCode = 0, m_detailCode = 0;
+ JSONNode* m_data = nullptr;
+ CMStringA m_requestId;
+
+public:
+ JsonReply(NETLIBHTTPREQUEST*);
+ ~JsonReply();
+
+ __forceinline const CMStringA& requestId() const { return m_requestId; }
+ __forceinline JSONNode& data() const { return *m_data; }
+ __forceinline int error() const { return m_errorCode; }
+ __forceinline int detail() const { return m_detailCode; }
+};
+
+class RobustReply
+{
+ JSONNode *m_root = nullptr;
+ int m_errorCode = 0;
+ JSONNode* m_results = nullptr;
+
+public:
+ RobustReply(NETLIBHTTPREQUEST*);
+ ~RobustReply();
+
+ __forceinline JSONNode& results() const { return *m_results; }
+ __forceinline int error() const { return m_errorCode; }
+};
diff --git a/protocols/ICQ-WIM/src/main.cpp b/protocols/ICQ-WIM/src/main.cpp
new file mode 100644
index 0000000000..0f1888952d
--- /dev/null
+++ b/protocols/ICQ-WIM/src/main.cpp
@@ -0,0 +1,77 @@
+// -----------------------------------------------------------------------------
+// ICQ plugin for Miranda NG
+// -----------------------------------------------------------------------------
+// Copyright © 2018-19 Miranda NG team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+// -----------------------------------------------------------------------------
+
+#include "stdafx.h"
+
+bool g_bPopupService;
+
+HWND g_hwndHeartbeat;
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+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::CMPlugin() :
+ ACCPROTOPLUGIN<CIcqProto>(MODULENAME, pluginInfoEx)
+{
+ SetUniqueId(DB_KEY_UIN);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST };
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+CMPlugin g_plugin;
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+int ModuleLoad(WPARAM, LPARAM)
+{
+ g_bPopupService = ServiceExists(MS_POPUP_ADDPOPUPT);
+ return 0;
+}
+
+int CMPlugin::Load()
+{
+ g_hwndHeartbeat = CreateWindowEx(0, L"STATIC", nullptr, 0, 0, 0, 0, 0, nullptr, nullptr, nullptr, nullptr);
+
+ HookEvent(ME_SYSTEM_MODULELOAD, ModuleLoad);
+ HookEvent(ME_SYSTEM_MODULEUNLOAD, ModuleLoad);
+ ModuleLoad(0, 0);
+ return 0;
+};
+
+int CMPlugin::Unload()
+{
+ DestroyWindow(g_hwndHeartbeat);
+ return 0;
+}
diff --git a/protocols/ICQ-WIM/src/options.cpp b/protocols/ICQ-WIM/src/options.cpp
new file mode 100644
index 0000000000..3ff4494232
--- /dev/null
+++ b/protocols/ICQ-WIM/src/options.cpp
@@ -0,0 +1,226 @@
+// -----------------------------------------------------------------------------
+// ICQ plugin for Miranda NG
+// -----------------------------------------------------------------------------
+// Copyright © 2018-19 Miranda NG team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+// -----------------------------------------------------------------------------
+
+#include "stdafx.h"
+
+struct CIcqRegistrationDlg : public CProtoDlgBase<CIcqProto>
+{
+ CMStringA szTrans, szMsisdn;
+ int iErrorCode;
+
+ CCtrlEdit edtPhone, edtCode;
+ CCtrlButton btnSendSms;
+
+ CIcqRegistrationDlg(CIcqProto *ppro) :
+ CProtoDlgBase<CIcqProto>(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://www.icq.com/smsreg/loginWithPhoneNumber.php", &CIcqProto::OnLoginViaPhone);
+ pReq << CHAR_PARAM("locale", "en") << CHAR_PARAM("msisdn", szMsisdn) << CHAR_PARAM("trans_id", szTrans) << CHAR_PARAM("k", ICQ_APP_ID)
+ << 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://clientapi.icq.net/fcgi-bin/smsphoneinfo", &CIcqProto::OnCheckPhone);
+ pReq << CHAR_PARAM("service", "icq_registration") << CHAR_PARAM("info", "typing_check,score,iso_country_code")
+ << 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://www.icq.com/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", ICQ_APP_ID);
+ 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", ICQ_APP_ID) << 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();
+ m_szAToken = ptrA(mir_urlDecode(m_szAToken));
+ setString(DB_KEY_ATOKEN, m_szAToken);
+
+ m_szSessionKey = data["sessionKey"].as_mstring();
+ m_szSessionKey = ptrA(mir_urlDecode(m_szSessionKey));
+ setString(DB_KEY_SESSIONKEY, m_szSessionKey);
+
+ m_dwUin = _wtoi(data["loginId"].as_mstring());
+ setByte("PhoneReg", 1);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+class CIcqOptionsDlg : public CProtoDlgBase<CIcqProto>
+{
+ CCtrlEdit edtUin, edtPassword;
+ CCtrlCheck chkUseFriendly, chkHideChats;
+ CCtrlButton btnCreate;
+ CMStringW wszOldPass;
+
+public:
+ CIcqOptionsDlg(CIcqProto *ppro, int iDlgID, bool bFullDlg) :
+ CProtoDlgBase<CIcqProto>(ppro, iDlgID),
+ edtUin(this, IDC_UIN),
+ btnCreate(this, IDC_REGISTER),
+ edtPassword(this, IDC_PASSWORD),
+ chkHideChats(this, IDC_HIDECHATS),
+ chkUseFriendly(this, IDC_USEFRIENDLY)
+ {
+ btnCreate.OnClick = Callback(this, &CIcqOptionsDlg::onClick_Register);
+
+ CreateLink(edtUin, ppro->m_dwUin);
+ CreateLink(edtPassword, ppro->m_szPassword);
+ if (bFullDlg) {
+ CreateLink(chkHideChats, ppro->m_bHideGroupchats);
+ CreateLink(chkUseFriendly, ppro->m_bUseFriendly);
+ }
+
+ wszOldPass = ppro->m_szPassword;
+ }
+
+ bool OnApply() override
+ {
+ if (wszOldPass != ptrW(edtPassword.GetText())) {
+ m_proto->delSetting(DB_KEY_ATOKEN);
+ m_proto->delSetting(DB_KEY_SESSIONKEY);
+ m_proto->delSetting("PhoneReg");
+ }
+ return true;
+ }
+
+ void onClick_Register(CCtrlButton*)
+ {
+ CIcqRegistrationDlg dlg(m_proto);
+ dlg.SetParent(m_hwnd);
+ if (dlg.DoModal()) {
+ edtUin.SetInt(m_proto->getDword(DB_KEY_UIN));
+ edtPassword.SetText(L"");
+ wszOldPass.Empty();
+ }
+ }
+};
+
+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;
+ odp.szGroup.w = LPGENW("Network");
+
+ odp.position = 1;
+// odp.szTab.w = LPGENW("Account");
+ odp.pDialog = new CIcqOptionsDlg(this, IDD_OPTIONS_FULL, true);
+ g_plugin.addOptions(wParam, &odp);
+ return 0;
+}
diff --git a/protocols/ICQ-WIM/src/proto.cpp b/protocols/ICQ-WIM/src/proto.cpp
new file mode 100644
index 0000000000..86fb3257fb
--- /dev/null
+++ b/protocols/ICQ-WIM/src/proto.cpp
@@ -0,0 +1,479 @@
+// ---------------------------------------------------------------------------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-2019 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)
+
+CIcqProto::CIcqProto(const char* aProtoName, const wchar_t* aUserName) :
+ PROTO<CIcqProto>(aProtoName, aUserName),
+ m_arHttpQueue(10),
+ m_arOwnIds(1),
+ m_arCache(20, NumericKeySortT),
+ arMarkReadQueue(10, NumericKeySortT),
+ m_evRequestsQueue(CreateEvent(nullptr, FALSE, FALSE, nullptr)),
+ m_dwUin(this, DB_KEY_UIN, 0),
+ m_szPassword(this, "Password"),
+ m_bUseFriendly(this, "UseFriendly", 1),
+ m_bHideGroupchats(this, "HideChats", 1)
+{
+ // services
+ CreateProtoService(PS_CREATEACCMGRUI, &CIcqProto::CreateAccMgrUI);
+ CreateProtoService(PS_GETAVATARINFO, &CIcqProto::GetAvatarInfo);
+ CreateProtoService(PS_GETMYAVATAR, &CIcqProto::GetAvatar);
+ CreateProtoService(PS_GETAVATARCAPS, &CIcqProto::GetAvatarCaps);
+ CreateProtoService(PS_SETMYAVATAR, &CIcqProto::SetAvatar);
+
+ // 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);
+
+ // netlib handle
+ CMStringW descr(FORMAT, TranslateT("%s server connection"), m_tszUserName);
+
+ NETLIBUSER nlu = {};
+ nlu.szSettingsModule = m_szModuleName;
+ nlu.flags = NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE;
+ nlu.szDescriptiveName.w = descr.GetBuffer();
+ m_hNetlibUser = Netlib_RegisterUser(&nlu);
+
+ m_hWorkerThread = ForkThreadEx(&CIcqProto::ServerThread, nullptr, nullptr);
+
+ InitContactCache();
+}
+
+CIcqProto::~CIcqProto()
+{
+ ::CloseHandle(m_evRequestsQueue);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////
+// OnModulesLoadedEx - performs hook registration
+
+void CIcqProto::OnModulesLoaded()
+{
+ GCREGISTER gcr = {};
+ gcr.dwFlags = GC_TYPNOTIF | GC_CHANMGR;
+ gcr.ptszDispName = m_tszUserName;
+ gcr.pszModule = m_szModuleName;
+ Chat_Register(&gcr);
+}
+
+void CIcqProto::OnShutdown()
+{
+ m_bTerminated = true;
+}
+
+void CIcqProto::OnContactDeleted(MCONTACT hContact)
+{
+ CMStringA szId(GetUserId(hContact));
+ if (!isChatRoom(hContact))
+ m_arCache.remove(FindContactByUIN(atoi(szId)));
+
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/removeBuddy");
+ pReq << CHAR_PARAM("f", "json") << CHAR_PARAM("aimsid", m_aimsid) << CHAR_PARAM("buddy", szId)
+ << CHAR_PARAM("r", pReq->m_reqId) << INT_PARAM("allGroups", 1);
+ pReq->flags |= NLHRF_NODUMPSEND;
+ Push(pReq);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+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);
+
+ Menu_ShowItem(m_hUploadGroups, false);
+}
+
+INT_PTR CIcqProto::UploadGroups(WPARAM, LPARAM)
+{
+ for (auto &it : AccContacts()) {
+ if (isChatRoom(it))
+ continue;
+
+ CMStringW wszIcqGroup(getMStringW(it, "IcqGroup")), wszMirGroup(db_get_wsm(it, "CList", "Group"));
+ if (wszMirGroup.IsEmpty())
+ wszMirGroup = L"General";
+ if (wszIcqGroup != wszMirGroup)
+ MoveContactToGroup(it, wszIcqGroup, wszMirGroup);
+ }
+ return 0;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+void CIcqProto::MarkReadTimerProc(HWND hwnd, UINT, UINT_PTR id, DWORD)
+{
+ CIcqProto *ppro = (CIcqProto*)id;
+
+ mir_cslock lck(ppro->csMarkReadQueue);
+ while (ppro->arMarkReadQueue.getCount()) {
+ IcqCacheItem *pUser = ppro->arMarkReadQueue[0];
+
+ auto *pReq = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER);
+ JSONNode request, params; params.set_name("params");
+ params << CHAR_PARAM("sn", ppro->GetUserId(pUser->m_hContact)) << INT64_PARAM("lastRead", ppro->getId(pUser->m_hContact, DB_KEY_LASTMSGID));
+ request << CHAR_PARAM("method", "setDlgStateWim") << CHAR_PARAM("reqId", pReq->m_reqId)
+ << CHAR_PARAM("authToken", ppro->m_szRToken) << INT_PARAM("clientId", ppro->m_iRClientId) << params;
+ pReq->m_szParam = ptrW(json_write(&request));
+ ppro->Push(pReq);
+
+ ppro->arMarkReadQueue.remove(0);
+ }
+ KillTimer(hwnd, id);
+}
+
+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 = GetContactProto(hContact);
+ if (mir_strcmp(szProto, m_szModuleName))
+ return 0;
+
+ if (m_bOnline) {
+ SetTimer(g_hwndHeartbeat, UINT_PTR(this), 200, &CIcqProto::MarkReadTimerProc);
+
+ IcqCacheItem *pCache = FindContactByUIN(getDword(hContact, DB_KEY_UIN));
+ if (pCache) {
+ mir_cslock lck(csMarkReadQueue);
+ if (arMarkReadQueue.indexOf(pCache) == -1)
+ arMarkReadQueue.insert(pCache);
+ }
+ }
+ 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
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/");
+ pReq << CHAR_PARAM("f", "json") << CHAR_PARAM("aimsid", m_aimsid) << CHAR_PARAM("r", pReq->m_reqId);
+ if (pParam->pszOldName == nullptr) {
+ pReq->m_szUrl += "addGroup";
+ pReq << WCHAR_PARAM("group", pParam->pszNewName);
+ }
+ else if (pParam->pszNewName == nullptr) {
+ pReq->m_szUrl += "removeGroup";
+ pReq << WCHAR_PARAM("group", pParam->pszOldName);
+ }
+ else {
+ pReq->m_szUrl += "renameGroup";
+ pReq << WCHAR_PARAM("oldGroup", pParam->pszOldName) << WCHAR_PARAM("newGroup", pParam->pszNewName);
+ }
+ Push(pReq);
+ }
+ else MoveContactToGroup(hContact, getMStringW(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;
+
+ DWORD dwUin = _wtol(psr->id.w);
+
+ MCONTACT hContact = CreateContact(dwUin, 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;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////
+// PSS_AUTHREQUEST
+
+int CIcqProto::AuthRequest(MCONTACT hContact, const wchar_t* szMessage)
+{
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/buddylist/addBuddy", &CIcqProto::OnAddBuddy);
+ pReq << CHAR_PARAM("f", "json") << CHAR_PARAM("aimsid", m_aimsid) << CHAR_PARAM("r", pReq->m_reqId) << WCHAR_PARAM("authorizationMsg", szMessage)
+ << CHAR_PARAM("buddy", GetUserId(hContact)) << CHAR_PARAM("group", "General") << INT_PARAM("preAuthorized", 1);
+ pReq->flags |= NLHRF_NODUMPSEND;
+ pReq->hContact = hContact;
+ Push(pReq);
+ return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////
+// PS_FileAllow - starts a file transfer
+
+HANDLE CIcqProto::FileAllow(MCONTACT, HANDLE, const wchar_t*)
+{
+ return nullptr; // Failure
+}
+
+////////////////////////////////////////////////////////////////////////////////////////
+// PS_FileCancel - cancels a file transfer
+
+int CIcqProto::FileCancel(MCONTACT, HANDLE)
+{
+ return 1; // Failure
+}
+
+////////////////////////////////////////////////////////////////////////////////////////
+// PS_FileDeny - denies a file transfer
+
+int CIcqProto::FileDeny(MCONTACT, HANDLE, const wchar_t*)
+{
+ return 1; // Invalid contact
+}
+
+////////////////////////////////////////////////////////////////////////////////////////
+// PS_FileResume - processes file renaming etc
+
+int CIcqProto::FileResume(HANDLE, int*, const wchar_t**)
+{
+ return 1; // Failure
+}
+
+////////////////////////////////////////////////////////////////////////////////////////
+// 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_URL | PF1_AUTHREQ | PF1_BASICSEARCH | PF1_ADDSEARCHRES | /*PF1_SEARCHBYNAME | TODO */
+ PF1_VISLIST | PF1_INVISLIST | PF1_MODEMSG | 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_4:
+ nReturn = PF4_FORCEAUTH | PF4_SUPPORTIDLE | PF4_IMSENDOFFLINE | PF4_SUPPORTTYPING | PF4_AVATARS | PF4_SERVERMSGID;
+ break;
+
+ case PFLAG_UNIQUEIDTEXT:
+ return (INT_PTR)Translate("ICQ number or 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 AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER, &CIcqProto::OnSearchResults);
+
+ JSONNode request, params; params.set_name("params");
+ params << WCHAR_PARAM("keyword", pszSearch);
+ request << CHAR_PARAM("method", "search") << CHAR_PARAM("reqId", pReq->m_reqId) << CHAR_PARAM("authToken", m_szRToken)
+ << INT_PARAM("clientId", m_iRClientId) << params;
+ pReq->m_szParam = ptrW(json_write(&request));
+ Push(pReq);
+ return pReq;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////
+// SendFile - sends a file
+
+HANDLE CIcqProto::SendFile(MCONTACT, const wchar_t*, wchar_t**)
+{
+ return nullptr; // 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;
+ m_arOwnIds.insert(pOwn);
+
+ pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("aimsid", m_aimsid) << CHAR_PARAM("f", "json") << CHAR_PARAM("k", ICQ_APP_ID)
+ << CHAR_PARAM("mentions", "") << CHAR_PARAM("message", pszSrc) << CHAR_PARAM("offlineIM", "true") << CHAR_PARAM("r", pReq->m_reqId)
+ << CHAR_PARAM("t", szUserid) << INT_PARAM("ts", time(0));
+ Push(pReq);
+ return id;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////
+// SendUrl
+
+int CIcqProto::SendUrl(MCONTACT, int, const char*)
+{
+ return 1; // Failure
+}
+
+////////////////////////////////////////////////////////////////////////////////////////
+// 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);
+ ShutdownSession();
+ }
+ 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 (!IsStatusConnecting(m_iStatus)) {
+ m_iStatus = ID_STATUS_CONNECTING;
+ ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus);
+
+ if (m_dwUin == 0) {
+ debugLogA("Thread ended, UIN/password are not configured");
+ ConnectionFailed(LOGINERR_BADUSERID);
+ return 0;
+ }
+
+ if (!getByte("PhoneReg") && mir_wstrlen(m_szPassword) == 0) {
+ 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_GetAwayMsg - returns a contact's away message
+
+HANDLE CIcqProto::GetAwayMsg(MCONTACT)
+{
+ return nullptr; // Failure
+}
+
+////////////////////////////////////////////////////////////////////////////////////////
+// PSR_AWAYMSG - processes received status mode message
+
+int CIcqProto::RecvAwayMsg(MCONTACT, int, PROTORECVEVENT*)
+{
+ return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////
+// PS_SetAwayMsg - sets the away status message
+
+int CIcqProto::SetAwayMsg(int, const wchar_t*)
+{
+ return 0; // Success
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// PS_UserIsTyping - sends a UTN notification
+
+int CIcqProto::UserIsTyping(MCONTACT hContact, int type)
+{
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/im/setTyping");
+ pReq->flags |= NLHRF_NODUMPSEND;
+ pReq << CHAR_PARAM("f", "json") << CHAR_PARAM("aimsid", m_aimsid) << CHAR_PARAM("f", "json")
+ << CHAR_PARAM("t", GetUserId(hContact)) << CHAR_PARAM("r", pReq->m_reqId)
+ << CHAR_PARAM("typingStatus", (type == PROTOTYPE_SELFTYPING_ON) ? "typing" : "typed");
+ Push(pReq);
+ return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////
+// PS_SetApparentMode - sets the visibility status
+
+int CIcqProto::SetApparentMode(MCONTACT, int)
+{
+ return 1; // Failure
+}
diff --git a/protocols/ICQ-WIM/src/proto.h b/protocols/ICQ-WIM/src/proto.h
new file mode 100644
index 0000000000..46bcb56e8f
--- /dev/null
+++ b/protocols/ICQ-WIM/src/proto.h
@@ -0,0 +1,263 @@
+// ---------------------------------------------------------------------------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-2019 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 ICQ_APP_ID "ic1nmMjqg7Yu-0hL"
+#define ICQ_API_SERVER "https://api.icq.net"
+#define ICQ_ROBUST_SERVER "https://rapi.icq.net"
+
+enum ChatMenuItems
+{
+ IDM_INVITE = 10, IDM_LEAVE
+};
+
+struct IcqCacheItem
+{
+ IcqCacheItem(DWORD _uin, MCONTACT _contact) :
+ m_uin(_uin),
+ m_hContact(_contact)
+ {}
+
+ DWORD m_uin;
+ MCONTACT m_hContact;
+ bool m_bInList = false;
+};
+
+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;
+};
+
+class CIcqProto : public PROTO<CIcqProto>
+{
+ friend struct CIcqRegistrationDlg;
+ friend class CGroupchatInviteDlg;
+
+ bool m_bOnline = false, m_bTerminated = false;
+ void CheckAvatarChange(MCONTACT hContact, const JSONNode&);
+ void CheckLastId(MCONTACT hContact, const JSONNode&);
+ void CheckNickChange(MCONTACT hContact, const JSONNode&);
+ MCONTACT CheckOwnMessage(const CMStringA &reqId, const CMStringA &msgId, bool bRemove);
+ void CheckPassword(void);
+ void ConnectionFailed(int iReason);
+ CMStringA GetUserId(MCONTACT);
+ void MoveContactToGroup(MCONTACT hContact, const wchar_t *pwszGroup, const wchar_t *pwszNewGroup);
+ MCONTACT ParseBuddyInfo(const JSONNode &buddy, MCONTACT hContact = -1);
+ void ParseMessage(MCONTACT hContact, __int64 &lastMsgId, const JSONNode &msg);
+ void RetrieveUserHistory(MCONTACT, __int64 startMsgId, __int64 endMsgId);
+ void RetrieveUserInfo(MCONTACT);
+ void SetServerStatus(int iNewStatus);
+ void ShutdownSession(void);
+ void StartSession(void);
+
+ void OnLoggedIn(void);
+ void OnLoggedOut(void);
+
+ mir_cs csMarkReadQueue;
+ LIST<IcqCacheItem> arMarkReadQueue;
+ static void CALLBACK MarkReadTimerProc(HWND hwnd, UINT, UINT_PTR id, DWORD);
+
+ __int64 getId(MCONTACT hContact, const char *szSetting);
+ void setId(MCONTACT hContact, const char *szSetting, __int64 iValue);
+
+ void OnAddBuddy(NETLIBHTTPREQUEST*, AsyncHttpRequest*);
+ void OnAddClient(NETLIBHTTPREQUEST*, AsyncHttpRequest*);
+ void OnCheckPassword(NETLIBHTTPREQUEST*, AsyncHttpRequest*);
+ void OnCheckPhone(NETLIBHTTPREQUEST*, AsyncHttpRequest*);
+ void OnFetchEvents(NETLIBHTTPREQUEST*, AsyncHttpRequest*);
+ void OnGetChatInfo(NETLIBHTTPREQUEST*, AsyncHttpRequest*);
+ void OnGetUserHistory(NETLIBHTTPREQUEST*, AsyncHttpRequest*);
+ void OnGetUserInfo(NETLIBHTTPREQUEST*, AsyncHttpRequest*);
+ void OnLoginViaPhone(NETLIBHTTPREQUEST*, AsyncHttpRequest*);
+ void OnNormalizePhone(NETLIBHTTPREQUEST*, AsyncHttpRequest*);
+ void OnReceiveAvatar(NETLIBHTTPREQUEST*, AsyncHttpRequest*);
+ void OnSearchResults(NETLIBHTTPREQUEST*, AsyncHttpRequest*);
+ void OnSendMessage(NETLIBHTTPREQUEST*, AsyncHttpRequest*);
+ void OnStartSession(NETLIBHTTPREQUEST*, AsyncHttpRequest*);
+ void OnValidateSms(NETLIBHTTPREQUEST*, AsyncHttpRequest*);
+
+ void ProcessBuddyList(const JSONNode&);
+ void ProcessDiff(const JSONNode&);
+ void ProcessEvent(const JSONNode&);
+ void ProcessGroupChat(const JSONNode&);
+ void ProcessHistData(const JSONNode&);
+ void ProcessImState(const JSONNode&);
+ void ProcessMyInfo(const JSONNode&);
+ void ProcessPresence(const JSONNode&);
+ void ProcessTyping(const JSONNode&);
+
+ IcqConn m_ConnPool[CONN_LAST];
+ CMStringA m_szSessionKey;
+ CMStringA m_szAToken;
+ CMStringA m_szRToken;
+ CMStringA m_fetchBaseURL;
+ CMStringA m_aimsid;
+ LONG m_msgId = 1;
+ int m_iRClientId;
+ HGENMENU m_hUploadGroups;
+
+ OBJLIST<IcqOwnMessage> m_arOwnIds;
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // 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 ExecuteRequest(AsyncHttpRequest*);
+ void Push(MHttpRequest*);
+ bool RefreshRobustToken();
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // cache
+
+ mir_cs m_csCache;
+ OBJLIST<IcqCacheItem> m_arCache;
+
+ void InitContactCache(void);
+ IcqCacheItem* FindContactByUIN(DWORD);
+ MCONTACT CreateContact(DWORD dwUin, bool bTemporary);
+
+ void GetAvatarFileName(MCONTACT hContact, wchar_t *pszDest, size_t cbLen);
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // threads
+
+ HANDLE m_hWorkerThread;
+ void __cdecl ServerThread(void*);
+
+ HANDLE m_hPollThread;
+ 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 UploadGroups(WPARAM, LPARAM);
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // events
+
+ int __cdecl OnGroupChange(WPARAM, LPARAM);
+ int __cdecl OnDbEventRead(WPARAM, LPARAM);
+ int __cdecl OnOptionsInit(WPARAM, LPARAM);
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // PROTO_INTERFACE
+
+ MCONTACT AddToList( int flags, PROTOSEARCHRESULT *psr) override;
+
+ int AuthRequest(MCONTACT hContact, const wchar_t *szMessage) override;
+
+ HANDLE FileAllow(MCONTACT hContact, HANDLE hTransfer, const wchar_t *szPath) override;
+ int FileCancel(MCONTACT hContact, HANDLE hTransfer) override;
+ int FileDeny(MCONTACT hContact, HANDLE hTransfer, const wchar_t *szReason) override;
+ int FileResume( HANDLE hTransfer, int *action, const wchar_t **szFilename) 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 SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) override;
+ int SendMsg(MCONTACT hContact, int flags, const char *msg) override;
+ int SendUrl(MCONTACT hContact, int flags, const char *url) override;
+
+ int SetApparentMode(MCONTACT hContact, int mode) override;
+ int SetStatus(int iNewStatus) override;
+
+ HANDLE GetAwayMsg(MCONTACT hContact) override;
+ int RecvAwayMsg(MCONTACT hContact, int mode, PROTORECVEVENT *evt) override;
+ int SetAwayMsg(int m_iStatus, const wchar_t *msg) override;
+
+ int UserIsTyping(MCONTACT hContact, int type) override;
+
+ void OnBuildProtoMenu(void) override;
+ void OnContactDeleted(MCONTACT) override;
+ void OnModulesLoaded() override;
+ void OnShutdown() override;
+
+public:
+ CIcqProto(const char*, const wchar_t*);
+ ~CIcqProto();
+
+ CMOption<DWORD> m_dwUin; // our own id
+ CMOption<wchar_t*> m_szPassword; // password, if present
+ CMOption<BYTE> m_bUseFriendly; // use friendly names instead of old icq nicks
+ CMOption<BYTE> m_bHideGroupchats; // don't pop up group chat windows on startup
+};
+
+struct CMPlugin : public ACCPROTOPLUGIN<CIcqProto>
+{
+ CMPlugin();
+
+ int Load() override;
+ int Unload() override;
+};
+
+#endif
diff --git a/protocols/ICQ-WIM/src/resource.h b/protocols/ICQ-WIM/src/resource.h
new file mode 100644
index 0000000000..beeac3d899
--- /dev/null
+++ b/protocols/ICQ-WIM/src/resource.h
@@ -0,0 +1,29 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by w:\miranda-ng\protocols\Icq10\res\resources.rc
+//
+#define IDD_OPTIONS_FULL 101
+#define IDD_OPTIONS_ACCMGR 102
+#define IDD_GROUPCHAT_INVITE 103
+#define IDD_REGISTER 105
+#define IDC_PASSWORD 1001
+#define IDC_UIN 1002
+#define IDC_USEFRIENDLY 1003
+#define IDC_USEFRIENDLY2 1004
+#define IDC_HIDECHATS 1004
+#define IDC_REGISTER 1005
+#define IDC_PHONE 1006
+#define IDC_CLIST 1007
+#define IDC_SENDSMS 1008
+#define IDC_CODE 1009
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 107
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1010
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/protocols/ICQ-WIM/src/server.cpp b/protocols/ICQ-WIM/src/server.cpp
new file mode 100644
index 0000000000..d77daf4075
--- /dev/null
+++ b/protocols/ICQ-WIM/src/server.cpp
@@ -0,0 +1,859 @@
+// -----------------------------------------------------------------------------
+// ICQ plugin for Miranda NG
+// -----------------------------------------------------------------------------
+// Copyright © 2018-19 Miranda NG team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+// -----------------------------------------------------------------------------
+
+#include "stdafx.h"
+
+#pragma comment(lib, "libeay32.lib")
+
+void CIcqProto::CheckAvatarChange(MCONTACT hContact, const JSONNode &ev)
+{
+ CMStringW wszIconId(ev["iconId"].as_mstring());
+ CMStringW oldIconID(getMStringW(hContact, "IconId"));
+ if (wszIconId != oldIconID) {
+ setWString(hContact, "IconId", wszIconId);
+
+ CMStringA szUrl(ev["buddyIcon"].as_mstring());
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, szUrl, &CIcqProto::OnReceiveAvatar);
+ pReq->hContact = hContact;
+ Push(pReq);
+ }
+}
+
+void CIcqProto::CheckLastId(MCONTACT hContact, const JSONNode &ev)
+{
+ __int64 msgId = _wtoi64(ev["histMsgId"].as_mstring());
+ __int64 lastId = getId(hContact, DB_KEY_LASTMSGID);
+ if (msgId > lastId)
+ setId(hContact, DB_KEY_LASTMSGID, msgId);
+}
+
+void CIcqProto::CheckNickChange(MCONTACT hContact, const JSONNode &ev)
+{
+ // if we already set a nick, ignore change when friendly names aren't used
+ if (!m_bUseFriendly && !getMStringW(hContact, "Nick").IsEmpty())
+ return;
+
+ CMStringW wszNick = ev["friendly"].as_mstring();
+ if (m_bUseFriendly || !wszNick.IsEmpty())
+ setWString(hContact, "Nick", wszNick);
+}
+
+MCONTACT CIcqProto::CheckOwnMessage(const CMStringA &reqId, const CMStringA &msgId, bool bRemove)
+{
+ for (auto &own: m_arOwnIds) {
+ if (!mir_strcmp(reqId, own->m_guid)) {
+ ProtoBroadcastAck(own->m_hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)own->m_msgid, (LPARAM)msgId.c_str());
+ MCONTACT ret = own->m_hContact;
+ if (bRemove)
+ m_arOwnIds.remove(m_arOwnIds.indexOf(&own));
+ return ret;
+ }
+ }
+ return 0;
+}
+
+void CIcqProto::CheckPassword()
+{
+ char mirVer[100];
+ Miranda_GetVersionText(mirVer, _countof(mirVer));
+
+ m_szAToken = getMStringA(DB_KEY_ATOKEN);
+ m_iRClientId = getDword(DB_KEY_RCLIENTID);
+ m_szSessionKey = getMStringA(DB_KEY_SESSIONKEY);
+ if (m_szAToken.IsEmpty() || m_szSessionKey.IsEmpty()) {
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, "https://api.login.icq.net/auth/clientLogin", &CIcqProto::OnCheckPassword);
+ pReq << CHAR_PARAM("clientName", "Miranda NG") << CHAR_PARAM("clientVersion", mirVer) << CHAR_PARAM("devId", ICQ_APP_ID)
+ << CHAR_PARAM("f", "json") << CHAR_PARAM("tokenType", "longTerm") << INT_PARAM("s", m_dwUin) << WCHAR_PARAM("pwd", m_szPassword);
+ pReq->flags |= NLHRF_NODUMPSEND;
+ Push(pReq);
+ }
+ else StartSession();
+}
+
+void CIcqProto::ConnectionFailed(int iReason)
+{
+ debugLogA("ConnectionFailed -> reason %d", iReason);
+
+ ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, nullptr, iReason);
+ ShutdownSession();
+}
+
+void CIcqProto::MoveContactToGroup(MCONTACT hContact, const wchar_t *pwszGroup, const wchar_t *pwszNewGroup)
+{
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/moveBuddy");
+ pReq << CHAR_PARAM("f", "json") << CHAR_PARAM("aimsid", m_aimsid) << CHAR_PARAM("r", pReq->m_reqId)
+ << CHAR_PARAM("buddy", GetUserId(hContact)) << WCHAR_PARAM("group", pwszGroup) << WCHAR_PARAM("newGroup", pwszNewGroup);
+ Push(pReq);
+}
+
+void CIcqProto::OnLoggedIn()
+{
+ debugLogA("CIcqProto::OnLoggedIn");
+ m_bOnline = true;
+ SetServerStatus(m_iDesiredStatus);
+ RetrieveUserInfo(0);
+}
+
+void CIcqProto::OnLoggedOut()
+{
+ debugLogA("CIcqProto::OnLoggedOut");
+ m_bOnline = false;
+
+ ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE);
+ m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE;
+
+ setAllContactStatuses(ID_STATUS_OFFLINE, false);
+}
+
+MCONTACT CIcqProto::ParseBuddyInfo(const JSONNode &buddy, MCONTACT hContact)
+{
+ // user chat?
+ if (buddy["userType"].as_mstring() == "interop") {
+ CMStringW wszChatId(buddy["aimId"].as_mstring());
+ CMStringW wszChatName(buddy["friendly"].as_mstring());
+
+ SESSION_INFO *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, wszChatId, wszChatName);
+ if (si == nullptr)
+ return INVALID_CONTACT_ID;
+
+ Chat_AddGroup(si, TranslateT("admin"));
+ Chat_AddGroup(si, TranslateT("member"));
+ Chat_Control(m_szModuleName, wszChatId, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE);
+ Chat_Control(m_szModuleName, wszChatId, SESSION_ONLINE);
+ return si->hContact;
+ }
+
+ DWORD dwUin = _wtol(buddy["aimId"].as_mstring());
+
+ if (hContact == -1) {
+ hContact = CreateContact(dwUin, false);
+ FindContactByUIN(dwUin)->m_bInList = true;
+ }
+
+ CMStringW str(buddy["state"].as_mstring());
+ setDword(hContact, "Status", StatusFromString(str));
+
+ const JSONNode &profile = buddy["profile"];
+ if (profile) {
+ str = profile["firstName"].as_mstring();
+ if (!str.IsEmpty())
+ setWString(hContact, "FirstName", str);
+
+ str = profile["lastName"].as_mstring();
+ if (!str.IsEmpty())
+ setWString(hContact, "LastName", str);
+
+ str = profile["friendlyName"].as_mstring();
+ if (!str.IsEmpty())
+ if (!m_bUseFriendly || getMStringW("Nick").IsEmpty())
+ setWString(hContact, "Nick", str);
+
+ time_t birthDate = profile["birthDate"].as_int();
+ if (birthDate != 0) {
+ struct tm *timeinfo = localtime(&birthDate);
+ if (timeinfo != nullptr) {
+ setWord(hContact, "BirthDay", timeinfo->tm_mday);
+ setWord(hContact, "BirthMonth", timeinfo->tm_mon+1);
+ setWord(hContact, "BirthYear", timeinfo->tm_year+1900);
+ }
+ }
+
+ str = profile["gender"].as_mstring();
+ if (!str.IsEmpty()) {
+ if (str == "male")
+ setByte(hContact, "Gender", 'M');
+ else if (str == "female")
+ setByte(hContact, "Gender", 'F');
+ }
+
+ for (auto &it : profile["homeAddress"]) {
+ str = it["city"].as_mstring();
+ if (!str.IsEmpty())
+ setWString(hContact, "City", str);
+
+ str = it["state"].as_mstring();
+ if (!str.IsEmpty())
+ setWString(hContact, "State", str);
+
+ str = it["country"].as_mstring();
+ if (!str.IsEmpty())
+ setWString(hContact, "Country", str);
+ }
+ }
+
+ CheckNickChange(hContact, buddy);
+
+ int lastLogin = buddy["lastseen"].as_int();
+ if (lastLogin)
+ setDword(hContact, "LastSeen", lastLogin);
+
+ str = buddy["statusMsg"].as_mstring();
+ if (str.IsEmpty())
+ db_unset(hContact, "CList", "StatusMsg");
+ else
+ db_set_ws(hContact, "CList", "StatusMsg", str);
+
+ CheckAvatarChange(hContact, buddy);
+ return hContact;
+}
+
+void CIcqProto::ParseMessage(MCONTACT hContact, __int64 &lastMsgId, const JSONNode &it)
+{
+ CMStringA szMsgId(it["msgId"].as_mstring());
+ __int64 msgId = _atoi64(szMsgId);
+ if (msgId > lastMsgId)
+ lastMsgId = msgId;
+
+ CMStringW type(it["mediaType"].as_mstring());
+ if (type != "text" && !type.IsEmpty())
+ return;
+
+ CMStringW wszText(it["text"].as_mstring());
+ if (isChatRoom(hContact)) {
+ CMStringA reqId(it["reqId"].as_mstring());
+ CheckOwnMessage(reqId, szMsgId, true);
+
+ CMStringW wszSender(it["chat"]["sender"].as_mstring());
+ CMStringW wszChatId(getMStringW(hContact, "ChatRoomID"));
+
+ GCEVENT gce = { m_szModuleName, wszChatId, GC_EVENT_MESSAGE };
+ gce.dwFlags = GCEF_ADDTOLOG;
+ gce.ptszUID = wszSender;
+ gce.ptszText = wszText;
+ gce.time = it["time"].as_int();
+ gce.bIsMe = _wtoi(wszSender) == (int)m_dwUin;
+ Chat_Event(&gce);
+ }
+ else {
+ // skip own messages, just set the server msgid
+ CMStringA reqId(it["reqId"].as_mstring());
+ if (CheckOwnMessage(reqId, szMsgId, true))
+ return;
+
+ // ignore duplicates
+ MEVENT hDbEvent = db_event_getById(m_szModuleName, szMsgId);
+ if (hDbEvent != 0)
+ return;
+
+ bool bIsOutgoing = it["outgoing"].as_bool();
+ ptrA szUtf(mir_utf8encodeW(wszText));
+
+ PROTORECVEVENT pre = {};
+ pre.flags = (bIsOutgoing) ? PREF_SENT : 0;
+ pre.szMsgId = szMsgId;
+ pre.timestamp = it["time"].as_int();
+ pre.szMessage = szUtf;
+ ProtoChainRecvMsg(hContact, &pre);
+ }
+}
+
+bool CIcqProto::RefreshRobustToken()
+{
+ if (!m_szRToken.IsEmpty())
+ return true;
+
+ bool bRet = false;
+ auto *tmp = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER "/genToken");
+
+ time_t ts = time(0);
+ tmp << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("k", ICQ_APP_ID)
+ << CHAR_PARAM("nonce", CMStringA(FORMAT, "%d-%d", ts, rand() % 10)) << INT_PARAM("ts", ts);
+ CalcHash(tmp);
+ tmp->flags |= NLHRF_PERSISTENT;
+ tmp->nlc = m_ConnPool[CONN_RAPI].s;
+ tmp->dataLength = tmp->m_szParam.GetLength();
+ tmp->pData = tmp->m_szParam.Detach();
+ tmp->szUrl = tmp->m_szUrl.GetBuffer();
+
+ CMStringA szAgent(FORMAT, "%d Mail.ru Windows ICQ (version 10.0.1999)", DWORD(m_dwUin));
+ tmp->AddHeader("User-Agent", szAgent);
+
+ NETLIBHTTPREQUEST *reply = Netlib_HttpTransaction(m_hNetlibUser, tmp);
+ if (reply != nullptr) {
+ m_ConnPool[CONN_RAPI].s = reply->nlc;
+
+ RobustReply result(reply);
+ if (result.error() == 20000) {
+ const JSONNode &results = result.results();
+ m_szRToken = results["authToken"].as_mstring();
+
+ // now add this token
+ auto *add = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER, &CIcqProto::OnAddClient);
+ JSONNode request, params; params.set_name("params");
+ request << CHAR_PARAM("method", "addClient") << CHAR_PARAM("reqId", add->m_reqId) << CHAR_PARAM("authToken", m_szRToken) << params;
+ add->m_szParam = ptrW(json_write(&request));
+ add->pUserInfo = &bRet;
+ ExecuteRequest(add);
+ }
+
+ Netlib_FreeHttpRequest(reply);
+ }
+ else m_ConnPool[CONN_RAPI].s = nullptr;
+
+ delete tmp;
+ return bRet;
+}
+
+void CIcqProto::RetrieveUserInfo(MCONTACT hContact)
+{
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/presence/get", &CIcqProto::OnGetUserInfo);
+ pReq->flags |= NLHRF_NODUMPSEND;
+ pReq->hContact = hContact;
+ pReq << CHAR_PARAM("f", "json") << CHAR_PARAM("aimsid", m_aimsid) << INT_PARAM("mdir", 1) << CHAR_PARAM("t", GetUserId(hContact));
+ Push(pReq);
+}
+
+void CIcqProto::RetrieveUserHistory(MCONTACT hContact, __int64 startMsgId, __int64 endMsgId)
+{
+ if (startMsgId == 0)
+ startMsgId = -1;
+ if (endMsgId == 0)
+ endMsgId = -1;
+
+ if (startMsgId >= endMsgId)
+ return;
+
+ auto *pReq = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER, &CIcqProto::OnGetUserHistory);
+ pReq->flags |= NLHRF_NODUMPSEND;
+ pReq->hContact = hContact;
+
+ JSONNode request, params; params.set_name("params");
+ params << CHAR_PARAM("sn", GetUserId(hContact)) << INT64_PARAM("fromMsgId", startMsgId);
+ if (endMsgId != -1)
+ params << INT64_PARAM("tillMsgId", endMsgId);
+ params << INT_PARAM("count", 1000) << CHAR_PARAM("aimSid", m_aimsid) << CHAR_PARAM("patchVersion", "1") << CHAR_PARAM("language", "ru-ru");
+ request << CHAR_PARAM("method", "getHistory") << CHAR_PARAM("reqId", pReq->m_reqId) << CHAR_PARAM("authToken", m_szRToken)
+ << INT_PARAM("clientId", m_iRClientId) << params;
+ pReq->m_szParam = ptrW(json_write(&request));
+ Push(pReq);
+}
+
+void CIcqProto::SetServerStatus(int iStatus)
+{
+ const char *szStatus = "online";
+ int invisible = 0;
+
+ switch (iStatus) {
+ case ID_STATUS_OFFLINE: szStatus = "offline"; break;
+ case ID_STATUS_NA: szStatus = "occupied"; break;
+ case ID_STATUS_AWAY:
+ case ID_STATUS_DND: szStatus = "away"; break;
+ case ID_STATUS_INVISIBLE:
+ invisible = 1;
+ }
+
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/presence/setState");
+ pReq->flags |= NLHRF_NODUMPSEND;
+ pReq << CHAR_PARAM("f", "json") << CHAR_PARAM("aimsid", m_aimsid) << CHAR_PARAM("r", pReq->m_reqId)
+ << CHAR_PARAM("view", szStatus) << INT_PARAM("invisible", invisible);
+ Push(pReq);
+
+ int iOldStatus = m_iStatus; m_iStatus = iStatus;
+ ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus);
+}
+
+void CIcqProto::ShutdownSession()
+{
+ if (m_bTerminated)
+ return;
+
+ debugLogA("CIcqProto::ShutdownSession");
+
+ // shutdown all resources
+ if (m_hWorkerThread)
+ SetEvent(m_evRequestsQueue);
+
+ OnLoggedOut();
+
+ for (auto &it : m_ConnPool)
+ if (it.s)
+ Netlib_Shutdown(it.s);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+#define CAPS "094613504c7f11d18222444553540000,094613514c7f11d18222444553540000,094613534c7f11d18222444553540000,094613544c7f11d18222444553540000,094613594c7f11d18222444553540000,0946135b4c7f11d18222444553540000,0946135a4c7f11d18222444553540000"
+#define EVENTS "myInfo,presence,buddylist,typing,dataIM,userAddedToBuddyList,webrtcMsg,mchat,hist,hiddenChat,diff,permitDeny,imState,notification,apps"
+#define FIELDS "aimId,buddyIcon,bigBuddyIcon,iconId,bigIconId,largeIconId,displayId,friendly,offlineMsg,state,statusMsg,userType,phoneNumber,cellNumber,smsNumber,workNumber,otherNumber,capabilities,ssl,abPhoneNumber,moodIcon,lastName,abPhones,abContactName,lastseen,mute,livechat,official"
+
+void CIcqProto::StartSession()
+{
+ ptrA szDeviceId(getStringA("DeviceId"));
+ if (szDeviceId == nullptr) {
+ UUID deviceId;
+ UuidCreate(&deviceId);
+ RPC_CSTR szId;
+ UuidToStringA(&deviceId, &szId);
+ szDeviceId = mir_strdup((char*)szId);
+ setString("DeviceId", szDeviceId);
+ RpcStringFreeA(&szId);
+ }
+
+ int ts = time(0);
+ CMStringA nonce(FORMAT, "%d-2", ts);
+
+ auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/aim/startSession", &CIcqProto::OnStartSession);
+
+ pReq << CHAR_PARAM("a", m_szAToken) << INT_PARAM("activeTimeout", 180) << CHAR_PARAM("assertCaps", CAPS)
+ << INT_PARAM("buildNumber", __BUILD_NUM) << CHAR_PARAM("deviceId", szDeviceId) << CHAR_PARAM("events", EVENTS)
+ << CHAR_PARAM("f", "json") << CHAR_PARAM("imf", "plain") << CHAR_PARAM("inactiveView", "offline")
+ << CHAR_PARAM("includePresenceFields", FIELDS) << CHAR_PARAM("invisible", "false")
+ << CHAR_PARAM("k", ICQ_APP_ID) << INT_PARAM("mobile", 0) << CHAR_PARAM("nonce", nonce) << CHAR_PARAM("r", pReq->m_reqId)
+ << INT_PARAM("rawMsg", 0) << INT_PARAM("sessionTimeout", 7776000) << INT_PARAM("ts", ts) << CHAR_PARAM("view", "online");
+
+ CalcHash(pReq);
+
+ Push(pReq);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+void CIcqProto::OnAddBuddy(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq)
+{
+ JsonReply root(pReply);
+ if (root.error() == 200) {
+ RetrieveUserInfo(pReq->hContact);
+ db_unset(pReq->hContact, "CList", "NotOnList");
+ }
+}
+
+void CIcqProto::OnAddClient(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq)
+{
+ bool *pRet = (bool*)pReq->pUserInfo;
+
+ RobustReply reply(pReply);
+ if (reply.error() != 20000) {
+ *pRet = false;
+ return;
+ }
+
+ const JSONNode &results = reply.results();
+ m_iRClientId = results["clientId"].as_int();
+ setDword(DB_KEY_RCLIENTID, m_iRClientId);
+ *pRet = true;
+}
+
+void CIcqProto::OnCheckPassword(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*)
+{
+ JsonReply root(pReply);
+ switch (root.error()) {
+ case 200:
+ break;
+
+ case 330:
+ case 440:
+ ConnectionFailed(LOGINERR_WRONGPASSWORD);
+ return;
+
+ default:
+ ConnectionFailed(LOGINERR_WRONGPROTOCOL);
+ return;
+ }
+
+ JSONNode &data = root.data();
+ m_szAToken = data["token"]["a"].as_mstring();
+ m_szAToken = ptrA(mir_urlDecode(m_szAToken));
+ setString(DB_KEY_ATOKEN, m_szAToken);
+
+ CMStringA szSessionSecret = data["sessionSecret"].as_mstring();
+ CMStringA szPassTemp = m_szPassword;
+
+ unsigned int len;
+ BYTE hashOut[MIR_SHA256_HASH_SIZE];
+ HMAC(EVP_sha256(), szPassTemp, szPassTemp.GetLength(), (BYTE*)szSessionSecret.c_str(), szSessionSecret.GetLength(), hashOut, &len);
+ m_szSessionKey = ptrA(mir_base64_encode(hashOut, sizeof(hashOut)));
+ setString(DB_KEY_SESSIONKEY, m_szSessionKey);
+
+ CMStringA szUin = data["loginId"].as_mstring();
+ if (szUin)
+ m_dwUin = atoi(szUin);
+
+ StartSession();
+}
+
+void CIcqProto::OnGetUserHistory(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq)
+{
+ RobustReply root(pReply);
+ if (root.error() != 20000)
+ return;
+
+ __int64 lastMsgId = getId(pReq->hContact, DB_KEY_LASTMSGID);
+
+ const JSONNode &results = root.results();
+ for (auto &it : results["messages"])
+ ParseMessage(pReq->hContact, lastMsgId, it);
+
+ setId(pReq->hContact, DB_KEY_LASTMSGID, lastMsgId);
+}
+
+void CIcqProto::OnGetUserInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq)
+{
+ JsonReply root(pReply);
+ if (root.error() != 200) {
+ ProtoBroadcastAck(pReq->hContact, ACKTYPE_GETINFO, ACKRESULT_FAILED, nullptr);
+ return;
+ }
+
+ const JSONNode &data = root.data();
+ for (auto &it : data["users"])
+ ParseBuddyInfo(it, pReq->hContact);
+
+ ProtoBroadcastAck(pReq->hContact, ACKTYPE_GETINFO, ACKRESULT_SUCCESS, nullptr);
+}
+
+void CIcqProto::OnStartSession(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*)
+{
+ JsonReply root(pReply);
+ switch (root.error()) {
+ case 200:
+ break;
+
+ case 401:
+ if (root.detail() == 1002) { // session expired
+ delSetting(DB_KEY_ATOKEN);
+ delSetting(DB_KEY_SESSIONKEY);
+ CheckPassword();
+ }
+ else ConnectionFailed(LOGINERR_WRONGPASSWORD);
+ return;
+
+ default:
+ ConnectionFailed(LOGINERR_WRONGPROTOCOL);
+ return;
+ }
+
+ JSONNode &data = root.data();
+ m_fetchBaseURL = data["fetchBaseURL"].as_mstring();
+ m_aimsid = data["aimsid"].as_mstring();
+
+ OnLoggedIn();
+
+ for (auto &it : data["events"])
+ ProcessEvent(it);
+
+ if (m_hPollThread == nullptr)
+ m_hPollThread = ForkThreadEx(&CIcqProto::PollThread, 0, 0);
+}
+
+void CIcqProto::OnReceiveAvatar(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq)
+{
+ PROTO_AVATAR_INFORMATION ai = {};
+ ai.hContact = pReq->hContact;
+
+ if (pReply->resultCode != 200 || pReply->pData == nullptr) {
+LBL_Error:
+ ProtoBroadcastAck(pReq->hContact, ACKTYPE_AVATAR, ACKRESULT_FAILED, HANDLE(&ai), 0);
+ return;
+ }
+
+ const wchar_t *pwszExtension;
+ ai.format = ProtoGetBufferFormat(pReply->pData, &pwszExtension);
+ setByte(pReq->hContact, "AvatarType", ai.format);
+ GetAvatarFileName(pReq->hContact, ai.filename, _countof(ai.filename));
+
+ FILE *out = _wfopen(ai.filename, L"wb");
+ if (out == nullptr)
+ goto LBL_Error;
+
+ fwrite(pReply->pData, pReply->dataLength, 1, out);
+ fclose(out);
+
+ if (pReq->hContact != 0) {
+ ProtoBroadcastAck(pReq->hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, HANDLE(&ai), 0);
+ debugLogW(L"Broadcast new avatar: %s", ai.filename);
+ }
+ else CallService(MS_AV_REPORTMYAVATARCHANGED, (WPARAM)m_szModuleName, 0);
+}
+
+void CIcqProto::OnSearchResults(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq)
+{
+ RobustReply root(pReply);
+ if (root.error() != 20000) {
+ ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_FAILED, (HANDLE)pReq, 0);
+ return;
+ }
+
+ const JSONNode &results = root.results();
+
+ PROTOSEARCHRESULT psr = {};
+ psr.cbSize = sizeof(psr);
+ psr.flags = PSR_UNICODE;
+ for (auto &it : results["data"]) {
+ const JSONNode &anketa = it["anketa"];
+
+ CMStringW wszId = it["sn"].as_mstring();
+ CMStringW wszNick = anketa["nickname"].as_mstring();
+ CMStringW wszFirst = anketa["firstName"].as_mstring();
+ CMStringW wszLast = anketa["lastName"].as_mstring();
+
+ psr.id.w = wszId.GetBuffer();
+ psr.nick.w = wszNick.GetBuffer();
+ psr.firstName.w = wszFirst.GetBuffer();
+ psr.lastName.w = wszLast.GetBuffer();
+ ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_DATA, (HANDLE)pReq, LPARAM(&psr));
+ }
+
+ ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)pReq);
+}
+
+void CIcqProto::OnSendMessage(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq)
+{
+ IcqOwnMessage *ownMsg = (IcqOwnMessage*)pReq->pUserInfo;
+
+ JsonReply root(pReply);
+ if (root.error() != 200) {
+ ProtoBroadcastAck(ownMsg->m_hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, (HANDLE)ownMsg->m_msgid, 0);
+ for (auto &it : m_arOwnIds) {
+ if (it == ownMsg) {
+ m_arOwnIds.remove(m_arOwnIds.indexOf(&it));
+ break;
+ }
+ }
+ }
+
+ const JSONNode &data = root.data();
+ CMStringA reqId(root.requestId());
+ CMStringA msgId(data["histMsgId"].as_mstring());
+ CheckOwnMessage(reqId, msgId, false);
+ CheckLastId(ownMsg->m_hContact, data);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+void CIcqProto::ProcessBuddyList(const JSONNode &ev)
+{
+ bool bEnableMenu = false;
+
+ for (auto &it : ev["groups"]) {
+ CMStringW szGroup = it["name"].as_mstring();
+ bool bCreated = false;
+
+ for (auto &buddy : it["buddies"]) {
+ MCONTACT hContact = ParseBuddyInfo(buddy);
+ if (hContact == INVALID_CONTACT_ID)
+ continue;
+
+ setWString(hContact, "IcqGroup", szGroup);
+
+ CMStringW mirGroup(db_get_sm(hContact, "CList", "Group"));
+ if (mirGroup != szGroup)
+ bEnableMenu = true;
+
+ if (mirGroup.IsEmpty()) {
+ if (!bCreated) {
+ Clist_GroupCreate(0, szGroup);
+ bCreated = true;
+ }
+
+ db_set_ws(hContact, "CList", "Group", szGroup);
+ }
+ }
+ }
+
+ if (bEnableMenu)
+ Menu_ShowItem(m_hUploadGroups, true);
+
+ for (auto &it : m_arCache)
+ if (!it->m_bInList)
+ db_set_b(it->m_hContact, "CList", "NotOnList", 1);
+}
+
+void CIcqProto::ProcessDiff(const JSONNode &ev)
+{
+ for (auto &block : ev) {
+ CMStringW szType = block["type"].as_mstring();
+ if (szType != "updated" && szType != "created")
+ continue;
+
+ for (auto &it : block["data"]) {
+ CMStringW szGroup = it["name"].as_mstring();
+ bool bCreated = false;
+
+ for (auto &buddy : it["buddies"]) {
+ MCONTACT hContact = ParseBuddyInfo(buddy);
+ if (hContact == INVALID_CONTACT_ID)
+ continue;
+
+ setWString(hContact, "IcqGroup", szGroup);
+
+ if (db_get_sm(hContact, "CList", "Group").IsEmpty()) {
+ if (!bCreated) {
+ Clist_GroupCreate(0, szGroup);
+ bCreated = true;
+ }
+
+ db_set_ws(hContact, "CList", "Group", szGroup);
+ }
+ }
+ }
+ }
+}
+
+void CIcqProto::ProcessEvent(const JSONNode &ev)
+{
+ const JSONNode &pData = ev["eventData"];
+ CMStringW szType = ev["type"].as_mstring();
+ if (szType == L"buddylist")
+ ProcessBuddyList(pData);
+ else if (szType == L"diff")
+ ProcessDiff(pData);
+ else if (szType == L"histDlgState")
+ ProcessHistData(pData);
+ else if (szType == L"imState")
+ ProcessImState(pData);
+ else if (szType == L"mchat")
+ ProcessGroupChat(pData);
+ else if (szType == L"myInfo")
+ ProcessMyInfo(pData);
+ else if (szType == L"presence")
+ ProcessPresence(pData);
+ else if (szType == L"typing")
+ ProcessTyping(pData);
+}
+
+void CIcqProto::ProcessHistData(const JSONNode &ev)
+{
+ MCONTACT hContact;
+
+ CMStringW wszId(ev["sn"].as_mstring());
+ if (IsChat(wszId)) {
+ SESSION_INFO *si = g_chatApi.SM_FindSession(wszId, m_szModuleName);
+ if (si == nullptr)
+ return;
+
+ hContact = si->hContact;
+
+ if (si->arUsers.getCount() == 0) {
+ __int64 srvInfoVer = _wtoi64(ev["mchatState"]["infoVersion"].as_mstring());
+ __int64 srvMembersVer = _wtoi64(ev["mchatState"]["membersVersion"].as_mstring());
+ if (srvInfoVer != getId(hContact, "InfoVersion") || srvMembersVer != getId(hContact, "MembersVersion")) {
+ auto *pReq = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER, &CIcqProto::OnGetChatInfo);
+ JSONNode request, params; params.set_name("params");
+ params << WCHAR_PARAM("sn", wszId) << INT_PARAM("memberLimit", 100) << CHAR_PARAM("aimSid", m_aimsid);
+ request << CHAR_PARAM("method", "getChatInfo") << CHAR_PARAM("reqId", pReq->m_reqId) << CHAR_PARAM("authToken", m_szRToken) << INT_PARAM("clientId", m_iRClientId) << params;
+ pReq->m_szParam = ptrW(json_write(&request));
+ pReq->pUserInfo = si;
+ Push(pReq);
+ }
+ else LoadChatInfo(si);
+ }
+ }
+ else hContact = CreateContact(_wtol(wszId), true);
+
+ __int64 lastMsgId = getId(hContact, DB_KEY_LASTMSGID);
+ __int64 srvLastId = _wtoi64(ev["lastMsgId"].as_mstring());
+ __int64 srvUnreadId = _wtoi64(ev["yours"]["lastRead"].as_mstring());
+
+ // on first start we don't load history not to create dups
+ if (lastMsgId == 0)
+ setId(hContact, DB_KEY_LASTMSGID, srvLastId);
+ // or load missing messages if any
+ else if (ev["unreadCnt"].as_int() > 0)
+ RetrieveUserHistory(hContact, min(srvUnreadId, lastMsgId), srvLastId);
+
+ for (auto &it : ev["tail"]["messages"])
+ ParseMessage(hContact, lastMsgId, it);
+ setId(hContact, DB_KEY_LASTMSGID, lastMsgId);
+}
+
+void CIcqProto::ProcessImState(const JSONNode &ev)
+{
+ for (auto &it : ev["imStates"]) {
+ if (it["state"].as_mstring() != L"sent")
+ continue;
+
+ CMStringA reqId(it["sendReqId"].as_mstring());
+ CMStringA msgId(it["histMsgId"].as_mstring());
+ MCONTACT hContact = CheckOwnMessage(reqId, msgId, false);
+ if (hContact)
+ CheckLastId(hContact, ev);
+ }
+}
+
+void CIcqProto::ProcessMyInfo(const JSONNode &ev)
+{
+ CheckNickChange(0, ev);
+ CheckAvatarChange(0, ev);
+}
+
+void CIcqProto::ProcessPresence(const JSONNode &ev)
+{
+ DWORD dwUin = _wtol(ev["aimId"].as_mstring());
+
+ IcqCacheItem *pCache = FindContactByUIN(dwUin);
+ if (pCache) {
+ setDword(pCache->m_hContact, "Status", StatusFromString(ev["state"].as_mstring()));
+
+ CheckNickChange(pCache->m_hContact, ev);
+ CheckAvatarChange(pCache->m_hContact, ev);
+ }
+}
+
+void CIcqProto::ProcessTyping(const JSONNode &ev)
+{
+ DWORD dwUin = _wtol(ev["aimId"].as_mstring());
+ CMStringW wszStatus = ev["typingStatus"].as_mstring();
+
+ IcqCacheItem *pCache = FindContactByUIN(dwUin);
+ if (pCache) {
+ if (wszStatus == "typing")
+ CallService(MS_PROTO_CONTACTISTYPING, pCache->m_hContact, 60);
+ else
+ CallService(MS_PROTO_CONTACTISTYPING, pCache->m_hContact, PROTOTYPE_CONTACTTYPING_OFF);
+ }
+}
+
+void CIcqProto::OnFetchEvents(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*)
+{
+ JsonReply root(pReply);
+ if (root.error() != 200) {
+ ShutdownSession();
+ return;
+ }
+
+ JSONNode &data = root.data();
+ m_fetchBaseURL = data["fetchBaseURL"].as_mstring();
+
+ for (auto &it : data["events"])
+ ProcessEvent(it);
+}
+
+void __cdecl CIcqProto::PollThread(void*)
+{
+ debugLogA("Polling thread started");
+ bool bFirst = true;
+
+ while (m_bOnline) {
+ CMStringA szUrl = m_fetchBaseURL;
+ if (bFirst) {
+ bFirst = false;
+ szUrl.Append("&first=1");
+ }
+ else szUrl.Append("&timeout=25000");
+
+ auto *pReq = new AsyncHttpRequest(CONN_FETCH, REQUEST_GET, szUrl, &CIcqProto::OnFetchEvents);
+ if (!bFirst)
+ pReq->timeout = 62000;
+ ExecuteRequest(pReq);
+ }
+
+ debugLogA("Polling thread ended");
+ m_hPollThread = nullptr;
+}
diff --git a/protocols/ICQ-WIM/src/stdafx.cxx b/protocols/ICQ-WIM/src/stdafx.cxx
new file mode 100644
index 0000000000..66afad80f1
--- /dev/null
+++ b/protocols/ICQ-WIM/src/stdafx.cxx
@@ -0,0 +1,18 @@
+/*
+Copyright (C) 2012-19 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
new file mode 100644
index 0000000000..929854990c
--- /dev/null
+++ b/protocols/ICQ-WIM/src/stdafx.h
@@ -0,0 +1,95 @@
+// ---------------------------------------------------------------------------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-2019 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_clist.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_netlib.h>
+#include <m_protocols.h>
+#include <m_protosvc.h>
+#include <m_options.h>
+#include <m_popup.h>
+#include <m_skin.h>
+#include <m_system.h>
+#include <m_timezones.h>
+#include <m_userinfo.h>
+#include <m_utils.h>
+#include <win2k.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_UIN "UIN"
+#define DB_KEY_ATOKEN "AToken"
+#define DB_KEY_RCLIENTID "RClientID"
+#define DB_KEY_LASTMSGID "LastMsgId"
+#define DB_KEY_SESSIONKEY "SessionKey"
+
+#include "http.h"
+#include "proto.h"
+
+bool IsChat(const CMStringW &aimid);
+
+int StatusFromString(const CMStringW&);
+
+extern HWND g_hwndHeartbeat; \ No newline at end of file
diff --git a/protocols/ICQ-WIM/src/utils.cpp b/protocols/ICQ-WIM/src/utils.cpp
new file mode 100644
index 0000000000..6cf6219322
--- /dev/null
+++ b/protocols/ICQ-WIM/src/utils.cpp
@@ -0,0 +1,239 @@
+// -----------------------------------------------------------------------------
+// ICQ plugin for Miranda NG
+// -----------------------------------------------------------------------------
+// Copyright © 2018-19 Miranda NG team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+// -----------------------------------------------------------------------------
+
+#include "stdafx.h"
+
+void CIcqProto::InitContactCache()
+{
+ mir_cslock l(m_csCache);
+ for (auto &it : AccContacts())
+ if (!isChatRoom(it))
+ m_arCache.insert(new IcqCacheItem(getDword(it, DB_KEY_UIN), it));
+}
+
+IcqCacheItem* CIcqProto::FindContactByUIN(DWORD dwUin)
+{
+ mir_cslock l(m_csCache);
+ return m_arCache.find((IcqCacheItem*)&dwUin);
+}
+
+MCONTACT CIcqProto::CreateContact(DWORD dwUin, bool bTemporary)
+{
+ auto *pCache = FindContactByUIN(dwUin);
+ if (pCache != nullptr)
+ return pCache->m_hContact;
+
+ MCONTACT hContact = db_add_contact();
+ Proto_AddToContact(hContact, m_szModuleName);
+ setDword(hContact, DB_KEY_UIN, dwUin);
+ pCache = new IcqCacheItem(dwUin, hContact);
+ {
+ mir_cslock l(m_csCache);
+ m_arCache.insert(pCache);
+ }
+ RetrieveUserInfo(hContact);
+
+ if (bTemporary)
+ db_set_b(hContact, "CList", "NotOnList", 1);
+
+ return hContact;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+void CIcqProto::CalcHash(AsyncHttpRequest *pReq)
+{
+ CMStringA hashData(FORMAT, "POST&%s&%s", ptrA(mir_urlEncode(pReq->m_szUrl)), ptrA(mir_urlEncode(pReq->m_szParam)));
+ unsigned int len;
+ BYTE hashOut[MIR_SHA256_HASH_SIZE];
+ HMAC(EVP_sha256(), m_szSessionKey, m_szSessionKey.GetLength(), (BYTE*)hashData.c_str(), hashData.GetLength(), hashOut, &len);
+ pReq << CHAR_PARAM("sig_sha256", ptrA(mir_base64_encode(hashOut, sizeof(hashOut))));
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// 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%"), m_szModuleName);
+
+ DWORD dwAttributes = GetFileAttributes(pszDest);
+ if (dwAttributes == 0xffffffff || (dwAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0)
+ 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", ptrA(mir_urlEncode(m_aimsid.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", _T2A(ProtoGetAvatarMimeType(iAvatarType)));
+ }
+ Push(pReq);
+
+ return 0; // TODO
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+CMStringA CIcqProto::GetUserId(MCONTACT hContact)
+{
+ if (isChatRoom(hContact))
+ return getMStringA(hContact, "ChatRoomID");
+
+ return CMStringA(FORMAT, "%d", getDword(hContact, DB_KEY_UIN));
+}
+
+bool IsChat(const CMStringW &aimid)
+{
+ return aimid.Right(11) == "@chat.agent";
+}
+
+int StatusFromString(const CMStringW &wszStatus)
+{
+ if (wszStatus == "online")
+ return ID_STATUS_ONLINE;
+ if (wszStatus == "n/a")
+ return ID_STATUS_NA;
+ if (wszStatus == "away")
+ return ID_STATUS_AWAY;
+ if (wszStatus == "occupied")
+ return ID_STATUS_OCCUPIED;
+ if (wszStatus == "dnd")
+ return ID_STATUS_DND;
+
+ return ID_STATUS_OFFLINE;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+__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));
+}
diff --git a/protocols/ICQ-WIM/src/version.h b/protocols/ICQ-WIM/src/version.h
new file mode 100644
index 0000000000..3c290b0655
--- /dev/null
+++ b/protocols/ICQ-WIM/src/version.h
@@ -0,0 +1,13 @@
+#define __MAJOR_VERSION 0
+#define __MINOR_VERSION 95
+#define __RELEASE_NUM 10
+#define __BUILD_NUM 1
+
+#include <stdver.h>
+
+#define __PLUGIN_NAME "ICQ-WIM protocol"
+#define __FILENAME "ICQ.dll"
+#define __DESCRIPTION "ICQ protocol support for Miranda NG."
+#define __AUTHOR "George Hazan"
+#define __AUTHORWEB "https://miranda-ng.org/p/ICQ/"
+#define __COPYRIGHT "© 2018-19 George Hazan"