From 385e1a01950b977ca6c0000887296ff2d50d85a1 Mon Sep 17 00:00:00 2001 From: Richard Chien Date: Thu, 10 Jan 2019 21:27:24 +0800 Subject: [PATCH] First commit --- api.cpp | 30 +++ api.h | 285 +++++++++++++++++++++++++++++ api_funcs.h | 67 +++++++ app.cpp | 83 +++++++++ app.h | 79 ++++++++ common.h | 12 ++ cqsdk.h | 34 ++++ def.h | 5 + dir.cpp | 42 +++++ dir.h | 9 + dllmain.cpp | 18 ++ enums.h | 79 ++++++++ event.cpp | 219 ++++++++++++++++++++++ event.h | 144 +++++++++++++++ exception.h | 31 ++++ logging.h | 39 ++++ menu.h | 11 ++ message.cpp | 185 +++++++++++++++++++ message.h | 163 +++++++++++++++++ target.h | 37 ++++ types.h | 186 +++++++++++++++++++ utils/base64.cpp | 8 + utils/base64.h | 8 + utils/binpack.h | 70 +++++++ utils/function.h | 20 ++ utils/memory.h | 10 + utils/string.cpp | 129 +++++++++++++ utils/string.h | 39 ++++ utils/vendor/.version | 1 + utils/vendor/cpp-base64/base64.cpp | 112 ++++++++++++ utils/vendor/cpp-base64/base64.h | 14 ++ 31 files changed, 2169 insertions(+) create mode 100644 api.cpp create mode 100644 api.h create mode 100644 api_funcs.h create mode 100644 app.cpp create mode 100644 app.h create mode 100644 common.h create mode 100644 cqsdk.h create mode 100644 def.h create mode 100644 dir.cpp create mode 100644 dir.h create mode 100644 dllmain.cpp create mode 100644 enums.h create mode 100644 event.cpp create mode 100644 event.h create mode 100644 exception.h create mode 100644 logging.h create mode 100644 menu.h create mode 100644 message.cpp create mode 100644 message.h create mode 100644 target.h create mode 100644 types.h create mode 100644 utils/base64.cpp create mode 100644 utils/base64.h create mode 100644 utils/binpack.h create mode 100644 utils/function.h create mode 100644 utils/memory.h create mode 100644 utils/string.cpp create mode 100644 utils/string.h create mode 100644 utils/vendor/.version create mode 100644 utils/vendor/cpp-base64/base64.cpp create mode 100644 utils/vendor/cpp-base64/base64.h diff --git a/api.cpp b/api.cpp new file mode 100644 index 0000000..9befb00 --- /dev/null +++ b/api.cpp @@ -0,0 +1,30 @@ +#include "./api.h" + +using namespace std; + +namespace cq::api { + static vector> api_func_initializers; + + static bool add_func_initializer(const function &initializer) { + api_func_initializers.push_back(initializer); + return true; + } + + void __init() { + const auto dll = GetModuleHandleW(L"CQP.dll"); + for (const auto &initializer : api_func_initializers) { + initializer(dll); + } + } + + namespace raw { +#define FUNC(ReturnType, FuncName, ...) \ + typedef __declspec(dllimport) ReturnType(__stdcall *__CQ_##FuncName##_T)(__VA_ARGS__); \ + __CQ_##FuncName##_T CQ_##FuncName; \ + static bool __dummy_CQ_##FuncName = add_func_initializer([](auto dll) { \ + CQ_##FuncName = reinterpret_cast<__CQ_##FuncName##_T>(GetProcAddress(dll, "CQ_" #FuncName)); \ + }); + +#include "./api_funcs.h" + } // namespace raw +} // namespace cq::api diff --git a/api.h b/api.h new file mode 100644 index 0000000..d4076a5 --- /dev/null +++ b/api.h @@ -0,0 +1,285 @@ +#pragma once + +#include "./common.h" + +#include "./app.h" +#include "./enums.h" +#include "./target.h" +#include "./types.h" +#include "./utils/string.h" + +namespace cq::exception { + struct ApiError : RuntimeError { + int code; + ApiError(const int code) : RuntimeError("failed to call coolq api") { this->code = code; } + + static const auto INVALID_DATA = 100; + static const auto INVALID_TARGET = 101; + }; +} // namespace cq::exception + +namespace cq::api { + /** + * Init all API functions. + * This is internally called in the Initialize exported function. + */ + void __init(); + + /** + * Provide ways to access the raw CoolQ API functions. + */ + namespace raw { +#include "./api_funcs.h" + } + + inline void __throw_if_needed(const int32_t ret) noexcept(false) { + if (ret < 0) { + throw exception::ApiError(ret); + } + } + + inline void __throw_if_needed(const void *const ret_ptr) noexcept(false) { + if (!ret_ptr) { + throw exception::ApiError(exception::ApiError::INVALID_DATA); + } + } + +#pragma region Message + + inline int64_t send_private_msg(const int64_t user_id, const std::string &msg) noexcept(false) { + const auto ret = raw::CQ_sendPrivateMsg(app::auth_code, user_id, utils::string_to_coolq(msg).c_str()); + __throw_if_needed(ret); + return ret; + } + + inline int64_t send_group_msg(const int64_t group_id, const std::string &msg) noexcept(false) { + const auto ret = raw::CQ_sendGroupMsg(app::auth_code, group_id, utils::string_to_coolq(msg).c_str()); + __throw_if_needed(ret); + return ret; + } + + inline int64_t send_discuss_msg(const int64_t discuss_id, const std::string &msg) noexcept(false) { + const auto ret = raw::CQ_sendDiscussMsg(app::auth_code, discuss_id, utils::string_to_coolq(msg).c_str()); + __throw_if_needed(ret); + return ret; + } + + inline void delete_msg(const int64_t msg_id) noexcept(false) { + __throw_if_needed(raw::CQ_deleteMsg(app::auth_code, msg_id)); + } + +#pragma endregion + +#pragma region Send Like + + inline void send_like(const int64_t user_id) noexcept(false) { + __throw_if_needed(raw::CQ_sendLike(app::auth_code, user_id)); + } + + inline void send_like(const int64_t user_id, const int32_t times) noexcept(false) { + __throw_if_needed(raw::CQ_sendLikeV2(app::auth_code, user_id, times)); + } + +#pragma endregion + +#pragma region Group &Discuss Operation + + inline void set_group_kick(const int64_t group_id, const int64_t user_id, + const bool reject_add_request) noexcept(false) { + __throw_if_needed(raw::CQ_setGroupKick(app::auth_code, group_id, user_id, reject_add_request)); + } + + inline void set_group_ban(const int64_t group_id, const int64_t user_id, const int64_t duration) noexcept(false) { + __throw_if_needed(raw::CQ_setGroupBan(app::auth_code, group_id, user_id, duration)); + } + + inline void set_group_anonymous_ban(const int64_t group_id, const std::string &flag, + const int64_t duration) noexcept(false) { + __throw_if_needed( + raw::CQ_setGroupAnonymousBan(app::auth_code, group_id, utils::string_to_coolq(flag).c_str(), duration)); + } + + inline void set_group_whole_ban(const int64_t group_id, const bool enable) noexcept(false) { + __throw_if_needed(raw::CQ_setGroupWholeBan(app::auth_code, group_id, enable)); + } + + inline void set_group_admin(const int64_t group_id, const int64_t user_id, const bool enable) noexcept(false) { + __throw_if_needed(raw::CQ_setGroupAdmin(app::auth_code, group_id, user_id, enable)); + } + + inline void set_group_anonymous(const int64_t group_id, const bool enable) noexcept(false) { + __throw_if_needed(raw::CQ_setGroupAnonymous(app::auth_code, group_id, enable)); + } + + inline void set_group_card(const int64_t group_id, const int64_t user_id, const std::string &card) noexcept(false) { + __throw_if_needed( + raw::CQ_setGroupCard(app::auth_code, group_id, user_id, utils::string_to_coolq(card).c_str())); + } + + inline void set_group_leave(const int64_t group_id, const bool is_dismiss) noexcept(false) { + __throw_if_needed(raw::CQ_setGroupLeave(app::auth_code, group_id, is_dismiss)); + } + + inline void set_group_special_title(const int64_t group_id, const int64_t user_id, const std::string &special_title, + const int64_t duration) noexcept(false) { + __throw_if_needed(raw::CQ_setGroupSpecialTitle( + app::auth_code, group_id, user_id, utils::string_to_coolq(special_title).c_str(), duration)); + } + + inline void set_discuss_leave(const int64_t discuss_id) noexcept(false) { + __throw_if_needed(raw::CQ_setDiscussLeave(app::auth_code, discuss_id)); + } + +#pragma endregion + +#pragma region Request Operation + + inline void set_friend_add_request(const std::string &flag, const request::Operation operation, + const std::string &remark) noexcept(false) { + __throw_if_needed(raw::CQ_setFriendAddRequest( + app::auth_code, utils::string_to_coolq(flag).c_str(), operation, utils::string_to_coolq(remark).c_str())); + } + + inline void set_group_add_request(const std::string &flag, const request::SubType type, + const request::Operation operation) noexcept(false) { + __throw_if_needed( + raw::CQ_setGroupAddRequest(app::auth_code, utils::string_to_coolq(flag).c_str(), type, operation)); + } + + inline void set_group_add_request(const std::string &flag, const request::SubType type, + const request::Operation operation, const std::string &reason) noexcept(false) { + __throw_if_needed(raw::CQ_setGroupAddRequestV2(app::auth_code, + utils::string_to_coolq(flag).c_str(), + type, + operation, + utils::string_to_coolq(reason).c_str())); + } + +#pragma endregion + +#pragma region Get QQ Information + + inline int64_t get_login_user_id() noexcept { return raw::CQ_getLoginQQ(app::auth_code); } + + inline std::string get_login_nickname() noexcept(false) { + const auto ret = raw::CQ_getLoginNick(app::auth_code); + __throw_if_needed(ret); + return utils::string_from_coolq(ret); + } + + inline std::string get_stranger_info_base64(const int64_t user_id, const bool no_cache = false) noexcept(false) { + const auto ret = raw::CQ_getStrangerInfo(app::auth_code, user_id, no_cache); + __throw_if_needed(ret); + return utils::string_from_coolq(ret); + } + + inline std::string get_group_list_base64() noexcept(false) { + const auto ret = raw::CQ_getGroupList(app::auth_code); + __throw_if_needed(ret); + return utils::string_from_coolq(ret); + } + + inline std::string get_group_member_list_base64(const int64_t group_id) noexcept(false) { + const auto ret = raw::CQ_getGroupMemberList(app::auth_code, group_id); + __throw_if_needed(ret); + return utils::string_from_coolq(ret); + } + + inline std::string get_group_member_info_base64(const int64_t group_id, const int64_t user_id, + const bool no_cache = false) noexcept(false) { + const auto ret = raw::CQ_getGroupMemberInfoV2(app::auth_code, group_id, user_id, no_cache); + __throw_if_needed(ret); + return utils::string_from_coolq(ret); + } + +#pragma endregion + +#pragma region Get CoolQ Information + + inline std::string get_cookies() noexcept(false) { + const auto ret = raw::CQ_getCookies(app::auth_code); + __throw_if_needed(ret); + return utils::string_from_coolq(ret); + } + + inline int32_t get_csrf_token() noexcept { return raw::CQ_getCsrfToken(app::auth_code); } + + inline std::string get_app_directory() noexcept(false) { + const auto ret = raw::CQ_getAppDirectory(app::auth_code); + __throw_if_needed(ret); + return utils::string_from_coolq(ret); + } + + inline std::string get_record(const std::string &file, const std::string &out_format) noexcept(false) { + const auto ret = raw::CQ_getRecord( + app::auth_code, utils::string_to_coolq(file).c_str(), utils::string_to_coolq(out_format).c_str()); + __throw_if_needed(ret); + return utils::string_from_coolq(ret); + } + +#pragma endregion + +#pragma region CoolQ Self - operation + + // int32_t set_fatal(const char *error_info) { + // return raw::CQ_setFatal(app::auth_code, error_info); + //} + // + // int32_t set_restart() { + // return raw::CQ_setRestart(app::auth_code); + //} + +#pragma endregion + +#pragma region CQSDK Bonus + + inline int64_t send_msg(const Target &target, const std::string &msg) noexcept(false) { + if (target.group_id.has_value()) { + return send_group_msg(target.group_id.value(), msg); + } + if (target.discuss_id.has_value()) { + return send_discuss_msg(target.discuss_id.value(), msg); + } + if (target.user_id.has_value()) { + return send_private_msg(target.user_id.value(), msg); + } + throw exception::ApiError(exception::ApiError::INVALID_TARGET); + } + + inline User get_stranger_info(const int64_t user_id, const bool no_cache = false) noexcept(false) { + try { + return ObjectHelper::from_base64(get_stranger_info_base64(user_id, no_cache)); + } catch (exception::ParseError &) { + throw exception::ApiError(exception::ApiError::INVALID_DATA); + } + } + + inline std::vector get_group_list() noexcept(false) { + try { + return ObjectHelper::multi_from_base64>(get_group_list_base64()); + } catch (exception::ParseError &) { + throw exception::ApiError(exception::ApiError::INVALID_DATA); + } + } + + inline std::vector get_group_member_list(const int64_t group_id) noexcept(false) { + try { + return ObjectHelper::multi_from_base64>(get_group_member_list_base64(group_id)); + } catch (exception::ParseError &) { + throw exception::ApiError(exception::ApiError::INVALID_DATA); + } + } + + inline GroupMember get_group_member_info(const int64_t group_id, const int64_t user_id, + const bool no_cache = false) noexcept(false) { + try { + return ObjectHelper::from_base64(get_group_member_info_base64(group_id, user_id, no_cache)); + } catch (exception::ParseError &) { + throw exception::ApiError(exception::ApiError::INVALID_DATA); + } + } + + inline User get_login_info() noexcept(false) { return get_stranger_info(get_login_user_id()); } + +#pragma endregion +} // namespace cq::api diff --git a/api_funcs.h b/api_funcs.h new file mode 100644 index 0000000..7444c68 --- /dev/null +++ b/api_funcs.h @@ -0,0 +1,67 @@ +// We don't use "#pragma once" here, because this file is intended to be included twice, +// by sdk_class.h and sdk.cpp, respectively to declare and define SDK functions. +// Except for the two files mentioned above, no file is allowed to include this. + +#ifndef FUNC +#define DEFINED_FUNC_MACRO +#define FUNC(ReturnType, FuncName, ...) \ + typedef ReturnType(__stdcall *__CQ_##FuncName##_T)(__VA_ARGS__); \ + extern __CQ_##FuncName##_T CQ_##FuncName; // only DECLARE the functions +#endif + +#include + +using cq_bool_t = int32_t; + +// Message +FUNC(int32_t, sendPrivateMsg, int32_t auth_code, int64_t qq, const char *msg) +FUNC(int32_t, sendGroupMsg, int32_t auth_code, int64_t group_id, const char *msg) +FUNC(int32_t, sendDiscussMsg, int32_t auth_code, int64_t discuss_id, const char *msg) +FUNC(int32_t, deleteMsg, int32_t auth_code, int64_t msg_id) + +// Send Like +FUNC(int32_t, sendLike, int32_t auth_code, int64_t qq) +FUNC(int32_t, sendLikeV2, int32_t auth_code, int64_t qq, int32_t times) + +// Group & Discuss Operation +FUNC(int32_t, setGroupKick, int32_t auth_code, int64_t group_id, int64_t qq, cq_bool_t reject_add_request) +FUNC(int32_t, setGroupBan, int32_t auth_code, int64_t group_id, int64_t qq, int64_t duration) +FUNC(int32_t, setGroupAnonymousBan, int32_t auth_code, int64_t group_id, const char *anonymous, int64_t duration) +FUNC(int32_t, setGroupWholeBan, int32_t auth_code, int64_t group_id, cq_bool_t enable) +FUNC(int32_t, setGroupAdmin, int32_t auth_code, int64_t group_id, int64_t qq, cq_bool_t set) +FUNC(int32_t, setGroupAnonymous, int32_t auth_code, int64_t group_id, cq_bool_t enable) +FUNC(int32_t, setGroupCard, int32_t auth_code, int64_t group_id, int64_t qq, const char *new_card) +FUNC(int32_t, setGroupLeave, int32_t auth_code, int64_t group_id, cq_bool_t is_dismiss) +FUNC(int32_t, setGroupSpecialTitle, int32_t auth_code, int64_t group_id, int64_t qq, const char *new_special_title, + int64_t duration) +FUNC(int32_t, setDiscussLeave, int32_t auth_code, int64_t discuss_id) + +// Request Operation +FUNC(int32_t, setFriendAddRequest, int32_t auth_code, const char *response_flag, int32_t response_operation, + const char *remark) +FUNC(int32_t, setGroupAddRequest, int32_t auth_code, const char *response_flag, int32_t request_type, + int32_t response_operation) +FUNC(int32_t, setGroupAddRequestV2, int32_t auth_code, const char *response_flag, int32_t request_type, + int32_t response_operation, const char *reason) + +// Get QQ Information +FUNC(int64_t, getLoginQQ, int32_t auth_code) +FUNC(const char *, getLoginNick, int32_t auth_code) +FUNC(const char *, getStrangerInfo, int32_t auth_code, int64_t qq, cq_bool_t no_cache) +FUNC(const char *, getGroupList, int32_t auth_code) +FUNC(const char *, getGroupMemberList, int32_t auth_code, int64_t group_id) +FUNC(const char *, getGroupMemberInfoV2, int32_t auth_code, int64_t group_id, int64_t qq, cq_bool_t no_cache) + +// Get CoolQ Information +FUNC(const char *, getCookies, int32_t auth_code) +FUNC(int32_t, getCsrfToken, int32_t auth_code) +FUNC(const char *, getAppDirectory, int32_t auth_code) +FUNC(const char *, getRecord, int32_t auth_code, const char *file, const char *out_format) + +FUNC(int32_t, addLog, int32_t auth_code, int32_t log_level, const char *category, const char *log_msg) +FUNC(int32_t, setFatal, int32_t auth_code, const char *error_info) +FUNC(int32_t, setRestart, int32_t auth_code) // currently ineffective + +#ifdef DEFINED_FUNC_MACRO +#undef FUNC +#endif diff --git a/app.cpp b/app.cpp new file mode 100644 index 0000000..5f39aba --- /dev/null +++ b/app.cpp @@ -0,0 +1,83 @@ +#include "./app.h" + +#include "./api.h" +#include "./def.h" +#include "./utils/function.h" + +namespace cq { + Config config; + + namespace app { + int32_t auth_code = 0; + std::string id = ""; + + std::function on_initialize; + std::function on_enable; + std::function on_disable; + std::function on_coolq_start; + std::function on_coolq_exit; + + std::function __main; + } // namespace app +} // namespace cq + +using namespace std; +using namespace cq; +using cq::utils::call_if_valid; + +/** + * Return app info. + */ +__CQ_EVENT(const char *, AppInfo, 0) +() { + // CoolQ API version: 9 + static auto info = "9," + app::id; + return info.c_str(); +} + +/** + * Initialize SDK using the auth code given by CoolQ. + */ +__CQ_EVENT(int32_t, Initialize, 4) +(const int32_t auth_code) { + app::auth_code = auth_code; + api::__init(); + call_if_valid(app::on_initialize); + return 0; +} + +/** + * Event: Plugin is enabled. + */ +__CQ_EVENT(int32_t, cq_app_enable, 0) +() { + call_if_valid(app::on_enable); + return 0; +} + +/** + * Event: Plugin is disabled. + */ +__CQ_EVENT(int32_t, cq_app_disable, 0) +() { + call_if_valid(app::on_disable); + return 0; +} + +/** + * Event: CoolQ is starting. + */ +__CQ_EVENT(int32_t, cq_coolq_start, 0) +() { + call_if_valid(app::on_coolq_start); + return 0; +} + +/** + * Event: CoolQ is exiting. + */ +__CQ_EVENT(int32_t, cq_coolq_exit, 0) +() { + call_if_valid(app::on_coolq_exit); + return 0; +} diff --git a/app.h b/app.h new file mode 100644 index 0000000..1b287e6 --- /dev/null +++ b/app.h @@ -0,0 +1,79 @@ +#pragma once + +#include "./common.h" + +namespace cq { + struct Config { + bool convert_unicode_emoji = true; + }; + + extern Config config; + + namespace app { + extern int32_t auth_code; + extern std::string id; + + /** + * Lifecycle: + * + * +-----------------------------------------+ + * | Enabled At Start | + * +-----------------------------------------+ + * | on_initialize | + * | + | + * | | | + * | v | + * | on_coolq_start | + * | + | + * | | | + * | v disabled by user | + * | on_enable +--------------> on_disable | + * | + + | + * | | | | + * | v | | + * | on_coolq_exit <-------------------+ | + * +-----------------------------------------+ + * + * +---------------------------------------+ + * | Disabled At Start | + * +---------------------------------------+ + * | on_initialize +------+ | + * | + |enabled by user | + * | | | | + * | | v | + * | | on_coolq_start | + * | | + | + * | | | | + * | | v | + * | | on_enable | + * | | + | + * | | | | + * | v | | + * | on_coolq_exit <------+ | + * +---------------------------------------+ + */ + extern std::function on_initialize; + extern std::function on_enable; + extern std::function on_disable; + extern std::function on_coolq_start; + extern std::function on_coolq_exit; + + extern std::function __main; + } // namespace app +} // namespace cq + +#define CQ_INITIALIZE(AppId) \ + static bool __cq_set_id() { \ + cq::app::id = AppId; \ + return true; \ + } \ + static bool __cq_set_id_dummy = __cq_set_id() + +#define CQ_MAIN \ + static void __cq_main(); \ + static bool __cq_set_main_function() { \ + cq::app::__main = __cq_main; \ + return true; \ + } \ + static bool __cq_main_dummy = __cq_set_main_function(); \ + static void __cq_main() diff --git a/common.h b/common.h new file mode 100644 index 0000000..df811e8 --- /dev/null +++ b/common.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/cqsdk.h b/cqsdk.h new file mode 100644 index 0000000..399a2d6 --- /dev/null +++ b/cqsdk.h @@ -0,0 +1,34 @@ +#pragma once + +#include "./common.h" + +#include "./api.h" +#include "./app.h" +#include "./dir.h" +#include "./enums.h" +#include "./event.h" +#include "./logging.h" +#include "./menu.h" +#include "./message.h" +#include "./target.h" +#include "./types.h" + +namespace cq { + using event::Event; + using event::MessageEvent; + using event::NoticeEvent; + using event::RequestEvent; + using event::PrivateMessageEvent; + using event::GroupMessageEvent; + using event::DiscussMessageEvent; + using event::GroupUploadEvent; + using event::GroupAdminEvent; + using event::GroupMemberDecreaseEvent; + using event::GroupMemberIncreaseEvent; + using event::FriendAddEvent; + using event::FriendRequestEvent; + using event::GroupRequestEvent; + + using message::Message; + using message::MessageSegment; +} // namespace cq diff --git a/def.h b/def.h new file mode 100644 index 0000000..f4e4573 --- /dev/null +++ b/def.h @@ -0,0 +1,5 @@ +#pragma once + +#define __CQ_EVENT(ReturnType, Name, Size) \ + __pragma(comment(linker, "/EXPORT:" #Name "=_" #Name "@" #Size)) extern "C" __declspec(dllexport) \ + ReturnType __stdcall Name diff --git a/dir.cpp b/dir.cpp new file mode 100644 index 0000000..3aede94 --- /dev/null +++ b/dir.cpp @@ -0,0 +1,42 @@ +#include "./dir.h" + +#include + +#include "./api.h" +#include "./app.h" +#include "./utils/string.h" + +using namespace std; +namespace fs = std::filesystem; + +namespace cq::dir { + static void create_dir_if_not_exists(const string &dir) { + const auto ansi_dir = utils::ansi(dir); + if (!fs::exists(ansi_dir)) { + fs::create_directories(ansi_dir); + } + } + + string root() { + constexpr size_t size = 1024; + wchar_t w_exec_path[size]{}; + GetModuleFileNameW(nullptr, w_exec_path, size); // this will get "C:\\Some\\Path\\CQA\\CQA.exe" + auto exec_path = utils::ws2s(w_exec_path); + return exec_path.substr(0, exec_path.rfind("\\")) + "\\"; + } + + string app(const std::string &sub_dir_name) { + if (sub_dir_name.empty()) { + return api::get_app_directory(); + } + const auto dir = api::get_app_directory() + (sub_dir_name.empty() ? "" : sub_dir_name + "\\"); + create_dir_if_not_exists(dir); + return dir; + } + + std::string app_per_account(const std::string &sub_dir_name) { + const auto dir = app(sub_dir_name) + to_string(api::get_login_user_id()) + "\\"; + create_dir_if_not_exists(dir); + return dir; + } +} // namespace cq::dir diff --git a/dir.h b/dir.h new file mode 100644 index 0000000..69372f6 --- /dev/null +++ b/dir.h @@ -0,0 +1,9 @@ +#pragma once + +#include "./common.h" + +namespace cq::dir { + std::string root(); + std::string app(const std::string &sub_dir_name = ""); + std::string app_per_account(const std::string &sub_dir_name); +} // namespace cq::dir diff --git a/dllmain.cpp b/dllmain.cpp new file mode 100644 index 0000000..ce73f38 --- /dev/null +++ b/dllmain.cpp @@ -0,0 +1,18 @@ +#include "./app.h" + +#pragma unmanaged + +BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { + switch (ul_reason_for_call) { + case DLL_PROCESS_ATTACH: + if (cq::app::__main) { + cq::app::__main(); + } + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + case DLL_PROCESS_DETACH: + default: + break; + } + return TRUE; +} diff --git a/enums.h b/enums.h new file mode 100644 index 0000000..0cbaa0c --- /dev/null +++ b/enums.h @@ -0,0 +1,79 @@ +#pragma once + +#undef IGNORE + +namespace cq { + namespace event { + enum Operation { + IGNORE = 0, + BLOCK = 1, + }; + + enum Type { + UNKNOWN, + MESSAGE, + NOTICE, + REQUEST, + }; + } // namespace event + + namespace message { + enum Type { + UNKNOWN, + PRIVATE, + GROUP, + DISCUSS, + }; + + enum SubType { + SUBTYPE_DEFAULT = 1, + + PRIVATE_FRIEND = 11, + PRIVATE_OTHER = 1, + PRIVATE_GROUP = 2, + PRIVATE_DISCUSS = 3, + }; + } // namespace message + + namespace notice { + enum Type { + UNKNOWN, + GROUP_UPLOAD, + GROUP_ADMIN, + GROUP_MEMBER_DECREASE, + GROUP_MEMBER_INCREASE, + FRIEND_ADD, + }; + + enum SubType { + SUBTYPE_DEFAULT = 1, + + GROUP_ADMIN_UNSET = 1, + GROUP_ADMIN_SET = 2, + GROUP_MEMBER_DECREASE_LEAVE = 1, + GROUP_MEMBER_DECREASE_KICK = 2, + GROUP_MEMBER_INCREASE_APPROVE = 1, + GROUP_MEMBER_INCREASE_INVITE = 2, + }; + } // namespace notice + + namespace request { + enum Operation { + APPROVE = 1, + REJECT = 2, + }; + + enum Type { + UNKNOWN, + FRIEND, + GROUP, + }; + + enum SubType { + SUBTYPE_DEFAULT = 1, + + GROUP_ADD = 1, + GROUP_INVITE = 2, + }; + } // namespace request +} // namespace cq diff --git a/event.cpp b/event.cpp new file mode 100644 index 0000000..719170d --- /dev/null +++ b/event.cpp @@ -0,0 +1,219 @@ +#include "./event.h" + +#include "./def.h" +#include "./exception.h" +#include "./utils/function.h" +#include "./utils/string.h" + +namespace cq::event { + std::function on_private_msg; + std::function on_group_msg; + std::function on_discuss_msg; + std::function on_group_upload; + std::function on_group_admin; + std::function on_group_member_decrease; + std::function on_group_member_increase; + std::function on_friend_add; + std::function on_friend_request; + std::function on_group_request; +} // namespace cq::event + +using namespace std; +using namespace cq; +using cq::utils::call_if_valid; +using cq::utils::string_from_coolq; + +/** + * Type=21 私聊消息 + * sub_type 子类型,11/来自好友 1/来自在线状态 2/来自群 3/来自讨论组 + */ +__CQ_EVENT(int32_t, cq_event_private_msg, 24) +(int32_t sub_type, int32_t msg_id, int64_t from_qq, const char *msg, int32_t font) { + event::PrivateMessageEvent e; + e.target = Target(from_qq); + e.sub_type = static_cast(sub_type); + e.message_id = msg_id; + e.raw_message = string_from_coolq(msg); + e.message = e.raw_message; + e.font = font; + e.user_id = from_qq; + call_if_valid(event::on_private_msg, e); + return e.operation; +} + +/** + * Type=2 群消息 + */ +__CQ_EVENT(int32_t, cq_event_group_msg, 36) +(int32_t sub_type, int32_t msg_id, int64_t from_group, int64_t from_qq, const char *from_anonymous, const char *msg, + int32_t font) { + event::GroupMessageEvent e; + e.target = Target(from_qq, from_group, Target::GROUP); + e.sub_type = static_cast(sub_type); + e.message_id = msg_id; + e.raw_message = string_from_coolq(msg); + // e.message = e.raw_message; // moved to the bottom + e.font = font; + e.user_id = from_qq; + e.group_id = from_group; + try { + e.anonymous = ObjectHelper::from_base64(string_from_coolq(from_anonymous)); + } catch (cq::exception::ParseError &) { + } + + if (e.is_anonymous()) { + // in CoolQ Air, there is a prefix in the message + auto prefix = "[" + e.anonymous.name + "]:"; + if (boost::starts_with(e.raw_message, prefix)) { + e.raw_message = e.raw_message.substr(prefix.length()); + } + } + + e.message = e.raw_message; + + call_if_valid(event::on_group_msg, e); + return e.operation; +} + +/** + * Type=4 讨论组消息 + */ +__CQ_EVENT(int32_t, cq_event_discuss_msg, 32) +(int32_t sub_type, int32_t msg_id, int64_t from_discuss, int64_t from_qq, const char *msg, int32_t font) { + event::DiscussMessageEvent e; + e.target = Target(from_qq, from_discuss, Target::DISCUSS); + e.sub_type = static_cast(sub_type); + e.message_id = msg_id; + e.raw_message = string_from_coolq(msg); + e.message = e.raw_message; + e.font = font; + e.user_id = from_qq; + e.discuss_id = from_discuss; + call_if_valid(event::on_discuss_msg, e); + return e.operation; +} + +/** + * Type=11 群事件-文件上传 + */ +__CQ_EVENT(int32_t, cq_event_group_upload, 28) +(int32_t sub_type, int32_t send_time, int64_t from_group, int64_t from_qq, const char *file) { + event::GroupUploadEvent e; + e.target = Target(from_qq, from_group, Target::GROUP); + e.time = send_time; + e.sub_type = static_cast(sub_type); + try { + e.file = ObjectHelper::from_base64(string_from_coolq(file)); + } catch (cq::exception::ParseError &) { + } + e.user_id = from_qq; + e.group_id = from_group; + call_if_valid(event::on_group_upload, e); + return e.operation; +} + +/** + * Type=101 群事件-管理员变动 + * sub_type 子类型,1/被取消管理员 2/被设置管理员 + */ +__CQ_EVENT(int32_t, cq_event_group_admin, 24) +(int32_t sub_type, int32_t send_time, int64_t from_group, int64_t being_operate_qq) { + event::GroupAdminEvent e; + e.target = Target(being_operate_qq, from_group, Target::GROUP); + e.time = send_time; + e.sub_type = static_cast(sub_type); + e.user_id = being_operate_qq; + e.group_id = from_group; + call_if_valid(event::on_group_admin, e); + return e.operation; +} + +/** + * Type=102 群事件-群成员减少 + * sub_type 子类型,1/群员离开 2/群员被踢 3/自己(即登录号)被踢 + * from_qq 操作者QQ(仅subType为2、3时存在) + * being_operate_qq 被操作QQ + */ +__CQ_EVENT(int32_t, cq_event_group_member_decrease, 32) +(int32_t sub_type, int32_t send_time, int64_t from_group, int64_t from_qq, int64_t being_operate_qq) { + event::GroupMemberDecreaseEvent e; + e.target = Target(being_operate_qq, from_group, Target::GROUP); + e.time = send_time; + e.sub_type = static_cast(sub_type); + e.user_id = being_operate_qq; + e.group_id = from_group; + e.operator_id = e.sub_type == notice::GROUP_MEMBER_DECREASE_LEAVE ? being_operate_qq : from_qq; + call_if_valid(event::on_group_member_decrease, e); + return e.operation; +} + +/** + * Type=103 群事件-群成员增加 + * sub_type 子类型,1/管理员已同意 2/管理员邀请 + * from_qq 操作者QQ(即管理员QQ) + * being_operate_qq 被操作QQ(即加群的QQ) + */ +__CQ_EVENT(int32_t, cq_event_group_member_increase, 32) +(int32_t sub_type, int32_t send_time, int64_t from_group, int64_t from_qq, int64_t being_operate_qq) { + event::GroupMemberIncreaseEvent e; + e.target = Target(being_operate_qq, from_group, Target::GROUP); + e.time = send_time; + e.sub_type = static_cast(sub_type); + e.user_id = being_operate_qq; + e.group_id = from_group; + e.operator_id = from_qq; + call_if_valid(event::on_group_member_increase, e); + return e.operation; +} + +/** + * Type=201 好友事件-好友已添加 + */ +__CQ_EVENT(int32_t, cq_event_friend_add, 16) +(int32_t sub_type, int32_t send_time, int64_t from_qq) { + event::FriendAddEvent e; + e.target = Target(from_qq); + e.time = send_time; + e.sub_type = static_cast(sub_type); + e.user_id = from_qq; + call_if_valid(event::on_friend_add, e); + return e.operation; +} + +/** + * Type=301 请求-好友添加 + * msg 附言 + * response_flag 反馈标识(处理请求用) + */ +__CQ_EVENT(int32_t, cq_event_add_friend_request, 24) +(int32_t sub_type, int32_t send_time, int64_t from_qq, const char *msg, const char *response_flag) { + event::FriendRequestEvent e; + e.target = Target(from_qq); + e.time = send_time; + e.sub_type = static_cast(sub_type); + e.comment = string_from_coolq(msg); + e.flag = string_from_coolq(response_flag); + e.user_id = from_qq; + call_if_valid(event::on_friend_request, e); + return e.operation; +} + +/** + * Type=302 请求-群添加 + * sub_type 子类型,1/他人申请入群 2/自己(即登录号)受邀入群 + * msg 附言 + * response_flag 反馈标识(处理请求用) + */ +__CQ_EVENT(int32_t, cq_event_add_group_request, 32) +(int32_t sub_type, int32_t send_time, int64_t from_group, int64_t from_qq, const char *msg, const char *response_flag) { + event::GroupRequestEvent e; + e.target = Target(from_qq, from_group, Target::GROUP); + e.time = send_time; + e.sub_type = static_cast(sub_type); + e.comment = string_from_coolq(msg); + e.flag = string_from_coolq(response_flag); + e.user_id = from_qq; + e.group_id = from_group; + call_if_valid(event::on_group_request, e); + return e.operation; +} diff --git a/event.h b/event.h new file mode 100644 index 0000000..855ad83 --- /dev/null +++ b/event.h @@ -0,0 +1,144 @@ +#pragma once + +#include "./common.h" + +#include "./enums.h" +#include "./message.h" +#include "./target.h" +#include "./types.h" + +namespace cq::event { + struct Event { + Type type; + Target target; + + mutable Operation operation = IGNORE; + void block() const { operation = BLOCK; } + }; + + struct MessageEvent : Event { + message::Type message_type; + message::SubType sub_type; + int32_t message_id; + std::string raw_message; + message::Message message; + int32_t font; + }; + + struct NoticeEvent : Event { + int32_t time; + notice::Type notice_type; + notice::SubType sub_type; + }; + + struct RequestEvent : Event { + int32_t time; + request::Type request_type; + request::SubType sub_type; + std::string comment; + std::string flag; + }; + + struct UserIdMixin { + int64_t user_id; + }; + + struct GroupIdMixin { + int64_t group_id; + }; + + struct DiscussIdMixin { + int64_t discuss_id; + }; + + struct OperatorIdMixin { + int64_t operator_id; + }; + + struct PrivateMessageEvent final : MessageEvent, UserIdMixin { + PrivateMessageEvent() { + type = MESSAGE; + message_type = message::PRIVATE; + } + }; + + struct GroupMessageEvent final : MessageEvent, UserIdMixin, GroupIdMixin { + GroupMessageEvent() { + type = MESSAGE; + message_type = message::GROUP; + } + + Anonymous anonymous; + + bool is_anonymous() const { return !anonymous.name.empty(); } + }; + + struct DiscussMessageEvent final : MessageEvent, UserIdMixin, DiscussIdMixin { + DiscussMessageEvent() { + type = MESSAGE; + message_type = message::DISCUSS; + } + }; + + struct GroupUploadEvent final : NoticeEvent, UserIdMixin, GroupIdMixin { + GroupUploadEvent() { + type = NOTICE; + notice_type = notice::GROUP_UPLOAD; + } + + File file; + }; + + struct GroupAdminEvent final : NoticeEvent, UserIdMixin, GroupIdMixin { + GroupAdminEvent() { + type = NOTICE; + notice_type = notice::GROUP_ADMIN; + } + }; + + struct GroupMemberDecreaseEvent final : NoticeEvent, UserIdMixin, GroupIdMixin, OperatorIdMixin { + GroupMemberDecreaseEvent() { + type = NOTICE; + notice_type = notice::GROUP_MEMBER_DECREASE; + } + }; + + struct GroupMemberIncreaseEvent final : NoticeEvent, UserIdMixin, GroupIdMixin, OperatorIdMixin { + GroupMemberIncreaseEvent() { + type = NOTICE; + notice_type = notice::GROUP_MEMBER_INCREASE; + } + }; + + struct FriendAddEvent final : NoticeEvent, UserIdMixin { + FriendAddEvent() { + type = NOTICE; + notice_type = notice::FRIEND_ADD; + } + }; + + struct FriendRequestEvent final : RequestEvent, UserIdMixin { + FriendRequestEvent() { + type = REQUEST; + request_type = request::FRIEND; + } + }; + + struct GroupRequestEvent final : RequestEvent, UserIdMixin, GroupIdMixin { + GroupRequestEvent() { + type = REQUEST; + request_type = request::GROUP; + } + }; + + extern std::function on_private_msg; + extern std::function on_group_msg; + extern std::function on_discuss_msg; + extern std::function on_group_upload; + extern std::function on_group_admin; + extern std::function on_group_member_decrease; + extern std::function on_group_member_increase; + extern std::function on_friend_add; + extern std::function on_friend_request; + extern std::function on_group_request; +} // namespace cq::event diff --git a/exception.h b/exception.h new file mode 100644 index 0000000..2613df5 --- /dev/null +++ b/exception.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +namespace cq::exception { + struct Exception : std::exception { + Exception(const char *what_arg) : exception(what_arg) {} + Exception(const std::string &what_arg) : exception(what_arg.c_str()) {} + }; + + /** + * Exception that caused by the user of a module. + */ + struct LogicError : Exception { + using Exception::Exception; + }; + + /** + * Exception that caused by outside user-uncontrollable factors. + */ + struct RuntimeError : Exception { + using Exception::Exception; + }; + + /** + * Exception that occurs when parsing objects from serialized data. + */ + struct ParseError : RuntimeError { + using RuntimeError::RuntimeError; + }; +} // namespace cq::exception diff --git a/logging.h b/logging.h new file mode 100644 index 0000000..3d77e38 --- /dev/null +++ b/logging.h @@ -0,0 +1,39 @@ +#pragma once + +#include "./api.h" + +#undef ERROR + +namespace cq::logging { + enum Level { + DEBUG = 0, + INFO = 10, + INFOSUCCESS = 11, + INFORECV = 12, + INFOSEND = 13, + WARNING = 20, + ERROR = 30, + FATAL = 40, + }; + + inline int32_t log(const Level level, const std::string &tag, const std::string &msg) { + return api::raw::CQ_addLog( + app::auth_code, level, utils::string_to_coolq(tag).c_str(), utils::string_to_coolq(msg).c_str()); + } + + inline void debug(const std::string &tag, const std::string &msg) { log(DEBUG, tag, msg); } + + inline void info(const std::string &tag, const std::string &msg) { log(INFO, tag, msg); } + + inline void info_success(const std::string &tag, const std::string &msg) { log(INFOSUCCESS, tag, msg); } + + inline void info_recv(const std::string &tag, const std::string &msg) { log(INFORECV, tag, msg); } + + inline void info_send(const std::string &tag, const std::string &msg) { log(INFOSEND, tag, msg); } + + inline void warning(const std::string &tag, const std::string &msg) { log(WARNING, tag, msg); } + + inline void error(const std::string &tag, const std::string &msg) { log(ERROR, tag, msg); } + + inline void fatal(const std::string &tag, const std::string &msg) { log(FATAL, tag, msg); } +} // namespace cq::logging diff --git a/menu.h b/menu.h new file mode 100644 index 0000000..e4d057f --- /dev/null +++ b/menu.h @@ -0,0 +1,11 @@ +#pragma once + +#include "./def.h" + +#define CQ_MENU(MenuName) \ + static void __cq_menu_##MenuName(); \ + __CQ_EVENT(int32_t, MenuName, 0)() { \ + __cq_menu_##MenuName(); \ + return 0; \ + } \ + static void __cq_menu_##MenuName() diff --git a/message.cpp b/message.cpp new file mode 100644 index 0000000..c0ec01e --- /dev/null +++ b/message.cpp @@ -0,0 +1,185 @@ +#include "./message.h" + +#include + +#include "./api.h" + +using namespace std; + +namespace cq::message { + string escape(string str, const bool escape_comma) { + boost::replace_all(str, "&", "&"); + boost::replace_all(str, "[", "["); + boost::replace_all(str, "]", "]"); + if (escape_comma) boost::replace_all(str, ",", ","); + return str; + } + + string unescape(string str) { + boost::replace_all(str, ",", ","); + boost::replace_all(str, "[", "["); + boost::replace_all(str, "]", "]"); + boost::replace_all(str, "&", "&"); + return str; + } + + Message::Message(const string &msg_str) { + // implement a DFA manually, because the regex lib of VC++ will throw stack overflow in some cases + + const static auto TEXT = 0; + const static auto FUNCTION_NAME = 1; + const static auto PARAMS = 2; + auto state = TEXT; + const auto end = msg_str.cend(); + stringstream text_s, function_name_s, params_s; + auto curr_cq_start = end; + for (auto it = msg_str.cbegin(); it != end; ++it) { + const auto curr = *it; + switch (state) { + case TEXT: { + text: + if (curr == '[' && end - 1 - it >= 5 /* [CQ:a] at least 5 chars behind */ + && *(it + 1) == 'C' && *(it + 2) == 'Q' && *(it + 3) == ':' && *(it + 4) != ']') { + state = FUNCTION_NAME; + curr_cq_start = it; + it += 3; + } else { + text_s << curr; + } + break; + } + case FUNCTION_NAME: { + if ((curr >= 'A' && curr <= 'Z') || (curr >= 'a' && curr <= 'z') || (curr >= '0' && curr <= '9')) { + function_name_s << curr; + } else if (curr == ',') { + // function name out, params in + state = PARAMS; + } else if (curr == ']') { + // CQ code end, with no params + goto params; + } else { + // unrecognized character + text_s << string(curr_cq_start, it); // mark as text + curr_cq_start = end; + function_name_s = stringstream(); + params_s = stringstream(); + state = TEXT; + // because the current char may be '[', we goto text part + goto text; + } + break; + } + case PARAMS: { + params: + if (curr == ']') { + // CQ code end + MessageSegment seg; + + seg.type = function_name_s.str(); + + vector params; + boost::split(params, params_s.str(), boost::is_any_of(",")); + for (const auto ¶m : params) { + const auto idx = param.find_first_of('='); + if (idx != string::npos) { + seg.data[boost::trim_copy(param.substr(0, idx))] = unescape(param.substr(idx + 1)); + } + } + + if (!text_s.str().empty()) { + // there is a text segment before this CQ code + this->push_back(MessageSegment{"text", {{"text", unescape(text_s.str())}}}); + text_s = stringstream(); + } + + this->push_back(seg); + curr_cq_start = end; + text_s = stringstream(); + function_name_s = stringstream(); + params_s = stringstream(); + state = TEXT; + } else { + params_s << curr; + } + } + default: + break; + } + } + + // iterator end, there may be some rest of message we haven't put into segments + switch (state) { + case FUNCTION_NAME: + case PARAMS: + // we are in CQ code, but it ended with no ']', so it's a text segment + text_s << string(curr_cq_start, end); + // should fall through + case TEXT: + if (!text_s.str().empty()) { + this->push_back(MessageSegment{"text", {{"text", unescape(text_s.str())}}}); + } + default: + break; + } + } + + Message::operator string() const { + stringstream ss; + for (auto seg : *this) { + if (seg.type.empty()) { + continue; + } + if (seg.type == "text") { + if (const auto it = seg.data.find("text"); it != seg.data.end()) { + ss << escape((*it).second, false); + } + } else { + ss << "[CQ:" << seg.type; + for (const auto &item : seg.data) { + ss << "," << item.first << "=" << escape(item.second, true); + } + ss << "]"; + } + } + return ss.str(); + } + + int64_t Message::send(const Target &target) const { return api::send_msg(target, *this); } + + string Message::extract_plain_text() const { + string result; + for (const auto &seg : *this) { + if (seg.type == "text") { + result += seg.data.at("text") + " "; + } + } + if (!result.empty()) { + result.erase(result.end() - 1); // remove the trailing space + } + return result; + } + + void Message::reduce() { + if (this->empty()) { + return; + } + + auto last_seg_it = this->begin(); + for (auto it = this->begin(); ++it != this->end();) { + if (it->type == "text" && last_seg_it->type == "text" && it->data.find("text") != it->data.end() + && last_seg_it->data.find("text") != last_seg_it->data.end()) { + // found adjacent "text" segments + last_seg_it->data["text"] += it->data["text"]; + // remove the current element and continue + this->erase(it); + it = last_seg_it; + } else { + last_seg_it = it; + } + } + + if (this->size() == 1 && this->front().type == "text" && this->extract_plain_text().empty()) { + this->clear(); // the only item is an empty text segment, we should remove it + } + } +} // namespace cq::message diff --git a/message.h b/message.h new file mode 100644 index 0000000..59d072f --- /dev/null +++ b/message.h @@ -0,0 +1,163 @@ +#pragma once + +#include "./common.h" + +#include "./target.h" +#include "./utils/string.h" + +namespace cq::message { + /** + * Escape special characters in the given string. + */ + std::string escape(std::string str, bool escape_comma = true); + + /** + * Unescape special characters in the given string. + */ + std::string unescape(std::string str); + + struct MessageSegment { + std::string type; + std::map data; + + static MessageSegment text(const std::string &text) { return {"text", {{"text", text}}}; } + static MessageSegment emoji(const uint32_t id) { return {"emoji", {{"id", std::to_string(id)}}}; } + static MessageSegment face(const int id) { return {"face", {{"id", std::to_string(id)}}}; } + static MessageSegment image(const std::string &file) { return {"image", {{"file", file}}}; } + + static MessageSegment record(const std::string &file, const bool magic = false) { + return {"record", {{"file", file}, {"magic", std::to_string(magic)}}}; + } + + static MessageSegment at(const int64_t user_id) { return {"at", {{"qq", std::to_string(user_id)}}}; } + static MessageSegment rps() { return {"rps", {}}; } + static MessageSegment dice() { return {"dice", {}}; } + static MessageSegment shake() { return {"shake", {}}; } + + static MessageSegment anonymous(const bool ignore_failure = false) { + return {"anonymous", {{"ignore", std::to_string(ignore_failure)}}}; + } + + static MessageSegment share(const std::string &url, const std::string &title, const std::string &content = "", + const std::string &image_url = "") { + return {"share", {{"url", url}, {"title", title}, {"content", content}, {"image", image_url}}}; + } + + enum class ContactType { USER, GROUP }; + + static MessageSegment contact(const ContactType &type, const int64_t id) { + return { + "contact", + { + {"type", type == ContactType::USER ? "qq" : "group"}, + {"id", std::to_string(id)}, + }, + }; + } + + static MessageSegment location(const double latitude, const double longitude, const std::string &title = "", + const std::string &content = "") { + return { + "location", + { + {"lat", std::to_string(latitude)}, + {"lon", std::to_string(longitude)}, + {"title", title}, + {"content", content}, + }, + }; + } + + static MessageSegment music(const std::string &type, const int64_t &id) { + return {"music", {{"type", type}, {"id", std::to_string(id)}}}; + } + + static MessageSegment music(const std::string &url, const std::string &audio_url, const std::string &title, + const std::string &content = "", const std::string &image_url = "") { + return { + "music", + { + {"type", "custom"}, + {"url", url}, + {"audio", audio_url}, + {"title", title}, + {"content", content}, + {"image", image_url}, + }, + }; + } + }; + + struct Message : std::list { + Message() = default; + + /** + * Split a string to a Message object. + */ + Message(const std::string &msg_str); + + Message(const char *msg_str) : Message(std::string(msg_str)) {} + + Message(const MessageSegment &seg) { this->push_back(seg); } + + /** + * Merge all segments to a string. + */ + operator std::string() const; + + Message &operator+=(const Message &other) { + this->insert(this->end(), other.begin(), other.end()); + this->reduce(); + return *this; + } + + template + Message &operator+=(const T &other) { + return this->operator+=(Message(other)); + } + + Message operator+(const Message &other) const { + auto result = *this; + result += other; // use operator+= + return result; + } + + template + Message operator+(const T &other) const { + return this->operator+(Message(other)); + } + + /** + * Send the message to a given target. + */ + int64_t send(const Target &target) const; + + /** + * Extract and merge plain text segments in the message. + */ + std::string extract_plain_text() const; + + std::list &segments() { return *this; } + const std::list &segments() const { return *this; } + + /** + * Merge adjacent "text" segments. + */ + void reduce(); + }; + + template + Message operator+(const T &lhs, const Message &rhs) { + return Message(lhs) + rhs; + } + + /** + * Send a message to the given target. + * Thanks to the auto type conversion, a Segment object can also be passed in. + */ + inline int64_t send(const Target &target, const Message &msg) { return msg.send(target); } +} // namespace cq::message + +namespace std { + inline string to_string(const cq::message::Message &msg) { return string(msg); } +} // namespace std diff --git a/target.h b/target.h new file mode 100644 index 0000000..b8d28b9 --- /dev/null +++ b/target.h @@ -0,0 +1,37 @@ +#pragma once + +#include "./common.h" + +namespace cq { + struct Target { + std::optional user_id; + std::optional group_id; + std::optional discuss_id; + + enum Type { USER, GROUP, DISCUSS }; + + Target() = default; + + explicit Target(const int64_t user_or_group_or_discuss_id, const Type type = USER) { + switch (type) { + case USER: + user_id = user_or_group_or_discuss_id; + break; + case GROUP: + group_id = user_or_group_or_discuss_id; + break; + case DISCUSS: + discuss_id = user_or_group_or_discuss_id; + } + } + + explicit Target(const int64_t user_id, const int64_t group_or_discuss_id, const Type type) + : Target(group_or_discuss_id, type) { + this->user_id = user_id; + } + + static Target user(const int64_t user_id) { return Target(user_id, USER); } + static Target group(const int64_t group_id) { return Target(group_id, GROUP); } + static Target discuss(const int64_t discuss_id) { return Target(discuss_id, DISCUSS); } + }; +} // namespace cq diff --git a/types.h b/types.h new file mode 100644 index 0000000..6f7c6d1 --- /dev/null +++ b/types.h @@ -0,0 +1,186 @@ +#pragma once + +#include "./common.h" + +#include "./exception.h" +#include "./utils/base64.h" +#include "./utils/binpack.h" + +namespace cq { + struct ObjectHelper { + /** + * Parse an object from a given base64 string. + * This is prefered to "T::from_bytes" because it may have extra behaviors. + */ + template + static T from_base64(const std::string &b64) { + return T::from_bytes(utils::base64::decode(b64)); + } + + /** + * Parse multiple objects from a given base64 string. + * This is prefered to "T::from_bytes" because it may have extra behaviors. + */ + template + static Container multi_from_base64(const std::string &b64) { + Container result; + auto inserter = std::back_inserter(result); + auto pack = utils::BinPack(utils::base64::decode(b64)); + try { + const auto count = pack.pop_int(); + for (auto i = 0; i < count; i++) { + *inserter = Container::value_type::from_bytes(pack.pop_token()); + } + } catch (exception::BytesNotEnough &) { + throw exception::ParseError("failed to parse from bytes to multiple objects"); + } + return result; + } + }; + + enum class Sex { MALE = 0, FEMALE = 1, UNKNOWN = 255 }; + + enum class GroupRole { MEMBER = 1, ADMIN = 2, OWNER = 3 }; + + struct User { + const static size_t MIN_SIZE = 18; + + int64_t user_id = 0; + std::string nickname; + Sex sex = Sex::UNKNOWN; + int32_t age = 0; + + static User from_bytes(const std::string &bytes) { + auto pack = utils::BinPack(bytes); + User stranger; + try { + stranger.user_id = pack.pop_int(); + stranger.nickname = pack.pop_string(); + stranger.sex = static_cast(pack.pop_int()); + stranger.age = pack.pop_int(); + } catch (exception::BytesNotEnough &) { + throw exception::ParseError("failed to parse from bytes to a User object"); + } + return stranger; + } + }; + + struct Group { + const static size_t MIN_SIZE = 10; + + int64_t group_id = 0; + std::string group_name; + + static Group from_bytes(const std::string &bytes) { + auto pack = utils::BinPack(bytes); + Group group; + try { + group.group_id = pack.pop_int(); + group.group_name = pack.pop_string(); + } catch (exception::BytesNotEnough &) { + throw exception::ParseError("failed to parse from bytes to a Group object"); + } + return group; + } + }; + + struct GroupMember : User { + const static size_t MIN_SIZE = 58; + + int64_t group_id = 0; + // int64_t user_id; // from User + // std::string nickname; // from User + std::string card; + // Sex sex = Sex::UNKNOWN; // from User + // int32_t age; // from User + std::string area; + int32_t join_time = 0; + int32_t last_sent_time = 0; + std::string level; + GroupRole role = GroupRole::MEMBER; + bool unfriendly = false; + std::string title; + int32_t title_expire_time = 0; + bool card_changeable = false; + + static GroupMember from_bytes(const std::string &bytes) { + auto pack = utils::BinPack(bytes); + GroupMember member; + try { + member.group_id = pack.pop_int(); + member.user_id = pack.pop_int(); + member.nickname = pack.pop_string(); + member.card = pack.pop_string(); + member.sex = static_cast(pack.pop_int()); + member.age = pack.pop_int(); + member.area = pack.pop_string(); + member.join_time = pack.pop_int(); + member.last_sent_time = pack.pop_int(); + member.level = pack.pop_string(); + member.role = static_cast(pack.pop_int()); + member.unfriendly = pack.pop_bool(); + member.title = pack.pop_string(); + member.title_expire_time = pack.pop_int(); + member.card_changeable = pack.pop_bool(); + } catch (exception::BytesNotEnough &) { + throw exception::ParseError("failed to parse from bytes to a GroupMember object"); + } + return member; + } + }; + + struct Anonymous { + const static size_t MIN_SIZE = 12; + + int64_t id = 0; + std::string name; + std::string token; // binary + std::string flag; // base64 of the whole Anonymous object + + static Anonymous from_bytes(const std::string &bytes) { + auto pack = utils::BinPack(bytes); + Anonymous anonymous; + try { + anonymous.id = pack.pop_int(); + anonymous.name = pack.pop_string(); + anonymous.token = pack.pop_token(); + // NOTE: we don't initialize "flag" here because it represents the + // whole object it will be initialized in the specialized + // ObjectHelper::from_base64 function + } catch (exception::BytesNotEnough &) { + throw exception::ParseError("failed to parse from bytes to an Anonymous object"); + } + return anonymous; + } + }; + + template <> + inline Anonymous ObjectHelper::from_base64(const std::string &b64) { + auto anonymous = Anonymous::from_bytes(utils::base64::decode(b64)); + anonymous.flag = b64; + return anonymous; + } + + struct File { + const static size_t MIN_SIZE = 20; + + std::string id; + std::string name; + int64_t size = 0; + int64_t busid = 0; + + static File from_bytes(const std::string &bytes) { + auto pack = utils::BinPack(bytes); + File file; + try { + file.id = pack.pop_string(); + file.name = pack.pop_string(); + file.size = pack.pop_int(); + file.busid = pack.pop_int(); + } catch (exception::BytesNotEnough &) { + throw exception::ParseError("failed to parse from bytes to a File object"); + } + return file; + } + }; +} // namespace cq diff --git a/utils/base64.cpp b/utils/base64.cpp new file mode 100644 index 0000000..0008612 --- /dev/null +++ b/utils/base64.cpp @@ -0,0 +1,8 @@ +#include "./base64.h" + +#include "./vendor/cpp-base64/base64.h" + +namespace cq::utils::base64 { + std::string encode(const unsigned char *bytes, const unsigned int len) { return base64_encode(bytes, len); } + std::string decode(const std::string &str) { return base64_decode(str); } +} // namespace cq::utils::base64 diff --git a/utils/base64.h b/utils/base64.h new file mode 100644 index 0000000..b174202 --- /dev/null +++ b/utils/base64.h @@ -0,0 +1,8 @@ +#pragma once + +#include "../common.h" + +namespace cq::utils::base64 { + std::string encode(const unsigned char *bytes, unsigned int len); + std::string decode(const std::string &str); +} // namespace cq::utils::base64 diff --git a/utils/binpack.h b/utils/binpack.h new file mode 100644 index 0000000..6568070 --- /dev/null +++ b/utils/binpack.h @@ -0,0 +1,70 @@ +#pragma once + +#include "../common.h" + +#include "../exception.h" +#include "./string.h" + +namespace cq::exception { + struct BytesNotEnough : LogicError { + BytesNotEnough(const size_t have, const size_t needed) + : LogicError("there aren't enough bytes remained (have " + std::to_string(have) + ", but " + + std::to_string(needed) + " are/is needed)") {} + }; +} // namespace cq::exception + +namespace cq::utils { + class BinPack { + public: + BinPack() : bytes_(""), curr_(0) {} + explicit BinPack(const std::string &b) : bytes_(b), curr_(0) {} + + size_t size() const noexcept { return bytes_.size() - curr_; } + bool empty() const noexcept { return size() == 0; } + + template + IntType pop_int() noexcept(false) { + constexpr auto size = sizeof(IntType); + check_enough(size); + + auto s = bytes_.substr(curr_, size); + curr_ += size; + std::reverse(s.begin(), s.end()); + + IntType result; + memcpy(static_cast(&result), s.data(), size); + return result; + } + + std::string pop_string() noexcept(false) { + const auto len = pop_int(); + if (len == 0) { + return std::string(); + } + check_enough(len); + auto result = string_from_coolq(bytes_.substr(curr_, len)); + curr_ += len; + return result; + } + + std::string pop_bytes(const size_t len) noexcept(false) { + auto result = bytes_.substr(curr_, len); + curr_ += len; + return result; + } + + std::string pop_token() noexcept(false) { return pop_bytes(pop_int()); } + + bool pop_bool() noexcept(false) { return static_cast(pop_int()); } + + private: + std::string bytes_; + size_t curr_; + + void check_enough(const size_t needed) const noexcept(false) { + if (size() < needed) { + throw exception::BytesNotEnough(size(), needed); + } + } + }; +} // namespace cq::utils diff --git a/utils/function.h b/utils/function.h new file mode 100644 index 0000000..5bc9c39 --- /dev/null +++ b/utils/function.h @@ -0,0 +1,20 @@ +#pragma once + +#include "../common.h" + +namespace cq::utils { + template + static ReturnType call_if_valid(std::function func, Args &&... args) { + if (func) { + return func(std::forward(args)...); + } + return {}; + } + + template + static void call_if_valid(std::function func, Args &&... args) { + if (func) { + func(std::forward(args)...); + } + } +} // namespace cq::utils diff --git a/utils/memory.h b/utils/memory.h new file mode 100644 index 0000000..a06ed9f --- /dev/null +++ b/utils/memory.h @@ -0,0 +1,10 @@ +#pragma once + +#include "../common.h" + +namespace cq::utils { + template + static std::shared_ptr make_shared_array(const size_t size) { + return std::shared_ptr(new T[size], [](T *p) { delete[] p; }); + } +} // namespace cq::utils diff --git a/utils/string.cpp b/utils/string.cpp new file mode 100644 index 0000000..3726186 --- /dev/null +++ b/utils/string.cpp @@ -0,0 +1,129 @@ +#include "./string.h" + +#include +#include + +#include "../app.h" +#include "./memory.h" + +using namespace std; + +namespace cq::utils { + string sregex_replace(const string &str, const regex &re, const function fmt_func) { + string result; + auto last_end_pos = 0; + for (sregex_iterator it(str.begin(), str.end(), re), end; it != end; ++it) { + result += it->prefix().str() + fmt_func(*it); + last_end_pos = it->position() + it->length(); + } + result += str.substr(last_end_pos); + return result; + } + + static shared_ptr multibyte_to_widechar(const unsigned code_page, const char *multibyte_str) { + const auto len = MultiByteToWideChar(code_page, 0, multibyte_str, -1, nullptr, 0); + auto c_wstr_sptr = make_shared_array(len + 1); + MultiByteToWideChar(code_page, 0, multibyte_str, -1, c_wstr_sptr.get(), len); + return c_wstr_sptr; + } + + static shared_ptr widechar_to_multibyte(const unsigned code_page, const wchar_t *widechar_str) { + const auto len = WideCharToMultiByte(code_page, 0, widechar_str, -1, nullptr, 0, nullptr, nullptr); + auto c_str_sptr = make_shared_array(len + 1); + WideCharToMultiByte(code_page, 0, widechar_str, -1, c_str_sptr.get(), len, nullptr, nullptr); + return c_str_sptr; + } + + string string_encode(const string &s, const Encoding encoding) { + return widechar_to_multibyte(static_cast(encoding), s2ws(s).c_str()).get(); + } + + string string_decode(const string &b, const Encoding encoding) { + return ws2s(wstring(multibyte_to_widechar(static_cast(encoding), b.c_str()).get())); + } + + string string_convert_encoding(const string &text, const string &from_enc, const string &to_enc, + const float capability_factor) { + string result; + + const auto cd = iconv_open(to_enc.c_str(), from_enc.c_str()); + auto in = const_cast(text.data()); + auto in_bytes_left = text.size(); + + if (in_bytes_left == 0) { + return result; + } + + auto out_bytes_left = + static_cast(static_cast(in_bytes_left) * capability_factor); + auto out = new char[out_bytes_left]{0}; + const auto out_begin = out; + + try { + if (static_cast(-1) != iconv(cd, &in, &in_bytes_left, &out, &out_bytes_left)) { + // successfully converted + result = out_begin; + } + } catch (...) { + } + + delete[] out_begin; + iconv_close(cd); + + return result; + } + + string string_encode(const string &s, const string &encoding, const float capability_factor) { + return string_convert_encoding(s, "utf-8", encoding, capability_factor); + } + + string string_decode(const string &b, const string &encoding, const float capability_factor) { + return string_convert_encoding(b, encoding, "utf-8", capability_factor); + } + + string string_to_coolq(const string &str) { + // call CoolQ API + return string_encode(str, "gb18030"); + } + + string string_from_coolq(const string &str) { + // handle CoolQ event or data + auto result = string_decode(str, "gb18030"); + + if (config.convert_unicode_emoji) { + result = sregex_replace(result, regex(R"(\[CQ:emoji,\s*id=(\d+)\])"), [](const smatch &m) { + const auto codepoint_str = m.str(1); + u32string u32_str; + + if (boost::starts_with(codepoint_str, "100000")) { + // keycap # to keycap 9 + const auto codepoint = static_cast(stoul(codepoint_str.substr(strlen("100000")))); + u32_str.append({codepoint, 0xFE0F, 0x20E3}); + } else { + const auto codepoint = static_cast(stoul(codepoint_str)); + u32_str.append({codepoint}); + } + + const auto p = reinterpret_cast(u32_str.data()); + wstring_convert, uint32_t> conv; + return conv.to_bytes(p, p + u32_str.size()); + }); + + // CoolQ sometimes use "#\uFE0F" to represent "#\uFE0F\u20E3" + // we should convert them into correct emoji codepoints here + // \uFE0F == \xef\xb8\x8f + // \u20E3 == \xe2\x83\xa3 + result = sregex_replace(result, regex("([#*0-9]\xef\xb8\x8f)(\xe2\x83\xa3)?"), [](const smatch &m) { + return m.str(1) + "\xe2\x83\xa3"; + }); + } + + return result; + } + + string ws2s(const wstring &ws) { return wstring_convert, wchar_t>().to_bytes(ws); } + + wstring s2ws(const string &s) { return wstring_convert, wchar_t>().from_bytes(s); } + + string ansi(const string &s) { return string_encode(s, Encoding::ANSI); } +} // namespace cq::utils diff --git a/utils/string.h b/utils/string.h new file mode 100644 index 0000000..d7c1d37 --- /dev/null +++ b/utils/string.h @@ -0,0 +1,39 @@ +#pragma once + +#include "../common.h" + +#include + +namespace cq::utils { + std::string sregex_replace(const std::string &str, const std::regex &re, + std::function fmt_func); + + enum class Encoding : unsigned { + // https://msdn.microsoft.com/en-us/library/windows/desktop/dd317756.aspx + + ANSI = 0, + UTF8 = 65001, + GB2312 = 936, + GB18030 = 54936, + }; + + std::string string_encode(const std::string &s, Encoding encoding); + std::string string_decode(const std::string &b, Encoding encoding); + + std::string string_convert_encoding(const std::string &text, const std::string &from_enc, const std::string &to_enc, + float capability_factor); + std::string string_encode(const std::string &s, const std::string &encoding, float capability_factor = 2.0f); + std::string string_decode(const std::string &b, const std::string &encoding, float capability_factor = 2.0f); + + std::string string_to_coolq(const std::string &str); + std::string string_from_coolq(const std::string &str); + + std::string ws2s(const std::wstring &ws); + std::wstring s2ws(const std::string &s); + std::string ansi(const std::string &s); +} // namespace cq::utils + +namespace std { + inline string to_string(const string &val) { return val; } + inline string to_string(const bool val) { return val ? "true" : "false"; } +} // namespace std diff --git a/utils/vendor/.version b/utils/vendor/.version new file mode 100644 index 0000000..f1b9db1 --- /dev/null +++ b/utils/vendor/.version @@ -0,0 +1 @@ +ReneNyffenegger/cpp-base64: v1.01.00 \ No newline at end of file diff --git a/utils/vendor/cpp-base64/base64.cpp b/utils/vendor/cpp-base64/base64.cpp new file mode 100644 index 0000000..ced8278 --- /dev/null +++ b/utils/vendor/cpp-base64/base64.cpp @@ -0,0 +1,112 @@ +/* + base64.cpp and base64.h + base64 encoding and decoding with C++. + Version: 1.01.00 + Copyright (C) 2004-2017 René Nyffenegger + This source code is provided 'as-is', without any express or implied + warranty. In no event will the author be held liable for any damages + arising from the use of this software. + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + 1. The origin of this source code must not be misrepresented; you must not + claim that you wrote the original source code. If you use this source code + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original source code. + 3. This notice may not be removed or altered from any source distribution. + René Nyffenegger rene.nyffenegger@adp-gmbh.ch +*/ + +#include "base64.h" +#include + +static const std::string base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + + +static inline bool is_base64(unsigned char c) { + return (isalnum(c) || (c == '+') || (c == '/')); +} + +std::string base64_encode(unsigned char const* bytes_to_encode, unsigned int in_len) { + std::string ret; + int i = 0; + int j = 0; + unsigned char char_array_3[3]; + unsigned char char_array_4[4]; + + while (in_len--) { + char_array_3[i++] = *(bytes_to_encode++); + if (i == 3) { + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for(i = 0; (i <4) ; i++) + ret += base64_chars[char_array_4[i]]; + i = 0; + } + } + + if (i) + { + for(j = i; j < 3; j++) + char_array_3[j] = '\0'; + + char_array_4[0] = ( char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + + for (j = 0; (j < i + 1); j++) + ret += base64_chars[char_array_4[j]]; + + while((i++ < 3)) + ret += '='; + + } + + return ret; + +} + +std::string base64_decode(std::string const& encoded_string) { + int in_len = encoded_string.size(); + int i = 0; + int j = 0; + int in_ = 0; + unsigned char char_array_4[4], char_array_3[3]; + std::string ret; + + while (in_len-- && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) { + char_array_4[i++] = encoded_string[in_]; in_++; + if (i ==4) { + for (i = 0; i <4; i++) + char_array_4[i] = base64_chars.find(char_array_4[i]); + + char_array_3[0] = ( char_array_4[0] << 2 ) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (i = 0; (i < 3); i++) + ret += char_array_3[i]; + i = 0; + } + } + + if (i) { + for (j = 0; j < i; j++) + char_array_4[j] = base64_chars.find(char_array_4[j]); + + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + + for (j = 0; (j < i - 1); j++) ret += char_array_3[j]; + } + + return ret; +} diff --git a/utils/vendor/cpp-base64/base64.h b/utils/vendor/cpp-base64/base64.h new file mode 100644 index 0000000..dd1134c --- /dev/null +++ b/utils/vendor/cpp-base64/base64.h @@ -0,0 +1,14 @@ +// +// base64 encoding and decoding with C++. +// Version: 1.01.00 +// + +#ifndef BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A +#define BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A + +#include + +std::string base64_encode(unsigned char const* , unsigned int len); +std::string base64_decode(std::string const& s); + +#endif /* BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A */