diff --git a/README.md b/README.md index b0b6c06..787dcfc 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ KevaChat Application for Gemini Protocol * [x] Multiple host support * [x] Room list -* [ ] Room threads +* [x] Room threads * [ ] Post publication * [ ] Media viewer * [ ] Users auth diff --git a/example/config.json b/example/config.json index 4b56345..398522a 100644 --- a/example/config.json +++ b/example/config.json @@ -22,6 +22,10 @@ { "room": { + "key": + { + "regex":"/^[\\w\\s\\._-]{2,64}$/ui" + }, "pin":[] }, "post": diff --git a/src/controller/room.php b/src/controller/room.php index d02ea27..c73b25e 100644 --- a/src/controller/room.php +++ b/src/controller/room.php @@ -36,48 +36,22 @@ class Room continue; } + // Validate room name compatible with settings + if (!preg_match((string) $this->_config->kevachat->room->key->regex, $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])) + // Is protocol compatible post + if ($this->post($namespace['namespaceId'], $record['key'], 'txid')) { - continue; + $total++; } - - // 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 @@ -159,4 +133,208 @@ class Room ) ); } + + public function posts(string $namespace): ?string + { + $posts = []; + + foreach ((array) $this->_kevacoin->kevaFilter($namespace) as $record) + { + if (empty($record['key'])) + { + continue; + } + + if ($post = $this->post($namespace, $record['key'])) + { + $posts[] = $post; + } + } + + return str_replace( + [ + '{logo}', + '{subject}', + '{posts}' + ], + [ + file_get_contents( + __DIR__ . '/../../logo.ascii' + ), + $namespace, + implode( + PHP_EOL, + $posts + ) + ], + file_get_contents( + __DIR__ . '/../view/posts.gemini' + ) + ); + } + + public function post(string $namespace, string $key, ?string $field = null): ?string + { + // Check record exists + if (!$record = (array) $this->_kevacoin->kevaGet($namespace, $key)) + { + return null; + } + + // Skip values with meta keys + if (str_starts_with($record['key'], '_')) + { + return null; + } + + // Validate value format allowed in settings + if (!preg_match((string) $this->_config->kevachat->post->value->regex, $record['value'])) + { + return null; + } + + // Validate key format allowed in settings + if (!preg_match($this->_config->kevachat->post->key->regex, $record['key'], $matches)) + { + return null; + } + + // Timestamp required in key + if (empty($matches[1])) + { + return null; + } + + // Username required in key + if (empty($matches[2])) + { + return null; + } + + // Is raw field request + if ($field && isset($record[$field])) + { + return $record[$field]; + } + + // Legacy usernames backport + if (!preg_match((string) $this->_config->kevachat->user->name->regex, $matches[2])) + { + $matches[2] = '@anon'; + } + + // Try to find related quote value + $quote = null; + if (preg_match('/^@([A-z0-9]{64})/', $record['value'], $mention)) + { + if (!empty($mention[1])) + { + $quote = '@' . $mention[1]; // @TODO replace to post message by txid + + // Remove mention from message + $record['value'] = preg_replace('/^@([A-z0-9]{64})/', null, $record['value']); + } + } + + // Build final view and send to response + return str_replace( + [ + '{time}', + '{author}', + '{quote}', + '{message}', + ], + [ + $this->_ago( + $matches[1] + ), + '@' . $matches[2], + trim( + $quote + ), + trim( + $record['value'] + ) + ], + file_get_contents( + __DIR__ . '/../view/post.gemini' + ) + ); + } + + private function _ago(int $time): string + { + $diff = time() - $time; + + if ($diff < 1) + { + return _('now'); + } + + $values = + [ + 365 * 24 * 60 * 60 => + [ + _('year ago'), + _('years ago'), + _(' years ago') + ], + 30 * 24 * 60 * 60 => + [ + _('month ago'), + _('months ago'), + _(' months ago') + ], + 24 * 60 * 60 => + [ + _('day ago'), + _('days ago'), + _(' days ago') + ], + 60 * 60 => + [ + _('hour ago'), + _('hours ago'), + _(' hours ago') + ], + 60 => + [ + _('minute ago'), + _('minutes ago'), + _(' minutes ago') + ], + 1 => + [ + _('second ago'), + _('seconds ago'), + _(' seconds ago') + ] + ]; + + foreach ($values as $key => $value) + { + $result = $diff / $key; + + if ($result >= 1) + { + $round = round($result); + + return sprintf( + '%s %s', + $round, + $this->_plural( + $round, + $value + ) + ); + } + } + } + + private function _plural(int $number, array $texts) + { + $cases = [2, 0, 1, 1, 1, 2]; + + return $texts[(($number % 100) > 4 && ($number % 100) < 20) ? 2 : $cases[min($number % 10, 5)]]; + } } \ No newline at end of file diff --git a/src/server.php b/src/server.php index 618422a..e1cb450 100644 --- a/src/server.php +++ b/src/server.php @@ -100,9 +100,25 @@ foreach ((array) scandir(__DIR__ . '/../host') as $host) // Dynamical requests default: - if (str_starts_with($request->getPath(), '/room/')) + // Room posts by namespace + if (preg_match('/^\/room\/(N[A-z0-9]{33})$/', $request->getPath(), $matches)) { - // @TODO + if (!empty($matches[1])) + { + include_once __DIR__ . '/controller/room.php'; + + $room = new \Kevachat\Geminiapp\Controller\Room( + $config + ); + + $response->setContent( + $room->posts( + $matches[1] + ) + ); + + return $response; + } } } diff --git a/src/view/post.gemini b/src/view/post.gemini index c872a18..74ee837 100644 --- a/src/view/post.gemini +++ b/src/view/post.gemini @@ -1,7 +1,6 @@ ## {author} - > {quote} - -``` {time} - -{message} \ No newline at end of file +{message} +``` +{time} +``` \ No newline at end of file diff --git a/src/view/posts.gemini b/src/view/posts.gemini new file mode 100644 index 0000000..330cd1c --- /dev/null +++ b/src/view/posts.gemini @@ -0,0 +1,5 @@ +```{logo}``` + +# {subject} + +{posts} \ No newline at end of file