Aggiunta infrastruttura avanzata per gestione aste
- Introdotta la classe `BidooApiClient` per interagire con le API Bidoo. - Aggiunto `SessionManager` per la gestione sicura delle sessioni. - Creato `TestBidooApi` per test manuali delle API. - Implementato `CsvExporter` per esportare dati e statistiche in CSV. - Aggiunto `PersistenceManager` per salvare e caricare aste in JSON. - Introdotto `AuctionViewModel` per supportare il pattern MVVM. - Migliorata l'interfaccia utente con layout moderno e stili dinamici. - Aggiornata la documentazione in `README.md` per riflettere le nuove funzionalità. - Aggiunte classi per rappresentare informazioni, stato e storico delle aste. - Ottimizzate le richieste HTTP per simulare un browser reale.
This commit is contained in:
@@ -0,0 +1,466 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AutoBidder.Models;
|
||||
|
||||
namespace AutoBidder.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Servizio centrale per monitoraggio multi-asta
|
||||
/// Gestisce polling API Bidoo e trigger dei click
|
||||
/// </summary>
|
||||
public class AuctionMonitor
|
||||
{
|
||||
private readonly BidooApiClient _apiClient;
|
||||
private readonly List<AuctionInfo> _auctions = new();
|
||||
private CancellationTokenSource? _monitoringCts;
|
||||
private Task? _monitoringTask;
|
||||
|
||||
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 AuctionMonitor()
|
||||
{
|
||||
_apiClient = new BidooApiClient();
|
||||
|
||||
// Subscribe to detailed per-auction logs from API client
|
||||
_apiClient.OnAuctionLog += (auctionId, message) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_auctions)
|
||||
{
|
||||
var auction = _auctions.FirstOrDefault(a => a.AuctionId == auctionId);
|
||||
if (auction != null)
|
||||
{
|
||||
auction.AddLog(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
};
|
||||
}
|
||||
|
||||
/// <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();
|
||||
}
|
||||
|
||||
public void AddAuction(AuctionInfo auction)
|
||||
{
|
||||
lock (_auctions)
|
||||
{
|
||||
if (!_auctions.Any(a => a.AuctionId == auction.AuctionId))
|
||||
{
|
||||
_auctions.Add(auction);
|
||||
OnLog?.Invoke($"[+] Asta aggiunta: {auction.Name} (ID: {auction.AuctionId})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveAuction(string auctionId)
|
||||
{
|
||||
lock (_auctions)
|
||||
{
|
||||
var auction = _auctions.FirstOrDefault(a => a.AuctionId == auctionId);
|
||||
if (auction != null)
|
||||
{
|
||||
_auctions.Remove(auction);
|
||||
OnLog?.Invoke($"[-] Asta rimossa: {auction.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<AuctionInfo> GetAuctions()
|
||||
{
|
||||
lock (_auctions)
|
||||
{
|
||||
return _auctions.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> InitializeCookies(Microsoft.Web.WebView2.Wpf.WebView2 webView)
|
||||
{
|
||||
// Non più utilizzato - usa InitializeSession invece
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_monitoringTask != null && !_monitoringTask.IsCompleted)
|
||||
{
|
||||
OnLog?.Invoke("[WARN] Monitoraggio gia' attivo");
|
||||
return;
|
||||
}
|
||||
|
||||
_monitoringCts = new CancellationTokenSource();
|
||||
_monitoringTask = Task.Run(() => MonitoringLoop(_monitoringCts.Token));
|
||||
OnLog?.Invoke("[START] Monitoraggio avviato");
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_monitoringCts?.Cancel();
|
||||
_monitoringTask?.Wait(TimeSpan.FromSeconds(2));
|
||||
_monitoringCts = null;
|
||||
_monitoringTask = null;
|
||||
OnLog?.Invoke("[STOP] Monitoraggio fermato");
|
||||
}
|
||||
|
||||
private async Task MonitoringLoop(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<AuctionInfo> activeAuctions;
|
||||
lock (_auctions)
|
||||
{
|
||||
// Filtra aste che devono ancora essere monitorate
|
||||
// Esclude: pausa manuale, chiuse definitivamente, vinte, perse
|
||||
activeAuctions = _auctions.Where(a =>
|
||||
a.IsActive &&
|
||||
!a.IsPaused &&
|
||||
!IsAuctionTerminated(a)
|
||||
).ToList();
|
||||
}
|
||||
|
||||
if (activeAuctions.Count == 0)
|
||||
{
|
||||
await Task.Delay(1000, token);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Poll tutte le aste in parallelo
|
||||
var pollTasks = activeAuctions.Select(a => PollAndProcessAuction(a, token));
|
||||
await Task.WhenAll(pollTasks);
|
||||
|
||||
// Ottimizzazione polling aste in pausa
|
||||
bool anyPaused = false;
|
||||
DateTime now = DateTime.Now;
|
||||
int pauseDelayMs = 1000; // default
|
||||
foreach (var a in activeAuctions)
|
||||
{
|
||||
if (a.BidHistory.LastOrDefault()?.Notes?.Contains("PAUSA") == true)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (anyPaused)
|
||||
{
|
||||
await Task.Delay(pauseDelayMs, token);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Delay adattivo OTTIMIZZATO basato su timer più basso
|
||||
var lowestTimer = activeAuctions
|
||||
.Select(a => GetLastTimer(a))
|
||||
.Where(t => t > 0)
|
||||
.DefaultIfEmpty(999)
|
||||
.Min();
|
||||
|
||||
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)
|
||||
};
|
||||
|
||||
await Task.Delay(delayMs, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnLog?.Invoke($"[ERRORE] Loop monitoraggio: {ex.Message}");
|
||||
await Task.Delay(1000, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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") ||
|
||||
lastHistory.Notes.Contains("Chiusa")))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task PollAndProcessAuction(AuctionInfo auction, CancellationToken token)
|
||||
{
|
||||
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)
|
||||
{
|
||||
auction.AddLog("ERRORE: Nessun dato ricevuto da API");
|
||||
OnLog?.Invoke($"[ERRORE] [{auction.AuctionId}] API non ha risposto");
|
||||
return;
|
||||
}
|
||||
|
||||
// Se l'asta è terminata, segnala e disattiva polling
|
||||
if (state.Status == AuctionStatus.EndedWon ||
|
||||
state.Status == AuctionStatus.EndedLost ||
|
||||
state.Status == AuctionStatus.Closed)
|
||||
{
|
||||
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,
|
||||
EventType = BidEventType.Reset,
|
||||
Bidder = state.LastBidder,
|
||||
Price = state.Price,
|
||||
Timer = 0,
|
||||
Notes = $"Asta {statusMsg}"
|
||||
});
|
||||
|
||||
// Notifica UI e fermati
|
||||
OnAuctionUpdated?.Invoke(state);
|
||||
return;
|
||||
}
|
||||
|
||||
// Log stato solo per aste attive (riduci spam)
|
||||
if (state.Status == AuctionStatus.Running)
|
||||
{
|
||||
auction.AddLog($"API OK - Timer: {state.Timer:F2}s, EUR{state.Price:F2}, {state.LastBidder}, {state.PollingLatencyMs}ms");
|
||||
}
|
||||
else if (state.Status == AuctionStatus.Paused)
|
||||
{
|
||||
auction.AddLog($"[PAUSA] Asta in pausa - Timer: {state.Timer:F2}s, EUR{state.Price:F2}");
|
||||
}
|
||||
|
||||
// Notifica aggiornamento UI
|
||||
OnAuctionUpdated?.Invoke(state);
|
||||
|
||||
// Aggiorna storico e bidders
|
||||
UpdateAuctionHistory(auction, state);
|
||||
|
||||
// Verifica se puntare (solo se asta Running, NON se in pausa)
|
||||
if (state.Status == AuctionStatus.Running && ShouldBid(auction, state))
|
||||
{
|
||||
auction.AddLog($"[TRIGGER] CONDIZIONI OK - Timer {state.Timer:F2}s <= {auction.TimerClick}s");
|
||||
auction.AddLog($"[BID] Invio puntata...");
|
||||
OnLog?.Invoke($"[BID] [{auction.AuctionId}] PUNTATA a {state.Timer:F2}s!");
|
||||
|
||||
// Attendi ritardo configurato
|
||||
if (auction.DelayMs > 0)
|
||||
{
|
||||
await Task.Delay(auction.DelayMs, token);
|
||||
}
|
||||
|
||||
// Esegui puntata API
|
||||
var result = await _apiClient.PlaceBidAsync(auction.AuctionId);
|
||||
|
||||
// Aggiorna contatori
|
||||
if (result.Success)
|
||||
{
|
||||
auction.MyClicks++;
|
||||
auction.LastClickAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// Notifica risultato
|
||||
OnBidExecuted?.Invoke(auction, result);
|
||||
|
||||
// Log
|
||||
if (result.Success)
|
||||
{
|
||||
auction.AddLog($"[OK] PUNTATA OK: {result.LatencyMs}ms -> EUR{result.NewPrice:F2}");
|
||||
OnLog?.Invoke($"[OK] [{auction.AuctionId}] Puntata riuscita {result.LatencyMs}ms");
|
||||
}
|
||||
else
|
||||
{
|
||||
auction.AddLog($"[FAIL] PUNTATA FALLITA: {result.Error}");
|
||||
OnLog?.Invoke($"[FAIL] [{auction.AuctionId}] ERRORE: {result.Error}");
|
||||
}
|
||||
|
||||
// Storico
|
||||
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}");
|
||||
OnLog?.Invoke($"[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;
|
||||
|
||||
if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice)
|
||||
return false;
|
||||
|
||||
// Cooldown check (evita click multipli ravvicinati)
|
||||
if (auction.LastClickAt.HasValue)
|
||||
{
|
||||
var timeSinceLastClick = DateTime.UtcNow - auction.LastClickAt.Value;
|
||||
if (timeSinceLastClick.TotalSeconds < 1)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
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))
|
||||
{
|
||||
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}"
|
||||
});
|
||||
|
||||
// Aggiorna statistiche bidder
|
||||
if (!string.IsNullOrEmpty(state.LastBidder))
|
||||
{
|
||||
if (!auction.BidderStats.ContainsKey(state.LastBidder))
|
||||
{
|
||||
auction.BidderStats[state.LastBidder] = new BidderInfo
|
||||
{
|
||||
Username = state.LastBidder
|
||||
};
|
||||
}
|
||||
|
||||
auction.BidderStats[state.LastBidder].BidCount++;
|
||||
auction.BidderStats[state.LastBidder].LastBidTime = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// Notifica cambio reset count per aggiornare UI
|
||||
OnResetCountChanged?.Invoke(auction.AuctionId);
|
||||
}
|
||||
}
|
||||
|
||||
private double GetLastTimer(AuctionInfo auction)
|
||||
{
|
||||
var lastEntry = auction.BidHistory.LastOrDefault();
|
||||
return lastEntry?.Timer ?? 999;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_apiClient?.Dispose();
|
||||
}
|
||||
|
||||
public async Task<BidResult> PlaceManualBidAsync(AuctionInfo auction)
|
||||
{
|
||||
return await _apiClient.PlaceBidAsync(auction.AuctionId, auction.OriginalUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
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)
|
||||
/// 100% API-based con simulazione completa del comportamento browser
|
||||
///
|
||||
/// FLUSSO DI AUTENTICAZIONE:
|
||||
/// - Cookie principale: __stattr (non PHPSESSID)
|
||||
/// - Il cookie deve essere estratto dal browser dopo login manuale
|
||||
/// - Tutti i request devono includere: Cookie + X-Requested-With: XMLHttpRequest
|
||||
///
|
||||
/// CHIAMATE GET (SOLO LETTURA - No CSRF Token):
|
||||
/// 1. Polling asta: GET data.php?ALL={id}&LISTID=0
|
||||
/// 2. Info utente: GET ajax/get_auction_bids_info_banner.php
|
||||
///
|
||||
/// CHIAMATE POST (AZIONI - Richiedono CSRF Token):
|
||||
/// 1. Piazza puntata: POST bid.php
|
||||
/// - Step 1: GET pagina asta HTML per estrarre bid_token
|
||||
/// - Step 2: POST con payload: a={id}&bid_type=manual&bid_token={token}&time_sent={ts}
|
||||
/// - Step 3: Analizza risposta: "ok|..." o "error|..."
|
||||
///
|
||||
/// SIMULAZIONE BROWSER:
|
||||
/// - User-Agent: Chrome su Windows
|
||||
/// - Headers CORS: Sec-Fetch-*
|
||||
/// - Referer: URL pagina asta
|
||||
/// - Content-Type: application/x-www-form-urlencoded (per POST)
|
||||
/// </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 (fallback)
|
||||
/// </summary>
|
||||
public void InitializeSessionWithCookie(string cookieString, string username)
|
||||
{
|
||||
_session.CookieString = cookieString;
|
||||
_session.Username = username;
|
||||
|
||||
Log($"[SESSION] Cookie impostato ({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 (priorità: CookieString completa, poi Token singolo)
|
||||
// Il cookie principale per Bidoo è __stattr (non PHPSESSID)
|
||||
if (!string.IsNullOrWhiteSpace(_session.CookieString))
|
||||
{
|
||||
// Usa la stringa cookie completa (es: "__stattr=eyJyZWZ...")
|
||||
request.Headers.Add("Cookie", _session.CookieString);
|
||||
Log("[AUTH] Using full cookie string", auctionId);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(_session.AuthToken))
|
||||
{
|
||||
// Fallback: se abbiamo solo il token, assumiamo sia __stattr
|
||||
request.Headers.Add("Cookie", $"__stattr={_session.AuthToken}");
|
||||
Log("[AUTH] Using __stattr token", 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";
|
||||
|
||||
Log($"[API REQUEST] GET {url}", auctionId);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
|
||||
var referer = !string.IsNullOrEmpty(auctionUrl)
|
||||
? auctionUrl
|
||||
: $"https://it.bidoo.com/auction.php?a=asta_{auctionId}";
|
||||
|
||||
Log($"[API REQUEST] Using referer: {referer}", auctionId);
|
||||
AddAuthHeaders(request, referer, auctionId);
|
||||
|
||||
var response = await _httpClient.SendAsync(request, token);
|
||||
var latency = (int)(DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||
|
||||
Log($"[API RESPONSE] Status: {(int)response.StatusCode} {response.StatusCode}", auctionId);
|
||||
Log($"[API RESPONSE] Latency: {latency}ms", auctionId);
|
||||
Log($"[API RESPONSE] Content-Type: {response.Content.Headers.ContentType}", auctionId);
|
||||
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
Log($"[API RESPONSE] Body ({responseText.Length} bytes): {responseText}", auctionId);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Log($"[API ERROR] Non-success status code: {response.StatusCode}", auctionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return ParsePollingResponse(auctionId, responseText, latency);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"[API EXCEPTION] Polling {auctionId}: {ex.GetType().Name} - {ex.Message}", auctionId);
|
||||
Log($"[API EXCEPTION] StackTrace: {ex.StackTrace}", auctionId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private AuctionState? ParsePollingResponse(string auctionId, string response, int latency)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log($"[PARSE] Starting parse for auction {auctionId}", auctionId);
|
||||
Log($"[PARSE] Raw response: {response}", auctionId);
|
||||
|
||||
// Step 1: Estrai Server Timestamp (può avere formato "timestamp*..." o "timestamp|flag*")
|
||||
string serverTimestamp;
|
||||
string mainData;
|
||||
|
||||
// Cerca il separatore '*' che divide timestamp da dati
|
||||
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);
|
||||
|
||||
// Il timestamp può contenere '|' (es: "1761120002|1")
|
||||
// Prendiamo solo la prima parte numerica
|
||||
if (timestampPart.Contains('|'))
|
||||
{
|
||||
serverTimestamp = timestampPart.Split('|')[0];
|
||||
Log($"[PARSE] Extended format detected: {timestampPart}", auctionId);
|
||||
}
|
||||
else
|
||||
{
|
||||
serverTimestamp = timestampPart;
|
||||
}
|
||||
|
||||
Log($"[PARSE] Server Timestamp: {serverTimestamp}", auctionId);
|
||||
Log($"[PARSE] Main Data: {mainData}", auctionId);
|
||||
|
||||
// Step 2: Estrai dati asta tra parentesi quadre [...]
|
||||
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);
|
||||
Log($"[PARSE] Auction Data extracted: {auctionData}", auctionId);
|
||||
|
||||
// Step 3: Split per ';' per ottenere i campi principali
|
||||
// Nota: la stringa può contenere '|' per separare bid history, quindi ci fermiamo al primo '|' o ','
|
||||
var firstSeparator = auctionData.IndexOfAny(new[] { '|', ',' });
|
||||
var coreData = firstSeparator > 0 ? auctionData.Substring(0, firstSeparator) : auctionData;
|
||||
|
||||
var fields = coreData.Split(';');
|
||||
Log($"[PARSE] Core fields count: {fields.Length}", auctionId);
|
||||
for (int i = 0; i < Math.Min(fields.Length, 10); i++)
|
||||
{
|
||||
Log($"[PARSE] Field[{i}] = '{fields[i]}'", auctionId);
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
// Step 4: Parse campi principali
|
||||
|
||||
// Field 0: AuctionID (verifica)
|
||||
Log($"[PARSE] Auction ID from response: {fields[0]} (expected: {auctionId})", auctionId);
|
||||
|
||||
// Field 1: Status (ON/OFF)
|
||||
var status = fields[1].Trim().ToUpperInvariant();
|
||||
|
||||
// Determiniamo lo stato in base a: Status API + LastBidder + Timer
|
||||
// Parsing di Field 4 (LastBidder) anticipato per logica stato
|
||||
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);
|
||||
|
||||
Log($"[PARSE] Status: {status} -> {state.Status}", auctionId);
|
||||
|
||||
// Field 2: Expiry Timestamp (CRITICO per timing)
|
||||
// IMPORTANTE: Usa il ServerTimestamp dalla risposta, NON il tempo locale!
|
||||
// Formato: ServerTimestamp*[..;ExpiryTimestamp;..]
|
||||
// Timer = ExpiryTimestamp - ServerTimestamp
|
||||
if (long.TryParse(serverTimestamp, out var serverTs) && long.TryParse(fields[2], out var expiryTs))
|
||||
{
|
||||
// Calcolo corretto usando il tempo del server
|
||||
var timerSeconds = (double)(expiryTs - serverTs);
|
||||
state.Timer = Math.Max(0, timerSeconds);
|
||||
|
||||
Log($"[PARSE] Server Timestamp: {serverTs}", auctionId);
|
||||
Log($"[PARSE] Expiry Timestamp: {expiryTs}", auctionId);
|
||||
Log($"[PARSE] Timer (Expiry - Server): {expiryTs} - {serverTs} = {timerSeconds:F2}s", auctionId);
|
||||
Log($"[PARSE] ⏱️ Final Timer: {state.Timer:F2}s", auctionId);
|
||||
|
||||
// DEBUG: Verifica se il timer sembra sbagliato
|
||||
if (state.Timer > 60)
|
||||
{
|
||||
Log("[PARSE WARN] Timer unusually high! Check data", auctionId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log($"[PARSE WARN] Failed to parse timestamps - Server: {serverTimestamp}, Expiry: {fields[2]}", auctionId);
|
||||
}
|
||||
|
||||
// Field 3: Price Index (0.01 EUR per index)
|
||||
if (int.TryParse(fields[3], out var priceIndex))
|
||||
{
|
||||
state.Price = priceIndex * 0.01;
|
||||
Log($"[PARSE] Price Index: {priceIndex} -> €{state.Price:F2}", auctionId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log($"[PARSE WARN] Failed to parse price index: {fields[3]}", auctionId);
|
||||
}
|
||||
|
||||
// Field 4: Last Bidder
|
||||
state.LastBidder = fields[4].Trim();
|
||||
state.IsMyBid = !string.IsNullOrEmpty(_session.Username) &&
|
||||
state.LastBidder.Equals(_session.Username, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
Log($"[PARSE] 👤 Last Bidder: {state.LastBidder}", auctionId);
|
||||
Log($"[PARSE] Is My Bid: {state.IsMyBid} (my username: {_session.Username})", auctionId);
|
||||
|
||||
// Field 5 (optional): Additional flags or data
|
||||
if (fields.Length > 5)
|
||||
{
|
||||
Log($"[PARSE] Additional data fields present: {fields.Length - 5}", auctionId);
|
||||
}
|
||||
|
||||
state.ParsingSuccess = true;
|
||||
|
||||
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);
|
||||
Log($"[PARSE EXCEPTION] StackTrace: {ex.StackTrace}", 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: {responseText}");
|
||||
|
||||
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 (no token)", 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 ({responseText.Length} bytes): {responseText}", 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] ✓ Bid placed successfully via GET", 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);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Success = false;
|
||||
result.Error = ex.Message;
|
||||
Log($"[BID EXCEPTION] {ex.GetType().Name}: {ex.Message}", auctionId);
|
||||
Log($"[BID EXCEPTION] StackTrace: {ex.StackTrace}", 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using AutoBidder.Models;
|
||||
|
||||
namespace AutoBidder.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Gestore persistenza sessione Bidoo
|
||||
/// Salva in modo sicuro il cookie di autenticazione per riutilizzo
|
||||
/// </summary>
|
||||
public class SessionManager
|
||||
{
|
||||
private static readonly string SessionFilePath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"AutoBidder",
|
||||
"session.dat"
|
||||
);
|
||||
|
||||
private static readonly byte[] Entropy = Encoding.UTF8.GetBytes("AutoBidder_V1_2025");
|
||||
|
||||
/// <summary>
|
||||
/// Salva la sessione in modo sicuro (crittografata con DPAPI)
|
||||
/// </summary>
|
||||
public static bool SaveSession(BidooSession session)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Crea directory se non esiste
|
||||
var directory = Path.GetDirectoryName(SessionFilePath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
// Serializza sessione in JSON
|
||||
var json = JsonSerializer.Serialize(session, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
|
||||
// Cripta usando DPAPI (Windows Data Protection API)
|
||||
var plainBytes = Encoding.UTF8.GetBytes(json);
|
||||
var encryptedBytes = ProtectedData.Protect(plainBytes, Entropy, DataProtectionScope.CurrentUser);
|
||||
|
||||
// Salva su file
|
||||
File.WriteAllBytes(SessionFilePath, encryptedBytes);
|
||||
|
||||
Console.WriteLine($"[SESSION] Saved to: {SessionFilePath}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[SESSION ERROR] Failed to save: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Carica la sessione salvata (decripta con DPAPI)
|
||||
/// </summary>
|
||||
public static BidooSession? LoadSession()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(SessionFilePath))
|
||||
{
|
||||
Console.WriteLine($"[SESSION] No saved session found");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Leggi file crittografato
|
||||
var encryptedBytes = File.ReadAllBytes(SessionFilePath);
|
||||
|
||||
// Decripta usando DPAPI
|
||||
var plainBytes = ProtectedData.Unprotect(encryptedBytes, Entropy, DataProtectionScope.CurrentUser);
|
||||
var json = Encoding.UTF8.GetString(plainBytes);
|
||||
|
||||
// Deserializza JSON
|
||||
var session = JsonSerializer.Deserialize<BidooSession>(json);
|
||||
|
||||
if (session != null && session.IsValid)
|
||||
{
|
||||
Console.WriteLine($"[SESSION] Loaded from: {SessionFilePath}");
|
||||
Console.WriteLine($"[SESSION] Username: {session.Username}");
|
||||
return session;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[SESSION] Loaded session is invalid");
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[SESSION ERROR] Failed to load: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Elimina la sessione salvata
|
||||
/// </summary>
|
||||
public static bool ClearSession()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(SessionFilePath))
|
||||
{
|
||||
File.Delete(SessionFilePath);
|
||||
Console.WriteLine($"[SESSION] Cleared: {SessionFilePath}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[SESSION ERROR] Failed to clear: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifica se esiste una sessione salvata
|
||||
/// </summary>
|
||||
public static bool HasSavedSession()
|
||||
{
|
||||
return File.Exists(SessionFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene informazioni sulla sessione salvata senza caricarla
|
||||
/// </summary>
|
||||
public static (bool exists, DateTime? lastModified) GetSessionInfo()
|
||||
{
|
||||
if (File.Exists(SessionFilePath))
|
||||
{
|
||||
var fileInfo = new FileInfo(SessionFilePath);
|
||||
return (true, fileInfo.LastWriteTime);
|
||||
}
|
||||
return (false, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user