solved #21, solved #18, solved #8, solved #6, solved #5, solved #4

This commit is contained in:
Arsen Mirzaev Tatyano-Muradovich 2024-03-26 17:40:11 +07:00
parent 8627411c8e
commit 28d550d53c
44 changed files with 2626 additions and 234 deletions

View File

@ -6,7 +6,10 @@ namespace mirzaev\notchat\controllers;
// Files of the project // Files of the project
use mirzaev\notchat\views\templater, 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 // Framework for PHP
use mirzaev\minimal\controller; use mirzaev\minimal\controller;
@ -30,7 +33,7 @@ class core extends controller
protected array $errors = []; protected array $errors = [];
/** /**
* Constructor of an instance * Constructor
* *
* @param bool $initialize Initialize a controller? * @param bool $initialize Initialize a controller?
* *
@ -44,12 +47,49 @@ class core extends controller
if ($initialize) { if ($initialize) {
// Initializing is requested // Initializing is requested
// Initializing of models core // Write to the log of connections
new models(); 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 // Initializing of preprocessor of views
$this->view = new templater(); $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}) default => isset($this->{$name})
}; };
} }
} }

View File

@ -6,7 +6,9 @@ namespace mirzaev\notchat\controllers;
// Files of the project // Files of the project
use mirzaev\notchat\controllers\core, use mirzaev\notchat\controllers\core,
mirzaev\notchat\models\server; mirzaev\notchat\models\dns,
mirzaev\notchat\models\server,
mirzaev\notchat\models\text;
/** /**
* Index controller * Index controller
@ -23,6 +25,62 @@ final class index extends core
*/ */
public function index(array $parameters = []): ?string 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) // Exit (success)
if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render('chats.html'); if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render('chats.html');
else if ($_SERVER['REQUEST_METHOD'] === 'POST') 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; 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 * Render the servers section
* *
@ -54,7 +212,17 @@ final class index extends core
// Generating the reponse // Generating the reponse
echo json_encode( 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 'errors' => null
] ]
); );

View File

@ -7,7 +7,13 @@ namespace mirzaev\notchat\controllers;
// Files of the project // Files of the project
use mirzaev\notchat\controllers\core, use mirzaev\notchat\controllers\core,
mirzaev\notchat\controllers\traits\errors, 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 * Server controller
@ -34,7 +40,7 @@ final class server extends core
// POST // POST
// Create a file with server data // 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 // Initializing a response headers
header('Content-Type: application/json'); header('Content-Type: application/json');
@ -74,11 +80,39 @@ final class server extends core
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// POST // POST
// Read a file with server data // Initializing of buffer of response
$server = json_decode(model::read(model::domain($parameters['server']), $this->errors), true, 8); $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 // 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 // Initializing a response headers
header('Content-Type: application/json'); header('Content-Type: application/json');
@ -90,8 +124,7 @@ final class server extends core
// Generating the reponse // Generating the reponse
echo json_encode( echo json_encode(
[ $return + [
'server' => $server,
'errors' => static::text($this->errors) 'errors' => static::text($this->errors)
] ]
); );

View File

@ -4,6 +4,9 @@ declare(strict_types=1);
namespace mirzaev\notchat\models; namespace mirzaev\notchat\models;
// Files of the project
use mirzaev\notchat\models\enumerations\log as type;
// Framework for PHP // Framework for PHP
use mirzaev\minimal\model; use mirzaev\minimal\model;
@ -24,7 +27,12 @@ class core extends model
final public const POSTFIX = ''; 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'; final public const STORAGE = '..' . DIRECTORY_SEPARATOR . 'storage';

View File

@ -4,15 +4,14 @@ declare(strict_types=1);
namespace mirzaev\notchat\models; namespace mirzaev\notchat\models;
// Framework for PHP // Files of the project
use mirzaev\minimal\model; use mirzaev\notchat\models\enumerations\log as type;
// Built-in libraries // Built-in libraries
use exception, use exception;
DirectoryIterator as parser;
/** /**
* Core of DNS registry * DNS registry
* *
* @package mirzaev\notchat\models * @package mirzaev\notchat\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
@ -41,9 +40,9 @@ class dns extends core
{ {
try { try {
// Open file with DNS records // 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 // Iterate over rows
// Initializing values of the server data // Initializing values of the server data
@ -52,27 +51,30 @@ class dns extends core
// Incrementing the line read counter // Incrementing the line read counter
++$line; ++$line;
if ($domain === $_domain || $ip === $_ip || $port === $_port) { if ($domain === $_domain || ($port && $ip === $_ip && $port === $_port) || (!$port && $ip === $_ip || $port === $_port)) {
// Server found // Server found (domain, ip, ip + port)
// Close file with DNS // Close file with DNS
fclose($dns); fclose($dns);
// Exit (success) // Exit (success)
return $record; return array_combine(['domain', 'ip', 'port'], $record);
} }
} }
// Close file with DNS // Close file with DNS
fclose($dns); fclose($dns);
} catch (exception $e) { } catch (exception $e) {
// Write to buffer of errors // Write to the buffer of errors
$errors[] = [ $errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
'line' => $e->getLine(), 'line' => $e->getLine(),
'stack' => $e->getTrace() '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) // Exit (fail)
@ -100,23 +102,23 @@ class dns extends core
// Initializing part the file buffer (rows before target) // Initializing part the file buffer (rows before target)
$after = []; $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 // Initializing the status that the DNS record has been found
$found = false; $found = false;
// Open file with DNS records if (file_exists(static::DNS) && filesize(static::DNS) > 0) {
$dns = fopen(static::DNS, 'r'); // 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 // Iterate over rows
// Initializing values of the server data // Initializing values of the server data
[$_domain] = explode(' ', $row); [$_domain] = explode(' ', $row);
// Writing the row to the file buffer (except the target record) // 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; else ${$found ? 'after' : 'before'}[] = $row;
} }
@ -134,7 +136,7 @@ class dns extends core
ftruncate($dns, 0); ftruncate($dns, 0);
// Write a new record to the DNS registry // 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 // Apply changes
fflush($dns); fflush($dns);
@ -142,14 +144,20 @@ class dns extends core
// Unlock file // Unlock file
flock($dns, LOCK_UN); 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) { } catch (exception $e) {
// Write to buffer of errors // Write to the buffer of errors
$errors[] = [ $errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
'line' => $e->getLine(), 'line' => $e->getLine(),
'stack' => $e->getTrace() '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 * Convert domain or IP-address to domain
* *
* @param string $server Domain or IP-address of the server * @param string $server Domain or IP-address of the server
* @param bool $strict Check for port compliance?
* @param array &$errors Buffer of errors * @param array &$errors Buffer of errors
* *
* @return string|null Domain of the server * @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 { try {
if (preg_match('/^(https:\/\/)?\d+\..*\d\/?$/', $server) === 1) { if (preg_match('/^(?:https:\/\/)?([\d\.]*)(?:$|:?(\d.*\d)?\/?$)/', $server, $matches) === 1) {
// IP-address // IP-address
// Initializing of parts of address
@[, $ip, $port] = $matches;
// Exit (success) // Exit (success)
return static::read(ip: $server, errors: $errors)['domain']; return static::read(ip: $ip, port: $strict ? $port : null, errors: $errors)['domain'] ?? null;
} else { } else {
// Domain (implied) // Domain (implied)
// Exit (success) // Exit (success)
return $server; return $server ?? null;
} }
} catch (exception $e) { } catch (exception $e) {
// Write to buffer of errors // Write to the buffer of errors
$errors[] = [ $errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
'line' => $e->getLine(), 'line' => $e->getLine(),
'stack' => $e->getTrace() '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) // Exit (fail)
@ -204,25 +219,31 @@ class dns extends core
public static function ip(string $server, &$errors = []): ?string public static function ip(string $server, &$errors = []): ?string
{ {
try { try {
if (preg_match('/^(https:\/\/)?\d+\..*\d\/?$/', $server) === 1) { if (preg_match('/^(?:https:\/\/)?(\d+\..*):?(\d.*\d)?\/?$/', $server, $matches) === 1) {
// IP-address // IP-address
// Initializing of parts of address
[, $ip, $port] = $matches;
// Exit (success) // Exit (success)
return $server; return $ip ?? null;
} else { } else {
// Domain (implied) // Domain (implied)
// Exit (success) // Exit (success)
return static::read(domain: $server, errors: $errors)['ip']; return static::read(domain: $server, errors: $errors)['ip'] ?? null;
} }
} catch (exception $e) { } catch (exception $e) {
// Write to buffer of errors // Write to the buffer of errors
$errors[] = [ $errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
'line' => $e->getLine(), 'line' => $e->getLine(),
'stack' => $e->getTrace() '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) // Exit (fail)

View File

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

View File

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

View File

@ -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()
];
}
}
}

View File

@ -4,15 +4,15 @@ declare(strict_types=1);
namespace mirzaev\notchat\models; namespace mirzaev\notchat\models;
// Framework for PHP // Files of the project
use mirzaev\minimal\model; use mirzaev\notchat\models\enumerations\log as type;
// Built-in libraries // Built-in libraries
use exception, use exception,
DirectoryIterator as parser; DirectoryIterator as parser;
/** /**
* Core of models * Server
* *
* @package mirzaev\notchat\models * @package mirzaev\notchat\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> * @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 public static function write(string $domain, string $json = '', array &$errors = []): void
{ {
try { try {
//
if (strlen($domain) > 32) throw new exception('Domain cannot be longer than 32 characters'); if (strlen($domain) > 32) throw new exception('Domain cannot be longer than 32 characters');
// Initializing of path to file // Initializing of path to file
@ -57,7 +58,7 @@ class server extends core
// File found // File found
// Open file with server data // Open file with server data
$file = fopen($path, "r"); $file = fopen($path, "c+");
// Read server data // Read server data
$old = json_decode(fread($file, filesize($path)), true, 8); $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 // The keys match or the file has not been updated for more than 3 days
// Open file with server data // Open file with server data
$file = fopen($path, "w"); $file = fopen($path, "c");
// Write server data // Write server data
fwrite($file, json_encode($new)); fwrite($file, json_encode($new));
@ -79,12 +80,15 @@ class server extends core
// Write DNS record // Write DNS record
dns::write(domain: $new['domain'], ip: $new['ip'], port: $new['port'], errors: $errors); 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 throw new exception('Public keys do not match');
} else { } else {
// File is not found // File is not found
// Open file with server data // Open file with server data
$file = fopen($path, "w"); $file = fopen($path, "c");
// Write server data // Write server data
fwrite($file, json_encode($new)); fwrite($file, json_encode($new));
@ -94,15 +98,21 @@ class server extends core
// Write DNS record // Write DNS record
dns::write(domain: $new['domain'], ip: $new['ip'], port: $new['port'], errors: $errors); 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) { } catch (exception $e) {
// Write to buffer of errors // Write to the buffer of errors
$errors[] = [ $errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
'line' => $e->getLine(), 'line' => $e->getLine(),
'stack' => $e->getTrace() '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 * Read JSON from file of server
* *
* @param string $domain Domain of the 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 * @param array &$errors Buffer of errors
* *
* @return string|null JSON with data of the server * @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 { try {
// Initializing of path to file // Initializing of path to file
$path = static::SERVERS . DIRECTORY_SEPARATOR . "$domain.json"; $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 // Open file with server data
$file = fopen($path, "r"); $file = fopen($path, 'c+');
// Read server data // Read server data
$server = fread($file, filesize($path)); $server = fread($file, filesize($path));
@ -133,14 +150,19 @@ class server extends core
// Exit (success) // Exit (success)
return $server; return $server;
}
}
} catch (exception $e) { } catch (exception $e) {
// Write to buffer of errors // Write to the buffer of errors
$errors[] = [ $errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
'line' => $e->getLine(), 'line' => $e->getLine(),
'stack' => $e->getTrace() '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) // Exit (fail)
@ -175,6 +197,8 @@ class server extends core
$skip = $page * $amount; $skip = $page * $amount;
foreach (new parser(static::SERVERS) as $file) { foreach (new parser(static::SERVERS) as $file) {
// Iterate through all files in a directory
// Skipping unnecessary files // Skipping unnecessary files
if (--$skip > $amount) continue; if (--$skip > $amount) continue;
@ -182,31 +206,39 @@ class server extends core
if ($file->isDot()) continue; if ($file->isDot()) continue;
if (time() - $file->getCTime() < $time && $file->isReadable()) { 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 if (($size = $file->getSize()) > 0) {
$server = $file->openFile('r'); // The file is not empty
// Open the file with server data
$server = $file->openFile('c+');
// Write server data to the output buffer // 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); unset($file);
} }
}
// Exit (success)
if (--$amount < 1) break; if (--$amount < 1) break;
} }
// Exit (success) // Exit (success)
return $buffer; return $buffer;
} catch (exception $e) { } catch (exception $e) {
// Write to buffer of errors // Write to the buffer of errors
$errors[] = [ $errors[] = [
'text' => $e->getMessage(), 'text' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
'line' => $e->getLine(), 'line' => $e->getLine(),
'stack' => $e->getTrace() '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) // Exit (fail)

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ define('STORAGE', realpath('..' . DIRECTORY_SEPARATOR . 'storage'));
define('INDEX', __DIR__); define('INDEX', __DIR__);
// Автозагрузка // Автозагрузка
require __DIR__ . DIRECTORY_SEPARATOR require INDEX . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR
. '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR
@ -33,10 +33,14 @@ require __DIR__ . DIRECTORY_SEPARATOR
$router = new router; $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/read/$server', 'server', 'read', 'POST');
$router->write('/server/write/$server', 'server', 'write', 'POST');
$router->write('/servers', 'index', 'servers', '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)); $core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false));

View File

@ -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);
}),
);
}),
);
}); */

View File

@ -10,27 +10,161 @@ if (typeof window.chats !== "function") {
*/ */
static server = { 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 * @return {void} Into the document will be generated and injected an HTML-element
*/ */
select(server = localStorage.server_ip ?? localStorage.server_domain) { select(
if (typeof server === "string" && server.length > 0) { server = localStorage.server_ip && localStorage.server_port
if (core.servers instanceof HTMLElement) { ? localStorage.server_ip + ":" + localStorage.server_port
core.request(`/server/read/${server}`).then((json) => { : 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 ( if (
json.errors !== null && typeof json.errors === "object" && json.errors !== null && typeof json.errors === "object" &&
json.errors.length > 0 json.errors.length > 0
) {} else { ) {
document.querySelector('figcaption[data-server="ip"]').innerText = `${json.ip}:${json.port}`; // Generating notifications with errors
document.querySelector('figcaption[data-server="description"]').innerText = `${json.description}`; // 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 * @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( core.request(
"/servers", "/servers",
typeof server === "string" && server.length > 0 typeof server === "string" && server.length > 0
? `server=${server}}` ? `server=${server}`
: "", : "",
).then((json) => { ).then((json) => {
if (core.servers instanceof HTMLElement) core.servers.remove(); if (core.servers instanceof HTMLElement) core.servers.remove();
if ( if (
json.errors !== null && typeof json.errors === "object" && json.errors !== null && typeof json.errors === "object" &&
json.errors.length > 0 json.errors.length > 0
) {} else { ) {
const element = document.createElement("div"); // Generating notifications with errors
core.header.after(element); 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; element.outerHTML = json.html;
core.servers = document.body.querySelector( core.menu = document.body.querySelector(
"section[data-section='servers']", "section[data-section='menu']",
); );
} }
}
}); });
}, },
@ -77,7 +226,11 @@ if (typeof window.chats !== "function") {
if ( if (
json.errors !== null && typeof json.errors === "object" && json.errors !== null && typeof json.errors === "object" &&
json.errors.length > 0 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 element = document.createElement("div");
const position = core.main.children.length; const position = core.main.children.length;
element.style.setProperty("--position", position); element.style.setProperty("--position", position);

View File

@ -5,8 +5,11 @@ if (typeof window.core !== "function") {
// Initialize of the class in global namespace // Initialize of the class in global namespace
window.core = class core { window.core = class core {
// Label for the <main> element // Domain
static main = document.body.getElementsByTagName('main')[0]; static domain = window.location.hostname;
// Animations are enabled?
static animations = getComputedStyle(document.body).getPropertyValue('--animations') === '1';
// Label for the <header> element // Label for the <header> element
static header = document.body.getElementsByTagName('header')[0]; static header = document.body.getElementsByTagName('header')[0];
@ -14,11 +17,11 @@ if (typeof window.core !== "function") {
// Label for the <aside> element // Label for the <aside> element
static aside = document.body.getElementsByTagName('aside')[0]; static aside = document.body.getElementsByTagName('aside')[0];
// Label for the "servers" element // Label for the "menu" element
static servers = document.body.querySelector("section[data-section='servers']"); static menu = document.body.querySelector("section[data-section='menu']");
// Label for the "chats" element // Label for the <main> element
static chats = document.body.querySelector("section[data-section='chats']"); static main = document.body.getElementsByTagName('main')[0];
// Label for the <footer> element // Label for the <footer> element
static footer = document.body.getElementsByTagName('footer')[0]; static footer = document.body.getElementsByTagName('footer')[0];

View File

@ -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 },
}),
);

View File

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

View File

@ -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 },
}),
);

View File

@ -0,0 +1,3 @@
"use strict";

View File

@ -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}`);
}

View File

@ -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 },
}),
);

View File

@ -0,0 +1,3 @@
{
"CHATS_SERVER_ERROR_LENGTH_MAX": "Server address longer than 512 characters"
}

View File

@ -0,0 +1,3 @@
{
"CHATS_SERVER_ERROR_LENGTH_MAX": "Адрес сервера длиннее 512-ти символов"
}

View File

@ -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

View File

@ -1,5 +1,9 @@
@charset "UTF-8"; @charset "UTF-8";
:root {
--animations: 1;
}
@keyframes uprise { @keyframes uprise {
0% { 0% {
opacity: 0; opacity: 0;
@ -13,16 +17,58 @@
} }
.animation.uprise { .animation.uprise {
animation-duration: 0.1s; animation-duration: var(--animation-duration, 0.1s);
animation-name: uprise; animation-name: uprise;
animation-fill-mode: forwards; animation-fill-mode: forwards;
animation-timing-function: ease-in; 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 { @keyframes marquee {
0% { left: 0; } 0% {
100% { left: -100%; } left: 0;
}
100% {
left: -100%;
}
} }
.animation.marquee { .animation.marquee {

View File

@ -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;
} */

View File

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

View File

@ -11,6 +11,7 @@
--red: #4d2d2d; --red: #4d2d2d;
--green: #415e53; --green: #415e53;
--blue: #243b4f; --blue: #243b4f;
--blue-dark: #09262d;
/* --input-servers: #898c25; очень крутой зелёно говняный цвет */ /* --input-servers: #898c25; очень крутой зелёно говняный цвет */
--input-servers-text: #083932; --input-servers-text: #083932;
@ -29,14 +30,13 @@
body { body {
--row-aside: 200px; --row-aside: 200px;
--row-settings: 100px;
--gap: 16px; --gap: 16px;
margin: 0; margin: 0;
width: auto; width: auto;
height: 100vh; height: 100vh;
display: grid; display: grid;
grid-template-columns: [header] 220px [settings] 320px [main] auto [footer] 180px; 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); gap: var(--gap, 16px);
padding: 0; padding: 0;
overflow-x: scroll; overflow-x: scroll;
@ -63,101 +63,33 @@ header>section[data-section="window"] {
header>section[data-section="main"] { header>section[data-section="main"] {
z-index: 1200; z-index: 1200;
grid-row: settings / -1; grid-row: main;
display: grid; display: flex;
flex-direction: column;
background-color: var(--envelope); background-color: var(--envelope);
} }
section[data-section="servers"] { aside {
z-index: 250; z-index: 250;
grid-column: 1 / -1;
grid-row: aside;
display: flex;
}
section[data-section="menu"] {
z-index: 300;
padding: 14px 15px; padding: 14px 15px;
grid-row: settings; grid-row: main;
grid-column: settings;
border-radius: 5px; border-radius: 5px;
background-color: var(--important); 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 { main {
--sections-default: 1; --sections-default: 1;
--sections-width: 480px; --sections-width: 480px;
z-index: 100; z-index: 100;
grid-row: settings / -1; grid-row: main;
grid-column: main; grid-column: main;
display: grid; display: grid;
grid-template-rows: [settings] var(--row-settings, 100px) [main] auto; grid-template-rows: [settings] var(--row-settings, 100px) [main] auto;
@ -177,10 +109,6 @@ main>section {
background-color: var(--section); background-color: var(--section);
} }
main>section[data-panel-type="settings"] {
grid-row: settings;
}
footer { footer {
z-index: 500; z-index: 500;
grid-column: footer; grid-column: footer;
@ -196,11 +124,16 @@ footer>section[data-section="window"] {
footer>section[data-section="main"] { footer>section[data-section="main"] {
z-index: 700; z-index: 700;
grid-row: settings / -1; grid-row: main;
display: grid; display: flex;
flex-direction: column;
background-color: var(--envelope); background-color: var(--envelope);
} }
footer>section[data-section="main"]>#language {
margin-top: auto;
}
:is(div, section).window { :is(div, section).window {
overflow: hidden; overflow: hidden;
border-right: 1px solid; border-right: 1px solid;
@ -217,3 +150,7 @@ footer>section[data-section="main"] {
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
} }
.untouchable {
pointer-events: none;
}

View File

@ -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

View File

@ -0,0 +1 @@
*.log

View File

@ -50,14 +50,14 @@ final class templater extends controller implements ArrayAccess
* Render a HTML-document * Render a HTML-document
* *
* @param string $file Related path to 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 * @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) // 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);
} }
/** /**

View File

@ -2,10 +2,13 @@
{% block css %} {% block css %}
{{ parent() }} {{ 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" /> <link type="text/css" rel="stylesheet" href="/themes/{{ theme }}/css/icons/data.css" />
{% endblock %} {% endblock %}
{% block main %} {% block menu %}
<section data-section="menu" data-menu="chats">
</section>
{% endblock %} {% endblock %}
{% block js %} {% block js %}

View File

@ -1,6 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru"> <html lang="ru" manifest="/manifest">
<head> <head>
{% use '/themes/default/head.html' with title as head_title, meta as head_meta, css as head_css %} {% use '/themes/default/head.html' with title as head_title, meta as head_meta, css as head_css %}

View File

@ -7,7 +7,14 @@
</div> </div>
<section data-section="main"> <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> </section>
</footer> </footer>
{% endblock %} {% endblock %}

View File

@ -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 %}

View File

@ -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>

View File

@ -2,22 +2,33 @@
{% use "/themes/default/header.html" with css as header_css, body as header, js as header_js %} {% 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/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 %} {% block css %}
{{ parent() }} {{ parent() }}
{{ block('header_css') }} {{ block('header_css') }}
{{ block('footer_css') }} {{ block('footer_css') }}
{{ block('hotline_css') }}
<link type="text/css" rel="stylesheet" href="/themes/default/css/trash.css">
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{{ block('header') }} {{ block('header') }}
<section data-section="chats">
</section>
<aside> <aside>
{% block aside %} {% block aside %}
{{ block('hotline_body') }}
{{ aside|raw }} {{ aside|raw }}
{% endblock %} {% endblock %}
</aside> </aside>
{% block menu %}
{% if menu %}
{{ menu|raw }}
{% else %}
<section data-section="menu" data-menu-type="chats">
</section>
{% endif %}
{% endblock %}
<main style="--sections: {{ sections ?? 1 }}"> <main style="--sections: {{ sections ?? 1 }}">
{% block main %} {% block main %}
{{ main|raw }} {{ main|raw }}
@ -30,4 +41,6 @@
{{ parent() }} {{ parent() }}
{{ block('footer_js') }} {{ block('footer_js') }}
{{ block('header_js') }} {{ block('header_js') }}
{{ block('hotline_js') }}
{{ block('hotline_js_init') }}
{% endblock %} {% endblock %}

View File

@ -1,3 +1,8 @@
{% block js %} {% block js %}
<script src="/js/core.js" defer></script> <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 %} {% endblock %}

View File

@ -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">

View File

@ -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>

View File

@ -1,23 +1,93 @@
<section data-section="servers" class="animation uprise"> <search class="animation uprise">
<search> <label class="{% if current %}active{% else %}empty{% endif %}">
<label>
<i class="icon data"></i> <i class="icon data"></i>
<input class="" name="server" type="text" placeholder="Сервер" list="servers" {% if current and current.domain %} <input
value="{{ current.domain }}" {% endif %}onkeypress="localStorage.server_domain = this.value" class=""
onselect="localStorage.server_domain = this.value" onchange="chats.server.select(this.value)" autofocus="true"> name="server"
<datalist id="servers"> 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 %} {% for server in servers %}
<option value="{{ server.domain }}">{{ server.domain }}{% if server.description %} {{ <option value="{{ server.ip }}:{{ server.port }}">{{ server.domain }}: {{ server.ip }}:{{ server.port }}</option>
server.description
}}{% endif %}</option>
{% endfor %} {% endfor %}
</datalist> </datalist> -->
</label> </label>
<output> <output class="animation slide-down" style="--animation-height: 64px">
<figure> <figure>
<figcaption data-server="ip">{% if current %}{{ current.ip }}:{{ current.port }}{% endif %}</figcaption> <figcaption data-server="domain">{% if current %}{{ current.domain }}{% endif %}</figcaption>
<span data-server="description" class="animation marquee">{{ current.description }}</span> <pre data-server="description">{{ current.description }}</pre>
</figure> </figure>
</output> </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> </search>
</section>