feat(onebot): enhance OneBot channel (#192)
* fix: change BotStatus type to json.RawMessage and add isAPIResponse function
* feat(onebot): add rich media, API callback, keepalive and voice transcription
Comprehensive improvements to the OneBot channel for better NapCatQQ
compatibility:
- Add echo-based API callback mechanism (sendAPIRequest) for
request/response correlation via pending map
- Add WebSocket ping/pong keepalive (30s ping, 60s read deadline)
- Fetch bot self ID via get_login_info on connect/reconnect
- Refactor parseMessageContentEx into parseMessageSegments supporting
image, record, video, file, reply, face, forward segments
- Add voice transcription via Groq transcriber (SetTranscriber)
- Switch to message segment array format for sending with auto reply
quote via lastMessageID tracking
- Add message_sent event handling and detailed notice event processing
(recall, poke, group increase/decrease, friend add, etc.)
- Use sync/atomic for echoCounter, optimize listen() lock pattern
- Clean up pending callbacks on Stop(), defer temp file cleanup
- Mount Groq transcriber on OneBot channel in main.go gateway
* feat(onebot): add user ID allowlist check for incoming messages
- Currently, the agent does not respond to messages sent by users outside the allowlist.
* refactor(onebot): simplify channel implementation and add emoji reaction
- onebot.go from 1179 to 980 lines (~17%)
This commit is contained in:
parent
e8afd31b28
commit
56a060ff61
2 changed files with 504 additions and 209 deletions
|
|
@ -623,6 +623,12 @@ func gatewayCmd() {
|
||||||
logger.InfoC("voice", "Groq transcription attached to Slack channel")
|
logger.InfoC("voice", "Groq transcription attached to Slack channel")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if onebotChannel, ok := channelManager.GetChannel("onebot"); ok {
|
||||||
|
if oc, ok := onebotChannel.(*channels.OneBotChannel); ok {
|
||||||
|
oc.SetTranscriber(transcriber)
|
||||||
|
logger.InfoC("voice", "Groq transcription attached to OneBot channel")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enabledChannels := channelManager.GetEnabledChannels()
|
enabledChannels := channelManager.GetEnabledChannels()
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
|
@ -14,20 +16,28 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/utils"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/voice"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OneBotChannel struct {
|
type OneBotChannel struct {
|
||||||
*BaseChannel
|
*BaseChannel
|
||||||
config config.OneBotConfig
|
config config.OneBotConfig
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
dedup map[string]struct{}
|
dedup map[string]struct{}
|
||||||
dedupRing []string
|
dedupRing []string
|
||||||
dedupIdx int
|
dedupIdx int
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
writeMu sync.Mutex
|
writeMu sync.Mutex
|
||||||
echoCounter int64
|
echoCounter int64
|
||||||
|
selfID int64
|
||||||
|
pending map[string]chan json.RawMessage
|
||||||
|
pendingMu sync.Mutex
|
||||||
|
transcriber *voice.GroqTranscriber
|
||||||
|
lastMessageID sync.Map
|
||||||
|
pendingEmojiMsg sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
type oneBotRawEvent struct {
|
type oneBotRawEvent struct {
|
||||||
|
|
@ -43,9 +53,11 @@ type oneBotRawEvent struct {
|
||||||
SelfID json.RawMessage `json:"self_id"`
|
SelfID json.RawMessage `json:"self_id"`
|
||||||
Time json.RawMessage `json:"time"`
|
Time json.RawMessage `json:"time"`
|
||||||
MetaEventType string `json:"meta_event_type"`
|
MetaEventType string `json:"meta_event_type"`
|
||||||
|
NoticeType string `json:"notice_type"`
|
||||||
Echo string `json:"echo"`
|
Echo string `json:"echo"`
|
||||||
RetCode json.RawMessage `json:"retcode"`
|
RetCode json.RawMessage `json:"retcode"`
|
||||||
Status BotStatus `json:"status"`
|
Status json.RawMessage `json:"status"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BotStatus struct {
|
type BotStatus struct {
|
||||||
|
|
@ -53,42 +65,36 @@ type BotStatus struct {
|
||||||
Good bool `json:"good"`
|
Good bool `json:"good"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isAPIResponse(raw json.RawMessage) bool {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var s string
|
||||||
|
if json.Unmarshal(raw, &s) == nil {
|
||||||
|
return s == "ok" || s == "failed"
|
||||||
|
}
|
||||||
|
var bs BotStatus
|
||||||
|
if json.Unmarshal(raw, &bs) == nil {
|
||||||
|
return bs.Online || bs.Good
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type oneBotSender struct {
|
type oneBotSender struct {
|
||||||
UserID json.RawMessage `json:"user_id"`
|
UserID json.RawMessage `json:"user_id"`
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
Card string `json:"card"`
|
Card string `json:"card"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type oneBotEvent struct {
|
|
||||||
PostType string
|
|
||||||
MessageType string
|
|
||||||
SubType string
|
|
||||||
MessageID string
|
|
||||||
UserID int64
|
|
||||||
GroupID int64
|
|
||||||
Content string
|
|
||||||
RawContent string
|
|
||||||
IsBotMentioned bool
|
|
||||||
Sender oneBotSender
|
|
||||||
SelfID int64
|
|
||||||
Time int64
|
|
||||||
MetaEventType string
|
|
||||||
}
|
|
||||||
|
|
||||||
type oneBotAPIRequest struct {
|
type oneBotAPIRequest struct {
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
Params interface{} `json:"params"`
|
Params interface{} `json:"params"`
|
||||||
Echo string `json:"echo,omitempty"`
|
Echo string `json:"echo,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type oneBotSendPrivateMsgParams struct {
|
type oneBotMessageSegment struct {
|
||||||
UserID int64 `json:"user_id"`
|
Type string `json:"type"`
|
||||||
Message string `json:"message"`
|
Data map[string]interface{} `json:"data"`
|
||||||
}
|
|
||||||
|
|
||||||
type oneBotSendGroupMsgParams struct {
|
|
||||||
GroupID int64 `json:"group_id"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOneBotChannel(cfg config.OneBotConfig, messageBus *bus.MessageBus) (*OneBotChannel, error) {
|
func NewOneBotChannel(cfg config.OneBotConfig, messageBus *bus.MessageBus) (*OneBotChannel, error) {
|
||||||
|
|
@ -101,9 +107,30 @@ func NewOneBotChannel(cfg config.OneBotConfig, messageBus *bus.MessageBus) (*One
|
||||||
dedup: make(map[string]struct{}, dedupSize),
|
dedup: make(map[string]struct{}, dedupSize),
|
||||||
dedupRing: make([]string, dedupSize),
|
dedupRing: make([]string, dedupSize),
|
||||||
dedupIdx: 0,
|
dedupIdx: 0,
|
||||||
|
pending: make(map[string]chan json.RawMessage),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *OneBotChannel) SetTranscriber(transcriber *voice.GroqTranscriber) {
|
||||||
|
c.transcriber = transcriber
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OneBotChannel) setMsgEmojiLike(messageID string, emojiID int, set bool) {
|
||||||
|
go func() {
|
||||||
|
_, err := c.sendAPIRequest("set_msg_emoji_like", map[string]interface{}{
|
||||||
|
"message_id": messageID,
|
||||||
|
"emoji_id": emojiID,
|
||||||
|
"set": set,
|
||||||
|
}, 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
logger.DebugCF("onebot", "Failed to set emoji like", map[string]interface{}{
|
||||||
|
"message_id": messageID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *OneBotChannel) Start(ctx context.Context) error {
|
func (c *OneBotChannel) Start(ctx context.Context) error {
|
||||||
if c.config.WSUrl == "" {
|
if c.config.WSUrl == "" {
|
||||||
return fmt.Errorf("OneBot ws_url not configured")
|
return fmt.Errorf("OneBot ws_url not configured")
|
||||||
|
|
@ -121,12 +148,12 @@ func (c *OneBotChannel) Start(ctx context.Context) error {
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
go c.listen()
|
go c.listen()
|
||||||
|
c.fetchSelfID()
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.config.ReconnectInterval > 0 {
|
if c.config.ReconnectInterval > 0 {
|
||||||
go c.reconnectLoop()
|
go c.reconnectLoop()
|
||||||
} else {
|
} else {
|
||||||
// If reconnect is disabled but initial connection failed, we cannot recover
|
|
||||||
if c.conn == nil {
|
if c.conn == nil {
|
||||||
return fmt.Errorf("failed to connect to OneBot and reconnect is disabled")
|
return fmt.Errorf("failed to connect to OneBot and reconnect is disabled")
|
||||||
}
|
}
|
||||||
|
|
@ -152,14 +179,141 @@ func (c *OneBotChannel) connect() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conn.SetPongHandler(func(appData string) error {
|
||||||
|
_ = conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
_ = conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||||
|
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
c.conn = conn
|
c.conn = conn
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
go c.pinger(conn)
|
||||||
|
|
||||||
logger.InfoC("onebot", "WebSocket connected")
|
logger.InfoC("onebot", "WebSocket connected")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *OneBotChannel) pinger(conn *websocket.Conn) {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
c.writeMu.Lock()
|
||||||
|
err := conn.WriteMessage(websocket.PingMessage, nil)
|
||||||
|
c.writeMu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
logger.DebugCF("onebot", "Ping write failed, stopping pinger", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OneBotChannel) fetchSelfID() {
|
||||||
|
resp, err := c.sendAPIRequest("get_login_info", nil, 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
logger.WarnCF("onebot", "Failed to get_login_info", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type loginInfo struct {
|
||||||
|
UserID json.RawMessage `json:"user_id"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
}
|
||||||
|
for _, extract := range []func() (*loginInfo, error){
|
||||||
|
func() (*loginInfo, error) {
|
||||||
|
var w struct {
|
||||||
|
Data loginInfo `json:"data"`
|
||||||
|
}
|
||||||
|
err := json.Unmarshal(resp, &w)
|
||||||
|
return &w.Data, err
|
||||||
|
},
|
||||||
|
func() (*loginInfo, error) {
|
||||||
|
var f loginInfo
|
||||||
|
err := json.Unmarshal(resp, &f)
|
||||||
|
return &f, err
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
info, err := extract()
|
||||||
|
if err != nil || len(info.UserID) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if uid, err := parseJSONInt64(info.UserID); err == nil && uid > 0 {
|
||||||
|
atomic.StoreInt64(&c.selfID, uid)
|
||||||
|
logger.InfoCF("onebot", "Bot self ID retrieved", map[string]interface{}{
|
||||||
|
"self_id": uid,
|
||||||
|
"nickname": info.Nickname,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.WarnCF("onebot", "Could not parse self ID from get_login_info response", map[string]interface{}{
|
||||||
|
"response": string(resp),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OneBotChannel) sendAPIRequest(action string, params interface{}, timeout time.Duration) (json.RawMessage, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
conn := c.conn
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if conn == nil {
|
||||||
|
return nil, fmt.Errorf("WebSocket not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
echo := fmt.Sprintf("api_%d_%d", time.Now().UnixNano(), atomic.AddInt64(&c.echoCounter, 1))
|
||||||
|
|
||||||
|
ch := make(chan json.RawMessage, 1)
|
||||||
|
c.pendingMu.Lock()
|
||||||
|
c.pending[echo] = ch
|
||||||
|
c.pendingMu.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
c.pendingMu.Lock()
|
||||||
|
delete(c.pending, echo)
|
||||||
|
c.pendingMu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
req := oneBotAPIRequest{
|
||||||
|
Action: action,
|
||||||
|
Params: params,
|
||||||
|
Echo: echo,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal API request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.writeMu.Lock()
|
||||||
|
err = conn.WriteMessage(websocket.TextMessage, data)
|
||||||
|
c.writeMu.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write API request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case resp := <-ch:
|
||||||
|
return resp, nil
|
||||||
|
case <-time.After(timeout):
|
||||||
|
return nil, fmt.Errorf("API request %s timed out after %v", action, timeout)
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return nil, fmt.Errorf("context cancelled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *OneBotChannel) reconnectLoop() {
|
func (c *OneBotChannel) reconnectLoop() {
|
||||||
interval := time.Duration(c.config.ReconnectInterval) * time.Second
|
interval := time.Duration(c.config.ReconnectInterval) * time.Second
|
||||||
if interval < 5*time.Second {
|
if interval < 5*time.Second {
|
||||||
|
|
@ -183,6 +337,7 @@ func (c *OneBotChannel) reconnectLoop() {
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
go c.listen()
|
go c.listen()
|
||||||
|
c.fetchSelfID()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -197,6 +352,13 @@ func (c *OneBotChannel) Stop(ctx context.Context) error {
|
||||||
c.cancel()
|
c.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.pendingMu.Lock()
|
||||||
|
for echo, ch := range c.pending {
|
||||||
|
close(ch)
|
||||||
|
delete(c.pending, echo)
|
||||||
|
}
|
||||||
|
c.pendingMu.Unlock()
|
||||||
|
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
if c.conn != nil {
|
if c.conn != nil {
|
||||||
c.conn.Close()
|
c.conn.Close()
|
||||||
|
|
@ -225,10 +387,7 @@ func (c *OneBotChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.writeMu.Lock()
|
echo := fmt.Sprintf("send_%d", atomic.AddInt64(&c.echoCounter, 1))
|
||||||
c.echoCounter++
|
|
||||||
echo := fmt.Sprintf("send_%d", c.echoCounter)
|
|
||||||
c.writeMu.Unlock()
|
|
||||||
|
|
||||||
req := oneBotAPIRequest{
|
req := oneBotAPIRequest{
|
||||||
Action: action,
|
Action: action,
|
||||||
|
|
@ -252,67 +411,78 @@ func (c *OneBotChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if msgID, ok := c.pendingEmojiMsg.LoadAndDelete(msg.ChatID); ok {
|
||||||
|
if mid, ok := msgID.(string); ok && mid != "" {
|
||||||
|
c.setMsgEmojiLike(mid, 289, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *OneBotChannel) buildMessageSegments(chatID, content string) []oneBotMessageSegment {
|
||||||
|
var segments []oneBotMessageSegment
|
||||||
|
|
||||||
|
if lastMsgID, ok := c.lastMessageID.Load(chatID); ok {
|
||||||
|
if msgID, ok := lastMsgID.(string); ok && msgID != "" {
|
||||||
|
segments = append(segments, oneBotMessageSegment{
|
||||||
|
Type: "reply",
|
||||||
|
Data: map[string]interface{}{"id": msgID},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
segments = append(segments, oneBotMessageSegment{
|
||||||
|
Type: "text",
|
||||||
|
Data: map[string]interface{}{"text": content},
|
||||||
|
})
|
||||||
|
|
||||||
|
return segments
|
||||||
|
}
|
||||||
|
|
||||||
func (c *OneBotChannel) buildSendRequest(msg bus.OutboundMessage) (string, interface{}, error) {
|
func (c *OneBotChannel) buildSendRequest(msg bus.OutboundMessage) (string, interface{}, error) {
|
||||||
chatID := msg.ChatID
|
chatID := msg.ChatID
|
||||||
|
segments := c.buildMessageSegments(chatID, msg.Content)
|
||||||
|
|
||||||
if len(chatID) > 6 && chatID[:6] == "group:" {
|
var action, idKey string
|
||||||
groupID, err := strconv.ParseInt(chatID[6:], 10, 64)
|
var rawID string
|
||||||
if err != nil {
|
if rest, ok := strings.CutPrefix(chatID, "group:"); ok {
|
||||||
return "", nil, fmt.Errorf("invalid group ID in chatID: %s", chatID)
|
action, idKey, rawID = "send_group_msg", "group_id", rest
|
||||||
}
|
} else if rest, ok := strings.CutPrefix(chatID, "private:"); ok {
|
||||||
return "send_group_msg", oneBotSendGroupMsgParams{
|
action, idKey, rawID = "send_private_msg", "user_id", rest
|
||||||
GroupID: groupID,
|
} else {
|
||||||
Message: msg.Content,
|
action, idKey, rawID = "send_private_msg", "user_id", chatID
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(chatID) > 8 && chatID[:8] == "private:" {
|
id, err := strconv.ParseInt(rawID, 10, 64)
|
||||||
userID, err := strconv.ParseInt(chatID[8:], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, fmt.Errorf("invalid user ID in chatID: %s", chatID)
|
|
||||||
}
|
|
||||||
return "send_private_msg", oneBotSendPrivateMsgParams{
|
|
||||||
UserID: userID,
|
|
||||||
Message: msg.Content,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, err := strconv.ParseInt(chatID, 10, 64)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("invalid chatID for OneBot: %s", chatID)
|
return "", nil, fmt.Errorf("invalid %s in chatID: %s", idKey, chatID)
|
||||||
}
|
}
|
||||||
|
return action, map[string]interface{}{idKey: id, "message": segments}, nil
|
||||||
return "send_private_msg", oneBotSendPrivateMsgParams{
|
|
||||||
UserID: userID,
|
|
||||||
Message: msg.Content,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *OneBotChannel) listen() {
|
func (c *OneBotChannel) listen() {
|
||||||
|
c.mu.Lock()
|
||||||
|
conn := c.conn
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if conn == nil {
|
||||||
|
logger.WarnC("onebot", "WebSocket connection is nil, listener exiting")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-c.ctx.Done():
|
case <-c.ctx.Done():
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
c.mu.Lock()
|
|
||||||
conn := c.conn
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
if conn == nil {
|
|
||||||
logger.WarnC("onebot", "WebSocket connection is nil, listener exiting")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, message, err := conn.ReadMessage()
|
_, message, err := conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorCF("onebot", "WebSocket read error", map[string]interface{}{
|
logger.ErrorCF("onebot", "WebSocket read error", map[string]interface{}{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
if c.conn != nil {
|
if c.conn == conn {
|
||||||
c.conn.Close()
|
c.conn.Close()
|
||||||
c.conn = nil
|
c.conn = nil
|
||||||
}
|
}
|
||||||
|
|
@ -320,10 +490,7 @@ func (c *OneBotChannel) listen() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("onebot", "Raw WebSocket message received", map[string]interface{}{
|
_ = conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||||
"length": len(message),
|
|
||||||
"payload": string(message),
|
|
||||||
})
|
|
||||||
|
|
||||||
var raw oneBotRawEvent
|
var raw oneBotRawEvent
|
||||||
if err := json.Unmarshal(message, &raw); err != nil {
|
if err := json.Unmarshal(message, &raw); err != nil {
|
||||||
|
|
@ -334,20 +501,37 @@ func (c *OneBotChannel) listen() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if raw.Echo != "" || raw.Status.Online || raw.Status.Good {
|
logger.DebugCF("onebot", "WebSocket event", map[string]interface{}{
|
||||||
logger.DebugCF("onebot", "Received API response, skipping", map[string]interface{}{
|
"length": len(message),
|
||||||
"echo": raw.Echo,
|
"post_type": raw.PostType,
|
||||||
"status": raw.Status,
|
"sub_type": raw.SubType,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if raw.Echo != "" {
|
||||||
|
c.pendingMu.Lock()
|
||||||
|
ch, ok := c.pending[raw.Echo]
|
||||||
|
c.pendingMu.Unlock()
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
select {
|
||||||
|
case ch <- message:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.DebugCF("onebot", "Received API response (no waiter)", map[string]interface{}{
|
||||||
|
"echo": raw.Echo,
|
||||||
|
"status": string(raw.Status),
|
||||||
|
})
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("onebot", "Parsed raw event", map[string]interface{}{
|
if isAPIResponse(raw.Status) {
|
||||||
"post_type": raw.PostType,
|
logger.DebugCF("onebot", "Received API response without echo, skipping", map[string]interface{}{
|
||||||
"message_type": raw.MessageType,
|
"status": string(raw.Status),
|
||||||
"sub_type": raw.SubType,
|
})
|
||||||
"meta_event_type": raw.MetaEventType,
|
continue
|
||||||
})
|
}
|
||||||
|
|
||||||
c.handleRawEvent(&raw)
|
c.handleRawEvent(&raw)
|
||||||
}
|
}
|
||||||
|
|
@ -386,9 +570,12 @@ func parseJSONString(raw json.RawMessage) string {
|
||||||
type parseMessageResult struct {
|
type parseMessageResult struct {
|
||||||
Text string
|
Text string
|
||||||
IsBotMentioned bool
|
IsBotMentioned bool
|
||||||
|
Media []string
|
||||||
|
LocalFiles []string
|
||||||
|
ReplyTo string
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseMessageContentEx(raw json.RawMessage, selfID int64) parseMessageResult {
|
func (c *OneBotChannel) parseMessageSegments(raw json.RawMessage, selfID int64) parseMessageResult {
|
||||||
if len(raw) == 0 {
|
if len(raw) == 0 {
|
||||||
return parseMessageResult{}
|
return parseMessageResult{}
|
||||||
}
|
}
|
||||||
|
|
@ -408,60 +595,155 @@ func parseMessageContentEx(raw json.RawMessage, selfID int64) parseMessageResult
|
||||||
}
|
}
|
||||||
|
|
||||||
var segments []map[string]interface{}
|
var segments []map[string]interface{}
|
||||||
if err := json.Unmarshal(raw, &segments); err == nil {
|
if err := json.Unmarshal(raw, &segments); err != nil {
|
||||||
var text string
|
return parseMessageResult{}
|
||||||
mentioned := false
|
}
|
||||||
selfIDStr := strconv.FormatInt(selfID, 10)
|
|
||||||
for _, seg := range segments {
|
var textParts []string
|
||||||
segType, _ := seg["type"].(string)
|
mentioned := false
|
||||||
data, _ := seg["data"].(map[string]interface{})
|
selfIDStr := strconv.FormatInt(selfID, 10)
|
||||||
switch segType {
|
var media []string
|
||||||
case "text":
|
var localFiles []string
|
||||||
if data != nil {
|
var replyTo string
|
||||||
if t, ok := data["text"].(string); ok {
|
|
||||||
text += t
|
for _, seg := range segments {
|
||||||
}
|
segType, _ := seg["type"].(string)
|
||||||
|
data, _ := seg["data"].(map[string]interface{})
|
||||||
|
|
||||||
|
switch segType {
|
||||||
|
case "text":
|
||||||
|
if data != nil {
|
||||||
|
if t, ok := data["text"].(string); ok {
|
||||||
|
textParts = append(textParts, t)
|
||||||
}
|
}
|
||||||
case "at":
|
}
|
||||||
if data != nil && selfID > 0 {
|
|
||||||
qqVal := fmt.Sprintf("%v", data["qq"])
|
case "at":
|
||||||
if qqVal == selfIDStr || qqVal == "all" {
|
if data != nil && selfID > 0 {
|
||||||
mentioned = true
|
qqVal := fmt.Sprintf("%v", data["qq"])
|
||||||
|
if qqVal == selfIDStr || qqVal == "all" {
|
||||||
|
mentioned = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "image", "video", "file":
|
||||||
|
if data != nil {
|
||||||
|
url, _ := data["url"].(string)
|
||||||
|
if url != "" {
|
||||||
|
defaults := map[string]string{"image": "image.jpg", "video": "video.mp4", "file": "file"}
|
||||||
|
filename := defaults[segType]
|
||||||
|
if f, ok := data["file"].(string); ok && f != "" {
|
||||||
|
filename = f
|
||||||
|
} else if n, ok := data["name"].(string); ok && n != "" {
|
||||||
|
filename = n
|
||||||
|
}
|
||||||
|
localPath := utils.DownloadFile(url, filename, utils.DownloadOptions{
|
||||||
|
LoggerPrefix: "onebot",
|
||||||
|
})
|
||||||
|
if localPath != "" {
|
||||||
|
media = append(media, localPath)
|
||||||
|
localFiles = append(localFiles, localPath)
|
||||||
|
textParts = append(textParts, fmt.Sprintf("[%s]", segType))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "record":
|
||||||
|
if data != nil {
|
||||||
|
url, _ := data["url"].(string)
|
||||||
|
if url != "" {
|
||||||
|
localPath := utils.DownloadFile(url, "voice.amr", utils.DownloadOptions{
|
||||||
|
LoggerPrefix: "onebot",
|
||||||
|
})
|
||||||
|
if localPath != "" {
|
||||||
|
localFiles = append(localFiles, localPath)
|
||||||
|
if c.transcriber != nil && c.transcriber.IsAvailable() {
|
||||||
|
tctx, tcancel := context.WithTimeout(c.ctx, 30*time.Second)
|
||||||
|
result, err := c.transcriber.Transcribe(tctx, localPath)
|
||||||
|
tcancel()
|
||||||
|
if err != nil {
|
||||||
|
logger.WarnCF("onebot", "Voice transcription failed", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
textParts = append(textParts, "[voice (transcription failed)]")
|
||||||
|
media = append(media, localPath)
|
||||||
|
} else {
|
||||||
|
textParts = append(textParts, fmt.Sprintf("[voice transcription: %s]", result.Text))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
textParts = append(textParts, "[voice]")
|
||||||
|
media = append(media, localPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "reply":
|
||||||
|
if data != nil {
|
||||||
|
if id, ok := data["id"]; ok {
|
||||||
|
replyTo = fmt.Sprintf("%v", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "face":
|
||||||
|
if data != nil {
|
||||||
|
faceID, _ := data["id"]
|
||||||
|
textParts = append(textParts, fmt.Sprintf("[face:%v]", faceID))
|
||||||
|
}
|
||||||
|
|
||||||
|
case "forward":
|
||||||
|
textParts = append(textParts, "[forward message]")
|
||||||
|
|
||||||
|
default:
|
||||||
|
|
||||||
}
|
}
|
||||||
return parseMessageResult{Text: strings.TrimSpace(text), IsBotMentioned: mentioned}
|
|
||||||
}
|
}
|
||||||
return parseMessageResult{}
|
|
||||||
|
return parseMessageResult{
|
||||||
|
Text: strings.TrimSpace(strings.Join(textParts, "")),
|
||||||
|
IsBotMentioned: mentioned,
|
||||||
|
Media: media,
|
||||||
|
LocalFiles: localFiles,
|
||||||
|
ReplyTo: replyTo,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *OneBotChannel) handleRawEvent(raw *oneBotRawEvent) {
|
func (c *OneBotChannel) handleRawEvent(raw *oneBotRawEvent) {
|
||||||
switch raw.PostType {
|
switch raw.PostType {
|
||||||
case "message":
|
case "message":
|
||||||
evt, err := c.normalizeMessageEvent(raw)
|
if userID, err := parseJSONInt64(raw.UserID); err == nil && userID > 0 {
|
||||||
if err != nil {
|
if !c.IsAllowed(strconv.FormatInt(userID, 10)) {
|
||||||
logger.WarnCF("onebot", "Failed to normalize message event", map[string]interface{}{
|
logger.DebugCF("onebot", "Message rejected by allowlist", map[string]interface{}{
|
||||||
"error": err.Error(),
|
"user_id": userID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
c.handleMessage(evt)
|
c.handleMessage(raw)
|
||||||
|
|
||||||
|
case "message_sent":
|
||||||
|
logger.DebugCF("onebot", "Bot sent message event", map[string]interface{}{
|
||||||
|
"message_type": raw.MessageType,
|
||||||
|
"message_id": parseJSONString(raw.MessageID),
|
||||||
|
})
|
||||||
|
|
||||||
case "meta_event":
|
case "meta_event":
|
||||||
c.handleMetaEvent(raw)
|
c.handleMetaEvent(raw)
|
||||||
|
|
||||||
case "notice":
|
case "notice":
|
||||||
logger.DebugCF("onebot", "Notice event received", map[string]interface{}{
|
c.handleNoticeEvent(raw)
|
||||||
"sub_type": raw.SubType,
|
|
||||||
})
|
|
||||||
case "request":
|
case "request":
|
||||||
logger.DebugCF("onebot", "Request event received", map[string]interface{}{
|
logger.DebugCF("onebot", "Request event received", map[string]interface{}{
|
||||||
"sub_type": raw.SubType,
|
"sub_type": raw.SubType,
|
||||||
})
|
})
|
||||||
|
|
||||||
case "":
|
case "":
|
||||||
logger.DebugCF("onebot", "Event with empty post_type (possibly API response)", map[string]interface{}{
|
logger.DebugCF("onebot", "Event with empty post_type (possibly API response)", map[string]interface{}{
|
||||||
"echo": raw.Echo,
|
"echo": raw.Echo,
|
||||||
"status": raw.Status,
|
"status": raw.Status,
|
||||||
})
|
})
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.DebugCF("onebot", "Unknown post_type", map[string]interface{}{
|
logger.DebugCF("onebot", "Unknown post_type", map[string]interface{}{
|
||||||
"post_type": raw.PostType,
|
"post_type": raw.PostType,
|
||||||
|
|
@ -469,18 +751,51 @@ func (c *OneBotChannel) handleRawEvent(raw *oneBotRawEvent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *OneBotChannel) normalizeMessageEvent(raw *oneBotRawEvent) (*oneBotEvent, error) {
|
func (c *OneBotChannel) handleMetaEvent(raw *oneBotRawEvent) {
|
||||||
|
if raw.MetaEventType == "lifecycle" {
|
||||||
|
logger.InfoCF("onebot", "Lifecycle event", map[string]interface{}{"sub_type": raw.SubType})
|
||||||
|
} else if raw.MetaEventType != "heartbeat" {
|
||||||
|
logger.DebugCF("onebot", "Meta event: "+raw.MetaEventType, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OneBotChannel) handleNoticeEvent(raw *oneBotRawEvent) {
|
||||||
|
fields := map[string]interface{}{
|
||||||
|
"notice_type": raw.NoticeType,
|
||||||
|
"sub_type": raw.SubType,
|
||||||
|
"group_id": parseJSONString(raw.GroupID),
|
||||||
|
"user_id": parseJSONString(raw.UserID),
|
||||||
|
"message_id": parseJSONString(raw.MessageID),
|
||||||
|
}
|
||||||
|
switch raw.NoticeType {
|
||||||
|
case "group_recall", "group_increase", "group_decrease",
|
||||||
|
"friend_add", "group_admin", "group_ban":
|
||||||
|
logger.InfoCF("onebot", "Notice: "+raw.NoticeType, fields)
|
||||||
|
default:
|
||||||
|
logger.DebugCF("onebot", "Notice: "+raw.NoticeType, fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OneBotChannel) handleMessage(raw *oneBotRawEvent) {
|
||||||
|
// Parse fields from raw event
|
||||||
userID, err := parseJSONInt64(raw.UserID)
|
userID, err := parseJSONInt64(raw.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse user_id: %w (raw: %s)", err, string(raw.UserID))
|
logger.WarnCF("onebot", "Failed to parse user_id", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
"raw": string(raw.UserID),
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
groupID, _ := parseJSONInt64(raw.GroupID)
|
groupID, _ := parseJSONInt64(raw.GroupID)
|
||||||
selfID, _ := parseJSONInt64(raw.SelfID)
|
selfID, _ := parseJSONInt64(raw.SelfID)
|
||||||
ts, _ := parseJSONInt64(raw.Time)
|
|
||||||
messageID := parseJSONString(raw.MessageID)
|
messageID := parseJSONString(raw.MessageID)
|
||||||
|
|
||||||
parsed := parseMessageContentEx(raw.Message, selfID)
|
if selfID == 0 {
|
||||||
|
selfID = atomic.LoadInt64(&c.selfID)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed := c.parseMessageSegments(raw.Message, selfID)
|
||||||
isBotMentioned := parsed.IsBotMentioned
|
isBotMentioned := parsed.IsBotMentioned
|
||||||
|
|
||||||
content := raw.RawMessage
|
content := raw.RawMessage
|
||||||
|
|
@ -495,6 +810,10 @@ func (c *OneBotChannel) normalizeMessageEvent(raw *oneBotRawEvent) (*oneBotEvent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if parsed.Text != "" && content != parsed.Text && (len(parsed.Media) > 0 || parsed.ReplyTo != "") {
|
||||||
|
content = parsed.Text
|
||||||
|
}
|
||||||
|
|
||||||
var sender oneBotSender
|
var sender oneBotSender
|
||||||
if len(raw.Sender) > 0 {
|
if len(raw.Sender) > 0 {
|
||||||
if err := json.Unmarshal(raw.Sender, &sender); err != nil {
|
if err := json.Unmarshal(raw.Sender, &sender); err != nil {
|
||||||
|
|
@ -505,137 +824,107 @@ func (c *OneBotChannel) normalizeMessageEvent(raw *oneBotRawEvent) (*oneBotEvent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.DebugCF("onebot", "Normalized message event", map[string]interface{}{
|
// Clean up temp files when done
|
||||||
"message_type": raw.MessageType,
|
if len(parsed.LocalFiles) > 0 {
|
||||||
"user_id": userID,
|
defer func() {
|
||||||
"group_id": groupID,
|
for _, f := range parsed.LocalFiles {
|
||||||
"message_id": messageID,
|
if err := os.Remove(f); err != nil {
|
||||||
"content_len": len(content),
|
logger.DebugCF("onebot", "Failed to remove temp file", map[string]interface{}{
|
||||||
"nickname": sender.Nickname,
|
"path": f,
|
||||||
})
|
"error": err.Error(),
|
||||||
|
})
|
||||||
return &oneBotEvent{
|
}
|
||||||
PostType: raw.PostType,
|
}
|
||||||
MessageType: raw.MessageType,
|
}()
|
||||||
SubType: raw.SubType,
|
|
||||||
MessageID: messageID,
|
|
||||||
UserID: userID,
|
|
||||||
GroupID: groupID,
|
|
||||||
Content: content,
|
|
||||||
RawContent: raw.RawMessage,
|
|
||||||
IsBotMentioned: isBotMentioned,
|
|
||||||
Sender: sender,
|
|
||||||
SelfID: selfID,
|
|
||||||
Time: ts,
|
|
||||||
MetaEventType: raw.MetaEventType,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *OneBotChannel) handleMetaEvent(raw *oneBotRawEvent) {
|
|
||||||
switch raw.MetaEventType {
|
|
||||||
case "lifecycle":
|
|
||||||
logger.InfoCF("onebot", "Lifecycle event", map[string]interface{}{
|
|
||||||
"sub_type": raw.SubType,
|
|
||||||
})
|
|
||||||
case "heartbeat":
|
|
||||||
logger.DebugC("onebot", "Heartbeat received")
|
|
||||||
default:
|
|
||||||
logger.DebugCF("onebot", "Unknown meta_event_type", map[string]interface{}{
|
|
||||||
"meta_event_type": raw.MetaEventType,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (c *OneBotChannel) handleMessage(evt *oneBotEvent) {
|
if c.isDuplicate(messageID) {
|
||||||
if c.isDuplicate(evt.MessageID) {
|
|
||||||
logger.DebugCF("onebot", "Duplicate message, skipping", map[string]interface{}{
|
logger.DebugCF("onebot", "Duplicate message, skipping", map[string]interface{}{
|
||||||
"message_id": evt.MessageID,
|
"message_id": messageID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
content := evt.Content
|
|
||||||
if content == "" {
|
if content == "" {
|
||||||
logger.DebugCF("onebot", "Received empty message, ignoring", map[string]interface{}{
|
logger.DebugCF("onebot", "Received empty message, ignoring", map[string]interface{}{
|
||||||
"message_id": evt.MessageID,
|
"message_id": messageID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
senderID := strconv.FormatInt(evt.UserID, 10)
|
senderID := strconv.FormatInt(userID, 10)
|
||||||
var chatID string
|
var chatID string
|
||||||
|
|
||||||
metadata := map[string]string{
|
metadata := map[string]string{
|
||||||
"message_id": evt.MessageID,
|
"message_id": messageID,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch evt.MessageType {
|
if parsed.ReplyTo != "" {
|
||||||
|
metadata["reply_to_message_id"] = parsed.ReplyTo
|
||||||
|
}
|
||||||
|
|
||||||
|
switch raw.MessageType {
|
||||||
case "private":
|
case "private":
|
||||||
chatID = "private:" + senderID
|
chatID = "private:" + senderID
|
||||||
logger.InfoCF("onebot", "Received private message", map[string]interface{}{
|
|
||||||
"sender": senderID,
|
|
||||||
"message_id": evt.MessageID,
|
|
||||||
"length": len(content),
|
|
||||||
"content": truncate(content, 100),
|
|
||||||
})
|
|
||||||
|
|
||||||
case "group":
|
case "group":
|
||||||
groupIDStr := strconv.FormatInt(evt.GroupID, 10)
|
groupIDStr := strconv.FormatInt(groupID, 10)
|
||||||
chatID = "group:" + groupIDStr
|
chatID = "group:" + groupIDStr
|
||||||
metadata["group_id"] = groupIDStr
|
metadata["group_id"] = groupIDStr
|
||||||
|
|
||||||
senderUserID, _ := parseJSONInt64(evt.Sender.UserID)
|
senderUserID, _ := parseJSONInt64(sender.UserID)
|
||||||
if senderUserID > 0 {
|
if senderUserID > 0 {
|
||||||
metadata["sender_user_id"] = strconv.FormatInt(senderUserID, 10)
|
metadata["sender_user_id"] = strconv.FormatInt(senderUserID, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
if evt.Sender.Card != "" {
|
if sender.Card != "" {
|
||||||
metadata["sender_name"] = evt.Sender.Card
|
metadata["sender_name"] = sender.Card
|
||||||
} else if evt.Sender.Nickname != "" {
|
} else if sender.Nickname != "" {
|
||||||
metadata["sender_name"] = evt.Sender.Nickname
|
metadata["sender_name"] = sender.Nickname
|
||||||
}
|
}
|
||||||
|
|
||||||
triggered, strippedContent := c.checkGroupTrigger(content, evt.IsBotMentioned)
|
triggered, strippedContent := c.checkGroupTrigger(content, isBotMentioned)
|
||||||
if !triggered {
|
if !triggered {
|
||||||
logger.DebugCF("onebot", "Group message ignored (no trigger)", map[string]interface{}{
|
logger.DebugCF("onebot", "Group message ignored (no trigger)", map[string]interface{}{
|
||||||
"sender": senderID,
|
"sender": senderID,
|
||||||
"group": groupIDStr,
|
"group": groupIDStr,
|
||||||
"is_mentioned": evt.IsBotMentioned,
|
"is_mentioned": isBotMentioned,
|
||||||
"content": truncate(content, 100),
|
"content": truncate(content, 100),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
content = strippedContent
|
content = strippedContent
|
||||||
|
|
||||||
logger.InfoCF("onebot", "Received group message", map[string]interface{}{
|
|
||||||
"sender": senderID,
|
|
||||||
"group": groupIDStr,
|
|
||||||
"message_id": evt.MessageID,
|
|
||||||
"is_mentioned": evt.IsBotMentioned,
|
|
||||||
"length": len(content),
|
|
||||||
"content": truncate(content, 100),
|
|
||||||
})
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.WarnCF("onebot", "Unknown message type, cannot route", map[string]interface{}{
|
logger.WarnCF("onebot", "Unknown message type, cannot route", map[string]interface{}{
|
||||||
"type": evt.MessageType,
|
"type": raw.MessageType,
|
||||||
"message_id": evt.MessageID,
|
"message_id": messageID,
|
||||||
"user_id": evt.UserID,
|
"user_id": userID,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if evt.Sender.Nickname != "" {
|
logger.InfoCF("onebot", "Received "+raw.MessageType+" message", map[string]interface{}{
|
||||||
metadata["nickname"] = evt.Sender.Nickname
|
"sender": senderID,
|
||||||
}
|
"chat_id": chatID,
|
||||||
|
"message_id": messageID,
|
||||||
logger.DebugCF("onebot", "Forwarding message to bus", map[string]interface{}{
|
"length": len(content),
|
||||||
"sender_id": senderID,
|
"content": truncate(content, 100),
|
||||||
"chat_id": chatID,
|
"media_count": len(parsed.Media),
|
||||||
"content": truncate(content, 100),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
c.HandleMessage(senderID, chatID, content, []string{}, metadata)
|
if sender.Nickname != "" {
|
||||||
|
metadata["nickname"] = sender.Nickname
|
||||||
|
}
|
||||||
|
|
||||||
|
c.lastMessageID.Store(chatID, messageID)
|
||||||
|
|
||||||
|
if raw.MessageType == "group" && messageID != "" && messageID != "0" {
|
||||||
|
c.setMsgEmojiLike(messageID, 289, true)
|
||||||
|
c.pendingEmojiMsg.Store(chatID, messageID)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HandleMessage(senderID, chatID, content, parsed.Media, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *OneBotChannel) isDuplicate(messageID string) bool {
|
func (c *OneBotChannel) isDuplicate(messageID string) bool {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue