feat: accept Buzz rich and diff messages

This commit is contained in:
Shakker
2026-07-29 21:48:30 +01:00
committed by Shakker
parent 014e1a6c3a
commit 9716ae37bd
7 changed files with 479 additions and 25 deletions
+56 -1
View File
@@ -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 = [
+129 -1
View File
@@ -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({
+4 -3
View File
@@ -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",
+74 -1
View File
@@ -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);
+20 -12
View File
@@ -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,
},
});
+194 -7
View File
@@ -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 } : {}),
};
}