""" Shared utilities for FunguyBot plugins. """ import html import ipaddress import socket import logging from wcwidth import wcswidth logger = logging.getLogger(__name__) # Networks considered unsafe for outbound connections _PRIVATE_RANGES = [ ipaddress.ip_network('10.0.0.0/8'), ipaddress.ip_network('172.16.0.0/12'), ipaddress.ip_network('192.168.0.0/16'), ipaddress.ip_network('127.0.0.0/8'), ipaddress.ip_network('169.254.0.0/16'), ipaddress.ip_network('0.0.0.0/8'), ipaddress.ip_network('::1/128'), ipaddress.ip_network('fc00::/7'), ipaddress.ip_network('fe80::/10'), ipaddress.ip_network('::/128'), ] def html_escape(text: str) -> str: """Escape HTML special characters for safe embedding in messages.""" return html.escape(str(text), quote=False) def collapsible_summary(title: str, body: str, expanded: bool = False) -> str: """Wrap content in a collapsible HTML details block.""" open_attr = ' open' if expanded else '' return f"\n{title}\n{body}\n" def is_public_destination(target: str) -> bool: """ Returns True if `target` (hostname or IP) does NOT resolve to any private, loopback, or link‑local address. """ try: addr = ipaddress.ip_address(target) if any(addr in net for net in _PRIVATE_RANGES): return False return True except ValueError: pass try: addrinfo = socket.getaddrinfo(target, None) for _, _, _, _, sockaddr in addrinfo: ip = sockaddr[0] addr = ipaddress.ip_address(ip) if any(addr in net for net in _PRIVATE_RANGES): return False return True except Exception as e: logger.warning(f"Cannot resolve {target}: {e}") return False async def handle_command(room, message, bot, prefix, config): """No-op handler so the bot doesn't crash when loading this module as a plugin.""" pass async def send_html_message(bot, room_id, html_body, markdown_fallback): """Send an HTML-formatted message with a Markdown fallback. Args: bot: simplematrixbotlib.Bot instance room_id: Matrix room ID html_body: HTML string (table, etc.) markdown_fallback: Markdown/plain text for clients that don't render HTML """ content = { "msgtype": "m.text", "body": markdown_fallback, "format": "org.matrix.custom.html", "formatted_body": html_body } await bot.async_client.room_send( room_id=room_id, message_type="m.room.message", content=content ) def code_block(title: str, sections: list) -> str: """ Build a Markdown code block with perfectly aligned columns (emoji‑aware). Args: title: header line inside the code block sections: list of dicts with keys 'title' (str) and 'rows' rows is a list of (emoji, label, value) tuples Returns: Markdown string with triple backticks and aligned content. """ labelled = [] for sec in sections: for emoji, text, value in sec["rows"]: if text.strip() or emoji.strip(): labelled.append((emoji, text, value)) max_label_width = max((len(str(t)) for _, t, _ in labelled), default=0) emoji_widths = {} for emoji, _, _ in labelled: if emoji: w = wcswidth(emoji) or 1 emoji_widths[emoji] = w else: emoji_widths[emoji] = 0 max_emoji_width = max(emoji_widths.values()) if emoji_widths else 0 prefix_width = max_emoji_width + 1 + max_label_width + 3 # "E label : " separator = "=" * (prefix_width + 30) lines = [title, separator] for sec in sections: # Only print a section header if the title is not empty if sec["title"].strip(): lines.append("") lines.append(f"── {sec['title']} ──") for emoji, text, value in sec["rows"]: if text.strip() or emoji.strip(): if emoji: actual_w = emoji_widths.get(emoji, 0) pad = max_emoji_width - actual_w emoji_field = emoji + " " * pad else: emoji_field = " " * max_emoji_width padded_label = f"{text:<{max_label_width}}" lines.append(f"{emoji_field} {padded_label} : {value}") else: lines.append(f"{' ' * prefix_width}{value}") lines.append("") lines.append(separator) return "```\n" + "\n".join(lines) + "\n```"