refactor(channels): Send returns ([]string, error), remove MessageIDsSender
Replace the optional MessageIDsSender interface with a direct return value on Send. Channels that support delivery IDs (Telegram, Discord, Slack, QQ) return them from Send; all others return nil. Manager.sendWithRetry drops the type-assertion branch and calls Send uniformly.
This commit is contained in:
parent
7f91fa90f2
commit
4d5d1d0509
27 changed files with 170 additions and 196 deletions
|
|
@ -28,7 +28,9 @@ type fakeChannel struct{ id string }
|
||||||
func (f *fakeChannel) Name() string { return "fake" }
|
func (f *fakeChannel) Name() string { return "fake" }
|
||||||
func (f *fakeChannel) Start(ctx context.Context) error { return nil }
|
func (f *fakeChannel) Start(ctx context.Context) error { return nil }
|
||||||
func (f *fakeChannel) Stop(ctx context.Context) error { return nil }
|
func (f *fakeChannel) Stop(ctx context.Context) error { return nil }
|
||||||
func (f *fakeChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { return nil }
|
func (f *fakeChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
func (f *fakeChannel) IsRunning() bool { return true }
|
func (f *fakeChannel) IsRunning() bool { return true }
|
||||||
func (f *fakeChannel) IsAllowed(string) bool { return true }
|
func (f *fakeChannel) IsAllowed(string) bool { return true }
|
||||||
func (f *fakeChannel) IsAllowedSender(sender bus.SenderInfo) bool { return true }
|
func (f *fakeChannel) IsAllowedSender(sender bus.SenderInfo) bool { return true }
|
||||||
|
|
|
||||||
|
|
@ -253,27 +253,27 @@ func (c *TelegramChannel) Stop(ctx context.Context) error {
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Old code: returns plain error
|
// Old code: returns plain error
|
||||||
func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.running { return fmt.Errorf("not running") }
|
if !c.running { return nil, fmt.Errorf("not running") }
|
||||||
// ...
|
// ...
|
||||||
if err != nil { return err }
|
if err != nil { return nil, err }
|
||||||
}
|
}
|
||||||
|
|
||||||
// New code: must return sentinel errors for Manager to determine retry strategy
|
// New code: must return sentinel errors for Manager to determine retry strategy
|
||||||
func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning // ← Manager will not retry
|
return nil, channels.ErrNotRunning // ← Manager will not retry
|
||||||
}
|
}
|
||||||
// ...
|
// ...
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Use ClassifySendError to wrap error based on HTTP status code
|
// Use ClassifySendError to wrap error based on HTTP status code
|
||||||
return channels.ClassifySendError(statusCode, err)
|
return nil, channels.ClassifySendError(statusCode, err)
|
||||||
// Or manually wrap:
|
// Or manually wrap:
|
||||||
// return fmt.Errorf("%w: %v", channels.ErrTemporary, err)
|
// return nil, fmt.Errorf("%w: %v", channels.ErrTemporary, err)
|
||||||
// return fmt.Errorf("%w: %v", channels.ErrRateLimit, err)
|
// return nil, fmt.Errorf("%w: %v", channels.ErrRateLimit, err)
|
||||||
// return fmt.Errorf("%w: %v", channels.ErrSendFailed, err)
|
// return nil, fmt.Errorf("%w: %v", channels.ErrSendFailed, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -301,6 +301,8 @@ sender := bus.SenderInfo{
|
||||||
CanonicalID: identity.BuildCanonicalID("telegram", strconv.FormatInt(from.ID, 10)),
|
CanonicalID: identity.BuildCanonicalID("telegram", strconv.FormatInt(from.ID, 10)),
|
||||||
Username: from.Username,
|
Username: from.Username,
|
||||||
DisplayName: from.FirstName,
|
DisplayName: from.FirstName,
|
||||||
|
FirstName: from.FirstName,
|
||||||
|
LastName: from.LastName,
|
||||||
}
|
}
|
||||||
|
|
||||||
peer := bus.Peer{
|
peer := bus.Peer{
|
||||||
|
|
@ -502,10 +504,10 @@ func (c *MatrixChannel) Stop(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MatrixChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *MatrixChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
// 1. Check running state
|
// 1. Check running state
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Send message to Matrix
|
// 2. Send message to Matrix
|
||||||
|
|
@ -513,14 +515,14 @@ func (c *MatrixChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 3. Must use error classification wrapping
|
// 3. Must use error classification wrapping
|
||||||
// If you have an HTTP status code:
|
// If you have an HTTP status code:
|
||||||
// return channels.ClassifySendError(statusCode, err)
|
// return nil, channels.ClassifySendError(statusCode, err)
|
||||||
// If it's a network error:
|
// If it's a network error:
|
||||||
// return channels.ClassifyNetError(err)
|
// return nil, channels.ClassifyNetError(err)
|
||||||
// If manual classification is needed:
|
// If manual classification is needed:
|
||||||
return fmt.Errorf("%w: %v", channels.ErrTemporary, err)
|
return nil, fmt.Errorf("%w: %v", channels.ErrTemporary, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Incoming Message Handling ==========
|
// ========== Incoming Message Handling ==========
|
||||||
|
|
@ -868,7 +870,9 @@ type SenderInfo struct {
|
||||||
PlatformID string `json:"platform_id,omitempty"` // Platform-native ID
|
PlatformID string `json:"platform_id,omitempty"` // Platform-native ID
|
||||||
CanonicalID string `json:"canonical_id,omitempty"` // "platform:id" canonical format
|
CanonicalID string `json:"canonical_id,omitempty"` // "platform:id" canonical format
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
DisplayName string `json:"display_name,omitempty"`
|
DisplayName string `json:"display_name,omitempty"` // fallback display name when first/last not available
|
||||||
|
FirstName string `json:"first_name,omitempty"` // given name (preferred over DisplayName when set)
|
||||||
|
LastName string `json:"last_name,omitempty"` // family name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inbound message
|
// Inbound message
|
||||||
|
|
@ -889,9 +893,11 @@ type InboundMessage struct {
|
||||||
|
|
||||||
// Outbound text message
|
// Outbound text message
|
||||||
type OutboundMessage struct {
|
type OutboundMessage struct {
|
||||||
Channel string
|
Channel string // Target channel name
|
||||||
ChatID string
|
ChatID string // Target chat/room ID
|
||||||
Content string
|
Content string // Message text
|
||||||
|
ReplyToMessageID string // Optional: reply to this platform message ID
|
||||||
|
OnDelivered func(msgIDs []string) // Optional: called with delivered platform message IDs (not serialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Outbound media message
|
// Outbound media message
|
||||||
|
|
@ -1272,7 +1278,7 @@ type Channel interface {
|
||||||
Name() string
|
Name() string
|
||||||
Start(ctx context.Context) error
|
Start(ctx context.Context) error
|
||||||
Stop(ctx context.Context) error
|
Stop(ctx context.Context) error
|
||||||
Send(ctx context.Context, msg bus.OutboundMessage) error
|
Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error)
|
||||||
IsRunning() bool
|
IsRunning() bool
|
||||||
IsAllowed(senderID string) bool
|
IsAllowed(senderID string) bool
|
||||||
IsAllowedSender(sender bus.SenderInfo) bool
|
IsAllowedSender(sender bus.SenderInfo) bool
|
||||||
|
|
|
||||||
|
|
@ -253,27 +253,27 @@ func (c *TelegramChannel) Stop(ctx context.Context) error {
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// 旧代码:返回普通 error
|
// 旧代码:返回普通 error
|
||||||
func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.running { return fmt.Errorf("not running") }
|
if !c.running { return nil, fmt.Errorf("not running") }
|
||||||
// ...
|
// ...
|
||||||
if err != nil { return err }
|
if err != nil { return nil, err }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新代码:必须返回哨兵错误,供 Manager 判断重试策略
|
// 新代码:必须返回哨兵错误,供 Manager 判断重试策略
|
||||||
func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning // ← Manager 不会重试
|
return nil, channels.ErrNotRunning // ← Manager 不会重试
|
||||||
}
|
}
|
||||||
// ...
|
// ...
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 使用 ClassifySendError 根据 HTTP 状态码包装错误
|
// 使用 ClassifySendError 根据 HTTP 状态码包装错误
|
||||||
return channels.ClassifySendError(statusCode, err)
|
return nil, channels.ClassifySendError(statusCode, err)
|
||||||
// 或手动包装:
|
// 或手动包装:
|
||||||
// return fmt.Errorf("%w: %v", channels.ErrTemporary, err)
|
// return nil, fmt.Errorf("%w: %v", channels.ErrTemporary, err)
|
||||||
// return fmt.Errorf("%w: %v", channels.ErrRateLimit, err)
|
// return nil, fmt.Errorf("%w: %v", channels.ErrRateLimit, err)
|
||||||
// return fmt.Errorf("%w: %v", channels.ErrSendFailed, err)
|
// return nil, fmt.Errorf("%w: %v", channels.ErrSendFailed, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -301,6 +301,8 @@ sender := bus.SenderInfo{
|
||||||
CanonicalID: identity.BuildCanonicalID("telegram", strconv.FormatInt(from.ID, 10)),
|
CanonicalID: identity.BuildCanonicalID("telegram", strconv.FormatInt(from.ID, 10)),
|
||||||
Username: from.Username,
|
Username: from.Username,
|
||||||
DisplayName: from.FirstName,
|
DisplayName: from.FirstName,
|
||||||
|
FirstName: from.FirstName,
|
||||||
|
LastName: from.LastName,
|
||||||
}
|
}
|
||||||
|
|
||||||
peer := bus.Peer{
|
peer := bus.Peer{
|
||||||
|
|
@ -502,10 +504,10 @@ func (c *MatrixChannel) Stop(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MatrixChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *MatrixChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
// 1. 检查运行状态
|
// 1. 检查运行状态
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 发送消息到 Matrix
|
// 2. 发送消息到 Matrix
|
||||||
|
|
@ -513,14 +515,14 @@ func (c *MatrixChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 3. 必须使用错误分类包装
|
// 3. 必须使用错误分类包装
|
||||||
// 如果你有 HTTP 状态码:
|
// 如果你有 HTTP 状态码:
|
||||||
// return channels.ClassifySendError(statusCode, err)
|
// return nil, channels.ClassifySendError(statusCode, err)
|
||||||
// 如果是网络错误:
|
// 如果是网络错误:
|
||||||
// return channels.ClassifyNetError(err)
|
// return nil, channels.ClassifyNetError(err)
|
||||||
// 如果需要手动分类:
|
// 如果需要手动分类:
|
||||||
return fmt.Errorf("%w: %v", channels.ErrTemporary, err)
|
return nil, fmt.Errorf("%w: %v", channels.ErrTemporary, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 消息接收处理 ==========
|
// ========== 消息接收处理 ==========
|
||||||
|
|
@ -867,7 +869,9 @@ type SenderInfo struct {
|
||||||
PlatformID string `json:"platform_id,omitempty"` // 平台原始 ID
|
PlatformID string `json:"platform_id,omitempty"` // 平台原始 ID
|
||||||
CanonicalID string `json:"canonical_id,omitempty"` // "platform:id" 规范格式
|
CanonicalID string `json:"canonical_id,omitempty"` // "platform:id" 规范格式
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
DisplayName string `json:"display_name,omitempty"`
|
DisplayName string `json:"display_name,omitempty"` // 无 first/last 时的回退显示名
|
||||||
|
FirstName string `json:"first_name,omitempty"` // 名(优先于 DisplayName)
|
||||||
|
LastName string `json:"last_name,omitempty"` // 姓
|
||||||
}
|
}
|
||||||
|
|
||||||
// 入站消息
|
// 入站消息
|
||||||
|
|
@ -888,9 +892,11 @@ type InboundMessage struct {
|
||||||
|
|
||||||
// 出站文本消息
|
// 出站文本消息
|
||||||
type OutboundMessage struct {
|
type OutboundMessage struct {
|
||||||
Channel string
|
Channel string // 目标 channel 名称
|
||||||
ChatID string
|
ChatID string // 目标聊天/房间 ID
|
||||||
Content string
|
Content string // 消息文本
|
||||||
|
ReplyToMessageID string // 可选:回复的平台消息 ID
|
||||||
|
OnDelivered func(msgIDs []string) // 可选:投递成功后携带平台消息 ID 回调(不序列化)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 出站媒体消息
|
// 出站媒体消息
|
||||||
|
|
@ -1271,7 +1277,7 @@ type Channel interface {
|
||||||
Name() string
|
Name() string
|
||||||
Start(ctx context.Context) error
|
Start(ctx context.Context) error
|
||||||
Stop(ctx context.Context) error
|
Stop(ctx context.Context) error
|
||||||
Send(ctx context.Context, msg bus.OutboundMessage) error
|
Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error)
|
||||||
IsRunning() bool
|
IsRunning() bool
|
||||||
IsAllowed(senderID string) bool
|
IsAllowed(senderID string) bool
|
||||||
IsAllowedSender(sender bus.SenderInfo) bool
|
IsAllowedSender(sender bus.SenderInfo) bool
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ type Channel interface {
|
||||||
Name() string
|
Name() string
|
||||||
Start(ctx context.Context) error
|
Start(ctx context.Context) error
|
||||||
Stop(ctx context.Context) error
|
Stop(ctx context.Context) error
|
||||||
Send(ctx context.Context, msg bus.OutboundMessage) error
|
Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error)
|
||||||
IsRunning() bool
|
IsRunning() bool
|
||||||
IsAllowed(senderID string) bool
|
IsAllowed(senderID string) bool
|
||||||
IsAllowedSender(sender bus.SenderInfo) bool
|
IsAllowedSender(sender bus.SenderInfo) bool
|
||||||
|
|
|
||||||
|
|
@ -103,20 +103,20 @@ func (c *DingTalkChannel) Stop(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send sends a message to DingTalk via the chatbot reply API
|
// Send sends a message to DingTalk via the chatbot reply API
|
||||||
func (c *DingTalkChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *DingTalkChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get session webhook from storage
|
// Get session webhook from storage
|
||||||
sessionWebhookRaw, ok := c.sessionWebhooks.Load(msg.ChatID)
|
sessionWebhookRaw, ok := c.sessionWebhooks.Load(msg.ChatID)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("no session_webhook found for chat %s, cannot send message", msg.ChatID)
|
return nil, fmt.Errorf("no session_webhook found for chat %s, cannot send message", msg.ChatID)
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionWebhook, ok := sessionWebhookRaw.(string)
|
sessionWebhook, ok := sessionWebhookRaw.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("invalid session_webhook type for chat %s", msg.ChatID)
|
return nil, fmt.Errorf("invalid session_webhook type for chat %s", msg.ChatID)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("dingtalk", "Sending message", map[string]any{
|
logger.DebugCF("dingtalk", "Sending message", map[string]any{
|
||||||
|
|
@ -125,7 +125,7 @@ func (c *DingTalkChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
|
||||||
})
|
})
|
||||||
|
|
||||||
// Use the session webhook to send the reply
|
// Use the session webhook to send the reply
|
||||||
return c.SendDirectReply(ctx, sessionWebhook, msg.Content)
|
return nil, c.SendDirectReply(ctx, sessionWebhook, msg.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// onChatBotMessageReceived implements the IChatBotMessageHandler function signature
|
// onChatBotMessageReceived implements the IChatBotMessageHandler function signature
|
||||||
|
|
|
||||||
|
|
@ -128,13 +128,7 @@ func (c *DiscordChannel) Stop(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DiscordChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *DiscordChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
_, err := c.SendMessageWithIDs(ctx, msg)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMessageWithIDs implements channels.MessageIDsSender.
|
|
||||||
func (c *DiscordChannel) SendMessageWithIDs(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return nil, channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,8 @@ func (c *FeishuChannel) Stop(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send is a stub method to satisfy the Channel interface
|
// Send is a stub method to satisfy the Channel interface
|
||||||
func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
return errUnsupported
|
return nil, errUnsupported
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditMessage is a stub method to satisfy MessageEditor
|
// EditMessage is a stub method to satisfy MessageEditor
|
||||||
|
|
|
||||||
|
|
@ -131,26 +131,26 @@ func (c *FeishuChannel) Stop(ctx context.Context) error {
|
||||||
|
|
||||||
// Send sends a message using Interactive Card format for markdown rendering.
|
// Send sends a message using Interactive Card format for markdown rendering.
|
||||||
// Falls back to plain text message if card sending fails (e.g., table limit exceeded).
|
// Falls back to plain text message if card sending fails (e.g., table limit exceeded).
|
||||||
func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg.ChatID == "" {
|
if msg.ChatID == "" {
|
||||||
return fmt.Errorf("chat ID is empty: %w", channels.ErrSendFailed)
|
return nil, fmt.Errorf("chat ID is empty: %w", channels.ErrSendFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build interactive card with markdown content
|
// Build interactive card with markdown content
|
||||||
cardContent, err := buildMarkdownCard(msg.Content)
|
cardContent, err := buildMarkdownCard(msg.Content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If card build fails, fall back to plain text
|
// If card build fails, fall back to plain text
|
||||||
return c.sendText(ctx, msg.ChatID, msg.Content)
|
return nil, c.sendText(ctx, msg.ChatID, msg.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// First attempt: try sending as interactive card
|
// First attempt: try sending as interactive card
|
||||||
err = c.sendCard(ctx, msg.ChatID, cardContent)
|
err = c.sendCard(ctx, msg.ChatID, cardContent)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if error is due to card table limit (error code 11310)
|
// Check if error is due to card table limit (error code 11310)
|
||||||
|
|
@ -167,14 +167,14 @@ func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
// Second attempt: fall back to plain text message
|
// Second attempt: fall back to plain text message
|
||||||
textErr := c.sendText(ctx, msg.ChatID, msg.Content)
|
textErr := c.sendText(ctx, msg.ChatID, msg.Content)
|
||||||
if textErr == nil {
|
if textErr == nil {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
// If text also fails, return the text error
|
// If text also fails, return the text error
|
||||||
return textErr
|
return nil, textErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other errors, return the original card error
|
// For other errors, return the original card error
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditMessage implements channels.MessageEditor.
|
// EditMessage implements channels.MessageEditor.
|
||||||
|
|
|
||||||
|
|
@ -62,12 +62,6 @@ type PlaceholderRecorder interface {
|
||||||
RecordReactionUndo(channel, chatID string, undo func())
|
RecordReactionUndo(channel, chatID string, undo func())
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageIDsSender is implemented by channels that can return the platform
|
|
||||||
// message IDs for a delivered outbound text message.
|
|
||||||
type MessageIDsSender interface {
|
|
||||||
SendMessageWithIDs(ctx context.Context, msg bus.OutboundMessage) (messageIDs []string, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommandRegistrarCapable is implemented by channels that can register
|
// CommandRegistrarCapable is implemented by channels that can register
|
||||||
// command menus with their upstream platform (e.g. Telegram BotCommand).
|
// command menus with their upstream platform (e.g. Telegram BotCommand).
|
||||||
// Channels that do not support platform-level command menus can ignore it.
|
// Channels that do not support platform-level command menus can ignore it.
|
||||||
|
|
|
||||||
|
|
@ -130,18 +130,18 @@ func (c *IRCChannel) Stop(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send sends a message to an IRC channel or user.
|
// Send sends a message to an IRC channel or user.
|
||||||
func (c *IRCChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *IRCChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
target := msg.ChatID
|
target := msg.ChatID
|
||||||
if target == "" {
|
if target == "" {
|
||||||
return fmt.Errorf("chat ID is empty: %w", channels.ErrSendFailed)
|
return nil, fmt.Errorf("chat ID is empty: %w", channels.ErrSendFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(msg.Content) == "" {
|
if strings.TrimSpace(msg.Content) == "" {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send each line separately (IRC is line-oriented)
|
// Send each line separately (IRC is line-oriented)
|
||||||
|
|
@ -158,7 +158,7 @@ func (c *IRCChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
||||||
"target": target,
|
"target": target,
|
||||||
"lines": len(lines),
|
"lines": len(lines),
|
||||||
})
|
})
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartTyping implements channels.TypingCapable using IRCv3 +typing client tag.
|
// StartTyping implements channels.TypingCapable using IRCv3 +typing client tag.
|
||||||
|
|
|
||||||
|
|
@ -496,9 +496,9 @@ func (c *LINEChannel) resolveChatID(source lineSource) string {
|
||||||
|
|
||||||
// Send sends a message to LINE. It first tries the Reply API (free)
|
// Send sends a message to LINE. It first tries the Reply API (free)
|
||||||
// using a cached reply token, then falls back to the Push API.
|
// using a cached reply token, then falls back to the Push API.
|
||||||
func (c *LINEChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *LINEChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load and consume quote token for this chat
|
// Load and consume quote token for this chat
|
||||||
|
|
@ -516,14 +516,14 @@ func (c *LINEChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
||||||
"chat_id": msg.ChatID,
|
"chat_id": msg.ChatID,
|
||||||
"quoted": quoteToken != "",
|
"quoted": quoteToken != "",
|
||||||
})
|
})
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
logger.DebugC("line", "Reply API failed, falling back to Push API")
|
logger.DebugC("line", "Reply API failed, falling back to Push API")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to Push API
|
// Fall back to Push API
|
||||||
return c.sendPush(ctx, msg.ChatID, msg.Content, quoteToken)
|
return nil, c.sendPush(ctx, msg.ChatID, msg.Content, quoteToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendMedia implements the channels.MediaSender interface.
|
// SendMedia implements the channels.MediaSender interface.
|
||||||
|
|
|
||||||
|
|
@ -240,15 +240,15 @@ func (c *MaixCamChannel) Stop(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MaixCamChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *MaixCamChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check ctx before entering write path
|
// Check ctx before entering write path
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return nil, ctx.Err()
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -257,7 +257,7 @@ func (c *MaixCamChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro
|
||||||
|
|
||||||
if len(c.clients) == 0 {
|
if len(c.clients) == 0 {
|
||||||
logger.WarnC("maixcam", "No MaixCam devices connected")
|
logger.WarnC("maixcam", "No MaixCam devices connected")
|
||||||
return fmt.Errorf("no connected MaixCam devices")
|
return nil, fmt.Errorf("no connected MaixCam devices")
|
||||||
}
|
}
|
||||||
|
|
||||||
response := map[string]any{
|
response := map[string]any{
|
||||||
|
|
@ -269,7 +269,7 @@ func (c *MaixCamChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro
|
||||||
|
|
||||||
data, err := json.Marshal(response)
|
data, err := json.Marshal(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal response: %w", err)
|
return nil, fmt.Errorf("failed to marshal response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sendErr error
|
var sendErr error
|
||||||
|
|
@ -285,5 +285,5 @@ func (c *MaixCamChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro
|
||||||
_ = conn.SetWriteDeadline(time.Time{})
|
_ = conn.SetWriteDeadline(time.Time{})
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendErr
|
return nil, sendErr
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -705,14 +705,8 @@ func (m *Manager) sendWithRetry(
|
||||||
|
|
||||||
var lastErr error
|
var lastErr error
|
||||||
var msgIDs []string
|
var msgIDs []string
|
||||||
sender, hasMessageIDsSender := w.ch.(MessageIDsSender)
|
|
||||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||||
msgIDs = nil
|
msgIDs, lastErr = w.ch.Send(ctx, msg)
|
||||||
if hasMessageIDsSender {
|
|
||||||
msgIDs, lastErr = sender.SendMessageWithIDs(ctx, msg)
|
|
||||||
} else {
|
|
||||||
lastErr = w.ch.Send(ctx, msg)
|
|
||||||
}
|
|
||||||
if lastErr == nil {
|
if lastErr == nil {
|
||||||
return msgIDs, true
|
return msgIDs, true
|
||||||
}
|
}
|
||||||
|
|
@ -1161,5 +1155,6 @@ func (m *Manager) SendToChannel(ctx context.Context, channelName, chatID, conten
|
||||||
|
|
||||||
// Fallback: direct send (should not happen)
|
// Fallback: direct send (should not happen)
|
||||||
channel, _ := m.channels[channelName]
|
channel, _ := m.channels[channelName]
|
||||||
return channel.Send(ctx, msg)
|
_, err := channel.Send(ctx, msg)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,20 +26,15 @@ type mockChannel struct {
|
||||||
lastPlaceholderID string
|
lastPlaceholderID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (m *mockChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
m.sentMessages = append(m.sentMessages, msg)
|
m.sentMessages = append(m.sentMessages, msg)
|
||||||
return m.sendFn(ctx, msg)
|
if m.sendWithIDsFn != nil {
|
||||||
}
|
return m.sendWithIDsFn(ctx, msg)
|
||||||
|
|
||||||
func (m *mockChannel) SendMessageWithIDs(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
|
||||||
m.sentMessages = append(m.sentMessages, msg)
|
|
||||||
if m.sendWithIDsFn == nil {
|
|
||||||
if m.sendFn == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, m.sendFn(ctx, msg)
|
|
||||||
}
|
}
|
||||||
return m.sendWithIDsFn(ctx, msg)
|
if m.sendFn == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, m.sendFn(ctx, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockChannel) Start(ctx context.Context) error { return nil }
|
func (m *mockChannel) Start(ctx context.Context) error { return nil }
|
||||||
|
|
|
||||||
|
|
@ -379,26 +379,26 @@ func markdownToHTML(md string) string {
|
||||||
return strings.TrimSpace(string(markdown.ToHTML([]byte(md), p, renderer)))
|
return strings.TrimSpace(string(markdown.ToHTML([]byte(md), p, renderer)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MatrixChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *MatrixChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
roomID := id.RoomID(strings.TrimSpace(msg.ChatID))
|
roomID := id.RoomID(strings.TrimSpace(msg.ChatID))
|
||||||
if roomID == "" {
|
if roomID == "" {
|
||||||
return fmt.Errorf("matrix room ID is empty: %w", channels.ErrSendFailed)
|
return nil, fmt.Errorf("matrix room ID is empty: %w", channels.ErrSendFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
content := strings.TrimSpace(msg.Content)
|
content := strings.TrimSpace(msg.Content)
|
||||||
if content == "" {
|
if content == "" {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := c.client.SendMessageEvent(ctx, roomID, event.EventMessage, c.messageContent(content))
|
_, err := c.client.SendMessageEvent(ctx, roomID, event.EventMessage, c.messageContent(content))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("matrix send: %w", channels.ErrTemporary)
|
return nil, fmt.Errorf("matrix send: %w", channels.ErrTemporary)
|
||||||
}
|
}
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MatrixChannel) messageContent(text string) *event.MessageEventContent {
|
func (c *MatrixChannel) messageContent(text string) *event.MessageEventContent {
|
||||||
|
|
|
||||||
|
|
@ -391,15 +391,15 @@ func (c *OneBotChannel) Stop(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *OneBotChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *OneBotChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check ctx before entering write path
|
// Check ctx before entering write path
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return nil, ctx.Err()
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -408,12 +408,12 @@ func (c *OneBotChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
return fmt.Errorf("OneBot WebSocket not connected")
|
return nil, fmt.Errorf("OneBot WebSocket not connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
action, params, err := c.buildSendRequest(msg)
|
action, params, err := c.buildSendRequest(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
echo := fmt.Sprintf("send_%d", atomic.AddInt64(&c.echoCounter, 1))
|
echo := fmt.Sprintf("send_%d", atomic.AddInt64(&c.echoCounter, 1))
|
||||||
|
|
@ -426,7 +426,7 @@ func (c *OneBotChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
|
|
||||||
data, err := json.Marshal(req)
|
data, err := json.Marshal(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal OneBot request: %w", err)
|
return nil, fmt.Errorf("failed to marshal OneBot request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.writeMu.Lock()
|
c.writeMu.Lock()
|
||||||
|
|
@ -439,10 +439,10 @@ func (c *OneBotChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
logger.ErrorCF("onebot", "Failed to send message", map[string]any{
|
logger.ErrorCF("onebot", "Failed to send message", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return fmt.Errorf("onebot send: %w", channels.ErrTemporary)
|
return nil, fmt.Errorf("onebot send: %w", channels.ErrTemporary)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendMedia implements the channels.MediaSender interface.
|
// SendMedia implements the channels.MediaSender interface.
|
||||||
|
|
|
||||||
|
|
@ -273,22 +273,22 @@ func (c *PicoClientChannel) handleServerMessage(pc *picoConn, msg PicoMessage) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send sends a message to the remote server.
|
// Send sends a message to the remote server.
|
||||||
func (c *PicoClientChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *PicoClientChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
pc := c.conn
|
pc := c.conn
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
if pc == nil || pc.closed.Load() {
|
if pc == nil || pc.closed.Load() {
|
||||||
return channels.ErrSendFailed
|
return nil, channels.ErrSendFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
outMsg := newMessage(TypeMessageSend, map[string]any{
|
outMsg := newMessage(TypeMessageSend, map[string]any{
|
||||||
"content": msg.Content,
|
"content": msg.Content,
|
||||||
})
|
})
|
||||||
outMsg.SessionID = strings.TrimPrefix(msg.ChatID, "pico_client:")
|
outMsg.SessionID = strings.TrimPrefix(msg.ChatID, "pico_client:")
|
||||||
return pc.writeJSON(outMsg)
|
return nil, pc.writeJSON(outMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartTyping implements channels.TypingCapable.
|
// StartTyping implements channels.TypingCapable.
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ func TestSend_NotRunning(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
err = ch.Send(context.Background(), bus.OutboundMessage{Content: "hi"})
|
_, err = ch.Send(context.Background(), bus.OutboundMessage{Content: "hi"})
|
||||||
if !errors.Is(err, channels.ErrNotRunning) {
|
if !errors.Is(err, channels.ErrNotRunning) {
|
||||||
t.Fatalf("expected ErrNotRunning, got %v", err)
|
t.Fatalf("expected ErrNotRunning, got %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -124,7 +124,7 @@ func TestClientChannel_ConnectAndSend(t *testing.T) {
|
||||||
defer ch.Stop(ctx)
|
defer ch.Stop(ctx)
|
||||||
|
|
||||||
// Send a message
|
// Send a message
|
||||||
err = ch.Send(ctx, bus.OutboundMessage{
|
_, err = ch.Send(ctx, bus.OutboundMessage{
|
||||||
ChatID: "pico_client:sess-1",
|
ChatID: "pico_client:sess-1",
|
||||||
Content: "hello",
|
Content: "hello",
|
||||||
})
|
})
|
||||||
|
|
@ -179,7 +179,7 @@ func TestClientChannel_ReceivesServerMessage(t *testing.T) {
|
||||||
defer ch.Stop(ctx)
|
defer ch.Stop(ctx)
|
||||||
|
|
||||||
// Send a message; the echo server replies with message.create
|
// Send a message; the echo server replies with message.create
|
||||||
err = ch.Send(ctx, bus.OutboundMessage{
|
_, err = ch.Send(ctx, bus.OutboundMessage{
|
||||||
ChatID: "pico_client:sess-echo",
|
ChatID: "pico_client:sess-echo",
|
||||||
Content: "ping",
|
Content: "ping",
|
||||||
})
|
})
|
||||||
|
|
@ -252,7 +252,7 @@ func TestSend_ClosedConnection(t *testing.T) {
|
||||||
ch.conn.close()
|
ch.conn.close()
|
||||||
ch.mu.Unlock()
|
ch.mu.Unlock()
|
||||||
|
|
||||||
err = ch.Send(ctx, bus.OutboundMessage{
|
_, err = ch.Send(ctx, bus.OutboundMessage{
|
||||||
ChatID: "pico_client:sess-close",
|
ChatID: "pico_client:sess-close",
|
||||||
Content: "should fail",
|
Content: "should fail",
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -234,16 +234,16 @@ func (c *PicoChannel) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send implements Channel — sends a message to the appropriate WebSocket connection.
|
// Send implements Channel — sends a message to the appropriate WebSocket connection.
|
||||||
func (c *PicoChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *PicoChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
outMsg := newMessage(TypeMessageCreate, map[string]any{
|
outMsg := newMessage(TypeMessageCreate, map[string]any{
|
||||||
"content": msg.Content,
|
"content": msg.Content,
|
||||||
})
|
})
|
||||||
|
|
||||||
return c.broadcastToSession(msg.ChatID, outMsg)
|
return nil, c.broadcastToSession(msg.ChatID, outMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditMessage implements channels.MessageEditor.
|
// EditMessage implements channels.MessageEditor.
|
||||||
|
|
|
||||||
|
|
@ -200,13 +200,7 @@ func (c *QQChannel) getChatKind(chatID string) string {
|
||||||
return "group"
|
return "group"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *QQChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *QQChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
_, err := c.SendMessageWithIDs(ctx, msg)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMessageWithIDs implements channels.MessageIDsSender.
|
|
||||||
func (c *QQChannel) SendMessageWithIDs(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return nil, channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,13 +108,7 @@ func (c *SlackChannel) Stop(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SlackChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *SlackChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
_, err := c.SendMessageWithIDs(ctx, msg)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMessageWithIDs implements channels.MessageIDsSender.
|
|
||||||
func (c *SlackChannel) SendMessageWithIDs(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return nil, channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -168,13 +168,7 @@ func (c *TelegramChannel) Stop(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
_, err := c.SendMessageWithIDs(ctx, msg)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMessageWithIDs implements channels.MessageIDsSender.
|
|
||||||
func (c *TelegramChannel) SendMessageWithIDs(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return nil, channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,7 @@ func TestSend_EmptyContent(t *testing.T) {
|
||||||
}
|
}
|
||||||
ch := newTestChannel(t, caller)
|
ch := newTestChannel(t, caller)
|
||||||
|
|
||||||
err := ch.Send(context.Background(), bus.OutboundMessage{
|
_, err := ch.Send(context.Background(), bus.OutboundMessage{
|
||||||
ChatID: "12345",
|
ChatID: "12345",
|
||||||
Content: "",
|
Content: "",
|
||||||
})
|
})
|
||||||
|
|
@ -253,7 +253,7 @@ func TestSend_ShortMessage_SingleCall(t *testing.T) {
|
||||||
}
|
}
|
||||||
ch := newTestChannel(t, caller)
|
ch := newTestChannel(t, caller)
|
||||||
|
|
||||||
err := ch.Send(context.Background(), bus.OutboundMessage{
|
_, err := ch.Send(context.Background(), bus.OutboundMessage{
|
||||||
ChatID: "12345",
|
ChatID: "12345",
|
||||||
Content: "Hello, world!",
|
Content: "Hello, world!",
|
||||||
})
|
})
|
||||||
|
|
@ -276,7 +276,7 @@ func TestSend_LongMessage_SingleCall(t *testing.T) {
|
||||||
|
|
||||||
longContent := strings.Repeat("a", 4000)
|
longContent := strings.Repeat("a", 4000)
|
||||||
|
|
||||||
err := ch.Send(context.Background(), bus.OutboundMessage{
|
_, err := ch.Send(context.Background(), bus.OutboundMessage{
|
||||||
ChatID: "12345",
|
ChatID: "12345",
|
||||||
Content: longContent,
|
Content: longContent,
|
||||||
})
|
})
|
||||||
|
|
@ -295,7 +295,7 @@ func TestSendMessageWithIDs_ReturnsAllChunkIDsAfterHTMLResplit(t *testing.T) {
|
||||||
chunk := "[x](https://example.com/" + strings.Repeat("a", 20) + ") "
|
chunk := "[x](https://example.com/" + strings.Repeat("a", 20) + ") "
|
||||||
content := strings.Repeat(chunk, 120)
|
content := strings.Repeat(chunk, 120)
|
||||||
|
|
||||||
ids, err := ch.SendMessageWithIDs(context.Background(), bus.OutboundMessage{
|
ids, err := ch.Send(context.Background(), bus.OutboundMessage{
|
||||||
ChatID: "12345",
|
ChatID: "12345",
|
||||||
Content: content,
|
Content: content,
|
||||||
})
|
})
|
||||||
|
|
@ -320,7 +320,7 @@ func TestSend_HTMLFallback_PerChunk(t *testing.T) {
|
||||||
}
|
}
|
||||||
ch := newTestChannel(t, caller)
|
ch := newTestChannel(t, caller)
|
||||||
|
|
||||||
err := ch.Send(context.Background(), bus.OutboundMessage{
|
_, err := ch.Send(context.Background(), bus.OutboundMessage{
|
||||||
ChatID: "12345",
|
ChatID: "12345",
|
||||||
Content: "Hello **world**",
|
Content: "Hello **world**",
|
||||||
})
|
})
|
||||||
|
|
@ -338,7 +338,7 @@ func TestSend_HTMLFallback_BothFail(t *testing.T) {
|
||||||
}
|
}
|
||||||
ch := newTestChannel(t, caller)
|
ch := newTestChannel(t, caller)
|
||||||
|
|
||||||
err := ch.Send(context.Background(), bus.OutboundMessage{
|
_, err := ch.Send(context.Background(), bus.OutboundMessage{
|
||||||
ChatID: "12345",
|
ChatID: "12345",
|
||||||
Content: "Hello",
|
Content: "Hello",
|
||||||
})
|
})
|
||||||
|
|
@ -360,7 +360,7 @@ func TestSend_LongMessage_HTMLFallback_StopsOnError(t *testing.T) {
|
||||||
|
|
||||||
longContent := strings.Repeat("x", 4001)
|
longContent := strings.Repeat("x", 4001)
|
||||||
|
|
||||||
err := ch.Send(context.Background(), bus.OutboundMessage{
|
_, err := ch.Send(context.Background(), bus.OutboundMessage{
|
||||||
ChatID: "12345",
|
ChatID: "12345",
|
||||||
Content: longContent,
|
Content: longContent,
|
||||||
})
|
})
|
||||||
|
|
@ -390,7 +390,7 @@ func TestSend_MarkdownShortButHTMLLong_MultipleCalls(t *testing.T) {
|
||||||
"HTML expansion must exceed Telegram limit for this test to be meaningful",
|
"HTML expansion must exceed Telegram limit for this test to be meaningful",
|
||||||
)
|
)
|
||||||
|
|
||||||
err := ch.Send(context.Background(), bus.OutboundMessage{
|
_, err := ch.Send(context.Background(), bus.OutboundMessage{
|
||||||
ChatID: "12345",
|
ChatID: "12345",
|
||||||
Content: markdownContent,
|
Content: markdownContent,
|
||||||
})
|
})
|
||||||
|
|
@ -425,7 +425,7 @@ func TestSend_HTMLOverflow_WordBoundary(t *testing.T) {
|
||||||
// Ensure the test content matches the intended boundary conditions.
|
// Ensure the test content matches the intended boundary conditions.
|
||||||
assert.LessOrEqual(t, len([]rune(content)), 4000, "markdown content must not exceed chunk size for this test")
|
assert.LessOrEqual(t, len([]rune(content)), 4000, "markdown content must not exceed chunk size for this test")
|
||||||
|
|
||||||
err := ch.Send(context.Background(), bus.OutboundMessage{
|
_, err := ch.Send(context.Background(), bus.OutboundMessage{
|
||||||
ChatID: "123456",
|
ChatID: "123456",
|
||||||
Content: content,
|
Content: content,
|
||||||
})
|
})
|
||||||
|
|
@ -461,7 +461,7 @@ func TestSend_NotRunning(t *testing.T) {
|
||||||
ch := newTestChannel(t, caller)
|
ch := newTestChannel(t, caller)
|
||||||
ch.SetRunning(false)
|
ch.SetRunning(false)
|
||||||
|
|
||||||
err := ch.Send(context.Background(), bus.OutboundMessage{
|
_, err := ch.Send(context.Background(), bus.OutboundMessage{
|
||||||
ChatID: "12345",
|
ChatID: "12345",
|
||||||
Content: "Hello",
|
Content: "Hello",
|
||||||
})
|
})
|
||||||
|
|
@ -479,7 +479,7 @@ func TestSend_InvalidChatID(t *testing.T) {
|
||||||
}
|
}
|
||||||
ch := newTestChannel(t, caller)
|
ch := newTestChannel(t, caller)
|
||||||
|
|
||||||
err := ch.Send(context.Background(), bus.OutboundMessage{
|
_, err := ch.Send(context.Background(), bus.OutboundMessage{
|
||||||
ChatID: "not-a-number",
|
ChatID: "not-a-number",
|
||||||
Content: "Hello",
|
Content: "Hello",
|
||||||
})
|
})
|
||||||
|
|
@ -536,7 +536,7 @@ func TestSend_WithForumThreadID(t *testing.T) {
|
||||||
}
|
}
|
||||||
ch := newTestChannel(t, caller)
|
ch := newTestChannel(t, caller)
|
||||||
|
|
||||||
err := ch.Send(context.Background(), bus.OutboundMessage{
|
_, err := ch.Send(context.Background(), bus.OutboundMessage{
|
||||||
ChatID: "-1001234567890/42",
|
ChatID: "-1001234567890/42",
|
||||||
Content: "Hello from topic",
|
Content: "Hello from topic",
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -184,20 +184,20 @@ func (c *WeComChannel) BeginStream(_ context.Context, chatID string) (channels.S
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WeComChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *WeComChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
content := strings.TrimSpace(msg.Content)
|
content := strings.TrimSpace(msg.Content)
|
||||||
if content == "" {
|
if content == "" {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if turn, ok := c.getTurn(msg.ChatID); ok {
|
if turn, ok := c.getTurn(msg.ChatID); ok {
|
||||||
if time.Since(turn.CreatedAt) <= wecomStreamMaxDuration {
|
if time.Since(turn.CreatedAt) <= wecomStreamMaxDuration {
|
||||||
if err := c.sendStreamReply(turn, content); err == nil {
|
if err := c.sendStreamReply(turn, content); err == nil {
|
||||||
c.consumeTurn(msg.ChatID, turn)
|
c.consumeTurn(msg.ChatID, turn)
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.consumeTurn(msg.ChatID, turn)
|
c.consumeTurn(msg.ChatID, turn)
|
||||||
|
|
@ -205,15 +205,15 @@ func (c *WeComChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
|
|
||||||
if route, ok := c.routes.Get(msg.ChatID); ok {
|
if route, ok := c.routes.Get(msg.ChatID); ok {
|
||||||
if err := c.sendActivePush(route.ChatID, route.ChatType, content); err != nil {
|
if err := c.sendActivePush(route.ChatID, route.ChatType, content); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.sendActivePush(msg.ChatID, 0, content); err != nil {
|
if err := c.sendActivePush(msg.ChatID, 0, content); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WeComChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessage) error {
|
func (c *WeComChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessage) error {
|
||||||
|
|
|
||||||
|
|
@ -313,16 +313,16 @@ func (c *WeixinChannel) handleInboundMessage(ctx context.Context, msg WeixinMess
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send implements channels.Channel by sending a text message to the WeChat user.
|
// Send implements channels.Channel by sending a text message to the WeChat user.
|
||||||
func (c *WeixinChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *WeixinChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
if err := c.ensureSessionActive(); err != nil {
|
if err := c.ensureSessionActive(); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg.Content == "" {
|
if msg.Content == "" {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need a context_token to send a reply. It should be stored in the conversation metadata.
|
// We need a context_token to send a reply. It should be stored in the conversation metadata.
|
||||||
|
|
@ -341,7 +341,7 @@ func (c *WeixinChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
logger.ErrorCF("weixin", "Missing context token, cannot send message", map[string]any{
|
logger.ErrorCF("weixin", "Missing context token, cannot send message", map[string]any{
|
||||||
"to_user_id": toUserID,
|
"to_user_id": toUserID,
|
||||||
})
|
})
|
||||||
return fmt.Errorf("weixin send: %w: missing context token for chat %s", channels.ErrSendFailed, toUserID)
|
return nil, fmt.Errorf("weixin send: %w: missing context token for chat %s", channels.ErrSendFailed, toUserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.sendTextMessage(ctx, toUserID, contextToken, msg.Content); err != nil {
|
if err := c.sendTextMessage(ctx, toUserID, contextToken, msg.Content); err != nil {
|
||||||
|
|
@ -350,10 +350,10 @@ func (c *WeixinChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
if c.remainingPause() > 0 {
|
if c.remainingPause() > 0 {
|
||||||
return fmt.Errorf("weixin send: %w", channels.ErrSendFailed)
|
return nil, fmt.Errorf("weixin send: %w", channels.ErrSendFailed)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("weixin send: %w", channels.ErrTemporary)
|
return nil, fmt.Errorf("weixin send: %w", channels.ErrTemporary)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,15 +104,15 @@ func (c *WhatsAppChannel) Stop(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WhatsAppChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *WhatsAppChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check ctx before acquiring lock
|
// Check ctx before acquiring lock
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return nil, ctx.Err()
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,7 +120,7 @@ func (c *WhatsAppChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
if c.conn == nil {
|
if c.conn == nil {
|
||||||
return fmt.Errorf("whatsapp connection not established: %w", channels.ErrTemporary)
|
return nil, fmt.Errorf("whatsapp connection not established: %w", channels.ErrTemporary)
|
||||||
}
|
}
|
||||||
|
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
|
|
@ -131,17 +131,17 @@ func (c *WhatsAppChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
|
||||||
|
|
||||||
data, err := json.Marshal(payload)
|
data, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal message: %w", err)
|
return nil, fmt.Errorf("failed to marshal message: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
_ = c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||||
if err := c.conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
if err := c.conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||||
_ = c.conn.SetWriteDeadline(time.Time{})
|
_ = c.conn.SetWriteDeadline(time.Time{})
|
||||||
return fmt.Errorf("whatsapp send: %w", channels.ErrTemporary)
|
return nil, fmt.Errorf("whatsapp send: %w", channels.ErrTemporary)
|
||||||
}
|
}
|
||||||
_ = c.conn.SetWriteDeadline(time.Time{})
|
_ = c.conn.SetWriteDeadline(time.Time{})
|
||||||
|
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WhatsAppChannel) listen() {
|
func (c *WhatsAppChannel) listen() {
|
||||||
|
|
|
||||||
|
|
@ -396,13 +396,13 @@ func (c *WhatsAppNativeChannel) handleIncoming(evt *events.Message) {
|
||||||
c.HandleMessage(c.runCtx, peer, messageID, senderID, chatID, content, mediaPaths, metadata, sender)
|
c.HandleMessage(c.runCtx, peer, messageID, senderID, chatID, content, mediaPaths, metadata, sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WhatsAppNativeChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
func (c *WhatsAppNativeChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
||||||
if !c.IsRunning() {
|
if !c.IsRunning() {
|
||||||
return channels.ErrNotRunning
|
return nil, channels.ErrNotRunning
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return nil, ctx.Err()
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -411,18 +411,18 @@ func (c *WhatsAppNativeChannel) Send(ctx context.Context, msg bus.OutboundMessag
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
if client == nil || !client.IsConnected() {
|
if client == nil || !client.IsConnected() {
|
||||||
return fmt.Errorf("whatsapp connection not established: %w", channels.ErrTemporary)
|
return nil, fmt.Errorf("whatsapp connection not established: %w", channels.ErrTemporary)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect unpaired state: the client is connected (to WhatsApp servers)
|
// Detect unpaired state: the client is connected (to WhatsApp servers)
|
||||||
// but has not completed QR-login yet, so sending would fail.
|
// but has not completed QR-login yet, so sending would fail.
|
||||||
if client.Store.ID == nil {
|
if client.Store.ID == nil {
|
||||||
return fmt.Errorf("whatsapp not yet paired (QR login pending): %w", channels.ErrTemporary)
|
return nil, fmt.Errorf("whatsapp not yet paired (QR login pending): %w", channels.ErrTemporary)
|
||||||
}
|
}
|
||||||
|
|
||||||
to, err := parseJID(msg.ChatID)
|
to, err := parseJID(msg.ChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid chat id %q: %w", msg.ChatID, err)
|
return nil, fmt.Errorf("invalid chat id %q: %w", msg.ChatID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
waMsg := &waE2E.Message{
|
waMsg := &waE2E.Message{
|
||||||
|
|
@ -430,9 +430,9 @@ func (c *WhatsAppNativeChannel) Send(ctx context.Context, msg bus.OutboundMessag
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = client.SendMessage(ctx, to, waMsg); err != nil {
|
if _, err = client.SendMessage(ctx, to, waMsg); err != nil {
|
||||||
return fmt.Errorf("whatsapp send: %w", channels.ErrTemporary)
|
return nil, fmt.Errorf("whatsapp send: %w", channels.ErrTemporary)
|
||||||
}
|
}
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseJID converts a chat ID (phone number or JID string) to types.JID.
|
// parseJID converts a chat ID (phone number or JID string) to types.JID.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue