scratchattach 2.1.13__py3-none-any.whl → 2.1.15b0__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 +86 -15
- scratchattach/editor/blockshape.py +10 -6
- 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 +49 -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 +2 -2
- scratchattach/eventhandlers/cloud_events.py +2 -2
- scratchattach/eventhandlers/cloud_requests.py +3 -3
- scratchattach/eventhandlers/cloud_server.py +3 -3
- 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 +42 -21
- scratchattach/site/session.py +170 -80
- scratchattach/site/studio.py +4 -4
- scratchattach/site/user.py +179 -64
- 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.13.dist-info → scratchattach-2.1.15b0.dist-info}/METADATA +8 -3
- scratchattach-2.1.15b0.dist-info/RECORD +66 -0
- {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/WHEEL +1 -1
- scratchattach/editor/sbuild.py +0 -2837
- scratchattach-2.1.13.dist-info/RECORD +0 -63
- {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/licenses/LICENSE +0 -0
- {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/top_level.txt +0 -0
scratchattach/site/user.py
CHANGED
|
@@ -3,22 +3,37 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import random
|
|
6
|
+
import re
|
|
6
7
|
import string
|
|
7
|
-
from datetime import datetime
|
|
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
|
|
8
22
|
|
|
9
|
-
from ..eventhandlers import message_events
|
|
10
23
|
from . import project
|
|
11
|
-
from ..utils import exceptions
|
|
12
24
|
from . import studio
|
|
13
25
|
from . import forum
|
|
14
|
-
from bs4 import BeautifulSoup
|
|
15
|
-
from ._base import BaseSiteComponent
|
|
16
|
-
from ..utils.commons import headers
|
|
17
|
-
from ..utils import commons
|
|
18
26
|
from . import comment
|
|
19
27
|
from . import activity
|
|
28
|
+
from . import classroom
|
|
20
29
|
|
|
21
|
-
|
|
30
|
+
class Rank(Enum):
|
|
31
|
+
"""
|
|
32
|
+
Possible ranks in scratch
|
|
33
|
+
"""
|
|
34
|
+
NEW_SCRATCHER = 0
|
|
35
|
+
SCRATCHER = 1
|
|
36
|
+
SCRATCH_TEAM = 2
|
|
22
37
|
|
|
23
38
|
class Verificator:
|
|
24
39
|
|
|
@@ -62,7 +77,7 @@ class User(BaseSiteComponent):
|
|
|
62
77
|
|
|
63
78
|
# Info on how the .update method has to fetch the data:
|
|
64
79
|
self.update_function = requests.get
|
|
65
|
-
self.
|
|
80
|
+
self.update_api = f"https://api.scratch.mit.edu/users/{entries['username']}"
|
|
66
81
|
|
|
67
82
|
# Set attributes every User object needs to have:
|
|
68
83
|
self._session = None
|
|
@@ -70,6 +85,10 @@ class User(BaseSiteComponent):
|
|
|
70
85
|
self.username = None
|
|
71
86
|
self.name = None
|
|
72
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
|
+
|
|
73
92
|
# Update attributes from entries dict:
|
|
74
93
|
entries.setdefault("name", entries.get("username"))
|
|
75
94
|
self.__dict__.update(entries)
|
|
@@ -120,27 +139,73 @@ class User(BaseSiteComponent):
|
|
|
120
139
|
raise exceptions.Unauthorized(
|
|
121
140
|
"You need to be authenticated as the profile owner to do this.")
|
|
122
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
|
+
|
|
123
182
|
def does_exist(self):
|
|
124
183
|
"""
|
|
125
184
|
Returns:
|
|
126
185
|
boolean : True if the user exists, False if the user is deleted, None if an error occured
|
|
127
186
|
"""
|
|
128
|
-
|
|
187
|
+
with requests.no_error_handling():
|
|
188
|
+
status_code = requests.get(f"https://scratch.mit.edu/users/{self.username}/").status_code
|
|
129
189
|
if status_code == 200:
|
|
130
190
|
return True
|
|
131
191
|
elif status_code == 404:
|
|
132
192
|
return False
|
|
133
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.")
|
|
134
196
|
def is_new_scratcher(self):
|
|
135
197
|
"""
|
|
136
198
|
Returns:
|
|
137
199
|
boolean : True if the user has the New Scratcher status, else False
|
|
138
200
|
"""
|
|
139
201
|
try:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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=}")
|
|
144
209
|
return None
|
|
145
210
|
|
|
146
211
|
def message_count(self):
|
|
@@ -169,19 +234,21 @@ class User(BaseSiteComponent):
|
|
|
169
234
|
|
|
170
235
|
def follower_count(self):
|
|
171
236
|
# follower count
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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 (", ")")
|
|
177
243
|
|
|
178
244
|
def following_count(self):
|
|
179
245
|
# following count
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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 (", ")")
|
|
185
252
|
|
|
186
253
|
def followers(self, *, limit=40, offset=0):
|
|
187
254
|
"""
|
|
@@ -215,7 +282,7 @@ class User(BaseSiteComponent):
|
|
|
215
282
|
"""
|
|
216
283
|
return [i.name for i in self.following(limit=limit, offset=offset)]
|
|
217
284
|
|
|
218
|
-
def is_following(self, user):
|
|
285
|
+
def is_following(self, user: str):
|
|
219
286
|
"""
|
|
220
287
|
Returns:
|
|
221
288
|
boolean: Whether the user is following the user provided as argument
|
|
@@ -229,11 +296,11 @@ class User(BaseSiteComponent):
|
|
|
229
296
|
if user in following_names:
|
|
230
297
|
following = True
|
|
231
298
|
break
|
|
232
|
-
if following_names
|
|
299
|
+
if not following_names:
|
|
233
300
|
break
|
|
234
301
|
offset += 20
|
|
235
|
-
except Exception:
|
|
236
|
-
print("Warning: API error when performing following check")
|
|
302
|
+
except Exception as e:
|
|
303
|
+
print(f"Warning: API error when performing following check: {e=}")
|
|
237
304
|
return following
|
|
238
305
|
return following
|
|
239
306
|
|
|
@@ -242,28 +309,46 @@ class User(BaseSiteComponent):
|
|
|
242
309
|
Returns:
|
|
243
310
|
boolean: Whether the user is followed by the user provided as argument
|
|
244
311
|
"""
|
|
245
|
-
|
|
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
|
|
246
328
|
|
|
247
329
|
def project_count(self):
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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 (", ")")
|
|
253
336
|
|
|
254
337
|
def studio_count(self):
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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 (", ")")
|
|
260
344
|
|
|
261
345
|
def studios_following_count(self):
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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 (", ")")
|
|
267
352
|
|
|
268
353
|
def studios(self, *, limit=40, offset=0):
|
|
269
354
|
_studios = commons.api_iterative(
|
|
@@ -314,8 +399,9 @@ class User(BaseSiteComponent):
|
|
|
314
399
|
# The index of the first project on page #n is just (n-1) * 40
|
|
315
400
|
first_idx = (page - 1) * 40
|
|
316
401
|
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
319
405
|
|
|
320
406
|
soup = BeautifulSoup(
|
|
321
407
|
page_content,
|
|
@@ -350,7 +436,7 @@ class User(BaseSiteComponent):
|
|
|
350
436
|
# A thumbnail link (no need to webscrape this)
|
|
351
437
|
# A title
|
|
352
438
|
# An Author (called an owner for some reason)
|
|
353
|
-
|
|
439
|
+
assert isinstance(project_element, Tag)
|
|
354
440
|
project_anchors = project_element.find_all("a")
|
|
355
441
|
# Each list item has three <a> tags, the first two linking the project
|
|
356
442
|
# 1st contains <img> tag
|
|
@@ -359,10 +445,16 @@ class User(BaseSiteComponent):
|
|
|
359
445
|
|
|
360
446
|
# This function is pretty handy!
|
|
361
447
|
# I'll use it for an id from a string like: /projects/1070616180/
|
|
362
|
-
|
|
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"],
|
|
363
455
|
"/projects/", "/")
|
|
364
|
-
title =
|
|
365
|
-
author =
|
|
456
|
+
title = second_anchor.contents[0]
|
|
457
|
+
author = third_anchor.contents[0]
|
|
366
458
|
|
|
367
459
|
# Instantiating a project with the properties that we know
|
|
368
460
|
# This may cause issues (see below)
|
|
@@ -383,10 +475,11 @@ class User(BaseSiteComponent):
|
|
|
383
475
|
return _projects
|
|
384
476
|
|
|
385
477
|
def loves_count(self):
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
|
390
483
|
|
|
391
484
|
# If there are no loved projects, then Scratch doesn't actually display the number - so we have to catch this
|
|
392
485
|
soup = BeautifulSoup(text, "html.parser")
|
|
@@ -407,10 +500,11 @@ class User(BaseSiteComponent):
|
|
|
407
500
|
return commons.parse_object_list(_projects, project.Project, self._session)
|
|
408
501
|
|
|
409
502
|
def favorites_count(self):
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
414
508
|
return commons.webscrape_count(text, "Favorites (", ")")
|
|
415
509
|
|
|
416
510
|
def toggle_commenting(self):
|
|
@@ -545,11 +639,11 @@ class User(BaseSiteComponent):
|
|
|
545
639
|
data = {
|
|
546
640
|
'id': text.split('<div id="comments-')[1].split('" class="comment')[0],
|
|
547
641
|
'author': {"username": text.split('" data-comment-user="')[1].split('"><img class')[0]},
|
|
548
|
-
'content': text.split('<div class="content">')[1].split('
|
|
642
|
+
'content': text.split('<div class="content">')[1].split('</div>')[0].strip(),
|
|
549
643
|
'reply_count': 0,
|
|
550
644
|
'cached_replies': []
|
|
551
645
|
}
|
|
552
|
-
_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())
|
|
553
647
|
_comment._update_from_dict(data)
|
|
554
648
|
return _comment
|
|
555
649
|
except Exception:
|
|
@@ -560,7 +654,7 @@ class User(BaseSiteComponent):
|
|
|
560
654
|
raw_error_data = text.split('<script id="error-data" type="application/json">')[1].split('</script>')[0]
|
|
561
655
|
error_data = json.loads(raw_error_data)
|
|
562
656
|
expires = error_data['mute_status']['muteExpiresAt']
|
|
563
|
-
expires = datetime.
|
|
657
|
+
expires = datetime.fromtimestamp(expires, timezone.utc)
|
|
564
658
|
raise(exceptions.CommentPostFailure(f"You have been muted. Mute expires on {expires}"))
|
|
565
659
|
else:
|
|
566
660
|
raise(exceptions.FetchError(f"Couldn't parse API response: {r.text!r}"))
|
|
@@ -588,7 +682,8 @@ class User(BaseSiteComponent):
|
|
|
588
682
|
Returns:
|
|
589
683
|
list<scratchattach.Activity>: The user's activity data as parsed list of scratchattach.activity.Activity objects
|
|
590
684
|
"""
|
|
591
|
-
|
|
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')
|
|
592
687
|
|
|
593
688
|
activities = []
|
|
594
689
|
source = soup.find_all("li")
|
|
@@ -606,7 +701,8 @@ class User(BaseSiteComponent):
|
|
|
606
701
|
Returns:
|
|
607
702
|
str: The raw user activity HTML data
|
|
608
703
|
"""
|
|
609
|
-
|
|
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
|
|
610
706
|
|
|
611
707
|
|
|
612
708
|
def follow(self):
|
|
@@ -675,7 +771,8 @@ class User(BaseSiteComponent):
|
|
|
675
771
|
URL = f"https://scratch.mit.edu/site-api/comments/user/{self.username}/?page={page}"
|
|
676
772
|
DATA = []
|
|
677
773
|
|
|
678
|
-
|
|
774
|
+
with requests.no_error_handling():
|
|
775
|
+
page_contents = requests.get(URL).content
|
|
679
776
|
|
|
680
777
|
soup = BeautifulSoup(page_contents, "html.parser")
|
|
681
778
|
|
|
@@ -820,6 +917,24 @@ class User(BaseSiteComponent):
|
|
|
820
917
|
v = Verificator(self, verification_project_id)
|
|
821
918
|
return v
|
|
822
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
|
+
|
|
823
938
|
# ------ #
|
|
824
939
|
|
|
825
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
|
+
"""
|