scratchattach 2.1.9__py3-none-any.whl → 2.1.10a1__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.10a1.dist-info}/METADATA +155 -146
  55. scratchattach-2.1.10a1.dist-info/RECORD +62 -0
  56. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/WHEEL +1 -1
  57. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info/licenses}/LICENSE +21 -21
  58. scratchattach-2.1.9.dist-info/RECORD +0 -40
  59. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/top_level.txt +0 -0
@@ -1,609 +1,611 @@
1
- """Studio class"""
2
-
3
- import json
4
- import random
5
- from . import user, comment, project, activity
6
- from ..utils import exceptions, commons
7
- from ..utils.commons import api_iterative, headers
8
- from ._base import BaseSiteComponent
9
-
10
- from ..utils.requests import Requests as requests
11
-
12
- class Studio(BaseSiteComponent):
13
- """
14
- Represents a Scratch studio.
15
-
16
- Attributes:
17
-
18
- :.id:
19
-
20
- :.title:
21
-
22
- :.description:
23
-
24
- :.host_id: The user id of the studio host
25
-
26
- :.open_to_all: Whether everyone is allowed to add projects
27
-
28
- :.comments_allowed:
29
-
30
- :.image_url:
31
-
32
- :.created:
33
-
34
- :.modified:
35
-
36
- :.follower_count:
37
-
38
- :.manager_count:
39
-
40
- :.project_count:
41
-
42
- :.update(): Updates the attributes
43
-
44
- """
45
-
46
- def __init__(self, **entries):
47
-
48
- # Info on how the .update method has to fetch the data:
49
- self.update_function = requests.get
50
- self.update_API = f"https://api.scratch.mit.edu/studios/{entries['id']}"
51
-
52
- # Set attributes every Project object needs to have:
53
- self._session = None
54
- self.id = 0
55
-
56
- # Update attributes from entries dict:
57
- self.__dict__.update(entries)
58
-
59
- # Headers and cookies:
60
- if self._session is None:
61
- self._headers = headers
62
- self._cookies = {}
63
- else:
64
- self._headers = self._session._headers
65
- self._cookies = self._session._cookies
66
-
67
- # Headers for operations that require accept and Content-Type fields:
68
- self._json_headers = dict(self._headers)
69
- self._json_headers["accept"] = "application/json"
70
- self._json_headers["Content-Type"] = "application/json"
71
-
72
- def _update_from_dict(self, studio):
73
- try: self.id = int(studio["id"])
74
- except Exception: pass
75
- try: self.title = studio["title"]
76
- except Exception: pass
77
- try: self.description = studio["description"]
78
- except Exception: pass
79
- try: self.host_id = studio["host"]
80
- except Exception: pass
81
- try: self.open_to_all = studio["open_to_all"]
82
- except Exception: pass
83
- try: self.comments_allowed = studio["comments_allowed"]
84
- except Exception: pass
85
- try: self.image_url = studio["image"]
86
- except Exception: pass
87
- try: self.created = studio["history"]["created"]
88
- except Exception: pass
89
- try: self.modified = studio["history"]["modified"]
90
- except Exception: pass
91
- try: self.follower_count = studio["stats"]["followers"]
92
- except Exception: pass
93
- try: self.manager_count = studio["stats"]["managers"]
94
- except Exception: pass
95
- try: self.project_count = studio["stats"]["projects"]
96
- except Exception: pass
97
- return True
98
-
99
- def follow(self):
100
- """
101
- You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
102
- """
103
- self._assert_auth()
104
- requests.put(
105
- f"https://scratch.mit.edu/site-api/users/bookmarkers/{self.id}/add/?usernames={self._session._username}",
106
- headers=headers,
107
- cookies=self._cookies,
108
- timeout=10,
109
- )
110
-
111
- def unfollow(self):
112
- """
113
- You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
114
- """
115
- self._assert_auth()
116
- requests.put(
117
- f"https://scratch.mit.edu/site-api/users/bookmarkers/{self.id}/remove/?usernames={self._session._username}",
118
- headers=headers,
119
- cookies=self._cookies,
120
- timeout=10,
121
- )
122
-
123
- def comments(self, *, limit=40, offset=0):
124
- """
125
- Returns the comments posted on the studio (except for replies. To get replies use :meth:`scratchattach.studio.Studio.get_comment_replies`).
126
-
127
- Keyword Arguments:
128
- page: The page of the comments that should be returned.
129
- limit: Max. amount of returned comments.
130
-
131
- Returns:
132
- list<Comment>: A list containing the requested comments as Comment objects.
133
- """
134
- response = commons.api_iterative(
135
- f"https://api.scratch.mit.edu/studios/{self.id}/comments/", limit=limit, offset=offset, add_params=f"&cachebust={random.randint(0,9999)}")
136
- for i in response:
137
- i["source"] = "studio"
138
- i["source_id"] = self.id
139
- return commons.parse_object_list(response, comment.Comment, self._session)
140
-
141
- def comment_replies(self, *, comment_id, limit=40, offset=0):
142
- response = commons.api_iterative(
143
- f"https://api.scratch.mit.edu/studios/{self.id}/comments/{comment_id}/replies", limit=limit, offset=offset, add_params=f"&cachebust={random.randint(0,9999)}")
144
- for x in response:
145
- x["parent_id"] = comment_id
146
- x["source"] = "studio"
147
- x["source_id"] = self.id
148
- return commons.parse_object_list(response, comment.Comment, self._session)
149
-
150
- def comment_by_id(self, comment_id):
151
- r = requests.get(
152
- f"https://api.scratch.mit.edu/studios/{self.id}/comments/{comment_id}",
153
- timeout=10,
154
- ).json()
155
- if r is None:
156
- raise exceptions.CommentNotFound()
157
- _comment = comment.Comment(id=r["id"], _session=self._session, source="studio", source_id=self.id)
158
- _comment._update_from_dict(r)
159
- return _comment
160
-
161
- def post_comment(self, content, *, parent_id="", commentee_id=""):
162
- """
163
- Posts a comment on the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
164
-
165
- Args:
166
- content: Content of the comment that should be posted
167
-
168
- Keyword Arguments:
169
- 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.
170
- 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.
171
-
172
- Returns:
173
- scratchattach.comment.Comment: The posted comment as Comment object.
174
- """
175
- self._assert_auth()
176
- data = {
177
- "commentee_id": commentee_id,
178
- "content": str(content),
179
- "parent_id": parent_id,
180
- }
181
- headers = dict(self._json_headers)
182
- headers["referer"] = "https://scratch.mit.edu/projects/" + str(self.id) + "/"
183
- r = requests.post(
184
- f"https://api.scratch.mit.edu/proxy/comments/studio/{self.id}/",
185
- headers=headers,
186
- cookies=self._cookies,
187
- data=json.dumps(data),
188
- timeout=10,
189
- ).json()
190
- if "id" not in r:
191
- raise exceptions.CommentPostFailure(r)
192
- _comment = comment.Comment(id=r["id"], _session=self._session, source="studio", source_id=self.id)
193
- _comment._update_from_dict(r)
194
- return _comment
195
-
196
- def delete_comment(self, *, comment_id):
197
- # NEEDS TO BE TESTED!
198
- """
199
- Deletes a comment by ID. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
200
-
201
- Args:
202
- comment_id: The id of the comment that should be deleted
203
- """
204
- self._assert_auth()
205
- return requests.delete(
206
- f"https://api.scratch.mit.edu/proxy/comments/studio/{self.id}/comment/{comment_id}/",
207
- headers=self._headers,
208
- cookies=self._cookies,
209
- ).headers
210
-
211
- def report_comment(self, *, comment_id):
212
- # NEEDS TO BE TESTED!
213
- """
214
- Reports a comment by ID to the Scratch team. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
215
-
216
- Args:
217
- comment_id: The id of the comment that should be reported
218
- """
219
- self._assert_auth()
220
- return requests.delete(
221
- f"https://api.scratch.mit.edu/proxy/comments/studio/{self.id}/comment/{comment_id}/report",
222
- headers=self._headers,
223
- cookies=self._cookies,
224
- )
225
-
226
- def set_thumbnail(self, *, file):
227
- """
228
- Sets the studio thumbnail. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
229
-
230
- Keyword Arguments:
231
- file: The path to the image file
232
-
233
- Returns:
234
- str: Scratch cdn link to the set thumbnail
235
- """
236
- self._assert_auth()
237
- with open(file, "rb") as f:
238
- thumbnail = f.read()
239
-
240
- filename = file.replace("\\", "/")
241
- if filename.endswith("/"):
242
- filename = filename[:-1]
243
- filename = filename.split("/").pop()
244
-
245
- file_type = filename.split(".").pop()
246
-
247
- payload1 = f'------WebKitFormBoundaryhKZwFjoxAyUTMlSh\r\nContent-Disposition: form-data; name="file"; filename="{filename}"\r\nContent-Type: image/{file_type}\r\n\r\n'
248
- payload1 = payload1.encode("utf-8")
249
- payload2 = b"\r\n------WebKitFormBoundaryhKZwFjoxAyUTMlSh--\r\n"
250
- payload = b"".join([payload1, thumbnail, payload2])
251
-
252
- r = requests.post(
253
- f"https://scratch.mit.edu/site-api/galleries/all/{self.id}/",
254
- headers={
255
- "accept": "*/",
256
- "content-type": "multipart/form-data; boundary=----WebKitFormBoundaryhKZwFjoxAyUTMlSh",
257
- "Referer": "https://scratch.mit.edu/",
258
- "x-csrftoken": "a",
259
- "x-requested-with": "XMLHttpRequest",
260
- },
261
- data=payload,
262
- cookies=self._cookies,
263
- timeout=10,
264
- ).json()
265
-
266
- if "errors" in r:
267
- raise (exceptions.BadRequest(", ".join(r["errors"])))
268
- else:
269
- return r["thumbnail_url"]
270
-
271
- def reply_comment(self, content, *, parent_id, commentee_id=""):
272
- """
273
- Posts a reply to a comment on the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
274
-
275
- Args:
276
- content: Content of the comment that should be posted
277
-
278
- Warning:
279
- 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.
280
-
281
- Therefore, parent_id should be the comment id of a top level comment.
282
-
283
- Keyword Arguments:
284
- parent_id: ID of the comment you want to reply to
285
- commentee_id: ID of the user you are replying to
286
- """
287
- self._assert_auth()
288
- return self.post_comment(
289
- content, parent_id=parent_id, commentee_id=commentee_id
290
- )
291
-
292
- def projects(self, limit=40, offset=0):
293
- """
294
- Gets the studio projects.
295
-
296
- Keyword arguments:
297
- limit (int): Max amount of returned projects.
298
- offset (int): Offset of the first returned project.
299
-
300
- Returns:
301
- list<scratchattach.project.Project>: A list containing the studio projects as Project objects
302
- """
303
- response = commons.api_iterative(
304
- f"https://api.scratch.mit.edu/studios/{self.id}/projects", limit=limit, offset=offset)
305
- return commons.parse_object_list(response, project.Project, self._session)
306
-
307
- def curators(self, limit=40, offset=0):
308
- """
309
- Gets the studio curators.
310
-
311
- Keyword arguments:
312
- limit (int): Max amount of returned curators.
313
- offset (int): Offset of the first returned curator.
314
-
315
- Returns:
316
- list<scratchattach.user.User>: A list containing the studio curators as User objects
317
- """
318
- response = commons.api_iterative(
319
- f"https://api.scratch.mit.edu/studios/{self.id}/curators", limit=limit, offset=offset)
320
- return commons.parse_object_list(response, user.User, self._session, "username")
321
-
322
-
323
- def invite_curator(self, curator):
324
- """
325
- You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
326
- """
327
- self._assert_auth()
328
- try:
329
- return requests.put(
330
- f"https://scratch.mit.edu/site-api/users/curators-in/{self.id}/invite_curator/?usernames={curator}",
331
- headers=headers,
332
- cookies=self._cookies,
333
- timeout=10,
334
- ).json()
335
- except Exception:
336
- raise (exceptions.Unauthorized)
337
-
338
- def promote_curator(self, curator):
339
- """
340
- You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
341
- """
342
- self._assert_auth()
343
- try:
344
- return requests.put(
345
- f"https://scratch.mit.edu/site-api/users/curators-in/{self.id}/promote/?usernames={curator}",
346
- headers=headers,
347
- cookies=self._cookies,
348
- timeout=10,
349
- ).json()
350
- except Exception:
351
- raise (exceptions.Unauthorized)
352
-
353
- def remove_curator(self, curator):
354
- """
355
- You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
356
- """
357
- self._assert_auth()
358
- try:
359
- return requests.put(
360
- f"https://scratch.mit.edu/site-api/users/curators-in/{self.id}/remove/?usernames={curator}",
361
- headers=headers,
362
- cookies=self._cookies,
363
- timeout=10,
364
- ).json()
365
- except Exception:
366
- raise (exceptions.Unauthorized)
367
-
368
- def transfer_ownership(self, new_owner, *, password):
369
- """
370
- Makes another Scratcher studio host. You need to specify your password to do this.
371
-
372
- Arguments:
373
- new_owner (str): Username of new host
374
-
375
- Keyword arguments:
376
- password (str): The password of your Scratch account
377
-
378
- Warning:
379
- This action is irreversible!
380
- """
381
- self._assert_auth()
382
- try:
383
- return requests.put(
384
- f"https://api.scratch.mit.edu/studios/{self.id}/transfer/{new_owner}",
385
- headers=self._headers,
386
- cookies=self._cookies,
387
- timeout=10,
388
- json={"password":password}
389
- ).json()
390
- except Exception:
391
- raise (exceptions.Unauthorized)
392
-
393
-
394
- def leave(self):
395
- """
396
- Removes yourself from the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
397
- """
398
- self._assert_auth()
399
- return self.remove_curator(self._session._username)
400
-
401
- def add_project(self, project_id):
402
- """
403
- Adds a project to the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
404
-
405
- Args:
406
- project_id: Project id of the project that should be added
407
- """
408
- self._assert_auth()
409
- return requests.post(
410
- f"https://api.scratch.mit.edu/studios/{self.id}/project/{project_id}",
411
- headers=self._headers,
412
- timeout=10,
413
- ).json()
414
-
415
- def remove_project(self, project_id):
416
- """
417
- Removes a project from the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
418
-
419
- Args:
420
- project_id: Project id of the project that should be removed
421
- """
422
- self._assert_auth()
423
- return requests.delete(
424
- f"https://api.scratch.mit.edu/studios/{self.id}/project/{project_id}",
425
- headers=self._headers,
426
- timeout=10,
427
- ).json()
428
-
429
- def managers(self, limit=40, offset=0):
430
- """
431
- Gets the studio managers.
432
-
433
- Keyword arguments:
434
- limit (int): Max amount of returned managers
435
- offset (int): Offset of the first returned manager.
436
-
437
- Returns:
438
- list<scratchattach.user.User>: A list containing the studio managers as user objects
439
- """
440
- response = commons.api_iterative(
441
- f"https://api.scratch.mit.edu/studios/{self.id}/managers", limit=limit, offset=offset)
442
- return commons.parse_object_list(response, user.User, self._session, "username")
443
-
444
- def host(self):
445
- """
446
- Gets the studio host.
447
-
448
- Returns:
449
- scratchattach.user.User: An object representing the studio host.
450
- """
451
- managers = self.managers(limit=1, offset=0)
452
- try:
453
- return managers[0]
454
- except Exception:
455
- return None
456
-
457
- def set_fields(self, fields_dict):
458
- """
459
- Sets fields. Uses the scratch.mit.edu/site-api PUT API.
460
- """
461
- self._assert_auth()
462
- requests.put(
463
- f"https://scratch.mit.edu/site-api/galleries/all/{self.id}/",
464
- headers=headers,
465
- cookies=self._cookies,
466
- data=json.dumps(fields_dict),
467
- timeout=10,
468
- )
469
-
470
-
471
- def set_description(self, new):
472
- """
473
- You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
474
- """
475
- self.set_fields({"description": new + "\n"})
476
-
477
- def set_title(self, new):
478
- """
479
- You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
480
- """
481
- self.set_fields({"title": new})
482
-
483
- def open_projects(self):
484
- """
485
- Changes the studio settings so everyone (including non-curators) is able to add projects to the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
486
- """
487
- self._assert_auth()
488
- requests.put(
489
- f"https://scratch.mit.edu/site-api/galleries/{self.id}/mark/open/",
490
- headers=headers,
491
- cookies=self._cookies,
492
- timeout=10,
493
- )
494
-
495
- def close_projects(self):
496
- """
497
- Changes the studio settings so only curators can add projects to the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
498
- """
499
- self._assert_auth()
500
- requests.put(
501
- f"https://scratch.mit.edu/site-api/galleries/{self.id}/mark/closed/",
502
- headers=headers,
503
- cookies=self._cookies,
504
- timeout=10,
505
- )
506
-
507
- def turn_off_commenting(self):
508
- """
509
- You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
510
- """
511
- self._assert_auth()
512
- if self.comments_allowed:
513
- requests.post(
514
- f"https://scratch.mit.edu/site-api/comments/gallery/{self.id}/toggle-comments/",
515
- headers=headers,
516
- cookies=self._cookies,
517
- timeout=10,
518
- )
519
- self.comments_allowed = not self.comments_allowed
520
-
521
- def turn_on_commenting(self):
522
- """
523
- You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
524
- """
525
- self._assert_auth()
526
- if not self.comments_allowed:
527
- requests.post(
528
- f"https://scratch.mit.edu/site-api/comments/gallery/{self.id}/toggle-comments/",
529
- headers=headers,
530
- cookies=self._cookies,
531
- timeout=10,
532
- )
533
- self.comments_allowed = not self.comments_allowed
534
-
535
- def toggle_commenting(self):
536
- """
537
- You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
538
- """
539
- self._assert_auth()
540
- requests.post(
541
- f"https://scratch.mit.edu/site-api/comments/gallery/{self.id}/toggle-comments/",
542
- headers=headers,
543
- cookies=self._cookies,
544
- timeout=10,
545
- )
546
- self.comments_allowed = not self.comments_allowed
547
-
548
- def activity(self, *, limit=40, offset=0, date_limit=None):
549
- add_params = ""
550
- if date_limit is not None:
551
- add_params = f"&dateLimit={date_limit}"
552
- response = commons.api_iterative(
553
- f"https://api.scratch.mit.edu/studios/{self.id}/activity", limit=limit, offset=offset, add_params=add_params)
554
- return commons.parse_object_list(response, activity.Activity, self._session)
555
-
556
- def accept_invite(self):
557
- self._assert_auth()
558
- return requests.put(
559
- f"https://scratch.mit.edu/site-api/users/curators-in/{self.id}/add/?usernames={self._session._username}",
560
- headers=headers,
561
- cookies=self._cookies,
562
- timeout=10,
563
- ).json()
564
-
565
- def your_role(self):
566
- """
567
- Returns a dict with information about your role in the studio (whether you're following, curating, managing it or are invited)
568
- """
569
- self._assert_auth()
570
- return requests.get(
571
- f"https://api.scratch.mit.edu/studios/{self.id}/users/{self._session.username}",
572
- headers=self._headers,
573
- cookies=self._cookies,
574
- timeout=10,
575
- ).json()
576
-
577
-
578
- def get_studio(studio_id) -> Studio:
579
- """
580
- Gets a studio without logging in.
581
-
582
- Args:
583
- studio_id (int): Studio id of the requested studio
584
-
585
- Returns:
586
- scratchattach.studio.Studio: An object representing the requested studio
587
-
588
- Warning:
589
- Any methods that authentication (like studio.follow) will not work on the returned object.
590
-
591
- If you want to use these, get the studio with :meth:`scratchattach.session.Session.connect_studio` instead.
592
- """
593
- print("Warning: For methods that require authentication, use session.connect_studio instead of get_studio")
594
- return commons._get_object("id", studio_id, Studio, exceptions.StudioNotFound)
595
-
596
- def search_studios(*, query="", mode="trending", language="en", limit=40, offset=0):
597
- if not query:
598
- raise ValueError("The query can't be empty for search")
599
- response = commons.api_iterative(
600
- f"https://api.scratch.mit.edu/search/studios", limit=limit, offset=offset, add_params=f"&language={language}&mode={mode}&q={query}")
601
- return commons.parse_object_list(response, Studio)
602
-
603
-
604
- def explore_studios(*, query="", mode="trending", language="en", limit=40, offset=0):
605
- if not query:
606
- raise ValueError("The query can't be empty for explore")
607
- response = commons.api_iterative(
608
- f"https://api.scratch.mit.edu/explore/studios", limit=limit, offset=offset, add_params=f"&language={language}&mode={mode}&q={query}")
609
- return commons.parse_object_list(response, Studio)
1
+ """Studio class"""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import random
6
+ from . import user, comment, project, activity
7
+ from ..utils import exceptions, commons
8
+ from ..utils.commons import api_iterative, headers
9
+ from ._base import BaseSiteComponent
10
+
11
+ from ..utils.requests import Requests as requests
12
+
13
+
14
+ class Studio(BaseSiteComponent):
15
+ """
16
+ Represents a Scratch studio.
17
+
18
+ Attributes:
19
+
20
+ :.id:
21
+
22
+ :.title:
23
+
24
+ :.description:
25
+
26
+ :.host_id: The user id of the studio host
27
+
28
+ :.open_to_all: Whether everyone is allowed to add projects
29
+
30
+ :.comments_allowed:
31
+
32
+ :.image_url:
33
+
34
+ :.created:
35
+
36
+ :.modified:
37
+
38
+ :.follower_count:
39
+
40
+ :.manager_count:
41
+
42
+ :.project_count:
43
+
44
+ :.update(): Updates the attributes
45
+
46
+ """
47
+
48
+ def __init__(self, **entries):
49
+
50
+ # Info on how the .update method has to fetch the data:
51
+ self.update_function = requests.get
52
+ self.update_API = f"https://api.scratch.mit.edu/studios/{entries['id']}"
53
+
54
+ # Set attributes every Project object needs to have:
55
+ self._session = None
56
+ self.id = 0
57
+
58
+ # Update attributes from entries dict:
59
+ self.__dict__.update(entries)
60
+
61
+ # Headers and cookies:
62
+ if self._session is None:
63
+ self._headers = headers
64
+ self._cookies = {}
65
+ else:
66
+ self._headers = self._session._headers
67
+ self._cookies = self._session._cookies
68
+
69
+ # Headers for operations that require accept and Content-Type fields:
70
+ self._json_headers = dict(self._headers)
71
+ self._json_headers["accept"] = "application/json"
72
+ self._json_headers["Content-Type"] = "application/json"
73
+
74
+ def _update_from_dict(self, studio):
75
+ try: self.id = int(studio["id"])
76
+ except Exception: pass
77
+ try: self.title = studio["title"]
78
+ except Exception: pass
79
+ try: self.description = studio["description"]
80
+ except Exception: pass
81
+ try: self.host_id = studio["host"]
82
+ except Exception: pass
83
+ try: self.open_to_all = studio["open_to_all"]
84
+ except Exception: pass
85
+ try: self.comments_allowed = studio["comments_allowed"]
86
+ except Exception: pass
87
+ try: self.image_url = studio["image"]
88
+ except Exception: pass
89
+ try: self.created = studio["history"]["created"]
90
+ except Exception: pass
91
+ try: self.modified = studio["history"]["modified"]
92
+ except Exception: pass
93
+ try: self.follower_count = studio["stats"]["followers"]
94
+ except Exception: pass
95
+ try: self.manager_count = studio["stats"]["managers"]
96
+ except Exception: pass
97
+ try: self.project_count = studio["stats"]["projects"]
98
+ except Exception: pass
99
+ return True
100
+
101
+ def follow(self):
102
+ """
103
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
104
+ """
105
+ self._assert_auth()
106
+ requests.put(
107
+ f"https://scratch.mit.edu/site-api/users/bookmarkers/{self.id}/add/?usernames={self._session._username}",
108
+ headers=headers,
109
+ cookies=self._cookies,
110
+ timeout=10,
111
+ )
112
+
113
+ def unfollow(self):
114
+ """
115
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
116
+ """
117
+ self._assert_auth()
118
+ requests.put(
119
+ f"https://scratch.mit.edu/site-api/users/bookmarkers/{self.id}/remove/?usernames={self._session._username}",
120
+ headers=headers,
121
+ cookies=self._cookies,
122
+ timeout=10,
123
+ )
124
+
125
+ def comments(self, *, limit=40, offset=0) -> list[comment.Comment]:
126
+ """
127
+ Returns the comments posted on the studio (except for replies. To get replies use :meth:`scratchattach.studio.Studio.get_comment_replies`).
128
+
129
+ Keyword Arguments:
130
+ page: The page of the comments that should be returned.
131
+ limit: Max. amount of returned comments.
132
+
133
+ Returns:
134
+ list<Comment>: A list containing the requested comments as Comment objects.
135
+ """
136
+ response = commons.api_iterative(
137
+ f"https://api.scratch.mit.edu/studios/{self.id}/comments/", limit=limit, offset=offset, add_params=f"&cachebust={random.randint(0,9999)}")
138
+ for i in response:
139
+ i["source"] = "studio"
140
+ i["source_id"] = self.id
141
+ return commons.parse_object_list(response, comment.Comment, self._session)
142
+
143
+ def comment_replies(self, *, comment_id, limit=40, offset=0) -> list[comment.Comment]:
144
+ response = commons.api_iterative(
145
+ f"https://api.scratch.mit.edu/studios/{self.id}/comments/{comment_id}/replies", limit=limit, offset=offset, add_params=f"&cachebust={random.randint(0,9999)}")
146
+ for x in response:
147
+ x["parent_id"] = comment_id
148
+ x["source"] = "studio"
149
+ x["source_id"] = self.id
150
+ return commons.parse_object_list(response, comment.Comment, self._session)
151
+
152
+ def comment_by_id(self, comment_id):
153
+ r = requests.get(
154
+ f"https://api.scratch.mit.edu/studios/{self.id}/comments/{comment_id}",
155
+ timeout=10,
156
+ ).json()
157
+ if r is None:
158
+ raise exceptions.CommentNotFound()
159
+ _comment = comment.Comment(id=r["id"], _session=self._session, source="studio", source_id=self.id)
160
+ _comment._update_from_dict(r)
161
+ return _comment
162
+
163
+ def post_comment(self, content, *, parent_id="", commentee_id=""):
164
+ """
165
+ Posts a comment on the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
166
+
167
+ Args:
168
+ content: Content of the comment that should be posted
169
+
170
+ Keyword Arguments:
171
+ 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.
172
+ 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.
173
+
174
+ Returns:
175
+ scratchattach.comment.Comment: The posted comment as Comment object.
176
+ """
177
+ self._assert_auth()
178
+ data = {
179
+ "commentee_id": commentee_id,
180
+ "content": str(content),
181
+ "parent_id": parent_id,
182
+ }
183
+ headers = dict(self._json_headers)
184
+ headers["referer"] = "https://scratch.mit.edu/projects/" + str(self.id) + "/"
185
+ r = requests.post(
186
+ f"https://api.scratch.mit.edu/proxy/comments/studio/{self.id}/",
187
+ headers=headers,
188
+ cookies=self._cookies,
189
+ data=json.dumps(data),
190
+ timeout=10,
191
+ ).json()
192
+ if "id" not in r:
193
+ raise exceptions.CommentPostFailure(r)
194
+ _comment = comment.Comment(id=r["id"], _session=self._session, source="studio", source_id=self.id)
195
+ _comment._update_from_dict(r)
196
+ return _comment
197
+
198
+ def delete_comment(self, *, comment_id):
199
+ # NEEDS TO BE TESTED!
200
+ """
201
+ Deletes a comment by ID. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
202
+
203
+ Args:
204
+ comment_id: The id of the comment that should be deleted
205
+ """
206
+ self._assert_auth()
207
+ return requests.delete(
208
+ f"https://api.scratch.mit.edu/proxy/comments/studio/{self.id}/comment/{comment_id}/",
209
+ headers=self._headers,
210
+ cookies=self._cookies,
211
+ ).headers
212
+
213
+ def report_comment(self, *, comment_id):
214
+ # NEEDS TO BE TESTED!
215
+ """
216
+ Reports a comment by ID to the Scratch team. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
217
+
218
+ Args:
219
+ comment_id: The id of the comment that should be reported
220
+ """
221
+ self._assert_auth()
222
+ return requests.delete(
223
+ f"https://api.scratch.mit.edu/proxy/comments/studio/{self.id}/comment/{comment_id}/report",
224
+ headers=self._headers,
225
+ cookies=self._cookies,
226
+ )
227
+
228
+ def set_thumbnail(self, *, file):
229
+ """
230
+ Sets the studio thumbnail. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
231
+
232
+ Keyword Arguments:
233
+ file: The path to the image file
234
+
235
+ Returns:
236
+ str: Scratch cdn link to the set thumbnail
237
+ """
238
+ self._assert_auth()
239
+ with open(file, "rb") as f:
240
+ thumbnail = f.read()
241
+
242
+ filename = file.replace("\\", "/")
243
+ if filename.endswith("/"):
244
+ filename = filename[:-1]
245
+ filename = filename.split("/").pop()
246
+
247
+ file_type = filename.split(".").pop()
248
+
249
+ payload1 = f'------WebKitFormBoundaryhKZwFjoxAyUTMlSh\r\nContent-Disposition: form-data; name="file"; filename="{filename}"\r\nContent-Type: image/{file_type}\r\n\r\n'
250
+ payload1 = payload1.encode("utf-8")
251
+ payload2 = b"\r\n------WebKitFormBoundaryhKZwFjoxAyUTMlSh--\r\n"
252
+ payload = b"".join([payload1, thumbnail, payload2])
253
+
254
+ r = requests.post(
255
+ f"https://scratch.mit.edu/site-api/galleries/all/{self.id}/",
256
+ headers={
257
+ "accept": "*/",
258
+ "content-type": "multipart/form-data; boundary=----WebKitFormBoundaryhKZwFjoxAyUTMlSh",
259
+ "Referer": "https://scratch.mit.edu/",
260
+ "x-csrftoken": "a",
261
+ "x-requested-with": "XMLHttpRequest",
262
+ },
263
+ data=payload,
264
+ cookies=self._cookies,
265
+ timeout=10,
266
+ ).json()
267
+
268
+ if "errors" in r:
269
+ raise (exceptions.BadRequest(", ".join(r["errors"])))
270
+ else:
271
+ return r["thumbnail_url"]
272
+
273
+ def reply_comment(self, content, *, parent_id, commentee_id=""):
274
+ """
275
+ Posts a reply to a comment on the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
276
+
277
+ Args:
278
+ content: Content of the comment that should be posted
279
+
280
+ Warning:
281
+ 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.
282
+
283
+ Therefore, parent_id should be the comment id of a top level comment.
284
+
285
+ Keyword Arguments:
286
+ parent_id: ID of the comment you want to reply to
287
+ commentee_id: ID of the user you are replying to
288
+ """
289
+ self._assert_auth()
290
+ return self.post_comment(
291
+ content, parent_id=parent_id, commentee_id=commentee_id
292
+ )
293
+
294
+ def projects(self, limit=40, offset=0):
295
+ """
296
+ Gets the studio projects.
297
+
298
+ Keyword arguments:
299
+ limit (int): Max amount of returned projects.
300
+ offset (int): Offset of the first returned project.
301
+
302
+ Returns:
303
+ list<scratchattach.project.Project>: A list containing the studio projects as Project objects
304
+ """
305
+ response = commons.api_iterative(
306
+ f"https://api.scratch.mit.edu/studios/{self.id}/projects", limit=limit, offset=offset)
307
+ return commons.parse_object_list(response, project.Project, self._session)
308
+
309
+ def curators(self, limit=40, offset=0):
310
+ """
311
+ Gets the studio curators.
312
+
313
+ Keyword arguments:
314
+ limit (int): Max amount of returned curators.
315
+ offset (int): Offset of the first returned curator.
316
+
317
+ Returns:
318
+ list<scratchattach.user.User>: A list containing the studio curators as User objects
319
+ """
320
+ response = commons.api_iterative(
321
+ f"https://api.scratch.mit.edu/studios/{self.id}/curators", limit=limit, offset=offset)
322
+ return commons.parse_object_list(response, user.User, self._session, "username")
323
+
324
+
325
+ def invite_curator(self, curator):
326
+ """
327
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
328
+ """
329
+ self._assert_auth()
330
+ try:
331
+ return requests.put(
332
+ f"https://scratch.mit.edu/site-api/users/curators-in/{self.id}/invite_curator/?usernames={curator}",
333
+ headers=headers,
334
+ cookies=self._cookies,
335
+ timeout=10,
336
+ ).json()
337
+ except Exception:
338
+ raise (exceptions.Unauthorized)
339
+
340
+ def promote_curator(self, curator):
341
+ """
342
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
343
+ """
344
+ self._assert_auth()
345
+ try:
346
+ return requests.put(
347
+ f"https://scratch.mit.edu/site-api/users/curators-in/{self.id}/promote/?usernames={curator}",
348
+ headers=headers,
349
+ cookies=self._cookies,
350
+ timeout=10,
351
+ ).json()
352
+ except Exception:
353
+ raise (exceptions.Unauthorized)
354
+
355
+ def remove_curator(self, curator):
356
+ """
357
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
358
+ """
359
+ self._assert_auth()
360
+ try:
361
+ return requests.put(
362
+ f"https://scratch.mit.edu/site-api/users/curators-in/{self.id}/remove/?usernames={curator}",
363
+ headers=headers,
364
+ cookies=self._cookies,
365
+ timeout=10,
366
+ ).json()
367
+ except Exception:
368
+ raise (exceptions.Unauthorized)
369
+
370
+ def transfer_ownership(self, new_owner, *, password):
371
+ """
372
+ Makes another Scratcher studio host. You need to specify your password to do this.
373
+
374
+ Arguments:
375
+ new_owner (str): Username of new host
376
+
377
+ Keyword arguments:
378
+ password (str): The password of your Scratch account
379
+
380
+ Warning:
381
+ This action is irreversible!
382
+ """
383
+ self._assert_auth()
384
+ try:
385
+ return requests.put(
386
+ f"https://api.scratch.mit.edu/studios/{self.id}/transfer/{new_owner}",
387
+ headers=self._headers,
388
+ cookies=self._cookies,
389
+ timeout=10,
390
+ json={"password":password}
391
+ ).json()
392
+ except Exception:
393
+ raise (exceptions.Unauthorized)
394
+
395
+
396
+ def leave(self):
397
+ """
398
+ Removes yourself from the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
399
+ """
400
+ self._assert_auth()
401
+ return self.remove_curator(self._session._username)
402
+
403
+ def add_project(self, project_id):
404
+ """
405
+ Adds a project to the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
406
+
407
+ Args:
408
+ project_id: Project id of the project that should be added
409
+ """
410
+ self._assert_auth()
411
+ return requests.post(
412
+ f"https://api.scratch.mit.edu/studios/{self.id}/project/{project_id}",
413
+ headers=self._headers,
414
+ timeout=10,
415
+ ).json()
416
+
417
+ def remove_project(self, project_id):
418
+ """
419
+ Removes a project from the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
420
+
421
+ Args:
422
+ project_id: Project id of the project that should be removed
423
+ """
424
+ self._assert_auth()
425
+ return requests.delete(
426
+ f"https://api.scratch.mit.edu/studios/{self.id}/project/{project_id}",
427
+ headers=self._headers,
428
+ timeout=10,
429
+ ).json()
430
+
431
+ def managers(self, limit=40, offset=0):
432
+ """
433
+ Gets the studio managers.
434
+
435
+ Keyword arguments:
436
+ limit (int): Max amount of returned managers
437
+ offset (int): Offset of the first returned manager.
438
+
439
+ Returns:
440
+ list<scratchattach.user.User>: A list containing the studio managers as user objects
441
+ """
442
+ response = commons.api_iterative(
443
+ f"https://api.scratch.mit.edu/studios/{self.id}/managers", limit=limit, offset=offset)
444
+ return commons.parse_object_list(response, user.User, self._session, "username")
445
+
446
+ def host(self):
447
+ """
448
+ Gets the studio host.
449
+
450
+ Returns:
451
+ scratchattach.user.User: An object representing the studio host.
452
+ """
453
+ managers = self.managers(limit=1, offset=0)
454
+ try:
455
+ return managers[0]
456
+ except Exception:
457
+ return None
458
+
459
+ def set_fields(self, fields_dict):
460
+ """
461
+ Sets fields. Uses the scratch.mit.edu/site-api PUT API.
462
+ """
463
+ self._assert_auth()
464
+ requests.put(
465
+ f"https://scratch.mit.edu/site-api/galleries/all/{self.id}/",
466
+ headers=headers,
467
+ cookies=self._cookies,
468
+ data=json.dumps(fields_dict),
469
+ timeout=10,
470
+ )
471
+
472
+
473
+ def set_description(self, new):
474
+ """
475
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
476
+ """
477
+ self.set_fields({"description": new + "\n"})
478
+
479
+ def set_title(self, new):
480
+ """
481
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
482
+ """
483
+ self.set_fields({"title": new})
484
+
485
+ def open_projects(self):
486
+ """
487
+ Changes the studio settings so everyone (including non-curators) is able to add projects to the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
488
+ """
489
+ self._assert_auth()
490
+ requests.put(
491
+ f"https://scratch.mit.edu/site-api/galleries/{self.id}/mark/open/",
492
+ headers=headers,
493
+ cookies=self._cookies,
494
+ timeout=10,
495
+ )
496
+
497
+ def close_projects(self):
498
+ """
499
+ Changes the studio settings so only curators can add projects to the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
500
+ """
501
+ self._assert_auth()
502
+ requests.put(
503
+ f"https://scratch.mit.edu/site-api/galleries/{self.id}/mark/closed/",
504
+ headers=headers,
505
+ cookies=self._cookies,
506
+ timeout=10,
507
+ )
508
+
509
+ def turn_off_commenting(self):
510
+ """
511
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
512
+ """
513
+ self._assert_auth()
514
+ if self.comments_allowed:
515
+ requests.post(
516
+ f"https://scratch.mit.edu/site-api/comments/gallery/{self.id}/toggle-comments/",
517
+ headers=headers,
518
+ cookies=self._cookies,
519
+ timeout=10,
520
+ )
521
+ self.comments_allowed = not self.comments_allowed
522
+
523
+ def turn_on_commenting(self):
524
+ """
525
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
526
+ """
527
+ self._assert_auth()
528
+ if not self.comments_allowed:
529
+ requests.post(
530
+ f"https://scratch.mit.edu/site-api/comments/gallery/{self.id}/toggle-comments/",
531
+ headers=headers,
532
+ cookies=self._cookies,
533
+ timeout=10,
534
+ )
535
+ self.comments_allowed = not self.comments_allowed
536
+
537
+ def toggle_commenting(self):
538
+ """
539
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
540
+ """
541
+ self._assert_auth()
542
+ requests.post(
543
+ f"https://scratch.mit.edu/site-api/comments/gallery/{self.id}/toggle-comments/",
544
+ headers=headers,
545
+ cookies=self._cookies,
546
+ timeout=10,
547
+ )
548
+ self.comments_allowed = not self.comments_allowed
549
+
550
+ def activity(self, *, limit=40, offset=0, date_limit=None):
551
+ add_params = ""
552
+ if date_limit is not None:
553
+ add_params = f"&dateLimit={date_limit}"
554
+ response = commons.api_iterative(
555
+ f"https://api.scratch.mit.edu/studios/{self.id}/activity", limit=limit, offset=offset, add_params=add_params)
556
+ return commons.parse_object_list(response, activity.Activity, self._session)
557
+
558
+ def accept_invite(self):
559
+ self._assert_auth()
560
+ return requests.put(
561
+ f"https://scratch.mit.edu/site-api/users/curators-in/{self.id}/add/?usernames={self._session._username}",
562
+ headers=headers,
563
+ cookies=self._cookies,
564
+ timeout=10,
565
+ ).json()
566
+
567
+ def your_role(self):
568
+ """
569
+ Returns a dict with information about your role in the studio (whether you're following, curating, managing it or are invited)
570
+ """
571
+ self._assert_auth()
572
+ return requests.get(
573
+ f"https://api.scratch.mit.edu/studios/{self.id}/users/{self._session.username}",
574
+ headers=self._headers,
575
+ cookies=self._cookies,
576
+ timeout=10,
577
+ ).json()
578
+
579
+
580
+ def get_studio(studio_id) -> Studio:
581
+ """
582
+ Gets a studio without logging in.
583
+
584
+ Args:
585
+ studio_id (int): Studio id of the requested studio
586
+
587
+ Returns:
588
+ scratchattach.studio.Studio: An object representing the requested studio
589
+
590
+ Warning:
591
+ Any methods that authentication (like studio.follow) will not work on the returned object.
592
+
593
+ If you want to use these, get the studio with :meth:`scratchattach.session.Session.connect_studio` instead.
594
+ """
595
+ print("Warning: For methods that require authentication, use session.connect_studio instead of get_studio")
596
+ return commons._get_object("id", studio_id, Studio, exceptions.StudioNotFound)
597
+
598
+ def search_studios(*, query="", mode="trending", language="en", limit=40, offset=0):
599
+ if not query:
600
+ raise ValueError("The query can't be empty for search")
601
+ response = commons.api_iterative(
602
+ f"https://api.scratch.mit.edu/search/studios", limit=limit, offset=offset, add_params=f"&language={language}&mode={mode}&q={query}")
603
+ return commons.parse_object_list(response, Studio)
604
+
605
+
606
+ def explore_studios(*, query="", mode="trending", language="en", limit=40, offset=0):
607
+ if not query:
608
+ raise ValueError("The query can't be empty for explore")
609
+ response = commons.api_iterative(
610
+ f"https://api.scratch.mit.edu/explore/studios", limit=limit, offset=offset, add_params=f"&language={language}&mode={mode}&q={query}")
611
+ return commons.parse_object_list(response, Studio)