update: Enable listening to both private and group messages.
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Dict
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -13,20 +13,22 @@ from nanobot.config.schema import QQConfig
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import botpy
|
import botpy
|
||||||
from botpy.message import C2CMessage
|
from botpy.message import C2CMessage, GroupMessage # 1. Import GroupMessage
|
||||||
|
|
||||||
QQ_AVAILABLE = True
|
QQ_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
QQ_AVAILABLE = False
|
QQ_AVAILABLE = False
|
||||||
botpy = None
|
botpy = None
|
||||||
C2CMessage = None
|
C2CMessage = None
|
||||||
|
GroupMessage = None
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from botpy.message import C2CMessage
|
from botpy.message import C2CMessage, GroupMessage
|
||||||
|
|
||||||
|
|
||||||
def _make_bot_class(channel: "QQChannel") -> "type[botpy.Client]":
|
def _make_bot_class(channel: "QQChannel") -> "type[botpy.Client]":
|
||||||
"""Create a botpy Client subclass bound to the given channel."""
|
"""Create a botpy Client subclass bound to the given channel."""
|
||||||
|
# 2. Ensure intents enable public_messages (required for group messages)
|
||||||
intents = botpy.Intents(public_messages=True, direct_message=True)
|
intents = botpy.Intents(public_messages=True, direct_message=True)
|
||||||
|
|
||||||
class _Bot(botpy.Client):
|
class _Bot(botpy.Client):
|
||||||
@@ -37,10 +39,17 @@ def _make_bot_class(channel: "QQChannel") -> "type[botpy.Client]":
|
|||||||
logger.info(f"QQ bot ready: {self.robot.name}")
|
logger.info(f"QQ bot ready: {self.robot.name}")
|
||||||
|
|
||||||
async def on_c2c_message_create(self, message: "C2CMessage"):
|
async def on_c2c_message_create(self, message: "C2CMessage"):
|
||||||
await channel._on_message(message)
|
# C2C (Private) message
|
||||||
|
await channel._on_message(message, is_group=False)
|
||||||
|
|
||||||
|
async def on_group_at_message_create(self, message: "GroupMessage"):
|
||||||
|
# 3. Added: Listen for group @messages
|
||||||
|
# Note: Official bots only receive messages @mentioning them unless privileged
|
||||||
|
await channel._on_message(message, is_group=True)
|
||||||
|
|
||||||
async def on_direct_message_create(self, message):
|
async def on_direct_message_create(self, message):
|
||||||
await channel._on_message(message)
|
# Guild Direct Message
|
||||||
|
await channel._on_message(message, is_group=False)
|
||||||
|
|
||||||
return _Bot
|
return _Bot
|
||||||
|
|
||||||
@@ -56,6 +65,9 @@ class QQChannel(BaseChannel):
|
|||||||
self._client: "botpy.Client | None" = None
|
self._client: "botpy.Client | None" = None
|
||||||
self._processed_ids: deque = deque(maxlen=1000)
|
self._processed_ids: deque = deque(maxlen=1000)
|
||||||
self._bot_task: asyncio.Task | None = None
|
self._bot_task: asyncio.Task | None = None
|
||||||
|
# Cache to track if chat_id is a group or individual to select the correct reply API
|
||||||
|
# Format: {chat_id: "group" | "c2c"}
|
||||||
|
self._chat_type_cache: Dict[str, str] = {}
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
"""Start the QQ bot."""
|
"""Start the QQ bot."""
|
||||||
@@ -72,14 +84,14 @@ class QQChannel(BaseChannel):
|
|||||||
self._client = BotClass()
|
self._client = BotClass()
|
||||||
|
|
||||||
self._bot_task = asyncio.create_task(self._run_bot())
|
self._bot_task = asyncio.create_task(self._run_bot())
|
||||||
logger.info("QQ bot started (C2C private message)")
|
logger.info("QQ bot started (C2C & Group supported)")
|
||||||
|
|
||||||
async def _run_bot(self) -> None:
|
async def _run_bot(self) -> None:
|
||||||
"""Run the bot connection."""
|
"""Run the bot connection."""
|
||||||
try:
|
try:
|
||||||
await self._client.start(appid=self.config.app_id, secret=self.config.secret)
|
await self._client.start(appid=self.config.app_id, secret=self.config.secret)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"QQ auth failed, check AppID/Secret at q.qq.com: {e}")
|
logger.error(f"QQ auth failed: {e}")
|
||||||
self._running = False
|
self._running = False
|
||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
@@ -98,16 +110,31 @@ class QQChannel(BaseChannel):
|
|||||||
if not self._client:
|
if not self._client:
|
||||||
logger.warning("QQ client not initialized")
|
logger.warning("QQ client not initialized")
|
||||||
return
|
return
|
||||||
try:
|
|
||||||
await self._client.api.post_c2c_message(
|
# 4. Modified send logic: Check chat_id type to call the correct API
|
||||||
openid=msg.chat_id,
|
msg_type = self._chat_type_cache.get(msg.chat_id, "c2c") # Default to c2c
|
||||||
msg_type=0,
|
|
||||||
content=msg.content,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error sending QQ message: {e}")
|
|
||||||
|
|
||||||
async def _on_message(self, data: "C2CMessage") -> None:
|
try:
|
||||||
|
if msg_type == "group":
|
||||||
|
# Send group message
|
||||||
|
await self._client.api.post_group_message(
|
||||||
|
group_openid=msg.chat_id,
|
||||||
|
msg_type=0,
|
||||||
|
msg_id=msg.metadata.get("message_id"), # Reply to specific message ID (optional but recommended)
|
||||||
|
content=msg.content
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Send C2C (private) message
|
||||||
|
await self._client.api.post_c2c_message(
|
||||||
|
openid=msg.chat_id,
|
||||||
|
msg_type=0,
|
||||||
|
msg_id=msg.metadata.get("message_id"),
|
||||||
|
content=msg.content,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error sending QQ message ({msg_type}): {e}")
|
||||||
|
|
||||||
|
async def _on_message(self, data: "C2CMessage | GroupMessage", is_group: bool = False) -> None:
|
||||||
"""Handle incoming message from QQ."""
|
"""Handle incoming message from QQ."""
|
||||||
try:
|
try:
|
||||||
# Dedup by message ID
|
# Dedup by message ID
|
||||||
@@ -115,17 +142,30 @@ class QQChannel(BaseChannel):
|
|||||||
return
|
return
|
||||||
self._processed_ids.append(data.id)
|
self._processed_ids.append(data.id)
|
||||||
|
|
||||||
author = data.author
|
|
||||||
user_id = str(getattr(author, 'id', None) or getattr(author, 'user_openid', 'unknown'))
|
|
||||||
content = (data.content or "").strip()
|
content = (data.content or "").strip()
|
||||||
if not content:
|
if not content:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 5. Extract ID and cache type
|
||||||
|
if is_group:
|
||||||
|
# Group message: chat_id uses group_openid
|
||||||
|
chat_id = data.group_openid
|
||||||
|
user_id = data.author.member_openid # Sender's ID
|
||||||
|
self._chat_type_cache[chat_id] = "group"
|
||||||
|
|
||||||
|
# Remove @bot text (optional, prevents Nanobot from treating the name as prompt)
|
||||||
|
# content = content.replace("@BotName", "").strip()
|
||||||
|
else:
|
||||||
|
# Private message: chat_id uses user_openid
|
||||||
|
chat_id = str(getattr(data.author, 'id', None) or getattr(data.author, 'user_openid', 'unknown'))
|
||||||
|
user_id = chat_id
|
||||||
|
self._chat_type_cache[chat_id] = "c2c"
|
||||||
|
|
||||||
await self._handle_message(
|
await self._handle_message(
|
||||||
sender_id=user_id,
|
sender_id=user_id,
|
||||||
chat_id=user_id,
|
chat_id=chat_id,
|
||||||
content=content,
|
content=content,
|
||||||
metadata={"message_id": data.id},
|
metadata={"message_id": data.id},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error handling QQ message: {e}")
|
logger.error(f"Error handling QQ message: {e}")
|
||||||
Reference in New Issue
Block a user