#!/usr/bin/env python3 """ Set up LINE Rich Menu for Stock Predictor bot. Run once: python setup_rich_menu.py Layout (2 columns x 2 rows = 4 buttons): [📊 æŸĨ股įĨĻ] [🌐 開啟įķēįŦ™] [📈 預æļŽ] [❓ čŠŠæ˜Ž] """ import os, json, sys, urllib.request, urllib.error TOKEN = os.environ.get("LINE_CHANNEL_ACCESS_TOKEN") or "" SITE_URL = os.environ.get("STOCK_SITE_URL", "https://dennischan0909-dockerspace.hf.space") if not TOKEN: # Try loading from .env in same dir env_file = os.path.join(os.path.dirname(__file__), ".env") if os.path.exists(env_file): for line in open(env_file): line = line.strip() if line.startswith("LINE_CHANNEL_ACCESS_TOKEN="): TOKEN = line.split("=", 1)[1].strip().strip('"').strip("'") if line.startswith("STOCK_SITE_URL="): SITE_URL = line.split("=", 1)[1].strip().strip('"').strip("'") if not TOKEN: print("ERROR: LINE_CHANNEL_ACCESS_TOKEN not set") sys.exit(1) HEADERS = { "Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json", } def api(method: str, path: str, body=None): url = f"https://api.line.me{path}" data = json.dumps(body).encode() if body else None req = urllib.request.Request(url, data=data, headers=HEADERS, method=method) try: with urllib.request.urlopen(req) as r: return json.loads(r.read()) except urllib.error.HTTPError as e: err = e.read().decode() print(f" HTTP {e.code}: {err}") raise # ── 1. Delete existing menus ────────────────────────────────────────────────── print("Deleting existing rich menus...") try: menus = api("GET", "/v2/bot/richmenu/list") for m in menus.get("richmenus", []): api("DELETE", f"/v2/bot/richmenu/{m['richMenuId']}") print(f" Deleted {m['richMenuId']}") except Exception as e: print(f" (skip) {e}") # ── 2. Create new menu ──────────────────────────────────────────────────────── W, H = 2500, 843 # LINE recommended size for 2-row menu HALF_W = W // 2 menu_body = { "size": {"width": W, "height": H}, "selected": True, "name": "Stock Menu", "chatBarText": "股įĨĻéļå–Ū", "areas": [ # Row 1 left — æŸĨ股įĨĻ { "bounds": {"x": 0, "y": 0, "width": HALF_W, "height": H // 2}, "action": {"type": "message", "text": "æŸĨ股įĨĻ"}, }, # Row 1 right — 開啟įķēįŦ™ { "bounds": {"x": HALF_W, "y": 0, "width": HALF_W, "height": H // 2}, "action": {"type": "uri", "uri": SITE_URL}, }, # Row 2 left — 預æļŽ { "bounds": {"x": 0, "y": H // 2, "width": HALF_W, "height": H // 2}, "action": {"type": "message", "text": "預æļŽ"}, }, # Row 2 right — čŠŠæ˜Ž { "bounds": {"x": HALF_W, "y": H // 2, "width": HALF_W, "height": H // 2}, "action": {"type": "message", "text": "čŠŠæ˜Ž"}, }, ], } print("Creating rich menu...") result = api("POST", "/v2/bot/richmenu", menu_body) menu_id = result["richMenuId"] print(f" Created: {menu_id}") # ── 3. Upload image ─────────────────────────────────────────────────────────── print("Uploading menu image...") img_path = os.path.join(os.path.dirname(__file__), "rich_menu.png") if not os.path.exists(img_path): # Generate a simple placeholder image with Pillow if available try: from PIL import Image, ImageDraw, ImageFont img = Image.new("RGB", (W, H), "#1a1a2e") draw = ImageDraw.Draw(img) labels = [ ("📊 æŸĨ股įĨĻ", HALF_W // 2, H // 4), ("🌐 開啟įķēįŦ™", HALF_W + HALF_W // 2, H // 4), ("📈 預æļŽ", HALF_W // 2, H * 3 // 4), ("❓ čŠŠæ˜Ž", HALF_W + HALF_W // 2, H * 3 // 4), ] # Grid lines draw.line([(HALF_W, 0), (HALF_W, H)], fill="#444", width=4) draw.line([(0, H // 2), (W, H // 2)], fill="#444", width=4) # Labels try: font = ImageFont.truetype("arial.ttf", 80) except Exception: font = ImageFont.load_default() for text, x, y in labels: draw.text((x, y), text, fill="white", font=font, anchor="mm") img.save(img_path) print(f" Generated placeholder image: {img_path}") except ImportError: print(" Pillow not installed — skipping auto-image generation") print(f" Please place a 2500×843 PNG at: {img_path}") print(" Then re-run this script (it will skip menu creation and only upload image + set default).") # Set default anyway so menu structure is ready api("POST", f"/v2/bot/richmenu/{menu_id}/default") print(f" Set as default (image pending). Menu ID: {menu_id}") sys.exit(0) with open(img_path, "rb") as f: img_data = f.read() img_req = urllib.request.Request( f"https://api-data.line.me/v2/bot/richmenu/{menu_id}/content", data=img_data, headers={**HEADERS, "Content-Type": "image/png"}, method="POST", ) try: with urllib.request.urlopen(img_req) as r: print(f" Image uploaded: {r.status}") except urllib.error.HTTPError as e: print(f" Image upload failed: {e.code} {e.read().decode()}") # ── 4. Set as default ───────────────────────────────────────────────────────── print("Setting as default menu...") api("POST", f"/v2/bot/user/all/richmenu/{menu_id}") print(f"Done! Menu ID: {menu_id}") print(f"Site URL: {SITE_URL}")