mirror of
https://github.com/block/buzz.git
synced 2026-11-02 15:31:27 +00:00
fix(desktop): omit invalid snapshot imeta thumbnails (#1800)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -171,9 +171,6 @@ export function AgentSnapshotSendDialog({
|
||||
memorySourcePubkey: linkedAgentPubkey,
|
||||
}),
|
||||
destination.id,
|
||||
// Pass the agent's avatar URL as the NIP-92 thumb hint so the recipient
|
||||
// card can show the avatar instead of the generic Bot icon.
|
||||
persona.avatarUrl ?? undefined,
|
||||
);
|
||||
// Phase transitions (done/error) are driven by beginSend via setState
|
||||
// inside the controller; the mirror effect above keeps `step` in sync.
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
recordTimeoutFromRejection,
|
||||
clearTimeoutState,
|
||||
} from "../../moderation/lib/timeoutStore.ts";
|
||||
import { buildOutgoingMessage } from "../../messages/lib/imetaMediaMarkdown.ts";
|
||||
|
||||
// ── isSendableDestination ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -403,6 +404,54 @@ test("runSendPipeline_checkpoint2_blocks_upload_after_encode", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("runSendPipeline_avatar_bearing_snapshot_omits_thumb_and_keeps_filename", async () => {
|
||||
let sentMediaTags = null;
|
||||
|
||||
const result = await runSendPipeline({
|
||||
channelId: "ch-1",
|
||||
checkEligibilityFn: () => null,
|
||||
encodeFn: async () => ({
|
||||
fileBytes: [1],
|
||||
fileName: "avatar-bearing.agent.json",
|
||||
}),
|
||||
uploadFn: async () => ({
|
||||
url: "https://example.com/avatar-bearing.agent.json",
|
||||
sha256: "a".repeat(64),
|
||||
size: 1,
|
||||
type: "application/json",
|
||||
uploaded: 0,
|
||||
// This represents an upload descriptor that happens to include a thumb.
|
||||
// A snapshot send must not serialize it as NIP-92 imeta metadata.
|
||||
thumb: "https://example.com/avatar.png",
|
||||
}),
|
||||
sendFn: async ({ mediaTags }) => {
|
||||
sentMediaTags = mediaTags;
|
||||
},
|
||||
setStateFn: () => {},
|
||||
buildMessageFn: (descriptor) => buildOutgoingMessage("", [descriptor]),
|
||||
});
|
||||
|
||||
assert.equal(result, true);
|
||||
assert.deepEqual(sentMediaTags, [
|
||||
[
|
||||
"imeta",
|
||||
"url https://example.com/avatar-bearing.agent.json",
|
||||
"m application/json",
|
||||
`x ${"a".repeat(64)}`,
|
||||
"size 1",
|
||||
"filename avatar-bearing.agent.json",
|
||||
],
|
||||
]);
|
||||
assert.ok(
|
||||
sentMediaTags[0].includes("filename avatar-bearing.agent.json"),
|
||||
"snapshot imeta must retain its filename",
|
||||
);
|
||||
assert.ok(
|
||||
!sentMediaTags[0].some((entry) => entry.startsWith("thumb ")),
|
||||
"snapshot imeta must not include a thumb field",
|
||||
);
|
||||
});
|
||||
|
||||
test("runSendPipeline_happy_path_sets_all_phases", async () => {
|
||||
// All checkpoints pass and encode/upload/send succeed — full phase sequence.
|
||||
const states = [];
|
||||
|
||||
@@ -212,10 +212,6 @@ export async function runSendPipeline(deps: {
|
||||
};
|
||||
checkEligibilityFn: () => string | null;
|
||||
channelId: string;
|
||||
/** Optional avatar URL emitted as the NIP-92 `thumb` field on the snapshot
|
||||
* attachment. Decorative — lets the recipient card show the agent's avatar
|
||||
* without trusting sender-supplied identity. */
|
||||
thumbUrl?: string;
|
||||
}): Promise<boolean> {
|
||||
const {
|
||||
encodeFn,
|
||||
@@ -225,7 +221,6 @@ export async function runSendPipeline(deps: {
|
||||
buildMessageFn,
|
||||
checkEligibilityFn,
|
||||
channelId,
|
||||
thumbUrl,
|
||||
} = deps;
|
||||
|
||||
// ── Eligibility checkpoint 1: before encode ───────────────────────────────
|
||||
@@ -284,13 +279,13 @@ export async function runSendPipeline(deps: {
|
||||
}
|
||||
|
||||
// Preserve the original filename so `buildImetaTags` emits a `filename`
|
||||
// field and the recipient's FileCard renders the correct label.
|
||||
// Also carry the agent's avatar URL as the NIP-92 `thumb` field when
|
||||
// the caller supplied one — decorative hint for the recipient card.
|
||||
// field and the recipient's FileCard renders the correct label. Snapshot
|
||||
// sends never emit `thumb`: NIP-92 requires it to be this upload's local
|
||||
// thumbnail sidecar, which an agent avatar is not.
|
||||
const { thumb: _thumb, ...descriptorWithoutThumb } = descriptor;
|
||||
const descriptorWithFilename: BlobDescriptor = {
|
||||
...descriptor,
|
||||
...descriptorWithoutThumb,
|
||||
filename: fileName,
|
||||
...(thumbUrl ? { thumb: thumbUrl } : {}),
|
||||
};
|
||||
|
||||
// ── Build message content + NIP-92 imeta tags ─────────────────────────────
|
||||
@@ -361,14 +356,10 @@ export type UseSnapshotSendControllerResult = {
|
||||
* false and sets error state if blocked, ineligible, or if any step fails.
|
||||
* Never throws.
|
||||
*
|
||||
* @param thumbUrl Optional avatar URL emitted as the NIP-92 `thumb` imeta
|
||||
* field on the snapshot attachment — decorative, lets the recipient card
|
||||
* show the agent's avatar without any identity assertion.
|
||||
*/
|
||||
beginSend: (
|
||||
encodeFn: () => Promise<{ fileBytes: number[]; fileName: string }>,
|
||||
channelId: string,
|
||||
thumbUrl?: string,
|
||||
) => Promise<boolean>;
|
||||
/** Set state to error with a message (for pre-send gate failures). */
|
||||
setErrorState: (message: string) => void;
|
||||
@@ -461,12 +452,10 @@ export function useSnapshotSendController(): UseSnapshotSendControllerResult {
|
||||
async function beginSend(
|
||||
encodeFn: () => Promise<{ fileBytes: number[]; fileName: string }>,
|
||||
channelId: string,
|
||||
thumbUrl?: string,
|
||||
): Promise<boolean> {
|
||||
return runGuardedSend(guardRef.current, {
|
||||
encodeFn,
|
||||
channelId,
|
||||
thumbUrl,
|
||||
// Eligibility is checked by reading directly from the React Query cache
|
||||
// and the timeout external store — not from rendered React state.
|
||||
checkEligibilityFn: () => checkSendEligibility(queryClient, channelId),
|
||||
|
||||
@@ -225,44 +225,7 @@ test("resolveSnapshotCard: uppercase .AGENT.JSON classifies as snapshot card", (
|
||||
assert.equal(card.sha256, SHA256);
|
||||
});
|
||||
|
||||
// ── thumb plumbing ────────────────────────────────────────────────────────────
|
||||
|
||||
test("resolveSnapshotCard: thumb field from imeta is forwarded for .agent.json", () => {
|
||||
const thumbUrl = "https://relay.example/avatar.png";
|
||||
const card = resolveSnapshotCard(
|
||||
{
|
||||
m: "application/json",
|
||||
size: 500,
|
||||
filename: "bot.agent.json",
|
||||
x: SHA256,
|
||||
thumb: thumbUrl,
|
||||
},
|
||||
JSON_URL,
|
||||
"",
|
||||
);
|
||||
assert.ok(card !== null);
|
||||
assert.equal(card.thumb, thumbUrl);
|
||||
});
|
||||
|
||||
test("resolveSnapshotCard: relay-hosted JSON thumb is rewritten through media proxy", () => {
|
||||
const thumbUrl = `https://relay.example/media/${"b".repeat(64)}.png`;
|
||||
const card = resolveSnapshotCard(
|
||||
{
|
||||
m: "application/json",
|
||||
size: 500,
|
||||
filename: "bot.agent.json",
|
||||
x: SHA256,
|
||||
thumb: thumbUrl,
|
||||
},
|
||||
JSON_URL,
|
||||
"",
|
||||
);
|
||||
assert.ok(card !== null);
|
||||
assert.equal(
|
||||
card.thumb,
|
||||
`buzz-media://localhost/media/${"b".repeat(64)}.png`,
|
||||
);
|
||||
});
|
||||
// ── snapshot card thumbnails ─────────────────────────────────────────────────
|
||||
|
||||
test("resolveSnapshotCard: .agent.png uses its own URL as thumb", () => {
|
||||
const card = resolveSnapshotCard(
|
||||
|
||||
@@ -86,14 +86,9 @@ export function resolveSnapshotCard(
|
||||
size: entry.size,
|
||||
sha256,
|
||||
snapshotKind: "agent",
|
||||
// For PNG snapshots, use the attachment URL as the thumb source (it IS
|
||||
// the avatar card image). For JSON, use the explicit imeta `thumb` field
|
||||
// if the sender included one.
|
||||
thumb: isPng
|
||||
? rewriteRelayUrl(href)
|
||||
: entry.thumb
|
||||
? rewriteRelayUrl(entry.thumb)
|
||||
: undefined,
|
||||
// PNG snapshots use the attachment URL as the thumb source because it is
|
||||
// the avatar card image. JSON snapshots use the generic icon.
|
||||
thumb: isPng ? rewriteRelayUrl(href) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user