diff options
Diffstat (limited to 'protocols/CloudFile')
41 files changed, 3938 insertions, 0 deletions
diff --git a/protocols/CloudFile/CloudFile.vcxproj b/protocols/CloudFile/CloudFile.vcxproj new file mode 100644 index 0000000000..88e1946426 --- /dev/null +++ b/protocols/CloudFile/CloudFile.vcxproj @@ -0,0 +1,40 @@ +<?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>CloudFile</ProjectName> + <ProjectGuid>{E876FE63-0701-4CDA-BED5-7C73A379C1D1}</ProjectGuid> + </PropertyGroup> + <ImportGroup Label="PropertySheets"> + <Import Project="$(ProjectDir)..\..\build\vc.common\plugin.props" /> + </ImportGroup> + <ItemGroup> + <ClInclude Include="src\Services\*.h" /> + <ClCompile Include="src\Services\*.cpp"> + <PrecompiledHeader>NotUsing</PrecompiledHeader> + </ClCompile> + <None Include="res\*.ico" /> + </ItemGroup> + <ItemDefinitionGroup> + <Link> + <AdditionalDependencies>comctl32.lib;shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies> + </Link> + </ItemDefinitionGroup> +</Project> diff --git a/protocols/CloudFile/CloudFile.vcxproj.filters b/protocols/CloudFile/CloudFile.vcxproj.filters new file mode 100644 index 0000000000..c26cb717b8 --- /dev/null +++ b/protocols/CloudFile/CloudFile.vcxproj.filters @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <ItemGroup> + <Filter Include="Header Files\Services"> + <UniqueIdentifier>{64D2CE87-E04F-4768-BD94-86282BBD138B}</UniqueIdentifier> + </Filter> + <Filter Include="Source Files\Services"> + <UniqueIdentifier>{1D61FE06-83AA-4C8A-A8FC-36EAD0EAAC97}</UniqueIdentifier> + </Filter> + </ItemGroup> + <ItemGroup> + <ClInclude Include="src\*.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="src\Services\*.h"> + <Filter>Header Files\Services</Filter> + </ClInclude> + <ClCompile Include="src\*.cpp;src\*.cxx"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="src\Services\*.cpp"> + <Filter>Source Files\Services</Filter> + </ClCompile> + <ResourceCompile Include="res\*.rc"> + <Filter>Resource Files</Filter> + </ResourceCompile> + <None Include="res\*.ico;res\*.bmp;res\*.cur"> + <Filter>Resource Files</Filter> + </None> + </ItemGroup> + <Import Project="$(ProjectDir)..\..\build\vc.common\common.filters" /> +</Project> diff --git a/protocols/CloudFile/res/dropbox.ico b/protocols/CloudFile/res/dropbox.ico Binary files differnew file mode 100644 index 0000000000..8a286d7f32 --- /dev/null +++ b/protocols/CloudFile/res/dropbox.ico diff --git a/protocols/CloudFile/res/gdrive.ico b/protocols/CloudFile/res/gdrive.ico Binary files differnew file mode 100644 index 0000000000..a34123e95b --- /dev/null +++ b/protocols/CloudFile/res/gdrive.ico diff --git a/protocols/CloudFile/res/onedrive.ico b/protocols/CloudFile/res/onedrive.ico Binary files differnew file mode 100644 index 0000000000..502cee9bb7 --- /dev/null +++ b/protocols/CloudFile/res/onedrive.ico diff --git a/protocols/CloudFile/res/resource.rc b/protocols/CloudFile/res/resource.rc new file mode 100644 index 0000000000..54eb588ffa --- /dev/null +++ b/protocols/CloudFile/res/resource.rc @@ -0,0 +1,200 @@ +// Microsoft Visual C++ generated resource script. +// +#include "..\src\resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// Russian (Russia) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_RUS) +LANGUAGE LANG_RUSSIAN, SUBLANG_DEFAULT +#pragma code_page(1251) + +#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 + +#endif // Russian (Russia) resources +///////////////////////////////////////////////////////////////////////////// + + +///////////////////////////////////////////////////////////////////////////// +// English resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_NEUTRAL +#pragma code_page(1252) + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_UPLOAD ICON "upload.ico" + +IDI_DROPBOX ICON "dropbox.ico" + +IDI_GDRIVE ICON "gdrive.ico" + +IDI_ONEDRIVE ICON "onedrive.ico" + +IDI_YADISK ICON "yadisk.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_OPTIONS_MAIN DIALOGEX 0, 0, 307, 234 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD +EXSTYLE WS_EX_CONTROLPARENT +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + LTEXT "Default service",IDC_STATIC,11,19,85,8 + COMBOBOX IDC_DEFAULTSERVICE,102,17,194,12,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + GROUPBOX "General",IDC_STATIC,5,5,297,33 + CONTROL "Autosend download link to contact",IDC_URL_AUTOSEND, + "Button",BS_AUTORADIOBUTTON | WS_GROUP,11,114,282,10 + CONTROL "Paste download link into message input area",IDC_URL_COPYTOMIA, + "Button",BS_AUTORADIOBUTTON,11,127,282,10 + CONTROL "Copy download link to clipboard",IDC_URL_COPYTOCB, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,11,140,282,10 + GROUPBOX "Download link",IDC_STATIC,5,100,297,57 + CONTROL "Do nothing",IDC_DONOTHINGONCONFLICT,"Button",BS_AUTORADIOBUTTON | WS_GROUP,11,54,285,10 + CONTROL "Try to rename",IDC_RENAMEONCONFLICT,"Button",BS_AUTORADIOBUTTON,11,67,285,10 + CONTROL "Try to replace",IDC_REPLACEONCONFLICT,"Button",BS_AUTORADIOBUTTON,11,80,285,10 + GROUPBOX "On conflict when upload",IDC_STATIC,5,41,297,56 +END + +IDD_OAUTH DIALOGEX 0, 0, 193, 83 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU +EXSTYLE WS_EX_TOPMOST +CAPTION "Authorization" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + EDITTEXT IDC_OAUTH_CODE,7,43,179,14,ES_AUTOHSCROLL + DEFPUSHBUTTON "OK",IDOK,75,62,50,14 + PUSHBUTTON "Cancel",IDCANCEL,136,62,50,14 + LTEXT "Enter authorization code:",IDC_STATIC,7,33,179,8 + LTEXT "To allow Miranda NG access to %s:",IDC_AUTH_TEXT,7,7,179,8 + CONTROL "Go to this link",IDC_OAUTH_AUTHORIZE,"Hyperlink",WS_GROUP | WS_TABSTOP | 0x1,7,18,179,8 +END + +IDD_ACCMGR DIALOGEX 0, 0, 188, 126 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +EXSTYLE WS_EX_CONTROLPARENT +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + PUSHBUTTON "Request access",IDC_REQUESTACCESS,5,19,83,14 + PUSHBUTTON "Revoke access",IDC_REVOKEACCESS,97,19,83,14 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// AFX_DIALOG_LAYOUT +// + +IDD_OPTIONS_MAIN AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_OAUTH AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_ACCMGR AFX_DIALOG_LAYOUT +BEGIN + 0 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN + IDD_OPTIONS_MAIN, DIALOG + BEGIN + LEFTMARGIN, 5 + RIGHTMARGIN, 302 + VERTGUIDE, 11 + VERTGUIDE, 96 + VERTGUIDE, 102 + VERTGUIDE, 296 + TOPMARGIN, 5 + BOTTOMMARGIN, 229 + END + + IDD_OAUTH, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 186 + TOPMARGIN, 7 + BOTTOMMARGIN, 76 + END + + IDD_ACCMGR, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 181 + TOPMARGIN, 7 + BOTTOMMARGIN, 119 + HORZGUIDE, 33 + END +END +#endif // APSTUDIO_INVOKED + +#endif // English resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/protocols/CloudFile/res/upload.ico b/protocols/CloudFile/res/upload.ico Binary files differnew file mode 100644 index 0000000000..b51e87ed35 --- /dev/null +++ b/protocols/CloudFile/res/upload.ico diff --git a/protocols/CloudFile/res/version.rc b/protocols/CloudFile/res/version.rc new file mode 100644 index 0000000000..5a5ddd63ed --- /dev/null +++ b/protocols/CloudFile/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/CloudFile/res/yadisk.ico b/protocols/CloudFile/res/yadisk.ico Binary files differnew file mode 100644 index 0000000000..b3a5ac69b4 --- /dev/null +++ b/protocols/CloudFile/res/yadisk.ico diff --git a/protocols/CloudFile/src/Services/dropbox_api.h b/protocols/CloudFile/src/Services/dropbox_api.h new file mode 100644 index 0000000000..e7e77600c0 --- /dev/null +++ b/protocols/CloudFile/src/Services/dropbox_api.h @@ -0,0 +1,221 @@ +#ifndef _DROPBOXSERVICE_API_H_ +#define _DROPBOXSERVICE_API_H_ + +// https://www.dropbox.com/developers/documentation/http/documentation +namespace DropboxAPI +{ +#define DROPBOX_API_VER "/2" +#define DROPBOX_API "https://api.dropboxapi.com" +#define DROPBOX_API_OAUTH DROPBOX_API "/oauth2" +#define DROPBOX_API_RPC DROPBOX_API DROPBOX_API_VER +#define DROPBOX_CONTENT "https://content.dropboxapi.com" +#define DROPBOX_API_CU DROPBOX_CONTENT DROPBOX_API_VER + +#define DROPBOX_APP_KEY "fa8du7gkf2q8xzg" +#include "../../../miranda-private-keys/Dropbox/secret_key.h" + +#define DROPBOX_API_AUTH "https://www.dropbox.com/oauth2/authorize?response_type=code&redirect_uri=https%3A%2F%2Foauth.miranda-ng.org%2Fverification&client_id=" DROPBOX_APP_KEY + + class GetAccessTokenRequest : public HttpRequest + { + public: + GetAccessTokenRequest(const char *code) : + HttpRequest(REQUEST_POST, DROPBOX_API_OAUTH "/token") + { + AddHeader("Content-Type", "application/x-www-form-urlencoded"); + + CMStringA data = "redirect_uri=https://oauth.miranda-ng.org/verification"; + data.AppendFormat("&client_id=%s&client_secret=%s", DROPBOX_APP_KEY, DROPBOX_API_SECRET); + data.AppendFormat("&grant_type=authorization_code&code=%s", code); + SetData(data.GetBuffer(), data.GetLength()); + } + }; + + class RevokeAccessTokenRequest : public HttpRequest + { + public: + RevokeAccessTokenRequest(const char *token) : + HttpRequest(REQUEST_POST, DROPBOX_API_OAUTH "/token/revoke") + { + AddBearerAuthHeader(token); + } + }; + + class UploadFileRequest : public HttpRequest + { + public: + UploadFileRequest(const char *token, const char *path, const char *data, size_t size, OnConflict strategy = NONE) : + HttpRequest(REQUEST_POST, DROPBOX_API_CU "/files/upload") + { + AddBearerAuthHeader(token); + AddHeader("Content-Type", "application/octet-stream"); + + JSONNode params(JSON_NODE); + params + << JSONNode("path", path); + if (strategy == OnConflict::RENAME) { + params + << JSONNode("mode", "add") + << JSONNode("autorename", true); + } + else if (strategy == OnConflict::REPLACE) { + params + << JSONNode("mode", "overwrite") + << JSONNode("autorename", false); + } + + AddHeader("Dropbox-API-Arg", params.write().c_str()); + + SetData(data, size); + } + }; + + class CreateUploadSessionRequest : public HttpRequest + { + public: + CreateUploadSessionRequest(const char *token, const char *chunk, size_t chunkSize) : + HttpRequest(REQUEST_POST, DROPBOX_API_CU "/files/upload_session/start") + { + AddBearerAuthHeader(token); + AddHeader("Content-Type", "application/octet-stream"); + + SetData(chunk, chunkSize); + } + }; + + class UploadFileChunkRequest : public HttpRequest + { + public: + UploadFileChunkRequest(const char *token, const char *sessionId, size_t offset, const char *chunk, size_t chunkSize) : + HttpRequest(REQUEST_POST, DROPBOX_API_CU "/files/upload_session/append_v2") + { + AddBearerAuthHeader(token); + AddHeader("Content-Type", "application/octet-stream"); + + + JSONNode cursor; + cursor.set_name("cursor"); + cursor + << JSONNode("session_id", sessionId) + << JSONNode("offset", (unsigned long)offset); + + JSONNode param; + param << cursor; + + AddHeader("Dropbox-API-Arg", param.write().c_str()); + + SetData(chunk, chunkSize); + } + }; + + class CommitUploadSessionRequest : public HttpRequest + { + public: + CommitUploadSessionRequest(const char *token, const char *sessionId, size_t offset, const char *path, const char *chunk, size_t chunkSize, OnConflict strategy = NONE) : + HttpRequest(REQUEST_POST, DROPBOX_API_CU "/files/upload_session/finish") + { + AddBearerAuthHeader(token); + AddHeader("Content-Type", "application/octet-stream"); + + JSONNode cursor(JSON_NODE); + cursor.set_name("cursor"); + cursor + << JSONNode("session_id", sessionId) + << JSONNode("offset", (unsigned long)offset); + + JSONNode commit(JSON_NODE); + commit.set_name("commit"); + commit + << JSONNode("path", path); + if (strategy == OnConflict::RENAME) { + commit + << JSONNode("mode", "add") + << JSONNode("autorename", true); + } + else if (strategy == OnConflict::REPLACE) { + commit + << JSONNode("mode", "overwrite") + << JSONNode("autorename", false); + } + + JSONNode params(JSON_NODE); + params + << cursor + << commit; + + AddHeader("Dropbox-API-Arg", params.write().c_str()); + + SetData(chunk, chunkSize); + } + }; + + class CreateFolderRequest : public HttpRequest + { + public: + CreateFolderRequest(const char *token, const char *path) : + HttpRequest(REQUEST_POST, DROPBOX_API_RPC "/files/create_folder_v2") + { + AddBearerAuthHeader(token); + AddHeader("Content-Type", "application/json"); + + JSONNode root(JSON_NODE); + root << JSONNode("path", path); + + json_string data = root.write(); + SetData(data.c_str(), data.length()); + } + }; + + class GetTemporaryLinkRequest : public HttpRequest + { + public: + GetTemporaryLinkRequest(const char *token, const char *path) : + HttpRequest(REQUEST_POST, DROPBOX_API_RPC "/files/get_temporary_link") + { + AddBearerAuthHeader(token); + AddHeader("Content-Type", "application/json"); + + JSONNode root(JSON_NODE); + root << JSONNode("path", path); + + json_string data = root.write(); + SetData(data.c_str(), data.length()); + } + }; + + class CreateSharedLinkRequest : public HttpRequest + { + public: + CreateSharedLinkRequest(const char *token, const char *path) : + HttpRequest(REQUEST_POST, DROPBOX_API_RPC "/sharing/create_shared_link_with_settings") + { + AddBearerAuthHeader(token); + AddHeader("Content-Type", "application/json"); + + JSONNode root(JSON_NODE); + root << JSONNode("path", path); + + json_string data = root.write(); + SetData(data.c_str(), data.length()); + } + }; + + class GetSharedLinkRequest : public HttpRequest + { + public: + GetSharedLinkRequest(const char *token, const char *path) : + HttpRequest(REQUEST_POST, DROPBOX_API_RPC "/sharing/list_shared_links") + { + AddBearerAuthHeader(token); + AddHeader("Content-Type", "application/json"); + + JSONNode root(JSON_NODE); + root << JSONNode("path", path); + + json_string data = root.write(); + SetData(data.c_str(), data.length()); + } + }; +}; + +#endif //_DROPBOXSERVICE_API_H_ diff --git a/protocols/CloudFile/src/Services/dropbox_service.cpp b/protocols/CloudFile/src/Services/dropbox_service.cpp new file mode 100644 index 0000000000..a2cfa5a2e5 --- /dev/null +++ b/protocols/CloudFile/src/Services/dropbox_service.cpp @@ -0,0 +1,302 @@ +#include "..\stdafx.h" +#include "dropbox_api.h" + +struct CMPluginDropbox : public PLUGIN<CMPluginDropbox> +{ + CMPluginDropbox() : + PLUGIN<CMPluginDropbox>(MODULENAME "/Dropbox", pluginInfoEx) + { + m_hInst = g_plugin.getInst(); + + RegisterProtocol(PROTOTYPE_PROTOWITHACCS, (pfnInitProto)CDropboxService::Init, (pfnUninitProto)CDropboxService::UnInit); + } +} +g_pluginDropbox; + +///////////////////////////////////////////////////////////////////////////////////////// + +CDropboxService::CDropboxService(const char *protoName, const wchar_t *userName) : + CCloudService(protoName, userName, &g_pluginDropbox) +{ + m_hProtoIcon = GetIconHandle(IDI_DROPBOX); +} + +CDropboxService* CDropboxService::Init(const char *moduleName, const wchar_t *userName) +{ + CDropboxService *proto = new CDropboxService(moduleName, userName); + Services.insert(proto); + return proto; +} + +int CDropboxService::UnInit(CDropboxService *proto) +{ + Services.remove(proto); + delete proto; + return 0; +} + +const char* CDropboxService::GetModuleName() const +{ + return "Dropbox"; +} + +int CDropboxService::GetIconId() const +{ + return IDI_DROPBOX; +} + +bool CDropboxService::IsLoggedIn() +{ + ptrA token(getStringA("TokenSecret")); + if (!token || token[0] == 0) + return false; + return true; +} + +void CDropboxService::Login(HWND owner) +{ + COAuthDlg dlg(this, DROPBOX_API_AUTH, (MyThreadFunc)&CDropboxService::RequestAccessTokenThread); + dlg.SetParent(owner); + dlg.DoModal(); +} + +void CDropboxService::Logout() +{ + ForkThread((MyThreadFunc)&CDropboxService::RevokeAccessTokenThread); +} + +void CDropboxService::RequestAccessTokenThread(void *param) +{ + HWND hwndDlg = (HWND)param; + + if (IsLoggedIn()) + Logout(); + + char requestToken[128]; + GetDlgItemTextA(hwndDlg, IDC_OAUTH_CODE, requestToken, _countof(requestToken)); + + DropboxAPI::GetAccessTokenRequest request(requestToken); + NLHR_PTR response(request.Send(m_hConnection)); + + if (response == nullptr || response->resultCode != HTTP_CODE_OK) { + Netlib_Logf(m_hConnection, "%s: %s", GetAccountName(), HttpStatusToError()); + ShowNotification(TranslateT("Server does not respond"), MB_ICONERROR); + EndDialog(hwndDlg, 0); + return; + } + + JSONNode root = JSONNode::parse(response->pData); + if (root.empty()) { + Netlib_Logf(m_hConnection, "%s: %s", GetAccountName(), HttpStatusToError(response->resultCode)); + ShowNotification(TranslateT("Server does not respond"), MB_ICONERROR); + EndDialog(hwndDlg, 0); + return; + } + + JSONNode node = root.at("error_description"); + if (!node.isnull()) { + CMStringW error_description = node.as_mstring(); + Netlib_Logf(m_hConnection, "%s: %s", GetAccountName(), HttpStatusToError(response->resultCode)); + ShowNotification(error_description, MB_ICONERROR); + EndDialog(hwndDlg, 0); + return; + } + + node = root.at("access_token"); + db_set_s(0, GetAccountName(), "TokenSecret", node.as_string().c_str()); + + SetDlgItemTextA(hwndDlg, IDC_OAUTH_CODE, ""); + + EndDialog(hwndDlg, 1); +} + +void CDropboxService::RevokeAccessTokenThread(void *) +{ + ptrA token(getStringA("TokenSecret")); + DropboxAPI::RevokeAccessTokenRequest request(token); + NLHR_PTR response(request.Send(m_hConnection)); + + delSetting("ExpiresIn"); + delSetting("TokenSecret"); + delSetting("RefreshToken"); +} + +void CDropboxService::HandleJsonError(JSONNode &node) +{ + JSONNode error = node.at("error"); + if (!error.isnull()) { + json_string tag = error.at(".tag").as_string(); + throw Exception(tag.c_str()); + } +} + +auto CDropboxService::UploadFile(const char *data, size_t size, const std::string &path) +{ + ptrA token(getStringA("TokenSecret")); + BYTE strategy = g_plugin.getByte("ConflictStrategy", OnConflict::REPLACE); + DropboxAPI::UploadFileRequest request(token, path.c_str(), data, size, (OnConflict)strategy); + NLHR_PTR response(request.Send(m_hConnection)); + + JSONNode root = GetJsonResponse(response); + return root["path_lower"].as_string(); +} + +auto CDropboxService::CreateUploadSession(const char *chunk, size_t chunkSize) +{ + ptrA token(getStringA("TokenSecret")); + DropboxAPI::CreateUploadSessionRequest request(token, chunk, chunkSize); + NLHR_PTR response(request.Send(m_hConnection)); + + JSONNode root = GetJsonResponse(response); + return root["session_id"].as_string(); +} + +void CDropboxService::UploadFileChunk(const std::string &sessionId, const char *chunk, size_t chunkSize, size_t offset) +{ + ptrA token(getStringA("TokenSecret")); + DropboxAPI::UploadFileChunkRequest request(token, sessionId.c_str(), offset, chunk, chunkSize); + NLHR_PTR response(request.Send(m_hConnection)); + HandleHttpError(response); +} + +auto CDropboxService::CommitUploadSession(const std::string &sessionId, const char *data, size_t size, size_t offset, const std::string &path) +{ + ptrA token(getStringA("TokenSecret")); + BYTE strategy = g_plugin.getByte("ConflictStrategy", OnConflict::REPLACE); + DropboxAPI::CommitUploadSessionRequest request(token, sessionId.c_str(), offset, path.c_str(), data, size, (OnConflict)strategy); + NLHR_PTR response(request.Send(m_hConnection)); + + JSONNode root = GetJsonResponse(response); + return root["path_lower"].as_string(); +} + +void CDropboxService::CreateFolder(const std::string &path) +{ + ptrA token(getStringA("TokenSecret")); + DropboxAPI::CreateFolderRequest request(token, path.c_str()); + NLHR_PTR response(request.Send(m_hConnection)); + + if (response == nullptr) + throw Exception(HttpStatusToError()); + + if (HTTP_CODE_SUCCESS(response->resultCode)) { + GetJsonResponse(response); + return; + } + + // forder exists on server + if (response->resultCode == HTTP_CODE_CONFLICT) { + return; + } + + HttpResponseToError(response); +} + +auto CDropboxService::CreateSharedLink(const std::string &path) +{ + ptrA token(getStringA("TokenSecret")); + DropboxAPI::CreateSharedLinkRequest shareRequest(token, path.c_str()); + NLHR_PTR response(shareRequest.Send(m_hConnection)); + + if (response && HTTP_CODE_SUCCESS(response->resultCode)) { + JSONNode root = GetJsonResponse(response); + return root["url"].as_string(); + } + + if (!response || response->resultCode != HTTP_CODE_CONFLICT) + HttpResponseToError(response); + + JSONNode root = JSONNode::parse(response->pData); + if (root.isnull()) + throw Exception(HttpStatusToError()); + + JSONNode error = root.at("error"); + if (error.isnull()) { + JSONNode link = root.at("url"); + return link.as_string(); + } + + json_string tag = error.at(".tag").as_string(); + if (tag != "shared_link_already_exists") + throw Exception(tag.c_str()); + + DropboxAPI::GetSharedLinkRequest getRequest(token, path.c_str()); + response = getRequest.Send(m_hConnection); + + root = GetJsonResponse(response); + + JSONNode links = root.at("links").as_array(); + JSONNode link = links[(size_t)0].at("url"); + return link.as_string(); +} + +void CDropboxService::Upload(FileTransferParam *ftp) +{ + auto serverDictionary = ftp->GetServerDirectory(); + std::string serverFolder = serverDictionary ? T2Utf(serverDictionary) : ""; + if (!serverFolder.empty()) { + auto path = PreparePath(serverFolder); + CreateFolder(path); + auto link = CreateSharedLink(path); + ftp->AddSharedLink(link.c_str()); + } + + ftp->FirstFile(); + do + { + std::string fileName = T2Utf(ftp->GetCurrentRelativeFilePath()); + uint64_t fileSize = ftp->GetCurrentFileSize(); + + size_t chunkSize = ftp->GetCurrentFileChunkSize(); + mir_ptr<char> chunk((char*)mir_calloc(chunkSize)); + + std::string path; + if (!serverFolder.empty()) + path = "/" + serverFolder + "/" + fileName; + else + path = PreparePath(fileName); + + if (chunkSize == fileSize) { + ftp->CheckCurrentFile(); + size_t size = ftp->ReadCurrentFile(chunk, chunkSize); + + path = UploadFile(chunk, size, path); + + ftp->Progress(size); + } + else { + ftp->CheckCurrentFile(); + size_t size = ftp->ReadCurrentFile(chunk, chunkSize); + + auto sessionId = CreateUploadSession(chunk, size); + + ftp->Progress(size); + + size_t offset = size; + double chunkCount = ceil(double(fileSize) / chunkSize) - 2; + for (size_t i = 0; i < chunkCount; i++) { + ftp->CheckCurrentFile(); + + size = ftp->ReadCurrentFile(chunk, chunkSize); + UploadFileChunk(sessionId, chunk, size, offset); + + offset += size; + ftp->Progress(size); + } + + ftp->CheckCurrentFile(); + size = offset < fileSize + ? ftp->ReadCurrentFile(chunk, fileSize - offset) + : 0; + + path = CommitUploadSession(sessionId, chunk, size, offset, path); + + ftp->Progress(size); + } + + if (!ftp->IsCurrentFileInSubDirectory()) { + auto link = CreateSharedLink(path); + ftp->AddSharedLink(link.c_str()); + } + } while (ftp->NextFile()); +} diff --git a/protocols/CloudFile/src/Services/dropbox_service.h b/protocols/CloudFile/src/Services/dropbox_service.h new file mode 100644 index 0000000000..b6c5a7dfcd --- /dev/null +++ b/protocols/CloudFile/src/Services/dropbox_service.h @@ -0,0 +1,36 @@ +#ifndef _CLOUDSERVICE_DROPBOX_H_ +#define _CLOUDSERVICE_DROPBOX_H_ + +class CDropboxService : public CCloudService +{ +private: + void __cdecl RequestAccessTokenThread(void *); + void __cdecl RevokeAccessTokenThread(void *); + + void HandleJsonError(JSONNode &node) override; + + auto UploadFile(const char *data, size_t size, const std::string &path); + auto CreateUploadSession(const char *chunk, size_t chunkSize); + void UploadFileChunk(const std::string &sessionId, const char *chunk, size_t chunkSize, size_t offset); + auto CommitUploadSession(const std::string &sessionId, const char *chunk, size_t chunkSize, size_t offset, const std::string &path); + void CreateFolder(const std::string &path); + auto CreateSharedLink(const std::string &path); + + void Upload(FileTransferParam *ftp) override; + +public: + CDropboxService(const char *protoName, const wchar_t *userName); + + static CDropboxService* Init(const char *szModuleName, const wchar_t *szUserName); + static int UnInit(CDropboxService*); + + const char* GetModuleName() const override; + + int GetIconId() const override; + + bool IsLoggedIn() override; + void Login(HWND owner = nullptr) override; + void Logout() override; +}; + +#endif //_CLOUDSERVICE_DROPBOX_H_
\ No newline at end of file diff --git a/protocols/CloudFile/src/Services/google_api.h b/protocols/CloudFile/src/Services/google_api.h new file mode 100644 index 0000000000..90cfdb4d03 --- /dev/null +++ b/protocols/CloudFile/src/Services/google_api.h @@ -0,0 +1,200 @@ +#ifndef _GDRIVESERVICE_API_H_ +#define _GDRIVESERVICE_API_H_ + +// https://developers.google.com/drive/v3/reference/ +namespace GDriveAPI +{ +#define GOOGLE_OAUTH "https://accounts.google.com/o/oauth2/v2" +#define GOOGLE_API "https://www.googleapis.com" +#define GDRIVE_API_OAUTH GOOGLE_API "/oauth2/v4" +#define GDRIVE_API_VER "/v3" +#define GDRIVE_API GOOGLE_API "/drive" GDRIVE_API_VER "/files" +#define GDRIVE_UPLOAD GOOGLE_API "/upload/drive" GDRIVE_API_VER "/files" +#define GDRIVE_SHARE "https://drive.google.com/open?id=" + +#define GOOGLE_APP_ID "528761318515-h1etlccvk5vjsbjuuj8i73cud8do4adi.apps.googleusercontent.com" +#include "../../../miranda-private-keys/Google/client_secret.h" + +#define GOOGLE_AUTH GOOGLE_OAUTH "/auth?response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive.file&access_type=offline&prompt=consent&redirect_uri=https%3A%2F%2Foauth.miranda-ng.org%2Fverification&client_id=" GOOGLE_APP_ID + + class GetAccessTokenRequest : public HttpRequest + { + public: + GetAccessTokenRequest(const char *code) : + HttpRequest(REQUEST_POST, GDRIVE_API_OAUTH "/token") + { + AddHeader("Content-Type", "application/x-www-form-urlencoded"); + + CMStringA data = "redirect_uri=https://oauth.miranda-ng.org/verification"; + data.AppendFormat("&client_id=%s&client_secret=%s", GOOGLE_APP_ID, GOOGLE_CLIENT_SECRET); + data.AppendFormat("&grant_type=authorization_code&code=%s", code); + SetData(data.GetBuffer(), data.GetLength()); + } + }; + + class RefreshTokenRequest : public HttpRequest + { + public: + RefreshTokenRequest(const char *refreshToken) : + HttpRequest(REQUEST_POST, GDRIVE_API_OAUTH "/token") + { + AddHeader("Content-Type", "application/x-www-form-urlencoded"); + + CMStringA data(CMStringDataFormat::FORMAT, + "client_id=%s&client_secret=%s&grant_type=refresh_token&refresh_token=%s", + GOOGLE_APP_ID, GOOGLE_CLIENT_SECRET, refreshToken); + SetData(data.GetBuffer(), data.GetLength()); + } + }; + + class RevokeAccessTokenRequest : public HttpRequest + { + public: + RevokeAccessTokenRequest(const char *token) : + HttpRequest(REQUEST_POST, GOOGLE_OAUTH "/revoke") + { + AddUrlParameter("token=%s", token); + } + }; + + class UploadFileRequest : public HttpRequest + { + public: + UploadFileRequest(const char *token, const char *parentId, const char *name, const char *data, size_t size) : + HttpRequest(REQUEST_POST, GDRIVE_UPLOAD) + { + AddUrlParameter("fields=id"); + + AddBearerAuthHeader(token); + AddHeader("Content-Type", "multipart/related; boundary=upload"); + + CMStringA body = "--upload"; + body.AppendChar(0x0A); + body.Append("Content-Type: application/json"); + body.AppendChar(0x0A); + body.AppendChar(0x0A); + body.Append("{"); + body.AppendFormat("\"name\": \"%s\"", name); + if (mir_strlen(parentId)) + body.AppendFormat(", \"parents\": [\"%s\"]", parentId); + body.Append("}"); + body.AppendChar(0x0A); + body.AppendChar(0x0A); + body.Append("--upload"); + body.AppendChar(0x0A); + body.Append("Content-Type: application/octet-stream"); + body.AppendChar(0x0A); + body.Append("Content-Transfer-Encoding: base64"); + body.AppendChar(0x0A); + body.AppendChar(0x0A); + body.Append(ptrA(mir_base64_encode(data, size))); + body.AppendChar(0x0A); + body.Append("--upload--"); + + SetData(body.GetBuffer(), body.GetLength()); + } + }; + + class CreateUploadSessionRequest : public HttpRequest + { + public: + CreateUploadSessionRequest(const char *token, const char *parentId, const char *name) : + HttpRequest(REQUEST_POST, GDRIVE_UPLOAD) + { + AddUrlParameter("uploadType=resumable"); + + AddBearerAuthHeader(token); + AddHeader("Content-Type", "application/json"); + + JSONNode parents(JSON_ARRAY); + parents << JSONNode("", parentId); + parents.set_name("parents"); + + JSONNode params(JSON_NODE); + params << JSONNode("name", name); + params << parents; + + json_string data = params.write(); + SetData(data.c_str(), data.length()); + } + }; + + class UploadFileChunkRequest : public HttpRequest + { + public: + UploadFileChunkRequest(const char *uploadUri, const char *chunk, size_t chunkSize, uint64_t offset, uint64_t fileSize): + HttpRequest(REQUEST_PUT, uploadUri) + { + AddUrlParameter("fields=id"); + + uint64_t rangeMin = offset; + uint64_t rangeMax = offset + chunkSize - 1; + CMStringA range(CMStringDataFormat::FORMAT, "bytes %I64u-%I64u/%I64u", rangeMin, rangeMax, fileSize); + AddHeader("Content-Range", range); + + SetData(chunk, chunkSize); + } + }; + + class GetFolderRequest : public HttpRequest + { + public: + GetFolderRequest(const char *token, const char *parentId, const char *name) : + HttpRequest(REQUEST_GET, GDRIVE_API) + { + AddUrlParameterWithEncode("q", "mimeType = 'application/vnd.google-apps.folder' and trashed = false and '%s' in parents and name = '%s'", mir_strlen(parentId) ? parentId : "root", name); + AddUrlParameterWithEncode("fields", "files(id)"); + + AddBearerAuthHeader(token); + } + }; + + class CreateFolderRequest : public HttpRequest + { + public: + CreateFolderRequest(const char *token, const char *parentId, const char *name) : + HttpRequest(REQUEST_POST, GDRIVE_API) + { + AddUrlParameter("fields=id"); + + AddBearerAuthHeader(token); + AddHeader("Content-Type", "application/json"); + + JSONNode parents(JSON_ARRAY); + parents.set_name("parents"); + parents.push_back(JSONNode("", parentId)); + + JSONNode params(JSON_NODE); + params + << JSONNode("name", name) + << JSONNode("mimeType", "application/vnd.google-apps.folder") + << parents; + + json_string data = params.write(); + SetData(data.c_str(), data.length()); + } + }; + + class GrantPermissionsRequest : public HttpRequest + { + public: + GrantPermissionsRequest(const char *token, const char *fileId) : + HttpRequest(REQUEST_POST, FORMAT, GDRIVE_API "/%s/permissions", fileId) + { + AddUrlParameter("fields=id"); + + AddBearerAuthHeader(token); + AddHeader("Content-Type", "application/json"); + + JSONNode params(JSON_NODE); + params + << JSONNode("role", "reader") + << JSONNode("type", "anyone"); + + json_string data = params.write(); + SetData(data.c_str(), data.length()); + } + }; +}; + +#endif //_GDRIVESERVICE_API_H_ diff --git a/protocols/CloudFile/src/Services/google_service.cpp b/protocols/CloudFile/src/Services/google_service.cpp new file mode 100644 index 0000000000..536d2ff65e --- /dev/null +++ b/protocols/CloudFile/src/Services/google_service.cpp @@ -0,0 +1,297 @@ +#include "..\stdafx.h" +#include "google_api.h" + + +struct CMPluginGoogle : public CMPluginBase +{ + CMPluginGoogle() : + CMPluginBase(MODULENAME "/GDrive", pluginInfoEx) + { + m_hInst = g_plugin.getInst(); + + RegisterProtocol(PROTOTYPE_PROTOWITHACCS, (pfnInitProto)CGDriveService::Init, (pfnUninitProto)CGDriveService::UnInit); + } +} +g_pluginGoogle; + +///////////////////////////////////////////////////////////////////////////////////////// + +CGDriveService::CGDriveService(const char *protoName, const wchar_t *userName) : + CCloudService(protoName, userName, &g_pluginGoogle) +{ + m_hProtoIcon = GetIconHandle(IDI_GDRIVE); +} + +CGDriveService* CGDriveService::Init(const char *moduleName, const wchar_t *userName) +{ + CGDriveService *proto = new CGDriveService(moduleName, userName); + Services.insert(proto); + return proto; +} + +int CGDriveService::UnInit(CGDriveService *proto) +{ + Services.remove(proto); + delete proto; + return 0; +} + +const char* CGDriveService::GetModuleName() const +{ + return "/Google"; +} + +int CGDriveService::GetIconId() const +{ + return IDI_GDRIVE; +} + +bool CGDriveService::IsLoggedIn() +{ + ptrA token(getStringA("TokenSecret")); + if (!token || token[0] == 0) + return false; + time_t now = time(0); + time_t expiresIn = getDword("ExpiresIn"); + return now < expiresIn; +} + +void CGDriveService::Login(HWND owner) +{ + ptrA token(getStringA("TokenSecret")); + ptrA refreshToken(getStringA("RefreshToken")); + if (token && refreshToken && refreshToken[0]) { + GDriveAPI::RefreshTokenRequest request(refreshToken); + NLHR_PTR response(request.Send(m_hConnection)); + + JSONNode root = GetJsonResponse(response); + + JSONNode node = root.at("access_token"); + setString("TokenSecret", node.as_string().c_str()); + + node = root.at("expires_in"); + time_t expiresIn = time(0) + node.as_int(); + setDword("ExpiresIn", expiresIn); + + return; + } + + COAuthDlg dlg(this, GOOGLE_AUTH, (MyThreadFunc)&CGDriveService::RequestAccessTokenThread); + dlg.SetParent(owner); + dlg.DoModal(); +} + +void CGDriveService::Logout() +{ + ForkThread((MyThreadFunc)&CGDriveService::RevokeAccessTokenThread); +} + +void CGDriveService::RequestAccessTokenThread(void *param) +{ + HWND hwndDlg = (HWND)param; + + if (IsLoggedIn()) + Logout(); + + char requestToken[128]; + GetDlgItemTextA(hwndDlg, IDC_OAUTH_CODE, requestToken, _countof(requestToken)); + + GDriveAPI::GetAccessTokenRequest request(requestToken); + NLHR_PTR response(request.Send(m_hConnection)); + + if (response == nullptr || response->resultCode != HTTP_CODE_OK) { + const char *error = response && response->dataLength + ? response->pData + : HttpStatusToError(response ? response->resultCode : 0); + + Netlib_Logf(m_hConnection, "%s: %s", GetAccountName(), error); + ShowNotification(TranslateT("Server does not respond"), MB_ICONERROR); + EndDialog(hwndDlg, 0); + return; + } + + JSONNode root = JSONNode::parse(response->pData); + if (root.empty()) { + Netlib_Logf(m_hConnection, "%s: %s", GetAccountName(), HttpStatusToError(response->resultCode)); + ShowNotification(TranslateT("Server does not respond"), MB_ICONERROR); + EndDialog(hwndDlg, 0); + return; + } + + JSONNode node = root.at("error_description"); + if (!node.isnull()) { + CMStringW error_description = node.as_mstring(); + Netlib_Logf(m_hConnection, "%s: %s", GetAccountName(), HttpStatusToError(response->resultCode)); + ShowNotification(error_description, MB_ICONERROR); + EndDialog(hwndDlg, 0); + return; + } + + node = root.at("access_token"); + db_set_s(0, GetAccountName(), "TokenSecret", node.as_string().c_str()); + + node = root.at("expires_in"); + time_t expiresIn = time(0) + node.as_int(); + db_set_dw(0, GetAccountName(), "ExpiresIn", expiresIn); + + node = root.at("refresh_token"); + db_set_s(0, GetAccountName(), "RefreshToken", node.as_string().c_str()); + + SetDlgItemTextA(hwndDlg, IDC_OAUTH_CODE, ""); + + EndDialog(hwndDlg, 1); +} + +void CGDriveService::RevokeAccessTokenThread(void*) +{ + ptrA token(db_get_sa(0, GetAccountName(), "TokenSecret")); + GDriveAPI::RevokeAccessTokenRequest request(token); + NLHR_PTR response(request.Send(m_hConnection)); + + delSetting("ExpiresIn"); + delSetting("TokenSecret"); + delSetting("RefreshToken"); +} + +void CGDriveService::HandleJsonError(JSONNode &node) +{ + JSONNode error = node.at("error"); + if (!error.isnull()) { + json_string tag = error.at(".tag").as_string(); + throw Exception(tag.c_str()); + } +} + +auto CGDriveService::UploadFile(const std::string &parentId, const std::string &fileName, const char *data, size_t size) +{ + ptrA token(getStringA("TokenSecret")); + GDriveAPI::UploadFileRequest request(token, parentId.c_str(), fileName.c_str(), data, size); + NLHR_PTR response(request.Send(m_hConnection)); + JSONNode root = GetJsonResponse(response); + return root["id"].as_string(); +} + +auto CGDriveService::CreateUploadSession(const std::string &parentId, const std::string &fileName) +{ + ptrA token(getStringA("TokenSecret")); + GDriveAPI::CreateUploadSessionRequest request(token, parentId.c_str(), fileName.c_str()); + NLHR_PTR response(request.Send(m_hConnection)); + + HandleHttpError(response); + + if (HTTP_CODE_SUCCESS(response->resultCode)) { + for (int i = 0; i < response->headersCount; i++) { + if (mir_strcmpi(response->headers[i].szName, "Location")) + continue; + return std::string(response->headers[i].szValue); + } + } + + HttpResponseToError(response); + + return std::string(); +} + +auto CGDriveService::UploadFileChunk(const std::string &uploadUri, const char *chunk, size_t chunkSize, uint64_t offset, uint64_t fileSize) +{ + GDriveAPI::UploadFileChunkRequest request(uploadUri.c_str(), chunk, chunkSize, offset, fileSize); + NLHR_PTR response(request.Send(m_hConnection)); + + if (response->resultCode == HTTP_CODE_PERMANENT_REDIRECT) + return std::string(); + + HandleHttpError(response); + + if (HTTP_CODE_SUCCESS(response->resultCode)) { + JSONNode root = GetJsonResponse(response); + return root["id"].as_string(); + } + + HttpResponseToError(response); + + return std::string(); +} + +auto CGDriveService::CreateFolder(const std::string &parentId, const std::string &name) +{ + ptrA token(getStringA("TokenSecret")); + GDriveAPI::GetFolderRequest getFolderRequest(token, parentId.c_str(), name.c_str()); + NLHR_PTR response(getFolderRequest.Send(m_hConnection)); + + JSONNode root = GetJsonResponse(response); + JSONNode files = root["files"].as_array(); + if (files.size() > 0) + return files[(size_t)0]["id"].as_string(); + + GDriveAPI::CreateFolderRequest createFolderRequest(token, parentId.c_str(), name.c_str()); + response = createFolderRequest.Send(m_hConnection); + + root = GetJsonResponse(response); + return root["id"].as_string(); +} + +auto CGDriveService::CreateSharedLink(const std::string &itemId) +{ + ptrA token(getStringA("TokenSecret")); + GDriveAPI::GrantPermissionsRequest request(token, itemId.c_str()); + NLHR_PTR response(request.Send(m_hConnection)); + + HandleHttpError(response); + + if (HTTP_CODE_SUCCESS(response->resultCode)) { + std::string url = GDRIVE_SHARE; + url += itemId; + return url; + } + + HttpResponseToError(response); + + return std::string(); +} + +void CGDriveService::Upload(FileTransferParam *ftp) +{ + std::string folderId = "root"; + auto serverDictionary = ftp->GetServerDirectory(); + std::string serverFolder = serverDictionary ? T2Utf(serverDictionary) : ""; + if (!serverFolder.empty()) { + folderId = CreateFolder(folderId, serverFolder); + auto link = CreateSharedLink(folderId); + ftp->AddSharedLink(link.c_str()); + } + + ftp->FirstFile(); + do { + std::string fileName = T2Utf(ftp->GetCurrentRelativeFilePath()); + uint64_t fileSize = ftp->GetCurrentFileSize(); + + size_t chunkSize = ftp->GetCurrentFileChunkSize(); + mir_ptr<char> chunk((char*)mir_calloc(chunkSize)); + + std::string fileId; + if (chunkSize == fileSize) { + ftp->CheckCurrentFile(); + size_t size = ftp->ReadCurrentFile(chunk, chunkSize); + fileId = UploadFile(folderId, fileName, chunk, size); + ftp->Progress(size); + } + else { + auto uploadUri = CreateUploadSession(folderId, fileName); + + uint64_t offset = 0; + double chunkCount = ceil(double(fileSize) / chunkSize); + for (size_t i = 0; i < chunkCount; i++) { + ftp->CheckCurrentFile(); + size_t size = ftp->ReadCurrentFile(chunk, chunkSize); + fileId = UploadFileChunk(uploadUri, chunk, size, offset, fileSize); + offset += size; + ftp->Progress(size); + } + } + + if (!ftp->IsCurrentFileInSubDirectory()) { + auto link = CreateSharedLink(fileId); + ftp->AddSharedLink(link.c_str()); + } + } while (ftp->NextFile()); +} diff --git a/protocols/CloudFile/src/Services/google_service.h b/protocols/CloudFile/src/Services/google_service.h new file mode 100644 index 0000000000..919babb86f --- /dev/null +++ b/protocols/CloudFile/src/Services/google_service.h @@ -0,0 +1,35 @@ +#ifndef _CLOUDFILE_GDRIVE_H_ +#define _CLOUDFILE_GDRIVE_H_ + +class CGDriveService : public CCloudService +{ +private: + void __cdecl RequestAccessTokenThread(void *param); + void __cdecl RevokeAccessTokenThread(void *param); + + void HandleJsonError(JSONNode &node) override; + + auto UploadFile(const std::string &parentId, const std::string &fileName, const char *data, size_t size); + auto CreateUploadSession(const std::string &parentId, const std::string &fileName); + auto UploadFileChunk(const std::string &uploadUri, const char *chunk, size_t chunkSize, uint64_t offset, uint64_t fileSize); + auto CreateFolder(const std::string &parentId, const std::string &name); + auto CreateSharedLink(const std::string &itemId); + + void Upload(FileTransferParam *ftp) override; + +public: + CGDriveService(const char *protoName, const wchar_t *userName); + + static CGDriveService* Init(const char *szModuleName, const wchar_t *szUserName); + static int UnInit(CGDriveService*); + + const char* GetModuleName() const override; + + int GetIconId() const override; + + bool IsLoggedIn() override; + void Login(HWND owner = nullptr) override; + void Logout() override; +}; + +#endif //_CLOUDFILE_GDRIVE_H_
\ No newline at end of file diff --git a/protocols/CloudFile/src/Services/microsoft_api.h b/protocols/CloudFile/src/Services/microsoft_api.h new file mode 100644 index 0000000000..6c22ac512f --- /dev/null +++ b/protocols/CloudFile/src/Services/microsoft_api.h @@ -0,0 +1,188 @@ +#ifndef _ONEDRIVESERVICE_API_H_ +#define _ONEDRIVESERVICE_API_H_ + +// https://docs.microsoft.com/onedrive/developer/rest-api/ +namespace OneDriveAPI +{ +#define MICROSOFT_OAUTH "https://login.microsoftonline.com/common/oauth2/v2.0" +#define ONEDRIVE_API "https://graph.microsoft.com/v1.0/drive" + +#define MS_APP_ID "72b87ac7-42eb-4a97-a620-91a7f8d8b5ae" +#include "../../../miranda-private-keys/Microsoft/client_secret.h" + +#define MICROSOFT_AUTH MICROSOFT_OAUTH "/authorize?response_type=code&scope=offline_access%20https%3A%2F%2Fgraph.microsoft.com%2Ffiles.readWrite&redirect_uri=https%3A%2F%2Foauth.miranda-ng.org%2Fverification&client_id=" MS_APP_ID + + class GetAccessTokenRequest : public HttpRequest + { + public: + GetAccessTokenRequest(const char *code) : + HttpRequest(REQUEST_POST, MICROSOFT_OAUTH "/token") + { + AddHeader("Content-Type", "application/x-www-form-urlencoded"); + + CMStringA data = "redirect_uri=https://oauth.miranda-ng.org/verification"; + data.Append("&scope=offline_access https://graph.microsoft.com/files.readWrite"); + data.AppendFormat("&client_id=%s&client_secret=%s", MS_APP_ID, MS_CLIENT_SECRET); + data.AppendFormat("&grant_type=authorization_code&code=%s", code); + SetData(data.GetBuffer(), data.GetLength()); + } + }; + + class RefreshTokenRequest : public HttpRequest + { + public: + RefreshTokenRequest(const char *refreshToken) : + HttpRequest(REQUEST_POST, MICROSOFT_OAUTH "/token") + { + AddHeader("Content-Type", "application/x-www-form-urlencoded"); + + CMStringA data = "redirect_uri=https://oauth.miranda-ng.org/verification"; + data.Append("&scope=offline_access https://graph.microsoft.com/files.readWrite"); + data.AppendFormat("&client_id=%s&client_secret=%s", MS_APP_ID, MS_CLIENT_SECRET); + data.AppendFormat("&grant_type=refresh_token&refresh_token=%s", refreshToken); + SetData(data.GetBuffer(), data.GetLength()); + } + }; + + class UploadFileRequest : public HttpRequest + { + public: + UploadFileRequest(const char *token, const char *name, const char *data, size_t size, OnConflict strategy = NONE) : + HttpRequest(REQUEST_PUT, FORMAT, ONEDRIVE_API "/special/approot:/%s:/content", mir_urlEncode(name).c_str()) + { + AddUrlParameter("select=id"); + + if (strategy == OnConflict::RENAME) + AddUrlParameter("@microsoft.graph.conflictBehavior=rename"); + else if (strategy == OnConflict::REPLACE) + AddUrlParameter("@microsoft.graph.conflictBehavior=replace"); + + AddBearerAuthHeader(token); + + SetData(data, size); + } + + UploadFileRequest(const char *token, const char *parentId, const char *name, const char *data, size_t size, OnConflict strategy = NONE) : + HttpRequest(REQUEST_PUT, FORMAT, ONEDRIVE_API "/items/%s:/%s:/content", parentId, mir_urlEncode(name).c_str()) + { + AddUrlParameter("select=id"); + + if (strategy == OnConflict::RENAME) + AddUrlParameter("@microsoft.graph.conflictBehavior=rename"); + else if (strategy == OnConflict::REPLACE) + AddUrlParameter("@microsoft.graph.conflictBehavior=replace"); + + AddBearerAuthHeader(token); + + SetData(data, size); + } + }; + + class CreateUploadSessionRequest : public HttpRequest + { + public: + CreateUploadSessionRequest(const char *token, const char *name, OnConflict strategy = NONE) : + HttpRequest(REQUEST_POST, FORMAT, ONEDRIVE_API "/special/approot:/%s:/createUploadSession", mir_urlEncode(name).c_str()) + { + AddBearerAuthHeader(token); + AddHeader("Content-Type", "application/json"); + + JSONNode item(JSON_NODE); + item.set_name("item"); + if (strategy == OnConflict::RENAME) + item << JSONNode("@microsoft.graph.conflictBehavior", "rename"); + if (strategy == OnConflict::REPLACE) + item << JSONNode("@microsoft.graph.conflictBehavior", "replace"); + + JSONNode params(JSON_NODE); + params << item; + + json_string data = params.write(); + SetData(data.c_str(), data.length()); + } + + CreateUploadSessionRequest(const char *token, const char *parentId, const char *name, OnConflict strategy = NONE) : + HttpRequest(REQUEST_POST, FORMAT, ONEDRIVE_API "/items/%s:/%s:/createUploadSession", parentId, mir_urlEncode(name).c_str()) + { + AddBearerAuthHeader(token); + AddHeader("Content-Type", "application/json"); + + JSONNode item(JSON_NODE); + item.set_name("item"); + if (strategy == OnConflict::RENAME) + item << JSONNode("@microsoft.graph.conflictBehavior", "rename"); + if (strategy == OnConflict::REPLACE) + item << JSONNode("@microsoft.graph.conflictBehavior", "replace"); + + JSONNode params(JSON_NODE); + params << item; + + json_string data = params.write(); + SetData(data.c_str(), data.length()); + } + }; + + class UploadFileChunkRequest : public HttpRequest + { + public: + UploadFileChunkRequest(const char *uploadUri, const char *chunk, size_t chunkSize, uint64_t offset, uint64_t fileSize) : + HttpRequest(REQUEST_PUT, uploadUri) + { + AddUrlParameter("select=id"); + + uint64_t rangeMin = offset; + uint64_t rangeMax = offset + chunkSize - 1; + CMStringA range(CMStringDataFormat::FORMAT, "bytes %I64u-%I64u/%I64u", rangeMin, rangeMax, fileSize); + AddHeader("Content-Range", range); + + SetData(chunk, chunkSize); + } + }; + + class CreateFolderRequest : public HttpRequest + { + public: + CreateFolderRequest(const char *token, const char *path) : + HttpRequest(REQUEST_POST, ONEDRIVE_API "/special/approot/children") + { + AddUrlParameter("select=id"); + + AddBearerAuthHeader(token); + AddHeader("Content-Type", "application/json"); + + JSONNode folder(JSON_NODE); + folder.set_name("folder"); + + JSONNode params(JSON_NODE); + params + << JSONNode("name", path) + << folder; + + json_string data = params.write(); + SetData(data.c_str(), data.length()); + } + }; + + class CreateSharedLinkRequest : public HttpRequest + { + public: + CreateSharedLinkRequest(const char *token, const char *itemId) : + HttpRequest(REQUEST_POST, FORMAT, ONEDRIVE_API "/items/%s/createLink", itemId) + { + AddUrlParameter("select=link"); + + AddBearerAuthHeader(token); + AddHeader("Content-Type", "application/json"); + + JSONNode params(JSON_NODE); + params + << JSONNode("type", "view") + << JSONNode("scope", "anonymous"); + + json_string data = params.write(); + SetData(data.c_str(), data.length()); + } + }; +}; + +#endif //_ONEDRIVESERVICE_API_H_ diff --git a/protocols/CloudFile/src/Services/microsoft_service.cpp b/protocols/CloudFile/src/Services/microsoft_service.cpp new file mode 100644 index 0000000000..992312e2a8 --- /dev/null +++ b/protocols/CloudFile/src/Services/microsoft_service.cpp @@ -0,0 +1,269 @@ +#include "..\stdafx.h" +#include "microsoft_api.h" + +struct CMPluginOnedrive : public CMPluginBase +{ + CMPluginOnedrive() : + CMPluginBase(MODULENAME "/OneDrive", pluginInfoEx) + { + m_hInst = g_plugin.getInst(); + + RegisterProtocol(PROTOTYPE_PROTOWITHACCS, (pfnInitProto)COneDriveService::Init, (pfnUninitProto)COneDriveService::UnInit); + } +} +g_pluginOnedrive; + +///////////////////////////////////////////////////////////////////////////////////////// + +COneDriveService::COneDriveService(const char *protoName, const wchar_t *userName) : + CCloudService(protoName, userName, &g_pluginOnedrive) +{ + m_hProtoIcon = GetIconHandle(IDI_ONEDRIVE); +} + +COneDriveService* COneDriveService::Init(const char *moduleName, const wchar_t *userName) +{ + COneDriveService *proto = new COneDriveService(moduleName, userName); + Services.insert(proto); + return proto; +} + +int COneDriveService::UnInit(COneDriveService *proto) +{ + Services.remove(proto); + delete proto; + return 0; +} + +const char* COneDriveService::GetModuleName() const +{ + return "/OneDrive"; +} + +int COneDriveService::GetIconId() const +{ + return IDI_ONEDRIVE; +} + +bool COneDriveService::IsLoggedIn() +{ + ptrA token(getStringA("TokenSecret")); + if (!token || token[0] == 0) + return false; + time_t now = time(0); + time_t expiresIn = getDword("ExpiresIn"); + return now < expiresIn; +} + +void COneDriveService::Login(HWND owner) +{ + ptrA refreshToken(getStringA("RefreshToken")); + if (refreshToken && refreshToken[0]) { + OneDriveAPI::RefreshTokenRequest request(refreshToken); + NLHR_PTR response(request.Send(m_hConnection)); + + JSONNode root = GetJsonResponse(response); + + JSONNode node = root.at("access_token"); + db_set_s(0, GetAccountName(), "TokenSecret", node.as_string().c_str()); + + node = root.at("expires_in"); + time_t expiresIn = time(0) + node.as_int(); + setDword("ExpiresIn", expiresIn); + + return; + } + + COAuthDlg dlg(this, MICROSOFT_AUTH, (MyThreadFunc)&COneDriveService::RequestAccessTokenThread); + dlg.SetParent(owner); + dlg.DoModal(); +} + +void COneDriveService::Logout() +{ + delSetting("ExpiresIn"); + delSetting("TokenSecret"); + delSetting("RefreshToken"); +} + +void COneDriveService::RequestAccessTokenThread(void *param) +{ + HWND hwndDlg = (HWND)param; + + if (IsLoggedIn()) + Logout(); + + char requestToken[128]; + GetDlgItemTextA(hwndDlg, IDC_OAUTH_CODE, requestToken, _countof(requestToken)); + + OneDriveAPI::GetAccessTokenRequest request(requestToken); + NLHR_PTR response(request.Send(m_hConnection)); + + if (response == nullptr || response->resultCode != HTTP_CODE_OK) { + const char *error = response->dataLength + ? response->pData + : HttpStatusToError(response->resultCode); + + Netlib_Logf(m_hConnection, "%s: %s", GetAccountName(), error); + ShowNotification(TranslateT("Server does not respond"), MB_ICONERROR); + EndDialog(hwndDlg, 0); + return; + } + + JSONNode root = JSONNode::parse(response->pData); + if (root.empty()) { + Netlib_Logf(m_hConnection, "%s: %s", GetAccountName(), HttpStatusToError(response->resultCode)); + ShowNotification(TranslateT("Server does not respond"), MB_ICONERROR); + EndDialog(hwndDlg, 0); + return; + } + + JSONNode node = root.at("error_description"); + if (!node.isnull()) { + CMStringW error_description = node.as_mstring(); + Netlib_Logf(m_hConnection, "%s: %s", GetAccountName(), HttpStatusToError(response->resultCode)); + ShowNotification(error_description, MB_ICONERROR); + EndDialog(hwndDlg, 0); + return; + } + + node = root.at("access_token"); + setString("TokenSecret", node.as_string().c_str()); + + node = root.at("expires_in"); + time_t expiresIn = time(0) + node.as_int(); + setDword("ExpiresIn", expiresIn); + + node = root.at("refresh_token"); + setString("RefreshToken", node.as_string().c_str()); + + SetDlgItemTextA(hwndDlg, IDC_OAUTH_CODE, ""); + + EndDialog(hwndDlg, 1); +} + +void COneDriveService::HandleJsonError(JSONNode &node) +{ + JSONNode error = node.at("error"); + if (!error.isnull()) { + json_string tag = error.at("message").as_string(); + throw Exception(tag.c_str()); + } +} + +auto COneDriveService::UploadFile(const std::string &parentId, const std::string &fileName, const char *data, size_t size) +{ + ptrA token(getStringA("TokenSecret")); + BYTE strategy = g_plugin.getByte("ConflictStrategy", OnConflict::REPLACE); + OneDriveAPI::UploadFileRequest *request = !parentId.empty() + ? new OneDriveAPI::UploadFileRequest(token, parentId.c_str(), fileName.c_str(), data, size, (OnConflict)strategy) + : new OneDriveAPI::UploadFileRequest(token, fileName.c_str(), data, size, (OnConflict)strategy); + NLHR_PTR response(request->Send(m_hConnection)); + delete request; + + JSONNode root = GetJsonResponse(response); + return root["id"].as_string(); +} + +auto COneDriveService::CreateUploadSession(const std::string &parentId, const std::string &fileName) +{ + ptrA token(getStringA("TokenSecret")); + BYTE strategy = g_plugin.getByte("ConflictStrategy", OnConflict::REPLACE); + OneDriveAPI::CreateUploadSessionRequest *request = !parentId.empty() + ? new OneDriveAPI::CreateUploadSessionRequest(token, parentId.c_str(), fileName.c_str(), (OnConflict)strategy) + : new OneDriveAPI::CreateUploadSessionRequest(token, fileName.c_str(), (OnConflict)strategy); + NLHR_PTR response(request->Send(m_hConnection)); + delete request; + + JSONNode root = GetJsonResponse(response); + return root["uploadUrl"].as_string(); +} + +auto COneDriveService::UploadFileChunk(const std::string &uploadUri, const char *chunk, size_t chunkSize, uint64_t offset, uint64_t fileSize) +{ + OneDriveAPI::UploadFileChunkRequest request(uploadUri.c_str(), chunk, chunkSize, offset, fileSize); + NLHR_PTR response(request.Send(m_hConnection)); + + HandleHttpError(response); + + if (response->resultCode == HTTP_CODE_ACCEPTED) + return std::string(); + + if (HTTP_CODE_SUCCESS(response->resultCode)) { + JSONNode root = GetJsonResponse(response); + return root["id"].as_string(); + } + + HttpResponseToError(response); + + return std::string(); +} + +auto COneDriveService::CreateFolder(const std::string &path) +{ + ptrA token(getStringA("TokenSecret")); + OneDriveAPI::CreateFolderRequest request(token, path.c_str()); + NLHR_PTR response(request.Send(m_hConnection)); + + JSONNode root = GetJsonResponse(response); + return root["id"].as_string(); +} + +auto COneDriveService::CreateSharedLink(const std::string &itemId) +{ + ptrA token(getStringA("TokenSecret")); + OneDriveAPI::CreateSharedLinkRequest request(token, itemId.c_str()); + NLHR_PTR response(request.Send(m_hConnection)); + + JSONNode root = GetJsonResponse(response); + return root["link"]["webUrl"].as_string(); +} + +void COneDriveService::Upload(FileTransferParam *ftp) +{ + std::string folderId; + auto serverDictionary = ftp->GetServerDirectory(); + std::string serverFolder = serverDictionary ? T2Utf(serverDictionary) : ""; + if (!serverFolder.empty()) { + folderId = CreateFolder(serverFolder); + auto link = CreateSharedLink(folderId); + ftp->AddSharedLink(link.c_str()); + } + + ftp->FirstFile(); + do { + std::string fileName = T2Utf(ftp->GetCurrentRelativeFilePath()); + uint64_t fileSize = ftp->GetCurrentFileSize(); + + size_t chunkSize = ftp->GetCurrentFileChunkSize(); + mir_ptr<char> chunk((char*)mir_calloc(chunkSize)); + + std::string fileId; + if (chunkSize == fileSize) { + ftp->CheckCurrentFile(); + size_t size = ftp->ReadCurrentFile(chunk, chunkSize); + + fileId = UploadFile(folderId, fileName, chunk, size); + + ftp->Progress(size); + } + else { + auto uploadUri = CreateUploadSession(folderId, fileName); + + uint64_t offset = 0; + double chunkCount = ceil(double(fileSize) / chunkSize); + for (size_t i = 0; i < chunkCount; i++) { + ftp->CheckCurrentFile(); + size_t size = ftp->ReadCurrentFile(chunk, chunkSize); + fileId = UploadFileChunk(uploadUri, chunk, size, offset, fileSize); + offset += size; + ftp->Progress(size); + } + } + + if (!ftp->IsCurrentFileInSubDirectory()) { + auto link = CreateSharedLink(fileId); + ftp->AddSharedLink(link.c_str()); + } + } while (ftp->NextFile()); +} diff --git a/protocols/CloudFile/src/Services/microsoft_service.h b/protocols/CloudFile/src/Services/microsoft_service.h new file mode 100644 index 0000000000..d993410003 --- /dev/null +++ b/protocols/CloudFile/src/Services/microsoft_service.h @@ -0,0 +1,34 @@ +#ifndef _CLOUDFILE_ONEDRIVE_H_ +#define _CLOUDFILE_ONEDRIVE_H_ + +class COneDriveService : public CCloudService +{ +private: + void __cdecl RequestAccessTokenThread(void *param); + + void HandleJsonError(JSONNode &node) override; + + auto UploadFile(const std::string &parentId, const std::string &fileName, const char *data, size_t size); + auto CreateUploadSession(const std::string &parentId, const std::string &fileName); + auto UploadFileChunk(const std::string &uploadUri, const char *chunk, size_t chunkSize, uint64_t offset, uint64_t fileSize); + auto CreateFolder(const std::string &name); + auto CreateSharedLink(const std::string &itemId); + + void Upload(FileTransferParam *ftp) override; + +public: + COneDriveService(const char *protoName, const wchar_t *userName); + + static COneDriveService* Init(const char *szModuleName, const wchar_t *szUserName); + static int UnInit(COneDriveService*); + + const char* GetModuleName() const override; + + int GetIconId() const override; + + bool IsLoggedIn() override; + void Login(HWND owner = nullptr) override; + void Logout() override; +}; + +#endif //_CLOUDFILE_ONEDRIVE_H_
\ No newline at end of file diff --git a/protocols/CloudFile/src/Services/yandex_api.h b/protocols/CloudFile/src/Services/yandex_api.h new file mode 100644 index 0000000000..c7c3dfe19d --- /dev/null +++ b/protocols/CloudFile/src/Services/yandex_api.h @@ -0,0 +1,131 @@ +#ifndef _YANDEXSERVICE_API_H_ +#define _YANDEXSERVICE_API_H_ + +// https://tech.yandex.ru/disk/api/concepts/about-docpage/ +namespace YandexAPI +{ +#define YANDEX_OAUTH "https://oauth.yandex.ru" +#define YADISK_API_VER "/v1" +#define YADISK_API "https://cloud-api.yandex.net" YADISK_API_VER "/disk/resources" + +#define YANDEX_APP_ID "c311a5967cae4efa88d1af97d01ea0e8" +#include "../../../miranda-private-keys/Yandex/client_secret.h" + +#define YANDEX_AUTH YANDEX_OAUTH "/authorize?response_type=code&client_id=" YANDEX_APP_ID + + class GetAccessTokenRequest : public HttpRequest + { + public: + GetAccessTokenRequest(const char *code) : + HttpRequest(REQUEST_POST, YANDEX_OAUTH "/token") + { + AddHeader("Content-Type", "application/x-www-form-urlencoded"); + + CMStringA data(CMStringDataFormat::FORMAT, + "client_id=%s&client_secret=%s&grant_type=authorization_code&code=%s", + YANDEX_APP_ID, YADISK_CLIENT_SECRET, code); + SetData(data.GetBuffer(), data.GetLength()); + } + }; + + class RefreshTokenRequest : public HttpRequest + { + public: + RefreshTokenRequest(const char *refreshToken) : + HttpRequest(REQUEST_POST, YANDEX_OAUTH "/token") + { + AddHeader("Content-Type", "application/x-www-form-urlencoded"); + + CMStringA data(CMStringDataFormat::FORMAT, + "client_id=%s&client_secret=%s&grant_type=refresh_token&refresh_token=%s", + YANDEX_APP_ID, YADISK_CLIENT_SECRET, refreshToken); + SetData(data.GetBuffer(), data.GetLength()); + } + }; + + class RevokeAccessTokenRequest : public HttpRequest + { + public: + RevokeAccessTokenRequest(const char *token) : + HttpRequest(REQUEST_POST, YANDEX_OAUTH "/token/revoke") + { + AddOAuthHeader(token); + } + }; + + class GetUploadUrlRequest : public HttpRequest + { + public: + GetUploadUrlRequest(const char *token, const char *path, OnConflict strategy = NONE) : + HttpRequest(REQUEST_GET, YADISK_API "/upload") + { + AddOAuthHeader(token); + AddUrlParameter("path=app:%s", mir_urlEncode(path).c_str()); + AddUrlParameter("fields=href"); + if (strategy == OnConflict::REPLACE) + AddUrlParameter("overwrite=true"); + } + }; + + class UploadFileRequest : public HttpRequest + { + public: + UploadFileRequest(const char *url, const char *data, size_t size) : + HttpRequest(REQUEST_PUT, url) + { + SetData(data, size); + } + }; + + class UploadFileChunkRequest : public HttpRequest + { + public: + UploadFileChunkRequest(const char *url, const char *chunk, size_t chunkSize, uint64_t offset, uint64_t fileSize) : + HttpRequest(REQUEST_PUT, url) + { + uint64_t rangeMin = offset; + uint64_t rangeMax = offset + chunkSize - 1; + CMStringA range(CMStringDataFormat::FORMAT, "bytes %I64u-%I64u/%I64u", rangeMin, rangeMax, fileSize); + AddHeader("Content-Range", range); + + SetData(chunk, chunkSize); + } + }; + + class CreateFolderRequest : public HttpRequest + { + public: + CreateFolderRequest(const char *token, const char *path) : + HttpRequest(REQUEST_PUT, YADISK_API) + { + AddOAuthHeader(token); + AddUrlParameterWithEncode("path", "app:%s", path); + AddUrlParameter("fields=href"); + } + }; + + class PublishRequest : public HttpRequest + { + public: + PublishRequest(const char *token, const char *path) : + HttpRequest(REQUEST_PUT, YADISK_API "/publish") + { + AddOAuthHeader(token); + AddUrlParameter("path=app:%s", mir_urlEncode(path).c_str()); + } + }; + + class GetResourcesRequest : public HttpRequest + { + public: + GetResourcesRequest(const char *token, const char *path) : + HttpRequest(REQUEST_GET, YADISK_API) + { + AddOAuthHeader(token); + AddUrlParameter("path=app:%s", mir_urlEncode(path).c_str()); + AddUrlParameter("fields=public_url"); + } + }; +}; + +#endif //_YANDEXSERVICE_API_H_ diff --git a/protocols/CloudFile/src/Services/yandex_service.cpp b/protocols/CloudFile/src/Services/yandex_service.cpp new file mode 100644 index 0000000000..4bd0210ca1 --- /dev/null +++ b/protocols/CloudFile/src/Services/yandex_service.cpp @@ -0,0 +1,290 @@ +#include "..\stdafx.h" +#include "yandex_api.h" + +struct CMPluginYandex : public CMPluginBase +{ + CMPluginYandex() : + CMPluginBase(MODULENAME "/YandexDisk", pluginInfoEx) + { + m_hInst = g_plugin.getInst(); + + RegisterProtocol(PROTOTYPE_PROTOWITHACCS, (pfnInitProto)CYandexService::Init, (pfnUninitProto)CYandexService::UnInit); + } +} +g_pluginYandex; + +///////////////////////////////////////////////////////////////////////////////////////// + +CYandexService::CYandexService(const char *protoName, const wchar_t *userName) : + CCloudService(protoName, userName, &g_pluginYandex) +{ + m_hProtoIcon = GetIconHandle(IDI_YADISK); +} + +CYandexService* CYandexService::Init(const char *moduleName, const wchar_t *userName) +{ + CYandexService *proto = new CYandexService(moduleName, userName); + Services.insert(proto); + return proto; +} + +int CYandexService::UnInit(CYandexService *proto) +{ + Services.remove(proto); + delete proto; + return 0; +} + +const char* CYandexService::GetModuleName() const +{ + return "Yandex.Disk"; +} + +int CYandexService::GetIconId() const +{ + return IDI_YADISK; +} + +bool CYandexService::IsLoggedIn() +{ + ptrA token(getStringA("TokenSecret")); + if (!token || token[0] == 0) + return false; + time_t now = time(0); + time_t expiresIn = getDword("ExpiresIn"); + return now < expiresIn; +} + +void CYandexService::Login(HWND owner) +{ + ptrA token(getStringA("TokenSecret")); + ptrA refreshToken(getStringA("RefreshToken")); + if (token && refreshToken && refreshToken[0]) { + YandexAPI::RefreshTokenRequest request(refreshToken); + NLHR_PTR response(request.Send(m_hConnection)); + + JSONNode root = GetJsonResponse(response); + + JSONNode node = root.at("access_token"); + setString("TokenSecret", node.as_string().c_str()); + + node = root.at("expires_in"); + time_t expiresIn = time(0) + node.as_int(); + setDword("ExpiresIn", expiresIn); + + node = root.at("refresh_token"); + setString("RefreshToken", node.as_string().c_str()); + + return; + } + + COAuthDlg dlg(this, YANDEX_AUTH, (MyThreadFunc)&CYandexService::RequestAccessTokenThread); + dlg.SetParent(owner); + dlg.DoModal(); +} + +void CYandexService::Logout() +{ + ForkThread((MyThreadFunc)&CYandexService::RevokeAccessTokenThread); +} + +void CYandexService::RequestAccessTokenThread(void *param) +{ + HWND hwndDlg = (HWND)param; + + if (IsLoggedIn()) + Logout(); + + char requestToken[128]; + GetDlgItemTextA(hwndDlg, IDC_OAUTH_CODE, requestToken, _countof(requestToken)); + + YandexAPI::GetAccessTokenRequest request(requestToken); + NLHR_PTR response(request.Send(m_hConnection)); + + if (response == nullptr || response->resultCode != HTTP_CODE_OK) { + const char *error = response->dataLength + ? response->pData + : HttpStatusToError(response->resultCode); + + Netlib_Logf(m_hConnection, "%s: %s", GetAccountName(), error); + ShowNotification(TranslateT("Server does not respond"), MB_ICONERROR); + EndDialog(hwndDlg, 0); + return; + } + + JSONNode root = JSONNode::parse(response->pData); + if (root.empty()) { + Netlib_Logf(m_hConnection, "%s: %s", GetAccountName(), HttpStatusToError(response->resultCode)); + ShowNotification(TranslateT("Server does not respond"), MB_ICONERROR); + EndDialog(hwndDlg, 0); + return; + } + + JSONNode node = root.at("error_description"); + if (!node.isnull()) { + CMStringW error_description = node.as_mstring(); + Netlib_Logf(m_hConnection, "%s: %s", GetAccountName(), HttpStatusToError(response->resultCode)); + ShowNotification(error_description, MB_ICONERROR); + EndDialog(hwndDlg, 0); + return; + } + + node = root.at("access_token"); + setString("TokenSecret", node.as_string().c_str()); + + node = root.at("expires_in"); + time_t expiresIn = time(0) + node.as_int(); + setDword("ExpiresIn", expiresIn); + + node = root.at("refresh_token"); + setString("RefreshToken", node.as_string().c_str()); + + SetDlgItemTextA(hwndDlg, IDC_OAUTH_CODE, ""); + + EndDialog(hwndDlg, 1); +} + +void CYandexService::RevokeAccessTokenThread(void*) +{ + ptrA token(db_get_sa(0, GetAccountName(), "TokenSecret")); + YandexAPI::RevokeAccessTokenRequest request(token); + NLHR_PTR response(request.Send(m_hConnection)); + + delSetting("ExpiresIn"); + delSetting("TokenSecret"); + delSetting("RefreshToken"); +} + +void CYandexService::HandleJsonError(JSONNode &node) +{ + JSONNode error = node.at("error"); + if (!error.isnull()) { + json_string tag = error.at(".tag").as_string(); + throw Exception(tag.c_str()); + } +} + +auto CYandexService::CreateUploadSession(const std::string &path) +{ + ptrA token(getStringA("TokenSecret")); + BYTE strategy = g_plugin.getByte("ConflictStrategy", OnConflict::REPLACE); + YandexAPI::GetUploadUrlRequest request(token, path.c_str(), (OnConflict)strategy); + NLHR_PTR response(request.Send(m_hConnection)); + + JSONNode root = GetJsonResponse(response); + return root["href"].as_string(); +} + +void CYandexService::UploadFile(const std::string &uploadUri, const char *data, size_t size) +{ + YandexAPI::UploadFileRequest request(uploadUri.c_str(), data, size); + NLHR_PTR response(request.Send(m_hConnection)); + + HandleHttpError(response); + + if (response->resultCode == HTTP_CODE_CREATED) + return; + + HttpResponseToError(response); +} + +void CYandexService::UploadFileChunk(const std::string &uploadUri, const char *chunk, size_t chunkSize, uint64_t offset, uint64_t fileSize) +{ + YandexAPI::UploadFileChunkRequest request(uploadUri.c_str(), chunk, chunkSize, offset, fileSize); + NLHR_PTR response(request.Send(m_hConnection)); + + HandleHttpError(response); + + if (response->resultCode == HTTP_CODE_ACCEPTED || + response->resultCode == HTTP_CODE_CREATED) + return; + + HttpResponseToError(response); +} + +void CYandexService::CreateFolder(const std::string &path) +{ + ptrA token(getStringA("TokenSecret")); + YandexAPI::CreateFolderRequest request(token, path.c_str()); + NLHR_PTR response(request.Send(m_hConnection)); + + if (HTTP_CODE_SUCCESS(response->resultCode)) { + GetJsonResponse(response); + return; + } + + // forder exists on server + if (response->resultCode == HTTP_CODE_CONFLICT) { + return; + } + + HttpResponseToError(response); +} + +auto CYandexService::CreateSharedLink(const std::string &path) +{ + ptrA token(getStringA("TokenSecret")); + YandexAPI::PublishRequest publishRequest(token, path.c_str()); + NLHR_PTR response(publishRequest.Send(m_hConnection)); + + GetJsonResponse(response); + + YandexAPI::GetResourcesRequest resourcesRequest(token, path.c_str()); + response = resourcesRequest.Send(m_hConnection); + + JSONNode root = GetJsonResponse(response); + return root["public_url"].as_string(); +} + +void CYandexService::Upload(FileTransferParam *ftp) +{ + auto serverDictionary = ftp->GetServerDirectory(); + std::string serverFolder = serverDictionary ? T2Utf(serverDictionary) : ""; + if (!serverFolder.empty()) { + auto path = PreparePath(serverFolder); + CreateFolder(path); + auto link = CreateSharedLink(path); + ftp->AddSharedLink(link.c_str()); + } + + ftp->FirstFile(); + do + { + std::string fileName = T2Utf(ftp->GetCurrentRelativeFilePath()); + uint64_t fileSize = ftp->GetCurrentFileSize(); + + size_t chunkSize = ftp->GetCurrentFileChunkSize(); + mir_ptr<char> chunk((char*)mir_calloc(chunkSize)); + + std::string path; + if (!serverFolder.empty()) + path = "/" + serverFolder + "/" + fileName; + else + path = PreparePath(fileName); + + auto uploadUri = CreateUploadSession(path); + + if (chunkSize == fileSize) { + ftp->CheckCurrentFile(); + size_t size = ftp->ReadCurrentFile(chunk, chunkSize); + UploadFile(uploadUri, chunk, size); + ftp->Progress(size); + } + else { + uint64_t offset = 0; + double chunkCount = ceil(double(fileSize) / chunkSize); + for (size_t i = 0; i < chunkCount; i++) { + ftp->CheckCurrentFile(); + size_t size = ftp->ReadCurrentFile(chunk, chunkSize); + UploadFileChunk(uploadUri, chunk, size, offset, fileSize); + offset += size; + ftp->Progress(size); + } + } + + if (!ftp->IsCurrentFileInSubDirectory()) { + auto link = CreateSharedLink(path); + ftp->AddSharedLink(link.c_str()); + } + } while (ftp->NextFile()); +} diff --git a/protocols/CloudFile/src/Services/yandex_service.h b/protocols/CloudFile/src/Services/yandex_service.h new file mode 100644 index 0000000000..0fdcdf679a --- /dev/null +++ b/protocols/CloudFile/src/Services/yandex_service.h @@ -0,0 +1,35 @@ +#ifndef _CLOUDFILE_YANDEX_H_ +#define _CLOUDFILE_YANDEX_H_ + +class CYandexService : public CCloudService +{ +private: + void __cdecl RequestAccessTokenThread(void *param); + void __cdecl RevokeAccessTokenThread(void *param); + + void HandleJsonError(JSONNode &node) override; + + auto CreateUploadSession(const std::string &path); + void UploadFile(const std::string &uploadUri, const char *data, size_t size); + void UploadFileChunk(const std::string &uploadUri, const char *chunk, size_t chunkSize, uint64_t offset, uint64_t fileSize); + void CreateFolder(const std::string &path); + auto CreateSharedLink(const std::string &path); + + void Upload(FileTransferParam *ftp) override; + +public: + CYandexService(const char *protoName, const wchar_t *userName); + + static CYandexService* Init(const char *szModuleName, const wchar_t *szUserName); + static int UnInit(CYandexService*); + + const char* GetModuleName() const override; + + int GetIconId() const override; + + bool IsLoggedIn() override; + void Login(HWND owner = nullptr) override; + void Logout() override; +}; + +#endif //_CLOUDFILE_YANDEX_H_
\ No newline at end of file diff --git a/protocols/CloudFile/src/cloud_file.cpp b/protocols/CloudFile/src/cloud_file.cpp new file mode 100644 index 0000000000..f6019f8196 --- /dev/null +++ b/protocols/CloudFile/src/cloud_file.cpp @@ -0,0 +1,197 @@ +#include "stdafx.h" + +CCloudService::CCloudService(const char *protoName, const wchar_t *userName, HPLUGIN pPlugin) + : PROTO<CCloudService>(protoName, userName), + m_pPlugin(pPlugin) +{ + NETLIBUSER nlu = {}; + nlu.flags = NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; + nlu.szSettingsModule = (char*)protoName; + nlu.szDescriptiveName.w = (wchar_t*)userName; + m_hConnection = Netlib_RegisterUser(&nlu); + + CreateProtoService(PS_CREATEACCMGRUI, &CCloudService::OnAccountManagerInit); +} + +CCloudService::~CCloudService() +{ + Netlib_CloseHandle(m_hConnection); + m_hConnection = nullptr; +} + +void CCloudService::OnErase() +{ + KillModuleMenus(m_pPlugin); +} + +HPLUGIN CCloudService::GetId() const +{ + return m_pPlugin; +} + +const char* CCloudService::GetAccountName() const +{ + return m_szModuleName; +} + +const wchar_t* CCloudService::GetUserName() const +{ + return m_tszUserName; +} + +INT_PTR CCloudService::GetCaps(int type, MCONTACT) +{ + switch (type) { + case PFLAGNUM_1: + return PF1_FILESEND; + case PFLAGNUM_2: + case PFLAGNUM_5: + return PF2_NONE; + default: + return 0; + } +} + +int CCloudService::FileCancel(MCONTACT, HANDLE hTransfer) +{ + FileTransferParam *ftp = Transfers.find((FileTransferParam*)&hTransfer); + if (ftp) + ftp->Terminate(); + + return 0; +} + +HANDLE CCloudService::SendFile(MCONTACT hContact, const wchar_t *description, wchar_t **paths) +{ + FileTransferParam *ftp = new FileTransferParam(hContact); + ftp->SetDescription(description); + ftp->SetWorkingDirectory(paths[0]); + for (int i = 0; paths[i]; i++) { + if (PathIsDirectory(paths[i])) + continue; + ftp->AddFile(paths[i]); + } + Transfers.insert(ftp); + mir_forkthreadowner(UploadAndReportProgressThread, this, ftp); + return (HANDLE)ftp->GetId(); +} + +void CCloudService::OpenUploadDialog(MCONTACT hContact) +{ + char *proto = GetContactProto(hContact); + if (!mir_strcmpi(proto, META_PROTO)) + hContact = db_mc_getMostOnline(hContact); + + auto it = InterceptedContacts.find(hContact); + if (it == InterceptedContacts.end()) { + HWND hwnd = (HWND)CallService(MS_FILE_SENDFILE, hContact, 0); + InterceptedContacts[hContact] = hwnd; + } + else + SetActiveWindow(it->second); +} + +INT_PTR CCloudService::OnAccountManagerInit(WPARAM, LPARAM lParam) +{ + CAccountManagerDlg *page = new CAccountManagerDlg(this); + page->SetParent((HWND)lParam); + page->Show(); + return (INT_PTR)page->GetHwnd(); +} + +std::string CCloudService::PreparePath(const std::string &path) const +{ + std::string newPath = path; + if (newPath[0] != '/') + newPath.insert(0, "/"); + std::replace(newPath.begin(), newPath.end(), '\\', '/'); + size_t pos = newPath.find("//"); + while (pos != std::string::npos) { + newPath.replace(pos, 2, "/"); + pos = newPath.find("//", pos + 1); + } + return newPath; +} + +char* CCloudService::HttpStatusToError(int status) +{ + switch (status) { + case HTTP_CODE_OK: + return "OK"; + case HTTP_CODE_BAD_REQUEST: + return "Bad input parameter. Error message should indicate which one and why"; + case HTTP_CODE_UNAUTHORIZED: + return "Bad or expired token. This can happen if the user or Dropbox revoked or expired an access token. To fix, you should re-authenticate the user"; + case HTTP_CODE_FORBIDDEN: + return "Bad OAuth request (wrong consumer key, bad nonce, expired timestamp...). Unfortunately, re-authenticating the user won't help here"; + case HTTP_CODE_NOT_FOUND: + return "File or folder not found at the specified path"; + case HTTP_CODE_METHOD_NOT_ALLOWED: + return "Request method not expected (generally should be GET or POST)"; + case HTTP_CODE_TOO_MANY_REQUESTS: + return "Your app is making too many requests and is being rate limited. 429s can trigger on a per-app or per-user basis"; + case HTTP_CODE_SERVICE_UNAVAILABLE: + return "If the response includes the Retry-After header, this means your OAuth 1.0 app is being rate limited. Otherwise, this indicates a transient server error, and your app should retry its request."; + } + + return "Unknown error"; +} + +void CCloudService::HttpResponseToError(NETLIBHTTPREQUEST *response) +{ + if (response == nullptr) + throw Exception(HttpStatusToError()); + if (response->dataLength) + throw Exception(response->pData); + throw Exception(HttpStatusToError(response->resultCode)); +} + +void CCloudService::HandleHttpError(NETLIBHTTPREQUEST *response) +{ + if (response == nullptr) + throw Exception(HttpStatusToError()); + + if (HTTP_CODE_SUCCESS(response->resultCode)) + return; + + if (response->resultCode == HTTP_CODE_UNAUTHORIZED) + delSetting("TokenSecret"); + + HttpResponseToError(response); +} + +JSONNode CCloudService::GetJsonResponse(NETLIBHTTPREQUEST *response) +{ + HandleHttpError(response); + + JSONNode root = JSONNode::parse(response->pData); + if (root.isnull()) + throw Exception(HttpStatusToError()); + + HandleJsonError(root); + + return root; +} + +UINT CCloudService::Upload(CCloudService *service, FileTransferParam *ftp) +{ + try { + if (!service->IsLoggedIn()) + service->Login(); + + if (!service->IsLoggedIn()) { + ftp->SetStatus(ACKRESULT_FAILED); + return ACKRESULT_FAILED; + } + + service->Upload(ftp); + } + catch (Exception &ex) { + service->debugLogA("%s: %s", service->GetModuleName(), ex.what()); + ftp->SetStatus(ACKRESULT_FAILED); + return ACKRESULT_FAILED; + } + + ftp->SetStatus(ACKRESULT_SUCCESS); + return ACKRESULT_SUCCESS; +}
\ No newline at end of file diff --git a/protocols/CloudFile/src/cloud_file.h b/protocols/CloudFile/src/cloud_file.h new file mode 100644 index 0000000000..1e70671054 --- /dev/null +++ b/protocols/CloudFile/src/cloud_file.h @@ -0,0 +1,61 @@ +#ifndef _CLOUD_SERVICE_H_ +#define _CLOUD_SERVICE_H_ + +enum OnConflict +{ + NONE, + RENAME, + REPLACE, +}; + +class CCloudService : public PROTO<CCloudService> +{ +protected: + HPLUGIN m_pPlugin; + HNETLIBUSER m_hConnection; + + INT_PTR __cdecl OnAccountManagerInit(WPARAM, LPARAM); + + // utils + std::string PreparePath(const std::string &path) const; + + virtual char* HttpStatusToError(int status = 0); + virtual void HttpResponseToError(NETLIBHTTPREQUEST *response); + virtual void HandleHttpError(NETLIBHTTPREQUEST *response); + virtual void HandleJsonError(JSONNode &node) = 0; + + void OnErase() override; + void OnModulesLoaded() override; + + JSONNode GetJsonResponse(NETLIBHTTPREQUEST *response); + + virtual void Upload(FileTransferParam *ftp) = 0; + +public: + std::map<MCONTACT, HWND> InterceptedContacts; + + CCloudService(const char *protoName, const wchar_t *userName, HPLUGIN); + virtual ~CCloudService(); + + INT_PTR GetCaps(int type, MCONTACT) override; + + int FileCancel(MCONTACT hContact, HANDLE hTransfer) override; + HANDLE SendFile(MCONTACT hContact, const wchar_t *msg, wchar_t **ppszFiles) override; + + HPLUGIN GetId() const; + virtual const char* GetModuleName() const = 0; + const char* GetAccountName() const; + const wchar_t* GetUserName() const; + + virtual int GetIconId() const = 0; + + virtual bool IsLoggedIn() = 0; + virtual void Login(HWND owner = nullptr) = 0; + virtual void Logout() = 0; + + void OpenUploadDialog(MCONTACT hContact); + + static UINT Upload(CCloudService *service, FileTransferParam *ftp); +}; + +#endif //_CLOUD_SERVICE_H_
\ No newline at end of file diff --git a/protocols/CloudFile/src/events.cpp b/protocols/CloudFile/src/events.cpp new file mode 100644 index 0000000000..010729dddb --- /dev/null +++ b/protocols/CloudFile/src/events.cpp @@ -0,0 +1,48 @@ +#include "stdafx.h" + +static int OnProtoAck(WPARAM, LPARAM lParam) +{ + ACKDATA *ack = (ACKDATA*)lParam; + + if (ack->type != ACKTYPE_STATUS) + return 0; + + for (auto &hContact : Contacts(ack->szModule)) { + MessageWindowData msgw; + if (Srmm_GetWindowData(hContact, msgw) || !(msgw.uState & MSG_WINDOW_STATE_EXISTS)) + continue; + + BBButton bbd = {}; + bbd.pszModuleName = MODULENAME; + bbd.dwButtonID = BBB_ID_FILE_SEND; + bbd.bbbFlags = CanSendToContact(hContact) + ? BBSF_RELEASED + : BBSF_DISABLED; + Srmm_SetButtonState(hContact, &bbd); + } + + return 0; +} + +static int OnFileDialogCanceled(WPARAM hContact, LPARAM) +{ + for (auto &service : Services) { + auto it = service->InterceptedContacts.find(hContact); + if (it != service->InterceptedContacts.end()) + service->InterceptedContacts.erase(it); + } + return 0; +} + +int OnModulesLoaded(WPARAM, LPARAM) +{ + InitializeMenus(); + + HookEvent(ME_PROTO_ACK, OnProtoAck); + + // srfile + HookEvent(ME_FILEDLG_CANCELED, OnFileDialogCanceled); + + HookTemporaryEvent(ME_MSG_TOOLBARLOADED, OnSrmmToolbarLoaded); + return 0; +}
\ No newline at end of file diff --git a/protocols/CloudFile/src/file_transfer.h b/protocols/CloudFile/src/file_transfer.h new file mode 100644 index 0000000000..692e717f2e --- /dev/null +++ b/protocols/CloudFile/src/file_transfer.h @@ -0,0 +1,255 @@ +#ifndef _FILE_TRANSFER_H_ +#define _FILE_TRANSFER_H_ + +class FileTransferParam +{ +private: + static ULONG hFileProcess; + + ULONG id; + FILE *hFile; + PROTOFILETRANSFERSTATUS pfts; + + bool isTerminated; + + CMStringW m_serverDirectory; + int m_relativePathStart; + + LIST<char> m_links; + CMStringW m_description; + +public: + FileTransferParam(MCONTACT hContact) + : m_links(1) + { + hFile = NULL; + id = InterlockedIncrement(&hFileProcess); + + isTerminated = false; + + m_relativePathStart = 0; + + pfts.flags = PFTS_UNICODE | PFTS_SENDING; + pfts.hContact = hContact; + pfts.currentFileNumber = -1; + pfts.currentFileProgress = 0; + pfts.currentFileSize = 0; + pfts.currentFileTime = 0; + pfts.totalBytes = 0; + pfts.totalFiles = 0; + pfts.totalProgress = 0; + pfts.pszFiles.w = (wchar_t**)mir_alloc(sizeof(wchar_t*) * (pfts.totalFiles + 1)); + pfts.pszFiles.w[pfts.totalFiles] = NULL; + pfts.szWorkingDir.w = NULL; + pfts.szCurrentFile.w = NULL; + + ProtoBroadcastAck(MODULENAME, pfts.hContact, ACKTYPE_FILE, ACKRESULT_INITIALISING, (HANDLE)id, 0); + } + + ~FileTransferParam() + { + CloseCurrentFile(); + + if (pfts.szWorkingDir.w) + mir_free(pfts.szWorkingDir.w); + + if (pfts.pszFiles.a) { + for (int i = 0; pfts.pszFiles.a[i]; i++) + mir_free(pfts.pszFiles.a[i]); + mir_free(pfts.pszFiles.a); + } + + for (auto &link : m_links) + mir_free(link); + m_links.destroy(); + } + + ULONG GetId() const + { + return id; + } + + MCONTACT GetContact() const + { + return pfts.hContact; + } + + const wchar_t* GetDescription() const + { + return m_description.GetString(); + } + + const char** GetSharedLinks(size_t &count) const + { + count = m_links.getCount(); + return (const char**)m_links.getArray(); + } + + void Terminate() + { + isTerminated = true; + } + + void SetDescription(const wchar_t *description) + { + m_description = description; + } + + void SetWorkingDirectory(const wchar_t *path) + { + m_relativePathStart = wcsrchr(path, '\\') - path + 1; + pfts.szWorkingDir.w = (wchar_t*)mir_calloc(sizeof(wchar_t) * m_relativePathStart); + mir_wstrncpy(pfts.szWorkingDir.w, path, m_relativePathStart); + if (PathIsDirectory(path)) + m_serverDirectory = wcsrchr(path, '\\') + 1; + } + + void SetServerDirectory(const wchar_t *name) + { + if (name) + m_serverDirectory = name; + } + + const wchar_t* GetServerDirectory() const + { + if (m_serverDirectory.IsEmpty()) + return nullptr; + return m_serverDirectory.GetString(); + } + + void AddFile(const wchar_t *path) + { + pfts.pszFiles.w = (wchar_t**)mir_realloc(pfts.pszFiles.w, sizeof(wchar_t*) * (pfts.totalFiles + 2)); + pfts.pszFiles.w[pfts.totalFiles++] = mir_wstrdup(path); + pfts.pszFiles.w[pfts.totalFiles] = NULL; + + FILE *file = _wfopen(path, L"rb"); + if (file != NULL) { + _fseeki64(file, 0, SEEK_END); + pfts.totalBytes += _ftelli64(file); + fclose(file); + } + } + + void AddSharedLink(const char *url) + { + m_links.insert(mir_strdup(url)); + } + + const bool IsCurrentFileInSubDirectory() const + { + const wchar_t *backslash = wcschr(GetCurrentRelativeFilePath(), L'\\'); + return backslash != nullptr; + } + + const wchar_t* GetCurrentFilePath() const + { + return pfts.pszFiles.w[pfts.currentFileNumber]; + } + + const wchar_t* GetCurrentRelativeFilePath() const + { + return &GetCurrentFilePath()[m_relativePathStart]; + } + + const wchar_t* GetCurrentFileName() const + { + return wcsrchr(GetCurrentFilePath(), '\\') + 1; + } + + void OpenCurrentFile() + { + hFile = _wfopen(GetCurrentFilePath(), L"rb"); + if (!hFile) + throw Exception("Unable to open file"); + _fseeki64(hFile, 0, SEEK_END); + pfts.currentFileSize = _ftelli64(hFile); + rewind(hFile); + } + + size_t ReadCurrentFile(void *buffer, size_t count) + { + return fread(buffer, sizeof(char), count, hFile); + } + + void CheckCurrentFile() + { + if (ferror(hFile)) + throw Exception("Error while file sending"); + + if (isTerminated) + throw Exception("Transfer was terminated"); + } + + void CloseCurrentFile() + { + if (hFile != NULL) + { + fclose(hFile); + hFile = NULL; + } + } + + const uint64_t GetCurrentFileSize() const + { + return pfts.currentFileSize; + } + + const size_t GetCurrentFileChunkSize() const + { + size_t chunkSize = 1024 * 1024; + if (pfts.currentFileSize < chunkSize) + chunkSize = min(pfts.currentFileSize, chunkSize / 4); + else if (pfts.currentFileSize > 20 * chunkSize) + chunkSize = chunkSize * 4; + return chunkSize; + } + + void Progress(size_t count) + { + pfts.currentFileProgress += count; + pfts.totalProgress += count; + if (pfts.hContact) + ProtoBroadcastAck(MODULENAME, pfts.hContact, ACKTYPE_FILE, ACKRESULT_DATA, (HANDLE)id, (LPARAM)&pfts); + } + + void FirstFile() + { + CloseCurrentFile(); + + pfts.currentFileNumber = 0; + pfts.currentFileProgress = 0; + pfts.szCurrentFile.w = wcsrchr(pfts.pszFiles.w[pfts.currentFileNumber], '\\') + 1; + if (pfts.hContact) + ProtoBroadcastAck(MODULENAME, pfts.hContact, ACKTYPE_FILE, ACKRESULT_DATA, (HANDLE)id, (LPARAM)&pfts); + + OpenCurrentFile(); + CheckCurrentFile(); + } + + bool NextFile() + { + CloseCurrentFile(); + + if (++pfts.currentFileNumber == pfts.totalFiles) + return false; + + pfts.currentFileProgress = 0; + pfts.szCurrentFile.w = wcsrchr(pfts.pszFiles.w[pfts.currentFileNumber], '\\') + 1; + if (pfts.hContact) + ProtoBroadcastAck(MODULENAME, pfts.hContact, ACKTYPE_FILE, ACKRESULT_NEXTFILE, (HANDLE)id, 0); + + OpenCurrentFile(); + CheckCurrentFile(); + + return true; + } + + void SetStatus(int status, LPARAM param = 0) + { + if (pfts.hContact) + ProtoBroadcastAck(MODULENAME, pfts.hContact, ACKTYPE_FILE, status, (HANDLE)id, param); + } +}; + +#endif //_FILE_TRANSFER_H_
\ No newline at end of file diff --git a/protocols/CloudFile/src/http_request.h b/protocols/CloudFile/src/http_request.h new file mode 100644 index 0000000000..c74b2ae40c --- /dev/null +++ b/protocols/CloudFile/src/http_request.h @@ -0,0 +1,180 @@ +#ifndef _HTTP_REQUEST_H_ +#define _HTTP_REQUEST_H_ + +class HttpRequestException +{ + CMStringA message; + +public: + HttpRequestException(const char *message) : + message(message) + { + } + + const char* what() const throw() + { + return message.c_str(); + } +}; + +class HttpRequest : protected NETLIBHTTPREQUEST +{ +private: + CMStringA m_szUrl; + + void Init(int type) + { + cbSize = sizeof(NETLIBHTTPREQUEST); + requestType = type; + flags = NLHRF_HTTP11 | NLHRF_SSL | NLHRF_NODUMP; + szUrl = NULL; + headers = NULL; + headersCount = 0; + pData = NULL; + dataLength = 0; + resultCode = 0; + szResultDescr = NULL; + nlc = NULL; + timeout = 0; + } + +protected: + enum HttpRequestUrlFormat { FORMAT }; + + void AddHeader(LPCSTR szName, LPCSTR szValue) + { + headers = (NETLIBHTTPHEADER*)mir_realloc(headers, sizeof(NETLIBHTTPHEADER) * (headersCount + 1)); + headers[headersCount].szName = mir_strdup(szName); + headers[headersCount].szValue = mir_strdup(szValue); + headersCount++; + } + + void AddBasicAuthHeader(LPCSTR szLogin, LPCSTR szPassword) + { + size_t length = mir_strlen(szLogin) + mir_strlen(szPassword) + 1; + ptrA cPair((char*)mir_calloc(length + 1)); + mir_snprintf( + cPair, + length, + "%s:%s", + szLogin, + szPassword); + + ptrA ePair(mir_base64_encode(cPair, length)); + + length = mir_strlen(ePair) + 7; + char *value = (char*)mir_calloc(length + 1); + mir_snprintf( + value, + length, + "Basic %s", + ePair); + + headers = (NETLIBHTTPHEADER*)mir_realloc(headers, sizeof(NETLIBHTTPHEADER)*(headersCount + 1)); + headers[headersCount].szName = mir_strdup("Authorization"); + headers[headersCount].szValue = value; + headersCount++; + } + + void AddBearerAuthHeader(LPCSTR szValue) + { + size_t length = mir_strlen(szValue) + 8; + char *value = (char*)mir_calloc(length + 1); + mir_snprintf( + value, + length, + "Bearer %s", + szValue); + + headers = (NETLIBHTTPHEADER*)mir_realloc(headers, sizeof(NETLIBHTTPHEADER)*(headersCount + 1)); + headers[headersCount].szName = mir_strdup("Authorization"); + headers[headersCount].szValue = value; + headersCount++; + } + + void AddOAuthHeader(LPCSTR szValue) + { + size_t length = mir_strlen(szValue) + 7; + char *value = (char*)mir_calloc(length + 1); + mir_snprintf( + value, + length, + "OAuth %s", + szValue); + + headers = (NETLIBHTTPHEADER*)mir_realloc(headers, sizeof(NETLIBHTTPHEADER)*(headersCount + 1)); + headers[headersCount].szName = mir_strdup("Authorization"); + headers[headersCount].szValue = value; + headersCount++; + } + + void AddUrlParameter(const char *urlFormat, ...) + { + va_list urlArgs; + va_start(urlArgs, urlFormat); + m_szUrl += m_szUrl.Find('?') == -1 ? '?' : '&'; + m_szUrl.AppendFormatV(urlFormat, urlArgs); + va_end(urlArgs); + } + + void AddUrlParameterWithEncode(const char *name, const char *valueFormat, ...) + { + va_list valueArgs; + va_start(valueArgs, valueFormat); + m_szUrl += m_szUrl.Find('?') == -1 ? '?' : '&'; + m_szUrl.AppendFormat("%s=", name); + CMStringA value; + value.AppendFormatV(valueFormat, valueArgs); + m_szUrl += mir_urlEncode(value); + va_end(valueArgs); + } + + void SetData(const char *data, size_t size) + { + if (pData != NULL) + mir_free(pData); + + dataLength = (int)size; + pData = (char*)mir_alloc(size); + memcpy(pData, data, size); + } + +public: + HttpRequest(int type, LPCSTR url) + { + Init(type); + + m_szUrl = url; + } + + HttpRequest(int type, HttpRequestUrlFormat, LPCSTR urlFormat, ...) + { + Init(type); + + va_list formatArgs; + va_start(formatArgs, urlFormat); + m_szUrl.AppendFormatV(urlFormat, formatArgs); + va_end(formatArgs); + } + + ~HttpRequest() + { + for (int i = 0; i < headersCount; i++) + { + mir_free(headers[i].szName); + mir_free(headers[i].szValue); + } + mir_free(headers); + if (pData) + mir_free(pData); + } + + NETLIBHTTPREQUEST* Send(HNETLIBUSER hConnection) + { + m_szUrl.Replace('\\', '/'); + szUrl = m_szUrl.GetBuffer(); + return Netlib_HttpTransaction(hConnection, this); + } +}; + +#endif //_HTTP_REQUEST_H_
\ No newline at end of file diff --git a/protocols/CloudFile/src/icons.cpp b/protocols/CloudFile/src/icons.cpp new file mode 100644 index 0000000000..001df50240 --- /dev/null +++ b/protocols/CloudFile/src/icons.cpp @@ -0,0 +1,39 @@ +#include "stdafx.h" + +static IconItem iconList[] = +{ + { LPGEN("Upload file(s)"), "upload", IDI_UPLOAD }, + { LPGEN("Dropbox"), "dropbox", IDI_DROPBOX }, + { LPGEN("Google Drive"), "gdrive", IDI_GDRIVE }, + { LPGEN("OneDrive"), "onedrive", IDI_ONEDRIVE }, + { LPGEN("Yandex.Disk"), "yadisk", IDI_YADISK } +}; + +void InitializeIcons() +{ + g_plugin.registerIcon("Protocols/" MODULENAME, iconList, MODULENAME); +} + +HANDLE GetIconHandle(int iconId) +{ + for (auto &it : iconList) + if (it.defIconID == iconId) + return it.hIcolib; + return nullptr; +} + +HANDLE GetIconHandle(const char *name) +{ + for (auto &it : iconList) + if (mir_strcmpi(it.szName, name) == 0) + return it.hIcolib; + return nullptr; +} + +HICON LoadIconEx(int iconId, bool big) +{ + for (auto &it : iconList) + if (it.defIconID == iconId) + return IcoLib_GetIconByHandle(it.hIcolib, big); + return nullptr; +}
\ No newline at end of file diff --git a/protocols/CloudFile/src/main.cpp b/protocols/CloudFile/src/main.cpp new file mode 100644 index 0000000000..2b9ffd6c2d --- /dev/null +++ b/protocols/CloudFile/src/main.cpp @@ -0,0 +1,42 @@ +#include "stdafx.h" + +CMPlugin g_plugin; + +PLUGININFOEX pluginInfoEx = +{ + sizeof(PLUGININFOEX), + __PLUGIN_NAME, + PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), + __DESCRIPTION, + __AUTHOR, + __COPYRIGHT, + __AUTHORWEB, + UNICODE_AWARE, + // {E876FE63-0701-4CDA-BED5-7C73A379C1D1} + { 0xe876fe63, 0x701, 0x4cda, { 0xbe, 0xd5, 0x7c, 0x73, 0xa3, 0x79, 0xc1, 0xd1 }} +}; + +CMPlugin::CMPlugin() : + PLUGIN<CMPlugin>(MODULENAME, pluginInfoEx) +{} + +///////////////////////////////////////////////////////////////////////////////////////// +// Interface information + +extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST }; + +///////////////////////////////////////////////////////////////////////////////////////// + +int CMPlugin::Load() +{ + HookEvent(ME_CLIST_PREBUILDCONTACTMENU, OnPrebuildContactMenu); + HookEvent(ME_MSG_WINDOWEVENT, OnSrmmWindowOpened); + HookEvent(ME_MSG_BUTTONPRESSED, OnSrmmButtonPressed); + HookEvent(ME_OPT_INITIALISE, OnOptionsInitialized); + HookEvent(ME_SYSTEM_MODULESLOADED, OnModulesLoaded); + + InitializeIcons(); + InitializeServices(); + + return 0; +} diff --git a/protocols/CloudFile/src/menus.cpp b/protocols/CloudFile/src/menus.cpp new file mode 100644 index 0000000000..3ca1b085b9 --- /dev/null +++ b/protocols/CloudFile/src/menus.cpp @@ -0,0 +1,52 @@ +#include "stdafx.h" + +HGENMENU hContactMenu; + +static INT_PTR UploadMenuCommand(void *obj, WPARAM hContact, LPARAM) +{ + CCloudService *service = (CCloudService*)obj; + service->OpenUploadDialog(hContact); + return 0; +} + +void InitializeMenus() +{ + CMenuItem mi(&g_plugin); + SET_UID(mi, 0x93d4495b, 0x259b, 0x4fba, 0xbc, 0x14, 0xf9, 0x46, 0x2c, 0xda, 0xfc, 0x6d); + mi.name.a = LPGEN("Upload to..."); + + ptrA defaultService(g_plugin.getStringA("DefaultService")); + if (defaultService) { + CCloudService *service = FindService(defaultService); + if (service) { + mi.name.a = LPGEN("Upload"); + mi.pszService = MODULENAME "/Default/Upload"; + CreateServiceFunctionObj(mi.pszService, UploadMenuCommand, service); + } + } + + mi.position = -2000019999; + mi.hIcon = LoadIconEx(IDI_UPLOAD); + hContactMenu = Menu_AddContactMenuItem(&mi); +} + +void CCloudService::OnModulesLoaded() +{ + CMenuItem mi(GetId()); + mi.root = hContactMenu; + CMStringA serviceName(FORMAT, "/%s/Upload", GetAccountName()); + mi.pszService = serviceName.GetBuffer(); + mi.flags = CMIF_SYSTEM | CMIF_UNICODE; + mi.name.w = (wchar_t*)GetUserName(); + mi.position = Services.getCount(); + mi.hIcolibItem = GetIconHandle(GetIconId()); + Menu_AddContactMenuItem(&mi); + + CreateServiceFunctionObj(mi.pszService, UploadMenuCommand, this); +} + +int OnPrebuildContactMenu(WPARAM hContact, LPARAM) +{ + Menu_ShowItem(hContactMenu, CanSendToContact(hContact)); + return 0; +} diff --git a/protocols/CloudFile/src/oauth.cpp b/protocols/CloudFile/src/oauth.cpp new file mode 100644 index 0000000000..e62629fcd8 --- /dev/null +++ b/protocols/CloudFile/src/oauth.cpp @@ -0,0 +1,33 @@ +#include "stdafx.h" + +COAuthDlg::COAuthDlg(CCloudService *service, const char *authUrl, CCloudService::MyThreadFunc requestAccessTokenThread) + : CDlgBase(g_plugin, IDD_OAUTH), m_service(service), + m_requestAccessTokenThread(requestAccessTokenThread), + m_authorize(this, IDC_OAUTH_AUTHORIZE, authUrl), + m_code(this, IDC_OAUTH_CODE), m_ok(this, IDOK) +{ + m_autoClose = CLOSE_ON_CANCEL; + m_code.OnChange = Callback(this, &COAuthDlg::Code_OnChange); + m_ok.OnClick = Callback(this, &COAuthDlg::Ok_OnClick); +} + +bool COAuthDlg::OnInitDialog() +{ + CCtrlLabel &ctrl = *(CCtrlLabel*)FindControl(IDC_AUTH_TEXT); + ptrW format(ctrl.GetText()); + wchar_t text[MAX_PATH]; + mir_snwprintf(text, (const wchar_t*)format, m_service->GetUserName()); + ctrl.SetText(text); + return true; +} + +void COAuthDlg::Code_OnChange(CCtrlBase*) +{ + ptrA requestToken(m_code.GetTextA()); + m_ok.Enable(mir_strlen(requestToken) != 0); +} + +void COAuthDlg::Ok_OnClick(CCtrlButton*) +{ + m_service->ForkThread(m_requestAccessTokenThread, m_hwnd); +}
\ No newline at end of file diff --git a/protocols/CloudFile/src/oauth.h b/protocols/CloudFile/src/oauth.h new file mode 100644 index 0000000000..2b32ecbd9b --- /dev/null +++ b/protocols/CloudFile/src/oauth.h @@ -0,0 +1,23 @@ +#ifndef _OAUTH_H_ +#define _OAUTH_H_ + +class COAuthDlg : public CDlgBase +{ + CCloudService *m_service; + CCloudService::MyThreadFunc m_requestAccessTokenThread; + + CCtrlHyperlink m_authorize; + CCtrlEdit m_code; + CCtrlButton m_ok; + +protected: + bool OnInitDialog() override; + + void Code_OnChange(CCtrlBase*); + void Ok_OnClick(CCtrlButton*); + +public: + COAuthDlg(CCloudService *service, const char *authUrl, CCloudService::MyThreadFunc requestAccessTokenThread); +}; + +#endif //_OAUTH_H_
\ No newline at end of file diff --git a/protocols/CloudFile/src/options.cpp b/protocols/CloudFile/src/options.cpp new file mode 100644 index 0000000000..0ac7c120a0 --- /dev/null +++ b/protocols/CloudFile/src/options.cpp @@ -0,0 +1,124 @@ +#include "stdafx.h" + +COptionsMainDlg::COptionsMainDlg() + : CDlgBase(g_plugin, IDD_OPTIONS_MAIN), + m_defaultService(this, IDC_DEFAULTSERVICE), + m_doNothingOnConflict(this, IDC_DONOTHINGONCONFLICT), + m_renameOnConflict(this, IDC_RENAMEONCONFLICT), + m_repalceOnConflict(this, IDC_REPLACEONCONFLICT), + m_urlAutoSend(this, IDC_URL_AUTOSEND), + m_urlPasteToMessageInputArea(this, IDC_URL_COPYTOMIA), + m_urlCopyToClipboard(this, IDC_URL_COPYTOCB) +{ + CreateLink(m_defaultService, "DefaultService", L""); + + CreateLink(m_urlAutoSend, "UrlAutoSend", DBVT_BYTE, 1); + CreateLink(m_urlPasteToMessageInputArea, "UrlPasteToMessageInputArea", DBVT_BYTE, 0); + CreateLink(m_urlCopyToClipboard, "UrlCopyToClipboard", DBVT_BYTE, 0); +} + +bool COptionsMainDlg::OnInitDialog() +{ + CDlgBase::OnInitDialog(); + + ptrA defaultService(g_plugin.getStringA("DefaultService")); + int iItem = m_defaultService.AddString(TranslateT("None")); + m_defaultService.SetCurSel(iItem); + + for (auto &service : Services) { + iItem = m_defaultService.AddString(mir_wstrdup(service->GetUserName()), (LPARAM)service); + if (!mir_strcmpi(service->GetAccountName(), defaultService)) + m_defaultService.SetCurSel(iItem); + } + + BYTE strategy = g_plugin.getByte("ConflictStrategy", OnConflict::REPLACE); + switch (strategy) + { + case OnConflict::RENAME: + m_renameOnConflict.SetState(TRUE); + m_repalceOnConflict.SetState(FALSE); + m_doNothingOnConflict.SetState(FALSE); + break; + case OnConflict::REPLACE: + m_renameOnConflict.SetState(FALSE); + m_repalceOnConflict.SetState(TRUE); + m_doNothingOnConflict.SetState(FALSE); + break; + default: + m_renameOnConflict.SetState(FALSE); + m_repalceOnConflict.SetState(FALSE); + m_doNothingOnConflict.SetState(TRUE); + break; + } + return true; +} + +bool COptionsMainDlg::OnApply() +{ + int iItem = m_defaultService.GetCurSel(); + CCloudService *service = (CCloudService*)m_defaultService.GetItemData(iItem); + if (service) + g_plugin.setString("DefaultService", service->GetAccountName()); + else + g_plugin.delSetting("DefaultService"); + + if (m_renameOnConflict.GetState()) + g_plugin.setByte("ConflictStrategy", OnConflict::RENAME); + else if (m_repalceOnConflict.GetState()) + g_plugin.setByte("ConflictStrategy", OnConflict::REPLACE); + else + g_plugin.delSetting("ConflictStrategy"); + return true; +} + +///////////////////////////////////////////////////////////////////////////////// + +int OnOptionsInitialized(WPARAM wParam, LPARAM) +{ + OPTIONSDIALOGPAGE odp = {}; + odp.szTitle.w = _A2W(MODULENAME); + odp.flags = ODPF_BOLDGROUPS | ODPF_UNICODE | ODPF_DONTTRANSLATE; + odp.szGroup.w = LPGENW("Services"); + + //odp.szTab.w = LPGENW("General"); + odp.pDialog = new COptionsMainDlg(); + g_plugin.addOptions(wParam, &odp); + + return 0; +} + +///////////////////////////////////////////////////////////////////////////////// + +CAccountManagerDlg::CAccountManagerDlg(CCloudService *service) + : CProtoDlgBase(service, IDD_ACCMGR), + m_requestAccess(this, IDC_REQUESTACCESS), + m_revokeAccess(this, IDC_REVOKEACCESS) +{ + m_requestAccess.OnClick = Callback(this, &CAccountManagerDlg::RequestAccess_OnClick); + m_revokeAccess.OnClick = Callback(this, &CAccountManagerDlg::RevokeAccess_OnClick); +} + +bool CAccountManagerDlg::OnInitDialog() +{ + ptrA token(m_proto->getStringA("TokenSecret")); + m_requestAccess.Enable(!token); + m_revokeAccess.Enable(token); + return true; +} + +void CAccountManagerDlg::RequestAccess_OnClick(CCtrlButton*) +{ + m_proto->Login(m_hwnd); + ptrA token(m_proto->getStringA("TokenSecret")); + m_requestAccess.Enable(!token); + m_revokeAccess.Enable(token); +} + +void CAccountManagerDlg::RevokeAccess_OnClick(CCtrlButton*) +{ + m_proto->Logout(); + m_requestAccess.Enable(); + m_revokeAccess.Disable(); +} + +///////////////////////////////////////////////////////////////////////////////// diff --git a/protocols/CloudFile/src/options.h b/protocols/CloudFile/src/options.h new file mode 100644 index 0000000000..aaf1440def --- /dev/null +++ b/protocols/CloudFile/src/options.h @@ -0,0 +1,43 @@ +#ifndef _OPTIONS_H_ +#define _OPTIONS_H_ + +class COptionsMainDlg : public CDlgBase +{ +private: + CCtrlCombo m_defaultService; + + CCtrlCheck m_doNothingOnConflict; + CCtrlCheck m_renameOnConflict; + CCtrlCheck m_repalceOnConflict; + + CCtrlCheck m_urlAutoSend; + CCtrlCheck m_urlPasteToMessageInputArea; + CCtrlCheck m_urlCopyToClipboard; + +protected: + bool OnInitDialog() override; + bool OnApply() override; + +public: + COptionsMainDlg(); +}; + +///////////////////////////////////////////////////////////////////////////////// + +class CAccountManagerDlg : public CProtoDlgBase<CCloudService> +{ +private: + CCtrlButton m_requestAccess; + CCtrlButton m_revokeAccess; + +protected: + bool OnInitDialog() override; + + void RequestAccess_OnClick(CCtrlButton*); + void RevokeAccess_OnClick(CCtrlButton*); + +public: + CAccountManagerDlg(CCloudService *service); +}; + +#endif //_OPTIONS_H_
\ No newline at end of file diff --git a/protocols/CloudFile/src/resource.h b/protocols/CloudFile/src/resource.h new file mode 100644 index 0000000000..d839f15da4 --- /dev/null +++ b/protocols/CloudFile/src/resource.h @@ -0,0 +1,38 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by D:\Projects\miranda-ng\miranda-ng\plugins\CloudFile\res\resource.rc +// +#define IDOK 1 +#define IDCANCEL 2 +#define IDD_ACCMGR 9 +#define IDI_UPLOAD 101 +#define IDI_DROPBOX 102 +#define IDI_GDRIVE 103 +#define IDI_ONEDRIVE 104 +#define IDI_YADISK 105 +#define IDD_OAUTH 120 +#define IDC_OAUTH_CODE 121 +#define IDC_OAUTH_AUTHORIZE 122 +#define IDD_OPTIONS_MAIN 1000 +#define IDC_DEFAULTSERVICE 1001 +#define IDC_DONOTHINGONCONFLICT 1010 +#define IDC_RENAMEONCONFLICT 1011 +#define IDC_REPLACEONCONFLICT 1012 +#define IDC_URL_ISTEMPORARY 1021 +#define IDC_URL_COPYTOCB 1022 +#define IDC_URL_COPYTOMIA 1023 +#define IDC_URL_AUTOSEND 1024 +#define IDC_AUTH_TEXT 1031 +#define IDC_REQUESTACCESS 1033 +#define IDC_REVOKEACCESS 1034 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 133 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1034 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/protocols/CloudFile/src/services.cpp b/protocols/CloudFile/src/services.cpp new file mode 100644 index 0000000000..e0a01fbfba --- /dev/null +++ b/protocols/CloudFile/src/services.cpp @@ -0,0 +1,106 @@ +#include "stdafx.h" + +static int CompareServices(const CCloudService *p1, const CCloudService *p2) +{ + return mir_strcmp(p1->GetAccountName(), p2->GetAccountName()); +} + +LIST<CCloudService> Services(10, CompareServices); + +CCloudService* FindService(const char *szProto) +{ + for (auto &it : Services) + if (!mir_strcmp(it->GetAccountName(), szProto)) + return it; + + return nullptr; +} + +static INT_PTR GetService(WPARAM wParam, LPARAM lParam) +{ + CFSERVICEINFO *info = (CFSERVICEINFO*)lParam; + if (info == nullptr) + return 1; + + ptrA accountName(mir_strdup((char*)wParam)); + if (!accountName || !mir_strlen(accountName)) + accountName = g_plugin.getStringA("DefaultService"); + if (accountName == nullptr) + return 2; + + CCloudService *service = FindService(accountName); + if (service == nullptr) + return 3; + + info->accountName = service->GetAccountName(); + info->userName = service->GetUserName(); + + return 0; +} + +static INT_PTR EnumServices(WPARAM wParam, LPARAM lParam) +{ + CFSERVICEINFO info = {}; + enumCFServiceFunc enumFunc = (enumCFServiceFunc)wParam; + void *param = (void*)lParam; + + for (auto &service : Services) { + info.accountName = service->GetAccountName(); + info.userName = service->GetUserName(); + int res = enumFunc(&info, param); + if (res != 0) + return res; + } + + return 0; +} + +INT_PTR Upload(WPARAM wParam, LPARAM lParam) +{ + CFUPLOADDATA *uploadData = (CFUPLOADDATA*)wParam; + if (uploadData == nullptr) + return 1; + + ptrA accountName(mir_strdup(uploadData->accountName)); + if (!mir_strlen(accountName)) + accountName = g_plugin.getStringA("DefaultService"); + if (accountName == nullptr) + return 2; + + CCloudService *service = FindService(uploadData->accountName); + if (service == nullptr) + return 3; + + if (PathIsDirectory(uploadData->localPath)) { + // temporary unsupported + return 4; + } + + FileTransferParam ftp(0); + ftp.SetWorkingDirectory(uploadData->localPath); + ftp.SetServerDirectory(uploadData->serverFolder); + ftp.AddFile(uploadData->localPath); + + int res = CCloudService::Upload(service, &ftp); + if (res == ACKRESULT_SUCCESS && lParam) { + size_t linkCount = 0; + const char **links = ftp.GetSharedLinks(linkCount); + if (linkCount > 0) { + CFUPLOADRESULT *result = (CFUPLOADRESULT*)lParam; + result->link = mir_strdup(links[linkCount - 1]); + } + } + + return res; +} + +void InitializeServices() +{ + Proto_RegisterModule(PROTOTYPE_FILTER, MODULENAME); + + CreateServiceFunction(MODULENAME PSS_FILE, SendFileInterceptor); + + CreateServiceFunction(MS_CLOUDFILE_GETSERVICE, GetService); + CreateServiceFunction(MS_CLOUDFILE_ENUMSERVICES, EnumServices); + CreateServiceFunction(MS_CLOUDFILE_UPLOAD, Upload); +} diff --git a/protocols/CloudFile/src/srmm.cpp b/protocols/CloudFile/src/srmm.cpp new file mode 100644 index 0000000000..b4d44c9fdd --- /dev/null +++ b/protocols/CloudFile/src/srmm.cpp @@ -0,0 +1,67 @@ +#include "stdafx.h" + +int OnSrmmToolbarLoaded(WPARAM, LPARAM) +{ + BBButton bbd = {}; + bbd.pszModuleName = MODULENAME; + bbd.bbbFlags = BBBF_ISIMBUTTON | BBBF_ISCHATBUTTON | BBBF_ISRSIDEBUTTON | BBBF_ISARROWBUTTON; + + CMStringW tooltip(FORMAT, TranslateT("Upload files to...")); + bbd.pwszTooltip = tooltip; + bbd.hIcon = GetIconHandle(IDI_UPLOAD); + bbd.dwButtonID = BBB_ID_FILE_SEND; + bbd.dwDefPos = 100 + bbd.dwButtonID; + Srmm_AddButton(&bbd, &g_plugin); + return 0; +} + +int OnSrmmWindowOpened(WPARAM, LPARAM lParam) +{ + MessageWindowEventData *ev = (MessageWindowEventData*)lParam; + if (ev->uType == MSG_WINDOW_EVT_OPENING && ev->hContact) { + BBButton bbd = {}; + bbd.pszModuleName = MODULENAME; + bbd.dwButtonID = BBB_ID_FILE_SEND; + bbd.bbbFlags = CanSendToContact(ev->hContact) + ? BBSF_RELEASED + : BBSF_DISABLED; + Srmm_SetButtonState(ev->hContact, &bbd); + } + + return 0; +} + +int OnSrmmButtonPressed(WPARAM, LPARAM lParam) +{ + CustomButtonClickData *cbc = (CustomButtonClickData*)lParam; + + if (mir_strcmp(cbc->pszModule, MODULENAME)) + return 0; + + if (cbc->dwButtonId != BBB_ID_FILE_SEND) + return 0; + + if (cbc->flags != BBCF_ARROWCLICKED) { + ptrA defaultService(g_plugin.getStringA("DefaultService")); + if (defaultService) { + CCloudService *service = FindService(defaultService); + if (service) + service->OpenUploadDialog(cbc->hContact); + return 0; + } + } + + HMENU hMenu = CreatePopupMenu(); + for (auto &it : Services) + AppendMenu(hMenu, MF_STRING, Services.indexOf(&it) + 1, TranslateW(it->GetUserName())); + + int pos = TrackPopupMenu(hMenu, TPM_RETURNCMD, cbc->pt.x, cbc->pt.y, 0, cbc->hwndFrom, nullptr); + DestroyMenu(hMenu); + + if (pos > 0) { + CCloudService *service = Services[pos - 1]; + service->OpenUploadDialog(cbc->hContact); + } + + return 0; +}
\ No newline at end of file diff --git a/protocols/CloudFile/src/stdafx.cxx b/protocols/CloudFile/src/stdafx.cxx new file mode 100644 index 0000000000..708e6f1c91 --- /dev/null +++ b/protocols/CloudFile/src/stdafx.cxx @@ -0,0 +1,20 @@ +/* +Copyright (C) 2012-19 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" + +ULONG FileTransferParam::hFileProcess = 1;
\ No newline at end of file diff --git a/protocols/CloudFile/src/stdafx.h b/protocols/CloudFile/src/stdafx.h new file mode 100644 index 0000000000..58801c4a34 --- /dev/null +++ b/protocols/CloudFile/src/stdafx.h @@ -0,0 +1,126 @@ +#ifndef _COMMON_H_ +#define _COMMON_H_ + +#include <windows.h> +#include <shlwapi.h> +#include <commctrl.h> + +#include <malloc.h> +#include <time.h> + +#include <map> +#include <algorithm> + +#include <newpluginapi.h> + +#include <m_options.h> +#include <m_database.h> +#include <m_netlib.h> +#include <m_clist.h> +#include <m_icolib.h> +#include <m_popup.h> +#include <m_file.h> +#include <m_langpack.h> +#include <m_message.h> +#include <m_gui.h> +#include <m_chat.h> +#include <m_http.h> +#include <m_json.h> +#include <m_metacontacts.h> +#include <m_protoint.h> +#include <m_protosvc.h> +#include <m_contacts.h> + +#include <m_cloudfile.h> + +#include "version.h" +#include "resource.h" + +class CCloudService; + +#include "options.h" + +extern HNETLIBUSER hNetlibConnection; +extern PLUGININFOEX pluginInfoEx; + +class Exception +{ + CMStringA message; + +public: + Exception(const char *message) : + message(message) + { + } + + const char* what() const throw() + { + return message.c_str(); + } +}; + +#define MODULENAME "CloudFile" + +#define FILE_CHUNK_SIZE 1024 * 1024 //1 MB + +#include "http_request.h" +#include "file_transfer.h" + +// services +#include "cloud_file.h" +#include "oauth.h" +#include "Services\dropbox_service.h" +#include "Services\google_service.h" +#include "Services\microsoft_service.h" +#include "Services\yandex_service.h" +extern LIST<CCloudService> Services; +void InitializeServices(); + +// events +int OnModulesLoaded(WPARAM, LPARAM); + +// icons +void InitializeIcons(); +HANDLE GetIconHandle(int iconId); +HANDLE GetIconHandle(const char *name); +HICON LoadIconEx(int iconId, bool big = false); + +// menus +extern HGENMENU hContactMenu; +void InitializeMenus(); +int OnPrebuildContactMenu(WPARAM, LPARAM); + +// srmm +#define BBB_ID_FILE_SEND 10001 +int OnSrmmToolbarLoaded(WPARAM, LPARAM); +int OnSrmmWindowOpened(WPARAM, LPARAM); +int OnSrmmButtonPressed(WPARAM, LPARAM); + +// options +int OnOptionsInitialized(WPARAM wParam, LPARAM); + +// transfers +extern LIST<FileTransferParam> Transfers; + +INT_PTR SendFileInterceptor(WPARAM wParam, LPARAM lParam); +UINT UploadAndReportProgressThread(void *owner, void *arg); + +// utils +void ShowNotification(const wchar_t *caption, const wchar_t *message, int flags, MCONTACT hContact = NULL); +void ShowNotification(const wchar_t *message, int flags, MCONTACT hContact = NULL); +bool CanSendToContact(MCONTACT hContact); +void SendToContact(MCONTACT hContact, const wchar_t *data); +void PasteToInputArea(MCONTACT hContact, const wchar_t *data); +void PasteToClipboard(const wchar_t *data); +void Report(MCONTACT hContact, const wchar_t *data); + +CCloudService* FindService(const char *szProto); + +struct CMPlugin : public PLUGIN<CMPlugin> +{ + CMPlugin(); + + int Load() override; +}; + +#endif //_COMMON_H_
\ No newline at end of file diff --git a/protocols/CloudFile/src/transfers.cpp b/protocols/CloudFile/src/transfers.cpp new file mode 100644 index 0000000000..5236e0c9f0 --- /dev/null +++ b/protocols/CloudFile/src/transfers.cpp @@ -0,0 +1,39 @@ +#include "stdafx.h" + +LIST<FileTransferParam> Transfers(1, HandleKeySortT); + +INT_PTR SendFileInterceptor(WPARAM, LPARAM lParam) +{ + CCSDATA *pccsd = (CCSDATA*)lParam; + for (auto &service : Services) { + auto it = service->InterceptedContacts.find(pccsd->hContact); + if (it == service->InterceptedContacts.end()) + continue; + service->InterceptedContacts.erase(it); + return (INT_PTR)service->SendFile(pccsd->hContact, (wchar_t*)pccsd->wParam, (wchar_t**)pccsd->lParam); + } + return CALLSERVICE_NOTFOUND; +} + +UINT UploadAndReportProgressThread(void *owner, void *arg) +{ + CCloudService *service = (CCloudService*)owner; + FileTransferParam *ftp = (FileTransferParam*)arg; + + int res = CCloudService::Upload(service, ftp); + if (res == ACKRESULT_SUCCESS) { + CMStringW data = ftp->GetDescription(); + size_t linkCount; + auto links = ftp->GetSharedLinks(linkCount); + for (size_t i = 0; i < linkCount; i++) { + data.Append(ptrW(mir_utf8decodeW(links[i]))); + data.AppendChar(0x0A); + } + Report(ftp->GetContact(), data); + } + + Transfers.remove(ftp); + delete ftp; + + return res; +} diff --git a/protocols/CloudFile/src/utils.cpp b/protocols/CloudFile/src/utils.cpp new file mode 100644 index 0000000000..79b743f5c2 --- /dev/null +++ b/protocols/CloudFile/src/utils.cpp @@ -0,0 +1,113 @@ +#include "stdafx.h" + +void ShowNotification(const wchar_t *caption, const wchar_t *message, int flags, MCONTACT hContact) +{ + if (Miranda_IsTerminated()) + return; + + if (ServiceExists(MS_POPUP_ADDPOPUPW) && db_get_b(0, "Popup", "ModuleIsEnabled", 1)) { + POPUPDATAW ppd = { 0 }; + ppd.lchContact = hContact; + wcsncpy(ppd.lpwzContactName, caption, MAX_CONTACTNAME); + wcsncpy(ppd.lpwzText, message, MAX_SECONDLINE); + ppd.lchIcon = IcoLib_GetIcon("Slack_main"); + + if (!PUAddPopupW(&ppd)) + return; + } + + MessageBox(nullptr, message, caption, MB_OK | flags); +} + +void ShowNotification(const wchar_t *message, int flags, MCONTACT hContact) +{ + ShowNotification(_A2W(MODULENAME), message, flags, hContact); +} + +MEVENT AddEventToDb(MCONTACT hContact, WORD type, DWORD flags, DWORD cbBlob, PBYTE pBlob) +{ + DBEVENTINFO dbei = {}; + dbei.szModule = MODULENAME; + dbei.timestamp = time(0); + dbei.eventType = type; + dbei.cbBlob = cbBlob; + dbei.pBlob = pBlob; + dbei.flags = flags; + return db_event_add(hContact, &dbei); +} + +bool CanSendToContact(MCONTACT hContact) +{ + if (!hContact) + return false; + + const char *proto = GetContactProto(hContact); + if (!proto) + return false; + + bool isCtrlPressed = (GetKeyState(VK_CONTROL) & 0x8000) != 0; + if (isCtrlPressed) + return true; + + bool canSend = (CallProtoService(proto, PS_GETCAPS, PFLAGNUM_1, 0) & PF1_IMSEND) != 0; + if (!canSend) + return false; + + bool isProtoOnline = Proto_GetStatus(proto) > ID_STATUS_OFFLINE; + if (!isProtoOnline) + return false; + + bool isContactOnline = Contact_GetStatus(hContact) > ID_STATUS_OFFLINE; + if (isContactOnline) + return true; + + return CallProtoService(proto, PS_GETCAPS, PFLAGNUM_4, 0) & PF4_IMSENDOFFLINE; +} + +void SendToContact(MCONTACT hContact, const wchar_t *data) +{ + const char *szProto = GetContactProto(hContact); + if (db_get_b(hContact, szProto, "ChatRoom", 0) == TRUE) { + ptrW tszChatRoom(db_get_wsa(hContact, szProto, "ChatRoomID")); + Chat_SendUserMessage(szProto, tszChatRoom, data); + return; + } + + char *message = mir_utf8encodeW(data); + if (ProtoChainSend(hContact, PSS_MESSAGE, 0, (LPARAM)message) != ACKRESULT_FAILED) + AddEventToDb(hContact, EVENTTYPE_MESSAGE, DBEF_UTF | DBEF_SENT, (DWORD)mir_strlen(message), (PBYTE)message); +} + +void PasteToInputArea(MCONTACT hContact, const wchar_t *data) +{ + CallService(MS_MSG_SENDMESSAGEW, hContact, (LPARAM)data); +} + +void PasteToClipboard(const wchar_t *data) +{ + if (OpenClipboard(nullptr)) { + EmptyClipboard(); + + size_t size = sizeof(wchar_t) * (mir_wstrlen(data) + 1); + HGLOBAL hClipboardData = GlobalAlloc(NULL, size); + if (hClipboardData) { + wchar_t *pchData = (wchar_t*)GlobalLock(hClipboardData); + mir_wstrcpy(pchData, data); + GlobalUnlock(hClipboardData); + SetClipboardData(CF_UNICODETEXT, hClipboardData); + } + CloseClipboard(); + } +} + +void Report(MCONTACT hContact, const wchar_t *data) +{ + if (g_plugin.getByte("UrlAutoSend", 1)) + SendToContact(hContact, data); + + if (g_plugin.getByte("UrlPasteToMessageInputArea", 0)) + PasteToInputArea(hContact, data); + + if (g_plugin.getByte("UrlCopyToClipboard", 0)) + PasteToClipboard(data); +} diff --git a/protocols/CloudFile/src/version.h b/protocols/CloudFile/src/version.h new file mode 100644 index 0000000000..fbc4ce1219 --- /dev/null +++ b/protocols/CloudFile/src/version.h @@ -0,0 +1,13 @@ +#define __MAJOR_VERSION 0 +#define __MINOR_VERSION 11 +#define __RELEASE_NUM 0 +#define __BUILD_NUM 6 + +#include <stdver.h> + +#define __PLUGIN_NAME "CloudFile" +#define __FILENAME "CloudFile.dll" +#define __DESCRIPTION "Allows you to transfer files via cloud services." +#define __AUTHOR "Miranda NG team" +#define __AUTHORWEB "https://miranda-ng.org/p/CloudFile/" +#define __COPYRIGHT "© 2017-19 Miranda NG team" |