Ottimizzazioni e introduzione protocollo final attack

- Rimosso binding di `MaxClicks` in `MainWindow.xaml` e aggiunto evento `TextChanged`.
- Migliorata leggibilità del codice in `MainWindow.xaml.cs` con rientri e ritorni a capo.
- Evitata duplicazione dei log per aste aggiunte in `MainWindow.xaml.cs`.
- Migliorata gestione della validità del cookie con fallback su scraping HTML.
- Aggiunta proprietà `IsAttackInProgress` in `AuctionInfo.cs` per gestire lo stato di attacco finale.
- Introdotto protocollo di "final attack" in `AuctionMonitor.cs` per puntate critiche sotto 0,5s.
- Migliorata gestione dei log e comportamento delle puntate normali in `AuctionMonitor.cs`.
- Aggiunto metodo `PlaceBidFinalAsync` in `BidooApiClient.cs` per puntate ottimizzate.
- Ridotti log ridondanti e migliorata gestione degli errori.
This commit is contained in:
Alberto Balbo
2025-10-28 23:27:53 +01:00
parent 717dc44b3b
commit cb30e1fb08
5 changed files with 229 additions and 79 deletions

View File

@@ -413,7 +413,7 @@
<UniformGrid Columns="2" Margin="0,0,0,12"> <UniformGrid Columns="2" Margin="0,0,0,12">
<StackPanel Margin="0,0,4,0"> <StackPanel Margin="0,0,4,0">
<TextBlock Text="Max Clicks (0=inf)" FontSize="10" Margin="0,0,0,4" Foreground="#999" /> <TextBlock Text="Max Clicks (0=inf)" FontSize="10" Margin="0,0,0,4" Foreground="#999" />
<TextBox x:Name="SelectedMaxClicks" Text="{Binding MaxClicks, Mode=TwoWay}" Background="#1a1a1a" Foreground="#FFF" Padding="8" FontSize="12" BorderBrush="#444" BorderThickness="1" /> <TextBox x:Name="SelectedMaxClicks" Text="0" TextChanged="SelectedMaxClicks_TextChanged" Background="#1a1a1a" Foreground="#FFF" Padding="8" FontSize="12" BorderBrush="#444" BorderThickness="1" />
</StackPanel> </StackPanel>
<StackPanel /> <StackPanel />
</UniformGrid> </UniformGrid>

View File

@@ -107,17 +107,24 @@ namespace AutoBidder
private void ExecuteGridStart(ViewModels.AuctionViewModel? vm) private void ExecuteGridStart(ViewModels.AuctionViewModel? vm)
{ {
if (vm == null) return; if (vm == null) return;
vm.IsActive = true; vm.IsPaused = false; Log($"[START] Asta avviata: {vm.Name}"); UpdateGlobalControlButtons(); vm.IsActive = true;
vm.IsPaused = false;
Log($"[START] Asta avviata: {vm.Name}");
UpdateGlobalControlButtons();
} }
private void ExecuteGridPause(ViewModels.AuctionViewModel? vm) private void ExecuteGridPause(ViewModels.AuctionViewModel? vm)
{ {
if (vm == null) return; if (vm == null) return;
vm.IsPaused = true; Log($"[PAUSA] Asta in pausa: {vm.Name}"); UpdateGlobalControlButtons(); vm.IsPaused = true;
Log($"[PAUSA] Asta in pausa: {vm.Name}");
UpdateGlobalControlButtons();
} }
private void ExecuteGridStop(ViewModels.AuctionViewModel? vm) private void ExecuteGridStop(ViewModels.AuctionViewModel? vm)
{ {
if (vm == null) return; if (vm == null) return;
vm.IsActive = false; Log($"[STOP] Asta fermata: {vm.Name}"); UpdateGlobalControlButtons(); vm.IsActive = false;
Log($"[STOP] Asta fermata: {vm.Name}");
UpdateGlobalControlButtons();
} }
private async Task ExecuteGridBidAsync(ViewModels.AuctionViewModel? vm) private async Task ExecuteGridBidAsync(ViewModels.AuctionViewModel? vm)
{ {
@@ -626,7 +633,7 @@ namespace AutoBidder
SaveAuctions(); SaveAuctions();
UpdateTotalCount(); UpdateTotalCount();
Log($"[+] Asta aggiunta: {displayName}"); // AuctionMonitor already emits a global log entry for added auctions
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -696,7 +703,7 @@ namespace AutoBidder
SaveAuctions(); SaveAuctions();
UpdateTotalCount(); UpdateTotalCount();
Log($"[+] Asta aggiunta: {name}"); // AuctionMonitor already emits a global log entry for added auctions
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -964,38 +971,50 @@ namespace AutoBidder
// Verifica validità cookie con chiamata API (eseguita in thread di background) // Verifica validità cookie con chiamata API (eseguita in thread di background)
Task.Run(() => Task.Run(() =>
{ {
try try
{ {
var success = _auctionMonitor.UpdateUserInfoAsync().GetAwaiter().GetResult(); var success = _auctionMonitor.UpdateUserInfoAsync().GetAwaiter().GetResult();
var updatedSession = _auctionMonitor.GetSession(); var updatedSession = _auctionMonitor.GetSession();
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
{ {
if (success) if (success)
{ {
RemainingBidsText.Text = (updatedSession?.RemainingBids ?? 0).ToString(); RemainingBidsText.Text = (updatedSession?.RemainingBids ?? 0).ToString();
Log($"[OK] Cookie valido - Crediti disponibili: {updatedSession?.RemainingBids ?? 0}"); Log($"[OK] Cookie valido - Crediti disponibili: {updatedSession?.RemainingBids ?? 0}");
} }
else else
{ {
Log($"[WARN] Cookie potrebbe essere scaduto - Riconfigura sessione"); // Secondary fallback: try scraping user data from HTML to ensure cookie is truly invalid
MessageBox.Show( var htmlUser = _auctionMonitor.GetUserDataFromHtmlAsync().GetAwaiter().GetResult();
"Il cookie salvato potrebbe essere scaduto.\nRiconfigura la sessione con 'Configura Sessione'.", if (htmlUser != null)
"Sessione Scaduta", {
MessageBoxButton.OK, Dispatcher.Invoke(() =>
MessageBoxImage.Warning); {
} SetUserBanner(htmlUser.Username, htmlUser.RemainingBids);
}); Log($"[OK] Dati utente rilevati via HTML - Utente: {htmlUser.Username}, Puntate residue: {htmlUser.RemainingBids}");
} });
catch (Exception ex) }
{ else
Dispatcher.Invoke(() => {
{ // Both checks failed: log a warning but do NOT show intrusive MessageBox on startup
Log($"[WARN] Errore verifica sessione: {ex.Message}"); Dispatcher.Invoke(() =>
}); {
} Log($"[WARN] Impossibile verificare sessione: cookie non valido o rete assente. Riconfigura la sessione se persistono i problemi.");
}); });
}
}
});
}
catch (Exception ex)
{
Dispatcher.Invoke(() =>
{
Log($"[WARN] Errore verifica sessione: {ex.Message}");
});
}
});
} }
else else
{ {
@@ -1107,7 +1126,7 @@ namespace AutoBidder
private void GridStartAuction_Click(object sender, RoutedEventArgs e) private void GridStartAuction_Click(object sender, RoutedEventArgs e)
{ {
if (sender is Button btn && btn.CommandParameter is AuctionViewModel vm) if (sender is Button btn && btn.CommandParameter is AuctionViewModel vm)
{ {
vm.IsActive = true; vm.IsActive = true;
vm.IsPaused = false; vm.IsPaused = false;
Log($"[START] Asta avviata: {vm.Name}"); Log($"[START] Asta avviata: {vm.Name}");

View File

@@ -46,6 +46,10 @@ namespace AutoBidder.Models
[System.Text.Json.Serialization.JsonIgnore] [System.Text.Json.Serialization.JsonIgnore]
public List<string> AuctionLog { get; set; } = new(); public List<string> AuctionLog { get; set; } = new();
// Flag runtime: indica che è in corso un'operazione di final attack per questa asta
[System.Text.Json.Serialization.JsonIgnore]
public bool IsAttackInProgress { get; set; } = false;
/// <summary> /// <summary>
/// Aggiunge una voce al log dell'asta /// Aggiunge una voce al log dell'asta
/// </summary> /// </summary>

View File

@@ -310,9 +310,10 @@ namespace AutoBidder.Services
return; return;
} }
// Log stato solo per aste attive (riduci spam) // Log stato solo per aste attive (riduci spam) - keep detailed per-auction log
if (state.Status == AuctionStatus.Running) if (state.Status == AuctionStatus.Running)
{ {
// Detailed info stays in auction log only
auction.AddLog($"API OK - Timer: {state.Timer:F2}s, EUR{state.Price:F2}, {state.LastBidder}, {state.PollingLatencyMs}ms"); auction.AddLog($"API OK - Timer: {state.Timer:F2}s, EUR{state.Price:F2}, {state.LastBidder}, {state.PollingLatencyMs}ms");
} }
else if (state.Status == AuctionStatus.Paused) else if (state.Status == AuctionStatus.Paused)
@@ -326,75 +327,92 @@ namespace AutoBidder.Services
// Aggiorna storico e bidders // Aggiorna storico e bidders
UpdateAuctionHistory(auction, state); UpdateAuctionHistory(auction, state);
// OTTIMIZZAZIONE PUNTATA // FINAL-ATTACK PROTOCOL: when the remaining timer is below our latency threshold (<= 0.5s)
// we stop the normal polling loop for this auction and send a single minimal bid request.
if (state.Status == AuctionStatus.Running && !auction.IsPaused && ShouldBid(auction, state)) if (state.Status == AuctionStatus.Running && !auction.IsPaused && ShouldBid(auction, state))
{ {
// Se timer è esattamente 0 // Use latency threshold (0.5s default) - treat as critical window
if (Math.Abs(state.Timer) < 0.001) var latencyThreshold = 0.5; // seconds
if (!auction.IsAttackInProgress && state.Timer <= latencyThreshold)
{ {
auction.AddLog($"[TRIGGER] Timer 0, attendo delay {auction.DelayMs}ms..."); // Enter attack state for this auction to avoid concurrent triggers
OnLog?.Invoke($"[BID] [{auction.AuctionId}] Timer 0, attendo delay..."); auction.IsAttackInProgress = true;
if (auction.DelayMs > 0)
await Task.Delay(auction.DelayMs, token); try
// Rileggi stato asta
var stateAfterDelay = await _apiClient.PollAuctionStateAsync(auction.AuctionId, auction.OriginalUrl, token);
if (stateAfterDelay != null && Math.Abs(stateAfterDelay.Timer) < 0.001 && stateAfterDelay.LastBidder == state.LastBidder)
{ {
auction.AddLog($"[BID] Condizioni OK dopo delay, invio puntata..."); // Log detailed info into auction log
OnLog?.Invoke($"[BID] [{auction.AuctionId}] PUNTATA dopo delay!"); auction.AddLog($"[ATTACK] Final attack: Timer {state.Timer:F3}s <= {latencyThreshold}s -> executing final bid...");
var result = await _apiClient.PlaceBidAsync(auction.AuctionId);
// Place final minimal bid (one GET with auctionID & submit=1)
var finalResult = await _apiClient.PlaceBidFinalAsync(auction.AuctionId, auction.OriginalUrl);
auction.LastClickAt = DateTime.UtcNow; auction.LastClickAt = DateTime.UtcNow;
OnBidExecuted?.Invoke(auction, result); OnBidExecuted?.Invoke(auction, finalResult);
if (result.Success)
if (finalResult.Success)
{ {
auction.AddLog($"[OK] PUNTATA OK: {result.LatencyMs}ms -> EUR{result.NewPrice:F2}"); // Success: log concise global, detailed per-auction
OnLog?.Invoke($"[OK] [{auction.AuctionId}] Puntata riuscita {result.LatencyMs}ms"); auction.AddLog($"[OK] Final bid OK: {finalResult.LatencyMs}ms -> EUR{finalResult.NewPrice:F2}");
OnLog?.Invoke($"[OK] Puntata riuscita su {auction.Name} ({auction.AuctionId}): {finalResult.LatencyMs}ms");
// After success, an expiration may be extended by server (T_exp = now + 8s)
// We simply resume normal monitoring to observe new timer
} }
else else
{ {
auction.AddLog($"[FAIL] PUNTATA FALLITA: {result.Error}"); // Failure cases: log and do not attempt immediate retries
OnLog?.Invoke($"[FAIL] [{auction.AuctionId}] ERRORE: {result.Error}"); auction.AddLog($"[FAIL] Final bid failed: {finalResult.Error}");
OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {finalResult.Error}");
} }
// Always add history entry
auction.BidHistory.Add(new BidHistory auction.BidHistory.Add(new BidHistory
{ {
Timestamp = result.Timestamp, Timestamp = finalResult.Timestamp,
EventType = result.Success ? BidEventType.MyBid : BidEventType.OpponentBid, EventType = finalResult.Success ? BidEventType.MyBid : BidEventType.OpponentBid,
Bidder = "Tu", Bidder = "Tu",
Price = stateAfterDelay.Price, Price = state.Price,
Timer = stateAfterDelay.Timer, Timer = state.Timer,
LatencyMs = result.LatencyMs, LatencyMs = finalResult.LatencyMs,
Success = result.Success, Success = finalResult.Success,
Notes = result.Success ? $"EUR{result.NewPrice:F2}" : result.Error Notes = finalResult.Success ? $"EUR{finalResult.NewPrice:F2}" : finalResult.Error
}); });
} }
else finally
{ {
auction.AddLog($"[SKIP] Puntata non inviata: timer/utente cambiato dopo delay"); auction.IsAttackInProgress = false;
} }
// Done with final attack; skip the older branch below
return;
} }
else
{ // Otherwise fallback to normal early-bid behavior
// Logica normale per timer > 0 if (Math.Abs(state.Timer) < 0.001)
auction.AddLog($"[TRIGGER] CONDIZIONI OK - Timer {state.Timer:F2}s <= {auction.TimerClick}s"); {
auction.AddLog($"[BID] Invio puntata..."); // Put detailed info into auction log but avoid noisy global log lines
OnLog?.Invoke($"[BID] [{auction.AuctionId}] PUNTATA a {state.Timer:F2}s!"); auction.AddLog($"[TRIGGER] Timer 0, attendo delay {auction.DelayMs}ms e invio puntata direttamente...");
if (auction.DelayMs > 0) if (auction.DelayMs > 0)
{
await Task.Delay(auction.DelayMs, token); await Task.Delay(auction.DelayMs, token);
}
// Direct bid - API client already writes detailed request/response into auction.AddLog via subscription
var result = await _apiClient.PlaceBidAsync(auction.AuctionId); var result = await _apiClient.PlaceBidAsync(auction.AuctionId);
auction.LastClickAt = DateTime.UtcNow; auction.LastClickAt = DateTime.UtcNow;
OnBidExecuted?.Invoke(auction, result); OnBidExecuted?.Invoke(auction, result);
// Add concise global log (single line) and keep extended details inside auction log
if (result.Success) if (result.Success)
{ {
auction.AddLog($"[OK] PUNTATA OK: {result.LatencyMs}ms -> EUR{result.NewPrice:F2}"); auction.AddLog($"[OK] PUNTATA OK: {result.LatencyMs}ms -> EUR{result.NewPrice:F2}");
OnLog?.Invoke($"[OK] [{auction.AuctionId}] Puntata riuscita {result.LatencyMs}ms"); OnLog?.Invoke($"[OK] Puntata riuscita su {auction.Name} ({auction.AuctionId}): {result.LatencyMs}ms");
} }
else else
{ {
auction.AddLog($"[FAIL] PUNTATA FALLITA: {result.Error}"); auction.AddLog($"[FAIL] PUNTATA FALLITA: {result.Error}");
OnLog?.Invoke($"[FAIL] [{auction.AuctionId}] ERRORE: {result.Error}"); OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {result.Error}");
} }
auction.BidHistory.Add(new BidHistory auction.BidHistory.Add(new BidHistory
{ {
Timestamp = result.Timestamp, Timestamp = result.Timestamp,
@@ -406,7 +424,44 @@ namespace AutoBidder.Services
Success = result.Success, Success = result.Success,
Notes = result.Success ? $"EUR{result.NewPrice:F2}" : result.Error Notes = result.Success ? $"EUR{result.NewPrice:F2}" : result.Error
}); });
} }
else
{
// Normal early-bid path: schedule immediate delay then place bid
auction.AddLog($"[TRIGGER] CONDIZIONI OK - Timer {state.Timer:F2}s <= {auction.TimerClick}s");
if (auction.DelayMs > 0)
{
await Task.Delay(auction.DelayMs, token);
}
var result = await _apiClient.PlaceBidAsync(auction.AuctionId);
auction.LastClickAt = DateTime.UtcNow;
OnBidExecuted?.Invoke(auction, result);
if (result.Success)
{
auction.AddLog($"[OK] PUNTATA OK: {result.LatencyMs}ms -> EUR{result.NewPrice:F2}");
OnLog?.Invoke($"[OK] Puntata riuscita su {auction.Name} ({auction.AuctionId}): {result.LatencyMs}ms");
}
else
{
auction.AddLog($"[FAIL] PUNTATA FALLITA: {result.Error}");
OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {result.Error}");
}
auction.BidHistory.Add(new BidHistory
{
Timestamp = result.Timestamp,
EventType = result.Success ? BidEventType.MyBid : BidEventType.OpponentBid,
Bidder = "Tu",
Price = state.Price,
Timer = state.Timer,
LatencyMs = result.LatencyMs,
Success = result.Success,
Notes = result.Success ? $"EUR{result.NewPrice:F2}" : result.Error
});
}
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -425,6 +425,78 @@ namespace AutoBidder.Services
} }
} }
/// <summary>
/// Place a minimal final bid using the simpler payload required by the final-attack protocol.
/// Uses: ?auctionID=[ID]&submit=1
/// </summary>
public async Task<BidResult> PlaceBidFinalAsync(string auctionId, string? auctionUrl = null)
{
var result = new BidResult
{
AuctionId = auctionId,
Timestamp = DateTime.UtcNow
};
try
{
Log($"[BID FINAL] Placing final bid minimal payload", auctionId);
var url = "https://it.bidoo.com/bid.php";
var payload = $"auctionID={WebUtility.UrlEncode(auctionId)}&submit=1";
Log($"[BID REQUEST] GET {url}?{payload}", auctionId);
var getUrl = url + "?" + payload;
var request = new HttpRequestMessage(HttpMethod.Get, getUrl);
var referer = !string.IsNullOrEmpty(auctionUrl) ? auctionUrl : $"https://it.bidoo.com/asta/nome-prodotto-{auctionId}";
AddAuthHeaders(request, referer, auctionId);
if (!request.Headers.Contains("Origin"))
{
request.Headers.Add("Origin", "https://it.bidoo.com");
}
var startTime = DateTime.UtcNow;
var response = await _httpClient.SendAsync(request);
result.LatencyMs = (int)(DateTime.UtcNow - startTime).TotalMilliseconds;
Log($"[BID RESPONSE] Status: {(int)response.StatusCode} {response.StatusCode}", auctionId);
Log($"[BID RESPONSE] Latency: {result.LatencyMs}ms", auctionId);
var responseText = await response.Content.ReadAsStringAsync();
result.Response = responseText;
Log($"[BID RESPONSE] Body length: {responseText.Length} bytes", auctionId);
if (!string.IsNullOrEmpty(responseText))
{
var preview = responseText.Length > 80 ? responseText.Substring(0, 80) + "..." : responseText;
Log($"[BID RESPONSE] Preview: {preview}", auctionId);
}
if (responseText.StartsWith("ok", StringComparison.OrdinalIgnoreCase))
{
result.Success = true;
var parts = responseText.Split('|');
if (parts.Length > 1 && double.TryParse(parts[1], out var priceIndex))
{
result.NewPrice = priceIndex * 0.01;
}
Log("[BID SUCCESS] ✓ Final bid placed successfully", auctionId);
}
else if (responseText.StartsWith("error", StringComparison.OrdinalIgnoreCase) || responseText.StartsWith("no|", StringComparison.OrdinalIgnoreCase))
{
result.Success = false;
var parts = responseText.Split('|');
result.Error = parts.Length > 1 ? parts[1] : responseText;
Log($"[BID ERROR] Server returned error: {result.Error}", auctionId);
}
else
{
result.Success = false;
result.Error = string.IsNullOrEmpty(responseText) ? $"HTTP {(int)response.StatusCode}" : responseText;
Log($"[BID ERROR] Unexpected response format: {result.Error}", auctionId);
}
return result;
}
catch (Exception ex)
{
result.Success = false;
result.Error = ex.Message;
Log($"[BID EXCEPTION] {ex.GetType().Name}: {ex.Message}", auctionId);
return result;
}
}
/// <summary> /// <summary>
/// Determina lo stato dell'asta basandosi su Status, LastBidder, Timer /// Determina lo stato dell'asta basandosi su Status, LastBidder, Timer
/// ///