Skip to content

fix: guard private OneBot deliveries - #10181

Open
94yi wants to merge 1 commit into
AstrBotDevs:masterfrom
94yi:fix/aiocqhttp-private-delivery-safety
Open

94yi wants to merge 1 commit into
AstrBotDevs:masterfrom
94yi:fix/aiocqhttp-private-delivery-safety

Conversation

@94yi

@94yi 94yi commented Sep 21, 2026

Copy link
Copy Markdown

变更\n- 为 OneBot 私聊发送增加投递登记、回执状态和熔断保护\n- 在事件停止或取消后阻止后续发送\n- 跟进任务增加终止检查和等待超时\n- 为插件上下文页面提供可选的逐条投递审计数据\n\n## 验证\n- Ruff 检查通过\n- 16 项投递安全和跟进终止测试通过

Summary by Sourcery

Guard private OneBot message delivery with persistent safety controls and terminate queued follow-ups when conversations stop.

New Features:

  • Add persistent audit records, receipts, and operator-visible pagination for private OneBot deliveries, including redacted request parameters and inbound events.

Bug Fixes:

  • Prevent private delivery bursts and repeated messages from continuing after safety limits are exceeded.
  • Stop deliveries when events are cancelled or stopped, including during segmented and separate response sends.
  • Terminate unresolved follow-up turns when a session is stopped and prevent them from waiting indefinitely.

Enhancements:

  • Add atomic private-send reservations, circuit-breaker protection, pre-send checks, and handling for uncertain network outcomes.

Tests:

  • Add regression coverage for private delivery limits, concurrency, persistence, redaction, receipts, failed calls, pre-send blocking, and follow-up termination.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/aiocqhttp/delivery_safety.py" line_range="296-325" />
<code_context>
+        self._delivery_locks: dict[str, asyncio.Lock] = {}
</code_context>
<issue_to_address>
**issue (bug_risk):** `_delivery_locks` retains one `asyncio.Lock` for every private UMO ever sent and never removes entries, so the adapter's in-memory lock map grows without bound as it communicates with new users.

**Triggers:** When the bot sends private messages to many distinct users over its lifetime.

**Suggested fix:** Remove the lock entry after the guarded send completes, taking care to avoid deleting a lock that has been replaced or is still in use.
</issue_to_address>

### Comment 2
<location path="astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py" line_range="105-110" />
<code_context>
+                inbound_text = event.get("raw_message")
+                if not isinstance(inbound_text, str):
+                    inbound_text = str(event.get("message") or "")
+                self.bot.delivery_store.record_inbound(
+                    umo,
+                    inbound_text,
+                    event,
+                    blocked=blocked,
+                )
+                if blocked:
+                    return
</code_context>
<issue_to_address>
**issue (bug_risk):** A SQLite failure in `blocked()` or `record_inbound()` is caught by the handler's broad exception block before `convert_message()` and `handle_msg()` run, so a transient audit-database error silently drops the incoming private message instead of processing it.

**Triggers:** When the audit database is locked, unavailable, or otherwise raises an SQLite exception while a private event is received.

**Suggested fix:** Keep message processing independent of audit persistence: handle audit errors separately, log them, and continue to `convert_message()`/`handle_msg()` unless the circuit state was successfully read as blocked.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 2 findings to address first, and an incorrect circuit-breaker or private-message classification could permanently block a conversation or allow a sensitive delivery, and the new audit database retains inbound and outbound records beyond the code revert. Reverting stops the new behavior but does not unsend messages or remove persisted circuits and audit data.

Blocking findings: astrbot/core/platform/sources/aiocqhttp/delivery_safety.py:325, astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py:110


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment on lines +296 to +325
self._delivery_locks: dict[str, asyncio.Lock] = {}

async def call_action(self, action: str, **params):
base_action = action.removesuffix("_async")
private = base_action in {"send_private_msg", "send_private_forward_msg"} or (
base_action in {"send_msg", "send_forward_msg"}
and not params.get("group_id")
and params.get("message_type") != "group"
)
if not private:
return await super().call_action(action, **params)
user_id = str(params.get("user_id") or "")
if not user_id.isdigit():
raise DeliveryBlocked("Private send has no valid user_id")
umo = f"{self.delivery_platform_id}:FriendMessage:{user_id}"
content = params.get("message", params.get("messages", []))
if isinstance(content, str):
text = content
else:
text = "".join(
str(part.get("data", {}).get("text", ""))
if part.get("type") == "text"
else f"[{part.get('type', 'unknown')}]"
for part in content
if isinstance(part, dict)
)
delivery_locks = getattr(self, "_delivery_locks", None)
if not isinstance(delivery_locks, dict):
delivery_locks = self._delivery_locks = {}
async with delivery_locks.setdefault(umo, asyncio.Lock()):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): _delivery_locks retains one asyncio.Lock for every private UMO ever sent and never removes entries, so the adapter's in-memory lock map grows without bound as it communicates with new users.

Triggers: When the bot sends private messages to many distinct users over its lifetime.

Suggested fix: Remove the lock entry after the guarded send completes, taking care to avoid deleting a lock that has been replaced or is still in use.

Comment on lines +105 to +110
self.bot.delivery_store.record_inbound(
umo,
inbound_text,
event,
blocked=blocked,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): A SQLite failure in blocked() or record_inbound() is caught by the handler's broad exception block before convert_message() and handle_msg() run, so a transient audit-database error silently drops the incoming private message instead of processing it.

Triggers: When the audit database is locked, unavailable, or otherwise raises an SQLite exception while a private event is received.

Suggested fix: Keep message processing independent of audit persistence: handle audit errors separately, log them, and continue to convert_message()/handle_msg() unless the circuit state was successfully read as blocked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant