Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added create_studio + did a little with exceptions #280

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions scratchattach/cloud/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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}
Expand Down
4 changes: 2 additions & 2 deletions scratchattach/eventhandlers/cloud_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 40 additions & 5 deletions scratchattach/site/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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):
Expand Down
23 changes: 17 additions & 6 deletions scratchattach/utils/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
"""
Expand All @@ -43,6 +43,7 @@ class XTokenError(Exception):

pass


# Not found errors:

class UserNotFound(Exception):
Expand All @@ -60,6 +61,7 @@ class ProjectNotFound(Exception):

pass


class ClassroomNotFound(Exception):
"""
Raised when a non-existent Classroom is requested.
Expand All @@ -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):
Expand All @@ -95,13 +100,15 @@ 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.
"""

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.
Expand All @@ -117,29 +124,33 @@ class Response429(Exception):

pass


class CommentPostFailure(Exception):
"""
Raised when a comment fails to post. This can have various reasons.
"""

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.
"""

pass


# Cloud / encoding errors:

class ConnectionError(Exception):
class CloudConnectionError(Exception):
"""
Raised when connecting to Scratch's cloud server fails. This can have various reasons.
"""
Expand Down Expand Up @@ -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
pass
4 changes: 2 additions & 2 deletions scratchattach/utils/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down