Spaces:
Running
Running
| #!/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}") | |