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:
Alberto Balbo
2025-10-23 23:10:46 +02:00
parent db1d99d424
commit 4e16f50aeb
26 changed files with 4522 additions and 2576 deletions

View File

@@ -4,6 +4,10 @@
xmlns:local="clr-namespace:Mimante"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Converters/Converters.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,93 @@
<Window x:Class="AutoBidder.BrowserWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
Title="Browser - Bidoo"
Height="800" Width="1200"
Background="#101219" Foreground="#E6EDF3">
<Window.Resources>
<Style x:Key="AddressBarStyle" TargetType="TextBox">
<Setter Property="Background" Value="#0B1220" />
<Setter Property="Foreground" Value="#E6EDF3" />
<Setter Property="BorderBrush" Value="#263143" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="8,6" />
<Setter Property="FontSize" Value="13" />
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Barra navigazione -->
<Grid Grid.Row="0" Margin="12,12,12,6" Background="#0B1015">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button x:Name="BackButton" Grid.Column="0" Content="?" Click="BackButton_Click"
Background="#374151" Foreground="White" Padding="12,8" Margin="8,8,6,8"
BorderThickness="0" FontWeight="Bold" FontSize="14" MinWidth="40" Height="38" IsEnabled="False">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="6">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Button.Template>
</Button>
<Button x:Name="RefreshButton" Grid.Column="1" Content="?" Click="RefreshButton_Click"
Background="#0EA5E9" Foreground="White" Padding="12,8" Margin="0,8,8,8"
BorderThickness="0" FontWeight="Bold" FontSize="16" MinWidth="40" Height="38">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="6">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Button.Template>
</Button>
<TextBox x:Name="AddressBar" Grid.Column="2" Style="{StaticResource AddressBarStyle}"
Text="https://it.bidoo.com" KeyDown="AddressBar_KeyDown" Margin="0,8,8,8" />
<Button x:Name="NavigateButton" Grid.Column="3" Content="Vai" Click="NavigateButton_Click"
Background="#16A34A" Foreground="White" Padding="20,8" Margin="0,8,8,8"
BorderThickness="0" FontWeight="Bold" FontSize="14" Height="38">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="6">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Button.Template>
</Button>
<Button x:Name="AddCurrentPageButton" Grid.Column="4" Content="? Aggiungi Asta" Click="AddCurrentPageButton_Click"
Background="#8B5CF6" Foreground="White" Padding="16,8" Margin="0,8,8,8"
BorderThickness="0" FontWeight="SemiBold" FontSize="13" Height="38">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="6">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
<!-- WebView -->
<Border Grid.Row="1" Margin="12,0,12,12" CornerRadius="8" Background="#0B1015">
<wv2:WebView2 x:Name="webView" Source="https://it.bidoo.com" Margin="2" />
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,204 @@
using System;
using System.Windows;
using System.Windows.Input;
using Microsoft.Web.WebView2.Core;
namespace AutoBidder
{
/// <summary>
/// Finestra browser separata per navigazione Bidoo
/// </summary>
public partial class BrowserWindow : Window
{
public event Action<string>? OnAddAuction;
public BrowserWindow()
{
InitializeComponent();
Loaded += BrowserWindow_Loaded;
}
private async void BrowserWindow_Loaded(object sender, RoutedEventArgs e)
{
try
{
if (webView.CoreWebView2 == null)
{
await webView.EnsureCoreWebView2Async();
}
webView.NavigationCompleted += WebView_NavigationCompleted;
webView.NavigationStarting += WebView_NavigationStarting;
// Context menu per aggiungere asta
if (webView.CoreWebView2 != null)
{
webView.CoreWebView2.ContextMenuRequested += CoreWebView2_ContextMenuRequested;
webView.CoreWebView2.Navigate("https://it.bidoo.com");
}
}
catch (Exception ex)
{
MessageBox.Show($"Errore inizializzazione: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void CoreWebView2_ContextMenuRequested(object? sender, CoreWebView2ContextMenuRequestedEventArgs e)
{
try
{
var currentUrl = webView.CoreWebView2?.Source ?? "";
if (IsValidAuctionUrl(currentUrl) && webView.CoreWebView2 != null)
{
// Aggiungi voce menu contestuale
var menuItem = webView.CoreWebView2.Environment.CreateContextMenuItem(
"Aggiungi asta al monitoraggio",
null,
CoreWebView2ContextMenuItemKind.Command);
menuItem.CustomItemSelected += (s, args) =>
{
OnAddAuction?.Invoke(currentUrl);
};
e.MenuItems.Insert(0, menuItem);
}
}
catch { }
}
private void WebView_NavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs e)
{
if (!string.IsNullOrEmpty(e.Uri) && !IsBidooUrl(e.Uri))
{
e.Cancel = true;
MessageBox.Show("Solo domini Bidoo consentiti!", "Navigazione Bloccata", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
private void WebView_NavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
{
try
{
Dispatcher.BeginInvoke(() =>
{
BackButton.IsEnabled = webView.CoreWebView2?.CanGoBack ?? false;
var url = webView.CoreWebView2?.Source ?? "";
if (!string.IsNullOrEmpty(url))
{
AddressBar.Text = url;
}
});
}
catch { }
}
private bool IsBidooUrl(string url)
{
if (string.IsNullOrWhiteSpace(url)) return false;
try
{
var uri = new Uri(url);
var host = uri.Host.ToLowerInvariant();
return host.Contains("bidoo.com") || host.Contains("bidoo.it") ||
host.Contains("bidoo.fr") || host.Contains("bidoo.es") ||
host.Contains("bidoo.de");
}
catch
{
return false;
}
}
private bool IsValidAuctionUrl(string url)
{
if (!IsBidooUrl(url)) return false;
try
{
var uri = new Uri(url);
return uri.AbsolutePath.Contains("/asta/") || uri.Query.Contains("?a=");
}
catch
{
return false;
}
}
private void BackButton_Click(object sender, RoutedEventArgs e)
{
webView.CoreWebView2?.GoBack();
}
private void RefreshButton_Click(object sender, RoutedEventArgs e)
{
webView.CoreWebView2?.Reload();
}
private void NavigateButton_Click(object sender, RoutedEventArgs e)
{
NavigateToAddress();
}
private void AddressBar_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
NavigateToAddress();
}
}
private void NavigateToAddress()
{
try
{
var url = AddressBar.Text.Trim();
if (string.IsNullOrEmpty(url)) return;
if (!url.StartsWith("http"))
{
url = "https://" + url;
}
if (!IsBidooUrl(url))
{
MessageBox.Show("Solo URL Bidoo consentiti!", "URL Non Valido", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
webView.CoreWebView2?.Navigate(url);
}
catch (Exception ex)
{
MessageBox.Show($"Errore navigazione: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void AddCurrentPageButton_Click(object sender, RoutedEventArgs e)
{
try
{
var currentUrl = webView.CoreWebView2?.Source ?? "";
if (string.IsNullOrEmpty(currentUrl) || !IsValidAuctionUrl(currentUrl))
{
MessageBox.Show("La pagina corrente non <20> un'asta valida!", "URL Non Valido", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
OnAddAuction?.Invoke(currentUrl);
MessageBox.Show("Asta aggiunta al monitoraggio!", "Successo", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace AutoBidder.Converters
{
public class AndNotPausedConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 2 && values[0] is bool isActive && values[1] is bool isPaused)
{
return isActive && !isPaused;
}
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace AutoBidder.Converters
{
// Converte bool in Opacity: true -> 0.5 (disabilitato), false -> 1.0 (abilitato)
public class BoolToOpacityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool inverse = parameter?.ToString() == "Inverse";
if (value is bool b)
{
if (inverse)
return b ? 0.5 : 1.0;
else
return b ? 1.0 : 0.5;
}
return 1.0;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
// Converte (IsActive, IsPaused) in Opacity per il pulsante Pausa
public class PauseButtonOpacityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 2 && values[0] is bool isActive && values[1] is bool isPaused)
{
return (isActive && !isPaused) ? 1.0 : 0.5;
}
return 0.5;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,9 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:AutoBidder.Converters">
<local:InverseBoolConverter x:Key="InverseBoolConverter"/>
<local:AndNotPausedConverter x:Key="AndNotPausedConverter"/>
<local:StartResumeConverter x:Key="StartResumeConverter"/>
<local:BoolToOpacityConverter x:Key="BoolToOpacityConverter"/>
<local:PauseButtonOpacityConverter x:Key="PauseButtonOpacityConverter"/>
</ResourceDictionary>

View File

@@ -0,0 +1,23 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace AutoBidder.Converters
{
public class InverseBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool b)
return !b;
return true;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool b)
return !b;
return true;
}
}
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace AutoBidder.Converters
{
// Returns true when the Start/Resume button for a row should be enabled:
// Enabled when NOT active (stopped) OR when paused (to resume).
public class StartResumeConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length >= 2 && values[0] is bool isActive && values[1] is bool isPaused)
{
return (!isActive) || isPaused;
}
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,247 @@
using System.Windows;
using System.Windows.Controls;
namespace AutoBidder
{
/// <summary>
/// Dialog per inizializzare sessione Bidoo
/// Richiede: Auth Token + Username
/// </summary>
public class SessionDialog : Window
{
private readonly TextBox _tokenTextBox;
private readonly TextBox _usernameTextBox;
public string AuthToken => _tokenTextBox.Text.Trim();
public string Username => _usernameTextBox.Text.Trim();
public SessionDialog(string existingToken = "", string existingUsername = "")
{
Title = "Configura Sessione Bidoo";
Width = 680;
Height = 420;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
ResizeMode = ResizeMode.NoResize;
Background = System.Windows.Media.Brushes.Black;
Foreground = System.Windows.Media.Brushes.White;
var grid = new Grid { Margin = new Thickness(20) };
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(10) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(140) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(15) });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(10) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(40) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(20) });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
var label1 = new TextBlock
{
Text = "Cookie __stattrb (F12 > Application > Cookies > __stattrb):",
FontWeight = FontWeights.SemiBold,
Foreground = System.Windows.Media.Brushes.White,
TextWrapping = TextWrapping.Wrap
};
Grid.SetRow(label1, 0);
_tokenTextBox = new TextBox
{
Text = existingToken,
Padding = new Thickness(10),
TextWrapping = TextWrapping.Wrap,
AcceptsReturn = false,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
VerticalContentAlignment = VerticalAlignment.Top,
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(15, 15, 15)),
Foreground = System.Windows.Media.Brushes.LightGray,
FontFamily = new System.Windows.Media.FontFamily("Consolas"),
FontSize = 11,
ToolTip = "Esempio: eyJVU0VSSUQiOiI2NzA3NjY0Ii..."
};
Grid.SetRow(_tokenTextBox, 2);
var label2 = new TextBlock
{
Text = "Username Bidoo:",
FontWeight = FontWeights.SemiBold,
Foreground = System.Windows.Media.Brushes.White
};
Grid.SetRow(label2, 4);
_usernameTextBox = new TextBox
{
Text = existingUsername,
Padding = new Thickness(10),
FontSize = 14,
VerticalContentAlignment = VerticalAlignment.Center,
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(15, 15, 15)),
Foreground = System.Windows.Media.Brushes.LightGray
};
Grid.SetRow(_usernameTextBox, 6);
var buttonPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right
};
Grid.SetRow(buttonPanel, 8);
var okButton = new Button
{
Content = "Conferma",
Width = 120,
Height = 40,
Margin = new Thickness(0, 0, 10, 0),
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 204, 102)),
Foreground = System.Windows.Media.Brushes.White,
FontWeight = FontWeights.Bold,
FontSize = 14
};
var cancelButton = new Button
{
Content = "Annulla",
Width = 120,
Height = 40,
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(204, 0, 0)),
Foreground = System.Windows.Media.Brushes.White,
FontWeight = FontWeights.Bold,
FontSize = 14
};
okButton.Click += (s, e) =>
{
if (string.IsNullOrWhiteSpace(AuthToken) || string.IsNullOrWhiteSpace(Username))
{
MessageBox.Show("Inserisci Token e Username!", "Errore", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
DialogResult = true;
Close();
};
cancelButton.Click += (s, e) => { DialogResult = false; Close(); };
buttonPanel.Children.Add(okButton);
buttonPanel.Children.Add(cancelButton);
grid.Children.Add(label1);
grid.Children.Add(_tokenTextBox);
grid.Children.Add(label2);
grid.Children.Add(_usernameTextBox);
grid.Children.Add(buttonPanel);
Content = grid;
Loaded += (s, e) => _tokenTextBox.Focus();
}
}
/// <summary>
/// Dialog semplificato per aggiungere asta (ID o URL completo)
/// </summary>
public class AddAuctionSimpleDialog : Window
{
private readonly TextBox _auctionIdTextBox;
public string AuctionId => _auctionIdTextBox.Text.Trim();
public AddAuctionSimpleDialog()
{
Title = "Aggiungi Asta";
Width = 680;
Height = 280;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
ResizeMode = ResizeMode.NoResize;
Background = System.Windows.Media.Brushes.Black;
Foreground = System.Windows.Media.Brushes.White;
var grid = new Grid { Margin = new Thickness(20) };
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(10) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(50) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(10) });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(20) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(50) });
var label = new TextBlock
{
Text = "URL Asta o ID:",
FontWeight = FontWeights.SemiBold,
Foreground = System.Windows.Media.Brushes.White
};
Grid.SetRow(label, 0);
_auctionIdTextBox = new TextBox
{
Text = "",
Padding = new Thickness(10),
FontSize = 13,
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(15, 15, 15)),
Foreground = System.Windows.Media.Brushes.LightGray,
VerticalContentAlignment = VerticalAlignment.Center
};
Grid.SetRow(_auctionIdTextBox, 2);
var hintLabel = new TextBlock
{
Text = "Esempio: https://it.bidoo.com/auction.php?a=Galaxy_S25_Ultra_256GB_81204324\nOppure: 81204324",
FontSize = 11,
Foreground = System.Windows.Media.Brushes.Gray,
TextWrapping = TextWrapping.Wrap
};
Grid.SetRow(hintLabel, 4);
var buttonPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right
};
Grid.SetRow(buttonPanel, 6);
var okButton = new Button
{
Content = "Aggiungi",
Width = 120,
Height = 40,
Margin = new Thickness(0, 0, 10, 0),
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 204, 102)),
Foreground = System.Windows.Media.Brushes.White,
FontWeight = FontWeights.Bold,
FontSize = 14
};
var cancelButton = new Button
{
Content = "Annulla",
Width = 120,
Height = 40,
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(204, 0, 0)),
Foreground = System.Windows.Media.Brushes.White,
FontWeight = FontWeights.Bold,
FontSize = 14
};
okButton.Click += (s, e) => { DialogResult = true; Close(); };
cancelButton.Click += (s, e) => { DialogResult = false; Close(); };
_auctionIdTextBox.KeyDown += (s, e) =>
{
if (e.Key == System.Windows.Input.Key.Enter)
{
DialogResult = true;
Close();
}
};
buttonPanel.Children.Add(okButton);
buttonPanel.Children.Add(cancelButton);
grid.Children.Add(label);
grid.Children.Add(_auctionIdTextBox);
grid.Children.Add(hintLabel);
grid.Children.Add(buttonPanel);
Content = grid;
Loaded += (s, e) => _auctionIdTextBox.Focus();
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
namespace AutoBidder.Models
{
/// <summary>
/// Informazioni base di un'asta monitorata
/// </summary>
public class AuctionInfo
{
public string AuctionId { get; set; } = "";
public string Name { get; set; } = ""; // Opzionale, può essere lasciato vuoto
public string OriginalUrl { get; set; } = ""; // URL completo dell'asta (per referer)
// Configurazione asta
public int TimerClick { get; set; } = 0; // Secondo del timer per click (default 0)
public int DelayMs { get; set; } = 50; // Ritardo aggiuntivo in ms (per compensare latenza)
public double MinPrice { get; set; } = 0;
public double MaxPrice { get; set; } = 0;
public int MinResets { get; set; } = 0; // Numero minimo reset prima di puntare
public int MaxResets { get; set; } = 0; // Numero massimo reset (0 = illimitati)
// Stato asta
public bool IsActive { get; set; } = true;
public bool IsPaused { get; set; } = false;
// Contatori
public int MyClicks { get; set; } = 0;
public int ResetCount { get; set; } = 0;
// Timestamp
public DateTime AddedAt { get; set; } = DateTime.UtcNow;
public DateTime? LastClickAt { get; set; }
// Storico
public List<BidHistory> BidHistory { get; set; } = new();
public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase);
// Legacy (deprecato, usa BidderStats)
[System.Text.Json.Serialization.JsonIgnore]
public Dictionary<string, int> Bidders { get; set; } = new(StringComparer.OrdinalIgnoreCase);
// Log per-asta (non serializzato)
[System.Text.Json.Serialization.JsonIgnore]
public List<string> AuctionLog { get; set; } = new();
/// <summary>
/// Aggiunge una voce al log dell'asta
/// </summary>
public void AddLog(string message)
{
var entry = $"{DateTime.Now:HH:mm:ss} - {message}";
AuctionLog.Add(entry);
// Mantieni solo ultimi 100 log
if (AuctionLog.Count > 100)
{
AuctionLog.RemoveAt(0);
}
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
namespace AutoBidder.Models
{
/// <summary>
/// Stato real-time di un'asta (snapshot dal polling HTTP)
/// </summary>
public class AuctionState
{
public string AuctionId { get; set; } = "";
// Dati correnti
public double Timer { get; set; } = 999;
public double Price { get; set; } = 0;
public string LastBidder { get; set; } = "";
public bool IsMyBid { get; set; } = false;
// Stato asta
public AuctionStatus Status { get; set; } = AuctionStatus.Unknown;
public string StartTime { get; set; } = ""; // Es: "Oggi alle 17:00" o "23 Ottobre 10:10"
// Timestamp snapshot
public DateTime SnapshotTime { get; set; } = DateTime.UtcNow;
// Latenza polling
public int PollingLatencyMs { get; set; } = 0;
// Dati estratti HTML
public string RawHtml { get; set; } = "";
public bool ParsingSuccess { get; set; } = true;
}
/// <summary>
/// Stato corrente dell'asta
/// </summary>
public enum AuctionStatus
{
Unknown, // Non determinato
Running, // Asta in corso (ON + timer attivo + utenti presenti)
Paused, // Asta in pausa (STOP nelle API - tipicamente 00:00-10:00)
EndedWon, // Asta terminata - HAI VINTO! (OFF + io sono last bidder)
EndedLost, // Asta terminata - Persa (OFF + altro è last bidder)
Pending, // In attesa di inizio (ON + no bidder + expiry < 30min)
Scheduled, // Programmata per più tardi (ON + no bidder + expiry > 30min)
Closed, // Asta chiusa/terminata (generico)
NotStarted // Non ancora iniziata (legacy)
}
}

View File

@@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace AutoBidder.Models
{
/// <summary>
/// Statistiche aggregate di un'asta per dashboard e export
/// </summary>
public class AuctionStatistics
{
public string AuctionId { get; set; } = "";
public string Name { get; set; } = "";
// Tempo monitoraggio
public DateTime MonitoringStarted { get; set; }
public TimeSpan MonitoringDuration { get; set; }
// Contatori
public int TotalBids { get; set; }
public int MyBids { get; set; }
public int OpponentBids { get; set; }
public int Resets { get; set; }
public int UniqueBidders { get; set; }
// Prezzi
public double StartPrice { get; set; }
public double CurrentPrice { get; set; }
public double MinPrice { get; set; }
public double MaxPrice { get; set; }
public double AvgPrice { get; set; }
// Timer
public double AvgTimerAtBid { get; set; }
public double MinTimerReached { get; set; }
// Latenza
public int AvgPollingLatencyMs { get; set; }
public int AvgClickLatencyMs { get; set; }
public int MinClickLatencyMs { get; set; }
public int MaxClickLatencyMs { get; set; }
// Rate
public double BidsPerMinute { get; set; }
public double ResetsPerHour { get; set; }
// Competitor analysis
public string MostActiveBidder { get; set; } = "";
public int MostActiveBidderCount { get; set; }
public Dictionary<string, int> BidderRanking { get; set; } = new();
// Success rate
public double MyBidSuccessRate { get; set; } // % mie puntate sul totale
// Calcola statistiche da BidHistory
public static AuctionStatistics Calculate(AuctionInfo auction)
{
var stats = new AuctionStatistics
{
AuctionId = auction.AuctionId,
Name = auction.Name,
MonitoringStarted = auction.AddedAt,
MonitoringDuration = DateTime.UtcNow - auction.AddedAt,
MyBids = auction.MyClicks,
Resets = auction.ResetCount,
UniqueBidders = auction.Bidders.Count,
BidderRanking = auction.Bidders
};
if (auction.BidHistory.Any())
{
var prices = auction.BidHistory.Select(h => h.Price).Where(p => p > 0).ToList();
if (prices.Any())
{
stats.StartPrice = prices.First();
stats.CurrentPrice = prices.Last();
stats.MinPrice = prices.Min();
stats.MaxPrice = prices.Max();
stats.AvgPrice = prices.Average();
}
stats.TotalBids = auction.BidHistory.Count(h => h.EventType == BidEventType.MyBid || h.EventType == BidEventType.OpponentBid);
stats.OpponentBids = stats.TotalBids - stats.MyBids;
var timers = auction.BidHistory.Select(h => h.Timer).ToList();
if (timers.Any())
{
stats.AvgTimerAtBid = timers.Average();
stats.MinTimerReached = timers.Min();
}
var latencies = auction.BidHistory.Where(h => h.EventType == BidEventType.MyBid).Select(h => h.LatencyMs).ToList();
if (latencies.Any())
{
stats.AvgClickLatencyMs = (int)latencies.Average();
stats.MinClickLatencyMs = latencies.Min();
stats.MaxClickLatencyMs = latencies.Max();
}
if (stats.MonitoringDuration.TotalMinutes > 0)
{
stats.BidsPerMinute = stats.TotalBids / stats.MonitoringDuration.TotalMinutes;
}
if (stats.MonitoringDuration.TotalHours > 0)
{
stats.ResetsPerHour = stats.Resets / stats.MonitoringDuration.TotalHours;
}
if (stats.TotalBids > 0)
{
stats.MyBidSuccessRate = (double)stats.MyBids / stats.TotalBids * 100;
}
}
if (auction.Bidders.Any())
{
var topBidder = auction.Bidders.OrderByDescending(b => b.Value).First();
stats.MostActiveBidder = topBidder.Key;
stats.MostActiveBidderCount = topBidder.Value;
}
return stats;
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
namespace AutoBidder.Models
{
/// <summary>
/// Entry storico per ogni puntata/evento dell'asta
/// </summary>
public class BidHistory
{
public DateTime Timestamp { get; set; }
public BidEventType EventType { get; set; }
public string Bidder { get; set; } = "";
public double Price { get; set; }
public double Timer { get; set; }
public int LatencyMs { get; set; }
public bool Success { get; set; }
public string Notes { get; set; } = "";
}
public enum BidEventType
{
MyBid, // Mia puntata
OpponentBid, // Puntata avversario
Reset, // Reset timer
PriceChange, // Cambio prezzo
AuctionStarted, // Asta iniziata
AuctionEnded // Asta terminata
}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace AutoBidder.Models
{
/// <summary>
/// Risultato di un tentativo di puntata API
/// </summary>
public class BidResult
{
public string AuctionId { get; set; } = "";
public DateTime Timestamp { get; set; }
public bool Success { get; set; }
public int LatencyMs { get; set; }
public string Response { get; set; } = "";
public string Error { get; set; } = "";
public double NewPrice { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace AutoBidder.Models
{
/// <summary>
/// Informazioni su un utente che ha piazzato puntate
/// </summary>
public class BidderInfo
{
public string Username { get; set; } = "";
public int BidCount { get; set; } = 0;
public DateTime LastBidTime { get; set; } = DateTime.MinValue;
public string LastBidTimeDisplay => LastBidTime == DateTime.MinValue
? "-"
: LastBidTime.ToString("HH:mm:ss");
}
}

View File

@@ -0,0 +1,48 @@
using System;
namespace AutoBidder.Models
{
/// <summary>
/// Sessione Bidoo con token di autenticazione
/// </summary>
public class BidooSession
{
/// <summary>
/// Token di autenticazione (estratto da cookie o header)
/// Usato per autenticare tutte le chiamate API
/// </summary>
public string AuthToken { get; set; } = "";
/// <summary>
/// Cookie string completa (opzionale, backup)
/// Formato: "cookie1=value1; cookie2=value2; ..."
/// </summary>
public string CookieString { get; set; } = "";
/// <summary>
/// Username estratto dalla sessione
/// </summary>
public string Username { get; set; } = "";
/// <summary>
/// Puntate rimanenti sull'account
/// </summary>
public int RemainingBids { get; set; } = 0;
/// <summary>
/// Timestamp ultimo aggiornamento info account
/// </summary>
public DateTime LastAccountUpdate { get; set; } = DateTime.MinValue;
/// <summary>
/// Flag sessione valida
/// </summary>
public bool IsValid => !string.IsNullOrWhiteSpace(AuthToken) || !string.IsNullOrWhiteSpace(CookieString);
/// <summary>
/// CSRF Token per puntate (estratto da pagina, opzionale)
/// </summary>
public string? CsrfToken { get; set; }
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,154 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace AutoBidder.Tests
{
/// <summary>
/// Test manuale delle API Bidoo
/// Compilare e eseguire per testare le chiamate
/// </summary>
public class TestBidooApi
{
public static async Task RunAsync(string[] args)
{
Console.WriteLine("=== TEST MANUALE API BIDOO ===\n");
// CONFIGURAZIONE
Console.Write("Inserisci il cookie __stattr: ");
string? cookie = Console.ReadLine();
Console.Write("Inserisci l'ID asta (es: 81417915): ");
string? auctionId = Console.ReadLine();
if (string.IsNullOrWhiteSpace(cookie) || string.IsNullOrWhiteSpace(auctionId))
{
Console.WriteLine("? Cookie o Auction ID mancanti!");
return;
}
Console.WriteLine("\n--- Test 1: Polling Stato Asta ---");
await TestPollingAsync(cookie, auctionId);
Console.WriteLine("\n--- Test 2: Info Utente ---");
await TestUserInfoAsync(cookie);
Console.WriteLine("? Test completati! Premi un tasto per uscire...");
Console.ReadKey();
}
/// <summary>
/// Test chiamata polling asta
/// </summary>
private static async Task TestPollingAsync(string cookie, string auctionId)
{
try
{
var handler = new HttpClientHandler { UseCookies = false };
using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(5) };
var url = $"https://it.bidoo.com/data.php?ALL={auctionId}&LISTID=0";
Console.WriteLine($"?? GET {url}");
var request = new HttpRequestMessage(HttpMethod.Get, url);
// Headers
request.Headers.Add("Cookie", $"__stattr={cookie}");
request.Headers.Add("X-Requested-With", "XMLHttpRequest");
request.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
request.Headers.Add("Referer", $"https://it.bidoo.com/asta/nome-prodotto-{auctionId}");
var startTime = DateTime.UtcNow;
var response = await client.SendAsync(request);
var latency = (int)(DateTime.UtcNow - startTime).TotalMilliseconds;
Console.WriteLine($"?? Status: {(int)response.StatusCode} {response.StatusCode}");
Console.WriteLine($"?? Latency: {latency}ms");
var responseText = await response.Content.ReadAsStringAsync();
Console.WriteLine($"?? Response ({responseText.Length} bytes):");
Console.WriteLine(responseText);
// Parse semplice
if (response.IsSuccessStatusCode && responseText.Contains("*"))
{
Console.WriteLine("\n?? Parsing:");
var parts = responseText.Split('*');
Console.WriteLine($" Server Time: {parts[0]}");
if (parts.Length > 1)
{
var data = parts[1].Replace("[", "").Replace("]", "").Split(';');
if (data.Length >= 5)
{
Console.WriteLine($" Auction ID: {data[0]}");
Console.WriteLine($" Status: {data[1]}");
Console.WriteLine($" Expiry: {data[2]}");
if (int.TryParse(data[3], out var priceIndex))
{
Console.WriteLine($" Price: {priceIndex} ? €{(priceIndex * 0.01):F2}");
}
Console.WriteLine($" Last Bidder: {data[4]}");
// Calcola timer
if (long.TryParse(data[2], out var expiryTs))
{
var expiryTime = DateTimeOffset.FromUnixTimeSeconds(expiryTs);
var timer = (expiryTime - DateTimeOffset.UtcNow).TotalSeconds;
Console.WriteLine($" Timer: {Math.Max(0, timer):F2}s");
}
}
}
}
else
{
Console.WriteLine("?? Risposta non valida o errore");
}
}
catch (Exception ex)
{
Console.WriteLine($"? Errore: {ex.GetType().Name} - {ex.Message}");
}
}
/// <summary>
/// Test chiamata info utente
/// </summary>
private static async Task TestUserInfoAsync(string cookie)
{
try
{
var handler = new HttpClientHandler { UseCookies = false };
using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(5) };
var url = "https://it.bidoo.com/ajax/get_auction_bids_info_banner.php";
Console.WriteLine($"?? GET {url}");
var request = new HttpRequestMessage(HttpMethod.Get, url);
// Headers
request.Headers.Add("Cookie", $"__stattr={cookie}");
request.Headers.Add("X-Requested-With", "XMLHttpRequest");
request.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
request.Headers.Add("Referer", "https://it.bidoo.com/");
var startTime = DateTime.UtcNow;
var response = await client.SendAsync(request);
var latency = (int)(DateTime.UtcNow - startTime).TotalMilliseconds;
Console.WriteLine($"?? Status: {(int)response.StatusCode} {response.StatusCode}");
Console.WriteLine($"?? Latency: {latency}ms");
var responseText = await response.Content.ReadAsStringAsync();
Console.WriteLine($"?? Response ({responseText.Length} bytes):");
Console.WriteLine(responseText);
}
catch (Exception ex)
{
Console.WriteLine($"? Errore: {ex.GetType().Name} - {ex.Message}");
}
}
}
}

View File

@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using AutoBidder.Models;
namespace AutoBidder.Utilities
{
/// <summary>
/// Esporta statistiche aste in formato CSV
/// </summary>
public static class CsvExporter
{
/// <summary>
/// Esporta cronologia completa di un'asta in CSV
/// </summary>
public static void ExportAuctionHistory(AuctionInfo auction, string filePath)
{
var csv = new StringBuilder();
// Header
csv.AppendLine("Timestamp,Event Type,Bidder,Price,Timer,Latency (ms),Success,Notes");
// Data
foreach (var entry in auction.BidHistory.OrderBy(h => h.Timestamp))
{
csv.AppendLine($"{entry.Timestamp:yyyy-MM-dd HH:mm:ss.fff}," +
$"{entry.EventType}," +
$"\"{entry.Bidder}\"," +
$"{entry.Price:F2}," +
$"{entry.Timer:F2}," +
$"{entry.LatencyMs}," +
$"{entry.Success}," +
$"\"{entry.Notes}\"");
}
File.WriteAllText(filePath, csv.ToString(), Encoding.UTF8);
}
/// <summary>
/// Esporta statistiche aggregate di un'asta in CSV
/// </summary>
public static void ExportAuctionStatistics(AuctionInfo auction, string filePath)
{
var stats = AuctionStatistics.Calculate(auction);
var csv = new StringBuilder();
// Informazioni asta
csv.AppendLine("=== AUCTION INFO ===");
csv.AppendLine($"Auction ID,{stats.AuctionId}");
csv.AppendLine($"Name,\"{stats.Name}\"");
csv.AppendLine($"Monitoring Started,{stats.MonitoringStarted:yyyy-MM-dd HH:mm:ss}");
csv.AppendLine($"Monitoring Duration,{stats.MonitoringDuration}");
csv.AppendLine();
// Contatori
csv.AppendLine("=== COUNTERS ===");
csv.AppendLine($"Total Bids,{stats.TotalBids}");
csv.AppendLine($"My Bids,{stats.MyBids}");
csv.AppendLine($"Opponent Bids,{stats.OpponentBids}");
csv.AppendLine($"Resets,{stats.Resets}");
csv.AppendLine($"Unique Bidders,{stats.UniqueBidders}");
csv.AppendLine();
// Prezzi
csv.AppendLine("=== PRICES ===");
csv.AppendLine($"Start Price,{stats.StartPrice:F2}");
csv.AppendLine($"Current Price,{stats.CurrentPrice:F2}");
csv.AppendLine($"Min Price,{stats.MinPrice:F2}");
csv.AppendLine($"Max Price,{stats.MaxPrice:F2}");
csv.AppendLine($"Avg Price,{stats.AvgPrice:F2}");
csv.AppendLine();
// Performance
csv.AppendLine("=== PERFORMANCE ===");
csv.AppendLine($"Avg Click Latency (ms),{stats.AvgClickLatencyMs}");
csv.AppendLine($"Min Click Latency (ms),{stats.MinClickLatencyMs}");
csv.AppendLine($"Max Click Latency (ms),{stats.MaxClickLatencyMs}");
csv.AppendLine($"Bids Per Minute,{stats.BidsPerMinute:F2}");
csv.AppendLine($"Resets Per Hour,{stats.ResetsPerHour:F2}");
csv.AppendLine($"My Bid Success Rate,{stats.MyBidSuccessRate:F1}%");
csv.AppendLine();
// Competitor ranking
csv.AppendLine("=== BIDDER RANKING ===");
csv.AppendLine("Bidder,Bids Count");
foreach (var bidder in stats.BidderRanking.OrderByDescending(b => b.Value))
{
csv.AppendLine($"\"{bidder.Key}\",{bidder.Value}");
}
File.WriteAllText(filePath, csv.ToString(), Encoding.UTF8);
}
/// <summary>
/// Esporta tutte le aste in un unico CSV
/// </summary>
public static void ExportAllAuctions(IEnumerable<AuctionInfo> auctions, string filePath)
{
var csv = new StringBuilder();
// Header
csv.AppendLine("Auction ID,Name,Timer Click,Delay (ms),Min Price,Max Price," +
"My Clicks,Resets,Total Bidders,Active,Paused," +
"Added At,Last Click At");
// Data
foreach (var auction in auctions)
{
csv.AppendLine($"{auction.AuctionId}," +
$"\"{auction.Name}\"," +
$"{auction.TimerClick}," +
$"{auction.DelayMs}," +
$"{auction.MinPrice:F2}," +
$"{auction.MaxPrice:F2}," +
$"{auction.MyClicks}," +
$"{auction.ResetCount}," +
$"{auction.Bidders.Count}," +
$"{auction.IsActive}," +
$"{auction.IsPaused}," +
$"{auction.AddedAt:yyyy-MM-dd HH:mm:ss}," +
$"{(auction.LastClickAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "")}");
}
File.WriteAllText(filePath, csv.ToString(), Encoding.UTF8);
}
}
}

View File

@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using AutoBidder.Models;
namespace AutoBidder.Utilities
{
/// <summary>
/// Gestisce salvataggio/caricamento lista aste
/// </summary>
public static class PersistenceManager
{
private static readonly string AppDataFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AutoBidder"
);
private static readonly string AuctionsFilePath = Path.Combine(AppDataFolder, "auctions.json");
/// <summary>
/// Salva lista aste su disco
/// </summary>
public static void SaveAuctions(IEnumerable<AuctionInfo> auctions)
{
try
{
// Crea cartella se non esiste
if (!Directory.Exists(AppDataFolder))
{
Directory.CreateDirectory(AppDataFolder);
}
// Serializza in JSON
var options = new JsonSerializerOptions
{
WriteIndented = true
};
var json = JsonSerializer.Serialize(auctions, options);
File.WriteAllText(AuctionsFilePath, json);
Console.WriteLine($"? Aste salvate in {AuctionsFilePath}");
}
catch (Exception ex)
{
Console.WriteLine($"? Errore salvataggio aste: {ex.Message}");
}
}
/// <summary>
/// Carica lista aste da disco
/// </summary>
public static List<AuctionInfo> LoadAuctions()
{
try
{
if (!File.Exists(AuctionsFilePath))
{
return new List<AuctionInfo>();
}
var json = File.ReadAllText(AuctionsFilePath);
var auctions = JsonSerializer.Deserialize<List<AuctionInfo>>(json);
Console.WriteLine($"? Caricate {auctions?.Count ?? 0} aste da {AuctionsFilePath}");
return auctions ?? new List<AuctionInfo>();
}
catch (Exception ex)
{
Console.WriteLine($"? Errore caricamento aste: {ex.Message}");
return new List<AuctionInfo>();
}
}
}
}

View File

@@ -0,0 +1,221 @@
using System;
using System.ComponentModel;
using AutoBidder.Models;
namespace AutoBidder.ViewModels
{
/// <summary>
/// ViewModel per una riga della griglia aste (DataBinding)
/// </summary>
public class AuctionViewModel : INotifyPropertyChanged
{
private readonly AuctionInfo _auctionInfo;
private AuctionState? _lastState;
public AuctionViewModel(AuctionInfo auctionInfo)
{
_auctionInfo = auctionInfo;
}
public AuctionInfo AuctionInfo => _auctionInfo;
// Proprietà base
public string AuctionId => _auctionInfo.AuctionId;
public string Name => _auctionInfo.Name;
// Configurazione
public int TimerClick
{
get => _auctionInfo.TimerClick;
set
{
_auctionInfo.TimerClick = value;
OnPropertyChanged(nameof(TimerClick));
}
}
public int DelayMs
{
get => _auctionInfo.DelayMs;
set
{
_auctionInfo.DelayMs = value;
OnPropertyChanged(nameof(DelayMs));
}
}
public double MinPrice
{
get => _auctionInfo.MinPrice;
set
{
_auctionInfo.MinPrice = value;
OnPropertyChanged(nameof(MinPrice));
}
}
public double MaxPrice
{
get => _auctionInfo.MaxPrice;
set
{
_auctionInfo.MaxPrice = value;
OnPropertyChanged(nameof(MaxPrice));
}
}
// Stato
public bool IsActive
{
get => _auctionInfo.IsActive;
set
{
_auctionInfo.IsActive = value;
OnPropertyChanged(nameof(IsActive));
}
}
public bool IsPaused
{
get => _auctionInfo.IsPaused;
set
{
_auctionInfo.IsPaused = value;
OnPropertyChanged(nameof(IsPaused));
}
}
// Contatori
public int MyClicks => _auctionInfo.MyClicks;
public int ResetCount => _auctionInfo.ResetCount;
// Dati live (da ultimo polling)
public string TimerDisplay
{
get
{
if (_lastState == null) return "-";
return _lastState.Status switch
{
AuctionStatus.EndedWon or AuctionStatus.EndedLost or AuctionStatus.Closed => "TERMINATA",
AuctionStatus.Paused => "IN PAUSA",
AuctionStatus.Pending => FormatPendingTime(_lastState.Timer),
AuctionStatus.Scheduled => FormatScheduledTime(_lastState.Timer),
AuctionStatus.Running when _lastState.Timer < 999 => $"{_lastState.Timer:F2}s",
AuctionStatus.Running when _lastState.Timer >= 999 => FormatLongTime(_lastState.Timer),
_ => "-"
};
}
}
private string FormatPendingTime(double seconds)
{
if (seconds < 60) return $"{seconds:F0}s";
int minutes = (int)(seconds / 60);
return $"{minutes}min";
}
private string FormatScheduledTime(double seconds)
{
if (seconds < 3600) // < 1 ora
{
int minutes = (int)(seconds / 60);
return $"{minutes}min";
}
int hours = (int)(seconds / 3600);
return $"{hours}h";
}
private string FormatLongTime(double seconds)
{
if (seconds < 3600) // < 1 ora
{
int minutes = (int)(seconds / 60);
return $"{minutes}min";
}
int hours = (int)(seconds / 3600);
return $"{hours}h";
}
public string PriceDisplay => _lastState != null && _lastState.Price > 0
? $"{_lastState.Price:F2}"
: "-";
public string LastBidder => _lastState?.LastBidder ?? "-";
public bool IsMyBid => _lastState?.IsMyBid ?? false;
public string StatusDisplay
{
get
{
if (_lastState == null) return "Sconosciuto";
return _lastState.Status switch
{
AuctionStatus.Running => "In Corso",
AuctionStatus.EndedWon => "VINTA",
AuctionStatus.EndedLost => "Persa",
AuctionStatus.Pending => "Inizia presto",
AuctionStatus.Scheduled => "Programmata",
AuctionStatus.Paused => "In Pausa",
AuctionStatus.Closed => "Chiusa",
_ => "Sconosciuto"
};
}
}
public string Strategy
{
get
{
if (_lastState == null) return "Idle";
// Gestione stati speciali
if (_lastState.Status == AuctionStatus.Pending)
return $"Inizia presto: {_lastState.StartTime}";
if (_lastState.Status == AuctionStatus.Scheduled)
return $"Programmata: {_lastState.StartTime}";
// Stati normali running
if (_lastState.Timer < 2) return "Active";
if (_lastState.Timer < 10) return "Fast";
if (_lastState.Timer < 30) return "HTTP";
return "Slow";
}
}
/// <summary>
/// Aggiorna stato da polling
/// </summary>
public void UpdateState(AuctionState state)
{
_lastState = state;
// Notifica tutte le proprietà dipendenti dallo stato
OnPropertyChanged(nameof(TimerDisplay));
OnPropertyChanged(nameof(PriceDisplay));
OnPropertyChanged(nameof(LastBidder));
OnPropertyChanged(nameof(IsMyBid));
OnPropertyChanged(nameof(StatusDisplay));
OnPropertyChanged(nameof(Strategy));
}
/// <summary>
/// Notifica cambio contatori
/// </summary>
public void RefreshCounters()
{
OnPropertyChanged(nameof(MyClicks));
OnPropertyChanged(nameof(ResetCount));
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}