9f1da9a00d
Changed the setting registration, removed the warnings. Loot roll messages are more obvious now. Superadmins are meant to stay mostily hidden, but are being exposed in !system again.
245 lines
12 KiB
Python
245 lines
12 KiB
Python
import base64
|
|
import json
|
|
import threading
|
|
|
|
from cryptography.fernet import Fernet
|
|
from cryptography.hazmat.primitives import hashes
|
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
|
|
from core.decorators import instance, timerevent
|
|
from core.dict_object import DictObject
|
|
from core.logger import Logger
|
|
from core.setting_service import SettingService
|
|
from core.setting_types import ColorSettingType, TextSettingType, HiddenSettingType, BooleanSettingType
|
|
from modules.standard.datanet.ws_worker import WebsocketRelayWorker
|
|
|
|
|
|
@instance()
|
|
class RelayController:
|
|
MESSAGE_SOURCE = "websocket_relay"
|
|
|
|
def __init__(self):
|
|
self.dthread = None
|
|
self.queue = []
|
|
self.logger = Logger(__name__)
|
|
self.worker = None
|
|
self.encrypter = None
|
|
|
|
def inject(self, registry):
|
|
self.bot = registry.get_instance("bot")
|
|
self.db = registry.get_instance("db")
|
|
self.util = registry.get_instance("util")
|
|
self.setting_service: SettingService = registry.get_instance("setting_service")
|
|
self.event_service = registry.get_instance("event_service")
|
|
self.character_service = registry.get_instance("character_service")
|
|
self.pork_service = registry.get_instance("pork_service")
|
|
self.online_controller = registry.get_instance("online_controller")
|
|
self.public_channel_service = registry.get_instance("public_channel_service")
|
|
self.message_hub_service = registry.get_instance("message_hub_service")
|
|
|
|
def pre_start(self):
|
|
self.message_hub_service.register_message_source(self.MESSAGE_SOURCE)
|
|
|
|
def start(self):
|
|
self.message_hub_service.register_message_destination(self.MESSAGE_SOURCE,
|
|
self.handle_message_from_hub,
|
|
["private_channel", "org_channel"],
|
|
[self.MESSAGE_SOURCE])
|
|
|
|
self.setting_service.register(self.module_name, "websocket_relay_enabled", False, BooleanSettingType(),
|
|
"Enable the websocket relay")
|
|
self.setting_service.register(self.module_name, "websocket_relay_server_address",
|
|
"ws://localhost/subscribe/relay",
|
|
TextSettingType(["ws://localhost/subscribe/relay"]),
|
|
"The address of the websocket relay server",
|
|
"All bots on the relay must connect to the same server and channel. "
|
|
"If using the public relay server, use a unique channel name.")
|
|
self.setting_service.register(self.module_name, "websocket_relay_channel_color", "#FFFF00",
|
|
ColorSettingType(), "Color of the channel in websocket relay messages")
|
|
self.setting_service.register(self.module_name, "websocket_relay_message_color", "#FCA712",
|
|
ColorSettingType(),
|
|
"Color of the message content in websocket relay messages")
|
|
self.setting_service.register(self.module_name, "websocket_relay_sender_color", "#00DE42",
|
|
ColorSettingType(), "Color of the sender in websocket relay messages")
|
|
self.setting_service.register(self.module_name, "websocket_encryption_key", "",
|
|
HiddenSettingType(allow_empty=True),
|
|
"An encryption key used to encrypt messages over a public websocket relay")
|
|
self.setting_service.register(self.module_name, "ws_relay_prefix", "", TextSettingType(allow_empty=True),
|
|
"Name of this relay (if you don't want to use org or bot name)")
|
|
self.setting_service.register(self.module_name, "ws_msg_relay_prefix", "||",
|
|
TextSettingType(["!", "#", "*", "@", "$", "+", "-"]),
|
|
"Prefix for Messages which should get relayed")
|
|
self.setting_service.register(self.module_name, "ws_relay_type", "with_symbol",
|
|
TextSettingType(["with_symbol", "unless_symbol", "always"]),
|
|
"Relay Messages", )
|
|
self.initialize_encrypter(self.setting_service.get("websocket_encryption_key").get_value())
|
|
|
|
self.setting_service.register_change_listener("websocket_relay_enabled", self.websocket_relay_update)
|
|
self.setting_service.register_change_listener("websocket_relay_server_address", self.websocket_relay_update)
|
|
self.setting_service.register_change_listener("websocket_encryption_key", self.websocket_relay_update)
|
|
|
|
def get_org_channel_prefix(self):
|
|
return self.setting_service.get_value(
|
|
"ws_relay_prefix") or self.public_channel_service.get_org_name() or self.bot.get_char_name()
|
|
|
|
def initialize_encrypter(self, password):
|
|
if password:
|
|
# using hard-coded salt is less secure as it nullifies the
|
|
# function of the salt and allows for rainbow attacks
|
|
salt = b"tyrbot"
|
|
kdf = PBKDF2HMAC(
|
|
algorithm=hashes.SHA256(),
|
|
length=32,
|
|
salt=salt,
|
|
iterations=10000, )
|
|
key = base64.urlsafe_b64encode(kdf.derive(password.encode("utf-8")))
|
|
self.encrypter = Fernet(key)
|
|
else:
|
|
self.encrypter = None
|
|
|
|
@timerevent(budatime="1s", description="Relay messages from Text relay to the internal message hub", is_hidden=True,
|
|
is_enabled=False)
|
|
def handle_queue_event(self, _, _1):
|
|
while self.queue:
|
|
obj = self.queue.pop(0)
|
|
if obj.type == "message":
|
|
payload = obj.payload
|
|
self.process_relay_message(obj.client_id, payload)
|
|
elif obj.type == "ping":
|
|
return_obj = json.dumps({"type": "ping", "payload": obj.payload})
|
|
self.worker.send_message(return_obj)
|
|
|
|
@timerevent(budatime="1m", description="Ensure the bot is connected to Text relay", is_hidden=True,
|
|
is_enabled=False, run_at_startup=True)
|
|
def handle_connect_event(self, _, _1):
|
|
if not self.worker or not self.dthread.is_alive():
|
|
self.connect()
|
|
|
|
def process_relay_message(self, _, message):
|
|
if self.encrypter:
|
|
message = self.encrypter.decrypt(message.encode('utf-8'))
|
|
obj = DictObject(json.loads(message))
|
|
|
|
if obj.type == "message":
|
|
channel = self.get_channel_name(obj.source)
|
|
|
|
message = ""
|
|
message += "[%s] " % self.setting_service.get("websocket_relay_channel_color").format_text(channel)
|
|
if obj.user:
|
|
message += "%s: " % self.setting_service.get("websocket_relay_sender_color").format_text(obj.user.name)
|
|
message += self.setting_service.get("websocket_relay_message_color").format_text(obj.message)
|
|
|
|
self.message_hub_service.send_message(self.MESSAGE_SOURCE, obj.get("user", None), obj.message, message)
|
|
|
|
def send_relay_event(self, char_id, event_type, source):
|
|
char_name = self.character_service.resolve_char_to_name(char_id)
|
|
obj = {"user": {"id": char_id,
|
|
"name": char_name},
|
|
"type": event_type,
|
|
"source": self.create_source_obj(source)}
|
|
self.send_relay_message(obj)
|
|
|
|
def send_relay_message(self, message):
|
|
if self.worker:
|
|
message = json.dumps(message)
|
|
if self.encrypter:
|
|
message = self.encrypter.encrypt(message.encode('utf-8')).decode('utf-8')
|
|
obj = json.dumps({"type": "message", "payload": message})
|
|
self.worker.send_message(obj)
|
|
|
|
def handle_message_from_hub(self, ctx):
|
|
if not ctx.sender:
|
|
return
|
|
if self.worker:
|
|
method = self.setting_service.get_value("ws_relay_type")
|
|
symbol = self.setting_service.get_value("ws_msg_relay_prefix")
|
|
plain_msg = ctx.message or ctx.formatted_message
|
|
if method == "unless_symbol" and len(plain_msg) > len(symbol) and plain_msg[:len(symbol)] == symbol:
|
|
return
|
|
elif method == "with_symbol":
|
|
if len(plain_msg) < len(symbol) or plain_msg[:len(symbol)] != symbol:
|
|
return
|
|
else:
|
|
plain_msg = plain_msg[len(symbol):]
|
|
elif method == "always":
|
|
trim = len(ctx.formatted_message) - len(plain_msg)
|
|
if not ctx.sender and ctx.message != ctx.formatted_message[trim:]:
|
|
return
|
|
|
|
obj = {"user": self.create_user_obj(ctx.sender), "message": plain_msg.strip(),
|
|
"type": "message",
|
|
"source": self.create_source_obj(ctx.source)}
|
|
self.send_relay_message(obj)
|
|
|
|
def connect(self):
|
|
|
|
self.worker = WebsocketRelayWorker(self.queue,
|
|
self.setting_service.get("websocket_relay_server_address").get_value(), True)
|
|
self.dthread = threading.Thread(target=self.worker.run, daemon=True)
|
|
self.dthread.start()
|
|
|
|
if self.worker:
|
|
self.worker.close()
|
|
self.worker = None
|
|
self.dthread.join()
|
|
self.dthread = None
|
|
|
|
def websocket_relay_update(self, setting_name, _, new_value):
|
|
if setting_name == "websocket_relay_enabled":
|
|
event_handlers = [self.handle_connect_event, self.handle_queue_event]
|
|
for handler in event_handlers:
|
|
event_handler = self.util.get_handler_name(handler)
|
|
event_base_type, event_sub_type = self.event_service.get_event_type_parts(handler.event.event_type)
|
|
self.event_service.update_event_status(event_base_type, event_sub_type, event_handler,
|
|
1 if new_value else 0)
|
|
|
|
if not new_value:
|
|
self.disconnect()
|
|
elif setting_name == "websocket_relay_server_address":
|
|
if self.setting_service.get("websocket_relay_enabled").get_value():
|
|
self.connect()
|
|
elif setting_name == "websocket_encryption_key":
|
|
self.initialize_encrypter(new_value)
|
|
if self.setting_service.get("websocket_relay_enabled").get_value():
|
|
self.connect()
|
|
|
|
def get_channel_name(self, source):
|
|
channel_name = source.label or source.name
|
|
if source.channel:
|
|
channel_name += " " + source.channel
|
|
return channel_name
|
|
|
|
def create_user_obj(self, sender):
|
|
if sender:
|
|
return {
|
|
"id": sender.get("char_id", None),
|
|
"name": sender.name
|
|
}
|
|
else:
|
|
return None
|
|
|
|
def create_source_obj(self, source):
|
|
org_name = self.public_channel_service.get_org_name() or self.get_org_channel_prefix()
|
|
if source == "private_channel":
|
|
if org_name:
|
|
channel = "Guest"
|
|
else:
|
|
channel = ""
|
|
elif org_name and source == "org_channel":
|
|
channel = ""
|
|
else:
|
|
channel = source.capitalize()
|
|
|
|
channel_type = source
|
|
if source == "private_channel":
|
|
channel_type = "priv"
|
|
elif source == "org_channel":
|
|
channel_type = "org"
|
|
return {
|
|
"name": org_name or self.bot.get_char_name(),
|
|
"label": self.setting_service.get("ws_relay_prefix").get_value() or "",
|
|
"channel": channel,
|
|
"type": channel_type,
|
|
"server": self.bot.dimension
|
|
}
|