Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc4696dec2 |
@@ -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 93100
|
||||
versionName "9.3.1"
|
||||
versionCode 94000
|
||||
versionName "9.4.0"
|
||||
buildConfigField "String", "DEVICE_REGISTRY_URL", '"http://194.163.191.200:4000"'
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
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,7 @@
|
||||
<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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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 <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() });
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user