diff --git a/e2e/flyer.spec.js b/e2e/flyer.spec.js index b181705..80e98c0 100644 --- a/e2e/flyer.spec.js +++ b/e2e/flyer.spec.js @@ -1089,6 +1089,27 @@ test.describe("VoxVera static client", () => { await expect(page.locator(".content h1")).toHaveCount(0); }); + test("board shows only VoxVera flyers, not foreign kind-30078 app data", async ({ page }) => { + const OTHER = "b".repeat(64); + await stubNip07(page); // viewer = 1*64, no follows → show-all + await stubRelays(page, [ + flyerEvent("real", OTHER, "Real VoxVera Flyer"), + // A foreign app's kind-30078 event that still carries a "voxvera" tag + // (hashtag collision, or a relay ignoring the #t filter). It has neither a + // voxvera: d-tag nor a voxvera_flyer payload, so it must be excluded. + { + id: "f".repeat(64), kind: 30078, pubkey: OTHER, created_at: 5000, + tags: [["d", "artstr:gallery"], ["t", "voxvera"], ["t", "flyer"]], + content: JSON.stringify({ type: "artstr_note", title: "Foreign App Note", url: "https://artstr.studio/x" }) + } + ]); + await page.goto("/board.html"); + await page.locator("#board-connect").click(); + await expect(page.locator("#board-content")).toBeVisible(); + await expect(page.locator("#board-rows")).toContainText("Real VoxVera Flyer"); + await expect(page.locator("#board-rows")).not.toContainText("Foreign App Note"); + }); + test("blocking an author hides their flyers, persists, and clears", async ({ page }) => { const OTHER = "b".repeat(64); await stubNip07(page); // viewer = 1*64 diff --git a/site/board.js b/site/board.js index c45f452..9b8f88b 100644 --- a/site/board.js +++ b/site/board.js @@ -190,7 +190,16 @@ if (!e || e.kind !== EVENT_KIND) return; const tags = e.tags || []; if (!tags.some((x) => x[0] === "t" && x[1] === "voxvera")) return; - const key = e.pubkey + ":" + dtagOf(tags); + // kind 30078 is a shared "application data" kind, and relays may ignore the + // #t filter (or another app may carry a "voxvera" hashtag), so foreign + // events can arrive. Require VoxVera's own markers: a voxvera: d-tag and a + // voxvera_flyer content payload (real flyers and tombstones both have it). + const d = dtagOf(tags); + if (!/^voxvera:/.test(d)) return; + let payload; + try { payload = JSON.parse(e.content); } catch (_) { return; } + if (!payload || payload.type !== "voxvera_flyer") return; + const key = e.pubkey + ":" + d; const prev = latest.get(key); if (!prev || (e.created_at || 0) > (prev.created_at || 0)) latest.set(key, e); });