diff --git a/.gitignore b/.gitignore
index 4c8ad88..b9c4685 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
/vendor
.env
-requests.sqlite3
\ No newline at end of file
+composer.lock
+requests.sqlite3
+requests.sqlite3-shm
+requests.sqlite3-wal
\ No newline at end of file
diff --git a/composer.json b/composer.json
index fb5951b..4f64167 100644
--- a/composer.json
+++ b/composer.json
@@ -5,7 +5,8 @@
"vlucas/phpdotenv": "^5.6",
"react/cache": "^1.2",
"clue/mq-react": "^1.6",
- "smarty/smarty": "^5.4"
+ "smarty/smarty": "^5.4",
+ "react/promise-timer": "^1.11"
},
"autoload": {
"psr-4": {
diff --git a/composer.lock b/composer.lock
index a0ad34e..40be67e 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "2ff629509c131622c46d8c67505d54b2",
+ "content-hash": "8e299b08324c21b9f02c215ffc70c444",
"packages": [
{
"name": "clue/framework-x",
@@ -1160,6 +1160,85 @@
],
"time": "2024-05-24T10:39:05+00:00"
},
+ {
+ "name": "react/promise-timer",
+ "version": "v1.11.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/promise-timer.git",
+ "reference": "4f70306ed66b8b44768941ca7f142092600fafc1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/4f70306ed66b8b44768941ca7f142092600fafc1",
+ "reference": "4f70306ed66b8b44768941ca7f142092600fafc1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3",
+ "react/event-loop": "^1.2",
+ "react/promise": "^3.2 || ^2.7.0 || ^1.2.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "React\\Promise\\Timer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.",
+ "homepage": "https://github.com/reactphp/promise-timer",
+ "keywords": [
+ "async",
+ "event-loop",
+ "promise",
+ "reactphp",
+ "timeout",
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/promise-timer/issues",
+ "source": "https://github.com/reactphp/promise-timer/tree/v1.11.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2024-06-04T14:27:45+00:00"
+ },
{
"name": "react/socket",
"version": "v1.16.0",
diff --git a/smarty/compile/affb24851ed623b62affa076808377b28b01c478_2.file_index.tpl.php b/smarty/compile/affb24851ed623b62affa076808377b28b01c478_2.file_index.tpl.php
new file mode 100644
index 0000000..bda19ff
--- /dev/null
+++ b/smarty/compile/affb24851ed623b62affa076808377b28b01c478_2.file_index.tpl.php
@@ -0,0 +1,326 @@
+getCompiled()->isFresh($_smarty_tpl, array (
+ 'version' => '5.4.2',
+ 'unifunc' => 'content_676160989e3635_07407148',
+ 'has_nocache_code' => false,
+ 'file_dependency' =>
+ array (
+ 'affb24851ed623b62affa076808377b28b01c478' =>
+ array (
+ 0 => 'index.tpl',
+ 1 => 1734434957,
+ 2 => 'file',
+ ),
+ ),
+ 'includes' =>
+ array (
+ ),
+))) {
+function content_676160989e3635_07407148 (\Smarty\Template $_smarty_tpl) {
+$_smarty_current_dir = '/home/l/public_html/xbotcontrol/smarty/template';
+?>
+
+
+
+
+
+
+
+ XBotControl
+
+
+
+ src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
+ integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous">
+
+>
+
+
+ src="https://code.jquery.com/jquery-3.7.1.min.js">
+>
+
+
+ src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.5/dist/bootstrap-table.min.js">
+>
+
+
+ src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.5/dist/locale/bootstrap-table-zh-CN.min.js">
+>
+
+
+
+ src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.5/dist/extensions/filter-control/bootstrap-table-filter-control.js">
+
+>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+>
+ document.getElementById('date-from').addEventListener('change', refreshTable);
+
+ document.getElementById('limit').addEventListener('change', refreshTable);
+
+ function refreshTable() {
+ $('#table').bootstrapTable('refresh');
+ }
+ window.onload = function() {
+ const dateFrom = document.getElementById('date-from');
+ const dateTo = document.getElementById('date-to');
+
+ const today = new Date();
+ const yesterday = new Date(today);
+ yesterday.setDate(today.getDate() - 1);
+
+ const tomorrow = new Date(today);
+ tomorrow.setDate(today.getDate() + 1);
+
+ dateFrom.value = yesterday.toISOString().slice(0, 16);
+ dateTo.value = tomorrow.toISOString().slice(0, 16);
+ };
+ document.getElementById('date-to').addEventListener('change', refreshTable);
+
+
+ function initializeTable(latest_requests) {
+ var url = location.pathname + '/api/report/' + latest_requests;
+ var $table = $('#table');
+
+ if ($table.length) {
+ $table.bootstrapTable('destroy');
+ }
+
+ $.get(url, function(response) {
+ $table.bootstrapTable({
+ url: url,
+ sortable: true,
+ toolbar: '#toolbar',
+ showRefresh: true,
+ iconsPrefix: 'fa',
+ showColumns: true,
+ classes: ['table', 'table-borderless', 'table-hover', 'table-striped'],
+ filterControl: true,
+ searchable: true,
+ pagination: false,
+ sidePagination: "server",
+ serverSort: false,
+ columns: response.columns,
+ queryParams: queryParams,
+ loadingFontSize: '12px'
+ });
+ });
+ }
+
+ function queryParams(params) {
+ const limit = document.getElementById('limit').value;
+ const from = document.getElementById('date-from').value;
+ const to = document.getElementById('date-to').value;
+
+ params.limit = limit;
+ params.from = from;
+ params.to = to;
+
+ return params;
+ }
+
+
+
+
+ function buttons() {
+
+ return {
+ btnAdd: {
+ text: 'Add new list',
+ icon: 'fa-plus',
+ event: function() {
+ // Prompt the user for a new list name
+ const newListName = prompt('Enter new list name:');
+
+ // Only proceed if the user provides a valid list name
+ if (newListName) {
+ // Define the URL where the form needs to be posted
+ const url = '
+/lists/create'; // Replace with actual URL
+
+ // Create a new hidden form element
+ const form = document.createElement('form');
+ form.method = 'POST';
+ form.action = url;
+
+ // Create hidden input to store the list name
+ const input = document.createElement('input');
+ input.type = 'hidden';
+ input.name = 'listName'; // The name expected by the server
+ input.value = newListName;
+
+ // Append the input to the form
+ form.appendChild(input);
+
+ // Append the form to the body to make it part of the DOM
+ document.body.appendChild(form);
+
+ // Submit the form automatically
+ form.submit();
+ } else {
+ // Handle case where user cancels or enters an empty name
+ alert('List creation was cancelled or name was empty.');
+ }
+ },
+ attributes: {
+ title: 'Add a new list to the table'
+ }
+ }
+ }
+
+ return {
+
+ }
+
+
+ }
+
+ function listFormatter(value, row, index) {
+ var editBtn = ' ';
+
+ var showBtn = '';
+
+ return [showBtn, editBtn, value, ].join('')
+
+ return [showBtn, value, ].join('')
+
+
+ }
+
+>
+
+
+
+
+
+
+
+
+
+ XBotControl
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Classes/LoadStat.php b/src/Classes/LoadStat.php
new file mode 100644
index 0000000..e45da3e
--- /dev/null
+++ b/src/Classes/LoadStat.php
@@ -0,0 +1,25 @@
+db->query($query, $params);
+ }
+}
diff --git a/src/Classes/Report.php b/src/Classes/Report.php
new file mode 100644
index 0000000..8bb677a
--- /dev/null
+++ b/src/Classes/Report.php
@@ -0,0 +1,132 @@
+ 'id',
+ 'field' => 'id',
+ 'visible' => false,
+ 'sortable' => true,
+ 'filterControl' => 'input',
+ 'widthUnit' => 'input',
+ 'width' => 'input',
+ ],
+ [
+ 'sortable' => true,
+ 'title' => 'ip',
+ 'field' => 'ip',
+ 'sortable' => true,
+ 'filterControl' => 'input',
+
+ ],
+ [
+ 'sortable' => true,
+ 'title' => 'domain',
+ 'field' => 'domain',
+ 'sortable' => true,
+ 'visible' => false,
+ 'filterControl' => 'input',
+
+ ],
+ [
+ 'sortable' => true,
+ 'title' => 'path',
+ 'field' => 'path',
+ 'sortable' => true,
+ 'filterControl' => 'input',
+
+ ],
+ [
+ 'sortable' => true,
+ 'title' => 'useragent',
+ 'field' => 'useragent',
+ 'sortable' => true,
+ 'filterControl' => 'input',
+
+ ],
+ [
+ 'sortable' => true,
+ 'title' => 'load',
+ 'field' => 'load',
+ 'sortable' => true,
+ 'filterControl' => 'input',
+
+ ],
+ [
+ 'sortable' => true,
+ 'title' => 'datetime',
+ 'field' => 'datetime',
+ 'sortable' => true,
+ 'filterControl' => 'input',
+
+ ],
+ ];
+
+
+ $sql = "SELECT
+ req.rowid AS id,
+ ip.data AS ip,
+ domain.data AS domain,
+ path.data AS path,
+ useragent.data AS useragent,
+ headers.data AS headers ,
+ (SELECT load.load1
+ FROM load
+ WHERE load.rowid >= req.timestamp
+ ORDER BY load.rowid DESC LIMIT 1) AS load,
+ datetime(req.timestamp, 'auto') AS datetime
+
+ FROM
+ request req
+ LEFT JOIN
+ ip ON req.id_ip = ip.rowid
+ LEFT JOIN
+ domain ON req.id_domain = domain.rowid
+ LEFT JOIN
+ path ON req.id_path = path.rowid
+ LEFT JOIN
+ useragent ON req.id_useragent = useragent.rowid
+ LEFT JOIN
+ headers ON req.id_headers = headers.rowid
+ WHERE 1=1 ";
+
+
+ $params = [];
+ $query = $request->getQueryParams();
+ if (isset($query['filter'])) {
+ $filter = json_decode($request->getQueryParams()['filter'], true);
+ } else {
+ $filter = [];
+ }
+
+ foreach ($filter as $field => $value) {
+ $sql .= 'AND ' . $field . ' LIKE ? ';
+ $params[] = '%' . $value . '%';
+ }
+ $sql .= " AND req.timestamp BETWEEN ? AND ? ";
+ $sql .= ' ORDER BY req.rowid DESC ';
+ $sql .= ' LIMIT ? ;';
+ $params[] = strtotime($request->getQueryParams()['from'] ?? 'yesterday');
+ $params[] = strtotime($request->getQueryParams()['to'] ?? 'now');
+ $params[] = (int)$request->getQueryParams()['limit'] ?? 100;
+
+ return \XBotControl\Storage::getInstance()->db->query($sql, $params)->then(function ($result) use ($columnsDefinition) {
+ return [
+ "columns" => $columnsDefinition,
+ "rows" => $result->rows,
+ ];
+ });
+ }
+}
diff --git a/src/Classes/Schedule.php b/src/Classes/Schedule.php
new file mode 100644
index 0000000..e33e65c
--- /dev/null
+++ b/src/Classes/Schedule.php
@@ -0,0 +1,62 @@
+ 60, 'task' => [\XBotControl\Classes\LoadStat::class, 'saveLoad1']],
+
+ ];
+
+ /**
+ * Initializes and runs the task scheduler.
+ *
+ * Loops through the SCHEDULE array, setting up periodic timers
+ * using React's event loop. If multiple tasks share the same interval,
+ * they are distributed by adding evenly spaced offsets to avoid collisions.
+ *
+ * @return void
+ */
+ public static function run(): void
+ {
+ $tasksByInterval = [];
+
+ // Group tasks by interval
+ foreach (self::SCHEDULE as $schedule) {
+ $interval = $schedule['interval'];
+ $tasksByInterval[$interval][] = $schedule['task'];
+ }
+
+ // Schedule tasks for each interval
+ foreach ($tasksByInterval as $interval => $tasks) {
+ $taskCount = count($tasks);
+ foreach ($tasks as $index => $task) {
+ // Distribute tasks evenly within the interval
+ $offset = ($index / $taskCount) * $interval;
+
+ Loop::addTimer($offset, function () use ($interval, $task) {
+ Loop::addPeriodicTimer($interval, fn() => call_user_func($task));
+ });
+ }
+ }
+ }
+}
diff --git a/src/Controllers/APIController.php b/src/Controllers/APIController.php
new file mode 100644
index 0000000..e858b7f
--- /dev/null
+++ b/src/Controllers/APIController.php
@@ -0,0 +1,28 @@
+getAttribute('action')) {
+ case 'report':
+ return call_user_func([\XBotControl\Classes\Report::class, $request->getAttribute('resource')], $request)->then(function ($result) {
+ return \React\Http\Message\Response::json($result);
+ });
+ default:
+ return \React\Http\Message\Response::json(
+ ['empty_response']
+ );
+ }
+ }
+}
diff --git a/src/Controllers/IndexController.php b/src/Controllers/IndexController.php
new file mode 100644
index 0000000..f8bb02d
--- /dev/null
+++ b/src/Controllers/IndexController.php
@@ -0,0 +1,25 @@
+smarty;
+ $smarty->assign([
+
+ ]);
+
+ return \React\Http\Message\Response::html(
+ $smarty->fetch('index.tpl')
+ );
+ }
+}
diff --git a/src/InitTables.php b/src/InitTables.php
index d11165a..83448da 100644
--- a/src/InitTables.php
+++ b/src/InitTables.php
@@ -11,6 +11,7 @@ class InitTables
public static function create():PromiseInterface
{
+
$db = Storage::getInstance()->db;
return $db->exec("CREATE TABLE IF NOT EXISTS ip (data TEXT UNIQUE NOT NULL CHECK (data LIKE '%'), CONSTRAINT valid_ip CHECK (data LIKE '%.%' OR data LIKE '%:%')) STRICT ;")
->then(function () use ($db) {
@@ -28,26 +29,12 @@ class InitTables
})->then(function () use ($db) {
return $db->exec('CREATE TABLE IF NOT EXISTS bot ( name TEXT NOT NULL, keyword TEXT NULL ) STRICT ;');
})->then(function () use ($db) {
- return $db->exec('PRAGMA journal_mode=WAL;');
- });
-
- return $db->exec("CREATE TABLE IF NOT EXISTS ip (id_ip INTEGER PRIMARY KEY AUTOINCREMENT, ip TEXT UNIQUE NOT NULL CHECK (ip LIKE '%'), CONSTRAINT valid_ip CHECK (ip LIKE '%.%' OR ip LIKE '%:%')) STRICT ;")
- ->then(function () use ($db) {
- return $db->exec('CREATE TABLE IF NOT EXISTS domain (id_domain INTEGER PRIMARY KEY AUTOINCREMENT, domain TEXT UNIQUE NOT NULL) STRICT ;');
+ return $db->exec('CREATE TABLE IF NOT EXISTS settings ( key TEXT UNIQUE NOT NULL, value TEXT NULL ) STRICT ;');
})->then(function () use ($db) {
- return $db->exec('CREATE TABLE IF NOT EXISTS path ( id_path INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT UNIQUE NOT NULL) STRICT ;');
- })->then(function () use ($db) {
- return $db->exec('CREATE TABLE IF NOT EXISTS useragent ( id_useragent INTEGER PRIMARY KEY AUTOINCREMENT, useragent TEXT UNIQUE NOT NULL) STRICT ;');
- })->then(function () use ($db) {
- return $db->exec('CREATE TABLE IF NOT EXISTS headers ( id_headers INTEGER PRIMARY KEY AUTOINCREMENT, headers TEXT UNIQUE NOT NULL) STRICT ;');
- })->then(function () use ($db) {
- return $db->exec("CREATE TABLE IF NOT EXISTS networkwhitelist ( id_networkwhitelist INTEGER PRIMARY KEY AUTOINCREMENT, network TEXT UNIQUE NOT NULL CHECK (network LIKE '%/%'), CONSTRAINT valid_network CHECK ( network LIKE '%.%/%' OR network LIKE '%:%/%' )) STRICT WITHOUT ROWID ;");
- })->then(function () use ($db) {
- return $db->exec('CREATE TABLE IF NOT EXISTS request ( id_request INTEGER PRIMARY KEY AUTOINCREMENT, id_ip INTEGER NOT NULL, id_method INTEGER NOT NULL, id_domain INTEGER NOT NULL, id_path INTEGER NOT NULL, id_useragent INTEGER NOT NULL, id_headers INTEGER NOT NULL, timestamp INTEGER NOT NULL, FOREIGN KEY (id_ip) REFERENCES ip(id_ip), FOREIGN KEY (id_domain) REFERENCES domain(id_domain), FOREIGN KEY (id_path) REFERENCES path(id_path), FOREIGN KEY (id_useragent) REFERENCES useragent(id_useragent), FOREIGN KEY (id_headers) REFERENCES headers(id_headers) ) STRICT WITHOUT ROWID ;');
- })->then(function () use ($db) {
- return $db->exec('CREATE TABLE IF NOT EXISTS bot ( id_bot INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, keyword TEXT NULL ) STRICT ;');
+ return $db->exec('CREATE TABLE IF NOT EXISTS load (load1 REAL NOT NULL) STRICT ;');
})->then(function () use ($db) {
return $db->exec('PRAGMA journal_mode=WAL;');
});
+
}
}
diff --git a/src/Instances/Whitelist.php b/src/Instances/Whitelist.php
new file mode 100644
index 0000000..bb7931d
--- /dev/null
+++ b/src/Instances/Whitelist.php
@@ -0,0 +1,33 @@
+getHeaderLine('User-Agent') ?: 'Unknown';
- $headers = json_encode($request->getHeaders(), JSON_UNESCAPED_UNICODE);
- $uri = $request->getUri();
- $storage = Storage::getInstance();
+ $realIp = self::getRealIP($request);
+ $userAgent = $request->getHeaderLine('User-Agent') ?: 'Unknown';
+ $headers = json_encode($request->getHeaders(), JSON_UNESCAPED_UNICODE);
+ $uri = $request->getUri();
+ $storage = Storage::getInstance();
- // Use parallel promises for ID generation to avoid waiting for each in sequence
- $idPromises = [
- 'id_ip' => $storage::getId('ip', $realIp),
- 'id_domain' => $storage::getId('domain', $uri->getHost()),
- 'id_path' => $storage::getId('path', $uri->getPath()),
- 'id_useragent' => $storage::getId('useragent', $userAgent),
- 'id_headers' => $storage::getId('headers', md5($headers)),
- ];
+ // Use parallel promises for ID generation to avoid waiting for each in sequence
+ $idPromises = [
+ 'id_ip' => $storage::getId('ip', $realIp),
+ 'id_domain' => $storage::getId('domain', $uri->getHost()),
+ 'id_path' => $storage::getId('path', '/' . $request->getAttribute('original_uri', '')),
+ 'id_useragent' => $storage::getId('useragent', $userAgent),
+ 'id_headers' => 0,
+ ];
- return \React\Promise\all($idPromises)
- ->then(function ($resolvedValues) use ($request, $storage) {
- // Set resolved values efficiently
- $resolvedValues['id_method'] = self::METHOD[$request->getMethod()] ?? 0;
- $resolvedValues['timestamp'] = time();
+ if ($_ENV['SAVE_HEADERS'] === true) {
+ $idPromises['id_headers'] = $storage::getId('headers', $headers);
+ }
- // Directly save data asynchronously
- return $storage::insert('request', $resolvedValues);
- })
- ->then(function () {
- return \React\Http\Message\Response::plaintext('');
- });
+
+
+ return \React\Promise\all($idPromises)
+ ->then(function ($resolvedValues) use ($request, $storage) {
+ // Set resolved values efficiently
+ $resolvedValues['id_method'] = self::METHOD[$request->getMethod()] ?? 0;
+ $resolvedValues['timestamp'] = time();
+
+ // Directly save data asynchronously
+ return $storage::insert('request', $resolvedValues);
+ })
+ ->then(function () {
+ return \React\Http\Message\Response::plaintext('');
+ });
+
}
public static function getRealIP(ServerRequestInterface $request): string
diff --git a/src/Storage.php b/src/Storage.php
index bbbbdb6..371b5ba 100644
--- a/src/Storage.php
+++ b/src/Storage.php
@@ -10,8 +10,10 @@ use Clue\React\SQLite\Result;
class Storage
{
- protected static $instance;
+ private static ?Storage $instance = null;
+
/** @var DatabaseInterface $db */
+
public $db;
public $cache = [];
@@ -23,7 +25,7 @@ class Storage
'path'
];
- public function __construct()
+ private function __construct()
{
$this->db = (new \Clue\React\SQLite\Factory())->openLazy($_ENV['APP_DIR'] . '/requests.sqlite3');
diff --git a/xbotcontrol.php b/xbotcontrol.php
index 3b3d75d..3021755 100644
--- a/xbotcontrol.php
+++ b/xbotcontrol.php
@@ -7,35 +7,33 @@ require __DIR__ . '/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$_ENV['APP_DIR'] = __DIR__;
-$_ENV['X_LISTEN'] = '0.0.0.0:7500';
+
XBotControl\InitTables::create();
$app = new FrameworkX\App();
-$app->get(
- '/mirror',
+$app->any(
+ '/mirror[/{original_uri:.*}]',
function (Psr\Http\Message\ServerRequestInterface $request) {
return XBotControl\Request::save($request);
}
);
-$app->get('/mirror1', function () {
- return React\Http\Message\Response::plaintext(
- "Hello wörld!\n"
- );
-});
+$app->any('/', XBotControl\Controllers\IndexController::class);
+
+$app->any('/api/{action}/{resource}', XBotControl\Controllers\APIController::class);
+
+
+XBotControl\Classes\Schedule::run();
-$app->get('/cp', function (Psr\Http\Message\ServerRequestInterface $request) {
- return React\Http\Message\Response::plaintext(
- "Hello " . $request->getAttribute('name') . "!\n"
- );
-});
$app->run();
-XBotControl\Storage::getInstance()->db->query('PRAGMA main.wal_checkpoint;')
+XBotControl\Storage::getInstance()->db->query('VACUUM;')
->then(function () {
+ XBotControl\Storage::getInstance()->db->query('PRAGMA main.wal_checkpoint;');
+ })->then(function () {
XBotControl\Storage::getInstance()->db->quit();
});