一坨更大的垃圾

· · 个人记录

可以监听洛谷私信。

现在可以提供 windows 下的系统通知提示。

用 win32api 做出了图形化的界面。

可以发送 http 请求来发消息。

暂时不会发 release。因为我的 github 账号被 flagged 了,发了你们也看不到。想要 release 可以联系我 qq。

适用 win10/11 x64。

编译方法

请安装所需的库和头文件(openssl, websocketpp, boost, nlohmann/json.hpp, httplib)。

请对 C++ 代码使用编译 dos 脚本(compile.bat / compile.cmd):

g++.exe -c main.cpp -o main.o -I"c:/mingw64/include/c++/12.2.0" -I"c:/mingw64/include/c++/12.2.0/x86_64-w64-mingw32" -I"c:/mingw64/include/c++/12.2.0/backward" -I"c:/mingw64/lib/gcc/x86_64-w64-mingw32/12.2.0/include" -I"c:/mingw64/include" -I"c:/mingw64/lib/gcc/x86_64-w64-mingw32/12.2.0/include-fixed" -I"c:/mingw64/x86_64-w64-mingw32/include" -Wall -O2 -lws2_32 -lcrypt32 -lcryptui -lssl -lcrypto  -finput-charset=utf-8 -fexec-charset=gbk 

g++.exe main.o -o "main.exe" -L"c:/mingw64/lib/gcc/x86_64-w64-mingw32/12.2.0/" -L"c:/mingw64/lib/gcc/" -L"c:/mingw64/x86_64-w64-mingw32/lib/" -L"c:/mingw64/lib/" -L"C:/mingw64/lib" -L"C:/mingw64/x86_64-w64-mingw32/lib" -s -mwindows -lws2_32 -lcrypt32 -lcryptui -lssl -lcrypto

pause

我的编译器在 C:/mingw64 处,boost 之类所需的 lib 和 headers 等都混进了 mingw64 里。

请对 js 代码使用:pkg NotifierNodeJS.js --target=node18-win-x64,需要安装 NodeJS 18。

使用需求

请在 cpp 文件编译出的 exe 所在文件夹下额外放三个文件和一个文件夹:

如果需要分发,请额外包含:

以及别忘了附上一个合适的 readme.txt 并删除 cookie.txt 中的内容。

源代码

为了缩短代码长度,删除了一些意义不大的内容和注释。

js

const notifier = require("node-notifier");
const opn = require("opn");
function notify(appName, title, content, icon, trigger) {
  notifier.notify( { appName: appName, title: title, message: content, icon: icon, sound: true,  wait: true }, (error, response, metadata) => {
      if (!error) {
        if (response == "activate" && metadata.activationType == "clicked") { if (trigger != "SP:NOTRIGGER") { opn(trigger); } }
        else if (response == undefined) { if (trigger != "SP:NOTRIGGER") { opn(trigger); } }
      }
    }
  );
}
const args = require('minimist')(process.argv.slice(2));
if ( args['appName'] != undefined && args['title'] != undefined && args['content'] != undefined && args['icon'] != undefined ) {
  if (args['trigger'] == undefined) { notify (args['appName'], args['title'], args['content'], args['icon'], "SP:NOTRIGGER"); }
  else { notify (args['appName'], args['title'], args['content'], args['icon'], args['trigger']); }
} else {
  console.error("Invalid arguments.");
  console.error("Usage: programName --appName=appName(string) --title=title(string) --content=content(string) --icon=icon(string: pathToFile / \"undefined\") [--trigger=trigger(string: app/link)]");
}

cpp

// Web headers: websocket, http
#include <websocketpp/config/asio_client.hpp>
#include <websocketpp/client.hpp>
#define CPPHTTPLIB_OPENSSL_SUPPORT
#include <httplib.h>

// parsers
#include <nlohmann/json.hpp>
#include <regex>

// other headers
#include <iostream>
#include <wchar.h>
#include <ctime>
#include <cmath>

// Specfied: For a Windows application
#include <windows.h>

// Luogu APIs:
// https://github.com/0f-0b/luogu-api-docs
// https://0f-0b.github.io/luogu-api-docs/

typedef websocketpp::client<websocketpp::config::asio_tls_client> client;
typedef websocketpp::lib::shared_ptr<websocketpp::lib::asio::ssl::context> context_ptr;

client::connection_ptr* conptr; // sharing "con" in wsClientMain
HWND* hwndInputBox;             // sharing "hwnd_ReceivedBox" in WndProc => DrawContents
int uid;                        // user UID of the client
std::string _uid;               // cookie value of _uid of the client (string)
std::string __client_id;        // cookie value of __client_id of the client
std::string cookie;             // full cookie of the client
httplib::Client cliFiles("https://cdn.luogu.com.cn");
httplib::Client cliMsg("https://www.luogu.com.cn");
const std::string cert = "ISRG Root X1.crt";

// For alive timing (invalid connection checkers)
clock_t time_req, timePing_req;
bool islogon = 0;

using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
using websocketpp::lib::bind;
namespace fs = std::filesystem;

/// @brief Check the existence of file, if file not exists then create one.
/// @param filePath Full (relative / absolute) path of the file.
/// @param initContent Initial content of the file (if need to create one)
/// @return the ifstream of the existing/created file 
std::ifstream initFile(const std::string& filePath, const std::string& initContent) {
// json parser wil not work without a existed json file and the program will terminate.
  fs::path directory = fs::path(filePath).parent_path();
  if (!fs::exists(directory)) {fs::create_directories(directory);} // creath directory
  std::ofstream file(filePath);
  if (file.is_open()) {
    file << initContent; // init file
    file.close();
  }
  return std::ifstream(filePath);
}

namespace TLS_INIT {
// This namespace is derived from examples and documentations. 
// https://github.com/zaphoyd/websocketpp/issues/706
// https://github.com/cotomonaga/websocketpp_tutorial/blob/master/utility_client/step7.cpp
// https://blog.csdn.net/byxdaz/article/details/84645586
  bool verify_subject_alternative_name(const char* hostname, X509* cert) {
    STACK_OF(GENERAL_NAME)* san_names = NULL;
    san_names = (STACK_OF(GENERAL_NAME)*) X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL);
    if (san_names == NULL) {
      return false;
    }
    int san_names_count = sk_GENERAL_NAME_num(san_names);
    bool result = false;
    for (int i = 0; i < san_names_count; i++) {
      const GENERAL_NAME* current_name = sk_GENERAL_NAME_value(san_names, i);
      if (current_name->type != GEN_DNS) {
        continue;
      }
      char const* dns_name = (char const*)ASN1_STRING_get0_data(current_name->d.dNSName);
      if (ASN1_STRING_length(current_name->d.dNSName) != (int) strlen(dns_name)) {
        break;
      }
      result = (strcasecmp(hostname, dns_name) == 0);
    }
    sk_GENERAL_NAME_pop_free(san_names, GENERAL_NAME_free);
    return result;
  }
  bool verify_common_name(char const* hostname, X509* cert) {
    int common_name_loc = X509_NAME_get_index_by_NID(X509_get_subject_name(cert), NID_commonName, -1);
    if (common_name_loc < 0) {
      return false;
    }
    X509_NAME_ENTRY* common_name_entry = X509_NAME_get_entry(X509_get_subject_name(cert), common_name_loc);
    if (common_name_entry == NULL) {
      return false;
    }
    ASN1_STRING* common_name_asn1 = X509_NAME_ENTRY_get_data(common_name_entry);
    if (common_name_asn1 == NULL) {
      return false;
    }
    char const* common_name_str = (char const*)ASN1_STRING_get0_data(common_name_asn1);
    if (ASN1_STRING_length(common_name_asn1) != (int) strlen(common_name_str)) {
      return false;
    }
    return (strcasecmp(hostname, common_name_str) == 0);
  }
  bool verify_certificate(const char* hostname, bool preverified, boost::asio::ssl::verify_context& ctx) {
    int depth = X509_STORE_CTX_get_error_depth(ctx.native_handle( ));
    if (depth == 0 && preverified) {
      X509* cert = X509_STORE_CTX_get_current_cert(ctx.native_handle( ));
      if (verify_subject_alternative_name(hostname, cert)) {
        return true;
      } else if (verify_common_name(hostname, cert)) {
        return true;
      } else {
        return false;
      }
    }
    return preverified;
  }
  context_ptr on_tls_init(const char* hostname, websocketpp::connection_hdl) {
    context_ptr ctx = websocketpp::lib::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::sslv23);
    try {
      ctx->set_options(boost::asio::ssl::context::default_workarounds |
                      boost::asio::ssl::context::no_sslv2 |
                      boost::asio::ssl::context::no_sslv3 |
                      boost::asio::ssl::context::single_dh_use);
      ctx->set_verify_mode(boost::asio::ssl::verify_peer);
      ctx->set_verify_callback(bind(&verify_certificate, hostname, ::_1, ::_2));
      ctx->load_verify_file(cert.c_str( ));
    } catch (std::exception& e) {
      std::cout << e.what( ) << std::endl;
    }
    return ctx;
  }
}
using namespace TLS_INIT;

// For "drawContentInputBox".
std::string msgConnected = ""; // the connected string of received messages
// std::string msgArchive[65536]; // the string archive of received messages
// int msgCount = 0;              // the number of received messages

/// @brief Draw text in a input box (specifically for message receive box)
/// @param addstr the text needed to add
void drawContentInputBox (std::string addstr) {
  // msgArchive[msgCount] = addstr;        // archive the message (in fact not necessary)
  msgConnected += addstr + "\r\n\r\n";     // connect the message
  // msgCount += 1;                        // for archiving
  SetWindowText((*hwndInputBox), msgConnected.c_str()); // print
}

/// @brief Function used to close connection
/// @param reason Reason to close connection that put into the input box.
void closeConnection (std::string reason) {
  drawContentInputBox (reason);
  websocketpp::lib::error_code ec;
  // std::string reason is NOT for the reason of closing connection here
  (*conptr)->close(websocketpp::close::status::going_away, "", ec);
  if (ec) {
    std::string errInfo = "Error closing connection: " + ec.message();
    drawContentInputBox (errInfo);
    MessageBox(NULL, errInfo.c_str(), TEXT("Error"), MB_OK | MB_ICONERROR);
  }
}

// Invalid connection checkers.
namespace checkerThreadFuncs {
/// @brief Check if the client is logged in. 
  void loginChecker ( ) { /*
    If the client have no "join_result" after the program start for 5 secs,
    the client may have a invalid cookie.
*/  Sleep (1000 * 5);
    if (islogon == 0) {
      closeConnection ("Connection will restart, for join_result is not received in 5 seconds. "
                      "Please fill your cookies (_uid, __client_id) correctly. " + _uid + " " + __client_id);
    }
  }

/// @brief Check if the client is disconnected by ping. 
  void pingTimer ( ) { /*
    If the client received no ping after the time interval of 60 secs,
    the client may be automaticly disconnected, maybe for logged in on a different client.
    Normally, the ping interval is 54 secs, the range of checking is 60 secs.
*/  Sleep (1000 * 60);
    if ((float) (clock ( ) - timePing_req) / CLOCKS_PER_SEC > 60.0) {
      closeConnection ("Connection will restart, for no ping is receiced for 1 minute "
                      "(normal: approx 54 seconds).");
    }
  }

/// @brief Check if the client is disconnected by heartbeat. 
  void heartbeatTimer ( ) { /*
    If the client received no heartbeat after the time interval of 96 secs,
    the client may be automaticly disconnected, maybe for logged in on a different client.
    Normally, the ping interval is 90 secs, the range of checking is 96 secs.
*/  Sleep (1000 * 96);
    if ((float) (clock ( ) - time_req) / CLOCKS_PER_SEC > 96.0) {
      closeConnection ("Connection will restart, for no heartbeat is receiced for 1.6 minutes "
                      "(normal: approx 90 seconds).");
    }
  }
}
using namespace checkerThreadFuncs;

/// @brief Turn UTF-8 string to GBK string. (-finput-charset=utf-8 -fexec-charset=gbk)
/// @param utf8Str The UTF-8 string
/// @return The GBK format string
std::string UTF8ToGBK (const std::string& utf8Str) {
// The program is targeted to a Chinese website, so there are encoding problems
  int utf8Size = MultiByteToWideChar (CP_UTF8, 0, utf8Str.c_str ( ), -1, NULL, 0);
  std::wstring utf16Str;
  utf16Str.resize (utf8Size - 1);
  MultiByteToWideChar (CP_UTF8, 0, utf8Str.c_str ( ), -1, &utf16Str[0], utf8Size);

  int gbkSize = WideCharToMultiByte (CP_ACP, 0, utf16Str.c_str ( ), -1, NULL, 0, NULL, NULL);
  std::string gbkStr;
  gbkStr.resize (gbkSize - 1);
  WideCharToMultiByte (CP_ACP, 0, utf16Str.c_str ( ), -1, &gbkStr[0], gbkSize, NULL, NULL);
  return gbkStr;
}

/// @brief Turn GBK string to UTF-8 string. (-finput-charset=utf-8 -fexec-charset=gbk)
/// @param gbkStr The GBK string
/// @return The UTF-8 format string
std::string GBKToUTF8(const std::string& gbkStr) {
// The program is targeted to a Chinese website, so there are encoding problems
  int utf16Size = MultiByteToWideChar(CP_ACP, 0, gbkStr.c_str(), -1, NULL, 0);
  std::wstring utf16Str;
  utf16Str.resize(utf16Size - 1);
  MultiByteToWideChar(CP_ACP, 0, gbkStr.c_str(), -1, &utf16Str[0], utf16Size);

  int utf8Size = WideCharToMultiByte(CP_UTF8, 0, utf16Str.c_str(), -1, NULL, 0, NULL, NULL);
  std::string utf8Str;
  utf8Str.resize(utf8Size - 1);
  WideCharToMultiByte(CP_UTF8, 0, utf16Str.c_str(), -1, &utf8Str[0], utf8Size, NULL, NULL);
  return utf8Str;
}

/// @brief Use notifier written out of NodeJS to send Win10/11 desktop notifications of Luogu messages.
/// @param Message Content of the message to display
/// @param Sender Name of the sender to display
/// @param SenderUID UID of the sender, for displaying the avatar
void NotifyNodeJS (std::string Message, std::string Sender, std::string SenderUID) {
// Generate the command
  std::string appNameParam = "--appName=\"Luogu Message Notifier\" ";
  std::string titleParam = "--title=\"A message from " + Sender + "\" ";
  std::string contentParam = "--content=\"" + Message + "\" ";
  std::string iconParam = "--icon=\"./cache/" + SenderUID + ".png\" ";
  std::string triggerParam = "--trigger=\"https://www.luogu.com.cn/chat?uid=" + SenderUID + "\" ";
  std::string command = "\"./NotifierNodeJS.exe\" " + appNameParam
                        + titleParam + contentParam + iconParam + triggerParam;
// Silent execute (not creating an extra flashing cmd window)
  STARTUPINFO si;
  PROCESS_INFORMATION pi;
  ZeroMemory (&si, sizeof (si));
  si.cb = sizeof (si);
  ZeroMemory (&pi, sizeof (pi));
  char cmdProcessed[command.length ( ) + 1];
  strcpy (cmdProcessed, command.c_str ( ));
  CreateProcess (NULL, cmdProcessed, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
  CloseHandle (pi.hProcess);
  CloseHandle (pi.hThread);
}

/// @brief Get the avatar of the user
/// @param userid Luogu user uid
void getAvatar (std::string userid) {
  // Read or generate the cache.json
  std::ifstream in = initFile("./cache/cache.json", "{}");
  nlohmann::json infile = nlohmann::json::parse(in);
  in.close( );
  // Check if the avatar exists and is valid. Avatar cached for 1 hour
  if (!infile[userid].is_null( )) {
    int usrID = infile[userid];
    if ((int)time(0) - usrID < 3600) { 
      return;
    }
  }

  // Use HTTP GET method to get the avatar. Luogu APIs
  std::string req = "/upload/usericon/" + userid + ".png";
  if (auto res = cliFiles.Get(req)) {
    if (res->status == 200) {
      std::string outpic = "./cache/" + userid + ".png";
      std::ofstream outfile(outpic.c_str( ), std::ofstream::binary);
      outfile.write(res->body.c_str( ), res->body.size( ));
      outfile.close( );
      // std::cout << " Image downloaded successfully.\n";
      infile[userid] = time(0);
      std::ofstream out("./cache/cache.json");
      out << std::setw(4) << infile << std::endl;
      out.close( );
    } else {
      std::string errInfo = "Failed to get avatar: HTTP STATUS ";
      errInfo += std::to_string((int) res->status);
      MessageBox(NULL, errInfo.c_str(), TEXT("Error"), MB_OK | MB_ICONERROR);
    }
  } else {
    auto err = res.error();
    std::string errInfo = "HTTP error: ";
    errInfo += httplib::to_string(err);
    MessageBox(NULL, errInfo.c_str(), TEXT("Error"), MB_OK | MB_ICONERROR);
  }
}

/// @brief Actions when receiving a ping.
/// @param hdl 
/// @param payload 
/// @return 
bool on_ping(websocketpp::connection_hdl hdl, std::string payload) {
  timePing_req = clock ( );
  std::thread thrPing (pingTimer);
  thrPing.detach ( );
  return true;
}

void on_message(websocketpp::connection_hdl hdl, client::message_ptr msg) {
  std::string message = msg->get_payload( );
  nlohmann::json u = nlohmann::json::parse(message);

  if (u["_ws_type"] == "server_broadcast" && u["message"]["receiver"]["uid"] == uid) {
    // Message from user
    getAvatar (std::to_string((int) u["message"]["sender"]["uid"]));
    std::string sender = u["message"]["sender"]["name"];
    std::string content = u["message"]["content"];
    // Convert format
    sender = UTF8ToGBK (sender);
    content = UTF8ToGBK (content);
    // Desktop notification
    std::thread Notif(bind(NotifyNodeJS, content, sender,
                            std::to_string((int) u["message"]["sender"]["uid"])));
    Notif.detach ( );
    // Write in input box
    drawContentInputBox (sender + " > " + content);
  } else if (u["_ws_type"] == "heartbeat") {
    // Heartbeat
    time_req = clock ( );
    std::thread thrHeartbeat (heartbeatTimer);
    thrHeartbeat.detach ( );
  } else if (u["_ws_type"] == "join_result") {
    // Logging in with correct cookie
    islogon = 1;
    drawContentInputBox ("Login successfully!");
    time_req = clock ( );
    std::thread thrHeartbeat (heartbeatTimer);
    thrHeartbeat.detach ( );
  }
}

signed wsClientMain( ) {
// Main part of the websocket client.
// the former "main" function before the WinMain update
  client::connection_ptr con;
  conptr = &con; // share
  client c;
  std::string uri = "wss://ws.luogu.com.cn/ws";
  std::string tls_init_handlr = "ws.luogu.com.cn";

  // Init cookies -> Luogu APIs

  std::ifstream inCookie("cookie.txt");
  inCookie >> _uid >> __client_id;
  inCookie.close ( );
  int ulen = _uid.length ( );
  uid = 0;
  for (int i = 0; i < ulen; i++) uid = uid * 10 + _uid[i] - '0';
  cookie = "__client_id=" + __client_id + "; _uid=" + _uid;
  cliFiles.set_default_headers ({ { "Cookie", cookie } });
  cliMsg.set_default_headers ({
    { "Cookie", cookie },
    { "referer", "https://www.luogu.com.cn/" }
  });

  // Init join message -> Luogu APIs
  nlohmann::json joinMessage = {
    {"type", "join_channel"},
    {"channel", "chat"},
    {"channel_param", _uid},
    {"exclusive_key", nullptr}
  };
  std::string messageStr = joinMessage.dump ( );

  try {
    // channels
    c.set_access_channels (websocketpp::log::alevel::all);
    c.clear_access_channels (websocketpp::log::alevel::frame_payload);
    c.set_error_channels (websocketpp::log::elevel::all);

    c.init_asio ( );

    // handlers
    c.set_message_handler (bind (&on_message, ::_1, ::_2));
    c.set_ping_handler (bind (&on_ping, ::_1, ::_2));
    c.set_tls_init_handler (bind (&on_tls_init, tls_init_handlr.c_str ( ), ::_1));
    c.set_open_handler ([messageStr, &c](websocketpp::connection_hdl hdl) {
      drawContentInputBox ("Connection opened!");
      c.send(hdl, messageStr, websocketpp::frame::opcode::text);
      timePing_req = clock ( );
      // Connection opened, start timing for join result and ping
      std::thread thrLogin (loginChecker);
      thrLogin.detach ( );
      std::thread thrPing (pingTimer);
      thrPing.detach ( );
    });
    websocketpp::lib::error_code ec;
    con = c.get_connection(uri, ec);
    if (ec) {
      std::string prefConnectionFailed = "Could not create connection because: ";
      std::string reasonConnectionFailed = ec.message( );
      prefConnectionFailed += reasonConnectionFailed;
      drawContentInputBox (prefConnectionFailed.c_str( ));
    }
    con->append_header ("Cookie", cookie);
    c.connect (con);
    c.get_alog ( ).write (websocketpp::log::alevel::app, "Connecting to " + uri);
    c.run ( );
    // After connection terminated
    c.stop ( );
    c.reset ( );
    con.reset ( );
  } catch (websocketpp::exception const& e) {
    std::string prefException = "Caught exception: ";
    std::string msgException = e.what ( );
    prefException += msgException;
    drawContentInputBox (prefException.c_str ( ));
    MessageBox(NULL, e.what( ), TEXT("Error"), MB_OK | MB_ICONERROR);
  }
  return 0;
}

void wsClientProt ( ) {
  bool isConnected = true;
  while (isConnected == true) {
    wsClientMain ( );
    drawContentInputBox("Connection refreshing... cooldown 5s");
    Sleep (5000);
  }
}

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
  WNDCLASSEX wc;
  HWND hwnd;
  MSG msg;
  memset (&wc, 0, sizeof (wc));
  wc.cbSize      = sizeof (WNDCLASSEX);
  wc.lpfnWndProc = WndProc;
  wc.hInstance   = hInstance;
  wc.hCursor     = LoadCursor (NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
  wc.lpszClassName = TEXT ("WindowClass");
  wc.hIcon         = LoadIcon (NULL, IDI_APPLICATION);
  wc.hIconSm       = LoadIcon (NULL, IDI_APPLICATION); 
  if (!RegisterClassEx (&wc)) {
    MessageBox (NULL, TEXT ("Window Registration Failed!"), TEXT ("Error"), MB_ICONEXCLAMATION | MB_OK);
    return 0;
  }
  HWND hwndDT = GetDesktopWindow ( );
  RECT rect;
  GetWindowRect (hwndDT, &rect);
  int dtWidth = rect.right - rect.left;
  int dtHeight = rect.bottom - rect.top;
  hwnd = CreateWindowEx (WS_EX_CLIENTEDGE, wc.lpszClassName, TEXT ("FALCON A Luogu Chat On-desktop Notifier"), WS_VISIBLE | WS_OVERLAPPEDWINDOW, dtWidth * 1 / 8, dtHeight * 1 / 8, dtWidth * 3 / 4, dtHeight * 3 / 4, NULL, NULL, hInstance, NULL);
  if (hwnd == NULL) {
    MessageBox(NULL, TEXT ("Window Creation Failed!"), TEXT ("Error"), MB_ICONEXCLAMATION | MB_OK);
    return 0;
  }
  while (GetMessage (&msg, NULL, 0, 0) > 0) {
    TranslateMessage (&msg); 
    DispatchMessage (&msg);
  }
  return msg.wParam;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT Message, WPARAM wParam, LPARAM lParam) {
  static HWND        hwnd_Button;
  static HWND        hwnd_msg_Editbox;
  static HWND        hwnd_UID_Editbox;
  static HWND        hwnd_ReceivedBox;
  char               buff[4096] = {0};
  char               uid_buff[64] = {0};
  const  int         ID_cmdButton = 1;
  const  int         ID_msg_EditBox = 2;
  const  int         ID_UID_EditBox = 3;
  const  int         ID_ReceivedBox = 4;
  RECT               wRect;
  int                wWidth, wHeight;
#define INPUT_HEIGHT  (wHeight * 0.17 < 108 ? 108 : wHeight * 0.17)
#define BUTTON_HEIGHT (wHeight * 0.13 < 80 ? 80 : wHeight * 0.13)
#define UID_HEIGHT    (wHeight * 0.04 < 28 ? 28 : wHeight * 0.04)
  auto DrawContents = [&] ( ) {
    GetClientRect(hwnd, &wRect);
    wWidth = wRect.right - wRect.left;
    wHeight = wRect.bottom - wRect.top;
    hwnd_Button = CreateWindow (TEXT ("button"), TEXT ("Send!"), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, wWidth * 0.78, wHeight - BUTTON_HEIGHT, wWidth * 0.22, BUTTON_HEIGHT, hwnd, reinterpret_cast <HMENU> (ID_cmdButton), ((LPCREATESTRUCT) lParam)->hInstance, NULL);
    if (!hwnd_Button) MessageBox(NULL, TEXT ("Failed to create button!"), TEXT ("Message"), MB_OK | MB_ICONERROR);
    hwnd_UID_Editbox = CreateWindow (TEXT ("edit"), NULL, WS_CHILD | WS_VISIBLE | WS_BORDER, wWidth * 0.78, wHeight - INPUT_HEIGHT, wWidth * 0.22, UID_HEIGHT, hwnd, reinterpret_cast <HMENU> (ID_UID_EditBox), NULL, NULL);
    if (!hwnd_UID_Editbox) MessageBox(NULL, TEXT ("Failed to create uid input box!"), TEXT ("Message"), MB_OK | MB_ICONERROR);
    hwnd_msg_Editbox = CreateWindow (TEXT ("edit"), NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_VSCROLL | ES_MULTILINE | ES_AUTOVSCROLL, wWidth * 0, wHeight - INPUT_HEIGHT, wWidth * 0.78, INPUT_HEIGHT, hwnd, reinterpret_cast <HMENU> (ID_msg_EditBox), NULL, NULL);
    if (!hwnd_msg_Editbox) MessageBox(NULL, TEXT ("Failed to create message input box!"), TEXT ("Message"), MB_OK | MB_ICONERROR);
    hwnd_ReceivedBox = CreateWindow (TEXT ("edit"), TEXT (""), WS_CHILD | WS_VISIBLE | WS_BORDER | WS_VSCROLL | ES_MULTILINE | ES_AUTOVSCROLL, 1 + wWidth * 0, wHeight * 0 + 1, wWidth * 1 - 1, wHeight - INPUT_HEIGHT - 1, hwnd, reinterpret_cast <HMENU> (ID_ReceivedBox), NULL, NULL);
    if (!hwnd_ReceivedBox) MessageBox(NULL, TEXT ("Failed to create message receive box!"), TEXT ("Message"), MB_OK | MB_ICONERROR);
    hwndInputBox = &hwnd_ReceivedBox; // share
  };
  auto Resize = [&] ( ) {
    GetClientRect(hwnd, &wRect);
    wWidth = wRect.right - wRect.left;
    wHeight = wRect.bottom - wRect.top;
    SetWindowPos (hwnd_Button, NULL, wWidth * 0.78, wHeight - BUTTON_HEIGHT, wWidth * 0.22, BUTTON_HEIGHT, SWP_NOZORDER);
    SetWindowPos (hwnd_msg_Editbox, NULL, wWidth * 0, wHeight - INPUT_HEIGHT, wWidth * 0.78, INPUT_HEIGHT, SWP_NOZORDER);
    SetWindowPos (hwnd_UID_Editbox, NULL, wWidth * 0.78, wHeight - INPUT_HEIGHT, wWidth * 0.22, UID_HEIGHT, SWP_NOZORDER);
    SetWindowPos (hwnd_ReceivedBox, NULL, 1 + wWidth * 0, wHeight * 0 + 1, wWidth * 1 - 1, wHeight - INPUT_HEIGHT - 1, SWP_NOZORDER);
  };

  auto extract_csrf_token = [] (std::string html) {
    std::regex csrf_regex("meta name=\"csrf-token\" content=\"(.*?)\"");
    std::smatch match;
    if (std::regex_search(html, match, csrf_regex)) {
      return (std::string) match[1];
    } else {
      return (std::string) "";
    }
  };

  switch (Message) {
    case WM_COMMAND: {
      switch (LOWORD (wParam)) {
        case 0:
          PostQuitMessage (0); break;
        case ID_cmdButton:
          GetWindowText (hwnd_msg_Editbox, buff, 4096);
          GetWindowText (hwnd_UID_Editbox, uid_buff, 64);
          if (buff[0] && uid_buff[0]) {
            std::string sbuff = buff;
            std::string suidbuff = uid_buff;
            std::string respLnk = "/api/user/search?keyword=" + suidbuff;
            std::string resp = cliMsg.Get (respLnk)->body;
            nlohmann::json respJson = nlohmann::json::parse(resp);
            int usr_uid = respJson["users"][0]["uid"];
            std::string usr_name = UTF8ToGBK((std::string)respJson["users"][0]["name"]);
            std::string csrf_token = extract_csrf_token (cliMsg.Get ("/")->body);
            httplib::Headers addHeaders = { { "x-csrf-token", csrf_token } };
            std::string cont = GBKToUTF8(sbuff);
            nlohmann::json body = { { "user", usr_uid }, { "content", cont } };
            auto res = cliMsg.Post("/api/chat/new", addHeaders, body.dump ( ), "application/json");
            if (res) {
              if (res->status == 200) {
                drawContentInputBox ("Request succeeded in sending message. You have sent to " +
                                      usr_name + ": " + sbuff);
              } else {
                drawContentInputBox ("Request failed in sending message. Status code: " + std::to_string(res->status));
              }
            } else {
              drawContentInputBox ("Request failed in sending message. Error code: " + std::to_string((int)res.error()));
            }
            SetWindowText (hwnd_msg_Editbox, TEXT (""));
            SetWindowText (hwnd_UID_Editbox, TEXT (""));
          } else {
            MessageBox(NULL, TEXT ("uid not filled"), TEXT ("Error"), MB_ICONASTERISK);
          }
          break;
      }
      break;
    }

    case WM_CREATE: {
      HFONT hFont = CreateFont(19, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_OUTLINE_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,DEFAULT_PITCH | FF_DONTCARE, "Microsoft YaHei UI");
      SendMessage(hwnd, WM_SETFONT, (WPARAM)hFont, TRUE);
      DrawContents();
      SendMessage(hwnd_Button, WM_SETFONT, (WPARAM)hFont, TRUE);
      SendMessage(hwnd_msg_Editbox, WM_SETFONT, (WPARAM)hFont, TRUE);
      SendMessage(hwnd_UID_Editbox, WM_SETFONT, (WPARAM)hFont, TRUE);
      SendMessage(hwnd_ReceivedBox, WM_SETFONT, (WPARAM)hFont, TRUE);
      std::thread thrWsClient(wsClientProt);
      thrWsClient.detach ( );
      break;
    }
    case WM_GETMINMAXINFO: {
      MINMAXINFO* lpMinMaxInfo = (MINMAXINFO*)lParam;
      lpMinMaxInfo->ptMinTrackSize.x = 600;
      lpMinMaxInfo->ptMinTrackSize.y = 400;
      break;
    }
    case WM_SIZE: {
      Resize(); break;
    }
    case WM_CLOSE: {
      DestroyWindow(hwnd); break;
    }
    case WM_DESTROY: {
      closeConnection("Connection closed because program shut down"); PostQuitMessage(0); break;
    }
    default: {
      return DefWindowProc(hwnd, Message, wParam, lParam);
    }
  }
  return 0;
}