2024-01-24 23:19:53 +08:00
|
|
|
#include "Application.h"
|
2024-11-26 22:58:54 +08:00
|
|
|
#include "Database/Database.h"
|
|
|
|
#include "Database/Session.h"
|
2024-01-24 23:19:53 +08:00
|
|
|
#include "DateTime.h"
|
|
|
|
#include "HttpSession.h"
|
|
|
|
#include "IoContext.h"
|
2024-11-10 18:33:39 +08:00
|
|
|
#include "Nng/SocketAisoWrapper.h"
|
2024-01-24 23:19:53 +08:00
|
|
|
#include "ServiceLogic.h"
|
|
|
|
#include "ServiceManager.h"
|
2024-05-03 22:30:46 +08:00
|
|
|
#include "SystemUsage.h"
|
2024-01-24 23:19:53 +08:00
|
|
|
#include "WeChatContext/CorporationContext.h"
|
2024-11-26 22:58:54 +08:00
|
|
|
#include <Wt/Dbo/Json.h>
|
2024-11-10 18:33:39 +08:00
|
|
|
#include <boost/json/object.hpp>
|
|
|
|
#include <boost/json/serialize.hpp>
|
2024-05-05 22:00:15 +08:00
|
|
|
#include <boost/stacktrace.hpp>
|
2024-01-24 23:19:53 +08:00
|
|
|
|
2024-11-10 18:33:39 +08:00
|
|
|
constexpr auto IpcUrl = "ipc:///tmp/nng_ipc_server";
|
|
|
|
|
2024-01-24 23:19:53 +08:00
|
|
|
Application::Application(const std::string &path)
|
|
|
|
: ApplicationSettings(path), m_router{std::make_shared<boost::urls::router<RequestHandler>>()} {
|
|
|
|
|
|
|
|
// clang-format off
|
|
|
|
m_router->insert("/{path*}",[this](HttpSession &session, const Request &request, const boost::urls::matches &matches) {
|
|
|
|
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(getDocumentRoot(), target);
|
|
|
|
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.content_length(size);
|
|
|
|
res.keep_alive(request.keep_alive());
|
|
|
|
session.reply(std::move(res));
|
|
|
|
});
|
|
|
|
|
|
|
|
m_router->insert("/wechat/{session*}",[this](HttpSession &session, const Request &request, const boost::urls::matches &matches) {
|
|
|
|
ServiceLogic::onWechat(shared_from_this(), request,
|
|
|
|
[&session](auto &&response) { session.reply(std::move(response)); });
|
|
|
|
});
|
|
|
|
|
|
|
|
m_router->insert("/api/v1/tasklist", [this](HttpSession &session, const Request &request, const boost::urls::matches &matches) {
|
|
|
|
using namespace boost::beast;
|
2024-11-26 22:58:54 +08:00
|
|
|
auto database = Amass::Singleton<Database>::instance()->session();
|
|
|
|
Tasks tasks = database->find<Task>();
|
|
|
|
std::ostringstream oss;
|
|
|
|
Wt::Dbo::jsonSerialize(tasks, oss);
|
2024-01-24 23:19:53 +08:00
|
|
|
|
|
|
|
http::response<boost::beast::http::string_body> s{boost::beast::http::status::ok, request.version()};
|
|
|
|
s.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
|
|
|
s.set(http::field::content_type, "application/json;charset=UTF-8");
|
|
|
|
s.keep_alive(request.keep_alive());
|
2024-11-26 22:58:54 +08:00
|
|
|
s.body() = oss.str();
|
2024-01-24 23:19:53 +08:00
|
|
|
s.prepare_payload();
|
|
|
|
session.reply(std::move(s));
|
|
|
|
});
|
|
|
|
|
2024-11-26 22:58:54 +08:00
|
|
|
m_router->insert("/api/v1/task/add", [this](HttpSession &session, const Request &request, const boost::urls::matches &matches) mutable {
|
2024-01-24 23:19:53 +08:00
|
|
|
using namespace boost::beast;
|
|
|
|
LOG(info) << "add task: " << request.body();
|
|
|
|
auto rootJson = boost::json::parse(request.body());
|
|
|
|
auto &root = rootJson.as_object();
|
|
|
|
|
|
|
|
std::string content;
|
|
|
|
if (root.contains("content")) {
|
|
|
|
content = root.at("content").as_string();
|
|
|
|
}
|
2024-11-26 22:58:54 +08:00
|
|
|
auto database = Amass::Singleton<Database>::instance()->session();
|
|
|
|
auto task = std::make_unique<Task>();
|
|
|
|
task->createTime = root.at("createTime").as_int64();
|
|
|
|
task->content = content;
|
|
|
|
task->comment = std::string(root.at("comment").as_string());
|
|
|
|
auto t = database->add(std::move(task));
|
|
|
|
Wt::Dbo::ptr<Task> parent = database->find<Task>("where id=?").bind(root.at("parentId").as_int64());
|
|
|
|
if (parent) {
|
|
|
|
parent.modify()->children.insert(t);
|
|
|
|
}
|
2024-01-24 23:19:53 +08:00
|
|
|
boost::json::object reply;
|
2024-11-26 22:58:54 +08:00
|
|
|
reply["status"] = 0;
|
2024-01-24 23:19:53 +08:00
|
|
|
http::response<boost::beast::http::string_body> s{boost::beast::http::status::ok, request.version()};
|
|
|
|
s.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
|
|
|
s.set(http::field::content_type, "application/json;charset=UTF-8");
|
|
|
|
s.keep_alive(request.keep_alive());
|
|
|
|
s.body() = boost::json::serialize(reply);
|
|
|
|
s.prepare_payload();
|
|
|
|
session.reply(std::move(s));
|
|
|
|
});
|
|
|
|
|
|
|
|
m_router->insert("/api/v1/task/delete/{id}", [this](HttpSession &session, const Request &request,const boost::urls::matches &matches) {
|
|
|
|
using namespace boost::beast;
|
|
|
|
LOG(info) << "delete task: " << matches.at("id");
|
2024-11-26 22:58:54 +08:00
|
|
|
auto database = Amass::Singleton<Database>::instance()->session();
|
|
|
|
Wt::Dbo::ptr<Task> joe = database->find<Task>().where("id = ?").bind(std::stoi(matches.at("id")));
|
|
|
|
int status = -1;
|
|
|
|
if (joe) {
|
|
|
|
joe.remove();
|
|
|
|
status = 0;
|
|
|
|
}
|
2024-01-24 23:19:53 +08:00
|
|
|
boost::json::object reply;
|
2024-11-26 22:58:54 +08:00
|
|
|
reply["status"] = status;
|
2024-01-24 23:19:53 +08:00
|
|
|
http::response<boost::beast::http::string_body> s{boost::beast::http::status::ok, request.version()};
|
|
|
|
s.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
|
|
|
s.set(http::field::content_type, "application/json;charset=UTF-8");
|
|
|
|
s.keep_alive(request.keep_alive());
|
|
|
|
s.body() = boost::json::serialize(reply);
|
|
|
|
s.prepare_payload();
|
|
|
|
session.reply(std::move(s));
|
|
|
|
});
|
|
|
|
|
|
|
|
m_router->insert("/trigger-ci.hook", [this](HttpSession &session, const Request &request, const boost::urls::matches &matches) {
|
|
|
|
LOG(info) << "webhook: " << request;
|
|
|
|
session.reply(ServiceLogic::make_200<boost::beast::http::string_body>(request, "Main page\n", "text/html"));
|
|
|
|
});
|
|
|
|
|
|
|
|
m_router->insert("/notify", [this](HttpSession &session, const Request &request, const boost::urls::matches &matches) {
|
|
|
|
auto corp = Amass::Singleton<CorporationContext>::instance();
|
|
|
|
corp->notify(request);
|
|
|
|
session.reply(
|
|
|
|
ServiceLogic::make_200<boost::beast::http::string_body>(request, "notify successed.\n", "text/html"));
|
|
|
|
});
|
2024-07-31 22:28:05 +08:00
|
|
|
// clang-format on
|
2024-11-26 22:58:54 +08:00
|
|
|
m_router->insert("/api/v1/visit_analysis", [this](HttpSession &session, const Request &request, const boost::urls::matches &matches) {
|
2024-07-30 22:17:55 +08:00
|
|
|
using namespace boost::beast;
|
|
|
|
auto rootJson = boost::json::parse(request.body());
|
|
|
|
auto &root = rootJson.as_object();
|
|
|
|
std::string url;
|
|
|
|
if (root.contains("url")) {
|
|
|
|
url = root["url"].as_string();
|
|
|
|
}
|
|
|
|
auto database = Amass::Singleton<Database>::instance();
|
2024-08-19 23:17:33 +08:00
|
|
|
if (std::filesystem::exists("amass_blog" + url) && url.find("/我的博客/page") != 0) {
|
2024-08-06 22:24:26 +08:00
|
|
|
std::string visitorUuid;
|
|
|
|
if (root.contains("visitor_uuid")) {
|
|
|
|
visitorUuid = root["visitor_uuid"].as_string();
|
|
|
|
}
|
|
|
|
std::string userAgent;
|
|
|
|
if (root.contains("user_agent")) {
|
|
|
|
userAgent = root["user_agent"].as_string();
|
|
|
|
}
|
|
|
|
auto now = std::chrono::system_clock::now();
|
|
|
|
std::time_t now_time = std::chrono::system_clock::to_time_t(now);
|
|
|
|
database->updateVisitCount(url, visitorUuid, userAgent, now_time);
|
|
|
|
}
|
2024-07-30 22:17:55 +08:00
|
|
|
auto data = database->visitAnalysisData(std::string(url));
|
2024-07-30 23:17:42 +08:00
|
|
|
auto total = database->siteVisitAnalysisData();
|
2024-07-30 22:17:55 +08:00
|
|
|
|
|
|
|
boost::json::object reply;
|
|
|
|
reply["page_view_count"] = data.pageViewCount;
|
|
|
|
reply["unique_visitor_count"] = data.uniqueVisitorCount;
|
2024-07-30 23:17:42 +08:00
|
|
|
reply["site_page_view_count"] = total.pageViewCount;
|
|
|
|
reply["site_unique_visitor_count"] = total.uniqueVisitorCount;
|
|
|
|
|
2024-07-30 22:17:55 +08:00
|
|
|
http::response<boost::beast::http::string_body> s{boost::beast::http::status::ok, request.version()};
|
|
|
|
s.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
|
|
|
s.set(http::field::content_type, "application/json;charset=UTF-8");
|
|
|
|
s.keep_alive(request.keep_alive());
|
|
|
|
s.body() = boost::json::serialize(reply);
|
|
|
|
s.prepare_payload();
|
|
|
|
session.reply(std::move(s));
|
|
|
|
});
|
2024-07-31 22:28:05 +08:00
|
|
|
|
2024-11-26 22:58:54 +08:00
|
|
|
m_router->insert("/api/v1/most_viewed_urls", [this](HttpSession &session, const Request &request, const boost::urls::matches &matches) {
|
2024-08-05 23:01:19 +08:00
|
|
|
using namespace boost::beast;
|
|
|
|
int size = 5;
|
|
|
|
std::error_code error;
|
|
|
|
if (!request.body().empty()) {
|
|
|
|
auto rootJson = boost::json::parse(request.body(), error);
|
|
|
|
if (error) {
|
|
|
|
LOG(info) << "<" << request.body() << "> parse json error: " << error.message();
|
|
|
|
} else {
|
|
|
|
auto &root = rootJson.as_object();
|
|
|
|
if (root.contains("size")) {
|
|
|
|
size = root.at("size").as_int64();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
auto data = Amass::Singleton<Database>::instance()->mostViewedUrls(size);
|
|
|
|
boost::json::array reply;
|
|
|
|
for (auto &d : data) {
|
|
|
|
boost::json::object object;
|
|
|
|
object["url"] = d.url;
|
|
|
|
object["count"] = d.pageViewCount;
|
|
|
|
reply.push_back(std::move(object));
|
|
|
|
}
|
|
|
|
http::response<boost::beast::http::string_body> s{boost::beast::http::status::ok, request.version()};
|
|
|
|
s.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
2024-08-06 09:48:25 +08:00
|
|
|
s.set(http::field::content_type, "application/json;charset=UTF-8");
|
|
|
|
s.keep_alive(request.keep_alive());
|
|
|
|
s.body() = boost::json::serialize(reply);
|
|
|
|
s.prepare_payload();
|
|
|
|
session.reply(std::move(s));
|
|
|
|
});
|
|
|
|
|
2024-11-26 22:58:54 +08:00
|
|
|
m_router->insert("/api/v1/latest_viewed_urls", [this](HttpSession &session, const Request &request, const boost::urls::matches &matches) {
|
2024-08-06 09:48:25 +08:00
|
|
|
using namespace boost::beast;
|
|
|
|
int size = 5;
|
|
|
|
std::error_code error;
|
|
|
|
if (!request.body().empty()) {
|
|
|
|
auto rootJson = boost::json::parse(request.body(), error);
|
|
|
|
if (error) {
|
|
|
|
LOG(info) << "<" << request.body() << "> parse json error: " << error.message();
|
|
|
|
} else {
|
|
|
|
auto &root = rootJson.as_object();
|
|
|
|
if (root.contains("size")) {
|
|
|
|
size = root.at("size").as_int64();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
auto data = Amass::Singleton<Database>::instance()->latestViewedUrls(size);
|
|
|
|
boost::json::array reply;
|
|
|
|
for (auto &d : data) {
|
|
|
|
boost::json::object object;
|
|
|
|
object["url"] = d.url;
|
|
|
|
object["time"] = d.lastViewTime;
|
|
|
|
reply.push_back(std::move(object));
|
|
|
|
}
|
|
|
|
http::response<boost::beast::http::string_body> s{boost::beast::http::status::ok, request.version()};
|
|
|
|
s.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
2024-08-05 23:01:19 +08:00
|
|
|
s.set(http::field::content_type, "application/json;charset=UTF-8");
|
|
|
|
s.keep_alive(request.keep_alive());
|
|
|
|
s.body() = boost::json::serialize(reply);
|
|
|
|
s.prepare_payload();
|
|
|
|
session.reply(std::move(s));
|
|
|
|
});
|
|
|
|
|
2024-01-24 23:19:53 +08:00
|
|
|
m_ioContext = Amass::Singleton<IoContext>::instance<Amass::Construct>(getThreads());
|
|
|
|
m_timer = std::make_shared<boost::asio::system_timer>(*m_ioContext->ioContext());
|
|
|
|
|
2024-11-10 18:33:39 +08:00
|
|
|
m_replyer = std::make_shared<Nng::Asio::Socket>(*m_ioContext->ioContext(), Nng::Reply);
|
|
|
|
m_replyer->listen(IpcUrl);
|
|
|
|
|
2024-05-03 22:30:46 +08:00
|
|
|
m_systemUsage = std::make_shared<SystemUsage>(*m_ioContext->ioContext(), getHomeAssistantAccessToken());
|
|
|
|
m_systemUsage->start();
|
|
|
|
|
2024-01-24 23:19:53 +08:00
|
|
|
alarmTask();
|
|
|
|
}
|
|
|
|
|
|
|
|
boost::asio::io_context &Application::ioContext() {
|
|
|
|
return *m_ioContext->ioContext();
|
|
|
|
}
|
|
|
|
|
|
|
|
const Application::RequestHandler *Application::find(boost::urls::segments_encoded_view path,
|
|
|
|
boost::urls::matches_base &matches) const noexcept {
|
2024-05-05 22:00:15 +08:00
|
|
|
const Application::RequestHandler *ret = nullptr;
|
|
|
|
try {
|
|
|
|
ret = m_router->find(path, matches);
|
|
|
|
} catch (const std::exception &e) {
|
|
|
|
boost::stacktrace::stacktrace trace = boost::stacktrace::stacktrace::from_current_exception();
|
|
|
|
LOG(error) << e.what() << ", trace:\n" << trace;
|
|
|
|
}
|
|
|
|
return ret;
|
2024-01-24 23:19:53 +08:00
|
|
|
}
|
|
|
|
|
2024-10-23 19:53:51 +08:00
|
|
|
void Application::insertUrl(std::string_view url, RequestHandler &&handler) {
|
|
|
|
m_router->insert(url, std::move(handler));
|
|
|
|
}
|
|
|
|
|
2024-01-24 23:19:53 +08:00
|
|
|
int Application::exec() {
|
2024-11-10 18:33:39 +08:00
|
|
|
startAcceptRequest();
|
2024-01-24 23:19:53 +08:00
|
|
|
LOG(info) << "application start successful ...";
|
|
|
|
startCheckInterval(*m_ioContext->ioContext(), 2);
|
2024-11-10 18:33:39 +08:00
|
|
|
m_ioContext->run();
|
2024-01-24 23:19:53 +08:00
|
|
|
LOG(info) << "application exit successful ...";
|
|
|
|
return m_status;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Application::alarmTask() {
|
|
|
|
int hour = 10;
|
|
|
|
int minute = 30;
|
|
|
|
auto alarmTime = DateTime::currentDateTime();
|
|
|
|
alarmTime.setHour(hour);
|
|
|
|
alarmTime.setMinute(minute);
|
|
|
|
if (std::chrono::system_clock::now() > alarmTime()) {
|
|
|
|
alarmTime = alarmTime.tomorrow();
|
|
|
|
}
|
|
|
|
m_timer->expires_at(alarmTime());
|
|
|
|
m_timer->async_wait([this](const boost::system::error_code &error) mutable {
|
|
|
|
if (error) {
|
|
|
|
LOG(error) << error.message();
|
|
|
|
return;
|
|
|
|
}
|
2024-11-26 22:58:54 +08:00
|
|
|
auto session = Amass::Singleton<Database>::instance()->session();
|
|
|
|
Tasks tasks = session->find<Task>();
|
2024-01-24 23:19:53 +08:00
|
|
|
bool founded = false;
|
|
|
|
std::string content;
|
|
|
|
for (auto &task : tasks) {
|
|
|
|
if (founded) break;
|
2024-11-26 22:58:54 +08:00
|
|
|
for (auto child : task->children) {
|
|
|
|
if (!child->finished) {
|
|
|
|
content = child->content;
|
2024-01-24 23:19:53 +08:00
|
|
|
founded = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2024-11-26 22:58:54 +08:00
|
|
|
if (!founded && !task->finished) {
|
|
|
|
content = task->content;
|
2024-01-24 23:19:53 +08:00
|
|
|
founded = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (founded) {
|
|
|
|
std::ostringstream oss;
|
|
|
|
oss << "待完成事项:" << std::endl;
|
|
|
|
oss << "==========" << std::endl;
|
|
|
|
oss << content << std::endl;
|
|
|
|
oss << "==========" << std::endl;
|
|
|
|
oss << "每天都要过得充实开心哦~";
|
|
|
|
auto manager = Amass::Singleton<ServiceManager>::instance();
|
|
|
|
if (manager) manager->sendMessage(NotifyServerChan, oss.str());
|
|
|
|
}
|
|
|
|
|
|
|
|
alarmTask();
|
|
|
|
});
|
2024-11-10 18:33:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void Application::startAcceptRequest() {
|
|
|
|
m_replyer->asyncReceive([ptr{weak_from_this()}](const boost::system::error_code &error, const Nng::Buffer &buffer) {
|
|
|
|
if (error) {
|
|
|
|
LOG(error) << error.message();
|
|
|
|
}
|
|
|
|
LOG(info) << buffer.data();
|
|
|
|
if (ptr.expired()) return;
|
|
|
|
auto self = ptr.lock();
|
|
|
|
auto value = boost::json::parse(buffer.data());
|
|
|
|
auto &request = value.as_object();
|
|
|
|
if (request.at("command").as_string() == "exit") {
|
|
|
|
boost::json::object reply;
|
|
|
|
reply["status"] = 0;
|
|
|
|
reply["message"] = "will exit.";
|
|
|
|
auto txt = boost::json::serialize(reply);
|
|
|
|
self->m_replyer->send(txt.data(), txt.size());
|
|
|
|
std::raise(SIGUSR1); // 发送自定义信号
|
|
|
|
}
|
|
|
|
self->startAcceptRequest();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void Application::requetExit() {
|
|
|
|
LOG(info) << "send exit request to program.";
|
|
|
|
Nng::Socket request(Nng::Request);
|
2024-11-10 20:23:00 +08:00
|
|
|
request.setOption(Nng::RecvTimeout, std::chrono::milliseconds(2000));
|
|
|
|
std::error_code error;
|
|
|
|
request.dial(IpcUrl, error);
|
|
|
|
if (error) {
|
|
|
|
LOG(error) << error.message();
|
|
|
|
return;
|
|
|
|
}
|
2024-11-10 18:33:39 +08:00
|
|
|
|
|
|
|
boost::json::object object;
|
|
|
|
object["command"] = "exit";
|
|
|
|
auto text = boost::json::serialize(object);
|
|
|
|
request.send(text.data(), text.size());
|
|
|
|
auto buffer = request.recv();
|
|
|
|
LOG(info) << buffer.data<char>();
|
2024-01-24 23:19:53 +08:00
|
|
|
}
|