generated from mirzaev/pot
This commit is contained in:
parent
8627411c8e
commit
28d550d53c
|
@ -6,7 +6,10 @@ namespace mirzaev\notchat\controllers;
|
|||
|
||||
// Files of the project
|
||||
use mirzaev\notchat\views\templater,
|
||||
mirzaev\notchat\models\core as models;
|
||||
mirzaev\notchat\models\core as models,
|
||||
mirzaev\notchat\models\log,
|
||||
mirzaev\notchat\models\enumerations\log as type,
|
||||
mirzaev\notchat\models\firewall;
|
||||
|
||||
// Framework for PHP
|
||||
use mirzaev\minimal\controller;
|
||||
|
@ -30,7 +33,7 @@ class core extends controller
|
|||
protected array $errors = [];
|
||||
|
||||
/**
|
||||
* Constructor of an instance
|
||||
* Constructor
|
||||
*
|
||||
* @param bool $initialize Initialize a controller?
|
||||
*
|
||||
|
@ -44,12 +47,49 @@ class core extends controller
|
|||
if ($initialize) {
|
||||
// Initializing is requested
|
||||
|
||||
// Initializing of models core
|
||||
new models();
|
||||
// Write to the log of connections
|
||||
log::write(
|
||||
type::CONNECTIONS,
|
||||
trim("[{$_SERVER['REMOTE_ADDR']}] "
|
||||
. (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ")
|
||||
. (empty($_SERVER['HTTP_REFERER']) ? '' : "[{$_SERVER['HTTP_REFERER']}] ")
|
||||
. (empty($_SERVER['HTTP_USER_AGENT']) ? '' : "[{$_SERVER['HTTP_USER_AGENT']}]"), ' ')
|
||||
);
|
||||
|
||||
// Initializing of preprocessor of views
|
||||
$this->view = new templater();
|
||||
|
||||
// Checking for ban
|
||||
if (
|
||||
(isset($_SERVER['HTTP_X_FORWARDED_FOR']) && firewall::banned($_SERVER['HTTP_X_FORWARDED_FOR']))
|
||||
|| (isset($_SERVER['REMOTE_ADDR']) && firewall::banned($_SERVER['REMOTE_ADDR']))
|
||||
) {
|
||||
// IP-address is banned
|
||||
|
||||
// Sending a reply
|
||||
echo $this->view->render('pages/ban.html');
|
||||
|
||||
// Exit (success)
|
||||
die;
|
||||
}
|
||||
|
||||
// Initializing of models core
|
||||
new models();
|
||||
|
||||
// Initializing a response headers
|
||||
header('Service-Worker-Allowed: /');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
// Analyze recent requests
|
||||
firewall::analyze();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -68,5 +108,4 @@ class core extends controller
|
|||
default => isset($this->{$name})
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,7 +6,9 @@ namespace mirzaev\notchat\controllers;
|
|||
|
||||
// Files of the project
|
||||
use mirzaev\notchat\controllers\core,
|
||||
mirzaev\notchat\models\server;
|
||||
mirzaev\notchat\models\dns,
|
||||
mirzaev\notchat\models\server,
|
||||
mirzaev\notchat\models\text;
|
||||
|
||||
/**
|
||||
* Index controller
|
||||
|
@ -23,6 +25,62 @@ final class index extends core
|
|||
*/
|
||||
public function index(array $parameters = []): ?string
|
||||
{
|
||||
// Initializing a list with languages
|
||||
$this->view->languages = text::list($this->errors);
|
||||
|
||||
// Инициализация бегущей строки
|
||||
$this->view->hotline = [
|
||||
'id' => 'hotline'
|
||||
];
|
||||
|
||||
// Инициализация параметров бегущей строки
|
||||
$this->view->hotline = [
|
||||
'parameters' => [
|
||||
'step' => '0.3'
|
||||
]
|
||||
] + $this->view->hotline;
|
||||
|
||||
// Инициализация аттрибутов бегущей строки
|
||||
$this->view->hotline = [
|
||||
'attributes' => []
|
||||
] + $this->view->hotline;
|
||||
|
||||
// Инициализация элементов бегущей строки
|
||||
$this->view->hotline = [
|
||||
'elements' => [
|
||||
['html' => ''],
|
||||
[
|
||||
'tag' => 'article',
|
||||
'attributes' => [
|
||||
'class' => 'trash'
|
||||
],
|
||||
'html' => $this->view->render(DIRECTORY_SEPARATOR . 'hotline' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . 'trash.html', [
|
||||
'id' => 'trash_1',
|
||||
'title' => 'Linoleum',
|
||||
'main' => '<p>Do you really like the rotting smell, dull sound and disgusting greasy shine of parquet-like fake pattern on a polymer toxic film? <b>Are you fucking insane?</b></p>',
|
||||
'image' => [
|
||||
'src' => 'https://virus.mirzaev.sexy/images/trash/linoleum.png',
|
||||
'alt' => 'Linoleum'
|
||||
]
|
||||
])
|
||||
],
|
||||
['html' => ''],
|
||||
['html' => ''],
|
||||
['html' => ''],
|
||||
['html' => ''],
|
||||
['html' => ''],
|
||||
['html' => ''],
|
||||
['html' => ''],
|
||||
['html' => ''],
|
||||
['html' => ''],
|
||||
['html' => ''],
|
||||
['html' => ''],
|
||||
['html' => ''],
|
||||
['html' => '']
|
||||
]
|
||||
] + $this->view->hotline;
|
||||
|
||||
|
||||
// Exit (success)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render('chats.html');
|
||||
else if ($_SERVER['REQUEST_METHOD'] === 'POST') return $this->view->render('chats.html');
|
||||
|
@ -31,6 +89,106 @@ final class index extends core
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the servers section
|
||||
*
|
||||
* @param array $parameters Parameters of the request (POST + GET)
|
||||
*
|
||||
* @return void Generated JSON to the output buffer
|
||||
*/
|
||||
/* public function cache(array $parameters = []): void
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
// GET
|
||||
|
||||
if (file_exists($path = INDEX . DIRECTORY_SEPARATOR . 'manifest')) {
|
||||
// File found
|
||||
|
||||
// Clearing the output buffer
|
||||
if (ob_get_level()) ob_end_clean();
|
||||
|
||||
// Initializing of the output buffer
|
||||
ob_start();
|
||||
|
||||
// Initializing a response headers
|
||||
header('Content-Type: text/cache-manifest');
|
||||
|
||||
// Generating the reponse
|
||||
if ($file = fopen($path, 'r')) {
|
||||
// File open
|
||||
|
||||
// Reading file
|
||||
while (!feof($file)) echo fread($path, 1024);
|
||||
|
||||
// Closing file
|
||||
fclose($file);
|
||||
}
|
||||
|
||||
// Initializing a response headers
|
||||
header('Content-Length: ' . ob_get_length());
|
||||
|
||||
// Sending and deinitializing of the output buffer
|
||||
ob_end_flush();
|
||||
flush();
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @return void Generated JSON to the output buffer
|
||||
*/
|
||||
/* public function cache(array $parameters = []): void
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
// GET
|
||||
|
||||
if (file_exists($path = INDEX . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . 'cache.js')) {
|
||||
// File found
|
||||
|
||||
// Clearing the output buffer
|
||||
if (ob_get_level()) ob_end_clean();
|
||||
|
||||
// Initializing of the output buffer
|
||||
ob_start();
|
||||
|
||||
// Initializing a response headers
|
||||
header('Content-Type: application/javascript charset=utf-8');
|
||||
|
||||
// Generating the reponse
|
||||
if ($file = fopen($path, 'r')) {
|
||||
// File open
|
||||
|
||||
// Reading file
|
||||
while (!feof($file)) echo fread($file, 1024);
|
||||
|
||||
// Closing file
|
||||
fclose($file);
|
||||
}
|
||||
|
||||
// Initializing a response headers
|
||||
header('Content-Length: ' . ob_get_length());
|
||||
|
||||
// Sending and deinitializing of the output buffer
|
||||
ob_end_flush();
|
||||
flush();
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
/**
|
||||
* Render the offline page
|
||||
*/
|
||||
public function offline(): ?string
|
||||
{
|
||||
// Initializing of the title
|
||||
$this->view->title = 'bye';
|
||||
|
||||
// Exit (success)
|
||||
return $this->view->render('pages/offline.html');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the servers section
|
||||
*
|
||||
|
@ -54,7 +212,17 @@ final class index extends core
|
|||
// Generating the reponse
|
||||
echo json_encode(
|
||||
[
|
||||
'html' => $this->view->render('sections/servers.html', ['current' => isset($parameters['server']) ? server::read($parameters['server'], errors: $this->errors) : null, 'servers' => server::all(100, errors: $this->errors) ?? []]),
|
||||
'html' => $this->view->render(
|
||||
'sections/servers.html',
|
||||
[
|
||||
'current' => isset($parameters['server'])
|
||||
&& ($server = server::read(domain: dns::domain($parameters['server'], errors: $this->errors), errors: $this->errors))
|
||||
? json_decode($server, true, 8)
|
||||
: null,
|
||||
'servers' => server::all(100, errors: $this->errors) ?? []
|
||||
]
|
||||
),
|
||||
'status' => isset($server) ? 'connected' : 'disconnected',
|
||||
'errors' => null
|
||||
]
|
||||
);
|
||||
|
|
|
@ -7,7 +7,13 @@ namespace mirzaev\notchat\controllers;
|
|||
// Files of the project
|
||||
use mirzaev\notchat\controllers\core,
|
||||
mirzaev\notchat\controllers\traits\errors,
|
||||
mirzaev\notchat\models\server as model;
|
||||
mirzaev\notchat\models\dns,
|
||||
mirzaev\notchat\models\server as model,
|
||||
mirzaev\notchat\models\log,
|
||||
mirzaev\notchat\models\enumerations\log as type;
|
||||
|
||||
// Built-in libraries
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Server controller
|
||||
|
@ -34,7 +40,7 @@ final class server extends core
|
|||
// POST
|
||||
|
||||
// Create a file with server data
|
||||
model::write($parameters['domain'], file_get_contents('php://input'), $this->errors);
|
||||
model::write(dns::domain($parameters['server'], errors: $this->errors), file_get_contents('php://input'), $this->errors);
|
||||
|
||||
// Initializing a response headers
|
||||
header('Content-Type: application/json');
|
||||
|
@ -74,11 +80,39 @@ final class server extends core
|
|||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// POST
|
||||
|
||||
// Read a file with server data
|
||||
$server = json_decode(model::read(model::domain($parameters['server']), $this->errors), true, 8);
|
||||
// Initializing of buffer of response
|
||||
$return = [];
|
||||
|
||||
try {
|
||||
// Decode of user input
|
||||
$parameters['server'] = urldecode($parameters['server']);
|
||||
|
||||
// Validation of user input
|
||||
if (mb_strlen($parameters['server']) > 512) throw new exception('Server address longer than 512 characters');
|
||||
|
||||
if ($domain = dns::domain($parameters['server'], errors: $this->errors)) {
|
||||
if ($raw = model::read(domain: $domain)) {
|
||||
// File found and read
|
||||
|
||||
// Decoding server data to remove protected parameters
|
||||
$return['server'] = json_decode($raw, true, 8);
|
||||
|
||||
// Remove protected parameters
|
||||
unset($server['key']);
|
||||
unset($return['server']['key']);
|
||||
} else throw new exception('Server offline');
|
||||
} else throw new exception('Server not found');
|
||||
} catch (exception $e) {
|
||||
// Write to the buffer of errors
|
||||
$this->errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
|
||||
// Write to the log of errors
|
||||
log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage());
|
||||
}
|
||||
|
||||
// Initializing a response headers
|
||||
header('Content-Type: application/json');
|
||||
|
@ -90,8 +124,7 @@ final class server extends core
|
|||
|
||||
// Generating the reponse
|
||||
echo json_encode(
|
||||
[
|
||||
'server' => $server,
|
||||
$return + [
|
||||
'errors' => static::text($this->errors)
|
||||
]
|
||||
);
|
||||
|
|
|
@ -4,6 +4,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace mirzaev\notchat\models;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\notchat\models\enumerations\log as type;
|
||||
|
||||
// Framework for PHP
|
||||
use mirzaev\minimal\model;
|
||||
|
||||
|
@ -24,7 +27,12 @@ class core extends model
|
|||
final public const POSTFIX = '';
|
||||
|
||||
/**
|
||||
* Path to storage
|
||||
* Path to public directory
|
||||
*/
|
||||
final public const PUBLIC = '..' . DIRECTORY_SEPARATOR . 'public';
|
||||
|
||||
/**
|
||||
* Path to storage directory
|
||||
*/
|
||||
final public const STORAGE = '..' . DIRECTORY_SEPARATOR . 'storage';
|
||||
|
||||
|
|
|
@ -4,15 +4,14 @@ declare(strict_types=1);
|
|||
|
||||
namespace mirzaev\notchat\models;
|
||||
|
||||
// Framework for PHP
|
||||
use mirzaev\minimal\model;
|
||||
// Files of the project
|
||||
use mirzaev\notchat\models\enumerations\log as type;
|
||||
|
||||
// Built-in libraries
|
||||
use exception,
|
||||
DirectoryIterator as parser;
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Core of DNS registry
|
||||
* DNS registry
|
||||
*
|
||||
* @package mirzaev\notchat\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
|
@ -41,9 +40,9 @@ class dns extends core
|
|||
{
|
||||
try {
|
||||
// Open file with DNS records
|
||||
$dns = fopen(static::DNS, 'r');
|
||||
$dns = fopen(static::DNS, 'c+');
|
||||
|
||||
while (($row = fgets($dns)) !== false) {
|
||||
while (($row = fgets($dns, 512)) !== false) {
|
||||
// Iterate over rows
|
||||
|
||||
// Initializing values of the server data
|
||||
|
@ -52,27 +51,30 @@ class dns extends core
|
|||
// Incrementing the line read counter
|
||||
++$line;
|
||||
|
||||
if ($domain === $_domain || $ip === $_ip || $port === $_port) {
|
||||
// Server found
|
||||
if ($domain === $_domain || ($port && $ip === $_ip && $port === $_port) || (!$port && $ip === $_ip || $port === $_port)) {
|
||||
// Server found (domain, ip, ip + port)
|
||||
|
||||
// Close file with DNS
|
||||
fclose($dns);
|
||||
|
||||
// Exit (success)
|
||||
return $record;
|
||||
return array_combine(['domain', 'ip', 'port'], $record);
|
||||
}
|
||||
}
|
||||
|
||||
// Close file with DNS
|
||||
fclose($dns);
|
||||
} catch (exception $e) {
|
||||
// Write to buffer of errors
|
||||
// Write to the buffer of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
|
||||
// Write to the log of errors
|
||||
log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage());
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
|
@ -100,23 +102,23 @@ class dns extends core
|
|||
// Initializing part the file buffer (rows before target)
|
||||
$after = [];
|
||||
|
||||
if (file_exists(static::DNS) && filesize(static::DNS) > 0) {
|
||||
// File exists and not empty
|
||||
|
||||
// Initializing the status that the DNS record has been found
|
||||
$found = false;
|
||||
|
||||
// Open file with DNS records
|
||||
$dns = fopen(static::DNS, 'r');
|
||||
if (file_exists(static::DNS) && filesize(static::DNS) > 0) {
|
||||
// File exists and not empty
|
||||
|
||||
while (($row = fgets($dns)) !== false) {
|
||||
// Open file with DNS records
|
||||
$dns = fopen(static::DNS, 'c+');
|
||||
|
||||
while (($row = fgets($dns, 512)) !== false) {
|
||||
// Iterate over rows
|
||||
|
||||
// Initializing values of the server data
|
||||
[$_domain] = explode(' ', $row);
|
||||
|
||||
// Writing the row to the file buffer (except the target record)
|
||||
if ($domain === $_domain) $found = true;
|
||||
if ($domain === $_domain) $found = $row;
|
||||
else ${$found ? 'after' : 'before'}[] = $row;
|
||||
}
|
||||
|
||||
|
@ -134,7 +136,7 @@ class dns extends core
|
|||
ftruncate($dns, 0);
|
||||
|
||||
// Write a new record to the DNS registry
|
||||
fwrite($dns, trim(implode("", $before)) . "\n$domain $ip $port\n" . trim(implode("", $after)));
|
||||
fwrite($dns, (count($before) > 0 ? trim(implode("", $before)) . "\n" : '') . "$domain $ip $port" . (count($after) ? "\n" . trim(implode("", $after)) : ''));
|
||||
|
||||
// Apply changes
|
||||
fflush($dns);
|
||||
|
@ -142,14 +144,20 @@ class dns extends core
|
|||
// Unlock file
|
||||
flock($dns, LOCK_UN);
|
||||
}
|
||||
|
||||
// Write to the log
|
||||
log::write(type::DNS, $found ? "[UPDATE] $found -> $domain $ip $port" : "[CREATE] $domain $ip $port");
|
||||
} catch (exception $e) {
|
||||
// Write to buffer of errors
|
||||
// Write to the buffer of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
|
||||
// Write to the log of errors
|
||||
log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -159,32 +167,39 @@ class dns extends core
|
|||
* Convert domain or IP-address to domain
|
||||
*
|
||||
* @param string $server Domain or IP-address of the server
|
||||
* @param bool $strict Check for port compliance?
|
||||
* @param array &$errors Buffer of errors
|
||||
*
|
||||
* @return string|null Domain of the server
|
||||
*/
|
||||
public static function domain(string $server, &$errors = []): ?string
|
||||
public static function domain(string $server, bool $strict = true, &$errors = []): ?string
|
||||
{
|
||||
try {
|
||||
if (preg_match('/^(https:\/\/)?\d+\..*\d\/?$/', $server) === 1) {
|
||||
if (preg_match('/^(?:https:\/\/)?([\d\.]*)(?:$|:?(\d.*\d)?\/?$)/', $server, $matches) === 1) {
|
||||
// IP-address
|
||||
|
||||
// Initializing of parts of address
|
||||
@[, $ip, $port] = $matches;
|
||||
|
||||
// Exit (success)
|
||||
return static::read(ip: $server, errors: $errors)['domain'];
|
||||
return static::read(ip: $ip, port: $strict ? $port : null, errors: $errors)['domain'] ?? null;
|
||||
} else {
|
||||
// Domain (implied)
|
||||
|
||||
// Exit (success)
|
||||
return $server;
|
||||
return $server ?? null;
|
||||
}
|
||||
} catch (exception $e) {
|
||||
// Write to buffer of errors
|
||||
// Write to the buffer of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
|
||||
// Write to the log of errors
|
||||
log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage());
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
|
@ -204,25 +219,31 @@ class dns extends core
|
|||
public static function ip(string $server, &$errors = []): ?string
|
||||
{
|
||||
try {
|
||||
if (preg_match('/^(https:\/\/)?\d+\..*\d\/?$/', $server) === 1) {
|
||||
if (preg_match('/^(?:https:\/\/)?(\d+\..*):?(\d.*\d)?\/?$/', $server, $matches) === 1) {
|
||||
// IP-address
|
||||
|
||||
// Initializing of parts of address
|
||||
[, $ip, $port] = $matches;
|
||||
|
||||
// Exit (success)
|
||||
return $server;
|
||||
return $ip ?? null;
|
||||
} else {
|
||||
// Domain (implied)
|
||||
|
||||
// Exit (success)
|
||||
return static::read(domain: $server, errors: $errors)['ip'];
|
||||
return static::read(domain: $server, errors: $errors)['ip'] ?? null;
|
||||
}
|
||||
} catch (exception $e) {
|
||||
// Write to buffer of errors
|
||||
// Write to the buffer of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
|
||||
// Write to the log of errors
|
||||
log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage());
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\notchat\models\enumerations;
|
||||
|
||||
/**
|
||||
* Types of logs
|
||||
*
|
||||
* @package mirzaev\notchat\models\enumerations
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
enum log: string
|
||||
{
|
||||
case ERRORS = 'errors.log';
|
||||
case CONNECTIONS = 'connections.log';
|
||||
case BANS = 'bans.log';
|
||||
case FIREWALL = 'firewall.log';
|
||||
case SESSIONS = 'sessions.log';
|
||||
case SERVERS = 'servers.log';
|
||||
case DNS = 'dns.log';
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\notchat\models;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\notchat\models\enumerations\log as type,
|
||||
mirzaev\notchat\models\traits\file,
|
||||
mirzaev\notchat\models\traits\log as read;
|
||||
|
||||
// Built-in libraries
|
||||
use exception,
|
||||
datetime;
|
||||
|
||||
/**
|
||||
* Firewall
|
||||
*
|
||||
* @package mirzaev\notchat\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
class firewall extends core
|
||||
{
|
||||
use file, read {
|
||||
file::read as protected file;
|
||||
read::ban as protected _ban;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write
|
||||
*
|
||||
* @param int $range Time range for reading last connections (seconds)
|
||||
* @param int $limit Limit on the number of requests in the allotted time
|
||||
* @param array &$errors Buffer of errors
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @todo
|
||||
* 1. Dividing logs based on volume achieved
|
||||
*/
|
||||
public static function analyze(int $range = 10, int $limit = 20, &$errors = []): void
|
||||
{
|
||||
try {
|
||||
// Initializing of path to the file with connections
|
||||
$path = log::LOGS . DIRECTORY_SEPARATOR . type::CONNECTIONS->value;
|
||||
|
||||
if (file_exists($path)) {
|
||||
// The file exists
|
||||
|
||||
// Initializing of current time
|
||||
$current = new datetime();
|
||||
|
||||
// Initializing of target past time
|
||||
$past = (clone $current)->modify("-$range seconds");
|
||||
|
||||
// Initializing of the buffer of IP-addresses found in the connections log [ip => amount of connections per $time]
|
||||
$ips = [];
|
||||
|
||||
// Open file with connections
|
||||
$connections = fopen($path, 'r');
|
||||
|
||||
foreach (static::file($connections, 300, 0, -1) as $row) {
|
||||
// Reading a file backwards (rows from end)
|
||||
|
||||
// Skipping of empty rows
|
||||
if (empty($row)) continue;
|
||||
|
||||
try {
|
||||
// Deserializing a row
|
||||
$parameters = static::connection($row, $errors);
|
||||
|
||||
if ($parameters !== null && is_array($parameters)) {
|
||||
// Parameters have been initialized
|
||||
|
||||
// Initializing of parameters of connection
|
||||
[, $date, $ip, $forwarded, $referer, $useragent] = $parameters;
|
||||
|
||||
// Initializing of date of connection
|
||||
$date = DateTime::createFromFormat('Y.m.d H:i:s', $date);
|
||||
|
||||
if (0 <= $elapsed = $date->getTimestamp() - $past->getTimestamp()) {
|
||||
// No more than $range seconds have passed since connection
|
||||
|
||||
// Initializing of counter of connections
|
||||
$ips[$ip] ??= 0;
|
||||
|
||||
// Incrementing of counter of connections
|
||||
++$ips[$ip];
|
||||
}
|
||||
}
|
||||
} catch (exception $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Close file with connections
|
||||
fclose($connections);
|
||||
}
|
||||
|
||||
// Ban IP-addresses that do not meet the conditions
|
||||
foreach ($ips ?? [] as $ip => $connections) if ($connections >= $limit) static::ban($ip, new datetime('+2 minutes'));
|
||||
} catch (exception $e) {
|
||||
// Write to buffer of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
|
||||
// Write to the log of errors
|
||||
log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ban
|
||||
*
|
||||
* @param string $ip IP-address
|
||||
* @param datetime $end Date for unban
|
||||
* @param array &$errors Buffer of errors
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function ban(string $ip, datetime $end, &$errors = []): void
|
||||
{
|
||||
try {
|
||||
// Write to the log of bans
|
||||
log::write(type::BANS, "[{$end->format('Y.m.d H:i:s')}] [$ip]");
|
||||
} catch (exception $e) {
|
||||
// Write to buffer of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
|
||||
// Write to the log of errors
|
||||
log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for ban
|
||||
*
|
||||
* Search in the ban log
|
||||
*
|
||||
* @param string $ip IP-address
|
||||
* @param array &$errors Buffer of errors
|
||||
*
|
||||
* @return bool IP-address is banned? (null - has errors)
|
||||
*
|
||||
* @todo Count rows of file for reading instead of constant value (500 rows)
|
||||
*/
|
||||
public static function banned(string $ip, &$errors = []): ?bool
|
||||
{
|
||||
try {
|
||||
// Initializing of path to the file with bans
|
||||
$path = log::LOGS . DIRECTORY_SEPARATOR . type::BANS->value;
|
||||
|
||||
if (file_exists($path)) {
|
||||
// The file exists
|
||||
|
||||
// Open file with bans
|
||||
$bans = fopen($path, 'r');
|
||||
|
||||
foreach (static::file($bans, 500, 0, -1) as $row) {
|
||||
// Reading a file backwards (rows from end)
|
||||
|
||||
// Skipping of empty rows
|
||||
if (empty($row)) continue;
|
||||
|
||||
try {
|
||||
// Deserializing a row
|
||||
$parameters = static::_ban($row, $errors);
|
||||
|
||||
if ($parameters !== null && is_array($parameters)) {
|
||||
// Parameters have been initialized
|
||||
|
||||
// Initializing of parameters of connection
|
||||
[, $from, $to, $_ip] = static::_ban($row, $errors);
|
||||
|
||||
// Check for ban and exit (success)
|
||||
if ($ip === $_ip && (new datetime)->getTimestamp() - DateTime::createFromFormat('Y.m.d H:i:s', $to)->getTimestamp() < 0) return true;
|
||||
}
|
||||
} catch (exception $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exit (success)
|
||||
return false;
|
||||
} catch (exception $e) {
|
||||
// Write to buffer of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
|
||||
// Write to the log of errors
|
||||
log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage());
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\notchat\models;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\notchat\models\enumerations\log as type;
|
||||
|
||||
// Built-in libraries
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Log
|
||||
*
|
||||
* @package mirzaev\notchat\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
class log extends core
|
||||
{
|
||||
/**
|
||||
* Path to DNS of storaged servers
|
||||
*/
|
||||
final public const LOGS = core::STORAGE . DIRECTORY_SEPARATOR . 'logs';
|
||||
|
||||
/**
|
||||
* Write
|
||||
*
|
||||
* @param type $type Type of log
|
||||
* @param string $value Text to write
|
||||
* @param array &$errors Buffer of errors
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @todo
|
||||
* 1. Dividing magazines based on volume achieved
|
||||
*/
|
||||
public static function write(type $type, string $value, &$errors = []): void
|
||||
{
|
||||
try {
|
||||
// Initializing of path to the file of the log
|
||||
$path = static::LOGS . DIRECTORY_SEPARATOR . $type->value;
|
||||
|
||||
// Open file of the log
|
||||
$log = fopen($path, 'a');
|
||||
|
||||
if (flock($log, LOCK_EX)) {
|
||||
// File locked
|
||||
|
||||
// Initializing of date
|
||||
$date = date_format(date_create(), 'Y.m.d H:i:s');
|
||||
|
||||
// Write to the log
|
||||
fwrite($log, (filesize($path) === 0 ? '' : PHP_EOL) . "[$date] $value");
|
||||
|
||||
// Apply changes
|
||||
fflush($log);
|
||||
|
||||
// Unlock file
|
||||
flock($log, LOCK_UN);
|
||||
}
|
||||
} catch (exception $e) {
|
||||
// Write to buffer of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,15 +4,15 @@ declare(strict_types=1);
|
|||
|
||||
namespace mirzaev\notchat\models;
|
||||
|
||||
// Framework for PHP
|
||||
use mirzaev\minimal\model;
|
||||
// Files of the project
|
||||
use mirzaev\notchat\models\enumerations\log as type;
|
||||
|
||||
// Built-in libraries
|
||||
use exception,
|
||||
DirectoryIterator as parser;
|
||||
|
||||
/**
|
||||
* Core of models
|
||||
* Server
|
||||
*
|
||||
* @package mirzaev\notchat\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
|
@ -38,6 +38,7 @@ class server extends core
|
|||
public static function write(string $domain, string $json = '', array &$errors = []): void
|
||||
{
|
||||
try {
|
||||
//
|
||||
if (strlen($domain) > 32) throw new exception('Domain cannot be longer than 32 characters');
|
||||
|
||||
// Initializing of path to file
|
||||
|
@ -57,7 +58,7 @@ class server extends core
|
|||
// File found
|
||||
|
||||
// Open file with server data
|
||||
$file = fopen($path, "r");
|
||||
$file = fopen($path, "c+");
|
||||
|
||||
// Read server data
|
||||
$old = json_decode(fread($file, filesize($path)), true, 8);
|
||||
|
@ -69,7 +70,7 @@ class server extends core
|
|||
// The keys match or the file has not been updated for more than 3 days
|
||||
|
||||
// Open file with server data
|
||||
$file = fopen($path, "w");
|
||||
$file = fopen($path, "c");
|
||||
|
||||
// Write server data
|
||||
fwrite($file, json_encode($new));
|
||||
|
@ -79,12 +80,15 @@ class server extends core
|
|||
|
||||
// Write DNS record
|
||||
dns::write(domain: $new['domain'], ip: $new['ip'], port: $new['port'], errors: $errors);
|
||||
|
||||
// Write to the log of servers
|
||||
log::write(type::SERVERS, "[UPDATE] {$old['domain']} {$old['ip']}:{$old['port']} -> {$new['domain']} {$new['ip']}:{$new['port']}");
|
||||
} else throw new exception('Public keys do not match');
|
||||
} else {
|
||||
// File is not found
|
||||
|
||||
// Open file with server data
|
||||
$file = fopen($path, "w");
|
||||
$file = fopen($path, "c");
|
||||
|
||||
// Write server data
|
||||
fwrite($file, json_encode($new));
|
||||
|
@ -94,15 +98,21 @@ class server extends core
|
|||
|
||||
// Write DNS record
|
||||
dns::write(domain: $new['domain'], ip: $new['ip'], port: $new['port'], errors: $errors);
|
||||
|
||||
// Write to the log of errors
|
||||
log::write(type::SERVERS, "[CREATE] {$new['domain']} {$new['ip']}:{$new['port']}");
|
||||
}
|
||||
} catch (exception $e) {
|
||||
// Write to buffer of errors
|
||||
// Write to the buffer of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
|
||||
// Write to the log of errors
|
||||
log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,18 +122,25 @@ class server extends core
|
|||
* Read JSON from file of server
|
||||
*
|
||||
* @param string $domain Domain of the server
|
||||
* @param int $time Number of seconds since the file was last edited (86400 seconds is 1 day)
|
||||
* @param array &$errors Buffer of errors
|
||||
*
|
||||
* @return string|null JSON with data of the server
|
||||
*/
|
||||
public static function read(string $domain, &$errors = []): ?string
|
||||
public static function read(string $domain, int $time = 86400, &$errors = []): ?string
|
||||
{
|
||||
try {
|
||||
// Initializing of path to file
|
||||
$path = static::SERVERS . DIRECTORY_SEPARATOR . "$domain.json";
|
||||
|
||||
if (file_exists($path) && filesize($path) > 0) {
|
||||
// File exists and not empty
|
||||
|
||||
if (time() - filectime($path) < $time && is_readable($path)) {
|
||||
// The file is actual (1 day by default) and writable
|
||||
|
||||
// Open file with server data
|
||||
$file = fopen($path, "r");
|
||||
$file = fopen($path, 'c+');
|
||||
|
||||
// Read server data
|
||||
$server = fread($file, filesize($path));
|
||||
|
@ -133,14 +150,19 @@ class server extends core
|
|||
|
||||
// Exit (success)
|
||||
return $server;
|
||||
}
|
||||
}
|
||||
} catch (exception $e) {
|
||||
// Write to buffer of errors
|
||||
// Write to the buffer of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
|
||||
// Write to the log of errors
|
||||
log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage());
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
|
@ -175,6 +197,8 @@ class server extends core
|
|||
$skip = $page * $amount;
|
||||
|
||||
foreach (new parser(static::SERVERS) as $file) {
|
||||
// Iterate through all files in a directory
|
||||
|
||||
// Skipping unnecessary files
|
||||
if (--$skip > $amount) continue;
|
||||
|
||||
|
@ -182,31 +206,39 @@ class server extends core
|
|||
if ($file->isDot()) continue;
|
||||
|
||||
if (time() - $file->getCTime() < $time && $file->isReadable()) {
|
||||
// The file is actual (3 days by default) and writable
|
||||
// The file is actual (1 day by default) and readable
|
||||
|
||||
// Open file with server data
|
||||
$server = $file->openFile('r');
|
||||
if (($size = $file->getSize()) > 0) {
|
||||
// The file is not empty
|
||||
|
||||
// Open the file with server data
|
||||
$server = $file->openFile('c+');
|
||||
|
||||
// Write server data to the output buffer
|
||||
$buffer[] = json_decode($server->fread($file->getSize()));
|
||||
$buffer[] = json_decode($server->fread($size));
|
||||
|
||||
// Close file with server data
|
||||
// Close the file with server data
|
||||
unset($file);
|
||||
}
|
||||
}
|
||||
|
||||
// Exit (success)
|
||||
if (--$amount < 1) break;
|
||||
}
|
||||
|
||||
// Exit (success)
|
||||
return $buffer;
|
||||
} catch (exception $e) {
|
||||
// Write to buffer of errors
|
||||
// Write to the buffer of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
|
||||
// Write to the log of errors
|
||||
log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage());
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\notchat\models;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\notchat\models\enumerations\log as type;
|
||||
|
||||
// Built-in libraries
|
||||
use exception,
|
||||
DirectoryIterator as parser;
|
||||
|
||||
/**
|
||||
* Text
|
||||
*
|
||||
* @package mirzaev\notchat\models
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
class text extends core
|
||||
{
|
||||
/**
|
||||
* Path to the directory with translation files (json)
|
||||
*/
|
||||
final public const LANGUAGES = core::PUBLIC . DIRECTORY_SEPARATOR . 'languages';
|
||||
|
||||
/**
|
||||
* Default language
|
||||
*/
|
||||
final public const LANGUAGE = 'english';
|
||||
|
||||
|
||||
/**
|
||||
* Read
|
||||
*
|
||||
* @param string $id Identifier
|
||||
* @param string $language Language (name of thw file without ".json")
|
||||
* @param array &$errors Buffer of errors
|
||||
*
|
||||
* @return string|null Text, if found
|
||||
*/
|
||||
public static function read(string $id, string $language = 'english', &$errors = []): ?string
|
||||
{
|
||||
try {
|
||||
// Initializing of path to the file of the log
|
||||
$path = static::LANGUAGES . DIRECTORY_SEPARATOR . "$language.json";
|
||||
|
||||
if (file_exists($path)) {
|
||||
// The file exists
|
||||
|
||||
// Open the file of translation
|
||||
$json = file_get_contents($path);
|
||||
|
||||
if (!empty($json)) {
|
||||
// The file is not empty
|
||||
|
||||
// Decoding JSON to Array
|
||||
$text = json_decode($json, true, 8);
|
||||
|
||||
// Exit (success)
|
||||
return $text[$id] ?? throw new exception('Could not find the text in translation file');
|
||||
}
|
||||
}
|
||||
} catch (exception $e) {
|
||||
// Write to buffer of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
|
||||
// Write to the log of errors
|
||||
log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage());
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a list of available languages
|
||||
*
|
||||
* @param array &$errors Buffer of errors
|
||||
*
|
||||
* @return array|null Languages, if they found
|
||||
*/
|
||||
public static function list(&$errors = []): ?array
|
||||
{
|
||||
try {
|
||||
// Initializing of the buffer of languages
|
||||
$languages = [];
|
||||
|
||||
foreach (new parser(static::LANGUAGES) as $file) {
|
||||
// Iterate through all files in the languages directory
|
||||
|
||||
// Skipping system shortcuts
|
||||
if ($file->isDot()) continue;
|
||||
|
||||
if ($file->isReadable() && $file->getSize() > 0) {
|
||||
// The file is readable and not empty
|
||||
|
||||
// Write a language to the buffer registry of available languages
|
||||
$languages[] = $file->getBasename('.json');
|
||||
}
|
||||
}
|
||||
|
||||
// Exit (success)
|
||||
return $languages;
|
||||
} catch (exception $e) {
|
||||
// Write to buffer of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
|
||||
// Write to the log of errors
|
||||
log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage());
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\notchat\models\traits;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\notchat\models\log,
|
||||
mirzaev\notchat\models\enumerations\log as type;
|
||||
|
||||
// Built-in libraries
|
||||
use exception,
|
||||
generator;
|
||||
|
||||
/**
|
||||
* Trait of the file handler
|
||||
*
|
||||
* @package mirzaev\notchat\models\traits
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
trait file
|
||||
{
|
||||
/**
|
||||
* Read
|
||||
*
|
||||
* @param resource $file File pointer (fopen())
|
||||
* @param int $limit Maximum limit of iterations (rows)
|
||||
* @param int $position Initial cursor position
|
||||
* @param int $step Row reading step
|
||||
* @param array &$errors Buffer of errors
|
||||
*
|
||||
* @return generator|string|null
|
||||
*/
|
||||
private static function read($file, int $limit = 500, int $position = 0, int $step = 1, &$errors = []): ?generator
|
||||
{
|
||||
try {
|
||||
while ($limit-- > 0) {
|
||||
// Recursive execution until $limit reaches 0
|
||||
|
||||
// Initializing of the buffer of row
|
||||
$row = '';
|
||||
|
||||
// Initializing the character buffer to generate $row
|
||||
$character = '';
|
||||
|
||||
do {
|
||||
// Iterate over rows
|
||||
|
||||
// End (or beginning) of file reached (success)
|
||||
if (feof($file)) return;
|
||||
|
||||
// Reading a row
|
||||
$row = $character . $row;
|
||||
|
||||
// Move to next position
|
||||
fseek($file, $position += $step, SEEK_END);
|
||||
|
||||
// Read a character
|
||||
$character = fgetc($file);
|
||||
|
||||
// Is the character a carriage return? (end of row)
|
||||
} while ($character != PHP_EOL);
|
||||
|
||||
// Exit (success)
|
||||
yield $row;
|
||||
}
|
||||
|
||||
// Exit (success)
|
||||
return null;
|
||||
} catch (exception $e) {
|
||||
// Write to the buffer of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
|
||||
// Write to the log of errors
|
||||
log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage());
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace mirzaev\notchat\models\traits;
|
||||
|
||||
// Files of the project
|
||||
use mirzaev\notchat\models\log as model,
|
||||
mirzaev\notchat\models\enumerations\log as type;
|
||||
|
||||
// Built-in libraries
|
||||
use exception;
|
||||
|
||||
/**
|
||||
* Trait of the log handler
|
||||
*
|
||||
* @package mirzaev\notchat\models\traits
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
trait log
|
||||
{
|
||||
/**
|
||||
* Deserializing of type::CONNECTIONS
|
||||
*
|
||||
* @param string $row Row from the log type::CONNECTIONS
|
||||
* @param array &$errors Buffer of errors
|
||||
*
|
||||
* @return array [$row, $date, $ip, $forwarded, $referer, $useragent]
|
||||
*/
|
||||
private static function connection(string $row, &$errors = []): ?array
|
||||
{
|
||||
try {
|
||||
// Search for parameters of connection
|
||||
preg_match('/(?:^\[(\d{4}\.\d{2}\.\d{2}\s\d{2}:\d{2}:\d{2})\]\s?)(?:\[(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]\s?)(?:\[(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]\s?)?(?:\[([^\]]+)\]\s[^$]?)?(?:\[([^\]]+)\]\s?)?$/', trim($row, PHP_EOL), $matches);
|
||||
|
||||
// Have all 5 parameters been detected?
|
||||
if (count($matches) !== 6) throw new exception('Failed to deserialize row');
|
||||
|
||||
// Exit (success)
|
||||
return $matches;
|
||||
} catch (exception $e) {
|
||||
// Write to the buffer of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
|
||||
// Write to the log of errors
|
||||
model::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage());
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializing of type::BANS
|
||||
*
|
||||
* @param string $row Row from the log type::BANS
|
||||
* @param array &$errors Buffer of errors
|
||||
*
|
||||
* @return array [$row, $from, $to, $ip]
|
||||
*/
|
||||
private static function ban(string $row, &$errors = []): ?array
|
||||
{
|
||||
try {
|
||||
// Search for parameters of ban
|
||||
preg_match('/(?:^\[(\d{4}\.\d{2}\.\d{2}\s\d{2}:\d{2}:\d{2})\]\s?)(?:\[(\d{4}\.\d{2}\.\d{2}\s\d{2}:\d{2}:\d{2})\]\s?)(?:\[([^\]]+)\]\s?)$/', trim($row, PHP_EOL), $matches);
|
||||
|
||||
// Have all 3 parameters been detected?
|
||||
if (count($matches) !== 4) throw new exception('Failed to deserialize row');
|
||||
|
||||
// Exit (success)
|
||||
return $matches;
|
||||
} catch (exception $e) {
|
||||
// Write to the buffer of errors
|
||||
$errors[] = [
|
||||
'text' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'stack' => $e->getTrace()
|
||||
];
|
||||
|
||||
// Write to the log of errors
|
||||
model::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage());
|
||||
}
|
||||
|
||||
// Exit (fail)
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ define('STORAGE', realpath('..' . DIRECTORY_SEPARATOR . 'storage'));
|
|||
define('INDEX', __DIR__);
|
||||
|
||||
// Автозагрузка
|
||||
require __DIR__ . DIRECTORY_SEPARATOR
|
||||
require INDEX . DIRECTORY_SEPARATOR
|
||||
. '..' . DIRECTORY_SEPARATOR
|
||||
. '..' . DIRECTORY_SEPARATOR
|
||||
. '..' . DIRECTORY_SEPARATOR
|
||||
|
@ -33,10 +33,14 @@ require __DIR__ . DIRECTORY_SEPARATOR
|
|||
$router = new router;
|
||||
|
||||
// Запись маршрутов
|
||||
$router->write('/', 'index', 'index');
|
||||
$router->write('/', 'index', 'index', 'GET');
|
||||
$router->write('/', 'index', 'index', 'POST');
|
||||
$router->write('/manifest', 'index', 'manifest', 'GET');
|
||||
$router->write('/offline', 'index', 'offline', 'GET');
|
||||
$router->write('/cache.js', 'index', 'cache', 'GET');
|
||||
$router->write('/server/read/$server', 'server', 'read', 'POST');
|
||||
$router->write('/server/write/$server', 'server', 'write', 'POST');
|
||||
$router->write('/servers', 'index', 'servers', 'POST');
|
||||
$router->write('/servers/connect/$domain', 'server', 'write', 'POST');
|
||||
|
||||
// Инициализация ядра
|
||||
$core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false));
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
"use strict";
|
||||
|
||||
const VERSION = "0.1.0";
|
||||
const EXPIRED = 86400000;
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
event
|
||||
.respondWith(
|
||||
caches
|
||||
.match(event.request)
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
// Found file in cache
|
||||
|
||||
if (
|
||||
Date.now() - new Date(response.headers.get("last-modified")) >
|
||||
EXPIRED
|
||||
) {
|
||||
// Expired period of storage response
|
||||
|
||||
return fetch(event.request.clone())
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
// Downloaded new version
|
||||
|
||||
return caches
|
||||
.open(VERSION)
|
||||
.then((cache) => {
|
||||
// Writing new version to cache
|
||||
cache.put(event.request, response.clone());
|
||||
|
||||
// Exit (success)
|
||||
return response;
|
||||
});
|
||||
} else throw "Failed to download new version";
|
||||
})
|
||||
.catch(() => {
|
||||
// Exit (success) (return old version)
|
||||
return response;
|
||||
});
|
||||
} else return response;
|
||||
} else {
|
||||
// Not found file in cache
|
||||
|
||||
return fetch(event.request.clone())
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
// Downloaded
|
||||
|
||||
return caches
|
||||
.open(VERSION)
|
||||
.then((cache) => {
|
||||
// Writing to cache
|
||||
cache.put(event.request, response.clone());
|
||||
|
||||
// Exit (success)
|
||||
return response;
|
||||
});
|
||||
} else throw "Failed to download";
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
return caches.match("/offline");
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
/* self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((keyList) => {
|
||||
return Promise.all(
|
||||
keyList.map((key) => {
|
||||
// Deleting old versions of cache
|
||||
if (VERSION.indexOf(key) === -1) return caches.delete(key);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}); */
|
|
@ -10,27 +10,161 @@ if (typeof window.chats !== "function") {
|
|||
*/
|
||||
static server = {
|
||||
/**
|
||||
* Select server
|
||||
* Select a server (dampered)
|
||||
*
|
||||
* @param {string} server Domain or IP-address of the server (from cache by default)
|
||||
* @param {string} server Domain or IP-address:port of the server (from cache by default)
|
||||
* @param {bool} force Force execution
|
||||
*
|
||||
* @return {void} Into the document will be generated and injected an HTML-element
|
||||
*/
|
||||
select(server = localStorage.server_ip ?? localStorage.server_domain) {
|
||||
if (typeof server === "string" && server.length > 0) {
|
||||
if (core.servers instanceof HTMLElement) {
|
||||
core.request(`/server/read/${server}`).then((json) => {
|
||||
select(
|
||||
server = localStorage.server_ip && localStorage.server_port
|
||||
? localStorage.server_ip + ":" + localStorage.server_port
|
||||
: localStorage.server_domain,
|
||||
force = false,
|
||||
) {
|
||||
// Writing status: "connecting"
|
||||
core.menu.setAttribute("data-menu-status", "connecting");
|
||||
|
||||
// Deinitializing animation of opening
|
||||
core.menu.getElementsByTagName("output")[0].classList.remove(
|
||||
"slide-down",
|
||||
);
|
||||
|
||||
// Initializing animation of closing
|
||||
core.menu.getElementsByTagName("output")[0].classList.add(
|
||||
"slide-down-revert",
|
||||
);
|
||||
|
||||
// Disabled for animation
|
||||
/* core.menu.querySelector('figcaption[data-server="domain"]')
|
||||
.innerText =
|
||||
core.menu.querySelector('pre[data-server="description"]')
|
||||
.innerText =
|
||||
""; */
|
||||
|
||||
this._select(server, force);
|
||||
},
|
||||
|
||||
/**
|
||||
* Select a server
|
||||
*
|
||||
* @param {string} server Domain or IP-address:port of the server (from cache by default)
|
||||
* @param {bool} force Force execution
|
||||
*
|
||||
* @return {void} Into the document will be generated and injected an HTML-element
|
||||
*/
|
||||
_select: damper(
|
||||
(
|
||||
server = localStorage.server_ip && localStorage.server_port
|
||||
? localStorage.server_ip + ":" + localStorage.server_port
|
||||
: localStorage.server_domain,
|
||||
force = false,
|
||||
) => {
|
||||
if (server.length > 512) {
|
||||
notifications.write(text.read("CHATS_SERVER_ERROR_LENGTH_MAX"));
|
||||
} else if (typeof server === "string" && server.length > 0) {
|
||||
if (
|
||||
core.menu instanceof HTMLElement &&
|
||||
core.menu.getAttribute("data-menu") === "chats"
|
||||
) {
|
||||
//
|
||||
|
||||
// Initializing the unlock function
|
||||
function unblock() {
|
||||
// Writing status: "empty"
|
||||
if (
|
||||
core.menu.querySelector('figcaption[data-server="domain"]')
|
||||
.innerText.length === 0 &&
|
||||
core.menu.querySelector('pre[data-server="description"]')
|
||||
.innerText.length === 0
|
||||
) {
|
||||
core.menu.getElementsByTagName("search")[0]
|
||||
.getElementsByTagName("label")[0].classList.add(
|
||||
"empty",
|
||||
);
|
||||
}
|
||||
|
||||
// Writing status: "disconnected"
|
||||
core.menu.setAttribute("data-menu-status", "disconnected");
|
||||
}
|
||||
|
||||
// Initiating a unlock delay in case the server does not respond
|
||||
const timeout = setTimeout(() => {
|
||||
// this.errors(["Server does not respond"]);
|
||||
unblock();
|
||||
}, 5000);
|
||||
|
||||
core.request(
|
||||
`/server/read/${encodeURIComponent(server)}`,
|
||||
`language=${localStorage.language ?? "english"}`,
|
||||
).then((json) => {
|
||||
// Deinitializing of unlock delay
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (
|
||||
json.errors !== null && typeof json.errors === "object" &&
|
||||
json.errors.length > 0
|
||||
) {} else {
|
||||
document.querySelector('figcaption[data-server="ip"]').innerText = `${json.ip}:${json.port}`;
|
||||
document.querySelector('figcaption[data-server="description"]').innerText = `${json.description}`;
|
||||
) {
|
||||
// Generating notifications with errors
|
||||
// for (const error of json.errors) notifications.write(error);
|
||||
|
||||
// Writing status: "disconnected"
|
||||
core.menu.setAttribute("data-menu-status", "disconnected");
|
||||
|
||||
// Writind domain of the server
|
||||
core.menu.querySelector('figcaption[data-server="domain"]')
|
||||
.innerText = "";
|
||||
|
||||
// Writing description of the server
|
||||
core.menu.querySelector('pre[data-server="description"]')
|
||||
.innerText = "";
|
||||
|
||||
// Writing status: "empty" (for opening the description window)
|
||||
core.menu.getElementsByTagName("search")[0]
|
||||
.getElementsByTagName("label")[0].classList.add(
|
||||
"empty",
|
||||
);
|
||||
} else {
|
||||
// Writing status: "connected"
|
||||
core.menu.setAttribute("data-menu-status", "connected");
|
||||
|
||||
// Writind domain of the server
|
||||
core.menu.querySelector('figcaption[data-server="domain"]')
|
||||
.innerText = `${json.server.domain}`;
|
||||
|
||||
// Writing description of the server
|
||||
core.menu.querySelector('pre[data-server="description"]')
|
||||
.innerText = `${json.server.description}`;
|
||||
|
||||
// Deleting status: "empty" (for opening the description window) (it is implied that the response from the server cannot be empty)
|
||||
core.menu.getElementsByTagName("search")[0]
|
||||
.getElementsByTagName("label")[0].classList.remove(
|
||||
"empty",
|
||||
);
|
||||
|
||||
// Deinitializing animation of closing
|
||||
core.menu.getElementsByTagName("output")[0].classList.remove(
|
||||
"slide-down-revert",
|
||||
);
|
||||
|
||||
// Initializing animation of opening
|
||||
core.menu.getElementsByTagName("output")[0].classList.add(
|
||||
"slide-down",
|
||||
);
|
||||
|
||||
// Writing data of the server to local storage (browser)
|
||||
localStorage.server_domain = json.server.domain;
|
||||
localStorage.server_ip = json.server.ip;
|
||||
localStorage.server_port = json.server.port;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
800,
|
||||
1,
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -44,26 +178,41 @@ if (typeof window.chats !== "function") {
|
|||
*
|
||||
* @return {void} Into the document will be generated and injected an HTML-element
|
||||
*/
|
||||
servers(server = localStorage.server_ip ?? localStorage.server_domain) {
|
||||
servers(
|
||||
server = `${localStorage.server_ip}:${localStorage.server_port}`,
|
||||
) {
|
||||
core.request(
|
||||
"/servers",
|
||||
typeof server === "string" && server.length > 0
|
||||
? `server=${server}}`
|
||||
? `server=${server}`
|
||||
: "",
|
||||
).then((json) => {
|
||||
if (core.servers instanceof HTMLElement) core.servers.remove();
|
||||
if (
|
||||
json.errors !== null && typeof json.errors === "object" &&
|
||||
json.errors.length > 0
|
||||
) {} else {
|
||||
const element = document.createElement("div");
|
||||
core.header.after(element);
|
||||
) {
|
||||
// Generating notifications with errors
|
||||
for (const error of json.errors) notifications.write(error);
|
||||
} else {
|
||||
if (core.menu instanceof HTMLElement) {
|
||||
// Writing status of connection (hack with replaying animations)
|
||||
core.menu.setAttribute('data-menu-status', 'disconnected');
|
||||
setTimeout(() => core.menu.setAttribute('data-menu-status', json.status ?? 'disconnected'), 100);
|
||||
|
||||
const element = document.createElement("search");
|
||||
|
||||
const search = core.menu.getElementsByTagName("search")[0];
|
||||
if (search instanceof HTMLElement) search.remove();
|
||||
|
||||
core.menu.prepend(element);
|
||||
element.outerHTML = json.html;
|
||||
|
||||
core.servers = document.body.querySelector(
|
||||
"section[data-section='servers']",
|
||||
core.menu = document.body.querySelector(
|
||||
"section[data-section='menu']",
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -77,7 +226,11 @@ if (typeof window.chats !== "function") {
|
|||
if (
|
||||
json.errors !== null && typeof json.errors === "object" &&
|
||||
json.errors.length > 0
|
||||
) {} else {
|
||||
) {
|
||||
// Generating notifications with errors
|
||||
for (const error of json.errors) notifications.write(error);
|
||||
} else {
|
||||
// сосать бебру
|
||||
const element = document.createElement("div");
|
||||
const position = core.main.children.length;
|
||||
element.style.setProperty("--position", position);
|
||||
|
|
|
@ -5,8 +5,11 @@ if (typeof window.core !== "function") {
|
|||
|
||||
// Initialize of the class in global namespace
|
||||
window.core = class core {
|
||||
// Label for the <main> element
|
||||
static main = document.body.getElementsByTagName('main')[0];
|
||||
// Domain
|
||||
static domain = window.location.hostname;
|
||||
|
||||
// Animations are enabled?
|
||||
static animations = getComputedStyle(document.body).getPropertyValue('--animations') === '1';
|
||||
|
||||
// Label for the <header> element
|
||||
static header = document.body.getElementsByTagName('header')[0];
|
||||
|
@ -14,11 +17,11 @@ if (typeof window.core !== "function") {
|
|||
// Label for the <aside> element
|
||||
static aside = document.body.getElementsByTagName('aside')[0];
|
||||
|
||||
// Label for the "servers" element
|
||||
static servers = document.body.querySelector("section[data-section='servers']");
|
||||
// Label for the "menu" element
|
||||
static menu = document.body.querySelector("section[data-section='menu']");
|
||||
|
||||
// Label for the "chats" element
|
||||
static chats = document.body.querySelector("section[data-section='chats']");
|
||||
// Label for the <main> element
|
||||
static main = document.body.getElementsByTagName('main')[0];
|
||||
|
||||
// Label for the <footer> element
|
||||
static footer = document.body.getElementsByTagName('footer')[0];
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
"use strict";
|
||||
|
||||
/**
|
||||
* Damper
|
||||
*
|
||||
* @param {function} function Function to execute after damping
|
||||
* @param {number} timeout Timer in milliseconds (ms)
|
||||
* @param {number} force Argument number storing the enforcement status of execution (see @example)
|
||||
*
|
||||
* @example
|
||||
* $a = damper(
|
||||
* async (
|
||||
* a, // 0
|
||||
* b, // 1
|
||||
* c, // 2
|
||||
* force = false, // 3
|
||||
* d // 4
|
||||
* ) => {
|
||||
* // Body of function
|
||||
* },
|
||||
* 500,
|
||||
* 3, // 3 -> "force" argument
|
||||
* );
|
||||
*
|
||||
* $a('for a', 'for b', 'for c', true, 'for d'); // Force execute is enabled
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
function damper(func, timeout = 300, force) {
|
||||
// Initializing of the timer
|
||||
let timer;
|
||||
|
||||
return (...args) => {
|
||||
// Deinitializing of the timer
|
||||
clearTimeout(timer);
|
||||
|
||||
if (typeof force === "number" && args[force]) {
|
||||
// Force execution (ignoring the timer)
|
||||
|
||||
func.apply(this, args);
|
||||
} else {
|
||||
// Normal execution
|
||||
|
||||
// Execute the handled function (entry into recursion)
|
||||
timer = setTimeout(() => {
|
||||
func.apply(this, args);
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Dispatch event: "initialized"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("damper.initialized", {
|
||||
detail: { damper },
|
||||
}),
|
||||
);
|
|
@ -0,0 +1,668 @@
|
|||
"use strict";
|
||||
|
||||
/**
|
||||
* Бегущая строка
|
||||
*
|
||||
* @description
|
||||
* Простой, но мощный класс для создания бегущих строк. Поддерживает
|
||||
* перемещение мышью и прокрутку колесом, полностью настраивается очень гибок
|
||||
* для настроек в CSS и подразумевается, что отлично индексируется поисковыми роботами.
|
||||
* Имеет свой препроцессор, благодаря которому можно создавать бегущие строки
|
||||
* без программирования - с помощью HTML-аттрибутов, а так же возможность
|
||||
* изменять параметры (data-hotline-* аттрибуты) на лету. Есть возможность вызывать
|
||||
* события при выбранных действиях для того, чтобы пользователь имел возможность
|
||||
* дорабатывать функционал без изучения и изменения моего кода
|
||||
*
|
||||
* @example
|
||||
* сonst hotline = new hotline();
|
||||
* hotline.step = '-5';
|
||||
* hotline.start();
|
||||
*
|
||||
* @todo
|
||||
* 1. Бесконечный режим - элементы не удаляются если видны на экране (будут дубликаты)
|
||||
*
|
||||
* @copyright WTFPL
|
||||
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
|
||||
*/
|
||||
class hotline {
|
||||
// Идентификатор
|
||||
#id = 0;
|
||||
|
||||
// Оболочка (instanceof HTMLElement)
|
||||
#shell = document.getElementById("hotline");
|
||||
|
||||
// Инстанция горячей строки
|
||||
#instance = null;
|
||||
|
||||
// Перемещение
|
||||
#transfer = true;
|
||||
|
||||
// Движение
|
||||
#move = true;
|
||||
|
||||
// Наблюдатель
|
||||
#observer = null;
|
||||
|
||||
// Наблюдатель
|
||||
#block = new Set(["events"]);
|
||||
|
||||
// Настраиваемые параметры
|
||||
transfer = null;
|
||||
move = null;
|
||||
delay = 10;
|
||||
step = 1;
|
||||
hover = true;
|
||||
movable = true;
|
||||
sticky = false;
|
||||
wheel = false;
|
||||
delta = null;
|
||||
vertical = false;
|
||||
observe = false;
|
||||
events = new Map([
|
||||
["start", false],
|
||||
["stop", false],
|
||||
["move", false],
|
||||
["move.block", false],
|
||||
["move.unblock", false],
|
||||
["offset", false],
|
||||
["transfer.start", true],
|
||||
["transfer.end", true],
|
||||
["onmousemove", false]
|
||||
]);
|
||||
|
||||
constructor(id, shell) {
|
||||
// Запись идентификатора
|
||||
if (typeof id === "string" || typeof id === "number") this.#id = id;
|
||||
|
||||
// Запись оболочки
|
||||
if (shell instanceof HTMLElement) this.#shell = shell;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.#instance === null) {
|
||||
// Нет запущенной инстанции бегущей строки
|
||||
|
||||
// Инициализация ссылки на ядро
|
||||
const _this = this;
|
||||
|
||||
// Запуск движения
|
||||
this.#instance = setInterval(function () {
|
||||
if (_this.#shell.childElementCount > 1) {
|
||||
// Найдено содержимое бегущей строки (2 и более)
|
||||
|
||||
// Инициализация буфера для временных данных
|
||||
let buffer;
|
||||
|
||||
// Инициализация данных первого элемента в строке
|
||||
const first = {
|
||||
element: (buffer = _this.#shell.firstElementChild),
|
||||
coords: buffer.getBoundingClientRect()
|
||||
};
|
||||
|
||||
if (_this.vertical) {
|
||||
// Вертикальная бегущая строка
|
||||
|
||||
// Инициализация сдвига у первого элемента (движение)
|
||||
first.offset = isNaN(
|
||||
(buffer = parseFloat(first.element.style.marginTop))
|
||||
)
|
||||
? 0
|
||||
: buffer;
|
||||
|
||||
// Инициализация отступа до второго элемента у первого элемента (разделение)
|
||||
first.separator = isNaN(
|
||||
(buffer = parseFloat(
|
||||
getComputedStyle(first.element).marginBottom
|
||||
))
|
||||
)
|
||||
? 0
|
||||
: buffer;
|
||||
|
||||
// Инициализация крайнего с конца ребра первого элемента в строке
|
||||
first.end = first.coords.y + first.coords.height + first.separator;
|
||||
} else {
|
||||
// Горизонтальная бегущая строка
|
||||
|
||||
// Инициализация отступа у первого элемента (движение)
|
||||
first.offset = isNaN(
|
||||
(buffer = parseFloat(first.element.style.marginLeft))
|
||||
)
|
||||
? 0
|
||||
: buffer;
|
||||
|
||||
// Инициализация отступа до второго элемента у первого элемента (разделение)
|
||||
first.separator = isNaN(
|
||||
(buffer = parseFloat(getComputedStyle(first.element).marginRight))
|
||||
)
|
||||
? 0
|
||||
: buffer;
|
||||
|
||||
// Инициализация крайнего с конца ребра первого элемента в строке
|
||||
first.end = first.coords.x + first.coords.width + first.separator;
|
||||
}
|
||||
|
||||
if (
|
||||
(_this.vertical &&
|
||||
Math.round(first.end) < _this.#shell.offsetTop) ||
|
||||
(!_this.vertical && Math.round(first.end) < _this.#shell.offsetLeft)
|
||||
) {
|
||||
// Элемент (вместе с отступом до второго элемента) вышел из области видимости (строки)
|
||||
|
||||
if (
|
||||
(_this.transfer === null && _this.#transfer) ||
|
||||
_this.transfer === true
|
||||
) {
|
||||
// Перенос разрешен
|
||||
|
||||
if (_this.vertical) {
|
||||
// Вертикальная бегущая строка
|
||||
|
||||
// Удаление отступов (движения)
|
||||
first.element.style.marginTop = null;
|
||||
} else {
|
||||
// Горизонтальная бегущая строка
|
||||
|
||||
// Удаление отступов (движения)
|
||||
first.element.style.marginLeft = null;
|
||||
}
|
||||
|
||||
// Копирование первого элемента в конец строки
|
||||
_this.#shell.appendChild(first.element);
|
||||
|
||||
if (_this.events.get("transfer.end")) {
|
||||
// Запрошен вызов события: "перемещение в конец"
|
||||
|
||||
// Вызов события: "перемещение в конец"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${_this.#id}.transfer.end`, {
|
||||
detail: {
|
||||
element: first.element,
|
||||
offset: -(
|
||||
(_this.vertical
|
||||
? first.coords.height
|
||||
: first.coords.width) + first.separator
|
||||
)
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
(_this.vertical &&
|
||||
Math.round(first.coords.y) > _this.#shell.offsetTop) ||
|
||||
(!_this.vertical &&
|
||||
Math.round(first.coords.x) > _this.#shell.offsetLeft)
|
||||
) {
|
||||
// Передняя (движущая) граница первого элемента вышла из области видимости
|
||||
|
||||
if (
|
||||
(_this.transfer === null && _this.#transfer) ||
|
||||
_this.transfer === true
|
||||
) {
|
||||
// Перенос разрешен
|
||||
|
||||
// Инициализация отступа у последнего элемента (разделение)
|
||||
const separator =
|
||||
(buffer = isNaN(
|
||||
(buffer = parseFloat(
|
||||
getComputedStyle(_this.#shell.lastElementChild)[
|
||||
_this.vertical ? "marginBottom" : "marginRight"
|
||||
]
|
||||
))
|
||||
)
|
||||
? 0
|
||||
: buffer) === 0
|
||||
? first.separator
|
||||
: buffer;
|
||||
|
||||
// Инициализация координат первого элемента в строке
|
||||
const coords = _this.#shell.lastElementChild.getBoundingClientRect();
|
||||
|
||||
if (_this.vertical) {
|
||||
// Вертикальная бегущая строка
|
||||
|
||||
// Удаление отступов (движения)
|
||||
_this.#shell.lastElementChild.style.marginTop =
|
||||
-coords.height - separator + "px";
|
||||
} else {
|
||||
// Горизонтальная бегущая строка
|
||||
|
||||
// Удаление отступов (движения)
|
||||
_this.#shell.lastElementChild.style.marginLeft =
|
||||
-coords.width - separator + "px";
|
||||
}
|
||||
|
||||
// Копирование последнего элемента в начало строки
|
||||
_this.#shell.insertBefore(
|
||||
_this.#shell.lastElementChild,
|
||||
first.element
|
||||
);
|
||||
|
||||
// Удаление отступов у второго элемента в строке (движения)
|
||||
_this.#shell.children[1].style[
|
||||
_this.vertical ? "marginTop" : "marginLeft"
|
||||
] = null;
|
||||
|
||||
if (_this.events.get("transfer.start")) {
|
||||
// Запрошен вызов события: "перемещение в начало"
|
||||
|
||||
// Вызов события: "перемещение в начало"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${_this.#id}.transfer.start`, {
|
||||
detail: {
|
||||
element: _this.#shell.lastElementChild,
|
||||
offset:
|
||||
(_this.vertical ? coords.height : coords.width) +
|
||||
separator
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Элемент в области видимости
|
||||
|
||||
if ((_this.move === null && _this.#move) || _this.move === true) {
|
||||
// Движение разрешено
|
||||
|
||||
// Запись новых координат сдвига
|
||||
const offset = first.offset + _this.step;
|
||||
|
||||
// Запись сдвига (движение)
|
||||
_this.offset(offset);
|
||||
|
||||
if (_this.events.get("move")) {
|
||||
// Запрошен вызов события: "движение"
|
||||
|
||||
// Вызов события: "движение"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${_this.#id}.move`, {
|
||||
detail: {
|
||||
from: first.offset,
|
||||
to: offset
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, _this.delay);
|
||||
|
||||
if (this.hover) {
|
||||
// Запрошена возможность останавливать бегущую строку
|
||||
|
||||
// Инициализация сдвига
|
||||
let offset = 0;
|
||||
|
||||
// Инициализация слушателя события при перемещении элемента в бегущей строке
|
||||
const listener = function (e) {
|
||||
// Увеличение сдвига
|
||||
offset += e.detail.offset ?? 0;
|
||||
};
|
||||
|
||||
// Инициализация обработчика наведения курсора (остановка движения)
|
||||
this.#shell.onmouseover = function (e) {
|
||||
// Курсор наведён на бегущую строку
|
||||
|
||||
// Блокировка движения
|
||||
_this.#move = false;
|
||||
|
||||
if (_this.events.get("move.block")) {
|
||||
// Запрошен вызов события: "блокировка движения"
|
||||
|
||||
// Вызов события: "блокировка движения"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${_this.#id}.move.block`)
|
||||
);
|
||||
}
|
||||
|
||||
if (_this.movable) {
|
||||
// Запрошена возможность двигать бегущую строку
|
||||
|
||||
_this.#shell.onmousedown = function (onmousedown) {
|
||||
// Курсор активирован
|
||||
|
||||
// Инициализация слушателей события перемещения элемента в бегущей строке
|
||||
document.addEventListener(
|
||||
`hotline.${_this.#id}.transfer.start`,
|
||||
listener
|
||||
);
|
||||
document.addEventListener(
|
||||
`hotline.${_this.#id}.transfer.end`,
|
||||
listener
|
||||
);
|
||||
|
||||
// Инициализация буфера для временных данных
|
||||
let buffer;
|
||||
|
||||
// Инициализация данных первого элемента в строке
|
||||
const first = {
|
||||
offset: isNaN(
|
||||
(buffer = parseFloat(
|
||||
_this.vertical
|
||||
? _this.#shell.firstElementChild.style.marginTop
|
||||
: _this.#shell.firstElementChild.style.marginLeft
|
||||
))
|
||||
)
|
||||
? 0
|
||||
: buffer
|
||||
};
|
||||
|
||||
document.onmousemove = function (onmousemove) {
|
||||
// Курсор движется
|
||||
|
||||
if (_this.vertical) {
|
||||
// Вертикальная бегущая строка
|
||||
|
||||
// Инициализация буфера местоположения
|
||||
const from = _this.#shell.firstElementChild.style.marginTop;
|
||||
const to = onmousemove.pageY - (onmousedown.pageY + offset - first.offset);
|
||||
|
||||
// Движение
|
||||
_this.#shell.firstElementChild.style.marginTop = to +
|
||||
"px";
|
||||
|
||||
if (_this.events.get("onmousemove")) {
|
||||
// Запрошен вызов события: "перемещение мышью"
|
||||
|
||||
// Вызов события: "перемещение мышью"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${_this.#id}.onmousemove`, {
|
||||
detail: { from, to }
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Горизонтальная бегущая строка
|
||||
|
||||
// Инициализация буфера местоположения
|
||||
const from = _this.#shell.firstElementChild.style.marginLeft;
|
||||
const to = onmousemove.pageX - (onmousedown.pageX + offset - first.offset);
|
||||
|
||||
// Движение
|
||||
_this.#shell.firstElementChild.style.marginLeft = to + "px";
|
||||
|
||||
if (_this.events.get("onmousemove")) {
|
||||
// Запрошен вызов события: "перемещение мышью"
|
||||
|
||||
// Вызов события: "перемещение мышью"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${_this.#id}.onmousemove`, {
|
||||
detail: { from, to }
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Запись курсора
|
||||
_this.#shell.style.cursor = "grabbing";
|
||||
};
|
||||
};
|
||||
|
||||
// Перещапись событий браузера (чтобы не дёргалось)
|
||||
_this.#shell.ondragstart = null;
|
||||
|
||||
_this.#shell.onmouseup = function () {
|
||||
// Курсор деактивирован
|
||||
|
||||
// Остановка обработки движения
|
||||
document.onmousemove = null;
|
||||
|
||||
// Сброс сдвига
|
||||
offset = 0;
|
||||
|
||||
document.removeEventListener(
|
||||
`hotline.${_this.#id}.transfer.start`,
|
||||
listener
|
||||
);
|
||||
document.removeEventListener(
|
||||
`hotline.${_this.#id}.transfer.end`,
|
||||
listener
|
||||
);
|
||||
|
||||
// Восстановление курсора
|
||||
_this.#shell.style.cursor = null;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Инициализация обработчика отведения курсора (остановка движения)
|
||||
this.#shell.onmouseleave = function (onmouseleave) {
|
||||
// Курсор отведён от бегущей строки
|
||||
|
||||
if (!_this.sticky) {
|
||||
// Отключено прилипание
|
||||
|
||||
// Остановка обработки движения
|
||||
document.onmousemove = null;
|
||||
|
||||
document.removeEventListener(
|
||||
`hotline.${_this.#id}.transfer.start`,
|
||||
listener
|
||||
);
|
||||
document.removeEventListener(
|
||||
`hotline.${_this.#id}.transfer.end`,
|
||||
listener
|
||||
);
|
||||
|
||||
// Восстановление курсора
|
||||
_this.#shell.style.cursor = null;
|
||||
}
|
||||
|
||||
// Сброс сдвига
|
||||
offset = 0;
|
||||
|
||||
// Разблокировка движения
|
||||
_this.#move = true;
|
||||
|
||||
if (_this.events.get("move.unblock")) {
|
||||
// Запрошен вызов события: "разблокировка движения"
|
||||
|
||||
// Вызов события: "разблокировка движения"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${_this.#id}.move.unblock`)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (this.wheel) {
|
||||
// Запрошена возможность прокручивать колесом мыши
|
||||
|
||||
// Инициализация обработчика наведения курсора (остановка движения)
|
||||
this.#shell.onwheel = function (e) {
|
||||
// Курсор наведён на бегущую
|
||||
|
||||
// Инициализация буфера для временных данных
|
||||
let buffer;
|
||||
|
||||
// Перемещение
|
||||
_this.offset(
|
||||
(isNaN(
|
||||
(buffer = parseFloat(
|
||||
_this.#shell.firstElementChild.style[
|
||||
_this.vertical ? "marginTop" : "marginLeft"
|
||||
]
|
||||
))
|
||||
)
|
||||
? 0
|
||||
: buffer) +
|
||||
(_this.delta === null
|
||||
? e.wheelDelta
|
||||
: e.wheelDelta > 0
|
||||
? _this.delta
|
||||
: -_this.delta)
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (this.observe) {
|
||||
// Запрошено наблюдение за изменениями аттрибутов элемента бегущей строки
|
||||
|
||||
if (this.#observer === null) {
|
||||
// Отсутствует наблюдатель
|
||||
|
||||
// Инициализация ссылки на ядро
|
||||
const _this = this;
|
||||
|
||||
// Инициализация наблюдателя
|
||||
this.#observer = new MutationObserver(function (mutations) {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "attributes") {
|
||||
// Запись параметра в инстанцию бегущей строки
|
||||
_this.write(mutation.attributeName);
|
||||
}
|
||||
}
|
||||
|
||||
// Перезапуск бегущей строки
|
||||
_this.restart();
|
||||
});
|
||||
|
||||
// Активация наблюдения
|
||||
this.#observer.observe(this.#shell, {
|
||||
attributes: true
|
||||
});
|
||||
}
|
||||
} else if (this.#observer instanceof MutationObserver) {
|
||||
// Запрошено отключение наблюдения
|
||||
|
||||
// Деактивация наблюдения
|
||||
this.#observer.disconnect();
|
||||
|
||||
// Удаление наблюдателя
|
||||
this.#observer = null;
|
||||
}
|
||||
|
||||
if (this.events.get("start")) {
|
||||
// Запрошен вызов события: "запуск"
|
||||
|
||||
// Вызов события: "запуск"
|
||||
document.dispatchEvent(new CustomEvent(`hotline.${this.#id}.start`));
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
// Остановка бегущей строки
|
||||
clearInterval(this.#instance);
|
||||
|
||||
// Удаление инстанции интервала
|
||||
this.#instance = null;
|
||||
|
||||
if (this.events.get("stop")) {
|
||||
// Запрошен вызов события: "остановка"
|
||||
|
||||
// Вызов события: "остановка"
|
||||
document.dispatchEvent(new CustomEvent(`hotline.${this.#id}.stop`));
|
||||
}
|
||||
}
|
||||
|
||||
restart() {
|
||||
// Остановка бегущей строки
|
||||
this.stop();
|
||||
|
||||
// Запуск бегущей строки
|
||||
this.start();
|
||||
}
|
||||
|
||||
write(attribute) {
|
||||
// Инициализация названия параметра
|
||||
const parameter = (/^data-hotline-(\w+)$/.exec(attribute) ?? [, null])[1];
|
||||
|
||||
if (typeof parameter === "string") {
|
||||
// Параметр найден
|
||||
|
||||
// Проверка на разрешение изменения
|
||||
if (this.#block.has(parameter)) return;
|
||||
|
||||
// Инициализация значения параметра
|
||||
const value = this.#shell.getAttribute(attribute);
|
||||
|
||||
// Инициализация буфера для временных данных
|
||||
let buffer;
|
||||
|
||||
// Запись параметра
|
||||
this[parameter] = isNaN((buffer = parseFloat(value)))
|
||||
? value === "true"
|
||||
? true
|
||||
: value === "false"
|
||||
? false
|
||||
: value
|
||||
: buffer;
|
||||
}
|
||||
}
|
||||
|
||||
offset(value) {
|
||||
// Запись отступа
|
||||
this.#shell.firstElementChild.style[
|
||||
this.vertical ? "marginTop" : "marginLeft"
|
||||
] = value + "px";
|
||||
|
||||
if (this.events.get("offset")) {
|
||||
// Запрошен вызов события: "сдвиг"
|
||||
|
||||
// Вызов события: "сдвиг"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.${this.#id}.offset`, {
|
||||
detail: {
|
||||
to: value
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static preprocessing(event = false) {
|
||||
// Инициализация счётчиков инстанций горячей строки
|
||||
const success = new Set();
|
||||
let error = 0;
|
||||
|
||||
for (const element of document.querySelectorAll('*[data-hotline="true"]')) {
|
||||
// Перебор бегущих строк
|
||||
|
||||
if (typeof element.id === "string") {
|
||||
// Найден идентификатор
|
||||
|
||||
// Инициализация инстанции бегущей строки
|
||||
const hotline = new this(element.id, element);
|
||||
|
||||
for (const attribute of element.getAttributeNames()) {
|
||||
// Перебор аттрибутов
|
||||
|
||||
// Запись параметра в инстанцию бегущей строки
|
||||
hotline.write(attribute);
|
||||
}
|
||||
|
||||
// Запуск бегущей строки
|
||||
hotline.start();
|
||||
|
||||
// Запись инстанции бегущей строки в элемент
|
||||
element.hotline = hotline;
|
||||
|
||||
// Запись в счётчик успешных инициализаций
|
||||
success.add(hotline);
|
||||
} else ++error;
|
||||
}
|
||||
|
||||
if (event) {
|
||||
// Запрошен вызов события: "предварительная подготовка"
|
||||
|
||||
// Вызов события: "предварительная подготовка"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(`hotline.preprocessed`, {
|
||||
detail: {
|
||||
success,
|
||||
error
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("hotline.loaded", {
|
||||
detail: { hotline }
|
||||
})
|
||||
);
|
|
@ -0,0 +1,19 @@
|
|||
"use strict";
|
||||
|
||||
if (typeof window.journal !== "function") {
|
||||
// Not initialized
|
||||
|
||||
// Initialize of the class in global namespace
|
||||
window.journal = class cache {
|
||||
static write(text) {
|
||||
console.log(`[${core.domain ?? "notchat"}] ${text}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Dispatch event: "initialized"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("journal.initialized", {
|
||||
detail: { journal: window.journal },
|
||||
}),
|
||||
);
|
|
@ -0,0 +1,3 @@
|
|||
"use strict";
|
||||
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
try {
|
||||
navigator.serviceWorker
|
||||
.register("cache.js")
|
||||
.then((cache) => {
|
||||
// Registered
|
||||
journal.write("ServiceWorker registered: " + cache.scope);
|
||||
})
|
||||
.catch((error) => {
|
||||
// Not registered
|
||||
journal.write("ServiceWorker not registered: " + error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`pizda ${error}`);
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
"use strict";
|
||||
|
||||
if (typeof window.text !== "function") {
|
||||
// Not initialized
|
||||
|
||||
// Initialize of the class in global namespace
|
||||
window.text = class text {
|
||||
/**
|
||||
* Language
|
||||
*/
|
||||
static language = {
|
||||
/**
|
||||
* Select a language
|
||||
*
|
||||
* @param {string} language Name of language
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
select(language = 'english') {
|
||||
// Write to the local storage in browser
|
||||
localStorage.language = language;
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Dispatch event: "initialized"
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("text.initialized", {
|
||||
detail: { text: window.text },
|
||||
}),
|
||||
);
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"CHATS_SERVER_ERROR_LENGTH_MAX": "Server address longer than 512 characters"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"CHATS_SERVER_ERROR_LENGTH_MAX": "Адрес сервера длиннее 512-ти символов"
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
CACHE MANIFEST
|
||||
|
||||
CACHE:
|
||||
/themes/default/css/main.css
|
||||
/themes/default/css/fonts.css
|
||||
/themes/default/css/fonts/dejavu.css
|
||||
/themes/default/css/fonts/fira.css
|
||||
/themes/default/css/fonts/hack.css
|
||||
/themes/default/css/animations.css
|
||||
/themes/default/css/chats.css
|
||||
/themes/default/images/favicon.ico
|
||||
/themes/default/fonts/dejavu/DejaVuLGCSans-ExtraLight.ttf
|
||||
/themes/default/fonts/dejavu/DejaVuLGCSans.ttf
|
||||
/themes/default/fonts/dejavu/DejaVuLGCSans-Oblique.ttf
|
||||
/themes/default/fonts/dejavu/DejaVuLGCSans-Bold.ttf
|
||||
/themes/default/fonts/dejavu/DejaVuLGCSans-BoldOblique.ttf
|
||||
/themes/default/fonts/fira/FiraSans-Hair.woff2
|
||||
/themes/default/fonts/fira/FiraSans-HairItalic.woff2
|
||||
/themes/default/fonts/fira/FiraSans-UltraLight.woff2
|
||||
/themes/default/fonts/fira/FiraSans-UltraLightItalic.woff2
|
||||
/themes/default/fonts/fira/FiraSans-Light.woff2
|
||||
/themes/default/fonts/fira/FiraSans-LightItalic.woff2
|
||||
/themes/default/fonts/fira/FiraSans-Regular.woff2
|
||||
/themes/default/fonts/fira/FiraSans-Italic.woff2
|
||||
/themes/default/fonts/fira/FiraMono-Medium.woff2
|
||||
/themes/default/fonts/fira/FiraSans-MediumItalic.woff2
|
||||
/themes/default/fonts/fira/FiraSans-SemiBold.woff2
|
||||
/themes/default/fonts/fira/FiraSans-SemiBoldItalic.woff2
|
||||
/themes/default/fonts/fira/FiraSans-Bold.woff2
|
||||
/themes/default/fonts/fira/FiraSans-BoldItalic.woff2
|
||||
/themes/default/fonts/fira/FiraSans-ExtraBold.woff2
|
||||
/themes/default/fonts/fira/FiraSans-ExtraBoldItalic.woff2
|
||||
/themes/default/fonts/fira/FiraSans-Heavy.woff2
|
||||
/themes/default/fonts/fira/FiraSans-HeavyItalic.woff2
|
||||
/themes/default/fonts/fira/FiraMono-Regular.woff2
|
||||
/themes/default/fonts/fira/FiraMono-Bold.woff2
|
||||
/themes/default/fonts/fira/FiraSans-Hair.woff
|
||||
/themes/default/fonts/fira/FiraSans-HairItalic.woff
|
||||
/themes/default/fonts/fira/FiraSans-UltraLight.woff
|
||||
/themes/default/fonts/fira/FiraSans-UltraLightItalic.woff
|
||||
/themes/default/fonts/fira/FiraSans-Light.woff
|
||||
/themes/default/fonts/fira/FiraSans-LightItalic.woff
|
||||
/themes/default/fonts/fira/FiraSans-Regular.woff
|
||||
/themes/default/fonts/fira/FiraSans-Italic.woff
|
||||
/themes/default/fonts/fira/FiraMono-Medium.woff
|
||||
/themes/default/fonts/fira/FiraSans-MediumItalic.woff
|
||||
/themes/default/fonts/fira/FiraSans-SemiBold.woff
|
||||
/themes/default/fonts/fira/FiraSans-SemiBoldItalic.woff
|
||||
/themes/default/fonts/fira/FiraSans-Bold.woff
|
||||
/themes/default/fonts/fira/FiraSans-BoldItalic.woff
|
||||
/themes/default/fonts/fira/FiraSans-ExtraBold.woff
|
||||
/themes/default/fonts/fira/FiraSans-ExtraBoldItalic.woff
|
||||
/themes/default/fonts/fira/FiraSans-Heavy.woff
|
||||
/themes/default/fonts/fira/FiraSans-HeavyItalic.woff
|
||||
/themes/default/fonts/fira/FiraMono-Regular.woff
|
||||
/themes/default/fonts/fira/FiraMono-Bold.woff
|
||||
/themes/default/fonts/hack/hack-regular.woff2?sha=3114f1256
|
||||
/themes/default/fonts/hack/hack-bold.woff2?sha=3114f1256
|
||||
/themes/default/fonts/hack/hack-italic.woff2?sha=3114f1256
|
||||
/themes/default/fonts/hack/hack-bolditalic.woff2?sha=3114f1256
|
||||
/themes/default/fonts/hack/hack-regular.woff?sha=3114f1256
|
||||
/themes/default/fonts/hack/hack-bold.woff?sha=3114f1256
|
||||
/themes/default/fonts/hack/hack-italic.woff?sha=3114f1256
|
||||
/themes/default/fonts/hack/hack-bolditalic.woff?sha=3114f1256
|
||||
/themes/default/icons/data.css
|
||||
/themes/default/images/ban.jpg
|
||||
/themes/default/images/offline.jpg
|
||||
/js/asdasd/libraries/noble-hashes.js
|
||||
/js/asdasd/asdasd.js
|
||||
/js/pages/chat.js
|
||||
/js/adaoter.js
|
||||
/js/cache.js
|
||||
/js/chats.js
|
||||
/js/core.js
|
||||
/js/damper.js
|
||||
/js/notchat.js
|
||||
/js/notifications.js
|
||||
/js/text.js
|
||||
/languages/english.json
|
||||
/languages/russian.json
|
||||
/
|
||||
|
||||
NETWORK:
|
||||
*
|
||||
|
||||
FALLBACK:
|
||||
/ /offline
|
|
@ -1,5 +1,9 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
:root {
|
||||
--animations: 1;
|
||||
}
|
||||
|
||||
@keyframes uprise {
|
||||
0% {
|
||||
opacity: 0;
|
||||
|
@ -13,16 +17,58 @@
|
|||
}
|
||||
|
||||
.animation.uprise {
|
||||
animation-duration: 0.1s;
|
||||
animation-duration: var(--animation-duration, 0.1s);
|
||||
animation-name: uprise;
|
||||
animation-fill-mode: forwards;
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
@keyframes slide-down {
|
||||
0% {
|
||||
transform: translate(0, -100%);
|
||||
clip-path: polygon(0% 100%, 100% 100%, 100% 200%, 0% 200%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(0, 0%);
|
||||
clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.animation.slide-down {
|
||||
animation-duration: var(--animation-duration, 0.2s);
|
||||
animation-name: slide-down;
|
||||
animation-fill-mode: forwards;
|
||||
animation-timing-function: cubic-bezier(0, 1, 1, 1);
|
||||
}
|
||||
|
||||
@keyframes slide-down-revert {
|
||||
0% {
|
||||
transform: translate(0, 0%);
|
||||
clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(0, -100%);
|
||||
clip-path: polygon(0% 100%, 100% 100%, 100% 200%, 0% 200%);
|
||||
}
|
||||
}
|
||||
|
||||
.animation.slide-down-revert {
|
||||
animation-duration: var(--animation-duration, 0.2s);
|
||||
animation-name: slide-down-revert;
|
||||
animation-fill-mode: forwards;
|
||||
animation-timing-function: cubic-bezier(1, 0, 1, 1);
|
||||
}
|
||||
|
||||
@keyframes marquee {
|
||||
0% { left: 0; }
|
||||
100% { left: -100%; }
|
||||
0% {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
left: -100%;
|
||||
}
|
||||
}
|
||||
|
||||
.animation.marquee {
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
section[data-section="menu"][data-menu="chats"] {
|
||||
position: relative;
|
||||
padding: unset;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
section[data-section="menu"][data-menu="chats"]>search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
section[data-section="menu"][data-menu="chats"]>search>label {
|
||||
z-index: 200;
|
||||
position: relative;
|
||||
height: 1.9rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: clip;
|
||||
color: var(--input-servers-text);
|
||||
}
|
||||
|
||||
section[data-section="menu"][data-menu="chats"]>search>label>input {
|
||||
border-radius: 5px;
|
||||
transition: 0.3s cubic-bezier(1, 0, 1, 1);
|
||||
}
|
||||
|
||||
section[data-section="menu"][data-menu="chats"]>search>label>input:placeholder-shown {
|
||||
/* blyaaaa */
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
section[data-section="menu"][data-menu="chats"]:not([data-menu-status="connecting"])>search>label:has(+ output>figure>figcaption:not(:empty) + pre:not(:empty))>input {
|
||||
border-radius: 5px 5px 0 0;
|
||||
transition: 0.3s cubic-bezier(0, 1, 1, 1);
|
||||
}
|
||||
|
||||
section[data-section="menu"][data-menu="chats"]>search>label>i:first-child:first-of-type {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
section[data-section="menu"][data-menu="chats"]>search>label>i:first-child:first-of-type+input:first-of-type {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
font-weight: 500;
|
||||
color: var(--input-servers-text);
|
||||
background-color: var(--input-servers-background);
|
||||
}
|
||||
|
||||
section[data-section="menu"][data-menu="chats"]>search>output {
|
||||
--max-height: 120px;
|
||||
z-index: 100;
|
||||
max-height: var(--max-height, 120px);
|
||||
overflow: hidden;
|
||||
border-radius: 0 0 5px 5px;
|
||||
background-color: var(--important);
|
||||
transition: .1s ease-in;
|
||||
}
|
||||
|
||||
section[data-section="menu"][data-menu="chats"]>search>output>figure {
|
||||
margin: 13px 17px;
|
||||
}
|
||||
|
||||
section[data-section="menu"][data-menu="chats"]>search>label+output>figure:has(> figcaption:empty + pre:empty) {
|
||||
margin: 0 17px;
|
||||
}
|
||||
|
||||
section[data-section="menu"][data-menu="chats"]>search>output>figure>pre[data-server="description"] {
|
||||
margin: unset;
|
||||
}
|
||||
|
||||
/* section[data-section="menu"][data-menu="chats"]>div#chats:empty {
|
||||
display: none;
|
||||
}
|
||||
*/
|
||||
section[data-section="menu"][data-menu="chats"]>search>img[data-server="image"] {
|
||||
z-index: 50;
|
||||
position: absolute;
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
/* section[data-section="menu"][data-menu="chats"]:not(:is([data-menu-status="connected"], [data-menu-status="disconnected"]))>search>img[data-server="image"] {
|
||||
opacity: 0;
|
||||
} */
|
|
@ -0,0 +1,38 @@
|
|||
section.hotline {
|
||||
display: inline-flex;
|
||||
height: calc(100% - 20px);
|
||||
padding: 10px 0px;
|
||||
transition: unset;
|
||||
/* gap нельзя */
|
||||
}
|
||||
|
||||
section.hotline * {
|
||||
transition: unset;
|
||||
}
|
||||
|
||||
|
||||
section.hotline:last-child {
|
||||
margin-bottom: unset;
|
||||
}
|
||||
|
||||
section.hotline>article {
|
||||
position: relative;
|
||||
margin-right: 18px;
|
||||
width: calc(140px - var(--padding, 0px) * 2);
|
||||
height: calc(180px - var(--padding, 0px) * 2);
|
||||
padding: var(--padding, 0px);
|
||||
display: flex;
|
||||
align-self: flex-end;
|
||||
overflow: clip;
|
||||
border-radius: 3px;
|
||||
background-color: var(--blue-dark);
|
||||
/* box-shadow: 0px -6px 6px rgba(0, 0, 0, 0.3); */
|
||||
}
|
||||
|
||||
section.hotline>article:last-child {
|
||||
margin-right: unset;
|
||||
}
|
||||
|
||||
section.hotline>article>* {
|
||||
margin: auto;
|
||||
}
|
|
@ -11,6 +11,7 @@
|
|||
--red: #4d2d2d;
|
||||
--green: #415e53;
|
||||
--blue: #243b4f;
|
||||
--blue-dark: #09262d;
|
||||
|
||||
/* --input-servers: #898c25; очень крутой зелёно говняный цвет */
|
||||
--input-servers-text: #083932;
|
||||
|
@ -29,14 +30,13 @@
|
|||
|
||||
body {
|
||||
--row-aside: 200px;
|
||||
--row-settings: 100px;
|
||||
--gap: 16px;
|
||||
margin: 0;
|
||||
width: auto;
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: [header] 220px [settings] 320px [main] auto [footer] 180px;
|
||||
grid-template-rows: [aside] var(--row-aside, 200px) [settings] var(--row-settings, 100px) [main] calc(100vh - var(--row-aside) - var(--gap) - var(--row-settings) - var(--gap)) auto;
|
||||
grid-template-rows: [aside] var(--row-aside, 200px) [main] calc(100vh - var(--row-aside));
|
||||
gap: var(--gap, 16px);
|
||||
padding: 0;
|
||||
overflow-x: scroll;
|
||||
|
@ -63,101 +63,33 @@ header>section[data-section="window"] {
|
|||
|
||||
header>section[data-section="main"] {
|
||||
z-index: 1200;
|
||||
grid-row: settings / -1;
|
||||
display: grid;
|
||||
grid-row: main;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--envelope);
|
||||
}
|
||||
|
||||
section[data-section="servers"] {
|
||||
aside {
|
||||
z-index: 250;
|
||||
grid-column: 1 / -1;
|
||||
grid-row: aside;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
section[data-section="menu"] {
|
||||
z-index: 300;
|
||||
padding: 14px 15px;
|
||||
grid-row: settings;
|
||||
grid-row: main;
|
||||
grid-column: settings;
|
||||
border-radius: 5px;
|
||||
background-color: var(--important);
|
||||
}
|
||||
|
||||
section[data-section="servers"]>search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
section[data-section="servers"]>search>label {
|
||||
position: relative;
|
||||
height: 1.9rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--input-servers-text);
|
||||
}
|
||||
|
||||
section[data-section="servers"]>search>label>i:first-child:first-of-type {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
section[data-section="servers"]>search>label>i:first-child:first-of-type+input:first-of-type {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
font-weight: 500;
|
||||
border-radius: 3px;
|
||||
color: var(--input-servers-text);
|
||||
background-color: var(--input-servers-background);
|
||||
}
|
||||
|
||||
section[data-section="chats"] {
|
||||
z-index: 200;
|
||||
margin-bottom: var(--gap);
|
||||
grid-row: main;
|
||||
background-color: var(--section);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
aside {
|
||||
z-index: 300;
|
||||
grid-column: 1 / -1;
|
||||
grid-row: aside;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
main {
|
||||
--sections-default: 1;
|
||||
--sections-width: 520px;
|
||||
z-index: 100;
|
||||
margin-bottom: var(--gap);
|
||||
grid-row: settings / -1;
|
||||
grid-column: main;
|
||||
display: grid;
|
||||
grid-template-rows: [settings] var(--row-settings, 100px) [main] auto;
|
||||
grid-template-columns: repeat(var(--sections, var(--sections-default, 1)), [chat] var(--sections-width, 520px));
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--gap, 16px);
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
main>section {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: var(--sections-width, 480px);
|
||||
grid-column: var(--position, 1);
|
||||
grid-row: main;
|
||||
overflow-x: crop;
|
||||
overflow-y: scroll;
|
||||
background-color: var(--section);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
aside {
|
||||
z-index: 300;
|
||||
grid-column: 1 / -1;
|
||||
grid-row: aside;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
main {
|
||||
--sections-default: 1;
|
||||
--sections-width: 480px;
|
||||
z-index: 100;
|
||||
grid-row: settings / -1;
|
||||
grid-row: main;
|
||||
grid-column: main;
|
||||
display: grid;
|
||||
grid-template-rows: [settings] var(--row-settings, 100px) [main] auto;
|
||||
|
@ -177,10 +109,6 @@ main>section {
|
|||
background-color: var(--section);
|
||||
}
|
||||
|
||||
main>section[data-panel-type="settings"] {
|
||||
grid-row: settings;
|
||||
}
|
||||
|
||||
footer {
|
||||
z-index: 500;
|
||||
grid-column: footer;
|
||||
|
@ -196,11 +124,16 @@ footer>section[data-section="window"] {
|
|||
|
||||
footer>section[data-section="main"] {
|
||||
z-index: 700;
|
||||
grid-row: settings / -1;
|
||||
display: grid;
|
||||
grid-row: main;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--envelope);
|
||||
}
|
||||
|
||||
footer>section[data-section="main"]>#language {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
:is(div, section).window {
|
||||
overflow: hidden;
|
||||
border-right: 1px solid;
|
||||
|
@ -217,3 +150,7 @@ footer>section[data-section="main"] {
|
|||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.untouchable {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
section.hotline > article.trash {
|
||||
--padding: 12px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
section.hotline > article.trash > h1 {
|
||||
z-index: 10;
|
||||
margin-top: 10px;
|
||||
flex-grow: 4;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
section.hotline > article.trash :is(p, b) {
|
||||
z-index: 10;
|
||||
flex-grow: 1;
|
||||
font-size: 0.6rem;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
section.hotline > article.trash small {
|
||||
z-index: 10;
|
||||
flex-grow: 1;
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
section.hotline > article.trash > img {
|
||||
z-index: 5;
|
||||
position: absolute;
|
||||
left: -5%;
|
||||
top: -5%;
|
||||
width: 110%;
|
||||
height: 110%;
|
||||
object-position: center;
|
||||
object-fit: cover;
|
||||
filter: blur(2px) saturate(30%) brightness(40%);
|
||||
transition: 0.2s ease-in;
|
||||
}
|
||||
|
||||
section.hotline > article.trash:is(:hover, :active) > img {
|
||||
left: -20%;
|
||||
top: -20%;
|
||||
width: 140%;
|
||||
height: 140%;
|
||||
filter: blur(3px) saturate(0) brightness(60%) contrast(150%);
|
||||
transition: 0.1s ease-out;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 149 KiB |
Binary file not shown.
After Width: | Height: | Size: 122 KiB |
|
@ -0,0 +1 @@
|
|||
*.log
|
|
@ -50,14 +50,14 @@ final class templater extends controller implements ArrayAccess
|
|||
* Render a HTML-document
|
||||
*
|
||||
* @param string $file Related path to a HTML-document
|
||||
* @param ?array $variables Registry of variables to push into registry of global variables
|
||||
* @param array $variables Registry of variables to push into registry of global variables
|
||||
*
|
||||
* @return ?string HTML-document
|
||||
*/
|
||||
public function render(string $file, ?array $variables = null): ?string
|
||||
public function render(string $file, array $variables = []): ?string
|
||||
{
|
||||
// Generation and exit (success)
|
||||
return $this->twig->render('themes' . DIRECTORY_SEPARATOR . $this->twig->getGlobals()['theme'] . DIRECTORY_SEPARATOR . $file, $this->twig->mergeGlobals($variables ?? []));
|
||||
return $this->twig->render('themes' . DIRECTORY_SEPARATOR . $this->twig->getGlobals()['theme'] . DIRECTORY_SEPARATOR . $file, $variables + $this->variables);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,10 +2,13 @@
|
|||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/chats.css" />
|
||||
<link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/data.css" />
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% block menu %}
|
||||
<section data-section="menu" data-menu="chats">
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html lang="ru">
|
||||
<html lang="ru" manifest="/manifest">
|
||||
|
||||
<head>
|
||||
{% use '/themes/default/head.html' with title as head_title, meta as head_meta, css as head_css %}
|
||||
|
|
|
@ -7,7 +7,14 @@
|
|||
|
||||
</div>
|
||||
<section data-section="main">
|
||||
|
||||
<select id="language" autocomplete="language" onselect="text.language.select(this.value)">
|
||||
{% for language in languages %}
|
||||
<option value="{{ language }}">{{ language }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<script>
|
||||
document.getElementById('language').value = localStorage.language ?? 'english';
|
||||
</script>
|
||||
</section>
|
||||
</footer>
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
{% block css %}
|
||||
<link type="text/css" rel="stylesheet" href="/themes/default/css/hotline.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% if hotline.id != empty %}
|
||||
<section id="{{ hotline.id }}" class="hotline unselectable" data-hotline="true" {% for name, value in hotline.parameters
|
||||
%} data-hotline-{{ name }}="{{value}}" {% endfor %} {% for name, value in hotline.attributes %} {{ name
|
||||
}}="{{value}}" {% endfor %}>
|
||||
{% for element in hotline.elements %}
|
||||
<{{element.tag??'article'}} {% for attribute, value in element.attributes %}{{ attribute }}="{{ value }}"{% endfor %}>{{ element.html|raw }}</{{element.tag??'article'}}>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script type="text/javascript" src="/js/hotline.js" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js_init %}
|
||||
<script>
|
||||
document.addEventListener('hotline.loaded', function (e) {
|
||||
// Запуск препроцессора бегущих строк
|
||||
e.detail.hotline.preprocessing();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,15 @@
|
|||
<h1>{{ title }}</h1>
|
||||
{{ main|raw }}
|
||||
{% if image is not null %}<img src="{{ image.src }}" alt="{{ image.alt }}">{% endif %}
|
||||
<script>
|
||||
// Initialization of the element
|
||||
const element = document.getElementById('{{ id }}');
|
||||
|
||||
if (element instanceof HTMLElement) {
|
||||
// Found the element
|
||||
|
||||
element.addEventListener('mouseenter', () => {
|
||||
console.log('{{ id }}');
|
||||
});
|
||||
}
|
||||
</script>
|
|
@ -2,22 +2,33 @@
|
|||
|
||||
{% use "/themes/default/header.html" with css as header_css, body as header, js as header_js %}
|
||||
{% use "/themes/default/footer.html" with css as footer_css, body as footer, js as footer_js %}
|
||||
{% use '/themes/default/hotline/index.html' with css as hotline_css, body as hotline_body, js as hotline_js, js_init as
|
||||
hotline_js_init %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ block('header_css') }}
|
||||
{{ block('footer_css') }}
|
||||
{{ block('hotline_css') }}
|
||||
<link type="text/css" rel="stylesheet" href="/themes/default/css/trash.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{{ block('header') }}
|
||||
<section data-section="chats">
|
||||
</section>
|
||||
<aside>
|
||||
{% block aside %}
|
||||
{{ block('hotline_body') }}
|
||||
{{ aside|raw }}
|
||||
{% endblock %}
|
||||
</aside>
|
||||
{% block menu %}
|
||||
{% if menu %}
|
||||
{{ menu|raw }}
|
||||
{% else %}
|
||||
<section data-section="menu" data-menu-type="chats">
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
<main style="--sections: {{ sections ?? 1 }}">
|
||||
{% block main %}
|
||||
{{ main|raw }}
|
||||
|
@ -30,4 +41,6 @@
|
|||
{{ parent() }}
|
||||
{{ block('footer_js') }}
|
||||
{{ block('header_js') }}
|
||||
{{ block('hotline_js') }}
|
||||
{{ block('hotline_js_init') }}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
{% block js %}
|
||||
<script src="/js/core.js" defer></script>
|
||||
<script src="/js/journal.js" defer></script>
|
||||
<script src="/js/damper.js" defer></script>
|
||||
<script src="/js/text.js" defer></script>
|
||||
<script src="/js/notifications.js" defer></script>
|
||||
<script src="/js/pages/index.js" defer></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<img
|
||||
style="z-index: 99999; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: none; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;"
|
||||
src="/themes/{{ theme }}/images/ban.jpg">
|
|
@ -0,0 +1,49 @@
|
|||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
z-index: 99999;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
z-index: 100000;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: max(8rem, 5vw);
|
||||
font-family: sans;
|
||||
color: #fff;
|
||||
<!-- backdrop-filter: contrast(500%) hue-rotate(var(--rotate)); -->
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.unselectable {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<img class="unselectable" src="/themes/{{ theme }}/images/offline.jpg">
|
||||
<h1 class="unselectable">{{ title ?? '🪦'}}</h1>
|
||||
|
||||
<script>
|
||||
const title = document.getElementsByTagName('h1')[0];
|
||||
|
||||
let rotate = 0;
|
||||
|
||||
setInterval(() => title.style.setProperty('--rotate', (rotate += 5) + 'deg'), 10)
|
||||
</script>
|
|
@ -1,23 +1,93 @@
|
|||
<section data-section="servers" class="animation uprise">
|
||||
<search>
|
||||
<label>
|
||||
<search class="animation uprise">
|
||||
<label class="{% if current %}active{% else %}empty{% endif %}">
|
||||
<i class="icon data"></i>
|
||||
<input class="" name="server" type="text" placeholder="Сервер" list="servers" {% if current and current.domain %}
|
||||
value="{{ current.domain }}" {% endif %}onkeypress="localStorage.server_domain = this.value"
|
||||
onselect="localStorage.server_domain = this.value" onchange="chats.server.select(this.value)" autofocus="true">
|
||||
<datalist id="servers">
|
||||
<input
|
||||
class=""
|
||||
name="server"
|
||||
type="text"
|
||||
placeholder="Сервер"
|
||||
list="servers"
|
||||
maxlength="120"
|
||||
spellcheck="false"
|
||||
{% if current and current.ip and current.port %} value="{{ current.ip }}:{{ current.port }}" {% endif %}
|
||||
onkeyup="chats.server.select(this.value)"
|
||||
onchange="chats.server.select(this.value, true)"
|
||||
{% if not current %} autofocus="true" {% endif %}
|
||||
>
|
||||
<!-- <datalist id="servers">
|
||||
{% for server in servers %}
|
||||
<option value="{{ server.domain }}">{{ server.domain }}{% if server.description %} {{
|
||||
server.description
|
||||
}}{% endif %}</option>
|
||||
<option value="{{ server.ip }}:{{ server.port }}">{{ server.domain }}: {{ server.ip }}:{{ server.port }}</option>
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
</datalist> -->
|
||||
</label>
|
||||
<output>
|
||||
<output class="animation slide-down" style="--animation-height: 64px">
|
||||
<figure>
|
||||
<figcaption data-server="ip">{% if current %}{{ current.ip }}:{{ current.port }}{% endif %}</figcaption>
|
||||
<span data-server="description" class="animation marquee">{{ current.description }}</span>
|
||||
<figcaption data-server="domain">{% if current %}{{ current.domain }}{% endif %}</figcaption>
|
||||
<pre data-server="description">{{ current.description }}</pre>
|
||||
</figure>
|
||||
</output>
|
||||
{% for image in current.images|slice(0,10)%}
|
||||
<style>
|
||||
section[data-section="menu"][data-menu="chats"]>search>img[data-server="image"]:nth-child({{ loop.index }}) {
|
||||
z-index: 5{{ loop.index }};
|
||||
}
|
||||
|
||||
section[data-section="menu"][data-menu="chats"][data-menu-status="connected"]>search>img[data-server="image"]:nth-of-type({{ loop.index }}):not(.animation) {
|
||||
{{ image.connected.css }}
|
||||
}
|
||||
|
||||
section[data-section="menu"][data-menu="chats"]:is([data-menu-status="disconnected"], [data-menu-status="connecting"])>search>img[data-server="image"]:nth-of-type({{ loop.index }}):not(.animation) {
|
||||
{{ image.disconnected.css }}
|
||||
}
|
||||
|
||||
section[data-section="menu"][data-menu="chats"]:not([data-menu-status="disconnected"], [data-menu-status="connected"])>search>img[data-server="image"]:nth-of-type({{ loop.index }}) {
|
||||
|
||||
}
|
||||
|
||||
{% if image.animations %}
|
||||
{% if image.animations.connected %}
|
||||
@keyframes server-image-{{ loop.index }}-connected {
|
||||
{{ image.animations.connected.keyframes }} /* нужен фильтр */
|
||||
}
|
||||
section[data-section="menu"][data-menu="chats"][data-menu-status="connected"]>search>img[data-server="image"]:nth-of-type({{ loop.index }}).animation {
|
||||
{{ image.animations.connected.css }}
|
||||
animation-name: server-image-{{ loop.index }}-connected;
|
||||
{% if image.animations.connected.duration %}animation-duration: {{ image.animations.connected.duration }}s;{% endif %}
|
||||
{% if image.animations.connected.fill.mode %}animation-fill-mode: {{ image.animations.connected.fill.mode }};{% endif %}
|
||||
{% if image.animations.connected.timing.function %}animation-timing-function: {{ image.animations.connected.timing.function }};{% endif %}
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{% if image.animations.disconnected %}
|
||||
@keyframes server-image-{{ loop.index }}-disconnected {
|
||||
{{ image.animations.disconnected.keyframes }} /* нужен фильтр */
|
||||
}
|
||||
section[data-section="menu"][data-menu="chats"]:is([data-menu-status="disconnected"], [data-menu-status="connecting"])>search>img[data-server="image"]:nth-of-type({{ loop.index }}).animation {
|
||||
{{ image.animations.disconnected.css }}
|
||||
animation-name: server-image-{{ loop.index }}-disconnected;
|
||||
{% if image.animations.disconnected.duration %}animation-duration: {{ image.animations.disconnected.duration }}s;{% endif %}
|
||||
{% if image.animations.disconnected.fill.mode %}animation-fill-mode: {{ image.animations.disconnected.fill.mode }};{% endif %}
|
||||
{% if image.animations.disconnected.timing.function %}animation-timing-function: {{ image.animations.disconnected.timing.function }};{% endif %}
|
||||
}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</style>
|
||||
{% if image.url %}
|
||||
<img
|
||||
data-server="image"
|
||||
class="unselectable untouchable animation"
|
||||
style="{% if image.style %} {{ image.style }}{% endif %}"
|
||||
src="{{ image.url }}"
|
||||
alt="{{ image.description }}"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
importance="high"
|
||||
referrerpolicy="no-referrer"
|
||||
data-nosnippet="true"
|
||||
{% if image.animations.connected.duration %}data-server-image-animations-connected-duration="{{ image.animations.connected.duration }}"{% endif %}
|
||||
{% if image.animations.disconnected.duration %}data-server-image-animations-disconnected-duration="{{ image.animations.disconnected.duration }}"{% endif %}
|
||||
ondragstart="return false"
|
||||
/>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</search>
|
||||
</section>
|
||||
|
|
Loading…
Reference in New Issue