nopoh22 commited on
Commit
bcb31ef
·
verified ·
1 Parent(s): 07ab8db

Upload 7 files

Browse files
Files changed (7) hide show
  1. Dockerfile +15 -0
  2. README.md +33 -0
  3. app.py +133 -0
  4. index.html +252 -0
  5. keep_alive.py +31 -0
  6. renewer.py +120 -0
  7. 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