Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c65578bdd | ||
|
|
6aef195f30 | ||
|
|
cc4696dec2 | ||
|
|
73a4f81341 |
5
.env
5
.env
@@ -1,5 +1,4 @@
|
||||
GITEA_TOKEN=7921aa22187b39125d29399d26f527ba26a2fb5b
|
||||
|
||||
GEMINI_API_KEY=AIzaSyDWOgyAJqscuPU6iSpS6gxupWBm4soNw5o
|
||||
telegram_bot_token:8593525164:AAGCX9B_RJGN35_F7tSB72rEZhS_4Zpcszs
|
||||
chat_id:692714536
|
||||
TELEGRAM_BOT_TOKEN=8593525164:AAGCX9B_RJGN35_F7tSB72rEZhS_4Zpcszs
|
||||
TELEGRAM_CHAT_ID=692714536
|
||||
|
||||
@@ -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 <deviceId> <token_cliente>` autoriza el dispositivo (verifica el token y lo desbloquea).
|
||||
- `/deny <deviceId> <token_cliente> [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
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ android {
|
||||
applicationId "com.streamplayer"
|
||||
minSdk 21
|
||||
targetSdk 33
|
||||
versionCode 93000
|
||||
versionName "9.3.0"
|
||||
versionCode 94200
|
||||
versionName "9.4.2"
|
||||
buildConfigField "String", "DEVICE_REGISTRY_URL", '"http://194.163.191.200:4000"'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.streamplayer;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
@@ -241,7 +244,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (updateDialog != null && updateDialog.isShowing()) {
|
||||
updateDialog.dismiss();
|
||||
}
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this)
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.ThemeOverlay_StreamPlayer_AlertDialog)
|
||||
.setTitle(mandatory ? R.string.update_required_title : R.string.update_available_title)
|
||||
.setMessage(buildUpdateMessage(info))
|
||||
.setPositiveButton(R.string.update_action_download,
|
||||
@@ -317,19 +320,42 @@ 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, R.style.ThemeOverlay_StreamPlayer_AlertDialog)
|
||||
.setTitle(R.string.device_blocked_title)
|
||||
.setMessage(messageBuilder.toString())
|
||||
.setView(dialogView)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.device_blocked_close,
|
||||
(dialog, which) -> finish())
|
||||
.show();
|
||||
(dialog, which) -> finish());
|
||||
if (hasToken) {
|
||||
builder.setNeutralButton(R.string.device_blocked_copy_token,
|
||||
(dialog, which) -> copyTokenToClipboard(tokenPart));
|
||||
}
|
||||
blockedDialog = builder.create();
|
||||
blockedDialog.show();
|
||||
}
|
||||
|
||||
private void copyTokenToClipboard(String tokenPart) {
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (clipboard == null) {
|
||||
Toast.makeText(this, R.string.device_blocked_copy_error, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
ClipData data = ClipData.newPlainText("token", tokenPart);
|
||||
clipboard.setPrimaryClip(data);
|
||||
Toast.makeText(this, R.string.device_blocked_copy_success, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private int getSpanCount() {
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.StrictMode;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
@@ -58,6 +59,7 @@ public class PlayerActivity extends AppCompatActivity {
|
||||
);
|
||||
|
||||
setContentView(R.layout.activity_player);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
|
||||
Intent intent = getIntent();
|
||||
if (intent == null) {
|
||||
@@ -254,6 +256,7 @@ public class PlayerActivity extends AppCompatActivity {
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
releasePlayer();
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
|
||||
private void toggleOverlay() {
|
||||
|
||||
48
app/src/main/res/layout/dialog_blocked.xml
Normal file
48
app/src/main/res/layout/dialog_blocked.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/blocked_message_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/blocked_token_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/blocked_token_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/device_blocked_token_label"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/blocked_token_value"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:padding="8dp"
|
||||
android:textColor="@android:color/black"
|
||||
android:textIsSelectable="true"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -40,6 +40,10 @@
|
||||
<string name="device_blocked_message">Este dispositivo fue bloqueado desde el panel de control. Motivo: %1$s</string>
|
||||
<string name="device_blocked_default_reason">Sin motivo especificado.</string>
|
||||
<string name="device_blocked_token_hint">Comparte este código con el administrador para solicitar acceso: %1$s</string>
|
||||
<string name="device_blocked_token_label">Código de verificación</string>
|
||||
<string name="device_blocked_close">Salir</string>
|
||||
<string name="device_blocked_copy_token">Copiar código</string>
|
||||
<string name="device_blocked_copy_success">Código copiado al portapapeles</string>
|
||||
<string name="device_blocked_copy_error">No se pudo copiar el código</string>
|
||||
<string name="device_registry_error">No se pudo registrar el dispositivo (%1$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -6,4 +6,10 @@
|
||||
<item name="android:statusBarColor">@color/black</item>
|
||||
<item name="android:navigationBarColor">@color/black</item>
|
||||
</style>
|
||||
|
||||
<style name="ThemeOverlay.StreamPlayer.AlertDialog" parent="ThemeOverlay.AppCompat.Dialog.Alert">
|
||||
<item name="android:textColorPrimary">@color/white</item>
|
||||
<item name="android:textColorSecondary">@color/white</item>
|
||||
<item name="colorAccent">@color/white</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -6,21 +6,21 @@
|
||||
"model": "SM-S928B",
|
||||
"manufacturer": "Samsung",
|
||||
"osVersion": "16 (API 36)",
|
||||
"appVersionName": "9.2.0",
|
||||
"appVersionCode": 92000,
|
||||
"firstSeen": "2025-11-23T20:53:43.615Z",
|
||||
"lastSeen": "2025-11-23T21:09:04.607Z",
|
||||
"appVersionName": "9.4.1",
|
||||
"appVersionCode": 94100,
|
||||
"firstSeen": "2025-11-23T22:31:13.359Z",
|
||||
"lastSeen": "2025-11-23T23:11:07.215Z",
|
||||
"blocked": false,
|
||||
"notes": "no pagó",
|
||||
"installs": 8,
|
||||
"blockedAt": "2025-11-23T20:54:05.413Z",
|
||||
"notes": "",
|
||||
"installs": 7,
|
||||
"ip": "181.23.253.20",
|
||||
"country": "AR",
|
||||
"verification": {
|
||||
"clientPart": "6e05a220abe0ed05",
|
||||
"adminPart": "19d6ee4c992ee1a0",
|
||||
"status": "pending",
|
||||
"createdAt": "2025-11-23T21:09:04.607Z"
|
||||
"clientPart": "1714c2bb93670c3f",
|
||||
"adminPart": "9924c7049211c58c",
|
||||
"status": "verified",
|
||||
"createdAt": "2025-11-23T22:31:13.359Z",
|
||||
"verifiedAt": "2025-11-23T22:33:11.942Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
12
dashboard/node_modules/.package-lock.json
generated
vendored
12
dashboard/node_modules/.package-lock.json
generated
vendored
@@ -393,6 +393,18 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
||||
13
dashboard/package-lock.json
generated
13
dashboard/package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.6.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.18.2",
|
||||
"geoip-lite": "^1.4.6",
|
||||
"morgan": "^1.10.0",
|
||||
@@ -409,6 +410,18 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.7",
|
||||
"dotenv": "^16.4.5",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"geoip-lite": "^1.4.6",
|
||||
|
||||
@@ -45,6 +45,7 @@ function renderTable(devices) {
|
||||
actions.push('<button data-action="verify" class="primary">Verificar token</button>');
|
||||
}
|
||||
actions.push(device.blocked ? '<button data-action="unblock" class="primary">Desbloquear</button>' : '<button data-action="block" class="danger">Bloquear</button>');
|
||||
actions.push('<button data-action="delete" class="danger ghost">Borrar</button>');
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
@@ -97,6 +98,19 @@ async function unblockDevice(deviceId) {
|
||||
await fetchDevices();
|
||||
}
|
||||
|
||||
async function deleteDevice(deviceId) {
|
||||
const confirmation = confirm('¿Seguro que quieres borrar este dispositivo? Generará un nuevo token cuando se registre de nuevo.');
|
||||
if (!confirmation) {
|
||||
return;
|
||||
}
|
||||
const response = await fetch(`/api/devices/${encodeURIComponent(deviceId)}`, { method: 'DELETE' });
|
||||
if (!response.ok) {
|
||||
alert('No se pudo borrar el dispositivo');
|
||||
return;
|
||||
}
|
||||
await fetchDevices();
|
||||
}
|
||||
|
||||
async function verifyDevice(deviceId) {
|
||||
const clientTokenPart = prompt('Introduce el token que aparece en el dispositivo:');
|
||||
if (clientTokenPart === null) {
|
||||
@@ -154,6 +168,8 @@ tableBody.addEventListener('click', async (event) => {
|
||||
await updateAlias(deviceId);
|
||||
} else if (action === 'verify') {
|
||||
await verifyDevice(deviceId);
|
||||
} else if (action === 'delete') {
|
||||
await deleteDevice(deviceId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
@@ -7,6 +7,14 @@ const crypto = require('crypto');
|
||||
const axios = require('axios');
|
||||
const requestIp = require('request-ip');
|
||||
const geoip = require('geoip-lite');
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
const envPath = path.resolve(__dirname, '..', '.env');
|
||||
if (fs.existsSync(envPath)) {
|
||||
dotenv.config({ path: envPath });
|
||||
} else {
|
||||
dotenv.config();
|
||||
}
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 4000;
|
||||
@@ -15,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());
|
||||
@@ -101,20 +111,28 @@ 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'
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to send Telegram notification', error.response ? error.response.data : error.message);
|
||||
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 message', error.response ? error.response.data : error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendTelegramNotification(message) {
|
||||
await sendTelegramMessage(message, { parseMode: 'Markdown' });
|
||||
}
|
||||
|
||||
function formatTelegramMessage(device, verificationRequired) {
|
||||
@@ -129,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 <deviceId> <token_cliente>\n/deny <deviceId> <token_cliente> [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() });
|
||||
});
|
||||
@@ -310,4 +460,5 @@ app.delete('/api/devices/:deviceId', (req, res) => {
|
||||
app.listen(PORT, () => {
|
||||
ensureDataFile();
|
||||
console.log(`StreamPlayer dashboard server listening on port ${PORT}`);
|
||||
startTelegramPolling();
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"versionCode": 92000,
|
||||
"versionName": "9.2.0",
|
||||
"versionCode": 94100,
|
||||
"versionName": "9.4.1",
|
||||
"minSupportedVersionCode": 91000,
|
||||
"forceUpdate": false,
|
||||
"downloadUrl": "https://gitea.cbcren.online/renato97/app/releases/download/v9.2.0/StreamPlayer-v9.2.0.apk",
|
||||
"fileName": "StreamPlayer-v9.2.0.apk",
|
||||
"sizeBytes": 5940764,
|
||||
"notes": "StreamPlayer v9.2.0\n\nMejoras en esta versión:\n\n- Interfaz de usuario optimizada para mayor claridad\n- Diálogos de actualización más intuitivos\n- Mejora general en la experiencia de uso\n- Mayor estabilidad y rendimiento\n- Correcciones de errores menores\n\nEsta actualización mejora la usabilidad y mantiene todas las funcionalidades de seguridad y gestión de dispositivos."
|
||||
"downloadUrl": "https://gitea.cbcren.online/renato97/app/releases/download/v9.4.1/StreamPlayer-v9.4.1.apk",
|
||||
"fileName": "StreamPlayer-v9.4.1.apk",
|
||||
"sizeBytes": 5944680,
|
||||
"notes": "StreamPlayer v9.4.1\n\nMejoras en esta versión:\n\n- Experiencia de reproducción optimizada e ininterrumpida\n- Mejores controles de administración y gestión de dispositivos\n- Funcionalidad de eliminación de registros con confirmación segura\n- Optimización de energía durante el uso de la aplicación\n- Interfaz administrativa mejorada con más opciones\n- Flujo de trabajo más eficiente para la gestión\n- Mejor respuesta y estabilidad general\n- Correcciones de usabilidad menores\n\nEsta actualización mejora tanto la experiencia de visualización como las herramientas de administración para un mejor control y uso de la aplicación."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user