diff --git a/pkg/channels/base.go b/pkg/channels/base.go index bd4ced849..b51eadb6e 100644 --- a/pkg/channels/base.go +++ b/pkg/channels/base.go @@ -18,6 +18,9 @@ import ( "github.com/sipeed/picoclaw/pkg/media" ) +// ForbiddenReplyText is the message sent to unauthorized users who message the bot. +const ForbiddenReplyText = "You are not authorized to use this bot." + var ( uniqueIDCounter uint64 uniqueIDPrefix string @@ -259,10 +262,22 @@ func (c *BaseChannel) HandleMessage( } if sender.CanonicalID != "" || sender.PlatformID != "" { if !c.IsAllowedSender(sender) { + logger.DebugCF(c.name, "Message blocked (not in allowlist)", map[string]any{"sender": sender.CanonicalID}) + if c.owner != nil { + _, _ = c.owner.Send(ctx, bus.OutboundMessage{ + Channel: c.name, ChatID: chatID, Content: ForbiddenReplyText, + }) + } return } } else { if !c.IsAllowed(senderID) { + logger.DebugCF(c.name, "Message blocked (not in allowlist)", map[string]any{"sender_id": senderID}) + if c.owner != nil { + _, _ = c.owner.Send(ctx, bus.OutboundMessage{ + Channel: c.name, ChatID: chatID, Content: ForbiddenReplyText, + }) + } return } } diff --git a/pkg/channels/base_test.go b/pkg/channels/base_test.go index 6132b8bf9..421ef9c4b 100644 --- a/pkg/channels/base_test.go +++ b/pkg/channels/base_test.go @@ -1,12 +1,17 @@ package channels import ( + "context" "testing" "github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/config" ) +func newTestBus() *bus.MessageBus { + return bus.NewMessageBus() +} + func TestBaseChannelIsAllowed(t *testing.T) { tests := []struct { name string @@ -263,3 +268,65 @@ func TestIsAllowedSender(t *testing.T) { }) } } + +func TestHandleMessageForbiddenReply(t *testing.T) { + const allowedID = "allowed:123" + const chatID = "chat-456" + + tests := []struct { + name string + sender bus.SenderInfo + wantReplied bool + }{ + { + name: "unauthorized sender receives forbidden reply", + sender: bus.SenderInfo{ + Platform: "test", + PlatformID: "999", + CanonicalID: "test:999", + }, + wantReplied: true, + }, + { + name: "authorized sender does not receive forbidden reply", + sender: bus.SenderInfo{ + Platform: "test", + PlatformID: "123", + CanonicalID: allowedID, + }, + wantReplied: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mock := &mockChannel{} + mock.BaseChannel = *NewBaseChannel("test", nil, newTestBus(), []string{allowedID}) + mock.SetOwner(mock) + + mock.HandleMessage( + context.Background(), + bus.Peer{Kind: "direct", ID: chatID}, + "msg-1", tt.sender.PlatformID, chatID, "hello", + nil, nil, + tt.sender, + ) + + if tt.wantReplied { + if len(mock.sentMessages) != 1 { + t.Fatalf("expected 1 forbidden reply, got %d", len(mock.sentMessages)) + } + if mock.sentMessages[0].Content != ForbiddenReplyText { + t.Errorf("reply content = %q, want %q", mock.sentMessages[0].Content, ForbiddenReplyText) + } + if mock.sentMessages[0].ChatID != chatID { + t.Errorf("reply chatID = %q, want %q", mock.sentMessages[0].ChatID, chatID) + } + } else { + if len(mock.sentMessages) != 0 { + t.Fatalf("expected no reply for authorized sender, got %d", len(mock.sentMessages)) + } + } + }) + } +} diff --git a/pkg/channels/telegram/telegram.go b/pkg/channels/telegram/telegram.go index 2d59de4dc..52ef82562 100644 --- a/pkg/channels/telegram/telegram.go +++ b/pkg/channels/telegram/telegram.go @@ -592,21 +592,25 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes DisplayName: user.FirstName, } + chatID := message.Chat.ID + chatIDStr := fmt.Sprintf("%d", chatID) + // check allowlist to avoid downloading attachments for rejected users if !c.IsAllowedSender(sender) { logger.DebugCF("telegram", "Message rejected by allowlist", map[string]any{ "user_id": platformID, }) + _, _ = c.Send(ctx, bus.OutboundMessage{ + Channel: "telegram", ChatID: chatIDStr, Content: channels.ForbiddenReplyText, + }) return nil } - chatID := message.Chat.ID c.chatIDs[platformID] = chatID content := "" mediaPaths := []string{} - chatIDStr := fmt.Sprintf("%d", chatID) messageIDStr := fmt.Sprintf("%d", message.MessageID) scope := channels.BuildMediaScope("telegram", chatIDStr, messageIDStr) diff --git a/pkg/channels/whatsapp_native/whatsapp_native.go b/pkg/channels/whatsapp_native/whatsapp_native.go index d0a74a405..38daca1a6 100644 --- a/pkg/channels/whatsapp_native/whatsapp_native.go +++ b/pkg/channels/whatsapp_native/whatsapp_native.go @@ -385,6 +385,8 @@ func (c *WhatsAppNativeChannel) handleIncoming(evt *events.Message) { } if !c.IsAllowedSender(sender) { + logger.DebugCF("whatsapp", "WhatsApp message blocked (not in allowlist)", map[string]any{"sender_id": senderID}) + _, _ = c.Send(c.runCtx, bus.OutboundMessage{Channel: "whatsapp", ChatID: chatID, Content: channels.ForbiddenReplyText}) return }