scratchattach 3.0.0b0__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.
Files changed (83) hide show
  1. cli/__about__.py +1 -0
  2. cli/__init__.py +26 -0
  3. cli/cmd/__init__.py +4 -0
  4. cli/cmd/group.py +127 -0
  5. cli/cmd/login.py +60 -0
  6. cli/cmd/profile.py +7 -0
  7. cli/cmd/sessions.py +5 -0
  8. cli/context.py +142 -0
  9. cli/db.py +66 -0
  10. cli/namespace.py +14 -0
  11. cloud/__init__.py +2 -0
  12. cloud/_base.py +483 -0
  13. cloud/cloud.py +183 -0
  14. editor/__init__.py +22 -0
  15. editor/asset.py +265 -0
  16. editor/backpack_json.py +115 -0
  17. editor/base.py +191 -0
  18. editor/block.py +584 -0
  19. editor/blockshape.py +357 -0
  20. editor/build_defaulting.py +51 -0
  21. editor/code_translation/__init__.py +0 -0
  22. editor/code_translation/parse.py +177 -0
  23. editor/comment.py +80 -0
  24. editor/commons.py +145 -0
  25. editor/extension.py +50 -0
  26. editor/field.py +99 -0
  27. editor/inputs.py +138 -0
  28. editor/meta.py +117 -0
  29. editor/monitor.py +185 -0
  30. editor/mutation.py +381 -0
  31. editor/pallete.py +88 -0
  32. editor/prim.py +174 -0
  33. editor/project.py +381 -0
  34. editor/sprite.py +609 -0
  35. editor/twconfig.py +114 -0
  36. editor/vlb.py +134 -0
  37. eventhandlers/__init__.py +0 -0
  38. eventhandlers/_base.py +101 -0
  39. eventhandlers/cloud_events.py +130 -0
  40. eventhandlers/cloud_recorder.py +26 -0
  41. eventhandlers/cloud_requests.py +544 -0
  42. eventhandlers/cloud_server.py +249 -0
  43. eventhandlers/cloud_storage.py +135 -0
  44. eventhandlers/combine.py +30 -0
  45. eventhandlers/filterbot.py +163 -0
  46. eventhandlers/message_events.py +42 -0
  47. other/__init__.py +0 -0
  48. other/other_apis.py +598 -0
  49. other/project_json_capabilities.py +475 -0
  50. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/METADATA +1 -1
  51. scratchattach-3.0.0b1.dist-info/RECORD +79 -0
  52. scratchattach-3.0.0b1.dist-info/top_level.txt +7 -0
  53. site/__init__.py +0 -0
  54. site/_base.py +93 -0
  55. site/activity.py +426 -0
  56. site/alert.py +226 -0
  57. site/backpack_asset.py +119 -0
  58. site/browser_cookie3_stub.py +17 -0
  59. site/browser_cookies.py +61 -0
  60. site/classroom.py +454 -0
  61. site/cloud_activity.py +121 -0
  62. site/comment.py +228 -0
  63. site/forum.py +436 -0
  64. site/placeholder.py +132 -0
  65. site/project.py +932 -0
  66. site/session.py +1323 -0
  67. site/studio.py +704 -0
  68. site/typed_dicts.py +151 -0
  69. site/user.py +1252 -0
  70. utils/__init__.py +0 -0
  71. utils/commons.py +263 -0
  72. utils/encoder.py +161 -0
  73. utils/enums.py +237 -0
  74. utils/exceptions.py +277 -0
  75. utils/optional_async.py +154 -0
  76. utils/requests.py +306 -0
  77. scratchattach/__init__.py +0 -37
  78. scratchattach/__main__.py +0 -93
  79. scratchattach-3.0.0b0.dist-info/RECORD +0 -8
  80. scratchattach-3.0.0b0.dist-info/top_level.txt +0 -1
  81. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/WHEEL +0 -0
  82. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/entry_points.txt +0 -0
  83. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,249 @@
1
+ from __future__ import annotations
2
+
3
+ from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket
4
+ from threading import Thread
5
+ from scratchattach.utils import exceptions
6
+ import json
7
+ import time
8
+ from scratchattach.site import cloud_activity
9
+ from scratchattach.site.user import User
10
+ from ._base import BaseEventHandler
11
+ import traceback
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
+ print(data)
23
+
24
+ if data["method"] == "set":
25
+ # cloud variable set received
26
+ # check if project_id is in whitelisted projects (if there's a list of whitelisted projects)
27
+ if self.server.whitelisted_projects is not None:
28
+ if data["project_id"] not in self.server.whitelisted_projects:
29
+ self.close(4002)
30
+ if self.server.log_var_sets:
31
+ 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"])
32
+ return
33
+ # check if value is valid
34
+ if not self.server._check_value(data["value"]):
35
+ if self.server.log_var_sets:
36
+ print(self.address[0]+":"+str(self.address[1]), "sent an invalid var value")
37
+ return
38
+ # perform cloud var and forward to other players
39
+ if self.server.log_var_sets:
40
+ print(self.address[0]+":"+str(self.address[1]), f"set {data['name']} to {data['value']}, project:", str(data["project_id"]), "user:",data["user"])
41
+ self.server.set_var(data["project_id"], data["name"], data["value"], user=data["user"], skip_forward=self)
42
+ send_to_clients = {
43
+ "method" : "set", "user" : data["user"], "project_id" : data["project_id"], "name" : data["name"],
44
+ "value" : data["value"], "timestamp" : round(time.time() * 1000), "server" : "scratchattach/2.0.0",
45
+ }
46
+ # raise event
47
+ _a = cloud_activity.CloudActivity(timestamp=time.time()*1000)
48
+ data["name"] = data["name"].replace("☁ ", "")
49
+ _a._update_from_dict(send_to_clients)
50
+ self.server.call_event("on_set", [_a, self])
51
+
52
+ elif data["method"] == "handshake":
53
+ data = json.loads(self.data)
54
+ # check if handshake is valid
55
+ if not "user" in data:
56
+ print(self.address[0]+":"+str(self.address[1]), "tried to handshake without providing a username")
57
+ self.close(4002)
58
+ return
59
+ if not "project_id" in data:
60
+ print(self.address[0]+":"+str(self.address[1]), "tried to handshake without providing a project_id")
61
+ self.close(4002)
62
+ return
63
+ # check if project_id is in username is allowed
64
+ if self.server.allow_nonscratch_names is False:
65
+ if not User(username=data["user"]).does_exist():
66
+ 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"])
67
+ self.close(4002)
68
+ return
69
+ # check if project_id is in whitelisted projects (if there's a list of whitelisted projects)
70
+ if self.server.whitelisted_projects is not None:
71
+ if str(data["project_id"]) not in self.server.whitelisted_projects:
72
+ self.close(4002)
73
+ print(self.address[0]+":"+str(self.address[1]), "tried to handshake on a non-whitelisted project:", data["project_id"], "user:",data["user"])
74
+ return
75
+ # register handshake in users list (save username and project_id)
76
+ print(self.address[0]+":"+str(self.address[1]), "handshaked, project:", data["project_id"], "user:",data["user"])
77
+ self.server.tw_clients[self.address]["username"] = data["user"]
78
+ self.server.tw_clients[self.address]["project_id"] = data["project_id"]
79
+ # send current cloud variable values to the user who handshaked
80
+ self.sendMessage("\n".join([
81
+ json.dumps({
82
+ "method" : "set", "project_id" : data["project_id"], "name" : "☁ "+varname,
83
+ "value" : self.server.tw_variables[str(data["project_id"])][varname], "server" : "scratchattach/2.0.0",
84
+ }) for varname in self.server.get_project_vars(str(data["project_id"]))])
85
+ )
86
+ self.sendMessage("This server uses @TimMcCool's scratchattach 2.0.0")
87
+ # raise event
88
+ self.server.call_event("on_handshake", [data["user"], data["project_id"], self])
89
+
90
+ else:
91
+ print("Error:", self.address[0]+":"+str(self.address[1]), "sent a message without providing a valid method (set, handshake)")
92
+
93
+ except Exception as e:
94
+ print("Internal error in handleMessage:", e, traceback.format_exc())
95
+
96
+ def handleConnected(self):
97
+ if not self.server.running:
98
+ return
99
+ try:
100
+ if self.server.check_for_ip_ban(self):
101
+ return
102
+
103
+ print(self.address[0]+":"+str(self.address[1]), "connected")
104
+ self.server.tw_clients[self.address] = {"client":self, "username":None, "project_id":None}
105
+ # raise event
106
+ self.server.call_event("on_connect", [self])
107
+ except Exception as e:
108
+ print("Internal error in handleConntected:", e)
109
+
110
+ def handleClose(self):
111
+ if not self.server.running:
112
+ return
113
+ try:
114
+ if self.address in self.server.tw_clients:
115
+ # raise event
116
+ self.server.call_event("on_disconnect", [self.server.tw_clients[self.address]["username"], self.server.tw_clients[self.address]["project_id"], self])
117
+ print(self.address[0]+":"+str(self.address[1]), "disconnected")
118
+ except Exception as e:
119
+ print("Internal error in handleClose:", e)
120
+
121
+ 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):
122
+ """
123
+ Inits a websocket server which can be used with TurboWarp's ?cloud_host URL parameter.
124
+
125
+ Prints out the websocket address in the console.
126
+ """
127
+ class TwCloudServer(SimpleWebSocketServer, BaseEventHandler):
128
+ def __init__(self, hostname, *, port, websocketclass):
129
+ SimpleWebSocketServer.__init__(self, hostname, port=port, websocketclass=websocketclass)
130
+ BaseEventHandler.__init__(self)
131
+
132
+ self.running = False
133
+ self._events = {} # saves event functions called on cloud updates
134
+
135
+ self.tw_clients = {} # saves connected clients
136
+ self.tw_variables = {} # holds cloud variable states
137
+
138
+ # server config
139
+ self.allow_non_numeric = allow_non_numeric
140
+ self.whitelisted_projects = whitelisted_projects
141
+ self.length_limit = length_limit
142
+ self.allow_nonscratch_names = allow_nonscratch_names
143
+ self.blocked_ips = blocked_ips
144
+ self.sync_players = sync_players
145
+ self.log_var_sets = log_var_sets
146
+
147
+ def check_for_ip_ban(self, client):
148
+ 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:
149
+ client.sendMessage("You have been banned from this server")
150
+ client.close(4002)
151
+ print(client.address[0]+":"+str(client.address[1]), "(IP-banned) was disconnected")
152
+ return True
153
+ return False
154
+
155
+ def active_projects(self):
156
+ only_active = {}
157
+ for project_id in self.tw_variables:
158
+ if self.active_user_ips(project_id) != []:
159
+ only_active[project_id] = self.tw_variables[project_id]
160
+ return only_active
161
+
162
+ def active_user_names(self, project_id):
163
+ return [self.tw_clients[user]["username"] for user in self.active_user_ips(project_id)]
164
+
165
+ def active_user_ips(self, project_id):
166
+ return list(filter(lambda user : str(self.tw_clients[user]["project_id"]) == str(project_id), self.tw_clients))
167
+
168
+ def get_global_vars(self):
169
+ return self.tw_variables
170
+
171
+ def get_project_vars(self, project_id):
172
+ project_id = str(project_id)
173
+ if project_id in self.tw_variables:
174
+ return self.tw_variables[project_id]
175
+ else: return {}
176
+
177
+ def get_var(self, project_id, var_name):
178
+ project_id = str(project_id)
179
+ var_name = var_name.replace("☁ ", "")
180
+ if project_id in self.tw_variables:
181
+ if var_name in self.tw_variables[project_id]:
182
+ return self.tw_variables[project_id][var_name]
183
+ else: return None
184
+ else: return None
185
+
186
+ def set_global_vars(self, data):
187
+ for project_id in data:
188
+ self.set_project_vars(project_id, data[project_id])
189
+
190
+ def set_project_vars(self, project_id, data, *, user="@server"):
191
+ project_id = str(project_id)
192
+ self.tw_variables[project_id] = data
193
+ for client in [self.tw_clients[ip]["client"] for ip in self.active_user_ips(project_id)]:
194
+ client.sendMessage("\n".join([
195
+ json.dumps({
196
+ "method" : "set", "project_id" : project_id, "name" : "☁ "+varname,
197
+ "value" : data[varname], "server" : "scratchattach/2.0.0", "timestamp" : time.time()*1000, "user" : user
198
+ }) for varname in data])
199
+ )
200
+
201
+ def set_var(self, project_id, var_name, value, *, user="@server", skip_forward=None):
202
+ var_name = var_name.replace("☁ ", "")
203
+ project_id = str(project_id)
204
+ if project_id not in self.tw_variables:
205
+ self.tw_variables[project_id] = {}
206
+ self.tw_variables[project_id][var_name] = value
207
+
208
+ if self.sync_players is True:
209
+ for client in [self.tw_clients[ip]["client"] for ip in self.active_user_ips(project_id)]:
210
+ if client == skip_forward:
211
+ continue
212
+ client.sendMessage(
213
+ json.dumps({
214
+ "method" : "set", "project_id" : project_id, "name" : "☁ "+var_name,
215
+ "value" : value, "timestamp" : time.time()*1000, "user" : user
216
+ })
217
+ )
218
+
219
+ def _check_value(self, value):
220
+ # Checks if a received cloud value satisfies the server's constraints
221
+ if self.length_limit is not None:
222
+ if len(str(value)) > self.length_limit:
223
+ return False
224
+ if self.allow_non_numeric is False:
225
+ x = value.replace(".", "")
226
+ x = x.replace("-", "")
227
+ if not (x.isnumeric() or x == ""):
228
+ return False
229
+ return True
230
+
231
+ def _updater(self):
232
+ try:
233
+ # Function called when .start() is executed (.start is inherited from BaseEventHandler)
234
+ print(f"Serving websocket server: ws://{hostname}:{port}")
235
+ self.serveforever()
236
+ except Exception as e:
237
+ raise exceptions.WebsocketServerError(str(e))
238
+
239
+ def pause(self):
240
+ self.running = False
241
+
242
+ def resume(self):
243
+ self.running = True
244
+
245
+ def stop(self):
246
+ self.running = False
247
+ self.close()
248
+
249
+ return TwCloudServer(hostname, port=port, websocketclass=TwCloudSocket)
@@ -0,0 +1,135 @@
1
+ """CloudStorage class"""
2
+ from __future__ import annotations
3
+
4
+ from .cloud_requests import CloudRequests
5
+ import json
6
+ import time
7
+ from threading import Thread
8
+
9
+ class Database:
10
+
11
+ """
12
+ A Database is a simple key-value storage that stores data in a JSON file saved locally (other database services like MongoDB can be implemented)
13
+ """
14
+
15
+ def __init__(self, name, *, json_file_path, save_interval=30):
16
+ self.save_event_function = None
17
+ self.set_event_function = None
18
+ self.name = name
19
+
20
+ # Import from JSON file
21
+ if not json_file_path.endswith(".json"):
22
+ json_file_path = json_file_path+".json"
23
+ self.json_file_path = json_file_path
24
+
25
+ try:
26
+ with open(json_file_path, 'r') as json_file:
27
+ self.data = json.load(json_file)
28
+ except FileNotFoundError:
29
+ print(f"Creating file {json_file_path}. Your database {name} will be stored there.")
30
+ self.data = {}
31
+ self.save_to_json()
32
+
33
+ if isinstance(self.data , list):
34
+ raise ValueError(
35
+ "Invalid JSON file content: Top-level object must be a dict, not a list"
36
+ )
37
+
38
+ # Start autosaving
39
+ self.save_interval = save_interval
40
+ if self.save_interval is not None:
41
+ Thread(target=self._autosaver).start()
42
+
43
+ def save_to_json(self):
44
+ with open(self.json_file_path, 'w') as json_file:
45
+ json.dump(self.data, json_file, indent=4)
46
+
47
+ if self.save_event_function is not None:
48
+ self.save_event_function()
49
+
50
+ def keys(self) -> list:
51
+ return list(self.data.keys())
52
+
53
+ def get(self, key) -> str:
54
+ if not key in self.data:
55
+ return None
56
+ return self.data[key]
57
+
58
+ def set(self, key, value):
59
+ self.data[key] = value
60
+
61
+ if self.set_event_function is not None:
62
+ self.set_event_function(key, value)
63
+
64
+ def event(self, event_function):
65
+ # Decorator function for adding the on_save event that is called when a save is performed
66
+ if event_function.__name__ == "on_save":
67
+ self.save_event_function = event_function
68
+ if event_function.__name__ == "on_set":
69
+ self.set_event_function = event_function
70
+
71
+ def _autosaver(self):
72
+ # Task autosaving the db. save interval specified in .save_interval attribute
73
+ while True:
74
+ time.sleep(self.save_interval)
75
+ self.save_to_json()
76
+
77
+ class CloudStorage(CloudRequests):
78
+
79
+ """
80
+ A CloudStorage object saves multiple databases and allows the connected Scratch project to access and modify the data of these databases through cloud requests
81
+
82
+ The CloudStorage class is built upon CloudRequests
83
+ """
84
+
85
+ def __init__(self, cloud, used_cloud_vars=["1", "2", "3", "4", "5", "6", "7", "8", "9"], no_packet_loss=False):
86
+ super().__init__(cloud, used_cloud_vars=used_cloud_vars, no_packet_loss=no_packet_loss)
87
+ # Setup
88
+ self._databases = {}
89
+ self.request(self.get, thread=False)
90
+ self.request(self.set, thread=False)
91
+ self.request(self.keys, thread=False)
92
+ self.request(self.database_names, thread=False)
93
+ self.request(self.ping, thread=False)
94
+
95
+ def ping(self):
96
+ return "Database backend is running"
97
+
98
+ def get(self, db_name, key) -> str:
99
+ try:
100
+ return self.get_database(db_name).get(key)
101
+ except Exception:
102
+ if self.get_database(db_name) is None:
103
+ return f"Error: Database {db_name} doesn't exist"
104
+ else:
105
+ return f"Error: Key {key} doesn't exist in database {db_name}"
106
+
107
+ def set(self, db_name, key, value):
108
+ return self.get_database(db_name).set(key, value)
109
+
110
+ def keys(self, db_name) -> list:
111
+ try:
112
+ return self.get_database(db_name).keys()
113
+ except Exception:
114
+ return f"Error: Database {db_name} doesn't exist"
115
+
116
+ def databases(self) -> list:
117
+ return list(self._databases.values())
118
+
119
+ def database_names(self) -> list:
120
+ return list(self._databases.keys())
121
+
122
+ def add_database(self, database:Database):
123
+ self._databases[database.name] = database
124
+
125
+ def get_database(self, name) -> Database:
126
+ if name in self._databases:
127
+ return self._databases[name]
128
+ return None
129
+
130
+ def save(self):
131
+ """
132
+ Saves the data in the JSON files for all databases in self._databases
133
+ """
134
+ for dbname in self._databases:
135
+ self._databases[dbname].save_to_json()
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ class MultiEventHandler:
4
+
5
+ def __init__(self, *handlers):
6
+ self.handlers = handlers
7
+
8
+ def request(self, function, *args, **kwargs):
9
+ for handler in self.handlers:
10
+ handler.request(function, *args, **kwargs)
11
+
12
+ def event(self, function, *args, **kwargs):
13
+ for handler in self.handlers:
14
+ handler.event(function, *args, **kwargs)
15
+
16
+ def start(self, *args, **kwargs):
17
+ for handler in self.handlers:
18
+ handler.start(*args, **kwargs)
19
+
20
+ def stop(self, *args, **kwargs):
21
+ for handler in self.handlers:
22
+ handler.stop(*args, **kwargs)
23
+
24
+ def pause(self, *args, **kwargs):
25
+ for handler in self.handlers:
26
+ handler.pause(*args, **kwargs)
27
+
28
+ def resume(self, *args, **kwargs):
29
+ for handler in self.handlers:
30
+ handler.resume(*args, **kwargs)
@@ -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}")
@@ -0,0 +1,42 @@
1
+ """MessageEvents class"""
2
+ from __future__ import annotations
3
+
4
+ from scratchattach.site import user
5
+ from ._base import BaseEventHandler
6
+ import time
7
+
8
+ class MessageEvents(BaseEventHandler):
9
+ """
10
+ Class that calls events when you receive messages on your Scratch account. Data fetched from Scratch's API.
11
+ """
12
+ def __init__(self, user, *, update_interval=2):
13
+ super().__init__()
14
+ self.user = user
15
+ self.current_message_count = 0
16
+ self.update_interval = update_interval
17
+
18
+ def _updater(self):
19
+ """
20
+ A process that listens for cloud activity and executes events on cloud activity
21
+ """
22
+ self.current_message_count = int(self.user.message_count())
23
+
24
+ self.call_event("on_ready")
25
+
26
+ while True:
27
+ if self.running is False:
28
+ return
29
+ message_count = int(self.user.message_count())
30
+ if message_count != self.current_message_count:
31
+ self.call_event("on_count_change", [int(self.current_message_count), int(message_count)])
32
+ if message_count != 0:
33
+ if message_count < self.current_message_count:
34
+ self.current_message_count = 0
35
+ if self.user._session is not None: # authentication check
36
+ if self.user._session.username == self.user.username: # authorization check
37
+ new_messages = self.user._session.messages(limit=message_count-self.current_message_count)
38
+ for message in new_messages[::-1]:
39
+ self.call_event("on_message", [message])
40
+ self.current_message_count = int(message_count)
41
+ time.sleep(self.update_interval)
42
+
other/__init__.py ADDED
File without changes