11import re
22from collections .abc import Awaitable , Callable
3+ from dataclasses import dataclass , field
34from http import HTTPStatus
45from typing import TYPE_CHECKING , Any , cast
56
1617 from ..context import LoopContext
1718 from ..fastloop import FastLoop
1819
19- SlackSetupCallback = Callable [["LoopContext" , "LoopEvent" ], Awaitable [str ]]
20+
21+ @dataclass
22+ class SlackConfig :
23+ bot_token : str
24+ signing_secret : str
25+ team_id : str | None = None
26+
27+
28+ @dataclass
29+ class SlackSetupInput :
30+ loop_name : str
31+ team_id : str
32+ channel : str
33+ root_ts : str
34+ event_type : str
35+ payload : dict [str , Any ] = field (default_factory = dict )
36+ event : dict [str , Any ] = field (default_factory = dict )
37+
38+
39+ SlackSetupCallback = Callable [["SlackSetupInput" ], Awaitable ["SlackConfig" ]]
2040
2141IGNORED_MESSAGE_SUBTYPES = frozenset (
2242 [
@@ -224,21 +244,44 @@ class SlackFileUploadEvent(LoopEvent):
224244
225245
226246class SlackIntegration (Integration ):
227- def __init__ (self , * , signing_secret : str , setup : SlackSetupCallback ):
247+ def __init__ (self , * , setup : SlackSetupCallback ):
228248 super ().__init__ ()
229249 self ._setup_callback = setup
230- self ._signing_secret = signing_secret
231- self .verifier = SignatureVerifier (signing_secret )
250+ self ._config_cache : dict [str , SlackConfig ] = {}
232251
233252 def type (self ) -> IntegrationType :
234253 return IntegrationType .SLACK
235254
255+ async def _resolve_config (self , setup_input : SlackSetupInput ) -> SlackConfig :
256+ team_id = setup_input .team_id
257+ if team_id in self ._config_cache :
258+ return self ._config_cache [team_id ]
259+ config = await self ._setup_callback (setup_input )
260+ self ._config_cache [team_id ] = config
261+ return config
262+
236263 async def setup_for_context (
237264 self , context : "LoopContext" , event : "LoopEvent"
238265 ) -> AsyncWebClient :
239- bot_token = await self ._setup_callback (context , event )
240- client = AsyncWebClient (token = bot_token )
266+ team_id = getattr (event , "team" , "" )
267+ channel = getattr (event , "channel" , "" )
268+ root_ts = getattr (event , "root_ts" , None ) or getattr (event , "ts" , "" )
269+ event_type = getattr (event , "type" , "" )
270+ event_dict = getattr (event , "raw_event" , None ) or {}
271+
272+ setup_input = SlackSetupInput (
273+ loop_name = self .loop_name ,
274+ team_id = team_id ,
275+ channel = channel ,
276+ root_ts = root_ts ,
277+ event_type = event_type ,
278+ payload = {},
279+ event = event_dict ,
280+ )
281+ config = await self ._resolve_config (setup_input )
282+ client = AsyncWebClient (token = config .bot_token )
241283 context .set_integration_client (self .type (), client )
284+ context .set_integration_client (f"{ self .type ()} _config" , config )
242285 return client
243286
244287 def get_client_for_context (self , context : "LoopContext" ) -> AsyncWebClient :
@@ -250,6 +293,12 @@ def get_client_for_context(self, context: "LoopContext") -> AsyncWebClient:
250293 )
251294 return cast ("AsyncWebClient" , client )
252295
296+ def get_config_for_context (self , context : "LoopContext" ) -> SlackConfig :
297+ config = context .get_integration_client (f"{ self .type ()} _config" )
298+ if config is None :
299+ raise ValueError ("Slack config not initialized for this context." )
300+ return cast ("SlackConfig" , config )
301+
253302 def register (self , fastloop : "FastLoop" , loop_name : str ) -> None :
254303 fastloop .register_events (SLACK_EVENT_TYPES )
255304 self ._fastloop : FastLoop = fastloop
@@ -265,18 +314,69 @@ def events(self) -> list[Any]:
265314 return list (SLACK_EVENT_TYPES )
266315
267316 async def _handle_slack_event (self , request : Request ):
317+ from ..logging import setup_logger
318+
319+ logger = setup_logger (__name__ )
320+
268321 body = await request .body ()
269- if not self .verifier .is_valid_request (body , dict (request .headers )):
270- raise HTTPException (
271- status_code = HTTPStatus .FORBIDDEN , detail = "Invalid signature"
272- )
273322
274- payload = await request .json ()
323+ logger .debug (
324+ "Slack webhook received" ,
325+ extra = {"body_length" : len (body ), "path" : str (request .url )},
326+ )
327+
328+ try :
329+ payload = await request .json ()
330+ except Exception as e :
331+ logger .error ("Failed to parse Slack payload" , extra = {"error" : str (e )})
332+ raise
333+
334+ logger .debug (
335+ "Parsed Slack payload" ,
336+ extra = {
337+ "type" : payload .get ("type" ),
338+ "event_type" : payload .get ("event" , {}).get ("type" ),
339+ },
340+ )
341+
275342 if payload .get ("type" ) == "url_verification" :
276343 return {"challenge" : payload ["challenge" ]}
277344
345+ team_id = payload .get ("team_id" , "" )
278346 event : dict [str , Any ] = payload .get ("event" , {})
279- event_type = event .get ("type" )
347+ event_type = event .get ("type" , "" )
348+ channel = event .get ("channel" , "" )
349+ root_ts = event .get ("thread_ts" ) or event .get ("ts" , "" )
350+
351+ logger .info (
352+ "Received Slack event" ,
353+ extra = {
354+ "event_type" : event_type ,
355+ "channel" : channel ,
356+ "root_ts" : root_ts ,
357+ "thread_ts" : event .get ("thread_ts" ),
358+ "ts" : event .get ("ts" ),
359+ "subtype" : event .get ("subtype" ),
360+ "has_bot_id" : bool (event .get ("bot_id" )),
361+ },
362+ )
363+
364+ setup_input = SlackSetupInput (
365+ loop_name = self .loop_name ,
366+ team_id = team_id ,
367+ channel = channel ,
368+ root_ts = root_ts ,
369+ event_type = event_type ,
370+ payload = payload ,
371+ event = event ,
372+ )
373+ config = await self ._resolve_config (setup_input )
374+ verifier = SignatureVerifier (config .signing_secret )
375+
376+ if not verifier .is_valid_request (body , dict (request .headers )):
377+ raise HTTPException (
378+ status_code = HTTPStatus .FORBIDDEN , detail = "Invalid signature"
379+ )
280380
281381 if event_type not in SUPPORTED_SLACK_EVENTS :
282382 return {"ok" : True }
@@ -288,18 +388,29 @@ async def _handle_slack_event(self, request: Request):
288388
289389 handler = self ._fastloop .loop_event_handlers .get (self .loop_name )
290390 if not handler :
391+ logger .warning ("No handler found" , extra = {"loop_name" : self .loop_name })
291392 return {"ok" : True }
292393
293- channel = event .get ("channel" , "" )
294- root_ts = event .get ("thread_ts" ) or event .get ("ts" , "" )
295- loop_id = await self ._fastloop .state_manager .get_loop_mapping (
296- f"slack_thread:{ channel } :{ root_ts } "
394+ mapping_key = f"slack_thread:{ channel } :{ root_ts } "
395+ loop_id = await self ._fastloop .state_manager .get_loop_mapping (mapping_key )
396+
397+ logger .info (
398+ "Loop mapping lookup" ,
399+ extra = {"mapping_key" : mapping_key , "loop_id" : loop_id },
297400 )
298401
299402 loop_event = self ._map_event (event , event_type , payload , loop_id )
300403 if loop_event is None :
404+ logger .warning (
405+ "Event could not be mapped" , extra = {"event_type" : event_type }
406+ )
301407 return {"ok" : True }
302408
409+ logger .info (
410+ "Dispatching event to handler" ,
411+ extra = {"event_type" : loop_event .type , "loop_id" : loop_id },
412+ )
413+
303414 loop : LoopState = await handler (loop_event .to_dict ())
304415 if loop .loop_id :
305416 await self ._fastloop .state_manager .set_loop_mapping (
0 commit comments