Examplebonus22 / app.py
Aleksmorshen's picture
Update app.py
c7cde40 verified
raw
history blame
45.9 kB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for
import hmac
import hashlib
import json
from urllib.parse import unquote, parse_qs, quote
import time
from datetime import datetime
import logging
import threading
import asyncio
from huggingface_hub import HfApi, hf_hub_download
from huggingface_hub.utils import RepositoryNotFoundError
from aiogram import Bot, Dispatcher, types, F
from google import genai
from google.genai import types as genai_types
BOT_TOKEN = os.getenv("BOT_TOKEN", "7566834146:AAGiG4MaTZZvvtVsqEJVG5SYK5hUlc_Ewo")
HOST = '0.0.0.0'
PORT = 7860
DATA_FILE = 'data.json'
REPO_ID = "flpolprojects/teledata"
HF_DATA_FILE_PATH = "data.json"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
GENAI_MODEL = "learnlm-2.0-flash-experimental"
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
app.secret_key = os.urandom(24)
_data_lock = threading.Lock()
visitor_data_cache = {}
def download_data_from_hf():
global visitor_data_cache
if not HF_TOKEN_READ:
return False
try:
hf_hub_download(
repo_id=REPO_ID,
filename=HF_DATA_FILE_PATH,
repo_type="dataset",
token=HF_TOKEN_READ,
local_dir=".",
local_dir_use_symlinks=False,
force_download=True,
etag_timeout=10
)
with _data_lock:
try:
with open(DATA_FILE, 'r', encoding='utf-8') as f:
visitor_data_cache = json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
visitor_data_cache = {}
return True
except RepositoryNotFoundError:
pass
except Exception as e:
pass
return False
def load_visitor_data():
global visitor_data_cache
with _data_lock:
if not visitor_data_cache:
try:
with open(DATA_FILE, 'r', encoding='utf-8') as f:
visitor_data_cache = json.load(f)
except FileNotFoundError:
visitor_data_cache = {}
except json.JSONDecodeError:
visitor_data_cache = {}
except Exception as e:
visitor_data_cache = {}
return visitor_data_cache
def save_visitor_data(data):
with _data_lock:
try:
visitor_data_cache.update(data)
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
upload_data_to_hf_async()
except Exception as e:
pass
def upload_data_to_hf():
if not HF_TOKEN_WRITE:
return
if not os.path.exists(DATA_FILE):
return
try:
api = HfApi()
with _data_lock:
file_content_exists = os.path.getsize(DATA_FILE) > 0
if not file_content_exists:
return
api.upload_file(
path_or_fileobj=DATA_FILE,
path_in_repo=HF_DATA_FILE_PATH,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Update visitor data {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
except Exception as e:
pass
def upload_data_to_hf_async():
upload_thread = threading.Thread(target=upload_data_to_hf, daemon=True)
upload_thread.start()
def periodic_backup():
if not HF_TOKEN_WRITE:
return
while True:
time.sleep(3600)
upload_data_to_hf()
def verify_telegram_data(init_data_str):
try:
parsed_data = parse_qs(init_data_str)
received_hash = parsed_data.pop('hash', [None])[0]
if not received_hash:
return None, False
data_check_list = []
for key, value in sorted(parsed_data.items()):
data_check_list.append(f"{key}={value[0]}")
data_check_string = "\n".join(data_check_list)
secret_key = hmac.new("WebAppData".encode(), BOT_TOKEN.encode(), hashlib.sha256).digest()
calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
if calculated_hash == received_hash:
auth_date = int(parsed_data.get('auth_date', [0])[0])
current_time = int(time.time())
if current_time - auth_date > 86400:
pass
return parsed_data, True
else:
pass
return parsed_data, False
except Exception as e:
return None, False
TEMPLATE = """
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
<title>Morshen Group</title>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--tg-theme-bg-color: {{ theme.bg_color | default('#121212') }};
--tg-theme-text-color: {{ theme.text_color | default('#ffffff') }};
--tg-theme-hint-color: {{ theme.hint_color | default('#aaaaaa') }};
--tg-theme-link-color: {{ theme.link_color | default('#62bcf9') }};
--tg-theme-button-color: {{ theme.button_color | default('#31a5f5') }};
--tg-theme-button-text-color: {{ theme.button_text_color | default('#ffffff') }};
--tg-theme-secondary-bg-color: {{ theme.secondary_bg_color | default('#1e1e1e') }};
--bg-gradient: linear-gradient(160deg, #1a232f 0%, #121212 100%);
--card-bg: rgba(44, 44, 46, 0.8);
--card-bg-solid: #2c2c2e;
--text-color: var(--tg-theme-text-color);
--text-secondary-color: var(--tg-theme-hint-color);
--accent-gradient: linear-gradient(95deg, var(--tg-theme-button-color, #007aff), #5856d6);
--accent-gradient-green: linear-gradient(95deg, #34c759, #30d158);
--tag-bg: rgba(255, 255, 255, 0.1);
--border-radius-s: 8px;
--border-radius-m: 14px;
--border-radius-l: 18px;
--padding-s: 10px;
--padding-m: 18px;
--padding-l: 28px;
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--shadow-color: rgba(0, 0, 0, 0.3);
--shadow-light: 0 4px 15px var(--shadow-color);
--shadow-medium: 0 6px 25px var(--shadow-color);
--backdrop-blur: 10px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html {
background-color: var(--tg-theme-bg-color);
scroll-behavior: smooth;
}
body {
font-family: var(--font-family);
background: var(--bg-gradient);
color: var(--text-color);
padding: var(--padding-m);
padding-bottom: 120px;
overscroll-behavior-y: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
visibility: hidden;
min-height: 100vh;
}
.container {
max-width: 650px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: var(--padding-l);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--padding-m);
padding: var(--padding-s) 0;
}
.logo { display: flex; align-items: center; gap: var(--padding-s); }
.logo img {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: var(--card-bg-solid);
object-fit: cover;
border: 2px solid rgba(255, 255, 255, 0.15);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.logo span { font-size: 1.6em; font-weight: 700; letter-spacing: -0.5px; }
.btn {
display: inline-flex; align-items: center; justify-content: center;
padding: 12px var(--padding-m); border-radius: var(--border-radius-m);
background: var(--accent-gradient); color: var(--tg-theme-button-text-color);
text-decoration: none; font-weight: 600;
border: none; cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
gap: 8px; font-size: 1em;
box-shadow: var(--shadow-light);
letter-spacing: 0.3px;
}
.btn:hover {
opacity: 0.9;
box-shadow: var(--shadow-medium);
transform: translateY(-2px);
}
.btn-secondary {
background: var(--card-bg);
color: var(--tg-theme-link-color);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1), var(--shadow-light);
}
.btn-secondary:hover {
background: rgba(44, 44, 46, 0.95);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), var(--shadow-medium);
}
.tag {
display: inline-block; background: var(--tag-bg); color: var(--text-secondary-color);
padding: 6px 12px; border-radius: var(--border-radius-s); font-size: 0.85em;
margin: 4px 6px 4px 0; white-space: nowrap;
font-weight: 500;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.tag i { margin-right: 5px; opacity: 0.8; }
.section-card {
background-color: var(--card-bg);
border-radius: var(--border-radius-l);
padding: var(--padding-l);
margin-bottom: 0;
box-shadow: var(--shadow-medium);
border: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: blur(var(--backdrop-blur));
-webkit-backdrop-filter: blur(var(--backdrop-blur));
}
.section-title {
font-size: 2em;
font-weight: 700; margin-bottom: var(--padding-s); line-height: 1.25;
letter-spacing: -0.6px;
}
.section-subtitle {
font-size: 1.2em; font-weight: 500; color: var(--text-secondary-color);
margin-bottom: var(--padding-m);
}
.description {
font-size: 1.05em; line-height: 1.6; color: var(--text-secondary-color);
margin-bottom: var(--padding-m);
}
.stats-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
gap: var(--padding-m); margin-top: var(--padding-m); text-align: center;
}
.stat-item {
background-color: rgba(255, 255, 255, 0.05);
padding: var(--padding-m); border-radius: var(--border-radius-m);
border: 1px solid rgba(255, 255, 255, 0.05);
transition: background-color 0.2s ease;
}
.stat-item:hover { background-color: rgba(255, 255, 255, 0.08); }
.stat-value { font-size: 1.7em; font-weight: 600; display: block; color: var(--tg-theme-link-color);}
.stat-label { font-size: 0.9em; color: var(--text-secondary-color); display: block; margin-top: 4px;}
.list-item {
background-color: var(--card-bg-solid);
padding: var(--padding-m); border-radius: var(--border-radius-m);
margin-bottom: var(--padding-s); display: flex; align-items: center;
gap: var(--padding-m);
font-size: 1.1em; font-weight: 500;
border: 1px solid rgba(255, 255, 255, 0.08);
transition: background-color 0.2s ease, transform 0.2s ease;
}
.list-item:hover {
background-color: rgba(44, 44, 46, 0.9);
transform: translateX(3px);
}
.list-item i { font-size: 1.4em; color: var(--accent-gradient-start, #34c759); opacity: 0.9; width: 25px; text-align: center;}
.footer-greeting {
text-align: center; color: var(--text-secondary-color); font-size: 0.95em;
margin-top: var(--padding-l); padding-bottom: var(--padding-l);
}
.save-card-button {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
padding: 14px 28px;
border-radius: 30px;
background: var(--accent-gradient-green);
color: var(--tg-theme-button-text-color);
text-decoration: none;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1000;
box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.5);
font-size: 1.05em;
display: flex;
align-items: center;
gap: 10px;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.save-card-button:hover {
opacity: 0.95;
transform: translateX(-50%) scale(1.05);
box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.3);
}
.save-card-button i { font-size: 1.2em; }
.modal {
display: none; position: fixed; z-index: 1001;
left: 0; top: 0; width: 100%; height: 100%;
overflow: auto; background-color: rgba(0,0,0,0.7);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.modal-content {
background-color: var(--card-bg-solid); color: var(--text-color);
margin: 15% auto; padding: var(--padding-l);
border: 1px solid rgba(255, 255, 255, 0.1);
width: 88%; max-width: 480px;
border-radius: var(--border-radius-l);
text-align: center; position: relative;
box-shadow: var(--shadow-medium);
animation: scaleUp 0.3s ease-out;
}
@keyframes scaleUp { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.modal-close {
color: var(--text-secondary-color);
position: absolute; top: 15px; right: 20px;
font-size: 32px; font-weight: bold; cursor: pointer;
line-height: 1; transition: color 0.2s ease;
}
.modal-close:hover, .modal-close:focus { color: var(--text-color); }
.modal-text { font-size: 1.2em; line-height: 1.6; margin-bottom: var(--padding-s); word-wrap: break-word; }
.modal-text b { color: var(--tg-theme-link-color); font-weight: 600; }
.modal-instruction { font-size: 1em; color: var(--text-secondary-color); margin-top: var(--padding-m); }
.icon { display: inline-block; width: 1.2em; text-align: center; margin-right: 8px; opacity: 0.9; }
.icon-save::before { content: '💾'; }
.icon-web::before { content: '🌐'; }
.icon-mobile::before { content: '📱'; }
.icon-code::before { content: '💻'; }
.icon-ai::before { content: '🧠'; }
.icon-quantum::before { content: '⚛️'; }
.icon-business::before { content: '💼'; }
.icon-speed::before { content: '⚡️'; }
.icon-complexity::before { content: '🧩'; }
.icon-experience::before { content: '⏳'; }
.icon-clients::before { content: '👥'; }
.icon-market::before { content: '📈'; }
.icon-location::before { content: '📍'; }
.icon-global::before { content: '🌍'; }
.icon-innovation::before { content: '💡'; }
.icon-contact::before { content: '💬'; }
.icon-link::before { content: '🔗'; }
.icon-leader::before { content: '🏆'; }
.icon-company::before { content: '🏢'; }
@media (max-width: 480px) {
.section-title { font-size: 1.8em; }
.logo span { font-size: 1.4em; }
.btn { padding: 10px var(--padding-m); font-size: 0.95em; }
.save-card-button { padding: 12px 24px; font-size: 1em; bottom: 20px; }
.stats-grid { grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: var(--padding-s); }
.stat-value { font-size: 1.5em; }
.modal-content { margin: 25% auto; width: 92%; }
}
</style>
</head>
<body>
<div class="container">
<section class="morshen-group-intro section-card">
<div class="header">
<div class="logo">
<img src="https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg" alt="Morshen Group Logo">
<span>Morshen Group</span>
</div>
<a href="#" class="btn contact-link"><i class="icon icon-contact"></i>Связаться</a>
</div>
<div>
<span class="tag"><i class="icon icon-leader"></i>Лидер инноваций 2025</span>
</div>
<h1 class="section-title">Международный IT холдинг</h1>
<p class="description">
Объединяем передовые технологические компании для создания инновационных
решений мирового уровня. Мы строим будущее технологий сегодня.
</p>
<a href="#" class="btn contact-link" style="background: var(--accent-gradient-green); width: 100%; margin-top: var(--padding-s);">
<i class="icon icon-contact"></i>Написать нам в Telegram
</a>
</section>
<section class="ecosystem-header">
<h2 class="section-title"><i class="icon icon-company"></i>Экосистема инноваций</h2>
<p class="description">
В состав холдинга входят компании, специализирующиеся на различных
направлениях передовых технологий, создавая синергию для прорывных решений.
</p>
</section>
<section class="section-card">
<div class="logo" style="margin-bottom: var(--padding-m);">
<img src="https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg" alt="Morshen Alpha Logo">
<span style="font-size: 1.4em; font-weight: 600;">Morshen Alpha</span>
</div>
<div style="margin-bottom: var(--padding-m);">
<span class="tag"><i class="icon icon-ai"></i>Искусственный интеллект</span>
<span class="tag"><i class="icon icon-quantum"></i>Квантовые технологии</span>
<span class="tag"><i class="icon icon-business"></i>Бизнес-решения</span>
</div>
<p class="description">
Флагманская компания холдинга. Разрабатываем передовые бизнес-решения, проводим R&D в сфере AI
и квантовых технологий. Наши инновации формируют будущее индустрии.
</p>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-value"><i class="icon icon-global"></i> 3+</span>
<span class="stat-label">Страны присутствия</span>
</div>
<div class="stat-item">
<span class="stat-value"><i class="icon icon-clients"></i> 3K+</span>
<span class="stat-label">Готовых клиентов</span>
</div>
<div class="stat-item">
<span class="stat-value"><i class="icon icon-market"></i> 5+</span>
<span class="stat-label">Лет на рынке</span>
</div>
</div>
</section>
<section class="section-card">
<div class="logo" style="margin-bottom: var(--padding-m);">
<img src="https://huggingface.co/spaces/holmgardstudio/dev/resolve/main/image.jpg" alt="Holmgard Logo" style="width: 50px; height: 50px;">
<span style="font-size: 1.4em; font-weight: 600;">Holmgard Studio</span>
</div>
<div style="margin-bottom: var(--padding-m);">
<span class="tag"><i class="icon icon-web"></i>Веб-разработка</span>
<span class="tag"><i class="icon icon-mobile"></i>Мобильные приложения</span>
<span class="tag"><i class="icon icon-code"></i>ПО на заказ</span>
</div>
<p class="description">
Инновационная студия разработки, создающая высокотехнологичные веб-сайты,
мобильные приложения и ПО для бизнеса любого масштаба.
Используем передовые технологии и гибкие методологии.
</p>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-value"><i class="icon icon-experience"></i> 10+</span>
<span class="stat-label">Лет опыта</span>
</div>
<div class="stat-item">
<span class="stat-value"><i class="icon icon-complexity"></i> PRO</span>
<span class="stat-label">Любая сложность</span>
</div>
<div class="stat-item">
<span class="stat-value"><i class="icon icon-speed"></i> FAST</span>
<span class="stat-label">Высокая скорость</span>
</div>
</div>
<div style="display: flex; gap: var(--padding-s); margin-top: var(--padding-m); flex-wrap: wrap;">
<a href="https://holmgard.ru" target="_blank" class="btn btn-secondary" style="flex-grow: 1;"><i class="icon icon-link"></i>Веб-сайт студии</a>
<a href="#" class="btn contact-link" style="flex-grow: 1;"><i class="icon icon-contact"></i>Связаться</a>
</div>
</section>
<section class="section-card">
<h2 class="section-title"><i class="icon icon-global"></i>Глобальное присутствие</h2>
<p class="description">Наши инновационные решения и экспертиза доступны в странах Центральной Азии и за ее пределами:</p>
<div>
<div class="list-item"><i class="icon icon-location"></i>Узбекистан</div>
<div class="list-item"><i class="icon icon-location"></i>Казахстан</div>
<div class="list-item"><i class="icon icon-location"></i>Кыргызстан</div>
<div class="list-item"><i class="icon icon-innovation"></i>Расширяем горизонты...</div>
</div>
</section>
<footer class="footer-greeting">
<p id="greeting">Инициализация...</p>
</footer>
</div>
<button class="save-card-button" id="save-card-btn">
<i class="icon icon-save"></i>Сохранить визитку
</button>
<div id="saveModal" class="modal">
<div class="modal-content">
<span class="modal-close" id="modal-close-btn">×</span>
<p class="modal-text"><b>+996 500 398 754</b></p>
<p class="modal-text">Morshen Group</p>
<p class="modal-text" style="font-size: 1em; color: var(--text-secondary-color);">Международный IT Холдинг</p>
<p class="modal-instruction">Сделайте скриншот экрана, чтобы сохранить контакт.</p>
</div>
</div>
<script>
const tg = window.Telegram.WebApp;
function applyTheme(themeParams) {
const root = document.documentElement;
root.style.setProperty('--tg-theme-bg-color', themeParams.bg_color || '#121212');
root.style.setProperty('--tg-theme-text-color', themeParams.text_color || '#ffffff');
root.style.setProperty('--tg-theme-hint-color', themeParams.hint_color || '#aaaaaa');
root.style.setProperty('--tg-theme-link-color', themeParams.link_color || '#62bcf9');
root.style.setProperty('--tg-theme-button-color', themeParams.button_color || '#31a5f5');
root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || '#ffffff');
root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || '#1e1e1e');
try {
const bgColor = themeParams.bg_color || '#121212';
const r = parseInt(bgColor.slice(1, 3), 16);
const g = parseInt(bgColor.slice(3, 5), 16);
const b = parseInt(bgColor.slice(5, 7), 16);
root.style.setProperty('--tg-theme-bg-color-rgb', `${r}, ${g}, ${b}`);
} catch (e) {
root.style.setProperty('--tg-theme-bg-color-rgb', `18, 18, 18`);
}
}
function setupTelegram() {
if (!tg || !tg.initData) {
const greetingElement = document.getElementById('greeting');
if(greetingElement) greetingElement.textContent = 'Не удалось связаться с Telegram.';
document.body.style.visibility = 'visible';
return;
}
tg.ready();
tg.expand();
applyTheme(tg.themeParams);
tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
fetch('/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ initData: tg.initData }),
})
.then(response => {
if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
return response.json();
})
.then(data => {
if (data.status === 'ok' && data.verified) {
} else {
}
})
.catch(error => {
});
const user = tg.initDataUnsafe?.user;
const greetingElement = document.getElementById('greeting');
if (user) {
const name = user.first_name || user.username || 'Гость';
greetingElement.textContent = `Добро пожаловать, ${name}! 👋`;
} else {
greetingElement.textContent = 'Добро пожаловать!';
}
const contactButtons = document.querySelectorAll('.contact-link');
contactButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
tg.openTelegramLink('https://t.me/morshenkhan');
});
});
const modal = document.getElementById("saveModal");
const saveCardBtn = document.getElementById("save-card-btn");
const closeBtn = document.getElementById("modal-close-btn");
if (saveCardBtn && modal && closeBtn) {
saveCardBtn.addEventListener('click', (e) => {
e.preventDefault();
modal.style.display = "block";
if (tg.HapticFeedback) {
tg.HapticFeedback.impactOccurred('light');
}
});
closeBtn.addEventListener('click', () => {
modal.style.display = "none";
});
window.addEventListener('click', (event) => {
if (event.target == modal) {
modal.style.display = "none";
}
});
} else {
}
document.body.style.visibility = 'visible';
}
if (window.Telegram && window.Telegram.WebApp) {
setupTelegram();
} else {
window.addEventListener('load', setupTelegram);
setTimeout(() => {
if (document.body.style.visibility !== 'visible') {
const greetingElement = document.getElementById('greeting');
if(greetingElement) greetingElement.textContent = 'Ошибка загрузки интерфейса Telegram.';
document.body.style.visibility = 'visible';
}
}, 3500);
}
</script>
</body>
</html>
"""
ADMIN_TEMPLATE = """
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin - Посетители</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--admin-bg: #f8f9fa;
--admin-text: #212529;
--admin-card-bg: #ffffff;
--admin-border: #dee2e6;
--admin-shadow: rgba(0, 0, 0, 0.05);
--admin-primary: #0d6efd;
--admin-secondary: #6c757d;
--admin-success: #198754;
--admin-danger: #dc3545;
--admin-warning: #ffc107;
--border-radius: 12px;
--padding: 1.5rem;
--font-family: 'Inter', sans-serif;
}
body {
font-family: var(--font-family);
background-color: var(--admin-bg);
color: var(--admin-text);
margin: 0;
padding: var(--padding);
line-height: 1.6;
}
.container { max-width: 1140px; margin: 0 auto; }
h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
.user-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--padding);
margin-top: var(--padding);
}
.user-card {
background-color: var(--admin-card-bg);
border-radius: var(--border-radius);
padding: var(--padding);
box-shadow: 0 4px 15px var(--admin-shadow);
border: 1px solid var(--admin-border);
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.user-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
}
.user-card img {
width: 80px; height: 80px;
border-radius: 50%; margin-bottom: 1rem;
object-fit: cover; border: 3px solid var(--admin-border);
background-color: #eee;
}
.user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
.user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
.user-card .details { font-size: 0.9em; color: #495057; word-break: break-word; }
.user-card .detail-item { margin-bottom: 0.3rem; }
.user-card .detail-item strong { color: var(--admin-text); }
.user-card .timestamp { font-size: 0.8em; color: var(--admin-secondary); margin-top: 1rem; }
.no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
.alert {
background-color: #fff3cd; border-left: 6px solid var(--admin-warning);
margin-bottom: var(--padding); padding: 1rem 1.5rem;
color: #664d03; border-radius: 8px; text-align: center; font-weight: 500;
}
.refresh-btn {
display: block;
margin: 1rem auto;
padding: 10px 20px;
font-size: 1em;
font-weight: 500;
color: #fff;
background-color: var(--admin-primary);
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.refresh-btn:hover { background-color: #0b5ed7; }
.admin-controls {
background: var(--admin-card-bg);
padding: var(--padding);
border-radius: var(--border-radius);
box-shadow: 0 4px 15px var(--admin-shadow);
border: 1px solid var(--admin-border);
margin-bottom: var(--padding);
text-align: center;
}
.admin-controls h2 { margin-top: 0; margin-bottom: 1rem; font-weight: 600; color: var(--admin-secondary); }
.admin-controls .btn {
padding: 10px 18px;
font-size: 0.95em;
font-weight: 500;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
margin: 0.5rem;
color: #fff;
}
.admin-controls .btn-backup { background-color: var(--admin-success); }
.admin-controls .btn-backup:hover { background-color: #157347; }
.admin-controls .btn-download { background-color: var(--admin-primary); }
.admin-controls .btn-download:hover { background-color: #0b5ed7; }
.admin-controls .status { font-size: 0.9em; margin-top: 1rem; color: var(--admin-secondary); }
.admin-controls .loader {
border: 4px solid #f3f3f3; border-radius: 50%; border-top: 4px solid var(--admin-primary);
width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-left: 10px; vertical-align: middle;
display: none;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="container">
<h1>Посетители Mini App</h1>
<div class="alert">ВНИМАНИЕ: Этот раздел не защищен! Добавьте аутентификацию для реального использования.</div>
<div class="admin-controls">
<h2>Управление данными</h2>
<button class="btn btn-download" onclick="triggerDownload()">Скачать данные с HF</button>
<button class="btn btn-backup" onclick="triggerUpload()">Загрузить данные на HF</button>
<div class="loader" id="loader"></div>
<div class="status" id="status-message"></div>
</div>
<button class="refresh-btn" onclick="location.reload()">Обновить список</button>
{% if users %}
<div class="user-grid">
{% for user in users|sort(attribute='visited_at', reverse=true) %}
<div class="user-card">
<img src="{{ user.photo_url if user.photo_url else 'data:image/svg+xml;charset=UTF-8,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 100 100%27%3e%3crect width=%27100%27 height=%27100%27 fill=%27%23e9ecef%27/%3e%3ctext x=%2750%25%27 y=%2755%25%27 dominant-baseline=%27middle%27 text-anchor=%27middle%27 font-size=%2745%27 font-family=%27sans-serif%27 fill=%27%23adb5bd%27%3e?%3c/text%3e%3c/svg%3e' }}" alt="User Avatar">
<div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
{% if user.username %}
<div class="username"><a href="https://t.me/{{ user.username }}" target="_blank" style="color: inherit; text-decoration: none;">@{{ user.username }}</a></div>
{% else %}
<div class="username" style="height: 1.3em;"></div>
{% endif %}
<div class="details">
<div class="detail-item"><strong>ID:</strong> {{ user.id }}</div>
<div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
<div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
<div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
</div>
<div class="timestamp">Визит: {{ user.visited_at_str }}</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="no-users">Данных о посетителях пока нет.</p>
{% endif %}
</div>
<script>
const loader = document.getElementById('loader');
const statusMessage = document.getElementById('status-message');
async function handleFetch(url, action) {
loader.style.display = 'inline-block';
statusMessage.textContent = `Выполняется ${action}...`;
statusMessage.style.color = 'var(--admin-secondary)';
try {
const response = await fetch(url, { method: 'POST' });
const data = await response.json();
if (response.ok && data.status === 'ok') {
statusMessage.textContent = data.message;
statusMessage.style.color = 'var(--admin-success)';
if (action === 'скачивание') {
setTimeout(() => location.reload(), 1500);
}
} else {
throw new Error(data.message || 'Произошла ошибка');
}
} catch (error) {
statusMessage.textContent = `Ошибка ${action}: ${error.message}`;
statusMessage.style.color = 'var(--admin-danger)';
} finally {
loader.style.display = 'none';
}
}
function triggerDownload() {
handleFetch('/admin/download_data', 'скачивание');
}
function triggerUpload() {
handleFetch('/admin/upload_data', 'загрузка');
}
</script>
</body>
</html>
"""
bot = Bot(token=BOT_TOKEN)
dp = Dispatcher()
def generate_bot_response(query: str, context: str):
if not GEMINI_API_KEY:
return "Извините, функция ответа с использованием AI временно недоступна."
lower_query = query.lower()
code_keywords = ["код", "скрипт", "программа", "напиши", "сделай", "код на", "пример кода"]
if any(keyword in lower_query for keyword in code_keywords):
return "Извините, я не могу предоставить вам код. Могу ли я ответить на другие вопросы о Morshen Group или Holmgard Studio?"
try:
client = genai.Client(api_key=GEMINI_API_KEY)
prompt = f"""You are a helpful assistant answering questions about the following website content for "Morshen Group" and "Holmgard Studio".
Only use information explicitly provided in the CONTENT below. Do not invent or assume information not present in the content.
If the user asks for code, *you must refuse* and explain that you cannot provide code.
Keep responses concise and directly related to the query and content.
CONTENT:
{context}
USER QUERY: {query}
ASSISTANT RESPONSE:"""
contents = [
genai_types.Content(
role="user",
parts=[
genai_types.Part.from_text(text=prompt),
],
),
]
generate_content_config = genai_types.GenerateContentConfig(
response_mime_type="text/plain",
temperature=0.1,
max_output_tokens=500
)
response = client.models.generate_content(
model=GENAI_MODEL,
contents=contents,
config=generate_content_config,
)
return response.text.strip()
except Exception as e:
return "Извините, произошла ошибка при обработке вашего запроса."
@dp.message()
async def handle_message(message: types.Message):
user_query = message.text
if not user_query:
return
await message.answer("Думаю...")
context = TEMPLATE
response_text = generate_bot_response(user_query, context)
await message.answer(response_text)
@app.route('/')
def index():
theme_params = {}
return render_template_string(TEMPLATE, theme=theme_params)
@app.route('/verify', methods=['POST'])
def verify_data():
try:
req_data = request.get_json()
init_data_str = req_data.get('initData')
if not init_data_str:
return jsonify({"status": "error", "message": "Missing initData"}), 400
user_data_parsed, is_valid = verify_telegram_data(init_data_str)
user_info_dict = {}
if user_data_parsed and 'user' in user_data_parsed:
try:
user_json_str = unquote(user_data_parsed['user'][0])
user_info_dict = json.loads(user_json_str)
except Exception as e:
user_info_dict = {}
if is_valid:
user_id = user_info_dict.get('id')
if user_id:
now = time.time()
user_entry = {
str(user_id): {
'id': user_id,
'first_name': user_info_dict.get('first_name'),
'last_name': user_info_dict.get('last_name'),
'username': user_info_dict.get('username'),
'photo_url': user_info_dict.get('photo_url'),
'language_code': user_info_dict.get('language_code'),
'is_premium': user_info_dict.get('is_premium', False),
'phone_number': user_info_dict.get('phone_number'),
'visited_at': now,
'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
}
}
save_visitor_data(user_entry)
return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
else:
return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
except Exception as e:
return jsonify({"status": "error", "message": "Internal server error"}), 500
@app.route('/admin')
def admin_panel():
current_data = load_visitor_data()
users_list = list(current_data.values())
return render_template_string(ADMIN_TEMPLATE, users=users_list)
@app.route('/admin/download_data', methods=['POST'])
def admin_trigger_download():
success = download_data_from_hf()
if success:
return jsonify({"status": "ok", "message": "Скачивание данных с Hugging Face завершено. Страница будет обновлена."})
else:
return jsonify({"status": "error", "message": "Ошибка скачивания данных с Hugging Face. Проверьте логи."}), 500
@app.route('/admin/upload_data', methods=['POST'])
def admin_trigger_upload():
if not HF_TOKEN_WRITE:
return jsonify({"status": "error", "message": "HF_TOKEN_WRITE не настроен на сервере."}), 400
upload_data_to_hf_async()
return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
def start_bot_polling():
try:
asyncio.run(dp.start_polling(bot))
except Exception as e:
pass
if __name__ == '__main__':
download_data_from_hf()
load_visitor_data()
if HF_TOKEN_WRITE:
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
backup_thread.start()
bot_thread = threading.Thread(target=start_bot_polling, daemon=True)
bot_thread.start()
app.run(host=HOST, port=PORT, debug=False)