Feat: UI/UX y Procesos

This commit is contained in:
2026-02-09 19:00:18 -03:00
parent 7222728591
commit f6d8b34520
13 changed files with 925 additions and 359 deletions

View File

@@ -1,7 +1,9 @@
using Microsoft.AspNetCore.Mvc;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using WhatsappPromo.Core.Models;
using WhatsappPromo.Core.Services;
@@ -28,7 +30,6 @@ namespace WhatsappPromo.Worker.Controllers
[HttpPost]
public async Task<IActionResult> UpdateConfig([FromBody] SystemConfig config)
{
// Validar ruta
if (!string.IsNullOrEmpty(config.DownloadPath))
{
try
@@ -62,5 +63,64 @@ namespace WhatsappPromo.Worker.Controllers
await _configService.UpdateConfigAsync(config);
return Ok(new { status = "Stopping" });
}
[System.Runtime.InteropServices.DllImport("user32.dll")]
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
private const uint SWP_NOSIZE = 0x0001;
private const uint SWP_NOMOVE = 0x0002;
private const uint SWP_SHOWWINDOW = 0x0040;
[HttpGet("browse")]
public IActionResult Browse()
{
string? selectedPath = null;
var thread = new Thread(() =>
{
using (var dummyForm = new Form())
{
dummyForm.TopMost = true;
// El form debe ser "real" para que Windows lo respete
dummyForm.Width = 1; dummyForm.Height = 1;
dummyForm.Opacity = 0.05;
dummyForm.ShowInTaskbar = false;
dummyForm.FormBorderStyle = FormBorderStyle.None;
dummyForm.StartPosition = FormStartPosition.CenterScreen;
dummyForm.Show();
// Forzamos la posición al frente de todo
SetWindowPos(dummyForm.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
SetForegroundWindow(dummyForm.Handle);
dummyForm.Activate();
dummyForm.Focus();
using (var fbd = new FolderBrowserDialog())
{
fbd.Description = "Selecciona la carpeta para descargas de WhatsApp";
fbd.UseDescriptionForTitle = true;
fbd.ShowNewFolderButton = true;
if (fbd.ShowDialog(dummyForm) == DialogResult.OK)
{
selectedPath = fbd.SelectedPath;
}
}
dummyForm.Close();
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
return Ok(new { path = selectedPath });
}
}
}

View File

@@ -39,7 +39,10 @@ var app = builder.Build();
app.UseCors("AllowDashboard");
app.UseHttpsRedirection();
if (!app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
}
app.UseAuthorization();
app.MapControllers();
app.MapHub<WhatsappHub>("/whatsappHub"); // SignalR Endpoint for Frontend

View File

@@ -1,9 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OutputType>Exe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
<ItemGroup>

View File

@@ -18,7 +18,7 @@ namespace WhatsappPromo.Worker
{
private readonly ILogger<WhatsappWorker> _logger;
private readonly IBrowserService _browserService;
private readonly IConfigService _configService; // Check this dependency
private readonly IConfigService _configService;
private readonly Channel<ProcessedMedia> _mediaChannel;
private readonly IHubContext<WhatsappHub> _hubContext;
private readonly SemaphoreSlim _lock = new(1, 1);
@@ -42,29 +42,44 @@ namespace WhatsappPromo.Worker
while (!stoppingToken.IsCancellationRequested)
{
var config = await _configService.GetConfigAsync();
if (config.IsActive && !_isAutomationRunning)
try
{
_ = StartAutomationAsync(stoppingToken);
var config = await _configService.GetConfigAsync();
if (config.IsActive && !_isAutomationRunning)
{
_ = StartAutomationAsync(stoppingToken);
}
await Task.Delay(2000, stoppingToken);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error en el bucle principal del Worker");
}
await Task.Delay(2000, stoppingToken);
}
_logger.LogInformation("Cerrando servicio worker...");
await _browserService.CloseAsync();
}
private async Task StartAutomationAsync(CancellationToken token)
private async Task StartAutomationAsync(CancellationToken stoppingToken)
{
if (_isAutomationRunning) return;
try
{
await _lock.WaitAsync(token);
await _lock.WaitAsync(stoppingToken);
if (_isAutomationRunning) return;
_isAutomationRunning = true;
_logger.LogInformation("Iniciando AUTOMATIZACIÓN...");
await _hubContext.Clients.All.SendAsync("StatusUpdate", "Iniciando...", token);
await SafeSendAsync("StatusUpdate", "Iniciando...");
await _browserService.InitializeAsync();
var page = await _browserService.GetPageAsync();
@@ -72,7 +87,7 @@ namespace WhatsappPromo.Worker
// Configurar bindings
await page.ExposeFunctionAsync("onNewMessage", (Func<string, Task>)(async (string jsonMessage) =>
{
await _hubContext.Clients.All.SendAsync("LogUpdate", $"Mensaje recibido: {jsonMessage}");
await SafeSendAsync("LogUpdate", $"Mensaje recibido: {jsonMessage}");
}));
await page.ExposeFunctionAsync("onMediaDownloaded", (Func<string, string, string, Task>)(async (string phone, string base64, string mimeType) =>
@@ -81,13 +96,13 @@ namespace WhatsappPromo.Worker
{
var msg = "No se pudo detectar el número de teléfono. Archivo ignorado. Asegúrese de tener el chat abierto.";
_logger.LogWarning(msg);
await _hubContext.Clients.All.SendAsync("PersistentLog", "ERROR", msg);
await SafeSendAsync("PersistentLog", "ERROR", msg);
return;
}
var size = base64.Length;
_logger.LogInformation("Media recibido de {Phone}. Tamaño: {Size} bytes", phone, size);
await _hubContext.Clients.All.SendAsync("NewMediaReceived", new { Phone = phone, Base64 = base64, MimeType = mimeType });
await SafeSendAsync("NewMediaReceived", new { Phone = phone, Base64 = base64, MimeType = mimeType });
await _mediaChannel.Writer.WriteAsync(new ProcessedMedia
{
@@ -95,19 +110,19 @@ namespace WhatsappPromo.Worker
Base64Content = base64,
MimeType = mimeType,
Timestamp = DateTime.Now
});
}, stoppingToken);
}));
await page.EvaluateFunctionOnNewDocumentAsync(JsSnippets.MediaExtractor);
await page.ReloadAsync();
// Procesamiento en segundo plano
using var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
var config = await _configService.GetConfigAsync();
_ = ProcessMediaQueueAsync(config.DownloadPath, cts.Token);
var processTask = ProcessMediaQueueAsync(config.DownloadPath, cts.Token);
// Bucle de monitoreo
while (!token.IsCancellationRequested)
while (!stoppingToken.IsCancellationRequested)
{
var currentConfig = await _configService.GetConfigAsync();
if (!currentConfig.IsActive)
@@ -116,7 +131,7 @@ namespace WhatsappPromo.Worker
break;
}
if (new Random().Next(0, 10) > 6)
if (Random.Shared.Next(0, 10) > 6)
{
await _browserService.PerformHumanIdleActionsAsync();
}
@@ -126,42 +141,63 @@ namespace WhatsappPromo.Worker
var qrBase64 = await _browserService.GetQrCodeAsync();
if (!string.IsNullOrEmpty(qrBase64))
{
await _hubContext.Clients.All.SendAsync("QrCodeUpdate", qrBase64, token);
await _hubContext.Clients.All.SendAsync("StatusUpdate", "Esperando Login (QR)", token);
await SafeSendAsync("QrCodeUpdate", qrBase64);
await SafeSendAsync("StatusUpdate", "Esperando Login (QR)");
}
else
{
await _hubContext.Clients.All.SendAsync("QrCodeUpdate", null, token);
await _hubContext.Clients.All.SendAsync("StatusUpdate", "Conectado y Escaneando", token);
await SafeSendAsync("QrCodeUpdate", null);
await SafeSendAsync("StatusUpdate", "Conectado y Escaneando");
}
}
catch {}
if (page.IsClosed) break;
await Task.Delay(2000, token);
await Task.Delay(2000, stoppingToken);
}
cts.Cancel();
await processTask;
}
catch (OperationCanceledException) {}
catch (Exception ex)
{
_logger.LogError(ex, "Error en automatización");
await _hubContext.Clients.All.SendAsync("LogUpdate", $"ERROR: {ex.Message}");
await SafeSendAsync("LogUpdate", $"ERROR: {ex.Message}");
var conf = await _configService.GetConfigAsync();
conf.IsActive = false;
await _configService.UpdateConfigAsync(conf);
try
{
var conf = await _configService.GetConfigAsync();
conf.IsActive = false;
await _configService.UpdateConfigAsync(conf);
} catch {}
}
finally
{
await _browserService.CloseAsync();
_isAutomationRunning = false;
await _hubContext.Clients.All.SendAsync("StatusUpdate", "Detenido");
await SafeSendAsync("QrCodeUpdate", null);
await SafeSendAsync("StatusUpdate", "Detenido");
_lock.Release();
}
}
private async Task SafeSendAsync(string method, object? arg1, object? arg2 = null)
{
try
{
if (arg2 != null)
await _hubContext.Clients.All.SendAsync(method, arg1, arg2);
else
await _hubContext.Clients.All.SendAsync(method, arg1);
}
catch (Exception ex)
{
_logger.LogDebug("SignalR Send skipped: {Msg}", ex.Message);
}
}
private int _processedToday = 0;
private int _processedThisHour = 0;
private int _currentDay = -1;
@@ -173,7 +209,8 @@ namespace WhatsappPromo.Worker
if (string.IsNullOrEmpty(storagePath))
storagePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ReceivedMedia");
Directory.CreateDirectory(storagePath);
if (!Directory.Exists(storagePath))
Directory.CreateDirectory(storagePath);
try
{
@@ -183,68 +220,35 @@ namespace WhatsappPromo.Worker
{
var config = await _configService.GetConfigAsync();
// 1. Verificar y resetear contadores de tiempo
var now = DateTime.Now;
if (_currentDay != now.Day)
{
_currentDay = now.Day;
_processedToday = 0;
}
if (_currentHour != now.Hour)
{
_currentHour = now.Hour;
_processedThisHour = 0;
}
if (_currentDay != now.Day) { _currentDay = now.Day; _processedToday = 0; }
if (_currentHour != now.Hour) { _currentHour = now.Hour; _processedThisHour = 0; }
// 2. Verificar CUOTAS
while (_processedToday >= config.MaxFilesPerDay || _processedThisHour >= config.MaxFilesPerHour)
{
var waitMsg = $"Cuota alcanzada ({_processedThisHour}/{config.MaxFilesPerHour}h, {_processedToday}/{config.MaxFilesPerDay}d). Pausando procesamiento...";
_logger.LogWarning(waitMsg);
await _hubContext.Clients.All.SendAsync("LogUpdate", $"[CUOTA] {waitMsg}", token);
// Esperar 1 minuto antes de volver a chequear
await SafeSendAsync("LogUpdate", $"[CUOTA] Límite alcanzado, esperando...");
await Task.Delay(60000, token);
now = DateTime.Now;
if (_currentDay != now.Day) { _currentDay = now.Day; _processedToday = 0; }
if (_currentHour != now.Hour) { _currentHour = now.Hour; _processedThisHour = 0; }
}
// 3. Humanizar tiempo de procesamiento (Delay aleatorio)
// Si estamos cerca del límite, aumentamos el delay para "suavizar" el pico
int baseDelay = Random.Shared.Next(5000, 15000); // 5-15s base
if (_processedThisHour > config.MaxFilesPerHour * 0.8)
{
baseDelay = Random.Shared.Next(30000, 60000); // Ralentizar al final de la hora
}
await Task.Delay(baseDelay, token);
var extension = media.MimeType switch
{
"image/jpeg" => ".jpg",
"image/png" => ".png",
"video/mp4" => ".mp4",
_ => ".dat"
};
int delay = Random.Shared.Next(5000, 15000);
if (_processedThisHour > config.MaxFilesPerHour * 0.8) delay = Random.Shared.Next(30000, 60000);
await Task.Delay(delay, token);
var extension = media.MimeType switch { "image/jpeg" => ".jpg", "image/png" => ".png", "video/mp4" => ".mp4", _ => ".dat" };
var fileName = $"{media.PhoneNumber}_{media.Timestamp:yyyyMMdd_HHmmss_fff}{extension}";
var fullPath = Path.Combine(storagePath, fileName);
var bytes = Convert.FromBase64String(media.Base64Content);
await File.WriteAllBytesAsync(fullPath, bytes, token);
await File.WriteAllBytesAsync(fullPath, Convert.FromBase64String(media.Base64Content), token);
_processedToday++;
_processedThisHour++;
_logger.LogInformation("Archivo guardado: {Path}. Cuota: {H}/{D}", fullPath, _processedThisHour, _processedToday);
await _hubContext.Clients.All.SendAsync("LogUpdate", $"Archivo guardado: {fileName} (Cuota: {_processedThisHour}/{config.MaxFilesPerHour}h)", token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error procesando media");
await SafeSendAsync("LogUpdate", $"Archivo guardado: {fileName}");
}
catch (OperationCanceledException) { break; }
catch (Exception ex) { _logger.LogError(ex, "Error en cola"); }
}
}
catch (OperationCanceledException) {}