Init Commit

This commit is contained in:
2026-02-09 18:19:44 -03:00
commit 7222728591
33 changed files with 5641 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
using Microsoft.AspNetCore.Mvc;
using System;
using System.IO;
using System.Threading.Tasks;
using WhatsappPromo.Core.Models;
using WhatsappPromo.Core.Services;
namespace WhatsappPromo.Worker.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ConfigController : ControllerBase
{
private readonly IConfigService _configService;
public ConfigController(IConfigService configService)
{
_configService = configService;
}
[HttpGet]
public async Task<IActionResult> GetConfig()
{
var config = await _configService.GetConfigAsync();
return Ok(config);
}
[HttpPost]
public async Task<IActionResult> UpdateConfig([FromBody] SystemConfig config)
{
// Validar ruta
if (!string.IsNullOrEmpty(config.DownloadPath))
{
try
{
Directory.CreateDirectory(config.DownloadPath);
}
catch (Exception ex)
{
return BadRequest($"Ruta inválida: {ex.Message}");
}
}
await _configService.UpdateConfigAsync(config);
return Ok(config);
}
[HttpPost("start")]
public async Task<IActionResult> Start()
{
var config = await _configService.GetConfigAsync();
config.IsActive = true;
await _configService.UpdateConfigAsync(config);
return Ok(new { status = "Starting" });
}
[HttpPost("stop")]
public async Task<IActionResult> Stop()
{
var config = await _configService.GetConfigAsync();
config.IsActive = false;
await _configService.UpdateConfigAsync(config);
return Ok(new { status = "Stopping" });
}
}
}

View File

@@ -0,0 +1,53 @@
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
using WhatsappPromo.Core.Models;
using WhatsappPromo.Core.Services;
namespace WhatsappPromo.Worker.Hubs
{
public class WhatsappHub : Hub
{
private readonly IConfigService _configService;
public WhatsappHub(IConfigService configService)
{
_configService = configService;
}
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
public async Task SendPersistentLog(string status, string message)
{
await Clients.All.SendAsync("PersistentLog", status, message);
}
// Métodos para control desde el cliente vía SignalR (alternativa a REST API)
public async Task StartAutomation()
{
var config = await _configService.GetConfigAsync();
config.IsActive = true;
await _configService.UpdateConfigAsync(config);
await Clients.All.SendAsync("ActiveStateUpdate", true);
await Clients.All.SendAsync("LogUpdate", "Comando de INICIO recibido vía SignalR.");
}
public async Task StopAutomation()
{
var config = await _configService.GetConfigAsync();
config.IsActive = false;
await _configService.UpdateConfigAsync(config);
await Clients.All.SendAsync("ActiveStateUpdate", false);
await Clients.All.SendAsync("LogUpdate", "Comando de PARADA recibido vía SignalR.");
}
public async Task UpdateConfig(SystemConfig newConfig)
{
await _configService.UpdateConfigAsync(newConfig);
await Clients.All.SendAsync("LogUpdate", "Configuración actualizada vía SignalR.");
}
}
}

View File

@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using WhatsappPromo.Core.Services;
using WhatsappPromo.Engine.Services;
using WhatsappPromo.Worker.Hubs;
using WhatsappPromo.Worker;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddSignalR();
builder.Services.AddSingleton<IBrowserService, BrowserService>();
builder.Services.AddSingleton<IConfigService, ConfigService>();
// CORS for Dashboard
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowDashboard",
policy =>
{
policy.WithOrigins("http://localhost:5173") // Vite default port
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials(); // Required for SignalR
});
});
builder.Services.AddHostedService<WhatsappWorker>(); // Run the worker in background
// Enable running as a Windows Service
builder.Host.UseWindowsService();
var app = builder.Build();
app.UseCors("AllowDashboard");
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.MapHub<WhatsappHub>("/whatsappHub"); // SignalR Endpoint for Frontend
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5067",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7195;http://localhost:5067",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WhatsappPromo.Core\WhatsappPromo.Core.csproj" />
<ProjectReference Include="..\WhatsappPromo.Engine\WhatsappPromo.Engine.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
@WhatsappPromo.Worker_HostAddress = http://localhost:5067
GET {{WhatsappPromo.Worker_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -0,0 +1,253 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using WhatsappPromo.Core.Models;
using WhatsappPromo.Core.Services;
using WhatsappPromo.Engine.Services;
using WhatsappPromo.Engine.Wapi;
using WhatsappPromo.Worker.Hubs;
namespace WhatsappPromo.Worker
{
public class WhatsappWorker : BackgroundService
{
private readonly ILogger<WhatsappWorker> _logger;
private readonly IBrowserService _browserService;
private readonly IConfigService _configService; // Check this dependency
private readonly Channel<ProcessedMedia> _mediaChannel;
private readonly IHubContext<WhatsappHub> _hubContext;
private readonly SemaphoreSlim _lock = new(1, 1);
private bool _isAutomationRunning = false;
public WhatsappWorker(ILogger<WhatsappWorker> logger,
IBrowserService browserService,
IConfigService configService,
IHubContext<WhatsappHub> hubContext)
{
_logger = logger;
_browserService = browserService;
_configService = configService;
_hubContext = hubContext;
_mediaChannel = Channel.CreateUnbounded<ProcessedMedia>();
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Servicio worker iniciado. Esperando activación manual...");
while (!stoppingToken.IsCancellationRequested)
{
var config = await _configService.GetConfigAsync();
if (config.IsActive && !_isAutomationRunning)
{
_ = StartAutomationAsync(stoppingToken);
}
await Task.Delay(2000, stoppingToken);
}
}
private async Task StartAutomationAsync(CancellationToken token)
{
if (_isAutomationRunning) return;
try
{
await _lock.WaitAsync(token);
if (_isAutomationRunning) return;
_isAutomationRunning = true;
_logger.LogInformation("Iniciando AUTOMATIZACIÓN...");
await _hubContext.Clients.All.SendAsync("StatusUpdate", "Iniciando...", token);
await _browserService.InitializeAsync();
var page = await _browserService.GetPageAsync();
// Configurar bindings
await page.ExposeFunctionAsync("onNewMessage", (Func<string, Task>)(async (string jsonMessage) =>
{
await _hubContext.Clients.All.SendAsync("LogUpdate", $"Mensaje recibido: {jsonMessage}");
}));
await page.ExposeFunctionAsync("onMediaDownloaded", (Func<string, string, string, Task>)(async (string phone, string base64, string mimeType) =>
{
if (phone == "ERROR_NO_PHONE")
{
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);
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 _mediaChannel.Writer.WriteAsync(new ProcessedMedia
{
PhoneNumber = phone,
Base64Content = base64,
MimeType = mimeType,
Timestamp = DateTime.Now
});
}));
await page.EvaluateFunctionOnNewDocumentAsync(JsSnippets.MediaExtractor);
await page.ReloadAsync();
// Procesamiento en segundo plano
using var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
var config = await _configService.GetConfigAsync();
_ = ProcessMediaQueueAsync(config.DownloadPath, cts.Token);
// Bucle de monitoreo
while (!token.IsCancellationRequested)
{
var currentConfig = await _configService.GetConfigAsync();
if (!currentConfig.IsActive)
{
_logger.LogInformation("Deteniendo AUTOMATIZACIÓN...");
break;
}
if (new Random().Next(0, 10) > 6)
{
await _browserService.PerformHumanIdleActionsAsync();
}
try
{
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);
}
else
{
await _hubContext.Clients.All.SendAsync("QrCodeUpdate", null, token);
await _hubContext.Clients.All.SendAsync("StatusUpdate", "Conectado y Escaneando", token);
}
}
catch {}
if (page.IsClosed) break;
await Task.Delay(2000, token);
}
cts.Cancel();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error en automatización");
await _hubContext.Clients.All.SendAsync("LogUpdate", $"ERROR: {ex.Message}");
var conf = await _configService.GetConfigAsync();
conf.IsActive = false;
await _configService.UpdateConfigAsync(conf);
}
finally
{
await _browserService.CloseAsync();
_isAutomationRunning = false;
await _hubContext.Clients.All.SendAsync("StatusUpdate", "Detenido");
_lock.Release();
}
}
private int _processedToday = 0;
private int _processedThisHour = 0;
private int _currentDay = -1;
private int _currentHour = -1;
private async Task ProcessMediaQueueAsync(string downloadPath, CancellationToken token)
{
var storagePath = downloadPath;
if (string.IsNullOrEmpty(storagePath))
storagePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ReceivedMedia");
Directory.CreateDirectory(storagePath);
try
{
await foreach (var media in _mediaChannel.Reader.ReadAllAsync(token))
{
try
{
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;
}
// 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 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"
};
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);
_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");
}
}
}
catch (OperationCanceledException) {}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}