scratchattach 2.1.15b0__py3-none-any.whl → 3.0.0b1__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.
- cli/__about__.py +1 -0
- cli/__init__.py +26 -0
- cli/cmd/__init__.py +4 -0
- cli/cmd/group.py +127 -0
- cli/cmd/login.py +60 -0
- cli/cmd/profile.py +7 -0
- cli/cmd/sessions.py +5 -0
- cli/context.py +142 -0
- cli/db.py +66 -0
- cli/namespace.py +14 -0
- {scratchattach/cloud → cloud}/_base.py +112 -87
- {scratchattach/cloud → cloud}/cloud.py +16 -16
- {scratchattach/editor → editor}/__init__.py +2 -1
- {scratchattach/editor → editor}/asset.py +26 -14
- {scratchattach/editor → editor}/backpack_json.py +3 -5
- {scratchattach/editor → editor}/base.py +2 -4
- {scratchattach/editor → editor}/block.py +27 -22
- {scratchattach/editor → editor}/blockshape.py +1 -1
- {scratchattach/editor → editor}/build_defaulting.py +2 -2
- editor/commons.py +145 -0
- {scratchattach/editor → editor}/field.py +1 -1
- {scratchattach/editor → editor}/inputs.py +6 -3
- {scratchattach/editor → editor}/meta.py +10 -7
- {scratchattach/editor → editor}/monitor.py +10 -8
- {scratchattach/editor → editor}/mutation.py +68 -11
- {scratchattach/editor → editor}/pallete.py +1 -3
- {scratchattach/editor → editor}/prim.py +4 -0
- {scratchattach/editor → editor}/project.py +118 -16
- {scratchattach/editor → editor}/sprite.py +25 -15
- {scratchattach/editor → editor}/vlb.py +2 -2
- {scratchattach/eventhandlers → eventhandlers}/_base.py +1 -0
- {scratchattach/eventhandlers → eventhandlers}/cloud_events.py +26 -6
- {scratchattach/eventhandlers → eventhandlers}/cloud_recorder.py +4 -4
- {scratchattach/eventhandlers → eventhandlers}/cloud_requests.py +139 -54
- {scratchattach/eventhandlers → eventhandlers}/cloud_server.py +6 -3
- {scratchattach/eventhandlers → eventhandlers}/cloud_storage.py +1 -2
- eventhandlers/filterbot.py +163 -0
- other/other_apis.py +598 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/METADATA +7 -11
- scratchattach-3.0.0b1.dist-info/RECORD +79 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/WHEEL +1 -1
- scratchattach-3.0.0b1.dist-info/entry_points.txt +2 -0
- scratchattach-3.0.0b1.dist-info/top_level.txt +7 -0
- {scratchattach/site → site}/_base.py +32 -5
- site/activity.py +426 -0
- {scratchattach/site → site}/alert.py +4 -5
- {scratchattach/site → site}/backpack_asset.py +2 -1
- {scratchattach/site → site}/classroom.py +80 -73
- {scratchattach/site → site}/cloud_activity.py +43 -29
- {scratchattach/site → site}/comment.py +86 -100
- {scratchattach/site → site}/forum.py +8 -4
- site/placeholder.py +132 -0
- {scratchattach/site → site}/project.py +228 -122
- {scratchattach/site → site}/session.py +156 -71
- {scratchattach/site → site}/studio.py +139 -46
- site/typed_dicts.py +151 -0
- {scratchattach/site → site}/user.py +511 -215
- {scratchattach/utils → utils}/commons.py +12 -4
- {scratchattach/utils → utils}/encoder.py +7 -4
- {scratchattach/utils → utils}/enums.py +1 -0
- {scratchattach/utils → utils}/exceptions.py +36 -2
- utils/optional_async.py +154 -0
- utils/requests.py +306 -0
- scratchattach/__init__.py +0 -29
- scratchattach/editor/commons.py +0 -273
- scratchattach/eventhandlers/filterbot.py +0 -161
- scratchattach/other/other_apis.py +0 -284
- scratchattach/site/activity.py +0 -382
- scratchattach/utils/requests.py +0 -93
- scratchattach-2.1.15b0.dist-info/RECORD +0 -66
- scratchattach-2.1.15b0.dist-info/top_level.txt +0 -1
- {scratchattach/cloud → cloud}/__init__.py +0 -0
- {scratchattach/editor → editor}/code_translation/__init__.py +0 -0
- {scratchattach/editor → editor}/code_translation/parse.py +0 -0
- {scratchattach/editor → editor}/comment.py +0 -0
- {scratchattach/editor → editor}/extension.py +0 -0
- {scratchattach/editor → editor}/twconfig.py +0 -0
- {scratchattach/eventhandlers → eventhandlers}/__init__.py +0 -0
- {scratchattach/eventhandlers → eventhandlers}/combine.py +0 -0
- {scratchattach/eventhandlers → eventhandlers}/message_events.py +0 -0
- {scratchattach/other → other}/__init__.py +0 -0
- {scratchattach/other → other}/project_json_capabilities.py +0 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
- {scratchattach/site → site}/__init__.py +0 -0
- {scratchattach/site → site}/browser_cookie3_stub.py +0 -0
- {scratchattach/site → site}/browser_cookies.py +0 -0
- {scratchattach/utils → utils}/__init__.py +0 -0
|
@@ -13,10 +13,12 @@ import warnings
|
|
|
13
13
|
import zlib
|
|
14
14
|
|
|
15
15
|
from dataclasses import dataclass, field
|
|
16
|
-
from typing import Optional, TypeVar, TYPE_CHECKING, overload, Any, Union
|
|
16
|
+
from typing import Optional, TypeVar, TYPE_CHECKING, overload, Any, Union, cast
|
|
17
17
|
from contextlib import contextmanager
|
|
18
18
|
from threading import local
|
|
19
19
|
|
|
20
|
+
from scratchattach import editor
|
|
21
|
+
|
|
20
22
|
Type = type
|
|
21
23
|
|
|
22
24
|
if TYPE_CHECKING:
|
|
@@ -30,11 +32,12 @@ from bs4 import BeautifulSoup, Tag
|
|
|
30
32
|
from typing_extensions import deprecated
|
|
31
33
|
|
|
32
34
|
from . import activity, classroom, forum, studio, user, project, backpack_asset, alert
|
|
35
|
+
from . import typed_dicts
|
|
33
36
|
# noinspection PyProtectedMember
|
|
34
37
|
from ._base import BaseSiteComponent
|
|
35
38
|
from scratchattach.cloud import cloud, _base
|
|
36
39
|
from scratchattach.eventhandlers import message_events, filterbot
|
|
37
|
-
from scratchattach.other import
|
|
40
|
+
from scratchattach.other import other_apis
|
|
38
41
|
from scratchattach.utils import commons, exceptions
|
|
39
42
|
from scratchattach.utils.commons import headers, empty_project_json, webscrape_count, get_class_sort_mode
|
|
40
43
|
from scratchattach.utils.requests import requests
|
|
@@ -57,8 +60,8 @@ def enforce_ratelimit(__type: str, name: str, amount: int = 5, duration: int = 6
|
|
|
57
60
|
"For security reasons, it cannot be turned off.\n\n"
|
|
58
61
|
"Don't spam-create studios or similar, it WILL get you banned."
|
|
59
62
|
)
|
|
60
|
-
|
|
61
|
-
C = TypeVar("C", bound=BaseSiteComponent)
|
|
63
|
+
|
|
64
|
+
C = TypeVar("C", bound=BaseSiteComponent)
|
|
62
65
|
|
|
63
66
|
@dataclass
|
|
64
67
|
class Session(BaseSiteComponent):
|
|
@@ -74,33 +77,65 @@ class Session(BaseSiteComponent):
|
|
|
74
77
|
mute_status: Information about commenting restrictions of the associated account
|
|
75
78
|
banned: Returns True if the associated account is banned
|
|
76
79
|
"""
|
|
77
|
-
username: str =
|
|
78
|
-
_user: user.User = field(repr=False, default=None)
|
|
80
|
+
username: str = field(repr=False, default="")
|
|
81
|
+
_user: Optional[user.User] = field(repr=False, default=None)
|
|
79
82
|
|
|
80
|
-
id: str = field(repr=False, default=
|
|
81
|
-
session_string: str
|
|
82
|
-
xtoken: str = field(repr=False, default=None)
|
|
83
|
-
email: str = field(repr=False, default=None)
|
|
83
|
+
id: str = field(repr=False, default="")
|
|
84
|
+
session_string: Optional[str] = field(repr=False, default=None)
|
|
85
|
+
xtoken: Optional[str] = field(repr=False, default=None)
|
|
86
|
+
email: Optional[str] = field(repr=False, default=None)
|
|
84
87
|
|
|
85
|
-
new_scratcher: bool = field(repr=False, default=
|
|
88
|
+
new_scratcher: bool = field(repr=False, default=False)
|
|
86
89
|
mute_status: Any = field(repr=False, default=None)
|
|
87
|
-
banned: bool = field(repr=False, default=
|
|
88
|
-
is_teacher: bool = field(repr=False, default=None)
|
|
90
|
+
banned: bool = field(repr=False, default=False)
|
|
89
91
|
|
|
90
|
-
time_created: datetime.datetime =
|
|
92
|
+
time_created: datetime.datetime = field(repr=False, default=datetime.datetime.fromtimestamp(0.0))
|
|
91
93
|
language: str = field(repr=False, default="en")
|
|
92
94
|
|
|
95
|
+
has_outstanding_email_confirmation: bool = field(repr=False, default=False)
|
|
96
|
+
is_teacher: bool = field(repr=False, default=False)
|
|
97
|
+
is_teacher_invitee: bool = field(repr=False, default=False)
|
|
98
|
+
ocular_token: Optional[str] = field(repr=False, default=None) # note that this is a header, not a cookie
|
|
99
|
+
_session: Optional[Session] = field(kw_only=True, default=None)
|
|
100
|
+
|
|
93
101
|
def __str__(self) -> str:
|
|
94
|
-
return f"
|
|
102
|
+
return f"-L {self.username}"
|
|
103
|
+
|
|
104
|
+
def __rich__(self):
|
|
105
|
+
from rich.panel import Panel
|
|
106
|
+
from rich.table import Table
|
|
107
|
+
from rich import box
|
|
108
|
+
from rich.markup import escape
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
self.update()
|
|
112
|
+
except KeyError as e:
|
|
113
|
+
warnings.warn(f"Ignored KeyError: {e}")
|
|
114
|
+
|
|
115
|
+
ret = Table(
|
|
116
|
+
f"[link={self.connect_linked_user().url}]{escape(self.username)}[/]",
|
|
117
|
+
f"Created: {self.time_created}", expand=True)
|
|
118
|
+
|
|
119
|
+
ret.add_row("Email", escape(str(self.email)))
|
|
120
|
+
ret.add_row("Language", escape(str(self.language)))
|
|
121
|
+
ret.add_row("Mute status", escape(str(self.mute_status)))
|
|
122
|
+
ret.add_row("New scratcher?", str(self.new_scratcher))
|
|
123
|
+
ret.add_row("Banned?", str(self.banned))
|
|
124
|
+
ret.add_row("Has outstanding email confirmation?", str(self.has_outstanding_email_confirmation))
|
|
125
|
+
ret.add_row("Is teacher invitee?", str(self.is_teacher_invitee))
|
|
126
|
+
ret.add_row("Is teacher?", str(self.is_teacher))
|
|
127
|
+
|
|
128
|
+
return ret
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def _username(self) -> str:
|
|
132
|
+
return self.username
|
|
95
133
|
|
|
96
134
|
def __post_init__(self):
|
|
97
135
|
# Info on how the .update method has to fetch the data:
|
|
98
136
|
self.update_function = requests.post
|
|
99
137
|
self.update_api = "https://scratch.mit.edu/session"
|
|
100
138
|
|
|
101
|
-
# Set alternative attributes:
|
|
102
|
-
self._username = self.username # backwards compatibility with v1
|
|
103
|
-
|
|
104
139
|
# Base headers and cookies of every session:
|
|
105
140
|
self._headers = dict(headers)
|
|
106
141
|
self._cookies = {
|
|
@@ -114,11 +149,15 @@ class Session(BaseSiteComponent):
|
|
|
114
149
|
if self.id:
|
|
115
150
|
self._process_session_id()
|
|
116
151
|
|
|
117
|
-
|
|
152
|
+
self._session = self
|
|
153
|
+
|
|
154
|
+
def _update_from_dict(self, data: Union[dict, typed_dicts.SessionDict]):
|
|
118
155
|
# Note: there are a lot more things you can get from this data dict.
|
|
119
156
|
# Maybe it would be a good idea to also store the dict itself?
|
|
120
157
|
# self.data = data
|
|
121
158
|
|
|
159
|
+
data = cast(typed_dicts.SessionDict, data)
|
|
160
|
+
|
|
122
161
|
self.xtoken = data['user']['token']
|
|
123
162
|
self._headers["X-Token"] = self.xtoken
|
|
124
163
|
|
|
@@ -128,11 +167,11 @@ class Session(BaseSiteComponent):
|
|
|
128
167
|
|
|
129
168
|
self.new_scratcher = data["permissions"]["new_scratcher"]
|
|
130
169
|
self.is_teacher = data["permissions"]["educator"]
|
|
170
|
+
self.is_teacher_invitee = data["permissions"]["educator_invitee"]
|
|
131
171
|
|
|
132
|
-
self.mute_status = data["permissions"]["mute_status"]
|
|
172
|
+
self.mute_status: dict = data["permissions"]["mute_status"]
|
|
133
173
|
|
|
134
174
|
self.username = data["user"]["username"]
|
|
135
|
-
self._username = data["user"]["username"]
|
|
136
175
|
self.banned = data["user"]["banned"]
|
|
137
176
|
|
|
138
177
|
if self.banned:
|
|
@@ -149,21 +188,27 @@ class Session(BaseSiteComponent):
|
|
|
149
188
|
data, self.time_created = decode_session_id(self.id)
|
|
150
189
|
|
|
151
190
|
self.username = data["username"]
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
self._user = user.User(_session=self, username=self.username)
|
|
191
|
+
# if self._user:
|
|
192
|
+
# self._user.username = self.username
|
|
193
|
+
# else:
|
|
194
|
+
# self._user = user.User(_session=self, username=self.username)
|
|
157
195
|
|
|
158
|
-
self._user.id = data["_auth_user_id"]
|
|
196
|
+
# self._user.id = data["_auth_user_id"]
|
|
159
197
|
self.xtoken = data["token"]
|
|
160
198
|
self._headers["X-Token"] = self.xtoken
|
|
161
199
|
|
|
162
200
|
# not saving the login ip because it is a security issue, and is not very helpful
|
|
163
201
|
|
|
164
|
-
self.language = data
|
|
202
|
+
self.language = data.get("_language", "en")
|
|
165
203
|
# self._cookies["scratchlanguage"] = self.language
|
|
166
204
|
|
|
205
|
+
def _assert_ocular_auth(self):
|
|
206
|
+
if not self.ocular_token:
|
|
207
|
+
raise ValueError(f"No ocular token supplied for {self}! You can add one by using Session.set_ocular_token(YOUR_TOKEN).")
|
|
208
|
+
|
|
209
|
+
def set_ocular_token(self, token: str):
|
|
210
|
+
self.ocular_token = token
|
|
211
|
+
|
|
167
212
|
def connect_linked_user(self) -> user.User:
|
|
168
213
|
"""
|
|
169
214
|
Gets the user associated with the login / session.
|
|
@@ -180,6 +225,7 @@ class Session(BaseSiteComponent):
|
|
|
180
225
|
|
|
181
226
|
if not cached:
|
|
182
227
|
self._user = self.connect_user(self._username)
|
|
228
|
+
assert self._user is not None
|
|
183
229
|
return self._user
|
|
184
230
|
|
|
185
231
|
def get_linked_user(self) -> user.User:
|
|
@@ -257,6 +303,35 @@ class Session(BaseSiteComponent):
|
|
|
257
303
|
requests.post("https://scratch.mit.edu/accounts/logout/",
|
|
258
304
|
headers=self._headers, cookies=self._cookies)
|
|
259
305
|
|
|
306
|
+
@property
|
|
307
|
+
def ocular_headers(self) -> dict[str, str]:
|
|
308
|
+
self._assert_ocular_auth()
|
|
309
|
+
return {
|
|
310
|
+
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
311
|
+
"(KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
|
|
312
|
+
"referer": "https://ocular.jeffalo.net/",
|
|
313
|
+
"authorization": self.ocular_token
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
def get_ocular_status(self) -> typed_dicts.OcularUserDict:
|
|
317
|
+
# You can use sess.connect_linked_user().ocular_status() but this uses the ocular token to work out the username.
|
|
318
|
+
# In the case the username does not match the session, this would mismatch, and a warning could even be issued
|
|
319
|
+
self._assert_ocular_auth()
|
|
320
|
+
|
|
321
|
+
resp = requests.get("https://my-ocular.jeffalo.net/auth/me", headers=self.ocular_headers).json()
|
|
322
|
+
return resp
|
|
323
|
+
|
|
324
|
+
def set_ocular_status(self, status: Optional[str] = None, color: Optional[str] = None) -> None:
|
|
325
|
+
self._assert_ocular_auth()
|
|
326
|
+
old = self.get_ocular_status()
|
|
327
|
+
payload = {"color": color or old["color"],
|
|
328
|
+
"status": status or old["status"]}
|
|
329
|
+
|
|
330
|
+
assert requests.put(f"https://my-ocular.jeffalo.net/api/user/{old["name"]}",
|
|
331
|
+
json=payload, headers=self.ocular_headers).json() == {
|
|
332
|
+
"ok": "user updated"
|
|
333
|
+
}, f"Error occured on setting ocular status. auth/me response: {old}"
|
|
334
|
+
|
|
260
335
|
def messages(self, *, limit: int = 40, offset: int = 0, date_limit=None, filter_by=None) -> list[activity.Activity]:
|
|
261
336
|
"""
|
|
262
337
|
Returns the messages.
|
|
@@ -377,60 +452,61 @@ class Session(BaseSiteComponent):
|
|
|
377
452
|
)
|
|
378
453
|
return commons.parse_object_list(data, project.Project, self)
|
|
379
454
|
|
|
380
|
-
"""
|
|
381
|
-
These methods are disabled because it is unclear if there is any case in which the response is not empty.
|
|
382
455
|
def shared_by_followed_users(self, *, limit=40, offset=0) -> list[project.Project]:
|
|
383
|
-
|
|
456
|
+
"""
|
|
384
457
|
Returns the "Projects by Scratchers I'm following" section (frontpage).
|
|
385
|
-
This section is only visible to old accounts (
|
|
458
|
+
This section is only visible to old accounts (until ~2018).
|
|
386
459
|
For newer users, this method will always return an empty list.
|
|
387
460
|
|
|
388
461
|
Returns:
|
|
389
|
-
list<scratchattach.project.Project>: List that contains all "Projects
|
|
462
|
+
list<scratchattach.project.Project>: List that contains all "Projects by Scratchers I'm following"
|
|
390
463
|
entries as Project objects
|
|
391
|
-
|
|
464
|
+
"""
|
|
392
465
|
data = commons.api_iterative(
|
|
393
466
|
f"https://api.scratch.mit.edu/users/{self._username}/following/users/projects",
|
|
394
|
-
limit = limit, offset = offset,
|
|
467
|
+
limit = limit, offset = offset, _headers = self._headers, cookies = self._cookies
|
|
395
468
|
)
|
|
396
|
-
|
|
469
|
+
ret = commons.parse_object_list(data, project.Project, self)
|
|
470
|
+
if not ret:
|
|
471
|
+
warnings.warn(f"`shared_by_followed_users` got empty list `[]`. Note that this method is not supported for "
|
|
472
|
+
f"accounts made after 2018.")
|
|
473
|
+
return ret
|
|
397
474
|
|
|
398
475
|
def in_followed_studios(self, *, limit=40, offset=0) -> list['project.Project']:
|
|
399
|
-
|
|
476
|
+
"""
|
|
400
477
|
Returns the "Projects in studios I'm following" section (frontpage).
|
|
401
|
-
This section is only visible to old accounts (
|
|
478
|
+
This section is only visible to old accounts (until ~2018)
|
|
402
479
|
For newer users, this method will always return an empty list.
|
|
403
480
|
|
|
404
481
|
Returns:
|
|
405
|
-
list<scratchattach.project.Project>: List that contains all "Projects
|
|
482
|
+
list<scratchattach.project.Project>: List that contains all "Projects in studios I'm following" section"
|
|
406
483
|
entries as Project objects
|
|
407
|
-
|
|
484
|
+
"""
|
|
408
485
|
data = commons.api_iterative(
|
|
409
486
|
f"https://api.scratch.mit.edu/users/{self._username}/following/studios/projects",
|
|
410
|
-
limit = limit, offset = offset,
|
|
487
|
+
limit = limit, offset = offset, _headers=self._headers, cookies = self._cookies
|
|
411
488
|
)
|
|
412
|
-
|
|
489
|
+
ret = commons.parse_object_list(data, project.Project, self)
|
|
490
|
+
if not ret:
|
|
491
|
+
warnings.warn(f"`in_followed_studios` got empty list `[]`. Note that this method is not supported for "
|
|
492
|
+
f"accounts made after 2018.")
|
|
493
|
+
return ret
|
|
413
494
|
|
|
414
495
|
# -- Project JSON editing capabilities ---
|
|
415
496
|
# These are set to staticmethods right now, but they probably should not be
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
pb =
|
|
419
|
-
pb.from_json(empty_project_json)
|
|
497
|
+
def connect_empty_project_pb(self) -> editor.Project:
|
|
498
|
+
pb = editor.Project.from_json(empty_project_json) # in the future, ideally just init a new editor.Project, instead of loading an empty one
|
|
499
|
+
pb._session = self
|
|
420
500
|
return pb
|
|
421
501
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
pb =
|
|
425
|
-
pb.from_json(project_json)
|
|
502
|
+
def connect_pb_from_dict(self, project_json: dict) -> editor.Project:
|
|
503
|
+
pb = editor.Project.from_json(project_json)
|
|
504
|
+
pb._session = self
|
|
426
505
|
return pb
|
|
427
506
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
pb =
|
|
431
|
-
# noinspection PyProtectedMember
|
|
432
|
-
# _load_sb3_file starts with an underscore
|
|
433
|
-
pb.from_json(project_json_capabilities._load_sb3_file(path_to_file))
|
|
507
|
+
def connect_pb_from_file(self, path_to_file) -> editor.Project:
|
|
508
|
+
pb = editor.Project.from_sb3(path_to_file)
|
|
509
|
+
pb._session = self
|
|
434
510
|
return pb
|
|
435
511
|
|
|
436
512
|
@staticmethod
|
|
@@ -488,9 +564,11 @@ class Session(BaseSiteComponent):
|
|
|
488
564
|
Returns:
|
|
489
565
|
list<scratchattach.project.Project>: List that contains the search results.
|
|
490
566
|
"""
|
|
567
|
+
query = f"&q={query}" if query else ""
|
|
568
|
+
|
|
491
569
|
response = commons.api_iterative(
|
|
492
570
|
f"https://api.scratch.mit.edu/search/projects", limit=limit, offset=offset,
|
|
493
|
-
add_params=f"&language={language}&mode={mode}
|
|
571
|
+
add_params=f"&language={language}&mode={mode}{query}")
|
|
494
572
|
return commons.parse_object_list(response, project.Project, self)
|
|
495
573
|
|
|
496
574
|
def explore_projects(self, *, query: str = "*", mode: str = "trending", language: str = "en", limit: int = 40,
|
|
@@ -518,9 +596,8 @@ class Session(BaseSiteComponent):
|
|
|
518
596
|
|
|
519
597
|
def search_studios(self, *, query: str = "", mode: str = "trending", language: str = "en", limit: int = 40,
|
|
520
598
|
offset: int = 0) -> list[studio.Studio]:
|
|
521
|
-
#if not query:
|
|
522
|
-
# raise ValueError("The query can't be empty for search")
|
|
523
599
|
query = f"&q={query}" if query else ""
|
|
600
|
+
|
|
524
601
|
response = commons.api_iterative(
|
|
525
602
|
f"https://api.scratch.mit.edu/search/studios", limit=limit, offset=offset,
|
|
526
603
|
add_params=f"&language={language}&mode={mode}{query}")
|
|
@@ -528,8 +605,6 @@ class Session(BaseSiteComponent):
|
|
|
528
605
|
|
|
529
606
|
def explore_studios(self, *, query: str = "", mode: str = "trending", language: str = "en", limit: int = 40,
|
|
530
607
|
offset: int = 0) -> list[studio.Studio]:
|
|
531
|
-
#if not query:
|
|
532
|
-
# raise ValueError("The query can't be empty for explore")
|
|
533
608
|
query = f"&q={query}" if query else ""
|
|
534
609
|
response = commons.api_iterative(
|
|
535
610
|
f"https://api.scratch.mit.edu/explore/studios", limit=limit, offset=offset,
|
|
@@ -571,7 +646,7 @@ class Session(BaseSiteComponent):
|
|
|
571
646
|
To prevent accidental spam, a rate limit (5 studios per minute) is implemented for this function.
|
|
572
647
|
"""
|
|
573
648
|
enforce_ratelimit("create_scratch_studio", "creating Scratch studios")
|
|
574
|
-
|
|
649
|
+
|
|
575
650
|
if self.new_scratcher:
|
|
576
651
|
raise exceptions.Unauthorized(f"\nNew scratchers (like {self.username}) cannot create studios.")
|
|
577
652
|
|
|
@@ -697,7 +772,7 @@ class Session(BaseSiteComponent):
|
|
|
697
772
|
raise exceptions.FetchError()
|
|
698
773
|
|
|
699
774
|
def mystuff_classes(self, mode: str = "Last created", page: Optional[int] = None) -> list[classroom.Classroom]:
|
|
700
|
-
if self.is_teacher
|
|
775
|
+
if not self.is_teacher:
|
|
701
776
|
self.update()
|
|
702
777
|
|
|
703
778
|
if not self.is_teacher:
|
|
@@ -775,7 +850,7 @@ class Session(BaseSiteComponent):
|
|
|
775
850
|
|
|
776
851
|
# --- Connect classes inheriting from BaseCloud ---
|
|
777
852
|
|
|
778
|
-
|
|
853
|
+
|
|
779
854
|
@overload
|
|
780
855
|
def connect_cloud(self, project_id, *, cloud_class: type[T]) -> T:
|
|
781
856
|
"""
|
|
@@ -790,7 +865,7 @@ class Session(BaseSiteComponent):
|
|
|
790
865
|
Returns: Type[scratchattach.cloud._base.BaseCloud]: An object representing the cloud of a project. Can be of any
|
|
791
866
|
class inheriting from BaseCloud.
|
|
792
867
|
"""
|
|
793
|
-
|
|
868
|
+
|
|
794
869
|
@overload
|
|
795
870
|
def connect_cloud(self, project_id) -> cloud.ScratchCloud:
|
|
796
871
|
"""
|
|
@@ -1023,6 +1098,12 @@ sess
|
|
|
1023
1098
|
except Exception as e:
|
|
1024
1099
|
raise exceptions.ScrapeError(str(e))
|
|
1025
1100
|
|
|
1101
|
+
def connect_featured(self) -> other_apis.FeaturedData:
|
|
1102
|
+
"""
|
|
1103
|
+
Request and return connected featured projects and studios from the front page.
|
|
1104
|
+
"""
|
|
1105
|
+
return other_apis.get_featured_data(self)
|
|
1106
|
+
|
|
1026
1107
|
# --- Connect classes inheriting from BaseEventHandler ---
|
|
1027
1108
|
|
|
1028
1109
|
def connect_message_events(self, *, update_interval=2) -> message_events.MessageEvents:
|
|
@@ -1036,10 +1117,10 @@ sess
|
|
|
1036
1117
|
def get_session_string(self) -> str:
|
|
1037
1118
|
assert self.session_string
|
|
1038
1119
|
return self.session_string
|
|
1039
|
-
|
|
1120
|
+
|
|
1040
1121
|
def get_headers(self) -> dict[str, str]:
|
|
1041
1122
|
return self._headers
|
|
1042
|
-
|
|
1123
|
+
|
|
1043
1124
|
def get_cookies(self) -> dict[str, str]:
|
|
1044
1125
|
return self._cookies
|
|
1045
1126
|
|
|
@@ -1052,7 +1133,7 @@ def decode_session_id(session_id: str) -> tuple[dict[str, str], datetime.datetim
|
|
|
1052
1133
|
Session id is in the format:
|
|
1053
1134
|
<p1: long base64 string>:<p2: short base64 string>:<p3: medium base64 string>
|
|
1054
1135
|
|
|
1055
|
-
p1 contains a base64
|
|
1136
|
+
p1 contains a base64 JSON string (if it starts with `.`, then it is zlib compressed)
|
|
1056
1137
|
p2 is a base 62 encoded timestamp
|
|
1057
1138
|
p3 might be a `synchronous signature` for the first 2 parts (might be useless for us)
|
|
1058
1139
|
|
|
@@ -1067,10 +1148,13 @@ def decode_session_id(session_id: str) -> tuple[dict[str, str], datetime.datetim
|
|
|
1067
1148
|
- django_timezone
|
|
1068
1149
|
- _auth_user_hash
|
|
1069
1150
|
"""
|
|
1070
|
-
p1, p2,
|
|
1151
|
+
p1, p2, _ = session_id.split(':')
|
|
1152
|
+
p1_bytes = base64.urlsafe_b64decode(p1 + "==")
|
|
1153
|
+
if p1.startswith('".'):
|
|
1154
|
+
p1_bytes = zlib.decompress(p1_bytes)
|
|
1071
1155
|
|
|
1072
1156
|
return (
|
|
1073
|
-
json.loads(
|
|
1157
|
+
json.loads(p1_bytes),
|
|
1074
1158
|
datetime.datetime.fromtimestamp(commons.b62_decode(p2))
|
|
1075
1159
|
)
|
|
1076
1160
|
|
|
@@ -1133,7 +1217,7 @@ def login_by_id(session_id: str, *, username: Optional[str] = None, password: Op
|
|
|
1133
1217
|
else:
|
|
1134
1218
|
session_string = None
|
|
1135
1219
|
|
|
1136
|
-
_session = Session(id=session_id, username=username, session_string=session_string)
|
|
1220
|
+
_session = Session(id=session_id, username=username or "", session_string=session_string)
|
|
1137
1221
|
if xtoken is not None:
|
|
1138
1222
|
# xtoken is retrievable from session id, so the most we can do is assert equality
|
|
1139
1223
|
assert xtoken == _session.xtoken
|
|
@@ -1169,6 +1253,7 @@ def login(username, password, *, timeout=10) -> Session:
|
|
|
1169
1253
|
"https://scratch.mit.edu/login/", json={"username": username, "password": password}, headers=_headers,
|
|
1170
1254
|
timeout=timeout
|
|
1171
1255
|
)
|
|
1256
|
+
|
|
1172
1257
|
try:
|
|
1173
1258
|
result = re.search('"(.*)"', request.headers["Set-Cookie"])
|
|
1174
1259
|
assert result is not None
|