diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -8,12 +8,13 @@ import json from urllib.parse import unquote, parse_qs, quote import time from datetime import datetime +from zoneinfo import ZoneInfo import logging import threading import random from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError -import pytz +import uuid BOT_TOKEN = os.getenv("BOT_TOKEN", "7835463659:AAGNePbelZIAOeaglyQi1qulOqnjs4BGQn4") HOST = '0.0.0.0' @@ -25,41 +26,30 @@ HF_DATA_FILE_PATH = "data.json" HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") -BISHKEK_TZ = pytz.timezone('Asia/Bishkek') - app = Flask(__name__) logging.basicConfig(level=logging.INFO) app.secret_key = os.urandom(24) _data_lock = threading.Lock() visitor_data_cache = {} +_organization_info_cache = {} + +BISHKEK_TZ = ZoneInfo("Asia/Bishkek") -DEFAULT_ORGANIZATION_INFO = { - "name": "Название вашей организации", - "phones": ["+996000123456"], - "address": "г. Бишкек, ул. Примерная, д.1", - "links": [{"label": "Наш сайт", "url": "https://example.com"}] -} +def get_now_bishkek(): + return datetime.now(BISHKEK_TZ) -def generate_unique_id(all_data_keys): +def generate_unique_id(all_data): while True: new_id = str(random.randint(10000, 99999)) - if new_id not in all_data_keys: + if new_id not in all_data and new_id != '__ORG_INFO__': return new_id -def generate_invoice_id(): - return f"INV-{int(time.time() * 1000)}-{random.randint(100, 999)}" - -def get_current_time_bishkek(): - return datetime.now(BISHKEK_TZ) - def download_data_from_hf(): - global visitor_data_cache + global visitor_data_cache, _organization_info_cache if not HF_TOKEN_READ: - logging.warning("HF_TOKEN_READ not set. Skipping Hugging Face download.") return False try: - logging.info(f"Attempting to download {HF_DATA_FILE_PATH} from {REPO_ID}...") hf_hub_download( repo_id=REPO_ID, filename=HF_DATA_FILE_PATH, @@ -70,61 +60,83 @@ def download_data_from_hf(): force_download=True, etag_timeout=10 ) - logging.info("Data file successfully downloaded from Hugging Face.") with _data_lock: try: with open(DATA_FILE, 'r', encoding='utf-8') as f: - visitor_data_cache = json.load(f) - logging.info("Successfully loaded downloaded data into cache.") - except (FileNotFoundError, json.JSONDecodeError) as e: - logging.error(f"Error reading downloaded data file: {e}. Starting with empty cache.") + full_data = json.load(f) + _organization_info_cache = full_data.pop('__ORG_INFO__', {}) + visitor_data_cache = full_data + except (FileNotFoundError, json.JSONDecodeError): visitor_data_cache = {} + _organization_info_cache = {} return True except RepositoryNotFoundError: - logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download data.") - except Exception as e: - logging.error(f"Error downloading data from Hugging Face: {e}") + pass + except Exception: + pass return False -def load_data(): - global visitor_data_cache +def load_visitor_data(): + global visitor_data_cache, _organization_info_cache with _data_lock: - if not visitor_data_cache: + if not visitor_data_cache and not _organization_info_cache: try: with open(DATA_FILE, 'r', encoding='utf-8') as f: - visitor_data_cache = json.load(f) - logging.info("Data loaded from local JSON.") + full_data = json.load(f) + _organization_info_cache = full_data.pop('__ORG_INFO__', {}) + visitor_data_cache = full_data except FileNotFoundError: - logging.warning(f"{DATA_FILE} not found locally. Initializing with default structure.") - visitor_data_cache = {"organization_info": DEFAULT_ORGANIZATION_INFO} + visitor_data_cache = {} + _organization_info_cache = {} except json.JSONDecodeError: - logging.error(f"Error decoding {DATA_FILE}. Initializing with default structure.") - visitor_data_cache = {"organization_info": DEFAULT_ORGANIZATION_INFO} - except Exception as e: - logging.error(f"Unexpected error loading data: {e}") - visitor_data_cache = {"organization_info": DEFAULT_ORGANIZATION_INFO} - - if 'organization_info' not in visitor_data_cache: - visitor_data_cache['organization_info'] = DEFAULT_ORGANIZATION_INFO - + visitor_data_cache = {} + _organization_info_cache = {} + except Exception: + visitor_data_cache = {} + _organization_info_cache = {} return visitor_data_cache -def save_data_sync(): +def get_organization_info(): + if not _organization_info_cache: + return { + "organization_name": "Название организации", + "phone_numbers": [], + "address": "Адрес организации", + "whatsapp_link": "", + "telegram_link": "" + } + return _organization_info_cache + +def save_visitor_data(data): with _data_lock: try: + visitor_data_cache.update(data) + data_to_save = visitor_data_cache.copy() + if _organization_info_cache: + data_to_save['__ORG_INFO__'] = _organization_info_cache with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4) - logging.info(f"Data successfully saved to {DATA_FILE}.") + json.dump(data_to_save, f, ensure_ascii=False, indent=4) upload_data_to_hf_async() - except Exception as e: - logging.error(f"Error saving data: {e}") + except Exception: + pass + +def save_organization_info(org_info): + global _organization_info_cache + with _data_lock: + try: + _organization_info_cache.update(org_info) + data_to_save = visitor_data_cache.copy() + data_to_save['__ORG_INFO__'] = _organization_info_cache + with open(DATA_FILE, 'w', encoding='utf-8') as f: + json.dump(data_to_save, f, ensure_ascii=False, indent=4) + upload_data_to_hf_async() + except Exception: + pass def upload_data_to_hf(): if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_WRITE not set. Skipping Hugging Face upload.") return if not os.path.exists(DATA_FILE): - logging.warning(f"{DATA_FILE} does not exist. Skipping upload.") return try: @@ -132,21 +144,18 @@ def upload_data_to_hf(): with _data_lock: file_content_exists = os.path.getsize(DATA_FILE) > 0 if not file_content_exists: - logging.warning(f"{DATA_FILE} is empty. Skipping upload.") return - logging.info(f"Attempting to upload {DATA_FILE} to {REPO_ID}/{HF_DATA_FILE_PATH}...") api.upload_file( path_or_fileobj=DATA_FILE, path_in_repo=HF_DATA_FILE_PATH, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"Update data {get_current_time_bishkek().strftime('%Y-%m-%d %H:%M:%S')}" + commit_message=f"Update bonus data {get_now_bishkek().strftime('%Y-%m-%d %H:%M:%S')}" ) - logging.info("Data successfully uploaded to Hugging Face.") - except Exception as e: - logging.error(f"Error uploading data to Hugging Face: {e}") + except Exception: + pass def upload_data_to_hf_async(): upload_thread = threading.Thread(target=upload_data_to_hf, daemon=True) @@ -154,11 +163,9 @@ def upload_data_to_hf_async(): def periodic_backup(): if not HF_TOKEN_WRITE: - logging.info("Periodic backup disabled: HF_TOKEN_WRITE not set.") return while True: time.sleep(3600) - logging.info("Initiating periodic backup...") upload_data_to_hf() def verify_telegram_data(init_data_str): @@ -180,14 +187,12 @@ def verify_telegram_data(init_data_str): if calculated_hash == received_hash: auth_date = int(parsed_data.get('auth_date', [0])[0]) current_time = int(time.time()) - if current_time - auth_date > 86400: # 24 hours - logging.warning(f"Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}).") + if current_time - auth_date > 86400: + pass return parsed_data, True else: - logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}") return parsed_data, False - except Exception as e: - logging.error(f"Error verifying Telegram data: {e}") + except Exception: return None, False TEMPLATE = """ @@ -217,6 +222,9 @@ TEMPLATE = """ --shadow-glow: 0 0 35px var(--shadow-color); --shadow-color-red: rgba(244, 67, 54, 0.15); --shadow-glow-red: 0 0 35px var(--shadow-color-red); + --btn-bg: #333; + --btn-text: #fff; + --btn-hover: #555; } * { box-sizing: border-box; margin: 0; padding: 0; } html, body { @@ -230,60 +238,231 @@ TEMPLATE = """ visibility: hidden; min-height: 100vh; } - .container { max-width: 600px; margin: 0 auto; display: flex; flex-direction: column; gap: var(--padding-m); } - .header { text-align: left; padding: var(--padding-m) 0; } - .logo { font-size: 2.5em; font-weight: 800; color: var(--text-color); letter-spacing: -1px; } + .container { + max-width: 600px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: var(--padding-m); + } + .header { + text-align: left; + padding: var(--padding-m) 0; + } + .logo { + font-size: 2.5em; + font-weight: 800; + color: var(--text-color); + letter-spacing: -1px; + } .logo span { color: var(--brand-yellow); } - .welcome-text { font-size: 1em; color: var(--text-secondary-color); margin-top: 4px; } - .card-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--padding-m); } - .bonus-card, .debt-card { background: linear-gradient(145deg, #2a2a2a, #1c1c1c); border-radius: calc(var(--border-radius) + 8px); padding: var(--padding-l); text-align: center; position: relative; overflow: hidden; } - .bonus-card { box-shadow: var(--shadow-glow); border: 1px solid rgba(255, 193, 7, 0.2); } - .debt-card { box-shadow: var(--shadow-glow-red); border: 1px solid rgba(244, 67, 54, 0.2); } - .card-label { font-size: 1.1em; font-weight: 500; color: var(--text-secondary-color); margin-bottom: 12px; } - .bonus-amount, .debt-amount { font-size: 3em; font-weight: 800; letter-spacing: -2px; line-height: 1; } - .bonus-amount { color: var(--brand-yellow); } - .debt-amount { color: var(--brand-red); } - .client-id-card { background-color: var(--card-bg); border-radius: var(--border-radius); padding: var(--padding-m); display: flex; justify-content: space-between; align-items: center; } - .client-id-label { font-weight: 500; color: var(--text-secondary-color); } - .client-id-value { font-size: 1.3em; font-weight: 700; color: var(--brand-yellow); letter-spacing: 2px; background-color: rgba(255,193,7,0.1); padding: 4px 10px; border-radius: 8px; } - - .navigation-tabs { display: flex; gap: 8px; margin-bottom: var(--padding-m); } - .navigation-tabs button { - flex-grow: 1; padding: 12px 10px; font-size: 0.9em; font-weight: 600; color: var(--text-secondary-color); - background-color: var(--card-bg); border: 1px solid rgba(255,255,255,0.1); border-radius: 10px; cursor: pointer; transition: all 0.2s ease; - } - .navigation-tabs button.active { color: var(--brand-black); background-color: var(--brand-yellow); border-color: var(--brand-yellow); } - .content-pane { background-color: var(--card-bg); border-radius: var(--border-radius); padding: var(--padding-l); display: none; } - .content-pane.active { display: block; } - .pane-title { font-size: 1.4em; font-weight: 700; margin-bottom: var(--padding-m); padding-bottom: var(--padding-m); border-bottom: 1px solid rgba(255, 255, 255, 0.1); } - .list { list-style: none; padding: 0; margin: 0; max-height: 40vh; overflow-y: auto; } - .list-item { display: flex; justify-content: space-between; align-items: center; padding: 14px 4px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); } - .list-item:last-child { border-bottom: none; } - .list-item-details { display: flex; flex-direction: column; } - .list-item-description { font-size: 1em; font-weight: 500; } - .list-item-subtext { font-size: 0.8em; color: var(--text-secondary-color); margin-top: 4px; } - .list-item-amount { font-size: 1.1em; font-weight: 700; } - .amount-accrual { color: #4CAF50; } - .amount-deduction { color: #F44336; } - .no-items { text-align: center; color: var(--text-secondary-color); padding: 2rem 0; } - - .biz-card-item { margin-bottom: 12px; } - .biz-card-label { font-size: 0.9em; color: var(--text-secondary-color); margin-bottom: 4px; } - .biz-card-value { font-size: 1.1em; font-weight: 500; } - .biz-card-phone-actions a { margin-right: 10px; color: var(--brand-yellow); text-decoration: none; font-size: 0.9em;} - .biz-card-link a { color: var(--brand-yellow); text-decoration: none; } - - .invoice-item-details { cursor: pointer; } - .invoice-details-modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.7); backdrop-filter: blur(5px); } - .invoice-modal-content { background-color: var(--card-bg); margin: 15% auto; padding: var(--padding-l); border-radius: var(--border-radius); width: 90%; max-width: 500px; box-shadow: 0 5px 25px rgba(0,0,0,0.3); } - .invoice-modal-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: var(--padding-m); margin-bottom: var(--padding-m); } - .invoice-modal-title { font-size: 1.3em; font-weight: 700; } - .invoice-modal-close { font-size: 1.8em; font-weight: bold; cursor: pointer; color: var(--text-secondary-color); } - .invoice-modal-body table { width: 100%; border-collapse: collapse; } - .invoice-modal-body th, .invoice-modal-body td { text-align: left; padding: 8px 4px; border-bottom: 1px solid rgba(255,255,255,0.05); font-size: 0.95em; } - .invoice-modal-body th { font-weight: 600; color: var(--text-secondary-color); } - .invoice-modal-body .item-qty, .invoice-modal-body .item-price, .invoice-modal-body .item-total { text-align: right; } - .invoice-grand-total { text-align: right; font-size: 1.1em; font-weight: 700; margin-top: var(--padding-m); padding-top: var(--padding-m); border-top: 1px solid rgba(255,255,255,0.1); } + .welcome-text { + font-size: 1em; + color: var(--text-secondary-color); + margin-top: 4px; + } + .card-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--padding-m); + } + .bonus-card, .debt-card { + background: linear-gradient(145deg, #2a2a2a, #1c1c1c); + border-radius: calc(var(--border-radius) + 8px); + padding: var(--padding-l); + text-align: center; + position: relative; + overflow: hidden; + } + .bonus-card { + box-shadow: var(--shadow-glow); + border: 1px solid rgba(255, 193, 7, 0.2); + } + .debt-card { + box-shadow: var(--shadow-glow-red); + border: 1px solid rgba(244, 67, 54, 0.2); + } + .card-label { + font-size: 1.1em; + font-weight: 500; + color: var(--text-secondary-color); + margin-bottom: 12px; + } + .bonus-amount { + font-size: 3em; + font-weight: 800; + color: var(--brand-yellow); + letter-spacing: -2px; + line-height: 1; + } + .debt-amount { + font-size: 3em; + font-weight: 800; + color: var(--brand-red); + letter-spacing: -2px; + line-height: 1; + } + .client-id-card { + background-color: var(--card-bg); + border-radius: var(--border-radius); + padding: var(--padding-m); + display: flex; + justify-content: space-between; + align-items: center; + } + .client-id-label { + font-weight: 500; + color: var(--text-secondary-color); + } + .client-id-value { + font-size: 1.3em; + font-weight: 700; + color: var(--brand-yellow); + letter-spacing: 2px; + background-color: rgba(255,193,7,0.1); + padding: 4px 10px; + border-radius: 8px; + } + .content-section { + background-color: var(--card-bg); + border-radius: var(--border-radius); + padding: var(--padding-l); + display: none; + } + .section-title { + font-size: 1.4em; + font-weight: 700; + margin-bottom: var(--padding-m); + padding-bottom: var(--padding-m); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + } + .history-list, .invoice-list { + list-style: none; + padding: 0; + margin: 0; + max-height: 35vh; + overflow-y: auto; + } + .history-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 4px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + } + .history-item:last-child { border-bottom: none; } + .history-details { display: flex; flex-direction: column; } + .history-description { font-size: 1em; font-weight: 500; } + .history-date { font-size: 0.8em; color: var(--text-secondary-color); margin-top: 4px; } + .history-amount { font-size: 1.1em; font-weight: 700; } + .history-amount.accrual { color: #4CAF50; } + .history-amount.deduction { color: #F44336; } + .no-history { + text-align: center; + color: var(--text-secondary-color); + padding: 2rem 0; + } + .nav-buttons { + display: flex; + gap: 10px; + margin-bottom: var(--padding-m); + } + .nav-buttons .btn { + flex: 1; + padding: 12px 15px; + background-color: var(--btn-bg); + color: var(--btn-text); + border: none; + border-radius: 8px; + font-size: 1em; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s ease; + } + .nav-buttons .btn:hover { + background-color: var(--btn-hover); + } + .nav-buttons .btn.active { + background-color: var(--brand-yellow); + color: var(--brand-black); + } + .biz-card-section .org-info-card { + background-color: var(--card-bg); + border-radius: var(--border-radius); + padding: var(--padding-m); + } + .org-info-card p { + margin-bottom: 10px; + line-height: 1.4; + } + .org-info-card strong { + color: var(--brand-yellow); + } + .org-info-card ul { + list-style: none; + padding-left: 20px; + margin-bottom: 10px; + } + .org-info-card ul li { + margin-bottom: 5px; + } + .org-info-card a { + color: var(--brand-yellow); + text-decoration: none; + word-break: break-all; + } + .org-info-card a:hover { + text-decoration: underline; + } + .social-link { + display: inline-block; + margin-top: 5px; + padding: 8px 12px; + border-radius: 8px; + font-weight: 500; + } + .social-link.whatsapp { + background-color: #25D366; + color: white; + } + .social-link.telegram { + background-color: #0088CC; + color: white; + } + .invoice-item { + display: flex; + flex-direction: column; + padding: 14px 4px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + cursor: pointer; + } + .invoice-item:last-child { border-bottom: none; } + .invoice-summary { + display: flex; + justify-content: space-between; + font-weight: 600; + } + .invoice-date { + font-size: 1em; + color: var(--text-secondary-color); + } + .invoice-total { + font-size: 1.1em; + color: var(--brand-yellow); + } + .invoice-details { + margin-top: 10px; + padding-left: 10px; + font-size: 0.9em; + color: var(--text-secondary-color); + } + .invoice-details ul { + list-style: disc; + padding-left: 20px; + } + .invoice-details li { + margin-bottom: 5px; + } @@ -309,28 +488,28 @@ TEMPLATE = """

{{ user.id }}

-