Update app.py
Browse files
app.py
CHANGED
|
@@ -13,6 +13,7 @@ import logging
|
|
| 13 |
import threading
|
| 14 |
from huggingface_hub import HfApi, hf_hub_download
|
| 15 |
from huggingface_hub.utils import RepositoryNotFoundError
|
|
|
|
| 16 |
|
| 17 |
BOT_TOKEN = os.getenv("BOT_TOKEN", "7566834146:AAGiG4MaTZZvvbTVsqEJVG5SYK5hUlc_Ewo")
|
| 18 |
HOST = '0.0.0.0'
|
|
@@ -83,11 +84,10 @@ def load_visitor_data():
|
|
| 83 |
visitor_data_cache = {}
|
| 84 |
return visitor_data_cache
|
| 85 |
|
| 86 |
-
def save_visitor_data(
|
| 87 |
-
global visitor_data_cache
|
| 88 |
with _data_lock:
|
| 89 |
try:
|
| 90 |
-
visitor_data_cache.update(
|
| 91 |
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 92 |
json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
|
| 93 |
logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
|
|
@@ -157,7 +157,7 @@ def verify_telegram_data(init_data_str):
|
|
| 157 |
auth_date = int(parsed_data.get('auth_date', [0])[0])
|
| 158 |
current_time = int(time.time())
|
| 159 |
if current_time - auth_date > 86400:
|
| 160 |
-
logging.warning(f"Telegram InitData is older than
|
| 161 |
return parsed_data, True
|
| 162 |
else:
|
| 163 |
logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
|
|
@@ -416,12 +416,21 @@ TEMPLATE = """
|
|
| 416 |
.icon-link::before { content: '🔗'; }
|
| 417 |
.icon-leader::before { content: '🏆'; }
|
| 418 |
.icon-company::before { content: '🏢'; }
|
| 419 |
-
.icon-wallet::before { content: '💎'; }
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
@media (max-width: 480px) {
|
| 426 |
.section-title { font-size: 1.8em; }
|
| 427 |
.logo span { font-size: 1.4em; }
|
|
@@ -455,13 +464,23 @@ TEMPLATE = """
|
|
| 455 |
<a href="#" class="btn contact-link" style="background: var(--accent-gradient-green); width: 100%; margin-top: var(--padding-s);">
|
| 456 |
<i class="icon icon-contact"></i>Написать нам в Telegram
|
| 457 |
</a>
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
<
|
| 463 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
</div>
|
|
|
|
| 465 |
</section>
|
| 466 |
|
| 467 |
<section class="ecosystem-header">
|
|
@@ -568,9 +587,9 @@ TEMPLATE = """
|
|
| 568 |
</div>
|
| 569 |
</div>
|
| 570 |
|
|
|
|
| 571 |
<script>
|
| 572 |
const tg = window.Telegram.WebApp;
|
| 573 |
-
let tonConnectUI;
|
| 574 |
|
| 575 |
function applyTheme(themeParams) {
|
| 576 |
const root = document.documentElement;
|
|
@@ -581,6 +600,7 @@ TEMPLATE = """
|
|
| 581 |
root.style.setProperty('--tg-theme-button-color', themeParams.button_color || '#31a5f5');
|
| 582 |
root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || '#ffffff');
|
| 583 |
root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || '#1e1e1e');
|
|
|
|
| 584 |
try {
|
| 585 |
const bgColor = themeParams.bg_color || '#121212';
|
| 586 |
const r = parseInt(bgColor.slice(1, 3), 16);
|
|
@@ -592,50 +612,61 @@ TEMPLATE = """
|
|
| 592 |
}
|
| 593 |
}
|
| 594 |
|
| 595 |
-
function
|
| 596 |
-
const
|
| 597 |
-
|
| 598 |
-
const tonWalletAddressEl = document.getElementById('ton-wallet-address');
|
| 599 |
-
|
| 600 |
-
if (address) {
|
| 601 |
-
tonConnectBtnContainer.style.display = 'none';
|
| 602 |
-
tonWalletAddressEl.textContent = `${address.slice(0, 6)}...${address.slice(-4)}`;
|
| 603 |
-
tonWalletInfoDiv.style.display = 'block';
|
| 604 |
-
} else {
|
| 605 |
-
tonConnectBtnContainer.style.display = 'block';
|
| 606 |
-
tonWalletInfoDiv.style.display = 'none';
|
| 607 |
-
}
|
| 608 |
-
}
|
| 609 |
-
|
| 610 |
-
async function connectTonWalletBackend(walletAddress) {
|
| 611 |
try {
|
| 612 |
-
const response = await fetch('/
|
| 613 |
method: 'POST',
|
| 614 |
headers: {
|
| 615 |
'Content-Type': 'application/json',
|
| 616 |
'Accept': 'application/json'
|
| 617 |
},
|
| 618 |
-
body: JSON.stringify({
|
| 619 |
});
|
|
|
|
| 620 |
if (!response.ok) {
|
| 621 |
-
|
| 622 |
-
throw new Error(`HTTP error ${response.status}: ${errorData.message || 'Failed to connect wallet on backend'}`);
|
| 623 |
}
|
|
|
|
| 624 |
const data = await response.json();
|
| 625 |
if (data.status === 'ok') {
|
| 626 |
-
|
| 627 |
-
updateTonWalletUI(walletAddress);
|
| 628 |
-
tg.HapticFeedback.notificationOccurred('success');
|
| 629 |
} else {
|
| 630 |
-
throw new Error(data.message || '
|
| 631 |
}
|
| 632 |
} catch (error) {
|
| 633 |
-
console.error('Error
|
| 634 |
-
|
| 635 |
-
|
| 636 |
}
|
| 637 |
}
|
| 638 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
|
| 640 |
function setupTelegram() {
|
| 641 |
if (!tg || !tg.initData) {
|
|
@@ -667,19 +698,12 @@ TEMPLATE = """
|
|
| 667 |
.then(data => {
|
| 668 |
if (data.status === 'ok' && data.verified) {
|
| 669 |
console.log('Backend verification successful.');
|
| 670 |
-
if (data.ton_wallet_address) {
|
| 671 |
-
updateTonWalletUI(data.ton_wallet_address);
|
| 672 |
-
} else {
|
| 673 |
-
updateTonWalletUI(null);
|
| 674 |
-
}
|
| 675 |
} else {
|
| 676 |
console.warn('Backend verification failed:', data.message);
|
| 677 |
-
updateTonWalletUI(null);
|
| 678 |
}
|
| 679 |
})
|
| 680 |
.catch(error => {
|
| 681 |
console.error('Error sending initData for verification:', error);
|
| 682 |
-
updateTonWalletUI(null);
|
| 683 |
});
|
| 684 |
|
| 685 |
const user = tg.initDataUnsafe?.user;
|
|
@@ -712,58 +736,21 @@ TEMPLATE = """
|
|
| 712 |
tg.HapticFeedback.impactOccurred('light');
|
| 713 |
}
|
| 714 |
});
|
| 715 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 716 |
window.addEventListener('click', (event) => {
|
| 717 |
-
if (event.target == modal) {
|
|
|
|
|
|
|
| 718 |
});
|
| 719 |
} else {
|
| 720 |
console.error("Modal elements not found!");
|
| 721 |
}
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
// The manifest an absolute URL to the manifest.json
|
| 725 |
-
const manifestUrl = new URL('/tonconnect-manifest.json', window.location.origin).toString();
|
| 726 |
-
tonConnectUI = new TON_CONNECT_UI.TonConnectUI({
|
| 727 |
-
manifestUrl: manifestUrl,
|
| 728 |
-
buttonRootId: 'ton-connect-button-container', // Optional: if you want SDK to render button
|
| 729 |
-
actionsConfiguration: {
|
| 730 |
-
twaReturnUrl: `https://t.me/${tg.WebApp. Gastgeberanwendung}/${tg.WebApp.Startparam}` // Optional
|
| 731 |
-
}
|
| 732 |
-
});
|
| 733 |
-
|
| 734 |
-
// Handle TON Connect button click manually if not using buttonRootId or want custom button
|
| 735 |
-
const tonConnectBtnManual = document.getElementById('ton-connect-btn');
|
| 736 |
-
if (tonConnectBtnManual) {
|
| 737 |
-
tonConnectBtnManual.addEventListener('click', async () => {
|
| 738 |
-
try {
|
| 739 |
-
const connectedWallet = await tonConnectUI.connectWallet();
|
| 740 |
-
if (connectedWallet && connectedWallet.account && connectedWallet.account.address) {
|
| 741 |
-
const rawAddress = TON_CONNECT_UI.Address.parse(connectedWallet.account.address).toString({ testOnly: false }); // mainnet address
|
| 742 |
-
console.log('TON Wallet connected:', rawAddress);
|
| 743 |
-
await connectTonWalletBackend(rawAddress);
|
| 744 |
-
} else {
|
| 745 |
-
console.warn('TON Wallet connection cancelled or failed.');
|
| 746 |
-
}
|
| 747 |
-
} catch (error) {
|
| 748 |
-
console.error('Error during TON wallet connection process:', error);
|
| 749 |
-
tg.showAlert('Не удалось подключить TON кошелек.');
|
| 750 |
-
}
|
| 751 |
-
});
|
| 752 |
-
}
|
| 753 |
-
|
| 754 |
-
// Subscribe to connection status changes (optional, good for advanced UI updates)
|
| 755 |
-
tonConnectUI.onStatusChange(walletAndAccount => {
|
| 756 |
-
if (walletAndAccount && walletAndAccount.account) {
|
| 757 |
-
const rawAddress = TON_CONNECT_UI.Address.parse(walletAndAccount.account.address).toString({ testOnly: false });
|
| 758 |
-
console.log('TON Connect status change, connected:', rawAddress);
|
| 759 |
-
// connectTonWalletBackend(rawAddress); // Potentially connect here too, or rely on button click
|
| 760 |
-
updateTonWalletUI(rawAddress);
|
| 761 |
-
} else {
|
| 762 |
-
console.log('TON Connect status change, disconnected.');
|
| 763 |
-
updateTonWalletUI(null);
|
| 764 |
-
}
|
| 765 |
-
});
|
| 766 |
-
|
| 767 |
|
| 768 |
document.body.style.visibility = 'visible';
|
| 769 |
}
|
|
@@ -782,6 +769,7 @@ TEMPLATE = """
|
|
| 782 |
}
|
| 783 |
}, 3500);
|
| 784 |
}
|
|
|
|
| 785 |
</script>
|
| 786 |
</body>
|
| 787 |
</html>
|
|
@@ -825,7 +813,7 @@ ADMIN_TEMPLATE = """
|
|
| 825 |
h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
|
| 826 |
.user-grid {
|
| 827 |
display: grid;
|
| 828 |
-
grid-template-columns: repeat(auto-fill, minmax(
|
| 829 |
gap: var(--padding);
|
| 830 |
margin-top: var(--padding);
|
| 831 |
}
|
|
@@ -853,8 +841,8 @@ ADMIN_TEMPLATE = """
|
|
| 853 |
}
|
| 854 |
.user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
|
| 855 |
.user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
|
| 856 |
-
.user-card .details { font-size: 0.9em; color: #495057; word-break: break-
|
| 857 |
-
.user-card .detail-item { margin-bottom: 0.3rem;
|
| 858 |
.user-card .detail-item strong { color: var(--admin-text); }
|
| 859 |
.user-card .timestamp { font-size: 0.8em; color: var(--admin-secondary); margin-top: 1rem; }
|
| 860 |
.no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
|
|
@@ -942,7 +930,6 @@ ADMIN_TEMPLATE = """
|
|
| 942 |
<div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
|
| 943 |
<div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
|
| 944 |
<div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
|
| 945 |
-
<div class="detail-item"><strong>TON Wallet:</strong> {{ user.ton_wallet_address or 'N/A' }}</div>
|
| 946 |
</div>
|
| 947 |
<div class="timestamp">Визит: {{ user.visited_at_str }}</div>
|
| 948 |
</div>
|
|
@@ -981,8 +968,14 @@ ADMIN_TEMPLATE = """
|
|
| 981 |
loader.style.display = 'none';
|
| 982 |
}
|
| 983 |
}
|
| 984 |
-
|
| 985 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 986 |
</script>
|
| 987 |
</body>
|
| 988 |
</html>
|
|
@@ -993,24 +986,6 @@ def index():
|
|
| 993 |
theme_params = {}
|
| 994 |
return render_template_string(TEMPLATE, theme=theme_params)
|
| 995 |
|
| 996 |
-
@app.route('/tonconnect-manifest.json')
|
| 997 |
-
def ton_manifest():
|
| 998 |
-
# Ensure this URL is correctly pointing to your app's domain
|
| 999 |
-
# For local dev, request.host_url should work. For production, ensure HOST is correct.
|
| 1000 |
-
app_url = request.host_url.strip('/')
|
| 1001 |
-
# Use a publicly accessible icon URL
|
| 1002 |
-
icon_url = "https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg"
|
| 1003 |
-
|
| 1004 |
-
manifest = {
|
| 1005 |
-
"url": app_url,
|
| 1006 |
-
"name": "Morshen Group Mini App",
|
| 1007 |
-
"iconUrl": icon_url,
|
| 1008 |
-
"termsOfUseUrl": f"{app_url}/terms-of-use", # Placeholder
|
| 1009 |
-
"privacyPolicyUrl": f"{app_url}/privacy-policy" # Placeholder
|
| 1010 |
-
}
|
| 1011 |
-
return jsonify(manifest)
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
@app.route('/verify', methods=['POST'])
|
| 1015 |
def verify_data():
|
| 1016 |
try:
|
|
@@ -1020,6 +995,7 @@ def verify_data():
|
|
| 1020 |
return jsonify({"status": "error", "message": "Missing initData"}), 400
|
| 1021 |
|
| 1022 |
user_data_parsed, is_valid = verify_telegram_data(init_data_str)
|
|
|
|
| 1023 |
user_info_dict = {}
|
| 1024 |
if user_data_parsed and 'user' in user_data_parsed:
|
| 1025 |
try:
|
|
@@ -1027,107 +1003,36 @@ def verify_data():
|
|
| 1027 |
user_info_dict = json.loads(user_json_str)
|
| 1028 |
except Exception as e:
|
| 1029 |
logging.error(f"Could not parse user JSON: {e}")
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
current_user_data.update(new_telegram_data)
|
| 1053 |
-
visitor_data_cache[user_id_str] = current_user_data
|
| 1054 |
-
ton_wallet_address_to_return = current_user_data.get('ton_wallet_address')
|
| 1055 |
-
|
| 1056 |
-
save_visitor_data({user_id_str: current_user_data})
|
| 1057 |
-
|
| 1058 |
-
return jsonify({
|
| 1059 |
-
"status": "ok",
|
| 1060 |
-
"verified": True,
|
| 1061 |
-
"user": user_info_dict,
|
| 1062 |
-
"ton_wallet_address": ton_wallet_address_to_return
|
| 1063 |
-
}), 200
|
| 1064 |
else:
|
| 1065 |
-
logging.warning(f"Verification failed for user: {user_info_dict.get('id')
|
| 1066 |
return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
|
| 1067 |
|
| 1068 |
except Exception as e:
|
| 1069 |
logging.exception("Error in /verify endpoint")
|
| 1070 |
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
| 1071 |
|
| 1072 |
-
@app.route('/connect_ton_wallet', methods=['POST'])
|
| 1073 |
-
def connect_ton_wallet():
|
| 1074 |
-
try:
|
| 1075 |
-
req_data = request.get_json()
|
| 1076 |
-
init_data_str = req_data.get('initData')
|
| 1077 |
-
wallet_address = req_data.get('walletAddress')
|
| 1078 |
-
|
| 1079 |
-
if not init_data_str or not wallet_address:
|
| 1080 |
-
return jsonify({"status": "error", "message": "Missing initData or walletAddress"}), 400
|
| 1081 |
-
|
| 1082 |
-
user_data_parsed, is_valid = verify_telegram_data(init_data_str)
|
| 1083 |
-
if not is_valid:
|
| 1084 |
-
return jsonify({"status": "error", "message": "Invalid Telegram data"}), 403
|
| 1085 |
-
|
| 1086 |
-
user_info_dict = {}
|
| 1087 |
-
if user_data_parsed and 'user' in user_data_parsed:
|
| 1088 |
-
try:
|
| 1089 |
-
user_json_str = unquote(user_data_parsed['user'][0])
|
| 1090 |
-
user_info_dict = json.loads(user_json_str)
|
| 1091 |
-
except Exception as e:
|
| 1092 |
-
logging.error(f"Could not parse user JSON during TON connect: {e}")
|
| 1093 |
-
return jsonify({"status": "error", "message": "Failed to parse user data"}), 400
|
| 1094 |
-
|
| 1095 |
-
user_id = user_info_dict.get('id')
|
| 1096 |
-
if not user_id:
|
| 1097 |
-
return jsonify({"status": "error", "message": "User ID not found in Telegram data"}), 400
|
| 1098 |
-
|
| 1099 |
-
user_id_str = str(user_id)
|
| 1100 |
-
now = time.time()
|
| 1101 |
-
|
| 1102 |
-
with _data_lock:
|
| 1103 |
-
user_record = visitor_data_cache.get(user_id_str)
|
| 1104 |
-
if not user_record:
|
| 1105 |
-
logging.info(f"User {user_id_str} not in cache, creating basic entry for TON connect.")
|
| 1106 |
-
user_record = {
|
| 1107 |
-
'id': user_id,
|
| 1108 |
-
'first_name': user_info_dict.get('first_name'),
|
| 1109 |
-
'last_name': user_info_dict.get('last_name'),
|
| 1110 |
-
'username': user_info_dict.get('username'),
|
| 1111 |
-
'visited_at': now, # Or use a specific 'created_at'
|
| 1112 |
-
'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
|
| 1113 |
-
}
|
| 1114 |
-
|
| 1115 |
-
user_record['ton_wallet_address'] = wallet_address
|
| 1116 |
-
user_record['ton_wallet_connected_at'] = now
|
| 1117 |
-
user_record['ton_wallet_connected_at_str'] = datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
|
| 1118 |
-
|
| 1119 |
-
visitor_data_cache[user_id_str] = user_record
|
| 1120 |
-
data_to_save = {user_id_str: user_record}
|
| 1121 |
-
|
| 1122 |
-
save_visitor_data(data_to_save)
|
| 1123 |
-
|
| 1124 |
-
return jsonify({"status": "ok", "message": "TON wallet connected and saved successfully."}), 200
|
| 1125 |
-
|
| 1126 |
-
except Exception as e:
|
| 1127 |
-
logging.exception("Error in /connect_ton_wallet endpoint")
|
| 1128 |
-
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
@app.route('/admin')
|
| 1132 |
def admin_panel():
|
| 1133 |
current_data = load_visitor_data()
|
|
@@ -1149,28 +1054,66 @@ def admin_trigger_upload():
|
|
| 1149 |
upload_data_to_hf_async()
|
| 1150 |
return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
|
| 1151 |
|
| 1152 |
-
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
|
| 1159 |
-
|
| 1160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1161 |
|
| 1162 |
if __name__ == '__main__':
|
|
|
|
| 1163 |
print("--- MORSHEN GROUP MINI APP SERVER ---")
|
|
|
|
| 1164 |
print(f"Flask server starting on http://{HOST}:{PORT}")
|
| 1165 |
print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
|
|
|
|
|
|
|
|
|
|
| 1166 |
if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
|
| 1167 |
-
print("---
|
|
|
|
|
|
|
|
|
|
| 1168 |
else:
|
| 1169 |
-
print("--- Hugging Face tokens found.
|
|
|
|
| 1170 |
download_data_from_hf()
|
| 1171 |
|
| 1172 |
load_visitor_data()
|
| 1173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1174 |
|
| 1175 |
if HF_TOKEN_WRITE:
|
| 1176 |
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|
|
|
|
| 13 |
import threading
|
| 14 |
from huggingface_hub import HfApi, hf_hub_download
|
| 15 |
from huggingface_hub.utils import RepositoryNotFoundError
|
| 16 |
+
import requests
|
| 17 |
|
| 18 |
BOT_TOKEN = os.getenv("BOT_TOKEN", "7566834146:AAGiG4MaTZZvvbTVsqEJVG5SYK5hUlc_Ewo")
|
| 19 |
HOST = '0.0.0.0'
|
|
|
|
| 84 |
visitor_data_cache = {}
|
| 85 |
return visitor_data_cache
|
| 86 |
|
| 87 |
+
def save_visitor_data(data):
|
|
|
|
| 88 |
with _data_lock:
|
| 89 |
try:
|
| 90 |
+
visitor_data_cache.update(data)
|
| 91 |
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 92 |
json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
|
| 93 |
logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
|
|
|
|
| 157 |
auth_date = int(parsed_data.get('auth_date', [0])[0])
|
| 158 |
current_time = int(time.time())
|
| 159 |
if current_time - auth_date > 86400:
|
| 160 |
+
logging.warning(f"Telegram InitData is older than 1 hour (Auth Date: {auth_date}, Current: {current_time}).")
|
| 161 |
return parsed_data, True
|
| 162 |
else:
|
| 163 |
logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
|
|
|
|
| 416 |
.icon-link::before { content: '🔗'; }
|
| 417 |
.icon-leader::before { content: '🏆'; }
|
| 418 |
.icon-company::before { content: '🏢'; }
|
| 419 |
+
.icon-wallet::before { content: '💎'; }
|
| 420 |
+
.wallet-info-item {
|
| 421 |
+
font-size: 1em; color: var(--text-secondary-color); word-break: break-all;
|
| 422 |
+
margin-top: var(--padding-s);
|
| 423 |
+
}
|
| 424 |
+
.wallet-info-item strong {
|
| 425 |
+
color: var(--text-color);
|
| 426 |
+
}
|
| 427 |
+
.wallet-balance {
|
| 428 |
+
font-size: 1.8em; font-weight: 700; color: var(--tg-theme-link-color);
|
| 429 |
+
margin-top: var(--padding-s);
|
| 430 |
+
}
|
| 431 |
+
#ton-connect-button > button {
|
| 432 |
+
width: 100%;
|
| 433 |
+
}
|
| 434 |
@media (max-width: 480px) {
|
| 435 |
.section-title { font-size: 1.8em; }
|
| 436 |
.logo span { font-size: 1.4em; }
|
|
|
|
| 464 |
<a href="#" class="btn contact-link" style="background: var(--accent-gradient-green); width: 100%; margin-top: var(--padding-s);">
|
| 465 |
<i class="icon icon-contact"></i>Написать нам в Telegram
|
| 466 |
</a>
|
| 467 |
+
</section>
|
| 468 |
+
|
| 469 |
+
<section class="section-card">
|
| 470 |
+
<h2 class="section-title"><i class="icon icon-wallet"></i>TON Кошелек</h2>
|
| 471 |
+
<p class="description">
|
| 472 |
+
Подключите ваш TON кошелек, чтобы проверить баланс прямо в приложении.
|
| 473 |
+
</p>
|
| 474 |
+
<div id="wallet-info" style="display: none; margin-top: var(--padding-m);">
|
| 475 |
+
<div class="wallet-info-item">
|
| 476 |
+
<strong>Баланс:</strong>
|
| 477 |
+
<div class="wallet-balance"><span id="wallet-balance">...</span> TON</div>
|
| 478 |
+
</div>
|
| 479 |
+
<div class="wallet-info-item">
|
| 480 |
+
<strong>Адрес:</strong> <span id="wallet-address"></span>
|
| 481 |
+
</div>
|
| 482 |
</div>
|
| 483 |
+
<div id="ton-connect-button" style="margin-top: var(--padding-m);"></div>
|
| 484 |
</section>
|
| 485 |
|
| 486 |
<section class="ecosystem-header">
|
|
|
|
| 587 |
</div>
|
| 588 |
</div>
|
| 589 |
|
| 590 |
+
|
| 591 |
<script>
|
| 592 |
const tg = window.Telegram.WebApp;
|
|
|
|
| 593 |
|
| 594 |
function applyTheme(themeParams) {
|
| 595 |
const root = document.documentElement;
|
|
|
|
| 600 |
root.style.setProperty('--tg-theme-button-color', themeParams.button_color || '#31a5f5');
|
| 601 |
root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || '#ffffff');
|
| 602 |
root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || '#1e1e1e');
|
| 603 |
+
|
| 604 |
try {
|
| 605 |
const bgColor = themeParams.bg_color || '#121212';
|
| 606 |
const r = parseInt(bgColor.slice(1, 3), 16);
|
|
|
|
| 612 |
}
|
| 613 |
}
|
| 614 |
|
| 615 |
+
async function fetchBalance(address) {
|
| 616 |
+
const balanceEl = document.getElementById('wallet-balance');
|
| 617 |
+
balanceEl.textContent = '...';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 618 |
try {
|
| 619 |
+
const response = await fetch('/ton_balance', {
|
| 620 |
method: 'POST',
|
| 621 |
headers: {
|
| 622 |
'Content-Type': 'application/json',
|
| 623 |
'Accept': 'application/json'
|
| 624 |
},
|
| 625 |
+
body: JSON.stringify({ address: address }),
|
| 626 |
});
|
| 627 |
+
|
| 628 |
if (!response.ok) {
|
| 629 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
|
| 630 |
}
|
| 631 |
+
|
| 632 |
const data = await response.json();
|
| 633 |
if (data.status === 'ok') {
|
| 634 |
+
balanceEl.textContent = data.balance;
|
|
|
|
|
|
|
| 635 |
} else {
|
| 636 |
+
throw new Error(data.message || 'Failed to fetch balance');
|
| 637 |
}
|
| 638 |
} catch (error) {
|
| 639 |
+
console.error('Error fetching balance:', error);
|
| 640 |
+
balanceEl.textContent = 'Ошибка';
|
| 641 |
+
if (tg.HapticFeedback) tg.HapticFeedback.notificationOccurred('error');
|
| 642 |
}
|
| 643 |
}
|
| 644 |
|
| 645 |
+
function setupTonConnect() {
|
| 646 |
+
const tonConnectUI = new TonConnectUI.TonConnectUI({
|
| 647 |
+
manifestUrl: window.location.origin + '/tonconnect-manifest.json',
|
| 648 |
+
uiOptions: {
|
| 649 |
+
buttonRootId: 'ton-connect-button'
|
| 650 |
+
}
|
| 651 |
+
});
|
| 652 |
+
|
| 653 |
+
const walletInfoEl = document.getElementById('wallet-info');
|
| 654 |
+
const walletAddressEl = document.getElementById('wallet-address');
|
| 655 |
+
|
| 656 |
+
tonConnectUI.onStatusChange(wallet => {
|
| 657 |
+
if (wallet) {
|
| 658 |
+
const address = TonConnectUI.toUserFriendlyAddress(wallet.account.address);
|
| 659 |
+
walletAddressEl.textContent = `${address.slice(0, 6)}...${address.slice(-4)}`;
|
| 660 |
+
walletInfoEl.style.display = 'block';
|
| 661 |
+
fetchBalance(wallet.account.address);
|
| 662 |
+
} else {
|
| 663 |
+
walletInfoEl.style.display = 'none';
|
| 664 |
+
document.getElementById('wallet-balance').textContent = '...';
|
| 665 |
+
walletAddressEl.textContent = '';
|
| 666 |
+
}
|
| 667 |
+
});
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
|
| 671 |
function setupTelegram() {
|
| 672 |
if (!tg || !tg.initData) {
|
|
|
|
| 698 |
.then(data => {
|
| 699 |
if (data.status === 'ok' && data.verified) {
|
| 700 |
console.log('Backend verification successful.');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 701 |
} else {
|
| 702 |
console.warn('Backend verification failed:', data.message);
|
|
|
|
| 703 |
}
|
| 704 |
})
|
| 705 |
.catch(error => {
|
| 706 |
console.error('Error sending initData for verification:', error);
|
|
|
|
| 707 |
});
|
| 708 |
|
| 709 |
const user = tg.initDataUnsafe?.user;
|
|
|
|
| 736 |
tg.HapticFeedback.impactOccurred('light');
|
| 737 |
}
|
| 738 |
});
|
| 739 |
+
|
| 740 |
+
closeBtn.addEventListener('click', () => {
|
| 741 |
+
modal.style.display = "none";
|
| 742 |
+
});
|
| 743 |
+
|
| 744 |
window.addEventListener('click', (event) => {
|
| 745 |
+
if (event.target == modal) {
|
| 746 |
+
modal.style.display = "none";
|
| 747 |
+
}
|
| 748 |
});
|
| 749 |
} else {
|
| 750 |
console.error("Modal elements not found!");
|
| 751 |
}
|
| 752 |
+
|
| 753 |
+
setupTonConnect();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 754 |
|
| 755 |
document.body.style.visibility = 'visible';
|
| 756 |
}
|
|
|
|
| 769 |
}
|
| 770 |
}, 3500);
|
| 771 |
}
|
| 772 |
+
|
| 773 |
</script>
|
| 774 |
</body>
|
| 775 |
</html>
|
|
|
|
| 813 |
h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
|
| 814 |
.user-grid {
|
| 815 |
display: grid;
|
| 816 |
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
| 817 |
gap: var(--padding);
|
| 818 |
margin-top: var(--padding);
|
| 819 |
}
|
|
|
|
| 841 |
}
|
| 842 |
.user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
|
| 843 |
.user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
|
| 844 |
+
.user-card .details { font-size: 0.9em; color: #495057; word-break: break-word; }
|
| 845 |
+
.user-card .detail-item { margin-bottom: 0.3rem; }
|
| 846 |
.user-card .detail-item strong { color: var(--admin-text); }
|
| 847 |
.user-card .timestamp { font-size: 0.8em; color: var(--admin-secondary); margin-top: 1rem; }
|
| 848 |
.no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
|
|
|
|
| 930 |
<div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
|
| 931 |
<div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
|
| 932 |
<div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
|
|
|
|
| 933 |
</div>
|
| 934 |
<div class="timestamp">Визит: {{ user.visited_at_str }}</div>
|
| 935 |
</div>
|
|
|
|
| 968 |
loader.style.display = 'none';
|
| 969 |
}
|
| 970 |
}
|
| 971 |
+
|
| 972 |
+
function triggerDownload() {
|
| 973 |
+
handleFetch('/admin/download_data', 'скачивание');
|
| 974 |
+
}
|
| 975 |
+
|
| 976 |
+
function triggerUpload() {
|
| 977 |
+
handleFetch('/admin/upload_data', 'загрузка');
|
| 978 |
+
}
|
| 979 |
</script>
|
| 980 |
</body>
|
| 981 |
</html>
|
|
|
|
| 986 |
theme_params = {}
|
| 987 |
return render_template_string(TEMPLATE, theme=theme_params)
|
| 988 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 989 |
@app.route('/verify', methods=['POST'])
|
| 990 |
def verify_data():
|
| 991 |
try:
|
|
|
|
| 995 |
return jsonify({"status": "error", "message": "Missing initData"}), 400
|
| 996 |
|
| 997 |
user_data_parsed, is_valid = verify_telegram_data(init_data_str)
|
| 998 |
+
|
| 999 |
user_info_dict = {}
|
| 1000 |
if user_data_parsed and 'user' in user_data_parsed:
|
| 1001 |
try:
|
|
|
|
| 1003 |
user_info_dict = json.loads(user_json_str)
|
| 1004 |
except Exception as e:
|
| 1005 |
logging.error(f"Could not parse user JSON: {e}")
|
| 1006 |
+
user_info_dict = {}
|
| 1007 |
+
|
| 1008 |
+
if is_valid:
|
| 1009 |
+
user_id = user_info_dict.get('id')
|
| 1010 |
+
if user_id:
|
| 1011 |
+
now = time.time()
|
| 1012 |
+
user_entry = {
|
| 1013 |
+
str(user_id): {
|
| 1014 |
+
'id': user_id,
|
| 1015 |
+
'first_name': user_info_dict.get('first_name'),
|
| 1016 |
+
'last_name': user_info_dict.get('last_name'),
|
| 1017 |
+
'username': user_info_dict.get('username'),
|
| 1018 |
+
'photo_url': user_info_dict.get('photo_url'),
|
| 1019 |
+
'language_code': user_info_dict.get('language_code'),
|
| 1020 |
+
'is_premium': user_info_dict.get('is_premium', False),
|
| 1021 |
+
'phone_number': user_info_dict.get('phone_number'),
|
| 1022 |
+
'visited_at': now,
|
| 1023 |
+
'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
|
| 1024 |
+
}
|
| 1025 |
+
}
|
| 1026 |
+
save_visitor_data(user_entry)
|
| 1027 |
+
return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1028 |
else:
|
| 1029 |
+
logging.warning(f"Verification failed for user: {user_info_dict.get('id')}")
|
| 1030 |
return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
|
| 1031 |
|
| 1032 |
except Exception as e:
|
| 1033 |
logging.exception("Error in /verify endpoint")
|
| 1034 |
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
| 1035 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1036 |
@app.route('/admin')
|
| 1037 |
def admin_panel():
|
| 1038 |
current_data = load_visitor_data()
|
|
|
|
| 1054 |
upload_data_to_hf_async()
|
| 1055 |
return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
|
| 1056 |
|
| 1057 |
+
@app.route('/tonconnect-manifest.json')
|
| 1058 |
+
def tonconnect_manifest():
|
| 1059 |
+
return jsonify({
|
| 1060 |
+
"url": request.host_url.rstrip('/'),
|
| 1061 |
+
"name": "Morshen Group App",
|
| 1062 |
+
"iconUrl": "https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg"
|
| 1063 |
+
})
|
| 1064 |
+
|
| 1065 |
+
@app.route('/ton_balance', methods=['POST'])
|
| 1066 |
+
def get_ton_balance():
|
| 1067 |
+
req_data = request.get_json()
|
| 1068 |
+
address = req_data.get('address')
|
| 1069 |
+
if not address:
|
| 1070 |
+
return jsonify({"status": "error", "message": "Address is required"}), 400
|
| 1071 |
+
|
| 1072 |
+
try:
|
| 1073 |
+
api_url = f"https://toncenter.com/api/v2/getWalletInformation?address={address}"
|
| 1074 |
+
response = requests.get(api_url)
|
| 1075 |
+
response.raise_for_status()
|
| 1076 |
+
data = response.json()
|
| 1077 |
+
|
| 1078 |
+
if data.get("ok"):
|
| 1079 |
+
balance_nanotons = int(data["result"]["balance"])
|
| 1080 |
+
balance_ton = balance_nanotons / 1_000_000_000
|
| 1081 |
+
return jsonify({"status": "ok", "balance": f"{balance_ton:.4f}"})
|
| 1082 |
+
else:
|
| 1083 |
+
return jsonify({"status": "error", "message": "Failed to get balance from API"}), 500
|
| 1084 |
+
except requests.exceptions.RequestException as e:
|
| 1085 |
+
logging.error(f"Error fetching TON balance from Toncenter: {e}")
|
| 1086 |
+
return jsonify({"status": "error", "message": "Could not connect to TON API"}), 503
|
| 1087 |
+
except (KeyError, ValueError) as e:
|
| 1088 |
+
logging.error(f"Error parsing Toncenter response: {e}")
|
| 1089 |
+
return jsonify({"status": "error", "message": "Invalid API response from Toncenter"}), 500
|
| 1090 |
|
| 1091 |
if __name__ == '__main__':
|
| 1092 |
+
print("---")
|
| 1093 |
print("--- MORSHEN GROUP MINI APP SERVER ---")
|
| 1094 |
+
print("---")
|
| 1095 |
print(f"Flask server starting on http://{HOST}:{PORT}")
|
| 1096 |
print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
|
| 1097 |
+
print(f"Visitor data file: {DATA_FILE}")
|
| 1098 |
+
print(f"Hugging Face Repo: {REPO_ID}")
|
| 1099 |
+
print(f"HF Data Path: {HF_DATA_FILE_PATH}")
|
| 1100 |
if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
|
| 1101 |
+
print("---")
|
| 1102 |
+
print("--- WARNING: HUGGING FACE TOKEN(S) NOT SET ---")
|
| 1103 |
+
print("--- Backup/restore functionality will be limited. Set HF_TOKEN_READ and HF_TOKEN_WRITE environment variables.")
|
| 1104 |
+
print("---")
|
| 1105 |
else:
|
| 1106 |
+
print("--- Hugging Face tokens found.")
|
| 1107 |
+
print("--- Attempting initial data download from Hugging Face...")
|
| 1108 |
download_data_from_hf()
|
| 1109 |
|
| 1110 |
load_visitor_data()
|
| 1111 |
+
|
| 1112 |
+
print("---")
|
| 1113 |
+
print("--- SECURITY WARNING ---")
|
| 1114 |
+
print("--- The /admin route and its sub-routes are NOT protected.")
|
| 1115 |
+
print("--- Implement proper authentication before deploying.")
|
| 1116 |
+
print("---")
|
| 1117 |
|
| 1118 |
if HF_TOKEN_WRITE:
|
| 1119 |
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|