diff options
author | George Hazan <ghazan@miranda.im> | 2019-01-16 18:06:52 +0300 |
---|---|---|
committer | George Hazan <ghazan@miranda.im> | 2019-01-16 18:06:52 +0300 |
commit | aa514a9f95c8cd95c01952250f7cdb670a66af9a (patch) | |
tree | ecb84e10fc3af008e1a8ac9027f1b526634c8d65 /protocols/ICQ-WIM | |
parent | 4f0a4ae4724fdfe65f63ee41dc1790ccbb0668f7 (diff) |
fixes #1761 (Rename Icq10 folder and ICQ/2018 in version.h to ICQ-WIM)
Diffstat (limited to 'protocols/ICQ-WIM')
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 Binary files differnew file mode 100644 index 0000000000..248a3e9916 --- /dev/null +++ b/protocols/ICQ-WIM/proto_icq/res/Away.ico diff --git a/protocols/ICQ-WIM/proto_icq/res/DND.ico b/protocols/ICQ-WIM/proto_icq/res/DND.ico Binary files differnew file mode 100644 index 0000000000..4833160eac --- /dev/null +++ b/protocols/ICQ-WIM/proto_icq/res/DND.ico diff --git a/protocols/ICQ-WIM/proto_icq/res/FFC.ico b/protocols/ICQ-WIM/proto_icq/res/FFC.ico Binary files differnew file mode 100644 index 0000000000..e7ec4d3ae2 --- /dev/null +++ b/protocols/ICQ-WIM/proto_icq/res/FFC.ico diff --git a/protocols/ICQ-WIM/proto_icq/res/Invisible.ico b/protocols/ICQ-WIM/proto_icq/res/Invisible.ico Binary files differnew file mode 100644 index 0000000000..6a337c2926 --- /dev/null +++ b/protocols/ICQ-WIM/proto_icq/res/Invisible.ico diff --git a/protocols/ICQ-WIM/proto_icq/res/NA.ico b/protocols/ICQ-WIM/proto_icq/res/NA.ico Binary files differnew file mode 100644 index 0000000000..ec0621dc9f --- /dev/null +++ b/protocols/ICQ-WIM/proto_icq/res/NA.ico diff --git a/protocols/ICQ-WIM/proto_icq/res/Occupied.ico b/protocols/ICQ-WIM/proto_icq/res/Occupied.ico Binary files differnew file mode 100644 index 0000000000..04ea2a5855 --- /dev/null +++ b/protocols/ICQ-WIM/proto_icq/res/Occupied.ico diff --git a/protocols/ICQ-WIM/proto_icq/res/Offline.ico b/protocols/ICQ-WIM/proto_icq/res/Offline.ico Binary files differnew file mode 100644 index 0000000000..af862168cd --- /dev/null +++ b/protocols/ICQ-WIM/proto_icq/res/Offline.ico diff --git a/protocols/ICQ-WIM/proto_icq/res/Online.ico b/protocols/ICQ-WIM/proto_icq/res/Online.ico Binary files differnew file mode 100644 index 0000000000..2e33305a76 --- /dev/null +++ b/protocols/ICQ-WIM/proto_icq/res/Online.ico diff --git a/protocols/ICQ-WIM/proto_icq/res/Phone.ico b/protocols/ICQ-WIM/proto_icq/res/Phone.ico Binary files differnew file mode 100644 index 0000000000..74c80b66ed --- /dev/null +++ b/protocols/ICQ-WIM/proto_icq/res/Phone.ico 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" |