frindlymap/lib/modules/sync/src/outbound.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;
}
}