fix: send forbidden reply when unauthorized user messages bot

When a user not on the allowlist sends a message, the bot now replies
with "You are not authorized to use this bot." instead of silently
dropping the message. Fixes the confirmed case in whatsapp_native and
also covers telegram and the HandleMessage safety-net in base.go.

Closes #49

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
github-actions[bot] 2026-04-05 12:25:52 +02:00
parent 84e42d6904
commit 0ca27794b4
4 changed files with 90 additions and 2 deletions

View file

@ -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
}
}

View file

@ -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))
}
}
})
}
}

View file

@ -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)

View file

@ -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
}