h-1.flutter.4/bin/mothership_server.dart

291 lines
No EOL
9.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Version: 1.0.0
import 'dart:async';
import 'dart:convert';
import 'dart:io';
/// 母艦「お局様」用サーバーアプリケーション
///
/// クライアント(販売アシスト 1 号)のハッシュチェーン監視、
/// システムバックアップ、チャットリレーを管理するサービスです。
///
/// 環境変数(推奨:`.env` を使用):
/// - MOTHERSHIP_HOST: サーバーホストlocalhost / 0.0.0.0
/// - MOTHERSHIP_PORT: ポート番号8787
/// - MOTHERSHIP_API_KEY: API キー(同期認証用)
/// - MOTHERSHIP_DATA_DIR: データ保存先ディレクトリ
class MothershipServer {
// サーバー状態管理
final statusFile = File('data/mothership/status.json');
// クライアントハッシュチェーンのステート
Map<String, ClientData> _clientStatuses = {};
// チャットメッセージキュー(永続化せず、メモリ保存)
List<ChatMessage> _chatQueue = [];
/// サーバー起動処理
Future<void> start() async {
// データディレクトリの準備
final dataDir = Directory(args['MOTHERSHIP_DATA_DIR'] ?? 'data/mothership');
await dataDir.create(recursive: true);
// チャットキューの読み込み(初期化)
_chatQueue = [];
print('=== 母艦「お局様」サーバー起動 ===');
print('ホスト:${args['MOTHERSHIP_HOST'] ?? '0.0.0.0'}:${args['MOTHERSHIP_PORT'] ?? '8787'}');
print('API キー:${args['MOTHERSHIP_API_KEY']}');
print('データディレクトリ:${dataDir.path}');
// 簡易な HTTP サーバーを起動dart:io の httpServer 使用)
final server = await HttpServer.bind(
args['MOTHERSHIP_HOST'] ?? '0.0.0.0',
int.parse(args['MOTHERSHIP_PORT'] ?? '8787'),
);
print('サーバー起動完了。HTTP リッスン開始');
// 各接続に対してループ処理
await for (final request in server.request) {
final response = switch (request.url.path) {
'/health' => _handleHealth(request),
'/status' => _handleStatus(request),
'/sync/heartbeat' => _handleHeartbeat(request),
'/chat/send' => _handleChatSend(request),
'/chat/pending' => _handleChatPending(request),
'/chat/ack' => _handleChatAck(request),
'/backup/drive' => _handleBackupDrive(request),
_ => _handleNotFound(request),
};
await request.response.write(response);
}
}
/// ハンドラヘルスチェックGET /health
String _handleHealth(HttpRequest request) {
return jsonEncode({
'status': 'ok',
'timestamp': DateTime.now().toUtc().toIso8601String(),
'clients_connected': _clientStatuses.length,
});
}
/// ハンドラステータス一覧取得GET /status
String _handleStatus(HttpRequest request) {
final clients = <String, Map<String, dynamic>>{};
_clientStatuses.forEach((clientId, clientData) {
clients[clientId] = {
'id': clientData.id,
'last_heartbeat': clientData.lastHeartbeat?.toIso8601String(),
'hash_chain_root': clientData.hashChainRoot,
'expiry_remaining_days': clientData.expiryRemainingDays,
};
});
return jsonEncode({
'server_time': DateTime.now().toUtc().toIso8601String(),
'clients': clients,
});
}
/// ハンドラハートビート同期POST /sync/heartbeat
Future<String> _handleHeartbeat(HttpRequest request) async {
final body = jsonDecode(utf8.decode(request.requestedBytes)) as Map<String, dynamic>;
final apiKey = body['api_key'] as String?;
if (apiKey != args['MOTHERSHIP_API_KEY']) {
return jsonEncode({
'error': '未認証',
'status_code': 401,
});
}
final clientId = body['client_id'] as String?;
if (clientId == null) {
return jsonEncode({'error': 'client_id が不足'});
}
final now = DateTime.now().toUtc();
_clientStatuses[clientId] = ClientData(
id: clientId,
lastHeartbeat: now,
hashChainRoot: body['hash_chain_root'] as String?,
expiryRemainingDays: body['expiry_remaining_days'] ?? 90,
);
// ステータスを永続化JSON ファイルに保存)
await _persistStatus();
return jsonEncode({
'status': 'success',
'client_id': clientId,
'last_heartbeat': now.toIso8601String(),
'expiry_warning': (_clientStatuses[clientId]?.expiryRemainingDays ?? 90) <= 30,
});
}
/// ハンドラチャット送信POST /chat/send
String _handleChatSend(HttpRequest request) {
final body = jsonDecode(utf8.decode(request.requestedBytes)) as Map<String, dynamic>;
final apiKey = body['api_key'] as String?;
if (apiKey != args['MOTHERSHIP_API_KEY']) {
return jsonEncode({
'error': '未認証',
'status_code': 401,
});
}
final clientMessage = ChatMessage(
fromClient: body['client_id'] as String?,
message: body['message'] as String,
priority: body['priority'] as int? ?? 0,
);
_chatQueue.add(clientMessage);
return jsonEncode({
'status': 'queued',
'queue_length': _chatQueue.length,
});
}
/// ハンドラチャット未送信メッセージ取得GET /chat/pending
String _handleChatPending(HttpRequest request) {
// クライアントごとにキューをフィルタ
final clientQueues = <String, List<ChatMessage>>{};
for (final message in _chatQueue) {
if (message.fromClient != null) {
clientQueues.putIfAbsent(message.fromClient!, () => []).add(message);
} else {
// 非クライアント用メッセージはすべてのクライアントに配信
final pending = clientQueues.values.fold([], (acc, curr) => [...acc, ...curr]);
if (pending.isNotEmpty) {
clientQueues['all_clients'] = pending;
}
}
}
return jsonEncode({'queues': clientQueues});
}
/// ハンドラチャット送信確認POST /chat/ack
String _handleChatAck(HttpRequest request) {
final body = jsonDecode(utf8.decode(request.requestedBytes)) as Map<String, dynamic>;
final apiKey = body['api_key'] as String?;
if (apiKey != args['MOTHERSHIP_API_KEY']) {
return jsonEncode({'error': '未認証'});
}
final acknowledgedIds = <String>[];
_chatQueue.removeWhere((msg) => msg.id == body['message_id']);
for (final msg in _chatQueue) {
acknowledgedIds.add(msg.id);
}
return jsonEncode({
'status': 'acknowledged',
'acknowledged_count': acknowledgedIds.length,
});
}
/// ハンドラバックアップドライブ確認POST /backup/drive
String _handleBackupDrive(HttpRequest request) {
final apiKey = body['api_key'] as String?;
if (apiKey != args['MOTHERSHIP_API_KEY']) {
return jsonEncode({'error': '未認証'});
}
// 簡易的な Drive API 接続は後実装(ここに OAuth2 フローを想定)
return jsonEncode({
'status': 'backup_ready',
'drive_quota_gb': 15,
'used_space_gb': 0.5,
});
}
/// ハンドラ:未定義エンドポイント
String _handleNotFound(HttpRequest request) {
return jsonEncode({
'error': 'Not Found',
'path': request.url.path,
'method': request.method,
}, statusCode: HttpStatus.notFound);
}
/// ステータスを永続化JSON ファイル)
Future<void> _persistStatus() async {
final now = DateTime.now().toUtc();
final data = {
'server_time': now.toIso8601String(),
'clients': {},
};
_clientStatuses.forEach((id, client) {
data['clients'][id] = {
'last_heartbeat': client.lastHeartbeat?.toIso8601String(),
'hash_chain_root': client.hashChainRoot,
'expiry_remaining_days': client.expiryRemainingDays,
};
});
final file = File(statusFile.path);
await file.writeAsString(jsonEncode(data));
}
}
/// クライアントデータ構造(簡易モデル)
class ClientData {
final String id;
final DateTime? lastHeartbeat;
final String? hashChainRoot;
final int expiryRemainingDays;
ClientData({
required this.id,
this.lastHeartbeat,
this.hashChainRoot,
required this.expiryRemainingDays,
});
}
/// チャットメッセージ(キュー単位)
class ChatMessage {
final String? fromClient;
final String message;
final int priority;
final String id; // ランダム文字列で生成
ChatMessage({
this.fromClient,
required this.message,
this.priority = 0,
String? id,
}) : id = id ?? const String.fromCharCodes([0x4a, 0x61, 0x70, 0x2d, DateTime.now().millisecondsSinceEpoch % 1000]);
}
void main(List<String> args) async {
// 環境変数の取得(簡易的な実装)
final host = Platform.environment['MOTHERSHIP_HOST'] ?? '0.0.0.0';
final port = Platform.environment['MOTHERSHIP_PORT'] ?? '8787';
final apiKey = Platform.environment['MOTHERSHIP_API_KEY'] ?? 'TEST_MOTHERSHIP_KEY';
final dataDir = Platform.environment['MOTHERSHIP_DATA_DIR'] ?? 'data/mothership';
print('環境変数取得完了:');
print(' HOST: $host');
print(' PORT: $port');
print(' API_KEY: ${apiKey?.substring(0, min(apiKey!.length, 10))}...');
final server = MothershipServer();
await server.start();
}
/// min 関数の簡易実装(標準ライブラリにないため)
int min(int a, int b) => a < b ? a : b;