实现live2d接口。

This commit is contained in:
root 2025-02-27 12:57:55 +00:00
parent b5f1c3343a
commit e9c3cde9de
21 changed files with 1151 additions and 3 deletions

17
.vscode/c_cpp_properties.json vendored Normal file
View File

@ -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
}

View File

@ -1,22 +1,100 @@
#include "Application.h" #include "Application.h"
#include "Base/Messages.h"
#include "Core/IoContext.h" #include "Core/IoContext.h"
#include "Core/MessageManager.h"
#include "Core/Singleton.h" #include "Core/Singleton.h"
#include "HttpSession.h"
#include "Router/router.hpp"
#include "ServiceLogic.h"
#include "Settings.h" #include "Settings.h"
#include "WeChat/Corporation/Context.h"
#include <boost/asio/strand.hpp>
namespace Older { 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; using namespace Core;
m_d->router = std::make_shared<router<RequestHandler>>();
m_messageManager = Singleton<MessageManager>::construct();
m_settings = Singleton<Settings>::construct(); m_settings = Singleton<Settings>::construct();
m_ioContext = Singleton<IoContext>::construct(m_settings->threads()); 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() { boost::asio::io_context &Application::ioContext() {
return *m_ioContext->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() { int Application::exec() {
using namespace Core;
auto settings = Singleton<Settings>::instance();
ServiceLogic::live2dBackend();
startAcceptHttpConnections(settings->server(), settings->port());
m_ioContext->run(); m_ioContext->run();
return 0; 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 } // namespace Older

View File

@ -1,26 +1,48 @@
#ifndef __APPLICATION_H__ #ifndef __APPLICATION_H__
#define __APPLICATION_H__ #define __APPLICATION_H__
#include <memory> #include "Router/matches.hpp"
#include <boost/asio/io_context.hpp> #include <boost/asio/io_context.hpp>
#include <boost/beast/http/string_body.hpp>
#include <memory>
namespace Core { namespace Core {
class IoContext; class IoContext;
class MessageManager;
} // namespace Core
namespace WeChat {
namespace Corporation {
class Context;
} }
} // namespace WeChat
namespace Older { namespace Older {
class Settings; class Settings;
class ApplicationPrivate;
class HttpSession;
class Application { class Application : public std::enable_shared_from_this<Application> {
public: 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(); Application();
boost::asio::io_context &ioContext(); boost::asio::io_context &ioContext();
void startAcceptHttpConnections(const std::string &address, uint16_t port);
void insertUrl(std::string_view url, RequestHandler &&handler);
int exec(); int exec();
protected:
void asyncAcceptHttpConnections();
private: private:
ApplicationPrivate *m_d = nullptr;
std::shared_ptr<Settings> m_settings; std::shared_ptr<Settings> m_settings;
std::shared_ptr<Core::IoContext> m_ioContext; std::shared_ptr<Core::IoContext> m_ioContext;
std::shared_ptr<Core::MessageManager> m_messageManager;
std::shared_ptr<WeChat::Corporation::Context> m_corporationContext;
}; };
} // namespace Older } // namespace Older
#endif // __APPLICATION_H__ #endif // __APPLICATION_H__

11
Base/Messages.h Normal file
View File

@ -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__

View File

@ -1,11 +1,24 @@
find_package(Boost REQUIRED COMPONENTS json)
add_subdirectory(/root/Projects/Kylin Kylin) add_subdirectory(/root/Projects/Kylin Kylin)
add_executable(Older main.cpp add_executable(Older main.cpp
WeChat/Corporation/Context.h WeChat/Corporation/Context.cpp
Application.h Application.cpp Application.h Application.cpp
HttpSession.h HttpSession.cpp
ResponseUtility.h ResponseUtility.cpp
ServiceLogic.h ServiceLogic.inl ServiceLogic.cpp
Settings.h Settings.cpp Settings.h Settings.cpp
) )
target_include_directories(Older
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(Older target_link_libraries(Older
PRIVATE Kylin::Core PRIVATE Kylin::Core
PRIVATE Kylin::Http
PRIVATE Kylin::Router
PRIVATE Boost::json
) )

119
HttpSession.cpp Normal file
View File

@ -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

51
HttpSession.h Normal file
View File

@ -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

51
ResponseUtility.cpp Normal file
View File

@ -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

19
ResponseUtility.h Normal file
View File

@ -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

77
ServiceLogic.cpp Normal file
View File

@ -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

53
ServiceLogic.h Normal file
View File

@ -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

41
ServiceLogic.inl Normal file
View File

@ -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

View File

@ -4,4 +4,17 @@ namespace Older {
uint32_t Settings::threads() const { uint32_t Settings::threads() const {
return m_threads; 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 } // namespace Older

View File

@ -2,14 +2,22 @@
#define __SETTINGS_H__ #define __SETTINGS_H__
#include <cstdint> #include <cstdint>
#include <string>
namespace Older { namespace Older {
class Settings { class Settings {
public: public:
uint32_t threads() const; uint32_t threads() const;
std::string server() const;
uint16_t port() const;
std::string live2dModelsRoot() const;
private: private:
uint32_t m_threads = 1; 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 } // namespace Older

View File

@ -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

View File

@ -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__

View File

@ -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);
});
}

View File

@ -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

View File

@ -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>();
}

View File

@ -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__

14
resources/older.service Normal file
View File

@ -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