Init Commit
This commit is contained in:
@@ -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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/Backend/WhatsappPromo.Worker/Hubs/WhatsappHub.cs
Normal file
53
src/Backend/WhatsappPromo.Worker/Hubs/WhatsappHub.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/Backend/WhatsappPromo.Worker/Program.cs
Normal file
47
src/Backend/WhatsappPromo.Worker/Program.cs
Normal 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();
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/Backend/WhatsappPromo.Worker/WhatsappPromo.Worker.csproj
Normal file
19
src/Backend/WhatsappPromo.Worker/WhatsappPromo.Worker.csproj
Normal 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>
|
||||
@@ -0,0 +1,6 @@
|
||||
@WhatsappPromo.Worker_HostAddress = http://localhost:5067
|
||||
|
||||
GET {{WhatsappPromo.Worker_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
253
src/Backend/WhatsappPromo.Worker/WhatsappWorker.cs
Normal file
253
src/Backend/WhatsappPromo.Worker/WhatsappWorker.cs
Normal 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) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/Backend/WhatsappPromo.Worker/appsettings.json
Normal file
9
src/Backend/WhatsappPromo.Worker/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user