96 lines
3.2 KiB
Dart
96 lines
3.2 KiB
Dart
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<Envelope> _in = StreamController<Envelope>.broadcast(sync: true);
|
|
final HttpClient _http = HttpClient();
|
|
|
|
@override
|
|
Stream<Envelope> get incoming => _in.stream;
|
|
|
|
@override
|
|
Future<void> start() async {}
|
|
|
|
@override
|
|
Future<void> stop() async => _http.close(force: true);
|
|
|
|
Future<Map<String, Object?>?> _call(String method, String path,
|
|
{Object? body, Map<String, String>? 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<String, Object?>();
|
|
} on Object {
|
|
return null; // 연결 실패는 "못 보냄"으로 다룬다.
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> send(Envelope envelope) async {
|
|
final r = await _call('POST', '/v1/send', body: envelope.toJson());
|
|
return r != null && r['ok'] == true;
|
|
}
|
|
|
|
@override
|
|
Future<int> 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<Object?>();
|
|
final ids = <String>[];
|
|
for (final raw in items) {
|
|
try {
|
|
final e = Envelope.fromJson((raw as Map).cast<String, Object?>());
|
|
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<int?> referenceStamp() async {
|
|
final r = await _call('GET', '/v1/time');
|
|
final v = r?['stamp'];
|
|
return v is int ? v : null;
|
|
}
|
|
}
|