Added chat server selection endpoints.
This commit is contained in:
parent
52632b42eb
commit
3bd556fba9
6 changed files with 321 additions and 3 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
20250422
|
||||
20250422.1
|
||||
|
|
23
database/2025_04_22_203044_create_chat_servers_table.php
Normal file
23
database/2025_04_22_203044_create_chat_servers_table.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\DbMigration;
|
||||
|
||||
final class CreateChatServersTable_20250422_203044 implements DbMigration {
|
||||
public function migrate(DbConnection $conn): void {
|
||||
$conn->execute(<<<SQL
|
||||
CREATE TABLE msz_chat_servers (
|
||||
server_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
server_order INT(11) NOT NULL DEFAULT '0',
|
||||
server_proto VARCHAR(255) NOT NULL COLLATE 'ascii_general_ci',
|
||||
server_secure TINYINT(3) UNSIGNED NOT NULL,
|
||||
server_address VARCHAR(255) NOT NULL COLLATE 'ascii_bin',
|
||||
server_created TIMESTAMP NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (server_id),
|
||||
KEY msz_chat_servers_order_index (server_order),
|
||||
KEY msz_chat_servers_proto_index (server_proto),
|
||||
KEY msz_chat_servers_secure_index (server_secure),
|
||||
KEY msz_chat_servers_created_index (server_created)
|
||||
) COLLATE='utf8mb4_bin' ENGINE=InnoDB
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -2,12 +2,15 @@
|
|||
namespace Misuzu\Chat;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use Index\XArray;
|
||||
use Index\Config\Config;
|
||||
use Index\Http\{HttpRequest,HttpResponseBuilder,HttpUri};
|
||||
use Index\Http\Routing\AccessControl\AccessControl;
|
||||
use Index\Http\Routing\Processors\Before;
|
||||
use Index\Http\Routing\Routes\ExactRoute;
|
||||
use Index\Http\Routing\Routes\{ExactRoute,PatternRoute};
|
||||
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon};
|
||||
use Misuzu\FieldTransformer;
|
||||
use Misuzu\Auth\{AuthContext,AuthInfo};
|
||||
use Misuzu\Routing\{HandlerRoles,RouteHandler,RouteHandlerCommon};
|
||||
|
||||
|
@ -67,4 +70,120 @@ final class ChatApiRoutes implements RouteHandler, UrlSource {
|
|||
'access_token' => $this->authCtx->createAuthTokenPacker()->pack($this->authInfo->tokenInfo),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return FieldTransformer<ChatServerInfo> */
|
||||
private function createServerTransformer(bool $protoDefault = true, bool $secureDefault = true): FieldTransformer {
|
||||
return new FieldTransformer([
|
||||
'id' => [
|
||||
'transform' => fn($server) => $server->id,
|
||||
],
|
||||
'order' => [
|
||||
'transform' => fn($server) => $server->order,
|
||||
],
|
||||
'proto' => [
|
||||
'default' => $protoDefault,
|
||||
'transform' => fn($server) => $server->proto,
|
||||
],
|
||||
'secure' => [
|
||||
'default' => $secureDefault,
|
||||
'transform' => fn($server) => $server->secure,
|
||||
],
|
||||
'uri' => [
|
||||
'default' => true,
|
||||
'transform' => fn($server) => $server->uri,
|
||||
],
|
||||
'created' => [
|
||||
'transform' => fn($server) => $server->createdAt->toIso8601ZuluString(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return int|mixed[] */
|
||||
#[AccessControl]
|
||||
#[ExactRoute('GET', '/api/v1/chat/servers')]
|
||||
public function getServers(HttpResponseBuilder $response, HttpRequest $request): array|int {
|
||||
$proto = $request->hasParam('proto') ? $request->getParam('proto') : null;
|
||||
$secure = $request->hasParam('secure') ? match($request->getParam('secure')) {
|
||||
'1' => true,
|
||||
'0' => false,
|
||||
default => 0,
|
||||
} : null;
|
||||
|
||||
if($secure === 0)
|
||||
return 400;
|
||||
|
||||
$transformer = $this->createServerTransformer(
|
||||
$proto === null,
|
||||
$secure === null,
|
||||
);
|
||||
if(!$transformer->filter($request))
|
||||
return 400;
|
||||
|
||||
$response->setCacheControl('max-age=300', 'stale-if-error=600', 'public', 'immutable');
|
||||
|
||||
return XArray::select(
|
||||
$this->chatCtx->servers->getServers(
|
||||
proto: $proto,
|
||||
secure: $secure,
|
||||
),
|
||||
fn($server) => $transformer->convert($server),
|
||||
);
|
||||
}
|
||||
|
||||
/** @return int|mixed[] */
|
||||
#[AccessControl]
|
||||
#[PatternRoute('GET', '/api/v1/chat/servers/([0-9]+)')]
|
||||
public function getServer(HttpResponseBuilder $response, HttpRequest $request, string $id): array|int {
|
||||
if(empty($id))
|
||||
return 404;
|
||||
|
||||
$transformer = $this->createServerTransformer();
|
||||
if(!$transformer->filter($request))
|
||||
return 400;
|
||||
|
||||
try {
|
||||
$server = $this->chatCtx->servers->getServer($id);
|
||||
} catch(RuntimeException $ex) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
$response->setCacheControl('max-age=300', 'stale-if-error=600', 'public', 'immutable');
|
||||
|
||||
return $transformer->convert($server);
|
||||
}
|
||||
|
||||
/** @return int|mixed[] */
|
||||
#[AccessControl]
|
||||
#[ExactRoute('GET', '/api/v1/chat/servers/recommended')]
|
||||
public function getRecommendedServer(HttpResponseBuilder $response, HttpRequest $request): array|int {
|
||||
if(!$request->hasParam('proto'))
|
||||
return 400;
|
||||
|
||||
$proto = $request->getParam('proto');
|
||||
|
||||
if($request->hasParam('secure')) {
|
||||
$secure = match($request->getParam('secure')) {
|
||||
'1' => true,
|
||||
'0' => false,
|
||||
default => null,
|
||||
};
|
||||
if($secure === null)
|
||||
return 400;
|
||||
} else
|
||||
$secure = $request->secure;
|
||||
|
||||
$transformer = $this->createServerTransformer(false, false);
|
||||
if(!$transformer->filter($request))
|
||||
return 400;
|
||||
|
||||
$servers = iterator_to_array($this->chatCtx->servers->getServers(
|
||||
proto: $proto,
|
||||
secure: $secure,
|
||||
), false);
|
||||
|
||||
$response->setCacheControl('max-age=300', 'stale-if-error=600', 'public', 'immutable');
|
||||
|
||||
// the algo of all time
|
||||
return $transformer->convert($servers[array_rand($servers)]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,12 @@
|
|||
namespace Misuzu\Chat;
|
||||
|
||||
use Index\Config\Config;
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Http\HttpUri;
|
||||
|
||||
class ChatContext {
|
||||
public private(set) ChatServersData $servers;
|
||||
|
||||
/** @var string[] */
|
||||
public array $origins {
|
||||
get => $this->config->getArray('origins');
|
||||
|
@ -16,7 +19,10 @@ class ChatContext {
|
|||
|
||||
public function __construct(
|
||||
public private(set) Config $config,
|
||||
) {}
|
||||
DbConnection $dbConn,
|
||||
) {
|
||||
$this->servers = new ChatServersData($dbConn);
|
||||
}
|
||||
|
||||
public function isTrustedHost(HttpUri|string $host): bool {
|
||||
if($host instanceof HttpUri)
|
||||
|
|
31
src/Chat/ChatServerInfo.php
Normal file
31
src/Chat/ChatServerInfo.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
namespace Misuzu\Chat;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Index\Db\DbResult;
|
||||
|
||||
class ChatServerInfo {
|
||||
public function __construct(
|
||||
public private(set) string $id,
|
||||
public private(set) int $order,
|
||||
public private(set) string $proto,
|
||||
public private(set) bool $secure,
|
||||
public private(set) string $uri,
|
||||
public private(set) int $createdTime,
|
||||
) {}
|
||||
|
||||
public static function fromResult(DbResult $result): ChatServerInfo {
|
||||
return new ChatServerInfo(
|
||||
id: $result->getString(0),
|
||||
order: $result->getInteger(1),
|
||||
proto: $result->getString(2),
|
||||
secure: $result->getBoolean(3),
|
||||
uri: $result->getString(4),
|
||||
createdTime: $result->getInteger(5),
|
||||
);
|
||||
}
|
||||
|
||||
public CarbonImmutable $createdAt {
|
||||
get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
|
||||
}
|
||||
}
|
139
src/Chat/ChatServersData.php
Normal file
139
src/Chat/ChatServersData.php
Normal file
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
namespace Misuzu\Chat;
|
||||
|
||||
use RuntimeException;
|
||||
use Index\Db\{DbConnection,DbStatementCache};
|
||||
|
||||
class ChatServersData {
|
||||
private DbStatementCache $cache;
|
||||
|
||||
public function __construct(DbConnection $dbConn) {
|
||||
$this->cache = new DbStatementCache($dbConn);
|
||||
}
|
||||
|
||||
/** @return iterable<ChatServerInfo> */
|
||||
public function getServers(
|
||||
?string $proto = null,
|
||||
?bool $secure = null,
|
||||
): iterable {
|
||||
$hasProto = $proto !== null;
|
||||
|
||||
$args = 0;
|
||||
$query = <<<SQL
|
||||
SELECT server_id, server_order, server_proto, server_secure, server_uri, UNIX_TIMESTAMP(server_created)
|
||||
FROM msz_chat_servers
|
||||
SQL;
|
||||
if($hasProto) {
|
||||
++$args;
|
||||
$query .= ' WHERE server_proto = ?';
|
||||
}
|
||||
if($secure !== null)
|
||||
$query .= sprintf(
|
||||
' %s server_secure %s 0',
|
||||
++$args > 1 ? 'AND' : 'WHERE',
|
||||
$secure ? '<>' : '='
|
||||
);
|
||||
$query .= ' ORDER BY server_order';
|
||||
|
||||
$stmt = $this->cache->get($query);
|
||||
if($hasProto)
|
||||
$stmt->nextParameter($proto);
|
||||
$stmt->execute();
|
||||
|
||||
return $stmt->getResultIterator(ChatServerInfo::fromResult(...));
|
||||
}
|
||||
|
||||
public function getServer(string $serverId): ChatServerInfo {
|
||||
$stmt = $this->cache->get(<<<SQL
|
||||
SELECT server_id, server_order, server_proto, server_secure, server_uri, UNIX_TIMESTAMP(server_created)
|
||||
FROM msz_chat_servers
|
||||
WHERE server_id = ?
|
||||
SQL);
|
||||
$stmt->nextParameter($serverId);
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
if(!$result->next())
|
||||
throw new RuntimeException('could not find that server');
|
||||
|
||||
return ChatServerInfo::fromResult($result);
|
||||
}
|
||||
|
||||
public function deleteServer(ChatServerInfo|string $serverInfo): void {
|
||||
$stmt = $this->cache->get(<<<SQL
|
||||
DELETE FROM msz_chat_servers
|
||||
WHERE server_id = ?
|
||||
SQL);
|
||||
$stmt->nextParameter($serverInfo instanceof ChatServerInfo ? $serverInfo->id : $serverInfo);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
public function createServer(
|
||||
string $proto,
|
||||
bool $secure,
|
||||
string $uri,
|
||||
?int $order = null,
|
||||
): ChatServerInfo {
|
||||
$stmt = $this->cache->get(<<<SQL
|
||||
INSERT INTO msz_chat_servers (server_proto, server_secure, server_uri, server_order)
|
||||
SELECT ?, ?, ?, COALESCE(?, (
|
||||
SELECT FLOOR(MAX(server_order) / 10) * 10 + 10
|
||||
FROM msz_chat_servers
|
||||
WHERE server_proto = ?
|
||||
AND server_secure = ?
|
||||
), 10)
|
||||
SQL);
|
||||
$stmt->nextParameter($proto);
|
||||
$stmt->nextParameter($secure);
|
||||
$stmt->nextParameter($uri);
|
||||
$stmt->nextParameter($order);
|
||||
$stmt->nextParameter($proto);
|
||||
$stmt->nextParameter($secure);
|
||||
$stmt->execute();
|
||||
|
||||
return $this->getServer((string)$stmt->lastInsertId);
|
||||
}
|
||||
|
||||
public function updateServer(
|
||||
ChatServerInfo|string $serverInfo,
|
||||
?string $proto = null,
|
||||
?bool $secure = null,
|
||||
?string $uri = null,
|
||||
?int $order = null,
|
||||
): void {
|
||||
$fields = [];
|
||||
$values = [];
|
||||
|
||||
if($proto !== null) {
|
||||
$fields[] = 'server_proto = ?';
|
||||
$values[] = $proto;
|
||||
}
|
||||
|
||||
if($secure !== null)
|
||||
$fields[] = sprintf('server_secure = %d', $secure ? 1 : 0);
|
||||
|
||||
if($uri !== null) {
|
||||
$fields[] = 'server_uri = ?';
|
||||
$values[] = $uri;
|
||||
}
|
||||
|
||||
if($order !== null) {
|
||||
$fields[] = 'server_order = ?';
|
||||
$values[] = $order;
|
||||
}
|
||||
|
||||
if(empty($fields))
|
||||
return;
|
||||
|
||||
$fields = implode(', ', $fields);
|
||||
$stmt = $this->cache->get(<<<SQL
|
||||
UPDATE msz_chat_servers
|
||||
SET {$fields}
|
||||
WHERE server_id = ?
|
||||
SQL);
|
||||
foreach($values as $value)
|
||||
$stmt->nextParameter($value);
|
||||
$stmt->nextParameter($serverInfo instanceof ChatServerInfo ? $serverInfo->id : $serverInfo);
|
||||
$stmt->execute();
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue