App / app.py
Kgshop's picture
Update app.py
09264eb verified
import os
import io
import base64
import json
import logging
import threading
import time
from datetime import datetime, timedelta
import random
import string
from flask import Flask, render_template_string, request, redirect, url_for, flash, make_response, jsonify
from huggingface_hub import HfApi, hf_hub_download
from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
from dotenv import load_dotenv
import requests
load_dotenv()
app = Flask(__name__)
app.secret_key = 'your_unique_secret_key_gippo_312_shop_54321_no_login_synkris'
DATA_FILE = 'data.json'
DATA_FILE_TEMP = 'data.json.tmp'
SYNC_FILES = [DATA_FILE]
REPO_ID = "Kgshop/neurospace"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
DOWNLOAD_RETRIES = 3
DOWNLOAD_DELAY = 5
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
return False
token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
files_to_download = [specific_file] if specific_file else SYNC_FILES
all_successful = True
for file_name in files_to_download:
success = False
for attempt in range(retries + 1):
try:
hf_hub_download(
repo_id=REPO_ID,
filename=file_name,
repo_type="dataset",
token=token_to_use,
local_dir=".",
local_dir_use_symlinks=False,
force_download=True,
resume_download=False
)
success = True
break
except RepositoryNotFoundError:
all_successful = False
break
except HfHubHTTPError as e:
if e.response.status_code == 404:
if attempt == 0 and not os.path.exists(file_name):
try:
if file_name == DATA_FILE:
with open(file_name, 'w', encoding='utf-8') as f:
json.dump({}, f)
except Exception:
pass
success = True
break
else:
pass
except Exception:
pass
if attempt < retries:
time.sleep(delay)
if not success:
all_successful = False
return all_successful
def upload_db_to_hf(specific_file=None):
if not HF_TOKEN_WRITE:
return
try:
api = HfApi()
files_to_upload = [specific_file] if specific_file else SYNC_FILES
for file_name in files_to_upload:
if os.path.exists(file_name):
try:
api.upload_file(
path_or_fileobj=file_name,
path_in_repo=file_name,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
except Exception:
pass
except Exception:
pass
def periodic_backup():
backup_interval = 1800
while True:
time.sleep(backup_interval)
upload_db_to_hf()
def load_data():
data = {}
if os.path.exists(DATA_FILE):
try:
with open(DATA_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
except json.JSONDecodeError:
if download_db_from_hf(specific_file=DATA_FILE):
try:
with open(DATA_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
data = {}
elif download_db_from_hf(specific_file=DATA_FILE):
try:
with open(DATA_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
data = {}
if not isinstance(data, dict):
data = {}
return data
def save_data(data):
try:
with open(DATA_FILE_TEMP, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
os.replace(DATA_FILE_TEMP, DATA_FILE)
upload_db_to_hf(specific_file=DATA_FILE)
except Exception:
if os.path.exists(DATA_FILE_TEMP):
os.remove(DATA_FILE_TEMP)
LANDING_PAGE_TEMPLATE = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> MetaStore - AI система для Вашего Бизнеса</title>
<style>
body, html {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
iframe {
border: none;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<iframe src="https://v0-ai-agent-landing-page-smoky-six.vercel.app/"></iframe>
</body>
</html>
'''
ADMHOSTO_TEMPLATE = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Админ-панель</title>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
:root {
--bg-light: #f4f6f9;
--bg-medium: #135D66;
--accent: #48D1CC;
--accent-hover: #77E4D8;
--text-dark: #333;
--text-on-accent: #003C43;
--danger: #E57373;
--warning: #ffb74d;
--info: #4fc3f7;
--success: #81c784;
--archive: #90a4ae;
}
* { box-sizing: border-box; }
body { font-family: 'Montserrat', sans-serif; background-color: var(--bg-light); color: var(--text-dark); margin: 0; padding: 15px; }
.container { max-width: 900px; margin: 0 auto; background-color: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 3px 15px rgba(0,0,0,0.08); }
h1, h2 { font-weight: 600; color: var(--bg-medium); text-align: center; }
h1 { margin-bottom: 25px; font-size: 1.5rem; }
h2 { font-size: 1.3rem; margin-top: 40px; border-bottom: 2px solid var(--accent); padding-bottom: 10px; margin-bottom: 20px; }
.section { margin-bottom: 25px; }
.add-env-form { display: flex; flex-direction: column; gap: 15px; background: #f8f9fa; padding: 15px; border-radius: 10px; border: 1px solid #e9ecef; }
input[type="text"] {
width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem;
font-family: inherit; background: #fff; -webkit-appearance: none;
}
.controls-row { display: flex; align-items: center; justify-content: space-between; gap: 15px; flex-wrap: wrap; }
.radio-group { display: flex; gap: 15px; }
.radio-group label { cursor: pointer; display: flex; align-items: center; gap: 6px; font-weight: 500; font-size: 0.95rem; }
.button {
padding: 10px 15px; border: none; border-radius: 8px; color: white; font-weight: 600; cursor: pointer; text-decoration: none;
display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-size: 0.9rem; transition: opacity 0.2s;
}
.button:hover { opacity: 0.85; }
.button:active { transform: scale(0.98); }
.button.primary { background-color: var(--accent); color: var(--text-on-accent); }
.button.danger { background-color: var(--danger); }
.button.warning { background-color: var(--warning); color: #333; }
.button.info { background-color: var(--info); }
.button.success { background-color: var(--success); }
.env-list { list-style: none; padding: 0; margin: 0; }
.env-item {
background: #fff; border: 1px solid #e0e0e0; border-radius: 10px; padding: 15px; margin-bottom: 12px;
display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 15px; box-shadow: 0 2px 5px rgba(0,0,0,0.02);
}
.env-item-archived { border-left: 4px solid var(--archive); }
.env-details { display: flex; flex-direction: column; gap: 4px; overflow: hidden; }
.env-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.env-id { font-weight: 700; color: var(--bg-medium); font-size: 1.1rem; }
.env-keyword { font-style: italic; color: #666; font-size: 0.9rem;}
.env-link { font-size: 0.9rem; color: #007bff; word-break: break-all; text-decoration: none; padding: 5px 0; display: block; }
.env-type-badge { font-size: 0.75rem; padding: 3px 8px; border-radius: 20px; font-weight: bold; text-transform: uppercase; white-space: nowrap; }
.type-open { background-color: #d4edda; color: #155724; }
.type-closed { background-color: #f8d7da; color: #721c24; }
.env-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.message { padding: 12px; border-radius: 8px; margin-bottom: 20px; text-align: center; font-size: 0.95rem; }
.message.success { background-color: #d4edda; color: #155724; }
.message.error { background-color: #f8d7da; color: #721c24; }
.modal { display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(2px); }
.modal-content { background-color: #fff; margin: 15% auto; padding: 25px; width: 90%; max-width: 600px; border-radius: 12px; position: relative; box-shadow: 0 5px 20px rgba(0,0,0,0.2); }
.close-modal { color: #888; position: absolute; right: 15px; top: 10px; font-size: 30px; font-weight: bold; cursor: pointer; padding: 5px; }
.stats-table { width: 100%; border-collapse: collapse; margin-top: 15px; font-size: 0.85rem; }
.stats-table th, .stats-table td { border: 1px solid #eee; padding: 10px 8px; text-align: left; }
.stats-table th { background-color: var(--bg-medium); color: white; }
.stats-table tr:nth-child(even) { background-color: #f9f9f9; }
.empty-list-placeholder { text-align:center; padding: 20px; color: #888; }
.no-margin { margin-bottom: 0; }
@media (max-width: 768px) {
.env-item { grid-template-columns: 1fr; gap: 12px; }
.env-actions { justify-content: flex-start; }
.modal-content { margin: 10% auto; width: 95%; padding: 20px 15px; }
}
@media (max-width: 600px) {
body { padding: 10px; }
.container { padding: 15px; }
h1 { font-size: 1.3rem; margin-bottom: 20px; }
.controls-row { flex-direction: column; align-items: stretch; }
.radio-group { justify-content: space-between; background: #fff; padding: 10px; border-radius: 8px; border: 1px solid #ddd; }
.add-env-form .button { width: 100%; padding: 14px; }
.stats-table th, .stats-table td { font-size: 0.75rem; padding: 6px 4px; }
}
</style>
</head>
<body>
<div class="container">
<h1><i class="fas fa-server"></i> Управление Средами</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="message {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="section">
<form method="POST" action="{{ url_for('create_environment') }}" class="add-env-form">
<input type="text" id="keyword" name="keyword" placeholder="Ключевое слово (например, 'магазин')" required>
<div class="controls-row">
<div class="radio-group">
<label><input type="radio" name="env_type" value="closed" checked> <i class="fas fa-lock"></i> Закрытая</label>
<label><input type="radio" name="env_type" value="open"> <i class="fas fa-globe"></i> Открытая</label>
</div>
<button type="submit" class="button primary"><i class="fas fa-plus-circle"></i> Создать</button>
</div>
</form>
</div>
<div class="section">
<input type="text" id="search-env" placeholder="🔍 Поиск...">
</div>
<div class="section">
{% if active_environments %}
<ul class="env-list">
{% for env in active_environments %}
<li class="env-item">
<div class="env-details">
<div class="env-header">
<span class="env-id">{{ env.id }}</span>
<span class="env-type-badge type-{{ env.type }}">
{{ 'ЗАКРЫТАЯ' if env.type == 'closed' else 'ОТКРЫТАЯ' }}
</span>
<small style="color:#888">{{ env.hits }} <i class="fas fa-eye"></i></small>
</div>
<span class="env-keyword">{{ env.keyword }}</span>
<a href="{{ env.link }}" class="env-link" target="_blank">{{ env.link }}</a>
</div>
<div class="env-actions">
<button class="button info" onclick="openStats('{{ env.id }}')"><i class="fas fa-chart-bar"></i> Инфо</button>
<form method="POST" action="{{ url_for('toggle_type', env_id=env.id) }}" style="display:contents;">
<button type="submit" class="button warning">
<i class="fas fa-{{ 'lock-open' if env.type == 'closed' else 'lock' }}"></i> {{ 'Открыть' if env.type == 'closed' else 'Закрыть' }}
</button>
</form>
{% if env.type == 'closed' %}
<form method="POST" action="{{ url_for('clear_user', env_id=env.id) }}" style="display:contents;" onsubmit="return confirm('Отвязать пользователя от среды {{ env.id }}? Первый, кто зайдет по ссылке, станет владельцем.');">
<button type="submit" class="button success"><i class="fas fa-user-slash"></i> Сброс</button>
</form>
{% endif %}
<form method="POST" action="{{ url_for('delete_environment', env_id=env.id) }}" style="display:contents;" onsubmit="return confirm('Переместить среду {{ env.id }} в архив?');">
<button type="submit" class="button danger"><i class="fas fa-archive"></i></button>
</form>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<div class="empty-list-placeholder">Список активных сред пуст</div>
{% endif %}
</div>
<div class="section no-margin">
<h2><i class="fas fa-archive"></i> Архив</h2>
{% if archived_environments %}
<ul class="env-list">
{% for env in archived_environments %}
<li class="env-item env-item-archived">
<div class="env-details">
<div class="env-header">
<span class="env-id">{{ env.id }}</span>
<span class="env-type-badge type-{{ env.type }}">
{{ 'ЗАКРЫТАЯ' if env.type == 'closed' else 'ОТКРЫТАЯ' }}
</span>
</div>
<span class="env-keyword">{{ env.keyword }}</span>
</div>
<div class="env-actions">
<form method="POST" action="{{ url_for('restore_environment', env_id=env.id) }}" style="display:contents;">
<button type="submit" class="button success"><i class="fas fa-undo"></i> Восстановить</button>
</form>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<div class="empty-list-placeholder">Архив пуст</div>
{% endif %}
</div>
</div>
<div id="statsModal" class="modal">
<div class="modal-content">
<span class="close-modal" onclick="closeStats()">&times;</span>
<h3 id="modalTitle" style="margin-top:0; color: var(--bg-medium)">Статистика</h3>
<p style="font-size: 0.8rem; color: #666;">Время: Алматы (UTC+5)</p>
<div id="statsContent" style="overflow-x: auto;">Загрузка...</div>
</div>
</div>
<script>
document.getElementById('search-env').addEventListener('input', function() {
const searchTerm = this.value.toLowerCase().trim();
document.querySelectorAll('.env-item').forEach(item => {
const text = item.innerText.toLowerCase();
item.style.display = text.includes(searchTerm) ? 'grid' : 'none';
});
});
function openStats(envId) {
const modal = document.getElementById('statsModal');
const content = document.getElementById('statsContent');
const title = document.getElementById('modalTitle');
title.innerText = `Среда: ${envId}`;
content.innerHTML = '<div style="text-align:center; padding: 20px;"><i class="fas fa-spinner fa-spin fa-2x"></i></div>';
modal.style.display = 'block';
fetch(`/admhosto/stats/${envId}`)
.then(response => response.json())
.then(data => {
if (data.error) {
content.innerHTML = `<p style="color:red">${data.error}</p>`;
return;
}
let html = `<div style="display:flex; justify-content:space-between; margin-bottom:10px;">
<span><strong>Всего входов:</strong> ${data.hits}</span>
<span><strong>Тип:</strong> ${data.type === 'closed' ? 'Закрытая' : 'Открытая'}</span>
</div>`;
if (data.logs && data.logs.length > 0) {
html += `<table class="stats-table">
<thead><tr><th>Время</th><th>IP</th><th>Browser</th></tr></thead>
<tbody>`;
data.logs.forEach(log => {
html += `<tr>
<td>${log.time.split(' ')[1]}<br><small style="color:#999">${log.time.split(' ')[0]}</small></td>
<td>${log.ip}</td>
<td style="max-width: 100px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${log.ua}">
${log.ua.includes('iPhone') ? '<i class="fab fa-apple"></i>' : (log.ua.includes('Android') ? '<i class="fab fa-android"></i>' : '<i class="fas fa-desktop"></i>')}
</td>
</tr>`;
});
html += `</tbody></table>`;
} else {
html += `<p>Журнал пуст.</p>`;
}
content.innerHTML = html;
})
.catch(err => {
content.innerHTML = '<p style="color:red">Ошибка сети.</p>';
});
}
function closeStats() {
document.getElementById('statsModal').style.display = 'none';
}
window.onclick = function(event) {
const modal = document.getElementById('statsModal');
if (event.target == modal) {
modal.style.display = 'none';
}
}
</script>
</body>
</html>
'''
SYNKRIS_LOOK_TEMPLATE = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NeuroSpace</title>
<style>
:root {
--bg: #000000;
--card-bg: #0a0a0a;
--primary: #ccff00;
--primary-hover: #b3e600;
--primary-gradient: linear-gradient(45deg, #ccff00, #b3e600);
--text: #ffffff;
--text-secondary: #a1a1a1;
--border: #333333;
}
body {
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg);
color: var(--text);
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background-color: var(--card-bg);
width: 100%;
max-width: 500px;
padding: 40px;
border-radius: 20px;
border: 1px solid #222;
box-shadow: 0 0 40px rgba(204, 255, 0, 0.08);
text-align: center;
}
h1 {
color: var(--primary);
margin-top: 0;
margin-bottom: 35px;
font-size: 2.5rem;
text-transform: uppercase;
letter-spacing: 3px;
text-shadow: 0 0 10px rgba(204, 255, 0, 0.3);
}
.btn-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.action-btn {
background-image: var(--primary-gradient);
color: #000;
border: none;
padding: 18px 30px;
font-size: 1.2rem;
font-weight: 800;
border-radius: 12px;
cursor: pointer;
width: 100%;
transition: all 0.2s ease;
box-shadow: 0 0 20px rgba(204, 255, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
text-transform: uppercase;
}
.action-btn:hover {
transform: scale(1.03);
box-shadow: 0 0 30px rgba(204, 255, 0, 0.6);
}
.action-btn:active {
transform: scale(0.99);
}
.action-btn.secondary {
background-image: none;
background-color: #111;
border: 2px solid var(--border);
color: var(--primary);
box-shadow: none;
}
.action-btn.secondary:hover {
border-color: var(--primary);
box-shadow: 0 0 15px rgba(204, 255, 0, 0.3);
}
</style>
</head>
<body>
<div class="container">
<h1>NeuroSpace</h1>
<div class="btn-container">
<button type="button" class="action-btn" onclick="window.open('https://arena.ai/ru?chat-modality=image&mode=direct', '_blank')">
<span>Фоторедактор</span>
<span style="font-size: 1.2em">⚡</span>
</button>
<button type="button" class="action-btn secondary" onclick="window.open('https://t.me/testoh199enbot?startapp=payload', '_blank')">
<span>Облако</span>
<span style="font-size: 1.2em">☁️</span>
</button>
</div>
</div>
</body>
</html>
'''
@app.route('/')
def index():
return render_template_string(LANDING_PAGE_TEMPLATE)
@app.route('/admhosto', methods=['GET'])
def admhosto():
data = load_data()
active_environments = []
archived_environments = []
for env_id, env_data in data.items():
if not isinstance(env_data, dict): continue
env_item = {
"id": env_id,
"keyword": env_data.get("keyword", "N/A"),
"type": env_data.get("type", "closed"),
"hits": env_data.get("hits", 0),
"created_at": env_data.get("created_at", ""),
"link": url_for('serve_env', env_id=env_id, _external=True)
}
if env_data.get("archived"):
archived_environments.append(env_item)
else:
active_environments.append(env_item)
active_environments.sort(key=lambda x: x.get('created_at', ''), reverse=True)
archived_environments.sort(key=lambda x: x.get('created_at', ''), reverse=True)
return render_template_string(ADMHOSTO_TEMPLATE, active_environments=active_environments, archived_environments=archived_environments)
@app.route('/admhosto/create', methods=['POST'])
def create_environment():
all_data = load_data()
keyword = request.form.get('keyword', '').strip()
env_type = request.form.get('env_type', 'closed')
if not keyword:
flash('Ключевое слово не может быть пустым.', 'error')
return redirect(url_for('admhosto'))
while True:
new_id = ''.join(random.choices(string.digits, k=6))
if new_id not in all_data:
break
all_data[new_id] = {
"keyword": keyword,
"type": env_type,
"device_token": None,
"hits": 0,
"logs": [],
"created_at": datetime.utcnow().isoformat(),
"archived": False
}
save_data(all_data)
flash(f'Новая {env_type} среда с ID {new_id} создана.', 'success')
return redirect(url_for('admhosto'))
@app.route('/admhosto/delete/<env_id>', methods=['POST'])
def delete_environment(env_id):
all_data = load_data()
if env_id in all_data:
all_data[env_id]['archived'] = True
save_data(all_data)
flash(f'Среда {env_id} перемещена в архив.', 'success')
else:
flash(f'Среда {env_id} не найдена.', 'error')
return redirect(url_for('admhosto'))
@app.route('/admhosto/restore/<env_id>', methods=['POST'])
def restore_environment(env_id):
all_data = load_data()
if env_id in all_data:
all_data[env_id]['archived'] = False
save_data(all_data)
flash(f'Среда {env_id} восстановлена из архива.', 'success')
else:
flash(f'Среда {env_id} не найдена.', 'error')
return redirect(url_for('admhosto'))
@app.route('/admhosto/clear_user/<env_id>', methods=['POST'])
def clear_user(env_id):
all_data = load_data()
if env_id in all_data and all_data[env_id].get('type') == 'closed':
all_data[env_id]['device_token'] = None
save_data(all_data)
flash(f'Пользователь отвязан от среды {env_id}.', 'success')
else:
flash(f'Ошибка: Среда не найдена или не является закрытой.', 'error')
return redirect(url_for('admhosto'))
@app.route('/admhosto/toggle_type/<env_id>', methods=['POST'])
def toggle_type(env_id):
all_data = load_data()
if env_id in all_data:
current_type = all_data[env_id].get('type', 'closed')
if current_type == 'closed':
all_data[env_id]['type'] = 'open'
flash(f'Среда {env_id} теперь открыта.', 'success')
else:
all_data[env_id]['type'] = 'closed'
all_data[env_id]['device_token'] = None
flash(f'Среда {env_id} теперь закрыта. Пользователь сброшен.', 'success')
save_data(all_data)
else:
flash(f'Среда {env_id} не найдена.', 'error')
return redirect(url_for('admhosto'))
@app.route('/admhosto/stats/<env_id>')
def get_env_stats(env_id):
data = load_data()
env_data = data.get(env_id)
if not env_data:
return jsonify({"error": "Среда не найдена"}), 404
raw_logs = env_data.get("logs", [])
formatted_logs = []
for log in reversed(raw_logs):
try:
utc_dt = datetime.fromisoformat(log['time'])
almaty_dt = utc_dt + timedelta(hours=5)
time_str = almaty_dt.strftime('%Y-%m-%d %H:%M:%S')
formatted_logs.append({
"time": time_str,
"ip": log.get('ip', 'unknown'),
"ua": log.get('ua', 'unknown')
})
except:
continue
response_data = {
"id": env_id,
"keyword": env_data.get("keyword"),
"type": env_data.get("type", "closed"),
"hits": env_data.get("hits", 0),
"logs": formatted_logs
}
return jsonify(response_data)
@app.route('/env/<env_id>')
def serve_env(env_id):
data = load_data()
env_data = data.get(env_id)
if not env_data or not isinstance(env_data, dict) or env_data.get("archived"):
return "Среда не найдена или заархивирована.", 404
keyword = env_data.get("keyword", "")
env_type = env_data.get("type", "closed")
current_log = {
"time": datetime.utcnow().isoformat(),
"ip": request.headers.get('X-Forwarded-For', request.remote_addr),
"ua": request.headers.get('User-Agent', '')[:150]
}
env_data['hits'] = env_data.get('hits', 0) + 1
if 'logs' not in env_data or not isinstance(env_data.get('logs'), list):
env_data['logs'] = []
env_data['logs'].append(current_log)
if len(env_data['logs']) > 30:
env_data['logs'] = env_data['logs'][-30:]
data[env_id] = env_data
save_data(data)
if env_type == 'open':
return render_template_string(SYNKRIS_LOOK_TEMPLATE, keyword=keyword)
stored_token = env_data.get("device_token")
user_token = request.cookies.get(f'access_token_{env_id}')
if stored_token:
if user_token != stored_token:
return """
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Доступ запрещен</title>
<style>
body { font-family: 'Segoe UI', sans-serif; background: #000; color: #fff; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; text-align: center; }
.container { padding: 20px; }
h1 { color: #E57373; margin-bottom: 10px; }
p { color: #aaa; }
</style>
</head>
<body>
<div class="container">
<h1>⛔ Доступ запрещен</h1>
<p>Эта ссылка уже привязана к другому устройству или браузеру.</p>
</div>
</body>
</html>
""", 403
return render_template_string(SYNKRIS_LOOK_TEMPLATE, keyword=keyword)
else:
new_token = ''.join(random.choices(string.ascii_letters + string.digits, k=40))
env_data['device_token'] = new_token
data[env_id] = env_data
save_data(data)
resp = make_response(render_template_string(SYNKRIS_LOOK_TEMPLATE, keyword=keyword))
resp.set_cookie(f'access_token_{env_id}', new_token, max_age=31536000, httponly=True, samesite='Lax')
return resp
if __name__ == '__main__':
download_db_from_hf()
if HF_TOKEN_WRITE:
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
backup_thread.start()
else:
logging.info("HF_TOKEN_WRITE is not set. Periodic backup is disabled.")
port = int(os.environ.get('PORT', 7860))
app.run(debug=False, host='0.0.0.0', port=port)