diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -7,13 +7,12 @@ import hashlib import json from urllib.parse import unquote, parse_qs, quote import time -from datetime import datetime +from datetime import datetime, timezone, timedelta import logging import threading import random from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError -import pytz # Added for timezone support BOT_TOKEN = os.getenv("BOT_TOKEN", "7835463659:AAGNePbelZIAOeaglyQi1qulOqnjs4BGQn4") HOST = '0.0.0.0' @@ -30,26 +29,23 @@ logging.basicConfig(level=logging.INFO) app.secret_key = os.urandom(24) _data_lock = threading.Lock() -visitor_data_cache = {} # Will hold {"users": {}, "organization_info": {}} +visitor_data_cache = {} -BISHKEK_TZ = pytz.timezone('Asia/Bishkek') +BISHKEK_TZ = timezone(timedelta(hours=6)) -DEFAULT_ORGANIZATION_INFO = { +DEFAULT_ORGANIZATION_DETAILS = { "name": "Название вашей организации", - "phone_numbers": ["+996 (XXX) XX-XX-XX"], - "address": "г. Бишкек, ул. Примерная, д. 1", - "links": [{"label": "Веб-сайт", "url": "https://example.com"}] + "phones": [], + "address": "Ваш адрес", + "links": [] } -def generate_unique_id(users_data): +def generate_unique_id(existing_ids_dict): while True: new_id = str(random.randint(10000, 99999)) - if new_id not in users_data: + if new_id not in existing_ids_dict: return new_id -def generate_invoice_id(): - return f"INV-{int(time.time() * 1000)}-{random.randint(100,999)}" - def download_data_from_hf(): global visitor_data_cache if not HF_TOKEN_READ: @@ -72,18 +68,13 @@ def download_data_from_hf(): try: with open(DATA_FILE, 'r', encoding='utf-8') as f: loaded_data = json.load(f) - if not isinstance(loaded_data, dict): # Basic check for structure - raise json.JSONDecodeError("Root is not a dictionary", "", 0) visitor_data_cache = loaded_data + if '_organization_details' not in visitor_data_cache: + visitor_data_cache['_organization_details'] = DEFAULT_ORGANIZATION_DETAILS.copy() logging.info("Successfully loaded downloaded data into cache.") except (FileNotFoundError, json.JSONDecodeError) as e: - logging.error(f"Error reading downloaded data file: {e}. Initializing with default structure.") - visitor_data_cache = {"users": {}, "organization_info": DEFAULT_ORGANIZATION_INFO.copy()} - # Ensure essential keys exist - if 'users' not in visitor_data_cache: - visitor_data_cache['users'] = {} - if 'organization_info' not in visitor_data_cache: - visitor_data_cache['organization_info'] = DEFAULT_ORGANIZATION_INFO.copy() + logging.error(f"Error reading downloaded data file: {e}. Initializing cache.") + visitor_data_cache = {'_organization_details': DEFAULT_ORGANIZATION_DETAILS.copy()} return True except RepositoryNotFoundError: logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download data.") @@ -94,41 +85,36 @@ def download_data_from_hf(): def load_visitor_data(): global visitor_data_cache with _data_lock: - if not visitor_data_cache or 'users' not in visitor_data_cache: # Ensure it's loaded and has basic structure + if not visitor_data_cache: try: with open(DATA_FILE, 'r', encoding='utf-8') as f: - loaded_data = json.load(f) - if not isinstance(loaded_data, dict): - raise json.JSONDecodeError("Root is not a dictionary", "", 0) - visitor_data_cache = loaded_data + visitor_data_cache = json.load(f) + if '_organization_details' not in visitor_data_cache: + visitor_data_cache['_organization_details'] = DEFAULT_ORGANIZATION_DETAILS.copy() logging.info("Visitor data loaded from local JSON.") except FileNotFoundError: - logging.warning(f"{DATA_FILE} not found locally. Initializing with default structure.") - visitor_data_cache = {"users": {}, "organization_info": DEFAULT_ORGANIZATION_INFO.copy()} + logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.") + visitor_data_cache = {'_organization_details': DEFAULT_ORGANIZATION_DETAILS.copy()} except json.JSONDecodeError: - logging.error(f"Error decoding {DATA_FILE}. Initializing with default structure.") - visitor_data_cache = {"users": {}, "organization_info": DEFAULT_ORGANIZATION_INFO.copy()} + logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.") + visitor_data_cache = {'_organization_details': DEFAULT_ORGANIZATION_DETAILS.copy()} except Exception as e: logging.error(f"Unexpected error loading visitor data: {e}") - visitor_data_cache = {"users": {}, "organization_info": DEFAULT_ORGANIZATION_INFO.copy()} - - # Ensure essential keys exist after loading or initializing - if 'users' not in visitor_data_cache: - visitor_data_cache['users'] = {} - if 'organization_info' not in visitor_data_cache: - visitor_data_cache['organization_info'] = DEFAULT_ORGANIZATION_INFO.copy() - + visitor_data_cache = {'_organization_details': DEFAULT_ORGANIZATION_DETAILS.copy()} return visitor_data_cache -def save_current_data(): +def save_data_locally_and_upload_async(data_to_update=None): with _data_lock: + if data_to_update: + for key, value in data_to_update.items(): + visitor_data_cache[key] = value try: 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}.") upload_data_to_hf_async() except Exception as e: - logging.error(f"Error saving current data: {e}") + logging.error(f"Error saving data: {e}") def upload_data_to_hf(): if not HF_TOKEN_WRITE: @@ -140,26 +126,21 @@ def upload_data_to_hf(): try: api = HfApi() - # No need for separate lock here if called from save_current_data which holds the lock, - # but if called directly (e.g. periodic backup), it's fine. - # However, to be safe and consistent, let's assume it should always grab the lock if it might write. - # For an async upload, it's better if it reads the file path rather than shared memory. - # The current design copies the file, so it's okay. - - 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 bonus data {datetime.now(BISHKEK_TZ).strftime('%Y-%m-%d %H:%M:%S %Z')}" - ) + 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 bonus data {datetime.now(BISHKEK_TZ).strftime('%Y-%m-%d %H:%M:%S')}" + ) logging.info("Bonus data successfully uploaded to Hugging Face.") except Exception as e: logging.error(f"Error uploading data to Hugging Face: {e}") @@ -175,9 +156,7 @@ def periodic_backup(): while True: time.sleep(3600) logging.info("Initiating periodic backup...") - # This will save the current state from memory to file, then upload - save_current_data() - + upload_data_to_hf() def verify_telegram_data(init_data_str): try: @@ -196,12 +175,10 @@ def verify_telegram_data(init_data_str): calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest() if calculated_hash == received_hash: - auth_date_ts = int(parsed_data.get('auth_date', [0])[0]) - current_time_ts = int(time.time()) - if current_time_ts - auth_date_ts > 86400: # 24 hours - auth_date_str = datetime.fromtimestamp(auth_date_ts, BISHKEK_TZ).strftime('%Y-%m-%d %H:%M:%S %Z') - current_time_str = datetime.fromtimestamp(current_time_ts, BISHKEK_TZ).strftime('%Y-%m-%d %H:%M:%S %Z') - logging.warning(f"Telegram InitData is older than 24 hours (Auth Date: {auth_date_str}, Current: {current_time_str}).") + 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}).") return parsed_data, True else: logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}") @@ -250,25 +227,27 @@ TEMPLATE = """ visibility: hidden; min-height: 100vh; } - .container { - max-width: 600px; - margin: 0 auto; - display: flex; - flex-direction: column; - gap: var(--padding-m); - } + .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; } + + .tabs { display: flex; gap: 8px; margin-bottom: var(--padding-m); border-bottom: 1px solid var(--card-bg); } + .tab-button { + padding: 10px 15px; background-color: transparent; color: var(--text-secondary-color); + border: none; border-bottom: 3px solid transparent; cursor: pointer; font-size: 1em; font-weight: 600; + transition: color 0.3s, border-color 0.3s; + } + .tab-button.active { color: var(--brand-yellow); border-bottom-color: var(--brand-yellow); } + .tab-content { display: none; flex-direction: column; gap: var(--padding-m); } + .tab-content.active { display: flex; } + .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; + 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); } @@ -276,62 +255,35 @@ TEMPLATE = """ .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-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; - } - .history-section { background-color: var(--card-bg); border-radius: var(--border-radius); padding: var(--padding-l); } - .history-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 { 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; } - .history-amount.invoice { color: var(--text-secondary-color); } - .view-invoice-btn { - background: rgba(255,255,255,0.1); color: var(--text-color); border: none; padding: 6px 10px; - border-radius: 6px; font-size: 0.8em; cursor: pointer; margin-left: 10px; - } - .no-history { text-align: center; color: var(--text-secondary-color); padding: 2rem 0; } - - .user-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(3px); - align-items: center; justify-content: center; padding: 15px; - } - .user-modal-content { - background-color: var(--card-bg); color: var(--text-color); margin: auto; padding: 20px; - border: 1px solid rgba(255,255,255,0.2); border-radius: var(--border-radius); - width: auto; max-width: 90%; box-shadow: 0 5px 25px rgba(0,0,0,0.3); - position: relative; - } - .user-modal-close { - color: var(--text-secondary-color); position: absolute; top: 10px; right: 15px; - font-size: 24px; font-weight: bold; cursor: pointer; - } - .user-modal-close:hover { color: var(--text-color); } - .user-modal-content h4 { margin-top: 0; margin-bottom: 15px; font-size: 1.2em; } - .user-modal-content p { margin-bottom: 8px; font-size: 0.9em; } - .user-modal-content ul { list-style: none; padding-left: 0; margin-bottom: 15px; } - .user-modal-content li { - padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.1); font-size: 0.9em; - display: flex; justify-content: space-between; + .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; } + + .section-card { background-color: var(--card-bg); border-radius: var(--border-radius); padding: var(--padding-l); } + .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); } + + .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; } + .item-details { display: flex; flex-direction: column; } + .item-description { font-size: 1em; font-weight: 500; } + .item-date { font-size: 0.8em; color: var(--text-secondary-color); margin-top: 4px; } + .item-amount { font-size: 1.1em; font-weight: 700; } + .item-amount.accrual { color: #4CAF50; } + .item-amount.deduction { color: #F44336; } + .no-items { text-align: center; color: var(--text-secondary-color); padding: 2rem 0; } + + .visiting-card-item { margin-bottom: 12px; } + .visiting-card-item strong { color: var(--brand-yellow); display: block; margin-bottom: 4px; font-size: 0.9em; } + .visiting-card-item span, .visiting-card-item a { color: var(--text-color); text-decoration: none; font-size: 1.1em; } + .visiting-card-item a:hover { text-decoration: underline; } + .phone-actions button { + background-color: rgba(255, 193, 7, 0.2); color: var(--brand-yellow); + border: none; padding: 8px 12px; margin-right: 8px; margin-top: 4px; border-radius: 8px; cursor: pointer; } - .user-modal-content li:last-child { border-bottom: none; } - .user-modal-content strong { font-weight: 600; } - .item-name { flex-grow: 1; } - .item-qty-price { color: var(--text-secondary-color); margin-left:10px; white-space:nowrap; } - .item-total { font-weight: 500; margin-left:10px; white-space:nowrap; } - + .invoice-item-details { background-color: rgba(255,255,255,0.05); padding: 10px; margin-top: 10px; border-radius: 8px;} + .invoice-item-product { display: flex; justify-content: space-between; font-size: 0.9em; margin-bottom: 5px; }
@@ -341,75 +293,127 @@ TEMPLATE = """Добро пожаловать!
-Ваши бонусы
-{{ "%.2f"|format(user.bonuses|float) }}
-Ваш долг
-{{ "%.2f"|format(user.debts|float) }}
-Ваш ID клиента
-{{ user.id }}
-Ваши бонусы
+{{ "%.2f"|format(user.bonuses|float) }}
+Ваш долг
+{{ "%.2f"|format(user.debts|float) }}
+Ваш ID клиента
+{{ user.id }}
+Операций пока не было.
+ {% endif %} +У вас пока нет накладных.
+ {% endif %} +Операций пока не было.
- {% endif %} - - - -Пользователей пока нет.
{% endif %} -