summaryrefslogtreecommitdiff
path: root/protocols/Discord
diff options
context:
space:
mode:
Diffstat (limited to 'protocols/Discord')
-rw-r--r--protocols/Discord/CMakeLists.txt8
-rw-r--r--protocols/Discord/discord.vcxproj130
-rw-r--r--protocols/Discord/discord.vcxproj.filters144
-rw-r--r--protocols/Discord/proto_discord/CMakeLists.txt2
-rw-r--r--protocols/Discord/proto_discord/Proto_Discord.vcxproj66
-rw-r--r--protocols/Discord/proto_discord/Proto_Discord.vcxproj.filters26
-rw-r--r--protocols/Discord/proto_discord/res/Proto_Discord.rc148
-rw-r--r--protocols/Discord/proto_discord/src/resource.h46
-rw-r--r--protocols/Discord/res/discord.rc318
-rw-r--r--protocols/Discord/res/version.rc18
-rw-r--r--protocols/Discord/src/avatars.cpp410
-rw-r--r--protocols/Discord/src/connection.cpp246
-rw-r--r--protocols/Discord/src/dispatch.cpp1184
-rw-r--r--protocols/Discord/src/gateway.cpp692
-rw-r--r--protocols/Discord/src/groupchat.cpp470
-rw-r--r--protocols/Discord/src/guilds.cpp826
-rw-r--r--protocols/Discord/src/http.cpp310
-rw-r--r--protocols/Discord/src/main.cpp142
-rw-r--r--protocols/Discord/src/menus.cpp344
-rw-r--r--protocols/Discord/src/options.cpp200
-rw-r--r--protocols/Discord/src/proto.cpp1536
-rw-r--r--protocols/Discord/src/proto.h952
-rw-r--r--protocols/Discord/src/resource.h60
-rw-r--r--protocols/Discord/src/server.cpp614
-rw-r--r--protocols/Discord/src/stdafx.cxx34
-rw-r--r--protocols/Discord/src/stdafx.h160
-rw-r--r--protocols/Discord/src/utils.cpp752
-rw-r--r--protocols/Discord/src/version.h26
-rw-r--r--protocols/Discord/src/voice.cpp232
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;
+}