mirror of https://github.com/kevachat/geminiapp
ghost
10 months ago
13 changed files with 466 additions and 1 deletions
@ -1,2 +1,30 @@ |
|||||||
# geminiapp |
# 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` |
@ -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" |
||||||
|
} |
||||||
|
} |
@ -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" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
|
||||||
|
_ __ ____ _ _ |
||||||
|
| |/ /_____ ____ _ / ___| |__ __ _| |_ |
||||||
|
| ' // _ \ \ / / _` | | | '_ \ / _` | __| |
||||||
|
| . \ __/\ V / (_| | |___| | | | (_| | |_ |
||||||
|
|_|\_\___| \_/ \__,_|\____|_| |_|\__,_|\__| |
@ -0,0 +1,43 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Kevachat\Geminiapp\Controller; |
||||||
|
|
||||||
|
class Error |
||||||
|
{ |
||||||
|
private $_config; |
||||||
|
|
||||||
|
public function __construct($config) |
||||||
|
{ |
||||||
|
$this->_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' |
||||||
|
) |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,162 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Kevachat\Geminiapp\Controller; |
||||||
|
|
||||||
|
class Room |
||||||
|
{ |
||||||
|
private $_config; |
||||||
|
|
||||||
|
private \Kevachat\Kevacoin\Client $_kevacoin; |
||||||
|
|
||||||
|
public function __construct($config) |
||||||
|
{ |
||||||
|
// Init config |
||||||
|
$this->_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' |
||||||
|
) |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,136 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
// Load dependencies |
||||||
|
require_once __DIR__ . '/../vendor/autoload.php'; |
||||||
|
|
||||||
|
// Scan host |
||||||
|
foreach ((array) scandir(__DIR__ . '/../host') as $host) |
||||||
|
{ |
||||||
|
// Skip meta |
||||||
|
if ($host == '.' || $host == '..' || is_file($host)) |
||||||
|
{ |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
// Check host configured |
||||||
|
if (!file_exists(__DIR__ . '/../host/' . $host . '/config.json')) |
||||||
|
{ |
||||||
|
echo sprintf( |
||||||
|
_('Host "%s" not configured!') . PHP_EOL, |
||||||
|
$host |
||||||
|
); |
||||||
|
|
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
// Check cert exists |
||||||
|
if (!file_exists(__DIR__ . '/../host/' . $host . '/cert.pem')) |
||||||
|
{ |
||||||
|
echo sprintf( |
||||||
|
_('Certificate for host "%s" not found!') . PHP_EOL, |
||||||
|
$host |
||||||
|
); |
||||||
|
|
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
// Check key exists |
||||||
|
if (!file_exists(__DIR__ . '/../host/' . $host . '/key.rsa')) |
||||||
|
{ |
||||||
|
echo sprintf( |
||||||
|
_('Key for host "%s" not found!') . PHP_EOL, |
||||||
|
$host |
||||||
|
); |
||||||
|
|
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
// Init config |
||||||
|
$config = json_decode( |
||||||
|
file_get_contents( |
||||||
|
__DIR__ . '/../host/' . $host . '/config.json' |
||||||
|
) |
||||||
|
); |
||||||
|
|
||||||
|
// Init server |
||||||
|
$server = new \Yggverse\TitanII\Server(); |
||||||
|
|
||||||
|
$server->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 |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
```{logo}``` |
||||||
|
|
||||||
|
# Oops! |
||||||
|
|
||||||
|
This page could not be opened |
||||||
|
|
||||||
|
Just try from |
||||||
|
|
||||||
|
=> {home} |
Loading…
Reference in new issue