From e9c3cde9de8f63e09f36b0a54e8ee35216fc3258 Mon Sep 17 00:00:00 2001 From: root <root@Ubuntu.amass.fun> Date: Thu, 27 Feb 2025 12:57:55 +0000 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0live2d=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/c_cpp_properties.json | 17 +++ Application.cpp | 80 ++++++++++++- Application.h | 26 +++- Base/Messages.h | 11 ++ CMakeLists.txt | 13 ++ HttpSession.cpp | 119 +++++++++++++++++++ HttpSession.h | 51 ++++++++ ResponseUtility.cpp | 51 ++++++++ ResponseUtility.h | 19 +++ ServiceLogic.cpp | 77 ++++++++++++ ServiceLogic.h | 53 +++++++++ ServiceLogic.inl | 41 +++++++ Settings.cpp | 13 ++ Settings.h | 8 ++ WeChat/Corporation/Context.cpp | 128 ++++++++++++++++++++ WeChat/Corporation/Context.h | 49 ++++++++ WeChat/OfficialAccount/Context.cpp | 183 +++++++++++++++++++++++++++++ WeChat/OfficialAccount/Context.h | 47 ++++++++ WeChat/OfficialAccount/Session.cpp | 101 ++++++++++++++++ WeChat/OfficialAccount/Session.h | 53 +++++++++ resources/older.service | 14 +++ 21 files changed, 1151 insertions(+), 3 deletions(-) create mode 100644 .vscode/c_cpp_properties.json create mode 100644 Base/Messages.h create mode 100644 HttpSession.cpp create mode 100644 HttpSession.h create mode 100644 ResponseUtility.cpp create mode 100644 ResponseUtility.h create mode 100644 ServiceLogic.cpp create mode 100644 ServiceLogic.h create mode 100644 ServiceLogic.inl create mode 100644 WeChat/Corporation/Context.cpp create mode 100644 WeChat/Corporation/Context.h create mode 100644 WeChat/OfficialAccount/Context.cpp create mode 100644 WeChat/OfficialAccount/Context.h create mode 100644 WeChat/OfficialAccount/Session.cpp create mode 100644 WeChat/OfficialAccount/Session.h create mode 100644 resources/older.service diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..3abccb4 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,17 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**", + "/opt/Libraries/boost_1_87_0/include" + ], + "defines": [], + "compilerPath": "/usr/bin/gcc", + "cStandard": "c17", + "cppStandard": "gnu++17", + "intelliSenseMode": "linux-gcc-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/Application.cpp b/Application.cpp index 651c08d..76176cd 100644 --- a/Application.cpp +++ b/Application.cpp @@ -1,22 +1,100 @@ #include "Application.h" +#include "Base/Messages.h" #include "Core/IoContext.h" +#include "Core/MessageManager.h" #include "Core/Singleton.h" +#include "HttpSession.h" +#include "Router/router.hpp" +#include "ServiceLogic.h" #include "Settings.h" +#include "WeChat/Corporation/Context.h" +#include <boost/asio/strand.hpp> namespace Older { -Application::Application() { + +class ApplicationPrivate { +public: + std::shared_ptr<boost::urls::router<Application::RequestHandler>> router; + std::shared_ptr<boost::asio::ip::tcp::acceptor> acceptor; +}; + +Application::Application() : m_d{new ApplicationPrivate()} { + using namespace boost::urls; using namespace Core; + m_d->router = std::make_shared<router<RequestHandler>>(); + + m_messageManager = Singleton<MessageManager>::construct(); m_settings = Singleton<Settings>::construct(); m_ioContext = Singleton<IoContext>::construct(m_settings->threads()); + + m_corporationContext = Singleton<WeChat::Corporation::Context>::construct(*m_ioContext->ioContext()); + m_corporationContext->start(); + + m_d->router->insert("/api/v1/notify", [this](HttpSession &session, const Request &request, const matches &matches) { + auto manager = Singleton<MessageManager>::instance(); + if (manager) { + manager->publish<NotifyServerChan>(request); + } + session.reply(ServiceLogic::make_200<boost::beast::http::string_body>(request, "notify successed.\n", "text/html")); + }); } boost::asio::io_context &Application::ioContext() { return *m_ioContext->ioContext(); } +void Application::startAcceptHttpConnections(const std::string &address, uint16_t port) { + m_d->acceptor = std::make_shared<boost::asio::ip::tcp::acceptor>(*m_ioContext->ioContext()); + boost::beast::error_code error; + boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::make_address(address), port); + m_d->acceptor->open(endpoint.protocol(), error); + if (error) { + LOG(error) << error.message(); + return; + } + m_d->acceptor->set_option(boost::asio::socket_base::reuse_address(true), error); + if (error) { + LOG(error) << error.message(); + return; + } + m_d->acceptor->bind(endpoint, error); + if (error) { + LOG(error) << error.message(); + return; + } + m_d->acceptor->listen(boost::asio::socket_base::max_listen_connections, error); + if (error) { + LOG(error) << error.message(); + return; + } + asyncAcceptHttpConnections(); +} + +void Application::insertUrl(std::string_view url, RequestHandler &&handler) { + m_d->router->insert(url, std::move(handler)); +} + int Application::exec() { + using namespace Core; + auto settings = Singleton<Settings>::instance(); + ServiceLogic::live2dBackend(); + startAcceptHttpConnections(settings->server(), settings->port()); m_ioContext->run(); return 0; } + +void Application::asyncAcceptHttpConnections() { + auto socket = std::make_shared<boost::asio::ip::tcp::socket>(boost::asio::make_strand(*m_ioContext->ioContext())); + m_d->acceptor->async_accept(*socket, [self{shared_from_this()}, socket](const boost::system::error_code &error) { + if (error) { + if (error == boost::asio::error::operation_aborted) return; + LOG(error) << error.message(); + } else { + auto session = std::make_shared<HttpSession>(std::move(*socket), self->m_d->router); + session->run(); + } + self->asyncAcceptHttpConnections(); + }); +} } // namespace Older diff --git a/Application.h b/Application.h index f67b1a5..885b6b2 100644 --- a/Application.h +++ b/Application.h @@ -1,26 +1,48 @@ #ifndef __APPLICATION_H__ #define __APPLICATION_H__ -#include <memory> +#include "Router/matches.hpp" #include <boost/asio/io_context.hpp> +#include <boost/beast/http/string_body.hpp> +#include <memory> namespace Core { class IoContext; +class MessageManager; +} // namespace Core + +namespace WeChat { +namespace Corporation { +class Context; } +} // namespace WeChat namespace Older { class Settings; +class ApplicationPrivate; +class HttpSession; -class Application { +class Application : public std::enable_shared_from_this<Application> { public: + using Pointer = std::shared_ptr<Application>; + using Request = boost::beast::http::request<boost::beast::http::string_body>; + using RequestHandler = std::function<void(HttpSession &, const Request &, const boost::urls::matches &)>; Application(); boost::asio::io_context &ioContext(); + void startAcceptHttpConnections(const std::string &address, uint16_t port); + void insertUrl(std::string_view url, RequestHandler &&handler); int exec(); +protected: + void asyncAcceptHttpConnections(); + private: + ApplicationPrivate *m_d = nullptr; std::shared_ptr<Settings> m_settings; std::shared_ptr<Core::IoContext> m_ioContext; + std::shared_ptr<Core::MessageManager> m_messageManager; + std::shared_ptr<WeChat::Corporation::Context> m_corporationContext; }; } // namespace Older #endif // __APPLICATION_H__ \ No newline at end of file diff --git a/Base/Messages.h b/Base/Messages.h new file mode 100644 index 0000000..145f9fa --- /dev/null +++ b/Base/Messages.h @@ -0,0 +1,11 @@ +#ifndef __MESSAGES_H__ +#define __MESSAGES_H__ + +#include "Core/MessageManager.h" +#include <boost/callable_traits/return_type.hpp> + +struct NotifyServerChan { + using Signature = void(const boost::beast::http::request<boost::beast::http::string_body> &); +}; + +#endif // __MESSAGES_H__ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 1039472..5c30bae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,24 @@ +find_package(Boost REQUIRED COMPONENTS json) add_subdirectory(/root/Projects/Kylin Kylin) add_executable(Older main.cpp + WeChat/Corporation/Context.h WeChat/Corporation/Context.cpp + Application.h Application.cpp + HttpSession.h HttpSession.cpp + ResponseUtility.h ResponseUtility.cpp + ServiceLogic.h ServiceLogic.inl ServiceLogic.cpp Settings.h Settings.cpp ) +target_include_directories(Older + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} +) + target_link_libraries(Older PRIVATE Kylin::Core + PRIVATE Kylin::Http + PRIVATE Kylin::Router + PRIVATE Boost::json ) \ No newline at end of file diff --git a/HttpSession.cpp b/HttpSession.cpp new file mode 100644 index 0000000..30b3816 --- /dev/null +++ b/HttpSession.cpp @@ -0,0 +1,119 @@ +#include "HttpSession.h" +#include "Core/Logger.h" +#include <boost/beast/http/read.hpp> +#include <boost/beast/version.hpp> +#include <boost/config.hpp> +#include <boost/stacktrace.hpp> +#include <boost/url/parse_path.hpp> +#include <boost/url/url_view.hpp> +#include <iostream> +#include <limits> + +namespace Older { +HttpSession::HttpSession(boost::asio::ip::tcp::socket &&socket, + const std::shared_ptr<boost::urls::router<RequestHandler>> &router) + : m_stream(std::move(socket)), m_router(router) { +} + +void HttpSession::run() { + doRead(); +} + +boost::beast::tcp_stream::executor_type HttpSession::executor() { + return m_stream.get_executor(); +} + +boost::asio::ip::tcp::socket HttpSession::releaseSocket() { + return m_stream.release_socket(); +} + +void HttpSession::errorReply(const Request &request, boost::beast::http::status status, boost::beast::string_view message) { + using namespace boost::beast; + // invalid route + http::response<http::string_body> res{status, request.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(request.keep_alive()); + res.body() = message; + res.prepare_payload(); + + reply(std::move(res)); +} + +void HttpSession::doRead() { + // Construct a new parser for each message + m_parser.emplace(); + + // Apply a reasonable limit to the allowed size + // of the body in bytes to prevent abuse. + m_parser->body_limit(std::numeric_limits<std::uint64_t>::max()); + m_parser->header_limit(std::numeric_limits<std::uint32_t>::max()); + m_buffer.clear(); + + // Set the timeout. + m_stream.expires_after(std::chrono::seconds(30)); + // clang-format off + boost::beast::http::async_read(m_stream, m_buffer, *m_parser, [self{shared_from_this()}](const boost::system::error_code &ec, std::size_t bytes_transferred) { + self->onRead(ec, bytes_transferred); + }); + // clang-format on +} + +void HttpSession::onRead(const boost::beast::error_code &error, std::size_t) { + using namespace boost::beast; + if (error) { + if (error == http::error::end_of_stream) { + boost::beast::error_code e; + m_stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_send, e); + } else if (error != boost::asio::error::operation_aborted) { + LOG(info) << error << " : " << error.message(); + } + return; + } else if (m_router.expired()) { + LOG(error) << "router is null."; + return; + } + + auto &request = m_parser->get(); + auto path = boost::urls::parse_path(request.target()); + if (!path) { + LOG(error) << request.target() << "failed, error: " << path.error().message(); + errorReply(request, http::status::bad_request, "Illegal request-target"); + return; + } + auto router = m_router.lock(); + boost::urls::matches matches; + auto handler = router->find(*path, matches); + if (handler) { + try { + (*handler)(*this, request, matches); + } catch (const std::exception &e) { + boost::stacktrace::stacktrace trace = boost::stacktrace::stacktrace::from_current_exception(); + LOG(error) << e.what() << ", trace:\n" << trace; + } + } else { + std::ostringstream oss; + oss << "The resource '" << request.target() << "' was not found."; + auto message = oss.str(); + errorReply(request, http::status::not_found, message); + LOG(error) << message; + } +} + +void HttpSession::onWrite(boost::beast::error_code ec, std::size_t, bool close) { + if (ec) { + if (ec == boost::asio::error::operation_aborted) return; + std::cerr << "write: " << ec.message() << "\n"; + } + + if (close) { + // This means we should close the connection, usually because + // the response indicated the "Connection: close" semantic. + m_stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec); + return; + } + + // Read another request + doRead(); +} +} // namespace Older diff --git a/HttpSession.h b/HttpSession.h new file mode 100644 index 0000000..039ac92 --- /dev/null +++ b/HttpSession.h @@ -0,0 +1,51 @@ +#ifndef HTTPSESSION_H +#define HTTPSESSION_H + +#include "Router/router.hpp" +#include <boost/beast/core/flat_buffer.hpp> +#include <boost/beast/core/tcp_stream.hpp> +#include <boost/beast/http/parser.hpp> +#include <boost/beast/http/string_body.hpp> +#include <boost/beast/http/write.hpp> +#include <cstdlib> +#include <memory> +#include <optional> + +namespace Older { + +/** Represents an established HTTP connection + */ +class HttpSession : public std::enable_shared_from_this<HttpSession> { + void doRead(); + void onWrite(boost::beast::error_code ec, std::size_t, bool close); + +public: + using Request = boost::beast::http::request<boost::beast::http::string_body>; + using RequestHandler = std::function<void(HttpSession &, const Request &, const boost::urls::matches &)>; + HttpSession(boost::asio::ip::tcp::socket &&socket, const std::shared_ptr<boost::urls::router<RequestHandler>> &router); + template <typename Response> + void reply(Response &&response) { + using ResponseType = typename std::decay_t<decltype(response)>; + auto sp = std::make_shared<ResponseType>(std::forward<decltype(response)>(response)); + boost::beast::http::async_write(m_stream, *sp, + [self = shared_from_this(), sp](boost::beast::error_code ec, std::size_t bytes) { + self->onWrite(ec, bytes, sp->need_eof()); + }); + } + void errorReply(const Request &request, boost::beast::http::status status, boost::beast::string_view message); + boost::beast::tcp_stream::executor_type executor(); + boost::asio::ip::tcp::socket releaseSocket(); + void run(); + +protected: + void onRead(const boost::beast::error_code &error, std::size_t); + +private: + boost::beast::tcp_stream m_stream; + std::weak_ptr<boost::urls::router<RequestHandler>> m_router; + boost::beast::flat_buffer m_buffer{std::numeric_limits<std::uint32_t>::max()}; + std::optional<boost::beast::http::request_parser<boost::beast::http::string_body>> m_parser; +}; +} // namespace Older + +#endif // HTTPSESSION_H diff --git a/ResponseUtility.cpp b/ResponseUtility.cpp new file mode 100644 index 0000000..dee717e --- /dev/null +++ b/ResponseUtility.cpp @@ -0,0 +1,51 @@ +#include "ResponseUtility.h" +#include "boost/beast.hpp" + +namespace ResponseUtility { + +std::string_view mimeType(std::string_view path) { + using boost::beast::iequals; + auto const ext = [&path] { + auto const pos = path.rfind("."); + if (pos == std::string_view::npos) return std::string_view{}; + return path.substr(pos); + }(); + if (iequals(ext, ".pdf")) return "Application/pdf"; + if (iequals(ext, ".htm")) return "text/html"; + if (iequals(ext, ".html")) return "text/html"; + if (iequals(ext, ".php")) return "text/html"; + if (iequals(ext, ".css")) return "text/css"; + if (iequals(ext, ".txt")) return "text/plain"; + if (iequals(ext, ".js")) return "application/javascript"; + if (iequals(ext, ".json")) return "application/json"; + if (iequals(ext, ".xml")) return "application/xml"; + if (iequals(ext, ".swf")) return "application/x-shockwave-flash"; + if (iequals(ext, ".flv")) return "video/x-flv"; + if (iequals(ext, ".png")) return "image/png"; + if (iequals(ext, ".jpe")) return "image/jpeg"; + if (iequals(ext, ".jpeg")) return "image/jpeg"; + if (iequals(ext, ".jpg")) return "image/jpeg"; + if (iequals(ext, ".gif")) return "image/gif"; + if (iequals(ext, ".bmp")) return "image/bmp"; + if (iequals(ext, ".ico")) return "image/vnd.microsoft.icon"; + if (iequals(ext, ".tiff")) return "image/tiff"; + if (iequals(ext, ".tif")) return "image/tiff"; + if (iequals(ext, ".svg")) return "image/svg+xml"; + if (iequals(ext, ".svgz")) return "image/svg+xml"; + return "application/text"; +} + +std::string pathCat(std::string_view base, std::string_view path) { + if (base.empty()) return std::string(path); + std::string result(base); + char constexpr path_separator = '/'; + if (result.back() == path_separator && path.front() == path_separator) { + result.resize(result.size() - 1); + } else if (result.back() != path_separator && path.front() != path_separator) { + result.append("/"); + } + result.append(path.data(), path.size()); + + return result; +} +} // namespace ResponseUtility diff --git a/ResponseUtility.h b/ResponseUtility.h new file mode 100644 index 0000000..c0ff08b --- /dev/null +++ b/ResponseUtility.h @@ -0,0 +1,19 @@ +#ifndef RESPONSEUTILITY_H +#define RESPONSEUTILITY_H + +#include <string_view> + +namespace ResponseUtility { +/** + * @brief Return a reasonable mime type based on the extension of a file. + */ +std::string_view mimeType(std::string_view path); + +/** + * @brief Append an HTTP rel-path to a local filesystem path.The returned path is normalized for the + * platform. + */ +std::string pathCat(std::string_view base, std::string_view path); +} // namespace ResponseUtility + +#endif // RESPONSEUTILITY_H diff --git a/ServiceLogic.cpp b/ServiceLogic.cpp new file mode 100644 index 0000000..7828fe1 --- /dev/null +++ b/ServiceLogic.cpp @@ -0,0 +1,77 @@ +#include "ServiceLogic.h" +#include "HttpSession.h" +#include "Settings.h" +#include <sstream> + +namespace ServiceLogic { +using namespace boost::beast; + +boost::beast::http::response<boost::beast::http::string_body> +serverError(const boost::beast::http::request<boost::beast::http::string_body> &request, std::string_view errorMessage) { + using namespace boost::beast; + http::response<http::string_body> res{http::status::internal_server_error, request.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(request.keep_alive()); + std::ostringstream oss; + oss << "An error occurred: '" << errorMessage << "'"; + res.body() = oss.str(); + res.prepare_payload(); + return res; +} + +http::response<http::string_body> badRequest(const http::request<http::string_body> &request, std::string_view why) { + http::response<http::string_body> res{http::status::bad_request, request.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(request.keep_alive()); + res.body() = std::string(why); + res.prepare_payload(); + return res; +} + +void live2dBackend() { + using namespace Core; + auto application = Singleton<Older::Application>::instance(); + application->insertUrl("/api/v1/live2d/{path*}", [](Older::HttpSession &session, const Older::Application::Request &request, + const boost::urls::matches &matches) { + auto settings = Singleton<Older::Settings>::instance(); + using namespace boost::beast; + boost::urls::url_view view(request.target()); + auto target = view.path(); + // LOG(info) << target; + if (target.find("..") != boost::beast::string_view::npos) { + session.reply(ServiceLogic::badRequest(request, "Illegal request-target")); + return; + } + std::string path = ResponseUtility::pathCat(settings->live2dModelsRoot(), matches["path"]); + if (target.back() == '/') path.append("index.html"); + if (std::filesystem::is_directory(path)) path.append("/index.html"); + boost::beast::error_code ec; + http::file_body::value_type body; + body.open(path.c_str(), boost::beast::file_mode::scan, ec); + if (ec == boost::beast::errc::no_such_file_or_directory) { + std::ostringstream oss; + oss << "The resource '" << target << "' was not found."; + LOG(error) << oss.str(); + session.errorReply(request, http::status::not_found, oss.str()); + return; + } else if (ec) { + session.reply(ServiceLogic::serverError(request, ec.message())); + return; + } + auto const size = body.size(); + http::response<http::file_body> res{std::piecewise_construct, std::make_tuple(std::move(body)), + std::make_tuple(http::status::ok, request.version())}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, ResponseUtility::mimeType(path)); + // res.set(http::field::access_control_allow_origin, "*"); + res.set(http::field::cache_control, "max-age=2592000"); + res.set(http::field::expires, "Fri, 22 Nov 2124 13:30:28 GMT"); + res.content_length(size); + res.keep_alive(request.keep_alive()); + session.reply(std::move(res)); + }); +} + +} // namespace ServiceLogic diff --git a/ServiceLogic.h b/ServiceLogic.h new file mode 100644 index 0000000..976311b --- /dev/null +++ b/ServiceLogic.h @@ -0,0 +1,53 @@ +#ifndef SERVICELOGIC_H +#define SERVICELOGIC_H + +#include "Application.h" +#include "ResponseUtility.h" +#include <boost/beast/http/message.hpp> +#include <boost/beast/http/string_body.hpp> +#include <boost/beast/http/vector_body.hpp> +#include <boost/beast/version.hpp> +#include <boost/uuid/uuid_generators.hpp> +#include <boost/uuid/uuid_io.hpp> +#include <fstream> + +using StringRequest = boost::beast::http::request<boost::beast::http::string_body>; + + + +namespace ServiceLogic { + +template <class Send> +static void onWechat(const Older::Application::Pointer &app, StringRequest &&request, Send &&send); + +// Returns a server error response +boost::beast::http::response<boost::beast::http::string_body> +serverError(const boost::beast::http::request<boost::beast::http::string_body> &request, std::string_view errorMessage); + +boost::beast::http::response<boost::beast::http::string_body> +badRequest(const boost::beast::http::request<boost::beast::http::string_body> &request, std::string_view why); + +template <class ResponseBody, class RequestBody> +boost::beast::http::response<ResponseBody> make_200(const boost::beast::http::request<RequestBody> &request, + typename ResponseBody::value_type body, + boost::beast::string_view content) { + boost::beast::http::response<ResponseBody> response{boost::beast::http::status::ok, request.version()}; + response.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING); + response.set(boost::beast::http::field::content_type, content); + response.body() = body; + response.prepare_payload(); + response.keep_alive(request.keep_alive()); + + return response; +} + +void live2dBackend(); + + + + +}; // namespace ServiceLogic + +#include "ServiceLogic.inl" + +#endif // SERVICELOGIC_H diff --git a/ServiceLogic.inl b/ServiceLogic.inl new file mode 100644 index 0000000..9aaea0e --- /dev/null +++ b/ServiceLogic.inl @@ -0,0 +1,41 @@ +#ifndef SERVICELOGIC_INL +#define SERVICELOGIC_INL + +#include "Core/Logger.h" +#include "WeChat/OfficialAccount/Context.h" +#include <boost/beast/http/empty_body.hpp> +#include <boost/beast/http/file_body.hpp> +#include <boost/json/parse.hpp> +#include <boost/json/serialize.hpp> +#include <boost/nowide/fstream.hpp> +#include <boost/url.hpp> +#include <filesystem> + +namespace ServiceLogic { + +template <class Send> +static void onWechat(const Older::Application::Pointer &app, const StringRequest &request, Send &&send) { + using namespace boost::beast; + boost::urls::url url(request.target()); + auto context = Core::Singleton<WeChatContext>::instance(); + http::response<boost::beast::http::string_body> response; + if (request.count("Content-Type") > 0 && request.at("Content-Type") == "text/xml") { + response.body() = context->reply(request.body()); + } else { + auto query = url.params(); + if (auto iterator = query.find("echostr"); iterator != query.end()) { + response.body() = (*iterator)->value; + } + } + boost::beast::error_code ec; + + response.set(http::field::server, BOOST_BEAST_VERSION_STRING); + response.set(http::field::content_type, "text/xml;charset=UTF-8"); + response.keep_alive(request.keep_alive()); + + response.prepare_payload(); + return send(std::move(response)); +} +} // namespace ServiceLogic + +#endif // SERVICELOGIC_INL diff --git a/Settings.cpp b/Settings.cpp index 6ae7a5f..c4a63b8 100644 --- a/Settings.cpp +++ b/Settings.cpp @@ -4,4 +4,17 @@ namespace Older { uint32_t Settings::threads() const { return m_threads; } + +std::string Settings::server() const { + return m_server; +} + +uint16_t Settings::port() const { + return m_port; +} + +std::string Settings::live2dModelsRoot() const { + return m_live2dModelsRoot; +} + } // namespace Older \ No newline at end of file diff --git a/Settings.h b/Settings.h index fd16d40..a634663 100644 --- a/Settings.h +++ b/Settings.h @@ -2,14 +2,22 @@ #define __SETTINGS_H__ #include <cstdint> +#include <string> namespace Older { class Settings { public: uint32_t threads() const; + std::string server() const; + uint16_t port() const; + std::string live2dModelsRoot() const; private: uint32_t m_threads = 1; + std::string m_server = "127.0.0.1"; + uint16_t m_port = 8081; + + std::string m_live2dModelsRoot = "resources/live2d"; }; } // namespace Older diff --git a/WeChat/Corporation/Context.cpp b/WeChat/Corporation/Context.cpp new file mode 100644 index 0000000..ba6c7d7 --- /dev/null +++ b/WeChat/Corporation/Context.cpp @@ -0,0 +1,128 @@ +#include "Context.h" +#include "Base/Messages.h" +#include "Core/Logger.h" +#include "Core/MessageManager.h" +#include "Http/Utility.h" +#include <boost/asio/defer.hpp> +#include <boost/beast/core.hpp> +#include <boost/format.hpp> +#include <boost/json/object.hpp> +#include <boost/json/parse.hpp> +#include <boost/json/serialize.hpp> + +namespace WeChat { +namespace Corporation { +Context::Context(boost::asio::io_context &ioContext) : m_ioContext(ioContext), m_timer(ioContext) { + using namespace Core; + auto manager = Singleton<MessageManager>::instance(); + if (manager) { + manager->subscribe<NotifyServerChan>( + [this](const boost::beast::http::request<boost::beast::http::string_body> &request) { notify(request); }); + } +} + +void Context::sendMessage(MessageType type, const std::string &message) { + boost::format target("/cgi-bin/message/send?access_token=%1%"); + target % m_accessToken; + + boost::json::object msg; + msg["content"] = message; + + boost::json::object request; + request["touser"] = "@all"; + request["agentid"] = agentid; + if (type == MessageType::Markdown) { + request["msgtype"] = "markdown"; + request["markdown"] = std::move(msg); + } else { + request["msgtype"] = "text"; + request["text"] = std::move(msg); + } + auto body = boost::json::serialize(request); + + boost::beast::error_code error; + auto response = Https::post(m_ioContext, host, port, target.str(), body, error); + if (error) { + LOG(error) << error.message(); + return; + } + LOG(info) << response; +} + +void Context::start() { + boost::asio::defer(m_ioContext, [ptr{weak_from_this()}]() { + if (ptr.expired()) { + LOG(error) << "Context instance was expired"; + return; + } + auto self = ptr.lock(); + self->updateAccessToken(); + }); +} + +void Context::notify(const RequestType &request) { + boost::system::error_code error; + auto json = boost::json::parse(request.body(), error); + if (error) { + LOG(error) << "parse: [" << request.body() << "] failed, reason: " << error.message(); + return; + } + // LOG(debug) << "parse: [" << request.body() << "] succeed."; + auto &req = json.as_object(); + MessageType type = MessageType::Text; + if (req.contains("type")) { + if (req.at("type").as_string() == "markdown") { + type = MessageType::Markdown; + } + } + if (req.contains("msg")) { + std::string msg(req.at("msg").as_string()); + sendMessage(type, std::move(msg)); + } +} + +void Context::updateAccessToken() { + boost::beast::error_code error; + + boost::format target("/cgi-bin/gettoken?corpid=%1%&corpsecret=%2%"); + target % corpid % corpsecret; + + auto response = Https::get(m_ioContext, host, port, target.str(), error); + if (error) { + LOG(error) << error.message(); + return; + } + if (response.empty()) { + LOG(warning) << "response is empty."; + return; + } + + auto json = boost::json::parse(response); + auto &accessTokenObject = json.as_object(); + int errcode = accessTokenObject.count("errcode") > 0 ? accessTokenObject.at("errcode").as_int64() : -1; + if (errcode != 0) { + LOG(error) << "get access_token failed,code: " << errcode << ", message: " << accessTokenObject.at("errmsg").as_string(); + return; + } + m_accessToken = accessTokenObject.at("access_token").as_string(); + auto expires_in = accessTokenObject.at("expires_in").as_int64(); + // LOG(info) << "access_token: " << m_accessToken; + LOG(info) << "re-access_token after " << expires_in << " s."; + m_timer.expires_after(std::chrono::seconds(expires_in)); + m_timer.async_wait([this](const boost::system::error_code &error) { + if (error) { + LOG(error) << error.message(); + return; + } + updateAccessToken(); + }); + + static bool started = true; + if (started) { + sendMessage(MessageType::Text, "您好,艾玛已上线......"); + started = false; + } +} + +} // namespace Corporation +} // namespace WeChat \ No newline at end of file diff --git a/WeChat/Corporation/Context.h b/WeChat/Corporation/Context.h new file mode 100644 index 0000000..da530fd --- /dev/null +++ b/WeChat/Corporation/Context.h @@ -0,0 +1,49 @@ +#ifndef __CORPORATIONCONTEXT_H__ +#define __CORPORATIONCONTEXT_H__ + +#include "Core/Singleton.h" +#include <boost/asio/steady_timer.hpp> +#include <boost/beast/http/string_body.hpp> + +namespace WeChat { +namespace Corporation { +class Context : public std::enable_shared_from_this<Context> { + friend class Core::Singleton<Context>; + +public: + enum MessageType { + Text, + Markdown, + }; + using RequestType = boost::beast::http::request<boost::beast::http::string_body>; + + void sendMessage(MessageType type, const std::string &message); + void start(); + + /** + * @brief + * + * @param request + * @example curl -H "Content-Type: application/json" -X POST -d '{"user_id": "123", "msg":"OK!" }' + * https://amass.fun/notify + */ + void notify(const RequestType &request); + +protected: + Context(boost::asio::io_context &ioContext); + void updateAccessToken(); + +private: + boost::asio::io_context &m_ioContext; + boost::asio::steady_timer m_timer; + std::string m_accessToken; + + constexpr static auto host = "qyapi.weixin.qq.com"; + constexpr static auto port = "443"; + constexpr static auto corpid = "ww1a786851749bdadc"; + constexpr static auto corpsecret = "LlyJmYLIBOxJkQxkhwyqNVf550AUQ3JT2MT4yuS31i0"; + constexpr static auto agentid = 1000002; +}; +} // namespace Corporation +} // namespace WeChat +#endif // __CORPORATIONCONTEXT_H__ \ No newline at end of file diff --git a/WeChat/OfficialAccount/Context.cpp b/WeChat/OfficialAccount/Context.cpp new file mode 100644 index 0000000..14b83bd --- /dev/null +++ b/WeChat/OfficialAccount/Context.cpp @@ -0,0 +1,183 @@ +#include "WeChatContext.h" +#include "../ServiceManager.h" +#include "BoostLog.h" +#include "WeChatSession.h" +#include <NetworkUtility.h> +#include <boost/algorithm/string/predicate.hpp> +#include <boost/algorithm/string/trim.hpp> +#include <boost/asio/defer.hpp> +#include <boost/beast/core.hpp> +#include <boost/format.hpp> +#include <boost/json/object.hpp> +#include <boost/json/parse.hpp> +#include <boost/json/serialize.hpp> +#include <boost/property_tree/ptree.hpp> +#include <boost/property_tree/xml_parser.hpp> +#include <sstream> + +std::string WeChatContext::reply(const std::string &body) { + std::ostringstream oss; + LOG(info) << "someone send message: \n" << body; + boost::property_tree::ptree ptree; + std::istringstream iss(body); + boost::property_tree::read_xml(iss, ptree); + + auto ToUserName = ptree.get_optional<std::string>("xml.ToUserName"); + if (!ToUserName) { + LOG(error) << "request dont contain ToUserName."; + return oss.str(); + } + + auto FromUserName = ptree.get<std::string>("xml.FromUserName"); + auto CreateTime = ptree.get<std::string>("xml.CreateTime"); + auto MsgType = ptree.get<std::string>("xml.MsgType"); + auto content = ptree.get<std::string>("xml.Content"); + auto MsgId = ptree.get<std::string>("xml.MsgId"); + + std::shared_ptr<WeChatSession> session; + if (m_sessions.count(FromUserName) > 0) { + session = m_sessions.at(FromUserName); + } else { + session = std::make_shared<WeChatSession>(FromUserName); + m_sessions.emplace(FromUserName, session); + } + boost::algorithm::trim(content); + auto reply = session->processInput(content); + + boost::property_tree::ptree sendXml; + sendXml.put("xml.Content", reply); + LOG(info) << "send " << FromUserName << ": " << reply; + + sendXml.put("xml.ToUserName", FromUserName); + sendXml.put("xml.FromUserName", *ToUserName); + sendXml.put("xml.CreateTime", CreateTime); + sendXml.put("xml.MsgType", MsgType); + + boost::property_tree::write_xml(oss, sendXml); + // LOG(info) << "reply content:\n " << oss.str(); + return oss.str(); +} + +WeChatContext::WeChatContext(boost::asio::io_context &ioContext) + : m_ioContext(ioContext), m_timer(ioContext), m_sessionsExpireTimer(ioContext) { + boost::asio::defer(m_ioContext, [this]() { updateAccessToken(); }); +} + +void WeChatContext::updateAccessToken() { + boost::beast::error_code error; + + boost::format target("/cgi-bin/token?grant_type=client_credential&appid=%1%&secret=%2%"); + target % appid % secret; + + auto response = Https::get(m_ioContext, host, port, target.str(), error); + if (error) { + LOG(error) << error.message(); + return; + } + if (response.empty()) { + LOG(warning) << "response is empty."; + return; + } + + auto json = boost::json::parse(response); + auto &accessTokenObject = json.as_object(); + if (accessTokenObject.count("errcode")) { + LOG(error) << "get access_token failed,code: " << accessTokenObject.at("errcode").as_int64() + << ", message: " << accessTokenObject.at("errmsg").as_string(); + return; + } + m_accessToken = accessTokenObject.at("access_token").as_string(); + auto expires_in = accessTokenObject.at("expires_in").as_int64(); + // LOG(info) << "access_token: " << m_accessToken; + LOG(info) << "re-access_token after " << expires_in << " s."; + m_timer.expires_after(std::chrono::seconds(expires_in)); + m_timer.async_wait([this](const boost::system::error_code &error) { + if (error) { + LOG(error) << error.message(); + return; + } + updateAccessToken(); + }); + broadcast("hello,amass."); +} + +WeChatContext::OpenIds WeChatContext::users() { + boost::beast::error_code error; + + boost::format target("/cgi-bin/user/get?access_token=%1%"); + + target % m_accessToken; + auto response = Https::get(m_ioContext, host, port, target.str(), error); + if (error) { + LOG(error) << error.message(); + return {}; + } + + auto json = boost::json::parse(response); + auto &responseObject = json.as_object(); + if (responseObject.contains("errcode")) { + LOG(error) << responseObject.at("errmsg").as_string(); + return {}; + } + auto &users = responseObject.at("data").as_object().at("openid").as_array(); + if (users.empty()) { + LOG(info) << "now we have no users."; + } + OpenIds ret; + for (auto &id : users) { + ret.emplace_back(id.as_string()); + } + return ret; +} + +std::string WeChatContext::broadcast(const std::string_view &message) { + boost::json::object messageObject; + auto users = this->users(); + LOG(info) << "users: " << users; + if (users.size() < 2) users.emplace_back("fake_user"); + boost::json::array usersArray; + for (auto &user : users) { + usersArray.emplace_back(user); + } + messageObject.emplace("touser", std::move(usersArray)); + messageObject.emplace("msgtype", "text"); + + boost::json::object textObject; + textObject.emplace("content", message.data()); + + messageObject.emplace("text", std::move(textObject)); + + boost::format target("/cgi-bin/message/mass/send?access_token=%1%"); + + target % m_accessToken; + + boost::system::error_code error; + + auto response = Https::post(m_ioContext, host, port, target.str(), boost::json::serialize(messageObject), error); + if (error) { + // LOG(error) << error.message(); + return response; + } + return response; +} + +void WeChatContext::cleanExpiredSessions(const boost::system::error_code &error) { + if (error) { + LOG(error) << error.message(); + return; + } + auto now = std::chrono::system_clock::now(); + for (auto iterator = m_sessions.begin(); iterator != m_sessions.cend();) { + if (std::chrono::duration_cast<std::chrono::seconds>(now - iterator->second->lastAccessedTime()) > + sessionExpireTime) { + iterator = m_sessions.erase(iterator); + } else { + ++iterator; + } + } + m_sessionsExpireTimer.expires_after(sessionExpireTime); + m_sessionsExpireTimer.async_wait([ptr{weak_from_this()}](const boost::system::error_code &error) { + if (ptr.expired()) return; + ptr.lock()->cleanExpiredSessions(error); + }); +} diff --git a/WeChat/OfficialAccount/Context.h b/WeChat/OfficialAccount/Context.h new file mode 100644 index 0000000..3b17604 --- /dev/null +++ b/WeChat/OfficialAccount/Context.h @@ -0,0 +1,47 @@ +#ifndef WECHATCONTEXT_H +#define WECHATCONTEXT_H + +#include "Core/Singleton.h" +#include <boost/asio/steady_timer.hpp> +#include <memory> +#include <unordered_map> + +class WeChatSession; + +class WeChatContext : public std::enable_shared_from_this<WeChatContext> { +public: + using OpenIds = std::vector<std::string>; + + /** + * @brief onWechat()函数调用了此函数,对接收到的消息进行处理 + * + * @param body + * @return std::string 返回给微信服务器 + */ + std::string reply(const std::string &body); + +protected: + WeChatContext(boost::asio::io_context &ioContext); + + void updateAccessToken(); + OpenIds users(); + std::string broadcast(const std::string_view &message); + void cleanExpiredSessions(const boost::system::error_code &error = boost::system::error_code()); + +private: + boost::asio::io_context &m_ioContext; + boost::asio::steady_timer m_timer; + std::string m_accessToken; + + boost::asio::steady_timer m_sessionsExpireTimer; + std::unordered_map<std::string, std::shared_ptr<WeChatSession>> m_sessions; + + constexpr static std::chrono::seconds sessionExpireTime{5}; + constexpr static int httpVersion = 11; + constexpr static auto host = "api.weixin.qq.com"; + constexpr static auto port = "443"; + constexpr static auto appid = "wxdb4253b5c4259708"; + constexpr static auto secret = "199780c4d3205d8b7b1f9be3382fbf82"; +}; + +#endif // WECHATCONTEXT_H diff --git a/WeChat/OfficialAccount/Session.cpp b/WeChat/OfficialAccount/Session.cpp new file mode 100644 index 0000000..244ed18 --- /dev/null +++ b/WeChat/OfficialAccount/Session.cpp @@ -0,0 +1,101 @@ +#include "WeChatSession.h" +#include "../ServiceManager.h" +#include <BoostLog.h> +#include <DateTime.h> + +WeChatSession::WeChatSession(const std::string_view &username) : m_username(username) { + m_lastAccessedTime = std::chrono::system_clock::now(); + initiate(); +} + +std::string WeChatSession::processInput(const std::string_view &text) { + ProcessInputEvent e; + e.text = text; + process_event(e); + m_lastAccessedTime = std::chrono::system_clock::now(); + + std::string ret = std::move(m_reply); + return ret; +} + +void WeChatSession::printHelp() { + std::ostringstream oss; + oss << "1:设置闹钟" << std::endl; + oss << "2:TTS" << std::endl; + oss << "3:当前时间" << std::endl; + oss << "4:随机播放音乐" << std::endl; + oss << "5:停止播放音乐" << std::endl; + oss << "<其它>:帮助" << std::endl; + setReply(oss.str()); +} + +void WeChatSession::printCurrentDateTime() { + auto manager = Amass::Singleton<ServiceManager>::instance(); + if (manager) manager->sendMessage<CurrentDatetimeService>(CurrentDatetime); + setReply("艾玛收到!将为您播报当前时间"); +} + +void WeChatSession::playRandomMusic() { + auto manager = Amass::Singleton<ServiceManager>::instance(); + if (manager) manager->sendMessage<PlayRandomMusicService>(PlayRandomMusic); + setReply("艾玛收到!将为您随机播放音乐"); +} + +void WeChatSession::stopPlayMusic() { + auto manager = Amass::Singleton<ServiceManager>::instance(); + if (manager) manager->sendMessage(StopPlayMusic); + setReply("艾玛收到!正在为您停止播放音乐"); +} + +std::chrono::system_clock::time_point WeChatSession::lastAccessedTime() const { + return m_lastAccessedTime; +} + +void WeChatSession::setReply(std::string &&reply) { + m_reply = std::move(reply); +} + +boost::statechart::result IdleState::react(const ProcessInputEvent &e) { + auto &text = e.text; + if (text == "1") { + outermost_context().setReply("请输入闹钟时间:"); + return transit<SetAlarmState>(); + } else if (text == "2") { + outermost_context().setReply("请输入TTS文字:"); + return transit<SetTtsState>(); + } else if (text == "3") { + outermost_context().printCurrentDateTime(); + return discard_event(); + } else if (text == "4") { + outermost_context().playRandomMusic(); + return discard_event(); + } else if (text == "5") { + outermost_context().stopPlayMusic(); + return discard_event(); + } else { + outermost_context().stopPlayMusic(); + outermost_context().printHelp(); + return discard_event(); + } +} + +boost::statechart::result SetAlarmState::react(const ProcessInputEvent &e) { + auto &text = e.text; + auto [hour, minute, second] = DateTime::parseTime(text); + auto manager = Amass::Singleton<ServiceManager>::instance(); + if (manager) manager->sendMessage<SetAlarmClockService>(SetAlarmClock, hour, minute); + std::ostringstream oss; + oss << "set alarm clock at " << (int)hour << ":" << (int)minute; + this->outermost_context().setReply(oss.str()); + return transit<IdleState>(); +} + +SetAlarmState::SetAlarmState() { +} + +boost::statechart::result SetTtsState::react(const ProcessInputEvent &e) { + auto manager = Amass::Singleton<ServiceManager>::instance(); + if (manager) manager->sendMessage(TextToSpeech, e.text); + outermost_context().setReply(e.text.data()); + return transit<IdleState>(); +} diff --git a/WeChat/OfficialAccount/Session.h b/WeChat/OfficialAccount/Session.h new file mode 100644 index 0000000..cc74036 --- /dev/null +++ b/WeChat/OfficialAccount/Session.h @@ -0,0 +1,53 @@ +#ifndef __WECHATSESSION_H__ +#define __WECHATSESSION_H__ + +#include <boost/statechart/custom_reaction.hpp> +#include <boost/statechart/simple_state.hpp> +#include <boost/statechart/state_machine.hpp> +#include <chrono> +#include <string_view> + +class ProcessInputEvent : public boost::statechart::event<ProcessInputEvent> { +public: + std::string text; +}; + +class IdleState; + +class WeChatSession : public boost::statechart::state_machine<WeChatSession, IdleState> { +public: + WeChatSession(const std::string_view &username); + std::string processInput(const std::string_view &text); + void printHelp(); + void printCurrentDateTime(); + void playRandomMusic(); + void stopPlayMusic(); + std::chrono::system_clock::time_point lastAccessedTime() const; + void setReply(std::string &&reply); + +private: + std::string m_username; + std::chrono::system_clock::time_point m_lastAccessedTime; + std::string m_reply; +}; + +class IdleState : public boost::statechart::simple_state<IdleState, WeChatSession> { +public: + typedef boost::statechart::custom_reaction<ProcessInputEvent> reactions; + boost::statechart::result react(const ProcessInputEvent &); +}; + +class SetAlarmState : public boost::statechart::simple_state<SetAlarmState, WeChatSession> { +public: + typedef boost::statechart::custom_reaction<ProcessInputEvent> reactions; + boost::statechart::result react(const ProcessInputEvent &); + SetAlarmState(); +}; + +class SetTtsState : public boost::statechart::simple_state<SetTtsState, WeChatSession> { +public: + typedef boost::statechart::custom_reaction<ProcessInputEvent> reactions; + boost::statechart::result react(const ProcessInputEvent &); +}; + +#endif // __WECHATSESSION_H__ \ No newline at end of file diff --git a/resources/older.service b/resources/older.service new file mode 100644 index 0000000..705a7e5 --- /dev/null +++ b/resources/older.service @@ -0,0 +1,14 @@ +[Unit] +Description=Http Server +After=network.target +# /etc/systemd/system/older.service + +[Service] +Type=simple +ExecStart=/root/Server/Older +WorkingDirectory=/root/Server +Restart=on-failure +User=root + +[Install] +WantedBy=multi-user.target