diff --git a/.env b/.env index 15ed30f..dd35b21 100644 --- a/.env +++ b/.env @@ -19,7 +19,7 @@ APP_ENV=dev APP_SECRET=EDIT_ME ###< symfony/framework-bundle ### -APP_VERSION=1.3.1 +APP_VERSION=1.4.0 APP_NAME=KevaChat @@ -61,23 +61,32 @@ APP_KEVACOIN_ROOM_NAMESPACE_DEFAULT=EDIT_ME # Online session expiration timeout APP_SESSION_ONLINE_TIMEOUT=900 +# Moderators IP with extra permissions, separated with | +APP_MODERATOR_REMOTE_IP= + # Allow remotes to create new rooms (namespaces) APP_ADD_ROOM_REMOTE_IP_REGEX=/.*/ +# Time quota delay for new room submit ability per IP (seconds) +APP_ADD_ROOM_REMOTE_IP_DELAY=86400 + +# Skip access limits for banned IPs separated by | +APP_ADD_ROOM_REMOTE_IP_DENIED= + +# Room name rules (for kevacoin _KEVA_NS_, max length 520) +APP_ADD_ROOM_KEVA_NS_VALUE_REGEX=/^[\w\s_-]{2,140}$/ui + # Allow remotes to create new posts (submit key/values) APP_ADD_POST_REMOTE_IP_REGEX=/.*/ -# Time quota for remote publications by IP (seconds) +# Time quota delay for post publications per IP (seconds) APP_ADD_POST_REMOTE_IP_DELAY=60 -# Skip access limits for following IPs separated by | -APP_ADD_POST_REMOTE_IP_MODERATORS= - # Skip access limits for banned IPs separated by | APP_ADD_POST_REMOTE_IP_DENIED= # Post ID rules (for kevacoin key) do not change to keep external KevaChat nodes compatibility APP_ADD_POST_KEY_REGEX=/^([\d]+)@([A-z0-9\.\:\[\]]+)$/ -# Post content rules (for kevacoin value) +# Post content rules (for kevacoin value, max length 3072) APP_ADD_POST_VALUE_REGEX=/^[\w\s\:\.\,\'\"\/\!\?\@\#\%\(\)\[\]\+\-\*\$\%]{2,3072}$/ui \ No newline at end of file diff --git a/composer.lock b/composer.lock index 45c329b..6f827e5 100644 --- a/composer.lock +++ b/composer.lock @@ -132,16 +132,16 @@ }, { "name": "kevachat/kevacoin", - "version": "1.4.2", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/kevachat/kevacoin-php.git", - "reference": "b6e5416c58d379fd197bbd6d1039a30042ea0481" + "reference": "44608e688f69ff1545f2a3103c4504bfb3294831" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kevachat/kevacoin-php/zipball/b6e5416c58d379fd197bbd6d1039a30042ea0481", - "reference": "b6e5416c58d379fd197bbd6d1039a30042ea0481", + "url": "https://api.github.com/repos/kevachat/kevacoin-php/zipball/44608e688f69ff1545f2a3103c4504bfb3294831", + "reference": "44608e688f69ff1545f2a3103c4504bfb3294831", "shasum": "" }, "type": "library", @@ -156,9 +156,9 @@ ], "support": { "issues": "https://github.com/kevachat/kevacoin-php/issues", - "source": "https://github.com/kevachat/kevacoin-php/tree/1.4.2" + "source": "https://github.com/kevachat/kevacoin-php/tree/1.5.0" }, - "time": "2023-12-06T09:20:08+00:00" + "time": "2023-12-09T18:55:04+00:00" }, { "name": "league/commonmark", diff --git a/config/services.yaml b/config/services.yaml index edaf413..6744c66 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -24,12 +24,15 @@ parameters: app.kevacoin.mine.solo.url: '%env(APP_KEVACOIN_MINE_SOLO_URL)%' app.session.online.timeout: '%env(APP_SESSION_ONLINE_TIMEOUT)%' app.add.room.remote.ip.regex: '%env(APP_ADD_ROOM_REMOTE_IP_REGEX)%' + app.add.room.remote.ip.delay: '%env(APP_ADD_ROOM_REMOTE_IP_DELAY)%' + app.add.room.remote.ip.denied: '%env(APP_ADD_ROOM_REMOTE_IP_DENIED)%' + app.add.room.keva.ns.value.regex: '%env(APP_ADD_ROOM_KEVA_NS_VALUE_REGEX)%' app.add.post.remote.ip.regex: '%env(APP_ADD_POST_REMOTE_IP_REGEX)%' app.add.post.remote.ip.delay: '%env(APP_ADD_POST_REMOTE_IP_DELAY)%' - app.add.post.remote.ip.moderators: '%env(APP_ADD_POST_REMOTE_IP_MODERATORS)%' app.add.post.remote.ip.denied: '%env(APP_ADD_POST_REMOTE_IP_DENIED)%' app.add.post.key.regex: '%env(APP_ADD_POST_KEY_REGEX)%' app.add.post.value.regex: '%env(APP_ADD_POST_VALUE_REGEX)%' + app.moderator.remote.ip: '%env(APP_MODERATOR_REMOTE_IP)%' services: # default configuration for services in *this* file diff --git a/public/css/default.css b/public/css/default.css index fd6b11a..722f86d 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -155,6 +155,7 @@ footer > form width: 100%; } +footer > form > input[type="text"], footer > form > textarea { box-sizing: border-box; diff --git a/src/Controller/ModuleController.php b/src/Controller/ModuleController.php index 8dd9a30..ebda53c 100644 --- a/src/Controller/ModuleController.php +++ b/src/Controller/ModuleController.php @@ -99,18 +99,27 @@ class ModuleController extends AbstractController Request $request ): Response { - $client = new \Kevachat\Kevacoin\Client( - $this->getParameter('app.kevacoin.protocol'), - $this->getParameter('app.kevacoin.host'), - $this->getParameter('app.kevacoin.port'), - $this->getParameter('app.kevacoin.username'), - $this->getParameter('app.kevacoin.password') - ); + // Create rooms list + $list = []; + + foreach ((array) explode('|', $this->getParameter('app.kevacoin.room.namespaces.pinned')) as $room) + { + $list[] = $room; + } + + // Append custom valid namespace to the rooms list menu + if (!in_array($request->get('namespace'), $list) && preg_match('/^[A-z0-9]{34}$/', $request->get('namespace'))) + { + $list[] = $request->get('namespace'); + } + // Render the template return $this->render( 'default/module/rooms.html.twig', [ - 'list' => (array) explode('|', $this->getParameter('app.kevacoin.room.namespaces.pinned')), + 'list' => array_unique( + $list + ), 'form' => [ 'namespace' => @@ -178,4 +187,17 @@ class ModuleController extends AbstractController ] ); } + + public function room( + Request $request + ): Response + { + return $this->render( + 'default/module/room.html.twig', + [ + 'name' => $request->get('name'), + 'error' => $request->get('error') + ] + ); + } } \ No newline at end of file diff --git a/src/Controller/RoomController.php b/src/Controller/RoomController.php index 04f92d3..78d6b4e 100644 --- a/src/Controller/RoomController.php +++ b/src/Controller/RoomController.php @@ -95,9 +95,9 @@ class RoomController extends AbstractController array_multisort( array_column( $list, - 'name' + 'total' ), - SORT_ASC, + SORT_DESC, $list ); @@ -165,7 +165,7 @@ class RoomController extends AbstractController $feed = []; // Get pending paradise - foreach ((array) $client->kevaPending() as $pending) + foreach ((array) $client->kevaPending() as $pending) // @TODO relate to this room { // Ignore pending posts from other rooms if ($pending['namespace'] !== $request->get('namespace')) @@ -347,7 +347,7 @@ class RoomController extends AbstractController $this->getParameter('app.kevacoin.password') ); - // Check namespace defined in config + // Check namespace available on this wallet $rooms = []; foreach ((array) $client->kevaListNamespaces() as $value) @@ -373,7 +373,7 @@ class RoomController extends AbstractController // Ignore this rule for is moderators !in_array( $request->getClientIp(), - (array) explode('|', $this->getParameter('app.add.post.remote.ip.moderators')) + (array) explode('|', $this->getParameter('app.moderator.remote.ip')) ) && // Check namespace writable or user is moderator @@ -401,7 +401,10 @@ class RoomController extends AbstractController [ 'namespace' => $request->get('namespace'), 'message' => $request->get('message'), - 'error' => $translator->trans('Access denied for this IP!') + 'error' => sprintf( + $translator->trans('Access denied for host %s!'), + $request->getClientIp() + ) ] ); } @@ -414,7 +417,10 @@ class RoomController extends AbstractController [ 'namespace' => $request->get('namespace'), 'message' => $request->get('message'), - 'error' => $translator->trans('Access not allowed for this IP!') + 'error' => sprintf( + $translator->trans('Access restricted for host %s!'), + $request->getClientIp() + ) ] ); } @@ -445,7 +451,7 @@ class RoomController extends AbstractController 'namespace' => $request->get('namespace'), 'message' => $request->get('message'), 'error' => sprintf( - $translator->trans('Please wait for %s seconds before post new message!'), + $translator->trans('Please wait %s seconds before post new message!'), (int) $this->getParameter('app.add.post.remote.ip.delay') - (time() - $delay) ) ] @@ -462,7 +468,7 @@ class RoomController extends AbstractController 'message' => $request->get('message'), 'error' => sprintf( $translator->trans('Insufficient funds, wallet: %s'), - $this->getParameter('app.kevacoin.mine.address') + $this->getParameter('app.kevacoin.boost.address') ) ] ); @@ -510,6 +516,187 @@ class RoomController extends AbstractController ); } + #[Route( + '/room/add', + name: 'room_add', + methods: + [ + 'POST' + ] + )] + public function add( + Request $request, + TranslatorInterface $translator + ): Response + { + // Check maintenance mode disabled + if ($this->getParameter('app.maintenance')) + { + return $this->redirectToRoute( + 'room_namespace', + [ + 'namespace' => $request->get('namespace'), + 'message' => $request->get('message'), + 'error' => $this->getParameter('app.maintenance') + ] + ); + } + + // Connect memcached + $memcached = new \Memcached(); + $memcached->addServer( + $this->getParameter('app.memcached.host'), + $this->getParameter('app.memcached.port') + ); + + $memory = md5( + sprintf( + '%s.RoomController::add:add.room.remote.ip.delay:%s', + __DIR__, + $request->getClientIp(), + ), + ); + + // Connect kevacoin + $client = new \Kevachat\Kevacoin\Client( + $this->getParameter('app.kevacoin.protocol'), + $this->getParameter('app.kevacoin.host'), + $this->getParameter('app.kevacoin.port'), + $this->getParameter('app.kevacoin.username'), + $this->getParameter('app.kevacoin.password') + ); + + // Trim extra spaces from room name + $name = trim( + $request->get('name') + ); + + // Validate room name regex + if (!preg_match($this->getParameter('app.add.room.keva.ns.value.regex'), $name)) + { + return $this->redirectToRoute( + 'room_list', + [ + 'name' => $name, + 'error' => sprintf( + $translator->trans('Room name does not match node requirements: %s'), + $this->getParameter('app.add.room.keva.ns.value.regex') + ) + ] + ); + } + + // Check room name not added before + $rooms = []; + + foreach ((array) $client->kevaListNamespaces() as $value) + { + $rooms[$value['namespaceId']] = $value['displayName']; + } + + if (in_array($name, $rooms)) + { + return $this->redirectToRoute( + 'room_list', + [ + 'name' => $name, + 'error' => $translator->trans('Room with same name already exists on this node!') + ] + ); + } + + // Deny requests from banned remote hosts + if (in_array($request->getClientIp(), (array) explode('|', $this->getParameter('app.add.room.remote.ip.denied')))) + { + return $this->redirectToRoute( + 'room_list', + [ + 'name' => $name, + 'error' => sprintf( + $translator->trans('Access denied for host %s!'), + $request->getClientIp() + ) + ] + ); + } + + // Validate remote IP regex + if (!preg_match($this->getParameter('app.add.room.remote.ip.regex'), $request->getClientIp())) + { + return $this->redirectToRoute( + 'room_list', + [ + 'name' => $name, + 'error' => sprintf( + $translator->trans('Access restricted for host %s!'), + $request->getClientIp() + ) + ] + ); + } + + // Validate remote IP limits + if ($delay = (int) $memcached->get($memory)) + { + // Error + return $this->redirectToRoute( + 'room_list', + [ + 'name' => $name, + 'error' => sprintf( + $translator->trans('Please wait for %s seconds before add new room!'), + (int) $this->getParameter('app.add.room.remote.ip.delay') - (time() - $delay) + ) + ] + ); + } + + // Validate funds available yet + if (1 > $client->getBalance()) + { + return $this->redirectToRoute( + 'room_list', + [ + 'name' => $name, + 'error' => sprintf( + $translator->trans('Insufficient funds, wallet: %s'), + $this->getParameter('app.kevacoin.boost.address') + ) + ] + ); + } + + // Send message to DHT + if ($namespace = $client->kevaNamespace($name)) + { + // Register event time + $memcached->set( + $memory, + time(), + (int) $this->getParameter('app.add.room.remote.ip.delay') // auto remove on cache expire + ); + + // Redirect to new room + return $this->redirectToRoute( + 'room_namespace', + [ + 'namespace' => $namespace['namespaceId'], + 'error' => null, + 'message' => null + ] + ); + } + + // Something went wrong, return error message + return $this->redirectToRoute( + 'room_list', + [ + 'name' => $name, + 'error' => $translator->trans('Internal error! Please feedback') + ] + ); + } + private function _post(array $data): ?object { // Validate key format allowed in settings diff --git a/templates/default/module/room.html.twig b/templates/default/module/room.html.twig new file mode 100644 index 0000000..e125a24 --- /dev/null +++ b/templates/default/module/room.html.twig @@ -0,0 +1,7 @@ +
+ {% if error %} + {{ error }} + {% endif %} + + +
\ No newline at end of file diff --git a/templates/default/module/rooms.html.twig b/templates/default/module/rooms.html.twig index 4b0da72..bf554b0 100644 --- a/templates/default/module/rooms.html.twig +++ b/templates/default/module/rooms.html.twig @@ -1,4 +1,4 @@ -
+
{% if list %} diff --git a/templates/default/room/list.html.twig b/templates/default/room/list.html.twig index 2b46d20..081761f 100644 --- a/templates/default/room/list.html.twig +++ b/templates/default/room/list.html.twig @@ -43,4 +43,16 @@ {% endif %} +{% endblock %} +{% block footer_content %} + {{ + render( + controller( + 'App\\Controller\\ModuleController::room', + { + request: request + } + ) + ) + }} {% endblock %} \ No newline at end of file