// 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 _clientStatuses = {}; // チャットメッセージキュー(永続化せず、メモリ保存) List _chatQueue = []; /// サーバー起動処理 Future 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 = >{}; _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 _handleHeartbeat(HttpRequest request) async { final body = jsonDecode(utf8.decode(request.requestedBytes)) as Map; 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; 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 = >{}; 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; final apiKey = body['api_key'] as String?; if (apiKey != args['MOTHERSHIP_API_KEY']) { return jsonEncode({'error': '未認証'}); } final acknowledgedIds = []; _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 _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 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;