249 lines
8.9 KiB
Python
249 lines
8.9 KiB
Python
#!/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 <CIDR> Show detailed info for a network
|
||
!subnet split <CIDR> --prefix <N> Split into smaller subnets (new prefix)
|
||
!subnet split <CIDR> --diff <N> Split by prefix delta
|
||
!subnet adjacent <CIDR> <count> 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
|