diff --git a/README.md b/README.md index 23f283b..5ba69b3 100644 --- a/README.md +++ b/README.md @@ -330,22 +330,33 @@ In order for the cloud socket to work, you'll need to add the sprite from this [ ## Using a cloud socket -Once you have created a cloud socket you have to start it using `scratchcommunication.cloud_socket.CloudSocket.listen` +Once you have created a cloud socket you have to start it using `scratchcommunication.cloud_socket.CloudSocket.listen` and you can also put it in a with statement, which makes the cloud socket shut down automatically when the code is done executing. ```python cloud_socket.listen() + +# OR + +with cloud_socket.listen(): + ... # Your code here ``` + + After you start the cloud socket you can wait for a new user using `scratchcommunication.cloud_socket.CloudSocket.accept` ```python -client, client_username = cloud_socket.accept() +client, client_username = cloud_socket.accept( + timeout = 10 # (Optional) +) ``` When you have a client, you can send messages to them and receive messages. ```python -msg = client.recv() +msg = client.recv( + timeout = 10 # (Optional) +) client.send("Hello!") ``` diff --git a/scratchcommunication/__init__.py b/scratchcommunication/__init__.py index 8602a5c..4b711de 100644 --- a/scratchcommunication/__init__.py +++ b/scratchcommunication/__init__.py @@ -2,7 +2,7 @@ Module for communicating with scratch projects. """ -__version_number__ = '2.10.2' +__version_number__ = '2.10.3' from .session import * from .cloud import * diff --git a/scratchcommunication/cloud.py b/scratchcommunication/cloud.py index 7e39c21..4a33455 100644 --- a/scratchcommunication/cloud.py +++ b/scratchcommunication/cloud.py @@ -5,7 +5,7 @@ from .exceptions import QuickAccessDisabledError, NotSupported, ErrorInEventHandler, StopException, EventExpiredError import scratchcommunication from func_timeout import StoppableThread -import json, time, requests, warnings, traceback, secrets, sys +import json, time, requests, warnings, traceback, secrets, sys, ssl from websocket import WebSocket, WebSocketConnectionClosedException, WebSocketTimeoutException NoneType = type(None) @@ -90,6 +90,7 @@ class CloudConnection: processed_events : list[Event] keep_all_events : bool supports_cloud_logs : bool + allow_no_certificate : bool def __init__( self, *, @@ -102,8 +103,11 @@ def __init__( warning_type : type[Warning] = ErrorInEventHandler, daemon_thread : bool = False, connect : bool = True, - keep_all_events : bool = False + keep_all_events : bool = False, + allow_no_certificate : bool = False ): + self.websocket = None + self.allow_no_certificate = allow_no_certificate self.supports_cloud_logs = True self.keep_all_events = keep_all_events self.event_order = {} @@ -124,14 +128,14 @@ def __init__( self.data_reception = None if not connect: return - self._connect() + self._handle_connect() if not self.receive_from_websocket: while True: try: self.receive_new_data() return except Exception: - self._connect() + self._handle_connect() self.data_reception = StoppableThread(target=self.receive_data, daemon=daemon_thread) self.data_reception.start() @@ -148,7 +152,7 @@ def stop_thread(self): self.thread_running = False self.data_reception.stop(StopException, 0.1) self.data_reception.join(5) - + def enable_quickaccess(self): """ Use for enabling the use of the object as a lookup table. @@ -161,27 +165,39 @@ def disable_quickaccess(self): """ self.quickaccess = False - def _connect(self, *, retry : int = 10): + def _connect(self, no_cert : bool = False): + self.websocket = WebSocket(sslopt=({"cert_reqs": ssl.CERT_NONE} if no_cert else None)) + self.websocket.connect( + "wss://clouddata.scratch.mit.edu", + cookie="scratchsessionsid=" + self.session.session_id + ";", + origin="https://scratch.mit.edu", + enable_multithread=True, + timeout=5 + ) + + def _handle_connect(self, *, retry : int = 3) -> Union[Exception, None]: """ Don't use this. """ try: - self.websocket = WebSocket() - self.websocket.connect( - "wss://clouddata.scratch.mit.edu", - cookie="scratchsessionsid=" + self.session.session_id + ";", - origin="https://scratch.mit.edu", - enable_multithread=True, - timeout=5 - ) + self._connect() self.handshake() self.emit_event("connect", timestamp=time.time()) + except ssl.SSLCertVerificationError: + if not self.allow_no_certificate: + return ConnectionError( + "The SSL certificate could not be verified. Add allow_no_certificate=True to allow a connection without a certificate." + ) + warnings.warn("Connecting without a certificate.", RuntimeWarning) + self._connect(no_cert=True) except Exception as e: if retry == 1: - raise ConnectionError( - f"There was an error while connecting to the cloud server." - ) from e - self._connect(retry=retry - 1) + return ConnectionError( + "There was an error while connecting to the cloud server." + ) + exc = self._handle_connect(retry=retry - 1) + if exc: + raise exc from e def handshake(self): self.send_packet( @@ -270,7 +286,7 @@ def _set_variable( raise ConnectionError( "There was an error while setting the cloud variable." ) from e - self._connect() + self._handle_connect() self._set_variable(name=name, value=value, retry=retry - 1) def set_variable( @@ -360,7 +376,7 @@ def receive_new_data(self, first : bool = False) -> dict: self.values[i["var"]] = i["value"] return self.values - def _prepare_connection(self): + def _prepare_handle_connection(self): while self.thread_running: try: self.receive_new_data(first=True) @@ -368,21 +384,21 @@ def _prepare_connection(self): except WebSocketTimeoutException: pass except WebSocketConnectionClosedException: - self._connect() + self._handle_connect() def receive_data(self): """ Use for receiving cloud data. """ - self._prepare_connection() + self._prepare_handle_connection() while self.thread_running: try: self.receive_new_data() except WebSocketTimeoutException: pass except WebSocketConnectionClosedException: - self._connect() - self._prepare_connection() + self._handle_connect() + self._prepare_handle_connection() def get_age_of_event(self, event : Event) -> int: """ @@ -478,7 +494,8 @@ def __init__( cloud_host : str = "wss://clouddata.turbowarp.org/", accept_strs : bool = False, keep_all_events : bool = False, - contact_info : str + contact_info : str, + allow_no_certificate : bool = False ): super().__init__( project_id=project_id, @@ -490,7 +507,8 @@ def __init__( warning_type=warning_type, daemon_thread=daemon_thread, connect=False, - keep_all_events=keep_all_events + keep_all_events=keep_all_events, + allow_no_certificate=allow_no_certificate ) self.supports_cloud_logs = False self.contact_info = contact_info or ((f"@{session.username} on scratch" if session else "Anonymous") if username == "player1000" else f"@{username} on scratch") @@ -498,31 +516,20 @@ def __init__( self.user_agent = f"scratchcommunication/{scratchcommunication.__version_number__} - {self.contact_info}" self.cloud_host = cloud_host self.accept_strs = accept_strs - self._connect() + self._handle_connect() if not self.receive_from_websocket: while True: try: self.receive_new_data() return except Exception: - self._connect() + self._handle_connect() self.data_reception = StoppableThread(target=self.receive_data, daemon=daemon_thread) self.data_reception.start() - - def _connect(self, *, cloud_host = None, retry : int = 10): - try: - if cloud_host is not None: - self.cloud_host = cloud_host - self.websocket = WebSocket() - self.websocket.connect(self.cloud_host, enable_multithread=True, timeout=5, header={"User-Agent": self.user_agent}) - self.handshake() - self.emit_event("connect") - except Exception as e: - if retry == 1: - raise ConnectionError( - f"There was an error while connecting to the cloud server." - ) from e - self._connect(cloud_host=cloud_host, retry=retry - 1) + + def _connect(self, no_cert : bool = False): + self.websocket = WebSocket(sslopt=({"cert_reqs": ssl.CERT_NONE} if no_cert else None)) + self.websocket.connect(self.cloud_host, enable_multithread=True, timeout=5, header={"User-Agent": self.user_agent}) @staticmethod def get_cloud_logs(*args, **kwargs): diff --git a/scratchcommunication/cloud_socket.py b/scratchcommunication/cloud_socket.py index 20cdf10..3786948 100644 --- a/scratchcommunication/cloud_socket.py +++ b/scratchcommunication/cloud_socket.py @@ -264,6 +264,7 @@ def on_packet(event): return except AssertionError: pass + return self def _decrypt_key(self, key : str) -> int: if isinstance(self.security, sec.ECSecurity): @@ -326,7 +327,7 @@ def accept(self, timeout : Union[float, None] = 10) -> tuple[BaseCloudSocketConn new_client = self.new_clients.pop(0) return new_client except IndexError: - raise TimeoutError("The timeout expired (consider setting timeout=None)") + raise TimeoutError("The timeout expired (consider setting timeout=None)") from None finally: self.accepting.release() diff --git a/setup.py b/setup.py index 0bb72a9..eaa76dd 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ with open("README.md", encoding="utf-8") as f: long_description = f.read() -VERSION = '2.10.2' +VERSION = '2.10.3' setup( name='scratchcommunication', diff --git a/tests/bugtest1.py b/tests/bugtest1.py new file mode 100644 index 0000000..839cb51 --- /dev/null +++ b/tests/bugtest1.py @@ -0,0 +1,29 @@ +import sys, os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +import scratchcommunication# +from dotenv import load_dotenv + +load_dotenv() + +USERNAME, PASSWORD = os.getenv("SCRATCH_USERNAME"), os.getenv("SCRATCH_PASSWORD") +PROJECT_ID = os.getenv("PROJECT_ID") + + +session = scratchcommunication.Session.login(USERNAME, PASSWORD) + +security = scratchcommunication.security.Security.from_string(os.getenv("SCRATCH_SECURITY")) + +cloud_socket = session.create_cloud_socket( + project_id = "884190099", + security = security +) + +with cloud_socket.listen(): + while True: + try: + client, client_username = cloud_socket.accept() + except TimeoutError: + continue + print(client_username + " connected!") + + client.send("Hello client!") \ No newline at end of file