Skip to content

Commit 439a12a

Browse files
authored
Merge branch 'TimMcCool:main' into tw-debug-compliance
2 parents 958b3d4 + 169b3bf commit 439a12a

8 files changed

Lines changed: 318 additions & 254 deletions

File tree

.github/workflows/deploy-website.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
commit_message: ${{ github.event.head_commit.message }}
3636

3737
- name: Set up Python 3.12
38-
uses: actions/setup-python@v6
38+
uses: actions/setup-python@v7
3939
with:
4040
python-version: "3.12"
4141

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[build-system]
2-
requires = ["setuptools >= 77.0.3"]
2+
requires = ["setuptools>=83.0.0"]
33
build-backend = "setuptools.build_meta"
44

55
[project]
@@ -102,4 +102,4 @@ where = ["."]
102102
include = ["scratchattach*"]
103103

104104
[dependency-groups]
105-
dev = ["ruff>=0.15.12"]
105+
dev = ["ruff>=0.15.22"]

scratchattach/eventhandlers/cloud_requests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,9 @@ def on_set(self, activity: cloud_activity.CloudActivity):
331331
# Parsing the received request
332332
raw_request, request_id = activity_value.split(".")
333333

334-
if len(request_id) == 9 and request_id[-1] == "9":
334+
if len(request_id) == 7 and request_id[-1] == "9":
335335
# A lost packet was re-requested
336-
self._request_packet_from_memory(request_id[:-1], int(raw_request))
336+
self._request_packet_from_memory(request_id[:-1], int(raw_request) + 1)
337337
return
338338

339339
if request_id in self.responded_request_ids:

scratchattach/site/user.py

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ class _OcularStatus(TypedDict):
5858

5959
class Verificator:
6060
def __init__(self, user: User, project_id: int):
61-
self.project = user._make_linked_object("id", project_id, project.Project, exceptions.ProjectNotFound)
61+
self.project = user._make_linked_object(
62+
"id", project_id, project.Project, exceptions.ProjectNotFound
63+
)
6264
self.projecturl = self.project.url
6365
self.code = "".join(random.choices(string.ascii_letters + string.digits, k=8))
6466
self.username = user.username
@@ -69,7 +71,11 @@ def check(self) -> bool:
6971
filter(
7072
lambda x: (
7173
x.author_name == self.username
72-
and (x.content == self.code or x.content.startswith(self.code) or x.content.endswith(self.code))
74+
and (
75+
x.content == self.code
76+
or x.content.startswith(self.code)
77+
or x.content.endswith(self.code)
78+
)
7379
),
7480
self.project.comments(),
7581
)
@@ -109,9 +115,11 @@ class User(BaseSiteComponent[typed_dicts.UserDict]):
109115
icon_url: str = field(kw_only=True, default="")
110116
id: int = field(kw_only=True, default=0)
111117
scratchteam: bool = field(kw_only=True, repr=False, default=False)
112-
is_member: bool = field(kw_only=True, repr=False, default=False)
113-
has_ears: bool = field(kw_only=True, repr=False, default=False)
114-
_classroom: tuple[bool, Optional[classroom.Classroom]] = field(init=False, default=(False, None))
118+
_is_member: bool = field(kw_only=True, repr=False, default=False)
119+
_has_ears: bool = field(kw_only=True, repr=False, default=False)
120+
_classroom: tuple[bool, Optional[classroom.Classroom]] = field(
121+
init=False, default=(False, None)
122+
)
115123
_headers: dict[str, str] = field(init=False, default_factory=headers.copy)
116124
_cookies: dict[str, str] = field(init=False, default_factory=dict)
117125
_json_headers: dict[str, str] = field(init=False, default_factory=dict)
@@ -137,6 +145,29 @@ def icon(self) -> bytes:
137145
def name(self) -> str:
138146
return self.username
139147

148+
@property
149+
@deprecated("memberships are over")
150+
def is_member(self) -> bool:
151+
return self._is_member
152+
153+
@is_member.setter
154+
@deprecated("memberships are over")
155+
def is_member(self, value: bool):
156+
"deprecated: memberships are over"
157+
self._is_member = value
158+
159+
@property
160+
@deprecated("memberships are over")
161+
def has_ears(self) -> bool:
162+
"deprecated: memberships are over"
163+
return self._has_ears
164+
165+
@has_ears.setter
166+
@deprecated("memberships are over")
167+
def has_ears(self, value: bool):
168+
"deprecated: memberships are over"
169+
self._has_ears = value
170+
140171
def __post_init__(self):
141172

142173
# Info on how the .update method has to fetch the data:
@@ -178,7 +209,9 @@ def _update_from_dict(self, data: Union[dict, typed_dicts.UserDict]):
178209
def _assert_permission(self):
179210
self._assert_auth()
180211
if self._session.username != self.username:
181-
raise exceptions.Unauthorized("You need to be authenticated as the profile owner to do this.")
212+
raise exceptions.Unauthorized(
213+
"You need to be authenticated as the profile owner to do this."
214+
)
182215

183216
@property
184217
def url(self):
@@ -266,7 +299,9 @@ def classroom(self) -> classroom.Classroom | None:
266299
href = str(a.get("href"))
267300
if re.match(r"/classes/\d*/", href):
268301
class_name = a.text.strip()[len("Student of: ") :]
269-
is_closed = bool(re.search(r"\n *\(ended\)", class_name)) # as this has a \n, we can be sure
302+
is_closed = bool(
303+
re.search(r"\n *\(ended\)", class_name)
304+
) # as this has a \n, we can be sure
270305
if is_closed:
271306
class_name = re.sub(r"\n *\(ended\)", "", class_name).strip()
272307

@@ -294,7 +329,9 @@ def does_exist(self) -> Optional[bool]:
294329
boolean : True if the user exists, False if the user is deleted, None if an error occured
295330
"""
296331
with requests.no_error_handling():
297-
status_code = requests.get(f"https://scratch.mit.edu/users/{self.username}/").status_code
332+
status_code = requests.get(
333+
f"https://scratch.mit.edu/users/{self.username}/"
334+
).status_code
298335
if status_code == 200:
299336
return True
300337
elif status_code == 404:
@@ -335,13 +372,16 @@ def featured_data(self):
335372
dict: Gets info on the user's featured project and featured label (like "Featured project", "My favorite things", etc.)
336373
"""
337374
try:
338-
response = requests.get(f"https://scratch.mit.edu/site-api/users/all/{self.username}/").json()
375+
response = requests.get(
376+
f"https://scratch.mit.edu/site-api/users/all/{self.username}/"
377+
).json()
339378
return {
340379
"label": response["featured_project_label_name"],
341380
"project": dict(
342381
id=str(response["featured_project_data"]["id"]),
343382
author=response["featured_project_data"]["creator"],
344-
thumbnail_url="https://" + response["featured_project_data"]["thumbnail_url"][2:],
383+
thumbnail_url="https://"
384+
+ response["featured_project_data"]["thumbnail_url"][2:],
345385
title=response["featured_project_data"]["title"],
346386
),
347387
}
@@ -371,7 +411,11 @@ def unfollowers(self) -> list[User]:
371411
# api response contains all-time followers, including deleted and unfollowed
372412
unfollowers = []
373413
for offset in range(0, follower_count, 40):
374-
unfollowers.extend(user for user in self.followers(offset=offset, limit=40) if user.username not in usernames)
414+
unfollowers.extend(
415+
user
416+
for user in self.followers(offset=offset, limit=40)
417+
if user.username not in usernames
418+
)
375419

376420
return unfollowers
377421

@@ -629,7 +673,9 @@ def loves(self, *, limit=40, offset=0, get_full_project: bool = False) -> list[p
629673
break
630674

631675
# Each project element is a list item with the class name 'project thumb item' so we can just use that
632-
for i, project_element in enumerate(soup.find_all("li", {"class": "project thumb item"})):
676+
for i, project_element in enumerate(
677+
soup.find_all("li", {"class": "project thumb item"})
678+
):
633679
# Remember we only want certain projects:
634680
# The current project idx = first_idx + i
635681
# We want to start at {offset} and end at {offset + limit}
@@ -656,7 +702,9 @@ def loves(self, *, limit=40, offset=0, get_full_project: bool = False) -> list[p
656702
assert isinstance(first_anchor, Tag)
657703
assert isinstance(second_anchor, Tag)
658704
assert isinstance(third_anchor, Tag)
659-
project_id = commons.webscrape_count(first_anchor.attrs["href"], "/projects/", "/")
705+
project_id = commons.webscrape_count(
706+
first_anchor.attrs["href"], "/projects/", "/"
707+
)
660708
title = second_anchor.text
661709
author = third_anchor.contents[0]
662710

@@ -715,10 +763,12 @@ def favorites_count(self):
715763
).text
716764
return commons.webscrape_count(text, "Favorites (", ")")
717765

766+
@deprecated("memberships are over")
718767
def has_badge(self) -> bool:
719768
"""
720769
Returns:
721770
bool: Whether the user has a scratch membership badge on their profile (located next to the follow button)
771+
deprecated: memberships are over
722772
"""
723773
with requests.no_error_handling():
724774
resp = requests.get(self.url)
@@ -876,7 +926,9 @@ def post_comment(self, content, *, parent_id="", commentee_id=""):
876926
try:
877927
data = {
878928
"id": text.split('<div id="comments-')[1].split('" class="comment')[0],
879-
"author": {"username": text.split('" data-comment-user="')[1].split('"><img class')[0]},
929+
"author": {
930+
"username": text.split('" data-comment-user="')[1].split('"><img class')[0]
931+
},
880932
"content": text.split('<div class="content">')[1].split("</div>")[0].strip(),
881933
"reply_count": 0,
882934
"cached_replies": [],
@@ -900,11 +952,15 @@ def post_comment(self, content, *, parent_id="", commentee_id=""):
900952
)
901953
) from e
902954
elif '<script id="error-data" type="application/json">' in text:
903-
raw_error_data = text.split('<script id="error-data" type="application/json">')[1].split("</script>")[0]
955+
raw_error_data = text.split('<script id="error-data" type="application/json">')[
956+
1
957+
].split("</script>")[0]
904958
error_data = json.loads(raw_error_data)
905959
expires = error_data["mute_status"]["muteExpiresAt"]
906960
expires = datetime.fromtimestamp(expires, timezone.utc)
907-
raise (exceptions.CommentPostFailure(f"You have been muted. Mute expires on {expires}")) from e
961+
raise (
962+
exceptions.CommentPostFailure(f"You have been muted. Mute expires on {expires}")
963+
) from e
908964
else:
909965
raise (exceptions.FetchError(f"Couldn't parse API response: {r.text!r}")) from e
910966

@@ -933,7 +989,9 @@ def activity(self, *, limit=1000):
933989
"""
934990
with requests.no_error_handling():
935991
soup = BeautifulSoup(
936-
requests.get(f"https://scratch.mit.edu/messages/ajax/user-activity/?user={self.username}&max={limit}").text,
992+
requests.get(
993+
f"https://scratch.mit.edu/messages/ajax/user-activity/?user={self.username}&max={limit}"
994+
).text,
937995
"html.parser",
938996
)
939997

@@ -953,7 +1011,9 @@ def activity_html(self, *, limit=1000):
9531011
str: The raw user activity HTML data
9541012
"""
9551013
with requests.no_error_handling():
956-
return requests.get(f"https://scratch.mit.edu/messages/ajax/user-activity/?user={self.username}&max={limit}").text
1014+
return requests.get(
1015+
f"https://scratch.mit.edu/messages/ajax/user-activity/?user={self.username}&max={limit}"
1016+
).text
9571017

9581018
def follow(self):
9591019
"""
@@ -1021,7 +1081,9 @@ def comments(self, *, page=1) -> list[comment.Comment] | None:
10211081
data = []
10221082

10231083
with requests.no_error_handling():
1024-
resp = requests.get(f"https://scratch.mit.edu/site-api/comments/user/{self.username}/?page={page}")
1084+
resp = requests.get(
1085+
f"https://scratch.mit.edu/site-api/comments/user/{self.username}/?page={page}"
1086+
)
10251087

10261088
if resp.status_code == 404:
10271089
# Profile comments seem to end at page 67, and afterwards give 404.
@@ -1137,7 +1199,9 @@ def stats(self):
11371199
dict: A dict containing the user's stats. If the stats aren't available, all values will be -1.
11381200
"""
11391201
try:
1140-
stats = requests.get(f"https://scratchdb.lefty.one/v3/user/info/{self.username}").json()["statistics"]
1202+
stats = requests.get(
1203+
f"https://scratchdb.lefty.one/v3/user/info/{self.username}"
1204+
).json()["statistics"]
11411205
stats.pop("ranks")
11421206
except Exception:
11431207
stats = {
@@ -1162,7 +1226,9 @@ def ranks(self):
11621226
dict: A dict containing the user's ranks. If the ranks aren't available, all values will be -1.
11631227
"""
11641228
try:
1165-
return requests.get(f"https://scratchdb.lefty.one/v3/user/info/{self.username}").json()["statistics"]["ranks"]
1229+
return requests.get(f"https://scratchdb.lefty.one/v3/user/info/{self.username}").json()[
1230+
"statistics"
1231+
]["ranks"]
11661232
except Exception:
11671233
return {
11681234
"country": {

tests/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ dependencies = [
99
"cryptography>=49.0.0",
1010
"pytest>=9.1.1",
1111
"python-dotenv>=1.2.2",
12-
"ruff>=0.15.19",
12+
"ruff>=0.15.22",
1313
"scratchattach",
1414
]
1515

1616
[build-system]
17-
requires = ["uv_build>=0.11.24,<0.12.0"]
17+
requires = ["uv_build>=0.11.30,<0.12.0"]
1818
build-backend = "uv_build"
1919

2020
[tool.uv]

tests/test_memberships.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
warnings.filterwarnings("ignore", category=sa.UserAuthenticationWarning)
77

88

9-
def test_memberships():
10-
if allow_before(datetime(2026, 6, 28)):
11-
return
9+
def test_memberships():
1210
# NOTE: these are subject to change
1311
member_users = [
1412
sa.get_user("-KittyMax-"),
@@ -23,14 +21,14 @@ def test_memberships():
2321
sa.get_user("Boss_1s")
2422
]
2523

26-
assert all(user.is_member for user in member_users)
27-
assert all(not user.is_member for user in nomember_users)
24+
# assert all(user.is_member for user in member_users)
25+
# assert all(not user.is_member for user in nomember_users)
2826

29-
assert any(user.has_ears for user in member_users)
30-
assert all(not user.has_ears for user in nomember_users)
27+
# assert any(user.has_ears for user in member_users)
28+
# assert all(not user.has_ears for user in nomember_users)
3129

32-
assert any(user.has_badge() for user in member_users)
33-
assert all(not user.has_badge() for user in nomember_users)
30+
# assert any(user.has_badge() for user in member_users)
31+
# assert all(not user.has_badge() for user in nomember_users)
3432

3533

3634
if __name__ == "__main__":

0 commit comments

Comments
 (0)