Files

139 lines
4.6 KiB
Python
Raw Permalink 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.
"""
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"<details{open_attr}>\n<summary><strong>{title}</strong></summary>\n{body}\n</details>"
def is_public_destination(target: str) -> bool:
"""
Returns True if `target` (hostname or IP) does NOT resolve to any
private, loopback, or linklocal 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 (emojiaware).
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```"