diff options
Diffstat (limited to 'protocols/Teams')
44 files changed, 5935 insertions, 0 deletions
diff --git a/protocols/Teams/CMakeLists.txt b/protocols/Teams/CMakeLists.txt new file mode 100644 index 0000000000..32662a7b47 --- /dev/null +++ b/protocols/Teams/CMakeLists.txt @@ -0,0 +1,6 @@ +file(GLOB SOURCES "src/*.h" "src/api/*.h" "src/*.cpp" "res/*.rc") +set(TARGET Teams) +include(${CMAKE_SOURCE_DIR}/cmake/plugin.cmake) +target_link_libraries(${TARGET}) +set_target_properties(${TARGET} PROPERTIES COMPILE_FLAGS "/std:c++17") +add_subdirectory(proto_teams)
\ No newline at end of file diff --git a/protocols/Teams/Teams.vcxproj b/protocols/Teams/Teams.vcxproj new file mode 100644 index 0000000000..b689769f6d --- /dev/null +++ b/protocols/Teams/Teams.vcxproj @@ -0,0 +1,73 @@ +<?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>{DCD56CEC-C61B-4275-A010-8C65C5B48815}</ProjectGuid> + <ProjectName>Teams</ProjectName> + </PropertyGroup> + <ImportGroup Label="PropertySheets"> + <Import Project="$(ProjectDir)..\..\build\vc.common\plugin.props" /> + </ImportGroup> + <ItemGroup> + <ClCompile Include="src\main.cpp" /> + <ClCompile Include="src\stdafx.cxx"> + <PrecompiledHeader>Create</PrecompiledHeader> + </ClCompile> + <ClCompile Include="src\teams_avatars.cpp" /> + <ClCompile Include="src\teams_chatrooms.cpp" /> + <ClCompile Include="src\teams_contacts.cpp" /> + <ClCompile Include="src\teams_files.cpp" /> + <ClCompile Include="src\teams_history.cpp" /> + <ClCompile Include="src\teams_http.cpp" /> + <ClCompile Include="src\teams_login.cpp" /> + <ClCompile Include="src\teams_menus.cpp" /> + <ClCompile Include="src\teams_messages.cpp" /> + <ClCompile Include="src\teams_options.cpp" /> + <ClCompile Include="src\teams_popups.cpp" /> + <ClCompile Include="src\teams_profile.cpp" /> + <ClCompile Include="src\teams_proto.cpp" /> + <ClCompile Include="src\teams_search.cpp" /> + <ClCompile Include="src\teams_server.cpp" /> + <ClCompile Include="src\teams_trouter.cpp" /> + <ClCompile Include="src\teams_utils.cpp" /> + <ClInclude Include="src\resource.h" /> + <ClInclude Include="src\stdafx.h" /> + <ClInclude Include="src\teams_menus.h" /> + <ClInclude Include="src\teams_proto.h" /> + <ClInclude Include="src\teams_utils.h" /> + <ClInclude Include="src\version.h" /> + </ItemGroup> + <ItemGroup> + <ResourceCompile Include="res\Resource.rc" /> + <ResourceCompile Include="res\version.rc" /> + </ItemGroup> + <ItemGroup> + <Image Include="res\teams.ico" /> + </ItemGroup> + <ItemDefinitionGroup> + <ClCompile> + <LanguageStandard>stdcpp17</LanguageStandard> + <AdditionalIncludeDirectories>.;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + </ClCompile> + <Link> + <AdditionalDependencies>libcrypto.lib;libssl.lib;%(AdditionalDependencies)</AdditionalDependencies> + </Link> + </ItemDefinitionGroup> +</Project>
\ No newline at end of file diff --git a/protocols/Teams/Teams.vcxproj.filters b/protocols/Teams/Teams.vcxproj.filters new file mode 100644 index 0000000000..45db7abfb8 --- /dev/null +++ b/protocols/Teams/Teams.vcxproj.filters @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <ItemGroup> + <ClCompile Include="src\main.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\stdafx.cxx"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_proto.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_options.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_login.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_http.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_avatars.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_chatrooms.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_contacts.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_files.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_history.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_menus.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_messages.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_popups.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_profile.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_utils.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_search.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_trouter.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\teams_server.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + </ItemGroup> + <ItemGroup> + <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> + <ClInclude Include="src\teams_menus.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="src\teams_utils.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="src\teams_proto.h"> + <Filter>Header Files</Filter> + </ClInclude> + </ItemGroup> + <ItemGroup> + <ResourceCompile Include="res\Resource.rc"> + <Filter>Resource Files</Filter> + </ResourceCompile> + <ResourceCompile Include="res\version.rc"> + <Filter>Resource Files</Filter> + </ResourceCompile> + </ItemGroup> + <ItemGroup> + <Image Include="res\teams.ico"> + <Filter>Resource Files</Filter> + </Image> + </ItemGroup> + <Import Project="$(ProjectDir)..\..\build\vc.common\common.filters" /> +</Project>
\ No newline at end of file diff --git a/protocols/Teams/proto_teams/CMakeLists.txt b/protocols/Teams/proto_teams/CMakeLists.txt new file mode 100644 index 0000000000..c574fe751e --- /dev/null +++ b/protocols/Teams/proto_teams/CMakeLists.txt @@ -0,0 +1,2 @@ +set(TARGET Proto_Teams) +include(${CMAKE_SOURCE_DIR}/cmake/icons.cmake)
\ No newline at end of file diff --git a/protocols/Teams/proto_teams/Proto_Teams.vcxproj b/protocols/Teams/proto_teams/Proto_Teams.vcxproj new file mode 100644 index 0000000000..bedc42ba98 --- /dev/null +++ b/protocols/Teams/proto_teams/Proto_Teams.vcxproj @@ -0,0 +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_Teams</ProjectName> + <ProjectGuid>{9C0BBF52-FE1D-4F07-9422-2B3321CFBE88}</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_Teams.rc" /> + </ItemGroup> +</Project>
\ No newline at end of file diff --git a/protocols/Teams/proto_teams/Proto_Teams.vcxproj.filters b/protocols/Teams/proto_teams/Proto_Teams.vcxproj.filters new file mode 100644 index 0000000000..fc4b59ab56 --- /dev/null +++ b/protocols/Teams/proto_teams/Proto_Teams.vcxproj.filters @@ -0,0 +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_Teams.rc"> + <Filter>Resource Files</Filter> + </ResourceCompile> + </ItemGroup> +</Project>
\ No newline at end of file diff --git a/protocols/Teams/proto_teams/res/Away.ico b/protocols/Teams/proto_teams/res/Away.ico Binary files differnew file mode 100644 index 0000000000..a6b544e597 --- /dev/null +++ b/protocols/Teams/proto_teams/res/Away.ico diff --git a/protocols/Teams/proto_teams/res/DND.ico b/protocols/Teams/proto_teams/res/DND.ico Binary files differnew file mode 100644 index 0000000000..5919b2e563 --- /dev/null +++ b/protocols/Teams/proto_teams/res/DND.ico diff --git a/protocols/Teams/proto_teams/res/Invisible.ico b/protocols/Teams/proto_teams/res/Invisible.ico Binary files differnew file mode 100644 index 0000000000..673f13a81a --- /dev/null +++ b/protocols/Teams/proto_teams/res/Invisible.ico diff --git a/protocols/Teams/proto_teams/res/NA.ico b/protocols/Teams/proto_teams/res/NA.ico Binary files differnew file mode 100644 index 0000000000..52565c70e5 --- /dev/null +++ b/protocols/Teams/proto_teams/res/NA.ico diff --git a/protocols/Teams/proto_teams/res/Occupied.ico b/protocols/Teams/proto_teams/res/Occupied.ico Binary files differnew file mode 100644 index 0000000000..442547f096 --- /dev/null +++ b/protocols/Teams/proto_teams/res/Occupied.ico diff --git a/protocols/Teams/proto_teams/res/Offline.ico b/protocols/Teams/proto_teams/res/Offline.ico Binary files differnew file mode 100644 index 0000000000..8ab458ca04 --- /dev/null +++ b/protocols/Teams/proto_teams/res/Offline.ico diff --git a/protocols/Teams/proto_teams/res/Online.ico b/protocols/Teams/proto_teams/res/Online.ico Binary files differnew file mode 100644 index 0000000000..a709cb11ad --- /dev/null +++ b/protocols/Teams/proto_teams/res/Online.ico diff --git a/protocols/Teams/proto_teams/res/Proto_Teams.rc b/protocols/Teams/proto_teams/res/Proto_Teams.rc new file mode 100644 index 0000000000..86540dee25 --- /dev/null +++ b/protocols/Teams/proto_teams/res/Proto_Teams.rc @@ -0,0 +1,76 @@ +// Microsoft Visual C++ generated resource script. +// +#include "..\src\resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "afxres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// Russian (Russia) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_RUS) +LANGUAGE LANG_RUSSIAN, SUBLANG_DEFAULT + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "..\\src\\resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""afxres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_ICON1 ICON "Offline.ico" +IDI_ICON2 ICON "Online.ico" +IDI_ICON3 ICON "Away.ico" +IDI_ICON4 ICON "Invisible.ico" +IDI_ICON5 ICON "NA.ico" +IDI_ICON6 ICON "DND.ico" +IDI_ICON7 ICON "Occupied.ico" + +#endif // Russian (Russia) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/protocols/Teams/proto_teams/src/resource.h b/protocols/Teams/proto_teams/src/resource.h new file mode 100644 index 0000000000..e473010b48 --- /dev/null +++ b/protocols/Teams/proto_teams/src/resource.h @@ -0,0 +1,22 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Proto_IRC.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 + +// 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 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/protocols/Teams/res/Resource.rc b/protocols/Teams/res/Resource.rc new file mode 100644 index 0000000000..25c910f1d2 --- /dev/null +++ b/protocols/Teams/res/Resource.rc @@ -0,0 +1,204 @@ +// 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 + +///////////////////////////////////////////////////////////////////////////// +// Neutral (Default) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_NEUD) +LANGUAGE LANG_NEUTRAL, SUBLANG_DEFAULT +#pragma code_page(1251) + +#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_TEAMS ICON "teams.ico" + +#endif // Neutral (Default) resources +///////////////////////////////////////////////////////////////////////////// + + +///////////////////////////////////////////////////////////////////////////// +// English (Neutral) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_NEUTRAL +#pragma code_page(1252) + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_ACCOUNT_MANAGER DIALOGEX 0, 0, 186, 119 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +EXSTYLE WS_EX_CONTROLPARENT +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "Login:",IDC_STATIC,0,2,58,12 + EDITTEXT IDC_LOGIN,60,0,124,12,ES_AUTOHSCROLL | WS_DISABLED + LTEXT "Default group:",IDC_STATIC,0,16,58,12 + EDITTEXT IDC_GROUP,60,14,124,12,ES_AUTOHSCROLL + PUSHBUTTON "Logout",IDC_LOGOUT,130,33,50,14 +END + +IDD_OPTIONS_MAIN DIALOGEX 0, 0, 310, 149 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD +EXSTYLE WS_EX_CONTROLPARENT +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + GROUPBOX "Account",IDC_STATIC,7,7,296,45 + LTEXT "Login:",IDC_STATIC,13,19,152,11 + EDITTEXT IDC_LOGIN,178,17,117,12,ES_AUTOHSCROLL | WS_DISABLED + LTEXT "Default group:",IDC_STATIC,13,34,152,12 + EDITTEXT IDC_GROUP,178,32,117,12,ES_AUTOHSCROLL + PUSHBUTTON "Logout",IDC_LOGOUT,253,56,50,14 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN + IDD_ACCOUNT_MANAGER, DIALOG + BEGIN + END + + IDD_OPTIONS_MAIN, DIALOG + BEGIN + END +END +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// AFX_DIALOG_LAYOUT +// + +IDD_OPTIONS_MAIN AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_ACCOUNT_MANAGER AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +#endif // English (Neutral) resources +///////////////////////////////////////////////////////////////////////////// + + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_DEVICECODE DIALOGEX 0, 0, 318, 182 +STYLE DS_SETFONT | DS_SETFOREGROUND | DS_3DLOOK | DS_FIXEDSYS | DS_CENTER | WS_CAPTION +EXSTYLE WS_EX_TOOLWINDOW | WS_EX_CLIENTEDGE | WS_EX_STATICEDGE | WS_EX_APPWINDOW +CAPTION "Teams" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "Proceed",IDOK,205,161,50,14 + PUSHBUTTON "Cancel",IDCANCEL,261,161,50,14 + LTEXT "Static",IDC_TEXT,7,7,304,147 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN + IDD_DEVICECODE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 311 + TOPMARGIN, 7 + BOTTOMMARGIN, 175 + END +END +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// AFX_DIALOG_LAYOUT +// + +IDD_DEVICECODE 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/Teams/res/teams.ico b/protocols/Teams/res/teams.ico Binary files differnew file mode 100644 index 0000000000..76e8549214 --- /dev/null +++ b/protocols/Teams/res/teams.ico diff --git a/protocols/Teams/res/version.rc b/protocols/Teams/res/version.rc new file mode 100644 index 0000000000..5a5ddd63ed --- /dev/null +++ b/protocols/Teams/res/version.rc @@ -0,0 +1,9 @@ +// Microsoft Visual C++ generated resource script. +// +#ifdef APSTUDIO_INVOKED +#error this file is not editable by Microsoft Visual C++ +#endif //APSTUDIO_INVOKED + +#include "..\src\version.h" + +#include "..\..\build\Version.rc" diff --git a/protocols/Teams/src/main.cpp b/protocols/Teams/src/main.cpp new file mode 100644 index 0000000000..07778b2405 --- /dev/null +++ b/protocols/Teams/src/main.cpp @@ -0,0 +1,80 @@ +/* +Copyright (C) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +CMPlugin g_plugin; + +char g_szMirVer[100]; +HANDLE g_hCallEvent; +HANDLE hExtraXStatus; + +///////////////////////////////////////////////////////////////////////////////////////// + +PLUGININFOEX pluginInfoEx = +{ + sizeof(PLUGININFOEX), + __PLUGIN_NAME, + PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), + __DESCRIPTION, + __AUTHOR, + __COPYRIGHT, + __AUTHORWEB, + UNICODE_AWARE, + // {DCD56CEC-C61B-4275-A010-8C65C5B48815} + { 0xDCD56CEC, 0xC61B, 0x4275, { 0xa0, 0x10, 0x8c, 0x65, 0xc5, 0x84, 0x88, 0x15 }} +}; + +CMPlugin::CMPlugin() : + ACCPROTOPLUGIN<CTeamsProto>("Teams", pluginInfoEx) +{ + SetUniqueId("id"); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST }; + +///////////////////////////////////////////////////////////////////////////////////////// + +static IconItem iconList[] = { + { LPGEN("Protocol icon"), "main", IDI_TEAMS }, + { LPGEN("Create new chat icon"), "conference", IDI_CONFERENCE }, + { LPGEN("Block user icon"), "user_block", IDI_BLOCKUSER }, + { LPGEN("Unblock user icon"), "user_unblock", IDI_UNBLOCKUSER }, + { LPGEN("Incoming call icon"), "inc_call", IDI_CALL }, + { LPGEN("Notification icon"), "notify", IDI_NOTIFY }, + { LPGEN("Error icon"), "error", IDI_ERRORICON }, + { LPGEN("Action icon"), "me_action", IDI_ACTION_ME } + +}; + +int CMPlugin::Load() +{ + registerIcon("Protocols/" MODULENAME, iconList, MODULENAME); + + g_hCallEvent = CreateHookableEvent(MODULENAME "/IncomingCall"); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +int CMPlugin::Unload() +{ + DestroyHookableEvent(g_hCallEvent); + return 0; +} diff --git a/protocols/Teams/src/resource.h b/protocols/Teams/src/resource.h new file mode 100644 index 0000000000..ea9044c6da --- /dev/null +++ b/protocols/Teams/src/resource.h @@ -0,0 +1,44 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by W:\miranda-ng\protocols\Teams\res\Resource.rc +// +#define IDI_TEAMS 100 +#define IDC_LOGIN 101 +#define IDD_ACCOUNT_MANAGER 103 +#define IDD_OPTIONS_MAIN 104 +#define IDD_MOOD 105 +#define IDC_GROUP 106 +#define IDD_DEVICECODE 107 +#define IDD_GC_CREATE 111 +#define IDD_GC_INVITE 112 +#define IDI_CONFERENCE 114 +#define IDI_BLOCKUSER 118 +#define IDI_UNBLOCKUSER 119 +#define IDI_CALL 120 +#define IDI_NOTIFY 121 +#define IDI_ERRORICON 122 +#define IDI_ACTION_ME 123 +#define IDC_TEXT 1001 +#define IDC_AUTOSYNC 1028 +#define IDC_LOCALTIME 1029 +#define IDC_CLIST 1030 +#define IDC_TITLE 1031 +#define IDC_CONTACT 1032 +#define IDC_USEHOST 1035 +#define IDC_BBCODES 1036 +#define IDC_MOOD_COMBO 1037 +#define IDC_CHANGEPASS 1038 +#define IDC_MOOD_EMOJI 1039 +#define IDC_MOOD_TEXT 1041 +#define IDC_LOGOUT 1042 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 126 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1044 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/protocols/Teams/src/stdafx.cxx b/protocols/Teams/src/stdafx.cxx new file mode 100644 index 0000000000..b64bcca703 --- /dev/null +++ b/protocols/Teams/src/stdafx.cxx @@ -0,0 +1,18 @@ +/* +Copyright (C) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h"
\ No newline at end of file diff --git a/protocols/Teams/src/stdafx.h b/protocols/Teams/src/stdafx.h new file mode 100644 index 0000000000..93478998e9 --- /dev/null +++ b/protocols/Teams/src/stdafx.h @@ -0,0 +1,91 @@ +#ifndef _COMMON_H_ +#define _COMMON_H_ + +#include <windows.h> + +#include <malloc.h> +#include <time.h> +#include <string> +#include <vector> +#include <regex> +#include <map> +#include <memory> +#include <functional> + +#include <newpluginapi.h> + +#include <m_protoint.h> +#include <m_protosvc.h> + +#include <m_database.h> +#include <m_langpack.h> +#include <m_clistint.h> +#include <m_options.h> +#include <m_netlib.h> +#include <m_popup.h> +#include <m_icolib.h> +#include <m_userinfo.h> +#include <m_timezones.h> +#include <m_contacts.h> +#include <m_message.h> +#include <m_avatars.h> +#include <m_skin.h> +#include <m_chat_int.h> +#include <m_genmenu.h> +#include <m_clc.h> +#include <m_json.h> +#include <m_gui.h> +#include <m_imgsrvc.h> +#include <m_xml.h> +#include <m_assocmgr.h> +#include <m_file.h> + +extern char g_szMirVer[]; +extern HANDLE g_hCallEvent; + +struct MessageId +{ + ULONGLONG id; + HANDLE handle; +}; + +#include "resource.h" +#include "version.h" +#include "teams_menus.h" +#include "teams_utils.h" + +#define MODULENAME "Teams" + +class CTeamsProto; + +///////////////////////////////////////////////////////////////////////////////////////// + +#define SKYPEWEB_CLIENTINFO_NAME "swx-skype.com" +#define SKYPEWEB_CLIENTINFO_VERSION "908/1.85.0.29" + +enum SkypeHost +{ + HOST_API, + HOST_CONTACTS, + HOST_LOGIN, + HOST_TEAMS, + HOST_TEAMS_API, + HOST_CHATS, + HOST_GROUPS, + HOST_PRESENCE, + HOST_OTHER +}; + +struct AsyncHttpRequest : public MTHttpRequest<CTeamsProto> +{ + SkypeHost m_host; + MCONTACT hContact = 0; + + AsyncHttpRequest(int type, SkypeHost host, LPCSTR url = nullptr, MTHttpRequestHandler pFunc = nullptr); + + void AddAuthentication(CTeamsProto *ppro); +}; + +#include "teams_proto.h" + +#endif //_COMMON_H_ diff --git a/protocols/Teams/src/teams_avatars.cpp b/protocols/Teams/src/teams_avatars.cpp new file mode 100644 index 0000000000..a535cab005 --- /dev/null +++ b/protocols/Teams/src/teams_avatars.cpp @@ -0,0 +1,218 @@ +/* +Copyright (c) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +void CTeamsProto::GetAvatarFileName(MCONTACT hContact, wchar_t *pszDest, size_t cbLen) +{ + CMStringW wszPath(GetAvatarPath()); + wszPath += '\\'; + + const wchar_t *szFileType = ProtoGetAvatarExtension(getByte(hContact, "AvatarType", PA_FORMAT_JPEG)); + CMStringA username(getId(hContact)); + username.Replace("live:", "__live_"); + username.Replace("facebook:", "__facebook_"); + wszPath.AppendFormat(L"%S%s", username.c_str(), szFileType); + + wcsncpy_s(pszDest, cbLen, wszPath, _TRUNCATE); +} + +void CTeamsProto::ReloadAvatarInfo(MCONTACT hContact) +{ + if (hContact == NULL) { + ReportSelfAvatarChanged(); + return; + } + + PROTO_AVATAR_INFORMATION ai = { 0 }; + ai.hContact = hContact; + SvcGetAvatarInfo(0, (LPARAM)&ai); +} + +void CTeamsProto::SetAvatarUrl(MCONTACT hContact, const CMStringW &tszUrl) +{ + ptrW oldUrl(getWStringA(hContact, "AvatarUrl")); + if (oldUrl != NULL) + if (tszUrl == oldUrl) + return; + + if (tszUrl.IsEmpty()) { + delSetting(hContact, "AvatarUrl"); + ProtoBroadcastAck(hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, nullptr); + } + else { + setWString(hContact, "AvatarUrl", tszUrl); + setByte(hContact, "NeedNewAvatar", 1); + + PROTO_AVATAR_INFORMATION ai = {}; + ai.hContact = hContact; + GetAvatarFileName(ai.hContact, ai.filename, _countof(ai.filename)); + ai.format = ProtoGetAvatarFormat(ai.filename); + ProtoBroadcastAck(hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, &ai); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Avatar services for Miranda + +INT_PTR CTeamsProto::SvcGetAvatarCaps(WPARAM wParam, LPARAM lParam) +{ + switch (wParam) { + case AF_MAXSIZE: + ((POINT*)lParam)->x = 98; + ((POINT*)lParam)->y = 98; + break; + + case AF_MAXFILESIZE: + return 32000; + + case AF_PROPORTION: + return PIP_SQUARE; + + case AF_FORMATSUPPORTED: + case AF_ENABLED: + case AF_DONTNEEDDELAYS: + case AF_FETCHIFPROTONOTVISIBLE: + case AF_FETCHIFCONTACTOFFLINE: + return 1; + } + return 0; +} + +INT_PTR CTeamsProto::SvcGetAvatarInfo(WPARAM, LPARAM lParam) +{ + PROTO_AVATAR_INFORMATION *pai = (PROTO_AVATAR_INFORMATION *)lParam; + + pai->format = getByte(pai->hContact, "AvatarType", PA_FORMAT_JPEG); + + wchar_t tszFileName[MAX_PATH]; + GetAvatarFileName(pai->hContact, tszFileName, _countof(tszFileName)); + wcsncpy(pai->filename, tszFileName, _countof(pai->filename)); + + if (::_waccess(pai->filename, 0) == 0 && !getBool(pai->hContact, "NeedNewAvatar", 0)) + return GAIR_SUCCESS; + + if (IsOnline()) + if (ReceiveAvatar(pai->hContact)) + return GAIR_WAITFOR; + + debugLogA("No avatar"); + return GAIR_NOAVATAR; +} + +INT_PTR CTeamsProto::SvcGetMyAvatar(WPARAM wParam, LPARAM lParam) +{ + wchar_t path[MAX_PATH]; + GetAvatarFileName(NULL, path, _countof(path)); + wcsncpy((wchar_t *)wParam, path, (int)lParam); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Avatars' receiving + +void CTeamsProto::OnReceiveAvatar(MHttpResponse *response, AsyncHttpRequest *pRequest) +{ + if (response == nullptr || response->body.IsEmpty()) + return; + + MCONTACT hContact = pRequest->hContact; + if (response->resultCode != 200) + return; + + PROTO_AVATAR_INFORMATION ai = { 0 }; + ai.format = ProtoGetBufferFormat(response->body); + setByte(hContact, "AvatarType", ai.format); + GetAvatarFileName(hContact, ai.filename, _countof(ai.filename)); + + FILE *out = _wfopen(ai.filename, L"wb"); + if (out == nullptr) { + ProtoBroadcastAck(hContact, ACKTYPE_AVATAR, ACKRESULT_FAILED, &ai, 0); + return; + } + + fwrite(response->body, 1, response->body.GetLength(), out); + fclose(out); + setByte(hContact, "NeedNewAvatar", 0); + ProtoBroadcastAck(hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, &ai, 0); +} + +bool CTeamsProto::ReceiveAvatar(MCONTACT hContact) +{ + ptrA szUrl(getStringA(hContact, "AvatarUrl")); + if (!mir_strlen(szUrl)) + return false; + + auto *pReq = new AsyncHttpRequest(REQUEST_GET, HOST_OTHER, szUrl, &CTeamsProto::OnReceiveAvatar); + pReq->hContact = hContact; + pReq->flags |= NLHRF_REDIRECT; + PushRequest(pReq); + + debugLogA("Requested to read an avatar from '%s'", szUrl.get()); + return true; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Setting my own avatar + +void CTeamsProto::OnSentAvatar(MHttpResponse *response, AsyncHttpRequest*) +{ + TeamsReply root(response); + if (root.error()) + return; +} + +INT_PTR CTeamsProto::SvcSetMyAvatar(WPARAM, LPARAM lParam) +{ + wchar_t *path = (wchar_t*)lParam; + wchar_t avatarPath[MAX_PATH]; + GetAvatarFileName(NULL, avatarPath, _countof(avatarPath)); + if (path != nullptr) { + if (CopyFile(path, avatarPath, FALSE)) { + FILE *hFile = _wfopen(path, L"rb"); + if (hFile) { + fseek(hFile, 0, SEEK_END); + size_t length = ftell(hFile); + if (length != -1) { + rewind(hFile); + + mir_ptr<uint8_t> data((uint8_t*)mir_alloc(length)); + + if (data != NULL && fread(data, sizeof(uint8_t), length, hFile) == length) { + const char *szMime = FreeImage_GetFIFMimeType(FreeImage_GetFIFFromFilenameU(path)); + + auto *pReq = new AsyncHttpRequest(REQUEST_PUT, HOST_API, 0, &CTeamsProto::OnSentAvatar); + pReq->m_szUrl.AppendFormat("/users/%s/profile/avatar", m_szSkypename.MakeLower().c_str()); + pReq->AddHeader("Content-Type", szMime); + pReq->m_szParam.Truncate((int)length); + memcpy(pReq->m_szParam.GetBuffer(), data, (int)length); + PushRequest(pReq); + + fclose(hFile); + return 0; + } + } + fclose(hFile); + } + } + return -1; + } + else if (IsFileExists(avatarPath)) + DeleteFile(avatarPath); + + return 0; +} diff --git a/protocols/Teams/src/teams_chatrooms.cpp b/protocols/Teams/src/teams_chatrooms.cpp new file mode 100644 index 0000000000..ca0621bed5 --- /dev/null +++ b/protocols/Teams/src/teams_chatrooms.cpp @@ -0,0 +1,665 @@ +/* +Copyright (c) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +void CTeamsProto::InitGroupChatModule() +{ + GCREGISTER gcr = {}; + gcr.dwFlags = GC_DATABASE | GC_PERSISTENT; + gcr.iMaxText = 0; + gcr.ptszDispName = m_tszUserName; + gcr.pszModule = m_szModuleName; + Chat_Register(&gcr); + + HookProtoEvent(ME_GC_EVENT, &CTeamsProto::OnGroupChatEventHook); + HookProtoEvent(ME_GC_BUILDMENU, &CTeamsProto::OnGroupChatMenuHook); + + CreateProtoService(PS_JOINCHAT, &CTeamsProto::OnJoinChatRoom); + CreateProtoService(PS_LEAVECHAT, &CTeamsProto::OnLeaveChatRoom); +} + +SESSION_INFO* CTeamsProto::StartChatRoom(const wchar_t *tid, const wchar_t *tname, const char *pszVersion) +{ + // Create the group chat session + SESSION_INFO *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, tid, tname); + if (!si) + return nullptr; + + bool bFetchInfo = si->arUsers.getCount() == 0; + if (pszVersion) { + CMStringA oldVersion(getMStringA(si->hContact, "Version")); + if (oldVersion != pszVersion) + bFetchInfo = true; + } + + if (bFetchInfo) { + // Create user statuses + Chat_AddGroup(si, TranslateT("Admin")); + Chat_AddGroup(si, TranslateT("User")); + + GetChatInfo(tid); + } + + // Finish initialization + Chat_Control(si, (getBool("HideChats", 1) ? WINDOW_HIDDEN : SESSION_INITDONE)); + Chat_Control(si, SESSION_ONLINE); + return si; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Group chat invitation dialog + +class CSkypeInviteDlg : public CTeamsDlgBase +{ + CCtrlCombo m_combo; + +public: + MCONTACT m_hContact = 0; + + CSkypeInviteDlg(CTeamsProto *proto) : + CTeamsDlgBase(proto, IDD_GC_INVITE), + m_combo(this, IDC_CONTACT) + {} + + bool OnInitDialog() override + { + for (auto &hContact : m_proto->AccContacts()) + if (!m_proto->isChatRoom(hContact)) + m_combo.AddString(Clist_GetContactDisplayName(hContact), hContact); + return true; + } + + bool OnApply() override + { + m_hContact = m_combo.GetCurData(); + return true; + } +}; + +int CTeamsProto::OnGroupChatEventHook(WPARAM, LPARAM lParam) +{ + GCHOOK *gch = (GCHOOK*)lParam; + if (!gch) + return 0; + + auto *si = gch->si; + if (mir_strcmp(si->pszModule, m_szModuleName) != 0) + return 0; + + T2Utf chat_id(si->ptszID), user_id(gch->ptszUID); + + switch (gch->iType) { + case GC_USER_MESSAGE: + SendChatMessage(si, gch->ptszText); + break; + + case GC_USER_PRIVMESS: + { + MCONTACT hContact = FindContact(user_id); + if (hContact == NULL) { + hContact = AddContact(user_id, T2Utf(gch->ptszNick), true); + setWord(hContact, "Status", ID_STATUS_ONLINE); + Contact::Hide(hContact); + } + CallService(MS_MSG_SENDMESSAGEW, hContact, 0); + } + break; + + case GC_USER_LOGMENU: + switch (gch->dwData) { + case 10: + { + CSkypeInviteDlg dlg(this); + if (dlg.DoModal()) + if (dlg.m_hContact != NULL) + InviteUserToChat(chat_id, getId(dlg.m_hContact), "User"); + } + break; + + case 20: + OnLeaveChatRoom(si->hContact, NULL); + break; + + case 30: + CMStringW newTopic = ChangeTopicForm(); + if (!newTopic.IsEmpty()) + SetChatProperty(chat_id, "topic", T2Utf(newTopic.GetBuffer())); + break; + } + break; + + case GC_USER_NICKLISTMENU: + switch (gch->dwData) { + case 10: + KickChatUser(chat_id, user_id); + break; + case 30: + InviteUserToChat(chat_id, user_id, "Admin"); + break; + case 40: + InviteUserToChat(chat_id, user_id, "User"); + break; + case 50: + ptrW tnick_old(GetChatContactNick(si, gch->ptszUID, gch->ptszText)); + + ENTER_STRING pForm = {}; + pForm.type = ESF_COMBO; + pForm.caption = TranslateT("Enter new nickname"); + pForm.szModuleName = m_szModuleName; + pForm.szDataPrefix = "renamenick_"; + + if (EnterString(&pForm)) { + if (si->hContact == NULL) + break; // This probably shouldn't happen, but if chat is NULL for some reason, do nothing + + ptrW tnick_new(pForm.ptszResult); + bool reset = mir_wstrlen(tnick_new) == 0; + if (reset) { + // User fill blank name, which means we reset the custom nick + db_unset(si->hContact, "UsersNicks", user_id); + tnick_new = GetChatContactNick(si, gch->ptszUID, gch->ptszText); + } + + if (!mir_wstrcmp(tnick_old, tnick_new)) + break; // New nick is same, do nothing + + GCEVENT gce = { si, GC_EVENT_NICK }; + gce.dwFlags = GCEF_ADDTOLOG; + gce.pszNick.w = tnick_old; + gce.bIsMe = IsMe(user_id); + gce.pszUID.w = gch->ptszUID; + gce.pszText.w= tnick_new; + gce.time = time(0); + Chat_Event(&gce); + + if (!reset) + db_set_ws(si->hContact, "UsersNicks", user_id, tnick_new); + } + break; + } + break; + } + return 1; +} + +INT_PTR CTeamsProto::OnJoinChatRoom(WPARAM hContact, LPARAM) +{ + if (hContact) { + ptrW idT(getWStringA(hContact, DBKEY_ID)); + ptrW nameT(getWStringA(hContact, "Nick")); + StartChatRoom(idT, nameT != NULL ? nameT : idT); + } + return 0; +} + +INT_PTR CTeamsProto::OnLeaveChatRoom(WPARAM hContact, LPARAM) +{ + if (!IsOnline()) + return 1; + + if (hContact && IDYES == MessageBox(nullptr, TranslateT("This chat is going to be destroyed forever with all its contents. This action cannot be undone. Are you sure?"), TranslateT("Warning"), MB_YESNO | MB_ICONQUESTION)) { + ptrW idT(getWStringA(hContact, DBKEY_ID)); + auto *si = Chat_Find(idT, m_szModuleName); + Chat_Control(si, SESSION_OFFLINE); + Chat_Terminate(si); + + db_delete_contact(hContact, CDF_DEL_CONTACT); + } + return 0; +} + +/* CHAT EVENT */ + +bool CTeamsProto::OnChatEvent(const JSONNode &node) +{ + CMStringW wszChatId(UrlToSkypeId(node["conversationLink"].as_mstring())); + CMStringW szFromId(UrlToSkypeId(node["from"].as_mstring())); + + CMStringW wszTopic(node["threadtopic"].as_mstring()); + CMStringW wszContent(node["content"].as_mstring()); + + SESSION_INFO *si = Chat_Find(wszChatId, m_szModuleName); + if (si == nullptr) { + si = StartChatRoom(wszChatId, wszTopic); + if (si == nullptr) { + debugLogW(L"unable to create chat %s", wszChatId.c_str()); + return true; + } + } + + std::string messageType = node["messagetype"].as_string(); + if (messageType == "ThreadActivity/AddMember") { + // <addmember><eventtime>1429186229164</eventtime><initiator>8:initiator</initiator><target>8:user</target></addmember> + TiXmlDocument doc; + if (!doc.Parse(T2Utf(wszContent))) { + if (auto *pRoot = doc.FirstChildElement("addmember")) { + auto *pszTarget = XmlGetChildText(pRoot, "target"); + if (!AddChatContact(si, Utf2T(pszTarget), L"User")) { + OBJLIST<char> arIds(1); + arIds.insert(newStr(pszTarget)); + GetChatMembers(arIds, si); + } + } + } + return true; + } + + if (messageType == "ThreadActivity/DeleteMember") { + // <deletemember><eventtime>1429186229164</eventtime><initiator>8:initiator</initiator><target>8:user</target></deletemember> + TiXmlDocument doc; + if (!doc.Parse(T2Utf(wszContent))) { + if (auto *pRoot = doc.FirstChildElement("deletemember")) { + CMStringW target = Utf2T(XmlGetChildText(pRoot, "target")); + CMStringW initiator = Utf2T(XmlGetChildText(pRoot, "initiator")); + RemoveChatContact(si, target, initiator); + } + } + return true; + } + + if (messageType == "ThreadActivity/TopicUpdate") { + // <topicupdate><eventtime>1429532702130</eventtime><initiator>8:user</initiator><value>test topic</value></topicupdate> + TiXmlDocument doc; + if (!doc.Parse(T2Utf(wszContent))) { + if (auto *pRoot = doc.FirstChildElement("topicupdate")) { + CMStringW initiator = Utf2T(XmlGetChildText(pRoot, "initiator")); + CMStringW value = Utf2T(XmlGetChildText(pRoot, "value")); + Chat_ChangeSessionName(si, value); + + GCEVENT gce = { si, GC_EVENT_TOPIC }; + gce.pszUID.w = initiator; + gce.pszNick.w = GetSkypeNick(initiator); + gce.pszText.w = wszTopic; + Chat_Event(&gce); + } + } + return true; + } + + if (messageType == "ThreadActivity/RoleUpdate") { + // <roleupdate><eventtime>1429551258363</eventtime><initiator>8:user</initiator><target><id>8:user1</id><role>admin</role></target></roleupdate> + TiXmlDocument doc; + if (!doc.Parse(T2Utf(wszContent))) { + if (auto *pRoot = doc.FirstChildElement("roleupdate")) { + CMStringW initiator = Utf2T(UrlToSkypeId(XmlGetChildText(pRoot, "initiator"))); + + auto *pTarget = pRoot->FirstChildElement("target"); + if (pTarget) { + CMStringW id = Utf2T(UrlToSkypeId(XmlGetChildText(pTarget, "id"))); + const char *role = XmlGetChildText(pTarget, "role"); + + GCEVENT gce = { si, !mir_strcmpi(role, "Admin") ? GC_EVENT_ADDSTATUS : GC_EVENT_REMOVESTATUS }; + gce.dwFlags = GCEF_ADDTOLOG; + gce.pszNick.w = id; + gce.pszUID.w = id; + gce.pszText.w = initiator; + gce.time = time(0); + gce.bIsMe = IsMe(T2Utf(id)); + gce.pszStatus.w = TranslateT("Admin"); + Chat_Event(&gce); + } + } + } + return true; + } + + // some slack, let's drop it + if (messageType == "ThreadActivity/HistoryDisclosedUpdate" || messageType == "ThreadActivity/JoiningEnabledUpdate") + return true; + + return false; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::SendChatMessage(SESSION_INFO *si, const wchar_t *tszMessage) +{ + if (!IsOnline()) + return; + + CMStringA szMessage(ptrA(mir_utf8encodeW(tszMessage))); + szMessage.TrimRight(); + bool bRich = AddBbcodes(szMessage); + + JSONNode node; + node << INT64_PARAM("clientmessageid", getRandomId()) << CHAR_PARAM("messagetype", bRich ? "RichText" : "Text") + << CHAR_PARAM("contenttype", "text") << CHAR_PARAM("content", szMessage); + if (strncmp(szMessage, "/me ", 4) == 0) + node << INT_PARAM("skypeemoteoffset", 4); + + CMStringA szUrl = "/users/ME/conversations/" + mir_urlEncode(T2Utf(si->ptszID)) + "/messages"; + AsyncHttpRequest *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_CHATS, szUrl, &CTeamsProto::OnMessageSent); + pReq->m_szParam = node.write().c_str(); + pReq->pUserInfo = new COwnMessage(szMessage); + pReq->hContact = si->hContact; + PushRequest(pReq); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::GetChatMembers(const LIST<char> &ids, SESSION_INFO *si) +{ + auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_CHATS, "/profiles", &CTeamsProto::OnGetChatMembers); + + JSONNode node, mris(JSON_ARRAY); mris.set_name("mris"); + for (auto &it : ids) + mris.push_back(JSONNode("", it)); + node << mris << CHAR_PARAM("locale", "en-US"); + pReq->m_szParam = node.write().c_str(); + + pReq->pUserInfo = si; + PushRequest(pReq); +}; + +void CTeamsProto::OnGetChatMembers(MHttpResponse *response, AsyncHttpRequest *pRequest) +{ + TeamsReply reply(response); + if (reply.error()) + return; + + auto &root = reply.data(); + auto *si = (SESSION_INFO *)pRequest->pUserInfo; + + for (auto &it : root["profiles"]) { + CMStringW wszUserId(Utf2T(it.name())); + if (auto *pUser = g_chatApi.UM_FindUser(si, wszUserId)) { + auto &pProfile = it["profile"]; + if (auto &pName = pProfile["displayName"]) + replaceStrW(pUser->pszNick, pName.as_mstring()); + } + } + + if (g_chatApi.OnChangeNick) + g_chatApi.OnChangeNick(si); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::OnGetChatInfo(MHttpResponse *response, AsyncHttpRequest*) +{ + TeamsReply reply(response); + if (reply.error()) + return; + + auto &root = reply.data(); + const JSONNode &properties = root["properties"]; + if (!properties["capabilities"] || properties["capabilities"].empty()) + return; + + CMStringW wszChatId(UrlToSkypeId(root["messages"].as_mstring())); + auto *si = Chat_Find(wszChatId, m_szModuleName); + if (si == nullptr) + return; + + setString(si->hContact, "Version", root["version"].as_string().c_str()); + + OBJLIST<char> arIds(1); + for (auto &member : root["members"]) { + CMStringW username(UrlToSkypeId(member["userLink"].as_mstring())); + CMStringW role = member["role"].as_mstring(); + if (!AddChatContact(si, username, role, true)) + arIds.insert(newStr(mir_u2a(username))); + } + + if (arIds.getCount()) + GetChatMembers(arIds, si); + + GetServerHistory(si->hContact, 100, 0, true); +} + +void CTeamsProto::GetChatInfo(const wchar_t *chatId) +{ + auto *pReq = new AsyncHttpRequest(REQUEST_GET, HOST_CHATS, 0, &CTeamsProto::OnGetChatInfo); + pReq->m_szUrl.AppendFormat("/threads/%S", chatId); + pReq << CHAR_PARAM("view", "msnp24Equivalent"); + PushRequest(pReq); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +wchar_t* CTeamsProto::GetChatContactNick(SESSION_INFO *si, const wchar_t *id, const wchar_t *name, bool *isQualified) +{ + if (isQualified) + *isQualified = true; + + // Check if there's a user with this id in a chat + if (auto *pUser = g_chatApi.UM_FindUser(si, id)) + return mir_wstrdup(pUser->pszNick); + + // Check if there is custom nick for this chat contact + if (auto *tname = db_get_wsa(si->hContact, "UsersNicks", T2Utf(id))) + return tname; + + // Check if we have this contact in database + if (IsMe(id)) { + // Return my nick + if (auto *tname = getWStringA("Nick")) + return tname; + } + else { + MCONTACT hContact = FindContact(id); + if (hContact != NULL) { + // Primarily return custom name + if (auto *tname = db_get_wsa(hContact, "CList", "MyHandle")) + return tname; + + // If not exists, then user nick + if (auto *tname = getWStringA(hContact, "Nick")) + return tname; + } + } + + if (isQualified) + *isQualified = false; + + // Return default value as nick - given name or user id + if (name != nullptr) + return mir_wstrdup(name); + return mir_wstrdup(GetSkypeNick(id)); +} + +void CTeamsProto::InviteUserToChat(const char *chatId, const char *skypename, const char *role) +{ + JSONNode node; + node << CHAR_PARAM("role", role); + + auto *pReq = new AsyncHttpRequest(REQUEST_PUT, HOST_CHATS); + pReq->m_szUrl.AppendFormat("/threads/%s/members/%s", chatId, skypename); + pReq->m_szParam = node.write().c_str(); + PushRequest(pReq); +} + +void CTeamsProto::SetChatProperty(const char *chatId, const char *propname, const char *value) +{ + JSONNode node; + node << CHAR_PARAM(propname, value); + + auto *pReq = new AsyncHttpRequest(REQUEST_PUT, HOST_CHATS); + pReq->m_szUrl.AppendFormat("/threads/%s/properties?name=%s", chatId, propname); + pReq->m_szParam = node.write().c_str(); + PushRequest(pReq); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +bool CTeamsProto::AddChatContact(SESSION_INFO *si, const wchar_t *id, const wchar_t *role, bool isChange) +{ + bool isQualified; + ptrW szNick(GetChatContactNick(si, id, 0, &isQualified)); + + GCEVENT gce = { si, GC_EVENT_JOIN }; + gce.dwFlags = GCEF_ADDTOLOG; + gce.pszNick.w = szNick; + gce.pszUID.w = id; + gce.time = !isChange ? time(0) : NULL; + gce.bIsMe = IsMe(id); + gce.pszStatus.w = TranslateW(role); + Chat_Event(&gce); + + return isQualified; +} + +void CTeamsProto::RemoveChatContact(SESSION_INFO *si, const wchar_t *id, const wchar_t *initiator) +{ + if (IsMe(id)) + return; + + ptrW szNick(GetChatContactNick(si, id)); + ptrW szInitiator(GetChatContactNick(si, initiator)); + + GCEVENT gce = { si, GC_EVENT_KICK }; + gce.dwFlags = GCEF_ADDTOLOG; + gce.pszNick.w = szNick; + gce.pszUID.w = id; + gce.time = time(0); + gce.bIsMe = IsMe(id); + gce.pszStatus.w = szInitiator; + Chat_Event(&gce); +} + +void CTeamsProto::KickChatUser(const char *chatId, const char *userId) +{ + PushRequest(new AsyncHttpRequest(REQUEST_DELETE, HOST_CHATS, "/threads/" + mir_urlEncode(chatId) + "/members/" + mir_urlEncode(userId))); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Group chat creation dialog + +class CSkypeGCCreateDlg : public CTeamsDlgBase +{ + CCtrlClc m_clc; + +public: + OBJLIST<char> m_ContactsList; + + CSkypeGCCreateDlg(CTeamsProto *proto) : + CTeamsDlgBase(proto, IDD_GC_CREATE), + m_clc(this, IDC_CLIST), + m_ContactsList(1) + { + m_clc.OnListRebuilt = Callback(this, &CSkypeGCCreateDlg::FilterList); + } + + ~CSkypeGCCreateDlg() + { + } + + bool OnInitDialog() override + { + SetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE, + GetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE) | CLS_CHECKBOXES | CLS_HIDEEMPTYGROUPS | CLS_USEGROUPS | CLS_GREYALTERNATE); + m_clc.SendMsg(CLM_SETEXSTYLE, CLS_EX_DISABLEDRAGDROP | CLS_EX_TRACKSELECT, 0); + + ResetListOptions(&m_clc); + return true; + } + + bool OnApply() override + { + for (auto &hContact : m_proto->AccContacts()) + if (!m_proto->isChatRoom(hContact)) + if (HANDLE hItem = m_clc.FindContact(hContact)) + if (m_clc.GetCheck(hItem)) + m_ContactsList.insert(newStr(m_proto->getId(hContact))); + + m_ContactsList.insert(newStr(m_proto->m_szSkypename)); + return true; + } + + void FilterList(CCtrlClc *) + { + for (auto &hContact : Contacts()) { + char *proto = Proto_GetBaseAccountName(hContact); + if (mir_strcmp(proto, m_proto->m_szModuleName) || m_proto->isChatRoom(hContact)) + if (HANDLE hItem = m_clc.FindContact(hContact)) + m_clc.DeleteItem(hItem); + } + } + + void ResetListOptions(CCtrlClc *) + { + m_clc.SetHideEmptyGroups(true); + m_clc.SetHideOfflineRoot(true); + } +}; + +INT_PTR CTeamsProto::SvcCreateChat(WPARAM, LPARAM) +{ + if (IsOnline()) { + CSkypeGCCreateDlg dlg(this); + if (dlg.DoModal()) { + JSONNode node; + JSONNode members(JSON_ARRAY); members.set_name("members"); + + for (auto &it : dlg.m_ContactsList) { + JSONNode member; + member << CHAR_PARAM("id", it) << CHAR_PARAM("role", !mir_strcmpi(it, m_szSkypename) ? "Admin" : "User"); + members << member; + } + node << members; + + auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_CHATS, "/threads"); + pReq->m_szParam = node.write().c_str(); + PushRequest(pReq); + return 0; + } + } + return 1; +} + +/* Menus */ + +int CTeamsProto::OnGroupChatMenuHook(WPARAM, LPARAM lParam) +{ + GCMENUITEMS *gcmi = (GCMENUITEMS*)lParam; + if (mir_strcmpi(gcmi->pszModule, m_szModuleName)) return 0; + + if (gcmi->Type == MENU_ON_LOG) { + static const struct gc_item Items[] = + { + { LPGENW("&Invite user..."), 10, MENU_ITEM, FALSE }, + { LPGENW("&Leave chat session"), 20, MENU_ITEM, FALSE }, + { LPGENW("&Change topic..."), 30, MENU_ITEM, FALSE } + }; + Chat_AddMenuItems(gcmi->hMenu, _countof(Items), Items, &g_plugin); + } + else if (gcmi->Type == MENU_ON_NICKLIST) { + static const struct gc_item Items[] = + { + { LPGENW("Kick &user"), 10, MENU_ITEM }, + { nullptr, 0, MENU_SEPARATOR }, + { LPGENW("Set &role"), 20, MENU_NEWPOPUP }, + { LPGENW("&Admin"), 30, MENU_POPUPITEM }, + { LPGENW("&User"), 40, MENU_POPUPITEM }, + { LPGENW("Change nick..."), 50, MENU_ITEM }, + }; + Chat_AddMenuItems(gcmi->hMenu, _countof(Items), Items, &g_plugin); + } + + return 0; +} + +CMStringW CTeamsProto::ChangeTopicForm() +{ + CMStringW caption(FORMAT, L"[%s] %s", _A2T(m_szModuleName).get(), TranslateT("Enter new chatroom topic")); + ENTER_STRING pForm = {}; + pForm.type = ESF_MULTILINE; + pForm.caption = caption; + pForm.szModuleName = m_szModuleName; + return (!EnterString(&pForm)) ? CMStringW() : CMStringW(ptrW(pForm.ptszResult)); +} diff --git a/protocols/Teams/src/teams_contacts.cpp b/protocols/Teams/src/teams_contacts.cpp new file mode 100644 index 0000000000..19c2817d6f --- /dev/null +++ b/protocols/Teams/src/teams_contacts.cpp @@ -0,0 +1,321 @@ +/* +Copyright (c) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +void CTeamsProto::SetContactStatus(MCONTACT hContact, uint16_t status) +{ + uint16_t oldStatus = getWord(hContact, "Status", ID_STATUS_OFFLINE); + if (oldStatus != status) { + setWord(hContact, "Status", status); + if (status == ID_STATUS_OFFLINE) + delSetting(hContact, "MirVer"); + } +} + +void CTeamsProto::SetChatStatus(MCONTACT hContact, int iStatus) +{ + ptrW tszChatID(getWStringA(hContact, DBKEY_ID)); + if (tszChatID != NULL) + Chat_Control(Chat_Find(tszChatID, m_szModuleName), (iStatus == ID_STATUS_OFFLINE) ? SESSION_OFFLINE : SESSION_ONLINE); +} + +MCONTACT CTeamsProto::GetContactFromAuthEvent(MEVENT hEvent) +{ + uint32_t body[3]; + DBEVENTINFO dbei = {}; + dbei.cbBlob = sizeof(uint32_t) * 2; + dbei.pBlob = (char *)&body; + + if (db_event_get(hEvent, &dbei)) + return INVALID_CONTACT_ID; + + if (dbei.eventType != EVENTTYPE_AUTHREQUEST) + return INVALID_CONTACT_ID; + + if (mir_strcmp(dbei.szModule, m_szModuleName) != 0) + return INVALID_CONTACT_ID; + return DbGetAuthEventContact(&dbei); +} + +MCONTACT CTeamsProto::FindContact(const char *skypeId) +{ + for (auto &hContact : AccContacts()) + if (!mir_strcmpi(skypeId, ptrA(getUStringA(hContact, DBKEY_ID)))) + return hContact; + + return 0; +} + +MCONTACT CTeamsProto::FindContact(const wchar_t *skypeId) +{ + for (auto &hContact : AccContacts()) + if (!mir_wstrcmpi(skypeId, getMStringW(hContact, DBKEY_ID))) + return hContact; + + return 0; +} + +MCONTACT CTeamsProto::AddContact(const char *skypeId, const char *nick, bool isTemporary) +{ + MCONTACT hContact = FindContact(skypeId); + if (hContact) + return hContact; + + hContact = db_add_contact(); + Proto_AddToContact(hContact, m_szModuleName); + + setString(hContact, DBKEY_ID, skypeId); + setUString(hContact, "Nick", (nick) ? nick : GetSkypeNick(skypeId)); + + if (m_wstrCListGroup) { + Clist_GroupCreate(0, m_wstrCListGroup); + Clist_SetGroup(hContact, m_wstrCListGroup); + } + + setByte(hContact, "Auth", 1); + setByte(hContact, "Grant", 1); + + if (isTemporary) + Contact::RemoveFromList(hContact); + return hContact; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::LoadContactsAuth(MHttpResponse *response, AsyncHttpRequest*) +{ + TeamsReply reply(response); + if (reply.error()) + return; + + auto &root = reply.data(); + for (auto &item : root["invite_list"]) { + std::string skypeId = item["mri"].as_string(); + std::string reason = item["greeting"].as_string(); + + time_t eventTime = 0; + for (auto &it : item["invites"]) + eventTime = Utils_IsoToUnixTime(it["time"].as_string()); + + std::string displayName = item["displayname"].as_string(); + const char *szNick = (displayName.empty()) ? nullptr : displayName.c_str(); + + MCONTACT hContact = AddContact(skypeId.c_str(), szNick); + time_t lastEventTime = getDword(hContact, "LastAuthRequestTime"); + if (lastEventTime && lastEventTime >= eventTime) + continue; + + setUString(hContact, "Nick", displayName.c_str()); + + setDword(hContact, "LastAuthRequestTime", eventTime); + delSetting(hContact, "Auth"); + + DB::AUTH_BLOB blob(hContact, displayName.c_str(), nullptr, nullptr, skypeId.c_str(), reason.c_str()); + + DB::EventInfo dbei; + dbei.iTimestamp = time(0); + dbei.cbBlob = blob.size(); + dbei.pBlob = blob; + ProtoChainRecv(hContact, PSR_AUTH, 0, (LPARAM)&dbei); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::RefreshContactsInfo() +{ + PushRequest(new AsyncHttpRequest(REQUEST_GET, HOST_CONTACTS, "/users/SELF/contacts", &CTeamsProto::OnGotContactsInfo)); +} + +void CTeamsProto::OnGotContactsInfo(MHttpResponse *response, AsyncHttpRequest*) +{ + TeamsReply reply(response); + if (reply.error()) + return; + + auto &root = reply.data(); + for (auto &item : root["contacts"]) { + CMStringA szSkypeId = item["person_id"].as_mstring(); + if (!IsPossibleUserType(szSkypeId)) + continue; + + MCONTACT hContact = AddContact(szSkypeId, nullptr); + if (szSkypeId == "28:e7a9407c-2467-4a04-9546-70081f4ea80d") + m_hMyContact = hContact; + + std::string displayName = item["display_name"].as_string(); + if (!displayName.empty()) { + if (m_hMyContact == hContact) { + displayName = getMStringU("Nick"); + displayName += " "; + displayName += TranslateU("(You)"); + + setWord(hContact, "Status", ID_STATUS_ONLINE); + } + setUString(hContact, "Nick", displayName.c_str()); + } + + if (item["authorized"].as_bool()) { + delSetting(hContact, "Auth"); + delSetting(hContact, "Grant"); + } + else setByte(hContact, "Grant", 1); + + if (item["blocked"].as_bool()) + setByte(hContact, "IsBlocked", 1); + else + delSetting(hContact, "IsBlocked"); + + ptrW wszGroup(Clist_GetGroup(hContact)); + if (wszGroup == nullptr && m_wstrCListGroup) { + Clist_GroupCreate(0, m_wstrCListGroup); + Clist_SetGroup(hContact, m_wstrCListGroup); + } + + auto &profile = item["profile"]; + SetString(hContact, "Homepage", profile["website"]); + + auto wstr = profile["birthday"].as_mstring(); + if (!wstr.IsEmpty() ) { + int nYear, nMonth, nDay; + if (swscanf(wstr, L"%d-%d-%d", &nYear, &nMonth, &nDay) == 3) + Contact::SetBirthday(hContact, nDay, nMonth, nYear); + } + + wstr = profile["gender"].as_mstring(); + if (wstr == "male") + setByte(hContact, "Gender", 'M'); + else if (wstr == "female") + setByte(hContact, "Gender", 'F'); + + auto &name = profile["name"]; + SetString(hContact, "FirstName", name["first"]); + SetString(hContact, "LastName", name["surname"]); + + if (auto &pMood = profile["mood"]) + db_set_ws(hContact, "CList", "StatusMsg", RemoveHtml(pMood.as_mstring())); + + SetAvatarUrl(hContact, profile["avatar_url"].as_mstring()); + ReloadAvatarInfo(hContact); + + for (auto &phone : profile["phones"]) { + CMStringW number = phone["number"].as_mstring(); + + auto wszType = phone["type"].as_mstring(); + if (wszType == L"mobile") + setWString(hContact, "Cellular", number); + else if (wszType == L"phone") + setWString(hContact, "Phone", number); + } + } + + PushRequest(new AsyncHttpRequest(REQUEST_GET, HOST_CONTACTS, "/users/SELF/invites", &CTeamsProto::LoadContactsAuth)); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::GetShortInfo(const OBJLIST<char> &ids) +{ + auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_TEAMS_API, "/users/fetchShortProfile?isMailAddress=false&canBeSmtpAddress=false&enableGuest=true&includeIBBarredUsers=true&skypeTeamsInfo=true&includeBots=true"); + + for (auto &it : ids) { + if (pReq->m_szParam.IsEmpty()) + pReq->m_szParam = "["; + else + pReq->m_szParam += ","; + pReq->m_szParam.AppendFormat("\"%s\"", it); + } + pReq->m_szParam += "]"; + + PushRequest(pReq); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR CTeamsProto::OnRequestAuth(WPARAM hContact, LPARAM) +{ + return AuthRequest(hContact, 0); +} + +INT_PTR CTeamsProto::OnGrantAuth(WPARAM hContact, LPARAM) +{ + if (hContact == INVALID_CONTACT_ID) + return 1; + + PushRequest(new AsyncHttpRequest(REQUEST_POST, HOST_CONTACTS, "/users/SELF/invites/" + mir_urlEncode(getId(hContact)) + "/accept")); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +bool CTeamsProto::OnContactDeleted(MCONTACT hContact, uint32_t flags) +{ + if (IsOnline() && hContact && (flags & CDF_DEL_CONTACT)) { + CMStringA szId(getId(hContact)); + AsyncHttpRequest *pReq = (isChatRoom(hContact)) + ? new AsyncHttpRequest(REQUEST_DELETE, HOST_GROUPS, "/threads/" + mir_urlEncode(szId)) + : new AsyncHttpRequest(REQUEST_DELETE, HOST_CONTACTS, "/users/SELF/contacts/" + mir_urlEncode(szId)); + PushRequest(pReq); + } + return true; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::OnBlockContact(MHttpResponse *response, AsyncHttpRequest *pRequest) +{ + MCONTACT hContact = (DWORD_PTR)pRequest->pUserInfo; + if (response != nullptr) + Contact::Hide(hContact); +} + +INT_PTR CTeamsProto::BlockContact(WPARAM hContact, LPARAM) +{ + if (!IsOnline()) return 1; + + if (IDYES == MessageBox(NULL, TranslateT("Are you sure?"), TranslateT("Warning"), MB_YESNO | MB_ICONQUESTION)) { + auto *pReq = new AsyncHttpRequest(REQUEST_PUT, HOST_CONTACTS, "/users/SELF/contacts/blocklist/" + mir_urlEncode(getId(hContact)), &CTeamsProto::OnBlockContact); + pReq->m_szParam = "{\"report_abuse\":\"false\",\"ui_version\":\"skype.com\"}"; + pReq->pUserInfo = (void *)hContact; + PushRequest(pReq); + } + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::OnUnblockContact(MHttpResponse *response, AsyncHttpRequest *pRequest) +{ + if (response == nullptr) + return; + + MCONTACT hContact = (DWORD_PTR)pRequest->pUserInfo; + Contact::Hide(hContact, false); + delSetting(hContact, "IsBlocked"); +} + +INT_PTR CTeamsProto::UnblockContact(WPARAM hContact, LPARAM) +{ + if (!IsOnline()) return 1; + + auto *pReq = new AsyncHttpRequest(REQUEST_DELETE, HOST_CONTACTS, "/users/SELF/contacts/blocklist/" + mir_urlEncode(getId(hContact)), &CTeamsProto::OnUnblockContact); + pReq->pUserInfo = (void *)hContact; + pReq << CHAR_PARAM("reporterIp", "123.123.123.123") << CHAR_PARAM("uiVersion", g_szMirVer); // TODO: user ip address + PushRequest(pReq); + return 0; +} diff --git a/protocols/Teams/src/teams_files.cpp b/protocols/Teams/src/teams_files.cpp new file mode 100644 index 0000000000..bab5d72c6d --- /dev/null +++ b/protocols/Teams/src/teams_files.cpp @@ -0,0 +1,314 @@ +/* +Copyright (c) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +//////////////////////////////////////////////////////////////////////////////////////// +// File receiving + +static void __cdecl DownloadCallack(size_t iProgress, void *pParam) +{ + auto *ofd = (OFDTHREAD *)pParam; + + DBVARIANT dbv = { DBVT_DWORD }; + dbv.dVal = unsigned(iProgress); + db_event_setJson(ofd->hDbEvent, "ft", &dbv); +} + +void CTeamsProto::ReceiveFileThread(void *param) +{ + auto *ofd = (OFDTHREAD *)param; + + DB::EventInfo dbei(ofd->hDbEvent); + if (IsOnline() && dbei && !strcmp(dbei.szModule, m_szModuleName) && dbei.eventType == EVENTTYPE_FILE) { + DB::FILE_BLOB blob(dbei); + + if (ofd->bCopy) { + ofd->wszPath = Utf2T(blob.getUrl()).get(); + ofd->pCallback->Invoke(*ofd); + } + else { + CMStringA szCookie, szUrl; + szCookie.AppendFormat("skypetoken_asm=%s", m_szSkypeToken.c_str()); + + auto &json = dbei.getJson(); + auto skft = json["skft"].as_string(); + { + const char *preview; + if (skft == "Picture.1") + preview = "imgpsh_mobile_save_anim"; + else if (skft == "Video.1") + preview = "video"; + else + preview = "original"; + + MHttpRequest nlhr(REQUEST_GET); + nlhr.flags = NLHRF_HTTP11 | NLHRF_NOUSERAGENT; + nlhr.m_szUrl = blob.getUrl(); + nlhr.m_szUrl.AppendFormat("/views/%s/status", preview); + nlhr.AddHeader("Accept", "*/*"); + nlhr.AddHeader("Accept-Encoding", "gzip, deflate"); + nlhr.AddHeader("Cookie", szCookie); + NLHR_PTR response(Netlib_HttpTransaction(m_hNetlibUser, &nlhr)); + if (response) { + TeamsReply reply(response); + if (!reply.error()) { + auto &root = reply.data(); + if (root["content_state"].as_string() == "ready") + szUrl = root["view_location"].as_string().c_str(); + } + } + } + + if (!szUrl.IsEmpty()) { + MHttpRequest nlhr(REQUEST_GET); + nlhr.flags = NLHRF_HTTP11 | NLHRF_NOUSERAGENT; + nlhr.m_szUrl = blob.getUrl(); + if (skft == "Picture.1") + nlhr.m_szUrl += "/views/imgpsh_fullsize_anim"; + else if (skft == "Video.1") + nlhr.m_szUrl += "/views/video"; + else + nlhr.m_szUrl += "/views/original"; + + nlhr.AddHeader("Accept", "*/*"); + nlhr.AddHeader("Accept-Encoding", "gzip, deflate"); + nlhr.AddHeader("Cookie", szCookie); + + NLHR_PTR reply(Netlib_DownloadFile(m_hNetlibUser, &nlhr, ofd->wszPath, DownloadCallack, ofd)); + if (reply && reply->resultCode == 200) { + struct _stat st; + _wstat(ofd->wszPath, &st); + + DBVARIANT dbv = { DBVT_DWORD }; + dbv.dVal = st.st_size; + db_event_setJson(ofd->hDbEvent, "ft", &dbv); + + ofd->Finish(); + } + } + } + } + + delete ofd; +} + +INT_PTR CTeamsProto::SvcOfflineFile(WPARAM param, LPARAM) +{ + ForkThread(&CTeamsProto::ReceiveFileThread, (void *)param); + return 0; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// File sending + +#define FILETRANSFER_FAILED(fup) { ProtoBroadcastAck(fup->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, (HANDLE)fup); delete fup; fup = nullptr;} + +void CTeamsProto::SendFile(CFileUploadParam *fup) +{ + auto *pwszFileName = &fup->arFileName[0]; + if (!IsOnline() || _waccess(pwszFileName, 0)) { + FILETRANSFER_FAILED(fup); + return; + } + + if (auto *pBitmap = FreeImage_LoadU(FreeImage_GetFIFFromFilenameU(pwszFileName), pwszFileName)) { + fup->isPicture = true; + fup->width = FreeImage_GetWidth(pBitmap); + fup->height = FreeImage_GetHeight(pBitmap); + FreeImage_Unload(pBitmap); + } + else fup->isPicture = false; + + ProtoBroadcastAck(fup->hContact, ACKTYPE_FILE, ACKRESULT_CONNECTING, (HANDLE)fup); + + // create upload slot + auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_OTHER, "https://api.asm.skype.com/v1/objects", &CTeamsProto::OnASMObjectCreated); + pReq->flags &= (~NLHRF_DUMPASTEXT); + pReq->pUserInfo = fup; + + pReq->AddHeader("Authorization", CMStringA(FORMAT, "skype_token %s", m_szSkypeToken.c_str())); + pReq->AddHeader("Content-Type", "application/json"); + pReq->AddHeader("X-Client-Version", "0/0.0.0.0"); + + CMStringA szContact(getId(fup->hContact)); + T2Utf uszFileName(&fup->arFileName[0]); + const char *szFileName = strrchr(uszFileName.get() + 1, '\\'); + + JSONNode node; + if (fup->isPicture) + node << CHAR_PARAM("type", "pish/image"); + else + node << CHAR_PARAM("type", "sharing/file"); + + JSONNode jPermission(JSON_ARRAY); jPermission.set_name(szContact.c_str()); jPermission << CHAR_PARAM("", "read"); + JSONNode jPermissions; jPermissions.set_name("permissions"); jPermissions << jPermission; + node << CHAR_PARAM("filename", szFileName) << jPermissions; + pReq->m_szParam = node.write().c_str(); + PushRequest(pReq); +} + +void CTeamsProto::OnASMObjectCreated(MHttpResponse *response, AsyncHttpRequest *pRequest) +{ + auto *fup = (CFileUploadParam*)pRequest->pUserInfo; + if (response == nullptr || response->body.IsEmpty()) { +LBL_Error: + FILETRANSFER_FAILED(fup); + return; + } + + if (response->resultCode != 200 && response->resultCode != 201) { + debugLogA("Object creation failed with error code %d", response->resultCode); + goto LBL_Error; + } + + JSONNode node = JSONNode::parse(response->body); + std::string strObjectId = node["id"].as_string(); + if (strObjectId.empty()) { + debugLogA("Invalid server response (empty object id)"); + goto LBL_Error; + } + + fup->uid = mir_strdup(strObjectId.c_str()); + FILE *pFile = _wfopen(&fup->arFileName[0], L"rb"); + if (pFile == nullptr) + goto LBL_Error; + + fseek(pFile, 0, SEEK_END); + long lFileLen = ftell(pFile); + if (lFileLen < 1) { + fclose(pFile); + goto LBL_Error; + } + + fseek(pFile, 0, SEEK_SET); + + mir_ptr<uint8_t> pData((uint8_t*)mir_alloc(lFileLen)); + long lBytes = (long)fread(pData, sizeof(uint8_t), lFileLen, pFile); + fclose(pFile); + + if (lBytes != lFileLen) + goto LBL_Error; + + fup->size = lBytes; + ProtoBroadcastAck(fup->hContact, ACKTYPE_FILE, ACKRESULT_INITIALISING, (HANDLE)fup); + + // upload file to the previously created slot + auto *pReq = new AsyncHttpRequest(REQUEST_PUT, HOST_OTHER, 0, &CTeamsProto::OnASMObjectUploaded); + pReq->m_szUrl.Format("https://api.asm.skype.com/v1/objects/%s/content/%s", + strObjectId.c_str(), fup->isPicture ? "imgpsh" : "original"); + pReq->pUserInfo = fup; + + pReq->AddHeader("Authorization", CMStringA(FORMAT, "skype_token %s", m_szSkypeToken.c_str())); + pReq->AddHeader("Content-Type", fup->isPicture ? "application" : "application/octet-stream"); + + pReq->m_szParam.Truncate(lBytes); + memcpy(pReq->m_szParam.GetBuffer(), pData, lBytes); + PushRequest(pReq); +} + +void CTeamsProto::OnASMObjectUploaded(MHttpResponse *response, AsyncHttpRequest *pRequest) +{ + auto *fup = (CFileUploadParam *)pRequest->pUserInfo; + if (response == nullptr) { + FILETRANSFER_FAILED(fup); + return; + } + + wchar_t *tszFile = wcsrchr(&fup->arFileName[0], L'\\') + 1; + + TiXmlDocument doc; + auto *pRoot = doc.NewElement("URIObject"); + doc.InsertEndChild(pRoot); + + pRoot->SetAttribute("doc_id", fup->uid.get()); + pRoot->SetAttribute("uri", CMStringA(FORMAT, "https://api.asm.skype.com/v1/objects/%s", fup->uid.get())); + + // is that a picture? + CMStringA href; + if (fup->isPicture) { + pRoot->SetAttribute("type", "Picture.1"); + pRoot->SetAttribute("url_thumbnail", CMStringA(FORMAT, "https://api.asm.skype.com/v1/objects/%s/views/imgt1_anim", fup->uid.get())); + pRoot->SetAttribute("width", fup->width); + pRoot->SetAttribute("height", fup->height); + pRoot->SetText("To view this shared photo, go to:"); + + href.Format("https://login.skype.com/login/sso?go=xmmfallback?pic=%s", fup->uid.get()); + } + else { + pRoot->SetAttribute("type", "File.1"); + pRoot->SetAttribute("url_thumbnail", CMStringA(FORMAT, "https://api.asm.skype.com/v1/objects/%s/views/original", fup->uid.get())); + pRoot->SetText("To view this file, go to:"); + + href.Format("https://login.skype.com/login/sso?go=webclient.xmm&docid=%s", fup->uid.get()); + } + + auto *xmlA = doc.NewElement("a"); xmlA->SetText(href); + xmlA->SetAttribute("href", href); + pRoot->InsertEndChild(xmlA); + + auto *xmlOrigName = doc.NewElement("OriginalName"); xmlOrigName->SetAttribute("v", tszFile); pRoot->InsertEndChild(xmlOrigName); + auto *xmlSize = doc.NewElement("FileSize"); xmlSize->SetAttribute("v", (int)fup->size); pRoot->InsertEndChild(xmlSize); + + if (fup->isPicture) { + auto xmlMeta = doc.NewElement("meta"); + xmlMeta->SetAttribute("type", "photo"); xmlMeta->SetAttribute("originalName", tszFile); + pRoot->InsertEndChild(xmlMeta); + } + + tinyxml2::XMLPrinter printer(0, true); + doc.Print(&printer); + + // create a new file transfer event using previously filled slot + auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_CHATS); + pReq->m_szUrl.AppendFormat("/users/ME/conversations/%s/messages", mir_urlEncode(getId(fup->hContact)).c_str()); + pReq->hContact = fup->hContact; + + JSONNode ref(JSON_ARRAY); ref.set_name("amsreferences"); ref << CHAR_PARAM("", fup->uid); + + JSONNode node; + if (fup->isPicture) + node << CHAR_PARAM("messagetype", "RichText/UriObject"); + else + node << CHAR_PARAM("messagetype", "RichText/Media_GenericFile"); + + node << INT64_PARAM("clientmessageid", getRandomId()) << CHAR_PARAM("contenttype", "text") << CHAR_PARAM("content", printer.CStr()) << ref; + pReq->m_szParam = node.write().c_str(); + + PushRequest(pReq); + + // if that's last file in the queue, finish file transfer, or proceed with the next file + if (fup->arFileName.getCount() == 1) { + ProtoBroadcastAck(fup->hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, (HANDLE)fup); + delete fup; + } + else { + fup->arFileName.remove(int(0)); + ProtoBroadcastAck(fup->hContact, ACKTYPE_FILE, ACKRESULT_NEXTFILE, (HANDLE)fup); + SendFile(fup); + } +} + +HANDLE CTeamsProto::SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) +{ + if (!IsOnline()) + return INVALID_HANDLE_VALUE; + + CFileUploadParam *fup = new CFileUploadParam(hContact, ppszFiles, szDescription); + SendFile(fup); + return fup; +} diff --git a/protocols/Teams/src/teams_history.cpp b/protocols/Teams/src/teams_history.cpp new file mode 100644 index 0000000000..4630b33aa3 --- /dev/null +++ b/protocols/Teams/src/teams_history.cpp @@ -0,0 +1,195 @@ +/* +Copyright (c) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +/* HISTORY SYNC */ + +void CTeamsProto::FetchMissingHistory(const JSONNode &node, MCONTACT hContact) +{ + const JSONNode &lastMessage = node["lastMessage"]; + if (lastMessage && hContact) { + int64_t id = _atoi64(lastMessage["id"].as_string().c_str()); + auto lastMsgTime = getLastTime(hContact); + if (lastMsgTime && lastMsgTime < id && m_bAutoHistorySync) + GetServerHistory(hContact, 100, lastMsgTime, true); + } +} + +void CTeamsProto::OnSyncConversations(MHttpResponse *response, AsyncHttpRequest *) +{ + TeamsReply reply(response); + if (reply.error()) + return; + + auto &root = reply.data(); + const JSONNode &conversations = root["conversations"].as_array(); + + for (auto &it : conversations) { + CMStringA szSkypename = it["id"].as_mstring(); + int iUserType = atoi(szSkypename); + MCONTACT hContact = FindContact(szSkypename); + + switch (iUserType) { + case 19: + { + auto &props = it["threadProperties"]; + CMStringA szType = props["productThreadType"].as_mstring(), szChatType; + + int idx = szSkypename.ReverseFind('@'); + if (idx != -1) + szChatType = szSkypename.Mid(idx + 1); + + if (szType == "Chat" || szChatType == "thread.skype") { + auto *si = StartChatRoom(it["id"].as_mstring(), props["topic"].as_mstring(), props["version"].as_string().c_str()); + for (auto &cc : props["members"]) + AddChatContact(si, cc.as_mstring(), L"Admin"); + } + else if (szType == "OneToOneChat") { + hContact = FindContact(it["properties"]["addedBy"].as_string().c_str()); + if (hContact) + setString(hContact, "ChatId", szSkypename); + } + } + FetchMissingHistory(it, hContact); + break; + + case 8: + case 2: + CMStringA szChatId(it["properties"]["onetoonethreadid"].as_mstring()); + if (!szChatId.IsEmpty() && hContact) { + if (szChatId.Left(3) != "19:") + szChatId.Insert(0, "19:"); + setString(hContact, "ChatId", szChatId); + } + + FetchMissingHistory(it, hContact); + } + } + + m_bHistorySynced = true; +} + +void CTeamsProto::RefreshConversations() +{ + auto *pReq = new AsyncHttpRequest(REQUEST_GET, HOST_CHATS, "/users/ME/conversations", &CTeamsProto::OnSyncConversations); + pReq << INT_PARAM("startTime", 0) << INT_PARAM("pageSize", 100) + << CHAR_PARAM("view", "msnp24Equivalent") << CHAR_PARAM("targetType", "Passport|Skype|Lync|Thread|PSTN|Agent"); + + PushRequest(pReq); +} + +////////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::GetServerHistory(MCONTACT hContact, int pageSize, int64_t timestamp, bool bOperative) +{ + CMStringA szChatId(getMStringA(hContact, "ChatId")); + if (szChatId.IsEmpty()) + szChatId = getId(hContact); + + auto *pReq = new AsyncHttpRequest(REQUEST_GET, HOST_CHATS, "/users/ME/conversations/" + mir_urlEncode(szChatId) + "/messages", &CTeamsProto::OnGetServerHistory); + pReq->hContact = hContact; + if (bOperative) + pReq->pUserInfo = this; + + pReq << INT64_PARAM("startTime", timestamp) << INT_PARAM("pageSize", pageSize) + << CHAR_PARAM("view", "msnp24Equivalent") << CHAR_PARAM("targetType", "Passport|Skype|Lync|Thread"); + + PushRequest(pReq); +} + +void CTeamsProto::OnGetServerHistory(MHttpResponse *response, AsyncHttpRequest *pRequest) +{ + TeamsReply reply(response); + if (reply.error()) + return; + + auto &root = reply.data(); + const JSONNode &metadata = root["_metadata"]; + + int totalCount = metadata["totalCount"].as_int(); + std::string syncState = metadata["syncState"].as_string(); + + bool bOperative = pRequest->pUserInfo != 0; + bool bSetLastTime = false; + + int64_t lastMsgTime = 0; // max timestamp on this page + + auto &conv = root["messages"]; + for (auto it = conv.rbegin(); it != conv.rend(); ++it) { + auto &message = *it; + CMStringA szId = message["id"].as_mstring(); + int64_t id = _atoi64(szId); + if (id > lastMsgTime) { + bSetLastTime = true; + lastMsgTime = id; + } + + int iUserType; + CMStringA szMessageId(getMessageId(message)); + CMStringA szChatId = UrlToSkypeId(message["conversationLink"].as_mstring(), &iUserType); + CMStringA szFrom = UrlToSkypeId(message["from"].as_mstring()); + + DB::EventInfo dbei(db_event_getById(m_szModuleName, szMessageId)); + dbei.hContact = pRequest->hContact; + dbei.szModule = m_szModuleName; + dbei.szId = szMessageId; + dbei.bSent = IsMe(szFrom); + dbei.bMsec = dbei.bUtf = true; + dbei.iTimestamp = _wtoi64(message["id"].as_mstring()); + + if (iUserType == 19) { + dbei.szUserId = szFrom; + + CMStringA szType(message["messagetype"].as_mstring()); + if (szType.Left(15) == "ThreadActivity/") + continue; + } + + if (!bOperative && !dbei.getEvent()) + dbei.bRead = true; + + if (ParseMessage(message, dbei)) { + if (dbei) + db_event_edit(dbei.getEvent(), &dbei, true); + else + db_event_add(pRequest->hContact, &dbei); + } + } + + if (bSetLastTime && lastMsgTime > getLastTime(pRequest->hContact)) + setLastTime(pRequest->hContact, lastMsgTime); + + if (totalCount >= 99 || conv.size() >= 99) + GetServerHistory(pRequest->hContact, 100, lastMsgTime, pRequest->pUserInfo != 0); +} + +INT_PTR CTeamsProto::SvcLoadHistory(WPARAM hContact, LPARAM) +{ + GetServerHistory(hContact, 100, 0, false); + return 0; +} + +////////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR CTeamsProto::SvcEmptyHistory(WPARAM hContact, LPARAM flags) +{ + if (flags & CDF_DEL_HISTORY) + PushRequest(new AsyncHttpRequest(REQUEST_DELETE, HOST_CHATS, "/users/ME/conversations/" + mir_urlEncode(getId(hContact)) + "/messages")); + + return 0; +} diff --git a/protocols/Teams/src/teams_http.cpp b/protocols/Teams/src/teams_http.cpp new file mode 100644 index 0000000000..7ac6d3be2d --- /dev/null +++ b/protocols/Teams/src/teams_http.cpp @@ -0,0 +1,198 @@ +/* +Copyright (c) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +AsyncHttpRequest::AsyncHttpRequest(int type, SkypeHost host, LPCSTR url, MTHttpRequestHandler pFunc) : + m_host(host) +{ + switch (host) { + case HOST_API: m_szUrl = "api.skype.com"; break; + case HOST_CONTACTS: m_szUrl = "contacts.skype.com/contacts/v2"; break; + case HOST_LOGIN: m_szUrl = "login.microsoftonline.com"; break; + case HOST_TEAMS: m_szUrl = TEAMS_BASE_HOST; break; + case HOST_TEAMS_API: m_szUrl = TEAMS_BASE_HOST "/api/mt/beta"; break; + case HOST_CHATS: m_szUrl = TEAMS_BASE_HOST "/api/chatsvc/consumer/v1"; break; + case HOST_GROUPS: m_szUrl = TEAMS_BASE_HOST "/api/groups/v1"; break; + case HOST_PRESENCE: m_szUrl = "presence." TEAMS_BASE_HOST "/v1"; break; + } + + AddHeader("User-Agent", TEAMS_USER_AGENT); + + if (url) + m_szUrl.Append(url); + m_pFunc = pFunc; + flags = NLHRF_HTTP11 | NLHRF_SSL | NLHRF_DUMPASTEXT; + requestType = type; +} + +void AsyncHttpRequest::AddAuthentication(CTeamsProto *ppro) +{ + AddHeader("Authentication", CMStringA("skypetoken=") + ppro->m_szSkypeToken); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::StartQueue() +{ + if (!m_isTerminated) + return; + + m_isTerminated = false; + if (m_hRequestQueueThread == nullptr) + ForkThread(&CTeamsProto::WorkerThread); +} + +void CTeamsProto::StopQueue() +{ + m_isTerminated = true; + + if (m_hRequestQueueThread) + m_hRequestQueueEvent.Set(); +} + +void CTeamsProto::PushRequest(AsyncHttpRequest *request) +{ + if (m_isTerminated) + return; + { + mir_cslock lock(m_requestQueueLock); + m_requests.insert(request); + } + m_hRequestQueueEvent.Set(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +MHttpResponse* CTeamsProto::DoSend(AsyncHttpRequest *pReq) +{ + if (pReq->m_host != HOST_OTHER) + pReq->m_szUrl.Insert(0, ((pReq->flags & NLHRF_SSL) ? "https://" : "http://")); + + if (!pReq->m_szParam.IsEmpty()) { + switch (pReq->requestType) { + case REQUEST_PUT: + case REQUEST_POST: + if (!pReq->FindHeader("Content-Type")) { + if (pReq->m_szParam[0] == '[' || pReq->m_szParam[0] == '{') + pReq->AddHeader("Content-Type", "application/json"); + else + pReq->AddHeader("Content-Type", "application/x-www-form-urlencoded"); + } + } + } + + pReq->AddHeader("X-MS-Client-Consumer-Type", "teams4life"); + + switch (pReq->m_host) { + case HOST_CHATS: + case HOST_CONTACTS: + pReq->AddAuthentication(this); + pReq->AddHeader("Accept", "application/json"); + pReq->AddHeader("X-Stratus-Caller", TEAMS_CLIENTINFO_NAME); + pReq->AddHeader("X-Stratus-Request", "abcd1234"); + pReq->AddHeader("Origin", "https://teams.live.com"); + pReq->AddHeader("Referer", "https://teams.live.com/"); + pReq->AddHeader("ms-ic3-product", "tfl"); + pReq->AddHeader("ms-ic3-additional-product", "Sfl"); + break; + + case HOST_GROUPS: + case HOST_TEAMS_API: + pReq->AddHeader("X-MS-Client-Type", "maglev"); + pReq->AddHeader("Origin", "https://teams.live.com"); + pReq->AddHeader("Referer", "https://teams.live.com/v2/"); + pReq->AddHeader("Cookie", mir_urlEncode(m_szApiCookie)); + __fallthrough; + + case HOST_TEAMS: + if (!pReq->FindHeader("Authorization")) + pReq->AddHeader("Authorization", "Bearer " + m_szAccessToken); + if (m_szSkypeToken) + pReq->AddHeader("X-Skypetoken", m_szSkypeToken); + pReq->AddHeader("Accept", "application/json"); + pReq->AddHeader("ms-ic3-product", "tfl"); + pReq->AddHeader("ms-ic3-additional-product", "Sfl"); + break; + + case HOST_API: + if (m_szSkypeToken) + pReq->AddHeader("X-Skypetoken", m_szSkypeToken); + pReq->AddHeader("Accept", "application/json"); + break; + + case HOST_PRESENCE: + pReq->flags |= NLHRF_REDIRECT; + + if (m_szSkypeToken) + pReq->AddHeader("X-Skypetoken", m_szSkypeToken); + + pReq->AddHeader("Accept", "application/json"); + pReq->AddHeader("x-ms-client-user-agent", "Teams-V2-Desktop"); + pReq->AddHeader("x-ms-correlation-id", "1"); + pReq->AddHeader("x-ms-client-version", TEAMS_CLIENTINFO_VERSION); + pReq->AddHeader("x-ms-endpoint-id", m_szEndpoint); + break; + + case HOST_LOGIN: + #ifndef _DEBUG + pReq->flags |= NLHRF_NODUMP; + #endif + break; + } + + debugLogA("Send request to %s", pReq->m_szUrl.c_str()); + + return Netlib_HttpTransaction(m_hNetlibUser, pReq); +} + +void CTeamsProto::Execute(AsyncHttpRequest *item) +{ + NLHR_PTR response(DoSend(item)); + if (item->m_pFunc != nullptr) + (this->*item->m_pFunc)(response, item); + m_requests.remove(item); + delete item; +} + +void CTeamsProto::WorkerThread(void*) +{ + m_hRequestQueueThread = GetCurrentThread(); + + while (true) { + m_hRequestQueueEvent.Wait(); + if (m_isTerminated) + break; + + while (true) { + AsyncHttpRequest *item = nullptr; + { + mir_cslock lock(m_requestQueueLock); + + if (m_requests.getCount() == 0) + break; + + item = m_requests[0]; + m_requests.remove(0); + } + if (item != nullptr) + Execute(item); + } + } + + m_hRequestQueueThread = nullptr; +} diff --git a/protocols/Teams/src/teams_login.cpp b/protocols/Teams/src/teams_login.cpp new file mode 100644 index 0000000000..b676af3641 --- /dev/null +++ b/protocols/Teams/src/teams_login.cpp @@ -0,0 +1,254 @@ +/* +Copyright (c) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +#define TEAMS_OAUTH_RESOURCE "https://api.spaces.skype.com" +#define TEAMS_OAUTH_SCOPE "service::api.fl.teams.microsoft.com::MBI_SSL" +#define TEAMS_SKYPETOKEN_SCOPE "service::api.fl.spaces.skype.com::MBI_SSL" +#define TEAMS_PERSONAL_TENANT_ID "9188040d-6c67-4c5b-b112-36a304b66dad" +#define SCOPE_SUFFIX " openid profile offline_access" + +void CTeamsProto::LoginError() +{ + ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, 1001); + SetStatus(ID_STATUS_OFFLINE); + + if (m_iLoginExpires) { + m_impl.m_loginPoll.StopSafe(); + m_iLoginExpires = 0; + } +} + +void CTeamsProto::LoggedIn() +{ + int oldStatus = m_iStatus; + m_iStatus = m_iDesiredStatus; + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus); + + SetServerStatus(m_iStatus); + + ReceiveAvatar(0); + RefreshContactsInfo(); + RefreshConversations(); + + GetProfileInfo(0); + + PushRequest(new AsyncHttpRequest(REQUEST_POST, HOST_TEAMS_API, "/imageauth/cookie", &CTeamsProto::OnReceiveApiCookie)); + + StartTrouter(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::OnReceiveDevicePoll(MHttpResponse *response, AsyncHttpRequest *) +{ + JsonReply reply(response); + if (!reply) { + if (!strstr(response->body, "\"error\":\"authorization_pending\"")) + LoginError(); + return; + } + + if (m_iLoginExpires) { + m_impl.m_loginPoll.StopSafe(); + m_iLoginExpires = 0; + } + m_szDeviceCode.Empty(); + + auto &root = reply.data(); + setWString(DBKEY_RTOKEN, root["refresh_token"].as_mstring()); + + OauthRefreshServices(); +} + +void CTeamsProto::LoginPoll() +{ + if (time(0) >= m_iLoginExpires) { + LoginError(); + return; + } + + auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_LOGIN, "/common/oauth2/token", &CTeamsProto::OnReceiveDevicePoll); + pReq->AddHeader("Cookie", m_szDeviceCookie); + pReq << CHAR_PARAM("client_id", TEAMS_CLIENT_ID) << CHAR_PARAM("grant_type", "urn:ietf:params:oauth:grant-type:device_code") + << CHAR_PARAM("code", m_szDeviceCode); + PushRequest(pReq); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +const wchar_t wszLoginMessage[] = +LPGENW("To login into Teams you need to open '%S' in a browser and select your Teams account there.") L"\r\n\r\n" +LPGENW("Enter the following code then: %s.") L"\r\n\r\n" +LPGENW("Click Proceed to copy that code to clipboard and launch a browser"); + +class CDeviceCodeDlg : public CTeamsDlgBase +{ + bool bSucceeded = false; + +public: + CDeviceCodeDlg(CTeamsProto *ppro) : + CTeamsDlgBase(ppro, IDD_DEVICECODE) + {} + + bool OnInitDialog() override + { + CMStringW wszText(FORMAT, TranslateW(wszLoginMessage), m_proto->m_szVerificationUrl.c_str(), m_proto->m_wszUserCode.c_str()); + SetDlgItemTextW(m_hwnd, IDC_TEXT, wszText); + return true; + } + + bool OnApply() override + { + bSucceeded = true; + Utils_OpenUrl(m_proto->m_szVerificationUrl); + return true; + } + + void OnDestroy() override + { + if (!bSucceeded) + m_proto->LoginError(); + } +}; + +static void CALLBACK LaunchDialog(void *param) +{ + (new CDeviceCodeDlg((CTeamsProto *)param))->Show(); +} + +void CTeamsProto::OnReceiveDeviceToken(MHttpResponse *response, AsyncHttpRequest *) +{ + JsonReply reply(response); + if (!reply) { + LoginError(); + return; + } + + auto &root = reply.data(); + m_wszUserCode = root["user_code"].as_mstring(); + m_szDeviceCode = root["device_code"].as_mstring(); + m_szVerificationUrl = root["verification_url"].as_mstring(); + m_iLoginExpires = time(0) + root["expires_in"].as_int(); + m_impl.m_loginPoll.StartSafe(root["interval"].as_int() * 1000); + m_szDeviceCookie = response->GetCookies(); + + Utils_ClipboardCopy(MClipUnicode(m_wszUserCode)); + CallFunctionAsync(LaunchDialog, this); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::OnReceiveSkypeToken(MHttpResponse *response, AsyncHttpRequest *) +{ + JsonReply reply(response); + if (!reply) { + LoginError(); + return; + } + + auto &token = reply.data()["skypeToken"]; + m_szSkypeToken = token["skypetoken"].as_mstring(); + + m_szOwnSkypeId = token["skypeid"].as_mstring(); + setString(DBKEY_ID, m_szOwnSkypeId); + + LoggedIn(); +} + +void CTeamsProto::OnRefreshAccessToken(MHttpResponse *response, AsyncHttpRequest *) +{ + JsonReply reply(response); + if (!reply) { + LoginError(); + return; + } + + auto &root = reply.data(); + m_szAccessToken = root["access_token"].as_mstring(); + setWString(DBKEY_RTOKEN, root["refresh_token"].as_mstring()); +} + +void CTeamsProto::OnRefreshSkypeToken(MHttpResponse *response, AsyncHttpRequest *) +{ + JsonReply reply(response); + if (!reply) { + LoginError(); + return; + } + + auto &root = reply.data(); + CMStringA szAccessToken(root["access_token"].as_mstring()); + + auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_TEAMS, "/api/auth/v1.0/authz/consumer", &CTeamsProto::OnReceiveSkypeToken); + pReq->AddHeader("Authorization", "Bearer " + szAccessToken); + PushRequest(pReq); +} + +void CTeamsProto::OnRefreshSubstrate(MHttpResponse *response, AsyncHttpRequest *) +{ + JsonReply reply(response); + if (!reply) { + LoginError(); + return; + } + + auto &root = reply.data(); + m_szSubstrateToken = root["access_token"].as_mstring(); +} + +void CTeamsProto::RefreshToken(const char *pszScope, AsyncHttpRequest::MTHttpRequestHandler pFunc) +{ + auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_LOGIN, "/" TEAMS_PERSONAL_TENANT_ID "/oauth2/v2.0/token", pFunc); + pReq << CHAR_PARAM("scope", pszScope) << CHAR_PARAM("client_id", TEAMS_CLIENT_ID) + << CHAR_PARAM("grant_type", "refresh_token") << CHAR_PARAM("refresh_token", getMStringA(DBKEY_RTOKEN)); + PushRequest(pReq); +} + +void CTeamsProto::OauthRefreshServices() +{ + RefreshToken(TEAMS_SKYPETOKEN_SCOPE SCOPE_SUFFIX, &CTeamsProto::OnRefreshSkypeToken); + RefreshToken("https://substrate.office.com/M365.Access" SCOPE_SUFFIX, &CTeamsProto::OnRefreshSubstrate); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// module entry point + +void CTeamsProto::Login() +{ + // set plugin status to connect + int oldStatus = m_iStatus; + m_iStatus = ID_STATUS_CONNECTING; + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus); + + // launch http queue + StartQueue(); + + // if refresh token doesn't exist, perform a device code authentication + m_szAccessToken = getMStringA(DBKEY_RTOKEN); + if (m_szAccessToken.IsEmpty()) { + auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_LOGIN, "/common/oauth2/devicecode", &CTeamsProto::OnReceiveDeviceToken); + pReq << CHAR_PARAM("client_id", TEAMS_CLIENT_ID) << CHAR_PARAM("resource", TEAMS_OAUTH_RESOURCE); + PushRequest(pReq); + } + // or use a refresh token otherwise + else { + RefreshToken(TEAMS_OAUTH_SCOPE SCOPE_SUFFIX, &CTeamsProto::OnRefreshAccessToken); + OauthRefreshServices(); + } +} diff --git a/protocols/Teams/src/teams_menus.cpp b/protocols/Teams/src/teams_menus.cpp new file mode 100644 index 0000000000..e46d2174e1 --- /dev/null +++ b/protocols/Teams/src/teams_menus.cpp @@ -0,0 +1,92 @@ +/* +Copyright (c) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +HGENMENU CTeamsProto::ContactMenuItems[CMI_MAX]; + +int CTeamsProto::OnPrebuildContactMenu(WPARAM hContact, LPARAM) +{ + if (!hContact) + return 0; + + if (m_iStatus < ID_STATUS_ONLINE) + return 0; + + if (isChatRoom(hContact)) + return 0; + + bool isCtrlPressed = (GetKeyState(VK_CONTROL) & 0x8000) != 0; + bool isAuthNeed = getByte(hContact, "Auth", 0) > 0; + bool isGrantNeed = getByte(hContact, "Grant", 0) > 0; + bool isBlocked = getBool(hContact, "IsBlocked", false); + + Menu_ShowItem(GetMenuItem(PROTO_MENU_REQ_AUTH), isCtrlPressed || isAuthNeed); + Menu_ShowItem(GetMenuItem(PROTO_MENU_GRANT_AUTH), isCtrlPressed || isGrantNeed); + + Menu_ShowItem(ContactMenuItems[CMI_BLOCK], true); + Menu_ShowItem(ContactMenuItems[CMI_UNBLOCK], isCtrlPressed || isBlocked); + return 0; +} + +int CTeamsProto::PrebuildContactMenu(WPARAM hContact, LPARAM lParam) +{ + for (auto &it : ContactMenuItems) + Menu_ShowItem(it, false); + CTeamsProto *proto = CMPlugin::getInstance(hContact); + return proto ? proto->OnPrebuildContactMenu(hContact, lParam) : 0; +} + +void CTeamsProto::InitMenus() +{ + HookEvent(ME_CLIST_PREBUILDCONTACTMENU, &CTeamsProto::PrebuildContactMenu); + + CMenuItem mi(&g_plugin); + mi.flags = CMIF_UNICODE; + + mi.pszService = MODULENAME "/BlockContact"; + mi.name.w = LPGENW("Block contact"); + mi.position = CMI_POSITION + CMI_BLOCK; + mi.hIcolibItem = g_plugin.getIconHandle(IDI_BLOCKUSER); + SET_UID(mi, 0xc6169b8f, 0x53ab, 0x4242, 0xbe, 0x90, 0xe2, 0x4a, 0xa5, 0x73, 0x88, 0x32); + ContactMenuItems[CMI_BLOCK] = Menu_AddContactMenuItem(&mi); + CreateServiceFunction(mi.pszService, GlobalService<&CTeamsProto::BlockContact>); + + mi.pszService = MODULENAME "/UnblockContact"; + mi.name.w = LPGENW("Unblock contact"); + mi.position = CMI_POSITION + CMI_UNBLOCK; + mi.hIcolibItem = g_plugin.getIconHandle(IDI_UNBLOCKUSER); + SET_UID(mi, 0x88542f43, 0x7448, 0x48d0, 0x81, 0xa3, 0x26, 0x0, 0x4f, 0x37, 0xee, 0xe0); + ContactMenuItems[CMI_UNBLOCK] = Menu_AddContactMenuItem(&mi); + CreateServiceFunction(mi.pszService, GlobalService<&CTeamsProto::UnblockContact>); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Protocol's menu in the status bar + +void CTeamsProto::OnBuildProtoMenu() +{ + CMenuItem mi(&g_plugin); + mi.root = Menu_GetProtocolRoot(this); + + mi.pszService = "/CreateNewChat"; + CreateProtoService(mi.pszService, &CTeamsProto::SvcCreateChat); + mi.name.a = LPGEN("Create new chat"); + mi.position = 200000; + mi.hIcolibItem = g_plugin.getIconHandle(IDI_CONFERENCE); + Menu_AddProtoMenuItem(&mi, m_szModuleName); +} diff --git a/protocols/Teams/src/teams_menus.h b/protocols/Teams/src/teams_menus.h new file mode 100644 index 0000000000..04b87c6e83 --- /dev/null +++ b/protocols/Teams/src/teams_menus.h @@ -0,0 +1,30 @@ +/* +Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef _SKYPE_MENUS_H_ +#define _SKYPE_MENUS_H_ + +#define CMI_POSITION -201001000 + +enum +{ + CMI_BLOCK, + CMI_UNBLOCK, + CMI_MAX // this item shall be the last one +}; + +#endif //_SKYPE_MENUS_H_
\ No newline at end of file diff --git a/protocols/Teams/src/teams_messages.cpp b/protocols/Teams/src/teams_messages.cpp new file mode 100644 index 0000000000..c2c4ad2473 --- /dev/null +++ b/protocols/Teams/src/teams_messages.cpp @@ -0,0 +1,337 @@ +/* +Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +///////////////////////////////////////////////////////////////////////////////////////// +// MESSAGE SENDING + +void CTeamsProto::OnMessageSent(MHttpResponse *response, AsyncHttpRequest *pRequest) +{ + // to delete it in any case + std::unique_ptr<COwnMessage> pMessage((COwnMessage *)pRequest->pUserInfo); + + MCONTACT hContact = pRequest->hContact; + if (response == nullptr) { + ProtoBroadcastAck(hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, pRequest->pUserInfo, (LPARAM)TranslateT("Network error!")); + return; + } + + if (response->resultCode == 201) { + JsonReply reply(response); + auto &pRoot = reply.data(); + + if (pMessage) { + if (!Contact::IsGroupChat(hContact)) { + pMessage->iTimestamp = _wtoi64(pRoot["OriginalArrivalTime"].as_mstring()); + + CMStringA szMsgId(FORMAT, "%lld", pMessage->hClientMessageId); + ProtoBroadcastAck(hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)pMessage->hMessage, (LPARAM)szMsgId.c_str()); + } + + mir_cslock lck(m_lckOutMessagesList); + m_OutMessages.remove(pMessage.get()); + } + } + else { + std::string strError = Translate("Unknown error!"); + + if (!response->body.IsEmpty()) { + JSONNode jRoot = JSONNode::parse(response->body); + const JSONNode &jErr = jRoot["errorCode"]; + if (jErr) + strError = jErr.as_string(); + } + + ProtoBroadcastAck(hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, pRequest->pUserInfo, _A2T(strError.c_str())); + } +} + +// outcoming message flow + +int CTeamsProto::SendServerMsg(MCONTACT hContact, const char *szMessage, int64_t existingMsgId) +{ + if (!IsOnline()) + return -1; + + CMStringA str(szMessage); + bool bRich = AddBbcodes(str); + m_iMessageId++; + + CMStringA szChatId(getMStringA(hContact, "ChatId")); + if (szChatId.IsEmpty()) + szChatId = getId(hContact); + + CMStringA szUrl = "/users/ME/conversations/" + mir_urlEncode(szChatId) + "/messages"; + if (existingMsgId) + szUrl.AppendFormat("/%lld", existingMsgId); + + JSONNode node; + node << CHAR_PARAM("messagetype", bRich ? "RichText" : "Text") << CHAR_PARAM("contenttype", "text"); + if (strncmp(str, "/me ", 4) == 0) + node << CHAR_PARAM("content", m_szSkypename + " " + str); + else + node << CHAR_PARAM("content", str); + + COwnMessage *pOwnMessage = nullptr; + if (!existingMsgId) { + int64_t iRandomId = getRandomId(); + node << INT64_PARAM("clientmessageid", iRandomId); + + mir_cslock lck(m_lckOutMessagesList); + m_OutMessages.insert(pOwnMessage = new COwnMessage(m_iMessageId, iRandomId)); + } + + AsyncHttpRequest *pReq = new AsyncHttpRequest(existingMsgId ? REQUEST_PUT : REQUEST_POST, HOST_CHATS, szUrl, &CTeamsProto::OnMessageSent); + pReq->hContact = hContact; + pReq->pUserInfo = pOwnMessage; + pReq->m_szParam = node.write().c_str(); + PushRequest(pReq); + + return m_iMessageId; +} + +// preparing message/action to be written into db +int CTeamsProto::OnPreCreateMessage(WPARAM, LPARAM lParam) +{ + MessageWindowEvent *evt = (MessageWindowEvent*)lParam; + if (mir_strcmp(Proto_GetBaseAccountName(evt->hContact), m_szModuleName)) + return 0; + + auto &dbei = evt->dbei; + if (dbei->szId) { + int64_t msgId = _atoi64(dbei->szId); + for (auto &it : m_OutMessages) { + if (it->hClientMessageId == msgId) { + dbei->bMsec = true; + dbei->iTimestamp = it->iTimestamp; + } } } + + return 0; +} + +/* MESSAGE EVENT */ + +bool CTeamsProto::ParseMessage(const JSONNode &node, DB::EventInfo &dbei) +{ + auto &pContent = node["content"]; + if (!pContent) { +LBL_Deleted: + if (dbei) + db_event_delete(dbei.getEvent()); + return false; + } + + CMStringW wszContent = pContent.as_mstring(); + if (wszContent.IsEmpty()) + goto LBL_Deleted; + + std::string strMessageType = node["messagetype"].as_string(); + if (strMessageType == "RichText/Media_GenericFile" || strMessageType == "RichText/Media_Video" || strMessageType == "RichText/UriObject" ) { + ProcessFileRecv(dbei.hContact, node["content"].as_string().c_str(), dbei); + return false; + } + if (strMessageType == "RichText/Contacts") { + ProcessContactRecv(dbei.hContact, node["content"].as_string().c_str(), dbei); + return false; + } + + if (strMessageType == "Text") { + dbei.eventType = EVENTTYPE_MESSAGE; + } + else if (strMessageType == "RichText/Html" || strMessageType == "RichText") { + wszContent = RemoveHtml(wszContent); + dbei.eventType = EVENTTYPE_MESSAGE; + } + else if (strMessageType == "RichText/Media_Album") + return false; + + replaceStr(dbei.pBlob, mir_utf8encodeW(wszContent)); + dbei.cbBlob = (uint32_t)mir_strlen(dbei.pBlob); + return true; +} + +void CTeamsProto::ProcessNewMessage(const JSONNode &node) +{ + int iUserType; + UrlToSkypeId(node["conversationLink"].as_string().c_str(), &iUserType); + + int64_t timestamp = _wtoi64(node["id"].as_mstring()); + CMStringA szMessageId(getMessageId(node)); + CMStringA szConversationName(UrlToSkypeId(node["conversationLink"].as_string().c_str())); + CMStringA szFromSkypename(UrlToSkypeId(node["from"].as_mstring())); + + if (iUserType == 19) + if (OnChatEvent(node)) + return; + + MCONTACT hContact = AddContact(szConversationName, nullptr, true); + + if (m_bHistorySynced && timestamp > getLastTime(hContact)) + setLastTime(hContact, timestamp); + + std::string strMessageType = node["messagetype"].as_string(); + if (strMessageType == "Control/Typing") { + CallService(MS_PROTO_CONTACTISTYPING, hContact, 30); + return; + } + if (strMessageType == "Control/ClearTyping") { + CallService(MS_PROTO_CONTACTISTYPING, hContact, PROTOTYPE_CONTACTTYPING_OFF); + return; + } + + DB::EventInfo dbei(db_event_getById(m_szModuleName, szMessageId)); + dbei.hContact = hContact; + dbei.iTimestamp = timestamp; + dbei.szId = szMessageId; + dbei.bUtf = dbei.bMsec = true; + dbei.bSent = IsMe(szFromSkypename); + if (iUserType == 19) + dbei.szUserId = szFromSkypename; + + if (ParseMessage(node, dbei)) { + if (dbei) + db_event_edit(dbei.getEvent(), &dbei, true); + else + ProtoChainRecvMsg(hContact, dbei); + } +} + +void CTeamsProto::OnMarkRead(MCONTACT hContact, MEVENT hDbEvent) +{ + if (IsOnline()) { + DB::EventInfo dbei(hDbEvent, false); + if (dbei && dbei.szId) { + auto *pReq = new AsyncHttpRequest(REQUEST_PUT, HOST_CHATS, "/users/ME/conversations/" + mir_urlEncode(getId(hContact)) + "/properties?name=consumptionhorizon"); + auto msgTimestamp = _atoi64(dbei.szId); + + JSONNode node(JSON_NODE); + node << CHAR_PARAM("consumptionhorizon", CMStringA(::FORMAT, "%lld;%lld;%lld", msgTimestamp, msgTimestamp, msgTimestamp)); + pReq->m_szParam = node.write().c_str(); + + PushRequest(pReq); + } + } +} + +void CTeamsProto::OnReceiveOfflineFile(DB::EventInfo &dbei, DB::FILE_BLOB &blob) +{ + if (auto *ft = (CSkypeTransfer *)blob.getUserInfo()) { + blob.setUrl(ft->url); + blob.setSize(ft->iFileSize); + + auto &json = dbei.setJson(); + json << CHAR_PARAM("skft", ft->fileType); + if (ft->iHeight != -1) + json << INT_PARAM("h", ft->iHeight); + if (ft->iWidth != -1) + json << INT_PARAM("w", ft->iWidth); + delete ft; + } +} + +void CTeamsProto::ProcessFileRecv(MCONTACT hContact, const char *szContent, DB::EventInfo &dbei) +{ + TiXmlDocument doc; + if (0 != doc.Parse(szContent)) + return; + + auto *xmlRoot = doc.FirstChildElement("URIObject"); + if (xmlRoot == nullptr) + return; + + CSkypeTransfer *ft = new CSkypeTransfer; + if (auto *str = xmlRoot->Attribute("doc_id")) + ft->docId = str; + if (auto *str = xmlRoot->Attribute("uri")) + ft->url = str; + ft->iWidth = xmlRoot->IntAttribute("width", -1); + ft->iHeight = xmlRoot->IntAttribute("heighr", -1); + if (auto *str = xmlRoot->Attribute("type")) + ft->fileType = str; + if (auto *xml = xmlRoot->FirstChildElement("FileSize")) + if (auto *str = xml->Attribute("v")) + ft->iFileSize = atoi(str); + if (auto *xml = xmlRoot->FirstChildElement("OriginalName")) + if (auto *str = xml->Attribute("v")) + ft->fileName = str; + + if (ft->url.IsEmpty() || ft->fileName.IsEmpty() || ft->iFileSize == 0) { + debugLogA("Missing file info: url=<%s> name=<%s> %d", ft->url.c_str(), ft->fileName.c_str(), ft->iFileSize); + delete ft; + return; + } + + int idx = ft->fileType.Find('/'); + if (idx != -1) + ft->fileType = ft->fileType.Left(idx); + + // ordinary file + if (ft->fileType == "File.1" || ft->fileType == "Picture.1" || ft->fileType == "Video.1") { + MEVENT hEvent; + dbei.flags |= DBEF_TEMPORARY | DBEF_JSON; + if (dbei) { + DB::FILE_BLOB blob(dbei); + OnReceiveOfflineFile(dbei, blob); + blob.write(dbei); + db_event_edit(dbei.getEvent(), &dbei, true); + delete ft; + hEvent = dbei.getEvent(); + } + else hEvent = ProtoChainRecvFile(hContact, DB::FILE_BLOB(ft, ft->fileName), dbei); + } + else debugLogA("Invalid or unsupported file type <%s> ignored", ft->fileType.c_str()); +} + +void CTeamsProto::ProcessContactRecv(MCONTACT hContact, const char *szContent, DB::EventInfo &dbei) +{ + TiXmlDocument doc; + if (0 != doc.Parse(szContent)) + return; + + auto *xmlNode = doc.FirstChildElement("contacts"); + if (xmlNode == nullptr) + return; + + int nCount = 0; + for (auto *it : TiXmlEnum(xmlNode)) { + UNREFERENCED_PARAMETER(it); + nCount++; + } + + PROTOSEARCHRESULT **psr = (PROTOSEARCHRESULT**)mir_calloc(sizeof(PROTOSEARCHRESULT*) * nCount); + + nCount = 0; + for (auto *xmlContact : TiXmlFilter(xmlNode, "c")) { + psr[nCount] = (PROTOSEARCHRESULT*)mir_calloc(sizeof(PROTOSEARCHRESULT)); + psr[nCount]->cbSize = sizeof(psr); + psr[nCount]->id.a = mir_strdup(xmlContact->Attribute("s")); + nCount++; + } + + if (nCount) { + dbei.pBlob = (char*)psr; + dbei.cbBlob = nCount; + + ProtoChainRecv(hContact, PSR_CONTACTS, 0, (LPARAM)&dbei); + for (int i = 0; i < nCount; i++) { + mir_free(psr[i]->id.a); + mir_free(psr[i]); + } + } + mir_free(psr); +} diff --git a/protocols/Teams/src/teams_options.cpp b/protocols/Teams/src/teams_options.cpp new file mode 100644 index 0000000000..0f50948446 --- /dev/null +++ b/protocols/Teams/src/teams_options.cpp @@ -0,0 +1,99 @@ +/* +Copyright (c) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +class COptionsMainDlg : public CTeamsDlgBase +{ + CCtrlEdit m_login, m_group; + CCtrlCheck m_autosync, m_usehostname, m_usebb; + CCtrlButton btnLogout; + +public: + COptionsMainDlg(CTeamsProto *proto, int idDialog) : + CTeamsDlgBase(proto, idDialog), + m_login(this, IDC_LOGIN), + m_group(this, IDC_GROUP), + m_autosync(this, IDC_AUTOSYNC), + m_usehostname(this, IDC_USEHOST), + m_usebb(this, IDC_BBCODES), + btnLogout(this, IDC_LOGOUT) + { + CreateLink(m_group, proto->m_wstrCListGroup); + CreateLink(m_autosync, proto->m_bAutoHistorySync); + CreateLink(m_usehostname, proto->m_bUseHostnameAsPlace); + CreateLink(m_usebb, proto->m_bUseBBCodes); + + btnLogout.OnClick = Callback(this, &COptionsMainDlg::onClick_Logout); + } + + bool OnInitDialog() override + { + if (m_proto->getMStringA(DBKEY_RTOKEN).IsEmpty()) + btnLogout.Disable(); + + CMStringA szLogin(m_proto->getMStringA(DBKEY_ID)); + if (szLogin.IsEmpty()) + m_login.SetText(TranslateT("<will appear after first login>")); + else + m_login.SetTextA(szLogin); + m_group.SendMsg(EM_LIMITTEXT, 64, 0); + return true; + } + + bool OnApply() override + { + ptrW group(m_group.GetText()); + if (mir_wstrlen(group) > 0 && !Clist_GroupExists(group)) + Clist_GroupCreate(0, group); + return true; + } + + void onClick_Logout(CCtrlButton *) + { + m_proto->delSetting(DBKEY_RTOKEN); + + if (m_proto->IsOnline()) + m_proto->SetStatus(ID_STATUS_OFFLINE); + + btnLogout.Disable(); + } +}; + +///////////////////////////////////////////////////////////////////////////////// + +MWindow CTeamsProto::OnCreateAccMgrUI(MWindow hwndParent) +{ + auto *pDlg = new COptionsMainDlg(this, IDD_ACCOUNT_MANAGER); + pDlg->SetParent(hwndParent); + pDlg->Show(); + return pDlg->GetHwnd(); +} + +int CTeamsProto::OnOptionsInit(WPARAM wParam, LPARAM) +{ + OPTIONSDIALOGPAGE odp = { sizeof(odp) }; + odp.szTitle.w = m_tszUserName; + odp.flags = ODPF_BOLDGROUPS | ODPF_UNICODE | ODPF_DONTTRANSLATE; + odp.szGroup.w = LPGENW("Network"); + + odp.szTab.w = LPGENW("Account"); + odp.pDialog = new COptionsMainDlg(this, IDD_OPTIONS_MAIN); + g_plugin.addOptions(wParam, &odp); + + return 0; +} diff --git a/protocols/Teams/src/teams_polling.cpp b/protocols/Teams/src/teams_polling.cpp new file mode 100644 index 0000000000..11a918034e --- /dev/null +++ b/protocols/Teams/src/teams_polling.cpp @@ -0,0 +1,137 @@ +/* +Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +void CTeamsProto::ParsePollData(const char *szData) +{ + debugLogA(__FUNCTION__); + + JSONNode data = JSONNode::parse(szData); + if (!data) + return; + + for (auto &message : data["eventMessages"]) { + int eventId = message["id"].as_int(); + if (eventId > m_iPollingId) + m_iPollingId = eventId; + + const JSONNode &resource = message["resource"]; + + std::string resourceType = message["resourceType"].as_string(); + if (resourceType == "NewMessage") + ProcessNewMessage(resource); + else if (resourceType == "UserPresence") + ProcessUserPresence(resource); + else if (resourceType == "EndpointPresence") + ProcessEndpointPresence(resource); + else if (resourceType == "ConversationUpdate") + ProcessConversationUpdate(resource); + else if (resourceType == "ThreadUpdate") + ProcessThreadUpdate(resource); + } +} + +void CTeamsProto::ProcessEndpointPresence(const JSONNode &node) +{ + debugLogA(__FUNCTION__); + std::string selfLink = node["selfLink"].as_string(); + CMStringA skypename(UrlToSkypeId(selfLink.c_str())); + + MCONTACT hContact = FindContact(skypename); + if (hContact == NULL) + return; + + const JSONNode &publicInfo = node["publicInfo"]; + const JSONNode &privateInfo = node["privateInfo"]; + CMStringA MirVer; + if (publicInfo) { + std::string skypeNameVersion = publicInfo["skypeNameVersion"].as_string(); + std::string version = publicInfo["version"].as_string(); + std::string typ = publicInfo["typ"].as_string(); + int iTyp = atoi(typ.c_str()); + switch (iTyp) { + case 0: + case 1: + MirVer.Append("Skype (Web) " + ParseUrl(version.c_str(), "/")); + break; + case 10: + MirVer.Append("Skype (XBOX) " + ParseUrl(skypeNameVersion.c_str(), "/")); + break; + case 17: + MirVer.Append("Skype (Android) " + ParseUrl(skypeNameVersion.c_str(), "/")); + break; + case 16: + MirVer.Append("Skype (iOS) " + ParseUrl(skypeNameVersion.c_str(), "/")); + break; + case 12: + MirVer.Append("Skype (WinRT) " + ParseUrl(skypeNameVersion.c_str(), "/")); + break; + case 15: + MirVer.Append("Skype (WP) " + ParseUrl(skypeNameVersion.c_str(), "/")); + break; + case 13: + MirVer.Append("Skype (OSX) " + ParseUrl(skypeNameVersion.c_str(), "/")); + break; + case 11: + MirVer.Append("Skype (Windows) " + ParseUrl(skypeNameVersion.c_str(), "/")); + break; + case 14: + MirVer.Append("Skype (Linux) " + ParseUrl(skypeNameVersion.c_str(), "/")); + break; + case 125: + MirVer.AppendFormat("Miranda NG Skype %s", version.c_str()); + break; + default: + MirVer.Append("Skype (Unknown)"); + } + } + + if (privateInfo != NULL) { + std::string epname = privateInfo["epname"].as_string(); + if (!epname.empty()) + MirVer.AppendFormat(" [%s]", epname.c_str()); + } + + setString(hContact, "MirVer", MirVer); +} + +void CTeamsProto::ProcessUserPresence(const JSONNode &node) +{ + debugLogA(__FUNCTION__); + + std::string selfLink = node["selfLink"].as_string(); + std::string status = node["availability"].as_string(); + CMStringA skypename = UrlToSkypeId(selfLink.c_str()); + + if (!skypename.IsEmpty()) { + if (IsMe(skypename)) { + int iNewStatus = SkypeToMirandaStatus(status.c_str()); + if (iNewStatus == ID_STATUS_OFFLINE) return; + int old_status = m_iStatus; + m_iDesiredStatus = iNewStatus; + m_iStatus = iNewStatus; + if (old_status != iNewStatus) + ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, iNewStatus); + } + else { + MCONTACT hContact = FindContact(skypename); + if (hContact != NULL) + SetContactStatus(hContact, SkypeToMirandaStatus(status.c_str())); + } + } +} diff --git a/protocols/Teams/src/teams_popups.cpp b/protocols/Teams/src/teams_popups.cpp new file mode 100644 index 0000000000..59cca9e937 --- /dev/null +++ b/protocols/Teams/src/teams_popups.cpp @@ -0,0 +1,100 @@ +/* +Copyright (c) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +void CTeamsProto::InitPopups() +{ + wchar_t desc[256]; + char name[256]; + + POPUPCLASS ppc = {}; + ppc.flags = PCF_UNICODE; + ppc.pszName = name; + ppc.pszDescription.w = desc; + + mir_snwprintf(desc, L"%s/%s", m_tszUserName, TranslateT("Notifications")); + mir_snprintf(name, "%s_%s", m_szModuleName, "Notification"); + ppc.hIcon = g_plugin.getIcon(IDI_NOTIFY); + ppc.iSeconds = 5; + m_PopupClasses.insert(Popup_RegisterClass(&ppc)); + + mir_snwprintf(desc, L"%s/%s", m_tszUserName, TranslateT("Errors")); + mir_snprintf(name, "%s_%s", m_szModuleName, "Error"); + ppc.hIcon = g_plugin.getIcon(IDI_ERRORICON); + ppc.iSeconds = -1; + m_PopupClasses.insert(Popup_RegisterClass(&ppc)); + + mir_snwprintf(desc, L"%s/%s", m_tszUserName, TranslateT("Calls")); + mir_snprintf(name, "%s_%s", m_szModuleName, "Call"); + ppc.hIcon = g_plugin.getIcon(IDI_CALL); + ppc.iSeconds = 30; + ppc.PluginWindowProc = PopupDlgProcCall; + m_PopupClasses.insert(Popup_RegisterClass(&ppc)); +} + +void CTeamsProto::UninitPopups() +{ + for (auto &it : m_PopupClasses) + Popup_UnregisterClass(it); +} + +void CTeamsProto::ShowNotification(const wchar_t *caption, const wchar_t *message, MCONTACT hContact, int type) +{ + if (Miranda_IsTerminated()) + return; + + CMStringA className(FORMAT, "%s_", m_szModuleName); + + switch (type) { + case 1: + className.Append("Error"); + break; + + default: + className.Append("Notification"); + break; + } + + POPUPDATACLASS ppd = {}; + ppd.szTitle.w = caption; + ppd.szText.w = message; + ppd.pszClassName = className.GetBuffer(); + ppd.hContact = hContact; + Popup_AddClass(&ppd); +} + +void CTeamsProto::ShowNotification(const wchar_t *message, MCONTACT hContact) +{ + ShowNotification(_T(MODULENAME), message, hContact); +} + +LRESULT CTeamsProto::PopupDlgProcCall(HWND hPopup, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + switch (uMsg) { + case WM_CONTEXTMENU: + PUDeletePopup(hPopup); + CallService(MODULENAME "/IncomingCallPP", 0, PUGetContact(hPopup)); + break; + case WM_COMMAND: + PUDeletePopup(hPopup); + CallService(MODULENAME"/IncomingCallPP", 1, PUGetContact(hPopup)); + break; + } + + return DefWindowProc(hPopup, uMsg, wParam, lParam); +} diff --git a/protocols/Teams/src/teams_profile.cpp b/protocols/Teams/src/teams_profile.cpp new file mode 100644 index 0000000000..2402467c51 --- /dev/null +++ b/protocols/Teams/src/teams_profile.cpp @@ -0,0 +1,153 @@ +/* +Copyright (c) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +void CTeamsProto::UpdateProfileDisplayName(const JSONNode &root, MCONTACT hContact) +{ + ptrW firstname(getWStringA(hContact, "FirstName")); + ptrW lastname(getWStringA(hContact, "LastName")); + if (firstname) { + CMStringW nick(firstname); + if (lastname) + nick.AppendFormat(L" %s", lastname.get()); + setWString(hContact, "Nick", nick); + } + else if (lastname) + setWString(hContact, "Nick", lastname); + else { + const JSONNode &node = root["displayname"]; + SetString(hContact, "Nick", (node) ? node : root["username"]); + } +} + +void CTeamsProto::UpdateProfileGender(const JSONNode &root, MCONTACT hContact) +{ + CMStringW gender = root["gender"].as_mstring(); + if (!gender.IsEmpty() && gender != "null") + setByte(hContact, "Gender", (uint8_t)(_wtoi(gender) == 1 ? 'M' : 'F')); + else + delSetting(hContact, "Gender"); +} + +void CTeamsProto::UpdateProfileBirthday(const JSONNode &root, MCONTACT hContact) +{ + CMStringW birthday = root["birthday"].as_mstring(); + if (!birthday.IsEmpty() && birthday != "null") { + int d, m, y; + if (3 == swscanf(birthday.GetBuffer(), L"%d-%d-%d", &y, &m, &d)) { + Contact::SetBirthday(hContact, d, m, y); + return; + } + } + + delSetting(hContact, "BirthYear"); + delSetting(hContact, "BirthDay"); + delSetting(hContact, "BirthMonth"); +} + +void CTeamsProto::UpdateProfileCountry(const JSONNode &root, MCONTACT hContact) +{ + std::string isocode = root["country"].as_string(); + if (!isocode.empty() && isocode != "null") { + char *country = (char *)CallService(MS_UTILS_GETCOUNTRYBYISOCODE, (WPARAM)isocode.c_str(), 0); + setString(hContact, "Country", country); + } + else delSetting(hContact, "Country"); +} + +void CTeamsProto::UpdateProfileEmails(const JSONNode &root, MCONTACT hContact) +{ + const JSONNode &node = root["emails"]; + if (node) { + const JSONNode &items = node.as_array(); + for (int i = 0; i < min(items.size(), 3); i++) { + const JSONNode &item = items.at(i); + if (!item) + break; + + CMStringA name(FORMAT, "e-mail%d", i); + setWString(hContact, name, item.as_mstring()); + } + } + else { + delSetting(hContact, "e-mail0"); + delSetting(hContact, "e-mail1"); + delSetting(hContact, "e-mail2"); + } +} + +void CTeamsProto::UpdateProfileAvatar(const JSONNode &root, MCONTACT hContact) +{ + CMStringW szUrl = root["avatarUrl"].as_mstring(); + if (!szUrl.IsEmpty() && szUrl != "null") { + SetAvatarUrl(hContact, szUrl); + ReloadAvatarInfo(hContact); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::OnGetProfileInfo(MHttpResponse *response, AsyncHttpRequest *pRequest) +{ + MCONTACT hContact = (DWORD_PTR)pRequest->pUserInfo; + + TeamsReply reply(response); + if (reply.error()) { + ProtoBroadcastAck(hContact, ACKTYPE_GETINFO, ACKRESULT_FAILED, 0); + return; + } + + auto &root = reply.data(); + std::string username = root["username"].as_string(); + if (username.empty()) { + ProtoBroadcastAck(hContact, ACKTYPE_GETINFO, ACKRESULT_FAILED, 0); + return; + } + + if (m_szSkypename != username.c_str()) + m_szMyname = username.c_str(); + else + m_szMyname = m_szSkypename; + + SetString(hContact, "City", root["city"]); + SetString(hContact, "About", root["about"]); + SetString(hContact, "Phone", root["phone"]); + SetString(hContact, "State", root["province"]); + SetString(hContact, "Cellular", root["phoneMobile"]); + SetString(hContact, "Homepage", root["homepage"]); + SetString(hContact, "LastName", root["lastname"]); + SetString(hContact, "FirstName", root["firstname"]); + SetString(hContact, "CompanyPhone", root["phoneOffice"]); + + UpdateProfileDisplayName(root, hContact); + UpdateProfileGender(root, hContact); + UpdateProfileBirthday(root, hContact); + UpdateProfileCountry(root, hContact); + UpdateProfileEmails(root, hContact); + UpdateProfileAvatar(root, hContact); + + ProtoBroadcastAck(hContact, ACKTYPE_GETINFO, ACKRESULT_SUCCESS, 0); +} + +void CTeamsProto::GetProfileInfo(MCONTACT hContact) +{ + auto *pReq = new AsyncHttpRequest(REQUEST_GET, HOST_API, 0, &CTeamsProto::OnGetProfileInfo); + pReq->m_szUrl.AppendFormat("/users/%s/profile", (hContact == 0) ? "self" : mir_urlEncode(getId(hContact))); + pReq->pUserInfo = (void *)hContact; + PushRequest(pReq); +} diff --git a/protocols/Teams/src/teams_proto.cpp b/protocols/Teams/src/teams_proto.cpp new file mode 100644 index 0000000000..dfa09503e0 --- /dev/null +++ b/protocols/Teams/src/teams_proto.cpp @@ -0,0 +1,297 @@ +/* +Copyright (c) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +CTeamsProto::CTeamsProto(const char *protoName, const wchar_t *userName) : + PROTO<CTeamsProto>(protoName, userName), + m_impl(*this), + m_requests(10), + m_PopupClasses(1), + m_OutMessages(3, PtrKeySortT), + m_bAutoHistorySync(this, "AutoSync", true), + m_bUseHostnameAsPlace(this, "UseHostName", true), + m_bUseBBCodes(this, "UseBBCodes", true), + m_wstrCListGroup(this, DBKEY_GROUP, L"Teams"), + m_wstrPlace(this, "Place", L""), + m_iMood(this, "Mood", 0), + m_wstrMoodEmoji(this, "MoodEmoji", L""), + m_wstrMoodMessage(this, "XStatusMsg", L"") +{ + // create endpoint + m_szEndpoint = getMStringA("Endpoint"); + if (m_szEndpoint.IsEmpty()) { + m_szEndpoint = Utils_GenerateUUID(); + setString("Endpoint", m_szEndpoint); + } + + // network + NETLIBUSER nlu = {}; + nlu.flags = NUF_OUTGOING | NUF_INCOMING | NUF_HTTPCONNS | NUF_UNICODE; + nlu.szDescriptiveName.w = m_tszUserName; + nlu.szSettingsModule = m_szModuleName; + m_hNetlibUser = Netlib_RegisterUser(&nlu); + + CMStringA module(FORMAT, "%s.TRouter", m_szModuleName); + CMStringW descr(FORMAT, TranslateT("%s websocket connection"), m_tszUserName); + nlu.szSettingsModule = module.GetBuffer(); + nlu.flags = NUF_INCOMING | NUF_OUTGOING | NUF_UNICODE; + nlu.szDescriptiveName.w = descr.GetBuffer(); + m_hTrouterNetlibUser = Netlib_RegisterUser(&nlu); + + CreateProtoService(PS_GETAVATARINFO, &CTeamsProto::SvcGetAvatarInfo); + CreateProtoService(PS_GETAVATARCAPS, &CTeamsProto::SvcGetAvatarCaps); + CreateProtoService(PS_GETMYAVATAR, &CTeamsProto::SvcGetMyAvatar); + CreateProtoService(PS_SETMYAVATAR, &CTeamsProto::SvcSetMyAvatar); + + CreateProtoService(PS_OFFLINEFILE, &CTeamsProto::SvcOfflineFile); + + CreateProtoService(PS_MENU_REQAUTH, &CTeamsProto::OnRequestAuth); + CreateProtoService(PS_MENU_GRANTAUTH, &CTeamsProto::OnGrantAuth); + + CreateProtoService(PS_MENU_LOADHISTORY, &CTeamsProto::SvcLoadHistory); + CreateProtoService(PS_EMPTY_SRV_HISTORY, &CTeamsProto::SvcEmptyHistory); + + HookProtoEvent(ME_OPT_INITIALISE, &CTeamsProto::OnOptionsInit); + + CreateDirectoryTreeW(GetAvatarPath()); + + // sounds + g_plugin.addSound("skype_inc_call", L"SkypeWeb", LPGENW("Incoming call")); + g_plugin.addSound("skype_call_canceled", L"SkypeWeb", LPGENW("Incoming call canceled")); + + InitGroupChatModule(); +} + +CTeamsProto::~CTeamsProto() +{ + UninitPopups(); +} + +void CTeamsProto::OnEventDeleted(MCONTACT hContact, MEVENT hDbEvent, int flags) +{ + if (!hContact || !(flags & CDF_DEL_HISTORY)) + return; + + DB::EventInfo dbei(hDbEvent, false); + if (dbei.szId) { + auto *pReq = new AsyncHttpRequest(REQUEST_DELETE, HOST_CHATS, "/users/ME/conversations/" + mir_urlEncode(getId(hContact)) + "/messages/" + dbei.szId); + pReq->AddAuthentication(this); + pReq->AddHeader("Origin", "https://web.skype.com"); + pReq->AddHeader("Referer", "https://web.skype.com/"); + PushRequest(pReq); + } +} + +void CTeamsProto::OnEventEdited(MCONTACT hContact, MEVENT, const DBEVENTINFO &dbei) +{ + if (dbei.szId) + SendServerMsg(hContact, dbei.pBlob, _atoi64(dbei.szId)); +} + +void CTeamsProto::OnModulesLoaded() +{ + setAllContactStatuses(ID_STATUS_OFFLINE, false); + + HookProtoEvent(ME_MSG_PRECREATEEVENT, &CTeamsProto::OnPreCreateMessage); + + InitPopups(); +} + +void CTeamsProto::OnShutdown() +{ + StopQueue(); + StopTrouter(); +} + +INT_PTR CTeamsProto::GetCaps(int type, MCONTACT) +{ + switch (type) { + case PFLAGNUM_1: + return PF1_IM | PF1_AUTHREQ | PF1_CHAT | PF1_BASICSEARCH | PF1_MODEMSG | PF1_FILE | PF1_SERVERCLIST; + case PFLAGNUM_2: + case PFLAGNUM_3: + return PF2_ONLINE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND; + case PFLAGNUM_4: + return PF4_NOAUTHDENYREASON | PF4_SUPPORTTYPING | PF4_AVATARS | PF4_IMSENDOFFLINE | PF4_OFFLINEFILES | PF4_SERVERMSGID | PF4_SERVERFORMATTING; + case PFLAG_UNIQUEIDTEXT: + return (INT_PTR)TranslateT("Teams ID"); + } + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +MCONTACT CTeamsProto::AddToList(int, PROTOSEARCHRESULT *psr) +{ + debugLogA(__FUNCTION__); + + if (psr->id.a == nullptr) + return NULL; + + MCONTACT hContact; + if (psr->flags & PSR_UNICODE) + hContact = AddContact(T2Utf(psr->id.w), T2Utf(psr->nick.w)); + else + hContact = AddContact(psr->id.a, psr->nick.a); + + return hContact; +} + +MCONTACT CTeamsProto::AddToListByEvent(int, int, MEVENT hDbEvent) +{ + debugLogA(__FUNCTION__); + + DB::EventInfo dbei(hDbEvent); + if (!dbei) + return NULL; + if (mir_strcmp(dbei.szModule, m_szModuleName)) + return NULL; + if (dbei.eventType != EVENTTYPE_AUTHREQUEST) + return NULL; + + DB::AUTH_BLOB blob(dbei.pBlob); + return AddContact(blob.get_email(), blob.get_nick()); +} + +int CTeamsProto::Authorize(MEVENT hDbEvent) +{ + MCONTACT hContact = GetContactFromAuthEvent(hDbEvent); + if (hContact == INVALID_CONTACT_ID) + return 1; + + PushRequest(new AsyncHttpRequest(REQUEST_POST, HOST_CONTACTS, "/users/SELF/invites/" + mir_urlEncode(getId(hContact)) + "/accept")); + return 0; +} + +int CTeamsProto::AuthDeny(MEVENT hDbEvent, const wchar_t *) +{ + MCONTACT hContact = GetContactFromAuthEvent(hDbEvent); + if (hContact == INVALID_CONTACT_ID) + return 1; + + PushRequest(new AsyncHttpRequest(REQUEST_POST, HOST_CONTACTS, "/users/SELF/invites/" + mir_urlEncode(getId(hContact)) + "/decline")); + return 0; +} + +int CTeamsProto::AuthRecv(MCONTACT, DB::EventInfo &dbei) +{ + return Proto_AuthRecv(m_szModuleName, dbei); +} + +int CTeamsProto::AuthRequest(MCONTACT hContact, const wchar_t *szMessage) +{ + if (hContact == INVALID_CONTACT_ID) + return 1; + + auto *pReq = new AsyncHttpRequest(REQUEST_PUT, HOST_CONTACTS, "/users/SELF/contacts"); + + JSONNode node; + node << CHAR_PARAM("mri", getId(hContact)); + if (mir_wstrlen(szMessage)) + node << WCHAR_PARAM("greeting", szMessage); + pReq->m_szParam = node.write().c_str(); + + PushRequest(pReq); + return 0; +} + +int CTeamsProto::GetInfo(MCONTACT hContact, int) +{ + if (isChatRoom(hContact)) + return 1; + + GetProfileInfo(hContact); + return 0; +} + +int CTeamsProto::SendMsg(MCONTACT hContact, MEVENT, const char *szMessage) +{ + return SendServerMsg(hContact, szMessage); +} + +int CTeamsProto::SetStatus(int iNewStatus) +{ + if (iNewStatus == m_iDesiredStatus) + return 0; + + debugLogA(__FUNCTION__ ": changing status from %i to %i", m_iStatus, iNewStatus); + + int old_status = m_iStatus; + m_iDesiredStatus = iNewStatus; + + if (iNewStatus == ID_STATUS_OFFLINE) { + m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; + StopQueue(); + StopTrouter(); + + ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, ID_STATUS_OFFLINE); + + if (!Miranda_IsTerminated()) + setAllContactStatuses(ID_STATUS_OFFLINE, false); + return 0; + } + + if (m_iStatus == ID_STATUS_OFFLINE) + Login(); + else + SetServerStatus(m_iDesiredStatus); + + return 0; +} + +int CTeamsProto::UserIsTyping(MCONTACT hContact, int iState) +{ + JSONNode node; + node << INT64_PARAM("clientmessageid", getRandomId()) << CHAR_PARAM("contenttype", "Application/Message") << CHAR_PARAM("content", "") + << CHAR_PARAM("messagetype", (iState == PROTOTYPE_SELFTYPING_ON) ? "Control/Typing" : "Control/ClearTyping"); + + auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_CHATS, "/users/ME/conversations/" + mir_urlEncode(getId(hContact)) + "/messages"); + pReq->m_szParam = node.write().c_str(); + PushRequest(pReq); + return 0; +} + +int CTeamsProto::RecvContacts(MCONTACT hContact, DB::EventInfo &dbei) +{ + PROTOSEARCHRESULT **isrList = (PROTOSEARCHRESULT **)dbei.pBlob; + + int nCount = dbei.cbBlob; + + uint32_t cbBlob = 0; + for (int i = 0; i < nCount; i++) + cbBlob += int(/*mir_wstrlen(isrList[i]->nick.w)*/0 + 2 + mir_wstrlen(isrList[i]->id.w)); + + char *pBlob = (char *)mir_calloc(cbBlob); + char *pCurBlob = pBlob; + + for (int i = 0; i < nCount; i++) { + pCurBlob += mir_strlen(pCurBlob) + 1; + + mir_strcpy(pCurBlob, _T2A(isrList[i]->id.w)); + pCurBlob += mir_strlen(pCurBlob) + 1; + } + + dbei.szModule = m_szModuleName; + dbei.eventType = EVENTTYPE_CONTACTS; + dbei.cbBlob = cbBlob; + dbei.pBlob = pBlob; + db_event_add(hContact, &dbei); + + mir_free(pBlob); + return 0; +} diff --git a/protocols/Teams/src/teams_proto.h b/protocols/Teams/src/teams_proto.h new file mode 100644 index 0000000000..921a8e1ef1 --- /dev/null +++ b/protocols/Teams/src/teams_proto.h @@ -0,0 +1,386 @@ +#define TEAMS_CLIENT_ID "8ec6bc83-69c8-4392-8f08-b3c986009232" +#define TEAMS_CLIENTINFO_NAME "skypeteams" +#define TEAMS_CLIENTINFO_VERSION "49/24062722442" + +#define TEAMS_BASE_HOST "teams.live.com" + +#define TEAMS_USER_AGENT "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0 Teams/24165.1410.2974.6689/49" + +#define DBKEY_ID "id" +#define DBKEY_GROUP "DefaultGroup" +#define DBKEY_RTOKEN "RefreshToken" + +struct COwnMessage +{ + COwnMessage(int _1, int64_t _2) : + hMessage(_1), + hClientMessageId(_2) + {} + + COwnMessage(const char *pszText) : + hMessage(0), + hClientMessageId(0), + szMessage(mir_strdup(pszText)) + {} + + int hMessage; + int64_t hClientMessageId, iTimestamp = -1; + ptrA szMessage; +}; + +struct CSkypeTransfer +{ + CMStringA docId, fileName, fileType, url; + int iFileSize = 0, iWidth = -1, iHeight = -1; +}; + +class CTeamsProto : public PROTO<CTeamsProto> +{ + friend class COptionsMainDlg; + friend class CDeviceCodeDlg; + + friend class CSkypeOptionsMain; + friend class CSkypeGCCreateDlg; + friend class CSkypeInviteDlg; + friend class CMoodDialog; + friend class CDeviceCodeDlg; + + class CTeamsProtoImpl + { + friend class CTeamsProto; + CTeamsProto &m_proto; + + CTimer m_heartBeat, m_loginPoll; + void OnHeartBeat(CTimer *) + { + m_proto.TRouterSendJson("ping"); + } + void OnLoginPoll(CTimer *) + { + m_proto.LoginPoll(); + } + + CTeamsProtoImpl(CTeamsProto &pro) : + m_proto(pro), + m_heartBeat(Miranda_GetSystemWindow(), UINT_PTR(this) + 1), + m_loginPoll(Miranda_GetSystemWindow(), UINT_PTR(this) + 2) + { + m_heartBeat.OnEvent = Callback(this, &CTeamsProtoImpl::OnHeartBeat); + m_loginPoll.OnEvent = Callback(this, &CTeamsProtoImpl::OnLoginPoll); + } + } m_impl; + +public: + // constructor + CTeamsProto(const char *protoName, const wchar_t *userName); + ~CTeamsProto(); + + ////////////////////////////////////////////////////////////////////////////////////// + // Virtual functions + + MCONTACT AddToList(int flags, PROTOSEARCHRESULT* psr) override; + MCONTACT AddToListByEvent(int flags, int iContact, MEVENT hDbEvent) override; + int AuthRequest(MCONTACT hContact, const wchar_t* szMessage) override; + int Authorize(MEVENT hDbEvent) override; + int AuthDeny(MEVENT hDbEvent, const wchar_t* szReason) override; + int AuthRecv(MCONTACT hContact, DB::EventInfo &dbei) override; + INT_PTR GetCaps(int type, MCONTACT hContact = NULL) override; + int GetInfo(MCONTACT hContact, int infoType) override; + HANDLE SearchBasic(const wchar_t* id) override; + int SendMsg(MCONTACT hContact, MEVENT hReplyEvent, const char* msg) override; + int SetStatus(int iNewStatus) override; + int UserIsTyping(MCONTACT hContact, int type) override; + int RecvContacts(MCONTACT hContact, DB::EventInfo &dbei) override; + HANDLE SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) override; + + void OnBuildProtoMenu(void) override; + bool OnContactDeleted(MCONTACT, uint32_t flags) override; + MWindow OnCreateAccMgrUI(MWindow) override; + void OnEventEdited(MCONTACT hContact, MEVENT hDbEvent, const DBEVENTINFO &dbei) override; + void OnEventDeleted(MCONTACT hContact, MEVENT hDbEvent, int flags) override; + void OnMarkRead(MCONTACT, MEVENT) override; + void OnModulesLoaded() override; + void OnReceiveOfflineFile(DB::EventInfo &dbei, DB::FILE_BLOB &blob) override; + void OnShutdown() override; + + // menus + static void InitMenus(); + + // popups + void InitPopups(); + void UninitPopups(); + + // search + + ////////////////////////////////////////////////////////////////////////////////////// + // settings + + CMOption<bool> m_bAutoHistorySync; + CMOption<bool> m_bUseBBCodes; + + CMOption<bool> m_bUseHostnameAsPlace; + CMOption<wchar_t*> m_wstrPlace; + + CMOption<wchar_t*> m_wstrCListGroup; + + CMOption<uint8_t> m_iMood; + CMOption<wchar_t*> m_wstrMoodMessage, m_wstrMoodEmoji; + + ////////////////////////////////////////////////////////////////////////////////////// + // other data + + int m_iPollingId, m_iMessageId = 1; + CMStringA m_szSkypename, m_szMyname, m_szOwnSkypeId, m_szSkypeToken, m_szEndpoint, m_szApiCookie; + MCONTACT m_hMyContact; + + __forceinline CMStringA getId(MCONTACT hContact) { + return getMStringA(hContact, DBKEY_ID); + } + +private: + bool m_bHistorySynced; + + static std::map<std::wstring, std::wstring> languages; + + LIST<void> m_PopupClasses; + + // avatars + bool ReceiveAvatar(MCONTACT hContact); + void OnReceiveAvatar(MHttpResponse *response, AsyncHttpRequest *pRequest); + + void SetAvatarUrl(MCONTACT hContact, const CMStringW &tszUrl); + + void ReloadAvatarInfo(MCONTACT hContact); + void GetAvatarFileName(MCONTACT hContact, wchar_t *pszDest, size_t cbLen); + + INT_PTR __cdecl SvcGetAvatarInfo(WPARAM, LPARAM); + INT_PTR __cdecl SvcGetAvatarCaps(WPARAM, LPARAM); + INT_PTR __cdecl SvcGetMyAvatar(WPARAM, LPARAM); + + void OnSentAvatar(MHttpResponse *response, AsyncHttpRequest *pRequest); + INT_PTR __cdecl SvcSetMyAvatar(WPARAM, LPARAM); + + // chats + void InitGroupChatModule(); + + int __cdecl OnGroupChatEventHook(WPARAM, LPARAM lParam); + int __cdecl OnGroupChatMenuHook(WPARAM, LPARAM lParam); + INT_PTR __cdecl OnJoinChatRoom(WPARAM hContact, LPARAM); + INT_PTR __cdecl OnLeaveChatRoom(WPARAM hContact, LPARAM); + + void OnGetChatInfo(MHttpResponse *response, AsyncHttpRequest *pRequest); + void GetChatInfo(const wchar_t *chatId); + + void OnGetChatMembers(MHttpResponse *response, AsyncHttpRequest *pRequest); + void GetChatMembers(const LIST<char> &ids, SESSION_INFO *si); + + SESSION_INFO *StartChatRoom(const wchar_t *tid, const wchar_t *tname, const char *pszVersion = nullptr); + + bool OnChatEvent(const JSONNode &node); + wchar_t *GetChatContactNick(SESSION_INFO *si, const wchar_t *id, const wchar_t *name = nullptr, bool *isQualified = nullptr); + + bool AddChatContact(SESSION_INFO *si, const wchar_t *id, const wchar_t *role, bool isChange = false); + void RemoveChatContact(SESSION_INFO *si, const wchar_t *id, const wchar_t *initiator = L""); + void SendChatMessage(SESSION_INFO *si, const wchar_t *tszMessage); + + void InviteUserToChat(const char *chatId, const char *skypename, const char *role); + void KickChatUser(const char *chatId, const char *userId); + void SetChatProperty(const char *chatId, const char *propname, const char *value); + void SetChatStatus(MCONTACT hContact, int iStatus); + + bool ParseMessage(const JSONNode &node, DB::EventInfo &dbei); + + // contacts + void LoadContactsAuth(MHttpResponse *response, AsyncHttpRequest *pRequest); + void OnGotContactsInfo(MHttpResponse *response, AsyncHttpRequest *pRequest); + + void GetShortInfo(const OBJLIST<char> &ids); + void RefreshContactsInfo(); + void SetContactStatus(MCONTACT hContact, uint16_t status); + + MCONTACT FindContact(const char *skypeId); + MCONTACT FindContact(const wchar_t *skypeId); + + MCONTACT AddContact(const char *skypename, const char *nick, bool isTemporary = false); + + MCONTACT GetContactFromAuthEvent(MEVENT hEvent); + + INT_PTR __cdecl BlockContact(WPARAM hContact, LPARAM); + void OnBlockContact(MHttpResponse *response, AsyncHttpRequest *pRequest); + + INT_PTR __cdecl UnblockContact(WPARAM hContact, LPARAM); + void OnUnblockContact(MHttpResponse *response, AsyncHttpRequest *pRequest); + + // files + void SendFile(CFileUploadParam *fup); + void OnASMObjectCreated(MHttpResponse *response, AsyncHttpRequest *pRequest); + void OnASMObjectUploaded(MHttpResponse *response, AsyncHttpRequest *pRequest); + + void __cdecl ReceiveFileThread(void *param); + + INT_PTR __cdecl SvcOfflineFile(WPARAM, LPARAM); + + // history + void FetchMissingHistory(const JSONNode &node, MCONTACT); + void GetServerHistory(MCONTACT hContact, int pageSize, int64_t timestamp, bool bOperative); + void RefreshConversations(); + + void OnGetServerHistory(MHttpResponse *response, AsyncHttpRequest *pRequest); + void OnSyncConversations(MHttpResponse *response, AsyncHttpRequest *pRequest); + + // http queue + bool m_isTerminated = true; + mir_cs m_requestQueueLock; + LIST<AsyncHttpRequest> m_requests; + MEventHandle m_hRequestQueueEvent; + HANDLE m_hRequestQueueThread; + CMStringA m_szAccessToken, m_szSubstrateToken; + + void __cdecl WorkerThread(void *); + + void StartQueue(); + void StopQueue(); + + MHttpResponse *DoSend(AsyncHttpRequest *request); + + void Execute(AsyncHttpRequest *request); + void PushRequest(AsyncHttpRequest *request); + + // login + CMStringW m_wszUserCode; + CMStringA m_szDeviceCode, m_szDeviceCookie, m_szVerificationUrl; + time_t m_iLoginExpires; + + void Login(); + void LoggedIn(); + void LoginPoll(); + void LoginError(); + + void OnEndpointCreated(MHttpResponse *response, AsyncHttpRequest *pRequest); + void OnReceiveApiCookie(MHttpResponse *response, AsyncHttpRequest *pRequest); + + void OauthRefreshServices(); + void RefreshToken(const char *pszScope, AsyncHttpRequest::MTHttpRequestHandler pFunc); + + void OnReceiveSkypeToken(MHttpResponse *response, AsyncHttpRequest *pRequest); + void OnReceiveDevicePoll(MHttpResponse *response, AsyncHttpRequest *pRequest); + void OnReceiveDeviceToken(MHttpResponse *response, AsyncHttpRequest *pRequest); + void OnRefreshAccessToken(MHttpResponse *response, AsyncHttpRequest *pRequest); + void OnRefreshSkypeToken(MHttpResponse *response, AsyncHttpRequest *pRequest); + void OnRefreshSubstrate(MHttpResponse *response, AsyncHttpRequest *pRequest); + + // menus + static HGENMENU ContactMenuItems[CMI_MAX]; + int OnPrebuildContactMenu(WPARAM hContact, LPARAM); + static int PrebuildContactMenu(WPARAM hContact, LPARAM lParam); + + // messages + mir_cs m_lckOutMessagesList; + LIST<COwnMessage> m_OutMessages; + + void OnMessageSent(MHttpResponse *response, AsyncHttpRequest *pRequest); + int SendServerMsg(MCONTACT hContact, const char *szMessage, int64_t iMessageId = 0); + + int __cdecl OnPreCreateMessage(WPARAM, LPARAM lParam); + + void ProcessContactRecv(MCONTACT hContact, const char *szContent, DB::EventInfo &dbei); + void ProcessFileRecv(MCONTACT hContact, const char *szContent, DB::EventInfo &dbei); + + // options + int __cdecl OnOptionsInit(WPARAM wParam, LPARAM lParam); + + // profile + void OnGetProfileInfo(MHttpResponse *response, AsyncHttpRequest *pRequest); + void GetProfileInfo(MCONTACT hContact); + + void UpdateProfileDisplayName(const JSONNode &root, MCONTACT hContact = NULL); + void UpdateProfileGender(const JSONNode &root, MCONTACT hContact = NULL); + void UpdateProfileBirthday(const JSONNode &root, MCONTACT hContact = NULL); + void UpdateProfileCountry(const JSONNode &node, MCONTACT hContact = NULL); + void UpdateProfileEmails(const JSONNode &root, MCONTACT hContact = NULL); + void UpdateProfileAvatar(const JSONNode &root, MCONTACT hContact = NULL); + + // search + void OnSearch(MHttpResponse *response, AsyncHttpRequest *pRequest); + + // server requests + void OnStatusChanged(MHttpResponse *response, AsyncHttpRequest *pRequest); + void SetServerStatus(int iStatus); + + void CreateContactSubscription(); + + // utils + __forceinline bool IsOnline() const + { return (m_iStatus > ID_STATUS_OFFLINE); + } + + bool IsMe(const wchar_t *str); + bool IsMe(const char *str); + + int64_t getLastTime(MCONTACT); + void setLastTime(MCONTACT, int64_t); + + CMStringW RemoveHtml(const CMStringW &src); + + void ShowNotification(const wchar_t *message, MCONTACT hContact = NULL); + void ShowNotification(const wchar_t *caption, const wchar_t *message, MCONTACT hContact = NULL, int type = 0); + static bool IsFileExists(std::wstring path); + + static LRESULT CALLBACK PopupDlgProcCall(HWND hPopup, UINT uMsg, WPARAM wParam, LPARAM lParam); + + void SetString(MCONTACT hContact, const char *pszSetting, const JSONNode &node); + + CMStringW ChangeTopicForm(); + + // services + INT_PTR __cdecl OnRequestAuth(WPARAM hContact, LPARAM); + INT_PTR __cdecl OnGrantAuth(WPARAM hContact, LPARAM); + INT_PTR __cdecl SvcLoadHistory(WPARAM hContact, LPARAM); + INT_PTR __cdecl SvcEmptyHistory(WPARAM hContact, LPARAM); + INT_PTR __cdecl SvcCreateChat(WPARAM, LPARAM); + INT_PTR __cdecl ParseSkypeUriService(WPARAM, LPARAM lParam); + + // trouter +public: + void TRouterProcess(const char *str); + +private: + HNETLIBUSER m_hTrouterNetlibUser; + CMStringA m_szTrouterUrl, m_szTrouterSurl; + WebSocket<CTeamsProto> *m_ws; + MHttpHeaders m_connectParams; + int iCommandId; + + void ProcessEvent(const JSONNode &node); + void ProcessNewMessage(const JSONNode &node); + void ProcessUserPresence(const JSONNode &node); + void ProcessThreadUpdate(const JSONNode &node); + void ProcessServerMessage(const std::string &szName, int packetId, const JSONNode &args); + void ProcessConversationUpdate(const JSONNode &node); + + void __cdecl GatewayThread(void *); + void GatewayThreadWorker(); + + void TRouterSendJson(const char *szName, const JSONNode *node = nullptr, int iReplyTo = -1); + void TRouterSendJson(const JSONNode &node, int iReplyTo = -1); + + void TRouterSendActive(bool bActive, int iReplyTo = -1); + void TRouterRegister(); + void TRouterRegister(const char *pszAppId, const char *pszKey, const char *pszPath, const char *pszContext); + + void StartTrouter(); + void StopTrouter(); + + void OnTrouterInfo(MHttpResponse *response, AsyncHttpRequest *pRequest); + void OnTrouterSession(MHttpResponse *response, AsyncHttpRequest *pRequest); +}; + +typedef CProtoDlgBase<CTeamsProto> CTeamsDlgBase; + +struct CMPlugin : public ACCPROTOPLUGIN<CTeamsProto> +{ + CMPlugin(); + + int Load() override; + int Unload() override; +}; diff --git a/protocols/Teams/src/teams_search.cpp b/protocols/Teams/src/teams_search.cpp new file mode 100644 index 0000000000..c175cbea37 --- /dev/null +++ b/protocols/Teams/src/teams_search.cpp @@ -0,0 +1,62 @@ +/* +Copyright (c) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +HANDLE CTeamsProto::SearchBasic(const wchar_t *id) +{ + debugLogA("CTeamsProto::OnSearchBasicThread"); + if (!IsOnline()) + return 0; + + auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_TEAMS_API, "/users/searchV2?includeDLs=true&includeBots=true&enableGuest=true&source=newChat&skypeTeamsInfo=true", &CTeamsProto::OnSearch); + pReq->m_szParam = mir_urlEncode(T2Utf(id)); + PushRequest(pReq); + + return (HANDLE)1; +} + +void CTeamsProto::OnSearch(MHttpResponse *response, AsyncHttpRequest*) +{ + debugLogA(__FUNCTION__); + + TeamsReply reply(response); + if (reply.error()) { + ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)1, 0); + return; + } + + auto &root = reply.data(); + const JSONNode &items = root["results"].as_array(); + for (auto &it : items) { + const JSONNode &item = it["nodeProfileData"]; + + std::string skypeId = item["skypeId"].as_string(); + if (UrlToSkypeId(skypeId.c_str()).IsEmpty()) + skypeId = "8:" + skypeId; + + std::string name = item["name"].as_string(); + + PROTOSEARCHRESULT psr = { sizeof(psr) }; + psr.flags = PSR_UTF8; + psr.id.a = const_cast<char *>(skypeId.c_str()); + psr.nick.a = const_cast<char *>(name.c_str()); + ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_DATA, (HANDLE)1, (LPARAM)&psr); + } + + ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)1, 0); +} diff --git a/protocols/Teams/src/teams_server.cpp b/protocols/Teams/src/teams_server.cpp new file mode 100644 index 0000000000..3345e1f265 --- /dev/null +++ b/protocols/Teams/src/teams_server.cpp @@ -0,0 +1,104 @@ +/* +Copyright (C) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +///////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::OnReceiveApiCookie(MHttpResponse *response, AsyncHttpRequest *) +{ + if (response == nullptr) { + ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, 1001); + SetStatus(ID_STATUS_OFFLINE); + return; + } + + m_szApiCookie = response->GetCookies(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::OnStatusChanged(MHttpResponse *response, AsyncHttpRequest *) +{ + if (response == nullptr || response->resultCode != 200) { + debugLogA(__FUNCTION__ ": failed to change status"); + ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, 1001); + SetStatus(ID_STATUS_OFFLINE); + return; + } + + int oldStatus = m_iStatus; + m_iStatus = m_iDesiredStatus; + ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus); +} + +void CTeamsProto::SetServerStatus(int iStatus) +{ + const char *pszAvailability; + switch (iStatus) { + case ID_STATUS_OFFLINE: + pszAvailability = "Offline"; + break; + case ID_STATUS_NA: + case ID_STATUS_AWAY: + pszAvailability = "Away"; + break; + case ID_STATUS_DND: + pszAvailability = "DoNotDisturb"; + break; + case ID_STATUS_OCCUPIED: + pszAvailability = "Busy"; + break; + default: + pszAvailability = "Available"; + } + + JSONNode node(JSON_NODE); + node << CHAR_PARAM("availability", pszAvailability); + + auto *pReq = new AsyncHttpRequest(REQUEST_PUT, HOST_PRESENCE, "/me/forceavailability", &CTeamsProto::OnStatusChanged); + pReq->m_szParam = node.write().c_str(); + PushRequest(pReq); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::CreateContactSubscription() +{ + CMStringA szUrl = m_szTrouterSurl; + if (szUrl[szUrl.GetLength() - 1] != '/') + szUrl += "/"; + szUrl += "TeamsUnifiedPresenceService"; + + JSONNode listAdd(JSON_ARRAY); listAdd.set_name("subscriptionsToAdd"); + for (auto &hContact : AccContacts()) + if (!isChatRoom(hContact)) { + JSONNode contact; + contact << CHAR_PARAM("mri", getId(hContact)); + listAdd << contact; + } + + JSONNode listRemove(JSON_ARRAY); listRemove.set_name("subscriptionsToRemove"); + + JSONNode node; + node << CHAR_PARAM("trouterUri", szUrl) << BOOL_PARAM("shouldPurgePreviousSubscriptions", true) + << listAdd << listRemove; + + auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_PRESENCE, "/pubsub/subscriptions/" + m_szEndpoint); + pReq->m_szParam = node.write().c_str(); + PushRequest(pReq); +} diff --git a/protocols/Teams/src/teams_trouter.cpp b/protocols/Teams/src/teams_trouter.cpp new file mode 100644 index 0000000000..0e54b8edb6 --- /dev/null +++ b/protocols/Teams/src/teams_trouter.cpp @@ -0,0 +1,342 @@ +/* +Copyright (C) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +#define TEAMS_TROUTER_TTL 86400 +#define TEAMS_TROUTER_TCCV "2024.23.01.2" + +void CTeamsProto::OnTrouterSession(MHttpResponse *response, AsyncHttpRequest *pRequest) +{ + if (!response || response->resultCode != 200) { + LoginError(); + return; + } + + int iStart = 0; + CMStringA szId = response->body.Tokenize(":", iStart); + m_szTrouterUrl = pRequest->m_szUrl; + m_szTrouterUrl.Replace("socket.io/1/", "socket.io/1/websocket/" + szId + "/"); + ForkThread(&CTeamsProto::GatewayThread); +} + +void CTeamsProto::OnTrouterInfo(MHttpResponse *response, AsyncHttpRequest *) +{ + TeamsReply reply(response); + if (reply.error()) { + LoginError(); + return; + } + + auto &root = reply.data(); + m_szTrouterSurl = root["surl"].as_mstring(); + CMStringA ccid = root["ccid"].as_mstring(); + CMStringA szUrl = root["socketio"].as_mstring(); + szUrl += "socket.io/1/"; + + CreateContactSubscription(); + + auto *pReq = new AsyncHttpRequest(REQUEST_GET, HOST_OTHER, szUrl, &CTeamsProto::OnTrouterSession); + pReq << CHAR_PARAM("v", "v4"); + + m_connectParams.destroy(); + for (auto &it : root["connectparams"]) { + m_connectParams.AddHeader(it.name(), it.as_string().c_str()); + pReq << CHAR_PARAM(it.name(), it.as_string().c_str()); + } + + pReq << CHAR_PARAM("tc", "{\"cv\":\"" TEAMS_TROUTER_TCCV "\",\"ua\":\"TeamsCDL\",\"hr\":\"\",\"v\":\"" TEAMS_CLIENTINFO_VERSION "\"}") + << CHAR_PARAM("con_num", "1234567890123_1") << CHAR_PARAM("epid", m_szEndpoint) << BOOL_PARAM("auth", true) << INT_PARAM("timeout", 40); + if (!ccid.IsEmpty()) + pReq << CHAR_PARAM("ccid", ccid); + PushRequest(pReq); +} + +void CTeamsProto::StartTrouter() +{ + auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_OTHER, "https://go.trouter.skype.com/v4/a", &CTeamsProto::OnTrouterInfo); + pReq->m_szUrl.AppendFormat("?epid=%s", m_szEndpoint.c_str()); + pReq->AddHeader("x-skypetoken", m_szSkypeToken); + pReq->flags |= NLHRF_NODUMPHEADERS; + PushRequest(pReq); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CTeamsProto::StopTrouter() +{ + m_impl.m_heartBeat.StopSafe(); + + if (m_ws) { + TRouterSendActive(false); + m_ws->terminate(); + m_ws = nullptr; + } +} + +void CTeamsProto::GatewayThread(void *) +{ + while (!m_isTerminated) + GatewayThreadWorker(); +} + +void CTeamsProto::GatewayThreadWorker() +{ + m_ws = nullptr; + + MHttpHeaders headers; + headers.AddHeader("x-skypetoken", m_szSkypeToken); + headers.AddHeader("User-Agent", TEAMS_USER_AGENT); + + WebSocket<CTeamsProto> ws(this); + NLHR_PTR pReply(ws.connect(m_hTrouterNetlibUser, m_szTrouterUrl, &headers)); + if (pReply) { + if (pReply->resultCode == 101) { + m_ws = &ws; + + iCommandId = 1; + m_impl.m_heartBeat.StartSafe(30000); + + debugLogA("Websocket connection succeeded"); + ws.run(); + } + else debugLogA("websocket connection failed: %d", pReply->resultCode); + } + else debugLogA("websocket connection failed"); + + StopTrouter(); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// TRouter send + +void CTeamsProto::TRouterSendJson(const JSONNode &node, int iReplyTo) +{ + CMStringA szJson; + if (iReplyTo == -1) { + iCommandId++; + szJson.Format("5:%d+::", iCommandId); + } + else szJson.Format("5:%d+::", iReplyTo); + szJson += node.write().c_str(); + + if (m_ws) + m_ws->sendText(szJson.c_str()); +} + +void CTeamsProto::TRouterSendJson(const char *szName, const JSONNode *node, int iReplyTo) +{ + JSONNode payload, args(JSON_ARRAY); + payload << CHAR_PARAM("name", szName); + if (node) { + if (mir_strcmp(node->name(), "args")) { + args.set_name("args"); + args << *node; + payload << args; + } + else payload << *node; + } + + CMStringA szJson; + if (iReplyTo == -1) { + iCommandId++; + szJson.Format("5:%d+::", iCommandId); + } + else szJson.Format("5:%d+::", iReplyTo); + szJson += payload.write().c_str(); + + if (m_ws) + m_ws->sendText(szJson.c_str()); +} + +static char szSuffix[4] = { 'A', 'g', 'Q', 'w' }; + +void CTeamsProto::TRouterSendActive(bool bActive, int iReplyTo) +{ + CMStringA cv; + srand(time(0)); + for (int i = 0; i < 21; i++) + cv.AppendChar('a' + rand() % 26); + cv.AppendChar(szSuffix[rand() % 4]); + cv += ".0.1"; + + JSONNode payload; + payload << CHAR_PARAM("state", bActive ? "active" : "inactive") << CHAR_PARAM("cv", cv); + TRouterSendJson("user.activity", &payload, iReplyTo); +} + +void CTeamsProto::TRouterRegister() +{ + TRouterRegister("NextGenCalling", "DesktopNgc_2.3:SkypeNgc", m_szTrouterSurl + "NGCallManagerWin", nullptr); + TRouterRegister("SkypeSpacesWeb", "SkypeSpacesWeb_2.3", m_szTrouterSurl + "SkypeSpacesWeb", nullptr); + TRouterRegister("TeamsCDLWebWorker", "TeamsCDLWebWorker_2.3", m_szTrouterSurl, ""); + TRouterRegister("TeamsCDLWebWorker", "TeamsCDLWebWorker_2.3", m_szTrouterSurl, "TFL"); +} + +void CTeamsProto::TRouterRegister(const char *pszAppId, const char *pszKey, const char *pszPath, const char *pszContext) +{ + JSONNode descr, reg, obj, trouter(JSON_ARRAY), transports; + descr.set_name("clientDescription"); + descr << CHAR_PARAM("appId", pszAppId) << CHAR_PARAM("aesKey", "") << CHAR_PARAM("languageId", "en-US") + << CHAR_PARAM("platform", "edge") << CHAR_PARAM("templateKey", pszKey) << CHAR_PARAM("platformUIVersion", TEAMS_CLIENTINFO_VERSION); + if (pszContext) + descr << CHAR_PARAM("productContext", pszContext); + + obj << CHAR_PARAM("context", "") << CHAR_PARAM("path", pszPath) << INT_PARAM("ttl", TEAMS_TROUTER_TTL); + trouter.set_name("TROUTER"); trouter << obj; + transports.set_name("transports"); transports << trouter; + + reg.set_name("registration"); + reg << descr << CHAR_PARAM("registrationId", m_szEndpoint) << CHAR_PARAM("nodeId", "") << transports; + + auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_OTHER, "https://edge.skype.com/registrar/prod/v2/registrations"); + pReq->flags |= NLHRF_NODUMPHEADERS; + pReq->AddHeader("Content-Type", "application/json"); + pReq->AddHeader("X-Skypetoken", m_szSkypeToken); + pReq->AddHeader("Authorization", "Bearer " + m_szAccessToken); + pReq->m_szParam = reg.write().c_str(); + PushRequest(pReq); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// TRouter receive + +void WebSocket<CTeamsProto>::process(const uint8_t *buf, size_t cbLen) +{ + Netlib_Dump(getConn(), buf, cbLen, false, 0); + + CMStringA payload((const char *)buf, (int)cbLen); + p->TRouterProcess(payload); +} + +static const char* skip3colons(const char *str, int *packet_id = nullptr) +{ + int nColons = 3; + for (const char *p = str; *p; p++) { + if (*p == ':') { + if (packet_id && nColons == 3) + *packet_id = atoi(p+1); + + if (--nColons == 0) + return p + 1; + } + } + return str; +} + +void CTeamsProto::TRouterProcess(const char *str) +{ + switch (*str) { + case '1': + TRouterRegister(); + break; + + case '3': + if (auto packet = JSONNode::parse(skip3colons(str))) { + std::string szBody(packet["body"].as_string()); + auto message = JSONNode::parse(szBody.c_str()); + if (message) { + Netlib_Logf(m_hTrouterNetlibUser, "Got event:\n%s", message.write_formatted().c_str()); + ProcessEvent(message); + } + + JSONNode reply, &old = packet["headers"], headers; headers.set_name("headers"); + headers << WCHAR_PARAM("MS-CV", old["MS-CV"].as_mstring()) << old["trouter-request"] << old["trouter-client"]; + reply << WCHAR_PARAM("id", packet["id"].as_mstring()) << INT_PARAM("status", 200) << headers << CHAR_PARAM("body", ""); + if (m_ws) + m_ws->sendText(("3:::" + reply.write()).c_str()); + } + break; + + case '5': + if (auto root = JSONNode::parse(skip3colons(str, &iCommandId))) { + std::string szName(root["name"].as_string()); + ProcessServerMessage(szName, iCommandId, root["args"]); + } + break; + } +} + +void CTeamsProto::ProcessEvent(const JSONNode &node) +{ + if (auto &presence = node["presence"]) { + for (auto &it : presence) + ProcessUserPresence(it); + return; + } + + auto szType = node["type"].as_string(); + if (szType == "EventMessage") { + auto &resource = node["resource"]; + auto szResourceType = node["resourceType"]; + if (szResourceType == "ConversationUpdate") + ProcessConversationUpdate(resource); + else if (szResourceType == "NewMessage") + ProcessNewMessage(resource); + } +} + +void CTeamsProto::ProcessUserPresence(const JSONNode &node) +{ + debugLogA(__FUNCTION__); + + CMStringA skypename = node["mri"].as_mstring(); + auto &presence = node["presence"]; + std::string status = presence["availability"].as_string(); + + if (!skypename.IsEmpty()) { + if (IsMe(skypename)) { + int iNewStatus = TeamsToMirandaStatus(status.c_str()); + if (iNewStatus == ID_STATUS_OFFLINE) + return; + + int old_status = m_iStatus; + m_iDesiredStatus = iNewStatus; + m_iStatus = iNewStatus; + if (old_status != iNewStatus) + ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, iNewStatus); + } + else if (MCONTACT hContact = FindContact(skypename)) { + SetContactStatus(hContact, TeamsToMirandaStatus(status.c_str())); + if (auto &p = presence["lastActiveTime"]) + setDword(hContact, "LastSeen", Utils_IsoToUnixTime(p.as_string().c_str())); + if (auto &p = presence["deviceType"]) + setWString(hContact, "MirVer", L"Teams (" + p.as_mstring() + L")"); + } + } +} + +void CTeamsProto::ProcessServerMessage(const std::string &szName, int packetId, const JSONNode &args) +{ + if (szName == "trouter.message_loss") + TRouterSendJson("trouter.processed_message_loss", &args, packetId); + + else if (szName == "trouter.connected") + TRouterSendActive(true, packetId); +} + +void CTeamsProto::ProcessConversationUpdate(const JSONNode &node) +{ + if (auto &properties = node["threadProperties"]) { + CMStringW wszId(node["id"].as_mstring()); + if (auto *si = Chat_Find(wszId, m_szModuleName)) + if (getMStringW(si->hContact, "Version") != properties["version"].as_mstring()) + GetChatInfo(wszId); + } +} + +void CTeamsProto::ProcessThreadUpdate(const JSONNode &) {} diff --git a/protocols/Teams/src/teams_utils.cpp b/protocols/Teams/src/teams_utils.cpp new file mode 100644 index 0000000000..4fcc168a34 --- /dev/null +++ b/protocols/Teams/src/teams_utils.cpp @@ -0,0 +1,774 @@ +/* +Copyright (c) 2025 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +#pragma warning(disable:4566) + +struct HtmlEntity +{ + const char *entity; + const char *symbol; +}; + +const HtmlEntity htmlEntities[] = +{ + { "AElig", "\u00C6" }, + { "Aacute", "\u00C1" }, + { "Acirc", "\u00C2" }, + { "Agrave", "\u00C0" }, + { "Alpha", "\u0391" }, + { "Aring", "\u00C5" }, + { "Atilde", "\u00C3" }, + { "Auml", "\u00C4" }, + { "Beta", "\u0392" }, + { "Ccedil", "\u00C7" }, + { "Chi", "\u03A7" }, + { "Dagger", "‡" }, + { "Delta", "\u0394" }, + { "ETH", "\u00D0" }, + { "Eacute", "\u00C9" }, + { "Ecirc", "\u00CA" }, + { "Egrave", "\u00C8" }, + { "Epsilon", "\u0395" }, + { "Eta", "\u0397" }, + { "Euml", "\u00CB" }, + { "Gamma", "\u0393" }, + { "Iacute", "\u00CD" }, + { "Icirc", "\u00CE" }, + { "Igrave", "\u00CC" }, + { "Iota", "\u0399" }, + { "Iuml", "\u00CF" }, + { "Kappa", "\u039A" }, + { "Lambda", "\u039B" }, + { "Mu", "\u039C" }, + { "Ntilde", "\u00D1" }, + { "Nu", "\u039D" }, + { "OElig", "\u0152" }, + { "Oacute", "\u00D3" }, + { "Ocirc", "\u00D4" }, + { "Ograve", "\u00D2" }, + { "Omega", "\u03A9" }, + { "Omicron", "\u039F" }, + { "Oslash", "\u00D8" }, + { "Otilde", "\u00D5" }, + { "Ouml", "\u00D6" }, + { "Phi", "\u03A6" }, + { "Pi", "\u03A0" }, + { "Prime", "\u2033" }, + { "Psi", "\u03A8" }, + { "Rho", "\u03A1" }, + { "Scaron", "Š" }, + { "Sigma", "Σ" }, + { "THORN", "Þ" }, + { "Tau", "Τ" }, + { "Theta", "Θ" }, + { "Uacute", "Ú" }, + { "Ucirc", "Û" }, + { "Ugrave", "Ù" }, + { "Upsilon", "Υ" }, + { "Uuml", "Ü" }, + { "Xi", "Ξ" }, + { "Yacute", "Ý" }, + { "Yuml", "Ÿ" }, + { "Zeta", "Ζ" }, + { "aacute", "á" }, + { "acirc", "â" }, + { "acute", "´" }, + { "aelig", "æ" }, + { "agrave", "à" }, + { "alefsym", "ℵ" }, + { "alpha", "α" }, + { "amp", "&" }, + { "and", "∧" }, + { "ang", "∠" }, + { "apos", "'" }, + { "aring", "å" }, + { "asymp", "≈" }, + { "atilde", "ã" }, + { "auml", "ä" }, + { "bdquo", "„" }, + { "beta", "β" }, + { "brvbar", "¦" }, + { "bull", "•" }, + { "cap", "∩" }, + { "ccedil", "ç" }, + { "cedil", "¸" }, + { "cent", "¢" }, + { "chi", "χ" }, + { "circ", "ˆ" }, + { "clubs", "♣" }, + { "cong", "≅" }, + { "copy", "©" }, + { "crarr", "↵" }, + { "cup", "∪" }, + { "curren", "¤" }, + { "dArr", "⇓" }, + { "dagger", "†" }, + { "darr", "↓" }, + { "deg", "°" }, + { "delta", "δ" }, + { "diams", "♦" }, + { "divide", "÷" }, + { "eacute", "é" }, + { "ecirc", "ê" }, + { "egrave", "è" }, + { "empty", "∅" }, + { "emsp", " " }, + { "ensp", " " }, + { "epsilon", "ε" }, + { "equiv", "≡" }, + { "eta", "η" }, + { "eth", "ð" }, + { "euml", "ë" }, + { "euro", "€" }, + { "exist", "∃" }, + { "fnof", "ƒ" }, + { "forall", "∀" }, + { "frac12", "½" }, + { "frac14", "¼" }, + { "frac34", "¾" }, + { "frasl", "⁄" }, + { "gamma", "γ" }, + { "ge", "≥" }, + { "gt", ">" }, + { "hArr", "⇔" }, + { "harr", "↔" }, + { "hearts", "♥" }, + { "hellip", "…" }, + { "iacute", "í" }, + { "icirc", "î" }, + { "iexcl", "¡" }, + { "igrave", "ì" }, + { "image", "ℑ" }, + { "infin", "∞" }, + { "int", "∫" }, + { "iota", "ι" }, + { "iquest", "¿" }, + { "isin", "∈" }, + { "iuml", "ï" }, + { "kappa", "κ" }, + { "lArr", "⇐" }, + { "lambda", "λ" }, + { "lang", "〈" }, + { "laquo", "«" }, + { "larr", "←" }, + { "lceil", "⌈" }, + { "ldquo", "“" }, + { "le", "≤" }, + { "lfloor", "⌊" }, + { "lowast", "∗" }, + { "loz", "◊" }, + { "lrm", "\xE2\x80\x8E" }, + { "lsaquo", "‹" }, + { "lsquo", "‘" }, + { "lt", "<" }, + { "macr", "¯" }, + { "mdash", "—" }, + { "micro", "µ" }, + { "middot", "·" }, + { "minus", "−" }, + { "mu", "μ" }, + { "nabla", "∇" }, + { "nbsp", " " }, + { "ndash", "–" }, + { "ne", "≠" }, + { "ni", "∋" }, + { "not", "¬" }, + { "notin", "∉" }, + { "nsub", "⊄" }, + { "ntilde", "ñ" }, + { "nu", "ν" }, + { "oacute", "ó" }, + { "ocirc", "ô" }, + { "oelig", "œ" }, + { "ograve", "ò" }, + { "oline", "‾" }, + { "omega", "ω" }, + { "omicron", "ο" }, + { "oplus", "⊕" }, + { "or", "∨" }, + { "ordf", "ª" }, + { "ordm", "º" }, + { "oslash", "ø" }, + { "otilde", "õ" }, + { "otimes", "⊗" }, + { "ouml", "ö" }, + { "para", "¶" }, + { "part", "∂" }, + { "permil", "‰" }, + { "perp", "⊥" }, + { "phi", "φ" }, + { "pi", "π" }, + { "piv", "ϖ" }, + { "plusmn", "±" }, + { "pound", "£" }, + { "prime", "′" }, + { "prod", "∏" }, + { "prop", "∝" }, + { "psi", "ψ" }, + { "quot", "\"" }, + { "rArr", "⇒" }, + { "radic", "√" }, + { "rang", "〉" }, + { "raquo", "»" }, + { "rarr", "→" }, + { "rceil", "⌉" }, + { "rdquo", "”" }, + { "real", "ℜ" }, + { "reg", "®" }, + { "rfloor", "⌋" }, + { "rho", "ρ" }, + { "rlm", "\xE2\x80\x8F" }, + { "rsaquo", "›" }, + { "rsquo", "’" }, + { "sbquo", "‚" }, + { "scaron", "š" }, + { "sdot", "⋅" }, + { "sect", "§" }, + { "shy", "\xC2\xAD" }, + { "sigma", "σ" }, + { "sigmaf", "ς" }, + { "sim", "∼" }, + { "spades", "♠" }, + { "sub", "⊂" }, + { "sube", "⊆" }, + { "sum", "∑" }, + { "sup", "⊃" }, + { "sup1", "¹" }, + { "sup2", "²" }, + { "sup3", "³" }, + { "supe", "⊇" }, + { "szlig", "ß" }, + { "tau", "τ" }, + { "there4", "∴" }, + { "theta", "θ" }, + { "thetasym", "ϑ" }, + { "thinsp", " " }, + { "thorn", "þ" }, + { "tilde", "˜" }, + { "times", "×" }, + { "trade", "™" }, + { "uArr", "⇑" }, + { "uacute", "ú" }, + { "uarr", "↑" }, + { "ucirc", "û" }, + { "ugrave", "ù" }, + { "uml", "¨" }, + { "upsih", "ϒ" }, + { "upsilon", "υ" }, + { "uuml", "ü" }, + { "weierp", "℘" }, + { "xi", "ξ" }, + { "yacute", "ý" }, + { "yen", "¥" }, + { "yuml", "ÿ" }, + { "zeta", "ζ" }, + { "zwj", "\xE2\x80\x8D" }, + { "zwnj", "\xE2\x80\x8C" } +}; + +static CMStringW getAttrText(const wchar_t *pwszText, const wchar_t *pwszAttrName) +{ + if (auto *p1 = mir_wstrstri(pwszText, pwszAttrName)) { + p1 += wcslen(pwszAttrName); + if (p1[0] == '=' && p1[1] == '\"') { + p1 += 2; + if (auto *p2 = wcschr(p1, '\"')) + return CMStringW(p1, p2 - p1); + } + } + return L""; +} + +CMStringW CTeamsProto::RemoveHtml(const CMStringW &data) +{ + CMStringW new_string; + + for (int i = 0; i < data.GetLength(); i++) { + wchar_t c = data[i]; + if (c == '<') { + if (m_bUseBBCodes) { + bool bEnable = true; + auto *p = data.c_str() + i + 1; + if (*p == '/') { + bEnable = false; + p++; + } + + if (!wcsncmp(p, L"b>", 2)) + new_string.Append(bEnable ? L"[b]" : L"[/b]"); + else if (!wcsncmp(p, L"i>", 2)) + new_string.Append(bEnable ? L"[i]" : L"[/i]"); + else if (!wcsncmp(p, L"u>", 2)) + new_string.Append(bEnable ? L"[u]" : L"[/u]"); + else if (!wcsncmp(p, L"s>", 2)) + new_string.Append(bEnable ? L"[s]" : L"[/s]"); + else if (!wcsncmp(p, L"pre ", 4)) + new_string.Append(L"[code]"); + else if (!wcsncmp(p, L"pre>", 4)) + new_string.Append(L"[/code]"); + else if (!wcsncmp(p, L"legacyquote>", 12)) { + if (bEnable) + i = data.Find(L"legacyquote>", i+13); + } + else if (!wcsncmp(p, L"quote ", 6)) { + CMStringW author(getAttrText(p, L"authorname")); + CMStringW timestamp(getAttrText(p, L"timestamp")); + + wchar_t wszTime[100]; + TimeZone_PrintTimeStamp(0, _wtoi(timestamp), L"D t", wszTime, _countof(wszTime), 0); + + new_string.AppendFormat(L"[quote]%s %s %s: \r\n", wszTime, author.c_str(), TranslateT("wrote")); + } + else if (!wcsncmp(p, L"quote>", 6)) { + new_string.Append(L"[/quote]"); + } + } + + i = data.Find('>', i); + if (i == -1) + break; + + continue; + } + + // special character + if (c == '&') { + int begin = i; + i = data.Find(';', i); + if (i == -1) + i = begin; + else { + CMStringW entity = data.Mid(begin + 1, i - begin - 1); + + bool found = false; + if (entity.GetLength() > 1 && entity[0] == '#') { + // Numeric replacement + bool hex = false; + if (entity[1] == 'x') { + hex = true; + entity.Delete(0, 2); + } + else entity.Delete(0, 1); + + if (!entity.IsEmpty()) { + found = true; + errno = 0; + unsigned long value = wcstoul(entity, nullptr, hex ? 16 : 10); + if (errno != 0) { // error with conversion in strtoul, ignore the result + found = false; + } + else if (value <= 127) { // U+0000 .. U+007F + new_string += (char)value; + } + else if (value >= 128 && value <= 2047) { // U+0080 .. U+07FF + new_string += (char)(192 + (value / 64)); + new_string += (char)(128 + (value % 64)); + } + else if (value >= 2048 && value <= 65535) { // U+0800 .. U+FFFF + new_string += (char)(224 + (value / 4096)); + new_string += (char)(128 + ((value / 64) % 64)); + new_string += (char)(128 + (value % 64)); + } + else { + new_string += (char)((value >> 24) & 0xFF); + new_string += (char)((value >> 16) & 0xFF); + new_string += (char)((value >> 8) & 0xFF); + new_string += (char)((value) & 0xFF); + } + } + } + else { + // Keyword replacement + CMStringA tmp = entity; + for (auto &it : htmlEntities) { + if (!mir_strcmpi(tmp, it.entity)) { + new_string += it.symbol; + found = true; + break; + } + } + } + + if (found) + continue; + else + i = begin; + } + } + + new_string.AppendChar(c); + } + + return new_string; +} + +bool AddBbcodes(CMStringA &str) +{ + bool bUsed = false; + CMStringA ret; + + for (const char *p = str; *p; p++) { + if (*p == '[') { + p++; + if (!strncmp(p, "b]", 2)) { + p++; + ret.Append("<b _pre=\"*\" _post=\"*\">"); + bUsed = true; + } + else if (!strncmp(p, "/b]", 3)) { + p += 2; + ret.Append("</b>"); + bUsed = true; + } + else if (!strncmp(p, "i]", 2)) { + p++; + ret.Append("<i _pre=\"_\" _post=\"_\">"); + bUsed = true; + } + else if (!strncmp(p, "/i]", 3)) { + p += 2; + ret.Append("</i>"); + bUsed = true; + } + else if (!strncmp(p, "s]", 2)) { + p++; + ret.Append("<s _pre=\"~\" _post=\"~\">"); + bUsed = true; + } + else if (!strncmp(p, "/s]", 3)) { + p += 2; + ret.Append("</s>"); + bUsed = true; + } + else if (!strncmp(p, "code]", 5)) { + p += 4; + ret.Append("<pre _pre=\"```\" _post=\"```\">"); + bUsed = true; + } + else if (!strncmp(p, "/code]", 6)) { + p += 5; + ret.Append("</pre>"); + bUsed = true; + } + } + else ret.AppendChar(*p); + } + + if (bUsed) + str = ret; + return bUsed; +} + +////////////////////////////////////////////////////////////////////////////////////////// + +void Utf32toUtf16(uint32_t c, CMStringW &dest) +{ + if (c < 0x10000) + dest.AppendChar(c); + else { + unsigned int t = c - 0x10000; + dest.AppendChar((((t << 12) >> 22) + 0xD800)); + dest.AppendChar((((t << 22) >> 22) + 0xDC00)); + } +} + +bool is_surrogate(wchar_t uc) { return (uc - 0xd800u) < 2048u; } +bool is_high_surrogate(wchar_t uc) { return (uc & 0xfffffc00) == 0xd800; } +bool is_low_surrogate(wchar_t uc) { return (uc & 0xfffffc00) == 0xdc00; } + +uint32_t Utf16toUtf32(const wchar_t *str) +{ + if (!is_surrogate(str[0])) + return str[0]; + + if (is_high_surrogate(str[0]) && is_low_surrogate(str[1])) + return (str[0] << 10) + str[1] - 0x35fdc00; + + return 0; +} + +////////////////////////////////////////////////////////////////////////////////////////// + +int TeamsToMirandaStatus(const char *status) +{ + if (!mir_strcmpi(status, "Available")) + return ID_STATUS_ONLINE; + if (!mir_strcmpi(status, "Away")) + return ID_STATUS_AWAY; + if (!mir_strcmpi(status, "BeRightBack")) + return ID_STATUS_NA; + if (!mir_strcmpi(status, "AvailableIdle")) + return ID_STATUS_IDLE; + if (!mir_strcmpi(status, "Busy")) + return ID_STATUS_OCCUPIED; + if (!mir_strcmpi(status, "DoNotDisturb")) + return ID_STATUS_DND; + return ID_STATUS_OFFLINE; +} + +////////////////////////////////////////////////////////////////////////////////////////// + +bool CTeamsProto::IsMe(const wchar_t *str) +{ + return (!mir_wstrcmpi(str, Utf2T(m_szMyname)) || !mir_wstrcmp(str, Utf2T(m_szOwnSkypeId))); +} + +bool CTeamsProto::IsMe(const char *str) +{ + return (!mir_strcmpi(str, m_szMyname) || !mir_strcmp(str, m_szOwnSkypeId)); +} + +////////////////////////////////////////////////////////////////////////////////////////// + +int64_t CTeamsProto::getLastTime(MCONTACT hContact) +{ + ptrA szLastTime(getStringA(hContact, "LastMsgTime")); + return (szLastTime) ? _atoi64(szLastTime) : 0; +} + +void CTeamsProto::setLastTime(MCONTACT hContact, int64_t iValue) +{ + char buf[100]; + _i64toa(iValue, buf, 10); + setString(hContact, "LastMsgTime", buf); + + if (iValue > getLastTime(0)) + setString("LastMsgTime", buf); +} + +////////////////////////////////////////////////////////////////////////////////////////// + +bool CTeamsProto::IsFileExists(std::wstring path) +{ + return _waccess(path.c_str(), 0) == 0; +} + +int64_t getRandomId() +{ + int64_t ret; + Utils_GetRandom(&ret, sizeof(ret)); + if (ret < 0) + ret = -ret; + return ret; +} + +CMStringA getMessageId(const JSONNode &node) +{ + CMStringA ret(node["skypeeditedid"].as_mstring()); + if (ret.IsEmpty()) + ret = node["clientmessageid"].as_mstring(); + return ret; +} + +const char* GetSkypeNick(const char *szSkypeId) +{ + if (auto *p = strchr(szSkypeId, ':')) + return p + 1; + return szSkypeId; +} + +const wchar_t* GetSkypeNick(const wchar_t *szSkypeId) +{ + if (auto *p = wcsrchr(szSkypeId, ':')) + return p + 1; + return szSkypeId; +} + +void CTeamsProto::SetString(MCONTACT hContact, const char *pszSetting, const JSONNode &node) +{ + CMStringW str = node.as_mstring(); + if (str.IsEmpty() || str == L"null") + delSetting(hContact, pszSetting); + else + setWString(hContact, pszSetting, str); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// url parsing + +CMStringA ParseUrl(const char *url, const char *token) +{ + if (url == nullptr) + return CMStringA(); + + auto *start = strstr(url, token); + if (start == nullptr) + return CMStringA(); + + auto *end = strchr(++start, '/'); + if (end == nullptr) + return CMStringA(start); + return CMStringA(start, end - start); +} + +CMStringW ParseUrl(const wchar_t *url, const wchar_t *token) +{ + if (url == nullptr) + return CMStringW(); + + auto *start = wcsstr(url, token); + if (start == nullptr) + return CMStringW(); + + auto *end = wcschr(++start, '/'); + if (end == nullptr) + return CMStringW(start); + return CMStringW(start, end - start); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static int possibleTypes[] = { 1, 2, 4, 8, 19, 28 }; + +bool IsPossibleUserType(const char *pszUserId) +{ + int iType = atoi(pszUserId); + + // we don't process 28 and other shit + for (auto &it : possibleTypes) + if (it == iType) + return true; + + return false; +} + +CMStringA UrlToSkypeId(const char *url, int *pUserType) +{ + int userType = -1; + CMStringA szResult; + + if (url != nullptr) { + for (auto &it : possibleTypes) { + char tmp[10]; + sprintf_s(tmp, "/%d:", it); + if (strstr(url, tmp)) { + userType = it; + szResult = ParseUrl(url, tmp); + break; + } + } + } + + if (pUserType) + *pUserType = userType; + + return szResult; +} + +CMStringW UrlToSkypeId(const wchar_t *url, int *pUserType) +{ + int userType = -1; + CMStringW szResult; + + if (url != nullptr) { + for (auto &it : possibleTypes) { + wchar_t tmp[10]; + swprintf_s(tmp, L"/%d:", it); + if (wcsstr(url, tmp)) { + userType = it; + szResult = ParseUrl(url, tmp); + break; + } + } + } + + if (pUserType) + *pUserType = userType; + + return szResult; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR CTeamsProto::ParseSkypeUriService(WPARAM, LPARAM lParam) +{ + wchar_t *arg = (wchar_t *)lParam; + if (arg == nullptr) + return 1; + + // skip leading prefix + wchar_t szUri[1024]; + wcsncpy_s(szUri, arg, _TRUNCATE); + wchar_t *szJid = wcschr(szUri, ':'); + if (szJid == nullptr) + return 1; + + // empty jid? + if (!*szJid) + return 1; + + // command code + wchar_t *szCommand = szJid; + szCommand = wcschr(szCommand, '?'); + if (szCommand) + *(szCommand++) = 0; + + // parameters + wchar_t *szSecondParam = szCommand ? wcschr(szCommand, '&') : nullptr; + if (szSecondParam) + *(szSecondParam++) = 0; + + // no command or message command + if (!szCommand || !mir_wstrcmpi(szCommand, L"chat")) { + if (szSecondParam) { + wchar_t *szChatId = wcsstr(szSecondParam, L"id="); + if (szChatId) { + szChatId += 5; + StartChatRoom(szChatId, szChatId); + return 0; + } + } + MCONTACT hContact = AddContact(_T2A(szJid), nullptr, true); + CallService(MS_MSG_SENDMESSAGE, (WPARAM)hContact, NULL); + return 0; + } + + if (!mir_wstrcmpi(szCommand, L"call")) { + MCONTACT hContact = AddContact(_T2A(szJid), nullptr, true); + NotifyEventHooks(g_hCallEvent, (WPARAM)hContact, (LPARAM)0); + return 0; + } + + if (!mir_wstrcmpi(szCommand, L"userinfo")) + return 0; + + if (!mir_wstrcmpi(szCommand, L"add")) { + MCONTACT hContact = FindContact(_T2A(szJid)); + if (hContact == NULL) { + PROTOSEARCHRESULT psr = { 0 }; + psr.cbSize = sizeof(psr); + psr.id.w = mir_wstrdup(szJid); + psr.nick.w = mir_wstrdup(szJid); + psr.flags = PSR_UNICODE; + Contact::AddBySearch(m_szModuleName, &psr); + } + return 0; + } + + if (!mir_wstrcmpi(szCommand, L"sendfile")) { + MCONTACT hContact = AddContact(_T2A(szJid), nullptr, true); + File::Send(hContact); + return 1; + } + + if (!mir_wstrcmpi(szCommand, L"voicemail")) + return 1; + + return 1; +} diff --git a/protocols/Teams/src/teams_utils.h b/protocols/Teams/src/teams_utils.h new file mode 100644 index 0000000000..59931a4a3a --- /dev/null +++ b/protocols/Teams/src/teams_utils.h @@ -0,0 +1,75 @@ +/* +Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef _UTILS_H_ +#define _UTILS_H_ + +void Utf32toUtf16(uint32_t c, CMStringW &dest); +uint32_t Utf16toUtf32(const wchar_t *str); + +const char* GetSkypeNick(const char *pszSkypeId); +const wchar_t* GetSkypeNick(const wchar_t *szSkypeId); + +CMStringA ParseUrl(const char *url, const char *token); + +int TeamsToMirandaStatus(const char *status); + +bool AddBbcodes(CMStringA &str); + +bool IsPossibleUserType(const char *pszUserId); + +CMStringA UrlToSkypeId(const char *url, int *pUserType = nullptr); +CMStringW UrlToSkypeId(const wchar_t *url, int *pUserType = nullptr); + +int getMoodIndex(const char *pszMood); + +int64_t getRandomId(); +CMStringA getMessageId(const JSONNode &node); + +struct CFileUploadParam : public MZeroedObject +{ + OBJLIST<wchar_t> arFileName; + ptrW tszDesc; + ptrA atr; + ptrA fname; + ptrA uid; + long size; + int width, height; + MCONTACT hContact; + bool isPicture; + + CFileUploadParam(MCONTACT _hContact, wchar_t **_files, const wchar_t* _desc) : + arFileName(1), + hContact(_hContact), + tszDesc(mir_wstrdup(_desc)) + { + for (auto p = _files; *p != 0; p++) + arFileName.insert(newStrW(*p)); + } +}; + +struct TeamsReply : public JsonReply +{ + TeamsReply(MHttpResponse *response) : + JsonReply(response) + { + if (m_root) + m_errorCode = (*m_root)["status"]["code"].as_int(); + } +}; + +#endif //_UTILS_H_ diff --git a/protocols/Teams/src/version.h b/protocols/Teams/src/version.h new file mode 100644 index 0000000000..3b23908bca --- /dev/null +++ b/protocols/Teams/src/version.h @@ -0,0 +1,13 @@ +#define __MAJOR_VERSION 0 +#define __MINOR_VERSION 96 +#define __RELEASE_NUM 6 +#define __BUILD_NUM 1 + +#include <stdver.h> + +#define __PLUGIN_NAME "Teams protocol" +#define __FILENAME "Teams.dll" +#define __DESCRIPTION "Microsoft Teams protocol support for Miranda NG." +#define __AUTHOR "Miranda NG team" +#define __AUTHORWEB "https://miranda-ng.org/p/Teams" +#define __COPYRIGHT "© 2025 Miranda NG team" |