first init
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| !.gitignore | ||||
| composer.phar | ||||
| vendor | ||||
							
								
								
									
										11
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE | ||||
| Version 2, December 2004 | ||||
|  | ||||
| Copyright (C) 2004 Sam Hocevar <sam@hocevar.net> | ||||
|  | ||||
| Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. | ||||
|  | ||||
| DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE | ||||
| TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION | ||||
|  | ||||
|   0. You just DO WHAT THE FUCK YOU WANT TO. | ||||
							
								
								
									
										51
									
								
								composer.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,51 @@ | ||||
| { | ||||
|     "name": "mirzaev/site-virus", | ||||
|     "description": "Site with viruses and scary pictures", | ||||
|     "readme": "README.md", | ||||
|     "keywords": [ | ||||
|         "virus", | ||||
|         "download", | ||||
|         "Evil Alliance", | ||||
|         "imageboard", | ||||
|         "site" | ||||
|     ], | ||||
|     "type": "site", | ||||
|     "homepage": "https://git.mirzaev.sexy/mirzaev/site-virus", | ||||
|     "license": "WTFPL", | ||||
|     "authors": [ | ||||
|         { | ||||
|             "name": "Arsen Mirzaev Tatyano-Muradovich", | ||||
|             "email": "arsen@mirzaev.sexy", | ||||
|             "homepage": "https://mirzaev.sexy", | ||||
|             "role": "Programmer" | ||||
|         } | ||||
|     ], | ||||
|     "support": { | ||||
|         "docs": "https://git.mirzaev.sexy/mirzaev/site-virus/manual", | ||||
|         "issues": "https://git.mirzaev.sexy/mirzaev/site-virus/issues" | ||||
|     }, | ||||
|     "require": { | ||||
|         "php": "~8.1", | ||||
|         "ext-sodium": "~8.1", | ||||
|         "mirzaev/minimal": "^2.0.x-dev", | ||||
|         "mirzaev/accounts": "~1.2.x-dev", | ||||
|         "mirzaev/arangodb": "^1.0.0", | ||||
|         "mirzaev/vk": "^4.0", | ||||
|         "triagens/arangodb": "~3.9.x-dev", | ||||
|         "twig/twig": "^3.4", | ||||
|         "guzzlehttp/guzzle": "^7.5" | ||||
|     }, | ||||
|     "require-dev": { | ||||
|         "phpunit/phpunit": "~9.5" | ||||
|     }, | ||||
|     "autoload": { | ||||
|         "psr-4": { | ||||
|             "mirzaev\\site\\virus\\": "mirzaev/site/virus/system" | ||||
|         } | ||||
|     }, | ||||
|     "autoload-dev": { | ||||
|         "psr-4": { | ||||
|             "mirzaev\\site\\virus\\tests\\": "mirzaev/site/virus/tests" | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3031
									
								
								composer.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										13
									
								
								config
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | ||||
| [core] | ||||
| 	repositoryformatversion = 0 | ||||
| 	filemode = false | ||||
| 	bare = false | ||||
| 	logallrefupdates = true | ||||
| 	symlinks = false | ||||
| 	ignorecase = true | ||||
| [remote "origin"] | ||||
| 	url = https://git.mirzaev.sexy/mirzaev/site-virus | ||||
| 	fetch = +refs/heads/*:refs/remotes/origin/* | ||||
| [branch "stable"] | ||||
| 	remote = origin | ||||
| 	merge = refs/heads/stable | ||||
							
								
								
									
										147
									
								
								mirzaev/site/virus/system/controllers/account_controller.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,147 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace mirzaev\site\virus\controllers; | ||||
|  | ||||
| // Файлы проекта | ||||
| use mirzaev\site\virus\controllers\core; | ||||
| use mirzaev\site\virus\models\account_model as account; | ||||
| use mirzaev\site\virus\models\session_model as session; | ||||
| use mirzaev\site\virus\models\vk_model as vk; | ||||
|  | ||||
| // Библиотека для ArangoDB | ||||
| use ArangoDBClient\Document as _document; | ||||
| use SebastianBergmann\Type\ObjectType; | ||||
| use stdClass; | ||||
|  | ||||
| // Фреймворк для ВКонтакте | ||||
| use mirzaev\vk\core as api; | ||||
|  | ||||
| /** | ||||
|  * Контроллер аккаунтов | ||||
|  * | ||||
|  * @package mirzaev\site\virus\controllers | ||||
|  * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> | ||||
|  */ | ||||
| final class account_controller extends core | ||||
| { | ||||
|     /** | ||||
|      * Страница профиля | ||||
|      * | ||||
|      * @param array $parameters Параметры запроса | ||||
|      */ | ||||
|     public function index(array $parameters = []): ?string | ||||
|     { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Инициализация | ||||
|      * | ||||
|      * @param array $parameters Параметры запроса | ||||
|      */ | ||||
|     public function initialization(array $parameters = []): ?string | ||||
|     { | ||||
|         if ($this->variables['account'] instanceof _document) { | ||||
|             // Найден аккаунт | ||||
|  | ||||
|             if ($this->variables['vk'] instanceof _document) { | ||||
|                 // Найден аккаунт ВКонтакте | ||||
|  | ||||
|                 // Инициализация данных аккаунта ВКонтакте | ||||
|                 vk::parse($this->variables['vk'], $this->variables['errors']['vk']); | ||||
|             } | ||||
|  | ||||
|             // Запись кода ответа | ||||
|             http_response_code(200); | ||||
|  | ||||
|             return null; | ||||
|         } else { | ||||
|             // Не найден аккаунт | ||||
|  | ||||
|             // Запись кода ответа | ||||
|             http_response_code(401); | ||||
|  | ||||
|             // Запись заголовка ответа с ключом аккаунта | ||||
|             header('session: ' . $this->variables['session']->hash); | ||||
|  | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         // Запись кода ответа | ||||
|         http_response_code(500); | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Связь аккаунта с аккаунтом ВКонтакте | ||||
|      * | ||||
|      * @param array $parameters Параметры запроса | ||||
|      */ | ||||
|     public function connect(array $parameters = []): ?string | ||||
|     { | ||||
|         if ($this->variables['session']->hash === $parameters['state']) { | ||||
|             // Совпадает хеш сессии с полученным хешем из ответа ВКонтакте | ||||
|  | ||||
|             if (!empty($response = vk::key($parameters['code'], $this->variables['errors']['vk']))) { | ||||
|                 // Получены данные аккаунта ВКонтакте | ||||
|  | ||||
|                 if (($this->variables['vk'] = vk::initialization($response, $this->variables['errors']['vk'])) instanceof _document) { | ||||
|                     // Инициализирован аккаунт ВКонтакте | ||||
|  | ||||
|                     if (($this->variables['account'] = vk::account($this->variables['vk'])) instanceof _document) { | ||||
|                         // Найден аккаунт (существующий) | ||||
|  | ||||
|                         if (session::connect($this->variables['session'], $this->variables['account'], $this->variables['errors']['session'])) { | ||||
|                             // Связана сессия с аккаунтом | ||||
|                         } | ||||
|                     } else if (($this->variables['account'] = account::create($this->variables['errors']['account'])) instanceof _document) { | ||||
|                         // Найден аккаунт (создан новый) | ||||
|  | ||||
|                         if (session::connect($this->variables['session'], $this->variables['account'], $this->variables['errors']['session'])) { | ||||
|                             // Связана сессия с аккаунтом | ||||
|  | ||||
|                             if (account::connect($this->variables['account'], $this->variables['vk'], $this->variables['errors']['account'])) { | ||||
|                                 // Связан аккаунт с аккаунтом ВКонтакте | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     // Инициализация робота для аккаунта ВКонтакте | ||||
|                     $this->vk = api::init()->user(key: $this->variables['vk']->access['key']); | ||||
|  | ||||
|                     if ($this->variables['vk'] instanceof _document) { | ||||
|                         // Инициализирован робот для аккаунта ВКонтакте | ||||
|  | ||||
|                         // Инициализация данных аккаунта ВКонтакте | ||||
|                         $data = vk::parse($this->vk, $this->variables['errors']['vk']); | ||||
|                         var_dump($data); die; | ||||
|  | ||||
|                         if ($data instanceof stdClass) { | ||||
|                             // Получены данные ВКонтакте | ||||
|  | ||||
|                             // Запись в базу данных | ||||
|                             vk::update($this->variables['vk'], $data, $this->variables['errors']['vk']); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Генерация представления | ||||
|         return $this->view->render(DIRECTORY_SEPARATOR . 'account' . DIRECTORY_SEPARATOR . 'vk.html', $this->variables); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Генерация панели аккаунта | ||||
|      * | ||||
|      * @param array $parameters Параметры запроса | ||||
|      */ | ||||
|     public function panel(array $parameters = []): ?string | ||||
|     { | ||||
|         // Генерация представления | ||||
|         return $this->view->render(DIRECTORY_SEPARATOR . 'account' . DIRECTORY_SEPARATOR . 'panel.html', $this->variables); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										100
									
								
								mirzaev/site/virus/system/controllers/core.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,100 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace mirzaev\site\virus\controllers; | ||||
|  | ||||
| // Файлы проекта | ||||
| use mirzaev\site\virus\views\manager; | ||||
| use mirzaev\site\virus\models\core as models; | ||||
| use mirzaev\site\virus\models\account_model as account; | ||||
| use mirzaev\site\virus\models\session_model as session; | ||||
|  | ||||
| // Библиотека для ArangoDB | ||||
| use ArangoDBClient\Document as _document; | ||||
|  | ||||
| // Фреймворк PHP | ||||
| use mirzaev\minimal\controller; | ||||
|  | ||||
| // Фреймворк ВКонтакте | ||||
| use mirzaev\vk\core as vk; | ||||
| use mirzaev\vk\robots\user as robot; | ||||
|  | ||||
| /** | ||||
|  * Ядро контроллеров | ||||
|  * | ||||
|  * @package mirzaev\site\virus\controllers | ||||
|  * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> | ||||
|  */ | ||||
| class core extends controller | ||||
| { | ||||
|     /** | ||||
|      * Переменные окружения | ||||
|      */ | ||||
|     protected robot $vk; | ||||
|  | ||||
|     /** | ||||
|      * Переменные окружения | ||||
|      */ | ||||
|     protected array $variables = []; | ||||
|  | ||||
|     /** | ||||
|      * Конструктор | ||||
|      * | ||||
|      * @return void | ||||
|      */ | ||||
|     public function __construct() | ||||
|     { | ||||
|         parent::__construct(); | ||||
|  | ||||
|         // Инициализация ядра моделей (соединение с базой данных...) | ||||
|         new models(); | ||||
|  | ||||
|         // Инициализация журнала ошибок | ||||
|         $this->variables['errors'] = [ | ||||
|             'session' => [], | ||||
|             'account' => [], | ||||
|             'vk' => [] | ||||
|         ]; | ||||
|  | ||||
|         // Инициализация даты до которой будет активна сессия | ||||
|         $expires = time() + 604800; | ||||
|  | ||||
|         // Инициализация сессии (без журналирования) | ||||
|         $this->variables['session'] = session::initialization($_COOKIE["session"] ?? null, $expires) ?? header('Location: https://virus.mirzaev.sexy/error?code=500&text=Не+удалось+инициализировать+сессию'); | ||||
|  | ||||
|         if ($_COOKIE["session"] ?? null !== $this->variables['session']->hash) { | ||||
|             // Изменился хеш сессии (подразумевается, что сессия устарела) | ||||
|  | ||||
|             // Запись хеша новой сессии | ||||
|             setcookie('session', $this->variables['session']->hash, [ | ||||
|                 'expires' => $expires, | ||||
|                 'domain' => 'virus.mirzaev.sexy', | ||||
|                 'path' => '/', | ||||
|                 'secure' => true, | ||||
|                 'httponly' => true, | ||||
|                 'samesite' => 'strict' | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         // Инициализация аккаунта (без журналирования) | ||||
|         $this->variables['account'] = session::account($this->variables['session']); | ||||
|  | ||||
|         if ($this->variables['account'] instanceof _document) { | ||||
|             // Инициализирован аккаунт | ||||
|  | ||||
|             // Инициализация аккаунта ВКонтакте (без журналирования) | ||||
|             $this->variables['vk'] = account::vk($this->variables['account']); | ||||
|  | ||||
|             if ($this->variables['vk'] instanceof _document) { | ||||
|                 // Инициализирован аккаунт ВКонтакте | ||||
|  | ||||
|                 // Инициализация робота для аккаунта ВКонтакте | ||||
|                 $this->vk = vk::init()->user(key: $this->variables['vk']->access['key']); | ||||
|             } else unset($this->variables['account'], $this->variables['vk']); | ||||
|         } | ||||
|  | ||||
|         // Инициализация препроцессора представления | ||||
|         $this->view = new manager; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										44
									
								
								mirzaev/site/virus/system/controllers/error_controller.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,44 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace mirzaev\site\virus\controllers; | ||||
|  | ||||
| // Файлы проекта | ||||
| use mirzaev\site\virus\controllers\core; | ||||
|  | ||||
| /** | ||||
|  * Контроллер ошибок | ||||
|  * | ||||
|  * @package mirzaev\site\virus\controllers | ||||
|  * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> | ||||
|  */ | ||||
| final class error_controller extends core | ||||
| { | ||||
|     /** | ||||
|      * Страница с ошибкой | ||||
|      * | ||||
|      * @param array $parameters | ||||
|      */ | ||||
|     public function index(array $parameters = []): ?string | ||||
|     { | ||||
|         // Запись текста ошибки в переменную окружения | ||||
|         $this->variables['text'] = $parameters['text'] ?? null; | ||||
|  | ||||
|         if (isset($parameters['code'])) { | ||||
|             // Получен код ошибки | ||||
|  | ||||
|             // Запись кода ошибки в переменную окружения | ||||
|             $this->variables['code'] = $parameters['code']; | ||||
|  | ||||
|             // Запись кода ответа | ||||
|             http_response_code($parameters['code']); | ||||
|  | ||||
|             // Генерация представления | ||||
|             return $this->view->render(DIRECTORY_SEPARATOR . 'errors' . DIRECTORY_SEPARATOR . 'index.html', $this->variables); | ||||
|         } | ||||
|  | ||||
|         // Генерация представления | ||||
|         return $this->view->render(DIRECTORY_SEPARATOR . 'errors' . DIRECTORY_SEPARATOR . ($parameters['code'] ?? 'index') . '.html', $this->variables); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										82
									
								
								mirzaev/site/virus/system/controllers/hotline_controller.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,82 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace mirzaev\site\virus\controllers; | ||||
|  | ||||
| // Файлы проекта | ||||
| use mirzaev\site\virus\controllers\core; | ||||
|  | ||||
| /** | ||||
|  * Контроллер бегущей строки | ||||
|  * | ||||
|  * @package mirzaev\site\virus\controllers | ||||
|  * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> | ||||
|  */ | ||||
| final class hotline_controller extends core | ||||
| { | ||||
|     /** | ||||
|      * Страница с бегущей строкой | ||||
|      * | ||||
|      * Можно использовать совместно с элементом <iframe> для изоляции | ||||
|      * содержимого бегущей строки от поисковых роботов | ||||
|      * | ||||
|      * @param array $parameters | ||||
|      */ | ||||
|     public function index(array $parameters = []): ?string | ||||
|     { | ||||
|         // Инициализация элементов для генерации в головном элементе | ||||
|         $this->variables['head'] = [ | ||||
|             'title' => 'Бегущая строка', | ||||
|             'metas' => [ | ||||
|                 [ | ||||
|                     'attributes' => [ | ||||
|                         'name' => 'robots', | ||||
|                         'content' => 'nofollow' | ||||
|                     ] | ||||
|                 ] | ||||
|             ] | ||||
|         ]; | ||||
|  | ||||
|         // Инициализация бегущей строки | ||||
|         $this->variables['hotline'] = [ | ||||
|             'id' => $this->variables['request']['id'] ?? 'hotline' | ||||
|         ]; | ||||
|  | ||||
|         // Инициализация параметров бегущей строки | ||||
|         $this->variables['hotline']['parameters'] = [ | ||||
|             // 'step' => 2 | ||||
|         ]; | ||||
|  | ||||
|         // Инициализация аттрибутов бегущей строки | ||||
|         $this->variables['hotline']['attributes'] = [ | ||||
|  | ||||
|         ]; | ||||
|  | ||||
|         // Инициализация элементов бегущей строки | ||||
|         $this->variables['hotline']['elements'] = [ | ||||
|             ['content' => '1'], | ||||
|             [ | ||||
|                 'tag' => 'article', | ||||
|                 'content' => '2' | ||||
|             ], | ||||
|             ['content' => '3'], | ||||
|             ['content' => '4'], | ||||
|             ['content' => '5'], | ||||
|             ['content' => '6'], | ||||
|             ['content' => '7'], | ||||
|             ['content' => '8'], | ||||
|             ['content' => '9'], | ||||
|             ['content' => '10'], | ||||
|             ['content' => '11'], | ||||
|             ['content' => '12'], | ||||
|             ['content' => '13'], | ||||
|             ['content' => '14'], | ||||
|             ['content' => '15'] | ||||
|         ]; | ||||
|  | ||||
|         // Генерация представления | ||||
|         return $this->view->render(DIRECTORY_SEPARATOR . 'hotline' . DIRECTORY_SEPARATOR . 'index.html', $this->variables); | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										71
									
								
								mirzaev/site/virus/system/controllers/index_controller.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,71 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace mirzaev\site\virus\controllers; | ||||
|  | ||||
| // Файлы проекта | ||||
| use mirzaev\site\virus\controllers\core; | ||||
|  | ||||
| /** | ||||
|  * Контроллер основной страницы | ||||
|  * | ||||
|  * @package mirzaev\site\virus\controllers | ||||
|  * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> | ||||
|  */ | ||||
| final class index_controller extends core | ||||
| { | ||||
|     /** | ||||
|      * Главная страница | ||||
|      * | ||||
|      * @param array $parameters Параметры запроса | ||||
|      */ | ||||
|     public function index(array $parameters = []): ?string | ||||
|     { | ||||
|         // Инициализация загружаемых категорий | ||||
|         $this->variables['include'] = [ | ||||
|             'head' => ['self'], | ||||
|             'body' => ['self'] | ||||
|         ]; | ||||
|  | ||||
|         // Инициализация бегущей строки | ||||
|         $this->variables['hotline'] = [ | ||||
|             'id' => $this->variables['request']['id'] ?? 'hotline' | ||||
|         ]; | ||||
|  | ||||
|         // Инициализация параметров бегущей строки | ||||
|         $this->variables['hotline']['parameters'] = [ | ||||
|             // 'step' => 2 | ||||
|         ]; | ||||
|  | ||||
|         // Инициализация аттрибутов бегущей строки | ||||
|         $this->variables['hotline']['attributes'] = [ | ||||
|  | ||||
|         ]; | ||||
|  | ||||
|         // Инициализация элементов бегущей строки | ||||
|         $this->variables['hotline']['elements'] = [ | ||||
|             ['content' => '1'], | ||||
|             [ | ||||
|                 'tag' => 'article', | ||||
|                 'content' => '2' | ||||
|             ], | ||||
|             ['content' => '3'], | ||||
|             ['content' => '4'], | ||||
|             ['content' => '5'], | ||||
|             ['content' => '6'], | ||||
|             ['content' => '7'], | ||||
|             ['content' => '8'], | ||||
|             ['content' => '9'], | ||||
|             ['content' => '10'], | ||||
|             ['content' => '11'], | ||||
|             ['content' => '12'], | ||||
|             ['content' => '13'], | ||||
|             ['content' => '14'], | ||||
|             ['content' => '15'] | ||||
|         ]; | ||||
|  | ||||
|         // Генерация представления | ||||
|         return $this->view->render(DIRECTORY_SEPARATOR . 'index.html', $this->variables); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										169
									
								
								mirzaev/site/virus/system/models/account_model.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,169 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace mirzaev\site\virus\models; | ||||
|  | ||||
| // Файлы проекта | ||||
| use mirzaev\site\virus\models\vk_model as vk; | ||||
|  | ||||
| // Фреймворк ArangoDB | ||||
| use mirzaev\arangodb\collection, | ||||
|     mirzaev\arangodb\document; | ||||
|  | ||||
| // Библиотека для ArangoDB | ||||
| use ArangoDBClient\Document as _document; | ||||
|  | ||||
| // Встроенные библиотеки | ||||
| use exception; | ||||
|  | ||||
| /** | ||||
|  * Модель регистрации, аутентификации и авторизации | ||||
|  * | ||||
|  * @package mirzaev\site\virus\models | ||||
|  * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> | ||||
|  */ | ||||
| final class account_model extends core | ||||
| { | ||||
|     /** | ||||
|      * Коллекция | ||||
|      */ | ||||
|     public const COLLECTION = 'account'; | ||||
|  | ||||
|     /** | ||||
|      * Создать | ||||
|      * | ||||
|      * @param array &$errors Журнал ошибок | ||||
|      * | ||||
|      * @return ?_document Инстанция аккаунта, если удалось создать | ||||
|      */ | ||||
|     public static function create(array &$errors = []): ?_document | ||||
|     { | ||||
|         try { | ||||
|             if (collection::init(static::$db->session, self::COLLECTION)) { | ||||
|                 // Инициализирована коллекция | ||||
|  | ||||
|                 // Запись аккаунта в базу данных | ||||
|                 $_id = document::write(static::$db->session, self::COLLECTION); | ||||
|  | ||||
|                 if ($account = collection::search(static::$db->session, sprintf( | ||||
|                     <<<AQL | ||||
|                             FOR d IN %s | ||||
|                             FILTER d._id == '$_id' | ||||
|                             RETURN d | ||||
|                         AQL, | ||||
|                     self::COLLECTION | ||||
|                 ))) { | ||||
|                     // Найден созданный аккаунт | ||||
|  | ||||
|                     return $account; | ||||
|                 } else throw new exception('Не удалось создать аккаунт'); | ||||
|             } else throw new exception('Не удалось инициализировать коллекцию'); | ||||
|         } catch (exception $e) { | ||||
|             // Запись в журнал ошибок | ||||
|             $errors[] = [ | ||||
|                 'text' => $e->getMessage(), | ||||
|                 'file' => $e->getFile(), | ||||
|                 'line' => $e->getLine(), | ||||
|                 'stack' => $e->getTrace() | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Связь аккаунта с аккаунтом ВКонтакте | ||||
|      * | ||||
|      * @param _document $account Инстанция аккаунта | ||||
|      * @param _document $vk Инстанция аккаунта ВКонтакте | ||||
|      * @param array &$errors Журнал ошибок | ||||
|      * | ||||
|      * @return bool Статус выполнения | ||||
|      */ | ||||
|     public static function connect(_document $account, _document $vk, array &$errors = []): bool | ||||
|     { | ||||
|         try { | ||||
|             if ( | ||||
|                 collection::init(static::$db->session, self::COLLECTION) | ||||
|                 && collection::init(static::$db->session, vk::COLLECTION) | ||||
|                 && collection::init(static::$db->session, self::COLLECTION . '_edge_' . vk::COLLECTION, true) | ||||
|             ) { | ||||
|                 // Инициализированы коллекции | ||||
|  | ||||
|                 if (document::write(static::$db->session, self::COLLECTION . '_edge_' . vk::COLLECTION, [ | ||||
|                     '_from' => $account->getId(), | ||||
|                     '_to' => $vk->getId() | ||||
|                 ])) { | ||||
|                     // Создано ребро: account -> vk | ||||
|  | ||||
|                     return true; | ||||
|                 } else throw new exception('Не удалось создать ребро: account -> vk'); | ||||
|             } else throw new exception('Не удалось инициализировать коллекцию'); | ||||
|         } catch (exception $e) { | ||||
|             // Запись в журнал ошибок | ||||
|             $errors[] = [ | ||||
|                 'text' => $e->getMessage(), | ||||
|                 'file' => $e->getFile(), | ||||
|                 'line' => $e->getLine(), | ||||
|                 'stack' => $e->getTrace() | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Поиск связанного аккаунта ВКонтакте | ||||
|      * | ||||
|      * @param _document $account Инстанция аккаунта | ||||
|      * @param array &$errors Журнал ошибок | ||||
|      * | ||||
|      * @return ?_document Инстанция аккаунта, если удалось найти | ||||
|      */ | ||||
|     public static function vk(_document $account, array &$errors = []): ?_document | ||||
|     { | ||||
|         try { | ||||
|             if ( | ||||
|                 collection::init(static::$db->session, self::COLLECTION) | ||||
|                 && collection::init(static::$db->session, vk::COLLECTION) | ||||
|                 && collection::init(static::$db->session, self::COLLECTION . '_edge_' . vk::COLLECTION, true) | ||||
|             ) { | ||||
|                 // Инициализирована коллекция | ||||
|  | ||||
|                 if ($vk = collection::search(static::$db->session, sprintf( | ||||
|                     <<<AQL | ||||
|                         FOR document IN %s | ||||
|                         LET edge = ( | ||||
|                             FOR edge IN %s | ||||
|                             FILTER edge._from == '%s' | ||||
|                             SORT edge._key DESC | ||||
|                             LIMIT 1 | ||||
|                             RETURN edge | ||||
|                         ) | ||||
|                         FILTER document._id == edge[0]._to | ||||
|                         LIMIT 1 | ||||
|                         RETURN document | ||||
|                     AQL, | ||||
|                     vk::COLLECTION, | ||||
|                     self::COLLECTION . '_edge_' . vk::COLLECTION, | ||||
|                     $account->getId() | ||||
|                 ))) { | ||||
|                     // Найден аккаунт ВКонтакте | ||||
|  | ||||
|                     return $vk; | ||||
|                 } else throw new exception('Не удалось найти аккаунт ВКонтакте'); | ||||
|             } else throw new exception('Не удалось инициализировать коллекцию'); | ||||
|         } catch (exception $e) { | ||||
|             // Запись в журнал ошибок | ||||
|             $errors[] = [ | ||||
|                 'text' => $e->getMessage(), | ||||
|                 'file' => $e->getFile(), | ||||
|                 'line' => $e->getLine(), | ||||
|                 'stack' => $e->getTrace() | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										143
									
								
								mirzaev/site/virus/system/models/core.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,143 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace mirzaev\site\virus\models; | ||||
|  | ||||
| use mirzaev\minimal\model; | ||||
|  | ||||
| use mirzaev\arangodb\connection; | ||||
|  | ||||
| use exception; | ||||
|  | ||||
| /** | ||||
|  * Ядро моделей | ||||
|  * | ||||
|  * @package mirzaev\site\virus\models | ||||
|  * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> | ||||
|  */ | ||||
| class core extends model | ||||
| { | ||||
|     /** | ||||
|      * Коллекция в которой хранятся аккаунты | ||||
|      */ | ||||
|     public const SETTINGS = '../settings/arangodb.php'; | ||||
|  | ||||
|     /** | ||||
|      * Соединение с базой данных | ||||
|      */ | ||||
|     protected static connection $db; | ||||
|  | ||||
|     public function __construct(connection $db = null) | ||||
|     { | ||||
|         if (isset($db)) { | ||||
|             // Получена инстанция соединения с базой данных | ||||
|  | ||||
|             // Запись и инициализация соединения с базой данных | ||||
|             $this->__set('db', $db); | ||||
|         } else { | ||||
|             // Не получена инстанция соединения с базой данных | ||||
|  | ||||
|             // Инициализация соединения с базой данных по умолчанию | ||||
|             $this->__get('db'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Записать свойство | ||||
|      * | ||||
|      * @param string $name Название | ||||
|      * @param mixed $value Значение | ||||
|      */ | ||||
|     public function __set(string $name, mixed $value = null): void | ||||
|     { | ||||
|         match ($name) { | ||||
|             'db' => (function () use ($value) { | ||||
|                 if ($this->__isset('db')) { | ||||
|                     // Свойство уже было инициализировано | ||||
|  | ||||
|                     // Выброс исключения (неудача) | ||||
|                     throw new exception('Запрещено реинициализировать соединение с базой данных ($this->db)', 500); | ||||
|                 } else { | ||||
|                     // Свойство ещё не было инициализировано | ||||
|  | ||||
|                     if ($value instanceof connection) { | ||||
|                         // Передано подходящее значение | ||||
|  | ||||
|                         // Запись свойства (успех) | ||||
|                         self::$db = $value; | ||||
|                     } else { | ||||
|                         // Передано неподходящее значение | ||||
|  | ||||
|                         // Выброс исключения (неудача) | ||||
|                         throw new exception('Соединение с базой данных ($this->db) должен быть инстанцией mirzaev\arangodb\connection', 500); | ||||
|                     } | ||||
|                 } | ||||
|             })(), | ||||
|             default => parent::__set($name, $value) | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Прочитать свойство | ||||
|      * | ||||
|      * @param string $name Название | ||||
|      * | ||||
|      * @return mixed Содержимое | ||||
|      */ | ||||
|     public function __get(string $name): mixed | ||||
|     { | ||||
|         return match ($name) { | ||||
|             'db' => (function () { | ||||
|                 if (!$this->__isset('db')) { | ||||
|                     // Свойство не инициализировано | ||||
|  | ||||
|                     // Инициализация значения по умолчанию исходя из настроек | ||||
|                     $this->__set('db', new connection(require static::SETTINGS)); | ||||
|                 } | ||||
|  | ||||
|                 return self::$db; | ||||
|             })(), | ||||
|             default => parent::__get($name) | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Проверить свойство на инициализированность | ||||
|      * | ||||
|      * @param string $name Название | ||||
|      */ | ||||
|     public function __isset(string $name): bool | ||||
|     { | ||||
|         return match ($name) { | ||||
|             default => parent::__isset($name) | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Удалить свойство | ||||
|      * | ||||
|      * @param string $name Название | ||||
|      */ | ||||
|     public function __unset(string $name): void | ||||
|     { | ||||
|         match ($name) { | ||||
|             default => parent::__isset($name) | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Статический вызов | ||||
|      * | ||||
|      * @param string $name Название | ||||
|      * @param array $arguments Параметры | ||||
|      */ | ||||
|     public static function __callStatic(string $name, array $arguments): mixed | ||||
|     { | ||||
|         match ($name) { | ||||
|             'db' => (new static)->__get('db'), | ||||
|             default => throw new exception("Не найдено свойство или функция: $name", 500) | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										213
									
								
								mirzaev/site/virus/system/models/session_model.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,213 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace mirzaev\site\virus\models; | ||||
|  | ||||
| // Файлы проекта | ||||
| use mirzaev\site\virus\models\account_model as account; | ||||
|  | ||||
| // Фреймворк ArangoDB | ||||
| use mirzaev\arangodb\collection, | ||||
|     mirzaev\arangodb\document; | ||||
|  | ||||
| // Библиотека для ArangoDB | ||||
| use ArangoDBClient\Document as _document; | ||||
|  | ||||
| // Встроенные библиотеки | ||||
| use exception; | ||||
|  | ||||
| /** | ||||
|  * Модель сессий | ||||
|  * | ||||
|  * @package mirzaev\site\virus\models | ||||
|  * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> | ||||
|  */ | ||||
| final class session_model extends core | ||||
| { | ||||
|     /** | ||||
|      * Коллекция | ||||
|      */ | ||||
|     public const COLLECTION = 'session'; | ||||
|  | ||||
|     /** | ||||
|      * Инициализация | ||||
|      * | ||||
|      * @param ?string $hash Хеш сессии в базе данных | ||||
|      * @param ?int $expires Дата окончания работы сессии (используется при создании новой сессии) | ||||
|      * @param array &$errors Журнал ошибок | ||||
|      * | ||||
|      * @return ?_document Инстанция сессии, если удалось найти или создать | ||||
|      */ | ||||
|     public static function initialization(?string $hash = null, ?int $expires = null, array &$errors = []): ?_document | ||||
|     { | ||||
|         try { | ||||
|             if (collection::init(static::$db->session, self::COLLECTION)) { | ||||
|                 // Инициализирована коллекция | ||||
|  | ||||
|                 if (isset($hash) && $session = collection::search(static::$db->session, sprintf( | ||||
|                     <<<AQL | ||||
|                         FOR d IN %s | ||||
|                         FILTER d.hash == '$hash' && d.expires > %d | ||||
|                         RETURN d | ||||
|                     AQL, | ||||
|                     self::COLLECTION, | ||||
|                     time() | ||||
|                 ))) { | ||||
|                     // Найдена сессия по хешу | ||||
|  | ||||
|                     // Возврат сессии | ||||
|                     return $session; | ||||
|                 } else if ($session = collection::search(static::$db->session, sprintf( | ||||
|                     <<<AQL | ||||
|                         FOR d IN %s | ||||
|                         FILTER d.ip == '%s' && d.expires > %d | ||||
|                         RETURN d | ||||
|                     AQL, | ||||
|                     self::COLLECTION, | ||||
|                     $_SERVER['REMOTE_ADDR'], | ||||
|                     time() | ||||
|                 ))) { | ||||
|                     // Найдена сессия по данным пользователя | ||||
|  | ||||
|                     // Возврат сессии | ||||
|                     return $session; | ||||
|                 } else { | ||||
|                     // Не найдена сессия | ||||
|  | ||||
|                     // Запись сессии в базу данных | ||||
|                     $_id = document::write(static::$db->session, self::COLLECTION, [ | ||||
|                         'ip' => $_SERVER['REMOTE_ADDR'], | ||||
|                         'expires' => $expires ?? time() + 604800 | ||||
|                     ]); | ||||
|  | ||||
|                     if ($session = collection::search(static::$db->session, sprintf( | ||||
|                         <<<AQL | ||||
|                             FOR d IN %s | ||||
|                             FILTER d._id == '$_id' && d.expires > %d | ||||
|                             RETURN d | ||||
|                         AQL, | ||||
|                         self::COLLECTION, | ||||
|                         time() | ||||
|                     ))) { | ||||
|                         // Найдена созданная сессия | ||||
|  | ||||
|                         // Запись хеша | ||||
|                         $session->hash = sodium_bin2hex(sodium_crypto_generichash($_id)); | ||||
|  | ||||
|                         if (document::update(static::$db->session, $session)) { | ||||
|                             // Записано обновление | ||||
|  | ||||
|                             return $session; | ||||
|                         } else throw new exception('Не удалось записать данные сессии'); | ||||
|                     } else throw new exception('Не удалось создать или найти созданную сессию'); | ||||
|                 } | ||||
|             } else throw new exception('Не удалось инициализировать коллекцию'); | ||||
|         } catch (exception $e) { | ||||
|             // Запись в журнал ошибок | ||||
|             $errors[] = [ | ||||
|                 'text' => $e->getMessage(), | ||||
|                 'file' => $e->getFile(), | ||||
|                 'line' => $e->getLine(), | ||||
|                 'stack' => $e->getTrace() | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Связь сессии с аккаунтом | ||||
|      * | ||||
|      * @param _document $session Инстанция сессии | ||||
|      * @param _document $account Инстанция аккаунта | ||||
|      * @param array &$errors Журнал ошибок | ||||
|      * | ||||
|      * @return bool Статус выполнения | ||||
|      */ | ||||
|     public static function connect(_document $session, _document $account, array &$errors = []): bool | ||||
|     { | ||||
|         try { | ||||
|             if ( | ||||
|                 collection::init(static::$db->session, self::COLLECTION) | ||||
|                 && collection::init(static::$db->session, account::COLLECTION) | ||||
|                 && collection::init(static::$db->session, self::COLLECTION . '_edge_' . account::COLLECTION, true) | ||||
|             ) { | ||||
|                 // Инициализирована коллекция | ||||
|  | ||||
|                 if (document::write(static::$db->session, self::COLLECTION . '_edge_' . account::COLLECTION, [ | ||||
|                     '_from' => $session->getId(), | ||||
|                     '_to' => $account->getId() | ||||
|                 ])) { | ||||
|                     // Создано ребро: session -> account | ||||
|  | ||||
|                     return true; | ||||
|                 } else throw new exception('Не удалось создать ребро: session -> account'); | ||||
|             } else throw new exception('Не удалось инициализировать коллекцию'); | ||||
|         } catch (exception $e) { | ||||
|             // Запись в журнал ошибок | ||||
|             $errors[] = [ | ||||
|                 'text' => $e->getMessage(), | ||||
|                 'file' => $e->getFile(), | ||||
|                 'line' => $e->getLine(), | ||||
|                 'stack' => $e->getTrace() | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Поиск связанного аккаунта | ||||
|      * | ||||
|      * @param _document $session Инстанция сессии | ||||
|      * @param array &$errors Журнал ошибок | ||||
|      * | ||||
|      * @return ?_document Инстанция аккаунта, если удалось найти | ||||
|      */ | ||||
|     public static function account(_document $session, array &$errors = []): ?_document | ||||
|     { | ||||
|         try { | ||||
|             if ( | ||||
|                 collection::init(static::$db->session, self::COLLECTION) | ||||
|                 && collection::init(static::$db->session, account::COLLECTION) | ||||
|                 && collection::init(static::$db->session, self::COLLECTION . '_edge_' . account::COLLECTION, true) | ||||
|             ) { | ||||
|                 // Инициализированы коллекции | ||||
|  | ||||
|                 if ($account = collection::search(static::$db->session, sprintf( | ||||
|                     <<<AQL | ||||
|                         FOR document IN %s | ||||
|                         LET edge = ( | ||||
|                             FOR edge IN %s | ||||
|                             FILTER edge._from == '%s' | ||||
|                             SORT edge._key DESC | ||||
|                             LIMIT 1 | ||||
|                             RETURN edge | ||||
|                         ) | ||||
|                         FILTER document._id == edge[0]._to | ||||
|                         LIMIT 1 | ||||
|                         RETURN document | ||||
|                     AQL, | ||||
|                     account::COLLECTION, | ||||
|                     self::COLLECTION . '_edge_' . account::COLLECTION, | ||||
|                     $session->getId() | ||||
|                 ))) { | ||||
|                     // Найден аккаунт | ||||
|  | ||||
|                     return $account; | ||||
|                 } else throw new exception('Не удалось найти аккаунт'); | ||||
|             } else throw new exception('Не удалось инициализировать коллекцию'); | ||||
|         } catch (exception $e) { | ||||
|             // Запись в журнал ошибок | ||||
|             $errors[] = [ | ||||
|                 'text' => $e->getMessage(), | ||||
|                 'file' => $e->getFile(), | ||||
|                 'line' => $e->getLine(), | ||||
|                 'stack' => $e->getTrace() | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										555
									
								
								mirzaev/site/virus/system/models/vk_model.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,555 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace mirzaev\site\virus\models; | ||||
|  | ||||
| // Файлы проекта | ||||
| use mirzaev\site\virus\models\account_model as account; | ||||
|  | ||||
| // Фреймворк ArangoDB | ||||
| use mirzaev\arangodb\collection, | ||||
|     mirzaev\arangodb\document; | ||||
|  | ||||
| // Фреймворк ВКонтакте | ||||
| use mirzaev\vk\robots\user as robot; | ||||
|  | ||||
| // Библиотека для ArangoDB | ||||
| use ArangoDBClient\Document as _document; | ||||
|  | ||||
| // Библиотека браузера | ||||
| use GuzzleHttp\Client as browser; | ||||
|  | ||||
| // Встроенные библиотеки | ||||
| use exception; | ||||
| use stdClass; | ||||
|  | ||||
| /** | ||||
|  * Модель аккаунта ВКонтакте | ||||
|  * | ||||
|  * @package mirzaev\site\virus\models | ||||
|  * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> | ||||
|  */ | ||||
| final class vk_model extends core | ||||
| { | ||||
|     /** | ||||
|      * Коллекция | ||||
|      */ | ||||
|     public const COLLECTION = 'vk'; | ||||
|  | ||||
|     /** | ||||
|      * Инициализация | ||||
|      * | ||||
|      * @param string $response Ответ сервера ВКонтакте с данными аккаунта | ||||
|      * @param array &$errors Журнал ошибок | ||||
|      * | ||||
|      * @return ?_document Инстанция аккаунта ВКонтакте, если удалось создать | ||||
|      */ | ||||
|     public static function initialization(string $response = '', array &$errors = []): ?_document | ||||
|     { | ||||
|         try { | ||||
|             if (collection::init(static::$db->session, self::COLLECTION)) { | ||||
|                 // Инициализирована коллекция | ||||
|  | ||||
|                 // Инициализация данных аккаунта ВКонтакте | ||||
|                 $data = json_decode($response); | ||||
|  | ||||
|                 if ($account = collection::search(static::$db->session, sprintf( | ||||
|                     <<<AQL | ||||
|                             FOR d IN %s | ||||
|                             FILTER d.id == $data->user_id | ||||
|                             RETURN d | ||||
|                         AQL, | ||||
|                     self::COLLECTION | ||||
|                 ))) { | ||||
|                     // Найден аккаунт ВКонтакте | ||||
|  | ||||
|                     return $account; | ||||
|                 } else { | ||||
|                     // Не найден аккаунт ВКонтакте | ||||
|  | ||||
|                     return self::create($response, $errors); | ||||
|                 } | ||||
|             } else throw new exception('Не удалось инициализировать коллекцию'); | ||||
|         } catch (exception $e) { | ||||
|             // Запись в журнал ошибок | ||||
|             $errors[] = [ | ||||
|                 'text' => $e->getMessage(), | ||||
|                 'file' => $e->getFile(), | ||||
|                 'line' => $e->getLine(), | ||||
|                 'stack' => $e->getTrace() | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Создание | ||||
|      * | ||||
|      * @param string $response Ответ сервера ВКонтакте с данными аккаунта | ||||
|      * @param array &$errors Журнал ошибок | ||||
|      * | ||||
|      * @return ?_document Инстанция аккаунта ВКонтакте, если удалось создать | ||||
|      */ | ||||
|     public static function create(string $response = '', array &$errors = []): ?_document | ||||
|     { | ||||
|         try { | ||||
|             if (collection::init(static::$db->session, self::COLLECTION)) { | ||||
|                 // Инициализирована коллекция | ||||
|  | ||||
|                 // Запись аккаунта в базу данных | ||||
|                 $_id = document::write(static::$db->session, self::COLLECTION); | ||||
|  | ||||
|                 if ($account = collection::search(static::$db->session, sprintf( | ||||
|                     <<<AQL | ||||
|                             FOR d IN %s | ||||
|                             FILTER d._id == '$_id' | ||||
|                             RETURN d | ||||
|                         AQL, | ||||
|                     self::COLLECTION | ||||
|                 ))) { | ||||
|                     // Найден созданный аккаунт ВКонтакте | ||||
|  | ||||
|                     if (document::update(static::$db->session, $account)) { | ||||
|                         // Записано обновление | ||||
|  | ||||
|                         // Запись данных об аккаунте ВКонтакте и возврат (bool) | ||||
|                         return self::update($account, json_decode($response), $errors); | ||||
|                     } | ||||
|                 } | ||||
|                 throw new exception('Не удалось создать аккаунт ВКонтакте'); | ||||
|             } else throw new exception('Не удалось инициализировать коллекцию'); | ||||
|         } catch (exception $e) { | ||||
|             // Запись в журнал ошибок | ||||
|             $errors[] = [ | ||||
|                 'text' => $e->getMessage(), | ||||
|                 'file' => $e->getFile(), | ||||
|                 'line' => $e->getLine(), | ||||
|                 'stack' => $e->getTrace() | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Запросить ключ | ||||
|      * | ||||
|      * @param string $code Код полученный от ВКонтакте | ||||
|      * @param array &$errors Журнал ошибок | ||||
|      * | ||||
|      * @return ?string Тело ответа, если получен код ответа 200 | ||||
|      */ | ||||
|     public static function key(string $code = '', array &$errors = []): ?string | ||||
|     { | ||||
|         try { | ||||
|             // Инициализация браузера | ||||
|             $browser = new browser(); | ||||
|  | ||||
|             // Запрос | ||||
|             $response = $browser->request('GET', "https://oauth.vk.com/access_token?client_id=51447080&client_secret=KYlk0nGELW0A9ds7NQi6&redirect_uri=https://virus.mirzaev.sexy/account/vk/connect&code=$code"); | ||||
|  | ||||
|             if ($response->getStatusCode() === 200) { | ||||
|                 // Ответ сервера: 200 | ||||
|  | ||||
|                 return (string) $response->getBody(); | ||||
|             } else throw new exception('Не удалось получить ключ ВКонтакте (' . $response->getStatusCode() . ')'); | ||||
|         } catch (exception $e) { | ||||
|             // Запись в журнал ошибок | ||||
|             $errors[] = [ | ||||
|                 'text' => $e->getMessage(), | ||||
|                 'file' => $e->getFile(), | ||||
|                 'line' => $e->getLine(), | ||||
|                 'stack' => $e->getTrace() | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Поиск связанного аккаунта | ||||
|      * | ||||
|      * @param _document $vk Инстанция аккаунта ВКонтакте | ||||
|      * @param array &$errors Журнал ошибок | ||||
|      * | ||||
|      * @return ?_document Инстанция аккаунта, если удалось найти | ||||
|      */ | ||||
|     public static function account(_document $vk, array &$errors = []): ?_document | ||||
|     { | ||||
|         try { | ||||
|             if ( | ||||
|                 collection::init(static::$db->session, self::COLLECTION) | ||||
|                 && collection::init(static::$db->session, account::COLLECTION) | ||||
|                 && collection::init(static::$db->session, account::COLLECTION . '_edge_' . self::COLLECTION, true) | ||||
|             ) { | ||||
|                 // Инициализированы коллекции | ||||
|  | ||||
|                 if ($account = collection::search(static::$db->session, sprintf( | ||||
|                     <<<AQL | ||||
|                         FOR document IN %s | ||||
|                         LET edge = ( | ||||
|                             FOR edge IN %s | ||||
|                             FILTER edge._to == '%s' | ||||
|                             SORT edge._key DESC | ||||
|                             LIMIT 1 | ||||
|                             RETURN edge | ||||
|                         ) | ||||
|                         FILTER document._id == edge[0]._from | ||||
|                         LIMIT 1 | ||||
|                         RETURN document | ||||
|                     AQL, | ||||
|                     account::COLLECTION, | ||||
|                     account::COLLECTION . '_edge_' . self::COLLECTION, | ||||
|                     $vk->getId() | ||||
|                 ))) { | ||||
|                     // Найден аккаунт | ||||
|  | ||||
|                     return $account; | ||||
|                 } else throw new exception('Не удалось найти аккаунт'); | ||||
|             } else throw new exception('Не удалось инициализировать коллекцию'); | ||||
|         } catch (exception $e) { | ||||
|             // Запись в журнал ошибок | ||||
|             $errors[] = [ | ||||
|                 'text' => $e->getMessage(), | ||||
|                 'file' => $e->getFile(), | ||||
|                 'line' => $e->getLine(), | ||||
|                 'stack' => $e->getTrace() | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Запрос данных аккаунта ВКонтакте с серверов ВКонтакте | ||||
|      * | ||||
|      * @param robot $vk Инстанция аккаунта ВКонтакте | ||||
|      * @param array &$errors Журнал ошибок | ||||
|      * | ||||
|      * @return ?stdClass Данные аккаунта ВКонтакте, если получены | ||||
|      */ | ||||
|     public static function parse(robot $vk, array &$errors = []): ?stdClass | ||||
|     { | ||||
|         try { | ||||
|             // Запрос к API-серверу ВКонтакте | ||||
|             $response = $vk->user->get(fields: [ | ||||
|                 'activities', | ||||
|                 'about', | ||||
|                 // 'blacklisted', | ||||
|                 // 'blacklisted_by_me', | ||||
|                 'books', | ||||
|                 'bdate', | ||||
|                 'can_be_invited_group', | ||||
|                 'can_post', | ||||
|                 'can_see_all_posts', | ||||
|                 'can_see_audio', | ||||
|                 'can_send_friend_request', | ||||
|                 'can_write_private_message', | ||||
|                 'career', | ||||
|                 'common_count', | ||||
|                 'connections', | ||||
|                 'contacts', | ||||
|                 'city', | ||||
|                 'country', | ||||
|                 'crop_photo', | ||||
|                 'domain', | ||||
|                 'education', | ||||
|                 'exports', | ||||
|                 'followers_count', | ||||
|                 'friend_status', | ||||
|                 'has_photo', | ||||
|                 'has_mobile', | ||||
|                 'home_town', | ||||
|                 'photo_50', | ||||
|                 'photo_100', | ||||
|                 'photo_200', | ||||
|                 'photo_200_orig', | ||||
|                 'photo_400_orig', | ||||
|                 'photo_max', | ||||
|                 'photo_max_orig', | ||||
|                 'sex', | ||||
|                 'site', | ||||
|                 'schools', | ||||
|                 'screen_name', | ||||
|                 'status', | ||||
|                 'verified', | ||||
|                 'games', | ||||
|                 'interests', | ||||
|                 'is_favorite', | ||||
|                 'is_friend', | ||||
|                 'is_hidden_from_feed', | ||||
|                 'last_seen', | ||||
|                 'maiden_name', | ||||
|                 'military', | ||||
|                 'movies', | ||||
|                 'music', | ||||
|                 'nickname', | ||||
|                 'occupation', | ||||
|                 'online', | ||||
|                 'personal', | ||||
|                 'photo_id', | ||||
|                 'quotes', | ||||
|                 'relation', | ||||
|                 'relatives', | ||||
|                 'timezone', | ||||
|                 'tv', | ||||
|                 'universities' | ||||
|             ])[0]; | ||||
|  | ||||
|             if (!empty($response)) { | ||||
|                 // Получен ответ | ||||
|  | ||||
|                 return $response; | ||||
|             } | ||||
|         } catch (exception $e) { | ||||
|             // Запись в журнал ошибок | ||||
|             $errors[] = [ | ||||
|                 'text' => $e->getMessage(), | ||||
|                 'file' => $e->getFile(), | ||||
|                 'line' => $e->getLine(), | ||||
|                 'stack' => $e->getTrace() | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Обновление данных аккаунта ВКонтакте | ||||
|      * | ||||
|      * Все файлы (аватар, например) будут скачаны на сервер | ||||
|      * | ||||
|      * @param _document $vk Инстанция аккаунта ВКонтакте | ||||
|      * @param stdClass $data Информация об аккаунте (self::parse() или json_decode()) | ||||
|      * @param array &$errors Журнал ошибок | ||||
|      * | ||||
|      * @return ?_document Инстанция аккаунта ВКонтакте, если удалось обновить | ||||
|      */ | ||||
|     public static function update(_document $vk, stdClass $data, array &$errors = []): ?_document | ||||
|     { | ||||
|         try { | ||||
|             if (collection::init(static::$db->session, self::COLLECTION)) { | ||||
|                 // Инициализирована коллекция | ||||
|  | ||||
|                 if (empty($vk->id) and isset($data->user_id) || isset($data->id)) { | ||||
|                     // Получен идентификатор | ||||
|  | ||||
|                     // Запись | ||||
|                     $vk->id = $data->user_id ?? $data->id; | ||||
|  | ||||
|                     // Удаление из списка необработанных | ||||
|                     unset($data->user_id, $data->id); | ||||
|                 } else if (empty($vk->id)) throw new exception('Не удалось найти идентификатор аккаунта ВКонтакте'); | ||||
|  | ||||
|                 if (isset($data->access_token, $data->expires_in)) { | ||||
|                     // Получен ключ | ||||
|  | ||||
|                     // Запись | ||||
|                     $vk->access = [ | ||||
|                         'key' => $data->access_token, | ||||
|                         'expires' => $data->expires_in | ||||
|                     ]; | ||||
|  | ||||
|                     // Удаление из списка необработанных | ||||
|                     unset($data->access_token, $data->expires_in); | ||||
|                 } | ||||
|  | ||||
|                 // Инициализация браузера | ||||
|                 $browser = new browser(); | ||||
|  | ||||
|                 // Инициализация директории с обложкой | ||||
|                 if (!file_exists($path = INDEX . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . $vk->id . DIRECTORY_SEPARATOR . 'cover' . DIRECTORY_SEPARATOR)) | ||||
|                     mkdir($path, 0775, true); | ||||
|  | ||||
|                 if (isset($data->photo_50)) { | ||||
|                     // Получено изображение 50x50 | ||||
|  | ||||
|                     if ($browser->get($data->photo_50, ['sink' => $file = "$path/50x50.jpg"])->getStatusCode() === 200) | ||||
|                         $vk->cover = | ||||
|                             ($vk->cover ?? []) + | ||||
|                             [ | ||||
|                                 '50x50' => ($vk->cover['50x50'] ?? []) + | ||||
|                                     [ | ||||
|                                         'source' => $data->photo_50, | ||||
|                                         'public' => "/storage/$vk->id/cover/50x50.jpg", | ||||
|                                         'local' => $file, | ||||
|                                     ] | ||||
|                             ]; | ||||
|                     else throw new exception('Не удалось получить изображение 50x50 с серверов ВКонтакте'); | ||||
|  | ||||
|                     // Удаление из списка необработанных | ||||
|                     unset($data->photo_50); | ||||
|                 } | ||||
|  | ||||
|                 // Инициализация директории с обложкой | ||||
|                 if (!file_exists($path = INDEX . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . $vk->id . DIRECTORY_SEPARATOR . 'cover' . DIRECTORY_SEPARATOR)) | ||||
|                     mkdir($path, 0775, true); | ||||
|  | ||||
|                 if (isset($data->photo_100)) { | ||||
|                     // Получено изображение 100x100 | ||||
|  | ||||
|                     if ($browser->get($data->photo_100, ['sink' => $file = "$path/100x100.jpg"])->getStatusCode() === 200) | ||||
|                         $vk->cover = | ||||
|                             ($vk->cover ?? []) + | ||||
|                             [ | ||||
|                                 '100x100' => ($vk->cover['100x100'] ?? []) + | ||||
|                                     [ | ||||
|                                         'source' => $data->photo_100, | ||||
|                                         'public' => "/storage/$vk->id/cover/100x100.jpg", | ||||
|                                         'local' => $file, | ||||
|                                     ] | ||||
|                             ]; | ||||
|                     else throw new exception('Не удалось получить изображение 100x100 с серверов ВКонтакте'); | ||||
|  | ||||
|                     // Удаление из списка необработанных | ||||
|                     unset($data->photo_100); | ||||
|                 } | ||||
|  | ||||
|                 // Инициализация директории с обложкой | ||||
|                 if (!file_exists($path = INDEX . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . $vk->id . DIRECTORY_SEPARATOR . 'cover' . DIRECTORY_SEPARATOR)) | ||||
|                     mkdir($path, 0775, true); | ||||
|  | ||||
|                 if (isset($data->photo_200)) { | ||||
|                     // Получено изображение 200x200 | ||||
|  | ||||
|                     if ($browser->get($data->photo_200, ['sink' => $file = "$path/200x200.jpg"])->getStatusCode() === 200) | ||||
|                         $vk->cover = | ||||
|                             ($vk->cover ?? []) + | ||||
|                             [ | ||||
|                                 '200x200' => ($vk->cover['200x200'] ?? []) + | ||||
|                                     [ | ||||
|                                         'source' => $data->photo_200, | ||||
|                                         'public' => "/storage/$vk->id/cover/200x200.jpg", | ||||
|                                         'local' => $file, | ||||
|                                     ] | ||||
|                             ]; | ||||
|                     else throw new exception('Не удалось получить изображение 200x200 с серверов ВКонтакте'); | ||||
|  | ||||
|                     // Удаление из списка необработанных | ||||
|                     unset($data->photo_200); | ||||
|                 } | ||||
|  | ||||
|                 if (isset($data->photo_200_orig)) { | ||||
|                     // Получено изображение 200x | ||||
|  | ||||
|                     if ($browser->get($data->photo_200_orig, ['sink' => $file = "$path/200x.jpg"])->getStatusCode() === 200) | ||||
|                         $vk->cover = | ||||
|                             ($vk->cover ?? []) + | ||||
|                             [ | ||||
|                                 '200x' => ($vk->cover['200x'] ?? []) + | ||||
|                                     [ | ||||
|                                         'source' => $data->photo_200_orig, | ||||
|                                         'public' => "/storage/$vk->id/cover/200x.jpg", | ||||
|                                         'local' => $file, | ||||
|                                     ] | ||||
|                             ]; | ||||
|                     else throw new exception('Не удалось получить изображение 200x с серверов ВКонтакте'); | ||||
|  | ||||
|                     // Удаление из списка необработанных | ||||
|                     unset($data->photo_200_orig); | ||||
|                 } | ||||
|  | ||||
|                 if (isset($data->photo_400_orig)) { | ||||
|                     // Получено изображение 400x | ||||
|  | ||||
|                     if ($browser->get($data->photo_400_orig, ['sink' => $file = "$path/400x.jpg"])->getStatusCode() === 200) | ||||
|                         $vk->cover = | ||||
|                             ($vk->cover ?? []) + | ||||
|                             [ | ||||
|                                 '400x' => ($vk->cover['400x'] ?? []) + | ||||
|                                     [ | ||||
|                                         'source' => $data->photo_400_orig, | ||||
|                                         'public' => "/storage/$vk->id/cover/400x.jpg", | ||||
|                                         'local' => $file, | ||||
|                                     ] | ||||
|                             ]; | ||||
|                     else throw new exception('Не удалось получить изображение 400x с серверов ВКонтакте'); | ||||
|  | ||||
|                     // Удаление из списка необработанных | ||||
|                     unset($data->photo_400_orig); | ||||
|                 } | ||||
|  | ||||
|                 if (isset($data->photo_max)) { | ||||
|                     // Получено изображение MAXxMAX | ||||
|  | ||||
|                     if ($browser->get($data->photo_max, ['sink' => $file = "$path/MAXxMAX.jpg"])->getStatusCode() === 200) | ||||
|                         $vk->cover = | ||||
|                             ($vk->cover ?? []) + | ||||
|                             [ | ||||
|                                 'MAXxMAX' => ($vk->cover['MAXxMAX'] ?? []) + | ||||
|                                     [ | ||||
|                                         'source' => $data->photo_max, | ||||
|                                         'public' => "/storage/$vk->id/cover/MAXxMAX.jpg", | ||||
|                                         'local' => $file, | ||||
|                                     ] | ||||
|                             ]; | ||||
|                     else throw new exception('Не удалось получить изображение MAXxMAX с серверов ВКонтакте'); | ||||
|  | ||||
|                     // Удаление из списка необработанных | ||||
|                     unset($data->photo_max); | ||||
|                 } | ||||
|  | ||||
|                 if (isset($data->photo_max_orig)) { | ||||
|                     // Получено изображение MAXx | ||||
|  | ||||
|                     if ($browser->get($data->photo_max_orig, ['sink' => $file = "$path/MAXx.jpg"])->getStatusCode() === 200) | ||||
|                         $vk->cover = | ||||
|                             ($vk->cover ?? []) + | ||||
|                             [ | ||||
|                                 'MAXx' => ($vk->cover['MAXx'] ?? []) + | ||||
|                                     [ | ||||
|                                         'source' => $data->photo_max_orig, | ||||
|                                         'public' => "/storage/$vk->id/cover/MAXx.jpg", | ||||
|                                         'local' => $file, | ||||
|                                     ] | ||||
|                             ]; | ||||
|                     else throw new exception('Не удалось получить изображение MAXx с серверов ВКонтакте'); | ||||
|  | ||||
|                     // Удаление из списка необработанных | ||||
|                     unset($data->photo_max_orig); | ||||
|                 } | ||||
|  | ||||
|                 if (isset($data->crop_photo)) { | ||||
|                     // Получено изображение MAXx | ||||
|  | ||||
|                     if ($browser->get($data->photo_max_orig, ['sink' => $file = "$path/MAXx.jpg"])->getStatusCode() === 200) | ||||
|                         $vk->cover = | ||||
|                             ($vk->cover ?? []) + | ||||
|                             [ | ||||
|                                 'MAXx' => ($vk->cover['MAXx'] ?? []) + | ||||
|                                     [ | ||||
|                                         'source' => $data->photo_max_orig, | ||||
|                                         'public' => "/storage/$vk->id/cover/MAXx.jpg", | ||||
|                                         'local' => $file, | ||||
|                                     ] | ||||
|                             ]; | ||||
|                     else throw new exception('Не удалось получить изображение MAXx с серверов ВКонтакте'); | ||||
|  | ||||
|                     // Удаление из списка необработанных | ||||
|                     unset($data->photo_max_orig); | ||||
|                 } | ||||
|  | ||||
|                 // Перебор оставшихся параметров | ||||
|                 foreach ($data as $key => $value) $vk->{$key} = $value; | ||||
|  | ||||
|                 if (document::update(static::$db->session, $vk)) { | ||||
|                     // Записано обновление | ||||
|  | ||||
|                     return $vk; | ||||
|                 } else throw new exception('Не удалось записать данные аккаунта ВКонтакте'); | ||||
|             } else throw new exception('Не удалось инициализировать коллекцию'); | ||||
|         } catch (exception $e) { | ||||
|             // Запись в журнал ошибок | ||||
|             $errors[] = [ | ||||
|                 'text' => $e->getMessage(), | ||||
|                 'file' => $e->getFile(), | ||||
|                 'line' => $e->getLine(), | ||||
|                 'stack' => $e->getTrace() | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										119
									
								
								mirzaev/site/virus/system/public/css/account.old.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,119 @@ | ||||
| #mail>:is(form, div) { | ||||
|     display       : flex; | ||||
|     flex-direction: column; | ||||
| } | ||||
|  | ||||
| #mail .exit { | ||||
|     margin-top: 25px; | ||||
| } | ||||
|  | ||||
| #mail p { | ||||
|     margin : 0; | ||||
|     display: flex; | ||||
| } | ||||
|  | ||||
| #mail p>span { | ||||
|     margin-left: auto; | ||||
| } | ||||
|  | ||||
| #mail>form>input:is([type=text], [type=password]) { | ||||
|     margin-bottom: 12px; | ||||
| } | ||||
|  | ||||
| #mail>form>input:last-child { | ||||
|     margin-bottom: unset; | ||||
| } | ||||
|  | ||||
| #mail>form>.submit { | ||||
|     margin-top   : 6px; | ||||
|     margin-bottom: 10px; | ||||
|     display      : flex; | ||||
| } | ||||
|  | ||||
| #mail>form>.submit>label { | ||||
|     padding      : 10px 20px; | ||||
|     border       : unset; | ||||
|     border-radius: 3px 0 0 3px; | ||||
| } | ||||
|  | ||||
| #mail>form>.submit>input { | ||||
|     padding      : 10px 20px; | ||||
|     flex-grow    : 1; | ||||
|     border       : unset; | ||||
|     border-radius: 0 3px 3px 0; | ||||
| } | ||||
|  | ||||
|  | ||||
| #mail>form>input[type=submit].registration { | ||||
|     padding         : 7px 20px; | ||||
|     background-color: #86781C; | ||||
| } | ||||
|  | ||||
| #mail>form>input[type=submit].registration:hover { | ||||
|     background-color: #9e8d20; | ||||
| } | ||||
|  | ||||
| #mail>form>input[type=submit].registration:is(:active, :focus) { | ||||
|     background-color: #776b19; | ||||
| } | ||||
|  | ||||
| #mail>form>ul.errors { | ||||
|     margin-top      : 18px; | ||||
|     margin-bottom   : 0px; | ||||
|     padding         : 10px; | ||||
|     text-align      : center; | ||||
|     list-style      : none; | ||||
|     background-color: #ae8f8f; | ||||
| } | ||||
|  | ||||
| #account { | ||||
|     display           : grid; | ||||
|     grid-template-rows: auto auto; | ||||
| } | ||||
|  | ||||
| #account>button#login { | ||||
|     z-index : 1500; | ||||
|     grid-row: 2; | ||||
| } | ||||
|  | ||||
| #account>section.tab { | ||||
|     z-index         : 1000; | ||||
|     position        : relative; | ||||
|     bottom          : -100%; | ||||
|     padding         : 1.5rem 10%; | ||||
|     display         : flex; | ||||
|     flex-direction  : column; | ||||
|     align-self      : end; | ||||
|     text-align      : center; | ||||
|     gap             : .8rem; | ||||
|     background-color: var(--background-light-2); | ||||
|     transition      : .2s ease-in; | ||||
| } | ||||
|  | ||||
| #account>button.active+section.tab { | ||||
|     bottom: 0%; | ||||
| } | ||||
|  | ||||
| #account>section.tab h3:first-of-type { | ||||
|     margin-bottom: 0.5rem; | ||||
| } | ||||
|  | ||||
| #account>section.tab>* { | ||||
|     margin: unset; | ||||
| } | ||||
|  | ||||
| #account>section#mail.tab { | ||||
|     grid-row          : 1; | ||||
|     display           : grid; | ||||
|     grid-template-rows: auto auto; | ||||
| } | ||||
|  | ||||
| #account>section#mail.tab>#profile { | ||||
|     grid-row          : 1; | ||||
|     display           : grid; | ||||
|     grid-template-rows: auto auto; | ||||
| } | ||||
|  | ||||
| #account>section#mail.tab>input[type=mail] { | ||||
|     grid-row          : 2; | ||||
| } | ||||
							
								
								
									
										52
									
								
								mirzaev/site/virus/system/public/css/books.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,52 @@ | ||||
| main>section#books { | ||||
|     display: flex; | ||||
|     flex-flow: row wrap; | ||||
| } | ||||
|  | ||||
| main>section#books>* { | ||||
|     margin-bottom: 20px; | ||||
| } | ||||
|  | ||||
| main>section#books>form.upload { | ||||
|     width: calc(100% / 3 - 20px - 9px * 2); | ||||
|     height: calc(220px - 9px * 2); | ||||
|     margin: 5px; | ||||
|     margin-right: 20px; | ||||
| } | ||||
|  | ||||
| main>section#books>form.upload>p { | ||||
|     font-size: 3rem; | ||||
|     height: 0.3rem; | ||||
|     line-height: 0; | ||||
| } | ||||
|  | ||||
| main>section#books>article.book { | ||||
|     width: calc(100% / 3 - 20px); | ||||
|     margin-right: 20px; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
| } | ||||
|  | ||||
| main>section#books>article.book:nth-child(3) { | ||||
|     width: calc(100% / 3); | ||||
|     margin-right: unset; | ||||
| } | ||||
|  | ||||
| main>section#books>article.book>img { | ||||
|     height: 220px; | ||||
|     object-fit: cover; | ||||
|     object-position: right; | ||||
|     overflow: hidden; | ||||
|     clip-path: polygon(5px calc(100% - 5px), calc(100% - 5px) calc(100% - 5px), calc(100% - 5px) 5px, 5px 5px); | ||||
| } | ||||
|  | ||||
| main>section#books>article.book>h4 { | ||||
|     margin-top: 5px; | ||||
|     margin-bottom: 10px; | ||||
|     height: 50px; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| main>section#books>article.book>p { | ||||
|     margin: unset; | ||||
| } | ||||
							
								
								
									
										31
									
								
								mirzaev/site/virus/system/public/css/hotline.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,31 @@ | ||||
| section.hotline { | ||||
|     display: inline-flex; | ||||
|     height : 100%; | ||||
| } | ||||
|  | ||||
| section.hotline * { | ||||
|     transition: unset; | ||||
| } | ||||
|  | ||||
| section.hotline:last-child { | ||||
|     margin-bottom: unset; | ||||
| } | ||||
|  | ||||
| section.hotline>article { | ||||
|     margin-right    : 18px; | ||||
|     width           : 140px; | ||||
|     height          : 190px; | ||||
|     display         : flex; | ||||
|     align-self      : flex-end; | ||||
|     border-radius   : 3px; | ||||
|     background-color: var(--background-light-1); | ||||
|     box-shadow      : 0px -6px 6px rgba(0, 0, 0, 0.3); | ||||
| } | ||||
|  | ||||
| section.hotline>article:last-child { | ||||
|     margin-right: unset; | ||||
| } | ||||
|  | ||||
| section.hotline>article>* { | ||||
|     margin: auto; | ||||
| } | ||||
							
								
								
									
										40
									
								
								mirzaev/site/virus/system/public/css/logo.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,40 @@ | ||||
| header>div#logo { | ||||
|     position          : relative; | ||||
|     height            : 260px; | ||||
|     display           : flex; | ||||
|     flex-direction    : column; | ||||
|     justify-content   : center; | ||||
|     gap               : unset; | ||||
|     border-right      : 1px solid; | ||||
|     border-right-color: #ccc; | ||||
|     border-right-color: rgba(255, 255, 255, 0.2); | ||||
|     background-color  : rgba(255, 255, 255, 0.2); | ||||
|     overflow          : hidden; | ||||
| } | ||||
|  | ||||
| header>div#logo>h4 { | ||||
|     z-index: 10; | ||||
|     margin : 0 20px; | ||||
| } | ||||
|  | ||||
| header>div#logo>h1 { | ||||
|     z-index      : 50; | ||||
|     margin       : 0 37px; | ||||
|     margin-bottom: -5px; | ||||
| } | ||||
|  | ||||
| header>div#logo>#hotline_logo { | ||||
|     z-index            : -50; | ||||
|     position           : absolute; | ||||
|     -webkit-filter     : blur(2px) brightness(1.3); | ||||
|     filter             : blur(2px) brightness(1.3); | ||||
|     /* background-color: var(--background); */ | ||||
| } | ||||
|  | ||||
| header>div#logo>#hotline_logo>article { | ||||
|     margin-bottom: 30px; | ||||
| } | ||||
|  | ||||
| aside>section#hotline { | ||||
|     clip-path: inset(0 0 0 250px); | ||||
| } | ||||
							
								
								
									
										165
									
								
								mirzaev/site/virus/system/public/css/main.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,165 @@ | ||||
| @import url('/fonts/comissioner.ttf'); | ||||
|  | ||||
| @media (prefers-color-scheme: light) { | ||||
|     :root { | ||||
|         --background: #eee; | ||||
|         --text      : 'dark'; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @media (prefers-color-scheme: dark) { | ||||
|     :root { | ||||
|         --background-light-3: #403939; | ||||
|         --background-light-2: #322d2d; | ||||
|         --background-light-1: #2b2525; | ||||
|         --background-light  : #252020; | ||||
|         --background        : #221e1e; | ||||
|         --background-dark   : ; | ||||
|         --text              : #e6e6e6; | ||||
|         --text-hover        : #fff; | ||||
|         --text-active       : #d0d0d0; | ||||
|         --red-light-1       : #dc4343; | ||||
|         --red-light         : #bf3737; | ||||
|         --red               : #a43333; | ||||
|         --red-dark          : #8d2a2a; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| * { | ||||
|     text-decoration: none; | ||||
|     outline        : none; | ||||
|     border         : none; | ||||
|     color          : var(--text); | ||||
|     font-family    : 'Commissioner', sans-serif; | ||||
|     transition     : 0.1s ease-out; | ||||
| } | ||||
|  | ||||
| .unselectable { | ||||
|     -webkit-touch-callout: none; | ||||
|     -webkit-user-select  : none; | ||||
|     -khtml-user-select   : none; | ||||
|     -moz-user-select     : none; | ||||
|     -ms-user-select      : none; | ||||
|     user-select          : none; | ||||
| } | ||||
|  | ||||
| a:hover { | ||||
|     color: var(--text-hover); | ||||
| } | ||||
|  | ||||
| a:active { | ||||
|     color: var(--text-active); | ||||
| } | ||||
|  | ||||
| body { | ||||
|     height               : 100vh; | ||||
|     margin               : 0; | ||||
|     display              : grid; | ||||
|     grid-template-rows   : 10px 200px auto 10px; | ||||
|     grid-template-columns: 250px auto 300px; | ||||
|     grid-row-gap         : 20px; | ||||
|     grid-column-gap      : 15px; | ||||
|     overflow: hidden; | ||||
|     background-color     : var(--background); | ||||
| } | ||||
|  | ||||
| aside { | ||||
|     z-index    : 500; | ||||
|     grid-column: 1/ 4; | ||||
|     grid-row   : 2; | ||||
|     overflow   : hidden; | ||||
| } | ||||
|  | ||||
| header { | ||||
|     z-index       : 5000; | ||||
|     grid-column   : 1; | ||||
|     grid-row      : 1 / 5; | ||||
|     display       : flex; | ||||
|     flex-direction: column; | ||||
|     box-shadow      : 2px 0 5px rgba(0, 0, 0, 0.3); | ||||
| } | ||||
|  | ||||
| header>menu { | ||||
|     margin          : unset; | ||||
|     padding         : 20px; | ||||
|     display         : flex; | ||||
|     flex-direction  : column; | ||||
|     flex-grow       : 1; | ||||
|     background-color: var(--background-light-1); | ||||
| } | ||||
|  | ||||
| header>#account>button#login { | ||||
|     z-index : 1500; | ||||
| } | ||||
|  | ||||
| header>menu a { | ||||
|     margin-bottom: 8px; | ||||
|     display      : flex; | ||||
|     align-items  : center; | ||||
| } | ||||
|  | ||||
| header>menu a:last-child { | ||||
|     margin-bottom: unset; | ||||
| } | ||||
|  | ||||
| header>menu a svg { | ||||
|     margin-right: 8px; | ||||
|     height      : 1.2rem; | ||||
|     position    : relative; | ||||
| } | ||||
|  | ||||
| header>menu a:hover svg { | ||||
|     margin-left : -5px; | ||||
|     margin-right: 13px; | ||||
| } | ||||
|  | ||||
| header>menu a svg path { | ||||
|     fill: var(--text); | ||||
| } | ||||
|  | ||||
| header>section { | ||||
|     background-color: var(--background-light-1); | ||||
| } | ||||
|  | ||||
| header :is(button, a[type="button"]) { | ||||
|     width           : 100%; | ||||
|     height          : 40px; | ||||
|     display         : flex; | ||||
|     justify-content : center; | ||||
|     align-items     : center; | ||||
|     cursor          : pointer; | ||||
|     background-color: var(--red); | ||||
|     transition      : unset; | ||||
| } | ||||
|  | ||||
| header button { | ||||
|     font-weight   : bold; | ||||
|     text-transform: uppercase; | ||||
| } | ||||
|  | ||||
| header :is(button, a[type="button"]):hover { | ||||
|     background-color: var(--red-light); | ||||
| } | ||||
|  | ||||
| header :is(button, a[type="button"]):active { | ||||
|     background-color: var(--red-dark); | ||||
| } | ||||
|  | ||||
| header>nav { | ||||
|     margin-top    : auto; | ||||
|     display       : flex; | ||||
|     flex-direction: column; | ||||
| } | ||||
|  | ||||
| main { | ||||
|     z-index    : 1000; | ||||
|     grid-column: 2; | ||||
|     grid-row   : 3; | ||||
| } | ||||
|  | ||||
| footer { | ||||
|     z-index    : 3000; | ||||
|     grid-column: 3; | ||||
|     grid-row   : 1 / 5; | ||||
| } | ||||
							
								
								
									
										17
									
								
								mirzaev/site/virus/system/public/css/trolling.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | ||||
| #what_image { | ||||
|     z-index: 9999999; | ||||
|     position: absolute; | ||||
|     width: 100vw; | ||||
|     height: 100vh; | ||||
|     display: none; | ||||
|     transition: unset; | ||||
| } | ||||
|  | ||||
| #what_image.active { | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| .hide { | ||||
|     position: absolute; | ||||
|     display: none; | ||||
| } | ||||
							
								
								
									
										33
									
								
								mirzaev/site/virus/system/public/css/upload.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,33 @@ | ||||
| form.upload { | ||||
|     width: 100%; | ||||
|     height: 100px; | ||||
|     position: relative; | ||||
|     display: flex; | ||||
|     border: 4px dashed #e5ddd1; | ||||
| } | ||||
|  | ||||
| form.upload:hover { | ||||
|     background-color: #ccc6bd; | ||||
|     border: 4px dashed #fff7ea; | ||||
| } | ||||
|  | ||||
| form.upload>p { | ||||
|     margin: auto; | ||||
|     font-weight: bold; | ||||
|     color: #eee6d9; | ||||
| } | ||||
|  | ||||
| form.upload:hover>p { | ||||
|     color: #fff7ea; | ||||
| } | ||||
|  | ||||
| form.upload>input { | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     position: absolute; | ||||
|     opacity: 0; | ||||
| } | ||||
|  | ||||
| form.upload:hover>input { | ||||
|     cursor: pointer; | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								mirzaev/site/virus/system/public/fonts/commissioner.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										1
									
								
								mirzaev/site/virus/system/public/images/botnet.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg height="32" width="32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M0 0h32v32H0z"/><path d="M30 17.349V12h-2v-2h2V2H2v8h2v2H2v8h2v2H2v8h15.349A8.97 8.97 0 0 0 23 32a9.002 9.002 0 0 0 9-9 8.968 8.968 0 0 0-2-5.651zM14.059 22H6v-2h8.522a8.932 8.932 0 0 0-.463 2zM26 12H6v-2h20v2zM4 6h4v2H4V6zm0 10h4v2H4v-2zm4 12H4v-2h4v2zm15 1.883A6.898 6.898 0 0 1 16.115 23 6.898 6.898 0 0 1 23 16.115 6.898 6.898 0 0 1 29.883 23 6.898 6.898 0 0 1 23 29.883z"/><path d="m19 25 2 2 2-2 2 2 2-2-2-2 2-2-2-2-2 2-2-2-2 2 2 2z"/></svg> | ||||
| After Width: | Height: | Size: 552 B | 
							
								
								
									
										1
									
								
								mirzaev/site/virus/system/public/images/keylogger.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M0 6c0-1.1.9-2 2-2h16a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6zm2 0v2h2V6H2zm1 3v2h2V9H3zm-1 3v2h2v-2H2zm3 0v2h10v-2H5zm11 0v2h2v-2h-2zM6 9v2h2V9H6zm3 0v2h2V9H9zm3 0v2h2V9h-2zm3 0v2h2V9h-2zM5 6v2h2V6H5zm3 0v2h2V6H8zm3 0v2h2V6h-2zm3 0v2h4V6h-4z"/></svg> | ||||
| After Width: | Height: | Size: 328 B | 
							
								
								
									
										
											BIN
										
									
								
								mirzaev/site/virus/system/public/images/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 18 KiB | 
							
								
								
									
										
											BIN
										
									
								
								mirzaev/site/virus/system/public/images/logo_compressed.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								mirzaev/site/virus/system/public/images/logo_red.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 17 KiB | 
							
								
								
									
										
											BIN
										
									
								
								mirzaev/site/virus/system/public/images/logo_red_compressed.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.3 KiB | 
							
								
								
									
										
											BIN
										
									
								
								mirzaev/site/virus/system/public/images/logo_red_white.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 18 KiB | 
| After Width: | Height: | Size: 5.6 KiB | 
							
								
								
									
										1
									
								
								mirzaev/site/virus/system/public/images/minecraft.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M0 0h256v256H0z"/><path d="M224 177.3V78.7a8.1 8.1 0 0 0-4.1-7l-88-49.5a7.8 7.8 0 0 0-7.8 0l-88 49.5a8.1 8.1 0 0 0-4.1 7v98.6a8.1 8.1 0 0 0 4.1 7l88 49.5a7.8 7.8 0 0 0 7.8 0l88-49.5a8.1 8.1 0 0 0 4.1-7Z" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m222.9 74.6-94 53.4-95.8-53.4M128.9 128l-.9 106.8"/></svg> | ||||
| After Width: | Height: | Size: 537 B | 
							
								
								
									
										1
									
								
								mirzaev/site/virus/system/public/images/miner.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m11.136 12.117-.596 2.415c.736.185 3.004.921 3.34-.441.35-1.421-2.009-1.789-2.744-1.974Zm.813-3.297-.54 2.191c.612.154 2.5.784 2.806-.455.318-1.293-1.654-1.581-2.266-1.736ZM12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2Zm4.358 8.575a1.743 1.743 0 0 1-1.385 1.611 1.933 1.933 0 0 1 .997 2.661c-.586 1.692-1.977 1.835-3.827 1.481l-.449 1.82-1.085-.274.443-1.795c-.28-.07-.568-.145-.864-.227l-.445 1.804-1.084-.273.45-1.824c-.254-.065-.511-.135-.774-.201l-1.412-.356.539-1.256s.8.215.788.199a.394.394 0 0 0 .498-.26l1.217-4.939a.583.583 0 0 0-.505-.638c.016-.011-.789-.198-.789-.198l.29-1.172 1.495.378-.001.006c.225.056.457.11.693.164l.444-1.802 1.085.274-.436 1.766c.291.068.584.135.87.207l.432-1.755 1.085.274-.445 1.802c1.37.477 2.372 1.193 2.175 2.523Z"/></svg> | ||||
| After Width: | Height: | Size: 825 B | 
							
								
								
									
										1
									
								
								mirzaev/site/virus/system/public/images/stealer.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 7.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								mirzaev/site/virus/system/public/images/truth.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 397 KiB | 
							
								
								
									
										
											BIN
										
									
								
								mirzaev/site/virus/system/public/images/what.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 295 KiB | 
							
								
								
									
										35
									
								
								mirzaev/site/virus/system/public/index.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,35 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace mirzaev\site\virus; | ||||
|  | ||||
| use mirzaev\minimal\core; | ||||
| use mirzaev\minimal\router; | ||||
|  | ||||
| ini_set('error_reporting', E_ALL); | ||||
| ini_set('display_errors', 1); | ||||
| ini_set('display_startup_errors', 1); | ||||
|  | ||||
| define('VIEWS', realpath('..' . DIRECTORY_SEPARATOR . 'views')); | ||||
| define('STORAGE', realpath('..' . DIRECTORY_SEPARATOR . 'storage')); | ||||
| define('INDEX', __DIR__); | ||||
|  | ||||
| // Автозагрузка | ||||
| require __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; | ||||
|  | ||||
| // Инициализация маршрутазитора | ||||
| $router = new router; | ||||
|  | ||||
| // Запись маршрутов | ||||
| $router->write('/', 'index', 'index'); | ||||
| $router->write('/system/hotline', 'hotline', 'index'); | ||||
| $router->write('/account/initialization', 'account', 'initialization', 'PUT'); | ||||
| $router->write('/account/vk/connect', 'account', 'connect'); | ||||
| $router->write('/account/panel', 'account', 'panel'); | ||||
|  | ||||
| // Инициализация ядра | ||||
| $core = new core(namespace: __NAMESPACE__, router: $router); | ||||
|  | ||||
| // Обработка запроса | ||||
| echo $core->start(); | ||||
							
								
								
									
										139
									
								
								mirzaev/site/virus/system/public/js/account.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,139 @@ | ||||
| "use strict"; | ||||
|  | ||||
| class account { | ||||
|     static async initialization() { | ||||
|         // Запрос | ||||
|         return fetch('https://virus.mirzaev.sexy/account/initialization', { | ||||
|             method: 'PUT' | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     static authentication(button) { | ||||
|         if (button instanceof HTMLElement) { | ||||
|             // Получены обязательные входные параметры | ||||
|  | ||||
|             if (button.classList.contains('active')) { | ||||
|                 // Кнопка активна (подразумевается) | ||||
|  | ||||
|                 if (window.vk !== undefined) { | ||||
|                     // Найдена инстанция окна | ||||
|  | ||||
|                     // Закрытие окна | ||||
|                     window.vk.close(); | ||||
|  | ||||
|                     // Удаление окна | ||||
|                     window.vk = undefined; | ||||
|                 } | ||||
|  | ||||
|                 // Генерация панели | ||||
|                 this.panel(button.parentElement); | ||||
|  | ||||
|                 // Вызов троллера | ||||
|                 troller.what.single(); | ||||
|             } else { | ||||
|                 // Кнопка неактивна (подразумевается) | ||||
|  | ||||
|                 // Инициализация активного статуса | ||||
|                 button.classList.add('active'); | ||||
|                 button.innerText = 'Закрыть'; | ||||
|  | ||||
|                 // Настройка окна | ||||
|                 const width = 500; | ||||
|                 const height = 500; | ||||
|                 const left = (window.screen.width / 2) - ((width / 2) + 10); | ||||
|                 const top = (window.screen.height / 2) - ((height / 2) + 50); | ||||
|  | ||||
|                 // Инициализация аккаунта | ||||
|                 this.initialization() | ||||
|                     .then( | ||||
|                         (response) => { | ||||
|                             if (response.status === 401 && typeof response.headers.get('session') === 'string') { | ||||
|                                 // Получен код ответа 401 (не аутентифицирован) и инициализирован аккаунт | ||||
|  | ||||
|                                 // Открытие окна с аунтентификацией ВКонтакте | ||||
|                                 window.vk = window.open( | ||||
|                                     'https://oauth.vk.com/authorize?client_id=51447080&redirect_uri=https://virus.mirzaev.sexy/account/vk/connect&display=popup&response_type=code&scope=4521990&state=' + response.headers.get('session'), | ||||
|                                     'virus_vk', | ||||
|                                     'left=' + left + ',top=' + top + ',width=' + width + ',height=' + height + ',resizable=no,status=no,toolbar=no,menubar=no,scrollbars=no,location=no,directories=no' | ||||
|                                 ); | ||||
|  | ||||
|                                 // Инициализация ссылки на ядро | ||||
|                                 const _this = this; | ||||
|  | ||||
|                                 // Инициализация интервала проверки закрытия окна с аунтентификацией ВКонтакте | ||||
|                                 const interval = setInterval(function () { | ||||
|                                     if (window.vk.closed || window.vk === undefined) { | ||||
|                                         // Окно с аутентификацией закрыто | ||||
|  | ||||
|                                         // Удаление интервала | ||||
|                                         clearInterval(interval); | ||||
|  | ||||
|                                         // Генерация панели | ||||
|                                         _this.panel(button.parentElement); | ||||
|                                     } | ||||
|                                 }, 100); | ||||
|                             } else if (response.status === 200) { | ||||
|                                 // Получен код ответа 200 (аутентифицирован) | ||||
|  | ||||
|                                 // Генерация панели | ||||
|                                 this.panel(button.parentElement); | ||||
|                             } | ||||
|                         } | ||||
|                     ); | ||||
|  | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static deauthentication() { | ||||
|         if (shell instanceof HTMLElement) { | ||||
|             // Получены обязательные входные параметры | ||||
|  | ||||
|             fetch('https://virus.mirzaev.sexy/account/panel', { | ||||
|                 method: 'GET' | ||||
|             }).then( | ||||
|                 (response) => { | ||||
|                     if (response.status === 200) { | ||||
|                         // Получен код ответа 200 | ||||
|  | ||||
|                         response.text().then( | ||||
|                             (text) => { | ||||
|                                 console.log(text); | ||||
|  | ||||
|                                 // Запись панели в оболочку | ||||
|                                 shell.outerHTML = text; | ||||
|                             } | ||||
|                         ); | ||||
|                     } | ||||
|                 } | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static async panel(shell) { | ||||
|         if (shell instanceof HTMLElement) { | ||||
|             // Получены обязательные входные параметры | ||||
|  | ||||
|             fetch('https://virus.mirzaev.sexy/account/panel', { | ||||
|                 method: 'GET' | ||||
|             }).then( | ||||
|                 (response) => { | ||||
|                     if (response.status === 200) { | ||||
|                         // Получен код ответа 200 | ||||
|  | ||||
|                         response.text().then( | ||||
|                             (text) => { | ||||
|                                 console.log(text); | ||||
|  | ||||
|                                 // Запись панели в оболочку | ||||
|                                 shell.outerHTML = text; | ||||
|                             } | ||||
|                         ); | ||||
|                     } | ||||
|                 } | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										0
									
								
								mirzaev/site/virus/system/public/js/graph.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										668
									
								
								mirzaev/site/virus/system/public/js/hotline.js
									
									
									
									
									
										Normal 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 } | ||||
|   }) | ||||
| ); | ||||
							
								
								
									
										2
									
								
								mirzaev/site/virus/system/public/js/js.cookie.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| /*! js-cookie v3.0.1 | MIT */ | ||||
| !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,(function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var o in n)e[o]=n[o]}return e}return function t(n,o){function r(t,r,i){if("undefined"!=typeof document){"number"==typeof(i=e({},o,i)).expires&&(i.expires=new Date(Date.now()+864e5*i.expires)),i.expires&&(i.expires=i.expires.toUTCString()),t=encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape);var c="";for(var u in i)i[u]&&(c+="; "+u,!0!==i[u]&&(c+="="+i[u].split(";")[0]));return document.cookie=t+"="+n.write(r,t)+c}}return Object.create({set:r,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var t=document.cookie?document.cookie.split("; "):[],o={},r=0;r<t.length;r++){var i=t[r].split("="),c=i.slice(1).join("=");try{var u=decodeURIComponent(i[0]);if(o[u]=n.read(c,u),e===u)break}catch(e){}}return e?o[e]:o}},remove:function(t,n){r(t,"",e({},n,{expires:-1}))},withAttributes:function(n){return t(this.converter,e({},this.attributes,n))},withConverter:function(n){return t(e({},this.converter,n),this.attributes)}},{attributes:{value:Object.freeze(o)},converter:{value:Object.freeze(n)}})}({read:function(e){return'"'===e[0]&&(e=e.slice(1,-1)),e.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(e){return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}},{path:"/"})})); | ||||
							
								
								
									
										127
									
								
								mirzaev/site/virus/system/public/js/trolling.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,127 @@ | ||||
| "use strict"; | ||||
|  | ||||
| class troller { | ||||
|     static what = { | ||||
|         enable() { | ||||
|             document.body.onmouseleave = function () { | ||||
|                 // if (Math.random() > 0.90) { | ||||
|                 // 10% | ||||
|  | ||||
|                 troller.what.start(); | ||||
|                 // } | ||||
|             }; | ||||
|  | ||||
|             document.body.onmouseenter = function () { | ||||
|                 troller.what.end(); | ||||
|             }; | ||||
|         }, | ||||
|         disable() { | ||||
|             document.body.onmouseleave = document.body.onmouseenter = undefined; | ||||
|         }, | ||||
|         start() { | ||||
|             // Отображение изображения | ||||
|             document.getElementById('what_image').classList.add('active'); | ||||
|  | ||||
|             // Инициализация элемента со звуком | ||||
|             const what_sound = document.getElementById('what_sound'); | ||||
|  | ||||
|             // Воспроизведение звука | ||||
|             what_sound.currentTime = 0; | ||||
|             what_sound.play(); | ||||
|         }, | ||||
|         end() { | ||||
|             // Сокрытие изображения | ||||
|             document.getElementById('what_image').classList.remove('active'); | ||||
|  | ||||
|             // Остановка звука | ||||
|             document.getElementById('what_sound').pause(); | ||||
|         }, | ||||
|         single(event = 'onmouseleave') { | ||||
|             if (typeof event === 'string') { | ||||
|                 // Получены обязательные входные параметры | ||||
|                 // Отображение изображения | ||||
|                 document.getElementById('what_image').classList.add('active'); | ||||
|  | ||||
|                 // Инициализация элемента со звуком | ||||
|                 const what_sound = document.getElementById('what_sound'); | ||||
|  | ||||
|                 // Воспроизведение звука | ||||
|                 what_sound.currentTime = 0; | ||||
|                 what_sound.play(); | ||||
|  | ||||
|                 document.body[event] = function () { | ||||
|                     troller.what.end(); | ||||
|  | ||||
|                     document.body[event] = undefined; | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static vk() { | ||||
|         setInterval(function () { | ||||
|             const sound = document.getElementById('sound_vk'); | ||||
|  | ||||
|             if (Math.random() > 0.95) { | ||||
|                 // 5% | ||||
|  | ||||
|                 // Воспроизведение звука | ||||
|                 sound.currentTime = 0; | ||||
|                 sound.play(); | ||||
|             } | ||||
|         }, 85000); | ||||
|     } | ||||
|  | ||||
|     static whatsapp() { | ||||
|         setInterval(function () { | ||||
|             const sound = document.getElementById('sound_whatsup'); | ||||
|  | ||||
|             if (Math.random() > 0.97) { | ||||
|                 // 3% | ||||
|  | ||||
|                 // Воспроизведение звука | ||||
|                 sound.currentTime = 0; | ||||
|                 sound.play(); | ||||
|             } | ||||
|         }, 125000); | ||||
|     } | ||||
|  | ||||
|     static iphone() { | ||||
|         setInterval(function () { | ||||
|             const sound = document.getElementById('sound_iphone'); | ||||
|  | ||||
|             if (Math.random() > 0.98) { | ||||
|                 // 2% | ||||
|  | ||||
|                 // Воспроизведение звука | ||||
|                 sound.currentTime = 0; | ||||
|                 sound.play(); | ||||
|             } | ||||
|         }, 265000); | ||||
|     } | ||||
| } | ||||
|  | ||||
| if (Math.random() > 0.90) { | ||||
|     // 10% | ||||
|  | ||||
|     troller.what.enable(); | ||||
| } | ||||
|  | ||||
| if (Math.random() > 0.90) { | ||||
|     // 10% | ||||
|  | ||||
|     troller.vk(); | ||||
| } | ||||
|  | ||||
|  | ||||
| if (Math.random() > 0.90) { | ||||
|     // 10% | ||||
|  | ||||
|     troller.whatsapp(); | ||||
| } | ||||
|  | ||||
| if (Math.random() > 0.90) { | ||||
|     // 10% | ||||
|  | ||||
|     troller.iphone(); | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								mirzaev/site/virus/system/public/sounds/iphone.mp3
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								mirzaev/site/virus/system/public/sounds/vk.mp3
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								mirzaev/site/virus/system/public/sounds/what.mp3
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								mirzaev/site/virus/system/public/sounds/whatsup.mp3
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 16 KiB | 
							
								
								
									
										1
									
								
								mirzaev/site/virus/system/settings/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| arangodb.php | ||||
							
								
								
									
										8
									
								
								mirzaev/site/virus/system/settings/arangodb.php.sample
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | ||||
| <?php | ||||
|  | ||||
| return [ | ||||
|     'endpoint' => 'unix:///var/run/arangodb3/arango.sock', | ||||
|     'database' => '', | ||||
|     'name' => '', | ||||
|     'password' => '' | ||||
| ]; | ||||
							
								
								
									
										14
									
								
								mirzaev/site/virus/system/views/account/panel.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| {% block body %} | ||||
| <section id="panel"> | ||||
|     {% if account %} | ||||
|     {{ account.getKey() }} | ||||
|     {% if vk %} | ||||
|     {{ vk.mail }} | ||||
|     {% endif %} | ||||
|     {% else %} | ||||
|     <button id="login" onclick="return account.authentication(this)"> | ||||
|         Войти в аккаунт | ||||
|     </button> | ||||
|     {% endif %} | ||||
| </section> | ||||
| {% endblock %} | ||||
							
								
								
									
										11
									
								
								mirzaev/site/virus/system/views/account/vk.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| {% block body %} | ||||
| <img src="/images/truth.jpg" alt="никому не показывай" style="position: absolute; left: 0; top: 0; width: 100%; height: 100%;"> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block js_init %} | ||||
| <script> | ||||
|     setTimeout(fn => { | ||||
|         window.opener.vk.close(); | ||||
|     }, 2000); | ||||
| </script> | ||||
| {% endblock %} | ||||
							
								
								
									
										19
									
								
								mirzaev/site/virus/system/views/aside.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | ||||
| {% use '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('hotline_css') }} #} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block body %} | ||||
| <aside> | ||||
|     {{ block('hotline_body') }} | ||||
| </aside> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block js %} | ||||
| {# {{ block('hotline_js') }} #} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block js_init %} | ||||
| {# {{ block('hotline_js_init') }} #} | ||||
| {% endblock %} | ||||
							
								
								
									
										33
									
								
								mirzaev/site/virus/system/views/core.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,33 @@ | ||||
| <!doctype html> | ||||
|  | ||||
| <html lang="ru"> | ||||
|  | ||||
| <head> | ||||
| 	{% use 'head.html' with title as head_title, meta as head_meta, css as head_css %} | ||||
|  | ||||
| 	{% block title %} | ||||
| 	{{ block('head_title') }} | ||||
| 	{% endblock %} | ||||
|  | ||||
| 	{% block meta %} | ||||
| 	{{ block('head_meta') }} | ||||
| 	{% endblock %} | ||||
|  | ||||
| 	{% block css %} | ||||
| 	{{ block('head_css') }} | ||||
| 	{% endblock %} | ||||
| </head> | ||||
|  | ||||
| <body> | ||||
| 	{% block body %} | ||||
| 	{% endblock %} | ||||
|  | ||||
| 	{% block js %} | ||||
| 	{% include 'js.html' %} | ||||
| 	{% endblock %} | ||||
|  | ||||
| 	{% block js_init %} | ||||
| 	{% endblock %} | ||||
| </body> | ||||
|  | ||||
| </html> | ||||
							
								
								
									
										4
									
								
								mirzaev/site/virus/system/views/footer.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| <footer> | ||||
|     <!-- <p><a href="http://www.anybrowser.org/campaign/"><img src="/img/logos/any_browser.gif" width="278" height="44" alt="Доступно на любом браузере" /></a></p> --> | ||||
|     <!-- <p><a href="/browsers"><img src="/img/logos/any_browser.gif" width="278" height="44" alt="Доступно на любом браузере" /></a></p> --> | ||||
| </footer> | ||||
							
								
								
									
										15
									
								
								mirzaev/site/virus/system/views/head.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | ||||
| {% block title %} | ||||
| <title>{% if head.title != empty %}{{head.title}}{% else %}Скачать вирусы бесплатно{% endif %}</title> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block meta %} | ||||
| <meta charset="utf-8"> | ||||
| <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
| {% for meta in head.metas %} | ||||
| <meta {% for name, value in meta.attributes %}{{name}}="{{value}}" {% endfor %}> | ||||
| {% endfor %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block css %} | ||||
| <link rel="stylesheet" type="text/css" href="/css/main.css" /> | ||||
| {% endblock %} | ||||
							
								
								
									
										22
									
								
								mirzaev/site/virus/system/views/header.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,22 @@ | ||||
| {% use 'logo.html' with css as logo_css, body as logo_body, js as logo_js, js_init as logo_js_init %} | ||||
| {% use 'account/panel.html' with body as panel_body %} | ||||
|  | ||||
| {% block css %} | ||||
| {{ block('logo_css') }} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block body %} | ||||
| <header> | ||||
|     {{ block('logo_body') }} | ||||
|     {% include 'menu.html' %} | ||||
|     {{ block('panel_body') }} | ||||
| </header> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block js %} | ||||
| {{ block('logo_js') }} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block js_init %} | ||||
| {{ block('logo_js_init') }} | ||||
| {% endblock %} | ||||
							
								
								
									
										28
									
								
								mirzaev/site/virus/system/views/hotline/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,28 @@ | ||||
| {% block css %} | ||||
| <link type="text/css" rel="stylesheet" href="/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'}}>{{ element.content }}</{{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 %} | ||||
							
								
								
									
										41
									
								
								mirzaev/site/virus/system/views/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,41 @@ | ||||
| {% extends "core.html" %} | ||||
|  | ||||
| {% use "core.html" with css as core_css, body as core_body, js as core_js, js_init as core_js_init %} | ||||
| {% use "trolling.html" with css as trolling_css, body as trolling_body, js as trolling_js %} | ||||
| {% use "header.html" with css as header_css, body as header_body, js as header_js, js_init as header_js_init %} | ||||
| {% use "aside.html" with css as aside_css, body as aside_body, js as aside_js, js_init as aside_js_init %} | ||||
|  | ||||
| {% block css %} | ||||
| {{ block('core_css') }} | ||||
| {{ block('trolling_css') }} | ||||
| {{ block('header_css') }} | ||||
| {{ block('aside_css') }} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block body %} | ||||
| {{ block('core_body') }} | ||||
| {{ block('trolling_body') }} | ||||
| {{ block('aside_body') }} | ||||
| {{ block('header_body') }} | ||||
|  | ||||
| <main> | ||||
| 	<noscript>К сожалению мой сайт ещё пока не готов для работы без javascript</noscript> | ||||
| 	{% block main %} | ||||
| 	{% endblock %} | ||||
| </main> | ||||
|  | ||||
| {# {% include 'footer.html' %} #} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block js %} | ||||
| {{ block('core_js') }} | ||||
| {{ block('trolling_js') }} | ||||
| {{ block('header_js') }} | ||||
| {{ block('aside_js') }} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block js_init %} | ||||
| {{ block('core_js_init') }} | ||||
| {{ block('header_js_init') }} | ||||
| {{ block('aside_js_init') }} | ||||
| {% endblock %} | ||||
							
								
								
									
										4
									
								
								mirzaev/site/virus/system/views/js.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| {% block js %} | ||||
| <script type="text/javascript" src="/js/js.cookie.min.js" defer></script> | ||||
| <script type="text/javascript" src="/js/account.js" defer></script> | ||||
| {% endblock %} | ||||
							
								
								
									
										56
									
								
								mirzaev/site/virus/system/views/logo.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,56 @@ | ||||
| {% use 'hotline/index.html' with css as hotline_css, body as hotline_body, js as hotline_js %} | ||||
|  | ||||
| {% block css %} | ||||
| <link rel="stylesheet" type="text/css" href="/css/logo.css" /> | ||||
| {{ block('hotline_css') }} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block body %} | ||||
| <div id="logo" class="unselectable"> | ||||
|     <h1>VIRUS</h1> | ||||
|     <h4>Скачать вирусы бесплатно</h4> | ||||
|  | ||||
|     {% with %} | ||||
|     {% set hotline = hotline|merge({'id': 'hotline_logo'}) %} | ||||
|     {{ block('hotline_body') }} | ||||
|     {% endwith %} | ||||
| </div> | ||||
| {% endblock %} | ||||
|  | ||||
|  | ||||
| {% block js %} | ||||
| {{ block('hotline_js') }} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block js_init %} | ||||
| {{ block('hotline_js_init') }} | ||||
| <script> | ||||
|     document.addEventListener('hotline.loaded', function (e) { | ||||
|         // Запуск препроцессора бегущих строк | ||||
|  | ||||
|         // Инициализация элемента бегущей строки | ||||
|         const element_hotline = document.getElementById('hotline'); | ||||
|         const element_hotline_logo = document.getElementById('hotline_logo'); | ||||
|  | ||||
|         // Инициализация слушателя | ||||
|         element_hotline.hotline.events.set('move.block', true); | ||||
|         element_hotline.hotline.events.set('move.unblock', true); | ||||
|         element_hotline.hotline.events.set('onmousemove', true); | ||||
|  | ||||
|         document.addEventListener(`hotline.${element_hotline.id}.move.block`, function (e) { | ||||
|             // Копирование блокировки | ||||
|             element_hotline_logo.hotline.move = false; | ||||
|         }); | ||||
|  | ||||
|         document.addEventListener(`hotline.${element_hotline.id}.move.unblock`, function (e) { | ||||
|             // Копирование блокировки | ||||
|             element_hotline_logo.hotline.move = true; | ||||
|         }); | ||||
|  | ||||
|         document.addEventListener(`hotline.${element_hotline.id}.onmousemove`, function (e) { | ||||
|             // Копирование перемещения | ||||
|             element_hotline_logo.hotline.offset(e.detail.to); | ||||
|         }); | ||||
|     }); | ||||
| </script> | ||||
| {% endblock %} | ||||
							
								
								
									
										25
									
								
								mirzaev/site/virus/system/views/manager.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,25 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace mirzaev\site\virus\views; | ||||
|  | ||||
| use mirzaev\minimal\controller; | ||||
|  | ||||
| use Twig\Loader\FilesystemLoader; | ||||
| use Twig\Environment as view; | ||||
|  | ||||
| /** | ||||
|  * Менеджер представлений | ||||
|  * | ||||
|  * @package mirzaev\site\virus\controllers | ||||
|  * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> | ||||
|  */ | ||||
| final class manager extends controller | ||||
| { | ||||
|     public function render(string $file, array $vars = []): ?string | ||||
|     { | ||||
|         // Генерация представления | ||||
|         return (new view(new FilesystemLoader(VIEWS)))->render($file, $vars); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										41
									
								
								mirzaev/site/virus/system/views/menu.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										15
									
								
								mirzaev/site/virus/system/views/trolling.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | ||||
| {% block css %} | ||||
| <link rel="stylesheet" type="text/css" href="/css/trolling.css" /> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block body %} | ||||
| <img id="what_image" class="hide" src="/images/what.png" /> | ||||
| <audio id="what_sound" class="hide" src="/sounds/what.mp3" controls></audio> | ||||
| <audio id="sound_vk" class="hide" src="/sounds/vk.mp3" controls></audio> | ||||
| <audio id="sound_whatsup" class="hide" src="/sounds/whatsup.mp3" controls></audio> | ||||
| <audio id="sound_iphone" class="hide" src="/sounds/iphone.mp3" controls></audio> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block js %} | ||||
| <script src="/js/trolling.js" defer></script> | ||||
| {% endblock %} | ||||