fix(qq): use openid routing and allowlist compatibility
This commit is contained in:
parent
415abc8cd4
commit
9063a6a25f
2 changed files with 390 additions and 38 deletions
|
|
@ -41,6 +41,7 @@ const (
|
||||||
typingResend = 8 * time.Second
|
typingResend = 8 * time.Second
|
||||||
typingSeconds = 10
|
typingSeconds = 10
|
||||||
bytesPerMiB = 1024 * 1024
|
bytesPerMiB = 1024 * 1024
|
||||||
|
qqStartupProbe = 15 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
type qqAPI interface {
|
type qqAPI interface {
|
||||||
|
|
@ -82,6 +83,60 @@ type QQChannel struct {
|
||||||
stopOnce sync.Once
|
stopOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type qqRawAuthor struct {
|
||||||
|
UserOpenID string `json:"user_openid"`
|
||||||
|
MemberOpenID string `json:"member_openid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type qqRawEnvelope struct {
|
||||||
|
D struct {
|
||||||
|
Author qqRawAuthor `json:"author"`
|
||||||
|
GroupOpenID string `json:"group_openid"`
|
||||||
|
} `json:"d"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseQQOpenIDs(raw []byte) (userOpenID, memberOpenID, groupOpenID string) {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return "", "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var env qqRawEnvelope
|
||||||
|
if err := json.Unmarshal(raw, &env); err != nil {
|
||||||
|
return "", "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(env.D.Author.UserOpenID),
|
||||||
|
strings.TrimSpace(env.D.Author.MemberOpenID),
|
||||||
|
strings.TrimSpace(env.D.GroupOpenID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveAllowedSender validates sender against allow_from using openid first,
|
||||||
|
// then falls back to legacy author/member id for backward compatibility.
|
||||||
|
func (c *QQChannel) resolveAllowedSender(primaryOpenID, legacyID string) (bus.SenderInfo, bool, bool) {
|
||||||
|
primary := bus.SenderInfo{
|
||||||
|
Platform: "qq",
|
||||||
|
PlatformID: primaryOpenID,
|
||||||
|
CanonicalID: identity.BuildCanonicalID("qq", primaryOpenID),
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsAllowedSender(primary) {
|
||||||
|
return primary, true, false
|
||||||
|
}
|
||||||
|
|
||||||
|
legacyID = strings.TrimSpace(legacyID)
|
||||||
|
if legacyID != "" && legacyID != primaryOpenID {
|
||||||
|
legacy := bus.SenderInfo{
|
||||||
|
Platform: "qq",
|
||||||
|
PlatformID: legacyID,
|
||||||
|
}
|
||||||
|
if c.IsAllowedSender(legacy) {
|
||||||
|
return legacy, true, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return primary, false, false
|
||||||
|
}
|
||||||
|
|
||||||
func NewQQChannel(cfg config.QQConfig, messageBus *bus.MessageBus) (*QQChannel, error) {
|
func NewQQChannel(cfg config.QQConfig, messageBus *bus.MessageBus) (*QQChannel, error) {
|
||||||
base := channels.NewBaseChannel("qq", cfg, messageBus, cfg.AllowFrom,
|
base := channels.NewBaseChannel("qq", cfg, messageBus, cfg.AllowFrom,
|
||||||
channels.WithMaxMessageLength(cfg.MaxMessageLength),
|
channels.WithMaxMessageLength(cfg.MaxMessageLength),
|
||||||
|
|
@ -127,8 +182,24 @@ func (c *QQChannel) Start(ctx context.Context) error {
|
||||||
// initialize OpenAPI client
|
// initialize OpenAPI client
|
||||||
c.api = botgo.NewOpenAPI(c.config.AppID, c.tokenSource).WithTimeout(5 * time.Second)
|
c.api = botgo.NewOpenAPI(c.config.AppID, c.tokenSource).WithTimeout(5 * time.Second)
|
||||||
|
|
||||||
|
readyCh := make(chan struct{}, 1)
|
||||||
|
sessionErrCh := make(chan error, 1)
|
||||||
|
|
||||||
// register event handlers
|
// register event handlers
|
||||||
intent := event.RegisterHandlers(
|
intent := event.RegisterHandlers(
|
||||||
|
event.ReadyHandler(func(_ *dto.WSPayload, _ *dto.WSReadyData) {
|
||||||
|
select {
|
||||||
|
case readyCh <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
event.ErrorNotifyHandler(func(err error) {
|
||||||
|
fmt.Printf("QQ gateway error: %v\n", err)
|
||||||
|
select {
|
||||||
|
case sessionErrCh <- err:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}),
|
||||||
c.handleC2CMessage(),
|
c.handleC2CMessage(),
|
||||||
c.handleGroupATMessage(),
|
c.handleGroupATMessage(),
|
||||||
)
|
)
|
||||||
|
|
@ -145,17 +216,50 @@ func (c *QQChannel) Start(ctx context.Context) error {
|
||||||
|
|
||||||
// create and save sessionManager
|
// create and save sessionManager
|
||||||
c.sessionManager = botgo.NewSessionManager()
|
c.sessionManager = botgo.NewSessionManager()
|
||||||
|
startupErr := make(chan error, 1)
|
||||||
|
|
||||||
// start WebSocket connection in goroutine to avoid blocking
|
// start WebSocket connection in goroutine to avoid blocking
|
||||||
go func() {
|
go func() {
|
||||||
if err := c.sessionManager.Start(wsInfo, c.tokenSource, &intent); err != nil {
|
err := c.sessionManager.Start(wsInfo, c.tokenSource, &intent)
|
||||||
|
if err != nil {
|
||||||
logger.ErrorCF("qq", "WebSocket session error", map[string]any{
|
logger.ErrorCF("qq", "WebSocket session error", map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
fmt.Printf("QQ WebSocket session error: %v\n", err)
|
||||||
c.SetRunning(false)
|
c.SetRunning(false)
|
||||||
}
|
}
|
||||||
|
select {
|
||||||
|
case startupErr <- err:
|
||||||
|
default:
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-readyCh:
|
||||||
|
fmt.Println("QQ WebSocket ready")
|
||||||
|
case err := <-sessionErrCh:
|
||||||
|
if c.cancel != nil {
|
||||||
|
c.cancel()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("QQ websocket failed before ready: %w", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("QQ websocket failed before ready")
|
||||||
|
case err := <-startupErr:
|
||||||
|
if c.cancel != nil {
|
||||||
|
c.cancel()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to establish QQ websocket session: %w", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("QQ websocket session exited unexpectedly during startup")
|
||||||
|
case <-time.After(qqStartupProbe):
|
||||||
|
if c.cancel != nil {
|
||||||
|
c.cancel()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("timeout waiting for QQ websocket READY event")
|
||||||
|
}
|
||||||
|
|
||||||
// start dedup janitor goroutine
|
// start dedup janitor goroutine
|
||||||
go c.dedupJanitor()
|
go c.dedupJanitor()
|
||||||
|
|
||||||
|
|
@ -597,29 +701,53 @@ func (c *QQChannel) maxBase64FileSizeBytes() int64 {
|
||||||
// handleC2CMessage handles QQ private messages.
|
// handleC2CMessage handles QQ private messages.
|
||||||
func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler {
|
func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler {
|
||||||
return func(event *dto.WSPayload, data *dto.WSC2CMessageData) error {
|
return func(event *dto.WSPayload, data *dto.WSC2CMessageData) error {
|
||||||
|
if data == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// deduplication check
|
// deduplication check
|
||||||
if c.isDuplicate(data.ID) {
|
if c.isDuplicate(data.ID) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract user info
|
var raw []byte
|
||||||
var senderID string
|
if event != nil {
|
||||||
if data.Author != nil && data.Author.ID != "" {
|
raw = event.RawMessage
|
||||||
senderID = data.Author.ID
|
}
|
||||||
} else {
|
userOpenID, _, _ := parseQQOpenIDs(raw)
|
||||||
|
legacyAuthorID := ""
|
||||||
|
if data.Author != nil {
|
||||||
|
legacyAuthorID = strings.TrimSpace(data.Author.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QQ C2C endpoint requires user_openid; fallback to author.id for compatibility.
|
||||||
|
senderID := userOpenID
|
||||||
|
if senderID == "" {
|
||||||
|
senderID = legacyAuthorID
|
||||||
|
}
|
||||||
|
if senderID == "" {
|
||||||
logger.WarnC("qq", "Received message with no sender ID")
|
logger.WarnC("qq", "Received message with no sender ID")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sender := bus.SenderInfo{
|
sender, allowed, usedLegacyFallback := c.resolveAllowedSender(senderID, legacyAuthorID)
|
||||||
Platform: "qq",
|
if !allowed {
|
||||||
PlatformID: data.Author.ID,
|
logger.WarnCF("qq", "Dropped C2C message by allow_from", map[string]any{
|
||||||
CanonicalID: identity.BuildCanonicalID("qq", data.Author.ID),
|
"sender_openid": senderID,
|
||||||
}
|
"legacy_id": legacyAuthorID,
|
||||||
|
"message_id": data.ID,
|
||||||
if !c.IsAllowedSender(sender) {
|
})
|
||||||
|
fmt.Println("QQ inbound C2C dropped by allow_from")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if usedLegacyFallback {
|
||||||
|
logger.WarnCF("qq", "allow_from matched legacy QQ sender id", map[string]any{
|
||||||
|
"sender_openid": senderID,
|
||||||
|
"legacy_id": legacyAuthorID,
|
||||||
|
"message_id": data.ID,
|
||||||
|
})
|
||||||
|
fmt.Println("QQ inbound C2C allow_from matched legacy id")
|
||||||
|
}
|
||||||
|
|
||||||
content := strings.TrimSpace(data.Content)
|
content := strings.TrimSpace(data.Content)
|
||||||
mediaPaths, attachmentNotes := c.extractInboundAttachments(senderID, data.ID, data.Attachments)
|
mediaPaths, attachmentNotes := c.extractInboundAttachments(senderID, data.ID, data.Attachments)
|
||||||
|
|
@ -638,21 +766,25 @@ func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Store chat routing context.
|
// Store chat routing context.
|
||||||
c.chatType.Store(senderID, "direct")
|
chatID := senderID
|
||||||
c.lastMsgID.Store(senderID, data.ID)
|
c.chatType.Store(chatID, "direct")
|
||||||
|
c.lastMsgID.Store(chatID, data.ID)
|
||||||
|
|
||||||
// Reset msg_seq counter for new inbound message.
|
// Reset msg_seq counter for new inbound message.
|
||||||
c.msgSeqCounters.Store(senderID, new(atomic.Uint64))
|
c.msgSeqCounters.Store(chatID, new(atomic.Uint64))
|
||||||
|
|
||||||
metadata := map[string]string{
|
metadata := map[string]string{
|
||||||
"account_id": senderID,
|
"account_id": senderID,
|
||||||
}
|
}
|
||||||
|
if legacyAuthorID != "" && legacyAuthorID != senderID {
|
||||||
|
metadata["legacy_account_id"] = legacyAuthorID
|
||||||
|
}
|
||||||
|
|
||||||
c.HandleMessage(c.ctx,
|
c.HandleMessage(c.ctx,
|
||||||
bus.Peer{Kind: "direct", ID: senderID},
|
bus.Peer{Kind: "direct", ID: chatID},
|
||||||
data.ID,
|
data.ID,
|
||||||
senderID,
|
senderID,
|
||||||
senderID,
|
chatID,
|
||||||
content,
|
content,
|
||||||
mediaPaths,
|
mediaPaths,
|
||||||
metadata,
|
metadata,
|
||||||
|
|
@ -666,32 +798,67 @@ func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler {
|
||||||
// handleGroupATMessage handles QQ group @ messages.
|
// handleGroupATMessage handles QQ group @ messages.
|
||||||
func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler {
|
func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler {
|
||||||
return func(event *dto.WSPayload, data *dto.WSGroupATMessageData) error {
|
return func(event *dto.WSPayload, data *dto.WSGroupATMessageData) error {
|
||||||
|
if data == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// deduplication check
|
// deduplication check
|
||||||
if c.isDuplicate(data.ID) {
|
if c.isDuplicate(data.ID) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract user info
|
var raw []byte
|
||||||
var senderID string
|
if event != nil {
|
||||||
if data.Author != nil && data.Author.ID != "" {
|
raw = event.RawMessage
|
||||||
senderID = data.Author.ID
|
}
|
||||||
} else {
|
_, memberOpenID, groupOpenID := parseQQOpenIDs(raw)
|
||||||
|
legacyMemberID := ""
|
||||||
|
if data.Author != nil {
|
||||||
|
legacyMemberID = strings.TrimSpace(data.Author.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For QQ group callbacks, member_openid/group_openid are preferred identifiers.
|
||||||
|
senderID := memberOpenID
|
||||||
|
if senderID == "" {
|
||||||
|
senderID = legacyMemberID
|
||||||
|
}
|
||||||
|
if senderID == "" {
|
||||||
logger.WarnC("qq", "Received group message with no sender ID")
|
logger.WarnC("qq", "Received group message with no sender ID")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sender := bus.SenderInfo{
|
chatID := groupOpenID
|
||||||
Platform: "qq",
|
if chatID == "" {
|
||||||
PlatformID: data.Author.ID,
|
chatID = strings.TrimSpace(data.GroupID)
|
||||||
CanonicalID: identity.BuildCanonicalID("qq", data.Author.ID),
|
|
||||||
}
|
}
|
||||||
|
if chatID == "" {
|
||||||
if !c.IsAllowedSender(sender) {
|
logger.WarnC("qq", "Received group message with no group ID")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sender, allowed, usedLegacyFallback := c.resolveAllowedSender(senderID, legacyMemberID)
|
||||||
|
if !allowed {
|
||||||
|
logger.WarnCF("qq", "Dropped group message by allow_from", map[string]any{
|
||||||
|
"sender_openid": senderID,
|
||||||
|
"legacy_id": legacyMemberID,
|
||||||
|
"group_id": chatID,
|
||||||
|
"message_id": data.ID,
|
||||||
|
})
|
||||||
|
fmt.Println("QQ inbound group dropped by allow_from")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if usedLegacyFallback {
|
||||||
|
logger.WarnCF("qq", "allow_from matched legacy QQ member id", map[string]any{
|
||||||
|
"sender_openid": senderID,
|
||||||
|
"legacy_id": legacyMemberID,
|
||||||
|
"group_id": chatID,
|
||||||
|
"message_id": data.ID,
|
||||||
|
})
|
||||||
|
fmt.Println("QQ inbound group allow_from matched legacy id")
|
||||||
|
}
|
||||||
|
|
||||||
content := strings.TrimSpace(data.Content)
|
content := strings.TrimSpace(data.Content)
|
||||||
mediaPaths, attachmentNotes := c.extractInboundAttachments(data.GroupID, data.ID, data.Attachments)
|
mediaPaths, attachmentNotes := c.extractInboundAttachments(chatID, data.ID, data.Attachments)
|
||||||
for _, note := range attachmentNotes {
|
for _, note := range attachmentNotes {
|
||||||
content = appendContent(content, note)
|
content = appendContent(content, note)
|
||||||
}
|
}
|
||||||
|
|
@ -709,28 +876,31 @@ func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler {
|
||||||
|
|
||||||
logger.InfoCF("qq", "Received group AT message", map[string]any{
|
logger.InfoCF("qq", "Received group AT message", map[string]any{
|
||||||
"sender": senderID,
|
"sender": senderID,
|
||||||
"group": data.GroupID,
|
"group": chatID,
|
||||||
"length": len(content),
|
"length": len(content),
|
||||||
"media_count": len(mediaPaths),
|
"media_count": len(mediaPaths),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Store chat routing context using GroupID as chatID.
|
// Store chat routing context using GroupID as chatID.
|
||||||
c.chatType.Store(data.GroupID, "group")
|
c.chatType.Store(chatID, "group")
|
||||||
c.lastMsgID.Store(data.GroupID, data.ID)
|
c.lastMsgID.Store(chatID, data.ID)
|
||||||
|
|
||||||
// Reset msg_seq counter for new inbound message.
|
// Reset msg_seq counter for new inbound message.
|
||||||
c.msgSeqCounters.Store(data.GroupID, new(atomic.Uint64))
|
c.msgSeqCounters.Store(chatID, new(atomic.Uint64))
|
||||||
|
|
||||||
metadata := map[string]string{
|
metadata := map[string]string{
|
||||||
"account_id": senderID,
|
"account_id": senderID,
|
||||||
"group_id": data.GroupID,
|
"group_id": chatID,
|
||||||
|
}
|
||||||
|
if legacyMemberID != "" && legacyMemberID != senderID {
|
||||||
|
metadata["legacy_account_id"] = legacyMemberID
|
||||||
}
|
}
|
||||||
|
|
||||||
c.HandleMessage(c.ctx,
|
c.HandleMessage(c.ctx,
|
||||||
bus.Peer{Kind: "group", ID: data.GroupID},
|
bus.Peer{Kind: "group", ID: chatID},
|
||||||
data.ID,
|
data.ID,
|
||||||
senderID,
|
senderID,
|
||||||
data.GroupID,
|
chatID,
|
||||||
content,
|
content,
|
||||||
mediaPaths,
|
mediaPaths,
|
||||||
metadata,
|
metadata,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -62,6 +63,187 @@ func TestHandleC2CMessage_IncludesAccountIDMetadata(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleC2CMessage_PrefersUserOpenIDFromRawPayload(t *testing.T) {
|
||||||
|
messageBus := bus.NewMessageBus()
|
||||||
|
ch := &QQChannel{
|
||||||
|
BaseChannel: channels.NewBaseChannel("qq", nil, messageBus, nil),
|
||||||
|
dedup: make(map[string]time.Time),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
ctx: context.Background(),
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := []byte(`{"d":{"author":{"id":"legacy-id","user_openid":"user-openid-123"}}}`)
|
||||||
|
err := ch.handleC2CMessage()(&dto.WSPayload{RawMessage: raw}, &dto.WSC2CMessageData{
|
||||||
|
ID: "msg-openid",
|
||||||
|
Content: "hello",
|
||||||
|
Author: &dto.User{
|
||||||
|
ID: "legacy-id",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("handleC2CMessage() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inbound := waitInboundMessage(t, messageBus)
|
||||||
|
if inbound.Metadata["account_id"] != "user-openid-123" {
|
||||||
|
t.Fatalf("account_id metadata = %q, want %q", inbound.Metadata["account_id"], "user-openid-123")
|
||||||
|
}
|
||||||
|
if inbound.ChatID != "user-openid-123" {
|
||||||
|
t.Fatalf("inbound.ChatID = %q, want %q", inbound.ChatID, "user-openid-123")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleC2CMessage_AllowListFallsBackToLegacyID(t *testing.T) {
|
||||||
|
messageBus := bus.NewMessageBus()
|
||||||
|
ch := &QQChannel{
|
||||||
|
BaseChannel: channels.NewBaseChannel("qq", nil, messageBus, []string{"legacy-id"}),
|
||||||
|
dedup: make(map[string]time.Time),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
ctx: context.Background(),
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := []byte(`{"d":{"author":{"id":"legacy-id","user_openid":"user-openid-123"}}}`)
|
||||||
|
err := ch.handleC2CMessage()(&dto.WSPayload{RawMessage: raw}, &dto.WSC2CMessageData{
|
||||||
|
ID: "msg-allow-fallback",
|
||||||
|
Content: "hello",
|
||||||
|
Author: &dto.User{
|
||||||
|
ID: "legacy-id",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("handleC2CMessage() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inbound := waitInboundMessage(t, messageBus)
|
||||||
|
if inbound.SenderID != "user-openid-123" {
|
||||||
|
t.Fatalf("inbound.SenderID = %q, want %q", inbound.SenderID, "user-openid-123")
|
||||||
|
}
|
||||||
|
if inbound.Metadata["account_id"] != "user-openid-123" {
|
||||||
|
t.Fatalf("account_id metadata = %q, want %q", inbound.Metadata["account_id"], "user-openid-123")
|
||||||
|
}
|
||||||
|
if inbound.Metadata["legacy_account_id"] != "legacy-id" {
|
||||||
|
t.Fatalf("legacy_account_id metadata = %q, want %q", inbound.Metadata["legacy_account_id"], "legacy-id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleGroupATMessage_PrefersGroupOpenIDFromRawPayload(t *testing.T) {
|
||||||
|
messageBus := bus.NewMessageBus()
|
||||||
|
ch := &QQChannel{
|
||||||
|
BaseChannel: channels.NewBaseChannel("qq", nil, messageBus, nil),
|
||||||
|
dedup: make(map[string]time.Time),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
ctx: context.Background(),
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := []byte(`{"d":{"group_openid":"group-openid-abc","author":{"id":"legacy-member-id","member_openid":"member-openid-xyz"}}}`)
|
||||||
|
err := ch.handleGroupATMessage()(&dto.WSPayload{RawMessage: raw}, &dto.WSGroupATMessageData{
|
||||||
|
ID: "group-openid-msg",
|
||||||
|
GroupID: "legacy-group-id",
|
||||||
|
Content: "@bot hello",
|
||||||
|
Author: &dto.User{
|
||||||
|
ID: "legacy-member-id",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("handleGroupATMessage() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inbound := waitInboundMessage(t, messageBus)
|
||||||
|
if inbound.Peer.ID != "group-openid-abc" {
|
||||||
|
t.Fatalf("inbound.Peer.ID = %q, want %q", inbound.Peer.ID, "group-openid-abc")
|
||||||
|
}
|
||||||
|
if inbound.Metadata["group_id"] != "group-openid-abc" {
|
||||||
|
t.Fatalf("group_id metadata = %q, want %q", inbound.Metadata["group_id"], "group-openid-abc")
|
||||||
|
}
|
||||||
|
if inbound.Metadata["account_id"] != "member-openid-xyz" {
|
||||||
|
t.Fatalf("account_id metadata = %q, want %q", inbound.Metadata["account_id"], "member-openid-xyz")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleGroupATMessage_AllowListFallsBackToLegacyMemberID(t *testing.T) {
|
||||||
|
messageBus := bus.NewMessageBus()
|
||||||
|
ch := &QQChannel{
|
||||||
|
BaseChannel: channels.NewBaseChannel("qq", nil, messageBus, []string{"legacy-member-id"}),
|
||||||
|
dedup: make(map[string]time.Time),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
ctx: context.Background(),
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := []byte(`{"d":{"group_openid":"group-openid-abc","author":{"id":"legacy-member-id","member_openid":"member-openid-xyz"}}}`)
|
||||||
|
err := ch.handleGroupATMessage()(&dto.WSPayload{RawMessage: raw}, &dto.WSGroupATMessageData{
|
||||||
|
ID: "group-allow-fallback",
|
||||||
|
GroupID: "legacy-group-id",
|
||||||
|
Content: "@bot hello",
|
||||||
|
Author: &dto.User{
|
||||||
|
ID: "legacy-member-id",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("handleGroupATMessage() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inbound := waitInboundMessage(t, messageBus)
|
||||||
|
if inbound.SenderID != "member-openid-xyz" {
|
||||||
|
t.Fatalf("inbound.SenderID = %q, want %q", inbound.SenderID, "member-openid-xyz")
|
||||||
|
}
|
||||||
|
if inbound.Metadata["account_id"] != "member-openid-xyz" {
|
||||||
|
t.Fatalf("account_id metadata = %q, want %q", inbound.Metadata["account_id"], "member-openid-xyz")
|
||||||
|
}
|
||||||
|
if inbound.Metadata["legacy_account_id"] != "legacy-member-id" {
|
||||||
|
t.Fatalf("legacy_account_id metadata = %q, want %q", inbound.Metadata["legacy_account_id"], "legacy-member-id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseQQOpenIDs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
raw []byte
|
||||||
|
wantUser string
|
||||||
|
wantMember string
|
||||||
|
wantGroup string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
raw: nil,
|
||||||
|
wantUser: "",
|
||||||
|
wantMember: "",
|
||||||
|
wantGroup: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid json",
|
||||||
|
raw: []byte("{not-json}"),
|
||||||
|
wantUser: "",
|
||||||
|
wantMember: "",
|
||||||
|
wantGroup: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all openids",
|
||||||
|
raw: []byte(`{"d":{"group_openid":"group-1","author":{"user_openid":"user-1","member_openid":"member-1"}}}`),
|
||||||
|
wantUser: "user-1",
|
||||||
|
wantMember: "member-1",
|
||||||
|
wantGroup: "group-1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
userOpenID, memberOpenID, groupOpenID := parseQQOpenIDs(tc.raw)
|
||||||
|
if userOpenID != tc.wantUser || memberOpenID != tc.wantMember || groupOpenID != tc.wantGroup {
|
||||||
|
t.Fatalf(
|
||||||
|
"parseQQOpenIDs(%s) = (%q, %q, %q), want (%q, %q, %q)",
|
||||||
|
tc.name,
|
||||||
|
userOpenID,
|
||||||
|
memberOpenID,
|
||||||
|
groupOpenID,
|
||||||
|
tc.wantUser,
|
||||||
|
tc.wantMember,
|
||||||
|
tc.wantGroup,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleC2CMessage_AttachmentOnlyPublishesMedia(t *testing.T) {
|
func TestHandleC2CMessage_AttachmentOnlyPublishesMedia(t *testing.T) {
|
||||||
messageBus := bus.NewMessageBus()
|
messageBus := bus.NewMessageBus()
|
||||||
store := media.NewFileMediaStore()
|
store := media.NewFileMediaStore()
|
||||||
|
|
@ -682,7 +864,7 @@ func waitInboundMessage(t *testing.T, messageBus *bus.MessageBus) bus.InboundMes
|
||||||
func writeTempFile(t *testing.T, dir, name string, content []byte) string {
|
func writeTempFile(t *testing.T, dir, name string, content []byte) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
path := dir + "/" + name
|
path := filepath.Join(dir, name)
|
||||||
if err := os.WriteFile(path, content, 0o600); err != nil {
|
if err := os.WriteFile(path, content, 0o600); err != nil {
|
||||||
t.Fatalf("WriteFile() error = %v", err)
|
t.Fatalf("WriteFile() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue