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 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 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 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 = >[ {'phone': me.phone, 'name': me.name}, ]; for (final id in (jsonDecode(room.membersJson) as List).cast()) { 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 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 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> message(Message m) async { final results = []; 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 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 _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 _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; } }