#!/usr/bin/env python3 """patch_missing_defs.py — insert missing `_bstate_log` into app.py. Fixes (verified from live build log): NameError: name '_bstate_log' is not defined (app.py:5574) -> every POST /webhooks returned 500 Internal Server Error, so the main bot NEVER replied and webhook tests (webhook.test) failed. Strategy (same proven pattern as patch_card_strings.py): - Fixes the LOCAL on-disk file first (running app uses the fix NOW). - If anything changed, commits the fixed file back to THIS Space's repo (SELF_REPO_ID env) -> repo converges after ONE rebuild, no loop. - Also installs a PEP 562 module-level __getattr__ that auto-provides any other missing `_bstate*` helper (fail-open) instead of NameError. - Idempotent & fail-open: never blocks the app from starting. """ import os import sys import tempfile import pathlib FILES = ["app.py"] _FIX_BLOCK = '''def _bstate_log(state): """Log a bot-state event dict. Fail-open: never raises. Called at the top of handle_webhook; if missing, the app crashes with NameError and every webhook returns 500. Mirrors the child-template log semantics: append to in-memory _logs + print a [WEBHOOK] line so the main.py journal bridge persists it to the registry dataset. """ try: if not isinstance(state, dict): state = {"data": str(state)} # Redact anything sensitive before persisting _red = {} for _k, _v in state.items(): if re.search(r"token|secret|key|hash|password", str(_k), re.I): _red[str(_k)] = "***" else: _red[str(_k)] = _v _entry = { "event": str(state.get("event", "bstate"))[:40], "sender_id": str(state.get("sender_id", state.get("sender", "system")) or "system"), "sender_name": str(state.get("sender_name", "") or ""), "chat_id": str(state.get("chat_id", state.get("chat", "SYSTEM")) or "SYSTEM"), "chat_type": str(state.get("chat_type", "") or ""), "text": json.dumps(_red, ensure_ascii=False)[:500], "is_zgr": bool(state.get("is_zgr", False)), "time": time.strftime("%Y-%m-%d %H:%M:%S"), } try: _logs.append(_entry) if len(_logs) > 200: del _logs[:100] except Exception: pass print("[WEBHOOK] event=" + str(_entry["event"]) + " sender=" + str(_entry["sender_id"]) + " chat=" + str(_entry["chat_id"]) + " is_zgr=" + str(_entry["is_zgr"]) + " text=" + str(_entry["text"])[:100], flush=True) except Exception: pass _BSTATE_STATE = {} def _bstate_get(key, default=None): """Read one key from the in-memory bot-state dict (fail-open).""" try: return _BSTATE_STATE.get(key, default) except Exception: return default def _bstate_set(key, value): """Store one key/value in the in-memory bot-state dict (fail-open).""" try: _BSTATE_STATE[key] = value except Exception: pass def __getattr__(name): """PEP 562: auto-provide any OTHER missing `_bstate_*` helper as a no-op logger so one forgotten call site can never 500 the webhook again. All other undefined names still raise AttributeError -> real bugs stay visible. """ if name.startswith("_bstate"): return _bstate_log raise AttributeError(name) ''' def main(): rc = 0 try: repo_id = os.getenv("SELF_REPO_ID", "").strip() token = os.getenv("HF_TOKEN", os.getenv("HF_API_TOKEN", "")).strip() changed = False for fname in FILES: if not os.path.exists(fname): continue with open(fname, encoding="utf-8") as f: content = f.read() if "def _bstate_log(" in content: print("[patch-defs] already fixed:", fname, flush=True) continue anchor = "def handle_webhook(" idx = content.find(anchor) if idx < 0: anchor = "async def handle_webhook(" idx = content.find(anchor) if idx < 0: print("[patch-defs] WARNING: handle_webhook anchor not found in", fname, flush=True) continue content = content[:idx] + _FIX_BLOCK + content[idx:] with open(fname, "w", encoding="utf-8") as f: f.write(content) print("[patch-defs] fixed locally:", fname, flush=True) changed = True if repo_id and token: try: from huggingface_hub import HfApi api = HfApi(token=token) with tempfile.TemporaryDirectory() as tmpdir: tmp = pathlib.Path(tmpdir) (tmp / fname).write_text(content, encoding="utf-8") api.upload_folder( repo_id=repo_id, repo_type="space", folder_path=str(tmp), token=token, commit_message="autofix: define missing _bstate_log in app.py", ) print("[patch-defs] committed", fname, "->", repo_id, flush=True) except Exception as e: print("[patch-defs] commit failed (local fix still applied):", str(e)[:200], flush=True) if not changed: print("[patch-defs] nothing to do — exiting clean", flush=True) except Exception as e: print("[patch-defs] FATAL (ignored, fail-open):", str(e)[:200], flush=True) rc = 0 return rc if __name__ == "__main__": sys.exit(main())