diff --git a/scratchattach/cloud/_base.py b/scratchattach/cloud/_base.py index f3076ed7..fbbe323b 100644 --- a/scratchattach/cloud/_base.py +++ b/scratchattach/cloud/_base.py @@ -100,7 +100,7 @@ def _send_packet(self, packet): self.websocket.send(json.dumps(packet) + "\n") except Exception: self.active_connection = False - raise exceptions.ConnectionError(f"Sending packet failed three times in a row: {packet}") + raise exceptions.CloudConnectionError(f"Sending packet failed three times in a row: {packet}") def _send_packet_list(self, packet_list): packet_string = "".join([json.dumps(packet) + "\n" for packet in packet_list]) @@ -126,7 +126,7 @@ def _send_packet_list(self, packet_list): self.websocket.send(packet_string) except Exception: self.active_connection = False - raise exceptions.ConnectionError(f"Sending packet list failed four times in a row: {packet_list}") + raise exceptions.CloudConnectionError(f"Sending packet list failed four times in a row: {packet_list}") def _handshake(self): packet = {"method": "handshake", "user": self.username, "project_id": self.project_id} diff --git a/scratchattach/eventhandlers/cloud_requests.py b/scratchattach/eventhandlers/cloud_requests.py index d3fcfa9b..10121cd8 100644 --- a/scratchattach/eventhandlers/cloud_requests.py +++ b/scratchattach/eventhandlers/cloud_requests.py @@ -167,8 +167,8 @@ def _parse_output(self, request_name, output, request_id): def _set_FROM_HOST_var(self, value): try: self.cloud.set_var(f"FROM_HOST_{self.used_cloud_vars[self.current_var]}", value) - except exceptions.ConnectionError: - self.call_even("on_disconnect") + except exceptions.CloudConnectionError: + self.call_event("on_disconnect") except Exception as e: print("scratchattach: internal error while responding (please submit a bug report on GitHub):", e) self.current_var += 1 diff --git a/scratchattach/site/session.py b/scratchattach/site/session.py index c13dc3a7..19220768 100644 --- a/scratchattach/site/session.py +++ b/scratchattach/site/session.py @@ -24,15 +24,16 @@ from ..eventhandlers import message_events, filterbot from . import activity from ._base import BaseSiteComponent -from ..utils.commons import headers, empty_project_json +from ..utils.commons import headers, empty_project_json, webscrape_count from bs4 import BeautifulSoup from ..other import project_json_capabilities from ..utils.requests import Requests as requests CREATE_PROJECT_USES = [] +CREATE_STUDIO_USES = [] -class Session(BaseSiteComponent): +class Session(BaseSiteComponent): ''' Represents a Scratch log in / session. Stores authentication data (session id and xtoken). @@ -164,7 +165,7 @@ def new_email_address(self) -> str | None: email = label_span.parent.contents[-1].text.strip("\n ") return email - + def logout(self): """ Sends a logout request to scratch. Might not do anything, might log out this account on other ips/sessions? I am not sure @@ -402,10 +403,9 @@ def explore_studios(self, *, query="", mode="trending", language="en", limit=40, f"https://api.scratch.mit.edu/explore/studios", limit=limit, offset=offset, add_params=f"&language={language}&mode={mode}&q={query}") return commons.parse_object_list(response, studio.Studio, self) - # --- Create project API --- - def create_project(self, *, title=None, project_json=empty_project_json, parent_id=None): # not working + def create_project(self, *, title=None, project_json=empty_project_json, parent_id=None): # not working """ Creates a project on the Scratch website. @@ -436,6 +436,41 @@ def create_project(self, *, title=None, project_json=empty_project_json, parent_ response = requests.post('https://projects.scratch.mit.edu/', params=params, cookies=self._cookies, headers=self._headers, json=project_json).json() return self.connect_project(response["content-name"]) + def create_studio(self, *, title=None, description: str = None): + """ + Create a studio on the scratch website + + Warning: + Don't spam this method - it WILL get you banned from Scratch. + To prevent accidental spam, a rate limit (5 studios per minute) is implemented for this function. + """ + global CREATE_STUDIO_USES + if len(CREATE_STUDIO_USES) < 5: + CREATE_STUDIO_USES.insert(0, time.time()) + else: + if CREATE_STUDIO_USES[-1] < time.time() - 300: + CREATE_STUDIO_USES.pop() + else: + raise exceptions.BadRequest("Rate limit for creating Scratch studios exceeded.\nThis rate limit is enforced by scratchattach, not by the Scratch API.\nFor security reasons, it cannot be turned off.\n\nDon't spam-create studios, it WILL get you banned.") + return + CREATE_STUDIO_USES.insert(0, time.time()) + + if self.new_scratcher: + raise exceptions.Unauthorized(f"\nNew scratchers (like {self.username}) cannot create studios.") + + response = requests.post("https://scratch.mit.edu/studios/create/", + cookies=self._cookies, headers=self._headers) + + studio_id = webscrape_count(response.json()["redirect"], "/studios/", "/") + new_studio = self.connect_studio(studio_id) + + if title is not None: + new_studio.set_title(title) + if description is not None: + new_studio.set_description(description) + + return new_studio + # --- My stuff page --- def mystuff_projects(self, filter_arg="all", *, page=1, sort_by="", descending=True): diff --git a/scratchattach/utils/exceptions.py b/scratchattach/utils/exceptions.py index 10b756e8..8167a2f2 100644 --- a/scratchattach/utils/exceptions.py +++ b/scratchattach/utils/exceptions.py @@ -18,7 +18,6 @@ class Unauthenticated(Exception): def __init__(self, message=""): self.message = "No login / session connected.\n\nThe object on which the method was called was created using scratchattach.get_xyz()\nUse session.connect_xyz() instead (xyz is a placeholder for user / project / cloud / ...).\n\nMore information: https://scratchattach.readthedocs.io/en/latest/scratchattach.html#scratchattach.utils.exceptions.Unauthenticated" super().__init__(self.message) - pass class Unauthorized(Exception): @@ -29,10 +28,11 @@ class Unauthorized(Exception): """ def __init__(self, message=""): - self.message = "The user corresponding to the connected login / session is not allowed to perform this action." + self.message = ( + f"The user corresponding to the connected login / session is not allowed to perform this action. " + f"{message}") super().__init__(self.message) - pass class XTokenError(Exception): """ @@ -43,6 +43,7 @@ class XTokenError(Exception): pass + # Not found errors: class UserNotFound(Exception): @@ -60,6 +61,7 @@ class ProjectNotFound(Exception): pass + class ClassroomNotFound(Exception): """ Raised when a non-existent Classroom is requested. @@ -75,15 +77,18 @@ class StudioNotFound(Exception): pass + class ForumContentNotFound(Exception): """ Raised when a non-existent forum topic / post is requested. """ pass + class CommentNotFound(Exception): pass + # API errors: class LoginFailure(Exception): @@ -95,6 +100,7 @@ class LoginFailure(Exception): pass + class FetchError(Exception): """ Raised when getting information from the Scratch API fails. This can have various reasons. Make sure all provided arguments are valid. @@ -102,6 +108,7 @@ class FetchError(Exception): pass + class BadRequest(Exception): """ Raised when the Scratch API responds with a "Bad Request" error message. This can have various reasons. Make sure all provided arguments are valid. @@ -117,6 +124,7 @@ class Response429(Exception): pass + class CommentPostFailure(Exception): """ Raised when a comment fails to post. This can have various reasons. @@ -124,12 +132,14 @@ class CommentPostFailure(Exception): pass + class APIError(Exception): """ For API errors that can't be classified into one of the above errors """ pass + class ScrapeError(Exception): """ Raised when something goes wrong while web-scraping a page with bs4. @@ -137,9 +147,10 @@ class ScrapeError(Exception): pass + # Cloud / encoding errors: -class ConnectionError(Exception): +class CloudConnectionError(Exception): """ Raised when connecting to Scratch's cloud server fails. This can have various reasons. """ @@ -172,12 +183,12 @@ class RequestNotFound(Exception): pass + # Websocket server errors: class WebsocketServerError(Exception): - """ Raised when the self-hosted cloud websocket server fails to start. """ - pass \ No newline at end of file + pass diff --git a/scratchattach/utils/requests.py b/scratchattach/utils/requests.py index 1c90a749..951bab42 100644 --- a/scratchattach/utils/requests.py +++ b/scratchattach/utils/requests.py @@ -9,9 +9,9 @@ class Requests: """ @staticmethod - def check_response(r : requests.Response): + def check_response(r: requests.Response): if r.status_code == 403 or r.status_code == 401: - raise exceptions.Unauthorized + raise exceptions.Unauthorized(f"Request content: {r.content}") if r.status_code == 500: raise exceptions.APIError("Internal Scratch server error") if r.status_code == 429: