diff --git a/bot.py b/bot.py index c76f210..fe9056c 100644 --- a/bot.py +++ b/bot.py @@ -72,29 +72,30 @@ async def cmd_start(message: Message): @dp.message(Command("get")) @allowed_only async def cmd_get(message: Message): - result = db.get_random_from_oldest() - if not result: - await message.answer("❌ База пуста, брат. Добавь номера!") + record = db.get_random_record() + if not record: + await message.answer("❌ База пуста, брат!") return - phone = result['phone'] - bonus_card = result.get('bonus_card') + record_type = record['type'] + phone = record.get('phone') + qr_code = record.get('qr_code') - if bonus_card: + if record_type == 'both' and qr_code: await message.answer_photo( - photo=bonus_card, - caption=f"🎲 *МракоЗвон*\n" - f"📞 Номер: `{phone}`\n" - f"🃏 Бонусная карта привязана\n" - f"🔄 Использован: {result['usage_count']} раз", + photo=qr_code, + caption=f"🎲 *МракоЗвон*\n📞 Номер: `{phone}`\n🃏 Бонусная карта привязана\n🔄 Использован: {record['usage_count']} раз", parse_mode="Markdown" ) - else: + elif record_type == 'phone': await message.answer( - f"🎲 *МракоЗвон*\n" - f"📞 `{phone}`\n" - f"🔄 Использован: {result['usage_count']} раз\n" - f"❌ Бонусная карта отсутствует", + f"🎲 *МракоЗвон*\n📞 Номер: `{phone}`\n🔄 Использован: {record['usage_count']} раз", + parse_mode="Markdown" + ) + elif record_type == 'qr' and qr_code: + await message.answer_photo( + photo=qr_code, + caption=f"🎲 *МракоЗвон*\n🃏 Бонусная карта\n🔄 Использована: {record['usage_count']} раз", parse_mode="Markdown" ) @@ -217,104 +218,61 @@ async def cmd_whitelist(message: Message): async def handle_message(message: Message): phone = None qr_raw_data = None + qr_image = None - # 1. Если есть фото - пытаемся распознать QR + # 1. Распознаём QR из фото if message.photo: photo = message.photo[-1] file = await bot.get_file(photo.file_id) file_bytes = await bot.download_file(file.file_path) qr_raw_data = decode_qr_from_image(file_bytes.read()) + if qr_raw_data: + qr_image = generate_qr_code(qr_raw_data) - # 2. Если есть текст - пробуем извлечь номер + # 2. Извлекаем номер из текста if message.text: phone = normalize_phone(message.text.strip()) - # 3. Если нет ничего - if not phone and not qr_raw_data: - await message.answer( - "⚠️ Не распознал.\n\n" - "Отправь:\n" - "• номер телефона текстом\n" - "• бонусную карту (QR-код)\n" - "• номер + карту вместе" - ) - return - - # 4. Только номер - if phone and not qr_raw_data: - if db.add_number(phone): - await message.answer(f"✅ Сохранён номер: `{phone}`", parse_mode="Markdown") - else: - await message.answer(f"⚠️ Номер `{phone}` уже есть в базе", parse_mode="Markdown") - return - - # 5. Только QR (без номера) - if qr_raw_data and not phone: - await message.answer( - f"🃏 Распознана бонусная карта:\n" - f"`{qr_raw_data[:50]}{'...' if len(qr_raw_data) > 50 else ''}`\n\n" - f"⚠️ Чтобы привязать карту к номеру, отправь и номер тоже.\n" - f"Пример: номер и карту в одном сообщении" - ) - return - - # 6. Номер + QR вместе + # 3. Определяем тип записи и сохраняем if phone and qr_raw_data: - qr_base64 = generate_qr_code(qr_raw_data) - - if db.add_number(phone, qr_base64): - await message.answer( - f"✅ Сохранено:\n" - f"📞 Номер: `{phone}`\n" - f"🃏 Бонусная карта привязана", - parse_mode="Markdown" - ) - else: - # Если номер уже есть - обновляем карту - if db.update_bonus_card(phone, qr_base64): - await message.answer( - f"🔄 Обновлена бонусная карта для номера `{phone}`", - parse_mode="Markdown" - ) - else: - await message.answer(f"⚠️ Ошибка сохранения", parse_mode="Markdown") - return + # Тип: номер + QR + db.add_record('both', phone=phone, qr_code=qr_image, qr_data=qr_raw_data) + await message.answer(f"✅ Сохранено: номер `{phone}` + бонусная карта", parse_mode="Markdown") + elif phone: + # Тип: только номер + db.add_record('phone', phone=phone) + await message.answer(f"✅ Сохранён номер: `{phone}`", parse_mode="Markdown") + elif qr_raw_data: + # Тип: только QR + db.add_record('qr', qr_code=qr_image, qr_data=qr_raw_data) + await message.answer(f"🃏 Сохранена бонусная карта: `{qr_raw_data[:50]}...`", parse_mode="Markdown") + else: + await message.answer("⚠️ Не распознано. Отправь номер, QR-код или оба вместе.") # ========== ВЕБ-СЕРВЕР ========== routes = web.RouteTableDef() -@routes.get("/get_webapp") -async def serve_webapp(request): - try: - with open("web_app.html", "r", encoding="utf-8") as f: - html = f.read() - return web.Response(text=html, content_type="text/html") - except FileNotFoundError: - return web.Response(text="web_app.html not found", status=404) +@routes.get("/get_records") +async def get_records(request): + records = db.get_all_records() + return web.json_response({"records": records}) +@routes.get("/get_qr_code/{record_id}") +async def get_qr_code(request): + record_id = int(request.match_info.get('record_id', 0)) + with sqlite3.connect(DB_NAME) as conn: + conn.row_factory = sqlite3.Row + result = conn.execute("SELECT qr_code FROM records WHERE id = ?", (record_id,)).fetchone() + if result and result['qr_code']: + return web.json_response({"qr_code": result['qr_code']}) + return web.json_response({"qr_code": None}) -@routes.get("/get_numbers") -async def get_numbers(request): - numbers = db.get_all_numbers() - safe_numbers = [] - for num in numbers: - safe_numbers.append({ - "id": num["id"], - "phone": num["phone"], - "has_bonus_card": bool(num.get("bonus_card")), - "created_at": num["created_at"], - "used_at": num["used_at"], - "usage_count": num["usage_count"] - }) - return web.json_response({"numbers": safe_numbers}) - - -@routes.post("/delete_number") -async def delete_number(request): +@routes.post("/delete_record") +async def delete_record(request): data = await request.json() - db.delete_number(data["id"]) + db.delete_record(data["id"]) return web.json_response({"status": "ok"}) diff --git a/database.py b/database.py index a033094..64bb924 100644 --- a/database.py +++ b/database.py @@ -7,19 +7,21 @@ DB_NAME = "bot.db" def init_db(): with sqlite3.connect(DB_NAME) as conn: - # Основная таблица с номерами и бонусными картами + # Единая таблица для всех записей conn.execute(""" - CREATE TABLE IF NOT EXISTS numbers ( + CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, - phone TEXT UNIQUE NOT NULL, - bonus_card TEXT, + type TEXT NOT NULL, + phone TEXT, + qr_code TEXT, + qr_data TEXT, created_at TEXT NOT NULL, used_at TEXT, usage_count INTEGER DEFAULT 0 ) """) - # Таблица белого списка пользователей + # Таблица белого списка conn.execute(""" CREATE TABLE IF NOT EXISTS allowed_users ( user_id INTEGER PRIMARY KEY, @@ -29,89 +31,66 @@ def init_db(): ) """) - conn.execute("CREATE INDEX IF NOT EXISTS idx_created ON numbers(created_at)") - conn.execute("CREATE INDEX IF NOT EXISTS idx_used ON numbers(used_at)") + # Индексы + conn.execute("CREATE INDEX IF NOT EXISTS idx_records_used ON records(used_at)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_records_type ON records(type)") -# ========== Функции для работы с номерами и бонусными картами ========== -def add_number(phone: str, bonus_card: str = None) -> bool: - """Добавляет номер и опционально бонусную карту""" +def add_record(record_type: str, phone: str = None, qr_code: str = None, qr_data: str = None) -> bool: + """Добавляет запись""" with sqlite3.connect(DB_NAME) as conn: try: - conn.execute( - "INSERT INTO numbers (phone, bonus_card, created_at) VALUES (?, ?, ?)", - (phone, bonus_card, datetime.now().isoformat()) - ) + conn.execute(""" + INSERT INTO records (type, phone, qr_code, qr_data, created_at) + VALUES (?, ?, ?, ?, ?) + """, (record_type, phone, qr_code, qr_data, datetime.now().isoformat())) return True except sqlite3.IntegrityError: return False -def update_bonus_card(phone: str, bonus_card: str) -> bool: - """Обновляет бонусную карту для существующего номера""" - with sqlite3.connect(DB_NAME) as conn: - cursor = conn.execute( - "UPDATE numbers SET bonus_card = ? WHERE phone = ?", - (bonus_card, phone) - ) - return cursor.rowcount > 0 - - -def get_random_from_oldest(): +def get_random_record(): + """Возвращает случайную запись""" with sqlite3.connect(DB_NAME) as conn: conn.row_factory = sqlite3.Row - total_count = conn.execute("SELECT COUNT(*) as count FROM numbers").fetchone()["count"] + total_count = conn.execute("SELECT COUNT(*) as count FROM records").fetchone()["count"] if total_count <= 50: limit = 10 else: limit = max(10, min(50, int(total_count * 0.2))) - oldest = conn.execute( - "SELECT *, COALESCE(used_at, created_at) as sort_date FROM numbers ORDER BY sort_date ASC LIMIT ?", - (limit,) - ).fetchall() + oldest = conn.execute(""" + SELECT *, COALESCE(used_at, created_at) as sort_date + FROM records + ORDER BY sort_date ASC + LIMIT ? + """, (limit,)).fetchall() if not oldest: return None selected = random.choice(oldest) - conn.execute( - "UPDATE numbers SET usage_count = usage_count + 1, used_at = ? WHERE id = ?", - (datetime.now().isoformat(), selected["id"]) - ) + conn.execute(""" + UPDATE records SET usage_count = usage_count + 1, used_at = ? + WHERE id = ? + """, (datetime.now().isoformat(), selected["id"])) return dict(selected) -def get_all_numbers(): +def get_all_records(): with sqlite3.connect(DB_NAME) as conn: conn.row_factory = sqlite3.Row - rows = conn.execute("SELECT * FROM numbers ORDER BY created_at DESC").fetchall() + rows = conn.execute("SELECT * FROM records ORDER BY created_at DESC").fetchall() return [dict(row) for row in rows] -def delete_number(record_id: int): +def delete_record(record_id: int): with sqlite3.connect(DB_NAME) as conn: - conn.execute("DELETE FROM numbers WHERE id = ?", (record_id,)) - - -def get_bonus_card(record_id: int) -> str | None: - """Получить бонусную карту по ID записи""" - with sqlite3.connect(DB_NAME) as conn: - conn.row_factory = sqlite3.Row - result = conn.execute("SELECT bonus_card FROM numbers WHERE id = ?", (record_id,)).fetchone() - return result["bonus_card"] if result else None - - -def get_bonus_card_by_phone(phone: str) -> str | None: - """Получить бонусную карту по номеру телефона""" - with sqlite3.connect(DB_NAME) as conn: - conn.row_factory = sqlite3.Row - result = conn.execute("SELECT bonus_card FROM numbers WHERE phone = ?", (phone,)).fetchone() - return result["bonus_card"] if result else None + conn.execute("DELETE FROM records WHERE id = ?", (record_id,)) # ========== Функции для белого списка ========== diff --git a/web_app.html b/web_app.html index 61e2a1e..17eec30 100644 --- a/web_app.html +++ b/web_app.html @@ -3,7 +3,7 @@ - МракоЗвон — управление номерами + МракоЗвон — управление @@ -111,10 +175,29 @@
-

🌑 МракоЗвон

+

🌑 МракоЗвон

Загрузка...
-
+ +
+ + + + +
+ +
+
+
+
+
+
+
+
+
+
+
+
@@ -122,83 +205,118 @@ const tg = window.Telegram.WebApp; tg.expand(); - async function loadNumbers() { - const listDiv = document.getElementById('list'); - const totalSpan = document.getElementById('totalCount'); - listDiv.innerHTML = '
⏳ Загрузка...
'; + let currentData = []; + async function loadData() { try { - const response = await fetch('/get_numbers'); + const response = await fetch('/get_records'); const data = await response.json(); - - if (!data.numbers || data.numbers.length === 0) { - listDiv.innerHTML = '
📭 Нет номеров
'; - totalSpan.innerText = 'Всего: 0'; - return; - } - - totalSpan.innerText = `Всего: ${data.numbers.length}`; - listDiv.innerHTML = ''; - - data.numbers.forEach(num => { - const card = document.createElement('div'); - card.className = 'number-card'; - const created = new Date(num.created_at).toLocaleString('ru'); - const used = num.used_at ? new Date(num.used_at).toLocaleString('ru') : '—'; - const hasBonus = num.has_bonus_card ? '✅ Есть' : '❌ Нет'; - - const bonusButton = num.has_bonus_card ? - `` : ''; - - card.innerHTML = ` -
-
📱 ${num.phone}
-
🃏 Бонусная карта: ${hasBonus}
-
➕ Добавлен: ${created}
-
🔄 Использован: ${num.usage_count} раз, последний: ${used}
-
-
- ${bonusButton} - -
- `; - listDiv.appendChild(card); - }); - - // Вешаем обработчики на кнопки удаления - document.querySelectorAll('.delete-btn').forEach(btn => { - btn.addEventListener('click', async (e) => { - const id = btn.getAttribute('data-id'); - if (confirm('Удалить номер?')) { - await fetch('/delete_number', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({id: parseInt(id)}) - }); - loadNumbers(); - } - }); - }); - - // Вешаем обработчики на кнопки бонусных карт - document.querySelectorAll('.bonus-btn').forEach(btn => { - btn.addEventListener('click', async (e) => { - const id = btn.getAttribute('data-id'); - await showBonusCard(id); - }); - }); + currentData = data.records || []; + updateCounters(); + renderAll(); + renderPhones(); + renderQRs(); + renderBoth(); } catch (e) { console.error(e); - listDiv.innerHTML = '
⚠️ Ошибка загрузки
'; + document.getElementById('list-all').innerHTML = '
⚠️ Ошибка загрузки
'; } } - async function showBonusCard(id) { + function updateCounters() { + const total = currentData.length; + const phones = currentData.filter(r => r.type === 'phone').length; + const qrs = currentData.filter(r => r.type === 'qr').length; + const both = currentData.filter(r => r.type === 'both').length; + document.getElementById('totalCount').innerHTML = `📊 ${total} | 📞 ${phones} | 🃏 ${qrs} | 🔗 ${both}`; + } + + function getTypeBadge(type) { + if (type === 'phone') return '📞 Только номер'; + if (type === 'qr') return '🃏 Только карта'; + return '🔗 Номер + карта'; + } + + function getRecordHTML(record) { + const created = new Date(record.created_at).toLocaleString('ru'); + const used = record.used_at ? new Date(record.used_at).toLocaleString('ru') : '—'; + const badge = getTypeBadge(record.type); + + let content = ''; + if (record.type === 'phone' || record.type === 'both') { + content += `
📱 ${record.phone}
`; + } + if (record.type === 'qr' || record.type === 'both') { + const shortData = record.qr_data ? record.qr_data.slice(0, 50) + (record.qr_data.length > 50 ? '...' : '') : '—'; + content += `
🃏 Данные карты: ${shortData}
`; + } + + const showQRBtn = (record.type === 'qr' || record.type === 'both') && record.qr_code ? + `` : ''; + + return ` +
+ ${badge} + ${content} +
➕ Добавлен: ${created}
+
🔄 Использован: ${record.usage_count} раз, последний: ${used}
+
+ ${showQRBtn} + +
+
+ `; + } + + function renderAll() { + const container = document.getElementById('list-all'); + if (currentData.length === 0) { + container.innerHTML = '
📭 Нет записей
'; + return; + } + container.innerHTML = currentData.map(r => getRecordHTML(r)).join(''); + attachEvents(); + } + + function renderPhones() { + const container = document.getElementById('list-phones'); + const phones = currentData.filter(r => r.type === 'phone'); + if (phones.length === 0) { + container.innerHTML = '
📭 Нет номеров
'; + return; + } + container.innerHTML = phones.map(r => getRecordHTML(r)).join(''); + attachEvents(); + } + + function renderQRs() { + const container = document.getElementById('list-qrs'); + const qrs = currentData.filter(r => r.type === 'qr'); + if (qrs.length === 0) { + container.innerHTML = '
🃏 Нет бонусных карт
'; + return; + } + container.innerHTML = qrs.map(r => getRecordHTML(r)).join(''); + attachEvents(); + } + + function renderBoth() { + const container = document.getElementById('list-both'); + const both = currentData.filter(r => r.type === 'both'); + if (both.length === 0) { + container.innerHTML = '
🔗 Нет связок номер+карта
'; + return; + } + container.innerHTML = both.map(r => getRecordHTML(r)).join(''); + attachEvents(); + } + + async function showQRModal(recordId) { try { - const response = await fetch(`/get_bonus_card/${id}`); + const response = await fetch(`/get_qr_code/${recordId}`); const data = await response.json(); - if (data.bonus_card) { + if (data.qr_code) { const modal = document.createElement('div'); modal.style.position = 'fixed'; modal.style.top = '0'; @@ -213,24 +331,75 @@ modal.style.cursor = 'pointer'; const img = document.createElement('img'); - img.src = data.bonus_card; + img.src = data.qr_code; img.style.maxWidth = '85%'; img.style.maxHeight = '85%'; img.style.borderRadius = '12px'; - img.style.boxShadow = '0 4px 20px rgba(0,0,0,0.5)'; modal.onclick = () => modal.remove(); modal.appendChild(img); document.body.appendChild(modal); } else { - alert('Бонусная карта отсутствует для этого номера'); + alert('QR-код отсутствует'); } } catch (e) { - alert('Ошибка загрузки бонусной карты'); + alert('Ошибка загрузки QR'); } } - loadNumbers(); + async function deleteRecord(recordId) { + if (!confirm('Удалить запись?')) return; + + try { + const response = await fetch('/delete_record', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({id: recordId}) + }); + if (response.ok) { + loadData(); + } else { + alert('Ошибка при удалении'); + } + } catch (e) { + alert('Ошибка'); + } + } + + function attachEvents() { + document.querySelectorAll('.show-qr-btn').forEach(btn => { + btn.removeEventListener('click', btn._handler); + btn._handler = () => showQRModal(btn.dataset.id); + btn.addEventListener('click', btn._handler); + }); + + document.querySelectorAll('.delete-btn').forEach(btn => { + btn.removeEventListener('click', btn._handler); + btn._handler = () => deleteRecord(btn.dataset.id); + btn.addEventListener('click', btn._handler); + }); + } + + function showTab(tabName) { + document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); + document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); + + if (tabName === 'all') { + document.querySelector('.tab-btn:first-child').classList.add('active'); + document.getElementById('tab-all').classList.add('active'); + } else if (tabName === 'phones') { + document.querySelectorAll('.tab-btn')[1].classList.add('active'); + document.getElementById('tab-phones').classList.add('active'); + } else if (tabName === 'qrs') { + document.querySelectorAll('.tab-btn')[2].classList.add('active'); + document.getElementById('tab-qrs').classList.add('active'); + } else if (tabName === 'both') { + document.querySelectorAll('.tab-btn')[3].classList.add('active'); + document.getElementById('tab-both').classList.add('active'); + } + } + + loadData(); \ No newline at end of file