mirror of
https://github.com/kevachat/geminiapp.git
synced 2025-03-10 04:21:02 +00:00
initial features draft
This commit is contained in:
parent
36b531afb1
commit
4da086c523
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/vendor/
|
||||
/host/*/
|
||||
|
||||
composer.lock
|
30
README.md
30
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`
|
15
composer.json
Normal file
15
composer.json
Normal file
@ -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
Normal file
46
example/config.json
Normal file
@ -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
Normal file
1
host/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/*/
|
6
logo.ascii
Normal file
6
logo.ascii
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
_ __ ____ _ _
|
||||
| |/ /_____ ____ _ / ___| |__ __ _| |_
|
||||
| ' // _ \ \ / / _` | | | '_ \ / _` | __|
|
||||
| . \ __/\ V / (_| | |___| | | | (_| | |_
|
||||
|_|\_\___| \_/ \__,_|\____|_| |_|\__,_|\__|
|
43
src/controller/error.php
Normal file
43
src/controller/error.php
Normal file
@ -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
Normal file
162
src/controller/room.php
Normal file
@ -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
Normal file
136
src/server.php
Normal file
@ -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
Normal file
9
src/view/oops.gemini
Normal file
@ -0,0 +1,9 @@
|
||||
```{logo}```
|
||||
|
||||
# Oops!
|
||||
|
||||
This page could not be opened
|
||||
|
||||
Just try from
|
||||
|
||||
=> {home}
|
7
src/view/post.gemini
Normal file
7
src/view/post.gemini
Normal file
@ -0,0 +1,7 @@
|
||||
## {author}
|
||||
|
||||
> {quote}
|
||||
|
||||
``` {time}
|
||||
|
||||
{message}
|
3
src/view/room.gemini
Normal file
3
src/view/room.gemini
Normal file
@ -0,0 +1,3 @@
|
||||
## {name} ({total})
|
||||
|
||||
=> {link}
|
5
src/view/rooms.gemini
Normal file
5
src/view/rooms.gemini
Normal file
@ -0,0 +1,5 @@
|
||||
```{logo}```
|
||||
|
||||
# Rooms
|
||||
|
||||
{rooms}
|
Loading…
x
Reference in New Issue
Block a user