Compare commits

...

2 Commits

Author SHA1 Message Date
Alberto Balbo
cb30e1fb08 Ottimizzazioni e introduzione protocollo final attack
- Rimosso binding di `MaxClicks` in `MainWindow.xaml` e aggiunto evento `TextChanged`.
- Migliorata leggibilità del codice in `MainWindow.xaml.cs` con rientri e ritorni a capo.
- Evitata duplicazione dei log per aste aggiunte in `MainWindow.xaml.cs`.
- Migliorata gestione della validità del cookie con fallback su scraping HTML.
- Aggiunta proprietà `IsAttackInProgress` in `AuctionInfo.cs` per gestire lo stato di attacco finale.
- Introdotto protocollo di "final attack" in `AuctionMonitor.cs` per puntate critiche sotto 0,5s.
- Migliorata gestione dei log e comportamento delle puntate normali in `AuctionMonitor.cs`.
- Aggiunto metodo `PlaceBidFinalAsync` in `BidooApiClient.cs` per puntate ottimizzate.
- Ridotti log ridondanti e migliorata gestione degli errori.
2025-10-28 23:27:53 +01:00
Alberto Balbo
717dc44b3b Aggiornamento alla versione 3.0.0
- Rimosse funzionalità legacy legate a WebView2.
- Introdotto uno stile globale per i pulsanti.
- Semplificata l'interfaccia con gestione tramite griglia unica.
- Aggiunti comandi per avviare, mettere in pausa e fermare aste.
- Introdotta gestione manuale dei cookie tramite dialog.
- Aggiunti dialog per configurare sessione e aggiungere aste.
- Migliorata la persistenza con salvataggio sicuro (DPAPI).
- Rifattorizzate statistiche per utilizzare `BidHistory` e `BidderStats`.
- Ottimizzato il polling per ridurre il carico di sistema.
- Aggiornata esportazione CSV con dati più dettagliati.
- Introdotti nuovi modelli dati per utente e banner aste.
- Rimossi file di test manuale e codice obsoleto.
- Aggiornata documentazione per riflettere le modifiche.
- Aggiunta nuova icona dell'applicazione.
- Migliorata la sicurezza eliminando il salvataggio in chiaro dei cookie.
2025-10-28 12:45:08 +01:00
32 changed files with 1224 additions and 1506 deletions

View File

@@ -5,9 +5,25 @@
StartupUri="MainWindow.xaml"> StartupUri="MainWindow.xaml">
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <!-- Stile pulsanti globale -->
<ResourceDictionary Source="Converters/Converters.xaml" /> <Style x:Key="SmallButtonStyle" TargetType="Button">
</ResourceDictionary.MergedDictionaries> <Setter Property="Foreground" Value="White" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="FontSize" Value="12" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="12"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
</Application> </Application>

View File

@@ -1,2 +0,0 @@
[Binary ICO placeholder removed in this environment]

View File

@@ -11,13 +11,22 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3530-prerelease" /> <Compile Remove=".github\**" />
<EmbeddedResource Remove=".github\**" />
<None Remove=".github\**" />
<Page Remove=".github\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Icon\favicon.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6584" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6584" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<!-- Ensure the application icon is included as a WPF Resource so Icon="Assets/app.ico" resolves --> <Resource Include="Icon\favicon.ico" />
<Resource Include="Assets\app.ico" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +0,0 @@
<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"/>
<local:StartButtonOpacityConverter x:Key="StartButtonOpacityConverter"/>
</ResourceDictionary>

View File

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

View File

@@ -1,24 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace AutoBidder.Converters
{
public class StartButtonOpacityConverter : 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)
{
// Bright (1.0) when not active (can start) or when paused (can resume)
return (!isActive || isPaused) ? 1.0 : 0.5;
}
return 1.0;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

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

View File

@@ -0,0 +1,28 @@
<Window x:Class="AutoBidder.Dialogs.AddAuctionSimpleDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Aggiungi Asta" Height="220" Width="600"
Background="#0a0a0a" Foreground="#FFFFFF"
WindowStartupLocation="CenterOwner"
Icon="pack://application:,,,/Icon/favicon.ico"
ResizeMode="NoResize">
<Border Background="#1a1a1a" CornerRadius="8" Padding="16" Margin="8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="Inserire URL dell'asta" Foreground="#CCCCCC" FontSize="14" Margin="0,0,0,10" />
<TextBox x:Name="AuctionUrlBox" Grid.Row="1" MinWidth="320" Margin="0,0,0,8"
Background="#181818" Foreground="#00CCFF" BorderBrush="#444" BorderThickness="1"
Padding="8" FontSize="13" ToolTip="Inserisci l'URL completo dell'asta" />
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,8,0,0">
<Button x:Name="OkButton" Content="OK" Width="110" Margin="6" Padding="10,8"
Style="{StaticResource SmallButtonStyle}" Background="#00CC66" Foreground="White" Click="OkButton_Click" />
<Button x:Name="CancelButton" Content="Annulla" Width="110" Margin="6" Padding="10,8"
Style="{StaticResource SmallButtonStyle}" Background="#666" Foreground="White" Click="CancelButton_Click" />
</StackPanel>
</Grid>
</Border>
</Window>

View File

@@ -0,0 +1,33 @@
using System.Windows;
namespace AutoBidder.Dialogs
{
public partial class AddAuctionSimpleDialog : Window
{
public string AuctionId { get; private set; } = string.Empty;
public AddAuctionSimpleDialog()
{
InitializeComponent();
}
private void OkButton_Click(object sender, RoutedEventArgs e)
{
var text = AuctionUrlBox.Text?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(text) || !text.StartsWith("http"))
{
MessageBox.Show("Inserisci un URL valido dell'asta.", "Errore", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
AuctionId = text;
DialogResult = true;
Close();
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}
}

View File

@@ -0,0 +1,41 @@
<Window x:Class="AutoBidder.Dialogs.SessionDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Configura Sessione" Height="440" Width="700"
Background="#0a0a0a" Foreground="#FFFFFF"
WindowStartupLocation="CenterOwner"
Icon="pack://application:,,,/Icon/favicon.ico"
ResizeMode="NoResize">
<Border Background="#1a1a1a" CornerRadius="8" Padding="16" Margin="8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="Inserisci Cookie di sessione" FontSize="16" FontWeight="SemiBold" Foreground="#00CC66" Margin="0,0,0,12" />
<TextBlock Grid.Row="1" Text="Cookie di sessione (copia dal menu delle Impostazioni/Applicazioni di Chrome)" Foreground="#CCCCCC" FontSize="13" />
<TextBlock Grid.Row="2" Foreground="#888" FontSize="12" TextWrapping="Wrap" Margin="0,4,0,10">
<Run Text="Come trovare i cookie di sessione in Chrome:" />
<LineBreak/>
<Run Text="1. Apri Chrome e premi F12 (Windows) o Cmd+Option+I (Mac) per aprire gli Strumenti per sviluppatori." />
<LineBreak/>
<Run Text="2. Vai alla scheda 'Application'." />
<LineBreak/>
<Run Text="3. Nel pannello a sinistra, espandi 'Storage' e seleziona 'Cookies'." />
<LineBreak/>
<Run Text="4. Scegli il sito desiderato e copia il valore del cookie di sessione." />
</TextBlock>
<TextBox x:Name="CookieBox" Grid.Row="3" MinWidth="320" MinHeight="120" MaxHeight="220" VerticalScrollBarVisibility="Auto"
Background="#181818" Foreground="#00CCFF" BorderBrush="#444" BorderThickness="1" Padding="8" FontSize="14" AcceptsReturn="True" TextWrapping="Wrap" />
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
<Button x:Name="OkButton" Content="OK" Width="110" Margin="6" Padding="10,8"
Style="{StaticResource SmallButtonStyle}" Background="#00CC66" Foreground="White" Click="OkButton_Click" />
<Button x:Name="CancelButton" Content="Annulla" Width="110" Margin="6" Padding="10,8"
Style="{StaticResource SmallButtonStyle}" Background="#666" Foreground="White" Click="CancelButton_Click" />
</StackPanel>
</Grid>
</Border>
</Window>

View File

@@ -0,0 +1,33 @@
using System.Windows;
namespace AutoBidder.Dialogs
{
public partial class SessionDialog : Window
{
public string AuthToken { get; private set; } = string.Empty;
public SessionDialog(string existingToken = "", string existingUsername = "")
{
InitializeComponent();
CookieBox.Text = existingToken ?? string.Empty;
}
private void OkButton_Click(object sender, RoutedEventArgs e)
{
AuthToken = CookieBox.Text?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(AuthToken))
{
MessageBox.Show("Cookie obbligatorio", "Errore", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
DialogResult = true;
Close();
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}
}

View File

@@ -1,247 +0,0 @@
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();
}
}
}

BIN
Mimante/Icon/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,11 +1,10 @@
<Window x:Class="AutoBidder.MainWindow" <Window x:Class="AutoBidder.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:AutoBidder.Converters"
Title="AutoBidder v3.0" Height="800" Width="1400" Title="AutoBidder v3.0" Height="800" Width="1400"
Background="#0a0a0a" Foreground="#FFFFFF" Background="#0a0a0a" Foreground="#FFFFFF"
WindowStartupLocation="CenterScreen"> WindowStartupLocation="CenterScreen"
Icon="pack://application:,,,/Icon/favicon.ico">
<Window.Resources> <Window.Resources>
<!-- Stile pulsanti stile vecchia versione --> <!-- Stile pulsanti stile vecchia versione -->
<Style x:Key="MainButtonStyle" TargetType="Button"> <Style x:Key="MainButtonStyle" TargetType="Button">
@@ -27,24 +26,127 @@
</Setter> </Setter>
</Style> </Style>
<Style x:Key="SmallButtonStyle" TargetType="Button"> <!-- Grid action button styles (enable/opacity based on auction state) -->
<Setter Property="Foreground" Value="White" /> <Style x:Key="GridStartButtonStyle" TargetType="Button" BasedOn="{StaticResource SmallButtonStyle}">
<Setter Property="FontWeight" Value="SemiBold" /> <Setter Property="IsEnabled" Value="True" />
<Setter Property="FontSize" Value="12" /> <Setter Property="Opacity" Value="1" />
<Setter Property="BorderThickness" Value="0" /> <Style.Triggers>
<Setter Property="Cursor" Value="Hand" /> <MultiDataTrigger>
<Setter Property="Template"> <MultiDataTrigger.Conditions>
<Setter.Value> <Condition Binding="{Binding IsActive}" Value="True" />
<ControlTemplate TargetType="Button"> <Condition Binding="{Binding IsPaused}" Value="False" />
<Border Background="{TemplateBinding Background}" </MultiDataTrigger.Conditions>
CornerRadius="6" <Setter Property="IsEnabled" Value="False" />
Padding="{TemplateBinding Padding}"> <Setter Property="Opacity" Value="0.5" />
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" /> </MultiDataTrigger>
</Border> <Trigger Property="IsEnabled" Value="False">
</ControlTemplate> <Trigger.EnterActions>
</Setter.Value> <BeginStoryboard>
</Setter> <Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="0.5" Duration="0:0:0.15" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.15" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style> </Style>
<Style x:Key="GridPauseButtonStyle" TargetType="Button" BasedOn="{StaticResource SmallButtonStyle}">
<Setter Property="IsEnabled" Value="False" />
<Setter Property="Opacity" Value="0.5" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsActive}" Value="True" />
<Condition Binding="{Binding IsPaused}" Value="False" />
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="True" />
<Setter Property="Opacity" Value="1" />
</MultiDataTrigger>
<Trigger Property="IsEnabled" Value="False">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="0.5" Duration="0:0:0.15" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.15" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="GridStopButtonStyle" TargetType="Button" BasedOn="{StaticResource SmallButtonStyle}">
<Setter Property="IsEnabled" Value="False" />
<Setter Property="Opacity" Value="0.5" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsActive}" Value="True">
<Setter Property="IsEnabled" Value="True" />
<Setter Property="Opacity" Value="1" />
</DataTrigger>
<Trigger Property="IsEnabled" Value="False">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="0.5" Duration="0:0:0.15" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.15" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="GridManualBidButtonStyle" TargetType="Button" BasedOn="{StaticResource SmallButtonStyle}">
<Setter Property="IsEnabled" Value="False" />
<Setter Property="Opacity" Value="0.5" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsActive}" Value="True" />
<Condition Binding="{Binding IsPaused}" Value="False" />
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="True" />
<Setter Property="Opacity" Value="1" />
</MultiDataTrigger>
<Trigger Property="IsEnabled" Value="False">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="0.5" Duration="0:0:0.15" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.15" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources> </Window.Resources>
<Grid Margin="12" Background="#0a0a0a"> <Grid Margin="12" Background="#0a0a0a">
@@ -60,42 +162,39 @@
<Border Grid.Row="0" Background="#1a1a1a" Padding="12" CornerRadius="8" Margin="0,0,0,12"> <Border Grid.Row="0" Background="#1a1a1a" Padding="12" CornerRadius="8" Margin="0,0,0,12">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- Sinistra: Utente, Puntate, Aste da confermare -->
<!-- Info Utente --> <StackPanel Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Left">
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,20,0"> <StackPanel Orientation="Horizontal" Margin="0,0,0,2">
<TextBlock Text="User" FontSize="16" Margin="0,0,8,0" Foreground="#00CC66" FontWeight="Bold" /> <TextBlock Text="Utente: " FontSize="14" Foreground="#00CC66" FontWeight="Bold" />
<TextBlock x:Name="UsernameText" Text="Non configurato" FontSize="13" FontWeight="Bold" Foreground="#00CC66" VerticalAlignment="Center" /> <TextBlock x:Name="UsernameText" Text="Non configurato" FontSize="14" FontWeight="Bold" Foreground="#00CC66" Width="160" TextTrimming="CharacterEllipsis" />
<TextBlock Text=" | Bids " FontSize="13" Foreground="#666" Margin="12,0,8,0" />
<TextBlock x:Name="RemainingBidsText" Text="--" FontSize="13" FontWeight="Bold" Foreground="#0099FF" VerticalAlignment="Center" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,2">
<!-- Pulsanti Centrali --> <TextBlock Text="Puntate: " FontSize="14" Foreground="#666" FontWeight="Bold" />
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock x:Name="RemainingBidsText" Text="--" FontSize="14" FontWeight="Bold" Foreground="#0099FF" Width="80" TextTrimming="CharacterEllipsis" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Aste vinte da confermare: " FontSize="14" Foreground="#FF9933" FontWeight="Bold" />
<TextBlock x:Name="BannerAsteDaRiscattare" Text="--" FontSize="14" Foreground="#FF9933" FontWeight="Bold" Width="40" TextTrimming="CharacterEllipsis" />
</StackPanel>
</StackPanel>
<!-- Destra: Pulsanti globali -->
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button x:Name="ConfigSessionButton" Content="Configura" Click="ConfigSessionButton_Click" <Button x:Name="ConfigSessionButton" Content="Configura" Click="ConfigSessionButton_Click"
Style="{StaticResource SmallButtonStyle}" Style="{StaticResource SmallButtonStyle}"
Background="#8B5CF6" Padding="20,10" Margin="0,0,6,0" Height="40" MinWidth="110" /> Background="#8B5CF6" Padding="20,10" Margin="0,0,6,0" Height="40" MinWidth="110" />
<Button x:Name="StartButton" Content="Avvia Tutti" Click="StartButton_Click" IsEnabled="False" <Button x:Name="StartButton" Content="Avvia Tutti" Command="{Binding StartAllCommand}" IsEnabled="False"
Style="{StaticResource SmallButtonStyle}" Style="{StaticResource SmallButtonStyle}"
Background="#00CC66" Padding="20,10" Margin="0,0,6,0" Height="40" MinWidth="110" /> Background="#00CC66" Padding="20,10" Margin="0,0,6,0" Height="40" MinWidth="110" />
<Button x:Name="PauseAllButton" Content="Pausa Tutti" Click="PauseAllButton_Click" IsEnabled="True" <Button x:Name="PauseAllButton" Content="Pausa Tutti" Command="{Binding PauseAllCommand}" IsEnabled="True"
Style="{StaticResource SmallButtonStyle}" Style="{StaticResource SmallButtonStyle}"
Background="#FF9933" Padding="20,10" Margin="0,0,6,0" Height="40" MinWidth="110" /> Background="#FF9933" Padding="20,10" Margin="0,0,6,0" Height="40" MinWidth="110" />
<Button x:Name="StopButton" Content="Ferma Tutti" Click="StopButton_Click" IsEnabled="False" <Button x:Name="StopButton" Content="Ferma Tutti" Command="{Binding StopAllCommand}" IsEnabled="False"
Style="{StaticResource SmallButtonStyle}" Style="{StaticResource SmallButtonStyle}"
Background="#CC0000" Padding="20,10" Opacity="0.5" Height="40" MinWidth="110" /> Background="#CC0000" Padding="20,10" Opacity="0.5" Height="40" MinWidth="110" />
</StackPanel> </StackPanel>
<!-- Contatore Aste -->
<Border Grid.Column="2" Background="#0f0f0f" CornerRadius="6" Padding="16,8" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Aste:" FontSize="12" Foreground="#999" Margin="0,0,8,0" VerticalAlignment="Center" />
<TextBlock x:Name="TotalAuctionsText" Text="0" FontWeight="Bold" FontSize="18" Foreground="#00CC66" VerticalAlignment="Center" />
</StackPanel>
</Border>
</Grid> </Grid>
</Border> </Border>
@@ -120,7 +219,7 @@
<!-- Header con titolo e pulsanti --> <!-- Header con titolo e pulsanti -->
<Grid Grid.Row="0" Margin="0,0,0,12"> <Grid Grid.Row="0" Margin="0,0,0,12">
<TextBlock Text="Aste Monitorate" FontSize="14" FontWeight="Bold" Foreground="#00CC66" VerticalAlignment="Center" /> <TextBlock x:Name="MonitorateTitle" Text="Aste monitorate: 0" FontSize="14" FontWeight="Bold" Foreground="#00CC66" VerticalAlignment="Center" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="AddUrlButton" Content="+ Aggiungi" Click="AddUrlButton_Click" <Button x:Name="AddUrlButton" Content="+ Aggiungi" Click="AddUrlButton_Click"
Style="{StaticResource SmallButtonStyle}" Style="{StaticResource SmallButtonStyle}"
@@ -182,6 +281,7 @@
</DataGrid.Resources> </DataGrid.Resources>
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="Asta" Binding="{Binding Name, Mode=OneWay}" Width="2*" IsReadOnly="True" /> <DataGridTextColumn Header="Asta" Binding="{Binding Name, Mode=OneWay}" Width="2*" IsReadOnly="True" />
<DataGridTextColumn Header="Latenza (ms)" Binding="{Binding AuctionInfo.PollingLatencyMs, Mode=OneWay}" Width="80" IsReadOnly="True" />
<DataGridTextColumn Header="Stato" Binding="{Binding StatusDisplay, Mode=OneWay}" Width="90" IsReadOnly="True" /> <DataGridTextColumn Header="Stato" Binding="{Binding StatusDisplay, Mode=OneWay}" Width="90" IsReadOnly="True" />
<DataGridTextColumn Header="Timer" Binding="{Binding TimerDisplay, Mode=OneWay}" Width="70" IsReadOnly="True" /> <DataGridTextColumn Header="Timer" Binding="{Binding TimerDisplay, Mode=OneWay}" Width="70" IsReadOnly="True" />
<DataGridTextColumn Header="Prezzo" Binding="{Binding PriceDisplay, Mode=OneWay}" Width="85" IsReadOnly="True" /> <DataGridTextColumn Header="Prezzo" Binding="{Binding PriceDisplay, Mode=OneWay}" Width="85" IsReadOnly="True" />
@@ -192,38 +292,10 @@
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="Avvia" CommandParameter="{Binding}" Click="GridStartAuction_Click" Style="{StaticResource SmallButtonStyle}" Background="#00CC66" Padding="6,2" MinWidth="40" Margin="0,0,4,0"> <Button Content="Avvia" Command="{Binding DataContext.GridStartCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Style="{StaticResource GridStartButtonStyle}" Background="#00CC66" Padding="6,2" MinWidth="40" Margin="0,0,4,0" />
<Button.Opacity> <Button Content="Pausa" Command="{Binding DataContext.GridPauseCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Style="{StaticResource GridPauseButtonStyle}" Background="#FF9933" Padding="6,2" MinWidth="40" Margin="0,0,4,0" />
<MultiBinding Converter="{StaticResource StartButtonOpacityConverter}"> <Button Content="Ferma" Command="{Binding DataContext.GridStopCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Style="{StaticResource GridStopButtonStyle}" Background="#CC0000" Padding="6,2" MinWidth="40" Margin="0,0,4,0" />
<Binding Path="IsActive" /> <Button Content="Punta" Command="{Binding DataContext.GridBidCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Style="{StaticResource GridManualBidButtonStyle}" Background="#8B5CF6" Padding="6,2" MinWidth="40" />
<Binding Path="IsPaused" />
</MultiBinding>
</Button.Opacity>
<Button.IsEnabled>
<MultiBinding Converter="{StaticResource StartResumeConverter}">
<Binding Path="IsActive" />
<Binding Path="IsPaused" />
</MultiBinding>
</Button.IsEnabled>
</Button>
<Button Content="Pausa" CommandParameter="{Binding}" Click="GridPauseAuction_Click" Style="{StaticResource SmallButtonStyle}" Background="#FF9933" Padding="6,2" MinWidth="40" Margin="0,0,4,0">
<Button.IsEnabled>
<MultiBinding Converter="{StaticResource AndNotPausedConverter}">
<Binding Path="IsActive" />
<Binding Path="IsPaused" />
</MultiBinding>
</Button.IsEnabled>
<Button.Opacity>
<MultiBinding Converter="{StaticResource PauseButtonOpacityConverter}">
<Binding Path="IsActive" />
<Binding Path="IsPaused" />
</MultiBinding>
</Button.Opacity>
</Button>
<Button Content="Ferma" CommandParameter="{Binding}" Click="GridStopAuction_Click" Style="{StaticResource SmallButtonStyle}" Background="#CC0000" Padding="6,2" MinWidth="40" Margin="0,0,4,0"
IsEnabled="{Binding IsActive}"
Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacityConverter}}" />
<Button Content="Punta" CommandParameter="{Binding}" Click="GridManualBid_Click" Style="{StaticResource SmallButtonStyle}" Background="#8B5CF6" Padding="6,2" MinWidth="40" />
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
@@ -295,7 +367,7 @@
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBox x:Name="SelectedAuctionUrl" Text="" IsReadOnly="True" MinWidth="220" Margin="0,0,8,0" VerticalAlignment="Center" Background="#181818" Foreground="#00CCFF" BorderBrush="#333" BorderThickness="1" FontSize="11" Grid.Column="0" HorizontalAlignment="Stretch" /> <TextBox x:Name="SelectedAuctionUrl" Text="" IsReadOnly="True" MinWidth="220" Margin="0,0,8,0" VerticalAlignment="Center" Background="#181818" Foreground="#00CCFF" BorderBrush="#333" BorderThickness="1" FontSize="11" Grid.Column="0" HorizontalAlignment="Stretch" />
<Button Content="Apri" x:Name="OpenAuctionButton" Click="OpenAuctionButton_Click" Style="{StaticResource SmallButtonStyle}" Background="#0099FF" Padding="10,6" Margin="0,0,4,0" Height="28" MinWidth="60" FontSize="11" Grid.Column="1" /> <Button Content="Apri" x:Name="OpenAuctionButton" Click="GridOpenAuction_Click" Style="{StaticResource SmallButtonStyle}" Background="#0099FF" Padding="10,6" Margin="0,0,4,0" Height="28" MinWidth="60" FontSize="11" Grid.Column="1" />
<Button Content="Copia" x:Name="CopyAuctionUrlButton" Click="CopyAuctionUrlButton_Click" Style="{StaticResource SmallButtonStyle}" Background="#666" Padding="10,6" Height="28" MinWidth="60" FontSize="11" Grid.Column="2" /> <Button Content="Copia" x:Name="CopyAuctionUrlButton" Click="CopyAuctionUrlButton_Click" Style="{StaticResource SmallButtonStyle}" Background="#666" Padding="10,6" Height="28" MinWidth="60" FontSize="11" Grid.Column="2" />
</Grid> </Grid>
@@ -341,13 +413,9 @@
<UniformGrid Columns="2" Margin="0,0,0,12"> <UniformGrid Columns="2" Margin="0,0,0,12">
<StackPanel Margin="0,0,4,0"> <StackPanel Margin="0,0,4,0">
<TextBlock Text="Max Clicks (0=inf)" FontSize="10" Margin="0,0,0,4" Foreground="#999" /> <TextBlock Text="Max Clicks (0=inf)" FontSize="10" Margin="0,0,0,4" Foreground="#999" />
<TextBox x:Name="SelectedMaxClicks" Text="0" TextChanged="SelectedMaxClicks_TextChanged" <TextBox x:Name="SelectedMaxClicks" Text="0" TextChanged="SelectedMaxClicks_TextChanged" Background="#1a1a1a" Foreground="#FFF" Padding="8" FontSize="12" BorderBrush="#444" BorderThickness="1" />
Background="#1a1a1a" Foreground="#FFF" Padding="8" FontSize="12" BorderBrush="#444" BorderThickness="1" />
</StackPanel>
<StackPanel Margin="4,0,0,0">
<TextBlock Text="(Opzionale)" FontSize="10" Margin="0,0,0,4" Foreground="#999" />
<TextBlock Text="Limita il numero di puntate del bot per questa asta" FontSize="10" Foreground="#AAA" TextWrapping="Wrap" />
</StackPanel> </StackPanel>
<StackPanel />
</UniformGrid> </UniformGrid>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>

View File

@@ -1,17 +1,13 @@
using System; using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net; using System.Net;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Threading.Tasks;
using Microsoft.Web.WebView2.Core;
using AutoBidder.Models; using AutoBidder.Models;
using AutoBidder.Services; using AutoBidder.Services;
using AutoBidder.ViewModels; using AutoBidder.ViewModels;
using AutoBidder.Utilities; using AutoBidder.Utilities;
using Microsoft.Win32; using AutoBidder.Dialogs;
namespace AutoBidder namespace AutoBidder
{ {
@@ -24,11 +20,23 @@ namespace AutoBidder
// SERVIZI CORE // SERVIZI CORE
private readonly AuctionMonitor _auctionMonitor; private readonly AuctionMonitor _auctionMonitor;
private readonly ObservableCollection<AuctionViewModel> _auctionViewModels = new(); private readonly ObservableCollection<AuctionViewModel> _auctionViewModels = new();
// UI State // UI State
private AuctionViewModel? _selectedAuction; private AuctionViewModel? _selectedAuction;
private bool _isAutomationActive = false; private bool _isAutomationActive = false;
private BrowserWindow? _browserWindow;
// Commands
public RelayCommand? StartAllCommand { get; private set; }
public RelayCommand? StopAllCommand { get; private set; }
public RelayCommand? PauseAllCommand { get; private set; }
// Grid item commands (parameter = AuctionViewModel)
public RelayCommand? GridStartCommand { get; private set; }
public RelayCommand? GridPauseCommand { get; private set; }
public RelayCommand? GridStopCommand { get; private set; }
public RelayCommand? GridBidCommand { get; private set; }
private System.Windows.Threading.DispatcherTimer _userBannerTimer;
private System.Windows.Threading.DispatcherTimer _userHtmlTimer;
public MainWindow() public MainWindow()
{ {
@@ -37,6 +45,18 @@ namespace AutoBidder
// Inizializza servizi // Inizializza servizi
_auctionMonitor = new AuctionMonitor(); _auctionMonitor = new AuctionMonitor();
// Commands
StartAllCommand = new RelayCommand(_ => ExecuteStartAll());
StopAllCommand = new RelayCommand(_ => ExecuteStopAll());
PauseAllCommand = new RelayCommand(_ => ExecutePauseAll());
GridStartCommand = new RelayCommand(param => ExecuteGridStart(param as ViewModels.AuctionViewModel));
GridPauseCommand = new RelayCommand(param => ExecuteGridPause(param as ViewModels.AuctionViewModel));
GridStopCommand = new RelayCommand(param => ExecuteGridStop(param as ViewModels.AuctionViewModel));
GridBidCommand = new RelayCommand(async param => await ExecuteGridBidAsync(param as ViewModels.AuctionViewModel));
this.DataContext = this;
// Event handlers // Event handlers
_auctionMonitor.OnAuctionUpdated += AuctionMonitor_OnAuctionUpdated; _auctionMonitor.OnAuctionUpdated += AuctionMonitor_OnAuctionUpdated;
_auctionMonitor.OnBidExecuted += AuctionMonitor_OnBidExecuted; _auctionMonitor.OnBidExecuted += AuctionMonitor_OnBidExecuted;
@@ -53,7 +73,75 @@ namespace AutoBidder
UpdateGlobalControlButtons(); UpdateGlobalControlButtons();
Log("[OK] AutoBidder v4.0 avviato (API-based)"); Log("[OK] AutoBidder v4.0 avviato (API-based)");
Log("[INFO] Usa 'Configura Sessione' per inserire PHPSESSID"); Log("[INFO] Usa 'Configura Sessione' per inserire cookie dal menu Applicazioni di Chrome");
// Timer per aggiornamento banner utente ogni minuto
_userBannerTimer = new System.Windows.Threading.DispatcherTimer();
_userBannerTimer.Interval = TimeSpan.FromMinutes(1);
_userBannerTimer.Tick += UserBannerTimer_Tick;
_userBannerTimer.Start();
_ = UpdateUserBannerInfoAsync();
// Timer per aggiornamento dati utente da HTML ogni 3 minuti
_userHtmlTimer = new System.Windows.Threading.DispatcherTimer();
_userHtmlTimer.Interval = TimeSpan.FromMinutes(3);
_userHtmlTimer.Tick += UserHtmlTimer_Tick;
_userHtmlTimer.Start();
_ = UpdateUserHtmlInfoAsync();
}
// Command implementations
private void ExecuteStartAll()
{
StartButton_Click(null, null);
}
private void ExecuteStopAll()
{
StopButton_Click(null, null);
}
private void ExecutePauseAll()
{
PauseAllButton_Click(null, null);
}
private void ExecuteGridStart(ViewModels.AuctionViewModel? vm)
{
if (vm == null) return;
vm.IsActive = true;
vm.IsPaused = false;
Log($"[START] Asta avviata: {vm.Name}");
UpdateGlobalControlButtons();
}
private void ExecuteGridPause(ViewModels.AuctionViewModel? vm)
{
if (vm == null) return;
vm.IsPaused = true;
Log($"[PAUSA] Asta in pausa: {vm.Name}");
UpdateGlobalControlButtons();
}
private void ExecuteGridStop(ViewModels.AuctionViewModel? vm)
{
if (vm == null) return;
vm.IsActive = false;
Log($"[STOP] Asta fermata: {vm.Name}");
UpdateGlobalControlButtons();
}
private async Task ExecuteGridBidAsync(ViewModels.AuctionViewModel? vm)
{
if (vm == null) return;
try
{
Log($"[BID] Puntata manuale richiesta su: {vm.Name}");
var result = await _auctionMonitor.PlaceManualBidAsync(vm.AuctionInfo);
if (result.Success)
Log($"[OK] Puntata manuale su {vm.Name}: {result.LatencyMs}ms");
else
Log($"[FAIL] Puntata manuale su {vm.Name}: {result.Error}");
}
catch (Exception ex)
{
Log($"[ERRORE] Puntata manuale: {ex.Message}");
}
} }
private void AuctionMonitor_OnAuctionUpdated(AuctionState state) private void AuctionMonitor_OnAuctionUpdated(AuctionState state)
@@ -116,8 +204,7 @@ namespace AutoBidder
{ {
// Carica sessione esistente (se presente) // Carica sessione esistente (se presente)
var currentSession = _auctionMonitor.GetSession(); var currentSession = _auctionMonitor.GetSession();
string existingToken = ""; string existingToken = string.Empty;
string existingUsername = "";
if (currentSession != null) if (currentSession != null)
{ {
@@ -130,11 +217,9 @@ namespace AutoBidder
{ {
existingToken = currentSession.AuthToken; existingToken = currentSession.AuthToken;
} }
existingUsername = currentSession.Username ?? "";
} }
var dialog = new SessionDialog(existingToken, existingUsername); var dialog = new SessionDialog(existingToken);
if (dialog.ShowDialog() == true) if (dialog.ShowDialog() == true)
{ {
try try
@@ -142,32 +227,43 @@ namespace AutoBidder
// Usa InitializeSessionWithCookie invece di InitializeSession // Usa InitializeSessionWithCookie invece di InitializeSession
// perché il token è __stattrb (non __stattr) // perché il token è __stattrb (non __stattr)
var cookieString = $"__stattrb={dialog.AuthToken}"; var cookieString = $"__stattrb={dialog.AuthToken}";
_auctionMonitor.InitializeSessionWithCookie(cookieString, dialog.Username); _auctionMonitor.InitializeSessionWithCookie(cookieString, ""); // Username non più passato
UsernameText.Text = dialog.Username ?? string.Empty;
StartButton.IsEnabled = true; StartButton.IsEnabled = true;
// Salva sessione in modo sicuro (crittografata) // Salva sessione in modo sicuro (crittografata)
var session = _auctionMonitor.GetSession(); var session = _auctionMonitor.GetSession();
SessionManager.SaveSession(session); SessionManager.SaveSession(session);
Log($"Sessione configurata per: {dialog.Username}"); Log($"Sessione configurata");
Log($"Cookie salvato in modo sicuro"); Log($"Cookie salvato in modo sicuro");
// Aggiorna info utente // Aggiorna info utente (nome e puntate) tramite chiamata automatica
Task.Run(() => Task.Run(async () =>
{
var userData = await _auctionMonitor.GetUserDataAsync();
if (userData != null)
{ {
_auctionMonitor.UpdateUserInfoAsync().GetAwaiter().GetResult();
var updatedSession = _auctionMonitor.GetSession();
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
{ {
RemainingBidsText.Text = updatedSession.RemainingBids.ToString(); SetUserBanner(userData.Username, userData.RemainingBids);
Log($"[OK] Utente: {userData.Username}, Puntate residue: {userData.RemainingBids}");
}); });
}
else
{
Dispatcher.Invoke(() =>
{
UsernameText.Text = "";
RemainingBidsText.Text = "0";
Log($"[WARN] Impossibile ricavare dati utente");
});
}
}); });
} }
catch (Exception ex) catch (Exception ex)
{ {
Log($"Errore configurazione sessione: {ex.Message}"); Log($"[ERRORE] Configurazione sessione: {ex.Message}");
MessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error); MessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
} }
} }
@@ -248,24 +344,6 @@ namespace AutoBidder
MessageBox.Show("Usa i pulsanti Pausa/Riprendi per asta", "Info", MessageBoxButton.OK, MessageBoxImage.Information); MessageBox.Show("Usa i pulsanti Pausa/Riprendi per asta", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
} }
private void OpenBrowserButton_Click(object sender, RoutedEventArgs e)
{
if (_browserWindow == null || !_browserWindow.IsVisible)
{
_browserWindow = new BrowserWindow();
_browserWindow.OnAddAuction += async (url) =>
{
await AddAuctionFromUrl(url);
};
_browserWindow.Show();
Log("[BROWSER] Browser aperto");
}
else
{
_browserWindow.Activate();
}
}
private async void AddUrlButton_Click(object sender, RoutedEventArgs e) private async void AddUrlButton_Click(object sender, RoutedEventArgs e)
{ {
var dialog = new AddAuctionSimpleDialog(); var dialog = new AddAuctionSimpleDialog();
@@ -402,7 +480,8 @@ namespace AutoBidder
{ {
if (int.TryParse(tb.Text, out var value) && value >= 0) if (int.TryParse(tb.Text, out var value) && value >= 0)
{ {
_selectedAuction.AuctionInfo.MaxClicks = value; _selectedAuction.MaxClicks = value;
SaveAuctions(); // Persist change immediately
} }
} }
} }
@@ -511,13 +590,13 @@ namespace AutoBidder
return Task.CompletedTask; return Task.CompletedTask;
} }
productName = ExtractProductName(originalUrl); productName = ExtractProductName(originalUrl) ?? string.Empty;
} }
else else
{ {
// È solo un ID numerico - costruisci URL generico // È solo un ID numerico - costruisci URL generico
auctionId = input.Trim(); auctionId = input.Trim();
productName = null; productName = string.Empty;
originalUrl = $"https://it.bidoo.com/auction.php?a=asta_{auctionId}"; originalUrl = $"https://it.bidoo.com/auction.php?a=asta_{auctionId}";
} }
@@ -554,7 +633,7 @@ namespace AutoBidder
SaveAuctions(); SaveAuctions();
UpdateTotalCount(); UpdateTotalCount();
Log($"[+] Asta aggiunta: {displayName}"); // AuctionMonitor already emits a global log entry for added auctions
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -593,7 +672,8 @@ namespace AutoBidder
var name = $"Asta {auctionId}"; var name = $"Asta {auctionId}";
try try
{ {
var html = await HttpClientProvider.GetStringAsync(url); using var httpClient = new System.Net.Http.HttpClient();
var html = await httpClient.GetStringAsync(url);
var match = System.Text.RegularExpressions.Regex.Match(html, @"<title>([^<]+)</title>"); var match = System.Text.RegularExpressions.Regex.Match(html, @"<title>([^<]+)</title>");
if (match.Success) if (match.Success)
{ {
@@ -623,7 +703,7 @@ namespace AutoBidder
SaveAuctions(); SaveAuctions();
UpdateTotalCount(); UpdateTotalCount();
Log($"[+] Asta aggiunta: {name}"); // AuctionMonitor already emits a global log entry for added auctions
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -753,18 +833,16 @@ namespace AutoBidder
SelectedMaxPrice.Text = auction.MaxPrice.ToString(); SelectedMaxPrice.Text = auction.MaxPrice.ToString();
SelectedMinResets.Text = auction.AuctionInfo.MinResets.ToString(); SelectedMinResets.Text = auction.AuctionInfo.MinResets.ToString();
SelectedMaxResets.Text = auction.AuctionInfo.MaxResets.ToString(); SelectedMaxResets.Text = auction.AuctionInfo.MaxResets.ToString();
SelectedMaxClicks.Text = auction.MaxClicks.ToString();
// Mostra il link dell'asta selezionata // Mostra il link dell'asta selezionata
var url = auction.AuctionInfo.OriginalUrl; var url = auction.AuctionInfo.OriginalUrl;
if (string.IsNullOrEmpty(url)) if (string.IsNullOrEmpty(url))
url = $"https://it.bidoo.com/auction.php?a=asta_{auction.AuctionId}"; url = $"https://it.bidoo.com/auction.php?a=asta_{auction.AuctionId}";
SelectedAuctionUrl.Text = url; SelectedAuctionUrl.Text = url;
// Abilita solo i pulsanti rimasti // Abilita solo i pulsanti rimasti
ResetSettingsButton.IsEnabled = true; ResetSettingsButton.IsEnabled = true;
ClearBiddersButton.IsEnabled = true; ClearBiddersButton.IsEnabled = true;
ClearLogButton.IsEnabled = true; ClearLogButton.IsEnabled = true;
// Aggiorna log asta // Aggiorna log asta
UpdateAuctionLog(auction); UpdateAuctionLog(auction);
// Aggiorna bidders con metodo dedicato // Aggiorna bidders con metodo dedicato
@@ -773,43 +851,14 @@ namespace AutoBidder
catch { } catch { }
} }
private void OpenAuctionButton_Click(object sender, RoutedEventArgs e)
{
if (_selectedAuction == null) return;
try
{
// Usa l'URL originale salvato nell'asta
var url = _selectedAuction.AuctionInfo.OriginalUrl;
if (string.IsNullOrEmpty(url))
{
// Fallback: costruisci URL generico
url = $"https://it.bidoo.com/auction.php?a=asta_{_selectedAuction.AuctionId}";
}
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = url,
UseShellExecute = true
});
Log($"[LINK] Apertura asta: {_selectedAuction.Name}");
}
catch (Exception ex)
{
Log($"[ERRORE] Errore apertura asta: {ex.Message}");
MessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void UpdateAuctionLog(AuctionViewModel auction) private void UpdateAuctionLog(AuctionViewModel auction)
{ {
try try
{ {
var auctionInfo = auction.AuctionInfo; var auctionInfo = auction.AuctionInfo;
var logBox = SelectedAuctionLog;
SelectedAuctionLog.Document.Blocks.Clear(); var doc = logBox.Document;
doc.Blocks.Clear();
foreach (var entry in auctionInfo.AuctionLog) foreach (var entry in auctionInfo.AuctionLog)
{ {
var upper = entry.ToUpperInvariant(); var upper = entry.ToUpperInvariant();
@@ -818,14 +867,21 @@ namespace AutoBidder
color = System.Windows.Media.Brushes.IndianRed; color = System.Windows.Media.Brushes.IndianRed;
else if (upper.Contains("[WARN]") || upper.Contains("WARN")) else if (upper.Contains("[WARN]") || upper.Contains("WARN"))
color = System.Windows.Media.Brushes.Orange; color = System.Windows.Media.Brushes.Orange;
var p = new System.Windows.Documents.Paragraph { Margin = new Thickness(0) }; var p = new System.Windows.Documents.Paragraph { Margin = new Thickness(0) };
var r = new System.Windows.Documents.Run(entry) { Foreground = color }; var r = new System.Windows.Documents.Run(entry) { Foreground = color };
p.Inlines.Add(r); p.Inlines.Add(r);
SelectedAuctionLog.Document.Blocks.Add(p); doc.Blocks.Add(p);
} }
// Scroll only if user is already near the bottom
SelectedAuctionLog.ScrollToEnd(); var viewer = logBox;
var scroll = viewer.VerticalScrollBarVisibility;
var vpos = viewer.VerticalOffset;
var vmax = viewer.ExtentHeight - viewer.ViewportHeight;
if (vmax - vpos < 40) // threshold: 40px from bottom
{
viewer.ScrollToEnd();
}
// else: preserve scroll position
} }
catch { } catch { }
} }
@@ -840,7 +896,7 @@ namespace AutoBidder
SelectedAuctionBiddersGrid.ItemsSource = null; // Force refresh SelectedAuctionBiddersGrid.ItemsSource = null; // Force refresh
SelectedAuctionBiddersGrid.ItemsSource = bidders; SelectedAuctionBiddersGrid.ItemsSource = bidders;
SelectedAuctionBiddersCount.Text = bidders.Count.ToString(); SelectedAuctionBiddersCount.Text = bidders?.Count.ToString() ?? "0";
} }
catch { } catch { }
} }
@@ -853,7 +909,7 @@ namespace AutoBidder
private void UpdateTotalCount() private void UpdateTotalCount()
{ {
TotalAuctionsText.Text = _auctionViewModels.Count.ToString(); MonitorateTitle.Text = $"Aste monitorate: {_auctionViewModels.Count}";
} }
private void LoadSavedAuctions() private void LoadSavedAuctions()
@@ -861,28 +917,25 @@ namespace AutoBidder
try try
{ {
var auctions = PersistenceManager.LoadAuctions(); var auctions = PersistenceManager.LoadAuctions();
foreach (var auction in auctions) foreach (var auction in auctions)
{ {
// Protezione: rimuovi eventuali BidHistory null
auction.BidHistory = auction.BidHistory?.Where(b => b != null).ToList() ?? new List<BidHistory>();
_auctionMonitor.AddAuction(auction); _auctionMonitor.AddAuction(auction);
var vm = new AuctionViewModel(auction); var vm = new AuctionViewModel(auction);
_auctionViewModels.Add(vm); _auctionViewModels.Add(vm);
} }
// On startup treat persisted auctions as stopped (user expectation) // On startup treat persisted auctions as stopped (user expectation)
foreach (var vm in _auctionViewModels) foreach (var vm in _auctionViewModels)
{ {
vm.IsActive = false; vm.IsActive = false;
vm.IsPaused = false; vm.IsPaused = false;
} }
UpdateTotalCount(); UpdateTotalCount();
if (auctions.Count > 0) if (auctions.Count > 0)
{ {
Log($"[OK] Caricate {auctions.Count} aste salvate"); Log($"[OK] Caricate {auctions.Count} aste salvate");
} }
// Carica sessione salvata (se esiste) // Carica sessione salvata (se esiste)
LoadSavedSession(); LoadSavedSession();
} }
@@ -933,12 +986,24 @@ namespace AutoBidder
} }
else else
{ {
Log($"[WARN] Cookie potrebbe essere scaduto - Riconfigura sessione"); // Secondary fallback: try scraping user data from HTML to ensure cookie is truly invalid
MessageBox.Show( var htmlUser = _auctionMonitor.GetUserDataFromHtmlAsync().GetAwaiter().GetResult();
"Il cookie salvato potrebbe essere scaduto.\nRiconfigura la sessione con 'Configura Sessione'.", if (htmlUser != null)
"Sessione Scaduta", {
MessageBoxButton.OK, Dispatcher.Invoke(() =>
MessageBoxImage.Warning); {
SetUserBanner(htmlUser.Username, htmlUser.RemainingBids);
Log($"[OK] Dati utente rilevati via HTML - Utente: {htmlUser.Username}, Puntate residue: {htmlUser.RemainingBids}");
});
}
else
{
// Both checks failed: log a warning but do NOT show intrusive MessageBox on startup
Dispatcher.Invoke(() =>
{
Log($"[WARN] Impossibile verificare sessione: cookie non valido o rete assente. Riconfigura la sessione se persistono i problemi.");
});
}
} }
}); });
} }
@@ -994,7 +1059,6 @@ namespace AutoBidder
else else
level = LogLevel.Info; level = LogLevel.Info;
} }
Dispatcher.BeginInvoke(() => Dispatcher.BeginInvoke(() =>
{ {
var para = new System.Windows.Documents.Paragraph(); var para = new System.Windows.Documents.Paragraph();
@@ -1015,6 +1079,11 @@ namespace AutoBidder
} }
para.Inlines.Add(run); para.Inlines.Add(run);
LogBox.Document.Blocks.Add(para); LogBox.Document.Blocks.Add(para);
// Mantieni solo ultimi 500 log
while (LogBox.Document.Blocks.Count > 500)
{
LogBox.Document.Blocks.Remove(LogBox.Document.Blocks.FirstBlock);
}
LogBox.ScrollToEnd(); LogBox.ScrollToEnd();
}); });
} }
@@ -1169,67 +1238,59 @@ namespace AutoBidder
StopButton.IsEnabled = true; StopButton.Opacity = 1.0; StopButton.IsEnabled = true; StopButton.Opacity = 1.0;
} }
} }
private async void UserBannerTimer_Tick(object? sender, EventArgs e)
{
await UpdateUserBannerInfoAsync();
} }
// ===== DIALOG HELPER ===== private async Task UpdateUserBannerInfoAsync()
public class AddAuctionDialog : Window
{ {
private readonly TextBox _urlTextBox; var info = await _auctionMonitor.GetUserBannerInfoAsync();
public string AuctionUrl => _urlTextBox.Text.Trim(); if (info != null)
public AddAuctionDialog()
{ {
Title = "Aggiungi Asta"; BannerAsteDaRiscattare.Text = $"{info.nAsteVinte - info.nAsteConfermate}";
Width = 500; // BannerPuntateBonus.Text = $"Bonus: {info.nPuntateBonus}"; // RIMOSSO
Height = 150; }
WindowStartupLocation = WindowStartupLocation.CenterOwner; else
ResizeMode = ResizeMode.NoResize; {
BannerAsteDaRiscattare.Text = "--";
var grid = new Grid { Margin = new Thickness(20) }; // BannerPuntateBonus.Text = "Bonus: --"; // RIMOSSO
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
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(15) });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
var label = new TextBlock { Text = "URL Asta:", FontWeight = FontWeights.SemiBold };
Grid.SetRow(label, 0);
_urlTextBox = new TextBox { Text = "https://it.bidoo.com/asta/", Height = 30, Padding = new Thickness(5) };
Grid.SetRow(_urlTextBox, 2);
var buttonPanel = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right };
Grid.SetRow(buttonPanel, 4);
var okButton = new Button { Content = "Aggiungi", Width = 100, Height = 32, Margin = new Thickness(0, 0, 10, 0) };
var cancelButton = new Button { Content = "Annulla", Width = 100, Height = 32 };
okButton.Click += (s, e) => { DialogResult = true; Close(); };
cancelButton.Click += (s, e) => { DialogResult = false; Close(); };
_urlTextBox.KeyDown += (s, e) => { if (e.Key == Key.Enter) { DialogResult = true; Close(); } };
buttonPanel.Children.Add(okButton);
buttonPanel.Children.Add(cancelButton);
grid.Children.Add(label);
grid.Children.Add(_urlTextBox);
grid.Children.Add(buttonPanel);
Content = grid;
Loaded += (s, e) => _urlTextBox.Focus();
} }
} }
// ===== HttpClient Helper ===== private async void UserHtmlTimer_Tick(object? sender, EventArgs e)
internal static class HttpClientProvider
{ {
private static readonly System.Net.Http.HttpClient _httpClient = new(); await UpdateUserHtmlInfoAsync();
}
public static async Task<string> GetStringAsync(string url) private async Task UpdateUserHtmlInfoAsync()
{ {
return await _httpClient.GetStringAsync(url); var userData = await _auctionMonitor.GetUserDataFromHtmlAsync();
if (userData != null)
{
Dispatcher.Invoke(() =>
{
SetUserBanner(userData.Username, userData.RemainingBids);
Log($"[HTML] Utente: {userData.Username}, Puntate residue: {userData.RemainingBids}");
});
}
}
private void SetUserBanner(string username, int remainingBids)
{
// Filtra valori non validi
if (string.IsNullOrWhiteSpace(username) || username.Contains("<!DOCTYPE") || username.Contains("<html") || username.Contains("<head") || username.Contains("<title"))
{
UsernameText.Text = "Non configurato";
}
else
{
// Rimuovi newline e taglia a max 20 caratteri
var clean = username.Replace("\r", "").Replace("\n", "");
UsernameText.Text = clean.Length > 20 ? clean.Substring(0, 20) + "..." : clean;
}
RemainingBidsText.Text = remainingBids.ToString();
} }
} }
} }

View File

@@ -1,10 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace AutoBidder.Models namespace AutoBidder.Models
{ {
/// <summary> /// <summary>
/// Informazioni base di un'asta monitorata /// Informazioni base di un'asta monitorata
/// Solo HTTP, nessuna modalità, browser o multi-click
/// </summary> /// </summary>
public class AuctionInfo public class AuctionInfo
{ {
@@ -19,15 +22,14 @@ namespace AutoBidder.Models
public double MaxPrice { get; set; } = 0; public double MaxPrice { get; set; } = 0;
public int MinResets { get; set; } = 0; // Numero minimo reset prima di puntare public int MinResets { get; set; } = 0; // Numero minimo reset prima di puntare
public int MaxResets { get; set; } = 0; // Numero massimo reset (0 = illimitati) public int MaxResets { get; set; } = 0; // Numero massimo reset (0 = illimitati)
// Numero massimo di click/puntate che il bot può eseguire per questa asta (0 = illimitato) [JsonPropertyName("MaxClicks")]
public int MaxClicks { get; set; } = 0; public int MaxClicks { get; set; } = 0; // Numero massimo di puntate consentite (0 = illimitato)
// Stato asta // Stato asta
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;
public bool IsPaused { get; set; } = false; public bool IsPaused { get; set; } = false;
// Contatori // Contatori
public int MyClicks { get; set; } = 0;
public int ResetCount { get; set; } = 0; public int ResetCount { get; set; } = 0;
// Timestamp // Timestamp
@@ -35,17 +37,19 @@ namespace AutoBidder.Models
public DateTime? LastClickAt { get; set; } public DateTime? LastClickAt { get; set; }
// Storico // Storico
public List<BidHistory> BidHistory { get; set; } = new(); public List<BidHistory> BidHistory { get; set; } = new List<BidHistory>();
public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase); public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase);
// Legacy (deprecato, usa BidderStats) // Legacy (deprecato) - removed `Bidders` dictionary; use `BidderStats` instead
[System.Text.Json.Serialization.JsonIgnore]
public Dictionary<string, int> Bidders { get; set; } = new(StringComparer.OrdinalIgnoreCase);
// Log per-asta (non serializzato) // Log per-asta (non serializzato)
[System.Text.Json.Serialization.JsonIgnore] [System.Text.Json.Serialization.JsonIgnore]
public List<string> AuctionLog { get; set; } = new(); public List<string> AuctionLog { get; set; } = new();
// Flag runtime: indica che è in corso un'operazione di final attack per questa asta
[System.Text.Json.Serialization.JsonIgnore]
public bool IsAttackInProgress { get; set; } = false;
/// <summary> /// <summary>
/// Aggiunge una voce al log dell'asta /// Aggiunge una voce al log dell'asta
/// </summary> /// </summary>
@@ -54,11 +58,13 @@ namespace AutoBidder.Models
var entry = $"{DateTime.Now:HH:mm:ss} - {message}"; var entry = $"{DateTime.Now:HH:mm:ss} - {message}";
AuctionLog.Add(entry); AuctionLog.Add(entry);
// Mantieni solo ultimi 100 log // Mantieni solo ultimi 500 log
if (AuctionLog.Count > 100) if (AuctionLog.Count > 500)
{ {
AuctionLog.RemoveAt(0); AuctionLog.RemoveAt(0);
} }
} }
public int PollingLatencyMs { get; set; } = 0; // Ultima latenza polling ms
} }
} }

View File

@@ -61,10 +61,12 @@ namespace AutoBidder.Models
Name = auction.Name, Name = auction.Name,
MonitoringStarted = auction.AddedAt, MonitoringStarted = auction.AddedAt,
MonitoringDuration = DateTime.UtcNow - auction.AddedAt, MonitoringDuration = DateTime.UtcNow - auction.AddedAt,
MyBids = auction.MyClicks, // MyBids calcolato dalle entry in BidHistory
MyBids = auction.BidHistory.Count(h => h.EventType == BidEventType.MyBid),
Resets = auction.ResetCount, Resets = auction.ResetCount,
UniqueBidders = auction.Bidders.Count, UniqueBidders = auction.BidderStats?.Count ?? 0,
BidderRanking = auction.Bidders BidderRanking = (auction.BidderStats ?? new Dictionary<string, BidderInfo>())
.ToDictionary(k => k.Key, v => v.Value.BidCount)
}; };
if (auction.BidHistory.Any()) if (auction.BidHistory.Any())
@@ -113,11 +115,11 @@ namespace AutoBidder.Models
} }
} }
if (auction.Bidders.Any()) if (auction.BidderStats != null && auction.BidderStats.Any())
{ {
var topBidder = auction.Bidders.OrderByDescending(b => b.Value).First(); var top = auction.BidderStats.OrderByDescending(b => b.Value.BidCount).First();
stats.MostActiveBidder = topBidder.Key; stats.MostActiveBidder = top.Key;
stats.MostActiveBidderCount = topBidder.Value; stats.MostActiveBidderCount = top.Value.BidCount;
} }
return stats; return stats;

View File

@@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace AutoBidder.Models
{
public class UserBannerInfo
{
public int nAsteConfermate { get; set; }
public int limiteAsteVinte { get; set; }
public int nAsteVinte { get; set; }
public int nPuntateRiscattate { get; set; }
public int nPuntateBonus { get; set; }
public int nPuntateDaRiscattare { get; set; }
public int nAstePerBonus { get; set; }
public long validUntil { get; set; }
public int percentualeBonus { get; set; }
public int viewSlot { get; set; }
public List<object> extraSlots { get; set; } = new();
}
}

View File

@@ -0,0 +1,10 @@
namespace AutoBidder.Models
{
public class UserData
{
public string? Username { get; set; }
public int RemainingBids { get; set; }
public double? CashBalance { get; set; }
public int? UserId { get; set; }
}
}

33
Mimante/README.md Normal file
View File

@@ -0,0 +1,33 @@
# AutoBidder
AutoBidder è una semplice applicazione WPF per monitorare aste Bidoo via API (HTTP-only).
Questa versione è stata ripulita da funzionalità legacy (WebView2/browser integrato, multi-click, modalità legacy) e si basa esclusivamente su chiamate HTTP verso le API del sito.
Features principali:
- Monitoraggio parallelo di più aste
- Polling adattivo basato su timer aste
- Invio puntate via HTTP
- Visualizzazione log globale e log per asta
- Esportazione CSV di cronologia e statistiche
- Salvataggio sessione (cookie) in modo sicuro (DPAPI)
Pulizia effettuata:
- Rimosse classi XAML/Converters non più utilizzate
- Rimosso file di test manuale
- Rifattorizzate statistiche per usare `BidHistory` e `BidderStats`
Come usare:
1. Avvia l'app
2. Configura la sessione con il cookie `__stattrb=...` tramite `Configura`
3. Aggiungi aste (ID o URL) con `+ Aggiungi`
4. Avvia il monitoraggio con `Avvia Tutti`
Sicurezza:
- La sessione viene salvata criptata in `%APPDATA%/AutoBidder/session.dat` usando DPAPI per l'utente corrente.
Note per sviluppatori:
- Progetto .NET 8 (WPF)
- File principali: `Services/BidooApiClient.cs`, `Services/AuctionMonitor.cs`, `MainWindow.xaml.cs`.
- Per ulteriori pulizie o refactor, eseguire una ricerca per `legacy` o `RIMOSSO`.

View File

@@ -8,8 +8,8 @@ using AutoBidder.Models;
namespace AutoBidder.Services namespace AutoBidder.Services
{ {
/// <summary> /// <summary>
/// Servizio centrale per monitoraggio multi-asta /// Servizio centrale per monitoraggio aste
/// Gestisce polling API Bidoo e trigger dei click /// Solo HTTP, nessuna modalità, browser o multi-click
/// </summary> /// </summary>
public class AuctionMonitor public class AuctionMonitor
{ {
@@ -79,6 +79,30 @@ namespace AutoBidder.Services
return _apiClient.GetSession(); return _apiClient.GetSession();
} }
/// <summary>
/// Ottiene dati utente (nome, puntate residue, saldo, id) tramite chiamata AJAX leggera
/// </summary>
public async Task<UserData?> GetUserDataAsync()
{
return await _apiClient.GetUserDataAsync();
}
/// <summary>
/// Ottiene info banner utente (aste vinte, bonus, ecc.) tramite chiamata AJAX
/// </summary>
public async Task<UserBannerInfo?> GetUserBannerInfoAsync()
{
return await _apiClient.GetUserBannerInfoAsync();
}
/// <summary>
/// Estrae nome utente e puntate residue dall'HTML di bids_history.php
/// </summary>
public async Task<UserData?> GetUserDataFromHtmlAsync()
{
return await _apiClient.GetUserDataFromHtmlAsync();
}
public void AddAuction(AuctionInfo auction) public void AddAuction(AuctionInfo auction)
{ {
lock (_auctions) lock (_auctions)
@@ -112,12 +136,6 @@ namespace AutoBidder.Services
} }
} }
public Task<bool> InitializeCookies(Microsoft.Web.WebView2.Wpf.WebView2 webView)
{
// Non più utilizzato - usa InitializeSession invece
return Task.FromResult(false);
}
public void Start() public void Start()
{ {
if (_monitoringTask != null && !_monitoringTask.IsCompleted) if (_monitoringTask != null && !_monitoringTask.IsCompleted)
@@ -259,6 +277,9 @@ namespace AutoBidder.Services
return; return;
} }
// Aggiorna la latenza per la DataGrid
auction.PollingLatencyMs = state.PollingLatencyMs;
// Se l'asta è terminata, segnala e disattiva polling // Se l'asta è terminata, segnala e disattiva polling
if (state.Status == AuctionStatus.EndedWon || if (state.Status == AuctionStatus.EndedWon ||
state.Status == AuctionStatus.EndedLost || state.Status == AuctionStatus.EndedLost ||
@@ -289,9 +310,10 @@ namespace AutoBidder.Services
return; return;
} }
// Log stato solo per aste attive (riduci spam) // Log stato solo per aste attive (riduci spam) - keep detailed per-auction log
if (state.Status == AuctionStatus.Running) if (state.Status == AuctionStatus.Running)
{ {
// Detailed info stays in auction log only
auction.AddLog($"API OK - Timer: {state.Timer:F2}s, EUR{state.Price:F2}, {state.LastBidder}, {state.PollingLatencyMs}ms"); auction.AddLog($"API OK - Timer: {state.Timer:F2}s, EUR{state.Price:F2}, {state.LastBidder}, {state.PollingLatencyMs}ms");
} }
else if (state.Status == AuctionStatus.Paused) else if (state.Status == AuctionStatus.Paused)
@@ -305,45 +327,92 @@ namespace AutoBidder.Services
// Aggiorna storico e bidders // Aggiorna storico e bidders
UpdateAuctionHistory(auction, state); UpdateAuctionHistory(auction, state);
// Verifica se puntare (solo se asta Running, NON se in pausa) // FINAL-ATTACK PROTOCOL: when the remaining timer is below our latency threshold (<= 0.5s)
if (state.Status == AuctionStatus.Running && ShouldBid(auction, state) && !auction.IsPaused) // we stop the normal polling loop for this auction and send a single minimal bid request.
if (state.Status == AuctionStatus.Running && !auction.IsPaused && ShouldBid(auction, state))
{ {
auction.AddLog($"[TRIGGER] CONDIZIONI OK - Timer {state.Timer:F2}s <= {auction.TimerClick}s"); // Use latency threshold (0.5s default) - treat as critical window
auction.AddLog($"[BID] Invio puntata..."); var latencyThreshold = 0.5; // seconds
OnLog?.Invoke($"[BID] [{auction.AuctionId}] PUNTATA a {state.Timer:F2}s!"); if (!auction.IsAttackInProgress && state.Timer <= latencyThreshold)
// Attendi ritardo configurato
if (auction.DelayMs > 0)
{ {
await Task.Delay(auction.DelayMs, token); // Enter attack state for this auction to avoid concurrent triggers
} auction.IsAttackInProgress = true;
// Esegui puntata API try
var result = await _apiClient.PlaceBidAsync(auction.AuctionId);
// Aggiorna contatori
if (result.Success)
{ {
auction.MyClicks++; // Log detailed info into auction log
auction.AddLog($"[ATTACK] Final attack: Timer {state.Timer:F3}s <= {latencyThreshold}s -> executing final bid...");
// Place final minimal bid (one GET with auctionID & submit=1)
var finalResult = await _apiClient.PlaceBidFinalAsync(auction.AuctionId, auction.OriginalUrl);
auction.LastClickAt = DateTime.UtcNow; auction.LastClickAt = DateTime.UtcNow;
OnBidExecuted?.Invoke(auction, finalResult);
if (finalResult.Success)
{
// Success: log concise global, detailed per-auction
auction.AddLog($"[OK] Final bid OK: {finalResult.LatencyMs}ms -> EUR{finalResult.NewPrice:F2}");
OnLog?.Invoke($"[OK] Puntata riuscita su {auction.Name} ({auction.AuctionId}): {finalResult.LatencyMs}ms");
// After success, an expiration may be extended by server (T_exp = now + 8s)
// We simply resume normal monitoring to observe new timer
}
else
{
// Failure cases: log and do not attempt immediate retries
auction.AddLog($"[FAIL] Final bid failed: {finalResult.Error}");
OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {finalResult.Error}");
} }
// Notifica risultato // Always add history entry
auction.BidHistory.Add(new BidHistory
{
Timestamp = finalResult.Timestamp,
EventType = finalResult.Success ? BidEventType.MyBid : BidEventType.OpponentBid,
Bidder = "Tu",
Price = state.Price,
Timer = state.Timer,
LatencyMs = finalResult.LatencyMs,
Success = finalResult.Success,
Notes = finalResult.Success ? $"EUR{finalResult.NewPrice:F2}" : finalResult.Error
});
}
finally
{
auction.IsAttackInProgress = false;
}
// Done with final attack; skip the older branch below
return;
}
// Otherwise fallback to normal early-bid behavior
if (Math.Abs(state.Timer) < 0.001)
{
// Put detailed info into auction log but avoid noisy global log lines
auction.AddLog($"[TRIGGER] Timer 0, attendo delay {auction.DelayMs}ms e invio puntata direttamente...");
if (auction.DelayMs > 0)
await Task.Delay(auction.DelayMs, token);
// Direct bid - API client already writes detailed request/response into auction.AddLog via subscription
var result = await _apiClient.PlaceBidAsync(auction.AuctionId);
auction.LastClickAt = DateTime.UtcNow;
OnBidExecuted?.Invoke(auction, result); OnBidExecuted?.Invoke(auction, result);
// Log // Add concise global log (single line) and keep extended details inside auction log
if (result.Success) if (result.Success)
{ {
auction.AddLog($"[OK] PUNTATA OK: {result.LatencyMs}ms -> EUR{result.NewPrice:F2}"); auction.AddLog($"[OK] PUNTATA OK: {result.LatencyMs}ms -> EUR{result.NewPrice:F2}");
OnLog?.Invoke($"[OK] [{auction.AuctionId}] Puntata riuscita {result.LatencyMs}ms"); OnLog?.Invoke($"[OK] Puntata riuscita su {auction.Name} ({auction.AuctionId}): {result.LatencyMs}ms");
} }
else else
{ {
auction.AddLog($"[FAIL] PUNTATA FALLITA: {result.Error}"); auction.AddLog($"[FAIL] PUNTATA FALLITA: {result.Error}");
OnLog?.Invoke($"[FAIL] [{auction.AuctionId}] ERRORE: {result.Error}"); OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {result.Error}");
} }
// Storico
auction.BidHistory.Add(new BidHistory auction.BidHistory.Add(new BidHistory
{ {
Timestamp = result.Timestamp, Timestamp = result.Timestamp,
@@ -355,13 +424,43 @@ namespace AutoBidder.Services
Success = result.Success, Success = result.Success,
Notes = result.Success ? $"EUR{result.NewPrice:F2}" : result.Error Notes = result.Success ? $"EUR{result.NewPrice:F2}" : result.Error
}); });
}
// Se abbiamo raggiunto il numero massimo di click per l'asta, metti in pausa le puntate (ma continua il monitor) else
if (auction.MaxClicks > 0 && auction.MyClicks >= auction.MaxClicks)
{ {
auction.IsPaused = true; // Normal early-bid path: schedule immediate delay then place bid
auction.AddLog($"[PAUSA] Massimo click ({auction.MaxClicks}) raggiunto - Puntate disabilitate"); auction.AddLog($"[TRIGGER] CONDIZIONI OK - Timer {state.Timer:F2}s <= {auction.TimerClick}s");
OnLog?.Invoke($"[PAUSA] [{auction.AuctionId}] MaxClicks raggiunti");
if (auction.DelayMs > 0)
{
await Task.Delay(auction.DelayMs, token);
}
var result = await _apiClient.PlaceBidAsync(auction.AuctionId);
auction.LastClickAt = DateTime.UtcNow;
OnBidExecuted?.Invoke(auction, result);
if (result.Success)
{
auction.AddLog($"[OK] PUNTATA OK: {result.LatencyMs}ms -> EUR{result.NewPrice:F2}");
OnLog?.Invoke($"[OK] Puntata riuscita su {auction.Name} ({auction.AuctionId}): {result.LatencyMs}ms");
}
else
{
auction.AddLog($"[FAIL] PUNTATA FALLITA: {result.Error}");
OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {result.Error}");
}
auction.BidHistory.Add(new BidHistory
{
Timestamp = result.Timestamp,
EventType = result.Success ? BidEventType.MyBid : BidEventType.OpponentBid,
Bidder = "Tu",
Price = state.Price,
Timer = state.Timer,
LatencyMs = result.LatencyMs,
Success = result.Success,
Notes = result.Success ? $"EUR{result.NewPrice:F2}" : result.Error
});
} }
} }
} }
@@ -393,10 +492,6 @@ namespace AutoBidder.Services
return false; return false;
} }
// Max clicks per auction
if (auction.MaxClicks > 0 && auction.MyClicks >= auction.MaxClicks)
return false;
return true; return true;
} }

View File

@@ -12,28 +12,7 @@ namespace AutoBidder.Services
{ {
/// <summary> /// <summary>
/// Servizio completo API Bidoo (polling, puntate, info utente) /// Servizio completo API Bidoo (polling, puntate, info utente)
/// 100% API-based con simulazione completa del comportamento browser /// Solo HTTP, nessuna modalità, browser o multi-click
///
/// 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> /// </summary>
public class BidooApiClient public class BidooApiClient
{ {
@@ -91,14 +70,13 @@ namespace AutoBidder.Services
} }
/// <summary> /// <summary>
/// Inizializza sessione con cookie string (fallback) /// Inizializza sessione con cookie string (manuale)
/// </summary> /// </summary>
public void InitializeSessionWithCookie(string cookieString, string username) public void InitializeSessionWithCookie(string cookieString, string username)
{ {
_session.CookieString = cookieString; _session.CookieString = cookieString;
_session.Username = username; _session.Username = username;
Log($"[SESSION] Cookie impostato manualmente ({cookieString.Length} chars)");
Log($"[SESSION] Cookie impostato ({cookieString.Length} chars)");
Log($"[SESSION] Username: {username}"); Log($"[SESSION] Username: {username}");
} }
@@ -108,20 +86,12 @@ namespace AutoBidder.Services
/// </summary> /// </summary>
private void AddAuthHeaders(HttpRequestMessage request, string? referer = null, string? auctionId = null) private void AddAuthHeaders(HttpRequestMessage request, string? referer = null, string? auctionId = null)
{ {
// 1. AUTENTICAZIONE (priorità: CookieString completa, poi Token singolo) // 1. AUTENTICAZIONE (solo cookie manuale)
// Il cookie principale per Bidoo è __stattr (non PHPSESSID)
if (!string.IsNullOrWhiteSpace(_session.CookieString)) if (!string.IsNullOrWhiteSpace(_session.CookieString))
{ {
// Usa la stringa cookie completa (es: "__stattr=eyJyZWZ...")
request.Headers.Add("Cookie", _session.CookieString); request.Headers.Add("Cookie", _session.CookieString);
Log("[AUTH] Using full cookie string", auctionId); 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 else
{ {
Log("[AUTH WARN] No authentication method available!", auctionId); Log("[AUTH WARN] No authentication method available!", auctionId);
@@ -244,40 +214,28 @@ namespace AutoBidder.Services
{ {
var startTime = DateTime.UtcNow; var startTime = DateTime.UtcNow;
var url = $"https://it.bidoo.com/data.php?ALL={auctionId}&LISTID=0"; 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 request = new HttpRequestMessage(HttpMethod.Get, url);
var referer = !string.IsNullOrEmpty(auctionUrl) var referer = !string.IsNullOrEmpty(auctionUrl)
? auctionUrl ? auctionUrl
: $"https://it.bidoo.com/auction.php?a=asta_{auctionId}"; : $"https://it.bidoo.com/auction.php?a=asta_{auctionId}";
Log($"[API REQUEST] Using referer: {referer}", auctionId);
AddAuthHeaders(request, referer, auctionId); AddAuthHeaders(request, referer, auctionId);
var response = await _httpClient.SendAsync(request, token); var response = await _httpClient.SendAsync(request, token);
var latency = (int)(DateTime.UtcNow - startTime).TotalMilliseconds; 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(); var responseText = await response.Content.ReadAsStringAsync();
Log($"[API RESPONSE] Body ({responseText.Length} bytes): {responseText}", auctionId);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
Log($"[API ERROR] Non-success status code: {response.StatusCode}", auctionId); string reason = response.StatusCode == System.Net.HttpStatusCode.RequestTimeout ? "timeout" : "errore HTTP";
Log($"[ERRORE] [{auctionId}] API non ha risposto (motivo: {reason})", null); // globale
Log($"API non ha risposto: {response.StatusCode} ({reason})", auctionId); // asta
return null; return null;
} }
var state = ParsePollingResponse(auctionId, responseText, latency);
return ParsePollingResponse(auctionId, responseText, latency); return state;
} }
catch (Exception ex) catch (Exception ex)
{ {
Log($"[API EXCEPTION] Polling {auctionId}: {ex.GetType().Name} - {ex.Message}", auctionId); Log($"[ERRORE] [{auctionId}] API non ha risposto (motivo: eccezione)", null); // globale
Log($"[API EXCEPTION] StackTrace: {ex.StackTrace}", auctionId); Log($"API non ha risposto: {ex.GetType().Name} - {ex.Message}", auctionId); // asta
return null; return null;
} }
} }
@@ -286,156 +244,71 @@ namespace AutoBidder.Services
{ {
try 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 serverTimestamp;
string mainData; string mainData;
// Cerca il separatore '*' che divide timestamp da dati
var starIndex = response.IndexOf('*'); var starIndex = response.IndexOf('*');
if (starIndex == -1) if (starIndex == -1)
{ {
Log("[PARSE ERROR] No '*' separator found in response", auctionId); Log("[PARSE ERROR] No '*' separator found in response", auctionId);
return null; return null;
} }
var timestampPart = response.Substring(0, starIndex); var timestampPart = response.Substring(0, starIndex);
mainData = response.Substring(starIndex + 1); mainData = response.Substring(starIndex + 1);
// Il timestamp può contenere '|' (es: "1761120002|1")
// Prendiamo solo la prima parte numerica
if (timestampPart.Contains('|')) if (timestampPart.Contains('|'))
{ {
serverTimestamp = timestampPart.Split('|')[0]; serverTimestamp = timestampPart.Split('|')[0];
Log($"[PARSE] Extended format detected: {timestampPart}", auctionId);
} }
else else
{ {
serverTimestamp = timestampPart; 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 bracketStart = mainData.IndexOf('[');
var bracketEnd = mainData.IndexOf(']'); var bracketEnd = mainData.IndexOf(']');
if (bracketStart == -1 || bracketEnd == -1) if (bracketStart == -1 || bracketEnd == -1)
{ {
Log("[PARSE ERROR] Missing brackets in auction data", auctionId); Log("[PARSE ERROR] Missing brackets in auction data", auctionId);
return null; return null;
} }
var auctionData = mainData.Substring(bracketStart + 1, bracketEnd - bracketStart - 1); 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 firstSeparator = auctionData.IndexOfAny(new[] { '|', ',' });
var coreData = firstSeparator > 0 ? auctionData.Substring(0, firstSeparator) : auctionData; var coreData = firstSeparator > 0 ? auctionData.Substring(0, firstSeparator) : auctionData;
var fields = coreData.Split(';'); 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) if (fields.Length < 5)
{ {
Log($"[PARSE ERROR] Expected at least 5 core fields, got {fields.Length}", auctionId); Log($"[PARSE ERROR] Expected at least 5 core fields, got {fields.Length}", auctionId);
return null; return null;
} }
var state = new AuctionState var state = new AuctionState
{ {
AuctionId = auctionId, AuctionId = auctionId,
SnapshotTime = DateTime.UtcNow, SnapshotTime = DateTime.UtcNow,
PollingLatencyMs = latency 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(); 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(); string lastBidder = fields[4].Trim();
bool hasWinner = !string.IsNullOrEmpty(lastBidder); bool hasWinner = !string.IsNullOrEmpty(lastBidder);
bool iAmWinner = hasWinner && lastBidder.Equals(_session.Username, StringComparison.OrdinalIgnoreCase); bool iAmWinner = hasWinner && lastBidder.Equals(_session.Username, StringComparison.OrdinalIgnoreCase);
state.Status = DetermineAuctionStatus(status, hasWinner, iAmWinner, ref state); 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)) 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); var timerSeconds = (double)(expiryTs - serverTs);
state.Timer = Math.Max(0, timerSeconds); 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)) if (int.TryParse(fields[3], out var priceIndex))
{ {
state.Price = priceIndex * 0.01; 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.LastBidder = fields[4].Trim();
state.IsMyBid = !string.IsNullOrEmpty(_session.Username) && state.IsMyBid = !string.IsNullOrEmpty(_session.Username) &&
state.LastBidder.Equals(_session.Username, StringComparison.OrdinalIgnoreCase); 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; state.ParsingSuccess = true;
// Log only summary on success
Log($"[PARSE SUCCESS] Timer: {state.Timer:F2}s, Price: €{state.Price:F2}, Bidder: {state.LastBidder}, Status: {state.Status}", auctionId); Log($"[PARSE SUCCESS] Timer: {state.Timer:F2}s, Price: €{state.Price:F2}, Bidder: {state.LastBidder}, Status: {state.Status}", auctionId);
return state; return state;
} }
catch (Exception ex) catch (Exception ex)
{ {
Log($"[PARSE EXCEPTION] {ex.GetType().Name}: {ex.Message}", auctionId); Log($"[PARSE EXCEPTION] {ex.GetType().Name}: {ex.Message}", auctionId);
Log($"[PARSE EXCEPTION] StackTrace: {ex.StackTrace}", auctionId);
return null; return null;
} }
} }
@@ -459,7 +332,7 @@ namespace AutoBidder.Services
Log($"[USER INFO RESPONSE] Latency: {latency}ms"); Log($"[USER INFO RESPONSE] Latency: {latency}ms");
var responseText = await response.Content.ReadAsStringAsync(); var responseText = await response.Content.ReadAsStringAsync();
Log($"[USER INFO RESPONSE] Body: {responseText}"); Log($"[USER INFO RESPONSE] Body length: {responseText.Length}");
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
@@ -484,39 +357,33 @@ namespace AutoBidder.Services
AuctionId = auctionId, AuctionId = auctionId,
Timestamp = DateTime.UtcNow Timestamp = DateTime.UtcNow
}; };
try try
{ {
Log($"[BID] Placing bid via direct GET to bid.php (no token)", auctionId); Log($"[BID] Placing bid via direct GET to bid.php", auctionId);
var url = "https://it.bidoo.com/bid.php"; var url = "https://it.bidoo.com/bid.php";
var payload = $"AID={WebUtility.UrlEncode(auctionId)}&sup=0&shock=0"; var payload = $"AID={WebUtility.UrlEncode(auctionId)}&sup=0&shock=0";
Log($"[BID REQUEST] GET {url}?{payload}", auctionId); Log($"[BID REQUEST] GET {url}?{payload}", auctionId);
var getUrl = url + "?" + payload; var getUrl = url + "?" + payload;
var request = new HttpRequestMessage(HttpMethod.Get, getUrl); var request = new HttpRequestMessage(HttpMethod.Get, getUrl);
var referer = !string.IsNullOrEmpty(auctionUrl) ? auctionUrl : $"https://it.bidoo.com/asta/nome-prodotto-{auctionId}"; var referer = !string.IsNullOrEmpty(auctionUrl) ? auctionUrl : $"https://it.bidoo.com/asta/nome-prodotto-{auctionId}";
AddAuthHeaders(request, referer, auctionId); AddAuthHeaders(request, referer, auctionId);
if (!request.Headers.Contains("Origin")) if (!request.Headers.Contains("Origin"))
{ {
request.Headers.Add("Origin", "https://it.bidoo.com"); request.Headers.Add("Origin", "https://it.bidoo.com");
} }
var startTime = DateTime.UtcNow; var startTime = DateTime.UtcNow;
var response = await _httpClient.SendAsync(request); var response = await _httpClient.SendAsync(request);
result.LatencyMs = (int)(DateTime.UtcNow - startTime).TotalMilliseconds; result.LatencyMs = (int)(DateTime.UtcNow - startTime).TotalMilliseconds;
Log($"[BID RESPONSE] Status: {(int)response.StatusCode} {response.StatusCode}", auctionId); Log($"[BID RESPONSE] Status: {(int)response.StatusCode} {response.StatusCode}", auctionId);
Log($"[BID RESPONSE] Latency: {result.LatencyMs}ms", auctionId); Log($"[BID RESPONSE] Latency: {result.LatencyMs}ms", auctionId);
var responseText = await response.Content.ReadAsStringAsync(); var responseText = await response.Content.ReadAsStringAsync();
result.Response = responseText; result.Response = responseText;
Log($"[BID RESPONSE] Body length: {responseText.Length} bytes", auctionId);
Log($"[BID RESPONSE] Body ({responseText.Length} bytes): {responseText}", auctionId); if (!string.IsNullOrEmpty(responseText))
{
var preview = responseText.Length > 80 ? responseText.Substring(0, 80) + "..." : responseText;
Log($"[BID RESPONSE] Preview: {preview}", auctionId);
}
if (responseText.StartsWith("ok", StringComparison.OrdinalIgnoreCase)) if (responseText.StartsWith("ok", StringComparison.OrdinalIgnoreCase))
{ {
result.Success = true; result.Success = true;
@@ -525,8 +392,7 @@ namespace AutoBidder.Services
{ {
result.NewPrice = priceIndex * 0.01; result.NewPrice = priceIndex * 0.01;
} }
Log("[BID SUCCESS] ✓ Bid placed successfully", auctionId);
Log("[BID SUCCESS] ✓ Bid placed successfully via GET", auctionId);
} }
else if (responseText.StartsWith("error", StringComparison.OrdinalIgnoreCase) || responseText.StartsWith("no|", StringComparison.OrdinalIgnoreCase)) else if (responseText.StartsWith("error", StringComparison.OrdinalIgnoreCase) || responseText.StartsWith("no|", StringComparison.OrdinalIgnoreCase))
{ {
@@ -547,7 +413,6 @@ namespace AutoBidder.Services
result.Error = string.IsNullOrEmpty(responseText) ? $"HTTP {(int)response.StatusCode}" : responseText; result.Error = string.IsNullOrEmpty(responseText) ? $"HTTP {(int)response.StatusCode}" : responseText;
Log($"[BID ERROR] Unexpected response format: {result.Error}", auctionId); Log($"[BID ERROR] Unexpected response format: {result.Error}", auctionId);
} }
return result; return result;
} }
catch (Exception ex) catch (Exception ex)
@@ -555,7 +420,79 @@ namespace AutoBidder.Services
result.Success = false; result.Success = false;
result.Error = ex.Message; result.Error = ex.Message;
Log($"[BID EXCEPTION] {ex.GetType().Name}: {ex.Message}", auctionId); Log($"[BID EXCEPTION] {ex.GetType().Name}: {ex.Message}", auctionId);
Log($"[BID EXCEPTION] StackTrace: {ex.StackTrace}", auctionId); Log($"[BID EXCEPTION] StackTrace available in debug logs", auctionId);
return result;
}
}
/// <summary>
/// Place a minimal final bid using the simpler payload required by the final-attack protocol.
/// Uses: ?auctionID=[ID]&submit=1
/// </summary>
public async Task<BidResult> PlaceBidFinalAsync(string auctionId, string? auctionUrl = null)
{
var result = new BidResult
{
AuctionId = auctionId,
Timestamp = DateTime.UtcNow
};
try
{
Log($"[BID FINAL] Placing final bid minimal payload", auctionId);
var url = "https://it.bidoo.com/bid.php";
var payload = $"auctionID={WebUtility.UrlEncode(auctionId)}&submit=1";
Log($"[BID REQUEST] GET {url}?{payload}", auctionId);
var getUrl = url + "?" + payload;
var request = new HttpRequestMessage(HttpMethod.Get, getUrl);
var referer = !string.IsNullOrEmpty(auctionUrl) ? auctionUrl : $"https://it.bidoo.com/asta/nome-prodotto-{auctionId}";
AddAuthHeaders(request, referer, auctionId);
if (!request.Headers.Contains("Origin"))
{
request.Headers.Add("Origin", "https://it.bidoo.com");
}
var startTime = DateTime.UtcNow;
var response = await _httpClient.SendAsync(request);
result.LatencyMs = (int)(DateTime.UtcNow - startTime).TotalMilliseconds;
Log($"[BID RESPONSE] Status: {(int)response.StatusCode} {response.StatusCode}", auctionId);
Log($"[BID RESPONSE] Latency: {result.LatencyMs}ms", auctionId);
var responseText = await response.Content.ReadAsStringAsync();
result.Response = responseText;
Log($"[BID RESPONSE] Body length: {responseText.Length} bytes", auctionId);
if (!string.IsNullOrEmpty(responseText))
{
var preview = responseText.Length > 80 ? responseText.Substring(0, 80) + "..." : responseText;
Log($"[BID RESPONSE] Preview: {preview}", auctionId);
}
if (responseText.StartsWith("ok", StringComparison.OrdinalIgnoreCase))
{
result.Success = true;
var parts = responseText.Split('|');
if (parts.Length > 1 && double.TryParse(parts[1], out var priceIndex))
{
result.NewPrice = priceIndex * 0.01;
}
Log("[BID SUCCESS] ✓ Final bid placed successfully", auctionId);
}
else if (responseText.StartsWith("error", StringComparison.OrdinalIgnoreCase) || responseText.StartsWith("no|", StringComparison.OrdinalIgnoreCase))
{
result.Success = false;
var parts = responseText.Split('|');
result.Error = parts.Length > 1 ? parts[1] : responseText;
Log($"[BID ERROR] Server returned error: {result.Error}", auctionId);
}
else
{
result.Success = false;
result.Error = string.IsNullOrEmpty(responseText) ? $"HTTP {(int)response.StatusCode}" : responseText;
Log($"[BID ERROR] Unexpected response format: {result.Error}", auctionId);
}
return result;
}
catch (Exception ex)
{
result.Success = false;
result.Error = ex.Message;
Log($"[BID EXCEPTION] {ex.GetType().Name}: {ex.Message}", auctionId);
return result; return result;
} }
} }
@@ -623,5 +560,136 @@ namespace AutoBidder.Services
{ {
_httpClient?.Dispose(); _httpClient?.Dispose();
} }
/// <summary>
/// Ottiene dati utente (nome, puntate residue, saldo, id) tramite chiamata AJAX leggera
/// </summary>
public async Task<UserData?> GetUserDataAsync()
{
try
{
var url = "https://it.bidoo.com/update_credits_status.php?submit=1";
Log($"[USER STATUS REQUEST] GET {url}");
var request = new HttpRequestMessage(HttpMethod.Get, url);
AddAuthHeaders(request, "https://it.bidoo.com/");
var response = await _httpClient.SendAsync(request);
var responseString = await response.Content.ReadAsStringAsync();
Log($"[USER STATUS RESPONSE] Body: {responseString}");
var userData = new UserData();
var trimmed = responseString.Trim();
// Caso: solo numero
if (int.TryParse(trimmed, out int bids))
{
userData.RemainingBids = bids;
return userData;
}
// Caso: HTML <span id="bids_count">125</span>
if (trimmed.StartsWith("<span") && trimmed.Contains("id=\"bids_count\""))
{
var start = trimmed.IndexOf('>');
var end = trimmed.IndexOf("</span>");
if (start >= 0 && end > start)
{
var value = trimmed.Substring(start + 1, end - start - 1);
if (int.TryParse(value, out bids))
{
userData.RemainingBids = bids;
return userData;
}
}
}
// Caso: stringa delimitata da pipe
var parts = trimmed.Split('|');
if (parts.Length >= 2)
{
userData.Username = parts[0];
if (int.TryParse(parts[1], out bids))
{
userData.RemainingBids = bids;
}
if (parts.Length >= 3 && double.TryParse(parts[2], out double cash))
{
userData.CashBalance = cash;
}
if (parts.Length >= 4 && int.TryParse(parts[3], out int userId))
{
userData.UserId = userId;
}
return userData;
}
// Se non riconosciuto
Log($"[USER STATUS PARSE ERROR] Formato non riconosciuto: {responseString}");
return null;
}
catch (Exception ex)
{
Log($"[USER STATUS EXCEPTION] {ex.GetType().Name}: {ex.Message}");
return null;
}
}
/// <summary>
/// Ottiene info banner utente (aste vinte, bonus, ecc.) tramite chiamata AJAX
/// </summary>
public async Task<UserBannerInfo?> GetUserBannerInfoAsync()
{
try
{
var url = "https://it.bidoo.com/ajax/get_auction_bids_info_banner.php";
Log($"[USER BANNER REQUEST] GET {url}");
var request = new HttpRequestMessage(HttpMethod.Get, url);
AddAuthHeaders(request, "https://it.bidoo.com/");
var response = await _httpClient.SendAsync(request);
var responseString = await response.Content.ReadAsStringAsync();
Log($"[USER BANNER RESPONSE] Body: {responseString}");
if (!response.IsSuccessStatusCode || string.IsNullOrWhiteSpace(responseString))
return null;
var info = System.Text.Json.JsonSerializer.Deserialize<UserBannerInfo>(responseString);
return info;
}
catch (Exception ex)
{
Log($"[USER BANNER EXCEPTION] {ex.GetType().Name}: {ex.Message}");
return null;
}
}
/// <summary>
/// Estrae nome utente e puntate residue dall'HTML di bids_history.php
/// </summary>
public async Task<UserData?> GetUserDataFromHtmlAsync()
{
try
{
var url = "https://it.bidoo.com/bids_history.php";
Log($"[USER HTML REQUEST] GET {url}");
var request = new HttpRequestMessage(HttpMethod.Get, url);
AddAuthHeaders(request, "https://it.bidoo.com/");
var response = await _httpClient.SendAsync(request);
var html = await response.Content.ReadAsStringAsync();
Log($"[USER HTML RESPONSE] Body length: {html.Length}");
var userData = new UserData();
// Estrai nome utente
var userMatch = System.Text.RegularExpressions.Regex.Match(html, @"<a class=""pers_lnk""[^>]*>([^<]+)</a>");
if (userMatch.Success)
{
userData.Username = userMatch.Groups[1].Value.Trim();
}
// Estrai puntate residue
var bidsMatch = System.Text.RegularExpressions.Regex.Match(html, @"<span id=""divSaldoBidBottom""[^>]*>(\d+)</span>");
if (bidsMatch.Success && int.TryParse(bidsMatch.Groups[1].Value, out int bids))
{
userData.RemainingBids = bids;
}
if (!string.IsNullOrEmpty(userData.Username) && userData.RemainingBids > 0)
return userData;
return null;
}
catch (Exception ex)
{
Log($"[USER HTML EXCEPTION] {ex.GetType().Name}: {ex.Message}");
return null;
}
}
} }
} }

View File

@@ -10,6 +10,7 @@ namespace AutoBidder.Services
/// <summary> /// <summary>
/// Gestore persistenza sessione Bidoo /// Gestore persistenza sessione Bidoo
/// Salva in modo sicuro il cookie di autenticazione per riutilizzo /// Salva in modo sicuro il cookie di autenticazione per riutilizzo
/// Il cookie deve essere inserito manualmente dall'utente (copiato dal browser)
/// </summary> /// </summary>
public class SessionManager public class SessionManager
{ {

View File

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

View File

@@ -101,22 +101,23 @@ namespace AutoBidder.Utilities
var csv = new StringBuilder(); var csv = new StringBuilder();
// Header // Header
csv.AppendLine("Auction ID,Name,Timer Click,Delay (ms),Min Price,Max Price," + csv.AppendLine("Auction ID,Name,Timer Click,Delay (ms),Min Price,Max Price,My Bids,Resets,Total Bidders,Active,Paused,Added At,Last Click At");
"My Clicks,Resets,Total Bidders,Active,Paused," +
"Added At,Last Click At");
// Data // Data
foreach (var auction in auctions) foreach (var auction in auctions)
{ {
var totalBidders = auction.BidderStats?.Count ?? 0;
var myBids = auction.BidHistory.Count(h => h.EventType == BidEventType.MyBid);
csv.AppendLine($"{auction.AuctionId}," + csv.AppendLine($"{auction.AuctionId}," +
$"\"{auction.Name}\"," + $"\"{auction.Name}\"," +
$"{auction.TimerClick}," + $"{auction.TimerClick}," +
$"{auction.DelayMs}," + $"{auction.DelayMs}," +
$"{auction.MinPrice:F2}," + $"{auction.MinPrice:F2}," +
$"{auction.MaxPrice:F2}," + $"{auction.MaxPrice:F2}," +
$"{auction.MyClicks}," + $"{myBids}," +
$"{auction.ResetCount}," + $"{auction.ResetCount}," +
$"{auction.Bidders.Count}," + $"{totalBidders}," +
$"{auction.IsActive}," + $"{auction.IsActive}," +
$"{auction.IsPaused}," + $"{auction.IsPaused}," +
$"{auction.AddedAt:yyyy-MM-dd HH:mm:ss}," + $"{auction.AddedAt:yyyy-MM-dd HH:mm:ss}," +

View File

@@ -0,0 +1,34 @@
using System;
using System.Windows.Input;
namespace AutoBidder.Utilities
{
public class RelayCommand : ICommand
{
private readonly Action<object?> _execute;
private readonly Func<object?, bool>? _canExecute;
public RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
return _canExecute?.Invoke(parameter) ?? true;
}
public void Execute(object? parameter)
{
_execute(parameter);
}
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
}

View File

@@ -1,11 +1,13 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Linq;
using AutoBidder.Models; using AutoBidder.Models;
namespace AutoBidder.ViewModels namespace AutoBidder.ViewModels
{ {
/// <summary> /// <summary>
/// ViewModel per una riga della griglia aste (DataBinding) /// ViewModel per una riga della griglia aste (DataBinding)
/// Solo HTTP, nessuna modalità, browser o multi-click
/// </summary> /// </summary>
public class AuctionViewModel : INotifyPropertyChanged public class AuctionViewModel : INotifyPropertyChanged
{ {
@@ -64,6 +66,16 @@ namespace AutoBidder.ViewModels
} }
} }
public int MaxClicks
{
get => _auctionInfo.MaxClicks;
set
{
_auctionInfo.MaxClicks = value;
OnPropertyChanged(nameof(MaxClicks));
}
}
// Stato // Stato
public bool IsActive public bool IsActive
{ {
@@ -74,7 +86,6 @@ namespace AutoBidder.ViewModels
OnPropertyChanged(nameof(IsActive)); OnPropertyChanged(nameof(IsActive));
} }
} }
public bool IsPaused public bool IsPaused
{ {
get => _auctionInfo.IsPaused; get => _auctionInfo.IsPaused;
@@ -84,11 +95,24 @@ namespace AutoBidder.ViewModels
OnPropertyChanged(nameof(IsPaused)); OnPropertyChanged(nameof(IsPaused));
} }
} }
// Contatori
public int MyClicks => _auctionInfo.MyClicks;
public int ResetCount => _auctionInfo.ResetCount; public int ResetCount => _auctionInfo.ResetCount;
// My clicks computed from history (thread-safe, null-safe)
public int MyClicks
{
get
{
var history = _auctionInfo.BidHistory;
if (history == null) return 0;
BidHistory[] snapshot;
lock (history)
{
snapshot = history.ToArray();
}
return snapshot.Count(h => h != null && h.EventType == BidEventType.MyBid);
}
}
// Dati live (da ultimo polling) // Dati live (da ultimo polling)
public string TimerDisplay public string TimerDisplay
{ {
@@ -192,7 +216,6 @@ namespace AutoBidder.ViewModels
public void UpdateState(AuctionState state) public void UpdateState(AuctionState state)
{ {
_lastState = state; _lastState = state;
// Notifica tutte le proprietà dipendenti dallo stato // Notifica tutte le proprietà dipendenti dallo stato
OnPropertyChanged(nameof(TimerDisplay)); OnPropertyChanged(nameof(TimerDisplay));
OnPropertyChanged(nameof(PriceDisplay)); OnPropertyChanged(nameof(PriceDisplay));
@@ -200,6 +223,8 @@ namespace AutoBidder.ViewModels
OnPropertyChanged(nameof(IsMyBid)); OnPropertyChanged(nameof(IsMyBid));
OnPropertyChanged(nameof(StatusDisplay)); OnPropertyChanged(nameof(StatusDisplay));
OnPropertyChanged(nameof(Strategy)); OnPropertyChanged(nameof(Strategy));
OnPropertyChanged(nameof(MyClicks));
OnPropertyChanged(nameof(AuctionInfo)); // For PollingLatencyMs
} }
/// <summary> /// <summary>
@@ -207,8 +232,9 @@ namespace AutoBidder.ViewModels
/// </summary> /// </summary>
public void RefreshCounters() public void RefreshCounters()
{ {
OnPropertyChanged(nameof(MyClicks)); // RIMOSSO: OnPropertyChanged(nameof(MyClicks));
OnPropertyChanged(nameof(ResetCount)); OnPropertyChanged(nameof(ResetCount));
OnPropertyChanged(nameof(MyClicks));
} }
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;

101
README.md
View File

@@ -2,7 +2,7 @@
> AutoBidder è uno strumento desktop per Windows (WPF, .NET 8) pensato per automatizzare le offerte sul sito Bidoo.com. Questa guida descrive caratteristiche, installazione, configurazione, modalità operative, strategie avanzate, dettagli tecnici e risoluzione dei problemi. > AutoBidder è uno strumento desktop per Windows (WPF, .NET 8) pensato per automatizzare le offerte sul sito Bidoo.com. Questa guida descrive caratteristiche, installazione, configurazione, modalità operative, strategie avanzate, dettagli tecnici e risoluzione dei problemi.
![Version](https://img.shields.io/badge/version-2.10-blue) ![Version](https://img.shields.io/badge/version-3.0-blue)
![.NET](https://img.shields.io/badge/.NET-8.0-purple) ![.NET](https://img.shields.io/badge/.NET-8.0-purple)
--- ---
@@ -12,9 +12,9 @@ Sommario
- Requisiti di sistema - Requisiti di sistema
- Installazione e avvio - Installazione e avvio
- Guida rapida (primi passi) - Guida rapida (primi passi)
- Modalità operative: Asta Singola e Multi-Asta - Gestione aste tramite griglia
- Strategie e consigli pratici - Strategie e consigli pratici
- Dettagli tecnici: Polling, Click HTTP e sincronizzazione cookie - Dettagli tecnici: Polling e Click HTTP
- Impostazioni e persistenza - Impostazioni e persistenza
- Esportazione e diagnostica - Esportazione e diagnostica
- FAQ e risoluzione problemi - FAQ e risoluzione problemi
@@ -23,10 +23,9 @@ Sommario
--- ---
## Caratteristiche principali ## Caratteristiche principali
- Monitoraggio in tempo reale di singole aste o molte aste contemporaneamente - Monitoraggio in tempo reale di tutte le aste tramite griglia unica
- Due modalità operative: `Asta Singola` (massima precisione) e `Multi-Asta` (monitoraggio e auto-switch) - Polling adattivo solo HTTP per ridurre uso di CPU e RAM
- Polling adattivo (HTTP / WebView2 / Active) per ridurre uso di CPU e RAM - Click HTTP diretto (reverse engineered) con cookie manuale
- Click HTTP diretto (reverse engineered) con sincronizzazione dei cookie dal WebView2
- Persistenza della lista aste (`auctions.json`) e esportazione CSV delle statistiche - Persistenza della lista aste (`auctions.json`) e esportazione CSV delle statistiche
- UI dark theme moderna con griglia in tempo reale, dettagli asta, log e contatori - UI dark theme moderna con griglia in tempo reale, dettagli asta, log e contatori
@@ -44,59 +43,47 @@ Sommario
- `dotnet restore` - `dotnet restore`
- `dotnet build --configuration Release` - `dotnet build --configuration Release`
4. Eseguire l'app: 4. Eseguire l'app:
- `dotnet run` (dalla cartella del progetto) oppure avviare `AutoBidder.exe` in `bin\\Release\\net8.0-windows` - `dotnet run` (dalla cartella del progetto) oppure avviare `AutoBidder.exe` in `bin\Release\net8.0-windows`
## Guida rapida (primi passi) ## Guida rapida (primi passi)
1. Avvia l'app: l'interfaccia principale mostra due pannelli (controlli a sinistra, browser WebView2 a destra). 1. Avvia l'app: l'interfaccia principale mostra la griglia delle aste monitorate.
2. Login: accedi a `bidoo.com` tramite il browser integrato (se vuoi usare il Click HTTP diretto devi essere loggato). 2. Configura la sessione: copia il cookie `__stattrb` dal browser (F12 > Application > Cookies) e inseriscilo tramite il dialog di configurazione.
3. Scegli modalità: 3. Aggiungi aste: usa il pulsante `+ Aggiungi` per inserire URL o ID delle aste da monitorare.
- `Asta Singola` per concentrarti su un solo oggetto 4. Configura impostazioni per ogni asta (Timer Click, Min/Max Price, Max Clicks, Max Resets, Ritardo).
- `Multi-Asta` per monitorare più aste e lasciare che l'app esegua auto-switch 5. Premi `Avvia Tutti` per attivare il monitoraggio e le puntate automatiche.
4. Aggiungi aste (Multi-Asta):
- Metodo Automatico: vai sulla pagina Preferiti e lascia che l'app rilevi le aste
- Metodo Manuale: clicca `+ URL` o `Pagina` e incolla/aggiungi l'URL dell'asta
5. Configura impostazioni globali o per-asta (Timer Click, Min/Max Price, Max Clicks, Max Resets, Ritardo, Multi-Click)
6. Premi `Avvia` per attivare il Click Loop; il background polling rimane sempre attivo anche senza Avvia.
## Modalità operative ## Gestione aste tramite griglia
- Tutte le aste sono gestite tramite una griglia unica.
### Asta Singola - Puoi avviare, mettere in pausa, fermare o puntare manualmente su ogni asta direttamente dalla griglia.
- Ideale per oggetti di valore dove la precisione è critica. - Non esistono più modalità "Asta Singola" o "Multi-Asta": tutto è gestito in modo uniforme.
- Monitoraggio intensivo del DOM tramite WebView2, contatori dettagliati, lista utenti e log dedicato.
- Supporta `Multi-Click` (click multipli paralleli) per aumentare probabilità in connessione instabile.
- Impostazioni consigliate per asta singola: `Timer Click` 0-1, `Multi-Click` ON, `Ritardo` 0ms.
### Multi-Asta
- Monitoraggio simultaneo di molte aste.
- L'app punta solo sull'asta con `timer` più basso (auto-switch) per evitare concorrere contro se stessa.
- Per ogni asta è possibile impostare opzioni indipendenti (Timer Click, Min/Max Price, Pausa/Riprendi).
- Metodo manuale (URL diretto) è raccomandato per monitoraggi massivi (consumo risorse minimo).
## Strategie consigliate ## Strategie consigliate
- Monitoraggio massivo (50+ aste): usa `URL Manuale`, `Timer Click` 0, limiti di prezzo bassi. - Monitoraggio massivo: aggiungi molte aste tramite URL o ID, imposta Timer Click basso e limiti di prezzo.
- Asta singola ad alto valore: `Timer Click` 0-1, `Multi-Click` ON, `Ritardo` 0ms. - Asta ad alto valore: Timer Click 0-1, Ritardo 0ms.
- Caccia all'affare: `Max Price` molto basso, monitora molte aste con Timer Click 0. - Caccia all'affare: Max Price basso, monitora molte aste con Timer Click 0.
## Approfondimento tecnico ## Approfondimento tecnico
### Polling adattivo (Dual-Track) ### Polling adattivo
- Track 1 — Background Polling: esegue richieste HTTP ogni ~5s per aggiornare timer, prezzo e ultimo bidder. Mantiene uso CPU/RAM minimo. - Il monitoraggio avviene solo tramite chiamate HTTP dirette alle API di Bidoo.
- Track 2 — Click Loop: attivo solo dopo `Avvia`. Polling dinamico 20-400ms per aste critiche e invio dei click (HTTP diretto o WebView2 fallback). - Nessun WebView2 viene utilizzato, nemmeno come fallback.
- Il polling si adatta al timer dell'asta per ottimizzare la frequenza delle richieste.
### Click HTTP diretto ### Click HTTP diretto
- Al momento dell'azione, il client costruisce una GET verso l'endpoint reverse engineered di Bidoo tipo: - Al momento dell'azione, il client costruisce una GET verso l'endpoint reverse engineered di Bidoo tipo:
`GET https://it.bidoo.com/bid.php?AID=81204347&sup=0&shock=0` `GET https://it.bidoo.com/bid.php?AID=81204347&sup=0&shock=0`
- Richiede i cookie di sessione (PHPSESSID, user_token, ecc.) che vengono sincronizzati dal WebView2. - Richiede i cookie di sessione (`__stattrb`) che vengono copiati manualmente dal browser.
- Latenza tipica: 10-30ms (molto più veloce del click via WebView2 che può impiegare 50-100ms) - Latenza tipica: 10-30ms.
- Se il Click HTTP fallisce, l'app esegue fallback automatico con `ExecuteScript` su WebView2. - Non esiste più alcun fallback WebView2: tutte le puntate sono gestite solo via HTTP.
### Sincronizzazione cookie ### Sincronizzazione cookie
- Al primo `Avvia` viene letto il `CookieManager` di `CoreWebView2`, copiato in un `CookieContainer` per l'`HttpClient` utilizzato dai Click HTTP. - I cookie vengono inseriti manualmente dall'utente tramite dialog.
- I cookie sono conservati solo in memoria, mai su disco. Devono essere rinfrescati ri-effettuando il login nel WebView2 se scadono. - Non vengono mai salvati su disco in chiaro.
- Se il cookie scade, è necessario copiarlo nuovamente dal browser.
## Persistenza e file locali ## Persistenza e file locali
- Lista aste manuali salvata in: - Lista aste manuali salvata in:
`%AppData%\\AutoBidder\\auctions.json` `%AppData%\AutoBidder\auctions.json`
- La lista viene ricaricata automaticamente all'avvio dell'app. - La lista viene ricaricata automaticamente all'avvio dell'app.
- Esportazione CSV: la funzionalità `Export CSV` genera un file contenente statistiche per ogni asta (nome, ID, URL, timer, prezzo, strategy, click/resets, impostazioni per-asta). - Esportazione CSV: la funzionalità `Export CSV` genera un file contenente statistiche per ogni asta (nome, ID, URL, timer, prezzo, strategy, click/resets, impostazioni per-asta).
@@ -105,13 +92,10 @@ Sommario
- `Max Clicks` / `Max Resets`: limiti operativi (0 = illimitato) - `Max Clicks` / `Max Resets`: limiti operativi (0 = illimitato)
- `Min/Max Price`: evita puntate fuori dal range desiderato - `Min/Max Price`: evita puntate fuori dal range desiderato
- `Ritardo (ms)`: delay aggiuntivo prima di inviare il click - `Ritardo (ms)`: delay aggiuntivo prima di inviare il click
- `Multi-Click`: invia più click paralleli (utile su connessioni con jitter)
## Interfaccia e controlli ## Interfaccia e controlli
- Pannello sinistro: controlli principali (modalità, Avvia, Pausa, Stop, aggiungi/rimuovi aste, esporta CSV) - Griglia centrale: mostra tutte le aste monitorate con controlli per avviare, mettere in pausa, fermare e puntare manualmente.
- Pannello destro: browser integrato (WebView2) + dettagli asta quando selezionata - Log in tempo reale per ogni asta con dettagli di latenza, risposta server e fallimenti.
- Griglia aste (multi-asta): mostra Timer, Prezzo, Strategia, Clicks, Resets, Ultimo bidder
- Log in tempo reale per ogni asta con dettagli di latenza, risposta server e fallimenti
## Diagnostica ed esportazione ## Diagnostica ed esportazione
- Abilita log dettagliato prima di aprire issue o per analisi locali - Abilita log dettagliato prima di aprire issue o per analisi locali
@@ -119,17 +103,18 @@ Sommario
- Mantieni screenshot del pannello log per report più chiari - Mantieni screenshot del pannello log per report più chiari
## FAQ e risoluzione dei problemi ## FAQ e risoluzione dei problemi
- "Non vedo Click HTTP riuscito": assicurati di essere loggato nel WebView2 e che la sincronizzazione cookie sia avvenuta (vedi log "Cookie sincronizzati (X cookie)"). Premi `Avvia` dopo il login. - "Non vedo Click HTTP riuscito": assicurati di aver inserito correttamente il cookie dal browser e che la sessione sia valida.
- "Aste non rilevate": assicurati di essere nella pagina Preferiti per il metodo automatico oppure aggiungi gli URL manualmente. - "Aste non rilevate": aggiungi manualmente gli URL o ID delle aste.
- "Il programma non si avvia": verifica .NET 8.0 Runtime installato e che il build sia andato a buon fine. - "Il programma non si avvia": verifica .NET 8.0 Runtime installato e che il build sia andato a buon fine.
- "Click non funzionano": verifica Timer Click, limiti di prezzo e connessione internet. Controlla se il fallback WebView è attivo nei log. - "Click non funzionano": verifica Timer Click, limiti di prezzo, connessione internet e validità del cookie.
## Avvisi e responsabilità ## Avvisi e responsabilità
- L'automazione potrebbe violare i Termini di Servizio di Bidoo. L'uso è a rischio e responsabilità dell'utente. - L'automazione potrebbe violare i Termini di Servizio di Bidoo. L'uso è a rischio e responsabilità dell'utente.
- Non salvare credenziali su disco: l'app non memorizza login, usa il WebView2 per la sessione. - Non salvare credenziali su disco: l'app non memorizza login, usa solo il cookie manuale.
- Cookie e dati di sessione rimangono in memoria e vengono rimossi alla chiusura dell'app. - Cookie e dati di sessione rimangono in memoria e vengono rimossi alla chiusura dell'app.
## Changelog sintetico (ultime versioni) ## Changelog sintetico (ultime versioni)
- v3.0: Solo HTTP, nessun WebView2, cookie manuale, interfaccia unica su griglia
- v2.10: Click HTTP diretto, sincronizzazione cookie automatica, miglioramenti prestazionali - v2.10: Click HTTP diretto, sincronizzazione cookie automatica, miglioramenti prestazionali
- v2.9: Persistenza automatica, UI improvements, export CSV - v2.9: Persistenza automatica, UI improvements, export CSV
- v2.8: Polling adattivo e strategie ibride - v2.8: Polling adattivo e strategie ibride
@@ -141,13 +126,13 @@ Sommario
--- ---
Note tecniche per sviluppatori Note tecniche per sviluppatori
- Progetto target: `.NET 8.0` (WPF + WebView2) - Progetto target: `.NET 8.0` (WPF)
- Aree chiave del codice: - Aree chiave del codice:
- `Services\\BidooApiClient.cs` — gestione Click HTTP e parsing risposte - `Services\BidooApiClient.cs` — gestione Click HTTP e parsing risposte
- `Services\\AuctionMonitor.cs` — loop di polling e logica auto-switch - `Services\AuctionMonitor.cs` — loop di polling
- `Services\\SessionManager.cs`sincronizzazione cookie e HttpClient creation - `Services\SessionManager.cs`gestione cookie manuale
- `Utilities\\PersistenceManager.cs` — salvataggio/ricaricamento `auctions.json` - `Utilities\PersistenceManager.cs` — salvataggio/ricaricamento `auctions.json`
- `ViewModels\\AuctionViewModel.cs` + XAML corrispondenti — visualizzazione e binding UI - `ViewModels\AuctionViewModel.cs` + XAML corrispondenti — visualizzazione e binding UI
## Contributi ## Contributi
- Questo repository è privato. Per contribuire, aprire PR verso branch `main` e seguire le convenzioni del progetto. - Questo repository è privato. Per contribuire, aprire PR verso branch `main` e seguire le convenzioni del progetto.