mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
feat: accept Buzz rich and diff messages
This commit is contained in:
@@ -63,6 +63,12 @@ vi.mock("nostr-tools", async (importOriginal) => {
|
||||
});
|
||||
|
||||
import { sendBuzzTextOneShot, startBuzzBus } from "./buzz-bus.js";
|
||||
import {
|
||||
BUZZ_DIFF_MESSAGE_KIND,
|
||||
BUZZ_INBOUND_MESSAGE_KINDS,
|
||||
BUZZ_RICH_MESSAGE_KIND,
|
||||
type BuzzInboundMessage,
|
||||
} from "./message-event.js";
|
||||
|
||||
const PRIVATE_KEY = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
|
||||
const SENDER_PRIVATE_KEY = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
|
||||
@@ -175,7 +181,7 @@ describe("Buzz bus lifecycle", () => {
|
||||
|
||||
it("deduplicates replayed relay events by event id", async () => {
|
||||
relayMocks.auth.mockResolvedValue("ok");
|
||||
const onMessage = vi.fn(async () => {});
|
||||
const onMessage = vi.fn(async (_message: BuzzInboundMessage) => {});
|
||||
const bus = await startBuzzBus({
|
||||
accountId: ACCOUNT_ID,
|
||||
relayUrl: "wss://buzz.example.com",
|
||||
@@ -203,6 +209,55 @@ describe("Buzz bus lifecycle", () => {
|
||||
await bus.close();
|
||||
});
|
||||
|
||||
it("subscribes to and dispatches every supported Buzz timeline message kind", async () => {
|
||||
relayMocks.auth.mockResolvedValue("ok");
|
||||
const receivedKinds: number[] = [];
|
||||
const onMessage = vi.fn(async (message: BuzzInboundMessage) => {
|
||||
receivedKinds.push(message.kind);
|
||||
});
|
||||
const bus = await startBuzzBus({
|
||||
accountId: ACCOUNT_ID,
|
||||
relayUrl: "wss://buzz.example.com",
|
||||
privateKey: PRIVATE_KEY,
|
||||
channelIds: [CHANNEL_ID],
|
||||
onMessage,
|
||||
});
|
||||
const messageSubscription = relayMocks.subscriptions.find((entry) =>
|
||||
entry.filter.kinds?.includes(9),
|
||||
);
|
||||
expect(messageSubscription?.filter.kinds).toEqual([...BUZZ_INBOUND_MESSAGE_KINDS]);
|
||||
|
||||
const richEvent = finalizeEvent(
|
||||
{
|
||||
kind: BUZZ_RICH_MESSAGE_KIND,
|
||||
created_at: 1_700_000_000,
|
||||
content: "**rich**",
|
||||
tags: [["h", CHANNEL_ID]],
|
||||
},
|
||||
Uint8Array.from(Buffer.from(SENDER_PRIVATE_KEY, "hex")),
|
||||
);
|
||||
const diffEvent = finalizeEvent(
|
||||
{
|
||||
kind: BUZZ_DIFF_MESSAGE_KIND,
|
||||
created_at: 1_700_000_001,
|
||||
content: "@@ -1 +1 @@\n-old\n+new",
|
||||
tags: [
|
||||
["h", CHANNEL_ID],
|
||||
["repo", "https://github.com/openclaw/openclaw"],
|
||||
["commit", "abcdef1"],
|
||||
],
|
||||
},
|
||||
Uint8Array.from(Buffer.from(SENDER_PRIVATE_KEY, "hex")),
|
||||
);
|
||||
|
||||
messageSubscription?.handlers.onevent(richEvent);
|
||||
messageSubscription?.handlers.onevent(diffEvent);
|
||||
|
||||
await vi.waitFor(() => expect(onMessage).toHaveBeenCalledTimes(2));
|
||||
expect(receivedKinds).toEqual([BUZZ_RICH_MESSAGE_KIND, BUZZ_DIFF_MESSAGE_KIND]);
|
||||
await bus.close();
|
||||
});
|
||||
|
||||
it("isolates message failures from fatal relay failures", async () => {
|
||||
relayMocks.auth.mockResolvedValue("ok");
|
||||
relayMocks.profileEvents = [
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { finalizeEvent } from "nostr-tools";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildBuzzMessageTags, parseBuzzMessageEvent } from "./message-event.js";
|
||||
import {
|
||||
BUZZ_DIFF_MESSAGE_KIND,
|
||||
BUZZ_NORMAL_MESSAGE_KIND,
|
||||
BUZZ_RICH_MESSAGE_KIND,
|
||||
buildBuzzMessageTags,
|
||||
formatBuzzMessageForAgent,
|
||||
parseBuzzMessageEvent,
|
||||
} from "./message-event.js";
|
||||
import { parseBuzzAuthTag } from "./relay-auth.js";
|
||||
|
||||
const SECRET_KEY = Uint8Array.from(
|
||||
@@ -26,6 +33,7 @@ describe("Buzz message events", () => {
|
||||
|
||||
expect(parseBuzzMessageEvent(event)).toMatchObject({
|
||||
id: event.id,
|
||||
kind: BUZZ_NORMAL_MESSAGE_KIND,
|
||||
text: "hello OpenClaw",
|
||||
channelId: "7c4a6d2a-2ed9-4b4e-a5e2-4d705ee9b34c",
|
||||
threadId: "root-id",
|
||||
@@ -34,6 +42,89 @@ describe("Buzz message events", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("parses Buzz rich-content messages", () => {
|
||||
const event = finalizeEvent(
|
||||
{
|
||||
kind: BUZZ_RICH_MESSAGE_KIND,
|
||||
created_at: 1_700_000_000,
|
||||
content: "**hello** OpenClaw",
|
||||
tags: [["h", "7c4a6d2a-2ed9-4b4e-a5e2-4d705ee9b34c"]],
|
||||
},
|
||||
SECRET_KEY,
|
||||
);
|
||||
|
||||
expect(parseBuzzMessageEvent(event)).toMatchObject({
|
||||
kind: BUZZ_RICH_MESSAGE_KIND,
|
||||
text: "**hello** OpenClaw",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses and formats Buzz structured diff events", () => {
|
||||
const event = finalizeEvent(
|
||||
{
|
||||
kind: BUZZ_DIFF_MESSAGE_KIND,
|
||||
created_at: 1_700_000_000,
|
||||
content: "@@ -1 +1 @@\n-old\n+new",
|
||||
tags: [
|
||||
["h", "7c4a6d2a-2ed9-4b4e-a5e2-4d705ee9b34c"],
|
||||
["repo", "https://github.com/openclaw/openclaw"],
|
||||
["commit", "abcdef1234567890"],
|
||||
["file", "extensions/buzz/src/message-event.ts"],
|
||||
["parent-commit", "1234567890abcdef"],
|
||||
["branch", "feature/buzz", "main"],
|
||||
["pr", "113419"],
|
||||
["l", "typescript"],
|
||||
["description", "Preserve\nstructured diff context"],
|
||||
["truncated", "true"],
|
||||
["alt", "Buzz plugin diff"],
|
||||
["e", "root-id", "", "root"],
|
||||
["e", "reply-id", "", "reply"],
|
||||
],
|
||||
},
|
||||
SECRET_KEY,
|
||||
);
|
||||
|
||||
const message = parseBuzzMessageEvent(event);
|
||||
expect(message).toMatchObject({
|
||||
kind: BUZZ_DIFF_MESSAGE_KIND,
|
||||
threadId: "root-id",
|
||||
replyToId: "reply-id",
|
||||
diff: {
|
||||
repoUrl: "https://github.com/openclaw/openclaw",
|
||||
commitSha: "abcdef1234567890",
|
||||
filePath: "extensions/buzz/src/message-event.ts",
|
||||
parentCommitSha: "1234567890abcdef",
|
||||
sourceBranch: "feature/buzz",
|
||||
targetBranch: "main",
|
||||
pullRequestNumber: 113419,
|
||||
language: "typescript",
|
||||
description: "Preserve\nstructured diff context",
|
||||
truncated: true,
|
||||
altText: "Buzz plugin diff",
|
||||
},
|
||||
});
|
||||
expect(message && formatBuzzMessageForAgent(message)).toBe(
|
||||
[
|
||||
"[Buzz structured diff]",
|
||||
"Repository: https://github.com/openclaw/openclaw",
|
||||
"Commit: abcdef1234567890",
|
||||
"Parent commit: 1234567890abcdef",
|
||||
"File: extensions/buzz/src/message-event.ts",
|
||||
"Branches: feature/buzz -> main",
|
||||
"Pull request: #113419",
|
||||
"Language: typescript",
|
||||
"Description: Preserve structured diff context",
|
||||
"Alt text: Buzz plugin diff",
|
||||
"Truncated: yes",
|
||||
"",
|
||||
"Unified diff:",
|
||||
"@@ -1 +1 @@",
|
||||
"-old",
|
||||
"+new",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores non-channel events", () => {
|
||||
const event = finalizeEvent(
|
||||
{ kind: 9, created_at: 1_700_000_000, content: "hello", tags: [] },
|
||||
@@ -42,6 +133,43 @@ describe("Buzz message events", () => {
|
||||
expect(parseBuzzMessageEvent(event)).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects unsupported, blank, oversized, and malformed diff events", () => {
|
||||
const sign = (kind: number, content: string, tags: string[][]) =>
|
||||
finalizeEvent({ kind, created_at: 1_700_000_000, content, tags }, SECRET_KEY);
|
||||
const room = ["h", "7c4a6d2a-2ed9-4b4e-a5e2-4d705ee9b34c"];
|
||||
|
||||
expect(parseBuzzMessageEvent(sign(1, "hello", [room]))).toBeNull();
|
||||
expect(parseBuzzMessageEvent(sign(BUZZ_RICH_MESSAGE_KIND, " \n ", [room]))).toBeNull();
|
||||
expect(
|
||||
parseBuzzMessageEvent(sign(BUZZ_NORMAL_MESSAGE_KIND, "x".repeat(256 * 1024 + 1), [room])),
|
||||
).toBeNull();
|
||||
expect(
|
||||
parseBuzzMessageEvent(
|
||||
sign(BUZZ_DIFF_MESSAGE_KIND, "diff", [
|
||||
room,
|
||||
["repo", "https://github.com/openclaw/openclaw"],
|
||||
]),
|
||||
),
|
||||
).toBeNull();
|
||||
expect(
|
||||
parseBuzzMessageEvent(
|
||||
sign(BUZZ_DIFF_MESSAGE_KIND, "x".repeat(60 * 1024 + 1), [
|
||||
room,
|
||||
["repo", "https://github.com/openclaw/openclaw"],
|
||||
["commit", "abcdef1"],
|
||||
]),
|
||||
),
|
||||
).toBeNull();
|
||||
expect(
|
||||
parseBuzzMessageEvent(
|
||||
sign(BUZZ_NORMAL_MESSAGE_KIND, "hello", [
|
||||
room,
|
||||
...Array.from({ length: 51 }, (_, index) => ["p", index.toString(16).padStart(64, "0")]),
|
||||
]),
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("builds direct and nested reply tags like the Buzz SDK", () => {
|
||||
expect(
|
||||
buildBuzzMessageTags({
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Relay, finalizeEvent, type Event } from "nostr-tools";
|
||||
import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
|
||||
import {
|
||||
BUZZ_INBOUND_MESSAGE_KINDS,
|
||||
BUZZ_NORMAL_MESSAGE_KIND,
|
||||
buildBuzzMessageTags,
|
||||
parseBuzzMessageEvent,
|
||||
type BuzzInboundMessage,
|
||||
@@ -17,7 +19,6 @@ import {
|
||||
} from "./room-membership.js";
|
||||
import { decodeBuzzPrivateKey, resolveBuzzPublicKey } from "./types.js";
|
||||
|
||||
const MESSAGE_KIND = 9;
|
||||
const PRESENCE_KIND = 20_001;
|
||||
const PRESENCE_HEARTBEAT_INTERVAL_MS = 30_000;
|
||||
const REPLAY_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
@@ -49,7 +50,7 @@ function buildBuzzTextEvent(params: {
|
||||
}): Event {
|
||||
return finalizeEvent(
|
||||
{
|
||||
kind: MESSAGE_KIND,
|
||||
kind: BUZZ_NORMAL_MESSAGE_KIND,
|
||||
content: params.text,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: buildBuzzMessageTags(params),
|
||||
@@ -614,7 +615,7 @@ export async function startBuzzBus(options: {
|
||||
relay.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [MESSAGE_KIND],
|
||||
kinds: [...BUZZ_INBOUND_MESSAGE_KINDS],
|
||||
"#h": [channelId],
|
||||
since: options.since ?? sessionStartedAt,
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@ vi.mock("./inbound.js", () => ({
|
||||
}));
|
||||
|
||||
import { buzzOutboundAdapter, startBuzzGatewayAccount } from "./gateway.js";
|
||||
import { BUZZ_NORMAL_MESSAGE_KIND } from "./message-event.js";
|
||||
import { setBuzzRuntime } from "./runtime.js";
|
||||
import { resolveBuzzAccount } from "./types.js";
|
||||
|
||||
@@ -312,6 +313,7 @@ describe("Buzz gateway lifecycle", () => {
|
||||
await gatewayMocks.onMessage?.(
|
||||
{
|
||||
id: "event-1",
|
||||
kind: BUZZ_NORMAL_MESSAGE_KIND,
|
||||
channelId: CHANNEL_ID,
|
||||
senderPubkey: "b".repeat(64),
|
||||
text: "hello",
|
||||
|
||||
@@ -4,7 +4,11 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { BuzzBus } from "./buzz-bus.js";
|
||||
import { handleBuzzInbound } from "./inbound.js";
|
||||
import type { BuzzInboundMessage } from "./message-event.js";
|
||||
import {
|
||||
BUZZ_DIFF_MESSAGE_KIND,
|
||||
BUZZ_NORMAL_MESSAGE_KIND,
|
||||
type BuzzInboundMessage,
|
||||
} from "./message-event.js";
|
||||
import { setBuzzRuntime } from "./runtime.js";
|
||||
import type { ResolvedBuzzAccount } from "./types.js";
|
||||
|
||||
@@ -40,6 +44,7 @@ function createAccount(
|
||||
function createMessage(overrides: Partial<BuzzInboundMessage> = {}): BuzzInboundMessage {
|
||||
return {
|
||||
id: "event-1",
|
||||
kind: BUZZ_NORMAL_MESSAGE_KIND,
|
||||
senderPubkey: SENDER_PUBLIC_KEY,
|
||||
text: "hello",
|
||||
channelId: ROOM_ID,
|
||||
@@ -216,6 +221,74 @@ describe("handleBuzzInbound", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("provides bounded structured diff context without treating diff content as commands", async () => {
|
||||
const runtime = createPluginRuntimeMock();
|
||||
vi.mocked(runtime.channel.commands.shouldComputeCommandAuthorized).mockReturnValue(true);
|
||||
setBuzzRuntime(runtime);
|
||||
|
||||
await handleBuzzInbound({
|
||||
account: createAccount({
|
||||
groups: {
|
||||
[ROOM_ID]: {
|
||||
requireMention: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
cfg: {} satisfies OpenClawConfig,
|
||||
bus: createBus(),
|
||||
message: createMessage({
|
||||
kind: BUZZ_DIFF_MESSAGE_KIND,
|
||||
text: "/status\n@@ -1 +1 @@\n-old\n+new",
|
||||
diff: {
|
||||
repoUrl: "https://github.com/openclaw/openclaw",
|
||||
commitSha: "abcdef1",
|
||||
description: `line one\n${"x".repeat(1_100)}`,
|
||||
truncated: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(runtime.channel.commands.shouldComputeCommandAuthorized).not.toHaveBeenCalled();
|
||||
const context = firstDispatch(runtime).ctxPayload;
|
||||
expect(context).toMatchObject({
|
||||
BuzzEventKind: BUZZ_DIFF_MESSAGE_KIND,
|
||||
RawBody: "/status\n@@ -1 +1 @@\n-old\n+new",
|
||||
CommandBody: "",
|
||||
BodyForCommands: "",
|
||||
});
|
||||
const bodyForAgent = context.BodyForAgent ?? "";
|
||||
expect(bodyForAgent).toContain("[Buzz structured diff]");
|
||||
expect(bodyForAgent).toContain("Repository: https://github.com/openclaw/openclaw");
|
||||
expect(bodyForAgent).toContain("Description: line one ");
|
||||
expect(bodyForAgent).toContain("Truncated: yes");
|
||||
expect(bodyForAgent).toContain("Unified diff:\n/status\n@@ -1 +1 @@\n-old\n+new");
|
||||
expect(bodyForAgent.length).toBeLessThan(2_000);
|
||||
});
|
||||
|
||||
it("does not treat mention-like text inside a structured diff as a bot mention", async () => {
|
||||
const runtime = createPluginRuntimeMock();
|
||||
vi.mocked(runtime.channel.mentions.matchesMentionPatterns).mockReturnValue(true);
|
||||
setBuzzRuntime(runtime);
|
||||
|
||||
await handleBuzzInbound({
|
||||
account: createAccount(),
|
||||
cfg: {} satisfies OpenClawConfig,
|
||||
bus: createBus(),
|
||||
message: createMessage({
|
||||
kind: BUZZ_DIFF_MESSAGE_KIND,
|
||||
text: "+const owner = '@OpenClaw';",
|
||||
diff: {
|
||||
repoUrl: "https://github.com/openclaw/openclaw",
|
||||
commitSha: "abcdef1",
|
||||
truncated: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(runtime.channel.mentions.matchesMentionPatterns).not.toHaveBeenCalled();
|
||||
expect(runtime.channel.inbound.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("propagates delivery and session-recording failures", async () => {
|
||||
const runtime = createPluginRuntimeMock();
|
||||
setBuzzRuntime(runtime);
|
||||
|
||||
@@ -5,7 +5,11 @@ import {
|
||||
import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import type { BuzzBus } from "./buzz-bus.js";
|
||||
import type { BuzzInboundMessage } from "./message-event.js";
|
||||
import {
|
||||
BUZZ_DIFF_MESSAGE_KIND,
|
||||
formatBuzzMessageForAgent,
|
||||
type BuzzInboundMessage,
|
||||
} from "./message-event.js";
|
||||
import { getBuzzRuntime } from "./runtime.js";
|
||||
import { buildBuzzTarget, parseBuzzTarget } from "./target.js";
|
||||
import type { ResolvedBuzzAccount } from "./types.js";
|
||||
@@ -24,21 +28,24 @@ export async function handleBuzzInbound(params: {
|
||||
const { account, cfg, bus, message } = params;
|
||||
const channelId = parseBuzzTarget(message.channelId);
|
||||
const target = buildBuzzTarget(channelId);
|
||||
const textForAgent = formatBuzzMessageForAgent(message);
|
||||
const { route, buildEnvelope } = resolveChannelInboundRouteEnvelope({
|
||||
cfg,
|
||||
channel: "buzz",
|
||||
accountId: account.accountId,
|
||||
peer: { kind: "group", id: target },
|
||||
});
|
||||
const textMention = runtime.channel.mentions.matchesMentionPatterns(
|
||||
message.text,
|
||||
runtime.channel.mentions.buildMentionRegexes(cfg, route.agentId),
|
||||
);
|
||||
const supportsTextInterpretation = message.kind !== BUZZ_DIFF_MESSAGE_KIND;
|
||||
const textMention =
|
||||
supportsTextInterpretation &&
|
||||
runtime.channel.mentions.matchesMentionPatterns(
|
||||
message.text,
|
||||
runtime.channel.mentions.buildMentionRegexes(cfg, route.agentId),
|
||||
);
|
||||
const wasMentioned = message.mentionedPubkeys.includes(bus.publicKey) || textMention;
|
||||
const shouldComputeCommandAuthorized = runtime.channel.commands.shouldComputeCommandAuthorized(
|
||||
message.text,
|
||||
cfg,
|
||||
);
|
||||
const shouldComputeCommandAuthorized =
|
||||
supportsTextInterpretation &&
|
||||
runtime.channel.commands.shouldComputeCommandAuthorized(message.text, cfg);
|
||||
const hasControlCommand =
|
||||
shouldComputeCommandAuthorized && runtime.channel.text.hasControlCommand(message.text, cfg);
|
||||
const groupConfig = account.config.groups?.[channelId];
|
||||
@@ -77,7 +84,7 @@ export async function handleBuzzInbound(params: {
|
||||
channel: "Buzz",
|
||||
from: senderName,
|
||||
timestamp: new Date(message.createdAt * 1000),
|
||||
body: message.text,
|
||||
body: textForAgent,
|
||||
});
|
||||
const ctxPayload = buildChannelInboundEventContext({
|
||||
channel: "buzz",
|
||||
@@ -109,9 +116,9 @@ export async function handleBuzzInbound(params: {
|
||||
},
|
||||
message: {
|
||||
body,
|
||||
bodyForAgent: message.text,
|
||||
bodyForAgent: textForAgent,
|
||||
rawBody: message.text,
|
||||
commandBody: message.text,
|
||||
commandBody: supportsTextInterpretation ? message.text : "",
|
||||
},
|
||||
access: {
|
||||
commands: { authorized: access.commandAccess.authorized },
|
||||
@@ -120,6 +127,7 @@ export async function handleBuzzInbound(params: {
|
||||
extra: {
|
||||
GroupChannel: channelId,
|
||||
GroupSubject: channelId,
|
||||
BuzzEventKind: message.kind,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,42 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
import type { Event } from "nostr-tools";
|
||||
|
||||
const MESSAGE_KIND = 9;
|
||||
export const BUZZ_NORMAL_MESSAGE_KIND = 9;
|
||||
export const BUZZ_RICH_MESSAGE_KIND = 40_002;
|
||||
export const BUZZ_DIFF_MESSAGE_KIND = 40_008;
|
||||
export const BUZZ_INBOUND_MESSAGE_KINDS = [
|
||||
BUZZ_NORMAL_MESSAGE_KIND,
|
||||
BUZZ_RICH_MESSAGE_KIND,
|
||||
BUZZ_DIFF_MESSAGE_KIND,
|
||||
] as const;
|
||||
|
||||
export type BuzzInboundMessageKind = (typeof BUZZ_INBOUND_MESSAGE_KINDS)[number];
|
||||
|
||||
// Buzz relay ingest accepts up to 256 KiB generally; diff events have their
|
||||
// own stricter validator. Keep inbound admission aligned with those limits.
|
||||
const BUZZ_MESSAGE_CONTENT_MAX_BYTES = 256 * 1024;
|
||||
const BUZZ_DIFF_CONTENT_MAX_BYTES = 60 * 1024;
|
||||
const BUZZ_MENTION_MAX_COUNT = 50;
|
||||
const BUZZ_DIFF_CONTEXT_FIELD_MAX_CHARS = 1_024;
|
||||
const BUZZ_INBOUND_MESSAGE_KIND_SET = new Set<number>(BUZZ_INBOUND_MESSAGE_KINDS);
|
||||
|
||||
export interface BuzzDiffMetadata {
|
||||
repoUrl: string;
|
||||
commitSha: string;
|
||||
filePath?: string;
|
||||
parentCommitSha?: string;
|
||||
sourceBranch?: string;
|
||||
targetBranch?: string;
|
||||
pullRequestNumber?: number;
|
||||
language?: string;
|
||||
description?: string;
|
||||
truncated: boolean;
|
||||
altText?: string;
|
||||
}
|
||||
|
||||
export interface BuzzInboundMessage {
|
||||
id: string;
|
||||
kind: BuzzInboundMessageKind;
|
||||
senderPubkey: string;
|
||||
text: string;
|
||||
channelId: string;
|
||||
@@ -11,18 +44,159 @@ export interface BuzzInboundMessage {
|
||||
threadId?: string;
|
||||
replyToId?: string;
|
||||
mentionedPubkeys: string[];
|
||||
diff?: BuzzDiffMetadata;
|
||||
}
|
||||
|
||||
function tagValue(event: Event, name: string): string | undefined {
|
||||
return event.tags.find((tag) => tag[0] === name)?.[1];
|
||||
const value = event.tags.find((tag) => tag[0] === name)?.[1]?.trim();
|
||||
return value ? value : undefined;
|
||||
}
|
||||
|
||||
function markerTagValue(event: Event, marker: string): string | undefined {
|
||||
return event.tags.find((tag) => tag[0] === "e" && tag[3] === marker)?.[1];
|
||||
const value = event.tags.find((tag) => tag[0] === "e" && tag[3] === marker)?.[1]?.trim();
|
||||
return value ? value : undefined;
|
||||
}
|
||||
|
||||
function isHexAtLeast(value: string, minimumLength: number): boolean {
|
||||
return value.length >= minimumLength && /^[a-f0-9]+$/iu.test(value);
|
||||
}
|
||||
|
||||
function parseBuzzDiffMetadata(event: Event): BuzzDiffMetadata | null {
|
||||
let repoUrl: string | undefined;
|
||||
let commitSha: string | undefined;
|
||||
let filePath: string | undefined;
|
||||
let parentCommitSha: string | undefined;
|
||||
let sourceBranch: string | undefined;
|
||||
let targetBranch: string | undefined;
|
||||
let pullRequestNumber: number | undefined;
|
||||
let language: string | undefined;
|
||||
let description: string | undefined;
|
||||
let truncated = false;
|
||||
let altText: string | undefined;
|
||||
|
||||
for (const tag of event.tags) {
|
||||
const name = tag[0];
|
||||
const value = tag[1];
|
||||
if (!name || value === undefined) {
|
||||
continue;
|
||||
}
|
||||
switch (name) {
|
||||
case "repo":
|
||||
if (!value.startsWith("http://") && !value.startsWith("https://")) {
|
||||
return null;
|
||||
}
|
||||
repoUrl ??= value;
|
||||
break;
|
||||
case "commit":
|
||||
if (!isHexAtLeast(value, 7)) {
|
||||
return null;
|
||||
}
|
||||
commitSha ??= value;
|
||||
break;
|
||||
case "file":
|
||||
filePath ??= value;
|
||||
break;
|
||||
case "parent-commit":
|
||||
if (!isHexAtLeast(value, 7)) {
|
||||
return null;
|
||||
}
|
||||
parentCommitSha ??= value;
|
||||
break;
|
||||
case "branch":
|
||||
if (!value || !tag[2]) {
|
||||
return null;
|
||||
}
|
||||
sourceBranch ??= value;
|
||||
targetBranch ??= tag[2];
|
||||
break;
|
||||
case "pr": {
|
||||
if (!/^[0-9]+$/u.test(value)) {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
if (!Number.isSafeInteger(parsed) || parsed <= 0 || parsed > 0xffff_ffff) {
|
||||
return null;
|
||||
}
|
||||
pullRequestNumber ??= parsed;
|
||||
break;
|
||||
}
|
||||
case "l":
|
||||
language ??= value;
|
||||
break;
|
||||
case "description":
|
||||
description ??= value;
|
||||
break;
|
||||
case "truncated":
|
||||
truncated ||= value === "true";
|
||||
break;
|
||||
case "alt":
|
||||
altText ??= value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!repoUrl || !commitSha) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
repoUrl,
|
||||
commitSha,
|
||||
filePath,
|
||||
parentCommitSha,
|
||||
sourceBranch,
|
||||
targetBranch,
|
||||
pullRequestNumber,
|
||||
language,
|
||||
description,
|
||||
truncated,
|
||||
altText,
|
||||
};
|
||||
}
|
||||
|
||||
function boundedDiffContextValue(value: string): string {
|
||||
const singleLine = value.replace(/\s+/gu, " ").trim();
|
||||
if (singleLine.length <= BUZZ_DIFF_CONTEXT_FIELD_MAX_CHARS) {
|
||||
return singleLine;
|
||||
}
|
||||
return `${singleLine.slice(0, BUZZ_DIFF_CONTEXT_FIELD_MAX_CHARS - 3)}...`;
|
||||
}
|
||||
|
||||
export function formatBuzzMessageForAgent(message: BuzzInboundMessage): string {
|
||||
if (message.kind !== BUZZ_DIFF_MESSAGE_KIND || !message.diff) {
|
||||
return message.text;
|
||||
}
|
||||
const { diff } = message;
|
||||
const metadata = [
|
||||
`Repository: ${boundedDiffContextValue(diff.repoUrl)}`,
|
||||
`Commit: ${boundedDiffContextValue(diff.commitSha)}`,
|
||||
diff.parentCommitSha
|
||||
? `Parent commit: ${boundedDiffContextValue(diff.parentCommitSha)}`
|
||||
: undefined,
|
||||
diff.filePath ? `File: ${boundedDiffContextValue(diff.filePath)}` : undefined,
|
||||
diff.sourceBranch && diff.targetBranch
|
||||
? `Branches: ${boundedDiffContextValue(diff.sourceBranch)} -> ${boundedDiffContextValue(diff.targetBranch)}`
|
||||
: undefined,
|
||||
diff.pullRequestNumber ? `Pull request: #${diff.pullRequestNumber}` : undefined,
|
||||
diff.language ? `Language: ${boundedDiffContextValue(diff.language)}` : undefined,
|
||||
diff.description ? `Description: ${boundedDiffContextValue(diff.description)}` : undefined,
|
||||
diff.altText ? `Alt text: ${boundedDiffContextValue(diff.altText)}` : undefined,
|
||||
diff.truncated ? "Truncated: yes" : undefined,
|
||||
].filter((line): line is string => Boolean(line));
|
||||
|
||||
return [`[Buzz structured diff]`, ...metadata, "", "Unified diff:", message.text].join("\n");
|
||||
}
|
||||
|
||||
export function parseBuzzMessageEvent(event: Event): BuzzInboundMessage | null {
|
||||
if (event.kind !== MESSAGE_KIND || !event.content.trim()) {
|
||||
if (
|
||||
!BUZZ_INBOUND_MESSAGE_KIND_SET.has(event.kind) ||
|
||||
!event.content.trim() ||
|
||||
Buffer.byteLength(event.content, "utf8") >
|
||||
(event.kind === BUZZ_DIFF_MESSAGE_KIND
|
||||
? BUZZ_DIFF_CONTENT_MAX_BYTES
|
||||
: BUZZ_MESSAGE_CONTENT_MAX_BYTES)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const channelId = tagValue(event, "h");
|
||||
@@ -31,17 +205,30 @@ export function parseBuzzMessageEvent(event: Event): BuzzInboundMessage | null {
|
||||
}
|
||||
const rootId = markerTagValue(event, "root");
|
||||
const replyToId = markerTagValue(event, "reply");
|
||||
const kind = event.kind as BuzzInboundMessageKind;
|
||||
const diff = kind === BUZZ_DIFF_MESSAGE_KIND ? parseBuzzDiffMetadata(event) : undefined;
|
||||
if (kind === BUZZ_DIFF_MESSAGE_KIND && !diff) {
|
||||
return null;
|
||||
}
|
||||
const mentionTagValues = event.tags
|
||||
.filter((tag) => tag[0] === "p" && Boolean(tag[1]))
|
||||
.map((tag) => (tag[1] as string).trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
if (mentionTagValues.length > BUZZ_MENTION_MAX_COUNT) {
|
||||
return null;
|
||||
}
|
||||
const mentionedPubkeys = [...new Set(mentionTagValues)];
|
||||
return {
|
||||
id: event.id,
|
||||
kind,
|
||||
senderPubkey: event.pubkey,
|
||||
text: event.content,
|
||||
channelId,
|
||||
createdAt: event.created_at,
|
||||
threadId: rootId ?? replyToId,
|
||||
replyToId,
|
||||
mentionedPubkeys: event.tags
|
||||
.filter((tag) => tag[0] === "p" && Boolean(tag[1]))
|
||||
.map((tag) => tag[1] as string),
|
||||
mentionedPubkeys,
|
||||
...(diff ? { diff } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user