Files
FunguyBot/plugins/subnet.py
T

249 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
plugins/subnet.py Subnet calculator and network splitting plugin for Funguy Bot.
Commands:
!subnet info <CIDR>
!subnet split <CIDR> --prefix <N>
!subnet split <CIDR> --diff <N>
!subnet adjacent <CIDR> <count>
!subnet help
Output is a clean code block with emojis and perfectly aligned columns.
"""
import ipaddress
import simplematrixbotlib as botlib
from plugins.common import collapsible_summary, html_escape, code_block
# -------------------------------------------------------------------
# Helper functions (synchronous)
# -------------------------------------------------------------------
def _fmt_subnet_info_rows(net):
"""Return list of (emoji, label, value) tuples."""
nw = net.network_address
bc = net.broadcast_address if hasattr(net, "broadcast_address") else None
total = net.num_addresses
if net.version == 4:
if net.prefixlen == 32:
usable_count = 1
first = last = nw
elif net.prefixlen == 31:
usable_count = 2
first = nw
last = bc
else:
usable_count = max(0, total - 2)
first = nw + 1 if usable_count > 0 else None
last = bc - 1 if usable_count > 0 else None
else:
hosts_iter = net.hosts()
try:
first = next(hosts_iter)
last = net.network_address + (total - 1)
usable_count = total
except StopIteration:
first = last = None
usable_count = 0
rows = [
("🌐", "CIDR", str(net.with_prefixlen)),
("📡", "Network", str(nw)),
("📢", "Broadcast", str(bc) if bc is not None else "N/A"),
("🧱", "Netmask", str(net.netmask) if hasattr(net, "netmask") else "N/A"),
("🕳️", "Wildcard Mask", str(net.hostmask) if hasattr(net, "hostmask") else "N/A"),
("🔢", "Total IPs", str(total)),
("👥", "Usable Hosts", str(usable_count)),
]
if first is not None and last is not None:
rows.append(("🏁", "First Usable", str(first)))
rows.append(("🏁", "Last Usable", str(last)))
rows.append(("↔️", "Usable Range", f"{first} - {last}"))
return rows
def _split_by_prefix(net, new_prefix):
if new_prefix < net.prefixlen:
return None
return list(net.subnets(new_prefix=new_prefix))
def _split_by_diff(net, diff):
return _split_by_prefix(net, net.prefixlen + diff)
def _adjacent_networks(net, count):
nets = [net]
current = net
for _ in range(count):
try:
next_addr = current.network_address + current.num_addresses
current = ipaddress.ip_network(f"{next_addr}/{current.prefixlen}", strict=True)
nets.append(current)
except (ValueError, ipaddress.AddressValueError):
break
return nets
# -------------------------------------------------------------------
# Output builders (each returns a collapsible Markdown message)
# -------------------------------------------------------------------
def _info_output(net):
"""Build a collapsible message for a single subnet."""
title = f"🔍 Subnet {net.with_prefixlen}"
rows = _fmt_subnet_info_rows(net)
block = code_block(title, [{"title": "", "rows": rows}])
return collapsible_summary(title, block)
def _split_output(networks):
"""Build a collapsible message for a split operation."""
total = len(networks)
title = f"🔀 Split into {total} subnets"
sections = []
for i, sub in enumerate(networks, 1):
rows = _fmt_subnet_info_rows(sub)
sections.append({"title": f"Subnet {sub.with_prefixlen}", "rows": rows})
block = code_block(title, sections)
return collapsible_summary(title, block)
def _adjacent_output(networks):
"""Build a collapsible message for adjacent networks."""
base = networks[0]
title = f"📐 Adjacent networks (base {base.with_prefixlen})"
sections = []
for i, net in enumerate(networks):
label = "Base network" if i == 0 else f"Adjacent #{i}"
rows = _fmt_subnet_info_rows(net)
sections.append({"title": label, "rows": rows})
block = code_block(title, sections)
return collapsible_summary(title, block)
# -------------------------------------------------------------------
# Help
# -------------------------------------------------------------------
_HELP_MD = """
<details>
<summary><strong>!subnet</strong> Subnet calculator and exploration</summary>
<pre>
!subnet info &lt;CIDR&gt; Show detailed info for a network
!subnet split &lt;CIDR&gt; --prefix &lt;N&gt; Split into smaller subnets (new prefix)
!subnet split &lt;CIDR&gt; --diff &lt;N&gt; Split by prefix delta
!subnet adjacent &lt;CIDR&gt; &lt;count&gt; Show current and adjacent networks
</pre>
<p>Example: <code>!subnet info 192.168.1.0/24</code></p>
<ul>
<li>IPv4 /31 and /32 networks show both addresses as usable (RFC 3021).</li>
<li>IPv6 networks list all addresses as hosts (no broadcast).</li>
</ul>
</details>
"""
# -------------------------------------------------------------------
# Command handler
# -------------------------------------------------------------------
async def handle_command(room, message, bot, prefix, config):
match = botlib.MessageMatch(room, message, bot, prefix)
if not (match.is_not_from_this_bot() and match.prefix() and match.command("subnet")):
return
args = match.args()
if not args:
await bot.api.send_text_message(room.room_id, "Usage: !subnet <info|split|adjacent> ...\n !subnet help")
return
subcmd = args[0].lower()
if subcmd in ("help", "-h", "--help"):
await bot.api.send_markdown_message(room.room_id, _HELP_MD)
return
if subcmd == "info" or "/" in subcmd:
cidr = args[1] if subcmd == "info" else subcmd
try:
net = ipaddress.ip_network(cidr, strict=False)
except ValueError as e:
await bot.api.send_text_message(room.room_id, f"[!] Invalid CIDR: {e}")
return
output = _info_output(net)
await bot.api.send_markdown_message(room.room_id, output)
return
if subcmd == "split":
if len(args) < 2:
await bot.api.send_text_message(room.room_id, "Usage: !subnet split <CIDR> --prefix <N> OR !subnet split <CIDR> --diff <delta>")
return
cidr = args[1]
try:
net = ipaddress.ip_network(cidr, strict=False)
except ValueError as e:
await bot.api.send_text_message(room.room_id, f"[!] Invalid CIDR: {e}")
return
if "--prefix" in args:
try:
idx = args.index("--prefix")
new_prefix = int(args[idx + 1])
except (ValueError, IndexError):
await bot.api.send_text_message(room.room_id, "Usage: !subnet split <CIDR> --prefix <number>")
return
subnets = _split_by_prefix(net, new_prefix)
elif "--diff" in args:
try:
idx = args.index("--diff")
diff = int(args[idx + 1])
except (ValueError, IndexError):
await bot.api.send_text_message(room.room_id, "Usage: !subnet split <CIDR> --diff <delta>")
return
subnets = _split_by_diff(net, diff)
else:
await bot.api.send_text_message(room.room_id, "You must provide --prefix <N> or --diff <N> for split.")
return
if subnets is None:
await bot.api.send_text_message(room.room_id, f"[!] New prefix /{new_prefix} is smaller than current prefix /{net.prefixlen}. Cannot split.")
return
output = _split_output(subnets)
await bot.api.send_markdown_message(room.room_id, output)
return
if subcmd == "adjacent":
if len(args) < 3:
await bot.api.send_text_message(room.room_id, "Usage: !subnet adjacent <CIDR> <count>")
return
cidr = args[1]
try:
net = ipaddress.ip_network(cidr, strict=False)
except ValueError as e:
await bot.api.send_text_message(room.room_id, f"[!] Invalid CIDR: {e}")
return
try:
count = int(args[2])
except ValueError:
await bot.api.send_text_message(room.room_id, "Count must be an integer.")
return
networks = _adjacent_networks(net, count)
output = _adjacent_output(networks)
await bot.api.send_markdown_message(room.room_id, output)
return
await bot.api.send_text_message(room.room_id, f"Unknown subcommand '{subcmd}'. Use !subnet help.")
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.3.2"
__author__ = "Funguy Bot"
__description__ = "Subnet calculator"
__help__ = _HELP_MD