diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 78f3a66..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,4 +0,0 @@ -# These are supported funding model platforms - -github: theshadow76 -custom: ['https://paypal.me/shadowtechsc?country.x=CL&locale.x=es_XC]' diff --git a/README.md b/README.md index 61d4b8f..edd8dec 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ pip install -r requirements.txt pip install -r requirements-dev.txt ``` -## šŸ”§ Quick Start +## Quick Start ### Getting Your SSID @@ -71,4 +71,4 @@ Example SSID format: 42["auth",{"session":"abcd1234efgh5678","isDemo":1,"uid":12345,"platform":1}] ``` -If you are unable to find it, try running the automatic SSID scraper under the `SSID` folder. \ No newline at end of file +If you are unable to find it, try running the automatic SSID scraper under the `tools` folder. \ No newline at end of file diff --git a/TODO/todo.md b/TODO/todo.md deleted file mode 100644 index d1f45cb..0000000 --- a/TODO/todo.md +++ /dev/null @@ -1,6 +0,0 @@ -# TO-DO List - -### Add Template for a basic PO bot - - TBD -### Organize files more efficiently - - TBD \ No newline at end of file diff --git a/connection_keep_alive.py b/connection_keep_alive.py deleted file mode 100644 index cdb4193..0000000 --- a/connection_keep_alive.py +++ /dev/null @@ -1,554 +0,0 @@ -""" -Enhanced Keep-Alive Connection Manager for PocketOption Async API -""" - -import asyncio -from typing import Optional, List, Callable, Dict, Any -from datetime import datetime, timedelta -from loguru import logger -import websockets -from websockets.exceptions import ConnectionClosed - -from pocketoptionapi_async.models import ConnectionInfo, ConnectionStatus -from pocketoptionapi_async.constants import REGIONS - - -class ConnectionKeepAlive: - """ - Advanced connection keep-alive manager based on old API patterns - """ - - def __init__(self, ssid: str, is_demo: bool = True): - self.ssid = ssid - self.is_demo = is_demo - - # Connection state - self.websocket: Optional[websockets.WebSocketServerProtocol] = None - self.connection_info: Optional[ConnectionInfo] = None - self.is_connected = False - self.should_reconnect = True - - # Background tasks - self._ping_task: Optional[asyncio.Task] = None - self._reconnect_task: Optional[asyncio.Task] = None - self._message_task: Optional[asyncio.Task] = None - self._health_task: Optional[asyncio.Task] = None - - # Keep-alive settings - self.ping_interval = 20 # seconds (same as old API) - self.reconnect_delay = 5 # seconds - self.max_reconnect_attempts = 10 - self.current_reconnect_attempts = 0 - - # Event handlers - self._event_handlers: Dict[str, List[Callable]] = {} - - # Connection pool with multiple regions - self.available_urls = ( - REGIONS.get_demo_regions() if is_demo else REGIONS.get_all() - ) - self.current_url_index = 0 - - # Statistics - self.connection_stats = { - "total_connections": 0, - "successful_connections": 0, - "total_reconnects": 0, - "last_ping_time": None, - "last_pong_time": None, - "total_messages_sent": 0, - "total_messages_received": 0, - } - - logger.info( - f"Initialized keep-alive manager with {len(self.available_urls)} available regions" - ) - - async def start_persistent_connection(self) -> bool: - """ - Start a persistent connection with automatic keep-alive - Similar to old API's daemon thread approach but with modern async - """ - logger.info("Starting persistent connection with keep-alive...") - - try: - # Initial connection - if await self._establish_connection(): - # Start all background tasks - await self._start_background_tasks() - logger.success( - "Success: Persistent connection established with keep-alive active" - ) - return True - else: - logger.error("Error: Failed to establish initial connection") - return False - - except Exception as e: - logger.error(f"Error: Error starting persistent connection: {e}") - return False - - async def stop_persistent_connection(self): - """Stop the persistent connection and all background tasks""" - logger.info("Stopping persistent connection...") - - self.should_reconnect = False - - # Cancel all background tasks - tasks = [ - self._ping_task, - self._reconnect_task, - self._message_task, - self._health_task, - ] - for task in tasks: - if task and not task.done(): - task.cancel() - try: - await task - except asyncio.CancelledError: - pass - - # Close connection - if self.websocket: - await self.websocket.close() - self.websocket = None - - self.is_connected = False - logger.info("Success: Persistent connection stopped") - - async def _establish_connection(self) -> bool: - """ - Establish connection with fallback URLs (like old API) - """ - for attempt in range(len(self.available_urls)): - url = self.available_urls[self.current_url_index] - - try: - logger.info( - f"Connecting: Attempting connection to {url} (attempt {attempt + 1})" - ) - - # SSL context (like old API) - import ssl - - ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - - # Connect with headers (like old API) - self.websocket = await asyncio.wait_for( - websockets.connect( - url, - ssl=ssl_context, - extra_headers={ - "Origin": "https://pocketoption.com", - "Cache-Control": "no-cache", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", - }, - ping_interval=None, # We handle pings manually - ping_timeout=None, - close_timeout=10, - ), - timeout=15.0, - ) - - # Update connection info - region = self._extract_region_from_url(url) - self.connection_info = ConnectionInfo( - url=url, - region=region, - status=ConnectionStatus.CONNECTED, - connected_at=datetime.now(), - reconnect_attempts=self.current_reconnect_attempts, - ) - - self.is_connected = True - self.current_reconnect_attempts = 0 - self.connection_stats["total_connections"] += 1 - self.connection_stats["successful_connections"] += 1 - - # Send initial handshake (like old API) - await self._send_handshake() - - logger.success(f"Success: Connected to {region} region successfully") - await self._emit_event("connected", {"url": url, "region": region}) - - return True - - except Exception as e: - logger.warning(f"Caution: Failed to connect to {url}: {e}") - - # Try next URL - self.current_url_index = (self.current_url_index + 1) % len( - self.available_urls - ) - - if self.websocket: - try: - await self.websocket.close() - except Exception: - pass - self.websocket = None - - await asyncio.sleep(1) # Brief delay before next attempt - - return False - - async def _send_handshake(self): - """Send initial handshake sequence (like old API)""" - try: - # Wait for initial connection message - initial_message = await asyncio.wait_for( - self.websocket.recv(), timeout=10.0 - ) - logger.debug(f"Received initial: {initial_message}") - - # Send handshake sequence (like old API) - await self.websocket.send("40") - await asyncio.sleep(0.1) - - # Wait for connection establishment - conn_message = await asyncio.wait_for(self.websocket.recv(), timeout=10.0) - logger.debug(f"Received connection: {conn_message}") - - # Send SSID authentication - await self.websocket.send(self.ssid) - logger.debug("Handshake completed") - - self.connection_stats["total_messages_sent"] += 2 - - except Exception as e: - logger.error(f"Handshake failed: {e}") - raise - - async def _start_background_tasks(self): - """Start all background tasks (like old API's concurrent tasks)""" - logger.info("Persistent: Starting background keep-alive tasks...") - - # Ping task (every 20 seconds like old API) - self._ping_task = asyncio.create_task(self._ping_loop()) - - # Message receiving task - self._message_task = asyncio.create_task(self._message_loop()) - - # Health monitoring task - self._health_task = asyncio.create_task(self._health_monitor_loop()) - - # Reconnection monitoring task - self._reconnect_task = asyncio.create_task(self._reconnection_monitor()) - - logger.success("Success: All background tasks started") - - async def _ping_loop(self): - """ - Continuous ping loop (like old API's send_ping function) - Sends '42["ps"]' every 20 seconds - """ - logger.info("Ping: Starting ping loop...") - - while self.should_reconnect: - try: - if self.is_connected and self.websocket: - # Send ping message (exact format from old API) - await self.websocket.send('42["ps"]') - self.connection_stats["last_ping_time"] = datetime.now() - self.connection_stats["total_messages_sent"] += 1 - - logger.debug("Ping: Ping sent") - - await asyncio.sleep(self.ping_interval) - - except ConnectionClosed: - logger.warning("Connecting: Connection closed during ping") - self.is_connected = False - break - except Exception as e: - logger.error(f"Error: Ping failed: {e}") - self.is_connected = False - break - - async def _message_loop(self): - """ - Continuous message receiving loop (like old API's websocket_listener) - """ - logger.info("Message: Starting message loop...") - - while self.should_reconnect: - try: - if self.is_connected and self.websocket: - try: - # Receive message with timeout - message = await asyncio.wait_for( - self.websocket.recv(), timeout=30.0 - ) - - self.connection_stats["total_messages_received"] += 1 - await self._process_message(message) - - except asyncio.TimeoutError: - logger.debug("Message: Message receive timeout (normal)") - continue - else: - await asyncio.sleep(1) - - except ConnectionClosed: - logger.warning("Connecting: Connection closed during message receive") - self.is_connected = False - break - except Exception as e: - logger.error(f"Error: Message loop error: {e}") - self.is_connected = False - break - - async def _health_monitor_loop(self): - """Monitor connection health and trigger reconnects if needed""" - logger.info("Health: Starting health monitor...") - - while self.should_reconnect: - try: - await asyncio.sleep(30) # Check every 30 seconds - - if not self.is_connected: - logger.warning("Health: Health check: Connection lost") - continue - - # Check if we received a pong recently - if self.connection_stats["last_ping_time"]: - time_since_ping = ( - datetime.now() - self.connection_stats["last_ping_time"] - ) - if time_since_ping > timedelta( - seconds=60 - ): # No response for 60 seconds - logger.warning( - "Health: Health check: No ping response, connection may be dead" - ) - self.is_connected = False - - # Check WebSocket state - if self.websocket and self.websocket.closed: - logger.warning("Health: Health check: WebSocket is closed") - self.is_connected = False - - except Exception as e: - logger.error(f"Error: Health monitor error: {e}") - - async def _reconnection_monitor(self): - """ - Monitor for disconnections and automatically reconnect (like old API) - """ - logger.info("Persistent: Starting reconnection monitor...") - - while self.should_reconnect: - try: - await asyncio.sleep(5) # Check every 5 seconds - - if not self.is_connected and self.should_reconnect: - logger.warning( - "Persistent: Detected disconnection, attempting reconnect..." - ) - - self.current_reconnect_attempts += 1 - self.connection_stats["total_reconnects"] += 1 - - if self.current_reconnect_attempts <= self.max_reconnect_attempts: - logger.info( - f"Persistent: Reconnection attempt {self.current_reconnect_attempts}/{self.max_reconnect_attempts}" - ) - - # Clean up current connection - if self.websocket: - try: - await self.websocket.close() - except Exception: - pass - self.websocket = None - - # Try to reconnect - success = await self._establish_connection() - - if success: - logger.success("Success: Reconnection successful!") - await self._emit_event( - "reconnected", - { - "attempt": self.current_reconnect_attempts, - "url": self.connection_info.url - if self.connection_info - else None, - }, - ) - else: - logger.error( - f"Error: Reconnection attempt {self.current_reconnect_attempts} failed" - ) - await asyncio.sleep(self.reconnect_delay) - else: - logger.error( - f"Error: Max reconnection attempts ({self.max_reconnect_attempts}) reached" - ) - await self._emit_event( - "max_reconnects_reached", - {"attempts": self.current_reconnect_attempts}, - ) - break - - except Exception as e: - logger.error(f"Error: Reconnection monitor error: {e}") - - async def _process_message(self, message): - """Process incoming messages (like old API's on_message)""" - try: - # Convert bytes to string if needed - if isinstance(message, bytes): - message = message.decode("utf-8") - - logger.debug(f"Message: Received: {message[:100]}...") - - # Handle ping-pong (like old API) - if message == "2": - await self.websocket.send("3") - self.connection_stats["last_pong_time"] = datetime.now() - logger.debug("Ping: Pong sent") - return - - # Handle authentication success (like old API) - if "successauth" in message: - logger.success("Success: Authentication successful") - await self._emit_event("authenticated", {}) - return - - # Handle other message types - await self._emit_event("message_received", {"message": message}) - - except Exception as e: - logger.error(f"Error: Error processing message: {e}") - - async def send_message(self, message: str) -> bool: - """Send message with connection check""" - try: - if self.is_connected and self.websocket: - await self.websocket.send(message) - self.connection_stats["total_messages_sent"] += 1 - logger.debug(f"Message: Sent: {message[:50]}...") - return True - else: - logger.warning("Caution: Cannot send message: not connected") - return False - except Exception as e: - logger.error(f"Error: Failed to send message: {e}") - self.is_connected = False - return False - - def add_event_handler(self, event: str, handler: Callable): - """Add event handler""" - if event not in self._event_handlers: - self._event_handlers[event] = [] - self._event_handlers[event].append(handler) - - async def _emit_event(self, event: str, data: Any): - """Emit event to handlers""" - if event in self._event_handlers: - for handler in self._event_handlers[event]: - try: - if asyncio.iscoroutinefunction(handler): - await handler(data) - else: - handler(data) - except Exception as e: - logger.error(f"Error: Error in event handler for {event}: {e}") - - def _extract_region_from_url(self, url: str) -> str: - """Extract region name from URL""" - try: - parts = url.split("//")[1].split(".")[0] - if "api-" in parts: - return parts.replace("api-", "").upper() - elif "demo" in parts: - return "DEMO" - else: - return "UNKNOWN" - except Exception: - return "UNKNOWN" - - def get_connection_stats(self) -> Dict[str, Any]: - """Get detailed connection statistics""" - return { - **self.connection_stats, - "is_connected": self.is_connected, - "current_url": self.connection_info.url if self.connection_info else None, - "current_region": self.connection_info.region - if self.connection_info - else None, - "reconnect_attempts": self.current_reconnect_attempts, - "uptime": ( - datetime.now() - self.connection_info.connected_at - if self.connection_info and self.connection_info.connected_at - else timedelta() - ), - "available_regions": len(self.available_urls), - } - - -async def demo_keep_alive(): - """Demo of the keep-alive connection manager""" - - # Example complete SSID - ssid = r'42["auth",{"session":"n1p5ah5u8t9438rbunpgrq0hlq","isDemo":1,"uid":0,"platform":1}]' - - # Create keep-alive manager - keep_alive = ConnectionKeepAlive(ssid, is_demo=True) - - # Add event handlers - async def on_connected(data): - logger.success(f"Successfully: Connected to: {data}") - - async def on_reconnected(data): - logger.success(f"Persistent: Reconnected after {data['attempt']} attempts") - - async def on_message(data): - logger.info(f"Message: Message: {data['message'][:50]}...") - - keep_alive.add_event_handler("connected", on_connected) - keep_alive.add_event_handler("reconnected", on_reconnected) - keep_alive.add_event_handler("message_received", on_message) - - try: - # Start persistent connection - success = await keep_alive.start_persistent_connection() - - if success: - logger.info( - "Starting: Keep-alive connection started, will maintain connection automatically..." - ) - - # Let it run for a while to demonstrate keep-alive - for i in range(60): # Run for 1 minute - await asyncio.sleep(1) - - # Print stats every 10 seconds - if i % 10 == 0: - stats = keep_alive.get_connection_stats() - logger.info( - f"Statistics: Stats: Connected={stats['is_connected']}, " - f"Messages sent={stats['total_messages_sent']}, " - f"Messages received={stats['total_messages_received']}, " - f"Uptime={stats['uptime']}" - ) - - # Send a test message every 30 seconds - if i % 30 == 0 and i > 0: - await keep_alive.send_message('42["test"]') - - else: - logger.error("Error: Failed to start keep-alive connection") - - finally: - # Clean shutdown - await keep_alive.stop_persistent_connection() - - -if __name__ == "__main__": - logger.info("Testing: Testing Enhanced Keep-Alive Connection Manager") - asyncio.run(demo_keep_alive()) diff --git a/comprehensive_demo.py b/demos/comprehensive_demo.py similarity index 100% rename from comprehensive_demo.py rename to demos/comprehensive_demo.py diff --git a/demo_enhanced_api.py b/demos/demo_enhanced_api.py similarity index 100% rename from demo_enhanced_api.py rename to demos/demo_enhanced_api.py diff --git a/tests/enhanced_test.py b/demos/enhanced_test.py similarity index 100% rename from tests/enhanced_test.py rename to demos/enhanced_test.py diff --git a/pocketoptionapi_async/__init__.py b/pocketoptionapi_async/__init__.py index 36d6164..bc3e914 100644 --- a/pocketoptionapi_async/__init__.py +++ b/pocketoptionapi_async/__init__.py @@ -62,7 +62,6 @@ "ConnectionStatus", "ASSETS", "REGIONS", - # Monitoring and error handling "ErrorMonitor", "HealthChecker", "ErrorSeverity", diff --git a/pocketoptionapi_async/connection_keep_alive.py b/pocketoptionapi_async/connection_keep_alive.py index a32a698..5705fd6 100644 --- a/pocketoptionapi_async/connection_keep_alive.py +++ b/pocketoptionapi_async/connection_keep_alive.py @@ -1,286 +1,554 @@ """ -Connection Keep-Alive Manager for PocketOption API +Enhanced Keep-Alive Connection Manager for PocketOption Async API """ import asyncio -import time -from collections import defaultdict -from typing import Dict, List, Callable, Optional +from typing import Optional, List, Callable, Dict, Any +from datetime import datetime, timedelta from loguru import logger +import websockets +from websockets.exceptions import ConnectionClosed + +from models import ConnectionInfo, ConnectionStatus +from constants import REGIONS class ConnectionKeepAlive: """ - Handles persistent connection with automatic keep-alive and reconnection + Advanced connection keep-alive manager based on old API patterns """ def __init__(self, ssid: str, is_demo: bool = True): - """ - Initialize connection keep-alive manager - - Args: - ssid: Session ID for authentication - is_demo: Whether this is a demo account (default: True) - """ self.ssid = ssid self.is_demo = is_demo + + # Connection state + self.websocket: Optional[websockets.WebSocketServerProtocol] = None + self.connection_info: Optional[ConnectionInfo] = None self.is_connected = False - self._websocket = None # Will store reference to websocket client - self._event_handlers: Dict[str, List[Callable]] = defaultdict(list) - self._ping_task = None - self._reconnect_task = None - self._connection_stats = { + self.should_reconnect = True + + # Background tasks + self._ping_task: Optional[asyncio.Task] = None + self._reconnect_task: Optional[asyncio.Task] = None + self._message_task: Optional[asyncio.Task] = None + self._health_task: Optional[asyncio.Task] = None + + # Keep-alive settings + self.ping_interval = 20 # seconds (same as old API) + self.reconnect_delay = 5 # seconds + self.max_reconnect_attempts = 10 + self.current_reconnect_attempts = 0 + + # Event handlers + self._event_handlers: Dict[str, List[Callable]] = {} + + # Connection pool with multiple regions + self.available_urls = ( + REGIONS.get_demo_regions() if is_demo else REGIONS.get_all() + ) + self.current_url_index = 0 + + # Statistics + self.connection_stats = { + "total_connections": 0, + "successful_connections": 0, + "total_reconnects": 0, "last_ping_time": None, - "total_reconnections": 0, - "messages_sent": 0, - "messages_received": 0, + "last_pong_time": None, + "total_messages_sent": 0, + "total_messages_received": 0, } - # Importing inside the class to avoid circular imports + logger.info( + f"Initialized keep-alive manager with {len(self.available_urls)} available regions" + ) + + async def start_persistent_connection(self) -> bool: + """ + Start a persistent connection with automatic keep-alive + Similar to old API's daemon thread approach but with modern async + """ + logger.info("Starting persistent connection with keep-alive...") + try: - from .websocket_client import AsyncWebSocketClient + # Initial connection + if await self._establish_connection(): + # Start all background tasks + await self._start_background_tasks() + logger.success( + "Success: Persistent connection established with keep-alive active" + ) + return True + else: + logger.error("Error: Failed to establish initial connection") + return False - self._websocket_client_class = AsyncWebSocketClient - except ImportError: - logger.error("Failed to import AsyncWebSocketClient") - raise ImportError("AsyncWebSocketClient module not available") + except Exception as e: + logger.error(f"Error: Error starting persistent connection: {e}") + return False + + async def stop_persistent_connection(self): + """Stop the persistent connection and all background tasks""" + logger.info("Stopping persistent connection...") + + self.should_reconnect = False + + # Cancel all background tasks + tasks = [ + self._ping_task, + self._reconnect_task, + self._message_task, + self._health_task, + ] + for task in tasks: + if task and not task.done(): + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + # Close connection + if self.websocket: + await self.websocket.close() + self.websocket = None - def add_event_handler(self, event: str, handler: Callable): - """Add event handler function""" - self._event_handlers[event].append(handler) + self.is_connected = False + logger.info("Success: Persistent connection stopped") - async def _trigger_event_async(self, event: str, *args, **kwargs): - """Trigger event handlers asynchronously""" - for handler in self._event_handlers.get(event, []): - try: - if asyncio.iscoroutinefunction(handler): - # Call async handlers directly - await handler(*args, **kwargs) - else: - # Call sync handlers directly - handler(*args, **kwargs) - except Exception as e: - logger.error(f"Error in {event} handler: {e}") + async def _establish_connection(self) -> bool: + """ + Establish connection with fallback URLs (like old API) + """ + for attempt in range(len(self.available_urls)): + url = self.available_urls[self.current_url_index] - def _trigger_event(self, event: str, *args, **kwargs): - """Trigger event handlers""" - for handler in self._event_handlers.get(event, []): try: - if asyncio.iscoroutinefunction(handler): - # Create task for async handlers - asyncio.create_task( - self._handle_async_callback(handler, args, kwargs) - ) - else: - # Call sync handlers directly - handler(*args, **kwargs) + logger.info( + f"Connecting: Attempting connection to {url} (attempt {attempt + 1})" + ) + + # SSL context (like old API) + import ssl + + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + # Connect with headers (like old API) + self.websocket = await asyncio.wait_for( + websockets.connect( + url, + ssl=ssl_context, + extra_headers={ + "Origin": "https://pocketoption.com", + "Cache-Control": "no-cache", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + }, + ping_interval=None, # We handle pings manually + ping_timeout=None, + close_timeout=10, + ), + timeout=15.0, + ) + + # Update connection info + region = self._extract_region_from_url(url) + self.connection_info = ConnectionInfo( + url=url, + region=region, + status=ConnectionStatus.CONNECTED, + connected_at=datetime.now(), + reconnect_attempts=self.current_reconnect_attempts, + ) + + self.is_connected = True + self.current_reconnect_attempts = 0 + self.connection_stats["total_connections"] += 1 + self.connection_stats["successful_connections"] += 1 + + # Send initial handshake (like old API) + await self._send_handshake() + + logger.success(f"Success: Connected to {region} region successfully") + await self._emit_event("connected", {"url": url, "region": region}) + + return True + except Exception as e: - logger.error(f"Error in {event} handler: {e}") + logger.warning(f"Caution: Failed to connect to {url}: {e}") + + # Try next URL + self.current_url_index = (self.current_url_index + 1) % len( + self.available_urls + ) + + if self.websocket: + try: + await self.websocket.close() + except Exception: + pass + self.websocket = None + + await asyncio.sleep(1) # Brief delay before next attempt + + return False - async def _handle_async_callback(self, callback, args, kwargs): - """Helper to handle async callbacks in tasks""" + async def _send_handshake(self): + """Send initial handshake sequence (like old API)""" try: - await callback(*args, **kwargs) - except Exception as e: - logger.error(f"Error in async callback: {e}") + # Wait for initial connection message + initial_message = await asyncio.wait_for( + self.websocket.recv(), timeout=10.0 + ) + logger.debug(f"Received initial: {initial_message}") - # Event forwarding methods - async def _forward_balance_data(self, data): - """Forward balance_data event from WebSocket to keep-alive handlers""" - await self._trigger_event_async("balance_data", data) + # Send handshake sequence (like old API) + await self.websocket.send("40") + await asyncio.sleep(0.1) - async def _forward_balance_updated(self, data): - """Forward balance_updated event from WebSocket to keep-alive handlers""" - await self._trigger_event_async("balance_updated", data) + # Wait for connection establishment + conn_message = await asyncio.wait_for(self.websocket.recv(), timeout=10.0) + logger.debug(f"Received connection: {conn_message}") - async def _forward_authenticated(self, data): - """Forward authenticated event from WebSocket to keep-alive handlers""" - await self._trigger_event_async("authenticated", data) + # Send SSID authentication + await self.websocket.send(self.ssid) + logger.debug("Handshake completed") - async def _forward_order_opened(self, data): - """Forward order_opened event from WebSocket to keep-alive handlers""" - await self._trigger_event_async("order_opened", data) + self.connection_stats["total_messages_sent"] += 2 - async def _forward_order_closed(self, data): - """Forward order_closed event from WebSocket to keep-alive handlers""" - await self._trigger_event_async("order_closed", data) + except Exception as e: + logger.error(f"Handshake failed: {e}") + raise - async def _forward_stream_update(self, data): - """Forward stream_update event from WebSocket to keep-alive handlers""" - await self._trigger_event_async("stream_update", data) + async def _start_background_tasks(self): + """Start all background tasks (like old API's concurrent tasks)""" + logger.info("Persistent: Starting background keep-alive tasks...") - async def _forward_json_data(self, data): - """Forward json_data event from WebSocket to keep-alive handlers""" - await self._trigger_event_async("json_data", data) + # Ping task (every 20 seconds like old API) + self._ping_task = asyncio.create_task(self._ping_loop()) - async def connect_with_keep_alive( - self, regions: Optional[List[str]] = None - ) -> bool: - """ - Connect with automatic keep-alive and reconnection + # Message receiving task + self._message_task = asyncio.create_task(self._message_loop()) - Args: - regions: List of region names to try (optional) + # Health monitoring task + self._health_task = asyncio.create_task(self._health_monitor_loop()) - Returns: - bool: Success status - """ - # Create websocket client if needed - if not self._websocket: - self._websocket = self._websocket_client_class() + # Reconnection monitoring task + self._reconnect_task = asyncio.create_task(self._reconnection_monitor()) - # Forward WebSocket events to keep-alive events - self._websocket.add_event_handler( - "balance_data", self._forward_balance_data - ) - self._websocket.add_event_handler( - "balance_updated", self._forward_balance_updated - ) - self._websocket.add_event_handler( - "authenticated", self._forward_authenticated - ) - self._websocket.add_event_handler( - "order_opened", self._forward_order_opened - ) - self._websocket.add_event_handler( - "order_closed", self._forward_order_closed - ) - self._websocket.add_event_handler( - "stream_update", self._forward_stream_update - ) - self._websocket.add_event_handler("json_data", self._forward_json_data) + logger.success("Success: All background tasks started") - # Format auth message - if self.ssid.startswith('42["auth",'): - ssid_message = self.ssid - else: - # Create basic auth message from raw session ID - ssid_message = f'42["auth", {{"ssid": "{self.ssid}", "is_demo": {str(self.is_demo).lower()}}}]' - - # Connect to WebSocket - from .constants import REGIONS - - if not regions: - # Use appropriate regions based on demo mode - if self.is_demo: - all_regions = REGIONS.get_all_regions() - demo_urls = REGIONS.get_demo_regions() - regions = [] - for name, url in all_regions.items(): - if url in demo_urls: - regions.append(name) - else: - # For live mode, use all regions except demo - all_regions = REGIONS.get_all_regions() - regions = [ - name - for name, url in all_regions.items() - if "DEMO" not in name.upper() - ] - - # Try to connect - for region_name in regions: - region_url = REGIONS.get_region(region_name) - if not region_url: - continue + async def _ping_loop(self): + """ + Continuous ping loop (like old API's send_ping function) + Sends '42["ps"]' every 20 seconds + """ + logger.info("Ping: Starting ping loop...") + while self.should_reconnect: try: - urls = [region_url] - logger.info(f"Trying to connect to {region_name} ({region_url})") - success = await self._websocket.connect(urls, ssid_message) + if self.is_connected and self.websocket: + # Send ping message (exact format from old API) + await self.websocket.send('42["ps"]') + self.connection_stats["last_ping_time"] = datetime.now() + self.connection_stats["total_messages_sent"] += 1 - if success: - logger.info(f"Connected to {region_name}") - self.is_connected = True + logger.debug("Ping: Ping sent") - # Start keep-alive - self._start_keep_alive_tasks() + await asyncio.sleep(self.ping_interval) - # Notify connection (async-aware) - await self._trigger_event_async("connected") - return True + except ConnectionClosed: + logger.warning("Connecting: Connection closed during ping") + self.is_connected = False + break except Exception as e: - logger.warning(f"Failed to connect to {region_name}: {e}") + logger.error(f"Error: Ping failed: {e}") + self.is_connected = False + break - return False + async def _message_loop(self): + """ + Continuous message receiving loop (like old API's websocket_listener) + """ + logger.info("Message: Starting message loop...") - def _start_keep_alive_tasks(self): - """Start keep-alive tasks""" - logger.info("Starting keep-alive tasks") + while self.should_reconnect: + try: + if self.is_connected and self.websocket: + try: + # Receive message with timeout + message = await asyncio.wait_for( + self.websocket.recv(), timeout=30.0 + ) + + self.connection_stats["total_messages_received"] += 1 + await self._process_message(message) + + except asyncio.TimeoutError: + logger.debug("Message: Message receive timeout (normal)") + continue + else: + await asyncio.sleep(1) - # Start ping task - if self._ping_task: - self._ping_task.cancel() - self._ping_task = asyncio.create_task(self._ping_loop()) + except ConnectionClosed: + logger.warning("Connecting: Connection closed during message receive") + self.is_connected = False + break + except Exception as e: + logger.error(f"Error: Message loop error: {e}") + self.is_connected = False + break - # Start reconnection monitor - if self._reconnect_task: - self._reconnect_task.cancel() - self._reconnect_task = asyncio.create_task(self._reconnection_monitor()) + async def _health_monitor_loop(self): + """Monitor connection health and trigger reconnects if needed""" + logger.info("Health: Starting health monitor...") - async def _ping_loop(self): - """Send periodic pings to keep connection alive""" - while self.is_connected and self._websocket: + while self.should_reconnect: try: - await self._websocket.send_message('42["ps"]') - self._connection_stats["last_ping_time"] = time.time() - self._connection_stats["messages_sent"] += 1 - await asyncio.sleep(20) # Ping every 20 seconds + await asyncio.sleep(30) # Check every 30 seconds + + if not self.is_connected: + logger.warning("Health: Health check: Connection lost") + continue + + # Check if we received a pong recently + if self.connection_stats["last_ping_time"]: + time_since_ping = ( + datetime.now() - self.connection_stats["last_ping_time"] + ) + if time_since_ping > timedelta( + seconds=60 + ): # No response for 60 seconds + logger.warning( + "Health: Health check: No ping response, connection may be dead" + ) + self.is_connected = False + + # Check WebSocket state + if self.websocket and self.websocket.closed: + logger.warning("Health: Health check: WebSocket is closed") + self.is_connected = False + except Exception as e: - logger.warning(f"Ping failed: {e}") - self.is_connected = False + logger.error(f"Error: Health monitor error: {e}") async def _reconnection_monitor(self): - """Monitor and reconnect if connection is lost""" - while True: - await asyncio.sleep(30) # Check every 30 seconds - - if ( - not self.is_connected - or not self._websocket - or not self._websocket.is_connected - ): - logger.info("Connection lost, reconnecting...") - self.is_connected = False + """ + Monitor for disconnections and automatically reconnect (like old API) + """ + logger.info("Persistent: Starting reconnection monitor...") - # Try to reconnect - success = await self.connect_with_keep_alive() + while self.should_reconnect: + try: + await asyncio.sleep(5) # Check every 5 seconds - if success: - self._connection_stats["total_reconnections"] += 1 - logger.info("Reconnection successful") - await self._trigger_event_async("reconnected") - else: - logger.error("Reconnection failed") - await asyncio.sleep(10) # Wait before next attempt + if not self.is_connected and self.should_reconnect: + logger.warning( + "Persistent: Detected disconnection, attempting reconnect..." + ) - async def disconnect(self): - """Disconnect and clean up resources""" - logger.info("Disconnecting...") + self.current_reconnect_attempts += 1 + self.connection_stats["total_reconnects"] += 1 + + if self.current_reconnect_attempts <= self.max_reconnect_attempts: + logger.info( + f"Persistent: Reconnection attempt {self.current_reconnect_attempts}/{self.max_reconnect_attempts}" + ) + + # Clean up current connection + if self.websocket: + try: + await self.websocket.close() + except Exception: + pass + self.websocket = None + + # Try to reconnect + success = await self._establish_connection() + + if success: + logger.success("Success: Reconnection successful!") + await self._emit_event( + "reconnected", + { + "attempt": self.current_reconnect_attempts, + "url": self.connection_info.url + if self.connection_info + else None, + }, + ) + else: + logger.error( + f"Error: Reconnection attempt {self.current_reconnect_attempts} failed" + ) + await asyncio.sleep(self.reconnect_delay) + else: + logger.error( + f"Error: Max reconnection attempts ({self.max_reconnect_attempts}) reached" + ) + await self._emit_event( + "max_reconnects_reached", + {"attempts": self.current_reconnect_attempts}, + ) + break - # Cancel tasks - if self._ping_task: - self._ping_task.cancel() - if self._reconnect_task: - self._reconnect_task.cancel() + except Exception as e: + logger.error(f"Error: Reconnection monitor error: {e}") - # Disconnect websocket - if self._websocket: - await self._websocket.disconnect() + async def _process_message(self, message): + """Process incoming messages (like old API's on_message)""" + try: + # Convert bytes to string if needed + if isinstance(message, bytes): + message = message.decode("utf-8") - self.is_connected = False - logger.info("Disconnected") - await self._trigger_event_async("disconnected") + logger.debug(f"Message: Received: {message[:100]}...") + + # Handle ping-pong (like old API) + if message == "2": + await self.websocket.send("3") + self.connection_stats["last_pong_time"] = datetime.now() + logger.debug("Ping: Pong sent") + return + + # Handle authentication success (like old API) + if "successauth" in message: + logger.success("Success: Authentication successful") + await self._emit_event("authenticated", {}) + return + + # Handle other message types + await self._emit_event("message_received", {"message": message}) + + except Exception as e: + logger.error(f"Error: Error processing message: {e}") + + async def send_message(self, message: str) -> bool: + """Send message with connection check""" + try: + if self.is_connected and self.websocket: + await self.websocket.send(message) + self.connection_stats["total_messages_sent"] += 1 + logger.debug(f"Message: Sent: {message[:50]}...") + return True + else: + logger.warning("Caution: Cannot send message: not connected") + return False + except Exception as e: + logger.error(f"Error: Failed to send message: {e}") + self.is_connected = False + return False + + def add_event_handler(self, event: str, handler: Callable): + """Add event handler""" + if event not in self._event_handlers: + self._event_handlers[event] = [] + self._event_handlers[event].append(handler) + + async def _emit_event(self, event: str, data: Any): + """Emit event to handlers""" + if event in self._event_handlers: + for handler in self._event_handlers[event]: + try: + if asyncio.iscoroutinefunction(handler): + await handler(data) + else: + handler(data) + except Exception as e: + logger.error(f"Error: Error in event handler for {event}: {e}") + + def _extract_region_from_url(self, url: str) -> str: + """Extract region name from URL""" + try: + parts = url.split("//")[1].split(".")[0] + if "api-" in parts: + return parts.replace("api-", "").upper() + elif "demo" in parts: + return "DEMO" + else: + return "UNKNOWN" + except Exception: + return "UNKNOWN" + + def get_connection_stats(self) -> Dict[str, Any]: + """Get detailed connection statistics""" + return { + **self.connection_stats, + "is_connected": self.is_connected, + "current_url": self.connection_info.url if self.connection_info else None, + "current_region": self.connection_info.region + if self.connection_info + else None, + "reconnect_attempts": self.current_reconnect_attempts, + "uptime": ( + datetime.now() - self.connection_info.connected_at + if self.connection_info and self.connection_info.connected_at + else timedelta() + ), + "available_regions": len(self.available_urls), + } + + +async def demo_keep_alive(): + """Demo of the keep-alive connection manager""" + + # Example complete SSID + ssid = r'42["auth",{"session":"n1p5ah5u8t9438rbunpgrq0hlq","isDemo":1,"uid":0,"platform":1}]' + + # Create keep-alive manager + keep_alive = ConnectionKeepAlive(ssid, is_demo=True) + + # Add event handlers + async def on_connected(data): + logger.success(f"Successfully: Connected to: {data}") + + async def on_reconnected(data): + logger.success(f"Persistent: Reconnected after {data['attempt']} attempts") + + async def on_message(data): + logger.info(f"Message: Message: {data['message'][:50]}...") + + keep_alive.add_event_handler("connected", on_connected) + keep_alive.add_event_handler("reconnected", on_reconnected) + keep_alive.add_event_handler("message_received", on_message) + + try: + # Start persistent connection + success = await keep_alive.start_persistent_connection() + + if success: + logger.info( + "Starting: Keep-alive connection started, will maintain connection automatically..." + ) + + # Let it run for a while to demonstrate keep-alive + for i in range(60): # Run for 1 minute + await asyncio.sleep(1) + + # Print stats every 10 seconds + if i % 10 == 0: + stats = keep_alive.get_connection_stats() + logger.info( + f"Statistics: Stats: Connected={stats['is_connected']}, " + f"Messages sent={stats['total_messages_sent']}, " + f"Messages received={stats['total_messages_received']}, " + f"Uptime={stats['uptime']}" + ) + + # Send a test message every 30 seconds + if i % 30 == 0 and i > 0: + await keep_alive.send_message('42["test"]') + + else: + logger.error("Error: Failed to start keep-alive connection") - async def send_message(self, message): - """Send WebSocket message""" - if not self.is_connected or not self._websocket: - raise ConnectionError("Not connected") + finally: + # Clean shutdown + await keep_alive.stop_persistent_connection() - await self._websocket.send_message(message) - self._connection_stats["messages_sent"] += 1 - async def on_message(self, message): - """Handle WebSocket message""" - self._connection_stats["messages_received"] += 1 - await self._trigger_event_async("message_received", message) +if __name__ == "__main__": + logger.info("Testing: Testing Enhanced Keep-Alive Connection Manager") + asyncio.run(demo_keep_alive()) diff --git a/connection_monitor.py b/pocketoptionapi_async/connection_monitor.py similarity index 99% rename from connection_monitor.py rename to pocketoptionapi_async/connection_monitor.py index e9a9d57..e3e87b6 100644 --- a/connection_monitor.py +++ b/pocketoptionapi_async/connection_monitor.py @@ -13,7 +13,7 @@ import statistics from loguru import logger -from pocketoptionapi_async.client import AsyncPocketOptionClient +from client import AsyncPocketOptionClient @dataclass diff --git a/requirements.txt b/requirements.txt index 732ce6d..c5bbe02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,9 +2,12 @@ aiohttp>=3.8.0 websockets>=11.0.0 asyncio python-dotenv>=1.0.0 -pandas>=1.5.0 tzlocal>=4.0.0 -pydantic>=2.0.0 typing-extensions>=4.0.0 -loguru>=0.7.0 rich>=13.0.0 +selenium>=4.0.0 +webdriver-manager>=4.0.0 +psutil>=5.9.0 +loguru>=0.7.2 +pydantic>=2.0.0 +pandas>=2.0.0 \ No newline at end of file diff --git a/advanced_testing_suite.py b/tests/advanced_testing_suite.py similarity index 100% rename from advanced_testing_suite.py rename to tests/advanced_testing_suite.py diff --git a/load_testing_tool.py b/tests/performance/load_testing_tool.py similarity index 100% rename from load_testing_tool.py rename to tests/performance/load_testing_tool.py diff --git a/performance_tests.py b/tests/performance/performance_tests.py similarity index 100% rename from performance_tests.py rename to tests/performance/performance_tests.py diff --git a/tests/test_complete_ssid.py b/tests/test_complete_ssid.py index 58a7400..ef00316 100644 --- a/tests/test_complete_ssid.py +++ b/tests/test_complete_ssid.py @@ -86,7 +86,7 @@ async def test_complete_ssid_format(): async def test_real_connection(): """Test with real SSID if available""" - print("\n🌐 Testing Real Connection (Optional)") + print("\nTesting Real Connection (Optional)") print("=" * 40) # Check for real SSID in environment diff --git a/tests/test_demo_live_connection.py b/tests/test_demo_live_connection.py index ac6f72b..7926074 100644 --- a/tests/test_demo_live_connection.py +++ b/tests/test_demo_live_connection.py @@ -12,7 +12,7 @@ async def test_demo_live_connection(): # Test SSID with demo=1 hardcoded (should be overridden by is_demo parameter) demo_ssid = r'42["auth",{"session":"n1p5ah5u8t9438rbunpgrq0hlq","isDemo":1,"uid":72645361,"platform":1,"isFastHistory":true}]' - print("🌐 Testing Demo/Live Connection Fix") + print("Testing Demo/Live Connection Fix") print("=" * 50) # Test 1: Demo mode connection (should connect to demo regions) @@ -28,7 +28,7 @@ async def test_demo_live_connection(): if success: print(" Connected successfully!") if hasattr(client_demo, "connection_info") and client_demo.connection_info: - print(f" 🌐 Connected to: {client_demo.connection_info.region}") + print(f" Connected to: {client_demo.connection_info.region}") await client_demo.disconnect() else: print(" Connection failed") @@ -51,7 +51,7 @@ async def test_demo_live_connection(): if success: print(" Connected successfully!") if hasattr(client_live, "connection_info") and client_live.connection_info: - print(f" 🌐 Connected to: {client_live.connection_info.region}") + print(f" Connected to: {client_live.connection_info.region}") await client_live.disconnect() else: print(" Connection failed") diff --git a/tests/test_fixed_connection.py b/tests/test_fixed_connection.py index 357a3d4..5e9bdb2 100644 --- a/tests/test_fixed_connection.py +++ b/tests/test_fixed_connection.py @@ -18,7 +18,7 @@ async def test_connection_fix(): """Test the fixed connection with proper handshake sequence""" - print("šŸ”§ Testing Fixed Connection Issue") + print("Testing Fixed Connection Issue") print("=" * 60) # Test with complete SSID format (like from browser) @@ -53,7 +53,7 @@ async def test_connection_fix(): print(" CONNECTION SUCCESSFUL!") print(f"šŸ“Š Connection info: {client.connection_info}") print( - f"🌐 Connected to: {client.connection_info.region if client.connection_info else 'Unknown'}" + f"Connected to: {client.connection_info.region if client.connection_info else 'Unknown'}" ) # Test basic functionality @@ -116,7 +116,7 @@ async def test_old_vs_new_comparison(): print(" 5. Wait for authentication response") print() - print("šŸ”§ Key Fixes Applied:") + print("Key Fixes Applied:") print(" Proper message sequence waiting (like old API)") print(" Handshake completion before background tasks") print(" Authentication event handling") @@ -143,7 +143,7 @@ async def main(): print( "šŸ“ The new async API now follows the same handshake pattern as the old API" ) - print("šŸ”§ Key improvements:") + print("Key improvements:") print(" • Proper server response waiting") print(" • Sequential handshake messages") print(" • Authentication event handling") diff --git a/tests/test_new_api.py b/tests/test_new_api.py index d505419..436c3ca 100644 --- a/tests/test_new_api.py +++ b/tests/test_new_api.py @@ -162,7 +162,7 @@ def test_api_structure(): async def test_context_manager(): """Test async context manager functionality""" - print("\nšŸ”§ Testing context manager...") + print("\nTesting context manager...") session_id = "n1p5ah5u8t9438rbunpgrq0hlq" diff --git a/tests/test_persistent_connection.py b/tests/test_persistent_connection.py index c1ed967..6d32f14 100644 --- a/tests/test_persistent_connection.py +++ b/tests/test_persistent_connection.py @@ -41,7 +41,7 @@ async def test_persistent_connection(): print() # Test 1: Regular connection (existing behavior) - print("šŸ”§ Test 1: Regular Connection (with basic keep-alive)") + print("Test 1: Regular Connection (with basic keep-alive)") print("-" * 50) try: @@ -172,7 +172,7 @@ def on_authenticated(data): print() # Test 3: Connection resilience simulation - print("šŸ”§ Test 3: Connection Resilience Simulation") + print("Test 3: Connection Resilience Simulation") print("-" * 50) print("This would test automatic reconnection when connection drops") print("(Requires real SSID for full testing)") diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..3af88e5 --- /dev/null +++ b/todo.md @@ -0,0 +1,7 @@ +# TO-DO List + +### Add Template for a basic PO bot + - Soon- Preferably before July 4th. + +### Integrate more fallbacks if there is any errors + - No ETA \ No newline at end of file diff --git a/tests/client_test.py b/tools/client_test.py similarity index 83% rename from tests/client_test.py rename to tools/client_test.py index 451b058..213c94a 100644 --- a/tests/client_test.py +++ b/tools/client_test.py @@ -11,10 +11,9 @@ async def websocket_client(url, pro): print(f"Trying {i}...") try: async with websockets.connect( - i, # teoria de los issues + i, extra_headers={ - # "Origin": "https://pocket-link19.co", - "Origin": "https://po.trade/" + "Origin": "https://pocketoption.com/" # main URL }, ) as websocket: async for message in websocket: @@ -42,17 +41,17 @@ async def pro(message, websocket, url): # await websocket.send(data) if message.startswith('0{"sid":"'): - print(f"{url.split('/')[2]} got 0 sid send 40 ") + print(f"{url.split('/')[2]} got 0 sid, sending 40 ") await websocket.send("40") elif message == "2": # ping-pong thing - print(f"{url.split('/')[2]} got 2 send 3") + print(f"{url.split('/')[2]} got 2, sending 3") await websocket.send("3") if message.startswith('40{"sid":"'): - print(f"{url.split('/')[2]} got 40 sid send session") + print(f"{url.split('/')[2]} got 40 sid, sending session") await websocket.send(SESSION) - print("message sent! We are logged in!!!") + print("Message sent! Logged in successfully.") async def main(): diff --git a/SSID/driver.py b/tools/driver.py similarity index 100% rename from SSID/driver.py rename to tools/driver.py diff --git a/SSID/get_ssid.py b/tools/get_ssid.py similarity index 100% rename from SSID/get_ssid.py rename to tools/get_ssid.py