scratchattach 2.1.13__py3-none-any.whl → 2.1.14__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/cloud/_base.py +12 -8
- scratchattach/cloud/cloud.py +19 -7
- scratchattach/editor/asset.py +59 -5
- scratchattach/editor/base.py +82 -31
- scratchattach/editor/block.py +86 -15
- scratchattach/editor/blockshape.py +8 -4
- scratchattach/editor/build_defaulting.py +6 -2
- scratchattach/editor/code_translation/__init__.py +0 -0
- scratchattach/editor/code_translation/parse.py +177 -0
- scratchattach/editor/comment.py +6 -0
- scratchattach/editor/commons.py +82 -19
- scratchattach/editor/extension.py +10 -3
- scratchattach/editor/field.py +9 -0
- scratchattach/editor/inputs.py +4 -1
- scratchattach/editor/meta.py +11 -3
- scratchattach/editor/monitor.py +46 -38
- scratchattach/editor/mutation.py +11 -4
- scratchattach/editor/pallete.py +24 -25
- scratchattach/editor/prim.py +2 -2
- scratchattach/editor/project.py +9 -3
- scratchattach/editor/sprite.py +19 -6
- scratchattach/editor/twconfig.py +2 -1
- scratchattach/editor/vlb.py +1 -1
- scratchattach/eventhandlers/_base.py +2 -2
- scratchattach/eventhandlers/cloud_events.py +2 -2
- scratchattach/eventhandlers/cloud_requests.py +3 -3
- scratchattach/eventhandlers/cloud_server.py +3 -3
- scratchattach/eventhandlers/message_events.py +1 -1
- scratchattach/other/other_apis.py +4 -4
- scratchattach/other/project_json_capabilities.py +3 -3
- scratchattach/site/_base.py +13 -12
- scratchattach/site/activity.py +11 -43
- scratchattach/site/alert.py +227 -0
- scratchattach/site/backpack_asset.py +2 -2
- scratchattach/site/browser_cookie3_stub.py +17 -0
- scratchattach/site/browser_cookies.py +27 -21
- scratchattach/site/classroom.py +51 -34
- scratchattach/site/cloud_activity.py +4 -4
- scratchattach/site/comment.py +30 -8
- scratchattach/site/forum.py +101 -69
- scratchattach/site/project.py +37 -17
- scratchattach/site/session.py +169 -79
- scratchattach/site/studio.py +4 -4
- scratchattach/site/user.py +179 -64
- scratchattach/utils/commons.py +35 -23
- scratchattach/utils/enums.py +44 -5
- scratchattach/utils/exceptions.py +10 -0
- scratchattach/utils/requests.py +57 -31
- {scratchattach-2.1.13.dist-info → scratchattach-2.1.14.dist-info}/METADATA +8 -3
- scratchattach-2.1.14.dist-info/RECORD +66 -0
- {scratchattach-2.1.13.dist-info → scratchattach-2.1.14.dist-info}/WHEEL +1 -1
- scratchattach/editor/sbuild.py +0 -2837
- scratchattach-2.1.13.dist-info/RECORD +0 -63
- {scratchattach-2.1.13.dist-info → scratchattach-2.1.14.dist-info}/licenses/LICENSE +0 -0
- {scratchattach-2.1.13.dist-info → scratchattach-2.1.14.dist-info}/top_level.txt +0 -0
scratchattach/editor/twconfig.py
CHANGED
|
@@ -17,7 +17,7 @@ You can move, resize, and minimize this comment, but don't edit it by hand. This
|
|
|
17
17
|
_END = " // _twconfig_"
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
@dataclass
|
|
20
|
+
@dataclass
|
|
21
21
|
class TWConfig(base.JSONSerializable):
|
|
22
22
|
framerate: int = None,
|
|
23
23
|
interpolation: bool = False,
|
|
@@ -100,6 +100,7 @@ def get_twconfig_data(string: str) -> dict | None:
|
|
|
100
100
|
return None
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
# todo: move this to commons.py?
|
|
103
104
|
def none_if_eq(data, compare) -> Any | None:
|
|
104
105
|
"""
|
|
105
106
|
Returns None if data and compare are the same
|
scratchattach/editor/vlb.py
CHANGED
|
@@ -5,8 +5,8 @@ from collections import defaultdict
|
|
|
5
5
|
from threading import Thread
|
|
6
6
|
from collections.abc import Callable
|
|
7
7
|
import traceback
|
|
8
|
-
from
|
|
9
|
-
from
|
|
8
|
+
from scratchattach.utils.requests import requests
|
|
9
|
+
from scratchattach.utils import exceptions
|
|
10
10
|
|
|
11
11
|
class BaseEventHandler(ABC):
|
|
12
12
|
_events: defaultdict[str, list[Callable]]
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""CloudEvents class"""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from
|
|
4
|
+
from scratchattach.cloud import _base
|
|
5
5
|
from ._base import BaseEventHandler
|
|
6
|
-
from
|
|
6
|
+
from scratchattach.site import cloud_activity
|
|
7
7
|
import time
|
|
8
8
|
import json
|
|
9
9
|
from collections.abc import Iterator
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
from .cloud_events import CloudEvents
|
|
5
|
-
from
|
|
5
|
+
from scratchattach.site import project
|
|
6
6
|
from threading import Thread, Event, current_thread
|
|
7
7
|
import time
|
|
8
8
|
import random
|
|
9
9
|
import traceback
|
|
10
|
-
from
|
|
11
|
-
from
|
|
10
|
+
from scratchattach.utils.encoder import Encoding
|
|
11
|
+
from scratchattach.utils import exceptions
|
|
12
12
|
|
|
13
13
|
class Request:
|
|
14
14
|
|
|
@@ -2,11 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket
|
|
4
4
|
from threading import Thread
|
|
5
|
-
from
|
|
5
|
+
from scratchattach.utils import exceptions
|
|
6
6
|
import json
|
|
7
7
|
import time
|
|
8
|
-
from
|
|
9
|
-
from
|
|
8
|
+
from scratchattach.site import cloud_activity
|
|
9
|
+
from scratchattach.site.user import User
|
|
10
10
|
from ._base import BaseEventHandler
|
|
11
11
|
|
|
12
12
|
class TwCloudSocket(WebSocket):
|
|
@@ -4,10 +4,10 @@ from __future__ import annotations
|
|
|
4
4
|
import json
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
from
|
|
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
11
|
from typing import Optional
|
|
12
12
|
|
|
13
13
|
|
|
@@ -10,9 +10,9 @@ import random
|
|
|
10
10
|
import string
|
|
11
11
|
import zipfile
|
|
12
12
|
from abc import ABC, abstractmethod
|
|
13
|
-
from
|
|
14
|
-
from
|
|
15
|
-
from
|
|
13
|
+
from scratchattach.utils import exceptions
|
|
14
|
+
from scratchattach.utils.commons import empty_project_json
|
|
15
|
+
from scratchattach.utils.requests import requests
|
|
16
16
|
# noinspection PyPep8Naming
|
|
17
17
|
def load_components(json_data: list, ComponentClass: type, target_list: list):
|
|
18
18
|
for element in json_data:
|
scratchattach/site/_base.py
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import TypeVar, Optional
|
|
4
5
|
|
|
5
6
|
import requests
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
from types import FunctionType
|
|
7
|
+
from scratchattach.utils import exceptions, commons
|
|
8
|
+
from . import session
|
|
9
9
|
|
|
10
10
|
C = TypeVar("C", bound="BaseSiteComponent")
|
|
11
11
|
class BaseSiteComponent(ABC):
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
_session: Optional[session.Session]
|
|
13
|
+
update_api: str
|
|
14
|
+
_headers: dict[str, str]
|
|
15
|
+
_cookies: dict[str, str]
|
|
16
|
+
|
|
17
|
+
# @abstractmethod
|
|
18
|
+
# def __init__(self): # dataclasses do not implement __init__ directly
|
|
19
|
+
# pass
|
|
18
20
|
|
|
19
21
|
def update(self):
|
|
20
22
|
"""
|
|
21
23
|
Updates the attributes of the object by performing an API response. Returns True if the update was successful.
|
|
22
24
|
"""
|
|
23
25
|
response = self.update_function(
|
|
24
|
-
self.
|
|
26
|
+
self.update_api,
|
|
25
27
|
headers=self._headers,
|
|
26
28
|
cookies=self._cookies, timeout=10
|
|
27
29
|
)
|
|
@@ -45,7 +47,6 @@ class BaseSiteComponent(ABC):
|
|
|
45
47
|
"""
|
|
46
48
|
Parses the API response that is fetched in the update-method. Class specific, must be overridden in classes inheriting from this one.
|
|
47
49
|
"""
|
|
48
|
-
pass
|
|
49
50
|
|
|
50
51
|
def _assert_auth(self):
|
|
51
52
|
if self._session is None:
|
|
@@ -59,7 +60,7 @@ class BaseSiteComponent(ABC):
|
|
|
59
60
|
"""
|
|
60
61
|
return commons._get_object(identificator_id, identificator, Class, NotFoundException, self._session)
|
|
61
62
|
|
|
62
|
-
update_function
|
|
63
|
+
update_function = requests.get
|
|
63
64
|
"""
|
|
64
65
|
Internal function run on update. Function is a method of the 'requests' module/class
|
|
65
66
|
"""
|
scratchattach/site/activity.py
CHANGED
|
@@ -5,7 +5,7 @@ from bs4 import PageElement
|
|
|
5
5
|
|
|
6
6
|
from . import user, project, studio
|
|
7
7
|
from ._base import BaseSiteComponent
|
|
8
|
-
from
|
|
8
|
+
from scratchattach.utils import exceptions
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Activity(BaseSiteComponent):
|
|
@@ -20,7 +20,6 @@ class Activity(BaseSiteComponent):
|
|
|
20
20
|
return str(self.raw)
|
|
21
21
|
|
|
22
22
|
def __init__(self, **entries):
|
|
23
|
-
|
|
24
23
|
# Set attributes every Activity object needs to have:
|
|
25
24
|
self._session = None
|
|
26
25
|
self.raw = None
|
|
@@ -81,7 +80,8 @@ class Activity(BaseSiteComponent):
|
|
|
81
80
|
recipient_username = None
|
|
82
81
|
|
|
83
82
|
default_case = False
|
|
84
|
-
|
|
83
|
+
# Even if `activity_type` is an invalid value; it will default to 'user performed an action'
|
|
84
|
+
|
|
85
85
|
if activity_type == 0:
|
|
86
86
|
# follow
|
|
87
87
|
followed_username = data["followed_username"]
|
|
@@ -150,13 +150,7 @@ class Activity(BaseSiteComponent):
|
|
|
150
150
|
self.project_id = project_id
|
|
151
151
|
self.recipient_username = recipient_username
|
|
152
152
|
|
|
153
|
-
elif activity_type
|
|
154
|
-
default_case = True
|
|
155
|
-
|
|
156
|
-
elif activity_type == 9:
|
|
157
|
-
default_case = True
|
|
158
|
-
|
|
159
|
-
elif activity_type == 10:
|
|
153
|
+
elif activity_type in (8, 9, 10):
|
|
160
154
|
# Share/Reshare project
|
|
161
155
|
project_id = data["project"]
|
|
162
156
|
is_reshare = data["is_reshare"]
|
|
@@ -187,9 +181,8 @@ class Activity(BaseSiteComponent):
|
|
|
187
181
|
self.project_id = parent_id
|
|
188
182
|
self.recipient_username = recipient_username
|
|
189
183
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
184
|
+
# type 12 does not exist in the HTML. That's why it was removed, not merged with type 13.
|
|
185
|
+
|
|
193
186
|
elif activity_type == 13:
|
|
194
187
|
# Create ('add') studio
|
|
195
188
|
studio_id = data["gallery"]
|
|
@@ -216,16 +209,7 @@ class Activity(BaseSiteComponent):
|
|
|
216
209
|
self.username = username
|
|
217
210
|
self.gallery_id = studio_id
|
|
218
211
|
|
|
219
|
-
elif activity_type
|
|
220
|
-
default_case = True
|
|
221
|
-
|
|
222
|
-
elif activity_type == 17:
|
|
223
|
-
default_case = True
|
|
224
|
-
|
|
225
|
-
elif activity_type == 18:
|
|
226
|
-
default_case = True
|
|
227
|
-
|
|
228
|
-
elif activity_type == 19:
|
|
212
|
+
elif activity_type in (16, 17, 18, 19):
|
|
229
213
|
# Remove project from studio
|
|
230
214
|
|
|
231
215
|
project_id = data["project"]
|
|
@@ -240,13 +224,7 @@ class Activity(BaseSiteComponent):
|
|
|
240
224
|
self.username = username
|
|
241
225
|
self.project_id = project_id
|
|
242
226
|
|
|
243
|
-
elif activity_type
|
|
244
|
-
default_case = True
|
|
245
|
-
|
|
246
|
-
elif activity_type == 21:
|
|
247
|
-
default_case = True
|
|
248
|
-
|
|
249
|
-
elif activity_type == 22:
|
|
227
|
+
elif activity_type in (20, 21, 22):
|
|
250
228
|
# Was promoted to manager for studio
|
|
251
229
|
studio_id = data["gallery"]
|
|
252
230
|
|
|
@@ -260,13 +238,7 @@ class Activity(BaseSiteComponent):
|
|
|
260
238
|
self.recipient_username = recipient_username
|
|
261
239
|
self.gallery_id = studio_id
|
|
262
240
|
|
|
263
|
-
elif activity_type
|
|
264
|
-
default_case = True
|
|
265
|
-
|
|
266
|
-
elif activity_type == 24:
|
|
267
|
-
default_case = True
|
|
268
|
-
|
|
269
|
-
elif activity_type == 25:
|
|
241
|
+
elif activity_type in (23, 24, 25):
|
|
270
242
|
# Update profile
|
|
271
243
|
raw = f"{username} made a profile update"
|
|
272
244
|
|
|
@@ -276,10 +248,7 @@ class Activity(BaseSiteComponent):
|
|
|
276
248
|
|
|
277
249
|
self.username = username
|
|
278
250
|
|
|
279
|
-
elif activity_type
|
|
280
|
-
default_case = True
|
|
281
|
-
|
|
282
|
-
elif activity_type == 27:
|
|
251
|
+
elif activity_type in (26, 27):
|
|
283
252
|
# Comment (quite complicated)
|
|
284
253
|
comment_type: int = data["comment_type"]
|
|
285
254
|
fragment = data["comment_fragment"]
|
|
@@ -313,13 +282,12 @@ class Activity(BaseSiteComponent):
|
|
|
313
282
|
self.comment_obj_id = comment_obj_id
|
|
314
283
|
self.comment_obj_title = comment_obj_title
|
|
315
284
|
self.comment_id = comment_id
|
|
316
|
-
|
|
317
285
|
else:
|
|
318
286
|
default_case = True
|
|
319
287
|
|
|
320
288
|
if default_case:
|
|
321
289
|
# This is coded in the scratch HTML, haven't found an example of it though
|
|
322
|
-
raw = f"{username} performed an action"
|
|
290
|
+
raw = f"{username} performed an action."
|
|
323
291
|
|
|
324
292
|
self.raw = raw
|
|
325
293
|
self.datetime_created = _time
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# classroom alerts (& normal alerts in the future)
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import pprint
|
|
7
|
+
import warnings
|
|
8
|
+
from dataclasses import dataclass, field, KW_ONLY
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
11
|
+
from typing_extensions import Self
|
|
12
|
+
|
|
13
|
+
from . import user, project, studio, comment, session
|
|
14
|
+
from scratchattach.utils import enums
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# todo: implement regular alerts
|
|
21
|
+
# If you implement regular alerts, it may be applicable to make EducatorAlert a subclass.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class EducatorAlert:
|
|
26
|
+
"""
|
|
27
|
+
Represents an alert for student activity, viewable at https://scratch.mit.edu/site-api/classrooms/alerts/
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
model: The type of alert (presumably); should always equal "educators.educatoralert" in this class
|
|
31
|
+
type: An integer that identifies the type of alert, differentiating e.g. against bans or autoban or censored comments etc
|
|
32
|
+
raw: The raw JSON data from the API
|
|
33
|
+
id: The ID of the alert (internally called 'pk' by scratch, not sure what this is for)
|
|
34
|
+
time_read: The time the alert was read
|
|
35
|
+
time_created: The time the alert was created
|
|
36
|
+
target: The user that the alert is about (the student)
|
|
37
|
+
actor: The user that created the alert (the admin)
|
|
38
|
+
target_object: The object that the alert is about (e.g. a project, studio, or comment)
|
|
39
|
+
notification_type: not sure what this is for, but inferred from the scratch HTML reference
|
|
40
|
+
"""
|
|
41
|
+
_: KW_ONLY
|
|
42
|
+
# required attrs
|
|
43
|
+
target: user.User
|
|
44
|
+
actor: user.User
|
|
45
|
+
target_object: Optional[Union[project.Project, studio.Studio, comment.Comment, studio.Studio]]
|
|
46
|
+
notification_type: str
|
|
47
|
+
_session: Optional[session.Session]
|
|
48
|
+
|
|
49
|
+
# defaulted attrs
|
|
50
|
+
model: str = "educators.educatoralert"
|
|
51
|
+
type: int = -1
|
|
52
|
+
raw: dict = field(repr=False, default_factory=dict)
|
|
53
|
+
id: int = -1
|
|
54
|
+
time_read: datetime = datetime.fromtimestamp(0.0)
|
|
55
|
+
time_created: datetime = datetime.fromtimestamp(0.0)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_json(cls, data: dict[str, Any], _session: Optional[session.Session] = None) -> Self:
|
|
60
|
+
"""
|
|
61
|
+
Load an EducatorAlert from a JSON object.
|
|
62
|
+
|
|
63
|
+
Arguments:
|
|
64
|
+
data (dict): The JSON object
|
|
65
|
+
_session (session.Session): The session object used to load this data, to 'connect' to the alerts rather than just 'get' them
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
EducatorAlert: The loaded EducatorAlert object
|
|
69
|
+
"""
|
|
70
|
+
model = data.get("model") # With this class, should be equal to educators.educatoralert
|
|
71
|
+
assert isinstance(model, str)
|
|
72
|
+
alert_id = data.get("pk") # not sure what kind of pk/id this is. Doesn't seem to be a user or class id.
|
|
73
|
+
assert isinstance(alert_id, int)
|
|
74
|
+
|
|
75
|
+
fields = data.get("fields")
|
|
76
|
+
assert isinstance(fields, dict)
|
|
77
|
+
|
|
78
|
+
time_read_raw = fields.get("educator_datetime_read")
|
|
79
|
+
assert isinstance(time_read_raw, str)
|
|
80
|
+
time_read: datetime = datetime.fromisoformat(time_read_raw)
|
|
81
|
+
|
|
82
|
+
admin_action = fields.get("admin_action")
|
|
83
|
+
assert isinstance(admin_action, dict)
|
|
84
|
+
|
|
85
|
+
time_created_raw = admin_action.get("datetime_created")
|
|
86
|
+
assert isinstance(time_created_raw, str)
|
|
87
|
+
time_created: datetime = datetime.fromisoformat(time_created_raw)
|
|
88
|
+
|
|
89
|
+
alert_type = admin_action.get("type")
|
|
90
|
+
assert isinstance(alert_type, int)
|
|
91
|
+
|
|
92
|
+
target_data = admin_action.get("target_user")
|
|
93
|
+
assert isinstance(target_data, dict)
|
|
94
|
+
target = user.User(username=target_data.get("username"),
|
|
95
|
+
id=target_data.get("pk"),
|
|
96
|
+
icon_url=target_data.get("thumbnail_url"),
|
|
97
|
+
admin=target_data.get("admin", False),
|
|
98
|
+
_session=_session)
|
|
99
|
+
|
|
100
|
+
actor_data = admin_action.get("actor")
|
|
101
|
+
assert isinstance(actor_data, dict)
|
|
102
|
+
actor = user.User(username=actor_data.get("username"),
|
|
103
|
+
id=actor_data.get("pk"),
|
|
104
|
+
icon_url=actor_data.get("thumbnail_url"),
|
|
105
|
+
admin=actor_data.get("admin", False),
|
|
106
|
+
_session=_session)
|
|
107
|
+
|
|
108
|
+
object_id = admin_action.get("object_id") # this could be a comment id, a project id, etc.
|
|
109
|
+
assert isinstance(object_id, int)
|
|
110
|
+
target_object: project.Project | studio.Studio | comment.Comment | None = None
|
|
111
|
+
|
|
112
|
+
extra_data: dict[str, Any] = json.loads(admin_action.get("extra_data", "{}"))
|
|
113
|
+
# todo: if possible, properly implement the incomplete parts of this parser (look for warning.warn())
|
|
114
|
+
notification_type: str = ""
|
|
115
|
+
|
|
116
|
+
if "project_title" in extra_data:
|
|
117
|
+
# project
|
|
118
|
+
target_object = project.Project(id=object_id,
|
|
119
|
+
title=extra_data["project_title"],
|
|
120
|
+
_session=_session)
|
|
121
|
+
elif "comment_content" in extra_data:
|
|
122
|
+
# comment
|
|
123
|
+
comment_data: dict[str, Any] = extra_data["comment_content"]
|
|
124
|
+
content: str | None = comment_data.get("content")
|
|
125
|
+
|
|
126
|
+
comment_obj_id: int | None = comment_data.get("comment_obj_id")
|
|
127
|
+
|
|
128
|
+
comment_type: int | None = comment_data.get("comment_type")
|
|
129
|
+
|
|
130
|
+
if comment_type == 0:
|
|
131
|
+
# project
|
|
132
|
+
comment_source_type = "project"
|
|
133
|
+
elif comment_type == 1:
|
|
134
|
+
# profile
|
|
135
|
+
comment_source_type = "profile"
|
|
136
|
+
else:
|
|
137
|
+
# probably a studio
|
|
138
|
+
comment_source_type = "studio"
|
|
139
|
+
warnings.warn(
|
|
140
|
+
f"The parser was not able to recognise the \"comment_type\" of {comment_type} in the alert JSON response.\n"
|
|
141
|
+
f"Full response: \n{pprint.pformat(data)}.\n\n"
|
|
142
|
+
f"Please draft an issue on github: https://github.com/TimMcCool/scratchattach/issues, providing this "
|
|
143
|
+
f"whole error message. This will allow us to implement an incomplete part of this parser")
|
|
144
|
+
|
|
145
|
+
# the comment_obj's corresponding attribute of comment.Comment is the place() method. As it has no cache, the title data is wasted.
|
|
146
|
+
# if the comment_obj is deleted, this is still a valid way of working out the title/username
|
|
147
|
+
|
|
148
|
+
target_object = comment.Comment(
|
|
149
|
+
id=object_id,
|
|
150
|
+
content=content,
|
|
151
|
+
source=comment_source_type,
|
|
152
|
+
source_id=comment_obj_id,
|
|
153
|
+
_session=_session
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
elif "gallery_title" in extra_data:
|
|
157
|
+
# studio
|
|
158
|
+
# possible implemented incorrectly
|
|
159
|
+
target_object = studio.Studio(
|
|
160
|
+
id=object_id,
|
|
161
|
+
title=extra_data["gallery_title"],
|
|
162
|
+
_session=_session
|
|
163
|
+
)
|
|
164
|
+
elif "notification_type" in extra_data:
|
|
165
|
+
# possible implemented incorrectly
|
|
166
|
+
notification_type = extra_data["notification_type"]
|
|
167
|
+
else:
|
|
168
|
+
warnings.warn(
|
|
169
|
+
f"The parser was not able to recognise the \"extra_data\" in the alert JSON response.\n"
|
|
170
|
+
f"Full response: \n{pprint.pformat(data)}.\n\n"
|
|
171
|
+
f"Please draft an issue on github: https://github.com/TimMcCool/scratchattach/issues, providing this "
|
|
172
|
+
f"whole error message. This will allow us to implement an incomplete part of this parser")
|
|
173
|
+
|
|
174
|
+
return cls(
|
|
175
|
+
id=alert_id,
|
|
176
|
+
model=model,
|
|
177
|
+
type=alert_type,
|
|
178
|
+
raw=data,
|
|
179
|
+
time_read=time_read,
|
|
180
|
+
time_created=time_created,
|
|
181
|
+
target=target,
|
|
182
|
+
actor=actor,
|
|
183
|
+
target_object=target_object,
|
|
184
|
+
notification_type=notification_type,
|
|
185
|
+
_session=_session
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def __str__(self):
|
|
189
|
+
return f"EducatorAlert: {self.message}"
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def alert_type(self) -> enums.AlertType:
|
|
193
|
+
"""
|
|
194
|
+
Get an associated AlertType object for this alert (based on the type index)
|
|
195
|
+
"""
|
|
196
|
+
alert_type = enums.AlertTypes.find(self.type)
|
|
197
|
+
if not alert_type:
|
|
198
|
+
alert_type = enums.AlertTypes.default.value
|
|
199
|
+
|
|
200
|
+
return alert_type
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def message(self):
|
|
204
|
+
"""
|
|
205
|
+
Format the alert message using the alert type's message template, as it would be on the website.
|
|
206
|
+
"""
|
|
207
|
+
raw_message = self.alert_type.message
|
|
208
|
+
comment_content = ""
|
|
209
|
+
if isinstance(self.target_object, comment.Comment):
|
|
210
|
+
comment_content = self.target_object.content
|
|
211
|
+
|
|
212
|
+
return raw_message.format(username=self.target.username,
|
|
213
|
+
project=self.target_object_title,
|
|
214
|
+
studio=self.target_object_title,
|
|
215
|
+
notification_type=self.notification_type,
|
|
216
|
+
comment=comment_content)
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def target_object_title(self):
|
|
220
|
+
"""
|
|
221
|
+
Get the title of the target object (if applicable)
|
|
222
|
+
"""
|
|
223
|
+
if isinstance(self.target_object, project.Project):
|
|
224
|
+
return self.target_object.title
|
|
225
|
+
if isinstance(self.target_object, studio.Studio):
|
|
226
|
+
return self.target_object.title
|
|
227
|
+
return None # explicit
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# browser_cookie3.pyi
|
|
2
|
+
|
|
3
|
+
import http.cookiejar
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
def chrome(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
|
|
7
|
+
def chromium(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
|
|
8
|
+
def firefox(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
|
|
9
|
+
def opera(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
|
|
10
|
+
def edge(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
|
|
11
|
+
def brave(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
|
|
12
|
+
def vivaldi(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
|
|
13
|
+
def safari(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
|
|
14
|
+
def lynx(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
|
|
15
|
+
def w3m(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
|
|
16
|
+
|
|
17
|
+
def load() -> http.cookiejar.CookieJar: return NotImplemented
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
from typing import Optional
|
|
1
|
+
from typing import Optional, TYPE_CHECKING
|
|
2
|
+
from typing_extensions import assert_never
|
|
2
3
|
from http.cookiejar import CookieJar
|
|
3
4
|
from enum import Enum, auto
|
|
4
5
|
browsercookie_err = None
|
|
5
6
|
try:
|
|
6
|
-
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from . import browser_cookie3_stub as browser_cookie3
|
|
9
|
+
else:
|
|
10
|
+
import browser_cookie3
|
|
7
11
|
except Exception as e:
|
|
8
12
|
browsercookie = None
|
|
9
13
|
browsercookie_err = e
|
|
@@ -15,8 +19,8 @@ class Browser(Enum):
|
|
|
15
19
|
EDGE = auto()
|
|
16
20
|
SAFARI = auto()
|
|
17
21
|
CHROMIUM = auto()
|
|
18
|
-
EDGE_DEV = auto()
|
|
19
22
|
VIVALDI = auto()
|
|
23
|
+
EDGE_DEV = auto()
|
|
20
24
|
|
|
21
25
|
|
|
22
26
|
FIREFOX = Browser.FIREFOX
|
|
@@ -24,32 +28,34 @@ CHROME = Browser.CHROME
|
|
|
24
28
|
EDGE = Browser.EDGE
|
|
25
29
|
SAFARI = Browser.SAFARI
|
|
26
30
|
CHROMIUM = Browser.CHROMIUM
|
|
27
|
-
EDGE_DEV = Browser.EDGE_DEV
|
|
28
31
|
VIVALDI = Browser.VIVALDI
|
|
29
32
|
ANY = Browser.ANY
|
|
33
|
+
EDGE_DEV = Browser.EDGE_DEV
|
|
30
34
|
|
|
31
35
|
def cookies_from_browser(browser : Browser = ANY) -> dict[str, str]:
|
|
32
36
|
"""
|
|
33
37
|
Import cookies from browser to login
|
|
34
38
|
"""
|
|
35
|
-
if not
|
|
39
|
+
if not browser_cookie3:
|
|
36
40
|
raise browsercookie_err or ModuleNotFoundError()
|
|
37
41
|
cookies : Optional[CookieJar] = None
|
|
38
|
-
if browser
|
|
39
|
-
cookies =
|
|
40
|
-
elif browser
|
|
41
|
-
cookies =
|
|
42
|
-
elif browser
|
|
43
|
-
cookies =
|
|
44
|
-
elif browser
|
|
45
|
-
cookies =
|
|
46
|
-
elif browser
|
|
47
|
-
cookies =
|
|
48
|
-
elif browser
|
|
49
|
-
cookies =
|
|
50
|
-
elif browser
|
|
51
|
-
cookies =
|
|
52
|
-
elif browser
|
|
53
|
-
|
|
42
|
+
if browser is Browser.ANY:
|
|
43
|
+
cookies = browser_cookie3.load()
|
|
44
|
+
elif browser is Browser.FIREFOX:
|
|
45
|
+
cookies = browser_cookie3.firefox()
|
|
46
|
+
elif browser is Browser.CHROME:
|
|
47
|
+
cookies = browser_cookie3.chrome()
|
|
48
|
+
elif browser is Browser.EDGE:
|
|
49
|
+
cookies = browser_cookie3.edge()
|
|
50
|
+
elif browser is Browser.SAFARI:
|
|
51
|
+
cookies = browser_cookie3.safari()
|
|
52
|
+
elif browser is Browser.CHROMIUM:
|
|
53
|
+
cookies = browser_cookie3.chromium()
|
|
54
|
+
elif browser is Browser.VIVALDI:
|
|
55
|
+
cookies = browser_cookie3.vivaldi()
|
|
56
|
+
elif browser is Browser.EDGE_DEV:
|
|
57
|
+
raise ValueError("EDGE_DEV is not supported anymore.")
|
|
58
|
+
else:
|
|
59
|
+
assert_never(browser)
|
|
54
60
|
assert isinstance(cookies, CookieJar)
|
|
55
61
|
return {cookie.name: cookie.value for cookie in cookies if "scratch.mit.edu" in cookie.domain and cookie.value}
|