zalo-bot-webhook / patch_missing_defs.py
bep40's picture
Fix NameError: define missing _bstate_log (webhook 500) via self-healing patcher
6925123 verified
Raw History Blame Contribute Delete
5.79 kB
#!/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())