package channelbridge import ( "context" "fmt" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" ) // bubbleSender is the per-bubble outbound call the shared IM dispatcher uses. A // reply exits through the adapter's SendMessage (UnifiedResponse) path, a proactive // push through its SendProactive path — both collapse to "dispatch one chat bubble to // the platform", which is what lets the reply and the proactive push share a single // sanitize/split/gap pipeline instead of each re-implementing it. type bubbleSender func(ctx context.Context, msg adapter.UnifiedMessage) error // sendIMBubbles is the single IM outbound exit shared by sendReply (reply path) and // SendProactive (proactive push). It turns content into the plain chat bubbles an IM // channel should receive: role-play/action/narration markup (rendered as muted italic // on the Web) is stripped and each
paragraph is delivered as its own
// bubble/message (design/17 §2), with a multi-bubble burst spacing its sends 0.5-2s
// apart so it does not flood the channel. The Web frontend is never affected — it
// renders the persisted stream directly and does not route through here.
//
// target carries the platform/channel targeting fields and is copied onto every
// bubble (only Content/ContentType/Timestamp differ per bubble); sender is the
// adapter's outbound call. A content that strips to nothing (a pure action line) is
// dropped without a send, and a per-bubble failure is wrapped with its index.
func (b *Bridge) sendIMBubbles(ctx context.Context, target adapter.UnifiedMessage, sender bubbleSender, content string) error {
// splitBubbles applies stripNarration per bubble (so . It is the
// single sanitize entry point, shared with the old reply path.
bubbles := splitBubbles(content)
if len(bubbles) == 0 {
b.log.Debug("im outbound has no plain text, skipped", "platform", target.Platform, "channel", target.ChannelID)
return nil
}
for i, bubble := range bubbles {
if i > 0 {
time.Sleep(b.bubbleGapFn())
}
out := target
out.Content = bubble
out.ContentType = adapter.ContentText
out.Timestamp = time.Now().UTC()
// Each bubble is its own send so the gap lands between messages.
if err := sender(ctx, out); err != nil {
return fmt.Errorf("channelbridge: send bubble #%d of %d: %w", i+1, len(bubbles), err)
}
}
return nil
}