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:
@@ -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>
|
||||
|
||||
93
Mimante/BrowserWindow.xaml
Normal file
93
Mimante/BrowserWindow.xaml
Normal 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>
|
||||
204
Mimante/BrowserWindow.xaml.cs
Normal file
204
Mimante/BrowserWindow.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Mimante/Converters/AndNotPausedConverter.cs
Normal file
23
Mimante/Converters/AndNotPausedConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
46
Mimante/Converters/BoolToOpacityConverter.cs
Normal file
46
Mimante/Converters/BoolToOpacityConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Mimante/Converters/Converters.xaml
Normal file
9
Mimante/Converters/Converters.xaml
Normal 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>
|
||||
23
Mimante/Converters/InverseBoolConverter.cs
Normal file
23
Mimante/Converters/InverseBoolConverter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Mimante/Converters/StartResumeConverter.cs
Normal file
25
Mimante/Converters/StartResumeConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
247
Mimante/Dialogs/SessionDialogs.cs
Normal file
247
Mimante/Dialogs/SessionDialogs.cs
Normal 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
62
Mimante/Models/AuctionInfo.cs
Normal file
62
Mimante/Models/AuctionInfo.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
Mimante/Models/AuctionState.cs
Normal file
48
Mimante/Models/AuctionState.cs
Normal 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)
|
||||
}
|
||||
}
|
||||
126
Mimante/Models/AuctionStatistics.cs
Normal file
126
Mimante/Models/AuctionStatistics.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Mimante/Models/BidHistory.cs
Normal file
29
Mimante/Models/BidHistory.cs
Normal 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
|
||||
}
|
||||
}
|
||||
18
Mimante/Models/BidResult.cs
Normal file
18
Mimante/Models/BidResult.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
18
Mimante/Models/BidderInfo.cs
Normal file
18
Mimante/Models/BidderInfo.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
48
Mimante/Models/BidooSession.cs
Normal file
48
Mimante/Models/BidooSession.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
|
||||
466
Mimante/Services/AuctionMonitor.cs
Normal file
466
Mimante/Services/AuctionMonitor.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
627
Mimante/Services/BidooApiClient.cs
Normal file
627
Mimante/Services/BidooApiClient.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
143
Mimante/Services/SessionManager.cs
Normal file
143
Mimante/Services/SessionManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
154
Mimante/Tests/TestBidooApi.cs
Normal file
154
Mimante/Tests/TestBidooApi.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
129
Mimante/Utilities/CsvExporter.cs
Normal file
129
Mimante/Utilities/CsvExporter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
77
Mimante/Utilities/PersistenceManager.cs
Normal file
77
Mimante/Utilities/PersistenceManager.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
221
Mimante/ViewModels/AuctionViewModel.cs
Normal file
221
Mimante/ViewModels/AuctionViewModel.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
311
README.md
311
README.md
@@ -3,7 +3,7 @@
|
||||
> **Programma intelligente per automatizzare le offerte su Bidoo.com**
|
||||
> Monitora le aste in tempo reale e piazza offerte precise al secondo ottimale per massimizzare le probabilità di vincita.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
@@ -171,14 +171,41 @@ dotnet run
|
||||
- Puntare automaticamente sulla **più conveniente**
|
||||
- Gestire più strategie simultaneamente
|
||||
|
||||
**Workflow:**
|
||||
**Due metodi di monitoraggio:**
|
||||
|
||||
#### ??? Metodo Automatico (Preferiti)
|
||||
```
|
||||
1. Click "Multi-Asta" (già attivo di default)
|
||||
2. Le aste preferite vengono rilevate automaticamente
|
||||
3. Click "Avvia"
|
||||
4. Il programma sceglie autonomamente dove puntare
|
||||
2. Naviga ai Preferiti su Bidoo (auto)
|
||||
3. Le aste preferite vengono rilevate automaticamente
|
||||
4. Click "Avvia"
|
||||
```
|
||||
|
||||
#### ??? Metodo Manuale (URL Diretti) ? RACCOMANDATO
|
||||
```
|
||||
1. Click "Multi-Asta"
|
||||
2. Metodo A: Click "+ URL" ? Incolla URL asta
|
||||
Metodo B: Click "?? Pagina" ? Aggiungi pagina corrente
|
||||
3. Ripeti per ogni asta
|
||||
4. Click "Avvia"
|
||||
```
|
||||
|
||||
**Formati URL supportati:**
|
||||
- ? `https://it.bidoo.com/asta/123456`
|
||||
- ? `https://it.bidoo.com/auction.php?a=Galaxy_A26_5G_6_128_81353316`
|
||||
|
||||
**Nuove Funzionalità v2.9:**
|
||||
- ?? **Persistenza automatica** - Lista aste salvata e ricaricata all'avvio
|
||||
- ?? **Quick Add Pagina** - Aggiungi l'asta che stai visualizzando con 1 click
|
||||
- ?? **Export CSV** - Esporta statistiche complete in Excel
|
||||
- ?? **Indicatore Strategia** - Vedi quale metodo usa ogni asta (HTTP/WebView/Active)
|
||||
|
||||
**Vantaggi Metodo Manuale:**
|
||||
- ? **Polling HTTP ultra-veloce** per aste lontane
|
||||
- ?? **CPU/RAM quasi zero** (no rendering browser)
|
||||
- ?? **Precisione massima** quando timer < 10s (auto-switch a WebView)
|
||||
- ?? **Strategia adattiva** automatica per ogni asta
|
||||
|
||||
**Strategia automatica:**
|
||||
```
|
||||
1. Legge timer di tutte le aste
|
||||
@@ -192,6 +219,7 @@ dotnet run
|
||||
- ?? **Asta:** Nome prodotto
|
||||
- ?? **Timer:** Tempo rimanente (aggiornamento real-time)
|
||||
- ?? **Prezzo:** Prezzo corrente
|
||||
- ?? **Strategia:** Metodo polling corrente (?? HTTP / ?? WebView / ? Active)
|
||||
- ??? **Clicks:** Tue puntate su questa asta
|
||||
- ?? **Resets:** Numero di reset rilevati
|
||||
- ?? **Ultimo:** Ultimo utente che ha puntato
|
||||
@@ -205,6 +233,31 @@ dotnet run
|
||||
|
||||
## ??? Funzionalità Avanzate
|
||||
|
||||
### ?? Gestione Aste (Multi-Asta v2.9)
|
||||
|
||||
#### Pulsanti Toolbar
|
||||
- **?? Pagina** - Aggiungi asta corrente visualizzata nel browser
|
||||
- **+ URL** - Aggiungi asta manualmente da URL
|
||||
- **-** - Rimuovi asta selezionata
|
||||
- **?? CSV** - Esporta statistiche complete in formato Excel
|
||||
|
||||
#### Persistenza Dati
|
||||
Le aste aggiunte manualmente vengono **salvate automaticamente** in:
|
||||
```
|
||||
%AppData%\AutoBidder\auctions.json
|
||||
```
|
||||
All'avvio, la lista viene **ricaricata automaticamente**.
|
||||
|
||||
#### Export Statistiche CSV
|
||||
File esportato contiene:
|
||||
- Nome asta, ID, URL
|
||||
- Timer corrente, Prezzo, Strategia
|
||||
- Miei click, Reset, Ultimo bidder
|
||||
- Impostazioni per-asta (Timer Click, Min/Max Prezzo)
|
||||
- Totale bidders competitori
|
||||
|
||||
---
|
||||
|
||||
### ?? Gestione Per-Asta (Multi-Asta)
|
||||
|
||||
**Seleziona un'asta** dalla griglia per accedere a:
|
||||
@@ -326,8 +379,174 @@ dotnet run
|
||||
|
||||
---
|
||||
|
||||
## ?? Polling Adattivo Tecnico (Multi-Asta v2.10)
|
||||
|
||||
### ?? Sistema Dual-Track: Polling + Click
|
||||
|
||||
AutoBidder v2.10 utilizza un **sistema a doppio binario** per massimizzare efficienza:
|
||||
|
||||
#### Track 1: Background Polling (Sempre Attivo) ??
|
||||
```
|
||||
? Attivo SEMPRE (anche senza "Avvia")
|
||||
? Polling HTTP ogni 5 secondi
|
||||
? Aggiorna: Timer, Prezzo, Ultimo Bidder
|
||||
? Calcola strategia polling automatica
|
||||
? CPU: <1% | RAM: 0MB
|
||||
? NO CLICK - Solo monitoraggio
|
||||
```
|
||||
|
||||
#### Track 2: Click Loop (Solo con Automazione) ?
|
||||
```
|
||||
? Attivo SOLO dopo click "Avvia"
|
||||
? Polling dinamico 20-400ms
|
||||
? Click HTTP diretto (10-30ms latenza)
|
||||
? Fallback WebView automatico
|
||||
? CPU: 5-15% | RAM: 25-50MB
|
||||
? CLICK ATTIVI - Puntate reali
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ? Click HTTP Diretto (Novità v2.10)
|
||||
|
||||
**Tecnologia:** Reverse Engineering endpoint Bidoo
|
||||
|
||||
**Come Funziona:**
|
||||
1. All'avvio automazione: Sincronizza cookie da WebView2 ? HttpClient
|
||||
2. Quando timer ? impostato: Invia GET a `/bid.php?AID=...&sup=0&shock=0`
|
||||
3. Risposta server: `ok|155|0|0|1|0|81204347|0|0`
|
||||
4. Se fallisce: Fallback automatico a click WebView
|
||||
|
||||
**Performance:**
|
||||
```
|
||||
Latenza: 10-30ms (vs 50-100ms WebView) ? ? 3-5x PIÙ VELOCE
|
||||
RAM: ~0MB (vs 50MB WebView) ? ?? 100% RISPARMIO
|
||||
CPU: <1% (vs 15% WebView) ? ?? 15x MENO CPU
|
||||
```
|
||||
|
||||
**Log Esempio:**
|
||||
```
|
||||
? Cookie sincronizzati (12 cookie)
|
||||
?? Timer asta 81204347: 0.8s
|
||||
? Click HTTP riuscito in 18ms ? ok|155|0|0|1|0|81204347|0|0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ?? Sincronizzazione Cookie
|
||||
|
||||
**Problema:** Click HTTP necessita cookie di sessione Bidoo
|
||||
**Soluzione:** Estrazione automatica da WebView2
|
||||
|
||||
**Processo:**
|
||||
```
|
||||
1. Click "Avvia" ? Trigger sincronizzazione
|
||||
2. Estrae cookie da webView.CoreWebView2.CookieManager
|
||||
3. Crea HttpClient dedicato con CookieContainer
|
||||
4. Copia tutti cookie Bidoo (PHPSESSID, user_token, dess, ecc.)
|
||||
5. Usa HttpClient per tutti i click HTTP
|
||||
```
|
||||
|
||||
**Sicurezza:**
|
||||
- ? Cookie gestiti solo in memoria RAM
|
||||
- ? Mai salvati su disco
|
||||
- ? Scadenza automatica con sessione
|
||||
|
||||
---
|
||||
|
||||
### ?? Strategie Polling Automatiche
|
||||
|
||||
Il sistema utilizza **3 strategie** basate sul timer dell'asta:
|
||||
|
||||
| Timer Asta | Strategia | Tecnologia | CPU | RAM | Polling | Precisione Click |
|
||||
|------------|-----------|------------|-----|-----|---------|------------------|
|
||||
| **> 30s** | ?? HTTP Headless | HttpClient + Regex | ~0% | 0 MB | 5s | Media (±500ms) |
|
||||
| **10-30s** | ?? WebView Rotation | WebView2 Background | ~5% | 50 MB | 1-2s | Alta (±100ms) |
|
||||
| **< 10s** | ? HTTP Click Active | HTTP Direct + Fallback | ~5% | 0 MB | 20ms | **Massima (±10ms)** |
|
||||
|
||||
### Come Funziona Internamente (v2.10)
|
||||
|
||||
**Fase 1: Avvio Applicazione**
|
||||
```
|
||||
???????????????????????????
|
||||
? Carica aste salvate ? ? auctions.json
|
||||
? Avvia Background Polling? ? HTTP ogni 5s
|
||||
? Naviga ai Preferiti ? ? Se Multi-Asta
|
||||
???????????????????????????
|
||||
```
|
||||
|
||||
**Fase 2: Background Polling (Sempre Attivo)**
|
||||
```
|
||||
???????????????????????????
|
||||
? HTTP GET ogni 5s ? ? Parsing HTML con Regex
|
||||
? Nessun rendering ? ? Estrai: Timer, Prezzo, Bidder
|
||||
? CPU: <1% per asta ? ? Aggiorna cache locale
|
||||
? ?? NO CLICK ? ? Solo monitoraggio dati
|
||||
???????????????????????????
|
||||
```
|
||||
|
||||
**Fase 3: Click "Avvia" ? Sincronizza Cookie**
|
||||
```
|
||||
???????????????????????????
|
||||
? Estrai cookie WebView2 ? ? PHPSESSID, user_token, dess
|
||||
? Crea HttpClient dedicato? ? Con CookieContainer
|
||||
? Avvia Click Loop ? ? MultiAuctionLoop attivo
|
||||
???????????????????????????
|
||||
```
|
||||
|
||||
**Fase 4: Click Loop (Solo se Automazione Attiva)**
|
||||
```
|
||||
???????????????????????????
|
||||
? Trova asta timer < X ? ? Auto-switch asta critica
|
||||
? Verifica prezzi/pausa ? ? Skip se fuori range
|
||||
? ? TENTATIVO 1: ?
|
||||
? HTTP Click Diretto ? ? GET /bid.php?AID=...
|
||||
? Latenza: 10-30ms ? ? ok|155|0|0|1|0|...
|
||||
? ?? TENTATIVO 2: ?
|
||||
? Fallback WebView ? ? Se HTTP fallisce
|
||||
? Latenza: 50-100ms ? ? JavaScript ExecuteScript
|
||||
???????????????????????????
|
||||
```
|
||||
|
||||
**Fase 5: Gestione Risposta**
|
||||
```
|
||||
???????????????????????????
|
||||
? Parsing risposta HTTP ? ? ok|155|... = Successo
|
||||
? Incrementa contatori ? ? MyClicks++
|
||||
? Aggiorna UI griglia ? ? Riga verde se tuo click
|
||||
? Log dettagliato ? ? ? Click HTTP 18ms
|
||||
???????????????????????????
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Esempio Pratico: 50 Aste Monitorate (v2.10)
|
||||
|
||||
**Composizione tipica:**
|
||||
- **40 aste** con timer > 30s ? Background Polling HTTP (CPU: ~0%, RAM: 0MB)
|
||||
- **8 aste** con timer 10-30s ? Background Polling HTTP (CPU: ~0%, RAM: 0MB)
|
||||
- **2 aste** con timer < 10s ? Click HTTP Diretto (CPU: ~5%, RAM: 0MB)
|
||||
- **1 asta** timer critico < 2s ? Fallback WebView se necessario (CPU: +10%, RAM: +50MB)
|
||||
|
||||
**Risultato:**
|
||||
- ? CPU Totale: ~5-15% (vs 80%+ v2.7)
|
||||
- ? RAM Totale: ~50-100 MB (vs 1.5 GB+ v2.7)
|
||||
- ? Click Precision: ±10ms (HTTP) / ±50ms (WebView fallback)
|
||||
- ? Latenza Media Click: **18ms** (vs 75ms v2.8)
|
||||
|
||||
---
|
||||
|
||||
## ?? Strategie Consigliate
|
||||
|
||||
### Multi-Asta - Monitoraggio Massivo (50+ Aste)
|
||||
```
|
||||
Metodo: URL Manuale
|
||||
Timer Click: 0 (ultra-aggressivo)
|
||||
Max Price: 15€ (solo occasioni)
|
||||
Polling: Automatico (adattivo)
|
||||
```
|
||||
**Perché:** Il polling HTTP consuma zero risorse finché non serve
|
||||
|
||||
### Asta Singola - Oggetto di Valore Alto
|
||||
```
|
||||
Timer Click: 0-1 (molto aggressivo)
|
||||
@@ -386,6 +605,63 @@ Il programma **salta il click** se il prezzo
|
||||
3. Click **"Pausa"** nel pannello dettagli
|
||||
4. Solo quella asta viene fermata, le altre continuano
|
||||
|
||||
### Come aggiungo velocemente un'asta che sto guardando?
|
||||
1. Naviga all'asta su Bidoo nel browser integrato
|
||||
2. Passa a **Multi-Asta**
|
||||
3. Click **"?? Pagina"** in alto a destra
|
||||
4. L'URL viene pre-compilato automaticamente
|
||||
5. Click **"? Aggiungi"**
|
||||
|
||||
### Dove vengono salvate le mie aste?
|
||||
Le aste aggiunte manualmente vengono salvate in:
|
||||
```
|
||||
C:\Users\[TuoNome]\AppData\Roaming\AutoBidder\auctions.json
|
||||
```
|
||||
Vengono **ricaricate automaticamente** all'avvio.
|
||||
|
||||
### Come esporto le statistiche?
|
||||
1. Passa a **Multi-Asta**
|
||||
2. Click **"?? CSV"** in alto
|
||||
3. Scegli dove salvare il file
|
||||
4. Apri con Excel o qualsiasi programma CSV
|
||||
|
||||
### Cosa significa "?? HTTP", "?? WebView", "? Active"?
|
||||
Sono le **strategie di polling** automatiche:
|
||||
- **?? HTTP** - Timer > 30s, polling HTTP leggero (background)
|
||||
- **?? WebView** - Timer 10-30s, WebView in background (legacy)
|
||||
- **? Active** - Timer < 10s, **Click HTTP diretto** (10-30ms latenza!)
|
||||
|
||||
### Come funziona il Click HTTP? (Novità v2.10)
|
||||
Quando clicchi **"Avvia"**:
|
||||
1. I cookie vengono **copiati** da WebView2 ? HttpClient
|
||||
2. Al momento del click, invia **richiesta GET diretta** a Bidoo:
|
||||
```
|
||||
GET https://it.bidoo.com/bid.php?AID=81204347&sup=0&shock=0
|
||||
Cookie: PHPSESSID=abc123; user_token=xyz789; ...
|
||||
```
|
||||
3. Bidoo risponde in **10-30ms** (vs 50-100ms WebView)
|
||||
4. Se fallisce: **Fallback automatico** a WebView
|
||||
|
||||
**Vantaggi:**
|
||||
- ? **3-5x più veloce** di WebView
|
||||
- ?? **Zero RAM** per i click
|
||||
- ?? **15x meno CPU**
|
||||
|
||||
**Svantaggi:**
|
||||
- ?? Richiede **login manuale** iniziale su Bidoo (una volta)
|
||||
- ?? Cookie **scadono** dopo X ore (ri-sincronizza con "Avvia")
|
||||
|
||||
### Perché non vedo "Click HTTP riuscito" nei log?
|
||||
Possibili cause:
|
||||
1. **Non hai cliccato "Avvia"** ? Solo background polling attivo
|
||||
2. **Cookie non sincronizzati** ? Fai login su Bidoo e riprova "Avvia"
|
||||
3. **Timer troppo alto** ? Click HTTP si attiva solo quando timer < TimerClick
|
||||
4. **Fallback WebView attivo** ? Se HTTP fallisce, usa WebView (silenzioso)
|
||||
|
||||
Cerca nei log:
|
||||
- ? `Cookie sincronizzati (X cookie)` ? HTTP click pronto
|
||||
- ?? `Nessun cookie trovato` ? Fai login su Bidoo
|
||||
|
||||
### Il Multi-Click migliora le probabilità?
|
||||
**Sì!** Invia **2 click paralleli** a 20ms di distanza per compensare lag di rete.
|
||||
?? Usa solo in Asta Singola su connessioni instabili.
|
||||
@@ -408,7 +684,30 @@ Il programma **emula comportamento umano**:
|
||||
|
||||
## ?? Changelog
|
||||
|
||||
### v2.7 (Corrente) - Layout Verticale
|
||||
### v2.10 (Corrente) - HTTP Click Diretto ?
|
||||
- ? **Reverse Engineering Completato** - Endpoint Bidoo scoperto
|
||||
- ? **Click HTTP Diretto** - Latenza 10-30ms (vs 50-100ms WebView)
|
||||
- ? **Sincronizzazione Cookie** - Automatica da WebView2
|
||||
- ? **RAM/CPU Minimi** - Click senza rendering browser
|
||||
- ? **Fallback Automatico** - WebView se HTTP fallisce
|
||||
- ? **Logging Dettagliato** - Latenza e risposta per ogni click
|
||||
|
||||
### v2.9 - Persistenza & UX Improvements
|
||||
- ? **Fix validazione URL** - Supporta formato `auction.php?a=...`
|
||||
- ? **Pulsante "Aggiungi Pagina Corrente"** - Quick add asta visualizzata
|
||||
- ? **Persistenza automatica** - Salva/Carica lista aste in JSON
|
||||
- ? **Indicatore Strategia** - Colonna che mostra HTTP/WebView/Active
|
||||
- ? **Dialog UI migliorato** - Icona, dimensioni corrette, pulsanti visibili
|
||||
- ? **Background Polling** - Monitoraggio continuo anche senza automazione
|
||||
|
||||
### v2.8 - Hybrid Adaptive Polling
|
||||
- ? **Polling HTTP Headless** per aste lontane (timer > 30s) - CPU/RAM quasi zero
|
||||
- ? **Strategia adattiva** automatica basata sul timer (3 livelli)
|
||||
- ? **Gestione URL manuale** - Aggiungi/Rimuovi aste singolarmente
|
||||
- ? **Multi-source polling** - Combina HTTP e WebView per efficienza
|
||||
- ? **Nessuna pagina Preferiti richiesta** - Monitora aste da URL diretti
|
||||
|
||||
### v2.7 - Layout Verticale
|
||||
- ? **Separazione verticale** Utenti/Log con GridSplitter
|
||||
- ? **Margini ottimizzati** rispetto al bordo principale
|
||||
- ? **MinHeight** garantito (80px) per entrambe le sezioni
|
||||
|
||||
Reference in New Issue
Block a user