#include "Application.h" #include "Database.h" #include "DateTime.h" #include "HttpSession.h" #include "IoContext.h" #include "Nng/SocketAisoWrapper.h" #include "ServiceLogic.h" #include "ServiceManager.h" #include "SystemUsage.h" #include "WeChatContext/CorporationContext.h" #include #include #include constexpr auto IpcUrl = "ipc:///tmp/nng_ipc_server"; Application::Application(const std::string &path) : ApplicationSettings(path), m_router{std::make_shared>()} { // 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 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; auto database = Amass::Singleton::instance(); auto tasks = database->tasks(); http::response 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(tasks); s.prepare_payload(); session.reply(std::move(s)); }); m_router->insert("/api/v1/task/add", [this](HttpSession &session, const Request &request, const boost::urls::matches &matches) { using namespace boost::beast; LOG(info) << "add task: " << request.body(); auto database = Amass::Singleton::instance(); 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(); } bool ret = database->addTask(root.at("createTime").as_int64(), content, std::string(root.at("comment").as_string()), root.at("parentId").as_int64()); boost::json::object reply; reply["status"] = ret ? 0 : -1; http::response 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"); auto database = Amass::Singleton::instance(); auto status = database->removeTask(std::stoi(matches.at("id"))); boost::json::object reply; reply["status"] = status ? 0 : -1; http::response 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(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::instance(); corp->notify(request); session.reply( ServiceLogic::make_200(request, "notify successed.\n", "text/html")); }); // clang-format on m_router->insert("/api/v1/visit_analysis", [this](HttpSession &session, const Request &request, const boost::urls::matches &matches) { 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::instance(); if (std::filesystem::exists("amass_blog" + url) && url.find("/我的博客/page") != 0) { 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); } auto data = database->visitAnalysisData(std::string(url)); auto total = database->siteVisitAnalysisData(); boost::json::object reply; reply["page_view_count"] = data.pageViewCount; reply["unique_visitor_count"] = data.uniqueVisitorCount; reply["site_page_view_count"] = total.pageViewCount; reply["site_unique_visitor_count"] = total.uniqueVisitorCount; http::response 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/most_viewed_urls", [this](HttpSession &session, const Request &request, const boost::urls::matches &matches) { 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::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 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/latest_viewed_urls", [this](HttpSession &session, const Request &request, const boost::urls::matches &matches) { 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::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 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_ioContext = Amass::Singleton::instance(getThreads()); m_timer = std::make_shared(*m_ioContext->ioContext()); m_replyer = std::make_shared(*m_ioContext->ioContext(), Nng::Reply); m_replyer->listen(IpcUrl); m_systemUsage = std::make_shared(*m_ioContext->ioContext(), getHomeAssistantAccessToken()); m_systemUsage->start(); 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 { 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; } void Application::insertUrl(std::string_view url, RequestHandler &&handler) { m_router->insert(url, std::move(handler)); } int Application::exec() { startAcceptRequest(); LOG(info) << "application start successful ..."; startCheckInterval(*m_ioContext->ioContext(), 2); m_ioContext->run(); 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; } auto database = Amass::Singleton::instance(); auto tasks = database->tasks(); bool founded = false; std::string content; for (auto &task : tasks) { if (founded) break; for (auto &child : task.children) { if (!child.finished) { content = child.content; founded = true; break; } } if (!founded && !task.finished) { content = task.content; 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::instance(); if (manager) manager->sendMessage(NotifyServerChan, oss.str()); } alarmTask(); }); } 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() { nng_log_set_logger(nng_stderr_logger); LOG(info) << "send exit request to program."; Nng::Socket request(Nng::Request); request.setOption(Nng::RecvTimeout, std::chrono::milliseconds(2000)); std::error_code error; request.dial(IpcUrl, error); if (error) { LOG(error) << error.message(); return; } 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(); }