From 4da086c52388c3cd2cb4e2e118b92eed9caeca7c Mon Sep 17 00:00:00 2001 From: ghost Date: Thu, 25 Jan 2024 18:33:35 +0200 Subject: [PATCH] initial features draft --- .gitignore | 4 + README.md | 30 +++++++- composer.json | 15 ++++ example/config.json | 46 +++++++++++ host/.gitignore | 1 + logo.ascii | 6 ++ src/controller/error.php | 43 +++++++++++ src/controller/room.php | 162 +++++++++++++++++++++++++++++++++++++++ src/server.php | 136 ++++++++++++++++++++++++++++++++ src/view/oops.gemini | 9 +++ src/view/post.gemini | 7 ++ src/view/room.gemini | 3 + src/view/rooms.gemini | 5 ++ 13 files changed, 466 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 example/config.json create mode 100644 host/.gitignore create mode 100644 logo.ascii create mode 100644 src/controller/error.php create mode 100644 src/controller/room.php create mode 100644 src/server.php create mode 100644 src/view/oops.gemini create mode 100644 src/view/post.gemini create mode 100644 src/view/room.gemini create mode 100644 src/view/rooms.gemini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2b77e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor/ +/host/*/ + +composer.lock \ No newline at end of file diff --git a/README.md b/README.md index 22fd394..9e5372c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,30 @@ # geminiapp -Application for Gemini Protocol + +KevaChat Application for Gemini Protocol + +## Roadmap + +* [x] Multiple host support +* [x] Room list +* [ ] Room threads +* [ ] Post publication +* [ ] Media viewer +* [ ] Users auth +* [x] Error handlers + +## Install + +`git clone https://github.com/kevachat/geminiapp.git` +`cd geminiapp` +`composer update` + +## Setup + +* `mkdir host/127.0.0.1` +* `cp example/config.json host/127.0.0.1/config.json` +* `cp host/127.0.0.1` +* `openssl req -x509 -newkey rsa:4096 -keyout key.rsa -out cert.pem -days 365 -nodes -subj "/CN=127.0.0.1"` + +## Start + +* `php src/server.php` \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6e4f5c8 --- /dev/null +++ b/composer.json @@ -0,0 +1,15 @@ +{ + "name": "kevachat/geminiapp", + "description": "KevaChat Application for Gemini Protocol", + "type": "project", + "license": "MIT", + "autoload": { + "psr-4": { + "Kevachat\\Geminiapp\\": "src/" + } + }, + "require": { + "yggverse/titan-ii": "^1.0", + "kevachat/kevacoin": "^1.6" + } +} diff --git a/example/config.json b/example/config.json new file mode 100644 index 0000000..4b56345 --- /dev/null +++ b/example/config.json @@ -0,0 +1,46 @@ +{ + "kevacoin": + { + "server": + { + "protocol":"http", + "host":"127.0.0.1", + "port":9992, + "username":"", + "password":"" + } + }, + "gemini": + { + "server": + { + "host":"127.0.0.1", + "port":1965 + } + }, + "kevachat": + { + "room": + { + "pin":[] + }, + "post": + { + "key": + { + "regex":"/^([\\d]+)@([A-z0-9\\.\\:\\[\\]]+)$/" + }, + "value": + { + "regex":"/.*/ui" + } + }, + "user": + { + "name": + { + "regex":"/^[0-9A-z-]{2,16}$/ui" + } + } + } +} \ No newline at end of file diff --git a/host/.gitignore b/host/.gitignore new file mode 100644 index 0000000..aa51ac1 --- /dev/null +++ b/host/.gitignore @@ -0,0 +1 @@ +/*/ \ No newline at end of file diff --git a/logo.ascii b/logo.ascii new file mode 100644 index 0000000..9d13932 --- /dev/null +++ b/logo.ascii @@ -0,0 +1,6 @@ + + _ __ ____ _ _ + | |/ /_____ ____ _ / ___| |__ __ _| |_ + | ' // _ \ \ / / _` | | | '_ \ / _` | __| + | . \ __/\ V / (_| | |___| | | | (_| | |_ + |_|\_\___| \_/ \__,_|\____|_| |_|\__,_|\__| diff --git a/src/controller/error.php b/src/controller/error.php new file mode 100644 index 0000000..7bc19de --- /dev/null +++ b/src/controller/error.php @@ -0,0 +1,43 @@ +_config = $config; + } + + public function oops() + { + return str_replace( + [ + '{logo}', + '{home}' + ], + [ + file_get_contents( + __DIR__ . '/../../logo.ascii' + ), + ( + $this->_config->gemini->server->port == 1965 ? + sprintf( + 'gemini://%s', + $this->_config->gemini->server->host + ) : + sprintf( + 'gemini://%s:%d', + $this->_config->gemini->server->host, + $this->_config->gemini->server->port + ) + ) + ], + file_get_contents( + __DIR__ . '/../view/oops.gemini' + ) + ); + } +} \ No newline at end of file diff --git a/src/controller/room.php b/src/controller/room.php new file mode 100644 index 0000000..d02ea27 --- /dev/null +++ b/src/controller/room.php @@ -0,0 +1,162 @@ +_config = $config; + + // Init kevacoin + $this->_kevacoin = new \Kevachat\Kevacoin\Client( + $this->_config->kevacoin->server->protocol, + $this->_config->kevacoin->server->host, + $this->_config->kevacoin->server->port, + $this->_config->kevacoin->server->username, + $this->_config->kevacoin->server->password + ); + } + + public function list(): string + { + // Get room list + $namespaces = []; + + foreach ((array) $this->_kevacoin->kevaListNamespaces() as $namespace) + { + // Skip system namespaces + if (str_starts_with($namespace['displayName'], '_')) + { + continue; + } + + // Calculate room totals + $total = 0; + + foreach ((array) $this->_kevacoin->kevaFilter($namespace['namespaceId']) as $record) + { + // Skip values with meta keys + if (str_starts_with($record['key'], '_')) + { + continue; + } + + // Validate value format allowed in settings + if (!preg_match((string) $this->_config->kevachat->post->value->regex, $record['value'])) + { + continue; + } + + // Validate key format allowed in settings + if (!preg_match($this->_config->kevachat->post->key->regex, $record['key'], $matches)) + { + continue; + } + + // Timestamp required in key + if (empty($matches[1])) + { + continue; + } + + // Username required in key + if (empty($matches[2])) + { + continue; + } + + // Legacy usernames backport (used to replace undefined names to @anon) + /* + if (!preg_match((string) $this->_config->kevachat->user->name->regex, $matches[2])) + {} + */ + + $total++; + } + + // Add to room list + $namespaces[] = + [ + 'namespace' => $namespace['namespaceId'], + 'name' => $namespace['displayName'], + 'total' => $total, + 'pin' => in_array( + $namespace['namespaceId'], + $this->_config->kevachat->room->pin + ) + ]; + } + + // Sort rooms by total + array_multisort( + array_column( + $namespaces, + 'total' + ), + SORT_DESC, + $namespaces + ); + + // Build rooms view + $view = file_get_contents( + __DIR__ . '/../view/room.gemini' + ); + + $rooms = []; + foreach ($namespaces as $namespace) + { + $rooms[] = str_replace( + [ + '{name}', + '{total}', + '{link}' + ], + [ + $namespace['name'], + $namespace['total'], + ( + $this->_config->gemini->server->port == 1965 ? + sprintf( + 'gemini://%s/room/%s', + $this->_config->gemini->server->host, + $namespace['namespace'] + ) : + sprintf( + 'gemini://%s:%d/%s', + $this->_config->gemini->server->host, + $this->_config->gemini->server->port, + $namespace['namespace'] + ) + ) + ], + $view + ); + } + + // Build final view and send to response + return str_replace( + [ + '{logo}', + '{rooms}' + ], + [ + file_get_contents( + __DIR__ . '/../../logo.ascii' + ), + implode( + PHP_EOL, + $rooms + ) + ], + file_get_contents( + __DIR__ . '/../view/rooms.gemini' + ) + ); + } +} \ No newline at end of file diff --git a/src/server.php b/src/server.php new file mode 100644 index 0000000..185c3b6 --- /dev/null +++ b/src/server.php @@ -0,0 +1,136 @@ +setCert( + __DIR__ . '/../host/' . $host . '/cert.pem' + ); + + $server->setKey( + __DIR__ . '/../host/' . $host . '/key.rsa' + ); + + $server->setHandler( + function (\Yggverse\TitanII\Request $request): \Yggverse\TitanII\Response + { + global $config; + + $response = new \Yggverse\TitanII\Response(); + + $response->setCode( + 20 + ); + + $response->setMeta( + 'text/gemini' + ); + + // Route begin + switch ($request->getPath()) + { + // Home page + case '/': + + // Get rooms list + include_once __DIR__ . '/controller/room.php'; + + $room = new \Kevachat\Geminiapp\Controller\Room( + $config + ); + + $response->setContent( + $room->list() + ); + + return $response; + + // Dynamical requests + default: + + if (str_starts_with($request->getPath(), '/room/')) + { + // @TODO + } + } + + // Set default response + include_once __DIR__ . '/controller/error.php'; + + $error = new \Kevachat\Geminiapp\Controller\Error( + $config + ); + + $response->setContent( + $error->oops() + ); + + return $response; + } + ); + + // Start server + echo sprintf( + _('Start server "%s" started on %s:%d') . PHP_EOL, + $host, + $config->gemini->server->host, + $config->gemini->server->port + ); + + $server->start( + $config->gemini->server->host, + $config->gemini->server->port + ); +} \ No newline at end of file diff --git a/src/view/oops.gemini b/src/view/oops.gemini new file mode 100644 index 0000000..dd6c729 --- /dev/null +++ b/src/view/oops.gemini @@ -0,0 +1,9 @@ +```{logo}``` + +# Oops! + +This page could not be opened + +Just try from + +=> {home} \ No newline at end of file diff --git a/src/view/post.gemini b/src/view/post.gemini new file mode 100644 index 0000000..c872a18 --- /dev/null +++ b/src/view/post.gemini @@ -0,0 +1,7 @@ +## {author} + +> {quote} + +``` {time} + +{message} \ No newline at end of file diff --git a/src/view/room.gemini b/src/view/room.gemini new file mode 100644 index 0000000..04457db --- /dev/null +++ b/src/view/room.gemini @@ -0,0 +1,3 @@ +## {name} ({total}) + +=> {link} \ No newline at end of file diff --git a/src/view/rooms.gemini b/src/view/rooms.gemini new file mode 100644 index 0000000..44d6a82 --- /dev/null +++ b/src/view/rooms.gemini @@ -0,0 +1,5 @@ +```{logo}``` + +# Rooms + +{rooms} \ No newline at end of file