diff --git a/clientapp/windows/TgBotClient/.gitignore b/clientapp/windows/TgBotClient/.gitignore new file mode 100644 index 00000000..adfd4e87 --- /dev/null +++ b/clientapp/windows/TgBotClient/.gitignore @@ -0,0 +1,4 @@ +TgBotClient/ +x64/ +x86/ +*.vcxproj.user \ No newline at end of file diff --git a/clientapp/windows/TgBotClient/About.cpp b/clientapp/windows/TgBotClient/About.cpp new file mode 100644 index 00000000..3411fcd9 --- /dev/null +++ b/clientapp/windows/TgBotClient/About.cpp @@ -0,0 +1,17 @@ +#include "UIComponents.h" + +INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { + UNREFERENCED_PARAMETER(lParam); + switch (message) { + case WM_INITDIALOG: + return (INT_PTR)TRUE; + + case WM_COMMAND: + if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) { + EndDialog(hDlg, LOWORD(wParam)); + return (INT_PTR)TRUE; + } + break; + } + return (INT_PTR)FALSE; +} diff --git a/clientapp/windows/TgBotClient/AbseilLogging.h b/clientapp/windows/TgBotClient/AbseilLogging.h new file mode 100644 index 00000000..5855100c --- /dev/null +++ b/clientapp/windows/TgBotClient/AbseilLogging.h @@ -0,0 +1,30 @@ +#pragma once + +#define _CRT_SECURE_NO_WARNINGS +#include +#include +#include +#include +#include +#include + +struct WindowSinkBase : absl::LogSink { + void Send(const absl::LogEntry& entry) override { + for (absl::string_view line : absl::StrSplit( + entry.text_message_with_prefix(), absl::ByChar('\n'))) { + const std::lock_guard lock(m); + OutputDebugStringA(line.data()); + } + } + WindowSinkBase() = default; + ~WindowSinkBase() override = default; + + protected: + std::mutex m; +}; + +inline void initLogging() { + static WindowSinkBase sink; + absl::InitializeLog(); + absl::AddLogSink(&sink); +} \ No newline at end of file diff --git a/clientapp/windows/TgBotClient/DestinationIP.cpp b/clientapp/windows/TgBotClient/DestinationIP.cpp new file mode 100644 index 00000000..672a6d67 --- /dev/null +++ b/clientapp/windows/TgBotClient/DestinationIP.cpp @@ -0,0 +1,72 @@ + +#define _CRT_SECURE_NO_WARNINGS +#include "TgBotSocketIntf.h" +#include "UIComponents.h" +#include + +INT_PTR CALLBACK DestinationIP(HWND hDlg, UINT message, WPARAM wParam, + LPARAM lParam) { + static HWND hIPAddr; + static HWND hUseINet4; + static HWND hUseINet6; + static std::optional config; + + UNREFERENCED_PARAMETER(lParam); + switch (message) { + case WM_INITDIALOG: + hIPAddr = GetDlgItem(hDlg, IDC_IPADDR); + hUseINet4 = GetDlgItem(hDlg, IDC_INET_4); + hUseINet6 = GetDlgItem(hDlg, IDC_INET_6); + SendMessage(hUseINet4, BM_SETCHECK, BST_CHECKED, 0); + config.emplace(); + config->mode = SocketConfig::Mode::USE_IPV4; + return DIALOG_OK; + + case WM_COMMAND: + switch (LOWORD(wParam)) { + case IDC_INET_4: + config->mode = SocketConfig::Mode::USE_IPV4; + break; + case IDC_INET_6: + config->mode = SocketConfig::Mode::USE_IPV6; + break; + case IDC_IPADDR: { + DWORD dwAddr; + SendMessage(hIPAddr, IPM_GETADDRESS, 0, (LPARAM)&dwAddr); + + // Extract the individual parts of the IP address + BYTE b1 = FIRST_IPADDRESS(dwAddr); + BYTE b2 = SECOND_IPADDRESS(dwAddr); + BYTE b3 = THIRD_IPADDRESS(dwAddr); + BYTE b4 = FOURTH_IPADDRESS(dwAddr); + + CHAR szIpAddress[16]; + sprintf(szIpAddress, "%d.%d.%d.%d", b1, b2, b3, b4); + config->address = szIpAddress; + break; + } + case IDOK: { + if (config->address.empty()) { + MessageBoxA(hDlg, "addressempty", + "Kys when?", + MB_ICONWARNING | MB_OK); + } else { + setSocketConfig(config.value()); + config.reset(); + EndDialog(hDlg, LOWORD(wParam)); + } + break; + } + case IDCANCEL: + config.reset(); + EndDialog(hDlg, LOWORD(wParam)); + break; + default: + return DIALOG_NO; + }; + return DIALOG_OK; + default: + break; + } + return DIALOG_NO; +} diff --git a/clientapp/windows/TgBotClient/Icon.ico b/clientapp/windows/TgBotClient/Icon.ico new file mode 100644 index 00000000..5d06b9f2 Binary files /dev/null and b/clientapp/windows/TgBotClient/Icon.ico differ diff --git a/clientapp/windows/TgBotClient/SendFileToChat.cpp b/clientapp/windows/TgBotClient/SendFileToChat.cpp new file mode 100644 index 00000000..ece8dc7c --- /dev/null +++ b/clientapp/windows/TgBotClient/SendFileToChat.cpp @@ -0,0 +1,121 @@ + +#define _CRT_SECURE_NO_WARNINGS +#include +#include + +#include "../../../src/include/TryParseStr.hpp" +#include "TgBotSocketIntf.h" +#include "UIComponents.h" +#include +#include +#include + +namespace { + +std::optional OpenFilePicker(HWND hwnd) { + OPENFILENAME ofn; // Common dialog box structure + wchar_t szFile[MAX_PATH_SIZE]; // Buffer for file name + HANDLE hf; // File handle + + // Initialize OPENFILENAME + ZeroMemory(&ofn, sizeof(ofn)); + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = hwnd; + ofn.lpstrFile = szFile; + // Set lpstrFile[0] to '\0' so that GetOpenFileName does not use the + // contents of szFile to initialize itself. + ofn.lpstrFile[0] = '\0'; + ofn.nMaxFile = sizeof(szFile); + ofn.lpstrFilter = L"All\0*.*\0Text\0*.TXT\0"; + ofn.nFilterIndex = 1; + ofn.lpstrFileTitle = NULL; + ofn.nMaxFileTitle = 0; + ofn.lpstrInitialDir = NULL; + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST; + + // Display the Open dialog box + if (GetOpenFileName(&ofn) == TRUE) + return ofn.lpstrFile; + return std::nullopt; +} + +} // namespace + + +INT_PTR CALLBACK SendFileToChat(HWND hDlg, UINT message, WPARAM wParam, + LPARAM lParam) { + static HWND hChatId; + static HWND hFileText; + static HWND hFileButton; + + UNREFERENCED_PARAMETER(lParam); + switch (message) { + case WM_INITDIALOG: + hChatId = GetDlgItem(hDlg, IDC_CHATID); + hFileButton = GetDlgItem(hDlg, IDC_BROWSE); + hFileText = GetDlgItem(hDlg, IDC_SEL_FILE); + SetFocus(hChatId); + return (INT_PTR)TRUE; + + case WM_COMMAND: + switch (LOWORD(wParam)) { + case IDSEND: { + ChatId chatid = 0; + std::array chatidbuf = {}; + std::array pathbuf = {}; + StringLoader::String errtext; + bool fail = false; + auto& loader = StringLoader::getInstance(); + + GetDlgItemText(hDlg, IDC_CHATID, chatidbuf.data(), + chatidbuf.size() - 1); + + if (Length(chatidbuf.data()) == 0) { + errtext = loader.getString(IDS_CHATID_EMPTY); + fail = true; + } else if (!try_parse(chatidbuf.data(), &chatid)) { + errtext = loader.getString(IDS_CHATID_NOTINT); + fail = true; + } + if (!fail) { + std::string serverReason; + GetDlgItemText(hDlg, IDC_SEL_FILE, pathbuf.data(), pathbuf.size()); + fail = !sendFileToChat( + chatid, pathbuf.data(), + [&serverReason](const GenericAck* data) { + serverReason = data->error_msg; + }); + if (fail) { + errtext = loader.getString(IDS_CMD_FAILED_SVR) + kLineBreak; + errtext += loader.getString(IDS_CMD_FAILED_SVR_RSN); + errtext += StringLoader::String(serverReason.begin(), serverReason.end()); + } else { + MessageBox( + hDlg, + loader.getString(IDS_SUCCESS_FILESENT).c_str(), + loader.getString(IDS_SUCCESS).c_str(), + MB_ICONINFORMATION | MB_OK); + } + } + if (fail) { + MessageBox(hDlg, errtext.c_str(), + loader.getString(IDS_FAILED).c_str(), + MB_ICONERROR | MB_OK); + } + } + + break; + case IDC_BROWSE: + if (const auto f = OpenFilePicker(hDlg); f) { + SetDlgItemText(hDlg, IDC_SEL_FILE, f->c_str()); + } + return (INT_PTR)TRUE; + + case IDCANCEL: + EndDialog(hDlg, LOWORD(wParam)); + return (INT_PTR)TRUE; + }; + break; + } + return (INT_PTR)FALSE; +} diff --git a/clientapp/windows/TgBotClient/SendMessageToChat.cpp b/clientapp/windows/TgBotClient/SendMessageToChat.cpp new file mode 100644 index 00000000..923f3267 --- /dev/null +++ b/clientapp/windows/TgBotClient/SendMessageToChat.cpp @@ -0,0 +1,82 @@ + +#define _CRT_SECURE_NO_WARNINGS +#include + +#include "../../../src/include/TryParseStr.hpp" +#include "TgBotSocketIntf.h" +#include "UIComponents.h" + +INT_PTR CALLBACK SendMsgToChat(HWND hDlg, UINT message, WPARAM wParam, + LPARAM lParam) { + static HWND hChatId; + static HWND hMsgText; + + UNREFERENCED_PARAMETER(lParam); + switch (message) { + case WM_INITDIALOG: + hChatId = GetDlgItem(hDlg, IDC_CHATID); + hMsgText = GetDlgItem(hDlg, IDC_MESSAGETXT); + return (INT_PTR)TRUE; + + case WM_COMMAND: + switch (LOWORD(wParam)) { + case IDSEND: { + ChatId chatid = 0; + std::array msgbuf = {}; + std::array chatidbuf = {}; + StringLoader::String errtext; + bool fail = false; + auto& loader = StringLoader::getInstance(); + + GetDlgItemText(hDlg, IDC_CHATID, chatidbuf.data(), + chatidbuf.size()); + GetDlgItemTextA(hDlg, IDC_MESSAGETXT, msgbuf.data(), + msgbuf.size()); + if (Length(chatidbuf.data()) == 0) { + errtext = loader.getString(IDS_CHATID_EMPTY); + fail = true; + } else if (Length(msgbuf.data()) == 0) { + errtext = loader.getString(IDS_MSG_EMPTY); + fail = true; + } else if (!try_parse(chatidbuf.data(), &chatid)) { + errtext = loader.getString(IDS_CHATID_NOTINT); + fail = true; + } + if (!fail) { + std::string serverReason; + fail = !sendMessageToChat( + chatid, msgbuf.data(), + [&serverReason](const GenericAck* data) { + serverReason = data->error_msg; + }); + if (fail) { + errtext = loader.getString(IDS_CMD_FAILED_SVR) + + kLineBreak; + errtext += loader.getString(IDS_CMD_FAILED_SVR_RSN); + errtext += StringLoader::String( + serverReason.begin(), serverReason.end()); + } else { + MessageBox( + hDlg, + loader.getString(IDS_SUCCESS_MSGSENT).c_str(), + loader.getString(IDS_SUCCESS).c_str(), + MB_ICONINFORMATION | MB_OK); + } + } + if (fail) { + MessageBox(hDlg, errtext.c_str(), + loader.getString(IDS_FAILED).c_str(), + MB_ICONERROR | MB_OK); + } + } + + break; + + case IDCANCEL: + EndDialog(hDlg, LOWORD(wParam)); + return (INT_PTR)TRUE; + }; + break; + } + return (INT_PTR)FALSE; +} diff --git a/clientapp/windows/TgBotClient/SendMessageToChat.h b/clientapp/windows/TgBotClient/SendMessageToChat.h new file mode 100644 index 00000000..6be67502 --- /dev/null +++ b/clientapp/windows/TgBotClient/SendMessageToChat.h @@ -0,0 +1,5 @@ +#pragma once +#include "resource.h" +#include + +INT_PTR CALLBACK SendMsgToChat(HWND, UINT, WPARAM, LPARAM); diff --git a/clientapp/windows/TgBotClient/StringLoader.cpp b/clientapp/windows/TgBotClient/StringLoader.cpp new file mode 100644 index 00000000..d19177fb --- /dev/null +++ b/clientapp/windows/TgBotClient/StringLoader.cpp @@ -0,0 +1,3 @@ +#include "UIComponents.h" + +StringLoader StringLoader::instance; \ No newline at end of file diff --git a/clientapp/windows/TgBotClient/TgBotClient.aps b/clientapp/windows/TgBotClient/TgBotClient.aps new file mode 100644 index 00000000..ad9e0924 Binary files /dev/null and b/clientapp/windows/TgBotClient/TgBotClient.aps differ diff --git a/clientapp/windows/TgBotClient/TgBotClient.cpp b/clientapp/windows/TgBotClient/TgBotClient.cpp new file mode 100644 index 00000000..aa8753d7 --- /dev/null +++ b/clientapp/windows/TgBotClient/TgBotClient.cpp @@ -0,0 +1,171 @@ +#define _CRT_SECURE_NO_WARNINGS + +#pragma comment(lib, "Shcore") + +#include "UIComponents.h" +#include // DPI +#include "AbseilLogging.h" + +#define MAX_LOADSTRING 100 + +// 전역 변수: +HINSTANCE hInst; // 현재 인스턴스입니다. +std::wstring szTitle; + +// 이 코드 모듈에 포함된 함수의 선언을 전달합니다: +ATOM MyRegisterClass(HINSTANCE hInstance); +BOOL InitInstance(HINSTANCE, int); +LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +int APIENTRY wWinMain(_In_ HINSTANCE hInstance, + _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, + _In_ int nCmdShow) { + UNREFERENCED_PARAMETER(hPrevInstance); + UNREFERENCED_PARAMETER(lpCmdLine); + StringLoader::initInstance(hInstance); + + szTitle = StringLoader::getInstance().getString(IDS_APP_TITLE); + MyRegisterClass(hInstance); + + // 애플리케이션 초기화를 수행합니다: + if (!InitInstance(hInstance, nCmdShow)) { + return FALSE; + } + + HACCEL hAccelTable = + LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_TGBOTCLIENT)); + + MSG msg; + + // Logging init + initLogging(); + + // DPI Scaling + SetProcessDpiAwareness(PROCESS_SYSTEM_DPI_AWARE); + + // 기본 메시지 루프입니다: + while (GetMessage(&msg, nullptr, 0, 0)) { + if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + return (int)msg.wParam; +} + +// +// 함수: MyRegisterClass() +// +// 용도: 창 클래스를 등록합니다. +// +ATOM MyRegisterClass(HINSTANCE hInstance) { + WNDCLASSEXW wcex; + + wcex.cbSize = sizeof(WNDCLASSEX); + + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProc; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = hInstance; + wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_TGBOTCLIENT)); + wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); + wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_MENU); + wcex.lpszClassName = szTitle.c_str(); + wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_TGBOTCLIENT)); + + return RegisterClassExW(&wcex); +} + +// +// 함수: InitInstance(HINSTANCE, int) +// +// 용도: 인스턴스 핸들을 저장하고 주 창을 만듭니다. +// +// 주석: +// +// 이 함수를 통해 인스턴스 핸들을 전역 변수에 저장하고 +// 주 프로그램 창을 만든 다음 표시합니다. +// +BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) { + hInst = hInstance; // 인스턴스 핸들을 전역 변수에 저장합니다. + + HWND hWnd = CreateWindowW( + szTitle.c_str(), szTitle.c_str(), WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, + nullptr, hInstance, nullptr); + + if (hWnd == nullptr) { + return FALSE; + } + + ShowWindow(hWnd, nCmdShow); + UpdateWindow(hWnd); + + return TRUE; +} + +// +// 함수: WndProc(HWND, UINT, WPARAM, LPARAM) +// +// 용도: 주 창의 메시지를 처리합니다. +// +// WM_COMMAND - 애플리케이션 메뉴를 처리합니다. +// WM_PAINT - 주 창을 그립니다. +// WM_DESTROY - 종료 메시지를 게시하고 반환합니다. +// +// +LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, + LPARAM lParam) { + switch (message) { + case WM_CREATE: { + break; + } + case WM_SIZE: + break; + case WM_COMMAND: { + int wmId = LOWORD(wParam); + switch (wmId) { + case IDM_ABOUT: + DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, + About); + break; + case ID_MENU_SENDMSG: + DialogBox(hInst, MAKEINTRESOURCE(IDD_SENDMSG_DLG), hWnd, + SendMsgToChat); + break; + case ID_MENU_SENDFILE: + DialogBox(hInst, MAKEINTRESOURCE(IDD_SENDFILE_DLG), hWnd, + SendFileToChat); + break; + case ID_MENU_UPTIME: + DialogBox(hInst, MAKEINTRESOURCE(IDD_UPTIME_DLG), hWnd, + Uptime); + break; + case ID_MENU_DESTIP: + DialogBox(hInst, MAKEINTRESOURCE(IDD_DEST_DLG), hWnd, + DestinationIP); + break; + case IDM_EXIT: + DestroyWindow(hWnd); + break; + default: + return DefWindowProc(hWnd, message, wParam, lParam); + } + } break; + case WM_PAINT: { + PAINTSTRUCT ps; + HDC hdc = BeginPaint(hWnd, &ps); + // TODO: 여기에 hdc를 사용하는 그리기 코드를 추가합니다... + EndPaint(hWnd, &ps); + } break; + case WM_DESTROY: + PostQuitMessage(0); + break; + default: + return DefWindowProc(hWnd, message, wParam, lParam); + } + return 0; +} diff --git a/clientapp/windows/TgBotClient/TgBotClient.h b/clientapp/windows/TgBotClient/TgBotClient.h new file mode 100644 index 00000000..d00d47e7 --- /dev/null +++ b/clientapp/windows/TgBotClient/TgBotClient.h @@ -0,0 +1,3 @@ +#pragma once + +#include "resource.h" diff --git a/clientapp/windows/TgBotClient/TgBotClient.ico b/clientapp/windows/TgBotClient/TgBotClient.ico new file mode 100644 index 00000000..a2048b23 Binary files /dev/null and b/clientapp/windows/TgBotClient/TgBotClient.ico differ diff --git a/clientapp/windows/TgBotClient/TgBotClient.rc b/clientapp/windows/TgBotClient/TgBotClient.rc new file mode 100644 index 00000000..f893aaa8 Binary files /dev/null and b/clientapp/windows/TgBotClient/TgBotClient.rc differ diff --git a/clientapp/windows/TgBotClient/TgBotClient.sln b/clientapp/windows/TgBotClient/TgBotClient.sln new file mode 100644 index 00000000..56176ccb --- /dev/null +++ b/clientapp/windows/TgBotClient/TgBotClient.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TgBotClient", "TgBotClient.vcxproj", "{814F6EBA-A004-4933-9B30-592D9E9027CA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {814F6EBA-A004-4933-9B30-592D9E9027CA}.Debug|x64.ActiveCfg = Debug|x64 + {814F6EBA-A004-4933-9B30-592D9E9027CA}.Debug|x64.Build.0 = Debug|x64 + {814F6EBA-A004-4933-9B30-592D9E9027CA}.Debug|x86.ActiveCfg = Debug|Win32 + {814F6EBA-A004-4933-9B30-592D9E9027CA}.Debug|x86.Build.0 = Debug|Win32 + {814F6EBA-A004-4933-9B30-592D9E9027CA}.Release|x64.ActiveCfg = Release|x64 + {814F6EBA-A004-4933-9B30-592D9E9027CA}.Release|x64.Build.0 = Release|x64 + {814F6EBA-A004-4933-9B30-592D9E9027CA}.Release|x86.ActiveCfg = Release|Win32 + {814F6EBA-A004-4933-9B30-592D9E9027CA}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0705977F-1BC2-4CBE-8CD5-8FCC4CD22EF4} + EndGlobalSection +EndGlobal diff --git a/clientapp/windows/TgBotClient/TgBotClient.vcxproj b/clientapp/windows/TgBotClient/TgBotClient.vcxproj new file mode 100644 index 00000000..b75304f7 --- /dev/null +++ b/clientapp/windows/TgBotClient/TgBotClient.vcxproj @@ -0,0 +1,159 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 17.0 + Win32Proj + {814f6eba-a004-4933-9b30-592d9e9027ca} + TgBotClient + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + Level3 + true + WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + + + Windows + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + + + Windows + true + true + true + + + + + Level3 + true + _DEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + stdcpp20 + stdc17 + + + Windows + true + + + + + Level3 + true + true + true + NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + + + Windows + true + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/clientapp/windows/TgBotClient/TgBotClient.vcxproj.filters b/clientapp/windows/TgBotClient/TgBotClient.vcxproj.filters new file mode 100644 index 00000000..b05fbb28 --- /dev/null +++ b/clientapp/windows/TgBotClient/TgBotClient.vcxproj.filters @@ -0,0 +1,79 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {0901d22e-1ed2-44b0-a92b-cc605fc2d51c} + + + + + 헤더 파일 + + + 헤더 파일 + + + 소스 파일 + + + 헤더 파일 + + + 헤더 파일 + + + 헤더 파일 + + + + + 소스 파일 + + + 소스 파일 + + + 소스 파일 + + + 소스 파일\ui + + + 소스 파일\ui + + + 소스 파일\ui + + + 소스 파일\ui + + + 소스 파일 + + + 소스 파일\ui + + + + + 리소스 파일 + + + + + 리소스 파일 + + + \ No newline at end of file diff --git a/clientapp/windows/TgBotClient/TgBotSocketIntf.cpp b/clientapp/windows/TgBotClient/TgBotSocketIntf.cpp new file mode 100644 index 00000000..6792293b --- /dev/null +++ b/clientapp/windows/TgBotClient/TgBotSocketIntf.cpp @@ -0,0 +1,379 @@ + +#pragma comment(lib, "ws2_32") + +#define _CRT_SECURE_NO_WARNINGS +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "TgBotSocketIntf.h" +#include + +using PktHeader = TgBotCommandPacketHeader; +using UniqueMalloc = std::unique_ptr; + +template > +struct TgBotCommandContext { + PktHeader header; + std::unique_ptr data; + + TgBotCommandContext() = default; + TgBotCommandContext(void *_data, Deleter deleter) : data(_data, deleter) {} +}; + +struct FileData { + void *data; + PktHeader::length_type len; +}; + +bool fileData_tofile(const void *ptr, size_t len) { + const auto *data = static_cast(ptr); + UploadFile destfilepath{}; + FILE *file = nullptr; + size_t ret = 0; + size_t file_size = len - sizeof(UploadFile); + + LOG(INFO) << "This buffer has a size of " << len << " bytes"; + LOG(INFO) << "Which is " << file_size << " bytes excluding the header"; + + strncpy(destfilepath, data, sizeof(UploadFile) - 1); + if ((file = fopen(destfilepath, "wb")) == nullptr) { + LOG(ERROR) << "Failed to open file: " << destfilepath; + return false; + } + ret = fwrite(data + sizeof(UploadFile), file_size, 1, file); + if (ret != 1) { + LOG(ERROR) << "Failed to write to file: " << destfilepath << " (Wrote " + << ret << " bytes)"; + fclose(file); + return false; + } + fclose(file); + return true; +} + +std::optional fileData_fromFile(const std::string_view filename, + const std::string_view destfilepath) { + constexpr size_t datahdr_size = sizeof(UploadFile); + size_t size = 0; + size_t total_size = 0; + char *buf = nullptr; + FILE *fp = nullptr; + std::optional pkt; + + fp = fopen(filename.data(), "rb"); + if (fp != nullptr) { + fseek(fp, 0, SEEK_END); + size = ftell(fp); + fseek(fp, 0, SEEK_SET); + total_size = size + datahdr_size; + LOG(INFO) << "Sending file " << filename.data(); + buf = static_cast(malloc(total_size)); + LOG(INFO) << "mem-alloc buffer of size " << total_size << " bytes"; + // Copy data header to the beginning of the buffer. + if (buf != nullptr) { + strncpy(buf, destfilepath.data(), datahdr_size); + char *moved_buf = buf + datahdr_size; + fread(moved_buf, 1, size, fp); + pkt = FileData{buf, total_size}; + } + fclose(fp); + } else { + LOG(ERROR) << "Failed to open file " << filename.data(); + } + return pkt; +} + +static std::string WSALastErrStr() { + char *s = nullptr; + FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, WSAGetLastError(), + MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), (LPSTR)&s, 0, + nullptr); + std::string ret(s); + LocalFree(s); + return ret; +} + +class TgBotSocketNative { + public: + using callback_data_handler_f = + std::function; + template + bool sendContext(TgBotCommandContext &context, + callback_data_handler_f fn) const { + bool ret = false; + WSADATA __data; + + if (WSAStartup(kWSAVersion, &__data) != 0) { + LOG(ERROR) << "Failed to WSAStartup"; + return false; + } + + switch (config.mode) { + case SocketConfig::Mode::USE_IPV4: + ret = sendContextCommon(context, fn); + break; + case SocketConfig::Mode::USE_IPV6: + ret = sendContextCommon(context, fn); + break; + default: + LOG(ERROR) << "Unknown mode: " << static_cast(config.mode); + break; + } + WSACleanup(); + return ret; + } + + void setSocketConfig(SocketConfig config_in) { + config = std::move(config_in); + } + + static std::shared_ptr getInstance() { + static auto instance = + std::make_shared(TgBotSocketNative()); + return instance; + } + + private: + constexpr static int kSocketPort = 50000; + constexpr static int kRecvFlags = MSG_WAITALL; + constexpr static DWORD kWSAVersion = MAKEWORD(2, 2); + SocketConfig config; + + TgBotSocketNative() = default; + + static void __cdecl closeFd(SOCKET *fd) { closesocket(*fd); } + static void LogWSAErr(const std::string_view message) { + LOG(ERROR) << "Failed to " << message << ": " << WSALastErrStr(); + } + + template + bool sendContextCommon(TgBotCommandContext &context, + callback_data_handler_f callback) const { + SockAddr addr{}; + socklen_t len = sizeof(SockAddr); + SOCKET sockfd{}; + int ret{}; + + LOG(INFO) << "Prepare to send CommandContext"; + + sockfd = socket(af, SOCK_STREAM, 0); + if (sockfd == INVALID_SOCKET) { + LogWSAErr("create socket"); + return false; + } + + auto sockFdCloser = std::unique_ptr( + &sockfd, &TgBotSocketNative::closeFd); + + LOG(INFO) << "Using IP: " << std::quoted(config.address, '\'') + << ", Port: " << kSocketPort << " with af: " << af; + setupSockAddress(&addr); + + // Calculate CRC32 + uLong crc = crc32(0L, Z_NULL, 0); // Initial value + crc = crc32(crc, reinterpret_cast(context.data.get()), + context.header.data_size); + context.header.checksum = crc; + + if (connect(sockfd, reinterpret_cast(&addr), len) != 0) { + LogWSAErr("connect to server"); + return false; + } + LOG(INFO) << "Connected to server"; + ret = send(sockfd, reinterpret_cast( & context.header), + sizeof(PktHeader), 0); + if (ret < 0) { + LogWSAErr("send packet header"); + return false; + } else { + DLOG(INFO) << "Sent header packet with cmd " << context.header.cmd + << ", " << ret << " bytes"; + } + ret = send(sockfd, reinterpret_cast(context.data.get()), context.header.data_size, 0); + if (ret < 0) { + LogWSAErr("send packet data"); + return false; + } else { + DLOG(INFO) << "Sent data packet, " << ret << " bytes"; + } + LOG(INFO) << "Done sending data"; + LOG(INFO) << "Now reading callback"; + + PktHeader header; + ret = recv(sockfd, reinterpret_cast(&header), sizeof(header), + kRecvFlags); + if (ret < 0) { + LogWSAErr("read callback header"); + return false; + } + if (header.magic != PktHeader::MAGIC_VALUE) { + LOG(ERROR) << "Failed to validate magic value of callback header"; + return false; + } + UniqueMalloc data(malloc(header.data_size), &free); + if (data == nullptr) { + LOG(ERROR) << "Failed to alloc data for callback header"; + return false; + } + ret = recv(sockfd, static_cast(data.get()), header.data_size, + kRecvFlags); + if (ret < 0) { + LogWSAErr("recv callback data"); + return false; + } + switch (header.cmd) { + case CMD_GET_UPTIME_CALLBACK: + case CMD_DOWNLOAD_FILE_CALLBACK: + if (!callback(data.get(), header.data_size)) { + LOG(ERROR) << "Failed to execute callback"; + return false; + } + break; + default: { + GenericAck AckData{}; + bool success{}; + + memcpy(&AckData, data.get(), sizeof(AckData)); + success = AckData.result == AckType::SUCCESS; + LOG(INFO) << "Command ACK: " << std::boolalpha << success; + if (!success) { + LOG(ERROR) << "Reason: " << AckData.error_msg; + } + // invoke the callback + if (!callback(data.get(), header.data_size)) { + LOG(ERROR) << "Failed to execute callback"; + return false; + } + return success; + } + } + return true; + } + + template + void setupSockAddress(SockAddr *addr) const = delete; + template <> + [[maybe_unused]] void setupSockAddress(sockaddr_in *addr) const { + addr->sin_family = AF_INET; + inet_pton(AF_INET, config.address.c_str(), &addr->sin_addr); + addr->sin_port = htons(kSocketPort); + } + template <> + [[maybe_unused]] void setupSockAddress(sockaddr_in6 *addr) const { + addr->sin6_family = AF_INET6; + inet_pton(AF_INET6, config.address.c_str(), &addr->sin6_addr); + addr->sin6_port = htons(kSocketPort); + } +}; + +using callback_t = TgBotSocketNative::callback_data_handler_f; + +template +TgBotCommandContext createContext(const DataStruct &data) { + TgBotCommandContext context; + context.header.cmd = cmd; + context.header.data_size = sizeof(DataStruct); + context.data = std::make_unique(data); + return context; +} + +template +bool trySendContext(DataType data, callback_t callback) { + auto context = createContext(data); + return trySendContext(context, callback); +} + +template +bool trySendContext(TgBotCommandContext &context, + callback_t callback) { + const auto socket = TgBotSocketNative::getInstance(); + return socket->sendContext(context, callback); +} + +std::string getUptime() { + std::string result; + bool dummy = true; + trySendContext( + dummy, [&result](const void *data, PktHeader::length_type sz) { + const auto *uptime = static_cast(data); + result = std::string(uptime, sz); + return true; + }); + return result; +} + + +bool sendMessageToChat(ChatId id, std::string message, GenericAckCallback callback) { + WriteMsgToChatId data{}; + strncpy(data.msg, message.c_str(), MAX_MSG_SIZE - 1); + data.to = id; + return trySendContext( + data, [callback](const void *data, PktHeader::length_type sz) { + const auto *result = static_cast(data); + callback(result); + return true; + }); +} + + +bool sendFileToChat(ChatId id, std::filesystem::path filepath, + GenericAckCallback callback) { + SendFileToChatId data{}; + GenericAck resultAck{}; + bool ret = false; + auto fData = fileData_fromFile(filepath.string().c_str(), + filepath.filename().string().c_str()); + if (!fData) { + resultAck.result = AckType::ERROR_RUNTIME_ERROR; + strncpy(resultAck.error_msg, "Client error: Failed to prepare file", + sizeof(resultAck.error_msg)); + callback(&resultAck); + return false; + } + TgBotCommandContext context(fData->data, &free); + context.header.cmd = CMD_UPLOAD_FILE; + context.header.data_size = fData->len; + // TODO: use the msg by server here too, when it failed + ret = trySendContext(context, [](const void *data, PktHeader::length_type sz) { + return true; + }); + if (!ret) { + resultAck.result = AckType::ERROR_RUNTIME_ERROR; + strncpy(resultAck.error_msg, "Client error: Failed to upload file", + sizeof(resultAck.error_msg)); + callback(&resultAck); + return false; + } + + strncpy(data.filepath, filepath.filename().string().c_str(), + MAX_PATH_SIZE - 1); + data.id = id; + data.type = TYPE_DOCUMENT; + ret = trySendContext( + data, [callback](const void *data, PktHeader::length_type sz) { + const auto *result = static_cast(data); + callback(result); + return true; + }); + return ret; +} + +void setSocketConfig(SocketConfig config) { + TgBotSocketNative::getInstance()->setSocketConfig(config); + LOG(INFO) << "New config: IPAddress: " << config.address; + LOG(INFO) << "New config: AF: " << ((config.mode == + SocketConfig::Mode::USE_IPV4) + ? "IPv4" + : "IPv6"); +} \ No newline at end of file diff --git a/clientapp/windows/TgBotClient/TgBotSocketIntf.h b/clientapp/windows/TgBotClient/TgBotSocketIntf.h new file mode 100644 index 00000000..30cbfe8c --- /dev/null +++ b/clientapp/windows/TgBotClient/TgBotSocketIntf.h @@ -0,0 +1,22 @@ + +#pragma once + +#include + +// Include the tgbot's exported header +#include "../../../src/socket/include/TgBotCommandExport.hpp" +#include + +struct SocketConfig { + std::string address; + enum class Mode { USE_IPV4, USE_IPV6 } mode = Mode::USE_IPV4; +}; + +using GenericAckCallback = std::function; + +extern bool sendMessageToChat(ChatId id, std::string message, + GenericAckCallback callback); +extern bool sendFileToChat(ChatId id, std::filesystem::path filepath, + GenericAckCallback callback); +extern std::string getUptime(); +extern void setSocketConfig(SocketConfig config); \ No newline at end of file diff --git a/clientapp/windows/TgBotClient/UIComponents.h b/clientapp/windows/TgBotClient/UIComponents.h new file mode 100644 index 00000000..5e284553 --- /dev/null +++ b/clientapp/windows/TgBotClient/UIComponents.h @@ -0,0 +1,59 @@ +#pragma once + +#include "resource.h" +#include +#include +#include +#include +#include + +#define DEFINE_COMPONENT(name) \ + INT_PTR CALLBACK name(HWND, UINT, WPARAM, LPARAM) + +DEFINE_COMPONENT(SendMsgToChat); +DEFINE_COMPONENT(SendFileToChat); +DEFINE_COMPONENT(About); +DEFINE_COMPONENT(Uptime); +DEFINE_COMPONENT(DestinationIP); + +class StringLoader { + public: + constexpr static size_t MAX_LEN = 256; + using String = std::basic_string; + + static void initInstance(HINSTANCE hAppInst) { + instance.hAppInst = hAppInst; + } + static StringLoader& getInstance() { return instance; } + + String getString(const int id) { + if (cachedStr.find(id) != cachedStr.end()) { + return cachedStr.at(id); + } + std::array buffer{}; + LoadString(hAppInst, id, buffer.data(), MAX_LEN - 1); + cachedStr[id] = buffer.data(); + return buffer.data(); + } + + private: + HINSTANCE hAppInst; + std::map cachedStr; + static StringLoader instance; +}; + +inline const TCHAR *kLineBreak = _T("\r\n"); +constexpr INT_PTR DIALOG_OK = (INT_PTR)TRUE; +constexpr INT_PTR DIALOG_NO = (INT_PTR)FALSE; + +template +std::size_t Length(CharT* str) = delete; + +template <> +inline std::size_t Length(char* str) { + return std::strlen(str); +} +template <> +inline std::size_t Length(wchar_t* str) { + return std::wcslen(str); +} \ No newline at end of file diff --git a/clientapp/windows/TgBotClient/Uptime.cpp b/clientapp/windows/TgBotClient/Uptime.cpp new file mode 100644 index 00000000..d701e718 --- /dev/null +++ b/clientapp/windows/TgBotClient/Uptime.cpp @@ -0,0 +1,28 @@ +#define _CRT_SECURE_NO_WARNINGS +#include "TgBotSocketIntf.h" +#include "UIComponents.h" + +INT_PTR CALLBACK Uptime(HWND hDlg, UINT message, WPARAM wParam, + LPARAM lParam) { + static HWND hUptimeText; + + UNREFERENCED_PARAMETER(lParam); + switch (message) { + case WM_INITDIALOG: + hUptimeText = GetDlgItem(hDlg, IDC_UPTIME); + return (INT_PTR)TRUE; + + case WM_COMMAND: + switch (LOWORD(wParam)) { + case IDC_LOAD: + SetDlgItemTextA(hDlg, IDC_UPTIME, getUptime().c_str()); + return (INT_PTR)TRUE; + case IDCANCEL: + case IDOK: + EndDialog(hDlg, LOWORD(wParam)); + return (INT_PTR)TRUE; + }; + break; + } + return (INT_PTR)FALSE; +} \ No newline at end of file diff --git a/clientapp/windows/TgBotClient/nullguard.cpp b/clientapp/windows/TgBotClient/nullguard.cpp new file mode 100644 index 00000000..5530b38c --- /dev/null +++ b/clientapp/windows/TgBotClient/nullguard.cpp @@ -0,0 +1,35 @@ +// Copyright 2023 The Abseil Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "absl/log/internal/nullguard.h" + +#include + +#include "absl/base/attributes.h" +#include "absl/base/config.h" + +namespace absl { +ABSL_NAMESPACE_BEGIN +namespace log_internal { + +ABSL_CONST_INIT ABSL_DLL const std::array kCharNull{ + {'(', 'n', 'u', 'l', 'l', ')', '\0'}}; +ABSL_CONST_INIT ABSL_DLL const std::array kSignedCharNull{ + {'(', 'n', 'u', 'l', 'l', ')', '\0'}}; +ABSL_CONST_INIT ABSL_DLL const std::array kUnsignedCharNull{ + {'(', 'n', 'u', 'l', 'l', ')', '\0'}}; + +} // namespace log_internal +ABSL_NAMESPACE_END +} // namespace absl \ No newline at end of file diff --git a/clientapp/windows/TgBotClient/resource.h b/clientapp/windows/TgBotClient/resource.h new file mode 100644 index 00000000..b1f80e24 --- /dev/null +++ b/clientapp/windows/TgBotClient/resource.h @@ -0,0 +1,69 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by TgBotClient.rc +// +#define IDC_MYICON 2 +#define IDD_TGBOTCLIENT_DIALOG 102 +#define IDD_SENDMSG_DLG 102 +#define IDS_APP_TITLE 103 +#define IDD_ABOUTBOX 103 +#define IDM_ABOUT 104 +#define IDD_SENDFILE_DLG 104 +#define IDS_CHATID_EMPTY 104 +#define IDM_EXIT 105 +#define IDS_MSG_EMPTY 105 +#define IDS_CHATID_NOTINT 106 +#define IDI_TGBOTCLIENT 107 +#define IDS_CMD_FAILED_SVR 107 +#define IDS_CMD_FAILED_SVR_RSN 108 +#define IDC_TGBOTCLIENT 109 +#define IDS_SUCCESS 109 +#define IDC_MENU 109 +#define IDS_FAILED 110 +#define IDS_SUCCESS_FILESENT 111 +#define IDS_SUCCESS_MSGSENT 112 +#define IDR_MAINFRAME 128 +#define IDD_UPTIME_DLG 130 +#define IDR_MENU1 131 +#define IDR_MENU 131 +#define IDD_DEST_CNG 132 +#define IDD_DEST_DLG 132 +#define IDC_BUTTON1 1000 +#define IDC_LOAD 1000 +#define IDC_CHATID 1001 +#define IDC_MESSAGETXT 1002 +#define IDC_ 1003 +#define IDSEND 1004 +#define IDC_BROWSE 1005 +#define IDC_SEL_FILE 1006 +#define IDC_UPTIME 1008 +#define IDC_INET_4 1009 +#define IDC_INET_6 1010 +#define IDC_IPADDR 1011 +#define ID_32771 32771 +#define ID_32772 32772 +#define ID_32773 32773 +#define ID_MENU_SENDFILE 32774 +#define ID_MENU_SENDMSG 32775 +#define ID_32776 32776 +#define ID_MENU_UPTIME 32777 +#define ID_HELP_WEBSITE 32778 +#define IDM_WEBSITE 32779 +#define ID_32780 32780 +#define ID_FILE_DESTINATIO 32781 +#define ID_DESTIP 32782 +#define IDM_DESTIP 32783 +#define ID_MENU_DESTIP 32784 +#define IDC_STATIC -1 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NO_MFC 1 +#define _APS_NEXT_RESOURCE_VALUE 133 +#define _APS_NEXT_COMMAND_VALUE 32785 +#define _APS_NEXT_CONTROL_VALUE 1012 +#define _APS_NEXT_SYMED_VALUE 110 +#endif +#endif diff --git a/clientapp/windows/TgBotClient/targetver.h b/clientapp/windows/TgBotClient/targetver.h new file mode 100644 index 00000000..95256fe6 --- /dev/null +++ b/clientapp/windows/TgBotClient/targetver.h @@ -0,0 +1,6 @@ +#pragma once + +// // SDKDDKVer.h를 포함하면 최고 수준의 가용성을 가진 Windows 플랫폼이 정의됩니다. +// 이전 Windows 플랫폼용 애플리케이션을 빌드하려는 경우에는 SDKDDKVer.h를 포함하기 전에 +// WinSDKVer.h를 포함하고 _WIN32_WINNT를 지원하려는 플랫폼으로 설정합니다. +#include