mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
refactor(runtime): drop non-session legacy context compatibility
This commit is contained in:
@@ -12,6 +12,12 @@ import (
|
||||
// ErrBusClosed is returned when publishing to a closed MessageBus.
|
||||
var ErrBusClosed = errors.New("message bus closed")
|
||||
|
||||
var (
|
||||
ErrMissingInboundContext = errors.New("inbound message context is required")
|
||||
ErrMissingOutboundContext = errors.New("outbound message context is required")
|
||||
ErrMissingOutboundMediaContext = errors.New("outbound media context is required")
|
||||
)
|
||||
|
||||
const defaultBusBufferSize = 64
|
||||
|
||||
// StreamDelegate is implemented by the channel Manager to provide streaming
|
||||
@@ -80,6 +86,9 @@ func publish[T any](ctx context.Context, mb *MessageBus, ch chan T, msg T) error
|
||||
}
|
||||
|
||||
func (mb *MessageBus) PublishInbound(ctx context.Context, msg InboundMessage) error {
|
||||
if msg.Context.isZero() {
|
||||
return ErrMissingInboundContext
|
||||
}
|
||||
msg = NormalizeInboundMessage(msg)
|
||||
return publish(ctx, mb, mb.inbound, msg)
|
||||
}
|
||||
@@ -89,6 +98,9 @@ func (mb *MessageBus) InboundChan() <-chan InboundMessage {
|
||||
}
|
||||
|
||||
func (mb *MessageBus) PublishOutbound(ctx context.Context, msg OutboundMessage) error {
|
||||
if msg.Context.isZero() {
|
||||
return ErrMissingOutboundContext
|
||||
}
|
||||
msg = NormalizeOutboundMessage(msg)
|
||||
return publish(ctx, mb, mb.outbound, msg)
|
||||
}
|
||||
@@ -98,6 +110,9 @@ func (mb *MessageBus) OutboundChan() <-chan OutboundMessage {
|
||||
}
|
||||
|
||||
func (mb *MessageBus) PublishOutboundMedia(ctx context.Context, msg OutboundMediaMessage) error {
|
||||
if msg.Context.isZero() {
|
||||
return ErrMissingOutboundMediaContext
|
||||
}
|
||||
msg = NormalizeOutboundMediaMessage(msg)
|
||||
return publish(ctx, mb, mb.outboundMedia, msg)
|
||||
}
|
||||
|
||||
+123
-51
@@ -14,10 +14,13 @@ func TestPublishConsume(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
msg := InboundMessage{
|
||||
Channel: "test",
|
||||
SenderID: "user1",
|
||||
ChatID: "chat1",
|
||||
Content: "hello",
|
||||
Context: InboundContext{
|
||||
Channel: "test",
|
||||
ChatID: "chat1",
|
||||
ChatType: "direct",
|
||||
SenderID: "user1",
|
||||
},
|
||||
Content: "hello",
|
||||
}
|
||||
|
||||
if err := mb.PublishInbound(ctx, msg); err != nil {
|
||||
@@ -45,25 +48,25 @@ func TestPublishConsume(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishInbound_NormalizesLegacyFieldsIntoContext(t *testing.T) {
|
||||
func TestPublishInbound_NormalizesContext(t *testing.T) {
|
||||
mb := NewMessageBus()
|
||||
defer mb.Close()
|
||||
|
||||
msg := InboundMessage{
|
||||
Channel: "slack",
|
||||
SenderID: "U123",
|
||||
ChatID: "C456/1712",
|
||||
Content: "hello",
|
||||
MessageID: "1712.01",
|
||||
Peer: Peer{Kind: "group", ID: "C456"},
|
||||
Metadata: map[string]string{
|
||||
"account_id": "workspace-a",
|
||||
"team_id": "T001",
|
||||
"reply_to_message_id": "1700.01",
|
||||
"is_mentioned": "true",
|
||||
"parent_peer_kind": "topic",
|
||||
"parent_peer_id": "1712",
|
||||
Context: InboundContext{
|
||||
Channel: "slack",
|
||||
Account: "workspace-a",
|
||||
ChatID: "C456/1712",
|
||||
ChatType: "group",
|
||||
TopicID: "1712",
|
||||
SpaceID: "T001",
|
||||
SpaceType: "team",
|
||||
SenderID: "U123",
|
||||
MessageID: "1712.01",
|
||||
ReplyToMessageID: "1700.01",
|
||||
Mentioned: true,
|
||||
},
|
||||
Content: "hello",
|
||||
}
|
||||
|
||||
if err := mb.PublishInbound(context.Background(), msg); err != nil {
|
||||
@@ -94,7 +97,7 @@ func TestPublishInbound_NormalizesLegacyFieldsIntoContext(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishInbound_MirrorsContextIntoLegacyFields(t *testing.T) {
|
||||
func TestPublishInbound_MirrorsContextIntoConvenienceFields(t *testing.T) {
|
||||
mb := NewMessageBus()
|
||||
defer mb.Close()
|
||||
|
||||
@@ -132,27 +135,8 @@ func TestPublishInbound_MirrorsContextIntoLegacyFields(t *testing.T) {
|
||||
if got.MessageID != "777" {
|
||||
t.Fatalf("expected legacy message ID 777, got %q", got.MessageID)
|
||||
}
|
||||
if got.Peer.Kind != "group" || got.Peer.ID != "-1001" {
|
||||
t.Fatalf("expected legacy peer group/-1001, got %q/%q", got.Peer.Kind, got.Peer.ID)
|
||||
}
|
||||
if got.Metadata["account_id"] != "bot-a" {
|
||||
t.Fatalf("expected mirrored account_id bot-a, got %q", got.Metadata["account_id"])
|
||||
}
|
||||
if got.Metadata["guild_id"] != "guild-9" {
|
||||
t.Fatalf("expected mirrored guild_id guild-9, got %q", got.Metadata["guild_id"])
|
||||
}
|
||||
if got.Metadata["parent_peer_kind"] != "topic" || got.Metadata["parent_peer_id"] != "42" {
|
||||
t.Fatalf(
|
||||
"expected mirrored topic parent peer, got %q/%q",
|
||||
got.Metadata["parent_peer_kind"],
|
||||
got.Metadata["parent_peer_id"],
|
||||
)
|
||||
}
|
||||
if got.Metadata["reply_to_message_id"] != "666" {
|
||||
t.Fatalf("expected mirrored reply_to_message_id 666, got %q", got.Metadata["reply_to_message_id"])
|
||||
}
|
||||
if got.Metadata["is_mentioned"] != "true" {
|
||||
t.Fatalf("expected mirrored is_mentioned true, got %q", got.Metadata["is_mentioned"])
|
||||
if got.Context.Account != "bot-a" || got.Context.SpaceID != "guild-9" || got.Context.TopicID != "42" {
|
||||
t.Fatalf("unexpected normalized context: %+v", got.Context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,8 +147,10 @@ func TestPublishOutboundSubscribe(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
msg := OutboundMessage{
|
||||
Channel: "telegram",
|
||||
ChatID: "123",
|
||||
Context: InboundContext{
|
||||
Channel: "telegram",
|
||||
ChatID: "123",
|
||||
},
|
||||
Content: "world",
|
||||
}
|
||||
|
||||
@@ -179,6 +165,9 @@ func TestPublishOutboundSubscribe(t *testing.T) {
|
||||
if got.Content != "world" {
|
||||
t.Fatalf("expected content 'world', got %q", got.Content)
|
||||
}
|
||||
if got.Context.Channel != "telegram" || got.Context.ChatID != "123" {
|
||||
t.Fatalf("expected normalized outbound context, got %+v", got.Context)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishOutbound_MirrorsContextToLegacyFields(t *testing.T) {
|
||||
@@ -241,6 +230,19 @@ func TestPublishOutboundMedia_MirrorsContextToLegacyFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewOutboundContext_NormalizesReplyAddress(t *testing.T) {
|
||||
ctx := NewOutboundContext(" telegram ", " chat-42 ", " msg-9 ")
|
||||
if ctx.Channel != "telegram" {
|
||||
t.Fatalf("expected channel telegram, got %q", ctx.Channel)
|
||||
}
|
||||
if ctx.ChatID != "chat-42" {
|
||||
t.Fatalf("expected chat_id chat-42, got %q", ctx.ChatID)
|
||||
}
|
||||
if ctx.ReplyToMessageID != "msg-9" {
|
||||
t.Fatalf("expected reply_to_message_id msg-9, got %q", ctx.ReplyToMessageID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishInbound_ContextCancel(t *testing.T) {
|
||||
mb := NewMessageBus()
|
||||
defer mb.Close()
|
||||
@@ -248,7 +250,15 @@ func TestPublishInbound_ContextCancel(t *testing.T) {
|
||||
// Fill the buffer
|
||||
ctx := context.Background()
|
||||
for i := range defaultBusBufferSize {
|
||||
if err := mb.PublishInbound(ctx, InboundMessage{Content: "fill"}); err != nil {
|
||||
if err := mb.PublishInbound(ctx, InboundMessage{
|
||||
Context: InboundContext{
|
||||
Channel: "test",
|
||||
ChatID: "chat-fill",
|
||||
ChatType: "direct",
|
||||
SenderID: "user-fill",
|
||||
},
|
||||
Content: "fill",
|
||||
}); err != nil {
|
||||
t.Fatalf("fill failed at %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
@@ -257,7 +267,15 @@ func TestPublishInbound_ContextCancel(t *testing.T) {
|
||||
cancelCtx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
err := mb.PublishInbound(cancelCtx, InboundMessage{Content: "overflow"})
|
||||
err := mb.PublishInbound(cancelCtx, InboundMessage{
|
||||
Context: InboundContext{
|
||||
Channel: "test",
|
||||
ChatID: "chat-overflow",
|
||||
ChatType: "direct",
|
||||
SenderID: "user-overflow",
|
||||
},
|
||||
Content: "overflow",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error from canceled context, got nil")
|
||||
}
|
||||
@@ -270,7 +288,15 @@ func TestPublishInbound_BusClosed(t *testing.T) {
|
||||
mb := NewMessageBus()
|
||||
mb.Close()
|
||||
|
||||
err := mb.PublishInbound(context.Background(), InboundMessage{Content: "test"})
|
||||
err := mb.PublishInbound(context.Background(), InboundMessage{
|
||||
Context: InboundContext{
|
||||
Channel: "test",
|
||||
ChatID: "chat1",
|
||||
ChatType: "direct",
|
||||
SenderID: "user1",
|
||||
},
|
||||
Content: "test",
|
||||
})
|
||||
if err != ErrBusClosed {
|
||||
t.Fatalf("expected ErrBusClosed, got %v", err)
|
||||
}
|
||||
@@ -280,7 +306,13 @@ func TestPublishOutbound_BusClosed(t *testing.T) {
|
||||
mb := NewMessageBus()
|
||||
mb.Close()
|
||||
|
||||
err := mb.PublishOutbound(context.Background(), OutboundMessage{Content: "test"})
|
||||
err := mb.PublishOutbound(context.Background(), OutboundMessage{
|
||||
Context: InboundContext{
|
||||
Channel: "test",
|
||||
ChatID: "chat1",
|
||||
},
|
||||
Content: "test",
|
||||
})
|
||||
if err != ErrBusClosed {
|
||||
t.Fatalf("expected ErrBusClosed, got %v", err)
|
||||
}
|
||||
@@ -292,14 +324,30 @@ func TestConsumeInbound_ContextCancel(t *testing.T) {
|
||||
defer mb.Close()
|
||||
|
||||
for i := range defaultBusBufferSize {
|
||||
if err := mb.PublishInbound(context.Background(), InboundMessage{Content: "fill"}); err != nil {
|
||||
if err := mb.PublishInbound(context.Background(), InboundMessage{
|
||||
Context: InboundContext{
|
||||
Channel: "test",
|
||||
ChatID: "chat-fill",
|
||||
ChatType: "direct",
|
||||
SenderID: "user-fill",
|
||||
},
|
||||
Content: "fill",
|
||||
}); err != nil {
|
||||
t.Fatalf("fill failed at %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
mb.PublishInbound(ctx, InboundMessage{Content: "ContextCancel"})
|
||||
mb.PublishInbound(ctx, InboundMessage{
|
||||
Context: InboundContext{
|
||||
Channel: "test",
|
||||
ChatID: "chat-cancel",
|
||||
ChatType: "direct",
|
||||
SenderID: "user-cancel",
|
||||
},
|
||||
Content: "ContextCancel",
|
||||
})
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -393,7 +441,15 @@ func TestPublishInbound_FullBuffer(t *testing.T) {
|
||||
|
||||
// Fill the buffer
|
||||
for i := range defaultBusBufferSize {
|
||||
if err := mb.PublishInbound(ctx, InboundMessage{Content: "fill"}); err != nil {
|
||||
if err := mb.PublishInbound(ctx, InboundMessage{
|
||||
Context: InboundContext{
|
||||
Channel: "test",
|
||||
ChatID: "chat-fill",
|
||||
ChatType: "direct",
|
||||
SenderID: "user-fill",
|
||||
},
|
||||
Content: "fill",
|
||||
}); err != nil {
|
||||
t.Fatalf("fill failed at %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
@@ -402,7 +458,15 @@ func TestPublishInbound_FullBuffer(t *testing.T) {
|
||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
err := mb.PublishInbound(timeoutCtx, InboundMessage{Content: "overflow"})
|
||||
err := mb.PublishInbound(timeoutCtx, InboundMessage{
|
||||
Context: InboundContext{
|
||||
Channel: "test",
|
||||
ChatID: "chat-overflow",
|
||||
ChatType: "direct",
|
||||
SenderID: "user-overflow",
|
||||
},
|
||||
Content: "overflow",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when buffer is full and context times out")
|
||||
}
|
||||
@@ -420,7 +484,15 @@ func TestCloseIdempotent(t *testing.T) {
|
||||
mb.Close()
|
||||
|
||||
// After close, publish should return ErrBusClosed
|
||||
err := mb.PublishInbound(context.Background(), InboundMessage{Content: "test"})
|
||||
err := mb.PublishInbound(context.Background(), InboundMessage{
|
||||
Context: InboundContext{
|
||||
Channel: "test",
|
||||
ChatID: "chat1",
|
||||
ChatType: "direct",
|
||||
SenderID: "user1",
|
||||
},
|
||||
Content: "test",
|
||||
})
|
||||
if err != ErrBusClosed {
|
||||
t.Fatalf("expected ErrBusClosed after multiple closes, got %v", err)
|
||||
}
|
||||
|
||||
+13
-203
@@ -2,92 +2,19 @@ package bus
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
metadataKeyAccountID = "account_id"
|
||||
metadataKeyGuildID = "guild_id"
|
||||
metadataKeyTeamID = "team_id"
|
||||
metadataKeyReplyToMessage = "reply_to_message_id"
|
||||
metadataKeyReplyToSender = "reply_to_sender_id"
|
||||
metadataKeyParentPeerKind = "parent_peer_kind"
|
||||
metadataKeyParentPeerID = "parent_peer_id"
|
||||
metadataKeyIsMentioned = "is_mentioned"
|
||||
)
|
||||
|
||||
// ContextFromLegacyInbound builds a normalized inbound context from the legacy
|
||||
// top-level fields on InboundMessage. This keeps older producers working while
|
||||
// new producers migrate to writing Context directly.
|
||||
func ContextFromLegacyInbound(msg InboundMessage) InboundContext {
|
||||
ctx := InboundContext{
|
||||
Channel: strings.TrimSpace(msg.Channel),
|
||||
ChatID: strings.TrimSpace(msg.ChatID),
|
||||
ChatType: normalizeKind(msg.Peer.Kind),
|
||||
SenderID: firstNonEmpty(
|
||||
strings.TrimSpace(msg.SenderID),
|
||||
strings.TrimSpace(msg.Sender.CanonicalID),
|
||||
strings.TrimSpace(msg.Sender.PlatformID),
|
||||
),
|
||||
MessageID: strings.TrimSpace(msg.MessageID),
|
||||
Raw: cloneStringMap(msg.Metadata),
|
||||
}
|
||||
|
||||
if account := metadataValue(msg.Metadata, metadataKeyAccountID); account != "" {
|
||||
ctx.Account = account
|
||||
}
|
||||
if replyToMsgID := metadataValue(msg.Metadata, metadataKeyReplyToMessage); replyToMsgID != "" {
|
||||
ctx.ReplyToMessageID = replyToMsgID
|
||||
}
|
||||
if replyToSenderID := metadataValue(msg.Metadata, metadataKeyReplyToSender); replyToSenderID != "" {
|
||||
ctx.ReplyToSenderID = replyToSenderID
|
||||
}
|
||||
if isTruthy(metadataValue(msg.Metadata, metadataKeyIsMentioned)) {
|
||||
ctx.Mentioned = true
|
||||
}
|
||||
|
||||
parentKind := normalizeKind(metadataValue(msg.Metadata, metadataKeyParentPeerKind))
|
||||
parentID := metadataValue(msg.Metadata, metadataKeyParentPeerID)
|
||||
if parentKind == "topic" && parentID != "" {
|
||||
ctx.TopicID = parentID
|
||||
}
|
||||
|
||||
switch {
|
||||
case metadataValue(msg.Metadata, metadataKeyGuildID) != "":
|
||||
ctx.SpaceType = "guild"
|
||||
ctx.SpaceID = metadataValue(msg.Metadata, metadataKeyGuildID)
|
||||
case metadataValue(msg.Metadata, metadataKeyTeamID) != "":
|
||||
ctx.SpaceType = "team"
|
||||
ctx.SpaceID = metadataValue(msg.Metadata, metadataKeyTeamID)
|
||||
}
|
||||
|
||||
return normalizeInboundContext(ctx)
|
||||
}
|
||||
|
||||
// NormalizeInboundMessage ensures the normalized Context is present and mirrors
|
||||
// missing legacy fields from it so older consumers continue to work during the
|
||||
// migration period.
|
||||
// NormalizeInboundMessage ensures the inbound context is normalized and keeps
|
||||
// convenience mirrors in sync for runtime consumers.
|
||||
func NormalizeInboundMessage(msg InboundMessage) InboundMessage {
|
||||
if msg.Context.isZero() {
|
||||
msg.Context = ContextFromLegacyInbound(msg)
|
||||
} else {
|
||||
msg.Context = normalizeInboundContext(msg.Context)
|
||||
}
|
||||
|
||||
if msg.Channel == "" {
|
||||
msg.Channel = msg.Context.Channel
|
||||
}
|
||||
if msg.SenderID == "" {
|
||||
msg.SenderID = msg.Context.SenderID
|
||||
}
|
||||
if msg.ChatID == "" {
|
||||
msg.ChatID = msg.Context.ChatID
|
||||
}
|
||||
msg.Context = normalizeInboundContext(msg.Context)
|
||||
msg.Channel = msg.Context.Channel
|
||||
msg.SenderID = msg.Context.SenderID
|
||||
msg.ChatID = msg.Context.ChatID
|
||||
if msg.MessageID == "" {
|
||||
msg.MessageID = msg.Context.MessageID
|
||||
}
|
||||
if msg.Peer.Kind == "" {
|
||||
msg.Peer = peerFromContext(msg.Context)
|
||||
if msg.Context.MessageID == "" {
|
||||
msg.Context.MessageID = msg.MessageID
|
||||
}
|
||||
|
||||
msg.Metadata = mergeLegacyMetadata(msg.Metadata, msg.Context)
|
||||
return msg
|
||||
}
|
||||
|
||||
@@ -125,110 +52,6 @@ func normalizeInboundContext(ctx InboundContext) InboundContext {
|
||||
return ctx
|
||||
}
|
||||
|
||||
func peerFromContext(ctx InboundContext) Peer {
|
||||
kind := normalizeKind(ctx.ChatType)
|
||||
if kind == "" {
|
||||
return Peer{}
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case "direct":
|
||||
return Peer{
|
||||
Kind: "direct",
|
||||
ID: firstNonEmpty(strings.TrimSpace(ctx.SenderID), strings.TrimSpace(ctx.ChatID)),
|
||||
}
|
||||
case "group", "channel":
|
||||
return Peer{
|
||||
Kind: kind,
|
||||
ID: strings.TrimSpace(ctx.ChatID),
|
||||
}
|
||||
default:
|
||||
return Peer{
|
||||
Kind: kind,
|
||||
ID: strings.TrimSpace(ctx.ChatID),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mergeLegacyMetadata(existing map[string]string, ctx InboundContext) map[string]string {
|
||||
merged := cloneStringMap(existing)
|
||||
if len(merged) == 0 {
|
||||
merged = cloneStringMap(ctx.Raw)
|
||||
} else {
|
||||
for k, v := range ctx.Raw {
|
||||
if _, ok := merged[k]; !ok {
|
||||
merged[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Account != "" {
|
||||
if merged == nil {
|
||||
merged = make(map[string]string)
|
||||
}
|
||||
setMissing(merged, metadataKeyAccountID, ctx.Account)
|
||||
}
|
||||
if ctx.ReplyToMessageID != "" {
|
||||
if merged == nil {
|
||||
merged = make(map[string]string)
|
||||
}
|
||||
setMissing(merged, metadataKeyReplyToMessage, ctx.ReplyToMessageID)
|
||||
}
|
||||
if ctx.ReplyToSenderID != "" {
|
||||
if merged == nil {
|
||||
merged = make(map[string]string)
|
||||
}
|
||||
setMissing(merged, metadataKeyReplyToSender, ctx.ReplyToSenderID)
|
||||
}
|
||||
if ctx.Mentioned {
|
||||
if merged == nil {
|
||||
merged = make(map[string]string)
|
||||
}
|
||||
setMissing(merged, metadataKeyIsMentioned, "true")
|
||||
}
|
||||
if ctx.TopicID != "" {
|
||||
if merged == nil {
|
||||
merged = make(map[string]string)
|
||||
}
|
||||
setMissing(merged, metadataKeyParentPeerKind, "topic")
|
||||
setMissing(merged, metadataKeyParentPeerID, ctx.TopicID)
|
||||
}
|
||||
|
||||
switch normalizeKind(ctx.SpaceType) {
|
||||
case "guild":
|
||||
if merged == nil {
|
||||
merged = make(map[string]string)
|
||||
}
|
||||
setMissing(merged, metadataKeyGuildID, ctx.SpaceID)
|
||||
case "team", "workspace":
|
||||
if merged == nil {
|
||||
merged = make(map[string]string)
|
||||
}
|
||||
setMissing(merged, metadataKeyTeamID, ctx.SpaceID)
|
||||
}
|
||||
|
||||
if len(merged) == 0 {
|
||||
return nil
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func setMissing(dst map[string]string, key, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := dst[key]; !ok {
|
||||
dst[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func metadataValue(metadata map[string]string, key string) string {
|
||||
if metadata == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(metadata[key])
|
||||
}
|
||||
|
||||
func cloneStringMap(src map[string]string) map[string]string {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
@@ -241,24 +64,11 @@ func cloneStringMap(src map[string]string) map[string]string {
|
||||
return dst
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeKind(value string) string {
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func isTruthy(value string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "1", "t", "true", "y", "yes", "on":
|
||||
return true
|
||||
func normalizeKind(kind string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(kind)) {
|
||||
case "direct", "group", "channel", "guild", "team", "workspace", "tenant", "topic":
|
||||
return strings.ToLower(strings.TrimSpace(kind))
|
||||
default:
|
||||
return false
|
||||
return strings.ToLower(strings.TrimSpace(kind))
|
||||
}
|
||||
}
|
||||
|
||||
+18
-46
@@ -2,62 +2,34 @@ package bus
|
||||
|
||||
import "strings"
|
||||
|
||||
// ContextFromLegacyOutbound builds a minimal outbound context from the legacy
|
||||
// top-level outbound fields. This keeps older outbound publishers working
|
||||
// while new publishers gradually start carrying the original InboundContext.
|
||||
func ContextFromLegacyOutbound(msg OutboundMessage) InboundContext {
|
||||
// NewOutboundContext builds the minimal normalized addressing context required
|
||||
// to deliver an outbound text message or reply.
|
||||
func NewOutboundContext(channel, chatID, replyToMessageID string) InboundContext {
|
||||
return normalizeInboundContext(InboundContext{
|
||||
Channel: strings.TrimSpace(msg.Channel),
|
||||
ChatID: strings.TrimSpace(msg.ChatID),
|
||||
ReplyToMessageID: strings.TrimSpace(msg.ReplyToMessageID),
|
||||
Channel: strings.TrimSpace(channel),
|
||||
ChatID: strings.TrimSpace(chatID),
|
||||
ReplyToMessageID: strings.TrimSpace(replyToMessageID),
|
||||
})
|
||||
}
|
||||
|
||||
// ContextFromLegacyOutboundMedia builds a minimal outbound context for media.
|
||||
func ContextFromLegacyOutboundMedia(msg OutboundMediaMessage) InboundContext {
|
||||
return normalizeInboundContext(InboundContext{
|
||||
Channel: strings.TrimSpace(msg.Channel),
|
||||
ChatID: strings.TrimSpace(msg.ChatID),
|
||||
})
|
||||
}
|
||||
|
||||
// NormalizeOutboundMessage ensures Context is present and mirrors legacy
|
||||
// top-level addressing fields from it so older senders keep working.
|
||||
// NormalizeOutboundMessage ensures Context is normalized and keeps convenience
|
||||
// mirrors in sync for runtime consumers.
|
||||
func NormalizeOutboundMessage(msg OutboundMessage) OutboundMessage {
|
||||
if msg.Context.isZero() {
|
||||
msg.Context = ContextFromLegacyOutbound(msg)
|
||||
} else {
|
||||
msg.Context = normalizeInboundContext(msg.Context)
|
||||
msg.Context = normalizeInboundContext(msg.Context)
|
||||
msg.Channel = msg.Context.Channel
|
||||
msg.ChatID = msg.Context.ChatID
|
||||
if msg.Context.ReplyToMessageID == "" {
|
||||
msg.Context.ReplyToMessageID = strings.TrimSpace(msg.ReplyToMessageID)
|
||||
}
|
||||
|
||||
if msg.Channel == "" {
|
||||
msg.Channel = msg.Context.Channel
|
||||
}
|
||||
if msg.ChatID == "" {
|
||||
msg.ChatID = msg.Context.ChatID
|
||||
}
|
||||
if msg.ReplyToMessageID == "" {
|
||||
msg.ReplyToMessageID = msg.Context.ReplyToMessageID
|
||||
}
|
||||
|
||||
msg.ReplyToMessageID = msg.Context.ReplyToMessageID
|
||||
return msg
|
||||
}
|
||||
|
||||
// NormalizeOutboundMediaMessage ensures media outbound messages also carry a
|
||||
// normalized context while preserving the legacy top-level routing fields.
|
||||
// normalized context while keeping convenience mirrors in sync.
|
||||
func NormalizeOutboundMediaMessage(msg OutboundMediaMessage) OutboundMediaMessage {
|
||||
if msg.Context.isZero() {
|
||||
msg.Context = ContextFromLegacyOutboundMedia(msg)
|
||||
} else {
|
||||
msg.Context = normalizeInboundContext(msg.Context)
|
||||
}
|
||||
|
||||
if msg.Channel == "" {
|
||||
msg.Channel = msg.Context.Channel
|
||||
}
|
||||
if msg.ChatID == "" {
|
||||
msg.ChatID = msg.Context.ChatID
|
||||
}
|
||||
|
||||
msg.Context = normalizeInboundContext(msg.Context)
|
||||
msg.Channel = msg.Context.Channel
|
||||
msg.ChatID = msg.Context.ChatID
|
||||
return msg
|
||||
}
|
||||
|
||||
+14
-21
@@ -1,11 +1,5 @@
|
||||
package bus
|
||||
|
||||
// Peer identifies the routing peer for a message (direct, group, channel, etc.)
|
||||
type Peer struct {
|
||||
Kind string `json:"kind"` // "direct" | "group" | "channel" | ""
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// SenderInfo provides structured sender identity information.
|
||||
type SenderInfo struct {
|
||||
Platform string `json:"platform,omitempty"` // "telegram", "discord", "slack", ...
|
||||
@@ -16,9 +10,8 @@ type SenderInfo struct {
|
||||
}
|
||||
|
||||
// InboundContext captures the normalized, platform-agnostic facts about an
|
||||
// inbound message. This is the long-term source of truth for routing and
|
||||
// session allocation. Legacy top-level fields on InboundMessage remain during
|
||||
// the transition and are derived from this context when missing.
|
||||
// inbound message. This is the source of truth for routing and session
|
||||
// allocation.
|
||||
type InboundContext struct {
|
||||
Channel string `json:"channel"`
|
||||
Account string `json:"account,omitempty"`
|
||||
@@ -43,18 +36,18 @@ type InboundContext struct {
|
||||
}
|
||||
|
||||
type InboundMessage struct {
|
||||
Channel string `json:"channel"`
|
||||
SenderID string `json:"sender_id"`
|
||||
Sender SenderInfo `json:"sender"`
|
||||
ChatID string `json:"chat_id"`
|
||||
Context InboundContext `json:"context"`
|
||||
Content string `json:"content"`
|
||||
Media []string `json:"media,omitempty"`
|
||||
Peer Peer `json:"peer"` // routing peer
|
||||
MessageID string `json:"message_id,omitempty"` // platform message ID
|
||||
MediaScope string `json:"media_scope,omitempty"` // media lifecycle scope
|
||||
SessionKey string `json:"session_key"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
Context InboundContext `json:"context"`
|
||||
Sender SenderInfo `json:"sender"`
|
||||
Content string `json:"content"`
|
||||
Media []string `json:"media,omitempty"`
|
||||
MediaScope string `json:"media_scope,omitempty"` // media lifecycle scope
|
||||
SessionKey string `json:"session_key"`
|
||||
|
||||
// Convenience mirrors derived from Context for runtime consumers.
|
||||
Channel string `json:"channel"`
|
||||
SenderID string `json:"sender_id"`
|
||||
ChatID string `json:"chat_id"`
|
||||
MessageID string `json:"message_id,omitempty"` // platform message ID
|
||||
}
|
||||
|
||||
type OutboundMessage struct {
|
||||
|
||||
Reference in New Issue
Block a user