Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
252d276
refactor: Add try-except blocks for loading json on connect and readi…
Boss-1s May 7, 2026
7fdfe0f
Finish debug for json loading and reading 'method' key
Boss-1s May 7, 2026
a44c3f5
a little more context on the ip banned message
Boss-1s May 8, 2026
a05828e
[UNIMPORTANT] notated unsued import
Boss-1s May 9, 2026
270025f
Merge branch 'TimMcCool:main' into main
Boss-1s May 10, 2026
11b1dd9
Merge branch 'TimMcCool:main' into tw-debug-compliance
Boss-1s May 16, 2026
d2d5f0c
Merge branch 'main' into tw-debug-compliance
Boss-1s Jun 2, 2026
7f2c18a
Merge branch 'TimMcCool:main' into tw-debug-compliance
Boss-1s Jun 2, 2026
5fee55e
Merge branch 'TimMcCool:main' into tw-debug-compliance
Boss-1s Jun 6, 2026
67c6148
Merge branch 'TimMcCool:main' into tw-debug-compliance
Boss-1s Jun 21, 2026
958b3d4
Merge branch 'TimMcCool:main' into tw-debug-compliance
Boss-1s Jul 1, 2026
439a12a
Merge branch 'TimMcCool:main' into tw-debug-compliance
Boss-1s Jul 25, 2026
80efca4
Merge branch 'TimMcCool:main' into tw-debug-compliance
Boss-1s Aug 10, 2026
e764962
Merge branch 'TimMcCool:main' into tw-debug-compliance
Boss-1s Aug 23, 2026
435d5f5
feat: rich
Boss-1s Aug 25, 2026
db50b0b
bump ruff up a patch
Boss-1s Aug 25, 2026
9863d50
feat(cloud_server): implement SSL secure websocket from semver2 into …
Boss-1s Aug 26, 2026
7e8fddd
fix(cloud_server): expose `init_ssl_cloud_sever` to top-level
Boss-1s Aug 26, 2026
bab2905
chore(eventhandlers._base): move mixin class to _base
Boss-1s Aug 26, 2026
5cd379d
fatal(eventhandlers._base): missing imports
Boss-1s Aug 26, 2026
494063c
Merge branch 'semver3-secure-ws' into tw-debug-compliance
Boss-1s Aug 26, 2026
4740cc8
fix(eventhandlers._base): remove stale reference
Boss-1s Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scratchattach/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .cloud.cloud import CustomCloud, ScratchCloud, TwCloud, get_cloud, get_scratch_cloud, get_tw_cloud
from .cloud._base import BaseCloud, AnyCloud

from .eventhandlers.cloud_server import init_cloud_server
from .eventhandlers.cloud_server import init_cloud_server, init_ssl_cloud_server
from .eventhandlers._base import BaseEventHandler
from .eventhandlers.filterbot import Filterbot, HardFilter, SoftFilter, SpamFilter
from .eventhandlers.cloud_storage import Database
Expand Down
180 changes: 176 additions & 4 deletions scratchattach/eventhandlers/_base.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
from __future__ import annotations

import json
import time
import ssl
from abc import ABC, abstractmethod
from typing import Optional
from typing import Optional, Any
from collections import defaultdict
from threading import Thread, Event
from collections.abc import Callable
import traceback

from SimpleWebSocketServer import WebSocket

from scratchattach.utils.requests import requests
from scratchattach.utils import exceptions

Expand Down Expand Up @@ -41,7 +47,7 @@ def start(self, *, thread=True, ignore_exceptions=True):
else:
self._thread = None
self._updater()

def call_event(self, event_name, args : list = []):
try:
# print(f"Calling for {event_name}...")
Expand Down Expand Up @@ -69,7 +75,7 @@ def call_event(self, event_name, args : list = []):
@abstractmethod
def _updater(self):
pass

def __del__(self):
self.stop()

Expand Down Expand Up @@ -120,4 +126,170 @@ def inner(function):
return inner
else:
# => the decorator doesn't provide arguments
inner(function)
inner(function)

class BaseCloudServer(BaseEventHandler):
def __init__(
self,
hostname,
*,
port,
websocketclass,
length_limit=None,
allow_non_numeric=True,
whitelisted_projects=None,
allow_nonscratch_names=True,
blocked_ips=None,
sync_players=True,
log_var_sets=True,
):
if blocked_ips is None:
blocked_ips = []

BaseEventHandler.__init__(self)

self.running = False
self._events = {} # saves event functions called on cloud updates

self.tw_clients = {} # saves connected clients
self.tw_variables = {} # holds cloud variable states

self.hostname = hostname
self.port = port

# server config
self.allow_non_numeric = allow_non_numeric
self.whitelisted_projects = whitelisted_projects
self.length_limit = length_limit
self.allow_nonscratch_names = allow_nonscratch_names
self.blocked_ips = blocked_ips
self.sync_players = sync_players
self.log_var_sets = log_var_sets

def check_for_ip_ban(self, client):
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
):
client.sendMessage("You have been banned from this server")
client.close(4002)
print(f"[yellow]Client {client.address[0]}:{client.address[1]} was forced disconnected "+
"due to IP ban. [b]If this dosen't look right, remove them from the list.[/][/]")
return True
return False

def active_projects(self):
only_active = {}
for project_id in self.tw_variables:
if self.active_user_ips(project_id) != []:
only_active[project_id] = self.tw_variables[project_id]
return only_active

def active_user_names(self, project_id):
return [self.tw_clients[user]["username"] for user in self.active_user_ips(project_id)]

def active_user_ips(self, project_id):
return list(filter(lambda user: str(self.tw_clients[user]["project_id"]) == str(project_id), self.tw_clients))

def get_global_vars(self):
return self.tw_variables

def get_project_vars(self, project_id):
project_id = str(project_id)
if project_id in self.tw_variables:
return self.tw_variables[project_id]
else:
return {}

def get_var(self, project_id, var_name):
project_id = str(project_id)
var_name = var_name.replace("☁ ", "")
if project_id in self.tw_variables:
if var_name in self.tw_variables[project_id]:
return self.tw_variables[project_id][var_name]
else:
return None
else:
return None

def set_global_vars(self, data):
for project_id in data:
self.set_project_vars(project_id, data[project_id])

def set_project_vars(self, project_id, data, *, user="@server"):
project_id = str(project_id)
self.tw_variables[project_id] = data
for client in [self.tw_clients[ip]["client"] for ip in self.active_user_ips(project_id)]:
client.sendMessage(
"\n".join(
[
json.dumps(
{
"method": "set",
"project_id": project_id,
"name": "☁ " + varname,
"value": data[varname],
"server": "scratchattach/2.0.0",
"timestamp": time.time() * 1000,
"user": user,
}
)
for varname in data
]
)
)

def set_var(self, project_id, var_name, value, *, user="@server", skip_forward=None):
var_name = var_name.replace("☁ ", "")
project_id = str(project_id)
if project_id not in self.tw_variables:
self.tw_variables[project_id] = {}
self.tw_variables[project_id][var_name] = value

if self.sync_players is True:
for client in [self.tw_clients[ip]["client"] for ip in self.active_user_ips(project_id)]:
if client == skip_forward:
continue
client.sendMessage(
json.dumps(
{
"method": "set",
"project_id": project_id,
"name": "☁ " + var_name,
"value": value,
"timestamp": time.time() * 1000,
"user": user,
}
)
)

def _check_value(self, value):
# Checks if a received cloud value satisfies the server's constraints
if self.length_limit is not None:
if len(str(value)) > self.length_limit:
return False
if self.allow_non_numeric is False:
x = value.replace(".", "")
x = x.replace("-", "")
if not (x.isnumeric() or x == ""):
return False
return True

def _updater(self):
try:
# Function called when .start() is executed (.start is inherited from BaseEventHandler)
print(f"Serving websocket server: ws://{self.hostname}:{self.port}")
self.serveforever()
except Exception as e:
raise exceptions.WebsocketServerError(str(e))

def pause(self):
self.running = False

def resume(self):
self.running = True

def stop(self, wait_call_threads: bool = True):
BaseEventHandler.stop(self, wait_call_threads) # wait_call_threads does not exist in BaseEventHandler.stop
self.close()
Loading