├── .env.example ├── .gitignore ├── NOTES.md ├── PACKAGES.md ├── README.md ├── app ├── Console │ └── Commands │ │ ├── MigrationCommand.php │ │ └── RunServerCommand.php ├── Http │ └── Controllers │ │ ├── AuthController.php │ │ ├── Controller.php │ │ ├── LinkController.php │ │ ├── MainController.php │ │ ├── Server │ │ └── Admin │ │ │ └── MainController.php │ │ └── User │ │ ├── CategoryController.php │ │ ├── ChatController.php │ │ ├── ListController.php │ │ ├── NoteController.php │ │ ├── SettingsController.php │ │ └── UserController.php ├── Kernel.php ├── Models │ ├── Model.php │ ├── Note.php │ └── User.php ├── Providers │ ├── AppServiceProvider.php │ ├── EventServiceProvider.php │ ├── HttpServiceProvider.php │ └── SocketServiceProvider.php ├── Scrapper │ └── Webpage.php ├── Servers │ ├── Http │ │ └── Server.php │ └── Websocket │ │ ├── AdminServer.php │ │ ├── PrivateChatServer.php │ │ └── PublicChatServer.php └── Websocket │ ├── Clients.php │ ├── Listeners │ ├── Chat │ │ ├── PrivateChat │ │ │ └── ChatListener.php │ │ └── PublicChat │ │ │ └── ChatListener.php │ ├── Listener.php │ ├── MainListener.php │ ├── Server │ │ └── Admin │ │ │ └── Config │ │ │ └── EnvironmentListener.php │ └── SystemListener.php │ ├── Models │ ├── Client.php │ └── Model.php │ ├── Room.php │ └── UserPresence.php ├── composer.json ├── config ├── app.php └── mime.php ├── database ├── Seeds │ └── UserSeeder.php └── migrations.sql ├── nodemon.json ├── phpstan.neon ├── public ├── assets │ ├── css │ │ ├── bootstrap.min.css │ │ ├── codemirror.css │ │ ├── fontawesome-all.min.css │ │ ├── mdb.min.css │ │ ├── site.message.css │ │ └── style.css │ ├── js │ │ ├── EventEmitter.min.js │ │ ├── bootstrap.bundle.min.js │ │ ├── handlebars.min-v4.7.6.js │ │ ├── howler.min.js │ │ ├── jquery-3.5.1.min.js │ │ ├── linkify-string.min.js │ │ ├── linkify.min.js │ │ ├── main.js │ │ ├── mdb.min.js │ │ ├── moment.min.js │ │ ├── reactificate-0.1.0.js │ │ ├── site │ │ │ ├── admin.js │ │ │ ├── chat.js │ │ │ └── typing-status.js │ │ ├── socket.js │ │ └── user │ │ │ ├── list-taking.js │ │ │ ├── note.category.js │ │ │ ├── note.note.js │ │ │ ├── private-chat.js │ │ │ └── private-connection.js │ ├── mp3 │ │ ├── done-for-you.mp3 │ │ └── juntos.mp3 │ └── webfonts │ │ ├── fa-brands-400.woff2 │ │ └── fa-solid-900.woff2 ├── images │ └── gender │ │ ├── avatar-unknown.png │ │ ├── iconfinder_female1_403023.png │ │ └── iconfinder_male3_403019.png └── mc43ntcwmzawmcaxntkxntc0mjyw.jpg ├── react.php ├── requests.http ├── resources └── views │ ├── auth │ ├── login-success.php │ ├── login.php │ ├── register-success.php │ └── register.php │ ├── chat │ ├── chat.php │ └── index.php │ ├── index-logged.php │ ├── index.php │ ├── layout │ ├── footer.php │ └── header.php │ ├── server │ ├── admin.php │ ├── admin │ │ └── index.php │ ├── index.php │ └── login.php │ ├── system │ ├── 302.php │ ├── 404.php │ ├── 405.php │ └── 500.php │ └── user │ ├── chat │ ├── index.php │ └── private.php │ ├── list.php │ ├── note.php │ ├── profile.php │ ├── settings.php │ └── settings │ ├── change-password.php │ └── index.php ├── routes ├── api.php ├── web.php └── websocket.php ├── src ├── Auth │ ├── Auth.php │ └── Token.php ├── Database │ ├── Connection.php │ └── SeederInterface.php ├── Error.php ├── EventEmitter.php ├── Exceptions │ └── Socket │ │ └── InvalidPayloadException.php ├── Helpers │ ├── Classes │ │ ├── ConsoleHelper.php │ │ ├── FormHelper.php │ │ ├── HelperTrait.php │ │ └── ValidationHelper.php │ ├── generalHelperFunctions.php │ ├── httpHelperFunctions.php │ └── socketHelperFunctions.php ├── Http │ ├── Middleware │ │ ├── AuthMiddleware.php │ │ ├── MiddlewareInterface.php │ │ ├── RootMiddleware.php │ │ └── RouteMiddleware.php │ ├── MiddlewareRunner.php │ ├── Request.php │ ├── Response.php │ ├── Router │ │ ├── Dispatcher.php │ │ ├── Matcher.php │ │ └── RouteCollector.php │ ├── Url.php │ └── View │ │ └── View.php ├── Kernel.php ├── RootServer.php ├── ServerStore.php ├── Servers │ ├── Http │ │ └── Middleware │ │ │ └── StaticFileResponseMiddleware.php │ ├── HttpServer.php │ ├── HttpServerInterface.php │ ├── Socket │ │ └── Middleware │ │ │ └── .gitkeep │ ├── SocketServer.php │ └── SocketServerInterface.php ├── ServiceProvider.php ├── Websocket │ ├── Colis │ │ ├── Colis.php │ │ ├── ColisInterface.php │ │ ├── Dispatcher.php │ │ ├── Matcher.php │ │ └── TheColis.php │ ├── ConnectionFactory.php │ ├── ConnectionInterface.php │ ├── Middleware │ │ ├── AuthMiddleware.php │ │ ├── ColisMiddleware.php │ │ └── MiddlewareInterface.php │ ├── MiddlewareRunner.php │ ├── Payload.php │ ├── Request.php │ ├── Response.php │ └── State.php └── WsServer.php └── storage ├── cache └── route │ └── .gitkeep └── logs └── .gitkeep /.env.example: -------------------------------------------------------------------------------- 1 | # APPLICATION 2 | 3 | # Application name 4 | APP_NAME=ReactPHP-Chat 5 | # Application title 6 | APP_TITLE=Live-chat 7 | # Version of application 8 | APP_VERSION=1 9 | # Application environment 10 | APP_ENVIRONMENT=development 11 | 12 | # Authentication token lifetime in seconds 13 | AUTH_TOKE_LIFE_TIME=86400 14 | 15 | # CONNECTIVITY 16 | 17 | # Server host ip 18 | HOST="0.0.0.0" 19 | # Server listening ip 20 | PORT=9000 21 | # This will be used to provide url for authRoute() function and similar usages 22 | DOMAIN="0.0.0.0:9000" 23 | # Http cookie name 24 | HTTP_COOKIE_NAME=REACT_PHP_LIVE_CHAT 25 | # Admin socket prefix 26 | ADMIN_SOCKET_URL_PREFIX=/admin 27 | # Public chat socket prefix 28 | PUBLIC_CHAT_SOCKET_URL_PREFIX=/ws/chat/public 29 | # Private chat socket prefix 30 | PRIVATE_CHAT_SOCKET_URL_PREFIX=/ws/chat/private 31 | 32 | # DATABASE 33 | 34 | # SQLite database file 35 | DB_FILE=database/database.db 36 | 37 | # PINGING 38 | 39 | # If system will ping clients 40 | WILL_PING_CLIENTS=true 41 | # Interval within which clients will be pinged 42 | CLIENT_PING_INTERVAL=15 43 | 44 | # GARBAGE COLLECTION 45 | 46 | # Check for unused variables/objects/arrays and delete them 47 | WILL_COLLECT_GARBAGE=true 48 | # Interval within witch garbage will be collected 49 | GARBAGE_COLLECTION_INTERVAL=15 50 | 51 | # HOW INFORMATION WILL BE DISPLAYED IN CLI 52 | 53 | # Don't display anything on console, event if forceDisplay is set to true 54 | SILENCE_CONSOLE=false 55 | # Hide messages staring with 'system' prefix 56 | SILENCE_SYSTEM_MESSAGES=false 57 | # This line makes it possible to display running server address 58 | SHOW_FIRST_X_CONSOLE_LOGS=4 59 | # Show messages like "user x joined room y" 60 | SHOW_CLIENT_DEBUG_INFO=true 61 | # Show message like "Pinging x clients round #x" 62 | SHOW_CLIENT_PING_MESSAGE=false 63 | # Display incoming messsages in console 64 | SHOW_SOCKET_INCOMING_MESSAGES=false 65 | # Display outgoing messsages in console 66 | SHOW_SOCKET_OUTGOING_MESSAGES=false 67 | # Display http resource request in console 68 | SHOW_HTTP_RESOURCE_REQUEST=false 69 | 70 | # SERVER ADMIN CREDENTIALS 71 | 72 | # Default server admin username 73 | SERVER_ADMIN_USERNAME=admin 74 | # Default server admin password 75 | SERVER_ADMIN_PASSWORD=1234 76 | 77 | # PRIVATE CHAT 78 | # For how long "typing" should be shown when indicating user is typing, default is half a second. 79 | PRIVATE_CHAT_TYPING_STATUS_TIMEOUT=500 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.env 3 | /storage/* 4 | /.idea 5 | /database/database.db 6 | /composer.lock -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | # Some Important Stuffs 2 | 3 | - All Http requests are dispatched in Server\Middleware\RouteMiddleware 4 | -------------------------------------------------------------------------------- /PACKAGES.md: -------------------------------------------------------------------------------- 1 | # Packages 2 | Here is a list of composer packages used to develop this program. 3 | 4 | - [Ratchet](https://github.com/cboden/ratchet) 5 | - [Colors](https://github.com/kevinlebrun/colors.php) 6 | - [ReactPHP Http](https://github.com/react/http) 7 | - [WebSocketMiddleware](https://github.com/voryx/websocketmiddleware) 8 | - [PHP DotEnv](https://github.com/vlucas/phpdotenv) 9 | - [Symfony Console](https://github.com/symfony/console) 10 | - [ReactPHP SQLite](https://github.com/clue/reactphp-sqlite) 11 | - [Symfony Validator](https://github.com/symfony/validator) 12 | - [QuickRoute](https://github.com/ahmard/quick-route) 13 | - [Static Preloaded Webroot](https://github.com/wyrihaximus/react-http-middleware-webroot-preload) 14 | - [Firebase PHP JWT](https://github.com/firebase/php-jwt) 15 | -------------------------------------------------------------------------------- /app/Console/Commands/MigrationCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Run migrations.') 25 | ->setHelp('Install database tables.') 26 | ->addOption('seed', null) 27 | ->setDefinition(new InputDefinition([ 28 | new InputOption('seed') 29 | ])); 30 | } 31 | 32 | protected function execute(InputInterface $input, OutputInterface $output): int 33 | { 34 | console(true)->comment('Migrating database tables...'); 35 | 36 | filesystem()->file(database_path('migrations.sql')) 37 | ->getContents() 38 | ->then(function ($plainSql) use ($input) { 39 | Connection::create()->exec($plainSql)->then(function () use ($input) { 40 | console(true)->info('Database table migrated.'); 41 | //Check if to seed database data 42 | if ($input->getOption('seed')) { 43 | console(true)->comment('Seeding database data...'); 44 | $this->seed(function () { 45 | console(true)->info('Database table seeded.'); 46 | //Connection::get()->close(); 47 | }); 48 | } 49 | })->otherwise(function (Throwable $error) { 50 | console(true)->error($error->getMessage()); 51 | //Close database connection 52 | Connection::get()->close(); 53 | }); 54 | })->otherwise(function (Throwable $error) { 55 | console(true)->error($error->getMessage()); 56 | }); 57 | 58 | //Run event loop 59 | Loop::run(); 60 | 61 | return Command::SUCCESS; 62 | } 63 | 64 | protected function seed(callable $callback): void 65 | { 66 | foreach ($_ENV['seeds'] as $seed) { 67 | (new $seed())->seed(); 68 | } 69 | 70 | $callback(); 71 | 72 | unset($_ENV['seeds']); 73 | } 74 | } -------------------------------------------------------------------------------- /app/Console/Commands/RunServerCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Start the server') 27 | ->setHelp('Run/Start HttpServer/SocketServer server.'); 28 | } 29 | 30 | protected function execute(InputInterface $input, OutputInterface $output): int 31 | { 32 | RootServer::run(); 33 | return Command::SUCCESS; 34 | } 35 | } -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | $object) { 30 | $this->$objectName = $object; 31 | } 32 | 33 | $this->response = $this->request->getResponse(); 34 | } 35 | } -------------------------------------------------------------------------------- /app/Http/Controllers/LinkController.php: -------------------------------------------------------------------------------- 1 | getParsedBody()['link']; 23 | 24 | if (empty($link)) { 25 | return $this->response->jsonError('Link field must not be empty'); 26 | } 27 | 28 | return database() 29 | ->query('SELECT * FROM link_previews WHERE href = ?', [$link]) 30 | ->then(function (Result $result) use ($link) { 31 | if (empty($result->rows)) { 32 | return $this->scrapeLink($link); 33 | } 34 | 35 | return $this->response->jsonSuccess(\Safe\json_decode($result->rows[0]['meta'])); 36 | }) 37 | ->otherwise(function () use ($link) { 38 | return $this->scrapeLink($link); 39 | }); 40 | } 41 | 42 | protected function scrapeLink(string $url): PromiseInterface 43 | { 44 | return Client::get($url) 45 | ->then(function (Queryable $queryable) use ($url) { 46 | $metaTags = Webpage::extractMetaData($queryable); 47 | 48 | if (!empty($metaTags)) { 49 | database()->query('INSERT INTO link_previews(href, meta) VALUES (?, ?)', [ 50 | $url, \Safe\json_encode($metaTags) 51 | ]); 52 | } 53 | 54 | return $this->response->jsonSuccess($metaTags); 55 | }) 56 | ->otherwise(function (Throwable $error) { 57 | return $this->response->jsonError($error); 58 | }); 59 | } 60 | } -------------------------------------------------------------------------------- /app/Http/Controllers/MainController.php: -------------------------------------------------------------------------------- 1 | request->auth()->check()) { 14 | return $this->response->view('index-logged'); 15 | } 16 | 17 | return $this->response->view('index'); 18 | } 19 | 20 | public function chatIndex(): Response 21 | { 22 | return $this->response->view('chat/index'); 23 | } 24 | 25 | public function publicChat(): Response 26 | { 27 | return $this->response->view('chat/chat', [ 28 | 'socket_prefix' => $_ENV['PUBLIC_CHAT_SOCKET_URL_PREFIX'], 29 | 'room' => [ 30 | 'name' => 'reactphp-is-awesome', 31 | 'user' => 'USER ' . clientCounter(), 32 | ], 33 | ]); 34 | } 35 | } -------------------------------------------------------------------------------- /app/Http/Controllers/Server/Admin/MainController.php: -------------------------------------------------------------------------------- 1 | response->view('server/admin/index'); 15 | } 16 | } -------------------------------------------------------------------------------- /app/Http/Controllers/User/CategoryController.php: -------------------------------------------------------------------------------- 1 | dataDBTable = $this->request 25 | ->getDispatchResult() 26 | ->getRoute() 27 | ->getFields()['dbTable']; 28 | 29 | $this->categoryDBTable = $this->dataDBTable == 'notes' 30 | ? 'note_categories' 31 | : 'list_categories'; 32 | } 33 | 34 | public function add(Request $request): PromiseInterface 35 | { 36 | $data = $request->getParsedBody(); 37 | 38 | return Connection::get()->query( 39 | "INSERT INTO $this->categoryDBTable (name, user_id) VALUES (?, ?);", 40 | [$data['name'], $request->auth()->userId()] 41 | )->then(function (Result $result) use (&$data) { 42 | $data['id'] = $result->insertId; 43 | return $this->response->jsonSuccess($data); 44 | })->otherwise(function () { 45 | return $this->response->jsonError('Insertion failed'); 46 | }); 47 | } 48 | 49 | public function list(Request $request): PromiseInterface 50 | { 51 | return Connection::get() 52 | ->query("SELECT * FROM $this->categoryDBTable WHERE user_id = ?;", [$request->auth()->userId()]) 53 | ->then(function (Result $result) { 54 | return $this->response->jsonSuccess($result->rows); 55 | })->otherwise(function (Throwable $throwable) { 56 | return $this->response->jsonError('List failed'); 57 | }); 58 | } 59 | 60 | public function open(Request $request, array $params): PromiseInterface 61 | { 62 | return Connection::get()->query( 63 | "SELECT * FROM $this->dataDBTable WHERE category_id = ? AND user_id = ?;", 64 | [$params['id'], $request->auth()->userId()] 65 | )->then(function (Result $result) { 66 | return $this->response->jsonSuccess($result->rows); 67 | })->otherwise(function () { 68 | return $this->response->jsonError('Selection failed'); 69 | }); 70 | } 71 | 72 | public function rename(Request $request, array $params): PromiseInterface 73 | { 74 | $data = $request->getParsedBody(); 75 | return Connection::get()->query( 76 | "UPDATE $this->categoryDBTable SET name = ?, updated_at = ? WHERE id = ? AND user_id = ?;", 77 | [$data['name'], time(), $params['id'], $request->auth()->userId()] 78 | )->then(function (Result $result) { 79 | return $this->response->jsonSuccess($result->rows); 80 | })->otherwise(function () { 81 | return $this->response->jsonError('Renaming failed'); 82 | }); 83 | } 84 | 85 | public function delete(Request $request, array $params): PromiseInterface 86 | { 87 | return Connection::get()->query( 88 | "DELETE FROM $this->categoryDBTable WHERE id = ? AND user_id = ?;", 89 | [$params['id'], $request->auth()->userId()] 90 | )->then(function (Result $result) { 91 | return $this->response->jsonSuccess($result->rows); 92 | })->otherwise(function () { 93 | return $this->response->jsonError('Deletion failed'); 94 | }); 95 | } 96 | } -------------------------------------------------------------------------------- /app/Http/Controllers/User/ListController.php: -------------------------------------------------------------------------------- 1 | response->view('user/list'); 20 | } 21 | 22 | public function add(Request $request): PromiseInterface 23 | { 24 | $postData = $request->getParsedBody(); 25 | return Connection::get()->query( 26 | 'INSERT INTO lists(user_id, category_id, content) VALUES (?, ?, ?)', 27 | [$request->auth()->userId(), $postData['category_id'], $postData['content']] 28 | ) 29 | ->then(function (Result $result) use (&$postData) { 30 | $postData['id'] = $result->insertId; 31 | return $this->response->jsonSuccess($postData); 32 | }) 33 | ->otherwise(function (Throwable $throwable) { 34 | return $this->response->jsonError($throwable); 35 | }); 36 | } 37 | 38 | public function update(Request $request, array $params): PromiseInterface 39 | { 40 | $postData = $request->getParsedBody(); 41 | $dbParams = [ 42 | $postData['content'], 43 | carbon()->toString(), 44 | $params['id'], 45 | $request->auth()->userId() 46 | ]; 47 | return Connection::get()->query( 48 | 'UPDATE lists SET content = ?, updated_at = ? WHERE id = ? AND user_id = ?', 49 | $dbParams 50 | ) 51 | ->then(function (Result $result) { 52 | return $this->response->jsonSuccess($result->rows); 53 | }) 54 | ->otherwise(function (Throwable $throwable) { 55 | return $this->response->jsonError($throwable); 56 | }); 57 | } 58 | 59 | public function move(Request $request, array $params): PromiseInterface 60 | { 61 | return Connection::get()->query( 62 | 'UPDATE lists SET category_id = ?, updated_at = ? WHERE id = ? AND user_id = ?;', 63 | [$params['catId'], time(), $params['noteId'], $request->auth()->userId()] 64 | )->then(function () { 65 | return $this->response->jsonSuccess([ 66 | 'message' => 'List item moved successfully.' 67 | ]); 68 | })->otherwise(function (Throwable $throwable) { 69 | return $this->response->jsonError('Moving failed'); 70 | }); 71 | } 72 | 73 | public function delete(Request $request, array $params): PromiseInterface 74 | { 75 | return Connection::get()->query('DELETE FROM lists WHERE id = ? AND user_id = ?', [$params['id'], $request->auth()->userId()]) 76 | ->then(function (Result $result) { 77 | return $this->response->jsonSuccess([]); 78 | }) 79 | ->otherwise(function (Throwable $throwable) { 80 | return $this->response->json($throwable); 81 | }); 82 | } 83 | } -------------------------------------------------------------------------------- /app/Http/Controllers/User/NoteController.php: -------------------------------------------------------------------------------- 1 | response->view('user/note'); 20 | } 21 | 22 | public function add(Request $request): PromiseInterface 23 | { 24 | $postData = $request->getParsedBody(); 25 | return Connection::get()->query( 26 | 'INSERT INTO notes(user_id, category_id, title, note) VALUES (?, ?, ?, ?)', 27 | [$request->auth()->userId(), $postData['category_id'], $postData['title'], $postData['note']] 28 | ) 29 | ->then(function (Result $result) use (&$postData) { 30 | $postData['id'] = $result->insertId; 31 | return $this->response->json([ 32 | 'status' => true, 33 | 'data' => $postData 34 | ]); 35 | }) 36 | ->otherwise(function (Throwable $throwable) { 37 | return $this->response->json([ 38 | 'status' => false, 39 | 'data' => $throwable, 40 | ]); 41 | }); 42 | } 43 | 44 | public function view(Request $request, array $params): PromiseInterface 45 | { 46 | return Connection::get()->query('SELECT * FROM notes WHERE id = ? AND user_id = ?', [$params['id'], $request->auth()->userId()]) 47 | ->then(function (Result $result) { 48 | return $this->response->json([ 49 | 'status' => true, 50 | 'data' => $result->rows, 51 | ]); 52 | }) 53 | ->otherwise(function (Throwable $throwable) { 54 | return $this->response->json([ 55 | 'status' => false, 56 | 'data' => $throwable, 57 | ]); 58 | }); 59 | } 60 | 61 | public function list(Request $request): PromiseInterface 62 | { 63 | return Connection::get() 64 | ->query('SELECT * FROM notes WHERE user_id = ?', [$request->auth()->userId()]) 65 | ->then(function (Result $result) { 66 | return $this->response->json([ 67 | 'status' => true, 68 | 'data' => $result->rows, 69 | ]); 70 | }) 71 | ->otherwise(function (Throwable $throwable) { 72 | return $this->response->json([ 73 | 'status' => false, 74 | 'data' => $throwable, 75 | ]); 76 | }); 77 | } 78 | 79 | public function update(Request $request, array $params): PromiseInterface 80 | { 81 | $postData = $request->getParsedBody(); 82 | $dbParams = [ 83 | $postData['title'], 84 | $postData['note'], 85 | time(), 86 | $params['id'], 87 | $request->auth()->userId() 88 | ]; 89 | return Connection::get()->query( 90 | 'UPDATE notes SET title = ?, note = ?, updated_at = ? WHERE id = ? AND user_id = ?', 91 | $dbParams 92 | ) 93 | ->then(function (Result $result) { 94 | return $this->response->json([ 95 | 'status' => true, 96 | 'data' => $result->rows, 97 | ]); 98 | }) 99 | ->otherwise(function (Throwable $throwable) { 100 | return $this->response->json([ 101 | 'status' => false, 102 | 'data' => $throwable, 103 | ]); 104 | }); 105 | } 106 | 107 | public function move(Request $request, array $params): PromiseInterface 108 | { 109 | return Connection::get()->query( 110 | 'UPDATE notes SET category_id = ?, updated_at = ? WHERE id = ? AND user_id = ?;', 111 | [$params['catId'], time(), $params['noteId'], $request->auth()->userId()] 112 | )->then(function () { 113 | return $this->response->json([ 114 | 'status' => true, 115 | 'message' => 'Note moved successfully.' 116 | ]); 117 | })->otherwise(function (Throwable $throwable) { 118 | return $this->response->json([ 119 | 'status' => false, 120 | 'message' => 'Moving failed' 121 | ]); 122 | }); 123 | } 124 | 125 | public function delete(Request $request, array $params): PromiseInterface 126 | { 127 | return Connection::get()->query('DELETE FROM notes WHERE id = ? AND user_id = ?', [$params['id'], $request->auth()->userId()]) 128 | ->then(function (Result $result) { 129 | return $this->response->json([ 130 | 'status' => true, 131 | ]); 132 | }) 133 | ->otherwise(function (Throwable $throwable) { 134 | return $this->response->json([ 135 | 'status' => false, 136 | 'data' => $throwable, 137 | ]); 138 | }); 139 | } 140 | } -------------------------------------------------------------------------------- /app/Http/Controllers/User/SettingsController.php: -------------------------------------------------------------------------------- 1 | response->view('user/settings/index'); 25 | } 26 | 27 | public function showChangePasswordForm(): Response 28 | { 29 | return $this->response->view('user/settings/change-password'); 30 | } 31 | 32 | public function doChangePassword(Request $request): ResponseInterface 33 | { 34 | $oldPassword = $request->getParsedBody()['old_password']; 35 | $newPassword = $request->getParsedBody()['new_password']; 36 | $confirmPassword = $request->getParsedBody()['confirm_password']; 37 | 38 | $validateOldPass = $this->validatePasswordLength($oldPassword, 'Old password'); 39 | $validateNewPass = $this->validatePasswordLength($newPassword, 'New password'); 40 | 41 | if (true !== $validateOldPass) { 42 | return $validateOldPass; 43 | } 44 | 45 | if (true !== $validateNewPass) { 46 | return $validateNewPass; 47 | } 48 | 49 | if ($newPassword !== $confirmPassword) { 50 | return $this->response->jsonError('New password and confirm password must be same value.'); 51 | } 52 | 53 | $userId = $request->auth()->userId(); 54 | return Connection::get()->query('SELECT password FROM users WHERE id = ?', [$userId]) 55 | ->then(function (Result $result) use ($userId, $newPassword, $oldPassword) { 56 | //Check if the provided old password match current one 57 | if (password_verify($oldPassword, $result->rows[0]['password'])) { 58 | $hashedNewPassword = password_hash($newPassword, PASSWORD_DEFAULT); 59 | return Connection::get()->query('UPDATE users SET password = ? WHERE id = ?', [$hashedNewPassword, $userId]) 60 | ->then(function () { 61 | return $this->response->jsonSuccessMessage('Password changed successfully.'); 62 | }) 63 | ->otherwise(function () { 64 | return $this->response->jsonError('Failed to verify old password.'); 65 | }); 66 | } 67 | 68 | return $this->response->jsonError('Old password is incorrect.'); 69 | }) 70 | ->otherwise(function () { 71 | return $this->response->jsonError('Failed to change password.'); 72 | }); 73 | } 74 | 75 | /** 76 | * @param string $password 77 | * @param string $inputName 78 | * @return ResponseInterface|bool 79 | */ 80 | private function validatePasswordLength(string $password, string $inputName): bool|ResponseInterface 81 | { 82 | if (strlen($password) < 4) { 83 | return $this->response->jsonError("{$inputName} length must be at least 4 characters"); 84 | } 85 | 86 | if (strlen($password) > 99) { 87 | return $this->response->jsonError("{$inputName} length must be lower than 99 characters"); 88 | } 89 | 90 | return true; 91 | } 92 | } -------------------------------------------------------------------------------- /app/Http/Controllers/User/UserController.php: -------------------------------------------------------------------------------- 1 | query('SELECT * FROM users WHERE id = ?', [$params['id']]) 21 | ->then(function (Result $result) { 22 | $userData = $result->rows[0]; 23 | 24 | //Remove sensitive data 25 | unset($userData['password']); 26 | unset($userData['token']); 27 | 28 | return $this->response->json([ 29 | 'status' => true, 30 | 'data' => $userData 31 | ]); 32 | }) 33 | ->otherwise(function (Throwable $exception) { 34 | return $this->response->json([ 35 | 'status' => false, 36 | 'error' => $exception 37 | ]); 38 | }); 39 | } 40 | 41 | public function profile(): Response 42 | { 43 | return $this->response->view('user/profile'); 44 | } 45 | } -------------------------------------------------------------------------------- /app/Kernel.php: -------------------------------------------------------------------------------- 1 | [ 17 | Server::class, 18 | ], 19 | 20 | 'socket' => [ 21 | AdminServer::class, 22 | PublicChatServer::class, 23 | PrivateChatServer::class, 24 | ] 25 | ]; 26 | 27 | protected static array $middlewares = []; 28 | 29 | protected static array $middlewareGroups = [ 30 | 'web' => [ 31 | 32 | ], 33 | 34 | 'api' => [ 35 | 36 | ], 37 | 38 | 'socket' => [ 39 | 40 | ], 41 | ]; 42 | 43 | protected static array $RouteMiddlewares = [ 44 | 'auth' => HttpAuthMiddleware::class 45 | ]; 46 | 47 | protected static array $colisMiddlewares = [ 48 | 'auth' => SocketAuthMiddleware::class 49 | ]; 50 | } -------------------------------------------------------------------------------- /app/Models/Model.php: -------------------------------------------------------------------------------- 1 | database = Connection::create(); 25 | } 26 | 27 | public static function populateModel(array $user): void 28 | { 29 | self::$user = $user; 30 | 31 | foreach ($user as $item => $value) { 32 | self::$$item = $value; 33 | } 34 | } 35 | 36 | /** 37 | * Insert new record to database 38 | * @param array $data 39 | * @return PromiseInterface 40 | */ 41 | public function create(array $data): PromiseInterface 42 | { 43 | $columns = $this->implode($data); 44 | $values = $this->implode($data, true); 45 | $query = "INSERT INTO {$this->table}({$columns}) VALUES ({$values});"; 46 | return $this->database->query($query); 47 | } 48 | 49 | protected function implode(array $data, bool $isValue = false): string 50 | { 51 | if ($isValue) { 52 | $values = array_values($data); 53 | return "'" . implode("', '", $values) . "'"; 54 | } 55 | 56 | $columns = array_keys($data); 57 | return implode(', ', $columns); 58 | } 59 | 60 | public function select(string ...$arguments): Model 61 | { 62 | $this->selectColumns = $arguments; 63 | return $this; 64 | } 65 | 66 | /** 67 | * @param string|array $key 68 | * @param string|null $value 69 | * @return $this 70 | */ 71 | public function where($key, ?string $value = null): Model 72 | { 73 | if (is_array($key)) { 74 | $this->whereValues = array_merge($this->whereValues, $key); 75 | } else { 76 | $this->whereValues[$key] = $value; 77 | } 78 | 79 | return $this; 80 | } 81 | 82 | public function get(): PromiseInterface 83 | { 84 | $selectKeys = $this->implode($this->selectColumns, true); 85 | //$whereKeys = $this->implode($this->whereValues); 86 | $plainSQL = "SELECT {$selectKeys} FROM {$this->table}"; 87 | $hasWhere = false; 88 | if (count($this->whereValues) > 0) { 89 | $hasWhere = true; 90 | $plainSQL .= " WHERE "; 91 | foreach ($this->whereValues as $whereKey => $whereValue) { 92 | $plainSQL .= "{$whereKey} = ?, "; 93 | } 94 | $plainSQL = substr($plainSQL, 0, strlen($plainSQL) - 2); 95 | } 96 | 97 | var_dump($plainSQL); 98 | 99 | if ($hasWhere) { 100 | return $this->query($plainSQL, array_values($this->whereValues)); 101 | } 102 | 103 | return $this->query($plainSQL); 104 | } 105 | 106 | /** 107 | * Execute sql query on current database connection 108 | * @param string $query 109 | * @param array $bindValue 110 | * @return PromiseInterface 111 | */ 112 | public function query(string $query, array $bindValue = []): PromiseInterface 113 | { 114 | if (count($bindValue) > 0) { 115 | return $this->database->query($query, $bindValue); 116 | } 117 | 118 | return $this->database->query($query); 119 | } 120 | 121 | /** 122 | * Execute sql query on current database connection 123 | * @param string $query 124 | * @param array $bindValue 125 | * @return PromiseInterface 126 | */ 127 | public function execute(string $query, array $bindValue = []): PromiseInterface 128 | { 129 | if (count($bindValue) > 0) { 130 | return $this->database->query($query, $bindValue); 131 | } 132 | 133 | return $this->database->query($query); 134 | } 135 | 136 | /** 137 | * Get current database connection 138 | * @return DatabaseInterface|LazyDatabase 139 | */ 140 | public function getDatabase() 141 | { 142 | return $this->database; 143 | } 144 | } -------------------------------------------------------------------------------- /app/Models/Note.php: -------------------------------------------------------------------------------- 1 | query('UPDATE users SET token = ? WHERE id = ?', [$token, $userId]); 29 | } 30 | } -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | on('chat.public.removeUser', [ChatListener::class, 'removeUser']); 17 | 18 | //Remove private chat clients when they are offline 19 | // event()->on('chat.private.user-left', [Model::class, 'remove']); 20 | } 21 | } -------------------------------------------------------------------------------- /app/Providers/HttpServiceProvider.php: -------------------------------------------------------------------------------- 1 | queryList() 28 | ->find('meta') 29 | ->each(function (Elements $element) use (&$metaTags) { 30 | $attrName = $element->attr('name'); 31 | $attrProperty = $element->attr('property'); 32 | 33 | if (!empty($attrName)) { 34 | $metaTags[$attrName] = self::mayHoldUrl($attrName) 35 | ? self::readyUrl($element->attr('content')) 36 | : $element->attr('content'); 37 | } 38 | 39 | if (!empty($attrProperty)) { 40 | $metaTags[$attrProperty] = self::mayHoldUrl($attrProperty) 41 | ? self::readyUrl($element->attr('content')) 42 | : $element->attr('content'); 43 | } 44 | }); 45 | 46 | $metaTags['displayable'][] = self::readyUrl(self::findDisplayableImage($queryable) ?? ''); 47 | 48 | return $metaTags; 49 | } 50 | 51 | public static function findDisplayableImage(Queryable $queryable): ?string 52 | { 53 | return $queryable->queryList() 54 | ->find('link[as="image"]') 55 | ->eq(0) 56 | ->attr('href'); 57 | } 58 | 59 | protected static function readyUrl(string $url): string 60 | { 61 | if (empty($url)) return $url; 62 | 63 | if ('//' == substr($url, 0, 2)) { 64 | $url = substr($url, 2); 65 | } 66 | 67 | if ( 68 | false == strstr($url, 'http://') 69 | && false == strstr($url, 'https://') 70 | && false == strstr($url, 'www.') 71 | ) { 72 | $url = 'http://' . $url; 73 | } 74 | 75 | return $url; 76 | } 77 | } -------------------------------------------------------------------------------- /app/Servers/Http/Server.php: -------------------------------------------------------------------------------- 1 | then(function ($finalReturn) use ($deferred, $request) { 47 | 48 | $html = ob_get_contents(); 49 | 50 | ob_end_clean(); 51 | 52 | if ($finalReturn) { 53 | $deferred->resolve($this->generateProperResponse($request, $finalReturn)); 54 | return; 55 | } 56 | 57 | if ($html) { 58 | $deferred->resolve($this->generateProperResponse($request, $request->getResponse()->ok($html))); 59 | return; 60 | } 61 | 62 | $deferred->resolve($request->getResponse()->internalServerError('Something went wrong and no response is returned')); 63 | 64 | })->otherwise(function (Throwable $exception) use ($deferred, $request) { 65 | handleApplicationException($exception); 66 | $deferred->resolve($request->getResponse()->internalServerError($exception)); 67 | }); 68 | } else { 69 | $html = ob_get_contents(); 70 | ob_end_clean(); 71 | 72 | if ($html) { 73 | $response = $request->getResponse()->ok($html); 74 | } else { 75 | $response = $this->generateProperResponse($request, $response); 76 | } 77 | 78 | $deferred->resolve($response); 79 | } 80 | 81 | return $deferred->promise(); 82 | } 83 | 84 | /** 85 | * @param Request $request 86 | * @param mixed $response 87 | * @return Response 88 | */ 89 | public function generateProperResponse(Request $request, $response): Response 90 | { 91 | if ($response instanceof PromiseInterface) { 92 | return $response->then(function ($returnedResponse) use ($request) { 93 | return $this->generateProperResponse($request, $returnedResponse); 94 | })->otherwise(function ($returnedResponse) use ($request) { 95 | return $this->generateProperResponse($request, $returnedResponse); 96 | }); 97 | } elseif (!$response instanceof Response) { 98 | //Let's see if object is callable 99 | if (is_callable($response)) { 100 | return $this->generateProperResponse($request, $response()); 101 | } //Since object is not callable, let's figure out a way to handle it 102 | else { 103 | //if object can be used as string 104 | switch ($response) { 105 | case ($response instanceof Throwable): 106 | if ($_ENV['APP_ENVIRONMENT'] == 'development') { 107 | $response = $request->getResponse()->ok($response); 108 | } else { 109 | $response = $request->getResponse()->internalServerError('Server returns an unexpected response, please check server logs'); 110 | //handleApplicationException($response); 111 | } 112 | break; 113 | case ( 114 | is_string($response) || 115 | is_int($response) || 116 | is_float($response) || 117 | is_double($response) || 118 | is_bool($response) 119 | ): 120 | $response = $request->getResponse()->ok($response); 121 | break; 122 | case (is_array($response)): 123 | $response = $request->getResponse()->json($response); 124 | break; 125 | default: 126 | $briefLogNAme = 'logs/http-response-' . date('d_m_Y-H_i_s') . '.log'; 127 | $responseLogFile = root_path('storage/' . $briefLogNAme); 128 | $message = "Server returns an unexpected response.\n Please check {$responseLogFile}."; 129 | file_put_contents($responseLogFile, serialize($response)); 130 | handleApplicationException(new Exception($message)); 131 | $response = $request->getResponse()->internalServerError($message); 132 | break; 133 | } 134 | } 135 | } 136 | 137 | return $response; 138 | } 139 | 140 | } -------------------------------------------------------------------------------- /app/Servers/Websocket/AdminServer.php: -------------------------------------------------------------------------------- 1 | colis = $colis; 26 | } 27 | 28 | /** 29 | * @inheritDoc 30 | */ 31 | public function onMessage(ConnectionInterface $connection, Payload $payload): void 32 | { 33 | $request = new Request([ 34 | 'colis' => $this->colis, 35 | 'client' => $connection, 36 | 'message' => $payload, 37 | 'payload' => $payload, 38 | ]); 39 | 40 | /** 41 | * Check if sent command matches any provided listeners 42 | * If its available the command class will be executed 43 | */ 44 | Matcher::match($request); 45 | } 46 | 47 | /** 48 | * @inheritDoc 49 | */ 50 | public function onOpen(ConnectionInterface $connection): void 51 | { 52 | console(true)->info('Admin connection opened'); 53 | } 54 | 55 | /** 56 | * @inheritDoc 57 | */ 58 | public function onClose(ConnectionInterface $connection): void 59 | { 60 | console(true)->comment('Admin connection closed'); 61 | } 62 | 63 | /** 64 | * @inheritDoc 65 | */ 66 | public function onError(ConnectionInterface $connection, Throwable $exception): void 67 | { 68 | // TODO: Implement onError() method. 69 | } 70 | } -------------------------------------------------------------------------------- /app/Servers/Websocket/PrivateChatServer.php: -------------------------------------------------------------------------------- 1 | prefix = $_ENV['PRIVATE_CHAT_SOCKET_URL_PREFIX']; 24 | } 25 | 26 | /** 27 | * @inheritDoc 28 | */ 29 | public function onMessage(ConnectionInterface $connection, Payload $payload): void 30 | { 31 | Auth::handle($payload->token)->then(function (Auth $auth) use ($connection, $payload) { 32 | $request = Request::init([ 33 | 'colis' => $this->colis, 34 | 'client' => $connection, 35 | 'message' => $payload, 36 | 'payload' => $payload, 37 | 'auth' => $auth 38 | ]); 39 | 40 | Dispatcher::dispatch($request); 41 | }); 42 | } 43 | 44 | /** 45 | * @inheritDoc 46 | */ 47 | public function onOpen(ConnectionInterface $connection): void 48 | { 49 | console(true)->info('New private chat connection: ' . $connection->getConnectionId()); 50 | } 51 | 52 | /** 53 | * @inheritDoc 54 | */ 55 | public function onClose(ConnectionInterface $connection): void 56 | { 57 | event()->emit('chat.private.user-left', [$connection]); 58 | console(true)->comment('Closed private chat connection: ' . $connection->getConnectionId()); 59 | } 60 | 61 | /** 62 | * @inheritDoc 63 | */ 64 | public function onError(ConnectionInterface $connection, Throwable $exception): void 65 | { 66 | event()->emit('chat.private.user-left', [$connection]); 67 | } 68 | } -------------------------------------------------------------------------------- /app/Websocket/Clients.php: -------------------------------------------------------------------------------- 1 | client()->getConnectionId()] = $request->client(); 22 | } 23 | 24 | /** 25 | * @param int $connId 26 | * @return bool 27 | */ 28 | public static function exists(int $connId): bool 29 | { 30 | return array_key_exists($connId, self::$clients); 31 | } 32 | 33 | /** 34 | * @param int $connId 35 | * @return ConnectionInterface|null 36 | */ 37 | public static function get(int $connId): ?ConnectionInterface 38 | { 39 | return self::$clients[$connId] ?? null; 40 | } 41 | } -------------------------------------------------------------------------------- /app/Websocket/Listeners/Chat/PrivateChat/ChatListener.php: -------------------------------------------------------------------------------- 1 | typingStatusTimeout = $_ENV['PRIVATE_CHAT_TYPING_STATUS_TIMEOUT']; 30 | } 31 | 32 | 33 | public function iamOnline(Request $request): void 34 | { 35 | //Add to online list 36 | Clients::add($request); 37 | 38 | //Let his trackers know he's online 39 | UserPresence::add( 40 | connId: $request->client()->getConnectionId(), 41 | userId: $request->auth()->userId() 42 | ); 43 | } 44 | 45 | public function monitorUsersPresence(Request $request): void 46 | { 47 | $message = $request->payload()->message; 48 | $users = $message->users ?? []; 49 | 50 | foreach ($users as $userTrackingData) { 51 | if (isset($userTrackingData->user_id)) { 52 | UserPresence::track( 53 | $userTrackingData->user_id, 54 | function ($trackedUserPresence, $trackedUserId) use ($request) { 55 | $command = 'chat.private.offline'; 56 | if ('online' == $trackedUserPresence) { 57 | $command = 'chat.private.online'; 58 | } 59 | 60 | resp($request->client())->send($command, [ 61 | 'user_id' => $trackedUserId 62 | ]); 63 | } 64 | ); 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * @param Request $request 71 | * @return bool|PromiseInterface 72 | */ 73 | public function send(Request $request): PromiseInterface|bool 74 | { 75 | $userId = $request->auth()->userId(); 76 | $payload = $request->payload(); 77 | $receiverId = $payload->message->receiver_id; 78 | 79 | if (empty(trim($payload->message->message))) { 80 | return true; 81 | } 82 | 83 | $plainSql = 'SELECT conversers FROM messages WHERE (sender_id = ? AND receiver_id =?) OR (sender_id = ? AND receiver_id = ?)'; 84 | return Connection::get()->query($plainSql, [$userId, $receiverId, $receiverId, $userId]) 85 | ->then(function (Result $result) use ($userId, $payload, $request) { 86 | if (!empty($result->rows)) { 87 | $conversers = $result->rows[0]['conversers']; 88 | } else { 89 | $conversers = "{$userId} {$payload->message->receiver_id}"; 90 | } 91 | 92 | //Send Message 93 | $sql = "INSERT INTO messages(sender_id, receiver_id, message, conversers) VALUES (?, ?, ?, ?)"; 94 | $userId = $request->auth()->userId(); 95 | return Connection::get()->query($sql, [$userId, $payload->message->receiver_id, $payload->message->message, $conversers]) 96 | ->then(function (Result $result) use ($payload, $request) { 97 | if (UserPresence::isOnline($payload->message->receiver_id)) { 98 | $client = UserPresence::getConnection($payload->message->receiver_id); 99 | resp($client)->send('chat.private.send', [ 100 | 'id' => $result->insertId, 101 | 'client_id' => $client->getConnectionId(), 102 | 'sender_id' => $request->auth()->userId(), 103 | 'time' => time(), 104 | 'message' => $payload->message->message, 105 | ]); 106 | } 107 | })->otherwise(function (Throwable $throwable) use ($request) { 108 | resp($request->client())->send('chat.private.error', $throwable); 109 | }); 110 | }); 111 | } 112 | 113 | public function typing(Request $request): void 114 | { 115 | $userId = $request->auth()->userId(); 116 | $payload = $request->payload(); 117 | $receiverId = $payload->message->receiver_id; 118 | 119 | if (UserPresence::isOnline($receiverId)) { 120 | 121 | $client = UserPresence::getConnection($receiverId); 122 | 123 | $data = [ 124 | 'client_id' => $client->getConnectionId(), 125 | 'sender_id' => $userId, 126 | 'status' => 'typing', 127 | 'timeout' => $this->typingStatusTimeout, 128 | ]; 129 | 130 | //Let's see if user is typing or stopped typing 131 | if ($request->payload()->message->status !== 'typing') { 132 | $data['status'] = 'stopped'; 133 | } 134 | 135 | resp($client)->send('chat.private.typing', $data); 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /app/Websocket/Listeners/Chat/PublicChat/ChatListener.php: -------------------------------------------------------------------------------- 1 | removeUser($request->client()); 21 | } 22 | 23 | public static function removeUser(ConnectionInterface $client): void 24 | { 25 | $storedClient = chatClients()[$client->getConnectionId()] ?? null; 26 | if ($storedClient) { 27 | self::sendToAll($client, [ 28 | 'command' => 'chat.public.left', 29 | 'data' => [ 30 | 'client_id' => $client->getConnectionId(), 31 | 'name' => $storedClient['name'], 32 | ] 33 | ]); 34 | 35 | //Remove client from list of chat clients 36 | unset(chatClients()[$client->getConnectionId()]); 37 | 38 | console()->write("\n[#] {$storedClient['name']}({$client->getConnectionId()}) left {$storedClient['room']}.", 'light_yellow'); 39 | } 40 | } 41 | 42 | protected static function sendToAll(ConnectionInterface $currentClient, array $message): void 43 | { 44 | $storedClient = chatClients()[$currentClient->getConnectionId()]; 45 | 46 | if ($storedClient) { 47 | $clientRoom = $storedClient['room']; 48 | 49 | $roomClients = chatRooms($clientRoom); 50 | 51 | foreach ($roomClients as $roomClient) { 52 | if ($roomClient !== $currentClient) { 53 | resp($roomClient)->send($message['command'], $message['data']); 54 | } 55 | } 56 | } 57 | } 58 | 59 | public function join(Request $request): void 60 | { 61 | $client = $request->client(); 62 | /**@var Payload|stdClass $message ;* */ 63 | $message = $request->payload()->message; 64 | 65 | console()->write("\n[#] {$message->name}({$client->getConnectionId()}) joined {$message->room}.", 'yellow'); 66 | 67 | Room::send($message->room, 'chat.public.user-joined', [ 68 | [ 69 | 'client_id' => $client->getConnectionId(), 70 | 'name' => $message->name, 71 | ] 72 | ]); 73 | 74 | //Send list of connected clients to connected user 75 | $roomPeople = []; 76 | foreach (Room::all($message->room) as $chatClient) { 77 | $theClient = Client::get($chatClient->getConnectionId()); 78 | 79 | if ($theClient) { 80 | $roomPeople[] = [ 81 | 'client_id' => $chatClient->getConnectionId(), 82 | 'name' => $theClient['name'], 83 | ]; 84 | } 85 | } 86 | 87 | //Add client to the clients list 88 | $this->storeClient($request); 89 | 90 | //Notify user that he joined the requested group 91 | Response::push($client, 'chat.public.joined'); 92 | 93 | //Send user list of users in current group 94 | Response::push($client, 'chat.public.user-joined', $roomPeople); 95 | } 96 | 97 | protected function storeClient(Request $request): void 98 | { 99 | $client = $request->client(); 100 | $message = $request->payload()->message; 101 | 102 | chatClients($client, [ 103 | 'name' => $message->name, 104 | 'room' => $message->room, 105 | ]); 106 | 107 | chatRooms($message->room, $client); 108 | } 109 | 110 | public function send(Request $request): void 111 | { 112 | $message = $request->payload()->message; 113 | $client = $request->client(); 114 | 115 | $storedClient = chatClients()[$client->getConnectionId()]; 116 | 117 | if ($storedClient) { 118 | self::sendToAll($client, [ 119 | 'command' => 'chat.public.send', 120 | 'data' => [ 121 | 'user' => $storedClient['name'], 122 | 'client_id' => $client->getConnectionId(), 123 | 'message' => $message 124 | ], 125 | ]); 126 | } 127 | } 128 | 129 | public function typing(Request $request): void 130 | { 131 | $client = $request->client(); 132 | 133 | $storedClient = chatClients()[$client->getConnectionId()]; 134 | 135 | if ($storedClient) { 136 | 137 | $data = [ 138 | 'client_id' => $client->getConnectionId(), 139 | 'user' => $storedClient['name'], 140 | 'status' => 'typing', 141 | 'timeout' => $this->userTypingTimeout, 142 | ]; 143 | 144 | //Let's see if user is typing or stopped typing 145 | if ($request->payload()->message->status !== 'typing') { 146 | $data['status'] = 'stopped'; 147 | } 148 | 149 | self::sendToAll($client, [ 150 | 'command' => 'chat.public.typing', 151 | 'data' => $data, 152 | ]); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /app/Websocket/Listeners/Listener.php: -------------------------------------------------------------------------------- 1 | $object) { 26 | $this->$objectName = $object; 27 | } 28 | 29 | return $this; 30 | } 31 | } -------------------------------------------------------------------------------- /app/Websocket/Listeners/MainListener.php: -------------------------------------------------------------------------------- 1 | request->payload()->message ?? null; 12 | if ($message) { 13 | $message = strtoupper($message); 14 | } else { 15 | $message = 'Hi, welcome to ReactPHP\'s world of awesomeness.'; 16 | } 17 | 18 | resp($this->client)->send('hail.reactphp', $message); 19 | } 20 | } -------------------------------------------------------------------------------- /app/Websocket/Listeners/Server/Admin/Config/EnvironmentListener.php: -------------------------------------------------------------------------------- 1 | listeners = [ 17 | 'get' => 'get', 18 | 'get-all' => 'getAll', 19 | 'set' => 'set', 20 | 'update' => 'update', 21 | 'delete' => 'delete', 22 | 'list-commands' => 'listCommands', 23 | ]; 24 | } 25 | 26 | public function __invoke(Request $request): bool 27 | { 28 | return call_user_func([$this, $this->listeners[$request->payload()->message->action]], $request); 29 | } 30 | 31 | public function listCommands(Request $request): void 32 | { 33 | resp($request->client())->send( 34 | $this->responseResultCommand, 35 | array_keys($this->listeners) 36 | ); 37 | } 38 | 39 | public function get(Request $request): void 40 | { 41 | $client = $request->client(); 42 | $message = $request->payload(); 43 | 44 | resp($client)->send( 45 | $this->responseResultCommand, 46 | $_ENV[$message->name] ?? null 47 | ); 48 | } 49 | 50 | public function getAll(Request $request): void 51 | { 52 | resp($request->client())->send( 53 | $this->responseResultCommand, 54 | $_ENV 55 | ); 56 | } 57 | 58 | public function set(Request $request): void 59 | { 60 | $client = $request->client(); 61 | $message = $request->payload(); 62 | 63 | if ($_ENV[$message->name]) { 64 | resp($client)->send( 65 | $this->responseResultCommand, 66 | "Environment variable \"{$message->name} already exist, you can either update or delete only.\"." 67 | ); 68 | return; 69 | } 70 | 71 | $_ENV[$message->name] = $message->value; 72 | 73 | resp($client)->send( 74 | $this->responseResultCommand, 75 | 'Variable has been set.' 76 | ); 77 | } 78 | 79 | public function update(Request $request): void 80 | { 81 | $client = $request->client(); 82 | $message = $request->payload(); 83 | 84 | if (!$_ENV[$message->name]) { 85 | resp($client)->send( 86 | $this->responseResultCommand, 87 | "Environment variable \"{$message->name}\" does not exits, create it first." 88 | ); 89 | return; 90 | } 91 | 92 | $_ENV[$message->name] = $message->value; 93 | 94 | resp($client)->send( 95 | 'server.admin.config.env.update.result', 96 | 'Variable has been updated.' 97 | ); 98 | } 99 | 100 | public function delete(Request $request): void 101 | { 102 | $client = $request->client(); 103 | $message = $request->payload(); 104 | 105 | if (!$_ENV[$message->name]) { 106 | resp($client)->send( 107 | $this->responseResultCommand, 108 | "Environment variable \"{$message->name}\" does not exits, create it first." 109 | ); 110 | return; 111 | } 112 | 113 | unset($_ENV[$message->name]); 114 | 115 | resp($client)->send( 116 | $this->responseResultCommand, 117 | 'Variable has been set.' 118 | ); 119 | } 120 | } -------------------------------------------------------------------------------- /app/Websocket/Listeners/SystemListener.php: -------------------------------------------------------------------------------- 1 | client); 12 | 13 | resp($this->client)->send('message', 'Ping received.'); 14 | } 15 | 16 | public function pong(): void 17 | { 18 | event()->emit('system.pong', [$this->client]); 19 | } 20 | } -------------------------------------------------------------------------------- /app/Websocket/Models/Client.php: -------------------------------------------------------------------------------- 1 | getConnectionId()] = $connection; 25 | } 26 | 27 | /** 28 | * Check if client exists 29 | * @param int $userId 30 | * @return bool 31 | */ 32 | public static function exists(int $userId): bool 33 | { 34 | return array_key_exists($userId, static::$clients); 35 | } 36 | 37 | /** 38 | * Get client 39 | * @param int $userId 40 | * @return ConnectionInterface|null 41 | */ 42 | public static function get(int $userId): ?ConnectionInterface 43 | { 44 | return static::$clients[$userId] ?? null; 45 | } 46 | 47 | /** 48 | * Get all clients 49 | * @return ConnectionInterface[] 50 | */ 51 | public static function getAll(): array 52 | { 53 | return static::$clients; 54 | } 55 | 56 | /** 57 | * Send message to all clients 58 | * 59 | * @param array $payload 60 | * @return void 61 | */ 62 | public static function send(array $payload): void 63 | { 64 | foreach (static::$clients as $client) { 65 | Response::push($client, $payload['command'], $payload['data']); 66 | } 67 | } 68 | 69 | /** 70 | * Remove client from online list 71 | * @param ConnectionInterface $connection 72 | * @param callable|null $callback 73 | */ 74 | public static function remove(ConnectionInterface $connection, ?callable $callback = null): void 75 | { 76 | foreach (static::$clients as $userId => $client) { 77 | if ($client === $connection) { 78 | unset(static::$clients[$userId]); 79 | 80 | //Now, let's notify that the user is offline 81 | if ($callback) $callback(); 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /app/Websocket/Room.php: -------------------------------------------------------------------------------- 1 | 36 | */ 37 | public static function getRooms(): array 38 | { 39 | return self::$rooms; 40 | } 41 | } -------------------------------------------------------------------------------- /app/Websocket/UserPresence.php: -------------------------------------------------------------------------------- 1 | on('chat.private.user-left', function (ConnectionInterface $connection) { 25 | foreach (self::$users as $userId => $connId) { 26 | if ($connId == $connection->getConnectionId()) { 27 | self::remove($userId); 28 | break; 29 | } 30 | } 31 | 32 | console(true)->comment('private connection closed: ' . $connection->getConnectionId()); 33 | }); 34 | } 35 | 36 | public static function add(int $connId, int $userId): void 37 | { 38 | self::$users[$userId] = $connId; 39 | self::$emitter->emit("user.online.$userId", [$userId]); 40 | } 41 | 42 | public static function remove(int $userId): void 43 | { 44 | unset(self::$users[$userId]); 45 | self::$emitter->emit("user.offline.$userId", [$userId]); 46 | } 47 | 48 | public static function get(int $userId): ?int 49 | { 50 | return self::$users[$userId] ?? null; 51 | } 52 | 53 | public static function isOnline(int $userId): bool 54 | { 55 | return array_key_exists($userId, self::$users); 56 | } 57 | 58 | public static function getConnection(int $userId): ?ConnectionInterface 59 | { 60 | $connId = self::get($userId); 61 | return $connId ? Clients::get($connId) : null; 62 | } 63 | 64 | public static function track(int $userId, callable $callback): void 65 | { 66 | self::$emitter->on("user.online.$userId", fn(int $user) => $callback('online', $userId)); 67 | self::$emitter->on("user.offline.$userId", fn(int $user) => $callback('offline', $userId)); 68 | } 69 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ahmard/reactphp-live-chat", 3 | "description": "A PHP-based live chat system written on top of Ratchet and ReactPHP", 4 | "type": "project", 5 | "license": "MIT", 6 | "require": { 7 | "php": "^8.0", 8 | "ext-json": "*", 9 | "ext-fileinfo": "*", 10 | "cboden/ratchet": "^0.4.4", 11 | "kevinlebrun/colors.php": "^1.0", 12 | "react/http": "^1.8", 13 | "react/filesystem": "^0.1.2", 14 | "voryx/websocketmiddleware": "^2.0", 15 | "vlucas/phpdotenv": "^5.5", 16 | "symfony/console": "^6.2", 17 | "clue/reactphp-sqlite": "^1.5", 18 | "symfony/validator": "^6.2", 19 | "wyrihaximus/react-http-middleware-webroot-preload": "^2.3", 20 | "firebase/php-jwt": "^6.3", 21 | "ahmard/quick-route": "^3.9", 22 | "nesbot/carbon": "^2.64", 23 | "ahmard/reactphp-querylist": "^0.0.2" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "App\\": "app/", 28 | "Server\\": "src/", 29 | "Database\\": "database/" 30 | } 31 | }, 32 | "scripts": { 33 | "post-autoload-dump": [ 34 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"", 35 | "@php -r \"file_exists('storage/cache') || mkdir('storage/cache');\"", 36 | "@php -r \"file_exists('storage/cache/route') || mkdir('storage/cache/route');\"", 37 | "@php -r \"file_exists('storage/logs') || mkdir('storage/logs');\"" 38 | ], 39 | "analyse": "phpstan analyse", 40 | "analyze": "@analyse" 41 | }, 42 | "require-dev": { 43 | "phpstan/phpstan": "^1.9", 44 | "symfony/var-dumper": "^6.2" 45 | }, 46 | "config": { 47 | "allow-plugins": { 48 | "wyrihaximus/composer-update-bin-autoload-path": true 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /config/app.php: -------------------------------------------------------------------------------- 1 | 'Admin', 18 | 'email' => 'admin@chat.test', 19 | 'type' => 'admin', 20 | 'password' => password_hash(1234, PASSWORD_DEFAULT) 21 | ], 22 | [ 23 | 'username' => 'Ahmard', 24 | 'email' => 'ahmard@chat.test', 25 | 'type' => 'user', 26 | 'password' => password_hash(1234, PASSWORD_DEFAULT) 27 | ], 28 | [ 29 | 'username' => 'Anonymous', 30 | 'email' => 'anonymous@chat.test', 31 | 'type' => 'admin', 32 | 'password' => password_hash(1234, PASSWORD_DEFAULT) 33 | ] 34 | ]; 35 | 36 | foreach ($seeds as $seed){ 37 | Connection::get() 38 | ->query('INSERT INTO users(username, email, type, password) VALUES (?, ?, ?, ?)', array_values($seed)) 39 | ->otherwise(function (\Throwable $throwable){ 40 | var_dump($throwable->getMessage()); 41 | }); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /database/migrations.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `users`; 2 | DROP TABLE IF EXISTS `messages`; 3 | DROP TABLE IF EXISTS `note_categories`; 4 | DROP TABLE IF EXISTS `notes`; 5 | DROP TABLE IF EXISTS `list_categories`; 6 | DROP TABLE IF EXISTS `lists`; 7 | DROP TABLE IF EXISTS `link_previews`; 8 | 9 | CREATE TABLE IF NOT EXISTS users 10 | ( 11 | id INTEGER PRIMARY KEY AUTOINCREMENT, 12 | username VARCHAR(30) NOT NULL, 13 | email VARCHAR(50) NOT NULL UNIQUE, 14 | password VARCHAR(100) NOT NULL, 15 | token VARCHAR(500) NULL, 16 | type VARCHAR(15) NOT NULL DEFAULT 'user', 17 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 18 | ); 19 | 20 | 21 | CREATE TABLE IF NOT EXISTS messages 22 | ( 23 | id INTEGER PRIMARY KEY AUTOINCREMENT, 24 | sender_id INTEGER NOT NULL, 25 | receiver_id INTEGER NOT NULL, 26 | message TEXT NOT NULL, 27 | conversers VARCHAR(100) NOT NULL, 28 | status INTEGER(1) DEFAULT 0, 29 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 30 | ); 31 | 32 | 33 | CREATE TABLE IF NOT EXISTS note_categories 34 | ( 35 | id INTEGER PRIMARY KEY AUTOINCREMENT, 36 | user_id INTEGER NOT NULL, 37 | name VARCHAR(250) NOT NULL, 38 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 39 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 40 | ); 41 | 42 | 43 | CREATE TABLE IF NOT EXISTS notes 44 | ( 45 | id INTEGER PRIMARY KEY AUTOINCREMENT, 46 | user_id INTEGER NOT NULL, 47 | category_id INTEGER NOT NULL, 48 | title VARCHAR(250) NOT NULL, 49 | note TEXT NOT NULL, 50 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 51 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 52 | ); 53 | 54 | 55 | CREATE TABLE IF NOT EXISTS list_categories 56 | ( 57 | id INTEGER PRIMARY KEY AUTOINCREMENT, 58 | user_id INTEGER NOT NULL, 59 | name VARCHAR(250) NOT NULL, 60 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 61 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 62 | ); 63 | 64 | 65 | CREATE TABLE IF NOT EXISTS lists 66 | ( 67 | id INTEGER PRIMARY KEY AUTOINCREMENT, 68 | user_id INTEGER NOT NULL, 69 | category_id INTEGER NOT NULL, 70 | content TEXT NOT NULL, 71 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 72 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 73 | ); 74 | 75 | 76 | CREATE TABLE IF NOT EXISTS link_previews 77 | ( 78 | id INTEGER PRIMARY KEY AUTOINCREMENT, 79 | href TEXT(3000) NOT NULL, 80 | meta TEXT NOT NULL, 81 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 82 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 83 | ); -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": false, 3 | "ignore": [ 4 | ".git", 5 | ".idea", 6 | "./vendor" 7 | ], 8 | "execMap": { 9 | "php": "php" 10 | }, 11 | "restartable": "r", 12 | "ext": "php" 13 | } 14 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 6 3 | paths: 4 | - react.php 5 | - app 6 | - src/Helpers/socketHelperFunctions.php 7 | - src/Helpers/generalHelperFunctions.php 8 | - src/Helpers/httpHelperFunctions.php 9 | checkMissingIterableValueType: false 10 | checkGenericClassInNonGenericObjectType: false 11 | ignoreErrors: 12 | - '#Call to an undefined method React\\Promise\\PromiseInterface::otherwise\(\)#' 13 | - '#Call to function is_double\(\) with mixed will always evaluate to false#' -------------------------------------------------------------------------------- /public/assets/css/site.message.css: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100%; 3 | margin: 0; 4 | background: #7F7FD5; 5 | background: -webkit-linear-gradient(toright, #91EAE4, #86A8E7, #7F7FD5); 6 | background: linear-gradient(toright, #91EAE4, #86A8E7, #7F7FD5); 7 | } 8 | 9 | .chat { 10 | margin-top: auto; 11 | margin-bottom: auto; 12 | } 13 | 14 | .card { 15 | height: 700px; 16 | border-radius: 15px !important; 17 | background-color: rgba(0, 0, 0, 0.4) !important; 18 | } 19 | 20 | .contacts_body { 21 | padding: 0.75rem 0 !important; 22 | overflow-y: auto; 23 | white-space: nowrap; 24 | } 25 | 26 | .msg_card_body { 27 | height: 500px; 28 | overflow-y: auto; 29 | white-space: nowrap; 30 | } 31 | 32 | .card-header { 33 | border-radius: 15px 15px 0 0 !important; 34 | border-bottom: 0 !important; 35 | } 36 | 37 | .card-footer { 38 | border-radius: 0 0 15px 15px !important; 39 | border-top: 0 !important; 40 | } 41 | 42 | .container { 43 | align-content: center; 44 | } 45 | 46 | .search { 47 | border-radius: 15px 0 0 15px !important; 48 | background-color: rgba(0, 0, 0, 0.3) !important; 49 | border: 0 !important; 50 | color: white !important; 51 | } 52 | 53 | .search:focus { 54 | box-shadow: none !important; 55 | outline: 0px !important; 56 | } 57 | 58 | .type_msg { 59 | background-color: rgba(0, 0, 0, 0.3) !important; 60 | border: 0 !important; 61 | color: white !important; 62 | height: 60px !important; 63 | overflow-y: auto; 64 | } 65 | 66 | .type_msg:focus { 67 | box-shadow: none !important; 68 | outline: 0px !important; 69 | } 70 | 71 | .attach_btn { 72 | border-radius: 15px 0 0 15px !important; 73 | background-color: rgba(0, 0, 0, 0.3) !important; 74 | border: 0 !important; 75 | color: white !important; 76 | cursor: pointer; 77 | } 78 | 79 | .send_btn { 80 | border-radius: 0 15px 15px 0 !important; 81 | background-color: rgba(0, 0, 0, 0.3) !important; 82 | border: 0 !important; 83 | color: white !important; 84 | cursor: pointer; 85 | } 86 | 87 | .search_btn { 88 | border-radius: 0 15px 15px 0 !important; 89 | background-color: rgba(0, 0, 0, 0.3) !important; 90 | border: 0 !important; 91 | color: white !important; 92 | cursor: pointer; 93 | } 94 | 95 | .contacts { 96 | list-style: none; 97 | padding: 0; 98 | } 99 | 100 | .contacts li { 101 | width: 100% !important; 102 | padding: 5px 10px; 103 | margin-bottom: 15px !important; 104 | } 105 | 106 | .m-active { 107 | background-color: rgba(0, 0, 0, 0.3); 108 | } 109 | 110 | .user_img { 111 | height: 70px; 112 | width: 70px; 113 | border: 1.5px solid #f5f6fa; 114 | 115 | } 116 | 117 | .user_img_msg { 118 | height: 40px; 119 | width: 40px; 120 | border: 1.5px solid #f5f6fa; 121 | 122 | } 123 | 124 | .img_cont { 125 | position: relative; 126 | height: 70px; 127 | width: 70px; 128 | } 129 | 130 | .img_cont_msg { 131 | height: 40px; 132 | width: 40px; 133 | } 134 | 135 | .online_icon { 136 | position: absolute; 137 | height: 15px; 138 | width: 15px; 139 | background-color: #4cd137; 140 | border-radius: 50%; 141 | bottom: 0.2em; 142 | right: 0.4em; 143 | border: 1.5px solid white; 144 | } 145 | 146 | .offline { 147 | background-color: #c23616 !important; 148 | } 149 | 150 | .user_info { 151 | margin-top: auto; 152 | margin-bottom: auto; 153 | margin-left: 15px; 154 | } 155 | 156 | .user_info span { 157 | font-size: 20px; 158 | color: white; 159 | } 160 | 161 | .user_info p { 162 | font-size: 10px; 163 | color: rgba(255, 255, 255, 0.6); 164 | } 165 | 166 | .video_cam { 167 | margin-left: 50px; 168 | margin-top: 5px; 169 | } 170 | 171 | .video_cam span { 172 | color: white; 173 | font-size: 20px; 174 | cursor: pointer; 175 | margin-right: 20px; 176 | } 177 | 178 | .msg_container { 179 | margin-top: auto; 180 | margin-bottom: auto; 181 | margin-left: 10px; 182 | border-radius: 25px; 183 | background-color: #82ccdd; 184 | padding: 10px; 185 | position: relative; 186 | } 187 | 188 | .msg_container_send { 189 | margin-top: auto; 190 | margin-bottom: auto; 191 | margin-right: 10px; 192 | border-radius: 25px; 193 | background-color: #78e08f; 194 | padding: 10px; 195 | position: relative; 196 | } 197 | 198 | .msg_time { 199 | position: absolute; 200 | left: 0; 201 | bottom: -15px; 202 | color: rgba(255, 255, 255, 0.5); 203 | font-size: 10px; 204 | } 205 | 206 | .msg_time_send { 207 | position: absolute; 208 | right: 0; 209 | bottom: -15px; 210 | color: rgba(255, 255, 255, 0.5); 211 | font-size: 10px; 212 | } 213 | 214 | .msg_head { 215 | position: relative; 216 | } 217 | 218 | #action_menu_btn { 219 | position: absolute; 220 | right: 10px; 221 | top: 10px; 222 | color: white; 223 | cursor: pointer; 224 | font-size: 20px; 225 | } 226 | 227 | .action_menu { 228 | z-index: 1; 229 | position: absolute; 230 | padding: 15px 0; 231 | background-color: rgba(0, 0, 0, 0.5); 232 | color: white; 233 | border-radius: 15px; 234 | top: 30px; 235 | right: 15px; 236 | display: none; 237 | } 238 | 239 | .action_menu ul { 240 | list-style: none; 241 | padding: 0; 242 | margin: 0; 243 | } 244 | 245 | .action_menu ul li { 246 | width: 100%; 247 | padding: 10px 15px; 248 | margin-bottom: 5px; 249 | } 250 | 251 | .action_menu ul li i { 252 | padding-right: 10px; 253 | 254 | } 255 | 256 | .action_menu ul li:hover { 257 | cursor: pointer; 258 | background-color: rgba(0, 0, 0, 0.2); 259 | } 260 | 261 | @media (max-width: 576px) { 262 | .contacts_card { 263 | margin-bottom: 15px !important; 264 | } 265 | } -------------------------------------------------------------------------------- /public/assets/css/style.css: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 1170px; 3 | margin: auto; 4 | } 5 | img { 6 | max-width: 100%; 7 | } 8 | .inbox_people { 9 | background: #f8f8f8 none repeat scroll 0 0; 10 | float: left; 11 | overflow: hidden; 12 | width: 40%; 13 | border-right: 1px solid #c4c4c4; 14 | } 15 | .inbox_msg { 16 | border: 1px solid #c4c4c4; 17 | clear: both; 18 | height: 400px; 19 | overflow: hidden; 20 | } 21 | .top_spac { 22 | margin: 20px 0 0; 23 | } 24 | 25 | 26 | .recent_heading { 27 | float: left; 28 | width: 40%; 29 | } 30 | .srch_bar { 31 | display: inline-block; 32 | text-align: right; 33 | width: 60%; 34 | } 35 | .headind_srch { 36 | padding: 10px 29px 10px 20px; 37 | overflow: hidden; 38 | border-bottom: 1px solid #c4c4c4; 39 | } 40 | 41 | .recent_heading h4 { 42 | color: #05728f; 43 | font-size: 21px; 44 | margin: auto; 45 | } 46 | .srch_bar input { 47 | border: 1px solid #cdcdcd; 48 | border-width: 0 0 1px 0; 49 | width: 80%; 50 | padding: 2px 0 4px 6px; 51 | background: none; 52 | } 53 | .srch_bar .input-group-addon button { 54 | background: rgba(0, 0, 0, 0) none repeat scroll 0 0; 55 | border: medium none; 56 | padding: 0; 57 | color: #707070; 58 | font-size: 18px; 59 | } 60 | .srch_bar .input-group-addon { 61 | margin: 0 0 0 -27px; 62 | } 63 | 64 | .chat_ib h5 { 65 | font-size: 15px; 66 | color: #464646; 67 | margin: 0 0 8px 0; 68 | } 69 | .chat_ib h5 span { 70 | font-size: 13px; 71 | float: right; 72 | } 73 | .chat_ib p { 74 | font-size: 14px; 75 | color: #989898; 76 | margin: auto 77 | } 78 | .chat_img { 79 | float: left; 80 | width: 11%; 81 | } 82 | .chat_ib { 83 | float: left; 84 | padding: 0 0 0 15px; 85 | width: 88%; 86 | } 87 | 88 | .chat_people { 89 | overflow: hidden; 90 | clear: both; 91 | } 92 | .chat_list { 93 | border-bottom: 1px solid #c4c4c4; 94 | margin: 0; 95 | padding: 18px 16px 10px; 96 | } 97 | .inbox_chat { 98 | height: 550px; 99 | overflow-y: scroll; 100 | } 101 | 102 | .active_chat { 103 | background: #ebebeb; 104 | } 105 | 106 | .incoming_msg_img { 107 | display: inline-block; 108 | width: 6%; 109 | } 110 | .received_msg { 111 | display: inline-block; 112 | padding: 0 0 0 10px; 113 | vertical-align: top; 114 | width: 92%; 115 | } 116 | .received_withd_msg p { 117 | background: #ebebeb none repeat scroll 0 0; 118 | border-radius: 3px; 119 | color: #646464; 120 | font-size: 14px; 121 | margin: 0; 122 | padding: 5px 10px 5px 12px; 123 | width: 100%; 124 | } 125 | .time_date { 126 | color: #747474; 127 | display: block; 128 | font-size: 12px; 129 | margin: 8px 0 0; 130 | } 131 | .received_withd_msg { 132 | width: 57%; 133 | } 134 | .mesgs { 135 | float: left; 136 | padding: 30px 15px 0 25px; 137 | width: 60%; 138 | } 139 | 140 | .sent_msg p { 141 | border-radius: 3px; 142 | font-size: 14px; 143 | margin: 0; 144 | color: #fff; 145 | padding: 5px 10px 5px 12px; 146 | width: 100%; 147 | } 148 | .outgoing_msg { 149 | overflow: hidden; 150 | margin: 26px 0 26px; 151 | } 152 | .sent_msg { 153 | float: right; 154 | width: 46%; 155 | } 156 | .input_msg_write input { 157 | background: rgba(0, 0, 0, 0) none repeat scroll 0 0; 158 | border: medium none; 159 | color: #4c4c4c; 160 | font-size: 15px; 161 | min-height: 48px; 162 | width: 100%; 163 | } 164 | 165 | .type_msg { 166 | border-top: 1px solid #c4c4c4; 167 | position: relative; 168 | } 169 | .msg_send_btn { 170 | background: #05728f none repeat scroll 0 0; 171 | border: medium none; 172 | border-radius: 50%; 173 | color: #fff; 174 | cursor: pointer; 175 | font-size: 17px; 176 | height: 33px; 177 | position: absolute; 178 | right: 0; 179 | top: 11px; 180 | width: 33px; 181 | } 182 | .messaging { 183 | padding: 0 0 10px 0; 184 | } 185 | .msg_history { 186 | height: 316px; 187 | overflow-y: auto; 188 | } -------------------------------------------------------------------------------- /public/assets/js/EventEmitter.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * EventEmitter v5.2.9 - git.io/ee 3 | * Unlicense - http://unlicense.org/ 4 | * Oliver Caldwell - https://oli.me.uk/ 5 | * @preserve 6 | */ 7 | !function(e){"use strict";function t(){}function n(e,t){for(var n=e.length;n--;)if(e[n].listener===t)return n;return-1}function r(e){return function(){return this[e].apply(this,arguments)}}function i(e){return"function"==typeof e||e instanceof RegExp||!(!e||"object"!=typeof e)&&i(e.listener)}var s=t.prototype,o=e.EventEmitter;s.getListeners=function(e){var t,n,r=this._getEvents();if(e instanceof RegExp){t={};for(n in r)r.hasOwnProperty(n)&&e.test(n)&&(t[n]=r[n])}else t=r[e]||(r[e]=[]);return t},s.flattenListeners=function(e){var t,n=[];for(t=0;t/g,">")}function n(t){return t.replace(/"/g,""")}function e(t){if(!t)return"";var r=[];for(var e in t){var i=t[e]+"";r.push(e+'="'+n(i)+'"')}return r.join(" ")}function i(t){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};i=new a(i);for(var f=o(t),u=[],c=0;c\n");else if(p.isLink&&i.check(p)){var l=i.resolve(p),s=l.formatted,g=l.formattedHref,y=l.tagName,h=l.className,v=l.target,k=l.attributes,S="<"+y+' href="'+n(g)+'"';h&&(S+=' class="'+n(h)+'"'),v&&(S+=' target="'+n(v)+'"'),k&&(S+=" "+e(k)),S+=">"+r(s)+"",u.push(S)}else u.push(r(p.toString()))}return u.join("")}var o=t.tokenize,f=t.options,a=f.Options;if(!String.prototype.linkify)try{Object.defineProperty(String.prototype,"linkify",{a:function(){},get:function(){return function(t){return i(this,t)}}})}catch(u){String.prototype.linkify||(String.prototype.linkify=function(t){return i(this,t)})}return i}(r);t.linkifyStr=n}(window,linkify); -------------------------------------------------------------------------------- /public/assets/js/main.js: -------------------------------------------------------------------------------- 1 | const ajaxErrorHandler = function (error) { 2 | console.log(error); 3 | alert('Ajax error occurred, check your console for more details.'); 4 | }; 5 | 6 | const apiUrl = (url) => '/api/' + url + '/' + TOKEN; 7 | 8 | const formatLink = (content) => content.linkify(); 9 | 10 | $(function (){ 11 | $('[data-toggle="tooltip"]').tooltip({ 12 | html: true 13 | }); 14 | }); -------------------------------------------------------------------------------- /public/assets/js/site/admin.js: -------------------------------------------------------------------------------- 1 | let ws; 2 | 3 | $(function (){ 4 | let adminSocketUrl = 'ws://' + window.location.host + adminSocketPrefix; 5 | 6 | let $btnSend = $('#btn-send'); 7 | let $inputCommand = $('#command'); 8 | let $fieldExecutionResult = $('#execution-result'); 9 | 10 | ws = new SocketWrapper({ 11 | url: adminSocketUrl 12 | }, () => adminBlock()); 13 | 14 | siteEvent.on('conn.connected', function () { 15 | console.log('CONNECTED') 16 | }); 17 | 18 | siteEvent.on('server.admin.config.env.result', function (response) { 19 | $fieldExecutionResult.html(JSON.stringify(response.message, null, 4)); 20 | }); 21 | 22 | $('#form-send-command').submit(function (event) { 23 | event.preventDefault(); 24 | 25 | let payload = JSON.parse($inputCommand.val()); 26 | payload.time = (new Date()).getTime(); 27 | 28 | ws.send(payload); 29 | }); 30 | 31 | let adminBlock = function () { 32 | ws.send({ 33 | command: 'server.admin.config.env', 34 | action: 'list-commands' 35 | }); 36 | }; 37 | }); 38 | -------------------------------------------------------------------------------- /public/assets/js/site/typing-status.js: -------------------------------------------------------------------------------- 1 | const TypingStatus = (function () { 2 | 3 | function TypingStatus() { 4 | const _this = this; 5 | 6 | this.typingStatuses = {}; 7 | let isInitialised = false; 8 | let invokeWhenInitialised = []; 9 | 10 | this.init = function (data) { 11 | isInitialised = true; 12 | 13 | this.ws = data.ws; 14 | this.command = data.command; 15 | 16 | invokeWhenInitialised.forEach((func) => { 17 | func[0](...func[1]); 18 | }); 19 | }; 20 | 21 | 22 | /** 23 | * Check if id is registered in typing statuses 24 | * @param clientId 25 | * @returns {boolean} 26 | */ 27 | this.has = function (clientId) { 28 | return !!this.typingStatuses[clientId]; 29 | }; 30 | 31 | /** 32 | * Send typing status to server 33 | * @param status 34 | * @param withData 35 | */ 36 | this.send = function (status = 'typing', withData = {}) { 37 | if ({} !== withData) { 38 | withData.status = status; 39 | this.ws.send(this.command, withData); 40 | } else { 41 | this.ws.send(this.command, {status: status}); 42 | } 43 | }; 44 | 45 | /** 46 | * Remove typing status 47 | * @param clientId 48 | */ 49 | this.remove = function (clientId) { 50 | if (this.has(clientId)) { 51 | clearTimeout(this.typingStatuses[clientId]); 52 | $('#typing-status-' + clientId).remove(); 53 | delete this.typingStatuses[clientId]; 54 | } 55 | }; 56 | 57 | /** 58 | * Listen to typing status and act on it 59 | * @param config 60 | */ 61 | this.listen = function (config) { 62 | if (!isInitialised) { 63 | invokeWhenInitialised.push([ 64 | _this.listen, [config] 65 | ]); 66 | return; 67 | } 68 | 69 | let $elTypingStatus = config.$elTypingStatus; 70 | let templateTypingStatus = config.templateTypingStatus; 71 | 72 | _this.ws.onCommand(_this.command, function (response) { 73 | let message = response.message; 74 | 75 | let clientId = message.client_id; 76 | let tStatusInterval = _this.typingStatuses[clientId]; 77 | 78 | if (message.status !== 'typing') { 79 | _this.remove(clientId); 80 | return; 81 | } 82 | 83 | //Create a typing message which will disappear in x seconds 84 | clearTimeout(tStatusInterval); 85 | _this.typingStatuses[clientId] = setTimeout(function () { 86 | _this.remove(clientId); 87 | }, message.timeout); 88 | 89 | //If message is already displayed 90 | if (tStatusInterval) { 91 | return; 92 | } 93 | 94 | $elTypingStatus.append(templateTypingStatus({ 95 | name: message.user, 96 | id: clientId 97 | })) 98 | }); 99 | 100 | }; 101 | 102 | } 103 | 104 | return TypingStatus; 105 | })(); -------------------------------------------------------------------------------- /public/assets/js/user/private-connection.js: -------------------------------------------------------------------------------- 1 | //Initialize socket wrapper 2 | let chatSocketUrl = 'ws://' + window.location.host + privateChatSocketPrefix; 3 | 4 | let $elNavMessageBadge = $('#nav-link-message').find('.badge'); 5 | 6 | const websocket = Reactificate.Websocket.connect(chatSocketUrl); 7 | websocket.setAuthToken(TOKEN); 8 | 9 | websocket.onOpen(function () { 10 | //Tone to be played when new message is received 11 | let toneMessage = new Howl({ 12 | src: ['/assets/mp3/juntos.mp3'], 13 | volume: 0.5 14 | }); 15 | 16 | websocket.send('user.iam-online', []); 17 | 18 | websocket.onCommand('chat.private.send', function () { 19 | toneMessage.play(); 20 | let totalMessage = parseInt($elNavMessageBadge.text()) || 0; 21 | $elNavMessageBadge.text(totalMessage + 1); 22 | }); 23 | 24 | }); -------------------------------------------------------------------------------- /public/assets/mp3/done-for-you.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ahmard/reactphp-live-chat/5b19073219e5dff7458c182bf920d21d3440ad03/public/assets/mp3/done-for-you.mp3 -------------------------------------------------------------------------------- /public/assets/mp3/juntos.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ahmard/reactphp-live-chat/5b19073219e5dff7458c182bf920d21d3440ad03/public/assets/mp3/juntos.mp3 -------------------------------------------------------------------------------- /public/assets/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ahmard/reactphp-live-chat/5b19073219e5dff7458c182bf920d21d3440ad03/public/assets/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /public/assets/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ahmard/reactphp-live-chat/5b19073219e5dff7458c182bf920d21d3440ad03/public/assets/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /public/images/gender/avatar-unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ahmard/reactphp-live-chat/5b19073219e5dff7458c182bf920d21d3440ad03/public/images/gender/avatar-unknown.png -------------------------------------------------------------------------------- /public/images/gender/iconfinder_female1_403023.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ahmard/reactphp-live-chat/5b19073219e5dff7458c182bf920d21d3440ad03/public/images/gender/iconfinder_female1_403023.png -------------------------------------------------------------------------------- /public/images/gender/iconfinder_male3_403019.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ahmard/reactphp-live-chat/5b19073219e5dff7458c182bf920d21d3440ad03/public/images/gender/iconfinder_male3_403019.png -------------------------------------------------------------------------------- /public/mc43ntcwmzawmcaxntkxntc0mjyw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ahmard/reactphp-live-chat/5b19073219e5dff7458c182bf920d21d3440ad03/public/mc43ntcwmzawmcaxntkxntc0mjyw.jpg -------------------------------------------------------------------------------- /react.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 3 | getMessage()} => {$exception->getFile()} @ Line {$exception->getLine()}\n\t --> Log File: {$filename}\n"); 29 | } 30 | 31 | //Handle all exceptions thrown 32 | set_exception_handler('handleApplicationException'); 33 | 34 | //Load environment variables 35 | $dotenv = Dotenv::createImmutable(__DIR__); 36 | $dotenv->load(); 37 | 38 | //Instantiate console application 39 | $app = new Application($_ENV['APP_NAME'], $_ENV['APP_VERSION']); 40 | 41 | //Load helpers 42 | require __DIR__ . '/src/Helpers/generalHelperFunctions.php'; 43 | require __DIR__ . '/src/Helpers/socketHelperFunctions.php'; 44 | require __DIR__ . '/src/Helpers/httpHelperFunctions.php'; 45 | 46 | //Load all commands 47 | $dirIterator = new DirectoryIterator(app_path('Console/Commands')); 48 | 49 | foreach ($dirIterator as $item) { 50 | if ($item->isFile()) { 51 | $className = $commandNamespace . substr($item->getFilename(), 0, -4); 52 | $command = new $className; 53 | $app->add($command); 54 | } 55 | } 56 | 57 | //Load all seeders 58 | $dirIterator = new DirectoryIterator(root_path('database/Seeds')); 59 | foreach ($dirIterator as $item) { 60 | if ($item->isFile()) { 61 | $_ENV['seeds'][] = $seedNamespace . substr($item->getFilename(), 0, -4); 62 | } 63 | } 64 | 65 | //Load event listeners 66 | EventServiceProvider::init()->boot(); 67 | 68 | //Run console application 69 | try { 70 | $app->run(); 71 | } catch (Exception $e) { 72 | handleApplicationException($e); 73 | } -------------------------------------------------------------------------------- /requests.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:9000 2 | 3 | ### 4 | GET http://localhost:9000/api/chat/private/check-user/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwaXJ5IjoxNjAzNjM0NzIyfQ.UIpeBph4hRdwi3WCJPQvWASn7CDIKdXJ4AlsCax7qFI?username=Ahmard 5 | Accept: application/json 6 | 7 | ### 8 | http://localhost:9000/chat/private 9 | -------------------------------------------------------------------------------- /resources/views/auth/login-success.php: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 |
Account Login
10 |
11 |
12 | Logged in successfully. 13 |
14 | 20 |
21 |
22 |
23 | 24 | -------------------------------------------------------------------------------- /resources/views/auth/login.php: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 |
14 |
Login
15 |
16 |
17 |
18 | Login to your account 19 | ' 22 | . "{$error}:" 23 | . '
'; 24 | } 25 | 26 | if (!empty($errors)) { 27 | foreach ($errors as $inputName => $validation) { 28 | $inputName = ucfirst($inputName); 29 | foreach ($validation as $error) { 30 | echo '
' 31 | . "{$inputName}: {$error->getMessage()}" 32 | . '
'; 33 | } 34 | } 35 | } 36 | ?> 37 |
38 |
39 |
40 |
41 | 42 | 44 |
45 |
46 | 47 | 49 |
50 | 51 | 55 |
56 | 57 |
58 | New here? Register 59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /resources/views/auth/register-success.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
Create Account
6 |
7 |
8 | Account created successfully. 9 |
10 | 16 |
17 |
18 |
19 | 20 | -------------------------------------------------------------------------------- /resources/views/auth/register.php: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 |
14 |
Create Account
15 |
16 |
17 |
18 | Create your account 19 | ' 22 | . "{$error}:" 23 | . '
'; 24 | } 25 | 26 | if (!empty($errors)) { 27 | foreach ($errors as $inputName => $validation) { 28 | $inputName = ucfirst($inputName); 29 | foreach ($validation as $error) { 30 | echo '
' 31 | . "{$inputName}: {$error->getMessage()}" 32 | . '
'; 33 | } 34 | } 35 | } 36 | ?> 37 |
38 |
39 |
40 |
41 | 42 | 44 |
45 |
46 | 47 | 49 |
50 |
51 | 52 | 54 |
55 | 56 | 60 |
61 | 62 |
63 | Already have an account? Login 64 |
65 |
66 |
67 |
68 |
69 | 70 | 71 | -------------------------------------------------------------------------------- /resources/views/chat/index.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
Choose chat mode
5 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /resources/views/index-logged.php: -------------------------------------------------------------------------------- 1 | 12 | 58 | 59 | -------------------------------------------------------------------------------- /resources/views/index.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | Welcome 6 |
7 |
8 | This is multi-purpose server that handles Http Requests and Socket Connections, built on top of 9 | ReactPHP and Ratchet PHP.
10 | Please know that this is entirely experimental, so production usage is discouraged. 11 |
12 | This is built to show a little of what ReactPHP can do. 13 |
14 |
15 | There are x users having public conversation 16 |
17 |

18 | Let's start :) 19 |

20 | 42 |
43 | 46 |
47 |
48 | -------------------------------------------------------------------------------- /resources/views/layout/footer.php: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 24 | 25 | 26 |
27 | 28 | 29 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | auth()->check()): ?> 58 | 59 | 60 | 61 | 62 | 81 | 82 | -------------------------------------------------------------------------------- /resources/views/layout/header.php: -------------------------------------------------------------------------------- 1 | auth(); 13 | if ($auth->check()) { 14 | $homeUrl = '/home/' . $auth->token(); 15 | } 16 | 17 | ?> 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | <?= $title ?? $_ENV['APP_TITLE'] ?> 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 90 | 91 | 92 | 93 |
-------------------------------------------------------------------------------- /resources/views/server/admin.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ahmard/reactphp-live-chat/5b19073219e5dff7458c182bf920d21d3440ad03/resources/views/server/admin.php -------------------------------------------------------------------------------- /resources/views/server/admin/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 |
7 | Welcome To Admin Panel 8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 21 |
22 |
23 | 27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | 35 | 36 | 37 | 40 | 41 | 46 | 47 | -------------------------------------------------------------------------------- /resources/views/server/index.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | Server 6 |
7 |
8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /resources/views/server/login.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ahmard/reactphp-live-chat/5b19073219e5dff7458c182bf920d21d3440ad03/resources/views/server/login.php -------------------------------------------------------------------------------- /resources/views/system/302.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Redirecting... 8 | 9 | 10 | Redirecting you to new url
Click here to manually redirect Let's go. 11 | 12 | -------------------------------------------------------------------------------- /resources/views/system/404.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 404(Not Found) 6 |
7 |
8 | The requested resources does not exists.
9 | Let's go home 10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /resources/views/system/405.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 405(Method Not Allowed) 6 |
7 |
8 | Request method not allowed.
9 | Let's go home 10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /resources/views/system/500.php: -------------------------------------------------------------------------------- 1 | 12 |
13 |
14 |
15 | 500(Internal Server Error) 16 |
17 |
18 | getMessage(); 22 | } else { 23 | echo $error; 24 | } 25 | } else { 26 | echo 'Server ran in to an error while processing your request.'; 27 | } 28 | ?> 29 |
30 | Let's go home 31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /resources/views/user/chat/index.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
User
5 | 8 |
9 |
10 | -------------------------------------------------------------------------------- /resources/views/user/profile.php: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
15 |
16 |
Profile
17 | 18 | Settings 19 | 20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 |
28 | 29 | 30 | 33 | -------------------------------------------------------------------------------- /resources/views/user/settings.php: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
15 |
16 |
Profile
17 | 18 | Settings 19 | 20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 |
28 | 29 | 30 | 33 | -------------------------------------------------------------------------------- /resources/views/user/settings/change-password.php: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
15 |
16 |
Change Password
17 |
18 |
19 |
20 |
21 | 22 |
23 | 24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 | 33 | 36 |
37 |
38 |
39 |
40 |
41 | 42 | 43 | 79 | -------------------------------------------------------------------------------- /resources/views/user/settings/index.php: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
15 |
16 |
Settings
17 | 18 |
19 | 25 |
26 |
27 |
28 |
29 | 30 | 31 | 34 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | group(function () { 7 | Route::post('meta', 'LinkController@meta'); 8 | }); 9 | 10 | Route::append(HttpServiceProvider::$routeTokenPrefix) 11 | ->group(function () { 12 | 13 | Route::prefix('links')->group(function () { 14 | Route::post('meta', 'LinkController@meta'); 15 | }); 16 | 17 | //User 18 | Route::prefix('user') 19 | ->middleware('auth') 20 | ->namespace('User') 21 | ->group(function () { 22 | Route::get('{id:\d+}', 'UserController@view'); 23 | 24 | Route::prefix('settings')->group(function () { 25 | Route::post('change-password', 'SettingsController@doChangePassword'); 26 | }); 27 | }); 28 | 29 | // CHAT 30 | Route::prefix('chat') 31 | ->middleware('auth') 32 | ->group(function () { 33 | //Private 34 | Route::prefix('private') 35 | ->namespace('User') 36 | ->group(function () { 37 | Route::get('check-user', 'ChatController@checkUser'); 38 | Route::get('fetch-conversations', 'ChatController@fetchConversations'); 39 | Route::get('get-conversation-status/{id:\d+}', 'ChatController@getConversationStatus'); 40 | Route::get('{id:\d+}', 'ChatController@fetchMessages'); 41 | Route::post('{id:\d+}', 'ChatController@send'); 42 | Route::patch('{id:\d+}/mark-as-read', 'ChatController@markAsRead'); 43 | }); 44 | }); 45 | 46 | // CATEGORIES 47 | $catRoutes = fn(string $dbTable, string $prefix) => Route::prefix($prefix) 48 | ->middleware('auth') 49 | ->namespace('User') 50 | ->addField('dbTable', $dbTable) 51 | ->group(function () { 52 | Route::get('/', 'CategoryController@list'); 53 | Route::get('/{id:\d+}/open', 'CategoryController@open'); 54 | Route::post('/', 'CategoryController@add'); 55 | Route::get('{id:\d+}', 'CategoryController@view'); 56 | Route::delete('{id:\d+}', 'CategoryController@delete'); 57 | Route::put('{id:\d+}', 'CategoryController@rename'); 58 | }); 59 | 60 | $catRoutes('notes', 'note-categories'); 61 | $catRoutes('lists', 'list-categories'); 62 | 63 | // NOTE-TAKING 64 | Route::prefix('notes') 65 | ->middleware('auth') 66 | ->namespace('User') 67 | ->group(function () { 68 | Route::get('/', 'NoteController@list'); 69 | Route::post('/', 'NoteController@add'); 70 | Route::get('{id:\d+}', 'NoteController@view'); 71 | Route::put('{id:\d+}', 'NoteController@update'); 72 | Route::get('{noteId:\d+}/move/{catId:\d+}', 'NoteController@move'); 73 | Route::delete('{id:\d+}', 'NoteController@delete'); 74 | }); 75 | 76 | // LIST-TAKING 77 | Route::prefix('lists') 78 | ->middleware('auth') 79 | ->namespace('User') 80 | ->group(function () { 81 | Route::get('/', 'ListController@list'); 82 | Route::post('/', 'ListController@add'); 83 | Route::get('{id:\d+}', 'ListController@view'); 84 | Route::put('{id:\d+}', 'ListController@update'); 85 | Route::get('{noteId:\d+}/move/{catId:\d+}', 'ListController@move'); 86 | Route::delete('{id:\d+}', 'ListController@delete'); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | name('index'); 11 | Route::get('/home', 'MainController@index') 12 | ->append(HttpServiceProvider::$routeTokenPrefix) 13 | ->name('index-logged'); 14 | 15 | //Authentication 16 | Route::get('/register', 'AuthController@showRegisterForm')->name('register'); 17 | Route::post('/register', 'AuthController@doRegister')->name('register.submit'); 18 | Route::get('/login', 'AuthController@showLoginForm')->name('login'); 19 | Route::post('/login', 'AuthController@doLogin')->name('login.submit'); 20 | Route::get('/forgot-password', 'AuthController@forgot-password')->name('forgot-password'); 21 | 22 | //Server routes 23 | Route::prefix('server') 24 | ->namespace('Server') 25 | ->middleware('auth') 26 | //->append(HttpServiceProvider::$routeTokenPrefix) 27 | ->name('server.') 28 | ->group(function () { 29 | Route::get('/', function (Request $request) { 30 | return $request->getResponse()->view('server/index'); 31 | }); 32 | Route::prefix('admin') 33 | ->namespace('Admin') 34 | ->name('admin.') 35 | ->group(function () { 36 | Route::get('', 'MainController@index')->name('index'); 37 | Route::post('add', 'MainController@add')->name('add'); 38 | }); 39 | }); 40 | 41 | Route::prefix('chat') 42 | ->name('chat.') 43 | ->group(function () { 44 | Route::get('/', 'MainController@chatIndex')->name('index'); 45 | Route::get('/public', 'MainController@publicChat')->name('public'); 46 | 47 | Route::middleware('auth') 48 | ->append(HttpServiceProvider::$routeTokenPrefix) 49 | ->group(function () { 50 | Route::get('/private', 'User\ChatController@privateChat')->name('private'); 51 | }); 52 | }); 53 | 54 | Route::namespace('User') 55 | ->middleware('auth') 56 | ->append(HttpServiceProvider::$routeTokenPrefix) 57 | ->group(function () { 58 | Route::get('note', 'NoteController@index'); 59 | Route::get('list', 'ListController@index'); 60 | Route::get('change-password', 'UserController@showChangePasswordForm'); 61 | }); 62 | 63 | 64 | Route::prefix('user') 65 | ->namespace('User') 66 | ->middleware('auth') 67 | ->append(HttpServiceProvider::$routeTokenPrefix) 68 | ->group(function () { 69 | Route::get('profile', 'UserController@profile'); 70 | 71 | Route::prefix('settings')->group(function () { 72 | Route::get('/', 'SettingsController@index'); 73 | Route::get('change-password', 'SettingsController@showChangePasswordForm'); 74 | }); 75 | }); 76 | 77 | -------------------------------------------------------------------------------- /routes/websocket.php: -------------------------------------------------------------------------------- 1 | namespace('Server') 11 | ->group(function (ColisInterface $colis) { 12 | //Admin 13 | $colis->prefix('admin.') 14 | ->namespace('Admin') 15 | ->group(function (ColisInterface $colis) { 16 | //Configuration 17 | $colis->prefix('config.') 18 | ->namespace('Config') 19 | ->group(function (ColisInterface $colis) { 20 | $colis->listen('env', 'EnvironmentListener@__invoke'); 21 | }); 22 | }); 23 | }); 24 | 25 | 26 | Colis::prefix('system.') 27 | ->group(function (ColisInterface $colis) { 28 | $colis->listen('ping', 'SystemListener@ping'); 29 | $colis->listen('pong', 'SystemListener@pong'); 30 | //Statistics 31 | $colis->prefix('stat.')->group(function (ColisInterface $colis) { 32 | //Count total users chatting publicly 33 | $colis->listen('public-chat-users', function (Request $request) { 34 | resp($request->client())->send('system.stat.public-chat-users', [ 35 | 'total_users' => chatClients()->count(), 36 | 'total_rooms' => (chatRooms()->count() - 1), 37 | ]); 38 | }); 39 | }); 40 | }); 41 | 42 | Colis::prefix('user.') 43 | ->namespace('Chat\PrivateChat') 44 | ->middleware('auth') 45 | ->group(function (ColisInterface $colis) { 46 | $colis->listen('iam-online', 'ChatListener@iamOnline'); 47 | }); 48 | 49 | Colis::prefix('chat.') 50 | ->namespace('Chat') 51 | ->group(function (ColisInterface $colis) { 52 | //Public Messaging 53 | $colis->prefix('public.') 54 | ->namespace('PublicChat') 55 | ->group(function (ColisInterface $colis) { 56 | $colis->listen('join', 'ChatListener@join'); 57 | $colis->listen('leave', 'ChatListener@leave'); 58 | $colis->listen('send', 'ChatListener@send'); 59 | $colis->listen('receive', 'ChatListener@receive'); 60 | $colis->listen('typing', 'ChatListener@typing'); 61 | }); 62 | 63 | //Private Messaging 64 | $colis->prefix('private.') 65 | ->namespace('PrivateChat') 66 | ->middleware('auth') 67 | ->group(function (ColisInterface $colis) { 68 | $colis->listen('init', 'ChatListener@init'); 69 | $colis->listen('send', 'ChatListener@send'); 70 | $colis->listen('typing', 'ChatListener@typing'); 71 | $colis->listen('monitor-users-presence', 'ChatListener@monitorUsersPresence'); 72 | }); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /src/Auth/Auth.php: -------------------------------------------------------------------------------- 1 | authToken(); 36 | } 37 | 38 | /** 39 | * @return PromiseInterface|Promise 40 | */ 41 | private function authToken(): PromiseInterface|Promise 42 | { 43 | if (self::$token) { 44 | 45 | try { 46 | $verified = Token::decode(self::$token); 47 | if ($verified) { 48 | return Connection::get() 49 | ->query('SELECT * FROM users WHERE id = ?', [$verified['id']]) 50 | ->then(function (Result $result) { 51 | 52 | if (!isset($result->rows[0])) { 53 | return resolve($this); 54 | } 55 | 56 | $this->user = $result->rows[0]; 57 | $this->isAuthenticated = true; 58 | return resolve($this); 59 | }) 60 | ->otherwise(function (Throwable $throwable) { 61 | echo "Auth check failed: "; 62 | }); 63 | } 64 | } catch (Throwable $exception) { 65 | } 66 | } 67 | 68 | return resolve($this); 69 | } 70 | 71 | 72 | /** 73 | * Check if user is authenticated 74 | * @return bool 75 | */ 76 | public function check(): bool 77 | { 78 | return $this->isAuthenticated; 79 | } 80 | 81 | public function userId(): ?int 82 | { 83 | return $this->user()['id'] ?? null; 84 | } 85 | 86 | /** 87 | * @return array 88 | */ 89 | public function user(): array 90 | { 91 | return $this->user; 92 | } 93 | 94 | /** 95 | * Get user token 96 | * @return string 97 | */ 98 | public function token(): string 99 | { 100 | return self::$token; 101 | } 102 | } -------------------------------------------------------------------------------- /src/Auth/Token.php: -------------------------------------------------------------------------------- 1 | time()) { 36 | unset($decodedToken['expiry']); 37 | return $decodedToken; 38 | } 39 | } 40 | 41 | return false; 42 | } 43 | 44 | private static function getAppKey(): string 45 | { 46 | return $_ENV['APP_KEY'] ?? 'ahmard'; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Database/Connection.php: -------------------------------------------------------------------------------- 1 | openLazy($_ENV['DB_FILE']); 38 | } 39 | 40 | return self::$database; 41 | } 42 | 43 | /** 44 | * Returns sqlite connection factory 45 | * 46 | * @return Factory 47 | */ 48 | public static function getFactory(): Factory 49 | { 50 | if (!isset(self::$connection)) { 51 | return self::$connection = new Factory(Loop::get()); 52 | } 53 | 54 | return self::$connection; 55 | } 56 | } -------------------------------------------------------------------------------- /src/Database/SeederInterface.php: -------------------------------------------------------------------------------- 1 | socketEvent)) { 32 | return $this->socketEvent = new EventEmitter(); 33 | } 34 | 35 | return $this->socketEvent; 36 | } 37 | 38 | /** 39 | * HttpServer event 40 | * @return EventEmitter|EventEmitterInterface 41 | */ 42 | public function http() 43 | { 44 | if (!isset($this->socketEvent)) { 45 | return $this->socketEvent = new EventEmitter(); 46 | } 47 | 48 | return $this->socketEvent; 49 | } 50 | } -------------------------------------------------------------------------------- /src/Exceptions/Socket/InvalidPayloadException.php: -------------------------------------------------------------------------------- 1 | output = new ConsoleOutput; 18 | } 19 | 20 | public function forceDisplay(): ConsoleHelper 21 | { 22 | $this->willForceDisplay = true; 23 | return $this; 24 | } 25 | 26 | public function info(string $text): ConsoleHelper 27 | { 28 | return $this->writeln("{$text}"); 29 | } 30 | 31 | private function writeln(string $data): ConsoleHelper 32 | { 33 | if ( 34 | ( 35 | $_ENV['SHOW_CLIENT_DEBUG_INFO'] == 'true' 36 | || $this->willForceDisplay 37 | ) 38 | && 39 | ( 40 | $_ENV['SILENCE_CONSOLE'] == 'false' 41 | //This will help us display which address http and socket servers listen two 42 | || self::$calls < $_ENV['SHOW_FIRST_X_CONSOLE_LOGS'] 43 | ) 44 | ) { 45 | $this->output->writeln($data); 46 | //Keep track of how much logs where displayed to console 47 | self::$calls++; 48 | 49 | return $this; 50 | } 51 | 52 | return $this; 53 | } 54 | 55 | public function comment(string $text): ConsoleHelper 56 | { 57 | return $this->writeln("{$text}"); 58 | } 59 | 60 | 61 | public function question(string $text): ConsoleHelper 62 | { 63 | return $this->writeln("{$text}"); 64 | } 65 | 66 | 67 | public function error(string $text): ConsoleHelper 68 | { 69 | return $this->writeln("{$text}"); 70 | } 71 | 72 | 73 | public function write(string $text, string $color = ''): ConsoleHelper 74 | { 75 | if ($color !== '') { 76 | return $this->writeln(color($text)->fg($color)); 77 | } 78 | 79 | return $this->writeln($text); 80 | } 81 | 82 | public function fg(string $color): void 83 | { 84 | 85 | } 86 | 87 | 88 | public function newLine(): ConsoleHelper 89 | { 90 | echo "\n"; 91 | return $this; 92 | } 93 | 94 | 95 | public function tab(): ConsoleHelper 96 | { 97 | echo "\t"; 98 | return $this; 99 | } 100 | } -------------------------------------------------------------------------------- /src/Helpers/Classes/FormHelper.php: -------------------------------------------------------------------------------- 1 | request = $request; 19 | } 20 | 21 | public function addFormError(string $inputName, ConstraintViolationListInterface $inputError): void 22 | { 23 | 24 | } 25 | 26 | /** 27 | * Retrieve sent form data 28 | * @param string $key 29 | * @return mixed|null 30 | */ 31 | public function getOldData(string $key) 32 | { 33 | return $this->request->getParsedBody()[$key] ?? null; 34 | } 35 | 36 | public function getFormError(string $inputName): ConstraintViolationListInterface 37 | { 38 | return $this->formErrors[$inputName]; 39 | } 40 | } -------------------------------------------------------------------------------- /src/Helpers/Classes/HelperTrait.php: -------------------------------------------------------------------------------- 1 | $value) { 21 | $errors = $validator->validate($value, $rules[$key]); 22 | if (0 !== count($errors)) { 23 | FormHelper::addFormError($key, $errors); 24 | $result[$key] = $errors; 25 | } 26 | } 27 | 28 | return $result; 29 | } 30 | } -------------------------------------------------------------------------------- /src/Helpers/generalHelperFunctions.php: -------------------------------------------------------------------------------- 1 | forceDisplay(); 166 | } 167 | return $console; 168 | } 169 | 170 | function database(): DatabaseInterface 171 | { 172 | return Connection::get(); 173 | } 174 | 175 | /** 176 | * Input validation helper 177 | * @return ValidationHelper 178 | */ 179 | function validator(): ValidationHelper 180 | { 181 | return new ValidationHelper(); 182 | } 183 | 184 | /** 185 | * @return ServerStore 186 | */ 187 | function server(): ServerStore 188 | { 189 | return ServerStore::getInstance(); 190 | } 191 | 192 | function carbon(): Carbon 193 | { 194 | return Carbon::createFromTimestamp(time()); 195 | } -------------------------------------------------------------------------------- /src/Helpers/httpHelperFunctions.php: -------------------------------------------------------------------------------- 1 | getConnectionId()] = $data; 43 | } 44 | 45 | return $chatClients; 46 | } 47 | 48 | $chatRooms = new ArrayObject(); 49 | /** 50 | * All created chat rooms 51 | * @param null|string $room 52 | * @param null|mixed $setValue 53 | * @return ConnectionInterface[] 54 | */ 55 | function chatRooms($room = null, $setValue = null) 56 | { 57 | global $chatRooms; 58 | 59 | if (!isset($chatRooms[$room])) { 60 | $chatRooms[$room] = []; 61 | } 62 | 63 | if ($setValue) { 64 | $chatRooms[$room][] = $setValue; 65 | } 66 | 67 | return $room ? $chatRooms[$room] : $chatRooms; 68 | } -------------------------------------------------------------------------------- /src/Http/Middleware/AuthMiddleware.php: -------------------------------------------------------------------------------- 1 | auth()->check()) { 17 | return resolve($next($request)); 18 | } 19 | 20 | return resolve($request->getResponse()->redirect('/')); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Http/Middleware/MiddlewareInterface.php: -------------------------------------------------------------------------------- 1 | emit('route.before.dispatch'); 28 | $dispatchResult = Dispatcher::dispatch($request); 29 | event()->emit('route.after.dispatch'); 30 | 31 | //Init authentication 32 | return Auth::handle($request->getToken())->then(function (Auth $auth) use ($request, $dispatchResult) { 33 | //Set auth class 34 | $request->init('auth', $auth); 35 | 36 | switch (true) { 37 | case $dispatchResult->isNotFound(): 38 | $response = $request->getResponse()->notFound(); 39 | break; 40 | case $dispatchResult->isMethodNotAllowed(): 41 | $response = $request->getResponse()->methodNotAllowed(); 42 | break; 43 | case $dispatchResult->isFound(): 44 | $routeData = $dispatchResult->getRoute(); 45 | 46 | //If request has middleware, run it. 47 | $middleware = $routeData->getMiddleware()[0] ?? null; 48 | 49 | if ($middleware) { 50 | $middlewares = Kernel::getMiddlewares(); 51 | $middleware = $middlewares['routes-middleware'][$middleware]; 52 | 53 | $response = MiddlewareRunner::runCustom($middleware, function () use ($request, $dispatchResult) { 54 | return Matcher::match($request, $dispatchResult); 55 | }, $request); 56 | } else { 57 | $response = Matcher::match($request, $dispatchResult); 58 | } 59 | break; 60 | default: 61 | $response = $request->getResponse()->internalServerError(); 62 | break; 63 | } 64 | 65 | return $response; 66 | }); 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /src/Http/MiddlewareRunner.php: -------------------------------------------------------------------------------- 1 | 1) { 32 | for ($i = 0; $i < $totalMiddlewares; $i++) { 33 | $nextMiddleware = $middlewares[$i + 1] ?? fn() => $response; 34 | $objMiddleware = new $middlewares[$i](); 35 | $response = $objMiddleware->handle($nextMiddleware, $request); 36 | 37 | //If response is returned, we break out from the cycle of middlewares 38 | if ($response instanceof Response) { 39 | break; 40 | } 41 | } 42 | } else { 43 | $objMiddleware = new $middlewares[0](); 44 | 45 | $response = $objMiddleware->handle(function (Request $request) { 46 | 47 | }, $request); 48 | } 49 | 50 | return $response; 51 | } 52 | 53 | public static function runCustom( 54 | string $middleware, 55 | Closure $next, 56 | Request $request 57 | ): PromiseInterface 58 | { 59 | return (new $middleware())->handle(function () use ($next, $request) { 60 | return $next($request); 61 | }, $request); 62 | } 63 | } -------------------------------------------------------------------------------- /src/Http/Request.php: -------------------------------------------------------------------------------- 1 | request = $request; 26 | $this->formHelper = new FormHelper($this); 27 | } 28 | 29 | public function init(string $name, mixed $value): void 30 | { 31 | $this->$name = $value; 32 | } 33 | 34 | public function __get(string $name): mixed 35 | { 36 | return $this->request->$name; 37 | } 38 | 39 | public function __call(string $name, array $arguments): mixed 40 | { 41 | return $this->request->$name(...$arguments); 42 | } 43 | 44 | public function expectsJson(): bool 45 | { 46 | if ( 47 | $this->request->hasHeader('X-Requested-With') 48 | && $this->request->getHeaderLine('X-Requested-With') == 'XMLHttpRequest' 49 | ) { 50 | return true; 51 | } 52 | 53 | return false; 54 | } 55 | 56 | public function expectsHtml(): bool 57 | { 58 | $contentType = $this->request->getHeaderLine('Accept'); 59 | $headers = explode(',', $contentType); 60 | if (in_array('text/html', $headers)) { 61 | return true; 62 | } 63 | 64 | return false; 65 | } 66 | 67 | /** 68 | * Append auth token to constructed url 69 | * 70 | * @param string $routePath 71 | * @return string 72 | */ 73 | public function authRoute(string $routePath): string 74 | { 75 | if (empty($_ENV['DOMAIN'])) { 76 | return "/$routePath/" . $this->auth()->token(); 77 | } 78 | 79 | return url($routePath) . '/' . $this->auth()->token(); 80 | } 81 | 82 | public function auth(): Auth 83 | { 84 | return $this->auth ?? new Auth(md5('asdsadqwsqasdasdsada')); 85 | } 86 | 87 | /** 88 | * @return Response 89 | */ 90 | public function getResponse(): Response 91 | { 92 | return new Response($this); 93 | } 94 | 95 | /** 96 | * @return FormHelper 97 | */ 98 | public function form(): FormHelper 99 | { 100 | return $this->formHelper; 101 | } 102 | 103 | /** 104 | * @param string|null $fieldName 105 | * @return array|mixed|object|null 106 | */ 107 | public function post(?string $fieldName = null): mixed 108 | { 109 | if (!$fieldName) { 110 | return $this->getParsedBody(); 111 | } 112 | 113 | return $this->getParsedBody()[$fieldName] ?? null; 114 | } 115 | 116 | public function isGET(): bool 117 | { 118 | return 'GET' == $this->request->getMethod(); 119 | } 120 | 121 | public function isPOST(): bool 122 | { 123 | return 'POST' == $this->request->getMethod(); 124 | } 125 | 126 | /** 127 | * Get route parameter 128 | * 129 | * @param string|null $name 130 | * @return mixed 131 | */ 132 | public function getParam(?string $name = null): mixed 133 | { 134 | if (null !== $name) { 135 | return $this->getDispatchResult()->getUrlParameters()[$name] ?? null; 136 | } 137 | 138 | return $this->getDispatchResult()->getUrlParameters(); 139 | } 140 | 141 | /** 142 | * @return DispatchResult 143 | */ 144 | public function getDispatchResult(): DispatchResult 145 | { 146 | return $this->dispatchResult; 147 | } 148 | 149 | public function getToken(): string 150 | { 151 | if ('' == $this->token) { 152 | $this->token = $this->getParam('primaryToken') ?? ''; 153 | } 154 | 155 | return $this->token; 156 | } 157 | } -------------------------------------------------------------------------------- /src/Http/Router/Dispatcher.php: -------------------------------------------------------------------------------- 1 | getUri()->getPath(); 21 | $method = $request->getMethod(); 22 | 23 | $dispatchResult = QuickRouteDispatcher::create(RouteCollector::getCollector()) 24 | ->dispatch($method, $path); 25 | 26 | $request->init('dispatchResult', $dispatchResult); 27 | 28 | return $dispatchResult; 29 | } 30 | } -------------------------------------------------------------------------------- /src/Http/Router/Matcher.php: -------------------------------------------------------------------------------- 1 | getRoute(); 24 | 25 | $requestParams = $dispatchResult->getUrlParameters(); 26 | //Handle controller 27 | $controller = $routeData->getHandler(); 28 | if (is_callable($controller)) { 29 | return call_user_func($controller, $request, $requestParams); 30 | } 31 | 32 | $explodedController = explode('@', $controller); 33 | $controllerClass = $explodedController[0]; 34 | $controllerMethod = $explodedController[1]; 35 | 36 | $namespacedController = $routeData->getNamespace() . $controllerClass; 37 | 38 | //Call defined method 39 | $instantiatedController = (new $namespacedController([ 40 | 'request' => $request, 41 | 'params' => $requestParams 42 | ])); 43 | 44 | 45 | if (!method_exists($instantiatedController, $controllerMethod)) { 46 | return new Exception("Method {$namespacedController}::{$controllerMethod}() does not exists."); 47 | } 48 | 49 | return call_user_func( 50 | [ 51 | $instantiatedController, 52 | $controllerMethod 53 | ], 54 | $request, 55 | $requestParams 56 | ); 57 | } 58 | } -------------------------------------------------------------------------------- /src/Http/Router/RouteCollector.php: -------------------------------------------------------------------------------- 1 | collectFile( 27 | root_path('routes/web.php'), 28 | [ 29 | 'namespace' => $controllerNamespace, 30 | ] 31 | ); 32 | 33 | //Api routes 34 | self::$collector->collectFile( 35 | root_path('routes/api.php'), 36 | [ 37 | 'namespace' => $controllerNamespace, 38 | 'prefix' => 'api', 39 | 'name' => 'api.', 40 | ] 41 | ); 42 | } 43 | 44 | public static function register(): void 45 | { 46 | self::$collector->register(); 47 | } 48 | 49 | /** 50 | * @return Collector 51 | */ 52 | public static function getCollector(): Collector 53 | { 54 | return self::$collector; 55 | } 56 | } -------------------------------------------------------------------------------- /src/Http/Url.php: -------------------------------------------------------------------------------- 1 | getUri(); 25 | self::$method = $request->getMethod(); 26 | 27 | if (Dispatcher::getDispatchResult()->isFound()) { 28 | $expPrefix = explode( 29 | '/', 30 | Dispatcher::getDispatchResult() 31 | ->getRoute() 32 | ->getPrefix() 33 | ); 34 | $routeTokenPrefix = self::$routeTokenPrefix; 35 | 36 | if (class_exists(HttpServiceProvider::class)) { 37 | $routeTokenPrefix = HttpServiceProvider::$routeTokenPrefix; 38 | } 39 | 40 | if (end($expPrefix) == $routeTokenPrefix) { 41 | $expUrl = explode('/', self::$url); 42 | $routeToken = end($expUrl); 43 | $expRouteToken = explode('?', $routeToken); 44 | self::$routeToken = current($expRouteToken); 45 | } else { 46 | self::$routeToken = ''; 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * Get http url 53 | * @return string 54 | */ 55 | public static function getUrl(): string 56 | { 57 | return self::$url; 58 | } 59 | 60 | /** 61 | * Get http request method 62 | * @return string 63 | */ 64 | public static function getMethod(): string 65 | { 66 | return self::$method; 67 | } 68 | 69 | /** 70 | * Get authentication token passed to route 71 | * @return string 72 | */ 73 | public static function getToken(): string 74 | { 75 | return self::$routeToken; 76 | } 77 | 78 | /** 79 | * Get current route class 80 | * @return RouteData 81 | */ 82 | public static function getRoute(): RouteData 83 | { 84 | return Dispatcher::getDispatchResult()->getRoute(); 85 | } 86 | } -------------------------------------------------------------------------------- /src/Http/View/View.php: -------------------------------------------------------------------------------- 1 | request = $request; 16 | } 17 | 18 | public static function create(Request $request): View 19 | { 20 | return new View($request); 21 | } 22 | 23 | public function load(string $viewFile, array $data = []): string 24 | { 25 | if (!strpos($viewFile, '.php')) { 26 | $viewFile .= '.php'; 27 | } 28 | 29 | $foundView = $this->find($viewFile); 30 | 31 | if ($foundView) { 32 | return self::render($foundView, $data); 33 | } 34 | 35 | return self::render($foundView, $data); 36 | } 37 | 38 | /** 39 | * @param string $viewFilePath 40 | * @return string 41 | * @throws Exception 42 | */ 43 | public function find(string $viewFilePath): string 44 | { 45 | $viewFile = view_path($viewFilePath); 46 | 47 | if (file_exists($viewFile)) { 48 | return $viewFile; 49 | } 50 | 51 | throw new Exception("View file($viewFile) not found"); 52 | } 53 | 54 | /** 55 | * Render string(view file source) 56 | * @param string $viewFile 57 | * @param array $data 58 | * @return string 59 | */ 60 | protected function render(string $viewFile, array $data): string 61 | { 62 | $data = array_merge([ 63 | 'request' => $this->request, 64 | ], $data); 65 | 66 | ob_start(); 67 | 68 | extract($data); 69 | 70 | require $viewFile; 71 | 72 | $html = ob_get_contents(); 73 | 74 | ob_end_clean(); 75 | 76 | return (string)$html; 77 | } 78 | } -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | [ 17 | 18 | ], 19 | 20 | 'socket' => [ 21 | 22 | ] 23 | ]; 24 | 25 | /** 26 | * Application middlewares 27 | * @var array 28 | */ 29 | protected static array $middlewares = []; 30 | 31 | /** 32 | * Route middlewares 33 | * @var array 34 | */ 35 | protected static array $RouteMiddlewares = []; 36 | 37 | /** 38 | * Colis middlewares 39 | * @var array 40 | */ 41 | protected static array $colisMiddlewares = []; 42 | 43 | /** 44 | * Application grouped middlewares 45 | * @var array 46 | */ 47 | protected static array $middlewareGroups = []; 48 | 49 | /** 50 | * Application default middlewares 51 | * @var array|string[] 52 | */ 53 | protected static array $defaultMiddlewares = [ 54 | RootMiddleware::class, 55 | ]; 56 | 57 | /** 58 | * Application default route middlewares 59 | * @var array|string[] 60 | */ 61 | protected static array $defaultRouteMiddleware = [ 62 | RootMiddleware::class, 63 | ]; 64 | 65 | /** 66 | * Application default colis middlewares 67 | * @var array|string[] 68 | */ 69 | protected static array $defaultColisMiddleware = [ 70 | RootMiddleware::class, 71 | ]; 72 | 73 | /** 74 | * Application default middleware groups 75 | * @var array 76 | */ 77 | protected static array $defaultMiddlewareGroups = [ 78 | 'web' => [ 79 | RouteMiddleware::class, 80 | ], 81 | 82 | 'api' => [ 83 | 84 | ], 85 | 86 | 'socket' => [ 87 | 88 | ], 89 | ]; 90 | 91 | /** 92 | * Get registered middlewares 93 | * @return array 94 | */ 95 | public static function getMiddlewares(): array 96 | { 97 | return [ 98 | 'middlewares' => array_merge( 99 | static::$middlewares, 100 | self::$defaultMiddlewares 101 | ), 102 | 'middleware-groups' => array_merge( 103 | static::$middlewareGroups, 104 | self::$defaultMiddlewareGroups 105 | ), 106 | 'routes-middleware' => array_merge( 107 | static::$RouteMiddlewares, 108 | self::$defaultRouteMiddleware, 109 | ), 110 | 'colis-middleware' => array_merge( 111 | static::$colisMiddlewares, 112 | self::$defaultColisMiddleware, 113 | ), 114 | ]; 115 | } 116 | 117 | /** 118 | * Get registered servers 119 | * @return array|array[] 120 | */ 121 | public static function getServers(): array 122 | { 123 | return static::$servers; 124 | } 125 | } -------------------------------------------------------------------------------- /src/RootServer.php: -------------------------------------------------------------------------------- 1 | boot(); 25 | 26 | //Server URI 27 | $serverUri = "{$_ENV['HOST']}:{$_ENV['PORT']}"; 28 | 29 | //Load command listeners 30 | $colis = Colis::getListeners(); 31 | //Load provided servers 32 | $servers = Kernel::getServers(); 33 | $httpServers = $servers['http']; 34 | $socketServers = $servers['socket']; 35 | 36 | //Stores servers 37 | $instantiatedServers = []; 38 | 39 | //Socket servers 40 | foreach ($socketServers as $socketServer) { 41 | $instantiated = new $socketServer($colis); 42 | $instantiated->colis = $colis; 43 | $instantiatedServers[] = new WebSocketMiddleware( 44 | [$instantiated->prefix], 45 | new WsServer($instantiated) 46 | ); 47 | } 48 | 49 | //Http servers 50 | foreach ($httpServers as $httpServer) { 51 | $instantiatedServers[] = new $httpServer(); 52 | } 53 | 54 | //Boot http service provider 55 | HttpServiceProvider::init()->boot(); 56 | //Collect http routes 57 | RouteCollector::collectRoutes(); 58 | //Register http routes 59 | RouteCollector::register(); 60 | 61 | //Start database connection 62 | Connection::create(); 63 | 64 | //Init server 65 | $server = new HttpServer( 66 | //Static file response handler 67 | StaticFileResponseMiddleware::create(), 68 | //Instantiated servers 69 | ...$instantiatedServers 70 | ); 71 | 72 | //Create servers 73 | $server->listen(new SocketServer($serverUri)); 74 | 75 | // Track online users 76 | UserPresence::initialize(); 77 | 78 | console(true)->write("[*] HttpServer-Server running on http://{$serverUri}"); 79 | console(true)->write("\n[*] Admin-SocketServer-Server running on ws://{$serverUri}{$_ENV['ADMIN_SOCKET_URL_PREFIX']}"); 80 | console(true)->write("\n[*] Public-Chat-SocketServer-Server running on ws://{$serverUri}{$_ENV['PUBLIC_CHAT_SOCKET_URL_PREFIX']}"); 81 | console(true)->write("\n[*] Private-Chat-SocketServer-Server running on ws://{$serverUri}{$_ENV['PRIVATE_CHAT_SOCKET_URL_PREFIX']}"); 82 | 83 | $server->on('error', 'handleApplicationException'); 84 | 85 | //Run event loop 86 | Loop::run(); 87 | } 88 | } -------------------------------------------------------------------------------- /src/ServerStore.php: -------------------------------------------------------------------------------- 1 | timers = new ArrayObject($_ENV); 19 | } 20 | 21 | public static function getInstance(): ServerStore 22 | { 23 | if (isset(ServerStore::$instance)) { 24 | return ServerStore::$instance; 25 | } 26 | 27 | return ServerStore::$instance = new ServerStore(); 28 | } 29 | 30 | /** 31 | * @return ArrayObject 32 | */ 33 | public function timers(): ArrayObject 34 | { 35 | return $this->timers; 36 | } 37 | } -------------------------------------------------------------------------------- /src/Servers/Http/Middleware/StaticFileResponseMiddleware.php: -------------------------------------------------------------------------------- 1 | getUri(); 22 | 23 | if ($_ENV['SHOW_HTTP_RESOURCE_REQUEST'] == 'true') { 24 | echo "\n" . date('H:i:s'); 25 | echo " -> New request({$url}).\n"; 26 | } 27 | 28 | $logger = new NullLogger(); // Require, PSR-3 logger for bootstrap logging 29 | $cache = new ArrayCache(); // Required, custom cache configuration 30 | return (new WebrootPreloadMiddleware( 31 | public_path(), 32 | $logger, $cache 33 | ))($request, $next); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Servers/HttpServer.php: -------------------------------------------------------------------------------- 1 | $name(...$args); 30 | } 31 | 32 | /** 33 | * Get all registered listeners 34 | * @return array 35 | */ 36 | public static function getListeners(): array 37 | { 38 | require(root_path('routes/websocket.php')); 39 | return self::$called; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Websocket/Colis/ColisInterface.php: -------------------------------------------------------------------------------- 1 | colis(); 28 | //Message 29 | $payload = $request->payload(); 30 | //If the matching listener listener 31 | $needle = self::findNeedle($colis, $payload); 32 | 33 | if (!$needle) { 34 | return resp($request->client())->send( 35 | 'system.response.404', 36 | "Command '{$payload->command}' not found" 37 | ); 38 | } 39 | 40 | $providedListener = $needle['listener']; 41 | 42 | if (is_callable($providedListener)) { 43 | return call_user_func($providedListener, $request); 44 | } 45 | 46 | $expListener = explode('@', $providedListener); 47 | $listenerClassName = $expListener[0]; 48 | $listenerMethod = $expListener[1]; 49 | //Listener namespace 50 | $listenerNS = self::$listenersNamespace . $needle['namespace']; 51 | 52 | //Apply default namespace 53 | $listenerClassFile = $listenerNS . $listenerClassName; 54 | 55 | try { 56 | $class = (new $listenerClassFile)->_initAndFeed_([ 57 | 'client' => $request->client(), 58 | 'request' => $request, 59 | ]); 60 | 61 | if (!empty($needle['middleware'])) { 62 | $middlewares = Kernel::getMiddlewares(); 63 | if (isset($middlewares['colis-middleware'][$needle['middleware']])) { 64 | $middleware = $middlewares['colis-middleware'][$needle['middleware']]; 65 | 66 | return (new $middleware()) 67 | ->handle($request, fn() => resolve()) 68 | ->then(function () use ($class, $listenerMethod, $request) { 69 | return $class->$listenerMethod($request); 70 | }); 71 | } else { 72 | throw new Exception("Middleware {$needle['middleware']} not found."); 73 | } 74 | } 75 | 76 | return $class->$listenerMethod($request); 77 | } catch (Throwable $exception) { 78 | handleApplicationException($exception); 79 | resp($request->client())->send('system.response.500'); 80 | } 81 | 82 | return resp($request->client())->internalServerError(); 83 | } 84 | 85 | /** 86 | * @param TheColis[] $colis 87 | * @param Payload $payload 88 | * @return array 89 | */ 90 | public static function findNeedle(array $colis, Payload $payload) 91 | { 92 | $needle = null; 93 | foreach ($colis as $coli) { 94 | $coliData = $coli->getListeners(); 95 | foreach ($coliData['listeners'] as $command => $listener) { 96 | $listenerName = $coliData['prefix'] . $command; 97 | 98 | if ($listenerName == ($payload->command ?? null)) { 99 | $needle = [ 100 | 'namespace' => $coliData['namespace'], 101 | 'listener' => $listener, 102 | 'middleware' => $coliData['middleware'] 103 | ]; 104 | break; 105 | } 106 | } 107 | if ($needle) break; 108 | } 109 | 110 | return $needle; 111 | } 112 | } -------------------------------------------------------------------------------- /src/Websocket/Colis/TheColis.php: -------------------------------------------------------------------------------- 1 | isWithUsed = true; 24 | $this->namespace = $withData['namespace']; 25 | $this->middleware = $withData['middleware']; 26 | $this->prefix = $withData['prefix']; 27 | return $this; 28 | } 29 | 30 | /** 31 | * @param string $prefix 32 | * @return TheColis $this 33 | */ 34 | public function prefix(string $prefix): ColisInterface 35 | { 36 | if ($this->prefix && !$this->isWithUsed) { 37 | return Colis::with([ 38 | 'namespace' => $this->namespace, 39 | 'middleware' => $this->middleware, 40 | 'prefix' => $this->prefix 41 | ])->prefix($prefix); 42 | } 43 | 44 | $this->prefix .= $prefix; 45 | return $this; 46 | } 47 | 48 | /** 49 | * Group listeners 50 | * @param callable $closure 51 | * @return TheColis $this 52 | */ 53 | public function group(callable $closure): ColisInterface 54 | { 55 | $closure($this); 56 | return $this; 57 | } 58 | 59 | /** 60 | * Add namespace to listener groups 61 | * @param string $namespace 62 | * @return TheColis $this 63 | */ 64 | public function namespace(string $namespace): ColisInterface 65 | { 66 | if ($namespace[strlen($namespace) - 1] !== "\\") { 67 | $namespace .= "\\"; 68 | } 69 | $this->namespace .= $namespace; 70 | return $this; 71 | } 72 | 73 | /** 74 | * Set middleware to command 75 | * @param string $middleware 76 | * @return $this 77 | */ 78 | public function middleware(string $middleware): TheColis 79 | { 80 | $this->middleware = $middleware; 81 | return $this; 82 | } 83 | 84 | /** 85 | * Listen to command 86 | * @param string $command 87 | * @param callable|string $listenerClass 88 | * @return TheColis $this 89 | */ 90 | public function listen(string $command, $listenerClass): ColisInterface 91 | { 92 | $this->listeners[$command] = $listenerClass; 93 | return $this; 94 | } 95 | 96 | /** 97 | * Retrieve listeners defined in this object 98 | * @return array 99 | */ 100 | public function getListeners(): array 101 | { 102 | return [ 103 | 'prefix' => $this->prefix, 104 | 'namespace' => $this->namespace, 105 | 'middleware' => $this->middleware, 106 | 'listeners' => $this->listeners, 107 | ]; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Websocket/ConnectionFactory.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 16 | 17 | $this->resourceId = spl_object_id($connection); 18 | } 19 | 20 | public static function init(WebSocketConnection $connection): self 21 | { 22 | return new self($connection); 23 | } 24 | 25 | public function send($payload) 26 | { 27 | if (!is_string($payload)) { 28 | $payload = json_encode($payload); 29 | } 30 | 31 | $this->connection->send($payload); 32 | } 33 | 34 | public function close() 35 | { 36 | $this->connection->close(); 37 | } 38 | 39 | public function getConnectionId(): int 40 | { 41 | return $this->resourceId; 42 | } 43 | 44 | public function getConnection(): WebSocketConnection 45 | { 46 | return $this->connection; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Websocket/ConnectionInterface.php: -------------------------------------------------------------------------------- 1 | auth()->check()) { 17 | return resolve(); 18 | } 19 | 20 | resp($request->client())->send('system.response.403', [ 21 | 'action' => 'redirect', 22 | 'uri' => '/login' 23 | ]); 24 | 25 | return reject(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Websocket/Middleware/ColisMiddleware.php: -------------------------------------------------------------------------------- 1 | __construct($this->originalPayload); 44 | 45 | return $this; 46 | } 47 | 48 | public function __construct(string $strPayload) 49 | { 50 | $payload = json_decode($strPayload); 51 | 52 | $this->originalPayload = $strPayload; 53 | 54 | if (!$payload->command) { 55 | InvalidPayloadException::create('No payload command specified.'); 56 | } 57 | 58 | $this->command = $payload->command ?? ''; 59 | 60 | $this->token = $payload->token ?? ''; 61 | 62 | $this->message = $payload->message ?? null; 63 | 64 | $this->time = (float)($payload->time ?? 0); 65 | 66 | 67 | foreach ($payload as $item => $value) { 68 | $this->$item = $value; 69 | } 70 | } 71 | 72 | /** 73 | * @return string 74 | */ 75 | public function getOriginalPayload(): string 76 | { 77 | return $this->originalPayload; 78 | } 79 | } -------------------------------------------------------------------------------- /src/Websocket/Request.php: -------------------------------------------------------------------------------- 1 | $object) { 22 | $this->$objName = $object; 23 | } 24 | } 25 | 26 | public static function init(array $objects): Request 27 | { 28 | return new Request($objects); 29 | } 30 | 31 | /** 32 | * Authentication class 33 | * @return Auth* @var Auth $auth 34 | */ 35 | public function auth(): Auth 36 | { 37 | return $this->auth; 38 | } 39 | 40 | /** 41 | * Command listeners 42 | * @return TheColis[] 43 | */ 44 | public function colis(): array 45 | { 46 | return $this->colis; 47 | } 48 | 49 | /** 50 | * Received json decoded message from client 51 | * @return Payload|stdClass 52 | */ 53 | public function payload() 54 | { 55 | return $this->payload; 56 | } 57 | 58 | /** 59 | * Connected client 60 | * @return ConnectionInterface 61 | */ 62 | public function client(): ConnectionInterface 63 | { 64 | return $this->client; 65 | } 66 | } -------------------------------------------------------------------------------- /src/Websocket/Response.php: -------------------------------------------------------------------------------- 1 | client = $client; 12 | } 13 | 14 | public function internalServerError(): Response 15 | { 16 | return resp($this->client)->send('system.500', 'Internal server error'); 17 | } 18 | 19 | /** 20 | * Send message to client 21 | * @param string $command 22 | * @param mixed $message 23 | * @return $this 24 | */ 25 | public function send(string $command, $message = null): Response 26 | { 27 | $jsonMessage = json_encode([ 28 | 'command' => $command, 29 | 'message' => $message, 30 | 'time' => time(), 31 | ]); 32 | 33 | if ($_ENV['SHOW_SOCKET_OUTGOING_MESSAGES'] == 'true') { 34 | if ( 35 | //We don't want to display unnecessary logs 36 | ( 37 | $command != 'system.ping' 38 | && $command != 'system.pong' 39 | && $command != 'system.ping.interval' 40 | ) 41 | //If we don't want to display system logs 42 | && ( 43 | $_ENV['SILENCE_SYSTEM_MESSAGES'] == 'true' ?: 44 | substr($command, 0, 6) != 'system' 45 | ) 46 | ) { 47 | console(true)->write("\n" . date('H:i:s')); 48 | console(true)->write(" -> Sending({$jsonMessage}) to UserStorage({$this->client->getConnectionId()}).\n"); 49 | } 50 | } 51 | 52 | //Send the json encoded message to client 53 | $this->client->send($jsonMessage); 54 | 55 | return $this; 56 | } 57 | 58 | public static function push(ConnectionInterface $connection, string $command, $message = null) 59 | { 60 | return (new static($connection))->send($command, $message); 61 | } 62 | } -------------------------------------------------------------------------------- /src/Websocket/State.php: -------------------------------------------------------------------------------- 1 | pong($client); 11 | } 12 | 13 | private function pong(ConnectionInterface $client): void 14 | { 15 | resp($client)->send('system.pong', 'acknowledged'); 16 | } 17 | 18 | public static function handlePong(ConnectionInterface $client): void 19 | { 20 | //InOut::send($client, self::$pongMessage); 21 | } 22 | 23 | public function ping(ConnectionInterface $client): void 24 | { 25 | resp($client)->send('system.ping', 'acknowledged'); 26 | } 27 | } -------------------------------------------------------------------------------- /src/WsServer.php: -------------------------------------------------------------------------------- 1 | handler = $handler; 23 | } 24 | 25 | public function __invoke( 26 | WebSocketConnection $connection, 27 | ServerRequestInterface $request, 28 | ResponseInterface $response 29 | ): void 30 | { 31 | SocketServiceProvider::init()->boot(); 32 | 33 | $constructedConnection = ConnectionFactory::init($connection); 34 | 35 | $this->handler->onOpen($constructedConnection); 36 | 37 | $connection->on('message', function (Message $message) use ($constructedConnection) { 38 | try { 39 | $constructedPayload = Payload::init($message->getPayload()); 40 | $this->handler->onMessage($constructedConnection, $constructedPayload); 41 | } catch (InvalidPayloadException $payloadException) { 42 | $constructedConnection->send([ 43 | 'command' => 'system.response.500', 44 | 'message' => $payloadException->getMessage() 45 | ]); 46 | } 47 | }); 48 | 49 | $connection->on('error', function (Throwable $e) use ($constructedConnection) { 50 | $this->handler->onError($constructedConnection, $e); 51 | }); 52 | 53 | $connection->on('close', function () use ($constructedConnection) { 54 | $this->handler->onClose($constructedConnection); 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /storage/cache/route/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ahmard/reactphp-live-chat/5b19073219e5dff7458c182bf920d21d3440ad03/storage/cache/route/.gitkeep -------------------------------------------------------------------------------- /storage/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ahmard/reactphp-live-chat/5b19073219e5dff7458c182bf920d21d3440ad03/storage/logs/.gitkeep --------------------------------------------------------------------------------