185 lines
6.3 KiB
C++
185 lines
6.3 KiB
C++
|
#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([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);
|
||
|
});
|
||
|
}
|