Implements a Buzz channel backed by a Nostr relay: - kind:9 events scoped by "h" tag, "p" tags for mentions - NIP-42 (kind:22242) auth before subscription - optional threaded replies via "e" tag - registered as config.ChannelBuzz with BuzzSettings
125 lines
2.8 KiB
Go
125 lines
2.8 KiB
Go
package buzz
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/nbd-wtf/go-nostr"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/bus"
|
|
"github.com/sipeed/picoclaw/pkg/identity"
|
|
"github.com/sipeed/picoclaw/pkg/logger"
|
|
)
|
|
|
|
// consume drains the subscription until the channel context is cancelled.
|
|
func (c *BuzzChannel) consume(sub *nostr.Subscription) {
|
|
for {
|
|
select {
|
|
case <-c.ctx.Done():
|
|
return
|
|
case reason, ok := <-sub.ClosedReason:
|
|
if !ok {
|
|
return
|
|
}
|
|
logger.WarnCF("buzz", "Relay closed subscription", map[string]any{"reason": reason})
|
|
return
|
|
case evt, ok := <-sub.Events:
|
|
if !ok {
|
|
return
|
|
}
|
|
c.onEvent(evt)
|
|
}
|
|
}
|
|
}
|
|
|
|
// onEvent converts a kind:9 relay event into an inbound bus message.
|
|
func (c *BuzzChannel) onEvent(evt *nostr.Event) {
|
|
if evt == nil || evt.Kind != kindStreamMessage {
|
|
return
|
|
}
|
|
|
|
// Ignore our own messages, otherwise the agent replies to itself.
|
|
if evt.PubKey == c.publicKey {
|
|
return
|
|
}
|
|
|
|
// The "h" tag scopes the event to a channel; without it there is nowhere to reply.
|
|
chatID := firstTagValue(evt.Tags, "h")
|
|
if chatID == "" {
|
|
return
|
|
}
|
|
|
|
sender := bus.SenderInfo{
|
|
Platform: "buzz",
|
|
PlatformID: evt.PubKey,
|
|
CanonicalID: identity.BuildCanonicalID("buzz", evt.PubKey),
|
|
Username: shortPubkey(evt.PubKey),
|
|
DisplayName: shortPubkey(evt.PubKey),
|
|
}
|
|
|
|
if !c.IsAllowedSender(sender) {
|
|
return
|
|
}
|
|
|
|
// Mentions are "p" tags carrying the mentioned pubkey.
|
|
isMentioned := hasPTag(evt.Tags, c.publicKey)
|
|
|
|
content := evt.Content
|
|
respond, cleaned := c.ShouldRespondInGroup(isMentioned, content)
|
|
if !respond {
|
|
return
|
|
}
|
|
content = cleaned
|
|
|
|
if strings.TrimSpace(content) == "" {
|
|
return
|
|
}
|
|
|
|
inboundCtx := bus.InboundContext{
|
|
Channel: "buzz",
|
|
ChatID: chatID,
|
|
ChatType: "channel",
|
|
SenderID: evt.PubKey,
|
|
MessageID: evt.ID,
|
|
Mentioned: isMentioned,
|
|
Raw: map[string]string{
|
|
"platform": "buzz",
|
|
"relay": c.config.RelayURL,
|
|
"channel": chatID,
|
|
},
|
|
}
|
|
|
|
if err := c.HandleInboundContext(c.ctx, chatID, content, nil, inboundCtx, sender); err != nil {
|
|
logger.WarnCF("buzz", "Inbound handling failed", map[string]any{
|
|
"error": err.Error(),
|
|
"event_id": evt.ID,
|
|
})
|
|
}
|
|
}
|
|
|
|
// firstTagValue returns the value of the first tag with the given key.
|
|
func firstTagValue(tags nostr.Tags, key string) string {
|
|
for _, t := range tags {
|
|
if len(t) >= 2 && t[0] == key {
|
|
return t[1]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// hasPTag reports whether any "p" tag references the given pubkey.
|
|
func hasPTag(tags nostr.Tags, pubkey string) bool {
|
|
for _, t := range tags {
|
|
if len(t) >= 2 && t[0] == "p" && strings.EqualFold(t[1], pubkey) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// shortPubkey renders a pubkey as a readable handle for display purposes.
|
|
func shortPubkey(pubkey string) string {
|
|
if len(pubkey) <= 12 {
|
|
return pubkey
|
|
}
|
|
return pubkey[:8] + "…" + pubkey[len(pubkey)-4:]
|
|
}
|