Aggiornamento alla versione 4.0.0

* Aggiunto `BooleanToOpacityConverter` per gestire opacità dinamica.
* Introdotto nuovo sistema di timing con `BidBeforeDeadlineMs`.
* Aggiunta opzione `CheckAuctionOpenBeforeBid` per maggiore sicurezza.
* Implementato polling adattivo (10ms-1000ms) e cooldown di 800ms.
* Migliorata gestione pulsanti globali con supporto `AUTO-START`/`AUTO-STOP`.
* Fix per il tasto `Canc` e focus automatico sul `DataGrid`.
* Fix per avvio singola asta senza necessità di "Avvia Tutti".
* Aggiornati formati CSV/JSON/XML con nuovi campi.
* Migliorata gestione cookie con endpoint unico `buy_bids.php`.
* Miglioramenti UI/UX: tooltip, formattazione prezzi, feedback visivo.
* Aggiornata documentazione e changelog per la versione 4.0.0.
This commit is contained in:
Alberto Balbo
2025-11-19 18:43:40 +01:00
parent 6036896f7d
commit f017ec0364
27 changed files with 2281 additions and 1069 deletions
+174 -263
View File
@@ -9,7 +9,7 @@ namespace AutoBidder.Services
{
/// <summary>
/// Servizio centrale per monitoraggio aste
/// Solo HTTP, nessuna modalità, browser o multi-click
/// Sistema di timing ottimizzato: punta solo se necessario, poco prima della scadenza
/// </summary>
public class AuctionMonitor
{
@@ -21,13 +21,12 @@ namespace AutoBidder.Services
public event Action<AuctionState>? OnAuctionUpdated;
public event Action<AuctionInfo, BidResult>? OnBidExecuted;
public event Action<string>? OnLog;
public event Action<string>? OnResetCountChanged; // Notifica cambio contatore reset
public event Action<string>? OnResetCountChanged;
public AuctionMonitor()
{
_apiClient = new BidooApiClient();
// Subscribe to detailed per-auction logs from API client
_apiClient.OnAuctionLog += (auctionId, message) =>
{
try
@@ -45,59 +44,38 @@ namespace AutoBidder.Services
};
}
/// <summary>
/// Inizializza sessione con token di autenticazione
/// </summary>
public void InitializeSession(string authToken, string username)
{
_apiClient.InitializeSession(authToken, username);
OnLog?.Invoke($"[OK] Sessione configurata per: {username}");
}
/// <summary>
/// Inizializza sessione con cookie (fallback legacy)
/// </summary>
public void InitializeSessionWithCookie(string cookieString, string username)
{
_apiClient.InitializeSessionWithCookie(cookieString, username);
OnLog?.Invoke($"[OK] Sessione configurata (cookie) per: {username}");
}
/// <summary>
/// Aggiorna info utente (puntate rimanenti)
/// </summary>
public async Task<bool> UpdateUserInfoAsync()
{
return await _apiClient.UpdateUserInfoAsync();
}
/// <summary>
/// Ottieni sessione corrente
/// </summary>
public BidooSession GetSession()
{
return _apiClient.GetSession();
}
/// <summary>
/// Ottiene dati utente (nome, puntate residue, saldo, id) tramite chiamata AJAX leggera
/// </summary>
public async Task<UserData?> GetUserDataAsync()
{
return await _apiClient.GetUserDataAsync();
}
/// <summary>
/// Ottiene info banner utente (aste vinte, bonus, ecc.) tramite chiamata AJAX
/// </summary>
public async Task<UserBannerInfo?> GetUserBannerInfoAsync()
{
return await _apiClient.GetUserBannerInfoAsync();
}
/// <summary>
/// Estrae nome utente e puntate residue dall'HTML di bids_history.php
/// </summary>
public async Task<UserData?> GetUserDataFromHtmlAsync()
{
return await _apiClient.GetUserDataFromHtmlAsync();
@@ -167,9 +145,6 @@ namespace AutoBidder.Services
List<AuctionInfo> activeAuctions;
lock (_auctions)
{
// Filtra aste che devono ancora essere monitorate
// Include aste attive anche se messe in pausa: vogliamo continuare a monitorarle
// ma non inviare puntate per quelle in pausa.
activeAuctions = _auctions.Where(a =>
a.IsActive &&
!IsAuctionTerminated(a)
@@ -189,16 +164,14 @@ namespace AutoBidder.Services
// Ottimizzazione polling per aste in pausa
bool anyPaused = false;
DateTime now = DateTime.Now;
int pauseDelayMs = 1000; // default
int pauseDelayMs = 1000;
foreach (var a in activeAuctions)
{
if (a.IsPaused)
{
anyPaused = true;
// Se tra le 00:00 e le 09:55 polling ogni 60s
if (now.Hour < 9 || (now.Hour == 9 && now.Minute < 55))
pauseDelayMs = 60000;
// Negli ultimi 5 minuti prima delle 10 polling ogni 5s
else if (now.Hour == 9 && now.Minute >= 55)
pauseDelayMs = 5000;
}
@@ -218,13 +191,14 @@ namespace AutoBidder.Services
int delayMs = lowestTimer switch
{
< 1 => 5, // Iper-veloce: polling ogni 5ms (0-1s rimanenti)
< 2 => 20, // Ultra-veloce: polling ogni 20ms (1-2s)
< 3 => 50, // Molto veloce: polling ogni 50ms (2-3s)
< 5 => 100, // Veloce: polling ogni 100ms (3-5s)
< 10 => 200, // Medio: polling ogni 200ms (5-10s)
< 30 => 500, // Lento: polling ogni 500ms (10-30s)
_ => 1000 // Molto lento: polling ogni 1s (>30s)
< 0.5 => 10, // Ultra-critico: polling ogni 10ms
< 1 => 20, // Iper-veloce: polling ogni 20ms
< 2 => 50, // Ultra-veloce: polling ogni 50ms
< 3 => 100, // Molto veloce: polling ogni 100ms
< 5 => 150, // Veloce: polling ogni 150ms
< 10 => 300, // Medio: polling ogni 300ms
< 30 => 500, // Lento: polling ogni 500ms
_ => 1000 // Molto lento: polling ogni 1s
};
await Task.Delay(delayMs, token);
@@ -241,16 +215,11 @@ namespace AutoBidder.Services
}
}
/// <summary>
/// Verifica se un'asta è terminata e non deve più essere monitorata
/// </summary>
private bool IsAuctionTerminated(AuctionInfo auction)
{
// Se l'ultima entry nello storico indica uno stato finale, ferma polling
var lastHistory = auction.BidHistory.LastOrDefault();
if (lastHistory != null)
{
// Controlla se c'è una nota che indica fine asta
if (lastHistory.Notes != null &&
(lastHistory.Notes.Contains("VINTA") ||
lastHistory.Notes.Contains("Persa") ||
@@ -267,7 +236,6 @@ namespace AutoBidder.Services
{
try
{
// Poll tramite API Bidoo (passa anche l'URL originale per referer corretto)
var state = await _apiClient.PollAuctionStateAsync(auction.AuctionId, auction.OriginalUrl, token);
if (state == null)
@@ -277,10 +245,8 @@ namespace AutoBidder.Services
return;
}
// Aggiorna la latenza per la DataGrid
auction.PollingLatencyMs = state.PollingLatencyMs;
// Se l'asta è terminata, segnala e disattiva polling
if (state.Status == AuctionStatus.EndedWon ||
state.Status == AuctionStatus.EndedLost ||
state.Status == AuctionStatus.Closed)
@@ -288,13 +254,10 @@ namespace AutoBidder.Services
string statusMsg = state.Status == AuctionStatus.EndedWon ? "VINTA" :
state.Status == AuctionStatus.EndedLost ? "Persa" : "Chiusa";
// Mark auction inactive immediately to stop further polling
auction.IsActive = false;
auction.AddLog($"[ASTA TERMINATA] {statusMsg}");
OnLog?.Invoke($"[FINE] [{auction.AuctionId}] Asta {statusMsg} - Polling fermato");
// Aggiungi entry nello storico per marcare come terminata
auction.BidHistory.Add(new BidHistory
{
Timestamp = DateTime.UtcNow,
@@ -305,222 +268,148 @@ namespace AutoBidder.Services
Notes = $"Asta {statusMsg}"
});
// Notifica UI e fermati
OnAuctionUpdated?.Invoke(state);
return;
}
// Log stato solo per aste attive (riduci spam) - keep detailed per-auction log
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");
// Log RIMOSSO per ridurre verbosità - polling continuo non necessita log
// Solo eventi importanti (bid, reset, errori) vengono loggati
}
else if (state.Status == AuctionStatus.Paused)
{
auction.AddLog($"[PAUSA] Asta in pausa - Timer: {state.Timer:F2}s, EUR{state.Price:F2}");
// Log solo primo cambio stato, non ad ogni polling
var lastLog = auction.AuctionLog.LastOrDefault();
if (lastLog == null || !lastLog.Contains("[PAUSA]"))
{
auction.AddLog($"[PAUSA] Asta in pausa - Timer: {state.Timer:F3}s, EUR{state.Price:F2}");
}
}
// Notifica aggiornamento UI
OnAuctionUpdated?.Invoke(state);
// Aggiorna storico e bidders
UpdateAuctionHistory(auction, state);
// 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))
// NUOVA LOGICA: Punta solo se siamo vicini alla deadline E nessun altro ha appena puntato
if (state.Status == AuctionStatus.Running && !auction.IsPaused && !auction.IsAttackInProgress)
{
// Use latency threshold (0.5s default) - treat as critical window
var latencyThreshold = 0.5; // seconds
if (!auction.IsAttackInProgress && state.Timer <= latencyThreshold)
if (ShouldBid(auction, state))
{
// Quick re-poll strategy: perform a couple of fast re-polls to confirm that the timer
// is still in the critical window and that the lastBidder did not change.
auction.IsAttackInProgress = true;
AuctionState? lastConfirmedState = state;
try
{
auction.AddLog($"[ATTACK] Candidate final attack: Timer {state.Timer:F3}s <= {latencyThreshold}s. Performing quick re-polls to confirm...");
int attempts = 2;
for (int i = 0; i < attempts; i++)
{
try
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
// small timeout for quick verification
cts.CancelAfter(TimeSpan.FromMilliseconds(400));
var quickState = await _apiClient.PollAuctionStateAsync(auction.AuctionId, auction.OriginalUrl, cts.Token);
if (quickState != null)
{
auction.AddLog($"[ATTACK] Quick re-poll #{i + 1}: Timer {quickState.Timer:F3}s, Bidder: {quickState.LastBidder}");
// If bidder changed to someone else, abort
if (!string.Equals(quickState.LastBidder, state.LastBidder, StringComparison.OrdinalIgnoreCase))
{
auction.AddLog($"[ATTACK] Aborting final attack: last bidder changed from '{state.LastBidder}' to '{quickState.LastBidder}'");
return;
}
// If timer increased above threshold, abort
if (quickState.Timer > latencyThreshold)
{
auction.AddLog($"[ATTACK] Aborting final attack: quickState.Timer {quickState.Timer:F3}s > threshold {latencyThreshold}s");
return;
}
// Confirmed critical window
lastConfirmedState = quickState;
break; // proceed to place bid
}
else
{
auction.AddLog($"[ATTACK] Quick re-poll #{i + 1} returned no data (timeout/error).\n");
}
}
catch (Exception exQuick)
{
auction.AddLog($"[ATTACK] Quick re-poll #{i + 1} exception: {exQuick.GetType().Name} - {exQuick.Message}");
}
// tiny delay between attempts
await Task.Delay(30, token);
}
// If no quickState confirmed but initial state indicated critical window, proceed but warn
if (lastConfirmedState == null)
{
auction.AddLog("[ATTACK] No quick re-poll confirmed state. Proceeding with final bid based on initial observation (risk of false positive).");
}
// Place final bid using the same request format as manual bids to mimic manual behavior
auction.AddLog($"[ATTACK] Executing final bid (using manual-format payload) for {auction.AuctionId} (confirmed: { (lastConfirmedState != null) })...");
var finalResult = await _apiClient.PlaceBidAsync(auction.AuctionId, auction.OriginalUrl);
auction.LastClickAt = DateTime.UtcNow;
OnBidExecuted?.Invoke(auction, finalResult);
if (finalResult.Success)
{
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");
}
else
{
auction.AddLog($"[FAIL] Final bid failed: {finalResult.Error}");
OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {finalResult.Error}");
}
auction.BidHistory.Add(new BidHistory
{
Timestamp = finalResult.Timestamp,
EventType = finalResult.Success ? BidEventType.MyBid : BidEventType.OpponentBid,
Bidder = "Tu",
Price = state.Price,
Timer = state.Timer,
LatencyMs = finalResult.LatencyMs,
Success = finalResult.Success,
Notes = finalResult.Success ? $"EUR{finalResult.NewPrice:F2}" : finalResult.Error
});
}
finally
{
auction.IsAttackInProgress = false;
}
return;
await ExecuteBidStrategy(auction, state, token);
}
// Otherwise fallback to normal early-bid behavior
if (Math.Abs(state.Timer) < 0.001)
{
// Put detailed info into auction log but avoid noisy global log lines
auction.AddLog($"[TRIGGER] Timer 0, attendo delay {auction.DelayMs}ms e invio puntata direttamente...");
if (auction.DelayMs > 0)
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);
auction.LastClickAt = DateTime.UtcNow;
OnBidExecuted?.Invoke(auction, result);
// Add concise global log (single line) and keep extended details inside auction log
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
});
}
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)
{
auction.AddLog($"[EXCEPTION] ERRORE: {ex.Message}");
auction.AddLog($"[EXCEPTION] {ex.Message}");
OnLog?.Invoke($"[EXCEPTION] [{auction.AuctionId}] {ex.Message}");
}
}
/// <summary>
/// Strategia di puntata ottimizzata: punta solo quando necessario
/// </summary>
private async Task ExecuteBidStrategy(AuctionInfo auction, AuctionState state, CancellationToken token)
{
// Calcola il tempo rimanente in millisecondi
double timerMs = state.Timer * 1000;
// Se siamo nella finestra di puntata (timer <= BidBeforeDeadlineMs)
if (timerMs <= auction.BidBeforeDeadlineMs)
{
auction.IsAttackInProgress = true;
try
{
auction.AddLog($"[STRATEGIA] Finestra di puntata raggiunta: {timerMs:F0}ms <= {auction.BidBeforeDeadlineMs}ms");
// Controlla se qualcun altro ha puntato di recente
var lastBidTime = GetLastBidTime(auction, state.LastBidder);
if (lastBidTime.HasValue)
{
var timeSinceLastBid = DateTime.UtcNow - lastBidTime.Value;
if (timeSinceLastBid.TotalMilliseconds < 500)
{
auction.AddLog($"[STRATEGIA] Puntata recente di {state.LastBidder} ({timeSinceLastBid.TotalMilliseconds:F0}ms fa), attendo...");
return;
}
}
// Esegui la puntata
await ExecuteBid(auction, state, token);
}
finally
{
auction.IsAttackInProgress = false;
}
}
}
/// <summary>
/// Esegue la puntata con verifica opzionale dello stato dell'asta
/// </summary>
private async Task ExecuteBid(AuctionInfo auction, AuctionState state, CancellationToken token)
{
try
{
// Se richiesto, verifica prima che l'asta sia ancora aperta
if (auction.CheckAuctionOpenBeforeBid)
{
auction.AddLog("[PRE-CHECK] Verifica stato asta...");
var preCheckState = await _apiClient.PollAuctionStateAsync(auction.AuctionId, auction.OriginalUrl, token);
if (preCheckState == null)
{
auction.AddLog("[PRE-CHECK] FALLITO: Nessuna risposta");
return;
}
if (preCheckState.Status != AuctionStatus.Running)
{
auction.AddLog($"[PRE-CHECK] ABORTITO: Asta non running (status: {preCheckState.Status})");
return;
}
auction.AddLog($"[PRE-CHECK] OK - Timer: {preCheckState.Timer:F3}s");
}
// Esegui la puntata
var result = await _apiClient.PlaceBidAsync(auction.AuctionId, auction.OriginalUrl);
auction.LastClickAt = DateTime.UtcNow;
OnBidExecuted?.Invoke(auction, result);
if (result.Success)
{
auction.AddLog($"[BID OK] Latenza: {result.LatencyMs}ms -> EUR{result.NewPrice:F2}");
OnLog?.Invoke($"[OK] Puntata riuscita su {auction.Name} ({auction.AuctionId}): {result.LatencyMs}ms");
}
else
{
auction.AddLog($"[BID FAIL] {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)
{
auction.AddLog($"[BID EXCEPTION] {ex.Message}");
OnLog?.Invoke($"[BID EXCEPTION] [{auction.AuctionId}] {ex.Message}");
}
}
private bool ShouldBid(AuctionInfo auction, AuctionState state)
{
// Timer check
if (state.Timer > auction.TimerClick)
return false;
// Price check
if (auction.MinPrice > 0 && state.Price < auction.MinPrice)
return false;
@@ -528,54 +417,77 @@ namespace AutoBidder.Services
if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice)
return false;
// Cooldown check (evita click multipli ravvicinati)
// Reset count check
if (auction.MinResets > 0 && auction.ResetCount < auction.MinResets)
return false;
if (auction.MaxResets > 0 && auction.ResetCount >= auction.MaxResets)
return false;
// Max clicks check
int myBidsCount = auction.BidHistory.Count(b => b.EventType == BidEventType.MyBid);
if (auction.MaxClicks > 0 && myBidsCount >= auction.MaxClicks)
return false;
// Cooldown check (evita puntate multiple ravvicinate)
if (auction.LastClickAt.HasValue)
{
var timeSinceLastClick = DateTime.UtcNow - auction.LastClickAt.Value;
if (timeSinceLastClick.TotalSeconds < 1)
if (timeSinceLastClick.TotalMilliseconds < 800)
return false;
}
return true;
}
private DateTime? GetLastBidTime(AuctionInfo auction, string bidder)
{
if (string.IsNullOrEmpty(bidder))
return null;
if (auction.BidderStats.TryGetValue(bidder, out var info))
{
return info.LastBidTime;
}
return null;
}
private void UpdateAuctionHistory(AuctionInfo auction, AuctionState state)
{
// Traccia l'ultima puntata per rilevare cambi
var lastHistory = auction.BidHistory.LastOrDefault();
var lastPrice = lastHistory?.Price ?? 0;
var lastBidder = lastHistory?.Bidder;
bool isNewBid = false;
// Nuova puntata = CAMBIO PREZZO (più affidabile)
// Ogni incremento di prezzo significa che qualcuno ha puntato
// Nuova puntata = CAMBIO PREZZO
if (state.Price > lastPrice && state.Price > 0)
{
isNewBid = true;
}
// Fallback: cambio utente (se il prezzo è uguale ma l'utente cambia)
if (!isNewBid &&
!string.IsNullOrEmpty(lastBidder) &&
!string.IsNullOrEmpty(state.LastBidder) &&
!lastBidder.Equals(state.LastBidder, StringComparison.OrdinalIgnoreCase))
// Fallback: cambio utente
if (!isNewBid &&
!string.IsNullOrEmpty(lastBidder) &&
!string.IsNullOrEmpty(state.LastBidder) &&
!lastBidder.Equals(state.LastBidder, StringComparison.OrdinalIgnoreCase))
{
isNewBid = true;
}
if (isNewBid)
{
auction.ResetCount++;
auction.BidHistory.Add(new BidHistory
{
isNewBid = true;
}
if (isNewBid)
{
auction.ResetCount++;
auction.BidHistory.Add(new BidHistory
{
Timestamp = DateTime.UtcNow,
EventType = BidEventType.Reset,
Bidder = state.LastBidder,
Price = state.Price,
Timer = state.Timer,
Notes = $"Puntata: EUR{state.Price:F2}"
});
Timestamp = DateTime.UtcNow,
EventType = BidEventType.Reset,
Bidder = state.LastBidder,
Price = state.Price,
Timer = state.Timer,
Notes = $"Puntata: EUR{state.Price:F2}"
});
// Aggiorna statistiche bidder
if (!string.IsNullOrEmpty(state.LastBidder))
@@ -592,7 +504,6 @@ namespace AutoBidder.Services
auction.BidderStats[state.LastBidder].LastBidTime = DateTime.UtcNow;
}
// Notifica cambio reset count per aggiornare UI
OnResetCountChanged?.Invoke(auction.AuctionId);
}
}
+226 -147
View File
@@ -90,7 +90,7 @@ namespace AutoBidder.Services
if (!string.IsNullOrWhiteSpace(_session.CookieString))
{
request.Headers.Add("Cookie", _session.CookieString);
Log("[AUTH] Using full cookie string", auctionId);
// Log rimosso per ridurre verbosità
}
else
{
@@ -98,7 +98,6 @@ namespace AutoBidder.Services
}
// 2. HEADERS BROWSER-LIKE (anti-detection)
// User-Agent realistico (Chrome su Windows)
request.Headers.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36");
@@ -131,7 +130,7 @@ namespace AutoBidder.Services
request.Headers.Add("Referer", "https://it.bidoo.com/");
}
Log("[HEADERS] Browser-like headers added (anti-bot)", auctionId);
// Log rimosso per ridurre verbosità - headers sempre aggiunti
}
/// <summary>
@@ -321,7 +320,8 @@ namespace AutoBidder.Services
{
try
{
var url = "https://it.bidoo.com/ajax/get_auction_bids_info_banner.php";
// OTTIMIZZATO: Usa buy_bids.php che contiene tutti i dati in un'unica chiamata
var url = "https://it.bidoo.com/buy_bids.php";
Log($"[USER INFO REQUEST] GET {url}");
@@ -332,24 +332,133 @@ namespace AutoBidder.Services
var response = await _httpClient.SendAsync(request);
var latency = (int)(DateTime.UtcNow - startTime).TotalMilliseconds;
Log($"[USER INFO RESPONSE] Status: {(int)response.StatusCode} {response.StatusCode}");
Log($"[USER INFO RESPONSE] Latency: {latency}ms");
Log($"[USER INFO RESPONSE] Status: {(int)response.StatusCode} {response.StatusCode}, Latency: {latency}ms");
var responseText = await response.Content.ReadAsStringAsync();
Log($"[USER INFO RESPONSE] Body length: {responseText.Length}");
if (!response.IsSuccessStatusCode)
{
Log($"[USER INFO ERROR] HTTP {response.StatusCode}");
Log($"[USER INFO ERROR] HTTP {response.StatusCode} - Cookie potrebbe essere scaduto o non valido");
return false;
}
var html = await response.Content.ReadAsStringAsync();
Log($"[USER INFO RESPONSE] Body length: {html.Length} chars");
// Verifica se la risposta contiene HTML valido
if (html.Length < 100)
{
Log($"[USER INFO ERROR] Risposta troppo corta ({html.Length} chars) - possibile errore server");
return false;
}
_session.LastAccountUpdate = DateTime.UtcNow;
return true;
// Parsa l'oggetto JavaScript BidooCnf.userObj
bool foundData = false;
// Estrai ID utente: BidooCnf.userObj.id = '6707664';
var idMatch = System.Text.RegularExpressions.Regex.Match(html, @"BidooCnf\.userObj\.id\s*=\s*'([^']+)'");
if (idMatch.Success)
{
if (int.TryParse(idMatch.Groups[1].Value, out int userId))
{
_session.UserId = userId;
Log($"[USER INFO PARSED] User ID: {userId}");
foundData = true;
}
}
else
{
Log($"[USER INFO WARN] User ID non trovato");
}
// Estrai email: BidooCnf.userObj.email = 'albertobalbo96@gmail.com';
var emailMatch = System.Text.RegularExpressions.Regex.Match(html, @"BidooCnf\.userObj\.email\s*=\s*'([^']+)'");
if (emailMatch.Success)
{
_session.Email = emailMatch.Groups[1].Value;
Log($"[USER INFO PARSED] Email: {_session.Email}");
foundData = true;
}
else
{
Log($"[USER INFO WARN] Email non trovata");
}
// Estrai username: BidooCnf.userObj.username = 'sirbietole23';
var usernameMatch = System.Text.RegularExpressions.Regex.Match(html, @"BidooCnf\.userObj\.username\s*=\s*'([^']+)'");
if (usernameMatch.Success)
{
_session.Username = usernameMatch.Groups[1].Value;
Log($"[USER INFO PARSED] Username: {_session.Username}");
foundData = true;
}
else
{
Log($"[USER INFO WARN] Username non trovato");
}
// Estrai telefono: BidooCnf.userObj.phone = '00393665920653';
var phoneMatch = System.Text.RegularExpressions.Regex.Match(html, @"BidooCnf\.userObj\.phone\s*=\s*'([^']+)'");
if (phoneMatch.Success)
{
_session.Phone = phoneMatch.Groups[1].Value;
Log($"[USER INFO PARSED] Phone: {_session.Phone}");
foundData = true;
}
// Estrai puntate residue dall'HTML: <span id="divSaldoBidMobile">206</span>
var bidsPatterns = new[]
{
@"<span[^>]*id=""divSaldoBidMobile""[^>]*>(\d+)</span>",
@"<span[^>]*id=""divSaldoBidBottom""[^>]*>(\d+)</span>",
@"<span[^>]*id=""divSaldoBid""[^>]*>(\d+)</span>"
};
bool foundBids = false;
foreach (var pattern in bidsPatterns)
{
var bidsMatch = System.Text.RegularExpressions.Regex.Match(html, pattern);
if (bidsMatch.Success && int.TryParse(bidsMatch.Groups[1].Value, out int bids))
{
_session.RemainingBids = bids;
Log($"[USER INFO PARSED] Remaining bids: {bids}");
foundData = true;
foundBids = true;
break;
}
}
if (!foundBids)
{
Log($"[USER INFO WARN] Puntate residue non trovate");
}
// Estrai credito Bidoo Shop: <span class="cbstotal">15.00</span>
var creditMatch = System.Text.RegularExpressions.Regex.Match(html, @"<span[^>]*class=""cbstotal""[^>]*>([\d.]+)</span>");
if (creditMatch.Success && double.TryParse(creditMatch.Groups[1].Value, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double credit))
{
_session.ShopCredit = credit;
Log($"[USER INFO PARSED] Shop credit: €{credit:F2}");
foundData = true;
}
if (foundData)
{
_session.LastAccountUpdate = DateTime.UtcNow;
Log($"[USER INFO SUCCESS] Dati estratti correttamente da buy_bids.php");
return true;
}
else
{
Log($"[USER INFO ERROR] NESSUN dato trovato nell'HTML - cookie probabilmente non valido");
// Salva snippet per debug
var htmlSnippet = html.Substring(0, Math.Min(500, html.Length)).Replace("\n", " ").Replace("\r", "");
Log($"[USER INFO DEBUG] Snippet HTML: {htmlSnippet}...");
return false;
}
}
catch (Exception ex)
{
Log($"[USER INFO EXCEPTION] {ex.GetType().Name}: {ex.Message}");
Log($"[USER INFO EXCEPTION] StackTrace: {ex.StackTrace}");
return false;
}
}
@@ -363,10 +472,8 @@ namespace AutoBidder.Services
};
try
{
Log($"[BID] Placing bid via direct GET to bid.php", auctionId);
var url = "https://it.bidoo.com/bid.php";
var payload = $"AID={WebUtility.UrlEncode(auctionId)}&sup=0&shock=0";
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}";
@@ -375,19 +482,14 @@ namespace AutoBidder.Services
{
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;
@@ -397,50 +499,43 @@ namespace AutoBidder.Services
result.NewPrice = priceIndex * 0.01;
}
// Parse remaining bids from response if present: ok|324|...
var parts2 = responseText.Split('|');
if (parts2.Length > 1 && int.TryParse(parts2[1], out var remaining))
if (parts.Length > 1 && int.TryParse(parts[1], out var remaining))
{
_session.RemainingBids = remaining;
Log($"[BID SUCCESS] ✓ Bid placed successfully - Remaining bids: {remaining}", auctionId);
Log($"[BID SUCCESS] Puntata piazzata - Crediti residui: {remaining}", auctionId);
}
else
{
Log("[BID SUCCESS] ✓ Bid placed successfully", auctionId);
Log("[BID SUCCESS] Puntata piazzata", 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);
var errorMsg = parts.Length > 1 ? parts[1] : responseText;
// Pulisci messaggio errore da HTML
if (errorMsg.Contains("<br>") || errorMsg.Contains("<a"))
{
var cleanMsg = System.Text.RegularExpressions.Regex.Replace(errorMsg, "<[^>]+>", "");
errorMsg = cleanMsg.Split(new[] { "<br>", "\n" }, StringSplitOptions.RemoveEmptyEntries)[0].Trim();
}
result.Error = errorMsg;
Log($"[BID ERROR] {errorMsg}", auctionId);
}
else if (responseText.Contains("alive"))
{
result.Success = false;
result.Error = "Keep-alive response (not a bid response)";
Log($"[BID WARN] Received keep-alive instead of bid confirmation", auctionId);
Log($"[BID WARN] Ricevuto keep-alive invece di conferma bid", auctionId);
}
else
{
result.Success = false;
result.Error = string.IsNullOrEmpty(responseText) ? $"HTTP {(int)response.StatusCode}" : responseText;
Log($"[BID ERROR] Unexpected response format: {result.Error}", auctionId);
}
// If initial attempt failed or returned unexpected format, try alternate payload once
if (!result.Success)
{
Log($"[BID] Initial attempt failed for {auctionId}. Trying alternate payload (auctionID=...)\n", auctionId);
try
{
var alt = await PlaceBidFinalAsync(auctionId, auctionUrl);
// Merge alt result into result (prefer alt)
return alt;
}
catch (Exception exAlt)
{
Log($"[BID] Alternate attempt threw: {exAlt.GetType().Name} - {exAlt.Message}", auctionId);
}
result.Error = string.IsNullOrEmpty(responseText) ? $"HTTP {(int)response.StatusCode}" : "Formato risposta inatteso";
Log($"[BID ERROR] Formato risposta inatteso: HTTP {(int)response.StatusCode}", auctionId);
}
return result;
@@ -449,95 +544,7 @@ namespace AutoBidder.Services
{
result.Success = false;
result.Error = ex.Message;
// Generic global-style hint (via auction log event, AuctionMonitor will emit concise global message)
Log($"[BID EXCEPTION] Errore durante il piazzamento della puntata: {ex.GetType().Name}. Vedere log asta per dettagli.", auctionId);
// Detailed per-auction info
var sb = new System.Text.StringBuilder();
sb.AppendLine("[BID EXCEPTION DETAILED]");
sb.AppendLine(ex.ToString());
sb.AppendLine($"RequestUri: { (auctionUrl ?? "https://it.bidoo.com/bid.php") }");
sb.AppendLine($"HttpClient.Timeout: {_httpClient.Timeout.TotalSeconds}s");
sb.AppendLine($"CookiePresent: {!string.IsNullOrEmpty(_session.CookieString)} (length: {(_session.CookieString?.Length ?? 0)})");
Log(sb.ToString(), auctionId);
return result;
}
}
/// <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] Errore durante il piazzamento della puntata (final): {ex.GetType().Name}. Vedere log asta per dettagli.", auctionId);
var sb = new System.Text.StringBuilder();
sb.AppendLine("[BID FINAL EXCEPTION DETAILED]");
sb.AppendLine(ex.ToString());
sb.AppendLine($"RequestUri: { (auctionUrl ?? "https://it.bidoo.com/bid.php") }");
sb.AppendLine($"HttpClient.Timeout: {_httpClient.Timeout.TotalSeconds}s");
sb.AppendLine($"CookiePresent: {!string.IsNullOrEmpty(_session.CookieString)} (length: {(_session.CookieString?.Length ?? 0)})");
Log(sb.ToString(), auctionId);
Log($"[BID EXCEPTION] {ex.GetType().Name}: {ex.Message}", auctionId);
return result;
}
}
@@ -607,7 +614,7 @@ namespace AutoBidder.Services
}
/// <summary>
/// Ottiene dati utente (nome, puntate residue, saldo, id) tramite chiamata AJAX leggera
/// OTTIMIZZATO: Estrae ID utente, username e saldo disponibile tramite chiamata AJAX leggera
/// </summary>
public async Task<UserData?> GetUserDataAsync()
{
@@ -708,31 +715,103 @@ namespace AutoBidder.Services
{
var url = "https://it.bidoo.com/bids_history.php";
Log($"[USER HTML REQUEST] GET {url}");
var request = new HttpRequestMessage(HttpMethod.Get, url);
AddAuthHeaders(request, "https://it.bidoo.com/");
var response = await _httpClient.SendAsync(request);
Log($"[USER HTML RESPONSE] Status: {(int)response.StatusCode} {response.StatusCode}");
if (!response.IsSuccessStatusCode)
{
Log($"[USER HTML ERROR] HTTP {response.StatusCode} - Cookie potrebbe essere scaduto");
return null;
}
var html = await response.Content.ReadAsStringAsync();
Log($"[USER HTML RESPONSE] Body length: {html.Length}");
Log($"[USER HTML RESPONSE] Body length: {html.Length} chars");
// Verifica se la risposta contiene HTML valido
if (html.Length < 100 || !html.Contains("<!DOCTYPE") && !html.Contains("<html"))
{
Log($"[USER HTML ERROR] Risposta non contiene HTML valido (possibile redirect o errore)");
return null;
}
var userData = new UserData();
// Estrai nome utente
var userMatch = System.Text.RegularExpressions.Regex.Match(html, @"<a class=""pers_lnk""[^>]*>([^<]+)</a>");
if (userMatch.Success)
bool foundUsername = false;
bool foundBids = false;
// Estrai nome utente - pattern multipli per maggiore robustezza
var usernamePatterns = new[]
{
userData.Username = userMatch.Groups[1].Value.Trim();
}
// Estrai puntate residue
var bidsMatch = System.Text.RegularExpressions.Regex.Match(html, @"<span id=""divSaldoBidBottom""[^>]*>(\d+)</span>");
if (bidsMatch.Success && int.TryParse(bidsMatch.Groups[1].Value, out int bids))
@"<a class=""pers_lnk""[^>]*>([^<]+)</a>",
@"<a[^>]*class=""pers_lnk""[^>]*>([^<]+)</a>",
@"<span[^>]*class=""username""[^>]*>([^<]+)</span>",
@"BidooCnf\.userObj\.username\s*=\s*'([^']+)'"
};
foreach (var pattern in usernamePatterns)
{
userData.RemainingBids = bids;
var userMatch = System.Text.RegularExpressions.Regex.Match(html, pattern);
if (userMatch.Success)
{
userData.Username = userMatch.Groups[1].Value.Trim();
foundUsername = true;
Log($"[USER HTML PARSED] Username trovato: {userData.Username}");
break;
}
}
if (!string.IsNullOrEmpty(userData.Username) && userData.RemainingBids > 0)
if (!foundUsername)
{
Log($"[USER HTML ERROR] Username NON trovato nell'HTML");
// Salva un estratto dell'HTML per debug (primi 500 caratteri)
var htmlSnippet = html.Substring(0, Math.Min(500, html.Length)).Replace("\n", " ").Replace("\r", "");
Log($"[USER HTML DEBUG] Snippet HTML: {htmlSnippet}...");
}
// Estrai puntate residue - pattern multipli
var bidsPatterns = new[]
{
@"<span[^>]*id=""divSaldoBidBottom""[^>]*>(\d+)</span>",
@"<span[^>]*id=""divSaldoBidMobile""[^>]*>(\d+)</span>",
@"<span[^>]*id=""divSaldoBid""[^>]*>(\d+)</span>",
@"<div[^>]*class=""bids[_-]count""[^>]*>(\d+)</div>"
};
foreach (var pattern in bidsPatterns)
{
var bidsMatch = System.Text.RegularExpressions.Regex.Match(html, pattern);
if (bidsMatch.Success && int.TryParse(bidsMatch.Groups[1].Value, out int bids))
{
userData.RemainingBids = bids;
foundBids = true;
Log($"[USER HTML PARSED] Puntate residue trovate: {bids}");
break;
}
}
if (!foundBids)
{
Log($"[USER HTML ERROR] Puntate residue NON trovate nell'HTML");
}
// Ritorna dati solo se almeno username è stato trovato
if (foundUsername)
{
Log($"[USER HTML SUCCESS] Dati estratti: {userData.Username}, {userData.RemainingBids} puntate");
return userData;
}
Log($"[USER HTML FAILED] Impossibile estrarre dati utente dall'HTML");
return null;
}
catch (Exception ex)
{
Log($"[USER HTML EXCEPTION] {ex.GetType().Name}: {ex.Message}");
Log($"[USER HTML EXCEPTION] StackTrace: {ex.StackTrace}");
return null;
}
}