From de40f3be3f08487937525c2ef096dad665dda61d Mon Sep 17 00:00:00 2001 From: dartraiden Date: Sat, 14 Jan 2023 01:30:59 +0300 Subject: Convert sources to CR+LF --- protocols/Discord/CMakeLists.txt | 8 +- protocols/Discord/discord.vcxproj | 130 +- protocols/Discord/discord.vcxproj.filters | 144 +- protocols/Discord/proto_discord/CMakeLists.txt | 2 +- .../Discord/proto_discord/Proto_Discord.vcxproj | 66 +- .../proto_discord/Proto_Discord.vcxproj.filters | 26 +- .../Discord/proto_discord/res/Proto_Discord.rc | 148 +- protocols/Discord/proto_discord/src/resource.h | 46 +- protocols/Discord/res/discord.rc | 318 ++-- protocols/Discord/res/version.rc | 18 +- protocols/Discord/src/avatars.cpp | 410 +++--- protocols/Discord/src/connection.cpp | 246 ++-- protocols/Discord/src/dispatch.cpp | 1184 +++++++-------- protocols/Discord/src/gateway.cpp | 692 ++++----- protocols/Discord/src/groupchat.cpp | 470 +++--- protocols/Discord/src/guilds.cpp | 826 +++++------ protocols/Discord/src/http.cpp | 310 ++-- protocols/Discord/src/main.cpp | 142 +- protocols/Discord/src/menus.cpp | 344 ++--- protocols/Discord/src/options.cpp | 200 +-- protocols/Discord/src/proto.cpp | 1536 ++++++++++---------- protocols/Discord/src/proto.h | 952 ++++++------ protocols/Discord/src/resource.h | 60 +- protocols/Discord/src/server.cpp | 614 ++++---- protocols/Discord/src/stdafx.cxx | 34 +- protocols/Discord/src/stdafx.h | 160 +- protocols/Discord/src/utils.cpp | 752 +++++----- protocols/Discord/src/version.h | 26 +- protocols/Discord/src/voice.cpp | 232 +-- 29 files changed, 5048 insertions(+), 5048 deletions(-) (limited to 'protocols/Discord') diff --git a/protocols/Discord/CMakeLists.txt b/protocols/Discord/CMakeLists.txt index a227eff6df..d0502167ed 100644 --- a/protocols/Discord/CMakeLists.txt +++ b/protocols/Discord/CMakeLists.txt @@ -1,5 +1,5 @@ -file(GLOB SOURCES "src/*.h" "src/*.cpp" "res/*.rc") -set(TARGET Discord) -include(${CMAKE_SOURCE_DIR}/cmake/plugin.cmake) -target_link_libraries(${TARGET} Zlib libjson) +file(GLOB SOURCES "src/*.h" "src/*.cpp" "res/*.rc") +set(TARGET Discord) +include(${CMAKE_SOURCE_DIR}/cmake/plugin.cmake) +target_link_libraries(${TARGET} Zlib libjson) add_subdirectory(proto_discord) \ No newline at end of file diff --git a/protocols/Discord/discord.vcxproj b/protocols/Discord/discord.vcxproj index ac4c73bd0f..e2f19b6be9 100644 --- a/protocols/Discord/discord.vcxproj +++ b/protocols/Discord/discord.vcxproj @@ -1,66 +1,66 @@ - - - - - Debug - Win32 - - - Debug - x64 - - - Release - Win32 - - - Release - x64 - - - - {88928401-2CE8-4568-AAA7-226141870CBF} - Discord - - - - - - - - - - - - - - - - - - - Create - - - - - - - - - - - {01F9E227-06F5-4BED-907F-402CA7DFAFE6} - false - - - - - {f6a9340e-b8d9-4c75-be30-47dc66d0abc7} - - - - - - + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {88928401-2CE8-4568-AAA7-226141870CBF} + Discord + + + + + + + + + + + + + + + + + + + Create + + + + + + + + + + + {01F9E227-06F5-4BED-907F-402CA7DFAFE6} + false + + + + + {f6a9340e-b8d9-4c75-be30-47dc66d0abc7} + + + + + + \ No newline at end of file diff --git a/protocols/Discord/discord.vcxproj.filters b/protocols/Discord/discord.vcxproj.filters index 18314b26b0..f8b955739b 100644 --- a/protocols/Discord/discord.vcxproj.filters +++ b/protocols/Discord/discord.vcxproj.filters @@ -1,73 +1,73 @@ - - - - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - - - Resource Files - - - Resource Files - - + + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Resource Files + + + Resource Files + + \ No newline at end of file diff --git a/protocols/Discord/proto_discord/CMakeLists.txt b/protocols/Discord/proto_discord/CMakeLists.txt index 5ea6891fa1..48da532df6 100644 --- a/protocols/Discord/proto_discord/CMakeLists.txt +++ b/protocols/Discord/proto_discord/CMakeLists.txt @@ -1,2 +1,2 @@ -set(TARGET Proto_Discord) +set(TARGET Proto_Discord) include(${CMAKE_SOURCE_DIR}/cmake/icons.cmake) \ No newline at end of file diff --git a/protocols/Discord/proto_discord/Proto_Discord.vcxproj b/protocols/Discord/proto_discord/Proto_Discord.vcxproj index 8ce8962a22..a17e91b938 100644 --- a/protocols/Discord/proto_discord/Proto_Discord.vcxproj +++ b/protocols/Discord/proto_discord/Proto_Discord.vcxproj @@ -1,34 +1,34 @@ - - - - - Debug - Win32 - - - Debug - x64 - - - Release - Win32 - - - Release - x64 - - - - Proto_Discord - {6B8BA5EE-3815-44A6-A13B-2A22E8B3A311} - - - - - - - - - - + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + Proto_Discord + {6B8BA5EE-3815-44A6-A13B-2A22E8B3A311} + + + + + + + + + + \ No newline at end of file diff --git a/protocols/Discord/proto_discord/Proto_Discord.vcxproj.filters b/protocols/Discord/proto_discord/Proto_Discord.vcxproj.filters index 3f512b9b20..a86aceb510 100644 --- a/protocols/Discord/proto_discord/Proto_Discord.vcxproj.filters +++ b/protocols/Discord/proto_discord/Proto_Discord.vcxproj.filters @@ -1,14 +1,14 @@ - - - - - - Header Files - - - - - Resource Files - - + + + + + + Header Files + + + + + Resource Files + + \ No newline at end of file diff --git a/protocols/Discord/proto_discord/res/Proto_Discord.rc b/protocols/Discord/proto_discord/res/Proto_Discord.rc index 13d3153e3e..fb320d064b 100644 --- a/protocols/Discord/proto_discord/res/Proto_Discord.rc +++ b/protocols/Discord/proto_discord/res/Proto_Discord.rc @@ -1,74 +1,74 @@ -// 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" -#endif // Russian (Russia) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED - +// 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" +#endif // Russian (Russia) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/protocols/Discord/proto_discord/src/resource.h b/protocols/Discord/proto_discord/src/resource.h index 70e0dd0372..1a283a2809 100644 --- a/protocols/Discord/proto_discord/src/resource.h +++ b/protocols/Discord/proto_discord/src/resource.h @@ -1,23 +1,23 @@ -//{{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 - -// 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 +//{{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 + +// 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/Discord/res/discord.rc b/protocols/Discord/res/discord.rc index 6fac650624..780cae5613 100644 --- a/protocols/Discord/res/discord.rc +++ b/protocols/Discord/res/discord.rc @@ -1,159 +1,159 @@ -// 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 - -///////////////////////////////////////////////////////////////////////////// -// English (United States) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US -#pragma code_page(1252) - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "..\\src\\resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""winres.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_MAIN ICON "discord.ico" - -IDI_GROUPCHAT ICON "groupchat.ico" - -IDI_VOICE_CALL ICON "voiceCall.ico" - -IDI_VOICE_ENDED ICON "voiceEnded.ico" - - -///////////////////////////////////////////////////////////////////////////// -// -// Dialog -// - -IDD_OPTIONS_ACCOUNT DIALOGEX 0, 0, 305, 144 -STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD -EXSTYLE WS_EX_CONTROLPARENT -FONT 8, "MS Shell Dlg", 0, 0, 0x1 -BEGIN - GROUPBOX "User details",IDC_STATIC,7,7,291,46 - LTEXT "E-mail:",IDC_STATIC,17,20,61,8,0,WS_EX_RIGHT - EDITTEXT IDC_USERNAME,84,18,123,13,ES_AUTOHSCROLL - LTEXT "Password:",IDC_STATIC,17,36,61,8,0,WS_EX_RIGHT - EDITTEXT IDC_PASSWORD,84,34,123,13,ES_PASSWORD | ES_AUTOHSCROLL - GROUPBOX "Contacts",IDC_STATIC,7,54,291,86 - LTEXT "Default group:",IDC_STATIC,17,73,61,8,0,WS_EX_RIGHT - EDITTEXT IDC_GROUP,84,71,123,13,ES_AUTOHSCROLL - CONTROL "Enable guilds (servers)",IDC_USEGUILDS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,14,90,275,10 - CONTROL "Do not open chat windows on creation",IDC_HIDECHATS, - "Button",BS_AUTOCHECKBOX | WS_TABSTOP,23,102,248,10 - CONTROL "Use subgroups for server channels (requires restart)",IDC_USEGROUPS, - "Button",BS_AUTOCHECKBOX | WS_TABSTOP,23,114,248,10 - CONTROL "Delete messages in Miranda when they are deleted from server",IDC_DELETE_MSGS, - "Button",BS_AUTOCHECKBOX | WS_TABSTOP,14,126,275,10 -END - -IDD_OPTIONS_ACCMGR DIALOGEX 0, 0, 200, 88 -STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD -EXSTYLE WS_EX_CONTROLPARENT -FONT 8, "MS Shell Dlg", 0, 0, 0x1 -BEGIN - GROUPBOX "User details",IDC_STATIC,7,7,178,46 - LTEXT "E-mail:",IDC_STATIC,17,20,69,8,0,WS_EX_RIGHT - EDITTEXT IDC_USERNAME,92,18,86,13,ES_AUTOHSCROLL - LTEXT "Password:",IDC_STATIC,17,36,69,8,0,WS_EX_RIGHT - EDITTEXT IDC_PASSWORD,92,34,86,13,ES_PASSWORD | ES_AUTOHSCROLL - GROUPBOX "Contacts",IDC_STATIC,7,56,178,28 - LTEXT "Default group:",IDC_STATIC,17,67,69,8,0,WS_EX_RIGHT - EDITTEXT IDC_GROUP,92,65,86,13,ES_AUTOHSCROLL -END - -IDD_EXTSEARCH DIALOGEX 0, 0, 114, 55 -STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU -EXSTYLE WS_EX_TRANSPARENT | WS_EX_CONTROLPARENT -FONT 8, "MS Shell Dlg", 400, 0, 0x1 -BEGIN - LTEXT "Nick:",IDC_STATIC,6,7,99,8 - EDITTEXT IDC_NICK,3,18,103,12,0,WS_EX_CLIENTEDGE -END - - -///////////////////////////////////////////////////////////////////////////// -// -// DESIGNINFO -// - -#ifdef APSTUDIO_INVOKED -GUIDELINES DESIGNINFO -BEGIN - IDD_OPTIONS_ACCOUNT, DIALOG - BEGIN - END - - IDD_OPTIONS_ACCMGR, DIALOG - BEGIN - END -END -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// AFX_DIALOG_LAYOUT -// - -IDD_OPTIONS_ACCOUNT AFX_DIALOG_LAYOUT -BEGIN - 0 -END - -#endif // English (United States) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED - +// 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 + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "..\\src\\resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.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_MAIN ICON "discord.ico" + +IDI_GROUPCHAT ICON "groupchat.ico" + +IDI_VOICE_CALL ICON "voiceCall.ico" + +IDI_VOICE_ENDED ICON "voiceEnded.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_OPTIONS_ACCOUNT DIALOGEX 0, 0, 305, 144 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD +EXSTYLE WS_EX_CONTROLPARENT +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + GROUPBOX "User details",IDC_STATIC,7,7,291,46 + LTEXT "E-mail:",IDC_STATIC,17,20,61,8,0,WS_EX_RIGHT + EDITTEXT IDC_USERNAME,84,18,123,13,ES_AUTOHSCROLL + LTEXT "Password:",IDC_STATIC,17,36,61,8,0,WS_EX_RIGHT + EDITTEXT IDC_PASSWORD,84,34,123,13,ES_PASSWORD | ES_AUTOHSCROLL + GROUPBOX "Contacts",IDC_STATIC,7,54,291,86 + LTEXT "Default group:",IDC_STATIC,17,73,61,8,0,WS_EX_RIGHT + EDITTEXT IDC_GROUP,84,71,123,13,ES_AUTOHSCROLL + CONTROL "Enable guilds (servers)",IDC_USEGUILDS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,14,90,275,10 + CONTROL "Do not open chat windows on creation",IDC_HIDECHATS, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,23,102,248,10 + CONTROL "Use subgroups for server channels (requires restart)",IDC_USEGROUPS, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,23,114,248,10 + CONTROL "Delete messages in Miranda when they are deleted from server",IDC_DELETE_MSGS, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,14,126,275,10 +END + +IDD_OPTIONS_ACCMGR DIALOGEX 0, 0, 200, 88 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD +EXSTYLE WS_EX_CONTROLPARENT +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + GROUPBOX "User details",IDC_STATIC,7,7,178,46 + LTEXT "E-mail:",IDC_STATIC,17,20,69,8,0,WS_EX_RIGHT + EDITTEXT IDC_USERNAME,92,18,86,13,ES_AUTOHSCROLL + LTEXT "Password:",IDC_STATIC,17,36,69,8,0,WS_EX_RIGHT + EDITTEXT IDC_PASSWORD,92,34,86,13,ES_PASSWORD | ES_AUTOHSCROLL + GROUPBOX "Contacts",IDC_STATIC,7,56,178,28 + LTEXT "Default group:",IDC_STATIC,17,67,69,8,0,WS_EX_RIGHT + EDITTEXT IDC_GROUP,92,65,86,13,ES_AUTOHSCROLL +END + +IDD_EXTSEARCH DIALOGEX 0, 0, 114, 55 +STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU +EXSTYLE WS_EX_TRANSPARENT | WS_EX_CONTROLPARENT +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "Nick:",IDC_STATIC,6,7,99,8 + EDITTEXT IDC_NICK,3,18,103,12,0,WS_EX_CLIENTEDGE +END + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN + IDD_OPTIONS_ACCOUNT, DIALOG + BEGIN + END + + IDD_OPTIONS_ACCMGR, DIALOG + BEGIN + END +END +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// AFX_DIALOG_LAYOUT +// + +IDD_OPTIONS_ACCOUNT AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/protocols/Discord/res/version.rc b/protocols/Discord/res/version.rc index 5a5ddd63ed..bd3c22d943 100644 --- a/protocols/Discord/res/version.rc +++ b/protocols/Discord/res/version.rc @@ -1,9 +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" +// 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/Discord/src/avatars.cpp b/protocols/Discord/src/avatars.cpp index aef0a76e48..fc49a7ec1a 100644 --- a/protocols/Discord/src/avatars.cpp +++ b/protocols/Discord/src/avatars.cpp @@ -1,205 +1,205 @@ -/* -Copyright © 2016-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -CMStringW CDiscordProto::GetAvatarFilename(MCONTACT hContact) -{ - CMStringW wszResult(FORMAT, L"%s\\%S", VARSW(L"%miranda_avatarcache%"), m_szModuleName); - CreateDirectoryTreeW(wszResult); - - wszResult.AppendChar('\\'); - - const wchar_t* szFileType = ProtoGetAvatarExtension(getByte(hContact, "AvatarType", PA_FORMAT_PNG)); - wszResult.AppendFormat(L"%lld%s", getId(hContact, DB_KEY_ID), szFileType); - return wszResult; -} - -INT_PTR CDiscordProto::GetAvatarCaps(WPARAM wParam, LPARAM lParam) -{ - int res = 0; - - switch (wParam) { - case AF_MAXSIZE: - ((POINT*)lParam)->x = ((POINT*)lParam)->y = 128; - break; - - case AF_FORMATSUPPORTED: - res = lParam == PA_FORMAT_PNG || lParam == PA_FORMAT_GIF || lParam == PA_FORMAT_JPEG; - break; - - case AF_ENABLED: - case AF_DONTNEEDDELAYS: - case AF_FETCHIFPROTONOTVISIBLE: - case AF_FETCHIFCONTACTOFFLINE: - return 1; - } - - return res; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CDiscordProto::OnReceiveAvatar(NETLIBHTTPREQUEST *reply, AsyncHttpRequest *pReq) -{ - PROTO_AVATAR_INFORMATION ai = { 0 }; - ai.format = PA_FORMAT_UNKNOWN; - ai.hContact = (UINT_PTR)pReq->pUserInfo; - - if (reply->resultCode != 200) { -LBL_Error: - ProtoBroadcastAck(ai.hContact, ACKTYPE_AVATAR, ACKRESULT_FAILED, (HANDLE)&ai); - return; - } - - if (auto *pszHdr = Netlib_GetHeader(reply, "Content-Type")) - ai.format = ProtoGetAvatarFormatByMimeType(pszHdr); - - if (ai.format == PA_FORMAT_UNKNOWN) { - debugLogA("unknown avatar mime type"); - goto LBL_Error; - } - - setByte(ai.hContact, "AvatarType", ai.format); - mir_wstrncpy(ai.filename, GetAvatarFilename(ai.hContact), _countof(ai.filename)); - - FILE *out = _wfopen(ai.filename, L"wb"); - if (out == nullptr) { - debugLogA("cannot open avatar file %S for writing", ai.filename); - goto LBL_Error; - } - - fwrite(reply->pData, 1, reply->dataLength, out); - fclose(out); - - if (ai.hContact) - ProtoBroadcastAck(ai.hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, (HANDLE)&ai); - else - ReportSelfAvatarChanged(); -} - -bool CDiscordProto::RetrieveAvatar(MCONTACT hContact) -{ - ptrA szAvatarHash(getStringA(hContact, DB_KEY_AVHASH)); - SnowFlake id = getId(hContact, DB_KEY_ID); - if (id == 0 || szAvatarHash == nullptr) - return false; - - CMStringA szUrl(FORMAT, "https://cdn.discordapp.com/avatars/%lld/%s.jpg", id, szAvatarHash.get()); - AsyncHttpRequest *pReq = new AsyncHttpRequest(this, REQUEST_GET, szUrl, &CDiscordProto::OnReceiveAvatar); - pReq->pUserInfo = (void*)hContact; - Push(pReq); - return true; -} - -INT_PTR CDiscordProto::GetAvatarInfo(WPARAM flags, LPARAM lParam) -{ - PROTO_AVATAR_INFORMATION *pai = (PROTO_AVATAR_INFORMATION *)lParam; - - CMStringW wszFileName(GetAvatarFilename(pai->hContact)); - if (!wszFileName.IsEmpty()) { - mir_wstrncpy(pai->filename, wszFileName, _countof(pai->filename)); - - bool bFileExist = _waccess(wszFileName, 0) == 0; - - // if we still need to load an avatar - if ((flags & GAIF_FORCE) || !bFileExist) { - if (RetrieveAvatar(pai->hContact)) - return GAIR_WAITFOR; - } - else if (bFileExist) - return GAIR_SUCCESS; - } - - return GAIR_NOAVATAR; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -INT_PTR CDiscordProto::GetMyAvatar(WPARAM wParam, LPARAM lParam) -{ - if (!wParam || !lParam) - return -3; - - wchar_t* buf = (wchar_t*)wParam; - int size = (int)lParam; - - PROTO_AVATAR_INFORMATION ai = {}; - switch (GetAvatarInfo(0, (LPARAM)&ai)) { - case GAIR_SUCCESS: - wcsncpy_s(buf, size, ai.filename, _TRUNCATE); - return 0; - - case GAIR_WAITFOR: - return -1; - } - - return -2; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -INT_PTR CDiscordProto::SetMyAvatar(WPARAM, LPARAM lParam) -{ - CMStringW wszFileName(GetAvatarFilename(0)); - - const wchar_t *pwszFilename = (const wchar_t*)lParam; - if (pwszFilename == nullptr) { // remove my avatar file - delSetting(DB_KEY_AVHASH); - DeleteFile(wszFileName); - } - - CMStringA szPayload("data:"); - - const char *szMimeType = ProtoGetAvatarMimeType(ProtoGetAvatarFileFormat(pwszFilename)); - if (szMimeType == nullptr) { - debugLogA("invalid file format for avatar %S", pwszFilename); - return 1; - } - szPayload.AppendFormat("%s;base64,", szMimeType); - FILE *in = _wfopen(pwszFilename, L"rb"); - if (in == nullptr) { - debugLogA("cannot open avatar file %S for reading", pwszFilename); - return 2; - } - - int iFileLength = _filelength(_fileno(in)); - ptrA szFileContents((char*)mir_alloc(iFileLength)); - fread(szFileContents, 1, iFileLength, in); - fclose(in); - szPayload.Append(ptrA(mir_base64_encode(szFileContents.get(), iFileLength))); - - JSONNode root; root << CHAR_PARAM("avatar", szPayload); - Push(new AsyncHttpRequest(this, REQUEST_PATCH, "/users/@me", nullptr, &root)); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CDiscordProto::CheckAvatarChange(MCONTACT hContact, const CMStringW &wszNewHash) -{ - if (wszNewHash.IsEmpty()) - return; - - ptrW wszOldAvatar(getWStringA(hContact, DB_KEY_AVHASH)); - - // if avatar's hash changed, we need to request a new one - if (mir_wstrcmp(wszNewHash, wszOldAvatar)) { - setWString(hContact, DB_KEY_AVHASH, wszNewHash); - RetrieveAvatar(hContact); - } -} +/* +Copyright © 2016-22 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +CMStringW CDiscordProto::GetAvatarFilename(MCONTACT hContact) +{ + CMStringW wszResult(FORMAT, L"%s\\%S", VARSW(L"%miranda_avatarcache%"), m_szModuleName); + CreateDirectoryTreeW(wszResult); + + wszResult.AppendChar('\\'); + + const wchar_t* szFileType = ProtoGetAvatarExtension(getByte(hContact, "AvatarType", PA_FORMAT_PNG)); + wszResult.AppendFormat(L"%lld%s", getId(hContact, DB_KEY_ID), szFileType); + return wszResult; +} + +INT_PTR CDiscordProto::GetAvatarCaps(WPARAM wParam, LPARAM lParam) +{ + int res = 0; + + switch (wParam) { + case AF_MAXSIZE: + ((POINT*)lParam)->x = ((POINT*)lParam)->y = 128; + break; + + case AF_FORMATSUPPORTED: + res = lParam == PA_FORMAT_PNG || lParam == PA_FORMAT_GIF || lParam == PA_FORMAT_JPEG; + break; + + case AF_ENABLED: + case AF_DONTNEEDDELAYS: + case AF_FETCHIFPROTONOTVISIBLE: + case AF_FETCHIFCONTACTOFFLINE: + return 1; + } + + return res; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordProto::OnReceiveAvatar(NETLIBHTTPREQUEST *reply, AsyncHttpRequest *pReq) +{ + PROTO_AVATAR_INFORMATION ai = { 0 }; + ai.format = PA_FORMAT_UNKNOWN; + ai.hContact = (UINT_PTR)pReq->pUserInfo; + + if (reply->resultCode != 200) { +LBL_Error: + ProtoBroadcastAck(ai.hContact, ACKTYPE_AVATAR, ACKRESULT_FAILED, (HANDLE)&ai); + return; + } + + if (auto *pszHdr = Netlib_GetHeader(reply, "Content-Type")) + ai.format = ProtoGetAvatarFormatByMimeType(pszHdr); + + if (ai.format == PA_FORMAT_UNKNOWN) { + debugLogA("unknown avatar mime type"); + goto LBL_Error; + } + + setByte(ai.hContact, "AvatarType", ai.format); + mir_wstrncpy(ai.filename, GetAvatarFilename(ai.hContact), _countof(ai.filename)); + + FILE *out = _wfopen(ai.filename, L"wb"); + if (out == nullptr) { + debugLogA("cannot open avatar file %S for writing", ai.filename); + goto LBL_Error; + } + + fwrite(reply->pData, 1, reply->dataLength, out); + fclose(out); + + if (ai.hContact) + ProtoBroadcastAck(ai.hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, (HANDLE)&ai); + else + ReportSelfAvatarChanged(); +} + +bool CDiscordProto::RetrieveAvatar(MCONTACT hContact) +{ + ptrA szAvatarHash(getStringA(hContact, DB_KEY_AVHASH)); + SnowFlake id = getId(hContact, DB_KEY_ID); + if (id == 0 || szAvatarHash == nullptr) + return false; + + CMStringA szUrl(FORMAT, "https://cdn.discordapp.com/avatars/%lld/%s.jpg", id, szAvatarHash.get()); + AsyncHttpRequest *pReq = new AsyncHttpRequest(this, REQUEST_GET, szUrl, &CDiscordProto::OnReceiveAvatar); + pReq->pUserInfo = (void*)hContact; + Push(pReq); + return true; +} + +INT_PTR CDiscordProto::GetAvatarInfo(WPARAM flags, LPARAM lParam) +{ + PROTO_AVATAR_INFORMATION *pai = (PROTO_AVATAR_INFORMATION *)lParam; + + CMStringW wszFileName(GetAvatarFilename(pai->hContact)); + if (!wszFileName.IsEmpty()) { + mir_wstrncpy(pai->filename, wszFileName, _countof(pai->filename)); + + bool bFileExist = _waccess(wszFileName, 0) == 0; + + // if we still need to load an avatar + if ((flags & GAIF_FORCE) || !bFileExist) { + if (RetrieveAvatar(pai->hContact)) + return GAIR_WAITFOR; + } + else if (bFileExist) + return GAIR_SUCCESS; + } + + return GAIR_NOAVATAR; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR CDiscordProto::GetMyAvatar(WPARAM wParam, LPARAM lParam) +{ + if (!wParam || !lParam) + return -3; + + wchar_t* buf = (wchar_t*)wParam; + int size = (int)lParam; + + PROTO_AVATAR_INFORMATION ai = {}; + switch (GetAvatarInfo(0, (LPARAM)&ai)) { + case GAIR_SUCCESS: + wcsncpy_s(buf, size, ai.filename, _TRUNCATE); + return 0; + + case GAIR_WAITFOR: + return -1; + } + + return -2; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR CDiscordProto::SetMyAvatar(WPARAM, LPARAM lParam) +{ + CMStringW wszFileName(GetAvatarFilename(0)); + + const wchar_t *pwszFilename = (const wchar_t*)lParam; + if (pwszFilename == nullptr) { // remove my avatar file + delSetting(DB_KEY_AVHASH); + DeleteFile(wszFileName); + } + + CMStringA szPayload("data:"); + + const char *szMimeType = ProtoGetAvatarMimeType(ProtoGetAvatarFileFormat(pwszFilename)); + if (szMimeType == nullptr) { + debugLogA("invalid file format for avatar %S", pwszFilename); + return 1; + } + szPayload.AppendFormat("%s;base64,", szMimeType); + FILE *in = _wfopen(pwszFilename, L"rb"); + if (in == nullptr) { + debugLogA("cannot open avatar file %S for reading", pwszFilename); + return 2; + } + + int iFileLength = _filelength(_fileno(in)); + ptrA szFileContents((char*)mir_alloc(iFileLength)); + fread(szFileContents, 1, iFileLength, in); + fclose(in); + szPayload.Append(ptrA(mir_base64_encode(szFileContents.get(), iFileLength))); + + JSONNode root; root << CHAR_PARAM("avatar", szPayload); + Push(new AsyncHttpRequest(this, REQUEST_PATCH, "/users/@me", nullptr, &root)); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordProto::CheckAvatarChange(MCONTACT hContact, const CMStringW &wszNewHash) +{ + if (wszNewHash.IsEmpty()) + return; + + ptrW wszOldAvatar(getWStringA(hContact, DB_KEY_AVHASH)); + + // if avatar's hash changed, we need to request a new one + if (mir_wstrcmp(wszNewHash, wszOldAvatar)) { + setWString(hContact, DB_KEY_AVHASH, wszNewHash); + RetrieveAvatar(hContact); + } +} diff --git a/protocols/Discord/src/connection.cpp b/protocols/Discord/src/connection.cpp index a85d5738a0..d98d6e4ec8 100644 --- a/protocols/Discord/src/connection.cpp +++ b/protocols/Discord/src/connection.cpp @@ -1,123 +1,123 @@ -/* -Copyright © 2016-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -void CDiscordProto::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->pData = mir_strdup(pReq->m_szParam); - pReq->dataLength = pReq->m_szParam.GetLength(); - } - } - - if (pReq->m_bMainSite) { - pReq->flags |= NLHRF_PERSISTENT; - pReq->nlc = m_hAPIConnection; - pReq->AddHeader("Cookie", m_szCookie); - } - - bool bRetryable = pReq->nlc != nullptr; - debugLogA("Executing request #%d:\n%s", pReq->m_iReqNum, pReq->szUrl); - -LBL_Retry: - NLHR_PTR reply(Netlib_HttpTransaction(m_hNetlibUser, pReq)); - if (reply == nullptr) { - debugLogA("Request %d failed", pReq->m_iReqNum); - - if (pReq->m_bMainSite) { - if (IsStatusConnecting(m_iStatus)) - ConnectionFailed(LOGINERR_NONETWORK); - m_hAPIConnection = nullptr; - } - - if (bRetryable) { - debugLogA("Attempt to retry request #%d", pReq->m_iReqNum); - pReq->nlc = nullptr; - bRetryable = false; - goto LBL_Retry; - } - } - else { - if (pReq->m_pFunc != nullptr) - (this->*(pReq->m_pFunc))(reply, pReq); - - if (pReq->m_bMainSite) - m_hAPIConnection = reply->nlc; - } - delete pReq; -} - -void CDiscordProto::OnLoggedIn() -{ - debugLogA("CDiscordProto::OnLoggedIn"); - m_bOnline = true; - SetServerStatus(m_iDesiredStatus); -} - -void CDiscordProto::OnLoggedOut() -{ - debugLogA("CDiscordProto::OnLoggedOut"); - m_bOnline = false; - m_bTerminated = true; - m_iGatewaySeq = 0; - m_szTempToken = nullptr; - m_szCookie.Empty(); - m_szWSCookie.Empty(); - - m_impl.m_heartBeat.StopSafe(); - - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE); - m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; - - setAllContactStatuses(ID_STATUS_OFFLINE, false); -} - -void CDiscordProto::ShutdownSession() -{ - if (m_bTerminated) - return; - - debugLogA("CDiscordProto::ShutdownSession"); - - // shutdown all resources - if (m_hWorkerThread) - SetEvent(m_evRequestsQueue); - if (m_hGatewayConnection) - Netlib_Shutdown(m_hGatewayConnection); - if (m_hAPIConnection) - Netlib_Shutdown(m_hAPIConnection); - - OnLoggedOut(); -} - -void CDiscordProto::ConnectionFailed(int iReason) -{ - debugLogA("CDiscordProto::ConnectionFailed -> reason %d", iReason); - delSetting("AccessToken"); - - ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, nullptr, iReason); - ShutdownSession(); -} +/* +Copyright © 2016-22 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +void CDiscordProto::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->pData = mir_strdup(pReq->m_szParam); + pReq->dataLength = pReq->m_szParam.GetLength(); + } + } + + if (pReq->m_bMainSite) { + pReq->flags |= NLHRF_PERSISTENT; + pReq->nlc = m_hAPIConnection; + pReq->AddHeader("Cookie", m_szCookie); + } + + bool bRetryable = pReq->nlc != nullptr; + debugLogA("Executing request #%d:\n%s", pReq->m_iReqNum, pReq->szUrl); + +LBL_Retry: + NLHR_PTR reply(Netlib_HttpTransaction(m_hNetlibUser, pReq)); + if (reply == nullptr) { + debugLogA("Request %d failed", pReq->m_iReqNum); + + if (pReq->m_bMainSite) { + if (IsStatusConnecting(m_iStatus)) + ConnectionFailed(LOGINERR_NONETWORK); + m_hAPIConnection = nullptr; + } + + if (bRetryable) { + debugLogA("Attempt to retry request #%d", pReq->m_iReqNum); + pReq->nlc = nullptr; + bRetryable = false; + goto LBL_Retry; + } + } + else { + if (pReq->m_pFunc != nullptr) + (this->*(pReq->m_pFunc))(reply, pReq); + + if (pReq->m_bMainSite) + m_hAPIConnection = reply->nlc; + } + delete pReq; +} + +void CDiscordProto::OnLoggedIn() +{ + debugLogA("CDiscordProto::OnLoggedIn"); + m_bOnline = true; + SetServerStatus(m_iDesiredStatus); +} + +void CDiscordProto::OnLoggedOut() +{ + debugLogA("CDiscordProto::OnLoggedOut"); + m_bOnline = false; + m_bTerminated = true; + m_iGatewaySeq = 0; + m_szTempToken = nullptr; + m_szCookie.Empty(); + m_szWSCookie.Empty(); + + m_impl.m_heartBeat.StopSafe(); + + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE); + m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; + + setAllContactStatuses(ID_STATUS_OFFLINE, false); +} + +void CDiscordProto::ShutdownSession() +{ + if (m_bTerminated) + return; + + debugLogA("CDiscordProto::ShutdownSession"); + + // shutdown all resources + if (m_hWorkerThread) + SetEvent(m_evRequestsQueue); + if (m_hGatewayConnection) + Netlib_Shutdown(m_hGatewayConnection); + if (m_hAPIConnection) + Netlib_Shutdown(m_hAPIConnection); + + OnLoggedOut(); +} + +void CDiscordProto::ConnectionFailed(int iReason) +{ + debugLogA("CDiscordProto::ConnectionFailed -> reason %d", iReason); + delSetting("AccessToken"); + + ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, nullptr, iReason); + ShutdownSession(); +} diff --git a/protocols/Discord/src/dispatch.cpp b/protocols/Discord/src/dispatch.cpp index 5d79feb9fe..7554fa669c 100644 --- a/protocols/Discord/src/dispatch.cpp +++ b/protocols/Discord/src/dispatch.cpp @@ -1,592 +1,592 @@ -/* -Copyright © 2016-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -#pragma pack(4) - -///////////////////////////////////////////////////////////////////////////////////////// - -struct CDiscordCommand -{ - const wchar_t *szCommandId; - GatewayHandlerFunc pFunc; -} -static handlers[] = // these structures must me sorted alphabetically -{ - { L"CALL_CREATE", &CDiscordProto::OnCommandCallCreated }, - { L"CALL_DELETE", &CDiscordProto::OnCommandCallDeleted }, - { L"CALL_UPDATE", &CDiscordProto::OnCommandCallUpdated }, - - { L"CHANNEL_CREATE", &CDiscordProto::OnCommandChannelCreated }, - { L"CHANNEL_DELETE", &CDiscordProto::OnCommandChannelDeleted }, - { L"CHANNEL_UPDATE", &CDiscordProto::OnCommandChannelUpdated }, - - { L"GUILD_CREATE", &CDiscordProto::OnCommandGuildCreated }, - { L"GUILD_DELETE", &CDiscordProto::OnCommandGuildDeleted }, - { L"GUILD_MEMBER_ADD", &CDiscordProto::OnCommandGuildMemberAdded }, - { L"GUILD_MEMBER_LIST_UPDATE", &CDiscordProto::OnCommandGuildMemberListUpdate }, - { L"GUILD_MEMBER_REMOVE", &CDiscordProto::OnCommandGuildMemberRemoved }, - { L"GUILD_MEMBER_UPDATE", &CDiscordProto::OnCommandGuildMemberUpdated }, - { L"GUILD_ROLE_CREATE", &CDiscordProto::OnCommandRoleCreated }, - { L"GUILD_ROLE_DELETE", &CDiscordProto::OnCommandRoleDeleted }, - { L"GUILD_ROLE_UPDATE", &CDiscordProto::OnCommandRoleCreated }, - - { L"MESSAGE_ACK", &CDiscordProto::OnCommandMessageAck }, - { L"MESSAGE_CREATE", &CDiscordProto::OnCommandMessageCreate }, - { L"MESSAGE_DELETE", &CDiscordProto::OnCommandMessageDelete }, - { L"MESSAGE_UPDATE", &CDiscordProto::OnCommandMessageUpdate }, - - { L"PRESENCE_UPDATE", &CDiscordProto::OnCommandPresence }, - - { L"READY", &CDiscordProto::OnCommandReady }, - - { L"RELATIONSHIP_ADD", &CDiscordProto::OnCommandFriendAdded }, - { L"RELATIONSHIP_REMOVE", &CDiscordProto::OnCommandFriendRemoved }, - - { L"TYPING_START", &CDiscordProto::OnCommandTyping }, - - { L"USER_SETTINGS_UPDATE", &CDiscordProto::OnCommandUserSettingsUpdate }, - { L"USER_UPDATE", &CDiscordProto::OnCommandUserUpdate }, -}; - -static int __cdecl pSearchFunc(const void *p1, const void *p2) -{ - return wcscmp(((CDiscordCommand*)p1)->szCommandId, ((CDiscordCommand*)p2)->szCommandId); -} - -GatewayHandlerFunc CDiscordProto::GetHandler(const wchar_t *pwszCommand) -{ - CDiscordCommand tmp = { pwszCommand, nullptr }; - CDiscordCommand *p = (CDiscordCommand*)bsearch(&tmp, handlers, _countof(handlers), sizeof(handlers[0]), pSearchFunc); - return (p != nullptr) ? p->pFunc : nullptr; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// channel operations - -void CDiscordProto::OnCommandChannelCreated(const JSONNode &pRoot) -{ - SnowFlake guildId = ::getId(pRoot["guild_id"]); - if (guildId == 0) - PreparePrivateChannel(pRoot); - else { - // group channel for a guild - CDiscordGuild *pGuild = FindGuild(guildId); - if (pGuild && m_bUseGroupchats) { - CDiscordUser *pUser = ProcessGuildChannel(pGuild, pRoot); - if (pUser) - CreateChat(pGuild, pUser); - } - } -} - -void CDiscordProto::OnCommandChannelDeleted(const JSONNode &pRoot) -{ - CDiscordUser *pUser = FindUserByChannel(::getId(pRoot["id"])); - if (pUser == nullptr) - return; - - SnowFlake guildId = ::getId(pRoot["guild_id"]); - if (guildId == 0) { - pUser->channelId = pUser->lastMsgId = 0; - delSetting(pUser->hContact, DB_KEY_CHANNELID); - } - else { - CDiscordGuild *pGuild = FindGuild(guildId); - if (pGuild != nullptr) - Chat_Terminate(m_szModuleName, pUser->wszUsername, true); - } -} - -void CDiscordProto::OnCommandChannelUpdated(const JSONNode &pRoot) -{ - CDiscordUser *pUser = FindUserByChannel(::getId(pRoot["id"])); - if (pUser == nullptr) - return; - - pUser->lastMsgId = ::getId(pRoot["last_message_id"]); - - SnowFlake guildId = ::getId(pRoot["guild_id"]); - if (guildId != 0) { - CDiscordGuild *pGuild = FindGuild(guildId); - if (pGuild == nullptr) - return; - - CMStringW wszName = pRoot["name"].as_mstring(); - if (!wszName.IsEmpty()) { - CMStringW wszNewName = pGuild->wszName + L"#" + wszName; - Chat_ChangeSessionName(m_szModuleName, pUser->wszUsername, wszNewName); - } - - CMStringW wszTopic = pRoot["topic"].as_mstring(); - Chat_SetStatusbarText(m_szModuleName, pUser->wszUsername, wszTopic); - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_TOPIC }; - gce.pszID.w = pUser->wszUsername; - gce.pszText.w = wszTopic; - gce.time = time(0); - Chat_Event(&gce); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// -// reading a new message - -void CDiscordProto::OnCommandFriendAdded(const JSONNode &pRoot) -{ - CDiscordUser *pUser = PrepareUser(pRoot["user"]); - pUser->bIsPrivate = true; - ProcessType(pUser, pRoot); -} - -void CDiscordProto::OnCommandFriendRemoved(const JSONNode &pRoot) -{ - SnowFlake id = ::getId(pRoot["id"]); - CDiscordUser *pUser = FindUser(id); - if (pUser != nullptr) { - if (pUser->hContact) - if (pUser->bIsPrivate) - db_delete_contact(pUser->hContact); - - arUsers.remove(pUser); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// -// guild synchronization - -void CDiscordProto::OnCommandGuildCreated(const JSONNode &pRoot) -{ - if (m_bUseGroupchats) - ProcessGuild(pRoot); -} - -void CDiscordProto::OnCommandGuildDeleted(const JSONNode &pRoot) -{ - CDiscordGuild *pGuild = FindGuild(::getId(pRoot["id"])); - if (pGuild == nullptr) - return; - - for (auto &it : arUsers.rev_iter()) - if (it->pGuild == pGuild) { - Chat_Terminate(m_szModuleName, it->wszUsername, true); - arUsers.removeItem(&it); - } - - Chat_Terminate(m_szModuleName, pRoot["name"].as_mstring(), true); - - arGuilds.remove(pGuild); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// guild members - -void CDiscordProto::OnCommandGuildMemberAdded(const JSONNode&) -{ -} - -void CDiscordProto::OnCommandGuildMemberListUpdate(const JSONNode &pRoot) -{ - auto *pGuild = FindGuild(::getId(pRoot["guild_id"])); - if (pGuild == nullptr) - return; - - int iStatus = 0; - - for (auto &ops: pRoot["ops"]) { - for (auto &it : ops["items"]) { - auto &item = it.at((size_t)0); - if (!mir_strcmp(item .name(), "group")) { - iStatus = item ["id"].as_string() == "online" ? ID_STATUS_ONLINE : ID_STATUS_OFFLINE; - continue; - } - - if (!mir_strcmp(item .name(), "member")) { - bool bNew = false; - auto *pm = ProcessGuildUser(pGuild, item, &bNew); - pm->iStatus = iStatus; - - if (bNew) - AddGuildUser(pGuild, *pm); - else if (iStatus) { - CMStringW wszUserId(FORMAT, L"%lld", pm->userId); - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_SETCONTACTSTATUS }; - gce.time = time(0); - gce.pszUID.w = wszUserId; - - for (auto &cc : pGuild->arChannels) { - if (!cc->bIsGroup) - continue; - - gce.pszID.w = cc->wszChannelName; - gce.dwItemData = iStatus; - Chat_Event(&gce); - } - } - } - } - } - - pGuild->bSynced = true; -} - -void CDiscordProto::OnCommandGuildMemberRemoved(const JSONNode &pRoot) -{ - CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"])); - if (pGuild == nullptr) - return; - - CMStringW wszUserId = pRoot["user"]["id"].as_mstring(); - - for (auto &pUser : arUsers) { - if (pUser->pGuild != pGuild) - continue; - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_PART }; - gce.pszUID.w = pUser->wszUsername; - gce.time = time(0); - gce.pszUID.w = wszUserId; - Chat_Event(&gce); - } -} - -void CDiscordProto::OnCommandGuildMemberUpdated(const JSONNode &pRoot) -{ - CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"])); - if (pGuild == nullptr) - return; - - CMStringW wszUserId = pRoot["user"]["id"].as_mstring(); - CDiscordGuildMember *gm = pGuild->FindUser(_wtoi64(wszUserId)); - if (gm == nullptr) - return; - - gm->wszDiscordId = pRoot["user"]["username"].as_mstring() + L"#" + pRoot["user"]["discriminator"].as_mstring(); - gm->wszNick = pRoot["nick"].as_mstring(); - if (gm->wszNick.IsEmpty()) - gm->wszNick = pRoot["user"]["username"].as_mstring(); - - for (auto &it : arUsers) { - if (it->pGuild != pGuild) - continue; - - CMStringW wszOldNick; - SESSION_INFO *si = g_chatApi.SM_FindSession(it->wszUsername, m_szModuleName); - if (si != nullptr) { - USERINFO *ui = g_chatApi.UM_FindUser(si, wszUserId); - if (ui != nullptr) - wszOldNick = ui->pszNick; - } - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_NICK }; - gce.pszID.w = it->wszUsername; - gce.time = time(0); - gce.pszUID.w = wszUserId; - gce.pszNick.w = wszOldNick; - gce.pszText.w = gm->wszNick; - Chat_Event(&gce); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// -// roles - -void CDiscordProto::OnCommandRoleCreated(const JSONNode &pRoot) -{ - CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"])); - if (pGuild != nullptr) - ProcessRole(pGuild, pRoot["role"]); -} - -void CDiscordProto::OnCommandRoleDeleted(const JSONNode &pRoot) -{ - CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"])); - if (pGuild == nullptr) - return; - - SnowFlake id = ::getId(pRoot["role_id"]); - CDiscordRole *pRole = pGuild->arRoles.find((CDiscordRole*)&id); - if (pRole == nullptr) - return; - - int iOldPosition = pRole->position; - pGuild->arRoles.remove(pRole); - - for (auto &it : pGuild->arRoles) - if (it->position > iOldPosition) - it->position--; - - for (auto &it : arUsers) { - if (it->pGuild != pGuild) - continue; - - SESSION_INFO *si = g_chatApi.SM_FindSession(it->wszUsername, m_szModuleName); - if (si != nullptr) { - g_chatApi.TM_RemoveAll(&si->pStatuses); - BuildStatusList(pGuild, si); - } - } -} - -///////////////////////////////////////////////////////////////////////////////////////// -// reading a new message - -void CDiscordProto::OnCommandMessageCreate(const JSONNode &pRoot) -{ - OnCommandMessage(pRoot, true); -} - -void CDiscordProto::OnCommandMessageUpdate(const JSONNode &pRoot) -{ - OnCommandMessage(pRoot, false); -} - -void CDiscordProto::OnCommandMessage(const JSONNode &pRoot, bool bIsNew) -{ - CMStringW wszMessageId = pRoot["id"].as_mstring(); - CMStringW wszUserId = pRoot["author"]["id"].as_mstring(); - SnowFlake userId = _wtoi64(wszUserId); - SnowFlake msgId = _wtoi64(wszMessageId); - - // try to find a sender by his channel - SnowFlake channelId = ::getId(pRoot["channel_id"]); - CDiscordUser *pUser = FindUserByChannel(channelId); - if (pUser == nullptr) { - debugLogA("skipping message with unknown channel id=%lld", channelId); - return; - } - - char szMsgId[100]; - _i64toa_s(msgId, szMsgId, _countof(szMsgId), 10); - - COwnMessage ownMsg(::getId(pRoot["nonce"]), 0); - COwnMessage *p = arOwnMessages.find(&ownMsg); - if (p != nullptr) { // own message? skip it - ProtoBroadcastAck(pUser->hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)p->reqId, (LPARAM)szMsgId); - debugLogA("skipping own message with nonce=%lld, id=%lld", ownMsg.nonce, msgId); - } - else { - CMStringW wszText = PrepareMessageText(pRoot); - if (wszText.IsEmpty()) - return; - - // old message? try to restore it from database - bool bOurMessage = userId == m_ownId; - if (!bIsNew) { - MEVENT hOldEvent = db_event_getById(m_szModuleName, szMsgId); - if (hOldEvent) { - DB::EventInfo dbei; - dbei.cbBlob = -1; - if (!db_event_get(hOldEvent, &dbei)) { - ptrW wszOldText(DbEvent_GetTextW(&dbei, CP_UTF8)); - if (wszOldText) - wszText.Insert(0, wszOldText); - if (dbei.flags & DBEF_SENT) - bOurMessage = true; - } - } - } - - const JSONNode &edited = pRoot["edited_timestamp"]; - if (!edited.isnull()) - wszText.AppendFormat(L" (%s %s)", TranslateT("edited at"), edited.as_mstring().c_str()); - - if (pUser->bIsPrivate && !pUser->bIsGroup) { - // if a message has myself as an author, add some flags - PROTORECVEVENT recv = {}; - if (bOurMessage) - recv.flags = PREF_CREATEREAD | PREF_SENT; - - debugLogA("store a message from private user %lld, channel id %lld", pUser->id, pUser->channelId); - ptrA buf(mir_utf8encodeW(wszText)); - - recv.timestamp = (uint32_t)StringToDate(pRoot["timestamp"].as_mstring()); - recv.szMessage = buf; - recv.szMsgId = szMsgId; - ProtoChainRecvMsg(pUser->hContact, &recv); - } - else { - debugLogA("store a message into the group channel id %lld", channelId); - - SESSION_INFO *si = g_chatApi.SM_FindSession(pUser->wszUsername, m_szModuleName); - if (si == nullptr) { - debugLogA("message to unknown channel %lld ignored", channelId); - return; - } - - ProcessChatUser(pUser, wszUserId, pRoot); - - ParseSpecialChars(si, wszText); - wszText.Replace(L"%", L"%%"); - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE }; - gce.pszID.w = pUser->wszUsername; - gce.dwFlags = GCEF_ADDTOLOG; - gce.pszUID.w = wszUserId; - gce.pszText.w = wszText; - gce.time = (uint32_t)StringToDate(pRoot["timestamp"].as_mstring()); - gce.bIsMe = bOurMessage; - Chat_Event(&gce); - - debugLogW(L"New channel %s message from %s: %s", si->ptszID, gce.pszUID.w, gce.pszText.w); - } - } - - pUser->lastMsgId = msgId; - - SnowFlake lastId = getId(pUser->hContact, DB_KEY_LASTMSGID); // as stored in a database - if (lastId < msgId) - setId(pUser->hContact, DB_KEY_LASTMSGID, msgId); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// someone changed its status - -void CDiscordProto::OnCommandMessageAck(const JSONNode &pRoot) -{ - CDiscordUser *pUser = FindUserByChannel(pRoot["channel_id"]); - if (pUser != nullptr) - pUser->lastMsgId = ::getId(pRoot["message_id"]); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// message deleted - -void CDiscordProto::OnCommandMessageDelete(const JSONNode &pRoot) -{ - if (!m_bSyncDeleteMsgs) - return; - - CMStringA msgid(pRoot["id"].as_mstring()); - if (!msgid.IsEmpty()) { - MEVENT hEvent = db_event_getById(m_szModuleName, msgid); - if (hEvent) - db_event_delete(hEvent); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// -// someone changed its status - -void CDiscordProto::OnCommandPresence(const JSONNode &pRoot) -{ - auto *pGuild = FindGuild(::getId(pRoot["user"]["guild_id"])); - if (pGuild == nullptr) - ProcessPresence(pRoot); - // else - // pGuild->ProcessPresence(pRoot); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// gateway session start - -void CDiscordProto::OnCommandReady(const JSONNode &pRoot) -{ - OnLoggedIn(); - - GatewaySendHeartbeat(); - m_impl.m_heartBeat.StartSafe(m_iHartbeatInterval); - - m_szGatewaySessionId = pRoot["session_id"].as_mstring(); - - if (m_bUseGroupchats) - for (auto &it : pRoot["guilds"]) - ProcessGuild(it); - - for (auto &it : pRoot["relationships"]) { - CDiscordUser *pUser = PrepareUser(it["user"]); - ProcessType(pUser, it); - } - - for (auto &it : pRoot["presences"]) - ProcessPresence(it); - - for (auto &it : pRoot["private_channels"]) - PreparePrivateChannel(it); - - for (auto &it : pRoot["read_state"]) { - CDiscordUser *pUser = FindUserByChannel(::getId(it["id"])); - if (pUser != nullptr) - pUser->lastReadId = ::getId(it["last_message_id"]); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// -// UTN support - -void CDiscordProto::OnCommandTyping(const JSONNode &pRoot) -{ - SnowFlake channelId = ::getId(pRoot["channel_id"]); - debugLogA("user typing notification: channelid=%lld", channelId); - - CDiscordUser *pChannel = FindUserByChannel(channelId); - if (pChannel == nullptr) { - debugLogA("channel with id=%lld is not found", channelId); - return; - } - - // both private groupchats & guild channels are chat rooms for Miranda - if (pChannel->pGuild) { - debugLogA("user is typing in a group channel"); - - CMStringW wszUerId = pRoot["user_id"].as_mstring(); - ProcessGuildUser(pChannel->pGuild, pRoot); // never returns null - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_TYPING }; - gce.pszID.w = pChannel->wszUsername; - gce.pszUID.w = wszUerId; - gce.dwItemData = 1; - gce.time = time(0); - Chat_Event(&gce); - } - else { - debugLogA("user is typing in his private channel"); - CallService(MS_PROTO_CONTACTISTYPING, pChannel->hContact, 20); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// -// User info update - -void CDiscordProto::OnCommandUserUpdate(const JSONNode &pRoot) -{ - SnowFlake id = ::getId(pRoot["id"]); - - MCONTACT hContact; - if (id != m_ownId) { - CDiscordUser *pUser = FindUser(id); - if (pUser == nullptr) - return; - - hContact = pUser->hContact; - } - else hContact = 0; - - // force rereading avatar - CheckAvatarChange(hContact, pRoot["avatar"].as_mstring()); -} - -void CDiscordProto::OnCommandUserSettingsUpdate(const JSONNode &pRoot) -{ - int iStatus = StrToStatus(pRoot["status"].as_mstring()); - if (iStatus != 0) { - int iOldStatus = m_iStatus; m_iStatus = iStatus; - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); - } -} +/* +Copyright © 2016-22 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +#pragma pack(4) + +///////////////////////////////////////////////////////////////////////////////////////// + +struct CDiscordCommand +{ + const wchar_t *szCommandId; + GatewayHandlerFunc pFunc; +} +static handlers[] = // these structures must me sorted alphabetically +{ + { L"CALL_CREATE", &CDiscordProto::OnCommandCallCreated }, + { L"CALL_DELETE", &CDiscordProto::OnCommandCallDeleted }, + { L"CALL_UPDATE", &CDiscordProto::OnCommandCallUpdated }, + + { L"CHANNEL_CREATE", &CDiscordProto::OnCommandChannelCreated }, + { L"CHANNEL_DELETE", &CDiscordProto::OnCommandChannelDeleted }, + { L"CHANNEL_UPDATE", &CDiscordProto::OnCommandChannelUpdated }, + + { L"GUILD_CREATE", &CDiscordProto::OnCommandGuildCreated }, + { L"GUILD_DELETE", &CDiscordProto::OnCommandGuildDeleted }, + { L"GUILD_MEMBER_ADD", &CDiscordProto::OnCommandGuildMemberAdded }, + { L"GUILD_MEMBER_LIST_UPDATE", &CDiscordProto::OnCommandGuildMemberListUpdate }, + { L"GUILD_MEMBER_REMOVE", &CDiscordProto::OnCommandGuildMemberRemoved }, + { L"GUILD_MEMBER_UPDATE", &CDiscordProto::OnCommandGuildMemberUpdated }, + { L"GUILD_ROLE_CREATE", &CDiscordProto::OnCommandRoleCreated }, + { L"GUILD_ROLE_DELETE", &CDiscordProto::OnCommandRoleDeleted }, + { L"GUILD_ROLE_UPDATE", &CDiscordProto::OnCommandRoleCreated }, + + { L"MESSAGE_ACK", &CDiscordProto::OnCommandMessageAck }, + { L"MESSAGE_CREATE", &CDiscordProto::OnCommandMessageCreate }, + { L"MESSAGE_DELETE", &CDiscordProto::OnCommandMessageDelete }, + { L"MESSAGE_UPDATE", &CDiscordProto::OnCommandMessageUpdate }, + + { L"PRESENCE_UPDATE", &CDiscordProto::OnCommandPresence }, + + { L"READY", &CDiscordProto::OnCommandReady }, + + { L"RELATIONSHIP_ADD", &CDiscordProto::OnCommandFriendAdded }, + { L"RELATIONSHIP_REMOVE", &CDiscordProto::OnCommandFriendRemoved }, + + { L"TYPING_START", &CDiscordProto::OnCommandTyping }, + + { L"USER_SETTINGS_UPDATE", &CDiscordProto::OnCommandUserSettingsUpdate }, + { L"USER_UPDATE", &CDiscordProto::OnCommandUserUpdate }, +}; + +static int __cdecl pSearchFunc(const void *p1, const void *p2) +{ + return wcscmp(((CDiscordCommand*)p1)->szCommandId, ((CDiscordCommand*)p2)->szCommandId); +} + +GatewayHandlerFunc CDiscordProto::GetHandler(const wchar_t *pwszCommand) +{ + CDiscordCommand tmp = { pwszCommand, nullptr }; + CDiscordCommand *p = (CDiscordCommand*)bsearch(&tmp, handlers, _countof(handlers), sizeof(handlers[0]), pSearchFunc); + return (p != nullptr) ? p->pFunc : nullptr; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// channel operations + +void CDiscordProto::OnCommandChannelCreated(const JSONNode &pRoot) +{ + SnowFlake guildId = ::getId(pRoot["guild_id"]); + if (guildId == 0) + PreparePrivateChannel(pRoot); + else { + // group channel for a guild + CDiscordGuild *pGuild = FindGuild(guildId); + if (pGuild && m_bUseGroupchats) { + CDiscordUser *pUser = ProcessGuildChannel(pGuild, pRoot); + if (pUser) + CreateChat(pGuild, pUser); + } + } +} + +void CDiscordProto::OnCommandChannelDeleted(const JSONNode &pRoot) +{ + CDiscordUser *pUser = FindUserByChannel(::getId(pRoot["id"])); + if (pUser == nullptr) + return; + + SnowFlake guildId = ::getId(pRoot["guild_id"]); + if (guildId == 0) { + pUser->channelId = pUser->lastMsgId = 0; + delSetting(pUser->hContact, DB_KEY_CHANNELID); + } + else { + CDiscordGuild *pGuild = FindGuild(guildId); + if (pGuild != nullptr) + Chat_Terminate(m_szModuleName, pUser->wszUsername, true); + } +} + +void CDiscordProto::OnCommandChannelUpdated(const JSONNode &pRoot) +{ + CDiscordUser *pUser = FindUserByChannel(::getId(pRoot["id"])); + if (pUser == nullptr) + return; + + pUser->lastMsgId = ::getId(pRoot["last_message_id"]); + + SnowFlake guildId = ::getId(pRoot["guild_id"]); + if (guildId != 0) { + CDiscordGuild *pGuild = FindGuild(guildId); + if (pGuild == nullptr) + return; + + CMStringW wszName = pRoot["name"].as_mstring(); + if (!wszName.IsEmpty()) { + CMStringW wszNewName = pGuild->wszName + L"#" + wszName; + Chat_ChangeSessionName(m_szModuleName, pUser->wszUsername, wszNewName); + } + + CMStringW wszTopic = pRoot["topic"].as_mstring(); + Chat_SetStatusbarText(m_szModuleName, pUser->wszUsername, wszTopic); + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_TOPIC }; + gce.pszID.w = pUser->wszUsername; + gce.pszText.w = wszTopic; + gce.time = time(0); + Chat_Event(&gce); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// +// reading a new message + +void CDiscordProto::OnCommandFriendAdded(const JSONNode &pRoot) +{ + CDiscordUser *pUser = PrepareUser(pRoot["user"]); + pUser->bIsPrivate = true; + ProcessType(pUser, pRoot); +} + +void CDiscordProto::OnCommandFriendRemoved(const JSONNode &pRoot) +{ + SnowFlake id = ::getId(pRoot["id"]); + CDiscordUser *pUser = FindUser(id); + if (pUser != nullptr) { + if (pUser->hContact) + if (pUser->bIsPrivate) + db_delete_contact(pUser->hContact); + + arUsers.remove(pUser); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// +// guild synchronization + +void CDiscordProto::OnCommandGuildCreated(const JSONNode &pRoot) +{ + if (m_bUseGroupchats) + ProcessGuild(pRoot); +} + +void CDiscordProto::OnCommandGuildDeleted(const JSONNode &pRoot) +{ + CDiscordGuild *pGuild = FindGuild(::getId(pRoot["id"])); + if (pGuild == nullptr) + return; + + for (auto &it : arUsers.rev_iter()) + if (it->pGuild == pGuild) { + Chat_Terminate(m_szModuleName, it->wszUsername, true); + arUsers.removeItem(&it); + } + + Chat_Terminate(m_szModuleName, pRoot["name"].as_mstring(), true); + + arGuilds.remove(pGuild); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// guild members + +void CDiscordProto::OnCommandGuildMemberAdded(const JSONNode&) +{ +} + +void CDiscordProto::OnCommandGuildMemberListUpdate(const JSONNode &pRoot) +{ + auto *pGuild = FindGuild(::getId(pRoot["guild_id"])); + if (pGuild == nullptr) + return; + + int iStatus = 0; + + for (auto &ops: pRoot["ops"]) { + for (auto &it : ops["items"]) { + auto &item = it.at((size_t)0); + if (!mir_strcmp(item .name(), "group")) { + iStatus = item ["id"].as_string() == "online" ? ID_STATUS_ONLINE : ID_STATUS_OFFLINE; + continue; + } + + if (!mir_strcmp(item .name(), "member")) { + bool bNew = false; + auto *pm = ProcessGuildUser(pGuild, item, &bNew); + pm->iStatus = iStatus; + + if (bNew) + AddGuildUser(pGuild, *pm); + else if (iStatus) { + CMStringW wszUserId(FORMAT, L"%lld", pm->userId); + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_SETCONTACTSTATUS }; + gce.time = time(0); + gce.pszUID.w = wszUserId; + + for (auto &cc : pGuild->arChannels) { + if (!cc->bIsGroup) + continue; + + gce.pszID.w = cc->wszChannelName; + gce.dwItemData = iStatus; + Chat_Event(&gce); + } + } + } + } + } + + pGuild->bSynced = true; +} + +void CDiscordProto::OnCommandGuildMemberRemoved(const JSONNode &pRoot) +{ + CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"])); + if (pGuild == nullptr) + return; + + CMStringW wszUserId = pRoot["user"]["id"].as_mstring(); + + for (auto &pUser : arUsers) { + if (pUser->pGuild != pGuild) + continue; + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_PART }; + gce.pszUID.w = pUser->wszUsername; + gce.time = time(0); + gce.pszUID.w = wszUserId; + Chat_Event(&gce); + } +} + +void CDiscordProto::OnCommandGuildMemberUpdated(const JSONNode &pRoot) +{ + CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"])); + if (pGuild == nullptr) + return; + + CMStringW wszUserId = pRoot["user"]["id"].as_mstring(); + CDiscordGuildMember *gm = pGuild->FindUser(_wtoi64(wszUserId)); + if (gm == nullptr) + return; + + gm->wszDiscordId = pRoot["user"]["username"].as_mstring() + L"#" + pRoot["user"]["discriminator"].as_mstring(); + gm->wszNick = pRoot["nick"].as_mstring(); + if (gm->wszNick.IsEmpty()) + gm->wszNick = pRoot["user"]["username"].as_mstring(); + + for (auto &it : arUsers) { + if (it->pGuild != pGuild) + continue; + + CMStringW wszOldNick; + SESSION_INFO *si = g_chatApi.SM_FindSession(it->wszUsername, m_szModuleName); + if (si != nullptr) { + USERINFO *ui = g_chatApi.UM_FindUser(si, wszUserId); + if (ui != nullptr) + wszOldNick = ui->pszNick; + } + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_NICK }; + gce.pszID.w = it->wszUsername; + gce.time = time(0); + gce.pszUID.w = wszUserId; + gce.pszNick.w = wszOldNick; + gce.pszText.w = gm->wszNick; + Chat_Event(&gce); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// +// roles + +void CDiscordProto::OnCommandRoleCreated(const JSONNode &pRoot) +{ + CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"])); + if (pGuild != nullptr) + ProcessRole(pGuild, pRoot["role"]); +} + +void CDiscordProto::OnCommandRoleDeleted(const JSONNode &pRoot) +{ + CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"])); + if (pGuild == nullptr) + return; + + SnowFlake id = ::getId(pRoot["role_id"]); + CDiscordRole *pRole = pGuild->arRoles.find((CDiscordRole*)&id); + if (pRole == nullptr) + return; + + int iOldPosition = pRole->position; + pGuild->arRoles.remove(pRole); + + for (auto &it : pGuild->arRoles) + if (it->position > iOldPosition) + it->position--; + + for (auto &it : arUsers) { + if (it->pGuild != pGuild) + continue; + + SESSION_INFO *si = g_chatApi.SM_FindSession(it->wszUsername, m_szModuleName); + if (si != nullptr) { + g_chatApi.TM_RemoveAll(&si->pStatuses); + BuildStatusList(pGuild, si); + } + } +} + +///////////////////////////////////////////////////////////////////////////////////////// +// reading a new message + +void CDiscordProto::OnCommandMessageCreate(const JSONNode &pRoot) +{ + OnCommandMessage(pRoot, true); +} + +void CDiscordProto::OnCommandMessageUpdate(const JSONNode &pRoot) +{ + OnCommandMessage(pRoot, false); +} + +void CDiscordProto::OnCommandMessage(const JSONNode &pRoot, bool bIsNew) +{ + CMStringW wszMessageId = pRoot["id"].as_mstring(); + CMStringW wszUserId = pRoot["author"]["id"].as_mstring(); + SnowFlake userId = _wtoi64(wszUserId); + SnowFlake msgId = _wtoi64(wszMessageId); + + // try to find a sender by his channel + SnowFlake channelId = ::getId(pRoot["channel_id"]); + CDiscordUser *pUser = FindUserByChannel(channelId); + if (pUser == nullptr) { + debugLogA("skipping message with unknown channel id=%lld", channelId); + return; + } + + char szMsgId[100]; + _i64toa_s(msgId, szMsgId, _countof(szMsgId), 10); + + COwnMessage ownMsg(::getId(pRoot["nonce"]), 0); + COwnMessage *p = arOwnMessages.find(&ownMsg); + if (p != nullptr) { // own message? skip it + ProtoBroadcastAck(pUser->hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)p->reqId, (LPARAM)szMsgId); + debugLogA("skipping own message with nonce=%lld, id=%lld", ownMsg.nonce, msgId); + } + else { + CMStringW wszText = PrepareMessageText(pRoot); + if (wszText.IsEmpty()) + return; + + // old message? try to restore it from database + bool bOurMessage = userId == m_ownId; + if (!bIsNew) { + MEVENT hOldEvent = db_event_getById(m_szModuleName, szMsgId); + if (hOldEvent) { + DB::EventInfo dbei; + dbei.cbBlob = -1; + if (!db_event_get(hOldEvent, &dbei)) { + ptrW wszOldText(DbEvent_GetTextW(&dbei, CP_UTF8)); + if (wszOldText) + wszText.Insert(0, wszOldText); + if (dbei.flags & DBEF_SENT) + bOurMessage = true; + } + } + } + + const JSONNode &edited = pRoot["edited_timestamp"]; + if (!edited.isnull()) + wszText.AppendFormat(L" (%s %s)", TranslateT("edited at"), edited.as_mstring().c_str()); + + if (pUser->bIsPrivate && !pUser->bIsGroup) { + // if a message has myself as an author, add some flags + PROTORECVEVENT recv = {}; + if (bOurMessage) + recv.flags = PREF_CREATEREAD | PREF_SENT; + + debugLogA("store a message from private user %lld, channel id %lld", pUser->id, pUser->channelId); + ptrA buf(mir_utf8encodeW(wszText)); + + recv.timestamp = (uint32_t)StringToDate(pRoot["timestamp"].as_mstring()); + recv.szMessage = buf; + recv.szMsgId = szMsgId; + ProtoChainRecvMsg(pUser->hContact, &recv); + } + else { + debugLogA("store a message into the group channel id %lld", channelId); + + SESSION_INFO *si = g_chatApi.SM_FindSession(pUser->wszUsername, m_szModuleName); + if (si == nullptr) { + debugLogA("message to unknown channel %lld ignored", channelId); + return; + } + + ProcessChatUser(pUser, wszUserId, pRoot); + + ParseSpecialChars(si, wszText); + wszText.Replace(L"%", L"%%"); + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE }; + gce.pszID.w = pUser->wszUsername; + gce.dwFlags = GCEF_ADDTOLOG; + gce.pszUID.w = wszUserId; + gce.pszText.w = wszText; + gce.time = (uint32_t)StringToDate(pRoot["timestamp"].as_mstring()); + gce.bIsMe = bOurMessage; + Chat_Event(&gce); + + debugLogW(L"New channel %s message from %s: %s", si->ptszID, gce.pszUID.w, gce.pszText.w); + } + } + + pUser->lastMsgId = msgId; + + SnowFlake lastId = getId(pUser->hContact, DB_KEY_LASTMSGID); // as stored in a database + if (lastId < msgId) + setId(pUser->hContact, DB_KEY_LASTMSGID, msgId); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// someone changed its status + +void CDiscordProto::OnCommandMessageAck(const JSONNode &pRoot) +{ + CDiscordUser *pUser = FindUserByChannel(pRoot["channel_id"]); + if (pUser != nullptr) + pUser->lastMsgId = ::getId(pRoot["message_id"]); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// message deleted + +void CDiscordProto::OnCommandMessageDelete(const JSONNode &pRoot) +{ + if (!m_bSyncDeleteMsgs) + return; + + CMStringA msgid(pRoot["id"].as_mstring()); + if (!msgid.IsEmpty()) { + MEVENT hEvent = db_event_getById(m_szModuleName, msgid); + if (hEvent) + db_event_delete(hEvent); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// +// someone changed its status + +void CDiscordProto::OnCommandPresence(const JSONNode &pRoot) +{ + auto *pGuild = FindGuild(::getId(pRoot["user"]["guild_id"])); + if (pGuild == nullptr) + ProcessPresence(pRoot); + // else + // pGuild->ProcessPresence(pRoot); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// gateway session start + +void CDiscordProto::OnCommandReady(const JSONNode &pRoot) +{ + OnLoggedIn(); + + GatewaySendHeartbeat(); + m_impl.m_heartBeat.StartSafe(m_iHartbeatInterval); + + m_szGatewaySessionId = pRoot["session_id"].as_mstring(); + + if (m_bUseGroupchats) + for (auto &it : pRoot["guilds"]) + ProcessGuild(it); + + for (auto &it : pRoot["relationships"]) { + CDiscordUser *pUser = PrepareUser(it["user"]); + ProcessType(pUser, it); + } + + for (auto &it : pRoot["presences"]) + ProcessPresence(it); + + for (auto &it : pRoot["private_channels"]) + PreparePrivateChannel(it); + + for (auto &it : pRoot["read_state"]) { + CDiscordUser *pUser = FindUserByChannel(::getId(it["id"])); + if (pUser != nullptr) + pUser->lastReadId = ::getId(it["last_message_id"]); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// +// UTN support + +void CDiscordProto::OnCommandTyping(const JSONNode &pRoot) +{ + SnowFlake channelId = ::getId(pRoot["channel_id"]); + debugLogA("user typing notification: channelid=%lld", channelId); + + CDiscordUser *pChannel = FindUserByChannel(channelId); + if (pChannel == nullptr) { + debugLogA("channel with id=%lld is not found", channelId); + return; + } + + // both private groupchats & guild channels are chat rooms for Miranda + if (pChannel->pGuild) { + debugLogA("user is typing in a group channel"); + + CMStringW wszUerId = pRoot["user_id"].as_mstring(); + ProcessGuildUser(pChannel->pGuild, pRoot); // never returns null + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_TYPING }; + gce.pszID.w = pChannel->wszUsername; + gce.pszUID.w = wszUerId; + gce.dwItemData = 1; + gce.time = time(0); + Chat_Event(&gce); + } + else { + debugLogA("user is typing in his private channel"); + CallService(MS_PROTO_CONTACTISTYPING, pChannel->hContact, 20); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// +// User info update + +void CDiscordProto::OnCommandUserUpdate(const JSONNode &pRoot) +{ + SnowFlake id = ::getId(pRoot["id"]); + + MCONTACT hContact; + if (id != m_ownId) { + CDiscordUser *pUser = FindUser(id); + if (pUser == nullptr) + return; + + hContact = pUser->hContact; + } + else hContact = 0; + + // force rereading avatar + CheckAvatarChange(hContact, pRoot["avatar"].as_mstring()); +} + +void CDiscordProto::OnCommandUserSettingsUpdate(const JSONNode &pRoot) +{ + int iStatus = StrToStatus(pRoot["status"].as_mstring()); + if (iStatus != 0) { + int iOldStatus = m_iStatus; m_iStatus = iStatus; + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); + } +} diff --git a/protocols/Discord/src/gateway.cpp b/protocols/Discord/src/gateway.cpp index 82c3b70eb5..0530945c3e 100644 --- a/protocols/Discord/src/gateway.cpp +++ b/protocols/Discord/src/gateway.cpp @@ -1,346 +1,346 @@ -/* -Copyright © 2016-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -////////////////////////////////////////////////////////////////////////////////////// -// sends a piece of JSON to a server via a websocket, masked - -bool CDiscordProto::GatewaySend(const JSONNode &pRoot) -{ - if (m_hGatewayConnection == nullptr) - return false; - - json_string szText = pRoot.write(); - debugLogA("Gateway send: %s", szText.c_str()); - WebSocket_SendText(m_hGatewayConnection, szText.c_str()); - return true; -} - -////////////////////////////////////////////////////////////////////////////////////// -// gateway worker thread - -void CDiscordProto::GatewayThread(void*) -{ - while (GatewayThreadWorker()) - ; - ShutdownSession(); -} - -bool CDiscordProto::GatewayThreadWorker() -{ - NETLIBHTTPHEADER hdrs[] = - { - { "Origin", "https://discord.com" }, - { 0, 0 }, - { 0, 0 }, - }; - - if (!m_szWSCookie.IsEmpty()) { - hdrs[1].szName = "Cookie"; - hdrs[1].szValue = m_szWSCookie.GetBuffer(); - } - - NLHR_PTR pReply(WebSocket_Connect(m_hGatewayNetlibUser, m_szGateway + "/?encoding=json&v=8", hdrs)); - if (pReply == nullptr) { - debugLogA("Gateway connection failed, exiting"); - return false; - } - - if (auto *pszNewCookie = Netlib_GetHeader(pReply, "Set-Cookie")) { - char *p = strchr(pszNewCookie, ';'); - if (p) *p = 0; - - m_szWSCookie = pszNewCookie; - } - - if (pReply->resultCode != 101) { - // if there's no cookie & Miranda is bounced with error 404, simply apply the cookie and try again - if (pReply->resultCode == 404) { - if (hdrs[1].szName == nullptr) - return true; - - m_szWSCookie.Empty(); // don't use the same cookie twice - } - return false; - } - - // succeeded! - debugLogA("Gateway connection succeeded"); - m_hGatewayConnection = pReply->nlc; - - bool bExit = false; - int offset = 0; - MBinBuffer netbuf; - - while (!bExit) { - if (m_bTerminated) - break; - - unsigned char buf[2048]; - int bufSize = Netlib_Recv(m_hGatewayConnection, (char*)buf + offset, _countof(buf) - offset, MSG_NODUMP); - if (bufSize == 0) { - debugLogA("Gateway connection gracefully closed"); - bExit = !m_bTerminated; - break; - } - if (bufSize < 0) { - debugLogA("Gateway connection error, exiting"); - break; - } - - WSHeader hdr; - if (!WebSocket_InitHeader(hdr, buf, bufSize)) { - offset += bufSize; - continue; - } - offset = 0; - - debugLogA("Got packet: buffer = %d, opcode = %d, headerSize = %d, final = %d, masked = %d", bufSize, hdr.opCode, hdr.headerSize, hdr.bIsFinal, hdr.bIsMasked); - - // we have some additional data, not only opcode - if ((size_t)bufSize > hdr.headerSize) { - size_t currPacketSize = bufSize - hdr.headerSize; - netbuf.append(buf, bufSize); - while (currPacketSize < hdr.payloadSize) { - int result = Netlib_Recv(m_hGatewayConnection, (char*)buf, _countof(buf), MSG_NODUMP); - if (result == 0) { - debugLogA("Gateway connection gracefully closed"); - bExit = !m_bTerminated; - break; - } - if (result < 0) { - debugLogA("Gateway connection error, exiting"); - break; - } - currPacketSize += result; - netbuf.append(buf, result); - } - } - - // read all payloads from the current buffer, one by one - size_t prevSize = 0; - while (true) { - switch (hdr.opCode) { - case 0: // text packet - case 1: // binary packet - case 2: // continuation - if (hdr.bIsFinal) { - // process a packet here - CMStringA szJson((char*)netbuf.data() + hdr.headerSize, (int)hdr.payloadSize); - debugLogA("JSON received:\n%s", szJson.c_str()); - JSONNode root = JSONNode::parse(szJson); - if (root) - bExit = GatewayProcess(root); - } - break; - - case 8: // close - debugLogA("server required to exit"); - bExit = true; // simply reconnect, don't exit - break; - - case 9: // ping - debugLogA("ping received"); - Netlib_Send(m_hGatewayConnection, (char*)buf + hdr.headerSize, bufSize - int(hdr.headerSize), 0); - break; - } - - if (hdr.bIsFinal) - netbuf.remove(hdr.headerSize + hdr.payloadSize); - - if (netbuf.length() == 0) - break; - - // if we have not enough data for header, continue reading - if (!WebSocket_InitHeader(hdr, netbuf.data(), netbuf.length())) - break; - - // if we have not enough data for data, continue reading - if (hdr.headerSize + hdr.payloadSize > netbuf.length()) - break; - - debugLogA("Got inner packet: buffer = %d, opcode = %d, headerSize = %d, payloadSize = %d, final = %d, masked = %d", netbuf.length(), hdr.opCode, hdr.headerSize, hdr.payloadSize, hdr.bIsFinal, hdr.bIsMasked); - if (prevSize == netbuf.length()) { - netbuf.remove(prevSize); - debugLogA("dropping current packet, exiting"); - break; - } - - prevSize = netbuf.length(); - } - } - - Netlib_CloseHandle(m_hGatewayConnection); - m_hGatewayConnection = nullptr; - return bExit; -} - -////////////////////////////////////////////////////////////////////////////////////// -// handles server commands - -bool CDiscordProto::GatewayProcess(const JSONNode &pRoot) -{ - int opCode = pRoot["op"].as_int(); - switch (opCode) { - case OPCODE_DISPATCH: // process incoming command - { - int iSeq = pRoot["s"].as_int(); - if (iSeq != 0) - m_iGatewaySeq = iSeq; - - CMStringW wszCommand = pRoot["t"].as_mstring(); - debugLogA("got a server command to dispatch: %S", wszCommand.c_str()); - - GatewayHandlerFunc pFunc = GetHandler(wszCommand); - if (pFunc) - (this->*pFunc)(pRoot["d"]); - } - break; - - case OPCODE_RECONNECT: // we need to reconnect asap - debugLogA("we need to reconnect, leaving worker thread"); - return true; - - case OPCODE_INVALID_SESSION: // session invalidated - if (pRoot["d"].as_bool()) // session can be resumed - GatewaySendResume(); - else { - Sleep(5000); // 5 seconds - recommended timeout - GatewaySendIdentify(); - } - break; - - case OPCODE_HELLO: // hello - m_iHartbeatInterval = pRoot["d"]["heartbeat_interval"].as_int(); - - GatewaySendIdentify(); - break; - - case OPCODE_HEARTBEAT_ACK: // heartbeat ack - break; - - default: - debugLogA("ACHTUNG! Unknown opcode: %d, report it to developer", opCode); - } - - return false; -} - -////////////////////////////////////////////////////////////////////////////////////// -// requests to be sent to a gateway - -void CDiscordProto::GatewaySendGuildInfo(CDiscordGuild *pGuild) -{ - if (!pGuild->arChannels.getCount()) - return; - - JSONNode a1(JSON_ARRAY); a1 << INT_PARAM("", 0) << INT_PARAM("", 99); - - CMStringA szId(FORMAT, "%lld", pGuild->arChannels[0]->id); - JSONNode chl(JSON_ARRAY); chl.set_name(szId.c_str()); chl << a1; - - JSONNode channels; channels.set_name("channels"); channels << chl; - - JSONNode payload; payload.set_name("d"); - payload << SINT64_PARAM("guild_id", pGuild->id) << BOOL_PARAM("typing", true) << BOOL_PARAM("activities", true) << BOOL_PARAM("presences", true) << channels; - - JSONNode root; - root << INT_PARAM("op", OPCODE_REQUEST_SYNC_CHANNEL) << payload; - GatewaySend(root); -} - -void CDiscordProto::GatewaySendHeartbeat() -{ - // we don't send heartbeat packets until we get logged in - if (!m_iHartbeatInterval || !m_iGatewaySeq) - return; - - JSONNode root; - root << INT_PARAM("op", OPCODE_HEARTBEAT) << INT_PARAM("d", m_iGatewaySeq); - GatewaySend(root); -} - -void CDiscordProto::GatewaySendIdentify() -{ - if (m_szAccessToken == nullptr) { - ConnectionFailed(LOGINERR_WRONGPASSWORD); - return; - } - - char szOs[256]; - OS_GetDisplayString(szOs, _countof(szOs)); - - char szVersion[256]; - Miranda_GetVersionText(szVersion, _countof(szVersion)); - - JSONNode props; props.set_name("properties"); - props << CHAR_PARAM("os", szOs) << CHAR_PARAM("browser", "Chrome") << CHAR_PARAM("device", szVersion) - << CHAR_PARAM("referrer", "https://miranda-ng.org") << CHAR_PARAM("referring_domain", "miranda-ng.org"); - - JSONNode payload; payload.set_name("d"); - payload << CHAR_PARAM("token", m_szAccessToken) << props << BOOL_PARAM("compress", false) << INT_PARAM("large_threshold", 250); - - JSONNode root; - root << INT_PARAM("op", OPCODE_IDENTIFY) << payload; - GatewaySend(root); -} - -void CDiscordProto::GatewaySendResume() -{ - char szRandom[40]; - uint8_t random[16]; - Utils_GetRandom(random, _countof(random)); - bin2hex(random, _countof(random), szRandom); - - JSONNode root; - root << CHAR_PARAM("token", szRandom) << CHAR_PARAM("session_id", m_szGatewaySessionId) << INT_PARAM("seq", m_iGatewaySeq); - GatewaySend(root); -} - -bool CDiscordProto::GatewaySendStatus(int iStatus, const wchar_t *pwszStatusText) -{ - if (iStatus == ID_STATUS_OFFLINE) { - Push(new AsyncHttpRequest(this, REQUEST_POST, "/auth/logout", nullptr)); - return true; - } - - const char *pszStatus; - switch (iStatus) { - case ID_STATUS_AWAY: - case ID_STATUS_NA: - pszStatus = "idle"; break; - case ID_STATUS_DND: - pszStatus = "dnd"; break; - case ID_STATUS_INVISIBLE: - pszStatus = "invisible"; break; - default: - pszStatus = "online"; break; - } - - JSONNode payload; payload.set_name("d"); - payload << INT64_PARAM("since", __int64(time(0)) * 1000) << BOOL_PARAM("afk", true) << CHAR_PARAM("status", pszStatus); - if (pwszStatusText == nullptr) - payload << CHAR_PARAM("game", nullptr); - else { - JSONNode game; game.set_name("game"); game << WCHAR_PARAM("name", pwszStatusText) << INT_PARAM("type", 0); - payload << game; - } - - JSONNode root; root << INT_PARAM("op", OPCODE_STATUS_UPDATE) << payload; - return GatewaySend(root); -} +/* +Copyright © 2016-22 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +////////////////////////////////////////////////////////////////////////////////////// +// sends a piece of JSON to a server via a websocket, masked + +bool CDiscordProto::GatewaySend(const JSONNode &pRoot) +{ + if (m_hGatewayConnection == nullptr) + return false; + + json_string szText = pRoot.write(); + debugLogA("Gateway send: %s", szText.c_str()); + WebSocket_SendText(m_hGatewayConnection, szText.c_str()); + return true; +} + +////////////////////////////////////////////////////////////////////////////////////// +// gateway worker thread + +void CDiscordProto::GatewayThread(void*) +{ + while (GatewayThreadWorker()) + ; + ShutdownSession(); +} + +bool CDiscordProto::GatewayThreadWorker() +{ + NETLIBHTTPHEADER hdrs[] = + { + { "Origin", "https://discord.com" }, + { 0, 0 }, + { 0, 0 }, + }; + + if (!m_szWSCookie.IsEmpty()) { + hdrs[1].szName = "Cookie"; + hdrs[1].szValue = m_szWSCookie.GetBuffer(); + } + + NLHR_PTR pReply(WebSocket_Connect(m_hGatewayNetlibUser, m_szGateway + "/?encoding=json&v=8", hdrs)); + if (pReply == nullptr) { + debugLogA("Gateway connection failed, exiting"); + return false; + } + + if (auto *pszNewCookie = Netlib_GetHeader(pReply, "Set-Cookie")) { + char *p = strchr(pszNewCookie, ';'); + if (p) *p = 0; + + m_szWSCookie = pszNewCookie; + } + + if (pReply->resultCode != 101) { + // if there's no cookie & Miranda is bounced with error 404, simply apply the cookie and try again + if (pReply->resultCode == 404) { + if (hdrs[1].szName == nullptr) + return true; + + m_szWSCookie.Empty(); // don't use the same cookie twice + } + return false; + } + + // succeeded! + debugLogA("Gateway connection succeeded"); + m_hGatewayConnection = pReply->nlc; + + bool bExit = false; + int offset = 0; + MBinBuffer netbuf; + + while (!bExit) { + if (m_bTerminated) + break; + + unsigned char buf[2048]; + int bufSize = Netlib_Recv(m_hGatewayConnection, (char*)buf + offset, _countof(buf) - offset, MSG_NODUMP); + if (bufSize == 0) { + debugLogA("Gateway connection gracefully closed"); + bExit = !m_bTerminated; + break; + } + if (bufSize < 0) { + debugLogA("Gateway connection error, exiting"); + break; + } + + WSHeader hdr; + if (!WebSocket_InitHeader(hdr, buf, bufSize)) { + offset += bufSize; + continue; + } + offset = 0; + + debugLogA("Got packet: buffer = %d, opcode = %d, headerSize = %d, final = %d, masked = %d", bufSize, hdr.opCode, hdr.headerSize, hdr.bIsFinal, hdr.bIsMasked); + + // we have some additional data, not only opcode + if ((size_t)bufSize > hdr.headerSize) { + size_t currPacketSize = bufSize - hdr.headerSize; + netbuf.append(buf, bufSize); + while (currPacketSize < hdr.payloadSize) { + int result = Netlib_Recv(m_hGatewayConnection, (char*)buf, _countof(buf), MSG_NODUMP); + if (result == 0) { + debugLogA("Gateway connection gracefully closed"); + bExit = !m_bTerminated; + break; + } + if (result < 0) { + debugLogA("Gateway connection error, exiting"); + break; + } + currPacketSize += result; + netbuf.append(buf, result); + } + } + + // read all payloads from the current buffer, one by one + size_t prevSize = 0; + while (true) { + switch (hdr.opCode) { + case 0: // text packet + case 1: // binary packet + case 2: // continuation + if (hdr.bIsFinal) { + // process a packet here + CMStringA szJson((char*)netbuf.data() + hdr.headerSize, (int)hdr.payloadSize); + debugLogA("JSON received:\n%s", szJson.c_str()); + JSONNode root = JSONNode::parse(szJson); + if (root) + bExit = GatewayProcess(root); + } + break; + + case 8: // close + debugLogA("server required to exit"); + bExit = true; // simply reconnect, don't exit + break; + + case 9: // ping + debugLogA("ping received"); + Netlib_Send(m_hGatewayConnection, (char*)buf + hdr.headerSize, bufSize - int(hdr.headerSize), 0); + break; + } + + if (hdr.bIsFinal) + netbuf.remove(hdr.headerSize + hdr.payloadSize); + + if (netbuf.length() == 0) + break; + + // if we have not enough data for header, continue reading + if (!WebSocket_InitHeader(hdr, netbuf.data(), netbuf.length())) + break; + + // if we have not enough data for data, continue reading + if (hdr.headerSize + hdr.payloadSize > netbuf.length()) + break; + + debugLogA("Got inner packet: buffer = %d, opcode = %d, headerSize = %d, payloadSize = %d, final = %d, masked = %d", netbuf.length(), hdr.opCode, hdr.headerSize, hdr.payloadSize, hdr.bIsFinal, hdr.bIsMasked); + if (prevSize == netbuf.length()) { + netbuf.remove(prevSize); + debugLogA("dropping current packet, exiting"); + break; + } + + prevSize = netbuf.length(); + } + } + + Netlib_CloseHandle(m_hGatewayConnection); + m_hGatewayConnection = nullptr; + return bExit; +} + +////////////////////////////////////////////////////////////////////////////////////// +// handles server commands + +bool CDiscordProto::GatewayProcess(const JSONNode &pRoot) +{ + int opCode = pRoot["op"].as_int(); + switch (opCode) { + case OPCODE_DISPATCH: // process incoming command + { + int iSeq = pRoot["s"].as_int(); + if (iSeq != 0) + m_iGatewaySeq = iSeq; + + CMStringW wszCommand = pRoot["t"].as_mstring(); + debugLogA("got a server command to dispatch: %S", wszCommand.c_str()); + + GatewayHandlerFunc pFunc = GetHandler(wszCommand); + if (pFunc) + (this->*pFunc)(pRoot["d"]); + } + break; + + case OPCODE_RECONNECT: // we need to reconnect asap + debugLogA("we need to reconnect, leaving worker thread"); + return true; + + case OPCODE_INVALID_SESSION: // session invalidated + if (pRoot["d"].as_bool()) // session can be resumed + GatewaySendResume(); + else { + Sleep(5000); // 5 seconds - recommended timeout + GatewaySendIdentify(); + } + break; + + case OPCODE_HELLO: // hello + m_iHartbeatInterval = pRoot["d"]["heartbeat_interval"].as_int(); + + GatewaySendIdentify(); + break; + + case OPCODE_HEARTBEAT_ACK: // heartbeat ack + break; + + default: + debugLogA("ACHTUNG! Unknown opcode: %d, report it to developer", opCode); + } + + return false; +} + +////////////////////////////////////////////////////////////////////////////////////// +// requests to be sent to a gateway + +void CDiscordProto::GatewaySendGuildInfo(CDiscordGuild *pGuild) +{ + if (!pGuild->arChannels.getCount()) + return; + + JSONNode a1(JSON_ARRAY); a1 << INT_PARAM("", 0) << INT_PARAM("", 99); + + CMStringA szId(FORMAT, "%lld", pGuild->arChannels[0]->id); + JSONNode chl(JSON_ARRAY); chl.set_name(szId.c_str()); chl << a1; + + JSONNode channels; channels.set_name("channels"); channels << chl; + + JSONNode payload; payload.set_name("d"); + payload << SINT64_PARAM("guild_id", pGuild->id) << BOOL_PARAM("typing", true) << BOOL_PARAM("activities", true) << BOOL_PARAM("presences", true) << channels; + + JSONNode root; + root << INT_PARAM("op", OPCODE_REQUEST_SYNC_CHANNEL) << payload; + GatewaySend(root); +} + +void CDiscordProto::GatewaySendHeartbeat() +{ + // we don't send heartbeat packets until we get logged in + if (!m_iHartbeatInterval || !m_iGatewaySeq) + return; + + JSONNode root; + root << INT_PARAM("op", OPCODE_HEARTBEAT) << INT_PARAM("d", m_iGatewaySeq); + GatewaySend(root); +} + +void CDiscordProto::GatewaySendIdentify() +{ + if (m_szAccessToken == nullptr) { + ConnectionFailed(LOGINERR_WRONGPASSWORD); + return; + } + + char szOs[256]; + OS_GetDisplayString(szOs, _countof(szOs)); + + char szVersion[256]; + Miranda_GetVersionText(szVersion, _countof(szVersion)); + + JSONNode props; props.set_name("properties"); + props << CHAR_PARAM("os", szOs) << CHAR_PARAM("browser", "Chrome") << CHAR_PARAM("device", szVersion) + << CHAR_PARAM("referrer", "https://miranda-ng.org") << CHAR_PARAM("referring_domain", "miranda-ng.org"); + + JSONNode payload; payload.set_name("d"); + payload << CHAR_PARAM("token", m_szAccessToken) << props << BOOL_PARAM("compress", false) << INT_PARAM("large_threshold", 250); + + JSONNode root; + root << INT_PARAM("op", OPCODE_IDENTIFY) << payload; + GatewaySend(root); +} + +void CDiscordProto::GatewaySendResume() +{ + char szRandom[40]; + uint8_t random[16]; + Utils_GetRandom(random, _countof(random)); + bin2hex(random, _countof(random), szRandom); + + JSONNode root; + root << CHAR_PARAM("token", szRandom) << CHAR_PARAM("session_id", m_szGatewaySessionId) << INT_PARAM("seq", m_iGatewaySeq); + GatewaySend(root); +} + +bool CDiscordProto::GatewaySendStatus(int iStatus, const wchar_t *pwszStatusText) +{ + if (iStatus == ID_STATUS_OFFLINE) { + Push(new AsyncHttpRequest(this, REQUEST_POST, "/auth/logout", nullptr)); + return true; + } + + const char *pszStatus; + switch (iStatus) { + case ID_STATUS_AWAY: + case ID_STATUS_NA: + pszStatus = "idle"; break; + case ID_STATUS_DND: + pszStatus = "dnd"; break; + case ID_STATUS_INVISIBLE: + pszStatus = "invisible"; break; + default: + pszStatus = "online"; break; + } + + JSONNode payload; payload.set_name("d"); + payload << INT64_PARAM("since", __int64(time(0)) * 1000) << BOOL_PARAM("afk", true) << CHAR_PARAM("status", pszStatus); + if (pwszStatusText == nullptr) + payload << CHAR_PARAM("game", nullptr); + else { + JSONNode game; game.set_name("game"); game << WCHAR_PARAM("name", pwszStatusText) << INT_PARAM("type", 0); + payload << game; + } + + JSONNode root; root << INT_PARAM("op", OPCODE_STATUS_UPDATE) << payload; + return GatewaySend(root); +} diff --git a/protocols/Discord/src/groupchat.cpp b/protocols/Discord/src/groupchat.cpp index f34e35c93a..146f8de1fe 100644 --- a/protocols/Discord/src/groupchat.cpp +++ b/protocols/Discord/src/groupchat.cpp @@ -1,235 +1,235 @@ -/* -Copyright © 2016-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -enum { - IDM_CANCEL, - IDM_COPY_ID, - - IDM_CHANGENICK, IDM_CHANGETOPIC, IDM_RENAME, IDM_DESTROY -}; - -///////////////////////////////////////////////////////////////////////////////////////// - -void BuildStatusList(const CDiscordGuild *pGuild, SESSION_INFO *si) -{ - Chat_AddGroup(si, L"@owner"); - - for (auto &it : pGuild->arRoles) - Chat_AddGroup(si, it->wszName); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static gc_item sttLogListItems[] = -{ - { LPGENW("Change &nickname"), IDM_CHANGENICK, MENU_ITEM }, - { LPGENW("Channel control"), FALSE, MENU_NEWPOPUP }, - { LPGENW("Change &topic"), IDM_CHANGETOPIC, MENU_POPUPITEM }, - { LPGENW("&Rename channel"), IDM_RENAME, MENU_POPUPITEM }, - { nullptr, 0, MENU_POPUPSEPARATOR }, - { LPGENW("&Destroy channel"), IDM_DESTROY, MENU_POPUPITEM }, -}; - -static gc_item sttNicklistItems[] = -{ - { LPGENW("Copy ID"), IDM_COPY_ID, MENU_ITEM }, -}; - -int CDiscordProto::GroupchatMenuHook(WPARAM, LPARAM lParam) -{ - GCMENUITEMS* gcmi = (GCMENUITEMS*)lParam; - if (gcmi == nullptr) - return 0; - - if (mir_strcmpi(gcmi->pszModule, m_szModuleName)) - return 0; - - CDiscordUser *pChat = FindUserByChannel(_wtoi64(gcmi->pszID)); - if (pChat == nullptr) - return 0; - - if (gcmi->Type == MENU_ON_LOG) - Chat_AddMenuItems(gcmi->hMenu, _countof(sttLogListItems), sttLogListItems, &g_plugin); - else if (gcmi->Type == MENU_ON_NICKLIST) - Chat_AddMenuItems(gcmi->hMenu, _countof(sttNicklistItems), sttNicklistItems, &g_plugin); - - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CDiscordProto::Chat_SendPrivateMessage(GCHOOK *gch) -{ - SnowFlake userId = _wtoi64(gch->ptszUID); - - MCONTACT hContact; - CDiscordUser *pUser = FindUser(userId); - if (pUser == nullptr) { - PROTOSEARCHRESULT psr = { sizeof(psr) }; - psr.id.w = (wchar_t*)gch->ptszUID; - psr.nick.w = (wchar_t*)gch->ptszNick; - if ((hContact = AddToList(PALF_TEMPORARY, &psr)) == 0) - return; - - setId(hContact, DB_KEY_ID, userId); - setId(hContact, DB_KEY_CHANNELID, _wtoi64(gch->si->ptszID)); - setWString(hContact, DB_KEY_NICK, gch->ptszNick); - Contact::Hide(hContact); - db_set_dw(hContact, "Ignore", "Mask1", 0); - } - else hContact = pUser->hContact; - - CallService(MS_MSG_SENDMESSAGE, hContact, 0); -} - -void CDiscordProto::Chat_ProcessLogMenu(GCHOOK *gch) -{ - CDiscordUser *pUser = FindUserByChannel(_wtoi64(gch->si->ptszID)); - if (pUser == nullptr) - return; - - ENTER_STRING es = {}; - es.szModuleName = m_szModuleName; - - switch (gch->dwData) { - case IDM_DESTROY: - if (IDYES == MessageBox(nullptr, TranslateT("Do you really want to destroy this channel? This action is non-revertable."), m_tszUserName, MB_YESNO | MB_ICONQUESTION)) { - CMStringA szUrl(FORMAT, "/channels/%S", pUser->wszUsername.c_str()); - Push(new AsyncHttpRequest(this, REQUEST_DELETE, szUrl, nullptr)); - } - break; - - case IDM_RENAME: - es.caption = TranslateT("Enter new channel name:"); - es.type = ESF_COMBO; - es.szDataPrefix = "chat_rename"; - if (EnterString(&es)) { - JSONNode root; root << WCHAR_PARAM("name", es.ptszResult); - CMStringA szUrl(FORMAT, "/channels/%S", pUser->wszUsername.c_str()); - Push(new AsyncHttpRequest(this, REQUEST_PATCH, szUrl, nullptr, &root)); - mir_free(es.ptszResult); - } - break; - - case IDM_CHANGETOPIC: - es.caption = TranslateT("Enter new topic:"); - es.type = ESF_RICHEDIT; - es.szDataPrefix = "chat_topic"; - if (EnterString(&es)) { - JSONNode root; root << WCHAR_PARAM("topic", es.ptszResult); - CMStringA szUrl(FORMAT, "/channels/%S", pUser->wszUsername.c_str()); - Push(new AsyncHttpRequest(this, REQUEST_PATCH, szUrl, nullptr, &root)); - mir_free(es.ptszResult); - } - break; - - case IDM_CHANGENICK: - es.caption = TranslateT("Enter your new nick name:"); - es.type = ESF_COMBO; - es.szDataPrefix = "chat_nick"; - es.recentCount = 5; - if (EnterString(&es)) { - JSONNode root; root << WCHAR_PARAM("nick", es.ptszResult); - CMStringA szUrl(FORMAT, "/guilds/%lld/members/@me/nick", pUser->pGuild->id); - Push(new AsyncHttpRequest(this, REQUEST_PATCH, szUrl, nullptr, &root)); - mir_free(es.ptszResult); - } - break; - } -} - -void CDiscordProto::Chat_ProcessNickMenu(GCHOOK* gch) -{ - auto *pChannel = FindUserByChannel(_wtoi64(gch->si->ptszID)); - if (pChannel == nullptr || pChannel->pGuild == nullptr) - return; - - auto* pUser = pChannel->pGuild->FindUser(_wtoi64(gch->ptszUID)); - if (pUser == nullptr) - return; - - switch (gch->dwData) { - case IDM_COPY_ID: - CopyId(pUser->wszDiscordId); - break; - } -} - -int CDiscordProto::GroupchatEventHook(WPARAM, LPARAM lParam) -{ - GCHOOK *gch = (GCHOOK*)lParam; - if (gch == nullptr) - return 0; - - if (mir_strcmpi(gch->si->pszModule, m_szModuleName)) - return 0; - - switch (gch->iType) { - case GC_USER_MESSAGE: - if (m_bOnline && mir_wstrlen(gch->ptszText) > 0) { - CMStringW wszText(gch->ptszText); - wszText.TrimRight(); - - int pos = wszText.Find(':'); - if (pos != -1) { - auto wszWord = wszText.Left(pos); - wszWord.Trim(); - if (auto *si = g_chatApi.SM_FindSession(gch->si->ptszID, gch->si->pszModule)) { - USERINFO *pUser = nullptr; - - for (auto &U : si->getUserList()) - if (wszWord == U->pszNick) { - pUser = U; - break; - } - - if (pUser) { - wszText.Delete(0, pos); - wszText.Insert(0, L"<@" + CMStringW(pUser->pszUID) + L">"); - } - } - } - - Chat_UnescapeTags(wszText.GetBuffer()); - - JSONNode body; body << WCHAR_PARAM("content", wszText); - CMStringA szUrl(FORMAT, "/channels/%S/messages", gch->si->ptszID); - Push(new AsyncHttpRequest(this, REQUEST_POST, szUrl, nullptr, &body)); - } - break; - - case GC_USER_PRIVMESS: - Chat_SendPrivateMessage(gch); - break; - - case GC_USER_LOGMENU: - Chat_ProcessLogMenu(gch); - break; - - case GC_USER_NICKLISTMENU: - Chat_ProcessNickMenu(gch); - break; - - case GC_USER_TYPNOTIFY: - UserIsTyping(gch->si->hContact, (int)gch->dwData); - break; - } - - return 1; -} +/* +Copyright © 2016-22 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +enum { + IDM_CANCEL, + IDM_COPY_ID, + + IDM_CHANGENICK, IDM_CHANGETOPIC, IDM_RENAME, IDM_DESTROY +}; + +///////////////////////////////////////////////////////////////////////////////////////// + +void BuildStatusList(const CDiscordGuild *pGuild, SESSION_INFO *si) +{ + Chat_AddGroup(si, L"@owner"); + + for (auto &it : pGuild->arRoles) + Chat_AddGroup(si, it->wszName); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static gc_item sttLogListItems[] = +{ + { LPGENW("Change &nickname"), IDM_CHANGENICK, MENU_ITEM }, + { LPGENW("Channel control"), FALSE, MENU_NEWPOPUP }, + { LPGENW("Change &topic"), IDM_CHANGETOPIC, MENU_POPUPITEM }, + { LPGENW("&Rename channel"), IDM_RENAME, MENU_POPUPITEM }, + { nullptr, 0, MENU_POPUPSEPARATOR }, + { LPGENW("&Destroy channel"), IDM_DESTROY, MENU_POPUPITEM }, +}; + +static gc_item sttNicklistItems[] = +{ + { LPGENW("Copy ID"), IDM_COPY_ID, MENU_ITEM }, +}; + +int CDiscordProto::GroupchatMenuHook(WPARAM, LPARAM lParam) +{ + GCMENUITEMS* gcmi = (GCMENUITEMS*)lParam; + if (gcmi == nullptr) + return 0; + + if (mir_strcmpi(gcmi->pszModule, m_szModuleName)) + return 0; + + CDiscordUser *pChat = FindUserByChannel(_wtoi64(gcmi->pszID)); + if (pChat == nullptr) + return 0; + + if (gcmi->Type == MENU_ON_LOG) + Chat_AddMenuItems(gcmi->hMenu, _countof(sttLogListItems), sttLogListItems, &g_plugin); + else if (gcmi->Type == MENU_ON_NICKLIST) + Chat_AddMenuItems(gcmi->hMenu, _countof(sttNicklistItems), sttNicklistItems, &g_plugin); + + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordProto::Chat_SendPrivateMessage(GCHOOK *gch) +{ + SnowFlake userId = _wtoi64(gch->ptszUID); + + MCONTACT hContact; + CDiscordUser *pUser = FindUser(userId); + if (pUser == nullptr) { + PROTOSEARCHRESULT psr = { sizeof(psr) }; + psr.id.w = (wchar_t*)gch->ptszUID; + psr.nick.w = (wchar_t*)gch->ptszNick; + if ((hContact = AddToList(PALF_TEMPORARY, &psr)) == 0) + return; + + setId(hContact, DB_KEY_ID, userId); + setId(hContact, DB_KEY_CHANNELID, _wtoi64(gch->si->ptszID)); + setWString(hContact, DB_KEY_NICK, gch->ptszNick); + Contact::Hide(hContact); + db_set_dw(hContact, "Ignore", "Mask1", 0); + } + else hContact = pUser->hContact; + + CallService(MS_MSG_SENDMESSAGE, hContact, 0); +} + +void CDiscordProto::Chat_ProcessLogMenu(GCHOOK *gch) +{ + CDiscordUser *pUser = FindUserByChannel(_wtoi64(gch->si->ptszID)); + if (pUser == nullptr) + return; + + ENTER_STRING es = {}; + es.szModuleName = m_szModuleName; + + switch (gch->dwData) { + case IDM_DESTROY: + if (IDYES == MessageBox(nullptr, TranslateT("Do you really want to destroy this channel? This action is non-revertable."), m_tszUserName, MB_YESNO | MB_ICONQUESTION)) { + CMStringA szUrl(FORMAT, "/channels/%S", pUser->wszUsername.c_str()); + Push(new AsyncHttpRequest(this, REQUEST_DELETE, szUrl, nullptr)); + } + break; + + case IDM_RENAME: + es.caption = TranslateT("Enter new channel name:"); + es.type = ESF_COMBO; + es.szDataPrefix = "chat_rename"; + if (EnterString(&es)) { + JSONNode root; root << WCHAR_PARAM("name", es.ptszResult); + CMStringA szUrl(FORMAT, "/channels/%S", pUser->wszUsername.c_str()); + Push(new AsyncHttpRequest(this, REQUEST_PATCH, szUrl, nullptr, &root)); + mir_free(es.ptszResult); + } + break; + + case IDM_CHANGETOPIC: + es.caption = TranslateT("Enter new topic:"); + es.type = ESF_RICHEDIT; + es.szDataPrefix = "chat_topic"; + if (EnterString(&es)) { + JSONNode root; root << WCHAR_PARAM("topic", es.ptszResult); + CMStringA szUrl(FORMAT, "/channels/%S", pUser->wszUsername.c_str()); + Push(new AsyncHttpRequest(this, REQUEST_PATCH, szUrl, nullptr, &root)); + mir_free(es.ptszResult); + } + break; + + case IDM_CHANGENICK: + es.caption = TranslateT("Enter your new nick name:"); + es.type = ESF_COMBO; + es.szDataPrefix = "chat_nick"; + es.recentCount = 5; + if (EnterString(&es)) { + JSONNode root; root << WCHAR_PARAM("nick", es.ptszResult); + CMStringA szUrl(FORMAT, "/guilds/%lld/members/@me/nick", pUser->pGuild->id); + Push(new AsyncHttpRequest(this, REQUEST_PATCH, szUrl, nullptr, &root)); + mir_free(es.ptszResult); + } + break; + } +} + +void CDiscordProto::Chat_ProcessNickMenu(GCHOOK* gch) +{ + auto *pChannel = FindUserByChannel(_wtoi64(gch->si->ptszID)); + if (pChannel == nullptr || pChannel->pGuild == nullptr) + return; + + auto* pUser = pChannel->pGuild->FindUser(_wtoi64(gch->ptszUID)); + if (pUser == nullptr) + return; + + switch (gch->dwData) { + case IDM_COPY_ID: + CopyId(pUser->wszDiscordId); + break; + } +} + +int CDiscordProto::GroupchatEventHook(WPARAM, LPARAM lParam) +{ + GCHOOK *gch = (GCHOOK*)lParam; + if (gch == nullptr) + return 0; + + if (mir_strcmpi(gch->si->pszModule, m_szModuleName)) + return 0; + + switch (gch->iType) { + case GC_USER_MESSAGE: + if (m_bOnline && mir_wstrlen(gch->ptszText) > 0) { + CMStringW wszText(gch->ptszText); + wszText.TrimRight(); + + int pos = wszText.Find(':'); + if (pos != -1) { + auto wszWord = wszText.Left(pos); + wszWord.Trim(); + if (auto *si = g_chatApi.SM_FindSession(gch->si->ptszID, gch->si->pszModule)) { + USERINFO *pUser = nullptr; + + for (auto &U : si->getUserList()) + if (wszWord == U->pszNick) { + pUser = U; + break; + } + + if (pUser) { + wszText.Delete(0, pos); + wszText.Insert(0, L"<@" + CMStringW(pUser->pszUID) + L">"); + } + } + } + + Chat_UnescapeTags(wszText.GetBuffer()); + + JSONNode body; body << WCHAR_PARAM("content", wszText); + CMStringA szUrl(FORMAT, "/channels/%S/messages", gch->si->ptszID); + Push(new AsyncHttpRequest(this, REQUEST_POST, szUrl, nullptr, &body)); + } + break; + + case GC_USER_PRIVMESS: + Chat_SendPrivateMessage(gch); + break; + + case GC_USER_LOGMENU: + Chat_ProcessLogMenu(gch); + break; + + case GC_USER_NICKLISTMENU: + Chat_ProcessNickMenu(gch); + break; + + case GC_USER_TYPNOTIFY: + UserIsTyping(gch->si->hContact, (int)gch->dwData); + break; + } + + return 1; +} diff --git a/protocols/Discord/src/guilds.cpp b/protocols/Discord/src/guilds.cpp index d05ff80863..760437ceb0 100644 --- a/protocols/Discord/src/guilds.cpp +++ b/protocols/Discord/src/guilds.cpp @@ -1,413 +1,413 @@ -/* -Copyright © 2016-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -int compareUsers(const CDiscordUser *p1, const CDiscordUser *p2); - -static int compareRoles(const CDiscordRole *p1, const CDiscordRole *p2) -{ - return compareInt64(p1->id, p2->id); -} - -static int compareChatUsers(const CDiscordGuildMember *p1, const CDiscordGuildMember *p2) -{ - return compareInt64(p1->userId, p2->userId); -} - -CDiscordGuild::CDiscordGuild(SnowFlake _id) : - id(_id), - arChannels(10, compareUsers), - arChatUsers(30, compareChatUsers), - arRoles(10, compareRoles) -{ -} - -CDiscordGuild::~CDiscordGuild() -{ -} - -CDiscordUser::~CDiscordUser() -{ - if (pGuild != nullptr) - pGuild->arChannels.remove(this); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// reads a presence block from json - -void CDiscordProto::ProcessPresence(const JSONNode &root) -{ - auto userId = ::getId(root["user"]["id"]); - CDiscordUser *pUser = FindUser(userId); - if (pUser == nullptr) { - debugLogA("Presence from unknown user id %lld ignored", userId); - return; - } - - setWord(pUser->hContact, "Status", StrToStatus(root["status"].as_mstring())); - - CheckAvatarChange(pUser->hContact, root["user"]["avatar"].as_mstring()); - - for (auto &act : root["activities"]) { - CMStringW wszStatus(act["state"].as_mstring()); - if (!wszStatus.IsEmpty()) - db_set_ws(pUser->hContact, "CList", "StatusMsg", wszStatus); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// -// reads a role from json - -void CDiscordProto::ProcessRole(CDiscordGuild *guild, const JSONNode &role) -{ - SnowFlake id = ::getId(role["id"]); - CDiscordRole *p = guild->arRoles.find((CDiscordRole*)&id); - if (p == nullptr) { - p = new CDiscordRole(); - p->id = id; - guild->arRoles.insert(p); - } - - p->color = role["color"].as_int(); - p->position = role["position"].as_int(); - p->permissions = role["permissions"].as_int(); - p->wszName = role["name"].as_mstring(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static void sttSetGroupName(MCONTACT hContact, const wchar_t *pwszGroupName) -{ - ptrW wszOldName(Clist_GetGroup(hContact)); - if (wszOldName != nullptr) { - ptrW wszChatGroup(Chat_GetGroup()); - if (mir_wstrcmpi(wszOldName, wszChatGroup)) - return; // custom group, don't touch it - } - - Clist_SetGroup(hContact, pwszGroupName); -} - -void CDiscordProto::BatchChatCreate(void *param) -{ - CDiscordGuild *pGuild = (CDiscordGuild*)param; - - for (auto &it : pGuild->arChannels) - if (!it->bIsPrivate && !it->bIsGroup) - CreateChat(pGuild, it); -} - -void CDiscordProto::CreateChat(CDiscordGuild *pGuild, CDiscordUser *pUser) -{ - SESSION_INFO *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, pUser->wszUsername, pUser->wszChannelName); - si->pParent = pGuild->pParentSi; - pUser->hContact = si->hContact; - setId(pUser->hContact, DB_KEY_ID, pUser->channelId); - setId(pUser->hContact, DB_KEY_CHANNELID, pUser->channelId); - - SnowFlake oldMsgId = getId(pUser->hContact, DB_KEY_LASTMSGID); - if (oldMsgId == 0) - RetrieveHistory(pUser, MSG_BEFORE, pUser->lastMsgId, 20); - else if (!pUser->bSynced && pUser->lastMsgId > oldMsgId) { - pUser->bSynced = true; - RetrieveHistory(pUser, MSG_AFTER, oldMsgId, 99); - } - - if (m_bUseGuildGroups) { - if (pUser->parentId) { - CDiscordUser *pParent = FindUserByChannel(pUser->parentId); - if (pParent != nullptr) - sttSetGroupName(pUser->hContact, pParent->wszChannelName); - } - else sttSetGroupName(pUser->hContact, Clist_GroupGetName(pGuild->groupId)); - } - - BuildStatusList(pGuild, si); - - Chat_Control(m_szModuleName, pUser->wszUsername, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE); - Chat_Control(m_szModuleName, pUser->wszUsername, SESSION_ONLINE); - - if (!pUser->wszTopic.IsEmpty()) { - Chat_SetStatusbarText(m_szModuleName, pUser->wszUsername, pUser->wszTopic); - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_TOPIC }; - gce.pszID.w = pUser->wszUsername; - gce.time = time(0); - gce.pszText.w = pUser->wszTopic; - Chat_Event(&gce); - } -} - -void CDiscordProto::ProcessGuild(const JSONNode &pRoot) -{ - SnowFlake guildId = ::getId(pRoot["id"]); - - CDiscordGuild *pGuild = FindGuild(guildId); - if (pGuild == nullptr) { - pGuild = new CDiscordGuild(guildId); - pGuild->LoadFromFile(); - arGuilds.insert(pGuild); - } - - pGuild->ownerId = ::getId(pRoot["owner_id"]); - pGuild->wszName = pRoot["name"].as_mstring(); - if (m_bUseGuildGroups) - pGuild->groupId = Clist_GroupCreate(Clist_GroupExists(m_wszDefaultGroup), pGuild->wszName); - - SESSION_INFO *si = Chat_NewSession(GCW_SERVER, m_szModuleName, pGuild->wszName, pGuild->wszName, pGuild); - if (si == nullptr) - return; - - pGuild->pParentSi = (SESSION_INFO*)si; - pGuild->hContact = si->hContact; - setId(pGuild->hContact, DB_KEY_CHANNELID, guildId); - - Chat_Control(m_szModuleName, pGuild->wszName, WINDOW_HIDDEN); - Chat_Control(m_szModuleName, pGuild->wszName, SESSION_ONLINE); - - for (auto &it : pRoot["roles"]) - ProcessRole(pGuild, it); - - BuildStatusList(pGuild, si); - - for (auto &it : pRoot["channels"]) - ProcessGuildChannel(pGuild, it); - - if (!pGuild->bSynced && getByte(si->hContact, "EnableSync")) - GatewaySendGuildInfo(pGuild); - - // store all guild members - for (auto &it : pRoot["members"]) { - auto *pm = ProcessGuildUser(pGuild, it); - - CMStringW wszNick = it["nick"].as_mstring(); - if (!wszNick.IsEmpty()) - pm->wszNick = wszNick; - - pm->iStatus = ID_STATUS_OFFLINE; - } - - // parse online statuses - for (auto &it : pRoot["presences"]) { - CDiscordGuildMember *gm = pGuild->FindUser(::getId(it["user"]["id"])); - if (gm != nullptr) - gm->iStatus = StrToStatus(it["status"].as_mstring()); - } - - for (auto &it : pGuild->arChatUsers) - AddGuildUser(pGuild, *it); - - if (!m_bTerminated) - ForkThread(&CDiscordProto::BatchChatCreate, pGuild); - - pGuild->bSynced = true; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -CDiscordUser* CDiscordProto::ProcessGuildChannel(CDiscordGuild *pGuild, const JSONNode &pch) -{ - CMStringW wszChannelId = pch["id"].as_mstring(); - SnowFlake channelId = _wtoi64(wszChannelId); - CMStringW wszName = pch["name"].as_mstring(); - CDiscordUser *pUser; - - // filter our all channels but the text ones - switch (pch["type"].as_int()) { - case 4: // channel group - if (!m_bUseGuildGroups) // ignore groups when they aren't enabled - return nullptr; - - pUser = FindUserByChannel(channelId); - if (pUser == nullptr) { - // missing channel - create it - pUser = new CDiscordUser(channelId); - pUser->bIsPrivate = false; - pUser->channelId = channelId; - pUser->bIsGroup = true; - arUsers.insert(pUser); - - pGuild->arChannels.insert(pUser); - - MGROUP grpId = Clist_GroupCreate(pGuild->groupId, wszName); - pUser->wszChannelName = Clist_GroupGetName(grpId); - } - return pUser; - - case 0: // text channel - pUser = FindUserByChannel(channelId); - if (pUser == nullptr) { - // missing channel - create it - pUser = new CDiscordUser(channelId); - pUser->bIsPrivate = false; - pUser->channelId = channelId; - arUsers.insert(pUser); - } - - if (pGuild->arChannels.find(pUser) == nullptr) - pGuild->arChannels.insert(pUser); - - pUser->wszUsername = wszChannelId; - if (m_bUseGuildGroups) - pUser->wszChannelName = L"#" + wszName; - else - pUser->wszChannelName = pGuild->wszName + L"#" + wszName; - pUser->wszTopic = pch["topic"].as_mstring(); - pUser->pGuild = pGuild; - pUser->lastMsgId = ::getId(pch["last_message_id"]); - pUser->parentId = _wtoi64(pch["parent_id"].as_mstring()); - return pUser; - } - - return nullptr; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -CDiscordGuildMember* CDiscordProto::ProcessGuildUser(CDiscordGuild *pGuild, const JSONNode &pRoot, bool *pbNew) -{ - auto& pUser = pRoot["user"]; - - bool bNew = false; - CMStringW wszUserId = pUser["id"].as_mstring(); - SnowFlake userId = _wtoi64(wszUserId); - CDiscordGuildMember *pm = pGuild->FindUser(userId); - if (pm == nullptr) { - pm = new CDiscordGuildMember(userId); - pGuild->arChatUsers.insert(pm); - bNew = true; - } - - pm->wszDiscordId = pUser["username"].as_mstring() + L"#" + pUser["discriminator"].as_mstring(); - pm->wszNick = pRoot["nick"].as_mstring(); - if (pm->wszNick.IsEmpty()) - pm->wszNick = pUser["username"].as_mstring(); - else - bNew = true; - - if (userId == pGuild->ownerId) - pm->wszRole = L"@owner"; - else { - CDiscordRole *pRole = nullptr; - for (auto &itr : pRoot["roles"]) { - SnowFlake roleId = ::getId(itr); - if (pRole = pGuild->arRoles.find((CDiscordRole *)&roleId)) - break; - } - pm->wszRole = (pRole == nullptr) ? L"@everyone" : pRole->wszName; - } - - if (pbNew) - *pbNew = bNew; - return pm; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CDiscordProto::ProcessChatUser(CDiscordUser *pChat, const CMStringW &wszUserId, const JSONNode &pRoot) -{ - // input data control - SnowFlake userId = _wtoi64(wszUserId); - CDiscordGuild *pGuild = pChat->pGuild; - if (pGuild == nullptr || userId == 0) - return; - - // does user exist? if yes, there's nothing to do - auto *pm = pGuild->FindUser(userId); - if (pm != nullptr) - return; - - // otherwise let's create a user and insert him into all guild's chats - pm = new CDiscordGuildMember(userId); - pm->wszDiscordId = pRoot["author"]["username"].as_mstring() + L"#" + pRoot["author"]["discriminator"].as_mstring(); - pm->wszNick = pRoot["nick"].as_mstring(); - if (pm->wszNick.IsEmpty()) - pm->wszNick = pRoot["author"]["username"].as_mstring(); - pGuild->arChatUsers.insert(pm); - - debugLogA("add missing user to chat: id=%lld, nick=%S", userId, pm->wszNick.c_str()); - AddGuildUser(pGuild, *pm); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CDiscordProto::AddGuildUser(CDiscordGuild *pGuild, const CDiscordGuildMember &pUser) -{ - int flags = 0; - switch (pUser.iStatus) { - case ID_STATUS_ONLINE: case ID_STATUS_NA: case ID_STATUS_DND: - flags = 1; - break; - } - - auto *pStatus = g_chatApi.TM_FindStatus(pGuild->pParentSi->pStatuses, pUser.wszRole); - - wchar_t wszUserId[100]; - _i64tow_s(pUser.userId, wszUserId, _countof(wszUserId), 10); - - auto *pu = g_chatApi.UM_AddUser(pGuild->pParentSi, wszUserId, pUser.wszNick, (pStatus) ? pStatus->iStatus : 0); - pu->iStatusEx = flags; - if (pUser.userId == m_ownId) - pGuild->pParentSi->pMe = pu; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CDiscordGuild::LoadFromFile() -{ - int fileNo = _wopen(GetCacheFile(), O_TEXT | O_RDONLY); - if (fileNo == -1) - return; - - int fSize = ::filelength(fileNo); - ptrA json((char*)mir_alloc(fSize + 1)); - read(fileNo, json, fSize); - close(fileNo); - - JSONNode cached = JSONNode::parse(json); - for (auto &it : cached) { - SnowFlake userId = getId(it["id"]); - auto *pUser = FindUser(userId); - if (pUser == nullptr) { - pUser = new CDiscordGuildMember(userId); - arChatUsers.insert(pUser); - } - - pUser->wszNick = it["n"].as_mstring(); - pUser->wszRole = it["r"].as_mstring(); - } -} - -void CDiscordGuild ::SaveToFile() -{ - JSONNode members(JSON_ARRAY); - for (auto &it : arChatUsers) { - JSONNode member; - member << INT64_PARAM("id", it->userId) << WCHAR_PARAM("n", it->wszNick) << WCHAR_PARAM("r", it->wszRole); - members << member; - } - - CMStringW wszFileName(GetCacheFile()); - CreatePathToFileW(wszFileName); - int fileNo = _wopen(wszFileName, O_CREAT | O_TRUNC | O_TEXT | O_WRONLY); - if (fileNo != -1) { - std::string json = members.write_formatted(); - write(fileNo, json.c_str(), (int)json.size()); - close(fileNo); - } -} +/* +Copyright © 2016-22 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +int compareUsers(const CDiscordUser *p1, const CDiscordUser *p2); + +static int compareRoles(const CDiscordRole *p1, const CDiscordRole *p2) +{ + return compareInt64(p1->id, p2->id); +} + +static int compareChatUsers(const CDiscordGuildMember *p1, const CDiscordGuildMember *p2) +{ + return compareInt64(p1->userId, p2->userId); +} + +CDiscordGuild::CDiscordGuild(SnowFlake _id) : + id(_id), + arChannels(10, compareUsers), + arChatUsers(30, compareChatUsers), + arRoles(10, compareRoles) +{ +} + +CDiscordGuild::~CDiscordGuild() +{ +} + +CDiscordUser::~CDiscordUser() +{ + if (pGuild != nullptr) + pGuild->arChannels.remove(this); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// reads a presence block from json + +void CDiscordProto::ProcessPresence(const JSONNode &root) +{ + auto userId = ::getId(root["user"]["id"]); + CDiscordUser *pUser = FindUser(userId); + if (pUser == nullptr) { + debugLogA("Presence from unknown user id %lld ignored", userId); + return; + } + + setWord(pUser->hContact, "Status", StrToStatus(root["status"].as_mstring())); + + CheckAvatarChange(pUser->hContact, root["user"]["avatar"].as_mstring()); + + for (auto &act : root["activities"]) { + CMStringW wszStatus(act["state"].as_mstring()); + if (!wszStatus.IsEmpty()) + db_set_ws(pUser->hContact, "CList", "StatusMsg", wszStatus); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// +// reads a role from json + +void CDiscordProto::ProcessRole(CDiscordGuild *guild, const JSONNode &role) +{ + SnowFlake id = ::getId(role["id"]); + CDiscordRole *p = guild->arRoles.find((CDiscordRole*)&id); + if (p == nullptr) { + p = new CDiscordRole(); + p->id = id; + guild->arRoles.insert(p); + } + + p->color = role["color"].as_int(); + p->position = role["position"].as_int(); + p->permissions = role["permissions"].as_int(); + p->wszName = role["name"].as_mstring(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static void sttSetGroupName(MCONTACT hContact, const wchar_t *pwszGroupName) +{ + ptrW wszOldName(Clist_GetGroup(hContact)); + if (wszOldName != nullptr) { + ptrW wszChatGroup(Chat_GetGroup()); + if (mir_wstrcmpi(wszOldName, wszChatGroup)) + return; // custom group, don't touch it + } + + Clist_SetGroup(hContact, pwszGroupName); +} + +void CDiscordProto::BatchChatCreate(void *param) +{ + CDiscordGuild *pGuild = (CDiscordGuild*)param; + + for (auto &it : pGuild->arChannels) + if (!it->bIsPrivate && !it->bIsGroup) + CreateChat(pGuild, it); +} + +void CDiscordProto::CreateChat(CDiscordGuild *pGuild, CDiscordUser *pUser) +{ + SESSION_INFO *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, pUser->wszUsername, pUser->wszChannelName); + si->pParent = pGuild->pParentSi; + pUser->hContact = si->hContact; + setId(pUser->hContact, DB_KEY_ID, pUser->channelId); + setId(pUser->hContact, DB_KEY_CHANNELID, pUser->channelId); + + SnowFlake oldMsgId = getId(pUser->hContact, DB_KEY_LASTMSGID); + if (oldMsgId == 0) + RetrieveHistory(pUser, MSG_BEFORE, pUser->lastMsgId, 20); + else if (!pUser->bSynced && pUser->lastMsgId > oldMsgId) { + pUser->bSynced = true; + RetrieveHistory(pUser, MSG_AFTER, oldMsgId, 99); + } + + if (m_bUseGuildGroups) { + if (pUser->parentId) { + CDiscordUser *pParent = FindUserByChannel(pUser->parentId); + if (pParent != nullptr) + sttSetGroupName(pUser->hContact, pParent->wszChannelName); + } + else sttSetGroupName(pUser->hContact, Clist_GroupGetName(pGuild->groupId)); + } + + BuildStatusList(pGuild, si); + + Chat_Control(m_szModuleName, pUser->wszUsername, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE); + Chat_Control(m_szModuleName, pUser->wszUsername, SESSION_ONLINE); + + if (!pUser->wszTopic.IsEmpty()) { + Chat_SetStatusbarText(m_szModuleName, pUser->wszUsername, pUser->wszTopic); + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_TOPIC }; + gce.pszID.w = pUser->wszUsername; + gce.time = time(0); + gce.pszText.w = pUser->wszTopic; + Chat_Event(&gce); + } +} + +void CDiscordProto::ProcessGuild(const JSONNode &pRoot) +{ + SnowFlake guildId = ::getId(pRoot["id"]); + + CDiscordGuild *pGuild = FindGuild(guildId); + if (pGuild == nullptr) { + pGuild = new CDiscordGuild(guildId); + pGuild->LoadFromFile(); + arGuilds.insert(pGuild); + } + + pGuild->ownerId = ::getId(pRoot["owner_id"]); + pGuild->wszName = pRoot["name"].as_mstring(); + if (m_bUseGuildGroups) + pGuild->groupId = Clist_GroupCreate(Clist_GroupExists(m_wszDefaultGroup), pGuild->wszName); + + SESSION_INFO *si = Chat_NewSession(GCW_SERVER, m_szModuleName, pGuild->wszName, pGuild->wszName, pGuild); + if (si == nullptr) + return; + + pGuild->pParentSi = (SESSION_INFO*)si; + pGuild->hContact = si->hContact; + setId(pGuild->hContact, DB_KEY_CHANNELID, guildId); + + Chat_Control(m_szModuleName, pGuild->wszName, WINDOW_HIDDEN); + Chat_Control(m_szModuleName, pGuild->wszName, SESSION_ONLINE); + + for (auto &it : pRoot["roles"]) + ProcessRole(pGuild, it); + + BuildStatusList(pGuild, si); + + for (auto &it : pRoot["channels"]) + ProcessGuildChannel(pGuild, it); + + if (!pGuild->bSynced && getByte(si->hContact, "EnableSync")) + GatewaySendGuildInfo(pGuild); + + // store all guild members + for (auto &it : pRoot["members"]) { + auto *pm = ProcessGuildUser(pGuild, it); + + CMStringW wszNick = it["nick"].as_mstring(); + if (!wszNick.IsEmpty()) + pm->wszNick = wszNick; + + pm->iStatus = ID_STATUS_OFFLINE; + } + + // parse online statuses + for (auto &it : pRoot["presences"]) { + CDiscordGuildMember *gm = pGuild->FindUser(::getId(it["user"]["id"])); + if (gm != nullptr) + gm->iStatus = StrToStatus(it["status"].as_mstring()); + } + + for (auto &it : pGuild->arChatUsers) + AddGuildUser(pGuild, *it); + + if (!m_bTerminated) + ForkThread(&CDiscordProto::BatchChatCreate, pGuild); + + pGuild->bSynced = true; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +CDiscordUser* CDiscordProto::ProcessGuildChannel(CDiscordGuild *pGuild, const JSONNode &pch) +{ + CMStringW wszChannelId = pch["id"].as_mstring(); + SnowFlake channelId = _wtoi64(wszChannelId); + CMStringW wszName = pch["name"].as_mstring(); + CDiscordUser *pUser; + + // filter our all channels but the text ones + switch (pch["type"].as_int()) { + case 4: // channel group + if (!m_bUseGuildGroups) // ignore groups when they aren't enabled + return nullptr; + + pUser = FindUserByChannel(channelId); + if (pUser == nullptr) { + // missing channel - create it + pUser = new CDiscordUser(channelId); + pUser->bIsPrivate = false; + pUser->channelId = channelId; + pUser->bIsGroup = true; + arUsers.insert(pUser); + + pGuild->arChannels.insert(pUser); + + MGROUP grpId = Clist_GroupCreate(pGuild->groupId, wszName); + pUser->wszChannelName = Clist_GroupGetName(grpId); + } + return pUser; + + case 0: // text channel + pUser = FindUserByChannel(channelId); + if (pUser == nullptr) { + // missing channel - create it + pUser = new CDiscordUser(channelId); + pUser->bIsPrivate = false; + pUser->channelId = channelId; + arUsers.insert(pUser); + } + + if (pGuild->arChannels.find(pUser) == nullptr) + pGuild->arChannels.insert(pUser); + + pUser->wszUsername = wszChannelId; + if (m_bUseGuildGroups) + pUser->wszChannelName = L"#" + wszName; + else + pUser->wszChannelName = pGuild->wszName + L"#" + wszName; + pUser->wszTopic = pch["topic"].as_mstring(); + pUser->pGuild = pGuild; + pUser->lastMsgId = ::getId(pch["last_message_id"]); + pUser->parentId = _wtoi64(pch["parent_id"].as_mstring()); + return pUser; + } + + return nullptr; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +CDiscordGuildMember* CDiscordProto::ProcessGuildUser(CDiscordGuild *pGuild, const JSONNode &pRoot, bool *pbNew) +{ + auto& pUser = pRoot["user"]; + + bool bNew = false; + CMStringW wszUserId = pUser["id"].as_mstring(); + SnowFlake userId = _wtoi64(wszUserId); + CDiscordGuildMember *pm = pGuild->FindUser(userId); + if (pm == nullptr) { + pm = new CDiscordGuildMember(userId); + pGuild->arChatUsers.insert(pm); + bNew = true; + } + + pm->wszDiscordId = pUser["username"].as_mstring() + L"#" + pUser["discriminator"].as_mstring(); + pm->wszNick = pRoot["nick"].as_mstring(); + if (pm->wszNick.IsEmpty()) + pm->wszNick = pUser["username"].as_mstring(); + else + bNew = true; + + if (userId == pGuild->ownerId) + pm->wszRole = L"@owner"; + else { + CDiscordRole *pRole = nullptr; + for (auto &itr : pRoot["roles"]) { + SnowFlake roleId = ::getId(itr); + if (pRole = pGuild->arRoles.find((CDiscordRole *)&roleId)) + break; + } + pm->wszRole = (pRole == nullptr) ? L"@everyone" : pRole->wszName; + } + + if (pbNew) + *pbNew = bNew; + return pm; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordProto::ProcessChatUser(CDiscordUser *pChat, const CMStringW &wszUserId, const JSONNode &pRoot) +{ + // input data control + SnowFlake userId = _wtoi64(wszUserId); + CDiscordGuild *pGuild = pChat->pGuild; + if (pGuild == nullptr || userId == 0) + return; + + // does user exist? if yes, there's nothing to do + auto *pm = pGuild->FindUser(userId); + if (pm != nullptr) + return; + + // otherwise let's create a user and insert him into all guild's chats + pm = new CDiscordGuildMember(userId); + pm->wszDiscordId = pRoot["author"]["username"].as_mstring() + L"#" + pRoot["author"]["discriminator"].as_mstring(); + pm->wszNick = pRoot["nick"].as_mstring(); + if (pm->wszNick.IsEmpty()) + pm->wszNick = pRoot["author"]["username"].as_mstring(); + pGuild->arChatUsers.insert(pm); + + debugLogA("add missing user to chat: id=%lld, nick=%S", userId, pm->wszNick.c_str()); + AddGuildUser(pGuild, *pm); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordProto::AddGuildUser(CDiscordGuild *pGuild, const CDiscordGuildMember &pUser) +{ + int flags = 0; + switch (pUser.iStatus) { + case ID_STATUS_ONLINE: case ID_STATUS_NA: case ID_STATUS_DND: + flags = 1; + break; + } + + auto *pStatus = g_chatApi.TM_FindStatus(pGuild->pParentSi->pStatuses, pUser.wszRole); + + wchar_t wszUserId[100]; + _i64tow_s(pUser.userId, wszUserId, _countof(wszUserId), 10); + + auto *pu = g_chatApi.UM_AddUser(pGuild->pParentSi, wszUserId, pUser.wszNick, (pStatus) ? pStatus->iStatus : 0); + pu->iStatusEx = flags; + if (pUser.userId == m_ownId) + pGuild->pParentSi->pMe = pu; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordGuild::LoadFromFile() +{ + int fileNo = _wopen(GetCacheFile(), O_TEXT | O_RDONLY); + if (fileNo == -1) + return; + + int fSize = ::filelength(fileNo); + ptrA json((char*)mir_alloc(fSize + 1)); + read(fileNo, json, fSize); + close(fileNo); + + JSONNode cached = JSONNode::parse(json); + for (auto &it : cached) { + SnowFlake userId = getId(it["id"]); + auto *pUser = FindUser(userId); + if (pUser == nullptr) { + pUser = new CDiscordGuildMember(userId); + arChatUsers.insert(pUser); + } + + pUser->wszNick = it["n"].as_mstring(); + pUser->wszRole = it["r"].as_mstring(); + } +} + +void CDiscordGuild ::SaveToFile() +{ + JSONNode members(JSON_ARRAY); + for (auto &it : arChatUsers) { + JSONNode member; + member << INT64_PARAM("id", it->userId) << WCHAR_PARAM("n", it->wszNick) << WCHAR_PARAM("r", it->wszRole); + members << member; + } + + CMStringW wszFileName(GetCacheFile()); + CreatePathToFileW(wszFileName); + int fileNo = _wopen(wszFileName, O_CREAT | O_TRUNC | O_TEXT | O_WRONLY); + if (fileNo != -1) { + std::string json = members.write_formatted(); + write(fileNo, json.c_str(), (int)json.size()); + close(fileNo); + } +} diff --git a/protocols/Discord/src/http.cpp b/protocols/Discord/src/http.cpp index 2facf00af7..f65451f9ce 100644 --- a/protocols/Discord/src/http.cpp +++ b/protocols/Discord/src/http.cpp @@ -1,155 +1,155 @@ -/* -Copyright © 2016-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -void CDiscordProto::Push(AsyncHttpRequest *pReq, int iTimeout) -{ - pReq->timeout = iTimeout; - { - mir_cslock lck(m_csHttpQueue); - m_arHttpQueue.insert(pReq); - } - SetEvent(m_evRequestsQueue); -} - -void CDiscordProto::SaveToken(const JSONNode &data) -{ - CMStringA szToken = data["token"].as_mstring(); - if (!szToken.IsEmpty()) - m_szTempToken = szToken.Detach(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static LONG g_reqNum = 0; - -AsyncHttpRequest::AsyncHttpRequest(CDiscordProto *ppro, int iRequestType, LPCSTR _url, MTHttpRequestHandler pFunc, JSONNode *pRoot) -{ - if (*_url == '/') { // relative url leads to a site - m_szUrl = "https://discord.com/api/v8"; - m_szUrl += _url; - m_bMainSite = true; - } - else { - m_szUrl = _url; - m_bMainSite = false; - } - - flags = NLHRF_HTTP11 | NLHRF_REDIRECT | NLHRF_SSL; - if (ppro->m_szAccessToken != nullptr) { - AddHeader("Authorization", ppro->m_szAccessToken); - flags |= NLHRF_DUMPASTEXT | NLHRF_NODUMPHEADERS; - } - else flags |= NLHRF_NODUMPSEND; - - if (pRoot != nullptr) { - ptrW text(json_write(pRoot)); - pData = mir_utf8encodeW(text); - dataLength = (int)mir_strlen(pData); - - AddHeader("Content-Type", "application/json"); - } - - m_pFunc = pFunc; - requestType = iRequestType; - m_iErrorCode = 0; - m_iReqNum = ::InterlockedIncrement(&g_reqNum); -} - -JsonReply::JsonReply(NETLIBHTTPREQUEST *pReply) -{ - if (pReply == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = pReply->resultCode; - - m_root = json_parse(pReply->pData); - if (m_root == nullptr) - m_errorCode = 500; -} - -JsonReply::~JsonReply() -{ - json_delete(m_root); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CDiscordProto::ServerThread(void*) -{ - m_szAccessToken = getStringA("AccessToken"); - m_hAPIConnection = nullptr; - m_bTerminated = false; - - debugLogA("CDiscordProto::WorkerThread: %s", "entering"); - - if (m_szAccessToken != nullptr) - RetrieveMyInfo(); // try to receive a response from server - else { - if (mir_wstrlen(m_wszEmail) == 0) { - ConnectionFailed(LOGINERR_BADUSERID); - return; - } - - ptrW wszPassword(getWStringA(DB_KEY_PASSWORD)); - if (wszPassword == nullptr) { - ConnectionFailed(LOGINERR_WRONGPASSWORD); - return; - } - - JSONNode root; root << WCHAR_PARAM("email", m_wszEmail) << WCHAR_PARAM("password", wszPassword); - Push(new AsyncHttpRequest(this, REQUEST_POST, "/auth/login", &CDiscordProto::OnReceiveToken, &root)); - } - - while (true) { - WaitForSingleObject(m_evRequestsQueue, 1000); - if (m_bTerminated) - break; - - AsyncHttpRequest *pReq; - bool need_sleep = false; - while (true) { - { - mir_cslock lck(m_csHttpQueue); - if (m_arHttpQueue.getCount() == 0) - break; - - pReq = m_arHttpQueue[0]; - m_arHttpQueue.remove(0); - need_sleep = (m_arHttpQueue.getCount() > 1); - } - if (m_bTerminated) - break; - ExecuteRequest(pReq); - if (need_sleep) { - Sleep(330); - debugLogA("CDiscordProto::WorkerThread: %s", "need to sleep"); - } - } - } - - m_hWorkerThread = nullptr; - if (m_hAPIConnection) { - Netlib_CloseHandle(m_hAPIConnection); - m_hAPIConnection = nullptr; - } - - debugLogA("CDiscordProto::WorkerThread: %s", "leaving"); -} +/* +Copyright © 2016-22 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +void CDiscordProto::Push(AsyncHttpRequest *pReq, int iTimeout) +{ + pReq->timeout = iTimeout; + { + mir_cslock lck(m_csHttpQueue); + m_arHttpQueue.insert(pReq); + } + SetEvent(m_evRequestsQueue); +} + +void CDiscordProto::SaveToken(const JSONNode &data) +{ + CMStringA szToken = data["token"].as_mstring(); + if (!szToken.IsEmpty()) + m_szTempToken = szToken.Detach(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static LONG g_reqNum = 0; + +AsyncHttpRequest::AsyncHttpRequest(CDiscordProto *ppro, int iRequestType, LPCSTR _url, MTHttpRequestHandler pFunc, JSONNode *pRoot) +{ + if (*_url == '/') { // relative url leads to a site + m_szUrl = "https://discord.com/api/v8"; + m_szUrl += _url; + m_bMainSite = true; + } + else { + m_szUrl = _url; + m_bMainSite = false; + } + + flags = NLHRF_HTTP11 | NLHRF_REDIRECT | NLHRF_SSL; + if (ppro->m_szAccessToken != nullptr) { + AddHeader("Authorization", ppro->m_szAccessToken); + flags |= NLHRF_DUMPASTEXT | NLHRF_NODUMPHEADERS; + } + else flags |= NLHRF_NODUMPSEND; + + if (pRoot != nullptr) { + ptrW text(json_write(pRoot)); + pData = mir_utf8encodeW(text); + dataLength = (int)mir_strlen(pData); + + AddHeader("Content-Type", "application/json"); + } + + m_pFunc = pFunc; + requestType = iRequestType; + m_iErrorCode = 0; + m_iReqNum = ::InterlockedIncrement(&g_reqNum); +} + +JsonReply::JsonReply(NETLIBHTTPREQUEST *pReply) +{ + if (pReply == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = pReply->resultCode; + + m_root = json_parse(pReply->pData); + if (m_root == nullptr) + m_errorCode = 500; +} + +JsonReply::~JsonReply() +{ + json_delete(m_root); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordProto::ServerThread(void*) +{ + m_szAccessToken = getStringA("AccessToken"); + m_hAPIConnection = nullptr; + m_bTerminated = false; + + debugLogA("CDiscordProto::WorkerThread: %s", "entering"); + + if (m_szAccessToken != nullptr) + RetrieveMyInfo(); // try to receive a response from server + else { + if (mir_wstrlen(m_wszEmail) == 0) { + ConnectionFailed(LOGINERR_BADUSERID); + return; + } + + ptrW wszPassword(getWStringA(DB_KEY_PASSWORD)); + if (wszPassword == nullptr) { + ConnectionFailed(LOGINERR_WRONGPASSWORD); + return; + } + + JSONNode root; root << WCHAR_PARAM("email", m_wszEmail) << WCHAR_PARAM("password", wszPassword); + Push(new AsyncHttpRequest(this, REQUEST_POST, "/auth/login", &CDiscordProto::OnReceiveToken, &root)); + } + + while (true) { + WaitForSingleObject(m_evRequestsQueue, 1000); + if (m_bTerminated) + break; + + AsyncHttpRequest *pReq; + bool need_sleep = false; + while (true) { + { + mir_cslock lck(m_csHttpQueue); + if (m_arHttpQueue.getCount() == 0) + break; + + pReq = m_arHttpQueue[0]; + m_arHttpQueue.remove(0); + need_sleep = (m_arHttpQueue.getCount() > 1); + } + if (m_bTerminated) + break; + ExecuteRequest(pReq); + if (need_sleep) { + Sleep(330); + debugLogA("CDiscordProto::WorkerThread: %s", "need to sleep"); + } + } + } + + m_hWorkerThread = nullptr; + if (m_hAPIConnection) { + Netlib_CloseHandle(m_hAPIConnection); + m_hAPIConnection = nullptr; + } + + debugLogA("CDiscordProto::WorkerThread: %s", "leaving"); +} diff --git a/protocols/Discord/src/main.cpp b/protocols/Discord/src/main.cpp index c615047d00..98f0b120de 100644 --- a/protocols/Discord/src/main.cpp +++ b/protocols/Discord/src/main.cpp @@ -1,71 +1,71 @@ -/* -Copyright © 2016-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -CMPlugin g_plugin; - -///////////////////////////////////////////////////////////////////////////////////////// - -PLUGININFOEX pluginInfoEx = { - sizeof(PLUGININFOEX), - __PLUGIN_NAME, - PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), - __DESCRIPTION, - __AUTHOR, - __COPYRIGHT, - __AUTHORWEB, - UNICODE_AWARE, - // {88928401-2CE8-4568-AAA7-226141870CBF} - { 0x88928401, 0x2ce8, 0x4568, { 0xaa, 0xa7, 0x22, 0x61, 0x41, 0x87, 0x0c, 0xbf } } -}; - -CMPlugin::CMPlugin() : - ACCPROTOPLUGIN("Discord", pluginInfoEx) -{ - SetUniqueId(DB_KEY_ID); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Interface information - -extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST }; - -///////////////////////////////////////////////////////////////////////////////////////// -// Load - -IconItem g_iconList[] = -{ - { LPGEN("Main icon"), "main", IDI_MAIN }, - { LPGEN("Group chats"), "groupchat", IDI_GROUPCHAT }, - { LPGEN("Call"), "voicecall", IDI_VOICE_CALL }, - { LPGEN("Call ended"), "voiceend", IDI_VOICE_ENDED } -}; - -static int OnModulesLoaded(WPARAM, LPARAM) -{ - g_plugin.bVoiceService = ServiceExists(MS_VOICESERVICE_REGISTER); - return 0; -} - -int CMPlugin::Load() -{ - HookEvent(ME_SYSTEM_MODULESLOADED, &OnModulesLoaded); - - g_plugin.registerIcon("Protocols/Discord", g_iconList); - return 0; -} +/* +Copyright © 2016-22 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +CMPlugin g_plugin; + +///////////////////////////////////////////////////////////////////////////////////////// + +PLUGININFOEX pluginInfoEx = { + sizeof(PLUGININFOEX), + __PLUGIN_NAME, + PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), + __DESCRIPTION, + __AUTHOR, + __COPYRIGHT, + __AUTHORWEB, + UNICODE_AWARE, + // {88928401-2CE8-4568-AAA7-226141870CBF} + { 0x88928401, 0x2ce8, 0x4568, { 0xaa, 0xa7, 0x22, 0x61, 0x41, 0x87, 0x0c, 0xbf } } +}; + +CMPlugin::CMPlugin() : + ACCPROTOPLUGIN("Discord", pluginInfoEx) +{ + SetUniqueId(DB_KEY_ID); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Interface information + +extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST }; + +///////////////////////////////////////////////////////////////////////////////////////// +// Load + +IconItem g_iconList[] = +{ + { LPGEN("Main icon"), "main", IDI_MAIN }, + { LPGEN("Group chats"), "groupchat", IDI_GROUPCHAT }, + { LPGEN("Call"), "voicecall", IDI_VOICE_CALL }, + { LPGEN("Call ended"), "voiceend", IDI_VOICE_ENDED } +}; + +static int OnModulesLoaded(WPARAM, LPARAM) +{ + g_plugin.bVoiceService = ServiceExists(MS_VOICESERVICE_REGISTER); + return 0; +} + +int CMPlugin::Load() +{ + HookEvent(ME_SYSTEM_MODULESLOADED, &OnModulesLoaded); + + g_plugin.registerIcon("Protocols/Discord", g_iconList); + return 0; +} diff --git a/protocols/Discord/src/menus.cpp b/protocols/Discord/src/menus.cpp index e88d91aa43..cc928221f3 100644 --- a/protocols/Discord/src/menus.cpp +++ b/protocols/Discord/src/menus.cpp @@ -1,172 +1,172 @@ -/* -Copyright © 2016-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -INT_PTR CDiscordProto::OnMenuCopyId(WPARAM hContact, LPARAM) -{ - CopyId(CMStringW(FORMAT, L"%s#%d", getMStringW(hContact, DB_KEY_NICK).c_str(), getDword(hContact, DB_KEY_DISCR))); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -INT_PTR CDiscordProto::OnMenuCreateChannel(WPARAM hContact, LPARAM) -{ - ENTER_STRING es = { m_szModuleName, "channel_name", TranslateT("Enter channel name"), nullptr, ESF_COMBO, 5 }; - if (EnterString(&es)) { - JSONNode roles(JSON_ARRAY); roles.set_name("permission_overwrites"); - JSONNode root; root << INT_PARAM("type", 0) << WCHAR_PARAM("name", es.ptszResult) << roles; - CMStringA szUrl(FORMAT, "/guilds/%lld/channels", getId(hContact, DB_KEY_CHANNELID)); - Push(new AsyncHttpRequest(this, REQUEST_POST, szUrl, nullptr, &root)); - mir_free(es.ptszResult); - } - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -INT_PTR CDiscordProto::OnMenuJoinGuild(WPARAM, LPARAM) -{ - ENTER_STRING es = { m_szModuleName, "guild_name", TranslateT("Enter invitation code you received"), nullptr, ESF_COMBO, 5 }; - if (EnterString(&es)) { - CMStringA szUrl(FORMAT, "/invite/%S", es.ptszResult); - Push(new AsyncHttpRequest(this, REQUEST_POST, szUrl, nullptr)); - mir_free(es.ptszResult); - } - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -INT_PTR CDiscordProto::OnMenuLeaveGuild(WPARAM hContact, LPARAM) -{ - if (IDYES == MessageBox(nullptr, TranslateT("Do you really want to leave the guild?"), m_tszUserName, MB_ICONQUESTION | MB_YESNOCANCEL)) { - CMStringA szUrl(FORMAT, "/users/@me/guilds/%lld", getId(hContact, DB_KEY_CHANNELID)); - Push(new AsyncHttpRequest(this, REQUEST_DELETE, szUrl, nullptr)); - } - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -INT_PTR CDiscordProto::OnMenuLoadHistory(WPARAM hContact, LPARAM) -{ - auto *pUser = FindUser(getId(hContact, DB_KEY_ID)); - if (pUser) { - RetrieveHistory(pUser, MSG_AFTER, 0, 100); - delSetting(hContact, DB_KEY_LASTMSGID); - } - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -INT_PTR CDiscordProto::OnMenuToggleSync(WPARAM hContact, LPARAM) -{ - bool bEnabled = !getBool(hContact, "EnableSync"); - setByte(hContact, "EnableSync", bEnabled); - - if (bEnabled) - if (auto *pGuild = FindGuild(getId(hContact, DB_KEY_CHANNELID))) - GatewaySendGuildInfo(pGuild); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -int CDiscordProto::OnMenuPrebuild(WPARAM hContact, LPARAM) -{ - // "Leave guild" menu item should be visible only for the guild contacts - bool bIsGuild = getByte(hContact, "ChatRoom") == 2; - Menu_ShowItem(m_hMenuLeaveGuild, bIsGuild); - Menu_ShowItem(m_hMenuCreateChannel, bIsGuild); - Menu_ShowItem(m_hMenuToggleSync, bIsGuild); - - if (!bIsGuild && getWord(hContact, "ApparentMode") != 0) - Menu_ShowItem(GetMenuItem(PROTO_MENU_REQ_AUTH), true); - - if (getByte(hContact, "EnableSync")) - Menu_ModifyItem(m_hMenuToggleSync, LPGENW("Disable sync"), Skin_GetIconHandle(SKINICON_CHAT_LEAVE)); - else - Menu_ModifyItem(m_hMenuToggleSync, LPGENW("Enable sync"), Skin_GetIconHandle(SKINICON_CHAT_JOIN)); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Protocol menu items - -void CDiscordProto::OnBuildProtoMenu() -{ - CMenuItem mi(&g_plugin); - mi.root = Menu_GetProtocolRoot(this); - mi.flags = CMIF_UNMOVABLE; - - mi.pszService = "/JoinGuild"; - CreateProtoService(mi.pszService, &CDiscordProto::OnMenuJoinGuild); - mi.name.a = LPGEN("Join guild"); - mi.position = 200001; - mi.hIcolibItem = g_iconList[1].hIcolib; - Menu_AddProtoMenuItem(&mi, m_szModuleName); - - mi.pszService = "/CopyId"; - CreateProtoService(mi.pszService, &CDiscordProto::OnMenuCopyId); - mi.name.a = LPGEN("Copy my Discord ID"); - mi.position = 200002; - mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_USERONLINE); - Menu_AddProtoMenuItem(&mi, m_szModuleName); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Contact menu items - -void CDiscordProto::InitMenus() -{ - CMenuItem mi(&g_plugin); - mi.pszService = "/LeaveGuild"; - CreateProtoService(mi.pszService, &CDiscordProto::OnMenuLeaveGuild); - SET_UID(mi, 0x6EF11AD6, 0x6111, 0x4E29, 0xBA, 0x8B, 0xA7, 0xB2, 0xE0, 0x22, 0xE1, 0x8C); - mi.name.a = LPGEN("Leave guild"); - mi.position = -200001000; - mi.hIcolibItem = Skin_GetIconHandle(SKINICON_CHAT_LEAVE); - m_hMenuLeaveGuild = Menu_AddContactMenuItem(&mi, m_szModuleName); - - mi.pszService = "/CreateChannel"; - CreateProtoService(mi.pszService, &CDiscordProto::OnMenuCreateChannel); - SET_UID(mi, 0x6EF11AD6, 0x6111, 0x4E29, 0xBA, 0x8B, 0xA7, 0xB2, 0xE0, 0x22, 0xE1, 0x8D); - mi.name.a = LPGEN("Create new channel"); - mi.position = -200001001; - mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_ADDCONTACT); - m_hMenuCreateChannel = Menu_AddContactMenuItem(&mi, m_szModuleName); - - SET_UID(mi, 0x6EF11AD6, 0x6111, 0x4E29, 0xBA, 0x8B, 0xA7, 0xB2, 0xE0, 0x22, 0xE1, 0x8E); - mi.pszService = "/CopyId"; - mi.name.a = LPGEN("Copy ID"); - mi.position = -200001002; - mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_USERONLINE); - Menu_AddContactMenuItem(&mi, m_szModuleName); - - mi.pszService = "/ToggleSync"; - CreateProtoService(mi.pszService, &CDiscordProto::OnMenuToggleSync); - SET_UID(mi, 0x6EF11AD6, 0x6111, 0x4E29, 0xBA, 0x8B, 0xA7, 0xB2, 0xE0, 0x22, 0xE1, 0x8F); - mi.name.a = LPGEN("Enable guild sync"); - mi.position = -200001003; - mi.hIcolibItem = Skin_GetIconHandle(SKINICON_CHAT_JOIN); - m_hMenuToggleSync = Menu_AddContactMenuItem(&mi, m_szModuleName); - - HookProtoEvent(ME_CLIST_PREBUILDCONTACTMENU, &CDiscordProto::OnMenuPrebuild); -} +/* +Copyright © 2016-22 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +INT_PTR CDiscordProto::OnMenuCopyId(WPARAM hContact, LPARAM) +{ + CopyId(CMStringW(FORMAT, L"%s#%d", getMStringW(hContact, DB_KEY_NICK).c_str(), getDword(hContact, DB_KEY_DISCR))); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR CDiscordProto::OnMenuCreateChannel(WPARAM hContact, LPARAM) +{ + ENTER_STRING es = { m_szModuleName, "channel_name", TranslateT("Enter channel name"), nullptr, ESF_COMBO, 5 }; + if (EnterString(&es)) { + JSONNode roles(JSON_ARRAY); roles.set_name("permission_overwrites"); + JSONNode root; root << INT_PARAM("type", 0) << WCHAR_PARAM("name", es.ptszResult) << roles; + CMStringA szUrl(FORMAT, "/guilds/%lld/channels", getId(hContact, DB_KEY_CHANNELID)); + Push(new AsyncHttpRequest(this, REQUEST_POST, szUrl, nullptr, &root)); + mir_free(es.ptszResult); + } + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR CDiscordProto::OnMenuJoinGuild(WPARAM, LPARAM) +{ + ENTER_STRING es = { m_szModuleName, "guild_name", TranslateT("Enter invitation code you received"), nullptr, ESF_COMBO, 5 }; + if (EnterString(&es)) { + CMStringA szUrl(FORMAT, "/invite/%S", es.ptszResult); + Push(new AsyncHttpRequest(this, REQUEST_POST, szUrl, nullptr)); + mir_free(es.ptszResult); + } + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR CDiscordProto::OnMenuLeaveGuild(WPARAM hContact, LPARAM) +{ + if (IDYES == MessageBox(nullptr, TranslateT("Do you really want to leave the guild?"), m_tszUserName, MB_ICONQUESTION | MB_YESNOCANCEL)) { + CMStringA szUrl(FORMAT, "/users/@me/guilds/%lld", getId(hContact, DB_KEY_CHANNELID)); + Push(new AsyncHttpRequest(this, REQUEST_DELETE, szUrl, nullptr)); + } + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR CDiscordProto::OnMenuLoadHistory(WPARAM hContact, LPARAM) +{ + auto *pUser = FindUser(getId(hContact, DB_KEY_ID)); + if (pUser) { + RetrieveHistory(pUser, MSG_AFTER, 0, 100); + delSetting(hContact, DB_KEY_LASTMSGID); + } + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR CDiscordProto::OnMenuToggleSync(WPARAM hContact, LPARAM) +{ + bool bEnabled = !getBool(hContact, "EnableSync"); + setByte(hContact, "EnableSync", bEnabled); + + if (bEnabled) + if (auto *pGuild = FindGuild(getId(hContact, DB_KEY_CHANNELID))) + GatewaySendGuildInfo(pGuild); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +int CDiscordProto::OnMenuPrebuild(WPARAM hContact, LPARAM) +{ + // "Leave guild" menu item should be visible only for the guild contacts + bool bIsGuild = getByte(hContact, "ChatRoom") == 2; + Menu_ShowItem(m_hMenuLeaveGuild, bIsGuild); + Menu_ShowItem(m_hMenuCreateChannel, bIsGuild); + Menu_ShowItem(m_hMenuToggleSync, bIsGuild); + + if (!bIsGuild && getWord(hContact, "ApparentMode") != 0) + Menu_ShowItem(GetMenuItem(PROTO_MENU_REQ_AUTH), true); + + if (getByte(hContact, "EnableSync")) + Menu_ModifyItem(m_hMenuToggleSync, LPGENW("Disable sync"), Skin_GetIconHandle(SKINICON_CHAT_LEAVE)); + else + Menu_ModifyItem(m_hMenuToggleSync, LPGENW("Enable sync"), Skin_GetIconHandle(SKINICON_CHAT_JOIN)); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Protocol menu items + +void CDiscordProto::OnBuildProtoMenu() +{ + CMenuItem mi(&g_plugin); + mi.root = Menu_GetProtocolRoot(this); + mi.flags = CMIF_UNMOVABLE; + + mi.pszService = "/JoinGuild"; + CreateProtoService(mi.pszService, &CDiscordProto::OnMenuJoinGuild); + mi.name.a = LPGEN("Join guild"); + mi.position = 200001; + mi.hIcolibItem = g_iconList[1].hIcolib; + Menu_AddProtoMenuItem(&mi, m_szModuleName); + + mi.pszService = "/CopyId"; + CreateProtoService(mi.pszService, &CDiscordProto::OnMenuCopyId); + mi.name.a = LPGEN("Copy my Discord ID"); + mi.position = 200002; + mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_USERONLINE); + Menu_AddProtoMenuItem(&mi, m_szModuleName); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Contact menu items + +void CDiscordProto::InitMenus() +{ + CMenuItem mi(&g_plugin); + mi.pszService = "/LeaveGuild"; + CreateProtoService(mi.pszService, &CDiscordProto::OnMenuLeaveGuild); + SET_UID(mi, 0x6EF11AD6, 0x6111, 0x4E29, 0xBA, 0x8B, 0xA7, 0xB2, 0xE0, 0x22, 0xE1, 0x8C); + mi.name.a = LPGEN("Leave guild"); + mi.position = -200001000; + mi.hIcolibItem = Skin_GetIconHandle(SKINICON_CHAT_LEAVE); + m_hMenuLeaveGuild = Menu_AddContactMenuItem(&mi, m_szModuleName); + + mi.pszService = "/CreateChannel"; + CreateProtoService(mi.pszService, &CDiscordProto::OnMenuCreateChannel); + SET_UID(mi, 0x6EF11AD6, 0x6111, 0x4E29, 0xBA, 0x8B, 0xA7, 0xB2, 0xE0, 0x22, 0xE1, 0x8D); + mi.name.a = LPGEN("Create new channel"); + mi.position = -200001001; + mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_ADDCONTACT); + m_hMenuCreateChannel = Menu_AddContactMenuItem(&mi, m_szModuleName); + + SET_UID(mi, 0x6EF11AD6, 0x6111, 0x4E29, 0xBA, 0x8B, 0xA7, 0xB2, 0xE0, 0x22, 0xE1, 0x8E); + mi.pszService = "/CopyId"; + mi.name.a = LPGEN("Copy ID"); + mi.position = -200001002; + mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_USERONLINE); + Menu_AddContactMenuItem(&mi, m_szModuleName); + + mi.pszService = "/ToggleSync"; + CreateProtoService(mi.pszService, &CDiscordProto::OnMenuToggleSync); + SET_UID(mi, 0x6EF11AD6, 0x6111, 0x4E29, 0xBA, 0x8B, 0xA7, 0xB2, 0xE0, 0x22, 0xE1, 0x8F); + mi.name.a = LPGEN("Enable guild sync"); + mi.position = -200001003; + mi.hIcolibItem = Skin_GetIconHandle(SKINICON_CHAT_JOIN); + m_hMenuToggleSync = Menu_AddContactMenuItem(&mi, m_szModuleName); + + HookProtoEvent(ME_CLIST_PREBUILDCONTACTMENU, &CDiscordProto::OnMenuPrebuild); +} diff --git a/protocols/Discord/src/options.cpp b/protocols/Discord/src/options.cpp index 86f3519df8..3ced623311 100644 --- a/protocols/Discord/src/options.cpp +++ b/protocols/Discord/src/options.cpp @@ -1,100 +1,100 @@ -/* -Copyright © 2016-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -///////////////////////////////////////////////////////////////////////////////////////// - -class CDiscardAccountOptions : public CProtoDlgBase -{ - CCtrlCheck chkUseChats, chkHideChats, chkUseGroups, chkDeleteMsgs; - CCtrlEdit m_edGroup, m_edUserName, m_edPassword; - ptrW m_wszOldGroup; - -public: - CDiscardAccountOptions(CDiscordProto *ppro, int iDlgID, bool bFullDlg) : - CProtoDlgBase(ppro, iDlgID), - m_edGroup(this, IDC_GROUP), - m_edUserName(this, IDC_USERNAME), - m_edPassword(this, IDC_PASSWORD), - chkUseChats(this, IDC_USEGUILDS), - chkHideChats(this, IDC_HIDECHATS), - chkUseGroups(this, IDC_USEGROUPS), - chkDeleteMsgs(this, IDC_DELETE_MSGS), - m_wszOldGroup(mir_wstrdup(ppro->m_wszDefaultGroup)) - { - CreateLink(m_edGroup, ppro->m_wszDefaultGroup); - CreateLink(m_edUserName, ppro->m_wszEmail); - if (bFullDlg) { - CreateLink(chkUseChats, ppro->m_bUseGroupchats); - CreateLink(chkHideChats, ppro->m_bHideGroupchats); - CreateLink(chkUseGroups, ppro->m_bUseGuildGroups); - CreateLink(chkDeleteMsgs, ppro->m_bSyncDeleteMsgs); - - chkUseChats.OnChange = Callback(this, &CDiscardAccountOptions::onChange_GroupChats); - } - } - - bool OnInitDialog() override - { - ptrW buf(m_proto->getWStringA(DB_KEY_PASSWORD)); - if (buf) - m_edPassword.SetText(buf); - return true; - } - - bool OnApply() override - { - if (mir_wstrcmp(m_proto->m_wszDefaultGroup, m_wszOldGroup)) - Clist_GroupCreate(0, m_proto->m_wszDefaultGroup); - - ptrW buf(m_edPassword.GetText()); - m_proto->setWString(DB_KEY_PASSWORD, buf); - return true; - } - - void onChange_GroupChats(CCtrlCheck*) - { - bool bEnabled = chkUseChats.GetState(); - chkHideChats.Enable(bEnabled); - chkUseGroups.Enable(bEnabled); - } -}; - -///////////////////////////////////////////////////////////////////////////////////////// - -INT_PTR CDiscordProto::SvcCreateAccMgrUI(WPARAM, LPARAM hwndParent) -{ - CDiscardAccountOptions *pDlg = new CDiscardAccountOptions(this, IDD_OPTIONS_ACCMGR, false); - pDlg->SetParent((HWND)hwndParent); - pDlg->Create(); - return (INT_PTR)pDlg->GetHwnd(); -} - -int CDiscordProto::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 CDiscardAccountOptions(this, IDD_OPTIONS_ACCOUNT, true); - g_plugin.addOptions(wParam, &odp); - return 0; -} +/* +Copyright © 2016-22 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +///////////////////////////////////////////////////////////////////////////////////////// + +class CDiscardAccountOptions : public CProtoDlgBase +{ + CCtrlCheck chkUseChats, chkHideChats, chkUseGroups, chkDeleteMsgs; + CCtrlEdit m_edGroup, m_edUserName, m_edPassword; + ptrW m_wszOldGroup; + +public: + CDiscardAccountOptions(CDiscordProto *ppro, int iDlgID, bool bFullDlg) : + CProtoDlgBase(ppro, iDlgID), + m_edGroup(this, IDC_GROUP), + m_edUserName(this, IDC_USERNAME), + m_edPassword(this, IDC_PASSWORD), + chkUseChats(this, IDC_USEGUILDS), + chkHideChats(this, IDC_HIDECHATS), + chkUseGroups(this, IDC_USEGROUPS), + chkDeleteMsgs(this, IDC_DELETE_MSGS), + m_wszOldGroup(mir_wstrdup(ppro->m_wszDefaultGroup)) + { + CreateLink(m_edGroup, ppro->m_wszDefaultGroup); + CreateLink(m_edUserName, ppro->m_wszEmail); + if (bFullDlg) { + CreateLink(chkUseChats, ppro->m_bUseGroupchats); + CreateLink(chkHideChats, ppro->m_bHideGroupchats); + CreateLink(chkUseGroups, ppro->m_bUseGuildGroups); + CreateLink(chkDeleteMsgs, ppro->m_bSyncDeleteMsgs); + + chkUseChats.OnChange = Callback(this, &CDiscardAccountOptions::onChange_GroupChats); + } + } + + bool OnInitDialog() override + { + ptrW buf(m_proto->getWStringA(DB_KEY_PASSWORD)); + if (buf) + m_edPassword.SetText(buf); + return true; + } + + bool OnApply() override + { + if (mir_wstrcmp(m_proto->m_wszDefaultGroup, m_wszOldGroup)) + Clist_GroupCreate(0, m_proto->m_wszDefaultGroup); + + ptrW buf(m_edPassword.GetText()); + m_proto->setWString(DB_KEY_PASSWORD, buf); + return true; + } + + void onChange_GroupChats(CCtrlCheck*) + { + bool bEnabled = chkUseChats.GetState(); + chkHideChats.Enable(bEnabled); + chkUseGroups.Enable(bEnabled); + } +}; + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR CDiscordProto::SvcCreateAccMgrUI(WPARAM, LPARAM hwndParent) +{ + CDiscardAccountOptions *pDlg = new CDiscardAccountOptions(this, IDD_OPTIONS_ACCMGR, false); + pDlg->SetParent((HWND)hwndParent); + pDlg->Create(); + return (INT_PTR)pDlg->GetHwnd(); +} + +int CDiscordProto::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 CDiscardAccountOptions(this, IDD_OPTIONS_ACCOUNT, true); + g_plugin.addOptions(wParam, &odp); + return 0; +} diff --git a/protocols/Discord/src/proto.cpp b/protocols/Discord/src/proto.cpp index 972c6ec312..2bd02f704d 100644 --- a/protocols/Discord/src/proto.cpp +++ b/protocols/Discord/src/proto.cpp @@ -1,768 +1,768 @@ -/* -Copyright © 2016-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -static int compareMessages(const COwnMessage *p1, const COwnMessage *p2) -{ - return compareInt64(p1->nonce, p2->nonce); -} - -static int compareRequests(const AsyncHttpRequest *p1, const AsyncHttpRequest *p2) -{ - return p1->m_iReqNum - p2->m_iReqNum; -} - -int compareUsers(const CDiscordUser *p1, const CDiscordUser *p2) -{ - return compareInt64(p1->id, p2->id); -} - -static int compareGuilds(const CDiscordGuild *p1, const CDiscordGuild *p2) -{ - return compareInt64(p1->id, p2->id); -} - -CDiscordProto::CDiscordProto(const char *proto_name, const wchar_t *username) : - PROTO(proto_name, username), - m_impl(*this), - m_arHttpQueue(10, compareRequests), - m_evRequestsQueue(CreateEvent(nullptr, FALSE, FALSE, nullptr)), - arUsers(10, compareUsers), - arGuilds(1, compareGuilds), - arMarkReadQueue(1, compareUsers), - arOwnMessages(1, compareMessages), - arVoiceCalls(1), - - m_wszEmail(this, "Email", L""), - m_wszDefaultGroup(this, "GroupName", DB_KEYVAL_GROUP), - m_bUseGroupchats(this, "UseGroupChats", true), - m_bHideGroupchats(this, "HideChats", true), - m_bUseGuildGroups(this, "UseGuildGroups", false), - m_bSyncDeleteMsgs(this, "DeleteServerMsgs", true) -{ - // Services - CreateProtoService(PS_CREATEACCMGRUI, &CDiscordProto::SvcCreateAccMgrUI); - - CreateProtoService(PS_GETAVATARINFO, &CDiscordProto::GetAvatarInfo); - CreateProtoService(PS_GETAVATARCAPS, &CDiscordProto::GetAvatarCaps); - CreateProtoService(PS_GETMYAVATAR, &CDiscordProto::GetMyAvatar); - CreateProtoService(PS_SETMYAVATAR, &CDiscordProto::SetMyAvatar); - - CreateProtoService(PS_MENU_REQAUTH, &CDiscordProto::RequestFriendship); - CreateProtoService(PS_MENU_LOADHISTORY, &CDiscordProto::OnMenuLoadHistory); - - CreateProtoService(PS_VOICE_CAPS, &CDiscordProto::VoiceCaps); - - // Events - HookProtoEvent(ME_OPT_INITIALISE, &CDiscordProto::OnOptionsInit); - HookProtoEvent(ME_DB_EVENT_MARKED_READ, &CDiscordProto::OnDbEventRead); - HookProtoEvent(ME_PROTO_ACCLISTCHANGED, &CDiscordProto::OnAccountChanged); - - HookProtoEvent(PE_VOICE_CALL_STATE, &CDiscordProto::OnVoiceState); - - // database - db_set_resident(m_szModuleName, "XStatusMsg"); - - // custom events - DBEVENTTYPEDESCR dbEventType = {}; - dbEventType.module = m_szModuleName; - dbEventType.flags = DETF_HISTORY | DETF_MSGWINDOW; - - dbEventType.eventType = EVENT_INCOMING_CALL; - dbEventType.descr = Translate("Incoming call"); - dbEventType.eventIcon = g_plugin.getIconHandle(IDI_VOICE_CALL); - DbEvent_RegisterType(&dbEventType); - - dbEventType.eventType = EVENT_CALL_FINISHED; - dbEventType.descr = Translate("Call ended"); - dbEventType.eventIcon = g_plugin.getIconHandle(IDI_VOICE_ENDED); - DbEvent_RegisterType(&dbEventType); - - // Groupchat initialization - GCREGISTER gcr = {}; - gcr.dwFlags = GC_TYPNOTIF | GC_CHANMGR; - gcr.ptszDispName = m_tszUserName; - gcr.pszModule = m_szModuleName; - Chat_Register(&gcr); - - // Network initialization - CMStringW descr; - NETLIBUSER nlu = {}; - - nlu.szSettingsModule = m_szModuleName; - nlu.flags = NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; - descr.Format(TranslateT("%s server connection"), m_tszUserName); - nlu.szDescriptiveName.w = descr.GetBuffer(); - m_hNetlibUser = Netlib_RegisterUser(&nlu); - - CMStringA module(FORMAT, "%s.Gateway", m_szModuleName); - nlu.szSettingsModule = module.GetBuffer(); - nlu.flags = NUF_OUTGOING | NUF_UNICODE; - descr.Format(TranslateT("%s gateway connection"), m_tszUserName); - nlu.szDescriptiveName.w = descr.GetBuffer(); - m_hGatewayNetlibUser = Netlib_RegisterUser(&nlu); -} - -CDiscordProto::~CDiscordProto() -{ - debugLogA("CDiscordProto::~CDiscordProto"); - - for (auto &msg : m_wszStatusMsg) - mir_free(msg); - - arUsers.destroy(); - - m_arHttpQueue.destroy(); - ::CloseHandle(m_evRequestsQueue); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CDiscordProto::OnModulesLoaded() -{ - std::vector lostIds; - - // Fill users list - for (auto &hContact : AccContacts()) { - CDiscordUser *pNew = new CDiscordUser(getId(hContact, DB_KEY_ID)); - pNew->hContact = hContact; - pNew->lastMsgId = getId(hContact, DB_KEY_LASTMSGID); - pNew->wszUsername = ptrW(getWStringA(hContact, DB_KEY_NICK)); - pNew->iDiscriminator = getDword(hContact, DB_KEY_DISCR); - - // set EnableSync = 1 by default for all existing guilds - switch (getByte(hContact, "ChatRoom")) { - case 2: // guild - delSetting(hContact, DB_KEY_CHANNELID); - if (getDword(hContact, "EnableSync", -1) == -1) - setDword(hContact, "EnableSync", 1); - break; - - case 1: // group chat - pNew->channelId = getId(hContact, DB_KEY_CHANNELID); - if (!pNew->channelId) { - lostIds.push_back(hContact); - delete pNew; - continue; - } - break; - - default: - pNew->channelId = getId(hContact, DB_KEY_CHANNELID); - break; - } - arUsers.insert(pNew); - } - - for (auto &hContact: lostIds) - db_delete_contact(hContact); - - // Clist - Clist_GroupCreate(0, m_wszDefaultGroup); - - HookProtoEvent(ME_GC_EVENT, &CDiscordProto::GroupchatEventHook); - HookProtoEvent(ME_GC_BUILDMENU, &CDiscordProto::GroupchatMenuHook); - - InitMenus(); - - // Voice support - if (g_plugin.bVoiceService) { - VOICE_MODULE voice = {}; - voice.cbSize = sizeof(voice); - voice.name = m_szModuleName; - voice.description = TranslateT("Discord voice call"); - voice.icon = m_hProtoIcon; - voice.flags = VOICE_CAPS_CALL_CONTACT | VOICE_CAPS_VOICE; - CallService(MS_VOICESERVICE_REGISTER, (WPARAM)&voice, 0); - } -} - -void CDiscordProto::OnShutdown() -{ - debugLogA("CDiscordProto::OnPreShutdown"); - - m_bTerminated = true; - SetEvent(m_evRequestsQueue); - - for (auto &it : arGuilds) - it->SaveToFile(); - - if (m_hGatewayConnection) - Netlib_Shutdown(m_hGatewayConnection); - - if (g_plugin.bVoiceService) - CallService(MS_VOICESERVICE_UNREGISTER, (WPARAM)m_szModuleName, 0); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -INT_PTR CDiscordProto::GetCaps(int type, MCONTACT) -{ - switch (type) { - case PFLAGNUM_1: - return PF1_IM | PF1_MODEMSG | PF1_MODEMSGRECV | PF1_SERVERCLIST | PF1_BASICSEARCH | PF1_EXTSEARCH | PF1_ADDSEARCHRES | PF1_FILESEND; - case PFLAGNUM_2: - return PF2_ONLINE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_HEAVYDND | PF2_INVISIBLE; - case PFLAGNUM_3: - return PF2_ONLINE | PF2_LONGAWAY | PF2_HEAVYDND | PF2_INVISIBLE; - case PFLAGNUM_4: - return PF4_FORCEAUTH | PF4_NOCUSTOMAUTH | PF4_NOAUTHDENYREASON | PF4_SUPPORTTYPING | PF4_SUPPORTIDLE | PF4_AVATARS | PF4_IMSENDOFFLINE | PF4_SERVERMSGID | PF4_OFFLINEFILES; - case PFLAG_UNIQUEIDTEXT: - return (INT_PTR)TranslateT("User ID"); - } - return 0; -} - -int CDiscordProto::SetStatus(int iNewStatus) -{ - debugLogA("CDiscordProto::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 (m_hWorkerThread == nullptr && !IsStatusConnecting(m_iStatus)) { - m_iStatus = ID_STATUS_CONNECTING; - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); - m_hWorkerThread = ForkThreadEx(&CDiscordProto::ServerThread, nullptr, nullptr); - } - else if (m_bOnline) { - debugLogA("setting server online status to %d", iNewStatus); - SetServerStatus(iNewStatus); - } - - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static INT_PTR CALLBACK AdvancedSearchDlgProc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM) -{ - switch (msg) { - case WM_INITDIALOG: - TranslateDialogDefault(hwndDlg); - SetFocus(GetDlgItem(hwndDlg, IDC_NICK)); - return TRUE; - - case WM_COMMAND: - if (HIWORD(wParam) == EN_SETFOCUS) - PostMessage(GetParent(hwndDlg), WM_COMMAND, MAKEWPARAM(0, EN_SETFOCUS), (LPARAM)hwndDlg); - } - return FALSE; -} - -HWND CDiscordProto::CreateExtendedSearchUI(HWND hwndParent) -{ - if (hwndParent) - return CreateDialogParam(g_plugin.getInst(), MAKEINTRESOURCE(IDD_EXTSEARCH), hwndParent, AdvancedSearchDlgProc, 0); - - return nullptr; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CDiscordProto::SearchThread(void *param) -{ - Sleep(100); - - PROTOSEARCHRESULT psr = { 0 }; - psr.cbSize = sizeof(psr); - psr.flags = PSR_UNICODE; - psr.nick.w = (wchar_t*)param; - psr.firstName.w = L""; - psr.lastName.w = L""; - psr.id.w = L""; - ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_DATA, (HANDLE)1, (LPARAM)&psr); - - ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)1, 0); - mir_free(param); -} - -HWND CDiscordProto::SearchAdvanced(HWND hwndDlg) -{ - if (!m_bOnline || !IsWindow(hwndDlg)) - return nullptr; - - wchar_t wszNick[200]; - GetDlgItemTextW(hwndDlg, IDC_NICK, wszNick, _countof(wszNick)); - if (wszNick[0] == 0) // empty string? reject - return nullptr; - - wchar_t *p = wcschr(wszNick, '#'); - if (p == nullptr) // wrong user id - return nullptr; - - ForkThread(&CDiscordProto::SearchThread, mir_wstrdup(wszNick)); - return (HWND)1; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Basic search - by SnowFlake - -void CDiscordProto::OnReceiveUserinfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) -{ - JsonReply root(pReply); - if (!root) { - ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_FAILED, (HANDLE)1); - return; - } - - auto &data = root.data(); - CMStringW wszUserId(data["username"].as_mstring() + L"#" + data["discriminator"].as_mstring()); - ForkThread(&CDiscordProto::SearchThread, wszUserId.Detach()); -} - -HANDLE CDiscordProto::SearchBasic(const wchar_t *wszId) -{ - if (!m_bOnline) - return nullptr; - - CMStringA szUrl = "/users/"; - szUrl.AppendFormat(ptrA(mir_utf8encodeW(wszId))); - Push(new AsyncHttpRequest(this, REQUEST_GET, szUrl, &CDiscordProto::OnReceiveUserinfo)); - return (HANDLE)1; // Success -} - -//////////////////////////////////////////////////////////////////////////////////////// -// Authorization - -int CDiscordProto::AuthRequest(MCONTACT hContact, const wchar_t*) -{ - ptrW wszUsername(getWStringA(hContact, DB_KEY_NICK)); - int iDiscriminator(getDword(hContact, DB_KEY_DISCR, -1)); - if (wszUsername == nullptr || iDiscriminator == -1) - return 1; // error - - JSONNode root; root << WCHAR_PARAM("username", wszUsername) << INT_PARAM("discriminator", iDiscriminator); - Push(new AsyncHttpRequest(this, REQUEST_POST, "/users/@me/relationships", nullptr, &root)); - return 0; -} - -int CDiscordProto::AuthRecv(MCONTACT, PROTORECVEVENT *pre) -{ - return Proto_AuthRecv(m_szModuleName, pre); -} - -int CDiscordProto::Authorize(MEVENT hDbEvent) -{ - DB::EventInfo dbei; - dbei.cbBlob = -1; - if (db_event_get(hDbEvent, &dbei)) return 1; - if (dbei.eventType != EVENTTYPE_AUTHREQUEST) return 1; - if (mir_strcmp(dbei.szModule, m_szModuleName)) return 1; - - JSONNode root; - MCONTACT hContact = DbGetAuthEventContact(&dbei); - CMStringA szUrl(FORMAT, "/users/@me/relationships/%lld", getId(hContact, DB_KEY_ID)); - Push(new AsyncHttpRequest(this, REQUEST_PUT, szUrl, nullptr, &root)); - return 0; -} - -int CDiscordProto::AuthDeny(MEVENT hDbEvent, const wchar_t*) -{ - DB::EventInfo dbei; - dbei.cbBlob = -1; - if (db_event_get(hDbEvent, &dbei)) return 1; - if (dbei.eventType != EVENTTYPE_AUTHREQUEST) return 1; - if (mir_strcmp(dbei.szModule, m_szModuleName)) return 1; - - MCONTACT hContact = DbGetAuthEventContact(&dbei); - RemoveFriend(getId(hContact, DB_KEY_ID)); - return 0; -} - -//////////////////////////////////////////////////////////////////////////////////////// - -MCONTACT CDiscordProto::AddToList(int flags, PROTOSEARCHRESULT *psr) -{ - if (mir_wstrlen(psr->nick.w) == 0) - return 0; - - wchar_t *p = wcschr(psr->nick.w, '#'); - if (p == nullptr) - return 0; - - MCONTACT hContact = db_add_contact(); - Proto_AddToContact(hContact, m_szModuleName); - if (flags & PALF_TEMPORARY) - Contact::RemoveFromList(hContact); - - *p = 0; - CDiscordUser *pUser = new CDiscordUser(0); - pUser->hContact = hContact; - pUser->wszUsername = psr->nick.w; - pUser->iDiscriminator = _wtoi(p + 1); - *p = '#'; - - if (mir_wstrlen(psr->id.w)) { - pUser->id = _wtoi64(psr->id.w); - setId(hContact, DB_KEY_ID, pUser->id); - } - - Clist_SetGroup(hContact, m_wszDefaultGroup); - setWString(hContact, DB_KEY_NICK, pUser->wszUsername); - setDword(hContact, DB_KEY_DISCR, pUser->iDiscriminator); - arUsers.insert(pUser); - - return hContact; -} - -MCONTACT CDiscordProto::AddToListByEvent(int flags, int, MEVENT hDbEvent) -{ - DB::EventInfo dbei; - dbei.cbBlob = -1; - if (db_event_get(hDbEvent, &dbei)) - return 0; - if (mir_strcmp(dbei.szModule, m_szModuleName)) - return 0; - if (dbei.eventType != EVENTTYPE_AUTHREQUEST) - return 0; - - DB::AUTH_BLOB blob(dbei.pBlob); - if (flags & PALF_TEMPORARY) - Contact::RemoveFromList(blob.get_contact()); - else - Contact::PutOnList(blob.get_contact()); - return blob.get_contact(); -} - -//////////////////////////////////////////////////////////////////////////////////////// -// SendMsg - -void CDiscordProto::OnSendMsg(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - JsonReply root(pReply); - if (!root) { - int iReqNum = -1; - for (auto &it : arOwnMessages) - if (it->reqId == pReq->m_iReqNum) { - iReqNum = it->reqId; - arOwnMessages.removeItem(&it); - break; - } - - if (iReqNum != -1) { - CMStringW wszErrorMsg(root.data()["message"].as_mstring()); - if (wszErrorMsg.IsEmpty()) - wszErrorMsg = TranslateT("Message send failed"); - ProtoBroadcastAck(pReq->hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, (HANDLE)iReqNum, (LPARAM)wszErrorMsg.c_str()); - } - } -} - -int CDiscordProto::SendMsg(MCONTACT hContact, int /*flags*/, const char *pszSrc) -{ - if (!m_bOnline) { - ProtoBroadcastAsync(hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, (HANDLE)1, (LPARAM)TranslateT("Protocol is offline or user isn't authorized yet")); - return 1; - } - - ptrW wszText(mir_utf8decodeW(pszSrc)); - if (wszText == nullptr) - return 0; - - CDiscordUser *pUser = FindUser(getId(hContact, DB_KEY_ID)); - if (pUser == nullptr || pUser->id == 0) - return 0; - - // no channel - we need to create one - if (pUser->channelId == 0) { - JSONNode list(JSON_ARRAY); list.set_name("recipients"); list << SINT64_PARAM("", pUser->id); - JSONNode body; body << list; - CMStringA szUrl(FORMAT, "/users/%lld/channels", m_ownId); - - // theoretically we get the same data from the gateway thread, but there could be a delay - // so we bind data analysis to the http packet reply - mir_cslock lck(m_csHttpQueue); - ExecuteRequest(new AsyncHttpRequest(this, REQUEST_POST, szUrl, &CDiscordProto::OnReceiveCreateChannel, &body)); - if (pUser->channelId == 0) - return 0; - } - - // we generate a random 64-bit integer and pass it to the server - // to distinguish our own messages from these generated by another clients - SnowFlake nonce; Utils_GetRandom(&nonce, sizeof(nonce)); nonce = abs(nonce); - JSONNode body; body << WCHAR_PARAM("content", wszText) << SINT64_PARAM("nonce", nonce); - - CMStringA szUrl(FORMAT, "/channels/%lld/messages", pUser->channelId); - AsyncHttpRequest *pReq = new AsyncHttpRequest(this, REQUEST_POST, szUrl, &CDiscordProto::OnSendMsg, &body); - pReq->hContact = hContact; - arOwnMessages.insert(new COwnMessage(nonce, pReq->m_iReqNum)); - Push(pReq); - return pReq->m_iReqNum; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void __cdecl CDiscordProto::GetAwayMsgThread(void *param) -{ - Thread_SetName("Jabber: GetAwayMsgThread"); - - auto *pUser = (CDiscordUser *)param; - if (pUser == nullptr) - return; - - if (pUser->wszTopic.IsEmpty()) - ProtoBroadcastAck(pUser->hContact, ACKTYPE_AWAYMSG, ACKRESULT_SUCCESS, (HANDLE)1, 0); - else - ProtoBroadcastAck(pUser->hContact, ACKTYPE_AWAYMSG, ACKRESULT_SUCCESS, (HANDLE)1, (LPARAM)pUser->wszTopic.c_str()); -} - -HANDLE CDiscordProto::GetAwayMsg(MCONTACT hContact) -{ - ForkThread(&CDiscordProto::GetAwayMsgThread, FindUser(getId(hContact, DB_KEY_ID))); - return (HANDLE)1; -} - -int CDiscordProto::SetAwayMsg(int iStatus, const wchar_t *msg) -{ - if (iStatus < ID_STATUS_MIN || iStatus > ID_STATUS_MAX) - return 0; - - wchar_t *&pwszMessage = m_wszStatusMsg[iStatus - ID_STATUS_MIN]; - if (!mir_wstrcmp(msg, pwszMessage)) - return 0; - - replaceStrW(pwszMessage, msg); - - if (m_bOnline) { - JSONNode status; status.set_name("custom_status"); status << WCHAR_PARAM("text", (msg) ? msg : L""); - JSONNode root; root << status; - Push(new AsyncHttpRequest(this, REQUEST_PATCH, "/users/@me/settings", nullptr, &root)); - } - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -int CDiscordProto::UserIsTyping(MCONTACT hContact, int type) -{ - if (type == PROTOTYPE_SELFTYPING_ON) { - CMStringA szUrl(FORMAT, "/channels/%lld/typing", getId(hContact, DB_KEY_CHANNELID)); - Push(new AsyncHttpRequest(this, REQUEST_POST, szUrl, nullptr)); - } - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CDiscordProto::OnReceiveMarkRead(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) -{ - JsonReply root(pReply); - if (root) - SaveToken(root.data()); -} - -void CDiscordProto::SendMarkRead() -{ - mir_cslock lck(csMarkReadQueue); - while (arMarkReadQueue.getCount()) { - CDiscordUser *pUser = arMarkReadQueue[0]; - JSONNode payload; payload << CHAR_PARAM("token", m_szTempToken); - CMStringA szUrl(FORMAT, "/channels/%lld/messages/%lld/ack", pUser->channelId, pUser->lastMsgId); - auto *pReq = new AsyncHttpRequest(this, REQUEST_POST, szUrl, &CDiscordProto::OnReceiveMarkRead, &payload); - Push(pReq); - arMarkReadQueue.remove(0); - } -} - -int CDiscordProto::OnDbEventRead(WPARAM, LPARAM hDbEvent) -{ - MCONTACT hContact = db_event_getContact(hDbEvent); - if (!hContact) - return 0; - - // filter out only events of my protocol - const char *szProto = Proto_GetBaseAccountName(hContact); - if (mir_strcmp(szProto, m_szModuleName)) - return 0; - - if (m_bOnline) { - m_impl.m_markRead.Start(200); - - CDiscordUser *pUser = FindUser(getId(hContact, DB_KEY_ID)); - if (pUser != nullptr) { - mir_cslock lck(csMarkReadQueue); - if (arMarkReadQueue.indexOf(pUser) == -1) - arMarkReadQueue.insert(pUser); - } - } - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -int CDiscordProto::OnAccountChanged(WPARAM iAction, LPARAM lParam) -{ - if (iAction == PRAC_ADDED) { - PROTOACCOUNT *pa = (PROTOACCOUNT*)lParam; - if (pa && pa->ppro == this) { - m_bUseGroupchats = false; - m_bUseGuildGroups = true; - } - } - - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CDiscordProto::OnContactDeleted(MCONTACT hContact) -{ - CDiscordUser *pUser = FindUser(getId(hContact, DB_KEY_ID)); - if (pUser == nullptr || !m_bOnline) - return; - - if (pUser->channelId) - Push(new AsyncHttpRequest(this, REQUEST_DELETE, CMStringA(FORMAT, "/channels/%lld", pUser->channelId), nullptr)); - - if (pUser->id) - RemoveFriend(pUser->id); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -INT_PTR CDiscordProto::RequestFriendship(WPARAM hContact, LPARAM) -{ - AuthRequest(hContact, 0); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -struct SendFileThreadParam -{ - MCONTACT hContact; - CMStringW wszDescr, wszFileName; - - SendFileThreadParam(MCONTACT _p1, LPCWSTR _p2, LPCWSTR _p3) : - hContact(_p1), - wszFileName(_p2), - wszDescr(_p3) - {} -}; - -void CDiscordProto::SendFileThread(void *param) -{ - SendFileThreadParam *p = (SendFileThreadParam*)param; - - FILE *in = _wfopen(p->wszFileName, L"rb"); - if (in == nullptr) { - debugLogA("cannot open file %S for reading", p->wszFileName.c_str()); - LBL_Error: - ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, param); - delete p; - return; - } - - ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_INITIALISING, param); - - char szRandom[16], szRandomText[33]; - Utils_GetRandom(szRandom, _countof(szRandom)); - bin2hex(szRandom, _countof(szRandom), szRandomText); - CMStringA szBoundary(FORMAT, "----Boundary%s", szRandomText); - - CMStringA szUrl(FORMAT, "/channels/%lld/messages", getId(p->hContact, DB_KEY_CHANNELID)); - AsyncHttpRequest *pReq = new AsyncHttpRequest(this, REQUEST_POST, szUrl, &CDiscordProto::OnReceiveFile); - pReq->AddHeader("Content-Type", CMStringA("multipart/form-data; boundary=" + szBoundary)); - pReq->AddHeader("Accept", "*/*"); - - szBoundary.Insert(0, "--"); - - CMStringA szBody; - szBody.Append(szBoundary + "\r\n"); - szBody.Append("Content-Disposition: form-data; name=\"content\"\r\n\r\n"); - szBody.Append(ptrA(mir_utf8encodeW(p->wszDescr))); - szBody.Append("\r\n"); - - szBody.Append(szBoundary + "\r\n"); - szBody.Append("Content-Disposition: form-data; name=\"tts\"\r\n\r\nfalse\r\n"); - - wchar_t *pFileName = wcsrchr(p->wszFileName.GetBuffer(), '\\'); - if (pFileName != nullptr) - pFileName++; - else - pFileName = p->wszFileName.GetBuffer(); - - szBody.Append(szBoundary + "\r\n"); - szBody.AppendFormat("Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n", ptrA(mir_utf8encodeW(pFileName)).get()); - szBody.AppendFormat("Content-Type: %S\r\n", ProtoGetAvatarMimeType(ProtoGetAvatarFileFormat(p->wszFileName))); - szBody.Append("\r\n"); - - size_t cbBytes = filelength(fileno(in)); - - szBoundary.Insert(0, "\r\n"); - szBoundary.Append("--\r\n"); - pReq->dataLength = int(szBody.GetLength() + szBoundary.GetLength() + cbBytes); - pReq->pData = (char*)mir_alloc(pReq->dataLength+1); - memcpy(pReq->pData, szBody.c_str(), szBody.GetLength()); - size_t cbRead = fread(pReq->pData + szBody.GetLength(), 1, cbBytes, in); - fclose(in); - if (cbBytes != cbRead) { - debugLogA("cannot read file %S: %d bytes read instead of %d", p->wszFileName.c_str(), cbRead, cbBytes); - delete pReq; - goto LBL_Error; - } - - memcpy(pReq->pData + szBody.GetLength() + cbBytes, szBoundary, szBoundary.GetLength()); - pReq->pUserInfo = p; - Push(pReq); - - ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_CONNECTED, param); -} - -void CDiscordProto::OnReceiveFile(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - SendFileThreadParam *p = (SendFileThreadParam*)pReq->pUserInfo; - if (pReply->resultCode != 200) { - ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, p); - debugLogA("CDiscordProto::SendFile failed: %d", pReply->resultCode); - } - else { - ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, p); - debugLogA("CDiscordProto::SendFile succeeded"); - } - - delete p; -} - -HANDLE CDiscordProto::SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) -{ - SnowFlake id = getId(hContact, DB_KEY_CHANNELID); - if (id == 0) - return nullptr; - - // we don't wanna block the main thread, right? - SendFileThreadParam *param = new SendFileThreadParam(hContact, ppszFiles[0], szDescription); - ForkThread(&CDiscordProto::SendFileThread, param); - return param; -} +/* +Copyright © 2016-22 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +static int compareMessages(const COwnMessage *p1, const COwnMessage *p2) +{ + return compareInt64(p1->nonce, p2->nonce); +} + +static int compareRequests(const AsyncHttpRequest *p1, const AsyncHttpRequest *p2) +{ + return p1->m_iReqNum - p2->m_iReqNum; +} + +int compareUsers(const CDiscordUser *p1, const CDiscordUser *p2) +{ + return compareInt64(p1->id, p2->id); +} + +static int compareGuilds(const CDiscordGuild *p1, const CDiscordGuild *p2) +{ + return compareInt64(p1->id, p2->id); +} + +CDiscordProto::CDiscordProto(const char *proto_name, const wchar_t *username) : + PROTO(proto_name, username), + m_impl(*this), + m_arHttpQueue(10, compareRequests), + m_evRequestsQueue(CreateEvent(nullptr, FALSE, FALSE, nullptr)), + arUsers(10, compareUsers), + arGuilds(1, compareGuilds), + arMarkReadQueue(1, compareUsers), + arOwnMessages(1, compareMessages), + arVoiceCalls(1), + + m_wszEmail(this, "Email", L""), + m_wszDefaultGroup(this, "GroupName", DB_KEYVAL_GROUP), + m_bUseGroupchats(this, "UseGroupChats", true), + m_bHideGroupchats(this, "HideChats", true), + m_bUseGuildGroups(this, "UseGuildGroups", false), + m_bSyncDeleteMsgs(this, "DeleteServerMsgs", true) +{ + // Services + CreateProtoService(PS_CREATEACCMGRUI, &CDiscordProto::SvcCreateAccMgrUI); + + CreateProtoService(PS_GETAVATARINFO, &CDiscordProto::GetAvatarInfo); + CreateProtoService(PS_GETAVATARCAPS, &CDiscordProto::GetAvatarCaps); + CreateProtoService(PS_GETMYAVATAR, &CDiscordProto::GetMyAvatar); + CreateProtoService(PS_SETMYAVATAR, &CDiscordProto::SetMyAvatar); + + CreateProtoService(PS_MENU_REQAUTH, &CDiscordProto::RequestFriendship); + CreateProtoService(PS_MENU_LOADHISTORY, &CDiscordProto::OnMenuLoadHistory); + + CreateProtoService(PS_VOICE_CAPS, &CDiscordProto::VoiceCaps); + + // Events + HookProtoEvent(ME_OPT_INITIALISE, &CDiscordProto::OnOptionsInit); + HookProtoEvent(ME_DB_EVENT_MARKED_READ, &CDiscordProto::OnDbEventRead); + HookProtoEvent(ME_PROTO_ACCLISTCHANGED, &CDiscordProto::OnAccountChanged); + + HookProtoEvent(PE_VOICE_CALL_STATE, &CDiscordProto::OnVoiceState); + + // database + db_set_resident(m_szModuleName, "XStatusMsg"); + + // custom events + DBEVENTTYPEDESCR dbEventType = {}; + dbEventType.module = m_szModuleName; + dbEventType.flags = DETF_HISTORY | DETF_MSGWINDOW; + + dbEventType.eventType = EVENT_INCOMING_CALL; + dbEventType.descr = Translate("Incoming call"); + dbEventType.eventIcon = g_plugin.getIconHandle(IDI_VOICE_CALL); + DbEvent_RegisterType(&dbEventType); + + dbEventType.eventType = EVENT_CALL_FINISHED; + dbEventType.descr = Translate("Call ended"); + dbEventType.eventIcon = g_plugin.getIconHandle(IDI_VOICE_ENDED); + DbEvent_RegisterType(&dbEventType); + + // Groupchat initialization + GCREGISTER gcr = {}; + gcr.dwFlags = GC_TYPNOTIF | GC_CHANMGR; + gcr.ptszDispName = m_tszUserName; + gcr.pszModule = m_szModuleName; + Chat_Register(&gcr); + + // Network initialization + CMStringW descr; + NETLIBUSER nlu = {}; + + nlu.szSettingsModule = m_szModuleName; + nlu.flags = NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; + descr.Format(TranslateT("%s server connection"), m_tszUserName); + nlu.szDescriptiveName.w = descr.GetBuffer(); + m_hNetlibUser = Netlib_RegisterUser(&nlu); + + CMStringA module(FORMAT, "%s.Gateway", m_szModuleName); + nlu.szSettingsModule = module.GetBuffer(); + nlu.flags = NUF_OUTGOING | NUF_UNICODE; + descr.Format(TranslateT("%s gateway connection"), m_tszUserName); + nlu.szDescriptiveName.w = descr.GetBuffer(); + m_hGatewayNetlibUser = Netlib_RegisterUser(&nlu); +} + +CDiscordProto::~CDiscordProto() +{ + debugLogA("CDiscordProto::~CDiscordProto"); + + for (auto &msg : m_wszStatusMsg) + mir_free(msg); + + arUsers.destroy(); + + m_arHttpQueue.destroy(); + ::CloseHandle(m_evRequestsQueue); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordProto::OnModulesLoaded() +{ + std::vector lostIds; + + // Fill users list + for (auto &hContact : AccContacts()) { + CDiscordUser *pNew = new CDiscordUser(getId(hContact, DB_KEY_ID)); + pNew->hContact = hContact; + pNew->lastMsgId = getId(hContact, DB_KEY_LASTMSGID); + pNew->wszUsername = ptrW(getWStringA(hContact, DB_KEY_NICK)); + pNew->iDiscriminator = getDword(hContact, DB_KEY_DISCR); + + // set EnableSync = 1 by default for all existing guilds + switch (getByte(hContact, "ChatRoom")) { + case 2: // guild + delSetting(hContact, DB_KEY_CHANNELID); + if (getDword(hContact, "EnableSync", -1) == -1) + setDword(hContact, "EnableSync", 1); + break; + + case 1: // group chat + pNew->channelId = getId(hContact, DB_KEY_CHANNELID); + if (!pNew->channelId) { + lostIds.push_back(hContact); + delete pNew; + continue; + } + break; + + default: + pNew->channelId = getId(hContact, DB_KEY_CHANNELID); + break; + } + arUsers.insert(pNew); + } + + for (auto &hContact: lostIds) + db_delete_contact(hContact); + + // Clist + Clist_GroupCreate(0, m_wszDefaultGroup); + + HookProtoEvent(ME_GC_EVENT, &CDiscordProto::GroupchatEventHook); + HookProtoEvent(ME_GC_BUILDMENU, &CDiscordProto::GroupchatMenuHook); + + InitMenus(); + + // Voice support + if (g_plugin.bVoiceService) { + VOICE_MODULE voice = {}; + voice.cbSize = sizeof(voice); + voice.name = m_szModuleName; + voice.description = TranslateT("Discord voice call"); + voice.icon = m_hProtoIcon; + voice.flags = VOICE_CAPS_CALL_CONTACT | VOICE_CAPS_VOICE; + CallService(MS_VOICESERVICE_REGISTER, (WPARAM)&voice, 0); + } +} + +void CDiscordProto::OnShutdown() +{ + debugLogA("CDiscordProto::OnPreShutdown"); + + m_bTerminated = true; + SetEvent(m_evRequestsQueue); + + for (auto &it : arGuilds) + it->SaveToFile(); + + if (m_hGatewayConnection) + Netlib_Shutdown(m_hGatewayConnection); + + if (g_plugin.bVoiceService) + CallService(MS_VOICESERVICE_UNREGISTER, (WPARAM)m_szModuleName, 0); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR CDiscordProto::GetCaps(int type, MCONTACT) +{ + switch (type) { + case PFLAGNUM_1: + return PF1_IM | PF1_MODEMSG | PF1_MODEMSGRECV | PF1_SERVERCLIST | PF1_BASICSEARCH | PF1_EXTSEARCH | PF1_ADDSEARCHRES | PF1_FILESEND; + case PFLAGNUM_2: + return PF2_ONLINE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_HEAVYDND | PF2_INVISIBLE; + case PFLAGNUM_3: + return PF2_ONLINE | PF2_LONGAWAY | PF2_HEAVYDND | PF2_INVISIBLE; + case PFLAGNUM_4: + return PF4_FORCEAUTH | PF4_NOCUSTOMAUTH | PF4_NOAUTHDENYREASON | PF4_SUPPORTTYPING | PF4_SUPPORTIDLE | PF4_AVATARS | PF4_IMSENDOFFLINE | PF4_SERVERMSGID | PF4_OFFLINEFILES; + case PFLAG_UNIQUEIDTEXT: + return (INT_PTR)TranslateT("User ID"); + } + return 0; +} + +int CDiscordProto::SetStatus(int iNewStatus) +{ + debugLogA("CDiscordProto::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 (m_hWorkerThread == nullptr && !IsStatusConnecting(m_iStatus)) { + m_iStatus = ID_STATUS_CONNECTING; + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); + m_hWorkerThread = ForkThreadEx(&CDiscordProto::ServerThread, nullptr, nullptr); + } + else if (m_bOnline) { + debugLogA("setting server online status to %d", iNewStatus); + SetServerStatus(iNewStatus); + } + + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static INT_PTR CALLBACK AdvancedSearchDlgProc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM) +{ + switch (msg) { + case WM_INITDIALOG: + TranslateDialogDefault(hwndDlg); + SetFocus(GetDlgItem(hwndDlg, IDC_NICK)); + return TRUE; + + case WM_COMMAND: + if (HIWORD(wParam) == EN_SETFOCUS) + PostMessage(GetParent(hwndDlg), WM_COMMAND, MAKEWPARAM(0, EN_SETFOCUS), (LPARAM)hwndDlg); + } + return FALSE; +} + +HWND CDiscordProto::CreateExtendedSearchUI(HWND hwndParent) +{ + if (hwndParent) + return CreateDialogParam(g_plugin.getInst(), MAKEINTRESOURCE(IDD_EXTSEARCH), hwndParent, AdvancedSearchDlgProc, 0); + + return nullptr; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordProto::SearchThread(void *param) +{ + Sleep(100); + + PROTOSEARCHRESULT psr = { 0 }; + psr.cbSize = sizeof(psr); + psr.flags = PSR_UNICODE; + psr.nick.w = (wchar_t*)param; + psr.firstName.w = L""; + psr.lastName.w = L""; + psr.id.w = L""; + ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_DATA, (HANDLE)1, (LPARAM)&psr); + + ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)1, 0); + mir_free(param); +} + +HWND CDiscordProto::SearchAdvanced(HWND hwndDlg) +{ + if (!m_bOnline || !IsWindow(hwndDlg)) + return nullptr; + + wchar_t wszNick[200]; + GetDlgItemTextW(hwndDlg, IDC_NICK, wszNick, _countof(wszNick)); + if (wszNick[0] == 0) // empty string? reject + return nullptr; + + wchar_t *p = wcschr(wszNick, '#'); + if (p == nullptr) // wrong user id + return nullptr; + + ForkThread(&CDiscordProto::SearchThread, mir_wstrdup(wszNick)); + return (HWND)1; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Basic search - by SnowFlake + +void CDiscordProto::OnReceiveUserinfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) +{ + JsonReply root(pReply); + if (!root) { + ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_FAILED, (HANDLE)1); + return; + } + + auto &data = root.data(); + CMStringW wszUserId(data["username"].as_mstring() + L"#" + data["discriminator"].as_mstring()); + ForkThread(&CDiscordProto::SearchThread, wszUserId.Detach()); +} + +HANDLE CDiscordProto::SearchBasic(const wchar_t *wszId) +{ + if (!m_bOnline) + return nullptr; + + CMStringA szUrl = "/users/"; + szUrl.AppendFormat(ptrA(mir_utf8encodeW(wszId))); + Push(new AsyncHttpRequest(this, REQUEST_GET, szUrl, &CDiscordProto::OnReceiveUserinfo)); + return (HANDLE)1; // Success +} + +//////////////////////////////////////////////////////////////////////////////////////// +// Authorization + +int CDiscordProto::AuthRequest(MCONTACT hContact, const wchar_t*) +{ + ptrW wszUsername(getWStringA(hContact, DB_KEY_NICK)); + int iDiscriminator(getDword(hContact, DB_KEY_DISCR, -1)); + if (wszUsername == nullptr || iDiscriminator == -1) + return 1; // error + + JSONNode root; root << WCHAR_PARAM("username", wszUsername) << INT_PARAM("discriminator", iDiscriminator); + Push(new AsyncHttpRequest(this, REQUEST_POST, "/users/@me/relationships", nullptr, &root)); + return 0; +} + +int CDiscordProto::AuthRecv(MCONTACT, PROTORECVEVENT *pre) +{ + return Proto_AuthRecv(m_szModuleName, pre); +} + +int CDiscordProto::Authorize(MEVENT hDbEvent) +{ + DB::EventInfo dbei; + dbei.cbBlob = -1; + if (db_event_get(hDbEvent, &dbei)) return 1; + if (dbei.eventType != EVENTTYPE_AUTHREQUEST) return 1; + if (mir_strcmp(dbei.szModule, m_szModuleName)) return 1; + + JSONNode root; + MCONTACT hContact = DbGetAuthEventContact(&dbei); + CMStringA szUrl(FORMAT, "/users/@me/relationships/%lld", getId(hContact, DB_KEY_ID)); + Push(new AsyncHttpRequest(this, REQUEST_PUT, szUrl, nullptr, &root)); + return 0; +} + +int CDiscordProto::AuthDeny(MEVENT hDbEvent, const wchar_t*) +{ + DB::EventInfo dbei; + dbei.cbBlob = -1; + if (db_event_get(hDbEvent, &dbei)) return 1; + if (dbei.eventType != EVENTTYPE_AUTHREQUEST) return 1; + if (mir_strcmp(dbei.szModule, m_szModuleName)) return 1; + + MCONTACT hContact = DbGetAuthEventContact(&dbei); + RemoveFriend(getId(hContact, DB_KEY_ID)); + return 0; +} + +//////////////////////////////////////////////////////////////////////////////////////// + +MCONTACT CDiscordProto::AddToList(int flags, PROTOSEARCHRESULT *psr) +{ + if (mir_wstrlen(psr->nick.w) == 0) + return 0; + + wchar_t *p = wcschr(psr->nick.w, '#'); + if (p == nullptr) + return 0; + + MCONTACT hContact = db_add_contact(); + Proto_AddToContact(hContact, m_szModuleName); + if (flags & PALF_TEMPORARY) + Contact::RemoveFromList(hContact); + + *p = 0; + CDiscordUser *pUser = new CDiscordUser(0); + pUser->hContact = hContact; + pUser->wszUsername = psr->nick.w; + pUser->iDiscriminator = _wtoi(p + 1); + *p = '#'; + + if (mir_wstrlen(psr->id.w)) { + pUser->id = _wtoi64(psr->id.w); + setId(hContact, DB_KEY_ID, pUser->id); + } + + Clist_SetGroup(hContact, m_wszDefaultGroup); + setWString(hContact, DB_KEY_NICK, pUser->wszUsername); + setDword(hContact, DB_KEY_DISCR, pUser->iDiscriminator); + arUsers.insert(pUser); + + return hContact; +} + +MCONTACT CDiscordProto::AddToListByEvent(int flags, int, MEVENT hDbEvent) +{ + DB::EventInfo dbei; + dbei.cbBlob = -1; + if (db_event_get(hDbEvent, &dbei)) + return 0; + if (mir_strcmp(dbei.szModule, m_szModuleName)) + return 0; + if (dbei.eventType != EVENTTYPE_AUTHREQUEST) + return 0; + + DB::AUTH_BLOB blob(dbei.pBlob); + if (flags & PALF_TEMPORARY) + Contact::RemoveFromList(blob.get_contact()); + else + Contact::PutOnList(blob.get_contact()); + return blob.get_contact(); +} + +//////////////////////////////////////////////////////////////////////////////////////// +// SendMsg + +void CDiscordProto::OnSendMsg(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + JsonReply root(pReply); + if (!root) { + int iReqNum = -1; + for (auto &it : arOwnMessages) + if (it->reqId == pReq->m_iReqNum) { + iReqNum = it->reqId; + arOwnMessages.removeItem(&it); + break; + } + + if (iReqNum != -1) { + CMStringW wszErrorMsg(root.data()["message"].as_mstring()); + if (wszErrorMsg.IsEmpty()) + wszErrorMsg = TranslateT("Message send failed"); + ProtoBroadcastAck(pReq->hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, (HANDLE)iReqNum, (LPARAM)wszErrorMsg.c_str()); + } + } +} + +int CDiscordProto::SendMsg(MCONTACT hContact, int /*flags*/, const char *pszSrc) +{ + if (!m_bOnline) { + ProtoBroadcastAsync(hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, (HANDLE)1, (LPARAM)TranslateT("Protocol is offline or user isn't authorized yet")); + return 1; + } + + ptrW wszText(mir_utf8decodeW(pszSrc)); + if (wszText == nullptr) + return 0; + + CDiscordUser *pUser = FindUser(getId(hContact, DB_KEY_ID)); + if (pUser == nullptr || pUser->id == 0) + return 0; + + // no channel - we need to create one + if (pUser->channelId == 0) { + JSONNode list(JSON_ARRAY); list.set_name("recipients"); list << SINT64_PARAM("", pUser->id); + JSONNode body; body << list; + CMStringA szUrl(FORMAT, "/users/%lld/channels", m_ownId); + + // theoretically we get the same data from the gateway thread, but there could be a delay + // so we bind data analysis to the http packet reply + mir_cslock lck(m_csHttpQueue); + ExecuteRequest(new AsyncHttpRequest(this, REQUEST_POST, szUrl, &CDiscordProto::OnReceiveCreateChannel, &body)); + if (pUser->channelId == 0) + return 0; + } + + // we generate a random 64-bit integer and pass it to the server + // to distinguish our own messages from these generated by another clients + SnowFlake nonce; Utils_GetRandom(&nonce, sizeof(nonce)); nonce = abs(nonce); + JSONNode body; body << WCHAR_PARAM("content", wszText) << SINT64_PARAM("nonce", nonce); + + CMStringA szUrl(FORMAT, "/channels/%lld/messages", pUser->channelId); + AsyncHttpRequest *pReq = new AsyncHttpRequest(this, REQUEST_POST, szUrl, &CDiscordProto::OnSendMsg, &body); + pReq->hContact = hContact; + arOwnMessages.insert(new COwnMessage(nonce, pReq->m_iReqNum)); + Push(pReq); + return pReq->m_iReqNum; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void __cdecl CDiscordProto::GetAwayMsgThread(void *param) +{ + Thread_SetName("Jabber: GetAwayMsgThread"); + + auto *pUser = (CDiscordUser *)param; + if (pUser == nullptr) + return; + + if (pUser->wszTopic.IsEmpty()) + ProtoBroadcastAck(pUser->hContact, ACKTYPE_AWAYMSG, ACKRESULT_SUCCESS, (HANDLE)1, 0); + else + ProtoBroadcastAck(pUser->hContact, ACKTYPE_AWAYMSG, ACKRESULT_SUCCESS, (HANDLE)1, (LPARAM)pUser->wszTopic.c_str()); +} + +HANDLE CDiscordProto::GetAwayMsg(MCONTACT hContact) +{ + ForkThread(&CDiscordProto::GetAwayMsgThread, FindUser(getId(hContact, DB_KEY_ID))); + return (HANDLE)1; +} + +int CDiscordProto::SetAwayMsg(int iStatus, const wchar_t *msg) +{ + if (iStatus < ID_STATUS_MIN || iStatus > ID_STATUS_MAX) + return 0; + + wchar_t *&pwszMessage = m_wszStatusMsg[iStatus - ID_STATUS_MIN]; + if (!mir_wstrcmp(msg, pwszMessage)) + return 0; + + replaceStrW(pwszMessage, msg); + + if (m_bOnline) { + JSONNode status; status.set_name("custom_status"); status << WCHAR_PARAM("text", (msg) ? msg : L""); + JSONNode root; root << status; + Push(new AsyncHttpRequest(this, REQUEST_PATCH, "/users/@me/settings", nullptr, &root)); + } + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +int CDiscordProto::UserIsTyping(MCONTACT hContact, int type) +{ + if (type == PROTOTYPE_SELFTYPING_ON) { + CMStringA szUrl(FORMAT, "/channels/%lld/typing", getId(hContact, DB_KEY_CHANNELID)); + Push(new AsyncHttpRequest(this, REQUEST_POST, szUrl, nullptr)); + } + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordProto::OnReceiveMarkRead(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) +{ + JsonReply root(pReply); + if (root) + SaveToken(root.data()); +} + +void CDiscordProto::SendMarkRead() +{ + mir_cslock lck(csMarkReadQueue); + while (arMarkReadQueue.getCount()) { + CDiscordUser *pUser = arMarkReadQueue[0]; + JSONNode payload; payload << CHAR_PARAM("token", m_szTempToken); + CMStringA szUrl(FORMAT, "/channels/%lld/messages/%lld/ack", pUser->channelId, pUser->lastMsgId); + auto *pReq = new AsyncHttpRequest(this, REQUEST_POST, szUrl, &CDiscordProto::OnReceiveMarkRead, &payload); + Push(pReq); + arMarkReadQueue.remove(0); + } +} + +int CDiscordProto::OnDbEventRead(WPARAM, LPARAM hDbEvent) +{ + MCONTACT hContact = db_event_getContact(hDbEvent); + if (!hContact) + return 0; + + // filter out only events of my protocol + const char *szProto = Proto_GetBaseAccountName(hContact); + if (mir_strcmp(szProto, m_szModuleName)) + return 0; + + if (m_bOnline) { + m_impl.m_markRead.Start(200); + + CDiscordUser *pUser = FindUser(getId(hContact, DB_KEY_ID)); + if (pUser != nullptr) { + mir_cslock lck(csMarkReadQueue); + if (arMarkReadQueue.indexOf(pUser) == -1) + arMarkReadQueue.insert(pUser); + } + } + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +int CDiscordProto::OnAccountChanged(WPARAM iAction, LPARAM lParam) +{ + if (iAction == PRAC_ADDED) { + PROTOACCOUNT *pa = (PROTOACCOUNT*)lParam; + if (pa && pa->ppro == this) { + m_bUseGroupchats = false; + m_bUseGuildGroups = true; + } + } + + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordProto::OnContactDeleted(MCONTACT hContact) +{ + CDiscordUser *pUser = FindUser(getId(hContact, DB_KEY_ID)); + if (pUser == nullptr || !m_bOnline) + return; + + if (pUser->channelId) + Push(new AsyncHttpRequest(this, REQUEST_DELETE, CMStringA(FORMAT, "/channels/%lld", pUser->channelId), nullptr)); + + if (pUser->id) + RemoveFriend(pUser->id); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR CDiscordProto::RequestFriendship(WPARAM hContact, LPARAM) +{ + AuthRequest(hContact, 0); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +struct SendFileThreadParam +{ + MCONTACT hContact; + CMStringW wszDescr, wszFileName; + + SendFileThreadParam(MCONTACT _p1, LPCWSTR _p2, LPCWSTR _p3) : + hContact(_p1), + wszFileName(_p2), + wszDescr(_p3) + {} +}; + +void CDiscordProto::SendFileThread(void *param) +{ + SendFileThreadParam *p = (SendFileThreadParam*)param; + + FILE *in = _wfopen(p->wszFileName, L"rb"); + if (in == nullptr) { + debugLogA("cannot open file %S for reading", p->wszFileName.c_str()); + LBL_Error: + ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, param); + delete p; + return; + } + + ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_INITIALISING, param); + + char szRandom[16], szRandomText[33]; + Utils_GetRandom(szRandom, _countof(szRandom)); + bin2hex(szRandom, _countof(szRandom), szRandomText); + CMStringA szBoundary(FORMAT, "----Boundary%s", szRandomText); + + CMStringA szUrl(FORMAT, "/channels/%lld/messages", getId(p->hContact, DB_KEY_CHANNELID)); + AsyncHttpRequest *pReq = new AsyncHttpRequest(this, REQUEST_POST, szUrl, &CDiscordProto::OnReceiveFile); + pReq->AddHeader("Content-Type", CMStringA("multipart/form-data; boundary=" + szBoundary)); + pReq->AddHeader("Accept", "*/*"); + + szBoundary.Insert(0, "--"); + + CMStringA szBody; + szBody.Append(szBoundary + "\r\n"); + szBody.Append("Content-Disposition: form-data; name=\"content\"\r\n\r\n"); + szBody.Append(ptrA(mir_utf8encodeW(p->wszDescr))); + szBody.Append("\r\n"); + + szBody.Append(szBoundary + "\r\n"); + szBody.Append("Content-Disposition: form-data; name=\"tts\"\r\n\r\nfalse\r\n"); + + wchar_t *pFileName = wcsrchr(p->wszFileName.GetBuffer(), '\\'); + if (pFileName != nullptr) + pFileName++; + else + pFileName = p->wszFileName.GetBuffer(); + + szBody.Append(szBoundary + "\r\n"); + szBody.AppendFormat("Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n", ptrA(mir_utf8encodeW(pFileName)).get()); + szBody.AppendFormat("Content-Type: %S\r\n", ProtoGetAvatarMimeType(ProtoGetAvatarFileFormat(p->wszFileName))); + szBody.Append("\r\n"); + + size_t cbBytes = filelength(fileno(in)); + + szBoundary.Insert(0, "\r\n"); + szBoundary.Append("--\r\n"); + pReq->dataLength = int(szBody.GetLength() + szBoundary.GetLength() + cbBytes); + pReq->pData = (char*)mir_alloc(pReq->dataLength+1); + memcpy(pReq->pData, szBody.c_str(), szBody.GetLength()); + size_t cbRead = fread(pReq->pData + szBody.GetLength(), 1, cbBytes, in); + fclose(in); + if (cbBytes != cbRead) { + debugLogA("cannot read file %S: %d bytes read instead of %d", p->wszFileName.c_str(), cbRead, cbBytes); + delete pReq; + goto LBL_Error; + } + + memcpy(pReq->pData + szBody.GetLength() + cbBytes, szBoundary, szBoundary.GetLength()); + pReq->pUserInfo = p; + Push(pReq); + + ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_CONNECTED, param); +} + +void CDiscordProto::OnReceiveFile(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + SendFileThreadParam *p = (SendFileThreadParam*)pReq->pUserInfo; + if (pReply->resultCode != 200) { + ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, p); + debugLogA("CDiscordProto::SendFile failed: %d", pReply->resultCode); + } + else { + ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, p); + debugLogA("CDiscordProto::SendFile succeeded"); + } + + delete p; +} + +HANDLE CDiscordProto::SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) +{ + SnowFlake id = getId(hContact, DB_KEY_CHANNELID); + if (id == 0) + return nullptr; + + // we don't wanna block the main thread, right? + SendFileThreadParam *param = new SendFileThreadParam(hContact, ppszFiles[0], szDescription); + ForkThread(&CDiscordProto::SendFileThread, param); + return param; +} diff --git a/protocols/Discord/src/proto.h b/protocols/Discord/src/proto.h index bf3929fd55..b5262f4e0a 100644 --- a/protocols/Discord/src/proto.h +++ b/protocols/Discord/src/proto.h @@ -1,476 +1,476 @@ -#pragma once - -#define EVENT_INCOMING_CALL 10001 -#define EVENT_CALL_FINISHED 10002 - -typedef __int64 SnowFlake; - -__forceinline int compareInt64(const SnowFlake i1, const SnowFlake i2) -{ - return (i1 == i2) ? 0 : (i1 < i2) ? -1 : 1; -} - -class CDiscordProto; -typedef void (CDiscordProto::*GatewayHandlerFunc)(const JSONNode&); - -struct AsyncHttpRequest : public MTHttpRequest -{ - AsyncHttpRequest(CDiscordProto*, int iRequestType, LPCSTR szUrl, MTHttpRequestHandler pFunc, JSONNode *pNode = nullptr); - - int m_iErrorCode, m_iReqNum; - bool m_bMainSite; - MCONTACT hContact; -}; - -class JsonReply -{ - JSONNode *m_root = nullptr; - int m_errorCode = 0; - -public: - JsonReply(NETLIBHTTPREQUEST *); - ~JsonReply(); - - __forceinline int error() const { return m_errorCode; } - __forceinline JSONNode& data() const { return *m_root; } - __forceinline operator bool() const { return m_errorCode == 200; } -}; - -///////////////////////////////////////////////////////////////////////////////////////// - -struct CDiscordRole : public MZeroedObject -{ - SnowFlake id; - COLORREF color; - uint32_t permissions; - int position; - CMStringW wszName; -}; - -///////////////////////////////////////////////////////////////////////////////////////// - -struct COwnMessage -{ - SnowFlake nonce; - int reqId; - - COwnMessage(SnowFlake _id, int _reqId) : - nonce(_id), - reqId(_reqId) - {} -}; - -///////////////////////////////////////////////////////////////////////////////////////// - -enum CDiscordHistoryOp -{ - MSG_NOFILTER, MSG_AFTER, MSG_BEFORE -}; - -struct CDiscordUser : public MZeroedObject -{ - CDiscordUser(SnowFlake _id) : - id(_id) - {} - - ~CDiscordUser(); - - SnowFlake id; - MCONTACT hContact; - - SnowFlake channelId; - SnowFlake lastReadId, lastMsgId; - SnowFlake parentId; - bool bIsPrivate; - bool bIsGroup; - bool bSynced; - - struct CDiscordGuild *pGuild; - - CMStringW wszUsername, wszChannelName, wszTopic; - int iDiscriminator; -}; - -///////////////////////////////////////////////////////////////////////////////////////// - -struct CDiscordGuildMember : public MZeroedObject -{ - CDiscordGuildMember(SnowFlake id) : - userId(id) - {} - - ~CDiscordGuildMember() - {} - - SnowFlake userId; - CMStringW wszDiscordId, wszNick, wszRole; - int iStatus; -}; - -struct CDiscordGuild : public MZeroedObject -{ - CDiscordGuild(SnowFlake _id); - ~CDiscordGuild(); - - __forceinline CDiscordGuildMember* FindUser(SnowFlake userId) - { - return arChatUsers.find((CDiscordGuildMember *)&userId); - } - - __inline CMStringW GetCacheFile() const - { - return CMStringW(FORMAT, L"%s\\DiscordCache\\%lld.json", VARSW(L"%miranda_userdata%").get(), id); - } - - SnowFlake id, ownerId; - CMStringW wszName; - MCONTACT hContact; - MGROUP groupId; - bool bSynced = false; - LIST arChannels; - - SESSION_INFO *pParentSi; - OBJLIST arChatUsers; - OBJLIST arRoles; // guild roles - - void LoadFromFile(); - void SaveToFile(); -}; - -struct CDiscordVoiceCall -{ - CMStringA szId; - SnowFlake channelId; - time_t startTime; -}; - -///////////////////////////////////////////////////////////////////////////////////////// - -#define OPCODE_DISPATCH 0 -#define OPCODE_HEARTBEAT 1 -#define OPCODE_IDENTIFY 2 -#define OPCODE_STATUS_UPDATE 3 -#define OPCODE_VOICE_UPDATE 4 -#define OPCODE_VOICE_PING 5 -#define OPCODE_RESUME 6 -#define OPCODE_RECONNECT 7 -#define OPCODE_REQUEST_MEMBERS 8 -#define OPCODE_INVALID_SESSION 9 -#define OPCODE_HELLO 10 -#define OPCODE_HEARTBEAT_ACK 11 -#define OPCODE_REQUEST_SYNC 12 -#define OPCODE_REQUEST_SYNC_GROUP 13 -#define OPCODE_REQUEST_SYNC_CHANNEL 14 - -class CDiscordProto : public PROTO -{ - friend struct AsyncHttpRequest; - friend class CDiscardAccountOptions; - - class CDiscordProtoImpl - { - friend class CDiscordProto; - CDiscordProto &m_proto; - - CTimer m_heartBeat, m_markRead; - void OnHeartBeat(CTimer *) { - m_proto.GatewaySendHeartbeat(); - } - - void OnMarkRead(CTimer *pTimer) { - m_proto.SendMarkRead(); - pTimer->Stop(); - } - - CDiscordProtoImpl(CDiscordProto &pro) : - m_proto(pro), - m_markRead(Miranda_GetSystemWindow(), UINT_PTR(this)), - m_heartBeat(Miranda_GetSystemWindow(), UINT_PTR(this) + 1) - { - m_markRead.OnEvent = Callback(this, &CDiscordProtoImpl::OnMarkRead); - m_heartBeat.OnEvent = Callback(this, &CDiscordProtoImpl::OnHeartBeat); - } - } m_impl; - - ////////////////////////////////////////////////////////////////////////////////////// - // threads - - void __cdecl SendFileThread(void*); - void __cdecl ServerThread(void*); - void __cdecl SearchThread(void *param); - void __cdecl BatchChatCreate(void* param); - void __cdecl GetAwayMsgThread(void *param); - - ////////////////////////////////////////////////////////////////////////////////////// - // session control - - void ConnectionFailed(int iReason); - void ShutdownSession(void); - - wchar_t *m_wszStatusMsg[MAX_STATUS_COUNT]; - - ptrA m_szAccessToken, m_szTempToken; - - mir_cs m_csHttpQueue; - HANDLE m_evRequestsQueue; - LIST m_arHttpQueue; - - void ExecuteRequest(AsyncHttpRequest *pReq); - void Push(AsyncHttpRequest *pReq, int iTimeout = 10000); - void SaveToken(const JSONNode &data); - - HANDLE m_hWorkerThread; // worker thread handle - HNETLIBCONN m_hAPIConnection; // working connection - - bool - m_bOnline, // protocol is online - m_bTerminated; // Miranda's going down - - ////////////////////////////////////////////////////////////////////////////////////// - // gateway - - CMStringA - m_szGateway, // gateway url - m_szGatewaySessionId, // current session id - m_szCookie, // cookie used for all http queries - m_szWSCookie; // cookie used for establishing websocket connection - - HNETLIBUSER m_hGatewayNetlibUser; // the separate netlib user handle for gateways - HNETLIBCONN m_hGatewayConnection; // gateway connection - - void __cdecl GatewayThread(void*); - bool GatewayThreadWorker(void); - - bool GatewaySend(const JSONNode &pNode); - bool GatewayProcess(const JSONNode &pNode); - - void GatewaySendGuildInfo(CDiscordGuild *pGuild); - void GatewaySendHeartbeat(void); - void GatewaySendIdentify(void); - void GatewaySendResume(void); - bool GatewaySendStatus(int iStatus, const wchar_t *pwszStatusText); - - GatewayHandlerFunc GetHandler(const wchar_t*); - - int m_iHartbeatInterval; // in milliseconds - int m_iGatewaySeq; // gateway sequence number - - ////////////////////////////////////////////////////////////////////////////////////// - // options - - CMOption m_wszEmail; // my own email - CMOption m_wszDefaultGroup; // clist group to store contacts - CMOption m_bUseGroupchats; // Shall we connect Guilds at all? - CMOption m_bHideGroupchats; // Do not open chat windows on creation - CMOption m_bUseGuildGroups; // use special subgroups for guilds - CMOption m_bSyncDeleteMsgs; // delete messages from Miranda if they are deleted at the server - - ////////////////////////////////////////////////////////////////////////////////////// - // common data - - SnowFlake m_ownId; - - mir_cs csMarkReadQueue; - LIST arMarkReadQueue; - - OBJLIST arUsers; - OBJLIST arOwnMessages; - OBJLIST arVoiceCalls; - - CDiscordUser* FindUser(SnowFlake id); - CDiscordUser* FindUser(const wchar_t *pwszUsername, int iDiscriminator); - CDiscordUser* FindUserByChannel(SnowFlake channelId); - - void PreparePrivateChannel(const JSONNode &); - CDiscordUser* PrepareUser(const JSONNode &); - - ////////////////////////////////////////////////////////////////////////////////////// - // menu items - - void InitMenus(void); - - int __cdecl OnMenuPrebuild(WPARAM, LPARAM); - - INT_PTR __cdecl OnMenuCopyId(WPARAM, LPARAM); - INT_PTR __cdecl OnMenuCreateChannel(WPARAM, LPARAM); - INT_PTR __cdecl OnMenuJoinGuild(WPARAM, LPARAM); - INT_PTR __cdecl OnMenuLeaveGuild(WPARAM, LPARAM); - INT_PTR __cdecl OnMenuLoadHistory(WPARAM, LPARAM); - INT_PTR __cdecl OnMenuToggleSync(WPARAM, LPARAM); - - HGENMENU m_hMenuLeaveGuild, m_hMenuCreateChannel, m_hMenuToggleSync; - - ////////////////////////////////////////////////////////////////////////////////////// - // guilds - - OBJLIST arGuilds; - - __forceinline CDiscordGuild* FindGuild(SnowFlake id) const - { - return arGuilds.find((CDiscordGuild*)&id); - } - - void AddGuildUser(CDiscordGuild *guild, const CDiscordGuildMember &pUser); - void ProcessGuild(const JSONNode &json); - void ProcessPresence(const JSONNode &json); - void ProcessRole(CDiscordGuild *guild, const JSONNode &json); - void ProcessType(CDiscordUser *pUser, const JSONNode &json); - - CDiscordUser* ProcessGuildChannel(CDiscordGuild *guild, const JSONNode &json); - CDiscordGuildMember* ProcessGuildUser(CDiscordGuild *pGuild, const JSONNode &json, bool *bNew = nullptr); - - ////////////////////////////////////////////////////////////////////////////////////// - // group chats - - int __cdecl GroupchatEventHook(WPARAM, LPARAM); - int __cdecl GroupchatMenuHook(WPARAM, LPARAM); - - void Chat_SendPrivateMessage(GCHOOK *gch); - void Chat_ProcessLogMenu(GCHOOK *gch); - void Chat_ProcessNickMenu(GCHOOK* gch); - - void CreateChat(CDiscordGuild *pGuild, CDiscordUser *pUser); - void ProcessChatUser(CDiscordUser *pChat, const CMStringW &wszUserId, const JSONNode &pRoot); - void ParseSpecialChars(SESSION_INFO *si, CMStringW &str); - - ////////////////////////////////////////////////////////////////////////////////////// - // misc methods - - SnowFlake getId(const char *szName); - SnowFlake getId(MCONTACT hContact, const char *szName); - - void setId(const char *szName, SnowFlake iValue); - void setId(MCONTACT hContact, const char *szName, SnowFlake iValue); - -public: - CDiscordProto(const char*,const wchar_t*); - ~CDiscordProto(); - - ////////////////////////////////////////////////////////////////////////////////////// - // PROTO_INTERFACE - - INT_PTR GetCaps(int, MCONTACT = 0) override; - - HWND CreateExtendedSearchUI(HWND owner) override; - HWND SearchAdvanced(HWND owner) override; - - HANDLE SearchBasic(const wchar_t *id) override; - MCONTACT AddToList(int flags, PROTOSEARCHRESULT *psr) override; - MCONTACT AddToListByEvent(int flags, int, MEVENT hDbEvent) override; - - int AuthRecv(MCONTACT, PROTORECVEVENT *pre) override; - int Authorize(MEVENT hDbEvent) override; - int AuthDeny(MEVENT hDbEvent, const wchar_t* szReason) override; - int AuthRequest(MCONTACT hContact, const wchar_t*) override; - - HANDLE GetAwayMsg(MCONTACT hContact) override; - int SetAwayMsg(int iStatus, const wchar_t *msg) override; - - int SendMsg(MCONTACT hContact, int flags, const char *pszSrc) override; - - HANDLE SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) override; - - int UserIsTyping(MCONTACT hContact, int type) override; - - int SetStatus(int iNewStatus) override; - - void OnBuildProtoMenu() override; - void OnContactDeleted(MCONTACT) override; - void OnModulesLoaded() override; - void OnShutdown() override; - - ////////////////////////////////////////////////////////////////////////////////////// - // Services - - INT_PTR __cdecl RequestFriendship(WPARAM, LPARAM); - INT_PTR __cdecl SvcCreateAccMgrUI(WPARAM, LPARAM); - - INT_PTR __cdecl GetAvatarCaps(WPARAM, LPARAM); - INT_PTR __cdecl GetAvatarInfo(WPARAM, LPARAM); - INT_PTR __cdecl GetMyAvatar(WPARAM, LPARAM); - INT_PTR __cdecl SetMyAvatar(WPARAM, LPARAM); - - INT_PTR __cdecl VoiceCaps(WPARAM, LPARAM); - - ////////////////////////////////////////////////////////////////////////////////////// - // Events - - int __cdecl OnOptionsInit(WPARAM, LPARAM); - int __cdecl OnAccountChanged(WPARAM, LPARAM); - int __cdecl OnDbEventRead(WPARAM, LPARAM); - - int __cdecl OnVoiceState(WPARAM, LPARAM); - - ////////////////////////////////////////////////////////////////////////////////////// - // dispatch commands - - void OnCommandCallCreated(const JSONNode &json); - void OnCommandCallDeleted(const JSONNode &json); - void OnCommandCallUpdated(const JSONNode &json); - void OnCommandChannelCreated(const JSONNode &json); - void OnCommandChannelDeleted(const JSONNode &json); - void OnCommandChannelUpdated(const JSONNode &json); - void OnCommandGuildCreated(const JSONNode &json); - void OnCommandGuildDeleted(const JSONNode &json); - void OnCommandGuildMemberAdded(const JSONNode &json); - void OnCommandGuildMemberListUpdate(const JSONNode &json); - void OnCommandGuildMemberRemoved(const JSONNode &json); - void OnCommandGuildMemberUpdated(const JSONNode &json); - void OnCommandFriendAdded(const JSONNode &json); - void OnCommandFriendRemoved(const JSONNode &json); - void OnCommandMessage(const JSONNode&, bool); - void OnCommandMessageCreate(const JSONNode &json); - void OnCommandMessageDelete(const JSONNode &json); - void OnCommandMessageUpdate(const JSONNode &json); - void OnCommandMessageAck(const JSONNode &json); - void OnCommandPresence(const JSONNode &json); - void OnCommandReady(const JSONNode &json); - void OnCommandRoleCreated(const JSONNode &json); - void OnCommandRoleDeleted(const JSONNode &json); - void OnCommandTyping(const JSONNode &json); - void OnCommandUserUpdate(const JSONNode &json); - void OnCommandUserSettingsUpdate(const JSONNode &json); - - void OnLoggedIn(); - void OnLoggedOut(); - - void OnReceiveCreateChannel(NETLIBHTTPREQUEST*, AsyncHttpRequest*); - void OnReceiveFile(NETLIBHTTPREQUEST*, AsyncHttpRequest*); - void OnReceiveGateway(NETLIBHTTPREQUEST*, AsyncHttpRequest*); - void OnReceiveMarkRead(NETLIBHTTPREQUEST *, AsyncHttpRequest *); - void OnReceiveMessageAck(NETLIBHTTPREQUEST*, AsyncHttpRequest*); - void OnReceiveToken(NETLIBHTTPREQUEST *, AsyncHttpRequest *); - void OnReceiveUserinfo(NETLIBHTTPREQUEST *, AsyncHttpRequest *); - - void RetrieveMyInfo(); - void OnReceiveMyInfo(NETLIBHTTPREQUEST*, AsyncHttpRequest*); - - void RetrieveHistory(CDiscordUser *pUser, CDiscordHistoryOp iOp = MSG_NOFILTER, SnowFlake msgid = 0, int iLimit = 50); - void OnReceiveHistory(NETLIBHTTPREQUEST*, AsyncHttpRequest*); - - bool RetrieveAvatar(MCONTACT hContact); - void OnReceiveAvatar(NETLIBHTTPREQUEST*, AsyncHttpRequest*); - - void OnSendMsg(NETLIBHTTPREQUEST*, AsyncHttpRequest*); - - ////////////////////////////////////////////////////////////////////////////////////// - // Misc - - void SendMarkRead(void); - void SetServerStatus(int iStatus); - void RemoveFriend(SnowFlake id); - - CMStringW GetAvatarFilename(MCONTACT hContact); - void CheckAvatarChange(MCONTACT hContact, const CMStringW &wszNewHash); -}; - -///////////////////////////////////////////////////////////////////////////////////////// - -struct CMPlugin : public ACCPROTOPLUGIN -{ - CMPlugin(); - - bool bVoiceService = false; - - int Load() override; -}; +#pragma once + +#define EVENT_INCOMING_CALL 10001 +#define EVENT_CALL_FINISHED 10002 + +typedef __int64 SnowFlake; + +__forceinline int compareInt64(const SnowFlake i1, const SnowFlake i2) +{ + return (i1 == i2) ? 0 : (i1 < i2) ? -1 : 1; +} + +class CDiscordProto; +typedef void (CDiscordProto::*GatewayHandlerFunc)(const JSONNode&); + +struct AsyncHttpRequest : public MTHttpRequest +{ + AsyncHttpRequest(CDiscordProto*, int iRequestType, LPCSTR szUrl, MTHttpRequestHandler pFunc, JSONNode *pNode = nullptr); + + int m_iErrorCode, m_iReqNum; + bool m_bMainSite; + MCONTACT hContact; +}; + +class JsonReply +{ + JSONNode *m_root = nullptr; + int m_errorCode = 0; + +public: + JsonReply(NETLIBHTTPREQUEST *); + ~JsonReply(); + + __forceinline int error() const { return m_errorCode; } + __forceinline JSONNode& data() const { return *m_root; } + __forceinline operator bool() const { return m_errorCode == 200; } +}; + +///////////////////////////////////////////////////////////////////////////////////////// + +struct CDiscordRole : public MZeroedObject +{ + SnowFlake id; + COLORREF color; + uint32_t permissions; + int position; + CMStringW wszName; +}; + +///////////////////////////////////////////////////////////////////////////////////////// + +struct COwnMessage +{ + SnowFlake nonce; + int reqId; + + COwnMessage(SnowFlake _id, int _reqId) : + nonce(_id), + reqId(_reqId) + {} +}; + +///////////////////////////////////////////////////////////////////////////////////////// + +enum CDiscordHistoryOp +{ + MSG_NOFILTER, MSG_AFTER, MSG_BEFORE +}; + +struct CDiscordUser : public MZeroedObject +{ + CDiscordUser(SnowFlake _id) : + id(_id) + {} + + ~CDiscordUser(); + + SnowFlake id; + MCONTACT hContact; + + SnowFlake channelId; + SnowFlake lastReadId, lastMsgId; + SnowFlake parentId; + bool bIsPrivate; + bool bIsGroup; + bool bSynced; + + struct CDiscordGuild *pGuild; + + CMStringW wszUsername, wszChannelName, wszTopic; + int iDiscriminator; +}; + +///////////////////////////////////////////////////////////////////////////////////////// + +struct CDiscordGuildMember : public MZeroedObject +{ + CDiscordGuildMember(SnowFlake id) : + userId(id) + {} + + ~CDiscordGuildMember() + {} + + SnowFlake userId; + CMStringW wszDiscordId, wszNick, wszRole; + int iStatus; +}; + +struct CDiscordGuild : public MZeroedObject +{ + CDiscordGuild(SnowFlake _id); + ~CDiscordGuild(); + + __forceinline CDiscordGuildMember* FindUser(SnowFlake userId) + { + return arChatUsers.find((CDiscordGuildMember *)&userId); + } + + __inline CMStringW GetCacheFile() const + { + return CMStringW(FORMAT, L"%s\\DiscordCache\\%lld.json", VARSW(L"%miranda_userdata%").get(), id); + } + + SnowFlake id, ownerId; + CMStringW wszName; + MCONTACT hContact; + MGROUP groupId; + bool bSynced = false; + LIST arChannels; + + SESSION_INFO *pParentSi; + OBJLIST arChatUsers; + OBJLIST arRoles; // guild roles + + void LoadFromFile(); + void SaveToFile(); +}; + +struct CDiscordVoiceCall +{ + CMStringA szId; + SnowFlake channelId; + time_t startTime; +}; + +///////////////////////////////////////////////////////////////////////////////////////// + +#define OPCODE_DISPATCH 0 +#define OPCODE_HEARTBEAT 1 +#define OPCODE_IDENTIFY 2 +#define OPCODE_STATUS_UPDATE 3 +#define OPCODE_VOICE_UPDATE 4 +#define OPCODE_VOICE_PING 5 +#define OPCODE_RESUME 6 +#define OPCODE_RECONNECT 7 +#define OPCODE_REQUEST_MEMBERS 8 +#define OPCODE_INVALID_SESSION 9 +#define OPCODE_HELLO 10 +#define OPCODE_HEARTBEAT_ACK 11 +#define OPCODE_REQUEST_SYNC 12 +#define OPCODE_REQUEST_SYNC_GROUP 13 +#define OPCODE_REQUEST_SYNC_CHANNEL 14 + +class CDiscordProto : public PROTO +{ + friend struct AsyncHttpRequest; + friend class CDiscardAccountOptions; + + class CDiscordProtoImpl + { + friend class CDiscordProto; + CDiscordProto &m_proto; + + CTimer m_heartBeat, m_markRead; + void OnHeartBeat(CTimer *) { + m_proto.GatewaySendHeartbeat(); + } + + void OnMarkRead(CTimer *pTimer) { + m_proto.SendMarkRead(); + pTimer->Stop(); + } + + CDiscordProtoImpl(CDiscordProto &pro) : + m_proto(pro), + m_markRead(Miranda_GetSystemWindow(), UINT_PTR(this)), + m_heartBeat(Miranda_GetSystemWindow(), UINT_PTR(this) + 1) + { + m_markRead.OnEvent = Callback(this, &CDiscordProtoImpl::OnMarkRead); + m_heartBeat.OnEvent = Callback(this, &CDiscordProtoImpl::OnHeartBeat); + } + } m_impl; + + ////////////////////////////////////////////////////////////////////////////////////// + // threads + + void __cdecl SendFileThread(void*); + void __cdecl ServerThread(void*); + void __cdecl SearchThread(void *param); + void __cdecl BatchChatCreate(void* param); + void __cdecl GetAwayMsgThread(void *param); + + ////////////////////////////////////////////////////////////////////////////////////// + // session control + + void ConnectionFailed(int iReason); + void ShutdownSession(void); + + wchar_t *m_wszStatusMsg[MAX_STATUS_COUNT]; + + ptrA m_szAccessToken, m_szTempToken; + + mir_cs m_csHttpQueue; + HANDLE m_evRequestsQueue; + LIST m_arHttpQueue; + + void ExecuteRequest(AsyncHttpRequest *pReq); + void Push(AsyncHttpRequest *pReq, int iTimeout = 10000); + void SaveToken(const JSONNode &data); + + HANDLE m_hWorkerThread; // worker thread handle + HNETLIBCONN m_hAPIConnection; // working connection + + bool + m_bOnline, // protocol is online + m_bTerminated; // Miranda's going down + + ////////////////////////////////////////////////////////////////////////////////////// + // gateway + + CMStringA + m_szGateway, // gateway url + m_szGatewaySessionId, // current session id + m_szCookie, // cookie used for all http queries + m_szWSCookie; // cookie used for establishing websocket connection + + HNETLIBUSER m_hGatewayNetlibUser; // the separate netlib user handle for gateways + HNETLIBCONN m_hGatewayConnection; // gateway connection + + void __cdecl GatewayThread(void*); + bool GatewayThreadWorker(void); + + bool GatewaySend(const JSONNode &pNode); + bool GatewayProcess(const JSONNode &pNode); + + void GatewaySendGuildInfo(CDiscordGuild *pGuild); + void GatewaySendHeartbeat(void); + void GatewaySendIdentify(void); + void GatewaySendResume(void); + bool GatewaySendStatus(int iStatus, const wchar_t *pwszStatusText); + + GatewayHandlerFunc GetHandler(const wchar_t*); + + int m_iHartbeatInterval; // in milliseconds + int m_iGatewaySeq; // gateway sequence number + + ////////////////////////////////////////////////////////////////////////////////////// + // options + + CMOption m_wszEmail; // my own email + CMOption m_wszDefaultGroup; // clist group to store contacts + CMOption m_bUseGroupchats; // Shall we connect Guilds at all? + CMOption m_bHideGroupchats; // Do not open chat windows on creation + CMOption m_bUseGuildGroups; // use special subgroups for guilds + CMOption m_bSyncDeleteMsgs; // delete messages from Miranda if they are deleted at the server + + ////////////////////////////////////////////////////////////////////////////////////// + // common data + + SnowFlake m_ownId; + + mir_cs csMarkReadQueue; + LIST arMarkReadQueue; + + OBJLIST arUsers; + OBJLIST arOwnMessages; + OBJLIST arVoiceCalls; + + CDiscordUser* FindUser(SnowFlake id); + CDiscordUser* FindUser(const wchar_t *pwszUsername, int iDiscriminator); + CDiscordUser* FindUserByChannel(SnowFlake channelId); + + void PreparePrivateChannel(const JSONNode &); + CDiscordUser* PrepareUser(const JSONNode &); + + ////////////////////////////////////////////////////////////////////////////////////// + // menu items + + void InitMenus(void); + + int __cdecl OnMenuPrebuild(WPARAM, LPARAM); + + INT_PTR __cdecl OnMenuCopyId(WPARAM, LPARAM); + INT_PTR __cdecl OnMenuCreateChannel(WPARAM, LPARAM); + INT_PTR __cdecl OnMenuJoinGuild(WPARAM, LPARAM); + INT_PTR __cdecl OnMenuLeaveGuild(WPARAM, LPARAM); + INT_PTR __cdecl OnMenuLoadHistory(WPARAM, LPARAM); + INT_PTR __cdecl OnMenuToggleSync(WPARAM, LPARAM); + + HGENMENU m_hMenuLeaveGuild, m_hMenuCreateChannel, m_hMenuToggleSync; + + ////////////////////////////////////////////////////////////////////////////////////// + // guilds + + OBJLIST arGuilds; + + __forceinline CDiscordGuild* FindGuild(SnowFlake id) const + { + return arGuilds.find((CDiscordGuild*)&id); + } + + void AddGuildUser(CDiscordGuild *guild, const CDiscordGuildMember &pUser); + void ProcessGuild(const JSONNode &json); + void ProcessPresence(const JSONNode &json); + void ProcessRole(CDiscordGuild *guild, const JSONNode &json); + void ProcessType(CDiscordUser *pUser, const JSONNode &json); + + CDiscordUser* ProcessGuildChannel(CDiscordGuild *guild, const JSONNode &json); + CDiscordGuildMember* ProcessGuildUser(CDiscordGuild *pGuild, const JSONNode &json, bool *bNew = nullptr); + + ////////////////////////////////////////////////////////////////////////////////////// + // group chats + + int __cdecl GroupchatEventHook(WPARAM, LPARAM); + int __cdecl GroupchatMenuHook(WPARAM, LPARAM); + + void Chat_SendPrivateMessage(GCHOOK *gch); + void Chat_ProcessLogMenu(GCHOOK *gch); + void Chat_ProcessNickMenu(GCHOOK* gch); + + void CreateChat(CDiscordGuild *pGuild, CDiscordUser *pUser); + void ProcessChatUser(CDiscordUser *pChat, const CMStringW &wszUserId, const JSONNode &pRoot); + void ParseSpecialChars(SESSION_INFO *si, CMStringW &str); + + ////////////////////////////////////////////////////////////////////////////////////// + // misc methods + + SnowFlake getId(const char *szName); + SnowFlake getId(MCONTACT hContact, const char *szName); + + void setId(const char *szName, SnowFlake iValue); + void setId(MCONTACT hContact, const char *szName, SnowFlake iValue); + +public: + CDiscordProto(const char*,const wchar_t*); + ~CDiscordProto(); + + ////////////////////////////////////////////////////////////////////////////////////// + // PROTO_INTERFACE + + INT_PTR GetCaps(int, MCONTACT = 0) override; + + HWND CreateExtendedSearchUI(HWND owner) override; + HWND SearchAdvanced(HWND owner) override; + + HANDLE SearchBasic(const wchar_t *id) override; + MCONTACT AddToList(int flags, PROTOSEARCHRESULT *psr) override; + MCONTACT AddToListByEvent(int flags, int, MEVENT hDbEvent) override; + + int AuthRecv(MCONTACT, PROTORECVEVENT *pre) override; + int Authorize(MEVENT hDbEvent) override; + int AuthDeny(MEVENT hDbEvent, const wchar_t* szReason) override; + int AuthRequest(MCONTACT hContact, const wchar_t*) override; + + HANDLE GetAwayMsg(MCONTACT hContact) override; + int SetAwayMsg(int iStatus, const wchar_t *msg) override; + + int SendMsg(MCONTACT hContact, int flags, const char *pszSrc) override; + + HANDLE SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) override; + + int UserIsTyping(MCONTACT hContact, int type) override; + + int SetStatus(int iNewStatus) override; + + void OnBuildProtoMenu() override; + void OnContactDeleted(MCONTACT) override; + void OnModulesLoaded() override; + void OnShutdown() override; + + ////////////////////////////////////////////////////////////////////////////////////// + // Services + + INT_PTR __cdecl RequestFriendship(WPARAM, LPARAM); + INT_PTR __cdecl SvcCreateAccMgrUI(WPARAM, LPARAM); + + INT_PTR __cdecl GetAvatarCaps(WPARAM, LPARAM); + INT_PTR __cdecl GetAvatarInfo(WPARAM, LPARAM); + INT_PTR __cdecl GetMyAvatar(WPARAM, LPARAM); + INT_PTR __cdecl SetMyAvatar(WPARAM, LPARAM); + + INT_PTR __cdecl VoiceCaps(WPARAM, LPARAM); + + ////////////////////////////////////////////////////////////////////////////////////// + // Events + + int __cdecl OnOptionsInit(WPARAM, LPARAM); + int __cdecl OnAccountChanged(WPARAM, LPARAM); + int __cdecl OnDbEventRead(WPARAM, LPARAM); + + int __cdecl OnVoiceState(WPARAM, LPARAM); + + ////////////////////////////////////////////////////////////////////////////////////// + // dispatch commands + + void OnCommandCallCreated(const JSONNode &json); + void OnCommandCallDeleted(const JSONNode &json); + void OnCommandCallUpdated(const JSONNode &json); + void OnCommandChannelCreated(const JSONNode &json); + void OnCommandChannelDeleted(const JSONNode &json); + void OnCommandChannelUpdated(const JSONNode &json); + void OnCommandGuildCreated(const JSONNode &json); + void OnCommandGuildDeleted(const JSONNode &json); + void OnCommandGuildMemberAdded(const JSONNode &json); + void OnCommandGuildMemberListUpdate(const JSONNode &json); + void OnCommandGuildMemberRemoved(const JSONNode &json); + void OnCommandGuildMemberUpdated(const JSONNode &json); + void OnCommandFriendAdded(const JSONNode &json); + void OnCommandFriendRemoved(const JSONNode &json); + void OnCommandMessage(const JSONNode&, bool); + void OnCommandMessageCreate(const JSONNode &json); + void OnCommandMessageDelete(const JSONNode &json); + void OnCommandMessageUpdate(const JSONNode &json); + void OnCommandMessageAck(const JSONNode &json); + void OnCommandPresence(const JSONNode &json); + void OnCommandReady(const JSONNode &json); + void OnCommandRoleCreated(const JSONNode &json); + void OnCommandRoleDeleted(const JSONNode &json); + void OnCommandTyping(const JSONNode &json); + void OnCommandUserUpdate(const JSONNode &json); + void OnCommandUserSettingsUpdate(const JSONNode &json); + + void OnLoggedIn(); + void OnLoggedOut(); + + void OnReceiveCreateChannel(NETLIBHTTPREQUEST*, AsyncHttpRequest*); + void OnReceiveFile(NETLIBHTTPREQUEST*, AsyncHttpRequest*); + void OnReceiveGateway(NETLIBHTTPREQUEST*, AsyncHttpRequest*); + void OnReceiveMarkRead(NETLIBHTTPREQUEST *, AsyncHttpRequest *); + void OnReceiveMessageAck(NETLIBHTTPREQUEST*, AsyncHttpRequest*); + void OnReceiveToken(NETLIBHTTPREQUEST *, AsyncHttpRequest *); + void OnReceiveUserinfo(NETLIBHTTPREQUEST *, AsyncHttpRequest *); + + void RetrieveMyInfo(); + void OnReceiveMyInfo(NETLIBHTTPREQUEST*, AsyncHttpRequest*); + + void RetrieveHistory(CDiscordUser *pUser, CDiscordHistoryOp iOp = MSG_NOFILTER, SnowFlake msgid = 0, int iLimit = 50); + void OnReceiveHistory(NETLIBHTTPREQUEST*, AsyncHttpRequest*); + + bool RetrieveAvatar(MCONTACT hContact); + void OnReceiveAvatar(NETLIBHTTPREQUEST*, AsyncHttpRequest*); + + void OnSendMsg(NETLIBHTTPREQUEST*, AsyncHttpRequest*); + + ////////////////////////////////////////////////////////////////////////////////////// + // Misc + + void SendMarkRead(void); + void SetServerStatus(int iStatus); + void RemoveFriend(SnowFlake id); + + CMStringW GetAvatarFilename(MCONTACT hContact); + void CheckAvatarChange(MCONTACT hContact, const CMStringW &wszNewHash); +}; + +///////////////////////////////////////////////////////////////////////////////////////// + +struct CMPlugin : public ACCPROTOPLUGIN +{ + CMPlugin(); + + bool bVoiceService = false; + + int Load() override; +}; diff --git a/protocols/Discord/src/resource.h b/protocols/Discord/src/resource.h index d0326e6857..099a4af3af 100644 --- a/protocols/Discord/src/resource.h +++ b/protocols/Discord/src/resource.h @@ -1,30 +1,30 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by w:\miranda-ng\protocols\Discord\res\discord.rc -// -#define IDI_MAIN 101 -#define IDI_GROUPCHAT 102 -#define IDD_OPTIONS_ACCOUNT 103 -#define IDD_EXTSEARCH 104 -#define IDD_OPTIONS_ACCMGR 105 -#define IDI_VOICE_CALL 106 -#define IDI_VOICE_ENDED 107 -#define IDC_PASSWORD 1001 -#define IDC_USERNAME 1002 -#define IDC_GROUP 1003 -#define IDC_NICK 1004 -#define IDC_HIDECHATS 1005 -#define IDC_USEGROUPS 1006 -#define IDC_USEGUILDS 1007 -#define IDC_DELETE_MSGS 1009 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 104 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1008 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by w:\miranda-ng\protocols\Discord\res\discord.rc +// +#define IDI_MAIN 101 +#define IDI_GROUPCHAT 102 +#define IDD_OPTIONS_ACCOUNT 103 +#define IDD_EXTSEARCH 104 +#define IDD_OPTIONS_ACCMGR 105 +#define IDI_VOICE_CALL 106 +#define IDI_VOICE_ENDED 107 +#define IDC_PASSWORD 1001 +#define IDC_USERNAME 1002 +#define IDC_GROUP 1003 +#define IDC_NICK 1004 +#define IDC_HIDECHATS 1005 +#define IDC_USEGROUPS 1006 +#define IDC_USEGUILDS 1007 +#define IDC_DELETE_MSGS 1009 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 104 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1008 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/protocols/Discord/src/server.cpp b/protocols/Discord/src/server.cpp index cc6dfe2280..16f716e89f 100644 --- a/protocols/Discord/src/server.cpp +++ b/protocols/Discord/src/server.cpp @@ -1,307 +1,307 @@ -/* -Copyright © 2016-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -///////////////////////////////////////////////////////////////////////////////////////// -// removes a friend from the server - -void CDiscordProto::RemoveFriend(SnowFlake id) -{ - Push(new AsyncHttpRequest(this, REQUEST_DELETE, CMStringA(FORMAT, "/users/@me/relationships/%lld", id), nullptr)); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// retrieves server history - -void CDiscordProto::RetrieveHistory(CDiscordUser *pUser, CDiscordHistoryOp iOp, SnowFlake msgid, int iLimit) -{ - if (!pUser->hContact || getByte(pUser->hContact, DB_KEY_DONT_FETCH)) - return; - - CMStringA szUrl(FORMAT, "/channels/%lld/messages", pUser->channelId); - AsyncHttpRequest *pReq = new AsyncHttpRequest(this, REQUEST_GET, szUrl, &CDiscordProto::OnReceiveHistory); - pReq << INT_PARAM("limit", iLimit); - - if (msgid) { - switch (iOp) { - case MSG_AFTER: - pReq << INT64_PARAM("after", msgid); break; - case MSG_BEFORE: - pReq << INT64_PARAM("before", msgid); break; - } - } - pReq->pUserInfo = pUser; - Push(pReq); -} - -static int compareMsgHistory(const JSONNode *p1, const JSONNode *p2) -{ - return wcscmp((*p1)["id"].as_mstring(), (*p2)["id"].as_mstring()); -} - -void CDiscordProto::OnReceiveHistory(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - CDiscordUser *pUser = (CDiscordUser*)pReq->pUserInfo; - - JsonReply root(pReply); - if (!root) { - if (root.error() == 403) // forbidden, don't try to read it anymore - setByte(pUser->hContact, DB_KEY_DONT_FETCH, true); - return; - } - - SESSION_INFO *si = nullptr; - if (!pUser->bIsPrivate) { - si = g_chatApi.SM_FindSession(pUser->wszUsername, m_szModuleName); - if (si == nullptr) { - debugLogA("message to unknown channel %lld ignored", pUser->channelId); - return; - } - } - - SnowFlake lastId = getId(pUser->hContact, DB_KEY_LASTMSGID); // as stored in a database - - LIST arNodes(10, compareMsgHistory); - int iNumMessages = 0; - for (auto &it : root.data()) { - arNodes.insert(&it); - iNumMessages++; - } - - for (auto &it : arNodes) { - auto &pNode = *it; - CMStringW wszText = PrepareMessageText(pNode); - CMStringW wszUserId = pNode["author"]["id"].as_mstring(); - SnowFlake msgid = ::getId(pNode["id"]); - SnowFlake authorid = _wtoi64(wszUserId); - uint32_t dwTimeStamp = StringToDate(pNode["timestamp"].as_mstring()); - - if (pUser->bIsPrivate) { - DBEVENTINFO dbei = {}; - dbei.szModule = m_szModuleName; - dbei.flags = DBEF_UTF; - dbei.eventType = EVENTTYPE_MESSAGE; - - if (authorid == m_ownId) - dbei.flags |= DBEF_SENT; - else - dbei.flags &= ~DBEF_SENT; - - if (msgid <= pUser->lastReadId) - dbei.flags |= DBEF_READ; - else - dbei.flags &= ~DBEF_READ; - - ptrA szBody(mir_utf8encodeW(wszText)); - dbei.timestamp = dwTimeStamp; - dbei.pBlob = (uint8_t*)szBody.get(); - dbei.cbBlob = (uint32_t)mir_strlen(szBody); - - bool bSucceeded = false; - char szMsgId[100]; - _i64toa_s(msgid, szMsgId, _countof(szMsgId), 10); - MEVENT hDbEvent = db_event_getById(m_szModuleName, szMsgId); - if (hDbEvent != 0) - bSucceeded = 0 == db_event_edit(pUser->hContact, hDbEvent, &dbei); - - if (!bSucceeded) { - dbei.szId = szMsgId; - db_event_add(pUser->hContact, &dbei); - } - } - else { - ProcessChatUser(pUser, wszUserId, pNode); - - ParseSpecialChars(si, wszText); - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE }; - gce.pszID.w = pUser->wszUsername; - gce.dwFlags = GCEF_ADDTOLOG; - gce.pszUID.w = wszUserId; - gce.pszText.w = wszText; - gce.time = dwTimeStamp; - gce.bIsMe = authorid == m_ownId; - Chat_Event(&gce); - } - - if (lastId < msgid) - lastId = msgid; - } - - setId(pUser->hContact, DB_KEY_LASTMSGID, lastId); - - // if we fetched 99 messages, but have smth more to go, continue fetching - if (iNumMessages == 99 && lastId < pUser->lastMsgId) - RetrieveHistory(pUser, MSG_AFTER, lastId, 99); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// retrieves user info - -void CDiscordProto::RetrieveMyInfo() -{ - Push(new AsyncHttpRequest(this, REQUEST_GET, "/users/@me", &CDiscordProto::OnReceiveMyInfo)); -} - -void CDiscordProto::OnReceiveMyInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) -{ - JsonReply root(pReply); - if (!root) { - ConnectionFailed(LOGINERR_WRONGPASSWORD); - return; - } - - auto &data = root.data(); - SnowFlake id = ::getId(data["id"]); - setId(0, DB_KEY_ID, id); - - setByte(0, DB_KEY_MFA, data["mfa_enabled"].as_bool()); - setDword(0, DB_KEY_DISCR, _wtoi(data["discriminator"].as_mstring())); - setWString(0, DB_KEY_NICK, data["username"].as_mstring()); - m_wszEmail = data["email"].as_mstring(); - - m_ownId = id; - - m_szCookie.Empty(); - for (int i=0; i < pReply->headersCount; i++) { - if (!mir_strcmpi(pReply->headers[i].szName, "Set-Cookie")) { - char *p = strchr(pReply->headers[i].szValue, ';'); - if (p) *p = 0; - if (!m_szCookie.IsEmpty()) - m_szCookie.Append("; "); - - m_szCookie.Append(pReply->headers[i].szValue); - } - } - - // launch gateway thread - if (m_szGateway.IsEmpty()) - Push(new AsyncHttpRequest(this, REQUEST_GET, "/gateway", &CDiscordProto::OnReceiveGateway)); - else - ForkThread(&CDiscordProto::GatewayThread, nullptr); - - CheckAvatarChange(0, data["avatar"].as_mstring()); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// finds a gateway address - -void CDiscordProto::OnReceiveGateway(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) -{ - JsonReply root(pReply); - if (!root) { - ShutdownSession(); - return; - } - - auto &data = root.data(); - m_szGateway = data["url"].as_mstring(); - ForkThread(&CDiscordProto::GatewayThread, nullptr); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CDiscordProto::SetServerStatus(int iStatus) -{ - if (GatewaySendStatus(iStatus, nullptr)) { - int iOldStatus = m_iStatus; m_iStatus = iStatus; - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// -// channels - -void CDiscordProto::OnReceiveCreateChannel(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) -{ - JsonReply root(pReply); - if (root) - OnCommandChannelCreated(root.data()); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CDiscordProto::OnReceiveMessageAck(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) -{ - JsonReply root(pReply); - if (!root) - return; - - auto &data = root.data(); - CMStringW wszToken(data["token"].as_mstring()); - if (!wszToken.IsEmpty()) { - JSONNode props; props.set_name("properties"); - JSONNode reply; reply << props; - reply << CHAR_PARAM("event", "ack_messages") << WCHAR_PARAM("token", data["token"].as_mstring()); - Push(new AsyncHttpRequest(this, REQUEST_POST, "/track", nullptr, &reply)); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -#define RECAPTCHA_API_KEY "6Lef5iQTAAAAAKeIvIY-DeexoO3gj7ryl9rLMEnn" -#define RECAPTCHA_SITE_URL "https://discord.com" - -void CDiscordProto::OnReceiveToken(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) -{ - if (pReply->resultCode != 200) { - JSONNode root = JSONNode::parse(pReply->pData); - if (root) { - const JSONNode &captcha = root["captcha_key"].as_array(); - if (captcha) { - for (auto &it : captcha) { - if (it.as_mstring() == "captcha-required") { - MessageBoxW(NULL, TranslateT("The server requires you to enter the captcha. Miranda will redirect you to a browser now"), L"Discord", MB_OK | MB_ICONINFORMATION); - Utils_OpenUrl("https://discord.com/app"); - } - } - } - - for (auto &err: root["errors"]["email"]["_errors"]) { - CMStringW code(err["code"].as_mstring()); - CMStringW message(err["message"].as_mstring()); - if (!code.IsEmpty() || !message.IsEmpty()) { - POPUPDATAW popup; - popup.lchIcon = IcoLib_GetIconByHandle(Skin_GetIconHandle(SKINICON_ERROR), true); - wcscpy_s(popup.lpwzContactName, m_tszUserName); - mir_snwprintf(popup.lpwzText, TranslateT("Connection failed.\n%s (%s)."), message.c_str(), code.c_str()); - PUAddPopupW(&popup); - } - } - } - ConnectionFailed(LOGINERR_WRONGPASSWORD); - return; - } - - JsonReply root(pReply); - if (!root) { - ConnectionFailed(LOGINERR_NOSERVER); - return; - } - - auto &data = root.data(); - CMStringA szToken = data["token"].as_mstring(); - if (szToken.IsEmpty()) { - debugLogA("Strange empty token received, exiting"); - return; - } - - m_szAccessToken = szToken.Detach(); - setString("AccessToken", m_szAccessToken); - RetrieveMyInfo(); -} +/* +Copyright © 2016-22 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +///////////////////////////////////////////////////////////////////////////////////////// +// removes a friend from the server + +void CDiscordProto::RemoveFriend(SnowFlake id) +{ + Push(new AsyncHttpRequest(this, REQUEST_DELETE, CMStringA(FORMAT, "/users/@me/relationships/%lld", id), nullptr)); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// retrieves server history + +void CDiscordProto::RetrieveHistory(CDiscordUser *pUser, CDiscordHistoryOp iOp, SnowFlake msgid, int iLimit) +{ + if (!pUser->hContact || getByte(pUser->hContact, DB_KEY_DONT_FETCH)) + return; + + CMStringA szUrl(FORMAT, "/channels/%lld/messages", pUser->channelId); + AsyncHttpRequest *pReq = new AsyncHttpRequest(this, REQUEST_GET, szUrl, &CDiscordProto::OnReceiveHistory); + pReq << INT_PARAM("limit", iLimit); + + if (msgid) { + switch (iOp) { + case MSG_AFTER: + pReq << INT64_PARAM("after", msgid); break; + case MSG_BEFORE: + pReq << INT64_PARAM("before", msgid); break; + } + } + pReq->pUserInfo = pUser; + Push(pReq); +} + +static int compareMsgHistory(const JSONNode *p1, const JSONNode *p2) +{ + return wcscmp((*p1)["id"].as_mstring(), (*p2)["id"].as_mstring()); +} + +void CDiscordProto::OnReceiveHistory(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + CDiscordUser *pUser = (CDiscordUser*)pReq->pUserInfo; + + JsonReply root(pReply); + if (!root) { + if (root.error() == 403) // forbidden, don't try to read it anymore + setByte(pUser->hContact, DB_KEY_DONT_FETCH, true); + return; + } + + SESSION_INFO *si = nullptr; + if (!pUser->bIsPrivate) { + si = g_chatApi.SM_FindSession(pUser->wszUsername, m_szModuleName); + if (si == nullptr) { + debugLogA("message to unknown channel %lld ignored", pUser->channelId); + return; + } + } + + SnowFlake lastId = getId(pUser->hContact, DB_KEY_LASTMSGID); // as stored in a database + + LIST arNodes(10, compareMsgHistory); + int iNumMessages = 0; + for (auto &it : root.data()) { + arNodes.insert(&it); + iNumMessages++; + } + + for (auto &it : arNodes) { + auto &pNode = *it; + CMStringW wszText = PrepareMessageText(pNode); + CMStringW wszUserId = pNode["author"]["id"].as_mstring(); + SnowFlake msgid = ::getId(pNode["id"]); + SnowFlake authorid = _wtoi64(wszUserId); + uint32_t dwTimeStamp = StringToDate(pNode["timestamp"].as_mstring()); + + if (pUser->bIsPrivate) { + DBEVENTINFO dbei = {}; + dbei.szModule = m_szModuleName; + dbei.flags = DBEF_UTF; + dbei.eventType = EVENTTYPE_MESSAGE; + + if (authorid == m_ownId) + dbei.flags |= DBEF_SENT; + else + dbei.flags &= ~DBEF_SENT; + + if (msgid <= pUser->lastReadId) + dbei.flags |= DBEF_READ; + else + dbei.flags &= ~DBEF_READ; + + ptrA szBody(mir_utf8encodeW(wszText)); + dbei.timestamp = dwTimeStamp; + dbei.pBlob = (uint8_t*)szBody.get(); + dbei.cbBlob = (uint32_t)mir_strlen(szBody); + + bool bSucceeded = false; + char szMsgId[100]; + _i64toa_s(msgid, szMsgId, _countof(szMsgId), 10); + MEVENT hDbEvent = db_event_getById(m_szModuleName, szMsgId); + if (hDbEvent != 0) + bSucceeded = 0 == db_event_edit(pUser->hContact, hDbEvent, &dbei); + + if (!bSucceeded) { + dbei.szId = szMsgId; + db_event_add(pUser->hContact, &dbei); + } + } + else { + ProcessChatUser(pUser, wszUserId, pNode); + + ParseSpecialChars(si, wszText); + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE }; + gce.pszID.w = pUser->wszUsername; + gce.dwFlags = GCEF_ADDTOLOG; + gce.pszUID.w = wszUserId; + gce.pszText.w = wszText; + gce.time = dwTimeStamp; + gce.bIsMe = authorid == m_ownId; + Chat_Event(&gce); + } + + if (lastId < msgid) + lastId = msgid; + } + + setId(pUser->hContact, DB_KEY_LASTMSGID, lastId); + + // if we fetched 99 messages, but have smth more to go, continue fetching + if (iNumMessages == 99 && lastId < pUser->lastMsgId) + RetrieveHistory(pUser, MSG_AFTER, lastId, 99); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// retrieves user info + +void CDiscordProto::RetrieveMyInfo() +{ + Push(new AsyncHttpRequest(this, REQUEST_GET, "/users/@me", &CDiscordProto::OnReceiveMyInfo)); +} + +void CDiscordProto::OnReceiveMyInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) +{ + JsonReply root(pReply); + if (!root) { + ConnectionFailed(LOGINERR_WRONGPASSWORD); + return; + } + + auto &data = root.data(); + SnowFlake id = ::getId(data["id"]); + setId(0, DB_KEY_ID, id); + + setByte(0, DB_KEY_MFA, data["mfa_enabled"].as_bool()); + setDword(0, DB_KEY_DISCR, _wtoi(data["discriminator"].as_mstring())); + setWString(0, DB_KEY_NICK, data["username"].as_mstring()); + m_wszEmail = data["email"].as_mstring(); + + m_ownId = id; + + m_szCookie.Empty(); + for (int i=0; i < pReply->headersCount; i++) { + if (!mir_strcmpi(pReply->headers[i].szName, "Set-Cookie")) { + char *p = strchr(pReply->headers[i].szValue, ';'); + if (p) *p = 0; + if (!m_szCookie.IsEmpty()) + m_szCookie.Append("; "); + + m_szCookie.Append(pReply->headers[i].szValue); + } + } + + // launch gateway thread + if (m_szGateway.IsEmpty()) + Push(new AsyncHttpRequest(this, REQUEST_GET, "/gateway", &CDiscordProto::OnReceiveGateway)); + else + ForkThread(&CDiscordProto::GatewayThread, nullptr); + + CheckAvatarChange(0, data["avatar"].as_mstring()); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// finds a gateway address + +void CDiscordProto::OnReceiveGateway(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) +{ + JsonReply root(pReply); + if (!root) { + ShutdownSession(); + return; + } + + auto &data = root.data(); + m_szGateway = data["url"].as_mstring(); + ForkThread(&CDiscordProto::GatewayThread, nullptr); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordProto::SetServerStatus(int iStatus) +{ + if (GatewaySendStatus(iStatus, nullptr)) { + int iOldStatus = m_iStatus; m_iStatus = iStatus; + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// +// channels + +void CDiscordProto::OnReceiveCreateChannel(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) +{ + JsonReply root(pReply); + if (root) + OnCommandChannelCreated(root.data()); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordProto::OnReceiveMessageAck(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) +{ + JsonReply root(pReply); + if (!root) + return; + + auto &data = root.data(); + CMStringW wszToken(data["token"].as_mstring()); + if (!wszToken.IsEmpty()) { + JSONNode props; props.set_name("properties"); + JSONNode reply; reply << props; + reply << CHAR_PARAM("event", "ack_messages") << WCHAR_PARAM("token", data["token"].as_mstring()); + Push(new AsyncHttpRequest(this, REQUEST_POST, "/track", nullptr, &reply)); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +#define RECAPTCHA_API_KEY "6Lef5iQTAAAAAKeIvIY-DeexoO3gj7ryl9rLMEnn" +#define RECAPTCHA_SITE_URL "https://discord.com" + +void CDiscordProto::OnReceiveToken(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) +{ + if (pReply->resultCode != 200) { + JSONNode root = JSONNode::parse(pReply->pData); + if (root) { + const JSONNode &captcha = root["captcha_key"].as_array(); + if (captcha) { + for (auto &it : captcha) { + if (it.as_mstring() == "captcha-required") { + MessageBoxW(NULL, TranslateT("The server requires you to enter the captcha. Miranda will redirect you to a browser now"), L"Discord", MB_OK | MB_ICONINFORMATION); + Utils_OpenUrl("https://discord.com/app"); + } + } + } + + for (auto &err: root["errors"]["email"]["_errors"]) { + CMStringW code(err["code"].as_mstring()); + CMStringW message(err["message"].as_mstring()); + if (!code.IsEmpty() || !message.IsEmpty()) { + POPUPDATAW popup; + popup.lchIcon = IcoLib_GetIconByHandle(Skin_GetIconHandle(SKINICON_ERROR), true); + wcscpy_s(popup.lpwzContactName, m_tszUserName); + mir_snwprintf(popup.lpwzText, TranslateT("Connection failed.\n%s (%s)."), message.c_str(), code.c_str()); + PUAddPopupW(&popup); + } + } + } + ConnectionFailed(LOGINERR_WRONGPASSWORD); + return; + } + + JsonReply root(pReply); + if (!root) { + ConnectionFailed(LOGINERR_NOSERVER); + return; + } + + auto &data = root.data(); + CMStringA szToken = data["token"].as_mstring(); + if (szToken.IsEmpty()) { + debugLogA("Strange empty token received, exiting"); + return; + } + + m_szAccessToken = szToken.Detach(); + setString("AccessToken", m_szAccessToken); + RetrieveMyInfo(); +} diff --git a/protocols/Discord/src/stdafx.cxx b/protocols/Discord/src/stdafx.cxx index 4b7f53343f..52b06cb953 100644 --- a/protocols/Discord/src/stdafx.cxx +++ b/protocols/Discord/src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright © 2016-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - +/* +Copyright © 2016-22 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + #include "stdafx.h" \ No newline at end of file diff --git a/protocols/Discord/src/stdafx.h b/protocols/Discord/src/stdafx.h index 6cba015cc3..48d68292dd 100644 --- a/protocols/Discord/src/stdafx.h +++ b/protocols/Discord/src/stdafx.h @@ -1,80 +1,80 @@ -// stdafx.h : include file for standard system include files, -// or project specific include files that are used frequently, but -// are changed infrequently -// - -#pragma once - -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include - -#include "resource.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../../libs/zlib/src/zlib.h" - -extern IconItem g_iconList[]; - -#define DB_KEY_ID "id" -#define DB_KEY_PASSWORD "Password" -#define DB_KEY_DISCR "Discriminator" -#define DB_KEY_MFA "MfaEnabled" -#define DB_KEY_NICK "Nick" -#define DB_KEY_AVHASH "AvatarHash" -#define DB_KEY_CHANNELID "ChannelID" -#define DB_KEY_LASTMSGID "LastMessageID" -#define DB_KEY_REQAUTH "ReqAuth" -#define DB_KEY_DONT_FETCH "DontFetch" - -#define DB_KEYVAL_GROUP L"Discord" - -#include "version.h" -#include "proto.h" - -///////////////////////////////////////////////////////////////////////////////////////// - -void BuildStatusList(const CDiscordGuild *pGuild, SESSION_INFO *si); - -void CopyId(const CMStringW &nick); -SnowFlake getId(const JSONNode &pNode); -CMStringW PrepareMessageText(const JSONNode &pRoot); -int StrToStatus(const CMStringW &str); -time_t StringToDate(const CMStringW &str); -int SerialNext(void); +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include "resource.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../libs/zlib/src/zlib.h" + +extern IconItem g_iconList[]; + +#define DB_KEY_ID "id" +#define DB_KEY_PASSWORD "Password" +#define DB_KEY_DISCR "Discriminator" +#define DB_KEY_MFA "MfaEnabled" +#define DB_KEY_NICK "Nick" +#define DB_KEY_AVHASH "AvatarHash" +#define DB_KEY_CHANNELID "ChannelID" +#define DB_KEY_LASTMSGID "LastMessageID" +#define DB_KEY_REQAUTH "ReqAuth" +#define DB_KEY_DONT_FETCH "DontFetch" + +#define DB_KEYVAL_GROUP L"Discord" + +#include "version.h" +#include "proto.h" + +///////////////////////////////////////////////////////////////////////////////////////// + +void BuildStatusList(const CDiscordGuild *pGuild, SESSION_INFO *si); + +void CopyId(const CMStringW &nick); +SnowFlake getId(const JSONNode &pNode); +CMStringW PrepareMessageText(const JSONNode &pRoot); +int StrToStatus(const CMStringW &str); +time_t StringToDate(const CMStringW &str); +int SerialNext(void); diff --git a/protocols/Discord/src/utils.cpp b/protocols/Discord/src/utils.cpp index ac40407c69..ce12a81443 100644 --- a/protocols/Discord/src/utils.cpp +++ b/protocols/Discord/src/utils.cpp @@ -1,376 +1,376 @@ -/* -Copyright © 2016-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -int StrToStatus(const CMStringW &str) -{ - if (str == L"idle") - return ID_STATUS_NA; - if (str == L"dnd") - return ID_STATUS_DND; - if (str == L"online") - return ID_STATUS_ONLINE; - if (str == L"offline") - return ID_STATUS_OFFLINE; - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -time_t StringToDate(const CMStringW &str) -{ - struct tm T = { 0 }; - int boo; - if (swscanf(str, L"%04d-%02d-%02dT%02d:%02d:%02d.%d", &T.tm_year, &T.tm_mon, &T.tm_mday, &T.tm_hour, &T.tm_min, &T.tm_sec, &boo) != 7) - return time(0); - - T.tm_year -= 1900; - T.tm_mon--; - time_t t = mktime(&T); - - _tzset(); - t -= _timezone; - return (t >= 0) ? t : 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static LONG volatile g_counter = 1; - -int SerialNext() -{ - return InterlockedIncrement(&g_counter); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -SnowFlake getId(const JSONNode &pNode) -{ - return _wtoi64(pNode.as_mstring()); -} - -SnowFlake CDiscordProto::getId(const char *szSetting) -{ - DBVARIANT dbv; - dbv.type = DBVT_BLOB; - if (db_get(0, m_szModuleName, szSetting, &dbv)) - return 0; - - SnowFlake result = (dbv.cpbVal == sizeof(SnowFlake)) ? *(SnowFlake*)dbv.pbVal : 0; - db_free(&dbv); - return result; -} - -SnowFlake CDiscordProto::getId(MCONTACT hContact, const char *szSetting) -{ - DBVARIANT dbv; - dbv.type = DBVT_BLOB; - if (db_get(hContact, m_szModuleName, szSetting, &dbv)) - return 0; - - SnowFlake result = (dbv.cpbVal == sizeof(SnowFlake)) ? *(SnowFlake*)dbv.pbVal : 0; - db_free(&dbv); - return result; -} - -void CDiscordProto::setId(const char *szSetting, SnowFlake iValue) -{ - SnowFlake oldVal = getId(szSetting); - if (oldVal != iValue) - db_set_blob(0, m_szModuleName, szSetting, &iValue, sizeof(iValue)); -} - -void CDiscordProto::setId(MCONTACT hContact, const char *szSetting, SnowFlake iValue) -{ - SnowFlake oldVal = getId(hContact, szSetting); - if (oldVal != iValue) - db_set_blob(hContact, m_szModuleName, szSetting, &iValue, sizeof(iValue)); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CopyId(const CMStringW &nick) -{ - if (!OpenClipboard(nullptr)) - return; - - EmptyClipboard(); - - int length = nick.GetLength() + 1; - if (HGLOBAL hMemory = GlobalAlloc(GMEM_FIXED, length * sizeof(wchar_t))) { - mir_wstrncpy((wchar_t*)GlobalLock(hMemory), nick, length); - GlobalUnlock(hMemory); - SetClipboardData(CF_UNICODETEXT, hMemory); - } - CloseClipboard(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static CDiscordUser *g_myUser = new CDiscordUser(0); - -CDiscordUser* CDiscordProto::FindUser(SnowFlake id) -{ - return arUsers.find((CDiscordUser*)&id); -} - -CDiscordUser* CDiscordProto::FindUser(const wchar_t *pwszUsername, int iDiscriminator) -{ - for (auto &p : arUsers) - if (p->wszUsername == pwszUsername && p->iDiscriminator == iDiscriminator) - return p; - - return nullptr; -} - -CDiscordUser* CDiscordProto::FindUserByChannel(SnowFlake channelId) -{ - for (auto &p : arUsers) - if (p->channelId == channelId) - return p; - - return nullptr; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Common JSON processing routines - -void CDiscordProto::PreparePrivateChannel(const JSONNode &root) -{ - CDiscordUser *pUser = nullptr; - - CMStringW wszChannelId = root["id"].as_mstring(); - SnowFlake channelId = _wtoi64(wszChannelId); - - int type = root["type"].as_int(); - switch (type) { - case 1: // single channel - for (auto &it : root["recipients"]) - pUser = PrepareUser(it); - if (pUser == nullptr) { - debugLogA("Invalid recipients list, exiting"); - return; - } - break; - - case 3: // private groupchat - if ((pUser = FindUserByChannel(channelId)) == nullptr) { - pUser = new CDiscordUser(channelId); - arUsers.insert(pUser); - } - pUser->bIsGroup = true; - pUser->wszUsername = wszChannelId; - pUser->wszChannelName = root["name"].as_mstring(); - { - SESSION_INFO *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, pUser->wszUsername, pUser->wszChannelName); - pUser->hContact = si->hContact; - - Chat_AddGroup(si, LPGENW("Owners")); - Chat_AddGroup(si, LPGENW("Participants")); - - SnowFlake ownerId = _wtoi64(root["owner_id"].as_mstring()); - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_JOIN }; - gce.pszID.w = pUser->wszUsername; - for (auto &it : root["recipients"]) { - CMStringW wszId = it["id"].as_mstring(); - CMStringW wszNick = it["nick"].as_mstring(); - if (wszNick.IsEmpty()) - wszNick = it["username"].as_mstring() + L"#" + it["discriminator"].as_mstring(); - - gce.pszUID.w = wszId; - gce.pszNick.w = wszNick; - gce.pszStatus.w = (_wtoi64(wszId) == ownerId) ? L"Owners" : L"Participants"; - Chat_Event(&gce); - } - - CMStringW wszId(FORMAT, L"%lld", getId(DB_KEY_ID)); - CMStringW wszNick(FORMAT, L"%s#%d", getMStringW(DB_KEY_NICK).c_str(), getDword(DB_KEY_DISCR)); - gce.bIsMe = true; - gce.pszUID.w = wszId; - gce.pszNick.w = wszNick; - gce.pszStatus.w = (_wtoi64(wszId) == ownerId) ? L"Owners" : L"Participants"; - Chat_Event(&gce); - - Chat_Control(m_szModuleName, pUser->wszUsername, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE); - Chat_Control(m_szModuleName, pUser->wszUsername, SESSION_ONLINE); - } - break; - - default: - debugLogA("Invalid channel type: %d, exiting", type); - return; - } - - pUser->channelId = channelId; - pUser->lastMsgId = ::getId(root["last_message_id"]); - pUser->bIsPrivate = true; - - setId(pUser->hContact, DB_KEY_CHANNELID, pUser->channelId); - - SnowFlake oldMsgId = getId(pUser->hContact, DB_KEY_LASTMSGID); - if (pUser->lastMsgId > oldMsgId) - RetrieveHistory(pUser, MSG_AFTER, oldMsgId, 99); -} - -CDiscordUser* CDiscordProto::PrepareUser(const JSONNode &user) -{ - SnowFlake id = ::getId(user["id"]); - if (id == m_ownId) - return g_myUser; - - int iDiscriminator = _wtoi(user["discriminator"].as_mstring()); - CMStringW username = user["username"].as_mstring(); - - CDiscordUser *pUser = FindUser(id); - if (pUser == nullptr) { - MCONTACT tmp = INVALID_CONTACT_ID; - - // no user found by userid, try to find him via username+discriminator - pUser = FindUser(username, iDiscriminator); - if (pUser != nullptr) { - // if found, remove the object from list to resort it (its userid==0) - if (pUser->hContact != 0) - tmp = pUser->hContact; - arUsers.remove(pUser); - } - pUser = new CDiscordUser(id); - pUser->wszUsername = username; - pUser->iDiscriminator = iDiscriminator; - if (tmp != INVALID_CONTACT_ID) { - // if we previously had a recently added contact without userid, write it down - pUser->hContact = tmp; - setId(pUser->hContact, DB_KEY_ID, id); - } - arUsers.insert(pUser); - } - - if (pUser->hContact == 0) { - MCONTACT hContact = db_add_contact(); - Proto_AddToContact(hContact, m_szModuleName); - - Clist_SetGroup(hContact, m_wszDefaultGroup); - setId(hContact, DB_KEY_ID, id); - setWString(hContact, DB_KEY_NICK, username); - setDword(hContact, DB_KEY_DISCR, iDiscriminator); - - pUser->hContact = hContact; - } - - CheckAvatarChange(pUser->hContact, user["avatar"].as_mstring()); - return pUser; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -CMStringW PrepareMessageText(const JSONNode &pRoot) -{ - CMStringW wszText = pRoot["content"].as_mstring(); - - bool bDelimiterAdded = false; - for (auto &it : pRoot["attachments"]) { - CMStringW wszUrl = it["url"].as_mstring(); - if (!wszUrl.IsEmpty()) { - if (!bDelimiterAdded) { - bDelimiterAdded = true; - wszText.Append(L"\n-----------------"); - } - wszText.AppendFormat(L"\n%s: %s", TranslateT("Attachment"), wszUrl.c_str()); - } - } - - for (auto &it : pRoot["embeds"]) { - wszText.Append(L"\n-----------------"); - - CMStringW str = it["url"].as_mstring(); - wszText.AppendFormat(L"\n%s: %s", TranslateT("Embed"), str.c_str()); - - str = it["provider"]["name"].as_mstring() + L" " + it["type"].as_mstring(); - if (str.GetLength() > 1) - wszText.AppendFormat(L"\n\t%s", str.c_str()); - - str = it["description"].as_mstring(); - if (!str.IsEmpty()) - wszText.AppendFormat(L"\n\t%s", str.c_str()); - - str = it["thumbnail"]["url"].as_mstring(); - if (!str.IsEmpty()) - wszText.AppendFormat(L"\n%s: %s", TranslateT("Preview"), str.c_str()); - } - - return wszText; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CDiscordProto::ProcessType(CDiscordUser *pUser, const JSONNode &pRoot) -{ - switch (pRoot["type"].as_int()) { - case 1: // confirmed - Contact::PutOnList(pUser->hContact); - delSetting(pUser->hContact, DB_KEY_REQAUTH); - delSetting(pUser->hContact, "ApparentMode"); - break; - - case 3: // expecting authorization - Contact::RemoveFromList(pUser->hContact); - if (!getByte(pUser->hContact, DB_KEY_REQAUTH, 0)) { - setByte(pUser->hContact, DB_KEY_REQAUTH, 1); - - CMStringA szId(FORMAT, "%lld", pUser->id); - DB::AUTH_BLOB blob(pUser->hContact, T2Utf(pUser->wszUsername), nullptr, nullptr, szId, nullptr); - - PROTORECVEVENT pre = { 0 }; - pre.timestamp = (uint32_t)time(0); - pre.lParam = blob.size(); - pre.szMessage = blob; - ProtoChainRecv(pUser->hContact, PSR_AUTH, 0, (LPARAM)&pre); - } - break; - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CDiscordProto::ParseSpecialChars(SESSION_INFO *si, CMStringW &str) -{ - for (int i = 0; (i = str.Find('<', i)) != -1; i++) { - int iEnd = str.Find('>', i + 1); - if (iEnd == -1) - return; - - CMStringW wszWord = str.Mid(i + 1, iEnd - i - 1); - if (wszWord[0] == '@') { // member highlight - int iStart = 1; - if (wszWord[1] == '!') - iStart++; - - USERINFO *ui = g_chatApi.UM_FindUser(si, wszWord.c_str() + iStart); - if (ui != nullptr) - str.Replace(L"<" + wszWord + L">", CMStringW(ui->pszNick) + L": "); - } - else if (wszWord[0] == '#') { - CDiscordUser *pUser = FindUserByChannel(_wtoi64(wszWord.c_str() + 1)); - if (pUser != nullptr) { - ptrW wszNick(getWStringA(pUser->hContact, DB_KEY_NICK)); - if (wszNick != nullptr) - str.Replace(L"<" + wszWord + L">", wszNick); - } - } - } -} +/* +Copyright © 2016-22 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +int StrToStatus(const CMStringW &str) +{ + if (str == L"idle") + return ID_STATUS_NA; + if (str == L"dnd") + return ID_STATUS_DND; + if (str == L"online") + return ID_STATUS_ONLINE; + if (str == L"offline") + return ID_STATUS_OFFLINE; + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +time_t StringToDate(const CMStringW &str) +{ + struct tm T = { 0 }; + int boo; + if (swscanf(str, L"%04d-%02d-%02dT%02d:%02d:%02d.%d", &T.tm_year, &T.tm_mon, &T.tm_mday, &T.tm_hour, &T.tm_min, &T.tm_sec, &boo) != 7) + return time(0); + + T.tm_year -= 1900; + T.tm_mon--; + time_t t = mktime(&T); + + _tzset(); + t -= _timezone; + return (t >= 0) ? t : 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static LONG volatile g_counter = 1; + +int SerialNext() +{ + return InterlockedIncrement(&g_counter); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +SnowFlake getId(const JSONNode &pNode) +{ + return _wtoi64(pNode.as_mstring()); +} + +SnowFlake CDiscordProto::getId(const char *szSetting) +{ + DBVARIANT dbv; + dbv.type = DBVT_BLOB; + if (db_get(0, m_szModuleName, szSetting, &dbv)) + return 0; + + SnowFlake result = (dbv.cpbVal == sizeof(SnowFlake)) ? *(SnowFlake*)dbv.pbVal : 0; + db_free(&dbv); + return result; +} + +SnowFlake CDiscordProto::getId(MCONTACT hContact, const char *szSetting) +{ + DBVARIANT dbv; + dbv.type = DBVT_BLOB; + if (db_get(hContact, m_szModuleName, szSetting, &dbv)) + return 0; + + SnowFlake result = (dbv.cpbVal == sizeof(SnowFlake)) ? *(SnowFlake*)dbv.pbVal : 0; + db_free(&dbv); + return result; +} + +void CDiscordProto::setId(const char *szSetting, SnowFlake iValue) +{ + SnowFlake oldVal = getId(szSetting); + if (oldVal != iValue) + db_set_blob(0, m_szModuleName, szSetting, &iValue, sizeof(iValue)); +} + +void CDiscordProto::setId(MCONTACT hContact, const char *szSetting, SnowFlake iValue) +{ + SnowFlake oldVal = getId(hContact, szSetting); + if (oldVal != iValue) + db_set_blob(hContact, m_szModuleName, szSetting, &iValue, sizeof(iValue)); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CopyId(const CMStringW &nick) +{ + if (!OpenClipboard(nullptr)) + return; + + EmptyClipboard(); + + int length = nick.GetLength() + 1; + if (HGLOBAL hMemory = GlobalAlloc(GMEM_FIXED, length * sizeof(wchar_t))) { + mir_wstrncpy((wchar_t*)GlobalLock(hMemory), nick, length); + GlobalUnlock(hMemory); + SetClipboardData(CF_UNICODETEXT, hMemory); + } + CloseClipboard(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static CDiscordUser *g_myUser = new CDiscordUser(0); + +CDiscordUser* CDiscordProto::FindUser(SnowFlake id) +{ + return arUsers.find((CDiscordUser*)&id); +} + +CDiscordUser* CDiscordProto::FindUser(const wchar_t *pwszUsername, int iDiscriminator) +{ + for (auto &p : arUsers) + if (p->wszUsername == pwszUsername && p->iDiscriminator == iDiscriminator) + return p; + + return nullptr; +} + +CDiscordUser* CDiscordProto::FindUserByChannel(SnowFlake channelId) +{ + for (auto &p : arUsers) + if (p->channelId == channelId) + return p; + + return nullptr; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Common JSON processing routines + +void CDiscordProto::PreparePrivateChannel(const JSONNode &root) +{ + CDiscordUser *pUser = nullptr; + + CMStringW wszChannelId = root["id"].as_mstring(); + SnowFlake channelId = _wtoi64(wszChannelId); + + int type = root["type"].as_int(); + switch (type) { + case 1: // single channel + for (auto &it : root["recipients"]) + pUser = PrepareUser(it); + if (pUser == nullptr) { + debugLogA("Invalid recipients list, exiting"); + return; + } + break; + + case 3: // private groupchat + if ((pUser = FindUserByChannel(channelId)) == nullptr) { + pUser = new CDiscordUser(channelId); + arUsers.insert(pUser); + } + pUser->bIsGroup = true; + pUser->wszUsername = wszChannelId; + pUser->wszChannelName = root["name"].as_mstring(); + { + SESSION_INFO *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, pUser->wszUsername, pUser->wszChannelName); + pUser->hContact = si->hContact; + + Chat_AddGroup(si, LPGENW("Owners")); + Chat_AddGroup(si, LPGENW("Participants")); + + SnowFlake ownerId = _wtoi64(root["owner_id"].as_mstring()); + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_JOIN }; + gce.pszID.w = pUser->wszUsername; + for (auto &it : root["recipients"]) { + CMStringW wszId = it["id"].as_mstring(); + CMStringW wszNick = it["nick"].as_mstring(); + if (wszNick.IsEmpty()) + wszNick = it["username"].as_mstring() + L"#" + it["discriminator"].as_mstring(); + + gce.pszUID.w = wszId; + gce.pszNick.w = wszNick; + gce.pszStatus.w = (_wtoi64(wszId) == ownerId) ? L"Owners" : L"Participants"; + Chat_Event(&gce); + } + + CMStringW wszId(FORMAT, L"%lld", getId(DB_KEY_ID)); + CMStringW wszNick(FORMAT, L"%s#%d", getMStringW(DB_KEY_NICK).c_str(), getDword(DB_KEY_DISCR)); + gce.bIsMe = true; + gce.pszUID.w = wszId; + gce.pszNick.w = wszNick; + gce.pszStatus.w = (_wtoi64(wszId) == ownerId) ? L"Owners" : L"Participants"; + Chat_Event(&gce); + + Chat_Control(m_szModuleName, pUser->wszUsername, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE); + Chat_Control(m_szModuleName, pUser->wszUsername, SESSION_ONLINE); + } + break; + + default: + debugLogA("Invalid channel type: %d, exiting", type); + return; + } + + pUser->channelId = channelId; + pUser->lastMsgId = ::getId(root["last_message_id"]); + pUser->bIsPrivate = true; + + setId(pUser->hContact, DB_KEY_CHANNELID, pUser->channelId); + + SnowFlake oldMsgId = getId(pUser->hContact, DB_KEY_LASTMSGID); + if (pUser->lastMsgId > oldMsgId) + RetrieveHistory(pUser, MSG_AFTER, oldMsgId, 99); +} + +CDiscordUser* CDiscordProto::PrepareUser(const JSONNode &user) +{ + SnowFlake id = ::getId(user["id"]); + if (id == m_ownId) + return g_myUser; + + int iDiscriminator = _wtoi(user["discriminator"].as_mstring()); + CMStringW username = user["username"].as_mstring(); + + CDiscordUser *pUser = FindUser(id); + if (pUser == nullptr) { + MCONTACT tmp = INVALID_CONTACT_ID; + + // no user found by userid, try to find him via username+discriminator + pUser = FindUser(username, iDiscriminator); + if (pUser != nullptr) { + // if found, remove the object from list to resort it (its userid==0) + if (pUser->hContact != 0) + tmp = pUser->hContact; + arUsers.remove(pUser); + } + pUser = new CDiscordUser(id); + pUser->wszUsername = username; + pUser->iDiscriminator = iDiscriminator; + if (tmp != INVALID_CONTACT_ID) { + // if we previously had a recently added contact without userid, write it down + pUser->hContact = tmp; + setId(pUser->hContact, DB_KEY_ID, id); + } + arUsers.insert(pUser); + } + + if (pUser->hContact == 0) { + MCONTACT hContact = db_add_contact(); + Proto_AddToContact(hContact, m_szModuleName); + + Clist_SetGroup(hContact, m_wszDefaultGroup); + setId(hContact, DB_KEY_ID, id); + setWString(hContact, DB_KEY_NICK, username); + setDword(hContact, DB_KEY_DISCR, iDiscriminator); + + pUser->hContact = hContact; + } + + CheckAvatarChange(pUser->hContact, user["avatar"].as_mstring()); + return pUser; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +CMStringW PrepareMessageText(const JSONNode &pRoot) +{ + CMStringW wszText = pRoot["content"].as_mstring(); + + bool bDelimiterAdded = false; + for (auto &it : pRoot["attachments"]) { + CMStringW wszUrl = it["url"].as_mstring(); + if (!wszUrl.IsEmpty()) { + if (!bDelimiterAdded) { + bDelimiterAdded = true; + wszText.Append(L"\n-----------------"); + } + wszText.AppendFormat(L"\n%s: %s", TranslateT("Attachment"), wszUrl.c_str()); + } + } + + for (auto &it : pRoot["embeds"]) { + wszText.Append(L"\n-----------------"); + + CMStringW str = it["url"].as_mstring(); + wszText.AppendFormat(L"\n%s: %s", TranslateT("Embed"), str.c_str()); + + str = it["provider"]["name"].as_mstring() + L" " + it["type"].as_mstring(); + if (str.GetLength() > 1) + wszText.AppendFormat(L"\n\t%s", str.c_str()); + + str = it["description"].as_mstring(); + if (!str.IsEmpty()) + wszText.AppendFormat(L"\n\t%s", str.c_str()); + + str = it["thumbnail"]["url"].as_mstring(); + if (!str.IsEmpty()) + wszText.AppendFormat(L"\n%s: %s", TranslateT("Preview"), str.c_str()); + } + + return wszText; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordProto::ProcessType(CDiscordUser *pUser, const JSONNode &pRoot) +{ + switch (pRoot["type"].as_int()) { + case 1: // confirmed + Contact::PutOnList(pUser->hContact); + delSetting(pUser->hContact, DB_KEY_REQAUTH); + delSetting(pUser->hContact, "ApparentMode"); + break; + + case 3: // expecting authorization + Contact::RemoveFromList(pUser->hContact); + if (!getByte(pUser->hContact, DB_KEY_REQAUTH, 0)) { + setByte(pUser->hContact, DB_KEY_REQAUTH, 1); + + CMStringA szId(FORMAT, "%lld", pUser->id); + DB::AUTH_BLOB blob(pUser->hContact, T2Utf(pUser->wszUsername), nullptr, nullptr, szId, nullptr); + + PROTORECVEVENT pre = { 0 }; + pre.timestamp = (uint32_t)time(0); + pre.lParam = blob.size(); + pre.szMessage = blob; + ProtoChainRecv(pUser->hContact, PSR_AUTH, 0, (LPARAM)&pre); + } + break; + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordProto::ParseSpecialChars(SESSION_INFO *si, CMStringW &str) +{ + for (int i = 0; (i = str.Find('<', i)) != -1; i++) { + int iEnd = str.Find('>', i + 1); + if (iEnd == -1) + return; + + CMStringW wszWord = str.Mid(i + 1, iEnd - i - 1); + if (wszWord[0] == '@') { // member highlight + int iStart = 1; + if (wszWord[1] == '!') + iStart++; + + USERINFO *ui = g_chatApi.UM_FindUser(si, wszWord.c_str() + iStart); + if (ui != nullptr) + str.Replace(L"<" + wszWord + L">", CMStringW(ui->pszNick) + L": "); + } + else if (wszWord[0] == '#') { + CDiscordUser *pUser = FindUserByChannel(_wtoi64(wszWord.c_str() + 1)); + if (pUser != nullptr) { + ptrW wszNick(getWStringA(pUser->hContact, DB_KEY_NICK)); + if (wszNick != nullptr) + str.Replace(L"<" + wszWord + L">", wszNick); + } + } + } +} diff --git a/protocols/Discord/src/version.h b/protocols/Discord/src/version.h index 138a7eaaec..1a33efa401 100644 --- a/protocols/Discord/src/version.h +++ b/protocols/Discord/src/version.h @@ -1,13 +1,13 @@ -#define __MAJOR_VERSION 0 -#define __MINOR_VERSION 6 -#define __RELEASE_NUM 2 -#define __BUILD_NUM 11 - -#include - -#define __PLUGIN_NAME "Discord protocol" -#define __FILENAME "Discord.dll" -#define __DESCRIPTION "Discord support for Miranda NG." -#define __AUTHOR "George Hazan" -#define __AUTHORWEB "https://miranda-ng.org/p/Discord/" -#define __COPYRIGHT "© 2016-22 Miranda NG team" +#define __MAJOR_VERSION 0 +#define __MINOR_VERSION 6 +#define __RELEASE_NUM 2 +#define __BUILD_NUM 11 + +#include + +#define __PLUGIN_NAME "Discord protocol" +#define __FILENAME "Discord.dll" +#define __DESCRIPTION "Discord support for Miranda NG." +#define __AUTHOR "George Hazan" +#define __AUTHORWEB "https://miranda-ng.org/p/Discord/" +#define __COPYRIGHT "© 2016-22 Miranda NG team" diff --git a/protocols/Discord/src/voice.cpp b/protocols/Discord/src/voice.cpp index 6e41bde300..5d1ccf1ea7 100644 --- a/protocols/Discord/src/voice.cpp +++ b/protocols/Discord/src/voice.cpp @@ -1,116 +1,116 @@ -/* -Copyright © 2016-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -///////////////////////////////////////////////////////////////////////////////////////// -// call operations (voice & video) - -void CDiscordProto::OnCommandCallCreated(const JSONNode &pRoot) -{ - for (auto &it : pRoot["voice_states"]) { - SnowFlake channelId = ::getId(pRoot["channel_id"]); - auto *pUser = FindUserByChannel(channelId); - if (pUser == nullptr) { - debugLogA("Call from unknown channel %lld, skipping", channelId); - continue; - } - - auto *pCall = new CDiscordVoiceCall(); - pCall->szId = it["session_id"].as_mstring(); - pCall->channelId = channelId; - pCall->startTime = time(0); - arVoiceCalls.insert(pCall); - - char *szMessage = TranslateU("Incoming call"); - DBEVENTINFO dbei = {}; - dbei.szModule = m_szModuleName; - dbei.timestamp = pCall->startTime; - dbei.eventType = EVENT_INCOMING_CALL; - dbei.cbBlob = uint32_t(mir_strlen(szMessage) + 1); - dbei.pBlob = (uint8_t *)szMessage; - dbei.flags = DBEF_UTF; - db_event_add(pUser->hContact, &dbei); - } -} - -void CDiscordProto::OnCommandCallDeleted(const JSONNode &pRoot) -{ - SnowFlake channelId = ::getId(pRoot["channel_id"]); - auto *pUser = FindUserByChannel(channelId); - if (pUser == nullptr) { - debugLogA("Call from unknown channel %lld, skipping", channelId); - return; - } - - int elapsed = 0, currTime = time(0); - for (auto &call : arVoiceCalls.rev_iter()) - if (call->channelId == channelId) { - elapsed = currTime - call->startTime; - arVoiceCalls.removeItem(&call); - break; - } - - if (!elapsed) { - debugLogA("Call from channel %lld isn't registered, skipping", channelId); - return; - } - - CMStringA szMessage(FORMAT, TranslateU("Call ended, %d seconds long"), elapsed); - DBEVENTINFO dbei = {}; - dbei.szModule = m_szModuleName; - dbei.timestamp = currTime; - dbei.eventType = EVENT_CALL_FINISHED; - dbei.cbBlob = uint32_t(szMessage.GetLength() + 1); - dbei.pBlob = (uint8_t *)szMessage.c_str(); - dbei.flags = DBEF_UTF; - db_event_add(pUser->hContact, &dbei); -} - -void CDiscordProto::OnCommandCallUpdated(const JSONNode&) -{ -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Events & services - -INT_PTR __cdecl CDiscordProto::VoiceCaps(WPARAM, LPARAM) -{ - return VOICE_CAPS_VOICE | VOICE_CAPS_CALL_CONTACT; -} - -int __cdecl CDiscordProto::OnVoiceState(WPARAM wParam, LPARAM) -{ - auto *pVoice = (VOICE_CALL *)wParam; - if (mir_strcmp(pVoice->moduleName, m_szModuleName)) - return 0; - - CDiscordVoiceCall *pCall = nullptr; - for (auto &it : arVoiceCalls) - if (it->szId == pVoice->id) { - pCall = it; - break; - } - - if (pCall == nullptr) { - debugLogA("Unknown call: %s, exiting", pVoice->id); - return 0; - } - - debugLogA("Call %s state changed to %d", pVoice->id, pVoice->state); - return 0; -} +/* +Copyright © 2016-22 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +///////////////////////////////////////////////////////////////////////////////////////// +// call operations (voice & video) + +void CDiscordProto::OnCommandCallCreated(const JSONNode &pRoot) +{ + for (auto &it : pRoot["voice_states"]) { + SnowFlake channelId = ::getId(pRoot["channel_id"]); + auto *pUser = FindUserByChannel(channelId); + if (pUser == nullptr) { + debugLogA("Call from unknown channel %lld, skipping", channelId); + continue; + } + + auto *pCall = new CDiscordVoiceCall(); + pCall->szId = it["session_id"].as_mstring(); + pCall->channelId = channelId; + pCall->startTime = time(0); + arVoiceCalls.insert(pCall); + + char *szMessage = TranslateU("Incoming call"); + DBEVENTINFO dbei = {}; + dbei.szModule = m_szModuleName; + dbei.timestamp = pCall->startTime; + dbei.eventType = EVENT_INCOMING_CALL; + dbei.cbBlob = uint32_t(mir_strlen(szMessage) + 1); + dbei.pBlob = (uint8_t *)szMessage; + dbei.flags = DBEF_UTF; + db_event_add(pUser->hContact, &dbei); + } +} + +void CDiscordProto::OnCommandCallDeleted(const JSONNode &pRoot) +{ + SnowFlake channelId = ::getId(pRoot["channel_id"]); + auto *pUser = FindUserByChannel(channelId); + if (pUser == nullptr) { + debugLogA("Call from unknown channel %lld, skipping", channelId); + return; + } + + int elapsed = 0, currTime = time(0); + for (auto &call : arVoiceCalls.rev_iter()) + if (call->channelId == channelId) { + elapsed = currTime - call->startTime; + arVoiceCalls.removeItem(&call); + break; + } + + if (!elapsed) { + debugLogA("Call from channel %lld isn't registered, skipping", channelId); + return; + } + + CMStringA szMessage(FORMAT, TranslateU("Call ended, %d seconds long"), elapsed); + DBEVENTINFO dbei = {}; + dbei.szModule = m_szModuleName; + dbei.timestamp = currTime; + dbei.eventType = EVENT_CALL_FINISHED; + dbei.cbBlob = uint32_t(szMessage.GetLength() + 1); + dbei.pBlob = (uint8_t *)szMessage.c_str(); + dbei.flags = DBEF_UTF; + db_event_add(pUser->hContact, &dbei); +} + +void CDiscordProto::OnCommandCallUpdated(const JSONNode&) +{ +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Events & services + +INT_PTR __cdecl CDiscordProto::VoiceCaps(WPARAM, LPARAM) +{ + return VOICE_CAPS_VOICE | VOICE_CAPS_CALL_CONTACT; +} + +int __cdecl CDiscordProto::OnVoiceState(WPARAM wParam, LPARAM) +{ + auto *pVoice = (VOICE_CALL *)wParam; + if (mir_strcmp(pVoice->moduleName, m_szModuleName)) + return 0; + + CDiscordVoiceCall *pCall = nullptr; + for (auto &it : arVoiceCalls) + if (it->szId == pVoice->id) { + pCall = it; + break; + } + + if (pCall == nullptr) { + debugLogA("Unknown call: %s, exiting", pVoice->id); + return 0; + } + + debugLogA("Call %s state changed to %d", pVoice->id, pVoice->state); + return 0; +} -- cgit v1.2.3