import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:friendly_map/modules/core/core.dart'; /// 외부 보관함 서버로 봉투를 주고받는다. 보관함은 잠긴 봉투를 잠시 맡아 두기만 한다. /// /// 서버 쪽 약속 (server/mailbox_server.py): /// POST /v1/send 봉투 JSON → {"ok":true} /// GET /v1/fetch?to=<번호> → {"items":[봉투 JSON ...]} (헤더 X-Fm-Token) /// POST /v1/ack {"to":..,"ids":[..]} → {"ok":true} (헤더 X-Fm-Token) /// GET /v1/time → {"stamp": 1/100초} class MailboxTransport implements Transport, TimeSource { MailboxTransport({ required this.baseUrl, required this.myPhone, required this.token, this.timeout = const Duration(seconds: 8), }); final String baseUrl; final String myPhone; /// 이 기기가 보관함에서 내 우편함을 열 때 내는 비밀값 (처음 쓴 기기에 묶인다). final String token; final Duration timeout; final StreamController _in = StreamController.broadcast(sync: true); final HttpClient _http = HttpClient(); @override Stream get incoming => _in.stream; @override Future start() async {} @override Future stop() async => _http.close(force: true); Future?> _call(String method, String path, {Object? body, Map? query, bool auth = false}) async { try { final uri = Uri.parse('$baseUrl$path').replace(queryParameters: query); final req = await _http.openUrl(method, uri).timeout(timeout); req.headers.contentType = ContentType.json; if (auth) req.headers.set('X-Fm-Token', token); final bytes = utf8.encode(body == null ? '{}' : jsonEncode(body)); if (method == 'POST') { req.contentLength = bytes.length; // 서버가 길이를 요구한다 req.add(bytes); } final res = await req.close().timeout(timeout); final text = await utf8.decodeStream(res).timeout(timeout); if (res.statusCode != 200) return null; return (jsonDecode(text) as Map).cast(); } on Object { return null; // 연결 실패는 "못 보냄"으로 다룬다. } } @override Future send(Envelope envelope) async { final r = await _call('POST', '/v1/send', body: envelope.toJson()); return r != null && r['ok'] == true; } @override Future fetchPending() async { final r = await _call('GET', '/v1/fetch', query: {'to': myPhone}, auth: true); if (r == null) return 0; final items = (r['items'] as List? ?? const []).cast(); final ids = []; for (final raw in items) { try { final e = Envelope.fromJson((raw as Map).cast()); ids.add(e.id); _in.add(e); } catch (_) { // 읽을 수 없는 봉투는 건너뛴다. } } if (ids.isNotEmpty) { await _call('POST', '/v1/ack', body: {'to': myPhone, 'ids': ids}, auth: true); } return ids.length; } @override Future referenceStamp() async { final r = await _call('GET', '/v1/time'); final v = r?['stamp']; return v is int ? v : null; } }