scratchattach 2.1.14__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.
Files changed (69) hide show
  1. scratchattach/__init__.py +14 -6
  2. scratchattach/__main__.py +93 -0
  3. {scratchattach-2.1.14.dist-info → scratchattach-3.0.0b0.dist-info}/METADATA +7 -11
  4. scratchattach-3.0.0b0.dist-info/RECORD +8 -0
  5. {scratchattach-2.1.14.dist-info → scratchattach-3.0.0b0.dist-info}/WHEEL +1 -1
  6. scratchattach-3.0.0b0.dist-info/entry_points.txt +2 -0
  7. scratchattach/cloud/__init__.py +0 -2
  8. scratchattach/cloud/_base.py +0 -458
  9. scratchattach/cloud/cloud.py +0 -183
  10. scratchattach/editor/__init__.py +0 -21
  11. scratchattach/editor/asset.py +0 -253
  12. scratchattach/editor/backpack_json.py +0 -117
  13. scratchattach/editor/base.py +0 -193
  14. scratchattach/editor/block.py +0 -579
  15. scratchattach/editor/blockshape.py +0 -357
  16. scratchattach/editor/build_defaulting.py +0 -51
  17. scratchattach/editor/code_translation/__init__.py +0 -0
  18. scratchattach/editor/code_translation/parse.py +0 -177
  19. scratchattach/editor/comment.py +0 -80
  20. scratchattach/editor/commons.py +0 -306
  21. scratchattach/editor/extension.py +0 -50
  22. scratchattach/editor/field.py +0 -99
  23. scratchattach/editor/inputs.py +0 -135
  24. scratchattach/editor/meta.py +0 -114
  25. scratchattach/editor/monitor.py +0 -183
  26. scratchattach/editor/mutation.py +0 -324
  27. scratchattach/editor/pallete.py +0 -90
  28. scratchattach/editor/prim.py +0 -170
  29. scratchattach/editor/project.py +0 -279
  30. scratchattach/editor/sprite.py +0 -599
  31. scratchattach/editor/twconfig.py +0 -114
  32. scratchattach/editor/vlb.py +0 -134
  33. scratchattach/eventhandlers/__init__.py +0 -0
  34. scratchattach/eventhandlers/_base.py +0 -100
  35. scratchattach/eventhandlers/cloud_events.py +0 -110
  36. scratchattach/eventhandlers/cloud_recorder.py +0 -26
  37. scratchattach/eventhandlers/cloud_requests.py +0 -459
  38. scratchattach/eventhandlers/cloud_server.py +0 -246
  39. scratchattach/eventhandlers/cloud_storage.py +0 -136
  40. scratchattach/eventhandlers/combine.py +0 -30
  41. scratchattach/eventhandlers/filterbot.py +0 -161
  42. scratchattach/eventhandlers/message_events.py +0 -42
  43. scratchattach/other/__init__.py +0 -0
  44. scratchattach/other/other_apis.py +0 -284
  45. scratchattach/other/project_json_capabilities.py +0 -475
  46. scratchattach/site/__init__.py +0 -0
  47. scratchattach/site/_base.py +0 -66
  48. scratchattach/site/activity.py +0 -382
  49. scratchattach/site/alert.py +0 -227
  50. scratchattach/site/backpack_asset.py +0 -118
  51. scratchattach/site/browser_cookie3_stub.py +0 -17
  52. scratchattach/site/browser_cookies.py +0 -61
  53. scratchattach/site/classroom.py +0 -447
  54. scratchattach/site/cloud_activity.py +0 -107
  55. scratchattach/site/comment.py +0 -242
  56. scratchattach/site/forum.py +0 -432
  57. scratchattach/site/project.py +0 -825
  58. scratchattach/site/session.py +0 -1238
  59. scratchattach/site/studio.py +0 -611
  60. scratchattach/site/user.py +0 -956
  61. scratchattach/utils/__init__.py +0 -0
  62. scratchattach/utils/commons.py +0 -255
  63. scratchattach/utils/encoder.py +0 -158
  64. scratchattach/utils/enums.py +0 -236
  65. scratchattach/utils/exceptions.py +0 -243
  66. scratchattach/utils/requests.py +0 -93
  67. scratchattach-2.1.14.dist-info/RECORD +0 -66
  68. {scratchattach-2.1.14.dist-info → scratchattach-3.0.0b0.dist-info}/licenses/LICENSE +0 -0
  69. {scratchattach-2.1.14.dist-info → scratchattach-3.0.0b0.dist-info}/top_level.txt +0 -0
@@ -1,825 +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 = 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.replace(".sb3", "")
265
- open(f"{dir}{filename}.sb3", "wb").write(response.content)
266
- except Exception:
267
- raise (
268
- exceptions.FetchError(
269
- "Method only works for projects created with Scratch 3"
270
- )
271
- )
272
-
273
- def get_json(self) -> str:
274
- """
275
- Downloads the project json and returns it as a string
276
- """
277
- try:
278
- self.update()
279
- response = requests.get(
280
- f"https://projects.scratch.mit.edu/{self.id}?token={self.project_token}",
281
- timeout=10,
282
- )
283
- return response.text
284
-
285
- except Exception:
286
- raise (
287
- exceptions.FetchError(
288
- "Method only works for projects created with Scratch 3"
289
- )
290
- )
291
-
292
- def body(self):
293
- """
294
- Method only works for project created with Scratch 3.
295
-
296
- Returns:
297
- scratchattach.ProjectBody: The contents of the project as ProjectBody object
298
- """
299
- raw_json = self.raw_json()
300
- pb = ProjectBody()
301
- pb.from_json(raw_json)
302
- return pb
303
-
304
- def raw_json(self):
305
- """
306
- Method only works for project created with Scratch 3.
307
-
308
- Returns:
309
- dict: The raw project JSON as decoded Python dictionary
310
- """
311
- try:
312
- self.update()
313
-
314
- except Exception as e:
315
- raise (
316
- exceptions.FetchError(
317
- f"You're not authorized for accessing {self}.\nException: {e}"
318
- )
319
- )
320
-
321
- with requests.no_error_handling():
322
- resp = requests.get(
323
- f"https://projects.scratch.mit.edu/{self.id}?token={self.project_token}",
324
- timeout=10,
325
- )
326
-
327
- try:
328
- return resp.json()
329
- except json.JSONDecodeError:
330
- # I am not aware of any cases where this will not be a zip file
331
- # in the future, cache a projectbody object here and just return the json
332
- # that is fetched from there to not waste existing asset data from this zip file
333
-
334
- with zipfile.ZipFile(BytesIO(resp.content)) as zipf:
335
- return json.load(zipf.open("project.json"))
336
-
337
- def raw_json_or_empty(self):
338
- return self.raw_json()
339
-
340
- def creator_agent(self):
341
- """
342
- Method only works for project created with Scratch 3.
343
-
344
- Returns:
345
- str: The user agent of the browser that this project was saved with.
346
- """
347
- return self.raw_json()["meta"]["agent"]
348
-
349
- def author(self):
350
- """
351
- Returns:
352
- scratchattach.user.User: An object representing the Scratch user who created this project.
353
- """
354
- return self._make_linked_object("username", self.author_name, user.User, exceptions.UserNotFound)
355
-
356
- def studios(self, *, limit=40, offset=0):
357
- """
358
- Returns:
359
- list<scratchattach.studio.Studio>: A list containing the studios this project is in, each studio is represented by a Studio object.
360
- """
361
- response = commons.api_iterative(
362
- 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)}")
363
- return commons.parse_object_list(response, studio.Studio, self._session)
364
-
365
- def comments(self, *, limit=40, offset=0) -> list['comment.Comment']:
366
- """
367
- Returns the comments posted on the project (except for replies. To get replies use :meth:`scratchattach.project.Project.comment_replies`).
368
-
369
- Keyword Arguments:
370
- page: The page of the comments that should be returned.
371
- limit: Max. amount of returned comments.
372
-
373
- Returns:
374
- list<scratchattach.comment.Comment>: A list containing the requested comments as Comment objects.
375
- """
376
-
377
- response = commons.api_iterative(
378
- 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)}")
379
- for i in response:
380
- i["source"] = "project"
381
- i["source_id"] = self.id
382
- return commons.parse_object_list(response, comment.Comment, self._session)
383
-
384
- def comment_replies(self, *, comment_id, limit=40, offset=0):
385
- response = commons.api_iterative(
386
- 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)}")
387
- for x in response:
388
- x["parent_id"] = comment_id
389
- x["source"] = "project"
390
- x["source_id"] = self.id
391
- return commons.parse_object_list(response, comment.Comment, self._session)
392
-
393
- def comment_by_id(self, comment_id):
394
- """
395
- Returns:
396
- scratchattach.comments.Comment: A Comment object representing the requested comment.
397
- """
398
- r = requests.get(
399
- f"https://api.scratch.mit.edu/users/{self.author_name}/projects/{self.id}/comments/{comment_id}",
400
- headers=self._headers,
401
- cookies=self._cookies
402
- ).json()
403
- if r is None:
404
- raise exceptions.CommentNotFound()
405
- _comment = comment.Comment(id=r["id"], _session=self._session, source="project", source_id=self.id)
406
- _comment._update_from_dict(r)
407
- return _comment
408
-
409
- def love(self):
410
- """
411
- Posts a love on the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
412
- """
413
- self._assert_auth()
414
- r = requests.post(
415
- f"https://api.scratch.mit.edu/proxy/projects/{self.id}/loves/user/{self._session._username}",
416
- headers=self._headers,
417
- cookies=self._cookies,
418
- ).json()
419
- if "userLove" in r:
420
- if r["userLove"] is False:
421
- self.love()
422
- else:
423
- raise exceptions.APIError(str(r))
424
-
425
- def unlove(self):
426
- """
427
- Removes the love from this project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
428
- """
429
- self._assert_auth()
430
- r = requests.delete(
431
- f"https://api.scratch.mit.edu/proxy/projects/{self.id}/loves/user/{self._session._username}",
432
- headers=self._headers,
433
- cookies=self._cookies,
434
- ).json()
435
- if "userLove" in r:
436
- if r["userLove"] is True:
437
- self.unlove()
438
- else:
439
- raise exceptions.APIError(str(r))
440
-
441
- def favorite(self):
442
- """
443
- Posts a favorite on the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
444
- """
445
- self._assert_auth()
446
- r = requests.post(
447
- f"https://api.scratch.mit.edu/proxy/projects/{self.id}/favorites/user/{self._session._username}",
448
- headers=self._headers,
449
- cookies=self._cookies,
450
- ).json()
451
- if "userFavorite" in r:
452
- if r["userFavorite"] is False:
453
- self.favorite()
454
- else:
455
- raise exceptions.APIError(str(r))
456
-
457
- def unfavorite(self):
458
- """
459
- Removes the favorite from this project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
460
- """
461
- self._assert_auth()
462
- r = requests.delete(
463
- f"https://api.scratch.mit.edu/proxy/projects/{self.id}/favorites/user/{self._session._username}",
464
- headers=self._headers,
465
- cookies=self._cookies,
466
- ).json()
467
- if "userFavorite" in r:
468
- if r["userFavorite"] is True:
469
- self.unfavorite()
470
- else:
471
- raise exceptions.APIError(str(r))
472
-
473
- def post_view(self):
474
- """
475
- Increases the project's view counter by 1. Doesn't require a login.
476
- """
477
- requests.post(
478
- f"https://api.scratch.mit.edu/users/{self.author_name}/projects/{self.id}/views/",
479
- headers=headers,
480
- )
481
-
482
- def set_fields(self, fields_dict, *, use_site_api=False):
483
- """
484
- Sets fields. By default, ueses the api.scratch.mit.edu/projects/xxx/ PUT API.
485
-
486
- Keyword Arguments:
487
- use_site_api (bool):
488
- When enabled, the fields are set using the scratch.mit.edu/site-api API.
489
- This function allows setting more fields than Project.set_fields.
490
- For example you can also share / unshare the project by setting the "shared" field.
491
- According to the Scratch team, this API is deprecated. As of 2024 it's still fully functional tho.
492
- """
493
- self._assert_permission()
494
- if use_site_api:
495
- r = requests.put(
496
- f"https://scratch.mit.edu/site-api/projects/all/{self.id}",
497
- headers=self._headers,
498
- cookies=self._cookies,
499
- json=fields_dict,
500
- ).json()
501
- else:
502
- r = requests.put(
503
- f"https://api.scratch.mit.edu/projects/{self.id}",
504
- headers=self._headers,
505
- cookies=self._cookies,
506
- json=fields_dict,
507
- ).json()
508
- return self._update_from_dict(r)
509
-
510
- def turn_off_commenting(self):
511
- """
512
- Disables commenting on the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
513
- """
514
- data = {"comments_allowed": False}
515
- self.set_fields(data)
516
-
517
- def turn_on_commenting(self):
518
- """
519
- Enables commenting on the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
520
- """
521
- data = {"comments_allowed": True}
522
- self.set_fields(data)
523
-
524
- def toggle_commenting(self):
525
- """
526
- 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`
527
- """
528
- data = {"comments_allowed": not self.comments_allowed}
529
- self.set_fields(data)
530
-
531
- def share(self):
532
- """
533
- Shares the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
534
- """
535
- self._assert_permission()
536
- requests.put(
537
- f"https://api.scratch.mit.edu/proxy/projects/{self.id}/share/",
538
- headers=self._json_headers,
539
- cookies=self._cookies,
540
- )
541
-
542
- def unshare(self):
543
- """
544
- Unshares the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
545
- """
546
- self._assert_permission()
547
- requests.put(
548
- f"https://api.scratch.mit.edu/proxy/projects/{self.id}/unshare/",
549
- headers=self._json_headers,
550
- cookies=self._cookies,
551
- )
552
-
553
- ''' doesn't work. the API's response is valid (no errors), but the fields don't change
554
- def move_to_trash(self):
555
- """
556
- Moves the project to trash folder. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
557
- """
558
- self.set_fields({"id":int(self.id), "visibility": "trshbyusr", "isPublished" : False}, use_site_api=True)'''
559
-
560
- def set_thumbnail(self, *, file):
561
- """
562
- You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
563
- """
564
- self._assert_permission()
565
- with open(file, "rb") as f:
566
- thumbnail = f.read()
567
- requests.post(
568
- f"https://scratch.mit.edu/internalapi/project/thumbnail/{self.id}/set/",
569
- data=thumbnail,
570
- headers=self._headers,
571
- cookies=self._cookies,
572
- )
573
-
574
- def delete_comment(self, *, comment_id):
575
- """
576
- Deletes a comment by its ID. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
577
-
578
- Args:
579
- comment_id: The id of the comment that should be deleted
580
- """
581
- self._assert_permission()
582
- return requests.delete(
583
- f"https://api.scratch.mit.edu/proxy/comments/project/{self.id}/comment/{comment_id}/",
584
- headers=self._headers,
585
- cookies=self._cookies,
586
- ).headers
587
-
588
- def report_comment(self, *, comment_id):
589
- """
590
- 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`
591
-
592
- Args:
593
- comment_id: The id of the comment that should be reported
594
- """
595
- self._assert_auth()
596
- return requests.delete(
597
- f"https://api.scratch.mit.edu/proxy/comments/project/{self.id}/comment/{comment_id}/report",
598
- headers=self._headers,
599
- cookies=self._cookies,
600
- )
601
-
602
- def post_comment(self, content, *, parent_id="", commentee_id=""):
603
- """
604
- Posts a comment on the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
605
-
606
- Args:
607
- content: Content of the comment that should be posted
608
-
609
- Keyword Arguments:
610
- 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.
611
- 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.
612
-
613
- Returns:
614
- scratchattach.comments.Comment: Comment object representing the posted comment.
615
- """
616
- self._assert_auth()
617
- data = {
618
- "commentee_id": commentee_id,
619
- "content": str(content),
620
- "parent_id": parent_id,
621
- }
622
- headers = dict(self._json_headers)
623
- headers["referer"] = "https://scratch.mit.edu/projects/" + str(self.id) + "/"
624
- r = json.loads(
625
- requests.post(
626
- f"https://api.scratch.mit.edu/proxy/comments/project/{self.id}/",
627
- headers=headers,
628
- cookies=self._cookies,
629
- data=json.dumps(data),
630
- ).text
631
- )
632
- if "id" not in r:
633
- raise exceptions.CommentPostFailure(r)
634
- _comment = comment.Comment(id=r["id"], _session=self._session, source="project", source_id=self.id)
635
- _comment._update_from_dict(r)
636
- return _comment
637
-
638
- def reply_comment(self, content, *, parent_id, commentee_id=""):
639
- """
640
- 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`
641
-
642
- Args:
643
- content: Content of the comment that should be posted
644
-
645
- Warning:
646
- 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.
647
-
648
- Therefore, parent_id should be the comment id of a top level comment.
649
-
650
- Keyword Arguments:
651
- parent_id: ID of the comment you want to reply to
652
- commentee_id: ID of the user you are replying to
653
- """
654
- return self.post_comment(
655
- content, parent_id=parent_id, commentee_id=commentee_id
656
- )
657
-
658
- def set_body(self, project_body:ProjectBody):
659
- """
660
- Sets the project's contents You can use this to upload projects to the Scratch website.
661
- Returns a dict with Scratch's raw JSON API response.
662
-
663
- Args:
664
- project_body (scratchattach.ProjectBody): A ProjectBody object containing the contents of the project
665
- """
666
- self._assert_permission()
667
-
668
- return self.set_json(project_body.to_json())
669
-
670
-
671
- def set_json(self, json_data):
672
- """
673
- Sets the project json. You can use this to upload projects to the Scratch website.
674
- Returns a dict with Scratch's raw JSON API response.
675
-
676
- Args:
677
- json_data (dict or JSON): The new project JSON as encoded JSON object or as dict
678
- """
679
-
680
- self._assert_permission()
681
-
682
- if not isinstance(json_data, dict):
683
- json_data = json.loads(json_data)
684
-
685
- return requests.put(
686
- f"https://projects.scratch.mit.edu/{self.id}",
687
- headers=self._headers,
688
- cookies=self._cookies,
689
- json=json_data,
690
- ).json()
691
-
692
- def upload_json_from(self, project_id):
693
- """
694
- Uploads the project json from the project with the given id to the project represented by this Project object
695
- """
696
- self._assert_auth()
697
- other_project = self._session.connect_project(project_id)
698
- self.set_json(other_project.get_raw_json())
699
-
700
- def set_title(self, text):
701
- """
702
- Changes the projects title. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
703
- """
704
- self.set_fields({"title": text})
705
-
706
-
707
- def set_instructions(self, text):
708
- """
709
- Changes the projects instructions. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
710
- """
711
- self.set_fields({"instructions": text})
712
-
713
- def set_notes(self, text):
714
- """
715
- Changes the projects notes and credits. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
716
- """
717
- self.set_fields({"description": text})
718
-
719
-
720
- def ranks(self):
721
- """
722
- Gets information about the project's ranks. Fetched from ScratchDB.
723
-
724
- Warning:
725
- This method is deprecated because ScratchDB is down indefinitely.
726
-
727
- Returns:
728
- dict: A dict containing the project's ranks. If the ranks aren't available, all values will be -1.
729
- """
730
- print("Warning: Project.ranks method is deprecated because ScratchDB is down indefinitely.")
731
- return requests.get(
732
- f"https://scratchdb.lefty.one/v3/project/info/{self.id}"
733
- ).json()["statistics"]["ranks"]
734
-
735
- def moderation_status(self):
736
- """
737
- Gets information about the project's moderation status. Fetched from jeffalo's API.
738
-
739
- Returns:
740
- str: The moderation status of the project.
741
-
742
- These moderation statuses exist:
743
-
744
- safe: The project was reviewed by the Scratch team and was considered safe for everyone.
745
-
746
- 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.
747
-
748
- notreviewed: The project hasn't been reviewed yet.
749
-
750
- no_remixes: Unable to fetch the project's moderation status.
751
- """
752
- try:
753
- return requests.get(
754
- f"https://jeffalo.net/api/nfe/?project={self.id}"
755
- ).json()["status"]
756
- except Exception:
757
- raise (exceptions.FetchError)
758
-
759
- def visibility(self):
760
- """
761
- Returns info about the project's visibility. Requires authentication.
762
- """
763
- self._assert_auth()
764
- return requests.get(f"https://api.scratch.mit.edu/users/{self._session.username}/projects/{self.id}/visibility", headers=self._headers, cookies=self._cookies).json()
765
-
766
- # ------ #
767
-
768
-
769
- def get_project(project_id) -> Project:
770
- """
771
- Gets a project without logging in.
772
-
773
- Args:
774
- project_id (int): Project id of the requested project
775
-
776
- Returns:
777
- scratchattach.project.Project: An object representing the requested project.
778
-
779
- Warning:
780
- Any methods that require authentication (like project.love) will not work on the returned object.
781
-
782
- If you want to use these methods, get the project with :meth:`scratchattach.session.Session.connect_project` instead.
783
- """
784
- print("Warning: For methods that require authentication, use session.connect_project instead of get_project")
785
- return commons._get_object("id", project_id, Project, exceptions.ProjectNotFound)
786
-
787
- def search_projects(*, query="", mode="trending", language="en", limit=40, offset=0):
788
- '''
789
- Uses the Scratch search to search projects.
790
-
791
- Keyword arguments:
792
- query (str): The query that will be searched.
793
- mode (str): Has to be one of these values: "trending", "popular" or "recent". Defaults to "trending".
794
- language (str): A language abbreviation, defaults to "en". (Depending on the language used on the Scratch website, Scratch displays you different results.)
795
- limit (int): Max. amount of returned projects.
796
- offset (int): Offset of the first returned project.
797
-
798
- Returns:
799
- list<scratchattach.project.Project>: List that contains the search results.
800
- '''
801
- if not query:
802
- raise ValueError("The query can't be empty for search")
803
- response = commons.api_iterative(
804
- f"https://api.scratch.mit.edu/search/projects", limit=limit, offset=offset, add_params=f"&language={language}&mode={mode}&q={query}")
805
- return commons.parse_object_list(response, Project)
806
-
807
- def explore_projects(*, query="*", mode="trending", language="en", limit=40, offset=0):
808
- '''
809
- Gets projects from the explore page.
810
-
811
- Keyword arguments:
812
- query (str): Specifies the tag of the explore page. To get the projects from the "All" tag, set this argument to "*".
813
- mode (str): Has to be one of these values: "trending", "popular" or "recent". Defaults to "trending".
814
- language (str): A language abbreviation, defaults to "en". (Depending on the language used on the Scratch website, Scratch displays you different explore pages.)
815
- limit (int): Max. amount of returned projects.
816
- offset (int): Offset of the first returned project.
817
-
818
- Returns:
819
- list<scratchattach.project.Project>: List that contains the explore page projects.
820
- '''
821
- if not query:
822
- raise ValueError("The query can't be empty for search")
823
- response = commons.api_iterative(
824
- f"https://api.scratch.mit.edu/explore/projects", limit=limit, offset=offset, add_params=f"&language={language}&mode={mode}&q={query}")
825
- return commons.parse_object_list(response, Project)