DockerSpace / setup_rich_menu.py
DennisChan0909's picture
fix: update rich menu URL to HF Space, expose loading animation errors
a5eb6b9
Raw
History Blame Contribute Delete
6.01 kB
#!/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}")