1
0

Compare commits

...

13 Commits

Author SHA1 Message Date
1f548eb0c5 shove these together 2025-10-01 21:44:49 -04:00
568ca7ec3e breh 2025-10-01 21:44:40 -04:00
81e42bc9a2 reconnect automatically 2025-10-01 21:44:32 -04:00
e79ec45c8d rework pretty much everything 2025-10-01 21:44:18 -04:00
b40ff985f0 init vm_botuser for vm 2025-10-01 21:43:42 -04:00
d22c76c091 overhaul connect 2025-10-01 21:43:33 -04:00
cc9e9dba8e remove state from connect scope 2025-10-01 21:41:46 -04:00
e681dac537 add debug to chat preparation 2025-10-01 21:41:15 -04:00
8930588217 add guac wrapper because i got bored
and also makes code look better.. generally. there was also a debug statement there but now its not
2025-10-01 21:41:04 -04:00
0ca85d37dc fix scope 2025-10-01 21:40:30 -04:00
b4c4bb51cf add state to global scope 2025-10-01 21:40:21 -04:00
cbb1fc9f29 add rename status 2025-10-01 21:39:50 -04:00
116ea10891 add rename status 2025-10-01 21:39:30 -04:00
2 changed files with 117 additions and 85 deletions

View File

@@ -62,6 +62,7 @@ def guac_encode(*args: str) -> str:
return f"{','.join(f'{len(arg)}.{arg}' for arg in args)};" return f"{','.join(f'{len(arg)}.{arg}' for arg in args)};"
class CollabVMState(IntEnum): class CollabVMState(IntEnum):
DISCONNECTED = -1
WS_CONNECTED = 0 WS_CONNECTED = 0
VM_CONNECTED = 1 VM_CONNECTED = 1
LOGGED_IN = 2 LOGGED_IN = 2
@@ -71,3 +72,9 @@ class CollabVMRank(IntEnum):
REGISTERED = 1 REGISTERED = 1
ADMIN = 2 ADMIN = 2
MOD = 3 MOD = 3
class CollabVMClientRenameStatus(IntEnum):
SUCCEEDED = 0
FAILED_TAKEN = 1
FAILED_INVALID = 2
FAILED_REJECTED = 3

View File

@@ -1,8 +1,7 @@
from typing import List, Optional from typing import List, Optional
from cvmlib import guac_decode, guac_encode, CollabVMRank, CollabVMState from cvmlib import guac_decode, guac_encode, CollabVMRank, CollabVMState, CollabVMClientRenameStatus
import config import config
import os import os, random, websockets, asyncio
import websockets, asyncio
from websockets import Subprotocol, Origin from websockets import Subprotocol, Origin
import logging import logging
import sys import sys
@@ -27,6 +26,7 @@ log.info(f"CVM-Sentry started")
users = {} users = {}
vm_botuser = {} vm_botuser = {}
STATE = CollabVMState.DISCONNECTED
def get_origin_from_ws_url(ws_url: str) -> str: def get_origin_from_ws_url(ws_url: str) -> str:
domain = ( domain = (
@@ -40,14 +40,20 @@ def get_origin_from_ws_url(ws_url: str) -> str:
return f"http{'s' if is_wss else ''}://{domain}/" return f"http{'s' if is_wss else ''}://{domain}/"
async def send_chat_message(websocket, message: str): async def send_chat_message(websocket, message: str):
log.debug(f"Sending chat message: {message}")
await websocket.send(guac_encode("chat", message)) await websocket.send(guac_encode("chat", message))
async def send_guac(websocket, *args: str):
await websocket.send(guac_encode(*args))
async def connect(vm_name: str): async def connect(vm_name: str):
global STATE
global users
global vm_botuser
if vm_name not in config.vms: if vm_name not in config.vms:
log.error(f"VM '{vm_name}' not found in configuration.") log.error(f"VM '{vm_name}' not found in configuration.")
return return
uri = config.vms[vm_name] uri = config.vms[vm_name]
STATE = None
log_file_path = os.path.join(getattr(config, "log_directory", "logs"), f"{vm_name}.json") log_file_path = os.path.join(getattr(config, "log_directory", "logs"), f"{vm_name}.json")
if not os.path.exists(log_file_path): if not os.path.exists(log_file_path):
with open(log_file_path, "w") as log_file: with open(log_file_path, "w") as log_file:
@@ -60,107 +66,126 @@ async def connect(vm_name: str):
) as websocket: ) as websocket:
log.info(f"Connected to VM '{vm_name}' at {uri}") log.info(f"Connected to VM '{vm_name}' at {uri}")
STATE = CollabVMState.WS_CONNECTED STATE = CollabVMState.WS_CONNECTED
await websocket.send(guac_encode("rename", "")) await send_guac(websocket, "rename", "")
await websocket.send(guac_encode("connect", vm_name)) await send_guac(websocket, "connect", vm_name)
if vm_name not in users: if vm_name not in users:
users[vm_name] = {} users[vm_name] = {}
if vm_name not in vm_botuser:
vm_botuser[vm_name] = ""
# response = await websocket.recv() # response = await websocket.recv()
async for message in websocket: async for message in websocket:
decoded: Optional[List[str]] = guac_decode(str(message)) decoded: Optional[List[str]] = guac_decode(str(message))
match decoded: match decoded:
case ["nop"]: case ["nop"]:
await websocket.send(guac_encode("nop")) await send_guac(websocket, "nop")
log.debug((f"({CollabVMState(STATE).name}) Received: {decoded}"))
case ["auth", config.auth_server]: case ["auth", config.auth_server]:
await asyncio.sleep(1) await asyncio.sleep(1)
await websocket.send( await send_guac(websocket, "login", config.credentials["session_auth"])
guac_encode("login", config.credentials["session_auth"]) case ["connect", *rest]:
)
case ["connect", "1", "1", "1", "0"]:
STATE = CollabVMState.VM_CONNECTED STATE = CollabVMState.VM_CONNECTED
log.debug((f"({CollabVMState(STATE).name} - {vm_name}) Connected")) connection_status = "Connected" if rest[0] == "1" else "Disconnected" if rest[0] == "2" else "Connected"
turns_status = "Enabled" if rest[1] == "1" else "Disabled"
votes_status = "Enabled" if rest[2] == "1" else "Disabled"
uploads_status = "Enabled" if rest[3] == "1" else "Disabled"
log.debug(f"({STATE.name} - {vm_name}) {connection_status} | Turns: {turns_status} | Votes: {votes_status} | Uploads: {uploads_status}")
case ["rename", *instructions]:
match instructions:
case ["0", status, new_name]:
if CollabVMClientRenameStatus(int(status)) == CollabVMClientRenameStatus.SUCCEEDED:
log.debug(f"({STATE.name} - {vm_name}) Bot rename on VM {vm_name}: {vm_botuser[vm_name]} -> {new_name}")
vm_botuser[vm_name] = new_name
else:
log.debug(f"({STATE.name} - {vm_name}) Bot rename on VM {vm_name} failed with status {CollabVMClientRenameStatus(int(status)).name}")
case ["1", old_name, new_name]:
if old_name in users[vm_name]:
log.debug(f"({STATE.name} - {vm_name}) User rename on VM {vm_name}: {old_name} -> {new_name}")
users[vm_name][new_name] = users[vm_name].pop(old_name)
case ["login", "1"]: case ["login", "1"]:
STATE = CollabVMState.LOGGED_IN STATE = CollabVMState.LOGGED_IN
await send_chat_message(websocket, "RICHARD NIXON TO THE RESCUE") #await send_chat_message(websocket, random.choice(config.autostart_messages))
case ["chat", user, message]:
system_message = (user == "")
if system_message:
continue
log.info(f"[{vm_name} - {user}]: {message}")
utc_now = datetime.now(timezone.utc)
utc_day = utc_now.strftime("%Y-%m-%d")
timestamp = utc_now.isoformat()
with open(log_file_path, "r+") as log_file:
try:
log_data = json.load(log_file)
except json.JSONDecodeError:
log_data = {}
if utc_day not in log_data:
log_data[utc_day] = []
log_data[utc_day].append({
"timestamp": timestamp,
"username": user,
"message": message
})
log_file.seek(0)
json.dump(log_data, log_file, indent=4)
log_file.truncate()
if config.commands["enabled"] and message.startswith(config.commands["prefix"]):
command = message[len(config.commands["prefix"]):].strip().lower()
match command:
case "whoami":
await send_chat_message(websocket, f"You are {user} with rank {users[vm_name][user]['rank'].name}.")
case "about":
await send_chat_message(websocket, config.responses.get("about", "CVM-Sentry (NO RESPONSE CONFIGURED)"))
case "dump":
log.debug(f"{json.dumps(users)}")
case ["adduser", count, *list]:
for i in range(int(count)):
user = list[i * 2]
rank = CollabVMRank(int(list[i * 2 + 1]))
if user in users[vm_name]:
users[vm_name][user]["rank"] = rank
log.info(f"[{vm_name}] User '{user}' rank updated to {rank.name}.")
else:
users[vm_name][user] = {"rank": rank, "turn_active": False}
log.info(f"[{vm_name}] User '{user}' connected with rank {rank.name}.")
case ["turn", _, "0"]:
if STATE < CollabVMState.LOGGED_IN:
continue
log.debug(f"({STATE.name} - {vm_name}) Turn queue exhausted.")
case ["turn", turn_time, count, current_turn, *queue]:
log.debug(f"({STATE.name} - {vm_name}) Turn queue updated: {queue} | Current turn: {current_turn} | Time left for current turn: {int(turn_time)//1000}s")
for user in users[vm_name]:
users[vm_name][user]["turn_active"] = (user == current_turn)
case ["remuser", count, *list]:
for i in range(int(count)):
username = list[i]
if username in users[vm_name]:
del users[vm_name][username]
log.info(f"[{vm_name}] User '{username}' left.")
case _: case _:
if decoded is not None: if decoded is not None:
if decoded[0] in ("sync", "png", "flag", "turn", "size"): if decoded[0] in ("sync", "png", "flag", "size"):
continue continue
elif decoded[0] == "chat": log.debug(f"({STATE.name} - {vm_name}) Unhandled message: {decoded}")
user = "System" if len(decoded[1]) == 0 else decoded[1]
if user == "System" or STATE < CollabVMState.LOGGED_IN:
continue
message = decoded[2]
log.info(f"[{vm_name} - {user}]: {message}")
utc_now = datetime.now(timezone.utc)
utc_day = utc_now.strftime("%Y-%m-%d")
timestamp = utc_now.isoformat()
with open(log_file_path, "r+") as log_file:
try:
log_data = json.load(log_file)
except json.JSONDecodeError:
log_data = {}
if utc_day not in log_data:
log_data[utc_day] = []
log_data[utc_day].append({
"timestamp": timestamp,
"username": user,
"message": message
})
log_file.seek(0)
json.dump(log_data, log_file, indent=4)
log_file.truncate()
if config.commands["enabled"] and message.startswith(config.commands["prefix"]):
command = message[len(config.commands["prefix"]):].strip().lower()
match command:
case "whoami":
await send_chat_message(websocket, f"You are {user} with rank {users[vm_name].get(user, 'Unknown').name}.")
case "about":
await send_chat_message(websocket, "RICHARD NIXONTRON 4000")
continue
elif decoded[0] == "adduser":
if STATE == CollabVMState.LOGGED_IN:
username = decoded[2]
rank = CollabVMRank(int(decoded[3]))
users[vm_name][username] = rank
elif STATE < CollabVMState.LOGGED_IN:
initial_user_payload = decoded[2:]
for i in range(0, len(initial_user_payload), 2):
username = initial_user_payload[i]
rank = CollabVMRank(int(initial_user_payload[i + 1]))
users[vm_name][username] = rank
elif decoded[0] == "remuser":
if STATE == CollabVMState.LOGGED_IN:
username = decoded[2]
if username in users[vm_name]:
del users[vm_name][username]
elif decoded[0] == "rename":
if STATE == CollabVMState.WS_CONNECTED and decoded[1:3] == ["0", "0"]:
log.debug(f"AUTHORITATIVE GUEST NAME!! {decoded[3]}")
## SET CURRENT BOT NAME ##
vm_botuser[vm_name] = decoded[3]
elif STATE == CollabVMState.LOGGED_IN and decoded[2] in vm_botuser[vm_name]:
log.debug(f"AUTHORITATIVE BOT NAME CHANGE!! {decoded[3]}")
## SET CURRENT BOT NAME ##
vm_botuser[vm_name] = decoded[3]
log.debug(
(
f"({CollabVMState(STATE).name} - {vm_name}) Received unfiltered: {decoded}"
)
)
for vm in config.vms.keys(): for vm in config.vms.keys():
def start_vm_thread(vm_name: str): def start_vm_thread(vm_name: str):
asyncio.run(connect(vm_name)) asyncio.run(connect(vm_name))
async def main(): async def main():
tasks = [connect(vm) for vm in config.vms.keys()] async def connect_with_reconnect(vm_name: str):
while True:
try:
await connect(vm_name)
except websockets.exceptions.ConnectionClosedError as e:
log.warning(f"Connection to VM '{vm_name}' closed with error: {e}. Reconnecting...")
await asyncio.sleep(5) # Wait before attempting to reconnect
except websockets.exceptions.ConnectionClosedOK:
log.warning(f"Connection to VM '{vm_name}' closed cleanly (code 1005). Reconnecting...")
await asyncio.sleep(5) # Wait before attempting to reconnect
tasks = [connect_with_reconnect(vm) for vm in config.vms.keys()]
await asyncio.gather(*tasks) await asyncio.gather(*tasks)
asyncio.run(main()) asyncio.run(main())