Skip to content

Commit 9432bb1

Browse files
authored
Merge pr #294 from FAReTek1/classes (if there are weird bugs maybe look here!)
Classroom abilities + create studio + misc NOTE: DUE TO THE SIZE OF THIS PR AND THE CONFLICTS THAT NEEDED TO BE SOLVED, IT IS POSSIBLE THAT SOME BUGS MAY HAVE BEEN INTRODUCED (sorry ;-;) # New classroom abilities Adds a wide array of capabilities for scratch's classroom system, including: - Creating classes - Loading closed classes using HTML web scraping - Checking if a class is closed - Setting thumbnails (also added the ability to set pfps of normal accounts as well) - Setting descriptions - Setting 'what we're working on'/status - Setting class title/name - Adding studios - Closing/Reopening classes - Registering students (NOT FROM CSV) **should this be removed?** - Generating signup link (This actually just uses the class token) - Fetching public activity (same format as user activity; loading from HTML) - Fetching private activity - things like comments or profile updates (in a bad format which is very improvable) - Registering users with simply a class token - Abilities to load stuff in classes in different sort modes # Other new abilities # create_studio() Added the `Session.create_studio()` function to create studios to solve [an issue](#277) (#277). I copy+pasted rate limit code from `Session.create_project()`. Also added title/desc settings to make it more convenient # exceptions - Now using the msg argument in `exceptions.Unauthorized()` so that you can add more detail to the exception if need be. When receiving a 403/401 status code in the `requests` wrapper, it will now print the request content which will sometimes be more descriptive than simply: "You are unauthorised" - Renamed `ConnectionError` to `CloudConnectionError` since `ConnectionError` is actually a builtin error ### misc changes/features - added password reset email sender (since i was resetting passwords of a lot of my old accounts) - A few scratchtools endpoints (isonline, is beta user, scratchtools display name) in `other_apis.py` - Added class option to `commons.webscrape_count` ### Extra Note(s) - In `cloud_requests.py`, I fixed a typo of `self.call_even`, changing it to `self.call_event`. In the rare case this is intentional, now you know - Also it looks like I pressed ctrl-alt-l again... - I added a lot of dosctrings at the start of when I made this, before I knew about `from __future__ import annotations` so that may need to be fixed
2 parents 52853ce + 4ba9ed0 commit 9432bb1

29 files changed

+1598
-590
lines changed

scratchattach/cloud/_base.py

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,62 @@
1-
from abc import ABC, abstractmethod
1+
from __future__ import annotations
22

3-
import websocket
43
import json
4+
import ssl
55
import time
6-
from ..utils import exceptions
7-
import warnings
6+
from abc import ABC
7+
8+
import websocket
9+
810
from ..eventhandlers import cloud_recorder
9-
import ssl
11+
from ..utils import exceptions
1012

11-
class BaseCloud(ABC):
1213

14+
class BaseCloud(ABC):
1315
"""
1416
Base class for a project's cloud variables. Represents a cloud.
1517
16-
When inheriting from this class, the __init__ function of the inherited class ...
17-
18+
When inheriting from this class, the __init__ function of the inherited class:
1819
- must first call the constructor of the super class: super().__init__()
19-
2020
- must then set some attributes
2121
2222
Attributes that must be specified in the __init__ function a class inheriting from this one:
23+
project_id: Project id of the cloud variables
2324
24-
:self.project_id: Project id of the cloud variables
25-
26-
:self.cloud_host: URL of the websocket server ("wss://..." or "ws://...")
25+
cloud_host: URL of the websocket server ("wss://..." or "ws://...")
2726
2827
Attributes that can, but don't have to be specified in the __init__ function:
2928
30-
:self._session: Either None or a site.session.Session object. Defaults to None.
29+
_session: Either None or a site.session.Session object. Defaults to None.
3130
32-
:self.ws_shortterm_ratelimit: The wait time between cloud variable sets. Defaults to 0.1
31+
ws_shortterm_ratelimit: The wait time between cloud variable sets. Defaults to 0.1
3332
34-
:self.ws_longterm_ratelimit: The amount of cloud variable set that can be performed long-term without ever getting ratelimited
33+
ws_longterm_ratelimit: The amount of cloud variable set that can be performed long-term without ever getting ratelimited
3534
36-
:self.allow_non_numeric: Whether non-numeric cloud variable values are allowed. Defaults to False
35+
allow_non_numeric: Whether non-numeric cloud variable values are allowed. Defaults to False
3736
38-
:self.length_limit: Length limit for cloud variable values. Defaults to 100000
37+
length_limit: Length limit for cloud variable values. Defaults to 100000
3938
40-
:self.username: The username to send during handshake. Defaults to "scratchattach"
39+
username: The username to send during handshake. Defaults to "scratchattach"
4140
42-
:self.header: The header to send. Defaults to None
41+
header: The header to send. Defaults to None
4342
44-
:self.cookie: The cookie to send. Defaults to None
43+
cookie: The cookie to send. Defaults to None
4544
46-
:self.origin: The origin to send. Defaults to None
45+
origin: The origin to send. Defaults to None
4746
48-
:self.print_connect_messages: Whether to print a message on every connect to the cloud server. Defaults to False.
47+
print_connect_messages: Whether to print a message on every connect to the cloud server. Defaults to False.
4948
"""
5049

50+
5151
def __init__(self, *, _session=None):
5252

5353
# Required internal attributes that every object representing a cloud needs to have (no matter what cloud is represented):
5454
self._session = _session
5555
self.active_connection = False #whether a connection to a cloud variable server is currently established
56+
5657
self.websocket = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
57-
self.recorder = None # A CloudRecorder object that records cloud activity for the values to be retrieved later will be saved in this attribute as soon as .get_var is called
58+
self.recorder = None # A CloudRecorder object that records cloud activity for the values to be retrieved later,
59+
# which will be saved in this attribute as soon as .get_var is called
5860
self.first_var_set = 0
5961
self.last_var_set = 0
6062
self.var_stets_since_first = 0
@@ -63,7 +65,8 @@ def __init__(self, *, _session=None):
6365
# (These attributes can be specifically in the constructors of classes inheriting from this base class)
6466
self.ws_shortterm_ratelimit = 0.06667
6567
self.ws_longterm_ratelimit = 0.1
66-
self.ws_timeout = 3 # Timeout for send operations (after the timeout, the connection will be renewed and the operation will be retried 3 times)
68+
self.ws_timeout = 3 # Timeout for send operations (after the timeout,
69+
# the connection will be renewed and the operation will be retried 3 times)
6770
self.allow_non_numeric = False
6871
self.length_limit = 100000
6972
self.username = "scratchattach"
@@ -100,7 +103,7 @@ def _send_packet(self, packet):
100103
self.websocket.send(json.dumps(packet) + "\n")
101104
except Exception:
102105
self.active_connection = False
103-
raise exceptions.ConnectionError(f"Sending packet failed three times in a row: {packet}")
106+
raise exceptions.CloudConnectionError(f"Sending packet failed three times in a row: {packet}")
104107

105108
def _send_packet_list(self, packet_list):
106109
packet_string = "".join([json.dumps(packet) + "\n" for packet in packet_list])
@@ -126,7 +129,8 @@ def _send_packet_list(self, packet_list):
126129
self.websocket.send(packet_string)
127130
except Exception:
128131
self.active_connection = False
129-
raise exceptions.ConnectionError(f"Sending packet list failed four times in a row: {packet_list}")
132+
raise exceptions.CloudConnectionError(
133+
f"Sending packet list failed four times in a row: {packet_list}")
130134

131135
def _handshake(self):
132136
packet = {"method": "handshake", "user": self.username, "project_id": self.project_id}
@@ -139,8 +143,8 @@ def connect(self):
139143
cookie=self.cookie,
140144
origin=self.origin,
141145
enable_multithread=True,
142-
timeout = self.ws_timeout,
143-
header = self.header
146+
timeout=self.ws_timeout,
147+
header=self.header
144148
)
145149
self._handshake()
146150
self.active_connection = True
@@ -166,29 +170,29 @@ def _assert_valid_value(self, value):
166170
if not (value in [True, False, float('inf'), -float('inf')]):
167171
value = str(value)
168172
if len(value) > self.length_limit:
169-
raise(exceptions.InvalidCloudValue(
173+
raise (exceptions.InvalidCloudValue(
170174
f"Value exceeds length limit: {str(value)}"
171175
))
172176
if not self.allow_non_numeric:
173177
x = value.replace(".", "")
174178
x = x.replace("-", "")
175179
if not (x.isnumeric() or x == ""):
176-
raise(exceptions.InvalidCloudValue(
180+
raise (exceptions.InvalidCloudValue(
177181
"Value not numeric"
178182
))
179183

180184
def _enforce_ratelimit(self, *, n):
181185
# n is the amount of variables being set
182-
if (time.time() - self.first_var_set) / (self.var_stets_since_first+1) > self.ws_longterm_ratelimit: # if the average delay between cloud variable sets has been bigger than the long-term rate-limit, cloud variables can be set fast (wait time smaller than long-term rate limit) again
186+
if (time.time() - self.first_var_set) / (
187+
self.var_stets_since_first + 1) > self.ws_longterm_ratelimit: # if the average delay between cloud variable sets has been bigger than the long-term rate-limit, cloud variables can be set fast (wait time smaller than long-term rate limit) again
183188
self.var_stets_since_first = 0
184189
self.first_var_set = time.time()
185190

186191
wait_time = self.ws_shortterm_ratelimit * n
187-
if time.time() - self.first_var_set > 25: # if cloud variables have been continously set fast (wait time smaller than long-term rate limit) for 25 seconds, they should be set slow now (wait time = long-term rate limit) to avoid getting rate-limited
192+
if time.time() - self.first_var_set > 25: # if cloud variables have been continously set fast (wait time smaller than long-term rate limit) for 25 seconds, they should be set slow now (wait time = long-term rate limit) to avoid getting rate-limited
188193
wait_time = self.ws_longterm_ratelimit * n
189194
while self.last_var_set + wait_time >= time.time():
190195
time.sleep(0.001)
191-
192196

193197
def set_var(self, variable, value):
194198
"""
@@ -231,7 +235,7 @@ def set_vars(self, var_value_dict, *, intelligent_waits=True):
231235
self.connect()
232236
if intelligent_waits:
233237
self._enforce_ratelimit(n=len(list(var_value_dict.keys())))
234-
238+
235239
self.var_stets_since_first += len(list(var_value_dict.keys()))
236240

237241
packet_list = []
@@ -256,7 +260,7 @@ def get_var(self, var, *, recorder_initial_values={}):
256260
self.recorder = cloud_recorder.CloudRecorder(self, initial_values=recorder_initial_values)
257261
self.recorder.start()
258262
start_time = time.time()
259-
while not (self.recorder.cloud_values != {} or start_time < time.time() -5):
263+
while not (self.recorder.cloud_values != {} or start_time < time.time() - 5):
260264
time.sleep(0.01)
261265
return self.recorder.get_var(var)
262266

@@ -265,17 +269,19 @@ def get_all_vars(self, *, recorder_initial_values={}):
265269
self.recorder = cloud_recorder.CloudRecorder(self, initial_values=recorder_initial_values)
266270
self.recorder.start()
267271
start_time = time.time()
268-
while not (self.recorder.cloud_values != {} or start_time < time.time() -5):
272+
while not (self.recorder.cloud_values != {} or start_time < time.time() - 5):
269273
time.sleep(0.01)
270274
return self.recorder.get_all_vars()
271275

272276
def events(self):
273277
from ..eventhandlers.cloud_events import CloudEvents
274278
return CloudEvents(self)
275279

276-
def requests(self, *, no_packet_loss=False, used_cloud_vars=["1", "2", "3", "4", "5", "6", "7", "8", "9"], respond_order="receive"):
280+
def requests(self, *, no_packet_loss=False, used_cloud_vars=["1", "2", "3", "4", "5", "6", "7", "8", "9"],
281+
respond_order="receive"):
277282
from ..eventhandlers.cloud_requests import CloudRequests
278-
return CloudRequests(self, used_cloud_vars=used_cloud_vars, no_packet_loss=no_packet_loss, respond_order=respond_order)
283+
return CloudRequests(self, used_cloud_vars=used_cloud_vars, no_packet_loss=no_packet_loss,
284+
respond_order=respond_order)
279285

280286
def storage(self, *, no_packet_loss=False, used_cloud_vars=["1", "2", "3", "4", "5", "6", "7", "8", "9"]):
281287
from ..eventhandlers.cloud_storage import CloudStorage

scratchattach/cloud/cloud.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
"""v2 ready: ScratchCloud, TwCloud and CustomCloud classes"""
22

3+
from __future__ import annotations
4+
35
from ._base import BaseCloud
46
from typing import Type
57
from ..utils.requests import Requests as requests
68
from ..utils import exceptions, commons
79
from ..site import cloud_activity
810

9-
class ScratchCloud(BaseCloud):
1011

12+
class ScratchCloud(BaseCloud):
1113
def __init__(self, *, project_id, _session=None):
1214
super().__init__()
1315

@@ -91,9 +93,10 @@ def events(self, *, use_logs=False):
9193
else:
9294
return super().events()
9395

94-
class TwCloud(BaseCloud):
9596

96-
def __init__(self, *, project_id, cloud_host="wss://clouddata.turbowarp.org", purpose="", contact=""):
97+
class TwCloud(BaseCloud):
98+
def __init__(self, *, project_id, cloud_host="wss://clouddata.turbowarp.org", purpose="", contact="",
99+
_session=None):
97100
super().__init__()
98101

99102
self.project_id = project_id

scratchattach/eventhandlers/_base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
from abc import ABC, abstractmethod
24
from ..utils.requests import Requests as requests
35
from threading import Thread

scratchattach/eventhandlers/cloud_events.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""CloudEvents class"""
2+
from __future__ import annotations
23

34
from ..cloud import cloud
45
from ._base import BaseEventHandler
Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
"""CloudRecorder class (used by ScratchCloud, TwCloud and other classes inheriting from BaseCloud to deliver cloud var values)"""
2+
from __future__ import annotations
23

34
from .cloud_events import CloudEvents
45

6+
57
class CloudRecorder(CloudEvents):
8+
def __init__(self, cloud, *, initial_values: dict = None):
9+
if initial_values is None:
10+
initial_values = {}
611

7-
def __init__(self, cloud, *, initial_values={}):
812
super().__init__(cloud)
913
self.cloud_values = initial_values
1014
self.event(self.on_set)
1115

1216
def get_var(self, var):
13-
if not var in self.cloud_values:
17+
if var not in self.cloud_values:
1418
return None
1519
return self.cloud_values[var]
16-
20+
1721
def get_all_vars(self):
1822
return self.cloud_values
19-
23+
2024
def on_set(self, activity):
2125
self.cloud_values[activity.var] = activity.value

scratchattach/eventhandlers/cloud_requests.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""CloudRequests class (threading.Event version)"""
2+
from __future__ import annotations
23

34
from .cloud_events import CloudEvents
45
from ..site import project
@@ -167,8 +168,8 @@ def _parse_output(self, request_name, output, request_id):
167168
def _set_FROM_HOST_var(self, value):
168169
try:
169170
self.cloud.set_var(f"FROM_HOST_{self.used_cloud_vars[self.current_var]}", value)
170-
except exceptions.ConnectionError:
171-
self.call_even("on_disconnect")
171+
except exceptions.CloudConnectionError:
172+
self.call_event("on_disconnect")
172173
except Exception as e:
173174
print("scratchattach: internal error while responding (please submit a bug report on GitHub):", e)
174175
self.current_var += 1

scratchattach/eventhandlers/cloud_server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket
24
from threading import Thread
35
from ..utils import exceptions

scratchattach/eventhandlers/cloud_storage.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""CloudStorage class"""
2+
from __future__ import annotations
23

34
from .cloud_requests import CloudRequests
45
import json

scratchattach/eventhandlers/combine.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
class MultiEventHandler:
24

35
def __init__(self, *handlers):

scratchattach/eventhandlers/filterbot.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""FilterBot class"""
2+
from __future__ import annotations
23

34
from .message_events import MessageEvents
45
import time

0 commit comments

Comments
 (0)