From 78a59ed94fee83f3d101d92f86a737e50d1a8c65 Mon Sep 17 00:00:00 2001 From: dynilath Date: Thu, 30 Apr 2020 11:15:44 +0800 Subject: [PATCH 1/9] Give ApiErrors different messages and types. --- src/core/api.hpp | 50 +++++++++++++++++++++++++++++++++++++++++++- src/std_mode/api.cpp | 16 +++++++------- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/core/api.hpp b/src/core/api.hpp index e453327..6148341 100644 --- a/src/core/api.hpp +++ b/src/core/api.hpp @@ -13,13 +13,61 @@ namespace cq { : RuntimeError("failed to call coolq api, error code: " + to_string(code)), code(code) { } + ApiError(const char *what, const int code) : RuntimeError(what), code(code) { + } + const int code; // 错误码 static const auto INVALID_DATA = 100; // 酷Q返回的数据无效 static const auto INVALID_TARGET = 101; // 发送目标无效 static const auto INVALID_ARGS = 102; // 参数无效 + static const auto LOG_DISABLED = -5; // 日志功能未启用 + static const auto LOG_PRIORITY_ERR = -6; // 日志优先级错误 + static const auto DATABASE_ERR = -7; // 数据入库失败 + static const auto APP_DISABLED = -997; // 应用未启用,请在应用窗中启用应用 + static const auto UNAUTHORIZED = -998; // 应用调用在 auth 声明之外的 Api,见日志警告。在 app.json + // 中添加相应的 auth,授予应用该 Api 的调用权限。 + static const auto UNKOWN_ERR = -1000; // 发生未知错误,由于系统限制,实际错误代码未能传递。 + + inline static void InvokeError(int code); }; +#define ApiErrorImpl(_ERR_CLASS_NAME, _ERR_WHAT, _ERR_ID) \ + struct _ERR_CLASS_NAME : ApiError { \ + _ERR_CLASS_NAME() : ApiError(_ERR_WHAT, _ERR_ID) { \ + } \ + } + + ApiErrorImpl(ApiErrorInvalidData, "Failed to call coolq api (INVALID_DATA).", INVALID_DATA); + ApiErrorImpl(ApiErrorInvalidTarget, "Message sent to invalid target.", INVALID_TARGET); + ApiErrorImpl(ApiErrorInvalidArgs, "Arguments is not valid.", INVALID_ARGS); + ApiErrorImpl(ApiErrorLogDisabled, "Log is disabled.", LOG_DISABLED); + ApiErrorImpl(ApiErrorLogPriority, "Log priority error.", LOG_PRIORITY_ERR); + + ApiErrorImpl(ApiErrorDatabaseErr, "Database error.", DATABASE_ERR); + ApiErrorImpl(ApiErrorAppDisabled, "App is disabled.", APP_DISABLED); + ApiErrorImpl(ApiErrorUnauthorized, "App is not authorized.", UNAUTHORIZED); + ApiErrorImpl(ApiErrorUnkownErr, "An unexpeted error has occurred.", UNKOWN_ERR); + +#undef ApiErrorImpl + + inline void ApiError::InvokeError(int code) { + switch (code) { + case LOG_DISABLED: + throw ApiErrorLogDisabled(); + case LOG_PRIORITY_ERR: + throw ApiErrorLogPriority(); + case DATABASE_ERR: + throw ApiErrorDatabaseErr(); + case APP_DISABLED: + throw ApiErrorAppDisabled(); + case UNAUTHORIZED: + throw ApiErrorUnauthorized(); + case UNKOWN_ERR: + throw ApiErrorUnkownErr(); + } + } + void _init_api(); // 发送私聊消息 @@ -49,7 +97,7 @@ namespace cq { if (target.user_id.has_value()) { return send_private_message(target.user_id.value(), message); } - throw ApiError(ApiError::INVALID_TARGET); + throw ApiErrorInvalidTarget(); } // 撤回消息(可撤回自己 2 分钟内发的消息和比自己更低权限的群成员发的消息) diff --git a/src/std_mode/api.cpp b/src/std_mode/api.cpp index 12a6c1a..89d714f 100644 --- a/src/std_mode/api.cpp +++ b/src/std_mode/api.cpp @@ -35,7 +35,7 @@ namespace cq { template > * = nullptr> inline decltype(auto) chk(T &&res) noexcept(false) { if (res < 0) { - throw ApiError(static_cast(res)); + ApiError::InvokeError(static_cast(res)); } return std::forward(res); } @@ -43,7 +43,7 @@ namespace cq { template > * = nullptr> inline decltype(auto) chk(T &&res_ptr) noexcept(false) { if (!res_ptr) { - throw ApiError(ApiError::INVALID_DATA); + throw ApiErrorInvalidData(); } return std::forward(res_ptr); } @@ -154,7 +154,7 @@ namespace cq { try { return ObjectHelper::from_base64(chk(raw::CQ_getStrangerInfo(_ac(), user_id, no_cache))); } catch (ParseError &) { - throw ApiError(ApiError::INVALID_DATA); + throw ApiErrorInvalidData(); } } @@ -162,7 +162,7 @@ namespace cq { try { return ObjectHelper::multi_from_base64>(chk(raw::CQ_getFriendList(_ac(), false))); } catch (ParseError &) { - throw ApiError(ApiError::INVALID_DATA); + throw ApiErrorInvalidData(); } } @@ -170,7 +170,7 @@ namespace cq { try { return ObjectHelper::multi_from_base64>(chk(raw::CQ_getGroupList(_ac()))); } catch (ParseError &) { - throw ApiError(ApiError::INVALID_DATA); + throw ApiErrorInvalidData(); } } @@ -178,7 +178,7 @@ namespace cq { try { return ObjectHelper::from_base64(chk(raw::CQ_getGroupInfo(_ac(), group_id, no_cache))); } catch (ParseError &) { - throw ApiError(ApiError::INVALID_DATA); + throw ApiErrorInvalidData(); } } @@ -187,7 +187,7 @@ namespace cq { return ObjectHelper::multi_from_base64>( chk(raw::CQ_getGroupMemberList(_ac(), group_id))); } catch (ParseError &) { - throw ApiError(ApiError::INVALID_DATA); + throw ApiErrorInvalidData(); } } @@ -196,7 +196,7 @@ namespace cq { return ObjectHelper::from_base64( chk(raw::CQ_getGroupMemberInfoV2(_ac(), group_id, user_id, no_cache))); } catch (ParseError &) { - throw ApiError(ApiError::INVALID_DATA); + throw ApiErrorInvalidData(); } } From 01d9a9ed0787cc2a05cfe89c1d27d9d925deecee Mon Sep 17 00:00:00 2001 From: dynilath Date: Thu, 30 Apr 2020 11:48:53 +0800 Subject: [PATCH 2/9] Use enum to mark MessageSegment type --- src/core/message.hpp | 86 +++++++++++++++++++++--------- src/core/message_segment_types.inc | 16 ++++++ 2 files changed, 76 insertions(+), 26 deletions(-) create mode 100644 src/core/message_segment_types.inc diff --git a/src/core/message.hpp b/src/core/message.hpp index f4027a3..12e5cac 100644 --- a/src/core/message.hpp +++ b/src/core/message.hpp @@ -31,83 +31,104 @@ namespace cq::message { // 消息段 (即 CQ 码) struct MessageSegment { - std::string type; // 消息段类型 (即 CQ 码的功能名) + enum class SegTypes { +#define MSG_SEG(val) val, +#include "./message_segment_types.inc" +#undef MSG_SEG + none + }; + + static constexpr char *const SegTypesName[] = { +#define MSG_SEG(val) #val, +#include "./message_segment_types.inc" +#undef MSG_SEG + ""}; + + SegTypes type = SegTypes::none; // 消息段类型 (即 CQ 码的功能名) std::map data; // 消息段数据 (即 CQ 码参数), 字符串全部使用未经 CQ 码转义的原始文本 + std::string SegTypeName() const { + return SegTypesName[static_cast(this->type)]; + } + // 转换为字符串形式 operator std::string() const { std::string s; - if (this->type.empty()) { + switch (this->type) { + case SegTypes::none: { return s; } - if (this->type == "text") { + case SegTypes::text: { if (const auto it = this->data.find("text"); it != this->data.end()) { s += escape((*it).second, false); } - } else { - s += "[CQ:" + this->type; + return s; + } + default: { + s += "[CQ:" + this->SegTypeName(); for (const auto &item : this->data) { s += "," + item.first + "=" + escape(item.second, true); } s += "]"; + return s; + } } - return s; } // 纯文本 static MessageSegment text(const std::string &text) { - return {"text", {{"text", text}}}; + return {SegTypes::text, {{"text", text}}}; } // Emoji 表情 static MessageSegment emoji(const uint32_t id) { - return {"emoji", {{"id", to_string(id)}}}; + return {SegTypes::emoji, {{"id", to_string(id)}}}; } // QQ 表情 static MessageSegment face(const int id) { - return {"face", {{"id", to_string(id)}}}; + return {SegTypes::face, {{"id", to_string(id)}}}; } // 图片 static MessageSegment image(const std::string &file) { - return {"image", {{"file", file}}}; + return {SegTypes::image, {{"file", file}}}; } // 语音 static MessageSegment record(const std::string &file, const bool magic = false) { - return {"record", {{"file", file}, {"magic", to_string(magic)}}}; + return {SegTypes::record, {{"file", file}, {"magic", to_string(magic)}}}; } // @某人 static MessageSegment at(const int64_t user_id) { - return {"at", {{"qq", to_string(user_id)}}}; + return {SegTypes::at, {{"qq", to_string(user_id)}}}; } // 猜拳魔法表情 static MessageSegment rps() { - return {"rps", {}}; + return {SegTypes::rps, {}}; } // 掷骰子魔法表情 static MessageSegment dice() { - return {"dice", {}}; + return {SegTypes::dice, {}}; } // 戳一戳 static MessageSegment shake() { - return {"shake", {}}; + return {SegTypes::shake, {}}; } // 匿名发消息 static MessageSegment anonymous(const bool ignore_failure = false) { - return {"anonymous", {{"ignore", to_string(ignore_failure)}}}; + return {SegTypes::anonymous, {{"ignore", 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}}}; + return {SegTypes::share, {{"url", url}, {"title", title}, {"content", content}, {"image", image_url}}}; } enum class ContactType { USER, GROUP }; @@ -115,7 +136,7 @@ namespace cq::message { // 推荐好友, 推荐群 static MessageSegment contact(const ContactType &type, const int64_t id) { return { - "contact", + SegTypes::contact, { {"type", type == ContactType::USER ? "qq" : "group"}, {"id", to_string(id)}, @@ -127,7 +148,7 @@ namespace cq::message { static MessageSegment location(const double latitude, const double longitude, const std::string &title = "", const std::string &content = "") { return { - "location", + SegTypes::location, { {"lat", to_string(latitude)}, {"lon", to_string(longitude)}, @@ -139,19 +160,19 @@ namespace cq::message { // 音乐 static MessageSegment music(const std::string &type, const int64_t id) { - return {"music", {{"type", type}, {"id", to_string(id)}}}; + return {SegTypes::music, {{"type", type}, {"id", to_string(id)}}}; } // 音乐 static MessageSegment music(const std::string &type, const int64_t id, const int32_t style) { - return {"music", {{"type", type}, {"id", to_string(id)}, {"style", to_string(style)}}}; + return {SegTypes::music, {{"type", type}, {"id", to_string(id)}, {"style", to_string(style)}}}; } // 音乐自定义分享 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", + SegTypes::music, { {"type", "custom"}, {"url", url}, @@ -164,6 +185,12 @@ namespace cq::message { } }; + const ::std::unordered_map<::std::string, MessageSegment::SegTypes> SegTypeName2SegTypes = { +#define MSG_SEG(val) {#val, MessageSegment::SegTypes::val}, +#include "./message_segment_types.inc" +#undef MSG_SEG + {"", MessageSegment::SegTypes::none}}; + struct Message : std::list { using std::list::list; @@ -215,7 +242,12 @@ namespace cq::message { eq_pos != std::string::npos ? std::string(param.begin() + eq_pos + 1, param.end()) : ""); param.clear(); } - this->push_back(MessageSegment{std::move(type), std::move(data)}); + auto iter = SegTypeName2SegTypes.find(type); + if (iter != SegTypeName2SegTypes.end()) { + this->push_back(MessageSegment{iter->second, std::move(data)}); + } else { + this->push_back(MessageSegment{MessageSegment::SegTypes::none, std::move(data)}); + } cq_code.clear(); temp_text.clear(); }; @@ -287,7 +319,7 @@ namespace cq::message { std::string extract_plain_text() const { std::string result; for (const auto &seg : *this) { - if (seg.type == "text") { + if (seg.type == MessageSegment::SegTypes::text) { result += seg.data.at("text") + " "; } } @@ -315,7 +347,8 @@ namespace cq::message { 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() + if (it->type == MessageSegment::SegTypes::text && last_seg_it->type == MessageSegment::SegTypes::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"]; @@ -327,7 +360,8 @@ namespace cq::message { } } - if (this->size() == 1 && this->front().type == "text" && this->extract_plain_text().empty()) { + if (this->size() == 1 && this->front().type == MessageSegment::SegTypes::text + && this->extract_plain_text().empty()) { this->clear(); // the only item is an empty text segment, we should remove it } } diff --git a/src/core/message_segment_types.inc b/src/core/message_segment_types.inc new file mode 100644 index 0000000..79c9b48 --- /dev/null +++ b/src/core/message_segment_types.inc @@ -0,0 +1,16 @@ +MSG_SEG(text) +MSG_SEG(emoji) +MSG_SEG(face) +MSG_SEG(image) +MSG_SEG(record) +MSG_SEG(at) +MSG_SEG(rps) +MSG_SEG(dice) +MSG_SEG(shake) +MSG_SEG(anonymous) +MSG_SEG(share) +MSG_SEG(contact) +MSG_SEG(location) +MSG_SEG(music) +MSG_SEG(bface) +MSG_SEG(rich) \ No newline at end of file From 6b79f1102795d2edde32c507ff213dc77d5e7f12 Mon Sep 17 00:00:00 2001 From: dynilath Date: Thu, 30 Apr 2020 16:01:26 +0800 Subject: [PATCH 3/9] INVALID_TARGET should be -23 and thrown from certain retcode --- src/core/api.hpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/api.hpp b/src/core/api.hpp index 6148341..6a4c337 100644 --- a/src/core/api.hpp +++ b/src/core/api.hpp @@ -19,11 +19,12 @@ namespace cq { const int code; // 错误码 static const auto INVALID_DATA = 100; // 酷Q返回的数据无效 - static const auto INVALID_TARGET = 101; // 发送目标无效 + // static const auto INVALID_TARGET = 101; // 发送目标无效 static const auto INVALID_ARGS = 102; // 参数无效 static const auto LOG_DISABLED = -5; // 日志功能未启用 static const auto LOG_PRIORITY_ERR = -6; // 日志优先级错误 static const auto DATABASE_ERR = -7; // 数据入库失败 + static const auto INVALID_TARGET = -23; // 找不到与目标的关系,消息无法发送 static const auto APP_DISABLED = -997; // 应用未启用,请在应用窗中启用应用 static const auto UNAUTHORIZED = -998; // 应用调用在 auth 声明之外的 Api,见日志警告。在 app.json // 中添加相应的 auth,授予应用该 Api 的调用权限。 @@ -61,6 +62,8 @@ namespace cq { throw ApiErrorDatabaseErr(); case APP_DISABLED: throw ApiErrorAppDisabled(); + case INVALID_TARGET: + throw ApiErrorInvalidTarget(); case UNAUTHORIZED: throw ApiErrorUnauthorized(); case UNKOWN_ERR: From 29863f32db29f203c5af44e747565052905cb9f2 Mon Sep 17 00:00:00 2001 From: dynilath Date: Thu, 30 Apr 2020 16:06:07 +0800 Subject: [PATCH 4/9] unimpl (former none) represents CQ code that is not directly supported --- src/core/message.hpp | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/core/message.hpp b/src/core/message.hpp index 12e5cac..babecbf 100644 --- a/src/core/message.hpp +++ b/src/core/message.hpp @@ -35,7 +35,7 @@ namespace cq::message { #define MSG_SEG(val) val, #include "./message_segment_types.inc" #undef MSG_SEG - none + unimpl }; static constexpr char *const SegTypesName[] = { @@ -44,7 +44,7 @@ namespace cq::message { #undef MSG_SEG ""}; - SegTypes type = SegTypes::none; // 消息段类型 (即 CQ 码的功能名) + SegTypes type = SegTypes::unimpl; // 消息段类型 (即 CQ 码的功能名) std::map data; // 消息段数据 (即 CQ 码参数), 字符串全部使用未经 CQ 码转义的原始文本 std::string SegTypeName() const { @@ -55,7 +55,11 @@ namespace cq::message { operator std::string() const { std::string s; switch (this->type) { - case SegTypes::none: { + case SegTypes::unimpl: { + if (!this->data.empty()) { + auto iter = this->data.begin(); + s += "[CQ:" + iter->first + "," + iter->second + "]"; + } return s; } case SegTypes::text: { @@ -189,7 +193,7 @@ namespace cq::message { #define MSG_SEG(val) {#val, MessageSegment::SegTypes::val}, #include "./message_segment_types.inc" #undef MSG_SEG - {"", MessageSegment::SegTypes::none}}; + {"", MessageSegment::SegTypes::unimpl}}; struct Message : std::list { using std::list::list; @@ -232,21 +236,23 @@ namespace cq::message { std::string type, param; std::map data; getline(iss, type, ','); // 读取功能名 - while (iss) { - getline(iss, param, ','); // 读取一个参数 - string_trim(param); - if (param.empty()) continue; - const auto eq_pos = param.find('='); - data.emplace( - std::string(param.begin(), param.begin() + eq_pos), - eq_pos != std::string::npos ? std::string(param.begin() + eq_pos + 1, param.end()) : ""); - param.clear(); - } - auto iter = SegTypeName2SegTypes.find(type); - if (iter != SegTypeName2SegTypes.end()) { - this->push_back(MessageSegment{iter->second, std::move(data)}); + auto segtype_iter = SegTypeName2SegTypes.find(type); + if (segtype_iter == SegTypeName2SegTypes.end()) { + getline(iss, param, ']'); + data.emplace(std::string(type), std::string(param)); + this->push_back(MessageSegment{MessageSegment::SegTypes::unimpl, std::move(data)}); } else { - this->push_back(MessageSegment{MessageSegment::SegTypes::none, std::move(data)}); + while (iss) { + getline(iss, param, ','); // 读取一个参数 + string_trim(param); + if (param.empty()) continue; + const auto eq_pos = param.find('='); + data.emplace( + std::string(param.begin(), param.begin() + eq_pos), + eq_pos != std::string::npos ? std::string(param.begin() + eq_pos + 1, param.end()) : ""); + param.clear(); + } + this->push_back(MessageSegment{segtype_iter->second, std::move(data)}); } cq_code.clear(); temp_text.clear(); From 2091f249ffd020d4fe808865312f531946d4f2d8 Mon Sep 17 00:00:00 2001 From: dynilath Date: Fri, 1 May 2020 23:43:56 +0800 Subject: [PATCH 5/9] Remove CQCode types not directly supported. --- src/core/message_segment_types.inc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/core/message_segment_types.inc b/src/core/message_segment_types.inc index 79c9b48..6eccd4c 100644 --- a/src/core/message_segment_types.inc +++ b/src/core/message_segment_types.inc @@ -11,6 +11,4 @@ MSG_SEG(anonymous) MSG_SEG(share) MSG_SEG(contact) MSG_SEG(location) -MSG_SEG(music) -MSG_SEG(bface) -MSG_SEG(rich) \ No newline at end of file +MSG_SEG(music) \ No newline at end of file From bfe021ddf7c405ffbc11e483c00e2e30aa68f37f Mon Sep 17 00:00:00 2001 From: dynilath Date: Sat, 2 May 2020 00:06:11 +0800 Subject: [PATCH 6/9] Refactor Message and MessageSegment. Using variant to represent data in Segment. Some methods are optimized. --- src/core/message.hpp | 468 +++++++++++++++++++++++++------------------ 1 file changed, 269 insertions(+), 199 deletions(-) diff --git a/src/core/message.hpp b/src/core/message.hpp index babecbf..6fd5379 100644 --- a/src/core/message.hpp +++ b/src/core/message.hpp @@ -4,6 +4,8 @@ #include "api.hpp" +#include + namespace cq::message { // 对字符串做 CQ 码转义 inline std::string escape(const std::string &str, const bool escape_comma = true) { @@ -30,7 +32,8 @@ namespace cq::message { } // 消息段 (即 CQ 码) - struct MessageSegment { + class MessageSegment { + public: enum class SegTypes { #define MSG_SEG(val) val, #include "./message_segment_types.inc" @@ -38,39 +41,160 @@ namespace cq::message { unimpl }; - static constexpr char *const SegTypesName[] = { + inline static constexpr char *const SegTypesName[] = { #define MSG_SEG(val) #val, #include "./message_segment_types.inc" #undef MSG_SEG ""}; - SegTypes type = SegTypes::unimpl; // 消息段类型 (即 CQ 码的功能名) - std::map data; // 消息段数据 (即 CQ 码参数), 字符串全部使用未经 CQ 码转义的原始文本 + inline static const ::std::unordered_map<::std::string, MessageSegment::SegTypes> SegTypeName2SegTypes = { +#define MSG_SEG(val) {#val, MessageSegment::SegTypes::val}, +#include "./message_segment_types.inc" +#undef MSG_SEG + {"", MessageSegment::SegTypes::unimpl}}; + + private: + using value_type = std::string; + using map_type = std::map; + using variant_type = ::std::variant; + + // 消息段类型 (即 CQ 码的功能名) + SegTypes _type = SegTypes::unimpl; - std::string SegTypeName() const { - return SegTypesName[static_cast(this->type)]; + // 当type为text和unimpl时,data为字符串 + // text为直接文本数据,unimpl为CQ码原文 + // 其他情况中,消息段数据 (即 CQ 码参数), 字符串全部使用未经 CQ 码转义的原始文本 + variant_type data; + + explicit MessageSegment(SegTypes t, map_type in_map) noexcept { + this->_type = t; + data = std::move(in_map); + } + + explicit MessageSegment(SegTypes t, value_type in_string) noexcept { + this->_type = t; + data = std::move(in_string); + } + + explicit MessageSegment(SegTypes t) noexcept { + this->_type = t; + data = map_type(); + } + + explicit MessageSegment(value_type in_string) noexcept { + this->_type = SegTypes::unimpl; + data = std::move(in_string); + } + + inline void forward_variant(variant_type &&val) { + switch (this->_type) { + case SegTypes::text: + case SegTypes::unimpl: + this->data = ::std::get(::std::move(val)); + break; + default: + this->data = ::std::get(::std::move(val)); + } + } + + inline void forward_variant(const variant_type &val) { + this->forward_variant(variant_type(val)); + } + + public: + MessageSegment(MessageSegment &&val) noexcept { + *this = ::std::move(val); + } + MessageSegment(const MessageSegment &val) noexcept { + *this = val; + } + MessageSegment &operator=(MessageSegment &&val) noexcept { + this->_type = val._type; + this->forward_variant(::std::forward(val.data)); + return *this; + } + MessageSegment &operator=(const MessageSegment &val) noexcept { + this->_type = val._type; + this->forward_variant(val.data); + return *this; + } + + // 获得segment的类型,unimpl类型表示该CQ码在sdk中未提供构造方法 + // 其他类型均有提供对应的构造方法,例如MessageSegment::SegTypes::face对应有MessageSegment::face方法 + inline SegTypes type() const { + return this->_type; + } + + // 获得segment的类型对应的字符串 + std::string segTypeName() const { + return SegTypesName[static_cast(this->_type)]; + } + + // 从cq码创建segment + static MessageSegment fromCQCode(std::string cq_code) { + auto find_char = [&](auto from, auto val) -> auto { + return ::std::find(from, cq_code.end(), val); + }; + + auto rfind_char = [&](auto val) -> auto { + return ::std::find(cq_code.rbegin(), cq_code.rend(), val); + }; + + auto is_end = [&](auto iter) -> bool { return iter == cq_code.end(); }; + + { + auto where_lbra = find_char(cq_code.begin(), '['); + cq_code.erase(cq_code.begin(), where_lbra); + + auto rwhere_rbra = ::std::find(cq_code.rbegin(), cq_code.rend(), ']'); + if (rwhere_rbra == cq_code.rend()) throw ::std::invalid_argument("Invalid CQCode"); + + auto where_rbra = rwhere_rbra.base(); + if (!is_end(where_rbra)) cq_code.erase(where_rbra, cq_code.end()); + } + + auto the_colon_after_CQ = find_char(cq_code.begin(), ':'); + if (is_end(the_colon_after_CQ)) throw ::std::invalid_argument("Invalid CQCode"); + + auto the_first_comma = find_char(the_colon_after_CQ, ','); + + ::std::string type = is_end(the_first_comma) + ? ::std::string(::std::next(the_colon_after_CQ), cq_code.end() - 2) + : ::std::string(::std::next(the_colon_after_CQ), the_first_comma); + auto type_iter = SegTypeName2SegTypes.find(type); + if (type_iter == SegTypeName2SegTypes.end()) { + return MessageSegment(::std::move(cq_code)); + } else { + auto pos = the_first_comma; + map_type temp_data; + while (pos != cq_code.end()) { + auto equal_pos = find_char(pos, '='); + auto comma_pos = find_char(equal_pos, ','); + if (is_end(comma_pos)) { + temp_data.insert({std::string(::std::next(pos), equal_pos), + std::string(::std::next(equal_pos), ::std::prev(cq_code.end()))}); + } else { + temp_data.insert({std::string(::std::next(pos), equal_pos), + std::string(::std::next(equal_pos), ::std::next(comma_pos))}); + } + pos = comma_pos; + } + return MessageSegment(type_iter->second, ::std::move(temp_data)); + } } // 转换为字符串形式 operator std::string() const { std::string s; - switch (this->type) { + switch (this->_type) { + case SegTypes::text: case SegTypes::unimpl: { - if (!this->data.empty()) { - auto iter = this->data.begin(); - s += "[CQ:" + iter->first + "," + iter->second + "]"; - } - return s; - } - case SegTypes::text: { - if (const auto it = this->data.find("text"); it != this->data.end()) { - s += escape((*it).second, false); - } - return s; + return ::std::get(this->data); } default: { - s += "[CQ:" + this->SegTypeName(); - for (const auto &item : this->data) { + auto &data_map = ::std::get(this->data); + s += "[CQ:" + this->segTypeName(); + for (const auto &item : data_map) { s += "," + item.first + "=" + escape(item.second, true); } s += "]"; @@ -79,121 +203,123 @@ namespace cq::message { } } + // 获得MessageSegment中的键值对,对于text和unimpl的Segment将抛出错误 + // 只提供返回常量引用,需要不同的内容应当另外构造 + inline const map_type &value_map() const { + return ::std::get(this->data); + } + + // 获得MessageSegment中的文本,对于非text且非unimpl的Segment将抛出错误 + // 只提供返回常量引用,需要不同的内容应当另外构造 + inline const value_type &plain_text() const { + return ::std::get(this->data); + } + // 纯文本 static MessageSegment text(const std::string &text) { - return {SegTypes::text, {{"text", text}}}; + return MessageSegment(SegTypes::text, text); } // Emoji 表情 static MessageSegment emoji(const uint32_t id) { - return {SegTypes::emoji, {{"id", to_string(id)}}}; + return MessageSegment(SegTypes::emoji, {{"id", to_string(id)}}); } // QQ 表情 static MessageSegment face(const int id) { - return {SegTypes::face, {{"id", to_string(id)}}}; + return MessageSegment(SegTypes::face, {{"id", to_string(id)}}); } // 图片 static MessageSegment image(const std::string &file) { - return {SegTypes::image, {{"file", file}}}; + return MessageSegment(SegTypes::image, {{"file", file}}); } // 语音 static MessageSegment record(const std::string &file, const bool magic = false) { - return {SegTypes::record, {{"file", file}, {"magic", to_string(magic)}}}; + return MessageSegment(SegTypes::record, {{"file", file}, {"magic", to_string(magic)}}); } // @某人 static MessageSegment at(const int64_t user_id) { - return {SegTypes::at, {{"qq", to_string(user_id)}}}; + return MessageSegment(SegTypes::at, {{"qq", to_string(user_id)}}); } // 猜拳魔法表情 static MessageSegment rps() { - return {SegTypes::rps, {}}; + return MessageSegment(SegTypes::rps); } // 掷骰子魔法表情 static MessageSegment dice() { - return {SegTypes::dice, {}}; + return MessageSegment(SegTypes::dice); } // 戳一戳 static MessageSegment shake() { - return {SegTypes::shake, {}}; + return MessageSegment(SegTypes::shake); } // 匿名发消息 static MessageSegment anonymous(const bool ignore_failure = false) { - return {SegTypes::anonymous, {{"ignore", to_string(ignore_failure)}}}; + return MessageSegment(SegTypes::anonymous, {{"ignore", 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 {SegTypes::share, {{"url", url}, {"title", title}, {"content", content}, {"image", image_url}}}; + return MessageSegment(SegTypes::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 { - SegTypes::contact, - { - {"type", type == ContactType::USER ? "qq" : "group"}, - {"id", to_string(id)}, - }, - }; + return MessageSegment(SegTypes::contact, + { + {"type", type == ContactType::USER ? "qq" : "group"}, + {"id", to_string(id)}, + }); } // 位置 static MessageSegment location(const double latitude, const double longitude, const std::string &title = "", const std::string &content = "") { - return { - SegTypes::location, - { - {"lat", to_string(latitude)}, - {"lon", to_string(longitude)}, - {"title", title}, - {"content", content}, - }, - }; + return MessageSegment(SegTypes::location, + { + {"lat", to_string(latitude)}, + {"lon", to_string(longitude)}, + {"title", title}, + {"content", content}, + }); } // 音乐 static MessageSegment music(const std::string &type, const int64_t id) { - return {SegTypes::music, {{"type", type}, {"id", to_string(id)}}}; + return MessageSegment(SegTypes::music, {{"type", type}, {"id", to_string(id)}}); } // 音乐 static MessageSegment music(const std::string &type, const int64_t id, const int32_t style) { - return {SegTypes::music, {{"type", type}, {"id", to_string(id)}, {"style", to_string(style)}}}; + return MessageSegment(SegTypes::music, + {{"type", type}, {"id", to_string(id)}, {"style", to_string(style)}}); } // 音乐自定义分享 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 { - SegTypes::music, - { - {"type", "custom"}, - {"url", url}, - {"audio", audio_url}, - {"title", title}, - {"content", content}, - {"image", image_url}, - }, - }; + return MessageSegment(SegTypes::music, + { + {"type", "custom"}, + {"url", url}, + {"audio", audio_url}, + {"title", title}, + {"content", content}, + {"image", image_url}, + }); } - }; - - const ::std::unordered_map<::std::string, MessageSegment::SegTypes> SegTypeName2SegTypes = { -#define MSG_SEG(val) {#val, MessageSegment::SegTypes::val}, -#include "./message_segment_types.inc" -#undef MSG_SEG - {"", MessageSegment::SegTypes::unimpl}}; + }; // namespace cq::message struct Message : std::list { using std::list::list; @@ -206,102 +332,31 @@ namespace cq::message { Message(const std::string &msg_str) { using cq::utils::string_trim; - // 定义字符流操作 - size_t idx = 0; - const auto has_next = [&] { return idx < msg_str.length(); }; - const auto next = [&] { return msg_str[idx++]; }; - const auto move_rel = [&](const size_t rel_steps = 0) { idx += rel_steps; }; - const auto peek = [&] { return msg_str[idx]; }; - const auto peek_n = [&](const size_t count = 1) { - return msg_str.substr(idx, std::min(count, msg_str.length() - idx)); - }; - - // 判断当前位置是否 CQ 码开头 - const auto is_cq_code_begin = [&](const char ch) { return ch == '[' && peek_n(3) == "CQ:"; }; + const ::std::string CQ_head = "[CQ:"; - // 定义状态 - enum { S0, S1 } state = S0; - - std::string temp_text; // 暂存以后可能作为 text 类型消息段保存的内容 - std::string cq_code; // 不包含 [CQ: 和 ] 的 CQ 码内容, 如 image,file=abc.jpg - - const auto save_temp_text = [&] { - if (!temp_text.empty()) this->push_back(MessageSegment::text(unescape(temp_text))); - temp_text.clear(); - cq_code.clear(); + // 不属于CQ码的"["和"]"在CQ信息中总是会escape为"["和"]",算是好处理的地方 + auto search_cq_head = [&](auto from) -> auto { + return ::std::search(from, msg_str.end(), CQ_head.begin(), CQ_head.end()); }; - - const auto save_cq_code = [&] { - std::istringstream iss(cq_code); - std::string type, param; - std::map data; - getline(iss, type, ','); // 读取功能名 - auto segtype_iter = SegTypeName2SegTypes.find(type); - if (segtype_iter == SegTypeName2SegTypes.end()) { - getline(iss, param, ']'); - data.emplace(std::string(type), std::string(param)); - this->push_back(MessageSegment{MessageSegment::SegTypes::unimpl, std::move(data)}); - } else { - while (iss) { - getline(iss, param, ','); // 读取一个参数 - string_trim(param); - if (param.empty()) continue; - const auto eq_pos = param.find('='); - data.emplace( - std::string(param.begin(), param.begin() + eq_pos), - eq_pos != std::string::npos ? std::string(param.begin() + eq_pos + 1, param.end()) : ""); - param.clear(); - } - this->push_back(MessageSegment{segtype_iter->second, std::move(data)}); - } - cq_code.clear(); - temp_text.clear(); + auto find_cq_tail = [&](auto from) -> auto { + return ::std::find(from, msg_str.end(), ']'); }; + auto is_end = [&](auto iter) -> bool { return iter == msg_str.end(); }; - /* - 状态图: - +---+ +---+ - | | | | - | other | other - v | v | - +--+-+ | +--+-+ | +----+ - | S0 +-+--[CQ:-->+ S1 +-+--]-->+ SF | - +--+-+ +--+-+ +----+ - ^ | - | | - +---[CQ:-back----+ - */ - while (has_next()) { - const auto ch = next(); - switch (state) { - case S0: // 处理纯文本或 CQ 码开头 - if (is_cq_code_begin(ch)) { - // 潜在的 CQ 码开始 - save_temp_text(); - temp_text += "[CQ:"; - move_rel(+3); // 跳过 CQ: - state = S1; - } else { - temp_text += ch; - } - break; - case S1: // 处理 CQ 码内容 - if (is_cq_code_begin(ch)) { - move_rel(-1); // 回退 [ - state = S0; // 回到 S0 - } else if (ch == ']') { - // CQ 码结束 - save_cq_code(); - state = S0; - } else { - cq_code += ch; - temp_text += ch; - } + auto work_pos = msg_str.begin(); + while (!is_end(work_pos)) { + auto cq_head_pos = search_cq_head(work_pos); + if (is_end(cq_head_pos)) { + this->push_back(MessageSegment::text(::std::string(work_pos, cq_head_pos))); break; + } else { + if (::std::distance(work_pos, cq_head_pos) > 0) + this->push_back(MessageSegment::text(::std::string(work_pos, cq_head_pos))); + auto cq_tail_pos = find_cq_tail(cq_head_pos); + this->push_back(MessageSegment::fromCQCode(::std::string(cq_head_pos, cq_tail_pos + 1))); + work_pos = ::std::next(cq_tail_pos); } } - save_temp_text(); // 保存剩余的临时文本 - this->reduce(); } // 将消息段转换为 Message 对象 @@ -325,13 +380,10 @@ namespace cq::message { std::string extract_plain_text() const { std::string result; for (const auto &seg : *this) { - if (seg.type == MessageSegment::SegTypes::text) { - result += seg.data.at("text") + " "; + if (seg.type() == MessageSegment::SegTypes::text) { + result += seg.plain_text(); } } - if (!result.empty()) { - result.erase(result.end() - 1); // remove the trailing space - } return result; } @@ -347,40 +399,44 @@ namespace cq::message { // 合并相邻的 text 消息段 void reduce() { - if (this->empty()) { - return; - } + if (this->empty()) return; - auto last_seg_it = this->begin(); - for (auto it = this->begin(); ++it != this->end();) { - if (it->type == MessageSegment::SegTypes::text && last_seg_it->type == MessageSegment::SegTypes::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; - } - } + auto iter_last = this->begin(); + + while (iter_last != this->end()) { + iter_last = ::std::find_if(iter_last, this->end(), [](auto &val) -> bool { + return val.type() == MessageSegment::SegTypes::text; + }); + if (iter_last == this->end()) break; + + std::string sum = iter_last->plain_text(); + auto iter_this = ::std::next(iter_last); - if (this->size() == 1 && this->front().type == MessageSegment::SegTypes::text - && this->extract_plain_text().empty()) { - this->clear(); // the only item is an empty text segment, we should remove it + while (iter_this != this->end() && iter_this->type() == MessageSegment::SegTypes::text) { + sum += iter_this->plain_text(); + this->erase(iter_this); + iter_this = ::std::next(iter_last); + } + if (iter_last->plain_text().size() != sum.size()) *iter_last = MessageSegment::text(sum); + iter_last = iter_this; } } Message &operator+=(const Message &other) { - this->insert(this->end(), other.begin(), other.end()); - this->reduce(); + auto start = other.begin(); + if (!this->empty() && !other.empty() && this->back().type() == other.front().type() + && this->back().type() == MessageSegment::SegTypes::text) { + this->back() = MessageSegment::text(this->back().plain_text() + other.front().plain_text()); + ::std::advance(start, 1); + } + this->insert(this->end(), start, other.end()); return *this; } - template - Message &operator+=(const T &other) { - return this->operator+=(Message(other)); + template , MessageSegment>, int> = 0> + Message &operator+=(Tx &&segment) { + return this->push_back(::std::forward(segment)); } Message operator+(const Message &other) const { @@ -388,28 +444,42 @@ namespace cq::message { result += other; // use operator+= return result; } - - template - Message operator+(const T &other) const { - return this->operator+(Message(other)); - } }; - template - inline Message operator+(const T &lhs, const Message &rhs) { - return Message(lhs) + rhs; + template < + typename T, typename Tx, + typename ::std::enable_if_t<::std::is_convertible_v && !::std::is_same_v, int> = 0, + typename ::std::enable_if_t<::std::is_same_v<::std::decay_t, MessageSegment>, int> = 0> + inline Message operator+(const T &lhs, Tx &&rhs) { + return Message(lhs) + ::std::forward(rhs); } - template - inline Message operator+(const MessageSegment &lhs, const T &rhs) { - return Message(lhs) + rhs; + template , MessageSegment>, int> = 0> + inline Message operator+(Tx &&lhs, const T &rhs) { + return Message(::std::forward(lhs)) + rhs; } inline bool operator==(const MessageSegment &lhs, const MessageSegment &rhs) { - return std::string(lhs) == std::string(rhs); + return lhs.type() == rhs.type() && ::std::string(lhs) == ::std::string(rhs); + } + + inline bool operator!=(const MessageSegment &lhs, const MessageSegment &rhs) { + return !(lhs == rhs); } inline bool operator==(const Message &lhs, const Message &rhs) { - return std::string(lhs) == std::string(rhs); + if (lhs.size() != rhs.size()) return false; + auto lhs_iter = lhs.begin(); + auto rhs_iter = rhs.begin(); + while (lhs_iter != lhs.end() && rhs_iter != rhs.end()) { + if (*lhs_iter != *rhs_iter) return false; + ::std::advance(lhs_iter, 1); + ::std::advance(rhs_iter, 1); + } + return true; + } + inline bool operator!=(const Message &lhs, const Message &rhs) { + return !(lhs == rhs); } } // namespace cq::message From 79365e33a10a1df8105205553332335f42fc1142 Mon Sep 17 00:00:00 2001 From: dynilath Date: Sat, 2 May 2020 00:07:27 +0800 Subject: [PATCH 7/9] Add some tests for sdk content. --- tests/CMakeLists.txt | 3 + tests/test_sdk_message.cpp | 164 +++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 tests/test_sdk_message.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1d10ad8..c5e045f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,3 +14,6 @@ cq_add_test(test_dolores test_dolores_watashi.cpp test_dolores_traits.cpp test_dolores_handler.cpp) + +cq_add_test(test_sdk + test_sdk_message.cpp) \ No newline at end of file diff --git a/tests/test_sdk_message.cpp b/tests/test_sdk_message.cpp new file mode 100644 index 0000000..026abb1 --- /dev/null +++ b/tests/test_sdk_message.cpp @@ -0,0 +1,164 @@ + +#define CATCH_CONFIG_MAIN +#include "cqcppsdk/cqcppsdk.hpp" + +#include "catch.hpp" + +TEST_CASE("MessageSegment ctor", "[message segment]") { + using cq::message::MessageSegment; + using seg_types = MessageSegment::SegTypes; + + ::std::string src = "test"; + MessageSegment seg = MessageSegment::text(src); + MessageSegment seg1 = seg; + seg = std::move(seg1); + REQUIRE(seg.type() == seg_types::text); + REQUIRE(::std::string(seg) == src); + + src = "[CQ:emoji,id=127838]"; + seg = MessageSegment::fromCQCode(src); + seg1 = seg; + seg = std::move(seg1); + REQUIRE(seg.type() == seg_types::emoji); + REQUIRE((seg.value_map().at("id") == ::std::string("127838"))); + REQUIRE(::std::string(seg) == src); + seg = MessageSegment::emoji(127838); + REQUIRE(seg.type() == seg_types::emoji); + REQUIRE((seg.value_map().at("id") == ::std::string("127838"))); + REQUIRE(::std::string(seg) == src); + + src = "[CQ:face,id=180]"; + seg = MessageSegment::fromCQCode(src); + seg1 = seg; + seg = std::move(seg1); + REQUIRE(seg.type() == seg_types::face); + REQUIRE((seg.value_map().at("id") == ::std::string("180"))); + REQUIRE(::std::string(seg) == src); + seg = MessageSegment::face(180); + REQUIRE(seg.type() == seg_types::face); + REQUIRE((seg.value_map().at("id") == ::std::string("180"))); + REQUIRE(::std::string(seg) == src); + + src = "[CQ:image,file=AE9CE0B5A1FBA37F95718FDC547945CC.jpg]"; + seg = MessageSegment::fromCQCode(src); + seg1 = seg; + seg = std::move(seg1); + REQUIRE(seg.type() == seg_types::image); + REQUIRE(seg.value_map().at("file") == "AE9CE0B5A1FBA37F95718FDC547945CC.jpg"); + REQUIRE(::std::string(seg) == src); + seg = MessageSegment::image("AE9CE0B5A1FBA37F95718FDC547945CC.jpg"); + REQUIRE(seg.type() == seg_types::image); + REQUIRE(::std::string(seg) == src); + + src = "[CQ:record,file=C5B0B7D8A515F1F652B0DEBB49335B9E.silk]"; + seg = MessageSegment::fromCQCode(src); + seg1 = seg; + seg = std::move(seg1); + REQUIRE(seg.type() == seg_types::record); + REQUIRE(seg.value_map().at("file") == "C5B0B7D8A515F1F652B0DEBB49335B9E.silk"); + REQUIRE(::std::string(seg) == src); + seg = MessageSegment::record("C5B0B7D8A515F1F652B0DEBB49335B9E.silk", true); + REQUIRE(seg.type() == seg_types::record); + REQUIRE(::std::string(seg) == "[CQ:record,file=C5B0B7D8A515F1F652B0DEBB49335B9E.silk,magic=true]"); +} +TEST_CASE("Message ctor", "[message]") { + ::std::string src = + "[CQ:emoji,id=127838]" + "test" + "[CQ:face,id=180]" + "[CQ:image,file=AE9CE0B5A1FBA37F95718FDC547945CC.jpg]" + "[CQ:bface,p=204112,id=2DD591BFD449F584C709D276EC472E55]"; + + cq::message::Message msg; + msg = src; + REQUIRE(msg.size() == 5); + + using seg_types = cq::message::MessageSegment::SegTypes; + auto i = msg.begin(); + REQUIRE(i->type() == seg_types::emoji); + REQUIRE((i->value_map().at("id") == ::std::string("127838"))); + std::advance(i, 1); + REQUIRE(i->type() == seg_types::text); + REQUIRE(i->plain_text() == "test"); + std::advance(i, 1); + REQUIRE(i->type() == seg_types::face); + REQUIRE(i->value_map().at("id") == "180"); + std::advance(i, 1); + REQUIRE(i->type() == seg_types::image); + REQUIRE(i->value_map().at("file") == "AE9CE0B5A1FBA37F95718FDC547945CC.jpg"); + std::advance(i, 1); + REQUIRE(i->type() == seg_types::unimpl); + REQUIRE(i->plain_text() == "[CQ:bface,p=204112,id=2DD591BFD449F584C709D276EC472E55]"); +} + +TEST_CASE("Message reduce and concat", "[message]") { + using namespace cq::message; + cq::message::Message msg; + msg.push_back(MessageSegment::text("1")); + msg.push_back(MessageSegment::text("2")); + msg.push_back(MessageSegment::text("34")); + msg.push_back(MessageSegment::text("56")); + msg.push_back(MessageSegment::text("78")); + msg.push_back(MessageSegment::text("9")); + REQUIRE(msg.size() == 6); + msg.reduce(); + REQUIRE(msg.size() == 1); + REQUIRE(msg.extract_plain_text() == "123456789"); + + msg.push_back(MessageSegment::text("A")); + msg.push_back(MessageSegment::text("BC")); + msg.push_back(MessageSegment::face(180)); + msg.push_back(MessageSegment::text("D")); + msg.push_back(MessageSegment::text("EF")); + REQUIRE(msg.size() == 6); + msg.reduce(); + REQUIRE(msg.size() == 3); + REQUIRE(msg.extract_plain_text() == "123456789ABCDEF"); + + msg.push_back(MessageSegment::text("GH")); + msg.push_back(MessageSegment::face(180)); + REQUIRE(msg.size() == 5); + msg.reduce(); + REQUIRE(msg.size() == 4); + REQUIRE(msg.extract_plain_text() == "123456789ABCDEFGH"); + + msg += "IJK"; + REQUIRE(msg.size() == 5); + REQUIRE(msg.extract_plain_text() == "123456789ABCDEFGHIJK"); + + msg += "LMN"; + REQUIRE(msg.size() == 5); + REQUIRE(msg.extract_plain_text() == "123456789ABCDEFGHIJKLMN"); + + cq::message::Message msg2 = + "[CQ:emoji,id=127838]" + "test" + "[CQ:face,id=180]" + "[CQ:image,file=AE9CE0B5A1FBA37F95718FDC547945CC.jpg]" + "[CQ:bface,p=204112,id=2DD591BFD449F584C709D276EC472E55]"; + REQUIRE(msg2.size() == 5); + msg += msg2; + REQUIRE(msg.size() == 10); + REQUIRE(msg.extract_plain_text() == "123456789ABCDEFGHIJKLMNtest"); +} + +TEST_CASE("Message equal", "[message]") { + using namespace cq::message; + ::std::string src = + "[CQ:emoji,id=127838]" + "test" + "[CQ:face,id=180]" + "[CQ:image,file=AE9CE0B5A1FBA37F95718FDC547945CC.jpg]" + "[CQ:bface,p=204112,id=2DD591BFD449F584C709D276EC472E55]"; + cq::message::Message msg1 = src; + cq::message::Message msg2 = src; + REQUIRE(msg1 == msg2); + msg2.pop_back(); + REQUIRE(msg1 != msg2); + msg2.push_back(MessageSegment::face(180)); + REQUIRE(msg1 != msg2); + msg2.pop_back(); + REQUIRE(msg1 != msg2); + msg2.push_back(MessageSegment::fromCQCode("[CQ:bface,p=204112,id=2DD591BFD449F584C709D276EC472E55]")); + REQUIRE(msg1 == msg2); +} \ No newline at end of file From bee31e46c44d0926848190a84cc7c02fec2996ab Mon Sep 17 00:00:00 2001 From: dynilath Date: Sun, 3 May 2020 18:37:47 +0800 Subject: [PATCH 8/9] Refactor Message --- src/core/message.hpp | 291 ++++++++++++++++++++++++------------------- 1 file changed, 166 insertions(+), 125 deletions(-) diff --git a/src/core/message.hpp b/src/core/message.hpp index 6fd5379..d1b4b96 100644 --- a/src/core/message.hpp +++ b/src/core/message.hpp @@ -31,6 +31,8 @@ namespace cq::message { return res; } + class Message; + // 消息段 (即 CQ 码) class MessageSegment { public: @@ -41,6 +43,7 @@ namespace cq::message { unimpl }; + private: inline static constexpr char *const SegTypesName[] = { #define MSG_SEG(val) #val, #include "./message_segment_types.inc" @@ -53,114 +56,95 @@ namespace cq::message { #undef MSG_SEG {"", MessageSegment::SegTypes::unimpl}}; - private: using value_type = std::string; using map_type = std::map; using variant_type = ::std::variant; // 消息段类型 (即 CQ 码的功能名) - SegTypes _type = SegTypes::unimpl; + SegTypes _type; // 当type为text和unimpl时,data为字符串 // text为直接文本数据,unimpl为CQ码原文 // 其他情况中,消息段数据 (即 CQ 码参数), 字符串全部使用未经 CQ 码转义的原始文本 variant_type data; + // 构造支持的键值对数据段 explicit MessageSegment(SegTypes t, map_type in_map) noexcept { this->_type = t; data = std::move(in_map); } + // 构造支持的字符串数据段,仅有text explicit MessageSegment(SegTypes t, value_type in_string) noexcept { this->_type = t; data = std::move(in_string); } + // 构造仅有type的段 explicit MessageSegment(SegTypes t) noexcept { this->_type = t; data = map_type(); } + // 构造不支持的段 explicit MessageSegment(value_type in_string) noexcept { this->_type = SegTypes::unimpl; data = std::move(in_string); } - inline void forward_variant(variant_type &&val) { - switch (this->_type) { - case SegTypes::text: - case SegTypes::unimpl: - this->data = ::std::get(::std::move(val)); - break; - default: - this->data = ::std::get(::std::move(val)); - } - } - - inline void forward_variant(const variant_type &val) { - this->forward_variant(variant_type(val)); - } - - public: - MessageSegment(MessageSegment &&val) noexcept { - *this = ::std::move(val); - } - MessageSegment(const MessageSegment &val) noexcept { - *this = val; - } - MessageSegment &operator=(MessageSegment &&val) noexcept { - this->_type = val._type; - this->forward_variant(::std::forward(val.data)); - return *this; - } - MessageSegment &operator=(const MessageSegment &val) noexcept { - this->_type = val._type; - this->forward_variant(val.data); - return *this; - } - - // 获得segment的类型,unimpl类型表示该CQ码在sdk中未提供构造方法 - // 其他类型均有提供对应的构造方法,例如MessageSegment::SegTypes::face对应有MessageSegment::face方法 - inline SegTypes type() const { - return this->_type; - } + friend Message; - // 获得segment的类型对应的字符串 - std::string segTypeName() const { - return SegTypesName[static_cast(this->_type)]; + inline static bool testCQCode(const ::std::string &src) noexcept { + constexpr char CQ_head[] = "[CQ:"; + constexpr char CQ_end = ']'; + if (src.size() <= sizeof(CQ_head) + sizeof(CQ_end)) return false; + for (size_t i = 0; !CQ_head[i]; i++) + if (src[i] != CQ_head[i]) return false; + if (src.back() != CQ_end) return false; + auto cursor = src.begin(); + auto expect = [&](char delim) -> bool { + cursor = ::std::find(cursor, src.end(), delim); + return cursor != src.end(); + }; + while (!expect(',')) { + if (!expect('=')) return false; + } + return true; } - // 从cq码创建segment - static MessageSegment fromCQCode(std::string cq_code) { + // 从cq码创建segment,会报错 + // MessageSegment总是对自身内部数据保证所有权,所以按值传参 + static MessageSegment fromCQCodeNoCheck(::std::string cq_code) noexcept(false) { auto find_char = [&](auto from, auto val) -> auto { return ::std::find(from, cq_code.end(), val); }; - auto rfind_char = [&](auto val) -> auto { - return ::std::find(cq_code.rbegin(), cq_code.rend(), val); + auto rfind_char = [&](auto from, auto val) -> auto { + return ::std::find(from, cq_code.rend(), val); }; auto is_end = [&](auto iter) -> bool { return iter == cq_code.end(); }; - { - auto where_lbra = find_char(cq_code.begin(), '['); - cq_code.erase(cq_code.begin(), where_lbra); - - auto rwhere_rbra = ::std::find(cq_code.rbegin(), cq_code.rend(), ']'); - if (rwhere_rbra == cq_code.rend()) throw ::std::invalid_argument("Invalid CQCode"); - - auto where_rbra = rwhere_rbra.base(); - if (!is_end(where_rbra)) cq_code.erase(where_rbra, cq_code.end()); - } +#define DECQCODE_ASSERT(test) \ + { \ + if (!(test)) throw ::std::invalid_argument("Invalid CQCode"); \ + } + DECQCODE_ASSERT(cq_code.front() == '[') + DECQCODE_ASSERT(cq_code.back() == ']'); auto the_colon_after_CQ = find_char(cq_code.begin(), ':'); - if (is_end(the_colon_after_CQ)) throw ::std::invalid_argument("Invalid CQCode"); + DECQCODE_ASSERT(the_colon_after_CQ != cq_code.end()); + + auto rbracket_pos = ::std::prev(cq_code.end()); auto the_first_comma = find_char(the_colon_after_CQ, ','); + ::std::string type; + if (is_end(the_first_comma)) { + ::std::string(::std::next(the_colon_after_CQ), rbracket_pos); + } else { + type = ::std::string(::std::next(the_colon_after_CQ), the_first_comma); + } - ::std::string type = is_end(the_first_comma) - ? ::std::string(::std::next(the_colon_after_CQ), cq_code.end() - 2) - : ::std::string(::std::next(the_colon_after_CQ), the_first_comma); auto type_iter = SegTypeName2SegTypes.find(type); if (type_iter == SegTypeName2SegTypes.end()) { return MessageSegment(::std::move(cq_code)); @@ -169,18 +153,71 @@ namespace cq::message { map_type temp_data; while (pos != cq_code.end()) { auto equal_pos = find_char(pos, '='); + _ASSERT(equal_pos != cq_code.end()); auto comma_pos = find_char(equal_pos, ','); if (is_end(comma_pos)) { temp_data.insert({std::string(::std::next(pos), equal_pos), std::string(::std::next(equal_pos), ::std::prev(cq_code.end()))}); } else { temp_data.insert({std::string(::std::next(pos), equal_pos), - std::string(::std::next(equal_pos), ::std::next(comma_pos))}); + std::string(::std::next(equal_pos), ::std::next(rbracket_pos))}); + break; } pos = comma_pos; } return MessageSegment(type_iter->second, ::std::move(temp_data)); } +#undef DECQCODE_ASSERT + } + + public: + MessageSegment() noexcept { + this->_type = SegTypes::unimpl; + } + MessageSegment(MessageSegment &&val) noexcept { + this->_type = val._type; + this->data = ::std::move(val.data); + } + MessageSegment(const MessageSegment &val) noexcept { + this->_type = val._type; + this->data = val.data; + } + MessageSegment &operator=(MessageSegment &&val) noexcept { + this->_type = val._type; + this->data = ::std::move(val.data); + return *this; + } + MessageSegment &operator=(const MessageSegment &val) noexcept { + this->_type = val._type; + this->data = val.data; + return *this; + } + + // 获得segment的类型,unimpl类型表示该CQ码在sdk中未提供构造方法 + // 其他类型均有提供对应的构造方法,例如MessageSegment::SegTypes::face对应有MessageSegment::face方法 + inline SegTypes type() const { + return this->_type; + } + + // 获得segment的类型对应的字符串 + std::string segTypeName() const { + return SegTypesName[static_cast(this->_type)]; + } + + // 提供==语义 + inline bool operator==(const MessageSegment &other) const noexcept { + return this->type() == other.type() && this->data == other.data; + } + + // 提供!=语义 + inline bool operator!=(const MessageSegment &other) const noexcept { + return !this->operator==(other); + } + + // 从cq码创建segment + static MessageSegment fromCQCode(std::string cq_code) { + if (!testCQCode(cq_code)) return MessageSegment(); + return MessageSegment::fromCQCodeNoCheck(cq_code); } // 转换为字符串形式 @@ -321,54 +358,72 @@ namespace cq::message { } }; // namespace cq::message - struct Message : std::list { - using std::list::list; + class Message : public std::list { + private: + using container_type = std::list; + + inline static MessageSegment fromCQCodeNoCheck(const ::std::string &cq_code) { + return MessageSegment::fromCQCodeNoCheck(cq_code); + } + + public: + Message() noexcept {}; // 将 C 字符串形式的消息转换为 Message 对象 Message(const char *msg_str) : Message(std::string(msg_str)) { } // 将字符串形式的消息转换为 Message 对象 + // 如果字符串中CQ码不符合规范,会抛出invalid_argument,此时构造的Message中无元素 Message(const std::string &msg_str) { using cq::utils::string_trim; const ::std::string CQ_head = "[CQ:"; - // 不属于CQ码的"["和"]"在CQ信息中总是会escape为"["和"]",算是好处理的地方 - auto search_cq_head = [&](auto from) -> auto { - return ::std::search(from, msg_str.end(), CQ_head.begin(), CQ_head.end()); - }; - auto find_cq_tail = [&](auto from) -> auto { - return ::std::find(from, msg_str.end(), ']'); + auto find_char = [&](auto from, char delim) -> auto { + return ::std::find(from, msg_str.end(), delim); }; auto is_end = [&](auto iter) -> bool { return iter == msg_str.end(); }; + container_type cont; auto work_pos = msg_str.begin(); while (!is_end(work_pos)) { - auto cq_head_pos = search_cq_head(work_pos); + // 不属于CQ码的"["和"]"在CQ信息中总是会escape为"["和"]",算是好处理的地方 + auto cq_head_pos = find_char(work_pos, '['); if (is_end(cq_head_pos)) { - this->push_back(MessageSegment::text(::std::string(work_pos, cq_head_pos))); + cont.push_back(MessageSegment::text(::std::string(work_pos, cq_head_pos))); break; } else { if (::std::distance(work_pos, cq_head_pos) > 0) - this->push_back(MessageSegment::text(::std::string(work_pos, cq_head_pos))); - auto cq_tail_pos = find_cq_tail(cq_head_pos); - this->push_back(MessageSegment::fromCQCode(::std::string(cq_head_pos, cq_tail_pos + 1))); - work_pos = ::std::next(cq_tail_pos); + cont.push_back(MessageSegment::text(::std::string(work_pos, cq_head_pos))); + + // 由于可以从用户指定的字符串构建,所以有可能先遇到'[' + auto cq_tail_pos = ::std::find_if( + ::std::next(cq_head_pos), msg_str.end(), [&](char w) -> bool { return w == '[' || w == ']'; }); + + if (!is_end(cq_tail_pos) && *cq_tail_pos == ']') cq_tail_pos = ::std::next(cq_tail_pos); + + // 如果先遇到'[',会发生异常,Message会保持空容器 + cont.push_back(MessageSegment::fromCQCodeNoCheck(::std::string(cq_head_pos, cq_tail_pos))); + work_pos = cq_tail_pos; } } + // fromCQCodeNoCheck可能抛出,使用swap来保证强异常安全 + this->swap(cont); } // 将消息段转换为 Message 对象 - Message(const MessageSegment &seg) { - this->push_back(seg); + Message(MessageSegment seg) { + this->push_back(::std::move(seg)); } // 将 Message 对象转换为字符串形式的消息 - operator std::string() const { - return std::accumulate(this->begin(), this->end(), std::string(), [](const auto &seg1, const auto &seg2) { - return std::string(seg1) + std::string(seg2); - }); + operator std::string() const noexcept { + ::std::ostringstream oss; + for (auto &seg : *this) { + oss << ::std::string(seg); + } + return oss.str(); } // 向指定主体发送消息 @@ -422,64 +477,50 @@ namespace cq::message { } } - Message &operator+=(const Message &other) { + // 连接另一个Message + inline Message &operator+=(Message other) { auto start = other.begin(); if (!this->empty() && !other.empty() && this->back().type() == other.front().type() && this->back().type() == MessageSegment::SegTypes::text) { this->back() = MessageSegment::text(this->back().plain_text() + other.front().plain_text()); ::std::advance(start, 1); } - this->insert(this->end(), start, other.end()); + this->splice(this->end(), other, start, other.end()); return *this; } - template , MessageSegment>, int> = 0> - Message &operator+=(Tx &&segment) { - return this->push_back(::std::forward(segment)); + // 连接另一个MessageSegment + inline Message &operator+=(MessageSegment segment) { + this->push_back(::std::move(segment)); + return *this; } - Message operator+(const Message &other) const { - auto result = *this; - result += other; // use operator+= - return result; + // 提供==语义 + inline bool operator==(const Message &rhs) { + if (this->size() != rhs.size()) return false; + auto lhs_iter = this->begin(); + auto rhs_iter = rhs.begin(); + while (lhs_iter != this->end() && rhs_iter != rhs.end()) { + if (*lhs_iter != *rhs_iter) return false; + ::std::advance(lhs_iter, 1); + ::std::advance(rhs_iter, 1); + } + return true; } - }; - template < - typename T, typename Tx, - typename ::std::enable_if_t<::std::is_convertible_v && !::std::is_same_v, int> = 0, - typename ::std::enable_if_t<::std::is_same_v<::std::decay_t, MessageSegment>, int> = 0> - inline Message operator+(const T &lhs, Tx &&rhs) { - return Message(lhs) + ::std::forward(rhs); - } - - template , MessageSegment>, int> = 0> - inline Message operator+(Tx &&lhs, const T &rhs) { - return Message(::std::forward(lhs)) + rhs; - } - - inline bool operator==(const MessageSegment &lhs, const MessageSegment &rhs) { - return lhs.type() == rhs.type() && ::std::string(lhs) == ::std::string(rhs); - } + // 提供!=语义 + inline bool operator!=(const Message &rhs) { + return !this->operator==(rhs); + } + }; - inline bool operator!=(const MessageSegment &lhs, const MessageSegment &rhs) { - return !(lhs == rhs); + // 提供任何能转换到Message的对象和MessageSegment之间的连接运算 + inline Message operator+(Message lhs, MessageSegment rhs) { + return lhs += ::std::move(rhs); } - inline bool operator==(const Message &lhs, const Message &rhs) { - if (lhs.size() != rhs.size()) return false; - auto lhs_iter = lhs.begin(); - auto rhs_iter = rhs.begin(); - while (lhs_iter != lhs.end() && rhs_iter != rhs.end()) { - if (*lhs_iter != *rhs_iter) return false; - ::std::advance(lhs_iter, 1); - ::std::advance(rhs_iter, 1); - } - return true; - } - inline bool operator!=(const Message &lhs, const Message &rhs) { - return !(lhs == rhs); + // 提供任何能转换到Message的对象和Message之间的连接运算 + inline Message operator+(Message lhs, Message rhs) { + return lhs += ::std::move(rhs); } } // namespace cq::message From 8f14c2ed85b6d2917ad7aef149acdfb89512937f Mon Sep 17 00:00:00 2001 From: dynilath Date: Sun, 3 May 2020 18:38:43 +0800 Subject: [PATCH 9/9] Add more tests to Message --- tests/test_sdk_message.cpp | 523 ++++++++++++++++++++++++++++--------- 1 file changed, 398 insertions(+), 125 deletions(-) diff --git a/tests/test_sdk_message.cpp b/tests/test_sdk_message.cpp index 026abb1..3434509 100644 --- a/tests/test_sdk_message.cpp +++ b/tests/test_sdk_message.cpp @@ -8,138 +8,393 @@ TEST_CASE("MessageSegment ctor", "[message segment]") { using cq::message::MessageSegment; using seg_types = MessageSegment::SegTypes; - ::std::string src = "test"; - MessageSegment seg = MessageSegment::text(src); - MessageSegment seg1 = seg; - seg = std::move(seg1); - REQUIRE(seg.type() == seg_types::text); - REQUIRE(::std::string(seg) == src); - - src = "[CQ:emoji,id=127838]"; - seg = MessageSegment::fromCQCode(src); - seg1 = seg; - seg = std::move(seg1); - REQUIRE(seg.type() == seg_types::emoji); - REQUIRE((seg.value_map().at("id") == ::std::string("127838"))); - REQUIRE(::std::string(seg) == src); - seg = MessageSegment::emoji(127838); - REQUIRE(seg.type() == seg_types::emoji); - REQUIRE((seg.value_map().at("id") == ::std::string("127838"))); - REQUIRE(::std::string(seg) == src); - - src = "[CQ:face,id=180]"; - seg = MessageSegment::fromCQCode(src); - seg1 = seg; - seg = std::move(seg1); - REQUIRE(seg.type() == seg_types::face); - REQUIRE((seg.value_map().at("id") == ::std::string("180"))); - REQUIRE(::std::string(seg) == src); - seg = MessageSegment::face(180); - REQUIRE(seg.type() == seg_types::face); - REQUIRE((seg.value_map().at("id") == ::std::string("180"))); - REQUIRE(::std::string(seg) == src); - - src = "[CQ:image,file=AE9CE0B5A1FBA37F95718FDC547945CC.jpg]"; - seg = MessageSegment::fromCQCode(src); - seg1 = seg; - seg = std::move(seg1); - REQUIRE(seg.type() == seg_types::image); - REQUIRE(seg.value_map().at("file") == "AE9CE0B5A1FBA37F95718FDC547945CC.jpg"); - REQUIRE(::std::string(seg) == src); - seg = MessageSegment::image("AE9CE0B5A1FBA37F95718FDC547945CC.jpg"); - REQUIRE(seg.type() == seg_types::image); - REQUIRE(::std::string(seg) == src); - - src = "[CQ:record,file=C5B0B7D8A515F1F652B0DEBB49335B9E.silk]"; - seg = MessageSegment::fromCQCode(src); - seg1 = seg; - seg = std::move(seg1); - REQUIRE(seg.type() == seg_types::record); - REQUIRE(seg.value_map().at("file") == "C5B0B7D8A515F1F652B0DEBB49335B9E.silk"); - REQUIRE(::std::string(seg) == src); - seg = MessageSegment::record("C5B0B7D8A515F1F652B0DEBB49335B9E.silk", true); - REQUIRE(seg.type() == seg_types::record); - REQUIRE(::std::string(seg) == "[CQ:record,file=C5B0B7D8A515F1F652B0DEBB49335B9E.silk,magic=true]"); + SECTION("default") { + SECTION("text") { + ::std::string src = "test"; + + MessageSegment seg = MessageSegment::text(src); + REQUIRE(seg.type() == seg_types::text); + REQUIRE(::std::string(seg) == src); + + MessageSegment seg2 = seg; + REQUIRE(seg2.type() == seg_types::text); + REQUIRE(::std::string(seg2) == src); + + MessageSegment seg3 = ::std::move(seg2); + REQUIRE(seg3.type() == seg_types::text); + REQUIRE(::std::string(seg3) == src); + } + + SECTION("not text") { + ::std::string src = "[CQ:emoji,id=127838]"; + + MessageSegment seg = MessageSegment::emoji(127838); + REQUIRE(seg.type() == seg_types::emoji); + REQUIRE(::std::string(seg) == src); + + MessageSegment seg2 = seg; + REQUIRE(seg2.type() == seg_types::emoji); + REQUIRE(::std::string(seg2) == src); + + MessageSegment seg3 = ::std::move(seg2); + REQUIRE(seg3.type() == seg_types::emoji); + REQUIRE(::std::string(seg3) == src); + } + + SECTION("unimpl") { + ::std::string src = "[CQ:bface,p=204112,id=2DD591BFD449F584C709D276EC472E55]"; + + MessageSegment seg = MessageSegment::fromCQCode(src); + REQUIRE(seg.type() == seg_types::unimpl); + REQUIRE(::std::string(seg) == src); + + MessageSegment seg2 = seg; + REQUIRE(seg2.type() == seg_types::unimpl); + REQUIRE(::std::string(seg2) == src); + + MessageSegment seg3 = ::std::move(seg2); + REQUIRE(seg3.type() == seg_types::unimpl); + REQUIRE(::std::string(seg3) == src); + } + } + + SECTION("text") { + ::std::string src = "test"; + MessageSegment seg = MessageSegment::text(src); + REQUIRE(seg.type() == seg_types::text); + REQUIRE(::std::string(seg) == src); + } + + SECTION("emoji") { + ::std::string src = "[CQ:emoji,id=127838]"; + + MessageSegment seg = MessageSegment::fromCQCode(src); + REQUIRE(seg.type() == seg_types::emoji); + REQUIRE((seg.value_map().at("id") == ::std::string("127838"))); + REQUIRE(::std::string(seg) == src); + + seg = MessageSegment::emoji(127838); + REQUIRE(seg.type() == seg_types::emoji); + REQUIRE((seg.value_map().at("id") == ::std::string("127838"))); + REQUIRE(::std::string(seg) == src); + } + + SECTION("face") { + ::std::string src = "[CQ:face,id=180]"; + + MessageSegment seg = MessageSegment::fromCQCode(src); + REQUIRE(seg.type() == seg_types::face); + REQUIRE((seg.value_map().at("id") == ::std::string("180"))); + REQUIRE(::std::string(seg) == src); + + seg = MessageSegment::face(180); + REQUIRE(seg.type() == seg_types::face); + REQUIRE((seg.value_map().at("id") == ::std::string("180"))); + REQUIRE(::std::string(seg) == src); + } + + SECTION("image") { + ::std::string src = "[CQ:image,file=AE9CE0B5A1FBA37F95718FDC547945CC.jpg]"; + + MessageSegment seg = MessageSegment::fromCQCode(src); + REQUIRE(seg.type() == seg_types::image); + REQUIRE(seg.value_map().at("file") == "AE9CE0B5A1FBA37F95718FDC547945CC.jpg"); + REQUIRE(::std::string(seg) == src); + + seg = MessageSegment::image("AE9CE0B5A1FBA37F95718FDC547945CC.jpg"); + REQUIRE(seg.type() == seg_types::image); + REQUIRE(::std::string(seg) == src); + } + + SECTION("record") { + ::std::string src = "[CQ:record,file=C5B0B7D8A515F1F652B0DEBB49335B9E.silk]"; + MessageSegment seg = MessageSegment::fromCQCode(src); + REQUIRE(seg.type() == seg_types::record); + REQUIRE(seg.value_map().at("file") == "C5B0B7D8A515F1F652B0DEBB49335B9E.silk"); + REQUIRE(::std::string(seg) == src); + + seg = MessageSegment::record("C5B0B7D8A515F1F652B0DEBB49335B9E.silk", true); + REQUIRE(seg.type() == seg_types::record); + REQUIRE(::std::string(seg) == "[CQ:record,file=C5B0B7D8A515F1F652B0DEBB49335B9E.silk,magic=true]"); + } } -TEST_CASE("Message ctor", "[message]") { - ::std::string src = - "[CQ:emoji,id=127838]" - "test" - "[CQ:face,id=180]" - "[CQ:image,file=AE9CE0B5A1FBA37F95718FDC547945CC.jpg]" - "[CQ:bface,p=204112,id=2DD591BFD449F584C709D276EC472E55]"; - cq::message::Message msg; - msg = src; - REQUIRE(msg.size() == 5); +TEST_CASE("MessageSegment equal", "[message segment]") { + using cq::message::MessageSegment; + using seg_types = MessageSegment::SegTypes; + + REQUIRE(MessageSegment::text("test") == MessageSegment::text("test")); + REQUIRE(MessageSegment::text("test") != MessageSegment::text("text")); + REQUIRE(MessageSegment::text("test") != MessageSegment::emoji(127838)); + REQUIRE(MessageSegment::emoji(127838) == MessageSegment::fromCQCode("[CQ:emoji,id=127838]")); +} + +TEST_CASE("MessageSegment bad construction", "[message segment]") { + using cq::message::MessageSegment; + ::std::string src = "[CQ:record,file=C5B0B7D8A515F1F652B0DEBB49335B9E.silk"; + try { + MessageSegment seg = MessageSegment::fromCQCode(src); + } catch (std::exception& err) { + REQUIRE(::std::string(err.what()) == "Invalid CQCode"); + } +} +TEST_CASE("Message ctor", "[message]") { + using namespace cq::message; using seg_types = cq::message::MessageSegment::SegTypes; - auto i = msg.begin(); - REQUIRE(i->type() == seg_types::emoji); - REQUIRE((i->value_map().at("id") == ::std::string("127838"))); - std::advance(i, 1); - REQUIRE(i->type() == seg_types::text); - REQUIRE(i->plain_text() == "test"); - std::advance(i, 1); - REQUIRE(i->type() == seg_types::face); - REQUIRE(i->value_map().at("id") == "180"); - std::advance(i, 1); - REQUIRE(i->type() == seg_types::image); - REQUIRE(i->value_map().at("file") == "AE9CE0B5A1FBA37F95718FDC547945CC.jpg"); - std::advance(i, 1); - REQUIRE(i->type() == seg_types::unimpl); - REQUIRE(i->plain_text() == "[CQ:bface,p=204112,id=2DD591BFD449F584C709D276EC472E55]"); + + SECTION("from string source") { + ::std::string src = + "[CQ:emoji,id=127838]" + "test" + "[CQ:face,id=180]" + "[CQ:image,file=AE9CE0B5A1FBA37F95718FDC547945CC.jpg]" + "[CQ:bface,p=204112,id=2DD591BFD449F584C709D276EC472E55]"; + + cq::message::Message msg; + msg = src; + REQUIRE(msg.size() == 5); + REQUIRE(::std::string(msg) == src); + + auto i = msg.begin(); + REQUIRE(i->type() == seg_types::emoji); + REQUIRE((i->value_map().at("id") == ::std::string("127838"))); + std::advance(i, 1); + REQUIRE(i->type() == seg_types::text); + REQUIRE(i->plain_text() == "test"); + std::advance(i, 1); + REQUIRE(i->type() == seg_types::face); + REQUIRE(i->value_map().at("id") == "180"); + std::advance(i, 1); + REQUIRE(i->type() == seg_types::image); + REQUIRE(i->value_map().at("file") == "AE9CE0B5A1FBA37F95718FDC547945CC.jpg"); + std::advance(i, 1); + REQUIRE(i->type() == seg_types::unimpl); + REQUIRE(i->plain_text() == "[CQ:bface,p=204112,id=2DD591BFD449F584C709D276EC472E55]"); + } + + SECTION("from MessageSegment") { + Message msg = MessageSegment::emoji(127838); + msg += MessageSegment::text("test"); + REQUIRE(msg.size() == 2); + + auto i = msg.begin(); + REQUIRE(i->type() == seg_types::emoji); + REQUIRE(i->value_map().at("id") == "127838"); + std::advance(i, 1); + REQUIRE(i->type() == seg_types::text); + REQUIRE(i->plain_text() == "test"); + } + + SECTION("from Message") { + Message msg = MessageSegment::emoji(127838); + REQUIRE(msg.size() == 1); + REQUIRE(msg.front().type() == seg_types::emoji); + REQUIRE(msg.front().value_map().at("id") == "127838"); + + SECTION("copy ctor") { + Message msg2 = msg; + REQUIRE(msg2.size() == 1); + REQUIRE(msg2.front().type() == seg_types::emoji); + REQUIRE(msg2.front().value_map().at("id") == "127838"); + } + + SECTION("move ctor") { + Message msg2 = msg; + Message msg3 = ::std::move(msg2); + REQUIRE(msg3.size() == 1); + REQUIRE(msg3.front().type() == seg_types::emoji); + REQUIRE(msg3.front().value_map().at("id") == "127838"); + } + } } -TEST_CASE("Message reduce and concat", "[message]") { +TEST_CASE("Message reduce", "[message]") { using namespace cq::message; - cq::message::Message msg; - msg.push_back(MessageSegment::text("1")); - msg.push_back(MessageSegment::text("2")); - msg.push_back(MessageSegment::text("34")); - msg.push_back(MessageSegment::text("56")); - msg.push_back(MessageSegment::text("78")); - msg.push_back(MessageSegment::text("9")); - REQUIRE(msg.size() == 6); - msg.reduce(); - REQUIRE(msg.size() == 1); - REQUIRE(msg.extract_plain_text() == "123456789"); - - msg.push_back(MessageSegment::text("A")); - msg.push_back(MessageSegment::text("BC")); - msg.push_back(MessageSegment::face(180)); - msg.push_back(MessageSegment::text("D")); - msg.push_back(MessageSegment::text("EF")); - REQUIRE(msg.size() == 6); - msg.reduce(); - REQUIRE(msg.size() == 3); - REQUIRE(msg.extract_plain_text() == "123456789ABCDEF"); - - msg.push_back(MessageSegment::text("GH")); - msg.push_back(MessageSegment::face(180)); - REQUIRE(msg.size() == 5); - msg.reduce(); - REQUIRE(msg.size() == 4); - REQUIRE(msg.extract_plain_text() == "123456789ABCDEFGH"); - - msg += "IJK"; - REQUIRE(msg.size() == 5); - REQUIRE(msg.extract_plain_text() == "123456789ABCDEFGHIJK"); - - msg += "LMN"; - REQUIRE(msg.size() == 5); - REQUIRE(msg.extract_plain_text() == "123456789ABCDEFGHIJKLMN"); - - cq::message::Message msg2 = - "[CQ:emoji,id=127838]" - "test" - "[CQ:face,id=180]" - "[CQ:image,file=AE9CE0B5A1FBA37F95718FDC547945CC.jpg]" - "[CQ:bface,p=204112,id=2DD591BFD449F584C709D276EC472E55]"; - REQUIRE(msg2.size() == 5); - msg += msg2; - REQUIRE(msg.size() == 10); - REQUIRE(msg.extract_plain_text() == "123456789ABCDEFGHIJKLMNtest"); + + SECTION("Message reduce all text") { + cq::message::Message msg; + msg.push_back(MessageSegment::text("1")); + msg.push_back(MessageSegment::text("2")); + msg.push_back(MessageSegment::text("34")); + msg.push_back(MessageSegment::text("56")); + msg.push_back(MessageSegment::text("78")); + msg.push_back(MessageSegment::text("9")); + REQUIRE(msg.size() == 6); + msg.reduce(); + REQUIRE(msg.size() == 1); + REQUIRE(msg.extract_plain_text() == "123456789"); + } + + SECTION("Message reduce ends with text") { + cq::message::Message msg; + msg.push_back(MessageSegment::text("ABC")); + msg.push_back(MessageSegment::face(180)); + msg.push_back(MessageSegment::text("D")); + msg.push_back(MessageSegment::text("EF")); + REQUIRE(msg.size() == 4); + msg.reduce(); + REQUIRE(msg.size() == 3); + REQUIRE(msg.extract_plain_text() == "ABCDEF"); + } + + SECTION("Message reduce ends with non-text") { + cq::message::Message msg; + msg.push_back(MessageSegment::text("ABC")); + msg.push_back(MessageSegment::text("D")); + msg.push_back(MessageSegment::dice()); + msg.push_back(MessageSegment::text("EF")); + msg.push_back(MessageSegment::face(180)); + REQUIRE(msg.size() == 5); + msg.reduce(); + REQUIRE(msg.size() == 4); + REQUIRE(msg.extract_plain_text() == "ABCDEF"); + } +} + +TEST_CASE("Message concat", "[message]") { + using namespace cq::message; + SECTION("concat string literal") { + SECTION("with text reduce") { + cq::message::Message msg = + "ABC" + "[CQ:emoji,id=127838]" + "[CQ:shake]" + "DEF"; + REQUIRE(msg.size() == 4); + REQUIRE(msg.extract_plain_text() == "ABCDEF"); + msg += "GHIJK"; + REQUIRE(msg.size() == 4); + REQUIRE(msg.extract_plain_text() == "ABCDEFGHIJK"); + } + + SECTION("no text reduce") { + cq::message::Message msg = + "ABCDEF" + "[CQ:emoji,id=127838]" + "[CQ:shake]"; + REQUIRE(msg.size() == 3); + REQUIRE(msg.extract_plain_text() == "ABCDEF"); + msg += "GHIJK"; + REQUIRE(msg.size() == 4); + REQUIRE(msg.extract_plain_text() == "ABCDEFGHIJK"); + } + } + + SECTION("string on the left") { + SECTION("with text reduce") { + Message msg1 = "GHIJK"; + REQUIRE(msg1.size() == 1); + REQUIRE(msg1.extract_plain_text() == "GHIJK"); + + ::std::string src = + "ABC" + "[CQ:emoji,id=127838]" + "[CQ:shake]" + "DEF"; + + Message msg = src + msg1; + REQUIRE(msg.size() == 4); + REQUIRE(msg.extract_plain_text() == "ABCDEFGHIJK"); + } + + SECTION("no text reduce") { + Message msg1 = "GHIJK"; + REQUIRE(msg1.size() == 1); + REQUIRE(msg1.extract_plain_text() == "GHIJK"); + + ::std::string src = + "ABCDEF" + "[CQ:emoji,id=127838]" + "[CQ:shake]"; + + Message msg = src + msg1; + REQUIRE(msg.size() == 4); + REQUIRE(msg.extract_plain_text() == "ABCDEFGHIJK"); + } + } + + SECTION("concat MessageSegment") { + SECTION("concat lvalue") { + cq::message::Message msg = + "ABC" + "[CQ:emoji,id=127838]" + "DEF"; + REQUIRE(msg.size() == 3); + REQUIRE(msg.extract_plain_text() == "ABCDEF"); + + auto seg1 = MessageSegment::text("GHIJK"); + msg += seg1; + REQUIRE(msg.size() == 4); + REQUIRE(msg.extract_plain_text() == "ABCDEFGHIJK"); + + auto seg2 = MessageSegment::dice(); + msg += seg2; + REQUIRE(msg.size() == 5); + REQUIRE(msg.extract_plain_text() == "ABCDEFGHIJK"); + } + + SECTION("concat rvalue") { + cq::message::Message msg = + "ABC" + "[CQ:emoji,id=127838]" + "DEF"; + REQUIRE(msg.size() == 3); + REQUIRE(msg.extract_plain_text() == "ABCDEF"); + + msg += MessageSegment::text("GHIJK"); + REQUIRE(msg.size() == 4); + REQUIRE(msg.extract_plain_text() == "ABCDEFGHIJK"); + + msg += MessageSegment::dice(); + REQUIRE(msg.size() == 5); + REQUIRE(msg.extract_plain_text() == "ABCDEFGHIJK"); + } + } + + SECTION("MessageSegment on the left") { + SECTION("concat string literal") { + MessageSegment seg = MessageSegment::emoji(127838); + REQUIRE(seg.type() == MessageSegment::SegTypes::emoji); + REQUIRE(seg.value_map().at("id") == "127838"); + + auto msg = seg + "ABCDEFGHIJK"; + REQUIRE(msg.size() == 2); + REQUIRE(msg.extract_plain_text() == "ABCDEFGHIJK"); + } + + SECTION("concat MessageSegment") { + MessageSegment seg = MessageSegment::emoji(127838); + REQUIRE(seg.type() == MessageSegment::SegTypes::emoji); + REQUIRE(seg.value_map().at("id") == "127838"); + + auto msg = seg + MessageSegment::text("ABCDEFGHIJK"); + REQUIRE(msg.size() == 2); + REQUIRE(msg.extract_plain_text() == "ABCDEFGHIJK"); + } + } + + SECTION("concat Message") { + cq::message::Message msg = + "ABC" + "[CQ:emoji,id=127838]" + "DEF"; + REQUIRE(msg.size() == 3); + REQUIRE(msg.extract_plain_text() == "ABCDEF"); + + cq::message::Message msg2 = + "[CQ:emoji,id=127838]" + "GHIJKLMN" + "[CQ:face,id=180]" + "[CQ:image,file=AE9CE0B5A1FBA37F95718FDC547945CC.jpg]" + "[CQ:bface,p=204112,id=2DD591BFD449F584C709D276EC472E55]"; + REQUIRE(msg2.size() == 5); + REQUIRE(msg2.extract_plain_text() == "GHIJKLMN"); + + msg += msg2; + REQUIRE(msg.size() == 8); + REQUIRE(msg.extract_plain_text() == "ABCDEFGHIJKLMN"); + } } TEST_CASE("Message equal", "[message]") { @@ -161,4 +416,22 @@ TEST_CASE("Message equal", "[message]") { REQUIRE(msg1 != msg2); msg2.push_back(MessageSegment::fromCQCode("[CQ:bface,p=204112,id=2DD591BFD449F584C709D276EC472E55]")); REQUIRE(msg1 == msg2); +} + +TEST_CASE("Message bad construction", "[message]") { + using namespace cq::message; + ::std::string src = + "[CQ:emoji,id=127838]" + "test" + "[CQ:face,id=" + "[CQ:image,file=AE9CE0B5A1FBA37F95718FDC547945CC.jpg]"; + + cq::message::Message msg; + + try { + msg = src; + } catch (std::exception& err) { + REQUIRE(::std::string(err.what()) == "Invalid CQCode"); + REQUIRE(msg.empty()); + } } \ No newline at end of file