实现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

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__