From 4309860ddbe1bceff1eea3611ad66f6e7a5e1318 Mon Sep 17 00:00:00 2001 From: Kirill Kirilenko Date: Mon, 14 Nov 2022 23:44:35 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=81=D0=B5=D1=81=D1=81=D0=B8=D0=B8=20=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=B8=D0=B0=D0=BB=D0=BE=D0=B3=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .clang-format | 2 +- Bot.cpp | 70 ++++++++++++++++++++------------- Bot.h | 11 +++--- BotSession.cpp | 65 +++++++++++++++++++++++++++++++ BotSession.h | 36 +++++++++++++++++ CMakeLists.txt | 3 ++ Dialog.cpp | 24 ++++++++++++ Dialog.h | 27 +++++++++++++ DialogHelpers.h | 55 ++++++++++++++++++++++++++ SubscribeCaseDialog.cpp | 86 +++++++++++++++++++++++++++++++++++++++++ SubscribeCaseDialog.h | 21 ++++++++++ main.cpp | 1 - 12 files changed, 368 insertions(+), 33 deletions(-) create mode 100644 BotSession.cpp create mode 100644 BotSession.h create mode 100644 Dialog.cpp create mode 100644 Dialog.h create mode 100644 DialogHelpers.h create mode 100644 SubscribeCaseDialog.cpp create mode 100644 SubscribeCaseDialog.h diff --git a/.clang-format b/.clang-format index 8e54c19..a341d41 100644 --- a/.clang-format +++ b/.clang-format @@ -109,7 +109,7 @@ SpacesInConditionalStatement: false SpacesInContainerLiterals: false SpacesInParentheses: false SpacesInSquareBrackets: false -Standard: c++14 +Standard: c++20 StatementMacros: ['Q_UNUSED'] TabWidth: 4 UseCRLF: false diff --git a/Bot.cpp b/Bot.cpp index 1b454d5..97758b4 100644 --- a/Bot.cpp +++ b/Bot.cpp @@ -16,15 +16,19 @@ Bot::Bot(boost::asio::io_context& asioContext, LocalStorage& storage, bool& term terminationFlag_(terminationFlag), agent_(storage.token, asioContext, sslContext) { + setupCommands(); 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.commands.push_back( + banana::api::bot_command_t{"/subscribe_case", "Подписаться на обновления дела"}); + args.commands.push_back( + banana::api::bot_command_t{"/unsubscribe_case", "Отписаться от обновлений дела"}); + args.commands.push_back(banana::api::bot_command_t{"/check_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) @@ -68,39 +72,53 @@ void Bot::getUpdates() getUpdates(); } else - LOG(bot, "failed to get updates: {}", updates.error()); + LOGE(bot, "failed to get updates: {}", updates.error()); }; - banana::api::get_updates(agent_, {.offset = updatesOffset_, .timeout = 50}, std::move(handler)); + banana::api::get_updates( + agent_, + {.offset = updatesOffset_, + .timeout = 50, + .allowed_updates = banana::array_t{"message", "callback_query"}}, + std::move(handler)); } void Bot::processUpdate(const banana::api::update_t& update) { if (update.message) { + banana::integer_t userId = update.message->from->id; + if (update.message->text) - { - LOG(bot, "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 - } - } + LOG(bot, "incoming message: user={} text='{}'", userId, *update.message->text); else - LOG(bot, "skip message without text"); // TODO ответить + LOG(bot, "incoming message: user={} (not text)", userId); + + auto sessionIt = sessions_.find(userId); + if (sessionIt == sessions_.end()) + { + bool ok; + std::tie(sessionIt, ok) = + sessions_.emplace(std::piecewise_construct, std::forward_as_tuple(userId), + std::forward_as_tuple(agent_, userId)); + } + sessionIt->second.processMessage(*update.message); + } + else if (update.callback_query) + { + banana::integer_t userId = update.callback_query->from.id; + LOG(bot, "incoming callback query: user={} data='{}'", userId, *update.callback_query->data); + + auto sessionIt = sessions_.find(userId); + if (sessionIt == sessions_.end()) + { + bool ok; + std::tie(sessionIt, ok) = + sessions_.emplace(std::piecewise_construct, std::forward_as_tuple(userId), + std::forward_as_tuple(agent_, userId)); + } + sessionIt->second.processCallbackQuery(*update.callback_query); } else - LOGE(bot, "skip unknown update type"); -} - -void Bot::processStartCommand(const banana::api::message_t& message) -{ -} - -void Bot::processSubscribeCaseCommand(const banana::api::message_t& message) -{ + LOG(bot, "skip unknown update type"); } diff --git a/Bot.h b/Bot.h index 376e0f7..bd609dd 100644 --- a/Bot.h +++ b/Bot.h @@ -1,35 +1,36 @@ #ifndef COURT_MONITOR_BOT_H #define COURT_MONITOR_BOT_H +#include "BotSession.h" #include "CourtApi.h" #include "Storage.h" #include #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 setupCommands(); + 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; + std::map sessions_; }; #endif // COURT_MONITOR_BOT_H diff --git a/BotSession.cpp b/BotSession.cpp new file mode 100644 index 0000000..8a2dc11 --- /dev/null +++ b/BotSession.cpp @@ -0,0 +1,65 @@ +#include "BotSession.h" + +#include "Logger.h" +#include "SubscribeCaseDialog.h" + +#include +#include + +BotSession::BotSession(banana::agent::beast_callback& agent, banana::integer_t userId) + : agent_(agent), userId_(userId) +{ + LOG(session, "new session created for user {}", userId_); +} + +BotSession::~BotSession() = default; + +void BotSession::processMessage(const banana::api::message_t& message) +{ + if (message.text) + { + auto text = *message.text; + if (text == "/start") + processStartCommand(message); + else if (text == "/stop") + processStopCommand(message); + else if (text == "/subscribe_case") + activeDialog_ = makeDialog(); + else if (activeDialog_) + { + if (activeDialog_->processMessage(message)) + { + // TODO + processStartCommand(message); + } + } + else + { + // TODO + processStartCommand(message); + } + } + else + LOGE(session, "skip message without text"); // TODO ответить +} + +void BotSession::processCallbackQuery(const banana::api::callback_query_t& query) +{ +} + +void BotSession::processStartCommand(const banana::api::message_t& message) +{ + activeDialog_.reset(); + + std::string text = + "Вас приветствует неофициальный бот Мирового Суда г. Санкт-Петербурга.\n\n" + "Выберите в меню желаемое действие."; + + banana::api::send_message(agent_, {.chat_id = userId_, .text = std::move(text)}, + [](const banana::expected& message) {}); +} + +void BotSession::processStopCommand(const banana::api::message_t& message) +{ + activeDialog_.reset(); +} diff --git a/BotSession.h b/BotSession.h new file mode 100644 index 0000000..494b15c --- /dev/null +++ b/BotSession.h @@ -0,0 +1,36 @@ +#ifndef COURT_MONITOR_BOT_SESSION_H +#define COURT_MONITOR_BOT_SESSION_H + +#include +#include +#include + +#include + +class Dialog; + +class BotSession final +{ +public: + BotSession(banana::agent::beast_callback& agent, banana::integer_t userId); + ~BotSession(); + + void processMessage(const banana::api::message_t& message); + void processCallbackQuery(const banana::api::callback_query_t& query); + +private: + template + std::unique_ptr makeDialog() + { + return std::make_unique(agent_, userId_); + } + + void processStartCommand(const banana::api::message_t& message); + void processStopCommand(const banana::api::message_t& message); + + banana::agent::beast_callback& agent_; + banana::integer_t userId_; + std::unique_ptr activeDialog_; +}; + +#endif // COURT_MONITOR_BOT_SESSION_H diff --git a/CMakeLists.txt b/CMakeLists.txt index 416fe7d..17690e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,9 +12,12 @@ add_subdirectory(external) add_executable(court_monitor Bot.cpp + BotSession.cpp CourtApi.cpp + Dialog.cpp Logger.cpp Storage.cpp + SubscribeCaseDialog.cpp main.cpp ) target_link_libraries(court_monitor diff --git a/Dialog.cpp b/Dialog.cpp new file mode 100644 index 0000000..1db9e6c --- /dev/null +++ b/Dialog.cpp @@ -0,0 +1,24 @@ +#include "Dialog.h" + +#include "Logger.h" + +Dialog::Dialog(banana::agent::beast_callback& agent, banana::integer_t userId, const char* name) + : agent_(agent), userId_(userId), name_(name) +{ + LOG(dialog, "{} dialog created for user {}", name_, userId_); +} + +Dialog::~Dialog() +{ + LOG(dialog, "{} dialog for user {} destroyed", name_, userId_); +} + +banana::agent::beast_callback& Dialog::getAgent() const +{ + return agent_; +} + +banana::integer_t Dialog::getUserId() const +{ + return userId_; +} diff --git a/Dialog.h b/Dialog.h new file mode 100644 index 0000000..bd140a1 --- /dev/null +++ b/Dialog.h @@ -0,0 +1,27 @@ +#ifndef COURT_MONITOR_DIALOG_H +#define COURT_MONITOR_DIALOG_H + +#include +#include +#include + +class Dialog +{ +public: + Dialog(banana::agent::beast_callback& agent, banana::integer_t userId, const char* name); + virtual ~Dialog(); + + [[nodiscard]] banana::agent::beast_callback& getAgent() const; + [[nodiscard]] banana::integer_t getUserId() const; + + // Возвращают true, если диалог завершен. + virtual bool processMessage(const banana::api::message_t& message) = 0; + virtual bool processCallbackQuery(const banana::api::callback_query_t& query) = 0; + +private: + banana::agent::beast_callback& agent_; + banana::integer_t userId_; + const char* name_; +}; + +#endif // COURT_MONITOR_DIALOG_H diff --git a/DialogHelpers.h b/DialogHelpers.h new file mode 100644 index 0000000..ff10612 --- /dev/null +++ b/DialogHelpers.h @@ -0,0 +1,55 @@ +#ifndef COURT_MONITOR_DIALOG_HELPERS_H +#define COURT_MONITOR_DIALOG_HELPERS_H + +#include "Logger.h" + +#include + +#include +#include + +namespace statechart = boost::statechart; + +template +struct StateMachine : public statechart::state_machine +{ + explicit StateMachine(Dialog& dialog) : dialog(dialog) {} + Dialog& dialog; +}; + +struct BasicState +{ + [[nodiscard]] virtual bool isFinal() const noexcept = 0; +}; + +template +class State : public statechart::state, public BasicState +{ +public: + State(typename statechart::state::my_context ctx, const char* name) + : statechart::state(ctx), name_(name) + { + LOG(dialog, "entering state {}", name_); + } + + ~State() { LOG(dialog, "leaving state {}", name_); } + + [[nodiscard]] bool isFinal() const noexcept override { return IsFinal; } + +private: + const char* name_; +}; + +struct NewMessageEvent : statechart::event +{ + explicit NewMessageEvent(banana::api::message_t message) : message(std::move(message)) {} + banana::api::message_t message; +}; + +struct NewCallbackQueryEvent : statechart::event +{ + explicit NewCallbackQueryEvent(banana::api::callback_query_t query) : query(std::move(query)) {} + banana::api::callback_query_t query; +}; + +#endif // COURT_MONITOR_DIALOG_HELPERS_H diff --git a/SubscribeCaseDialog.cpp b/SubscribeCaseDialog.cpp new file mode 100644 index 0000000..5c82fe2 --- /dev/null +++ b/SubscribeCaseDialog.cpp @@ -0,0 +1,86 @@ +#include "SubscribeCaseDialog.h" + +#include "CourtApi.h" +#include "DialogHelpers.h" +#include "Logger.h" + +#include + +#include +#include + +namespace { + +// clang-format off + +// Состояния +struct WaitingForInput; +struct GettingCaseDetails; +struct WaitingForConfirmation; + +// События +struct CaseDetailsFetched : statechart::event { }; +struct SubscriptionConfirmed : statechart::event { }; + +// clang-format on + +} // namespace + +struct SubscribeCaseStateMachine : StateMachine +{ + using StateMachine::StateMachine; +}; + +namespace { + +struct WaitingForInput : State +{ + using reactions = statechart::custom_reaction; + + explicit WaitingForInput(const my_context& ctx) : State(ctx, "WaitingForInput") + { + auto& dialog = context().dialog; + std::string text = "Введите номер дела"; + banana::api::send_message(dialog.getAgent(), + {.chat_id = dialog.getUserId(), .text = std::move(text)}, + [](auto) {}); + } + + statechart::result react(const NewMessageEvent& event) { return transit(); } +}; + +struct GettingCaseDetails : State +{ + explicit GettingCaseDetails(const my_context& ctx) : State(ctx, "GettingCaseDetails") {} +}; + +struct WaitingForConfirmation : State +{ + explicit WaitingForConfirmation(const my_context& ctx) : State(ctx, "WaitingForConfirmation") {} +}; + +} // namespace + +///////////////////////////////////////////////////////////////////////////// + +SubscribeCaseDialog::SubscribeCaseDialog(banana::agent::beast_callback& agent, + banana::integer_t userId) + : Dialog(agent, userId, "SubscribeCase"), + machine_(std::make_unique(*this)) +{ + machine_->initiate(); +} + +SubscribeCaseDialog::~SubscribeCaseDialog() = default; + +bool SubscribeCaseDialog::processMessage(const banana::api::message_t& message) +{ + machine_->process_event(NewMessageEvent{message}); + return machine_->state_cast().isFinal(); +} + +bool SubscribeCaseDialog::processCallbackQuery(const banana::api::callback_query_t& query) +{ + machine_->process_event(NewCallbackQueryEvent{query}); + return machine_->state_cast().isFinal(); +} diff --git a/SubscribeCaseDialog.h b/SubscribeCaseDialog.h new file mode 100644 index 0000000..80cf4c8 --- /dev/null +++ b/SubscribeCaseDialog.h @@ -0,0 +1,21 @@ +#ifndef COURT_MONITOR_SUBSCRIBE_CASE_DIALOG_H +#define COURT_MONITOR_SUBSCRIBE_CASE_DIALOG_H + +#include "Dialog.h" + +struct SubscribeCaseStateMachine; + +class SubscribeCaseDialog : public Dialog +{ +public: + SubscribeCaseDialog(banana::agent::beast_callback& agent, banana::integer_t userId); + ~SubscribeCaseDialog() override; + + bool processMessage(const banana::api::message_t& message) override; + bool processCallbackQuery(const banana::api::callback_query_t& query) override; + +private: + std::unique_ptr machine_; +}; + +#endif // COURT_MONITOR_SUBSCRIBE_CASE_DIALOG_H diff --git a/main.cpp b/main.cpp index 3db82e0..a69fd2e 100644 --- a/main.cpp +++ b/main.cpp @@ -79,7 +79,6 @@ int main() // Создать бота Bot bot(asioContext, storage, terminate); - bot.setupCommands(); // Создать таймер ежедневной проверки boost::asio::system_timer checkTimer(asioContext);