fix(bridge): approval ENOENT race condition + multi-choice button grouping #task-276 #task-277
This commit is contained in:
97
bot.py
97
bot.py
@@ -32,16 +32,77 @@ logger = logging.getLogger(__name__)
|
||||
# ─── Discord UI Components ──────────────────────────────────────────
|
||||
|
||||
class ApprovalView(discord.ui.View):
|
||||
"""Discord buttons for approving/rejecting Antigravity actions."""
|
||||
"""Discord buttons for approving/rejecting Antigravity actions.
|
||||
|
||||
def __init__(self, bridge: BridgeProtocol, request: ApprovalRequest):
|
||||
Supports two modes:
|
||||
1. Legacy: ✅ 승인 / ❌ 거부 (when no buttons array)
|
||||
2. Multi-choice: dynamic buttons from pending's buttons array
|
||||
(e.g., ✅ Allow Once / ✅ Allow This Conversation / ❌ Deny)
|
||||
"""
|
||||
|
||||
def __init__(self, bridge: BridgeProtocol, request: ApprovalRequest,
|
||||
buttons: list[dict] | None = None):
|
||||
super().__init__(timeout=1800) # 30 minutes
|
||||
self.bridge = bridge
|
||||
self.request = request
|
||||
self.responded = False
|
||||
self.buttons_data = buttons
|
||||
|
||||
if buttons and len(buttons) > 1:
|
||||
# Multi-choice mode: remove the default decorated buttons first
|
||||
# (they are added by @discord.ui.button at class definition time)
|
||||
self.clear_items()
|
||||
|
||||
# Add a Discord button for each option
|
||||
for btn_info in buttons:
|
||||
btn_text = btn_info.get("text", "?")
|
||||
btn_index = btn_info.get("index", 0)
|
||||
is_reject = btn_text.lower() in ("deny", "reject", "cancel",
|
||||
"reject all", "decline",
|
||||
"dismiss", "stop")
|
||||
style = discord.ButtonStyle.red if is_reject else discord.ButtonStyle.green
|
||||
emoji = "❌" if is_reject else "✅"
|
||||
|
||||
button = discord.ui.Button(
|
||||
label=f"{emoji} {btn_text}",
|
||||
style=style,
|
||||
custom_id=f"choice_{request.request_id}_{btn_index}",
|
||||
)
|
||||
# Bind the callback with closure over btn_index and btn_text
|
||||
button.callback = self._make_choice_callback(btn_index, btn_text,
|
||||
is_reject)
|
||||
self.add_item(button)
|
||||
# else: use the default @discord.ui.button decorated methods below
|
||||
|
||||
def _make_choice_callback(self, btn_index: int, btn_text: str,
|
||||
is_reject: bool):
|
||||
async def callback(interaction: discord.Interaction):
|
||||
if self.responded:
|
||||
await interaction.response.send_message("이미 응답됨",
|
||||
ephemeral=True)
|
||||
return
|
||||
self.responded = True
|
||||
self.bridge.write_response(UserResponse(
|
||||
request_id=self.request.request_id,
|
||||
approved=not is_reject,
|
||||
button_index=btn_index,
|
||||
))
|
||||
embed = interaction.message.embeds[0] if interaction.message.embeds else None
|
||||
if embed:
|
||||
color = discord.Color.red() if is_reject else discord.Color.green()
|
||||
embed.color = color
|
||||
emoji = "❌" if is_reject else "✅"
|
||||
embed.set_footer(
|
||||
text=f"{emoji} {btn_text} by {interaction.user.display_name}"
|
||||
)
|
||||
await interaction.response.edit_message(embed=embed, view=None)
|
||||
return callback
|
||||
|
||||
@discord.ui.button(label="✅ 승인", style=discord.ButtonStyle.green)
|
||||
async def approve(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
# Only active in legacy mode (no buttons array)
|
||||
if self.buttons_data and len(self.buttons_data) > 1:
|
||||
return # multi-choice mode handles via dynamic buttons
|
||||
if self.responded:
|
||||
await interaction.response.send_message("이미 응답됨", ephemeral=True)
|
||||
return
|
||||
@@ -57,6 +118,9 @@ class ApprovalView(discord.ui.View):
|
||||
|
||||
@discord.ui.button(label="❌ 거부", style=discord.ButtonStyle.red)
|
||||
async def reject(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
# Only active in legacy mode (no buttons array)
|
||||
if self.buttons_data and len(self.buttons_data) > 1:
|
||||
return # multi-choice mode handles via dynamic buttons
|
||||
if self.responded:
|
||||
await interaction.response.send_message("이미 응답됨", ephemeral=True)
|
||||
return
|
||||
@@ -516,21 +580,38 @@ class GravityBot(commands.Bot):
|
||||
async def _send_approval_request(
|
||||
self, channel: discord.TextChannel, request: ApprovalRequest
|
||||
):
|
||||
# Read buttons array from pending file (if present)
|
||||
buttons = None
|
||||
pending_file = self.bridge.pending_dir / f"{request.request_id}.json"
|
||||
if pending_file.exists():
|
||||
try:
|
||||
pending_data = json.loads(
|
||||
pending_file.read_text(encoding="utf-8-sig")
|
||||
)
|
||||
buttons = pending_data.get("buttons")
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
# Build embed description
|
||||
desc_parts = [f"**명령어:**\n```\n{request.command[:1000]}\n```"]
|
||||
if buttons and len(buttons) > 1:
|
||||
# Multi-choice: show all options in description
|
||||
btn_names = [b.get("text", "?") for b in buttons]
|
||||
desc_parts.append(f"**선택지:** {' / '.join(btn_names)}")
|
||||
if request.description:
|
||||
desc_parts.append(request.description[:500])
|
||||
|
||||
embed = discord.Embed(
|
||||
title="⚠️ 승인 요청",
|
||||
description=(
|
||||
f"**명령어:**\n```\n{request.command[:1000]}\n```\n"
|
||||
f"{request.description[:500]}"
|
||||
),
|
||||
description="\n".join(desc_parts),
|
||||
color=discord.Color.orange(),
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
embed.set_footer(text=f"ID: {request.request_id}")
|
||||
|
||||
view = ApprovalView(self.bridge, request)
|
||||
view = ApprovalView(self.bridge, request, buttons=buttons)
|
||||
msg = await channel.send(embed=embed, view=view)
|
||||
|
||||
pending_file = self.bridge.pending_dir / f"{request.request_id}.json"
|
||||
if pending_file.exists():
|
||||
try:
|
||||
data = json.loads(pending_file.read_text(encoding="utf-8-sig"))
|
||||
|
||||
Reference in New Issue
Block a user