Update app.py
Browse files
app.py
CHANGED
|
@@ -14,25 +14,22 @@ import threading
|
|
| 14 |
from huggingface_hub import HfApi, hf_hub_download
|
| 15 |
from huggingface_hub.utils import RepositoryNotFoundError
|
| 16 |
|
| 17 |
-
|
| 18 |
-
BOT_TOKEN = os.getenv("BOT_TOKEN", "7566834146:AAGiG4MaTZZvvbTVsqEJVG5SYK5hUlc_Ewo") # Use environment variable or default
|
| 19 |
HOST = '0.0.0.0'
|
| 20 |
PORT = 7860
|
| 21 |
-
DATA_FILE = 'data.json'
|
| 22 |
|
| 23 |
-
# Hugging Face Settings
|
| 24 |
REPO_ID = "flpolprojects/teledata"
|
| 25 |
-
HF_DATA_FILE_PATH = "data.json"
|
| 26 |
-
HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
|
| 27 |
-
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 28 |
|
| 29 |
app = Flask(__name__)
|
| 30 |
logging.basicConfig(level=logging.INFO)
|
| 31 |
-
app.secret_key = os.urandom(24)
|
| 32 |
|
| 33 |
-
# --- Hugging Face & Data Handling ---
|
| 34 |
_data_lock = threading.Lock()
|
| 35 |
-
visitor_data_cache = {}
|
| 36 |
|
| 37 |
def download_data_from_hf():
|
| 38 |
global visitor_data_cache
|
|
@@ -48,11 +45,10 @@ def download_data_from_hf():
|
|
| 48 |
token=HF_TOKEN_READ,
|
| 49 |
local_dir=".",
|
| 50 |
local_dir_use_symlinks=False,
|
| 51 |
-
force_download=True,
|
| 52 |
-
etag_timeout=10
|
| 53 |
)
|
| 54 |
logging.info("Data file successfully downloaded from Hugging Face.")
|
| 55 |
-
# Force reload from downloaded file
|
| 56 |
with _data_lock:
|
| 57 |
try:
|
| 58 |
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
|
@@ -64,16 +60,14 @@ def download_data_from_hf():
|
|
| 64 |
return True
|
| 65 |
except RepositoryNotFoundError:
|
| 66 |
logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download data.")
|
| 67 |
-
# Don't clear local cache if repo not found, might have local data
|
| 68 |
except Exception as e:
|
| 69 |
logging.error(f"Error downloading data from Hugging Face: {e}")
|
| 70 |
-
# Don't clear local cache on generic download errors
|
| 71 |
return False
|
| 72 |
|
| 73 |
def load_visitor_data():
|
| 74 |
global visitor_data_cache
|
| 75 |
with _data_lock:
|
| 76 |
-
if not visitor_data_cache:
|
| 77 |
try:
|
| 78 |
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 79 |
visitor_data_cache = json.load(f)
|
|
@@ -89,17 +83,15 @@ def load_visitor_data():
|
|
| 89 |
visitor_data_cache = {}
|
| 90 |
return visitor_data_cache
|
| 91 |
|
| 92 |
-
def save_visitor_data(
|
|
|
|
| 93 |
with _data_lock:
|
| 94 |
try:
|
| 95 |
-
|
| 96 |
-
visitor_data_cache.update(data)
|
| 97 |
-
# Save updated cache to file
|
| 98 |
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 99 |
json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
|
| 100 |
logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
|
| 101 |
-
|
| 102 |
-
upload_data_to_hf_async() # Use async upload
|
| 103 |
except Exception as e:
|
| 104 |
logging.error(f"Error saving visitor data: {e}")
|
| 105 |
|
|
@@ -113,7 +105,7 @@ def upload_data_to_hf():
|
|
| 113 |
|
| 114 |
try:
|
| 115 |
api = HfApi()
|
| 116 |
-
with _data_lock:
|
| 117 |
file_content_exists = os.path.getsize(DATA_FILE) > 0
|
| 118 |
if not file_content_exists:
|
| 119 |
logging.warning(f"{DATA_FILE} is empty. Skipping upload.")
|
|
@@ -131,10 +123,8 @@ def upload_data_to_hf():
|
|
| 131 |
logging.info("Visitor data successfully uploaded to Hugging Face.")
|
| 132 |
except Exception as e:
|
| 133 |
logging.error(f"Error uploading data to Hugging Face: {e}")
|
| 134 |
-
# Consider adding retry logic here if needed
|
| 135 |
|
| 136 |
def upload_data_to_hf_async():
|
| 137 |
-
# Run upload in a separate thread to avoid blocking web requests
|
| 138 |
upload_thread = threading.Thread(target=upload_data_to_hf, daemon=True)
|
| 139 |
upload_thread.start()
|
| 140 |
|
|
@@ -143,11 +133,10 @@ def periodic_backup():
|
|
| 143 |
logging.info("Periodic backup disabled: HF_TOKEN_WRITE not set.")
|
| 144 |
return
|
| 145 |
while True:
|
| 146 |
-
time.sleep(3600)
|
| 147 |
logging.info("Initiating periodic backup...")
|
| 148 |
upload_data_to_hf()
|
| 149 |
|
| 150 |
-
# --- Telegram Verification ---
|
| 151 |
def verify_telegram_data(init_data_str):
|
| 152 |
try:
|
| 153 |
parsed_data = parse_qs(init_data_str)
|
|
@@ -167,8 +156,8 @@ def verify_telegram_data(init_data_str):
|
|
| 167 |
if calculated_hash == received_hash:
|
| 168 |
auth_date = int(parsed_data.get('auth_date', [0])[0])
|
| 169 |
current_time = int(time.time())
|
| 170 |
-
if current_time - auth_date > 86400:
|
| 171 |
-
logging.warning(f"Telegram InitData is older than
|
| 172 |
return parsed_data, True
|
| 173 |
else:
|
| 174 |
logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
|
|
@@ -177,7 +166,6 @@ def verify_telegram_data(init_data_str):
|
|
| 177 |
logging.error(f"Error verifying Telegram data: {e}")
|
| 178 |
return None, False
|
| 179 |
|
| 180 |
-
# --- HTML Templates ---
|
| 181 |
TEMPLATE = """
|
| 182 |
<!DOCTYPE html>
|
| 183 |
<html lang="ru">
|
|
@@ -186,6 +174,7 @@ TEMPLATE = """
|
|
| 186 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
|
| 187 |
<title>Morshen Group</title>
|
| 188 |
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
|
|
|
| 189 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 190 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 191 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
@@ -200,7 +189,7 @@ TEMPLATE = """
|
|
| 200 |
--tg-theme-secondary-bg-color: {{ theme.secondary_bg_color | default('#1e1e1e') }};
|
| 201 |
|
| 202 |
--bg-gradient: linear-gradient(160deg, #1a232f 0%, #121212 100%);
|
| 203 |
-
--card-bg: rgba(44, 44, 46, 0.8);
|
| 204 |
--card-bg-solid: #2c2c2e;
|
| 205 |
--text-color: var(--tg-theme-text-color);
|
| 206 |
--text-secondary-color: var(--tg-theme-hint-color);
|
|
@@ -208,16 +197,16 @@ TEMPLATE = """
|
|
| 208 |
--accent-gradient-green: linear-gradient(95deg, #34c759, #30d158);
|
| 209 |
--tag-bg: rgba(255, 255, 255, 0.1);
|
| 210 |
--border-radius-s: 8px;
|
| 211 |
-
--border-radius-m: 14px;
|
| 212 |
-
--border-radius-l: 18px;
|
| 213 |
--padding-s: 10px;
|
| 214 |
-
--padding-m: 18px;
|
| 215 |
-
--padding-l: 28px;
|
| 216 |
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 217 |
--shadow-color: rgba(0, 0, 0, 0.3);
|
| 218 |
--shadow-light: 0 4px 15px var(--shadow-color);
|
| 219 |
--shadow-medium: 0 6px 25px var(--shadow-color);
|
| 220 |
-
--backdrop-blur: 10px;
|
| 221 |
}
|
| 222 |
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 223 |
html {
|
|
@@ -229,11 +218,11 @@ TEMPLATE = """
|
|
| 229 |
background: var(--bg-gradient);
|
| 230 |
color: var(--text-color);
|
| 231 |
padding: var(--padding-m);
|
| 232 |
-
padding-bottom: 120px;
|
| 233 |
overscroll-behavior-y: none;
|
| 234 |
-webkit-font-smoothing: antialiased;
|
| 235 |
-moz-osx-font-smoothing: grayscale;
|
| 236 |
-
visibility: hidden;
|
| 237 |
min-height: 100vh;
|
| 238 |
}
|
| 239 |
.container {
|
|
@@ -252,7 +241,7 @@ TEMPLATE = """
|
|
| 252 |
}
|
| 253 |
.logo { display: flex; align-items: center; gap: var(--padding-s); }
|
| 254 |
.logo img {
|
| 255 |
-
width: 50px;
|
| 256 |
height: 50px;
|
| 257 |
border-radius: 50%;
|
| 258 |
background-color: var(--card-bg-solid);
|
|
@@ -260,12 +249,12 @@ TEMPLATE = """
|
|
| 260 |
border: 2px solid rgba(255, 255, 255, 0.15);
|
| 261 |
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
| 262 |
}
|
| 263 |
-
.logo span { font-size: 1.6em; font-weight: 700; letter-spacing: -0.5px; }
|
| 264 |
.btn {
|
| 265 |
display: inline-flex; align-items: center; justify-content: center;
|
| 266 |
padding: 12px var(--padding-m); border-radius: var(--border-radius-m);
|
| 267 |
background: var(--accent-gradient); color: var(--tg-theme-button-text-color);
|
| 268 |
-
text-decoration: none; font-weight: 600;
|
| 269 |
border: none; cursor: pointer;
|
| 270 |
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
| 271 |
gap: 8px; font-size: 1em;
|
|
@@ -298,14 +287,14 @@ TEMPLATE = """
|
|
| 298 |
background-color: var(--card-bg);
|
| 299 |
border-radius: var(--border-radius-l);
|
| 300 |
padding: var(--padding-l);
|
| 301 |
-
margin-bottom: 0;
|
| 302 |
box-shadow: var(--shadow-medium);
|
| 303 |
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 304 |
backdrop-filter: blur(var(--backdrop-blur));
|
| 305 |
-webkit-backdrop-filter: blur(var(--backdrop-blur));
|
| 306 |
}
|
| 307 |
.section-title {
|
| 308 |
-
font-size: 2em;
|
| 309 |
font-weight: 700; margin-bottom: var(--padding-s); line-height: 1.25;
|
| 310 |
letter-spacing: -0.6px;
|
| 311 |
}
|
|
@@ -314,7 +303,7 @@ TEMPLATE = """
|
|
| 314 |
margin-bottom: var(--padding-m);
|
| 315 |
}
|
| 316 |
.description {
|
| 317 |
-
font-size: 1.05em; line-height: 1.6; color: var(--text-secondary-color);
|
| 318 |
margin-bottom: var(--padding-m);
|
| 319 |
}
|
| 320 |
.stats-grid {
|
|
@@ -334,7 +323,7 @@ TEMPLATE = """
|
|
| 334 |
background-color: var(--card-bg-solid);
|
| 335 |
padding: var(--padding-m); border-radius: var(--border-radius-m);
|
| 336 |
margin-bottom: var(--padding-s); display: flex; align-items: center;
|
| 337 |
-
gap: var(--padding-m);
|
| 338 |
font-size: 1.1em; font-weight: 500;
|
| 339 |
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 340 |
transition: background-color 0.2s ease, transform 0.2s ease;
|
|
@@ -350,11 +339,11 @@ TEMPLATE = """
|
|
| 350 |
}
|
| 351 |
.save-card-button {
|
| 352 |
position: fixed;
|
| 353 |
-
bottom: 30px;
|
| 354 |
left: 50%;
|
| 355 |
transform: translateX(-50%);
|
| 356 |
-
padding: 14px 28px;
|
| 357 |
-
border-radius: 30px;
|
| 358 |
background: var(--accent-gradient-green);
|
| 359 |
color: var(--tg-theme-button-text-color);
|
| 360 |
text-decoration: none;
|
|
@@ -363,26 +352,24 @@ TEMPLATE = """
|
|
| 363 |
cursor: pointer;
|
| 364 |
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 365 |
z-index: 1000;
|
| 366 |
-
box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.5);
|
| 367 |
-
font-size: 1.05em;
|
| 368 |
display: flex;
|
| 369 |
align-items: center;
|
| 370 |
-
gap: 10px;
|
| 371 |
backdrop-filter: blur(5px);
|
| 372 |
-webkit-backdrop-filter: blur(5px);
|
| 373 |
}
|
| 374 |
.save-card-button:hover {
|
| 375 |
opacity: 0.95;
|
| 376 |
-
transform: translateX(-50%) scale(1.05);
|
| 377 |
box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.3);
|
| 378 |
}
|
| 379 |
.save-card-button i { font-size: 1.2em; }
|
| 380 |
-
|
| 381 |
-
/* Modal Styles */
|
| 382 |
.modal {
|
| 383 |
display: none; position: fixed; z-index: 1001;
|
| 384 |
left: 0; top: 0; width: 100%; height: 100%;
|
| 385 |
-
overflow: auto; background-color: rgba(0,0,0,0.7);
|
| 386 |
backdrop-filter: blur(8px);
|
| 387 |
-webkit-backdrop-filter: blur(8px);
|
| 388 |
animation: fadeIn 0.3s ease-out;
|
|
@@ -409,8 +396,6 @@ TEMPLATE = """
|
|
| 409 |
.modal-text { font-size: 1.2em; line-height: 1.6; margin-bottom: var(--padding-s); word-wrap: break-word; }
|
| 410 |
.modal-text b { color: var(--tg-theme-link-color); font-weight: 600; }
|
| 411 |
.modal-instruction { font-size: 1em; color: var(--text-secondary-color); margin-top: var(--padding-m); }
|
| 412 |
-
|
| 413 |
-
/* Icons */
|
| 414 |
.icon { display: inline-block; width: 1.2em; text-align: center; margin-right: 8px; opacity: 0.9; }
|
| 415 |
.icon-save::before { content: '💾'; }
|
| 416 |
.icon-web::before { content: '🌐'; }
|
|
@@ -431,8 +416,12 @@ TEMPLATE = """
|
|
| 431 |
.icon-link::before { content: '🔗'; }
|
| 432 |
.icon-leader::before { content: '🏆'; }
|
| 433 |
.icon-company::before { content: '🏢'; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
|
| 435 |
-
/* Responsive adjustments */
|
| 436 |
@media (max-width: 480px) {
|
| 437 |
.section-title { font-size: 1.8em; }
|
| 438 |
.logo span { font-size: 1.4em; }
|
|
@@ -466,6 +455,13 @@ TEMPLATE = """
|
|
| 466 |
<a href="#" class="btn contact-link" style="background: var(--accent-gradient-green); width: 100%; margin-top: var(--padding-s);">
|
| 467 |
<i class="icon icon-contact"></i>Написать нам в Telegram
|
| 468 |
</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
</section>
|
| 470 |
|
| 471 |
<section class="ecosystem-header">
|
|
@@ -562,7 +558,6 @@ TEMPLATE = """
|
|
| 562 |
<i class="icon icon-save"></i>Сохранить визитку
|
| 563 |
</button>
|
| 564 |
|
| 565 |
-
<!-- The Modal -->
|
| 566 |
<div id="saveModal" class="modal">
|
| 567 |
<div class="modal-content">
|
| 568 |
<span class="modal-close" id="modal-close-btn">×</span>
|
|
@@ -573,9 +568,9 @@ TEMPLATE = """
|
|
| 573 |
</div>
|
| 574 |
</div>
|
| 575 |
|
| 576 |
-
|
| 577 |
<script>
|
| 578 |
const tg = window.Telegram.WebApp;
|
|
|
|
| 579 |
|
| 580 |
function applyTheme(themeParams) {
|
| 581 |
const root = document.documentElement;
|
|
@@ -586,8 +581,6 @@ TEMPLATE = """
|
|
| 586 |
root.style.setProperty('--tg-theme-button-color', themeParams.button_color || '#31a5f5');
|
| 587 |
root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || '#ffffff');
|
| 588 |
root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || '#1e1e1e');
|
| 589 |
-
|
| 590 |
-
// Optional: Convert main bg color to RGB for glow effect alpha
|
| 591 |
try {
|
| 592 |
const bgColor = themeParams.bg_color || '#121212';
|
| 593 |
const r = parseInt(bgColor.slice(1, 3), 16);
|
|
@@ -595,16 +588,60 @@ TEMPLATE = """
|
|
| 595 |
const b = parseInt(bgColor.slice(5, 7), 16);
|
| 596 |
root.style.setProperty('--tg-theme-bg-color-rgb', `${r}, ${g}, ${b}`);
|
| 597 |
} catch (e) {
|
| 598 |
-
root.style.setProperty('--tg-theme-bg-color-rgb', `18, 18, 18`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 599 |
}
|
| 600 |
}
|
| 601 |
|
|
|
|
| 602 |
function setupTelegram() {
|
| 603 |
if (!tg || !tg.initData) {
|
| 604 |
console.error("Telegram WebApp script not loaded or initData is missing.");
|
| 605 |
const greetingElement = document.getElementById('greeting');
|
| 606 |
if(greetingElement) greetingElement.textContent = 'Не удалось связаться с Telegram.';
|
| 607 |
-
// Apply default dark theme maybe? Or leave as is.
|
| 608 |
document.body.style.visibility = 'visible';
|
| 609 |
return;
|
| 610 |
}
|
|
@@ -615,7 +652,6 @@ TEMPLATE = """
|
|
| 615 |
applyTheme(tg.themeParams);
|
| 616 |
tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
|
| 617 |
|
| 618 |
-
// Send initData for verification and user logging
|
| 619 |
fetch('/verify', {
|
| 620 |
method: 'POST',
|
| 621 |
headers: {
|
|
@@ -631,18 +667,21 @@ TEMPLATE = """
|
|
| 631 |
.then(data => {
|
| 632 |
if (data.status === 'ok' && data.verified) {
|
| 633 |
console.log('Backend verification successful.');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
} else {
|
| 635 |
console.warn('Backend verification failed:', data.message);
|
| 636 |
-
|
| 637 |
}
|
| 638 |
})
|
| 639 |
.catch(error => {
|
| 640 |
console.error('Error sending initData for verification:', error);
|
| 641 |
-
|
| 642 |
});
|
| 643 |
|
| 644 |
-
|
| 645 |
-
// User Greeting (using unsafe data for immediate feedback)
|
| 646 |
const user = tg.initDataUnsafe?.user;
|
| 647 |
const greetingElement = document.getElementById('greeting');
|
| 648 |
if (user) {
|
|
@@ -653,16 +692,14 @@ TEMPLATE = """
|
|
| 653 |
console.warn('Telegram User data not available (initDataUnsafe.user is empty).');
|
| 654 |
}
|
| 655 |
|
| 656 |
-
// Contact Links
|
| 657 |
const contactButtons = document.querySelectorAll('.contact-link');
|
| 658 |
contactButtons.forEach(button => {
|
| 659 |
button.addEventListener('click', (e) => {
|
| 660 |
e.preventDefault();
|
| 661 |
-
tg.openTelegramLink('https://t.me/morshenkhan');
|
| 662 |
});
|
| 663 |
});
|
| 664 |
|
| 665 |
-
// Modal Setup
|
| 666 |
const modal = document.getElementById("saveModal");
|
| 667 |
const saveCardBtn = document.getElementById("save-card-btn");
|
| 668 |
const closeBtn = document.getElementById("modal-close-btn");
|
|
@@ -675,20 +712,59 @@ TEMPLATE = """
|
|
| 675 |
tg.HapticFeedback.impactOccurred('light');
|
| 676 |
}
|
| 677 |
});
|
| 678 |
-
|
| 679 |
-
closeBtn.addEventListener('click', () => {
|
| 680 |
-
modal.style.display = "none";
|
| 681 |
-
});
|
| 682 |
-
|
| 683 |
window.addEventListener('click', (event) => {
|
| 684 |
-
if (event.target == modal) {
|
| 685 |
-
modal.style.display = "none";
|
| 686 |
-
}
|
| 687 |
});
|
| 688 |
} else {
|
| 689 |
console.error("Modal elements not found!");
|
| 690 |
}
|
| 691 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 692 |
document.body.style.visibility = 'visible';
|
| 693 |
}
|
| 694 |
|
|
@@ -704,9 +780,8 @@ TEMPLATE = """
|
|
| 704 |
if(greetingElement) greetingElement.textContent = 'Ошибка загрузки интерфейса Telegram.';
|
| 705 |
document.body.style.visibility = 'visible';
|
| 706 |
}
|
| 707 |
-
}, 3500);
|
| 708 |
}
|
| 709 |
-
|
| 710 |
</script>
|
| 711 |
</body>
|
| 712 |
</html>
|
|
@@ -750,7 +825,7 @@ ADMIN_TEMPLATE = """
|
|
| 750 |
h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
|
| 751 |
.user-grid {
|
| 752 |
display: grid;
|
| 753 |
-
grid-template-columns: repeat(auto-fill, minmax(
|
| 754 |
gap: var(--padding);
|
| 755 |
margin-top: var(--padding);
|
| 756 |
}
|
|
@@ -774,12 +849,12 @@ ADMIN_TEMPLATE = """
|
|
| 774 |
width: 80px; height: 80px;
|
| 775 |
border-radius: 50%; margin-bottom: 1rem;
|
| 776 |
object-fit: cover; border: 3px solid var(--admin-border);
|
| 777 |
-
background-color: #eee;
|
| 778 |
}
|
| 779 |
.user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
|
| 780 |
.user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
|
| 781 |
-
.user-card .details { font-size: 0.9em; color: #495057; word-break: break-
|
| 782 |
-
.user-card .detail-item { margin-bottom: 0.3rem; }
|
| 783 |
.user-card .detail-item strong { color: var(--admin-text); }
|
| 784 |
.user-card .timestamp { font-size: 0.8em; color: var(--admin-secondary); margin-top: 1rem; }
|
| 785 |
.no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
|
|
@@ -802,8 +877,6 @@ ADMIN_TEMPLATE = """
|
|
| 802 |
transition: background-color 0.2s ease;
|
| 803 |
}
|
| 804 |
.refresh-btn:hover { background-color: #0b5ed7; }
|
| 805 |
-
|
| 806 |
-
/* Admin Controls */
|
| 807 |
.admin-controls {
|
| 808 |
background: var(--admin-card-bg);
|
| 809 |
padding: var(--padding);
|
|
@@ -833,7 +906,7 @@ ADMIN_TEMPLATE = """
|
|
| 833 |
.admin-controls .loader {
|
| 834 |
border: 4px solid #f3f3f3; border-radius: 50%; border-top: 4px solid var(--admin-primary);
|
| 835 |
width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-left: 10px; vertical-align: middle;
|
| 836 |
-
display: none;
|
| 837 |
}
|
| 838 |
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
| 839 |
</style>
|
|
@@ -862,13 +935,14 @@ ADMIN_TEMPLATE = """
|
|
| 862 |
{% if user.username %}
|
| 863 |
<div class="username"><a href="https://t.me/{{ user.username }}" target="_blank" style="color: inherit; text-decoration: none;">@{{ user.username }}</a></div>
|
| 864 |
{% else %}
|
| 865 |
-
<div class="username" style="height: 1.3em;"></div>
|
| 866 |
{% endif %}
|
| 867 |
<div class="details">
|
| 868 |
<div class="detail-item"><strong>ID:</strong> {{ user.id }}</div>
|
| 869 |
<div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
|
| 870 |
<div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
|
| 871 |
<div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
|
|
|
|
| 872 |
</div>
|
| 873 |
<div class="timestamp">Визит: {{ user.visited_at_str }}</div>
|
| 874 |
</div>
|
|
@@ -894,7 +968,7 @@ ADMIN_TEMPLATE = """
|
|
| 894 |
statusMessage.textContent = data.message;
|
| 895 |
statusMessage.style.color = 'var(--admin-success)';
|
| 896 |
if (action === 'скачивание') {
|
| 897 |
-
setTimeout(() => location.reload(), 1500);
|
| 898 |
}
|
| 899 |
} else {
|
| 900 |
throw new Error(data.message || 'Произошла ошибка');
|
|
@@ -907,27 +981,36 @@ ADMIN_TEMPLATE = """
|
|
| 907 |
loader.style.display = 'none';
|
| 908 |
}
|
| 909 |
}
|
| 910 |
-
|
| 911 |
-
function
|
| 912 |
-
handleFetch('/admin/download_data', 'скачивание');
|
| 913 |
-
}
|
| 914 |
-
|
| 915 |
-
function triggerUpload() {
|
| 916 |
-
handleFetch('/admin/upload_data', 'загрузка');
|
| 917 |
-
}
|
| 918 |
</script>
|
| 919 |
</body>
|
| 920 |
</html>
|
| 921 |
"""
|
| 922 |
|
| 923 |
-
# --- Flask Routes ---
|
| 924 |
@app.route('/')
|
| 925 |
def index():
|
| 926 |
-
|
| 927 |
-
# For simplicity, we let the JS handle theme application after tg.ready()
|
| 928 |
-
theme_params = {} # Or load from request if needed
|
| 929 |
return render_template_string(TEMPLATE, theme=theme_params)
|
| 930 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 931 |
@app.route('/verify', methods=['POST'])
|
| 932 |
def verify_data():
|
| 933 |
try:
|
|
@@ -937,7 +1020,6 @@ def verify_data():
|
|
| 937 |
return jsonify({"status": "error", "message": "Missing initData"}), 400
|
| 938 |
|
| 939 |
user_data_parsed, is_valid = verify_telegram_data(init_data_str)
|
| 940 |
-
|
| 941 |
user_info_dict = {}
|
| 942 |
if user_data_parsed and 'user' in user_data_parsed:
|
| 943 |
try:
|
|
@@ -945,48 +1027,115 @@ def verify_data():
|
|
| 945 |
user_info_dict = json.loads(user_json_str)
|
| 946 |
except Exception as e:
|
| 947 |
logging.error(f"Could not parse user JSON: {e}")
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 972 |
else:
|
| 973 |
-
logging.warning(f"Verification failed for user: {user_info_dict.get('id')}")
|
| 974 |
return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
|
| 975 |
|
| 976 |
except Exception as e:
|
| 977 |
logging.exception("Error in /verify endpoint")
|
| 978 |
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
| 979 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 980 |
@app.route('/admin')
|
| 981 |
def admin_panel():
|
| 982 |
-
|
| 983 |
-
current_data = load_visitor_data() # Load from cache/file
|
| 984 |
users_list = list(current_data.values())
|
| 985 |
return render_template_string(ADMIN_TEMPLATE, users=users_list)
|
| 986 |
|
| 987 |
@app.route('/admin/download_data', methods=['POST'])
|
| 988 |
def admin_trigger_download():
|
| 989 |
-
# WARNING: Unprotected endpoint
|
| 990 |
success = download_data_from_hf()
|
| 991 |
if success:
|
| 992 |
return jsonify({"status": "ok", "message": "Скачивание данных с Hugging Face завершено. Страница будет обновлена."})
|
|
@@ -995,44 +1144,34 @@ def admin_trigger_download():
|
|
| 995 |
|
| 996 |
@app.route('/admin/upload_data', methods=['POST'])
|
| 997 |
def admin_trigger_upload():
|
| 998 |
-
# WARNING: Unprotected endpoint
|
| 999 |
if not HF_TOKEN_WRITE:
|
| 1000 |
return jsonify({"status": "error", "message": "HF_TOKEN_WRITE не настроен на сервере."}), 400
|
| 1001 |
-
upload_data_to_hf_async()
|
| 1002 |
return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
|
| 1003 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1004 |
|
| 1005 |
-
# --- App Initialization ---
|
| 1006 |
if __name__ == '__main__':
|
| 1007 |
-
print("---")
|
| 1008 |
print("--- MORSHEN GROUP MINI APP SERVER ---")
|
| 1009 |
-
print("---")
|
| 1010 |
print(f"Flask server starting on http://{HOST}:{PORT}")
|
| 1011 |
print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
|
| 1012 |
-
print(f"Visitor data file: {DATA_FILE}")
|
| 1013 |
-
print(f"Hugging Face Repo: {REPO_ID}")
|
| 1014 |
-
print(f"HF Data Path: {HF_DATA_FILE_PATH}")
|
| 1015 |
if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
|
| 1016 |
-
print("---")
|
| 1017 |
-
print("--- WARNING: HUGGING FACE TOKEN(S) NOT SET ---")
|
| 1018 |
-
print("--- Backup/restore functionality will be limited. Set HF_TOKEN_READ and HF_TOKEN_WRITE environment variables.")
|
| 1019 |
-
print("---")
|
| 1020 |
else:
|
| 1021 |
-
print("--- Hugging Face tokens found.")
|
| 1022 |
-
# Initial attempt to download data on startup
|
| 1023 |
-
print("--- Attempting initial data download from Hugging Face...")
|
| 1024 |
download_data_from_hf()
|
| 1025 |
|
| 1026 |
-
# Load initial data from local file (might have been updated by download)
|
| 1027 |
load_visitor_data()
|
|
|
|
| 1028 |
|
| 1029 |
-
print("---")
|
| 1030 |
-
print("--- SECURITY WARNING ---")
|
| 1031 |
-
print("--- The /admin route and its sub-routes are NOT protected.")
|
| 1032 |
-
print("--- Implement proper authentication before deploying.")
|
| 1033 |
-
print("---")
|
| 1034 |
-
|
| 1035 |
-
# Start periodic backup thread if write token is available
|
| 1036 |
if HF_TOKEN_WRITE:
|
| 1037 |
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|
| 1038 |
backup_thread.start()
|
|
@@ -1041,7 +1180,4 @@ if __name__ == '__main__':
|
|
| 1041 |
print("--- Periodic backup disabled (HF_TOKEN_WRITE missing).")
|
| 1042 |
|
| 1043 |
print("--- Server Ready ---")
|
| 1044 |
-
|
| 1045 |
-
# from waitress import serve
|
| 1046 |
-
# serve(app, host=HOST, port=PORT)
|
| 1047 |
-
app.run(host=HOST, port=PORT, debug=False) # debug=False for production recommended
|
|
|
|
| 14 |
from huggingface_hub import HfApi, hf_hub_download
|
| 15 |
from huggingface_hub.utils import RepositoryNotFoundError
|
| 16 |
|
| 17 |
+
BOT_TOKEN = os.getenv("BOT_TOKEN", "7566834146:AAGiG4MaTZZvvbTVsqEJVG5SYK5hUlc_Ewo")
|
|
|
|
| 18 |
HOST = '0.0.0.0'
|
| 19 |
PORT = 7860
|
| 20 |
+
DATA_FILE = 'data.json'
|
| 21 |
|
|
|
|
| 22 |
REPO_ID = "flpolprojects/teledata"
|
| 23 |
+
HF_DATA_FILE_PATH = "data.json"
|
| 24 |
+
HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
|
| 25 |
+
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 26 |
|
| 27 |
app = Flask(__name__)
|
| 28 |
logging.basicConfig(level=logging.INFO)
|
| 29 |
+
app.secret_key = os.urandom(24)
|
| 30 |
|
|
|
|
| 31 |
_data_lock = threading.Lock()
|
| 32 |
+
visitor_data_cache = {}
|
| 33 |
|
| 34 |
def download_data_from_hf():
|
| 35 |
global visitor_data_cache
|
|
|
|
| 45 |
token=HF_TOKEN_READ,
|
| 46 |
local_dir=".",
|
| 47 |
local_dir_use_symlinks=False,
|
| 48 |
+
force_download=True,
|
| 49 |
+
etag_timeout=10
|
| 50 |
)
|
| 51 |
logging.info("Data file successfully downloaded from Hugging Face.")
|
|
|
|
| 52 |
with _data_lock:
|
| 53 |
try:
|
| 54 |
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
|
|
|
| 60 |
return True
|
| 61 |
except RepositoryNotFoundError:
|
| 62 |
logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download data.")
|
|
|
|
| 63 |
except Exception as e:
|
| 64 |
logging.error(f"Error downloading data from Hugging Face: {e}")
|
|
|
|
| 65 |
return False
|
| 66 |
|
| 67 |
def load_visitor_data():
|
| 68 |
global visitor_data_cache
|
| 69 |
with _data_lock:
|
| 70 |
+
if not visitor_data_cache:
|
| 71 |
try:
|
| 72 |
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
| 73 |
visitor_data_cache = json.load(f)
|
|
|
|
| 83 |
visitor_data_cache = {}
|
| 84 |
return visitor_data_cache
|
| 85 |
|
| 86 |
+
def save_visitor_data(data_to_update):
|
| 87 |
+
global visitor_data_cache
|
| 88 |
with _data_lock:
|
| 89 |
try:
|
| 90 |
+
visitor_data_cache.update(data_to_update)
|
|
|
|
|
|
|
| 91 |
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 92 |
json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
|
| 93 |
logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
|
| 94 |
+
upload_data_to_hf_async()
|
|
|
|
| 95 |
except Exception as e:
|
| 96 |
logging.error(f"Error saving visitor data: {e}")
|
| 97 |
|
|
|
|
| 105 |
|
| 106 |
try:
|
| 107 |
api = HfApi()
|
| 108 |
+
with _data_lock:
|
| 109 |
file_content_exists = os.path.getsize(DATA_FILE) > 0
|
| 110 |
if not file_content_exists:
|
| 111 |
logging.warning(f"{DATA_FILE} is empty. Skipping upload.")
|
|
|
|
| 123 |
logging.info("Visitor data successfully uploaded to Hugging Face.")
|
| 124 |
except Exception as e:
|
| 125 |
logging.error(f"Error uploading data to Hugging Face: {e}")
|
|
|
|
| 126 |
|
| 127 |
def upload_data_to_hf_async():
|
|
|
|
| 128 |
upload_thread = threading.Thread(target=upload_data_to_hf, daemon=True)
|
| 129 |
upload_thread.start()
|
| 130 |
|
|
|
|
| 133 |
logging.info("Periodic backup disabled: HF_TOKEN_WRITE not set.")
|
| 134 |
return
|
| 135 |
while True:
|
| 136 |
+
time.sleep(3600)
|
| 137 |
logging.info("Initiating periodic backup...")
|
| 138 |
upload_data_to_hf()
|
| 139 |
|
|
|
|
| 140 |
def verify_telegram_data(init_data_str):
|
| 141 |
try:
|
| 142 |
parsed_data = parse_qs(init_data_str)
|
|
|
|
| 156 |
if calculated_hash == received_hash:
|
| 157 |
auth_date = int(parsed_data.get('auth_date', [0])[0])
|
| 158 |
current_time = int(time.time())
|
| 159 |
+
if current_time - auth_date > 86400:
|
| 160 |
+
logging.warning(f"Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}).")
|
| 161 |
return parsed_data, True
|
| 162 |
else:
|
| 163 |
logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
|
|
|
|
| 166 |
logging.error(f"Error verifying Telegram data: {e}")
|
| 167 |
return None, False
|
| 168 |
|
|
|
|
| 169 |
TEMPLATE = """
|
| 170 |
<!DOCTYPE html>
|
| 171 |
<html lang="ru">
|
|
|
|
| 174 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
|
| 175 |
<title>Morshen Group</title>
|
| 176 |
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 177 |
+
<script src="https://unpkg.com/@tonconnect/ui@latest/dist/tonconnect-ui.min.js"></script>
|
| 178 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 179 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 180 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
| 189 |
--tg-theme-secondary-bg-color: {{ theme.secondary_bg_color | default('#1e1e1e') }};
|
| 190 |
|
| 191 |
--bg-gradient: linear-gradient(160deg, #1a232f 0%, #121212 100%);
|
| 192 |
+
--card-bg: rgba(44, 44, 46, 0.8);
|
| 193 |
--card-bg-solid: #2c2c2e;
|
| 194 |
--text-color: var(--tg-theme-text-color);
|
| 195 |
--text-secondary-color: var(--tg-theme-hint-color);
|
|
|
|
| 197 |
--accent-gradient-green: linear-gradient(95deg, #34c759, #30d158);
|
| 198 |
--tag-bg: rgba(255, 255, 255, 0.1);
|
| 199 |
--border-radius-s: 8px;
|
| 200 |
+
--border-radius-m: 14px;
|
| 201 |
+
--border-radius-l: 18px;
|
| 202 |
--padding-s: 10px;
|
| 203 |
+
--padding-m: 18px;
|
| 204 |
+
--padding-l: 28px;
|
| 205 |
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 206 |
--shadow-color: rgba(0, 0, 0, 0.3);
|
| 207 |
--shadow-light: 0 4px 15px var(--shadow-color);
|
| 208 |
--shadow-medium: 0 6px 25px var(--shadow-color);
|
| 209 |
+
--backdrop-blur: 10px;
|
| 210 |
}
|
| 211 |
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 212 |
html {
|
|
|
|
| 218 |
background: var(--bg-gradient);
|
| 219 |
color: var(--text-color);
|
| 220 |
padding: var(--padding-m);
|
| 221 |
+
padding-bottom: 120px;
|
| 222 |
overscroll-behavior-y: none;
|
| 223 |
-webkit-font-smoothing: antialiased;
|
| 224 |
-moz-osx-font-smoothing: grayscale;
|
| 225 |
+
visibility: hidden;
|
| 226 |
min-height: 100vh;
|
| 227 |
}
|
| 228 |
.container {
|
|
|
|
| 241 |
}
|
| 242 |
.logo { display: flex; align-items: center; gap: var(--padding-s); }
|
| 243 |
.logo img {
|
| 244 |
+
width: 50px;
|
| 245 |
height: 50px;
|
| 246 |
border-radius: 50%;
|
| 247 |
background-color: var(--card-bg-solid);
|
|
|
|
| 249 |
border: 2px solid rgba(255, 255, 255, 0.15);
|
| 250 |
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
| 251 |
}
|
| 252 |
+
.logo span { font-size: 1.6em; font-weight: 700; letter-spacing: -0.5px; }
|
| 253 |
.btn {
|
| 254 |
display: inline-flex; align-items: center; justify-content: center;
|
| 255 |
padding: 12px var(--padding-m); border-radius: var(--border-radius-m);
|
| 256 |
background: var(--accent-gradient); color: var(--tg-theme-button-text-color);
|
| 257 |
+
text-decoration: none; font-weight: 600;
|
| 258 |
border: none; cursor: pointer;
|
| 259 |
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
| 260 |
gap: 8px; font-size: 1em;
|
|
|
|
| 287 |
background-color: var(--card-bg);
|
| 288 |
border-radius: var(--border-radius-l);
|
| 289 |
padding: var(--padding-l);
|
| 290 |
+
margin-bottom: 0;
|
| 291 |
box-shadow: var(--shadow-medium);
|
| 292 |
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 293 |
backdrop-filter: blur(var(--backdrop-blur));
|
| 294 |
-webkit-backdrop-filter: blur(var(--backdrop-blur));
|
| 295 |
}
|
| 296 |
.section-title {
|
| 297 |
+
font-size: 2em;
|
| 298 |
font-weight: 700; margin-bottom: var(--padding-s); line-height: 1.25;
|
| 299 |
letter-spacing: -0.6px;
|
| 300 |
}
|
|
|
|
| 303 |
margin-bottom: var(--padding-m);
|
| 304 |
}
|
| 305 |
.description {
|
| 306 |
+
font-size: 1.05em; line-height: 1.6; color: var(--text-secondary-color);
|
| 307 |
margin-bottom: var(--padding-m);
|
| 308 |
}
|
| 309 |
.stats-grid {
|
|
|
|
| 323 |
background-color: var(--card-bg-solid);
|
| 324 |
padding: var(--padding-m); border-radius: var(--border-radius-m);
|
| 325 |
margin-bottom: var(--padding-s); display: flex; align-items: center;
|
| 326 |
+
gap: var(--padding-m);
|
| 327 |
font-size: 1.1em; font-weight: 500;
|
| 328 |
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 329 |
transition: background-color 0.2s ease, transform 0.2s ease;
|
|
|
|
| 339 |
}
|
| 340 |
.save-card-button {
|
| 341 |
position: fixed;
|
| 342 |
+
bottom: 30px;
|
| 343 |
left: 50%;
|
| 344 |
transform: translateX(-50%);
|
| 345 |
+
padding: 14px 28px;
|
| 346 |
+
border-radius: 30px;
|
| 347 |
background: var(--accent-gradient-green);
|
| 348 |
color: var(--tg-theme-button-text-color);
|
| 349 |
text-decoration: none;
|
|
|
|
| 352 |
cursor: pointer;
|
| 353 |
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 354 |
z-index: 1000;
|
| 355 |
+
box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.5);
|
| 356 |
+
font-size: 1.05em;
|
| 357 |
display: flex;
|
| 358 |
align-items: center;
|
| 359 |
+
gap: 10px;
|
| 360 |
backdrop-filter: blur(5px);
|
| 361 |
-webkit-backdrop-filter: blur(5px);
|
| 362 |
}
|
| 363 |
.save-card-button:hover {
|
| 364 |
opacity: 0.95;
|
| 365 |
+
transform: translateX(-50%) scale(1.05);
|
| 366 |
box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.3);
|
| 367 |
}
|
| 368 |
.save-card-button i { font-size: 1.2em; }
|
|
|
|
|
|
|
| 369 |
.modal {
|
| 370 |
display: none; position: fixed; z-index: 1001;
|
| 371 |
left: 0; top: 0; width: 100%; height: 100%;
|
| 372 |
+
overflow: auto; background-color: rgba(0,0,0,0.7);
|
| 373 |
backdrop-filter: blur(8px);
|
| 374 |
-webkit-backdrop-filter: blur(8px);
|
| 375 |
animation: fadeIn 0.3s ease-out;
|
|
|
|
| 396 |
.modal-text { font-size: 1.2em; line-height: 1.6; margin-bottom: var(--padding-s); word-wrap: break-word; }
|
| 397 |
.modal-text b { color: var(--tg-theme-link-color); font-weight: 600; }
|
| 398 |
.modal-instruction { font-size: 1em; color: var(--text-secondary-color); margin-top: var(--padding-m); }
|
|
|
|
|
|
|
| 399 |
.icon { display: inline-block; width: 1.2em; text-align: center; margin-right: 8px; opacity: 0.9; }
|
| 400 |
.icon-save::before { content: '💾'; }
|
| 401 |
.icon-web::before { content: '🌐'; }
|
|
|
|
| 416 |
.icon-link::before { content: '🔗'; }
|
| 417 |
.icon-leader::before { content: '🏆'; }
|
| 418 |
.icon-company::before { content: '🏢'; }
|
| 419 |
+
.icon-wallet::before { content: '💎'; } /* TON Wallet Icon */
|
| 420 |
+
|
| 421 |
+
#ton-connect-button-container { margin-top: var(--padding-m); }
|
| 422 |
+
#ton-wallet-info { margin-top: var(--padding-s); font-size: 0.9em; color: var(--text-secondary-color); text-align: center;}
|
| 423 |
+
#ton-wallet-info b { color: var(--tg-theme-link-color); }
|
| 424 |
|
|
|
|
| 425 |
@media (max-width: 480px) {
|
| 426 |
.section-title { font-size: 1.8em; }
|
| 427 |
.logo span { font-size: 1.4em; }
|
|
|
|
| 455 |
<a href="#" class="btn contact-link" style="background: var(--accent-gradient-green); width: 100%; margin-top: var(--padding-s);">
|
| 456 |
<i class="icon icon-contact"></i>Написать нам в Telegram
|
| 457 |
</a>
|
| 458 |
+
|
| 459 |
+
<div id="ton-connect-button-container">
|
| 460 |
+
<button id="ton-connect-btn" class="btn" style="width: 100%; background: var(--tg-theme-button-color);"><i class="icon icon-wallet"></i>Подключить TON кошелек</button>
|
| 461 |
+
</div>
|
| 462 |
+
<div id="ton-wallet-info" style="display: none;">
|
| 463 |
+
<p>TON кошелек: <b id="ton-wallet-address"></b></p>
|
| 464 |
+
</div>
|
| 465 |
</section>
|
| 466 |
|
| 467 |
<section class="ecosystem-header">
|
|
|
|
| 558 |
<i class="icon icon-save"></i>Сохранить визитку
|
| 559 |
</button>
|
| 560 |
|
|
|
|
| 561 |
<div id="saveModal" class="modal">
|
| 562 |
<div class="modal-content">
|
| 563 |
<span class="modal-close" id="modal-close-btn">×</span>
|
|
|
|
| 568 |
</div>
|
| 569 |
</div>
|
| 570 |
|
|
|
|
| 571 |
<script>
|
| 572 |
const tg = window.Telegram.WebApp;
|
| 573 |
+
let tonConnectUI;
|
| 574 |
|
| 575 |
function applyTheme(themeParams) {
|
| 576 |
const root = document.documentElement;
|
|
|
|
| 581 |
root.style.setProperty('--tg-theme-button-color', themeParams.button_color || '#31a5f5');
|
| 582 |
root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || '#ffffff');
|
| 583 |
root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || '#1e1e1e');
|
|
|
|
|
|
|
| 584 |
try {
|
| 585 |
const bgColor = themeParams.bg_color || '#121212';
|
| 586 |
const r = parseInt(bgColor.slice(1, 3), 16);
|
|
|
|
| 588 |
const b = parseInt(bgColor.slice(5, 7), 16);
|
| 589 |
root.style.setProperty('--tg-theme-bg-color-rgb', `${r}, ${g}, ${b}`);
|
| 590 |
} catch (e) {
|
| 591 |
+
root.style.setProperty('--tg-theme-bg-color-rgb', `18, 18, 18`);
|
| 592 |
+
}
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
function updateTonWalletUI(address) {
|
| 596 |
+
const tonConnectBtnContainer = document.getElementById('ton-connect-button-container');
|
| 597 |
+
const tonWalletInfoDiv = document.getElementById('ton-wallet-info');
|
| 598 |
+
const tonWalletAddressEl = document.getElementById('ton-wallet-address');
|
| 599 |
+
|
| 600 |
+
if (address) {
|
| 601 |
+
tonConnectBtnContainer.style.display = 'none';
|
| 602 |
+
tonWalletAddressEl.textContent = `${address.slice(0, 6)}...${address.slice(-4)}`;
|
| 603 |
+
tonWalletInfoDiv.style.display = 'block';
|
| 604 |
+
} else {
|
| 605 |
+
tonConnectBtnContainer.style.display = 'block';
|
| 606 |
+
tonWalletInfoDiv.style.display = 'none';
|
| 607 |
+
}
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
async function connectTonWalletBackend(walletAddress) {
|
| 611 |
+
try {
|
| 612 |
+
const response = await fetch('/connect_ton_wallet', {
|
| 613 |
+
method: 'POST',
|
| 614 |
+
headers: {
|
| 615 |
+
'Content-Type': 'application/json',
|
| 616 |
+
'Accept': 'application/json'
|
| 617 |
+
},
|
| 618 |
+
body: JSON.stringify({ initData: tg.initData, walletAddress: walletAddress }),
|
| 619 |
+
});
|
| 620 |
+
if (!response.ok) {
|
| 621 |
+
const errorData = await response.json().catch(() => ({}));
|
| 622 |
+
throw new Error(`HTTP error ${response.status}: ${errorData.message || 'Failed to connect wallet on backend'}`);
|
| 623 |
+
}
|
| 624 |
+
const data = await response.json();
|
| 625 |
+
if (data.status === 'ok') {
|
| 626 |
+
console.log('TON Wallet connected successfully on backend.');
|
| 627 |
+
updateTonWalletUI(walletAddress);
|
| 628 |
+
tg.HapticFeedback.notificationOccurred('success');
|
| 629 |
+
} else {
|
| 630 |
+
throw new Error(data.message || 'Backend rejected TON wallet connection.');
|
| 631 |
+
}
|
| 632 |
+
} catch (error) {
|
| 633 |
+
console.error('Error connecting TON wallet with backend:', error);
|
| 634 |
+
tg.showAlert(`Ошибка подключения кошелька: ${error.message}`);
|
| 635 |
+
// Revert UI if needed, or keep button visible for retry
|
| 636 |
}
|
| 637 |
}
|
| 638 |
|
| 639 |
+
|
| 640 |
function setupTelegram() {
|
| 641 |
if (!tg || !tg.initData) {
|
| 642 |
console.error("Telegram WebApp script not loaded or initData is missing.");
|
| 643 |
const greetingElement = document.getElementById('greeting');
|
| 644 |
if(greetingElement) greetingElement.textContent = 'Не удалось связаться с Telegram.';
|
|
|
|
| 645 |
document.body.style.visibility = 'visible';
|
| 646 |
return;
|
| 647 |
}
|
|
|
|
| 652 |
applyTheme(tg.themeParams);
|
| 653 |
tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
|
| 654 |
|
|
|
|
| 655 |
fetch('/verify', {
|
| 656 |
method: 'POST',
|
| 657 |
headers: {
|
|
|
|
| 667 |
.then(data => {
|
| 668 |
if (data.status === 'ok' && data.verified) {
|
| 669 |
console.log('Backend verification successful.');
|
| 670 |
+
if (data.ton_wallet_address) {
|
| 671 |
+
updateTonWalletUI(data.ton_wallet_address);
|
| 672 |
+
} else {
|
| 673 |
+
updateTonWalletUI(null);
|
| 674 |
+
}
|
| 675 |
} else {
|
| 676 |
console.warn('Backend verification failed:', data.message);
|
| 677 |
+
updateTonWalletUI(null);
|
| 678 |
}
|
| 679 |
})
|
| 680 |
.catch(error => {
|
| 681 |
console.error('Error sending initData for verification:', error);
|
| 682 |
+
updateTonWalletUI(null);
|
| 683 |
});
|
| 684 |
|
|
|
|
|
|
|
| 685 |
const user = tg.initDataUnsafe?.user;
|
| 686 |
const greetingElement = document.getElementById('greeting');
|
| 687 |
if (user) {
|
|
|
|
| 692 |
console.warn('Telegram User data not available (initDataUnsafe.user is empty).');
|
| 693 |
}
|
| 694 |
|
|
|
|
| 695 |
const contactButtons = document.querySelectorAll('.contact-link');
|
| 696 |
contactButtons.forEach(button => {
|
| 697 |
button.addEventListener('click', (e) => {
|
| 698 |
e.preventDefault();
|
| 699 |
+
tg.openTelegramLink('https://t.me/morshenkhan');
|
| 700 |
});
|
| 701 |
});
|
| 702 |
|
|
|
|
| 703 |
const modal = document.getElementById("saveModal");
|
| 704 |
const saveCardBtn = document.getElementById("save-card-btn");
|
| 705 |
const closeBtn = document.getElementById("modal-close-btn");
|
|
|
|
| 712 |
tg.HapticFeedback.impactOccurred('light');
|
| 713 |
}
|
| 714 |
});
|
| 715 |
+
closeBtn.addEventListener('click', () => { modal.style.display = "none"; });
|
|
|
|
|
|
|
|
|
|
|
|
|
| 716 |
window.addEventListener('click', (event) => {
|
| 717 |
+
if (event.target == modal) { modal.style.display = "none"; }
|
|
|
|
|
|
|
| 718 |
});
|
| 719 |
} else {
|
| 720 |
console.error("Modal elements not found!");
|
| 721 |
}
|
| 722 |
|
| 723 |
+
// TON Connect UI Initialization
|
| 724 |
+
// The manifest an absolute URL to the manifest.json
|
| 725 |
+
const manifestUrl = new URL('/tonconnect-manifest.json', window.location.origin).toString();
|
| 726 |
+
tonConnectUI = new TON_CONNECT_UI.TonConnectUI({
|
| 727 |
+
manifestUrl: manifestUrl,
|
| 728 |
+
buttonRootId: 'ton-connect-button-container', // Optional: if you want SDK to render button
|
| 729 |
+
actionsConfiguration: {
|
| 730 |
+
twaReturnUrl: `https://t.me/${tg.WebApp. Gastgeberanwendung}/${tg.WebApp.Startparam}` // Optional
|
| 731 |
+
}
|
| 732 |
+
});
|
| 733 |
+
|
| 734 |
+
// Handle TON Connect button click manually if not using buttonRootId or want custom button
|
| 735 |
+
const tonConnectBtnManual = document.getElementById('ton-connect-btn');
|
| 736 |
+
if (tonConnectBtnManual) {
|
| 737 |
+
tonConnectBtnManual.addEventListener('click', async () => {
|
| 738 |
+
try {
|
| 739 |
+
const connectedWallet = await tonConnectUI.connectWallet();
|
| 740 |
+
if (connectedWallet && connectedWallet.account && connectedWallet.account.address) {
|
| 741 |
+
const rawAddress = TON_CONNECT_UI.Address.parse(connectedWallet.account.address).toString({ testOnly: false }); // mainnet address
|
| 742 |
+
console.log('TON Wallet connected:', rawAddress);
|
| 743 |
+
await connectTonWalletBackend(rawAddress);
|
| 744 |
+
} else {
|
| 745 |
+
console.warn('TON Wallet connection cancelled or failed.');
|
| 746 |
+
}
|
| 747 |
+
} catch (error) {
|
| 748 |
+
console.error('Error during TON wallet connection process:', error);
|
| 749 |
+
tg.showAlert('Не удалось подключить TON кошелек.');
|
| 750 |
+
}
|
| 751 |
+
});
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
// Subscribe to connection status changes (optional, good for advanced UI updates)
|
| 755 |
+
tonConnectUI.onStatusChange(walletAndAccount => {
|
| 756 |
+
if (walletAndAccount && walletAndAccount.account) {
|
| 757 |
+
const rawAddress = TON_CONNECT_UI.Address.parse(walletAndAccount.account.address).toString({ testOnly: false });
|
| 758 |
+
console.log('TON Connect status change, connected:', rawAddress);
|
| 759 |
+
// connectTonWalletBackend(rawAddress); // Potentially connect here too, or rely on button click
|
| 760 |
+
updateTonWalletUI(rawAddress);
|
| 761 |
+
} else {
|
| 762 |
+
console.log('TON Connect status change, disconnected.');
|
| 763 |
+
updateTonWalletUI(null);
|
| 764 |
+
}
|
| 765 |
+
});
|
| 766 |
+
|
| 767 |
+
|
| 768 |
document.body.style.visibility = 'visible';
|
| 769 |
}
|
| 770 |
|
|
|
|
| 780 |
if(greetingElement) greetingElement.textContent = 'Ошибка загрузки интерфейса Telegram.';
|
| 781 |
document.body.style.visibility = 'visible';
|
| 782 |
}
|
| 783 |
+
}, 3500);
|
| 784 |
}
|
|
|
|
| 785 |
</script>
|
| 786 |
</body>
|
| 787 |
</html>
|
|
|
|
| 825 |
h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
|
| 826 |
.user-grid {
|
| 827 |
display: grid;
|
| 828 |
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); /* Wider cards */
|
| 829 |
gap: var(--padding);
|
| 830 |
margin-top: var(--padding);
|
| 831 |
}
|
|
|
|
| 849 |
width: 80px; height: 80px;
|
| 850 |
border-radius: 50%; margin-bottom: 1rem;
|
| 851 |
object-fit: cover; border: 3px solid var(--admin-border);
|
| 852 |
+
background-color: #eee;
|
| 853 |
}
|
| 854 |
.user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
|
| 855 |
.user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
|
| 856 |
+
.user-card .details { font-size: 0.9em; color: #495057; word-break: break-all; width:100%; } /* break-all */
|
| 857 |
+
.user-card .detail-item { margin-bottom: 0.3rem; text-align: left; padding-left: 10px; }
|
| 858 |
.user-card .detail-item strong { color: var(--admin-text); }
|
| 859 |
.user-card .timestamp { font-size: 0.8em; color: var(--admin-secondary); margin-top: 1rem; }
|
| 860 |
.no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
|
|
|
|
| 877 |
transition: background-color 0.2s ease;
|
| 878 |
}
|
| 879 |
.refresh-btn:hover { background-color: #0b5ed7; }
|
|
|
|
|
|
|
| 880 |
.admin-controls {
|
| 881 |
background: var(--admin-card-bg);
|
| 882 |
padding: var(--padding);
|
|
|
|
| 906 |
.admin-controls .loader {
|
| 907 |
border: 4px solid #f3f3f3; border-radius: 50%; border-top: 4px solid var(--admin-primary);
|
| 908 |
width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-left: 10px; vertical-align: middle;
|
| 909 |
+
display: none;
|
| 910 |
}
|
| 911 |
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
| 912 |
</style>
|
|
|
|
| 935 |
{% if user.username %}
|
| 936 |
<div class="username"><a href="https://t.me/{{ user.username }}" target="_blank" style="color: inherit; text-decoration: none;">@{{ user.username }}</a></div>
|
| 937 |
{% else %}
|
| 938 |
+
<div class="username" style="height: 1.3em;"></div>
|
| 939 |
{% endif %}
|
| 940 |
<div class="details">
|
| 941 |
<div class="detail-item"><strong>ID:</strong> {{ user.id }}</div>
|
| 942 |
<div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
|
| 943 |
<div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
|
| 944 |
<div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
|
| 945 |
+
<div class="detail-item"><strong>TON Wallet:</strong> {{ user.ton_wallet_address or 'N/A' }}</div>
|
| 946 |
</div>
|
| 947 |
<div class="timestamp">Визит: {{ user.visited_at_str }}</div>
|
| 948 |
</div>
|
|
|
|
| 968 |
statusMessage.textContent = data.message;
|
| 969 |
statusMessage.style.color = 'var(--admin-success)';
|
| 970 |
if (action === 'скачивание') {
|
| 971 |
+
setTimeout(() => location.reload(), 1500);
|
| 972 |
}
|
| 973 |
} else {
|
| 974 |
throw new Error(data.message || 'Произошла ошибка');
|
|
|
|
| 981 |
loader.style.display = 'none';
|
| 982 |
}
|
| 983 |
}
|
| 984 |
+
function triggerDownload() { handleFetch('/admin/download_data', 'скачивание'); }
|
| 985 |
+
function triggerUpload() { handleFetch('/admin/upload_data', 'загрузка'); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 986 |
</script>
|
| 987 |
</body>
|
| 988 |
</html>
|
| 989 |
"""
|
| 990 |
|
|
|
|
| 991 |
@app.route('/')
|
| 992 |
def index():
|
| 993 |
+
theme_params = {}
|
|
|
|
|
|
|
| 994 |
return render_template_string(TEMPLATE, theme=theme_params)
|
| 995 |
|
| 996 |
+
@app.route('/tonconnect-manifest.json')
|
| 997 |
+
def ton_manifest():
|
| 998 |
+
# Ensure this URL is correctly pointing to your app's domain
|
| 999 |
+
# For local dev, request.host_url should work. For production, ensure HOST is correct.
|
| 1000 |
+
app_url = request.host_url.strip('/')
|
| 1001 |
+
# Use a publicly accessible icon URL
|
| 1002 |
+
icon_url = "https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg"
|
| 1003 |
+
|
| 1004 |
+
manifest = {
|
| 1005 |
+
"url": app_url,
|
| 1006 |
+
"name": "Morshen Group Mini App",
|
| 1007 |
+
"iconUrl": icon_url,
|
| 1008 |
+
"termsOfUseUrl": f"{app_url}/terms-of-use", # Placeholder
|
| 1009 |
+
"privacyPolicyUrl": f"{app_url}/privacy-policy" # Placeholder
|
| 1010 |
+
}
|
| 1011 |
+
return jsonify(manifest)
|
| 1012 |
+
|
| 1013 |
+
|
| 1014 |
@app.route('/verify', methods=['POST'])
|
| 1015 |
def verify_data():
|
| 1016 |
try:
|
|
|
|
| 1020 |
return jsonify({"status": "error", "message": "Missing initData"}), 400
|
| 1021 |
|
| 1022 |
user_data_parsed, is_valid = verify_telegram_data(init_data_str)
|
|
|
|
| 1023 |
user_info_dict = {}
|
| 1024 |
if user_data_parsed and 'user' in user_data_parsed:
|
| 1025 |
try:
|
|
|
|
| 1027 |
user_info_dict = json.loads(user_json_str)
|
| 1028 |
except Exception as e:
|
| 1029 |
logging.error(f"Could not parse user JSON: {e}")
|
| 1030 |
+
|
| 1031 |
+
if is_valid and user_info_dict.get('id'):
|
| 1032 |
+
user_id = user_info_dict['id']
|
| 1033 |
+
user_id_str = str(user_id)
|
| 1034 |
+
now = time.time()
|
| 1035 |
+
|
| 1036 |
+
new_telegram_data = {
|
| 1037 |
+
'id': user_id,
|
| 1038 |
+
'first_name': user_info_dict.get('first_name'),
|
| 1039 |
+
'last_name': user_info_dict.get('last_name'),
|
| 1040 |
+
'username': user_info_dict.get('username'),
|
| 1041 |
+
'photo_url': user_info_dict.get('photo_url'),
|
| 1042 |
+
'language_code': user_info_dict.get('language_code'),
|
| 1043 |
+
'is_premium': user_info_dict.get('is_premium', False),
|
| 1044 |
+
'phone_number': user_info_dict.get('phone_number'),
|
| 1045 |
+
'visited_at': now,
|
| 1046 |
+
'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
|
| 1047 |
+
}
|
| 1048 |
+
|
| 1049 |
+
ton_wallet_address_to_return = None
|
| 1050 |
+
with _data_lock:
|
| 1051 |
+
current_user_data = visitor_data_cache.get(user_id_str, {})
|
| 1052 |
+
current_user_data.update(new_telegram_data)
|
| 1053 |
+
visitor_data_cache[user_id_str] = current_user_data
|
| 1054 |
+
ton_wallet_address_to_return = current_user_data.get('ton_wallet_address')
|
| 1055 |
+
|
| 1056 |
+
save_visitor_data({user_id_str: current_user_data})
|
| 1057 |
+
|
| 1058 |
+
return jsonify({
|
| 1059 |
+
"status": "ok",
|
| 1060 |
+
"verified": True,
|
| 1061 |
+
"user": user_info_dict,
|
| 1062 |
+
"ton_wallet_address": ton_wallet_address_to_return
|
| 1063 |
+
}), 200
|
| 1064 |
else:
|
| 1065 |
+
logging.warning(f"Verification failed for user: {user_info_dict.get('id') if user_info_dict else 'Unknown'}")
|
| 1066 |
return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
|
| 1067 |
|
| 1068 |
except Exception as e:
|
| 1069 |
logging.exception("Error in /verify endpoint")
|
| 1070 |
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
| 1071 |
|
| 1072 |
+
@app.route('/connect_ton_wallet', methods=['POST'])
|
| 1073 |
+
def connect_ton_wallet():
|
| 1074 |
+
try:
|
| 1075 |
+
req_data = request.get_json()
|
| 1076 |
+
init_data_str = req_data.get('initData')
|
| 1077 |
+
wallet_address = req_data.get('walletAddress')
|
| 1078 |
+
|
| 1079 |
+
if not init_data_str or not wallet_address:
|
| 1080 |
+
return jsonify({"status": "error", "message": "Missing initData or walletAddress"}), 400
|
| 1081 |
+
|
| 1082 |
+
user_data_parsed, is_valid = verify_telegram_data(init_data_str)
|
| 1083 |
+
if not is_valid:
|
| 1084 |
+
return jsonify({"status": "error", "message": "Invalid Telegram data"}), 403
|
| 1085 |
+
|
| 1086 |
+
user_info_dict = {}
|
| 1087 |
+
if user_data_parsed and 'user' in user_data_parsed:
|
| 1088 |
+
try:
|
| 1089 |
+
user_json_str = unquote(user_data_parsed['user'][0])
|
| 1090 |
+
user_info_dict = json.loads(user_json_str)
|
| 1091 |
+
except Exception as e:
|
| 1092 |
+
logging.error(f"Could not parse user JSON during TON connect: {e}")
|
| 1093 |
+
return jsonify({"status": "error", "message": "Failed to parse user data"}), 400
|
| 1094 |
+
|
| 1095 |
+
user_id = user_info_dict.get('id')
|
| 1096 |
+
if not user_id:
|
| 1097 |
+
return jsonify({"status": "error", "message": "User ID not found in Telegram data"}), 400
|
| 1098 |
+
|
| 1099 |
+
user_id_str = str(user_id)
|
| 1100 |
+
now = time.time()
|
| 1101 |
+
|
| 1102 |
+
with _data_lock:
|
| 1103 |
+
user_record = visitor_data_cache.get(user_id_str)
|
| 1104 |
+
if not user_record:
|
| 1105 |
+
logging.info(f"User {user_id_str} not in cache, creating basic entry for TON connect.")
|
| 1106 |
+
user_record = {
|
| 1107 |
+
'id': user_id,
|
| 1108 |
+
'first_name': user_info_dict.get('first_name'),
|
| 1109 |
+
'last_name': user_info_dict.get('last_name'),
|
| 1110 |
+
'username': user_info_dict.get('username'),
|
| 1111 |
+
'visited_at': now, # Or use a specific 'created_at'
|
| 1112 |
+
'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
|
| 1113 |
+
}
|
| 1114 |
+
|
| 1115 |
+
user_record['ton_wallet_address'] = wallet_address
|
| 1116 |
+
user_record['ton_wallet_connected_at'] = now
|
| 1117 |
+
user_record['ton_wallet_connected_at_str'] = datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
|
| 1118 |
+
|
| 1119 |
+
visitor_data_cache[user_id_str] = user_record
|
| 1120 |
+
data_to_save = {user_id_str: user_record}
|
| 1121 |
+
|
| 1122 |
+
save_visitor_data(data_to_save)
|
| 1123 |
+
|
| 1124 |
+
return jsonify({"status": "ok", "message": "TON wallet connected and saved successfully."}), 200
|
| 1125 |
+
|
| 1126 |
+
except Exception as e:
|
| 1127 |
+
logging.exception("Error in /connect_ton_wallet endpoint")
|
| 1128 |
+
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
| 1129 |
+
|
| 1130 |
+
|
| 1131 |
@app.route('/admin')
|
| 1132 |
def admin_panel():
|
| 1133 |
+
current_data = load_visitor_data()
|
|
|
|
| 1134 |
users_list = list(current_data.values())
|
| 1135 |
return render_template_string(ADMIN_TEMPLATE, users=users_list)
|
| 1136 |
|
| 1137 |
@app.route('/admin/download_data', methods=['POST'])
|
| 1138 |
def admin_trigger_download():
|
|
|
|
| 1139 |
success = download_data_from_hf()
|
| 1140 |
if success:
|
| 1141 |
return jsonify({"status": "ok", "message": "Скачивание данных с Hugging Face завершено. Страница будет обновлена."})
|
|
|
|
| 1144 |
|
| 1145 |
@app.route('/admin/upload_data', methods=['POST'])
|
| 1146 |
def admin_trigger_upload():
|
|
|
|
| 1147 |
if not HF_TOKEN_WRITE:
|
| 1148 |
return jsonify({"status": "error", "message": "HF_TOKEN_WRITE не настроен на сервере."}), 400
|
| 1149 |
+
upload_data_to_hf_async()
|
| 1150 |
return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
|
| 1151 |
|
| 1152 |
+
# Placeholder routes for terms and privacy for tonconnect-manifest.json
|
| 1153 |
+
@app.route('/terms-of-use')
|
| 1154 |
+
def terms_of_use():
|
| 1155 |
+
return "Terms of Use: To be defined.", 200
|
| 1156 |
+
|
| 1157 |
+
@app.route('/privacy-policy')
|
| 1158 |
+
def privacy_policy():
|
| 1159 |
+
return "Privacy Policy: To be defined.", 200
|
| 1160 |
+
|
| 1161 |
|
|
|
|
| 1162 |
if __name__ == '__main__':
|
|
|
|
| 1163 |
print("--- MORSHEN GROUP MINI APP SERVER ---")
|
|
|
|
| 1164 |
print(f"Flask server starting on http://{HOST}:{PORT}")
|
| 1165 |
print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
|
|
|
|
|
|
|
|
|
|
| 1166 |
if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
|
| 1167 |
+
print("--- WARNING: HUGGING FACE TOKEN(S) NOT SET. Backup/restore limited. ---")
|
|
|
|
|
|
|
|
|
|
| 1168 |
else:
|
| 1169 |
+
print("--- Hugging Face tokens found. Attempting initial data download...")
|
|
|
|
|
|
|
| 1170 |
download_data_from_hf()
|
| 1171 |
|
|
|
|
| 1172 |
load_visitor_data()
|
| 1173 |
+
print("--- SECURITY WARNING: /admin route and sub-routes are NOT protected. ---")
|
| 1174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1175 |
if HF_TOKEN_WRITE:
|
| 1176 |
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|
| 1177 |
backup_thread.start()
|
|
|
|
| 1180 |
print("--- Periodic backup disabled (HF_TOKEN_WRITE missing).")
|
| 1181 |
|
| 1182 |
print("--- Server Ready ---")
|
| 1183 |
+
app.run(host=HOST, port=PORT, debug=False)
|
|
|
|
|
|
|
|
|