mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Fix agent timeline identity and avatars (#46)
This commit is contained in:
@@ -680,6 +680,8 @@ pub struct UserRecord {
|
||||
pub pubkey: Vec<u8>,
|
||||
/// Optional display name.
|
||||
pub display_name: Option<String>,
|
||||
/// Optional avatar image URL.
|
||||
pub avatar_url: Option<String>,
|
||||
/// Optional NIP-05 identifier (e.g. `user@example.com`).
|
||||
pub nip05_handle: Option<String>,
|
||||
}
|
||||
@@ -794,7 +796,7 @@ pub async fn get_users_bulk(pool: &MySqlPool, pubkeys: &[Vec<u8>]) -> Result<Vec
|
||||
// `.bind()` below. No user input is interpolated into the SQL string.
|
||||
let placeholders = pubkeys.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
|
||||
let sql = format!(
|
||||
"SELECT pubkey, display_name, nip05_handle FROM users WHERE pubkey IN ({placeholders})"
|
||||
"SELECT pubkey, display_name, avatar_url, nip05_handle FROM users WHERE pubkey IN ({placeholders})"
|
||||
);
|
||||
|
||||
let mut q = sqlx::query(&sql);
|
||||
@@ -809,6 +811,7 @@ pub async fn get_users_bulk(pool: &MySqlPool, pubkeys: &[Vec<u8>]) -> Result<Vec
|
||||
out.push(UserRecord {
|
||||
pubkey: row.try_get("pubkey")?,
|
||||
display_name: row.try_get("display_name")?,
|
||||
avatar_url: row.try_get("avatar_url")?,
|
||||
nip05_handle: row.try_get("nip05_handle")?,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use nostr::{EventBuilder, Kind, Tag};
|
||||
use rmcp::{
|
||||
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
|
||||
model::{ServerCapabilities, ServerInfo},
|
||||
@@ -634,8 +635,34 @@ impl SproutMcpServer {
|
||||
);
|
||||
}
|
||||
|
||||
// Route all messages through REST — avoids WebSocket timeout (~5 min).
|
||||
// The relay determines kind from channel_type; parent_event_id is optional.
|
||||
// Use a user-signed WebSocket event for top-level messages so downstream
|
||||
// clients see the agent pubkey directly rather than the relay pubkey.
|
||||
// Threaded replies still go through REST because that path handles the
|
||||
// reply ancestry tags and DB bookkeeping for us.
|
||||
if p.parent_event_id.is_none() {
|
||||
let kind = Kind::from(p.kind.unwrap_or(40001));
|
||||
let tags = vec![match Tag::parse(&["h", &p.channel_id]) {
|
||||
Ok(tag) => tag,
|
||||
Err(e) => return format!("Error: failed to build channel tag: {e}"),
|
||||
}];
|
||||
|
||||
let event =
|
||||
match EventBuilder::new(kind, p.content, tags).sign_with_keys(self.client.keys()) {
|
||||
Ok(event) => event,
|
||||
Err(e) => return format!("Error: failed to sign message event: {e}"),
|
||||
};
|
||||
|
||||
return match self.client.send_event(event).await {
|
||||
Ok(ok) => serde_json::json!({
|
||||
"event_id": ok.event_id,
|
||||
"accepted": ok.accepted,
|
||||
"message": ok.message,
|
||||
})
|
||||
.to_string(),
|
||||
Err(e) => format!("Error: {e}"),
|
||||
};
|
||||
}
|
||||
|
||||
let mut body = serde_json::json!({
|
||||
"content": p.content,
|
||||
});
|
||||
|
||||
@@ -173,7 +173,7 @@ pub struct BatchProfilesRequest {
|
||||
pub pubkeys: Vec<String>,
|
||||
}
|
||||
|
||||
/// `POST /api/users/batch` — resolve display names for multiple pubkeys.
|
||||
/// `POST /api/users/batch` — resolve profile summaries for multiple pubkeys.
|
||||
pub async fn get_users_batch(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
@@ -237,6 +237,7 @@ pub async fn get_users_batch(
|
||||
hex,
|
||||
serde_json::json!({
|
||||
"display_name": r.display_name,
|
||||
"avatar_url": r.avatar_url,
|
||||
"nip05_handle": r.nip05_handle,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -33,6 +33,7 @@ pub struct ProfileInfo {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct UserProfileSummaryInfo {
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub nip05_handle: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -6,18 +6,31 @@ import {
|
||||
type UserProfileLookup,
|
||||
} from "@/features/profile/lib/identity";
|
||||
|
||||
function getEffectiveAuthorPubkey(event: RelayEvent) {
|
||||
const [firstTag] = event.tags;
|
||||
if (
|
||||
firstTag?.[0] === "p" &&
|
||||
firstTag[1] &&
|
||||
event.tags.some((tag) => tag[0] === "h")
|
||||
) {
|
||||
return firstTag[1];
|
||||
}
|
||||
|
||||
return event.pubkey;
|
||||
}
|
||||
|
||||
function formatMessageAuthor(
|
||||
event: RelayEvent,
|
||||
channel: Channel | null,
|
||||
currentPubkey: string | undefined,
|
||||
profiles: UserProfileLookup | undefined,
|
||||
) {
|
||||
const authorPubkey = getEffectiveAuthorPubkey(event);
|
||||
const fallbackName =
|
||||
channel?.channelType === "dm"
|
||||
? (() => {
|
||||
const participantIndex = channel.participantPubkeys.indexOf(
|
||||
event.pubkey,
|
||||
);
|
||||
const participantIndex =
|
||||
channel.participantPubkeys.indexOf(authorPubkey);
|
||||
if (participantIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -27,7 +40,7 @@ function formatMessageAuthor(
|
||||
: null;
|
||||
|
||||
return resolveUserLabel({
|
||||
pubkey: event.pubkey,
|
||||
pubkey: authorPubkey,
|
||||
currentPubkey,
|
||||
fallbackName,
|
||||
profiles,
|
||||
@@ -35,6 +48,21 @@ function formatMessageAuthor(
|
||||
});
|
||||
}
|
||||
|
||||
function getAuthorAvatarUrl(input: {
|
||||
authorPubkey: string;
|
||||
currentPubkey: string | undefined;
|
||||
currentUserAvatarUrl: string | null;
|
||||
profiles: UserProfileLookup | undefined;
|
||||
}) {
|
||||
const { authorPubkey, currentPubkey, currentUserAvatarUrl, profiles } = input;
|
||||
|
||||
if (currentPubkey === authorPubkey) {
|
||||
return currentUserAvatarUrl ?? null;
|
||||
}
|
||||
|
||||
return profiles?.[authorPubkey.toLowerCase()]?.avatarUrl ?? null;
|
||||
}
|
||||
|
||||
export function formatTimelineMessages(
|
||||
events: RelayEvent[],
|
||||
channel: Channel | null,
|
||||
@@ -42,24 +70,36 @@ export function formatTimelineMessages(
|
||||
currentUserAvatarUrl: string | null,
|
||||
profiles?: UserProfileLookup,
|
||||
): TimelineMessage[] {
|
||||
return events.map((event) => ({
|
||||
id: event.id,
|
||||
pubkey: event.pubkey,
|
||||
author: formatMessageAuthor(event, channel, currentPubkey, profiles),
|
||||
avatarUrl:
|
||||
currentPubkey === event.pubkey ? (currentUserAvatarUrl ?? null) : null,
|
||||
time: new Intl.DateTimeFormat("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
}).format(new Date(event.created_at * 1_000)),
|
||||
body: event.content,
|
||||
accent: currentPubkey === event.pubkey,
|
||||
pending: event.pending,
|
||||
kind: event.kind,
|
||||
tags: event.tags,
|
||||
}));
|
||||
return events.map((event) => {
|
||||
const authorPubkey = getEffectiveAuthorPubkey(event);
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
pubkey: authorPubkey,
|
||||
author: formatMessageAuthor(event, channel, currentPubkey, profiles),
|
||||
avatarUrl: getAuthorAvatarUrl({
|
||||
authorPubkey,
|
||||
currentPubkey,
|
||||
currentUserAvatarUrl,
|
||||
profiles,
|
||||
}),
|
||||
time: new Intl.DateTimeFormat("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
}).format(new Date(event.created_at * 1_000)),
|
||||
body: event.content,
|
||||
accent: currentPubkey === authorPubkey,
|
||||
pending: event.pending,
|
||||
kind: event.kind,
|
||||
tags: event.tags,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function collectMessageAuthorPubkeys(events: RelayEvent[]) {
|
||||
return [...new Set(events.map((event) => event.pubkey.toLowerCase()))];
|
||||
return [
|
||||
...new Set(
|
||||
events.map((event) => getEffectiveAuthorPubkey(event).toLowerCase()),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ type RawProfile = {
|
||||
|
||||
type RawUserProfileSummary = {
|
||||
display_name: string | null;
|
||||
avatar_url: string | null;
|
||||
nip05_handle: string | null;
|
||||
};
|
||||
|
||||
@@ -297,6 +298,7 @@ function fromRawUserProfileSummary(
|
||||
): UserProfileSummary {
|
||||
return {
|
||||
displayName: profile.display_name,
|
||||
avatarUrl: profile.avatar_url,
|
||||
nip05Handle: profile.nip05_handle,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ export type Profile = {
|
||||
|
||||
export type UserProfileSummary = {
|
||||
displayName: string | null;
|
||||
avatarUrl: string | null;
|
||||
nip05Handle: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ type RawProfile = {
|
||||
|
||||
type RawUserProfileSummary = {
|
||||
display_name: string | null;
|
||||
avatar_url: string | null;
|
||||
nip05_handle: string | null;
|
||||
};
|
||||
|
||||
@@ -1098,6 +1099,7 @@ async function handleGetUsersBatch(
|
||||
|
||||
profiles[normalizedPubkey] = {
|
||||
display_name: profile.display_name,
|
||||
avatar_url: profile.avatar_url,
|
||||
nip05_handle: profile.nip05_handle,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user