diff options
Diffstat (limited to 'protocols/Discord')
29 files changed, 5048 insertions, 5048 deletions
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 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> - <ItemGroup Label="ProjectConfigurations"> - <ProjectConfiguration Include="Debug|Win32"> - <Configuration>Debug</Configuration> - <Platform>Win32</Platform> - </ProjectConfiguration> - <ProjectConfiguration Include="Debug|x64"> - <Configuration>Debug</Configuration> - <Platform>x64</Platform> - </ProjectConfiguration> - <ProjectConfiguration Include="Release|Win32"> - <Configuration>Release</Configuration> - <Platform>Win32</Platform> - </ProjectConfiguration> - <ProjectConfiguration Include="Release|x64"> - <Configuration>Release</Configuration> - <Platform>x64</Platform> - </ProjectConfiguration> - </ItemGroup> - <PropertyGroup Label="Globals"> - <ProjectGuid>{88928401-2CE8-4568-AAA7-226141870CBF}</ProjectGuid> - <ProjectName>Discord</ProjectName> - </PropertyGroup> - <ImportGroup Label="PropertySheets"> - <Import Project="$(ProjectDir)..\..\build\vc.common\plugin.props" /> - </ImportGroup> - <ItemGroup> - <ClCompile Include="src\avatars.cpp" /> - <ClCompile Include="src\connection.cpp" /> - <ClCompile Include="src\dispatch.cpp" /> - <ClCompile Include="src\gateway.cpp" /> - <ClCompile Include="src\groupchat.cpp" /> - <ClCompile Include="src\guilds.cpp" /> - <ClCompile Include="src\http.cpp" /> - <ClCompile Include="src\main.cpp" /> - <ClCompile Include="src\menus.cpp" /> - <ClCompile Include="src\options.cpp" /> - <ClCompile Include="src\proto.cpp" /> - <ClCompile Include="src\server.cpp" /> - <ClCompile Include="src\stdafx.cxx"> - <PrecompiledHeader>Create</PrecompiledHeader> - </ClCompile> - <ClCompile Include="src\utils.cpp" /> - <ClCompile Include="src\voice.cpp" /> - <ClInclude Include="src\proto.h" /> - <ClInclude Include="src\resource.h" /> - <ClInclude Include="src\stdafx.h" /> - <ClInclude Include="src\version.h" /> - </ItemGroup> - <ItemGroup> - <ProjectReference Include="..\..\libs\zlib\zlib.vcxproj"> - <Project>{01F9E227-06F5-4BED-907F-402CA7DFAFE6}</Project> - <ReferenceOutputAssembly>false</ReferenceOutputAssembly> - </ProjectReference> - </ItemGroup> - <ItemGroup> - <ProjectReference Include="..\..\libs\libjson\libjson.vcxproj"> - <Project>{f6a9340e-b8d9-4c75-be30-47dc66d0abc7}</Project> - </ProjectReference> - </ItemGroup> - <ItemGroup> - <ResourceCompile Include="res\discord.rc" /> - <ResourceCompile Include="res\version.rc" /> - </ItemGroup> +<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ItemGroup Label="ProjectConfigurations">
+ <ProjectConfiguration Include="Debug|Win32">
+ <Configuration>Debug</Configuration>
+ <Platform>Win32</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Debug|x64">
+ <Configuration>Debug</Configuration>
+ <Platform>x64</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Release|Win32">
+ <Configuration>Release</Configuration>
+ <Platform>Win32</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Release|x64">
+ <Configuration>Release</Configuration>
+ <Platform>x64</Platform>
+ </ProjectConfiguration>
+ </ItemGroup>
+ <PropertyGroup Label="Globals">
+ <ProjectGuid>{88928401-2CE8-4568-AAA7-226141870CBF}</ProjectGuid>
+ <ProjectName>Discord</ProjectName>
+ </PropertyGroup>
+ <ImportGroup Label="PropertySheets">
+ <Import Project="$(ProjectDir)..\..\build\vc.common\plugin.props" />
+ </ImportGroup>
+ <ItemGroup>
+ <ClCompile Include="src\avatars.cpp" />
+ <ClCompile Include="src\connection.cpp" />
+ <ClCompile Include="src\dispatch.cpp" />
+ <ClCompile Include="src\gateway.cpp" />
+ <ClCompile Include="src\groupchat.cpp" />
+ <ClCompile Include="src\guilds.cpp" />
+ <ClCompile Include="src\http.cpp" />
+ <ClCompile Include="src\main.cpp" />
+ <ClCompile Include="src\menus.cpp" />
+ <ClCompile Include="src\options.cpp" />
+ <ClCompile Include="src\proto.cpp" />
+ <ClCompile Include="src\server.cpp" />
+ <ClCompile Include="src\stdafx.cxx">
+ <PrecompiledHeader>Create</PrecompiledHeader>
+ </ClCompile>
+ <ClCompile Include="src\utils.cpp" />
+ <ClCompile Include="src\voice.cpp" />
+ <ClInclude Include="src\proto.h" />
+ <ClInclude Include="src\resource.h" />
+ <ClInclude Include="src\stdafx.h" />
+ <ClInclude Include="src\version.h" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\libs\zlib\zlib.vcxproj">
+ <Project>{01F9E227-06F5-4BED-907F-402CA7DFAFE6}</Project>
+ <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\libs\libjson\libjson.vcxproj">
+ <Project>{f6a9340e-b8d9-4c75-be30-47dc66d0abc7}</Project>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <ResourceCompile Include="res\discord.rc" />
+ <ResourceCompile Include="res\version.rc" />
+ </ItemGroup>
</Project>
\ 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 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> - <Import Project="$(ProjectDir)..\..\build\vc.common\common.filters" /> - <ItemGroup> - <ClCompile Include="src\avatars.cpp"> - <Filter>Source Files</Filter> - </ClCompile> - <ClCompile Include="src\connection.cpp"> - <Filter>Source Files</Filter> - </ClCompile> - <ClCompile Include="src\dispatch.cpp"> - <Filter>Source Files</Filter> - </ClCompile> - <ClCompile Include="src\gateway.cpp"> - <Filter>Source Files</Filter> - </ClCompile> - <ClCompile Include="src\groupchat.cpp"> - <Filter>Source Files</Filter> - </ClCompile> - <ClCompile Include="src\guilds.cpp"> - <Filter>Source Files</Filter> - </ClCompile> - <ClCompile Include="src\http.cpp"> - <Filter>Source Files</Filter> - </ClCompile> - <ClCompile Include="src\main.cpp"> - <Filter>Source Files</Filter> - </ClCompile> - <ClCompile Include="src\menus.cpp"> - <Filter>Source Files</Filter> - </ClCompile> - <ClCompile Include="src\options.cpp"> - <Filter>Source Files</Filter> - </ClCompile> - <ClCompile Include="src\proto.cpp"> - <Filter>Source Files</Filter> - </ClCompile> - <ClCompile Include="src\server.cpp"> - <Filter>Source Files</Filter> - </ClCompile> - <ClCompile Include="src\stdafx.cxx"> - <Filter>Source Files</Filter> - </ClCompile> - <ClCompile Include="src\utils.cpp"> - <Filter>Source Files</Filter> - </ClCompile> - <ClCompile Include="src\voice.cpp"> - <Filter>Source Files</Filter> - </ClCompile> - </ItemGroup> - <ItemGroup> - <ClInclude Include="src\proto.h"> - <Filter>Header Files</Filter> - </ClInclude> - <ClInclude Include="src\resource.h"> - <Filter>Header Files</Filter> - </ClInclude> - <ClInclude Include="src\stdafx.h"> - <Filter>Header Files</Filter> - </ClInclude> - <ClInclude Include="src\version.h"> - <Filter>Header Files</Filter> - </ClInclude> - </ItemGroup> - <ItemGroup> - <ResourceCompile Include="res\discord.rc"> - <Filter>Resource Files</Filter> - </ResourceCompile> - <ResourceCompile Include="res\version.rc"> - <Filter>Resource Files</Filter> - </ResourceCompile> - </ItemGroup> +<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(ProjectDir)..\..\build\vc.common\common.filters" />
+ <ItemGroup>
+ <ClCompile Include="src\avatars.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\connection.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\dispatch.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\gateway.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\groupchat.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\guilds.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\http.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\main.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\menus.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\options.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\proto.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\server.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\stdafx.cxx">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\utils.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\voice.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ </ItemGroup>
+ <ItemGroup>
+ <ClInclude Include="src\proto.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
+ <ClInclude Include="src\resource.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
+ <ClInclude Include="src\stdafx.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
+ <ClInclude Include="src\version.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
+ </ItemGroup>
+ <ItemGroup>
+ <ResourceCompile Include="res\discord.rc">
+ <Filter>Resource Files</Filter>
+ </ResourceCompile>
+ <ResourceCompile Include="res\version.rc">
+ <Filter>Resource Files</Filter>
+ </ResourceCompile>
+ </ItemGroup>
</Project>
\ 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 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> - <ItemGroup Label="ProjectConfigurations"> - <ProjectConfiguration Include="Debug|Win32"> - <Configuration>Debug</Configuration> - <Platform>Win32</Platform> - </ProjectConfiguration> - <ProjectConfiguration Include="Debug|x64"> - <Configuration>Debug</Configuration> - <Platform>x64</Platform> - </ProjectConfiguration> - <ProjectConfiguration Include="Release|Win32"> - <Configuration>Release</Configuration> - <Platform>Win32</Platform> - </ProjectConfiguration> - <ProjectConfiguration Include="Release|x64"> - <Configuration>Release</Configuration> - <Platform>x64</Platform> - </ProjectConfiguration> - </ItemGroup> - <PropertyGroup Label="Globals"> - <ProjectName>Proto_Discord</ProjectName> - <ProjectGuid>{6B8BA5EE-3815-44A6-A13B-2A22E8B3A311}</ProjectGuid> - </PropertyGroup> - <ImportGroup Label="PropertySheets"> - <Import Project="$(ProjectDir)..\..\..\build\vc.common\icons.props" /> - </ImportGroup> - <ItemGroup> - <ClInclude Include="src\resource.h" /> - </ItemGroup> - <ItemGroup> - <ResourceCompile Include="res\Proto_Discord.rc" /> - </ItemGroup> +<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ItemGroup Label="ProjectConfigurations">
+ <ProjectConfiguration Include="Debug|Win32">
+ <Configuration>Debug</Configuration>
+ <Platform>Win32</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Debug|x64">
+ <Configuration>Debug</Configuration>
+ <Platform>x64</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Release|Win32">
+ <Configuration>Release</Configuration>
+ <Platform>Win32</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Release|x64">
+ <Configuration>Release</Configuration>
+ <Platform>x64</Platform>
+ </ProjectConfiguration>
+ </ItemGroup>
+ <PropertyGroup Label="Globals">
+ <ProjectName>Proto_Discord</ProjectName>
+ <ProjectGuid>{6B8BA5EE-3815-44A6-A13B-2A22E8B3A311}</ProjectGuid>
+ </PropertyGroup>
+ <ImportGroup Label="PropertySheets">
+ <Import Project="$(ProjectDir)..\..\..\build\vc.common\icons.props" />
+ </ImportGroup>
+ <ItemGroup>
+ <ClInclude Include="src\resource.h" />
+ </ItemGroup>
+ <ItemGroup>
+ <ResourceCompile Include="res\Proto_Discord.rc" />
+ </ItemGroup>
</Project>
\ 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 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> - <Import Project="$(ProjectDir)..\..\..\build\vc.common\common.filters" /> - <ItemGroup> - <ClInclude Include="src\resource.h"> - <Filter>Header Files</Filter> - </ClInclude> - </ItemGroup> - <ItemGroup> - <ResourceCompile Include="res\Proto_Discord.rc"> - <Filter>Resource Files</Filter> - </ResourceCompile> - </ItemGroup> +<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(ProjectDir)..\..\..\build\vc.common\common.filters" />
+ <ItemGroup>
+ <ClInclude Include="src\resource.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
+ </ItemGroup>
+ <ItemGroup>
+ <ResourceCompile Include="res\Proto_Discord.rc">
+ <Filter>Resource Files</Filter>
+ </ResourceCompile>
+ </ItemGroup>
</Project>
\ 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 <http://www.gnu.org/licenses/>. -*/ - -#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <http://www.gnu.org/licenses/>. -*/ - -#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <http://www.gnu.org/licenses/>. -*/ - -#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <http://www.gnu.org/licenses/>. -*/ - -#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <http://www.gnu.org/licenses/>. -*/ - -#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <http://www.gnu.org/licenses/>. -*/ - -#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <http://www.gnu.org/licenses/>. -*/ - -#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <http://www.gnu.org/licenses/>. -*/ - -#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<CDiscordProto>("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 <http://www.gnu.org/licenses/>.
+*/
+
+#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<CDiscordProto>("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 <http://www.gnu.org/licenses/>. -*/ - -#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <http://www.gnu.org/licenses/>. -*/ - -#include "stdafx.h" - -///////////////////////////////////////////////////////////////////////////////////////// - -class CDiscardAccountOptions : public CProtoDlgBase<CDiscordProto> -{ - CCtrlCheck chkUseChats, chkHideChats, chkUseGroups, chkDeleteMsgs; - CCtrlEdit m_edGroup, m_edUserName, m_edPassword; - ptrW m_wszOldGroup; - -public: - CDiscardAccountOptions(CDiscordProto *ppro, int iDlgID, bool bFullDlg) : - CProtoDlgBase<CDiscordProto>(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 <http://www.gnu.org/licenses/>.
+*/
+
+#include "stdafx.h"
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+class CDiscardAccountOptions : public CProtoDlgBase<CDiscordProto>
+{
+ CCtrlCheck chkUseChats, chkHideChats, chkUseGroups, chkDeleteMsgs;
+ CCtrlEdit m_edGroup, m_edUserName, m_edPassword;
+ ptrW m_wszOldGroup;
+
+public:
+ CDiscardAccountOptions(CDiscordProto *ppro, int iDlgID, bool bFullDlg) :
+ CProtoDlgBase<CDiscordProto>(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 <http://www.gnu.org/licenses/>. -*/ - -#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<CDiscordProto>(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<MCONTACT> 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 <http://www.gnu.org/licenses/>.
+*/
+
+#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<CDiscordProto>(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<MCONTACT> 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<CDiscordProto> -{ - 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<CDiscordUser> arChannels; - - SESSION_INFO *pParentSi; - OBJLIST<CDiscordGuildMember> arChatUsers; - OBJLIST<CDiscordRole> 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<CDiscordProto> -{ - 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<AsyncHttpRequest> 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<wchar_t*> m_wszEmail; // my own email - CMOption<wchar_t*> m_wszDefaultGroup; // clist group to store contacts - CMOption<uint8_t> m_bUseGroupchats; // Shall we connect Guilds at all? - CMOption<uint8_t> m_bHideGroupchats; // Do not open chat windows on creation - CMOption<uint8_t> m_bUseGuildGroups; // use special subgroups for guilds - CMOption<uint8_t> m_bSyncDeleteMsgs; // delete messages from Miranda if they are deleted at the server - - ////////////////////////////////////////////////////////////////////////////////////// - // common data - - SnowFlake m_ownId; - - mir_cs csMarkReadQueue; - LIST<CDiscordUser> arMarkReadQueue; - - OBJLIST<CDiscordUser> arUsers; - OBJLIST<COwnMessage> arOwnMessages; - OBJLIST<CDiscordVoiceCall> 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<CDiscordGuild> 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<CDiscordProto> -{ - 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<CDiscordProto>
+{
+ 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<CDiscordUser> arChannels;
+
+ SESSION_INFO *pParentSi;
+ OBJLIST<CDiscordGuildMember> arChatUsers;
+ OBJLIST<CDiscordRole> 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<CDiscordProto>
+{
+ 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<AsyncHttpRequest> 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<wchar_t*> m_wszEmail; // my own email
+ CMOption<wchar_t*> m_wszDefaultGroup; // clist group to store contacts
+ CMOption<uint8_t> m_bUseGroupchats; // Shall we connect Guilds at all?
+ CMOption<uint8_t> m_bHideGroupchats; // Do not open chat windows on creation
+ CMOption<uint8_t> m_bUseGuildGroups; // use special subgroups for guilds
+ CMOption<uint8_t> m_bSyncDeleteMsgs; // delete messages from Miranda if they are deleted at the server
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // common data
+
+ SnowFlake m_ownId;
+
+ mir_cs csMarkReadQueue;
+ LIST<CDiscordUser> arMarkReadQueue;
+
+ OBJLIST<CDiscordUser> arUsers;
+ OBJLIST<COwnMessage> arOwnMessages;
+ OBJLIST<CDiscordVoiceCall> 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<CDiscordGuild> 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<CDiscordProto>
+{
+ 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 <http://www.gnu.org/licenses/>. -*/ - -#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<JSONNode> 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 <http://www.gnu.org/licenses/>.
+*/
+
+#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<JSONNode> 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 <http://www.gnu.org/licenses/>. -*/ - +/*
+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 <http://www.gnu.org/licenses/>.
+*/
+
#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 <Windows.h> -#include <Shlwapi.h> -#include <Wincrypt.h> - -#include <malloc.h> -#include <stdio.h> -#include <io.h> -#include <fcntl.h> -#include <direct.h> -#include <time.h> - -#include <vector> - -#include "resource.h" - -#include <m_system.h> -#include <newpluginapi.h> -#include <m_avatars.h> -#include <m_chat_int.h> -#include <m_clist.h> -#include <m_contacts.h> -#include <m_database.h> -#include <m_folders.h> -#include <m_gui.h> -#include <m_history.h> -#include <m_hotkeys.h> -#include <m_icolib.h> -#include <m_json.h> -#include <m_langpack.h> -#include <m_message.h> -#include <m_netlib.h> -#include <m_options.h> -#include <m_popup.h> -#include <m_protocols.h> -#include <m_protosvc.h> -#include <m_protoint.h> -#include <m_skin.h> -#include <m_srmm_int.h> -#include <m_userinfo.h> -#include <m_utils.h> -#include <m_voice.h> -#include <m_voiceservice.h> - -#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 <Windows.h>
+#include <Shlwapi.h>
+#include <Wincrypt.h>
+
+#include <malloc.h>
+#include <stdio.h>
+#include <io.h>
+#include <fcntl.h>
+#include <direct.h>
+#include <time.h>
+
+#include <vector>
+
+#include "resource.h"
+
+#include <m_system.h>
+#include <newpluginapi.h>
+#include <m_avatars.h>
+#include <m_chat_int.h>
+#include <m_clist.h>
+#include <m_contacts.h>
+#include <m_database.h>
+#include <m_folders.h>
+#include <m_gui.h>
+#include <m_history.h>
+#include <m_hotkeys.h>
+#include <m_icolib.h>
+#include <m_json.h>
+#include <m_langpack.h>
+#include <m_message.h>
+#include <m_netlib.h>
+#include <m_options.h>
+#include <m_popup.h>
+#include <m_protocols.h>
+#include <m_protosvc.h>
+#include <m_protoint.h>
+#include <m_skin.h>
+#include <m_srmm_int.h>
+#include <m_userinfo.h>
+#include <m_utils.h>
+#include <m_voice.h>
+#include <m_voiceservice.h>
+
+#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 <http://www.gnu.org/licenses/>. -*/ - -#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <stdver.h> - -#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 <stdver.h>
+
+#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 <http://www.gnu.org/licenses/>. -*/ - -#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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;
+}
|