diff --git a/scratchattach/__init__.py b/scratchattach/__init__.py index 2da282d2..3e96d7b4 100644 --- a/scratchattach/__init__.py +++ b/scratchattach/__init__.py @@ -15,7 +15,7 @@ from .site.backpack_asset import BackpackAsset from .site.comment import Comment from .site.cloud_activity import CloudActivity -from .site.forum import ForumPost, ForumTopic, get_topic, get_topic_list +from .site.forum import ForumPost, ForumTopic, get_topic, get_topic_list, youtube_link_to_scratch from .site.project import Project, get_project, search_projects, explore_projects from .site.session import Session, login, login_by_id, login_by_session_string from .site.studio import Studio, get_studio, search_studios, explore_studios diff --git a/scratchattach/other/other_apis.py b/scratchattach/other/other_apis.py index a53746d0..df8d4235 100644 --- a/scratchattach/other/other_apis.py +++ b/scratchattach/other/other_apis.py @@ -101,3 +101,17 @@ def aprilfools_get_counter() -> int: def aprilfools_increment_counter() -> int: return requests.post("https://api.scratch.mit.edu/surprise").json()["surprise"] + +# --- Resources --- +def get_resource_urls(): + return requests.get("https://resources.scratch.mit.edu/localized-urls.json").json() + +# --- Misc --- +# I'm not sure what to label this as +def scratch_team_members() -> dict: + # Unfortunately, the only place to find this is a js file, not a json file, which is annoying + text = requests.get("https://scratch.mit.edu/js/credits.bundle.js").text + text = "[{\"userName\"" + text.split("JSON.parse('[{\"userName\"")[1] + text = text.split("\"}]')")[0] + "\"}]" + + return json.loads(text) diff --git a/scratchattach/site/forum.py b/scratchattach/site/forum.py index 7da145e6..030ff47c 100644 --- a/scratchattach/site/forum.py +++ b/scratchattach/site/forum.py @@ -6,6 +6,7 @@ from ._base import BaseSiteComponent import xml.etree.ElementTree as ET from bs4 import BeautifulSoup +from urllib.parse import urlparse, parse_qs from ..utils.requests import Requests as requests @@ -383,3 +384,16 @@ def get_topic_list(category_id, *, page=1): except Exception as e: raise exceptions.ScrapeError(str(e)) + +def youtube_link_to_scratch(link: str): + """ + Converts a YouTube url (in multiple formats) like https://youtu.be/1JTgg4WVAX8?si=fIEskaEaOIRZyTAz + to a link like https://scratch.mit.edu/discuss/youtube/1JTgg4WVAX8 + """ + url_parse = urlparse(link) + query_parse = parse_qs(url_parse.query) + if 'v' in query_parse: + video_id = query_parse['v'][0] + else: + video_id = url_parse.path.split('/')[-1] + return f"https://scratch.mit.edu/discuss/youtube/{video_id}" diff --git a/scratchattach/site/project.py b/scratchattach/site/project.py index 6c318886..7df79743 100644 --- a/scratchattach/site/project.py +++ b/scratchattach/site/project.py @@ -100,6 +100,14 @@ def _update_from_dict(self, data): return False return True + @property + def embed_url(self): + """ + Returns: + the url of the embed of the project + """ + return f"{self.url}/embed" + def remixes(self, *, limit=40, offset=0): """ Returns: diff --git a/scratchattach/site/session.py b/scratchattach/site/session.py index 3769ce99..f9bafbfd 100644 --- a/scratchattach/site/session.py +++ b/scratchattach/site/session.py @@ -85,16 +85,28 @@ def __init__(self, **entries): } def _update_from_dict(self, data): + # Note: there are a lot more things you can get from this data dict. + # Maybe it would be a good idea to also store the dict itself? + # self.data = data + self.xtoken = data['user']['token'] self._headers["X-Token"] = self.xtoken + + self.has_outstanding_email_confirmation = data["flags"]["has_outstanding_email_confirmation"] + self.email = data["user"]["email"] + self.new_scratcher = data["permissions"]["new_scratcher"] self.mute_status = data["permissions"]["mute_status"] + self.username = data["user"]["username"] self._username = data["user"]["username"] self.banned = data["user"]["banned"] + if self.banned: warnings.warn(f"Warning: The account {self._username} you logged in to is BANNED. Some features may not work properly.") + if self.has_outstanding_email_confirmation: + warnings.warn(f"Warning: The account {self._username} you logged is not email confirmed. Some features may not work properly.") return True def connect_linked_user(self): @@ -115,6 +127,90 @@ def get_linked_user(self): # backwards compatibility with v1 return self.connect_linked_user() # To avoid inconsistencies with "connect" and "get", this function was renamed + def set_country(self, country: str="Antarctica"): + requests.post("https://scratch.mit.edu/accounts/settings/", + data={"country": country}, + headers=self._headers, cookies=self._cookies) + + def change_password(self, old_password: str, new_password: str = None): + if new_password is None or new_password == old_password: + return + requests.post("https://scratch.mit.edu/accounts/password_change/", + data={"old_password": old_password, + "new_password1": new_password, + "new_password2": new_password}, + headers=self._headers, cookies=self._cookies) + + def resend_email(self, password: str): + """ + Sends a request to resend a confirmation email for this session's account + + Keyword arguments: + password (str): Password associated with the session (not stored) + """ + requests.post("https://scratch.mit.edu/accounts/email_change/", + data={"email_address": self.new_email_address, + "password": password}, + headers=self._headers, cookies=self._cookies) + + def change_email(self, new_email: str, password: str): + """ + Sends a request to change the email of this session + + Keyword arguments: + new_email (str): The email you want to switch to + password (str): Password associated with the session (not stored) + """ + requests.post("https://scratch.mit.edu/accounts/email_change/", + data={"email_address": new_email, + "password": password}, + headers=self._headers, cookies=self._cookies) + + @property + def new_email_address(self) -> str | None: + """ + Gets the (unconfirmed) email address that this session has requested to transfer to, if any, otherwise the current address. + + Returns: + str: The email that this session wants to switch to + """ + response = requests.get("https://scratch.mit.edu/accounts/email_change/", + headers=self._headers, cookies=self._cookies) + + soup = BeautifulSoup(response.content, "html.parser") + + email = None + for label_span in soup.find_all("span", {"class": "label"}): + if label_span.contents[0] == "New Email Address": + return label_span.parent.contents[-1].text.strip("\n ") + elif label_span.contents[0] == "Current Email Address": + email = label_span.parent.contents[-1].text.strip("\n ") + + return email + + def delete_account(self, *, password: str, delete_projects: bool = False): + """ + !!! Dangerous !!! + Sends a request to delete the account that is associated with this session. + You can cancel the deletion simply by logging back in (including using sa.login(username, password)) + + Keyword arguments: + password (str): The password associated with the account + delete_projects (bool): Whether to delete all the projects as well + """ + requests.post("https://scratch.mit.edu/accounts/settings/delete_account/", + data={ + "delete_state": "delbyusrwproj" if delete_projects else "delbyusr", + "password": password + }, headers=self._headers, cookies=self._cookies) + + 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 + """ + requests.post("https://scratch.mit.edu/accounts/logout/", + headers=self._headers, cookies=self._cookies) + def messages(self, *, limit=40, offset=0, date_limit=None, filter_by=None): ''' Returns the messages. @@ -255,7 +351,7 @@ def connect_pb_from_file(path_to_file): pb = project_json_capabilities.ProjectBody() pb.from_json(project_json_capabilities._load_sb3_file(path_to_file)) return pb - + def download_asset(asset_id_with_file_ext, *, filename=None, dir=""): if not (dir.endswith("/") or dir.endswith("\\")): dir = dir+"/" @@ -461,7 +557,7 @@ def mystuff_studios(self, filter_arg="all", *, page=1, sort_by="", descending=Tr except Exception: raise(exceptions.FetchError) - + def backpack(self,limit=20, offset=0): ''' Lists the assets that are in the backpack of the user associated with the session. @@ -474,7 +570,7 @@ def backpack(self,limit=20, offset=0): limit = limit, offset = offset, headers = self._headers ) return commons.parse_object_list(data, backpack_asset.BackpackAsset, self) - + def delete_from_backpack(self, backpack_asset_id): ''' Deletes an asset from the backpack. @@ -786,7 +882,7 @@ def login(username, password, *, timeout=10) -> Session: except Exception: raise exceptions.LoginFailure( "Either the provided authentication data is wrong or your network is banned from Scratch.\n\nIf you're using an online IDE (like replit.com) Scratch possibly banned its IP adress. In this case, try logging in with your session id: https://github.com/TimMcCool/scratchattach/wiki#logging-in") - + # Create session object: return login_by_id(session_id, username=username, password=password)