scratchattach 2.1.15b0__py3-none-any.whl → 3.0.0b1__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.
- cli/__about__.py +1 -0
- cli/__init__.py +26 -0
- cli/cmd/__init__.py +4 -0
- cli/cmd/group.py +127 -0
- cli/cmd/login.py +60 -0
- cli/cmd/profile.py +7 -0
- cli/cmd/sessions.py +5 -0
- cli/context.py +142 -0
- cli/db.py +66 -0
- cli/namespace.py +14 -0
- {scratchattach/cloud → cloud}/_base.py +112 -87
- {scratchattach/cloud → cloud}/cloud.py +16 -16
- {scratchattach/editor → editor}/__init__.py +2 -1
- {scratchattach/editor → editor}/asset.py +26 -14
- {scratchattach/editor → editor}/backpack_json.py +3 -5
- {scratchattach/editor → editor}/base.py +2 -4
- {scratchattach/editor → editor}/block.py +27 -22
- {scratchattach/editor → editor}/blockshape.py +1 -1
- {scratchattach/editor → editor}/build_defaulting.py +2 -2
- editor/commons.py +145 -0
- {scratchattach/editor → editor}/field.py +1 -1
- {scratchattach/editor → editor}/inputs.py +6 -3
- {scratchattach/editor → editor}/meta.py +10 -7
- {scratchattach/editor → editor}/monitor.py +10 -8
- {scratchattach/editor → editor}/mutation.py +68 -11
- {scratchattach/editor → editor}/pallete.py +1 -3
- {scratchattach/editor → editor}/prim.py +4 -0
- {scratchattach/editor → editor}/project.py +118 -16
- {scratchattach/editor → editor}/sprite.py +25 -15
- {scratchattach/editor → editor}/vlb.py +2 -2
- {scratchattach/eventhandlers → eventhandlers}/_base.py +1 -0
- {scratchattach/eventhandlers → eventhandlers}/cloud_events.py +26 -6
- {scratchattach/eventhandlers → eventhandlers}/cloud_recorder.py +4 -4
- {scratchattach/eventhandlers → eventhandlers}/cloud_requests.py +139 -54
- {scratchattach/eventhandlers → eventhandlers}/cloud_server.py +6 -3
- {scratchattach/eventhandlers → eventhandlers}/cloud_storage.py +1 -2
- eventhandlers/filterbot.py +163 -0
- other/other_apis.py +598 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/METADATA +7 -11
- scratchattach-3.0.0b1.dist-info/RECORD +79 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/WHEEL +1 -1
- scratchattach-3.0.0b1.dist-info/entry_points.txt +2 -0
- scratchattach-3.0.0b1.dist-info/top_level.txt +7 -0
- {scratchattach/site → site}/_base.py +32 -5
- site/activity.py +426 -0
- {scratchattach/site → site}/alert.py +4 -5
- {scratchattach/site → site}/backpack_asset.py +2 -1
- {scratchattach/site → site}/classroom.py +80 -73
- {scratchattach/site → site}/cloud_activity.py +43 -29
- {scratchattach/site → site}/comment.py +86 -100
- {scratchattach/site → site}/forum.py +8 -4
- site/placeholder.py +132 -0
- {scratchattach/site → site}/project.py +228 -122
- {scratchattach/site → site}/session.py +156 -71
- {scratchattach/site → site}/studio.py +139 -46
- site/typed_dicts.py +151 -0
- {scratchattach/site → site}/user.py +511 -215
- {scratchattach/utils → utils}/commons.py +12 -4
- {scratchattach/utils → utils}/encoder.py +7 -4
- {scratchattach/utils → utils}/enums.py +1 -0
- {scratchattach/utils → utils}/exceptions.py +36 -2
- utils/optional_async.py +154 -0
- utils/requests.py +306 -0
- scratchattach/__init__.py +0 -29
- scratchattach/editor/commons.py +0 -273
- scratchattach/eventhandlers/filterbot.py +0 -161
- scratchattach/other/other_apis.py +0 -284
- scratchattach/site/activity.py +0 -382
- scratchattach/utils/requests.py +0 -93
- scratchattach-2.1.15b0.dist-info/RECORD +0 -66
- scratchattach-2.1.15b0.dist-info/top_level.txt +0 -1
- {scratchattach/cloud → cloud}/__init__.py +0 -0
- {scratchattach/editor → editor}/code_translation/__init__.py +0 -0
- {scratchattach/editor → editor}/code_translation/parse.py +0 -0
- {scratchattach/editor → editor}/comment.py +0 -0
- {scratchattach/editor → editor}/extension.py +0 -0
- {scratchattach/editor → editor}/twconfig.py +0 -0
- {scratchattach/eventhandlers → eventhandlers}/__init__.py +0 -0
- {scratchattach/eventhandlers → eventhandlers}/combine.py +0 -0
- {scratchattach/eventhandlers → eventhandlers}/message_events.py +0 -0
- {scratchattach/other → other}/__init__.py +0 -0
- {scratchattach/other → other}/project_json_capabilities.py +0 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
- {scratchattach/site → site}/__init__.py +0 -0
- {scratchattach/site → site}/browser_cookie3_stub.py +0 -0
- {scratchattach/site → site}/browser_cookies.py +0 -0
- {scratchattach/utils → utils}/__init__.py +0 -0
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
"""Other Scratch API-related functions"""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
|
-
|
|
7
|
-
from scratchattach.utils import commons
|
|
8
|
-
from scratchattach.utils.enums import Languages, Language, TTSVoices, TTSVoice
|
|
9
|
-
from scratchattach.utils.exceptions import BadRequest, InvalidLanguage, InvalidTTSGender
|
|
10
|
-
from scratchattach.utils.requests import requests
|
|
11
|
-
from typing import Optional
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# --- Front page ---
|
|
15
|
-
|
|
16
|
-
def get_news(*, limit=10, offset=0):
|
|
17
|
-
return commons.api_iterative("https://api.scratch.mit.edu/news", limit=limit, offset=offset)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def featured_data():
|
|
21
|
-
return requests.get("https://api.scratch.mit.edu/proxy/featured").json()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def featured_projects():
|
|
25
|
-
return featured_data()["community_featured_projects"]
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def featured_studios():
|
|
29
|
-
return featured_data()["community_featured_studios"]
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def top_loved():
|
|
33
|
-
return featured_data()["community_most_loved_projects"]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def top_remixed():
|
|
37
|
-
return featured_data()["community_most_remixed_projects"]
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def newest_projects():
|
|
41
|
-
return featured_data()["community_newest_projects"]
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def curated_projects():
|
|
45
|
-
return featured_data()["curator_top_projects"]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def design_studio_projects():
|
|
49
|
-
return featured_data()["scratch_design_studio"]
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# --- Statistics ---
|
|
53
|
-
|
|
54
|
-
def total_site_stats():
|
|
55
|
-
data = requests.get("https://scratch.mit.edu/statistics/data/daily/").json()
|
|
56
|
-
data.pop("_TS")
|
|
57
|
-
return data
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def monthly_site_traffic():
|
|
61
|
-
data = requests.get("https://scratch.mit.edu/statistics/data/monthly-ga/").json()
|
|
62
|
-
data.pop("_TS")
|
|
63
|
-
return data
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def country_counts():
|
|
67
|
-
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["country_distribution"]
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def age_distribution():
|
|
71
|
-
data = requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["age_distribution_data"][0]["values"]
|
|
72
|
-
return_data = {}
|
|
73
|
-
for value in data:
|
|
74
|
-
return_data[value["x"]] = value["y"]
|
|
75
|
-
return return_data
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def monthly_comment_activity():
|
|
79
|
-
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["comment_data"]
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def monthly_project_shares():
|
|
83
|
-
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["project_data"]
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def monthly_active_users():
|
|
87
|
-
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["active_user_data"]
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def monthly_activity_trends():
|
|
91
|
-
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["activity_data"]
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
# --- CSRF Token Generation API ---
|
|
95
|
-
|
|
96
|
-
def get_csrf_token():
|
|
97
|
-
"""
|
|
98
|
-
Generates a scratchcsrftoken using Scratch's API.
|
|
99
|
-
|
|
100
|
-
Returns:
|
|
101
|
-
str: The generated scratchcsrftoken
|
|
102
|
-
"""
|
|
103
|
-
return requests.get(
|
|
104
|
-
"https://scratch.mit.edu/csrf_token/"
|
|
105
|
-
).headers["set-cookie"].split(";")[3][len(" Path=/, scratchcsrftoken="):]
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
# --- Various other api.scratch.mit.edu API endpoints ---
|
|
109
|
-
|
|
110
|
-
def get_health():
|
|
111
|
-
return requests.get("https://api.scratch.mit.edu/health").json()
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def get_total_project_count() -> int:
|
|
115
|
-
return requests.get("https://api.scratch.mit.edu/projects/count/all").json()["count"]
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def check_username(username):
|
|
119
|
-
return requests.get(f"https://api.scratch.mit.edu/accounts/checkusername/{username}").json()["msg"]
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def check_password(password):
|
|
123
|
-
return requests.post("https://api.scratch.mit.edu/accounts/checkpassword/", json={"password": password}).json()[
|
|
124
|
-
"msg"]
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
# --- April fools endpoints ---
|
|
128
|
-
|
|
129
|
-
def aprilfools_get_counter() -> int:
|
|
130
|
-
return requests.get("https://api.scratch.mit.edu/surprise").json()["surprise"]
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def aprilfools_increment_counter() -> int:
|
|
134
|
-
return requests.post("https://api.scratch.mit.edu/surprise").json()["surprise"]
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
# --- Resources ---
|
|
138
|
-
def get_resource_urls():
|
|
139
|
-
return requests.get("https://resources.scratch.mit.edu/localized-urls.json").json()
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
# --- ScratchTools endpoints ---
|
|
143
|
-
def scratchtools_online_status(username: str) -> bool | None:
|
|
144
|
-
"""
|
|
145
|
-
Get the online status of an account.
|
|
146
|
-
:return: Boolean whether the account is online; if they do not use scratchtools, return None.
|
|
147
|
-
"""
|
|
148
|
-
data = requests.get(f"https://data.scratchtools.app/isonline/{username}").json()
|
|
149
|
-
|
|
150
|
-
if data["scratchtools"]:
|
|
151
|
-
return data["online"]
|
|
152
|
-
else:
|
|
153
|
-
return None
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def scratchtools_beta_user(username: str) -> bool:
|
|
157
|
-
"""
|
|
158
|
-
Get whether a user is a scratchtools beta tester (I think that's what it means)
|
|
159
|
-
"""
|
|
160
|
-
return requests.get(f"https://data.scratchtools.app/isbeta/{username}").json()["beta"]
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def scratchtools_display_name(username: str) -> str | None:
|
|
164
|
-
"""
|
|
165
|
-
Get the display name of a user for scratchtools. Returns none if there is no display name or the username is invalid
|
|
166
|
-
"""
|
|
167
|
-
return requests.get(f"https://data.scratchtools.app/name/{username}").json().get("displayName")
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
@dataclass(init=True, repr=True)
|
|
171
|
-
class ScratchToolsTutorial:
|
|
172
|
-
title: str
|
|
173
|
-
description: str = field(repr=False)
|
|
174
|
-
id: str
|
|
175
|
-
|
|
176
|
-
@classmethod
|
|
177
|
-
def from_json(cls, data: dict[str, str]) -> ScratchToolsTutorial:
|
|
178
|
-
return cls(**data)
|
|
179
|
-
|
|
180
|
-
@property
|
|
181
|
-
def yt_link(self):
|
|
182
|
-
return f"https://www.youtube.com/watch?v={self.id}"
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def scratchtools_tutorials() -> list[ScratchToolsTutorial]:
|
|
186
|
-
"""
|
|
187
|
-
Returns a list of scratchtools tutorials (just yt videos)
|
|
188
|
-
"""
|
|
189
|
-
data_list = requests.get("https://data.scratchtools.app/tutorials/").json()
|
|
190
|
-
return [ScratchToolsTutorial.from_json(data) for data in data_list]
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def scratchtools_emoji_status(username: str) -> str | None:
|
|
194
|
-
return requests.get(f"https://data.scratchtools.app/status/{username}").json().get("status",
|
|
195
|
-
'🍪') # Cookie is the default status, even if the user does not use ScratchTools
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def scratchtools_pinned_comment(project_id: int) -> dict[str, str | int]:
|
|
199
|
-
data = requests.get(f"https://data.scratchtools.app/pinned/{project_id}/").json()
|
|
200
|
-
# Maybe use this info to instantiate a partial comment object?
|
|
201
|
-
return data
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
# --- Misc ---
|
|
205
|
-
# I'm not sure what to label this as
|
|
206
|
-
def scratch_team_members() -> dict:
|
|
207
|
-
# Unfortunately, the only place to find this is a js file, not a json file, which is annoying
|
|
208
|
-
text = requests.get("https://scratch.mit.edu/js/credits.bundle.js").text
|
|
209
|
-
text = "[{\"userName\"" + text.split("JSON.parse('[{\"userName\"")[1]
|
|
210
|
-
text = text.split("\"}]')")[0] + "\"}]"
|
|
211
|
-
|
|
212
|
-
return json.loads(text)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def send_password_reset_email(username: Optional[str] = None, email: Optional[str] = None):
|
|
216
|
-
requests.post("https://scratch.mit.edu/accounts/password_reset/", data={
|
|
217
|
-
"username": username,
|
|
218
|
-
"email": email,
|
|
219
|
-
}, headers=commons.headers, cookies={"scratchcsrftoken": 'a'})
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def translate(language: str | Languages, text: str = "hello"):
|
|
223
|
-
if isinstance(language, str):
|
|
224
|
-
lang = Languages.find_by_attrs(language.lower(), ["code", "tts_locale", "name"], str.lower)
|
|
225
|
-
elif isinstance(language, Languages):
|
|
226
|
-
lang = language.value
|
|
227
|
-
else:
|
|
228
|
-
lang = language
|
|
229
|
-
|
|
230
|
-
if not isinstance(lang, Language):
|
|
231
|
-
raise InvalidLanguage(f"{language} is not a language")
|
|
232
|
-
|
|
233
|
-
if lang.code is None:
|
|
234
|
-
raise InvalidLanguage(f"{lang} is not a valid translate language")
|
|
235
|
-
|
|
236
|
-
response_json = requests.get(
|
|
237
|
-
f"https://translate-service.scratch.mit.edu/translate?language={lang.code}&text={text}").json()
|
|
238
|
-
|
|
239
|
-
if "result" in response_json:
|
|
240
|
-
return response_json["result"]
|
|
241
|
-
else:
|
|
242
|
-
raise BadRequest(f"Language '{language}' does not seem to be valid.\nResponse: {response_json}")
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def text2speech(text: str = "hello", voice_name: str = "female", language: str = "en-US"):
|
|
246
|
-
"""
|
|
247
|
-
Sends a request to Scratch's TTS synthesis service.
|
|
248
|
-
Returns:
|
|
249
|
-
- The TTS audio (mp3) as bytes
|
|
250
|
-
- The playback rate (e.g. for giant it would be 0.84)
|
|
251
|
-
"""
|
|
252
|
-
if isinstance(voice_name, str):
|
|
253
|
-
voice = TTSVoices.find_by_attrs(voice_name.lower(), ["name", "gender"], str.lower)
|
|
254
|
-
elif isinstance(voice_name, TTSVoices):
|
|
255
|
-
voice = voice_name.value
|
|
256
|
-
else:
|
|
257
|
-
voice = voice_name
|
|
258
|
-
|
|
259
|
-
if not isinstance(voice, TTSVoice):
|
|
260
|
-
raise InvalidTTSGender(f"TTS Gender {voice_name} is not supported.")
|
|
261
|
-
|
|
262
|
-
# If it's kitten, make sure to change everything to just meows
|
|
263
|
-
if voice.name == "kitten":
|
|
264
|
-
text = ''
|
|
265
|
-
for word in text.split(' '):
|
|
266
|
-
if word.strip() != '':
|
|
267
|
-
text += "meow "
|
|
268
|
-
|
|
269
|
-
if isinstance(language, str):
|
|
270
|
-
lang = Languages.find_by_attrs(language.lower(), ["code", "tts_locale", "name"], str.lower)
|
|
271
|
-
elif isinstance(language, Languages):
|
|
272
|
-
lang = language.value
|
|
273
|
-
else:
|
|
274
|
-
lang = language
|
|
275
|
-
|
|
276
|
-
if not isinstance(lang, Language):
|
|
277
|
-
raise InvalidLanguage(f"Language '{language}' is not a language")
|
|
278
|
-
|
|
279
|
-
if lang.tts_locale is None:
|
|
280
|
-
raise InvalidLanguage(f"Language '{language}' is not a valid TTS language")
|
|
281
|
-
|
|
282
|
-
response = requests.get(f"https://synthesis-service.scratch.mit.edu/synth"
|
|
283
|
-
f"?locale={lang.tts_locale}&gender={voice.gender}&text={text}")
|
|
284
|
-
return response.content, voice.playback_rate
|
scratchattach/site/activity.py
DELETED
|
@@ -1,382 +0,0 @@
|
|
|
1
|
-
"""Activity and CloudActivity class"""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
from bs4 import PageElement
|
|
5
|
-
|
|
6
|
-
from . import user, project, studio
|
|
7
|
-
from ._base import BaseSiteComponent
|
|
8
|
-
from scratchattach.utils import exceptions
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Activity(BaseSiteComponent):
|
|
12
|
-
"""
|
|
13
|
-
Represents a Scratch activity (message or other user page activity)
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
def __repr__(self):
|
|
17
|
-
return f"Activity({repr(self.raw)})"
|
|
18
|
-
|
|
19
|
-
def __str__(self):
|
|
20
|
-
return str(self.raw)
|
|
21
|
-
|
|
22
|
-
def __init__(self, **entries):
|
|
23
|
-
# Set attributes every Activity object needs to have:
|
|
24
|
-
self._session = None
|
|
25
|
-
self.raw = None
|
|
26
|
-
|
|
27
|
-
# Possible attributes
|
|
28
|
-
self.project_id = None
|
|
29
|
-
self.gallery_id = None
|
|
30
|
-
|
|
31
|
-
self.username = None
|
|
32
|
-
self.followed_username = None
|
|
33
|
-
self.recipient_username = None
|
|
34
|
-
|
|
35
|
-
self.comment_type = None
|
|
36
|
-
self.comment_obj_id = None
|
|
37
|
-
self.comment_obj_title = None
|
|
38
|
-
self.comment_id = None
|
|
39
|
-
|
|
40
|
-
self.datetime_created = None
|
|
41
|
-
self.time = None
|
|
42
|
-
self.type = None
|
|
43
|
-
|
|
44
|
-
# Update attributes from entries dict:
|
|
45
|
-
self.__dict__.update(entries)
|
|
46
|
-
|
|
47
|
-
def update(self):
|
|
48
|
-
print("Warning: Activity objects can't be updated")
|
|
49
|
-
return False # Objects of this type cannot be updated
|
|
50
|
-
|
|
51
|
-
def _update_from_dict(self, data):
|
|
52
|
-
self.raw = data
|
|
53
|
-
self.__dict__.update(data)
|
|
54
|
-
return True
|
|
55
|
-
|
|
56
|
-
def _update_from_json(self, data: dict):
|
|
57
|
-
"""
|
|
58
|
-
Update using JSON, used in the classroom API.
|
|
59
|
-
"""
|
|
60
|
-
activity_type = data["type"]
|
|
61
|
-
|
|
62
|
-
_time = data["datetime_created"] if "datetime_created" in data else None
|
|
63
|
-
|
|
64
|
-
if "actor" in data:
|
|
65
|
-
username = data["actor"]["username"]
|
|
66
|
-
elif "actor_username" in data:
|
|
67
|
-
username = data["actor_username"]
|
|
68
|
-
else:
|
|
69
|
-
username = None
|
|
70
|
-
|
|
71
|
-
if data.get("recipient") is not None:
|
|
72
|
-
recipient_username = data["recipient"]["username"]
|
|
73
|
-
|
|
74
|
-
elif data.get("recipient_username") is not None:
|
|
75
|
-
recipient_username = data["recipient_username"]
|
|
76
|
-
|
|
77
|
-
elif data.get("project_creator") is not None:
|
|
78
|
-
recipient_username = data["project_creator"]["username"]
|
|
79
|
-
else:
|
|
80
|
-
recipient_username = None
|
|
81
|
-
|
|
82
|
-
default_case = False
|
|
83
|
-
# Even if `activity_type` is an invalid value; it will default to 'user performed an action'
|
|
84
|
-
|
|
85
|
-
if activity_type == 0:
|
|
86
|
-
# follow
|
|
87
|
-
followed_username = data["followed_username"]
|
|
88
|
-
|
|
89
|
-
self.raw = f"{username} followed user {followed_username}"
|
|
90
|
-
|
|
91
|
-
self.datetime_created = _time
|
|
92
|
-
self.type = "followuser"
|
|
93
|
-
self.username = username
|
|
94
|
-
self.followed_username = followed_username
|
|
95
|
-
|
|
96
|
-
elif activity_type == 1:
|
|
97
|
-
# follow studio
|
|
98
|
-
studio_id = data["gallery"]
|
|
99
|
-
|
|
100
|
-
raw = f"{username} followed studio https://scratch.mit.edu/studios/{studio_id}"
|
|
101
|
-
|
|
102
|
-
self.raw = raw
|
|
103
|
-
self.datetime_created = _time
|
|
104
|
-
self.type = "followstudio"
|
|
105
|
-
|
|
106
|
-
self.username = username
|
|
107
|
-
self.gallery_id = studio_id
|
|
108
|
-
|
|
109
|
-
elif activity_type == 2:
|
|
110
|
-
# love project
|
|
111
|
-
project_id = data["project"]
|
|
112
|
-
|
|
113
|
-
raw = f"{username} loved project https://scratch.mit.edu/projects/{project_id}"
|
|
114
|
-
|
|
115
|
-
self.raw = raw
|
|
116
|
-
self.datetime_created = _time,
|
|
117
|
-
self.type = "loveproject"
|
|
118
|
-
|
|
119
|
-
self.username = username
|
|
120
|
-
self.project_id = project_id
|
|
121
|
-
self.recipient_username = recipient_username
|
|
122
|
-
|
|
123
|
-
elif activity_type == 3:
|
|
124
|
-
# Favorite project
|
|
125
|
-
project_id = data["project"]
|
|
126
|
-
|
|
127
|
-
raw = f"{username} favorited project https://scratch.mit.edu/projects/{project_id}"
|
|
128
|
-
|
|
129
|
-
self.raw = raw
|
|
130
|
-
self.datetime_created = _time
|
|
131
|
-
self.type = "favoriteproject"
|
|
132
|
-
|
|
133
|
-
self.username = username
|
|
134
|
-
self.project_id = project_id
|
|
135
|
-
self.recipient_username = recipient_username
|
|
136
|
-
|
|
137
|
-
elif activity_type == 7:
|
|
138
|
-
# Add project to studio
|
|
139
|
-
|
|
140
|
-
project_id = data["project"]
|
|
141
|
-
studio_id = data["gallery"]
|
|
142
|
-
|
|
143
|
-
raw = f"{username} added the project https://scratch.mit.edu/projects/{project_id} to studio https://scratch.mit.edu/studios/{studio_id}"
|
|
144
|
-
|
|
145
|
-
self.raw = raw
|
|
146
|
-
self.datetime_created = _time
|
|
147
|
-
self.type = "addprojecttostudio"
|
|
148
|
-
|
|
149
|
-
self.username = username
|
|
150
|
-
self.project_id = project_id
|
|
151
|
-
self.recipient_username = recipient_username
|
|
152
|
-
|
|
153
|
-
elif activity_type in (8, 9, 10):
|
|
154
|
-
# Share/Reshare project
|
|
155
|
-
project_id = data["project"]
|
|
156
|
-
is_reshare = data["is_reshare"]
|
|
157
|
-
|
|
158
|
-
raw_reshare = "reshared" if is_reshare else "shared"
|
|
159
|
-
|
|
160
|
-
raw = f"{username} {raw_reshare} the project https://scratch.mit.edu/projects/{project_id}"
|
|
161
|
-
|
|
162
|
-
self.raw = raw
|
|
163
|
-
self.datetime_created = _time
|
|
164
|
-
self.type = "shareproject"
|
|
165
|
-
|
|
166
|
-
self.username = username
|
|
167
|
-
self.project_id = project_id
|
|
168
|
-
self.recipient_username = recipient_username
|
|
169
|
-
|
|
170
|
-
elif activity_type == 11:
|
|
171
|
-
# Remix
|
|
172
|
-
parent_id = data["parent"]
|
|
173
|
-
|
|
174
|
-
raw = f"{username} remixed the project https://scratch.mit.edu/projects/{parent_id}"
|
|
175
|
-
|
|
176
|
-
self.raw = raw
|
|
177
|
-
self.datetime_created = _time
|
|
178
|
-
self.type = "remixproject"
|
|
179
|
-
|
|
180
|
-
self.username = username
|
|
181
|
-
self.project_id = parent_id
|
|
182
|
-
self.recipient_username = recipient_username
|
|
183
|
-
|
|
184
|
-
# type 12 does not exist in the HTML. That's why it was removed, not merged with type 13.
|
|
185
|
-
|
|
186
|
-
elif activity_type == 13:
|
|
187
|
-
# Create ('add') studio
|
|
188
|
-
studio_id = data["gallery"]
|
|
189
|
-
|
|
190
|
-
raw = f"{username} created the studio https://scratch.mit.edu/studios/{studio_id}"
|
|
191
|
-
|
|
192
|
-
self.raw = raw
|
|
193
|
-
self.datetime_created = _time
|
|
194
|
-
self.type = "createstudio"
|
|
195
|
-
|
|
196
|
-
self.username = username
|
|
197
|
-
self.gallery_id = studio_id
|
|
198
|
-
|
|
199
|
-
elif activity_type == 15:
|
|
200
|
-
# Update studio
|
|
201
|
-
studio_id = data["gallery"]
|
|
202
|
-
|
|
203
|
-
raw = f"{username} updated the studio https://scratch.mit.edu/studios/{studio_id}"
|
|
204
|
-
|
|
205
|
-
self.raw = raw
|
|
206
|
-
self.datetime_created = _time
|
|
207
|
-
self.type = "updatestudio"
|
|
208
|
-
|
|
209
|
-
self.username = username
|
|
210
|
-
self.gallery_id = studio_id
|
|
211
|
-
|
|
212
|
-
elif activity_type in (16, 17, 18, 19):
|
|
213
|
-
# Remove project from studio
|
|
214
|
-
|
|
215
|
-
project_id = data["project"]
|
|
216
|
-
studio_id = data["gallery"]
|
|
217
|
-
|
|
218
|
-
raw = f"{username} removed the project https://scratch.mit.edu/projects/{project_id} from studio https://scratch.mit.edu/studios/{studio_id}"
|
|
219
|
-
|
|
220
|
-
self.raw = raw
|
|
221
|
-
self.datetime_created = _time
|
|
222
|
-
self.type = "removeprojectfromstudio"
|
|
223
|
-
|
|
224
|
-
self.username = username
|
|
225
|
-
self.project_id = project_id
|
|
226
|
-
|
|
227
|
-
elif activity_type in (20, 21, 22):
|
|
228
|
-
# Was promoted to manager for studio
|
|
229
|
-
studio_id = data["gallery"]
|
|
230
|
-
|
|
231
|
-
raw = f"{recipient_username} was promoted to manager by {username} for studio https://scratch.mit.edu/studios/{studio_id}"
|
|
232
|
-
|
|
233
|
-
self.raw = raw
|
|
234
|
-
self.datetime_created = _time
|
|
235
|
-
self.type = "promotetomanager"
|
|
236
|
-
|
|
237
|
-
self.username = username
|
|
238
|
-
self.recipient_username = recipient_username
|
|
239
|
-
self.gallery_id = studio_id
|
|
240
|
-
|
|
241
|
-
elif activity_type in (23, 24, 25):
|
|
242
|
-
# Update profile
|
|
243
|
-
raw = f"{username} made a profile update"
|
|
244
|
-
|
|
245
|
-
self.raw = raw
|
|
246
|
-
self.datetime_created = _time
|
|
247
|
-
self.type = "updateprofile"
|
|
248
|
-
|
|
249
|
-
self.username = username
|
|
250
|
-
|
|
251
|
-
elif activity_type in (26, 27):
|
|
252
|
-
# Comment (quite complicated)
|
|
253
|
-
comment_type: int = data["comment_type"]
|
|
254
|
-
fragment = data["comment_fragment"]
|
|
255
|
-
comment_id = data["comment_id"]
|
|
256
|
-
comment_obj_id = data["comment_obj_id"]
|
|
257
|
-
comment_obj_title = data["comment_obj_title"]
|
|
258
|
-
|
|
259
|
-
if comment_type == 0:
|
|
260
|
-
# Project comment
|
|
261
|
-
raw = f"{username} commented on project https://scratch.mit.edu/projects/{comment_obj_id}/#comments-{comment_id} {fragment!r}"
|
|
262
|
-
|
|
263
|
-
elif comment_type == 1:
|
|
264
|
-
# Profile comment
|
|
265
|
-
raw = f"{username} commented on user https://scratch.mit.edu/users/{comment_obj_title}/#comments-{comment_id} {fragment!r}"
|
|
266
|
-
|
|
267
|
-
elif comment_type == 2:
|
|
268
|
-
# Studio comment
|
|
269
|
-
# Scratch actually provides an incorrect link, but it is fixed here:
|
|
270
|
-
raw = f"{username} commented on studio https://scratch.mit.edu/studios/{comment_obj_id}/comments/#comments-{comment_id} {fragment!r}"
|
|
271
|
-
|
|
272
|
-
else:
|
|
273
|
-
raw = f"{username} commented {fragment!r}" # This should never happen
|
|
274
|
-
|
|
275
|
-
self.raw = raw
|
|
276
|
-
self.datetime_created = _time
|
|
277
|
-
self.type = "addcomment"
|
|
278
|
-
|
|
279
|
-
self.username = username
|
|
280
|
-
|
|
281
|
-
self.comment_type = comment_type
|
|
282
|
-
self.comment_obj_id = comment_obj_id
|
|
283
|
-
self.comment_obj_title = comment_obj_title
|
|
284
|
-
self.comment_id = comment_id
|
|
285
|
-
else:
|
|
286
|
-
default_case = True
|
|
287
|
-
|
|
288
|
-
if default_case:
|
|
289
|
-
# This is coded in the scratch HTML, haven't found an example of it though
|
|
290
|
-
raw = f"{username} performed an action."
|
|
291
|
-
|
|
292
|
-
self.raw = raw
|
|
293
|
-
self.datetime_created = _time
|
|
294
|
-
self.type = "performaction"
|
|
295
|
-
|
|
296
|
-
self.username = username
|
|
297
|
-
|
|
298
|
-
def _update_from_html(self, data: PageElement):
|
|
299
|
-
|
|
300
|
-
self.raw = data
|
|
301
|
-
|
|
302
|
-
_time = data.find('div').find('span').findNext().findNext().text.strip()
|
|
303
|
-
|
|
304
|
-
if '\xa0' in _time:
|
|
305
|
-
while '\xa0' in _time:
|
|
306
|
-
_time = _time.replace('\xa0', ' ')
|
|
307
|
-
|
|
308
|
-
self.datetime_created = _time
|
|
309
|
-
self.actor_username = data.find('div').find('span').text
|
|
310
|
-
|
|
311
|
-
self.target_name = data.find('div').find('span').findNext().text
|
|
312
|
-
self.target_link = data.find('div').find('span').findNext()["href"]
|
|
313
|
-
self.target_id = data.find('div').find('span').findNext()["href"].split("/")[-2]
|
|
314
|
-
|
|
315
|
-
self.type = data.find('div').find_all('span')[0].next_sibling.strip()
|
|
316
|
-
if self.type == "loved":
|
|
317
|
-
self.type = "loveproject"
|
|
318
|
-
|
|
319
|
-
elif self.type == "favorited":
|
|
320
|
-
self.type = "favoriteproject"
|
|
321
|
-
|
|
322
|
-
elif "curator" in self.type:
|
|
323
|
-
self.type = "becomecurator"
|
|
324
|
-
|
|
325
|
-
elif "shared" in self.type:
|
|
326
|
-
self.type = "shareproject"
|
|
327
|
-
|
|
328
|
-
elif "is now following" in self.type:
|
|
329
|
-
if "users" in self.target_link:
|
|
330
|
-
self.type = "followuser"
|
|
331
|
-
else:
|
|
332
|
-
self.type = "followstudio"
|
|
333
|
-
|
|
334
|
-
return True
|
|
335
|
-
|
|
336
|
-
def actor(self):
|
|
337
|
-
"""
|
|
338
|
-
Returns the user that performed the activity as User object
|
|
339
|
-
"""
|
|
340
|
-
return self._make_linked_object("username", self.actor_username, user.User, exceptions.UserNotFound)
|
|
341
|
-
|
|
342
|
-
def target(self):
|
|
343
|
-
"""
|
|
344
|
-
Returns the activity's target (depending on the activity, this is either a User, Project, Studio or Comment object).
|
|
345
|
-
May also return None if the activity type is unknown.
|
|
346
|
-
"""
|
|
347
|
-
|
|
348
|
-
if "project" in self.type: # target is a project
|
|
349
|
-
if "target_id" in self.__dict__:
|
|
350
|
-
return self._make_linked_object("id", self.target_id, project.Project, exceptions.ProjectNotFound)
|
|
351
|
-
if "project_id" in self.__dict__:
|
|
352
|
-
return self._make_linked_object("id", self.project_id, project.Project, exceptions.ProjectNotFound)
|
|
353
|
-
|
|
354
|
-
if self.type == "becomecurator" or self.type == "followstudio": # target is a studio
|
|
355
|
-
if "target_id" in self.__dict__:
|
|
356
|
-
return self._make_linked_object("id", self.target_id, studio.Studio, exceptions.StudioNotFound)
|
|
357
|
-
if "gallery_id" in self.__dict__:
|
|
358
|
-
return self._make_linked_object("id", self.gallery_id, studio.Studio, exceptions.StudioNotFound)
|
|
359
|
-
# NOTE: the "becomecurator" type is ambigous - if it is inside the studio activity tab, the target is the user who joined
|
|
360
|
-
if "username" in self.__dict__:
|
|
361
|
-
return self._make_linked_object("username", self.username, user.User, exceptions.UserNotFound)
|
|
362
|
-
|
|
363
|
-
if self.type == "followuser" or "curator" in self.type: # target is a user
|
|
364
|
-
if "target_name" in self.__dict__:
|
|
365
|
-
return self._make_linked_object("username", self.target_name, user.User, exceptions.UserNotFound)
|
|
366
|
-
if "followed_username" in self.__dict__:
|
|
367
|
-
return self._make_linked_object("username", self.followed_username, user.User, exceptions.UserNotFound)
|
|
368
|
-
if "recipient_username" in self.__dict__: # the recipient_username field always indicates the target is a user
|
|
369
|
-
return self._make_linked_object("username", self.recipient_username, user.User, exceptions.UserNotFound)
|
|
370
|
-
|
|
371
|
-
if self.type == "addcomment": # target is a comment
|
|
372
|
-
if self.comment_type == 0:
|
|
373
|
-
_c = project.Project(id=self.comment_obj_id, author_name=self._session.username,
|
|
374
|
-
_session=self._session).comment_by_id(self.comment_id)
|
|
375
|
-
if self.comment_type == 1:
|
|
376
|
-
_c = user.User(username=self.comment_obj_title, _session=self._session).comment_by_id(self.comment_id)
|
|
377
|
-
if self.comment_type == 2:
|
|
378
|
-
_c = user.User(id=self.comment_obj_id, _session=self._session).comment_by_id(self.comment_id)
|
|
379
|
-
else:
|
|
380
|
-
raise ValueError(f"{self.comment_type} is an invalid comment type")
|
|
381
|
-
|
|
382
|
-
return _c
|