Browse Source

initial features draft

main
ghost 10 months ago
parent
commit
4da086c523
  1. 4
      .gitignore
  2. 30
      README.md
  3. 15
      composer.json
  4. 46
      example/config.json
  5. 1
      host/.gitignore
  6. 6
      logo.ascii
  7. 43
      src/controller/error.php
  8. 162
      src/controller/room.php
  9. 136
      src/server.php
  10. 9
      src/view/oops.gemini
  11. 7
      src/view/post.gemini
  12. 3
      src/view/room.gemini
  13. 5
      src/view/rooms.gemini

4
.gitignore vendored

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
/vendor/
/host/*/
composer.lock

30
README.md

@ -1,2 +1,30 @@ @@ -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`

15
composer.json

@ -0,0 +1,15 @@ @@ -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"
}
}

46
example/config.json

@ -0,0 +1,46 @@ @@ -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"
}
}
}
}

1
host/.gitignore vendored

@ -0,0 +1 @@ @@ -0,0 +1 @@
/*/

6
logo.ascii

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
_ __ ____ _ _
| |/ /_____ ____ _ / ___| |__ __ _| |_
| ' // _ \ \ / / _` | | | '_ \ / _` | __|
| . \ __/\ V / (_| | |___| | | | (_| | |_
|_|\_\___| \_/ \__,_|\____|_| |_|\__,_|\__|

43
src/controller/error.php

@ -0,0 +1,43 @@ @@ -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'
)
);
}
}

162
src/controller/room.php

@ -0,0 +1,162 @@ @@ -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'
)
);
}
}

136
src/server.php

@ -0,0 +1,136 @@ @@ -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
);
}

9
src/view/oops.gemini

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
```{logo}```
# Oops!
This page could not be opened
Just try from
=> {home}

7
src/view/post.gemini

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
## {author}
> {quote}
``` {time}
{message}

3
src/view/room.gemini

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
## {name} ({total})
=> {link}

5
src/view/rooms.gemini

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
```{logo}```
# Rooms
{rooms}
Loading…
Cancel
Save