From 24898ebdcf0e0fe59a6747a0877e532a4ae812a9 Mon Sep 17 00:00:00 2001 From: shxyke Date: Sun, 26 Mar 2023 13:07:31 +0000 Subject: [PATCH] refactor: use libcpr instead of curl --- CMakeLists.txt | 3 +- resource/config.json | 20 ++- src/MaaCore/Config/GeneralConfig.cpp | 14 ++ src/MaaCore/Config/GeneralConfig.h | 12 ++ src/MaaCore/MaaCore.vcxproj | 8 +- .../Task/Fight/StageDropsTaskPlugin.cpp | 6 +- .../Task/Miscellaneous/AutoRecruitTask.cpp | 6 +- src/MaaCore/Task/ReportDataTask.cpp | 109 +++++++------ src/MaaCore/Task/ReportDataTask.h | 29 ++-- src/MaaCore/Utils/Http.hpp | 146 ------------------ 10 files changed, 137 insertions(+), 216 deletions(-) delete mode 100644 src/MaaCore/Utils/Http.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d9400afbd..e081396768 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,8 +74,9 @@ find_package(ZLIB REQUIRED) find_package(MaaDerpLearning REQUIRED) find_package(asio REQUIRED) find_package(ONNXRuntime) +find_package(cpr CONFIG REQUIRED) -target_link_libraries(MaaCore ${OpenCV_LIBS} MaaDerpLearning asio::asio ZLIB::ZLIB ONNXRuntime::ONNXRuntime header_only_libraries) +target_link_libraries(MaaCore ${OpenCV_LIBS} MaaDerpLearning asio::asio ZLIB::ZLIB ONNXRuntime::ONNXRuntime cpr::cpr header_only_libraries) if(WIN32) target_link_libraries(MaaCore ws2_32) diff --git a/resource/config.json b/resource/config.json index 69177f847a..0310e1f395 100644 --- a/resource/config.json +++ b/resource/config.json @@ -28,12 +28,26 @@ "penguinReport": { "Doc": "企鹅物流汇报: https://penguin-stats.cn/", "cmdFormat": "curl -H \"Content-Type: application/json\" -s -S -m 10 -i -d \"[body]\" \"https://penguin-stats.io/PenguinStats/api/v2/report\" --ssl-no-revoke --http1.1 [extra]", - "cmdFormat_Doc": "命令格式,想打印详细信息可以尝试添加 -v -i" + "cmdFormat_Doc": "命令格式,想打印详细信息可以尝试添加 -v -i", + "cprFormat": { + "url": "https://penguin-stats.io/PenguinStats/api/v2/report", + "timeout": 10000, + "headers": { + "Content-Type": "application/json" + } + } }, "yituliuReport": { "Doc": "一图流汇报:https://yituliu.site/maarecruitdata", "cmdFormat": "curl -H \"Content-Type: application/json\" -s -S -m 5 -i -d \"[body]\" \"https://houduan.yituliu.site/tool/recruitUpload\" --ssl-no-revoke --http1.1 [extra]", - "cmdFormat_Doc": "命令格式,想打印详细信息可以尝试添加 -v -i" + "cmdFormat_Doc": "命令格式,想打印详细信息可以尝试添加 -v -i", + "cprFormat": { + "url": "https://houduan.yituliu.site/tool/recruitUpload", + "timeout": 5000, + "headers": { + "Content-Type": "application/json" + } + } }, "depotExportTemplate": { "Doc": "仓库识别导出结果模板", @@ -129,4 +143,4 @@ "screencapEncode": "[Adb] -s [AdbSerial] exec-out \"screencap -p 2>/tmp/maa-assistant-arknights.log\"" } ] -} +} \ No newline at end of file diff --git a/src/MaaCore/Config/GeneralConfig.cpp b/src/MaaCore/Config/GeneralConfig.cpp index b391c5f675..418fe1a8e6 100644 --- a/src/MaaCore/Config/GeneralConfig.cpp +++ b/src/MaaCore/Config/GeneralConfig.cpp @@ -25,7 +25,21 @@ bool asst::GeneralConfig::parse(const json::value& json) } } m_options.penguin_report.cmd_format = options_json.get("penguinReport", "cmdFormat", std::string()); + m_options.penguin_report.cpr_format.url = options_json.get("penguinReport", "cprFormat", "url", std::string()); + m_options.penguin_report.cpr_format.timeout = options_json.get("penguinReport", "cprFormat", "timeout", 10000); + if (auto headers = options_json.at("penguinReport").at("cprFormat").find("headers")) { + for (const auto& [key, value] : *headers) { + m_options.penguin_report.cpr_format.headers.emplace(key, value.as_string()); + } + } m_options.yituliu_report.cmd_format = options_json.get("yituliuReport", "cmdFormat", std::string()); + m_options.yituliu_report.cpr_format.url = options_json.get("yituliuReport", "cprFormat", "url", std::string()); + m_options.yituliu_report.cpr_format.timeout = options_json.get("yituliuReport", "cprFormat", "timeout", 5000); + if (auto headers = options_json.at("yituliuReport").at("cprFormat").find("headers")) { + for (const auto& [key, value] : *headers) { + m_options.yituliu_report.cpr_format.headers.emplace(key, value.as_string()); + } + } m_options.depot_export_template.ark_planner = options_json.get("depotExportTemplate", "arkPlanner", std::string()); } diff --git a/src/MaaCore/Config/GeneralConfig.h b/src/MaaCore/Config/GeneralConfig.h index f7c616845f..0da5a1b586 100644 --- a/src/MaaCore/Config/GeneralConfig.h +++ b/src/MaaCore/Config/GeneralConfig.h @@ -14,6 +14,12 @@ namespace asst struct PenguinReportCfg // 企鹅物流数据汇报 的配置 { std::string cmd_format; // 命令格式 + struct cpr_format + { + std::string url; // 上传地址 + std::unordered_map headers; // 请求头 + int timeout = 0; // 超时时间 + } cpr_format; }; struct DepotExportTemplate @@ -24,6 +30,12 @@ namespace asst struct yituliuReportCfg // 一图流大数据汇报 的配置 { std::string cmd_format; // 命令格式 + struct cpr_format + { + std::string url; // 上传地址 + std::unordered_map headers; // 请求头 + int timeout = 0; // 超时时间 + } cpr_format; }; struct Options diff --git a/src/MaaCore/MaaCore.vcxproj b/src/MaaCore/MaaCore.vcxproj index 565bbb2444..0001b1a617 100644 --- a/src/MaaCore/MaaCore.vcxproj +++ b/src/MaaCore/MaaCore.vcxproj @@ -402,7 +402,7 @@ true true true - MaaDerpLearning.lib;opencv_world4.lib;zlib.lib;ws2_32.lib;onnxruntime.lib;%(AdditionalDependencies) + MaaDerpLearning.lib;opencv_world4.lib;zlib.lib;ws2_32.lib;onnxruntime.lib;cpr.lib;libcurl.lib;crypt32.lib;%(AdditionalDependencies) RequireAdministrator true msvcrt.lib @@ -449,7 +449,7 @@ true true true - MaaDerpLearning.lib;opencv_world4.lib;zlib.lib;ws2_32.lib;onnxruntime.lib;%(AdditionalDependencies) + MaaDerpLearning.lib;opencv_world4.lib;zlib.lib;ws2_32.lib;onnxruntime.lib;cpr.lib;libcurl.lib;crypt32.lib;%(AdditionalDependencies) RequireAdministrator true msvcrt.lib @@ -499,7 +499,7 @@ true true true - MaaDerpLearning.lib;opencv_world4.lib;zlib.lib;ws2_32.lib;onnxruntime.lib;%(AdditionalDependencies) + MaaDerpLearning.lib;opencv_world4.lib;zlib.lib;ws2_32.lib;onnxruntime.lib;cpr.lib;libcurl.lib;crypt32.lib;%(AdditionalDependencies) RequireAdministrator true msvcrt.lib @@ -549,7 +549,7 @@ true true true - MaaDerpLearning.lib;opencv_world4.lib;zlib.lib;ws2_32.lib;onnxruntime.lib;%(AdditionalDependencies) + MaaDerpLearning.lib;opencv_world4.lib;zlib.lib;ws2_32.lib;onnxruntime.lib;cpr.lib;libcurl.lib;crypt32.lib;%(AdditionalDependencies) RequireAdministrator true msvcrt.lib diff --git a/src/MaaCore/Task/Fight/StageDropsTaskPlugin.cpp b/src/MaaCore/Task/Fight/StageDropsTaskPlugin.cpp index a0f8064402..634d1f640e 100644 --- a/src/MaaCore/Task/Fight/StageDropsTaskPlugin.cpp +++ b/src/MaaCore/Task/Fight/StageDropsTaskPlugin.cpp @@ -256,9 +256,9 @@ void asst::StageDropsTaskPlugin::upload_to_penguin() body["source"] = UploadDataSource; body["version"] = Version; - std::string extra_param; + std::unordered_map extra_headers; if (!m_penguin_id.empty()) { - extra_param = "-H \"authorization: PenguinID " + m_penguin_id + "\""; + extra_headers.insert({ "authorization", "PenguinID " + m_penguin_id }); } if (!m_report_penguin_task_ptr) { @@ -267,7 +267,7 @@ void asst::StageDropsTaskPlugin::upload_to_penguin() m_report_penguin_task_ptr->set_report_type(ReportType::PenguinStats) .set_body(body.to_string()) - .set_extra_param(extra_param) + .set_extra_headers(extra_headers) .set_retry_times(5) .run(); } diff --git a/src/MaaCore/Task/Miscellaneous/AutoRecruitTask.cpp b/src/MaaCore/Task/Miscellaneous/AutoRecruitTask.cpp index 12e4c8b8d4..8ef52de736 100644 --- a/src/MaaCore/Task/Miscellaneous/AutoRecruitTask.cpp +++ b/src/MaaCore/Task/Miscellaneous/AutoRecruitTask.cpp @@ -713,9 +713,9 @@ void asst::AutoRecruitTask::upload_to_penguin(Rng&& tags) body["source"] = UploadDataSource; body["version"] = Version; - std::string extra_param; + std::unordered_map extra_headers; if (!m_penguin_id.empty()) { - extra_param = "-H \"authorization: PenguinID " + m_penguin_id + "\""; + extra_headers = { { "authorization", "PenguinID " + m_penguin_id } }; } if (!m_report_penguin_task_ptr) { @@ -724,7 +724,7 @@ void asst::AutoRecruitTask::upload_to_penguin(Rng&& tags) m_report_penguin_task_ptr->set_report_type(ReportType::PenguinStats) .set_body(body.to_string()) - .set_extra_param(extra_param) + .set_extra_headers(extra_headers) .set_retry_times(5) .run(); } diff --git a/src/MaaCore/Task/ReportDataTask.cpp b/src/MaaCore/Task/ReportDataTask.cpp index 5c0efb6d5e..77ee49d8e6 100644 --- a/src/MaaCore/Task/ReportDataTask.cpp +++ b/src/MaaCore/Task/ReportDataTask.cpp @@ -29,9 +29,9 @@ asst::ReportDataTask& asst::ReportDataTask::set_body(std::string body) return *this; } -asst::ReportDataTask& asst::ReportDataTask::set_extra_param(std::string extra_param) +asst::ReportDataTask& asst::ReportDataTask::set_extra_headers(std::unordered_map headers) { - m_extra_param = std::move(extra_param); + m_extra_headers = std::move(headers); return *this; } @@ -83,17 +83,16 @@ void asst::ReportDataTask::report_to_penguin() key_ss << "MAA" << std::hex << tick << rand; std::string key = key_ss.str(); Log.info("X-Penguin-Idempotency-Key:", key); - m_extra_param += " -H \"X-Penguin-Idempotency-Key: " + std::move(key) + "\""; constexpr int DefaultBackoff = 10 * 1000; // 10s int backoff = DefaultBackoff; - auto penguin_success_cond = [](const http::Response& response) -> bool { return response.success(); }; - auto penguin_retry_cond = [&](const http::Response& response) -> bool { - if (!response.status_code()) { + auto penguin_success_cond = [](const cpr::Response& response) -> bool { return response.status_code == 200; }; + auto penguin_retry_cond = [&](const cpr::Response& response) -> bool { + if (!response.status_code) { return true; } - if (response.status_5xx()) { + if (response.status_code >= 500 && response.status_code < 600) { backoff = static_cast(backoff * 1.5); sleep(backoff); return true; @@ -101,19 +100,33 @@ void asst::ReportDataTask::report_to_penguin() return false; }; + std::string url = Config.get_options().penguin_report.cpr_format.url; + int timeout = Config.get_options().penguin_report.cpr_format.timeout; + cpr::Header headers {}; + + for (const auto& [header_key, value] : Config.get_options().penguin_report.cpr_format.headers) { + headers.emplace(header_key, value); + } + + for (const auto& field : m_extra_headers) { + headers.emplace(field); + } + + headers.emplace("X-Penguin-Idempotency-Key", std::move(key)); + constexpr std::string_view PenguinSubtaskName = "ReportToPenguinStats"; - const std::string& cmd_format = Config.get_options().penguin_report.cmd_format; - http::Response response = report(PenguinSubtaskName, cmd_format, penguin_success_cond, penguin_retry_cond, false); + cpr::Response response = + report(PenguinSubtaskName, url, headers, timeout, penguin_success_cond, penguin_retry_cond, false); auto proc_response_id = [&]() { - if (auto penguinid_opt = response.find_header("x-penguin-set-penguinid")) [[unlikely]] { + if (response.header.contains("x-penguin-set-penguinid")) [[unlikely]] { json::value id_info = basic_info_with_what("PenguinId"); - id_info["details"]["id"] = std::string(penguinid_opt.value()); + id_info["details"]["id"] = response.header["x-penguin-set-penguinid"]; callback(AsstMsg::SubTaskExtraInfo, id_info); } }; - if (response.success()) { + if (response.status_code == 200) { proc_response_id(); return; } @@ -121,16 +134,16 @@ void asst::ReportDataTask::report_to_penguin() // 重新向企鹅物流统计的 CN 域名发送数据 constexpr std::string_view Penguin_IO = "https://penguin-stats.io"; constexpr std::string_view Penguin_CN = "https://penguin-stats.cn"; - if (cmd_format.find(Penguin_IO) == std::string::npos) { + if (url.find(Penguin_IO) == std::string::npos) { return; } Log.info("Re-report to penguin-stats.cn", Penguin_CN); - std::string new_cmd_format = utils::string_replace_all(cmd_format, Penguin_IO, Penguin_CN); + std::string new_url = utils::string_replace_all(url, Penguin_IO, Penguin_CN); backoff = DefaultBackoff; - response = report(PenguinSubtaskName, new_cmd_format, penguin_success_cond, penguin_retry_cond); + response = report(PenguinSubtaskName, new_url, headers, timeout, penguin_success_cond, penguin_retry_cond); - if (response.success()) { + if (response.status_code == 200) { proc_response_id(); return; } @@ -141,13 +154,24 @@ void asst::ReportDataTask::report_to_yituliu() LogTraceFunction; constexpr std::string_view YituliuSubtaskName = "ReportToYituliu"; - const std::string& cmd_format = Config.get_options().yituliu_report.cmd_format; - report(YituliuSubtaskName, cmd_format); + const std::string& url = Config.get_options().yituliu_report.cpr_format.url; + int timeout = Config.get_options().yituliu_report.cpr_format.timeout; + cpr::Header headers = {}; + + for (const auto& [header_key, value] : Config.get_options().yituliu_report.cpr_format.headers) { + headers.emplace(header_key, value); + } + + for (const auto& field : m_extra_headers) { + headers.emplace(field); + } + + report(YituliuSubtaskName, url, headers, timeout); } -asst::http::Response asst::ReportDataTask::report(std::string_view subtask, const std::string& format, - HttpResponsePred success_cond, HttpResponsePred retry_cond, - bool callback_on_failure) +cpr::Response asst::ReportDataTask::report(std::string_view subtask, const std::string& url, const cpr::Header& headers, + const int timeout, HttpResponsePred success_cond, + HttpResponsePred retry_cond, bool callback_on_failure) { LogTraceFunction; @@ -155,9 +179,20 @@ asst::http::Response asst::ReportDataTask::report(std::string_view subtask, cons cb_info["subtask"] = std::string(subtask); callback(AsstMsg::SubTaskStart, cb_info); - http::Response response; + Log.info("Report url: ", url); + for (const auto& [key, value] : headers) { + Log.info("Report header: ", key, ":", value); + } + Log.info("Report body: ", m_body); + + cpr::Response response; for (int i = 0; i <= m_retry_times; ++i) { - response = escape_and_request(format); + response = cpr::Post(cpr::Url { url }, cpr::Body { m_body }, headers, cpr::Timeout { timeout }); + Log.info("Report response status code: ", response.status_code); + for (const auto& [key, value] : response.header) { + Log.info("Report response header: ", key, ":", value); + } + Log.info("Report response: ", response.text); if (success_cond(response)) { callback(AsstMsg::SubTaskCompleted, cb_info); return response; @@ -166,7 +201,7 @@ asst::http::Response asst::ReportDataTask::report(std::string_view subtask, cons break; } else { - Log.trace("retrying... | why:", response.get_last_error(), "status_code:", response.status_code()); + Log.trace("retrying... | why:", response.error.message, "status_code:", response.status_code); } if (need_exit()) { break; @@ -176,10 +211,10 @@ asst::http::Response asst::ReportDataTask::report(std::string_view subtask, cons if (callback_on_failure) { cb_info["why"] = "上报失败"; cb_info["details"] = json::object { - { "error", response.get_last_error() }, - { "status_code", response.status_code() }, - { "status_code_info", std::string(response.status_code_info()) }, - { "response", std::string(response.body()) }, + { "error", response.error.message }, + { "status_code", response.status_code }, + { "status_code_info", response.reason }, + { "response", response.text }, }; callback(AsstMsg::SubTaskError, cb_info); @@ -187,21 +222,3 @@ asst::http::Response asst::ReportDataTask::report(std::string_view subtask, cons return response; } - -asst::http::Response asst::ReportDataTask::escape_and_request(const std::string& format) -{ - std::string body_escape = utils::string_replace_all(m_body, "\"", "\\\""); - -#ifdef _WIN32 - body_escape = utils::utf8_to_unicode_escape(body_escape); -#endif - - std::string cmd_line = - utils::string_replace_all(format, { { "[body]", body_escape }, { "[extra]", m_extra_param } }); - - Log.info("request:\n" + cmd_line); - std::string response = utils::call_command(cmd_line); - Log.info("response:\n" + response); - - return response; -} diff --git a/src/MaaCore/Task/ReportDataTask.h b/src/MaaCore/Task/ReportDataTask.h index 27dedb3e79..655191a98f 100644 --- a/src/MaaCore/Task/ReportDataTask.h +++ b/src/MaaCore/Task/ReportDataTask.h @@ -1,7 +1,15 @@ #pragma once #include "AbstractTask.h" -#include "Utils/Http.hpp" + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4244) +#endif +#include +#ifdef _MSC_VER +#pragma warning(pop) +#endif #include #include @@ -30,28 +38,29 @@ namespace asst ReportDataTask& set_report_type(ReportType type); ReportDataTask& set_body(std::string body); - ReportDataTask& set_extra_param(std::string extra_param); + ReportDataTask& set_extra_headers(std::unordered_map headers); protected: - using HttpResponsePred = std::function; + using HttpResponsePred = std::function; virtual bool _run() override; virtual void callback(AsstMsg msg, const json::value& detail) override; void report_to_penguin(); void report_to_yituliu(); - http::Response escape_and_request(const std::string& format); - http::Response report( - std::string_view subtask, const std::string& format, - HttpResponsePred success_cond = [](const http::Response& response) -> bool { return response.success(); }, - HttpResponsePred retry_cond = [](const http::Response& response) -> bool { - return !response.status_code() || response.status_5xx(); + cpr::Response report( + std::string_view subtask, const std::string& url, const cpr::Header& headers, const int timeout, + HttpResponsePred success_cond = [](const cpr::Response& response) -> bool { + return response.status_code == 200; + }, + HttpResponsePred retry_cond = [](const cpr::Response& response) -> bool { + return !response.status_code || (response.status_code >= 500 && response.status_code < 600); }, bool callback_on_failure = true); ReportType m_report_type = ReportType::Invalid; std::string m_body; - std::string m_extra_param; + std::unordered_map m_extra_headers; std::vector> m_upload_pending; TaskCallback m_task_callback = nullptr; diff --git a/src/MaaCore/Utils/Http.hpp b/src/MaaCore/Utils/Http.hpp deleted file mode 100644 index 5015e8ac53..0000000000 --- a/src/MaaCore/Utils/Http.hpp +++ /dev/null @@ -1,146 +0,0 @@ -#pragma once - -#include "Ranges.hpp" -#include "StringMisc.hpp" - -#include -#include -#include -#include -#include - -namespace asst::http -{ - class Response - { - private: - static const inline std::unordered_set AcceptedProtocolVersions = { "HTTP/1.1", "HTTP/2.0", - "HTTP/2" }; - std::string m_raw_data; - std::string m_last_error; - unsigned m_status_code = 0; - std::string_view m_protocol_version; - std::string_view m_status_code_info; - std::string_view m_body; - std::unordered_map m_headers; - - bool analyze_status_line(std::string_view status_line) - { - size_t word_count = 0; - for (std::string_view word : - views::split(status_line, ' ') | views::transform([&](const auto& rng) -> std::string_view { - return utils::make_string_view(rng); - })) { - ++word_count; - if (word_count == 1) { - if (!AcceptedProtocolVersions.contains(word)) { - m_last_error = "unsupported protocol version`" + std::string(word) + "`"; - } - m_protocol_version = word; - } - else if (word_count == 2) { - if (word.length() != 3) { - m_last_error = "status code length is not 3"; - return false; - } - if (!ranges::all_of(word, [](char c) -> bool { return std::isdigit(c); })) { - return false; - } - m_status_code = std::atoi(word.data()); - } - else { - m_status_code_info = utils::make_string_view(word.begin(), status_line.end()); - return true; - } - } - if (word_count == 2) { - m_status_code_info = ""; - return true; - } - m_last_error = "status line too short"; - return false; - } - bool analyze_headers_line(std::string_view status_line) - { - size_t colon_pos = status_line.find(':'); - if (colon_pos == status_line.npos) { - m_last_error = "cannot decode header `" + std::string(status_line) + "`"; - return false; - } - // 把 key 转换为小写 - for (auto& c : status_line.substr(0, colon_pos)) { - const_cast(c) = static_cast(std::tolower(c)); - } - m_headers[status_line.substr(0, colon_pos)] = - status_line.substr(colon_pos + 1 + status_line.substr(colon_pos + 1).find_first_not_of(' ')); - return true; - } - - public: - template - requires std::is_constructible_v - Response(Args&&... args) : m_raw_data(std::forward(args)...) - { - constexpr std::string_view delim = "\r\n"; - std::string_view this_view = m_raw_data; - bool is_status_line = true; - bool is_data_line = false; - auto make_view = [&](const auto& rng) -> std::string_view { return utils::make_string_view(rng); }; - for (std::string_view line : views::split(this_view, delim) | views::transform(make_view)) { - // status - if (is_status_line) { - is_status_line = false; - analyze_status_line(line); - } - // headers - else if (!is_data_line) { - if (!line.empty()) { - analyze_headers_line(line); - } - else { - is_data_line = true; // 当前行是空行,则下一行开始都是 data 段 - } - } - // data - else { - m_body = utils::make_string_view(line.begin(), this_view.end()); - break; - } - } - } - - Response() = default; - ~Response() noexcept = default; - - Response(Response&&) noexcept = default; - Response& operator=(Response&&) noexcept = default; - - // string_view 类型的成员在复制时,其指向的 string 可能被析构 - // 虽然应该有更高效的复制方式,不过在这里还是直接调用 Args&&... 的构造函数 - Response(const Response&) = delete; - Response& operator=(const Response&) = delete; - - unsigned status_code() const noexcept { return m_status_code; } - std::string_view protocol_version() const noexcept { return m_protocol_version; } - std::string_view status_code_info() const noexcept { return m_status_code_info; } - std::string_view body() const noexcept { return m_body; } - std::optional find_header(std::string_view key) const - { - if (auto iter = m_headers.find(key); iter != m_headers.cend()) { - return iter->second; - } - else { - return std::nullopt; - } - } - const auto& headers() const noexcept { return m_headers; } - const std::string& get_last_error() const noexcept { return m_last_error; } - operator std::string() const noexcept { return m_raw_data; } - - bool success() const noexcept { return m_status_code == 200; } - bool status_2xx() const noexcept { return m_status_code >= 200 && m_status_code < 300; } - bool status_3xx() const noexcept { return m_status_code >= 300 && m_status_code < 400; } - bool status_4xx() const noexcept { return m_status_code >= 400 && m_status_code < 500; } - bool status_5xx() const noexcept { return m_status_code >= 500 && m_status_code < 600; } - }; -} // namespace asst::http