Feat: UI/UX y Procesos
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
Reference in New Issue
Block a user