Source code for evennia.server.portal.wire_formats.base

"""
Base wire format interface for WebSocket subprotocol codecs.

All wire format implementations must subclass WireFormat and implement
the encoding/decoding methods. Each format represents a specific
WebSocket subprotocol as defined by RFC 6455 Sec-WebSocket-Protocol
negotiation.
"""

import re

from django.conf import settings

from evennia.utils.ansi import parse_ansi

_RE_SCREENREADER_REGEX = re.compile(
    r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE
)
_RE_N = re.compile(r"\|n$")


[docs] class WireFormat: """ Abstract base class for WebSocket wire format codecs. A wire format handles the translation between Evennia's internal message representation and the bytes sent over the WebSocket connection. Each subclass corresponds to a specific WebSocket subprotocol name (e.g., "v1.evennia.com", "json.mudstandards.org"). Attributes: name (str): The subprotocol identifier string, used in Sec-WebSocket-Protocol negotiation. supports_oob (bool): Whether this format supports out-of-band data (structured commands beyond plain text). """ name = None supports_oob = True @staticmethod def _extract_text_and_flags(args, kwargs, protocol_flags): """ Extract text string and display flags from encode arguments. This is a shared helper for encode_text/encode_prompt in formats that use raw ANSI output (terminal, json, gmcp). The EvenniaV1 format has its own logic (HTML conversion, raw mode) and does not use this helper. Args: args (tuple): Positional args passed to encode_text/encode_prompt. args[0] should be the text string. kwargs (dict): Keyword args. The "options" key is popped and inspected for "raw", "nocolor" and "screenreader" overrides. protocol_flags (dict or None): Session protocol flags. Returns: tuple or None: (text, raw, nocolor, screenreader) if text is valid, or None if there is no text to encode. """ if args: text = args[0] if text is None: return None else: return None flags = protocol_flags or {} options = kwargs.pop("options", {}) raw = options.get("raw", flags.get("RAW", False)) nocolor = options.get("nocolor", flags.get("NOCOLOR", False)) screenreader = options.get("screenreader", flags.get("SCREENREADER", False)) return (text, raw, nocolor, screenreader) @staticmethod def _process_ansi(text, raw, nocolor, screenreader): """ Process Evennia ANSI markup into terminal escape sequences. Applies screenreader stripping, nocolor stripping, or full ANSI conversion depending on the flags. This is the shared logic for all non-HTML wire formats (terminal, json, gmcp). When raw is True, text is returned unmodified (no ANSI processing). For non-raw output, a trailing reset (|n) is appended to prevent color/attribute bleed into subsequent output, mirroring the TelnetProtocol behavior. Args: text (str): Text with Evennia ANSI markup (|r, |n, etc.). raw (bool): If True, bypass all ANSI processing. nocolor (bool): If True, strip all ANSI codes. screenreader (bool): If True, strip ANSI and apply SCREENREADER_REGEX_STRIP. Returns: str: Processed text with real ANSI escape sequences, stripped text, or raw text. """ if raw: return text if screenreader: text = parse_ansi(text, strip_ansi=True, xterm256=False, mxp=False) text = _RE_SCREENREADER_REGEX.sub("", text) elif nocolor: text = parse_ansi(text, strip_ansi=True, xterm256=False, mxp=False) else: # Ensure ANSI state is reset at the end of the string to prevent # color/attribute bleed into subsequent output. This mirrors # TelnetProtocol/SSH behavior: strip any existing trailing |n, # then append ||n (preserving a literal trailing pipe via the || # escape) or |n as appropriate. text = _RE_N.sub("", text) + ("||n" if text.endswith("|") else "|n") text = parse_ansi(text, xterm256=True, mxp=False) return text
[docs] def decode_incoming(self, payload, is_binary, protocol_flags=None): """ Decode an incoming WebSocket message into kwargs for data_in(). Args: payload (bytes): Raw WebSocket message payload. is_binary (bool): True if this was a BINARY frame (opcode 2), False if it was a TEXT frame (opcode 1). protocol_flags (dict, optional): The session's protocol flags, which may affect decoding behavior. Returns: dict or None: A dict of kwargs to pass to session.data_in(), where each key is an inputfunc name and value is [args, kwargs]. Returns None if the message should be silently ignored. """ raise NotImplementedError(f"{self.__class__.__name__} must implement decode_incoming()")
[docs] def encode_text(self, *args, protocol_flags=None, **kwargs): """ Encode text output for sending to the client. This handles the "text" outputfunc — the primary game output. The default implementation processes ANSI markup and returns a UTF-8 encoded BINARY frame. Subclasses that need a different encoding (e.g. HTML conversion) should override this method. Args: *args: Text arguments. args[0] is typically the text string. protocol_flags (dict, optional): Session protocol flags that may affect encoding (e.g., NOCOLOR, SCREENREADER, RAW). **kwargs: Additional keyword arguments. May include an "options" dict with keys like "raw", "nocolor", "screenreader", "send_prompt". Returns: tuple or None: A (data_bytes, is_binary) tuple for sendMessage(), or None if nothing should be sent. """ extracted = self._extract_text_and_flags(args, kwargs, protocol_flags) if extracted is None: return None text, raw, nocolor, screenreader = extracted text = self._process_ansi(text, raw, nocolor, screenreader) return (text.encode("utf-8"), True)
[docs] def encode_prompt(self, *args, protocol_flags=None, **kwargs): """ Encode a prompt for sending to the client. Default implementation delegates to encode_text with the send_prompt option set. Args: *args: Prompt arguments. protocol_flags (dict, optional): Session protocol flags. **kwargs: Additional keyword arguments. May include an "options" dict; if absent, one is created with "send_prompt" set to True. Returns: tuple or None: A (data_bytes, is_binary) tuple for sendMessage(), or None if nothing should be sent. """ options = kwargs.get("options", {}) options["send_prompt"] = True kwargs["options"] = options return self.encode_text(*args, protocol_flags=protocol_flags, **kwargs)
[docs] def encode_default(self, cmdname, *args, protocol_flags=None, **kwargs): """ Encode a non-text OOB command for sending to the client. This handles all outputfuncs that don't have a specific send_* method, including custom OOB commands. Args: cmdname (str): The OOB command name. *args: Command arguments. protocol_flags (dict, optional): Session protocol flags. **kwargs: Additional keyword arguments. Returns: tuple or None: A (data_bytes, is_binary) tuple for sendMessage(), or None if nothing should be sent (e.g., if the format doesn't support OOB). """ raise NotImplementedError(f"{self.__class__.__name__} must implement encode_default()")