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 */