diff --git a/Asio.cpp b/Asio.cpp deleted file mode 100644 index 75f1ccb..0000000 --- a/Asio.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "Asio.h" - -#include - -void initSSL() -{ - sslContext.set_verify_mode(boost::asio::ssl::verify_peer | - boost::asio::ssl::verify_fail_if_no_peer_cert); - sslContext.set_default_verify_paths(); - boost::certify::enable_native_https_server_verification(sslContext); -} diff --git a/Asio.h b/Asio.h deleted file mode 100644 index f95f0cc..0000000 --- a/Asio.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef COURT_MONITOR_ASIO_H -#define COURT_MONITOR_ASIO_H - -#include -#include - -static boost::asio::io_context asioContext; - -static boost::asio::ssl::context sslContext(boost::asio::ssl::context::tls_client); - -void initSSL(); - -#endif // COURT_MONITOR_ASIO_H diff --git a/Bot.cpp b/Bot.cpp new file mode 100644 index 0000000..710cc11 --- /dev/null +++ b/Bot.cpp @@ -0,0 +1,104 @@ +#include "Bot.h" + +#include +#include + +#include + +namespace { +boost::asio::ssl::context sslContext(boost::asio::ssl::context::tlsv13_client); +} + +Bot::Bot(boost::asio::io_context& asioContext, LocalStorage& storage, bool& terminationFlag) + : storage_(storage), + terminationFlag_(terminationFlag), + agent_(storage.token, asioContext, sslContext) +{ + getUpdates(); +} + +void Bot::setupCommands() +{ + banana::api::set_my_commands_args_t args; + args.commands.push_back(banana::api::bot_command_t{ + "/subscribe_case", "Подписаться на обновления дела по его номеру"}); + args.commands.push_back(banana::api::bot_command_t{"/find_case", "Найти дело по параметрам"}); + args.scope = banana::api::bot_command_scope_all_private_chats_t{"all_private_chats"}; + banana::api::set_my_commands(agent_, std::move(args), + [](banana::expected result) + { + if (result) + fmt::print("commands set up successfully\n"); + else + fmt::print(stderr, "failed to set up commands\n"); + }); +} + +void Bot::notifyUser(int userId, + const std::string& caseNumber, + std::string caseUrl, + const CaseHistoryItem& item) +{ + caseUrl = fmt::format("https://mirsud.spb.ru{}", caseUrl); + std::string message = fmt::format( + "Новое событие по делу [№{}]({}):\n" + "{}\n" + "Дата: {} {}\n", + caseNumber, caseUrl, item.status, item.date, item.time); + banana::api::send_message(agent_, {.chat_id = userId, .text = message, .parse_mode = "markdown"}, + [](const auto&) {}); +} + +void Bot::getUpdates() +{ + auto handler = [this](banana::expected> updates) + { + if (terminationFlag_) + return; + + if (updates) + { + for (const auto& update : *updates) + { + processUpdate(update); + updatesOffset_ = update.update_id + 1; + } + getUpdates(); + } + else + fmt::print(stderr, "failed to get updates: {}\n", updates.error()); + }; + + banana::api::get_updates(agent_, {.offset = updatesOffset_, .timeout = 50}, std::move(handler)); +} + +void Bot::processUpdate(const banana::api::update_t& update) +{ + if (update.message) + { + if (update.message->text) + { + fmt::print("rx: {}\n", *update.message->text); + if (*update.message->text == "/start") + processStartCommand(*update.message); + else if (*update.message->text == "/subscribe_case") + processSubscribeCaseCommand(*update.message); + else + { + // TODO + } + } + else + fmt::print("skip message without text"); // TODO ответить + } + else + fmt::print("skip unknown update type"); +} + +void Bot::processStartCommand(const banana::api::message_t& message) +{ +} + +void Bot::processSubscribeCaseCommand(const banana::api::message_t& message) +{ +} diff --git a/Bot.h b/Bot.h new file mode 100644 index 0000000..376e0f7 --- /dev/null +++ b/Bot.h @@ -0,0 +1,35 @@ +#ifndef COURT_MONITOR_BOT_H +#define COURT_MONITOR_BOT_H + +#include "CourtApi.h" +#include "Storage.h" + +#include +#include + +class Bot +{ +public: + explicit Bot(boost::asio::io_context& asioContext, LocalStorage& storage, bool& terminationFlag); + + void setupCommands(); + + void notifyUser(int userId, + const std::string& caseNumber, + std::string caseUrl, + const CaseHistoryItem& item); + +private: + void getUpdates(); + void processUpdate(const banana::api::update_t& update); + + void processStartCommand(const banana::api::message_t& message); + void processSubscribeCaseCommand(const banana::api::message_t& message); + + LocalStorage& storage_; + bool& terminationFlag_; + banana::agent::beast_callback agent_; + std::int64_t updatesOffset_ = 0; +}; + +#endif // COURT_MONITOR_BOT_H diff --git a/CMakeLists.txt b/CMakeLists.txt index e4a4166..e7925a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ find_package(Boost COMPONENTS system REQUIRED) add_subdirectory(external) add_executable(court_monitor - Asio.cpp + Bot.cpp CourtApi.cpp Storage.cpp main.cpp diff --git a/CourtApi.cpp b/CourtApi.cpp index 221d584..d1a519d 100644 --- a/CourtApi.cpp +++ b/CourtApi.cpp @@ -1,10 +1,9 @@ #include "CourtApi.h" -#include "Asio.h" - #include #include +#include #include #include #include @@ -15,10 +14,17 @@ const char* serverDomain = "mirsud.spb.ru"; +namespace { +boost::asio::ssl::context sslContext(boost::asio::ssl::context::tlsv13_client); using ssl_stream = boost::asio::ssl::stream; +} // namespace -ssl_stream connect(const std::string& hostname) +ssl_stream connect(boost::asio::io_context& asioContext, const std::string& hostname) { + sslContext.set_verify_mode(boost::asio::ssl::verify_peer | + boost::asio::ssl::verify_fail_if_no_peer_cert); + sslContext.set_default_verify_paths(); + boost::certify::enable_native_https_server_verification(sslContext); ssl_stream stream(asioContext, sslContext); static boost::asio::ip::tcp::resolver resolver(asioContext); @@ -77,9 +83,11 @@ nlohmann::json getResults(ssl_stream& stream, const std::string_view& uuid) fmt::format("failed to retrieve JSON (server returned code {})", status)); } -nlohmann::json getCaseDetails(int courtId, const std::string_view& caseNumber) +nlohmann::json getCaseDetails(boost::asio::io_context& asioContext, + int courtId, + const std::string_view& caseNumber) { - ssl_stream stream = connect(serverDomain); + ssl_stream stream = connect(asioContext, serverDomain); int status; std::string result; @@ -105,3 +113,20 @@ nlohmann::json getCaseDetails(int courtId, const std::string_view& caseNumber) throw std::runtime_error( fmt::format("failed to retrieve JSON (server returned code {})", status)); } + +std::vector parseHistory(const nlohmann::json& details) +{ + std::vector items; + const auto& history = details.at("history"); + for (const auto& obj : history) + { + CaseHistoryItem item; + item.date = obj.at("date").get(); + item.time = obj.at("time").get(); + item.status = obj.at("status").get(); + item.publishDate = obj.at("publish_date").get(); + item.publishTime = obj.at("publish_time").get(); + items.push_back(std::move(item)); + } + return items; +} diff --git a/CourtApi.h b/CourtApi.h index 91e5861..970e8c0 100644 --- a/CourtApi.h +++ b/CourtApi.h @@ -3,10 +3,27 @@ #include +#include + +#include #include +#include -nlohmann::json findCases(const std::string_view& name); +struct CaseHistoryItem +{ + std::string date; + std::string time; + std::string status; + std::string publishDate; + std::string publishTime; +}; -nlohmann::json getCaseDetails(int courtId, const std::string_view& caseNumber); +nlohmann::json findCases(boost::asio::io_context& asioContext, const std::string_view& name); + +nlohmann::json getCaseDetails(boost::asio::io_context& asioContext, + int courtId, + const std::string_view& caseNumber); + +std::vector parseHistory(const nlohmann::json& details); #endif // COURT_MONITOR_COURT_API_H diff --git a/external/banana b/external/banana index 075ac78..787d34e 160000 --- a/external/banana +++ b/external/banana @@ -1 +1 @@ -Subproject commit 075ac78bba0950341c9c477ca38a523730857fee +Subproject commit 787d34ea6f616d3a37a09c7262c07d0ff1f0bef4 diff --git a/main.cpp b/main.cpp index 9ba33db..c68afcd 100644 --- a/main.cpp +++ b/main.cpp @@ -1,12 +1,13 @@ -#include "Asio.h" +#include "Bot.h" #include "CourtApi.h" #include "Storage.h" #include #include -#include +#include #include +#include #include #include @@ -14,49 +15,9 @@ #include #include -struct CaseHistoryItem -{ - std::string date; - std::string time; - std::string status; - std::string publishDate; - std::string publishTime; -}; +boost::asio::io_context asioContext; -std::vector parseHistory(const nlohmann::json& details) -{ - std::vector items; - const auto& history = details.at("history"); - for (const auto& obj : history) - { - CaseHistoryItem item; - item.date = obj.at("date").get(); - item.time = obj.at("time").get(); - item.status = obj.at("status").get(); - item.publishDate = obj.at("publish_date").get(); - item.publishTime = obj.at("publish_time").get(); - items.push_back(std::move(item)); - } - return items; -} - -void notifyUser(banana::agent::beast_callback& bot, - int userId, - const std::string& caseNumber, - std::string caseUrl, - const CaseHistoryItem& item) -{ - caseUrl = fmt::format("https://mirsud.spb.ru{}", caseUrl); - std::string message = fmt::format( - "Новое событие по делу [№{}]({}):\n" - "{}\n" - "Дата: {} {}\n", - caseNumber, caseUrl, item.status, item.date, item.time); - banana::api::send_message(bot, {.chat_id = userId, .text = message, .parse_mode = "markdown"}, - [](const auto&) {}); -} - -void processAllSubscriptions(LocalStorage& storage, banana::agent::beast_callback& bot) +void processAllSubscriptions(LocalStorage& storage, Bot& bot) { for (auto& subscription : storage.subscriptions) { @@ -66,13 +27,13 @@ void processAllSubscriptions(LocalStorage& storage, banana::agent::beast_callbac for (auto& counter : subscription.counters) { fmt::print("** Processing case {}\n", counter.caseNumber); - auto details = getCaseDetails(counter.courtId, counter.caseNumber); + auto details = getCaseDetails(asioContext, counter.courtId, counter.caseNumber); fmt::print("{}\n", details.dump()); auto url = details["url"].get(); auto history = parseHistory(details); for (std::size_t i = counter.value; i < history.size(); i++) - notifyUser(bot, subscription.userId, counter.caseNumber, url, history[i]); + bot.notifyUser(subscription.userId, counter.caseNumber, url, history[i]); counter.value = history.size(); } } @@ -84,17 +45,6 @@ void processAllSubscriptions(LocalStorage& storage, banana::agent::beast_callbac } } -void processUpdate(const banana::api::update_t& update) -{ - if (update.message) - { - if (update.message->text) - { - fmt::print("rx: {}\n", *update.message->text); - } - } -} - std::chrono::system_clock::time_point getNextCheckTime(std::uint32_t checkTime) { std::time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); @@ -116,39 +66,6 @@ void handleSignal(int) asioContext.stop(); } -int64_t offset = 0; - -void getUpdates(banana::agent::beast_callback& bot); - -void processUpdates(banana::agent::beast_callback bot, - banana::expected> updates) -{ - if (terminate) - { - fmt::print("exit\n"); - return; - } - - if (updates) - { - for (const auto& update : *updates) - { - processUpdate(update); - offset = update.update_id + 1; - } - } - else - fmt::print(stderr, "failed to get updates: {}\n", updates.error()); - - getUpdates(bot); -} - -void getUpdates(banana::agent::beast_callback& bot) -{ - banana::api::get_updates(bot, {.offset = offset, .timeout = 50}, - [bot](auto updates) { processUpdates(bot, std::move(updates)); }); -} - int main() { std::signal(SIGTERM, handleSignal); @@ -160,11 +77,9 @@ int main() loadStorage(storage); fmt::print("Storage loaded\n"); - // Инициализировать SSL - initSSL(); - // Создать бота - banana::agent::beast_callback bot(storage.token, asioContext, sslContext); + Bot bot(asioContext, storage, terminate); + bot.setupCommands(); // Создать таймер ежедневной проверки boost::asio::system_timer checkTimer(asioContext); @@ -181,9 +96,6 @@ int main() checkTimer.expires_at(getNextCheckTime(storage.checkTime)); checkTimer.async_wait(onTimer); - // Запустить асинхронное получение обновлений - getUpdates(bot); - // Запустить цикл обработки событий asioContext.run();