Upload 7 files
Browse files- Dockerfile +15 -0
- README.md +33 -0
- app.py +133 -0
- index.html +252 -0
- keep_alive.py +31 -0
- renewer.py +120 -0
- requirements.txt +3 -0
Dockerfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY renewer.py keep_alive.py app.py index.html ./
|
| 9 |
+
|
| 10 |
+
RUN useradd -m -u 1000 appuser && chown -R appuser /app
|
| 11 |
+
USER appuser
|
| 12 |
+
|
| 13 |
+
EXPOSE 7860 8080
|
| 14 |
+
|
| 15 |
+
CMD ["python", "app.py"]
|
README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: PythonAnywhere Auto-Renewer
|
| 3 |
+
emoji: 🐍
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# 🐍 PythonAnywhere Auto-Renewer
|
| 11 |
+
|
| 12 |
+
أداة لإدارة وتجديد حسابات PythonAnywhere تلقائيًا كل 10 أيام.
|
| 13 |
+
|
| 14 |
+
## الميزات
|
| 15 |
+
- ✅ دعم حسابات متعددة
|
| 16 |
+
- ✅ تجديد تلقائي كل 10 أيام ("Run until 1 month from today")
|
| 17 |
+
- ✅ دعم الخوادم الأمريكية والأوروبية
|
| 18 |
+
- ✅ واجهة عربية سهلة الاستخدام
|
| 19 |
+
- ✅ سجل عمليات مفصّل
|
| 20 |
+
|
| 21 |
+
## طريقة الاستخدام
|
| 22 |
+
|
| 23 |
+
1. اذهب إلى تبويب **إضافة / حذف**
|
| 24 |
+
2. أدخل اسم المستخدم وAPI Token بتاعك
|
| 25 |
+
3. اضغط **إضافة الحساب**
|
| 26 |
+
4. اذهب إلى **التحكم** وشغّل المجدول التلقائي
|
| 27 |
+
|
| 28 |
+
## كيف تحصل على API Token
|
| 29 |
+
اذهب إلى: `https://www.pythonanywhere.com/account/#api_token`
|
| 30 |
+
|
| 31 |
+
## ملاحظة
|
| 32 |
+
البيانات تُخزَّن محليًا داخل الـ Space. إذا أعدت تشغيل الـ Space، يجب إعادة إضافة الحسابات.
|
| 33 |
+
للحفاظ على البيانات بشكل دائم، استخدم Hugging Face Secrets أو Dataset.
|
app.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import threading
|
| 4 |
+
import time
|
| 5 |
+
import logging
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
from fastapi import FastAPI, Request
|
| 8 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 9 |
+
import uvicorn
|
| 10 |
+
|
| 11 |
+
from keep_alive import start_ping_server
|
| 12 |
+
from renewer import load_accounts, save_accounts, get_host, renew_account, run_renewals
|
| 13 |
+
|
| 14 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
app = FastAPI()
|
| 18 |
+
SCHEDULER_RUNNING = False
|
| 19 |
+
SCHEDULER_THREAD = None
|
| 20 |
+
LOG_LINES = []
|
| 21 |
+
|
| 22 |
+
def add_log(msg):
|
| 23 |
+
ts = datetime.utcnow().strftime("%H:%M:%S")
|
| 24 |
+
LOG_LINES.append(f"[{ts}] {msg}")
|
| 25 |
+
if len(LOG_LINES) > 200:
|
| 26 |
+
LOG_LINES.pop(0)
|
| 27 |
+
logger.info(msg)
|
| 28 |
+
|
| 29 |
+
HTML = open("index.html", encoding="utf-8").read()
|
| 30 |
+
|
| 31 |
+
@app.get("/", response_class=HTMLResponse)
|
| 32 |
+
async def index():
|
| 33 |
+
return HTML
|
| 34 |
+
|
| 35 |
+
@app.get("/api/accounts")
|
| 36 |
+
async def get_accounts():
|
| 37 |
+
return [{k:v for k,v in a.items() if k!='token'} for a in load_accounts()]
|
| 38 |
+
|
| 39 |
+
@app.post("/api/accounts")
|
| 40 |
+
async def add_account(request: Request):
|
| 41 |
+
body = await request.json()
|
| 42 |
+
username = body.get("username","").strip()
|
| 43 |
+
token = body.get("token","").strip()
|
| 44 |
+
if not username or not token:
|
| 45 |
+
return JSONResponse({"ok":False,"message":"❌ بيانات ناقصة"})
|
| 46 |
+
accounts = load_accounts()
|
| 47 |
+
if any(a["username"]==username for a in accounts):
|
| 48 |
+
return JSONResponse({"ok":False,"message":f"⚠️ الحساب {username} موجود مسبقاً"})
|
| 49 |
+
add_log(f"🔍 جاري التحقق من {username}...")
|
| 50 |
+
host = get_host(username, token)
|
| 51 |
+
if not host:
|
| 52 |
+
add_log(f"❌ فشل الاتصال بـ {username}")
|
| 53 |
+
return JSONResponse({"ok":False,"message":"❌ فشل الاتصال. تحقق من البيانات."})
|
| 54 |
+
accounts.append({"username":username,"token":token,"host":host,
|
| 55 |
+
"last_renewed":None,"next_renewal":None,"last_results":[]})
|
| 56 |
+
save_accounts(accounts)
|
| 57 |
+
add_log(f"✅ تمت إضافة {username} على {host}")
|
| 58 |
+
return JSONResponse({"ok":True,"message":f"✅ تمت إضافة {username} على {host}"})
|
| 59 |
+
|
| 60 |
+
@app.delete("/api/accounts/{username}")
|
| 61 |
+
async def delete_account(username: str):
|
| 62 |
+
accounts = load_accounts()
|
| 63 |
+
new_list = [a for a in accounts if a["username"]!=username]
|
| 64 |
+
if len(new_list)==len(accounts):
|
| 65 |
+
return JSONResponse({"ok":False,"message":f"⚠️ لم يُعثر على {username}"})
|
| 66 |
+
save_accounts(new_list)
|
| 67 |
+
add_log(f"🗑️ تم حذف {username}")
|
| 68 |
+
return JSONResponse({"ok":True,"message":f"🗑️ تم حذف {username}"})
|
| 69 |
+
|
| 70 |
+
@app.post("/api/renew")
|
| 71 |
+
async def renew_all():
|
| 72 |
+
add_log("🚀 بدء تجديد جميع الحسابات...")
|
| 73 |
+
def do():
|
| 74 |
+
accs = run_renewals()
|
| 75 |
+
if accs:
|
| 76 |
+
for a in accs:
|
| 77 |
+
for r in (a.get("last_results") or []):
|
| 78 |
+
add_log(f" [{a['username']}] {r['domain']} → {r['status']}")
|
| 79 |
+
add_log("✅ انتهى التجديد.")
|
| 80 |
+
threading.Thread(target=do, daemon=True).start()
|
| 81 |
+
return {"ok":True}
|
| 82 |
+
|
| 83 |
+
@app.post("/api/renew/{username}")
|
| 84 |
+
async def renew_one(username: str):
|
| 85 |
+
accounts = load_accounts()
|
| 86 |
+
account = next((a for a in accounts if a["username"]==username), None)
|
| 87 |
+
if not account:
|
| 88 |
+
return {"ok":False}
|
| 89 |
+
account["next_renewal"] = None
|
| 90 |
+
save_accounts(accounts)
|
| 91 |
+
def do():
|
| 92 |
+
add_log(f"🔄 تجديد {username}...")
|
| 93 |
+
success, results = renew_account(account)
|
| 94 |
+
accs2 = load_accounts()
|
| 95 |
+
for i,a in enumerate(accs2):
|
| 96 |
+
if a["username"]==username:
|
| 97 |
+
accs2[i]=account
|
| 98 |
+
save_accounts(accs2)
|
| 99 |
+
for r in (results if success else []):
|
| 100 |
+
add_log(f" [{username}] {r['domain']} → {r['status']}")
|
| 101 |
+
add_log(f"{'✅' if success else '❌'} {'تم' if success else 'فشل'} تجديد {username}")
|
| 102 |
+
threading.Thread(target=do, daemon=True).start()
|
| 103 |
+
return {"ok":True}
|
| 104 |
+
|
| 105 |
+
@app.post("/api/scheduler")
|
| 106 |
+
async def toggle_scheduler():
|
| 107 |
+
global SCHEDULER_RUNNING, SCHEDULER_THREAD
|
| 108 |
+
if not SCHEDULER_RUNNING:
|
| 109 |
+
SCHEDULER_RUNNING = True
|
| 110 |
+
def loop():
|
| 111 |
+
while SCHEDULER_RUNNING:
|
| 112 |
+
run_renewals()
|
| 113 |
+
time.sleep(3600)
|
| 114 |
+
SCHEDULER_THREAD = threading.Thread(target=loop, daemon=True)
|
| 115 |
+
SCHEDULER_THREAD.start()
|
| 116 |
+
add_log("⏰ المجدول التلقائي شغّال — يجدد كل 10 أيام")
|
| 117 |
+
else:
|
| 118 |
+
SCHEDULER_RUNNING = False
|
| 119 |
+
add_log("⏹ تم إيقاف المجدول التلقائي")
|
| 120 |
+
return {"running": SCHEDULER_RUNNING}
|
| 121 |
+
|
| 122 |
+
@app.get("/api/log")
|
| 123 |
+
async def get_log():
|
| 124 |
+
return {"lines": LOG_LINES[-100:]}
|
| 125 |
+
|
| 126 |
+
@app.get("/health")
|
| 127 |
+
async def health():
|
| 128 |
+
return {"status":"alive"}
|
| 129 |
+
|
| 130 |
+
if __name__ == "__main__":
|
| 131 |
+
start_ping_server(port=8080)
|
| 132 |
+
add_log("🚀 PythonAnywhere Manager started")
|
| 133 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
index.html
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ar" dir="rtl">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>PythonAnywhere Manager</title>
|
| 7 |
+
<style>
|
| 8 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 9 |
+
body { font-family: 'Segoe UI', Tahoma, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; }
|
| 10 |
+
header { background: linear-gradient(135deg, #1e3a5f, #0f172a); padding: 20px 30px; border-bottom: 1px solid #334155; }
|
| 11 |
+
header h1 { font-size: 1.6rem; color: #38bdf8; }
|
| 12 |
+
header p { color: #94a3b8; font-size: 0.9rem; margin-top: 4px; }
|
| 13 |
+
.container { max-width: 960px; margin: 30px auto; padding: 0 20px; }
|
| 14 |
+
.card { background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 24px; margin-bottom: 24px; }
|
| 15 |
+
.card h2 { color: #38bdf8; margin-bottom: 18px; font-size: 1.05rem; display:flex; align-items:center; gap:8px; }
|
| 16 |
+
.form-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
|
| 17 |
+
input[type=text], input[type=password] {
|
| 18 |
+
flex: 1; min-width: 200px; padding: 10px 14px;
|
| 19 |
+
background: #0f172a; border: 1px solid #475569; border-radius: 8px;
|
| 20 |
+
color: #e2e8f0; font-size: 0.95rem;
|
| 21 |
+
}
|
| 22 |
+
input:focus { outline: none; border-color: #38bdf8; }
|
| 23 |
+
input::placeholder { color: #475569; }
|
| 24 |
+
button {
|
| 25 |
+
padding: 10px 22px; border: none; border-radius: 8px; cursor: pointer;
|
| 26 |
+
font-size: 0.9rem; font-weight: 600; transition: all .15s;
|
| 27 |
+
}
|
| 28 |
+
button:hover { transform: translateY(-1px); opacity: .9; }
|
| 29 |
+
button:active { transform: translateY(0); }
|
| 30 |
+
.btn-primary { background: #2563eb; color: #fff; }
|
| 31 |
+
.btn-danger { background: #dc2626; color: #fff; }
|
| 32 |
+
.btn-success { background: #16a34a; color: #fff; }
|
| 33 |
+
.btn-warning { background: #d97706; color: #fff; }
|
| 34 |
+
.btn-info { background: #0891b2; color: #fff; }
|
| 35 |
+
.btn-gray { background: #475569; color: #fff; }
|
| 36 |
+
table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
|
| 37 |
+
th { background: #0f172a; color: #94a3b8; padding: 10px 14px; text-align: right; font-weight: 600; }
|
| 38 |
+
td { padding: 10px 14px; border-top: 1px solid #1e293b; vertical-align: middle; }
|
| 39 |
+
tr:hover td { background: #1a2d47; }
|
| 40 |
+
.badge { display:inline-block; padding:3px 10px; border-radius:20px; font-size:0.78rem; font-weight:600; }
|
| 41 |
+
.badge-ok { background:#14532d; color:#86efac; }
|
| 42 |
+
.badge-na { background:#334155; color:#94a3b8; }
|
| 43 |
+
#log {
|
| 44 |
+
background: #020617; border: 1px solid #1e3a5f; border-radius: 8px;
|
| 45 |
+
padding: 14px; height: 260px; overflow-y: auto;
|
| 46 |
+
font-family: 'Courier New', monospace; font-size: 0.8rem; color: #7dd3fc;
|
| 47 |
+
direction: ltr; text-align: left;
|
| 48 |
+
}
|
| 49 |
+
#log .line { margin: 2px 0; }
|
| 50 |
+
#log .line.err { color: #f87171; }
|
| 51 |
+
#log .line.ok { color: #86efac; }
|
| 52 |
+
.sch-badge { display:inline-flex; align-items:center; gap:6px; padding:6px 16px; border-radius:20px; font-size:0.85rem; font-weight:600; margin-left:12px; }
|
| 53 |
+
.sch-on { background:#14532d; color:#86efac; }
|
| 54 |
+
.sch-off { background:#3f1818; color:#fca5a5; }
|
| 55 |
+
.toast { position:fixed; bottom:24px; left:50%; transform:translateX(-50%);
|
| 56 |
+
padding:12px 28px; border-radius:10px; font-size:0.92rem; font-weight:600;
|
| 57 |
+
z-index:9999; display:none; animation: fadeup .3s ease; }
|
| 58 |
+
.toast-ok { background:#14532d; color:#bbf7d0; border:1px solid #16a34a; }
|
| 59 |
+
.toast-err { background:#7f1d1d; color:#fca5a5; border:1px solid #dc2626; }
|
| 60 |
+
@keyframes fadeup { from { opacity:0; transform:translateX(-50%) translateY(10px); } to { opacity:1; transform:translateX(-50%) translateY(0); } }
|
| 61 |
+
.spinner { display:inline-block; width:14px; height:14px; border:2px solid #fff; border-top-color:transparent; border-radius:50%; animation:spin .6s linear infinite; vertical-align:middle; margin-left:6px; }
|
| 62 |
+
@keyframes spin { to { transform:rotate(360deg); } }
|
| 63 |
+
.ctrl-row { display:flex; align-items:center; flex-wrap:wrap; gap:10px; margin-bottom:14px; }
|
| 64 |
+
.actions-row { display:flex; gap:10px; flex-wrap:wrap; }
|
| 65 |
+
.empty { color:#475569; padding:16px 0; text-align:center; }
|
| 66 |
+
</style>
|
| 67 |
+
</head>
|
| 68 |
+
<body>
|
| 69 |
+
|
| 70 |
+
<header>
|
| 71 |
+
<h1>🐍 PythonAnywhere Auto-Renewer</h1>
|
| 72 |
+
<p>إدارة وتجديد حسابات PythonAnywhere تلقائياً كل 10 أيام</p>
|
| 73 |
+
</header>
|
| 74 |
+
|
| 75 |
+
<div class="container">
|
| 76 |
+
|
| 77 |
+
<!-- الحسابات -->
|
| 78 |
+
<div class="card">
|
| 79 |
+
<h2>📋 الحسابات المضافة</h2>
|
| 80 |
+
<div id="accounts-wrap"><div class="empty">جاري التحميل...</div></div>
|
| 81 |
+
<button class="btn-gray" style="margin-top:14px" onclick="loadAccounts()">🔄 تحديث القائمة</button>
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
<!-- إضافة حساب -->
|
| 85 |
+
<div class="card">
|
| 86 |
+
<h2>➕ إضافة حساب جديد</h2>
|
| 87 |
+
<div class="form-row">
|
| 88 |
+
<input type="text" id="new-user" placeholder="اسم المستخدم — مثال: nopoh55678">
|
| 89 |
+
<input type="password" id="new-token" placeholder="API Token">
|
| 90 |
+
</div>
|
| 91 |
+
<button class="btn-primary" id="add-btn" onclick="addAccount()">➕ إضافة الحساب</button>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<!-- حذف حساب -->
|
| 95 |
+
<div class="card">
|
| 96 |
+
<h2>🗑️ حذف حساب</h2>
|
| 97 |
+
<div class="form-row">
|
| 98 |
+
<input type="text" id="del-user" placeholder="اسم المستخدم المراد حذفه">
|
| 99 |
+
</div>
|
| 100 |
+
<button class="btn-danger" onclick="deleteAccount()">🗑️ حذف</button>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
<!-- التحكم -->
|
| 104 |
+
<div class="card">
|
| 105 |
+
<h2>🎮 التحكم والجدولة</h2>
|
| 106 |
+
<div class="ctrl-row">
|
| 107 |
+
<span id="sch-badge" class="sch-badge sch-off">⏹ المجدول متوقف</span>
|
| 108 |
+
<button class="btn-success" id="sch-btn" onclick="toggleScheduler()">▶️ تشغيل المجدول التلقائي</button>
|
| 109 |
+
</div>
|
| 110 |
+
<div class="actions-row" style="margin-bottom:14px">
|
| 111 |
+
<button class="btn-primary" onclick="renewAll()">🚀 تجديد كل الحسابات الآن</button>
|
| 112 |
+
</div>
|
| 113 |
+
<div class="form-row">
|
| 114 |
+
<input type="text" id="single-user" placeholder="تجديد حساب معين — اسم المستخدم">
|
| 115 |
+
<button class="btn-warning" onclick="renewOne()">🔄 تجديد</button>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
|
| 119 |
+
<!-- السجل -->
|
| 120 |
+
<div class="card">
|
| 121 |
+
<h2>📜 السجل المباشر</h2>
|
| 122 |
+
<div id="log"></div>
|
| 123 |
+
<button class="btn-gray" style="margin-top:10px" onclick="loadLog()">🔄 تحديث</button>
|
| 124 |
+
</div>
|
| 125 |
+
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<div id="toast" class="toast"></div>
|
| 129 |
+
|
| 130 |
+
<script>
|
| 131 |
+
let schedulerOn = false;
|
| 132 |
+
|
| 133 |
+
function toast(msg, ok=true) {
|
| 134 |
+
const el = document.getElementById('toast');
|
| 135 |
+
el.textContent = msg;
|
| 136 |
+
el.className = 'toast ' + (ok ? 'toast-ok' : 'toast-err');
|
| 137 |
+
el.style.display = 'block';
|
| 138 |
+
clearTimeout(el._t);
|
| 139 |
+
el._t = setTimeout(() => el.style.display='none', 3500);
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
async function api(path, method='GET', body=null) {
|
| 143 |
+
try {
|
| 144 |
+
const opts = { method, headers:{'Content-Type':'application/json'} };
|
| 145 |
+
if (body) opts.body = JSON.stringify(body);
|
| 146 |
+
const r = await fetch(path, opts);
|
| 147 |
+
return await r.json();
|
| 148 |
+
} catch(e) {
|
| 149 |
+
return { ok: false, message: '❌ خطأ في الاتصال' };
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
async function loadAccounts() {
|
| 154 |
+
const data = await api('/api/accounts');
|
| 155 |
+
const el = document.getElementById('accounts-wrap');
|
| 156 |
+
if (!Array.isArray(data) || !data.length) {
|
| 157 |
+
el.innerHTML = '<div class="empty">لا توجد حسابات مضافة بعد.</div>';
|
| 158 |
+
return;
|
| 159 |
+
}
|
| 160 |
+
let html = `<table>
|
| 161 |
+
<tr><th>المستخدم</th><th>الخادم</th><th>آخر تجديد</th><th>التجديد القادم</th><th>الحالة</th></tr>`;
|
| 162 |
+
for (const a of data) {
|
| 163 |
+
const last = a.last_renewed ? a.last_renewed.slice(0,16).replace('T',' ') : '—';
|
| 164 |
+
const nxt = a.next_renewal ? a.next_renewal.slice(0,16).replace('T',' ') : '—';
|
| 165 |
+
const res = (a.last_results||[]).map(r=>`${r.domain}: ${r.status}`).join('<br>') || '—';
|
| 166 |
+
const badge = a.last_renewed
|
| 167 |
+
? '<span class="badge badge-ok">✅ مجدَّد</span>'
|
| 168 |
+
: '<span class="badge badge-na">⏳ لم يُجدَّد</span>';
|
| 169 |
+
html += `<tr>
|
| 170 |
+
<td><b>${a.username}</b></td>
|
| 171 |
+
<td style="font-size:.8rem;color:#94a3b8">${a.host||'—'}</td>
|
| 172 |
+
<td style="font-size:.82rem">${last}</td>
|
| 173 |
+
<td style="font-size:.82rem">${nxt}</td>
|
| 174 |
+
<td>${badge}<br><span style="font-size:.78rem;color:#64748b">${res}</span></td>
|
| 175 |
+
</tr>`;
|
| 176 |
+
}
|
| 177 |
+
el.innerHTML = html + '</table>';
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
async function addAccount() {
|
| 181 |
+
const user = document.getElementById('new-user').value.trim();
|
| 182 |
+
const token = document.getElementById('new-token').value.trim();
|
| 183 |
+
if (!user || !token) { toast('❌ أدخل اسم المستخدم والتوكن', false); return; }
|
| 184 |
+
const btn = document.getElementById('add-btn');
|
| 185 |
+
btn.innerHTML = 'جاري التحقق... <span class="spinner"></span>';
|
| 186 |
+
btn.disabled = true;
|
| 187 |
+
const r = await api('/api/accounts','POST',{username:user,token});
|
| 188 |
+
btn.innerHTML = '➕ إضافة الحساب';
|
| 189 |
+
btn.disabled = false;
|
| 190 |
+
toast(r.message, r.ok);
|
| 191 |
+
if (r.ok) {
|
| 192 |
+
document.getElementById('new-user').value='';
|
| 193 |
+
document.getElementById('new-token').value='';
|
| 194 |
+
loadAccounts();
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
async function deleteAccount() {
|
| 199 |
+
const user = document.getElementById('del-user').value.trim();
|
| 200 |
+
if (!user) { toast('❌ أدخل اسم المستخدم', false); return; }
|
| 201 |
+
if (!confirm(`هل أنت متأكد من حذف ${user}؟`)) return;
|
| 202 |
+
const r = await api('/api/accounts/'+user,'DELETE');
|
| 203 |
+
toast(r.message, r.ok);
|
| 204 |
+
if (r.ok) { document.getElementById('del-user').value=''; loadAccounts(); }
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
async function renewAll() {
|
| 208 |
+
toast('🚀 جاري إرسال طلب التجديد...');
|
| 209 |
+
await api('/api/renew','POST');
|
| 210 |
+
setTimeout(() => { loadLog(); loadAccounts(); }, 2000);
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
async function renewOne() {
|
| 214 |
+
const user = document.getElementById('single-user').value.trim();
|
| 215 |
+
if (!user) { toast('❌ أدخل اسم المستخدم', false); return; }
|
| 216 |
+
toast(`🔄 جاري تجديد ${user}...`);
|
| 217 |
+
await api('/api/renew/'+user,'POST');
|
| 218 |
+
setTimeout(() => { loadLog(); loadAccounts(); }, 2000);
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
async function toggleScheduler() {
|
| 222 |
+
const r = await api('/api/scheduler','POST');
|
| 223 |
+
schedulerOn = r.running;
|
| 224 |
+
document.getElementById('sch-badge').className = 'sch-badge ' + (schedulerOn?'sch-on':'sch-off');
|
| 225 |
+
document.getElementById('sch-badge').textContent = schedulerOn ? '🟢 المجدول شغّال' : '⏹ المجدول متوقف';
|
| 226 |
+
document.getElementById('sch-btn').textContent = schedulerOn ? '⏹ إيقاف المجدول' : '▶️ تشغيل المجدول التلقائي';
|
| 227 |
+
toast(schedulerOn ? '✅ المجدول شغّال الآن' : '⏹ تم إيقاف المجدول');
|
| 228 |
+
loadLog();
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
function colorLine(l) {
|
| 232 |
+
if (l.includes('❌') || l.includes('فشل') || l.includes('Error')) return 'err';
|
| 233 |
+
if (l.includes('✅') || l.includes('تمت') || l.includes('تم تجديد')) return 'ok';
|
| 234 |
+
return '';
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
async function loadLog() {
|
| 238 |
+
const data = await api('/api/log');
|
| 239 |
+
const el = document.getElementById('log');
|
| 240 |
+
if (!data.lines) return;
|
| 241 |
+
el.innerHTML = data.lines.map(l=>`<div class="line ${colorLine(l)}">${l}</div>`).join('');
|
| 242 |
+
el.scrollTop = el.scrollHeight;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
// تهيئة
|
| 246 |
+
loadAccounts();
|
| 247 |
+
loadLog();
|
| 248 |
+
setInterval(loadLog, 10000);
|
| 249 |
+
setInterval(loadAccounts, 30000);
|
| 250 |
+
</script>
|
| 251 |
+
</body>
|
| 252 |
+
</html>
|
keep_alive.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
keep_alive.py
|
| 3 |
+
خادم HTTP بسيط على بورت 8080 يُستخدم فقط لإبقاء الـ Space نشطًا.
|
| 4 |
+
يمكن لأي خدمة ping خارجية (UptimeRobot, cron-job.org) أن تطرقه كل 5 دقائق.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
| 8 |
+
import threading
|
| 9 |
+
import logging
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class PingHandler(BaseHTTPRequestHandler):
|
| 15 |
+
def do_GET(self):
|
| 16 |
+
self.send_response(200)
|
| 17 |
+
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
| 18 |
+
self.end_headers()
|
| 19 |
+
self.wfile.write("alive\n".encode())
|
| 20 |
+
|
| 21 |
+
# Suppress default request logs to keep console clean
|
| 22 |
+
def log_message(self, format, *args):
|
| 23 |
+
pass
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def start_ping_server(port: int = 8080):
|
| 27 |
+
server = HTTPServer(("0.0.0.0", port), PingHandler)
|
| 28 |
+
t = threading.Thread(target=server.serve_forever, daemon=True)
|
| 29 |
+
t.start()
|
| 30 |
+
logger.info(f"✅ Keep-alive server running on port {port} → GET / → 200 alive")
|
| 31 |
+
return server
|
renewer.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import logging
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
|
| 7 |
+
logging.basicConfig(
|
| 8 |
+
level=logging.INFO,
|
| 9 |
+
format='%(asctime)s [%(levelname)s] %(message)s'
|
| 10 |
+
)
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
ACCOUNTS_FILE = "accounts.json"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def load_accounts():
|
| 17 |
+
if os.path.exists(ACCOUNTS_FILE):
|
| 18 |
+
with open(ACCOUNTS_FILE, "r") as f:
|
| 19 |
+
return json.load(f)
|
| 20 |
+
return []
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def save_accounts(accounts):
|
| 24 |
+
with open(ACCOUNTS_FILE, "w") as f:
|
| 25 |
+
json.dump(accounts, f, indent=2)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def get_host(username, token):
|
| 29 |
+
"""Detect if account is on US or EU server."""
|
| 30 |
+
for host in ["www.pythonanywhere.com", "eu.pythonanywhere.com"]:
|
| 31 |
+
try:
|
| 32 |
+
r = requests.get(
|
| 33 |
+
f"https://{host}/api/v0/user/{username}/cpu/",
|
| 34 |
+
headers={"Authorization": f"Token {token}"},
|
| 35 |
+
timeout=10
|
| 36 |
+
)
|
| 37 |
+
if r.status_code == 200:
|
| 38 |
+
return host
|
| 39 |
+
except Exception:
|
| 40 |
+
continue
|
| 41 |
+
return None
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def extend_webapp(username, token, host):
|
| 45 |
+
"""Extend all webapps by 3 months (Run until 1 month from today)."""
|
| 46 |
+
results = []
|
| 47 |
+
try:
|
| 48 |
+
r = requests.get(
|
| 49 |
+
f"https://{host}/api/v0/user/{username}/webapps/",
|
| 50 |
+
headers={"Authorization": f"Token {token}"},
|
| 51 |
+
timeout=10
|
| 52 |
+
)
|
| 53 |
+
if r.status_code != 200:
|
| 54 |
+
logger.warning(f"[{username}] Failed to list webapps: {r.status_code}")
|
| 55 |
+
return results
|
| 56 |
+
|
| 57 |
+
webapps = r.json()
|
| 58 |
+
for app in webapps:
|
| 59 |
+
domain = app.get("domain_name")
|
| 60 |
+
# Reload = "Run until 1 month from today" for free accounts
|
| 61 |
+
reload_r = requests.post(
|
| 62 |
+
f"https://{host}/api/v0/user/{username}/webapps/{domain}/reload/",
|
| 63 |
+
headers={"Authorization": f"Token {token}"},
|
| 64 |
+
timeout=10
|
| 65 |
+
)
|
| 66 |
+
status = "✅ OK" if reload_r.status_code == 200 else f"❌ {reload_r.status_code}"
|
| 67 |
+
logger.info(f"[{username}] Reload {domain}: {status}")
|
| 68 |
+
results.append({"domain": domain, "status": status})
|
| 69 |
+
except Exception as e:
|
| 70 |
+
logger.error(f"[{username}] Error: {e}")
|
| 71 |
+
return results
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def renew_account(account):
|
| 75 |
+
username = account["username"]
|
| 76 |
+
token = account["token"]
|
| 77 |
+
host = account.get("host") or get_host(username, token)
|
| 78 |
+
|
| 79 |
+
if not host:
|
| 80 |
+
logger.error(f"[{username}] Cannot connect - invalid credentials?")
|
| 81 |
+
return False, "Cannot connect"
|
| 82 |
+
|
| 83 |
+
account["host"] = host
|
| 84 |
+
logger.info(f"[{username}] Connected to {host}")
|
| 85 |
+
|
| 86 |
+
results = extend_webapp(username, token, host)
|
| 87 |
+
account["last_renewed"] = datetime.utcnow().isoformat()
|
| 88 |
+
account["next_renewal"] = (datetime.utcnow() + timedelta(days=10)).isoformat()
|
| 89 |
+
account["last_results"] = results
|
| 90 |
+
return True, results
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def run_renewals():
|
| 94 |
+
accounts = load_accounts()
|
| 95 |
+
if not accounts:
|
| 96 |
+
logger.info("No accounts configured yet.")
|
| 97 |
+
return
|
| 98 |
+
|
| 99 |
+
now = datetime.utcnow()
|
| 100 |
+
renewed_count = 0
|
| 101 |
+
|
| 102 |
+
for account in accounts:
|
| 103 |
+
username = account["username"]
|
| 104 |
+
next_renewal_str = account.get("next_renewal")
|
| 105 |
+
|
| 106 |
+
if next_renewal_str:
|
| 107 |
+
next_renewal = datetime.fromisoformat(next_renewal_str)
|
| 108 |
+
if now < next_renewal:
|
| 109 |
+
remaining = (next_renewal - now).days
|
| 110 |
+
logger.info(f"[{username}] Skipping - next renewal in {remaining} days")
|
| 111 |
+
continue
|
| 112 |
+
|
| 113 |
+
logger.info(f"[{username}] Starting renewal...")
|
| 114 |
+
success, results = renew_account(account)
|
| 115 |
+
if success:
|
| 116 |
+
renewed_count += 1
|
| 117 |
+
|
| 118 |
+
save_accounts(accounts)
|
| 119 |
+
logger.info(f"Done. Renewed {renewed_count}/{len(accounts)} accounts.")
|
| 120 |
+
return accounts
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
requests==2.31.0
|
| 2 |
+
fastapi==0.104.1
|
| 3 |
+
uvicorn==0.24.0
|