Examplebonus33 / app.py
Aleksmorshen's picture
Update app.py
5758652 verified
raw
history blame
50.3 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
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:
logging.warning("HF_TOKEN_READ not set. Skipping Hugging Face download.")
return False
try:
logging.info(f"Attempting to download {HF_DATA_FILE_PATH} from {REPO_ID}...")
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
)
logging.info("Data file successfully downloaded from Hugging Face.")
with _data_lock:
try:
with open(DATA_FILE, 'r', encoding='utf-8') as f:
visitor_data_cache = json.load(f)
logging.info("Successfully loaded downloaded data into cache.")
except (FileNotFoundError, json.JSONDecodeError) as e:
logging.error(f"Error reading downloaded data file: {e}. Starting with empty cache.")
visitor_data_cache = {}
return True
except RepositoryNotFoundError:
logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download data.")
except Exception as e:
logging.error(f"Error downloading data from Hugging Face: {e}")
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)
logging.info("Visitor data loaded from local JSON.")
except FileNotFoundError:
logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.")
visitor_data_cache = {}
except json.JSONDecodeError:
logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.")
visitor_data_cache = {}
except Exception as e:
logging.error(f"Unexpected error loading visitor data: {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)
logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
upload_data_to_hf_async()
except Exception as e:
logging.error(f"Error saving visitor data: {e}")
def upload_data_to_hf():
if not HF_TOKEN_WRITE:
logging.warning("HF_TOKEN_WRITE not set. Skipping Hugging Face upload.")
return
if not os.path.exists(DATA_FILE):
logging.warning(f"{DATA_FILE} does not exist. Skipping upload.")
return
try:
api = HfApi()
with _data_lock:
file_content_exists = os.path.getsize(DATA_FILE) > 0
if not file_content_exists:
logging.warning(f"{DATA_FILE} is empty. Skipping upload.")
return
logging.info(f"Attempting to upload {DATA_FILE} to {REPO_ID}/{HF_DATA_FILE_PATH}...")
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')}"
)
logging.info("Visitor data successfully uploaded to Hugging Face.")
except Exception as e:
logging.error(f"Error uploading data to Hugging Face: {e}")
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:
logging.info("Periodic backup disabled: HF_TOKEN_WRITE not set.")
return
while True:
time.sleep(3600)
logging.info("Initiating periodic backup...")
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:
logging.warning(f"Telegram InitData is older than 1 hour (Auth Date: {auth_date}, Current: {current_time}).")
return parsed_data, True
else:
logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
return parsed_data, False
except Exception as e:
logging.error(f"Error verifying Telegram data: {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) {
console.error("Telegram WebApp script not loaded or initData is missing.");
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) {
console.log('Backend verification successful.');
} else {
console.warn('Backend verification failed:', data.message);
}
})
.catch(error => {
console.error('Error sending initData for verification:', 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 = 'Добро пожаловать!';
console.warn('Telegram User data not available (initDataUnsafe.user is empty).');
}
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 {
console.error("Modal elements not found!");
}
document.body.style.visibility = 'visible';
}
if (window.Telegram && window.Telegram.WebApp) {
setupTelegram();
} else {
console.warn("Telegram WebApp script not immediately available, waiting for window.onload");
window.addEventListener('load', setupTelegram);
setTimeout(() => {
if (document.body.style.visibility !== 'visible') {
console.error("Telegram WebApp script fallback timeout triggered.");
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)';
console.error(`Error during ${action}:`, error);
} 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:
logging.warning("GEMINI_API_KEY not set. Cannot use AI.")
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 = [
types.Content(
role="user",
parts=[
types.Part.from_text(text=prompt),
],
),
]
generate_content_config = 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:
logging.error(f"Error generating AI response: {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:
logging.error(f"Could not parse user JSON: {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:
logging.warning(f"Verification failed for user: {user_info_dict.get('id')}")
return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
except Exception as e:
logging.exception("Error in /verify endpoint")
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:
logging.error(f"Error in bot polling thread: {e}")
if __name__ == '__main__':
print("---")
print("--- MORSHEN GROUP SERVER (FLASK + AIOGRAM) ---")
print("---")
print(f"Flask server starting on http://{HOST}:{PORT}")
print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
print(f"Visitor data file: {DATA_FILE}")
print(f"Hugging Face Repo: {REPO_ID}")
print(f"HF Data Path: {HF_DATA_FILE_PATH}")
if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
print("---")
print("--- WARNING: HUGGING FACE TOKEN(S) NOT SET ---")
print("--- Backup/restore functionality will be limited. Set HF_TOKEN_READ and HF_TOKEN_WRITE environment variables.")
print("---")
else:
print("--- Hugging Face tokens found.")
print("--- Attempting initial data download from Hugging Face...")
download_data_from_hf()
if not GEMINI_API_KEY:
print("---")
print("--- WARNING: GEMINI_API_KEY NOT SET ---")
print("--- AI response functionality will be limited or unavailable. Set GEMINI_API_KEY environment variable.")
print("---")
else:
print("--- GEMINI_API_KEY found.")
print(f"--- Using AI model: {GENAI_MODEL}")
load_visitor_data()
print("---")
print("--- SECURITY WARNING ---")
print("--- The /admin route and its sub-routes are NOT protected.")
print("--- Implement proper authentication before deploying.")
print("---")
if HF_TOKEN_WRITE:
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
backup_thread.start()
print("--- Periodic backup thread started (every hour).")
else:
print("--- Periodic backup disabled (HF_TOKEN_WRITE missing).")
bot_thread = threading.Thread(target=start_bot_polling, daemon=True)
bot_thread.start()
print("--- Telegram bot polling thread started.")
print("--- Server Ready ---")
app.run(host=HOST, port=PORT, debug=False)