""" router.py — Event router Only processes app_mention events — when someone tags @intern-eod. All other channel messages are ignored by Slack before they reach us. Cron-based miss warnings run independently and are unaffected. """ import os import time import sheets import handlers HR_SLACK_IDS = set(os.environ.get("HR_SLACK_IDS", "").split(",")) # ───────────────────────────────────────────── # CHANNEL CACHE # ───────────────────────────────────────────── _managed_channels_cache: set = set() _cache_last_updated: float = 0 _CACHE_TTL: int = 300 def _get_managed_channels() -> set: global _managed_channels_cache, _cache_last_updated if time.time() - _cache_last_updated > _CACHE_TTL: try: candidates = sheets.get_active_candidates() managed = { c.get("channel_id", "").strip() for c in candidates if c.get("channel_id", "").strip() } except Exception as e: print(f"[router] cache refresh failed: {e}") managed = set() env_channel = os.environ.get("SLACK_CHANNEL_ID", "").strip() if env_channel: managed.add(env_channel) _managed_channels_cache = managed _cache_last_updated = time.time() print(f"[router] managed channels: {_managed_channels_cache}") return _managed_channels_cache def invalidate_channel_cache() -> None: global _cache_last_updated _cache_last_updated = 0 # ───────────────────────────────────────────── # ROLE DETECTION # ───────────────────────────────────────────── def _get_ft_slack_ids() -> set: try: return {c["ft_slack_id"] for c in sheets.get_active_candidates() if c.get("ft_slack_id")} except Exception: return set() def _get_sender_role(user_id: str) -> str: """Candidate check first — person can be both HR and candidate.""" try: if sheets.get_candidate_by_slack_id(user_id): return "candidate" except Exception: pass if user_id in HR_SLACK_IDS: return "hr" if user_id in _get_ft_slack_ids(): return "ft" return "unknown" def _get_candidate_with_pending_recommendation() -> dict | None: try: for candidate in sheets.get_active_candidates(): if sheets.get_pending_recommendation(candidate["candidate_id"]): return candidate except Exception: pass return None # ───────────────────────────────────────────── # MAIN ROUTER # ───────────────────────────────────────────── def route_event(event: dict) -> None: """ Processes two event types: 1. app_mention — someone tagged @intern-eod (primary EOD submission path) 2. message — for HR approve/reject confirmations only """ event_type = event.get("type", "") user_id = event.get("user", "") channel_id = event.get("channel", "") text = event.get("text", "").strip() # Ignore bot messages and empty messages if event.get("bot_id") or event.get("subtype") or not text or not user_id: return # Ignore thread replies if event.get("thread_ts") and event.get("thread_ts") != event.get("ts"): return # Only handle managed channels if channel_id not in _get_managed_channels(): return role = _get_sender_role(user_id) if role == "unknown": return print(f"[router] type={event_type} role={role} user={user_id} channel={channel_id}") print(f"[router] text_preview={text[:80]!r}") # ── Candidate: Claude will verify if it is a real EOD report ── if role == "candidate": candidate = sheets.get_candidate_by_slack_id(user_id) if candidate: handlers.handle_candidate_message(event, candidate) return # ── FT: short signal answers ── if role == "ft": lower = text.strip().lower() ft_answers = {"yes","no","partial","specific","vague","on_time", "late_flagged","late_no_flag","not_delivered","engaged", "defensive","collapsed","pushing","avoiding","unclear"} if len(lower) < 80 and any(a in lower for a in ft_answers): candidate = _get_ft_candidate(user_id, channel_id) if candidate: handlers.handle_ft_signal_answer(event, candidate) return # ── HR approve/reject ── if role == "hr": lower = text.strip().lower() if lower in ("approve", "reject"): candidate = _get_candidate_with_pending_recommendation() if candidate: handlers.handle_hr_confirmation(event, candidate) def _get_ft_candidate(ft_slack_id: str, channel_id: str) -> dict | None: try: candidates = sheets.get_active_candidates() matches = [ c for c in candidates if c.get("ft_slack_id") == ft_slack_id and c.get("channel_id", "").strip() == channel_id ] return matches[0] if matches else None except Exception: return None