Одиночный проход по всем подпискам из конфига.

This commit is contained in:
Kirill Kirilenko 2022-10-27 18:50:15 +03:00
commit 0de99a97e8
16 changed files with 629 additions and 0 deletions

118
.clang-format Normal file
View file

@ -0,0 +1,118 @@
---
BasedOnStyle: WebKit
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: false
AlignConsecutiveBitFields: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: DontAlign
AlignOperands: Align
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: true
AllowAllConstructorInitializersOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortEnumsOnASingleLine: false
AllowShortFunctionsOnASingleLine: InlineOnly
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: Inline
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakTemplateDeclarations: Yes
BinPackArguments: true
BinPackParameters: false
BraceWrapping:
AfterCaseLabel: true
AfterClass: true
AfterControlStatement: Always
AfterEnum: true
AfterFunction: true
AfterNamespace: false
AfterStruct: true
AfterUnion: true
AfterExternBlock: false
BeforeCatch: true
BeforeElse: true
BeforeLambdaBody: true
BeforeWhile: true
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Custom
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: BeforeColon
BreakInheritanceList: BeforeColon
BreakStringLiterals: true
ColumnLimit: 100
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DeriveLineEnding: true
DerivePointerAlignment: false
DisableFormat: false
FixNamespaceComments: true
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^.*Private[.]h"'
Priority: 0
- Regex: '^".*[.]pb[.]h"'
Priority: 2
- Regex: '^".*[.]h"'
Priority: 1
- Regex: '^<boost/.*>'
Priority: 5
- Regex: '^<.*[.](h|hpp|hxx)>'
Priority: 3
- Regex: '^<queue>'
Priority: 6
- Regex: '^<Q.*>'
Priority: 4
- Regex: '^<.*>'
Priority: 6
IndentCaseBlocks: false
IndentCaseLabels: false
IndentExternBlock: NoIndent
IndentGotoLabels: false
IndentPPDirectives: None
IndentWidth: 4
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: false
Language: Cpp
MaxEmptyLinesToKeep: 2
NamespaceIndentation: None
PenaltyExcessCharacter: 10
PointerAlignment: Left
ReflowComments: true
SortIncludes: true
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInCStyleCastParentheses: false
SpacesInConditionalStatement: false
SpacesInContainerLiterals: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: c++14
StatementMacros: ['Q_UNUSED']
TabWidth: 4
UseCRLF: false
UseTab: ForIndentation
...

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
.idea
cmake-build-*
storage.json

12
.gitmodules vendored Normal file
View file

@ -0,0 +1,12 @@
[submodule "external/fmt"]
path = external/fmt
url = https://github.com/fmtlib/fmt.git
[submodule "external/nlohmann_json"]
path = external/nlohmann_json
url = https://github.com/nlohmann/json.git
[submodule "external/banana"]
path = external/banana
url = https://github.com/UltraCoderRU/banana.git
[submodule "external/certify"]
path = external/certify
url = https://github.com/djarek/certify.git

11
Asio.cpp Normal file
View file

@ -0,0 +1,11 @@
#include "Asio.h"
#include <boost/certify/https_verification.hpp>
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);
}

13
Asio.h Normal file
View file

@ -0,0 +1,13 @@
#ifndef COURT_MONITOR_ASIO_H
#define COURT_MONITOR_ASIO_H
#include <boost/asio/io_context.hpp>
#include <boost/asio/ssl/context.hpp>
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

26
CMakeLists.txt Normal file
View file

@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.5)
project(court_monitor)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
find_package(OpenSSL REQUIRED)
find_package(Boost COMPONENTS system REQUIRED)
add_subdirectory(external)
add_executable(court_monitor
Asio.cpp
CourtApi.cpp
Storage.cpp
main.cpp
)
target_link_libraries(court_monitor
banana-beast
fmt::fmt
nlohmann_json::nlohmann_json
certify::core
Boost::system
${OPENSSL_LIBRARIES}
)

107
CourtApi.cpp Normal file
View file

@ -0,0 +1,107 @@
#include "CourtApi.h"
#include "Asio.h"
#include <fmt/format.h>
#include <nlohmann/json.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/beast.hpp>
#include <boost/certify/extensions.hpp>
#include <boost/certify/https_verification.hpp>
#include <iostream>
#include <thread>
const char* serverDomain = "mirsud.spb.ru";
using ssl_stream = boost::asio::ssl::stream<boost::beast::tcp_stream>;
ssl_stream connect(const std::string& hostname)
{
ssl_stream stream(asioContext, sslContext);
static boost::asio::ip::tcp::resolver resolver(asioContext);
auto const results = resolver.resolve(hostname, "https");
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;
}
std::pair<int, std::string> get(ssl_stream& stream,
const std::string& hostname,
const std::string& url,
std::optional<std::string> payload = {})
{
// Создать HTTP-запрос
boost::beast::http::request<boost::beast::http::string_body> request;
request.method(boost::beast::http::verb::get);
request.target(url);
request.keep_alive(true);
request.set(boost::beast::http::field::host, hostname);
if (payload)
{
request.set(boost::beast::http::field::content_type, "application/json");
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()};
}
nlohmann::json getResults(ssl_stream& stream, const std::string_view& uuid)
{
int status;
std::string result;
std::tie(status, result) =
get(stream, serverDomain, fmt::format("/cases/api/results/?id={}", uuid));
if (status == 200)
{
return nlohmann::json::parse(result);
}
else
throw std::runtime_error(
fmt::format("failed to retrieve JSON (server returned code {})", status));
}
nlohmann::json getCaseDetails(int courtId, const std::string_view& caseNumber)
{
ssl_stream stream = connect(serverDomain);
int status;
std::string result;
std::tie(status, result) =
get(stream, serverDomain,
fmt::format("/cases/api/detail/?id={}&court_site_id={}", caseNumber, courtId));
if (status == 200)
{
auto uuid = nlohmann::json::parse(result).at("id").get<std::string>();
for (int i = 0; i < 10; i++)
{
auto results = getResults(stream, uuid);
bool finished = results.at("finished").get<bool>();
if (finished)
return results.at("result");
else
std::this_thread::sleep_for(std::chrono::seconds(1));
}
throw std::runtime_error("failed to get results in time");
}
else
throw std::runtime_error(
fmt::format("failed to retrieve JSON (server returned code {})", status));
}

12
CourtApi.h Normal file
View file

@ -0,0 +1,12 @@
#ifndef COURT_MONITOR_COURT_API_H
#define COURT_MONITOR_COURT_API_H
#include <nlohmann/json_fwd.hpp>
#include <string_view>
nlohmann::json findCases(const std::string_view& name);
nlohmann::json getCaseDetails(int courtId, const std::string_view& caseNumber);
#endif // COURT_MONITOR_COURT_API_H

84
Storage.cpp Normal file
View file

@ -0,0 +1,84 @@
#include "Storage.h"
#include <fmt/format.h>
#include <nlohmann/json.hpp>
#include <fstream>
using json = nlohmann::json;
std::uint32_t parseTime(const std::string& str)
{
std::istringstream ss(str);
std::tm tm = {};
ss >> std::get_time(&tm, "%H:%M:%S");
if (ss.fail())
throw std::invalid_argument("invalid time string");
return tm.tm_hour * 3600 + tm.tm_min * 60 + tm.tm_sec;
}
std::string timeToString(std::uint32_t time)
{
auto hours = time / 3600;
auto minutes = (time % 3600) / 60;
auto seconds = (time % 3600) % 60;
return fmt::format("{:0>2}:{:0>2}:{:0>2}", hours, minutes, seconds);
}
void loadStorage(LocalStorage& storage)
{
std::ifstream ifs("storage.json");
if (ifs.is_open())
{
auto data = json::parse(ifs);
storage.token = data.at("token").get<std::string>();
storage.checkTime = parseTime(data.at("check_time").get<std::string>());
for (const auto& subscription : data.at("subscriptions"))
{
Subscription s;
s.userId = subscription.at("user_id");
for (const auto& counter : subscription.at("counters"))
{
Counter c;
c.courtId = counter.at("court").get<int>();
c.caseNumber = counter.at("case").get<std::string>();
c.value = counter.at("value").get<std::size_t>();
s.counters.push_back(std::move(c));
}
storage.subscriptions.push_back(std::move(s));
}
}
else
throw std::runtime_error("failed to load storage");
}
void saveStorage(const LocalStorage& storage)
{
json data;
data["token"] = storage.token;
data["check_time"] = timeToString(storage.checkTime);
data["subscriptions"] = json::array();
for (const auto& subscription : storage.subscriptions)
{
json jsonSubscription;
jsonSubscription["user_id"] = subscription.userId;
jsonSubscription["counters"] = json::array();
for (const auto& counter : subscription.counters)
{
json jsonCounter;
jsonCounter["court"] = counter.courtId;
jsonCounter["case"] = counter.caseNumber;
jsonCounter["value"] = counter.value;
jsonSubscription["counters"].push_back(std::move(jsonCounter));
}
data["subscriptions"].push_back(std::move(jsonSubscription));
}
std::ofstream ofs("storage.json");
if (ofs.is_open())
ofs << std::setw(2) << data;
else
throw std::runtime_error("failed to save storage");
}

31
Storage.h Normal file
View file

@ -0,0 +1,31 @@
#ifndef COURT_MONITOR_STORAGE_H
#define COURT_MONITOR_STORAGE_H
#include <cstddef>
#include <string>
#include <vector>
struct Counter
{
int courtId = 0;
std::string caseNumber;
std::size_t value = 0;
};
struct Subscription
{
int userId = 0;
std::vector<Counter> counters;
};
struct LocalStorage
{
std::string token;
std::vector<Subscription> subscriptions;
std::uint32_t checkTime; // секунды с 00:00
};
void loadStorage(LocalStorage& storage);
void saveStorage(const LocalStorage& storage);
#endif // COURT_MONITOR_STORAGE_H

8
external/CMakeLists.txt vendored Normal file
View file

@ -0,0 +1,8 @@
add_subdirectory(fmt EXCLUDE_FROM_ALL)
add_subdirectory(nlohmann_json EXCLUDE_FROM_ALL)
add_subdirectory(banana EXCLUDE_FROM_ALL)
set(BUILD_TESTING OFF CACHE INTERNAL "")
add_subdirectory(certify EXCLUDE_FROM_ALL)

1
external/banana vendored Submodule

@ -0,0 +1 @@
Subproject commit 075ac78bba0950341c9c477ca38a523730857fee

1
external/certify vendored Submodule

@ -0,0 +1 @@
Subproject commit 97f5eebfd99a5d6e99d07e4820240994e4e59787

1
external/fmt vendored Submodule

@ -0,0 +1 @@
Subproject commit a33701196adfad74917046096bf5a2aa0ab0bb50

1
external/nlohmann_json vendored Submodule

@ -0,0 +1 @@
Subproject commit bc889afb4c5bf1c0d8ee29ef35eaaf4c8bef8a5d

200
main.cpp Normal file
View file

@ -0,0 +1,200 @@
#include "Asio.h"
#include "CourtApi.h"
#include "Storage.h"
#include <banana/agent/beast.hpp>
#include <banana/api.hpp>
#include <fmt/format.h>
#include <nlohmann/json.hpp>
#include <boost/asio/system_timer.hpp>
#include <chrono>
#include <csignal>
#include <ctime>
#include <string>
struct CaseHistoryItem
{
std::string date;
std::string time;
std::string status;
std::string publishDate;
std::string publishTime;
};
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;
}
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)
{
for (auto& subscription : storage.subscriptions)
{
try
{
fmt::print("* Processing subscriptions for user {}\n", subscription.userId);
for (auto& counter : subscription.counters)
{
fmt::print("** Processing case {}\n", counter.caseNumber);
auto details = getCaseDetails(counter.courtId, counter.caseNumber);
fmt::print("{}\n", details.dump());
auto url = details["url"].get<std::string>();
auto history = parseHistory(details);
for (std::size_t i = counter.value; i < history.size(); i++)
notifyUser(bot, subscription.userId, counter.caseNumber, url, history[i]);
counter.value = history.size();
}
}
catch (const std::exception& e)
{
fmt::print(stderr, "{}\n", e.what());
continue;
}
}
}
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());
std::tm tm = *std::localtime(&now);
std::uint32_t secsOfDay = tm.tm_hour * 3600 + tm.tm_min * 60 + tm.tm_sec;
auto dayBegin = std::chrono::system_clock::from_time_t(now - secsOfDay);
auto t = dayBegin + std::chrono::seconds(checkTime);
return (secsOfDay < checkTime) ? t : (t + std::chrono::days(1));
}
auto asioWork = boost::asio::make_work_guard(asioContext);
bool terminate = false;
void handleSignal(int)
{
fmt::print("signal!\n");
terminate = true;
asioWork.reset();
asioContext.stop();
}
int64_t offset = 0;
void getUpdates(banana::agent::beast_callback& bot);
void processUpdates(banana::agent::beast_callback bot,
banana::expected<banana::array_t<banana::api::update_t>> 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);
try
{
// Загрузить данные из локального хранилища
LocalStorage storage;
loadStorage(storage);
fmt::print("Storage loaded\n");
// Инициализировать SSL
initSSL();
// Создать бота
banana::agent::beast_callback bot(storage.token, asioContext, sslContext);
// Создать таймер ежедневной проверки
boost::asio::system_timer checkTimer(asioContext);
std::function<void(const boost::system::error_code&)> onTimer =
[&](const boost::system::error_code& error)
{
if (!error)
{
processAllSubscriptions(storage, bot);
checkTimer.expires_at(getNextCheckTime(storage.checkTime));
checkTimer.async_wait(onTimer);
}
};
checkTimer.expires_at(getNextCheckTime(storage.checkTime));
checkTimer.async_wait(onTimer);
// Запустить асинхронное получение обновлений
getUpdates(bot);
// Запустить цикл обработки событий
asioContext.run();
// Сохранить данные в локальное хранилище
fmt::print("Saving storage\n");
saveStorage(storage);
return 0;
}
catch (const std::exception& e)
{
fmt::print(stderr, "{}\n", e.what());
return 1;
}
}