aboutsummaryrefslogtreecommitdiff
path: root/src/python
diff options
context:
space:
mode:
authorGravatar Sadie Powell2026-04-11 14:28:20 +0100
committerGravatar Sadie Powell2026-04-16 13:43:43 +0100
commit18b131dbc3928bea50433fa4bbc5a5323091deb0 (patch)
tree11a94af9d696f159ed13626d62b29e5fb842f22c /src/python
parentDelete the old module manager. (diff)
Add the new module manager.
Diffstat (limited to 'src/python')
-rw-r--r--src/python/console.py127
-rw-r--r--src/python/module.py80
2 files changed, 207 insertions, 0 deletions
diff --git a/src/python/console.py b/src/python/console.py
new file mode 100644
index 000000000..69e5c0abf
--- /dev/null
+++ b/src/python/console.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+#
+# InspIRCd -- Internet Relay Chat Daemon
+#
+# Copyright (C) 2026 Sadie Powell <sadie@witchery.services>
+#
+# This file is part of InspIRCd. InspIRCd is free software: you can
+# redistribute it and/or modify it under the terms of the GNU General Public
+# License as published by the Free Software Foundation, version 2.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+
+import os
+import re
+import sys
+
+DEBUG = int(os.getenv("INSPIRCD_DEBUG", "0")) > 0
+RESET = "\x1B[0m" if sys.stdout.isatty() else ""
+BOLD = "\x1B[1m" if sys.stdout.isatty() else ""
+BOLD_RED = "\x1B[1;31m" if sys.stdout.isatty() else ""
+BOLD_GREEN = "\x1B[1;32m" if sys.stdout.isatty() else ""
+BOLD_YELLOW = "\x1B[1;33m" if sys.stdout.isatty() else ""
+FAINT = "\x1B[2m" if sys.stdout.isatty() else ""
+
+
+# Measures the length of a str with the ANSI escape codes removed.
+def ansi_len(text):
+ return len(re.sub(r"\x1b\[[0-9;]*m", "", text))
+
+
+# Wraps some text with console colored text.
+def color(text, color):
+ return "".join([color, text, RESET])
+
+
+# Prints a debug message to the standard output stream if INSPIRCD_DEBUG is set.
+def debug(message):
+ if DEBUG:
+ print(f"{BOLD}!!!{RESET} {message}", file=sys.stderr)
+
+
+# Prints an error message to the standard error stream and exits.
+def error(*messages):
+ print(f"{BOLD_RED}Error:{RESET} ", end="", file=sys.stderr)
+ for message in messages:
+ print(message, file=sys.stderr)
+ sys.exit(1)
+
+
+# Dispatches commands to their handler method.
+def subcommand(args, start, commands, default="help"):
+ commands["help"] = {
+ "function": subcommand_help,
+ "help": "Show this message and exit",
+ }
+
+ command = args[start].lower() if len(args) > start else default
+ if command in commands: # The user specified an exact match.
+ command_match = command
+ else: # Also allow users to specify a partial command for convenience.
+ command_matches = [c for c in commands if c.startswith(command)]
+ command_match = command_matches[0] if len(command_matches) == 1 else None
+
+ if not command_match:
+ prefix = " ".join(sys.argv[0:start])
+ error(
+ f"{command} is not a recognised subcommand.",
+ f"Use `{prefix} help` for a list of subcommands.",
+ )
+
+ debug(f"Command match: {command} => {command_match}")
+ command_data = commands[command_match]
+ return command_data["function"](commands, args, start + 1)
+
+
+# Prints the help output for a subcommand.
+def subcommand_help(commands, args, start):
+ print(f"{BOLD}Usage{RESET}: {args[0]} <command> [options...]")
+ print()
+ print("Commands:")
+
+ command_end = start - (1 if len(args) >= start else 0)
+ command_prefix = "".join([f"{a} " for a in args[1:command_end]])
+
+ help_entries = {}
+ for command_name, command in commands.items():
+ if "help" not in command:
+ continue
+
+ help_entries[f"{command_prefix}{command_name}"] = command["help"]
+ option_padding = " " * len(command_prefix)
+ for option_name, option_help in command.get("options", {}).items():
+ help_entries[f"{option_padding}{command_name} {option_name}"] = option_help
+
+ help_padding = max(len(e) for e in help_entries)
+ for help_name, help_message in help_entries.items():
+ print(f" {help_name:<{help_padding}} {help_message}")
+
+
+# Formats and prints a table to the console.
+def table(headers, rows, separator="|"):
+ if headers is not None:
+ rows.insert(0, ["-" * ansi_len(header) for header in headers])
+ rows.insert(0, [color(header, BOLD) for header in headers])
+
+ widths = [max(ansi_len(field) for field in col) for col in zip(*rows)]
+ for row in rows:
+ padded_row = []
+ for i, item in enumerate(row):
+ padding = " " * (widths[i] - ansi_len(item))
+ padded_row.append(f"{item}{padding}")
+ print(f" {separator} ".join(padded_row).strip())
+
+
+# Prints a warning message to the standard error stream.
+def warning(*messages):
+ print(f"{BOLD_YELLOW}Warning:{RESET} ", end="", file=sys.stderr)
+ for message in messages:
+ print(message, file=sys.stderr)
diff --git a/src/python/module.py b/src/python/module.py
new file mode 100644
index 000000000..a68947785
--- /dev/null
+++ b/src/python/module.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+#
+# InspIRCd -- Internet Relay Chat Daemon
+#
+# Copyright (C) 2026 Sadie Powell <sadie@witchery.services>
+#
+# This file is part of InspIRCd. InspIRCd is free software: you can
+# redistribute it and/or modify it under the terms of the GNU General Public
+# License as published by the Free Software Foundation, version 2.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+
+import pathlib
+
+DIR = pathlib.Path(__file__).resolve().parents[2] / "modules"
+CONTRIB_DIR = DIR / "contrib"
+EXTRA_DIR = DIR / "extra"
+
+
+# Converts a module name to a file name.
+def file_name(module):
+ return pathlib.Path(module).with_suffix(".cpp").name
+
+
+# Convert a module file name or path to a short name.
+def short_name(module):
+ return pathlib.Path(module).stem
+
+
+# Compares two module versions and returns -1 if older, 0 if equal, or 1 if newer.
+def version_compare(lhs, rhs):
+ for lhs, rhs in zip(lhs, rhs):
+ if lhs is None or rhs is None:
+ break
+ elif lhs < rhs:
+ return -1
+ elif lhs > rhs:
+ return 1
+ return 0
+
+
+# Checks whether a module version matches a requirement range.
+def version_in_range(version, requirement):
+ if not version:
+ return requirement
+
+ min_requirement, max_requirement = requirement
+ if version_compare(min_requirement, version) > 0:
+ return False
+ elif version_compare(version, max_requirement) > 0:
+ return False
+
+ return True
+
+
+# Converts a module version string to a tuple of three elements.
+def version_parse(version):
+ if not version:
+ return [None, None, None]
+
+ segments = [int(s) for s in version.split(".") if s.isdigit()]
+ return tuple((segments + [None, None, None])[:3])
+
+
+# Converts a tuple of three elements to a module version string.
+def version_string(segments):
+ return ".".join(str(segment) for segment in segments if segment is not None)
+
+
+# Converts a two tuples of three elements to a module version range.
+def version_range_string(range):
+ return "-".join(version_string(version) for version in set(range))