- Aggiunta proprietà `FinalAttackThresholdSec` (0.8s) in `AuctionInfo.cs`. - Implementata strategia di "quick re-poll" in `AuctionMonitor.cs` per confermare stato critico prima dell'attacco finale. - Migliorata gestione delle eccezioni in `BidooApiClient.cs` con log dettagliati e tentativi alternativi. - Registrazione del numero di offerte rimanenti dopo successo in `BidooApiClient.cs`. - Ottimizzati messaggi di log per maggiore chiarezza e trasparenza. - Rimossa logica obsoleta e aggiunti ritardi minimi tra tentativi di polling rapido.
741 lines
33 KiB
C#
741 lines
33 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Text;
|
|
using AutoBidder.Models;
|
|
|
|
namespace AutoBidder.Services
|
|
{
|
|
/// <summary>
|
|
/// Servizio completo API Bidoo (polling, puntate, info utente)
|
|
/// Solo HTTP, nessuna modalità, browser o multi-click
|
|
/// </summary>
|
|
public class BidooApiClient
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
private BidooSession _session;
|
|
|
|
// Event used to push detailed logs into per-auction log in the monitor
|
|
public event Action<string, string>? OnAuctionLog;
|
|
|
|
public BidooApiClient()
|
|
{
|
|
var handler = new HttpClientHandler
|
|
{
|
|
UseCookies = false, // Gestiamo manualmente i cookie
|
|
AutomaticDecompression = System.Net.DecompressionMethods.All // Decomprimi GZIP/Deflate/Brotli
|
|
};
|
|
|
|
_httpClient = new HttpClient(handler)
|
|
{
|
|
Timeout = TimeSpan.FromSeconds(3)
|
|
};
|
|
|
|
_session = new BidooSession();
|
|
}
|
|
|
|
// Helper that writes to Console and, when auctionId provided, emits per-auction log event
|
|
private void Log(string message, string? auctionId = null)
|
|
{
|
|
try
|
|
{
|
|
Console.WriteLine(message);
|
|
}
|
|
catch { }
|
|
|
|
if (!string.IsNullOrEmpty(auctionId))
|
|
{
|
|
try
|
|
{
|
|
OnAuctionLog?.Invoke(auctionId, message);
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inizializza sessione con token di autenticazione
|
|
/// </summary>
|
|
public void InitializeSession(string authToken, string username)
|
|
{
|
|
_session.AuthToken = authToken;
|
|
_session.Username = username;
|
|
|
|
Log($"[SESSION] Token impostato ({authToken.Length} chars)");
|
|
Log($"[SESSION] Username: {username}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inizializza sessione con cookie string (manuale)
|
|
/// </summary>
|
|
public void InitializeSessionWithCookie(string cookieString, string username)
|
|
{
|
|
_session.CookieString = cookieString;
|
|
_session.Username = username;
|
|
Log($"[SESSION] Cookie impostato manualmente ({cookieString.Length} chars)");
|
|
Log($"[SESSION] Username: {username}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Aggiunge header di autenticazione e browser-like alla richiesta
|
|
/// Headers critici per evitare rilevamento come bot
|
|
/// </summary>
|
|
private void AddAuthHeaders(HttpRequestMessage request, string? referer = null, string? auctionId = null)
|
|
{
|
|
// 1. AUTENTICAZIONE (solo cookie manuale)
|
|
if (!string.IsNullOrWhiteSpace(_session.CookieString))
|
|
{
|
|
request.Headers.Add("Cookie", _session.CookieString);
|
|
Log("[AUTH] Using full cookie string", auctionId);
|
|
}
|
|
else
|
|
{
|
|
Log("[AUTH WARN] No authentication method available!", auctionId);
|
|
}
|
|
|
|
// 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");
|
|
|
|
// Accept headers
|
|
request.Headers.Add("Accept", "*/*");
|
|
request.Headers.Add("Accept-Language", "it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7");
|
|
request.Headers.Add("Accept-Encoding", "gzip, deflate, br");
|
|
|
|
// Security headers (critici per CORS)
|
|
request.Headers.Add("Sec-Fetch-Dest", "empty");
|
|
request.Headers.Add("Sec-Fetch-Mode", "cors");
|
|
request.Headers.Add("Sec-Fetch-Site", "same-origin");
|
|
|
|
// Chrome-specific headers
|
|
request.Headers.Add("sec-ch-ua", "\"Google Chrome\";v=\"141\", \"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"141\"");
|
|
request.Headers.Add("sec-ch-ua-mobile", "?0");
|
|
request.Headers.Add("sec-ch-ua-platform", "\"Windows\"");
|
|
|
|
// XMLHttpRequest identifier (FONDAMENTALE per API AJAX)
|
|
request.Headers.Add("X-Requested-With", "XMLHttpRequest");
|
|
|
|
// Referer (importante per validazione origin)
|
|
if (!string.IsNullOrEmpty(referer))
|
|
{
|
|
request.Headers.Add("Referer", referer);
|
|
}
|
|
else
|
|
{
|
|
request.Headers.Add("Referer", "https://it.bidoo.com/");
|
|
}
|
|
|
|
Log("[HEADERS] Browser-like headers added (anti-bot)", auctionId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Estrae CSRF/Bid token dalla pagina asta
|
|
/// PASSO 1: Ottenere la pagina HTML dell'asta per estrarre il token di sicurezza
|
|
/// Il token può essere chiamato: bid_token, csrf_token, _token, etc.
|
|
/// </summary>
|
|
private async Task<(string? tokenName, string? tokenValue)> ExtractBidTokenAsync(string auctionId, string? auctionUrl = null)
|
|
{
|
|
try
|
|
{
|
|
var url = !string.IsNullOrEmpty(auctionUrl) ? auctionUrl : $"https://it.bidoo.com/asta/nome-prodotto-{auctionId}";
|
|
Log($"[TOKEN] GET {url}", auctionId);
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
|
AddAuthHeaders(request, url, auctionId);
|
|
|
|
var response = await _httpClient.SendAsync(request);
|
|
var html = await response.Content.ReadAsStringAsync();
|
|
|
|
Log($"[TOKEN] Response: {response.StatusCode}, HTML length: {html.Length}", auctionId);
|
|
|
|
var patterns = new System.Collections.Generic.List<(string pattern, string name)>
|
|
{
|
|
// double-quoted input attributes
|
|
("(?i)<input[^>]*name=\"bid_token\"[^>]*value=\"([^\"]+)\"", "bid_token"),
|
|
("(?i)<input[^>]*value=\"([^\"]+)\"[^>]*name=\"bid_token\"", "bid_token"),
|
|
("(?i)<input[^>]*name=\"csrf_token\"[^>]*value=\"([^\"]+)\"", "csrf_token"),
|
|
("(?i)<input[^>]*value=\"([^\"]+)\"[^>]*name=\"csrf_token\"", "csrf_token"),
|
|
("(?i)<input[^>]*name=\"_token\"[^>]*value=\"([^\"]+)\"", "_token"),
|
|
("(?i)<input[^>]*name=\"token\"[^>]*value=\"([^\"]+)\"", "token"),
|
|
|
|
// single-quoted input attributes
|
|
("(?i)<input[^>]*name='bid_token'[^>]*value='([^']+)'", "bid_token"),
|
|
("(?i)<input[^>]*value='([^']+)'[^>]*name='bid_token'", "bid_token"),
|
|
("(?i)<input[^>]*name='csrf_token'[^>]*value='([^']+)'", "csrf_token"),
|
|
("(?i)<input[^>]*value='([^']+)'[^>]*name='csrf_token'", "csrf_token"),
|
|
("(?i)<input[^>]*name='_token'[^>]*value='([^']+)'", "_token"),
|
|
("(?i)<input[^>]*name='token'[^>]*value='([^']+)'", "token"),
|
|
|
|
// JavaScript style assignments (double and single quotes)
|
|
("(?i)bid_token\\s*[:=]\\s*\"([^\\\"]+)\"", "bid_token"),
|
|
("(?i)bid_token\\s*[:=]\\s*'([^']+)'", "bid_token"),
|
|
("(?i)csrf_token\\s*[:=]\\s*\"([^\\\"]+)\"", "csrf_token"),
|
|
("(?i)csrf_token\\s*[:=]\\s*'([^']+)'", "csrf_token"),
|
|
|
|
// JSON style
|
|
("\"token\"\\s*:\\s*\"([^\\\"]+)\"", "token")
|
|
};
|
|
|
|
foreach (var pattern in patterns)
|
|
{
|
|
var match = System.Text.RegularExpressions.Regex.Match(html, pattern.pattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
|
if (match.Success)
|
|
{
|
|
var tokenValue = match.Groups[1].Value;
|
|
Log($"[TOKEN] ✓ Token found: {pattern.name} = {tokenValue.Substring(0, Math.Min(20, tokenValue.Length))}...", auctionId);
|
|
return (pattern.name, tokenValue);
|
|
}
|
|
}
|
|
|
|
Log("[TOKEN] ⚠ No bid token found in HTML", auctionId);
|
|
return (null, null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"[TOKEN ERROR] {ex.Message}", auctionId);
|
|
return (null, null);
|
|
}
|
|
}
|
|
|
|
private Task<(string? tokenName, string? tokenValue)> ExtractBidTokenAsync(string auctionId)
|
|
{
|
|
return ExtractBidTokenAsync(auctionId, null);
|
|
}
|
|
|
|
public async Task<AuctionState?> PollAuctionStateAsync(string auctionId, string? auctionUrl, CancellationToken token)
|
|
{
|
|
try
|
|
{
|
|
var startTime = DateTime.UtcNow;
|
|
var url = $"https://it.bidoo.com/data.php?ALL={auctionId}&LISTID=0";
|
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
|
var referer = !string.IsNullOrEmpty(auctionUrl)
|
|
? auctionUrl
|
|
: $"https://it.bidoo.com/auction.php?a=asta_{auctionId}";
|
|
AddAuthHeaders(request, referer, auctionId);
|
|
var response = await _httpClient.SendAsync(request, token);
|
|
var latency = (int)(DateTime.UtcNow - startTime).TotalMilliseconds;
|
|
var responseText = await response.Content.ReadAsStringAsync();
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
string reason = response.StatusCode == System.Net.HttpStatusCode.RequestTimeout ? "timeout" : "errore HTTP";
|
|
Log($"[ERRORE] [{auctionId}] API non ha risposto (motivo: {reason})", null); // globale
|
|
Log($"API non ha risposto: {response.StatusCode} ({reason})", auctionId); // asta
|
|
return null;
|
|
}
|
|
var state = ParsePollingResponse(auctionId, responseText, latency);
|
|
return state;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Global concise message
|
|
Log($"[ERRORE] [{auctionId}] API non ha risposto (verificare dettagli nel log asta)", null);
|
|
// Detailed per-auction log with full exception and context
|
|
var details = ex.ToString();
|
|
details = "[API EXCEPTION] " + details;
|
|
Log(details, auctionId);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private AuctionState? ParsePollingResponse(string auctionId, string response, int latency)
|
|
{
|
|
try
|
|
{
|
|
string serverTimestamp;
|
|
string mainData;
|
|
var starIndex = response.IndexOf('*');
|
|
if (starIndex == -1)
|
|
{
|
|
Log("[PARSE ERROR] No '*' separator found in response", auctionId);
|
|
return null;
|
|
}
|
|
var timestampPart = response.Substring(0, starIndex);
|
|
mainData = response.Substring(starIndex + 1);
|
|
if (timestampPart.Contains('|'))
|
|
{
|
|
serverTimestamp = timestampPart.Split('|')[0];
|
|
}
|
|
else
|
|
{
|
|
serverTimestamp = timestampPart;
|
|
}
|
|
var bracketStart = mainData.IndexOf('[');
|
|
var bracketEnd = mainData.IndexOf(']');
|
|
if (bracketStart == -1 || bracketEnd == -1)
|
|
{
|
|
Log("[PARSE ERROR] Missing brackets in auction data", auctionId);
|
|
return null;
|
|
}
|
|
var auctionData = mainData.Substring(bracketStart + 1, bracketEnd - bracketStart - 1);
|
|
var firstSeparator = auctionData.IndexOfAny(new[] { '|', ',' });
|
|
var coreData = firstSeparator > 0 ? auctionData.Substring(0, firstSeparator) : auctionData;
|
|
var fields = coreData.Split(';');
|
|
if (fields.Length < 5)
|
|
{
|
|
Log($"[PARSE ERROR] Expected at least 5 core fields, got {fields.Length}", auctionId);
|
|
return null;
|
|
}
|
|
var state = new AuctionState
|
|
{
|
|
AuctionId = auctionId,
|
|
SnapshotTime = DateTime.UtcNow,
|
|
PollingLatencyMs = latency
|
|
};
|
|
var status = fields[1].Trim().ToUpperInvariant();
|
|
string lastBidder = fields[4].Trim();
|
|
bool hasWinner = !string.IsNullOrEmpty(lastBidder);
|
|
bool iAmWinner = hasWinner && lastBidder.Equals(_session.Username, StringComparison.OrdinalIgnoreCase);
|
|
state.Status = DetermineAuctionStatus(status, hasWinner, iAmWinner, ref state);
|
|
if (long.TryParse(serverTimestamp, out var serverTs) && long.TryParse(fields[2], out var expiryTs))
|
|
{
|
|
var timerSeconds = (double)(expiryTs - serverTs);
|
|
state.Timer = Math.Max(0, timerSeconds);
|
|
}
|
|
if (int.TryParse(fields[3], out var priceIndex))
|
|
{
|
|
state.Price = priceIndex * 0.01;
|
|
}
|
|
state.LastBidder = fields[4].Trim();
|
|
state.IsMyBid = !string.IsNullOrEmpty(_session.Username) &&
|
|
state.LastBidder.Equals(_session.Username, StringComparison.OrdinalIgnoreCase);
|
|
state.ParsingSuccess = true;
|
|
// Log only summary on success
|
|
Log($"[PARSE SUCCESS] Timer: {state.Timer:F2}s, Price: €{state.Price:F2}, Bidder: {state.LastBidder}, Status: {state.Status}", auctionId);
|
|
return state;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"[PARSE EXCEPTION] {ex.GetType().Name}: {ex.Message}", auctionId);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async Task<bool> UpdateUserInfoAsync()
|
|
{
|
|
try
|
|
{
|
|
var url = "https://it.bidoo.com/ajax/get_auction_bids_info_banner.php";
|
|
|
|
Log($"[USER INFO REQUEST] GET {url}");
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
|
AddAuthHeaders(request, "https://it.bidoo.com/");
|
|
|
|
var startTime = DateTime.UtcNow;
|
|
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");
|
|
|
|
var responseText = await response.Content.ReadAsStringAsync();
|
|
Log($"[USER INFO RESPONSE] Body length: {responseText.Length}");
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
Log($"[USER INFO ERROR] HTTP {response.StatusCode}");
|
|
return false;
|
|
}
|
|
|
|
_session.LastAccountUpdate = DateTime.UtcNow;
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"[USER INFO EXCEPTION] {ex.GetType().Name}: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async Task<BidResult> PlaceBidAsync(string auctionId, string? auctionUrl = null)
|
|
{
|
|
var result = new BidResult
|
|
{
|
|
AuctionId = auctionId,
|
|
Timestamp = DateTime.UtcNow
|
|
};
|
|
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}";
|
|
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;
|
|
}
|
|
// Parse remaining bids from response if present: ok|324|...
|
|
var parts2 = responseText.Split('|');
|
|
if (parts2.Length > 1 && int.TryParse(parts2[1], out var remaining))
|
|
{
|
|
_session.RemainingBids = remaining;
|
|
Log($"[BID SUCCESS] ✓ Bid placed successfully - Remaining bids: {remaining}", auctionId);
|
|
}
|
|
else
|
|
{
|
|
Log("[BID SUCCESS] ✓ 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 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);
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
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);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determina lo stato dell'asta basandosi su Status, LastBidder, Timer
|
|
///
|
|
/// STATI API BIDOO:
|
|
/// - ON: Asta attiva e in corso
|
|
/// - OFF: Asta terminata definitivamente
|
|
/// - STOP: Asta in pausa (tipicamente 00:00-10:00) - riprenderà più tardi
|
|
/// </summary>
|
|
private AuctionStatus DetermineAuctionStatus(string apiStatus, bool hasWinner, bool iAmWinner, ref AuctionState state)
|
|
{
|
|
// Gestione stato STOP (pausa notturna)
|
|
if (apiStatus == "STOP")
|
|
{
|
|
// L'asta è iniziata ma è in pausa
|
|
// Controlla se c'è già un vincitore temporaneo
|
|
if (hasWinner)
|
|
{
|
|
state.LastBidder = state.LastBidder; // Mantieni il last bidder
|
|
return AuctionStatus.Paused;
|
|
}
|
|
// Pausa senza puntate ancora
|
|
return AuctionStatus.Paused;
|
|
}
|
|
|
|
if (apiStatus == "OFF")
|
|
{
|
|
// Asta terminata definitivamente
|
|
if (hasWinner)
|
|
{
|
|
return iAmWinner ? AuctionStatus.EndedWon : AuctionStatus.EndedLost;
|
|
}
|
|
return AuctionStatus.Closed;
|
|
}
|
|
|
|
if (apiStatus == "ON")
|
|
{
|
|
// Asta attiva
|
|
if (hasWinner)
|
|
{
|
|
// Ci sono già puntate → Running
|
|
return AuctionStatus.Running;
|
|
}
|
|
|
|
// Nessuna puntata ancora → Pending o Scheduled
|
|
// Se timer molto alto (> 30 minuti), è programmata per più tardi
|
|
if (state.Timer > 1800) // 30 minuti
|
|
{
|
|
return AuctionStatus.Scheduled;
|
|
}
|
|
|
|
// Altrimenti sta per iniziare
|
|
return AuctionStatus.Pending;
|
|
}
|
|
|
|
return AuctionStatus.Unknown;
|
|
}
|
|
|
|
public BidooSession GetSession() => _session;
|
|
|
|
public void Dispose()
|
|
{
|
|
_httpClient?.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene dati utente (nome, puntate residue, saldo, id) tramite chiamata AJAX leggera
|
|
/// </summary>
|
|
public async Task<UserData?> GetUserDataAsync()
|
|
{
|
|
try
|
|
{
|
|
var url = "https://it.bidoo.com/update_credits_status.php?submit=1";
|
|
Log($"[USER STATUS REQUEST] GET {url}");
|
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
|
AddAuthHeaders(request, "https://it.bidoo.com/");
|
|
var response = await _httpClient.SendAsync(request);
|
|
var responseString = await response.Content.ReadAsStringAsync();
|
|
Log($"[USER STATUS RESPONSE] Body: {responseString}");
|
|
var userData = new UserData();
|
|
var trimmed = responseString.Trim();
|
|
// Caso: solo numero
|
|
if (int.TryParse(trimmed, out int bids))
|
|
{
|
|
userData.RemainingBids = bids;
|
|
return userData;
|
|
}
|
|
// Caso: HTML <span id="bids_count">125</span>
|
|
if (trimmed.StartsWith("<span") && trimmed.Contains("id=\"bids_count\""))
|
|
{
|
|
var start = trimmed.IndexOf('>');
|
|
var end = trimmed.IndexOf("</span>");
|
|
if (start >= 0 && end > start)
|
|
{
|
|
var value = trimmed.Substring(start + 1, end - start - 1);
|
|
if (int.TryParse(value, out bids))
|
|
{
|
|
userData.RemainingBids = bids;
|
|
return userData;
|
|
}
|
|
}
|
|
}
|
|
// Caso: stringa delimitata da pipe
|
|
var parts = trimmed.Split('|');
|
|
if (parts.Length >= 2)
|
|
{
|
|
userData.Username = parts[0];
|
|
if (int.TryParse(parts[1], out bids))
|
|
{
|
|
userData.RemainingBids = bids;
|
|
}
|
|
if (parts.Length >= 3 && double.TryParse(parts[2], out double cash))
|
|
{
|
|
userData.CashBalance = cash;
|
|
}
|
|
if (parts.Length >= 4 && int.TryParse(parts[3], out int userId))
|
|
{
|
|
userData.UserId = userId;
|
|
}
|
|
return userData;
|
|
}
|
|
// Se non riconosciuto
|
|
Log($"[USER STATUS PARSE ERROR] Formato non riconosciuto: {responseString}");
|
|
return null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"[USER STATUS EXCEPTION] {ex.GetType().Name}: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene info banner utente (aste vinte, bonus, ecc.) tramite chiamata AJAX
|
|
/// </summary>
|
|
public async Task<UserBannerInfo?> GetUserBannerInfoAsync()
|
|
{
|
|
try
|
|
{
|
|
var url = "https://it.bidoo.com/ajax/get_auction_bids_info_banner.php";
|
|
Log($"[USER BANNER REQUEST] GET {url}");
|
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
|
AddAuthHeaders(request, "https://it.bidoo.com/");
|
|
var response = await _httpClient.SendAsync(request);
|
|
var responseString = await response.Content.ReadAsStringAsync();
|
|
Log($"[USER BANNER RESPONSE] Body: {responseString}");
|
|
if (!response.IsSuccessStatusCode || string.IsNullOrWhiteSpace(responseString))
|
|
return null;
|
|
var info = System.Text.Json.JsonSerializer.Deserialize<UserBannerInfo>(responseString);
|
|
return info;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"[USER BANNER EXCEPTION] {ex.GetType().Name}: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Estrae nome utente e puntate residue dall'HTML di bids_history.php
|
|
/// </summary>
|
|
public async Task<UserData?> GetUserDataFromHtmlAsync()
|
|
{
|
|
try
|
|
{
|
|
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);
|
|
var html = await response.Content.ReadAsStringAsync();
|
|
Log($"[USER HTML RESPONSE] Body length: {html.Length}");
|
|
var userData = new UserData();
|
|
// Estrai nome utente
|
|
var userMatch = System.Text.RegularExpressions.Regex.Match(html, @"<a class=""pers_lnk""[^>]*>([^<]+)</a>");
|
|
if (userMatch.Success)
|
|
{
|
|
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))
|
|
{
|
|
userData.RemainingBids = bids;
|
|
}
|
|
if (!string.IsNullOrEmpty(userData.Username) && userData.RemainingBids > 0)
|
|
return userData;
|
|
return null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"[USER HTML EXCEPTION] {ex.GetType().Name}: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|