99import hmac
1010import json
1111import logging
12+ from functools import lru_cache
1213from typing import Any , Dict , Optional
1314
1415from praisonaiagents .bots .pairing_types import PairingApprovalResult
1516
1617logger = logging .getLogger (__name__ )
1718
1819
20+ @lru_cache (maxsize = 1 )
1921def _get_callback_secret () -> str :
2022 """Get HMAC secret for callback payload verification."""
2123 import os
2224 import secrets
23- return os .environ .get ("PRAISONAI_CALLBACK_SECRET" , "" ) or secrets .token_hex (16 )
25+ return os .environ .get ("PRAISONAI_CALLBACK_SECRET" , "" ) or secrets .token_hex (32 )
2426
2527
2628class PairingUIBuilder :
@@ -29,8 +31,8 @@ class PairingUIBuilder:
2931 @staticmethod
3032 def create_telegram_keyboard (user_name : str , code : str , channel : str , user_id : str ) -> Dict [str , Any ]:
3133 """Create Telegram inline keyboard for approval."""
32- approve_data = f"pair:approve:{ channel } :{ code } "
33- deny_data = f"pair:deny:{ channel } :{ code } "
34+ approve_data = f"pair:approve:{ channel } :{ user_id } : { code } "
35+ deny_data = f"pair:deny:{ channel } :{ user_id } : { code } "
3436
3537 # Add HMAC signature to prevent tampering
3638 secret = _get_callback_secret ().encode ()
@@ -55,8 +57,8 @@ def create_telegram_keyboard(user_name: str, code: str, channel: str, user_id: s
5557 @staticmethod
5658 def create_discord_components (user_name : str , code : str , channel : str , user_id : str ) -> list :
5759 """Create Discord button components for approval."""
58- approve_id = f"pair:approve:{ channel } :{ code } "
59- deny_id = f"pair:deny:{ channel } :{ code } "
60+ approve_id = f"pair:approve:{ channel } :{ user_id } : { code } "
61+ deny_id = f"pair:deny:{ channel } :{ user_id } : { code } "
6062
6163 # Add HMAC signature
6264 secret = _get_callback_secret ().encode ()
@@ -86,8 +88,8 @@ def create_discord_components(user_name: str, code: str, channel: str, user_id:
8688 @staticmethod
8789 def create_slack_blocks (user_name : str , code : str , channel : str , user_id : str ) -> list :
8890 """Create Slack block kit for approval."""
89- approve_value = f"pair:approve:{ channel } :{ code } "
90- deny_value = f"pair:deny:{ channel } :{ code } "
91+ approve_value = f"pair:approve:{ channel } :{ user_id } : { code } "
92+ deny_value = f"pair:deny:{ channel } :{ user_id } : { code } "
9193
9294 # Add HMAC signature
9395 secret = _get_callback_secret ().encode ()
@@ -140,23 +142,24 @@ def parse_and_verify_callback(self, callback_data: str) -> Optional[Dict[str, st
140142 """Parse and verify callback data from button press.
141143
142144 Args:
143- callback_data: Raw callback data (e.g., "pair:approve:telegram:abc123:sig")
145+ callback_data: Raw callback data (e.g., "pair:approve:telegram:user123: abc123:sig")
144146
145147 Returns:
146148 Parsed data dict or None if invalid
147149 """
148150 try :
149151 parts = callback_data .split (":" )
150- if len (parts ) < 5 or parts [0 ] != "pair" :
152+ if len (parts ) < 6 or parts [0 ] != "pair" :
151153 return None
152154
153- action = parts [1 ] # approve/deny
154- channel = parts [2 ] # telegram/discord/slack
155- code = parts [3 ] # pairing code
156- signature = parts [4 ] # HMAC signature
155+ action = parts [1 ] # approve/deny
156+ channel = parts [2 ] # telegram/discord/slack
157+ user_id = parts [3 ] # original requester user ID
158+ code = parts [4 ] # pairing code
159+ signature = parts [5 ] # HMAC signature
157160
158161 # Verify signature
159- payload = f"pair:{ action } :{ channel } :{ code } "
162+ payload = f"pair:{ action } :{ channel } :{ user_id } : { code } "
160163 secret = _get_callback_secret ().encode ()
161164 expected_sig = hmac .new (secret , payload .encode (), hashlib .sha256 ).hexdigest ()[:8 ]
162165
@@ -166,7 +169,8 @@ def parse_and_verify_callback(self, callback_data: str) -> Optional[Dict[str, st
166169
167170 return {
168171 "action" : action ,
169- "channel" : channel ,
172+ "channel" : channel ,
173+ "user_id" : user_id ,
170174 "code" : code
171175 }
172176
@@ -200,12 +204,13 @@ async def handle_approval_callback(
200204 action = parsed ["action" ]
201205 channel = parsed ["channel" ]
202206 code = parsed ["code" ]
207+ requester_user_id = parsed ["user_id" ]
203208
204209 if action == "approve" :
205210 # Approve the pairing
206211 success = self .pairing_store .verify_and_pair (
207212 code = code ,
208- channel_id = owner_user_id , # This would be the original user_id
213+ channel_id = requester_user_id , # Use original requester's ID
209214 channel_type = channel ,
210215 label = f"Approved by { owner_user_id } "
211216 )
@@ -214,7 +219,7 @@ async def handle_approval_callback(
214219 # Notify original user
215220 try :
216221 await bot_adapter .reply (
217- owner_user_id , # This should be the original requester
222+ requester_user_id , # Notify the original requester
218223 "You've been approved! Send me a message."
219224 )
220225 except Exception as e :
@@ -223,7 +228,7 @@ async def handle_approval_callback(
223228 return PairingApprovalResult (
224229 success = True ,
225230 message = "✅ Approved" ,
226- user_id = owner_user_id ,
231+ user_id = requester_user_id ,
227232 channel = channel
228233 )
229234 else :
@@ -233,8 +238,19 @@ async def handle_approval_callback(
233238 )
234239
235240 elif action == "deny" :
236- # Revoke if was temporarily added, or just ignore
237- # Note: In real implementation, we'd need to track the original user_id
241+ # Consume the code to prevent reuse
242+ try :
243+ # Try to consume the code without pairing by calling verify_and_pair with dummy values
244+ # This will consume the code and return False since it won't actually pair
245+ self .pairing_store .verify_and_pair (
246+ code = code ,
247+ channel_id = "__denied__" , # Dummy value that won't be stored
248+ channel_type = "__denied__" ,
249+ label = "Denied pairing request"
250+ )
251+ except Exception as e :
252+ logger .error (f"Failed to consume denied code: { e } " )
253+
238254 return PairingApprovalResult (
239255 success = True ,
240256 message = "❌ Denied" ,
0 commit comments