scratchattach 2.1.12__py3-none-any.whl → 2.1.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scratchattach/cloud/_base.py +12 -8
- scratchattach/cloud/cloud.py +19 -7
- scratchattach/editor/asset.py +59 -5
- scratchattach/editor/base.py +82 -31
- scratchattach/editor/block.py +87 -15
- scratchattach/editor/blockshape.py +8 -4
- scratchattach/editor/build_defaulting.py +6 -2
- scratchattach/editor/code_translation/__init__.py +0 -0
- scratchattach/editor/code_translation/parse.py +177 -0
- scratchattach/editor/comment.py +6 -0
- scratchattach/editor/commons.py +82 -19
- scratchattach/editor/extension.py +10 -3
- scratchattach/editor/field.py +9 -0
- scratchattach/editor/inputs.py +4 -1
- scratchattach/editor/meta.py +11 -3
- scratchattach/editor/monitor.py +46 -38
- scratchattach/editor/mutation.py +11 -4
- scratchattach/editor/pallete.py +24 -25
- scratchattach/editor/prim.py +2 -2
- scratchattach/editor/project.py +9 -3
- scratchattach/editor/sprite.py +19 -6
- scratchattach/editor/twconfig.py +2 -1
- scratchattach/editor/vlb.py +1 -1
- scratchattach/eventhandlers/_base.py +3 -3
- scratchattach/eventhandlers/cloud_events.py +2 -2
- scratchattach/eventhandlers/cloud_requests.py +4 -7
- scratchattach/eventhandlers/cloud_server.py +3 -3
- scratchattach/eventhandlers/combine.py +2 -2
- scratchattach/eventhandlers/message_events.py +1 -1
- scratchattach/other/other_apis.py +4 -4
- scratchattach/other/project_json_capabilities.py +3 -3
- scratchattach/site/_base.py +13 -12
- scratchattach/site/activity.py +11 -43
- scratchattach/site/alert.py +227 -0
- scratchattach/site/backpack_asset.py +2 -2
- scratchattach/site/browser_cookie3_stub.py +17 -0
- scratchattach/site/browser_cookies.py +27 -21
- scratchattach/site/classroom.py +51 -34
- scratchattach/site/cloud_activity.py +4 -4
- scratchattach/site/comment.py +30 -8
- scratchattach/site/forum.py +101 -69
- scratchattach/site/project.py +37 -17
- scratchattach/site/session.py +177 -83
- scratchattach/site/studio.py +4 -4
- scratchattach/site/user.py +184 -62
- scratchattach/utils/commons.py +35 -23
- scratchattach/utils/enums.py +44 -5
- scratchattach/utils/exceptions.py +10 -0
- scratchattach/utils/requests.py +57 -31
- {scratchattach-2.1.12.dist-info → scratchattach-2.1.14.dist-info}/METADATA +9 -3
- scratchattach-2.1.14.dist-info/RECORD +66 -0
- {scratchattach-2.1.12.dist-info → scratchattach-2.1.14.dist-info}/WHEEL +1 -1
- scratchattach/editor/sbuild.py +0 -2837
- scratchattach-2.1.12.dist-info/RECORD +0 -63
- {scratchattach-2.1.12.dist-info → scratchattach-2.1.14.dist-info}/licenses/LICENSE +0 -0
- {scratchattach-2.1.12.dist-info → scratchattach-2.1.14.dist-info}/top_level.txt +0 -0
scratchattach/site/user.py
CHANGED
|
@@ -3,21 +3,37 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import random
|
|
6
|
+
import re
|
|
6
7
|
import string
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from enum import Enum
|
|
10
|
+
import warnings
|
|
11
|
+
|
|
12
|
+
from typing_extensions import deprecated
|
|
13
|
+
from bs4 import BeautifulSoup, Tag
|
|
14
|
+
|
|
15
|
+
from ._base import BaseSiteComponent
|
|
16
|
+
from scratchattach.eventhandlers import message_events
|
|
17
|
+
|
|
18
|
+
from scratchattach.utils import commons
|
|
19
|
+
from scratchattach.utils import exceptions
|
|
20
|
+
from scratchattach.utils.commons import headers
|
|
21
|
+
from scratchattach.utils.requests import requests
|
|
7
22
|
|
|
8
|
-
from ..eventhandlers import message_events
|
|
9
23
|
from . import project
|
|
10
|
-
from ..utils import exceptions
|
|
11
24
|
from . import studio
|
|
12
25
|
from . import forum
|
|
13
|
-
from bs4 import BeautifulSoup
|
|
14
|
-
from ._base import BaseSiteComponent
|
|
15
|
-
from ..utils.commons import headers
|
|
16
|
-
from ..utils import commons
|
|
17
26
|
from . import comment
|
|
18
27
|
from . import activity
|
|
28
|
+
from . import classroom
|
|
19
29
|
|
|
20
|
-
|
|
30
|
+
class Rank(Enum):
|
|
31
|
+
"""
|
|
32
|
+
Possible ranks in scratch
|
|
33
|
+
"""
|
|
34
|
+
NEW_SCRATCHER = 0
|
|
35
|
+
SCRATCHER = 1
|
|
36
|
+
SCRATCH_TEAM = 2
|
|
21
37
|
|
|
22
38
|
class Verificator:
|
|
23
39
|
|
|
@@ -61,7 +77,7 @@ class User(BaseSiteComponent):
|
|
|
61
77
|
|
|
62
78
|
# Info on how the .update method has to fetch the data:
|
|
63
79
|
self.update_function = requests.get
|
|
64
|
-
self.
|
|
80
|
+
self.update_api = f"https://api.scratch.mit.edu/users/{entries['username']}"
|
|
65
81
|
|
|
66
82
|
# Set attributes every User object needs to have:
|
|
67
83
|
self._session = None
|
|
@@ -69,6 +85,10 @@ class User(BaseSiteComponent):
|
|
|
69
85
|
self.username = None
|
|
70
86
|
self.name = None
|
|
71
87
|
|
|
88
|
+
# cache value for classroom getter method (using @property)
|
|
89
|
+
# first value is whether the cache has actually been set (because it can be None), second is the value itself
|
|
90
|
+
self._classroom: tuple[bool, classroom.Classroom | None] = False, None
|
|
91
|
+
|
|
72
92
|
# Update attributes from entries dict:
|
|
73
93
|
entries.setdefault("name", entries.get("username"))
|
|
74
94
|
self.__dict__.update(entries)
|
|
@@ -119,27 +139,73 @@ class User(BaseSiteComponent):
|
|
|
119
139
|
raise exceptions.Unauthorized(
|
|
120
140
|
"You need to be authenticated as the profile owner to do this.")
|
|
121
141
|
|
|
142
|
+
@property
|
|
143
|
+
def classroom(self) -> classroom.Classroom | None:
|
|
144
|
+
"""
|
|
145
|
+
Get a user's associated classroom, and return it as a `scratchattach.classroom.Classroom` object.
|
|
146
|
+
If there is no associated classroom, returns `None`
|
|
147
|
+
"""
|
|
148
|
+
if not self._classroom[0]:
|
|
149
|
+
with requests.no_error_handling():
|
|
150
|
+
resp = requests.get(f"https://scratch.mit.edu/users/{self.username}/")
|
|
151
|
+
soup = BeautifulSoup(resp.text, "html.parser")
|
|
152
|
+
|
|
153
|
+
details = soup.find("p", {"class": "profile-details"})
|
|
154
|
+
assert isinstance(details, Tag)
|
|
155
|
+
|
|
156
|
+
class_name, class_id, is_closed = None, 0, False
|
|
157
|
+
for a in details.find_all("a"):
|
|
158
|
+
if not isinstance(a, Tag):
|
|
159
|
+
continue
|
|
160
|
+
href = str(a.get("href"))
|
|
161
|
+
if re.match(r"/classes/\d*/", href):
|
|
162
|
+
class_name = a.text.strip()[len("Student of: "):]
|
|
163
|
+
is_closed = class_name.endswith("\n (ended)") # as this has a \n, we can be sure
|
|
164
|
+
if is_closed:
|
|
165
|
+
class_name = class_name[:-7].strip()
|
|
166
|
+
|
|
167
|
+
class_id = int(href.split('/')[2])
|
|
168
|
+
break
|
|
169
|
+
|
|
170
|
+
if class_name:
|
|
171
|
+
self._classroom = True, classroom.Classroom(
|
|
172
|
+
_session=self._session,
|
|
173
|
+
id=class_id,
|
|
174
|
+
title=class_name,
|
|
175
|
+
is_closed=is_closed
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
self._classroom = True, None
|
|
179
|
+
|
|
180
|
+
return self._classroom[1]
|
|
181
|
+
|
|
122
182
|
def does_exist(self):
|
|
123
183
|
"""
|
|
124
184
|
Returns:
|
|
125
185
|
boolean : True if the user exists, False if the user is deleted, None if an error occured
|
|
126
186
|
"""
|
|
127
|
-
|
|
187
|
+
with requests.no_error_handling():
|
|
188
|
+
status_code = requests.get(f"https://scratch.mit.edu/users/{self.username}/").status_code
|
|
128
189
|
if status_code == 200:
|
|
129
190
|
return True
|
|
130
191
|
elif status_code == 404:
|
|
131
192
|
return False
|
|
132
193
|
|
|
194
|
+
# Will maybe be deprecated later, but for now still has its own purpose.
|
|
195
|
+
#@deprecated("This function is partially deprecated. Use user.rank() instead.")
|
|
133
196
|
def is_new_scratcher(self):
|
|
134
197
|
"""
|
|
135
198
|
Returns:
|
|
136
199
|
boolean : True if the user has the New Scratcher status, else False
|
|
137
200
|
"""
|
|
138
201
|
try:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
202
|
+
with requests.no_error_handling():
|
|
203
|
+
res = requests.get(f"https://scratch.mit.edu/users/{self.username}/").text
|
|
204
|
+
group = res[res.rindex('<span class="group">'):][:70]
|
|
205
|
+
return "new scratcher" in group.lower()
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
warnings.warn(f"Caught exception {e=}")
|
|
143
209
|
return None
|
|
144
210
|
|
|
145
211
|
def message_count(self):
|
|
@@ -168,19 +234,21 @@ class User(BaseSiteComponent):
|
|
|
168
234
|
|
|
169
235
|
def follower_count(self):
|
|
170
236
|
# follower count
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
237
|
+
with requests.no_error_handling():
|
|
238
|
+
text = requests.get(
|
|
239
|
+
f"https://scratch.mit.edu/users/{self.username}/followers/",
|
|
240
|
+
headers = self._headers
|
|
241
|
+
).text
|
|
242
|
+
return commons.webscrape_count(text, "Followers (", ")")
|
|
176
243
|
|
|
177
244
|
def following_count(self):
|
|
178
245
|
# following count
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
246
|
+
with requests.no_error_handling():
|
|
247
|
+
text = requests.get(
|
|
248
|
+
f"https://scratch.mit.edu/users/{self.username}/following/",
|
|
249
|
+
headers = self._headers
|
|
250
|
+
).text
|
|
251
|
+
return commons.webscrape_count(text, "Following (", ")")
|
|
184
252
|
|
|
185
253
|
def followers(self, *, limit=40, offset=0):
|
|
186
254
|
"""
|
|
@@ -214,7 +282,7 @@ class User(BaseSiteComponent):
|
|
|
214
282
|
"""
|
|
215
283
|
return [i.name for i in self.following(limit=limit, offset=offset)]
|
|
216
284
|
|
|
217
|
-
def is_following(self, user):
|
|
285
|
+
def is_following(self, user: str):
|
|
218
286
|
"""
|
|
219
287
|
Returns:
|
|
220
288
|
boolean: Whether the user is following the user provided as argument
|
|
@@ -228,11 +296,11 @@ class User(BaseSiteComponent):
|
|
|
228
296
|
if user in following_names:
|
|
229
297
|
following = True
|
|
230
298
|
break
|
|
231
|
-
if following_names
|
|
299
|
+
if not following_names:
|
|
232
300
|
break
|
|
233
301
|
offset += 20
|
|
234
|
-
except Exception:
|
|
235
|
-
print("Warning: API error when performing following check")
|
|
302
|
+
except Exception as e:
|
|
303
|
+
print(f"Warning: API error when performing following check: {e=}")
|
|
236
304
|
return following
|
|
237
305
|
return following
|
|
238
306
|
|
|
@@ -241,28 +309,46 @@ class User(BaseSiteComponent):
|
|
|
241
309
|
Returns:
|
|
242
310
|
boolean: Whether the user is followed by the user provided as argument
|
|
243
311
|
"""
|
|
244
|
-
|
|
312
|
+
offset = 0
|
|
313
|
+
followed = False
|
|
314
|
+
|
|
315
|
+
while True:
|
|
316
|
+
try:
|
|
317
|
+
followed_names = self.follower_names(limit=20, offset=offset)
|
|
318
|
+
if user in followed_names:
|
|
319
|
+
followed = True
|
|
320
|
+
break
|
|
321
|
+
if not followed_names:
|
|
322
|
+
break
|
|
323
|
+
offset += 20
|
|
324
|
+
except Exception as e:
|
|
325
|
+
print(f"Warning: API error when performing following check: {e=}")
|
|
326
|
+
return followed
|
|
327
|
+
return followed
|
|
245
328
|
|
|
246
329
|
def project_count(self):
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
330
|
+
with requests.no_error_handling():
|
|
331
|
+
text = requests.get(
|
|
332
|
+
f"https://scratch.mit.edu/users/{self.username}/projects/",
|
|
333
|
+
headers = self._headers
|
|
334
|
+
).text
|
|
335
|
+
return commons.webscrape_count(text, "Shared Projects (", ")")
|
|
252
336
|
|
|
253
337
|
def studio_count(self):
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
338
|
+
with requests.no_error_handling():
|
|
339
|
+
text = requests.get(
|
|
340
|
+
f"https://scratch.mit.edu/users/{self.username}/studios/",
|
|
341
|
+
headers = self._headers
|
|
342
|
+
).text
|
|
343
|
+
return commons.webscrape_count(text, "Studios I Curate (", ")")
|
|
259
344
|
|
|
260
345
|
def studios_following_count(self):
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
346
|
+
with requests.no_error_handling():
|
|
347
|
+
text = requests.get(
|
|
348
|
+
f"https://scratch.mit.edu/users/{self.username}/studios_following/",
|
|
349
|
+
headers = self._headers
|
|
350
|
+
).text
|
|
351
|
+
return commons.webscrape_count(text, "Studios I Follow (", ")")
|
|
266
352
|
|
|
267
353
|
def studios(self, *, limit=40, offset=0):
|
|
268
354
|
_studios = commons.api_iterative(
|
|
@@ -313,8 +399,9 @@ class User(BaseSiteComponent):
|
|
|
313
399
|
# The index of the first project on page #n is just (n-1) * 40
|
|
314
400
|
first_idx = (page - 1) * 40
|
|
315
401
|
|
|
316
|
-
|
|
317
|
-
|
|
402
|
+
with requests.no_error_handling():
|
|
403
|
+
page_content = requests.get(f"https://scratch.mit.edu/projects/all/{self.username}/loves/"
|
|
404
|
+
f"?page={page}", headers=self._headers).content
|
|
318
405
|
|
|
319
406
|
soup = BeautifulSoup(
|
|
320
407
|
page_content,
|
|
@@ -349,7 +436,7 @@ class User(BaseSiteComponent):
|
|
|
349
436
|
# A thumbnail link (no need to webscrape this)
|
|
350
437
|
# A title
|
|
351
438
|
# An Author (called an owner for some reason)
|
|
352
|
-
|
|
439
|
+
assert isinstance(project_element, Tag)
|
|
353
440
|
project_anchors = project_element.find_all("a")
|
|
354
441
|
# Each list item has three <a> tags, the first two linking the project
|
|
355
442
|
# 1st contains <img> tag
|
|
@@ -358,10 +445,16 @@ class User(BaseSiteComponent):
|
|
|
358
445
|
|
|
359
446
|
# This function is pretty handy!
|
|
360
447
|
# I'll use it for an id from a string like: /projects/1070616180/
|
|
361
|
-
|
|
448
|
+
first_anchor = project_anchors[0]
|
|
449
|
+
second_anchor = project_anchors[1]
|
|
450
|
+
third_anchor = project_anchors[2]
|
|
451
|
+
assert isinstance(first_anchor, Tag)
|
|
452
|
+
assert isinstance(second_anchor, Tag)
|
|
453
|
+
assert isinstance(third_anchor, Tag)
|
|
454
|
+
project_id = commons.webscrape_count(first_anchor.attrs["href"],
|
|
362
455
|
"/projects/", "/")
|
|
363
|
-
title =
|
|
364
|
-
author =
|
|
456
|
+
title = second_anchor.contents[0]
|
|
457
|
+
author = third_anchor.contents[0]
|
|
365
458
|
|
|
366
459
|
# Instantiating a project with the properties that we know
|
|
367
460
|
# This may cause issues (see below)
|
|
@@ -382,10 +475,11 @@ class User(BaseSiteComponent):
|
|
|
382
475
|
return _projects
|
|
383
476
|
|
|
384
477
|
def loves_count(self):
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
478
|
+
with requests.no_error_handling():
|
|
479
|
+
text = requests.get(
|
|
480
|
+
f"https://scratch.mit.edu/projects/all/{self.username}/loves/",
|
|
481
|
+
headers=self._headers
|
|
482
|
+
).text
|
|
389
483
|
|
|
390
484
|
# If there are no loved projects, then Scratch doesn't actually display the number - so we have to catch this
|
|
391
485
|
soup = BeautifulSoup(text, "html.parser")
|
|
@@ -406,10 +500,11 @@ class User(BaseSiteComponent):
|
|
|
406
500
|
return commons.parse_object_list(_projects, project.Project, self._session)
|
|
407
501
|
|
|
408
502
|
def favorites_count(self):
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
503
|
+
with requests.no_error_handling():
|
|
504
|
+
text = requests.get(
|
|
505
|
+
f"https://scratch.mit.edu/users/{self.username}/favorites/",
|
|
506
|
+
headers=self._headers
|
|
507
|
+
).text
|
|
413
508
|
return commons.webscrape_count(text, "Favorites (", ")")
|
|
414
509
|
|
|
415
510
|
def toggle_commenting(self):
|
|
@@ -544,17 +639,23 @@ class User(BaseSiteComponent):
|
|
|
544
639
|
data = {
|
|
545
640
|
'id': text.split('<div id="comments-')[1].split('" class="comment')[0],
|
|
546
641
|
'author': {"username": text.split('" data-comment-user="')[1].split('"><img class')[0]},
|
|
547
|
-
'content': text.split('<div class="content">')[1].split('
|
|
642
|
+
'content': text.split('<div class="content">')[1].split('</div>')[0].strip(),
|
|
548
643
|
'reply_count': 0,
|
|
549
644
|
'cached_replies': []
|
|
550
645
|
}
|
|
551
|
-
_comment = comment.Comment(source="profile", parent_id=None if parent_id=="" else parent_id, commentee_id=commentee_id, source_id=self.username, id=data["id"], _session = self._session)
|
|
646
|
+
_comment = comment.Comment(source="profile", parent_id=None if parent_id=="" else parent_id, commentee_id=commentee_id, source_id=self.username, id=data["id"], _session = self._session, datetime = datetime.now())
|
|
552
647
|
_comment._update_from_dict(data)
|
|
553
648
|
return _comment
|
|
554
649
|
except Exception:
|
|
555
650
|
if '{"error": "isFlood"}' in text:
|
|
556
651
|
raise(exceptions.CommentPostFailure(
|
|
557
652
|
"You are being rate-limited for running this operation too often. Implement a cooldown of about 10 seconds."))
|
|
653
|
+
elif '<script id="error-data" type="application/json">' in text:
|
|
654
|
+
raw_error_data = text.split('<script id="error-data" type="application/json">')[1].split('</script>')[0]
|
|
655
|
+
error_data = json.loads(raw_error_data)
|
|
656
|
+
expires = error_data['mute_status']['muteExpiresAt']
|
|
657
|
+
expires = datetime.fromtimestamp(expires, timezone.utc)
|
|
658
|
+
raise(exceptions.CommentPostFailure(f"You have been muted. Mute expires on {expires}"))
|
|
558
659
|
else:
|
|
559
660
|
raise(exceptions.FetchError(f"Couldn't parse API response: {r.text!r}"))
|
|
560
661
|
|
|
@@ -581,7 +682,8 @@ class User(BaseSiteComponent):
|
|
|
581
682
|
Returns:
|
|
582
683
|
list<scratchattach.Activity>: The user's activity data as parsed list of scratchattach.activity.Activity objects
|
|
583
684
|
"""
|
|
584
|
-
|
|
685
|
+
with requests.no_error_handling():
|
|
686
|
+
soup = BeautifulSoup(requests.get(f"https://scratch.mit.edu/messages/ajax/user-activity/?user={self.username}&max={limit}").text, 'html.parser')
|
|
585
687
|
|
|
586
688
|
activities = []
|
|
587
689
|
source = soup.find_all("li")
|
|
@@ -599,7 +701,8 @@ class User(BaseSiteComponent):
|
|
|
599
701
|
Returns:
|
|
600
702
|
str: The raw user activity HTML data
|
|
601
703
|
"""
|
|
602
|
-
|
|
704
|
+
with requests.no_error_handling():
|
|
705
|
+
return requests.get(f"https://scratch.mit.edu/messages/ajax/user-activity/?user={self.username}&max={limit}").text
|
|
603
706
|
|
|
604
707
|
|
|
605
708
|
def follow(self):
|
|
@@ -668,7 +771,8 @@ class User(BaseSiteComponent):
|
|
|
668
771
|
URL = f"https://scratch.mit.edu/site-api/comments/user/{self.username}/?page={page}"
|
|
669
772
|
DATA = []
|
|
670
773
|
|
|
671
|
-
|
|
774
|
+
with requests.no_error_handling():
|
|
775
|
+
page_contents = requests.get(URL).content
|
|
672
776
|
|
|
673
777
|
soup = BeautifulSoup(page_contents, "html.parser")
|
|
674
778
|
|
|
@@ -813,6 +917,24 @@ class User(BaseSiteComponent):
|
|
|
813
917
|
v = Verificator(self, verification_project_id)
|
|
814
918
|
return v
|
|
815
919
|
|
|
920
|
+
def rank(self):
|
|
921
|
+
"""
|
|
922
|
+
Finds the rank of the user.
|
|
923
|
+
May replace user.scratchteam and user.is_new_scratcher in the future.
|
|
924
|
+
"""
|
|
925
|
+
|
|
926
|
+
if self.is_new_scratcher():
|
|
927
|
+
return Rank.NEW_SCRATCHER
|
|
928
|
+
# Is New Scratcher
|
|
929
|
+
|
|
930
|
+
if not self.scratchteam:
|
|
931
|
+
return Rank.SCRATCHER
|
|
932
|
+
# Is Scratcher
|
|
933
|
+
|
|
934
|
+
return Rank.SCRATCH_TEAM
|
|
935
|
+
# Is Scratch Team member
|
|
936
|
+
|
|
937
|
+
|
|
816
938
|
# ------ #
|
|
817
939
|
|
|
818
940
|
def get_user(username) -> User:
|
scratchattach/utils/commons.py
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
"""v2 ready: Common functions used by various internal modules"""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import string
|
|
5
|
+
|
|
4
6
|
from typing import Optional, Final, Any, TypeVar, Callable, TYPE_CHECKING, Union
|
|
7
|
+
from threading import Event as ManualResetEvent
|
|
5
8
|
from threading import Lock
|
|
6
9
|
|
|
7
10
|
from . import exceptions
|
|
8
|
-
from .requests import
|
|
11
|
+
from .requests import requests
|
|
12
|
+
|
|
13
|
+
from scratchattach.site import _base
|
|
9
14
|
|
|
10
|
-
from ..site import _base
|
|
11
15
|
|
|
12
16
|
headers: Final = {
|
|
13
17
|
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
@@ -130,7 +134,7 @@ def _get_object(identificator_name, identificator, __class: type[C], NotFoundExc
|
|
|
130
134
|
# Internal function: Generalization of the process ran by get_user, get_studio etc.
|
|
131
135
|
# Builds an object of class that is inheriting from BaseSiteComponent
|
|
132
136
|
# # Class must inherit from BaseSiteComponent
|
|
133
|
-
from
|
|
137
|
+
from scratchattach.site import project
|
|
134
138
|
try:
|
|
135
139
|
use_class: type = __class
|
|
136
140
|
if __class is project.PartialProject:
|
|
@@ -182,44 +186,41 @@ class LockEvent:
|
|
|
182
186
|
"""
|
|
183
187
|
Can be waited on and triggered. Not to be confused with threading.Event, which has to be reset.
|
|
184
188
|
"""
|
|
185
|
-
|
|
189
|
+
_event: ManualResetEvent
|
|
190
|
+
_locks: list[Lock]
|
|
191
|
+
_access_locks: Lock
|
|
186
192
|
def __init__(self):
|
|
187
|
-
self.
|
|
188
|
-
self.
|
|
193
|
+
self._event = ManualResetEvent()
|
|
194
|
+
self._locks = []
|
|
195
|
+
self._access_locks = Lock()
|
|
189
196
|
|
|
190
197
|
def wait(self, blocking: bool = True, timeout: Optional[Union[int, float]] = None) -> bool:
|
|
191
198
|
"""
|
|
192
199
|
Wait for the event.
|
|
193
200
|
"""
|
|
194
|
-
timeout
|
|
195
|
-
if not blocking:
|
|
196
|
-
timeout = 0
|
|
197
|
-
return self.on().acquire(timeout=timeout)
|
|
201
|
+
return self._event.wait(timeout if blocking else 0)
|
|
198
202
|
|
|
199
203
|
def trigger(self):
|
|
200
204
|
"""
|
|
201
205
|
Trigger all threads waiting on this event to continue.
|
|
202
206
|
"""
|
|
203
|
-
with self.
|
|
204
|
-
for lock in self.
|
|
207
|
+
with self._access_locks:
|
|
208
|
+
for lock in self._locks:
|
|
205
209
|
try:
|
|
206
|
-
lock.release()
|
|
210
|
+
lock.release()
|
|
207
211
|
except RuntimeError:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
self.locks.remove(lock)
|
|
213
|
-
except RuntimeError:
|
|
214
|
-
lock.acquire(timeout=0) # Lock the lock again.
|
|
212
|
+
pass
|
|
213
|
+
self._locks.clear()
|
|
214
|
+
self._event.set()
|
|
215
|
+
self._event = ManualResetEvent()
|
|
215
216
|
|
|
216
217
|
def on(self) -> Lock:
|
|
217
218
|
"""
|
|
218
|
-
Return a lock that will unlock once the event takes place.
|
|
219
|
+
Return a lock that will unlock once the event takes place. Return value has to be waited on to wait for the event.
|
|
219
220
|
"""
|
|
220
221
|
lock = Lock()
|
|
221
|
-
with self.
|
|
222
|
-
self.
|
|
222
|
+
with self._access_locks:
|
|
223
|
+
self._locks.append(lock)
|
|
223
224
|
lock.acquire(timeout=0)
|
|
224
225
|
return lock
|
|
225
226
|
|
|
@@ -241,3 +242,14 @@ def get_class_sort_mode(mode: str) -> tuple[str, str]:
|
|
|
241
242
|
descsort = "title"
|
|
242
243
|
|
|
243
244
|
return ascsort, descsort
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def b62_decode(s: str):
|
|
248
|
+
chars = string.digits + string.ascii_uppercase + string.ascii_lowercase
|
|
249
|
+
|
|
250
|
+
ret = 0
|
|
251
|
+
for char in s:
|
|
252
|
+
ret = ret * 62 + chars.index(char)
|
|
253
|
+
|
|
254
|
+
return ret
|
|
255
|
+
|
scratchattach/utils/enums.py
CHANGED
|
@@ -4,13 +4,12 @@ Adapted from https://translate-service.scratch.mit.edu/supported?language=en
|
|
|
4
4
|
"""
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from enum import Enum
|
|
8
7
|
from dataclasses import dataclass
|
|
9
|
-
|
|
8
|
+
from enum import Enum
|
|
10
9
|
from typing import Optional, Callable, Iterable
|
|
11
10
|
|
|
12
11
|
|
|
13
|
-
@dataclass
|
|
12
|
+
@dataclass
|
|
14
13
|
class Language:
|
|
15
14
|
name: str = None
|
|
16
15
|
code: str = None
|
|
@@ -44,7 +43,7 @@ class _EnumWrapper(Enum):
|
|
|
44
43
|
|
|
45
44
|
if apply_func(_val) == value:
|
|
46
45
|
return item_obj
|
|
47
|
-
|
|
46
|
+
|
|
48
47
|
except TypeError:
|
|
49
48
|
pass
|
|
50
49
|
|
|
@@ -167,7 +166,7 @@ class Languages(_EnumWrapper):
|
|
|
167
166
|
return super().all_of(attr_name, apply_func)
|
|
168
167
|
|
|
169
168
|
|
|
170
|
-
@dataclass
|
|
169
|
+
@dataclass
|
|
171
170
|
class TTSVoice:
|
|
172
171
|
name: str
|
|
173
172
|
gender: str
|
|
@@ -195,3 +194,43 @@ class TTSVoices(_EnumWrapper):
|
|
|
195
194
|
def all_of(cls, attr_name: str = "name", apply_func: Optional[Callable] = None) -> Iterable:
|
|
196
195
|
return super().all_of(attr_name, apply_func)
|
|
197
196
|
|
|
197
|
+
|
|
198
|
+
@dataclass
|
|
199
|
+
class AlertType:
|
|
200
|
+
id: int
|
|
201
|
+
message: str
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class AlertTypes(_EnumWrapper):
|
|
205
|
+
"""
|
|
206
|
+
Enum for associating alert type indecies with their messages, for use with the str.format() method.
|
|
207
|
+
"""
|
|
208
|
+
# Reference: https://github.com/TimMcCool/scratchattach/issues/304#issuecomment-2800110811
|
|
209
|
+
# NOTE: THE TEXT WITHIN THE BRACES HERE MATTERS! IF YOU WANT TO CHANGE IT, MAKE SURE TO EDIT `site.alert.EducatorAlert`!
|
|
210
|
+
ban = AlertType(0, "{username} was banned.")
|
|
211
|
+
unban = AlertType(1, "{username} was unbanned.")
|
|
212
|
+
excluded_from_homepage = AlertType(2, "{username} was excluded from homepage")
|
|
213
|
+
excluded_from_homepage2 = AlertType(3, "{username} was excluded from homepage") # for some reason there are duplicates
|
|
214
|
+
notified = AlertType(4, "{username} was notified by a Scratch Administrator. Notification type: {notification_type}") # not sure what notification type is
|
|
215
|
+
autoban = AlertType(5, "{username} was banned automatically")
|
|
216
|
+
autoremoved = AlertType(6, "{project} by {username} was removed automatically")
|
|
217
|
+
project_censored2 = AlertType(7, "{project} by {username} was censored.") # <empty #7>
|
|
218
|
+
project_censored = AlertType(20, "{project} by {username} was censored.")
|
|
219
|
+
project_uncensored = AlertType(8, "{project} by {username} was uncensored.")
|
|
220
|
+
project_reviewed2 = AlertType(9, "{project} by {username} was reviewed by a Scratch Administrator.") # <empty #9>
|
|
221
|
+
project_reviewed = AlertType(10, "{project} by {username} was reviewed by a Scratch Administrator.")
|
|
222
|
+
project_deleted = AlertType(11, "{project} by {username} was deleted by a Scratch Administrator.")
|
|
223
|
+
user_deleted2 = AlertType(12, "{username} was deleted by a Scratch Administrator") # <empty #12>
|
|
224
|
+
user_deleted = AlertType(17, "{username} was deleted by a Scratch Administrator")
|
|
225
|
+
studio_reviewed2 = AlertType(13, "{studio} was reviewed by a Scratch Administrator.") # <empty #13>
|
|
226
|
+
studio_reviewed = AlertType(14, "{studio} was reviewed by a Scratch Administrator.")
|
|
227
|
+
studio_deleted = AlertType(15, "{studio} was deleted by a Scratch Administrator.")
|
|
228
|
+
email_confirm2 = AlertType(16, "The email address of {username} was confirmed by a Scratch Administrator") # <empty #16>
|
|
229
|
+
email_confirm = AlertType(18, "The email address of {username} was confirmed by a Scratch Administrator") # no '.' in HTML
|
|
230
|
+
email_unconfirm = AlertType(19, "The email address of {username} was set as not confirmed by a Scratch Administrator")
|
|
231
|
+
automute = AlertType(22, "{username} was automatically muted by our comment filters. The comment they tried to post was: {comment}")
|
|
232
|
+
default = AlertType(-1, "{username} had an admin action performed.") # default case
|
|
233
|
+
|
|
234
|
+
@classmethod
|
|
235
|
+
def find(cls, value, by: str = "id", apply_func: Optional[Callable] = None) -> AlertType:
|
|
236
|
+
return super().find(value, by, apply_func)
|
|
@@ -231,3 +231,13 @@ class LoginDataWarning(UserWarning):
|
|
|
231
231
|
"""
|
|
232
232
|
Warns you not to accidentally share your login data.
|
|
233
233
|
"""
|
|
234
|
+
|
|
235
|
+
class AnonymousSiteComponentWarning(UserWarning):
|
|
236
|
+
"""
|
|
237
|
+
Warns about a usage of an anonymous site component.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
class UnexpectedWebsocketEventWarning(RuntimeWarning):
|
|
241
|
+
"""
|
|
242
|
+
Warns about an unexpected occurrence with a websocket.
|
|
243
|
+
"""
|