fix(mobile): read channel metadata and roster separately

findJoinedChannel asked for kinds 39000 and 39002 under one limit of 2.
A channel with lingering pre-backfill metadata revisions could fill that
limit with metadata and drop the roster, so reopening an older DM would
fail after the relay had already opened it. Query each kind with its own
limit of 1, in parallel.

Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-27 21:41:14 -07:00
parent 762def4036
commit 8d820e4f62
4 changed files with 67 additions and 53 deletions

View File

@@ -70,22 +70,31 @@ class ChannelActions {
/// and every channel's recent messages. Missing membership fails closed.
Future<Channel?> findJoinedChannel(String channelId) async {
_ensureCommunityValid();
final events = await _session.fetchHistory(
NostrFilter(
kinds: const [39000, 39002],
tags: {
'#d': [channelId],
},
limit: 2,
),
);
// One query per kind: stale kind:39000 revisions from before the relay's
// d_tag backfill can linger and would crowd the roster out of a shared
// limit.
Future<NostrEvent?> latest(int kind) async {
final events = await _session.fetchHistory(
NostrFilter(
kinds: [kind],
tags: {
'#d': [channelId],
},
limit: 1,
),
);
return events
.where((e) => e.kind == kind && e.getTagValue('d') == channelId)
.firstOrNull;
}
final metadataRequest = latest(39000);
final rosterRequest = latest(39002);
// Future.wait rethrows the first failure unchanged.
await Future.wait(eagerError: true, [metadataRequest, rosterRequest]);
final metadata = await metadataRequest;
final roster = await rosterRequest;
_ensureCommunityValid();
final metadata = events
.where((e) => e.kind == 39000 && e.getTagValue('d') == channelId)
.firstOrNull;
final roster = events
.where((e) => e.kind == 39002 && e.getTagValue('d') == channelId)
.firstOrNull;
if (metadata == null || roster == null) return null;
final members = membersFromEvent(roster);
if (!members.any((m) => m.pubkey == _currentPubkey)) return null;

View File

@@ -140,42 +140,45 @@ class _DmSession extends RelaySessionNotifier {
NostrFilter filter, {
Duration timeout = const Duration(seconds: 8),
}) async {
expect(filter.kinds, [39000, 39002]);
expect(filter.tags, {
'#d': [_dmId],
});
onRead?.call();
return [
NostrEvent(
id: 'metadata',
pubkey: 'relay',
createdAt: 1,
kind: 39000,
tags: [
['d', _dmId],
['name', 'DM'],
['t', 'dm'],
['private'],
['hidden'],
['p', self],
['p', _peer],
],
content: '',
sig: '',
),
NostrEvent(
id: 'roster',
pubkey: 'relay',
createdAt: 1,
kind: 39002,
tags: [
['d', _dmId],
if (member) ['p', self, '', 'member'],
['p', _peer, '', 'member'],
],
content: '',
sig: '',
),
];
// Serve like a relay: newest first, one limit across every kind. A stale
// metadata revision from before the d_tag backfill still lingers.
final events = [
_event(39000, 3, [
['name', 'DM'],
['t', 'dm'],
['private'],
['hidden'],
['p', self],
['p', _peer],
]),
_event(39000, 2, [
['name', 'DM'],
['t', 'dm'],
['private'],
]),
_event(39002, 1, [
if (member) ['p', self, '', 'member'],
['p', _peer, '', 'member'],
]),
].where((e) => filter.kinds.contains(e.kind)).toList();
return events.take(filter.limit).toList();
}
}
NostrEvent _event(int kind, int createdAt, List<List<String>> tags) =>
NostrEvent(
id: '$kind-$createdAt',
pubkey: 'relay',
createdAt: createdAt,
kind: kind,
tags: [
['d', _dmId],
...tags,
],
content: '',
sig: '',
);

View File

@@ -46,7 +46,7 @@ class _Relay extends RelaySessionNotifier {
Future<List<NostrEvent>> fetchHistory(
NostrFilter filter, {
Duration timeout = const Duration(seconds: 8),
}) async => filter.kinds.contains(39000) ? existing : [];
}) async => existing.where((e) => filter.kinds.contains(e.kind)).toList();
@override
Future<NostrEvent> publish(

View File

@@ -144,12 +144,14 @@ class _SoloSession extends RelaySessionNotifier {
NostrFilter filter, {
Duration timeout = const Duration(seconds: 8),
}) async {
reads++;
expect(filter.kinds, [39000, 39002]);
// Each lookup reads metadata and roster separately.
final kind = filter.kinds.single;
if (kind == 39000) reads++;
expect(kind, anyOf(39000, 39002));
expect(filter.tags, {
'#d': ['solo-id'],
});
expect(filter.limit, 2);
expect(filter.limit, 1);
onRead?.call();
if (failure != null) throw failure!;
if (!exists) return [];
@@ -182,6 +184,6 @@ class _SoloSession extends RelaySessionNotifier {
content: '',
sig: '',
),
];
].where((e) => e.kind == kind).toList();
}
}