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