150 lines
5.9 KiB
Dart
150 lines
5.9 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:friendly_map/modules/core/core.dart';
|
|
import 'package:friendly_map/modules/storage/storage.dart';
|
|
|
|
import 'sync_context.dart';
|
|
import 'sync_event.dart';
|
|
|
|
/// 복구 조각 봉투의 단위 이름 (맡기는 사람 번호별로 하나).
|
|
String recoveryUnit(String ownerPhone) => 'recovery:$ownerPhone';
|
|
|
|
/// 친구가 맡아 둔 복구 조각을 적어 두는 설정 이름: 맡긴 사람 번호별.
|
|
String heldRecoveryKey(String ownerPhone) => 'recovery_held:$ownerPhone';
|
|
|
|
/// 친구 위치 봉투의 단위 이름 (보내는 사람 번호별로 하나).
|
|
String locationUnit(String ownerPhone) => 'location:$ownerPhone';
|
|
|
|
/// 받은 친구 위치를 적어 두는 설정 이름: 보낸 사람 번호별.
|
|
String friendLocationKey(String ownerPhone) => 'loc:$ownerPhone';
|
|
|
|
/// 위치를 받은 친구 번호 목록(JSON)을 적어 두는 설정 이름.
|
|
const String friendLocationIndexKey = 'loc_index';
|
|
|
|
/// 맡아 둔 조각의 주인 번호 목록(JSON)을 적어 두는 설정 이름.
|
|
const String heldRecoveryIndexKey = 'recovery_held_index';
|
|
|
|
/// 내보내기: 봉투를 만들어 보내고, 못 보낸 것은 보낼함에 둔다.
|
|
class Outbound {
|
|
Outbound(this._c);
|
|
|
|
final SyncContext _c;
|
|
|
|
/// 인사. 내 이름과 공개 키를 상대에게 알린다. [reply] 는 상대 인사에 대한 답일 때.
|
|
Future<SendResult> hello(String peerPhone, {bool reply = false}) async {
|
|
final me = await _c.me();
|
|
if (me == null) return SendResult.noTarget;
|
|
final body = jsonEncode({
|
|
'name': me.name,
|
|
'key': await _c.identity.myPublicKey(),
|
|
'reply': reply,
|
|
});
|
|
return _dispatch(_envelope(me.phone, peerPhone, 'hello', '', body));
|
|
}
|
|
|
|
/// 공유 중인 명함을 상대에게 보낸다 (처음 공유 또는 "다시 보내기").
|
|
Future<SendResult> card(ShareRecord share) async {
|
|
if (share.unit != 'card') return SendResult.noTarget;
|
|
final card = await _c.storage.myCards.get(share.targetId);
|
|
final peer = await _c.storage.people.get(share.peerId);
|
|
if (card == null || peer == null) return SendResult.noTarget;
|
|
final plain = jsonEncode({
|
|
'kind': card.kind,
|
|
'title': card.title,
|
|
'fields': share.snapshot ?? card.fieldsJson,
|
|
});
|
|
return _sealAndSend(peer, 'card.share', card.id, plain);
|
|
}
|
|
|
|
/// 공유 중인 대화방의 정보(제목·맥락·참여자)를 상대에게 보낸다.
|
|
Future<SendResult> room(ShareRecord share) async {
|
|
if (share.unit != 'room') return SendResult.noTarget;
|
|
final room = await _c.storage.rooms.get(share.targetId);
|
|
final peer = await _c.storage.people.get(share.peerId);
|
|
final me = await _c.me();
|
|
if (room == null || peer == null || me == null) return SendResult.noTarget;
|
|
final members = <Map<String, String>>[
|
|
{'phone': me.phone, 'name': me.name},
|
|
];
|
|
for (final id in (jsonDecode(room.membersJson) as List).cast<String>()) {
|
|
final p = await _c.storage.people.get(id);
|
|
if (p != null) members.add({'phone': p.phone, 'name': p.name});
|
|
}
|
|
final plain = jsonEncode({
|
|
'title': room.title,
|
|
'kind': room.kind,
|
|
'context': room.contextKind,
|
|
'members': members,
|
|
});
|
|
return _sealAndSend(peer, 'chat.room', room.id, plain);
|
|
}
|
|
|
|
/// 복구 조각 한 장을 친구에게 맡긴다. 둘만 아는 키로 잠가 보낸다.
|
|
Future<SendResult> recovery(Person peer, String code) async {
|
|
final me = await _c.me();
|
|
if (me == null) return SendResult.noTarget;
|
|
return _sealAndSend(peer, 'recovery.share', recoveryUnit(me.phone), jsonEncode({'code': code}));
|
|
}
|
|
|
|
/// 내 현재 위치를 친구에게 보낸다 (둘만 아는 키로 잠금). 보낸 시각 [at] 은 1/100초.
|
|
Future<SendResult> location(Person peer, double lat, double lng, int at) async {
|
|
final me = await _c.me();
|
|
if (me == null) return SendResult.noTarget;
|
|
return _sealAndSend(peer, 'location.update', locationUnit(me.phone), jsonEncode({'lat': lat, 'lng': lng, 'at': at}));
|
|
}
|
|
|
|
/// 대화 한 건을 그 방을 공유 중인 모든 상대에게 보낸다. 보낸 상대별 결과를 돌려준다.
|
|
Future<List<SendResult>> message(Message m) async {
|
|
final results = <SendResult>[];
|
|
for (final s in await _c.storage.shares.activeFor(m.roomId)) {
|
|
if (s.unit != 'room') continue;
|
|
final peer = await _c.storage.people.get(s.peerId);
|
|
if (peer == null) continue;
|
|
final plain = jsonEncode({'id': m.id, 'body': m.body, 'sentAt': m.sentAt});
|
|
results.add(await _sealAndSend(peer, 'chat.message', m.roomId, plain));
|
|
}
|
|
return results;
|
|
}
|
|
|
|
/// 보낼함에 쌓인 것을 다시 보내 본다. 보낸 개수를 돌려준다.
|
|
Future<int> flush() async {
|
|
var sent = 0;
|
|
for (final item in await _c.storage.outbox.pending()) {
|
|
var ok = false;
|
|
try {
|
|
ok = await _c.transport.send(Envelope.decode(item.envelopeJson));
|
|
} catch (_) {}
|
|
if (ok) {
|
|
await _c.storage.outbox.remove(item.id);
|
|
sent++;
|
|
} else {
|
|
await _c.storage.outbox.update(item.tried());
|
|
}
|
|
}
|
|
return sent;
|
|
}
|
|
|
|
Future<SendResult> _sealAndSend(Person peer, String kind, String unitId, String plain) async {
|
|
final key = peer.publicKey;
|
|
final me = await _c.me();
|
|
if (key == null || me == null) return SendResult.noKey;
|
|
final sealed = await _c.cipher.seal(plain: plain, peerPublicKey: key, context: unitId);
|
|
return _dispatch(_envelope(me.phone, peer.phone, kind, unitId, sealed));
|
|
}
|
|
|
|
Envelope _envelope(String from, String to, String kind, String unit, String body) =>
|
|
Envelope(id: newId(), from: from, to: to, kind: kind, unitId: unit, body: body, stamp: _c.clock.now());
|
|
|
|
Future<SendResult> _dispatch(Envelope e) async {
|
|
var ok = false;
|
|
try {
|
|
ok = await _c.transport.send(e);
|
|
} catch (_) {}
|
|
if (ok) return SendResult.sent;
|
|
await _c.storage.outbox.add(
|
|
OutboxItem(id: e.id, peerPhone: e.to, envelopeJson: e.encode(), createdAt: _c.clock.now()),
|
|
);
|
|
return SendResult.queued;
|
|
}
|
|
}
|