Update app.py
Browse files
app.py
CHANGED
|
@@ -13,8 +13,10 @@ import logging
|
|
| 13 |
import threading
|
| 14 |
from huggingface_hub import HfApi, hf_hub_download
|
| 15 |
from huggingface_hub.utils import RepositoryNotFoundError
|
|
|
|
| 16 |
|
| 17 |
-
|
|
|
|
| 18 |
HOST = '0.0.0.0'
|
| 19 |
PORT = 7860
|
| 20 |
DATA_FILE = 'data.json'
|
|
@@ -24,6 +26,8 @@ HF_DATA_FILE_PATH = "data.json"
|
|
| 24 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
|
| 25 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 26 |
|
|
|
|
|
|
|
| 27 |
app = Flask(__name__)
|
| 28 |
logging.basicConfig(level=logging.INFO)
|
| 29 |
app.secret_key = os.urandom(24)
|
|
@@ -31,10 +35,21 @@ app.secret_key = os.urandom(24)
|
|
| 31 |
_data_lock = threading.Lock()
|
| 32 |
visitor_data_cache = {}
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
def download_data_from_hf():
|
| 35 |
global visitor_data_cache
|
| 36 |
if not HF_TOKEN_READ:
|
| 37 |
-
logging.warning("HF_TOKEN_READ not set.")
|
| 38 |
return False
|
| 39 |
try:
|
| 40 |
logging.info(f"Attempting to download {HF_DATA_FILE_PATH} from {REPO_ID}...")
|
|
@@ -83,12 +98,14 @@ def load_visitor_data():
|
|
| 83 |
visitor_data_cache = {}
|
| 84 |
return visitor_data_cache
|
| 85 |
|
| 86 |
-
def save_visitor_data(
|
| 87 |
with _data_lock:
|
| 88 |
try:
|
| 89 |
-
visitor_data_cache.
|
|
|
|
|
|
|
| 90 |
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 91 |
-
json.dump(
|
| 92 |
logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
|
| 93 |
upload_data_to_hf_async()
|
| 94 |
except Exception as e:
|
|
@@ -173,7 +190,7 @@ TEMPLATE = """
|
|
| 173 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
|
| 174 |
<title>Morshen Group</title>
|
| 175 |
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 176 |
-
<script src="https://unpkg.com/@tonconnect/
|
| 177 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 178 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 179 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
@@ -194,6 +211,7 @@ TEMPLATE = """
|
|
| 194 |
--text-secondary-color: var(--tg-theme-hint-color);
|
| 195 |
--accent-gradient: linear-gradient(95deg, var(--tg-theme-button-color, #007aff), #5856d6);
|
| 196 |
--accent-gradient-green: linear-gradient(95deg, #34c759, #30d158);
|
|
|
|
| 197 |
--tag-bg: rgba(255, 255, 255, 0.1);
|
| 198 |
--border-radius-s: 8px;
|
| 199 |
--border-radius-m: 14px;
|
|
@@ -274,6 +292,14 @@ TEMPLATE = """
|
|
| 274 |
background: rgba(44, 44, 46, 0.95);
|
| 275 |
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), var(--shadow-medium);
|
| 276 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
.tag {
|
| 278 |
display: inline-block; background: var(--tag-bg); color: var(--text-secondary-color);
|
| 279 |
padding: 6px 12px; border-radius: var(--border-radius-s); font-size: 0.85em;
|
|
@@ -366,6 +392,59 @@ TEMPLATE = """
|
|
| 366 |
}
|
| 367 |
.save-card-button i { font-size: 1.2em; }
|
| 368 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
.modal {
|
| 370 |
display: none; position: fixed; z-index: 1001;
|
| 371 |
left: 0; top: 0; width: 100%; height: 100%;
|
|
@@ -397,37 +476,7 @@ TEMPLATE = """
|
|
| 397 |
.modal-text b { color: var(--tg-theme-link-color); font-weight: 600; }
|
| 398 |
.modal-instruction { font-size: 1em; color: var(--text-secondary-color); margin-top: var(--padding-m); }
|
| 399 |
|
| 400 |
-
|
| 401 |
-
margin-top: var(--padding-l);
|
| 402 |
-
text-align: center;
|
| 403 |
-
padding: var(--padding-l);
|
| 404 |
-
border-radius: var(--border-radius-l);
|
| 405 |
-
background-color: var(--card-bg);
|
| 406 |
-
box-shadow: var(--shadow-medium);
|
| 407 |
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 408 |
-
backdrop-filter: blur(var(--backdrop-blur));
|
| 409 |
-
-webkit-backdrop-filter: blur(var(--backdrop-blur));
|
| 410 |
-
}
|
| 411 |
-
.ton-section h3 {
|
| 412 |
-
font-size: 1.5em;
|
| 413 |
-
font-weight: 700;
|
| 414 |
-
margin-bottom: var(--padding-s);
|
| 415 |
-
color: var(--text-color);
|
| 416 |
-
}
|
| 417 |
-
.ton-connect-widget-container {
|
| 418 |
-
margin-top: var(--padding-m);
|
| 419 |
-
}
|
| 420 |
-
.ton-info {
|
| 421 |
-
margin-top: var(--padding-m);
|
| 422 |
-
font-size: 1.1em;
|
| 423 |
-
color: var(--text-secondary-color);
|
| 424 |
-
}
|
| 425 |
-
.ton-info div { margin-bottom: var(--padding-s); word-break: break-all; }
|
| 426 |
-
.ton-info strong { color: var(--text-color); font-weight: 600; }
|
| 427 |
-
.ton-address { color: var(--tg-theme-link-color); font-weight: 500;}
|
| 428 |
-
.ton-balance { color: var(--accent-gradient-green, #34c759); font-weight: 600; }
|
| 429 |
-
|
| 430 |
-
|
| 431 |
.icon { display: inline-block; width: 1.2em; text-align: center; margin-right: 8px; opacity: 0.9; }
|
| 432 |
.icon-save::before { content: '💾'; }
|
| 433 |
.icon-web::before { content: '🌐'; }
|
|
@@ -448,8 +497,10 @@ TEMPLATE = """
|
|
| 448 |
.icon-link::before { content: '🔗'; }
|
| 449 |
.icon-leader::before { content: '🏆'; }
|
| 450 |
.icon-company::before { content: '🏢'; }
|
| 451 |
-
|
| 452 |
|
|
|
|
|
|
|
| 453 |
@media (max-width: 480px) {
|
| 454 |
.section-title { font-size: 1.8em; }
|
| 455 |
.logo span { font-size: 1.4em; }
|
|
@@ -458,8 +509,8 @@ TEMPLATE = """
|
|
| 458 |
.stats-grid { grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: var(--padding-s); }
|
| 459 |
.stat-value { font-size: 1.5em; }
|
| 460 |
.modal-content { margin: 25% auto; width: 92%; }
|
| 461 |
-
.ton-section h3 { font-size: 1.3em; }
|
| 462 |
-
.ton-info { font-size: 1em; }
|
| 463 |
}
|
| 464 |
</style>
|
| 465 |
</head>
|
|
@@ -487,6 +538,19 @@ TEMPLATE = """
|
|
| 487 |
</a>
|
| 488 |
</section>
|
| 489 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
<section class="ecosystem-header">
|
| 491 |
<h2 class="section-title"><i class="icon icon-company"></i>Экосистема инноваций</h2>
|
| 492 |
<p class="description">
|
|
@@ -571,18 +635,6 @@ TEMPLATE = """
|
|
| 571 |
</div>
|
| 572 |
</section>
|
| 573 |
|
| 574 |
-
<section class="ton-section">
|
| 575 |
-
<h3><i class="icon icon-ton"></i> TON Wallet</h3>
|
| 576 |
-
<p class="description" style="margin-bottom: var(--padding-m); font-size: 1em;">Подключите свой TON кошелек для авторизации и просмотра баланса.</p>
|
| 577 |
-
<div id="ton-connect-ui" class="ton-connect-widget-container"></div>
|
| 578 |
-
<div class="ton-info">
|
| 579 |
-
<div id="ton-status">Статус: Не подключен</div>
|
| 580 |
-
<div id="ton-address" style="display: none;">Адрес: <span class="ton-address"></span></div>
|
| 581 |
-
<div id="ton-balance" style="display: none;">Баланс: <span class="ton-balance"></span></div>
|
| 582 |
-
</div>
|
| 583 |
-
</section>
|
| 584 |
-
|
| 585 |
-
|
| 586 |
<footer class="footer-greeting">
|
| 587 |
<p id="greeting">Инициализация...</p>
|
| 588 |
</footer>
|
|
@@ -593,6 +645,7 @@ TEMPLATE = """
|
|
| 593 |
<i class="icon icon-save"></i>Сохранить визитку
|
| 594 |
</button>
|
| 595 |
|
|
|
|
| 596 |
<div id="saveModal" class="modal">
|
| 597 |
<div class="modal-content">
|
| 598 |
<span class="modal-close" id="modal-close-btn">×</span>
|
|
@@ -603,6 +656,7 @@ TEMPLATE = """
|
|
| 603 |
</div>
|
| 604 |
</div>
|
| 605 |
|
|
|
|
| 606 |
<script>
|
| 607 |
const tg = window.Telegram.WebApp;
|
| 608 |
|
|
@@ -627,28 +681,135 @@ TEMPLATE = """
|
|
| 627 |
}
|
| 628 |
}
|
| 629 |
|
| 630 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 631 |
|
| 632 |
-
async function fetchTonBalance(address) {
|
| 633 |
-
const tonCenterUrl = `https://toncenter.com/api/v3/account/getBalance?address=${address}`;
|
| 634 |
-
try {
|
| 635 |
-
const response = await fetch(tonCenterUrl);
|
| 636 |
-
if (!response.ok) {
|
| 637 |
-
throw new Error(`HTTP error! status: ${response.status}`);
|
| 638 |
-
}
|
| 639 |
-
const data = await response.json();
|
| 640 |
-
if (data.error) {
|
| 641 |
-
throw new Error(data.error);
|
| 642 |
-
}
|
| 643 |
-
// Balance is in NanoTONs, convert to TON
|
| 644 |
-
const balanceNano = BigInt(data.result.balance);
|
| 645 |
-
const balanceTon = Number(balanceNano) / 1e9;
|
| 646 |
-
return balanceTon.toFixed(2); // Format to 2 decimal places
|
| 647 |
-
} catch (error) {
|
| 648 |
-
console.error("Error fetching TON balance:", error);
|
| 649 |
-
return "Ошибка";
|
| 650 |
-
}
|
| 651 |
-
}
|
| 652 |
|
| 653 |
function setupTelegram() {
|
| 654 |
if (!tg || !tg.initData) {
|
|
@@ -656,6 +817,7 @@ TEMPLATE = """
|
|
| 656 |
const greetingElement = document.getElementById('greeting');
|
| 657 |
if(greetingElement) greetingElement.textContent = 'Не удалось связаться с Telegram.';
|
| 658 |
document.body.style.visibility = 'visible';
|
|
|
|
| 659 |
return;
|
| 660 |
}
|
| 661 |
|
|
@@ -680,12 +842,18 @@ TEMPLATE = """
|
|
| 680 |
.then(data => {
|
| 681 |
if (data.status === 'ok' && data.verified) {
|
| 682 |
console.log('Backend verification successful.');
|
|
|
|
|
|
|
| 683 |
} else {
|
| 684 |
console.warn('Backend verification failed:', data.message);
|
|
|
|
|
|
|
| 685 |
}
|
| 686 |
})
|
| 687 |
.catch(error => {
|
| 688 |
console.error('Error sending initData for verification:', error);
|
|
|
|
|
|
|
| 689 |
});
|
| 690 |
|
| 691 |
|
|
@@ -733,87 +901,9 @@ TEMPLATE = """
|
|
| 733 |
console.error("Modal elements not found!");
|
| 734 |
}
|
| 735 |
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
document.body.style.visibility = 'visible';
|
| 739 |
}
|
| 740 |
|
| 741 |
-
function setupTonConnect() {
|
| 742 |
-
if (!window.TonConnectUI) {
|
| 743 |
-
console.error("TonConnectUI script not loaded.");
|
| 744 |
-
document.getElementById('ton-status').textContent = 'Ошибка загрузки TonConnect.';
|
| 745 |
-
return;
|
| 746 |
-
}
|
| 747 |
-
|
| 748 |
-
try {
|
| 749 |
-
tonConnectUI = new window.TonConnectUI({
|
| 750 |
-
manifestUrl: window.location.origin + '/tonconnect-manifest.json',
|
| 751 |
-
uiPreferences: {
|
| 752 |
-
colorsSet: tg.themeParams.bg_color ? 'STANDARD' : 'DARK', // Use standard or dark based on TG theme
|
| 753 |
-
// You can customize colors further based on tg.themeParams if needed
|
| 754 |
-
// colors: {
|
| 755 |
-
// dark: { ... },
|
| 756 |
-
// light: { ... }
|
| 757 |
-
// }
|
| 758 |
-
},
|
| 759 |
-
});
|
| 760 |
-
|
| 761 |
-
tonConnectUI.onStatusChange(async wallet => {
|
| 762 |
-
const tonStatusElement = document.getElementById('ton-status');
|
| 763 |
-
const tonAddressElement = document.getElementById('ton-address');
|
| 764 |
-
const tonBalanceElement = document.getElementById('ton-balance');
|
| 765 |
-
|
| 766 |
-
if (wallet) {
|
| 767 |
-
const address = wallet.account.address;
|
| 768 |
-
tonStatusElement.textContent = 'Статус: Подключен';
|
| 769 |
-
tonAddressElement.style.display = 'block';
|
| 770 |
-
tonAddressElement.querySelector('span').textContent = address;
|
| 771 |
-
|
| 772 |
-
tonBalanceElement.style.display = 'block';
|
| 773 |
-
tonBalanceElement.querySelector('span').textContent = 'Загрузка...';
|
| 774 |
-
|
| 775 |
-
const balance = await fetchTonBalance(address);
|
| 776 |
-
tonBalanceElement.querySelector('span').textContent = balance + ' TON';
|
| 777 |
-
|
| 778 |
-
console.log('TON Wallet connected:', address);
|
| 779 |
-
console.log('TON Account:', wallet.account);
|
| 780 |
-
// You might want to send this address to the backend here
|
| 781 |
-
// to link it to the Telegram user ID if needed for your application logic.
|
| 782 |
-
|
| 783 |
-
} else {
|
| 784 |
-
tonStatusElement.textContent = 'Статус: Не подключен';
|
| 785 |
-
tonAddressElement.style.display = 'none';
|
| 786 |
-
tonBalanceElement.style.display = 'none';
|
| 787 |
-
console.log('TON Wallet disconnected.');
|
| 788 |
-
}
|
| 789 |
-
});
|
| 790 |
-
|
| 791 |
-
// Render the TonConnect button/widget
|
| 792 |
-
const tonConnectUiContainer = document.getElementById('ton-connect-ui');
|
| 793 |
-
if (tonConnectUiContainer) {
|
| 794 |
-
tonConnectUI.renderButtons(tonConnectUiContainer, {
|
| 795 |
-
messages: {
|
| 796 |
-
connectingModalWithoutWallets: 'Пожалуйста, установите кошелек, поддерживающий TonConnect.',
|
| 797 |
-
connectingModalTitle: 'Подключение кошелька TON',
|
| 798 |
-
walletConnectRequestTemporarilyUnavailable: 'Запрос подключения временно недоступен. Попробуйте позже.',
|
| 799 |
-
},
|
| 800 |
-
labels: {
|
| 801 |
-
disconnect: 'Отключить кошелек',
|
| 802 |
-
connectWallet: 'Подключить TON Кошелек',
|
| 803 |
-
connectedWallet: 'Подключен {{walletName}}',
|
| 804 |
-
}
|
| 805 |
-
});
|
| 806 |
-
} else {
|
| 807 |
-
console.error("TON Connect UI container not found!");
|
| 808 |
-
}
|
| 809 |
-
|
| 810 |
-
} catch (e) {
|
| 811 |
-
console.error("Error initializing TonConnectUI:", e);
|
| 812 |
-
document.getElementById('ton-status').textContent = 'Ошибка инициализации TonConnect.';
|
| 813 |
-
}
|
| 814 |
-
}
|
| 815 |
-
|
| 816 |
-
|
| 817 |
if (window.Telegram && window.Telegram.WebApp) {
|
| 818 |
setupTelegram();
|
| 819 |
} else {
|
|
@@ -825,7 +915,7 @@ TEMPLATE = """
|
|
| 825 |
const greetingElement = document.getElementById('greeting');
|
| 826 |
if(greetingElement) greetingElement.textContent = 'Ошибка загрузки интерфейса Telegram.';
|
| 827 |
document.body.style.visibility = 'visible';
|
| 828 |
-
setupTonConnect(); // Still try
|
| 829 |
}
|
| 830 |
}, 3500);
|
| 831 |
}
|
|
@@ -901,10 +991,17 @@ ADMIN_TEMPLATE = """
|
|
| 901 |
}
|
| 902 |
.user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
|
| 903 |
.user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
|
| 904 |
-
.user-card .
|
| 905 |
-
.user-card .details { font-size: 0.9em; color: #495057; word-break: break-word; }
|
| 906 |
.user-card .detail-item { margin-bottom: 0.3rem; }
|
| 907 |
-
.user-card .detail-item strong { color: var(--admin-text); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 908 |
.user-card .timestamp { font-size: 0.8em; color: var(--admin-secondary); margin-top: 1rem; }
|
| 909 |
.no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
|
| 910 |
.alert {
|
|
@@ -927,6 +1024,7 @@ ADMIN_TEMPLATE = """
|
|
| 927 |
}
|
| 928 |
.refresh-btn:hover { background-color: #0b5ed7; }
|
| 929 |
|
|
|
|
| 930 |
.admin-controls {
|
| 931 |
background: var(--admin-card-bg);
|
| 932 |
padding: var(--padding);
|
|
@@ -983,7 +1081,7 @@ ADMIN_TEMPLATE = """
|
|
| 983 |
<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">
|
| 984 |
<div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
|
| 985 |
{% if user.username %}
|
| 986 |
-
<div class="username"><a href="https://t.me/{{ user.username }}" target="_blank">@{{ user.username }}</a></div>
|
| 987 |
{% else %}
|
| 988 |
<div class="username" style="height: 1.3em;"></div>
|
| 989 |
{% endif %}
|
|
@@ -992,9 +1090,14 @@ ADMIN_TEMPLATE = """
|
|
| 992 |
<div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
|
| 993 |
<div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
|
| 994 |
<div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 998 |
</div>
|
| 999 |
<div class="timestamp">Визит: {{ user.visited_at_str }}</div>
|
| 1000 |
</div>
|
|
@@ -1046,19 +1149,22 @@ ADMIN_TEMPLATE = """
|
|
| 1046 |
</html>
|
| 1047 |
"""
|
| 1048 |
|
| 1049 |
-
TON_MANIFEST = {
|
| 1050 |
-
"url": os.getenv("APP_BASE_URL", "https://localhost:7860") + "/",
|
| 1051 |
-
"name": "Morshen Group TMA",
|
| 1052 |
-
"iconUrl": os.getenv("APP_BASE_URL", "https://localhost:7860") + "/static/morshengroup.jpg", # Or a publicly hosted image
|
| 1053 |
-
"termsOfServiceUrl": "https://example.com/terms", # Replace with your actual terms
|
| 1054 |
-
"privacyPolicyUrl": "https://example.com/privacy" # Replace with your actual privacy policy
|
| 1055 |
-
}
|
| 1056 |
-
|
| 1057 |
@app.route('/')
|
| 1058 |
def index():
|
| 1059 |
theme_params = {}
|
| 1060 |
return render_template_string(TEMPLATE, theme=theme_params)
|
| 1061 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1062 |
@app.route('/verify', methods=['POST'])
|
| 1063 |
def verify_data():
|
| 1064 |
try:
|
|
@@ -1077,48 +1183,129 @@ def verify_data():
|
|
| 1077 |
user_info_dict = json.loads(user_json_str)
|
| 1078 |
user_id = user_info_dict.get('id')
|
| 1079 |
except Exception as e:
|
| 1080 |
-
logging.error(f"Could not parse user JSON: {e}")
|
| 1081 |
user_info_dict = {}
|
| 1082 |
|
| 1083 |
if is_valid and user_id:
|
| 1084 |
now = time.time()
|
| 1085 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1086 |
current_data = load_visitor_data()
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
'first_name': user_info_dict.get('first_name', existing_user_entry.get('first_name')),
|
| 1096 |
-
'last_name': user_info_dict.get('last_name', existing_user_entry.get('last_name')),
|
| 1097 |
-
'username': user_info_dict.get('username', existing_user_entry.get('username')),
|
| 1098 |
-
'photo_url': user_info_dict.get('photo_url', existing_user_entry.get('photo_url')),
|
| 1099 |
-
'language_code': user_info_dict.get('language_code', existing_user_entry.get('language_code')),
|
| 1100 |
-
'is_premium': user_info_dict.get('is_premium', existing_user_entry.get('is_premium', False)),
|
| 1101 |
-
'phone_number': user_info_dict.get('phone_number', existing_user_entry.get('phone_number')),
|
| 1102 |
-
'visited_at': now,
|
| 1103 |
-
'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
|
| 1104 |
}
|
|
|
|
| 1105 |
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
|
|
|
| 1109 |
|
| 1110 |
-
|
| 1111 |
-
|
|
|
|
| 1112 |
|
| 1113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1114 |
else:
|
| 1115 |
-
logging.warning(f"
|
| 1116 |
-
return jsonify({"status": "error", "
|
| 1117 |
|
| 1118 |
except Exception as e:
|
| 1119 |
-
logging.exception("Error in /
|
| 1120 |
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
| 1121 |
|
|
|
|
| 1122 |
@app.route('/admin')
|
| 1123 |
def admin_panel():
|
| 1124 |
current_data = load_visitor_data()
|
|
@@ -1140,37 +1327,15 @@ def admin_trigger_upload():
|
|
| 1140 |
upload_data_to_hf_async()
|
| 1141 |
return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
|
| 1142 |
|
| 1143 |
-
@app.route('/
|
| 1144 |
-
def
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
"url": manifest_url + "/",
|
| 1153 |
-
"name": "Morshen Group TMA",
|
| 1154 |
-
"iconUrl": manifest_url + "/static/morshengroup.jpg",
|
| 1155 |
-
"termsOfServiceUrl": manifest_url + "/terms", # Placeholder
|
| 1156 |
-
"privacyPolicyUrl": manifest_url + "/privacy" # Placeholder
|
| 1157 |
-
}
|
| 1158 |
-
return jsonify(manifest)
|
| 1159 |
-
|
| 1160 |
-
@app.route('/static/morshengroup.jpg')
|
| 1161 |
-
def serve_logo():
|
| 1162 |
-
# This route serves the logo locally if needed,
|
| 1163 |
-
# but the HF link in the template is likely better.
|
| 1164 |
-
# If you put morshengroup.jpg in a 'static' folder next to app.py,
|
| 1165 |
-
# Flask can serve it. For Hugging Face Spaces, serving static files
|
| 1166 |
-
# from the repo path directly via URL (like in the template) is common.
|
| 1167 |
-
# Keeping this route as a placeholder or for local testing.
|
| 1168 |
-
try:
|
| 1169 |
-
# Attempt to serve from local static folder
|
| 1170 |
-
return app.send_static_file('morshengroup.jpg')
|
| 1171 |
-
except FileNotFoundError:
|
| 1172 |
-
# Fallback or error if file isn't there
|
| 1173 |
-
return "Logo not found", 404
|
| 1174 |
|
| 1175 |
|
| 1176 |
if __name__ == '__main__':
|
|
@@ -1182,10 +1347,6 @@ if __name__ == '__main__':
|
|
| 1182 |
print(f"Visitor data file: {DATA_FILE}")
|
| 1183 |
print(f"Hugging Face Repo: {REPO_ID}")
|
| 1184 |
print(f"HF Data Path: {HF_DATA_FILE_PATH}")
|
| 1185 |
-
if os.getenv("APP_BASE_URL"):
|
| 1186 |
-
print(f"App Base URL (for manifest): {os.getenv('APP_BASE_URL')}")
|
| 1187 |
-
else:
|
| 1188 |
-
print("APP_BASE_URL not set. Manifest URL will be based on request host.")
|
| 1189 |
if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
|
| 1190 |
print("---")
|
| 1191 |
print("--- WARNING: HUGGING FACE TOKEN(S) NOT SET ---")
|
|
@@ -1196,6 +1357,15 @@ if __name__ == '__main__':
|
|
| 1196 |
print("--- Attempting initial data download from Hugging Face...")
|
| 1197 |
download_data_from_hf()
|
| 1198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1199 |
load_visitor_data()
|
| 1200 |
|
| 1201 |
print("---")
|
|
@@ -1211,5 +1381,22 @@ if __name__ == '__main__':
|
|
| 1211 |
else:
|
| 1212 |
print("--- Periodic backup disabled (HF_TOKEN_WRITE missing).")
|
| 1213 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1214 |
print("--- Server Ready ---")
|
|
|
|
| 1215 |
app.run(host=HOST, port=PORT, debug=False)
|
|
|
|
| 13 |
import threading
|
| 14 |
from huggingface_hub import HfApi, hf_hub_download
|
| 15 |
from huggingface_hub.utils import RepositoryNotFoundError
|
| 16 |
+
from pytonapi import Tonapi
|
| 17 |
|
| 18 |
+
|
| 19 |
+
BOT_TOKEN = os.getenv("BOT_TOKEN", "7566834146:AAGiG4MaTZZvvtTVsqEJVG5SYK5hUlc_Ewo")
|
| 20 |
HOST = '0.0.0.0'
|
| 21 |
PORT = 7860
|
| 22 |
DATA_FILE = 'data.json'
|
|
|
|
| 26 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
|
| 27 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 28 |
|
| 29 |
+
TONAPI_KEY = os.getenv("TONAPI_KEY")
|
| 30 |
+
|
| 31 |
app = Flask(__name__)
|
| 32 |
logging.basicConfig(level=logging.INFO)
|
| 33 |
app.secret_key = os.urandom(24)
|
|
|
|
| 35 |
_data_lock = threading.Lock()
|
| 36 |
visitor_data_cache = {}
|
| 37 |
|
| 38 |
+
tonapi = None
|
| 39 |
+
if TONAPI_KEY:
|
| 40 |
+
try:
|
| 41 |
+
tonapi = Tonapi(api_key=TONAPI_KEY)
|
| 42 |
+
logging.info("TONAPI initialized.")
|
| 43 |
+
except Exception as e:
|
| 44 |
+
logging.error(f"Failed to initialize TONAPI: {e}")
|
| 45 |
+
else:
|
| 46 |
+
logging.warning("TONAPI_KEY not set. TON wallet functionality will be limited.")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
def download_data_from_hf():
|
| 50 |
global visitor_data_cache
|
| 51 |
if not HF_TOKEN_READ:
|
| 52 |
+
logging.warning("HF_TOKEN_READ not set. Skipping Hugging Face download.")
|
| 53 |
return False
|
| 54 |
try:
|
| 55 |
logging.info(f"Attempting to download {HF_DATA_FILE_PATH} from {REPO_ID}...")
|
|
|
|
| 98 |
visitor_data_cache = {}
|
| 99 |
return visitor_data_cache
|
| 100 |
|
| 101 |
+
def save_visitor_data(data_entry):
|
| 102 |
with _data_lock:
|
| 103 |
try:
|
| 104 |
+
current_data = visitor_data_cache.copy()
|
| 105 |
+
current_data.update(data_entry)
|
| 106 |
+
visitor_data_cache.update(data_entry)
|
| 107 |
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 108 |
+
json.dump(current_data, f, ensure_ascii=False, indent=4)
|
| 109 |
logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
|
| 110 |
upload_data_to_hf_async()
|
| 111 |
except Exception as e:
|
|
|
|
| 190 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
|
| 191 |
<title>Morshen Group</title>
|
| 192 |
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 193 |
+
<script src="https://unpkg.com/@tonconnect/sdk@2.0.0/dist/tonconnect-sdk.min.js"></script>
|
| 194 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 195 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 196 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
| 211 |
--text-secondary-color: var(--tg-theme-hint-color);
|
| 212 |
--accent-gradient: linear-gradient(95deg, var(--tg-theme-button-color, #007aff), #5856d6);
|
| 213 |
--accent-gradient-green: linear-gradient(95deg, #34c759, #30d158);
|
| 214 |
+
--accent-gradient-ton: linear-gradient(95deg, #0098EA, #00baff); /* TON Gradient */
|
| 215 |
--tag-bg: rgba(255, 255, 255, 0.1);
|
| 216 |
--border-radius-s: 8px;
|
| 217 |
--border-radius-m: 14px;
|
|
|
|
| 292 |
background: rgba(44, 44, 46, 0.95);
|
| 293 |
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), var(--shadow-medium);
|
| 294 |
}
|
| 295 |
+
.btn-ton {
|
| 296 |
+
background: var(--accent-gradient-ton);
|
| 297 |
+
color: var(--tg-theme-button-text-color);
|
| 298 |
+
}
|
| 299 |
+
.btn-ton:hover {
|
| 300 |
+
background: linear-gradient(95deg, #00baff, #0098EA);
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
.tag {
|
| 304 |
display: inline-block; background: var(--tag-bg); color: var(--text-secondary-color);
|
| 305 |
padding: 6px 12px; border-radius: var(--border-radius-s); font-size: 0.85em;
|
|
|
|
| 392 |
}
|
| 393 |
.save-card-button i { font-size: 1.2em; }
|
| 394 |
|
| 395 |
+
.ton-wallet-section {
|
| 396 |
+
margin-top: var(--padding-l);
|
| 397 |
+
text-align: center;
|
| 398 |
+
background-color: var(--card-bg);
|
| 399 |
+
border-radius: var(--border-radius-l);
|
| 400 |
+
padding: var(--padding-l);
|
| 401 |
+
box-shadow: var(--shadow-medium);
|
| 402 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 403 |
+
backdrop-filter: blur(var(--backdrop-blur));
|
| 404 |
+
-webkit-backdrop-filter: blur(var(--backdrop-blur));
|
| 405 |
+
}
|
| 406 |
+
.ton-wallet-section h3 {
|
| 407 |
+
font-size: 1.5em;
|
| 408 |
+
font-weight: 600;
|
| 409 |
+
margin-bottom: var(--padding-s);
|
| 410 |
+
color: var(--tg-theme-link-color);
|
| 411 |
+
}
|
| 412 |
+
.ton-wallet-section .status-message {
|
| 413 |
+
font-size: 1em;
|
| 414 |
+
color: var(--text-secondary-color);
|
| 415 |
+
margin-bottom: var(--padding-m);
|
| 416 |
+
}
|
| 417 |
+
.ton-wallet-section .wallet-info {
|
| 418 |
+
margin-top: var(--padding-m);
|
| 419 |
+
padding: var(--padding-m);
|
| 420 |
+
background-color: var(--card-bg-solid);
|
| 421 |
+
border-radius: var(--border-radius-m);
|
| 422 |
+
text-align: left;
|
| 423 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 424 |
+
}
|
| 425 |
+
.ton-wallet-section .wallet-info p {
|
| 426 |
+
margin-bottom: 0.5rem;
|
| 427 |
+
word-break: break-all;
|
| 428 |
+
}
|
| 429 |
+
.ton-wallet-section .wallet-info strong {
|
| 430 |
+
color: var(--tg-theme-text-color);
|
| 431 |
+
font-weight: 600;
|
| 432 |
+
}
|
| 433 |
+
.ton-wallet-section .wallet-info .balance {
|
| 434 |
+
font-size: 1.3em;
|
| 435 |
+
font-weight: 700;
|
| 436 |
+
color: var(--accent-gradient-ton-start, #0098EA); /* Use TON color */
|
| 437 |
+
margin-top: 1rem;
|
| 438 |
+
}
|
| 439 |
+
.ton-wallet-section .wallet-info .balance span {
|
| 440 |
+
font-size: 0.8em;
|
| 441 |
+
font-weight: 500;
|
| 442 |
+
color: var(--text-secondary-color);
|
| 443 |
+
margin-left: 5px;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
/* Modal Styles */
|
| 448 |
.modal {
|
| 449 |
display: none; position: fixed; z-index: 1001;
|
| 450 |
left: 0; top: 0; width: 100%; height: 100%;
|
|
|
|
| 476 |
.modal-text b { color: var(--tg-theme-link-color); font-weight: 600; }
|
| 477 |
.modal-instruction { font-size: 1em; color: var(--text-secondary-color); margin-top: var(--padding-m); }
|
| 478 |
|
| 479 |
+
/* Icons */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
.icon { display: inline-block; width: 1.2em; text-align: center; margin-right: 8px; opacity: 0.9; }
|
| 481 |
.icon-save::before { content: '💾'; }
|
| 482 |
.icon-web::before { content: '🌐'; }
|
|
|
|
| 497 |
.icon-link::before { content: '🔗'; }
|
| 498 |
.icon-leader::before { content: '🏆'; }
|
| 499 |
.icon-company::before { content: '🏢'; }
|
| 500 |
+
.icon-ton::before { content: '💎'; }
|
| 501 |
|
| 502 |
+
|
| 503 |
+
/* Responsive adjustments */
|
| 504 |
@media (max-width: 480px) {
|
| 505 |
.section-title { font-size: 1.8em; }
|
| 506 |
.logo span { font-size: 1.4em; }
|
|
|
|
| 509 |
.stats-grid { grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: var(--padding-s); }
|
| 510 |
.stat-value { font-size: 1.5em; }
|
| 511 |
.modal-content { margin: 25% auto; width: 92%; }
|
| 512 |
+
.ton-wallet-section h3 { font-size: 1.3em; }
|
| 513 |
+
.ton-wallet-section .wallet-info .balance { font-size: 1.1em; }
|
| 514 |
}
|
| 515 |
</style>
|
| 516 |
</head>
|
|
|
|
| 538 |
</a>
|
| 539 |
</section>
|
| 540 |
|
| 541 |
+
<!-- TON Wallet Section -->
|
| 542 |
+
<section class="ton-wallet-section">
|
| 543 |
+
<h3><i class="icon icon-ton"></i>TON Wallet</h3>
|
| 544 |
+
<div id="ton-status" class="status-message">Подключение к TON...</div>
|
| 545 |
+
<button id="connect-ton-btn" class="btn btn-ton" style="display: none; width: 100%;">Подключить TON Кошелек</button>
|
| 546 |
+
<div id="wallet-info" class="wallet-info" style="display: none;">
|
| 547 |
+
<p><strong>Адрес:</strong> <span id="ton-address"></span></p>
|
| 548 |
+
<p><strong>Баланс:</strong> <span id="ton-balance" class="balance"></span></p>
|
| 549 |
+
</div>
|
| 550 |
+
</section>
|
| 551 |
+
<!-- End TON Wallet Section -->
|
| 552 |
+
|
| 553 |
+
|
| 554 |
<section class="ecosystem-header">
|
| 555 |
<h2 class="section-title"><i class="icon icon-company"></i>Экосистема инноваций</h2>
|
| 556 |
<p class="description">
|
|
|
|
| 635 |
</div>
|
| 636 |
</section>
|
| 637 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
<footer class="footer-greeting">
|
| 639 |
<p id="greeting">Инициализация...</p>
|
| 640 |
</footer>
|
|
|
|
| 645 |
<i class="icon icon-save"></i>Сохранить визитку
|
| 646 |
</button>
|
| 647 |
|
| 648 |
+
<!-- The Modal -->
|
| 649 |
<div id="saveModal" class="modal">
|
| 650 |
<div class="modal-content">
|
| 651 |
<span class="modal-close" id="modal-close-btn">×</span>
|
|
|
|
| 656 |
</div>
|
| 657 |
</div>
|
| 658 |
|
| 659 |
+
|
| 660 |
<script>
|
| 661 |
const tg = window.Telegram.WebApp;
|
| 662 |
|
|
|
|
| 681 |
}
|
| 682 |
}
|
| 683 |
|
| 684 |
+
const TON_MANIFEST_URL = window.location.origin + '/.well-known/ton-connect/manifest.json';
|
| 685 |
+
let connector = null;
|
| 686 |
+
|
| 687 |
+
function setupTonConnect() {
|
| 688 |
+
if (!window.TonConnectSDK) {
|
| 689 |
+
console.error("TON Connect SDK not loaded.");
|
| 690 |
+
document.getElementById('ton-status').textContent = 'Ошибка загрузки TON SDK.';
|
| 691 |
+
document.getElementById('connect-ton-btn').style.display = 'none';
|
| 692 |
+
return;
|
| 693 |
+
}
|
| 694 |
+
document.getElementById('ton-status').textContent = 'Инициализация TON Connect...';
|
| 695 |
+
|
| 696 |
+
try {
|
| 697 |
+
connector = new TonConnectSDK.TonConnect({
|
| 698 |
+
manifestUrl: TON_MANIFEST_URL,
|
| 699 |
+
});
|
| 700 |
+
|
| 701 |
+
connector.onStatusChange(wallet => {
|
| 702 |
+
console.log('TON Wallet status changed:', wallet);
|
| 703 |
+
updateWalletUI(wallet);
|
| 704 |
+
if (wallet) {
|
| 705 |
+
// Wallet connected or changed, save address and fetch balance
|
| 706 |
+
saveTonWalletAddress(wallet.account.address);
|
| 707 |
+
fetchTonBalance();
|
| 708 |
+
} else {
|
| 709 |
+
// Wallet disconnected
|
| 710 |
+
document.getElementById('ton-status').textContent = 'TON Кошелек не подключен.';
|
| 711 |
+
}
|
| 712 |
+
});
|
| 713 |
+
|
| 714 |
+
document.getElementById('connect-ton-btn').addEventListener('click', () => {
|
| 715 |
+
if (connector) {
|
| 716 |
+
document.getElementById('ton-status').textContent = 'Подключение... Ожидайте в кошельке.';
|
| 717 |
+
connector.connect();
|
| 718 |
+
if (tg.HapticFeedback) {
|
| 719 |
+
tg.HapticFeedback.impactOccurred('light');
|
| 720 |
+
}
|
| 721 |
+
}
|
| 722 |
+
});
|
| 723 |
+
|
| 724 |
+
// Attempt to restore existing connection
|
| 725 |
+
connector.restoreConnection();
|
| 726 |
+
|
| 727 |
+
} catch (e) {
|
| 728 |
+
console.error("Failed to initialize TON Connect:", e);
|
| 729 |
+
document.getElementById('ton-status').textContent = 'Ошибка инициализации TON Connect.';
|
| 730 |
+
document.getElementById('connect-ton-btn').style.display = 'none';
|
| 731 |
+
}
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
function updateWalletUI(wallet) {
|
| 735 |
+
const connectBtn = document.getElementById('connect-ton-btn');
|
| 736 |
+
const walletInfoDiv = document.getElementById('wallet-info');
|
| 737 |
+
const statusDiv = document.getElementById('ton-status');
|
| 738 |
+
const addressSpan = document.getElementById('ton-address');
|
| 739 |
+
const balanceSpan = document.getElementById('ton-balance');
|
| 740 |
+
|
| 741 |
+
if (wallet) {
|
| 742 |
+
connectBtn.style.display = 'none';
|
| 743 |
+
walletInfoDiv.style.display = 'block';
|
| 744 |
+
statusDiv.textContent = 'TON Кошелек подключен.';
|
| 745 |
+
addressSpan.textContent = TonConnectSDK.toUserFriendlyAddress(wallet.account.address, wallet.account.chain);
|
| 746 |
+
balanceSpan.textContent = 'Загрузка...'; // Placeholder while fetching balance
|
| 747 |
+
} else {
|
| 748 |
+
connectBtn.style.display = 'block';
|
| 749 |
+
walletInfoDiv.style.display = 'none';
|
| 750 |
+
addressSpan.textContent = '';
|
| 751 |
+
balanceSpan.textContent = '';
|
| 752 |
+
statusDiv.textContent = 'TON Кошелек не подключен.';
|
| 753 |
+
}
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
function saveTonWalletAddress(address) {
|
| 757 |
+
if (!tg || !tg.initData) {
|
| 758 |
+
console.error("Telegram WebApp initData is missing for saving wallet.");
|
| 759 |
+
return;
|
| 760 |
+
}
|
| 761 |
+
fetch('/save_ton_wallet', {
|
| 762 |
+
method: 'POST',
|
| 763 |
+
headers: { 'Content-Type': 'application/json' },
|
| 764 |
+
body: JSON.stringify({ initData: tg.initData, tonAddress: address }),
|
| 765 |
+
})
|
| 766 |
+
.then(response => response.json())
|
| 767 |
+
.then(data => {
|
| 768 |
+
if (data.status === 'ok') {
|
| 769 |
+
console.log('TON wallet address saved successfully.');
|
| 770 |
+
} else {
|
| 771 |
+
console.error('Failed to save TON wallet address:', data.message);
|
| 772 |
+
}
|
| 773 |
+
})
|
| 774 |
+
.catch(error => {
|
| 775 |
+
console.error('Error sending TON wallet address to backend:', error);
|
| 776 |
+
});
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
+
function fetchTonBalance() {
|
| 780 |
+
if (!tg || !tg.initData) {
|
| 781 |
+
console.error("Telegram WebApp initData is missing for fetching balance.");
|
| 782 |
+
document.getElementById('ton-balance').textContent = 'Ошибка (нет данных ТГ)';
|
| 783 |
+
return;
|
| 784 |
+
}
|
| 785 |
+
const balanceSpan = document.getElementById('ton-balance');
|
| 786 |
+
balanceSpan.textContent = 'Загрузка...';
|
| 787 |
+
|
| 788 |
+
fetch('/get_ton_balance', {
|
| 789 |
+
method: 'POST',
|
| 790 |
+
headers: { 'Content-Type': 'application/json' },
|
| 791 |
+
body: JSON.stringify({ initData: tg.initData }),
|
| 792 |
+
})
|
| 793 |
+
.then(response => {
|
| 794 |
+
if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
|
| 795 |
+
return response.json();
|
| 796 |
+
})
|
| 797 |
+
.then(data => {
|
| 798 |
+
if (data.status === 'ok') {
|
| 799 |
+
// Balance is in nanoton, convert to TON
|
| 800 |
+
const balanceTon = (data.balance / 1e9).toFixed(4);
|
| 801 |
+
balanceSpan.innerHTML = `${balanceTon} <span>TON</span>`;
|
| 802 |
+
} else {
|
| 803 |
+
balanceSpan.textContent = `Ошибка: ${data.message}`;
|
| 804 |
+
console.error('Failed to fetch TON balance:', data.message);
|
| 805 |
+
}
|
| 806 |
+
})
|
| 807 |
+
.catch(error => {
|
| 808 |
+
balanceSpan.textContent = `Ошибка: ${error.message}`;
|
| 809 |
+
console.error('Error fetching TON balance:', error);
|
| 810 |
+
});
|
| 811 |
+
}
|
| 812 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 813 |
|
| 814 |
function setupTelegram() {
|
| 815 |
if (!tg || !tg.initData) {
|
|
|
|
| 817 |
const greetingElement = document.getElementById('greeting');
|
| 818 |
if(greetingElement) greetingElement.textContent = 'Не удалось связаться с Telegram.';
|
| 819 |
document.body.style.visibility = 'visible';
|
| 820 |
+
setupTonConnect(); // Try setting up TON anyway, it might work in standard browser
|
| 821 |
return;
|
| 822 |
}
|
| 823 |
|
|
|
|
| 842 |
.then(data => {
|
| 843 |
if (data.status === 'ok' && data.verified) {
|
| 844 |
console.log('Backend verification successful.');
|
| 845 |
+
// Verification successful, now setup TON Connect
|
| 846 |
+
setupTonConnect();
|
| 847 |
} else {
|
| 848 |
console.warn('Backend verification failed:', data.message);
|
| 849 |
+
document.getElementById('ton-status').textContent = 'Проверка Telegram данных не пройдена.';
|
| 850 |
+
document.getElementById('connect-ton-btn').style.display = 'none';
|
| 851 |
}
|
| 852 |
})
|
| 853 |
.catch(error => {
|
| 854 |
console.error('Error sending initData for verification:', error);
|
| 855 |
+
document.getElementById('ton-status').textContent = 'Ошибка связи с сервером для проверки.';
|
| 856 |
+
document.getElementById('connect-ton-btn').style.display = 'none';
|
| 857 |
});
|
| 858 |
|
| 859 |
|
|
|
|
| 901 |
console.error("Modal elements not found!");
|
| 902 |
}
|
| 903 |
|
| 904 |
+
document.body.style.visibility = 'visible';
|
|
|
|
|
|
|
| 905 |
}
|
| 906 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 907 |
if (window.Telegram && window.Telegram.WebApp) {
|
| 908 |
setupTelegram();
|
| 909 |
} else {
|
|
|
|
| 915 |
const greetingElement = document.getElementById('greeting');
|
| 916 |
if(greetingElement) greetingElement.textContent = 'Ошибка загрузки интерфейса Telegram.';
|
| 917 |
document.body.style.visibility = 'visible';
|
| 918 |
+
setupTonConnect(); // Still try TON Connect even without Telegram initData fully
|
| 919 |
}
|
| 920 |
}, 3500);
|
| 921 |
}
|
|
|
|
| 991 |
}
|
| 992 |
.user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
|
| 993 |
.user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
|
| 994 |
+
.user-card .details { font-size: 0.9em; color: #495057; word-break: break-word; text-align: left; width: 100%; }
|
|
|
|
| 995 |
.user-card .detail-item { margin-bottom: 0.3rem; }
|
| 996 |
+
.user-card .detail-item strong { color: var(--admin-text); margin-right: 5px;}
|
| 997 |
+
.user-card .detail-item .ton-address-text {
|
| 998 |
+
display: block;
|
| 999 |
+
font-size: 0.8em;
|
| 1000 |
+
color: #6c757d;
|
| 1001 |
+
margin-top: 5px;
|
| 1002 |
+
word-break: break-all;
|
| 1003 |
+
}
|
| 1004 |
+
|
| 1005 |
.user-card .timestamp { font-size: 0.8em; color: var(--admin-secondary); margin-top: 1rem; }
|
| 1006 |
.no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
|
| 1007 |
.alert {
|
|
|
|
| 1024 |
}
|
| 1025 |
.refresh-btn:hover { background-color: #0b5ed7; }
|
| 1026 |
|
| 1027 |
+
/* Admin Controls */
|
| 1028 |
.admin-controls {
|
| 1029 |
background: var(--admin-card-bg);
|
| 1030 |
padding: var(--padding);
|
|
|
|
| 1081 |
<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">
|
| 1082 |
<div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
|
| 1083 |
{% if user.username %}
|
| 1084 |
+
<div class="username"><a href="https://t.me/{{ user.username }}" target="_blank" style="color: inherit; text-decoration: none;">@{{ user.username }}</a></div>
|
| 1085 |
{% else %}
|
| 1086 |
<div class="username" style="height: 1.3em;"></div>
|
| 1087 |
{% endif %}
|
|
|
|
| 1090 |
<div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
|
| 1091 |
<div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
|
| 1092 |
<div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
|
| 1093 |
+
<div class="detail-item">
|
| 1094 |
+
<strong>TON:</strong>
|
| 1095 |
+
{% if user.ton_wallet %}
|
| 1096 |
+
<span class="ton-address-text">{{ user.ton_wallet }}</span>
|
| 1097 |
+
{% else %}
|
| 1098 |
+
Не подключен
|
| 1099 |
+
{% endif %}
|
| 1100 |
+
</div>
|
| 1101 |
</div>
|
| 1102 |
<div class="timestamp">Визит: {{ user.visited_at_str }}</div>
|
| 1103 |
</div>
|
|
|
|
| 1149 |
</html>
|
| 1150 |
"""
|
| 1151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1152 |
@app.route('/')
|
| 1153 |
def index():
|
| 1154 |
theme_params = {}
|
| 1155 |
return render_template_string(TEMPLATE, theme=theme_params)
|
| 1156 |
|
| 1157 |
+
@app.route('/.well-known/ton-connect/manifest.json')
|
| 1158 |
+
def tonconnect_manifest():
|
| 1159 |
+
manifest = {
|
| 1160 |
+
"url": request.url_root.rstrip('/') ,
|
| 1161 |
+
"name": "Morshen Group TMA",
|
| 1162 |
+
"iconUrl": request.url_root.rstrip('/') + "/static/morshengroup_icon.png", # Ensure you have a static folder and this icon
|
| 1163 |
+
"termsOfUseUrl": "", # Optional
|
| 1164 |
+
"privacyPolicyUrl": "" # Optional
|
| 1165 |
+
}
|
| 1166 |
+
return jsonify(manifest)
|
| 1167 |
+
|
| 1168 |
@app.route('/verify', methods=['POST'])
|
| 1169 |
def verify_data():
|
| 1170 |
try:
|
|
|
|
| 1183 |
user_info_dict = json.loads(user_json_str)
|
| 1184 |
user_id = user_info_dict.get('id')
|
| 1185 |
except Exception as e:
|
| 1186 |
+
logging.error(f"Could not parse user JSON from initData: {e}")
|
| 1187 |
user_info_dict = {}
|
| 1188 |
|
| 1189 |
if is_valid and user_id:
|
| 1190 |
now = time.time()
|
| 1191 |
+
user_entry = {
|
| 1192 |
+
str(user_id): {
|
| 1193 |
+
'id': user_id,
|
| 1194 |
+
'first_name': user_info_dict.get('first_name'),
|
| 1195 |
+
'last_name': user_info_dict.get('last_name'),
|
| 1196 |
+
'username': user_info_dict.get('username'),
|
| 1197 |
+
'photo_url': user_info_dict.get('photo_url'),
|
| 1198 |
+
'language_code': user_info_dict.get('language_code'),
|
| 1199 |
+
'is_premium': user_info_dict.get('is_premium', False),
|
| 1200 |
+
'phone_number': user_info_dict.get('phone_number'),
|
| 1201 |
+
'visited_at': now,
|
| 1202 |
+
'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
|
| 1203 |
+
}
|
| 1204 |
+
}
|
| 1205 |
+
save_visitor_data(user_entry)
|
| 1206 |
+
return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
|
| 1207 |
+
else:
|
| 1208 |
+
logging.warning(f"Verification failed or user_id missing for initData.")
|
| 1209 |
+
return jsonify({"status": "error", "verified": False, "message": "Invalid data or missing user ID"}), 403
|
| 1210 |
+
|
| 1211 |
+
except Exception as e:
|
| 1212 |
+
logging.exception("Error in /verify endpoint")
|
| 1213 |
+
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
| 1214 |
+
|
| 1215 |
+
@app.route('/save_ton_wallet', methods=['POST'])
|
| 1216 |
+
def save_ton_wallet():
|
| 1217 |
+
try:
|
| 1218 |
+
req_data = request.get_json()
|
| 1219 |
+
init_data_str = req_data.get('initData')
|
| 1220 |
+
ton_address = req_data.get('tonAddress')
|
| 1221 |
+
|
| 1222 |
+
if not init_data_str or not ton_address:
|
| 1223 |
+
return jsonify({"status": "error", "message": "Missing initData or tonAddress"}), 400
|
| 1224 |
+
|
| 1225 |
+
user_data_parsed, is_valid = verify_telegram_data(init_data_str)
|
| 1226 |
+
|
| 1227 |
+
user_id = None
|
| 1228 |
+
if user_data_parsed and 'user' in user_data_parsed:
|
| 1229 |
+
try:
|
| 1230 |
+
user_json_str = unquote(user_data_parsed['user'][0])
|
| 1231 |
+
user_info_dict = json.loads(user_json_str)
|
| 1232 |
+
user_id = user_info_dict.get('id')
|
| 1233 |
+
except Exception as e:
|
| 1234 |
+
logging.error(f"Could not parse user JSON from initData during save_ton_wallet: {e}")
|
| 1235 |
+
|
| 1236 |
+
|
| 1237 |
+
if is_valid and user_id:
|
| 1238 |
+
user_id_str = str(user_id)
|
| 1239 |
current_data = load_visitor_data()
|
| 1240 |
+
if user_id_str not in current_data:
|
| 1241 |
+
current_data[user_id_str] = {'id': user_id} # Ensure user entry exists
|
| 1242 |
+
|
| 1243 |
+
# Update only the ton_wallet field
|
| 1244 |
+
user_entry_update = {
|
| 1245 |
+
user_id_str: {
|
| 1246 |
+
'ton_wallet': ton_address
|
| 1247 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1248 |
}
|
| 1249 |
+
save_visitor_data(user_entry_update)
|
| 1250 |
|
| 1251 |
+
return jsonify({"status": "ok", "message": "TON wallet address saved."}), 200
|
| 1252 |
+
else:
|
| 1253 |
+
logging.warning(f"Invalid initData or missing user ID during save_ton_wallet.")
|
| 1254 |
+
return jsonify({"status": "error", "message": "Invalid Telegram data or missing user ID"}), 403
|
| 1255 |
|
| 1256 |
+
except Exception as e:
|
| 1257 |
+
logging.exception("Error in /save_ton_wallet endpoint")
|
| 1258 |
+
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
| 1259 |
|
| 1260 |
+
|
| 1261 |
+
@app.route('/get_ton_balance', methods=['POST'])
|
| 1262 |
+
def get_ton_balance():
|
| 1263 |
+
if not tonapi:
|
| 1264 |
+
return jsonify({"status": "error", "message": "TONAPI is not initialized (check TONAPI_KEY)."}), 503
|
| 1265 |
+
|
| 1266 |
+
try:
|
| 1267 |
+
req_data = request.get_json()
|
| 1268 |
+
init_data_str = req_data.get('initData')
|
| 1269 |
+
|
| 1270 |
+
if not init_data_str:
|
| 1271 |
+
return jsonify({"status": "error", "message": "Missing initData"}), 400
|
| 1272 |
+
|
| 1273 |
+
user_data_parsed, is_valid = verify_telegram_data(init_data_str)
|
| 1274 |
+
|
| 1275 |
+
user_id = None
|
| 1276 |
+
if user_data_parsed and 'user' in user_data_parsed:
|
| 1277 |
+
try:
|
| 1278 |
+
user_json_str = unquote(user_data_parsed['user'][0])
|
| 1279 |
+
user_info_dict = json.loads(user_json_str)
|
| 1280 |
+
user_id = user_info_dict.get('id')
|
| 1281 |
+
except Exception as e:
|
| 1282 |
+
logging.error(f"Could not parse user JSON from initData during get_ton_balance: {e}")
|
| 1283 |
+
|
| 1284 |
+
if is_valid and user_id:
|
| 1285 |
+
user_id_str = str(user_id)
|
| 1286 |
+
current_data = load_visitor_data()
|
| 1287 |
+
user_data = current_data.get(user_id_str)
|
| 1288 |
+
|
| 1289 |
+
if user_data and 'ton_wallet' in user_data and user_data['ton_wallet']:
|
| 1290 |
+
ton_address = user_data['ton_wallet']
|
| 1291 |
+
try:
|
| 1292 |
+
account_info = tonapi.account.get_wallet_v4(account_id=ton_address)
|
| 1293 |
+
balance_nanoton = account_info.balance
|
| 1294 |
+
return jsonify({"status": "ok", "balance": balance_nanoton, "address": ton_address}), 200
|
| 1295 |
+
except Exception as e:
|
| 1296 |
+
logging.error(f"Error fetching TON balance for {ton_address}: {e}")
|
| 1297 |
+
return jsonify({"status": "error", "message": f"Failed to fetch balance: {e}"}), 500
|
| 1298 |
+
else:
|
| 1299 |
+
return jsonify({"status": "error", "message": "TON wallet not linked for this user."}), 404
|
| 1300 |
else:
|
| 1301 |
+
logging.warning(f"Invalid initData or missing user ID during get_ton_balance.")
|
| 1302 |
+
return jsonify({"status": "error", "message": "Invalid Telegram data or missing user ID"}), 403
|
| 1303 |
|
| 1304 |
except Exception as e:
|
| 1305 |
+
logging.exception("Error in /get_ton_balance endpoint")
|
| 1306 |
return jsonify({"status": "error", "message": "Internal server error"}), 500
|
| 1307 |
|
| 1308 |
+
|
| 1309 |
@app.route('/admin')
|
| 1310 |
def admin_panel():
|
| 1311 |
current_data = load_visitor_data()
|
|
|
|
| 1327 |
upload_data_to_hf_async()
|
| 1328 |
return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
|
| 1329 |
|
| 1330 |
+
@app.route('/static/<filename>')
|
| 1331 |
+
def static_files(filename):
|
| 1332 |
+
if filename == 'morshengroup_icon.png':
|
| 1333 |
+
# Basic handling for the manifest icon
|
| 1334 |
+
try:
|
| 1335 |
+
return app.send_static_file(filename)
|
| 1336 |
+
except FileNotFoundError:
|
| 1337 |
+
return "Icon not found", 404
|
| 1338 |
+
return "Not found", 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1339 |
|
| 1340 |
|
| 1341 |
if __name__ == '__main__':
|
|
|
|
| 1347 |
print(f"Visitor data file: {DATA_FILE}")
|
| 1348 |
print(f"Hugging Face Repo: {REPO_ID}")
|
| 1349 |
print(f"HF Data Path: {HF_DATA_FILE_PATH}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1350 |
if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
|
| 1351 |
print("---")
|
| 1352 |
print("--- WARNING: HUGGING FACE TOKEN(S) NOT SET ---")
|
|
|
|
| 1357 |
print("--- Attempting initial data download from Hugging Face...")
|
| 1358 |
download_data_from_hf()
|
| 1359 |
|
| 1360 |
+
if not TONAPI_KEY:
|
| 1361 |
+
print("---")
|
| 1362 |
+
print("--- WARNING: TONAPI_KEY NOT SET ---")
|
| 1363 |
+
print("--- TON wallet functionality will be disabled. Set TONAPI_KEY environment variable.")
|
| 1364 |
+
print("---")
|
| 1365 |
+
else:
|
| 1366 |
+
print("--- TONAPI_KEY found. TON wallet functionality enabled.")
|
| 1367 |
+
|
| 1368 |
+
|
| 1369 |
load_visitor_data()
|
| 1370 |
|
| 1371 |
print("---")
|
|
|
|
| 1381 |
else:
|
| 1382 |
print("--- Periodic backup disabled (HF_TOKEN_WRITE missing).")
|
| 1383 |
|
| 1384 |
+
# Create a static directory if it doesn't exist and add a dummy icon
|
| 1385 |
+
if not os.path.exists('static'):
|
| 1386 |
+
os.makedirs('static')
|
| 1387 |
+
dummy_icon_path = os.path.join('static', 'morshengroup_icon.png')
|
| 1388 |
+
if not os.path.exists(dummy_icon_path):
|
| 1389 |
+
try:
|
| 1390 |
+
# Create a minimal dummy PNG (1x1 transparent)
|
| 1391 |
+
import base64
|
| 1392 |
+
dummy_png = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\xfc\xff\xff?\x03\x00\x08\xfb\x02\xfe\xa7\xcd\x8c\x00\x00\x00\x00IEND\xaeB`\x82'
|
| 1393 |
+
with open(dummy_icon_path, 'wb') as f:
|
| 1394 |
+
f.write(dummy_png)
|
| 1395 |
+
print(f"Created dummy icon at {dummy_icon_path} for TON Connect manifest.")
|
| 1396 |
+
except Exception as e:
|
| 1397 |
+
print(f"Could not create dummy icon: {e}. TON Connect manifest icon might fail.")
|
| 1398 |
+
|
| 1399 |
+
|
| 1400 |
print("--- Server Ready ---")
|
| 1401 |
+
|
| 1402 |
app.run(host=HOST, port=PORT, debug=False)
|