Skip to content

Commit c3ff8d9

Browse files
authored
Merge pull request #273 from FAReTek1/main
Settings abilities + small features
2 parents 5665909 + 97bdc86 commit c3ff8d9

5 files changed

Lines changed: 137 additions & 5 deletions

File tree

scratchattach/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from .site.backpack_asset import BackpackAsset
1616
from .site.comment import Comment
1717
from .site.cloud_activity import CloudActivity
18-
from .site.forum import ForumPost, ForumTopic, get_topic, get_topic_list
18+
from .site.forum import ForumPost, ForumTopic, get_topic, get_topic_list, youtube_link_to_scratch
1919
from .site.project import Project, get_project, search_projects, explore_projects
2020
from .site.session import Session, login, login_by_id, login_by_session_string
2121
from .site.studio import Studio, get_studio, search_studios, explore_studios

scratchattach/other/other_apis.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,17 @@ def aprilfools_get_counter() -> int:
101101

102102
def aprilfools_increment_counter() -> int:
103103
return requests.post("https://api.scratch.mit.edu/surprise").json()["surprise"]
104+
105+
# --- Resources ---
106+
def get_resource_urls():
107+
return requests.get("https://resources.scratch.mit.edu/localized-urls.json").json()
108+
109+
# --- Misc ---
110+
# I'm not sure what to label this as
111+
def scratch_team_members() -> dict:
112+
# Unfortunately, the only place to find this is a js file, not a json file, which is annoying
113+
text = requests.get("https://scratch.mit.edu/js/credits.bundle.js").text
114+
text = "[{\"userName\"" + text.split("JSON.parse('[{\"userName\"")[1]
115+
text = text.split("\"}]')")[0] + "\"}]"
116+
117+
return json.loads(text)

scratchattach/site/forum.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ._base import BaseSiteComponent
77
import xml.etree.ElementTree as ET
88
from bs4 import BeautifulSoup
9+
from urllib.parse import urlparse, parse_qs
910

1011
from ..utils.requests import Requests as requests
1112

@@ -383,3 +384,16 @@ def get_topic_list(category_id, *, page=1):
383384
except Exception as e:
384385
raise exceptions.ScrapeError(str(e))
385386

387+
388+
def youtube_link_to_scratch(link: str):
389+
"""
390+
Converts a YouTube url (in multiple formats) like https://youtu.be/1JTgg4WVAX8?si=fIEskaEaOIRZyTAz
391+
to a link like https://scratch.mit.edu/discuss/youtube/1JTgg4WVAX8
392+
"""
393+
url_parse = urlparse(link)
394+
query_parse = parse_qs(url_parse.query)
395+
if 'v' in query_parse:
396+
video_id = query_parse['v'][0]
397+
else:
398+
video_id = url_parse.path.split('/')[-1]
399+
return f"https://scratch.mit.edu/discuss/youtube/{video_id}"

scratchattach/site/project.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@ def _update_from_dict(self, data):
100100
return False
101101
return True
102102

103+
@property
104+
def embed_url(self):
105+
"""
106+
Returns:
107+
the url of the embed of the project
108+
"""
109+
return f"{self.url}/embed"
110+
103111
def remixes(self, *, limit=40, offset=0):
104112
"""
105113
Returns:

scratchattach/site/session.py

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,28 @@ def __init__(self, **entries):
8585
}
8686

8787
def _update_from_dict(self, data):
88+
# Note: there are a lot more things you can get from this data dict.
89+
# Maybe it would be a good idea to also store the dict itself?
90+
# self.data = data
91+
8892
self.xtoken = data['user']['token']
8993
self._headers["X-Token"] = self.xtoken
94+
95+
self.has_outstanding_email_confirmation = data["flags"]["has_outstanding_email_confirmation"]
96+
9097
self.email = data["user"]["email"]
98+
9199
self.new_scratcher = data["permissions"]["new_scratcher"]
92100
self.mute_status = data["permissions"]["mute_status"]
101+
93102
self.username = data["user"]["username"]
94103
self._username = data["user"]["username"]
95104
self.banned = data["user"]["banned"]
105+
96106
if self.banned:
97107
warnings.warn(f"Warning: The account {self._username} you logged in to is BANNED. Some features may not work properly.")
108+
if self.has_outstanding_email_confirmation:
109+
warnings.warn(f"Warning: The account {self._username} you logged is not email confirmed. Some features may not work properly.")
98110
return True
99111

100112
def connect_linked_user(self) -> 'user.User':
@@ -115,6 +127,90 @@ def get_linked_user(self):
115127
# backwards compatibility with v1
116128
return self.connect_linked_user() # To avoid inconsistencies with "connect" and "get", this function was renamed
117129

130+
def set_country(self, country: str="Antarctica"):
131+
requests.post("https://scratch.mit.edu/accounts/settings/",
132+
data={"country": country},
133+
headers=self._headers, cookies=self._cookies)
134+
135+
def change_password(self, old_password: str, new_password: str = None):
136+
if new_password is None or new_password == old_password:
137+
return
138+
requests.post("https://scratch.mit.edu/accounts/password_change/",
139+
data={"old_password": old_password,
140+
"new_password1": new_password,
141+
"new_password2": new_password},
142+
headers=self._headers, cookies=self._cookies)
143+
144+
def resend_email(self, password: str):
145+
"""
146+
Sends a request to resend a confirmation email for this session's account
147+
148+
Keyword arguments:
149+
password (str): Password associated with the session (not stored)
150+
"""
151+
requests.post("https://scratch.mit.edu/accounts/email_change/",
152+
data={"email_address": self.new_email_address,
153+
"password": password},
154+
headers=self._headers, cookies=self._cookies)
155+
156+
def change_email(self, new_email: str, password: str):
157+
"""
158+
Sends a request to change the email of this session
159+
160+
Keyword arguments:
161+
new_email (str): The email you want to switch to
162+
password (str): Password associated with the session (not stored)
163+
"""
164+
requests.post("https://scratch.mit.edu/accounts/email_change/",
165+
data={"email_address": new_email,
166+
"password": password},
167+
headers=self._headers, cookies=self._cookies)
168+
169+
@property
170+
def new_email_address(self) -> str | None:
171+
"""
172+
Gets the (unconfirmed) email address that this session has requested to transfer to, if any, otherwise the current address.
173+
174+
Returns:
175+
str: The email that this session wants to switch to
176+
"""
177+
response = requests.get("https://scratch.mit.edu/accounts/email_change/",
178+
headers=self._headers, cookies=self._cookies)
179+
180+
soup = BeautifulSoup(response.content, "html.parser")
181+
182+
email = None
183+
for label_span in soup.find_all("span", {"class": "label"}):
184+
if label_span.contents[0] == "New Email Address":
185+
return label_span.parent.contents[-1].text.strip("\n ")
186+
elif label_span.contents[0] == "Current Email Address":
187+
email = label_span.parent.contents[-1].text.strip("\n ")
188+
189+
return email
190+
191+
def delete_account(self, *, password: str, delete_projects: bool = False):
192+
"""
193+
!!! Dangerous !!!
194+
Sends a request to delete the account that is associated with this session.
195+
You can cancel the deletion simply by logging back in (including using sa.login(username, password))
196+
197+
Keyword arguments:
198+
password (str): The password associated with the account
199+
delete_projects (bool): Whether to delete all the projects as well
200+
"""
201+
requests.post("https://scratch.mit.edu/accounts/settings/delete_account/",
202+
data={
203+
"delete_state": "delbyusrwproj" if delete_projects else "delbyusr",
204+
"password": password
205+
}, headers=self._headers, cookies=self._cookies)
206+
207+
def logout(self):
208+
"""
209+
Sends a logout request to scratch. Might not do anything, might log out this account on other ips/sessions? I am not sure
210+
"""
211+
requests.post("https://scratch.mit.edu/accounts/logout/",
212+
headers=self._headers, cookies=self._cookies)
213+
118214
def messages(self, *, limit=40, offset=0, date_limit=None, filter_by=None):
119215
'''
120216
Returns the messages.
@@ -255,7 +351,7 @@ def connect_pb_from_file(path_to_file) -> 'project_json_capabilities.ProjectBody
255351
pb = project_json_capabilities.ProjectBody()
256352
pb.from_json(project_json_capabilities._load_sb3_file(path_to_file))
257353
return pb
258-
354+
259355
def download_asset(asset_id_with_file_ext, *, filename=None, dir=""):
260356
if not (dir.endswith("/") or dir.endswith("\\")):
261357
dir = dir+"/"
@@ -463,7 +559,7 @@ def mystuff_studios(self, filter_arg="all", *, page=1, sort_by="", descending=Tr
463559
except Exception:
464560
raise(exceptions.FetchError)
465561

466-
562+
467563
def backpack(self,limit=20, offset=0):
468564
'''
469565
Lists the assets that are in the backpack of the user associated with the session.
@@ -476,7 +572,7 @@ def backpack(self,limit=20, offset=0):
476572
limit = limit, offset = offset, headers = self._headers
477573
)
478574
return commons.parse_object_list(data, backpack_asset.BackpackAsset, self)
479-
575+
480576
def delete_from_backpack(self, backpack_asset_id):
481577
'''
482578
Deletes an asset from the backpack.
@@ -788,7 +884,7 @@ def login(username, password, *, timeout=10) -> Session:
788884
except Exception:
789885
raise exceptions.LoginFailure(
790886
"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")
791-
887+
792888
# Create session object:
793889
return login_by_id(session_id, username=username, password=password)
794890

0 commit comments

Comments
 (0)