fix err and placeholder
This commit is contained in:
parent
df4f322f09
commit
930dd028f1
2 changed files with 127 additions and 3 deletions
|
|
@ -206,6 +206,40 @@ func (m *Manager) preSend(ctx context.Context, name string, msg bus.OutboundMess
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// preSendMedia handles typing stop, reaction undo, and placeholder cleanup
|
||||||
|
// before sending media attachments. Unlike preSend for text messages, media
|
||||||
|
// delivery never edits the placeholder because there is no text payload to
|
||||||
|
// replace it with; it only attempts to delete the placeholder when possible.
|
||||||
|
func (m *Manager) preSendMedia(ctx context.Context, name string, msg bus.OutboundMediaMessage, ch Channel) {
|
||||||
|
key := name + ":" + msg.ChatID
|
||||||
|
|
||||||
|
// 1. Stop typing
|
||||||
|
if v, loaded := m.typingStops.LoadAndDelete(key); loaded {
|
||||||
|
if entry, ok := v.(typingEntry); ok {
|
||||||
|
entry.stop() // idempotent, safe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Undo reaction
|
||||||
|
if v, loaded := m.reactionUndos.LoadAndDelete(key); loaded {
|
||||||
|
if entry, ok := v.(reactionEntry); ok {
|
||||||
|
entry.undo() // idempotent, safe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Clear any finalized stream marker for this chat before media delivery.
|
||||||
|
m.streamActive.LoadAndDelete(key)
|
||||||
|
|
||||||
|
// 4. Delete placeholder if present.
|
||||||
|
if v, loaded := m.placeholders.LoadAndDelete(key); loaded {
|
||||||
|
if entry, ok := v.(placeholderEntry); ok && entry.id != "" {
|
||||||
|
if deleter, ok := ch.(MessageDeleter); ok {
|
||||||
|
deleter.DeleteMessage(ctx, msg.ChatID, entry.id) // best effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewManager(cfg *config.Config, messageBus *bus.MessageBus, store media.MediaStore) (*Manager, error) {
|
func NewManager(cfg *config.Config, messageBus *bus.MessageBus, store media.MediaStore) (*Manager, error) {
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
channels: make(map[string]Channel),
|
channels: make(map[string]Channel),
|
||||||
|
|
@ -779,7 +813,8 @@ func (m *Manager) runMediaWorker(ctx context.Context, name string, w *channelWor
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendMediaWithRetry sends a media message through the channel with rate limiting and
|
// sendMediaWithRetry sends a media message through the channel with rate limiting and
|
||||||
// retry logic. It returns nil on success, or the last error after retries.
|
// retry logic. It returns nil on success, or the last error after retries,
|
||||||
|
// including when the channel does not support MediaSender.
|
||||||
func (m *Manager) sendMediaWithRetry(
|
func (m *Manager) sendMediaWithRetry(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
name string,
|
name string,
|
||||||
|
|
@ -788,10 +823,12 @@ func (m *Manager) sendMediaWithRetry(
|
||||||
) error {
|
) error {
|
||||||
ms, ok := w.ch.(MediaSender)
|
ms, ok := w.ch.(MediaSender)
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.DebugCF("channels", "Channel does not support MediaSender, skipping media", map[string]any{
|
err := fmt.Errorf("channel %q does not support media sending", name)
|
||||||
|
logger.WarnCF("channels", "Channel does not support MediaSender", map[string]any{
|
||||||
"channel": name,
|
"channel": name,
|
||||||
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate limit: wait for token
|
// Rate limit: wait for token
|
||||||
|
|
@ -799,6 +836,9 @@ func (m *Manager) sendMediaWithRetry(
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-send: stop typing and clean up any placeholder before sending media.
|
||||||
|
m.preSendMedia(ctx, name, msg, w.ch)
|
||||||
|
|
||||||
var lastErr error
|
var lastErr error
|
||||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||||
lastErr = ms.SendMedia(ctx, msg)
|
lastErr = ms.SendMedia(ctx, msg)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -57,6 +58,26 @@ func (m *mockMediaChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaM
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockDeletingMediaChannel struct {
|
||||||
|
mockMediaChannel
|
||||||
|
deleteCalls int
|
||||||
|
lastDeleted struct {
|
||||||
|
chatID string
|
||||||
|
messageID string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockDeletingMediaChannel) DeleteMessage(
|
||||||
|
_ context.Context,
|
||||||
|
chatID string,
|
||||||
|
messageID string,
|
||||||
|
) error {
|
||||||
|
m.deleteCalls++
|
||||||
|
m.lastDeleted.chatID = chatID
|
||||||
|
m.lastDeleted.messageID = messageID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// newTestManager creates a minimal Manager suitable for unit tests.
|
// newTestManager creates a minimal Manager suitable for unit tests.
|
||||||
func newTestManager() *Manager {
|
func newTestManager() *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
|
|
@ -278,6 +299,69 @@ func TestSendMedia_PropagatesFailure(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSendMedia_UnsupportedChannelReturnsError(t *testing.T) {
|
||||||
|
m := newTestManager()
|
||||||
|
ch := &mockChannel{
|
||||||
|
sendFn: func(_ context.Context, _ bus.OutboundMessage) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
w := &channelWorker{
|
||||||
|
ch: ch,
|
||||||
|
limiter: rate.NewLimiter(rate.Inf, 1),
|
||||||
|
}
|
||||||
|
m.channels["test"] = ch
|
||||||
|
m.workers["test"] = w
|
||||||
|
|
||||||
|
err := m.SendMedia(context.Background(), bus.OutboundMediaMessage{
|
||||||
|
Channel: "test",
|
||||||
|
ChatID: "chat1",
|
||||||
|
Parts: []bus.MediaPart{{Ref: "media://abc"}},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected SendMedia to return error for unsupported channel")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "does not support media sending") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendMedia_DeletesPlaceholderBeforeSending(t *testing.T) {
|
||||||
|
m := newTestManager()
|
||||||
|
ch := &mockDeletingMediaChannel{
|
||||||
|
mockMediaChannel: mockMediaChannel{
|
||||||
|
sendMediaFn: func(_ context.Context, _ bus.OutboundMediaMessage) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
w := &channelWorker{
|
||||||
|
ch: ch,
|
||||||
|
limiter: rate.NewLimiter(rate.Inf, 1),
|
||||||
|
}
|
||||||
|
m.channels["test"] = ch
|
||||||
|
m.workers["test"] = w
|
||||||
|
m.RecordPlaceholder("test", "chat1", "placeholder-1")
|
||||||
|
|
||||||
|
err := m.SendMedia(context.Background(), bus.OutboundMediaMessage{
|
||||||
|
Channel: "test",
|
||||||
|
ChatID: "chat1",
|
||||||
|
Parts: []bus.MediaPart{{Ref: "media://abc"}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SendMedia() error = %v", err)
|
||||||
|
}
|
||||||
|
if ch.deleteCalls != 1 {
|
||||||
|
t.Fatalf("expected placeholder delete to be called once, got %d", ch.deleteCalls)
|
||||||
|
}
|
||||||
|
if ch.lastDeleted.chatID != "chat1" || ch.lastDeleted.messageID != "placeholder-1" {
|
||||||
|
t.Fatalf("unexpected placeholder deletion target: %+v", ch.lastDeleted)
|
||||||
|
}
|
||||||
|
if len(ch.sentMediaMessages) != 1 {
|
||||||
|
t.Fatalf("expected media to be sent once, got %d", len(ch.sentMediaMessages))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSendWithRetry_UnknownError(t *testing.T) {
|
func TestSendWithRetry_UnknownError(t *testing.T) {
|
||||||
m := newTestManager()
|
m := newTestManager()
|
||||||
var callCount int
|
var callCount int
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue