fix: invalidate removed Buzz mention members

This commit is contained in:
Shakker
2026-08-02 09:52:01 +01:00
parent c2d6ecee3c
commit 6129fbf784
2 changed files with 67 additions and 3 deletions
+42 -1
View File
@@ -14,6 +14,11 @@ const relayMocks = vi.hoisted(() => ({
membershipEvents: [] as Event[],
subscriptions: [] as Array<{
filters: Filter[];
handlers: {
onevent: (event: Event) => void;
oneose?: () => void;
onclose?: (reason: string) => void;
};
close: ReturnType<typeof vi.fn>;
}>,
}));
@@ -39,10 +44,11 @@ vi.mock("nostr-tools", async (importOriginal) => {
handlers: {
onevent: (event: Event) => void;
oneose?: () => void;
onclose?: (reason: string) => void;
},
) {
const close = vi.fn();
relayMocks.subscriptions.push({ filters, close });
relayMocks.subscriptions.push({ filters, handlers, close });
if (filters.some((filter) => filter.kinds?.includes(39002))) {
for (const event of relayMocks.membershipEvents) {
handlers.onevent(event);
@@ -251,6 +257,41 @@ describe("Buzz mention delivery", () => {
await bus.close();
});
it("stops mentioning a removed member before the signed roster refresh completes", async () => {
const explicitSender = `nostr:${nip19.npubEncode(SENDER_PUBLIC_KEY)}`;
const bus = await startBuzzBus({
accountId: ACCOUNT_ID,
relayUrl: "wss://buzz.example.com",
privateKey: PRIVATE_KEY,
channelIds: [CHANNEL_ID],
onMessage: async () => {},
});
await bus.sendText({ channelId: CHANNEL_ID, text: explicitSender });
relayMocks.publish.mockClear();
relayMocks.subscriptions
.find((entry) => subscriptionIncludesKind(entry, 40_099))
?.handlers.onevent({
id: "member-removed-1",
kind: 40_099,
pubkey: RELAY_PUBLIC_KEY,
created_at: 1_700_000_001,
content: JSON.stringify({ type: "member_removed", target: SENDER_PUBLIC_KEY }),
sig: "e".repeat(128),
tags: [["h", CHANNEL_ID]],
});
expect(bus.directory.mentionMembers(CHANNEL_ID)).toEqual([
expect.objectContaining({ publicKey: BOT_PUBLIC_KEY }),
]);
await expect(bus.sendText({ channelId: CHANNEL_ID, text: explicitSender })).rejects.toThrow(
"is not a current room member",
);
expect(relayMocks.publish).not.toHaveBeenCalled();
await bus.close();
});
it("keeps mention-free active sends off the room roster path", async () => {
const bus = await startBuzzBus({
accountId: ACCOUNT_ID,
+25 -2
View File
@@ -90,6 +90,28 @@ export async function createBuzzRoomMembershipTracker(params: {
const refreshes = new Map<string, RefreshState>();
let membershipQueryTail = Promise.resolve();
const memberships = await queryBuzzRoomMemberships(params);
const effectiveMemberships = (): ReadonlyMap<string, BuzzRoomMembership> => {
if (blockedRooms.size === 0 && deniedMembers.size === 0) {
return memberships;
}
const effective = new Map<string, BuzzRoomMembership>();
for (const [channelId, membership] of memberships) {
if (blockedRooms.has(channelId)) {
continue;
}
const denied = deniedMembers.get(channelId);
if (!denied || denied.size === 0) {
effective.set(channelId, membership);
continue;
}
effective.set(channelId, {
...membership,
members: new Set([...membership.members].filter((publicKey) => !denied.has(publicKey))),
roles: new Map([...membership.roles].filter(([publicKey]) => !denied.has(publicKey))),
});
}
return effective;
};
const isMember = (channelId: string, publicKey: string) =>
!blockedRooms.has(channelId) &&
!deniedMembers.get(channelId)?.has(publicKey.trim().toLowerCase()) &&
@@ -181,7 +203,7 @@ export async function createBuzzRoomMembershipTracker(params: {
pendingMemberships.delete(channelId);
deniedMembers.delete(channelId);
blockedRooms.delete(channelId);
params.onMembershipsChanged?.(memberships);
params.onMembershipsChanged?.(effectiveMemberships());
return;
}
if (state.generation !== state.lastAttemptedGeneration) {
@@ -255,6 +277,7 @@ export async function createBuzzRoomMembershipTracker(params: {
if (change.targetPublicKey === params.botPublicKey) {
blockedRooms.add(channelId);
}
params.onMembershipsChanged?.(effectiveMemberships());
return refreshMembershipOnce(channelId);
};
const handleRoomEvent = (event: Event, reservation?: BuzzReplayDispatchReservation) => {
@@ -358,7 +381,7 @@ export async function createBuzzRoomMembershipTracker(params: {
}
return {
memberships: () => memberships,
memberships: effectiveMemberships,
catchUpHistory: async () => {
for (const channelId of params.channelIds) {
const page = historyPages.get(channelId);