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 namespace Older { -Application::Application() { + +class ApplicationPrivate { +public: + std::shared_ptr> router; + std::shared_ptr acceptor; +}; + +Application::Application() : m_d{new ApplicationPrivate()} { + using namespace boost::urls; using namespace Core; + m_d->router = std::make_shared>(); + + m_messageManager = Singleton::construct(); m_settings = Singleton::construct(); m_ioContext = Singleton::construct(m_settings->threads()); + + m_corporationContext = Singleton::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::instance(); + if (manager) { + manager->publish(request); + } + session.reply(ServiceLogic::make_200(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(*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::instance(); + ServiceLogic::live2dBackend(); + startAcceptHttpConnections(settings->server(), settings->port()); m_ioContext->run(); return 0; } + +void Application::asyncAcceptHttpConnections() { + auto socket = std::make_shared(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(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 +#include "Router/matches.hpp" #include +#include +#include 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 { public: + using Pointer = std::shared_ptr; + using Request = boost::beast::http::request; + using RequestHandler = std::function; 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 m_settings; std::shared_ptr m_ioContext; + std::shared_ptr m_messageManager; + std::shared_ptr 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 + +struct NotifyServerChan { + using Signature = void(const boost::beast::http::request &); +}; + +#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 +#include +#include +#include +#include +#include +#include +#include + +namespace Older { +HttpSession::HttpSession(boost::asio::ip::tcp::socket &&socket, + const std::shared_ptr> &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 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::max()); + m_parser->header_limit(std::numeric_limits::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 +#include +#include +#include +#include +#include +#include +#include + +namespace Older { + +/** Represents an established HTTP connection + */ +class HttpSession : public std::enable_shared_from_this { + void doRead(); + void onWrite(boost::beast::error_code ec, std::size_t, bool close); + +public: + using Request = boost::beast::http::request; + using RequestHandler = std::function; + HttpSession(boost::asio::ip::tcp::socket &&socket, const std::shared_ptr> &router); + template + void reply(Response &&response) { + using ResponseType = typename std::decay_t; + auto sp = std::make_shared(std::forward(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> m_router; + boost::beast::flat_buffer m_buffer{std::numeric_limits::max()}; + std::optional> 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 + +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 + +namespace ServiceLogic { +using namespace boost::beast; + +boost::beast::http::response +serverError(const boost::beast::http::request &request, std::string_view errorMessage) { + using namespace boost::beast; + http::response 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 badRequest(const http::request &request, std::string_view why) { + http::response 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::instance(); + application->insertUrl("/api/v1/live2d/{path*}", [](Older::HttpSession &session, const Older::Application::Request &request, + const boost::urls::matches &matches) { + auto settings = Singleton::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 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 +#include +#include +#include +#include +#include +#include + +using StringRequest = boost::beast::http::request; + + + +namespace ServiceLogic { + +template +static void onWechat(const Older::Application::Pointer &app, StringRequest &&request, Send &&send); + +// Returns a server error response +boost::beast::http::response +serverError(const boost::beast::http::request &request, std::string_view errorMessage); + +boost::beast::http::response +badRequest(const boost::beast::http::request &request, std::string_view why); + +template +boost::beast::http::response make_200(const boost::beast::http::request &request, + typename ResponseBody::value_type body, + boost::beast::string_view content) { + boost::beast::http::response 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 +#include +#include +#include +#include +#include +#include + +namespace ServiceLogic { + +template +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::instance(); + http::response 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 +#include 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 +#include +#include +#include +#include +#include + +namespace WeChat { +namespace Corporation { +Context::Context(boost::asio::io_context &ioContext) : m_ioContext(ioContext), m_timer(ioContext) { + using namespace Core; + auto manager = Singleton::instance(); + if (manager) { + manager->subscribe( + [this](const boost::beast::http::request &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 +#include + +namespace WeChat { +namespace Corporation { +class Context : public std::enable_shared_from_this { + friend class Core::Singleton; + +public: + enum MessageType { + Text, + Markdown, + }; + using RequestType = boost::beast::http::request; + + 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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("xml.ToUserName"); + if (!ToUserName) { + LOG(error) << "request dont contain ToUserName."; + return oss.str(); + } + + auto FromUserName = ptree.get("xml.FromUserName"); + auto CreateTime = ptree.get("xml.CreateTime"); + auto MsgType = ptree.get("xml.MsgType"); + auto content = ptree.get("xml.Content"); + auto MsgId = ptree.get("xml.MsgId"); + + std::shared_ptr session; + if (m_sessions.count(FromUserName) > 0) { + session = m_sessions.at(FromUserName); + } else { + session = std::make_shared(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(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 +#include +#include + +class WeChatSession; + +class WeChatContext : public std::enable_shared_from_this { +public: + using OpenIds = std::vector; + + /** + * @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> 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 +#include + +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::instance(); + if (manager) manager->sendMessage(CurrentDatetime); + setReply("艾玛收到!将为您播报当前时间"); +} + +void WeChatSession::playRandomMusic() { + auto manager = Amass::Singleton::instance(); + if (manager) manager->sendMessage(PlayRandomMusic); + setReply("艾玛收到!将为您随机播放音乐"); +} + +void WeChatSession::stopPlayMusic() { + auto manager = Amass::Singleton::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(); + } else if (text == "2") { + outermost_context().setReply("请输入TTS文字:"); + return transit(); + } 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::instance(); + if (manager) manager->sendMessage(SetAlarmClock, hour, minute); + std::ostringstream oss; + oss << "set alarm clock at " << (int)hour << ":" << (int)minute; + this->outermost_context().setReply(oss.str()); + return transit(); +} + +SetAlarmState::SetAlarmState() { +} + +boost::statechart::result SetTtsState::react(const ProcessInputEvent &e) { + auto manager = Amass::Singleton::instance(); + if (manager) manager->sendMessage(TextToSpeech, e.text); + outermost_context().setReply(e.text.data()); + return transit(); +} 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 +#include +#include +#include +#include + +class ProcessInputEvent : public boost::statechart::event { +public: + std::string text; +}; + +class IdleState; + +class WeChatSession : public boost::statechart::state_machine { +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 { +public: + typedef boost::statechart::custom_reaction reactions; + boost::statechart::result react(const ProcessInputEvent &); +}; + +class SetAlarmState : public boost::statechart::simple_state { +public: + typedef boost::statechart::custom_reaction reactions; + boost::statechart::result react(const ProcessInputEvent &); + SetAlarmState(); +}; + +class SetTtsState : public boost::statechart::simple_state { +public: + typedef boost::statechart::custom_reaction 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