scratchattach 2.1.15b0__py3-none-any.whl → 3.0.0b0__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/__init__.py +14 -6
- scratchattach/__main__.py +93 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b0.dist-info}/METADATA +7 -11
- scratchattach-3.0.0b0.dist-info/RECORD +8 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b0.dist-info}/WHEEL +1 -1
- scratchattach-3.0.0b0.dist-info/entry_points.txt +2 -0
- scratchattach/cloud/__init__.py +0 -2
- scratchattach/cloud/_base.py +0 -458
- scratchattach/cloud/cloud.py +0 -183
- scratchattach/editor/__init__.py +0 -21
- scratchattach/editor/asset.py +0 -253
- scratchattach/editor/backpack_json.py +0 -117
- scratchattach/editor/base.py +0 -193
- scratchattach/editor/block.py +0 -579
- scratchattach/editor/blockshape.py +0 -357
- scratchattach/editor/build_defaulting.py +0 -51
- scratchattach/editor/code_translation/__init__.py +0 -0
- scratchattach/editor/code_translation/parse.py +0 -177
- scratchattach/editor/comment.py +0 -80
- scratchattach/editor/commons.py +0 -273
- scratchattach/editor/extension.py +0 -50
- scratchattach/editor/field.py +0 -99
- scratchattach/editor/inputs.py +0 -135
- scratchattach/editor/meta.py +0 -114
- scratchattach/editor/monitor.py +0 -183
- scratchattach/editor/mutation.py +0 -324
- scratchattach/editor/pallete.py +0 -90
- scratchattach/editor/prim.py +0 -170
- scratchattach/editor/project.py +0 -279
- scratchattach/editor/sprite.py +0 -599
- scratchattach/editor/twconfig.py +0 -114
- scratchattach/editor/vlb.py +0 -134
- scratchattach/eventhandlers/__init__.py +0 -0
- scratchattach/eventhandlers/_base.py +0 -100
- scratchattach/eventhandlers/cloud_events.py +0 -110
- scratchattach/eventhandlers/cloud_recorder.py +0 -26
- scratchattach/eventhandlers/cloud_requests.py +0 -459
- scratchattach/eventhandlers/cloud_server.py +0 -246
- scratchattach/eventhandlers/cloud_storage.py +0 -136
- scratchattach/eventhandlers/combine.py +0 -30
- scratchattach/eventhandlers/filterbot.py +0 -161
- scratchattach/eventhandlers/message_events.py +0 -42
- scratchattach/other/__init__.py +0 -0
- scratchattach/other/other_apis.py +0 -284
- scratchattach/other/project_json_capabilities.py +0 -475
- scratchattach/site/__init__.py +0 -0
- scratchattach/site/_base.py +0 -66
- scratchattach/site/activity.py +0 -382
- scratchattach/site/alert.py +0 -227
- scratchattach/site/backpack_asset.py +0 -118
- scratchattach/site/browser_cookie3_stub.py +0 -17
- scratchattach/site/browser_cookies.py +0 -61
- scratchattach/site/classroom.py +0 -447
- scratchattach/site/cloud_activity.py +0 -107
- scratchattach/site/comment.py +0 -242
- scratchattach/site/forum.py +0 -432
- scratchattach/site/project.py +0 -826
- scratchattach/site/session.py +0 -1238
- scratchattach/site/studio.py +0 -611
- scratchattach/site/user.py +0 -956
- scratchattach/utils/__init__.py +0 -0
- scratchattach/utils/commons.py +0 -255
- scratchattach/utils/encoder.py +0 -158
- scratchattach/utils/enums.py +0 -236
- scratchattach/utils/exceptions.py +0 -243
- scratchattach/utils/requests.py +0 -93
- scratchattach-2.1.15b0.dist-info/RECORD +0 -66
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b0.dist-info}/licenses/LICENSE +0 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b0.dist-info}/top_level.txt +0 -0
scratchattach/site/project.py
DELETED
|
@@ -1,826 +0,0 @@
|
|
|
1
|
-
"""Project and PartialProject classes"""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
import random
|
|
6
|
-
import base64
|
|
7
|
-
import time
|
|
8
|
-
import zipfile
|
|
9
|
-
from io import BytesIO
|
|
10
|
-
|
|
11
|
-
from typing import Any
|
|
12
|
-
from . import user, comment, studio
|
|
13
|
-
from scratchattach.utils import exceptions
|
|
14
|
-
from scratchattach.utils import commons
|
|
15
|
-
from scratchattach.utils.commons import empty_project_json, headers
|
|
16
|
-
from ._base import BaseSiteComponent
|
|
17
|
-
from scratchattach.other.project_json_capabilities import ProjectBody
|
|
18
|
-
from scratchattach.utils.requests import requests
|
|
19
|
-
|
|
20
|
-
CREATE_PROJECT_USES: list[float] = []
|
|
21
|
-
|
|
22
|
-
class PartialProject(BaseSiteComponent):
|
|
23
|
-
"""
|
|
24
|
-
Represents an unshared Scratch project that can't be accessed.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
def __str__(self):
|
|
28
|
-
return f"Unshared project with id {self.id}"
|
|
29
|
-
|
|
30
|
-
def __init__(self, **entries):
|
|
31
|
-
|
|
32
|
-
# Info on how the .update method has to fetch the data:
|
|
33
|
-
self.update_function = requests.get
|
|
34
|
-
self.update_api = f"https://api.scratch.mit.edu/projects/{entries['id']}"
|
|
35
|
-
|
|
36
|
-
# Set attributes every Project object needs to have:
|
|
37
|
-
self._session = None
|
|
38
|
-
self.project_token = None
|
|
39
|
-
self.id = 0
|
|
40
|
-
self.instructions = None
|
|
41
|
-
self.parent_title = None
|
|
42
|
-
|
|
43
|
-
# Update attributes from entries dict:
|
|
44
|
-
self.__dict__.update(entries)
|
|
45
|
-
|
|
46
|
-
# Headers and cookies:
|
|
47
|
-
if self._session is None:
|
|
48
|
-
self._headers = headers
|
|
49
|
-
self._cookies = {}
|
|
50
|
-
else:
|
|
51
|
-
self._headers = self._session._headers
|
|
52
|
-
self._cookies = self._session._cookies
|
|
53
|
-
|
|
54
|
-
# Headers for operations that require accept and Content-Type fields:
|
|
55
|
-
self._json_headers = dict(self._headers)
|
|
56
|
-
self._json_headers["accept"] = "application/json"
|
|
57
|
-
self._json_headers["Content-Type"] = "application/json"
|
|
58
|
-
|
|
59
|
-
def _update_from_dict(self, data):
|
|
60
|
-
try:
|
|
61
|
-
self.id = int(data["id"])
|
|
62
|
-
except KeyError:
|
|
63
|
-
pass
|
|
64
|
-
try: self.url = "https://scratch.mit.edu/projects/" + str(self.id)
|
|
65
|
-
except Exception: pass
|
|
66
|
-
try: self.author_name = data["author"]["username"]
|
|
67
|
-
except Exception: pass
|
|
68
|
-
try: self.author_name = data["username"]
|
|
69
|
-
except Exception: pass
|
|
70
|
-
try: self.comments_allowed = data["comments_allowed"]
|
|
71
|
-
except Exception: pass
|
|
72
|
-
try: self.instructions = data["instructions"]
|
|
73
|
-
except Exception: pass
|
|
74
|
-
try: self.notes = data["description"]
|
|
75
|
-
except Exception: pass
|
|
76
|
-
try: self.created = data["history"]["created"]
|
|
77
|
-
except Exception: pass
|
|
78
|
-
try: self.last_modified = data["history"]["modified"]
|
|
79
|
-
except Exception: pass
|
|
80
|
-
try: self.share_date = data["history"]["shared"]
|
|
81
|
-
except Exception: pass
|
|
82
|
-
try: self.thumbnail_url = data["image"]
|
|
83
|
-
except Exception: pass
|
|
84
|
-
try:
|
|
85
|
-
self.remix_parent = data["remix"]["parent"]
|
|
86
|
-
self.remix_root = data["remix"]["root"]
|
|
87
|
-
except Exception:
|
|
88
|
-
self.remix_parent = None
|
|
89
|
-
self.remix_root = None
|
|
90
|
-
try: self.favorites = data["stats"]["favorites"]
|
|
91
|
-
except Exception: pass
|
|
92
|
-
try: self.loves = data["stats"]["loves"]
|
|
93
|
-
except Exception: pass
|
|
94
|
-
try: self.remix_count = data["stats"]["remixes"]
|
|
95
|
-
except Exception: pass
|
|
96
|
-
try: self.views = data["stats"]["views"]
|
|
97
|
-
except Exception: pass
|
|
98
|
-
try: self.title = data["title"]
|
|
99
|
-
except Exception: pass
|
|
100
|
-
try:
|
|
101
|
-
self.project_token = data["project_token"]
|
|
102
|
-
except Exception:
|
|
103
|
-
self.project_token = None
|
|
104
|
-
if "code" in data: # Project is unshared -> return false
|
|
105
|
-
return False
|
|
106
|
-
return True
|
|
107
|
-
|
|
108
|
-
@property
|
|
109
|
-
def embed_url(self):
|
|
110
|
-
"""
|
|
111
|
-
Returns:
|
|
112
|
-
the url of the embed of the project
|
|
113
|
-
"""
|
|
114
|
-
return f"{self.url}/embed"
|
|
115
|
-
|
|
116
|
-
def remixes(self, *, limit=40, offset=0):
|
|
117
|
-
"""
|
|
118
|
-
Returns:
|
|
119
|
-
list<scratchattach.project.Project>: A list containing the remixes of the project, each project is represented by a Project object.
|
|
120
|
-
"""
|
|
121
|
-
response = commons.api_iterative(
|
|
122
|
-
f"https://api.scratch.mit.edu/projects/{self.id}/remixes", limit=limit, offset=offset)
|
|
123
|
-
return commons.parse_object_list(response, Project, self._session)
|
|
124
|
-
|
|
125
|
-
def is_shared(self):
|
|
126
|
-
"""
|
|
127
|
-
Returns:
|
|
128
|
-
boolean: Returns whether the project is currently shared
|
|
129
|
-
"""
|
|
130
|
-
p = get_project(self.id)
|
|
131
|
-
return isinstance(p, Project)
|
|
132
|
-
|
|
133
|
-
def raw_json_or_empty(self) -> dict[str, Any]:
|
|
134
|
-
return empty_project_json
|
|
135
|
-
|
|
136
|
-
def create_remix(self, *, title=None, project_json=None): # not working
|
|
137
|
-
"""
|
|
138
|
-
Creates a project on the Scratch website.
|
|
139
|
-
|
|
140
|
-
Warning:
|
|
141
|
-
Don't spam this method - it WILL get you banned from Scratch.
|
|
142
|
-
To prevent accidental spam, a rate limit (5 projects per minute) is implemented for this function.
|
|
143
|
-
"""
|
|
144
|
-
self._assert_auth()
|
|
145
|
-
|
|
146
|
-
if title is None:
|
|
147
|
-
if "title" in self.__dict__:
|
|
148
|
-
title = self.title+" remix"
|
|
149
|
-
else:
|
|
150
|
-
title = " remix"
|
|
151
|
-
if project_json is None:
|
|
152
|
-
project_json = self.raw_json_or_empty()
|
|
153
|
-
|
|
154
|
-
if len(CREATE_PROJECT_USES) < 5:
|
|
155
|
-
CREATE_PROJECT_USES.insert(0, time.time())
|
|
156
|
-
else:
|
|
157
|
-
if CREATE_PROJECT_USES[-1] < time.time() - 300:
|
|
158
|
-
CREATE_PROJECT_USES.pop()
|
|
159
|
-
else:
|
|
160
|
-
raise exceptions.BadRequest("Rate limit for remixing Scratch projects exceeded.\nThis rate limit is enforced by scratchattach, not by the Scratch API.\nFor security reasons, it cannot be turned off.\n\nDon't spam-create projects, it WILL get you banned.")
|
|
161
|
-
return
|
|
162
|
-
CREATE_PROJECT_USES.insert(0, time.time())
|
|
163
|
-
|
|
164
|
-
params = {
|
|
165
|
-
'is_remix': '1',
|
|
166
|
-
'original_id': self.id,
|
|
167
|
-
'title': title,
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
response = requests.post('https://projects.scratch.mit.edu/', params=params, cookies=self._cookies, headers=self._headers, json=project_json).json()
|
|
171
|
-
_project = self._session.connect_project(response["content-name"])
|
|
172
|
-
_project.parent_title = base64.b64decode(response['content-title']).decode('utf-8').split(' remix')[0]
|
|
173
|
-
return _project
|
|
174
|
-
|
|
175
|
-
def load_description(self):
|
|
176
|
-
"""
|
|
177
|
-
Gets the instructions of the unshared project. Requires authentication.
|
|
178
|
-
|
|
179
|
-
Warning:
|
|
180
|
-
It's unclear if Scratch allows using this method. This method will create a remix of the unshared project using your account.
|
|
181
|
-
"""
|
|
182
|
-
self._assert_auth()
|
|
183
|
-
new_project = self.create_remix(project_json=empty_project_json)
|
|
184
|
-
self.instructions = new_project.instructions
|
|
185
|
-
self.title = new_project.parent_title
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
class Project(PartialProject):
|
|
189
|
-
"""
|
|
190
|
-
Represents a Scratch project.
|
|
191
|
-
|
|
192
|
-
Attributes:
|
|
193
|
-
|
|
194
|
-
:.id: The project id
|
|
195
|
-
|
|
196
|
-
:.url: The project url
|
|
197
|
-
|
|
198
|
-
:.title:
|
|
199
|
-
|
|
200
|
-
:.author_name: The username of the author
|
|
201
|
-
|
|
202
|
-
:.comments_allowed: boolean that is True if comments are enabled
|
|
203
|
-
|
|
204
|
-
:.instructions:
|
|
205
|
-
|
|
206
|
-
:.notes: The 'Notes and Credits' section
|
|
207
|
-
|
|
208
|
-
:.created: The date of the project creation
|
|
209
|
-
|
|
210
|
-
:.last_modified: The date when the project was modified the last time
|
|
211
|
-
|
|
212
|
-
:.share_date:
|
|
213
|
-
|
|
214
|
-
:.thumbnail_url:
|
|
215
|
-
|
|
216
|
-
:.remix_parent:
|
|
217
|
-
|
|
218
|
-
:.remix_root:
|
|
219
|
-
|
|
220
|
-
:.loves: The project's love count
|
|
221
|
-
|
|
222
|
-
:.favorites: The project's favorite count
|
|
223
|
-
|
|
224
|
-
:.remix_count: The number of remixes
|
|
225
|
-
|
|
226
|
-
:.views: The view count
|
|
227
|
-
|
|
228
|
-
:.project_token: The project token (required to access the project json)
|
|
229
|
-
|
|
230
|
-
:.update(): Updates the attributes
|
|
231
|
-
"""
|
|
232
|
-
|
|
233
|
-
def __str__(self):
|
|
234
|
-
return str(self.title)
|
|
235
|
-
|
|
236
|
-
def _assert_permission(self):
|
|
237
|
-
self._assert_auth()
|
|
238
|
-
if self._session._username != self.author_name:
|
|
239
|
-
raise exceptions.Unauthorized(
|
|
240
|
-
"You need to be authenticated as the profile owner to do this.")
|
|
241
|
-
|
|
242
|
-
def load_description(self):
|
|
243
|
-
# Overrides the load_description method that exists for unshared projects
|
|
244
|
-
self.update()
|
|
245
|
-
|
|
246
|
-
def download(self, *, filename=None, dir="."):
|
|
247
|
-
"""
|
|
248
|
-
Downloads the project json to the given directory.
|
|
249
|
-
|
|
250
|
-
Args:
|
|
251
|
-
filename (str): The name that will be given to the downloaded file.
|
|
252
|
-
dir (str): The path of the directory the file will be saved in.
|
|
253
|
-
"""
|
|
254
|
-
try:
|
|
255
|
-
if filename is None:
|
|
256
|
-
filename = str(self.id)
|
|
257
|
-
if not (dir.endswith("/") or dir.endswith("\\")):
|
|
258
|
-
dir += "/"
|
|
259
|
-
self.update()
|
|
260
|
-
response = requests.get(
|
|
261
|
-
f"https://projects.scratch.mit.edu/{self.id}?token={self.project_token}",
|
|
262
|
-
timeout=10,
|
|
263
|
-
)
|
|
264
|
-
filename = filename.removesuffix(".sb3")
|
|
265
|
-
with open(f"{dir}{filename}.sb3", "wb") as f:
|
|
266
|
-
f.write(response.content)
|
|
267
|
-
except Exception:
|
|
268
|
-
raise (
|
|
269
|
-
exceptions.FetchError(
|
|
270
|
-
"Method only works for projects created with Scratch 3"
|
|
271
|
-
)
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
def get_json(self) -> str:
|
|
275
|
-
"""
|
|
276
|
-
Downloads the project json and returns it as a string
|
|
277
|
-
"""
|
|
278
|
-
try:
|
|
279
|
-
self.update()
|
|
280
|
-
response = requests.get(
|
|
281
|
-
f"https://projects.scratch.mit.edu/{self.id}?token={self.project_token}",
|
|
282
|
-
timeout=10,
|
|
283
|
-
)
|
|
284
|
-
return response.text
|
|
285
|
-
|
|
286
|
-
except Exception:
|
|
287
|
-
raise (
|
|
288
|
-
exceptions.FetchError(
|
|
289
|
-
"Method only works for projects created with Scratch 3"
|
|
290
|
-
)
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
def body(self):
|
|
294
|
-
"""
|
|
295
|
-
Method only works for project created with Scratch 3.
|
|
296
|
-
|
|
297
|
-
Returns:
|
|
298
|
-
scratchattach.ProjectBody: The contents of the project as ProjectBody object
|
|
299
|
-
"""
|
|
300
|
-
raw_json = self.raw_json()
|
|
301
|
-
pb = ProjectBody()
|
|
302
|
-
pb.from_json(raw_json)
|
|
303
|
-
return pb
|
|
304
|
-
|
|
305
|
-
def raw_json(self):
|
|
306
|
-
"""
|
|
307
|
-
Method only works for project created with Scratch 3.
|
|
308
|
-
|
|
309
|
-
Returns:
|
|
310
|
-
dict: The raw project JSON as decoded Python dictionary
|
|
311
|
-
"""
|
|
312
|
-
try:
|
|
313
|
-
self.update()
|
|
314
|
-
|
|
315
|
-
except Exception as e:
|
|
316
|
-
raise (
|
|
317
|
-
exceptions.FetchError(
|
|
318
|
-
f"You're not authorized for accessing {self}.\nException: {e}"
|
|
319
|
-
)
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
with requests.no_error_handling():
|
|
323
|
-
resp = requests.get(
|
|
324
|
-
f"https://projects.scratch.mit.edu/{self.id}?token={self.project_token}",
|
|
325
|
-
timeout=10,
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
try:
|
|
329
|
-
return resp.json()
|
|
330
|
-
except json.JSONDecodeError:
|
|
331
|
-
# I am not aware of any cases where this will not be a zip file
|
|
332
|
-
# in the future, cache a projectbody object here and just return the json
|
|
333
|
-
# that is fetched from there to not waste existing asset data from this zip file
|
|
334
|
-
|
|
335
|
-
with zipfile.ZipFile(BytesIO(resp.content)) as zipf:
|
|
336
|
-
return json.load(zipf.open("project.json"))
|
|
337
|
-
|
|
338
|
-
def raw_json_or_empty(self):
|
|
339
|
-
return self.raw_json()
|
|
340
|
-
|
|
341
|
-
def creator_agent(self):
|
|
342
|
-
"""
|
|
343
|
-
Method only works for project created with Scratch 3.
|
|
344
|
-
|
|
345
|
-
Returns:
|
|
346
|
-
str: The user agent of the browser that this project was saved with.
|
|
347
|
-
"""
|
|
348
|
-
return self.raw_json()["meta"]["agent"]
|
|
349
|
-
|
|
350
|
-
def author(self):
|
|
351
|
-
"""
|
|
352
|
-
Returns:
|
|
353
|
-
scratchattach.user.User: An object representing the Scratch user who created this project.
|
|
354
|
-
"""
|
|
355
|
-
return self._make_linked_object("username", self.author_name, user.User, exceptions.UserNotFound)
|
|
356
|
-
|
|
357
|
-
def studios(self, *, limit=40, offset=0):
|
|
358
|
-
"""
|
|
359
|
-
Returns:
|
|
360
|
-
list<scratchattach.studio.Studio>: A list containing the studios this project is in, each studio is represented by a Studio object.
|
|
361
|
-
"""
|
|
362
|
-
response = commons.api_iterative(
|
|
363
|
-
f"https://api.scratch.mit.edu/users/{self.author_name}/projects/{self.id}/studios", limit=limit, offset=offset, add_params=f"&cachebust={random.randint(0,9999)}")
|
|
364
|
-
return commons.parse_object_list(response, studio.Studio, self._session)
|
|
365
|
-
|
|
366
|
-
def comments(self, *, limit=40, offset=0) -> list['comment.Comment']:
|
|
367
|
-
"""
|
|
368
|
-
Returns the comments posted on the project (except for replies. To get replies use :meth:`scratchattach.project.Project.comment_replies`).
|
|
369
|
-
|
|
370
|
-
Keyword Arguments:
|
|
371
|
-
page: The page of the comments that should be returned.
|
|
372
|
-
limit: Max. amount of returned comments.
|
|
373
|
-
|
|
374
|
-
Returns:
|
|
375
|
-
list<scratchattach.comment.Comment>: A list containing the requested comments as Comment objects.
|
|
376
|
-
"""
|
|
377
|
-
|
|
378
|
-
response = commons.api_iterative(
|
|
379
|
-
f"https://api.scratch.mit.edu/users/{self.author_name}/projects/{self.id}/comments/", limit=limit, offset=offset, add_params=f"&cachebust={random.randint(0,9999)}")
|
|
380
|
-
for i in response:
|
|
381
|
-
i["source"] = "project"
|
|
382
|
-
i["source_id"] = self.id
|
|
383
|
-
return commons.parse_object_list(response, comment.Comment, self._session)
|
|
384
|
-
|
|
385
|
-
def comment_replies(self, *, comment_id, limit=40, offset=0):
|
|
386
|
-
response = commons.api_iterative(
|
|
387
|
-
f"https://api.scratch.mit.edu/users/{self.author_name}/projects/{self.id}/comments/{comment_id}/replies/", limit=limit, offset=offset, add_params=f"&cachebust={random.randint(0,9999)}")
|
|
388
|
-
for x in response:
|
|
389
|
-
x["parent_id"] = comment_id
|
|
390
|
-
x["source"] = "project"
|
|
391
|
-
x["source_id"] = self.id
|
|
392
|
-
return commons.parse_object_list(response, comment.Comment, self._session)
|
|
393
|
-
|
|
394
|
-
def comment_by_id(self, comment_id):
|
|
395
|
-
"""
|
|
396
|
-
Returns:
|
|
397
|
-
scratchattach.comments.Comment: A Comment object representing the requested comment.
|
|
398
|
-
"""
|
|
399
|
-
r = requests.get(
|
|
400
|
-
f"https://api.scratch.mit.edu/users/{self.author_name}/projects/{self.id}/comments/{comment_id}",
|
|
401
|
-
headers=self._headers,
|
|
402
|
-
cookies=self._cookies
|
|
403
|
-
).json()
|
|
404
|
-
if r is None:
|
|
405
|
-
raise exceptions.CommentNotFound()
|
|
406
|
-
_comment = comment.Comment(id=r["id"], _session=self._session, source="project", source_id=self.id)
|
|
407
|
-
_comment._update_from_dict(r)
|
|
408
|
-
return _comment
|
|
409
|
-
|
|
410
|
-
def love(self):
|
|
411
|
-
"""
|
|
412
|
-
Posts a love on the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
413
|
-
"""
|
|
414
|
-
self._assert_auth()
|
|
415
|
-
r = requests.post(
|
|
416
|
-
f"https://api.scratch.mit.edu/proxy/projects/{self.id}/loves/user/{self._session._username}",
|
|
417
|
-
headers=self._headers,
|
|
418
|
-
cookies=self._cookies,
|
|
419
|
-
).json()
|
|
420
|
-
if "userLove" in r:
|
|
421
|
-
if r["userLove"] is False:
|
|
422
|
-
self.love()
|
|
423
|
-
else:
|
|
424
|
-
raise exceptions.APIError(str(r))
|
|
425
|
-
|
|
426
|
-
def unlove(self):
|
|
427
|
-
"""
|
|
428
|
-
Removes the love from this project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
429
|
-
"""
|
|
430
|
-
self._assert_auth()
|
|
431
|
-
r = requests.delete(
|
|
432
|
-
f"https://api.scratch.mit.edu/proxy/projects/{self.id}/loves/user/{self._session._username}",
|
|
433
|
-
headers=self._headers,
|
|
434
|
-
cookies=self._cookies,
|
|
435
|
-
).json()
|
|
436
|
-
if "userLove" in r:
|
|
437
|
-
if r["userLove"] is True:
|
|
438
|
-
self.unlove()
|
|
439
|
-
else:
|
|
440
|
-
raise exceptions.APIError(str(r))
|
|
441
|
-
|
|
442
|
-
def favorite(self):
|
|
443
|
-
"""
|
|
444
|
-
Posts a favorite on the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
445
|
-
"""
|
|
446
|
-
self._assert_auth()
|
|
447
|
-
r = requests.post(
|
|
448
|
-
f"https://api.scratch.mit.edu/proxy/projects/{self.id}/favorites/user/{self._session._username}",
|
|
449
|
-
headers=self._headers,
|
|
450
|
-
cookies=self._cookies,
|
|
451
|
-
).json()
|
|
452
|
-
if "userFavorite" in r:
|
|
453
|
-
if r["userFavorite"] is False:
|
|
454
|
-
self.favorite()
|
|
455
|
-
else:
|
|
456
|
-
raise exceptions.APIError(str(r))
|
|
457
|
-
|
|
458
|
-
def unfavorite(self):
|
|
459
|
-
"""
|
|
460
|
-
Removes the favorite from this project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
461
|
-
"""
|
|
462
|
-
self._assert_auth()
|
|
463
|
-
r = requests.delete(
|
|
464
|
-
f"https://api.scratch.mit.edu/proxy/projects/{self.id}/favorites/user/{self._session._username}",
|
|
465
|
-
headers=self._headers,
|
|
466
|
-
cookies=self._cookies,
|
|
467
|
-
).json()
|
|
468
|
-
if "userFavorite" in r:
|
|
469
|
-
if r["userFavorite"] is True:
|
|
470
|
-
self.unfavorite()
|
|
471
|
-
else:
|
|
472
|
-
raise exceptions.APIError(str(r))
|
|
473
|
-
|
|
474
|
-
def post_view(self):
|
|
475
|
-
"""
|
|
476
|
-
Increases the project's view counter by 1. Doesn't require a login.
|
|
477
|
-
"""
|
|
478
|
-
requests.post(
|
|
479
|
-
f"https://api.scratch.mit.edu/users/{self.author_name}/projects/{self.id}/views/",
|
|
480
|
-
headers=headers,
|
|
481
|
-
)
|
|
482
|
-
|
|
483
|
-
def set_fields(self, fields_dict, *, use_site_api=False):
|
|
484
|
-
"""
|
|
485
|
-
Sets fields. By default, ueses the api.scratch.mit.edu/projects/xxx/ PUT API.
|
|
486
|
-
|
|
487
|
-
Keyword Arguments:
|
|
488
|
-
use_site_api (bool):
|
|
489
|
-
When enabled, the fields are set using the scratch.mit.edu/site-api API.
|
|
490
|
-
This function allows setting more fields than Project.set_fields.
|
|
491
|
-
For example you can also share / unshare the project by setting the "shared" field.
|
|
492
|
-
According to the Scratch team, this API is deprecated. As of 2024 it's still fully functional tho.
|
|
493
|
-
"""
|
|
494
|
-
self._assert_permission()
|
|
495
|
-
if use_site_api:
|
|
496
|
-
r = requests.put(
|
|
497
|
-
f"https://scratch.mit.edu/site-api/projects/all/{self.id}",
|
|
498
|
-
headers=self._headers,
|
|
499
|
-
cookies=self._cookies,
|
|
500
|
-
json=fields_dict,
|
|
501
|
-
).json()
|
|
502
|
-
else:
|
|
503
|
-
r = requests.put(
|
|
504
|
-
f"https://api.scratch.mit.edu/projects/{self.id}",
|
|
505
|
-
headers=self._headers,
|
|
506
|
-
cookies=self._cookies,
|
|
507
|
-
json=fields_dict,
|
|
508
|
-
).json()
|
|
509
|
-
return self._update_from_dict(r)
|
|
510
|
-
|
|
511
|
-
def turn_off_commenting(self):
|
|
512
|
-
"""
|
|
513
|
-
Disables commenting on the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
514
|
-
"""
|
|
515
|
-
data = {"comments_allowed": False}
|
|
516
|
-
self.set_fields(data)
|
|
517
|
-
|
|
518
|
-
def turn_on_commenting(self):
|
|
519
|
-
"""
|
|
520
|
-
Enables commenting on the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
521
|
-
"""
|
|
522
|
-
data = {"comments_allowed": True}
|
|
523
|
-
self.set_fields(data)
|
|
524
|
-
|
|
525
|
-
def toggle_commenting(self):
|
|
526
|
-
"""
|
|
527
|
-
Switches commenting on / off on the project (If comments are on, they will be turned off, else they will be turned on). You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
528
|
-
"""
|
|
529
|
-
data = {"comments_allowed": not self.comments_allowed}
|
|
530
|
-
self.set_fields(data)
|
|
531
|
-
|
|
532
|
-
def share(self):
|
|
533
|
-
"""
|
|
534
|
-
Shares the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
535
|
-
"""
|
|
536
|
-
self._assert_permission()
|
|
537
|
-
requests.put(
|
|
538
|
-
f"https://api.scratch.mit.edu/proxy/projects/{self.id}/share/",
|
|
539
|
-
headers=self._json_headers,
|
|
540
|
-
cookies=self._cookies,
|
|
541
|
-
)
|
|
542
|
-
|
|
543
|
-
def unshare(self):
|
|
544
|
-
"""
|
|
545
|
-
Unshares the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
546
|
-
"""
|
|
547
|
-
self._assert_permission()
|
|
548
|
-
requests.put(
|
|
549
|
-
f"https://api.scratch.mit.edu/proxy/projects/{self.id}/unshare/",
|
|
550
|
-
headers=self._json_headers,
|
|
551
|
-
cookies=self._cookies,
|
|
552
|
-
)
|
|
553
|
-
|
|
554
|
-
''' doesn't work. the API's response is valid (no errors), but the fields don't change
|
|
555
|
-
def move_to_trash(self):
|
|
556
|
-
"""
|
|
557
|
-
Moves the project to trash folder. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
558
|
-
"""
|
|
559
|
-
self.set_fields({"id":int(self.id), "visibility": "trshbyusr", "isPublished" : False}, use_site_api=True)'''
|
|
560
|
-
|
|
561
|
-
def set_thumbnail(self, *, file):
|
|
562
|
-
"""
|
|
563
|
-
You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
564
|
-
"""
|
|
565
|
-
self._assert_permission()
|
|
566
|
-
with open(file, "rb") as f:
|
|
567
|
-
thumbnail = f.read()
|
|
568
|
-
requests.post(
|
|
569
|
-
f"https://scratch.mit.edu/internalapi/project/thumbnail/{self.id}/set/",
|
|
570
|
-
data=thumbnail,
|
|
571
|
-
headers=self._headers,
|
|
572
|
-
cookies=self._cookies,
|
|
573
|
-
)
|
|
574
|
-
|
|
575
|
-
def delete_comment(self, *, comment_id):
|
|
576
|
-
"""
|
|
577
|
-
Deletes a comment by its ID. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
578
|
-
|
|
579
|
-
Args:
|
|
580
|
-
comment_id: The id of the comment that should be deleted
|
|
581
|
-
"""
|
|
582
|
-
self._assert_permission()
|
|
583
|
-
return requests.delete(
|
|
584
|
-
f"https://api.scratch.mit.edu/proxy/comments/project/{self.id}/comment/{comment_id}/",
|
|
585
|
-
headers=self._headers,
|
|
586
|
-
cookies=self._cookies,
|
|
587
|
-
).headers
|
|
588
|
-
|
|
589
|
-
def report_comment(self, *, comment_id):
|
|
590
|
-
"""
|
|
591
|
-
Reports a comment by its ID to the Scratch team. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
592
|
-
|
|
593
|
-
Args:
|
|
594
|
-
comment_id: The id of the comment that should be reported
|
|
595
|
-
"""
|
|
596
|
-
self._assert_auth()
|
|
597
|
-
return requests.delete(
|
|
598
|
-
f"https://api.scratch.mit.edu/proxy/comments/project/{self.id}/comment/{comment_id}/report",
|
|
599
|
-
headers=self._headers,
|
|
600
|
-
cookies=self._cookies,
|
|
601
|
-
)
|
|
602
|
-
|
|
603
|
-
def post_comment(self, content, *, parent_id="", commentee_id=""):
|
|
604
|
-
"""
|
|
605
|
-
Posts a comment on the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
606
|
-
|
|
607
|
-
Args:
|
|
608
|
-
content: Content of the comment that should be posted
|
|
609
|
-
|
|
610
|
-
Keyword Arguments:
|
|
611
|
-
parent_id: ID of the comment you want to reply to. If you don't want to mention a user, don't put the argument.
|
|
612
|
-
commentee_id: ID of the user that will be mentioned in your comment and will receive a message about your comment. If you don't want to mention a user, don't put the argument.
|
|
613
|
-
|
|
614
|
-
Returns:
|
|
615
|
-
scratchattach.comments.Comment: Comment object representing the posted comment.
|
|
616
|
-
"""
|
|
617
|
-
self._assert_auth()
|
|
618
|
-
data = {
|
|
619
|
-
"commentee_id": commentee_id,
|
|
620
|
-
"content": str(content),
|
|
621
|
-
"parent_id": parent_id,
|
|
622
|
-
}
|
|
623
|
-
headers = dict(self._json_headers)
|
|
624
|
-
headers["referer"] = "https://scratch.mit.edu/projects/" + str(self.id) + "/"
|
|
625
|
-
r = json.loads(
|
|
626
|
-
requests.post(
|
|
627
|
-
f"https://api.scratch.mit.edu/proxy/comments/project/{self.id}/",
|
|
628
|
-
headers=headers,
|
|
629
|
-
cookies=self._cookies,
|
|
630
|
-
data=json.dumps(data),
|
|
631
|
-
).text
|
|
632
|
-
)
|
|
633
|
-
if "id" not in r:
|
|
634
|
-
raise exceptions.CommentPostFailure(r)
|
|
635
|
-
_comment = comment.Comment(id=r["id"], _session=self._session, source="project", source_id=self.id)
|
|
636
|
-
_comment._update_from_dict(r)
|
|
637
|
-
return _comment
|
|
638
|
-
|
|
639
|
-
def reply_comment(self, content, *, parent_id, commentee_id=""):
|
|
640
|
-
"""
|
|
641
|
-
Posts a reply to a comment on the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
642
|
-
|
|
643
|
-
Args:
|
|
644
|
-
content: Content of the comment that should be posted
|
|
645
|
-
|
|
646
|
-
Warning:
|
|
647
|
-
Only replies to top-level comments are shown on the Scratch website. Replies to replies are actually replies to the corresponding top-level comment in the API.
|
|
648
|
-
|
|
649
|
-
Therefore, parent_id should be the comment id of a top level comment.
|
|
650
|
-
|
|
651
|
-
Keyword Arguments:
|
|
652
|
-
parent_id: ID of the comment you want to reply to
|
|
653
|
-
commentee_id: ID of the user you are replying to
|
|
654
|
-
"""
|
|
655
|
-
return self.post_comment(
|
|
656
|
-
content, parent_id=parent_id, commentee_id=commentee_id
|
|
657
|
-
)
|
|
658
|
-
|
|
659
|
-
def set_body(self, project_body:ProjectBody):
|
|
660
|
-
"""
|
|
661
|
-
Sets the project's contents You can use this to upload projects to the Scratch website.
|
|
662
|
-
Returns a dict with Scratch's raw JSON API response.
|
|
663
|
-
|
|
664
|
-
Args:
|
|
665
|
-
project_body (scratchattach.ProjectBody): A ProjectBody object containing the contents of the project
|
|
666
|
-
"""
|
|
667
|
-
self._assert_permission()
|
|
668
|
-
|
|
669
|
-
return self.set_json(project_body.to_json())
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
def set_json(self, json_data):
|
|
673
|
-
"""
|
|
674
|
-
Sets the project json. You can use this to upload projects to the Scratch website.
|
|
675
|
-
Returns a dict with Scratch's raw JSON API response.
|
|
676
|
-
|
|
677
|
-
Args:
|
|
678
|
-
json_data (dict or JSON): The new project JSON as encoded JSON object or as dict
|
|
679
|
-
"""
|
|
680
|
-
|
|
681
|
-
self._assert_permission()
|
|
682
|
-
|
|
683
|
-
if not isinstance(json_data, dict):
|
|
684
|
-
json_data = json.loads(json_data)
|
|
685
|
-
|
|
686
|
-
return requests.put(
|
|
687
|
-
f"https://projects.scratch.mit.edu/{self.id}",
|
|
688
|
-
headers=self._headers,
|
|
689
|
-
cookies=self._cookies,
|
|
690
|
-
json=json_data,
|
|
691
|
-
).json()
|
|
692
|
-
|
|
693
|
-
def upload_json_from(self, project_id):
|
|
694
|
-
"""
|
|
695
|
-
Uploads the project json from the project with the given id to the project represented by this Project object
|
|
696
|
-
"""
|
|
697
|
-
self._assert_auth()
|
|
698
|
-
other_project = self._session.connect_project(project_id)
|
|
699
|
-
self.set_json(other_project.get_raw_json())
|
|
700
|
-
|
|
701
|
-
def set_title(self, text):
|
|
702
|
-
"""
|
|
703
|
-
Changes the projects title. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
704
|
-
"""
|
|
705
|
-
self.set_fields({"title": text})
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
def set_instructions(self, text):
|
|
709
|
-
"""
|
|
710
|
-
Changes the projects instructions. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
711
|
-
"""
|
|
712
|
-
self.set_fields({"instructions": text})
|
|
713
|
-
|
|
714
|
-
def set_notes(self, text):
|
|
715
|
-
"""
|
|
716
|
-
Changes the projects notes and credits. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
|
|
717
|
-
"""
|
|
718
|
-
self.set_fields({"description": text})
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
def ranks(self):
|
|
722
|
-
"""
|
|
723
|
-
Gets information about the project's ranks. Fetched from ScratchDB.
|
|
724
|
-
|
|
725
|
-
Warning:
|
|
726
|
-
This method is deprecated because ScratchDB is down indefinitely.
|
|
727
|
-
|
|
728
|
-
Returns:
|
|
729
|
-
dict: A dict containing the project's ranks. If the ranks aren't available, all values will be -1.
|
|
730
|
-
"""
|
|
731
|
-
print("Warning: Project.ranks method is deprecated because ScratchDB is down indefinitely.")
|
|
732
|
-
return requests.get(
|
|
733
|
-
f"https://scratchdb.lefty.one/v3/project/info/{self.id}"
|
|
734
|
-
).json()["statistics"]["ranks"]
|
|
735
|
-
|
|
736
|
-
def moderation_status(self):
|
|
737
|
-
"""
|
|
738
|
-
Gets information about the project's moderation status. Fetched from jeffalo's API.
|
|
739
|
-
|
|
740
|
-
Returns:
|
|
741
|
-
str: The moderation status of the project.
|
|
742
|
-
|
|
743
|
-
These moderation statuses exist:
|
|
744
|
-
|
|
745
|
-
safe: The project was reviewed by the Scratch team and was considered safe for everyone.
|
|
746
|
-
|
|
747
|
-
notsafe: The project was reviewed by the Scratch team and was considered not safe for everyone (nfe). It can't appear in search results, on the explore page and on the front page.
|
|
748
|
-
|
|
749
|
-
notreviewed: The project hasn't been reviewed yet.
|
|
750
|
-
|
|
751
|
-
no_remixes: Unable to fetch the project's moderation status.
|
|
752
|
-
"""
|
|
753
|
-
try:
|
|
754
|
-
return requests.get(
|
|
755
|
-
f"https://jeffalo.net/api/nfe/?project={self.id}"
|
|
756
|
-
).json()["status"]
|
|
757
|
-
except Exception:
|
|
758
|
-
raise (exceptions.FetchError)
|
|
759
|
-
|
|
760
|
-
def visibility(self):
|
|
761
|
-
"""
|
|
762
|
-
Returns info about the project's visibility. Requires authentication.
|
|
763
|
-
"""
|
|
764
|
-
self._assert_auth()
|
|
765
|
-
return requests.get(f"https://api.scratch.mit.edu/users/{self._session.username}/projects/{self.id}/visibility", headers=self._headers, cookies=self._cookies).json()
|
|
766
|
-
|
|
767
|
-
# ------ #
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
def get_project(project_id) -> Project:
|
|
771
|
-
"""
|
|
772
|
-
Gets a project without logging in.
|
|
773
|
-
|
|
774
|
-
Args:
|
|
775
|
-
project_id (int): Project id of the requested project
|
|
776
|
-
|
|
777
|
-
Returns:
|
|
778
|
-
scratchattach.project.Project: An object representing the requested project.
|
|
779
|
-
|
|
780
|
-
Warning:
|
|
781
|
-
Any methods that require authentication (like project.love) will not work on the returned object.
|
|
782
|
-
|
|
783
|
-
If you want to use these methods, get the project with :meth:`scratchattach.session.Session.connect_project` instead.
|
|
784
|
-
"""
|
|
785
|
-
print("Warning: For methods that require authentication, use session.connect_project instead of get_project")
|
|
786
|
-
return commons._get_object("id", project_id, Project, exceptions.ProjectNotFound)
|
|
787
|
-
|
|
788
|
-
def search_projects(*, query="", mode="trending", language="en", limit=40, offset=0):
|
|
789
|
-
'''
|
|
790
|
-
Uses the Scratch search to search projects.
|
|
791
|
-
|
|
792
|
-
Keyword arguments:
|
|
793
|
-
query (str): The query that will be searched.
|
|
794
|
-
mode (str): Has to be one of these values: "trending", "popular" or "recent". Defaults to "trending".
|
|
795
|
-
language (str): A language abbreviation, defaults to "en". (Depending on the language used on the Scratch website, Scratch displays you different results.)
|
|
796
|
-
limit (int): Max. amount of returned projects.
|
|
797
|
-
offset (int): Offset of the first returned project.
|
|
798
|
-
|
|
799
|
-
Returns:
|
|
800
|
-
list<scratchattach.project.Project>: List that contains the search results.
|
|
801
|
-
'''
|
|
802
|
-
if not query:
|
|
803
|
-
raise ValueError("The query can't be empty for search")
|
|
804
|
-
response = commons.api_iterative(
|
|
805
|
-
f"https://api.scratch.mit.edu/search/projects", limit=limit, offset=offset, add_params=f"&language={language}&mode={mode}&q={query}")
|
|
806
|
-
return commons.parse_object_list(response, Project)
|
|
807
|
-
|
|
808
|
-
def explore_projects(*, query="*", mode="trending", language="en", limit=40, offset=0):
|
|
809
|
-
'''
|
|
810
|
-
Gets projects from the explore page.
|
|
811
|
-
|
|
812
|
-
Keyword arguments:
|
|
813
|
-
query (str): Specifies the tag of the explore page. To get the projects from the "All" tag, set this argument to "*".
|
|
814
|
-
mode (str): Has to be one of these values: "trending", "popular" or "recent". Defaults to "trending".
|
|
815
|
-
language (str): A language abbreviation, defaults to "en". (Depending on the language used on the Scratch website, Scratch displays you different explore pages.)
|
|
816
|
-
limit (int): Max. amount of returned projects.
|
|
817
|
-
offset (int): Offset of the first returned project.
|
|
818
|
-
|
|
819
|
-
Returns:
|
|
820
|
-
list<scratchattach.project.Project>: List that contains the explore page projects.
|
|
821
|
-
'''
|
|
822
|
-
if not query:
|
|
823
|
-
raise ValueError("The query can't be empty for search")
|
|
824
|
-
response = commons.api_iterative(
|
|
825
|
-
f"https://api.scratch.mit.edu/explore/projects", limit=limit, offset=offset, add_params=f"&language={language}&mode={mode}&q={query}")
|
|
826
|
-
return commons.parse_object_list(response, Project)
|