From cc4696dec2464ce15667e1d2640e9c128f11b96a Mon Sep 17 00:00:00 2001 From: renato97 Date: Sun, 23 Nov 2025 23:15:37 +0100 Subject: [PATCH] Update v9.4.0: Advanced Telegram Integration & Custom UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Incremented version to 9.4.0 (versionCode: 94000) - Added custom blocked dialog layout with improved UX - Implemented Telegram commands: /allow, /deny, /pending - Enhanced Telegram polling for real-time device management - Custom token display with click-to-copy functionality - Improved device verification workflow via Telegram - Better error handling and user feedback - Enhanced documentation for Telegram integration - Improved token validation and management system 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 8 +- app/build.gradle | 4 +- .../java/com/streamplayer/MainActivity.java | 30 ++-- app/src/main/res/layout/dialog_blocked.xml | 48 ++++++ app/src/main/res/values/strings.xml | 1 + dashboard/data/devices.json | 13 +- dashboard/server.js | 157 +++++++++++++++++- update-manifest.json | 12 +- 8 files changed, 239 insertions(+), 34 deletions(-) create mode 100644 app/src/main/res/layout/dialog_blocked.xml diff --git a/README.md b/README.md index 41a28ee..b1c5798 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,13 @@ Para autorizar un dispositivo pendiente: 3. En el dashboard presiona “Verificar token” e introduce ambas mitades. Si coinciden, el estado pasa a "Verificado" y la app se desbloquea automáticamente. 4. A partir de allí puedes bloquear/desbloquear manualmente cuando quieras. -Cada nuevo registro también dispara una notificación de Telegram para que puedas reaccionar en tiempo real. +También puedes gestionar todo desde Telegram: + +- `/allow ` autoriza el dispositivo (verifica el token y lo desbloquea). +- `/deny [motivo]` lo bloquea con un motivo opcional. +- `/pending` lista los registros que aún esperan un token válido. + +Cada nuevo registro dispara una notificación de Telegram con la parte admin del token y recordatorios de esos comandos. ## 📱 Estructura del Proyecto diff --git a/app/build.gradle b/app/build.gradle index 891d2d5..8912a88 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "com.streamplayer" minSdk 21 targetSdk 33 - versionCode 93100 - versionName "9.3.1" + versionCode 94000 + versionName "9.4.0" buildConfigField "String", "DEVICE_REGISTRY_URL", '"http://194.163.191.200:4000"' } diff --git a/app/src/main/java/com/streamplayer/MainActivity.java b/app/src/main/java/com/streamplayer/MainActivity.java index f214b56..120d47c 100644 --- a/app/src/main/java/com/streamplayer/MainActivity.java +++ b/app/src/main/java/com/streamplayer/MainActivity.java @@ -320,24 +320,30 @@ public class MainActivity extends AppCompatActivity { if (blockedDialog != null && blockedDialog.isShowing()) { blockedDialog.dismiss(); } - StringBuilder messageBuilder = new StringBuilder(); - messageBuilder.append(getString(R.string.device_blocked_message, finalReason)); - if (!TextUtils.isEmpty(tokenPart)) { - messageBuilder.append("\n\n") - .append(getString(R.string.device_blocked_token_hint, tokenPart)); + View dialogView = getLayoutInflater().inflate(R.layout.dialog_blocked, null); + TextView messageText = dialogView.findViewById(R.id.blocked_message_text); + View tokenContainer = dialogView.findViewById(R.id.blocked_token_container); + TextView tokenValue = dialogView.findViewById(R.id.blocked_token_value); + messageText.setText(getString(R.string.device_blocked_message, finalReason)); + boolean hasToken = !TextUtils.isEmpty(tokenPart); + if (hasToken) { + tokenContainer.setVisibility(View.VISIBLE); + tokenValue.setText(tokenPart); + tokenValue.setOnClickListener(v -> copyTokenToClipboard(tokenPart)); + } else { + tokenContainer.setVisibility(View.GONE); } - blockedDialog = new AlertDialog.Builder(this) + AlertDialog.Builder builder = new AlertDialog.Builder(this) .setTitle(R.string.device_blocked_title) - .setMessage(messageBuilder.toString()) + .setView(dialogView) .setCancelable(false) .setPositiveButton(R.string.device_blocked_close, - (dialog, which) -> finish()) - .create(); - if (!TextUtils.isEmpty(tokenPart)) { - blockedDialog.setButton(AlertDialog.BUTTON_NEUTRAL, - getString(R.string.device_blocked_copy_token), + (dialog, which) -> finish()); + if (hasToken) { + builder.setNeutralButton(R.string.device_blocked_copy_token, (dialog, which) -> copyTokenToClipboard(tokenPart)); } + blockedDialog = builder.create(); blockedDialog.show(); } diff --git a/app/src/main/res/layout/dialog_blocked.xml b/app/src/main/res/layout/dialog_blocked.xml new file mode 100644 index 0000000..ea156c3 --- /dev/null +++ b/app/src/main/res/layout/dialog_blocked.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b314f37..ade8589 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,6 +40,7 @@ Este dispositivo fue bloqueado desde el panel de control. Motivo: %1$s Sin motivo especificado. Comparte este código con el administrador para solicitar acceso: %1$s + Código de verificación Salir Copiar código Código copiado al portapapeles diff --git a/dashboard/data/devices.json b/dashboard/data/devices.json index 8bd2041..c140778 100644 --- a/dashboard/data/devices.json +++ b/dashboard/data/devices.json @@ -6,21 +6,22 @@ "model": "SM-S928B", "manufacturer": "Samsung", "osVersion": "16 (API 36)", - "appVersionName": "9.3.0", - "appVersionCode": 93000, + "appVersionName": "9.3.1", + "appVersionCode": 93100, "firstSeen": "2025-11-23T20:53:43.615Z", - "lastSeen": "2025-11-23T21:12:30.345Z", + "lastSeen": "2025-11-23T21:57:09.997Z", "blocked": false, "notes": "no pagó", - "installs": 9, + "installs": 14, "blockedAt": "2025-11-23T20:54:05.413Z", "ip": "181.23.253.20", "country": "AR", "verification": { "clientPart": "6e05a220abe0ed05", "adminPart": "19d6ee4c992ee1a0", - "status": "pending", - "createdAt": "2025-11-23T21:09:04.607Z" + "status": "verified", + "createdAt": "2025-11-23T21:09:04.607Z", + "verifiedAt": "2025-11-23T21:57:05.081Z" } } ] \ No newline at end of file diff --git a/dashboard/server.js b/dashboard/server.js index be1f95d..b3c3477 100644 --- a/dashboard/server.js +++ b/dashboard/server.js @@ -23,7 +23,9 @@ const CONFIG_PATH = path.join(__dirname, 'config.json'); const config = loadConfig(); const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || config.telegramBotToken || ''; -const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID || config.telegramChatId || ''; +const TELEGRAM_CHAT_ID = (process.env.TELEGRAM_CHAT_ID || config.telegramChatId || '').toString(); +let telegramOffset = 0; +let telegramPollingStarted = false; app.use(cors()); app.use(express.json()); @@ -109,22 +111,30 @@ function generateTokenParts() { }; } -async function sendTelegramNotification(message) { +async function sendTelegramMessage(message, options = {}) { if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) { return; } const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`; try { - await axios.post(url, { + const payload = { chat_id: TELEGRAM_CHAT_ID, text: message, - parse_mode: 'Markdown' - }); + disable_web_page_preview: true + }; + if (options.parseMode) { + payload.parse_mode = options.parseMode; + } + await axios.post(url, payload); } catch (error) { - console.warn('Failed to send Telegram notification', error.response ? error.response.data : error.message); + console.warn('Failed to send Telegram message', error.response ? error.response.data : error.message); } } +async function sendTelegramNotification(message) { + await sendTelegramMessage(message, { parseMode: 'Markdown' }); +} + function formatTelegramMessage(device, verificationRequired) { const lines = [ '*Nuevo registro de dispositivo*', @@ -137,11 +147,143 @@ function formatTelegramMessage(device, verificationRequired) { ]; if (verificationRequired && device.verification) { lines.push('`Token Admin` (guárdalo): `' + device.verification.adminPart + '`'); - lines.push('Comparte el token del cliente y este admin para autorizar.'); + lines.push('Autorizar: `/allow ' + device.deviceId + ' TOKEN_CLIENTE`'); + lines.push('Rechazar: `/deny ' + device.deviceId + ' TOKEN_CLIENTE [motivo]`'); } return lines.join('\n'); } +async function initializeTelegramOffset() { + if (!TELEGRAM_BOT_TOKEN) { + return; + } + const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates`; + try { + const { data } = await axios.get(url, { params: { timeout: 1 } }); + if (data.ok && Array.isArray(data.result) && data.result.length > 0) { + telegramOffset = data.result[data.result.length - 1].update_id + 1; + } + } catch (error) { + console.warn('Failed to initialize Telegram offset', error.message); + } +} + +async function pollTelegramUpdates() { + if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) { + return; + } + const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates`; + const params = { + timeout: 25 + }; + if (telegramOffset) { + params.offset = telegramOffset; + } + try { + const { data } = await axios.get(url, { params }); + if (data.ok && Array.isArray(data.result)) { + for (const update of data.result) { + telegramOffset = update.update_id + 1; + if (update.message) { + processTelegramMessage(update.message); + } + } + } + } catch (error) { + console.warn('Telegram polling error', error.message); + } finally { + setTimeout(pollTelegramUpdates, 3000); + } +} + +async function startTelegramPolling() { + if (telegramPollingStarted || !TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) { + return; + } + telegramPollingStarted = true; + await initializeTelegramOffset(); + pollTelegramUpdates(); +} + +function processTelegramMessage(message) { + const chatId = message.chat && message.chat.id ? message.chat.id.toString() : ''; + if (chatId && TELEGRAM_CHAT_ID && chatId !== TELEGRAM_CHAT_ID.toString()) { + return; + } + const text = (message.text || '').trim(); + if (!text.startsWith('/')) { + return; + } + const parts = text.split(/\s+/); + const command = parts[0].toLowerCase(); + if (command === '/allow' && parts.length >= 3) { + handleTelegramAllow(parts[1], parts[2]); + } else if (command === '/deny' && parts.length >= 3) { + const reason = parts.slice(3).join(' ') || 'Bloqueado desde Telegram'; + handleTelegramDeny(parts[1], parts[2], reason); + } else if (command === '/pending') { + handleTelegramPending(); + } else { + sendTelegramMessage('Comandos disponibles:\n/allow \n/deny [motivo]\n/pending'); + } +} + +async function handleTelegramAllow(deviceId, clientToken) { + const devices = readDevices(); + const device = devices.find(d => d.deviceId === deviceId); + if (!device || !device.verification) { + await sendTelegramMessage(`❌ Dispositivo ${deviceId} no encontrado.`); + return; + } + if (device.verification.status === 'verified') { + await sendTelegramMessage(`ℹ️ El dispositivo ${deviceId} ya está verificado.`); + return; + } + if (device.verification.clientPart !== clientToken.trim()) { + await sendTelegramMessage('❌ Token del cliente inválido.'); + return; + } + device.verification.status = 'verified'; + device.verification.verifiedAt = new Date().toISOString(); + device.blocked = false; + device.notes = ''; + writeDevices(devices); + await sendTelegramMessage(`✅ Dispositivo ${deviceId} autorizado correctamente.`); +} + +async function handleTelegramDeny(deviceId, clientToken, reason) { + const devices = readDevices(); + const device = devices.find(d => d.deviceId === deviceId); + if (!device || !device.verification) { + await sendTelegramMessage(`❌ Dispositivo ${deviceId} no encontrado.`); + return; + } + if (device.verification.clientPart !== clientToken.trim()) { + await sendTelegramMessage('❌ Token del cliente inválido.'); + return; + } + device.verification.status = 'denied'; + device.verification.deniedAt = new Date().toISOString(); + device.blocked = true; + device.notes = reason; + writeDevices(devices); + await sendTelegramMessage(`🚫 Dispositivo ${deviceId} bloqueado. Motivo: ${reason}`); +} + +async function handleTelegramPending() { + const devices = readDevices(); + const pending = devices.filter(d => !d.verification || d.verification.status !== 'verified'); + if (!pending.length) { + await sendTelegramMessage('No hay dispositivos pendientes.'); + return; + } + const lines = pending.slice(0, 10).map(device => { + const token = device.verification ? device.verification.clientPart : 'N/A'; + return `${device.deviceId} - Token cliente: ${token}`; + }); + await sendTelegramMessage('Pendientes:\n' + lines.join('\n')); +} + app.get('/api/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); @@ -318,4 +460,5 @@ app.delete('/api/devices/:deviceId', (req, res) => { app.listen(PORT, () => { ensureDataFile(); console.log(`StreamPlayer dashboard server listening on port ${PORT}`); + startTelegramPolling(); }); diff --git a/update-manifest.json b/update-manifest.json index f1de133..1fc9cdc 100644 --- a/update-manifest.json +++ b/update-manifest.json @@ -1,10 +1,10 @@ { - "versionCode": 93000, - "versionName": "9.3.0", + "versionCode": 93100, + "versionName": "9.3.1", "minSupportedVersionCode": 91000, "forceUpdate": false, - "downloadUrl": "https://gitea.cbcren.online/renato97/app/releases/download/v9.3.0/StreamPlayer-v9.3.0.apk", - "fileName": "StreamPlayer-v9.3.0.apk", - "sizeBytes": 5941443, - "notes": "StreamPlayer v9.3.0\n\nMejoras importantes en esta versión:\n\n- Sistema de seguridad mejorado con verificación avanzada\n- Nuevas herramientas de administración y control\n- Mejoras en la gestión de dispositivos\n- Interfaz de usuario optimizada\n- Mayor rendimiento y estabilidad\n- Correcciones de seguridad mejoradas\n- Sistema de notificaciones más eficiente\n\nEsta actualización fortalece la seguridad y mejora la experiencia general de uso manteniendo todas las funcionalidades existentes." + "downloadUrl": "https://gitea.cbcren.online/renato97/app/releases/download/v9.3.1/StreamPlayer-v9.3.1.apk", + "fileName": "StreamPlayer-v9.3.1.apk", + "sizeBytes": 5943075, + "notes": "StreamPlayer v9.3.1\n\nMejoras en esta versión:\n\n- Interfaz de usuario mejorada con nuevos controles\n- Funcionalidad de copiado mejorada\n- Diálogos más intuitivos y fáciles de usar\n- Mejor retroalimentación para el usuario\n- Configuración optimizada para mejor funcionamiento\n- Mayor estabilidad general de la aplicación\n- Correcciones menores de usabilidad\n\nEsta actualización mejora la experiencia de uso y facilita la interacción con las funcionalidades de verificación y seguridad." }