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,14 +1,36 @@
|
|
|
1
1
|
"""CloudRequests class (threading.Event version)"""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from
|
|
5
|
-
from scratchattach.site import project
|
|
6
|
-
from threading import Thread, Event, current_thread
|
|
4
|
+
from threading import Thread, Event, local
|
|
7
5
|
import time
|
|
8
6
|
import random
|
|
9
7
|
import traceback
|
|
8
|
+
import warnings
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Protocol, Any, runtime_checkable, TypedDict, Union, Optional
|
|
11
|
+
from enum import Enum, auto
|
|
12
|
+
|
|
10
13
|
from scratchattach.utils.encoder import Encoding
|
|
11
14
|
from scratchattach.utils import exceptions
|
|
15
|
+
from scratchattach.site import project, cloud_activity
|
|
16
|
+
from scratchattach.cloud import _base
|
|
17
|
+
from .cloud_events import CloudEvents
|
|
18
|
+
|
|
19
|
+
class RequestHandlerThreadInfo(local):
|
|
20
|
+
request_id: str = ""
|
|
21
|
+
|
|
22
|
+
request_handler_thread_info = RequestHandlerThreadInfo()
|
|
23
|
+
|
|
24
|
+
class ErrorInRequest(RuntimeWarning):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
class ErrorWithMessage(Exception):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@runtime_checkable
|
|
31
|
+
class RequestHandler(Protocol):
|
|
32
|
+
def __call__(self, *args: str) -> Any:
|
|
33
|
+
pass
|
|
12
34
|
|
|
13
35
|
class Request:
|
|
14
36
|
|
|
@@ -16,7 +38,17 @@ class Request:
|
|
|
16
38
|
Saves a request added to the request handler
|
|
17
39
|
"""
|
|
18
40
|
|
|
19
|
-
def __init__(
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
request_name: str,
|
|
44
|
+
*,
|
|
45
|
+
on_call: RequestHandler,
|
|
46
|
+
cloud_requests: CloudRequests,
|
|
47
|
+
thread: bool = True,
|
|
48
|
+
enabled: bool = True,
|
|
49
|
+
response_priority: int = 0,
|
|
50
|
+
debug: bool = False
|
|
51
|
+
):
|
|
20
52
|
self.name = request_name
|
|
21
53
|
self.on_call = on_call
|
|
22
54
|
self.thread = thread
|
|
@@ -25,45 +57,93 @@ class Request:
|
|
|
25
57
|
self.cloud_requests = cloud_requests # the corresponding CloudRequests object
|
|
26
58
|
self.debug = debug or self.cloud_requests.debug
|
|
27
59
|
|
|
28
|
-
def __call__(self, received_request):
|
|
60
|
+
def __call__(self, received_request: ReceivedRequest):
|
|
29
61
|
if not self.enabled:
|
|
30
62
|
self.cloud_requests.call_event("on_disabled_request", [received_request])
|
|
63
|
+
return
|
|
31
64
|
try:
|
|
32
|
-
|
|
65
|
+
request_handler_thread_info.request_id = received_request.request_id # Used by .get_requester() / .get_timestamp() as lookup key
|
|
33
66
|
output = self.on_call(*received_request.arguments)
|
|
34
|
-
self.cloud_requests.request_outputs.append({"receive":received_request.timestamp, "request_id":received_request.request_id, "output":output, "priority":self.response_priority})
|
|
67
|
+
self.cloud_requests.request_outputs.append({"receive":received_request.timestamp, "request_id": received_request.request_id, "output":output, "priority":self.response_priority})
|
|
68
|
+
except ErrorWithMessage as e:
|
|
69
|
+
self.cloud_requests.call_event("on_error", [received_request, e])
|
|
70
|
+
output = [i for arg in e.args for i in str(arg).splitlines()]
|
|
71
|
+
self.cloud_requests.request_outputs.append({"receive": received_request.timestamp, "request_id": received_request.request_id, "output": output, "priority": self.response_priority})
|
|
35
72
|
except Exception as e:
|
|
36
73
|
self.cloud_requests.call_event("on_error", [received_request, e])
|
|
37
74
|
if self.cloud_requests.ignore_exceptions:
|
|
38
|
-
|
|
39
|
-
f"Warning: Caught error in request
|
|
75
|
+
warnings.warn(
|
|
76
|
+
f"Warning: Caught error in request {self.name!r} - Full error below\n{traceback.format_exc()}",
|
|
77
|
+
ErrorInRequest
|
|
40
78
|
)
|
|
41
|
-
try:
|
|
42
|
-
traceback.print_exc()
|
|
43
|
-
except Exception:
|
|
44
|
-
print(e)
|
|
45
79
|
else:
|
|
46
|
-
print(f"Exception in request
|
|
80
|
+
print(f"Exception in request {self.name!r}:")
|
|
47
81
|
raise(e)
|
|
48
82
|
if self.debug:
|
|
49
83
|
traceback_full = traceback.format_exc().splitlines()
|
|
50
84
|
output = [f"Error in request {self.name}", "Traceback: "]
|
|
51
85
|
output.extend(traceback_full)
|
|
52
|
-
self.cloud_requests.request_outputs.append({"receive":received_request.timestamp, "request_id":received_request.request_id, "output":output, "priority":self.response_priority})
|
|
86
|
+
self.cloud_requests.request_outputs.append({"receive": received_request.timestamp, "request_id": received_request.request_id, "output": output, "priority": self.response_priority})
|
|
53
87
|
else:
|
|
54
|
-
self.cloud_requests.request_outputs.append({"receive":received_request.timestamp, "request_id":received_request.request_id, "output":[f"Error in request {self.name}","Check the Python console"], "priority":self.response_priority})
|
|
88
|
+
self.cloud_requests.request_outputs.append({"receive": received_request.timestamp, "request_id": received_request.request_id, "output": [f"Error in request {self.name}", "Check the Python console"], "priority": self.response_priority})
|
|
55
89
|
self.cloud_requests.responder_event.set() # Activate the .cloud_requests._responder process so it sends back the data to Scratch
|
|
56
90
|
|
|
57
|
-
class
|
|
91
|
+
class EmptyRequest(Request):
|
|
92
|
+
def __init__(self):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
def __call__(self, received_request):
|
|
96
|
+
raise TypeError("Empty request can not be called.")
|
|
58
97
|
|
|
59
|
-
|
|
60
|
-
|
|
98
|
+
@dataclass
|
|
99
|
+
class ReceivedRequest:
|
|
100
|
+
request: Request = field(kw_only=True, default_factory=EmptyRequest)
|
|
101
|
+
request_name: str = field(kw_only=True, default="")
|
|
102
|
+
requester: str = field(kw_only=True, default="")
|
|
103
|
+
timestamp: float = field(kw_only=True, default=0.0)
|
|
104
|
+
arguments: list[str] = field(kw_only=True, default_factory=list)
|
|
105
|
+
request_id: str = field(kw_only=True, default="0")
|
|
106
|
+
activity: cloud_activity.CloudActivity = field(kw_only=True)
|
|
107
|
+
|
|
108
|
+
class RespondOrder(Enum):
|
|
109
|
+
FINISH = auto() # {"receive":time.time()*1000, "request_id":"100000000"+str(random.randint(1000, 9999)), "output":data, "priority":priority}
|
|
110
|
+
RECEIVE = auto()
|
|
111
|
+
REQUEST_ID = auto()
|
|
112
|
+
OUTPUT = auto()
|
|
113
|
+
PRIORITY = auto()
|
|
114
|
+
|
|
115
|
+
class RequestOutput(TypedDict):
|
|
116
|
+
receive: float
|
|
117
|
+
request_id: str
|
|
118
|
+
output: Union[str, list[str]]
|
|
119
|
+
priority: int
|
|
120
|
+
|
|
121
|
+
class ResponseMemory(TypedDict):
|
|
122
|
+
rid: str
|
|
123
|
+
packets: dict[int, str]
|
|
61
124
|
|
|
62
125
|
class CloudRequests(CloudEvents):
|
|
63
126
|
|
|
64
127
|
# The CloudRequests class is built upon CloudEvents, similar to how Filterbot is built upon MessageEvents
|
|
65
128
|
|
|
66
|
-
|
|
129
|
+
_requests: dict[str, Request]
|
|
130
|
+
request_parts: dict[str, list[str]]
|
|
131
|
+
received_requests: list[ReceivedRequest]
|
|
132
|
+
executed_requests: dict[str, ReceivedRequest]
|
|
133
|
+
request_outputs: list[RequestOutput]
|
|
134
|
+
responded_request_ids: list[str]
|
|
135
|
+
packet_memory: list[ResponseMemory]
|
|
136
|
+
_packets_to_resend: list[str]
|
|
137
|
+
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
cloud: _base.AnyCloud,
|
|
141
|
+
used_cloud_vars: Optional[list[str]] = None,
|
|
142
|
+
no_packet_loss: bool = False,
|
|
143
|
+
respond_order: RespondOrder = RespondOrder.RECEIVE,
|
|
144
|
+
debug = False
|
|
145
|
+
):
|
|
146
|
+
used_cloud_vars = used_cloud_vars or ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
|
67
147
|
super().__init__(cloud)
|
|
68
148
|
# Setup
|
|
69
149
|
self._requests = {}
|
|
@@ -95,7 +175,7 @@ class CloudRequests(CloudEvents):
|
|
|
95
175
|
|
|
96
176
|
self.current_var = 0 # ID of the last set FROM_HOST_ variable (when a response is sent back to Scratch, these are set cyclically)
|
|
97
177
|
self.credit_check()
|
|
98
|
-
self.
|
|
178
|
+
self.hard_stopped = False # When set to True, all processes will halt immediately without finishing safely (can result in not fully received / responded requests etc.)
|
|
99
179
|
|
|
100
180
|
# -- Adding and removing requests --
|
|
101
181
|
|
|
@@ -103,24 +183,26 @@ class CloudRequests(CloudEvents):
|
|
|
103
183
|
"""
|
|
104
184
|
Decorator function. Adds a request to the request handler.
|
|
105
185
|
"""
|
|
106
|
-
def inner(
|
|
186
|
+
def inner(_function):
|
|
187
|
+
_name = _function.__name__ if name is None else name
|
|
107
188
|
# called if the decorator provides arguments
|
|
108
|
-
self._requests[
|
|
109
|
-
|
|
110
|
-
enabled
|
|
189
|
+
self._requests[_name] = Request(
|
|
190
|
+
_name,
|
|
191
|
+
enabled=enabled,
|
|
111
192
|
thread=thread,
|
|
112
193
|
response_priority=response_priority,
|
|
113
|
-
on_call=
|
|
194
|
+
on_call=_function,
|
|
114
195
|
cloud_requests=self,
|
|
115
196
|
debug=debug
|
|
116
197
|
)
|
|
198
|
+
return _function
|
|
117
199
|
|
|
118
200
|
if function is None:
|
|
119
201
|
# => the decorator provides arguments
|
|
120
202
|
return inner
|
|
121
203
|
else:
|
|
122
204
|
# => the decorator doesn't provide arguments
|
|
123
|
-
inner(function)
|
|
205
|
+
return inner(function)
|
|
124
206
|
|
|
125
207
|
def add_request(self, function, *, enabled=True, name=None):
|
|
126
208
|
self.request(enabled=enabled, name=name)(function)
|
|
@@ -128,10 +210,10 @@ class CloudRequests(CloudEvents):
|
|
|
128
210
|
def remove_request(self, name):
|
|
129
211
|
try:
|
|
130
212
|
self._requests.pop(name)
|
|
131
|
-
except
|
|
132
|
-
raise
|
|
213
|
+
except KeyError as e:
|
|
214
|
+
raise KeyError(
|
|
133
215
|
f"No request with name {name} found to remove"
|
|
134
|
-
)
|
|
216
|
+
) from e
|
|
135
217
|
|
|
136
218
|
# -- Parse and send back the request output --
|
|
137
219
|
|
|
@@ -191,7 +273,7 @@ class CloudRequests(CloudEvents):
|
|
|
191
273
|
) or self.no_packet_loss:
|
|
192
274
|
self.cloud.reconnect()
|
|
193
275
|
|
|
194
|
-
memory = {"rid":request_id}
|
|
276
|
+
memory = ResponseMemory(rid=request_id, packets={})#{"rid":request_id}
|
|
195
277
|
remaining_response = str(response)
|
|
196
278
|
length_limit = self.cloud.length_limit - (len(str(request_id))+6) # the subtrahend is the worst-case length of the "."+numbers after the "."
|
|
197
279
|
|
|
@@ -210,7 +292,7 @@ class CloudRequests(CloudEvents):
|
|
|
210
292
|
iteration_string = "00" + str(i)
|
|
211
293
|
|
|
212
294
|
value_to_send = f"{response_part}.{request_id}{iteration_string}1"
|
|
213
|
-
memory[i] = value_to_send
|
|
295
|
+
memory["packets"][i] = value_to_send
|
|
214
296
|
|
|
215
297
|
self._set_FROM_HOST_var(value_to_send)
|
|
216
298
|
|
|
@@ -221,26 +303,27 @@ class CloudRequests(CloudEvents):
|
|
|
221
303
|
self.packet_memory.pop(0)
|
|
222
304
|
remaining_response = ""
|
|
223
305
|
|
|
224
|
-
if self.
|
|
306
|
+
if self.hard_stopped: # stop immediately without exiting safely
|
|
225
307
|
break
|
|
226
308
|
|
|
227
|
-
def _request_packet_from_memory(self, request_id, packet_id):
|
|
309
|
+
def _request_packet_from_memory(self, request_id: str, packet_id: Union[str, int]):
|
|
228
310
|
memory = list(filter(lambda x : x["rid"] == request_id, self.packet_memory))
|
|
229
311
|
if len(memory) > 0:
|
|
230
|
-
self._packets_to_resend.append(memory[0][int(packet_id)])
|
|
312
|
+
self._packets_to_resend.append(memory[0]["packets"][int(packet_id)])
|
|
231
313
|
self.responder_event.set() # activate _responder process
|
|
232
314
|
|
|
233
315
|
# -- Register and handle incoming requests --
|
|
234
316
|
|
|
235
|
-
def on_set(self, activity):
|
|
317
|
+
def on_set(self, activity: cloud_activity.CloudActivity):
|
|
236
318
|
"""
|
|
237
319
|
This function is automatically called on cloud activites by the underlying cloud events that this CloudRequests class inherits from
|
|
238
320
|
It registers incoming cloud activity and (if request.thread is True) runs them directly or (else) adds detected request to the .received_requests list
|
|
239
321
|
"""
|
|
240
322
|
# Note for contributors: All functions called in this on_set function MUST be executed in threads because this function blocks the cloud events receiver, which is usually not a problem (because of the websocket buffer) but can cause problems in rare cases
|
|
241
|
-
|
|
323
|
+
activity_value = str(activity.value)
|
|
324
|
+
if activity.var == "TO_HOST" and "." in activity_value:
|
|
242
325
|
# Parsing the received request
|
|
243
|
-
raw_request, request_id =
|
|
326
|
+
raw_request, request_id = activity_value.split(".")
|
|
244
327
|
|
|
245
328
|
if len(request_id) == 8 and request_id[-1] == "9":
|
|
246
329
|
# A lost packet was re-requested
|
|
@@ -251,7 +334,7 @@ class CloudRequests(CloudEvents):
|
|
|
251
334
|
# => The received request has already been answered, meaning this activity has already been received
|
|
252
335
|
return
|
|
253
336
|
|
|
254
|
-
if
|
|
337
|
+
if activity_value[0] == "-":
|
|
255
338
|
# => The received request is actually part of a bigger request
|
|
256
339
|
if not request_id in self.request_parts:
|
|
257
340
|
self.request_parts[request_id] = []
|
|
@@ -273,15 +356,16 @@ class CloudRequests(CloudEvents):
|
|
|
273
356
|
# Decode request and parse arguemtns:
|
|
274
357
|
request = Encoding.decode(raw_request)
|
|
275
358
|
arguments = request.split("&")
|
|
276
|
-
request_name = arguments.pop(0)
|
|
359
|
+
request_name = arguments.pop(0)
|
|
277
360
|
|
|
278
361
|
# Check if the request is unknown:
|
|
279
362
|
if request_name not in self._requests:
|
|
280
363
|
print(
|
|
281
|
-
f"Warning: Client received an unknown request called
|
|
364
|
+
f"Warning: Client received an unknown request called {request_name!r}"
|
|
282
365
|
)
|
|
283
366
|
self.call_event("on_unknown_request", [
|
|
284
|
-
ReceivedRequest(
|
|
367
|
+
ReceivedRequest(
|
|
368
|
+
request_name=request,
|
|
285
369
|
requester=activity.user,
|
|
286
370
|
timestamp=activity.timestamp,
|
|
287
371
|
arguments=arguments,
|
|
@@ -293,6 +377,7 @@ class CloudRequests(CloudEvents):
|
|
|
293
377
|
|
|
294
378
|
received_request = ReceivedRequest(
|
|
295
379
|
request = self._requests[request_name],
|
|
380
|
+
request_name=request_name,
|
|
296
381
|
requester=activity.user,
|
|
297
382
|
timestamp=activity.timestamp,
|
|
298
383
|
arguments=arguments,
|
|
@@ -328,7 +413,7 @@ class CloudRequests(CloudEvents):
|
|
|
328
413
|
|
|
329
414
|
if use_extra_data:
|
|
330
415
|
Thread(target=self.on_reconnect).start()
|
|
331
|
-
if self.
|
|
416
|
+
if self.hard_stopped: # stop immediately without exiting safely
|
|
332
417
|
break
|
|
333
418
|
|
|
334
419
|
self.executer_event.wait(timeout = 2.5 if use_extra_data else None) # Wait for requests to be received
|
|
@@ -343,21 +428,21 @@ class CloudRequests(CloudEvents):
|
|
|
343
428
|
|
|
344
429
|
while self._packets_to_resend != []:
|
|
345
430
|
self._set_FROM_HOST_var(self._packets_to_resend.pop(0))
|
|
346
|
-
if self.
|
|
431
|
+
if self.hard_stopped: # stop immediately without exiting safely
|
|
347
432
|
break
|
|
348
433
|
|
|
349
|
-
while self.request_outputs
|
|
350
|
-
if self.respond_order ==
|
|
434
|
+
while self.request_outputs:
|
|
435
|
+
if self.respond_order == RespondOrder.FINISH:
|
|
351
436
|
output_obj = self.request_outputs.pop(0)
|
|
352
437
|
else:
|
|
353
|
-
output_obj = min(self.request_outputs, key=lambda x : x[self.respond_order])
|
|
438
|
+
output_obj = min(self.request_outputs, key=lambda x : x[self.respond_order.name.lower()])
|
|
354
439
|
self.request_outputs.remove(output_obj)
|
|
355
440
|
if output_obj["request_id"] in self.executed_requests:
|
|
356
441
|
received_request = self.executed_requests.pop(output_obj["request_id"])
|
|
357
|
-
self._parse_output(received_request, output_obj["output"], output_obj["request_id"])
|
|
442
|
+
self._parse_output(received_request.request_name, output_obj["output"], output_obj["request_id"])
|
|
358
443
|
else:
|
|
359
444
|
self._parse_output("[sent from backend]", output_obj["output"], output_obj["request_id"])
|
|
360
|
-
if self.
|
|
445
|
+
if self.hard_stopped: # stop immediately without exiting safely
|
|
361
446
|
break
|
|
362
447
|
|
|
363
448
|
def on_reconnect(self):
|
|
@@ -379,7 +464,7 @@ class CloudRequests(CloudEvents):
|
|
|
379
464
|
"""
|
|
380
465
|
Can be used inside a request to get the username that performed the request.
|
|
381
466
|
"""
|
|
382
|
-
activity = self.executed_requests[
|
|
467
|
+
activity = self.executed_requests[request_handler_thread_info.request_id].activity
|
|
383
468
|
if activity.user is None:
|
|
384
469
|
activity.load_log_data()
|
|
385
470
|
return activity.user
|
|
@@ -388,14 +473,14 @@ class CloudRequests(CloudEvents):
|
|
|
388
473
|
"""
|
|
389
474
|
Can be used inside a request to get the timestamp of when the request was received.
|
|
390
475
|
"""
|
|
391
|
-
activity = self.executed_requests[
|
|
476
|
+
activity = self.executed_requests[request_handler_thread_info.request_id].activity
|
|
392
477
|
return activity.timestamp
|
|
393
478
|
|
|
394
479
|
def get_exact_timestamp(self):
|
|
395
480
|
"""
|
|
396
481
|
Can be used inside a request to get the exact timestamp of when the request was performed.
|
|
397
482
|
"""
|
|
398
|
-
activity = self.executed_requests[
|
|
483
|
+
activity = self.executed_requests[request_handler_thread_info.request_id].activity
|
|
399
484
|
activity.load_log_data()
|
|
400
485
|
return activity.timestamp
|
|
401
486
|
|
|
@@ -434,10 +519,10 @@ class CloudRequests(CloudEvents):
|
|
|
434
519
|
"""
|
|
435
520
|
Stops the request handler and all associated threads forever. Stops running response sending processes immediately.
|
|
436
521
|
"""
|
|
437
|
-
self.
|
|
522
|
+
self.hard_stopped = True
|
|
438
523
|
self.stop()
|
|
439
524
|
time.sleep(0.5)
|
|
440
|
-
self.
|
|
525
|
+
self.hard_stopped = False
|
|
441
526
|
|
|
442
527
|
def credit_check(self):
|
|
443
528
|
try:
|
|
@@ -8,7 +8,7 @@ import time
|
|
|
8
8
|
from scratchattach.site import cloud_activity
|
|
9
9
|
from scratchattach.site.user import User
|
|
10
10
|
from ._base import BaseEventHandler
|
|
11
|
-
|
|
11
|
+
import traceback
|
|
12
12
|
class TwCloudSocket(WebSocket):
|
|
13
13
|
|
|
14
14
|
def handleMessage(self):
|
|
@@ -19,6 +19,7 @@ class TwCloudSocket(WebSocket):
|
|
|
19
19
|
return
|
|
20
20
|
|
|
21
21
|
data = json.loads(self.data)
|
|
22
|
+
print(data)
|
|
22
23
|
|
|
23
24
|
if data["method"] == "set":
|
|
24
25
|
# cloud variable set received
|
|
@@ -90,7 +91,7 @@ class TwCloudSocket(WebSocket):
|
|
|
90
91
|
print("Error:", self.address[0]+":"+str(self.address[1]), "sent a message without providing a valid method (set, handshake)")
|
|
91
92
|
|
|
92
93
|
except Exception as e:
|
|
93
|
-
print("Internal error in handleMessage:", e)
|
|
94
|
+
print("Internal error in handleMessage:", e, traceback.format_exc())
|
|
94
95
|
|
|
95
96
|
def handleConnected(self):
|
|
96
97
|
if not self.server.running:
|
|
@@ -125,7 +126,9 @@ def init_cloud_server(hostname='127.0.0.1', port=8080, *, thread=True, length_li
|
|
|
125
126
|
"""
|
|
126
127
|
class TwCloudServer(SimpleWebSocketServer, BaseEventHandler):
|
|
127
128
|
def __init__(self, hostname, *, port, websocketclass):
|
|
128
|
-
|
|
129
|
+
SimpleWebSocketServer.__init__(self, hostname, port=port, websocketclass=websocketclass)
|
|
130
|
+
BaseEventHandler.__init__(self)
|
|
131
|
+
|
|
129
132
|
self.running = False
|
|
130
133
|
self._events = {} # saves event functions called on cloud updates
|
|
131
134
|
|
|
@@ -105,7 +105,6 @@ class CloudStorage(CloudRequests):
|
|
|
105
105
|
return f"Error: Key {key} doesn't exist in database {db_name}"
|
|
106
106
|
|
|
107
107
|
def set(self, db_name, key, value):
|
|
108
|
-
print(db_name, key, value, self._databases)
|
|
109
108
|
return self.get_database(db_name).set(key, value)
|
|
110
109
|
|
|
111
110
|
def keys(self, db_name) -> list:
|
|
@@ -133,4 +132,4 @@ class CloudStorage(CloudRequests):
|
|
|
133
132
|
Saves the data in the JSON files for all databases in self._databases
|
|
134
133
|
"""
|
|
135
134
|
for dbname in self._databases:
|
|
136
|
-
self._databases[dbname].save_to_json()
|
|
135
|
+
self._databases[dbname].save_to_json()
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""FilterBot class"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from .message_events import MessageEvents
|
|
4
|
+
import time
|
|
5
|
+
from collections import deque
|
|
6
|
+
|
|
7
|
+
class HardFilter:
|
|
8
|
+
|
|
9
|
+
def __init__(self, filter_name="UntitledFilter", *, equals=None, contains=None, author_name=None, project_id=None, profile=None, case_sensitive=False):
|
|
10
|
+
self.equals = equals
|
|
11
|
+
self.contains = contains
|
|
12
|
+
self.author_name = author_name
|
|
13
|
+
self.project_id = project_id
|
|
14
|
+
self.profile = profile
|
|
15
|
+
self.case_sensitive = case_sensitive
|
|
16
|
+
self.filter_name = filter_name
|
|
17
|
+
|
|
18
|
+
def apply(self, content, author_name, source_id):
|
|
19
|
+
text_to_check = content if self.case_sensitive else content.lower()
|
|
20
|
+
if self.equals is not None:
|
|
21
|
+
comparison_equals = self.equals if self.case_sensitive else self.equals.lower()
|
|
22
|
+
if text_to_check == comparison_equals:
|
|
23
|
+
return True
|
|
24
|
+
if self.contains is not None:
|
|
25
|
+
comparison_contains = self.contains if self.case_sensitive else self.contains.lower()
|
|
26
|
+
if comparison_contains in text_to_check:
|
|
27
|
+
return True
|
|
28
|
+
if self.author_name is not None and self.author_name == author_name:
|
|
29
|
+
return True
|
|
30
|
+
if (self.project_id is not None and self.project_id == source_id) or \
|
|
31
|
+
(self.profile is not None and self.profile == source_id):
|
|
32
|
+
return True
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
class SoftFilter(HardFilter):
|
|
36
|
+
def __init__(self, score:float, filter_name="UntitledFilter", *, equals=None, contains=None, author_name=None, project_id=None, profile=None, case_sensitive=False):
|
|
37
|
+
self.score = score
|
|
38
|
+
super().__init__(filter_name, equals=equals, contains=contains, author_name=author_name, project_id=project_id, profile=profile, case_sensitive=case_sensitive)
|
|
39
|
+
|
|
40
|
+
class SpamFilter(HardFilter):
|
|
41
|
+
def __init__(self, filter_name="UntitledFilter", *, equals=None, contains=None, author_name=None, project_id=None, profile=None, case_sensitive=False):
|
|
42
|
+
super().__init__(filter_name, equals=equals, contains=contains, author_name=author_name, project_id=project_id, profile=profile, case_sensitive=case_sensitive)
|
|
43
|
+
self.memory = deque()
|
|
44
|
+
self.retention_period = 300
|
|
45
|
+
|
|
46
|
+
def apply(self, content, author_name, source_id):
|
|
47
|
+
if not super().apply(content, author_name, source_id):
|
|
48
|
+
return False
|
|
49
|
+
current_time = time.time()
|
|
50
|
+
|
|
51
|
+
# Prune old entries from memory
|
|
52
|
+
while self.memory and self.memory[-1]["time"] < current_time - self.retention_period:
|
|
53
|
+
self.memory.pop()
|
|
54
|
+
|
|
55
|
+
content_lower = content.lower()
|
|
56
|
+
# Check for duplicates
|
|
57
|
+
for comment in self.memory:
|
|
58
|
+
if comment["content"].lower() == content_lower:
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
# Add new comment to memory
|
|
62
|
+
self.memory.appendleft({"content": content, "time": current_time})
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
class Filterbot(MessageEvents):
|
|
66
|
+
|
|
67
|
+
# The Filterbot class is built upon MessageEvents, similar to how CloudEvents is built upon CloudEvents
|
|
68
|
+
|
|
69
|
+
def __init__(self, user, *, log_deletions=True):
|
|
70
|
+
super().__init__(user)
|
|
71
|
+
self.hard_filters = []
|
|
72
|
+
self.soft_filters = []
|
|
73
|
+
self.spam_filters = []
|
|
74
|
+
self.log_deletions = log_deletions
|
|
75
|
+
self.event(self.on_message, thread=False)
|
|
76
|
+
self.update_interval = 2
|
|
77
|
+
|
|
78
|
+
def add_filter(self, filter_obj):
|
|
79
|
+
if isinstance(filter_obj, SpamFilter):
|
|
80
|
+
self.spam_filters.append(filter_obj)
|
|
81
|
+
elif isinstance(filter_obj, SoftFilter):
|
|
82
|
+
self.soft_filters.append(filter_obj)
|
|
83
|
+
elif isinstance(filter_obj, HardFilter):
|
|
84
|
+
self.hard_filters.append(filter_obj)
|
|
85
|
+
|
|
86
|
+
def add_f4f_filter(self):
|
|
87
|
+
self.add_filter(HardFilter("(f4f_filter) 'f4f'", contains="f4f"))
|
|
88
|
+
self.add_filter(HardFilter("(f4f_filter) 'follow me'", contains="follow me"))
|
|
89
|
+
self.add_filter(HardFilter("(f4f_filter) 'follow @'", contains="follow @"))
|
|
90
|
+
self.add_filter(HardFilter("(f4f_filter) f 4 f'", contains="f 4 f"))
|
|
91
|
+
self.add_filter(HardFilter("(f4f_filter) 'follow for'", contains="follow for"))
|
|
92
|
+
|
|
93
|
+
def add_ads_filter(self):
|
|
94
|
+
self.add_filter(SoftFilter(1, "(ads_filter) links", contains="scratch.mit.edu/projects/"))
|
|
95
|
+
self.add_filter(SoftFilter(-1, "(ads_filter) feedback", contains="feedback"))
|
|
96
|
+
self.add_filter(HardFilter("(ads_filter) 'check out my'", contains="check out my"))
|
|
97
|
+
self.add_filter(HardFilter("(ads_filter) 'play my'", contains="play my"))
|
|
98
|
+
self.add_filter(SoftFilter(1, "(ads_filter) 'advertis'", contains="advertis"))
|
|
99
|
+
|
|
100
|
+
def add_spam_filter(self):
|
|
101
|
+
self.add_filter(SpamFilter("(spam_filter)", contains=""))
|
|
102
|
+
|
|
103
|
+
def add_genalpha_nonsense_filter(self):
|
|
104
|
+
self.add_filter(HardFilter("(genalpha_nonsene_filter) 'skibidi'", contains="skibidi"))
|
|
105
|
+
self.add_filter(HardFilter("[genalpha_nonsene_filter) 'rizzler'", contains="rizzler"))
|
|
106
|
+
self.add_filter(HardFilter("(genalpha_nonsene_filter) 'fanum tax'", contains="fanum tax"))
|
|
107
|
+
|
|
108
|
+
def on_message(self, message):
|
|
109
|
+
if message.type != "addcomment":
|
|
110
|
+
return
|
|
111
|
+
source_id = None
|
|
112
|
+
content = message.comment_fragment
|
|
113
|
+
if message.comment_type == 0: # project comment
|
|
114
|
+
source_id = message.comment_obj_id
|
|
115
|
+
if self.user._session.connect_project(message.comment_obj_id).author_name != self.user.username:
|
|
116
|
+
return # no permission to delete comments that aren't on our own project
|
|
117
|
+
elif message.comment_type == 1: # profile comment
|
|
118
|
+
source_id = message.comment_obj_title
|
|
119
|
+
if source_id != self.user.username:
|
|
120
|
+
return # no permission to delete messages that are not on our profile
|
|
121
|
+
elif message.comment_type == 2: # studio comment
|
|
122
|
+
return # studio comments aren't handled
|
|
123
|
+
else:
|
|
124
|
+
return
|
|
125
|
+
delete = False
|
|
126
|
+
reason = ""
|
|
127
|
+
|
|
128
|
+
# Apply hard filters
|
|
129
|
+
for hard_filter in self.hard_filters:
|
|
130
|
+
if hard_filter.apply(content, message.actor_username, source_id):
|
|
131
|
+
delete = True
|
|
132
|
+
reason = f"hard filter: {hard_filter.filter_name}"
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
# Apply spam filters
|
|
136
|
+
if not delete:
|
|
137
|
+
for spam_filter in self.spam_filters:
|
|
138
|
+
if spam_filter.apply(content, message.actor_username, source_id):
|
|
139
|
+
delete = True
|
|
140
|
+
reason = f"spam filter: {spam_filter.filter_name}"
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
# Apply soft filters
|
|
144
|
+
if not delete:
|
|
145
|
+
score = 0
|
|
146
|
+
violated_filters = []
|
|
147
|
+
for soft_filter in self.soft_filters:
|
|
148
|
+
if soft_filter.apply(content, message.actor_username, source_id):
|
|
149
|
+
score += soft_filter.score
|
|
150
|
+
violated_filters.append(soft_filter.filter_name)
|
|
151
|
+
if score >= 1:
|
|
152
|
+
delete = True
|
|
153
|
+
reason = f"too many soft filters: {violated_filters}"
|
|
154
|
+
if delete:
|
|
155
|
+
if self.log_deletions:
|
|
156
|
+
print(f"DETECTED: #{message.comment_id} violates {reason}")
|
|
157
|
+
try:
|
|
158
|
+
resp = message.target().delete()
|
|
159
|
+
if self.log_deletions:
|
|
160
|
+
print(f"DELETED: #{message.comment_id} by {message.actor_username!r}: '{content}' with message {resp.content!r} & headers {resp.headers!r}")
|
|
161
|
+
except Exception as e:
|
|
162
|
+
if self.log_deletions:
|
|
163
|
+
print(f"DELETION FAILED: #{message.comment_id} by {message.actor_username!r}: '{content}'; exception: {e}")
|