Добавлены сессии и диалоги.

This commit is contained in:
Kirill Kirilenko 2022-11-14 23:44:35 +03:00
parent 1b325a343c
commit 4309860ddb
12 changed files with 368 additions and 33 deletions

View file

@ -109,7 +109,7 @@ SpacesInConditionalStatement: false
SpacesInContainerLiterals: false SpacesInContainerLiterals: false
SpacesInParentheses: false SpacesInParentheses: false
SpacesInSquareBrackets: false SpacesInSquareBrackets: false
Standard: c++14 Standard: c++20
StatementMacros: ['Q_UNUSED'] StatementMacros: ['Q_UNUSED']
TabWidth: 4 TabWidth: 4
UseCRLF: false UseCRLF: false

70
Bot.cpp
View file

@ -16,15 +16,19 @@ Bot::Bot(boost::asio::io_context& asioContext, LocalStorage& storage, bool& term
terminationFlag_(terminationFlag), terminationFlag_(terminationFlag),
agent_(storage.token, asioContext, sslContext) agent_(storage.token, asioContext, sslContext)
{ {
setupCommands();
getUpdates(); getUpdates();
} }
void Bot::setupCommands() void Bot::setupCommands()
{ {
banana::api::set_my_commands_args_t args; banana::api::set_my_commands_args_t args;
args.commands.push_back(banana::api::bot_command_t{ args.commands.push_back(
"/subscribe_case", "Подписаться на обновления дела по его номеру"}); 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{"/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"}; args.scope = banana::api::bot_command_scope_all_private_chats_t{"all_private_chats"};
banana::api::set_my_commands(agent_, std::move(args), banana::api::set_my_commands(agent_, std::move(args),
[](banana::expected<banana::boolean_t> result) [](banana::expected<banana::boolean_t> result)
@ -68,39 +72,53 @@ void Bot::getUpdates()
getUpdates(); getUpdates();
} }
else 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<banana::string_t>{"message", "callback_query"}},
std::move(handler));
} }
void Bot::processUpdate(const banana::api::update_t& update) void Bot::processUpdate(const banana::api::update_t& update)
{ {
if (update.message) if (update.message)
{ {
banana::integer_t userId = update.message->from->id;
if (update.message->text) if (update.message->text)
{ LOG(bot, "incoming message: user={} text='{}'", userId, *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 else
{ LOG(bot, "incoming message: user={} (not text)", userId);
// TODO
}
}
else
LOG(bot, "skip message without text"); // TODO ответить
}
else
LOGE(bot, "skip unknown update type");
}
void Bot::processStartCommand(const banana::api::message_t& message) 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);
void Bot::processSubscribeCaseCommand(const banana::api::message_t& message) 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
LOG(bot, "skip unknown update type");
} }

11
Bot.h
View file

@ -1,35 +1,36 @@
#ifndef COURT_MONITOR_BOT_H #ifndef COURT_MONITOR_BOT_H
#define COURT_MONITOR_BOT_H #define COURT_MONITOR_BOT_H
#include "BotSession.h"
#include "CourtApi.h" #include "CourtApi.h"
#include "Storage.h" #include "Storage.h"
#include <banana/agent/beast.hpp> #include <banana/agent/beast.hpp>
#include <banana/types_fwd.hpp> #include <banana/types_fwd.hpp>
#include <map>
class Bot class Bot
{ {
public: public:
explicit Bot(boost::asio::io_context& asioContext, LocalStorage& storage, bool& terminationFlag); explicit Bot(boost::asio::io_context& asioContext, LocalStorage& storage, bool& terminationFlag);
void setupCommands();
void notifyUser(int userId, void notifyUser(int userId,
const std::string& caseNumber, const std::string& caseNumber,
std::string caseUrl, std::string caseUrl,
const CaseHistoryItem& item); const CaseHistoryItem& item);
private: private:
void setupCommands();
void getUpdates(); void getUpdates();
void processUpdate(const banana::api::update_t& update); 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_; LocalStorage& storage_;
bool& terminationFlag_; bool& terminationFlag_;
banana::agent::beast_callback agent_; banana::agent::beast_callback agent_;
std::int64_t updatesOffset_ = 0; std::int64_t updatesOffset_ = 0;
std::map<banana::integer_t, BotSession> sessions_;
}; };
#endif // COURT_MONITOR_BOT_H #endif // COURT_MONITOR_BOT_H

65
BotSession.cpp Normal file
View file

@ -0,0 +1,65 @@
#include "BotSession.h"
#include "Logger.h"
#include "SubscribeCaseDialog.h"
#include <banana/api.hpp>
#include <fmt/core.h>
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<SubscribeCaseDialog>();
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<banana::api::message_t>& message) {});
}
void BotSession::processStopCommand(const banana::api::message_t& message)
{
activeDialog_.reset();
}

36
BotSession.h Normal file
View file

@ -0,0 +1,36 @@
#ifndef COURT_MONITOR_BOT_SESSION_H
#define COURT_MONITOR_BOT_SESSION_H
#include <banana/agent/beast.hpp>
#include <banana/types_fwd.hpp>
#include <banana/utils/basic_types.hpp>
#include <memory>
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 <class T>
std::unique_ptr<Dialog> makeDialog()
{
return std::make_unique<T>(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<Dialog> activeDialog_;
};
#endif // COURT_MONITOR_BOT_SESSION_H

View file

@ -12,9 +12,12 @@ add_subdirectory(external)
add_executable(court_monitor add_executable(court_monitor
Bot.cpp Bot.cpp
BotSession.cpp
CourtApi.cpp CourtApi.cpp
Dialog.cpp
Logger.cpp Logger.cpp
Storage.cpp Storage.cpp
SubscribeCaseDialog.cpp
main.cpp main.cpp
) )
target_link_libraries(court_monitor target_link_libraries(court_monitor

24
Dialog.cpp Normal file
View file

@ -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_;
}

27
Dialog.h Normal file
View file

@ -0,0 +1,27 @@
#ifndef COURT_MONITOR_DIALOG_H
#define COURT_MONITOR_DIALOG_H
#include <banana/agent/beast.hpp>
#include <banana/types_fwd.hpp>
#include <banana/utils/basic_types.hpp>
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

55
DialogHelpers.h Normal file
View file

@ -0,0 +1,55 @@
#ifndef COURT_MONITOR_DIALOG_HELPERS_H
#define COURT_MONITOR_DIALOG_HELPERS_H
#include "Logger.h"
#include <banana/types.hpp>
#include <boost/statechart/state.hpp>
#include <boost/statechart/state_machine.hpp>
namespace statechart = boost::statechart;
template <class MostDerived, class InitialState>
struct StateMachine : public statechart::state_machine<MostDerived, InitialState>
{
explicit StateMachine(Dialog& dialog) : dialog(dialog) {}
Dialog& dialog;
};
struct BasicState
{
[[nodiscard]] virtual bool isFinal() const noexcept = 0;
};
template <class MostDerived, class Context, bool IsFinal = false>
class State : public statechart::state<MostDerived, Context>, public BasicState
{
public:
State(typename statechart::state<MostDerived, Context>::my_context ctx, const char* name)
: statechart::state<MostDerived, Context>(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<NewMessageEvent>
{
explicit NewMessageEvent(banana::api::message_t message) : message(std::move(message)) {}
banana::api::message_t message;
};
struct NewCallbackQueryEvent : statechart::event<NewCallbackQueryEvent>
{
explicit NewCallbackQueryEvent(banana::api::callback_query_t query) : query(std::move(query)) {}
banana::api::callback_query_t query;
};
#endif // COURT_MONITOR_DIALOG_HELPERS_H

86
SubscribeCaseDialog.cpp Normal file
View file

@ -0,0 +1,86 @@
#include "SubscribeCaseDialog.h"
#include "CourtApi.h"
#include "DialogHelpers.h"
#include "Logger.h"
#include <banana/api.hpp>
#include <boost/statechart/custom_reaction.hpp>
#include <boost/statechart/event.hpp>
namespace {
// clang-format off
// Состояния
struct WaitingForInput;
struct GettingCaseDetails;
struct WaitingForConfirmation;
// События
struct CaseDetailsFetched : statechart::event<CaseDetailsFetched> { };
struct SubscriptionConfirmed : statechart::event<SubscriptionConfirmed> { };
// clang-format on
} // namespace
struct SubscribeCaseStateMachine : StateMachine<SubscribeCaseStateMachine, WaitingForInput>
{
using StateMachine::StateMachine;
};
namespace {
struct WaitingForInput : State<WaitingForInput, SubscribeCaseStateMachine>
{
using reactions = statechart::custom_reaction<NewMessageEvent>;
explicit WaitingForInput(const my_context& ctx) : State(ctx, "WaitingForInput")
{
auto& dialog = context<SubscribeCaseStateMachine>().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<GettingCaseDetails>(); }
};
struct GettingCaseDetails : State<GettingCaseDetails, SubscribeCaseStateMachine, true>
{
explicit GettingCaseDetails(const my_context& ctx) : State(ctx, "GettingCaseDetails") {}
};
struct WaitingForConfirmation : State<WaitingForConfirmation, SubscribeCaseStateMachine>
{
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<SubscribeCaseStateMachine>(*this))
{
machine_->initiate();
}
SubscribeCaseDialog::~SubscribeCaseDialog() = default;
bool SubscribeCaseDialog::processMessage(const banana::api::message_t& message)
{
machine_->process_event(NewMessageEvent{message});
return machine_->state_cast<const BasicState&>().isFinal();
}
bool SubscribeCaseDialog::processCallbackQuery(const banana::api::callback_query_t& query)
{
machine_->process_event(NewCallbackQueryEvent{query});
return machine_->state_cast<const BasicState&>().isFinal();
}

21
SubscribeCaseDialog.h Normal file
View file

@ -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<SubscribeCaseStateMachine> machine_;
};
#endif // COURT_MONITOR_SUBSCRIBE_CASE_DIALOG_H

View file

@ -79,7 +79,6 @@ int main()
// Создать бота // Создать бота
Bot bot(asioContext, storage, terminate); Bot bot(asioContext, storage, terminate);
bot.setupCommands();
// Создать таймер ежедневной проверки // Создать таймер ежедневной проверки
boost::asio::system_timer checkTimer(asioContext); boost::asio::system_timer checkTimer(asioContext);