Наработки по машинам состояния.

This commit is contained in:
Kirill Kirilenko 2022-11-19 21:42:18 +03:00
parent 4309860ddb
commit 193dabe805
13 changed files with 293 additions and 108 deletions

27
Bot.cpp
View file

@ -94,31 +94,30 @@ void Bot::processUpdate(const banana::api::update_t& update)
else
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);
auto& session = getOrCreateSession(userId);
session.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& session = getOrCreateSession(userId);
session.processCallbackQuery(*update.callback_query);
}
else
LOG(bot, "skip unknown update type");
}
BotSession& Bot::getOrCreateSession(banana::integer_t 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));
std::forward_as_tuple(agent_, userId, storage_));
}
sessionIt->second.processCallbackQuery(*update.callback_query);
}
else
LOG(bot, "skip unknown update type");
return sessionIt->second;
}

1
Bot.h
View file

@ -25,6 +25,7 @@ private:
void getUpdates();
void processUpdate(const banana::api::update_t& update);
BotSession& getOrCreateSession(banana::integer_t userId);
LocalStorage& storage_;
bool& terminationFlag_;

View file

@ -1,13 +1,16 @@
#include "BotSession.h"
#include "Logger.h"
#include "Storage.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)
BotSession::BotSession(banana::agent::beast_callback& agent,
banana::integer_t userId,
LocalStorage& storage)
: agent_(agent), userId_(userId), storage_(storage)
{
LOG(session, "new session created for user {}", userId_);
}
@ -20,9 +23,9 @@ void BotSession::processMessage(const banana::api::message_t& message)
{
auto text = *message.text;
if (text == "/start")
processStartCommand(message);
processStartCommand();
else if (text == "/stop")
processStopCommand(message);
processStopCommand();
else if (text == "/subscribe_case")
activeDialog_ = makeDialog<SubscribeCaseDialog>();
else if (activeDialog_)
@ -30,13 +33,13 @@ void BotSession::processMessage(const banana::api::message_t& message)
if (activeDialog_->processMessage(message))
{
// TODO
processStartCommand(message);
processStartCommand();
}
}
else
{
// TODO
processStartCommand(message);
processStartCommand();
}
}
else
@ -45,9 +48,21 @@ void BotSession::processMessage(const banana::api::message_t& message)
void BotSession::processCallbackQuery(const banana::api::callback_query_t& query)
{
if (query.data)
{
if (activeDialog_)
{
if (activeDialog_->processCallbackQuery(query))
processStartCommand();
}
else
LOGE(session, "skip callback query, because there is no active dialog");
}
else
LOGE(session, "skip callback query without data");
}
void BotSession::processStartCommand(const banana::api::message_t& message)
void BotSession::processStartCommand()
{
activeDialog_.reset();
@ -59,7 +74,7 @@ void BotSession::processStartCommand(const banana::api::message_t& message)
[](const banana::expected<banana::api::message_t>& message) {});
}
void BotSession::processStopCommand(const banana::api::message_t& message)
void BotSession::processStopCommand()
{
activeDialog_.reset();
}

View file

@ -1,6 +1,8 @@
#ifndef COURT_MONITOR_BOT_SESSION_H
#define COURT_MONITOR_BOT_SESSION_H
#include "Storage.h"
#include <banana/agent/beast.hpp>
#include <banana/types_fwd.hpp>
#include <banana/utils/basic_types.hpp>
@ -12,7 +14,7 @@ class Dialog;
class BotSession final
{
public:
BotSession(banana::agent::beast_callback& agent, banana::integer_t userId);
BotSession(banana::agent::beast_callback& agent, banana::integer_t userId, LocalStorage& storage);
~BotSession();
void processMessage(const banana::api::message_t& message);
@ -22,14 +24,15 @@ private:
template <class T>
std::unique_ptr<Dialog> makeDialog()
{
return std::make_unique<T>(agent_, userId_);
return std::make_unique<T>(agent_, userId_, storage_);
}
void processStartCommand(const banana::api::message_t& message);
void processStopCommand(const banana::api::message_t& message);
void processStartCommand();
void processStopCommand();
banana::agent::beast_callback& agent_;
banana::integer_t userId_;
LocalStorage& storage_;
std::unique_ptr<Dialog> activeDialog_;
};

View file

@ -1,5 +1,7 @@
#include "CourtApi.h"
#include "Logger.h"
#include <fmt/format.h>
#include <nlohmann/json.hpp>
@ -9,7 +11,6 @@
#include <boost/certify/extensions.hpp>
#include <boost/certify/https_verification.hpp>
#include <iostream>
#include <thread>
const char* serverDomain = "mirsud.spb.ru";
@ -24,15 +25,16 @@ ssl_stream connect(boost::asio::io_context& asioContext, const std::string& host
sslContext.set_verify_mode(boost::asio::ssl::verify_peer |
boost::asio::ssl::verify_fail_if_no_peer_cert);
sslContext.set_default_verify_paths();
sslContext.load_verify_file("ISRG_X1.pem");
boost::certify::enable_native_https_server_verification(sslContext);
ssl_stream stream(asioContext, sslContext);
static boost::asio::ip::tcp::resolver resolver(asioContext);
auto const results = resolver.resolve(hostname, "https");
boost::beast::get_lowest_layer(stream).connect(results);
boost::certify::set_server_hostname(stream, hostname);
boost::certify::sni_hostname(stream, hostname);
boost::beast::get_lowest_layer(stream).connect(results);
stream.handshake(boost::asio::ssl::stream_base::client);
return stream;
@ -56,14 +58,11 @@ std::pair<int, std::string> get(ssl_stream& stream,
request.body() = std::move(*payload);
}
std::cout << "tx: " << request << std::endl;
boost::beast::http::write(stream, request);
boost::beast::http::response<boost::beast::http::string_body> response;
boost::beast::flat_buffer buffer;
boost::beast::http::read(stream, buffer, response);
std::cout << "rx: " << response << std::endl;
return {response.base().result_int(), response.body()};
}
@ -83,27 +82,58 @@ nlohmann::json getResults(ssl_stream& stream, const std::string_view& uuid)
fmt::format("failed to retrieve JSON (server returned code {})", status));
}
nlohmann::json getCaseDetails(boost::asio::io_context& asioContext,
int courtId,
const std::string_view& caseNumber)
CaseDetails getCaseDetails(boost::asio::io_context& asioContext, const std::string_view& caseNumber)
{
ssl_stream stream = connect(asioContext, serverDomain);
int status;
std::string result;
std::tie(status, result) =
get(stream, serverDomain,
fmt::format("/cases/api/detail/?id={}&court_site_id={}", caseNumber, courtId));
get(stream, serverDomain, fmt::format("/cases/api/detail/?id={}", caseNumber));
if (status == 200)
{
auto uuid = nlohmann::json::parse(result).at("id").get<std::string>();
for (int i = 0; i < 10; i++)
for (int i = 0; i < 5; i++)
{
auto results = getResults(stream, uuid);
bool finished = results.at("finished").get<bool>();
if (finished)
return results.at("result");
auto response = getResults(stream, uuid);
if (response.at("finished").get<bool>())
{
auto& results = response.at("result");
LOG(court, results.dump());
CaseDetails details;
details.id = results["id"].get<std::string>();
details.courtNumber = results["court_number"].get<std::string>();
details.name = results["name"].get<std::string>();
details.description = results["description"].get<std::string>();
details.url =
fmt::format("https://{}{}", serverDomain, results["url"].get<std::string>());
details.districtName = results["district_name"].get<std::string>();
details.judgeName = results["judge"].get<std::string>();
for (const auto& participant : results["participants"])
{
CaseParticipant p;
p.title = participant["title"].get<std::string>();
p.name = participant["name"].get<std::string>();
details.participants.push_back(std::move(p));
}
for (const auto& obj : results["history"])
{
CaseHistoryItem item;
item.date = obj.at("date").get<std::string>();
item.time = obj.at("time").get<std::string>();
item.status = obj.at("status").get<std::string>();
item.publishDate = obj.at("publish_date").get<std::string>();
item.publishTime = obj.at("publish_time").get<std::string>();
details.history.push_back(std::move(item));
}
return details;
}
else
std::this_thread::sleep_for(std::chrono::seconds(1));
}
@ -113,20 +143,3 @@ nlohmann::json getCaseDetails(boost::asio::io_context& asioContext,
throw std::runtime_error(
fmt::format("failed to retrieve JSON (server returned code {})", status));
}
std::vector<CaseHistoryItem> parseHistory(const nlohmann::json& details)
{
std::vector<CaseHistoryItem> items;
const auto& history = details.at("history");
for (const auto& obj : history)
{
CaseHistoryItem item;
item.date = obj.at("date").get<std::string>();
item.time = obj.at("time").get<std::string>();
item.status = obj.at("status").get<std::string>();
item.publishDate = obj.at("publish_date").get<std::string>();
item.publishTime = obj.at("publish_time").get<std::string>();
items.push_back(std::move(item));
}
return items;
}

View file

@ -9,6 +9,12 @@
#include <string_view>
#include <vector>
struct CaseParticipant
{
std::string title;
std::string name;
};
struct CaseHistoryItem
{
std::string date;
@ -18,12 +24,29 @@ struct CaseHistoryItem
std::string publishTime;
};
nlohmann::json findCases(boost::asio::io_context& asioContext, const std::string_view& name);
struct CaseDetails
{
std::string id;
std::string courtNumber;
std::string name;
std::string description;
std::string url;
nlohmann::json getCaseDetails(boost::asio::io_context& asioContext,
int courtId,
const std::string_view& caseNumber);
std::string districtName;
std::string judgeName;
std::vector<CaseParticipant> participants;
std::vector<CaseHistoryItem> parseHistory(const nlohmann::json& details);
std::string status;
std::vector<CaseHistoryItem> history;
std::string createdDate;
std::string acceptedDate;
std::string judicialUid;
};
std::vector<CaseDetails> findCases(boost::asio::io_context& asioContext,
const std::string_view& name);
CaseDetails getCaseDetails(boost::asio::io_context& asioContext, const std::string_view& caseNumber);
#endif // COURT_MONITOR_COURT_API_H

View file

@ -2,8 +2,7 @@
#include "Logger.h"
Dialog::Dialog(banana::agent::beast_callback& agent, banana::integer_t userId, const char* name)
: agent_(agent), userId_(userId), name_(name)
Dialog::Dialog(banana::integer_t userId, const char* name) : userId_(userId), name_(name)
{
LOG(dialog, "{} dialog created for user {}", name_, userId_);
}
@ -12,13 +11,3 @@ 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_;
}

View file

@ -1,25 +1,20 @@
#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);
Dialog(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_;
};

View file

@ -2,6 +2,7 @@
#define COURT_MONITOR_DIALOG_HELPERS_H
#include "Logger.h"
#include "Storage.h"
#include <banana/types.hpp>
@ -13,8 +14,13 @@ 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;
explicit StateMachine(banana::agent::beast_callback& agent, banana::integer_t userId)
: agent(agent), userId(userId)
{
}
banana::agent::beast_callback& agent;
banana::integer_t userId;
};
struct BasicState

31
ISRG_X1.pem Normal file
View file

@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----

View file

@ -5,10 +5,13 @@
#include "Logger.h"
#include <banana/api.hpp>
#include <nlohmann/json.hpp>
#include <boost/statechart/custom_reaction.hpp>
#include <boost/statechart/event.hpp>
#include <regex>
namespace {
// clang-format off
@ -17,6 +20,7 @@ namespace {
struct WaitingForInput;
struct GettingCaseDetails;
struct WaitingForConfirmation;
struct Subscribed;
// События
struct CaseDetailsFetched : statechart::event<CaseDetailsFetched> { };
@ -28,7 +32,12 @@ struct SubscriptionConfirmed : statechart::event<SubscriptionConfirmed> { };
struct SubscribeCaseStateMachine : StateMachine<SubscribeCaseStateMachine, WaitingForInput>
{
using StateMachine::StateMachine;
SubscribeCaseStateMachine(banana::agent::beast_callback& agent, long userId, LocalStorage& storage)
: StateMachine(agent, userId), storage(storage)
{
}
LocalStorage& storage;
std::string caseNumber;
};
namespace {
@ -39,24 +48,122 @@ struct WaitingForInput : State<WaitingForInput, SubscribeCaseStateMachine>
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) {});
auto& machine = context<SubscribeCaseStateMachine>();
std::string text = "Введите номер дела...";
banana::api::send_message(machine.agent,
{.chat_id = machine.userId, .text = std::move(text)}, [](auto) {});
}
statechart::result react(const NewMessageEvent& event) { return transit<GettingCaseDetails>(); }
statechart::result react(const NewMessageEvent& event)
{
auto& machine = context<SubscribeCaseStateMachine>();
const std::regex rex(R"(\d-(?:\d+)/(?:\d){4}-(?:\d+))");
std::smatch captures;
if (std::regex_match(*event.message.text, captures, rex))
{
machine.caseNumber = *event.message.text;
return transit<GettingCaseDetails>();
}
else
{
std::string text =
"Некорректный формат номера дела!\n"
"Попробуйте еще раз.";
banana::api::send_message(
machine.agent, {.chat_id = machine.userId, .text = std::move(text)}, [](auto) {});
return discard_event();
}
}
};
struct GettingCaseDetails : State<GettingCaseDetails, SubscribeCaseStateMachine, true>
struct GettingCaseDetails : State<GettingCaseDetails, SubscribeCaseStateMachine>
{
explicit GettingCaseDetails(const my_context& ctx) : State(ctx, "GettingCaseDetails") {}
using reactions = statechart::custom_reaction<CaseDetailsFetched>;
explicit GettingCaseDetails(const my_context& ctx) : State(ctx, "GettingCaseDetails")
{
auto& machine = context<SubscribeCaseStateMachine>();
boost::asio::io_context ioContext;
try
{
auto details = getCaseDetails(ioContext, machine.caseNumber);
std::string text;
fmt::format_to(std::back_inserter(text), "Проверьте информацию:\n{}\n", details.name);
for (const auto& participant : details.participants)
fmt::format_to(std::back_inserter(text), "{}: {}\n", participant.title,
participant.name);
fmt::format_to(std::back_inserter(text), "Судья: {}", details.judgeName);
banana::api::inline_keyboard_markup_t keyboard;
keyboard.inline_keyboard.resize(1);
keyboard.inline_keyboard[0].resize(2);
keyboard.inline_keyboard[0][0].text = "Верно";
keyboard.inline_keyboard[0][0].callback_data = "yes";
keyboard.inline_keyboard[0][1].text = "Отмена";
keyboard.inline_keyboard[0][1].callback_data = "no";
banana::api::send_message(
machine.agent,
{.chat_id = machine.userId, .text = std::move(text), .reply_markup = keyboard},
[](auto) {});
post_event(CaseDetailsFetched());
}
catch (const std::exception& e)
{
LOGE(dialog, e.what());
// TODO ???
}
}
statechart::result react(const CaseDetailsFetched& event)
{
return transit<WaitingForConfirmation>();
}
};
struct WaitingForConfirmation : State<WaitingForConfirmation, SubscribeCaseStateMachine>
{
using reactions = statechart::custom_reaction<NewCallbackQueryEvent>;
explicit WaitingForConfirmation(const my_context& ctx) : State(ctx, "WaitingForConfirmation") {}
statechart::result react(const NewCallbackQueryEvent& event)
{
auto& machine = context<SubscribeCaseStateMachine>();
if (event.query.data)
{
if (event.query.message)
banana::api::edit_message_reply_markup(
machine.agent,
{.chat_id = event.query.message->chat.id,
.message_id = event.query.message->message_id},
[](banana::expected<banana::variant_t<banana::api::message_t, banana::boolean_t>> result)
{
if (!result)
LOGE(dialog, result.error());
});
if (*event.query.data == "yes")
{
// TODO
return transit<Subscribed>();
}
else if (*event.query.data == "no")
{
return transit<WaitingForInput>();
}
}
return discard_event();
}
};
struct Subscribed : State<Subscribed, SubscribeCaseStateMachine, true>
{
explicit Subscribed(const my_context& ctx) : State(ctx, "Subscribed") {}
};
} // namespace
@ -64,9 +171,10 @@ struct WaitingForConfirmation : State<WaitingForConfirmation, SubscribeCaseState
/////////////////////////////////////////////////////////////////////////////
SubscribeCaseDialog::SubscribeCaseDialog(banana::agent::beast_callback& agent,
banana::integer_t userId)
: Dialog(agent, userId, "SubscribeCase"),
machine_(std::make_unique<SubscribeCaseStateMachine>(*this))
banana::integer_t userId,
LocalStorage& storage)
: Dialog(userId, "SubscribeCase"),
machine_(std::make_unique<SubscribeCaseStateMachine>(agent, userId, storage))
{
machine_->initiate();
}

View file

@ -2,13 +2,18 @@
#define COURT_MONITOR_SUBSCRIBE_CASE_DIALOG_H
#include "Dialog.h"
#include "Storage.h"
#include <banana/agent/beast.hpp>
struct SubscribeCaseStateMachine;
class SubscribeCaseDialog : public Dialog
{
public:
SubscribeCaseDialog(banana::agent::beast_callback& agent, banana::integer_t userId);
SubscribeCaseDialog(banana::agent::beast_callback& agent,
banana::integer_t userId,
LocalStorage& storage);
~SubscribeCaseDialog() override;
bool processMessage(const banana::api::message_t& message) override;

View file

@ -27,14 +27,11 @@ void processAllSubscriptions(LocalStorage& storage, Bot& bot)
for (auto& counter : subscription.counters)
{
LOG(main, "** Processing case {}", counter.caseNumber);
auto details = getCaseDetails(asioContext, counter.courtId, counter.caseNumber);
LOG(main, details.dump());
auto url = details["url"].get<std::string>();
auto history = parseHistory(details);
for (std::size_t i = counter.value; i < history.size(); i++)
bot.notifyUser(subscription.userId, counter.caseNumber, url, history[i]);
counter.value = history.size();
auto details = getCaseDetails(asioContext, counter.caseNumber);
for (std::size_t i = counter.value; i < details.history.size(); i++)
bot.notifyUser(subscription.userId, counter.caseNumber, details.url,
details.history[i]);
counter.value = details.history.size();
}
}
catch (const std::exception& e)