From 7f10ff6faea91f22ec32b27d57d8a1ac62e20597 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 22:41:26 +0000 Subject: [PATCH] fix: narrow relay-group follows/authors REQ instead of broad directory pull MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A follows/authors filter no longer pulls every group on the relay and filters client-side. It now emits, per relay, the three narrowed REQs a relay-signed group is discoverable by: 1. {kinds:[39000], authors:} — the relay signing-key is a follow 2. {kinds:[39001,39002], #p:} — a follow is an admin/member 3. {kinds:[39000], #d:} — metadata backfill for (2) (3) reads the group-ids of cached 39001/39002 rosters that already mention a follow (empty on the first pass, filled once (2)'s events land and the sub-assembler re-invalidates), since a #p roster hit doesn't carry the 39000. ByFollows routes through the shared per-relay author builder (mirrors Git); muted-authors likewise. Global / communities keep the broad directory pull, since they carry no author dimension to narrow on. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj --- .../FilterRelayGroupsByAuthors.kt | 83 +++++++++++++++++-- .../FilterRelayGroupsByFollows.kt | 14 ++-- 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/subassemblies/FilterRelayGroupsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/subassemblies/FilterRelayGroupsByAuthors.kt index d947567c4c..b6713932cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/subassemblies/FilterRelayGroupsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/subassemblies/FilterRelayGroupsByAuthors.kt @@ -20,14 +20,85 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.subassemblies +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.dTag.DTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupAdminsEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent -// Authors can't be pushed into the REQ (a group's 39000 is signed by its relay, never a follow), -// so an author filter contributes only its RELAY set; the directory is pulled per relay and the -// dal keeps the groups whose relay-key/admins/members intersect the selected authors. +/** + * A group's kind-39000 is relay-signed, so a follow shows up in three distinct ways, and each is a + * real relay-side REQ (not a broad "pull everything" pass): + * 1. the follow IS the relay signing key → `{kinds:[39000], authors:}` (metadata included); + * 2. a follow is an admin/member → `{kinds:[39001,39002], #p:}` (standard p-tag filter); + * 3. metadata backfill for the groups (2) surfaced → `{kinds:[39000], #d:}`, read + * from the rosters already in cache (empty on the first pass, filled once (2)'s events land and + * the sub-assembler re-invalidates). + */ +fun filterRelayGroupsByAuthors( + relay: NormalizedRelayUrl, + authors: Set, + since: Long? = null, +): List { + if (authors.isEmpty()) return emptyList() + val authorList = authors.sorted() + + val filters = + mutableListOf( + // (1) groups whose relay signing-key is a follow + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authorList, + kinds = listOf(GroupMetadataEvent.KIND), + limit = 200, + since = since, + ), + ), + // (2) groups where a follow is an admin (39001) or member (39002) + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = listOf(GroupAdminsEvent.KIND, GroupMembersEvent.KIND), + tags = mapOf(PTag.TAG_NAME to authorList), + limit = 200, + since = since, + ), + ), + ) + + // (3) backfill 39000 metadata for the roster-discovered groups already known on this relay. + val rosterGroupIds = + LocalCache + .getRelayGroupChannelsOnRelay(relay) + .filter { channel -> channel.admins.any { it.pubKey in authors } || channel.members.any { it in authors } } + .map { it.groupId.id } + if (rosterGroupIds.isNotEmpty()) { + filters += + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = listOf(GroupMetadataEvent.KIND), + tags = mapOf(DTag.TAG_NAME to rosterGroupIds), + limit = 200, + since = since, + ), + ) + } + + return filters +} fun filterRelayGroupsByAuthors( authorSet: AuthorsTopNavPerRelayFilterSet, @@ -37,8 +108,9 @@ fun filterRelayGroupsByAuthors( if (authorSet.set.isEmpty()) return emptyList() return authorSet.set.flatMap { - filterRelayGroupsDirectory( + filterRelayGroupsByAuthors( relay = it.key, + authors = it.value.authors, since = since?.get(it.key)?.time ?: defaultSince, ) } @@ -52,8 +124,9 @@ fun filterRelayGroupsByMutedAuthors( if (authorSet.set.isEmpty()) return emptyList() return authorSet.set.flatMap { - filterRelayGroupsDirectory( + filterRelayGroupsByAuthors( relay = it.key, + authors = it.value.authors, since = since?.get(it.key)?.time ?: defaultSince, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/subassemblies/FilterRelayGroupsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/subassemblies/FilterRelayGroupsByFollows.kt index 48d5850b18..c5b2b43a9d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/subassemblies/FilterRelayGroupsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/subassemblies/FilterRelayGroupsByFollows.kt @@ -24,9 +24,9 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavP import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -// A follows filter pulls the whole directory (metadata + rosters) from each relay the follow set -// routes to; the dal keeps groups where a follow is the relay key, an admin, or a member, and any -// topic/geo groups the AllFollows big-OR also selects (all present in the directory pull). +// A follows filter routes to each relay the follow set uses and asks, per relay, for the three +// ways a follow surfaces in a relay-signed group: the relay signing-key is a follow, or a follow +// is an admin (39001) or a member (39002). See [filterRelayGroupsByAuthors]. fun filterRelayGroupsByFollows( followsSet: AllFollowsTopNavPerRelayFilterSet, since: SincePerRelayMap?, @@ -35,9 +35,9 @@ fun filterRelayGroupsByFollows( if (followsSet.set.isEmpty()) return emptyList() return followsSet.set.flatMap { - filterRelayGroupsDirectory( - relay = it.key, - since = since?.get(it.key)?.time ?: defaultSince, - ) + val relaySince = since?.get(it.key)?.time ?: defaultSince + it.value.authors?.let { authors -> + filterRelayGroupsByAuthors(it.key, authors, relaySince) + } ?: emptyList() } }