scratchattach 2.1.9__py3-none-any.whl → 2.1.10a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scratchattach/__init__.py +28 -25
- scratchattach/cloud/__init__.py +2 -0
- scratchattach/cloud/_base.py +454 -282
- scratchattach/cloud/cloud.py +171 -168
- scratchattach/editor/__init__.py +21 -0
- scratchattach/editor/asset.py +199 -0
- scratchattach/editor/backpack_json.py +117 -0
- scratchattach/editor/base.py +142 -0
- scratchattach/editor/block.py +507 -0
- scratchattach/editor/blockshape.py +353 -0
- scratchattach/editor/build_defaulting.py +47 -0
- scratchattach/editor/comment.py +74 -0
- scratchattach/editor/commons.py +243 -0
- scratchattach/editor/extension.py +43 -0
- scratchattach/editor/field.py +90 -0
- scratchattach/editor/inputs.py +132 -0
- scratchattach/editor/meta.py +106 -0
- scratchattach/editor/monitor.py +175 -0
- scratchattach/editor/mutation.py +317 -0
- scratchattach/editor/pallete.py +91 -0
- scratchattach/editor/prim.py +170 -0
- scratchattach/editor/project.py +273 -0
- scratchattach/editor/sbuild.py +2837 -0
- scratchattach/editor/sprite.py +586 -0
- scratchattach/editor/twconfig.py +113 -0
- scratchattach/editor/vlb.py +134 -0
- scratchattach/eventhandlers/_base.py +99 -92
- scratchattach/eventhandlers/cloud_events.py +110 -103
- scratchattach/eventhandlers/cloud_recorder.py +26 -21
- scratchattach/eventhandlers/cloud_requests.py +460 -452
- scratchattach/eventhandlers/cloud_server.py +246 -244
- scratchattach/eventhandlers/cloud_storage.py +135 -134
- scratchattach/eventhandlers/combine.py +29 -27
- scratchattach/eventhandlers/filterbot.py +160 -159
- scratchattach/eventhandlers/message_events.py +41 -40
- scratchattach/other/other_apis.py +284 -212
- scratchattach/other/project_json_capabilities.py +475 -546
- scratchattach/site/_base.py +64 -46
- scratchattach/site/activity.py +414 -122
- scratchattach/site/backpack_asset.py +118 -84
- scratchattach/site/classroom.py +430 -142
- scratchattach/site/cloud_activity.py +107 -103
- scratchattach/site/comment.py +220 -190
- scratchattach/site/forum.py +400 -399
- scratchattach/site/project.py +806 -787
- scratchattach/site/session.py +1134 -867
- scratchattach/site/studio.py +611 -609
- scratchattach/site/user.py +835 -837
- scratchattach/utils/commons.py +243 -148
- scratchattach/utils/encoder.py +157 -156
- scratchattach/utils/enums.py +197 -190
- scratchattach/utils/exceptions.py +233 -206
- scratchattach/utils/requests.py +67 -59
- {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a0.dist-info}/LICENSE +21 -21
- {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a0.dist-info}/METADATA +154 -146
- scratchattach-2.1.10a0.dist-info/RECORD +62 -0
- {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a0.dist-info}/WHEEL +1 -1
- scratchattach-2.1.9.dist-info/RECORD +0 -40
- {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a0.dist-info}/top_level.txt +0 -0
|
@@ -1,244 +1,246 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
from
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
self.server.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
_a.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
self.
|
|
76
|
-
|
|
77
|
-
self.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
self.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
self.server.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
self.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
#
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
self.
|
|
137
|
-
self.
|
|
138
|
-
self.
|
|
139
|
-
self.
|
|
140
|
-
self.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
self.tw_variables
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
client
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket
|
|
4
|
+
from threading import Thread
|
|
5
|
+
from ..utils import exceptions
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from ..site import cloud_activity
|
|
9
|
+
from ..site.user import User
|
|
10
|
+
from ._base import BaseEventHandler
|
|
11
|
+
|
|
12
|
+
class TwCloudSocket(WebSocket):
|
|
13
|
+
|
|
14
|
+
def handleMessage(self):
|
|
15
|
+
if not self.server.running:
|
|
16
|
+
return
|
|
17
|
+
try:
|
|
18
|
+
if self.server.check_for_ip_ban(self):
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
data = json.loads(self.data)
|
|
22
|
+
|
|
23
|
+
if data["method"] == "set":
|
|
24
|
+
# cloud variable set received
|
|
25
|
+
# check if project_id is in whitelisted projects (if there's a list of whitelisted projects)
|
|
26
|
+
if self.server.whitelisted_projects is not None:
|
|
27
|
+
if data["project_id"] not in self.server.whitelisted_projects:
|
|
28
|
+
self.close(4002)
|
|
29
|
+
if self.server.log_var_sets:
|
|
30
|
+
print(self.address[0]+":"+str(self.address[1]), "tried to set a var on non-whitelisted project and was disconnected, project:", data["project_id"], "user:",data["user"])
|
|
31
|
+
return
|
|
32
|
+
# check if value is valid
|
|
33
|
+
if not self.server._check_value(data["value"]):
|
|
34
|
+
if self.server.log_var_sets:
|
|
35
|
+
print(self.address[0]+":"+str(self.address[1]), "sent an invalid var value")
|
|
36
|
+
return
|
|
37
|
+
# perform cloud var and forward to other players
|
|
38
|
+
if self.server.log_var_sets:
|
|
39
|
+
print(self.address[0]+":"+str(self.address[1]), f"set {data['name']} to {data['value']}, project:", str(data["project_id"]), "user:",data["user"])
|
|
40
|
+
self.server.set_var(data["project_id"], data["name"], data["value"], user=data["user"], skip_forward=self)
|
|
41
|
+
send_to_clients = {
|
|
42
|
+
"method" : "set", "user" : data["user"], "project_id" : data["project_id"], "name" : data["name"],
|
|
43
|
+
"value" : data["value"], "timestamp" : round(time.time() * 1000), "server" : "scratchattach/2.0.0",
|
|
44
|
+
}
|
|
45
|
+
# raise event
|
|
46
|
+
_a = cloud_activity.CloudActivity(timestamp=time.time()*1000)
|
|
47
|
+
data["name"] = data["name"].replace("☁ ", "")
|
|
48
|
+
_a._update_from_dict(send_to_clients)
|
|
49
|
+
self.server.call_event("on_set", [_a, self])
|
|
50
|
+
|
|
51
|
+
elif data["method"] == "handshake":
|
|
52
|
+
data = json.loads(self.data)
|
|
53
|
+
# check if handshake is valid
|
|
54
|
+
if not "user" in data:
|
|
55
|
+
print(self.address[0]+":"+str(self.address[1]), "tried to handshake without providing a username")
|
|
56
|
+
self.close(4002)
|
|
57
|
+
return
|
|
58
|
+
if not "project_id" in data:
|
|
59
|
+
print(self.address[0]+":"+str(self.address[1]), "tried to handshake without providing a project_id")
|
|
60
|
+
self.close(4002)
|
|
61
|
+
return
|
|
62
|
+
# check if project_id is in username is allowed
|
|
63
|
+
if self.server.allow_nonscratch_names is False:
|
|
64
|
+
if not User(username=data["user"]).does_exist():
|
|
65
|
+
print(self.address[0]+":"+str(self.address[1]), "tried to handshake using a username not existing on Scratch, project:", data["project_id"], "user:",data["user"])
|
|
66
|
+
self.close(4002)
|
|
67
|
+
return
|
|
68
|
+
# check if project_id is in whitelisted projects (if there's a list of whitelisted projects)
|
|
69
|
+
if self.server.whitelisted_projects is not None:
|
|
70
|
+
if str(data["project_id"]) not in self.server.whitelisted_projects:
|
|
71
|
+
self.close(4002)
|
|
72
|
+
print(self.address[0]+":"+str(self.address[1]), "tried to handshake on a non-whitelisted project:", data["project_id"], "user:",data["user"])
|
|
73
|
+
return
|
|
74
|
+
# register handshake in users list (save username and project_id)
|
|
75
|
+
print(self.address[0]+":"+str(self.address[1]), "handshaked, project:", data["project_id"], "user:",data["user"])
|
|
76
|
+
self.server.tw_clients[self.address]["username"] = data["user"]
|
|
77
|
+
self.server.tw_clients[self.address]["project_id"] = data["project_id"]
|
|
78
|
+
# send current cloud variable values to the user who handshaked
|
|
79
|
+
self.sendMessage("\n".join([
|
|
80
|
+
json.dumps({
|
|
81
|
+
"method" : "set", "project_id" : data["project_id"], "name" : "☁ "+varname,
|
|
82
|
+
"value" : self.server.tw_variables[str(data["project_id"])][varname], "server" : "scratchattach/2.0.0",
|
|
83
|
+
}) for varname in self.server.get_project_vars(str(data["project_id"]))])
|
|
84
|
+
)
|
|
85
|
+
self.sendMessage("This server uses @TimMcCool's scratchattach 2.0.0")
|
|
86
|
+
# raise event
|
|
87
|
+
self.server.call_event("on_handshake", [data["user"], data["project_id"], self])
|
|
88
|
+
|
|
89
|
+
else:
|
|
90
|
+
print("Error:", self.address[0]+":"+str(self.address[1]), "sent a message without providing a valid method (set, handshake)")
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print("Internal error in handleMessage:", e)
|
|
94
|
+
|
|
95
|
+
def handleConnected(self):
|
|
96
|
+
if not self.server.running:
|
|
97
|
+
return
|
|
98
|
+
try:
|
|
99
|
+
if self.server.check_for_ip_ban(self):
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
print(self.address[0]+":"+str(self.address[1]), "connected")
|
|
103
|
+
self.server.tw_clients[self.address] = {"client":self, "username":None, "project_id":None}
|
|
104
|
+
# raise event
|
|
105
|
+
self.server.call_event("on_connect", [self])
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print("Internal error in handleConntected:", e)
|
|
108
|
+
|
|
109
|
+
def handleClose(self):
|
|
110
|
+
if not self.server.running:
|
|
111
|
+
return
|
|
112
|
+
try:
|
|
113
|
+
if self.address in self.server.tw_clients:
|
|
114
|
+
# raise event
|
|
115
|
+
self.server.call_event("on_disconnect", [self.server.tw_clients[self.address]["username"], self.server.tw_clients[self.address]["project_id"], self])
|
|
116
|
+
print(self.address[0]+":"+str(self.address[1]), "disconnected")
|
|
117
|
+
except Exception as e:
|
|
118
|
+
print("Internal error in handleClose:", e)
|
|
119
|
+
|
|
120
|
+
def init_cloud_server(hostname='127.0.0.1', port=8080, *, thread=True, length_limit=None, allow_non_numeric=True, whitelisted_projects=None, allow_nonscratch_names=True, blocked_ips=[], sync_players=True, log_var_sets=True):
|
|
121
|
+
"""
|
|
122
|
+
Inits a websocket server which can be used with TurboWarp's ?cloud_host URL parameter.
|
|
123
|
+
|
|
124
|
+
Prints out the websocket address in the console.
|
|
125
|
+
"""
|
|
126
|
+
class TwCloudServer(SimpleWebSocketServer, BaseEventHandler):
|
|
127
|
+
def __init__(self, hostname, *, port, websocketclass):
|
|
128
|
+
super().__init__(hostname, port=port, websocketclass=websocketclass)
|
|
129
|
+
self.running = False
|
|
130
|
+
self._events = {} # saves event functions called on cloud updates
|
|
131
|
+
|
|
132
|
+
self.tw_clients = {} # saves connected clients
|
|
133
|
+
self.tw_variables = {} # holds cloud variable states
|
|
134
|
+
|
|
135
|
+
# server config
|
|
136
|
+
self.allow_non_numeric = allow_non_numeric
|
|
137
|
+
self.whitelisted_projects = whitelisted_projects
|
|
138
|
+
self.length_limit = length_limit
|
|
139
|
+
self.allow_nonscratch_names = allow_nonscratch_names
|
|
140
|
+
self.blocked_ips = blocked_ips
|
|
141
|
+
self.sync_players = sync_players
|
|
142
|
+
self.log_var_sets = log_var_sets
|
|
143
|
+
|
|
144
|
+
def check_for_ip_ban(self, client):
|
|
145
|
+
if client.address[0] in self.blocked_ips or client.address[0]+":"+str(client.address[1]) in self.blocked_ips or client.address in self.blocked_ips:
|
|
146
|
+
client.sendMessage("You have been banned from this server")
|
|
147
|
+
client.close(4002)
|
|
148
|
+
print(client.address[0]+":"+str(client.address[1]), "(IP-banned) was disconnected")
|
|
149
|
+
return True
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
def active_projects(self):
|
|
153
|
+
only_active = {}
|
|
154
|
+
for project_id in self.tw_variables:
|
|
155
|
+
if self.active_user_ips(project_id) != []:
|
|
156
|
+
only_active[project_id] = self.tw_variables[project_id]
|
|
157
|
+
return only_active
|
|
158
|
+
|
|
159
|
+
def active_user_names(self, project_id):
|
|
160
|
+
return [self.tw_clients[user]["username"] for user in self.active_user_ips(project_id)]
|
|
161
|
+
|
|
162
|
+
def active_user_ips(self, project_id):
|
|
163
|
+
return list(filter(lambda user : str(self.tw_clients[user]["project_id"]) == str(project_id), self.tw_clients))
|
|
164
|
+
|
|
165
|
+
def get_global_vars(self):
|
|
166
|
+
return self.tw_variables
|
|
167
|
+
|
|
168
|
+
def get_project_vars(self, project_id):
|
|
169
|
+
project_id = str(project_id)
|
|
170
|
+
if project_id in self.tw_variables:
|
|
171
|
+
return self.tw_variables[project_id]
|
|
172
|
+
else: return {}
|
|
173
|
+
|
|
174
|
+
def get_var(self, project_id, var_name):
|
|
175
|
+
project_id = str(project_id)
|
|
176
|
+
var_name = var_name.replace("☁ ", "")
|
|
177
|
+
if project_id in self.tw_variables:
|
|
178
|
+
if var_name in self.tw_variables[project_id]:
|
|
179
|
+
return self.tw_variables[project_id][var_name]
|
|
180
|
+
else: return None
|
|
181
|
+
else: return None
|
|
182
|
+
|
|
183
|
+
def set_global_vars(self, data):
|
|
184
|
+
for project_id in data:
|
|
185
|
+
self.set_project_vars(project_id, data[project_id])
|
|
186
|
+
|
|
187
|
+
def set_project_vars(self, project_id, data, *, user="@server"):
|
|
188
|
+
project_id = str(project_id)
|
|
189
|
+
self.tw_variables[project_id] = data
|
|
190
|
+
for client in [self.tw_clients[ip]["client"] for ip in self.active_user_ips(project_id)]:
|
|
191
|
+
client.sendMessage("\n".join([
|
|
192
|
+
json.dumps({
|
|
193
|
+
"method" : "set", "project_id" : project_id, "name" : "☁ "+varname,
|
|
194
|
+
"value" : data[varname], "server" : "scratchattach/2.0.0", "timestamp" : time.time()*1000, "user" : user
|
|
195
|
+
}) for varname in data])
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def set_var(self, project_id, var_name, value, *, user="@server", skip_forward=None):
|
|
199
|
+
var_name = var_name.replace("☁ ", "")
|
|
200
|
+
project_id = str(project_id)
|
|
201
|
+
if project_id not in self.tw_variables:
|
|
202
|
+
self.tw_variables[project_id] = {}
|
|
203
|
+
self.tw_variables[project_id][var_name] = value
|
|
204
|
+
|
|
205
|
+
if self.sync_players is True:
|
|
206
|
+
for client in [self.tw_clients[ip]["client"] for ip in self.active_user_ips(project_id)]:
|
|
207
|
+
if client == skip_forward:
|
|
208
|
+
continue
|
|
209
|
+
client.sendMessage(
|
|
210
|
+
json.dumps({
|
|
211
|
+
"method" : "set", "project_id" : project_id, "name" : "☁ "+var_name,
|
|
212
|
+
"value" : value, "timestamp" : time.time()*1000, "user" : user
|
|
213
|
+
})
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def _check_value(self, value):
|
|
217
|
+
# Checks if a received cloud value satisfies the server's constraints
|
|
218
|
+
if self.length_limit is not None:
|
|
219
|
+
if len(str(value)) > self.length_limit:
|
|
220
|
+
return False
|
|
221
|
+
if self.allow_non_numeric is False:
|
|
222
|
+
x = value.replace(".", "")
|
|
223
|
+
x = x.replace("-", "")
|
|
224
|
+
if not (x.isnumeric() or x == ""):
|
|
225
|
+
return False
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
def _updater(self):
|
|
229
|
+
try:
|
|
230
|
+
# Function called when .start() is executed (.start is inherited from BaseEventHandler)
|
|
231
|
+
print(f"Serving websocket server: ws://{hostname}:{port}")
|
|
232
|
+
self.serveforever()
|
|
233
|
+
except Exception as e:
|
|
234
|
+
raise exceptions.WebsocketServerError(str(e))
|
|
235
|
+
|
|
236
|
+
def pause(self):
|
|
237
|
+
self.running = False
|
|
238
|
+
|
|
239
|
+
def resume(self):
|
|
240
|
+
self.running = True
|
|
241
|
+
|
|
242
|
+
def stop(self):
|
|
243
|
+
self.running = False
|
|
244
|
+
self.close()
|
|
245
|
+
|
|
246
|
+
return TwCloudServer(hostname, port=port, websocketclass=TwCloudSocket)
|