Release 0.9.0
This commit is contained in:
466
bot/modules/telnet/__init__.py
Normal file
466
bot/modules/telnet/__init__.py
Normal file
@@ -0,0 +1,466 @@
|
||||
import re
|
||||
from bot.module import Module
|
||||
from bot import loaded_modules_dict
|
||||
from bot.logger import get_logger
|
||||
from bot.constants import TELNET_TIMEOUT_NORMAL, TELNET_TIMEOUT_RECONNECT
|
||||
from time import time
|
||||
from collections import deque
|
||||
import telnetlib
|
||||
|
||||
logger = get_logger("telnet")
|
||||
|
||||
|
||||
class Telnet(Module):
|
||||
tn = object
|
||||
|
||||
telnet_buffer = str
|
||||
valid_telnet_lines = deque
|
||||
|
||||
telnet_lines_to_process = deque
|
||||
telnet_command_queue = deque
|
||||
|
||||
def __init__(self):
|
||||
self.telnet_command_queue = deque()
|
||||
setattr(self, "default_options", {
|
||||
"module_name": self.get_module_identifier()[7:],
|
||||
"host": "127.0.0.1",
|
||||
"port": 8081,
|
||||
"password": "thisissecret",
|
||||
"web_username": "",
|
||||
"web_password": "",
|
||||
"max_queue_length": 100,
|
||||
"run_observer_interval": 3,
|
||||
"run_observer_interval_idle": 10,
|
||||
"max_telnet_buffer": 16384,
|
||||
"max_command_queue_execution": 6,
|
||||
"match_types_generic": {
|
||||
'log_start': [
|
||||
r"\A(?P<datetime>\d{4}.+?)\s(?P<gametime_in_seconds>.+?)\sINF .*",
|
||||
r"\ATime:\s(?P<servertime_in_minutes>.*)m\s",
|
||||
],
|
||||
'log_end': [
|
||||
r"\r\n$",
|
||||
r"\sby\sTelnet\sfrom\s(.*)\:(\d.*)\s*$"
|
||||
]
|
||||
}
|
||||
})
|
||||
setattr(self, "required_modules", [
|
||||
"module_dom",
|
||||
"module_webserver"
|
||||
])
|
||||
|
||||
self.next_cycle = 0
|
||||
self.telnet_response = ""
|
||||
|
||||
Module.__init__(self)
|
||||
|
||||
@staticmethod
|
||||
def get_module_identifier():
|
||||
return "module_telnet"
|
||||
|
||||
def on_socket_connect(self, steamid):
|
||||
Module.on_socket_connect(self, steamid)
|
||||
|
||||
def on_socket_disconnect(self, steamid):
|
||||
Module.on_socket_disconnect(self, steamid)
|
||||
|
||||
# region Standard module stuff
|
||||
def setup(self, options=dict):
|
||||
Module.setup(self, options)
|
||||
|
||||
self.telnet_lines_to_process = deque(maxlen=self.options["max_queue_length"])
|
||||
self.valid_telnet_lines = deque(maxlen=self.options["max_queue_length"])
|
||||
self.run_observer_interval = self.options.get(
|
||||
"run_observer_interval", self.default_options.get("run_observer_interval", None)
|
||||
)
|
||||
self.run_observer_interval_idle = self.options.get(
|
||||
"run_observer_interval_idle", self.default_options.get("run_observer_interval_idle", None)
|
||||
)
|
||||
self.max_command_queue_execution = self.options.get(
|
||||
"max_command_queue_execution", self.default_options.get("max_command_queue_execution", None)
|
||||
)
|
||||
self.telnet_buffer = ""
|
||||
|
||||
self.last_execution_time = 0.0
|
||||
|
||||
setattr(self, "last_connection_loss", None)
|
||||
setattr(self, "recent_telnet_response", None)
|
||||
# endregion
|
||||
|
||||
# region Handling telnet initialization and authentication
|
||||
def setup_telnet(self):
|
||||
try:
|
||||
connection = telnetlib.Telnet(
|
||||
self.options.get("host"),
|
||||
self.options.get("port"),
|
||||
timeout=TELNET_TIMEOUT_NORMAL
|
||||
)
|
||||
self.tn = self.authenticate(connection, self.options.get("password"))
|
||||
except Exception as error:
|
||||
logger.error("telnet_connection_failed",
|
||||
host=self.options.get("host"),
|
||||
port=self.options.get("port"),
|
||||
error=str(error),
|
||||
error_type=type(error).__name__)
|
||||
raise IOError
|
||||
|
||||
return True
|
||||
|
||||
def authenticate(self, connection, password):
|
||||
try:
|
||||
# Waiting for the prompt.
|
||||
found_prompt = False
|
||||
while found_prompt is not True:
|
||||
telnet_response = connection.read_until(b"\r\n", timeout=TELNET_TIMEOUT_NORMAL).decode("utf-8")
|
||||
if re.match(r"Please enter password:\r\n", telnet_response):
|
||||
found_prompt = True
|
||||
else:
|
||||
raise IOError
|
||||
|
||||
# Sending password.
|
||||
full_auth_response = ''
|
||||
authenticated = False
|
||||
connection.write(password.encode('ascii') + b"\r\n")
|
||||
while authenticated is not True: # loop until authenticated, it's required
|
||||
telnet_response = connection.read_until(b"\r\n").decode("utf-8")
|
||||
full_auth_response += telnet_response.rstrip()
|
||||
# last 'welcome' line from the games telnet. it might change with a new game-version
|
||||
if re.match(r"Password incorrect, please enter password:\r\n", telnet_response) is not None:
|
||||
logger.error("telnet_auth_failed",
|
||||
host=self.options.get("host"),
|
||||
port=self.options.get("port"),
|
||||
reason="incorrect password")
|
||||
raise ValueError
|
||||
if re.match(r"Logon successful.\r\n", telnet_response) is not None:
|
||||
authenticated = True
|
||||
|
||||
# Waiting for banner.
|
||||
full_banner = ''
|
||||
displayed_welcome = False
|
||||
while displayed_welcome is not True: # loop until ready, it's required
|
||||
telnet_response = connection.read_until(b"\r\n").decode("utf-8")
|
||||
full_banner += telnet_response.rstrip("\r\n")
|
||||
if re.match(
|
||||
r"Press 'help' to get a list of all commands. Press 'exit' to end session.",
|
||||
telnet_response
|
||||
):
|
||||
displayed_welcome = True
|
||||
|
||||
except Exception as e:
|
||||
raise IOError
|
||||
|
||||
# Connection successful - no log needed
|
||||
return connection
|
||||
# endregion
|
||||
|
||||
# region handling and preparing telnet-lines
|
||||
def is_a_valid_line(self, telnet_line):
|
||||
telnet_response_is_a_valid_line = False
|
||||
if self.has_valid_start(telnet_line) and self.has_valid_end(telnet_line):
|
||||
telnet_response_is_a_valid_line = True
|
||||
|
||||
return telnet_response_is_a_valid_line
|
||||
|
||||
def has_valid_start(self, telnet_response):
|
||||
telnet_response_has_valid_start = False
|
||||
for match_type in self.options.get("match_types_generic").get("log_start"):
|
||||
if re.match(match_type, telnet_response):
|
||||
telnet_response_has_valid_start = True
|
||||
|
||||
return telnet_response_has_valid_start
|
||||
|
||||
def has_valid_end(self, telnet_response):
|
||||
telnet_response_has_valid_end = False
|
||||
for match_type in self.options.get("match_types_generic").get("log_end"):
|
||||
if re.search(match_type, telnet_response):
|
||||
telnet_response_has_valid_end = True
|
||||
|
||||
return telnet_response_has_valid_end
|
||||
|
||||
def has_mutliple_lines(self, telnet_response):
|
||||
telnet_response_has_multiple_lines = False
|
||||
telnet_response_count = telnet_response.count(b"\r\n")
|
||||
if telnet_response_count >= 1:
|
||||
telnet_response_has_multiple_lines = telnet_response_count
|
||||
|
||||
return telnet_response_has_multiple_lines
|
||||
|
||||
@staticmethod
|
||||
def extract_lines(telnet_response):
|
||||
return [telnet_line for telnet_line in telnet_response.splitlines(True)]
|
||||
|
||||
def get_a_bunch_of_lines_from_queue(self, this_many_lines):
|
||||
telnet_lines = []
|
||||
current_queue_length = 0
|
||||
done = False
|
||||
while (current_queue_length < this_many_lines) and not done:
|
||||
try:
|
||||
telnet_lines.append(self.telnet_lines_to_process.popleft())
|
||||
current_queue_length += 1
|
||||
except IndexError:
|
||||
done = True
|
||||
|
||||
if len(telnet_lines) >= 1:
|
||||
return telnet_lines
|
||||
else:
|
||||
return []
|
||||
|
||||
def add_telnet_command_to_queue(self, command):
|
||||
if command not in self.telnet_command_queue:
|
||||
self.telnet_command_queue.appendleft(command)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def execute_telnet_command_queue(self, this_many_lines):
|
||||
telnet_command_list = []
|
||||
current_queue_length = 0
|
||||
done = False
|
||||
initial_queue_length = len(self.telnet_command_queue)
|
||||
while (current_queue_length < this_many_lines) and not done:
|
||||
try:
|
||||
telnet_command_list.append(self.telnet_command_queue.popleft())
|
||||
current_queue_length += 1
|
||||
except IndexError:
|
||||
done = True
|
||||
|
||||
remaining_queue_length = len(self.telnet_command_queue)
|
||||
# print(initial_queue_length, ":", remaining_queue_length)
|
||||
|
||||
for telnet_command in reversed(telnet_command_list):
|
||||
command = "{command}{line_end}".format(command=telnet_command, line_end="\r\n")
|
||||
|
||||
try:
|
||||
self.tn.write(command.encode('ascii'))
|
||||
|
||||
except Exception as error:
|
||||
logger.error("telnet_command_send_failed",
|
||||
command=telnet_command,
|
||||
error=str(error),
|
||||
error_type=type(error).__name__,
|
||||
queue_size=remaining_queue_length)
|
||||
# endregion
|
||||
|
||||
# ==================== Line Processing Helper Methods ====================
|
||||
|
||||
def _should_exclude_from_logs(self, telnet_line: str) -> bool:
|
||||
"""Check if a telnet line should be excluded from logs."""
|
||||
elements_excluded_from_logs = [
|
||||
"'lp'", "'gettime'", "'listents'", # system calls
|
||||
"INF Time: ", "SleeperVolume", " killed by " # irrelevant lines for now
|
||||
]
|
||||
return any(exclude in telnet_line for exclude in elements_excluded_from_logs)
|
||||
|
||||
def _store_valid_line(self, valid_line: str) -> None:
|
||||
"""Store a valid telnet line in DOM."""
|
||||
# Store in DOM if clients are connected and line is relevant
|
||||
if not self._should_exclude_from_logs(valid_line):
|
||||
if len(self.webserver.connected_clients) >= 1:
|
||||
self.dom.data.append({
|
||||
self.get_module_identifier(): {
|
||||
"telnet_lines": valid_line
|
||||
}
|
||||
}, maxlen=150)
|
||||
|
||||
# Debug log only (disabled by default to avoid spam)
|
||||
# Uncomment next line and enable debug logging if needed for troubleshooting
|
||||
# logger.debug("telnet_line_received", line=valid_line[:100])
|
||||
|
||||
self.valid_telnet_lines.append(valid_line)
|
||||
|
||||
def _process_first_component(self, component: str) -> str:
|
||||
"""
|
||||
Process the first component of telnet response.
|
||||
|
||||
This might be the remainder of the previous run combined with new data.
|
||||
Returns the validated line or None.
|
||||
"""
|
||||
if self.recent_telnet_response is not None:
|
||||
# Try to combine with previous incomplete response
|
||||
combined_line = f"{self.recent_telnet_response}{component}"
|
||||
if self.is_a_valid_line(combined_line):
|
||||
self.recent_telnet_response = None
|
||||
return combined_line.rstrip("\r\n")
|
||||
else:
|
||||
# Combined line still doesn't make sense
|
||||
stripped = combined_line.rstrip("\r\n")
|
||||
logger.warn("telnet_invalid_line_combined", line=stripped)
|
||||
self.recent_telnet_response = None
|
||||
return None
|
||||
else:
|
||||
# No previous response - check if this is an incomplete line to store
|
||||
if self.has_valid_start(component):
|
||||
self.recent_telnet_response = component
|
||||
else:
|
||||
# Invalid start - warn if not empty
|
||||
stripped = component.rstrip("\r\n")
|
||||
if len(stripped) != 0:
|
||||
logger.warn("telnet_invalid_line_start", line=stripped)
|
||||
return None
|
||||
|
||||
def _process_last_component(self, component: str) -> str:
|
||||
"""
|
||||
Process the last component of telnet response.
|
||||
|
||||
This might be the start of the next run.
|
||||
Returns the validated line or None.
|
||||
"""
|
||||
if self.has_valid_start(component):
|
||||
# Store for next run
|
||||
self.recent_telnet_response = component
|
||||
# else: part of a telnet-command response, ignore
|
||||
return None
|
||||
|
||||
def _process_middle_component(self, component: str) -> str:
|
||||
"""
|
||||
Process a middle component (neither first nor last).
|
||||
|
||||
These are usually incomplete lines or command responses.
|
||||
Returns None as these are typically not valid complete lines.
|
||||
"""
|
||||
# Middle components are usually fragmented, ignore them
|
||||
return None
|
||||
|
||||
def _process_line_component(
|
||||
self,
|
||||
component: str,
|
||||
component_index: int,
|
||||
total_components: int
|
||||
) -> str:
|
||||
"""
|
||||
Process a single component of the telnet response.
|
||||
|
||||
Args:
|
||||
component: The line component to process
|
||||
component_index: 1-based index of this component
|
||||
total_components: Total number of components
|
||||
|
||||
Returns:
|
||||
Validated telnet line or None
|
||||
"""
|
||||
# Check if it's a complete, valid line first
|
||||
if self.is_a_valid_line(component):
|
||||
return component.rstrip("\r\n")
|
||||
|
||||
# Handle incomplete lines based on position
|
||||
is_first = (component_index == 1)
|
||||
is_last = (component_index == total_components)
|
||||
is_single = (total_components == 1)
|
||||
|
||||
if is_first and is_single:
|
||||
# Single incomplete component - special handling
|
||||
return self._process_first_component(component)
|
||||
elif is_first:
|
||||
# First of multiple - might combine with previous
|
||||
return self._process_first_component(component)
|
||||
elif is_last:
|
||||
# Last component - might be start of next
|
||||
return self._process_last_component(component)
|
||||
else:
|
||||
# Middle component - usually fragmented
|
||||
return self._process_middle_component(component)
|
||||
|
||||
def _process_telnet_response_lines(self) -> None:
|
||||
"""
|
||||
Process telnet response and extract valid lines.
|
||||
|
||||
Handles line fragmentation across multiple reads and stores
|
||||
valid lines for further processing.
|
||||
"""
|
||||
telnet_response_components = self.extract_lines(self.telnet_response)
|
||||
total_components = len(telnet_response_components)
|
||||
|
||||
for index, component in enumerate(telnet_response_components, start=1):
|
||||
valid_line = self._process_line_component(
|
||||
component,
|
||||
index,
|
||||
total_components
|
||||
)
|
||||
|
||||
if valid_line is not None:
|
||||
self.telnet_lines_to_process.append(valid_line)
|
||||
self._store_valid_line(valid_line)
|
||||
|
||||
def _handle_connection_error(self, error: Exception) -> None:
|
||||
"""Handle telnet connection errors and attempt reconnection."""
|
||||
try:
|
||||
self.setup_telnet()
|
||||
self.dom.data.upsert({
|
||||
self.get_module_identifier(): {
|
||||
"server_is_online": True
|
||||
}
|
||||
})
|
||||
except (OSError, Exception, ConnectionRefusedError) as error:
|
||||
self.dom.data.upsert({
|
||||
self.get_module_identifier(): {
|
||||
"server_is_online": False
|
||||
}
|
||||
})
|
||||
self.telnet_buffer = ""
|
||||
self.telnet_response = ""
|
||||
|
||||
# Only log on first connection loss, not on every retry
|
||||
if self.last_connection_loss is None:
|
||||
logger.error("telnet_server_unreachable",
|
||||
host=self.options.get("host"),
|
||||
port=self.options.get("port"),
|
||||
error=str(error),
|
||||
error_type=type(error).__name__,
|
||||
note="will retry every 10 seconds")
|
||||
|
||||
self.last_connection_loss = time()
|
||||
|
||||
def _update_telnet_buffer(self) -> None:
|
||||
"""Update the telnet buffer with new response data."""
|
||||
self.telnet_buffer += self.telnet_response.lstrip()
|
||||
max_telnet_buffer = self.options.get(
|
||||
"max_telnet_buffer",
|
||||
self.default_options.get("max_telnet_buffer", 12288)
|
||||
)
|
||||
# Trim buffer to max size
|
||||
self.telnet_buffer = self.telnet_buffer[-max_telnet_buffer:]
|
||||
|
||||
# Expose buffer to other modules via DOM
|
||||
self.dom.data.upsert({
|
||||
self.get_module_identifier(): {
|
||||
"telnet_buffer": self.telnet_buffer
|
||||
}
|
||||
})
|
||||
|
||||
# ==================== Main Run Loop ====================
|
||||
|
||||
def run(self):
|
||||
while not self.stopped.wait(self.next_cycle):
|
||||
profile_start = time()
|
||||
|
||||
# Throttle connection attempts: only try if connected or timeout passed since last failure
|
||||
can_attempt_connection = (
|
||||
self.last_connection_loss is None or
|
||||
profile_start > self.last_connection_loss + TELNET_TIMEOUT_RECONNECT
|
||||
)
|
||||
|
||||
if can_attempt_connection:
|
||||
try:
|
||||
self.telnet_response = self.tn.read_very_eager().decode("utf-8")
|
||||
except (AttributeError, EOFError, ConnectionAbortedError, ConnectionResetError) as error:
|
||||
self._handle_connection_error(error)
|
||||
except Exception as error:
|
||||
logger.error("telnet_unforeseen_error",
|
||||
error=str(error),
|
||||
error_type=type(error).__name__,
|
||||
host=self.options.get("host"),
|
||||
port=self.options.get("port"))
|
||||
|
||||
# Process any telnet response data
|
||||
if len(self.telnet_response) > 0:
|
||||
self._update_telnet_buffer()
|
||||
self._process_telnet_response_lines()
|
||||
|
||||
if self.dom.data.get(self.get_module_identifier()).get("server_is_online") is True:
|
||||
self.execute_telnet_command_queue(self.max_command_queue_execution)
|
||||
|
||||
self.last_execution_time = time() - profile_start
|
||||
self.next_cycle = self.run_observer_interval - self.last_execution_time
|
||||
|
||||
|
||||
loaded_modules_dict[Telnet().get_module_identifier()] = Telnet()
|
||||
45
bot/modules/telnet/actions/toggle_telnet_widget_view.py
Normal file
45
bot/modules/telnet/actions/toggle_telnet_widget_view.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from bot import loaded_modules_dict
|
||||
from os import path, pardir
|
||||
|
||||
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
|
||||
action_name = path.basename(path.abspath(__file__))[:-3]
|
||||
|
||||
|
||||
def main_function(module, event_data, dispatchers_steamid):
|
||||
action = event_data[1].get("action", None)
|
||||
event_data[1]["action_identifier"] = action_name
|
||||
|
||||
if action == "show_options":
|
||||
current_view = "options"
|
||||
elif action == "show_frontend":
|
||||
current_view = "frontend"
|
||||
elif action == "show_test":
|
||||
current_view = "test"
|
||||
else:
|
||||
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
|
||||
return
|
||||
|
||||
module.set_current_view(dispatchers_steamid, {
|
||||
"current_view": current_view
|
||||
})
|
||||
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
|
||||
|
||||
|
||||
def callback_success(module, event_data, dispatchers_steamid, match=None):
|
||||
pass
|
||||
|
||||
|
||||
def callback_fail(module, event_data, dispatchers_steamid):
|
||||
pass
|
||||
|
||||
|
||||
action_meta = {
|
||||
"description": "Shows information about Telnet",
|
||||
"main_function": main_function,
|
||||
"callback_success": callback_success,
|
||||
"callback_fail": callback_fail,
|
||||
"requires_telnet_connection": False,
|
||||
"enabled": True
|
||||
}
|
||||
|
||||
loaded_modules_dict["module_" + module_name].register_action(action_name, action_meta)
|
||||
56
bot/modules/telnet/templates/jinja2_macros.html
Normal file
56
bot/modules/telnet/templates/jinja2_macros.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{%- macro construct_toggle_link(bool, active_text, deactivate_event, inactive_text, activate_event) -%}
|
||||
{%- set bool = bool|default(false) -%}
|
||||
{%- set active_text = active_text|default(none) -%}
|
||||
{%- set deactivate_event = deactivate_event|default(none) -%}
|
||||
{%- set inactive_text = inactive_text|default(none) -%}
|
||||
{%- set activate_event = activate_event|default(none) -%}
|
||||
{%- if bool == true -%}
|
||||
{%- if deactivate_event != none and activate_event != none -%}
|
||||
<span class="active"><a href="#" onclick="window.socket.emit('{{ deactivate_event[0] }}', {{ deactivate_event[1] }}); return false;">{{ active_text }}</a></span>
|
||||
{%- elif deactivate_event != none and activate_event == none -%}
|
||||
<span class="active"><a href="#" onclick="window.socket.emit('{{ deactivate_event[0] }}', {{ deactivate_event[1] }}); return false;">{{ active_text }}</a></span>
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
{%- if deactivate_event != none and activate_event != none -%}
|
||||
<span class="inactive"><a href="#" onclick="window.socket.emit('{{ activate_event[0] }}', {{ activate_event[1] }}); return false;">{{ inactive_text }}</a></span>
|
||||
{%- elif deactivate_event != none and activate_event == none -%}
|
||||
<span class="inactive"><a href="#" onclick="window.socket.emit('{{ deactivate_event[0] }}', {{ deactivate_event[1] }}); return false;">{{ active_text }}</a></span>
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro construct_view_menu(views, current_view, module_name, steamid, default_view='frontend') -%}
|
||||
{#
|
||||
Dynamically construct a navigation menu for widget views.
|
||||
|
||||
Parameters:
|
||||
views: Dict of view configurations
|
||||
Example: {
|
||||
'frontend': {'label_active': 'back', 'label_inactive': 'main', 'action': 'show_frontend'},
|
||||
'options': {'label_active': 'back', 'label_inactive': 'options', 'action': 'show_options'}
|
||||
}
|
||||
current_view: Current active view name (string)
|
||||
module_name: Module name for socket.io event (e.g., 'telnet')
|
||||
steamid: User's steamid for action parameters
|
||||
default_view: View to return to when deactivating (default: 'frontend')
|
||||
#}
|
||||
{%- for view_id, config in views.items() -%}
|
||||
{%- if config.get('include_in_menu', True) -%}
|
||||
{%- set is_active = (current_view == view_id) -%}
|
||||
{%- set label_active = config.get('label_active', config.get('label', 'back')) -%}
|
||||
{%- set label_inactive = config.get('label_inactive', config.get('label', view_id)) -%}
|
||||
{%- set action = config.get('action', 'show_' ~ view_id) -%}
|
||||
{%- set default_action = config.get('default_action', 'show_' ~ default_view) -%}
|
||||
|
||||
<div>
|
||||
{{ construct_toggle_link(
|
||||
is_active,
|
||||
label_active,
|
||||
['widget_event', [module_name, ['toggle_' ~ module_name ~ '_widget_view', {'steamid': steamid, 'action': default_action}]]],
|
||||
label_inactive,
|
||||
['widget_event', [module_name, ['toggle_' ~ module_name ~ '_widget_view', {'steamid': steamid, 'action': action}]]]
|
||||
)}}
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endmacro -%}
|
||||
@@ -0,0 +1,9 @@
|
||||
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
|
||||
<div>
|
||||
{{ construct_toggle_link(
|
||||
options_view_toggle,
|
||||
"options", ['widget_event', ['telnet', ['toggle_telnet_widget_view', {'steamid': steamid, "action": "show_options"}]]],
|
||||
"back", ['widget_event', ['telnet', ['toggle_telnet_widget_view', {'steamid': steamid, "action": "show_frontend"}]]]
|
||||
)}}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<div id="telnet_table_widget_options_toggle" class="pull_out right">
|
||||
{{ control_switch_options_view }}
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
{%- from 'jinja2_macros.html' import construct_view_menu with context -%}
|
||||
<div id="telnet_table_widget_options_toggle" class="pull_out right">
|
||||
{{ construct_view_menu(
|
||||
views=views,
|
||||
current_view=current_view,
|
||||
module_name='telnet',
|
||||
steamid=steamid,
|
||||
default_view='frontend'
|
||||
)}}
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<tr{% if css_class != None %} class="{{ css_class }}"{% endif %}><td>{{ log_line }}</td></tr>
|
||||
@@ -0,0 +1,3 @@
|
||||
<tr>
|
||||
<th>Log</th>
|
||||
</tr>
|
||||
@@ -0,0 +1,3 @@
|
||||
<tr>
|
||||
<th>Log</th>
|
||||
</tr>
|
||||
@@ -0,0 +1,27 @@
|
||||
<header>
|
||||
<div>
|
||||
<span>Telnet Log</span>
|
||||
</div>
|
||||
</header>
|
||||
<aside>
|
||||
{{ options_toggle }}
|
||||
</aside>
|
||||
<main>
|
||||
<table>
|
||||
<caption>
|
||||
<span class="log_line">standard message</span>
|
||||
<span class="game_chat">player chat</span>
|
||||
<span class="player_logged">login / logout</span>
|
||||
<span class="bot_command">bot command</span>
|
||||
</caption>
|
||||
<thead>
|
||||
{{ table_header }}
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>{{ log_lines }}</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
{{ table_footer }}
|
||||
</tfoot>
|
||||
</table>
|
||||
</main>
|
||||
@@ -0,0 +1,27 @@
|
||||
<header>
|
||||
<div>
|
||||
<span>Telnet Log</span>
|
||||
</div>
|
||||
</header>
|
||||
<aside>
|
||||
{{ options_toggle }}
|
||||
</aside>
|
||||
<main>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">Telnet Log Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th colspan="2"><span>widget-options</span></th>
|
||||
</tr>
|
||||
{% for key, value in widget_options.items() %}
|
||||
<tr>
|
||||
<td><span>{{key}}</span></td><td>{{value}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
@@ -0,0 +1,22 @@
|
||||
<header>
|
||||
<div>
|
||||
<span>Telnet Log</span>
|
||||
</div>
|
||||
</header>
|
||||
<aside>
|
||||
{{ options_toggle }}
|
||||
</aside>
|
||||
<main>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">Test View</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2"><span>Test</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
36
bot/modules/telnet/triggers/server_time.py
Normal file
36
bot/modules/telnet/triggers/server_time.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from bot import loaded_modules_dict
|
||||
from bot import telnet_prefixes
|
||||
from os import path, pardir
|
||||
|
||||
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
|
||||
trigger_name = path.basename(path.abspath(__file__))[:-3]
|
||||
|
||||
|
||||
def main_function(origin_module, module, regex_result):
|
||||
datetime = regex_result.group("datetime")
|
||||
last_recorded_datetime = module.dom.data.get("module_telnet", {}).get("last_recorded_servertime", "")
|
||||
executed_trigger = False
|
||||
if datetime is not None:
|
||||
executed_trigger = True
|
||||
|
||||
if all([
|
||||
executed_trigger is True,
|
||||
datetime > last_recorded_datetime
|
||||
]):
|
||||
module.dom.data.upsert({
|
||||
"module_telnet": {
|
||||
"last_recorded_servertime": datetime
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
trigger_meta = {
|
||||
"description": "DISABLED: Modern 7D2D (V 2.x+) no longer includes timestamps in telnet output",
|
||||
"main_function": main_function,
|
||||
"triggers": [
|
||||
# Disabled: Modern 7D2D servers no longer include datetime/stardate in telnet responses
|
||||
# This trigger is obsolete for modern server versions
|
||||
]
|
||||
}
|
||||
|
||||
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)
|
||||
233
bot/modules/telnet/widgets/telnet_log_widget.py
Normal file
233
bot/modules/telnet/widgets/telnet_log_widget.py
Normal file
@@ -0,0 +1,233 @@
|
||||
from bot import loaded_modules_dict
|
||||
from os import path, pardir
|
||||
|
||||
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
|
||||
widget_name = path.basename(path.abspath(__file__))[:-3]
|
||||
|
||||
|
||||
# View Registry (mirrors locations menu pattern, but only one visible button here)
|
||||
VIEW_REGISTRY = {
|
||||
'frontend': {
|
||||
'label_active': 'back',
|
||||
'label_inactive': 'main',
|
||||
'action': 'show_frontend',
|
||||
'include_in_menu': False
|
||||
},
|
||||
'options': {
|
||||
'label_active': 'back',
|
||||
'label_inactive': 'options',
|
||||
'action': 'show_options',
|
||||
'include_in_menu': True
|
||||
},
|
||||
'test': {
|
||||
'label_active': 'back',
|
||||
'label_inactive': 'test',
|
||||
'action': 'show_test',
|
||||
'include_in_menu': True
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_log_line_css_class(log_line):
|
||||
css_classes = [
|
||||
"log_line"
|
||||
]
|
||||
|
||||
if r"INF Chat" in log_line:
|
||||
css_classes.append("game_chat")
|
||||
if r"(BCM) Command from" in log_line:
|
||||
css_classes.append("bot_command")
|
||||
if any([
|
||||
r"joined the game" in log_line,
|
||||
r"left the game" in log_line
|
||||
]):
|
||||
css_classes.append("player_logged")
|
||||
|
||||
return " ".join(css_classes)
|
||||
|
||||
|
||||
def select_view(*args, **kwargs):
|
||||
module = args[0]
|
||||
dispatchers_steamid = kwargs.get('dispatchers_steamid', None)
|
||||
current_view = module.get_current_view(dispatchers_steamid)
|
||||
if current_view == "options":
|
||||
options_view(module, dispatchers_steamid=dispatchers_steamid)
|
||||
elif current_view == "test":
|
||||
test_view(module, dispatchers_steamid=dispatchers_steamid)
|
||||
else:
|
||||
frontend_view(module, dispatchers_steamid=dispatchers_steamid)
|
||||
|
||||
|
||||
def frontend_view(*args, **kwargs):
|
||||
module = args[0]
|
||||
dispatchers_steamid = kwargs.get("dispatchers_steamid", None)
|
||||
|
||||
telnet_log_frontend = module.templates.get_template('telnet_log_widget/view_frontend.html')
|
||||
template_table_header = module.templates.get_template('telnet_log_widget/table_header.html')
|
||||
log_line = module.templates.get_template('telnet_log_widget/log_line.html')
|
||||
|
||||
# new view menu (pattern from locations module)
|
||||
template_view_menu = module.templates.get_template('telnet_log_widget/control_view_menu.html')
|
||||
|
||||
if len(module.webserver.connected_clients) >= 1:
|
||||
telnet_lines = module.dom.data.get("module_telnet", {}).get("telnet_lines", {})
|
||||
if len(telnet_lines) >= 1:
|
||||
# Build log lines efficiently using list comprehension
|
||||
log_lines_list = []
|
||||
for line in reversed(telnet_lines):
|
||||
css_class = get_log_line_css_class(line)
|
||||
log_lines_list.append(module.template_render_hook(
|
||||
module,
|
||||
template=log_line,
|
||||
log_line=line,
|
||||
css_class=css_class
|
||||
))
|
||||
log_lines = ''.join(log_lines_list)
|
||||
|
||||
current_view = module.get_current_view(dispatchers_steamid)
|
||||
options_toggle = module.template_render_hook(
|
||||
module,
|
||||
template=template_view_menu,
|
||||
views=VIEW_REGISTRY,
|
||||
current_view=current_view,
|
||||
steamid=dispatchers_steamid
|
||||
)
|
||||
data_to_emit = module.template_render_hook(
|
||||
module,
|
||||
template=telnet_log_frontend,
|
||||
options_toggle=options_toggle,
|
||||
log_lines=log_lines,
|
||||
table_header=module.template_render_hook(
|
||||
module,
|
||||
template=template_table_header
|
||||
)
|
||||
)
|
||||
module.webserver.send_data_to_client_hook(
|
||||
module,
|
||||
payload=data_to_emit,
|
||||
data_type="widget_content",
|
||||
clients=[dispatchers_steamid],
|
||||
method="update",
|
||||
target_element={
|
||||
"id": "telnet_log_widget",
|
||||
"type": "table",
|
||||
"selector": "body > main > div"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def options_view(*args, **kwargs):
|
||||
module = args[0]
|
||||
dispatchers_steamid = kwargs.get('dispatchers_steamid', None)
|
||||
|
||||
template_frontend = module.templates.get_template('telnet_log_widget/view_options.html')
|
||||
template_view_menu = module.templates.get_template('telnet_log_widget/control_view_menu.html')
|
||||
|
||||
current_view = module.get_current_view(dispatchers_steamid)
|
||||
options_toggle = module.template_render_hook(
|
||||
module,
|
||||
template=template_view_menu,
|
||||
views=VIEW_REGISTRY,
|
||||
current_view=current_view,
|
||||
steamid=dispatchers_steamid
|
||||
)
|
||||
|
||||
data_to_emit = module.template_render_hook(
|
||||
module,
|
||||
template=template_frontend,
|
||||
options_toggle=options_toggle,
|
||||
widget_options=module.options
|
||||
)
|
||||
|
||||
module.webserver.send_data_to_client_hook(
|
||||
module,
|
||||
payload=data_to_emit,
|
||||
data_type="widget_content",
|
||||
clients=[dispatchers_steamid],
|
||||
method="update",
|
||||
target_element={
|
||||
"id": "telnet_log_widget",
|
||||
"type": "table",
|
||||
"selector": "body > main > div"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_view(*args, **kwargs):
|
||||
module = args[0]
|
||||
dispatchers_steamid = kwargs.get('dispatchers_steamid', None)
|
||||
|
||||
template_test = module.templates.get_template('telnet_log_widget/view_test.html')
|
||||
template_view_menu = module.templates.get_template('telnet_log_widget/control_view_menu.html')
|
||||
|
||||
current_view = module.get_current_view(dispatchers_steamid)
|
||||
options_toggle = module.template_render_hook(
|
||||
module,
|
||||
template=template_view_menu,
|
||||
views=VIEW_REGISTRY,
|
||||
current_view=current_view,
|
||||
steamid=dispatchers_steamid
|
||||
)
|
||||
|
||||
data_to_emit = module.template_render_hook(
|
||||
module,
|
||||
template=template_test,
|
||||
options_toggle=options_toggle
|
||||
)
|
||||
|
||||
module.webserver.send_data_to_client_hook(
|
||||
module,
|
||||
payload=data_to_emit,
|
||||
data_type="widget_content",
|
||||
clients=[dispatchers_steamid],
|
||||
method="update",
|
||||
target_element={
|
||||
"id": "telnet_log_widget",
|
||||
"type": "table",
|
||||
"selector": "body > main > div"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def update_widget(*args, **kwargs):
|
||||
module = args[0]
|
||||
updated_values_dict = kwargs.get("updated_values_dict", None)
|
||||
|
||||
# Iterate directly over connected clients (no need to convert to list)
|
||||
for clientid in module.webserver.connected_clients.keys():
|
||||
current_view = module.get_current_view(clientid)
|
||||
if current_view == "frontend":
|
||||
telnet_log_line = module.templates.get_template('telnet_log_widget/log_line.html')
|
||||
css_class = get_log_line_css_class(updated_values_dict["telnet_lines"])
|
||||
data_to_emit = module.template_render_hook(
|
||||
module,
|
||||
template=telnet_log_line,
|
||||
log_line=updated_values_dict["telnet_lines"],
|
||||
css_class=css_class
|
||||
)
|
||||
|
||||
module.webserver.send_data_to_client_hook(
|
||||
module,
|
||||
method="prepend",
|
||||
data_type="widget_content",
|
||||
payload=data_to_emit,
|
||||
clients=[clientid],
|
||||
target_element={
|
||||
"id": "telnet_log_widget",
|
||||
"type": "table",
|
||||
"selector": "body > main > div"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
widget_meta = {
|
||||
"description": "displays a bunch of telnet lines, updating in real time",
|
||||
"main_widget": select_view,
|
||||
"handlers": {
|
||||
"module_telnet/visibility/%steamid%/current_view": select_view,
|
||||
"module_telnet/telnet_lines": update_widget,
|
||||
},
|
||||
"enabled": True
|
||||
}
|
||||
|
||||
loaded_modules_dict["module_" + module_name].register_widget(widget_name, widget_meta)
|
||||
Reference in New Issue
Block a user