scratchattach 2.1.15b0__py3-none-any.whl → 3.0.0b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. scratchattach/__init__.py +14 -6
  2. scratchattach/__main__.py +93 -0
  3. {scratchattach-2.1.15b0.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.15b0.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 -273
  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 -826
  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.15b0.dist-info/RECORD +0 -66
  68. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b0.dist-info}/licenses/LICENSE +0 -0
  69. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b0.dist-info}/top_level.txt +0 -0
@@ -1,956 +0,0 @@
1
- """User class"""
2
- from __future__ import annotations
3
-
4
- import json
5
- import random
6
- import re
7
- import string
8
- from datetime import datetime, timezone
9
- from enum import Enum
10
- import warnings
11
-
12
- from typing_extensions import deprecated
13
- from bs4 import BeautifulSoup, Tag
14
-
15
- from ._base import BaseSiteComponent
16
- from scratchattach.eventhandlers import message_events
17
-
18
- from scratchattach.utils import commons
19
- from scratchattach.utils import exceptions
20
- from scratchattach.utils.commons import headers
21
- from scratchattach.utils.requests import requests
22
-
23
- from . import project
24
- from . import studio
25
- from . import forum
26
- from . import comment
27
- from . import activity
28
- from . import classroom
29
-
30
- class Rank(Enum):
31
- """
32
- Possible ranks in scratch
33
- """
34
- NEW_SCRATCHER = 0
35
- SCRATCHER = 1
36
- SCRATCH_TEAM = 2
37
-
38
- class Verificator:
39
-
40
- def __init__(self, user: User, project_id: int):
41
- self.project = user._make_linked_object("id", project_id, project.Project, exceptions.ProjectNotFound)
42
- self.projecturl = self.project.url
43
- self.code = ''.join(random.choices(string.ascii_letters + string.digits, k=8))
44
- self.username = user.username
45
-
46
- def check(self) -> bool:
47
- return bool(list(filter(lambda x : x.author_name == self.username and (x.content == self.code or x.content.startswith(self.code) or x.content.endswith(self.code)), self.project.comments())))
48
-
49
- class User(BaseSiteComponent):
50
-
51
- '''
52
- Represents a Scratch user.
53
-
54
- Attributes:
55
-
56
- :.join_date:
57
-
58
- :.about_me:
59
-
60
- :.wiwo: Returns the user's 'What I'm working on' section
61
-
62
- :.country: Returns the country from the user profile
63
-
64
- :.icon_url: Returns the link to the user's pfp (90x90)
65
-
66
- :.id: Returns the id of the user
67
-
68
- :.scratchteam: Retuns True if the user is in the Scratch team
69
-
70
- :.update(): Updates the attributes
71
- '''
72
-
73
- def __str__(self):
74
- return str(self.username)
75
-
76
- def __init__(self, **entries):
77
-
78
- # Info on how the .update method has to fetch the data:
79
- self.update_function = requests.get
80
- self.update_api = f"https://api.scratch.mit.edu/users/{entries['username']}"
81
-
82
- # Set attributes every User object needs to have:
83
- self._session = None
84
- self.id = None
85
- self.username = None
86
- self.name = None
87
-
88
- # cache value for classroom getter method (using @property)
89
- # first value is whether the cache has actually been set (because it can be None), second is the value itself
90
- self._classroom: tuple[bool, classroom.Classroom | None] = False, None
91
-
92
- # Update attributes from entries dict:
93
- entries.setdefault("name", entries.get("username"))
94
- self.__dict__.update(entries)
95
-
96
- # Set alternative attributes:
97
- if hasattr(self, "bio"):
98
- self.about_me = self.bio
99
- if hasattr(self, "status"):
100
- self.wiwo = self.status
101
- if hasattr(self, "name"):
102
- self.username = self.name
103
-
104
- # Headers and cookies:
105
- if self._session is None:
106
- self._headers :dict = headers
107
- self._cookies = {}
108
- else:
109
- self._headers :dict = self._session._headers
110
- self._cookies = self._session._cookies
111
-
112
- # Headers for operations that require accept and Content-Type fields:
113
- self._json_headers = dict(self._headers)
114
- self._json_headers["accept"] = "application/json"
115
- self._json_headers["Content-Type"] = "application/json"
116
-
117
- def _update_from_dict(self, data):
118
- try: self.id = data["id"]
119
- except KeyError: pass
120
- try: self.username = data["username"]
121
- except KeyError: pass
122
- try: self.scratchteam = data["scratchteam"]
123
- except KeyError: pass
124
- try: self.join_date = data["history"]["joined"]
125
- except KeyError: pass
126
- try: self.about_me = data["profile"]["bio"]
127
- except KeyError: pass
128
- try: self.wiwo = data["profile"]["status"]
129
- except KeyError: pass
130
- try: self.country = data["profile"]["country"]
131
- except KeyError: pass
132
- try: self.icon_url = data["profile"]["images"]["90x90"]
133
- except KeyError: pass
134
- return True
135
-
136
- def _assert_permission(self):
137
- self._assert_auth()
138
- if self._session._username != self.username:
139
- raise exceptions.Unauthorized(
140
- "You need to be authenticated as the profile owner to do this.")
141
-
142
- @property
143
- def classroom(self) -> classroom.Classroom | None:
144
- """
145
- Get a user's associated classroom, and return it as a `scratchattach.classroom.Classroom` object.
146
- If there is no associated classroom, returns `None`
147
- """
148
- if not self._classroom[0]:
149
- with requests.no_error_handling():
150
- resp = requests.get(f"https://scratch.mit.edu/users/{self.username}/")
151
- soup = BeautifulSoup(resp.text, "html.parser")
152
-
153
- details = soup.find("p", {"class": "profile-details"})
154
- assert isinstance(details, Tag)
155
-
156
- class_name, class_id, is_closed = None, 0, False
157
- for a in details.find_all("a"):
158
- if not isinstance(a, Tag):
159
- continue
160
- href = str(a.get("href"))
161
- if re.match(r"/classes/\d*/", href):
162
- class_name = a.text.strip()[len("Student of: "):]
163
- is_closed = class_name.endswith("\n (ended)") # as this has a \n, we can be sure
164
- if is_closed:
165
- class_name = class_name[:-7].strip()
166
-
167
- class_id = int(href.split('/')[2])
168
- break
169
-
170
- if class_name:
171
- self._classroom = True, classroom.Classroom(
172
- _session=self._session,
173
- id=class_id,
174
- title=class_name,
175
- is_closed=is_closed
176
- )
177
- else:
178
- self._classroom = True, None
179
-
180
- return self._classroom[1]
181
-
182
- def does_exist(self):
183
- """
184
- Returns:
185
- boolean : True if the user exists, False if the user is deleted, None if an error occured
186
- """
187
- with requests.no_error_handling():
188
- status_code = requests.get(f"https://scratch.mit.edu/users/{self.username}/").status_code
189
- if status_code == 200:
190
- return True
191
- elif status_code == 404:
192
- return False
193
-
194
- # Will maybe be deprecated later, but for now still has its own purpose.
195
- #@deprecated("This function is partially deprecated. Use user.rank() instead.")
196
- def is_new_scratcher(self):
197
- """
198
- Returns:
199
- boolean : True if the user has the New Scratcher status, else False
200
- """
201
- try:
202
- with requests.no_error_handling():
203
- res = requests.get(f"https://scratch.mit.edu/users/{self.username}/").text
204
- group = res[res.rindex('<span class="group">'):][:70]
205
- return "new scratcher" in group.lower()
206
-
207
- except Exception as e:
208
- warnings.warn(f"Caught exception {e=}")
209
- return None
210
-
211
- def message_count(self):
212
-
213
- return json.loads(requests.get(f"https://api.scratch.mit.edu/users/{self.username}/messages/count/?cachebust={random.randint(0,10000)}", headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.3c6 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36',}).text)["count"]
214
-
215
- def featured_data(self):
216
- """
217
- Returns:
218
- dict: Gets info on the user's featured project and featured label (like "Featured project", "My favorite things", etc.)
219
- """
220
- try:
221
- response = json.loads(requests.get(f"https://scratch.mit.edu/site-api/users/all/{self.username}/").text)
222
- return {
223
- "label":response["featured_project_label_name"],
224
- "project":
225
- dict(
226
- id=str(response["featured_project_data"]["id"]),
227
- author=response["featured_project_data"]["creator"],
228
- thumbnail_url="https://"+response["featured_project_data"]["thumbnail_url"][2:],
229
- title=response["featured_project_data"]["title"]
230
- )
231
- }
232
- except Exception:
233
- return None
234
-
235
- def follower_count(self):
236
- # follower count
237
- with requests.no_error_handling():
238
- text = requests.get(
239
- f"https://scratch.mit.edu/users/{self.username}/followers/",
240
- headers = self._headers
241
- ).text
242
- return commons.webscrape_count(text, "Followers (", ")")
243
-
244
- def following_count(self):
245
- # following count
246
- with requests.no_error_handling():
247
- text = requests.get(
248
- f"https://scratch.mit.edu/users/{self.username}/following/",
249
- headers = self._headers
250
- ).text
251
- return commons.webscrape_count(text, "Following (", ")")
252
-
253
- def followers(self, *, limit=40, offset=0):
254
- """
255
- Returns:
256
- list<scratchattach.user.User>: The user's followers as list of scratchattach.user.User objects
257
- """
258
- response = commons.api_iterative(
259
- f"https://api.scratch.mit.edu/users/{self.username}/followers/", limit=limit, offset=offset)
260
- return commons.parse_object_list(response, User, self._session, "username")
261
-
262
- def follower_names(self, *, limit=40, offset=0):
263
- """
264
- Returns:
265
- list<str>: The usernames of the user's followers
266
- """
267
- return [i.name for i in self.followers(limit=limit, offset=offset)]
268
-
269
- def following(self, *, limit=40, offset=0):
270
- """
271
- Returns:
272
- list<scratchattach.user.User>: The users that the user is following as list of scratchattach.user.User objects
273
- """
274
- response = commons.api_iterative(
275
- f"https://api.scratch.mit.edu/users/{self.username}/following/", limit=limit, offset=offset)
276
- return commons.parse_object_list(response, User, self._session, "username")
277
-
278
- def following_names(self, *, limit=40, offset=0):
279
- """
280
- Returns:
281
- list<str>: The usernames of the users the user is following
282
- """
283
- return [i.name for i in self.following(limit=limit, offset=offset)]
284
-
285
- def is_following(self, user: str):
286
- """
287
- Returns:
288
- boolean: Whether the user is following the user provided as argument
289
- """
290
- offset = 0
291
- following = False
292
-
293
- while True:
294
- try:
295
- following_names = self.following_names(limit=20, offset=offset)
296
- if user in following_names:
297
- following = True
298
- break
299
- if not following_names:
300
- break
301
- offset += 20
302
- except Exception as e:
303
- print(f"Warning: API error when performing following check: {e=}")
304
- return following
305
- return following
306
-
307
- def is_followed_by(self, user):
308
- """
309
- Returns:
310
- boolean: Whether the user is followed by the user provided as argument
311
- """
312
- offset = 0
313
- followed = False
314
-
315
- while True:
316
- try:
317
- followed_names = self.follower_names(limit=20, offset=offset)
318
- if user in followed_names:
319
- followed = True
320
- break
321
- if not followed_names:
322
- break
323
- offset += 20
324
- except Exception as e:
325
- print(f"Warning: API error when performing following check: {e=}")
326
- return followed
327
- return followed
328
-
329
- def project_count(self):
330
- with requests.no_error_handling():
331
- text = requests.get(
332
- f"https://scratch.mit.edu/users/{self.username}/projects/",
333
- headers = self._headers
334
- ).text
335
- return commons.webscrape_count(text, "Shared Projects (", ")")
336
-
337
- def studio_count(self):
338
- with requests.no_error_handling():
339
- text = requests.get(
340
- f"https://scratch.mit.edu/users/{self.username}/studios/",
341
- headers = self._headers
342
- ).text
343
- return commons.webscrape_count(text, "Studios I Curate (", ")")
344
-
345
- def studios_following_count(self):
346
- with requests.no_error_handling():
347
- text = requests.get(
348
- f"https://scratch.mit.edu/users/{self.username}/studios_following/",
349
- headers = self._headers
350
- ).text
351
- return commons.webscrape_count(text, "Studios I Follow (", ")")
352
-
353
- def studios(self, *, limit=40, offset=0):
354
- _studios = commons.api_iterative(
355
- f"https://api.scratch.mit.edu/users/{self.username}/studios/curate", limit=limit, offset=offset)
356
- studios = []
357
- for studio_dict in _studios:
358
- _studio = studio.Studio(_session = self._session, id = studio_dict["id"])
359
- _studio._update_from_dict(studio_dict)
360
- studios.append(_studio)
361
- return studios
362
-
363
- def projects(self, *, limit=40, offset=0) -> list[project.Project]:
364
- """
365
- Returns:
366
- list<projects.projects.Project>: The user's shared projects
367
- """
368
- _projects = commons.api_iterative(
369
- f"https://api.scratch.mit.edu/users/{self.username}/projects/", limit=limit, offset=offset, _headers= self._headers)
370
- for p in _projects:
371
- p["author"] = {"username":self.username}
372
- return commons.parse_object_list(_projects, project.Project, self._session)
373
-
374
- def loves(self, *, limit=40, offset=0, get_full_project: bool = False) -> list[project.Project]:
375
- """
376
- Returns:
377
- list<projects.projects.Project>: The user's loved projects
378
- """
379
- # We need to use beautifulsoup webscraping so we cant use the api_iterative function
380
- if offset < 0:
381
- raise exceptions.BadRequest("offset parameter must be >= 0")
382
- if limit < 0:
383
- raise exceptions.BadRequest("limit parameter must be >= 0")
384
-
385
- # There are 40 projects on display per page
386
- # So the first page you need to view is 1 + offset // 40
387
- # (You have to add one because the first page is idx 1 instead of 0)
388
-
389
- # The final project to view is at idx offset + limit - 1
390
- # (You have to -1 because the index starts at 0)
391
- # So the page number for this is 1 + (offset + limit - 1) // 40
392
-
393
- # But this is a range so we have to add another 1 for the second argument
394
- pages = range(1 + offset // 40,
395
- 2 + (offset + limit - 1) // 40)
396
- _projects = []
397
-
398
- for page in pages:
399
- # The index of the first project on page #n is just (n-1) * 40
400
- first_idx = (page - 1) * 40
401
-
402
- with requests.no_error_handling():
403
- page_content = requests.get(f"https://scratch.mit.edu/projects/all/{self.username}/loves/"
404
- f"?page={page}", headers=self._headers).content
405
-
406
- soup = BeautifulSoup(
407
- page_content,
408
- "html.parser"
409
- )
410
-
411
- # We need to check if we are out of bounds
412
- # If we are, we can jump out early
413
- # This is detectable if Scratch gives you a '404'
414
-
415
- # We can't just detect if the 404 text is within the whole of the page content
416
- # because it would break if someone made a project with that name
417
-
418
- # This page only uses <h1> tags for the 404 text, so we can just use a soup for those
419
- h1_tag = soup.find("h1")
420
- if h1_tag is not None:
421
- # Just to confirm that it's a 404, in case I am wrong. It can't hurt
422
- if "Whoops! Our server is Scratch'ing its head" in h1_tag.text:
423
- break
424
-
425
- # Each project element is a list item with the class name 'project thumb item' so we can just use that
426
- for i, project_element in enumerate(
427
- soup.find_all("li", {"class": "project thumb item"})):
428
- # Remember we only want certain projects:
429
- # The current project idx = first_idx + i
430
- # We want to start at {offset} and end at {offset + limit}
431
-
432
- # So the offset <= current project idx <= offset + limit
433
- if offset <= first_idx + i <= offset + limit:
434
- # Each of these elements provides:
435
- # A project id
436
- # A thumbnail link (no need to webscrape this)
437
- # A title
438
- # An Author (called an owner for some reason)
439
- assert isinstance(project_element, Tag)
440
- project_anchors = project_element.find_all("a")
441
- # Each list item has three <a> tags, the first two linking the project
442
- # 1st contains <img> tag
443
- # 2nd contains project title
444
- # 3rd links to the author & contains their username
445
-
446
- # This function is pretty handy!
447
- # I'll use it for an id from a string like: /projects/1070616180/
448
- first_anchor = project_anchors[0]
449
- second_anchor = project_anchors[1]
450
- third_anchor = project_anchors[2]
451
- assert isinstance(first_anchor, Tag)
452
- assert isinstance(second_anchor, Tag)
453
- assert isinstance(third_anchor, Tag)
454
- project_id = commons.webscrape_count(first_anchor.attrs["href"],
455
- "/projects/", "/")
456
- title = second_anchor.contents[0]
457
- author = third_anchor.contents[0]
458
-
459
- # Instantiating a project with the properties that we know
460
- # This may cause issues (see below)
461
- _project = project.Project(id=project_id,
462
- _session=self._session,
463
- title=title,
464
- author_name=author,
465
- url=f"https://scratch.mit.edu/projects/{project_id}/")
466
- if get_full_project:
467
- # Put this under an if statement since making api requests for every single
468
- # project will cause the function to take a lot longer
469
- _project.update()
470
-
471
- _projects.append(
472
- _project
473
- )
474
-
475
- return _projects
476
-
477
- def loves_count(self):
478
- with requests.no_error_handling():
479
- text = requests.get(
480
- f"https://scratch.mit.edu/projects/all/{self.username}/loves/",
481
- headers=self._headers
482
- ).text
483
-
484
- # If there are no loved projects, then Scratch doesn't actually display the number - so we have to catch this
485
- soup = BeautifulSoup(text, "html.parser")
486
-
487
- if not soup.find("li", {"class": "project thumb item"}):
488
- # There are no projects, so there are no projects loved
489
- return 0
490
-
491
- return commons.webscrape_count(text, "&raquo;\n\n (", ")")
492
-
493
- def favorites(self, *, limit=40, offset=0):
494
- """
495
- Returns:
496
- list<projects.projects.Project>: The user's favorite projects
497
- """
498
- _projects = commons.api_iterative(
499
- f"https://api.scratch.mit.edu/users/{self.username}/favorites/", limit=limit, offset=offset, _headers= self._headers)
500
- return commons.parse_object_list(_projects, project.Project, self._session)
501
-
502
- def favorites_count(self):
503
- with requests.no_error_handling():
504
- text = requests.get(
505
- f"https://scratch.mit.edu/users/{self.username}/favorites/",
506
- headers=self._headers
507
- ).text
508
- return commons.webscrape_count(text, "Favorites (", ")")
509
-
510
- def toggle_commenting(self):
511
- """
512
- You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_user`
513
- """
514
- self._assert_permission()
515
- requests.post(f"https://scratch.mit.edu/site-api/comments/user/{self.username}/toggle-comments/",
516
- headers = headers,
517
- cookies = self._cookies
518
- )
519
-
520
- def viewed_projects(self, limit=24, offset=0):
521
- """
522
- Returns:
523
- list<projects.projects.Project>: The user's recently viewed projects
524
-
525
- You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_user`
526
- """
527
- self._assert_permission()
528
- _projects = commons.api_iterative(
529
- f"https://api.scratch.mit.edu/users/{self.username}/projects/recentlyviewed", limit=limit, offset=offset, _headers= self._headers)
530
- return commons.parse_object_list(_projects, project.Project, self._session)
531
-
532
- def set_pfp(self, image: bytes):
533
- """
534
- Sets the user's profile picture. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_user`
535
- """
536
- # Teachers can set pfp! - Should update this method to check for that
537
- # self._assert_permission()
538
- requests.post(
539
- f"https://scratch.mit.edu/site-api/users/all/{self.username}/",
540
- headers=self._headers,
541
- cookies=self._cookies,
542
- files={"file": image})
543
-
544
- def set_bio(self, text):
545
- """
546
- Sets the user's "About me" section. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_user`
547
- """
548
- # Teachers can set bio! - Should update this method to check for that
549
- # self._assert_permission()
550
- requests.put(
551
- f"https://scratch.mit.edu/site-api/users/all/{self.username}/",
552
- headers=self._json_headers,
553
- cookies=self._cookies,
554
- json={"bio": text})
555
-
556
- def set_wiwo(self, text):
557
- """
558
- Sets the user's "What I'm working on" section. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_user`
559
- """
560
- # Teachers can also change your wiwo
561
- # self._assert_permission()
562
- requests.put(
563
- f"https://scratch.mit.edu/site-api/users/all/{self.username}/",
564
- headers=self._json_headers,
565
- cookies=self._cookies,
566
- json={"status": text})
567
-
568
- def set_featured(self, project_id, *, label=""):
569
- """
570
- Sets the user's featured project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_user`
571
-
572
- Args:
573
- project_id: Project id of the project that should be set as featured
574
-
575
- Keyword Args:
576
- label: The label that should appear above the featured project on the user's profile (Like "Featured project", "Featured tutorial", "My favorite things", etc.)
577
- """
578
- self._assert_permission()
579
- requests.put(
580
- f"https://scratch.mit.edu/site-api/users/all/{self.username}/",
581
- headers=self._json_headers,
582
- cookies=self._cookies,
583
- json={"featured_project": int(project_id), "featured_project_label": label}
584
- )
585
-
586
- def set_forum_signature(self, text):
587
- """
588
- Sets the user's discuss forum signature. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_user`
589
- """
590
- self._assert_permission()
591
- headers = {
592
- 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
593
- 'content-type': 'application/x-www-form-urlencoded',
594
- 'origin': 'https://scratch.mit.edu',
595
- 'referer': 'https://scratch.mit.edu/discuss/settings/TimMcCool/',
596
- 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
597
- }
598
- data = {
599
- 'csrfmiddlewaretoken': 'a',
600
- 'signature': text,
601
- 'update': '',
602
- }
603
- response = requests.post(f'https://scratch.mit.edu/discuss/settings/{self.username}/', cookies=self._cookies, headers=headers, data=data)
604
-
605
- def post_comment(self, content, *, parent_id="", commentee_id=""):
606
- """
607
- Posts a comment on the user's profile. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_user`
608
-
609
- Args:
610
- content: Content of the comment that should be posted
611
-
612
- Keyword Arguments:
613
- 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.
614
- 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.
615
-
616
- Returns:
617
- scratchattach.comment.Comment: An object representing the created comment.
618
- """
619
- self._assert_auth()
620
- data = {
621
- "commentee_id": commentee_id,
622
- "content": str(content),
623
- "parent_id": parent_id,
624
- }
625
- r = requests.post(
626
- f"https://scratch.mit.edu/site-api/comments/user/{self.username}/add/",
627
- headers=headers,
628
- cookies=self._cookies,
629
- data=json.dumps(data),
630
- )
631
- if r.status_code != 200:
632
- if "Looks like we are having issues with our servers!" in r.text:
633
- raise exceptions.BadRequest("Invalid arguments passed")
634
- else:
635
- raise exceptions.CommentPostFailure(r.text)
636
-
637
- try:
638
- text = r.text
639
- data = {
640
- 'id': text.split('<div id="comments-')[1].split('" class="comment')[0],
641
- 'author': {"username": text.split('" data-comment-user="')[1].split('"><img class')[0]},
642
- 'content': text.split('<div class="content">')[1].split('</div>')[0].strip(),
643
- 'reply_count': 0,
644
- 'cached_replies': []
645
- }
646
- _comment = comment.Comment(source="profile", parent_id=None if parent_id=="" else parent_id, commentee_id=commentee_id, source_id=self.username, id=data["id"], _session = self._session, datetime = datetime.now())
647
- _comment._update_from_dict(data)
648
- return _comment
649
- except Exception:
650
- if '{"error": "isFlood"}' in text:
651
- raise(exceptions.CommentPostFailure(
652
- "You are being rate-limited for running this operation too often. Implement a cooldown of about 10 seconds."))
653
- elif '<script id="error-data" type="application/json">' in text:
654
- raw_error_data = text.split('<script id="error-data" type="application/json">')[1].split('</script>')[0]
655
- error_data = json.loads(raw_error_data)
656
- expires = error_data['mute_status']['muteExpiresAt']
657
- expires = datetime.fromtimestamp(expires, timezone.utc)
658
- raise(exceptions.CommentPostFailure(f"You have been muted. Mute expires on {expires}"))
659
- else:
660
- raise(exceptions.FetchError(f"Couldn't parse API response: {r.text!r}"))
661
-
662
- def reply_comment(self, content, *, parent_id, commentee_id=""):
663
- """
664
- Replies to a comment given by its id
665
-
666
- Warning:
667
- 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.
668
-
669
- Therefore, parent_id should be the comment id of a top level comment.
670
-
671
- Args:
672
- content: Content of the comment that should be posted
673
-
674
- Keyword Arguments:
675
- parent_id: ID of the comment you want to reply to
676
- 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.
677
- """
678
- return self.post_comment(content, parent_id=parent_id, commentee_id=commentee_id)
679
-
680
- def activity(self, *, limit=1000):
681
- """
682
- Returns:
683
- list<scratchattach.Activity>: The user's activity data as parsed list of scratchattach.activity.Activity objects
684
- """
685
- with requests.no_error_handling():
686
- soup = BeautifulSoup(requests.get(f"https://scratch.mit.edu/messages/ajax/user-activity/?user={self.username}&max={limit}").text, 'html.parser')
687
-
688
- activities = []
689
- source = soup.find_all("li")
690
-
691
- for data in source:
692
- _activity = activity.Activity(_session = self._session, raw=data)
693
- _activity._update_from_html(data)
694
- activities.append(_activity)
695
-
696
- return activities
697
-
698
-
699
- def activity_html(self, *, limit=1000):
700
- """
701
- Returns:
702
- str: The raw user activity HTML data
703
- """
704
- with requests.no_error_handling():
705
- return requests.get(f"https://scratch.mit.edu/messages/ajax/user-activity/?user={self.username}&max={limit}").text
706
-
707
-
708
- def follow(self):
709
- """
710
- Follows the user represented by the User object. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_user`
711
- """
712
- self._assert_auth()
713
- requests.put(
714
- f"https://scratch.mit.edu/site-api/users/followers/{self.username}/add/?usernames={self._session._username}",
715
- headers = headers,
716
- cookies = self._cookies,
717
- )
718
-
719
- def unfollow(self):
720
- """
721
- Unfollows the user represented by the User object. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_user`
722
- """
723
- self._assert_auth()
724
- requests.put(
725
- f"https://scratch.mit.edu/site-api/users/followers/{self.username}/remove/?usernames={self._session._username}",
726
- headers = headers,
727
- cookies = self._cookies,
728
- )
729
-
730
- def delete_comment(self, *, comment_id):
731
- """
732
- Deletes a comment by its ID. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_user`
733
-
734
- Args:
735
- comment_id: The id of the comment that should be deleted
736
- """
737
- self._assert_permission()
738
- return requests.post(
739
- f"https://scratch.mit.edu/site-api/comments/user/{self.username}/del/",
740
- headers = headers,
741
- cookies = self._cookies,
742
- data = json.dumps({"id":str(comment_id)})
743
- )
744
-
745
- def report_comment(self, *, comment_id):
746
- """
747
- 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_user`
748
-
749
- Args:
750
- comment_id: The id of the comment that should be reported
751
- """
752
- self._assert_auth()
753
- return requests.post(
754
- f"https://scratch.mit.edu/site-api/comments/user/{self.username}/rep/",
755
- headers = headers,
756
- cookies = self._cookies,
757
- data = json.dumps({"id":str(comment_id)})
758
- )
759
-
760
- def comments(self, *, page=1, limit=None):
761
- """
762
- Returns the comments posted on the user's profile (with replies).
763
-
764
- Keyword Arguments:
765
- page: The page of the comments that should be returned.
766
- limit: Max. amount of returned comments.
767
-
768
- Returns:
769
- list<scratchattach.comment.Comment>: A list containing the requested comments as Comment objects.
770
- """
771
- URL = f"https://scratch.mit.edu/site-api/comments/user/{self.username}/?page={page}"
772
- DATA = []
773
-
774
- with requests.no_error_handling():
775
- page_contents = requests.get(URL).content
776
-
777
- soup = BeautifulSoup(page_contents, "html.parser")
778
-
779
- _comments = soup.find_all("li", {"class": "top-level-reply"})
780
-
781
- if len(_comments) == 0:
782
- return None
783
-
784
- for entity in _comments:
785
- comment_id = entity.find("div", {"class": "comment"})['data-comment-id']
786
- user = entity.find("a", {"id": "comment-user"})['data-comment-user']
787
- content = str(entity.find("div", {"class": "content"}).text).strip()
788
- time = entity.find("span", {"class": "time"})['title']
789
-
790
- main_comment = {
791
- 'id': comment_id,
792
- 'author': {"username":user},
793
- 'content': content,
794
- 'datetime_created': time,
795
- }
796
- _comment = comment.Comment(source="profile", source_id=self.username, _session = self._session)
797
- _comment._update_from_dict(main_comment)
798
-
799
- ALL_REPLIES = []
800
- replies = entity.find_all("li", {"class": "reply"})
801
- if len(replies) > 0:
802
- hasReplies = True
803
- else:
804
- hasReplies = False
805
- for reply in replies:
806
- r_comment_id = reply.find("div", {"class": "comment"})['data-comment-id']
807
- r_user = reply.find("a", {"id": "comment-user"})['data-comment-user']
808
- r_content = str(reply.find("div", {"class": "content"}).text).strip().replace("\n", "").replace(
809
- " ", " ")
810
- r_time = reply.find("span", {"class": "time"})['title']
811
- reply_data = {
812
- 'id': r_comment_id,
813
- 'author':{'username': r_user},
814
- 'content': r_content,
815
- 'datetime_created': r_time,
816
- "parent_id" : comment_id,
817
- "cached_parent_comment" : _comment,
818
- }
819
- _r_comment = comment.Comment(source="profile", source_id=self.username, _session = self._session, cached_parent_comment=_comment)
820
- _r_comment._update_from_dict(reply_data)
821
- ALL_REPLIES.append(_r_comment)
822
-
823
- _comment.reply_count = len(ALL_REPLIES)
824
- _comment.cached_replies = list(ALL_REPLIES)
825
-
826
- DATA.append(_comment)
827
- return DATA
828
-
829
- def comment_by_id(self, comment_id) -> comment.Comment:
830
- """
831
- Gets a comment on this user's profile by id.
832
-
833
- Warning:
834
- For comments very far down on the user's profile, this method will take a while to find the comment. Very old comment are deleted from Scratch's database and may not appear.
835
-
836
- Returns:
837
- scratchattach.comments.Comment: The request comment.
838
- """
839
-
840
- page = 1
841
- page_content = self.comments(page=page)
842
- while page_content != []:
843
- results = list(filter(lambda x : str(x.id) == str(comment_id), page_content))
844
- if results == []:
845
- results = list(filter(lambda x : str(x.id) == str(comment_id), [item for x in page_content for item in x.cached_replies]))
846
- if results != []:
847
- return results[0]
848
- else:
849
- return results[0]
850
- page += 1
851
- page_content = self.comments(page=page)
852
- raise exceptions.CommentNotFound()
853
-
854
- def message_events(self):
855
- return message_events.MessageEvents(self)
856
-
857
- def stats(self):
858
- """
859
- Gets information about the user's stats. Fetched from ScratchDB.
860
-
861
- Warning:
862
- ScratchDB is down indefinitely, therefore this method is deprecated.
863
-
864
- Returns:
865
- dict: A dict containing the user's stats. If the stats aren't available, all values will be -1.
866
- """
867
- print("Warning: ScratchDB is down indefinitely, therefore this method is deprecated.")
868
- try:
869
- stats= requests.get(
870
- f"https://scratchdb.lefty.one/v3/user/info/{self.username}"
871
- ).json()["statistics"]
872
- stats.pop("ranks")
873
- except Exception:
874
- stats = {"loves":-1,"favorites":-1,"comments":-1,"views":-1,"followers":-1,"following":-1}
875
- return stats
876
-
877
- def ranks(self):
878
- """
879
- Gets information about the user's ranks. Fetched from ScratchDB.
880
-
881
- Warning:
882
- ScratchDB is down indefinitely, therefore this method is deprecated.
883
-
884
- Returns:
885
- dict: A dict containing the user's ranks. If the ranks aren't available, all values will be -1.
886
- """
887
- print("Warning: ScratchDB is down indefinitely, therefore this method is deprecated.")
888
- try:
889
- return requests.get(
890
- f"https://scratchdb.lefty.one/v3/user/info/{self.username}"
891
- ).json()["statistics"]["ranks"]
892
- except Exception:
893
- return {"country":{"loves":0,"favorites":0,"comments":0,"views":0,"followers":0,"following":0},"loves":0,"favorites":0,"comments":0,"views":0,"followers":0,"following":0}
894
-
895
- def ocular_status(self):
896
- """
897
- Gets information about the user's ocular status. Ocular is a website developed by jeffalo: https://ocular.jeffalo.net/
898
-
899
- Returns:
900
- dict
901
- """
902
- return requests.get(f"https://my-ocular.jeffalo.net/api/user/{self.username}").json()
903
-
904
- def verify_identity(self, *, verification_project_id=395330233):
905
- """
906
- Can be used in applications to verify a user's identity.
907
-
908
- This function returns a Verifactor object. Attributs of this object:
909
- :.projecturl: The link to the project where the user has to go to verify
910
- :.project: The project where the user has to go to verify as scratchattach.Project object
911
- :.code: The code that the user has to comment
912
-
913
- To check if the user verified successfully, call the .check() function on the returned object.
914
- It will return True if the user commented the code.
915
- """
916
-
917
- v = Verificator(self, verification_project_id)
918
- return v
919
-
920
- def rank(self):
921
- """
922
- Finds the rank of the user.
923
- May replace user.scratchteam and user.is_new_scratcher in the future.
924
- """
925
-
926
- if self.is_new_scratcher():
927
- return Rank.NEW_SCRATCHER
928
- # Is New Scratcher
929
-
930
- if not self.scratchteam:
931
- return Rank.SCRATCHER
932
- # Is Scratcher
933
-
934
- return Rank.SCRATCH_TEAM
935
- # Is Scratch Team member
936
-
937
-
938
- # ------ #
939
-
940
- def get_user(username) -> User:
941
- """
942
- Gets a user without logging in.
943
-
944
- Args:
945
- username (str): Username of the requested user
946
-
947
- Returns:
948
- scratchattach.user.User: An object representing the requested user
949
-
950
- Warning:
951
- Any methods that require authentication (like user.follow) will not work on the returned object.
952
-
953
- If you want to use these, get the user with :meth:`scratchattach.session.Session.connect_user` instead.
954
- """
955
- print("Warning: For methods that require authentication, use session.connect_user instead of get_user")
956
- return commons._get_object("username", username, User, exceptions.UserNotFound)