-
Notifications
You must be signed in to change notification settings - Fork 7
feat: add driver selection config & external driver import support #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -513,6 +513,57 @@ | |
|
|
||
| if data.get("message_type") == "group": | ||
| await self._on_group_message(data) | ||
| elif data.get("message_type") == "private": | ||
| await self._on_private_message(data) | ||
|
|
||
| async def _on_private_message(self, event: dict): | ||
| if event.get("user_id") == event.get("self_id"): | ||
| return | ||
|
|
||
| user_id = str(event.get("user_id", "")) | ||
| sender = event.get("sender", {}) | ||
| nickname = sender.get("nickname") or user_id | ||
| self.logger.debug( | ||
| f"NapCat [{self.instance_id}] private message from {nickname}({user_id})" | ||
| ) | ||
|
|
||
| time = event.get("time") | ||
|
|
||
| face_as_emoji: bool = self.config.cqface_mode == "emoji" | ||
| text, attachments, reply_id, mentions = await self._parse_message( | ||
| event, face_as_emoji=face_as_emoji | ||
| ) | ||
|
|
||
| if not text.strip() and not attachments: | ||
| self.logger.debug( | ||
| f"NapCat [{self.instance_id}] ignoring empty private message from {nickname}({user_id})" | ||
| ) | ||
| return | ||
|
|
||
| avatar_url = f"https://q.qlogo.cn/headimg_dl?dst_uin={user_id}&spec=640" | ||
| self_id = str(event.get("self_id", "")) | ||
| source_mentioned_self = any(str(m.get("id", "")) == self_id for m in mentions) | ||
|
|
||
| msg = NormalizedMessage( | ||
| platform=self.platform_name, | ||
| instance_id=self.instance_id, | ||
| channel={"user_id": user_id}, | ||
| nickname=nickname, | ||
| user_id=user_id, | ||
| user_avatar=avatar_url, | ||
| text=text, | ||
| attachments=attachments, | ||
| message_id=str(event.get("message_id", "")), | ||
| reply_parent=reply_id, | ||
| mentions=mentions, | ||
| source_self_id=self_id, | ||
| source_mentioned_self=source_mentioned_self, | ||
| time=datetime.datetime.fromtimestamp(time).isoformat() if time else None, | ||
| source_proxy=self._media_proxy, | ||
|
Check warning on line 562 in drivers/qq.py
|
||
| username="", | ||
| is_dm=True, | ||
| ) | ||
| await self.bridge.on_message(msg) | ||
|
|
||
| async def _on_group_message(self, event: dict): | ||
| # NapCat echoes the bot's own sent messages back as real events; | ||
|
|
@@ -818,11 +869,14 @@ | |
|
|
||
| return _DEFAULT_FORWARD_CQFACE_GIF_HOST | ||
|
|
||
| async def _upload_group_file_from_bytes( | ||
| async def _upload_file_from_bytes( | ||
| self, | ||
| data_bytes: bytes, | ||
| filename: str, | ||
| group_id: str, | ||
| target_id: str, | ||
| *, | ||
| upload_api: str = "upload_group_file", | ||
| id_key: str = "group_id", | ||
| ) -> bool: | ||
| with tempfile.NamedTemporaryFile( | ||
| prefix="nextbridge-qq-", | ||
|
|
@@ -834,17 +888,17 @@ | |
|
|
||
| try: | ||
| resp = await self._call( | ||
| "upload_group_file", | ||
| upload_api, | ||
| { | ||
| "group_id": int(group_id), | ||
| id_key: int(target_id), | ||
| "file": tmp_path, | ||
| "name": filename, | ||
| }, | ||
| ) | ||
| if resp and resp.get("status") == "ok": | ||
| return True | ||
| self.logger.warning( | ||
| f"QQ [{self.instance_id}] upload_group_file failed for '{filename}': {resp}" | ||
| f"QQ [{self.instance_id}] {upload_api} failed for '{filename}': {resp}" | ||
| ) | ||
| return False | ||
| finally: | ||
|
|
@@ -1947,6 +2001,21 @@ | |
| return str(data["message_id"]) | ||
| return None | ||
|
|
||
| async def _api_send_private_msg( | ||
| self, user_id, message, *, timeout: float = 30.0 | ||
| ) -> str | None: | ||
| """Send a private message via OneBot. Returns ``message_id`` on success or ``None``.""" | ||
| resp = await self._call( | ||
| "send_private_msg", | ||
| {"user_id": int(user_id), "message": message}, | ||
| timeout=timeout, | ||
| ) | ||
| if resp and resp.get("status") == "ok": | ||
| data = resp.get("data") or {} | ||
| if "message_id" in data: | ||
| return str(data["message_id"]) | ||
| return None | ||
|
|
||
| async def _api_get_group_member_info( | ||
| self, group_id, user_id, *, no_cache: bool = False | ||
| ) -> dict | None: | ||
|
|
@@ -2109,9 +2178,10 @@ | |
| **kwargs, | ||
| ): | ||
| group_id = channel.get("group_id") | ||
| if not group_id: | ||
| user_id = channel.get("user_id") | ||
| if not group_id and not user_id: | ||
| self.logger.warning( | ||
| f"NapCat [{self.instance_id}] send: no group_id in channel {channel}" | ||
| f"NapCat [{self.instance_id}] send: no group_id or user_id in channel {channel}" | ||
| ) | ||
| return None | ||
|
|
||
|
|
@@ -2121,6 +2191,14 @@ | |
| ) | ||
| return None | ||
|
|
||
| is_group = bool(group_id) | ||
|
Check notice on line 2194 in drivers/qq.py
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. info (maintainability): is_group = bool(group_id)
if is_group:
assert group_id is not None # for type checker only
target_id: str = str(group_id)
else:
assert user_id is not None
target_id = str(user_id)
async def _send_msg(segments):
if is_group:
return await self._api_send_group_msg(target_id, segments)
return await self._api_send_private_msg(target_id, segments)Then pass |
||
| assert group_id or user_id | ||
|
|
||
| async def _send_msg(segments): | ||
| if is_group: | ||
| return await self._api_send_group_msg(group_id, segments) | ||
| return await self._api_send_private_msg(user_id, segments) | ||
|
|
||
| segments: list[dict] = [] | ||
| msg_ids: list[str] = [] | ||
| deferred_file_uploads = [] | ||
|
|
@@ -2151,9 +2229,7 @@ | |
| else: | ||
| t, c = rich_header.get("title", ""), rich_header.get("content", "") | ||
| prefix = f"[{t}" + (f" · {c}" if c else "") + "]" | ||
| msg_id = await self._api_send_group_msg( | ||
| group_id, [{"type": "text", "data": {"text": prefix}}] | ||
| ) | ||
| msg_id = await _send_msg([{"type": "text", "data": {"text": prefix}}]) | ||
| if not msg_id: | ||
| self.logger.warning( | ||
| f"NapCat [{self.instance_id}] failed to send standalone rich header " | ||
|
|
@@ -2185,8 +2261,11 @@ | |
| segments.append( | ||
| {"type": "text", "data": {"text": text[last_idx:idx]}} | ||
| ) | ||
| # Add mention segment | ||
| segments.append({"type": "at", "data": {"qq": m["id"]}}) | ||
| # Add mention segment (converted to text in private chats) | ||
| if is_group: | ||
| segments.append({"type": "at", "data": {"qq": m["id"]}}) | ||
| else: | ||
| segments.append({"type": "text", "data": {"text": mention_str}}) | ||
| last_idx = idx + len(mention_str) | ||
|
|
||
| # Add remaining text | ||
|
|
@@ -2278,15 +2357,26 @@ | |
| data_bytes, _ = result | ||
| fname = att.name or "file" | ||
|
|
||
| async def _do_upload(d=data_bytes, fn=fname, gid=group_id): | ||
| async def _do_upload(d=data_bytes, fn=fname, is_grp=is_group): | ||
| if is_grp: | ||
| assert group_id is not None | ||
| upload_api = "upload_group_file" | ||
| id_key = "group_id" | ||
| id_val = int(group_id) | ||
| else: | ||
| assert user_id is not None | ||
| upload_api = "upload_private_file" | ||
| id_key = "user_id" | ||
| id_val = int(user_id) | ||
|
|
||
| if self._supports_stream_file_upload(): | ||
| mode = self._resolve_send_mode(len(d)) | ||
| if mode == "base64": | ||
| b64 = base64.b64encode(d).decode() | ||
| await self._call( | ||
| "upload_group_file", | ||
| upload_api, | ||
| { | ||
| "group_id": int(gid), | ||
| id_key: id_val, | ||
| "file": f"base64://{b64}", | ||
| "name": fn, | ||
| }, | ||
|
|
@@ -2295,16 +2385,15 @@ | |
| file_path = await self._upload_file_stream(d, fn) | ||
| if file_path: | ||
| await self._call( | ||
| "upload_group_file", | ||
| upload_api, | ||
| { | ||
| "group_id": int(gid), | ||
| id_key: id_val, | ||
| "file": file_path, | ||
| "name": fn, | ||
| }, | ||
| ) | ||
| else: | ||
| await self._api_send_group_msg( | ||
| gid, | ||
| await _send_msg( | ||
| [ | ||
| { | ||
| "type": "text", | ||
|
|
@@ -2315,13 +2404,14 @@ | |
| ], | ||
| ) | ||
| else: | ||
| if not await self._upload_group_file_from_bytes( | ||
| if not await self._upload_file_from_bytes( | ||
| d, | ||
| fn, | ||
| str(gid), | ||
| str(id_val), | ||
| upload_api=upload_api, | ||
| id_key=id_key, | ||
| ): | ||
| await self._api_send_group_msg( | ||
| gid, | ||
| await _send_msg( | ||
| [ | ||
| { | ||
| "type": "text", | ||
|
|
@@ -2356,16 +2446,19 @@ | |
| and standalone_segments | ||
| ): | ||
| # If only reply segment remains, attach it to the first standalone segment | ||
| standalone_segments[0] = [main_segments[0], standalone_segments[0]] # ty: ignore[invalid-assignment] | ||
| standalone_segments[0] = [ | ||
| main_segments[0], | ||
| standalone_segments[0], | ||
| ] # ty: ignore[invalid-assignment] | ||
| main_segments = [] | ||
| else: | ||
| msg_id = await self._api_send_group_msg(group_id, main_segments) | ||
| msg_id = await _send_msg(main_segments) | ||
| if msg_id: | ||
| msg_ids.append(msg_id) | ||
|
|
||
| for seg in standalone_segments: | ||
| msg_to_send = seg if isinstance(seg, list) else [seg] | ||
| msg_id = await self._api_send_group_msg(group_id, msg_to_send) | ||
| msg_id = await _send_msg(msg_to_send) | ||
| if msg_id: | ||
| msg_ids.append(msg_id) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
warning (bug_risk):
datetime.datetime.fromtimestamp(time)uses local time without a tz. Elsewhere in this codebase ISO strings are typically expected to be timezone-aware/UTC; emitting a naive local-time ISO string for DMs will be inconsistent with group messages and any downstream consumer that parses these. It also silently breaks iftimeis a string from some OneBot impls.