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