mrfakename's picture
update
3e016a0
import os
import secrets
import urllib.parse
import sqlite3
import json
import time
from datetime import datetime, timezone
from flask import Flask, redirect, request, session, render_template, url_for, g, jsonify
import requests
app = Flask(__name__)
app.secret_key = os.environ.get("SECRET_KEY", secrets.token_hex(32))
# Configure session cookies for iframe compatibility
app.config["SESSION_COOKIE_SAMESITE"] = "None"
app.config["SESSION_COOKIE_SECURE"] = True
# OAuth configuration from HF Space environment
OAUTH_CLIENT_ID = os.environ.get("OAUTH_CLIENT_ID")
OAUTH_CLIENT_SECRET = os.environ.get("OAUTH_CLIENT_SECRET")
OPENID_PROVIDER_URL = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co")
SPACE_HOST = os.environ.get("SPACE_HOST", "localhost:7860")
# Cache settings
CACHE_TTL = 300 # 5 minutes
DB_PATH = "cache.db"
def relative_time(iso_timestamp):
if not iso_timestamp:
return ""
try:
dt = datetime.fromisoformat(iso_timestamp.replace("Z", "+00:00"))
now = datetime.now(timezone.utc)
diff = now - dt
seconds = diff.total_seconds()
if seconds < 60:
return "now"
elif seconds < 3600:
mins = int(seconds // 60)
return f"{mins}m"
elif seconds < 86400:
hours = int(seconds // 3600)
return f"{hours}h"
elif seconds < 604800:
days = int(seconds // 86400)
return f"{days}d"
elif seconds < 2592000:
weeks = int(seconds // 604800)
return f"{weeks}w"
elif seconds < 31536000:
months = int(seconds // 2592000)
return f"{months}mo"
else:
years = int(seconds // 31536000)
return f"{years}y"
except Exception:
return ""
def get_db():
if "db" not in g:
g.db = sqlite3.connect(DB_PATH)
g.db.row_factory = sqlite3.Row
return g.db
@app.teardown_appcontext
def close_db(exception):
db = g.pop("db", None)
if db is not None:
db.close()
def init_db():
with sqlite3.connect(DB_PATH) as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS cache (
key TEXT PRIMARY KEY,
value TEXT,
expires_at REAL
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS jobs (
id TEXT PRIMARY KEY,
type TEXT,
user_id TEXT,
status TEXT DEFAULT 'pending',
progress_current INTEGER DEFAULT 0,
progress_total INTEGER DEFAULT 0,
progress_stage TEXT DEFAULT '',
result TEXT,
error TEXT,
created_at REAL,
updated_at REAL
)
""")
conn.commit()
def cache_get(key):
db = get_db()
row = db.execute(
"SELECT value, expires_at FROM cache WHERE key = ?", (key,)
).fetchone()
if row and row["expires_at"] > time.time():
return json.loads(row["value"])
return None
def cache_set(key, value, ttl=CACHE_TTL):
db = get_db()
expires_at = time.time() + ttl
db.execute(
"INSERT OR REPLACE INTO cache (key, value, expires_at) VALUES (?, ?, ?)",
(key, json.dumps(value), expires_at),
)
db.commit()
def get_base_url():
if "localhost" in SPACE_HOST or "127.0.0.1" in SPACE_HOST:
return f"http://{SPACE_HOST}"
return f"https://{SPACE_HOST}"
# Import jobs module after app is defined
from jobs import (
create_job, get_job, update_job_progress, complete_job,
start_job_thread, run_initial_load_job, run_wake_job
)
from hf_api import HuggingFaceAPI
def get_discussions_feed(spaces, discussions_map, logged_in_user=None):
"""Build discussions feed from cached data."""
all_discussions = []
for space in spaces:
space_id = space.get("id", "")
discussions = discussions_map.get(space_id, [])
for d in discussions:
owner_responded = d.get("repoOwner", {}).get("isParticipating", False)
discussion_author = d.get("author", {}).get("name", "")
is_own_discussion = logged_in_user and discussion_author.lower() == logged_in_user.lower()
base_score = d.get("numComments", 0) + d.get("numReactionUsers", 0) * 2
score = base_score
if owner_responded:
score -= 100
if is_own_discussion:
score -= 1000
all_discussions.append({
"space_id": space_id,
"space_name": space_id.split("/")[-1] if "/" in space_id else space_id,
"num": d.get("num"),
"title": d.get("title"),
"status": d.get("status"),
"is_pr": d.get("isPullRequest", False),
"author": d.get("author", {}).get("name", "unknown"),
"author_avatar": d.get("author", {}).get("avatarUrl", ""),
"created_at": d.get("createdAt", ""),
"relative_time": relative_time(d.get("createdAt", "")),
"num_comments": d.get("numComments", 0),
"num_reactions": d.get("numReactionUsers", 0),
"top_reactions": d.get("topReactions", []),
"score": score,
"owner_responded": owner_responded,
"is_own": is_own_discussion,
"url": f"https://huggingface.co/spaces/{space_id}/discussions/{d.get('num')}",
})
all_discussions.sort(key=lambda x: x["score"], reverse=True)
return all_discussions
@app.route("/")
def index():
if "user" in session:
return redirect(url_for("dashboard"))
return render_template("index.html")
@app.route("/login")
def login():
if not OAUTH_CLIENT_ID:
return "OAuth not configured. Make sure hf_oauth: true is set in your Space's README.md", 500
state = secrets.token_urlsafe(32)
session["oauth_state"] = state
redirect_uri = f"{get_base_url()}/login/callback"
params = {
"client_id": OAUTH_CLIENT_ID,
"redirect_uri": redirect_uri,
"scope": "openid profile",
"response_type": "code",
"state": state,
}
auth_url = f"{OPENID_PROVIDER_URL}/oauth/authorize?{urllib.parse.urlencode(params)}"
return redirect(auth_url)
@app.route("/login/callback")
def callback():
state = request.args.get("state")
if state != session.get("oauth_state"):
return "Invalid state parameter", 400
code = request.args.get("code")
if not code:
error = request.args.get("error", "Unknown error")
return f"Authorization failed: {error}", 400
redirect_uri = f"{get_base_url()}/login/callback"
token_url = f"{OPENID_PROVIDER_URL}/oauth/token"
token_response = requests.post(
token_url,
data={
"client_id": OAUTH_CLIENT_ID,
"client_secret": OAUTH_CLIENT_SECRET,
"code": code,
"grant_type": "authorization_code",
"redirect_uri": redirect_uri,
},
headers={
"Content-Type": "application/x-www-form-urlencoded",
},
)
if token_response.status_code != 200:
return f"Token exchange failed: {token_response.text}", 400
tokens = token_response.json()
access_token = tokens.get("access_token")
userinfo_url = f"{OPENID_PROVIDER_URL}/oauth/userinfo"
userinfo_response = requests.get(
userinfo_url,
headers={"Authorization": f"Bearer {access_token}"},
)
if userinfo_response.status_code != 200:
return f"Failed to get user info: {userinfo_response.text}", 400
user_info = userinfo_response.json()
session["user"] = {
"sub": user_info.get("sub"),
"username": user_info.get("preferred_username"),
"name": user_info.get("name"),
"email": user_info.get("email"),
"avatar_url": user_info.get("picture"),
}
session["access_token"] = access_token
session.pop("oauth_state", None)
# Start background job to load data
username = session["user"]["username"]
job_id = create_job("initial_load", username)
session["loading_job_id"] = job_id
start_job_thread(run_initial_load_job, job_id, username, access_token)
return redirect(url_for("loading"))
@app.route("/loading")
def loading():
if "user" not in session:
return redirect(url_for("index"))
job_id = session.get("loading_job_id")
if not job_id:
return redirect(url_for("dashboard"))
job = get_job(job_id)
if job and job["status"] == "completed":
session.pop("loading_job_id", None)
return redirect(url_for("dashboard"))
return render_template("loading.html", job_id=job_id)
@app.route("/api/job/<job_id>")
def get_job_status(job_id):
job = get_job(job_id)
if not job:
return jsonify({"error": "Job not found"}), 404
return jsonify(job)
@app.route("/dashboard")
def dashboard():
if "user" not in session:
return redirect(url_for("index"))
# Check if still loading
job_id = session.get("loading_job_id")
if job_id:
job = get_job(job_id)
if job and job["status"] not in ("completed", "failed"):
return redirect(url_for("loading"))
session.pop("loading_job_id", None)
username = session["user"]["username"]
token = session.get("access_token")
sort_by = request.args.get("sort", "score")
filter_status = request.args.get("status", "open")
# Try to get from cache first
spaces = cache_get(f"spaces:{username}")
# If no cache, trigger a reload
if spaces is None:
job_id = create_job("initial_load", username)
session["loading_job_id"] = job_id
start_job_thread(run_initial_load_job, job_id, username, token)
return redirect(url_for("loading"))
# Build discussions from cache
discussions_map = {}
for space in spaces:
space_id = space.get("id", "")
cached_discussions = cache_get(f"discussions:{space_id}")
if cached_discussions is not None:
discussions_map[space_id] = cached_discussions
discussions = get_discussions_feed(spaces, discussions_map, logged_in_user=username)
# Filter by status
if filter_status == "open":
discussions = [d for d in discussions if d["status"] == "open"]
elif filter_status == "closed":
discussions = [d for d in discussions if d["status"] in ("closed", "merged")]
# Sort discussions
if sort_by == "comments":
discussions.sort(key=lambda x: x["num_comments"], reverse=True)
elif sort_by == "reactions":
discussions.sort(key=lambda x: x["num_reactions"], reverse=True)
else:
discussions.sort(key=lambda x: x["score"], reverse=True)
return render_template(
"dashboard.html",
user=session["user"],
spaces=spaces,
discussions=discussions,
sort_by=sort_by,
filter_status=filter_status,
)
@app.route("/logout")
def logout():
session.clear()
return redirect(url_for("index"))
@app.route("/api/refresh", methods=["POST"])
def force_refresh():
if "user" not in session:
return jsonify({"error": "Not authenticated"}), 401
username = session["user"]["username"]
token = session.get("access_token")
if not token:
return jsonify({"error": "No access token"}), 401
# Clear all user cache
db = get_db()
db.execute("DELETE FROM cache WHERE key LIKE ?", (f"spaces:{username}%",))
db.execute("DELETE FROM cache WHERE key LIKE ?", (f"discussions:%",))
db.execute("DELETE FROM cache WHERE key LIKE ?", (f"space_detail:%",))
db.commit()
# Start background job to reload
job_id = create_job("initial_load", username)
start_job_thread(run_initial_load_job, job_id, username, token)
return jsonify({"job_id": job_id})
@app.route("/api/wake-all", methods=["POST"])
def wake_all_spaces():
if "user" not in session:
return jsonify({"error": "Not authenticated"}), 401
username = session["user"]["username"]
token = session.get("access_token")
if not token:
return jsonify({"error": "No access token"}), 401
# Get sleeping spaces from cache
spaces = cache_get(f"spaces:{username}")
if not spaces:
return jsonify({"error": "No spaces cached, please refresh"}), 400
sleeping_space_ids = [
s.get("id", "") for s in spaces
if s.get("runtime", {}).get("stage", "").upper() == "SLEEPING"
]
if not sleeping_space_ids:
return jsonify({"job_id": None, "total": 0, "message": "No sleeping spaces"})
# Create background job
job_id = create_job("wake_spaces", username)
start_job_thread(run_wake_job, job_id, username, token, sleeping_space_ids)
return jsonify({"job_id": job_id, "total": len(sleeping_space_ids)})
# Initialize database on startup
init_db()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=7860, debug=True)